diff --git a/game.js b/game.js
index d532eda..44eafee 100644
--- a/game.js
+++ b/game.js
@@ -2704,9 +2704,73 @@
$('#player-status').textContent=selectedCharacter==='fenr'?(hero.form==='wolf'?'FERAL ACTIVE':'RANGER READY'):selectedCharacter==='mira'?'PILOT READY':selectedCharacter==='cora'?'WINGS READY':selectedCharacter==='naja'?'SANDS READY':selectedCharacter==='haldor'?'FURNACE HOT':selectedCharacter==='zanni'?'SHOWTIME':selectedCharacter==='isolde'?'LANCE READY':selectedCharacter==='rhea'?'ORBITS SET':selectedCharacter==='solan'?'PRIDE READY':selectedCharacter==='nib'?'ON THE ROUTE':selectedCharacter==='edda'?'STEADY AS STONE':CF[selectedCharacter]?(bwx.rage.player>0&&modeOf(selectedCharacter)?modeOf(selectedCharacter).label+' '+Math.ceil(bwx.rage.player)+'s':CF[selectedCharacter].status):selectedCharacter==='browo'?(bwx.rage.player>0?'MENGAMUK '+Math.ceil(bwx.rage.player)+'s':'SAFARI READY'):'CORE ACTIVE';
for(const [actor,id] of [[hero,'player-form-timer'],[dummy,'enemy-form-timer']]){const el=$('#'+id),active=actor.form==='wolf';el.hidden=!active;el.textContent=active?'FERAL '+Math.max(0,actor.formTime).toFixed(1)+' s':'';el.style.setProperty?.('--remaining',String(actor.formTime/F.balance.duration));}
}
+ // ---- Meter ultimate di HUD (Tahap 2.1) ----
+ // Satu bar tipis di bawah HP tiap sisi (#ult-player, #ult-enemy). Data yang sama untuk pemain dan CPU/P2: cooldown ultimate
+ // aktor, mode pasca-ultimate dari modeOf()/bwx.rage, bentuk feral FENR dari form/formTime. Bar terisi = cooldown pulih;
+ // SIAP berdenyut; selama mode bar menjadi waktu mode yang menyusut. DOM hanya ditulis saat tampilannya berubah.
+ const ULT_COLOR = { arco: '#67e4dd', fenr: '#e0a060', browo: '#d9b44a' }, ultMeters = {};
+ function ultMax(side) {
+ if (side === 'player') return cooldownMax.ultimate;
+ const id = opponentCharacter;
+ return (id === 'fenr' ? F?.balance?.cooldowns?.ultimate : KITS[id]?.balance?.cooldowns?.ultimate) || BALANCE.ultimate.cooldown;
+ }
+ function ultState(side) {
+ const mine = side === 'player', actor = mine ? hero : dummy, id = mine ? selectedCharacter : opponentCharacter;
+ if (id === 'fenr' && F && (actor.form === 'wolf' || actor.transformPending > 0)) return { mode: true, label: 'FERAL', left: actor.formTime || F.balance.duration, total: F.balance.duration, color: ULT_COLOR.fenr };
+ const md = bwx.rage[side] > 0 ? modeOf(id) : null;
+ if (md) return { mode: true, label: md.label, left: bwx.rage[side], total: md.duration, color: md.aura, again: (actor.cooldowns.ultimate || 0) <= 0 };
+ return { mode: false, cd: Math.max(0, actor.cooldowns.ultimate || 0), max: Math.min(ultMax(side), 120), color: CF[id]?.color || ULT_COLOR[id] || '#e6c78f' };
+ }
+ function updateUltMeter(side) {
+ let m = ultMeters[side];
+ if (m === undefined) { const root = $('#ult-' + side); m = ultMeters[side] = root ? { root, fill: root.querySelector('.ult-fill'), name: root.querySelector('.ult-name'), val: root.querySelector('.ult-val'), kbd: root.querySelector('.ult-kbd'), key: '', p: -1 } : null; }
+ if (!m) return;
+ const live = side === 'player' || aiEnabled || p2Mode, st = ultState(side);
+ let state, p, name, val, low = false;
+ if (st.mode) { state = 'mode'; p = clamp(st.left / st.total, 0, 1); name = st.label; val = st.left.toFixed(1).replace('.', ',') + ' dtk'; low = st.left <= 2; }
+ else if (st.cd <= 0) { state = 'ready'; p = 1; name = 'ULTIMATE'; val = 'SIAP'; }
+ else { state = 'charging'; p = clamp(1 - st.cd / st.max, 0, 1); name = 'ULTIMATE'; val = Math.ceil(st.cd) + ' dtk'; }
+ const kbd = side === 'player' ? 'P' : '', key = [live, state, name, val, low, st.again, st.color, kbd].join('|');
+ if (key !== m.key) {
+ m.key = key; m.root.hidden = !live; m.root.dataset.state = state; m.root.classList.toggle('low', low); m.root.classList.toggle('again', !!st.again);
+ m.root.style.setProperty('--ult', st.color); m.name.textContent = name; m.val.textContent = val; m.kbd.textContent = kbd; m.kbd.hidden = !kbd;
+ m.root.setAttribute('aria-label', (side === 'player' ? 'Ultimate pemain: ' : 'Ultimate lawan: ') + (st.mode ? 'mode ' + name + ' aktif' : state === 'ready' ? 'siap' : 'mengisi'));
+ }
+ if (Math.abs(p - m.p) > .002) { m.p = p; m.fill.style.transform = 'scaleX(' + p.toFixed(3) + ')'; }
+ }
+ function updateUltMeters() { if (menuOpen) return; updateUltMeter('player'); updateUltMeter('enemy'); }
+ // ---- Daftar gerakan di menu jeda (Tahap 2.4) ----
+ // Diisi ulang tiap jeda dari data karakter yang sedang dimainkan (nama skill, mode, jurus toko) dan dari mode dua pemain.
+ const MOVE_KEY_PREF = 'konoha.moveList';
+ function buildMoveList() {
+ const body = $('#move-list-body'); if (!body) return;
+ const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
+ const kb = list => { const k = el('span', 'mv-keys'); list.forEach(x => { if (x === '+') k.append(el('span', '', '+')); else k.append(el('kbd', 'mv-key', x)); }); return k; };
+ // hint satu baris di samping nama (inline) atau di baris kedua (nama skill yang panjang)
+ const row = (list, name, hint, inline) => { const r = el('div', 'mv-row'), t = el('span'); t.append(el('span', '', name)); if (hint) t.append(el('small', inline ? 'inl' : '', hint)); r.append(kb(list), t); return r; };
+ const sec = (title, sub, cls) => { const s = el('section', 'mv-sec' + (cls ? ' ' + cls : '')), h = el('h3', '', title); if (sub) h.append(el('small', '', sub)); s.append(h); body.append(s); return s; };
+ body.textContent = '';
+ const id = selectedCharacter, me = fighterInfo(id, 'human'), base = bwBaseCooldowns(id) || cooldownMax, md = modeOf(id), cd = n => Math.round(base[n]) + ' dtk';
+ const shop = SHOP_MOVES[window.Meta?.equippedMove?.()];
+ sec('Kontrol dasar').append(row(['A', 'D'], 'Jalan', 'ketuk 2× = lari', 1), row(['W'], 'Lompat', '2× = lompat ganda', 1), row(['S'], 'Menunduk'), row(['L'], 'Blok', 'ketuk cepat = parry', 1), row(['J'], 'Lempar'), row(['K'], 'Jurus toko', shop ? shop.name : 'belum dipasang', 1));
+ sec('Serangan dan skill', me.name).append(row(['Spasi'], me.names[0], 'kombo 3 pukulan'), row(['I'], me.names[1], 'skill 1 · cooldown ' + cd('skill1')), row(['O'], me.names[2], 'skill 2 · cooldown ' + cd('skill2')), row(['P'], me.names[3], 'ultimate · cooldown ' + cd('ultimate')));
+ sec('Gerakan perintah', null, 'cmd').append(row(['↓', '↓', '+', 'Spasi'], 'Uppercut', 'peluncur ke atas'), row(['↓', '→', '+', 'Spasi'], 'Tinju maju', '→ = arah lawan'));
+ if (md) { const ms = sec('Mode ' + md.label, Math.round(md.duration) + ' dtk'); ['Spasi', 'I', 'O', 'P'].forEach((k, i) => ms.append(row([k], md.names[i], i === 3 ? 'sekali pakai, mengakhiri mode' : '', i !== 3))); }
+ const other = sec('Lainnya'); other.append(row(['Esc'], 'Jeda / lanjut'), row(['R'], 'Mulai ulang'));
+ if (document.documentElement.classList.contains('touch')) other.append(el('small', 'mv-note', 'Layar sentuh: UP, →P, K, BLOK, dan LEMPAR ada di sisi kanan.'));
+ if (p2Mode) { const rv = fighterInfo(opponentCharacter, 'human'), p2 = sec('Pemain 2', rv.name); p2.append(row(['←', '→'], 'Jalan'), row(['↑'], 'Lompat'), row(['N'], rv.names[0], 'kombo 3 pukulan'), row(['M'], rv.names[1], 'skill 1'), row([','], rv.names[2], 'skill 2'), row(['.'], rv.names[3], 'ultimate')); }
+ }
+ // Pilihan lipat/buka diingat per perangkat; tanpa pilihan, terbuka bila jendela cukup lebar. Hanya klik pengguna pada ringkasan yang menyimpan.
+ function openMoveList() {
+ buildMoveList();
+ const d = $('#move-list'); if (!d) return;
+ const saved = prefs.get(MOVE_KEY_PREF, null); d.open = saved === null ? window.innerWidth >= 700 : saved === '1';
+ }
+ $('#move-list > summary')?.addEventListener('click', () => prefs.set(MOVE_KEY_PREF, $('#move-list').open ? '0' : '1'));
function updateHud() {
updateFighterIdentity();
updateMatchHud();
+ updateUltMeters();
if(systemAnnouncer){const a=systemAnnouncer.snapshot();canvas.dataset.announcerVoice=a.voice;canvas.dataset.announcerCue=a.cue;canvas.dataset.announcerLastCue=a.lastCue;canvas.dataset.announcerActive=String(a.active);canvas.dataset.announcerPaused=String(a.paused);canvas.dataset.announcerTime=String(a.time);canvas.dataset.announcerStarts=String(a.starts);canvas.dataset.announcerError=a.error;}
canvas.dataset.character=selectedCharacter;canvas.dataset.playerForm=hero.form||'human';canvas.dataset.opponentForm=dummy.form||'human';canvas.dataset.opponentState=dummy.state;canvas.dataset.aiEnabled=String(aiEnabled);canvas.dataset.opponentFrame=String(opponentPose().frame);
canvas.dataset.ready = String(ready);
@@ -2769,7 +2833,7 @@
for(const id of Object.keys(CF)){CUTIN_ART[id]='assets/'+id+'/ui/cutin.webp';if(CF[id].mode)CUTIN_ART[id+'~mode']='assets/'+id+'/ui/cutin-mode.webp';}
let cutinWarm=[];
function warmCutins(){cutinWarm=[...new Set([selectedCharacter,opponentCharacter])].flatMap(id=>id==='browo'?[CUTIN_ART.browo,CUTIN_ART.browofire]:[CUTIN_ART[id],CUTIN_ART[id+'~mode']]).filter(Boolean).map(src=>{const im=new Image();im.decoding='async';im.src=src;return im;});}
- function setPaused(value) { paused = value; clearInput(); syncUltimateVoice(); $('#pause-btn').textContent = value ? '▶' : 'Ⅱ'; const panel = $('#pause-panel'); if (value && !panel.open) panel.showModal(); if (!value && panel.open) panel.close(); if (!value) canvas.focus({ preventScroll: true }); }
+ function setPaused(value) { paused = value; clearInput(); syncUltimateVoice(); $('#pause-btn').textContent = value ? '▶' : 'Ⅱ'; const panel = $('#pause-panel'); if (value && !panel.open) { try { openMoveList(); } catch (e) { console.error(e); } panel.showModal(); } if (!value && panel.open) panel.close(); if (!value) canvas.focus({ preventScroll: true }); }
function openSettings() { settingsOpen = true; clearInput(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); $('#close-settings').innerHTML=(menuOpen?'Kembali ke menu':'Kembali ke arena')+' ↗'; $('#settings-panel').showModal(); }
function closeSettings() { settingsOpen = false; $('#settings-panel').close(); syncUltimateVoice(); window.FrontEnd?.syncBackground(); canvas.focus({ preventScroll: true }); }
const handled = new Set(['w','a','s','d',' ','i','o','p','k','l','j','escape','r','arrowleft','arrowright','arrowup','n','m',',','.']);
diff --git a/index.html b/index.html
index ce54105..1ba82da 100644
--- a/index.html
+++ b/index.html
@@ -7,9 +7,9 @@
P1ARCO
MECHAARCO
MECHAFENR
CPUFENR
CPU