|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 3 | +// |
| 4 | +// JessicaBackground -- the operative-background numeric core extracted from |
| 5 | +// src/app/player/JessicaBackground.res. The rank/XP progression tables |
| 6 | +// (rankValue, rankXpThreshold, canPromote) already live in the SkillRank |
| 7 | +// co-processor and are NOT duplicated here; this binding carries the remaining |
| 8 | +// numeric tables that switch on the `background` variant: the per-background |
| 9 | +// attribute-bonus matrix that getBonus folds, and the starting-skill grant that |
| 10 | +// makeSkillSet applies (which skill category a background seeds at Trained). |
| 11 | +// |
| 12 | +// Per the DESIGN-VISION ("AffineScript is the brain, the JS/Pixi host the |
| 13 | +// senses; only primitives cross the wasm boundary"), the host keeps every |
| 14 | +// string (display names, descriptions), the attributeBonus and skill records, |
| 15 | +// the skillSet construction, and the variant tags. AffineScript owns only the |
| 16 | +// scalar tables. The `background` variant collapses to its JessicaBackground.all |
| 17 | +// index (0 Assault .. 5 Logistics), exactly the dense tag order the .res.mjs |
| 18 | +// emits; each attribute collapses to a column ordinal; each skill category to |
| 19 | +// its JessicaBackground skillCategory index. |
| 20 | +// |
| 21 | +// getBonus returns a six-field float record per background. A record cannot |
| 22 | +// cross the boundary, so it re-decomposes into a single bonus(background, attr) |
| 23 | +// kernel selected by an attribute-column ordinal; the host reassembles the six |
| 24 | +// columns into its attributeBonus record. The values are the baseline shifts |
| 25 | +// added to the base 100.0 — ALL of the bonus shifts are whole numbers (e.g. |
| 26 | +// Assault: str+20, dex+10, con+15, int_-5, wil+5, cha-5), so they cross as Int |
| 27 | +// without loss. The host receives an integer shift and applies it to the float |
| 28 | +// base (100.0 + shift). The signed integer suffices and avoids Float literals |
| 29 | +// in the wasm codegen, which the v0.1.1 backend does not lower cleanly to f64 |
| 30 | +// at the ABI level. |
| 31 | +// |
| 32 | +//## Background encoding (the header contract for the JS host) |
| 33 | +// Backgrounds index the JessicaBackground.all array order: |
| 34 | +// 0 Assault 1 Recon 2 Engineer 3 Signals 4 Medic 5 Logistics |
| 35 | +// An off-domain background index yields 0 from bonus (a neutral shift) and -1 |
| 36 | +// from start_skill (a sentinel the host reads as "no grant"). |
| 37 | +// |
| 38 | +//## Attribute-column encoding (the second contract, for the bonus selector) |
| 39 | +// Columns index the attributeBonus record field order: |
| 40 | +// 0 str 1 dex 2 con 3 int_ 4 wil 5 cha |
| 41 | +// An off-domain column yields 0. |
| 42 | +// |
| 43 | +//## Skill-category encoding (the third contract, for the starting grant) |
| 44 | +// Categories index the JessicaBackground skillCategory variant order: |
| 45 | +// 0 Infiltration 1 CombatSkill 2 Athletics 3 Observation |
| 46 | +// 4 TechLiteracy 5 Fieldcraft 6 Composure |
| 47 | +// start_skill returns the category index a background seeds at Trained, or -1 |
| 48 | +// when the background is off-domain. |
| 49 | +// |
| 50 | +// PURE: integers only, no floats, no strings, no arrays, no effects, no I/O. |
| 51 | +// Each entry point is a flat sequence of guarded returns rather than nested |
| 52 | +// if/else, which keeps the wasm backend's parser within its nesting tolerance. |
| 53 | + |
| 54 | +//## Attribute bonus for the STR column |
| 55 | +// All shifts are whole numbers; crossing as Int preserves the value exactly. |
| 56 | +fn bonus_str(background: Int) -> Int { |
| 57 | + if background == 0 { return 20; } |
| 58 | + if background == 1 { return -5; } |
| 59 | + if background == 2 { return 5; } |
| 60 | + if background == 3 { return -5; } |
| 61 | + if background == 4 { return 0; } |
| 62 | + if background == 5 { return 10; } |
| 63 | + 0 |
| 64 | +} |
| 65 | + |
| 66 | +//## Attribute bonus for the DEX column |
| 67 | +fn bonus_dex(background: Int) -> Int { |
| 68 | + if background == 0 { return 10; } |
| 69 | + if background == 1 { return 15; } |
| 70 | + if background == 2 { return 5; } |
| 71 | + if background == 3 { return 5; } |
| 72 | + if background == 4 { return 5; } |
| 73 | + if background == 5 { return 0; } |
| 74 | + 0 |
| 75 | +} |
| 76 | + |
| 77 | +//## Attribute bonus for the CON column |
| 78 | +fn bonus_con(background: Int) -> Int { |
| 79 | + if background == 0 { return 15; } |
| 80 | + if background == 1 { return 0; } |
| 81 | + if background == 2 { return 5; } |
| 82 | + if background == 3 { return 0; } |
| 83 | + if background == 4 { return 20; } |
| 84 | + if background == 5 { return 10; } |
| 85 | + 0 |
| 86 | +} |
| 87 | + |
| 88 | +//## Attribute bonus for the INT column |
| 89 | +fn bonus_int(background: Int) -> Int { |
| 90 | + if background == 0 { return -5; } |
| 91 | + if background == 1 { return 10; } |
| 92 | + if background == 2 { return 15; } |
| 93 | + if background == 3 { return 10; } |
| 94 | + if background == 4 { return 10; } |
| 95 | + if background == 5 { return 5; } |
| 96 | + 0 |
| 97 | +} |
| 98 | + |
| 99 | +//## Attribute bonus for the WIL column |
| 100 | +fn bonus_wil(background: Int) -> Int { |
| 101 | + if background == 0 { return 5; } |
| 102 | + if background == 1 { return 10; } |
| 103 | + if background == 2 { return 5; } |
| 104 | + if background == 3 { return 10; } |
| 105 | + if background == 4 { return 10; } |
| 106 | + if background == 5 { return 0; } |
| 107 | + 0 |
| 108 | +} |
| 109 | + |
| 110 | +//## Attribute bonus for the CHA column |
| 111 | +fn bonus_cha(background: Int) -> Int { |
| 112 | + if background == 0 { return -5; } |
| 113 | + if background == 1 { return 0; } |
| 114 | + if background == 2 { return -5; } |
| 115 | + if background == 3 { return 10; } |
| 116 | + if background == 4 { return 0; } |
| 117 | + if background == 5 { return 10; } |
| 118 | + 0 |
| 119 | +} |
| 120 | + |
| 121 | +//## Attribute bonus selector |
| 122 | +// bonus(background, attr): getBonus re-decomposed to a single signed integer |
| 123 | +// shift per (background, attribute-column) pairing. The host calls this six |
| 124 | +// times, once per column ordinal, and applies each shift to the float base |
| 125 | +// (100.0 + shift). Off-domain column yields 0; off-domain background yields 0 |
| 126 | +// from each column kernel. |
| 127 | +pub fn bonus(background: Int, attr: Int) -> Int { |
| 128 | + if attr == 0 { return bonus_str(background); } |
| 129 | + if attr == 1 { return bonus_dex(background); } |
| 130 | + if attr == 2 { return bonus_con(background); } |
| 131 | + if attr == 3 { return bonus_int(background); } |
| 132 | + if attr == 4 { return bonus_wil(background); } |
| 133 | + if attr == 5 { return bonus_cha(background); } |
| 134 | + 0 |
| 135 | +} |
| 136 | + |
| 137 | +//## Starting-skill grant |
| 138 | +// makeSkillSet re-decomposed. Each background seeds exactly one skill category |
| 139 | +// at Trained rank; this returns that category's index. Mirrors the switch: |
| 140 | +// Assault -> CombatSkill(1) Recon -> Observation(3) |
| 141 | +// Engineer -> TechLiteracy(4) Signals -> TechLiteracy(4) |
| 142 | +// Medic -> Composure(6) Logistics -> Fieldcraft(5) |
| 143 | +// Off-domain background yields -1 (no grant), distinct from any valid 0..6 index. |
| 144 | +pub fn start_skill(background: Int) -> Int { |
| 145 | + if background == 0 { return 1; } |
| 146 | + if background == 1 { return 3; } |
| 147 | + if background == 2 { return 4; } |
| 148 | + if background == 3 { return 4; } |
| 149 | + if background == 4 { return 6; } |
| 150 | + if background == 5 { return 5; } |
| 151 | + -1 |
| 152 | +} |
0 commit comments