# Inform Porting Workflow

This is the current prep workflow for moving Flexible Survival source content into the Ren'Py project.

## Fresh Baseline

- Date checked: `2026-06-12`
- Local runtime: `Ren'Py 8.5.3.26051504` at `renpy-8.5.3-sdk`
- Current local lint status: passes clean (0 errors, 0 warnings)

Re-run lint against the current SDK before porting a large batch.

## Prep Order

1. Point the manifest builder at the current Inform source path.
2. Generate a manifest with wolf-first sorting.
3. Confirm which source files still exist at the resolved paths.
4. Port content in playable clusters, not isolated records.
5. Lint and do an in-game pass after each batch.

## Manifest Builder

Use `tools/build_inform_manifest.py` to turn the current `story.ni` into a migration manifest.

Example:

```powershell
python tools/build_inform_manifest.py `
  --story-file "C:\path\to\Inform\story.ni" `
  --source-root "C:\path\to\inform-source-root" `
  --wolf-first `
  --output-json "docs\inform_source_manifest.json" `
  --output-md "docs\inform_source_manifest.md"
```

What it does:

- Reads `Book` and `Part` headings from the story file.
- Extracts every `Include X by Y.` line.
- Assigns a recommended Ren'Py target family:
  `Locations`, `Events`, `Quests`, `Enemies`, `Infections`, `NPCs`, `Companions`.
- Flags wolf-related source entries so they can be pulled first.
- Optionally tries to resolve real source file matches under the provided source root.

## Batch Rules

- Port by area plus dependencies.
- Keep each batch playable on its own.
- Treat `Quests & Events` as a split category. Event-like includes should not be forced into quest files.
- Treat `Monsters/Infections` as a review category. The manifest gives a recommendation, but some entries still need a manual pass.

## Wolf-First Rule

The first backlog pass should prefer wolf-related content. Use `wolf_first_port_queue.md` as the starting seed list, then refresh it from the generated manifest once the current Inform source path is available.

## I7 Text Conversion

Inform 7 uses bracket syntax for random text, conditionals, and text substitutions. These must be converted to Ren'Py Python expressions. Below is a reference covering the porting patterns currently documented from the converted content.

---

### 1. Random Text Choice

**I7:** `[one of]A[or]B[or]C[at random]`

**Ren'Py (inline in dialogue):**
```renpy
"Your [renpy.random.choice(['firm', 'cute', 'sexy', 'hot'])] bottom."
```
Use single quotes inside the `choice()` call so the outer dialogue double quotes don't conflict. For options containing apostrophes, use double quotes inside and escape the outer:
```renpy
# BAD — will crash (quote conflict):
"[renpy.random.choice([\"I'm ready\", \"Let's go\"])]"

# GOOD — single quotes inside, double quotes outside:
"I'm gonna show you a good time, babe,"   # just pick first option as plain text
```

**Ren'Py (in a python block for complex cases):**
```renpy
python:
    greeting = renpy.random.choice([
        "I'm gonna show you a good time, babe,",
        "I hope you're ready for this, toots,",
        "I bet you can't wait for this, hun,",
    ])
    renpy.say(None, greeting)
```

**I7:** `[purely at random]A[or]B[stopping]` — same as above, treat as `[one of]`/`[at random]`.

**I7:** `[as decreasingly likely outcomes]` — weighted random. Replace with flat `renpy.random.choice()`, or implement weighted logic in Python.

---

### 2. Player Anatomy Conditionals

These are the most common conditionals. The I7 checks creature type via table membership; Ren'Py checks player attributes directly.

| I7 expression | Ren'Py equivalent |
|---|---|
| `[if Player is female]` | `if player.cunt_count > 0:` |
| `[if Player is male]` | `if player.cock_count > 0:` |
| `[if Player is neuter]` | `if player.cock_count == 0 and player.cunt_count == 0:` |
| `[if Player is puremale]` | `if player.cock_count > 0 and player.cunt_count == 0:` |
| `[if Player is not female]` | `if player.cunt_count == 0:` |
| `[if Player is not male]` | `if player.cock_count == 0:` |

**Before (I7):**
```renpy
"pushing you onto your [if Player is female]back[else]stomach[end if]."
```

**After (Ren'Py):**
```renpy
if player.cunt_count > 0:
    "pushing you onto your back."
