// 地图场景基类：每张 Tiled 地图一个 Scene（PRD §12）
// 移动 96px/s、相机钳制、交互（≤24px 出「按 E」）、任务/线索/悲伤行为/侵蚀表现在此落地
import Phaser from "phaser";
import { director, NPC_OFFSET } from "../core/director";
import { audio } from "../core/audio";
import { UIScene, UiChoice } from "./UIScene";
import {
  ANNOUNCEMENTS, NPC_TALK, QUEST_LINES, CLUE_TEXT, DISSUADE, MISC, DialogChoice,
} from "../data/story";

const SPEED = 96;
const INTERACT_DIST = 24;
const NAMED = ["afu", "yuanyuan", "dabao", "mimi", "fu", "gui"];
const OUTDOOR = ["plaza", "lake", "field", "well", "fogwall"];

interface Poi { name: string; x: number; y: number; clue?: string; }
interface NpcSprite {
  id: string; sprite: Phaser.GameObjects.Sprite; x0: number; y0: number;
  patrol?: number; dir: number; nextBlink: number; offUntil: number; blinkUntil: number;
}

export class WorldScene extends Phaser.Scene {
  mapKey: string;
  protected map!: Phaser.Tilemaps.Tilemap;
  protected obstacles!: Phaser.Tilemaps.TilemapLayer;
  protected ground!: Phaser.Tilemaps.TilemapLayer;
  protected player!: Phaser.Physics.Arcade.Sprite;
  protected npcs: NpcSprite[] = [];
  protected pois: Poi[] = [];
  private keys!: Record<string, Phaser.Input.Keyboard.Key>;
  private transitioning = false;
  private colorFx!: Phaser.FX.ColorMatrix;
  private vignetteFx!: Phaser.FX.Vignette;
  private lastMoveAt = 0;
  private rainStillAt = 0;
  private nearWellSince = 0;
  private nearFogSince = 0;
  private dissuaded = false;
  private reflection?: Phaser.GameObjects.Sprite;
  private prevPos = { x: 0, y: 0 };
  private rain?: Phaser.GameObjects.Particles.ParticleEmitter;
  private t14Active = false;
  private t14Start = 0;
  private t14Faded = false;
  private celebTimer?: Phaser.Time.TimerEvent;
  private buntings: Phaser.GameObjects.Image[] = [];
  private interactCooldown = 0;

  constructor(key: string) { super(key); this.mapKey = key; }

  get ui(): UIScene { return this.scene.get("ui") as UIScene; }
  get isWhite(): boolean { return this.mapKey.startsWith("white"); }

  create() {
    director.currentScene = this.mapKey;
    this.transitioning = false;
    this.npcs = []; this.pois = []; this.buntings = [];
    this.t14Active = false; this.dissuaded = false;

    this.map = this.make.tilemap({ key: "map_" + this.mapKey });
    const ts = this.map.addTilesetImage("tiles", "tiles")!;
    this.ground = this.map.createLayer("ground", ts)!;
    this.obstacles = this.map.createLayer("obstacles", ts)!;
    this.obstacles.setCollisionByExclusion([-1]);

    // 玩家
    const spawn = this.findSpawn(director.pendingSpawn) ?? this.findSpawn("entry") ?? { x: 40, y: 40 };
    this.player = this.physics.add.sprite(spawn.x, spawn.y, "player", 0);
    this.player.setSize(10, 8).setOffset(3, 16).setCollideWorldBounds(true);
    this.player.setDepth(spawn.y);
    this.physics.world.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
    this.physics.add.collider(this.player, this.obstacles);
    this.prevPos = { x: spawn.x, y: spawn.y };

    // 相机：跟随并钳制地图边界
    const cam = this.cameras.main;
    // 相机钳制地图边界；地图小于视口时对称扩展 bounds 使其居中
    const mw = this.map.widthInPixels, mh = this.map.heightInPixels;
    cam.setBounds(
      mw < 320 ? (mw - 320) / 2 : 0,
      mh < 180 ? (mh - 180) / 2 : 0,
      Math.max(mw, 320),
      Math.max(mh, 180),
    );
    cam.startFollow(this.player, true);
    cam.setRoundPixels(true);

    // 对象层
    const objLayer = this.map.getObjectLayer("objects");
    for (const o of objLayer?.objects ?? []) {
      const type = (o as any).type || (o as any).class || "";
      const props: Record<string, any> = {};
      for (const p of (o.properties as any[]) ?? []) props[p.name] = p.value;
      if (type === "door") {
        const zone = this.add.zone(o.x! + o.width! / 2, o.y! + o.height! / 2, o.width!, o.height!);
        this.physics.add.existing(zone, true);
        this.physics.add.overlap(this.player, zone, () => this.goTo(props.target, props.spawn));
      } else if (type === "npc") {
        this.spawnNpc(o.name!, o.x!, o.y!, props.patrol);
      } else if (type === "poi") {
        this.pois.push({ name: o.name!, x: o.x!, y: o.y!, clue: props.clue });
      }
    }

    // 恢复日内世界状态（花坛/莓果/彩带）
    this.restoreTiles();
    // Day 7：庆典布置
    if (director.day === 7 && this.mapKey === "plaza") this.setupCelebrationDay();
    // 湖面倒影
    if (this.mapKey === "lake") {
      this.reflection = this.add.sprite(0, 0, "player", 0).setFlipY(true).setAlpha(0.3).setDepth(1);
    }
    // Day 6 傍晚脚本雨（室外，NPC 假装看不见）
    if (director.day === 6 && director.dusk && OUTDOOR.includes(this.mapKey)) this.startRain();

    this.keys = this.input.keyboard!.addKeys("W,A,S,D,UP,DOWN,LEFT,RIGHT,E,ENTER") as any;

    // 侵蚀矩阵 → 相机 postFX
    this.colorFx = this.cameras.main.postFX.addColorMatrix();
    this.vignetteFx = this.cameras.main.postFX.addVignette(0.5, 0.5, 0.9, 0);
    this.applyStageVisuals();
    audio.applyStage();

    director.on("stage", this.onStage, this);
    director.on("negflash", this.onNegFlash, this);
    director.on("sadact", this.onSadAct, this);
    director.on("forcedCelebration", this.onForcedCelebration, this);
    director.on("dusk", this.onDuskWorld, this);
    this.events.on("shutdown", () => {
      director.off("stage", this.onStage, this);
      director.off("negflash", this.onNegFlash, this);
      director.off("sadact", this.onSadAct, this);
      director.off("forcedCelebration", this.onForcedCelebration, this);
      director.off("dusk", this.onDuskWorld, this);
      if (director.celebrationActive) director.celebrationEnd(true); // 从场景边缘走离 = 逃离
    });

    this.lastMoveAt = this.time.now;
    this.nearWellSince = this.time.now;
    this.nearFogSince = this.time.now;
    this.rainStillAt = this.time.now;
    this.ui?.forceClose();
    this.ui?.fadeIn(400);
  }

