Bro Wo mengamuk: skill diganti (Truk Katering, Hutan Terbakar), ultimate jadi Tornado Api Sawit

Saat mengamuk 10 detik, skill 1 dan 2 berganti jurus baru dengan cooldown sendiri, ikon dan nama HUD
ikut berubah, dan ultimate menjadi Tornado Api Sawit (sekali pakai, mengakhiri mengamuk). Efek truk,
pohon terbakar, tornado, ikon, dan cut-in baru ditambahkan.
This commit is contained in:
rizkyaz0 2026-10-01 12:44:14 +07:00
parent 87de6cb108
commit 16c20bbdc1
9 changed files with 143 additions and 22 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -25,20 +25,40 @@
giantAt:2.85, giantRise:.55, giantHold:.3, slam:.16, giantDamage:40, tail:.55 }; giantAt:2.85, giantRise:.55, giantHold:.3, slam:.16, giantDamage:40, tail:.55 };
forest.release = forest.giantAt + forest.giantRise + forest.giantHold + forest.slam; forest.release = forest.giantAt + forest.giantRise + forest.giantHold + forest.slam;
forest.duration = forest.release + forest.tail; forest.duration = forest.release + forest.tail;
// Mengamuk: for `duration` s after the ultimate BRO WO carries a palm tree as a club: stronger hits, longer reach, faster. // Mengamuk: for `duration` s after the ultimate BRO WO is stronger, longer-reaching and faster, and his three specials are
// replaced (skills 1 and 2 are refreshed with their own cooldowns, the ultimate is usable once and ends the rampage):
// skill1 TRUK KATERING - a delivery truck crosses the whole stage at `speed` px/s, hits once (`damage`, knockdown).
// skill2 HUTAN TERBAKAR - `palms` palms catch fire in a row ahead; each burns an area for `life` s at `dps` HP/s.
// ultimate TORNADO API SAWIT - a fire tornado hunts the rival, holds and burns them, then explodes.
const rage = { duration:10, damageMul:1.4, speedMul:1.25, reachAdd:25 }; const rage = { duration:10, damageMul:1.4, speedMul:1.25, reachAdd:25 };
const rageNames = ['TINJU API','TRUK KATERING','HUTAN TERBAKAR','TORNADO API SAWIT'];
const rageBalance = { cooldowns:{skill1:5,skill2:8,ultimate:999} };
const truck = { speed:1250, damage:24, tall:120, size:360 };
const fire = { palms:5, first:130, gap:105, radius:70, ignite:8, dps:6, life:3.6, delay:.09 };
// Tornado, seconds from the cast: the camera dives on BRO WO (`spawnAt`), the tornado is born and then chases the rival at
// `chase` px/s for `life` s; inside `radius` the rival is held, pulled to its core and burned for `tickDamage` every `tick` s.
// It ends in an explosion (`blast`, knockdown) and the rampage ends with it.
const tornado = { cutinDuration:.78, spawnAt:.85, life:3.6, chase:330, radius:150, tick:.22, tickDamage:4, blast:30, tail:.6 };
tornado.duration = tornado.spawnAt + tornado.life + tornado.tail;
function move(name,index=1) { function move(name,index=1) {
if(name==='attack')return {name:`attack${index}`,type:'attack',index,...combo[index-1]}; if(name==='attack')return {name:`attack${index}`,type:'attack',index,...combo[index-1]};
if(name==='skill1')return {name,type:name,damage:18,duration:.62,knockback:210,tree:true,reach:tree.ahead+tree.radius,hitAt:.55}; if(name==='skill1')return {name,type:name,damage:18,duration:.62,knockback:210,tree:true,reach:tree.ahead+tree.radius,hitAt:.55};
if(name==='skill2')return {name,type:name,damage:0,duration:dance.duration,knockback:0,dance:true,loops:dance.loops,reach:dance.radius,hitAt:.04}; if(name==='skill2')return {name,type:name,damage:0,duration:dance.duration,knockback:0,dance:true,loops:dance.loops,reach:dance.radius,hitAt:.04};
return {name:'ultimate',type:'ultimate',duration:balance.castTime,damage:0,hitAt:1}; return {name:'ultimate',type:'ultimate',duration:balance.castTime,damage:0,hitAt:1};
} }
// While `a.bwRage` is set (the game keeps it in step with the rampage) the three specials are the rage versions.
function rmove(name) {
if(name==='skill1')return {name,type:name,damage:truck.damage,duration:.6,knockback:320,truck:true,rage:true,reach:900,hitAt:.5};
if(name==='skill2')return {name,type:name,damage:fire.ignite,duration:.62,knockback:120,fire:true,rage:true,reach:fire.first+fire.gap*fire.palms,hitAt:.55};
return {name:'ultimate',type:'ultimate',duration:balance.castTime,damage:0,hitAt:1,rage:true};
}
function start(a,name,index=1) { function start(a,name,index=1) {
if(a.action||a.state==='hurt'||a.state==='down'||a.state==='recover')return false; 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]>0)return false;
if(name!=='attack')a.cooldowns[name]=balance.cooldowns[name]; const rg=!!a.bwRage&&name!=='attack';
a.action={...move(name,index),t:0,fired:false,queued:0};a.state=a.action.name;a.stateTime=0;return true; if(name!=='attack')a.cooldowns[name]=rg?rageBalance.cooldowns[name]:balance.cooldowns[name];
a.action={...(rg?rmove(name):move(name,index)),t:0,fired:false,queued:0};a.state=a.action.name;a.stateTime=0;return true;
} }
function refund(a) {for(const name of Object.keys(balance.cooldowns))a.cooldowns[name]=Math.max(0,a.cooldowns[name]-balance.cooldowns[name]*balance.basicRefund);} function refund(a) {for(const name of Object.keys(balance.cooldowns))a.cooldowns[name]=Math.max(0,a.cooldowns[name]-balance.cooldowns[name]*balance.basicRefund);}
window.Browo={balance,names,combo,tree,dance,forest,rage,move,start,refund}; window.Browo={balance,names,combo,tree,dance,forest,rage,rageNames,rageBalance,truck,fire,tornado,move,rmove,start,refund};
})(); })();

