// 地图场景基类（CONTRACT §4）。子类只提供 mapKey 与少量钩子。
// 负责：tilemap 加载、player（96 px/s、四向动画、obstacles 层碰撞、相机跟随+钳制）、
// ≤24px「按 E」提示、door 切换、NPC/群演实例化与表情档、注册 CorruptionDirector 视听参数
// （相机 postFX：ColorMatrix 饱和度 / Vignette 晕影 / 负片单帧）、通用任务逻辑、
// 悲伤行为（发呆/纠缠/雨）、欢乐庆典、脚本雨。

import Phaser from 'phaser';
import { director } from '../core/CorruptionDirector';
import { audio } from '../audio/AudioEngine';
import { NpcActor, pickStallExtra } from '../actors/Npc';
import {
  GAME_W, GAME_H, MOVE_SPEED, INTERACT_RANGE, MAP_SCENES, OUTDOOR_MAPS,
  NPC_DEFS, STANDARD_CHOICES,
  type NpcId, type Stage, type StageParams, type DialogueChoice, type DialoguePayload,
} from '../types';
import { propsOf, strProp, type TiledObj } from '../core/tiled';
import {
  npcLinesOf, CHOICE_REPLIES, BREAD_RECEIVERS,
  WATER_FLOWER_LINES, WATER_FLOWER_DONE, TAKE_LETTERS_LINES, LETTER_RECEIVERS,
  LETTER_GLITCH_TEXT, BERRY_PICK_LINES, BERRY_EMPTY_LINES,
  KNEAD_ASK, KNEAD_ACCEPT, KNEAD_REFUSE, NARRATOR,
  TAKE_BREAD_LINES, SHELF_COUNT_LINES, SHELF_COUNT_DONE, T14_HINT, T15_HINT,
} from '../data/dialogues';
import { CLUES } from '../data/clues';
import { announcementOf } from '../data/announcements';
import { tasksOfDay } from '../data/tasks';

export interface InteractZone {
  id: string;
  x: number;
  y: number;
}

type AnyFn = (...args: never[]) => void;

export abstract class MapScene extends Phaser.Scene {
  abstract readonly mapKey: string;

  protected player!: Phaser.Physics.Arcade.Sprite;
  protected actors: NpcActor[] = [];
  protected interactZones: InteractZone[] = [];
  protected doors: { x: number; y: number; target: string; spawn: string }[] = [];
  /** 门武装集合：出生/传送落在门旁时，须先离开感应圈（>18px）才允许触发，防止落点回弹 */
  private armedDoors = new Set<{ x: number; y: number; target: string; spawn: string }>();
  /** 已触发待重武装的交互目标：离开其感应圈（>30px）后才可再次触发，防连按 E 使同一对话无限循环 */
  private spentTargets = new Set<string>();
  protected obstacleLayer!: Phaser.Tilemaps.TilemapLayer;
  protected groundLayer?: Phaser.Tilemaps.TilemapLayer;

  private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
  private wasd!: Record<string, Phaser.Input.Keyboard.Key>;
  private keyE!: Phaser.Input.Keyboard.Key;
  private lastDir: 'down' | 'up' | 'left' | 'right' = 'down';
  private transitioning = false;

  private colorMatrix!: Phaser.FX.ColorMatrix;
  private vignetteFx!: Phaser.FX.Vignette;
  private negativeFrames = 0;

  private listeners: [string, AnyFn][] = [];
  private lastPrompt: string | null = null;

  private idleMs = 0;
  private idleArmed = true;
  private rainMs = 0;
  private rainCounted = false;
  private rainGraphics!: Phaser.GameObjects.Graphics;
  private rainTick = 0;

  private duskAnnouncedDay = 0;