  // ---------- 基础 ----------
  private findSpawn(name: string): { x: number; y: number } | null {
    const objLayer = this.map.getObjectLayer("objects");
    let first: { x: number; y: number } | null = null;
    for (const o of objLayer?.objects ?? []) {
      const type = (o as any).type || (o as any).class || "";
      if (type !== "spawn") continue;
      if (!first) first = { x: o.x!, y: o.y! };
      if (o.name === name) return { x: o.x!, y: o.y! };
    }
    return name === "entry" ? first : null;
  }

  private spawnNpc(id: string, x: number, y: number, patrol?: number) {
    if (director.day === 7 && this.mapKey !== "plaza" && (NAMED.includes(id) || id.startsWith("bird"))) return;
    let tex = "npc_" + id, frame = 0;
    if (id.startsWith("bird")) tex = "bird";
    if (id.startsWith("extra")) tex = id;
    const s = this.add.sprite(x, y, tex, frame).setOrigin(0.5, 1).setDepth(y);
    if (id.startsWith("bird")) s.play("bird-sing");
    if (id.startsWith("extra")) s.play(id + "-walk");
    // 物理体（具名 NPC 阻挡）
    if (NAMED.includes(id)) {
      this.physics.add.existing(s, true);
      const body = s.body as Phaser.Physics.Arcade.StaticBody;
      body.setSize(12, 10); body.setOffset(2, 14);
      this.physics.add.collider(this.player, s);
    }
    this.npcs.push({ id, sprite: s, x0: x, y0: y, patrol, dir: 1, nextBlink: this.time.now + 1000 + Math.random() * 2000, offUntil: 0, blinkUntil: 0 });
  }

  goTo(target: string, spawnName: string) {
    if (this.transitioning || director.cutscene) return;
    this.transitioning = true;
    director.pendingSpawn = spawnName;
    this.ui.fadeOut(250, () => {
      this.scene.start(target);
    });
  }

  private restoreTiles() {
    if (this.mapKey === "plaza") {
      for (const w of director.watered) {
        const p = this.pois.find(q => q.name === w);
        if (p) this.obstacles.putTileAt(56, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1); // 盛开
      }
      for (const r of director.ribbons) this.hangRibbonVisual(r);
    }
    if (this.mapKey === "field") {
      for (const b of director.berries) {
        const p = this.pois.find(q => q.name === b);
        if (p) this.obstacles.putTileAt(13, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1); // 已采
      }
    }
  }

  private hangRibbonVisual(poiName: string) {
    const p = this.pois.find(q => q.name === poiName);
    if (!p) return;
    const img = this.add.image(Math.floor(p.x / 16) * 16 + 8, (Math.floor(p.y / 16) - 2) * 16 + 8, "tiles", 58).setDepth(3);
    this.buntings.push(img);
  }

