From 41421b7c2aa0409e8fdd70d71795682089f9ced7 Mon Sep 17 00:00:00 2001 From: rizkyaz0 Date: Thu, 1 Oct 2026 12:05:36 +0700 Subject: [PATCH] Bro Wo: ultimate sinematik (zoom, latar gelap, 10 pohon + pohon raksasa), mode mengamuk 10 detik, joget gemoy menyetun dan meracuni Ultimate menukik kamera ke Bro Wo dengan latar menggelap, menahan lawan, menumbuhkan 10 pohon beruntun lalu mengangkat dan membanting pohon raksasa. Setelahnya Bro Wo mengamuk 10 detik membawa pohon sawit sebagai senjata (damage, jangkauan, dan kecepatan naik). Joget Gemoy kini goyang berulang, menyetun lawan, dan menguras darah perlahan lewat racun. --- browo.js | 25 +++++-- game.js | 222 ++++++++++++++++++++++++++++++++++++++++++++++--------- menu.js | 2 +- 3 files changed, 205 insertions(+), 44 deletions(-) diff --git a/browo.js b/browo.js index 6f3cb4d..bd9c315 100644 --- a/browo.js +++ b/browo.js @@ -1,7 +1,7 @@ /* Shared BRO WO moves (stocky safari ranger, fists and oil-palm trees): player and AI use the same rules. */ (() => { 'use strict'; - const balance = { cooldowns:{skill1:4,skill2:7,ultimate:18}, castTime:.5, forestDamage:12, basicRefund:.05 }; + const balance = { cooldowns:{skill1:4,skill2:9,ultimate:24}, castTime:.5, basicRefund:.05 }; const names = ['TINJU SAFARI','POHON SAWIT','JOGET GEMOY','HUTAN SAWIT']; // Safari chain: a quick jab, a wide hook, then a heavy shoulder smash with big knockback. Reach sits inside the measured // fist tip (82/82/97 px in BROWO_METRICS.emitters). @@ -13,15 +13,24 @@ // Pohon Sawit: BRO WO slams a palm on the floor and an oil-palm erupts `ahead` px in front; everything inside `radius` // px of that spot on the floor is hit. He does not need to touch the rival; a jump above 150 px clears it. const tree = { ahead:190, radius:90 }; - // Joget Gemoy: a cheerful shoulder-swaying dance; leaves burst around him and everything within `radius` is hit. - const dance = { radius:125 }; - // Hutan Sawit: four palms erupt one after another in front of him (spacing `trees`, every `gap` s from `at`); a rival - // standing inside `radius` of any one takes 12 (48 total if all four land). He is free after the 0.5 s cast. - const forest = { cutinDuration:.78, trees:[150,280,410,540], at:.8, gap:.34, radius:85, duration:2.6 }; + // Joget Gemoy: a long cheerful dance. `pulses` leaf bursts, `gap` s apart from `first`; every burst that reaches a rival + // inside `radius` stuns them (for gap + stunExtra s), deals `pulseDamage`, and refreshes a poison that drains `poisonDps` + // HP per second for `poisonTime` s without interrupting them. + const dance = { radius:150, pulses:5, first:.12, gap:.3, pulseDamage:3, stunExtra:.5, poisonDps:2.5, poisonTime:6, duration:1.7, loops:3 }; + // Hutan Sawit (ultimate), seconds from the cast. The camera dives onto BRO WO under a dark screen (zoom to `zoomIn`), + // holds, then `treeCount` palms erupt in a rapid run toward the rival (`treeDamage` each) while the rival is held in place. + // At `giantAt` one giant palm rises (`giantRise`), leans back (`giantHold`) and is brought down on the rival (`slam`), + // dealing `giantDamage` and a knockdown. Afterwards BRO WO rampages (see `rage`). + const forest = { cutinDuration:.78, zoomIn:.45, lockAt:.3, treesAt:1.45, treeCount:10, treeGap:.11, treeDamage:3, + giantAt:2.85, giantRise:.55, giantHold:.3, slam:.16, giantDamage:40, tail:.55 }; + forest.release = forest.giantAt + forest.giantRise + forest.giantHold + forest.slam; + 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. + const rage = { duration:10, damageMul:1.4, speedMul:1.25, reachAdd:25 }; function move(name,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==='skill2')return {name,type:name,damage:14,duration:.62,knockback:260,dance:true,reach:dance.radius,hitAt:.5}; + 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}; } function start(a,name,index=1) { @@ -31,5 +40,5 @@ a.action={...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);} - window.Browo={balance,names,combo,tree,dance,forest,move,start,refund}; + window.Browo={balance,names,combo,tree,dance,forest,rage,move,start,refund}; })(); diff --git a/game.js b/game.js index 1997ed4..2f8d0e1 100644 --- a/game.js +++ b/game.js @@ -172,7 +172,7 @@ for(const [i,n] of notes.entries()){const at=audio.currentTime+i*gap,o=audio.createOscillator(),g=audio.createGain();o.type=kind==='locked'||kind==='pick'?'triangle':'sine';o.frequency.setValueAtTime(n,at);g.gain.setValueAtTime(.001,at);g.gain.exponentialRampToValueAtTime(peak,at+.005);g.gain.exponentialRampToValueAtTime(.001,at+tail);o.connect(g);g.connect(master);o.start(at);o.stop(at+tail+.015);audioSources++;} } function setMenuOpen(value) { - menuOpen=!!value;clearInput();if(menuOpen){systemAnnouncer?.clear();stopUltimateVoice();squad=enemySquad=parade=enemyParade=flock=enemyFlock=serpent=enemySerpent=quake=enemyQuake=finale=enemyFinale=skyfall=enemySkyfall=orrery=enemyOrrery=sunroar=enemySunroar=delivery=enemyDelivery=tortoise=enemyTortoise=null;fenrCutin=null;projectiles.length=effects.length=0;setPaused(false);} + menuOpen=!!value;clearInput();if(menuOpen){systemAnnouncer?.clear();stopUltimateVoice();bwReset();squad=enemySquad=parade=enemyParade=flock=enemyFlock=serpent=enemySerpent=quake=enemyQuake=finale=enemyFinale=skyfall=enemySkyfall=orrery=enemyOrrery=sunroar=enemySunroar=delivery=enemyDelivery=tortoise=enemyTortoise=null;fenrCutin=null;projectiles.length=effects.length=0;setPaused(false);} document.body.classList.toggle('in-menu',menuOpen);$('#front-menu').hidden=!menuOpen; window.FrontEnd?.syncBackground(); } @@ -198,7 +198,7 @@ for(const event of Rules.tick(match,dt,hero.hp,currentDummyHP())) { if(event.type==='cue')emitRoundCue(event.cue,event.round); if(event.type==='round-end') { - clearInput();stopUltimateVoice();squad=enemySquad=parade=enemyParade=flock=enemyFlock=serpent=enemySerpent=quake=enemyQuake=finale=enemyFinale=skyfall=enemySkyfall=orrery=enemyOrrery=sunroar=enemySunroar=delivery=enemyDelivery=tortoise=enemyTortoise=null;fenrCutin=null;hero.action=dummy.action=null;projectiles.length=effects.length=0;hitstop=cinematic=trauma=0; + clearInput();stopUltimateVoice();bwReset();squad=enemySquad=parade=enemyParade=flock=enemyFlock=serpent=enemySerpent=quake=enemyQuake=finale=enemyFinale=skyfall=enemySkyfall=orrery=enemyOrrery=sunroar=enemySunroar=delivery=enemyDelivery=tortoise=enemyTortoise=null;fenrCutin=null;hero.action=dummy.action=null;projectiles.length=effects.length=0;hitstop=cinematic=trauma=0; if(hero.hp<=0)state('down');else state('idle');if(currentDummyHP()>0){dummy.state='idle';dummy.stateTime=0;} match.endText=event.doubleKO?'DOUBLE K.O.':event.timeout?'TIME UP':'K.O.';announceTimer=0;sound('heavy'); const call=event.doubleKO?'double_ko':event.timeout?'time_up':'ko'; @@ -311,7 +311,7 @@ if(F) {F.reset(hero);F.reset(dummy);Object.assign(dummy,{facing:-1,action:null,cooldowns:{skill1:0,skill2:0,ultimate:0},aiThink:.4,aiTime:0,aiChain:0,walkPhase:0,jumps:0});cpu={seen:new WeakMap(),decided:new WeakSet(),read:null,heroActionAt:0,heroAction:null,airborne:false,airAt:0,antiAirRead:false,plan:null,doubleAt:0};syncPlayerForm();} fenrCutin=null;hudIdentity=''; particles.length = effects.length = projectiles.length = damageNumbers.length = 0; totalDamage = comboHits = bestCombo = comboDamage = comboTimer = hitstop = trauma = cinematic = 0; clearInput(); setPaused(false); announce('TRAINING READY', 1.25); - squad = null; enemySquad = null; parade = null; enemyParade = null; flock = null; enemyFlock = null; serpent = null; enemySerpent = null; quake = null; enemyQuake = null; finale = null; enemyFinale = null; skyfall = null; enemySkyfall = null; orrery = null; enemyOrrery = null; sunroar = null; enemySunroar = null; delivery = null; enemyDelivery = null; tortoise = null; enemyTortoise = null; + bwReset(); squad = null; enemySquad = null; parade = null; enemyParade = null; flock = null; enemyFlock = null; serpent = null; enemySerpent = null; quake = null; enemyQuake = null; finale = null; enemyFinale = null; skyfall = null; enemySkyfall = null; orrery = null; enemyOrrery = null; sunroar = null; enemySunroar = null; delivery = null; enemyDelivery = null; tortoise = null; enemyTortoise = null; for(const name of Object.keys(rechargePulse)) rechargePulse[name]=0; } function reset() { @@ -694,23 +694,114 @@ s.phase = s.t < U.cutinDuration ? 'cutin' : s.roars < U.roars.length ? 'roaring' : 'fading'; if (s.t >= U.duration && !projectiles.some(p => p.asset === 'solan-roar' && p.owner === s.owner)) { if (s.owner === 'enemy') enemySunroar = null; else sunroar = null; } } - // BRO WO Hutan Sawit: four oil-palms erupt one after another in front of him (BW.forest.trees). The group object is the - // shared `sunroar` slot, so the pause/reset/cut-in plumbing is the same as SOLAN's; `roars` counts the trees raised. + // ---- 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. + 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 }; + 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 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. + function dotDamage(target, amount, color = '#9bd05a', numbers = true) { + if (amount <= 0 || menuOpen || (match && match.phase !== 'fight') || !bwAlive(target)) return false; + if (target === dummy) { + dummy.damage += amount; totalDamage += amount; comboHits++; bestCombo = Math.max(bestCombo, comboHits); comboDamage += amount; comboTimer = 2.3; + if (numbers) damageNumbers.push({ x: dummy.x + (random() - .5) * 30, y: dummy.y - 120, amount, life: .9, maxLife: .9, color }); + if (dummy.damage >= BALANCE.dummyMax) { dummy.ko = true; dummy.state = 'down'; dummy.vy = -330; dummy.vx = 0; dummy.fallDirection = hero.x <= dummy.x ? 1 : -1; announce('K.O.', .9); } + return true; + } + hero.hp = Math.max(0, hero.hp - amount); + if (numbers) damageNumbers.push({ x: hero.x + (random() - .5) * 30, y: hero.y - 120, amount, life: .9, maxLife: .9, color }); + if (!hero.hp) { hero.action = null; hero.hurtTime = 0; state('down'); hero.koTime = 1.7; clearInput(); if (match?.mode !== 'versus') announce('CORE RESTART', 1.7); } + return true; + } + function bwLock(key, seconds) { bwx.lock[key] = Math.max(bwx.lock[key], seconds); } + 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; + // 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'; + d.t += dt; + if (caster === hero ? hero.hp <= 0 || hero.state === 'hurt' || hero.state === 'down' : dummy.ko || dummy.state === 'hurt' || dummy.state === 'down') { bwx.dance.splice(n, 1); continue; } + while (d.pulses < D.pulses && d.t >= D.first + d.pulses * D.gap) { + d.pulses++; + effects.push({ type: 'browo-fx', asset: 'browo-sunburst', x: caster.x, y: caster.y - 95, facing: 1, life: .5, maxLife: .5, size: 320 }); + spawnParticles(caster.x, caster.y - 95, '#8fd14f', 12, 260); trauma = Math.min(1, trauma + .12); sound('heavy'); + if (Math.abs(target.x - caster.x) < D.radius && Math.abs(target.y - caster.y) < 130 && bwAlive(target)) { + dotDamage(target, D.pulseDamage, '#cfe9a0'); bwLock(tk, D.gap + D.stunExtra); bwPoison(tk); + spawnParticles(target.x, target.y - 85, '#7be04a', 10, 200); + } + } + if (d.pulses >= D.pulses) bwx.dance.splice(n, 1); + } + // Holds: the rival stays in a hurt pose, without moving or acting, while a lock is active. + for (const key of ['player', 'enemy']) { + if (bwx.lock[key] <= 0) continue; + bwx.lock[key] = Math.max(0, bwx.lock[key] - dt); + if (key === 'enemy') { if (bwAlive(dummy)) { dummy.vx = 0; dummy.action = null; dummy.state = 'hurt'; dummy.stateTime = Math.min(dummy.stateTime, .2); } } + else if (hero.hp > 0) { hero.vx = 0; hero.action = null; hero.hurtTime = .25; if (hero.state !== 'hurt') state('hurt'); } + } + // Poison drain. + for (const key of ['player', 'enemy']) { + const p = bwx.poison[key]; if (!p) continue; + const target = key === 'player' ? hero : dummy; + p.t -= dt; p.acc += D.poisonDps * dt; + if (p.acc >= 1) { const n = Math.floor(p.acc); p.acc -= n; p.shown += n; dotDamage(target, n, '#7be04a', false); } + if (p.shown >= 3) { damageNumbers.push({ x: target.x + (random() - .5) * 24, y: target.y - 130, amount: p.shown, life: .9, maxLife: .9, color: '#7be04a' }); p.shown = 0; } + if (p.t <= 0 || !bwAlive(target)) bwx.poison[key] = null; + } + // Rage countdown. + for (const key of ['player', 'enemy']) if (bwx.rage[key] > 0) bwx.rage[key] = Math.max(0, bwx.rage[key] - dt); + // 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; } + 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; + } + // BRO WO Hutan Sawit: the group object is the shared `sunroar` slot, so pause/reset/cut-in plumbing is SOLAN's; `roars` + // counts the small palms raised and `giant` is the final palm. function updateForest(dt, s) { - const U = BW.forest, a = s.owner === 'enemy' ? dummy : hero, target = s.owner === 'enemy' ? hero : dummy; + const U = BW.forest, 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; - while (s.roars < U.trees.length && s.t >= U.at + s.roars * U.gap) { - const i = s.roars++, x = clamp(a.x + s.facing * U.trees[i], 70, W - 70); - effects.push({ type: 'browo-tree', x, y: CONFIG.groundY, life: .9, maxLife: .9 }); - effects.push({ type: 'browo-fx', asset: 'browo-impact', x, y: CONFIG.groundY - 24, facing: 1, life: .45, maxLife: .45, size: 240 }); - spawnParticles(x, CONFIG.groundY - 10, '#a47a3a', 14, 260, 'dust'); trauma = Math.min(1, trauma + .25); sound('heavy'); - if (Math.abs(target.x - x) < U.radius && target.y > CONFIG.groundY - 150) { - if (s.owner === 'enemy') window.__game.receiveHit(BW.balance.forestDamage); - else hitDummy(BW.balance.forestDamage, i === U.trees.length - 1 ? 190 : 50, '#9bd05a', x, CONFIG.groundY - 90, false, true); + const mx = (a.x + target.x) / 2; + // Camera script. + if (s.t < U.treesAt) { bwx.zoomT = 1.75; bwx.dimT = .82; bwx.cx = a.x; bwx.cy = a.y - 90; } + else if (s.t < U.giantAt) { bwx.zoomT = 1.2; bwx.dimT = .78; bwx.cx = mx; bwx.cy = a.y - 90; } + else if (s.t < U.release + .35) { bwx.zoomT = 1.12; bwx.dimT = .7; bwx.cx = mx; bwx.cy = a.y - 130; } + else { bwx.zoomT = 1; bwx.dimT = 0; } + if (s.t >= .15 && s.t < U.treesAt) trauma = Math.min(.35, trauma + dt * .5); + if (s.t >= U.lockAt && s.t < U.release && bwAlive(target)) bwLock(tk, .25); + // Ten palms in a rapid run from BRO WO toward (and a little past) the rival. + const dist = Math.abs(target.x - a.x); + while (s.roars < U.treeCount && s.t >= U.treesAt + s.roars * U.treeGap) { + const i = s.roars++, x = clamp(a.x + s.facing * (150 + i * (dist - 50) / (U.treeCount - 1)), 70, W - 70); + effects.push({ type: 'browo-tree', x, y: CONFIG.groundY, life: .6, maxLife: .6 }); + effects.push({ type: 'browo-fx', asset: 'browo-impact', x, y: CONFIG.groundY - 24, facing: 1, life: .4, maxLife: .4, size: 220 }); + spawnParticles(x, CONFIG.groundY - 10, '#a47a3a', 10, 240, 'dust'); trauma = Math.min(1, trauma + .18); sound('hit'); + if (bwAlive(target)) dotDamage(target, U.treeDamage, '#9bd05a'); + } + // The giant palm: rises, leans back, then comes down on the rival. + if (!s.giant && s.t >= U.giantAt) { + s.giant = bwx.giant = { t: 0, x: a.x + s.facing * 26, dir: s.facing, L: clamp(dist * 1.7 + 300, 760, 1250), hit: false }; + sound('heavy'); trauma = Math.min(1, trauma + .3); + } + if (s.giant) { + const g = s.giant; g.t += dt; + const slamAt = U.giantRise + U.giantHold; + if (!g.hit && g.t >= slamAt + U.slam) { + g.hit = true; + const tx = clamp(a.x + s.facing * dist, 70, W - 70); + effects.push({ type: 'browo-fx', asset: 'browo-impact', x: tx, y: CONFIG.groundY - 30, facing: 1, life: .6, maxLife: .6, size: 520 }); + effects.push({ type: 'browo-fx', asset: 'browo-sunburst', x: tx, y: CONFIG.groundY - 120, facing: 1, life: .5, maxLife: .5, size: 520 }); + for (let q = 0; q < 3; q++) spawnParticles(tx, CONFIG.groundY - 12, q === 1 ? '#fff1c8' : '#a47a3a', 22, 460, 'dust'); + trauma = 1; hitstop = Math.max(hitstop, .14); sound('heavy'); + if (bwAlive(target)) { if (target === dummy) { if (!hitDummy(U.giantDamage, 360, '#ffe08a', a.x, CONFIG.groundY - 100, true, true, 2)) dotDamage(dummy, U.giantDamage, '#ffe08a'); } else if (!window.__game.receiveHit(U.giantDamage, { shake: 2 })) dotDamage(hero, U.giantDamage, '#ffe08a'); } } } - s.phase = s.t < U.cutinDuration ? 'cutin' : s.roars < U.trees.length ? 'roaring' : 'fading'; - if (s.t >= U.duration) { if (s.owner === 'enemy') enemySunroar = null; else sunroar = null; } + s.phase = s.t < U.cutinDuration ? 'cutin' : s.t < U.treesAt ? 'roaring' : 'fading'; + if (s.t >= U.duration) { + bwx.rage[ok] = BW.rage.duration; 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 }); + 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 // projectiles with a limited turn rate, 16 each, 48 total). NIB is free after the 0.5 s cast; a late sidestep or a @@ -813,7 +904,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); + 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); 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'); @@ -839,7 +930,7 @@ if (!hero.grounded) { if (keys.has('s') && hero.vy > 0 && hero.doublePose <= 0) hero.vy = Math.max(hero.vy, CONFIG.fastFall); hero.vy += CONFIG.gravity * dt; hero.y += hero.vy * dt; if (hero.y >= CONFIG.groundY) { hero.y = CONFIG.groundY; hero.vy = 0; hero.grounded = true; hero.jumps = 0; hero.doublePose = 0; hero.airTime = 0; dust(hero.x, 11); sound('step'); } } const stride = metrics?.states?.[hero.state]?.stride_estimate || manifest?.metrics?.[hero.state === 'run' ? 'runStride' : 'walkStride'] || manifest?.locomotion?.[hero.state]?.stride_px || (hero.state === 'run' ? 184 : 148); hero.walkPhase += Math.abs(hero.vx) * dt / stride; - updateDummy(dt); updateProjectiles(dt); updateSquadron(dt); updateSquadron(dt,enemySquad); updateParade(dt,parade); updateParade(dt,enemyParade); updateMurmuration(dt,flock); updateMurmuration(dt,enemyFlock); updateSerpent(dt,serpent); updateSerpent(dt,enemySerpent); updateQuake(dt,quake); updateFinale(dt,finale); updateFinale(dt,enemyFinale); updateSkyfall(dt,skyfall); updateSkyfall(dt,enemySkyfall); updateOrrery(dt,orrery); updateOrrery(dt,enemyOrrery); updateSunroar(dt,sunroar); updateSunroar(dt,enemySunroar); updateDelivery(dt,delivery); updateDelivery(dt,enemyDelivery); updateTortoise(dt,tortoise); updateTortoise(dt,enemyTortoise); updateQuake(dt,enemyQuake); + updateDummy(dt); updateProjectiles(dt); updateSquadron(dt); updateSquadron(dt,enemySquad); updateParade(dt,parade); updateParade(dt,enemyParade); updateMurmuration(dt,flock); updateMurmuration(dt,enemyFlock); updateSerpent(dt,serpent); updateSerpent(dt,enemySerpent); updateQuake(dt,quake); updateFinale(dt,finale); updateFinale(dt,enemyFinale); updateSkyfall(dt,skyfall); updateSkyfall(dt,enemySkyfall); updateOrrery(dt,orrery); updateOrrery(dt,enemyOrrery); updateSunroar(dt,sunroar); updateSunroar(dt,enemySunroar);updateBrowo(dt); updateDelivery(dt,delivery); updateDelivery(dt,enemyDelivery); updateTortoise(dt,tortoise); updateTortoise(dt,enemyTortoise); updateQuake(dt,enemyQuake); if (voiceActive && (voiceKind === 'mira' || voiceKind === 'cora' || voiceKind === 'naja' || voiceKind === 'haldor' || voiceKind === 'zanni' || voiceKind === 'isolde' || voiceKind === 'rhea' || voiceKind === 'solan' || voiceKind === 'browo' || voiceKind === 'nib' || voiceKind === 'edda')) voiceClock += dt; if (voiceActive && voiceKind !== 'arco' && (!voiceRelevant() || (Number.isFinite(ultimateVoice.duration) && voiceElapsed() >= ultimateVoice.duration))) stopUltimateVoice(); advanceMatch(dt); @@ -1120,6 +1211,11 @@ function browoStrike(actor,move) { 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 raging=bwx.rage[enemy?'enemy':'player']>0,mul=raging?BW.rage.damageMul:1,dmg=Math.round(move.damage*mul); + if(move.dance){ + // 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; + } let hitX=null,inside; if(move.tree){ // Pohon Sawit: a palm erupts BW.tree.ahead px in front; everything within the radius on the floor is hit. @@ -1127,26 +1223,20 @@ effects.push({type:'browo-tree',x:hitX,y:CONFIG.groundY,life:.9,maxLife:.9}); effects.push({type:'browo-fx',asset:'browo-impact',x:hitX,y:CONFIG.groundY-24,facing:1,life:.45,maxLife:.45,size:240}); dust(hitX,18);spawnParticles(hitX,CONFIG.groundY-10,'#a47a3a',18,300,'dust');trauma=Math.min(1,trauma+.3); - inside=Math.abs(target.x-hitX)CONFIG.groundY-150; - } else if(move.dance){ - // Joget Gemoy: a leaf burst around BRO WO; everything within BW.dance.radius is hit. - hitX=actor.x; - effects.push({type:'browo-fx',asset:'browo-sunburst',x:actor.x,y:actor.y-95,facing:1,life:.5,maxLife:.5,size:300}); - spawnParticles(actor.x,actor.y-95,'#8fd14f',16,260);trauma=Math.min(1,trauma+.2); - inside=Math.abs(target.x-actor.x)CONFIG.groundY-150; } else { - effects.push({type:'slash',x:actor.x+actor.facing*55,y:hy+5,life:.24,maxLife:.24,facing:actor.facing,index:move.index,color:move.index===3?'#ffe7a0':'#f0dba5'}); + effects.push({type:'slash',x:actor.x+actor.facing*55,y:hy+5,life:.24,maxLife:.24,facing:actor.facing,index:move.index,color:raging?'#ff9a5a':move.index===3?'#ffe7a0':'#f0dba5'}); if(move.type==='attack'&&move.index===3){dust(actor.x+actor.facing*(em?.x??95),12);trauma=Math.min(1,trauma+.15);} const dx=(target.x-actor.x)*actor.facing; - inside=dx>-22&&dx-22&&dx0)rechargePulse[name]=.22;hero.cooldowns[name]=Math.max(0,hero.cooldowns[name]-cooldownMax[name]*BALANCE.basicCooldownRefund);} } - if(connected)sound(move.tree||move.dance||move.index===3?'heavy':'hit'); + if(connected)sound(move.tree||move.index===3?'heavy':'hit'); } function nibStrike(actor,move) { const enemy=actor===dummy,target=enemy?hero:dummy,em=window.NIB_METRICS?.emitters?.[move.name]; @@ -1438,6 +1528,7 @@ let index=0; // Projectile casts that release at 50% show wind-up -> thrust -> full extension, like the player's pose. if(dummy.action?.type==='skill1'&&opponentCharacter!=='fenr')index=Math.min(2,list.length-1,Math.floor(dummy.action.t/(dummy.action.duration*.5)*3)); + else if(dummy.action?.loops)index=Math.floor(dummy.action.t/dummy.action.duration*list.length*dummy.action.loops)%list.length; else if(dummy.action)index=Math.floor(dummy.action.t/dummy.action.duration*list.length); else if(name==='walk'||name==='run')index=Math.floor(dummy.walkPhase*list.length)%list.length; else if(name==='down')index=Math.floor(dummy.stateTime/.55*list.length); @@ -1492,6 +1583,7 @@ result.phase = 'somersault'; } else if (hero.state === 'walk' || hero.state === 'run') index = Math.floor(hero.walkPhase * list.length) % list.length; else if (hero.action?.type === 'skill1') index = Math.min(2,list.length-1,Math.floor(hero.action.t/(hero.action.duration*.5)*3)); + else if (hero.action?.loops) index = Math.floor(hero.action.t / hero.action.duration * list.length * hero.action.loops) % list.length; else if (hero.action) index = Math.min(list.length - 1, Math.floor(hero.action.t / hero.action.duration * list.length)); else if (row.loop === false) index = Math.min(list.length - 1, Math.floor(hero.animTime * (row.fps || 8))); else index = Math.floor(hero.animTime * (row.fps || 8)) % list.length; @@ -1522,6 +1614,55 @@ ctx.beginPath();ctx.ellipse(dummy.x,CONFIG.groundY+3,37,7,0,0,Math.PI*2);ctx.stroke();ctx.restore(); } } + // BRO WO extras: poison aura on a drained rival, rage aura + palm-tree club on BRO WO, and the giant palm. + // The giant palm of the ultimate: behind the fighters while it rises and leans back, in front of them as it comes down. + function drawBrowoGiant(front) { + const gnt = bwx.giant, im = images['fx-browo-sawit']; if (!gnt || !im) return; + const U = BW.forest, t = gnt.t, slamning = t >= U.giantRise + U.giantHold; if (slamning !== front) return; + const riseK = clamp(t / U.giantRise, 0, 1), eRise = 1 - Math.pow(1 - riseK, 3), holdT = clamp((t - U.giantRise) / U.giantHold, 0, 1), slamT = clamp((t - U.giantRise - U.giantHold) / U.slam, 0, 1); + let ang = -.12 * eRise - .22 * holdT; if (slamT > 0) ang = -.34 + (Math.PI / 2 + .34) * slamT * slamT; + const after = t - (U.giantRise + U.giantHold + U.slam), alpha = after > 0 ? clamp(1 - after / .5, 0, 1) : 1; + if (alpha <= 0) return; + const L = gnt.L * eRise, w = im.width / im.height * gnt.L * .9, shake = t > U.giantRise && slamT === 0 ? Math.sin(realTime * 70) * 2 : 0; + ctx.save(); ctx.globalAlpha = alpha; ctx.translate(gnt.x + shake, CONFIG.groundY + 8); ctx.rotate(gnt.dir * ang); ctx.drawImage(im, -w / 2, -L, w, L); ctx.restore(); + } + function drawBrowoExtras() { + if (!BW) return; + const bubbles = (a, color) => { for (let i = 0; i < 6; i++) { const ph = (realTime * .9 + i * .17) % 1, bx = a.x + Math.sin(realTime * 3 + i * 2.1) * 26 + (i - 3) * 6, by = a.y - 15 - ph * 130; ctx.globalAlpha = (1 - ph) * .85; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(bx, by, 4 + (i % 3) * 2, 0, 7); ctx.fill(); } ctx.globalAlpha = 1; }; + for (const key of ['player', 'enemy']) { + const a = key === 'player' ? hero : dummy; + if (bwx.poison[key] && bwAlive(a)) { + const g = ctx.createRadialGradient(a.x, a.y - 75, 10, a.x, a.y - 75, 95); g.addColorStop(0, 'rgba(120,230,60,.36)'); g.addColorStop(1, 'rgba(120,230,60,0)'); + ctx.fillStyle = g; ctx.beginPath(); ctx.arc(a.x, a.y - 75, 95, 0, 7); ctx.fill(); bubbles(a, '#9be84f'); + } + } + const im = images['fx-browo-sawit']; + for (const key of ['player', 'enemy']) { + if (bwx.rage[key] <= 0) continue; + const a = key === 'player' ? hero : dummy; if (!bwAlive(a)) continue; + 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(); + if (im) { + // The club: a palm tree held in the front hand, leaning back at rest and swinging through every attack. + let ang = -.38; const act = a.action; + if (act && act.type === 'attack') { const p = clamp(act.t / act.duration, 0, 1), e = 1 - Math.pow(1 - p, 2); ang = -1.15 + (act.index === 2 ? 2.7 : 2.3) * e; } + else if (act && act.type === 'skill1') { const p = clamp(act.t / act.duration, 0, 1); ang = -.9 + 2.2 * p * p; } + else if (act) ang = -.2; + const h = 190, w = im.width / im.height * h; + ctx.save(); ctx.translate(a.x + a.facing * 34, a.y - 72); ctx.rotate(a.facing * ang); ctx.drawImage(im, -w / 2, -h + 14, w, h); ctx.restore(); + } + if (Math.sin(realTime * 40) > .6) spawnParticles(a.x + (random() - .5) * 50, a.y - 40 - random() * 80, '#ff8a3a', 1, 80); + } + } + // Small palms erupt behind both fighters so BRO WO and the rival stay readable. + function drawBrowoTrees() { + const im = images['fx-browo-sawit']; if (!im) return; + for (const e of effects) { + if (e.type !== 'browo-tree') continue; + const t = 1 - e.life / e.maxLife, k = Math.min(1, t * 7) * (t > .7 ? 1 - (t - .7) / .3 : 1), h = im.height * .62, w = im.width * .62; + ctx.save(); ctx.translate(Math.round(e.x), Math.round(e.y)); ctx.beginPath(); ctx.rect(-w, -h - 10, w * 2, h + 10); ctx.clip(); ctx.drawImage(im, -w / 2, -h * k, w, h); ctx.restore(); + } + } function drawEffects() { for (const p of projectiles) { if(p.asset && images['fx-'+p.asset]){const w=p.size||84,h=p.size||76;ctx.save();ctx.translate(p.x,p.y);ctx.scale(p.facing,1);if(p.vy)ctx.rotate(Math.atan2(p.vy,Math.abs(p.vx)));if(p.spin)ctx.rotate(realTime*p.spin);ctx.drawImage(images['fx-'+p.asset],-w/2,-h/2,w,h);ctx.restore();continue;} @@ -1533,7 +1674,6 @@ const t = 1 - e.life / e.maxLife; ctx.save(); ctx.translate(Math.round(e.x), Math.round(e.y)); ctx.globalAlpha = Math.min(1, e.life / e.maxLife * 2); ctx.lineCap = 'square'; if((e.type==='fenr-fx'||e.type==='mira-fx'||e.type==='cora-fx'||e.type==='naja-fx'||e.type==='haldor-fx'||e.type==='zanni-fx'||e.type==='isolde-fx'||e.type==='rhea-fx'||e.type==='solan-fx'||e.type==='nib-fx'||e.type==='edda-fx'||e.type==='browo-fx') && images['fx-'+e.asset]){ctx.scale(e.facing||1,1);const size=e.size*(.75+t*.4);ctx.drawImage(images['fx-'+e.asset],-size/2,-size/2,size,size);} // BRO WO's palm erupts from the floor (clipped at ground level), holds, then sinks back as it fades. - if(e.type==='browo-tree'&&images['fx-browo-sawit']){const im=images['fx-browo-sawit'],k=Math.min(1,t*7)*(t>.7?1-(t-.7)/.3:1),h=im.height*.62,w=im.width*.62;ctx.globalAlpha=1;ctx.beginPath();ctx.rect(-w,-h-10,w*2,h+10);ctx.clip();ctx.drawImage(im,-w/2,-h*k+ (1-k)*0,w,h);} // EDDA's tortoise spirit keeps one size; it fades in and out and lifts before each stomp. if(e.type==='edda-spirit'&&images['fx-'+e.asset]){ctx.globalAlpha=Math.min(1,t*6,(1-t)*5)*.92;ctx.scale(e.facing||1,1);ctx.drawImage(images['fx-'+e.asset],-e.size/2,-e.size-e.lift,e.size,e.size);} if (e.type === 'slash') { ctx.scale(e.facing, 1); ctx.rotate(e.index === 2 ? -.5 : .15); ctx.strokeStyle = e.color; ctx.lineWidth = 9 * (1 - t) + 2; ctx.beginPath(); ctx.arc(10, 4, 55 + e.index * 10, -1.2 + t * .3, 1.0 + t * .4); ctx.stroke(); ctx.strokeStyle = '#ffffffaa'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(4, 4, 46 + e.index * 10, -.85, .7); ctx.stroke(); } @@ -1638,11 +1778,23 @@ function draw() { ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); ctx.imageSmoothingEnabled = false; ctx.fillStyle = '#9ed2dc'; ctx.fillRect(0, 0, viewW, viewH); const shakeX = Math.sin(realTime * 93) * trauma * trauma * 13, shakeY = Math.cos(realTime * 111) * trauma * trauma * 9; - drawBackground(shakeX, shakeY); ctx.save(); ctx.translate(offsetX + shakeX * scale, offsetY + shakeY * scale); ctx.scale(scale, scale); + // BRO WO's ultimate dives the camera onto him: the whole picture (stage and fighters) is zoomed around a point that eases + // toward the screen centre, clamped so the zoomed stage never leaves an empty edge; the stage is then darkened. + const camera = bwx.zoom > 1.002 || bwx.dim > .004; + if (camera) { + const sx = offsetX + bwx.cx * scale, sy = offsetY + bwx.cy * scale, z = bwx.zoom, w = clamp((z - 1) / .75, 0, 1); + const ax = clamp(sx + (viewW / 2 - sx) * w, viewW - z * (viewW - sx), z * sx), ay = clamp(sy + (viewH * .55 - sy) * w, viewH - z * (viewH - sy), z * sy); + ctx.save(); ctx.translate(ax, ay); ctx.scale(z, z); ctx.translate(-sx, -sy); + } + drawBackground(shakeX, shakeY); + if (bwx.dim > .004) { ctx.fillStyle = `rgba(3,8,5,${bwx.dim})`; ctx.fillRect(-viewW, -viewH, viewW * 3, viewH * 3); } + 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. 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); if (hero.x < dummy.x) { drawHero(); drawDummy(); } else { drawDummy(); drawHero(); } - drawSquadron(); drawSquadron(enemySquad); drawParade(parade); drawParade(enemyParade); drawMurmuration(flock); drawMurmuration(enemyFlock); drawSerpent(serpent); drawSerpent(enemySerpent); drawEffects(); drawHitboxes(); ctx.restore(); updateHud(); + 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(); } function fighterInfo(id,form) { if(id==='fenr'){const wolf=form==='wolf';return {name:'FENR',cls:wolf?'WEREWOLF':'DEMI-HUMAN',deck:wolf?'FENR ยท FERAL':'FENR',title:'WOLF RANGER',portrait:'assets/fenr/ui/portrait-'+(wolf?'wolf':'human')+'.webp',names:F.kits[wolf?'wolf':'human'].names,icons:['basic','skill1','skill2','ultimate'].map((s,i)=>'assets/fenr/ui/icon-'+(i===3?'ultimate':(wolf?'wolf':'human')+'-'+s)+'.webp')};} @@ -1672,7 +1824,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':selectedCharacter==='browo'?'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':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() { @@ -1726,7 +1878,7 @@ 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'); 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'):''; - 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
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',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:'FOUR-PALM FOREST',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,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',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',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]);} const visible = ct < SQUAD.cutinDuration; 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; diff --git a/menu.js b/menu.js index 49db330..f812311 100644 --- a/menu.js +++ b/menu.js @@ -12,7 +12,7 @@ ...(window.Zanni&&window.ZANNI_MANIFEST?{zanni:{name:'ZANNI',tag:'THE CLOCKWORK JESTER',race:'MECHA',portrait:'assets/zanni/ui/portrait.webp',art:'assets/menu/zanni-select.webp',detail:'Arms that stretch. Jokes that sting.',basic:'SCISSOR JAB',ultimate:'GRAND FINALE',style:'Trickster / extending scissor-arm strikes and bladed rings',color:'#c9d64a'}}:{}), ...(window.Isolde&&window.ISOLDE_MANIFEST?{isolde:{name:'ISOLDE',tag:'THE WHITE STRIDER',race:'MECHA',portrait:'assets/isolde/ui/portrait.webp',art:'assets/menu/isolde-select.webp',detail:'Long legs, longer lance, zero mercy.',basic:'LANCE LINE',ultimate:'SKYFALL LANCES',style:'Long-range lancer / stilt-leg charges',color:'#9fd4ff'}}:{}), ...(window.Rhea&&window.RHEA_MANIFEST?{rhea:{name:'RHEA',tag:'THE LITTLE ORRERY',race:'MECHA',portrait:'assets/rhea/ui/portrait.webp',art:'assets/menu/rhea-select.webp',detail:'Homework done. Planets armed. Class dismissed.',basic:'ORBIT STRIKE',ultimate:'GRAND ORRERY',style:'Zoner / orbiting orrery spheres',color:'#c7485e'}}:{}), - ...(window.Browo&&window.BROWO_MANIFEST?{browo:{name:'BRO WO',tag:'THE PALM RANGER',race:'HUMAN',portrait:'assets/browo/ui/portrait.webp',art:'assets/menu/browo-select.webp',detail:'Steady boots. Wide hat. Whole forests.',basic:'TINJU SAFARI',ultimate:'HUTAN SAWIT',style:'Bruiser / palm trees and area slam',color:'#c9b87a'}}:{}), + ...(window.Browo&&window.BROWO_MANIFEST?{browo:{name:'BRO WO',tag:'THE PALM RANGER',race:'HUMAN',portrait:'assets/browo/ui/portrait.webp',art:'assets/menu/browo-select.webp',detail:'Steady boots. Wide hat. Whole forests, then a rampage.',basic:'TINJU SAFARI',ultimate:'HUTAN SAWIT',style:'Bruiser / palm trees and area slam',color:'#c9b87a'}}:{}), ...(window.Solan&&window.SOLAN_MANIFEST?{solan:{name:'SOLAN',tag:'THE SUNMANE',race:'DEMI-HUMAN',portrait:'assets/solan/ui/portrait.webp',art:'assets/menu/solan-select.webp',detail:'Golden mane, heavy blade, louder roar.',basic:'SUNBLADE',ultimate:'SUNMANE ROAR',style:'Powerhouse / greatsword and roar',color:'#f2b632'}}:{}), ...(window.Nib&&window.NIB_MANIFEST?{nib:{name:'NIB',tag:'THE ROOFTOP POST',race:'DEMI-HUMAN',portrait:'assets/nib/ui/portrait.webp',art:'assets/menu/nib-select.webp',detail:'Every letter delivered. Every punch, too.',basic:'BATON FLURRY',ultimate:'SPECIAL DELIVERY',style:'Rushdown / twin courier batons',color:'#8fd4ff'}}:{}), ...(window.Edda&&window.EDDA_MANIFEST?{edda:{name:'EDDA',tag:'THE SHELL SAGE',race:'DEMI-HUMAN',portrait:'assets/edda/ui/portrait.webp',art:'assets/menu/edda-select.webp',detail:'Slow to anger, impossible to knock down.',basic:'STAFF FORMS',ultimate:'ELDER TORTOISE',style:'Counter defender / staff and shell guard',color:'#8fe3b4'}}:{})