polats Claude Opus 4.8 (1M context) commited on
Commit
710dcf3
·
1 Parent(s): ae61c19

Fix first-spawn hero recognition; tour portrait/voice; lighter cinematic

Browse files

- Created heroes now spawn with their roster id (openCreateModal carries savedId
through to spawnWithFly), so portrait/voice/skills/chat are recognized on the
FIRST spawn instead of only after re-picking from the roster.
- Guided tour gains explicit "paint portrait" + "create voice" steps (data-tut
markers + onPortrait/onVoice hooks) before Save & Play, so later skill/chat
steps have real media.
- Cinematic: harden startCinematic early-returns to tear down partial builds if
cancelled mid-setup; cut warbands 6→4 anchors (~60 vs ~90 sprites) to fix
title-screen slowdowns. (Armies were already cleaned up between runs.)

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

Files changed (3) hide show
  1. web/comboBattler.js +8 -2
  2. web/heroCreator.js +4 -2
  3. web/tiny.js +13 -3
web/comboBattler.js CHANGED
@@ -6896,10 +6896,16 @@ function mountComboBattler(pixi, host, opts = {}) {
6896
  map.getEntityLayer().addChild(combatRoot);
6897
  const firstSheet = groups[0]?.units?.[0]?.sheets;
6898
  const ch = await contentHeight(firstSheet?.idle || firstSheet?.walk);
6899
- if (!alive || !cinematic) return [];
 
 
 
6900
  depthScale.v = SPRITE_TILES * TILE8 / ((ch || 24) * G) * CINE_SPRITE_MUL;
6901
  R = await createCombatRenderer({ pixi, defsById: {}, layers: { units, fx, projLayer: proj }, coords: { mapX: (x) => x, mapY: (y) => y, depthOf: () => depthScale.v }, getBattle: () => battle });
6902
- if (!alive || !cinematic) return [];
 
 
 
6903
  let n = 0;
6904
  const anchors = [];
6905
  for (const g of groups) {
 
6896
  map.getEntityLayer().addChild(combatRoot);
6897
  const firstSheet = groups[0]?.units?.[0]?.sheets;
6898
  const ch = await contentHeight(firstSheet?.idle || firstSheet?.walk);
6899
+ if (!alive || !cinematic) {
6900
+ teardownCombat();
6901
+ return [];
6902
+ }
6903
  depthScale.v = SPRITE_TILES * TILE8 / ((ch || 24) * G) * CINE_SPRITE_MUL;
6904
  R = await createCombatRenderer({ pixi, defsById: {}, layers: { units, fx, projLayer: proj }, coords: { mapX: (x) => x, mapY: (y) => y, depthOf: () => depthScale.v }, getBattle: () => battle });
6905
+ if (!alive || !cinematic) {
6906
+ teardownCombat();
6907
+ return [];
6908
+ }
6909
  let n = 0;
6910
  const anchors = [];
6911
  for (const g of groups) {
web/heroCreator.js CHANGED
@@ -156,10 +156,10 @@ export function mountHeroCreator(host, opts = {}) {
156
  const voicePickEl = el('select', { class: 'persona-input persona-voice-pick' })
157
  const voicePickRow = el('div', { class: 'persona-voice-pick-row' },
158
  [el('label', { class: 'persona-label' }, 'Voice'), voicePickEl])
159
- const playBtn = el('button', { class: 'persona-ico persona-play', type: 'button', title: 'Play voice' }, '▶')
160
  const voiceStatus = el('span', { class: 'persona-act-status' }) // "generating voice via …" beside ▶
161
  const appearanceEl = el('div', { class: 'persona-appearance persona-edit', 'data-ph': 'How they look…' })
162
- const portraitBtn = el('button', { class: 'persona-ico persona-portrait-btn', type: 'button', title: 'Paint portrait' }, '🎨')
163
  const portraitStatus = el('span', { class: 'persona-act-status' }) // "painting via …" beside 🎨
164
  const portraitImg = el('img', { class: 'persona-portrait-img', alt: '' })
165
  const portraitWrap = el('div', { class: 'persona-portrait-wrap' }, [portraitImg])
@@ -363,6 +363,7 @@ export function mountHeroCreator(host, opts = {}) {
363
  await putPortrait(savedId, blob)
364
  lastPersona.appearance = appearance; lastPersona.portraitUsed = appearance
365
  hasPortrait = true; setPortrait(blob); autosave()
 
366
  portraitStatus.textContent = ''
367
  } catch (e) { portraitStatus.textContent = `failed: ${e.message || e}` }
368
  finally { portraitBusy = false; portraitBtn.classList.remove('busy'); portraitBtn.disabled = false; updatePortraitUI() }
@@ -428,6 +429,7 @@ export function mountHeroCreator(host, opts = {}) {
428
  lastPersona.voiceDesignUsed = lastPersona.voice || ''
429
  lastPersona.voiceIdUsed = lastPersona.voiceId || ''
430
  hasVoice = true; autosave()
 
431
  voiceStatus.textContent = ''
432
  } catch (e) { voiceStatus.textContent = `failed: ${e.message || e}` }
433
  finally { working = false; playBtn.classList.remove('busy'); playBtn.disabled = false; updateVoiceUI() }
 
156
  const voicePickEl = el('select', { class: 'persona-input persona-voice-pick' })
157
  const voicePickRow = el('div', { class: 'persona-voice-pick-row' },
158
  [el('label', { class: 'persona-label' }, 'Voice'), voicePickEl])
159
+ const playBtn = el('button', { class: 'persona-ico persona-play', type: 'button', title: 'Play voice', 'data-tut': 'voice' }, '▶')
160
  const voiceStatus = el('span', { class: 'persona-act-status' }) // "generating voice via …" beside ▶
161
  const appearanceEl = el('div', { class: 'persona-appearance persona-edit', 'data-ph': 'How they look…' })
162
+ const portraitBtn = el('button', { class: 'persona-ico persona-portrait-btn', type: 'button', title: 'Paint portrait', 'data-tut': 'portrait' }, '🎨')
163
  const portraitStatus = el('span', { class: 'persona-act-status' }) // "painting via …" beside 🎨
164
  const portraitImg = el('img', { class: 'persona-portrait-img', alt: '' })
165
  const portraitWrap = el('div', { class: 'persona-portrait-wrap' }, [portraitImg])
 
363
  await putPortrait(savedId, blob)
364
  lastPersona.appearance = appearance; lastPersona.portraitUsed = appearance
365
  hasPortrait = true; setPortrait(blob); autosave()
366
+ try { opts.onPortrait?.() } catch { /* host hook */ }
367
  portraitStatus.textContent = ''
368
  } catch (e) { portraitStatus.textContent = `failed: ${e.message || e}` }
369
  finally { portraitBusy = false; portraitBtn.classList.remove('busy'); portraitBtn.disabled = false; updatePortraitUI() }
 
429
  lastPersona.voiceDesignUsed = lastPersona.voice || ''
430
  lastPersona.voiceIdUsed = lastPersona.voiceId || ''
431
  hasVoice = true; autosave()
432
+ try { opts.onVoice?.() } catch { /* host hook */ }
433
  voiceStatus.textContent = ''
434
  } catch (e) { voiceStatus.textContent = `failed: ${e.message || e}` }
435
  finally { working = false; playBtn.classList.remove('busy'); playBtn.disabled = false; updateVoiceUI() }
web/tiny.js CHANGED
@@ -32,7 +32,9 @@ const TUTORIAL_STEPS = [
32
  { anchor: 'center', nextLabel: 'Begin', skipLabel: 'No thanks', text: 'Welcome, commander. Your champions aren’t chosen from a roster — each is forged anew, body and soul. A quick tour?' },
33
  { anchor: 'bottom-center', advanceOn: 'create-open', target: '[data-tut="create"]', text: 'Recruit your first champion — tap “+ Create hero”.' },
34
  { anchor: 'center', advanceOn: 'persona', target: '[data-tut="recruit"]', text: 'Choose a calling and Recruit. Their name and legend are written for them alone — here and now.' },
35
- { anchor: 'center', advanceOn: 'spawn', target: '[data-tut="save"]', text: 'A face all their own, and a voice to match. Save & Play to bring them to the field.' },
 
 
36
  { anchor: 'bottom-left', advanceOn: 'move', target: '[data-tut="move"]', text: 'Lead them with the stick — or WASD on a keyboard.' },
37
  { anchor: 'bottom-right', advanceOn: 'kill', target: '[data-tut="attack"]', text: 'Strike with ⚔ (or Space). Most foes fall in a blow or two — thin the ranks.' },
38
  { anchor: 'bottom-right', target: '[data-tut="dodge"]', text: 'Roll with ⟲ (or Shift) to slip a blow — untouchable for a heartbeat.' },
@@ -403,6 +405,8 @@ function openCreateModal(host, onCreated) {
403
  showBarracks: true,
404
  backSlot: foot,
405
  onSaved: () => { save.disabled = false; try { tut.emit('persona') } catch { /* ignore */ } },
 
 
406
  onState: (s) => { title.textContent = s === 'portrait' ? 'Hero Details' : 'Create a Hero'; if (s === 'recruit') save.disabled = true },
407
  })
408
  const close = () => { try { creator.stop() } catch { /* ignore */ } backdrop.remove() }
@@ -411,7 +415,11 @@ function openCreateModal(host, onCreated) {
411
  backdrop.addEventListener('pointerdown', (e) => { if (e.target === backdrop) close() })
412
  save.addEventListener('click', () => {
413
  const cur = creator.current(); if (!cur.persona) return
414
- close(); onCreated(cur.persona)
 
 
 
 
415
  })
416
  }
417
  whenEl('battle-stage', async (el) => {
@@ -479,7 +487,9 @@ whenEl('battle-stage', async (el) => {
479
  // pulled from the whole pool so groups read as a varied mix of characters and monsters.
480
  const buildCineGroups = () => {
481
  if (!ROSTER_POOL.length) return []
482
- const anchors = comboCtrl.getCineAnchors?.(6) || []
 
 
483
  return anchors.map((an) => {
484
  const br = (rosters[an.biome] && rosters[an.biome].length) ? rosters[an.biome] : ROSTER_POOL
485
  const n = 13 + (Math.random() * 5 | 0) // ~15 per group (13–17)
 
32
  { anchor: 'center', nextLabel: 'Begin', skipLabel: 'No thanks', text: 'Welcome, commander. Your champions aren’t chosen from a roster — each is forged anew, body and soul. A quick tour?' },
33
  { anchor: 'bottom-center', advanceOn: 'create-open', target: '[data-tut="create"]', text: 'Recruit your first champion — tap “+ Create hero”.' },
34
  { anchor: 'center', advanceOn: 'persona', target: '[data-tut="recruit"]', text: 'Choose a calling and Recruit. Their name and legend are written for them alone — here and now.' },
35
+ { anchor: 'center', advanceOn: 'portrait', target: '[data-tut="portrait"]', text: 'Give them a face tap 🎨 to paint their portrait. (Needed before they take the field.)' },
36
+ { anchor: 'center', advanceOn: 'voice', target: '[data-tut="voice"]', text: 'And a voice — tap ▶ to hear them speak their line. This is the voice they’ll cry out in battle.' },
37
+ { anchor: 'center', advanceOn: 'spawn', target: '[data-tut="save"]', text: 'Now they’re whole — Save & Play to bring them to the field.' },
38
  { anchor: 'bottom-left', advanceOn: 'move', target: '[data-tut="move"]', text: 'Lead them with the stick — or WASD on a keyboard.' },
39
  { anchor: 'bottom-right', advanceOn: 'kill', target: '[data-tut="attack"]', text: 'Strike with ⚔ (or Space). Most foes fall in a blow or two — thin the ranks.' },
40
  { anchor: 'bottom-right', target: '[data-tut="dodge"]', text: 'Roll with ⟲ (or Shift) to slip a blow — untouchable for a heartbeat.' },
 
405
  showBarracks: true,
406
  backSlot: foot,
407
  onSaved: () => { save.disabled = false; try { tut.emit('persona') } catch { /* ignore */ } },
408
+ onPortrait: () => { try { tut.emit('portrait') } catch { /* ignore */ } },
409
+ onVoice: () => { try { tut.emit('voice') } catch { /* ignore */ } },
410
  onState: (s) => { title.textContent = s === 'portrait' ? 'Hero Details' : 'Create a Hero'; if (s === 'recruit') save.disabled = true },
411
  })
412
  const close = () => { try { creator.stop() } catch { /* ignore */ } backdrop.remove() }
 
415
  backdrop.addEventListener('pointerdown', (e) => { if (e.target === backdrop) close() })
416
  save.addEventListener('click', () => {
417
  const cur = creator.current(); if (!cur.persona) return
418
+ // The creator tracks the roster id separately (savedId) from the persona object — carry it
419
+ // through so spawnWithFly resolves the SAVED record (portrait/voice in IndexedDB, equipped
420
+ // skills, chat history). Without the id the freshly-created hero spawns "anonymous" and its
421
+ // portrait/voice/skills/chat aren't recognized until you re-pick it from the roster.
422
+ close(); onCreated(cur.savedId ? { ...cur.persona, id: cur.savedId } : cur.persona)
423
  })
424
  }
425
  whenEl('battle-stage', async (el) => {
 
487
  // pulled from the whole pool so groups read as a varied mix of characters and monsters.
488
  const buildCineGroups = () => {
489
  if (!ROSTER_POOL.length) return []
490
+ // 4 anchors × ~15 = ~60 decor sprites — enough armies for the tour without the per-frame cost
491
+ // of ~90 animated sprites (which caused slowdowns).
492
+ const anchors = comboCtrl.getCineAnchors?.(4) || []
493
  return anchors.map((an) => {
494
  const br = (rosters[an.biome] && rosters[an.biome].length) ? rosters[an.biome] : ROSTER_POOL
495
  const n = 13 + (Math.random() * 5 | 0) // ~15 per group (13–17)