else:
    "pushing you onto your stomach."
```

**Before (I7 — complex block):**
```renpy
"[if Player is female]When one of the mushrooms pinches your clit, you scream in ecstasy[else if Player is male]You groan loudly as you climax, blowing your load onto the ground[end if]."
```

**After (Ren'Py):**
```renpy
if player.cunt_count > 0:
    "When one of the mushrooms pinches your clit, you scream in ecstasy."
elif player.cock_count > 0:
    "You groan loudly as you climax, blowing your load onto the ground."
```

---

### 3. Stat & State Conditionals

| I7 expression | Ren'Py equivalent |
|---|---|
| `[if HP of Player > 0]` | `if player.hp > 0:` |
| `[if HP of Player > N]` | `if player.hp > N:` |
| `[if Breast Size of Player > 0]` | `if player.breast_size > 0:` |
| `[if Cock Count of Player > 1]` | `if player.cock_count > 1:` |
| `[if Cunt Count of Player > 0]` | `if player.cunt_count > 0:` |
| `[if scalevalue of Player < 4]` | `if player.scalevalue < 4:` |
| `[if scalevalue of Player > 3]` | `if player.scalevalue > 3:` |
| `[if Libido of Player > 20]` | `if player.libido > 20:` |
| `[if Player is submissive]` | `if getattr(player, "submissive", False):` |
| `[if Player is MalePreferred]` | `if "Male Preferred" in getattr(player, "feats", []):` |
| `[if Player is FemalePreferred]` | `if "Female Preferred" in getattr(player, "feats", []):` |

**Pattern for generic `X of Player > N`:**
```renpy
# [if Strength of Player > 15]
if getattr(player, "strength", 0) > 15:

# [if humanity of Player < 50]
if player.humanity < 50:
```

---

### 4. Random Chance Conditionals

**I7:** `[if a random chance of X in Y succeeds]A[else]B[end if]`

**Ren'Py:**
```renpy
if renpy.random.randint(1, Y) <= X:
    "A"  # success branch
else:
    "B"  # failure branch
```

Example — `[if a random chance of 1 in 2 succeeds]`:
```renpy
if renpy.random.randint(1, 2) == 1:
    "Your opponent hits the ground with a loud thud."
else:
    "Driven back by you, the herm whimpers with unsatisfied need."
```

---

### 5. Inventory & Quest Checks

| I7 expression | Ren'Py equivalent |
|---|---|
| `[if carried of X > 0]` | `if has_item("X"):` |
| `[if carried of X > N]` | `if has_item("X", N):` |
| `[if quest Y is complete]` | `if is_quest_complete("Y"):` |
| `[if X is listed in feats of Player]` | `if "X" in getattr(player, "feats", []):` |
| `[if X is not listed in feats of Player]` | `if "X" not in getattr(player, "feats", []):` |

Use `has_feat(player, "X")` when you want the named helper already used elsewhere in the engine. Use direct membership checks when translating literal Inform conditions inline.

---

### 6. Text Substitutions (I7 "to say" phrases)

I7 "to say" phrases like `[gryphonfuck1]` or `[cock size desc of Player]` reference named text blocks. These were defined in the I7 source as:
```inform7
to say gryphonfuck1:
    say "The gryphon, victorious, pushes you onto your back..."
```

**Ren'Py conversion:** Define them as Python string variables at the top of the file:
```renpy
init python:
    gryphonfuck1 = "The gryphon, victorious, pushes you onto your back..."
```
Then reference inline: `"[gryphonfuck1]"` — Ren'Py interpolates the variable.

For I7 attribute descriptors like `[cock size desc of Player]`, `[Cock of Player]`, `[Skin of Player]`, `[breast size desc of Player]`, `[bodytype of Player]`, `[Cum Load Size of Player]`, `[Ball Size Adjective of Player]`, `[Balls]` — these need Python helper functions:
```renpy
init python:
    def cock_size_desc():
        length = player.cock_length
        if length <= 4: return "small"
        elif length <= 8: return "average"
        elif length <= 12: return "large"
        else: return "massive"
