Add hardcore foreplay action pool

This commit is contained in:
2026-06-26 12:02:26 +02:00
parent 20483ca019
commit 22df4f754f
5 changed files with 290 additions and 6 deletions
+4
View File
@@ -265,6 +265,7 @@ NODE_INPUT_TOOLTIPS = {
"allow_toys": "Allow toy/strap-on wording in hardcore actions.", "allow_toys": "Allow toy/strap-on wording in hardcore actions.",
"allow_double": "Allow double-penetration or second-contact wording.", "allow_double": "Allow double-penetration or second-contact wording.",
"allow_penetration": "Allow vaginal/penetrative sex subcategories.", "allow_penetration": "Allow vaginal/penetrative sex subcategories.",
"allow_foreplay": "Allow hardcore teasing/foreplay setup actions such as kissing, caressing, breast/face touching, and undressing.",
"allow_oral": "Allow oral sex subcategories.", "allow_oral": "Allow oral sex subcategories.",
"allow_outercourse": "Allow non-penetrative penis-contact acts such as boobjob/titjob, footjob, penis licking, and testicle sucking.", "allow_outercourse": "Allow non-penetrative penis-contact acts such as boobjob/titjob, footjob, penis licking, and testicle sucking.",
"allow_anal": "Allow anal subcategories.", "allow_anal": "Allow anal subcategories.",
@@ -1939,6 +1940,7 @@ class SxCPHardcoreActionFilter:
"allow_toys": ("BOOLEAN", {"default": False}), "allow_toys": ("BOOLEAN", {"default": False}),
"allow_double": ("BOOLEAN", {"default": False}), "allow_double": ("BOOLEAN", {"default": False}),
"allow_penetration": ("BOOLEAN", {"default": True}), "allow_penetration": ("BOOLEAN", {"default": True}),
"allow_foreplay": ("BOOLEAN", {"default": True}),
"allow_oral": ("BOOLEAN", {"default": True}), "allow_oral": ("BOOLEAN", {"default": True}),
"allow_outercourse": ("BOOLEAN", {"default": True}), "allow_outercourse": ("BOOLEAN", {"default": True}),
"allow_anal": ("BOOLEAN", {"default": True}), "allow_anal": ("BOOLEAN", {"default": True}),
@@ -1960,6 +1962,7 @@ class SxCPHardcoreActionFilter:
allow_toys, allow_toys,
allow_double, allow_double,
allow_penetration, allow_penetration,
allow_foreplay,
allow_oral, allow_oral,
allow_outercourse, allow_outercourse,
allow_anal, allow_anal,
@@ -1972,6 +1975,7 @@ class SxCPHardcoreActionFilter:
allow_toys=allow_toys, allow_toys=allow_toys,
allow_double=allow_double, allow_double=allow_double,
allow_penetration=allow_penetration, allow_penetration=allow_penetration,
allow_foreplay=allow_foreplay,
allow_oral=allow_oral, allow_oral=allow_oral,
allow_outercourse=allow_outercourse, allow_outercourse=allow_outercourse,
allow_anal=allow_anal, allow_anal=allow_anal,
@@ -195,6 +195,22 @@
"intense shameless eye contact", "intense shameless eye contact",
"spent satisfied expression" "spent satisfied expression"
], ],
"hardcore_foreplay_expressions": [
"heavy-lidded teasing stare",
"breathless close-range expression",
"soft moan while being touched",
"heated kissing expression",
"hungry eye contact during undressing",
"parted lips before a kiss",
"flushed anticipation",
"controlled teasing smile",
"dazed aroused stare",
"eyes half-closed during caressing",
"mouth open against a kiss",
"shameless pre-sex grin",
"focused stare while clothing is pulled aside",
"close-range lustful eye contact"
],
"hardcore_penetration_expressions": [ "hardcore_penetration_expressions": [
"controlled eye contact during penetration", "controlled eye contact during penetration",
"focused adult pleasure face during thrusting", "focused adult pleasure face during thrusting",
@@ -455,6 +471,18 @@
"dramatic overhead studio crop", "dramatic overhead studio crop",
"tight vertical frame with props and body centered" "tight vertical frame with props and body centered"
], ],
"foreplay_compositions": [
{"text": "close body-contact frame centered on kissing, hands, and exposed skin", "min_people": 2, "max_people": 3},
{"text": "standing close-range foreplay frame with faces and hand placement readable", "min_people": 2, "max_people": 3},
{"text": "bed-edge undressing composition with clothing movement visible", "min_people": 2, "max_people": 3},
{"text": "mirror-view undressing frame with faces, hands, and exposed skin visible", "min_people": 2, "max_people": 3},
{"text": "tight crop on hands touching breasts, face, waist, and clothing", "min_people": 2, "max_people": 3},
{"text": "couch foreplay composition with bodies pressed close", "min_people": 2, "max_people": 3},
{"text": "wall-pressed kissing composition with hands on face and hips", "min_people": 2, "max_people": 3},
{"text": "seated lap teasing composition with clothing being pulled aside", "min_people": 2, "max_people": 3},
{"text": "close candid creator-shot frame centered on undressing and body contact", "min_people": 2, "max_people": 3},
{"text": "full-body pre-sex foreplay frame with faces, hands, and clothes readable", "min_people": 2, "max_people": 3}
],
"hardcore_explicit_compositions": [ "hardcore_explicit_compositions": [
{"text": "full-body explicit sex frame with all adult bodies visible", "min_people": 2}, {"text": "full-body explicit sex frame with all adult bodies visible", "min_people": 2},
{"text": "bed-level camera angle focused on genital contact and faces", "min_people": 2, "max_people": 3}, {"text": "bed-level camera angle focused on genital contact and faces", "min_people": 2, "max_people": 3},
+119
View File
@@ -84,6 +84,125 @@
} }
], ],
"subcategories": [ "subcategories": [
{
"name": "Foreplay and teasing",
"slug": "foreplay_teasing",
"min_people": 2,
"min_women": 1,
"inherit_expressions": false,
"inherit_compositions": false,
"weight": 0.75,
"item_label": "Foreplay action",
"positive_suffix": "Use clear adult body contact, readable hands and faces, visible undressing, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Foreplay action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult body contact, kissing, caressing, undressing, visible arousal, exposed skin, and readable hand placement. {positive_suffix} Avoid: {negative_prompt}.",
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, foreplay teasing action: {item}, {scene}, {composition}, explicit consensual adult erotic foreplay illustration",
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
"expression_pools": ["hardcore_foreplay_expressions"],
"composition_pools": ["foreplay_compositions"],
"item_templates": [
"{tease_act} in {position}, with {touch_detail}, {clothing_detail}, and {mood_detail}",
"{position} featuring {tease_act}, {body_contact}, {touch_detail}, and {visibility}",
"hardcore foreplay setup: {tease_act}, {clothing_detail}, {face_detail}, and {body_contact}",
"{tease_act} on {surface}, with {touch_detail}, {mood_detail}, and {visibility}",
"{position} while {tease_act}, with {face_detail}, {clothing_detail}, and {touch_detail}"
],
"item_axes": {
"position": [
"standing kissing position",
"bed-edge undressing position",
"seated lap teasing position",
"kneeling clothing-removal position",
"side-lying caressing position",
"mirror undressing position",
"couch foreplay position",
"wall-pressed kissing position"
],
"tease_act": [
"deep kissing before sex",
"mouth-to-mouth kissing with bodies pressed close",
"slow caressing over bare skin",
"hands roaming over the body before sex",
"touching and squeezing breasts during foreplay",
"one hand cupping a breast while kissing",
"hand on the cheek and jaw during a heated kiss",
"fingers under the chin guiding the face closer",
"pulling clothing aside before sex",
"slowly removing clothing with visible skin revealed",
"sliding straps off shoulders and exposing the chest",
"unbuttoning clothing while bodies press together"
],
"touch_detail": [
"hands cupping breasts",
"thumbs brushing nipples through parted clothing",
"fingers tracing the waist and hips",
"one hand gripping the ass",
"one hand on the cheek and jaw",
"fingers under the chin",
"hands sliding under fabric",
"hands pulling bodies closer",
"fingers tangled in hair",
"hands on the lower back"
],
"clothing_detail": [
"clothing partly removed with fabric bunched at the waist",
"shirt pulled open and straps sliding off shoulders",
"bra or top pushed aside with chest exposed",
"lower garments loosened while bodies stay pressed close",
"clothing being pulled off one shoulder",
"open shirt exposing bare chest and stomach",
"fabric lifted to reveal skin while kissing",
"discarded clothing visible nearby"
],
"face_detail": [
"foreheads close and lips nearly touching",
"mouths pressed together in a deep kiss",
"cheek held in one hand",
"chin lifted by fingertips",
"heavy eye contact at close range",
"breathless parted lips",
"face turned toward the partner's hand",
"hair pulled back gently from the face"
],
"body_contact": [
"chests pressed together",
"hips pressed close",
"bodies leaning into each other",
"one body pinned gently against the wall",
"torso arched into the touch",
"thighs pressed together",
"standing close with bodies aligned",
"seated close with legs interlaced"
],
"mood_detail": [
"slow teasing buildup",
"heated pre-sex tension",
"intimate close-contact anticipation",
"urgent hands and heavy breathing",
"consensual teasing before the explicit act",
"breathless pause before sex",
"visible arousal before sex",
"adult foreplay tension"
],
"visibility": [
"hands, faces, and exposed skin clearly readable",
"clothing movement and body contact visible",
"breasts, hands, and faces centered",
"face-touching and undressing action readable",
"body contact visible before sex",
"teasing hand placement clearly visible"
],
"surface": [
"rumpled bed sheets",
"a wide couch",
"against a wall",
"beside a mirror",
"a hotel bed",
"floor cushions",
"a dressing-room bench",
"a low mattress"
]
}
},
{ {
"name": "Penetrative sex", "name": "Penetrative sex",
"slug": "penetrative_sex", "slug": "penetrative_sex",
+36 -1
View File
@@ -875,9 +875,42 @@ def _is_oral_text(*parts: Any) -> bool:
) )
def _is_foreplay_text(*parts: Any) -> bool:
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
if not text:
return False
return any(
term in text
for term in (
"foreplay",
"pre-sex",
"before sex",
"before penetration",
"kissing",
"deep kiss",
"mouth-to-mouth",
"lips pressed",
"caressing",
"hands roaming",
"stroking skin",
"touching breasts",
"cupping a breast",
"hand on the cheek",
"cheek and jaw",
"fingers under the chin",
"undressing",
"removing clothing",
"removing clothes",
"pulling clothing",
"sliding straps",
"unbuttoning",
)
)
def _is_vaginal_penetration_text(*parts: Any) -> bool: def _is_vaginal_penetration_text(*parts: Any) -> bool:
text = " ".join(_clean(part).lower() for part in parts if _clean(part)) text = " ".join(_clean(part).lower() for part in parts if _clean(part))
if not text or _is_outercourse_text(text) or _is_oral_text(text): if not text or _is_outercourse_text(text) or _is_oral_text(text) or _is_foreplay_text(text):
return False return False
if any(term in text for term in ("anal", "double penetration", "double-penetration", "toy-assisted", "strap-on")): if any(term in text for term in ("anal", "double penetration", "double-penetration", "toy-assisted", "strap-on")):
return False return False
@@ -952,6 +985,8 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = ""
position_text = _clean(axis_values.get("position", "")).lower() position_text = _clean(axis_values.get("position", "")).lower()
if not text: if not text:
return "" return ""
if _is_foreplay_text(role_graph, hard_item, composition, _axis_values_text(axis_values)):
return ""
if _is_outercourse_text(role_graph, hard_item, composition, _axis_values_text(axis_values)): if _is_outercourse_text(role_graph, hard_item, composition, _axis_values_text(axis_values)):
if any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex")): if any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex")):
return "breast-sex outercourse pose" return "breast-sex outercourse pose"
+103 -5
View File
@@ -299,6 +299,7 @@ HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
HARDCORE_POSITION_FAMILY_CHOICES = [ HARDCORE_POSITION_FAMILY_CHOICES = [
"any", "any",
"penetrative", "penetrative",
"foreplay",
"oral", "oral",
"outercourse", "outercourse",
"anal", "anal",
@@ -309,6 +310,7 @@ HARDCORE_POSITION_FAMILY_CHOICES = [
HARDCORE_POSITION_FOCUS_CHOICES = [ HARDCORE_POSITION_FOCUS_CHOICES = [
"keep_pool", "keep_pool",
"penetration_only", "penetration_only",
"foreplay_only",
"oral_only", "oral_only",
"outercourse_only", "outercourse_only",
"anal_only", "anal_only",
@@ -334,6 +336,11 @@ HARDCORE_POSITION_KEY_CHOICES = [
"straddled_oral", "straddled_oral",
"spread_leg_oral", "spread_leg_oral",
"chair_oral", "chair_oral",
"kissing",
"caressing",
"breast_touch",
"face_touch",
"undressing",
"boobjob", "boobjob",
"testicle_sucking", "testicle_sucking",
"penis_licking", "penis_licking",
@@ -345,6 +352,7 @@ HARDCORE_POSITION_KEY_CHOICES = [
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = { HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
"any": [ "any": [
"penetrative_sex", "penetrative_sex",
"foreplay_teasing",
"oral_sex", "oral_sex",
"outercourse_sex", "outercourse_sex",
"anal_double_penetration", "anal_double_penetration",
@@ -353,6 +361,7 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
"cumshot_climax", "cumshot_climax",
], ],
"penetrative": ["penetrative_sex"], "penetrative": ["penetrative_sex"],
"foreplay": ["foreplay_teasing"],
"oral": ["oral_sex"], "oral": ["oral_sex"],
"outercourse": ["outercourse_sex"], "outercourse": ["outercourse_sex"],
"anal": ["anal_double_penetration"], "anal": ["anal_double_penetration"],
@@ -378,6 +387,11 @@ HARDCORE_POSITION_KEY_MATCHES = {
"straddled_oral": ("straddled oral",), "straddled_oral": ("straddled oral",),
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"), "spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
"chair_oral": ("chair oral",), "chair_oral": ("chair oral",),
"kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"),
"caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"),
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"), "boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"), "testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"), "penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
@@ -2332,6 +2346,7 @@ def _empty_hardcore_position_config() -> dict[str, Any]:
"allow_toys": True, "allow_toys": True,
"allow_double": True, "allow_double": True,
"allow_penetration": True, "allow_penetration": True,
"allow_foreplay": True,
"allow_oral": True, "allow_oral": True,
"allow_outercourse": True, "allow_outercourse": True,
"allow_anal": True, "allow_anal": True,
@@ -2356,7 +2371,16 @@ def _parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[
parsed["family"] = _normalize_hardcore_position_family(parsed.get("family")) parsed["family"] = _normalize_hardcore_position_family(parsed.get("family"))
parsed["positions"] = _normalize_hardcore_position_values(parsed.get("positions")) parsed["positions"] = _normalize_hardcore_position_values(parsed.get("positions"))
parsed["require_position"] = not _is_false(parsed.get("require_position", False)) parsed["require_position"] = not _is_false(parsed.get("require_position", False))
for key in ("allow_toys", "allow_double", "allow_penetration", "allow_oral", "allow_outercourse", "allow_anal", "allow_climax"): for key in (
"allow_toys",
"allow_double",
"allow_penetration",
"allow_foreplay",
"allow_oral",
"allow_outercourse",
"allow_anal",
"allow_climax",
):
parsed[key] = not _is_false(parsed.get(key, True)) parsed[key] = not _is_false(parsed.get(key, True))
return parsed return parsed
@@ -2376,6 +2400,7 @@ def _hardcore_position_summary(config: dict[str, Any]) -> str:
("allow_toys", "toys"), ("allow_toys", "toys"),
("allow_double", "double"), ("allow_double", "double"),
("allow_penetration", "penetration"), ("allow_penetration", "penetration"),
("allow_foreplay", "foreplay"),
("allow_oral", "oral"), ("allow_oral", "oral"),
("allow_outercourse", "outercourse"), ("allow_outercourse", "outercourse"),
("allow_anal", "anal"), ("allow_anal", "anal"),
@@ -2420,6 +2445,7 @@ def build_hardcore_action_filter_json(
allow_toys: bool = False, allow_toys: bool = False,
allow_double: bool = False, allow_double: bool = False,
allow_penetration: bool = True, allow_penetration: bool = True,
allow_foreplay: bool = True,
allow_oral: bool = True, allow_oral: bool = True,
allow_outercourse: bool = True, allow_outercourse: bool = True,
allow_anal: bool = True, allow_anal: bool = True,
@@ -2430,6 +2456,7 @@ def build_hardcore_action_filter_json(
focus = str(focus or "keep_pool").strip() focus = str(focus or "keep_pool").strip()
focus_family = { focus_family = {
"penetration_only": "penetrative", "penetration_only": "penetrative",
"foreplay_only": "foreplay",
"oral_only": "oral", "oral_only": "oral",
"outercourse_only": "outercourse", "outercourse_only": "outercourse",
"anal_only": "anal", "anal_only": "anal",
@@ -2442,6 +2469,7 @@ def build_hardcore_action_filter_json(
config["allow_toys"] = bool(allow_toys) config["allow_toys"] = bool(allow_toys)
config["allow_double"] = bool(allow_double) config["allow_double"] = bool(allow_double)
config["allow_penetration"] = bool(allow_penetration) config["allow_penetration"] = bool(allow_penetration)
config["allow_foreplay"] = bool(allow_foreplay)
config["allow_oral"] = bool(allow_oral) config["allow_oral"] = bool(allow_oral)
config["allow_outercourse"] = bool(allow_outercourse) config["allow_outercourse"] = bool(allow_outercourse)
config["allow_anal"] = bool(allow_anal) config["allow_anal"] = bool(allow_anal)
@@ -2452,6 +2480,7 @@ def build_hardcore_action_filter_json(
family family
for enabled, family in ( for enabled, family in (
(config["allow_penetration"], "penetrative"), (config["allow_penetration"], "penetrative"),
(config["allow_foreplay"], "foreplay"),
(config["allow_oral"], "oral"), (config["allow_oral"], "oral"),
(config["allow_outercourse"], "outercourse"), (config["allow_outercourse"], "outercourse"),
(config["allow_anal"], "anal"), (config["allow_anal"], "anal"),
@@ -2462,7 +2491,9 @@ def build_hardcore_action_filter_json(
if config["family"] in enabled_action_families and len(enabled_action_families) > 1: if config["family"] in enabled_action_families and len(enabled_action_families) > 1:
config["family"] = "any" config["family"] = "any"
if focus == "oral_only": if focus == "foreplay_only":
config["allow_foreplay"] = True
elif focus == "oral_only":
config["allow_oral"] = True config["allow_oral"] = True
config["allow_penetration"] = False config["allow_penetration"] = False
elif focus == "outercourse_only": elif focus == "outercourse_only":
@@ -2497,6 +2528,8 @@ def _hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])) allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
if not config.get("allow_penetration", True): if not config.get("allow_penetration", True):
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"}) allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
if not config.get("allow_foreplay", True):
allowed.discard("foreplay_teasing")
if not config.get("allow_oral", True): if not config.get("allow_oral", True):
allowed.discard("oral_sex") allowed.discard("oral_sex")
if not config.get("allow_outercourse", True): if not config.get("allow_outercourse", True):
@@ -2567,6 +2600,32 @@ def _hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str
or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex")) or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex"))
): ):
return True return True
if not config.get("allow_foreplay", True) and (
axis_name in ("tease_act", "touch_detail", "clothing_detail", "foreplay_detail", "face_detail", "body_contact", "mood_detail")
or any(
term in text
for term in (
"kiss",
"kissing",
"mouth-to-mouth",
"caress",
"caressing",
"stroking skin",
"hands roaming",
"touching breasts",
"cupping breasts",
"hand on the cheek",
"fingers under the chin",
"undressing",
"removing clothing",
"removing clothes",
"pulling clothing",
"sliding straps",
"unbuttoning",
)
)
):
return True
if not config.get("allow_climax", True) and ( if not config.get("allow_climax", True) and (
axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location") axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location")
or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration")) or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration"))
@@ -5975,6 +6034,39 @@ def _role_graph(
] ]
return f" {extra} {rng.choice(actions)}." return f" {extra} {rng.choice(actions)}."
def foreplay_position_graph(primary: str, partner: str) -> str:
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
if any(term in text for term in ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning")):
return (
f"{primary} and {partner} stand close while {partner}'s hands pull clothing aside from {primary}'s body; "
f"{primary}'s exposed skin and the clothing being removed stay clearly visible."
)
if any(term in text for term in ("breast", "breasts", "nipple", "cupping breasts", "touching breasts")):
return (
f"{primary} and {partner} press their bodies close while {partner}'s hand cups {primary}'s breast; "
f"their faces stay close and the breast-touching gesture is clear."
)
if any(term in text for term in ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin")):
return (
f"{primary} and {partner} stand face-to-face at close range while one hand holds {primary}'s cheek and jaw; "
f"their lips are close and the face-touching gesture is clear."
)
if any(term in text for term in ("kiss", "kissing", "mouth-to-mouth", "lips pressed")):
return (
f"{primary} and {partner} press their bodies together and kiss deeply, "
f"with hands on each other's face, waist, and hips."
)
return (
f"{primary} and {partner} are pressed close in a heated foreplay setup, "
f"hands caressing skin while clothing is pulled aside."
)
def mentions_ass(text: str) -> bool: def mentions_ass(text: str) -> bool:
return bool( return bool(
re.search( re.search(
@@ -6265,7 +6357,9 @@ def _role_graph(
a, b = _pick_distinct(rng, women, 2) a, b = _pick_distinct(rng, women, 2)
c = any_woman({a, b}) if len(women) >= 3 else "" c = any_woman({a, b}) if len(women) >= 3 else ""
used = {a, b} used = {a, b}
if "outercourse" in slug: if "foreplay" in slug:
graph = foreplay_position_graph(a, b)
elif "outercourse" in slug:
graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact." graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact."
elif "oral" in slug: elif "oral" in slug:
graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy." graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy."
@@ -6285,7 +6379,9 @@ def _role_graph(
a, b = _pick_distinct(rng, men, 2) a, b = _pick_distinct(rng, men, 2)
c = any_man({a, b}) if len(men) >= 3 else "" c = any_man({a, b}) if len(men) >= 3 else ""
used = {a, b} used = {a, b}
if "outercourse" in slug: if "foreplay" in slug:
graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside."
elif "outercourse" in slug:
graph = f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet." graph = f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet."
elif "oral" in slug: elif "oral" in slug:
graph = f"{a} kneels and takes {b}'s penis in his mouth while holding his hips." graph = f"{a} kneels and takes {b}'s penis in his mouth while holding his hips."
@@ -6305,7 +6401,9 @@ def _role_graph(
woman = any_woman() woman = any_woman()
man = any_man() man = any_man()
third = any_person({woman, man}) if people_count >= 3 else "" third = any_person({woman, man}) if people_count >= 3 else ""
if "outercourse" in slug: if "foreplay" in slug:
graph = foreplay_position_graph(woman, man)
elif "outercourse" in slug:
graph = outercourse_position_graph(woman, man) graph = outercourse_position_graph(woman, man)
elif "oral" in slug: elif "oral" in slug:
graph = oral_position_graph(woman, man) graph = oral_position_graph(woman, man)