"""WorldState:客观世界状态——唯一真理(docs/agent-runtime-v2.md §6)。

- 地点拓扑/在场者/设施/物品/尸体/电力/天气,全部持久;
- 动作改变状态后,任何人此后的感知读到的都是同一份客观事实;
- describe(place) 生成"走进房间看到的样子"(状态快照),与事件流同源不矛盾。
键一律用地点/人物**中文名**(与 viz/positions 现约定一致)。
"""

from __future__ import annotations

from collections import defaultdict, deque
from dataclasses import dataclass, field

from genesis.obs.logging_setup import get_logger

logger = get_logger("runtime.worldstate")


@dataclass
class Facility:
    """可被改变状态的设施(电闸/船/灯塔油罐…)。state 变化即世界真理变化。"""

    id: str
    name: str
    place: str
    state: str                                  # 当前状态键
    desc: dict[str, str] = field(default_factory=dict)   # state -> 客观描述
    salient: bool = True                        # 进房间是否一眼可见
    hint: str = ""                              # 这东西能做什么/用什么动作/有何后果(感知时一并告知)
    verbs: dict[str, dict] = field(default_factory=dict)  # 动词→反应(G15 可供性):
    # {"feedback":给操作者, "event":广播文案|None, "scope":"place|global", "salience":float,
    #  "set_state":新state|None, "reveal_item":物品id|None}

    def line(self) -> str:
        return self.desc.get(self.state, f"{self.name}({self.state})")


@dataclass
class Item:
    """可拾取/移动/销毁的物品(剧本残页/账本/灯油…)。"""

    id: str
    name: str
    place: str = ""        # 所在地点("" = 被人持有或已销毁)
    holder: str = ""       # 持有人
    destroyed: bool = False
    hidden: bool = False   # 需要搜索才能发现(不在 describe 里直接出现)
    hint: str = ""         # 这件东西是干嘛的/能拿它做什么(感知时一并告知,让"道具→作用"明明白白)