  // ---------- 侵蚀矩阵 → 视觉（§6.4，唯一真源驱动） ----------
  private onStage() { this.applyStageVisuals(); audio.applyStage(); }
  applyStageVisuals() {
    const v = director.visual;
    if (this.isWhite) { // 白色层：去饱和灰白色板，不参与糖果侵蚀
      this.colorFx.reset(); this.colorFx.saturate(-0.4);
      this.vignetteFx.strength = 0;
      return;
    }
    this.colorFx.reset();
    this.colorFx.saturate(v.saturation - 1);
    this.vignetteFx.strength = v.vignette;
  }
  private onNegFlash() {
    // 单帧负片（S4 溢出池 / 随机）
    this.colorFx.reset(); this.colorFx.negative();
    this.time.delayedCall(50, () => this.applyStageVisuals());
  }
  private onSadAct() {
    // 最近的 NPC 转头看向玩家一次
    let best: NpcSprite | null = null; let bd = 1e9;
    for (const n of this.npcs) {
      const d = Phaser.Math.Distance.Between(n.sprite.x, n.sprite.y, this.player.x, this.player.y);
      if (d < bd) { bd = d; best = n; }
    }
    if (!best) return;
    const s = best.sprite;
    s.setFlipX(this.player.x < s.x);
    this.tweens.add({ targets: s, y: s.y - 1, duration: 90, yoyo: true });
    this.time.delayedCall(900, () => s.setFlipX(false));
  }
  private onDuskWorld() {
    if (director.day === 6 && OUTDOOR.includes(this.mapKey)) this.startRain();
  }

  private startRain() {
    if (this.rain) return;
    this.rain = this.add.particles(0, 0, "rainpx", {
      x: { min: 0, max: this.map.widthInPixels },
      y: -8,
      lifespan: 1400,
      speedY: { min: 150, max: 190 },
      speedX: -12,
      quantity: 2,
      frequency: 30,
      alpha: 0.6,
    }).setDepth(200);
    this.rainStillAt = this.time.now;
  }

  // ---------- 强制欢乐庆典（§6.1 反制） ----------
  private onForcedCelebration() {
    this.ui.banner("欢 乐 庆 典 ！");
    this.ui.toast("（世界不允许你难过）");
    audio.sfxDing();
    for (const n of this.npcs) {
      this.tweens.add({
        targets: n.sprite,
        x: Phaser.Math.Linear(n.sprite.x, this.player.x, 0.5),
        y: Phaser.Math.Linear(n.sprite.y, this.player.y, 0.5),
        duration: 3000,
      });
    }
    this.celebTimer = this.time.delayedCall(8000, () => {
      if (director.celebrationActive) {
        director.celebrationEnd(false); // 未逃离：幸福拉回 50
        this.ui.toast("（你重新开心了起来）");
        for (const n of this.npcs) this.tweens.add({ targets: n.sprite, x: n.x0, y: n.y0, duration: 2000 });
      }
    });
  }

  // ---------- Day 7 庆典 ----------
  private setupCelebrationDay() {
    director.celebrationStarted = true;
    // E4：静默——庆典无人出席（优先级最高，进场即判）
    if (director.e4ok && director.escapes >= 3) {
      // 清空所有 NPC，彩带在没有风的空气里飘
      for (const n of this.npcs) n.sprite.destroy();
      this.npcs = [];
      for (let i = 0; i < 6; i++) {
        const img = this.add.image(200 + i * 16, 168, "tiles", 58).setDepth(3);
        this.tweens.add({ targets: img, y: 166, duration: 1800 + i * 130, yoyo: true, repeat: -1 });
      }
      this.time.delayedCall(2600, () => this.startEnding("E4"));
      return;
    }
    // 全村到场
    const places: [string, number, number][] = [
      ["yuanyuan", 214, 202], ["mimi", 266, 202], ["fu", 202, 224], ["gui", 278, 224],
    ];
    for (const [id, x, y] of places) this.spawnNpc(id, x, y);
    // 彩带装饰
    for (let i = 0; i < 6; i++) this.add.image(200 + i * 16, 168, "tiles", 58).setDepth(3);
  }

  startEnding(code: "E1" | "E2" | "E3" | "E4") {
    if (director.endingPlayed) return;
    director.endingPlayed = true;
    this.registry.set("ending", code);
    this.scene.stop("ui");
    this.scene.start("ending");
  }

