Spaces:
Running
Running
LLM: feed live combat state into character chat
Browse filesbuildStateTurn 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>
- web/characterChat.js +2 -2
- web/characterContext.js +19 -4
- 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 |
-
//
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|