polats Claude Opus 4.8 (1M context) commited on
Commit
f1fdeff
·
1 Parent(s): 68fbbfb

Chat voice loading indicator + rebuild combat gating/feedback bundles

Browse files

Voice button now shows the loading state while the line is being generated (was jumping
straight to stop). Rebuild engine/combo/sandbox bundles for freeCast-off gating, over-head
'no target'/resource feedback, and yellow skill text.

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

web/characterChat.js CHANGED
@@ -14,7 +14,7 @@ import {
14
 
15
  const msgId = () => 'm_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
16
  const HISTORY_CAP = 50
17
- const SPEAKER = '🔊', STOP = '⏹'
18
 
19
  export function mountCharacterChat(host, personaId, { onUpdate, getLive } = {}) {
20
  const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;height:100%;min-height:0'
@@ -39,9 +39,10 @@ export function mountCharacterChat(host, personaId, { onUpdate, getLive } = {})
39
  async function speak(id, text, p, btn) {
40
  if (playing && playing.id === id) { stopAll(); return } // tap the playing one → stop (toggle off)
41
  stopAll() // stop any other line first
42
- playing = { id, btn }; btn.textContent = STOP
 
43
  try {
44
- if (activeEngineIsNative()) { await ensureTts(); await speakVoiceLive(p.voiceId || currentVoiceId(), text) }
45
  else {
46
  let bytes
47
  const blob = await getChatAudio(id)
@@ -62,7 +63,7 @@ export function mountCharacterChat(host, personaId, { onUpdate, getLive } = {})
62
  bytes = wav instanceof Blob ? await wav.arrayBuffer() : wav
63
  try { await putChatAudio(id, new Blob([bytes.slice(0)])) } catch { /* cache best-effort */ }
64
  }
65
- if (playing && playing.id === id) await playWav(bytes.slice(0)) // skip if stopped during synth
66
  }
67
  } catch { /* voice is best-effort */ } finally { if (playing && playing.id === id) { btn.textContent = SPEAKER; playing = null } }
68
  }
 
14
 
15
  const msgId = () => 'm_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
16
  const HISTORY_CAP = 50
17
+ const SPEAKER = '🔊', STOP = '⏹', LOADING = '⏳'
18
 
19
  export function mountCharacterChat(host, personaId, { onUpdate, getLive } = {}) {
20
  const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;height:100%;min-height:0'
 
39
  async function speak(id, text, p, btn) {
40
  if (playing && playing.id === id) { stopAll(); return } // tap the playing one → stop (toggle off)
41
  stopAll() // stop any other line first
42
+ playing = { id, btn }; btn.textContent = LOADING // ⏳ while preparing/generating the voice
43
+ const toStop = () => { if (playing && playing.id === id) btn.textContent = STOP }
44
  try {
45
+ if (activeEngineIsNative()) { await ensureTts(); toStop(); await speakVoiceLive(p.voiceId || currentVoiceId(), text) }
46
  else {
47
  let bytes
48
  const blob = await getChatAudio(id)
 
63
  bytes = wav instanceof Blob ? await wav.arrayBuffer() : wav
64
  try { await putChatAudio(id, new Blob([bytes.slice(0)])) } catch { /* cache best-effort */ }
65
  }
66
+ if (playing && playing.id === id) { toStop(); await playWav(bytes.slice(0)) } // ⏹ once actually playing; skip if stopped during synth
67
  }
68
  } catch { /* voice is best-effort */ } finally { if (playing && playing.id === id) { btn.textContent = SPEAKER; playing = null } }
69
  }
web/classesSandbox.js CHANGED
@@ -2588,7 +2588,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
2588
  if (!playGridOnce(v, play?.animGrid)) playAttack(e.who, a);
2589
  spawnEffects(e.who, play?.effects);
2590
  }
2591
- if (!floatIcon(e.who, e.skillId)) floatText(e.who, e.name + (e.elite ? " \u2605" : ""), 15787730);
2592
  if (e.combo === "dual") floatText(e.who, "\u2726", GOLD);
2593
  }
2594
  } else if (e.kind === "swing") {
 
2588
  if (!playGridOnce(v, play?.animGrid)) playAttack(e.who, a);
2589
  spawnEffects(e.who, play?.effects);
2590
  }
2591
+ if (!floatIcon(e.who, e.skillId)) floatText(e.who, e.name + (e.elite ? " \u2605" : ""), 16769357);
2592
  if (e.combo === "dual") floatText(e.who, "\u2726", GOLD);
2593
  }
2594
  } else if (e.kind === "swing") {
web/comboBattler.js CHANGED
@@ -4942,7 +4942,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
4942
  if (!playGridOnce(v, play?.animGrid)) playAttack(e.who, a);
4943
  spawnEffects(e.who, play?.effects);
4944
  }