  // ---------- 更新循环 ----------
  update(time: number, _dt: number) {
    if (!this.player) return;
    const ui = this.ui;
    const blocked = ui.dialogOpen || director.cutscene;

    // 移动
    let vx = 0, vy = 0;
    if (!blocked && !this.t14Active) {
      if (this.keys.A.isDown || this.keys.LEFT.isDown) vx = -1;
      else if (this.keys.D.isDown || this.keys.RIGHT.isDown) vx = 1;
      if (this.keys.W.isDown || this.keys.UP.isDown) vy = -1;
      else if (this.keys.S.isDown || this.keys.DOWN.isDown) vy = 1;
    }
    const moving = vx !== 0 || vy !== 0;
    const v = new Phaser.Math.Vector2(vx, vy).normalize().scale(SPEED);
    this.player.setVelocity(v.x, v.y);
    if (moving) {
      this.lastMoveAt = time;
      this.rainStillAt = time;
      if (vy < 0) this.player.play("walk-up", true);
      else if (vy > 0) this.player.play("walk-down", true);
      else if (vx < 0) this.player.play("walk-left", true);
      else this.player.play("walk-right", true);
    } else {
      // 待机：当前朝向第 1 帧
      const anim = this.player.anims.currentAnim;
      this.player.anims.stop();
      if (anim) this.player.setFrame(anim.frames[0].frame.name);
    }
    this.player.setDepth(this.player.y);

    // T14：保持微笑 60 秒
    if (this.t14Active) {
      if (moving || Phaser.Input.Keyboard.JustDown(this.keys.A) || Phaser.Input.Keyboard.JustDown(this.keys.W)) {
        this.cancelT14();
      } else {
        const el = (time - this.t14Start) / 1000;
        if (el >= 40 && !this.t14Faded) { this.t14Faded = true; audio.fadeToSilence(19); } // 40–60 秒渐隐至零
        if (el >= 60) this.finishT14();
      }
    }

    // 发呆（悲伤行为③）：连续 10 秒不移动且不在任务点
    if (!blocked && !this.t14Active && !director.celebrationActive && time - this.lastMoveAt > 10_000) {
      this.lastMoveAt = time;
      if (!this.nearAnyPoi()) director.sadAct("idle");
    }
    // 雨中站立 ≥10 秒（悲伤行为⑤）
    if (this.rain && !blocked && time - this.rainStillAt > 10_000) {
      this.rainStillAt = time;
      director.sadAct("rain");
      this.ui.toast("（雨很冷。没有人看见雨。）");
    }

    // 注视古井/雾墙 5 秒 → 觉知 +3（各每日 1 次）
    this.stareCheck(time, "well");
    this.stareCheck(time, "fog");

    // 湖面倒影（Day2 起延迟 1 帧）
    if (this.reflection) {
      const src = director.day >= 2 ? this.prevPos : { x: this.player.x, y: this.player.y };
      const shoreY = 9.5 * 16;
      this.reflection.setPosition(src.x, shoreY + (shoreY - src.y) + 20);
      this.reflection.setVisible(this.player.y > 80 && this.player.y < 152);
      this.reflection.setFrame(this.player.frame.name);
      this.prevPos = { x: this.player.x, y: this.player.y };
    }

    this.updateNpcs(time);

    // 交互提示与触发
    if (!blocked && !this.t14Active) {
      const target = this.nearestInteract();
      if (target) {
        ui.prompt(`按 E ${target.kind === "npc" ? "对话" : "查看"}`);
        const pressed = Phaser.Input.Keyboard.JustDown(this.keys.E) || Phaser.Input.Keyboard.JustDown(this.keys.ENTER);
        if (pressed && time > this.interactCooldown) this.interact(target);
      } else {
        ui.prompt(null);
      }
      // D5 前古井/雾墙劝离
      this.dissuadeCheck();
    } else {
      // 消费残留按键：关闭对话的那一次 E 不应立刻再次触发交互
      Phaser.Input.Keyboard.JustDown(this.keys.E);
      Phaser.Input.Keyboard.JustDown(this.keys.ENTER);
      this.interactCooldown = time + 250;
      ui.prompt(null);
    }
  }

  private updateNpcs(time: number) {
    const v = director.visual;
    const s4 = director.stage >= 4;
    for (const n of this.npcs) {
      const base = n.id.startsWith("bird") ? "bird" : n.id;
      // 群演巡逻；S4 时 1 号群演停摆面朝玩家
      if (n.patrol) {
        const frozen = s4 && n.id === "extra1";
        if (frozen || this.t14Active) {
          n.sprite.anims.pause();
          n.sprite.setFlipX(this.player.x < n.sprite.x);
        } else {
          if (!n.sprite.anims.isPlaying) n.sprite.anims.resume();
          n.sprite.x += n.dir * 0.25;
          if (n.sprite.x > n.x0 + n.patrol) n.dir = -1;
          if (n.sprite.x < n.x0 - n.patrol) n.dir = 1;
          n.sprite.setFlipX(n.dir < 0);
        }
        continue;
      }
      if (n.id.startsWith("bird")) {
        // 音乐变调时动画同步卡帧；S4 鸟鸣消失但仍在张嘴
        const stg = director.stage;
        const rate = [1, 1, 0.97, 0.92, 0.7][stg];
        n.sprite.anims.timeScale = rate;
        continue;
      }
      if (!NAMED.includes(n.id)) continue;
      // 具名 NPC 表情档 + 眨眼 + off 闪现
      const expr = director.npcExpr(n.id);
      let frame = expr === "flat" ? 1 : 0;
      const npcStage = director.npcStage(n.id);
      if (n.offUntil > time) frame = 2;          // off 档闪现 0.1 秒
      else if (n.blinkUntil > time) frame = 3;   // 眨眼
      else if (time > n.nextBlink) {
        n.blinkUntil = time + 120;
        n.nextBlink = time + v.blinkInterval + Math.random() * 800;
      }
      // S4：off 档闪现（0.6 次/分钟 ⇒ 每帧概率）
      if (npcStage >= 4 && NPC_OFFSET[base] !== null && n.offUntil < time && Math.random() < 0.6 / (60 * 60)) {
        n.offUntil = time + 100;
      }
      n.sprite.setFrame(frame);
      // S3+：静止 NPC 面朝玩家
      if (npcStage >= 3) n.sprite.setFlipX(this.player.x < n.sprite.x);
    }
    // S4 随机单帧负片：0.4 次/分钟
    if (s4 && !this.isWhite && Math.random() < 0.4 / (60 * 60)) this.onNegFlash();
  }

