polats Claude Opus 4.8 (1M context) commited on
Commit
71a4292
·
1 Parent(s): f467b2b

LLM: feed live combat state into character chat

Browse files

buildStateTurn now takes a live snapshot (HP/%, in-combat, threat, kills, location) and
renders it as in-voice 'right now' lines — so asking a fighter mid-run gets answers aware
of their current wounds and the threat around them, not just persisted history. tiny.js
passes comboCtrl.getHero()+getThreat() via getLive when this hero is the one on the field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. web/characterChat.js +2 -2
  2. web/characterContext.js +19 -4
  3. web/tiny.js +8 -1
web/characterChat.js CHANGED
@@ -32,7 +32,7 @@ async function speak(id, text, p, btn) {
32
  } catch { /* voice is best-effort */ } finally { set('▶') }
33
  }
34
 
35
- export function mountCharacterChat(host, personaId, { onUpdate } = {}) {
36
  const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;height:100%;min-height:0'
37
  const thread = document.createElement('div'); thread.style.cssText = 'flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding:4px 2px 8px'
38
  const form = document.createElement('div'); form.style.cssText = 'display:flex;gap:8px;align-items:flex-end;border-top:1px solid #232b36;padding-top:8px'
@@ -86,7 +86,7 @@ export function mountCharacterChat(host, personaId, { onUpdate } = {}) {
86
  persist('user', msgId(), text); renderThread()
87
  // Transient streaming bubble (not yet in history) we write tokens into live.
88
  const row = bubble('assistant', '…'); thread.append(row); thread.scrollTop = thread.scrollHeight
89
- const { system, user } = buildContext(p, text)
90
  let raw = ''
91
  try {
92
  await ensureModel(() => {})
 
32
  } catch { /* voice is best-effort */ } finally { set('▶') }
33
  }
34
 
35
+ export function mountCharacterChat(host, personaId, { onUpdate, getLive } = {}) {
36
  const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;height:100%;min-height:0'
37
  const thread = document.createElement('div'); thread.style.cssText = 'flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding:4px 2px 8px'
38
  const form = document.createElement('div'); form.style.cssText = 'display:flex;gap:8px;align-items:flex-end;border-top:1px solid #232b36;padding-top:8px'
 
86
  persist('user', msgId(), text); renderThread()
87
  // Transient streaming bubble (not yet in history) we write tokens into live.
88
  const row = bubble('assistant', '…'); thread.append(row); thread.scrollTop = thread.scrollHeight
89
+ const { system, user } = buildContext(p, text, { live: getLive ? getLive() : null })
90
  let raw = ''
91
  try {
92
  await ensureModel(() => {})
web/characterContext.js CHANGED
@@ -30,8 +30,22 @@ export function buildSystemPrompt(p) {
30
  return lines.filter((l) => l !== '').join('\n')
31
  }
32
 
33
- // DYNAMIC turn: current standing + recent perception events + verbatim recent chat + new line.
34
- export function buildStateTurn(p, userMsg, { maxEvents = 5, maxHistory = 8 } = {}) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  const prog = (p && p.progression) || { level: 1, kills: 0 }
36
  const cs = (p && p.customSkills) || {}
37
  const equipped = (p && p.equippedSkills || []).map((id) => cs[id]).filter(Boolean)
@@ -39,7 +53,8 @@ export function buildStateTurn(p, userMsg, { maxEvents = 5, maxHistory = 8 } = {
39
  const history = (p && p.chat && p.chat.history || []).slice(-maxHistory)
40
 
41
  const state = [
42
- `Level ${prog.level}${prog.kills ? `, ${prog.kills} foes felled` : ''}.`,
 
43
  equipped.length ? `Skills at the ready: ${equipped.map((s) => `${s.name} (${effectSummary(s)})`).join('; ')}.` : '',
44
  events.length ? `Lately:\n${events.map((e) => ` - ${e}`).join('\n')}` : '',
45
  ].filter(Boolean).join('\n')
@@ -57,7 +72,7 @@ export function buildStateTurn(p, userMsg, { maxEvents = 5, maxHistory = 8 } = {
57
  ].filter((l) => l !== '').join('\n')
58
  }
59
 
60
- // Convenience: assemble both halves for a turn.
61
  export function buildContext(p, userMsg, opts) {
62
  return { system: buildSystemPrompt(p), user: buildStateTurn(p, userMsg, opts) }
63
  }
 
30
  return lines.filter((l) => l !== '').join('\n')
31
  }
32
 
33
+ // Turn a live combat snapshot into in-voice "right now" lines (woid's trigger/needs cue).
34
+ function liveLines(live) {
35
+ if (!live) return ''
36
+ const out = []
37
+ if (live.hp != null && live.maxHp) {
38
+ const pct = Math.round((live.hp / live.maxHp) * 100)
39
+ const cond = pct < 25 ? ' — you are badly wounded and bleeding' : pct < 60 ? ' — battered but standing' : ''
40
+ out.push(`Right now you are ${live.inCombat ? 'in the thick of a fight' : 'out roaming the field'}, at ${live.hp}/${live.maxHp} HP (${pct}%)${cond}.`)
41
+ }
42
+ if (live.inCombat && live.threat) out.push(`The threat around you is ${live.threat}× and climbing; you have cut down ${live.kills || 0} foes this push.`)
43
+ if (live.location) out.push(`You are in ${live.location}.`)
44
+ return out.join(' ')
45
+ }
46
+
47
+ // DYNAMIC turn: live combat snapshot + current standing + recent events + verbatim chat + new line.
48
+ export function buildStateTurn(p, userMsg, { maxEvents = 5, maxHistory = 8, live = null } = {}) {
49
  const prog = (p && p.progression) || { level: 1, kills: 0 }
50
  const cs = (p && p.customSkills) || {}
51
  const equipped = (p && p.equippedSkills || []).map((id) => cs[id]).filter(Boolean)
 
53
  const history = (p && p.chat && p.chat.history || []).slice(-maxHistory)
54
 
55
  const state = [
56
+ liveLines(live),
57
+ `Level ${prog.level}${prog.kills ? `, ${prog.kills} foes felled in all` : ''}.`,
58
  equipped.length ? `Skills at the ready: ${equipped.map((s) => `${s.name} (${effectSummary(s)})`).join('; ')}.` : '',
59
  events.length ? `Lately:\n${events.map((e) => ` - ${e}`).join('\n')}` : '',
60
  ].filter(Boolean).join('\n')
 
72
  ].filter((l) => l !== '').join('\n')
73
  }
74
 
75
+ // Convenience: assemble both halves for a turn. `opts.live` carries a live combat snapshot.
76
  export function buildContext(p, userMsg, opts) {
77
  return { system: buildSystemPrompt(p), user: buildStateTurn(p, userMsg, opts) }
78
  }
web/tiny.js CHANGED
@@ -469,7 +469,14 @@ whenEl('battle-stage', async (el) => {
469
  const closeChat = () => { back.remove(); chatSheet = null }
470
  x.addEventListener('click', closeChat); head.append(ttl, x); sh.append(head)
471
  const body = document.createElement('div'); body.style.cssText = 'flex:1;min-height:0;display:flex;flex-direction:column'; sh.append(body)
472
- const chat = mountCharacterChat(body, currentHero.id, {}); setTimeout(() => chat.focus(), 50)
 
 
 
 
 
 
 
473
  back.addEventListener('pointerdown', (e) => { if (e.target === back) closeChat() })
474
  back.append(sh); el.append(back); chatSheet = back
475
  }
 
469
  const closeChat = () => { back.remove(); chatSheet = null }
470
  x.addEventListener('click', closeChat); head.append(ttl, x); sh.append(head)
471
  const body = document.createElement('div'); body.style.cssText = 'flex:1;min-height:0;display:flex;flex-direction:column'; sh.append(body)
472
+ // Feed the live combat snapshot in so the fighter's chat reflects what's happening NOW.
473
+ const getLive = () => {
474
+ const h = comboCtrl?.getHero?.(); const th = comboCtrl?.getThreat?.()
475
+ if (!h || h.name !== (currentHero.name || h.name)) return null // only when THIS hero is on the field
476
+ const inCombat = h.hp < h.maxHp || (th && th.kills > 0)
477
+ return { hp: h.hp, maxHp: h.maxHp, inCombat: !!inCombat, threat: th ? +th.diff.toFixed(1) : null, kills: th ? th.kills : 0, location: 'the Forgotten Plains' }
478
+ }
479
+ const chat = mountCharacterChat(body, currentHero.id, { getLive }); setTimeout(() => chat.focus(), 50)
480
  back.addEventListener('pointerdown', (e) => { if (e.target === back) closeChat() })
481
  back.append(sh); el.append(back); chatSheet = back
482
  }