"""Per-location special mechanics from DETAILS.md §3.""" from __future__ import annotations import json import random from world.book_of_ages import create_entry, get_entries from world.database import db_session from world.entities import add_tag, get_entity, update_memory from world.locations import get_location_by_slug def effective_days_in_realm(entity: dict) -> float: """Clock Forest souls age 1.5× faster for lifecycle logic.""" loc = entity.get("location_slug") if loc is None and entity.get("location_id"): from world.locations import get_location_by_id row = get_location_by_id(entity["location_id"]) loc = row["slug"] if row else None days = entity.get("days_in_realm", 0) if loc == "clock-forest": return days * 1.5 return float(days) def process_library_memory_fragments(world_day: int) -> int: """Every 3 days, Library inhabitants gain a half-remembered fragment.""" if world_day % 3 != 0: return 0 library = get_location_by_slug("library") if not library: return 0 past = get_entries(limit=30, entry_type=None) if not past: return 0 fragment_source = random.choice(past) snippet = (fragment_source.get("content") or "")[:120].strip() if not snippet: return 0 updated = 0 with db_session() as conn: rows = conn.execute( "SELECT * FROM entities WHERE location_id = ? AND status != 'dormant'", (library["id"],), ).fetchall() for row in rows: entity = dict(row) current = entity.get("memory_summary") or "" addition = f"They half-remember something from the shelves: {snippet}" merged = f"{current} {addition}".strip() if len(merged) > 600: merged = merged[-600:] update_memory(entity["id"], merged) updated += 1 if updated: create_entry( world_day=world_day, entry_type="milestone", content=( f"The Library breathed out {updated} half-forgotten fragments " "into the minds of those who dwell among unfinished books." ), location_id=library["id"], is_milestone=False, ) return updated def process_mirror_contradictions(world_day: int) -> int: """Mirror Bogs: after 5+ interactions, a soul may develop a contradicting trait.""" bogs = get_location_by_slug("mirror-bogs") if not bogs: return 0 count = 0 with db_session() as conn: entities = conn.execute( "SELECT * FROM entities WHERE location_id = ?", (bogs["id"],), ).fetchall() opposites = [ ("patient", "restless"), ("gentle", "sharp-tongued"), ("honest", "evasive"), ("hopeful", "cynical"), ("quiet", "loud"), ("generous", "possessive"), ] for row in entities: entity = dict(row) traits = json.loads(entity["personality_traits"]) if isinstance(entity["personality_traits"], str) else entity["personality_traits"] tags = json.loads(entity["tags"]) if isinstance(entity["tags"], str) else entity["tags"] if "contradiction" in tags: continue n = conn_count_interactions_at(entity["id"], bogs["id"]) if n < 5: continue pick_trait = random.choice(traits) opposite = next((b for a, b in opposites if a in pick_trait.lower()), None) if not opposite: opposite = f"secretly unlike their {pick_trait} nature" new_traits = traits[:] idx = traits.index(pick_trait) new_traits[idx] = f"{pick_trait}, yet {opposite}" with db_session() as conn: conn.execute( "UPDATE entities SET personality_traits = ? WHERE id = ?", (json.dumps(new_traits), entity["id"]), ) add_tag(entity["id"], "contradiction") create_entry( world_day=world_day, entry_type="milestone", content=( f"{entity['name']} looked into the bog and came away disagreeing " f"with who they had been." ), entity_ids=[entity["id"]], location_id=bogs["id"], is_milestone=True, title="A Contradiction Surfaces", ) count += 1 return count def conn_count_interactions_at(entity_id: str, location_id: int) -> int: with db_session() as conn: row = conn.execute( """ SELECT COUNT(*) AS c FROM interactions WHERE location_id = ? AND (entity_a_id = ? OR entity_b_id = ?) """, (location_id, entity_id, entity_id), ).fetchone() return row["c"] if row else 0 def apply_moon_market_memory_trade( entity_a: dict, entity_b: dict, location_slug: str, memory_a: str, memory_b: str, ) -> tuple[str, str]: """Moon Market: traded memory fragments bleed between two souls.""" if location_slug != "moon-market": return memory_a, memory_b frag_a = (entity_a.get("memory_summary") or "").split(". ")[-1][:80] frag_b = (entity_b.get("memory_summary") or "").split(". ")[-1][:80] if frag_b and frag_b not in memory_a: memory_a = f"{memory_a} Carries a traded fragment: {frag_b}.".strip() if frag_a and frag_a not in memory_b: memory_b = f"{memory_b} Carries a traded fragment: {frag_a}.".strip() return memory_a[:600], memory_b[:600]