# Making a character file (for people and AI assistants) This folder describes the file the Last Arc party app imports as one character (**Last Arc: Tactics Analogue**, rulebook RCPD v2.5.8). The usual job: the player already has a character made from the rulebook — a filled character sheet as a PDF, a photo or text — and an AI assistant moves it into this JSON so nobody types it in by hand. Give the assistant the sheet, this README, `reference.md` and `template.json`, and follow **Converting an existing character sheet** below. (No sheet yet? See **Building a new character** at the end.) The GM imports the JSON in the app (**Панель GM → Інструменти → Імпорт персонажа**); the app checks the file first and lists every problem before anything is saved. The files are also on the party site: `/seed/README.md`, `/seed/reference.md`, `/seed/template.json`. **Download these files in full** (curl, a file download, the player's attachment) and read them to the end. Do not use a web tool that returns a summary or excerpts of a page: you need the exact text, above all every table of `reference.md`. If you cannot get the full text, ask the player to attach the files. - `template.json` — a complete, legal level-1 Human Warrior to start from. - `reference.md` — every key the file may use (races, classes, skills, technicks, talents, spells, performances, items, languages, statuses), generated from the rules. - `sahir.json` — a full example (level-1 Nasha Bard with custom items and a story); in the project repository only, not on the site. ## The rules of the file 1. One JSON object, UTF-8, no comments, no trailing commas. 2. Store **choices, not results.** The app computes modifiers, defences, skill totals, attack and damage, HP / MP maximums, initiative, speed, load and every limit. Never write totals. 3. Rule things are stored by **key** exactly as in `reference.md` (`"race": "dark_elf"`, not "Dark Elf"). 4. Free text (name, bio, journal, notes, custom item names) may be in Ukrainian; rule names stay English. 5. Leave a field out or empty (`""`, `[]`, `{}`) when it does not apply. Unknown fields are reported and ignored. 6. The rules never block: something outside them imports with a warning. Aim for zero warnings anyway. ## Fields | field | type | meaning | |---|---|---| | `name` | text | Character name. | | `race` | key | A race key. | | `raceChoice.attr` | attribute key | Races with "+ to one" (Human, Half-Elf): the attribute that gets the bonus. | | `raceChoice.def` | `fort` / `ref` / `will` | Races with a trait that picks a defence (Half-Elf's Elven Heritage). | | `cls` | key | A class key. | | `level` | 1–20 | Character level. | | `xp` | number | Experience, at least the level's XP in the Levels table. | | `gender`, `align`, `adv`, `multi` | text | Gender, alignment (free text, e.g. "Neutral Good"), advanced class, multiclass notes (leave the last two empty for a new character). | | `attrs` | `{str, vit, agi, int, mnd, chr}` | **Base** scores (Point Buy or the standard array), before race and level-ups. | | `attrUp` | `{attr: number}` | Increases from levels (see *Levels*) and permanent item bonuses. | | `attrNotes` | text | Notes next to the attributes. | | `skills` | `{skill key: {t, f, b}}` | `t: true` trained, `f: true` Skill Focus (only with the `skill_focus` technick for that skill — set both), `b` a flat bonus (rarely needed). Only list skills that have one. | | `techs` | `[{key, note}]` | Chosen technicks. `note` holds the parameter: a weapon group for `weapon_proficiency`, a skill key for `skill_focus` / `skill_training`. Do **not** list the class's starting technicks. | | `tals` | `[{key}]` | Chosen talents from the class's trees. | | `magick` | `[{key}]` | Known spells and High Arcana. | | `perfs` | `[{key}]` | Known Bardic Performances. | | `langs` | `[name]` | Extra languages from the Languages list (the race's own are automatic). | | `armour`, `shield`, `instrument` | item key or `"custom"` | Worn armour, shield, instrument. | | `armourC` | `{name, group, ref, dr, maxAgi}` | With `armour: "custom"`: `group` light / heavy / mystic, `maxAgi` "" for no cap. | | `shieldC` | `{name, size, atk}` | With `shield: "custom"`: `atk` is the block bonus. | | `instrumentC` | `{name, atk}` | With `instrument: "custom"`: `atk` is the Perform bonus. | | `acc` | `[{key}]` or `[{key: "custom", name, desc, to, add}]` | Accessories (2 at most). A custom one may carry one modifier: `to` like `def.ref`, `skill.stealth`, `attack`, `dmg`, `speed`, `hpMax`. | | `attacks` | list | One row per weapon: `{item: weapon key}`; a custom weapon `{item: "custom", weapon: "Name", cgroup: group, csize: size, cdmg: "1d6", catk: 0}`; an unarmed or special attack `{weapon: "Claws", type, range, atk, dmg}` typed by hand. `grip` (`light` / `1h` / `2h`) only to override the default. | | `inventory` | `[{name, qty, bulk, item}]` | Carried items. `item` links a catalog key (`wooden_arrows`), a scroll `scroll:` or a score `score:`; use the item's name as `name`, `qty` = how many (copy the sheet's count), `bulk` 0 (the app knows it). A row without `item` is free text for something outside the catalog (a keepsake, a letter): its `bulk` as the sheet has it (a small one for a new character) and no price — agree such things with the GM. | | `gold` | number | Gold pieces. | | `hp.cur`, `mp.cur` | number | Current HP / MP: set them to the maximums the app shows (the import check prints them). `hp.max` / `mp.max` stay `""` (automatic). | | `hero` | number | Current Hero Points (the Levels table gives the daily amount). | | `condition` | index | 0 = Normal (see `reference.md`). | | `def..tech`, `.bonus` | number | Extra defence from technicks / anything else not automatic. Usually 0. | | `speed.tech`, `.bonus`, `.pen`; `dt.tech`, `.bonus` | number | Extra speed / damage threshold. Usually 0. | | `init`, `dr` | text / number | Leave `""`: initiative die and damage reduction are automatic (`dr` adds to armour). | | `senses`, `specialMove`, `immun`, `resist`, `vuln` | text | Only what does not come from the race. | | `racial` | `[{t, d}]` | Extra traits the race list does not have (title, description). | | `effects` | `[{name, status, mods, turns}]` | Temporary effects; usually `[]` for a new character. | | `spells`, `technicks`, `talents` | text | Free notes for abilities the catalogs do not cover. | | `bio`, `journal` | text | Story and session notes. | | `portrait` | text | Optional `data:image/jpeg;base64,…`. When converting a sheet, leave it out: the GM adds the picture in the app. | ## Converting an existing character sheet Transcribe, do not redesign: the character stays exactly as the sheet has it, even where it breaks the rules (the app only warns). Never "fix" a number so it looks right. Sheets show results; this file stores the choices behind them, so some numbers have to be worked back. **Bonus fields** (`skills..b`, `def..bonus`, `speed.bonus`, `dt.bonus`) hold the player's own bonuses that the app cannot know. Work out what the app adds by itself (*How the app computes combat numbers*, the race's trait effects, armour and shield, and the column *applied by the app* in *Technicks*); whatever the sheet still has on top of that in a bonus or misc column is the player's bonus — store it. A difference you cannot explain that way goes into the report, not into a bonus field. 1. **Identity.** `name`, `race`, `cls`, `level`, `xp`, `gender`, `align` from the sheet; race and class become keys (match the sheet's names against the *name* column of `reference.md`). 2. **Attributes.** The sheet normally shows **final** scores (base + race + level-ups). Store `attrs` = final − the race's adjustment (*Races*: `adjust`, plus the "+ to one" bonus on the attribute in `raceChoice.attr`) − `attrUp`. Put level-up increases (levels 4, 8, 12, 16, 20) in `attrUp` only when the sheet says which they were; otherwise leave `attrUp` empty and keep them in `attrs`. For a race with "+ to one" (Human, Half-Elf) the sheet may say which attribute got it; if not, pick the one that makes the base scores a legal Point Buy, or the highest score. A defence picked by a race trait goes to `raceChoice.def`. Check: the app's modifier is floor((score − 10) / 2) of the final score. 3. **Skills.** Mark `t: true` for skills the sheet marks as trained. If it has no marks, a skill whose total is 5 more than ½ level (rounded down) + its attribute modifier (+ race bonuses) is trained; 10 more means trained with Skill Focus (`f: true`, and the `skill_focus` technick with that skill in `note`). Lore, Perform and weapon skills each have their own key (`lore_arcane`, `ranged`…). 4. **Technicks and talents.** Every named technick and talent becomes its key in `techs` / `tals`. Leave out the class's **starting technicks** (column *starting technicks* in *Classes*): the app adds them. A technick with a parameter keeps it in `note` (`{"key": "weapon_proficiency", "note": "bows"}`, `{"key": "skill_focus", "note": "stealth"}`). 5. **Magick.** Spells and High Arcana → `magick`, Bardic Performances → `perfs`, by key. 6. **Languages.** Only the ones the race does not give (*Races*: `languages`), by name as in *Languages*. 7. **Gear.** Worn armour → `armour`, shield → `shield`, instrument → `instrument`, accessories → `acc`, each weapon → a row in `attacks` with `item`; ammunition, potions, tools, scrolls → `inventory` with `item` and `qty` (copy the count from the sheet; scrolls and scores keep the sheet's name). Match names against *Items*. A catalog weapon row needs only `item`: leave `type`, `range`, `atk`, `dmg` empty — typed values replace the app's. A weapon not in the catalog (claws, a fist) keeps them as text from the sheet (`"atk": "+3"`, `"dmg": "1d6-1"`). Something not in the catalog becomes `custom` with its typed stats (`armourC`, `shieldC`, `instrumentC`, a custom weapon or accessory row — see *Fields*) or a free-text inventory row. Money → `gold`. 8. **Current state.** `hp.cur`, `mp.cur`, `hero`, `condition` (index), `effects` as the sheet has them now. `hp.max`, `mp.max`, `init`, `speed.base` stay `""` — the app computes them. 9. **Everything else.** Story → `bio`; session notes → `journal`; extra traits the race list does not have → `racial` (`[{t, d}]`); abilities without a key → the notes `spells` / `technicks` / `talents`; the player's own remarks about attributes → `attrNotes` (leave out lines the app shows itself, such as the race's adjustments or Point Buy spent); extra senses, immunities, resistances → their text fields. 10. **Report.** After the JSON, list for the player: (a) everything you could not map to a key or field and where you put it instead; (b) numbers on the sheet that differ from what the app will compute (*How the app computes combat numbers* in `reference.md`, the HP / MP formula at the top of *Classes*); (c) rule breaks you noticed. The GM decides about them; the file stays true to the sheet. ## Building a new character (no sheet yet) 1. **Race and class.** Pick keys from `reference.md`. Note the race's size, speed, adjustments, "+ to one" choice, traits, languages; the class's trained-skill count, class skills, starting technicks, talent trees and starting gold. 2. **Attributes (Point Buy).** Every score starts at 7; the costs in the Point Buy table must add up to at most 35 (spend all 35: unspent points are lost). Or use the standard array 15, 14, 13, 12, 10, 8. These are base scores: the race adjustment and `raceChoice.attr` are added by the app. Keep final scores within the race's caps. Modifier = floor((score − 10) / 2). 3. **Trained skills.** Count = class `trained` + Int modifier (+1 for a Human's *Skilled*, and any other race bonus). Prefer class skills; skills marked "trained only" need `t: true` to be used at all. 4. **Technicks.** Allowed = one per level with "character technick" in the Levels table (+1 for a Human's *Versatile*) + class technicks on even levels (2, 4, …). Check each technick's prerequisites against the final attributes, trained skills and other technicks. 5. **Talents.** One on level 1 and on every odd level (3, 5, …), from the class's talent trees, respecting prerequisites (including other talents). 6. **Magick.** A class or technick with *Arcane Study* knows (1 + Int modifier, at least 1) spells per Arcane Study (the class's starting one counts, and so does each one chosen in `techs`); *Bardic Study* works the same for performances. Spells may come from any school. High Arcana (school `arcana`) do not count toward the limit, but they belong to the Archmage advanced class: leave them out of a new character. 7. **Languages.** The race's languages are automatic; add as many from the Languages list as the Int modifier. 8. **Levels above 1.** Set `xp` to the level's XP. At levels 4, 8, 12, 16, 20 add +1 to two different attributes in `attrUp`. Add the extra technicks and talents of each level. 9. **Equipment.** Spend the class's starting gold on catalog items available at that level: common from character level 1, uncommon from 4, rare from 8, exotic from 12. Put the rest in `gold`. Wear armour and a shield the character is proficient with (the class's starting technicks: `armour_proficiency_*`, `shield_proficiency`, `weapon_proficiency:`) and at most 2 accessories (no proficiency needed); add weapons to `attacks` and ammunition, tools and potions to `inventory`. *How the app computes combat numbers* in `reference.md` shows which skill each weapon uses, so train that skill. 10. **HP, MP, Hero Points.** Compute the maximums with the formula at the top of *Classes* in `reference.md` (final scores, after race) and put them into `hp.cur` / `mp.cur`; the GM's import check shows the app's numbers too. Hero Points = the daily amount of the level. 11. **Story.** `bio`, `gender`, `align`, maybe `journal` — in any language. ## Importing The GM opens **Панель GM → Інструменти → Імпорт персонажа**, picks the file and the owner. The app shows the character (name, race, class, level) and: - **errors** (red) — the file is not one JSON object or a field has the wrong shape; nothing is imported; - **warnings** (yellow) — unknown keys and every rule break the sheet itself would flag (Point Buy, trained limit, technick / talent limits and prerequisites, languages, accessories, HP over the maximum…). The file can still be imported (“Імпортувати попри …”); fix and re-import to get a clean sheet. After a rules change run `python tools/seed_reference.py` to refresh `reference.md`.