import {
  resolveTheQuestLocationMeta,
  useTheQuestStore,
  type EnemyState,
} from './theQuestStore';
import { THE_QUEST_STAGE_PORTAL, THE_QUEST_WORLD_SIZE } from './theQuestData';

type CreateTheQuestGameParams = {
  mountElement: HTMLDivElement;
};

type GameHandle = {
  destroy: () => void;
  requestManualSkillCast: (skillId: string) => void;
};

type EnemySpriteState = {
  body: import('phaser').GameObjects.Container;
  label: import('phaser').GameObjects.Text;
  hpBar: import('phaser').GameObjects.Graphics;
  flash: import('phaser').GameObjects.Ellipse;
  aura: import('phaser').GameObjects.Ellipse;
  core: import('phaser').GameObjects.Arc;
  shadow: import('phaser').GameObjects.Ellipse;
  baseX: number;
  baseY: number;
  patrolTimeOffset: number;
  attackCooldownUntil: number;
};

type PortalState = {
  ring: import('phaser').GameObjects.Ellipse;
  core: import('phaser').GameObjects.Ellipse;
  glow: import('phaser').GameObjects.Ellipse;
  label: import('phaser').GameObjects.Text;
};

type PerformanceProfile = {
  isMobile: boolean;
  lowEffects: boolean;
  treeCount: number;
  shrubCount: number;
  crystalCount: number;
  lanternCount: number;
};

const PASSIVE_HP_RECOVERY_PER_SECOND = 0.9;
const PASSIVE_MP_RECOVERY_PER_SECOND = 0.6;
const AUTO_COMBAT_MP_RESERVE = 38;
const AUTO_COMBAT_BASIC_ATTACK_RANGE = 236;
const AUTO_COMBAT_SPELL_GAP_MS = 980;

function normalizeVector(x: number, y: number) {
  const length = Math.hypot(x, y);
  if (!length) {
    return { x: 0, y: 0, length: 0 };
  }

  return {
    x: x / length,
    y: y / length,
    length,
  };
}

function clamp(value: number, min: number, max: number) {
  return Math.min(Math.max(value, min), max);
}

function createPerformanceProfile(width: number): PerformanceProfile {
  const isMobile = width <= 768;
  const lowEffects = width <= 540;
  return {
    isMobile,
    lowEffects,
    treeCount: lowEffects ? 34 : isMobile ? 52 : 80,
    shrubCount: lowEffects ? 48 : isMobile ? 72 : 110,
    crystalCount: lowEffects ? 10 : isMobile ? 14 : 22,
    lanternCount: lowEffects ? 12 : isMobile ? 16 : 24,
  };
}

function shouldPreferCanvasRenderer() {
  if (typeof window === 'undefined') {
    return false;
  }

  const isCoarsePointer = window.matchMedia?.('(pointer: coarse)').matches ?? false;
  const deviceMemory = typeof navigator !== 'undefined' && typeof navigator.deviceMemory === 'number' ? navigator.deviceMemory : null;
  return window.innerWidth <= 768 || isCoarsePointer || (deviceMemory !== null && deviceMemory <= 4);
}

function blendTint(baseTint: number, highlightTint: number, ratio: number) {
  const blend = clamp(ratio, 0, 1);
  const baseR = (baseTint >> 16) & 0xff;
  const baseG = (baseTint >> 8) & 0xff;
  const baseB = baseTint & 0xff;
  const highR = (highlightTint >> 16) & 0xff;
  const highG = (highlightTint >> 8) & 0xff;
  const highB = highlightTint & 0xff;
  const r = Math.round(baseR + (highR - baseR) * blend);
  const g = Math.round(baseG + (highG - baseG) * blend);
  const b = Math.round(baseB + (highB - baseB) * blend);
  return (r << 16) | (g << 8) | b;
}

function getEnemyLabelOffset(role: EnemyState['role']) {
  return role === 'boss' ? 74 : 58;
}

function createPlayerSprite(scene: import('phaser').Scene, x: number, y: number) {
  const shadow = scene.add.ellipse(0, 42, 72, 24, 0x000000, 0.32).setName('player-shadow');
  const aura = scene.add.circle(0, 10, 38, 0x88dfff, 0.2).setName('player-aura').setBlendMode('ADD');
  const auraOuter = scene.add.ellipse(0, 10, 108, 46, 0x78c6ff, 0.1).setName('player-aura-outer').setBlendMode('ADD');
  const auraRing = scene.add.ellipse(0, 10, 86, 34, 0xe2f5ff, 0.05).setName('player-aura-ring').setStrokeStyle(3, 0xe7fbff, 0.4);
  auraRing.setBlendMode('ADD');
  const auraFloor = scene.add.ellipse(0, 28, 96, 24, 0x9bd8ff, 0.06).setBlendMode('ADD');
  const robeBack = scene.add.triangle(0, 18, -24, -6, 24, -6, 0, 58, 0x13263f, 0.98);
  const cape = scene.add.triangle(-8, 18, -8, -6, 28, 4, 2, 56, 0x243b59, 0.92).setRotation(-0.24);
  const capeGlow = scene.add.ellipse(-2, 22, 54, 62, 0x72c8ff, 0.08).setBlendMode('ADD');
  const robeHem = scene.add.ellipse(0, 26, 42, 24, 0x19365b, 1).setStrokeStyle(3, 0x86d9ff, 0.18);
  const hipGuardLeft = scene.add.triangle(-13, 22, 0, 0, 9, 2, 5, 18, 0x2e537e, 0.96).setRotation(-0.1);
  const hipGuardRight = scene.add.triangle(13, 22, 0, 0, 9, 2, 5, 18, 0x2e537e, 0.96).setRotation(0.1);
  const pauldronLeft = scene.add.ellipse(-16, -4, 18, 14, 0xd7c889, 0.94).setStrokeStyle(2, 0x4a3518, 0.34).setRotation(-0.28);
  const pauldronRight = scene.add.ellipse(16, -4, 18, 14, 0xd7c889, 0.94).setStrokeStyle(2, 0x4a3518, 0.34).setRotation(0.28);
  const body = scene.add.rectangle(0, 4, 36, 38, 0x29507f, 1).setStrokeStyle(2, 0xe7d8a4, 0.24);
  const robeFront = scene.add.triangle(0, 17, -16, -4, 16, -4, 0, 42, 0x24436d, 0.98);
  const sash = scene.add.rectangle(0, 12, 28, 6, 0x6a4a2a, 0.96);
  const sashGem = scene.add.circle(0, 12, 4, 0xaee7ff, 0.8);
  const runeCore = scene.add.circle(0, 2, 7, 0xaee7ff, 0.5).setName('player-rune-core').setBlendMode('ADD');
  const runeHalo = scene.add.circle(0, 2, 14, 0xeaf9ff, 0.1).setName('player-rune-halo').setBlendMode('ADD');
  const chestTrim = scene.add.rectangle(0, 0, 14, 22, 0xc4b06d, 0.9);
  const trimGem = scene.add.circle(0, 6, 4, 0xe6f8ff, 0.95);
  const armLeft = scene.add.rectangle(-18, 8, 10, 30, 0x29507f, 1).setRotation(-0.2);
  const armRight = scene.add.rectangle(18, 8, 10, 30, 0x29507f, 1).setRotation(0.28);
  const handLeft = scene.add.circle(-21, 20, 4, 0xf3c89a, 1);
  const handRight = scene.add.circle(23, 18, 4, 0xf3c89a, 1);
  const bootLeft = scene.add.rectangle(-8, 42, 12, 16, 0x1a232f, 1).setRotation(-0.04);
  const bootRight = scene.add.rectangle(8, 42, 12, 16, 0x1a232f, 1).setRotation(0.04);
  const head = scene.add.ellipse(0, -26, 28, 26, 0xf3c89a, 1).setStrokeStyle(2, 0x2f1a10, 0.24);
  const hairBack = scene.add.ellipse(0, -31, 30, 18, 0x4b2d19, 1);
  const hood = scene.add.arc(0, -35, 22, 180, 360, false, 0x22385d, 0.98);
  const hair = scene.add.rectangle(0, -34, 22, 8, 0x2f1e14, 0.94);
  const bangs = scene.add.rectangle(-2, -29, 18, 6, 0x24140c, 0.94);
  const eyeLeft = scene.add.rectangle(-5, -25, 3, 2, 0x1c1209, 1);
  const eyeRight = scene.add.rectangle(5, -25, 3, 2, 0x1c1209, 1);
  const faceLight = scene.add.circle(8, -28, 4, 0xfff1d7, 0.46);
  const crownRing = scene.add.ellipse(0, -46, 22, 8, 0xeff9ff, 0.08).setStrokeStyle(2, 0xcaf0ff, 0.52).setBlendMode('ADD');
  const staffShaft = scene.add
    .rectangle(28, 8, 6, 58, 0x6d4b2b, 1)
    .setName('blade')
    .setRotation(0.3)
    .setStrokeStyle(2, 0x25170f, 0.28);
  const staffHeadGlow = scene.add.circle(22, -22, 14, 0x91d9ff, 0.28).setName('staff-glow').setBlendMode('ADD');
  const staffHeadRing = scene.add.circle(22, -22, 10, 0xbedbff, 0.12).setStrokeStyle(3, 0xe9fbff, 0.8).setRotation(0.24);
  const staffWingLeft = scene.add.triangle(14, -22, 0, 0, 10, 6, 10, -6, 0xd9f3ff, 0.88).setRotation(-0.34);
  const staffWingRight = scene.add.triangle(30, -22, 0, 0, 10, 6, 10, -6, 0xd9f3ff, 0.88).setRotation(0.34);
  const staffGem = scene.add.circle(22, -22, 5, 0xf5fcff, 0.95);
  const staffSparkLeft = scene.add.circle(15, -27, 2.4, 0xd7f8ff, 0.8);
  const staffSparkRight = scene.add.circle(29, -17, 2.1, 0xd7f8ff, 0.84);
  const highlight = scene.add.circle(8, -28, 2.6, 0xffffff, 0.8);

  const container = scene.add.container(x, y, [
    shadow,
    aura,
    auraOuter,
    auraRing,
    auraFloor,
    robeBack,
    cape,
    capeGlow,
    robeHem,
    hipGuardLeft,
    hipGuardRight,
    pauldronLeft,
    pauldronRight,
    body,
    robeFront,
    sash,
    sashGem,
    runeCore,
    runeHalo,
    chestTrim,
    trimGem,
    armLeft,
    armRight,
    handLeft,
    handRight,
    bootLeft,
    bootRight,
    head,
    hairBack,
    hood,
    hair,
    bangs,
    eyeLeft,
    eyeRight,
    faceLight,
    crownRing,
    staffHeadGlow,
    staffShaft,
    staffHeadRing,
    staffWingLeft,
    staffWingRight,
    staffGem,
    staffSparkLeft,
    staffSparkRight,
    highlight,
  ]);
  container.setScale(1.08);
  container.setSize(92, 112);
  container.setDepth(10);
  scene.physics.add.existing(container);
  const physicsBody = container.body as import('phaser').Physics.Arcade.Body;
  physicsBody.setCircle(18, -18, 18);
  physicsBody.setCollideWorldBounds(true);

  return container;
}

