Imported from FL0R1AN84/hero-quest (
AGENTS.md). Install upstream withnpx skills add FL0R1AN84/hero-quest. Copyright stays with the author.
AGENTS.md — HeroQuest Character Sheet
Project Overview
HeroQuest is a mobile-friendly digital character sheet for the classic German HeroQuest board game. It's a Vue 3 + Pinia single-page application (no backend) that persists character data to localStorage and supports JSON export/import for sharing between players.
UI Language: German (Persönlichkeits-Pass). All labels, buttons, and validation messages use German terms specific to the HeroQuest universe.
Architecture
Data Flow
skillSheetData.ts (game rules & options)
↓
Pinia Store (useSkillSheetStore)
↓
Vue Components (read state via storeToRefs, dispatch mutations)
↓
Computed Properties (derive displayed stats: bonuses, effective dice)
↓
Template Rendering + localStorage persistence
Key Files & Responsibilities
| File | Purpose |
|---|---|
src/data/skillSheetData.ts |
Single source of truth for all game data: 8 character classes, weapons, armor, special items with bonuses, restrictions, and item types. Also exports characterAvatars (symbol + color per class). Never duplicate these definitions. |
src/stores/skillSheet.ts |
Pinia store managing character state (stats, equipment, items, quantities, charges, Druide shape-shift). Auto-persisted via pinia-plugin-persistedstate. |
src/views/SkillSheet.vue |
Main page orchestrating save/load/end-game flows. Contains file I/O logic (saveToFile(), loadFromFile()). |
src/components/*.vue |
Presentational components (Header, Stats, Equipment, etc.) using Composition API <script setup>. |
src/components/KillListModal.vue |
Kill list tracker — modal dialog for recording monster kills by match day, with animations and persistent storage. |
src/components/AppUpdatePrompt.vue |
PWA update banner — listens for sw-update-available custom event, prompts user to reload for new version. |
src/components/AppVersion.vue |
Version display — shows package.json version; 5× click reveals a hidden full-reset button (store.reset()). |
State Management Patterns
- Use
storeToRefs(store)in components — wraps refs in reactive proxies for template binding - Computed properties with getters/setters —
effectiveAttackDiceadds weapon bonus on read, subtracts on write (so base stats stay clean) - Array mutations — use
.splice()and.push()for arrays; avoid.filter()+ reassignment in nested objects - Persistence is automatic — all store mutations auto-save to
localStorage; no manualstore.$patch()needed except during file load
Example pattern from SkillSheetStats.vue:
const { attackDice } = storeToRefs(store)
const effectiveAttackDice = computed({
get: () => (attackDice.value ?? 0) + store.weaponBonus + store.druideShapeBonus,
set: (v: number) => { attackDice.value = v - store.weaponBonus - store.druideShapeBonus }
})
Game Rules & Domain Knowledge
Character Classes & Starting Stats
8 characters (Barbar, Barde, Berserker, Druide, Elf, Ritter, Zwerg, Zauberer) with unique base stats for Attack Dice, Defense Dice, Body Strength, Intelligence. Stored in defaultStats lookup table — always fetch from there, never hardcode values. Each class also has an emoji avatar and theme color defined in characterAvatars.
Equipment Restrictions
- Breitschwert (Broadsword): Barbar and Berserker only
- PlattenrĂĽstung (Plate Armor): Barbar only
- Stab der Magie + Ring der Magie: Druide/Zauberer only
- Stab des Zauberers / Telekinese-Stab: Druide/Zauberer only (caster weapons)
- Amulett der Weisheit: Barbar only
- Mantel des Zauberers: Druide/Zauberer only (passive +1 Intelligence)
- Zauberer cannot equip any weapons — enforced by
canEquipWeapon()returningfalsefor Zauberer; Druide CAN equip weapons
Bonus System
- Weapon bonuses applied to Attack Dice (cumulative, max 6)
- Armor bonuses applied to Defense Dice (cumulative, max 6)
- Passive item bonuses (e.g., Amulett +1 Intelligence) added via
intelligenceBonusfield; computed in store - Consumables (potions) used up: either quantity-tracked or charge-tracked (1-use vs 2-use items)
Item Types & Behaviors
- heal-fixed: Restore body strength +4 (never above starting value)
- heal-fixed-2: Restore body strength +2 (e.g. Gegengift)
- heal-potion: Roll 1d6 → heal that amount (never above starting value); opens inline dice dialog
- attack-potion / defense-potion: Boost dice temporarily (bounded 1–6)
- extra-attack-same: Roll combat dice a 2nd time against the same enemy (e.g. Kampfestrank, Fluch der Orks)
- extra-attack-multi: Roll 2× attack dice, usable against 2 enemies (e.g. Stärkungstrank)
- movement-potion: Temporary +5 movement points (e.g. Geschicklichkeitstrank; display-only, no store field)
- restore-small: Restore +1 Körperkraft and +1 Intelligence (never above starting value)
- fire-shield (Ring des Feuers): 2-charge item, tracks uses in
itemChargesUsedlookup - magic-ring (Ring der Magie): 2-charge item for Druide/Zauberer
- passive items: Permanently active (no tracking), applied via store computed properties (
intelligenceBonus);bonusLabelis display-only for other passive bonuses (e.g. Ring der Stärke +1 ⚔️ — label only, no computed store field)
Death & Revival
- When Body Strength = 0 → fullscreen death overlay appears
- Player may optionally use a healing potion (from inventory) before selecting revival points
- Player selects revival Body Strength (1 to class default), overlay closes
- End-of-game reset restores all stats to class defaults but keeps all equipment & items
Druide Shape-Shifting
- Store ref
druideShapeShifted(boolean) tracks active shift state druideShapeBonuscomputed: adds +1 to both attack and defense dice whencharacter === 'Druide'and shiftedtoggleDruideShape(): only togglable when Körperkraft is at the Druide's starting max (6); auto-exposed in storedeactivateDruideShape(): called automatically inchangeBodyStrength()whenever body strength decreaseseffectiveAttackDiceandeffectiveDefenseDiceboth includedruideShapeBonusin their getter/setter
Kill List Feature
- Purpose: Track every monster defeat during a campaign with animations and statistics
- UI Location: đź’€ button in top-right corner of character sheet
- Recording: Single click on "+ Kill hinzufĂĽgen" button instantly records a kill with timestamp
- Grouping: Kills automatically grouped by match day (gaming session date)
- Display: Shows last 5 match days with per-day kill counts; total cumulative kills at top
- Storage: Organized by
matchDay(ISO date YYYY-MM-DD) with individualtimestampfor time-of-day display - Persistence: Saved to
localStoragevia Pinia, included in JSON export/import, survives app restart
Kill Data Structure (in store and JSON):
interface Kill {
id: string // Unique identifier
matchDay: string // ISO date (YYYY-MM-DD) for grouping
timestamp: number // JavaScript timestamp for time display
}
Animations on Kill Record:
- Button Pulse (600ms) — "+ Kill hinzufügen" button scales 1x→1.05x with expanding red glow
- Badge Bounce (600ms) — Kill counter badge scales 1x→1.2x→1x
- Stats Glow (600ms) — "Gesamt Kills" number scales and brightens (#f67449→#ff8566)
- Slide-In (500ms) — New kill item slides in from left with highlight
- Highlight (1000ms) — New kill has gold background + orange border, auto-fades
Store Methods & Computed:
store.addKill()— Record new kill for todaystore.removeKill(killId)— Delete specific killstore.clearKills()— Clear all killsstore.killsByMatchDay— Kills grouped by datestore.last5MatchDays— Last 5 gaming daysstore.totalKills— Total across all gamesstore.todayKills— Today's kills only
Component Patterns
Composition API + <script setup>
All components use Vue 3 Composition API with <script setup>. No <script> block without setup attribute.
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
// imports here
</script>
Derived State (Computed Properties)
- Use getter/setter pattern for bidirectional binding (e.g., effective dice subtracting bonuses)
- Use dependencies in templates for auto-updates (computed properties track reactivity)
- Avoid manual
.watch()unless absolutely needed (prefer computed)
Event Handlers
- Use
@click.preventfor buttons inside forms;@submit.preventfor form submission - Boolean logic in templates:
v-if="isDead",v-showfor visibility toggles - Disabled state:
:disabled="!character || effectiveAttackDice <= 1"
Scoped Styling
All components use <style scoped>. Never use global selectors in component styles.
- CSS variables for theme colors:
var(--color-red),var(--hq-bg), etc. (defined insrc/assets/variables.css) - Dark/Light mode: system prefers-color-scheme handled via CSS variable swaps
- Mobile-first: breakpoints at
480pxand above
Equipment UI Pattern
SkillSheetEquipment.vue uses a dropdown-add pattern: only equipped items are shown in the list; unequipped available items appear in a <select> dropdown with an "HinzufĂĽgen" button. Clicking an equipped item removes it. Key computed helpers:
availableWeapons/availableArmor/availableSpecialItems— filtered & sorted lists for the dropdownsequippedWeaponOptions/equippedArmorOptions/equippedSpecialOptions— resolve IDs to option objects for renderinggetItemCategory(item)— returns 0 (Tränke), 1 (Magische Gegenstände), or 2 (Sonstiges) for thematic groupingpotionKinds/magicKinds—Setconstants that drive categorization and rendering branches
Build & Development Workflow
npm run dev # Start Vite dev server (hot reload)
npm run build # Type-check + Vite build (for deployment)
npm run test:unit # Vitest (jsdom environment)
npm run test:e2e # Playwright tests (need `npx playwright install` first)
npm run lint # Oxlint + ESLint (dual linting)
npm run format # Prettier (src/ only)
Key Build Configuration
- Vite aliases:
@maps to./src - TypeScript strict mode: Full type checking, no implicit
any - Tailwind CSS v4: via
@tailwindcss/viteplugin - Vue DevTools: Vite plugin included (dev only)
- PWA: Service worker at
public/service-worker.jspolls for updates every 60 s; firessw-update-availablecustom event consumed byAppUpdatePrompt.vue
File Format (Save/Load)
When users export a character, it's a .json file with this structure:
{
"name": "Hero Name",
"character": "Zwerg",
"attackDice": 2,
"defenseDice": 3,
"bodyStrength": 7,
"intelligence": 3,
"equippedWeapon": ["helm"],
"equippedArmor": ["schild"],
"equippedSpecialItems": ["ring-des-feuers"],
"usedSpecialItems": [],
"itemQuantities": {"heiltrank": 2},
"itemChargesUsed": {"ring-des-feuers": 1},
"druideShapeShifted": false,
"kills": [
{"id": "1722716400000-abc123", "matchDay": "2026-08-04", "timestamp": 1722716400000},
{"id": "1722716420000-xyz789", "matchDay": "2026-08-04", "timestamp": 1722716420000},
{"id": "1722802800000-def456", "matchDay": "2026-08-03", "timestamp": 1722802800000}
]
}
When loading: store.$patch({...data}) applies entire snapshot. Validate IDs exist in current game data before patching to prevent orphaned references. Kills are preserved as-is with their match days intact.
Testing Practices
Unit Tests (Vitest)
- Located in
src/__tests__/matching component names - Use
describe()andit()blocks - Mount Vue components with
@vue/test-utils - Example:
mount(App)and check template output
E2E Tests (Playwright)
- Located in
e2e/directory - Test full user workflows (character creation, equipment changes, save/load)
- Must run
npm run buildfirst, thennpm run test:e2e
Code Conventions
Naming
- Files: kebab-case (e.g.,
SkillSheetStats.vuenotskillSheetStats.vue) - JavaScript variables/functions: camelCase
- CSS classes: kebab-case or BEM in scoped contexts
- Data IDs: kebab-case (e.g.,
'ork-kurzschwert','ring-des-feuers')
TypeScript
- Interfaces for data structures — defined in
src/data/skillSheetData.ts(WeaponOption,ArmorOption,SpecialItemOption) - Use
typefor simple unions,interfacefor complex shapes - No implicit
any— always provide type hints
German UI Text
- All user-facing text is in German (buttons, labels, error messages)
- Translatable strings hardcoded in component templates (no i18n framework)
- Common terms: Werte (stats), Ausrüstung (equipment), Gegenstände (items), Körperkraft (body strength), Angriffswürfel (attack dice)
Common Tasks
Adding a New Item Type
- Add entry to
specialItemOptionsarray inskillSheetData.tswith correctkind,allowedCharacters, andmaxUses - Add UI rendering logic in
SkillSheetEquipment.vuetemplate if new layout needed — items are categorized bygetItemCategory(): potion kinds → category 0 (Tränke), passive/magic-ring/fire-shield → category 1 (Magische Gegenstände), others → category 2 (Sonstiges) - If item has charges, hook into
itemChargesUsedref - If passive bonus, add
passive: trueandintelligenceBonusto definition; other passive bonus types (e.g. attack) usebonusLabelfor display only
Adding a New Character
- Add character name to
characterOptionsarray inskillSheetData.ts - Define avatar emoji and theme color in
characterAvatars - Add base stats entry in
defaultStats(attackDice, defenseDice, bodyStrength, intelligence) - If the character has weapon restrictions, add them to
allowedCharacterson the relevant weapon (e.g. Berserker gets Breitschwert alongside Barbar)
Modifying Equipment Bonuses
- Update
weaponOptionsorarmorOptionsinskillSheetData.ts - Computed properties in store (
weaponBonus,armorBonus) automatically recalculate - Components using
effectiveAttackDiceoreffectiveDefenseDiceautomatically re-render
Changing Character Restrictions
- Edit
allowedCharactersarray in item definition (null= unrestricted) - Restriction helpers in
SkillSheetEquipment.vue(canEquipWeapon(), etc.) automatically filter options - Template
v-ifbindings prevent equipping restricted items
Fixing Display Bugs
- Stats not updating? → Check if computed property is reactive (use
storeToRefs()) - Equipment not showing? → Verify
allowedCharactersrestriction or item visibility logic - Save/load broken? → Inspect JSON structure in
saveToFile()andloadFromFile()inSkillSheet.vue
Linting & Code Quality
- Oxlint (Rust-based, fast) — runs first for performance
- ESLint with Prettier — runs after for Vue/TypeScript specifics
- No console warnings — watch build output for unused imports or type issues
- .oxlintrc.json configures Oxlint rules (custom config file in project root)
Debugging Tips
- Vue DevTools: Installed via Vite plugin during dev
- Pinia DevTools: Inspect store state mutations in browser console
- Vitest UI: Run
npx vitest --uito see test coverage in browser - localStorage inspection: Open DevTools → Application → localStorage →
file://entry to see persisted store - Character restrictions failing? → Log
canEquipWeapon(item)in template or add breakpoint in helper function
Last Updated: July 29, 2026 | Framework: Vue 3 (Composition API) + Pinia + Tailwind CSS v4 | Target: German HeroQuest players