class WorldState:
    def __init__(self, places: dict[str, dict]) -> None:
        """places: 中文名 -> {description, geography, adjacent:[中文名]}"""
        self.places = places
        self.adjacent: dict[str, list[str]] = {n: list(p["adjacent"]) for n, p in places.items()}
        self.positions: dict[str, str] = {}
        self.facilities: dict[str, Facility] = {}
        self.items: dict[str, Item] = {}
        self.bodies: dict[str, list[str]] = defaultdict(list)
        self.dead: dict[str, str] = {}
        self.death_info: dict[str, dict] = {}   # victim -> {place, killer, method, scripted} 客观死亡记录(验尸以此为准,不被剧本台词污染)
        self.weather = "暴雨"
        self.power_on = True

    # —— 拓扑 ——
    def occupants(self, place: str) -> list[str]:
        return sorted(a for a, p in self.positions.items() if p == place)

    def is_indoor(self, place: str) -> bool:
        return self.places.get(place, {}).get("geography") == "indoor"

    def path(self, src: str, dst: str, blocked=frozenset()) -> list[str]:
        """BFS 最短路(返回不含起点的逐跳地点序列;不可达返回 [])。blocked 中的地点绕开(如密道仅凶手可走)。"""
        if src == dst:
            return []
        if dst in blocked:
            return []
        prev: dict[str, str] = {src: ""}
        q = deque([src])
        while q:
            cur = q.popleft()
            for nxt in self.adjacent.get(cur, []):
                if nxt in blocked:
                    continue
                if nxt not in prev:
                    prev[nxt] = cur
                    if nxt == dst:
                        hops = [dst]
                        while prev[hops[-1]] != src:
                            hops.append(prev[hops[-1]])
                        return list(reversed(hops))
                    q.append(nxt)
        return []

    def connect(self, a: str, b: str) -> None:
        """运行时打通两地(如撬开/解锁青之房后,二层走廊 ⇄ 青之房)。"""
        if b not in self.adjacent.setdefault(a, []):
            self.adjacent[a].append(b)
        if a not in self.adjacent.setdefault(b, []):
            self.adjacent[b].append(a)

    def move(self, aid: str, to: str) -> bool:
        """逐跳移动一步(必须相邻);成功与否即客观结果。"""
        cur = self.positions.get(aid)
        if to not in self.adjacent.get(cur, []):
            logger.warning("%s 试图从 %s 跳到不相邻的 %s,拒绝", aid, cur, to)
            return False
        self.positions[aid] = to
        return True

    # —— 设施/物品:动作 → 持久状态 ——
    def add_facility(self, f: Facility) -> None:
        self.facilities[f.id] = f

    def set_facility(self, fid: str, state: str) -> Facility:
        f = self.facilities[fid]
        f.state = state
        logger.info("设施[%s]状态 → %s", f.name, state)
        return f

    def add_item(self, it: Item) -> None:
        self.items[it.id] = it

    def take_item(self, iid: str, by: str) -> Item | None:
        it = self.items.get(iid)
        if it and not it.destroyed and it.place and self.positions.get(by) == it.place:
            it.place, it.holder = "", by
            return it
        return None

    def destroy_item(self, iid: str) -> Item | None:
        it = self.items.get(iid)
        if it:
            it.place, it.holder, it.destroyed = "", "", True
        return it

    def kill(self, victim: str, place: str, method: str = "", killer: str = "", scripted: bool = False) -> None:
        self.dead[victim] = place
        self.bodies[place].append(victim)
        self.positions.pop(victim, None)
        self.death_info[victim] = {"place": place, "killer": killer, "method": method, "scripted": scripted}

    # —— 感知快照:走进一个地点,客观看到什么 ——
    def describe(self, place: str, viewer: str = "") -> list[str]:
        lines: list[str] = []
        others = [a for a in self.occupants(place) if a != viewer]
        if others:
            lines.append("在场:" + "、".join(others))
        for body in self.bodies.get(place, []):
            lines.append(f"⚠️ {body} 的尸体倒在这里,已经没了气息。")
        for f in self.facilities.values():
            if f.place == place and f.salient:
                # 优先显式 hint;否则自动列出这件设施可用的动作,让"能拿它做什么"一目了然
                h = f.hint or (("可" + "/".join(f.verbs.keys())) if f.verbs else "")
                lines.append(f.line() + (f"（{h}）" if h else ""))
        for it in self.items.values():
            if it.place == place and not it.hidden and not it.destroyed:
                lines.append(f"这里有:{it.name}" + (f"——{it.hint}" if it.hint else ""))
        if not self.power_on and self.is_indoor(place):
            lines.append("四下漆黑——电还没恢复,只能靠烛火和手电照明。")
        return lines


# ─────────────────── 道具层解耦:世界初始道具/设施 = 数据(items.json),引擎与世界无关 ───────────────────

def seed_from_data(ws: "WorldState", data: dict) -> None:
    """从数据装填初始设施+物品(道具层解耦):任何世界只需提供 items.json,引擎零改动。
    JSON 键与 Facility/Item 字段一一对应(由 dump_seed 产出,保证忠实往返)。"""
    for f in data.get("facilities", []):
        ws.add_facility(Facility(**f))
    for it in data.get("items", []):
        ws.add_item(Item(**it))


def dump_seed(ws: "WorldState") -> dict:
    """把当前世界的设施+物品序列化为 items.json 结构(用于把代码 seed 迁成数据,或存档)。"""
    from dataclasses import asdict
    return {"facilities": [asdict(f) for f in ws.facilities.values()],
            "items": [asdict(it) for it in ws.items.values()]}


def seed_world(world_dir, ws: "WorldState") -> None:
    """按世界目录装填道具层:有 items.json 走数据(解耦);否则回退到内置 seed_ravenisle(向前兼容)。"""
    import json
    from pathlib import Path
    p = Path(world_dir) / "items.json"
    if p.exists():
        seed_from_data(ws, json.loads(p.read_text(encoding="utf-8")))
        return
    if Path(world_dir).name == "ravenisle":      # 旧世界无 items.json 时的兜底(迁移完成后可删)
        seed_ravenisle(ws)


