| |
| |
| |
| |
| |
| |
| |
| (function () { |
| "use strict"; |
|
|
| |
| const W = 900, Hgt = 420, GROUND = 372; |
| const CFG = { |
| BOSS_HP: 100, PLAYER_HP: 100, |
| BOSS_MOVE: 190, PLAYER_MOVE: 188, ROLL_SPEED: 230, |
| MIN_GAP: 46, |
| CLEAVE_REACH: 170, CLEAVE_DMG: 22, CLEAVE_LUNGE: 270, |
| CLEAVE_WINDUP: 0.30, CLEAVE_ACTIVE: 0.17, CLEAVE_RECOVER: 0.42, CLEAVE_COOLDOWN: 0.34, |
| PATK_REACH: 120, PATK_DMG: 6, |
| PATK_WINDUP: 0.13, PATK_ACTIVE: 0.10, PATK_RECOVER: 0.30, |
| ROLL_DUR: 0.53, ROLL_IFR_A: 0.10, ROLL_IFR_B: 0.37, |
| BLOCK_MIT: 0.2, STAGGER: 0.5, |
| BOSS_BLOCK_TIME: 0.4, BOSS_BLOCK_MIT: 0.1, |
| STAM_MAX: 100, STAM_ROLL: 28, STAM_ATK: 18, STAM_BLOCK: 24, STAM_REGEN: 26, |
| COMMIT_MOVE: 0.30, COMMIT_IDLE: 0.34, |
| THINK_MIN: 0.10, |
| SHIELD_TIME: 5.0, |
| |
| }; |
|
|
| |
| const A = window.GAME_ASSETS; |
| const imgs = {}; |
| function loadImg(src) { return new Promise(r => { const i = new Image(); i.onload = () => r(i); i.src = src; }); } |
|
|
| |
| const AUDIO_BASE = window.MM_AUDIO_BASE || "audio/"; |
| const SFX = { |
| p_attack: "sfx/attack_knight.wav", p_hurt: "sfx/hurt_knight.wav", |
| p_roll: "sfx/jump_knight.wav", p_die: "sfx/die_knight.wav", |
| shield: "sfx/gem.wav", ui: "sfx/coin.wav", |
| b_cleave: "sfx/axe_boss.wav", b_hurt: "sfx/hurt_monster.wav", |
| b_die: "sfx/die_boss.wav", roar: "sfx/roar_monster.wav", |
| b_block: "sfx/bit_monster.wav", |
| }; |
| const MUSIC = Array.from({ length: 12 }, (_, i) => `mp3/Pixel ${i + 1}.mp3`); |
| const sfxBuf = {}; |
| let musicEl = null, muted = false, audioReady = false; |
| const aurl = p => AUDIO_BASE + encodeURI(p); |
|
|
| function initAudio() { |
| if (audioReady) return; audioReady = true; |
| for (const k in SFX) { const a = new Audio(aurl(SFX[k])); a.preload = "auto"; sfxBuf[k] = a; } |
| } |
| function playSfx(k, vol = 0.6) { |
| if (muted || !sfxBuf[k]) return; |
| const a = sfxBuf[k].cloneNode(); a.volume = vol; a.play().catch(() => {}); |
| } |
| function startMusic() { |
| if (muted) return; |
| const track = MUSIC[Math.floor(Math.random() * MUSIC.length)]; |
| if (musicEl) musicEl.pause(); |
| musicEl = new Audio(aurl(track)); |
| musicEl.volume = 0.3; |
| musicEl.addEventListener("ended", () => { if (!muted) startMusic(); }); |
| musicEl.play().catch(() => {}); |
| } |
| function stopMusic() { if (musicEl) { musicEl.pause(); musicEl = null; } } |
| function toggleMute() { |
| muted = !muted; |
| if (muted) { if (musicEl) musicEl.pause(); } |
| else if (musicEl) musicEl.play().catch(() => {}); |
| const b = document.getElementById("mm-mute"); if (b) b.textContent = muted ? "🔇" : "🔊"; |
| } |
|
|
| |
| class Anim { |
| constructor(who) { this.man = A[who].manifest; this.img = imgs[who]; this.who = who; |
| |
| |
| this.native = who === "boss" ? -1 : 1; |
| this.name = "idle"; this.t = 0; this.idx = 0; this.done = false; this.loop = true; this.speed = 1; this.rev = false; } |
| set(name, loop = true, speed = 1, rev = false) { |
| |
| |
| if (this.name === name) { this.loop = loop; this.speed = speed; this.rev = rev; return; } |
| this.name = name; this.loop = loop; this.speed = speed; this.rev = rev; this.t = 0; this.done = false; |
| this.idx = rev ? this.man.anims[name].frames - 1 : 0; |
| } |
| update(dt) { |
| const a = this.man.anims[this.name]; if (!a) return; |
| this.t += dt * a.fps * this.speed; |
| if (this.t >= 1) { |
| const steps = Math.floor(this.t); this.t -= steps; |
| this.idx += steps * (this.rev ? -1 : 1); |
| if (this.rev) { |
| if (this.idx < 0) { |
| if (this.loop) this.idx = ((this.idx % a.frames) + a.frames) % a.frames; |
| else { this.idx = 0; this.done = true; } |
| } |
| } else if (this.idx >= a.frames) { |
| if (this.loop) this.idx %= a.frames; |
| else { this.idx = a.frames - 1; this.done = true; } |
| } |
| } |
| } |
| progress() { const a = this.man.anims[this.name]; return (this.idx + this.t) / a.frames; } |
| draw(ctx, x, facing, scale) { |
| const m = this.man, a = m.anims[this.name]; |
| const sx = this.idx * m.frameW, sy = a.row * m.frameH; |
| const dw = m.frameW * scale, dh = m.frameH * scale; |
| const dy = GROUND - m.footY * scale; |
| |
| |
| ctx.save(); |
| ctx.translate(x, dy); |
| |
| ctx.scale(facing * this.native < 0 ? -1 : 1, 1); |
| ctx.drawImage(this.img, sx, sy, m.frameW, m.frameH, -m.centerX * scale, 0, dw, dh); |
| ctx.restore(); |
| } |
| } |
|
|
| |
| const boss = { x: 250, hp: CFG.BOSS_HP, facing: 1, anim: null, |
| state: "idle", t: 0, cd: 0, think: 0, pending: false, action: "IDLE", |
| actT: 0, cleavePhase: "", cleaveHit: false, scale: 1.7, telemetry: null, blockFlash: 0 }; |
| const player = { x: 650, hp: CFG.PLAYER_HP, stam: CFG.STAM_MAX, facing: -1, anim: null, |
| state: "idle", t: 0, atkHit: false, scale: 2.4, shield: 0, shieldFlash: 0 }; |
|
|
| const keys = {}; |
| let game = "menu"; |
| let lastDecision = null; |
| let difficulty = "hard"; |
| let fightLog = []; |
|
|
| |
| const dist = () => Math.abs(player.x - boss.x); |
| |
| |
| |
| function updateFacings() { |
| const towardBossFromPlayer = boss.x >= player.x ? 1 : -1; |
| if (player.state === "run") { |
| const left = keys["ArrowLeft"] || keys["a"] || keys["A"]; |
| const right = keys["ArrowRight"] || keys["d"] || keys["D"]; |
| if (right && !left) player.facing = 1; |
| else if (left && !right) player.facing = -1; |
| } else if (player.state === "roll") { |
| player.facing = player.rollDir; |
| } else if (player.state !== "dead") { |
| player.facing = towardBossFromPlayer; |
| } |
|
|
| |
| |
| if (boss.state !== "dead") boss.facing = player.x >= boss.x ? 1 : -1; |
| } |
| function clampX(o) { o.x = Math.max(40, Math.min(W - 40, o.x)); } |
| function enforceGap() { |
| |
| |
| if (player.state === "roll") return; |
| if (dist() < CFG.MIN_GAP) { |
| if (player.x < boss.x) player.x = boss.x - CFG.MIN_GAP; else player.x = boss.x + CFG.MIN_GAP; |
| clampX(player); |
| } |
| } |
| const phase = () => (boss.hp < CFG.BOSS_HP * 0.5 ? 2 : 1); |
|
|
| |
| function playerUpdate(dt) { |
| if (player.shield > 0) player.shield = Math.max(0, player.shield - dt); |
| if (player.shieldFlash > 0) player.shieldFlash = Math.max(0, player.shieldFlash - dt); |
| if (player.state === "dead") { player.anim.set("death", false); return; } |
| player.stam = Math.min(CFG.STAM_MAX, player.stam + CFG.STAM_REGEN * dt); |
|
|
| |
| if (player.state === "roll") { |
| player.t += dt; player.x += player.rollDir * CFG.ROLL_SPEED * dt; clampX(player); enforceGap(); |
| if (player.t >= CFG.ROLL_DUR) { player.state = "idle"; } |
| return; |
| } |
| if (player.state === "attack") { |
| player.t += dt; |
| const a0 = CFG.PATK_WINDUP, a1 = CFG.PATK_WINDUP + CFG.PATK_ACTIVE; |
| if (player.t >= a0 && player.t < a1 && !player.atkHit) { |
| if (dist() <= CFG.PATK_REACH) { hitBoss(CFG.PATK_DMG); player.atkHit = true; } |
| } |
| if (player.t >= a1 + CFG.PATK_RECOVER) player.state = "idle"; |
| return; |
| } |
| if (player.state === "stagger") { player.t += dt; if (player.t >= CFG.STAGGER) player.state = "idle"; return; } |
| if (player.state === "hit") { player.t += dt; if (player.anim.done) player.state = "idle"; } |
|
|
| |
| let moving = false; |
| const left = keys["ArrowLeft"] || keys["a"] || keys["A"]; |
| const right = keys["ArrowRight"] || keys["d"] || keys["D"]; |
| const blocking = keys["k"] || keys["K"]; |
| if (blocking) { player.state = "block"; } |
| else if (player.state === "block") { player.state = "idle"; } |
|
|
| if (player.state !== "block") { |
| if (left) { player.x -= CFG.PLAYER_MOVE * dt; moving = true; } |
| if (right) { player.x += CFG.PLAYER_MOVE * dt; moving = true; } |
| clampX(player); enforceGap(); |
| player.state = moving ? "run" : "idle"; |
| } |
| } |
| function playerAttack() { |
| if (["attack", "roll", "stagger", "hit", "dead"].includes(player.state)) return; |
| if (player.stam < CFG.STAM_ATK) return; |
| player.state = "attack"; player.t = 0; player.atkHit = false; player.stam -= CFG.STAM_ATK; |
| playSfx("p_attack", 0.5); |
| } |
| function playerRoll() { |
| if (["roll", "stagger", "hit", "dead"].includes(player.state)) return; |
| if (player.stam < CFG.STAM_ROLL) return; |
| player.state = "roll"; player.t = 0; player.stam -= CFG.STAM_ROLL; |
| playSfx("p_roll", 0.5); |
| player.rollDir = -player.facing; |
| if (keys["ArrowLeft"] || keys["a"]) player.rollDir = -1; |
| if (keys["ArrowRight"] || keys["d"]) player.rollDir = 1; |
| } |
| function playerIframe() { |
| return player.state === "roll" && player.t >= CFG.ROLL_IFR_A && player.t <= CFG.ROLL_IFR_B; |
| } |
| function playerRecovering() { |
| if (player.state === "stagger") return true; |
| if (player.state === "attack" && player.t >= CFG.PATK_WINDUP + CFG.PATK_ACTIVE) return true; |
| if (player.state === "roll" && player.t > CFG.ROLL_IFR_B) return true; |
| return false; |
| } |
| function setPlayerAnim() { |
| const s = player.state; |
| if (s === "dead") player.anim.set("death", false); |
| else if (s === "hit") player.anim.set("take_hit", false); |
| else if (s === "stagger") player.anim.set("take_hit", false); |
| else if (s === "attack") player.anim.set("attack", false, 1.25); |
| else if (s === "roll") player.anim.set("roll", false, 1.1); |
| else if (s === "block") player.anim.set("defend", true); |
| else if (s === "run") player.anim.set("run", true); |
| else player.anim.set("idle", true); |
| } |
|
|
| |
| function hitBoss(dmg) { |
| if (boss.state === "dead") return; |
| |
| if (boss.state === "block") { |
| boss.blockFlash = 0.18; playSfx("b_block", 0.5); |
| boss.hp = Math.max(0, boss.hp - dmg * CFG.BOSS_BLOCK_MIT); |
| return; |
| } |
| const wasPhase1 = boss.hp >= CFG.BOSS_HP * 0.5; |
| boss.hp = Math.max(0, boss.hp - dmg); |
| if (boss.hp <= 0) { boss.state = "dead"; boss.anim.set("death", false); playSfx("b_die", 0.8); return; } |
| playSfx("b_hurt", 0.5); |
| if (wasPhase1 && boss.hp < CFG.BOSS_HP * 0.5) playSfx("roar", 0.8); |
| |
| if (boss.state !== "cleave") { boss.state = "hit"; boss.t = 0; boss.anim.set("take_hit", false); } |
| } |
| function hitPlayer(dmg) { |
| if (player.state === "dead" || playerIframe()) return; |
| if (player.shield > 0) { player.shieldFlash = 0.22; playSfx("shield", 0.5); return; } |
| let d = dmg; |
| if (player.state === "block") { |
| player.stam -= CFG.STAM_BLOCK; |
| if (player.stam <= 0) { player.stam = 0; player.state = "stagger"; player.t = 0; } |
| else { d *= CFG.BLOCK_MIT; } |
| } |
| player.hp = Math.max(0, player.hp - d); |
| if (player.hp <= 0) { player.state = "dead"; player.t = 0; player.anim.set("death", false); playSfx("p_die", 0.8); return; } |
| playSfx("p_hurt", 0.5); |
| if (player.state !== "stagger" && player.state !== "block") { player.state = "hit"; player.t = 0; } |
| } |
|
|
| function bossState() { |
| return { |
| arenaW: W, cleaveReach: CFG.CLEAVE_REACH, |
| bossX: boss.x, playerX: player.x, |
| bossHP: boss.hp / CFG.BOSS_HP, playerHP: player.hp / CFG.PLAYER_HP, |
| bossCooldown: boss.cd > 0 ? boss.cd : 0, |
| difficulty: difficulty, |
| playerAttacking: player.state === "attack", |
| playerApproaching: (player.state === "run") && (Math.sign(player.facing) === Math.sign(boss.x - player.x)), |
| playerRecovering: playerRecovering(), |
| playerBlocking: player.state === "block", |
| playerThreat: player.state === "attack" && dist() <= CFG.PATK_REACH + 12, |
| }; |
| } |
|
|
| async function requestDecision() { |
| boss.pending = true; |
| const st = bossState(); |
| let tel; |
| try { tel = await window.MM_DECIDE(st); } |
| catch (e) { tel = fallbackBrain(st); } |
| boss.pending = false; |
| if (boss.state === "dead" || game !== "fight") return; |
| boss.telemetry = tel; lastDecision = tel; |
| applyBossAction(tel.action); |
| |
| fightLog.push({ state: st, action: tel.action, bossHP: st.bossHP, playerHP: st.playerHP }); |
| updatePanel(tel); |
| } |
| function applyBossAction(action) { |
| boss.action = action; boss.actT = 0; |
| if (action === "CLEAVE" && boss.cd <= 0) { startCleave(); } |
| else if (action === "APPROACH") { boss.state = "approach"; } |
| else if (action === "RETREAT") { boss.state = "retreat"; } |
| else if (action === "BLOCK") { startBlock(); } |
| else { boss.state = "idle"; } |
| } |
| function startCleave() { |
| boss.state = "cleave"; boss.cleavePhase = "windup"; boss.t = 0; boss.cleaveHit = false; |
| const sp = phase() === 2 ? 1.25 : 1.0; |
| boss.anim.set("cleave", false, sp); |
| playSfx("b_cleave", 0.6); |
| } |
| function startBlock() { |
| boss.state = "block"; boss.t = 0; boss.blockFlash = 0; |
| boss.anim.set("idle", true); |
| playSfx("b_block", 0.35); |
| } |
|
|
| function bossUpdate(dt) { |
| if (boss.cd > 0) boss.cd -= dt; |
| if (boss.state === "dead") { boss.anim.set("death", false); return; } |
| const ph = phase(); |
| const moveMul = ph === 2 ? 1.18 : 1.0; |
|
|
| if (boss.state === "hit") { |
| boss.t += dt; if (boss.anim.done) { boss.state = "idle"; boss.think = CFG.THINK_MIN; } |
| boss.anim.set("take_hit", false); return; |
| } |
| if (boss.state === "block") { |
| boss.t += dt; if (boss.blockFlash > 0) boss.blockFlash -= dt; |
| boss.anim.set("idle", true); |
| if (boss.t >= CFG.BOSS_BLOCK_TIME) { boss.state = "idle"; boss.actT = 0; boss.think = CFG.THINK_MIN; } |
| return; |
| } |
| if (boss.state === "cleave") { cleaveUpdate(dt, ph); return; } |
|
|
| if (boss.state === "approach" || boss.state === "retreat") { |
| boss.actT += dt; |
| const sign = boss.state === "approach" ? 1 : -1; |
| const toward = player.x > boss.x ? 1 : -1; |
| boss.x += sign * toward * CFG.BOSS_MOVE * moveMul * dt; |
| clampX(boss); enforceGap(); |
| |
| |
| const backstep = boss.state === "retreat"; |
| boss.anim.set("walk", true, backstep ? 1.0 : 1.1, backstep); |
| if (boss.actT >= CFG.COMMIT_MOVE) tryDecide(); |
| return; |
| } |
|
|
| |
| boss.anim.set("idle", true); |
| boss.actT += dt; |
| if (boss.actT >= CFG.COMMIT_IDLE) tryDecide(); |
| } |
| function tryDecide() { |
| if (boss.pending) return; |
| if (boss.think > 0) { boss.think -= 1 / 60; return; } |
| requestDecision(); |
| boss.think = CFG.THINK_MIN; |
| } |
| function cleaveUpdate(dt, ph) { |
| boss.t += dt; |
| const spMul = ph === 2 ? 1.25 : 1.0; |
| const wu = CFG.CLEAVE_WINDUP / spMul, ac = CFG.CLEAVE_ACTIVE / spMul, rc = CFG.CLEAVE_RECOVER / spMul; |
| if (boss.t < wu) { boss.cleavePhase = "windup"; } |
| else if (boss.t < wu + ac) { |
| boss.cleavePhase = "active"; |
| const toward = player.x > boss.x ? 1 : -1; |
| boss.x += toward * CFG.CLEAVE_LUNGE * spMul * dt; clampX(boss); enforceGap(); |
| if (!boss.cleaveHit && dist() <= CFG.CLEAVE_REACH && !playerIframe()) { |
| hitPlayer(CFG.CLEAVE_DMG); boss.cleaveHit = true; |
| } |
| } else if (boss.t < wu + ac + rc) { boss.cleavePhase = "recover"; } |
| else { |
| boss.cd = CFG.CLEAVE_COOLDOWN; boss.state = "idle"; boss.actT = 0; |
| boss.think = CFG.THINK_MIN; |
| } |
| } |
|
|
| |
| function fallbackBrain(s) { |
| const inRange = Math.abs(s.playerX - s.bossX) <= s.cleaveReach; |
| const ready = !(s.bossCooldown > 0); |
| let action = "IDLE"; |
| if (inRange && ready) action = "CLEAVE"; |
| else if (!inRange) action = "APPROACH"; |
| else if (inRange && !ready) action = "RETREAT"; |
| const mk = a => ({ name: a, owns: null, color: "#888", drive: 0, latent_norm: 0 }); |
| return { action, phase: s.bossHP < 0.5 ? 2 : 1, trained: false, fallback: true, |
| specialists: [], base_drive: {}, modulation: {}, final_drive: {}, probs: {}, |
| legal: {}, shared_latent: [] }; |
| } |
|
|
| |
| function drawArena(ctx) { |
| |
| const g = ctx.createLinearGradient(0, 0, 0, Hgt); |
| g.addColorStop(0, "#241726"); g.addColorStop(0.6, "#160f1c"); g.addColorStop(1, "#0a0710"); |
| ctx.fillStyle = g; ctx.fillRect(0, 0, W, Hgt); |
| |
| ctx.fillStyle = "rgba(40,30,52,.6)"; |
| for (let i = 0; i < 6; i++) { const x = 60 + i * 150; ctx.fillRect(x, 120, 46, GROUND - 120); } |
| |
| ctx.fillStyle = "#0c0a12"; ctx.fillRect(0, GROUND, W, Hgt - GROUND); |
| ctx.fillStyle = "rgba(202,161,90,.10)"; ctx.fillRect(0, GROUND, W, 3); |
| |
| ctx.fillStyle = "rgba(160,27,27,.05)"; ctx.fillRect(0, GROUND - 40, W, 40); |
| } |
| function drawShadow(ctx, x, w) { |
| ctx.save(); ctx.globalAlpha = .35; ctx.fillStyle = "#000"; |
| ctx.beginPath(); ctx.ellipse(x, GROUND + 2, w, 7, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); |
| } |
| function drawTelegraph(ctx) { |
| if (boss.state === "cleave" && boss.cleavePhase === "windup") { |
| const reach = CFG.CLEAVE_REACH; |
| ctx.save(); |
| ctx.fillStyle = "rgba(210,59,47,.18)"; |
| const x0 = boss.facing > 0 ? boss.x : boss.x - reach; |
| ctx.fillRect(x0, GROUND - 90, reach, 90); |
| ctx.strokeStyle = "rgba(210,59,47,.55)"; ctx.lineWidth = 2; ctx.strokeRect(x0, GROUND - 90, reach, 90); |
| ctx.restore(); |
| } |
| } |
|
|
| function drawShield(ctx) { |
| if (player.shield <= 0) return; |
| const frac = player.shield / CFG.SHIELD_TIME; |
| const cx = player.x, cy = GROUND - 52; |
| const pulse = 1 + 0.05 * Math.sin(performance.now() / 140); |
| const rx = 46 * pulse, ry = 64 * pulse; |
| ctx.save(); |
| |
| ctx.globalAlpha = 0.10 + 0.16 * frac + (player.shieldFlash > 0 ? 0.4 : 0); |
| ctx.fillStyle = "#5fd9ff"; |
| ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.fill(); |
| |
| ctx.globalAlpha = 0.5 + 0.4 * frac + (player.shieldFlash > 0 ? 0.4 : 0); |
| ctx.lineWidth = 2; ctx.strokeStyle = "#bff0ff"; |
| ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.stroke(); |
| ctx.restore(); |
| } |
|
|
| function drawBossGuard(ctx) { |
| if (boss.state !== "block") return; |
| const cx = boss.x + boss.facing * 52, cy = GROUND - 70; |
| const flash = boss.blockFlash > 0 ? 0.5 : 0; |
| ctx.save(); |
| |
| ctx.translate(cx, cy); |
| ctx.strokeStyle = "#ff7a3c"; ctx.fillStyle = "rgba(255,120,60," + (0.12 + flash) + ")"; |
| ctx.lineWidth = 2.5; |
| ctx.beginPath(); |
| for (let i = 0; i < 6; i++) { |
| const a = Math.PI / 2 + i * Math.PI / 3; |
| const px = Math.cos(a) * 26 * boss.facing, py = Math.sin(a) * 56; |
| i ? ctx.lineTo(px, py) : ctx.moveTo(px, py); |
| } |
| ctx.closePath(); ctx.fill(); |
| ctx.globalAlpha = 0.7 + flash; ctx.stroke(); |
| ctx.restore(); |
| } |
|
|
| function render() { |
| const ctx = window.__mmctx; |
| drawArena(ctx); |
| drawTelegraph(ctx); |
| drawShadow(ctx, boss.x, 64); drawShadow(ctx, player.x, 34); |
| |
| boss.anim.draw(ctx, boss.x, boss.facing, boss.scale); |
| drawBossGuard(ctx); |
| player.anim.draw(ctx, player.x, player.facing, player.scale); |
| drawShield(ctx); |
| } |
|
|
| |
| function setW(id, frac) { const e = document.getElementById(id); if (e) e.style.width = (Math.max(0, Math.min(1, frac)) * 100) + "%"; } |
| function updateHUD() { |
| setW("mm-hp-boss", boss.hp / CFG.BOSS_HP); |
| setW("mm-hp-php", player.hp / CFG.PLAYER_HP); |
| setW("mm-stam", player.stam / CFG.STAM_MAX); |
| const row = document.getElementById("mm-shield-row"); |
| if (row) { |
| setW("mm-shield", player.shield / CFG.SHIELD_TIME); |
| row.style.opacity = player.shield > 0 ? "1" : "0.25"; |
| } |
| } |
|
|
| const ACTIONS = ["IDLE", "APPROACH", "RETREAT", "CLEAVE", "BLOCK"]; |
| function updatePanel(t) { |
| const dec = document.getElementById("mm-act"); if (dec) dec.textContent = t.action; |
| const ph = document.getElementById("mm-phase"); |
| if (ph) { ph.textContent = "PHASE " + t.phase; ph.className = "mm-phase p" + t.phase; } |
| |
| const wrap = document.getElementById("mm-specs"); if (!wrap) return; |
| let maxDrive = 0.001; |
| (t.specialists || []).forEach(s => { if (s.drive != null) maxDrive = Math.max(maxDrive, Math.abs(s.drive)); }); |
| let html = ""; |
| (t.specialists || []).forEach(s => { |
| const isMod = s.owns === null; |
| const winning = (s.owns === t.action); |
| const val = isMod ? s.latent_norm : s.drive; |
| const norm = isMod ? Math.min(1, (s.latent_norm || 0) / 3) : Math.max(0, (s.drive || 0) / maxDrive); |
| html += `<div class="mm-spec ${winning ? "win" : ""}"> |
| <div class="top"><span class="nm"><span class="dot" style="background:${s.color}"></span>${s.name}` |
| + (isMod ? `<span class="mod-tag">latent-only</span>` : `<span class="owns">→ ${s.owns}</span>`) |
| + `</span><span class="val">${val == null ? "" : (isMod ? "‖z‖ " : "") + val.toFixed(2)}</span></div> |
| <div class="track"><i style="width:${(norm * 100).toFixed(0)}%;background:${s.color}"></i></div></div>`; |
| }); |
| wrap.innerHTML = html; |
| |
| const lat = document.getElementById("mm-latent"); |
| if (lat && t.shared_latent) { |
| const mx = Math.max(0.5, ...t.shared_latent.map(Math.abs)); |
| lat.innerHTML = t.shared_latent.map(v => |
| `<i style="height:${(Math.abs(v) / mx * 100).toFixed(0)}%;opacity:${v < 0 ? .45 : 1}"></i>`).join(""); |
| } |
| const flow = document.getElementById("mm-flow"); |
| if (flow) { |
| const mod = t.modulation || {}; |
| const cl = mod.CLEAVE || 0, ap = mod.APPROACH || 0; |
| flow.innerHTML = t.fallback |
| ? `<b>offline fallback</b> — Python brain unreachable, using heuristic.` |
| : `RecursiveLink → coordinator modulation: <b>CLEAVE ${cl >= 0 ? "+" : ""}${cl.toFixed(2)}</b>, ` |
| + `APPROACH ${ap >= 0 ? "+" : ""}${ap.toFixed(2)}. Modulators (Punisher/Enrage) act only here.`; |
| } |
| } |
|
|
| |
| let last = 0; |
| function loop(ts) { |
| const dt = Math.min(0.05, (ts - last) / 1000 || 0); last = ts; |
| if (game === "fight") { |
| playerUpdate(dt); bossUpdate(dt); |
| updateFacings(); |
| setPlayerAnim(); |
| player.anim.update(dt); boss.anim.update(dt); |
| updateHUD(); |
| if (player.hp <= 0 && game === "fight") endGame(false); |
| else if (boss.hp <= 0 && game === "fight" && boss.anim.name === "death" && boss.anim.done) endGame(true); |
| else if (boss.hp <= 0 && game === "fight") { } |
| } else { |
| player.anim.update(dt); boss.anim.update(dt); |
| } |
| render(); |
| requestAnimationFrame(loop); |
| } |
|
|
| function endGame(win) { |
| if (win) { game = "win"; } |
| else { game = "dead"; } |
| stopMusic(); |
| |
| if (window.MM_LEARN && fightLog.length > 1) { |
| try { window.MM_LEARN({ difficulty, steps: fightLog, result: { bossDied: win, playerDied: !win } }); } |
| catch (e) { } |
| } |
| showOverlay(win ? "win" : "dead"); |
| } |
|
|
| |
| function showOverlay(kind) { |
| const ov = document.getElementById("mm-overlay"); |
| const big = document.getElementById("mm-big"); |
| const sub = document.getElementById("mm-sub"); |
| const btn = document.getElementById("mm-start-btn"); |
| ov.classList.remove("hidden"); |
| if (kind === "menu") { |
| big.className = ""; big.textContent = "BOSS FIGHT"; |
| sub.innerHTML = `A Modular Mind controls the <b>Demon Slime</b>. Six tiny specialists vote through a shared latent; the trained brain picks each move. You start with a brief <b style="color:#bff0ff">Aegis shield</b> — a few seconds to learn the controls before it fades. Defeat the boss.`; |
| btn.textContent = "Enter the Fog"; |
| } else if (kind === "dead") { |
| big.className = "died"; big.textContent = "YOU DIED"; |
| sub.innerHTML = `The Modular Mind read you. Watch the panel — punish its <b>recovery</b>, dodge the <b>red telegraph</b>.`; |
| btn.textContent = "Try Again"; |
| } else { |
| big.className = "win"; big.textContent = "VICTORY"; |
| sub.innerHTML = `Demon Slime felled. You out-played a brain trained by self-play reinforcement learning.`; |
| btn.textContent = "Fight Again"; |
| } |
| } |
| function startFight() { |
| boss.x = 250; boss.hp = CFG.BOSS_HP; boss.state = "idle"; boss.cd = 0; boss.t = 0; |
| boss.actT = 0; boss.pending = false; boss.action = "IDLE"; boss.think = 0; |
| player.x = 650; player.hp = CFG.PLAYER_HP; player.stam = CFG.STAM_MAX; player.state = "idle"; player.t = 0; |
| player.shield = CFG.SHIELD_TIME; player.shieldFlash = 0; |
| fightLog = []; |
| boss.anim.set("idle", true); player.anim.set("idle", true); |
| document.getElementById("mm-overlay").classList.add("hidden"); |
| game = "fight"; |
| initAudio(); playSfx("ui", 0.4); playSfx("roar", 0.7); startMusic(); |
| } |
|
|
| |
| function bindInput() { |
| document.addEventListener("keydown", e => { |
| if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", " "].includes(e.key)) e.preventDefault(); |
| keys[e.key] = true; |
| if (e.key === "m" || e.key === "M") toggleMute(); |
| if (game !== "fight") return; |
| if (e.key === " ") playerRoll(); |
| if (e.key === "j" || e.key === "J") playerAttack(); |
| }, { passive: false }); |
| document.addEventListener("keyup", e => { keys[e.key] = false; }); |
| document.getElementById("mm-start-btn").addEventListener("click", startFight); |
| document.getElementById("mm-overlay").addEventListener("click", e => { |
| if (e.target.closest(".mm-diff")) return; |
| if (e.target.id === "mm-start-btn") return; |
| if (game !== "fight") startFight(); |
| }); |
| const mute = document.getElementById("mm-mute"); |
| if (mute) mute.addEventListener("click", e => { e.stopPropagation(); toggleMute(); }); |
| |
| document.querySelectorAll(".mm-diff-btn").forEach(btn => { |
| btn.addEventListener("click", e => { |
| e.stopPropagation(); |
| difficulty = btn.dataset.diff; |
| document.querySelectorAll(".mm-diff-btn").forEach(b => b.classList.toggle("mm-diff-on", b === btn)); |
| const tag = document.getElementById("mm-difftag"); |
| if (tag) tag.textContent = difficulty.toUpperCase(); |
| }); |
| }); |
| } |
|
|
| |
| async function boot() { |
| imgs.boss = await loadImg(A.boss.image); |
| imgs.knight = await loadImg(A.knight.image); |
| boss.anim = new Anim("boss"); player.anim = new Anim("knight"); |
| const cv = document.getElementById("mm-canvas"); |
| cv.width = W; cv.height = Hgt; window.__mmctx = cv.getContext("2d"); |
| window.__mmctx.imageSmoothingEnabled = false; |
| bindInput(); |
| showOverlay("menu"); |
| |
| window.__mmDebug = { |
| get boss() { return boss; }, get player() { return player; }, |
| get game() { return game; }, get keys() { return keys; }, |
| hurtBoss: d => hitBoss(d), hurtPlayer: d => hitPlayer(d), faces: () => updateFacings(), |
| }; |
| requestAnimationFrame(loop); |
| } |
| window.__mmBoot = boot; |
| })(); |
|
|