# Technical Architecture

## Init Priority Order

```
-10:  registry → companions_system → combat_system     (global dicts)
-5:   navigation → quest → shopping → storage → special_mechanics …
0:    gui, screens, content registration
1:    feat_runtime_hooks (late-bound references)
5:    everyturn rule registration, populate critter/combat tables, wire art
10:   sleep timer registration
100:  config.say_menu_text_filter hook (last, after all init blocks)
```

## Data Flow

Content `.rpy` files call `register_*()` → stored in global dicts → wrapped in data classes → consumed by engine systems → exposed via Godot-style action interface (`action_interface.rpy`) and game loop.

The action interface replaces the original text-menu `game_loop` with a visual panel: compass rose (directional movement), dynamic action buttons (context-sensitive), items tab (room pickups), and navigation tab (fast travel). Text history accumulates room descriptions.

## Key State Variables

| Variable | Purpose |
|---|---|
| `player` | All character state (~930-line Player class) |
| `current_location` | Current room (LocationData) |
| `turns` | Counts down to evacuation |
| `daytimer` | "day" / "night" |
| `inasituation` | Suppresses random encounters during scripted scenes |
| `_post_combat_scene` | Stashed victory/defeat label for dispatch after `combat_menu()` returns |
| `infectbypass` | Global flag: bypass Resistant feat check during forced infection |

Combat state (monsterHP, monsterLibido, fightoutcome, etc.) is reset in `prepforfight()` — effectively a closure over each fight.

## Registry Differences

- **`register_item()`** — **merges** with existing entries (allows partial definitions across engine, content, and equipment layers)
- **`register_quest()`** — writes to QUESTS_REGISTRY; uses `setdefault` for EVENTS_REGISTRY (preserves existing event data)
- **All other `register_*()`** — **overwrites** with a warning

## Combat: Attack Roll

`1d50 + combat_bonus > 20` to hit. `combat_bonus = (dex + level*2 + plhitbonus - 10) - (enemy.dex + enemy.level*2 + mondodgebonus - 10)`, capped at -22..19 (normal) or -25..16 (hard).

Damage = `weapon_damage × random(80, 120+level) / 100`, plus stat bonuses (str for melee, per for ranged) and feat multipliers (Weaponsmaster, Powerful, Mayhem).

9 absorbancy functions handle damage reduction for different contexts (normal, high, weak, head-only, body-only, AoE, no-armor, no-shield, no-feat).

## Combat Resolution & Scenes

When combat ends (monster HP ≤ 0 or player HP ≤ 0), the outcome is handled in `win()` or `lose()`. Victory/defeat scene labels are **stashed** into `_post_combat_scene` rather than called immediately, because `renpy.call()` cannot start a new interaction inside `renpy.display_menu()` callbacks. The dispatch happens in `_dispatch_post_combat_scene()` after `combat_menu()` returns to a RenPy label context.

Order of operations on player win: image swap (_hard→_soft) → scene plays → loot/XP awarded.
Order on player loss: image hide → `loss_rule()` (infection, unless scene handles it) → scene plays.

## Post-Combat Infection (`loss_rule`)

When the player loses a fight (`fightoutcome` 20/21/22), `loss_rule()` runs:

1. Looks up the enemy's `infection` field → `INFECTIONS_REGISTRY`
2. Checks `special.non_infectious` and `special.cross_infection` (I7 pattern: alternate strain on loss)
3. Respects Security Syringe (`sec_syringe`), warded infections, `resbypass`/`infectbypass`
4. Intensity scales by enemy level: 1-9→1 part, 10-14→2, 15+→3
5. Calls `transform_player()` — Resistant feat check skipped when `infectbypass` is True

If the enemy has a victory scene that calls `transform_player()` itself, `loss_rule()` is skipped (the scene handles infection). This matches the original I7 double-infection design.

## Image Rendering

Two persistent screens display images:

- `persistent_background` (zorder 0): location maps and enemy creatures, left side at 33% width
- `persistent_character` (zorder 0): NPC portraits, right side at 14% width

Images use `fit "contain"` with `xalign 0.5` for centered display. Image wiring is handled by `game_art.rpy` which maps enemy/NPC/location names to RenPy image tags via `Table_of_Game_Art`, `_npc_art`, and `_location_art`. Post-combat, `_swap_post_combat_image()` swaps `_hard` (combat pose) → `_soft` (defeated pose) for enemies that have both variants.

## Transformation: Core Flow

