import Phaser from 'phaser';
import { audio } from '../AudioDirector';
import { director } from '../CorruptionDirector';
import { ANNOUNCEMENTS, C9_LINES, NPCS, type EndingId, type NpcId } from '../data';
import type { DialogueChoice, UIScene } from './UIScene';

interface SceneData { spawn?: string; dream?: boolean }
interface MapProperty { name: string; value: string | number | boolean }
interface MapObject extends Phaser.Types.Tilemaps.TiledObject { type: string; properties?: MapProperty[] }

const NPC_IDS = new Set(Object.keys(NPCS));
const STAGE_SATURATION = { S0: 0.9, S1: 1, S2: 1.15, S3: 1.3, S4: 1.4 } as const;

export class WorldScene extends Phaser.Scene {
  private spawn = 'default';
  private dream = false;
  private player!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
  private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
  private wasd!: Record<'up' | 'down' | 'left' | 'right', Phaser.Input.Keyboard.Key>;
  private objects: MapObject[] = [];
  private npcs = new Map<string, Phaser.GameObjects.Sprite>();
  private extras: Phaser.GameObjects.Sprite[] = [];
  private nearest?: MapObject;
  private interacting = false;
  private idleSince = 0;
  private movedAfterIdle = true;
  private rainGraphics?: Phaser.GameObjects.Graphics;
  private rainSince?: number;
  private t14Start?: number;
  private cheerTimer?: Phaser.Time.TimerEvent;
  private cheerSprites: Phaser.GameObjects.Sprite[] = [];
  private reflection?: Phaser.GameObjects.Sprite;
  private reflectionHistory: Array<{ x: number; y: number; frame: number }> = [];
  private colorFx?: Phaser.FX.ColorMatrix;
  private vignetteFx?: Phaser.FX.Vignette;

  constructor(key: string) { super(key); }

  init(data: SceneData): void {
    this.spawn = data.spawn ?? 'default';
    this.dream = data.dream ?? false;
  }

