Add hardcore outercourse actions
This commit is contained in:
@@ -735,6 +735,13 @@ and larger groups.
|
|||||||
cast, such as who penetrates, who receives oral, and who joins from the side.
|
cast, such as who penetrates, who receives oral, and who joins from the side.
|
||||||
It is currently most useful for `Hardcore sexual poses`.
|
It is currently most useful for `Hardcore sexual poses`.
|
||||||
|
|
||||||
|
For `Hardcore sexual poses`, `SxCP Hardcore Position Pool` also includes an
|
||||||
|
`outercourse` family and position checkboxes for `boobjob`, `testicle_sucking`,
|
||||||
|
`penis_licking`, and `footjob`. `SxCP Hardcore Action Filter` exposes
|
||||||
|
`outercourse_only` and `allow_outercourse` so these can be selected separately
|
||||||
|
from oral or penetration. If a man slot is set to `presence_mode=pov`, these
|
||||||
|
positions emit first-person wording from that participant's viewpoint.
|
||||||
|
|
||||||
Subcategories, templates, and axis values can declare cast constraints:
|
Subcategories, templates, and axis values can declare cast constraints:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"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_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_anal": "Allow anal subcategories.",
|
"allow_anal": "Allow anal subcategories.",
|
||||||
"allow_climax": "Allow cumshot/climax aftermath subcategories.",
|
"allow_climax": "Allow cumshot/climax aftermath subcategories.",
|
||||||
},
|
},
|
||||||
@@ -1784,6 +1785,7 @@ class SxCPHardcoreActionFilter:
|
|||||||
"allow_double": ("BOOLEAN", {"default": False}),
|
"allow_double": ("BOOLEAN", {"default": False}),
|
||||||
"allow_penetration": ("BOOLEAN", {"default": True}),
|
"allow_penetration": ("BOOLEAN", {"default": True}),
|
||||||
"allow_oral": ("BOOLEAN", {"default": True}),
|
"allow_oral": ("BOOLEAN", {"default": True}),
|
||||||
|
"allow_outercourse": ("BOOLEAN", {"default": True}),
|
||||||
"allow_anal": ("BOOLEAN", {"default": True}),
|
"allow_anal": ("BOOLEAN", {"default": True}),
|
||||||
"allow_climax": ("BOOLEAN", {"default": True}),
|
"allow_climax": ("BOOLEAN", {"default": True}),
|
||||||
},
|
},
|
||||||
@@ -1804,6 +1806,7 @@ class SxCPHardcoreActionFilter:
|
|||||||
allow_double,
|
allow_double,
|
||||||
allow_penetration,
|
allow_penetration,
|
||||||
allow_oral,
|
allow_oral,
|
||||||
|
allow_outercourse,
|
||||||
allow_anal,
|
allow_anal,
|
||||||
allow_climax,
|
allow_climax,
|
||||||
hardcore_position_config="",
|
hardcore_position_config="",
|
||||||
@@ -1815,6 +1818,7 @@ class SxCPHardcoreActionFilter:
|
|||||||
allow_double=allow_double,
|
allow_double=allow_double,
|
||||||
allow_penetration=allow_penetration,
|
allow_penetration=allow_penetration,
|
||||||
allow_oral=allow_oral,
|
allow_oral=allow_oral,
|
||||||
|
allow_outercourse=allow_outercourse,
|
||||||
allow_anal=allow_anal,
|
allow_anal=allow_anal,
|
||||||
allow_climax=allow_climax,
|
allow_climax=allow_climax,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -390,6 +390,142 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Outercourse and genital teasing",
|
||||||
|
"slug": "outercourse_sex",
|
||||||
|
"min_people": 2,
|
||||||
|
"min_women": 1,
|
||||||
|
"min_men": 1,
|
||||||
|
"inherit_expressions": false,
|
||||||
|
"inherit_compositions": false,
|
||||||
|
"weight": 1.0,
|
||||||
|
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
|
"expression_pools": ["hardcore_oral_expressions", "hardcore_messy_expressions"],
|
||||||
|
"compositions": [
|
||||||
|
{"text": "tight contact close-up with penis, hands, and face readable", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "side-profile non-penetrative contact frame with body geometry clear", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "POV-aligned close-up from the man's hips with the visible partner centered", "min_people": 2, "max_people": 2},
|
||||||
|
{"text": "low-angle close crop on breasts or feet around the penis", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "front-facing kneeling contact composition with eyes, mouth, and hands visible", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "mirror-reflected non-penetrative sex composition with contact point readable", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "waist-level explicit contact frame with natural skin compression visible", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "close candid creator-shot frame centered on non-penetrative genital contact", "min_people": 2, "max_people": 3}
|
||||||
|
],
|
||||||
|
"item_templates": [
|
||||||
|
"{outer_act} in {position}, with {contact_detail}, {hand_detail}, and {visibility}",
|
||||||
|
"{position} featuring {outer_act}, {body_contact}, {texture_detail}, and {angle}",
|
||||||
|
"{angle} view of {outer_act}, with {visibility}, {contact_detail}, and {expression_detail}",
|
||||||
|
"explicit non-penetrative sex pose: {outer_act}, {position}, {contact_detail}, and {visibility}",
|
||||||
|
"{outer_act} on {surface}, with {hand_detail}, {body_contact}, and {texture_detail}",
|
||||||
|
"{position} while {outer_act}, with {texture_detail}, {hand_detail}, and {visibility}"
|
||||||
|
],
|
||||||
|
"item_axes": {
|
||||||
|
"angle": [
|
||||||
|
"tight close-up",
|
||||||
|
"side-profile",
|
||||||
|
"front-facing",
|
||||||
|
"low-angle",
|
||||||
|
"overhead",
|
||||||
|
"POV-aligned",
|
||||||
|
"mirror-reflected",
|
||||||
|
"waist-level"
|
||||||
|
],
|
||||||
|
"body_contact": [
|
||||||
|
"hips held steady at contact height",
|
||||||
|
"knees braced close to the body",
|
||||||
|
"torso leaning forward into the contact",
|
||||||
|
"legs spread enough to keep the contact point visible",
|
||||||
|
"body angled so the penis, hands, and face stay readable",
|
||||||
|
"shoulders relaxed while the contact stays centered",
|
||||||
|
"lower body held still while the upper body works",
|
||||||
|
"close body alignment around the penis"
|
||||||
|
],
|
||||||
|
"contact_detail": [
|
||||||
|
"compressed soft tissue around the shaft",
|
||||||
|
"glans kept visible at the mouth or between lips",
|
||||||
|
"tongue and lips making clear contact",
|
||||||
|
"soles pressing from both sides of the shaft",
|
||||||
|
"toes curled around the shaft",
|
||||||
|
"balls held gently against the mouth",
|
||||||
|
"skin visibly compressed under fingers",
|
||||||
|
"wet contact visible on the penis"
|
||||||
|
],
|
||||||
|
"expression_detail": [
|
||||||
|
"focused downward gaze",
|
||||||
|
"eyes looking up from the contact point",
|
||||||
|
"mouth open in concentration",
|
||||||
|
"messy lips and steady eye contact",
|
||||||
|
"controlled hungry expression",
|
||||||
|
"soft flushed concentration",
|
||||||
|
"tongue visible during the tease",
|
||||||
|
"intense close-camera stare"
|
||||||
|
],
|
||||||
|
"hand_detail": [
|
||||||
|
"hands pressing breasts firmly together",
|
||||||
|
"fingers digging into soft breast flesh",
|
||||||
|
"one hand guiding the penis",
|
||||||
|
"both hands bracing the thighs",
|
||||||
|
"hands holding the ankles in place",
|
||||||
|
"one hand holding the base of the penis",
|
||||||
|
"hands cupping balls close to the mouth",
|
||||||
|
"fingers spreading the breasts around the shaft"
|
||||||
|
],
|
||||||
|
"outer_act": [
|
||||||
|
"boobjob with the penis squeezed between both breasts",
|
||||||
|
"titjob with the shaft compressed between breasts and the glans near the mouth",
|
||||||
|
"breast sex with hands pressing the breasts tightly around the penis",
|
||||||
|
"testicle sucking with lips around the balls",
|
||||||
|
"balls licking with tongue contact under the shaft",
|
||||||
|
"penis licking with tongue along the shaft and glans",
|
||||||
|
"slow tongue licking on the underside of the penis",
|
||||||
|
"footjob with both soles wrapped around the penis",
|
||||||
|
"footjob with toes curled around the shaft",
|
||||||
|
"feet stroking the penis while the legs frame the contact"
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
"kneeling boobjob position",
|
||||||
|
"seated titjob position",
|
||||||
|
"tight close-up breast-sex position",
|
||||||
|
"kneeling testicle-sucking position",
|
||||||
|
"low-angle balls-licking position",
|
||||||
|
"kneeling penis-licking position",
|
||||||
|
"side-profile penis-licking position",
|
||||||
|
"reclining footjob position",
|
||||||
|
"seated footjob position",
|
||||||
|
"POV footjob position"
|
||||||
|
],
|
||||||
|
"surface": [
|
||||||
|
"rumpled bed sheets",
|
||||||
|
"a wide couch",
|
||||||
|
"a velvet chair",
|
||||||
|
"floor cushions",
|
||||||
|
"a hotel bed",
|
||||||
|
"a soft rug",
|
||||||
|
"a low mattress",
|
||||||
|
"a private shower bench"
|
||||||
|
],
|
||||||
|
"texture_detail": [
|
||||||
|
"realistic skin compression",
|
||||||
|
"soft flesh squeezed around the shaft",
|
||||||
|
"matte skin texture visible in the close-up",
|
||||||
|
"wet lips and tongue contact",
|
||||||
|
"slight saliva shine on skin",
|
||||||
|
"toes pressing into the shaft",
|
||||||
|
"natural asymmetry and soft tissue movement",
|
||||||
|
"visible pressure marks from fingers or soles"
|
||||||
|
],
|
||||||
|
"visibility": [
|
||||||
|
"penis, breasts, and mouth clearly visible",
|
||||||
|
"shaft compressed between breasts",
|
||||||
|
"glans and lips visible at the same contact point",
|
||||||
|
"balls and mouth contact visible",
|
||||||
|
"tongue contact on the penis clearly visible",
|
||||||
|
"feet and penis centered in frame",
|
||||||
|
"soles and shaft contact clearly visible",
|
||||||
|
"explicit non-penetrative genital contact visible"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Anal and double penetration",
|
"name": "Anal and double penetration",
|
||||||
"slug": "anal_double_penetration",
|
"slug": "anal_double_penetration",
|
||||||
|
|||||||
+186
-7
@@ -299,6 +299,7 @@ HARDCORE_POSITION_FAMILY_CHOICES = [
|
|||||||
"any",
|
"any",
|
||||||
"penetrative",
|
"penetrative",
|
||||||
"oral",
|
"oral",
|
||||||
|
"outercourse",
|
||||||
"anal",
|
"anal",
|
||||||
"climax",
|
"climax",
|
||||||
"threesome",
|
"threesome",
|
||||||
@@ -308,6 +309,7 @@ HARDCORE_POSITION_FOCUS_CHOICES = [
|
|||||||
"keep_pool",
|
"keep_pool",
|
||||||
"penetration_only",
|
"penetration_only",
|
||||||
"oral_only",
|
"oral_only",
|
||||||
|
"outercourse_only",
|
||||||
"anal_only",
|
"anal_only",
|
||||||
"climax_only",
|
"climax_only",
|
||||||
"threesome_only",
|
"threesome_only",
|
||||||
@@ -331,6 +333,10 @@ HARDCORE_POSITION_KEY_CHOICES = [
|
|||||||
"straddled_oral",
|
"straddled_oral",
|
||||||
"spread_leg_oral",
|
"spread_leg_oral",
|
||||||
"chair_oral",
|
"chair_oral",
|
||||||
|
"boobjob",
|
||||||
|
"testicle_sucking",
|
||||||
|
"penis_licking",
|
||||||
|
"footjob",
|
||||||
"open_thighs",
|
"open_thighs",
|
||||||
"front_back",
|
"front_back",
|
||||||
]
|
]
|
||||||
@@ -338,6 +344,7 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
|||||||
"any": [
|
"any": [
|
||||||
"penetrative_sex",
|
"penetrative_sex",
|
||||||
"oral_sex",
|
"oral_sex",
|
||||||
|
"outercourse_sex",
|
||||||
"anal_double_penetration",
|
"anal_double_penetration",
|
||||||
"threesomes",
|
"threesomes",
|
||||||
"group_sex_orgy",
|
"group_sex_orgy",
|
||||||
@@ -345,6 +352,7 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
|||||||
],
|
],
|
||||||
"penetrative": ["penetrative_sex"],
|
"penetrative": ["penetrative_sex"],
|
||||||
"oral": ["oral_sex"],
|
"oral": ["oral_sex"],
|
||||||
|
"outercourse": ["outercourse_sex"],
|
||||||
"anal": ["anal_double_penetration"],
|
"anal": ["anal_double_penetration"],
|
||||||
"climax": ["cumshot_climax"],
|
"climax": ["cumshot_climax"],
|
||||||
"threesome": ["threesomes"],
|
"threesome": ["threesomes"],
|
||||||
@@ -368,6 +376,10 @@ 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",),
|
||||||
|
"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"),
|
"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"),
|
"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
|
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(
|
def _compose_item(
|
||||||
rng: random.Random,
|
rng: random.Random,
|
||||||
category: dict[str, Any],
|
category: dict[str, Any],
|
||||||
@@ -1008,15 +1097,20 @@ def _compose_item(
|
|||||||
fields = [key for _, key, _, _ in Formatter().parse(template) if key]
|
fields = [key for _, key, _, _ in Formatter().parse(template) if key]
|
||||||
unique_fields = list(dict.fromkeys(fields))
|
unique_fields = list(dict.fromkeys(fields))
|
||||||
axis_values: dict[str, str] = {}
|
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)
|
position_values = _compatible_entries(axes["position"], women_count, men_count)
|
||||||
axis_values["position"] = _entry_text(_weighted_choice(rng, position_values))
|
axis_values["position"] = _entry_text(_weighted_choice(rng, position_values))
|
||||||
for name in unique_fields:
|
for name in unique_fields:
|
||||||
if name in axis_values or name not in axes or not axes[name]:
|
if name in axis_values or name not in axes or not axes[name]:
|
||||||
continue
|
continue
|
||||||
values = _compatible_entries(axes[name], women_count, men_count)
|
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", ""))
|
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))
|
axis_values[name] = _entry_text(_weighted_choice(rng, values))
|
||||||
item_text = _format(template, axis_values).strip()
|
item_text = _format(template, axis_values).strip()
|
||||||
item_name = _item_name(item) or subcategory["name"]
|
item_name = _item_name(item) or subcategory["name"]
|
||||||
@@ -1664,6 +1758,7 @@ def _empty_hardcore_position_config() -> dict[str, Any]:
|
|||||||
"allow_double": True,
|
"allow_double": True,
|
||||||
"allow_penetration": True,
|
"allow_penetration": True,
|
||||||
"allow_oral": True,
|
"allow_oral": True,
|
||||||
|
"allow_outercourse": True,
|
||||||
"allow_anal": True,
|
"allow_anal": True,
|
||||||
"allow_climax": 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["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_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))
|
parsed[key] = not _is_false(parsed.get(key, True))
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
@@ -1707,6 +1802,7 @@ def _hardcore_position_summary(config: dict[str, Any]) -> str:
|
|||||||
("allow_double", "double"),
|
("allow_double", "double"),
|
||||||
("allow_penetration", "penetration"),
|
("allow_penetration", "penetration"),
|
||||||
("allow_oral", "oral"),
|
("allow_oral", "oral"),
|
||||||
|
("allow_outercourse", "outercourse"),
|
||||||
("allow_anal", "anal"),
|
("allow_anal", "anal"),
|
||||||
("allow_climax", "climax"),
|
("allow_climax", "climax"),
|
||||||
)
|
)
|
||||||
@@ -1750,6 +1846,7 @@ def build_hardcore_action_filter_json(
|
|||||||
allow_double: bool = False,
|
allow_double: bool = False,
|
||||||
allow_penetration: bool = True,
|
allow_penetration: bool = True,
|
||||||
allow_oral: bool = True,
|
allow_oral: bool = True,
|
||||||
|
allow_outercourse: bool = True,
|
||||||
allow_anal: bool = True,
|
allow_anal: bool = True,
|
||||||
allow_climax: bool = True,
|
allow_climax: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -1759,6 +1856,7 @@ def build_hardcore_action_filter_json(
|
|||||||
focus_family = {
|
focus_family = {
|
||||||
"penetration_only": "penetrative",
|
"penetration_only": "penetrative",
|
||||||
"oral_only": "oral",
|
"oral_only": "oral",
|
||||||
|
"outercourse_only": "outercourse",
|
||||||
"anal_only": "anal",
|
"anal_only": "anal",
|
||||||
"climax_only": "climax",
|
"climax_only": "climax",
|
||||||
"threesome_only": "threesome",
|
"threesome_only": "threesome",
|
||||||
@@ -1770,6 +1868,7 @@ def build_hardcore_action_filter_json(
|
|||||||
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_oral"] = bool(allow_oral)
|
config["allow_oral"] = bool(allow_oral)
|
||||||
|
config["allow_outercourse"] = bool(allow_outercourse)
|
||||||
config["allow_anal"] = bool(allow_anal)
|
config["allow_anal"] = bool(allow_anal)
|
||||||
config["allow_climax"] = bool(allow_climax)
|
config["allow_climax"] = bool(allow_climax)
|
||||||
|
|
||||||
@@ -1779,6 +1878,7 @@ def build_hardcore_action_filter_json(
|
|||||||
for enabled, family in (
|
for enabled, family in (
|
||||||
(config["allow_penetration"], "penetrative"),
|
(config["allow_penetration"], "penetrative"),
|
||||||
(config["allow_oral"], "oral"),
|
(config["allow_oral"], "oral"),
|
||||||
|
(config["allow_outercourse"], "outercourse"),
|
||||||
(config["allow_anal"], "anal"),
|
(config["allow_anal"], "anal"),
|
||||||
(config["allow_climax"], "climax"),
|
(config["allow_climax"], "climax"),
|
||||||
)
|
)
|
||||||
@@ -1790,6 +1890,10 @@ def build_hardcore_action_filter_json(
|
|||||||
if focus == "oral_only":
|
if focus == "oral_only":
|
||||||
config["allow_oral"] = True
|
config["allow_oral"] = True
|
||||||
config["allow_penetration"] = False
|
config["allow_penetration"] = False
|
||||||
|
elif focus == "outercourse_only":
|
||||||
|
config["allow_outercourse"] = True
|
||||||
|
config["allow_oral"] = False
|
||||||
|
config["allow_penetration"] = False
|
||||||
elif focus == "anal_only":
|
elif focus == "anal_only":
|
||||||
config["allow_anal"] = True
|
config["allow_anal"] = True
|
||||||
config["allow_penetration"] = 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"})
|
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
||||||
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):
|
||||||
|
allowed.discard("outercourse_sex")
|
||||||
if not config.get("allow_anal", True):
|
if not config.get("allow_anal", True):
|
||||||
allowed.discard("anal_double_penetration")
|
allowed.discard("anal_double_penetration")
|
||||||
if not config.get("allow_climax", True):
|
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"))
|
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
|
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 (
|
if not config.get("allow_penetration", True) and (
|
||||||
axis_name in ("penetration_act", "penetration_detail", "anal_act", "double_act", "thrust_detail")
|
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"))
|
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],
|
subcategory: dict[str, Any],
|
||||||
context: dict[str, str],
|
context: dict[str, str],
|
||||||
item_axis_values: dict[str, str] | None = None,
|
item_axis_values: dict[str, str] | None = None,
|
||||||
|
pov_labels: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
if context.get("subject_type") != "configured_cast":
|
if context.get("subject_type") != "configured_cast":
|
||||||
return ""
|
return ""
|
||||||
@@ -4840,6 +4952,7 @@ def _role_graph(
|
|||||||
people = participants["people"]
|
people = participants["people"]
|
||||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
||||||
item_text = " ".join((item_axis_values or {}).values()).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:
|
def any_person(exclude: set[str] | None = None) -> str:
|
||||||
exclude = exclude or set()
|
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} 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."
|
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:
|
def oral_position_graph(woman: str, man: str) -> str:
|
||||||
position_text = str((item_axis_values or {}).get("position") or "").lower()
|
position_text = str((item_axis_values or {}).get("position") or "").lower()
|
||||||
text = " ".join(
|
text = " ".join(
|
||||||
@@ -5073,7 +5246,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 "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."
|
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:
|
elif "anal" in slug or "double" in slug:
|
||||||
graph = f"{a} uses a strap-on on {b} while keeping her hips held open."
|
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)
|
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 "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."
|
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:
|
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."
|
graph = f"{a} penetrates {b} anally while {b}'s hips are held open."
|
||||||
@@ -5109,7 +5286,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 "oral" in slug:
|
if "outercourse" in slug:
|
||||||
|
graph = outercourse_position_graph(woman, man)
|
||||||
|
elif "oral" in slug:
|
||||||
graph = oral_position_graph(woman, man)
|
graph = oral_position_graph(woman, man)
|
||||||
elif "anal" in slug or "double" in slug:
|
elif "anal" in slug or "double" in slug:
|
||||||
if "double" in item_text or "toy" in item_text:
|
if "double" in item_text or "toy" in item_text:
|
||||||
@@ -5534,7 +5713,7 @@ def _build_custom_row(
|
|||||||
if subject_type == "configured_cast"
|
if subject_type == "configured_cast"
|
||||||
else []
|
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:
|
if is_pose_category:
|
||||||
source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph)
|
source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph)
|
||||||
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)
|
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)
|
||||||
|
|||||||
Reference in New Issue
Block a user