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
+103 -5
View File
@@ -299,6 +299,7 @@ HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
HARDCORE_POSITION_FAMILY_CHOICES = [
"any",
"penetrative",
"foreplay",
"oral",
"outercourse",
"anal",
@@ -309,6 +310,7 @@ HARDCORE_POSITION_FAMILY_CHOICES = [
HARDCORE_POSITION_FOCUS_CHOICES = [
"keep_pool",
"penetration_only",
"foreplay_only",
"oral_only",
"outercourse_only",
"anal_only",
@@ -334,6 +336,11 @@ HARDCORE_POSITION_KEY_CHOICES = [
"straddled_oral",
"spread_leg_oral",
"chair_oral",
"kissing",
"caressing",
"breast_touch",
"face_touch",
"undressing",
"boobjob",
"testicle_sucking",
"penis_licking",
@@ -345,6 +352,7 @@ HARDCORE_POSITION_KEY_CHOICES = [
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
"any": [
"penetrative_sex",
"foreplay_teasing",
"oral_sex",
"outercourse_sex",
"anal_double_penetration",
@@ -353,6 +361,7 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
"cumshot_climax",
],
"penetrative": ["penetrative_sex"],
"foreplay": ["foreplay_teasing"],
"oral": ["oral_sex"],
"outercourse": ["outercourse_sex"],
"anal": ["anal_double_penetration"],
@@ -378,6 +387,11 @@ HARDCORE_POSITION_KEY_MATCHES = {
"straddled_oral": ("straddled oral",),
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
"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"),
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
"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_double": True,
"allow_penetration": True,
"allow_foreplay": True,
"allow_oral": True,
"allow_outercourse": 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["positions"] = _normalize_hardcore_position_values(parsed.get("positions"))
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))
return parsed
@@ -2376,6 +2400,7 @@ def _hardcore_position_summary(config: dict[str, Any]) -> str:
("allow_toys", "toys"),
("allow_double", "double"),
("allow_penetration", "penetration"),
("allow_foreplay", "foreplay"),
("allow_oral", "oral"),
("allow_outercourse", "outercourse"),
("allow_anal", "anal"),
@@ -2420,6 +2445,7 @@ def build_hardcore_action_filter_json(
allow_toys: bool = False,
allow_double: bool = False,
allow_penetration: bool = True,
allow_foreplay: bool = True,
allow_oral: bool = True,
allow_outercourse: bool = True,
allow_anal: bool = True,
@@ -2430,6 +2456,7 @@ def build_hardcore_action_filter_json(
focus = str(focus or "keep_pool").strip()
focus_family = {
"penetration_only": "penetrative",
"foreplay_only": "foreplay",
"oral_only": "oral",
"outercourse_only": "outercourse",
"anal_only": "anal",
@@ -2442,6 +2469,7 @@ def build_hardcore_action_filter_json(
config["allow_toys"] = bool(allow_toys)
config["allow_double"] = bool(allow_double)
config["allow_penetration"] = bool(allow_penetration)
config["allow_foreplay"] = bool(allow_foreplay)
config["allow_oral"] = bool(allow_oral)
config["allow_outercourse"] = bool(allow_outercourse)
config["allow_anal"] = bool(allow_anal)
@@ -2452,6 +2480,7 @@ def build_hardcore_action_filter_json(
family
for enabled, family in (
(config["allow_penetration"], "penetrative"),
(config["allow_foreplay"], "foreplay"),
(config["allow_oral"], "oral"),
(config["allow_outercourse"], "outercourse"),
(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:
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_penetration"] = False
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"]))
if not config.get("allow_penetration", True):
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):
allowed.discard("oral_sex")
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"))
):
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 (
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"))
@@ -5975,6 +6034,39 @@ def _role_graph(
]
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:
return bool(
re.search(
@@ -6265,7 +6357,9 @@ def _role_graph(
a, b = _pick_distinct(rng, women, 2)
c = any_woman({a, b}) if len(women) >= 3 else ""
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."
elif "oral" in slug:
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)
c = any_man({a, b}) if len(men) >= 3 else ""
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."
elif "oral" in slug:
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()
man = any_man()
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)
elif "oral" in slug:
graph = oral_position_graph(woman, man)