# ─────────────────── 雾鸦岛的初始客观状态(对照 docs/island-design-v3.md §6)───────────────────

def seed_ravenisle(ws: WorldState) -> None:
    F = Facility
    ws.add_facility(F("switchboard", "总电闸", "发电机房", "intact", {
        "intact": "整面墙的闸刀锈迹斑斑,唯独总电闸的手柄被人擦得干干净净。",
        "smashed": "总电闸被人砸毁了,闸刀歪斜、电线裸露——全岛因此断电。",
        "repaired": "总电闸缠着应急胶布勉强复位,电力恢复了,但看得出被人砸过。"},
        hint="全岛供电的总开关:用『砸电闸』会让整座堡瞬间漆黑(黑暗里最易出事、也是制造混乱的手段);停电后用『修电闸』复电"))
    ws.add_facility(F("boat", "码头缆桩", "码头", "cut", {
        "cut": "系船桩上只剩半截缆绳,断口平整——是刀割的,船早被放走了。"},
        hint="船缆已被人割断、船被放走——这条路逃不了,只能另想办法(灯塔求救)或熬到第七天渡船"))
    ws.add_facility(F("skiff", "备用小艇", "船屋", "holed", {
        "holed": "备用小艇翻扣在架上,船底被凿出三个整齐的洞。"},
        hint="唯一的备用小艇被凿穿、开不走——水路逃生无望,有人蓄意断了所有人的退路"))
    ws.add_facility(F("light_tank", "灯塔储油罐", "灯塔", "empty", {
        "empty": "灯室完好,储油罐却被提前搬空——想点灯求救,得先找到灯油。",
        "fueled": "储油罐重新注了油,只差有人上去点亮塔灯。",
        "lit": "塔灯已被点亮!雪亮的光柱扫过海面——大陆方向应该能看见。"},
        hint="全岛唯一的求救手段:把灯油(船屋/储藏室/温室能搜到)搬来用『加油』灌满,再用『点亮』点灯,对岸就能看见来救——但灯一亮渡船提前到"))
    ws.add_facility(F("knife_rack", "厨房刀架", "厨房", "one_missing", {
        "one_missing": "刀架按尺寸排得一丝不苟——有一格是空的。"},
        hint="少了一把刀——凶器可能就出自这里,留意谁身上有刀"))
    ws.add_facility(F("film_boxes", "七个胶片盒", "餐厅", "act0", {
        **{f"act{i}": (f"长桌中央一字排开七个铁皮胶片盒——前 {i} 个已被打开,里面的胶片烧成了灰。" if i else
                       "长桌中央一字排开七个铁皮胶片盒,标签从『第一幕』写到『第七幕』,都还封着。") for i in range(8)}},
        hint="像一座倒计时:每死一个人,就有一盒『第N幕』自动烧成灰——烧到第七幕,意味着大限将至"))
    ws.add_facility(F("master_door", "青之房的门", "二层走廊", "locked", {
        "locked": "走廊正中那扇双开门紧锁着,门缝透出极淡的栀子香。",
        "open": "青之房的门敞开着——十年未动的房间暴露在所有人面前。"},
        hint="锁了十年的青之房(阮青旧居,里面藏着她最后的日记):有黄铜钥匙就用『钥匙开青之房』无声打开;没钥匙也能『撬开青之房』,但动静极大、全堡可闻"))
    # 线索物品(hidden = 要搜索才会发现;每件都带 hint=用途,让"道具→作用"明明白白)
    for i, (pid, place) in enumerate([("p1", "码头"), ("p2", "餐厅"), ("p3", "琴房"), ("p4", "荒废教堂"),
                                      ("p5", "发电机房"), ("p6", "断崖"), ("p7", "灯塔")], 1):
        ws.add_item(Item(f"script_{pid}", f"《雾鸦》剧本残页·第{i}幕", place, hidden=True,
                         hint="拼齐各幕残页就能看出凶手在按《雾鸦》逐幕『补拍』杀人,预判下一个死者、地点与死法,提前设防"))
    ws.add_item(Item("ledger", "封口账本与七份协议", "书房", hidden=True,
                     hint="邵铭轩的封口账本——记着每个人收的脏钱,见光七人皆完(邵最先完);邵想烧掉它,别人拿到则是要挟与指证的铁证"))
    ws.add_item(Item("negatives", "《雾鸦》负片母带", "暗房", hidden=True,
                     hint="当年实拍的负片母带——顾长风想找到并烧进门厅壁炉销毁;它能证明那晚究竟发生了什么"))
    ws.add_item(Item("callsheet", "当晚的通告单与酒水单", "道具间", hidden=True,
                     hint="白纸黑字记着程亦深当晚喝醉走错位——程想撕掉;别人拿到能戳穿他甩锅给死者的谎"))
    ws.add_item(Item("diary", "阮青的日记", "青之房", hidden=True,
                     hint="阮青最后的日记——记着唐曼逼她『这一跳你就红了』的原话;唐曼想销毁,别人拿到是指证唐曼的铁证"))
    ws.add_item(Item("med_record", "真实急救记录原件", "冰窖", hidden=True,
                     hint="白聿当年签过假医疗记录,这份真原件能揭穿被掩盖的真实死因;白聿想销毁或公开,自己也没拿定主意"))
    ws.add_item(Item("draft", "带真名批注的报道原稿", "书房", hidden=True,
                     hint="温言当年收钱写黑稿的原稿、带她真名批注——温言想抢先抽走销毁,别人拿到能揭穿她"))
    for i, place in enumerate(["船屋", "储藏室", "温室"], 1):
        ws.add_item(Item(f"fuel_{i}", "一桶灯油", place, hidden=True,
                         hint="搬去灯塔倒进储油罐(用『加油』),集齐三桶就能『点亮』塔灯向大陆求救——这是今夜唯一靠谱的逃生指望;但灯一亮渡船会提前出发"))
    ws.add_item(Item("master_key", "青之房的黄铜钥匙", hidden=True,
                     hint="开二层走廊那扇锁了十年的青之房双开门(到二层走廊用『钥匙开青之房』)"))  # holder 由引擎设给凶手
    # ── 可供性全量(G15):每个地点都有能"动手"的东西 ──
    ws.add_item(Item("shoot_log", "当年的拍摄日志(记录第七幕实拍决定)", "温室", hidden=True,
                     hint="记录第七幕实拍那晚的决定——能印证『不用替身,再来一条』是谁拍的板"))
    ws.add_item(Item("burnt_photo", "烧剩一半的剧组合影", "阁楼", hidden=True,
                     hint="烧剩一半的旧合影——当年剧组人事关系的线索"))
    F2 = Facility
    ws.add_facility(F2("bell", "教堂吊钟", "荒废教堂", "idle",
        {"idle": "锈死多年的吊钟悬在钟楼里,钟绳垂到地面。"},
        verbs={"敲钟": {"feedback": "你拽下钟绳——沉闷的钟声滚过整座岛,惊起一片鸦群。",
                       "event": "教堂的钟被人敲响了!沉闷的声音滚过全岛——岛上的老传说:鸦堡死人的夜里,钟会自己响。",
                       "scope": "global", "salience": 0.85}}))
    ws.add_facility(F2("soil", "温室土畦", "温室", "fresh",
        {"fresh": "几畦泥土新翻不久,一把雪亮的园艺剪插在土里。",
         "dug": "土畦被彻底挖开,坑底的油布已经掀开了。"},
        verbs={"挖掘": {"feedback": "你用园艺剪撬开湿土——油布下面埋着当年的摄影器材箱,里面有一本拍摄日志!",
                       "event": "有人在温室挖开了那畦新土", "scope": "place", "salience": 0.5,
                       "set_state": "dug", "reveal_item": "shoot_log"}}))
    ws.add_facility(F2("gravestone", "阮青的墓碑", "后山墓园", "clean",
        {"clean": "唯独这块青石碑干净得反常,碑前一束白菊还没蔫。"},
        verbs={"查看": {"feedback": "碑文:『阮青之墓 1994-2016』。落款没有名字,只刻着半句词:"
                                  "『戏未杀青,人不散场』。碑前的白菊,断口很新——是登岛那天剪的。",
                       "event": None, "scope": "place", "salience": 0.3}}))
    ws.add_facility(F2("keeper_log", "灯塔看守人日志", "灯塔", "dusty",
        {"dusty": "值班台上压着一本受潮的看守人日志。"},
        verbs={"查看": {"feedback": "日志最后一页(三年前):『又一艘私人快艇夜里靠岸,没report。"
                                  "船上的人上了岛,天亮才走。』之后的页码被整齐地撕掉了。",
                       "event": None, "scope": "place", "salience": 0.3}}))
    ws.add_facility(F2("camphor_chest", "樟木箱", "阁楼", "locked",
        {"locked": "角落的樟木箱上着一把新换的锁。", "pried": "樟木箱被撬开了,箱盖歪在一边。"},
        verbs={"撬开": {"feedback": "锁被你撬断——箱里是叠得整齐的旧戏服,最底下压着半张烧剩的剧组合影。",
                       "event": "阁楼里传来撬锁的金属声", "scope": "place", "salience": 0.5,
                       "set_state": "pried", "reveal_item": "burnt_photo"}}))
    ws.add_facility(F2("bar_glasses", "酒柜与七只酒杯", "大客厅", "poured",
        {"poured": "酒柜里水晶瓶一尘不染——酒是新斟的,七只杯子,不多不少。"},
        verbs={"检查": {"feedback": "你逐只端详:杯沿干净,酒色清亮——但谁有胆子喝第一口?"
                                  "杯底压着的杯垫,是当年杀青宴定制的那批。",
                       "event": None, "scope": "place", "salience": 0.3}}))

    # ── G16:补齐 17 个空场景的可供性(让每处都值得探索、都连着剧情;凶手也多了取景地)──
    SEARCH_EVT = {"event": "隔壁有人在翻找东西,窸窸窣窣的。", "scope": "place", "salience": 0.4}
    # 七间卧室:各自的私人破绽(搜别人的房=社交越界,制造戏剧)
    ws.add_item(Item("kill_plan", "七幕『补拍』排期表", hidden=True,
                     hint="谁、在哪取景地、第几幕动手,墨迹是新的——这是直指凶手按《雾鸦》补拍的铁证,写《雾鸦》又来补第七幕的只有江离"))   # 江离房间的终极线索
    ws.add_facility(F2("gu_room", "顾长风床头的分镜本", "顾长风的房间", "idle",
        {"idle": "床头摊开一本翻烂的分镜本,压着一沓『十周年纪录片企划』。"},
        verbs={"搜": {"feedback": "企划扉页写满『翻身之作』四个字;夹着一张当年的报道剪影,被反复摩挲——他至死不肯认那句『不用替身,再来一条』是自己喊的。", **SEARCH_EVT}}))
    ws.add_facility(F2("shao_room", "邵铭轩的对账手机", "邵铭轩的房间", "idle",
        {"idle": "行李箱半开,一台还亮着屏的手机搁在西装上。"},
        verbs={"搜": {"feedback": "锁屏跳着银行到账提醒;一张便签写着一串数字的前两位——像是某个保险柜的密码。那本封口账本就锁在书房。", **SEARCH_EVT}}))
    ws.add_facility(F2("cheng_room", "程亦深枕下的镇定药", "程亦深的房间", "idle",
        {"idle": "床头滚着两只空酒瓶,枕头下露出半板药。"},
        verbs={"搜": {"feedback": "镇定剂吃了大半,他怕得睡不着;垃圾桶里揉着撕碎的通告单复印件——记着他那晚醉态的正本,还在道具间的场记箱里。", **SEARCH_EVT}}))
    ws.add_facility(F2("tang_room", "唐曼梳妆台的抽屉", "唐曼的房间", "idle",
        {"idle": "梳妆台上摊着给新人的合约,最下一格抽屉没关严。"},
        verbs={"搜": {"feedback": "抽屉夹层里是一套小巧的撬锁工具和一张青之房的平面图——她铁了心要进那间上锁的房,拿回阮青那本日记。", **SEARCH_EVT}}))
    ws.add_facility(F2("bai_room", "白聿的医药箱", "白聿的房间", "idle",
        {"idle": "床头一只医药箱,针剂排得一丝不苟。"},
        verbs={"搜": {"feedback": "镇静剂用了大半;一叠急救记录草稿被反复涂改撕扯——他签过一份假的,真正那份原件,藏在了冰窖的旧医疗箱里。", **SEARCH_EVT}}))
    ws.add_facility(F2("wen_room", "温言的录音笔", "温言的房间", "idle",
        {"idle": "桌上一支录音笔还亮着红灯,旁边摊着写满问号的采访提纲。"},
        verbs={"搜": {"feedback": "提纲上列着在场每个人的旧账;她为这条十年独家什么险地都敢闯——也最怕自己那篇署了真名的黑稿原稿被人先一步抽走(在书房剪报册里)。", **SEARCH_EVT}}))
    ws.add_facility(F2("jiang_room", "江离的采风笔记本", "江离的房间", "idle",
        {"idle": "屋里干净得不像住过人,只有一本摊开的采风笔记本。"},
        verbs={"搜": {"feedback": "你翻到笔记本的夹层——一张折叠的纸滑出来:七幕『补拍』排期表,谁、在哪取景地、第几幕动手,墨迹是新的!写《雾鸦》、又被请来『补完第七幕』的,只有他一个人。",
                      "event": "江离房里有人在翻东西。", "scope": "place", "salience": 0.45, "reveal_item": "kill_plan"}}))
    # 城堡公共空场景
    ws.add_facility(F2("hall_projector", "门厅的旧放映机", "门厅", "idle",
        {"idle": "门厅角落那台老放映机,胶片还挂在齿轮上,镜头对着墙。"},
        verbs={"重放": {"feedback": "你按下开关——墙上又映出那行手写字幕:『诸位老师,《雾鸦》杀青宴,现在开始。——长夜影业』。机器被人改装成会自己启动:这场局,早布好了。",
                       "event": "门厅的放映机又自己转了起来,惨白的光打在墙上。", "scope": "place", "salience": 0.5},
               "查看": {"feedback": "放映机被人动过手脚,接了个定时启动的机关——不是闹鬼,是有人精心安排。", "scope": "place", "salience": 0.35}}))
    ws.add_facility(F2("screening_reel", "放映室的未剪拷贝", "放映室", "idle",
        {"idle": "拷贝架上,『第七幕·未剪』那盘带子被人抽出来,搁在最显眼的地方。"},
        verbs={"重放": {"feedback": "你穿上带子重放:收音轨里清清楚楚——狂风、阮青带哭腔的『风太大了,我害怕』,和一个男人冷冷的『不用替身,再来一条』。那是顾长风的声音。",
                       "event": "放映室传出十年前的收音:风声,和一句『不用替身,再来一条』。", "scope": "global", "salience": 0.6}}))
    ws.add_facility(F2("cellar_rack", "酒窖最里排的酒架", "酒窖", "idle",
        {"idle": "最里排的酒架透着穿堂风,像是和墙之间有缝。"},
        verbs={"查看": {"feedback": "酒架后面是一道砖砌的暗墙,缝里漏出海腥味的穿堂风——这堡里的暗道,不止一条。", "scope": "place", "salience": 0.4},
               "搬": {"feedback": "你费力挪开酒架,砖墙后果然是黑黢黢的夹道,通向地下更深处。", "scope": "place", "salience": 0.45}}))
    ws.add_facility(F2("storeroom_shelf", "储藏室角落的旧木架", "储藏室", "idle",
        {"idle": "靠墙一排旧木架,落了厚灰——唯独最里那座,地上的灰被蹭出几道新鲜的拖痕。"},
        verbs={"查看": {"feedback": "你推了推最里那座木架——它竟能整个挪开!后面是个仅容一人的黑窟窿,一股海腥味的穿堂风扑面而来。"
                                  "这是条暗道。架子边沿有新鲜的手印和来回的鞋印:有人一直在用它神不知鬼不觉地来去。",
                       "event": "储藏室里传来挪动木架的刺啦声", "scope": "place", "salience": 0.5},
               "搜": {"feedback": "木架后藏着一条暗道,边上鞋印来回好几趟——谁知道这条道,谁就能在城堡和海边之间凭空消失再出现。",
                      "scope": "place", "salience": 0.45}}))
    ws.add_facility(F2("passage_tunnel", "储藏室通船屋的密道", "密道", "idle",
        {"idle": "这条砖砌窄道从储藏室木架后一直通到海边船屋,壁上留着新鲜鞋印。"},
        verbs={"查看": {"feedback": "鞋印来回好几趟,鞋码不小、沾着海沙——谁知道这条道,就能在城堡和海边之间神不知鬼不觉地来去,做出一副不在场的样子。", "scope": "place", "salience": 0.4}}))
    ws.add_facility(F2("corridor3_photos", "三层走廊的剧照墙", "三层走廊", "idle",
        {"idle": "走廊墙上挂着一排《雾鸦》剧照,落满了灰。"},
        verbs={"查看": {"feedback": "其中一张:阮青和一个低头记笔记的年轻男人并肩站着,身体挨得很近——那是当年的编剧江离。两人的距离,不像同事。", "scope": "place", "salience": 0.35}}))
    ws.add_facility(F2("balcony_scope", "露台的黄铜望远镜", "露台", "idle",
        {"idle": "露台栏杆上锈着一架黄铜望远镜,正对着海面和远处的灯塔。"},
        verbs={"查看": {"feedback": "你举镜望出去:海面死黑,没有一点船灯;灯塔也黑着。想让对岸看见这座岛,得先有人爬上灯塔把那盏灯点亮。", "scope": "place", "salience": 0.35},
               "瞭望": {"feedback": "镜里只有翻涌的黑浪和熄着的灯塔——求救的唯一指望,是点亮那座塔。", "scope": "place", "salience": 0.35}}))
    # 室外空场景
    ws.add_facility(F2("forecourt_tracks", "前院泥地的拖痕", "前院", "idle",
        {"idle": "前院的泥地上有几道拖拽的痕迹,雨还没冲干净。"},
        verbs={"查看": {"feedback": "拖痕从大门一路拖向码头方向,半截湿透的鞋套陷在泥里——有人往海边运过沉重的东西。", "scope": "place", "salience": 0.4}}))
    ws.add_facility(F2("beach_flotsam", "沙滩退潮线的杂物", "沙滩", "idle",
        {"idle": "退潮线上堆着海浪卷上来的杂物,湿漉漉的。"},
        verbs={"搜": {"feedback": "你翻拣杂物:一截被刀割断的船缆、一只进了水的手电——还有件硬物半埋在沙里,像是有人匆匆丢弃的东西。这片海,藏得住秘密。", "scope": "place", "salience": 0.4}}))
    ws.add_facility(F2("garden_bench", "花园深处的石凳", "花园", "idle",
        {"idle": "花园最深处一张石凳,凳面像是刻过字。"},
        verbs={"查看": {"feedback": "凳面刻着一个字:『青』。凳边湿土里插着一束还没蔫的白菊——和后山墓园碑前那束,是同一天剪的。有人记着她。", "scope": "place", "salience": 0.35}}))
    ws.add_facility(F2("woods_sign", "断崖小径口的警示牌", "风暴林", "idle",
        {"idle": "风暴林尽头通向断崖的小径口,新立着一块木牌。"},
        verbs={"查看": {"feedback": "木牌写着『崖边湿滑·小心脚下』,油漆还没干透——封岛的暴雨夜,谁会专程来这僻静地方,新刷这么一块牌子?像是为了让什么'失足意外'显得更真。", "scope": "place", "salience": 0.42}}))
