# Battle Field v2 — the fabulous fight (contract v2, 2026-10-09, after two design critiques) Repo: /home/xopher/www/x/do-it-for-the-xp. Vue 3 + Ionic 7 + Pinia + Capacitor 6, Vue CLI 5 / webpack, jest 27 (+ @vue/test-utils 2, vue3-jest, jest-transform-stub for assets), Cypress 8.7. Shell is zsh: quote globs (--include='*.ts'). Use `pnpm`, never npm/yarn. Board: every task below is a chemx team task (#25-#29). Claim it (`chemx team task claim --as=@`), comment progress (`chemx team task comment ""`), close with `chemx team task done --as=@ --force` (the AST gate is red on this legacy repo, --force is expected). ## Shared facts (read before coding) - Battle code lives in src/app/Console/BattleField/: page BattleField.vue/.ts/_BattleField.scss, battle/* (battleContext, encounterLoader, battleRewards, battleMenu, battleView), composables/*, components/*, hooks/useBattlePlayer.ts, hud/XpHpMpHud (to be deleted by F), __tests__/BattleField.spec.ts. - Engine: src/lib/services/battle/BattleService.ts (reactive per-battle state, Task-ATB clock, defend/run, pending damage) and BestiaryService.ts (Beast -> Enemy). Factory in src/lib/services/battle/index.ts. - SHARED TYPES (frozen, import from here, do not edit): src/app/Console/BattleField/battle/battleView.ts — EnemyAnimation, EnemySize, HitPopup {id, amount, smash?}, RewardTally {xp,gp,ap}, VictoryCardState {title, subtitle, rewards, questName?, choresDone, elapsedMs, held, levelUp?}, DialogLine {text, auto?}, EngineMessage = string | DialogLine. - Theme: Earthbound. Fonts loaded (src/styles/_fonts.scss): "Apple Kid", "Year Is 199X", "StatusPlz", "Wisdom of the World", "Curtain Call", "Fourside", "Onett", "Twoson", "The Font Against Giygas", "Orange Kid". Never "Press Start 2P" (not loaded). Use the literal font-family strings (the SCSS var $eb-font-199x is "199X", wrong). - Box: `@use "@/styles/themes/earthbound.scss" as *;` then `@include eb-box();` = dark purple #280828 box with 4 ring shadows that reach 16 px OUTSIDE the element (never put overflow:hidden on the eb-box element itself; give it >= 20 px margin inside overflow-hidden parents). Text cream = #e7e6b3 ($eb-color-cream); accent pale yellow = #f7e8a8 (= var(--ion-color-rpg)). Stat colours: var(--xp-color-xp) green, var(--xp-color-gp) yellow, var(--xp-color-ap) light blue, var(--xp-color-hp) red, var(--xp-color-mp) purple. - Sounds: `import { play$fx } from "@/assets/fx"`; play$fx(key) looks up ui.nintendo[key] then rpg.earthbound[key]. Valid: ui: select, yes, no, alert, options, openTask, closeTask, openPage, closePage, chooseUser, openShop, start, loading, intro, setup, noSelect. rpg: startBattle, attack, preAttack, damage (hero hit), enemyDamage (beast hit), enemyDie, win (local eb_win), levelUp (fanfare), die, text, cursor, restoreHP, countCoins, fillPoints, message, error, okdesuka. Unknown keys play nothing ("confirm"/"cancel" do not exist). - Battle BGM is started by the router (src/router/_Guards/bgm.guard.ts) for every battle-* route; components never start music. - Haptics: @capacitor/haptics 6.0.3; on the web it throws without navigator.vibrate; only the page (F) calls it, always in try/catch. - Jest: `jest.useFakeTimers()` is the modern implementation (fakes Date, timers, queueMicrotask). Asset imports resolve to "" via jest-transform-stub; `require.context` is shimmed (tests/jest/require-context.setup.js) and throws MODULE_NOT_FOUND on unknown keys like webpack. Never `await new Promise(r => setTimeout(r))` under fake timers; use `await nextTick()` / `flushPromises()` from @vue/test-utils. `pnpm typecheck` also checks tests/**/*.ts, so specs must type-check. Component specs mount in isolation; no component may import "@ionic/vue" or "@/lib/mixins/ionic" (plain HTML +