  // ---------- 子类钩子 ----------
  /** 代码补充的不可见交互区（如 C3 邮筒、T13 彩带点、T14 站立点、庆典场地） */
  protected extraInteractZones(): InteractZone[] { return []; }
  /** 子类专属交互；返回 true 表示已处理 */
  protected async onInteract(_id: string): Promise<boolean> { return false; }
  /** 场景就绪后 */
  protected onReady(_data: { spawn?: string }): void {}
  /** 每帧追加逻辑 */
  protected extraUpdate(_time: number, _delta: number): void {}
  /** 是否可以走到 door（子类可拦截，如雾墙 D5 前劝离）；返回 false 表示已自行处理 */
  protected async canUseDoor(_target: string): Promise<boolean> { return true; }

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

  // ---------- 生命周期 ----------
  create(data: { spawn?: string }): void {
    // 场景实例会被复用：重置全部运行态字段
    this.actors = [];
    this.transitioning = false;
    this.lastPrompt = null;
    this.negativeFrames = 0;
    this.idleMs = 0;
    this.idleArmed = true;
    this.rainMs = 0;
    this.rainCounted = false;
    this.rainTick = 0;
    this.lastDir = 'down';
    // 进入新场景 = 上一场景的切换过场结束（床/门淡出期间置位，抑制庆典）
    director.sceneTransition = false;

    const map = this.make.tilemap({ key: `map_${this.mapKey}` });
    const tsName = map.tilesets.length > 0 ? map.tilesets[0].name : 'candy';
    const tiles = map.addTilesetImage(tsName, 'tileset');
    // 小地图（小于视口）居中：层与对象统一偏移
    const ox = Math.max(0, Math.floor((GAME_W - map.widthInPixels) / 2));
    const oy = Math.max(0, Math.floor((GAME_H - map.heightInPixels) / 2));
    const ground = tiles ? map.createLayer('ground', tiles, ox, oy) : null;
    const obstacles = tiles ? map.createLayer('obstacles', tiles, ox, oy) : null;
    if (!obstacles) throw new Error(`map ${this.mapKey} missing obstacles layer`);
    this.obstacleLayer = obstacles;
    this.groundLayer = ground ?? undefined;
    this.obstacleLayer.setCollisionByExclusion([-1]); // obstacles 层整层碰撞
    this.obstacleLayer.setCollision([29, 30, 31], false); // 路灯/庆典彩带（gid 29-31）仅装饰，可穿过

    // 对象解析
    const objLayer = map.getObjectLayer('objects');
    const objects = (objLayer?.objects ?? []) as unknown as TiledObj[];

    this.interactZones = [];
    this.doors = [];
    this.armedDoors.clear();
    this.spentTargets.clear();
    let spawnX = 16;
    let spawnY = 16;
    const wantSpawn = data?.spawn ?? 'default';
    let fallbackSpawn: { x: number; y: number } | null = null;

    for (const obj of objects) {
      const p = propsOf(obj);
      const objX = obj.x + ox;
      const objY = obj.y + oy;
      if (obj.name === 'spawn') {
        const id = strProp(p, 'id');
        if (id === 'default') fallbackSpawn = { x: objX, y: objY };
        if (id === wantSpawn) { spawnX = objX; spawnY = objY; }
      } else if (obj.name === 'door') {
        this.doors.push({ x: objX, y: objY, target: strProp(p, 'target'), spawn: strProp(p, 'spawn') });
      } else if (obj.name === 'npc') {
        const id = strProp(p, 'id') as NpcId;
        if (NPC_DEFS[id]) this.actors.push(new NpcActor(this, id, objX, objY));
      } else if (obj.name === 'interact') {
        this.interactZones.push({ id: strProp(p, 'id'), x: objX, y: objY });
      }
    }
    if (wantSpawn === 'default' && fallbackSpawn) {
      spawnX = fallbackSpawn.x; spawnY = fallbackSpawn.y;
    }
    this.interactZones.push(...this.extraInteractZones());

    // 玩家
    this.player = this.physics.add.sprite(spawnX, spawnY, 'player', 0);
    this.player.setOrigin(0.5, 1);
    this.player.setSize(10, 8).setOffset(3, 16);
    this.player.setCollideWorldBounds(true);
    this.physics.world.setBounds(ox, oy, map.widthInPixels, map.heightInPixels);
    this.physics.add.collider(this.player, this.obstacleLayer);

    // 相机：跟随 + 钳制（小地图按视口尺寸钳制 → 居中不滚动）
    const cam = this.cameras.main;
    cam.setBounds(0, 0, Math.max(map.widthInPixels, GAME_W), Math.max(map.heightInPixels, GAME_H));
    cam.setZoom(1); // 场景实例复用，重置 T14 的拉近
    cam.startFollow(this.player, true, 0.15, 0.15);
    cam.roundPixels = true;
    cam.fadeIn(250, 0, 0, 0);

    // 崩坏 postFX（场景重进时先清掉旧的）
    cam.clearFX();
    this.colorMatrix = cam.postFX.addColorMatrix();
    this.vignetteFx = cam.postFX.addVignette(0.5, 0.5, 0.9, 0);
    this.applyParams(director.params);

    // 输入
    this.cursors = this.input.keyboard!.createCursorKeys();
    this.wasd = this.input.keyboard!.addKeys('W,A,S,D') as Record<string, Phaser.Input.Keyboard.Key>;
    this.keyE = this.input.keyboard!.addKey('E');

    // 雨图层（屏幕空间）
    this.rainGraphics = this.add.graphics().setScrollFactor(0).setDepth(9990);

    // UIScene 常驻
    if (!this.scene.isActive('UIScene')) this.scene.launch('UIScene');

    // 事件注册
    this.onDir('stage', (s: Stage, p: StageParams) => {
      this.applyParams(p);
      void s;
      // 音乐变调时双子鸟动画同步卡帧
      const bird = this.actors.find((a) => a.def.id === 'bird');
      if (bird && bird.sprite.anims.isPlaying) {
        bird.sprite.anims.pause();
        this.time.delayedCall(180, () => bird.sprite.anims.resume());
      }
    });
    this.onDir('negativeFlash', () => { this.negativeFrames = 1; });
    this.onDir('firstFull', async () => {
      this.cameras.main.flash(1400, 255, 255, 245);
      await this.say({ lines: [NARRATOR.firstFull] });
    });
    this.onDir('dusk', () => {
      if (this.duskAnnouncedDay === director.day) return;
      this.duskAnnouncedDay = director.day;
      audio.bell();
      // D6 傍晚脚本雨（即使 T14 未完成，黄昏也下雨）
      if (director.day === 6) director.setRain(true);
      // 等当前对话结束后再播黄昏提示，避免互相覆盖
      const tryShow = (): void => {
        if (!director.dialogueActive) void this.say({ lines: [NARRATOR.duskBell] });
        else this.time.delayedCall(500, tryShow);
      };
      tryShow();
    });
    this.onDir('npcGlance', () => this.glanceNearest());
    this.onDir('celebration', (phase: string) => {
      if (phase === 'start') void this.beginCelebration();
    });

    this.events.once('shutdown', () => {
      for (const [ev, fn] of this.listeners) director.off(ev, fn);
      this.listeners = [];
    });

    director.emit('prompt', null); // 清掉上一场景残留的「按 E」
    this.onReady(data ?? {});
  }

