slaywithfriends/docs/plans/2026-02-23-single-combat-design.md

5.1 KiB

single combat encounter - design doc

first playable slice of slaywithfriends. one ironclad player vs one enemy. proves the core card engine works.

decisions

  • rendering: html/css with card images (no canvas)
  • framework: none. vanilla js, es modules, bun serve
  • networking: client-only. all state in browser
  • character: ironclad (simplest mechanics)
  • interaction: two-tap (select card, tap target)
  • mobile: design for 375px baseline, scales up naturally

state model

single object drives everything. pure functions produce new state. nothing mutates directly.

state = {
  player: {
    hp, maxHp,
    energy, maxEnergy,
    block,
    strength,
    drawPile: [...cardIds],
    hand: [...cardIds],
    discardPile: [...cardIds],
    exhaustPile: [...cardIds],
    powers: [...cardIds],
  },
  enemy: {
    id, name, hp, maxHp, block,
    strength, vulnerable, weak,
    action,
    actionTrack: [...],
    trackPosition,
  },
  combat: {
    turn,
    phase: 'player_turn' | 'enemy_turn' | 'rewards' | 'ended',
    dieResult,
    selectedCard,
    log: [...],
  }
}

card data schema

cards are json entries keyed by id. effects are typed arrays.

{
  "strike_r": {
    "name": "Strike",
    "cost": 1,
    "type": "attack",
    "effects": [{"type": "hit", "value": 6}],
    "image": "assets/images/ironclad/starter/0.png",
    "keywords": [],
    "description": "Deal 6 damage.",
    "upgraded": "strike_r+"
  }
}

effect types for first slice: hit, block, draw, strength, vulnerable, weak, exhaust, lose_hp.

enemy data schema

{
  "jaw_worm": {
    "name": "Jaw Worm",
    "hp": 6,
    "actionType": "die",
    "actions": {
      "1": {"intent": "attack", "effects": [{"type": "hit", "value": 3}]},
      ...
    }
  }
}

three action types: single (same every turn), die (keyed to die roll), cube (ordered list with advancing pointer, gray actions don't repeat).

rendering layout

three vertical zones:

enemy zone (~40%)    enemy art, hp bar, block, status tokens, intent
info bar (fixed)     energy pips, hp bar, block shield, strength, pile counts
hand (~35%)          cards fan out, overlap if many, selected card lifts

pile overlays: tap draw/discard count to see scrollable card grid.

two-tap interaction

  1. tap card in hand - it lifts and highlights
  2. enemy zone becomes valid target (highlighted border)
  3. skills that don't need a target auto-play
  4. tap enemy - card plays, effects resolve, card animates to discard
  5. not enough energy - card shakes and drops back
  6. tap selected card again to deselect

combat loop

player turn:

  1. reset energy to 3, block to 0
  2. draw 5 cards (shuffle discard into draw if needed)
  3. roll die (1-6), determines enemy intent
  4. resolve start-of-turn triggers
  5. play phase - player acts freely
  6. "end turn" button ends play phase
  7. end-of-turn triggers, discard remaining hand

enemy turn (auto-resolves):

  1. remove enemy block
  2. enemy executes action
  3. advance cube-action pointer if applicable
  4. brief pause (~1s) for player to read

combat ends when enemy hp hits 0 (victory) or player hp hits 0 (defeat).

damage formula

  1. start with base hit value
  2. add attacker strength (+1 per token)
  3. if target vulnerable, double result
  4. if attacker weak, subtract 1
  5. weak AND vulnerable cancel out - skip both
  6. subtract target block, remainder hits hp
  7. consume 1 vulnerable from target after attack
  8. consume 1 weak from attacker after attack
  9. multi-hit: loop N times, consume tokens only after all hits
  10. aoe/multi-hit: only 1 weak consumed total

enemy ai

data-driven. each enemy's action is an effects array resolved the same way as card effects. intent displayed as icon + number after die roll so player can plan.

project structure

slaywithfriends/
  index.html
  style.css
  src/
    main.js          entry point, init, render loop
    state.js         state creation, pure action functions
    effects.js       effect resolver
    combat.js        turn flow orchestration
    enemy-ai.js      resolve enemy actions
    render.js        state to dom
    cards.js         load/query card data
    enemies.js       load/query enemy data
    die.js           die roll
  data/
    cards.json       card database
    enemies.json     enemy database
    potions.json     potion database
    relics.json      relic database
  assets/            video game card images
  StS_BG_assets/     board game assets
  docs/              rules and plans

out of scope

  • map / room navigation
  • deck building / rewards / card selection
  • merchant / campfire / events
  • potions and relics
  • multiplayer / networking
  • persistent state / save / load
  • sound
  • ironclad passive (heal 1 hp end of combat)
  • tutorial

data pipeline

board game card sheet images being extracted to json by agent. video game wiki data (wiki.gg Module:Cards/data) available as secondary reference. board game values are source of truth.