function createEnemySprite(scene: import('phaser').Scene, enemy: EnemyState): EnemySpriteState {
  const isBoss = enemy.role === 'boss';
  const shadow = scene.add.ellipse(0, isBoss ? 30 : 22, isBoss ? 82 : 60, isBoss ? 22 : 16, 0x000000, 0.28);
  const aura = scene.add
    .ellipse(0, isBoss ? 10 : 6, isBoss ? 108 : 78, isBoss ? 76 : 50, blendTint(enemy.tint, 0xffffff, 0.12), 0.18)
    .setBlendMode('ADD');
  const auraBack = scene.add
    .ellipse(0, isBoss ? 4 : 2, isBoss ? 78 : 52, isBoss ? 52 : 34, blendTint(enemy.tint, 0xffffff, 0.2), 0.14)
    .setBlendMode('ADD');
  const smokeHalo = scene.add
    .ellipse(0, isBoss ? 4 : 1, isBoss ? 92 : 58, isBoss ? 64 : 40, blendTint(enemy.tint, 0x0f1720, 0.22), 0.16)
    .setBlendMode('MULTIPLY');
  const bodyBottom = scene.add
    .ellipse(0, isBoss ? 12 : 7, isBoss ? 68 : 46, isBoss ? 46 : 30, enemy.tint, 1)
    .setStrokeStyle(isBoss ? 3 : 2, isBoss ? 0xffe0ad : 0xffffff, isBoss ? 0.28 : 0.12);
  const bodyMid = scene.add.ellipse(0, isBoss ? 2 : 0, isBoss ? 60 : 40, isBoss ? 40 : 26, blendTint(enemy.tint, 0xffffff, 0.14), 0.88);
  const bodyTop = scene.add.ellipse(0, isBoss ? -4 : -5, isBoss ? 52 : 34, isBoss ? 36 : 24, blendTint(enemy.tint, 0xffffff, 0.22), 0.95);
  const jaw = scene.add.ellipse(0, isBoss ? 14 : 10, isBoss ? 40 : 28, isBoss ? 16 : 12, blendTint(enemy.tint, 0x28150f, 0.18), 0.96);
  const ridge = scene.add.ellipse(0, isBoss ? -8 : -7, isBoss ? 28 : 18, isBoss ? 12 : 8, blendTint(enemy.tint, 0xffffff, 0.36), 0.7);
  const cheekLeft = scene.add.circle(-12, 1, isBoss ? 5 : 4, 0xffffff, 0.18);
  const cheekRight = scene.add.circle(12, 1, isBoss ? 5 : 4, 0xffffff, 0.18);
  const shine = scene.add.circle(-10, -10, isBoss ? 10 : 7, 0xffffff, 0.4);
  const core = scene.add.circle(0, isBoss ? 1 : 0, isBoss ? 15 : 10, blendTint(enemy.tint, 0xffffff, 0.52), 0.58);
  const coreHalo = scene.add.circle(0, isBoss ? 1 : 0, isBoss ? 24 : 16, blendTint(enemy.tint, 0xffffff, 0.36), 0.14).setBlendMode('ADD');
  const flash = scene.add.ellipse(0, -1, isBoss ? 76 : 50, isBoss ? 50 : 34, 0xffffff, 0).setBlendMode('ADD');
  const eyeAuraLeft = scene.add.circle(-8, -2, isBoss ? 6 : 5, 0xffffff, 0.1).setBlendMode('ADD');
  const eyeAuraRight = scene.add.circle(8, -2, isBoss ? 6 : 5, 0xffffff, 0.1).setBlendMode('ADD');
  const eyeLeft = scene.add.circle(-8, -2, isBoss ? 3 : 2.4, 0x163047, 1);
  const eyeRight = scene.add.circle(8, -2, isBoss ? 3 : 2.4, 0x163047, 1);
  const browLeft = scene.add.rectangle(-8, -8, isBoss ? 8 : 6, 2, 0x16202c, 0.72).setRotation(-0.2);
  const browRight = scene.add.rectangle(8, -8, isBoss ? 8 : 6, 2, 0x16202c, 0.72).setRotation(0.2);
  const mouth = scene.add.arc(0, isBoss ? 10 : 8, isBoss ? 7 : 5, 10, 170, false, 0x1f3951, 1).setScale(1, 0.6);
  const fangs = scene.add.rectangle(0, isBoss ? 13 : 10, isBoss ? 10 : 8, isBoss ? 6 : 5, 0xfff5e8, 0.92);
  const hornLeft = scene.add.triangle(-18, -16, 0, 0, 12, 0, 6, -14, 0xfdebc3, 0.86).setRotation(-0.24);
  const hornRight = scene.add.triangle(18, -16, 0, 0, 12, 0, 6, -14, 0xfdebc3, 0.86).setRotation(0.24);
  const dorsalSpineLeft = scene.add.triangle(-10, -18, 0, 0, 10, 0, 5, -12, blendTint(enemy.tint, 0xffffff, 0.26), 0.8).setRotation(-0.18);
  const dorsalSpineRight = scene.add.triangle(10, -18, 0, 0, 10, 0, 5, -12, blendTint(enemy.tint, 0xffffff, 0.26), 0.8).setRotation(0.18);
  const finLeft = scene.add.triangle(-20, 6, 0, 0, 14, 4, 6, 18, blendTint(enemy.tint, 0xffffff, 0.2), 0.84).setRotation(-0.34);
  const finRight = scene.add.triangle(20, 6, 0, 0, 14, 4, 6, 18, blendTint(enemy.tint, 0xffffff, 0.2), 0.84).setRotation(0.34);
  const clawLeft = scene.add.triangle(-24, isBoss ? 16 : 12, 0, 0, 10, 4, 5, 15, blendTint(enemy.tint, 0xffffff, 0.14), 0.92).setRotation(-0.48);
  const clawRight = scene.add.triangle(24, isBoss ? 16 : 12, 0, 0, 10, 4, 5, 15, blendTint(enemy.tint, 0xffffff, 0.14), 0.92).setRotation(0.48);
  const tailLeft = scene.add.ellipse(-24, isBoss ? 18 : 12, isBoss ? 16 : 12, isBoss ? 10 : 8, blendTint(enemy.tint, 0x0f1720, 0.22), 0.82).setRotation(-0.34);
  const tailRight = scene.add.ellipse(24, isBoss ? 18 : 12, isBoss ? 16 : 12, isBoss ? 10 : 8, blendTint(enemy.tint, 0x0f1720, 0.22), 0.82).setRotation(0.34);
  const crown = isBoss ? scene.add.star(0, -30, 5, 10, 20, 0xffd36d, 0.9).setStrokeStyle(2, 0x5c2f10, 0.4) : null;
  const container = scene.add.container(enemy.x, enemy.y, [
    shadow,
    aura,
    auraBack,
    smokeHalo,
    hornLeft,
    hornRight,
    dorsalSpineLeft,
    dorsalSpineRight,
    ...(crown ? [crown] : []),
    tailLeft,
    tailRight,
    bodyBottom,
    bodyMid,
    bodyTop,
    jaw,
    ridge,
    cheekLeft,
    cheekRight,
    shine,
    core,
    coreHalo,
    flash,
    eyeAuraLeft,
    eyeAuraRight,
    eyeLeft,
    eyeRight,
    browLeft,
    browRight,
    mouth,
    fangs,
    finLeft,
    finRight,
    clawLeft,
    clawRight,
  ]);
  container.setScale(isBoss ? 1.1 : 1.06);
  container.setSize(isBoss ? 92 : 68, isBoss ? 78 : 54);
  container.setDepth(9);
  scene.physics.add.existing(container);
  const body = container.body as import('phaser').Physics.Arcade.Body;
  body.setCircle(isBoss ? 28 : 20, isBoss ? -18 : -10, isBoss ? 0 : -1);
  body.setCollideWorldBounds(true);

  const label = scene.add.text(enemy.x, enemy.y - getEnemyLabelOffset(enemy.role), `${isBoss ? 'BOSS ' : ''}Lv.${enemy.level} ${enemy.name}`, {
    fontFamily: '"Trebuchet MS", sans-serif',
    fontSize: isBoss ? '14px' : '13px',
    color: '#eff7ff',
    stroke: '#122131',
    strokeThickness: 4,
  });
  label.setOrigin(0.5, 1);
  label.setDepth(12);

  const hpBar = scene.add.graphics().setDepth(12);

  return {
    body: container,
    label,
    hpBar,
    flash,
    aura,
    core,
    shadow,
    baseX: enemy.x,
    baseY: enemy.y,
    patrolTimeOffset: Math.random() * 2000,
    attackCooldownUntil: 0,
  };
}

function createPortalMarker(scene: import('phaser').Scene) {
  const glow = scene.add.ellipse(THE_QUEST_STAGE_PORTAL.x, THE_QUEST_STAGE_PORTAL.y, 170, 170, 0x5da7ff, 0.12).setDepth(6);
  glow.setBlendMode('ADD');
  const ring = scene.add.ellipse(THE_QUEST_STAGE_PORTAL.x, THE_QUEST_STAGE_PORTAL.y, 116, 132, 0x284062, 0.16).setDepth(7);
  ring.setStrokeStyle(5, 0x87c8ff, 0.62);
  const core = scene.add.ellipse(THE_QUEST_STAGE_PORTAL.x, THE_QUEST_STAGE_PORTAL.y, 64, 92, 0x71b6ff, 0.28).setDepth(7);
  core.setStrokeStyle(3, 0xe9f6ff, 0.72);
  core.setBlendMode('ADD');
  const label = scene.add.text(THE_QUEST_STAGE_PORTAL.x, THE_QUEST_STAGE_PORTAL.y + 84, 'PORTAL LOCK', {
    fontFamily: '"Trebuchet MS", sans-serif',
    fontSize: '13px',
    color: '#d4ecff',
    stroke: '#122131',
    strokeThickness: 5,
  });
  label.setOrigin(0.5, 0);
  label.setDepth(8);
  return { ring, core, glow, label } satisfies PortalState;
}

function createNpcMarker(scene: import('phaser').Scene, npc: { x: number; y: number; name: string; role: string }) {
  const halo = scene.add.circle(0, 0, 22, 0xffd372, 0.18);
  const body = scene.add.container(npc.x, npc.y, [
    halo,
    scene.add.rectangle(0, -15, 18, 18, 0xf4d09d, 1).setStrokeStyle(2, 0x2d170d, 0.26),
    scene.add.rectangle(0, 8, 22, 34, npc.role === 'shop' ? 0x58795b : npc.role === 'quest' ? 0x3f5072 : 0x704960, 1)
      .setStrokeStyle(2, 0xffffff, 0.18),
    scene.add.text(0, -43, npc.role === 'quest' ? '!' : npc.role === 'shop' ? '$' : '?', {
      fontFamily: '"Trebuchet MS", sans-serif',
      fontSize: '16px',
      color: '#fff2a6',
      stroke: '#2d2016',
      strokeThickness: 4,
    }).setOrigin(0.5, 0.5),
  ]);
  body.setDepth(8);
  scene.add.text(npc.x, npc.y + 34, npc.name, {
    fontFamily: '"Trebuchet MS", sans-serif',
    fontSize: '12px',
    color: '#eff7ff',
    stroke: '#122131',
    strokeThickness: 4,
  }).setOrigin(0.5, 0);
}

function updateEnemyHpBar(hpBar: import('phaser').GameObjects.Graphics, enemy: EnemyState, sprite: EnemySpriteState) {
  hpBar.clear();
  if (!enemy.alive) {
    return;
  }
  const width = enemy.role === 'boss' ? 126 : 108;
  const hpProgress = clamp(enemy.hp / enemy.maxHp, 0, 1);
  const mpProgress = clamp(enemy.maxMp > 0 ? enemy.mp / enemy.maxMp : 0, 0, 1);
  hpBar.fillStyle(0x182432, 0.9);
  hpBar.fillRoundedRect(sprite.body.x - width / 2, sprite.body.y - (enemy.role === 'boss' ? 74 : 62), width, 10, 4);
  hpBar.fillStyle(0xca3c42, 1);
  hpBar.fillRoundedRect(sprite.body.x - width / 2 + 1, sprite.body.y - (enemy.role === 'boss' ? 73 : 61), (width - 2) * hpProgress, 8, 4);
  hpBar.fillStyle(0x102030, 0.8);
  hpBar.fillRoundedRect(sprite.body.x - width / 2, sprite.body.y - (enemy.role === 'boss' ? 60 : 48), width, 6, 3);
  hpBar.fillStyle(0x73c9ff, 0.95);
  hpBar.fillRoundedRect(sprite.body.x - width / 2 + 1, sprite.body.y - (enemy.role === 'boss' ? 59 : 47), (width - 2) * mpProgress, 4, 3);
}

function createFloatingText(
  scene: import('phaser').Scene,
  x: number,
  y: number,
  value: string,
  color: string,
) {
  const text = scene.add.text(x, y, value, {
    fontFamily: '"Trebuchet MS", sans-serif',
    fontSize: '22px',
    fontStyle: 'bold',
    color,
    stroke: '#1b120d',
    strokeThickness: 5,
  });
  text.setOrigin(0.5, 0.5);
  text.setDepth(30);
  scene.tweens.add({
    targets: text,
    y: y - 44,
    alpha: 0,
    duration: 760,
    ease: 'Cubic.easeOut',
    onComplete: () => text.destroy(),
  });
}