  protected onDir(event: string, fn: AnyFn): void {
    director.on(event, fn);
    this.listeners.push([event, fn]);
  }

  // ---------- 视听参数（§6.4 矩阵） ----------
  private applyParams(p: StageParams): void {
    if (!this.colorMatrix) return;
    this.colorMatrix.reset();
    this.colorMatrix.saturate(p.saturation);
    this.vignetteFx.strength = p.vignette;
    this.vignetteFx.radius = p.vignette > 0.4 ? 0.65 : 0.9;
  }

  // ---------- 每帧 ----------
  update(time: number, delta: number): void {
    director.tick(delta);

    // 负片单帧
    if (this.negativeFrames > 0) {
      this.negativeFrames -= 1;
      this.colorMatrix.reset();
      this.colorMatrix.negative();
      this.time.delayedCall(40, () => this.applyParams(director.params));
    }

    // S4：群演 1 只停摆
    if (director.stage === 4) {
      const extras = this.actors.filter((a) => a.isExtra);
      if (extras.length > 0 && !extras.some((a) => a.stalled)) {
        const pick = pickStallExtra(this.actors);
        if (pick) pick.stalled = true;
      }
    }

    if (!this.transitioning) {
      this.handleMovement(delta);
      this.handleIdleSadness(delta);
      this.handleInteraction();
      void this.handleDoors();
    }

    for (const a of this.actors) a.update(delta, this.player);
    this.updateRain(delta);
    this.player.setDepth(this.player.y);
    this.extraUpdate(time, delta);
  }

