Spaces:
Running
Running
Chat voice loading indicator + rebuild combat gating/feedback bundles
Browse filesVoice 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 +5 -4
- web/classesSandbox.js +1 -1
- web/comboBattler.js +27 -4
- web/enemiesSandbox.js +1 -1
- web/engine.js +14 -0
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 =
|
|
|
|
| 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" : ""),
|
| 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" : ""),
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6450 |
}
|
| 6451 |
if (req.skill) {
|
| 6452 |
const s = p?.bar?.[req.skill - 1];
|
| 6453 |
req.skill = 0;
|
| 6454 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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" : ""),
|
| 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,
|