function createCombatCallout(
  scene: import('phaser').Scene,
  x: number,
  y: number,
  primary: string,
  secondary: string | null,
  color: string,
) {
  const text = scene.add.text(x, y, secondary ? `${primary}\n${secondary}` : primary, {
    fontFamily: '"Trebuchet MS", sans-serif',
    fontSize: secondary ? '20px' : '22px',
    fontStyle: 'bold',
    align: 'center',
    color,
    stroke: '#1b120d',
    strokeThickness: 5,
    lineSpacing: -4,
  });
  text.setOrigin(0.5, 0.5);
  text.setDepth(31);
  scene.tweens.add({
    targets: text,
    y: y - 56,
    alpha: 0,
    scale: 1.08,
    duration: 820,
    ease: 'Cubic.easeOut',
    onComplete: () => text.destroy(),
  });
}

function createSlashEffect(scene: import('phaser').Scene, x: number, y: number, rotation: number, color: number) {
  const slash = scene.add.ellipse(x, y, 118, 30, color, 0.48);
  slash.setDepth(20);
  slash.setRotation(rotation);
  slash.setBlendMode('ADD');
  const core = scene.add.ellipse(x, y, 82, 10, 0xfef7e8, 0.78);
  core.setDepth(21);
  core.setRotation(rotation);
  core.setBlendMode('ADD');
  scene.tweens.add({
    targets: [slash, core],
    scaleX: 1.28,
    scaleY: 0.3,
    alpha: 0,
    duration: 240,
    ease: 'Quad.easeOut',
    onComplete: () => {
      slash.destroy();
      core.destroy();
    },
  });
}

function createSlashComboEffect(scene: import('phaser').Scene, x: number, y: number, rotation: number, chainStep: number) {
  const widths = [88, 106, 124];
  const heights = [18, 20, 24];
  const colors = [0xffcb6d, 0xff8f62, 0x8edcff];
  const slash = scene.add.ellipse(x, y, (widths[chainStep] ?? 88) + 24, (heights[chainStep] ?? 18) + 8, colors[chainStep] ?? 0xffcb6d, 0.58);
  slash.setDepth(21);
  slash.setRotation(rotation + (chainStep - 1) * 0.2);
  slash.setBlendMode('ADD');
  const ribbon = scene.add.rectangle(x, y, (widths[chainStep] ?? 88) + 12, 6, 0xfff6d8, 0.82).setDepth(22);
  ribbon.setRotation(rotation + (chainStep - 1) * 0.2);
  ribbon.setBlendMode('ADD');
  scene.tweens.add({
    targets: [slash, ribbon],
    scaleX: 1.38,
    scaleY: 0.16,
    alpha: 0,
    duration: 280,
    ease: 'Cubic.easeOut',
    onComplete: () => {
      slash.destroy();
      ribbon.destroy();
    },
  });
}

function createImpactBurst(scene: import('phaser').Scene, x: number, y: number, color: number) {
  const effectCount = useTheQuestStore.getState().performance.lowEffects ? 6 : 10;
  for (let index = 0; index < effectCount; index += 1) {
    const angle = (Math.PI * 2 * index) / effectCount;
    const particle = scene.add.circle(x, y, 5, color, 0.82);
    particle.setDepth(21);
    scene.tweens.add({
      targets: particle,
      x: x + Math.cos(angle) * 42,
      y: y + Math.sin(angle) * 42,
      alpha: 0,
      scale: 0.2,
      duration: 280,
      ease: 'Quad.easeOut',
      onComplete: () => particle.destroy(),
    });
  }
  createRadialWave(scene, x, y, color, 16, 280);
}

function createSparkBurst(
  scene: import('phaser').Scene,
  x: number,
  y: number,
  color: number,
  count: number,
  distance: number,
) {
  for (let index = 0; index < count; index += 1) {
    const angle = (Math.PI * 2 * index) / count + Math.random() * 0.22;
    const spark = scene.add.rectangle(x, y, 4, 18, color, 0.9).setDepth(22);
    spark.setRotation(angle);
    spark.setBlendMode('ADD');
    scene.tweens.add({
      targets: spark,
      x: x + Math.cos(angle) * (distance * (0.7 + Math.random() * 0.45)),
      y: y + Math.sin(angle) * (distance * (0.7 + Math.random() * 0.45)),
      alpha: 0,
      scaleX: 0.5,
      scaleY: 0.2,
      duration: 280 + Math.random() * 120,
      ease: 'Cubic.easeOut',
      onComplete: () => spark.destroy(),
    });
  }
}

function createGroundSigil(
  scene: import('phaser').Scene,
  x: number,
  y: number,
  color: number,
  radius: number,
  duration: number,
) {
  const sigil = scene.add.circle(x, y, radius, color, 0.12).setDepth(17);
  sigil.setStrokeStyle(4, color, 0.76);
  sigil.setBlendMode('ADD');
  const innerRing = scene.add.circle(x, y, radius * 0.56, color, 0.04).setDepth(18);
  innerRing.setStrokeStyle(2, blendTint(color, 0xffffff, 0.5), 0.9);
  innerRing.setBlendMode('ADD');
  scene.tweens.add({
    targets: [sigil, innerRing],
    scale: 1.28,
    alpha: 0,
    duration,
    ease: 'Quad.easeOut',
    onComplete: () => {
      sigil.destroy();
      innerRing.destroy();
    },
  });
}

function createProjectileTrail(
  scene: import('phaser').Scene,
  fromX: number,
  fromY: number,
  toX: number,
  toY: number,
  color: number,
) {
  const distance = Math.hypot(toX - fromX, toY - fromY);
  const line = scene.add.rectangle((fromX + toX) / 2, (fromY + toY) / 2, distance, 9, color, 0.24);
  line.setRotation(Math.atan2(toY - fromY, toX - fromX));
  line.setDepth(18);
  line.setBlendMode('ADD');
  const core = scene.add.rectangle((fromX + toX) / 2, (fromY + toY) / 2, distance, 3, 0xf8fdff, 0.92);
  core.setRotation(Math.atan2(toY - fromY, toX - fromX));
  core.setDepth(19);
  core.setBlendMode('ADD');
  const tail = scene.add.ellipse(fromX, fromY, 24, 14, blendTint(color, 0xffffff, 0.36), 0.54).setDepth(19).setBlendMode('ADD');
  scene.tweens.add({
    targets: [line, core, tail],
    alpha: 0,
    scaleY: 0.2,
    duration: 180,
    ease: 'Quad.easeOut',
    onComplete: () => {
      line.destroy();
      core.destroy();
      tail.destroy();
    },
  });
}

function createLightningArc(
  scene: import('phaser').Scene,
  fromX: number,
  fromY: number,
  toX: number,
  toY: number,
  color: number,
) {
  const midX = (fromX + toX) / 2 + (Math.random() - 0.5) * 18;
  const midY = (fromY + toY) / 2 + (Math.random() - 0.5) * 18;
  const line = scene.add.graphics().setDepth(20);
  line.lineStyle(6, color, 0.2);
  line.beginPath();
  line.moveTo(fromX, fromY);
  line.lineTo(midX, midY);
  line.lineTo(toX, toY);
  line.strokePath();
  line.lineStyle(2.4, 0xf8fdff, 0.94);
  line.beginPath();
  line.moveTo(fromX, fromY);
  line.lineTo(midX, midY);
  line.lineTo(toX, toY);
  line.strokePath();
  line.setBlendMode('ADD');
  const halo = scene.add.circle(toX, toY, 12, blendTint(color, 0xffffff, 0.3), 0.28).setDepth(21).setBlendMode('ADD');
  scene.tweens.add({
    targets: [line, halo],
    alpha: 0,
    duration: 180,
    ease: 'Quad.easeOut',
    onComplete: () => {
      line.destroy();
      halo.destroy();
    },
  });
}

function createArcaneShardRing(scene: import('phaser').Scene, x: number, y: number, color: number, lowEffects: boolean) {
  const count = lowEffects ? 6 : 10;
  for (let index = 0; index < count; index += 1) {
    const angle = (Math.PI * 2 * index) / count;
    const shard = scene.add.rectangle(x, y, 12, 30, color, 0.72).setDepth(19);
    shard.setRotation(angle);
    shard.setBlendMode('ADD');
    scene.tweens.add({
      targets: shard,
      x: x + Math.cos(angle) * 92,
      y: y + Math.sin(angle) * 92,
      alpha: 0,
      scaleX: 0.6,
      scaleY: 1.4,
      duration: 320,
      ease: 'Cubic.easeOut',
      onComplete: () => shard.destroy(),
    });
  }
}

function createCritRing(scene: import('phaser').Scene, x: number, y: number) {
  const ring = scene.add.circle(x, y, 18, 0xffe78f, 0.08).setDepth(22);
  ring.setStrokeStyle(3, 0xffd36d, 0.92);
  ring.setBlendMode('ADD');
  scene.tweens.add({
    targets: ring,
    scale: 2.4,
    alpha: 0,
    duration: 320,
    ease: 'Quad.easeOut',
    onComplete: () => ring.destroy(),
  });
}

function createRadialWave(scene: import('phaser').Scene, x: number, y: number, color: number, radius: number, duration: number) {
  const wave = scene.add.circle(x, y, radius, color, 0.12).setDepth(20);
  wave.setStrokeStyle(4, color, 0.78);
  wave.setBlendMode('ADD');
  scene.tweens.add({
    targets: wave,
    scale: 2.1,
    alpha: 0,
    duration,
    ease: 'Cubic.easeOut',
    onComplete: () => wave.destroy(),
  });
}

function createAfterimageTrail(
  scene: import('phaser').Scene,
  x: number,
  y: number,
  rotation: number,
  color: number,
  lowEffects: boolean,
) {
  const count = lowEffects ? 2 : 4;
  for (let index = 0; index < count; index += 1) {
    const streak = scene.add.ellipse(
      x - Math.cos(rotation) * index * 14,
      y - Math.sin(rotation) * index * 14,
      58 - index * 8,
      22 - index * 3,
      blendTint(color, 0xffffff, 0.2 + index * 0.1),
      0.16 - index * 0.02,
    );
    streak.setDepth(18);
    streak.setRotation(rotation);
    streak.setBlendMode('ADD');
    scene.tweens.add({
      targets: streak,
      alpha: 0,
      scaleX: 1.18,
      scaleY: 0.66,
      duration: 140 + index * 30,
      ease: 'Quad.easeOut',
      onComplete: () => streak.destroy(),
    });
  }
}

function createMeteorStreak(scene: import('phaser').Scene, fromX: number, fromY: number, toX: number, toY: number, color: number) {
  const streak = scene.add.rectangle((fromX + toX) / 2, (fromY + toY) / 2, Math.hypot(toX - fromX, toY - fromY), 10, color, 0.34);
  streak.setRotation(Math.atan2(toY - fromY, toX - fromX));
  streak.setDepth(19);
  streak.setBlendMode('ADD');
  const core = scene.add.rectangle((fromX + toX) / 2, (fromY + toY) / 2, Math.hypot(toX - fromX, toY - fromY), 3, 0xfff5d8, 0.9);
  core.setRotation(Math.atan2(toY - fromY, toX - fromX));
  core.setDepth(20);
  core.setBlendMode('ADD');
  scene.tweens.add({
    targets: [streak, core],
    alpha: 0,
    duration: 220,
    ease: 'Quad.easeOut',
    onComplete: () => {
      streak.destroy();
      core.destroy();
    },
  });
}

function createOrbitalWisps(scene: import('phaser').Scene, x: number, y: number, color: number) {
  for (let index = 0; index < 3; index += 1) {
    const angle = (Math.PI * 2 * index) / 3;
    const orb = scene.add.circle(x + Math.cos(angle) * 18, y + Math.sin(angle) * 12, 4, color, 0.82).setDepth(21);
    orb.setBlendMode('ADD');
    scene.tweens.add({
      targets: orb,
      angle: 180,
      x: x + Math.cos(angle + Math.PI * 0.8) * 34,
      y: y + Math.sin(angle + Math.PI * 0.8) * 18,
      alpha: 0,
      scale: 0.3,
      duration: 420,
      ease: 'Sine.easeOut',
      onComplete: () => orb.destroy(),
    });
  }
}