  private handleMovement(_delta: number): void {
    if (director.dialogueActive) {
      this.player.setVelocity(0);
      this.player.anims.stop();
      this.player.setFrame(this.idleFrame());
      return;
    }
    let vx = 0;
    let vy = 0;
    const k = this.wasd;
    if (this.cursors.left.isDown || k.A.isDown) vx -= 1;
    if (this.cursors.right.isDown || k.D.isDown) vx += 1;
    if (this.cursors.up.isDown || k.W.isDown) vy -= 1;
    if (this.cursors.down.isDown || k.S.isDown) vy += 1;

    if (vx === 0 && vy === 0) {
      this.player.setVelocity(0);
      this.player.anims.stop();
      this.player.setFrame(this.idleFrame());
      return;
    }
    const len = Math.hypot(vx, vy);
    this.player.setVelocity((vx / len) * MOVE_SPEED, (vy / len) * MOVE_SPEED);

    let dir: 'down' | 'up' | 'left' | 'right';
    if (Math.abs(vx) > Math.abs(vy)) dir = vx < 0 ? 'left' : 'right';
    else dir = vy < 0 ? 'up' : 'down';
    this.lastDir = dir;
    const anim = `walk-${dir}`;
    if (this.anims.exists(anim)) this.player.play(anim, true);
  }

  private idleFrame(): number {
    return { down: 0, up: 4, left: 8, right: 12 }[this.lastDir];
  }

  /** 发呆：连续 10 秒不移动且不在任务点 → 悲伤行为（白色层除外） */
  private handleIdleSadness(delta: number): void {
    if (this.mapKey === 'white') return;
    const moving = (this.player.body?.velocity.lengthSq() ?? 0) > 1;
    if (moving) {
      this.idleMs = 0;
      this.idleArmed = true;
      return;
    }
    if (director.dialogueActive || director.celebrationActive || director.t14Active) {
      this.idleMs = 0;
      return;
    }
    if (this.nearAnyPoint(40)) {
      this.idleMs = 0; // 在任务点/交互点旁不算发呆
      return;
    }
    if (!this.idleArmed) return;
    this.idleMs += delta;
    if (this.idleMs >= 10_000) {
      this.idleArmed = false;
      director.recordSadness('idle');
    }
  }

  private nearAnyPoint(range: number): boolean {
    const px = this.player.x;
    const py = this.player.y - 8;
    for (const z of this.interactZones) {
      if (Phaser.Math.Distance.Between(px, py, z.x, z.y) <= range) return true;
    }
    for (const d of this.doors) {
      if (Phaser.Math.Distance.Between(px, py, d.x, d.y) <= range) return true;
    }
    for (const a of this.actors) {
      if (Phaser.Math.Distance.Between(px, py, a.sprite.x, a.sprite.y - 8) <= range) return true;
    }
    return false;
  }

