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.