# Theriapolis → Godot 4.6 Porting Guide
A companion to `README.md`. This document maps the React/HTML prototype onto a Godot 4.6 project, file by file, with code sketches for the parts that are not obvious. It assumes you've read the existing README and have a working Godot 4.6 install.
> **Scope.** This guide covers the character-creation wizard only — the same surface the prototype covers. It does not address how the resulting character is consumed by the rest of the game; that handoff is described under "The handoff contract" at the end.
---
## 0. The honest summary
The port is **a UI rebuild, not a translation.** What carries over verbatim:
- The four content JSON files in `data/`.
- The `state` shape used as the character record.
- All math (`abilityMod`, modifier summing, validation predicates).
- Step-gating rules and the wizard flow.
- Drag/drop *semantics* (pool→slot, slot→slot, slot→pool).
What gets rebuilt in Godot's idiom:
- All views (HTML/CSS → Control scenes + Theme).
- The hover-popover system (`TraitName`) → a custom `PopupPanel` Control.
- HTML5 drag-and-drop → Godot's `_get_drag_data` / `_can_drop_data` / `_drop_data`.
- The Tweaks panel — almost certainly delete it; bake one theme.
Plan on roughly: 1–2 days for scaffolding, ~1 day per step scene, 1–2 days for the popover and drag system, 2–3 days for theming the parchment look. Plus polish.
---
## 1. Suggested Godot project layout
```
res://
├── project.godot
├── data/ # Copy unchanged from prototype/data/
│ ├── clades.json
│ ├── species.json
│ ├── classes.json
│ └── backgrounds.json
├── fonts/ # Bundle these — do NOT rely on Google Fonts at runtime
│ ├── CormorantGaramond-Medium.ttf
│ ├── CormorantGaramond-MediumItalic.ttf
│ ├── CrimsonPro-Regular.ttf
│ ├── CrimsonPro-Italic.ttf
│ ├── CrimsonPro-SemiBold.ttf
│ └── JetBrainsMono-Regular.ttf
├── theme/
│ ├── parchment.tres # Main Theme resource
│ ├── parchment_styles.gd # Programmatic StyleBox factory (optional)
│ └── paper_grain.png # Baked grain background
├── autoload/
│ ├── content.gd # Loads the four JSON files; replaces data.jsx
│ ├── character.gd # Resource defining the character record
│ └── rules.gd # ABILITIES, SKILL_*, abilityMod(), validate()
├── scenes/
│ ├── wizard.tscn # Root; replaces app.jsx
│ ├── wizard.gd
│ ├── stepper.tscn # 7-tab header; replaces .stepper
│ ├── aside.tscn # Right-rail summary; replaces
│ ├── nav_bar.tscn # Bottom Back / Next / Confirm bar
│ ├── steps/
│ │ ├── step_clade.tscn / .gd
│ │ ├── step_species.tscn / .gd
│ │ ├── step_class.tscn / .gd
│ │ ├── step_background.tscn / .gd
│ │ ├── step_stats.tscn / .gd
│ │ ├── step_skills.tscn / .gd
│ │ └── step_review.tscn / .gd
│ └── widgets/
│ ├── codex_card.tscn # Reusable selectable card
│ ├── trait_chip.tscn # Replaces TraitName
│ ├── trait_popover.tscn # The hover hint window
│ ├── ability_token.tscn # Replaces .die / draggable score
│ ├── ability_slot.tscn # Replaces .slot
│ └── stat_strip.tscn # The 6-cell summary row
└── icon.svg
```
### Autoloads (Project Settings → Autoload)
| Name | Path | Singleton | Why |
|-------------|-------------------------|-----------|---------------------------------------|
| `Content` | `res://autoload/content.gd` | Yes | One-shot JSON loader. |
| `Rules` | `res://autoload/rules.gd` | Yes | Static tables and pure functions. |
| (no autoload for the character) | -- | -- | The character is a `Resource` instance owned by `wizard.tscn`. |
---
## 2. The data layer — concrete GDScript
### 2.1 `Character` resource
Mirrors the JS `state` shape exactly. Using `Resource` makes it serializable (`ResourceSaver.save`) and trivially debuggable in the inspector.
```gdscript
# autoload/character.gd
class_name Character
extends Resource
@export var clade_id: StringName = &"canidae"
@export var species_id: StringName = &""
@export var class_id: StringName = &"fangsworn"
@export var background_id: StringName = &"pack_raised"
@export var stat_method: StringName = &"array" # "array" | "roll"
@export var stat_pool: Array[int] = [15, 14, 13, 12, 10, 8]
@export var stat_assign: Dictionary = {} # { "STR": 15, ... }
@export var stat_history: Array[Dictionary] = [] # [{ vals: [int], ts: int }]
@export var chosen_skills: Array[StringName] = []
@export var name_text: String = ""
@export var portrait_style: StringName = &"silhouette"
signal changed # emit on any mutation
func patch(d: Dictionary) -> void:
for k in d:
set(k, d[k])
changed.emit()
```
Wizard steps connect to `changed` and re-render.
### 2.2 `Content` autoload — replaces `loadData()`
```gdscript
# autoload/content.gd
extends Node
var clades: Array = []
var species: Array = []
var classes: Array = []
var backgrounds: Array = []
func _ready() -> void:
clades = _load_json("res://data/clades.json")
species = _load_json("res://data/species.json")
classes = _load_json("res://data/classes.json")
backgrounds = _load_json("res://data/backgrounds.json")
func _load_json(path: String) -> Array:
var f := FileAccess.open(path, FileAccess.READ)
assert(f, "Missing %s" % path)
var parsed = JSON.parse_string(f.get_as_text())
return parsed if parsed is Array else []
func clade(id: StringName): return _find(clades, id)
func species_of(id: StringName): return _find(species, id)
func cls(id: StringName): return _find(classes, id)
func background(id: StringName): return _find(backgrounds, id)
func _find(arr: Array, id: StringName):
for o in arr:
if StringName(o.id) == id: return o
return null
```
### 2.3 `Rules` autoload — replaces `data.jsx` constants and helpers
```gdscript
# autoload/rules.gd
extends Node
const ABILITIES := ["STR", "DEX", "CON", "INT", "WIS", "CHA"]
const STANDARD_ARRAY := [15, 14, 13, 12, 10, 8]
const ABILITY_LABELS := {
"STR": "Strength", "DEX": "Dexterity", "CON": "Constitution",
"INT": "Intellect", "WIS": "Wisdom", "CHA": "Charisma",
}
const SKILL_ABILITY := {
"acrobatics": "DEX", "athletics": "STR",
# ... copy verbatim from src/data.jsx
}
const SKILL_LABEL := { ... } # copy from src/data.jsx
const SKILL_DESC := { ... } # copy from src/data.jsx
const LANGUAGES := { ... } # copy from src/data.jsx
const CLASS_CLADE_REC := {
"fangsworn": ["canidae", "felidae", "ursidae"],
# ... copy verbatim
}
func ability_mod(score: int) -> int:
return int(floor((score - 10) / 2.0))
func signed_str(n: int) -> String:
return "+%d" % n if n >= 0 else str(n)
# Validation per step. Mirrors the JS validate() in app.jsx.
# Returns "" if valid, otherwise an error message.
func validate_step(step: int, ch: Character) -> String:
match step:
0: return "" if ch.clade_id else "Pick a clade."
1: return "" if ch.species_id else "Pick a species."
2: return "" if ch.class_id else "Pick a calling."
3: return "" if ch.background_id else "Pick a background."
4:
var n := ch.stat_assign.size()
return "" if n == 6 else "Assign all six abilities (%d/6)." % n
5:
var c = Content.cls(ch.class_id)
var need: int = c.skills_choose if c else 0
var got := ch.chosen_skills.size()
return "" if got == need else "Pick exactly %d (%d/%d)." % [need, got, need]
6: return "" if ch.name_text.strip_edges() else "Enter a name."
return ""
```
> Tip: copy the full `SKILL_DESC`, `LANGUAGES`, and `TRAIT_READING` dictionaries straight from `src/data.jsx` — the JS object literal is valid as a GDScript dictionary with two find/replace passes (`:` is fine; you mostly need to swap `\n` strings and remove backticks).
---
## 3. Mapping React concepts to Godot
| React / web concept | Godot 4.6 equivalent |
|------------------------------------|--------------------------------------------------------------|
| Component (`StepClade`) | `Control`-derived scene + script |
| Props (`{ state, set }`) | Scene exports + a shared `Character` resource reference |
| `useState` | Plain `var` on the script + `signal` for change notifications |
| `useEffect` on dep change | Connect to `Character.changed`; gate work with diffs |
| `useMemo` | Cache in `_ready()` or recompute in `_process` only on dirty |
| Conditional render | `node.visible = ...` (avoid `queue_free` for steps; reuse) |
| `