Add hardcore outercourse actions
This commit is contained in:
+186
-7
@@ -299,6 +299,7 @@ HARDCORE_POSITION_FAMILY_CHOICES = [
|
||||
"any",
|
||||
"penetrative",
|
||||
"oral",
|
||||
"outercourse",
|
||||
"anal",
|
||||
"climax",
|
||||
"threesome",
|
||||
@@ -308,6 +309,7 @@ HARDCORE_POSITION_FOCUS_CHOICES = [
|
||||
"keep_pool",
|
||||
"penetration_only",
|
||||
"oral_only",
|
||||
"outercourse_only",
|
||||
"anal_only",
|
||||
"climax_only",
|
||||
"threesome_only",
|
||||
@@ -331,6 +333,10 @@ HARDCORE_POSITION_KEY_CHOICES = [
|
||||
"straddled_oral",
|
||||
"spread_leg_oral",
|
||||
"chair_oral",
|
||||
"boobjob",
|
||||
"testicle_sucking",
|
||||
"penis_licking",
|
||||
"footjob",
|
||||
"open_thighs",
|
||||
"front_back",
|
||||
]
|
||||
@@ -338,6 +344,7 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
||||
"any": [
|
||||
"penetrative_sex",
|
||||
"oral_sex",
|
||||
"outercourse_sex",
|
||||
"anal_double_penetration",
|
||||
"threesomes",
|
||||
"group_sex_orgy",
|
||||
@@ -345,6 +352,7 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
||||
],
|
||||
"penetrative": ["penetrative_sex"],
|
||||
"oral": ["oral_sex"],
|
||||
"outercourse": ["outercourse_sex"],
|
||||
"anal": ["anal_double_penetration"],
|
||||
"climax": ["cumshot_climax"],
|
||||
"threesome": ["threesomes"],
|
||||
@@ -368,6 +376,10 @@ HARDCORE_POSITION_KEY_MATCHES = {
|
||||
"straddled_oral": ("straddled oral",),
|
||||
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
|
||||
"chair_oral": ("chair oral",),
|
||||
"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"),
|
||||
"footjob": ("footjob", "soles", "toes curled", "feet stroking"),
|
||||
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
|
||||
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
|
||||
}
|
||||
@@ -993,6 +1005,83 @@ def _oral_acts_for_position(values: list[Any], position: str) -> list[Any]:
|
||||
return values
|
||||
|
||||
|
||||
def _outercourse_acts_for_position(values: list[Any], position: str) -> list[Any]:
|
||||
position_text = str(position or "").lower()
|
||||
if not position_text:
|
||||
return values
|
||||
|
||||
def act_text(value: Any) -> str:
|
||||
return _entry_text(value).lower()
|
||||
|
||||
def filtered(predicate: Callable[[str], bool]) -> list[Any]:
|
||||
matches = [value for value in values if predicate(act_text(value))]
|
||||
return matches or values
|
||||
|
||||
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
|
||||
return filtered(lambda text: any(term in text for term in ("boobjob", "titjob", "breast sex", "breasts")))
|
||||
if any(term in position_text for term in ("testicle", "balls")):
|
||||
return filtered(lambda text: any(term in text for term in ("testicle", "balls")))
|
||||
if "penis-licking" in position_text or "penis licking" in position_text:
|
||||
return filtered(lambda text: "licking" in text or "tongue" in text)
|
||||
if "footjob" in position_text:
|
||||
return filtered(lambda text: any(term in text for term in ("footjob", "feet", "soles", "toes")))
|
||||
return values
|
||||
|
||||
|
||||
def _outercourse_axis_values_for_position(values: list[Any], position: str, axis_name: str) -> list[Any]:
|
||||
position_text = str(position or "").lower()
|
||||
if not position_text:
|
||||
return values
|
||||
axis_name = str(axis_name or "").lower()
|
||||
if axis_name not in {"contact_detail", "hand_detail", "texture_detail", "visibility", "body_contact"}:
|
||||
return values
|
||||
|
||||
def value_text(value: Any) -> str:
|
||||
return _entry_text(value).lower()
|
||||
|
||||
def filtered(terms: tuple[str, ...]) -> list[Any]:
|
||||
matches = [value for value in values if any(term in value_text(value) for term in terms)]
|
||||
return matches or values
|
||||
|
||||
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
|
||||
by_axis = {
|
||||
"contact_detail": ("compressed", "glans", "shaft", "skin", "fingers"),
|
||||
"hand_detail": ("breast", "breasts", "fingers"),
|
||||
"texture_detail": ("compression", "soft flesh", "skin", "flesh", "asymmetry"),
|
||||
"visibility": ("breast", "breasts", "glans", "shaft"),
|
||||
"body_contact": ("torso", "body angled", "shoulders", "hips"),
|
||||
}
|
||||
return filtered(by_axis.get(axis_name, ("breast", "breasts", "shaft")))
|
||||
if any(term in position_text for term in ("testicle", "balls")):
|
||||
by_axis = {
|
||||
"contact_detail": ("balls", "lips", "tongue", "wet"),
|
||||
"hand_detail": ("balls", "base", "thigh"),
|
||||
"texture_detail": ("wet", "saliva", "skin"),
|
||||
"visibility": ("balls", "mouth"),
|
||||
"body_contact": ("hips", "knees", "thigh", "lower body"),
|
||||
}
|
||||
return filtered(by_axis.get(axis_name, ("balls", "mouth", "tongue")))
|
||||
if "penis-licking" in position_text or "penis licking" in position_text:
|
||||
by_axis = {
|
||||
"contact_detail": ("tongue", "lips", "glans", "shaft", "wet"),
|
||||
"hand_detail": ("base", "penis", "thigh"),
|
||||
"texture_detail": ("wet", "saliva", "skin"),
|
||||
"visibility": ("tongue", "penis"),
|
||||
"body_contact": ("hips", "body angled", "lower body"),
|
||||
}
|
||||
return filtered(by_axis.get(axis_name, ("tongue", "glans", "shaft")))
|
||||
if "footjob" in position_text:
|
||||
by_axis = {
|
||||
"contact_detail": ("soles", "toes", "shaft"),
|
||||
"hand_detail": ("ankles", "thighs"),
|
||||
"texture_detail": ("toes", "soles", "pressure"),
|
||||
"visibility": ("feet", "soles"),
|
||||
"body_contact": ("legs", "knees", "body angled"),
|
||||
}
|
||||
return filtered(by_axis.get(axis_name, ("feet", "soles", "toes")))
|
||||
return values
|
||||
|
||||
|
||||
def _compose_item(
|
||||
rng: random.Random,
|
||||
category: dict[str, Any],
|
||||
@@ -1008,15 +1097,20 @@ def _compose_item(
|
||||
fields = [key for _, key, _, _ in Formatter().parse(template) if key]
|
||||
unique_fields = list(dict.fromkeys(fields))
|
||||
axis_values: dict[str, str] = {}
|
||||
if str(subcategory.get("slug") or "").lower() == "oral_sex" and "position" in unique_fields and axes.get("position"):
|
||||
subcategory_slug = str(subcategory.get("slug") or "").lower()
|
||||
if subcategory_slug in ("oral_sex", "outercourse_sex") and "position" in unique_fields and axes.get("position"):
|
||||
position_values = _compatible_entries(axes["position"], women_count, men_count)
|
||||
axis_values["position"] = _entry_text(_weighted_choice(rng, position_values))
|
||||
for name in unique_fields:
|
||||
if name in axis_values or name not in axes or not axes[name]:
|
||||
continue
|
||||
values = _compatible_entries(axes[name], women_count, men_count)
|
||||
if str(subcategory.get("slug") or "").lower() == "oral_sex" and name == "oral_act":
|
||||
if subcategory_slug == "oral_sex" and name == "oral_act":
|
||||
values = _oral_acts_for_position(values, axis_values.get("position", ""))
|
||||
if subcategory_slug == "outercourse_sex" and name == "outer_act":
|
||||
values = _outercourse_acts_for_position(values, axis_values.get("position", ""))
|
||||
if subcategory_slug == "outercourse_sex":
|
||||
values = _outercourse_axis_values_for_position(values, axis_values.get("position", ""), name)
|
||||
axis_values[name] = _entry_text(_weighted_choice(rng, values))
|
||||
item_text = _format(template, axis_values).strip()
|
||||
item_name = _item_name(item) or subcategory["name"]
|
||||
@@ -1664,6 +1758,7 @@ def _empty_hardcore_position_config() -> dict[str, Any]:
|
||||
"allow_double": True,
|
||||
"allow_penetration": True,
|
||||
"allow_oral": True,
|
||||
"allow_outercourse": True,
|
||||
"allow_anal": True,
|
||||
"allow_climax": True,
|
||||
}
|
||||
@@ -1686,7 +1781,7 @@ 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_anal", "allow_climax"):
|
||||
for key in ("allow_toys", "allow_double", "allow_penetration", "allow_oral", "allow_outercourse", "allow_anal", "allow_climax"):
|
||||
parsed[key] = not _is_false(parsed.get(key, True))
|
||||
return parsed
|
||||
|
||||
@@ -1707,6 +1802,7 @@ def _hardcore_position_summary(config: dict[str, Any]) -> str:
|
||||
("allow_double", "double"),
|
||||
("allow_penetration", "penetration"),
|
||||
("allow_oral", "oral"),
|
||||
("allow_outercourse", "outercourse"),
|
||||
("allow_anal", "anal"),
|
||||
("allow_climax", "climax"),
|
||||
)
|
||||
@@ -1750,6 +1846,7 @@ def build_hardcore_action_filter_json(
|
||||
allow_double: bool = False,
|
||||
allow_penetration: bool = True,
|
||||
allow_oral: bool = True,
|
||||
allow_outercourse: bool = True,
|
||||
allow_anal: bool = True,
|
||||
allow_climax: bool = True,
|
||||
) -> str:
|
||||
@@ -1759,6 +1856,7 @@ def build_hardcore_action_filter_json(
|
||||
focus_family = {
|
||||
"penetration_only": "penetrative",
|
||||
"oral_only": "oral",
|
||||
"outercourse_only": "outercourse",
|
||||
"anal_only": "anal",
|
||||
"climax_only": "climax",
|
||||
"threesome_only": "threesome",
|
||||
@@ -1770,6 +1868,7 @@ def build_hardcore_action_filter_json(
|
||||
config["allow_double"] = bool(allow_double)
|
||||
config["allow_penetration"] = bool(allow_penetration)
|
||||
config["allow_oral"] = bool(allow_oral)
|
||||
config["allow_outercourse"] = bool(allow_outercourse)
|
||||
config["allow_anal"] = bool(allow_anal)
|
||||
config["allow_climax"] = bool(allow_climax)
|
||||
|
||||
@@ -1779,6 +1878,7 @@ def build_hardcore_action_filter_json(
|
||||
for enabled, family in (
|
||||
(config["allow_penetration"], "penetrative"),
|
||||
(config["allow_oral"], "oral"),
|
||||
(config["allow_outercourse"], "outercourse"),
|
||||
(config["allow_anal"], "anal"),
|
||||
(config["allow_climax"], "climax"),
|
||||
)
|
||||
@@ -1790,6 +1890,10 @@ def build_hardcore_action_filter_json(
|
||||
if focus == "oral_only":
|
||||
config["allow_oral"] = True
|
||||
config["allow_penetration"] = False
|
||||
elif focus == "outercourse_only":
|
||||
config["allow_outercourse"] = True
|
||||
config["allow_oral"] = False
|
||||
config["allow_penetration"] = False
|
||||
elif focus == "anal_only":
|
||||
config["allow_anal"] = True
|
||||
config["allow_penetration"] = True
|
||||
@@ -1820,6 +1924,8 @@ def _hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
||||
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
||||
if not config.get("allow_oral", True):
|
||||
allowed.discard("oral_sex")
|
||||
if not config.get("allow_outercourse", True):
|
||||
allowed.discard("outercourse_sex")
|
||||
if not config.get("allow_anal", True):
|
||||
allowed.discard("anal_double_penetration")
|
||||
if not config.get("allow_climax", True):
|
||||
@@ -1876,6 +1982,11 @@ def _hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str
|
||||
or any(term in text for term in ("oral sex", "mouth on genitals", "mouth on pussy", "blowjob", "cunnilingus", "tongue on pussy", "deepthroat", "fellatio"))
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_outercourse", True) and (
|
||||
axis_name in ("outer_act", "contact_detail", "texture_detail")
|
||||
or any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex", "testicle", "balls", "penis licking", "penis-licking", "footjob", "soles", "toes"))
|
||||
):
|
||||
return True
|
||||
if not config.get("allow_penetration", True) and (
|
||||
axis_name in ("penetration_act", "penetration_detail", "anal_act", "double_act", "thrust_detail")
|
||||
or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex"))
|
||||
@@ -4825,6 +4936,7 @@ def _role_graph(
|
||||
subcategory: dict[str, Any],
|
||||
context: dict[str, str],
|
||||
item_axis_values: dict[str, str] | None = None,
|
||||
pov_labels: list[str] | None = None,
|
||||
) -> str:
|
||||
if context.get("subject_type") != "configured_cast":
|
||||
return ""
|
||||
@@ -4840,6 +4952,7 @@ def _role_graph(
|
||||
people = participants["people"]
|
||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
||||
item_text = " ".join((item_axis_values or {}).values()).lower()
|
||||
pov_set = set(pov_labels or [])
|
||||
|
||||
def any_person(exclude: set[str] | None = None) -> str:
|
||||
exclude = exclude or set()
|
||||
@@ -4974,6 +5087,66 @@ def _role_graph(
|
||||
return f"{woman} kneels forward with hips raised while {man} kneels behind her and thrusts his penis into her ass."
|
||||
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
|
||||
|
||||
def outercourse_position_graph(woman: str, man: str) -> str:
|
||||
position_text = str((item_axis_values or {}).get("position") or "").lower()
|
||||
text = " ".join(
|
||||
str(part or "").lower()
|
||||
for part in (
|
||||
item_text,
|
||||
*((item_axis_values or {}).values()),
|
||||
)
|
||||
)
|
||||
man_is_pov = man in pov_set
|
||||
if any(term in text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} kneels between the POV viewer's thighs with her breasts squeezed around the POV viewer's penis, "
|
||||
"hands pressing both breasts together around the shaft while the glans stays near her mouth."
|
||||
)
|
||||
return (
|
||||
f"{man} sits or reclines with legs apart while {woman} kneels between his thighs, squeezing her breasts "
|
||||
f"around {man}'s penis with both hands while the glans stays near her mouth."
|
||||
)
|
||||
if any(term in text for term in ("testicle", "balls-licking", "balls licking", "balls and mouth", "balls held")):
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} kneels between the POV viewer's thighs with her mouth at the POV viewer's balls, "
|
||||
"lips and tongue making visible contact while the POV viewer's penis remains above her face."
|
||||
)
|
||||
return (
|
||||
f"{man} sits with legs apart while {woman} kneels low between his thighs, sucking and licking his balls "
|
||||
f"with {man}'s penis visible above her face."
|
||||
)
|
||||
if "penis-licking" in position_text or "penis licking" in text or "tongue along" in text or "tongue licking" in text:
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} kneels at the POV viewer's hips with her tongue along the POV viewer's penis, "
|
||||
"face close to the shaft and glans while her hands steady the base."
|
||||
)
|
||||
return (
|
||||
f"{woman} kneels at {man}'s hips with her tongue along {man}'s penis, face close to the shaft and glans "
|
||||
"while her hands steady the base."
|
||||
)
|
||||
if "footjob" in text or "soles" in text or "toes curled" in text or "feet stroking" in text:
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} faces the POV viewer while the POV viewer reclines, wrapping both soles around the POV viewer's penis "
|
||||
"and stroking the shaft with her feet from a clear first-person view."
|
||||
)
|
||||
return (
|
||||
f"{man} reclines with hips forward while {woman} faces him and wraps both soles around {man}'s penis, "
|
||||
"stroking the shaft with her feet while the contact stays centered."
|
||||
)
|
||||
if man_is_pov:
|
||||
return (
|
||||
f"{woman} kneels close to the POV viewer's hips and keeps the POV viewer's penis centered in clear non-penetrative contact, "
|
||||
"with her mouth, hands, breasts, or feet visibly working around the shaft."
|
||||
)
|
||||
return (
|
||||
f"{woman} kneels close to {man}'s hips and keeps {man}'s penis centered in clear non-penetrative contact, "
|
||||
"with her mouth, hands, breasts, or feet visibly working around the shaft."
|
||||
)
|
||||
|
||||
def oral_position_graph(woman: str, man: str) -> str:
|
||||
position_text = str((item_axis_values or {}).get("position") or "").lower()
|
||||
text = " ".join(
|
||||
@@ -5073,7 +5246,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 "oral" in slug:
|
||||
if "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."
|
||||
elif "anal" in slug or "double" in slug:
|
||||
graph = f"{a} uses a strap-on on {b} while keeping her hips held open."
|
||||
@@ -5091,7 +5266,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 "oral" in slug:
|
||||
if "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."
|
||||
elif "anal" in slug or "double" in slug or "penetrative" in slug:
|
||||
graph = f"{a} penetrates {b} anally while {b}'s hips are held open."
|
||||
@@ -5109,7 +5286,9 @@ def _role_graph(
|
||||
woman = any_woman()
|
||||
man = any_man()
|
||||
third = any_person({woman, man}) if people_count >= 3 else ""
|
||||
if "oral" in slug:
|
||||
if "outercourse" in slug:
|
||||
graph = outercourse_position_graph(woman, man)
|
||||
elif "oral" in slug:
|
||||
graph = oral_position_graph(woman, man)
|
||||
elif "anal" in slug or "double" in slug:
|
||||
if "double" in item_text or "toy" in item_text:
|
||||
@@ -5534,7 +5713,7 @@ def _build_custom_row(
|
||||
if subject_type == "configured_cast"
|
||||
else []
|
||||
)
|
||||
source_role_graph = _role_graph(role_rng, subcategory, context, item_axis_values)
|
||||
source_role_graph = _role_graph(role_rng, subcategory, context, item_axis_values, pov_character_labels)
|
||||
if is_pose_category:
|
||||
source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph)
|
||||
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)
|
||||
|
||||
Reference in New Issue
Block a user