  private stareCheck(time: number, kind: "well" | "fog") {
    const p = this.pois.find(q => q.name === kind);
    if (!p || director.day < 5) return;
    const near = Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < 30;
    if (kind === "well") {
      if (!near) { this.nearWellSince = time; return; }
      if (time - this.nearWellSince > 5000 && !director.stared.has("well")) {
        director.stare("well");
        this.ui.toast(MISC.wellStare);
      }
    } else {
      if (!near) { this.nearFogSince = time; return; }
      if (time - this.nearFogSince > 5000 && !director.stared.has("fog")) {
        director.stare("fog");
        this.ui.toast(MISC.fogStare);
      }
    }
  }

  private dissuadeCheck() {
    if (director.day >= 5 || this.dissuaded) return;
    const p = this.pois.find(q => q.name === "well" || q.name === "fog");
    if (!p) return;
    if (Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < 28) {
      this.dissuaded = true;
      const lines = p.name === "well" ? DISSUADE.well : DISSUADE.fog;
      this.ui.say("villager", lines, {
        onDone: () => {
          // 温柔劝离：把玩家推回入口方向
          const spawn = this.findSpawn("entry")!;
          const dir = new Phaser.Math.Vector2(spawn.x - this.player.x, spawn.y - this.player.y).normalize().scale(40);
          this.player.setPosition(this.player.x + dir.x, this.player.y + dir.y);
          this.time.delayedCall(3000, () => (this.dissuaded = false));
        },
      });
    }
  }

  private nearAnyPoi(): boolean {
    for (const p of this.pois) {
      if (Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < INTERACT_DIST + 8) return true;
    }
    return false;
  }

  private nearestInteract(): { kind: "npc" | "poi"; id: string; poi?: Poi } | null {
    let best: { kind: "npc" | "poi"; id: string; poi?: Poi; d: number } | null = null;
    for (const n of this.npcs) {
      if (n.id.startsWith("extra") || n.id.startsWith("bird")) {
        // 群演可摸头；双子鸟经 poi 交互
        if (n.id.startsWith("bird")) continue;
      }
      const d = Phaser.Math.Distance.Between(n.sprite.x, n.sprite.y - 10, this.player.x, this.player.y);
      if (d < INTERACT_DIST && (!best || d < best.d)) best = { kind: "npc", id: n.id, d };
    }
    for (const p of this.pois) {
      const d = Phaser.Math.Distance.Between(p.x, p.y, this.player.x, this.player.y);
      if (d < INTERACT_DIST && (!best || d < best.d)) best = { kind: "poi", id: p.name, poi: p, d };
    }
    return best;
  }

  // ---------- 交互分发 ----------
  private interact(t: { kind: "npc" | "poi"; id: string; poi?: Poi }) {
    if (t.kind === "npc") {
      if (t.id.startsWith("extra")) return this.petCritter();
      return this.talkTo(t.id);
    }
    this.usePoi(t.id);
  }

  private petCritter() {
    if (director.smallAct("pet")) this.ui.say("me", ["（你摸了摸它的头。它的循环停顿了半拍，又继续了。）"], { speakerName: "" });
    else this.ui.say("me", ["（它心满意足地走开了。）"], { speakerName: "" });
  }