4945
- if (!floatIcon(e.who, e.skillId)) floatText(e.who, e.name + (e.elite ? " \u2605" : ""), 15787730);
4946
  if (e.combo === "dual") floatText(e.who, "\u2726", GOLD);
4947
  }
4948
  } else if (e.kind === "swing") {
@@ -5765,6 +5765,19 @@ function resolveOverlaps(b) {
5765
  }
5766
  }
5767
  var isImmovable = (b, a) => !!a.casting || isKd(b, a);
 
 
 
 
 
 
 
 
 
 
 
 
 
5768
  function stepPlayer(b, a, foe, dt) {
5769
  const cmd = b.input && b.input[a.id] || {};
5770
  const mx = cmd.moveX || 0, my = cmd.moveY || 0;
@@ -6017,6 +6030,8 @@ async function createEmoteLayer(pixi, parent, { url, unit, sizeTiles = 1.3, head
6017
 
6018
  // ../auto-battler/src/render/comboBattler.js
6019
  var TILE8 = 8;
 
 
6020
  var SPRITE_TILES = 0.95;
6021
  var STEP = 0.05;
6022
  var SPAWN_TILES = 22;
@@ -6446,12 +6461,20 @@ function mountComboBattler(pixi, host, opts = {}) {
6446
  } else setInput(battle, "P0", { moveX: 0, moveY: 0 });
6447
  if (req.attack) {
6448
  req.attack = false;
6449
- if (p && nearestFoeDist() <= p.weapon.range + p.radius) setInput(battle, "P0", { action: "basic" });
 
 
 
 
6450
  }
6451
  if (req.skill) {
6452
  const s = p?.bar?.[req.skill - 1];
6453
  req.skill = 0;
6454
- if (s) setInput(battle, "P0", { action: s.id });
 
 
 
 
6455
  }
6456
  if (hitstopT > 0) hitstopT = Math.max(0, hitstopT - dt);
6457
  else {
@@ -6701,7 +6724,7 @@ function mountComboBattler(pixi, host, opts = {}) {
6701
  const pProf = player?.unit?.profession;
6702
  const pSkills = player?.unit?.skills?.length ? player.unit.skills : CB_SKILLS.filter((s) => s.profession === pProf).slice(0, 3).map((s) => s.id);
6703
  const players = [{ ...player?.unit || {}, name: player?.name || "Hero", control: "player", skills: pSkills }];
6704
- battle = makeTeamBattle({ seed, players, enemies: [], sandbox: true, freeCast: true, world, field });
6705
  const sw = getSpawnWorld();
6706
  const start = worldToField(sw.x, sw.y);
6707
  const p0 = snapField(start.x, start.y);
 
4942
  if (!playGridOnce(v, play?.animGrid)) playAttack(e.who, a);
4943
  spawnEffects(e.who, play?.effects);
4944
  }
4945
+ if (!floatIcon(e.who, e.skillId)) floatText(e.who, e.name + (e.elite ? " \u2605" : ""), 16769357);
4946
  if (e.combo === "dual") floatText(e.who, "\u2726", GOLD);
4947
  }
4948
  } else if (e.kind === "swing") {
 
5765
  }
5766
  }
5767
  var isImmovable = (b, a) => !!a.casting || isKd(b, a);
5768
+ function playerActionReason(b, a, action) {
5769
+ if (!a || !action) return null;
5770
+ const foe = nearestFoe(b, a);
5771
+ if (action === "basic") return !foe || edgeGap(a, foe) > reachOf(a) ? "no-target" : null;
5772
+ const s = a.bar.find((x) => x.id === action);
5773
+ if (!s) return null;
5774
+ if (s.cost?.adrenaline && a.adrenaline < s.cost.adrenaline) return "need-adrenaline";
5775
+ if (s.cost?.energy && a.energy < s.cost.energy) return "need-energy";
5776
+ if (b.t < (a.recharge[s.name] || 0)) return "recharging";
5777
+ if (isAttack(s)) return !foe || edgeGap(a, foe) > reachOf(a) ? "no-target" : null;
5778
+ if (!isSupport(s)) return !foe || dist2(a, foe) > SPELL_RANGE ? "no-target" : null;
5779
+ return null;
5780
+ }
5781
  function stepPlayer(b, a, foe, dt) {
5782
  const cmd = b.input && b.input[a.id] || {};
5783
  const mx = cmd.moveX || 0, my = cmd.moveY || 0;
 
6030
 
6031
  // ../auto-battler/src/render/comboBattler.js
6032
  var TILE8 = 8;
6033
+ var SKILL_YELLOW = 16769357;
6034
+ var FEED = { "no-target": "no target", "need-adrenaline": "need adrenaline", "need-energy": "need energy" };
6035
  var SPRITE_TILES = 0.95;
6036
  var STEP = 0.05;
6037
  var SPAWN_TILES = 22;
 
6461
  } else setInput(battle, "P0", { moveX: 0, moveY: 0 });
6462
  if (req.attack) {
6463
  req.attack = false;
6464
+ if (p) {
6465
+ const why = playerActionReason(battle, p, "basic");
6466
+ if (why === "no-target") R.floatText(p.id, "no target", SKILL_YELLOW);
6467
+ else setInput(battle, "P0", { action: "basic" });
6468
+ }
6469
  }
6470
  if (req.skill) {
6471
  const s = p?.bar?.[req.skill - 1];
6472
  req.skill = 0;
6473
+ if (p && s) {
6474
+ const why = playerActionReason(battle, p, s.id);
6475
+ if (why && why !== "recharging") R.floatText(p.id, FEED[why] || "cannot use", SKILL_YELLOW);
6476
+ else setInput(battle, "P0", { action: s.id });
6477
+ }
6478
  }
6479
  if (hitstopT > 0) hitstopT = Math.max(0, hitstopT - dt);
6480
  else {
 
6724
  const pProf = player?.unit?.profession;
6725
  const pSkills = player?.unit?.skills?.length ? player.unit.skills : CB_SKILLS.filter((s) => s.profession === pProf).slice(0, 3).map((s) => s.id);
6726
  const players = [{ ...player?.unit || {}, name: player?.name || "Hero", control: "player", skills: pSkills }];
6727
+ battle = makeTeamBattle({ seed, players, enemies: [], sandbox: true, freeCast: false, world, field });
6728
  const sw = getSpawnWorld();
6729
  const start = worldToField(sw.x, sw.y);
6730
  const p0 = snapField(start.x, start.y);
web/enemiesSandbox.js CHANGED
@@ -2551,7 +2551,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
2551
  if (!playGridOnce(v, play?.animGrid)) playAttack(e.who, a);
2552
  spawnEffects(e.who, play?.effects);
2553
  }
2554
- if (!floatIcon(e.who, e.skillId)) floatText(e.who, e.name + (e.elite ? " \u2605" : ""), 15787730);
2555
  if (e.combo === "dual") floatText(e.who, "\u2726", GOLD);
2556
  }