```
Then reference: `"your [cock_size_desc()] cock"`

When the Inform source changes only part of a sentence with a condition or substitution, keep the full line as one Ren'Py dialogue string and splice the variable fragment inline. Do not split the sentence into separate `say` lines unless the source really has separate dialogue beats. If the source fragment needs leading or trailing whitespace, include it inside the inline expression so the sentence still reads naturally after interpolation.

**Good:**
```renpy
"Knowing what he wants, you divest yourself of your gear and move onto all fours. Nuzzling between your cheeks, he [\"steers clear of your feminine entrance and instead\" if player.cunt_count > 0 else \"\"]licks across your crinkled hole."
```

**Bad:**
```renpy
"Knowing what he wants, you divest yourself of your gear and move onto all fours. Nuzzling between your cheeks, he"
if player.cunt_count > 0:
    "steers clear of your feminine entrance and instead"
"licks across your crinkled hole."
```

This produces three separate dialogue beats and is not equivalent to the Inform source.
When you inline the conditional fragment, keep any needed leading/trailing spaces inside the expression itself and do not force a period onto the line unless the Inform source actually ends the sentence there. If the source continues in the same sentence, leave the punctuation inside the full dialogue string, not on the conditional fragment.

---

### 7. I7-Only Constructs (Strip)

These have no direct Ren'Py equivalent. Strip them and re-implement with Python state if needed:

| I7 construct | Action |
|---|---|
| `[first time]` | Strip — use `renpy.seen_label()` or a boolean flag |
| `[only once]` | Strip — use a boolean flag |
| `[stopping]` | Strip — end of a `[purely at random]` block |
| `[one of]...[or]...[or]...[at random]` | Convert to `renpy.random.choice()` |
| `[as decreasingly likely outcomes]` | Strip — re-implement weighted random in Python |

---

### 8. Image References

**I7:** `project Figure of LatexFox_hard_icon;`

**Ren'Py:** Images are auto-declared in `images.rpy` with `char_` and `map_` prefixes. Wire them in `game_art.rpy`:
```renpy
art_table["enemy_latex_fox"] = "char_latexfox_hard"
art_table["latex fox"] = "char_latexfox_hard"
```
Post-combat, `_swap_post_combat_image()` handles `_hard` → `_soft` swaps automatically.

---

### 9. Complete Example: Porting a Victory Scene

**I7 source (`Werewolf.i7x`):**
```inform7
to say werewolfvictory:
    project Figure of Werewolf_hard_icon;
    say "The wolf woman slashes again with her claw...";
    if Player is female:
        say "She grinds her cunt into your mouth...";
    else if Player is male:
        say "She takes a hold of your cock and strokes...";
    CreatureSexAftermath "Player" receives "PussyFuck" from "Werewolf";
```

**Ren'Py port (`Werewolf.rpy`):**
```renpy
label werewolf_victory:

    "The wolf woman slashes again with her claw, ripping whatever shirt you had and knocking you to the ground."

    if player.cunt_count > 0:
        "She grinds her canine muff into your mouth while her tail hole presses against your nose."
    elif player.cock_count > 0:
        "She takes a hold of your nearly fully engorged Johnson and strokes up before rubbing back down."

    $ transform_player("werewolf", intensity=2)
    return
```

---

## Validation

Recommended local validation command:

```powershell
& 'F:\Users\dmssi\Downloads\renpy-8.5.3-sdk\lib\py3-windows-x86_64\python.exe' `
  'F:\Users\dmssi\Downloads\renpy-8.5.3-sdk\renpy.py' `
  'C:\Users\dmssi\Documents\GitHub\FS-Renpy' `
  lint
```

This matches `tools/release_all.ps1`: it resolves the project directory, then runs the SDK's `python.exe` against `renpy.py` with the project root as the first argument and `lint` as the subcommand.

Minimum batch verification:

- lint passes
- new game reaches the main loop
- new or changed locations can be entered
- related events or combat trigger from the intended path
- quest state does not repeat incorrectly
- victory and defeat scenes play after combat (test both winning and losing)