  create(): void {
    const map = this.make.tilemap({ key: this.scene.key });
    const tileset = map.addTilesetImage('honey-village', 'tiles');
    if (!tileset) throw new Error(`Tileset missing in ${this.scene.key}`);
    map.createLayer('ground', tileset, 0, 0)?.setDepth(0);
    this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
    this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels).setRoundPixels(true);
    this.objects = (map.getObjectLayer('objects')?.objects ?? []) as MapObject[];
    this.createCollision(map);
    this.createNpcs();
    const spawn = this.objects.find((object) => object.type === 'spawn' && this.prop(object, 'id') === this.spawn)
      ?? this.objects.find((object) => object.type === 'spawn');
    this.player = this.physics.add.sprite(spawn?.x ?? 32, spawn?.y ?? 32, 'characters', 0).setDepth(20);
    this.player.body.setSize(10, 8).setOffset(3, 15);
    this.player.setCollideWorldBounds(true);
    this.cameras.main.startFollow(this.player, true, 0.12, 0.12);
    this.createInputs();
    this.createReflection();
    this.setupPostFx();
    this.bindDirectorEvents();
    this.applyCorruption();
    this.idleSince = performance.now();
    if (this.scene.key === 'WhiteScene' && this.dream) this.playDreamFlash();
  }

  private createCollision(map: Phaser.Tilemaps.Tilemap): void {
    const group = this.physics.add.staticGroup();
    const obstacles = (map.getObjectLayer('obstacles')?.objects ?? []) as MapObject[];
    obstacles.forEach((object) => {
      if (!this.prop(object, 'collides')) return;
      const block = this.add.rectangle((object.x ?? 0) + (object.width ?? 0) / 2, (object.y ?? 0) + (object.height ?? 0) / 2, object.width ?? 0, object.height ?? 0, 0, 0);
      this.physics.add.existing(block, true); group.add(block);
    });
    this.events.once(Phaser.Scenes.Events.CREATE, () => this.physics.add.collider(this.player, group));
    this.time.delayedCall(0, () => this.physics.add.collider(this.player, group));
  }

  private createInputs(): void {
    const keyboard = this.input.keyboard;
    if (!keyboard) throw new Error('Keyboard input unavailable');
    this.cursors = keyboard.createCursorKeys();
    const keys = keyboard.addKeys('W,S,A,D') as Record<'W' | 'S' | 'A' | 'D', Phaser.Input.Keyboard.Key>;
    this.wasd = { up: keys.W, down: keys.S, left: keys.A, right: keys.D };
    keyboard.on('keydown-E', () => void this.interact());
  }

  private createNpcs(): void {
    this.objects.filter((object) => object.type === 'npc').forEach((object, index) => {
      const id = String(this.prop(object, 'id'));
      if (id === 'birds') {
        const left = this.add.sprite(object.x ?? 0, object.y ?? 0, 'characters', 42).setDepth(18).play('birds-sing');
        const right = this.add.sprite((object.x ?? 0) + 13, object.y ?? 0, 'characters', 44).setDepth(18).play('birds-sing');
        this.npcs.set(id, left); this.npcs.set('birds-right', right);
        return;
      }
      const info = NPCS[id as NpcId];
      const frame = info?.frame ?? 37 + (index % 5);
      const sprite = this.add.sprite(object.x ?? 0, object.y ?? 0, 'characters', frame).setDepth(18);
      if (id.startsWith('extra')) {
        this.extras.push(sprite);
        this.tweens.add({ targets: sprite, x: sprite.x + (index % 2 ? 22 : -22), duration: 1800 + index * 140, yoyo: true, repeat: -1, ease: 'Linear' });
      }
      this.npcs.set(id, sprite);
    });
  }

  private createReflection(): void {
    if (this.scene.key !== 'LakeScene' || director.snapshot.day < 2) return;
    this.reflection = this.add.sprite(this.player?.x ?? 0, 88, 'characters', 0).setAlpha(0.35).setTint(0x89dbe8).setFlipY(true).setDepth(5);
  }

  private setupPostFx(): void {
    if (this.game.renderer.type !== Phaser.WEBGL) return;
    this.colorFx = this.cameras.main.postFX.addColorMatrix();
    this.vignetteFx = this.cameras.main.postFX.addVignette(0.5, 0.5, 0, 0.3);
  }

  private bindDirectorEvents(): void {
    director.on('change', this.applyCorruption, this);
    director.on('negative-flash', this.negativeFlash, this);
    director.on('first-full', this.firstFull, this);
    director.on('sad-behavior', this.turnNearestNpc, this);
    director.on('forced-cheer', this.startForcedCheer, this);
    director.on('debug-teleport', this.debugTeleport, this);
    director.on('force-ending', this.startEnding, this);
    this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
      director.off('change', this.applyCorruption, this);
      director.off('negative-flash', this.negativeFlash, this);
      director.off('first-full', this.firstFull, this);
      director.off('sad-behavior', this.turnNearestNpc, this);
      director.off('forced-cheer', this.startForcedCheer, this);
      director.off('debug-teleport', this.debugTeleport, this);
      director.off('force-ending', this.startEnding, this);
      this.ui().setPrompt();
    });
  }

  update(_time: number, delta: number): void {
    if (!this.player?.body) return;
    director.tick();
    this.updateMovement();
    this.updateNearest();
    this.updateIdle();
    this.updateT14();
    this.updateRain(delta);
    this.updateReflection();
    this.updateNpcFacing();
  }

  private updateMovement(): void {
    const locked = this.interacting || director.snapshot.dialogueOpen || director.snapshot.t14Active || (this.scene.key === 'WhiteScene' && this.dream);
    const left = this.cursors.left.isDown || this.wasd.left.isDown;
    const right = this.cursors.right.isDown || this.wasd.right.isDown;
    const up = this.cursors.up.isDown || this.wasd.up.isDown;
    const down = this.cursors.down.isDown || this.wasd.down.isDown;
    let vx = locked ? 0 : Number(right) - Number(left);
    let vy = locked ? 0 : Number(down) - Number(up);
    if (vx && vy) { vx *= Math.SQRT1_2; vy *= Math.SQRT1_2; }
    this.player.setVelocity(vx * 96, vy * 96);
    if (vx < 0) this.player.play('walk-left', true);
    else if (vx > 0) this.player.play('walk-right', true);
    else if (vy < 0) this.player.play('walk-up', true);
    else if (vy > 0) this.player.play('walk-down', true);
    else { this.player.stop(); this.player.setFrame(this.idleFrame()); }
    if (vx || vy) { this.idleSince = performance.now(); this.movedAfterIdle = true; }
  }

  private idleFrame(): number {
    const key = this.player.anims.currentAnim?.key;
    return key === 'walk-up' ? 4 : key === 'walk-left' ? 8 : key === 'walk-right' ? 12 : 0;
  }

  private updateNearest(): void {
    if (director.snapshot.dialogueOpen) { this.ui().setPrompt(); return; }
    let distance = Number.POSITIVE_INFINITY;
    let nearest: MapObject | undefined;
    this.objects.filter((object) => object.type === 'door' || object.type === 'npc' || object.type === 'interact').forEach((object) => {
      const current = Phaser.Math.Distance.Between(this.player.x, this.player.y, object.x ?? 0, object.y ?? 0);
      if (current <= 24 && current < distance) { distance = current; nearest = object; }
    });
    this.nearest = nearest;
    this.ui().setPrompt(nearest ? String(this.prop(nearest, 'label') || (nearest.type === 'door' ? '离开' : '交互')) : undefined);
  }

  private updateIdle(): void {
    if (this.player.body.speed > 0 || director.snapshot.dialogueOpen || this.nearest || director.snapshot.t14Active) return;
    if (this.movedAfterIdle && performance.now() - this.idleSince >= 10_000) {
      if (director.sadBehavior('idle')) this.ui().toast('你发了一会儿呆。');
      this.movedAfterIdle = false;
    }
  }

  private updateT14(): void {
    if (!this.t14Start) return;
    const seconds = Math.min(60, (performance.now() - this.t14Start) / 1000);
    this.cameras.main.setZoom(1 + 0.28 * seconds / 60);
    audio.fadeForSmile(seconds);
    this.npcs.forEach((sprite, id) => { if (!id.startsWith('birds')) sprite.setFlipX(sprite.x > this.player.x); });
    if (seconds >= 60) {
      this.t14Start = undefined;
      director.setT14(false);
      audio.restore();
      this.cameras.main.setZoom(1);
      director.completeTask('T14');
      this.ui().toast('你保持了整整 60 秒的微笑。', 2500);
    }
  }

  private updateRain(_delta: number): void {
    const raining = director.snapshot.day === 6 && director.snapshot.sunset;
    if (!raining) { this.rainGraphics?.clear(); this.rainSince = undefined; return; }
    this.rainGraphics ??= this.add.graphics().setScrollFactor(0).setDepth(50);
    this.rainGraphics.clear().lineStyle(1, 0xb8e7f2, 0.75);
    for (let i = 0; i < 45; i += 1) {
      const x = (i * 43 + Math.floor(performance.now() / 12)) % 340 - 10;
      const y = (i * 29 + Math.floor(performance.now() / 7)) % 190 - 10;
      this.rainGraphics.lineBetween(x, y, x - 3, y + 8);
    }
    if (this.player.body.speed > 0) { this.rainSince = undefined; return; }
    this.rainSince ??= performance.now();
    if (performance.now() - this.rainSince >= 10_000 && director.sadBehavior('rain')) this.ui().toast('你在雨里站了十秒。');
  }

  private updateReflection(): void {
    if (!this.reflection) return;
    this.reflectionHistory.push({ x: this.player.x, y: this.player.y, frame: Number(this.player.frame.name) });
    const delayed = this.reflectionHistory.length > 1 ? this.reflectionHistory.shift() : undefined;
    if (!delayed) return;
    this.reflection.setPosition(delayed.x, 106 + (106 - delayed.y) * 0.15).setFrame(delayed.frame);
  }

  private updateNpcFacing(): void {
    if (director.stage === 'S3' || director.stage === 'S4') this.npcs.forEach((sprite, id) => { if (!id.startsWith('birds')) sprite.setFlipX(sprite.x > this.player.x); });
    if (director.stage === 'S4' && this.extras.length) this.tweens.killTweensOf(this.extras[0]);
    if (director.stage === 'S4' && Math.random() < 0.6 / 3600) {
      const ids = [...this.npcs.keys()].filter((id): id is NpcId => id in NPCS && id !== 'doctor' && id !== 'turtle' && id !== 'birds');
      const id = Phaser.Utils.Array.GetRandom(ids);
      const sprite = id ? this.npcs.get(id) : undefined;
      if (id && sprite) {
        sprite.setFrame(NPCS[id].frame + 2);
        this.time.delayedCall(100, () => sprite.active && sprite.setFrame(NPCS[id].frame + 1));
      }
    }
  }

  private async interact(): Promise<void> {
    if (!this.nearest || this.interacting || director.snapshot.dialogueOpen) return;
    this.interacting = true;
    try {
      if (this.nearest.type === 'door') this.useDoor(this.nearest);
      else if (this.nearest.type === 'npc') await this.interactNpc(String(this.prop(this.nearest, 'id')));
      else await this.interactPoint(String(this.prop(this.nearest, 'id')));
    } finally { this.interacting = false; }
  }

  private useDoor(object: MapObject): void {
    if (director.snapshot.cheerActive) director.escapeCheer();
    const target = String(this.prop(object, 'target'));
    const spawn = String(this.prop(object, 'spawn') || 'default');
    this.scene.start(target, { spawn });
  }

  private async interactNpc(id: string): Promise<void> {
    if (id.startsWith('extra')) { await this.say('村民', '今天也是完美的一天。', 0); return; }
    if (!NPC_IDS.has(id)) return;
    const npcId = id as NpcId;
    director.recordDialogue(id);
    if (await this.handleNpcTask(npcId)) return;
    if (npcId === 'turtle' && C9_LINES[director.snapshot.day]) {
      await this.say('龟爷爷', C9_LINES[director.snapshot.day], NPCS.turtle.portrait);
      director.setFlag(`c9-${director.snapshot.day}`, true);
      if (director.snapshot.day === 6) director.discoverClue('C9');
      this.checkGreeting(id);
      return;
    }
    const lines: Record<NpcId, string> = {
      afu: '今天也要把幸福装满哦。', yuanyuan: '刚出炉的蜂蜜面包最甜啦。', dabao: '每一封信都会准时送到。',
      mimi: '货架永远整整齐齐，真让人开心。', doctor: '村里有诊所，但你不需要去那里哦。',
      birds: '啾、啾。旋律每天都一样好听。', turtle: '湖水一直很平静。',
    };
    const replayGreeting = director.snapshot.day === 4 && director.isTaskActive('T10');
    const choice = await this.say(NPCS[npcId].name, lines[npcId], NPCS[npcId].portrait, true, undefined, replayGreeting);
    this.applyChoice(choice, id);
    if (npcId === 'birds') director.smallInteraction('feed');
    if (npcId === 'afu') director.smallInteraction('pet');
    this.checkGreeting(id);
  }

  private async handleNpcTask(id: NpcId): Promise<boolean> {
    const breadTask = director.snapshot.day === 4 ? 'T8' : 'T1';
    const greetTask = director.snapshot.day === 4 ? 'T10' : 'T3';
    if (id === 'yuanyuan' && director.isTaskActive(breadTask) && !director.flag(`${breadTask}-picked`)) {
      director.setFlag(`${breadTask}-picked`);
      await this.say('圆圆', '蜂蜜面包刚刚出炉。请趁热送给大家吧。', NPCS.yuanyuan.portrait, false, undefined, director.snapshot.day === 4);
      this.checkGreeting(id, greetTask); return true;
    }
    if (director.isTaskActive(breadTask) && director.flag(`${breadTask}-picked`) && ['dabao', 'mimi', 'turtle'].includes(id) && !director.flag(`${breadTask}-${id}`)) {
      director.setFlag(`${breadTask}-${id}`);
      await this.say(NPCS[id].name, '谢谢你。今天的面包也和昨天一样甜。', NPCS[id].portrait, false, undefined, director.snapshot.day === 4);
      if (director.increment(`${breadTask}-delivered`, 3) === 3) director.completeTask(breadTask);
      this.checkGreeting(id, greetTask); return true;
    }
    if (id === 'dabao' && director.isTaskActive('T4') && !director.flag('T4-picked')) {
      director.setFlag('T4-picked');
      await this.say('大宝', '三封信。圆圆、咪咪、芙医生。邮戳都已经盖好了。', NPCS.dabao.portrait);
      return true;
    }
    if (director.isTaskActive('T4') && director.flag('T4-picked') && ['yuanyuan', 'mimi', 'doctor'].includes(id) && !director.flag(`T4-${id}`)) {
      director.setFlag(`T4-${id}`);
      await this.say(NPCS[id].name, '信收到了。谢谢你。', NPCS[id].portrait);
      const count = director.increment('T4-delivered', 3);
      if (count === 3) {
        this.ui().toast('署名：PT-0▓', 200);
        await this.wait(200);
        director.discoverClue('C4');
        director.completeTask('T4');
      }
      return true;
    }
    if (id === 'yuanyuan' && director.isTaskActive('T11')) {
      const choice = await this.say('圆圆', '来帮我揉面吧。只要一直微笑，面团就会很柔软。', NPCS.yuanyuan.portrait, false, [
        { id: 'gentle', label: '好，我来帮忙。' }, { id: 'hesitate', label: '让我想一想。' }, { id: 'negative', label: '我不想做。' },
      ]);
      if (choice === 'negative') director.refuseTask('T11');
      else { if (choice === 'hesitate') director.adjustAwareness(2); director.completeTask('T11'); }
      return true;
    }
    return false;
  }

  private checkGreeting(id: string, forcedTask?: string): void {
    const task = forcedTask ?? (director.snapshot.day === 4 ? 'T10' : 'T3');
    if (!director.isTaskActive(task) || director.flag(`${task}-greet-${id}`)) return;
    director.setFlag(`${task}-greet-${id}`);
    if (director.increment(`${task}-greetings`, 3) === 3) director.completeTask(task);
  }

  private async interactPoint(id: string): Promise<void> {
    if (id === 'board') { await this.interactBoard(); return; }
    if (id === 'bed') { await this.interactBed(); return; }
    if (id === 'calendar') {
      if (director.snapshot.day < 2) await this.say('日历', '今天的日期旁画着一朵向日葵。');
      else { await this.say('日历', '每一页都是同一个日期'); director.discoverClue('C1'); }
      return;
    }
    if (id === 'recipe') {
      if (director.snapshot.day < 3) await this.say('配方纸', '纸张背面粘着糖霜。');
      else { await this.say('配方纸', '手写小字：「按疗程配比。甜度：最大。」'); director.discoverClue('C2'); }
      return;
    }
    if (id === 'mail') {
      if (director.snapshot.day < 2) await this.say('散落信件', '几封还没有投递的信。');
      else { await this.say('散落信件', '所有邮戳是同一天'); director.discoverClue('C3'); }
      return;
    }
    if (id === 'shelf') {
      await this.say('货架', '同一商品重复排列，标签全部空白');
      if (director.snapshot.day >= 2) director.discoverClue('C5');
      if (director.isTaskActive('T5')) director.completeTask('T5');
      return;
    }
    if (id.startsWith('flower')) {
      const task = director.snapshot.day === 4 ? 'T9' : 'T2';
      if (director.isTaskActive(task)) this.handleRepeatedPoint(id, task, 'flowers', 3, '你给花坛浇了水。');
      else if (director.smallInteraction('water')) this.ui().toast('你给花浇了一点水。');
      else this.ui().toast('今天的花已经喝饱了。');
      return;
    }
    if (id.startsWith('berry')) { this.handleRepeatedPoint(id, 'T6', 'berries', 5, '你采下一把莓果。'); return; }
    if (id.startsWith('ribbon')) { this.handleRepeatedPoint(id, 'T13', 'ribbons', 4, '彩带被挂得笔直。'); return; }
    if (id === 'picnic' && director.isTaskActive('T7')) {
      await this.say('湖畔', ['野餐布上的餐具摆得一丝不差。', '湖中的倒影晚了一帧才坐下。']);
      director.completeTask('T7'); return;
    }
    if (id === 'well') { await this.interactWell(); return; }
    if (id === 'form') {
      if (director.snapshot.day < 5) await this.say('芙医生', '桌上的表格还没有填写，请别碰哦。', NPCS.doctor.portrait);
      else { await this.say('表格', '芙医生桌上的表格抬头被手肘挡住，只露出「…谷情绪疗…」'); director.discoverClue('C8'); }
      return;
    }
    if (id === 'mist') { await this.interactMist(); return; }
    if (id === 'festival') { await this.interactFestival(); return; }
    if (id === 'oven') await this.say('烤炉', '炉火永远保持同样的温度。');
    if (id === 'lake') await this.say('湖面', '倒影跟着你。慢了一帧。');
  }

  private async interactBoard(): Promise<void> {
    const day = director.snapshot.day;
    if (!director.snapshot.boardRead) {
      await this.say('阿福', ANNOUNCEMENTS[day], NPCS.afu.portrait, false, undefined, day === 4);
      director.acceptDay();
      return;
    }
    if (day === 5 && director.isTaskActive('T12')) {
      const choice = await this.say('公告板', '木板需要擦得干干净净。', undefined, false, [
        { id: 'gentle', label: '认真擦拭。' }, { id: 'hesitate', label: '停下来看看旧字。' }, { id: 'negative', label: '拒绝擦拭。' },
      ]);
      if (choice === 'negative') director.refuseTask('T12');
      else {
        if (choice === 'hesitate') director.adjustAwareness(2);
        await this.say('旧字迹', '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」');
        director.discoverClue('C6'); director.completeTask('T12');
      }
      return;
    }
    const tasks = director.tasksForDay().map((task) => `${task.id}  ${task.title}`).join('\n');
    await this.say('今日幸福任务', tasks || '今天没有新的任务。');
  }

  private async interactBed(): Promise<void> {
    if (!director.snapshot.sunset) { await this.say('床', '现在还不到睡觉时间。'); return; }
    const oldDay = director.snapshot.day;
    await this.say('梦境', oldDay >= 3 ? ['你闭上眼睛。', '白色墙壁闪过一秒。'] : '你闭上眼睛。甜味沉进梦里。');
    this.cameras.main.fadeOut(450, 255, 245, 220);
    this.time.delayedCall(500, () => {
      director.advanceDay();
      if (oldDay >= 3) this.scene.start('WhiteScene', { dream: true });
      else this.scene.restart({ spawn: 'default' });
    });
  }

  private async interactWell(): Promise<void> {
    if (director.snapshot.day < 5) { await this.say('阿福', '井边有点滑。等自由日再来吧。', NPCS.afu.portrait); return; }
    await this.say('古井', '你向井底看去。五秒里，只有自己的呼吸。');
    this.ui().toast('注视古井……', 5000);
    await this.wait(5000);
    if (!director.flag('well-gaze')) { director.setFlag('well-gaze'); director.adjustAwareness(3); }
    audio.beep('monitor'); director.discoverClue('C7');
  }

  private async interactMist(): Promise<void> {
    if (director.snapshot.day < 5) { await this.say('雾墙', '一位村民温柔地摇头。现在还不能靠近。'); return; }
    if (director.snapshot.day === 7 && director.snapshot.festivalStarted && director.ending() === 'E3') { this.startEnding('E3'); return; }
    await this.say('雾墙', '雾在五秒里维持着完全相同的形状。');
    this.ui().toast('注视雾墙……', 5000);
    await this.wait(5000);
    if (!director.flag('mist-gaze')) { director.setFlag('mist-gaze'); director.adjustAwareness(3); }
  }

  private async interactFestival(): Promise<void> {
    if (director.isTaskActive('T14')) {
      if (!director.snapshot.completed.has('T13')) { await this.say('庆典场地', '彩带还没有挂完。'); return; }
      await this.say('阿福', '请站在这里，保持微笑 60 秒。', NPCS.afu.portrait);
      director.setT14(true); this.t14Start = performance.now(); this.ui().toast('保持微笑。', 2000); return;
    }
    if (!director.isTaskActive('T15')) { await this.say('庆典场地', '这里正等待第 413 届丰收庆典。'); return; }
    director.completeTask('T15'); director.setFestivalStarted();
    const ending = director.ending();
    if (ending === 'E3') { await this.say('雾墙', '庆典的歌声里，雾墙裂开了一条白色的缝。'); this.ui().toast('走向雾墙。', 3000); }
    else if (ending) this.startEnding(ending);
    else await this.say('庆典场地', '庆典仍在等待幸福抵达满格。');
  }

  private handleRepeatedPoint(id: string, task: string, counter: string, total: number, message: string): void {
    if (!director.isTaskActive(task)) { this.ui().toast('这里暂时没有任务。'); return; }
    if (director.flag(`${task}-${id}`)) { this.ui().toast('这里已经完成了。'); return; }
    director.setFlag(`${task}-${id}`);
    this.ui().toast(message);
    if (director.increment(`${task}-${counter}`, total) === total) director.completeTask(task);
  }

  private async say(speaker: string, text: string | string[], portrait?: number, defaultChoices = false, choices?: Array<{ id: DialogueChoice; label: string }>, clean = false): Promise<DialogueChoice | undefined> {
    const options = choices ?? (defaultChoices ? [
      { id: 'gentle' as const, label: '温柔地回应。' }, { id: 'hesitate' as const, label: '迟疑片刻。' }, { id: 'negative' as const, label: '否定这句话。' },
    ] : undefined);
    return this.ui().showDialogue({ speaker, text, portrait, choices: options, clean });
  }

  private applyChoice(choice: DialogueChoice | undefined, npc: string): void {
    if (choice === 'gentle') director.adjustHappiness(2);
    if (choice === 'hesitate') director.adjustAwareness(2);
    if (choice === 'negative') director.sadBehavior(`negative-${npc}-${director.increment(`negative-${npc}`)}`);
  }

  private applyCorruption = (): void => {
    const saturation = STAGE_SATURATION[director.stage];
    this.colorFx?.reset().saturate(saturation - 1);
    if (this.vignetteFx) {
      this.vignetteFx.strength = director.stage === 'S4' ? 0.55 : director.stage === 'S3' ? 0.28 : 0;
      this.vignetteFx.radius = 0.72;
    }
    this.npcs.forEach((sprite, id) => {
      if (id.startsWith('birds')) {
        sprite.anims.timeScale = director.stage === 'S2' ? 0.8 : director.stage === 'S3' ? 0.55 : 1;
      }
      if (!(id in NPCS)) return;
      const npcId = id as NpcId;
      const expression = director.npcExpression(npcId);
      const offset = expression === 'flat' ? 1 : expression === 'off' ? 2 : 0;
      if (npcId !== 'birds') sprite.setFrame(NPCS[npcId].frame + offset);
    });
  };

  private negativeFlash = (): void => {
    this.colorFx?.negative();
    this.time.delayedCall(17, () => this.applyCorruption());
  };

  private firstFull = (): void => {
    this.cameras.main.flash(1000, 255, 250, 210);
    this.ui().toast('幸福溢出了。', 1800);
  };

  private turnNearestNpc = (): void => {
    const nearest = [...this.npcs.values()].sort((a, b) => Phaser.Math.Distance.Between(a.x, a.y, this.player.x, this.player.y) - Phaser.Math.Distance.Between(b.x, b.y, this.player.x, this.player.y))[0];
    if (!nearest) return;
    nearest.setFlipX(nearest.x > this.player.x);
    nearest.setTint(0xfff1c2);
    this.time.delayedCall(1000, () => nearest.active && nearest.clearTint());
  };

  private startForcedCheer = (): void => {
    this.cameras.main.flash(500, 255, 224, 120);
    this.cheerSprites.forEach((sprite) => sprite.destroy());
    this.cheerSprites = [];
    for (let i = 0; i < 6; i += 1) this.cheerSprites.push(this.add.sprite(this.player.x + Math.cos(i) * 38, this.player.y + Math.sin(i) * 28, 'characters', 37 + (i % 5)).setDepth(19));
    this.cheerTimer?.remove();
    this.cheerTimer = this.time.delayedCall(15_000, () => {
      this.cheerSprites.forEach((sprite) => sprite.destroy()); this.cheerSprites = [];
      director.finishCheer();
    });
  };

  private debugTeleport = (key: string): void => { if (key && key !== this.scene.key) this.scene.start(key, { spawn: 'default' }); };
  private startEnding = (ending: EndingId): void => { this.scene.start('EndingScene', { ending }); };

  private playDreamFlash(): void {
    this.player.setVisible(false);
    this.cameras.main.flash(250, 255, 255, 255);
    this.add.text(160, 82, '白色病房。', { fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '9px', color: '#626a6c' }).setOrigin(0.5).setScrollFactor(0).setDepth(60);
    this.time.delayedCall(1000, () => this.scene.start('HouseScene', { spawn: 'default' }));
  }

  private prop(object: MapObject, name: string): string | number | boolean | undefined {
    return object.properties?.find((property) => property.name === name)?.value;
  }

  private wait(ms: number): Promise<void> { return new Promise((resolve) => this.time.delayedCall(ms, resolve)); }
  private ui(): UIScene { return this.scene.get('UIScene') as UIScene; }
}