  // ---------- 交互 ----------
  private handleInteraction(): void {
    if (director.dialogueActive || director.celebrationActive || director.sceneTransition) {
      this.setPrompt(null);
      return;
    }
    const px = this.player.x;
    const py = this.player.y - 8;
    let best: { kind: 'interact' | 'npc'; id: string; dist: number } | null = null;

    for (const z of this.interactZones) {
      const d = Phaser.Math.Distance.Between(px, py, z.x, z.y);
      if (d > 30) this.spentTargets.delete(`interact:${z.id}`); // 离开感应圈 → 重武装
      else if (d <= INTERACT_RANGE && !this.spentTargets.has(`interact:${z.id}`) && (!best || d < best.dist)) best = { kind: 'interact', id: z.id, dist: d };
    }
    for (const a of this.actors) {
      const d = Phaser.Math.Distance.Between(px, py, a.sprite.x, a.sprite.y - 8);
      if (d > 30) this.spentTargets.delete(`npc:${a.def.id}`);
      else if (d <= INTERACT_RANGE && !this.spentTargets.has(`npc:${a.def.id}`) && (!best || d < best.dist)) best = { kind: 'npc', id: a.def.id, dist: d };
    }

    if (!best) {
      this.setPrompt(null);
      return;
    }
    this.setPrompt('按 E');
    // 同一帧内 UIScene 先用 E 关掉对话时，这次按下不得再触发新交互（250ms 冷却）
    if (Phaser.Input.Keyboard.JustDown(this.keyE) && Date.now() - director.dialogueClosedAt > 250) {
      this.spentTargets.add(`${best.kind}:${best.id}`); // 触发后须离开感应圈才可再触发
      if (best.kind === 'interact') void this.activateInteract(best.id);
      else void this.talkTo(best.id as NpcId);
    }
  }

  private setPrompt(text: string | null): void {
    if (text === this.lastPrompt) return;
    this.lastPrompt = text;
    director.emit('prompt', text);
  }

  private async activateInteract(id: string): Promise<void> {
    // 通用任务交互
    if (id.startsWith('flowerbed_')) {
      await this.waterFlowerbed(id);
      return;
    }
    if (id === 'water_point') {
      if (director.smallInteraction('water')) {
        await this.say({ lines: ['你浇了浇花。水珠亮晶晶的。'] });
      } else {
        await this.say({ lines: ['今天的浇水已经够多了。花儿说谢谢。'] });
      }
      return;
    }
    if (id.startsWith('berry_')) {
      await this.pickBerry(id);
      return;
    }
    // 子类专属
    const handled = await this.onInteract(id);
    if (!handled) {
      await this.say({ lines: ['……'] });
    }
  }

  // ---------- 门 ----------
  private async handleDoors(): Promise<void> {
    if (this.transitioning || director.dialogueActive || director.sceneTransition) return;
    const px = this.player.x;
    const py = this.player.y - 8;
    for (const d of this.doors) {
      const dist = Phaser.Math.Distance.Between(px, py, d.x, d.y);
      if (dist > 18) this.armedDoors.add(d); // 离开感应圈后才武装
      if (!this.armedDoors.has(d)) continue;
      if (dist <= 14) {
        // 先消耗武装再 await：对话关闭的同一帧内推离微任务尚未执行，
        // 若不消耗武装，update 会在玩家仍位于圈内时再触发一次（无限劝离循环的根因）
        this.armedDoors.delete(d);
        const ok = await this.canUseDoor(d.target);
        if (!ok) {
          // 劝离：沿"门→玩家"方向把玩家推出感应圈（原固定 +y 会把从上方接近的玩家推回门圈，造成劝离对话自循环）
          let nx = px - d.x;
          let ny = py - d.y;
          const len = Math.hypot(nx, ny);
          if (len < 1e-6) {
            nx = 0;
            ny = 1;
          } else {
            nx /= len;
            ny /= len;
          }
          // 测量点是 player.y-8，setPosition 时 y 补回 8
          this.player.setPosition(d.x + nx * 24, d.y + ny * 24 + 8);
          return;
        }
        if (director.celebrationActive) {
          director.escapeCelebration();
          await this.say({ lines: [...NARRATOR.celebrationEscape] });
        }
        this.transitioning = true;
        this.setPrompt(null);
        // 标记场景切换过场：抑制淡出期间 tick 触发庆典（庆典对话会被切场景覆盖，导致永久卡死）
        director.sceneTransition = true;
        this.cameras.main.fadeOut(250, 0, 0, 0);
        this.cameras.main.once('camerafadeoutcomplete', () => {
          const sceneKey = MAP_SCENES[d.target];
          if (sceneKey) this.scene.start(sceneKey, { spawn: d.spawn || 'default' });
          else this.transitioning = false;
        });
        return;
      }
    }
  }