`transform_player(infection_id, intensity)` → check Resistant/Microwaved feats (skipped if `infectbypass` is True) → apply intensity modifiers → select uninfected body parts → call `transform_body_part()` per part → `apply_body_stats()` (gradual stat drift) → `attributeinfect()` (sync scale/bodydesc, unequip mismatched armor).

Transformation text reads `tf_text` from infection data, falling back to `change` (I7 field) for the 211 infections ported with the older field name.

## INFECTION_LISTS

Defined in `classes.rpy`. Maps species names to categories (InternalCockList, FelineList, etc.) for property checks like `player.is_furred`, `player.is_nocturnal`. Populated at init from infection tags, names, species aliases, and transformation metadata. A general `FurryList` catches explicitly furry infections even without taxonomy tags.

## Turn Processing (`pass_turn()`)

Executes in order: cock/cunt/breast descr rules → Sanity Saver → libido drift (Horny Bastard / Cold Fish) → HP regen → Singular feat → Passing Grade Chest → Always Cocky / Always A Pussy → ball oversize penalty → heat check → spontaneous orgasm → hunger/thirst increase → 3-turn periodic effects → corruption/humanity decay → pregnancy → game-over checks → date advance → everyturn hooks.

## Event System (`event_system.rpy`)

Parity with Inform's **Situations.i7x** architecture. Three separate event paths match Inform's three firing modes:

### Walkin Events (fires on location change)

`game_loop` calls `walkin_check()` after the location is established. Equivalent to Inform's `WalkInEvent_Check` ("after going" rule).

**Checks (all must pass):**
1. Event not already resolved (permanent via `renpy.store._persistent_event_resolved`)
2. Player level ≥ event `level` - 1 (level 0 = no restriction)
3. Cooldown expired: `(current_turn - last_encounter_turn) > cooldown`
4. Prerequisites met: `Prereq1/2/3` resolved, `PrereqTime` matches (Day/Night/Any), `PrereqCompanion` present
5. Encounter percentage roll succeeds: `rand(1-100) ≤ encounter_percentage`
6. Trigger condition met: `"random"` (1/40) or `"area:LocationName"` (case-insensitive match)

Only **one** event fires per location change (picks highest priority first).

### Scavenge Events (fires during scavenge)

`scavenging_system` calls `scavenge_check()` after successful scavenge. Equivalent to Inform's scavenge/hunt event flow. Same checks as walkin, plus a 1/4 chance roll (matching Inform's 3/4 resources, 1/4 event ratio).

### Rest Events (fires during rest)

Calls `rest_check()` during the rest action. Only fires for `"rest:LocationName"` triggers. Same checks as walkin.

### Trigger Formats

| Format | Path | Behavior |
|---|---|---|
| `"random"` | Walkin / Scavenge | 1/40 (walkin) or 1/4 (scavenge) chance |
| `"area:LocationName"` | Walkin | Fires when current location matches (case-insensitive) |
| `"rest:LocationName"` | Rest | Fires during rest at matching location |
| Missing or empty | — | Never auto-fires; dev console only |

### Scene Resolution

Events store scenes as a list of label names (e.g. `['my_event_scene']`). The engine picks the first scene and calls it via `renpy.call()`. After firing, the event is marked **resolved** — it will never fire again, matching Inform's "is resolved" semantics.

### Event Metadata Fields

Fields added to the event dictionary (defaults shown):
| Field | Default | Description |
|---|---|---|
| `level` | 0 | Minimum player level (0 = no restriction) |
| `cooldown` | 5 | Turns between firings at the same location |
| `encounter_percentage` | 50 | Chance 1-100 to fire when conditions are met |
| `prereqs` | `[]` | List of event IDs that must be resolved first |
| `prereq_time` | `"Any"` | `"Day"`, `"Night"`, or `"Any"` |
| `prereq_companion` | `null` | Companion ID that must be present |
| `priority` | 5 | Lower = higher priority when multiple eligible |

### Dev Console Integration

The dev console provides two ways to fire events manually (unchanged from original):
- **Event list buttons** — call `_dev_force_event_fixed(eid)` which jumps to `_dev_do_event` label, runs `renpy.call(scene_ref)` outside screen context, returns to `game_loop`.
- **Force Run by ID** — reads the input field at click time and calls the same flow.

The `_dev_do_event` label handles the call safely: checks `renpy.has_label()` first, catches exceptions, and sets feedback on the screen.