2557
  } else if (e.kind === "swing") {
 
2551
  if (!playGridOnce(v, play?.animGrid)) playAttack(e.who, a);
2552
  spawnEffects(e.who, play?.effects);
2553
  }
2554
+ if (!floatIcon(e.who, e.skillId)) floatText(e.who, e.name + (e.elite ? " \u2605" : ""), 16769357);
2555
  if (e.combo === "dual") floatText(e.who, "\u2726", GOLD);
2556
  }
2557
  } else if (e.kind === "swing") {
web/engine.js CHANGED
@@ -1488,6 +1488,19 @@ function resolveOverlaps(b) {
1488
  }
1489
  }
1490
  var isImmovable = (b, a) => !!a.casting || isKd(b, a);
 
 
 
 
 
 
 
 
 
 
 
 
 
1491
  function stepPlayer(b, a, foe, dt) {
1492
  const cmd = b.input && b.input[a.id] || {};
1493
  const mx = cmd.moveX || 0, my = cmd.moveY || 0;
@@ -1651,6 +1664,7 @@ export {
1651
  FIELD,
1652
  isSupport,
1653
  makeTeamBattle,
 
1654
  removeActor,
1655
  runToEnd,
1656
  setInput,
 
1488
  }
1489
  }
1490
  var isImmovable = (b, a) => !!a.casting || isKd(b, a);
1491
+ function playerActionReason(b, a, action) {
1492
+ if (!a || !action) return null;
1493
+ const foe = nearestFoe(b, a);
1494
+ if (action === "basic") return !foe || edgeGap(a, foe) > reachOf(a) ? "no-target" : null;
1495
+ const s = a.bar.find((x) => x.id === action);
1496
+ if (!s) return null;
1497
+ if (s.cost?.adrenaline && a.adrenaline < s.cost.adrenaline) return "need-adrenaline";
1498
+ if (s.cost?.energy && a.energy < s.cost.energy) return "need-energy";
1499
+ if (b.t < (a.recharge[s.name] || 0)) return "recharging";
1500
+ if (isAttack(s)) return !foe || edgeGap(a, foe) > reachOf(a) ? "no-target" : null;
1501
+ if (!isSupport(s)) return !foe || dist(a, foe) > SPELL_RANGE ? "no-target" : null;
1502
+ return null;
1503
+ }
1504
  function stepPlayer(b, a, foe, dt) {
1505
  const cmd = b.input && b.input[a.id] || {};
1506
  const mx = cmd.moveX || 0, my = cmd.moveY || 0;
 
1664
  FIELD,
1665
  isSupport,
1666
  makeTeamBattle,
1667
+ playerActionReason,
1668
  removeActor,
1669
  runToEnd,
1670
  setInput,