  // ---------- 对话（通用） ----------
  protected async say(payload: DialoguePayload): Promise<number | null> {
    audio.beep();
    return director.showDialogue(payload);
  }

  protected async sayNpc(id: NpcId, lines: string[], choices?: DialogueChoice[]): Promise<number | null> {
    return this.say({ speakerId: id, lines, choices });
  }

  /** 与 NPC 对话（含任务分流、问好计数、纠缠登记） */
  protected async talkTo(id: NpcId): Promise<void> {
    const def = NPC_DEFS[id];

    // 群演：摸头小互动（无对话）
    if (def.isExtra) {
      if (director.smallInteraction('pat')) {
        await this.say({ lines: [`你摸了摸${def.name}的头。毛茸茸的。`] });
      } else {
        await this.say({ lines: [`${def.name}打了个哈欠。`] });
      }
      return;
    }

    // 双子鸟：喂食小互动
    if (id === 'bird') {
      if (director.smallInteraction('feed')) {
        await this.sayNpc('bird', ['啾啾！啾——（吃得很开心）']);
      } else {
        await this.sayNpc('bird', ['啾啾——啾啾——']);
      }
      return;
    }

    director.registerTalk(id);

    // 任务分流
    if (await this.taskTalk(id)) return;

    // C9：龟爷爷对话链（D2/D4/D6 递进）
    if (id === 'guiyeye' && [2, 4, 6].includes(director.day)) {
      await this.sayNpc('guiyeye', npcLinesOf('guiyeye', director.day), STANDARD_CHOICES).then((c) => this.afterChoice(id, c));
      director.addClue('C9');
      this.countGreet(id);
      return;
    }

    // 默认闲聊 + 三选项
    const choice = await this.sayNpc(id, npcLinesOf(id, director.day), STANDARD_CHOICES);
    await this.afterChoice(id, choice);
    this.countGreet(id); // T3/T10：与任意三位村民问好
  }

  private async afterChoice(id: NpcId, choice: number | null): Promise<void> {
    if (choice === null) return;
    const kind = STANDARD_CHOICES[choice]?.kind;
    if (!kind) return;
    director.applyChoice(kind);
    if (kind === 'gentle' || kind === 'hesitant' || kind === 'deny') {
      await this.sayNpc(id, [CHOICE_REPLIES[kind]]);
    }
  }

