diff --git a/game.js b/game.js index 7042f17..56864f2 100644 --- a/game.js +++ b/game.js @@ -677,7 +677,7 @@ // along the floor (ground projectiles with `tall`, so a jump does not clear them), 16 each, 48 total, fading after // SO.roar.range px. SOLAN is free after the 0.5 s cast; a rival farther away than the range is safe. function startSunroar(actor = hero) { - const group = { t: 0, owner: actor === hero ? 'player' : 'enemy', facing: actor.facing, roars: 0, phase: 'cutin', kind: actor.bwRage ? 'tornado' : 'forest', cf: CF[actor === hero ? selectedCharacter : opponentCharacter] ? (actor === hero ? selectedCharacter : opponentCharacter) : null }; + const group = { t: 0, owner: actor === hero ? 'player' : 'enemy', facing: actor.facing, roars: 0, phase: 'cutin', kind: actor.bwRage ? (CF[actor === hero ? selectedCharacter : opponentCharacter] ? 'mode' : 'tornado') : 'forest', cf: CF[actor === hero ? selectedCharacter : opponentCharacter] ? (actor === hero ? selectedCharacter : opponentCharacter) : null }; if (actor === hero) sunroar = group; else enemySunroar = group; cinematic = .9; announceTimer = 0; { const vid = actor === hero ? selectedCharacter : opponentCharacter; startUltimateVoice(CF[vid] || vid === 'browo' ? vid : 'solan', actor); } @@ -705,10 +705,10 @@ // dives onto the caster under a dark screen, the rival is held, the beats land, then an optional finisher. See work/WORKFLOW.md. const cfx = { ents: [], hazards: [], marks: [] }; function cfKit(d) { - const move = (name, index = 1) => { + const move = (name, index = 1, mode = false) => { if (name === 'attack') return { name: `attack${index}`, type: 'attack', index, ...d.combo[index - 1] }; if (name === 'ultimate') return { name: 'ultimate', type: 'ultimate', duration: d.balance.castTime, damage: 0, hitAt: 1 }; - const k = d.skills[name], m = { name, type: name, knockback: 160, duration: .6, ...k, hitAt: k.hitAt ?? .5 }; + const k = (mode && d.mode?.skills?.[name]) || d.skills[name], m = { name, type: name, knockback: 160, duration: .6, ...k, hitAt: k.hitAt ?? .5 }; if (k.kind === 'projectile' || k.kind === 'wave' || k.kind === 'homing' || k.kind === 'lob') { m.projectile = true; m.range = k.range || 560; m.reach = m.range; } if (k.kind === 'dash') m.dash = k.dash; if (k.kind === 'area' || k.kind === 'pillar') { m.area = true; m.reach = k.reach || ((k.ahead || 0) + (k.radius || 0)); } @@ -717,8 +717,9 @@ const start = (a, name, index = 1) => { if (a.action || a.state === 'hurt' || a.state === 'down' || a.state === 'recover') return false; if (name !== 'attack' && a.cooldowns[name] > 0) return false; - if (name !== 'attack') a.cooldowns[name] = d.balance.cooldowns[name]; - a.action = { ...move(name, index), t: 0, fired: false, queued: 0 }; a.state = a.action.name; a.stateTime = 0; return true; + const rg = !!a.bwRage && !!d.mode && name !== 'attack'; + if (name !== 'attack') a.cooldowns[name] = rg ? (name === 'ultimate' ? 999 : (d.mode.cooldowns?.[name] ?? 6)) : d.balance.cooldowns[name]; + a.action = { ...move(name, index, rg), t: 0, fired: false, queued: 0 }; a.state = a.action.name; a.stateTime = 0; return true; }; const refund = a => { for (const n of Object.keys(d.balance.cooldowns)) a.cooldowns[n] = Math.max(0, a.cooldowns[n] - d.balance.cooldowns[n] * (d.balance.basicRefund ?? .05)); }; return { balance: d.balance, names: d.names, combo: d.combo, move, start, refund, def: d }; @@ -727,6 +728,8 @@ const enemy = actor === dummy, target = enemy ? hero : dummy, id = def.id, em = def.metrics?.emitters?.[move.name], col = def.color || '#ffd36a'; const hy = clamp(actor.y + (em?.y ?? -95), actor.y - 190, actor.y - 40), owner = enemy ? 'enemy' : 'player', key = a => id + '-' + a; const hit = (dmg, kb, x, y, knockdown = false) => enemy ? window.__game.receiveHit(dmg) : hitDummy(dmg, kb, col, x, y, knockdown, true); + const raging = !!actor.bwRage && !!def.mode, mul = raging ? (def.mode.damageMul || 1.3) : 1, radd = raging ? (def.mode.reachAdd || 15) : 0; + move = { ...move, damage: Math.round((move.damage || 0) * mul), reach: (move.reach || 0) + radd }; const k = move.kind; if (k === 'projectile' || k === 'wave' || k === 'homing') { const n = move.count || 1, ox = actor.x + actor.facing * Math.round((em?.x ?? 100) * .7); @@ -813,7 +816,7 @@ } // The ultimate director. s.t counts from the cast; beats start after `hold` seconds of camera dive. function updateCfUltimate(dt, s) { - const def = CF[s.cf], U = def.ultimate, a = s.owner === 'enemy' ? dummy : hero, target = s.owner === 'enemy' ? hero : dummy, tk = s.owner === 'enemy' ? 'player' : 'enemy'; + const def = CF[s.cf], U = s.kind === 'mode' ? def.mode.ultimate : def.ultimate, a = s.owner === 'enemy' ? dummy : hero, target = s.owner === 'enemy' ? hero : dummy, tk = s.owner === 'enemy' ? 'player' : 'enemy'; const hold = U.hold ?? 1.1, gap = U.gap ?? .12, count = U.count ?? 10, endAct = hold + (count - 1) * gap + (U.settle ?? .5), col = def.color || '#ffd36a', id = def.id; s.t += dt; const mx = (a.x + target.x) / 2, dist = Math.abs(target.x - a.x); if (s.t < hold) { bwx.zoomT = U.zoom ?? 1.7; bwx.dimT = U.dim ?? .8; bwx.cx = a.x; bwx.cy = a.y - 90; if (s.t > .15) trauma = Math.min(.35, trauma + dt * .5); } @@ -850,16 +853,20 @@ if (bwAlive(target)) { if (target === dummy) { if (!hitDummy(F2.damage, 360, col, a.x, CONFIG.groundY - 100, true, true, 2)) dotDamage(dummy, F2.damage, col); } else if (!window.__game.receiveHit(F2.damage, { shake: 2 })) dotDamage(hero, F2.damage, col); } } s.phase = s.t < .78 ? 'cutin' : 'roaring'; - if (s.t >= endAct + (U.tail ?? .5)) { if (s.owner === 'enemy') enemySunroar = null; else sunroar = null; } + if (s.t >= endAct + (U.tail ?? .5)) { + const ok = s.owner === 'enemy' ? 'enemy' : 'player'; + if (s.kind === 'mode') bwExitRage(ok); else if (def.mode) { bwEnterRage(ok); announce(def.mode.announce || 'MODE!', 1.3); effects.push({ type: 'cf-fx', asset: s.cf + '-burst', x: a.x, y: a.y - 95, facing: 1, life: .6, maxLife: .6, size: 380 }); } + if (s.owner === 'enemy') enemySunroar = null; else sunroar = null; + } } function cfCutin(key, owner) { - const el = document.querySelector('.cutin'); if (!el) return; const f = CF[key]; - if (!f) { el.style.background = ''; el.style.borderColor = ''; el.querySelector('.cutin-kicker').style.color = ''; el.querySelector('.cutin-caption em') && (el.querySelector('.cutin-caption em').style.color = ''); return; } - const c = f.cutin || {}; - el.style.background = `linear-gradient(100deg,${c.a || '#222'},${c.b || '#555'} 58%,${c.a || '#222'})`; el.style.borderColor = f.color || '#fff'; + const el = document.querySelector('.cutin'); if (!el) return; const isMode = key.endsWith('~mode'), f = CF[key.replace('~mode', '')]; + if (!f) { el.style.background = ''; el.style.borderColor = ''; el.querySelector('.cutin-kicker').style.color = ''; const e0 = el.querySelector('.cutin-caption em'); if (e0) e0.style.color = ''; return; } + const c = (isMode ? f.mode?.cutin : f.cutin) || {}, nm = isMode ? f.mode.names[3] : f.names[3]; + el.style.background = `linear-gradient(100deg,${c.a || '#222'},${c.b || '#555'} 58%,${c.a || '#222'})`; el.style.borderColor = (isMode && f.mode.aura) || f.color || '#fff'; el.querySelector('.cutin-kicker').textContent = f.name + ' / ' + owner; el.querySelector('.cutin-kicker').style.color = c.kicker || '#fff'; - el.querySelector('strong').innerHTML = c.title || f.names[3]; el.querySelector('.cutin-detail').textContent = c.detail || ''; - el.setAttribute('aria-label', f.name + ' ultimate: ' + f.names[3]); + el.querySelector('strong').innerHTML = c.title || nm; el.querySelector('.cutin-detail').textContent = c.detail || ''; + el.setAttribute('aria-label', f.name + ' ultimate: ' + nm); const em = el.querySelector('.cutin-caption em'); if (em) em.style.color = f.color || ''; } function drawCfBack() { @@ -892,20 +899,32 @@ bwx.zoom = bwx.zoomT = 1; bwx.dim = bwx.dimT = 0; bwx.rage.player = bwx.rage.enemy = 0; bwx.poison.player = bwx.poison.enemy = null; bwx.lock.player = bwx.lock.enemy = 0; bwx.dance.length = 0; bwx.giant = null; bwx.fires.length = 0; bwx.burn.player = bwx.burn.enemy = 0; bwx.burn.shown.player = bwx.burn.shown.enemy = 0; bwx.tornado = null; cfx.ents.length = cfx.hazards.length = cfx.marks.length = 0; hero.bwRage = dummy.bwRage = false; hero.bwSaved = dummy.bwSaved = null; - if (BW && selectedCharacter === 'browo') Object.assign(cooldownMax, BW.balance.cooldowns); + { const b = bwBaseCooldowns(selectedCharacter); if (b) Object.assign(cooldownMax, b); if (CF[selectedCharacter]?.mode?.form) syncPlayerForm(); } } // Rage swaps the specials: skills 1/2 and the ultimate start fresh (their own cooldowns, see BW.rageBalance); the normal // cooldowns are put back when the rampage ends. + // The post-ultimate "mode": BRO WO's Mengamuk (bespoke) or CF[id].mode (data). Same machinery for both: timer in bwx.rage[side], + // a.bwRage tells the kit to use the mode's skills, cooldowns swap in and out, and an optional `form` swaps the whole atlas. + function modeOf(id) { + if (id === 'browo') return BW ? { duration: BW.rage.duration, aura: '#ff6e28', label: 'MENGAMUK', announce: 'MENGAMUK!', names: BW.rageNames, icons: ['basic', 'rage-skill1', 'rage-skill2', 'rage-ultimate'], cooldowns: BW.rageBalance.cooldowns, speedMul: BW.rage.speedMul } : null; + const m = CF[id]?.mode; if (!m) return null; + return { duration: m.duration || 10, aura: m.aura || '#ff5a3c', label: m.label || 'MODE', announce: m.announce || 'MODE!', names: m.names, icons: ['basic', 'mode-skill1', 'mode-skill2', 'mode-ultimate'], cooldowns: { skill1: m.cooldowns?.skill1 ?? 5, skill2: m.cooldowns?.skill2 ?? 8, ultimate: 999 }, speedMul: m.speedMul || 1.15 }; + } + function cfFormOn(id, side) { return !!(CF[id]?.mode?.form && bwx.rage[side] > 0); } + function bwBaseCooldowns(id) { return id === 'browo' ? BW.balance.cooldowns : CF[id] ? CF[id].balance.cooldowns : null; } function bwEnterRage(key) { - const a = key === 'player' ? hero : dummy; bwx.rage[key] = BW.rage.duration; + const a = key === 'player' ? hero : dummy, id = key === 'player' ? selectedCharacter : opponentCharacter, M = modeOf(id); if (!M) return; + bwx.rage[key] = M.duration; a.bwSaved = { skill1: a.cooldowns.skill1, skill2: a.cooldowns.skill2, ultimate: a.cooldowns.ultimate }; a.cooldowns.skill1 = a.cooldowns.skill2 = a.cooldowns.ultimate = 0; a.bwRage = true; - if (key === 'player') Object.assign(cooldownMax, BW.rageBalance.cooldowns); + if (key === 'player') { if (CF[id]?.mode?.form) syncPlayerForm(); Object.assign(cooldownMax, M.cooldowns); } + hudIdentity = ''; } function bwExitRage(key) { - const a = key === 'player' ? hero : dummy; bwx.rage[key] = 0; a.bwRage = false; + const a = key === 'player' ? hero : dummy, id = key === 'player' ? selectedCharacter : opponentCharacter; bwx.rage[key] = 0; a.bwRage = false; if (a.bwSaved) { for (const n of ['skill1', 'skill2', 'ultimate']) a.cooldowns[n] = a.bwSaved[n]; a.bwSaved = null; } - if (key === 'player' && BW) Object.assign(cooldownMax, BW.balance.cooldowns); + if (key === 'player') { if (CF[id]?.mode?.form) syncPlayerForm(); const b = bwBaseCooldowns(id); if (b) Object.assign(cooldownMax, b); } + hudIdentity = ''; } function bwAlive(target) { return target === dummy ? !dummy.ko && dummy.state !== 'down' && dummy.state !== 'recover' : hero.hp > 0; } // Damage over time / scripted damage: no hurt animation, no knockback, no cooldown refunds; K.O. still applies. @@ -926,7 +945,7 @@ function bwPoison(key) { const D = BW.dance, p = bwx.poison[key]; if (p) p.t = D.poisonTime; else bwx.poison[key] = { t: D.poisonTime, acc: 0, shown: 0 }; } function updateBrowo(dt) { const D = BW ? BW.dance : null; if (!D) return; - hero.bwRage = selectedCharacter === 'browo' && bwx.rage.player > 0; dummy.bwRage = opponentCharacter === 'browo' && bwx.rage.enemy > 0; + hero.bwRage = !!modeOf(selectedCharacter) && bwx.rage.player > 0; dummy.bwRage = !!modeOf(opponentCharacter) && bwx.rage.enemy > 0; // Dance bursts: each burst that reaches the rival stuns them, deals damage and refreshes the poison. for (let n = bwx.dance.length - 1; n >= 0; n--) { const d = bwx.dance[n], caster = d.owner === 'enemy' ? dummy : hero, target = d.owner === 'enemy' ? hero : dummy, tk = d.owner === 'enemy' ? 'player' : 'enemy'; @@ -1167,7 +1186,7 @@ if (!hero.action && hero.hurtTime <= 0 && hero.hp > 0) { if (dir && hero.doublePose <= 0) hero.facing = dir; if (hero.runDirection && dir !== hero.runDirection) hero.runDirection = 0; - const speed = (hero.runDirection === dir && dir ? CONFIG.runSpeed : CONFIG.walkSpeed)*(selectedCharacter==='fenr'&&hero.form==='wolf'?1.08:1)*(selectedCharacter==='browo'&&bwx.rage.player>0?BW.rage.speedMul:1); + const speed = (hero.runDirection === dir && dir ? CONFIG.runSpeed : CONFIG.walkSpeed)*(selectedCharacter==='fenr'&&hero.form==='wolf'?1.08:1)*(bwx.rage.player>0?(modeOf(selectedCharacter)?.speedMul||1):1); hero.vx = approach(hero.vx, crouching ? 0 : dir * speed, dt * (hero.grounded ? 2800 : 1600)); if (hero.grounded) state(crouching ? 'crouch' : dir ? hero.runDirection ? 'run' : 'walk' : 'idle'); else state(hero.doublePose > 0 ? 'doublejump' : 'jump'); @@ -1263,7 +1282,7 @@ else if(selectedCharacter==='haldor') {manifest=window.HALDOR_MANIFEST;metrics=window.HALDOR_METRICS;images.hero=images.haldor;Object.assign(cooldownMax,HD.balance.cooldowns);} else if(selectedCharacter==='edda') {manifest=window.EDDA_MANIFEST;metrics=window.EDDA_METRICS;images.hero=images.edda;Object.assign(cooldownMax,ED.balance.cooldowns);} else if(selectedCharacter==='nib') {manifest=window.NIB_MANIFEST;metrics=window.NIB_METRICS;images.hero=images.nib;Object.assign(cooldownMax,NB.balance.cooldowns);} - else if(CF[selectedCharacter]) {manifest=CF[selectedCharacter].manifest;metrics=CF[selectedCharacter].metrics;images.hero=images[selectedCharacter];Object.assign(cooldownMax,CF[selectedCharacter].balance.cooldowns);} + else if(CF[selectedCharacter]) {const fo=cfFormOn(selectedCharacter,'player'),F0=CF[selectedCharacter];manifest=fo?F0.mode.form.manifest:F0.manifest;metrics=fo?F0.mode.form.metrics:F0.metrics;images.hero=images[fo?selectedCharacter+'-form':selectedCharacter];Object.assign(cooldownMax,CF[selectedCharacter].balance.cooldowns);} else if(selectedCharacter==='browo') {manifest=window.BROWO_MANIFEST;metrics=window.BROWO_METRICS;images.hero=images.browo;Object.assign(cooldownMax,BW.balance.cooldowns);} else if(selectedCharacter==='solan') {manifest=window.SOLAN_MANIFEST;metrics=window.SOLAN_METRICS;images.hero=images.solan;Object.assign(cooldownMax,SO.balance.cooldowns);} else if(selectedCharacter==='rhea') {manifest=window.RHEA_MANIFEST;metrics=window.RHEA_METRICS;images.hero=images.rhea;Object.assign(cooldownMax,RH.balance.cooldowns);} @@ -1800,7 +1819,7 @@ dummy.aiThink = pick || dist < 260 ? Math.min(P.reaction, 1 / 60) : P.reaction * .5; } function opponentPose() { - const m=CF[opponentCharacter]?CF[opponentCharacter].manifest:opponentCharacter==='arco'?window.MECHA_MANIFEST:opponentCharacter==='mira'?window.MIRA_MANIFEST:opponentCharacter==='cora'?window.CORA_MANIFEST:opponentCharacter==='naja'?window.NAJA_MANIFEST:opponentCharacter==='haldor'?window.HALDOR_MANIFEST:opponentCharacter==='zanni'?window.ZANNI_MANIFEST:opponentCharacter==='isolde'?window.ISOLDE_MANIFEST:opponentCharacter==='rhea'?window.RHEA_MANIFEST:opponentCharacter==='solan'?window.SOLAN_MANIFEST:opponentCharacter==='nib'?window.NIB_MANIFEST:opponentCharacter==='edda'?window.EDDA_MANIFEST:opponentCharacter==='browo'?window.BROWO_MANIFEST:dummy.form==='wolf'?window.FENR_WOLF_MANIFEST:window.FENR_HUMAN_MANIFEST; + const m=CF[opponentCharacter]?(cfFormOn(opponentCharacter,'enemy')?CF[opponentCharacter].mode.form.manifest:CF[opponentCharacter].manifest):opponentCharacter==='arco'?window.MECHA_MANIFEST:opponentCharacter==='mira'?window.MIRA_MANIFEST:opponentCharacter==='cora'?window.CORA_MANIFEST:opponentCharacter==='naja'?window.NAJA_MANIFEST:opponentCharacter==='haldor'?window.HALDOR_MANIFEST:opponentCharacter==='zanni'?window.ZANNI_MANIFEST:opponentCharacter==='isolde'?window.ISOLDE_MANIFEST:opponentCharacter==='rhea'?window.RHEA_MANIFEST:opponentCharacter==='solan'?window.SOLAN_MANIFEST:opponentCharacter==='nib'?window.NIB_MANIFEST:opponentCharacter==='edda'?window.EDDA_MANIFEST:opponentCharacter==='browo'?window.BROWO_MANIFEST:dummy.form==='wolf'?window.FENR_WOLF_MANIFEST:window.FENR_HUMAN_MANIFEST; const name=m?.frame_layout?.rows?.[dummy.state]?dummy.state:'idle',list=m?.frame_layout?.rows?.[name]||[]; let index=0; // Projectile casts that release at 50% show wind-up -> thrust -> full extension, like the player's pose. @@ -1880,7 +1899,7 @@ ctx.drawImage(images.hero, r.x, r.y, r.w, r.h, -anchorX, -anchorY, r.w, r.h); ctx.restore(); } function drawDummy() { - const pose=opponentPose(),m=pose.manifest,img=images[opponentCharacter==='fenr'?(dummy.form==='wolf'?'fenrWolf':'fenrHuman'):opponentCharacter]; + const pose=opponentPose(),m=pose.manifest,img=images[opponentCharacter==='fenr'?(dummy.form==='wolf'?'fenrWolf':'fenrHuman'):(cfFormOn(opponentCharacter,'enemy')?opponentCharacter+'-form':opponentCharacter)]; shadow(dummy.x,CONFIG.groundY,dummy.state==='down'?65:opponentCharacter==='mira'?61:opponentCharacter==='haldor'?55:43,.20); if(!m||!img)return; const r=m.frame_layout.rows[pose.state][pose.frame]; @@ -1925,8 +1944,9 @@ for (const key of ['player', 'enemy']) { if (bwx.rage[key] <= 0) continue; const a = key === 'player' ? hero : dummy; if (!bwAlive(a)) continue; + const aura = modeOf(key === 'player' ? selectedCharacter : opponentCharacter)?.aura || '#ff6e28', rgb = [1, 3, 5].map(k => parseInt(aura.slice(k, k + 2), 16)).join(','); const pulse = .75 + Math.sin(realTime * 9) * .25, g = ctx.createRadialGradient(a.x, a.y - 80, 8, a.x, a.y - 80, 125); - g.addColorStop(0, `rgba(255,110,40,${.42 * pulse})`); g.addColorStop(1, 'rgba(255,60,20,0)'); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(a.x, a.y - 80, 125, 0, 7); ctx.fill(); + g.addColorStop(0, `rgba(${rgb},${.42 * pulse})`); g.addColorStop(1, `rgba(${rgb},0)`); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(a.x, a.y - 80, 125, 0, 7); ctx.fill(); if (Math.sin(realTime * 40) > .6) spawnParticles(a.x + (random() - .5) * 50, a.y - 40 - random() * 80, '#ff8a3a', 1, 80); } } @@ -2099,10 +2119,10 @@ } function updateFighterIdentity() { if(!F)return; - const identity=selectedCharacter+(selectedCharacter==='browo'&&bwx.rage.player>0?'*rage':'')+':'+hero.form+':'+opponentCharacter+':'+dummy.form; + const identity=selectedCharacter+(bwx.rage.player>0&&modeOf(selectedCharacter)?'*mode':'')+':'+hero.form+':'+opponentCharacter+':'+dummy.form; if(identity!==hudIdentity) { hudIdentity=identity;const me=fighterInfo(selectedCharacter,hero.form),rival=fighterInfo(opponentCharacter,dummy.form); - if(selectedCharacter==='browo'&&bwx.rage.player>0){me.names=BW.rageNames;me.icons=['basic','rage-skill1','rage-skill2','rage-ultimate'].map(n=>'assets/browo/ui/icon-'+n+'.webp');} + if(bwx.rage.player>0&&modeOf(selectedCharacter)){const M=modeOf(selectedCharacter);me.names=M.names;me.icons=M.icons.map(n=>'assets/'+selectedCharacter+'/ui/icon-'+n+'.webp');} $('#player-name').textContent=me.name;$('#player-class').textContent=me.cls;$('#deck-character').textContent=me.deck; $('#player-title').textContent=me.title; $('#player-portrait').src=me.portrait;$('#player-portrait').alt='Portrait '+me.name; @@ -2111,7 +2131,7 @@ $('.fighter-two').setAttribute('aria-label','Status lawan '+opponentCharacter.toUpperCase()); document.querySelectorAll('.skill-card').forEach((card,i)=>{card.querySelector('.skill-name').textContent=me.names[i];card.querySelector('img').src=me.icons[i];card.setAttribute('aria-label',me.names[i]+', '+['Spasi','I','O','P'][i]);}); } - $('#player-status').textContent=selectedCharacter==='fenr'?(hero.form==='wolf'?'FERAL ACTIVE':'RANGER READY'):selectedCharacter==='mira'?'PILOT READY':selectedCharacter==='cora'?'WINGS READY':selectedCharacter==='naja'?'SANDS READY':selectedCharacter==='haldor'?'FURNACE HOT':selectedCharacter==='zanni'?'SHOWTIME':selectedCharacter==='isolde'?'LANCE READY':selectedCharacter==='rhea'?'ORBITS SET':selectedCharacter==='solan'?'PRIDE READY':selectedCharacter==='nib'?'ON THE ROUTE':selectedCharacter==='edda'?'STEADY AS STONE':CF[selectedCharacter]?CF[selectedCharacter].status:selectedCharacter==='browo'?(bwx.rage.player>0?'MENGAMUK '+Math.ceil(bwx.rage.player)+'s':'SAFARI READY'):'CORE ACTIVE'; + $('#player-status').textContent=selectedCharacter==='fenr'?(hero.form==='wolf'?'FERAL ACTIVE':'RANGER READY'):selectedCharacter==='mira'?'PILOT READY':selectedCharacter==='cora'?'WINGS READY':selectedCharacter==='naja'?'SANDS READY':selectedCharacter==='haldor'?'FURNACE HOT':selectedCharacter==='zanni'?'SHOWTIME':selectedCharacter==='isolde'?'LANCE READY':selectedCharacter==='rhea'?'ORBITS SET':selectedCharacter==='solan'?'PRIDE READY':selectedCharacter==='nib'?'ON THE ROUTE':selectedCharacter==='edda'?'STEADY AS STONE':CF[selectedCharacter]?(bwx.rage.player>0&&modeOf(selectedCharacter)?modeOf(selectedCharacter).label+' '+Math.ceil(bwx.rage.player)+'s':CF[selectedCharacter].status):selectedCharacter==='browo'?(bwx.rage.player>0?'MENGAMUK '+Math.ceil(bwx.rage.player)+'s':'SAFARI READY'):'CORE ACTIVE'; for(const [actor,id] of [[hero,'player-form-timer'],[dummy,'enemy-form-timer']]){const el=$('#'+id),active=actor.form==='wolf';el.hidden=!active;el.textContent=active?'FERAL '+Math.max(0,actor.formTime).toFixed(1)+' s':'';el.style.setProperty?.('--remaining',String(actor.formTime/F.balance.duration));} } function updateHud() { @@ -2161,10 +2181,10 @@ // One banner at a time: FENR transformation, then ARCO, then a MIRA/CORA/NAJA/HALDOR summon still inside its cut-in window. const cutin=$('#ultimate-cutin'),live=g=>g&&g.t{cutinArt.style.visibility='';};}cutin.querySelector('.cutin-kicker').textContent={fenr:'FENR / '+cutinOwner,mira:'MIRA / '+cutinOwner,cora:'CORA / '+cutinOwner,naja:'NAJA / '+cutinOwner,haldor:'HALDOR / '+cutinOwner,zanni:'ZANNI / '+cutinOwner,isolde:'ISOLDE / '+cutinOwner,rhea:'RHEA / '+cutinOwner,solan:'SOLAN / '+cutinOwner,browo:'BRO WO / '+cutinOwner,browofire:'BRO WO / '+cutinOwner,nib:'NIB / '+cutinOwner,edda:'EDDA / '+cutinOwner,arco:'ARCO / AETHER COMMAND'}[cutinKey];cutin.querySelector('strong').innerHTML={fenr:'FERAL
AWAKENING',mira:'ROCKET
PARADE',cora:'NIGHT
MURMURATION',naja:'DUNE
SERPENT',haldor:'FORGE
QUAKE',zanni:'GRAND
FINALE',isolde:'SKYFALL
LANCES',rhea:'GRAND
ORRERY',solan:'SUNMANE
ROAR',browo:'HUTAN
SAWIT',browofire:'TORNADO
API SAWIT',nib:'SPECIAL
DELIVERY',edda:'ELDER
TORTOISE',arco:'HELIOS
SQUADRON'}[cutinKey];cutin.querySelector('.cutin-detail').textContent={fenr:'THE BEAST WITHIN',mira:'TWELVE-ROCKET SALVO',cora:'THREE-PASS RAVEN STORM',naja:'THREE-STRIKE SAND COBRA',haldor:'THREE-SLAM MOLTEN SHOCKWAVE',zanni:'THREE-RING BLADE BOOMERANG',isolde:'THREE-LANCE ICE DIVE',rhea:'THREE-PLANET ORBIT',solan:'THREE-ROAR SHOCKWAVE',browo:'TEN-PALM RUN + GIANT SLAM',browofire:'FIRE TORNADO FINISHER',nib:'THREE-PARCEL HOMING RUN',edda:'THREE-STOMP SPIRIT WALK',arco:'ORBITAL LASER STRIKE'}[cutinKey];cutin.setAttribute('aria-label',{fenr:'FENR ultimate: Feral Awakening',mira:'MIRA ultimate: Rocket Parade',cora:'CORA ultimate: Night Murmuration',naja:'NAJA ultimate: Dune Serpent',haldor:'HALDOR ultimate: Forge Quake',zanni:'ZANNI ultimate: Grand Finale',isolde:'ISOLDE ultimate: Skyfall Lances',rhea:'RHEA ultimate: Grand Orrery',solan:'SOLAN ultimate: Sunmane Roar',browo:'BRO WO ultimate: Hutan Sawit',browofire:'BRO WO rage ultimate: Tornado Api Sawit',nib:'NIB ultimate: Special Delivery',edda:'EDDA ultimate: Elder Tortoise',arco:'ARCO ultimate: Helios Squadron'}[cutinKey]);cfCutin(cutinKey,cutinOwner);} const visible = ct < SQUAD.cutinDuration; cutin.classList.toggle('visible', visible); cutin.setAttribute('aria-hidden', String(!visible)); @@ -2176,9 +2196,9 @@ // Cut-in art per fighter. Only the two fighters of the current fight are warmed (at start/select), so a banner never waits // for its image; until the right image is decoded the art stays hidden (see updateHud) instead of showing the last one. const CUTIN_ART={fenr:'assets/fenr/ui/cutin.webp',mira:'assets/mira/ui/cutin.webp',cora:'assets/cora/ui/cutin.webp',naja:'assets/naja/ui/cutin.webp',haldor:'assets/haldor/ui/cutin.webp',zanni:'assets/zanni/ui/cutin.webp',isolde:'assets/isolde/ui/cutin.webp',rhea:'assets/rhea/ui/cutin.webp',solan:'assets/solan/ui/cutin.webp',browo:'assets/browo/ui/cutin.webp',browofire:'assets/browo/ui/cutin-fire.webp',nib:'assets/nib/ui/cutin.webp',edda:'assets/edda/ui/cutin.webp',arco:'assets/ui/ultimate-cutin.webp'}; - for(const id of Object.keys(CF))CUTIN_ART[id]='assets/'+id+'/ui/cutin.webp'; + for(const id of Object.keys(CF)){CUTIN_ART[id]='assets/'+id+'/ui/cutin.webp';if(CF[id].mode)CUTIN_ART[id+'~mode']='assets/'+id+'/ui/cutin-mode.webp';} let cutinWarm=[]; - function warmCutins(){cutinWarm=[...new Set([selectedCharacter,opponentCharacter])].flatMap(id=>id==='browo'?[CUTIN_ART.browo,CUTIN_ART.browofire]:[CUTIN_ART[id]]).filter(Boolean).map(src=>{const im=new Image();im.decoding='async';im.src=src;return im;});} + function warmCutins(){cutinWarm=[...new Set([selectedCharacter,opponentCharacter])].flatMap(id=>id==='browo'?[CUTIN_ART.browo,CUTIN_ART.browofire]:[CUTIN_ART[id],CUTIN_ART[id+'~mode']]).filter(Boolean).map(src=>{const im=new Image();im.decoding='async';im.src=src;return im;});} function setPaused(value) { paused = value; clearInput(); syncUltimateVoice(); $('#pause-btn').textContent = value ? '▶' : 'Ⅱ'; const panel = $('#pause-panel'); if (value && !panel.open) panel.showModal(); if (!value && panel.open) panel.close(); if (!value) canvas.focus({ preventScroll: true }); } function openSettings() { settingsOpen = true; clearInput(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); $('#close-settings').innerHTML=(menuOpen?'Kembali ke menu':'Kembali ke arena')+' ↗'; $('#settings-panel').showModal(); } function closeSettings() { settingsOpen = false; $('#settings-panel').close(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); canvas.focus({ preventScroll: true }); } @@ -2219,7 +2239,7 @@ if(window.FRONTEND_ENABLED)setMenuOpen(true); resize(); requestAnimationFrame(tick); // Wait for the first-visit download (preload.js) so these requests are served from the cache, not fetched twice. - Promise.resolve(window.AETHER_PRELOAD).then(() => Promise.all([loadImage('hero', 'assets/mecha/run/sprite-sheet-alpha.webp', true), loadImage('stage', 'assets/stage.webp', true), ...(Rules?Object.entries(Rules.stages).filter(([id])=>id!=='bellora').map(([id,s])=>loadImage('stage-'+id,s.image,true)):[]), ...(F?[loadImage('fenrHuman','assets/fenr/human/run/sprite-sheet-alpha.webp',true),loadImage('fenrWolf','assets/fenr/wolf/run/sprite-sheet-alpha.webp',true),...['claw','gale','rush','bite','howl','transform'].map(name=>loadImage('fx-'+name,'assets/fenr/ui/fx-'+name+'.webp',true))]:[]), ...(M?[loadImage('mira','assets/mira/run/sprite-sheet-alpha.webp',true),...['star','rocket','burst','crash'].map(name=>loadImage('fx-mira-'+name,'assets/mira/ui/fx-'+name+'.webp',true))]:[]), ...(C?[loadImage('cora','assets/cora/run/sprite-sheet-alpha.webp',true),...['feather','gust','raven'].map(name=>loadImage('fx-cora-'+name,'assets/cora/ui/fx-'+name+'.webp',true))]:[]), ...(N?[loadImage('naja','assets/naja/run/sprite-sheet-alpha.webp',true),...['sandwave','cyclone','serpent','ripple'].map(name=>loadImage('fx-naja-'+name,'assets/naja/ui/fx-'+name+'.webp',true))]:[]), ...(HD?[loadImage('haldor','assets/haldor/run/sprite-sheet-alpha.webp',true),...['slag','splash','steam','quake'].map(name=>loadImage('fx-haldor-'+name,'assets/haldor/ui/fx-'+name+'.webp',true))]:[]), ...(Z?[loadImage('zanni','assets/zanni/run/sprite-sheet-alpha.webp',true),...['ring','bigring','snatch','confetti'].map(name=>loadImage('fx-zanni-'+name,'assets/zanni/ui/fx-'+name+'.webp',true))]:[]), ...(IS?[loadImage('isolde','assets/isolde/run/sprite-sheet-alpha.webp',true),...['piercer','skyfall','shatter','frost'].map(name=>loadImage('fx-isolde-'+name,'assets/isolde/ui/fx-'+name+'.webp',true))]:[]), ...(RH?[loadImage('rhea','assets/rhea/run/sprite-sheet-alpha.webp',true),...['drift','planet','well','burst'].map(name=>loadImage('fx-rhea-'+name,'assets/rhea/ui/fx-'+name+'.webp',true))]:[]), ...(SO?[loadImage('solan','assets/solan/run/sprite-sheet-alpha.webp',true),...['crescent','roar','impact','sunburst'].map(name=>loadImage('fx-solan-'+name,'assets/solan/ui/fx-'+name+'.webp',true))]:[]), ...(NB?[loadImage('nib','assets/nib/run/sprite-sheet-alpha.webp',true),...['letter','plane','slip','stamp'].map(name=>loadImage('fx-nib-'+name,'assets/nib/ui/fx-'+name+'.webp',true))]:[]), ...(BW?[loadImage('browo','assets/browo/run/sprite-sheet-alpha.webp',true),...['sawit','impact','sunburst','truck','burntree','tornado'].map(name=>loadImage('fx-browo-'+name,'assets/browo/ui/fx-'+name+'.webp',true))]:[]), ...Object.values(CF).flatMap(f=>[loadImage(f.id,'assets/'+f.id+'/run/sprite-sheet-alpha.webp',true),...f.fx.map(n=>loadImage('fx-'+f.id+'-'+n,'assets/'+f.id+'/ui/fx-'+n+'.webp',true))]), ...(ED?[loadImage('edda','assets/edda/run/sprite-sheet-alpha.webp',true),...['stone','ripple','shell','tortoise','stomp'].map(name=>loadImage('fx-edda-'+name,'assets/edda/ui/fx-'+name+'.webp',true))]:[]), loadImage('drone', 'assets/ui/drone.png', true), loadImage('cutin', 'assets/ui/ultimate-cutin.webp', true)])).then(results => { + Promise.resolve(window.AETHER_PRELOAD).then(() => Promise.all([loadImage('hero', 'assets/mecha/run/sprite-sheet-alpha.webp', true), loadImage('stage', 'assets/stage.webp', true), ...(Rules?Object.entries(Rules.stages).filter(([id])=>id!=='bellora').map(([id,s])=>loadImage('stage-'+id,s.image,true)):[]), ...(F?[loadImage('fenrHuman','assets/fenr/human/run/sprite-sheet-alpha.webp',true),loadImage('fenrWolf','assets/fenr/wolf/run/sprite-sheet-alpha.webp',true),...['claw','gale','rush','bite','howl','transform'].map(name=>loadImage('fx-'+name,'assets/fenr/ui/fx-'+name+'.webp',true))]:[]), ...(M?[loadImage('mira','assets/mira/run/sprite-sheet-alpha.webp',true),...['star','rocket','burst','crash'].map(name=>loadImage('fx-mira-'+name,'assets/mira/ui/fx-'+name+'.webp',true))]:[]), ...(C?[loadImage('cora','assets/cora/run/sprite-sheet-alpha.webp',true),...['feather','gust','raven'].map(name=>loadImage('fx-cora-'+name,'assets/cora/ui/fx-'+name+'.webp',true))]:[]), ...(N?[loadImage('naja','assets/naja/run/sprite-sheet-alpha.webp',true),...['sandwave','cyclone','serpent','ripple'].map(name=>loadImage('fx-naja-'+name,'assets/naja/ui/fx-'+name+'.webp',true))]:[]), ...(HD?[loadImage('haldor','assets/haldor/run/sprite-sheet-alpha.webp',true),...['slag','splash','steam','quake'].map(name=>loadImage('fx-haldor-'+name,'assets/haldor/ui/fx-'+name+'.webp',true))]:[]), ...(Z?[loadImage('zanni','assets/zanni/run/sprite-sheet-alpha.webp',true),...['ring','bigring','snatch','confetti'].map(name=>loadImage('fx-zanni-'+name,'assets/zanni/ui/fx-'+name+'.webp',true))]:[]), ...(IS?[loadImage('isolde','assets/isolde/run/sprite-sheet-alpha.webp',true),...['piercer','skyfall','shatter','frost'].map(name=>loadImage('fx-isolde-'+name,'assets/isolde/ui/fx-'+name+'.webp',true))]:[]), ...(RH?[loadImage('rhea','assets/rhea/run/sprite-sheet-alpha.webp',true),...['drift','planet','well','burst'].map(name=>loadImage('fx-rhea-'+name,'assets/rhea/ui/fx-'+name+'.webp',true))]:[]), ...(SO?[loadImage('solan','assets/solan/run/sprite-sheet-alpha.webp',true),...['crescent','roar','impact','sunburst'].map(name=>loadImage('fx-solan-'+name,'assets/solan/ui/fx-'+name+'.webp',true))]:[]), ...(NB?[loadImage('nib','assets/nib/run/sprite-sheet-alpha.webp',true),...['letter','plane','slip','stamp'].map(name=>loadImage('fx-nib-'+name,'assets/nib/ui/fx-'+name+'.webp',true))]:[]), ...(BW?[loadImage('browo','assets/browo/run/sprite-sheet-alpha.webp',true),...['sawit','impact','sunburst','truck','burntree','tornado'].map(name=>loadImage('fx-browo-'+name,'assets/browo/ui/fx-'+name+'.webp',true))]:[]), ...Object.values(CF).flatMap(f=>[loadImage(f.id,'assets/'+f.id+'/run/sprite-sheet-alpha.webp',true),...(f.mode?.form?[loadImage(f.id+'-form','assets/'+f.id+'/form/run/sprite-sheet-alpha.webp',true)]:[]),...f.fx.map(n=>loadImage('fx-'+f.id+'-'+n,'assets/'+f.id+'/ui/fx-'+n+'.webp',true))]), ...(ED?[loadImage('edda','assets/edda/run/sprite-sheet-alpha.webp',true),...['stone','ripple','shell','tortoise','stomp'].map(name=>loadImage('fx-edda-'+name,'assets/edda/ui/fx-'+name+'.webp',true))]:[]), loadImage('drone', 'assets/ui/drone.png', true), loadImage('cutin', 'assets/ui/ultimate-cutin.webp', true)])).then(results => { images['stage-bellora']=images.stage;images.arco=images.hero;if(F)syncPlayerForm(); ready = results.every(Boolean) && (!F || (!!window.FENR_HUMAN_MANIFEST && !!window.FENR_WOLF_MANIFEST)) && !!manifest && frames('idle').length > 0; if (ready) { $('#load-state').classList.add('hidden'); announce('TRAINING READY', 1.65); }