function createCasterFlare(
  scene: import('phaser').Scene,
  x: number,
  y: number,
  color: number,
  rotation: number,
  lowEffects: boolean,
) {
  const ring = scene.add.ellipse(x, y, 54, 20, color, 0.18).setDepth(19);
  ring.setStrokeStyle(2, blendTint(color, 0xffffff, 0.42), 0.72);
  ring.setRotation(rotation);
  ring.setBlendMode('ADD');
  const halo = scene.add.circle(x, y, 18, blendTint(color, 0xffffff, 0.24), 0.22).setDepth(20);
  halo.setBlendMode('ADD');
  scene.tweens.add({
    targets: [ring, halo],
    scaleX: 1.5,
    scaleY: 1.18,
    alpha: 0,
    duration: 220,
    ease: 'Cubic.easeOut',
    onComplete: () => {
      ring.destroy();
      halo.destroy();
    },
  });

  const sparkCount = lowEffects ? 4 : 7;
  for (let index = 0; index < sparkCount; index += 1) {
    const angle = rotation - 0.7 + (1.4 * index) / Math.max(1, sparkCount - 1);
    const spark = scene.add.rectangle(x, y, 3, 16, blendTint(color, 0xffffff, 0.34), 0.86).setDepth(21);
    spark.setRotation(angle);
    spark.setBlendMode('ADD');
    scene.tweens.add({
      targets: spark,
      x: x + Math.cos(angle) * (lowEffects ? 24 : 34),
      y: y + Math.sin(angle) * (lowEffects ? 24 : 34),
      alpha: 0,
      scaleX: 0.5,
      scaleY: 0.3,
      duration: 180 + Math.random() * 80,
      ease: 'Quad.easeOut',
      onComplete: () => spark.destroy(),
    });
  }
}

function createEnemyAttackBolt(
  scene: import('phaser').Scene,
  fromX: number,
  fromY: number,
  toX: number,
  toY: number,
  color: number,
  lowEffects: boolean,
) {
  const rotation = Math.atan2(toY - fromY, toX - fromX);
  const distance = Math.hypot(toX - fromX, toY - fromY);
  const veil = scene.add.rectangle((fromX + toX) / 2, (fromY + toY) / 2, distance, lowEffects ? 8 : 12, color, 0.22);
  veil.setDepth(18);
  veil.setRotation(rotation);
  veil.setBlendMode('ADD');
  const beam = scene.add.rectangle((fromX + toX) / 2, (fromY + toY) / 2, distance, 3, blendTint(color, 0xffffff, 0.5), 0.9);
  beam.setDepth(19);
  beam.setRotation(rotation);
  beam.setBlendMode('ADD');
  const orb = scene.add.circle(toX, toY, lowEffects ? 8 : 11, blendTint(color, 0xffffff, 0.38), 0.4).setDepth(20);
  orb.setBlendMode('ADD');
  scene.tweens.add({
    targets: [veil, beam, orb],
    alpha: 0,
    duration: lowEffects ? 150 : 190,
    ease: 'Quad.easeOut',
    onComplete: () => {
      veil.destroy();
      beam.destroy();
      orb.destroy();
    },
  });
}

function animatePlayerSlash(
  scene: import('phaser').Scene,
  player: import('phaser').GameObjects.Container,
  rotation: number,
  chainStep: number,
) {
  const blade = player.getByName('blade') as import('phaser').GameObjects.Rectangle | null;
  const staffGlow = player.getByName('staff-glow') as import('phaser').GameObjects.Arc | null;
  if (!blade) {
    return;
  }

  const dash = 10 + chainStep * 4;
  const originalRotation = blade.rotation;
  scene.tweens.add({
    targets: player,
    x: player.x + Math.cos(rotation) * dash,
    y: player.y + Math.sin(rotation) * dash,
    duration: 90,
    ease: 'Quad.easeOut',
    yoyo: true,
  });
  scene.tweens.add({
    targets: blade,
    rotation: rotation + 0.94 - chainStep * 0.1,
    duration: 120,
    ease: 'Cubic.easeOut',
    yoyo: true,
    onComplete: () => blade.setRotation(originalRotation),
  });
  if (staffGlow) {
    scene.tweens.add({
      targets: staffGlow,
      scale: 1.42,
      alpha: 0.06,
      duration: 140,
      yoyo: true,
      ease: 'Sine.easeOut',
      onComplete: () => {
        staffGlow.setScale(1);
        staffGlow.setAlpha(0.24);
      },
    });
  }
}

function triggerEnemyHitFlash(scene: import('phaser').Scene, sprite: EnemySpriteState, critical: boolean) {
  sprite.flash.setFillStyle(critical ? 0xffd36d : 0xffffff, critical ? 0.78 : 0.52);
  sprite.flash.setAlpha(critical ? 0.78 : 0.52);
  scene.tweens.add({
    targets: sprite.body,
    scaleX: critical ? 1.16 : 1.08,
    scaleY: critical ? 0.86 : 0.92,
    duration: critical ? 120 : 90,
    yoyo: true,
    ease: 'Quad.easeOut',
  });
  scene.tweens.add({
    targets: sprite.flash,
    alpha: 0,
    duration: critical ? 220 : 150,
    ease: 'Quad.easeOut',
  });
}

function findNearestAliveEnemy(
  player: import('phaser').GameObjects.Container,
  enemies: EnemyState[],
): { nearestEnemy: EnemyState | null; nearestDistance: number } {
  let nearestEnemy: EnemyState | null = null;
  let nearestDistance = Number.POSITIVE_INFINITY;

  enemies.forEach((enemy) => {
    if (!enemy.alive) {
      return;
    }

    const distance = Math.hypot(enemy.x - player.x, enemy.y - player.y);
    if (distance < nearestDistance) {
      nearestDistance = distance;
      nearestEnemy = enemy;
    }
  });

  return { nearestEnemy, nearestDistance };
}

const PRIMARY_ATTACK_SKILL_ID = 'energy-bolt';
const THE_QUEST_MANUAL_SKILL_EVENT = 'the-quest:manual-skill-cast';