  /** 任务相关对话分流；返回 true 表示已处理 */
  private async taskTalk(id: NpcId): Promise<boolean> {
    const day = director.day;

    // T1/T8：送面包
    const breadTask = day === 1 ? 'T1' : day === 4 ? 'T8' : null;
    if (breadTask && director.isTaskActive(breadTask)) {
      // 从圆圆处取蜂蜜面包（与圆圆对话即可取，不必找到隐形柜台区）
      if (id === 'yuanyuan' && !director.taskProgress['breadTaken']) {
        director.taskProgress['breadTaken'] = 1;
        await this.sayNpc('yuanyuan', TAKE_BREAD_LINES);
        this.countGreet(id);
        return true;
      }
      if (director.taskProgress['breadTaken'] && id in BREAD_RECEIVERS) {
        const done = director.taskSetProgress['breadTo'] ?? new Set<string>();
        if (done.has(id)) {
          await this.sayNpc(id, ['面包已经收到啦，谢谢你！']);
          return true;
        }
        done.add(id);
        director.taskSetProgress['breadTo'] = done;
        await this.sayNpc(id, BREAD_RECEIVERS[id]);
        this.countGreet(id);
        if (done.size >= 3) {
          director.completeTask(breadTask);
          audio.ding();
          await this.say({ lines: ['面包都送到了。大家很幸福。'] });
        }
        return true;
      }
    }

    // T4：送信
    if (day === 2 && director.isTaskActive('T4')) {
      if (id === 'dabao' && !director.taskProgress['letters']) {
        director.taskProgress['letters'] = 3;
        await this.sayNpc('dabao', TAKE_LETTERS_LINES);
        this.countGreet(id);
        return true;
      }
      if (director.taskProgress['letters'] && id in LETTER_RECEIVERS) {
        const done = director.taskSetProgress['lettersTo'] ?? new Set<string>();
        if (done.has(id)) {
          await this.sayNpc(id, ['信已经收到啦。']);
          return true;
        }
        done.add(id);
        director.taskSetProgress['lettersTo'] = done;
        await this.sayNpc(id, LETTER_RECEIVERS[id]);
        this.countGreet(id);
        if (done.size >= 3) {
          // 第 3 封信署名乱码 0.2 秒（C4）
          director.emit('flashText', LETTER_GLITCH_TEXT, 200);
          await this.wait(400);
          director.addClue('C4');
          director.completeTask('T4'); // 先结算任务，C4 文本随后展示，避免任务态依赖对话关闭
          audio.ding();
          await this.say({ lines: CLUES.C4.lines });
        }
        return true;
      }
    }

    // T14/T15：与阿福对话指引任务地点（站立点/庆典场地无视觉标记）
    if (id === 'afu' && director.isTaskActive('T14')) {
      await this.sayNpc('afu', T14_HINT);
      return true;
    }
    if (id === 'afu' && director.isTaskActive('T15')) {
      await this.sayNpc('afu', T15_HINT);
      return true;
    }

    // T5：帮咪咪清点货架（与咪咪对话即可，不必找到隐形货架区）
    if (id === 'mimi' && director.isTaskActive('T5')) {
      await this.say({ lines: SHELF_COUNT_LINES });
      director.addClue('C5'); // 清点中发现：同一商品重复 9 格、标签空白
      await this.sayNpc('mimi', SHELF_COUNT_DONE);
      director.completeTask('T5');
      audio.ding();
      this.countGreet(id);
      return true;
    }

    // T11：帮圆圆揉面（可拒绝）
    if (day === 5 && id === 'yuanyuan' && director.isTaskActive('T11')) {
      const c = await this.sayNpc('yuanyuan', KNEAD_ASK, [
        { label: '（接过面团）', kind: 'accept' },
        { label: '（摇头拒绝）', kind: 'refuse' },
      ]);
      if (c === 0) {
        await this.sayNpc('yuanyuan', KNEAD_ACCEPT);
        director.completeTask('T11');
        audio.ding();
      } else {
        await this.sayNpc('yuanyuan', KNEAD_REFUSE);
        director.refuseTask('T11');
      }
      this.countGreet(id);
      return true;
    }

    return false;
  }

  /** T3/T10：与任意三位村民问好（去重计数） */
  protected countGreet(id: NpcId): void {
    const greetTask = director.day === 1 ? 'T3' : director.day === 4 ? 'T10' : null;
    if (!greetTask || !director.isTaskActive(greetTask)) return;
    const done = director.taskSetProgress['greeted'] ?? new Set<string>();
    if (done.has(id)) return;
    done.add(id);
    director.taskSetProgress['greeted'] = done;
    if (done.size >= 3) {
      director.completeTask(greetTask);
      audio.ding();
      void this.say({ lines: ['大家都很热情。今天也是美好的一天。'] });
    }
  }

  // T2/T9：给广场花坛浇水 ×3
  private async waterFlowerbed(id: string): Promise<void> {
    const task = director.day === 1 ? 'T2' : director.day === 4 ? 'T9' : null;
    if (!task || !director.isTaskActive(task)) {
      await this.say({ lines: ['花儿开得很好。'] });
      return;
    }
    const done = director.taskSetProgress['watered'] ?? new Set<string>();
    if (done.has(id)) {
      await this.say({ lines: WATER_FLOWER_DONE });
      return;
    }
    done.add(id);
    director.taskSetProgress['watered'] = done;
    await this.say({ lines: WATER_FLOWER_LINES });
    if (done.size >= 3) {
      director.completeTask(task);
      audio.ding();
    }
  }