  // ---- NPC 对话（含任务链） ----
  private talkTo(id: string) {
    const d = director;
    d.greeted.add(id);
    d.talkCounts[id] = (d.talkCounts[id] ?? 0) + 1;
    // 纠缠（悲伤行为④）：同一 NPC 一天 ≥5 次
    if (d.talkCounts[id] >= 5 && !d.nagged.has(id)) {
      d.nagged.add(id);
      d.sadAct("nag");
    }
    const greetQuest = d.day === 4 ? "T10" : "T3";
    const checkGreet = () => {
      if (d.isOpen(greetQuest) && d.greeted.size >= 3) d.completeQuest(greetQuest);
    };

    const breadQuest = d.day === 4 ? "T8" : "T1";
    // T1/T8：取面包
    if (d.isOpen(breadQuest) && id === "yuanyuan" && d.breadHeld === 0) {
      d.breadHeld = 3;
      this.ui.say("yuanyuan", QUEST_LINES.t1Give, { onDone: checkGreet });
      return;
    }
    // T1/T8：送面包
    if (d.isOpen(breadQuest) && d.breadHeld > 0 && ["dabao", "mimi", "gui"].includes(id) && !d.breadDelivered.has(id)) {
      d.breadDelivered.add(id);
      d.breadHeld--;
      this.ui.say(id, [QUEST_LINES.t1Deliver[id]], {
        onDone: () => {
          if (d.breadDelivered.size >= 3) d.completeQuest(breadQuest);
          checkGreet();
        },
      });
      return;
    }
    // T4：取信
    if (d.day === 2 && d.isOpen("T4") && id === "dabao" && d.lettersHeld === 0 && d.lettersDelivered.size === 0) {
      d.lettersHeld = 3;
      this.ui.say("dabao", QUEST_LINES.t4Give, { onDone: checkGreet });
      return;
    }
    // T4：送信（第 3 封署名乱码 0.2 秒 → C4）
    if (d.isOpen("T4") && d.lettersHeld > 0 && ["yuanyuan", "mimi", "fu"].includes(id) && !d.lettersDelivered.has(id)) {
      d.lettersDelivered.add(id);
      d.lettersHeld--;
      const third = d.lettersDelivered.size >= 3;
      this.ui.say(id, [QUEST_LINES.t4Deliver[id]], {
        onDone: () => {
          if (third) {
            this.ui.glitchText("署名：PT-0▓", 200);
            this.time.delayedCall(240, () => {
              this.ui.say("me", CLUE_TEXT.C4, {
                speakerName: "", clean: true,
                onDone: () => { director.foundClue("C4"); d.completeQuest("T4"); },
              });
            });
          }
          checkGreet();
        },
      });
      return;
    }
    // T5：咪咪请求清点
    if (d.day === 2 && d.isOpen("T5") && id === "mimi" && d.shelves.size === 0) {
      this.ui.say("mimi", QUEST_LINES.t5Ask, { onDone: checkGreet });
      return;
    }
    // T11：帮圆圆揉面（可拒绝，拒绝被记录）
    if (d.day === 5 && d.isOpen("T11") && id === "yuanyuan" && !d.t11Accepted) {
      this.ui.say("yuanyuan", [QUEST_LINES.t11Ask], {
        choices: [
          { label: "好呀，我来帮忙。", cb: () => { d.t11Accepted = true; this.ui.toast("（去揉面台吧）"); } },
          { label: "今天不想帮。", cb: () => { d.refuseQuest("T11"); this.ui.say("yuanyuan", [QUEST_LINES.t11Refused]); } },
        ],
        onDone: checkGreet,
      });
      return;
    }
    // 龟爷爷 C9 对话链（D2/D4/D6 三段递进）
    if (id === "gui" && [2, 4, 6].includes(d.day)) {
      const node = NPC_TALK.gui[d.day];
      this.ui.say("gui", node.lines, {
        onDone: () => {
          d.guiChain.add(d.day);
          if (d.day === 6 && !d.clues.has("C9")) {
            this.ui.say("me", CLUE_TEXT.C9, { speakerName: "", clean: true, onDone: () => director.foundClue("C9") });
          }
          checkGreet();
        },
      });
      return;
    }
    // 默认闲聊
    const node = NPC_TALK[id]?.[d.day];
    if (!node) { this.ui.say(id, ["……"]); return; }
    const opts: UiChoice[] | undefined = node.choices?.map((c: DialogChoice) => ({
      label: c.label,
      cb: () => {
        if (c.kind === "gentle") d.addHappiness(2);
        else if (c.kind === "hesitant") d.addAwareness(2);
        else d.sadAct("negative"); // 否定选项 = 悲伤行为②
        this.ui.say(id, c.reply, { onDone: checkGreet });
      },
    }));
    this.ui.say(id, node.lines, { choices: opts, onDone: checkGreet });
  }

