Items And Classes Contract - xopherdeep/do-it-for-the-xp GitHub Wiki
Items and Classes Contract (2026-10-10, items rewritten 2026-10-13)
What every equipped item does, where, and what each hero class changes. Rule zero, which keeps the family's trust in the app: no item and no Power ever completes a chore or changes its rewards, and no class ever completes a chore (a class passive may add a bonus on top of the Game Master's reward: section C). Items are in-game tools: in a fight they change time, pressure and the hero's own body, in a temple they open the way. The chore is still done in the real world and still approved by the Game Master. Anything that bends a real chore or a house rule is a Power the Game Master prices (section B).
Shared facts: items live in UserHud/components/equipment/EquipmentItems.ts (catalogue), on the profile as
plain copies (user.equipment.leftHand[0] / rightHand[0]), and the live hands come from
src/hooks/useHeroEquipment.ts. What an item costs and does is one table, src/lib/services/items/ItemRules.ts,
read by item name: its MP, its temple use, its fight effect, the pack item it spends and the two lines its card
shows. The stored copy's desc is never read for a rule, so an old Ice Wand ("50MP | ...") costs what the table
says. The table in prose is docs/Equipment-Mechanics.md. Temple use is src/lib/services/temples/ItemUse.ts
(docs/Temple-Interactions-Contract.md). The battle engine is src/lib/services/battle/BattleService.ts (chores
are the hero's swings, the beast strikes on a clock, the beast keeps 1 HP until the last chore).
A. In battle — src/lib/services/battle/BattleItems.ts
The battle menu is Tackle | Defend, Powers | Goods, Run Away. Goods opens My Goods in battle mode
(XpInventoryModal mode="battle", the same sheet as the Goods page): one row for each thing in the pack and each
tool held in one of the two hands, with its effect line, its MP chip and a Use button. A row that cannot be
used is disabled and says why in a red tag.
Hands only. Only a tool held in a hand can be used in a fight. A tool the hero has unlocked but does not hold reads "Hold it in a hand before a fight." (swap the hands before the fight; the sheet has no hand slots). Potions and food still drink from the pack, Bottles and Goods (when held) open the potions sheet and the Book Of Spells opens the Powers sheet. The reasons, in the order they are asked: the unlock hint of a locked item, "Hold it in a hand before a fight.", "Already used this fight" (or the engine's own wait: "The clock is not running.", "A strike is already on its way."), "Out of bombs.", "Not enough MP". Using one costs the MP (the list price with the hero's class discount taken) or, for the Bombs, one Bomb from the pack; it plays a sound, shows one dialog line, and hands the move back to the hero (it does not end the turn). Nothing is charged on a refusal.
The effects live in one shared table, BattleEffects.ts, so a Power and its matching item behave the same. There
are eleven, and every one is once per fight, counted per effect whoever cast it (Chronos' Clock and the Stop
Power share one Time Stop; Bombs and Thor's Hammer share one Stun). The full rules, the engine verbs and the sheets
are in docs/superpowers/specs/2026-10-13-equipment-rules-design.md (5) and
docs/superpowers/specs/2026-10-12-battle-powers-and-bag-design.md; the fight contract is section H of
docs/Battle-Field-Contract-v2.md.
| Item | MP | In battle | Effect | Engine call |
|---|---|---|---|---|
| Ice Wand | 10 | +10 minutes on the deadline clock. | frost |
extendClock(ms) |
| Chronos' Clock | 20 | Pauses the clock for 15 minutes (the beast cannot strike while paused). | time-stop |
freezeClock(ms) |
| Gale's Wind | 15 | Refills the clock to full (refused when the clock is not running). | gust |
refillClock() |
| Fire Wand | 10 | Burns the fuse: each chore earns 1.5x the minutes for the rest of the fight. | kindle |
scaleCooldown(1.5) |
| Thor's Hammer | 15 | The beast is stunned: it skips its next strike. | stun |
skipNextStrike() |
| Bombs | a Bomb | The same Stun, paid with one Bomb from the pack instead of MP. | stun |
skipNextStrike() |
| Silver Arrow | 25 | The beast is pinned: it skips its next two strikes. | pin |
skipNextStrike() twice |
| Sol's Flare | 20 | Halves the beast's attack for the rest of the fight. | weaken |
weakenEnemy(0.5) |
| X's Invisibility | 15 | Run away with no parting strike: the page runs Run Away at once. Refused while a strike is already on its way. | vanish |
vanishNextRun() |
| Tesla's Bolt | 5 | Chores give back double MP for the rest of the fight. | charge |
a flag choreGains reads |
| Night's Light | 10 | Each chore heals 5% of max HP for the rest of the fight. | moonlight |
a flag choreGains reads |
| Bottles, Goods | 0 | Open the potion sheet: Health Potion, Mana Potion, Ether, Elixir, Spring Water and Silver Water heal or restore and are spent from the pack. No MP, no once-per-fight limit. | drinkPotion (BattlePotions.ts) |
|
| Book Of Spells | 0 | Opens the Powers sheet (the hero's in-game Powers, cast with MP). | castBattlePower (BattlePowers.ts) |
|
| Everything else (Lantern, Hook Shot, Gaia's Quake, Pegasus Boots, Wind Whistle, the navigation items) | "Can't use that in a fight." The Use button is disabled and shows that reason. |
The eleventh effect, heal (25% of max HP), belongs to Powers only. No item gives a chore back to the board: the
old Silver Arrow and the Rewind Power's recall are retired (a stored recall reads as gust), and the chore
checklist has no postpone mode. A family's Rewind saved before this keeps its old description text until the Game
Master edits it; its effect is gust either way.
MP comes back. A chore finished in a fight that holds a quest gives back 10% of max MP (at least 1; charge
doubles it) and, under moonlight, heals 5% of max HP. That includes a quest ambush (an overdue quest's beast
springing at a random encounter): it carries its quest. A free fight (a random ambush with no quest behind it, Test
Fight) gives nothing back, because its moves are not real chores. The rule is src/lib/services/battle/choreGains.ts (pure); the page
applies it as each chore completes.
useBattleItem(item, ctx) is pure apart from the store and the service: it returns { ok, line, sound } like
ItemUse.useItem does, runs the item's effect through applyBattleEffect and charges the MP (or the Bomb) only on
ok. ItemRules.spec.ts, BattleItems.spec.ts, BattleEffects.spec.ts and choreGains.spec.ts cover every row
above plus the hands, the Bombs, MP refusal and once-per-fight, and the Cypress walk-through
(tests/e2e/specs/battle.spec.js) uses the Ice Wand, sees Sol's Flare refused as not held, drinks a Health Potion
and casts the Stop Power.
B. On the quest board — real-life bends are Powers
Items have no quest-board effect, except the Lantern's overdue list. Tapping a tool in the portal (outside a
temple or a fight) shows its card: its MP, its temple use, its fight use. The Lantern also lists the hero's
overdue quests, free (lanternOverdue in src/lib/services/quests/ItemEffects.ts); it reads the board and never
writes it. Items write no questEffects, set no cooldown and need no quest picker. Heroes keep what the old items
once wrote (questEffects, itemCooldowns, unlockedItems): every quest effect is timed, so it runs out, and
nothing is migrated or removed.
The real-life bends are Powers the Game Master prices (frequency, MP and AP, all the Power's own). A real-life
Power carries Ability.boardEffect ('snooze' | 'extra-time' | 'quiet-day' | 'trusted-hour' | 'none', picked in
the Compendium editor's On the quest board select; the presets below start with theirs). The effects live in
src/lib/services/abilities/PowerBoardEffects.ts, which writes the same questEffects fields the old items did:
| Power (preset) | On the quest board | Refused when | Frequency | MP | AP |
|---|---|---|---|---|---|
| Snooze | One open quest moves to tomorrow (snoozedOn). |
That quest is already snoozed today. | Weekly | 10 | 30 a week |
| Extra Time | +24 hours on one open quest's deadline (extendedUntil). |
The quest already has an extension that has not passed (no stacking). | Weekly | 10 | 30 a week |
| Quiet Day | No ambushes for 24 hours (noEncountersUntil). |
Weekly | 15 | 40 a week | |
| Trusted Hour | For one hour, claims skip the approval queue (trustedUntil). |
The family setting trustedHour is off. |
Weekly | 20 | 60 a week |
Order of a cast (src/app/Console/MyPortal/UserHud/MyAbilities/boardCast.ts): My Powers' Cast opens the quest
picker first for Snooze and Extra Time (a cancelled pick ends it) → the effect is checked
(powerBoardEffectBlocked) → the normal cast (AbilityService.useAbility: frequency, MP, cast log, Town Crier,
adult note) → the effect lands. A refusal or a cancelled pick charges nothing and logs nothing.
What became of the old board effects: the Ice Wand's +24 hours is Extra Time, the Silver Arrow's snooze is
Snooze, Tesla's Bolt's trusted hour is Trusted Hour, X's Invisibility's no-encounters is Quiet Day.
Dropped: Chronos' 1-hour pause (Extra Time covers it), the Hook Shot's pull-in, the Bombs' split, Thor's half-done
mark, Gaia's shuffle (it never shuffled), Night's Light's bedtime lift (the bedtime setting locks nothing) and
Gale's Wind's day off (Day of Rest says the Game Master picks the day). The fields they wrote stay readable for old
saves (HeroQuestEffects in src/lib/engine/quests/QuestSchedule.ts). The family setting dayOffBroadcast is
hidden in the settings screen; its stored value stays. Day of Rest and Night Owl stay ledger-only Powers.
Locked items. The equipment modal shows the tools the hero has not unlocked greyed with a lock and the unlock
hint (unlockHintOf: a freed Pegasus's gift, a temple chest's relic or a shop); the hands cannot equip a locked
item. The Lantern, the Bottles, the Bombs and the navigation items are always unlocked (the Bombs still need Bombs
in the pack). A profile without an unlockedItems list keeps everything. The Game Master can unlock any tool for a
hero from the Compendium (unlockableItemNames).
C. Classes (task 35) — src/lib/services/stats/ClassPassives.ts
Each class gets one passive that the loop feels. classPassive(jobClass) returns a typed object the engine reads.
The class tree is 4 base classes, 8 advanced classes (one base parent each, unlocked at that parent's class level 10)
and 3 crown classes (two advanced parents each, both at class level 10). An advanced class is its parent's passive
plus one nudge made only of existing ClassPassive fields, so no engine changed for them.
| Class | Passive |
|---|---|
| Warrior | Braces like a wall: Defend takes a quarter of the strike instead of half. |
| Mage | Items and abilities cost 25 % less MP; wins restore 10 % MP. |
| Thief | +25 % GP from quests and chests; slips away: the parting strike of a Run Away lands at half (the brace factor). |
| Monk | Each chore earns +1 minute on the clock; +1 AP on every win. |
| Knight (Warrior 10) | Warrior's wall (brace 0.25), and every win heals 5 % HP. |
| Berserker (Warrior 10) | Brace 0.35 (Defend takes 35 % of the strike), and every win heals 15 % HP. |
| White Mage (Mage 10) | Mage's 25 % MP discount; wins restore 20 % MP and heal 10 % HP. |
| Black Mage (Mage 10) | 35 % off every MP cost; wins restore 10 % MP. |
| Assassin (Thief 10) | Slips away like the Thief (the parting strike lands at half); +30 % GP from quests and chests. |
| Ranger (Thief 10) | +25 % GP from quests and chests; each chore earns +1 minute on the clock. |
| Martial Artist (Monk 10) | Each chore earns +2 minutes on the clock; +1 AP on every win. |
| Mystic (Monk 10) | +2 AP on every win; wins restore 10 % MP. |
| Paladin (Knight 10 + Mystic 10) | Warrior's brace, and wins heal 10 % HP. |
| Ninja (Assassin 10 + Berserker 10) | Thief's slip-away, and the beast's first strike of every fight is skipped (so is the parting strike, if it is still owed). |
| Sage (White Mage 10 + Mystic 10) | Mage's discount doubled (50 %), and Monk's minute. |
Run Away (QR8, docs/Quest-Rooms-Spec.md 5.6): a run always works for every class. It costs one parting strike
(a full beast strike) unless the fight is free. runChance stays on the table only as a flag: a class that has it
set (Thief, Assassin, Ninja) slips away, so the parting strike is softened through the brace factor (half, or less
with trained guts), once, even when the hero was also bracing. The number itself is no longer read, and
escapeChanceAgainst and its 75/90/50/25 odds are gone. The Ninja's skipFirstStrike dodges the parting strike
while it is still owed.
Rules: calculateLevelUp adds the class level bonus (getClassStatBonus) to max HP/MP on a normal level-up
too. The eight advanced classes and Paladin, Ninja and Sage become pickable in the Class Codex once
JOB_UNLOCK_REQUIREMENTS are met (the class levels are on the hero: stats.classes[jobClass].level, keyed by the
class name exactly as spelled, spaces included); the Codex shows them sealed with the requirement until then.
Paladin, Ninja and Sage keep their passives when the parents changed (they used to unlock from two base classes);
a hero who already plays one keeps it, and one who met only the old pair may find it sealed again (no migration).
Tests: a spec per passive on the pure function (tests/unit/ClassPassives.spec.ts), the tree rules in
tests/unit/ClassTree.spec.ts, and the engine hooks that read the passives.