  // T6：去花田采莓果 ×5
  private async pickBerry(id: string): Promise<void> {
    if (!director.isTaskActive('T6')) {
      await this.say({ lines: ['莓果还没熟透。'] });
      return;
    }
    const done = director.taskSetProgress['berries'] ?? new Set<string>();
    if (done.has(id)) {
      await this.say({ lines: BERRY_EMPTY_LINES });
      return;
    }
    done.add(id);
    director.taskSetProgress['berries'] = done;
    // 莓果丛(满) id16 → (空) id17
    const z = this.interactZones.find((zz) => zz.id === id);
    if (z) this.swapTileAt(z.x, z.y, 17, 18);
    await this.say({ lines: BERRY_PICK_LINES });
    if (done.size >= 5) {
      director.completeTask('T6');
      audio.ding();
    }
  }

  /** 把 (x,y) 附近 3×3 范围内第一个 fromGid 瓦片换成 toGid（莓果丛满→空等） */
  protected swapTileAt(x: number, y: number, fromGid: number, toGid: number): void {
    const layers = [this.obstacleLayer, this.groundLayer].filter((l): l is Phaser.Tilemaps.TilemapLayer => !!l);
    for (const layer of layers) {
      for (let dy = -16; dy <= 16; dy += 8) {
        for (let dx = -16; dx <= 16; dx += 8) {
          const tile = layer.getTileAtWorldXY(x + dx, y + dy, true);
          if (tile && tile.index === fromGid) {
            tile.index = toGid;
            return;
          }
        }
      }
    }
  }

  // ---------- 公告板（广场共用） ----------
  protected async showNoticeBoard(): Promise<void> {
    const lines: string[] = [announcementOf(director.day), '今日幸福任务：'];
    for (const t of tasksOfDay(director.day)) {
      const done = director.tasksDone.has(t.id);
      const refused = director.refusedTasks.has(t.id);
      lines.push(`${done ? '・（完成）' : refused ? '・（已拒绝）' : '・ '}${t.title}`);
    }
    await this.say({ speakerId: 'afu', lines });
  }

  // ---------- 欢乐庆典 ----------
  private async beginCelebration(): Promise<void> {
    if (!this.scene.isActive()) return;
    await this.say({ lines: [...NARRATOR.celebrationStart] });
    // 15 秒内不逃离 → 强制拉回 50
    this.time.delayedCall(15_000, async () => {
      if (!director.celebrationActive) return;
      director.completeCelebration();
      await this.say({ lines: [...NARRATOR.celebrationEnd] });
    });
  }

  private glanceNearest(): void {
    let best: NpcActor | null = null;
    let bestD = Infinity;
    for (const a of this.actors) {
      if (a.isExtra) continue;
      const d = Phaser.Math.Distance.Between(this.player.x, this.player.y, a.sprite.x, a.sprite.y);
      if (d < bestD) { bestD = d; best = a; }
    }
    if (best && bestD < 200) best.glanceAt(this.player.x);
  }

  // ---------- 脚本雨（D6 傍晚） ----------
  private updateRain(delta: number): void {
    const g = this.rainGraphics;
    if (!director.rainActive || !OUTDOOR_MAPS.includes(this.mapKey)) {
      g.clear();
      return;
    }
    g.clear();
    g.lineStyle(1, 0x8fd3ff, 0.55);
    this.rainTick += delta;
    const t = this.rainTick;
    for (let i = 0; i < 70; i++) {
      const x = (i * 53 + (t * 0.06)) % GAME_W;
      const y = (i * 97 + (t * 0.35)) % GAME_H;
      g.lineBetween(x, y, x - 1, y + 5);
    }
    // 站雨中 ≥10 秒计悲伤行为
    if (!this.rainCounted) {
      this.rainMs += delta;
      if (this.rainMs >= 10_000) {
        this.rainCounted = true;
        director.recordSadness('rain');
      }
    }
  }

  protected wait(ms: number): Promise<void> {
    return new Promise((resolve) => this.time.delayedCall(ms, resolve));
  }

  protected getPlayer(): Phaser.Physics.Arcade.Sprite {
    return this.player;
  }
}