export async function createTheQuestGame({ mountElement }: CreateTheQuestGameParams): Promise<GameHandle> {
  const Phaser = await import('phaser');

  class TheQuestScene extends Phaser.Scene {
    private player!: import('phaser').GameObjects.Container;
    private enemySprites = new Map<string, EnemySpriteState>();
    private portal!: PortalState;
    private lastEnemyResetVersion = -1;
    private lastTransitionVersion = -1;
    private lastPlayerAttackAt = 0;
    private lastAnySkillCastAt = 0;
    private lastSkillCastAt = new Map<string, number>();
    private pendingRespawns = new Map<string, number>();
    private attackFlash?: import('phaser').GameObjects.Rectangle;
    private playerImpactFlash?: import('phaser').GameObjects.Ellipse;
    private playerWardRing?: import('phaser').GameObjects.Ellipse;
    private familiarHalo?: import('phaser').GameObjects.Ellipse;
    private performance = createPerformanceProfile(window.innerWidth);
    private lastMinimapSyncAt = 0;
    private regenCarryHp = 0;
    private regenCarryMp = 0;
    private lastHandledManualSkillCastId = 0;
    private lastBlockedSkillFeedbackAt = new Map<string, number>();
    private pendingManualSkillEvents: Array<{ skillId: string; requestedAt: number }> = [];

    requestManualSkillCast(skillId: string) {
      this.pendingManualSkillEvents.push({
        skillId,
        requestedAt: Date.now(),
      });
    }

    private showBlockedSkillFeedback(message: string, time: number, key = message) {
      const lastFeedbackAt = this.lastBlockedSkillFeedbackAt.get(key) ?? 0;
      if (time - lastFeedbackAt < 420) {
        return;
      }
      this.lastBlockedSkillFeedbackAt.set(key, time);
    }

    private getSpellAmpMultiplier(nowMs: number) {
      const { consumableEffects } = useTheQuestStore.getState();
      return consumableEffects.arcaneSurgeUntil > nowMs ? consumableEffects.arcaneSurgeMultiplier : 1;
    }

    private restorePlayerMana(amount: number) {
      if (amount <= 0) {
        return;
      }
      useTheQuestStore.getState().setVitals((vitals) => ({
        ...vitals,
        mp: Math.min(vitals.maxMp, vitals.mp + amount),
      }));
      createFloatingText(this, this.player.x, this.player.y - 70, `+${amount} MP`, '#7fe2ff');
    }

    private updateEnemyMana(enemyId: string, amount: number) {
      const store = useTheQuestStore.getState();
      store.setEnemySnapshot(
        store.enemies.map((enemy) =>
          enemy.id === enemyId
            ? {
                ...enemy,
                mp: clamp(enemy.mp + amount, 0, enemy.maxMp),
              }
            : enemy,
        ),
      );
    }

    private drainEnemyMana(enemyId: string, desiredAmount: number) {
      const store = useTheQuestStore.getState();
      const enemy = store.enemies.find((entry) => entry.id === enemyId);
      if (!enemy || desiredAmount <= 0 || enemy.mp <= 0) {
        return 0;
      }
      const drained = Math.min(enemy.mp, Math.round(desiredAmount));
      this.updateEnemyMana(enemyId, -drained);
      return drained;
    }

    private resolveManaAbsorb(enemyId: string, baseAmount: number, sourceLabel: string) {
      const { vitals } = useTheQuestStore.getState();
      if (vitals.manaAbsorbChance <= 0 || Math.random() > vitals.manaAbsorbChance) {
        return;
      }
      const drained = this.drainEnemyMana(enemyId, baseAmount);
      if (drained <= 0) {
        return;
      }
      this.restorePlayerMana(drained);
      useTheQuestStore.getState().setCombatText({
        id: `mana-absorb-${enemyId}-${Date.now()}`,
        value: `${sourceLabel} MP +${drained}`,
        color: '#8ce6ff',
      });
    }

    private handleManualSkillCast(skillId: string, time: number) {
      const store = useTheQuestStore.getState();
      const skill = store.skills.find((entry) => entry.id === skillId);
      if (!skill) {
        return;
      }

      const readyAt = store.skillCooldowns[skill.id] ?? 0;
      if (Date.now() < readyAt) {
        this.showBlockedSkillFeedback(`${skill.name} 대기중`, time, `cooldown:${skill.id}`);
        store.setLastCombatLog(`${skill.name} 재사용 대기 중입니다.`);
        return;
      }

      if (store.vitals.mp < skill.manaCost) {
        this.showBlockedSkillFeedback('MP 부족', time, `mana:${skill.id}`);
        store.setLastCombatLog(`${skill.name} 사용에 필요한 MP가 부족합니다.`);
        return;
      }

      const nearest = findNearestAliveEnemy(this.player, store.enemies);
      if (skill.role !== 'attack') {
        const targetX = nearest.nearestEnemy?.x ?? this.player.x + 1;
        const targetY = nearest.nearestEnemy?.y ?? this.player.y;
        const rotation = Math.atan2(targetY - this.player.y, targetX - this.player.x);
        this.castSkill(nearest.nearestEnemy?.id ?? null, skill.id, rotation, time);
        return;
      }

      if (!nearest.nearestEnemy) {
        this.showBlockedSkillFeedback('대상 없음', time, `target:${skill.id}`);
        store.setLastCombatLog(`${skill.name}을(를) 사용할 대상이 없습니다.`);
        return;
      }

      const sprite = this.enemySprites.get(nearest.nearestEnemy.id);
      if (!sprite) {
        this.showBlockedSkillFeedback('대상 없음', time, `sprite:${skill.id}`);
        return;
      }

      const rotation = Math.atan2(sprite.body.y - this.player.y, sprite.body.x - this.player.x);
      if (skillId === PRIMARY_ATTACK_SKILL_ID) {
        this.castSkill(nearest.nearestEnemy.id, skill.id, rotation, time);
        return;
      }

      this.castSkill(nearest.nearestEnemy.id, skillId, rotation, time);
    }

    constructor() {
      super('TheQuestScene');
    }

    create() {
      this.performance = createPerformanceProfile(this.scale.width);
      this.cameras.main.setBackgroundColor('#13223a');
      this.physics.world.setBounds(0, 0, THE_QUEST_WORLD_SIZE.width, THE_QUEST_WORLD_SIZE.height);
      this.cameras.main.setBounds(0, 0, THE_QUEST_WORLD_SIZE.width, THE_QUEST_WORLD_SIZE.height);
      this.cameras.main.roundPixels = this.performance.isMobile;
      this.cameras.main.setZoom(this.performance.lowEffects ? 0.92 : this.performance.isMobile ? 0.98 : 1.1);

      this.drawTerrain();
      const { playerLocation } = useTheQuestStore.getState();
      this.player = createPlayerSprite(this, playerLocation.x, playerLocation.y);
      this.playerImpactFlash = this.add.ellipse(this.player.x, this.player.y - 10, 96, 122, 0xffffff, 0).setDepth(13).setBlendMode('ADD');
      this.playerWardRing = this.add.ellipse(this.player.x, this.player.y + 16, 114, 46, 0x8de8ff, 0).setDepth(9).setBlendMode('ADD');
      this.familiarHalo = this.add.ellipse(this.player.x, this.player.y - 16, 32, 32, 0xdff7ff, 0).setDepth(14).setBlendMode('ADD');
      this.cameras.main.startFollow(this.player, true, 0.08, 0.08);
      useTheQuestStore.getState().setPerformance({
        isMobile: this.performance.isMobile,
        lowEffects: this.performance.lowEffects,
      });

      const state = useTheQuestStore.getState();
      state.npcs.forEach((npc) => createNpcMarker(this, npc));

      state.enemies.forEach((enemy) => {
        const sprite = createEnemySprite(this, enemy);
        this.enemySprites.set(enemy.id, sprite);
        updateEnemyHpBar(sprite.hpBar, enemy, sprite);
      });
      this.portal = createPortalMarker(this);
      this.lastEnemyResetVersion = state.stage.enemyResetVersion;
      this.lastTransitionVersion = state.stage.transitionVersion;
      this.updatePortalState();

      this.attackFlash = this.add.rectangle(0, 0, THE_QUEST_WORLD_SIZE.width, THE_QUEST_WORLD_SIZE.height, 0xffffff, 0);
      this.attackFlash.setDepth(2);
      this.attackFlash.setOrigin(0, 0);

      const manualSkillEventHandler = (event: Event) => {
        const detail = (event as CustomEvent<{ skillId?: string; requestedAt?: number }>).detail;
        if (!detail?.skillId) {
          return;
        }
        this.pendingManualSkillEvents.push({
          skillId: detail.skillId,
          requestedAt: typeof detail.requestedAt === 'number' ? detail.requestedAt : Date.now(),
        });
      };
      window.addEventListener(THE_QUEST_MANUAL_SKILL_EVENT, manualSkillEventHandler as EventListener);
      this.events.once('shutdown', () => {
        window.removeEventListener(THE_QUEST_MANUAL_SKILL_EVENT, manualSkillEventHandler as EventListener);
      });

      this.refreshMinimap();
    }

    update(time: number, delta: number) {
      const body = this.player.body as import('phaser').Physics.Arcade.Body;
      const store = useTheQuestStore.getState();
      const { vitals, autoCombat, joystick, enemies, npcs } = store;
      const nowMs = Date.now();

      if (store.stage.enemyResetVersion !== this.lastEnemyResetVersion) {
        this.syncEnemySprites(store.enemies);
        this.pendingRespawns.clear();
        this.lastEnemyResetVersion = store.stage.enemyResetVersion;
      }
      if (store.stage.transitionVersion !== this.lastTransitionVersion) {
        this.player.setPosition(store.playerLocation.x, store.playerLocation.y);
        this.lastTransitionVersion = store.stage.transitionVersion;
      }
      this.updatePortalState();

      let moveX = joystick.x;
      let moveY = joystick.y;
      const nearest = findNearestAliveEnemy(this.player, enemies);
      store.setCurrentTarget(nearest.nearestEnemy?.id ?? null);
      store.clearExpiredCombo(time);
      store.clearExpiredSkillState(time);

      if (autoCombat && !joystick.active && nearest.nearestEnemy && nearest.nearestDistance > 88) {
        const direction = normalizeVector(nearest.nearestEnemy.x - this.player.x, nearest.nearestEnemy.y - this.player.y);
        moveX = direction.x;
        moveY = direction.y;
      }

      const movement = normalizeVector(moveX, moveY);
      const speedMultiplier =
        store.consumableEffects.moveSpeedBoostUntil > nowMs ? store.consumableEffects.moveSpeedMultiplier : 1;
      const moveSpeed = (autoCombat && nearest.nearestEnemy ? 168 : 192) * speedMultiplier;
      body.setVelocity(movement.x * moveSpeed, movement.y * moveSpeed);

      if (movement.length > 0) {
        this.player.setScale(1 + Math.sin(time / 120) * 0.02, 1 - Math.sin(time / 120) * 0.02);
      } else {
        this.player.setScale(1, 1);
      }
      this.updatePlayerPresentation(time, movement.length > 0);

      const activeNpc = npcs.find((npc) => Math.hypot(npc.x - this.player.x, npc.y - this.player.y) < 96) ?? null;
      store.setActiveNpc(activeNpc?.id ?? null);

      this.updateEnemies(time, delta);
      this.applyConsumableEffects(nowMs);
      this.applyPassiveRecovery(delta);
      this.triggerFamiliarPulse(time);
      this.consumeManualSkillEventQueue(time);
      this.consumeManualSkillCastCommand(time);
      this.handleAutoCombat(time, vitals.mp);
      if (time - this.lastMinimapSyncAt >= (this.performance.isMobile ? 120 : 60)) {
        this.refreshMinimap();
        this.lastMinimapSyncAt = time;
      }
      this.handleRespawns(time);
    }

    private consumeManualSkillCastCommand(time: number) {
      const store = useTheQuestStore.getState();
      const command = store.manualSkillCastCommand;
      if (!command || command.id === this.lastHandledManualSkillCastId) {
        return;
      }

      this.lastHandledManualSkillCastId = command.id;
      this.handleManualSkillCast(command.skillId, time);
      store.consumeManualSkillCast(command.id);
    }

    private consumeManualSkillEventQueue(time: number) {
      if (!this.pendingManualSkillEvents.length) {
        return;
      }

      const queuedEvents = [...this.pendingManualSkillEvents];
      this.pendingManualSkillEvents = [];
      queuedEvents.forEach((entry) => {
        this.handleManualSkillCast(entry.skillId, time);
      });
    }

    private syncEnemySprites(enemies: EnemyState[]) {
      enemies.forEach((enemy) => {
        const sprite = this.enemySprites.get(enemy.id);
        if (!sprite) {
          return;
        }

        sprite.baseX = enemy.x;
        sprite.baseY = enemy.y;
        sprite.body.setPosition(enemy.x, enemy.y);
        sprite.body.setVisible(enemy.alive);
        sprite.body.setRotation(0);
        sprite.label.setText(`${enemy.role === 'boss' ? 'BOSS ' : ''}Lv.${enemy.level} ${enemy.name}`);
        sprite.label.setPosition(enemy.x, enemy.y - getEnemyLabelOffset(enemy.role));
        sprite.label.setVisible(enemy.alive);
        sprite.attackCooldownUntil = 0;
        updateEnemyHpBar(sprite.hpBar, enemy, sprite);
      });
    }

    private updatePlayerPresentation(time: number, moving: boolean) {
      const aura = this.player.getByName('player-aura') as import('phaser').GameObjects.Arc | null;
      const auraOuter = this.player.getByName('player-aura-outer') as import('phaser').GameObjects.Ellipse | null;
      const auraRing = this.player.getByName('player-aura-ring') as import('phaser').GameObjects.Ellipse | null;
      const runeCore = this.player.getByName('player-rune-core') as import('phaser').GameObjects.Arc | null;
      const runeHalo = this.player.getByName('player-rune-halo') as import('phaser').GameObjects.Arc | null;
      const staffGlow = this.player.getByName('staff-glow') as import('phaser').GameObjects.Arc | null;
      const effects = useTheQuestStore.getState().consumableEffects;
      const surgeActive = effects.arcaneSurgeUntil > Date.now();
      const wardActive = effects.manaWardUntil > Date.now();
      const familiarActive = effects.familiarUntil > Date.now();

      if (aura) {
        aura.setScale(1 + Math.sin(time / 260) * 0.08);
        aura.setAlpha(surgeActive ? 0.28 : moving ? 0.2 : 0.16);
      }
      if (auraOuter) {
        auraOuter.setRotation(time / 1600);
        auraOuter.setScale(1 + Math.cos(time / 400) * 0.06, 1 + Math.sin(time / 460) * 0.04);
        auraOuter.setAlpha(surgeActive ? 0.14 : 0.08);
      }
      if (auraRing) {
        auraRing.setRotation(-time / 1100);
        auraRing.setScale(1 + Math.cos(time / 320) * 0.08, 1 + Math.sin(time / 300) * 0.05);
        auraRing.setAlpha(surgeActive ? 0.16 : wardActive ? 0.1 : 0.04);
      }
      if (runeCore) {
        runeCore.setAlpha(surgeActive ? 0.7 : 0.44);
        runeCore.setScale(1 + Math.sin(time / 180) * (surgeActive ? 0.22 : 0.1));
      }
      if (runeHalo) {
        runeHalo.setAlpha(surgeActive ? 0.2 : 0.08);
        runeHalo.setScale(1 + Math.cos(time / 210) * 0.18);
      }
      if (staffGlow) {
        staffGlow.setAlpha(surgeActive ? 0.4 : 0.24);
        staffGlow.setScale(1 + Math.cos(time / 220) * 0.08);
      }
      if (this.playerImpactFlash) {
        this.playerImpactFlash.setPosition(this.player.x, this.player.y - 10);
      }
      if (this.playerWardRing) {
        this.playerWardRing.setPosition(this.player.x, this.player.y + 16);
        this.playerWardRing.setAlpha(wardActive ? 0.2 + Math.sin(time / 180) * 0.04 : 0);
        this.playerWardRing.setScale(1 + Math.sin(time / 260) * 0.06);
      }
      if (this.familiarHalo) {
        this.familiarHalo.setPosition(this.player.x + Math.cos(time / 240) * 34, this.player.y - 34 + Math.sin(time / 170) * 14);
        this.familiarHalo.setAlpha(familiarActive ? 0.74 : 0);
        this.familiarHalo.setScale(1 + Math.sin(time / 140) * 0.2);
      }
    }

    private updatePortalState() {
      if (!this.portal) {
        return;
      }

      const bossAlive = useTheQuestStore.getState().enemies.some((enemy) => enemy.role === 'boss' && enemy.alive);
      const unlocked = !bossAlive;
      this.portal.ring.setStrokeStyle(5, unlocked ? 0xffd983 : 0x87c8ff, unlocked ? 0.9 : 0.62);
      this.portal.core.setFillStyle(unlocked ? 0xffc96c : 0x71b6ff, unlocked ? 0.38 : 0.28);
      this.portal.core.setStrokeStyle(3, unlocked ? 0xfff3da : 0xe9f6ff, unlocked ? 0.95 : 0.72);
      this.portal.glow.setFillStyle(unlocked ? 0xffd58b : 0x5da7ff, unlocked ? 0.18 : 0.12);
      this.portal.label.setText(unlocked ? 'PORTAL OPEN' : 'PORTAL LOCK');
      this.portal.label.setColor(unlocked ? '#fff1bf' : '#d4ecff');
    }

    private applyConsumableEffects(nowMs: number) {
      const store = useTheQuestStore.getState();
      const { consumableEffects } = store;
      let queuedRecovery = consumableEffects.queuedRecovery;
      let healedAmount = 0;
      let didChange = false;

      while (queuedRecovery.length > 0 && queuedRecovery[0].nextTickAt <= nowMs) {
        const current = queuedRecovery[0];
        healedAmount += current.healPerTick;
        didChange = true;

        if (current.ticksRemaining <= 1) {
          queuedRecovery = queuedRecovery.slice(1);
        } else {
          queuedRecovery = [
            {
              ...current,
              ticksRemaining: current.ticksRemaining - 1,
              nextTickAt: current.nextTickAt + current.tickIntervalMs,
            },
            ...queuedRecovery.slice(1),
          ];
        }
      }

      const autoHealExpired = consumableEffects.autoHealBoostUntil > 0 && consumableEffects.autoHealBoostUntil <= nowMs;
      const speedExpired = consumableEffects.moveSpeedBoostUntil > 0 && consumableEffects.moveSpeedBoostUntil <= nowMs;
      const arcaneSurgeExpired = consumableEffects.arcaneSurgeUntil > 0 && consumableEffects.arcaneSurgeUntil <= nowMs;
      const manaWardExpired = consumableEffects.manaWardUntil > 0 && consumableEffects.manaWardUntil <= nowMs;
      const familiarExpired = consumableEffects.familiarUntil > 0 && consumableEffects.familiarUntil <= nowMs;

      if (didChange || autoHealExpired || speedExpired || arcaneSurgeExpired || manaWardExpired || familiarExpired) {
        store.setConsumableEffects(() => ({
          queuedRecovery,
          autoHealMultiplier: autoHealExpired ? 1 : consumableEffects.autoHealMultiplier,
          autoHealBoostUntil: autoHealExpired ? 0 : consumableEffects.autoHealBoostUntil,
          moveSpeedMultiplier: speedExpired ? 1 : consumableEffects.moveSpeedMultiplier,
          moveSpeedBoostUntil: speedExpired ? 0 : consumableEffects.moveSpeedBoostUntil,
          arcaneSurgeMultiplier: arcaneSurgeExpired ? 1 : consumableEffects.arcaneSurgeMultiplier,
          arcaneSurgeUntil: arcaneSurgeExpired ? 0 : consumableEffects.arcaneSurgeUntil,
          manaWardReduction: manaWardExpired ? 0 : consumableEffects.manaWardReduction,
          manaWardUntil: manaWardExpired ? 0 : consumableEffects.manaWardUntil,
          familiarUntil: familiarExpired ? 0 : consumableEffects.familiarUntil,
          familiarPulseAt: familiarExpired ? 0 : consumableEffects.familiarPulseAt,
        }));
      }

      if (healedAmount > 0) {
        store.setVitals((currentVitals) => ({
          ...currentVitals,
          hp: Math.min(currentVitals.maxHp, currentVitals.hp + healedAmount),
        }));
        createFloatingText(this, this.player.x, this.player.y - 58, `+${healedAmount}`, '#7df0b3');
      }
    }

    private applyPassiveRecovery(delta: number) {
      const store = useTheQuestStore.getState();
      const { hp, maxHp, mp, maxMp } = store.vitals;
      const autoHealMultiplier =
        store.consumableEffects.autoHealBoostUntil > Date.now() ? store.consumableEffects.autoHealMultiplier : 1;
      const arcaneSurgeMultiplier = this.getSpellAmpMultiplier(Date.now());

      if (hp >= maxHp && mp >= maxMp) {
        this.regenCarryHp = 0;
        this.regenCarryMp = 0;
        return;
      }

      this.regenCarryHp += (delta / 1000) * PASSIVE_HP_RECOVERY_PER_SECOND * autoHealMultiplier;
      this.regenCarryMp +=
        (delta / 1000) *
        PASSIVE_MP_RECOVERY_PER_SECOND *
        autoHealMultiplier *
        (arcaneSurgeMultiplier > 1 ? 1.4 : 1);

      const hpGain = hp < maxHp ? Math.floor(this.regenCarryHp) : 0;
      const mpGain = mp < maxMp ? Math.floor(this.regenCarryMp) : 0;

      if (hpGain <= 0 && mpGain <= 0) {
        return;
      }

      this.regenCarryHp = Math.max(0, this.regenCarryHp - hpGain);
      this.regenCarryMp = Math.max(0, this.regenCarryMp - mpGain);

      store.setVitals((currentVitals) => ({
        ...currentVitals,
        hp: Math.min(currentVitals.maxHp, currentVitals.hp + hpGain),
        mp: Math.min(currentVitals.maxMp, currentVitals.mp + mpGain),
      }));
    }

    private triggerFamiliarPulse(time: number) {
      const store = useTheQuestStore.getState();
      const { consumableEffects } = store;
      if (consumableEffects.familiarUntil <= 0 || consumableEffects.familiarUntil <= Date.now()) {
        return;
      }
      if (consumableEffects.familiarPulseAt > time) {
        return;
      }

      const targets = store.enemies
        .filter((enemy) => enemy.alive && Math.hypot(enemy.x - this.player.x, enemy.y - this.player.y) < 182)
        .slice(0, 3);
      if (!targets.length) {
        store.setConsumableEffects((current) => ({
          ...current,
          familiarPulseAt: time + 900,
        }));
        return;
      }

      createArcaneShardRing(this, this.player.x, this.player.y, 0xc6f3ff, this.performance.lowEffects);
      createOrbitalWisps(this, this.player.x, this.player.y - 12, 0xe6fbff);
      targets.forEach((target) => {
        const targetSprite = this.enemySprites.get(target.id);
        if (!targetSprite) {
          return;
        }
        createLightningArc(this, this.player.x, this.player.y - 18, targetSprite.body.x, targetSprite.body.y, 0xc4f1ff);
        triggerEnemyHitFlash(this, targetSprite, false);
        createFloatingText(this, targetSprite.body.x, targetSprite.body.y - 18, '26', '#c4f1ff');
        store.applyDamageToEnemy(target.id, 26);
        this.handleEnemyDeath(target.id);
      });
      this.restorePlayerMana(6);
      store.setConsumableEffects((current) => ({
        ...current,
        familiarPulseAt: time + 900,
      }));
    }

    private drawTerrain() {
      const background = this.add.graphics();
      background.fillGradientStyle(0x314629, 0x314629, 0x173625, 0x0f2119, 1);
      background.fillRect(0, 0, THE_QUEST_WORLD_SIZE.width, THE_QUEST_WORLD_SIZE.height);
      background.fillGradientStyle(0x284a58, 0x284a58, 0x0f2130, 0x0b1823, 0.2);
      background.fillEllipse(1500, 420, 1180, 440);

      const mist = this.add.graphics();
      mist.fillStyle(0xe6efbe, 0.1);
      mist.fillEllipse(1120, 350, 1080, 320);
      mist.fillStyle(0x9ad4ff, 0.08);
      mist.fillEllipse(1660, 480, 620, 260);
      mist.fillStyle(0xffffff, 0.04);
      mist.fillEllipse(1300, 260, 780, 180);

      const river = this.add.graphics();
      river.fillStyle(0x264c58, 0.9);
      river.fillEllipse(1720, 1080, 480, 260);
      river.fillStyle(0x8cc9d8, 0.18);
      river.fillEllipse(1700, 1060, 420, 180);
      river.fillStyle(0xe8fbff, 0.08);
      river.fillEllipse(1700, 1036, 328, 72);

      const path = this.add.graphics();
      path.fillStyle(0xa88b5d, 0.98);
      path.fillRoundedRect(1000, 480, 230, 840, 96);
      path.fillRoundedRect(1016, 860, 620, 176, 92);
      path.fillStyle(0xcca875, 0.3);
      path.fillRoundedRect(1060, 520, 92, 752, 70);
      path.fillStyle(0xf7d59a, 0.08);
      path.fillRoundedRect(1086, 520, 28, 748, 30);

      const cliffs = this.add.graphics();
      cliffs.fillStyle(0x101b18, 0.18);
      cliffs.fillEllipse(540, 1180, 660, 260);
      cliffs.fillEllipse(1880, 1340, 540, 240);

      const foliage = this.add.graphics();
      const treePalette = [0x1a321f, 0x23452a, 0x315f39, 0x487650];
      for (let index = 0; index < this.performance.treeCount; index += 1) {
        const x = Math.random() * THE_QUEST_WORLD_SIZE.width;
        const y = Math.random() * THE_QUEST_WORLD_SIZE.height;
        const canopyRadius = 28 + Math.random() * 26;
        const trunkHeight = 18 + Math.random() * 12;
        foliage.fillStyle(0x493522, 0.92);
        foliage.fillRect(x - 4, y + 6, 8, trunkHeight);
        foliage.fillStyle(treePalette[index % treePalette.length], 0.92);
        foliage.fillCircle(x, y, canopyRadius);
        foliage.fillStyle(0x5d9665, 0.28);
        foliage.fillCircle(x - canopyRadius * 0.18, y - canopyRadius * 0.18, canopyRadius * 0.42);
      }

      const shrubs = this.add.graphics();
      for (let index = 0; index < this.performance.shrubCount; index += 1) {
        const x = Math.random() * THE_QUEST_WORLD_SIZE.width;
        const y = Math.random() * THE_QUEST_WORLD_SIZE.height;
        shrubs.fillStyle(index % 4 === 0 ? 0x6f5d41 : index % 4 === 1 ? 0x95b55d : index % 4 === 2 ? 0xa74f4b : 0x55703d, 0.88);
        shrubs.fillCircle(x, y, 6 + Math.random() * 8);
      }

      for (let index = 0; index < this.performance.crystalCount; index += 1) {
        const crystal = this.add.star(0, 0, 4, 5, 14, index % 2 === 0 ? 0x7bc8ff : 0xf6dd92, 0.55);
        crystal.setPosition(820 + (index % 6) * 148, 500 + Math.floor(index / 6) * 196);
        crystal.setRotation(Math.random());
        crystal.setScale(1 + Math.random() * 0.35);
        crystal.setDepth(1);
        this.tweens.add({
          targets: crystal,
          alpha: { from: 0.35, to: 0.8 },
          yoyo: true,
          repeat: -1,
          duration: (this.performance.lowEffects ? 1700 : 1300) + index * 90,
          ease: 'Sine.easeInOut',
        });
      }

      for (let index = 0; index < this.performance.lanternCount; index += 1) {
        const lanternGlow = this.add.circle(870 + (index % 6) * 132, 540 + Math.floor(index / 6) * 214, 24, 0xffe3a0, 0.06);
        lanternGlow.setBlendMode('ADD');
        lanternGlow.setDepth(2);
        this.tweens.add({
          targets: lanternGlow,
          alpha: { from: 0.04, to: 0.12 },
          yoyo: true,
          repeat: -1,
          duration: (this.performance.lowEffects ? 2200 : 1800) + index * 50,
          ease: 'Sine.easeInOut',
        });
      }

      const sunShaft = this.add.graphics();
      sunShaft.fillStyle(0xfef7c5, 0.04);
      sunShaft.fillTriangle(740, 0, 1240, 0, 980, 760);
      sunShaft.fillStyle(0xe7fbff, 0.025);
      sunShaft.fillTriangle(1320, 0, 1780, 0, 1500, 680);
    }

    private updateEnemies(time: number, delta: number) {
      const store = useTheQuestStore.getState();
      const currentEnemies = store.enemies.map((enemy) => {
        const sprite = this.enemySprites.get(enemy.id);
        if (!sprite) {
          return enemy;
        }

        if (!enemy.alive) {
          sprite.body.setVisible(false);
          sprite.label.setVisible(false);
          sprite.hpBar.clear();
          return enemy;
        }

        sprite.body.setVisible(true);
        sprite.label.setVisible(true);

        const orbitX = Math.cos((time + sprite.patrolTimeOffset) / 1100) * enemy.patrolRadius;
        const orbitY = Math.sin((time + sprite.patrolTimeOffset) / 920) * enemy.patrolRadius * 0.55;
        const driftX = (sprite.baseX + orbitX - sprite.body.x) * 0.02;
        const driftY = (sprite.baseY + orbitY - sprite.body.y) * 0.02;
        sprite.body.x += driftX * delta;
        sprite.body.y += driftY * delta;
        sprite.body.setScale(1 + Math.sin((time + sprite.patrolTimeOffset) / 140) * 0.02, 1 - Math.sin((time + sprite.patrolTimeOffset) / 140) * 0.02);
        sprite.body.setRotation(Math.sin((time + sprite.patrolTimeOffset) / 360) * 0.035);
        sprite.aura.setScale(1 + Math.sin((time + sprite.patrolTimeOffset) / 300) * 0.08);
        sprite.aura.setAlpha(enemy.role === 'boss' ? 0.24 : 0.18);
        sprite.core.setScale(1 + Math.cos((time + sprite.patrolTimeOffset) / 240) * 0.12);
        sprite.shadow.setScale(1 + Math.sin((time + sprite.patrolTimeOffset) / 420) * 0.04);
        sprite.label.setPosition(sprite.body.x, sprite.body.y - getEnemyLabelOffset(enemy.role));

        const distance = Math.hypot(this.player.x - sprite.body.x, this.player.y - sprite.body.y);
        if (distance < 78 && time >= sprite.attackCooldownUntil) {
          sprite.attackCooldownUntil = time + 1450;
          const damageReduction =
            store.consumableEffects.manaWardUntil > Date.now() ? store.consumableEffects.manaWardReduction : 0;
          const damage = Math.max(5, Math.round((enemy.attack - Math.floor(store.vitals.defense * 0.3)) * (1 - damageReduction)));
          createEnemyAttackBolt(
            this,
            sprite.body.x,
            sprite.body.y - 2,
            this.player.x,
            this.player.y - 10,
            blendTint(enemy.tint, 0xffffff, enemy.role === 'boss' ? 0.28 : 0.14),
            this.performance.lowEffects,
          );
          store.setVitals((vitals) => ({
            ...vitals,
            hp: Math.max(0, vitals.hp - damage),
          }));
          this.triggerPlayerHitFeedback(enemy.tint, damageReduction > 0);
          createFloatingText(this, this.player.x, this.player.y - 46, `-${damage}`, '#ff7f7f');
          this.flashScreen();
        }

        updateEnemyHpBar(sprite.hpBar, enemy, sprite);

        return {
          ...enemy,
          x: sprite.body.x,
          y: sprite.body.y,
          mp: Math.min(enemy.maxMp, enemy.mp + delta * 0.004),
        };
      });

      store.setEnemySnapshot(currentEnemies);
    }

    private handleAutoCombat(time: number, currentMp: number) {
      const store = useTheQuestStore.getState();
      if (!store.autoCombat) {
        return;
      }
      const nearest = findNearestAliveEnemy(this.player, store.enemies);
      if (!nearest.nearestEnemy) {
        return;
      }

      const enemySprite = this.enemySprites.get(nearest.nearestEnemy.id);
      if (!enemySprite || !nearest.nearestEnemy.alive) {
        return;
      }

      const speedMultiplier =
        store.consumableEffects.moveSpeedBoostUntil > Date.now() ? store.consumableEffects.moveSpeedMultiplier : 1;
      const aimRotation = Math.atan2(enemySprite.body.y - this.player.y, enemySprite.body.x - this.player.x);
      const isNear = nearest.nearestDistance < AUTO_COMBAT_BASIC_ATTACK_RANGE;
      const canBasicAttack = time >= this.lastPlayerAttackAt + 640 / speedMultiplier;
      const canCastSpell = time >= this.lastAnySkillCastAt + AUTO_COMBAT_SPELL_GAP_MS / speedMultiplier;
      const shouldWeaveBasicAttack =
        isNear &&
        canBasicAttack &&
        (this.lastAnySkillCastAt > this.lastPlayerAttackAt || currentMp <= AUTO_COMBAT_MP_RESERVE + 8 || nearest.nearestDistance < 170);
      const fireArrowReady = time >= (this.lastSkillCastAt.get('fire-arrow') ?? 0);
      const iceDaggerReady = time >= (this.lastSkillCastAt.get('ice-dagger') ?? 0);
      const lightningReady = time >= (this.lastSkillCastAt.get('lightning') ?? 0);
      const fireballReady = time >= (this.lastSkillCastAt.get('fireball') ?? 0);
      const hasteReady = time >= (this.lastSkillCastAt.get('greater-haste') ?? 0);
      const shieldReady = time >= (this.lastSkillCastAt.get('magic-shield') ?? 0);
      const summonReady = time >= (this.lastSkillCastAt.get('summon-skeleton') ?? 0);
      const canSpendMana = (cost: number, reserve = AUTO_COMBAT_MP_RESERVE) => currentMp >= cost && currentMp - cost >= reserve;

      if (shouldWeaveBasicAttack) {
        this.basicAttack(nearest.nearestEnemy.id, aimRotation, time);
        return;
      }

      if (!canCastSpell) {
        if (isNear && canBasicAttack) {
          this.basicAttack(nearest.nearestEnemy.id, aimRotation, time);
        }
        return;
      }

      if (currentMp >= 26 && shieldReady && store.consumableEffects.manaWardUntil <= Date.now() && store.vitals.hp < store.vitals.maxHp * 0.52) {
        this.castSkill(nearest.nearestEnemy.id, 'magic-shield', aimRotation, time);
        return;
      }
      if (currentMp >= 22 && hasteReady && store.consumableEffects.arcaneSurgeUntil <= Date.now() && canSpendMana(22, 24)) {
        this.castSkill(nearest.nearestEnemy.id, 'greater-haste', aimRotation, time);
        return;
      }
      if (
        currentMp >= 26 &&
        shieldReady &&
        store.consumableEffects.manaWardUntil <= Date.now() &&
        store.vitals.hp < store.vitals.maxHp * 0.76 &&
        canSpendMana(26, 14)
      ) {
        this.castSkill(nearest.nearestEnemy.id, 'magic-shield', aimRotation, time);
        return;
      }
      if (currentMp >= 34 && summonReady && store.consumableEffects.familiarUntil <= Date.now() && canSpendMana(34)) {
        this.castSkill(nearest.nearestEnemy.id, 'summon-skeleton', aimRotation, time);
        return;
      }
      if (nearest.nearestDistance < 232 && fireArrowReady && canSpendMana(10)) {
        this.castSkill(nearest.nearestEnemy.id, 'fire-arrow', aimRotation, time);
        return;
      }
      if (nearest.nearestDistance < 244 && iceDaggerReady && canSpendMana(16)) {
        this.castSkill(nearest.nearestEnemy.id, 'ice-dagger', aimRotation, time);
        return;
      }
      if (nearest.nearestDistance < 224 && lightningReady && canSpendMana(24)) {
        this.castSkill(nearest.nearestEnemy.id, 'lightning', aimRotation, time);
        return;
      }
      if (nearest.nearestDistance < 188 && fireballReady && canSpendMana(30, 28)) {
        this.castSkill(nearest.nearestEnemy.id, 'fireball', aimRotation, time);
        return;
      }

      if (isNear && canBasicAttack) {
        this.basicAttack(nearest.nearestEnemy.id, aimRotation, time);
      }
    }

    private basicAttack(enemyId: string, rotation: number, time: number) {
      const store = useTheQuestStore.getState();
      const enemy = store.enemies.find((entry) => entry.id === enemyId);
      const sprite = this.enemySprites.get(enemyId);
      if (!enemy || !sprite || !enemy.alive) {
        return;
      }

      this.lastPlayerAttackAt = time;

      const previousComboActive = store.combat.comboExpiresAt > time;
      const chainStep = previousComboActive ? (store.combat.chainStep + 1) % 3 : 0;
      const comboCount = previousComboActive ? store.combat.comboCount + 1 : 1;
      const crit = Math.random() < store.vitals.critRate;
      const comboScale = [1.08, 1.26, 1.42][chainStep] ?? 1.08;
      const spellAmp = this.getSpellAmpMultiplier(Date.now());
      const damage = Math.round((store.vitals.spellPower * comboScale + store.vitals.attack * 0.4) * spellAmp * (crit ? 1.82 : 1));
      createCasterFlare(this, this.player.x, this.player.y - 12, crit ? 0xffd980 : 0x8fdfff, rotation, this.performance.lowEffects);
      createAfterimageTrail(this, this.player.x, this.player.y - 6, rotation, crit ? 0xffdd91 : 0x9ce7ff, this.performance.lowEffects);
      createGroundSigil(this, this.player.x, this.player.y, 0x7fc8ff, 20, 160);
      createProjectileTrail(this, this.player.x, this.player.y - 14, sprite.body.x, sprite.body.y, 0x97ddff);
      createLightningArc(this, this.player.x, this.player.y - 10, sprite.body.x, sprite.body.y, 0xb8e8ff);
      createSparkBurst(this, sprite.body.x, sprite.body.y, crit ? 0xffe7aa : 0x9ae8ff, crit ? 10 : 7, crit ? 52 : 38);
      createRadialWave(this, sprite.body.x, sprite.body.y, crit ? 0xffdb78 : 0x9edfff, crit ? 18 : 12, crit ? 360 : 260);
      createImpactBurst(this, sprite.body.x, sprite.body.y, crit ? 0xe9fbff : 0x8fdbff);
      if (crit) {
        createCritRing(this, sprite.body.x, sprite.body.y);
      }
      triggerEnemyHitFlash(this, sprite, crit);
      animatePlayerSlash(this, this.player, rotation, chainStep);
      createFloatingText(this, sprite.body.x, sprite.body.y - 18, crit ? `CRIT ${damage}` : `${damage}`, crit ? '#ffe27a' : '#ffb873');
      createCombatCallout(
        this,
        sprite.body.x,
        sprite.body.y - 54,
        `${comboCount} COMBO`,
        crit ? 'CRITICAL' : chainStep === 2 ? 'FINISH' : 'CHAIN',
        crit ? '#ffe27a' : '#ffd58b',
      );
      store.applyDamageToEnemy(enemyId, damage);
      store.registerComboHit({ time, damage, critical: crit, chainStep });
      this.handleEnemyDeath(enemyId);
      this.resolveManaAbsorb(enemyId, 9, '흡수');
      store.setCombatText({
        id: `${enemyId}-${time}`,
        value: crit ? `CRIT ${damage}` : `${chainStep + 1} HIT`,
        color: crit ? '#ffe27a' : '#ffba78',
      });
      if (crit) {
        this.flashScreen(0xfff1bf, 0.18, 130);
      }
    }

    private castSkill(enemyId: string | null, skillId: string, rotation: number, time: number) {
      const store = useTheQuestStore.getState();
      const skill = store.skills.find((entry) => entry.id === skillId);
      const enemy = enemyId ? store.enemies.find((entry) => entry.id === enemyId) ?? null : null;
      const sprite = enemyId ? this.enemySprites.get(enemyId) ?? null : null;
      if (!skill) {
        return;
      }
      const readyAt = store.skillCooldowns[skill.id] ?? 0;
      if (Date.now() < readyAt) {
        this.showBlockedSkillFeedback(`${skill.name} 대기중`, time, `cooldown:${skill.id}`);
        return;
      }
      if (store.vitals.mp < skill.manaCost) {
        this.showBlockedSkillFeedback('MP 부족', time, `mana:${skill.id}`);
        return;
      }
      if (skill.role === 'attack' && (!enemy || !sprite || !enemy.alive)) {
        this.showBlockedSkillFeedback('대상 없음', time, `target:${skill.id}`);
        return;
      }

      const speedMultiplier =
        store.consumableEffects.moveSpeedBoostUntil > Date.now() ? store.consumableEffects.moveSpeedMultiplier : 1;
      const cooldownMs = skill.cooldownMs / speedMultiplier;
      const cooldownReadyAt = Date.now() + cooldownMs;
      this.lastAnySkillCastAt = time;
      this.lastSkillCastAt.set(skillId, time + cooldownMs);
      store.setSkillCooldown(skillId, cooldownReadyAt);
      store.setActiveSkill({
        skillId,
        label: skill.name,
        endsAt: time + skill.castTimeMs + 520,
      });
      store.setVitals((vitals) => ({
        ...vitals,
        mp: Math.max(0, vitals.mp - skill.manaCost),
      }));
      createCasterFlare(this, this.player.x, this.player.y - 14, parseInt(skill.color.slice(1), 16), rotation, this.performance.lowEffects);
      createAfterimageTrail(this, this.player.x, this.player.y - 10, rotation, parseInt(skill.color.slice(1), 16), this.performance.lowEffects);
      createCombatCallout(this, this.player.x, this.player.y - 82, skill.name, skill.role === 'buff' ? 'BUFF' : skill.role === 'summon' ? 'SUMMON' : 'CAST', skill.color);

      const spellAmp = this.getSpellAmpMultiplier(Date.now());
      const baseDamage = Math.round((store.vitals.spellPower + skill.damage) * spellAmp);

      if (skillId === 'greater-haste') {
        createGroundSigil(this, this.player.x, this.player.y, 0xcaacff, 34, 360);
        createArcaneShardRing(this, this.player.x, this.player.y, 0xd5b8ff, this.performance.lowEffects);
        createRadialWave(this, this.player.x, this.player.y, 0xd9b8ff, 20, 420);
        store.setConsumableEffects((current) => ({
          ...current,
          arcaneSurgeMultiplier: 1.14,
          arcaneSurgeUntil: Date.now() + (skill.durationMs ?? 10000),
        }));
        store.setCombatText({ id: `${skillId}-${time}`, value: '가속', color: skill.color });
        return;
      }

      if (skillId === 'magic-shield') {
        createGroundSigil(this, this.player.x, this.player.y, 0x8be3ff, 38, 420);
        createRadialWave(this, this.player.x, this.player.y, 0x8fe8ff, 22, 460);
        createFloatingText(this, this.player.x, this.player.y - 26, 'SHIELD', '#9be8ff');
        store.setConsumableEffects((current) => ({
          ...current,
          manaWardReduction: 0.24,
          manaWardUntil: Date.now() + (skill.durationMs ?? 9200),
        }));
        store.setCombatText({ id: `${skillId}-${time}`, value: '피해 감소', color: skill.color });
        return;
      }

      if (skillId === 'summon-skeleton') {
        createGroundSigil(this, this.player.x, this.player.y, 0xc1f0ff, 44, 420);
        createArcaneShardRing(this, this.player.x, this.player.y, 0xe0f7ff, this.performance.lowEffects);
        createOrbitalWisps(this, this.player.x, this.player.y - 10, 0xe7fbff);
        store.setConsumableEffects((current) => ({
          ...current,
          familiarUntil: Date.now() + (skill.durationMs ?? 15000),
          familiarPulseAt: time + 300,
        }));
        store.setCombatText({ id: `${skillId}-${time}`, value: '언데드 소환', color: skill.color });
        return;
      }

      const targets =
        skillId === 'lightning'
          ? store.enemies
              .filter((candidate) => candidate.alive)
              .sort(
                (left, right) =>
                  Math.hypot(left.x - enemy.x, left.y - enemy.y) - Math.hypot(right.x - enemy.x, right.y - enemy.y),
              )
              .slice(0, 3)
          : skillId === 'fireball'
          ? store.enemies.filter((candidate) => candidate.alive && Math.hypot(candidate.x - enemy.x, candidate.y - enemy.y) < 182)
          : [enemy];

      if (skillId === 'fire-arrow') {
        createProjectileTrail(this, this.player.x, this.player.y - 12, sprite.body.x, sprite.body.y, 0xffad74);
        createSparkBurst(this, sprite.body.x, sprite.body.y, 0xffcf9b, 8, 38);
      } else if (skillId === 'ice-dagger') {
        createProjectileTrail(this, this.player.x, this.player.y - 12, sprite.body.x, sprite.body.y, 0xa9efff);
        createGroundSigil(this, sprite.body.x, sprite.body.y, 0x9ae8ff, 28, 260);
        createSparkBurst(this, sprite.body.x, sprite.body.y, 0xdbfbff, 8, 36);
      } else if (skillId === 'lightning') {
        createArcaneShardRing(this, this.player.x, this.player.y, 0xffe08a, this.performance.lowEffects);
        createRadialWave(this, this.player.x, this.player.y, 0xffdb8c, 18, 320);
      } else if (skillId === 'fireball') {
        createGroundSigil(this, enemy.x, enemy.y, 0xff9f74, 54, 420);
        const meteorCount = this.performance.lowEffects ? 2 : 4;
        for (let index = 0; index < meteorCount; index += 1) {
          createMeteorStreak(
            this,
            enemy.x - 104 + index * 40,
            enemy.y - 168 - index * 20,
            enemy.x + index * 6,
            enemy.y,
            0xffb283,
          );
        }
      }

      targets.forEach((target, index) => {
        const targetSprite = this.enemySprites.get(target.id);
        if (!targetSprite) {
          return;
        }
        if (skillId === 'lightning') {
          const sourceX = index === 0 ? this.player.x : targets[index - 1]?.x ?? this.player.x;
          const sourceY = index === 0 ? this.player.y - 12 : targets[index - 1]?.y ?? this.player.y;
          createLightningArc(this, sourceX, sourceY, targetSprite.body.x, targetSprite.body.y, 0xffdf88);
          createSparkBurst(this, targetSprite.body.x, targetSprite.body.y, 0xffebae, 7, 30);
        }
        if (skillId === 'fireball') {
          createMeteorStreak(this, targetSprite.body.x - 68 + index * 18, targetSprite.body.y - 132, targetSprite.body.x, targetSprite.body.y, 0xff9a6f);
          createRadialWave(this, targetSprite.body.x, targetSprite.body.y, 0xffb377, 16, 280);
          createImpactBurst(this, targetSprite.body.x, targetSprite.body.y, 0xffa16f);
        } else {
          createImpactBurst(this, targetSprite.body.x, targetSprite.body.y, skillId === 'fire-arrow' ? 0xffbf8d : 0xc6efff);
        }
        createSparkBurst(
          this,
          targetSprite.body.x,
          targetSprite.body.y,
          skillId === 'fire-arrow' ? 0xffc58d : skillId === 'ice-dagger' ? 0xe8fbff : skillId === 'lightning' ? 0xffefb2 : 0xffc193,
          skillId === 'fireball' ? 11 : 7,
          skillId === 'fireball' ? 44 : 32,
        );
        triggerEnemyHitFlash(this, targetSprite, skillId === 'fireball');
        const hitDamage = Math.round(baseDamage * (skillId === 'lightning' ? 1 - index * 0.16 : 1));
        createFloatingText(this, targetSprite.body.x, targetSprite.body.y - 22, `${hitDamage}`, skill.color);
        store.applyDamageToEnemy(target.id, hitDamage);
        store.registerComboHit({
          time,
          damage: hitDamage,
          critical: skillId === 'fireball',
          chainStep: skillId === 'fireball' ? 2 : 1,
        });
        if (skillId === 'fire-arrow') {
          this.restorePlayerMana(4);
        } else {
          this.resolveManaAbsorb(target.id, skillId === 'ice-dagger' ? 12 : 8, '지팡이');
        }
        this.handleEnemyDeath(target.id);
      });

      store.setCombatText({ id: `${skillId}-${time}`, value: skill.name, color: skill.color });
    }

    private handleEnemyDeath(enemyId: string) {
      const store = useTheQuestStore.getState();
      const enemy = store.enemies.find((entry) => entry.id === enemyId);
      if (!enemy || enemy.hp > 0) {
        return;
      }

      store.rewardEnemyKill(enemyId);
      const sprite = this.enemySprites.get(enemyId);
      const nextState = useTheQuestStore.getState();
      const nextEnemy = nextState.enemies.find((entry) => entry.id === enemyId) ?? enemy;
      if (sprite) {
        createFloatingText(this, sprite.body.x, sprite.body.y - 48, `+${enemy.expReward} EXP`, '#ffe27a');
        createFloatingText(this, sprite.body.x, sprite.body.y - 72, `+${enemy.goldReward}G`, '#ffd07b');
        if (nextState.combat.lastLootLabel) {
          createFloatingText(this, sprite.body.x, sprite.body.y - 96, nextState.combat.lastLootLabel, '#9cf4c6');
        }
        if (nextEnemy.role === 'boss') {
          createCombatCallout(this, sprite.body.x, sprite.body.y - 126, 'PORTAL OPEN', 'BOSS CLEAR', '#ffe2a2');
        }
      }
      if (nextEnemy.role === 'boss') {
        nextState.setLastCombatLog(`${enemy.name} 처치 · 포탈이 개방되었습니다.`);
        this.pendingRespawns.delete(enemyId);
        this.updatePortalState();
        return;
      }
      this.pendingRespawns.set(enemyId, this.time.now + 4500);
    }

    private handleRespawns(time: number) {
      this.pendingRespawns.forEach((readyAt, enemyId) => {
        if (time < readyAt) {
          return;
        }

        this.pendingRespawns.delete(enemyId);
        const enemy = useTheQuestStore.getState().enemies.find((entry) => entry.id === enemyId);
        if (enemy?.role === 'boss') {
          return;
        }
        useTheQuestStore.getState().respawnEnemy(enemyId);
      });
    }

    private triggerPlayerHitFeedback(color: number, warded: boolean) {
      if (this.playerImpactFlash) {
        this.playerImpactFlash.setFillStyle(warded ? 0x9be8ff : blendTint(color, 0xffffff, 0.34), warded ? 0.34 : 0.28);
        this.playerImpactFlash.setAlpha(warded ? 0.34 : 0.28);
        this.tweens.add({
          targets: this.playerImpactFlash,
          alpha: 0,
          duration: warded ? 180 : 140,
          ease: 'Quad.easeOut',
        });
      }
      const auraRing = this.player.getByName('player-aura-ring') as import('phaser').GameObjects.Ellipse | null;
      const runeHalo = this.player.getByName('player-rune-halo') as import('phaser').GameObjects.Arc | null;
      if (auraRing) {
        this.tweens.add({
          targets: auraRing,
          alpha: 0,
          scaleX: warded ? 1.5 : 1.34,
          scaleY: warded ? 1.56 : 1.42,
          duration: warded ? 240 : 180,
          ease: 'Quad.easeOut',
          onComplete: () => {
            auraRing.setAlpha(warded ? 0.1 : 0.04);
            auraRing.setScale(1);
          },
        });
      }
      if (runeHalo) {
        this.tweens.add({
          targets: runeHalo,
          alpha: 0,
          scale: warded ? 1.9 : 1.6,
          duration: warded ? 260 : 180,
          ease: 'Quad.easeOut',
          onComplete: () => {
            runeHalo.setAlpha(0.08);
            runeHalo.setScale(1);
          },
        });
      }
      createSparkBurst(this, this.player.x, this.player.y - 6, warded ? 0xa7efff : 0xffb099, warded ? 8 : 6, warded ? 36 : 28);
      createRadialWave(this, this.player.x, this.player.y - 4, warded ? 0x92e7ff : 0xff9a86, 10, warded ? 260 : 200);
    }

    private refreshMinimap() {
      const store = useTheQuestStore.getState();
      const nextLocation = resolveTheQuestLocationMeta(this.player.x, this.player.y, store.npcs);
      store.setMinimap({
        player: { x: this.player.x, y: this.player.y },
        enemies: store.enemies.map(({ id, x, y, alive }) => ({ id, x, y, alive })),
        npcs: store.npcs.map(({ id, name, x, y }) => ({ id, name, x, y })),
      });
      store.setPlayerLocation(nextLocation);
    }

    private flashScreen(color = 0xffffff, alpha = 0.12, duration = 160) {
      if (!this.attackFlash) {
        return;
      }

      this.attackFlash.setFillStyle(color, alpha);
      this.attackFlash.setAlpha(alpha);
      this.tweens.add({
        targets: this.attackFlash,
        alpha: 0,
        duration,
      });
    }
  }

  const game = new Phaser.Game({
    type: shouldPreferCanvasRenderer() ? Phaser.CANVAS : Phaser.AUTO,
    parent: mountElement,
    transparent: true,
    backgroundColor: '#101b2e',
    render: {
      antialias: window.innerWidth > 960,
      pixelArt: window.innerWidth <= 768,
      powerPreference: shouldPreferCanvasRenderer() ? 'default' : 'high-performance',
    },
    scale: {
      mode: Phaser.Scale.RESIZE,
      autoCenter: Phaser.Scale.CENTER_BOTH,
      width: mountElement.clientWidth,
      height: mountElement.clientHeight,
    },
    physics: {
      default: 'arcade',
      arcade: {
        gravity: { x: 0, y: 0 },
        debug: false,
      },
    },
    scene: [TheQuestScene],
  });
  const scene = game.scene.getScene('TheQuestScene') as TheQuestScene;

  const resizeObserver = new ResizeObserver(() => {
    const scaleManager = game.scale;
    scaleManager.resize(mountElement.clientWidth, mountElement.clientHeight);
  });
  resizeObserver.observe(mountElement);

  return {
    destroy: () => {
      resizeObserver.disconnect();
      game.destroy(true);
    },
    requestManualSkillCast: (skillId) => {
      scene.requestManualSkillCast(skillId);
    },
  };
}