137
game.js
View File

@ -674,14 +674,14 @@
// along the floor (ground projectiles with `tall`, so a jump does not clear them), 16 each, 48 total, fading after // 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. // 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) { function startSunroar(actor = hero) {
const group = { t: 0, owner: actor === hero ? 'player' : 'enemy', facing: actor.facing, roars: 0, phase: 'cutin' }; const group = { t: 0, owner: actor === hero ? 'player' : 'enemy', facing: actor.facing, roars: 0, phase: 'cutin', kind: actor.bwRage ? 'tornado' : 'forest' };
if (actor === hero) sunroar = group; else enemySunroar = group; if (actor === hero) sunroar = group; else enemySunroar = group;
cinematic = .9; announceTimer = 0; cinematic = .9; announceTimer = 0;
startUltimateVoice((actor === hero ? selectedCharacter : opponentCharacter) === 'browo' ? 'browo' : 'solan', actor); startUltimateVoice((actor === hero ? selectedCharacter : opponentCharacter) === 'browo' ? 'browo' : 'solan', actor);
} }
function updateSunroar(dt, s) { function updateSunroar(dt, s) {
if (!s) return; if (!s) return;
if ((s.owner === 'enemy' ? opponentCharacter : selectedCharacter) === 'browo') { updateForest(dt, s); return; } if ((s.owner === 'enemy' ? opponentCharacter : selectedCharacter) === 'browo') { (s.kind === 'tornado' ? updateTornado : updateForest)(dt, s); return; }
const U = SO.roar, a = s.owner === 'enemy' ? dummy : hero; const U = SO.roar, a = s.owner === 'enemy' ? dummy : hero;
s.t += dt; s.t += dt;
while (s.roars < U.roars.length && s.t >= U.roars[s.roars]) { while (s.roars < U.roars.length && s.t >= U.roars[s.roars]) {
@ -696,8 +696,26 @@
} }
// ---- BRO WO status layer: cinematic camera, stun holds, poison drain, rage ------------------------------------------- // ---- BRO WO status layer: cinematic camera, stun holds, poison drain, rage -------------------------------------------
// `bwx` is shared by the player's and the CPU's BRO WO; 'player' is the hero, 'enemy' is the dummy. // `bwx` is shared by the player's and the CPU's BRO WO; 'player' is the hero, 'enemy' is the dummy.
const bwx = { zoom: 1, zoomT: 1, dim: 0, dimT: 0, cx: 640, cy: 480, rage: { player: 0, enemy: 0 }, poison: { player: null, enemy: null }, lock: { player: 0, enemy: 0 }, dance: [], giant: null }; const bwx = { zoom: 1, zoomT: 1, dim: 0, dimT: 0, cx: 640, cy: 480, rage: { player: 0, enemy: 0 }, poison: { player: null, enemy: null }, lock: { player: 0, enemy: 0 }, dance: [], giant: null, fires: [], burn: { player: 0, enemy: 0, shown: { player: 0, enemy: 0 } }, tornado: null };
function bwReset() { 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; } function bwReset() {
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;
hero.bwRage = dummy.bwRage = false; hero.bwSaved = dummy.bwSaved = null;
if (BW && selectedCharacter === 'browo') Object.assign(cooldownMax, BW.balance.cooldowns);
}
// 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.
function bwEnterRage(key) {
const a = key === 'player' ? hero : dummy; bwx.rage[key] = BW.rage.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);
}
function bwExitRage(key) {
const a = key === 'player' ? hero : dummy; 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);
}
function bwAlive(target) { return target === dummy ? !dummy.ko && dummy.state !== 'down' && dummy.state !== 'recover' : hero.hp > 0; } 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. // Damage over time / scripted damage: no hurt animation, no knockback, no cooldown refunds; K.O. still applies.
function dotDamage(target, amount, color = '#9bd05a', numbers = true) { function dotDamage(target, amount, color = '#9bd05a', numbers = true) {
@ -717,6 +735,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 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) { function updateBrowo(dt) {
const D = BW ? BW.dance : null; if (!D) return; 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;
// Dance bursts: each burst that reaches the rival stuns them, deals damage and refreshes the poison. // 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--) { 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'; const d = bwx.dance[n], caster = d.owner === 'enemy' ? dummy : hero, target = d.owner === 'enemy' ? hero : dummy, tk = d.owner === 'enemy' ? 'player' : 'enemy';
@ -750,9 +769,25 @@
if (p.t <= 0 || !bwAlive(target)) bwx.poison[key] = null; if (p.t <= 0 || !bwAlive(target)) bwx.poison[key] = null;
} }
// Rage countdown. // Rage countdown.
for (const key of ['player', 'enemy']) if (bwx.rage[key] > 0) bwx.rage[key] = Math.max(0, bwx.rage[key] - dt); for (const key of ['player', 'enemy']) if (bwx.rage[key] > 0) { bwx.rage[key] = Math.max(0, bwx.rage[key] - dt); if (bwx.rage[key] === 0) bwExitRage(key); }
// Hutan Terbakar: each palm ignites once, then burns anyone standing on the ground inside its radius (one burn per rival).
const F2 = BW.fire, inside = { player: false, enemy: false };
for (let n = bwx.fires.length - 1; n >= 0; n--) {
const f = bwx.fires[n], target = f.owner === 'enemy' ? hero : dummy, tk = f.owner === 'enemy' ? 'player' : 'enemy';
f.t += dt; if (f.t < 0) continue;
const near = Math.abs(target.x - f.x) < F2.radius && target.y > CONFIG.groundY - 120 && bwAlive(target);
if (!f.lit) { f.lit = true; effects.push({ type: 'browo-fx', asset: 'browo-impact', x: f.x, y: CONFIG.groundY - 30, facing: 1, life: .4, maxLife: .4, size: 200 }); spawnParticles(f.x, CONFIG.groundY - 40, '#ff8a3a', 10, 220); sound('hit'); if (near) dotDamage(target, F2.ignite, '#ff9a4a'); }
if (near) inside[tk] = true;
if (f.t > F2.life) bwx.fires.splice(n, 1);
}
for (const key of ['player', 'enemy']) {
if (!inside[key]) continue;
const target = key === 'player' ? hero : dummy; bwx.burn[key] += F2.dps * dt;
if (bwx.burn[key] >= 1) { const m = Math.floor(bwx.burn[key]); bwx.burn[key] -= m; bwx.burn.shown[key] += m; dotDamage(target, m, '#ff9a4a', false); }
if (bwx.burn.shown[key] >= 3) { damageNumbers.push({ x: target.x + (random() - .5) * 24, y: target.y - 130, amount: bwx.burn.shown[key], life: .9, maxLife: .9, color: '#ff9a4a' }); bwx.burn.shown[key] = 0; }
}
// Camera eases toward the targets set by the ultimate; with no ultimate running they are neutral. // Camera eases toward the targets set by the ultimate; with no ultimate running they are neutral.
if (!sunroar && !enemySunroar) { bwx.zoomT = 1; bwx.dimT = 0; bwx.giant = null; } if (!sunroar && !enemySunroar) { bwx.zoomT = 1; bwx.dimT = 0; bwx.giant = null; bwx.tornado = null; }
const k = Math.min(1, dt * 7); bwx.zoom += (bwx.zoomT - bwx.zoom) * k; bwx.dim += (bwx.dimT - bwx.dim) * k; const k = Math.min(1, dt * 7); bwx.zoom += (bwx.zoomT - bwx.zoom) * k; bwx.dim += (bwx.dimT - bwx.dim) * k;
if (Math.abs(bwx.zoom - 1) < .002 && bwx.zoomT === 1) bwx.zoom = 1; if (bwx.dim < .004 && bwx.dimT === 0) bwx.dim = 0; if (Math.abs(bwx.zoom - 1) < .002 && bwx.zoomT === 1) bwx.zoom = 1; if (bwx.dim < .004 && bwx.dimT === 0) bwx.dim = 0;
} }
@ -798,11 +833,48 @@
} }
s.phase = s.t < U.cutinDuration ? 'cutin' : s.t < U.treesAt ? 'roaring' : 'fading'; s.phase = s.t < U.cutinDuration ? 'cutin' : s.t < U.treesAt ? 'roaring' : 'fading';
if (s.t >= U.duration) { if (s.t >= U.duration) {
bwx.rage[ok] = BW.rage.duration; bwx.giant = null; announce('MENGAMUK!', 1.3); bwEnterRage(ok); bwx.giant = null; announce('MENGAMUK!', 1.3);
effects.push({ type: 'browo-fx', asset: 'browo-sunburst', x: a.x, y: a.y - 95, facing: 1, life: .6, maxLife: .6, size: 380 }); effects.push({ type: 'browo-fx', asset: 'browo-sunburst', x: a.x, y: a.y - 95, facing: 1, life: .6, maxLife: .6, size: 380 });
if (s.owner === 'enemy') enemySunroar = null; else sunroar = null; if (s.owner === 'enemy') enemySunroar = null; else sunroar = null;
} }
} }
// BRO WO rage ultimate, Tornado Api Sawit: the camera dives on him, a fire tornado is born and hunts the rival; inside its
// radius the rival is held, dragged to the core and burned; it ends in an explosion (and the rampage ends with it).
function updateTornado(dt, s) {
const U = BW.tornado, a = s.owner === 'enemy' ? dummy : hero, target = s.owner === 'enemy' ? hero : dummy, tk = s.owner === 'enemy' ? 'player' : 'enemy', ok = s.owner === 'enemy' ? 'enemy' : 'player';
s.t += dt;
if (s.t < U.spawnAt) { bwx.zoomT = 1.65; bwx.dimT = .85; bwx.cx = a.x; bwx.cy = a.y - 90; if (s.t > .12) trauma = Math.min(.4, trauma + dt * .6); }
else if (s.t < U.spawnAt + U.life) { bwx.zoomT = 1.18; bwx.dimT = .6; bwx.cx = s.tw ? s.tw.x : a.x; bwx.cy = a.y - 120; }
else { bwx.zoomT = 1; bwx.dimT = 0; }
if (!s.tw && s.t >= U.spawnAt) {
s.tw = bwx.tornado = { x: clamp(a.x + s.facing * 70, 60, W - 60), age: 0, tick: 0, blasted: false };
effects.push({ type: 'browo-fx', asset: 'browo-sunburst', x: s.tw.x, y: CONFIG.groundY - 100, facing: 1, life: .6, maxLife: .6, size: 460 });
sound('heavy'); trauma = Math.min(1, trauma + .45);
}
const tw = s.tw;
if (tw) {
tw.age += dt;
if (tw.age < U.life) {
if (bwAlive(target)) tw.x += clamp(target.x - tw.x, -U.chase * dt, U.chase * dt);
tw.x = clamp(tw.x, 60, W - 60);
if (bwAlive(target) && Math.abs(target.x - tw.x) < U.radius) {
bwLock(tk, .25); target.x += (tw.x - target.x) * Math.min(1, dt * 3.2); target.vx = 0;
tw.tick -= dt; if (tw.tick <= 0) { tw.tick += U.tick; dotDamage(target, U.tickDamage, '#ff9a4a'); sound('hit'); }
}
if (Math.sin(realTime * 33) > .5) spawnParticles(tw.x + (random() - .5) * 90, CONFIG.groundY - random() * 260, '#ff7a30', 1, 160);
trauma = Math.min(.5, trauma + dt * .4);
} else if (!tw.blasted) {
tw.blasted = true;
effects.push({ type: 'browo-fx', asset: 'browo-impact', x: tw.x, y: CONFIG.groundY - 40, facing: 1, life: .6, maxLife: .6, size: 560 });
effects.push({ type: 'browo-fx', asset: 'browo-sunburst', x: tw.x, y: CONFIG.groundY - 150, facing: 1, life: .55, maxLife: .55, size: 620 });
for (let q = 0; q < 3; q++) spawnParticles(tw.x, CONFIG.groundY - 60, q === 1 ? '#fff1c8' : '#ff7a30', 24, 520);
trauma = 1; hitstop = Math.max(hitstop, .14); sound('heavy');
if (bwAlive(target) && Math.abs(target.x - tw.x) < U.radius * 1.4) { if (target === dummy) { if (!hitDummy(U.blast, 400, '#ffb060', tw.x, CONFIG.groundY - 100, true, true, 2)) dotDamage(dummy, U.blast, '#ffb060'); } else if (!window.__game.receiveHit(U.blast, { shake: 2 })) dotDamage(hero, U.blast, '#ffb060'); }
}
}
s.phase = s.t < U.cutinDuration ? 'cutin' : 'roaring';
if (s.t >= U.duration) { bwx.tornado = null; bwExitRage(ok); if (s.owner === 'enemy') enemySunroar = null; else sunroar = null; }
}
// NIB Special Delivery: three paper-plane parcels leave the satchel 0.38 s apart and home in on the rival (homing // NIB Special Delivery: three paper-plane parcels leave the satchel 0.38 s apart and home in on the rival (homing
// projectiles with a limited turn rate, 16 each, 48 total). NIB is free after the 0.5 s cast; a late sidestep or a // projectiles with a limited turn rate, 16 each, 48 total). NIB is free after the 0.5 s cast; a late sidestep or a
// jump makes a plane overshoot. // jump makes a plane overshoot.
@ -983,11 +1055,11 @@
} }
if (p.spent) { if (p.life <= 0) projectiles.splice(i, 1); continue; } if (p.spent) { if (p.life <= 0) projectiles.splice(i, 1); continue; }
if(p.owner==='enemy') { if(p.owner==='enemy') {
if(Math.min(oldX,p.x)<=hero.x+28&&Math.max(oldX,p.x)>=hero.x-28&&Math.abs(p.y-(hero.y-85))<(p.tall||75)&&window.__game.receiveHit(p.damage,{projectile:true,freeze:p.freeze}))p.boomerang?(p.spent=true,p.returning=true):p.orbit?(p.spent=true):p.life=0; if(Math.min(oldX,p.x)<=hero.x+28&&Math.max(oldX,p.x)>=hero.x-28&&Math.abs(p.y-(hero.y-85))<(p.tall||75)&&window.__game.receiveHit(p.damage,{projectile:true,freeze:p.freeze}))p.boomerang?(p.spent=true,p.returning=true):(p.orbit||p.pass)?(p.spent=true):p.life=0;
if(p.life<=0||p.x<-100||p.x>W+100)projectiles.splice(i,1);continue; if(p.life<=0||p.x<-100||p.x>W+100)projectiles.splice(i,1);continue;
} }
// Swept x interval keeps a high-speed bolt from tunneling through a target. // Swept x interval keeps a high-speed bolt from tunneling through a target.
if (Math.min(oldX, p.x) <= dummy.x + 30 && Math.max(oldX, p.x) >= dummy.x - 30 && Math.abs(p.y - (dummy.y - (F?95:68))) < (p.tall || (F?90:66)) && dummy.state !== 'down' && dummy.state !== 'recover') { if (hitDummy(p.damage||BALANCE.skill1.damage, p.knockback ?? BALANCE.skill1.knockback, p.color || '#94ffde', oldX, p.y, false, false)) { if (p.boomerang) { p.spent = true; p.returning = true; } else if (p.orbit) p.spent = true; else p.life = 0; } } if (Math.min(oldX, p.x) <= dummy.x + 30 && Math.max(oldX, p.x) >= dummy.x - 30 && Math.abs(p.y - (dummy.y - (F?95:68))) < (p.tall || (F?90:66)) && dummy.state !== 'down' && dummy.state !== 'recover') { if (hitDummy(p.damage||BALANCE.skill1.damage, p.knockback ?? BALANCE.skill1.knockback, p.color || '#94ffde', oldX, p.y, !!p.knockdown, false)) { if (p.boomerang) { p.spent = true; p.returning = true; } else if (p.orbit || p.pass) p.spent = true; else p.life = 0; } }
if (p.life <= 0 || p.x < -100 || p.x > W + 100) projectiles.splice(i, 1); if (p.life <= 0 || p.x < -100 || p.x > W + 100) projectiles.splice(i, 1);
} }
} }
@ -1212,6 +1284,17 @@
const enemy=actor===dummy,target=enemy?hero:dummy,em=window.BROWO_METRICS?.emitters?.[move.name]; const enemy=actor===dummy,target=enemy?hero:dummy,em=window.BROWO_METRICS?.emitters?.[move.name];
const hy=clamp(actor.y+(em?.y??-95),actor.y-190,actor.y-40); const hy=clamp(actor.y+(em?.y??-95),actor.y-190,actor.y-40);
const raging=bwx.rage[enemy?'enemy':'player']>0,mul=raging?BW.rage.damageMul:1,dmg=Math.round(move.damage*mul); const raging=bwx.rage[enemy?'enemy':'player']>0,mul=raging?BW.rage.damageMul:1,dmg=Math.round(move.damage*mul);
if(move.truck){
// Truk Katering: a delivery truck crosses the whole stage from behind BRO WO and hits once (it keeps going after the hit).
const dir=actor.facing;
projectiles.push({x:dir>0?-92:W+92,y:CONFIG.groundY-62,vx:dir*BW.truck.speed,life:(W+210)/BW.truck.speed,facing:dir,owner:enemy?'enemy':'player',damage:Math.round(move.damage*mul),knockback:move.knockback,asset:'browo-truck',size:BW.truck.size,color:'#ffb060',tall:BW.truck.tall,pass:true,knockdown:true});
trauma=Math.min(1,trauma+.35);sound('heavy');return;
}
if(move.fire){
// Hutan Terbakar: palms catch fire in a row ahead of BRO WO (updateBrowo does the burning).
for(let i=0;i<BW.fire.palms;i++)bwx.fires.push({x:clamp(actor.x+actor.facing*(BW.fire.first+i*BW.fire.gap),60,W-60),t:-i*BW.fire.delay,owner:enemy?'enemy':'player',lit:false});
trauma=Math.min(1,trauma+.3);sound('cast');return;
}
if(move.dance){ if(move.dance){
// Joget Gemoy: the bursts are driven by updateBrowo (stun + poison); nothing lands at the cast itself. // Joget Gemoy: the bursts are driven by updateBrowo (stun + poison); nothing lands at the cast itself.
bwx.dance.push({owner:enemy?'enemy':'player',t:0,pulses:0});sound('cast');return; bwx.dance.push({owner:enemy?'enemy':'player',t:0,pulses:0});sound('cast');return;
@ -1636,6 +1719,14 @@
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(a.x, a.y - 75, 95, 0, 7); ctx.fill(); bubbles(a, '#9be84f'); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(a.x, a.y - 75, 95, 0, 7); ctx.fill(); bubbles(a, '#9be84f');
} }
} }
const tw = bwx.tornado, ti = images['fx-browo-tornado'];
if (tw && ti) {
const U = BW.tornado, grow = Math.min(1, tw.age / .5), fade = tw.age > U.life ? Math.max(0, 1 - (tw.age - U.life) / .45) : 1;
for (const [sc, off, al] of [[1, 0, .88], [.78, 3, .5]]) {
const h = 580 * grow * sc, w = ti.width / ti.height * h * (1 + Math.sin(realTime * 13 + off) * .05);
ctx.save(); ctx.globalAlpha = al * fade; ctx.drawImage(ti, Math.round(tw.x - w / 2 + Math.sin(realTime * 21 + off) * 4), CONFIG.groundY + 14 - h, w, h); ctx.restore();
}
}
const im = images['fx-browo-sawit']; const im = images['fx-browo-sawit'];
for (const key of ['player', 'enemy']) { for (const key of ['player', 'enemy']) {
if (bwx.rage[key] <= 0) continue; if (bwx.rage[key] <= 0) continue;
@ -1645,6 +1736,15 @@
if (Math.sin(realTime * 40) > .6) spawnParticles(a.x + (random() - .5) * 50, a.y - 40 - random() * 80, '#ff8a3a', 1, 80); if (Math.sin(realTime * 40) > .6) spawnParticles(a.x + (random() - .5) * 50, a.y - 40 - random() * 80, '#ff8a3a', 1, 80);
} }
} }
// Burning palms of Hutan Terbakar, behind the fighters.
function drawBrowoFires() {
const im = images['fx-browo-burntree']; if (!im || !bwx.fires.length) return;
for (const f of bwx.fires) {
if (f.t < 0) continue;
const life = BW.fire.life, grow = Math.min(1, f.t / .25), fade = f.t > life - .6 ? Math.max(0, (life - f.t) / .6) : 1, h = 250 * (.35 + .65 * grow) * (1 + Math.sin(realTime * 14 + f.x) * .03), w = im.width / im.height * h;
ctx.save(); ctx.globalAlpha = .95 * fade; ctx.drawImage(im, Math.round(f.x - w / 2), CONFIG.groundY + 6 - h, w, h); ctx.restore();
}
}
// Small palms erupt behind both fighters so BRO WO and the rival stay readable. // Small palms erupt behind both fighters so BRO WO and the rival stay readable.
function drawBrowoTrees() { function drawBrowoTrees() {
const im = images['fx-browo-sawit']; if (!im) return; const im = images['fx-browo-sawit']; if (!im) return;
@ -1782,7 +1882,7 @@
ctx.save(); ctx.translate(offsetX + shakeX * scale, offsetY + shakeY * scale); ctx.scale(scale, scale); ctx.save(); ctx.translate(offsetX + shakeX * scale, offsetY + shakeY * scale); ctx.scale(scale, scale);
// Light motes provide life without moving the fixed stage or camera. // Light motes provide life without moving the fixed stage or camera.
for (let i = 0; i < 14; i++) { const x = (i * 117 + realTime * (6 + i % 3)) % W, y = 210 + (i * 47) % 330 + Math.sin(realTime + i) * 9; ctx.fillStyle = i % 2 ? '#fff8cb88' : '#f0ffe94d'; ctx.fillRect(Math.round(x), Math.round(y), 2, 2); } for (let i = 0; i < 14; i++) { const x = (i * 117 + realTime * (6 + i % 3)) % W, y = 210 + (i * 47) % 330 + Math.sin(realTime + i) * 9; ctx.fillStyle = i % 2 ? '#fff8cb88' : '#f0ffe94d'; ctx.fillRect(Math.round(x), Math.round(y), 2, 2); }
drawBrowoTrees(); drawBrowoGiant(false); drawBrowoTrees(); drawBrowoFires(); drawBrowoGiant(false);
if (hero.x < dummy.x) { drawHero(); drawDummy(); } else { drawDummy(); drawHero(); } if (hero.x < dummy.x) { drawHero(); drawDummy(); } else { drawDummy(); drawHero(); }
drawBrowoGiant(true); drawBrowoGiant(true);
drawSquadron(); drawSquadron(enemySquad); drawParade(parade); drawParade(enemyParade); drawMurmuration(flock); drawMurmuration(enemyFlock); drawSerpent(serpent); drawSerpent(enemySerpent); drawEffects(); drawBrowoExtras(); drawHitboxes(); ctx.restore(); if (camera) ctx.restore(); updateHud(); drawSquadron(); drawSquadron(enemySquad); drawParade(parade); drawParade(enemyParade); drawMurmuration(flock); drawMurmuration(enemyFlock); drawSerpent(serpent); drawSerpent(enemySerpent); drawEffects(); drawBrowoExtras(); drawHitboxes(); ctx.restore(); if (camera) ctx.restore(); updateHud();
@ -1804,9 +1904,10 @@
} }
function updateFighterIdentity() { function updateFighterIdentity() {
if(!F)return; if(!F)return;
const identity=selectedCharacter+':'+hero.form+':'+opponentCharacter+':'+dummy.form; const identity=selectedCharacter+(selectedCharacter==='browo'&&bwx.rage.player>0?'*rage':'')+':'+hero.form+':'+opponentCharacter+':'+dummy.form;
if(identity!==hudIdentity) { if(identity!==hudIdentity) {
hudIdentity=identity;const me=fighterInfo(selectedCharacter,hero.form),rival=fighterInfo(opponentCharacter,dummy.form); 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');}
$('#player-name').textContent=me.name;$('#player-class').textContent=me.cls;$('#deck-character').textContent=me.deck; $('#player-name').textContent=me.name;$('#player-class').textContent=me.cls;$('#deck-character').textContent=me.deck;
$('#player-title').textContent=me.title; $('#player-title').textContent=me.title;
$('#player-portrait').src=me.portrait;$('#player-portrait').alt='Portrait '+me.name; $('#player-portrait').src=me.portrait;$('#player-portrait').alt='Portrait '+me.name;
@ -1865,11 +1966,11 @@
// One banner at a time: FENR transformation, then ARCO, then a MIRA/CORA/NAJA/HALDOR summon still inside its cut-in window. // 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<SQUAD.cutinDuration?g:null; const cutin=$('#ultimate-cutin'),live=g=>g&&g.t<SQUAD.cutinDuration?g:null;
const arcoGroup=live(enemySquad)||live(squad),summon=live(enemyParade)||live(parade)||live(enemyFlock)||live(flock)||live(enemySerpent)||live(serpent)||live(enemyQuake)||live(quake)||live(enemyFinale)||live(finale)||live(enemySkyfall)||live(skyfall)||live(enemyOrrery)||live(orrery)||live(enemySunroar)||live(sunroar)||live(enemyDelivery)||live(delivery)||live(enemyTortoise)||live(tortoise); const arcoGroup=live(enemySquad)||live(squad),summon=live(enemyParade)||live(parade)||live(enemyFlock)||live(flock)||live(enemySerpent)||live(serpent)||live(enemyQuake)||live(quake)||live(enemyFinale)||live(finale)||live(enemySkyfall)||live(skyfall)||live(enemyOrrery)||live(orrery)||live(enemySunroar)||live(sunroar)||live(enemyDelivery)||live(delivery)||live(enemyTortoise)||live(tortoise);
const cutinKey=fenrCutin?'fenr':arcoGroup?'arco':summon?(summon===parade||summon===enemyParade?'mira':summon===serpent||summon===enemySerpent?'naja':summon===quake||summon===enemyQuake?'haldor':summon===finale||summon===enemyFinale?'zanni':summon===skyfall||summon===enemySkyfall?'isolde':summon===orrery||summon===enemyOrrery?'rhea':summon===sunroar||summon===enemySunroar?((summon===enemySunroar?opponentCharacter:selectedCharacter)==='browo'?'browo':'solan'):summon===delivery||summon===enemyDelivery?'nib':summon===tortoise||summon===enemyTortoise?'edda':'cora'):'arco'; const cutinKey=fenrCutin?'fenr':arcoGroup?'arco':summon?(summon===parade||summon===enemyParade?'mira':summon===serpent||summon===enemySerpent?'naja':summon===quake||summon===enemyQuake?'haldor':summon===finale||summon===enemyFinale?'zanni':summon===skyfall||summon===enemySkyfall?'isolde':summon===orrery||summon===enemyOrrery?'rhea':summon===sunroar||summon===enemySunroar?((summon===enemySunroar?opponentCharacter:selectedCharacter)==='browo'?(summon.kind==='tornado'?'browofire':'browo'):'solan'):summon===delivery||summon===enemyDelivery?'nib':summon===tortoise||summon===enemyTortoise?'edda':'cora'):'arco';
const ct=fenrCutin?.t ?? (arcoGroup||summon)?.t ?? 100; const ct=fenrCutin?.t ?? (arcoGroup||summon)?.t ?? 100;
cutin.classList.toggle('fenr-cutin',cutinKey==='fenr');cutin.classList.toggle('mira-cutin',cutinKey==='mira');cutin.classList.toggle('cora-cutin',cutinKey==='cora');cutin.classList.toggle('naja-cutin',cutinKey==='naja');cutin.classList.toggle('haldor-cutin',cutinKey==='haldor');cutin.classList.toggle('zanni-cutin',cutinKey==='zanni');cutin.classList.toggle('isolde-cutin',cutinKey==='isolde');cutin.classList.toggle('rhea-cutin',cutinKey==='rhea');cutin.classList.toggle('solan-cutin',cutinKey==='solan');cutin.classList.toggle('nib-cutin',cutinKey==='nib');cutin.classList.toggle('edda-cutin',cutinKey==='edda');cutin.classList.toggle('browo-cutin',cutinKey==='browo'); cutin.classList.toggle('fenr-cutin',cutinKey==='fenr');cutin.classList.toggle('mira-cutin',cutinKey==='mira');cutin.classList.toggle('cora-cutin',cutinKey==='cora');cutin.classList.toggle('naja-cutin',cutinKey==='naja');cutin.classList.toggle('haldor-cutin',cutinKey==='haldor');cutin.classList.toggle('zanni-cutin',cutinKey==='zanni');cutin.classList.toggle('isolde-cutin',cutinKey==='isolde');cutin.classList.toggle('rhea-cutin',cutinKey==='rhea');cutin.classList.toggle('solan-cutin',cutinKey==='solan');cutin.classList.toggle('nib-cutin',cutinKey==='nib');cutin.classList.toggle('edda-cutin',cutinKey==='edda');cutin.classList.toggle('browo-cutin',cutinKey==='browo'||cutinKey==='browofire');
const cutinArt=cutin.querySelector('.cutin-art'),cutinOwner=cutinKey==='fenr'?fenrCutin.owner:cutinKey==='mira'||cutinKey==='cora'||cutinKey==='naja'||cutinKey==='haldor'||cutinKey==='zanni'||cutinKey==='isolde'||cutinKey==='rhea'||cutinKey==='solan'||cutinKey==='browo'||cutinKey==='nib'||cutinKey==='edda'?(summon.owner==='enemy'?'RIVAL':'PLAYER'):''; const cutinArt=cutin.querySelector('.cutin-art'),cutinOwner=cutinKey==='fenr'?fenrCutin.owner:cutinKey==='mira'||cutinKey==='cora'||cutinKey==='naja'||cutinKey==='haldor'||cutinKey==='zanni'||cutinKey==='isolde'||cutinKey==='rhea'||cutinKey==='solan'||cutinKey==='browo'||cutinKey==='browofire'||cutinKey==='nib'||cutinKey==='edda'?(summon.owner==='enemy'?'RIVAL':'PLAYER'):'';
if(cutin.dataset.identity!==cutinKey+cutinOwner){cutin.dataset.identity=cutinKey+cutinOwner;cutinArt.src=CUTIN_ART[cutinKey];if(cutinArt.complete)cutinArt.style.visibility='';else{cutinArt.style.visibility='hidden';cutinArt.onload=()=>{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,nib:'NIB / '+cutinOwner,edda:'EDDA / '+cutinOwner,arco:'ARCO / AETHER COMMAND'}[cutinKey];cutin.querySelector('strong').innerHTML={fenr:'FERAL<br><em>AWAKENING</em>',mira:'ROCKET<br><em>PARADE</em>',cora:'NIGHT<br><em>MURMURATION</em>',naja:'DUNE<br><em>SERPENT</em>',haldor:'FORGE<br><em>QUAKE</em>',zanni:'GRAND<br><em>FINALE</em>',isolde:'SKYFALL<br><em>LANCES</em>',rhea:'GRAND<br><em>ORRERY</em>',solan:'SUNMANE<br><em>ROAR</em>',browo:'HUTAN<br><em>SAWIT</em>',nib:'SPECIAL<br><em>DELIVERY</em>',edda:'ELDER<br><em>TORTOISE</em>',arco:'HELIOS<br><em>SQUADRON</em>'}[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',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',nib:'NIB ultimate: Special Delivery',edda:'EDDA ultimate: Elder Tortoise',arco:'ARCO ultimate: Helios Squadron'}[cutinKey]);} if(cutin.dataset.identity!==cutinKey+cutinOwner){cutin.dataset.identity=cutinKey+cutinOwner;cutinArt.src=CUTIN_ART[cutinKey];if(cutinArt.complete)cutinArt.style.visibility='';else{cutinArt.style.visibility='hidden';cutinArt.onload=()=>{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<br><em>AWAKENING</em>',mira:'ROCKET<br><em>PARADE</em>',cora:'NIGHT<br><em>MURMURATION</em>',naja:'DUNE<br><em>SERPENT</em>',haldor:'FORGE<br><em>QUAKE</em>',zanni:'GRAND<br><em>FINALE</em>',isolde:'SKYFALL<br><em>LANCES</em>',rhea:'GRAND<br><em>ORRERY</em>',solan:'SUNMANE<br><em>ROAR</em>',browo:'HUTAN<br><em>SAWIT</em>',browofire:'TORNADO<br><em>API SAWIT</em>',nib:'SPECIAL<br><em>DELIVERY</em>',edda:'ELDER<br><em>TORTOISE</em>',arco:'HELIOS<br><em>SQUADRON</em>'}[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]);}
const visible = ct < SQUAD.cutinDuration; const visible = ct < SQUAD.cutinDuration;
cutin.classList.toggle('visible', visible); cutin.setAttribute('aria-hidden', String(!visible)); cutin.classList.toggle('visible', visible); cutin.setAttribute('aria-hidden', String(!visible));
const slide = !visible ? -110 : ct < .16 ? -110 * Math.pow(1 - ct / .16,3) : ct > .57 ? 110 * Math.pow((ct-.57)/.21,2) : 0; const slide = !visible ? -110 : ct < .16 ? -110 * Math.pow(1 - ct / .16,3) : ct > .57 ? 110 * Math.pow((ct-.57)/.21,2) : 0;
@ -1879,9 +1980,9 @@
} }
// Cut-in art per fighter. Only the two fighters of the current fight are warmed (at start/select), so a banner never waits // 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. // 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',nib:'assets/nib/ui/cutin.webp',edda:'assets/edda/ui/cutin.webp',arco:'assets/ui/ultimate-cutin.webp'}; 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'};
let cutinWarm=[]; let cutinWarm=[];
function warmCutins(){cutinWarm=[...new Set([selectedCharacter,opponentCharacter])].map(id=>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]]).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 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')+' <span>↗</span>'; $('#settings-panel').showModal(); } function openSettings() { settingsOpen = true; clearInput(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); $('#close-settings').innerHTML=(menuOpen?'Kembali ke menu':'Kembali ke arena')+' <span>↗</span>'; $('#settings-panel').showModal(); }
function closeSettings() { settingsOpen = false; $('#settings-panel').close(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); canvas.focus({ preventScroll: true }); } function closeSettings() { settingsOpen = false; $('#settings-panel').close(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); canvas.focus({ preventScroll: true }); }
@ -1922,7 +2023,7 @@
if(window.FRONTEND_ENABLED)setMenuOpen(true); if(window.FRONTEND_ENABLED)setMenuOpen(true);
resize(); requestAnimationFrame(tick); resize(); requestAnimationFrame(tick);
// Wait for the first-visit download (preload.js) so these requests are served from the cache, not fetched twice. // 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'].map(name=>loadImage('fx-browo-'+name,'assets/browo/ui/fx-'+name+'.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))]:[]), ...(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(); 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; 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); } if (ready) { $('#load-state').classList.add('hidden'); announce('TRAINING READY', 1.65); }