  // ---- POI 交互 ----
  private usePoi(name: string) {
    const d = director;
    const say = (sp: string, lines: string[], opts: any = {}) => this.ui.say(sp, lines, opts);
    const narr = (lines: string[], onDone?: () => void, clean = true) =>
      say("me", lines, { speakerName: "", clean, onDone });

    switch (true) {
      // 公告板：公告 + T12 擦拭
      case name === "board": {
        if (!d.announced) {
          d.announced = true;
          const tasks = d.questsOfDay().map(q => `・${q.title}`);
          say("afu", [ANNOUNCEMENTS[d.day], ...(tasks.length ? ["今日的幸福任务：", ...tasks] : [])], { clean: true });
          return;
        }
        if (d.day === 5 && d.isOpen("T12")) {
          if (!d.t12Accepted) {
            this.ui.say("me", [QUEST_LINES.t12Ask], {
              speakerName: "", clean: true,
              choices: [
                { label: "擦拭公告板。", cb: () => { d.t12Accepted = true; this.wipeBoard(); } },
                { label: "拒绝。", cb: () => { d.refuseQuest("T12"); narr([QUEST_LINES.t12Refused]); } },
              ],
            });
            return;
          }
          this.wipeBoard();
          return;
        }
        const open = d.questsOfDay().some(q => d.questStatus[q.id] === "open");
        if (!open && d.day < 7) narr([MISC.boardDone]);
        else say("afu", [ANNOUNCEMENTS[d.day]], { clean: true });
        return;
      }
      // 床
      case name === "bed": {
        if (d.day >= 7) { narr(["（今天不用睡。庆典在等你。）"]); return; }
        if (!d.dusk) { narr([MISC.bedEarly]); return; }
        narr([MISC.bedSleep], () => {
          director.cutscene = true;
          this.ui.fadeOut(900, () => { director.cutscene = false; this.scene.start("dream"); });
        });
        return;
      }
      // 日历（C1，D2 起）
      case name === "calendar": {
        if (d.day >= 2) narr(CLUE_TEXT.C1, () => d.foundClue("C1"));
        else narr(["（床头的日历。明天也会是晴天。）"]);
        return;
      }
      // 配方纸（C2，D3 起）
      case name === "recipe": {
        if (d.day >= 3) narr(CLUE_TEXT.C2, () => d.foundClue("C2"));
        else narr(["（柜台上的配方纸。字迹工整。）"]);
        return;
      }
      // 散落信件（C3，D2 起）
      case name === "letters": {
        if (d.day >= 2) narr(CLUE_TEXT.C3, () => d.foundClue("C3"));
        else narr(["（邮筒旁散落着几封信。）"]);
        return;
      }
      // 货架清点（T5）与 C5
      case name.startsWith("shelfA") || name.startsWith("shelfB") || name.startsWith("shelfC"): {
        if (d.day === 2 && d.isOpen("T5")) {
          if (!d.shelves.has(name)) {
            d.shelves.add(name);
            narr([`（${d.shelves.size}/3……数字对得上。）`], () => {
              if (d.shelves.size >= 3) {
                say("mimi", QUEST_LINES.t5Done, { onDone: () => d.completeQuest("T5") });
              }
            }, true);
            return;
          }
        }
        narr(["（货架排得整整齐齐。）"]);
        return;
      }
      case name === "shelf_c5": {
        if (d.day >= 2) narr(CLUE_TEXT.C5, () => d.foundClue("C5"));
        else narr(["（货架上的商品排得很满。）"]);
        return;
      }
      // 诊所表格（C8，D5 起）
      case name === "chart": {
        if (d.day >= 5) narr(CLUE_TEXT.C8, () => d.foundClue("C8"));
        else narr(["（桌上放着一份表格，字很小。）"]);
        return;
      }
      // 花坛浇水（T2/T9 + 小互动）
      case name.startsWith("flowerbed"): {
        const wq = d.day === 4 ? "T9" : "T2";
        if (d.isOpen(wq) && !d.watered.has(name)) {
          d.watered.add(name);
          const p = this.pois.find(q => q.name === name)!;
          this.obstacles.putTileAt(56, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1);
          narr([`（浇水 ${d.watered.size}/3。花立刻精神了起来。）`], () => {
            if (d.watered.size >= 3) d.completeQuest(wq);
          });
          return;
        }
        if (d.smallAct("water")) narr(["（又浇了点水。）"]);
        else narr(["（土已经很湿了。）"]);
        return;
      }
      case name === "fieldwater": {
        if (d.smallAct("water")) narr(["（你给花田浇了水。）"]);
        else narr(["（今天浇得够多了。）"]);
        return;
      }
      // 采莓果（T6）
      case name.startsWith("berry"): {
        if (d.day === 3 && d.isOpen("T6") && !d.berries.has(name)) {
          d.berries.add(name);
          const p = this.pois.find(q => q.name === name)!;
          this.obstacles.putTileAt(13, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1);
          narr([`（采到莓果 ${d.berries.size}/5。）`], () => {
            if (d.berries.size >= 5) d.completeQuest("T6");
          });
          return;
        }
        narr(["（莓果丛。果子还没长出来。）"]);
        return;
      }
      // 野餐（T7）
      case name === "picnic": {
        if (d.day === 3 && d.isOpen("T7")) {
          director.cutscene = true;
          this.cameras.main.pan(this.player.x, this.map.heightInPixels - 60, 1200, "Sine.easeInOut");
          narr(MISC.picnic, () => {
            this.cameras.main.pan(this.player.x, this.player.y, 800, "Sine.easeInOut");
            director.cutscene = false;
            d.completeQuest("T7");
          }, true);
          return;
        }
        narr(["（野餐布。有阳光晒过的味道。）"], undefined, false);
        return;
      }
      // 揉面（T11）
      case name === "knead": {
        if (d.day === 5 && d.isOpen("T11") && d.t11Accepted) {
          say("yuanyuan", QUEST_LINES.t11Work, { onDone: () => d.completeQuest("T11") });
          return;
        }
        narr(["（揉面台。面粉是甜的。）"]);
        return;
      }
      // 挂彩带（T13）
      case name.startsWith("ribbon"): {
        if (d.day === 6 && d.isOpen("T13") && !d.ribbons.has(name)) {
          d.ribbons.add(name);
          this.hangRibbonVisual(name);
          narr([`（挂好彩带 ${d.ribbons.size}/4。）`], () => {
            if (d.ribbons.size >= 4) d.completeQuest("T13");
          });
          return;
        }
        narr(["（灯笼杆。漆是新的——每年都是新的。）"]);
        return;
      }
      // 庆典场地（T14 / T15）
      case name === "festival": {
        if (d.day === 6 && d.isOpen("T14")) { this.beginT14(); return; }
        if (d.day === 7) { this.festivalDay7(); return; }
        narr(["（庆典的场地，打扫得很干净。）"]);
        return;
      }
      // 古井（C7，D5 起可交互）
      case name === "well": {
        if (d.day < 5) return; // 劝离逻辑在 dissuadeCheck
        director.cutscene = true;
        this.time.delayedCall(600, () => audio.sfxEcg());
        this.time.delayedCall(1400, () => audio.sfxEcg());
        this.time.delayedCall(2200, () => audio.sfxEcg());
        narr(CLUE_TEXT.C7, () => { director.cutscene = false; d.foundClue("C7"); });
        return;
      }
      // 雾墙（E3 入口）
      case name === "fog": {
        if (d.day < 5) return;
        if (d.day === 7 && d.celebrationStarted && d.e3Ready()) { this.beginE3(); return; }
        narr(["（雾很冷，纹丝不动。）", "（雾的后面，好像有一条走廊。）"]);
        return;
      }
      // 白色层病历（E3 终点）
      case name === "chart_ward": {
        if (d.e3Mode) { this.startEnding("E3"); return; }
        narr(["（一张病历。字被白光糊住了。）"]);
        return;
      }
      // 双子鸟树（喂食小互动）
      case name === "birdtree": {
        if (director.stage >= 4) { narr(["（它们在张嘴。没有声音。）"]); return; }
        if (d.smallAct("feed")) narr(["（你撒了点面包屑。歌声更亮了。）"]);
        else narr(["（双子鸟歪头看你。歌没有停。）"]);
        return;
      }
      case name === "bench": { narr(["（长椅被晒得暖暖的。）"]); return; }
      case name === "sign_house": { narr([`（写着：${d.playerName}的小屋。）`]); return; }
      default: return;
    }
  }

  private wipeBoard() {
    const d = director;
    d.wipes++;
    if (d.wipes === 1) {
      this.ui.say("me", [QUEST_LINES.wipe1], { speakerName: "", clean: true });
    } else if (d.wipes === 2) {
      this.ui.say("me", [QUEST_LINES.wipe2, ...CLUE_TEXT.C6], {
        speakerName: "", clean: true,
        onDone: () => d.foundClue("C6"),
      });
    } else {
      this.ui.say("me", [QUEST_LINES.wipe3], {
        speakerName: "", clean: true,
        onDone: () => d.completeQuest("T12"),
      });
    }
  }

  // ---- T14：保持微笑 60 秒 ----
  private beginT14() {
    this.ui.say("me", [MISC.t14Start], {
      speakerName: "", clean: true,
      onDone: () => {
        this.t14Active = true;
        this.t14Faded = false;
        this.t14Start = this.time.now;
        this.cameras.main.zoomTo(1.9, 60_000, "Linear"); // 镜头缓慢拉近
        this.ui.toast("（不要动）");
      },
    });
  }
  private cancelT14() {
    this.t14Active = false;
    this.cameras.main.zoomTo(1, 600);
    audio.restoreVolume();
    this.ui.say("me", [MISC.t14Cancel], { speakerName: "", clean: true });
  }
  private finishT14() {
    this.t14Active = false;
    director.t14done = true;
    this.cameras.main.zoomTo(1, 900);
    audio.restoreVolume(2);
    this.ui.say("me", [MISC.t14Done], {
      speakerName: "", clean: true,
      onDone: () => director.completeQuest("T14"),
    });
  }

  // ---- Day 7 庆典交互（T15 → 结局分歧） ----
  private festivalDay7() {
    const d = director;
    const lines = ["第 413 届丰收庆典，现在开始。", MISC.festivalGather];
    if (d.e3Ready()) lines.push(MISC.festivalHintFog);
    this.ui.say("afu", lines, {
      choices: [
        {
          label: "加入合影。",
          cb: () => {
            d.completeQuest("T15");
            const e = d.decideEnding(false);
            this.time.delayedCall(600, () => this.startEnding(e as any));
          },
        },
        { label: "再看看。", cb: () => { } },
      ],
    });
  }

  // ---- E3：雾墙裂开 → 白色走廊 ----
  private beginE3() {
    const d = director;
    d.e3Mode = true;
    d.cutscene = true;
    audio.stopBgm();
    this.ui.say("me", ["（雾，裂开了。）", "（里面是一条白色的走廊。很安静。）"], {
      speakerName: "", clean: true,
      onDone: () => {
        this.cameras.main.flash(1200, 255, 255, 255);
        this.time.delayedCall(1100, () => {
          d.cutscene = false;
          d.pendingSpawn = "entry";
          this.scene.start("white_corridor");
        });
      },
    });
  }
}

// ---- 11 张地图各一个 Scene ----
export class HouseScene extends WorldScene { constructor() { super("house"); } }
export class PlazaScene extends WorldScene { constructor() { super("plaza"); } }
export class BakeryScene extends WorldScene { constructor() { super("bakery"); } }
export class StoreScene extends WorldScene { constructor() { super("store"); } }
export class ClinicScene extends WorldScene { constructor() { super("clinic"); } }
export class LakeScene extends WorldScene { constructor() { super("lake"); } }
export class FieldScene extends WorldScene { constructor() { super("field"); } }
export class WellScene extends WorldScene { constructor() { super("well"); } }
export class FogwallScene extends WorldScene { constructor() { super("fogwall"); } }
export class WhiteCorridorScene extends WorldScene { constructor() { super("white_corridor"); } }
export class WhiteWardScene extends WorldScene { constructor() { super("white_ward"); } }
