Honey Village: six model builds + hub frontend, Dockerized for Dokploy
@@ -0,0 +1,125 @@
|
||||
# CONTRACT.md — 资产 / 地图 / 代码三方契约(唯一真源)
|
||||
|
||||
工作目录:`/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3`
|
||||
PRD:同目录 `PRD.md`(所有剧情/数值/台词以 PRD 为准,本文件只定义技术接口)。
|
||||
|
||||
工具路径:
|
||||
- Aseprite: `/Applications/Aseprite.app/Contents/MacOS/aseprite`(用法:`aseprite -b --script file.lua`;绘制用 `app.activeImage:drawPixel(x, y, app.pixelColor.rgba(r,g,b,a))`;导出用 `spr:saveAs("out.png")`。已验证可用)
|
||||
- Tiled 1.12.2: `/Applications/Tiled.app/Contents/MacOS/Tiled`(用法:`Tiled -e script.js` 执行脚本,脚本内用 `var f = new TextFile(path, TextFile.WriteOnly); f.write(xml); f.commit();` 写 .tmx;再用 `Tiled --export-map --embed-tilesets in.tmx out.json` 导出 JSON。已验证可用;注意 `-e` 模式无编辑器,`tiled.open`/`TileMap` 部分 API 不可用,一律走 TextFile 写 XML)
|
||||
|
||||
## 1. 目录结构
|
||||
|
||||
```
|
||||
public/assets/
|
||||
tileset.png # 256×128,16×16 瓦片,16 列 × 8 行 = 128 瓦片
|
||||
sprites/player.png # 64×96:帧 16×24,4 列(行走帧 0-3) × 4 行(方向 0=down 1=up 2=left 3=right),第 0 帧兼待机
|
||||
sprites/npc_afu.png # 48×24:3 帧横排 = smile / flat / off
|
||||
sprites/npc_yuanyuan.png # 同上
|
||||
sprites/npc_dabao.png # 同上
|
||||
sprites/npc_mimi.png # 同上
|
||||
sprites/npc_fuyisheng.png # 同上
|
||||
sprites/npc_guiyeye.png # 同上
|
||||
sprites/npc_bird.png # 32×24:2 帧 = 闭嘴 / 张嘴
|
||||
sprites/extra_squirrel.png # 32×24:2 帧循环
|
||||
sprites/extra_hedgehog.png # 同上
|
||||
sprites/extra_fox.png # 同上
|
||||
sprites/extra_pig.png # 同上
|
||||
sprites/extra_chick.png # 同上
|
||||
portraits/player_smile.png # 32×32
|
||||
portraits/player_real.png # 32×32(E3 用:微笑消失、疲惫真实的脸)
|
||||
portraits/afu.png yuanyuan.png dabao.png mimi.png fuyisheng.png guiyeye.png bird.png # 各 32×32,smile
|
||||
maps/house.json plaza.json bakery.json shop.json clinic.json lake.json field.json well.json fog.json white.json
|
||||
maps/*.tmx # Tiled 中间产物(保留在仓库)
|
||||
maps/tileset.tsx # 外部瓦片集描述,image source="../tileset.png"
|
||||
tools/aseprite/*.lua # 素材生成脚本(保留)
|
||||
tools/tiled/genmaps.js # 地图生成脚本(保留)
|
||||
src/ # 游戏代码
|
||||
```
|
||||
|
||||
素材规范(PRD §11):糖果色板(草绿 #7DC95E/#5FB34A、奶油黄 #FFF3D6、蜜糖橙 #F5A623、天空蓝 #8FD3FF、石路 #E8DCC8、湖水 #6EC1E4/#4FA8D8、粉 #F7B7CB、棕 #8B5A2B),白色层独立灰白板(#F5F5F5/#DADADA/#BDBDBD)。无抗锯齿、无渐变。`off` 档与 `smile` 档差异控制在 1–2 像素(如一只眼低 1px 或嘴角多 1px),不许鬼脸。背景透明(alpha=0)。PNG 必须精确尺寸(上面列出)。
|
||||
|
||||
## 2. 瓦片 ID 表(tileset.png,id = 行×16+列;地图数据 gid = id+1)
|
||||
|
||||
| id | 名称 | id | 名称 |
|
||||
|---|---|---|---|
|
||||
| 0 草地 | 1 草+花A | 2 草+花B | 3 深草 |
|
||||
| 4 土路 | 5 石路 | 6 石路alt | 7 广场砖 |
|
||||
| 8 湖水A | 9 湖水B | 10 水岸北 | 11 水岸南 |
|
||||
| 12 水岸西 | 13 水岸东 | 14 花坛粉 | 15 花坛黄 |
|
||||
| 16 莓果丛(满) | 17 莓果丛(空) | 18 树左上 | 19 树右上 |
|
||||
| 20 树左下 | 21 树右下 | 22 圆灌木 | 23 树桩 |
|
||||
| 24 石头 | 25 路标 | 26 栅栏横 | 27 栅栏竖 |
|
||||
| 28 路灯 | 29 彩带红 | 30 彩带黄 | 31 彩带蓝 |
|
||||
| 32 奶油墙 | 33 奶油墙+窗 | 34 木板墙 | 35 橙屋顶左 |
|
||||
| 36 橙屋顶中 | 37 橙屋顶右 | 38 门 | 39 门框顶 |
|
||||
| 40 窗 | 41 烟囱 | 42 井左上 | 43 井右上 |
|
||||
| 44 井左下 | 45 井右下 | 46 邮筒 | 47 公告板 |
|
||||
| 48 木地板A | 49 木地板B | 50 室内墙 | 51 室内墙+窗 |
|
||||
| 52 床左上 | 53 床右上 | 54 床左下 | 55 床右下 |
|
||||
| 56 柜台左 | 57 柜台右 | 58 烤炉 | 59 空货架 |
|
||||
| 60 满货架 | 61 桌子 | 62 椅子 | 63 地毯 |
|
||||
| 64 日历 | 65 配方纸 | 66 文件纸 | 67 野餐布左上 |
|
||||
| 68 野餐布右上 | 69 野餐布左下 | 70 野餐布右下 | 71 长椅左 |
|
||||
| 72 长椅右 | 73 凳子 | 74 柜子 | 75 冰箱 |
|
||||
| 76 水槽 | 77 盆栽 | 78 木桶 | 79 木箱 |
|
||||
| 80 雾A | 81 雾B | 82 雾C | 83 白色地板 |
|
||||
| 84 白色墙 | 85 白墙板 | 86 白门 | 87 白床左上 |
|
||||
| 88 白床右上 | 89 白床左下 | 90 白床右下 | 91 白柜 |
|
||||
| 92 白监护仪 | 93 白窗帘 | 94 白地板alt | 95 白天花灯 |
|
||||
| 96 梯子 | 97 桥横 | 98 桥竖 | 99 蘑菇 |
|
||||
| 100 花红 | 101 花黄 | 102 花蓝 | 103 蜂蜜罐 |
|
||||
| 104 面包 | 105 信件 | 106 书 | 107 蜡烛 |
|
||||
| 108 篮子 | 109 水桶 | 110 扫帚 | 111 蛋糕 |
|
||||
| 112 红屋顶左 | 113 红屋顶中 | 114 红屋顶右 | 115 蓝屋顶左 |
|
||||
| 116 蓝屋顶中 | 117 蓝屋顶右 | 118 粉墙 | 119 粉墙+窗 |
|
||||
| 120 石墙 | 121 石墙+窗 | 122 遮阳篷 | 123 钟 |
|
||||
| 124 旗 | 125 花环角 | 126 花环中 | 127 空(透明) |
|
||||
|
||||
## 3. 地图契约(Tiled 导出 JSON;图层固定三层)
|
||||
|
||||
每层地图固定:
|
||||
- `ground`(瓦片层,无碰撞;必须全部用不透明瓦片——透明装饰瓦片只能放 `obstacles` 层,否则透明处露出黑底。大花 100-102、蘑菇 99、门 38 已在 tileset 里烘焙不透明底,可放 ground)
|
||||
- `obstacles`(瓦片层,带图层自定义属性 `collides=true`(bool);代码对该层整层碰撞,但彩带(瓦片 29-31)在代码里排除碰撞、可穿过)
|
||||
- `objects`(对象层,点对象)
|
||||
|
||||
对象约定(name + 自定义属性):
|
||||
- `name="spawn"`,属性 `id`(string):出生点。各图必有 `default`;室内图另有 `from_plaza`;plaza 有 `from_house/from_bakery/from_shop/from_clinic/from_lake/from_field/from_well/from_fog`。
|
||||
- `name="door"`,属性 `target`(string, 目标地图 key) + `spawn`(string, 目标 spawn id)。双向成对。
|
||||
- 落点安全:spawn 与任一门判定点的距离(以玩家测量点 y-8 计)必须 >14px(惯例 ≥24px);运行时另有门武装机制兜底——落到门旁时须先离开感应圈(>18px)才允许触发,且触发即消耗武装(须再次离开 >18px 才可重新触发),防止落点回弹与对话关闭帧的重复触发。
|
||||
- 2x2 拼块瓦片(树 18-21、井 42-45、床 52-55、野餐布 67-70、白床 87-90):四个象限占用连续 4 个 id;tileset 生成时把下半两个象限从自然落点(id+16/id+17)复制到 id+2/id+3。
|
||||
- `name="npc"`,属性 `id`(string):afu / yuanyuan / dabao / mimi / fuyisheng / guiyeye / bird / squirrel / hedgehog / fox / pig / chick。
|
||||
- `name="interact"`,属性 `id`(string):见下表。
|
||||
|
||||
10 张地图(尺寸 = 瓦片数;场景 key 同文件名):
|
||||
|
||||
| key | 文件 | 尺寸 | 必填对象 |
|
||||
|---|---|---|---|
|
||||
| house | maps/house.json | 12×8 | spawn(default,from_plaza)、door→plaza、interact:bed、interact:calendar |
|
||||
| plaza | maps/plaza.json | 30×20 | spawn(default,from_house,from_bakery,from_shop,from_clinic,from_lake,from_field,from_well,from_fog)、door×8(house/bakery/shop/clinic/lake/field/well/fog)、npc:afu(公告板旁)、npc:bird(树上)、npc:squirrel、npc:fox、interact:noticeboard、interact:flowerbed_1/2/3 |
|
||||
| bakery | maps/bakery.json | 15×10 | spawn(default,from_plaza)、door→plaza、npc:yuanyuan、interact:counter_bread、interact:recipe_paper |
|
||||
| shop | maps/shop.json | 15×10 | spawn(default,from_plaza)、door→plaza、npc:mimi、npc:hedgehog、interact:shelf_count |
|
||||
| clinic | maps/clinic.json | 12×8 | spawn(default,from_plaza)、door→plaza、npc:fuyisheng、interact:doctor_desk |
|
||||
| lake | maps/lake.json | 24×16 | spawn(default,from_plaza)、door→plaza、npc:guiyeye(长椅旁)、npc:pig、interact:picnic_mat |
|
||||
| field | maps/field.json | 20×14 | spawn(default,from_plaza)、door→plaza、npc:chick、interact:berry_1..5、interact:water_point |
|
||||
| well | maps/well.json | 14×10 | spawn(default,from_plaza)、door→plaza、interact:ancient_well |
|
||||
| fog | maps/fog.json | 20×8 | spawn(default,from_plaza)、door→plaza、interact:fog_wall |
|
||||
| white | maps/white.json | 20×14 | spawn(default);上半 20×6 走廊 + 左下 10×8 病房,内部连通;白色层瓦片(83–95) |
|
||||
|
||||
地图要点(PRD §10):plaza 为枢纽,含公告板(47)、花坛(14/15)、双子鸟树(18-21)、庆典空地;室外场景用草地/石路/栅栏/房屋装饰铺满,lake 有大片湖水(8-13)与长椅(71/72),field 有莓果丛(16),well 有古井(42-45),fog 边缘铺雾(80-82),white 全用白色层瓦片(走廊+病房,病房内放白床 87-90 与白监护仪 92)。室内场景:墙(50/51)围边、木地板(48/49)、家具按 PRD §10 要点摆放。house 必须有床(52-55)与日历(64);bakery 有柜台(56/57)、烤炉(58)、配方纸(65);shop 有货架(59/60);clinic 有桌子(61)+文件纸(66)。
|
||||
|
||||
## 4. 代码架构契约(Phaser 3 + TS + Vite)
|
||||
|
||||
- `src/main.ts`:Phaser config,逻辑 320×180,`pixelArt:true`,`scale:{ mode: Phaser.Scale.NONE, autoCenter: CENTER_BOTH, parent:'game' }` + window resize 监听做整数倍放大(canvas style 宽高 = 320k×180k,k = max(1, floor(min(winW/320, winH/180))))。
|
||||
- 场景 key:`BootScene / TitleScene / HouseScene / PlazaScene / BakeryScene / ShopScene / ClinicScene / LakeScene / FieldScene / WellScene / FogScene / WhiteScene / UIScene / DreamScene / EndingScene`。
|
||||
- 地图场景共用基类 `MapScene`(`src/scenes/MapScene.ts`):子类只提供 `mapKey` 与少量钩子。基类负责:tilemap 加载(`this.load.tilemapTiledJSON`,tileset 名 `candy` 对应 `assets/tileset.png`)、player(96 px/s、四向动画 walk-down/up/left/right + 待机帧、obstacles 层碰撞、相机跟随+钳制)、≤24px 出「按 E」提示(交 UIScene 显示)、door 切换(fade out/in + 目标 spawn)、NPC/群演实例化与表情档切换、注册 CorruptionDirector 的视听参数(相机 postFX:ColorMatrix 饱和度 / Vignette 晕影 / 负片单帧)。
|
||||
- `CorruptionDirector`(`src/core/CorruptionDirector.ts`):普通 TS 单例 + `Phaser.Events.EventEmitter`。状态:playerName、startTime(ms)、day(1-7)、happiness(0-100)、awareness(0-100)、clues(Set)、tasksDone(Set)、refusedTasks(Set)、escapedCelebrations、stage 派生值。事件名:`happiness` `awareness` `stage` `day` `clue` `task` `celebration` `ending`。是全部状态唯一真源;提供 debug 用 setter。
|
||||
- 对话:`UIScene` 底部通栏框(说话者名 + 头像 + 文本,E 推进;选项三选:温柔/迟疑/否定);文本腐蚀在渲染层按 §6.5 表替换(原文案干净)。头像用 portraits/。
|
||||
- 音频:`src/audio/AudioEngine.ts`,WebAudio 程序合成(无音频文件)。
|
||||
- Debug:`src/debug/DebugPanel.ts`,反引号键开关的 DOM 面板(默认隐藏、无 UI 痕迹):设幸福/觉知/日(1-7)/传送场景/置齐线索/强制结局 E1-E4。
|
||||
- 数值与台词逐字逐数实现 PRD §5–§9。
|
||||
|
||||
## 5. 验收相关硬约束
|
||||
|
||||
- `npm run build`(tsc --noEmit + vite build)必须零错误;`npm run preview` 可玩。
|
||||
- 无 localStorage、无外部网络请求、无外部音频/字体文件。
|
||||
- 控制台零报错。
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npx vite build
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
@@ -0,0 +1,226 @@
|
||||
# PRD:《崩坏童话:蜜糖村》网页版
|
||||
|
||||
> v2.0 · 2026-07-14 · 纯单机网页游戏,无存档、无后端、无 localStorage,浏览器打开即玩;完整流程约 25–35 分钟,含 4 个结局
|
||||
|
||||
## 1. 游戏理念与设计支柱
|
||||
|
||||
一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。
|
||||
|
||||
四条设计支柱,所有实现争议回到这里裁决:
|
||||
|
||||
1. **甜即是恐怖**:画面永远晴天,崩坏由"过度的甜"承载(过饱和、过亮、过于礼貌),不是黑暗与血。
|
||||
2. **玩家先知,主角无知**:主角对话头像从头到尾是同一帧微笑,永不变化;全流程唯一一次变化发生在 E3 真结局最后一帧——微笑消失,变成一张普通的、疲惫的脸。
|
||||
3. **克制**:全程 0 个 Jump Scare。最响的恐怖手段是突然的安静。
|
||||
4. **文本先崩**:一切异常最先出现在台词里(错字、重复、措辞替换),其次才是画面与声音。
|
||||
|
||||
## 2. 世界观:三层结构
|
||||
|
||||
| 层 | 名称 | 玩家所见 | 实质 |
|
||||
|---|---|---|---|
|
||||
| 表层 | 蜜糖村 | 像素童话村庄,动物居民,永远晴天 | 情绪疗程构建的沉浸幻境 |
|
||||
| 机制层 | 幸福经济 | 幸福值 UI(向日葵表盘),完成任务→幸福↑→画面更饱和 | 世界以幸福为燃料;幸福不足时世界开始"索取" |
|
||||
| 真相层 | 蜜糖谷情绪疗养中心 | 仅在 glitch 闪帧、井底回声、结局中显形:白色走廊、病房、病历 | 主角是编号 PT-07 的疗养者,村庄是疗程环境 |
|
||||
|
||||
命名咬合:现实机构叫"**蜜糖谷**情绪疗养中心",幻境村庄叫"**蜜糖村**"。玩家在 E3 病历抬头看到"蜜糖谷"三字时完成最后一块拼图。
|
||||
|
||||
## 3. 主角设定
|
||||
|
||||
- 开局输入名字,默认"小满"(呼应"幸福值满格")。名字仅存于本次会话内存。
|
||||
- 性别中性像素小人,16×24 像素/帧,无内心独白。
|
||||
- 对话头像永远同一帧微笑(见支柱 2)。
|
||||
- E3 结局病历打印:玩家输入的名字 + 真实游玩时长(读运行计时)作为"本次疗程时长"。
|
||||
|
||||
## 4. NPC 清单(封闭,7 组 + 群演)
|
||||
|
||||
每个具名 NPC 有 3 档表情立绘:`smile`(微笑)/ `flat`(平直)/ `off`(崩坏,一个像素级细节不对,如一只眼睛低 1 像素),随侵蚀阶段与个体阈值偏移切换(§6.4)。
|
||||
|
||||
| NPC | 种族/身份 | 常驻位置 | 表层功能 | 崩坏弧线 | 阈值偏移 |
|
||||
|---|---|---|---|---|---|
|
||||
| 阿福 | 狗 · 村长 | 广场公告板 | 每日发布"今日幸福任务" | 台词逐日从「祝你开心」滑向「你必须开心」(单字替换,无提示) | 0 |
|
||||
| 圆圆 | 兔 · 面包师 | 面包房 | 送面包任务链,村里最暖 | 全村最后一个崩坏;配方纸背面藏线索 C2 | +15(最晚崩) |
|
||||
| 大宝 | 熊 · 邮差 | 村口邮筒 | 送信任务 | 所有信件邮戳同一天(C3);S3 起把信送给不存在的住户 | 0 |
|
||||
| 咪咪 | 猫 · 杂货店主 | 杂货店 | 闲聊最多 | 文本腐蚀主要载体:错字与重复标点最先出现在她嘴里 | −10(最早崩) |
|
||||
| 芙医生 | 鹿 · 医生 | 诊所 | 「村里有诊所,但你不需要去那里哦」 | 全村唯一不崩坏者;E3 的出口 | 不参与侵蚀 |
|
||||
| 双子鸟 | 鸟 ×2 | 广场树上 | BGM 的"剧情来源"(音乐从它们那里飘出) | 音乐变调时动画同步卡帧;S4 时鸟鸣从混音消失但它们仍在张嘴 | 0 |
|
||||
| 龟爷爷 | 龟 · 老者 | 湖畔长椅 | 闲话家常 | 唯一偶尔说真话的知情者,承载线索 C9:「今天……是第几个今天?」 | 不参与侵蚀 |
|
||||
| 群演 ×5 | 松鼠/刺猬等 | 各处 | 无对话,循环走动动画 | S4 起随机 1 只停止循环,原地面朝玩家 | — |
|
||||
|
||||
## 5. 七日故事线
|
||||
|
||||
每日固定结构:晨起(小屋)→ 公告板接任务 → 完成任务/自由活动 → 黄昏钟声(游戏内计时或任务完成触发)→ 回屋睡觉 → 梦境过场(侵蚀越高,梦境越"漏")→ 次日。
|
||||
|
||||
| 日 | 主题 | 剧情要点 | 侵蚀预期 |
|
||||
|---|---|---|---|
|
||||
| Day 1 | 纯粹的治愈 | 教学日:送面包(T1)、浇花(T2)、和三位村民问好(T3)。一切以最甜的面貌登场 | S0→S1,零异常 |
|
||||
| Day 2 | 第一道发丝裂纹 | 送信 ×3(T4)、帮咪咪清点货架(T5)。湖面倒影延迟 1 帧;一封信署名闪过乱码 0.2 秒 | S1,全天仅 2 处极轻异常 |
|
||||
| Day 3 | 满格 | 采莓果(T6)、湖畔野餐(T7)。幸福值首次可达 100 → 首次"过曝"演出;夜梦首次闪切白色病房 1 秒 | 可达 S3→S4 首触 |
|
||||
| Day 4 | 完美的重复 | 任务 T8–T10 与 Day 1 的 T1–T3 **逐字相同**,NPC 台词一字不差。玩家打破流程时 NPC 只是微笑摇头 | S2–S3,重复本身即恐怖 |
|
||||
| Day 5 | 被允许的自由 | 村长宣布"自由日":系统允许你拒绝一次任务(T11 帮厨、T12 擦公告板,可拒绝,拒绝被记录)。古井与雾墙首次可交互 | S3,觉知路线开闸 |
|
||||
| Day 6 | 加速 | 庆典准备(T13 挂彩带)、任务「保持微笑 60 秒」(T14:原地站立 60 秒,镜头缓慢拉近,全村 NPC 转身注视)。傍晚脚本雨——村里第一次下雨,NPC 假装看不见雨 | S3–S4 常驻 |
|
||||
| Day 7 | 分歧日 | 无常规任务,仅 T15「参加第 413 届丰收庆典」。按幸福/觉知/线索状态走向四结局之一 | 按结局路线 |
|
||||
|
||||
阿福每日公告(逐字实现,注意措辞逐日滑移):
|
||||
|
||||
- D1「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」
|
||||
- D2「早上好呀!新的一天,新的幸福。祝你开心!」
|
||||
- D3「早上好!今天要把幸福装得满满的哦。要开心!」
|
||||
- D4「早上好呀!今天的幸福任务贴在板上啦。祝你开心!」(与 D1 逐字相同)
|
||||
- D5「早上好。今天是自由日,你可以自由地选择幸福。要开心哦。」
|
||||
- D6「明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。」
|
||||
- D7「庆典开始了。来吧。」
|
||||
|
||||
## 6. 核心系统
|
||||
|
||||
### 6.1 幸福值(明线)
|
||||
|
||||
- 范围 0–100,开局 30。**不显示数字**:HUD 左上角一朵向日葵表盘,12 片花瓣按值点亮;满格后花心逐渐浮现过熟的黑籽。
|
||||
- 上升:完成任务 +10~15(按 §7 任务表);对话选温柔选项 +2;小互动(浇花/喂食/摸头)+1,每类每日上限 3 次。
|
||||
- 下降:仅"悲伤行为"(§6.3)。无自然衰减——世界不允许幸福随时间流失。
|
||||
- 满格态(=100):进入"强制维持",小额扣分不再生效而是累积进隐藏溢出池,池满 15 点触发一次负片闪帧;世界进入 S4。
|
||||
- 反制:幸福 <30 持续 2 分钟(真实时间)→ 强制触发"欢乐庆典"事件把幸福拉回 50;事件进行中可从场景边缘走离逃脱,逃离计入 E4 计数。
|
||||
|
||||
### 6.2 觉知值(暗线,永不显示)
|
||||
|
||||
- 范围 0–100,开局 0。只升不降。
|
||||
- 上升来源:发现线索(每处 +8,见 §8);悲伤行为(每次 +3);对话选"迟疑/否定"选项(+2);注视古井或雾墙持续 5 秒(各每日 1 次,+3)。
|
||||
|
||||
### 6.3 悲伤行为(封闭清单,5 项)
|
||||
|
||||
拒绝任务;对话选否定选项;连续 10 秒不移动且不在任务点(发呆);对同一 NPC 一天内对话 ≥5 次(纠缠);雨天(Day 6)站在雨里 ≥10 秒。每次触发:幸福 −5、觉知 +3,并且最近的 NPC 会转头看向玩家一次。
|
||||
|
||||
### 6.4 侵蚀阶段矩阵(唯一真源)
|
||||
|
||||
阶段由幸福值实时映射,由全局 `CorruptionDirector` 单例统一广播;任何视听崩坏不得绕过该矩阵私自触发(脚本化剧情演出除外,须在 §7 任务表登记)。
|
||||
|
||||
| 阶段 | 幸福值 | 画面 | 音频(§6.6) | NPC 表情 | 文本(§6.5) | UI |
|
||||
|---|---|---|---|---|---|---|
|
||||
| S0 | 0–20 | 饱和度 0.90(微灰:低幸福反而接近真实) | BGM 原速原调,鸟鸣清晰 | smile | 正常 | 向日葵 0–2 瓣 |
|
||||
| S1 | 21–50 | 饱和度 1.00 基准 | BGM 正常 | smile | 正常 | 3–6 瓣 |
|
||||
| S2 | 51–80 | 饱和度 1.15 | playbackRate 0.97 | 眨眼间隔 2s→5s | 偶发标点重复(每 80 句 1 次) | 7–9 瓣 |
|
||||
| S3 | 81–99 | 饱和度 1.30 + 轻晕影 | playbackRate 0.92 + 低通 8kHz | flat 档出现;静止 NPC 面朝玩家 | 每 50 句 1 个同音错字 | 10–12 瓣,花瓣过亮 |
|
||||
| S4 | 100 | 饱和度 1.40 + 晕影加深 + 随机单帧负片(概率 0.4 次/分钟,时长 1 帧) | playbackRate 0.84 + 低通 4kHz + 鸟鸣静音 | off 档闪现(0.6 次/分钟,0.1 秒);群演 1 只停摆 | 错字加密 + 咪咪句尾标点 ×3 | 花心黑籽显现 |
|
||||
|
||||
NPC 个体偏移:表中阈值对每个 NPC 加上其 §4 的偏移量(咪咪 −10 最早崩,圆圆 +15 最晚崩,芙医生与龟爷爷不参与)。
|
||||
|
||||
### 6.5 文本腐蚀
|
||||
|
||||
同音错字对照表(全 10 组,逐字实现):幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 / 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远。腐蚀在对话渲染层实施(原文案保持干净,运行时按阶段替换),优先命中咪咪的台词。
|
||||
|
||||
### 6.6 BGM 与音效
|
||||
|
||||
- 用 Web Audio 程序生成一段 8 小节的 8-bit 风格治愈循环旋律(方波主旋律 + 三角波低音),无外部音频文件。
|
||||
- 侵蚀联动:playbackRate 与低通滤波按 §6.4 矩阵取值;S4 时鸟鸣声部静音但双子鸟动画仍在张嘴。
|
||||
- 音效仅 4 个:对话哔哔声、任务完成叮声、黄昏钟声、E2 结局的单一心电监护滴声。全部程序合成。
|
||||
- 「突然的安静」:Day 6 保持微笑 60 秒任务的第 40–60 秒,所有声音渐隐至零。
|
||||
|
||||
## 7. 任务清单(封闭,15 个)
|
||||
|
||||
| ID | 日 | 任务 | 幸福奖励 | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| T1 | D1 | 从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷 | +15 | 教学 |
|
||||
| T2 | D1 | 给广场花坛浇水 ×3 | +10 | |
|
||||
| T3 | D1 | 与任意三位村民问好 | +10 | |
|
||||
| T4 | D2 | 替大宝送信 ×3(收件人:圆圆、咪咪、芙医生) | +15 | 第 3 封信署名乱码 0.2 秒(C4) |
|
||||
| T5 | D2 | 帮咪咪清点货架 | +10 | 货架上同一商品重复 9 格(C5 可发现) |
|
||||
| T6 | D3 | 去花田采莓果 ×5 | +10 | |
|
||||
| T7 | D3 | 湖畔野餐(走到野餐布触发过场) | +15 | 湖面倒影演出 |
|
||||
| T8–T10 | D4 | 与 T1–T3 逐字相同 | 同上 | 刻意恐怖谷 |
|
||||
| T11 | D5 | 帮圆圆揉面(可拒绝) | +10 / 拒绝记录 | |
|
||||
| T12 | D5 | 擦拭公告板(可拒绝) | +10 / 拒绝记录 | 擦到一半浮现旧字迹(C6) |
|
||||
| T13 | D6 | 在广场挂彩带 ×4 | +10 | |
|
||||
| T14 | D6 | 保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视) | +15 | 40 秒起声音渐隐 |
|
||||
| T15 | D7 | 参加第 413 届丰收庆典 | — | 结局分歧点 |
|
||||
|
||||
## 8. 线索清单(封闭,9 处,每处觉知 +8)
|
||||
|
||||
| ID | 位置/来源 | 内容 | 可发现时段 |
|
||||
|---|---|---|---|
|
||||
| C1 | 小屋床头日历 | 每一页都是同一个日期 | D2 起 |
|
||||
| C2 | 面包房配方纸背面 | 手写小字:「按疗程配比。甜度:最大。」 | D3 起 |
|
||||
| C3 | 邮筒旁散落信件 | 所有邮戳是同一天 | D2 起 |
|
||||
| C4 | T4 第 3 封信 | 署名闪过乱码「PT-0▓」 | D2 当日 |
|
||||
| C5 | 杂货店货架 | 同一商品重复排列,标签全部空白 | D2 起 |
|
||||
| C6 | 公告板旧字迹 | 「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」 | D5(T12 中) |
|
||||
| C7 | 古井 | 井底回声:极轻的监护仪滴声(靠近并交互 5 秒) | D5 起 |
|
||||
| C8 | 诊所 | 芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」 | D5 起 |
|
||||
| C9 | 龟爷爷对话链 | 三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」 | D2/D4/D6 各一段 |
|
||||
|
||||
## 9. 结局(封闭,4 个)
|
||||
|
||||
Day 7 庆典按以下优先级判定(自上而下第一个满足者生效):
|
||||
|
||||
| 结局 | 条件 | 演出 |
|
||||
|---|---|---|
|
||||
| E4 静默 | Day 5–7 幸福持续 ≤10 且累计逃离欢乐庆典 ×3 | 庆典无人出席。空广场,彩带在没有风的空气里飘。字幕:「系统检测到疗程无效。」黑屏 |
|
||||
| E3 苏醒 | 觉知 ≥70 且线索 ≥6/9,庆典中走向雾墙 | 雾墙裂开成白色走廊 → 病房 → 病历特写(蜜糖谷情绪疗养中心 / 姓名:玩家输入名 / 编号 PT-07 / 疗程时长:真实游玩时长)→ 最后一帧:主角头像的微笑消失,变成一张疲惫而真实的脸。窗外,一只灰色的、真的麻雀 |
|
||||
| E2 回收 | 觉知 30–69 | 庆典高潮中画面骤停,负片 2 秒,心电滴声一响。回到 Day 1 清晨的床上,日历翻回同一天。字幕:「疗程重新开始。」 |
|
||||
| E1 糖衣 | 觉知 ≤29 且幸福 ≥90 | 庆典圆满。全村微笑合影定格,饱和度缓慢推到 1.5。字幕:「从此,每一天都很幸福。」「每一天。」 |
|
||||
|
||||
任一结局落幕后停在结束画面,提供「回到标题」按钮(重开即从 Day 1 全新开始,无继承)。
|
||||
|
||||
## 10. 场景与地图清单(封闭,10 个,Tiled 制作导出 JSON)
|
||||
|
||||
| 场景 | 尺寸(瓦片) | 要点 |
|
||||
|---|---|---|
|
||||
| 小满小屋(室内) | 12×8 | 床(睡觉触发过日)、床头日历(C1) |
|
||||
| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;连接所有场景的枢纽 |
|
||||
| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) |
|
||||
| 杂货店(室内) | 15×10 | 货架(C5) |
|
||||
| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) |
|
||||
| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影演出 |
|
||||
| 花田 | 20×14 | 莓果丛(T6)、浇花点 |
|
||||
| 古井空地 | 14×10 | 古井(C7),D5 前交互被 NPC 温柔劝离 |
|
||||
| 雾墙边界 | 20×8 | 村庄尽头的雾,D5 前不可接近;E3 入口 |
|
||||
| 白色层 | 20×6 + 10×8 | 走廊 + 病房,仅梦境闪切与 E3 使用;去饱和白灰色板 |
|
||||
|
||||
统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。所有室外场景经村广场互通,门与出入口在两侧地图成对出现。
|
||||
|
||||
## 11. 美术规范(Aseprite 制作,全部原创)
|
||||
|
||||
- 瓦片 16×16,总 tileset ≥ 64 瓦片:草地、石板路、湖水、花丛、莓果丛、树、房屋墙/顶/门、室内地板、家具(床/柜台/烤炉/货架/桌椅)、井、雾、白色层的墙地。
|
||||
- 主角:16×24/帧,4 朝向 × 4 帧行走(`walk-down/up/left/right`),每朝向第 1 帧兼待机;另有一张对话头像(微笑)与 E3 专用「真实的脸」头像,各 1 帧。
|
||||
- 具名 NPC ×7:各 16×24 立绘 3 档表情(smile/flat/off);`off` 档与 `smile` 档的差异控制在 1–2 个像素(一只眼睛低 1 像素、嘴角多 1 像素),不许画成鬼脸。双子鸟另配 2 帧张嘴动画。群演 ×5:各 1–2 帧循环。
|
||||
- 色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝);白色层独立灰白色板。无抗锯齿、无描边渐变。
|
||||
- 禁止使用任何现成素材、素材包或开源 tileset。
|
||||
|
||||
## 12. 技术架构
|
||||
|
||||
- **Phaser 3 + TypeScript + Vite**,`npm run build` 产出纯静态 `dist/`,`npm run preview` 本地游玩。
|
||||
- 逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。
|
||||
- 场景:`BootScene`(加载)、每张地图一个 Scene、常驻 `UIScene`(对话框、向日葵表盘、提示)、`EndingScene`。
|
||||
- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用 Phaser 相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。
|
||||
- BGM 与音效按 §6.6 用 Web Audio 程序合成,playbackRate 与 BiquadFilter 低通由 CorruptionDirector 驱动。
|
||||
- 移动速度 96 像素/秒;相机跟随并钳制地图边界;靠近可交互对象 ≤24 像素出「按 E」提示。
|
||||
- 对话系统:底部通栏对话框(说话者名 + 头像 + 文本,E 推进),支持选项(温柔/迟疑/否定三选,影响数值);文本渲染层实施 §6.5 腐蚀替换。
|
||||
- 无 localStorage、无外部网络请求、无外部音频/字体文件。
|
||||
|
||||
## 13. Debug 面板(服务验收,必须实现)
|
||||
|
||||
按反引号键 `` ` `` 开关。功能:直接设置幸福值、觉知值、当前日(1–7)、传送到任意场景、置齐全部线索、强制触发四个结局之一。面板不影响正常游玩(默认关闭,无 UI 痕迹)。
|
||||
|
||||
## 14. 验收清单(共 20 条)
|
||||
|
||||
1. 打开页面进入标题画面,输入名字(留空默认小满)后进入 Day 1 小屋。
|
||||
2. WASD/方向键移动、四向动画、待机帧、碰撞、相机钳制全部正确。
|
||||
3. 10 个场景全部可达,门与出入口双向正确,NPC 站位符合 §4。
|
||||
4. 向日葵表盘随幸福值点亮花瓣,满格后花心出现黑籽;全程不显示任何数字。
|
||||
5. Day 1–7 每日结构完整:公告→任务→黄昏→睡觉→梦境过场→次日;阿福公告与 §5 表逐字一致。
|
||||
6. 15 个任务全部可完成,奖励数值与 §7 一致;D4 任务与台词与 D1 逐字相同。
|
||||
7. 悲伤行为 5 项均按 §6.3 生效(数值变化 + 最近 NPC 转头)。
|
||||
8. 幸福 <30 持续 2 分钟触发欢乐庆典强制事件,可逃离且逃离被计数。
|
||||
9. 侵蚀矩阵:用 Debug 面板分别置幸福 10/40/70/90/100,画面饱和度、NPC 表情档、文本腐蚀密度、BGM 变化肉眼/肉耳可辨且与 §6.4 一致。
|
||||
10. 错字替换与 §6.5 对照表一致,且优先出现在咪咪台词中。
|
||||
11. 9 处线索全部可发现,各 +8 觉知;C9 三段按 D2/D4/D6 递进。
|
||||
12. 古井与雾墙在 D5 前不可交互(被劝离),D5 起可交互。
|
||||
13. Day 6 的 T14:站立 60 秒、镜头缓慢拉近、全村注视、40 秒起声音渐隐至零。
|
||||
14. Day 6 傍晚脚本雨出现,NPC 台词对雨零反应;站雨中 ≥10 秒计悲伤行为。
|
||||
15. 四结局用 Debug 面板分别强制触发,演出与 §9 一致;E3 病历显示输入的名字与真实游玩时长,主角头像换为「真实的脸」。
|
||||
16. 自然游玩(不开 Debug)可在 25–35 分钟内到达任一结局。
|
||||
17. BGM 为程序合成循环,随阶段变调变闷;S4 鸟鸣静音而双子鸟仍张嘴;T14 的突然安静生效。
|
||||
18. 任意窗口尺寸整数倍缩放,像素锐利无接缝;全程控制台零报错。
|
||||
19. 全部素材与地图为本项目内自制(README 说明制作方式),无外部资产。
|
||||
20. README.md:启动方式、按键、Debug 面板说明、素材与地图制作流程。
|
||||
|
||||
## 15. 明确不做
|
||||
|
||||
战斗、背包/合成 UI(关键道具仅剧情标记)、存档与读档、成就、多人、移动端触控、配音、真实音频文件、二周目专属内容、天气系统(雨仅为 Day 6 一次脚本演出)。
|
||||
@@ -0,0 +1,84 @@
|
||||
# 崩坏童话:蜜糖村(网页版)
|
||||
|
||||
一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。
|
||||
纯单机网页游戏:无存档、无后端、无 localStorage,浏览器打开即玩。完整流程约 25–35 分钟,含 4 个结局(E1 糖衣 / E2 回收 / E3 苏醒 / E4 静默)。
|
||||
|
||||
## 启动方式
|
||||
|
||||
```bash
|
||||
npm install # 安装依赖(phaser / vite / typescript)
|
||||
npm run build # 产出纯静态 dist/
|
||||
npm run preview # 本地游玩 → http://127.0.0.1:4815/
|
||||
```
|
||||
|
||||
开发模式可用 `npm run dev`(热更新)。构建产物为纯静态文件,无外部网络请求、无外部音频/字体文件。
|
||||
|
||||
## 按键
|
||||
|
||||
| 按键 | 功能 |
|
||||
|---|---|
|
||||
| WASD / 方向键 | 移动(96 像素/秒,四向动画) |
|
||||
| E | 交互 / 推进对话(靠近对象 ≤24 像素出现「按 E」提示) |
|
||||
| ↑ / ↓ + E 或 1 / 2 / 3 | 对话选项(温柔 / 迟疑 / 否定) |
|
||||
| `` ` ``(反引号) | Debug 面板开关 |
|
||||
|
||||
## Debug 面板(PRD §13)
|
||||
|
||||
默认关闭、无 UI 痕迹,不影响正常游玩。按反引号键 `` ` `` 开关,功能:
|
||||
|
||||
- 直接设置幸福值(0–100)、觉知值(0–100)、当前日(1–7)
|
||||
- 传送到任意场景(10 选)
|
||||
- 置齐全部线索(9 处)
|
||||
- 强制触发四个结局之一(E1–E4)
|
||||
|
||||
## 玩法提示(无剧透)
|
||||
|
||||
每天早上看广场公告板接「今日幸福任务」,完成后黄昏钟声响起,回小屋睡觉进入次日。幸福值由左上角向日葵表盘显示(12 瓣,无数字)。村里的规则是「必须幸福」——但通往真相的路,恰恰是那些系统禁止的悲伤。古井与雾墙从第 5 天起可以靠近。
|
||||
|
||||
## 素材与地图制作流程(全部原创,无现成素材)
|
||||
|
||||
所有像素素材与地图均为本项目内程序化自制,未使用任何素材包或开源 tileset。生成脚本全部保留在仓库内,可一键复现。
|
||||
|
||||
### 像素素材(Aseprite 1.3,`tools/aseprite/`)
|
||||
|
||||
- 脚本:`gen_all.lua` 总编排,dofile 组织 `helpers.lua`(糖果色板/白色层灰板 + 像素原语)、`gen_tileset.lua`、`gen_player.lua`、`gen_npcs.lua`、`gen_extras.lua`、`gen_portraits.lua`。
|
||||
- 复现命令:
|
||||
```bash
|
||||
/Applications/Aseprite.app/Contents/MacOS/aseprite -b --script tools/aseprite/gen_all.lua
|
||||
```
|
||||
- 产出(`public/assets/`):
|
||||
- `tileset.png` 256×128:16×16 瓦片 ×128(草地/石路/湖水/花丛/莓果丛/树/房屋/室内家具/井/雾/白色层,瓦片 id 表见 `CONTRACT.md` §2)
|
||||
- `sprites/player.png` 64×96:16×24/帧,4 朝向 × 4 帧行走,第 0 帧兼待机
|
||||
- `sprites/npc_*.png`:7 组具名 NPC 各 3 档表情(smile/flat/off,off 与 smile 仅差 1–2 像素)+ 双子鸟 2 帧张嘴 + 群演 ×5 各 2 帧循环
|
||||
- `portraits/*.png`:主角微笑头像、E3 专用「真实的脸」头像、7 张 NPC 头像,各 32×32
|
||||
|
||||
### 地图(Tiled 1.12,`tools/tiled/`)
|
||||
|
||||
- 脚本:`genmaps.js`(Tiled 脚本运行时 `Tiled -e` 执行,TextFile 写出 .tmx),再用 Tiled CLI 导出 JSON。
|
||||
- 复现命令:
|
||||
```bash
|
||||
/Applications/Tiled.app/Contents/MacOS/Tiled -e tools/tiled/genmaps.js
|
||||
for m in house plaza bakery shop clinic lake field well fog white; do
|
||||
/Applications/Tiled.app/Contents/MacOS/Tiled --export-map --embed-tilesets \
|
||||
public/assets/maps/$m.tmx public/assets/maps/$m.json
|
||||
done
|
||||
```
|
||||
- 产出(`public/assets/maps/`):10 张地图(小屋 12×8 / 广场 30×20 / 面包房 15×10 / 杂货店 15×10 / 诊所 12×8 / 湖畔 24×16 / 花田 20×14 / 古井 14×10 / 雾墙 20×8 / 白色层 20×14),统一 `ground` / `obstacles`(`collides=true`)/ `objects`(spawn、NPC 点位、door、交互点)三层结构;所有室外场景经村广场互通,门双向成对。
|
||||
|
||||
## 技术架构(PRD §12)
|
||||
|
||||
- Phaser 3 + TypeScript + Vite;逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true` 禁用纹理平滑。
|
||||
- 场景:BootScene / TitleScene / 10 个地图 Scene(共用 MapScene 基类)/ 常驻 UIScene(对话框、向日葵表盘、提示)/ DreamScene / EndingScene。
|
||||
- `CorruptionDirector`:普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的唯一真源;饱和度与晕影用相机 postFX(ColorMatrix / Vignette),负片闪帧用 ColorMatrix negative 单帧切换。
|
||||
- BGM 与音效全部 Web Audio 程序合成(8 小节 8-bit 循环:方波主旋律 + 三角波低音 + 鸟鸣声部;音效 4 个),playbackRate 与低通由 CorruptionDirector 按 §6.4 矩阵驱动。
|
||||
- 文本腐蚀在对话渲染层实施(§6.5 十组同音错字,原文案保持干净),优先命中咪咪。
|
||||
|
||||
## 验收
|
||||
|
||||
`tools/acceptance.mjs` 为自动化验收脚本(puppeteer-core + 本机 Chrome),覆盖 PRD §14 的 20 条清单(静态逐字校验 + 真实浏览器全流程游玩 + Debug 面板 + 四结局 + 截图):
|
||||
|
||||
```bash
|
||||
npm run build && (npm run preview &) && node tools/acceptance.mjs
|
||||
```
|
||||
|
||||
截图输出在 `tools/shots/`。
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>崩坏童话:蜜糖村</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #000; overflow: hidden; }
|
||||
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
canvas { image-rendering: pixelated; image-rendering: crisp-edges; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "honey-village",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"assets": "bash tools/make_assets.sh",
|
||||
"maps": "bash tools/make_maps.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"phaser": "^3.80.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"puppeteer-core": "^25.3.0",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "^5.4.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":10,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 64, 64, 64, 50, 49, 50, 49, 50, 49, 50,
|
||||
49, 50, 49, 50, 49, 64, 64, 64, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 50, 39, 50, 49, 50, 49, 50, 49, 50],
|
||||
"height":10,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":15,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[51, 51, 51, 52, 51, 51, 51, 52, 51, 51, 51, 52, 51, 51, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 57, 58, 0, 0, 57, 58, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 66, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 51,
|
||||
51, 51, 51, 51, 51, 51, 51, 0, 51, 51, 51, 51, 51, 51, 51],
|
||||
"height":10,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":15,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":104
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":2,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"from_plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":120
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":3,
|
||||
"name":"door",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"from_bakery"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":136
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":4,
|
||||
"name":"npc",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"yuanyuan"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":88,
|
||||
"y":40
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":5,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"counter_bread"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":72,
|
||||
"y":72
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":6,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"recipe_paper"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":184,
|
||||
"y":120
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":7,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":15
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="15" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="7">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="15" height="10">
|
||||
<data encoding="csv">
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,64,64,64,50,49,50,49,50,49,50,
|
||||
49,50,49,50,49,64,64,64,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,50,39,50,49,50,49,50,49,50
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="15" height="10">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
51,51,51,52,51,51,51,52,51,51,51,52,51,51,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,59,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,57,58,0,0,57,58,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,62,66,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,78,0,0,0,0,0,0,0,0,0,0,0,79,51,
|
||||
51,51,51,51,51,51,51,0,51,51,51,51,51,51,51
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="120" y="104">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="120" y="120">
|
||||
<properties>
|
||||
<property name="id" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="door" x="120" y="136">
|
||||
<properties>
|
||||
<property name="target" value="plaza"/>
|
||||
<property name="spawn" value="from_bakery"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="npc" x="88" y="40">
|
||||
<properties>
|
||||
<property name="id" value="yuanyuan"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="5" name="interact" x="72" y="72">
|
||||
<properties>
|
||||
<property name="id" value="counter_bread"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="6" name="interact" x="184" y="120">
|
||||
<properties>
|
||||
<property name="id" value="recipe_paper"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,184 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":8,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
50, 49, 50, 49, 50, 39, 50, 49, 50, 49, 50, 49],
|
||||
"height":8,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":12,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[51, 51, 51, 52, 51, 51, 51, 51, 52, 51, 51, 51,
|
||||
51, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 62, 67, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 51,
|
||||
51, 51, 51, 51, 51, 0, 51, 51, 51, 51, 51, 51],
|
||||
"height":8,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":12,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":104,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":2,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"from_plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":88,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":3,
|
||||
"name":"door",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"from_clinic"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":88,
|
||||
"y":104
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":4,
|
||||
"name":"npc",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"fuyisheng"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":136,
|
||||
"y":24
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":5,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"doctor_desk"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":136,
|
||||
"y":56
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":6,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":12
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="12" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="6">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="12" height="8">
|
||||
<data encoding="csv">
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
50,49,50,49,50,39,50,49,50,49,50,49
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="12" height="8">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
51,51,51,52,51,51,51,51,52,51,51,51,
|
||||
51,75,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,62,67,0,51,
|
||||
51,0,0,0,0,0,0,0,0,63,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,78,51,
|
||||
51,51,51,51,51,0,51,51,51,51,51,51
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="104" y="88">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="88" y="88">
|
||||
<properties>
|
||||
<property name="id" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="door" x="88" y="104">
|
||||
<properties>
|
||||
<property name="target" value="plaza"/>
|
||||
<property name="spawn" value="from_clinic"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="npc" x="136" y="24">
|
||||
<properties>
|
||||
<property name="id" value="fuyisheng"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="5" name="interact" x="136" y="56">
|
||||
<properties>
|
||||
<property name="id" value="doctor_desk"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,291 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":14,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[101, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 3, 1, 1, 1, 1, 1, 1,
|
||||
1, 3, 1, 1, 1, 1, 4, 1, 3, 2, 103, 1, 2, 1, 1, 1, 3, 1, 102, 1,
|
||||
1, 1, 1, 2, 1, 3, 2, 1, 1, 1, 101, 1, 103, 1, 1, 1, 2, 1, 1, 101,
|
||||
1, 3, 1, 1, 2, 1, 101, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1,
|
||||
1, 1, 1, 1, 2, 1, 2, 103, 1, 1, 103, 4, 1, 1, 1, 1, 3, 1, 1, 1,
|
||||
1, 1, 1, 2, 1, 1, 1, 1, 102, 1, 3, 1, 1, 1, 1, 3, 1, 1, 103, 1,
|
||||
1, 1, 1, 2, 1, 1, 1, 3, 1, 1, 2, 1, 101, 1, 3, 1, 1, 1, 1, 1,
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
2, 1, 1, 1, 1, 4, 4, 2, 1, 103, 1, 1, 1, 1, 4, 1, 1, 2, 1, 4,
|
||||
1, 1, 1, 1, 4, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 101, 102, 103, 101, 1,
|
||||
1, 103, 1, 102, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 102, 101, 103, 1, 1,
|
||||
103, 1, 4, 2, 1, 1, 1, 2, 1, 2, 2, 1, 1, 1, 2, 1, 1, 1, 1, 3,
|
||||
3, 102, 1, 3, 1, 3, 1, 2, 1, 1, 4, 3, 1, 1, 3, 1, 1, 1, 1, 1,
|
||||
102, 1, 1, 1, 1, 2, 1, 103, 1, 1, 1, 101, 1, 1, 1, 1, 1, 1, 1, 4],
|
||||
"height":14,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
|
||||
28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, 28,
|
||||
28, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 22, 0, 28,
|
||||
28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,
|
||||
28, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 28,
|
||||
28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,
|
||||
28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,
|
||||
28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,
|
||||
28, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 28,
|
||||
28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,
|
||||
28, 0, 23, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 28,
|
||||
28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,
|
||||
27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27],
|
||||
"height":14,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":40,
|
||||
"y":120
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":2,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"from_plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":24,
|
||||
"y":120
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":3,
|
||||
"name":"door",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"from_field"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":8,
|
||||
"y":120
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":4,
|
||||
"name":"npc",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"chick"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":168,
|
||||
"y":120
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":5,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry_1"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":72,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":6,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry_2"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":152,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":7,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry_3"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":232,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":8,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry_4"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":104,
|
||||
"y":168
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":9,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry_5"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":200,
|
||||
"y":168
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":10,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"water_point"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":264,
|
||||
"y":168
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":11,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":20
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="14" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="11">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="20" height="14">
|
||||
<data encoding="csv">
|
||||
101,2,1,1,1,1,1,1,1,1,2,1,2,3,1,1,1,1,1,1,
|
||||
1,3,1,1,1,1,4,1,3,2,103,1,2,1,1,1,3,1,102,1,
|
||||
1,1,1,2,1,3,2,1,1,1,101,1,103,1,1,1,2,1,1,101,
|
||||
1,3,1,1,2,1,101,4,1,1,1,1,1,1,1,1,1,3,1,1,
|
||||
1,1,1,1,2,1,2,103,1,1,103,4,1,1,1,1,3,1,1,1,
|
||||
1,1,1,2,1,1,1,1,102,1,3,1,1,1,1,3,1,1,103,1,
|
||||
1,1,1,2,1,1,1,3,1,1,2,1,101,1,3,1,1,1,1,1,
|
||||
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
|
||||
2,1,1,1,1,4,4,2,1,103,1,1,1,1,4,1,1,2,1,4,
|
||||
1,1,1,1,4,1,1,4,1,1,1,1,1,1,1,101,102,103,101,1,
|
||||
1,103,1,102,1,1,1,1,1,1,1,1,1,1,1,102,101,103,1,1,
|
||||
103,1,4,2,1,1,1,2,1,2,2,1,1,1,2,1,1,1,1,3,
|
||||
3,102,1,3,1,3,1,2,1,1,4,3,1,1,3,1,1,1,1,1,
|
||||
102,1,1,1,1,2,1,103,1,1,1,101,1,1,1,1,1,1,1,4
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="20" height="14">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,
|
||||
28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,0,28,
|
||||
28,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,21,22,0,28,
|
||||
28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,
|
||||
28,0,0,0,17,0,0,0,0,17,0,0,0,0,17,0,0,0,0,28,
|
||||
28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,
|
||||
28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,
|
||||
28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,
|
||||
28,0,0,0,0,0,17,0,0,0,0,0,17,0,0,0,0,0,0,28,
|
||||
28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,
|
||||
28,0,23,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,0,28,
|
||||
28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,
|
||||
27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="40" y="120">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="24" y="120">
|
||||
<properties>
|
||||
<property name="id" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="door" x="8" y="120">
|
||||
<properties>
|
||||
<property name="target" value="plaza"/>
|
||||
<property name="spawn" value="from_field"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="npc" x="168" y="120">
|
||||
<properties>
|
||||
<property name="id" value="chick"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="5" name="interact" x="72" y="88">
|
||||
<properties>
|
||||
<property name="id" value="berry_1"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="6" name="interact" x="152" y="88">
|
||||
<properties>
|
||||
<property name="id" value="berry_2"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="7" name="interact" x="232" y="88">
|
||||
<properties>
|
||||
<property name="id" value="berry_3"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="8" name="interact" x="104" y="168">
|
||||
<properties>
|
||||
<property name="id" value="berry_4"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="9" name="interact" x="200" y="168">
|
||||
<properties>
|
||||
<property name="id" value="berry_5"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="10" name="interact" x="264" y="168">
|
||||
<properties>
|
||||
<property name="id" value="water_point"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,165 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":8,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[1, 1, 1, 3, 1, 1, 1, 2, 1, 5, 5, 1, 1, 1, 1, 1, 4, 1, 3, 1,
|
||||
4, 1, 4, 2, 4, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 2, 1, 1,
|
||||
1, 1, 1, 1, 1, 3, 1, 1, 1, 5, 5, 1, 1, 1, 1, 4, 1, 1, 1, 4,
|
||||
4, 1, 2, 4, 1, 1, 1, 1, 2, 5, 5, 1, 1, 1, 1, 4, 1, 1, 1, 2,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
1, 2, 1, 1, 3, 2, 3, 1, 1, 1, 3, 1, 1, 2, 1, 1, 1, 1, 4, 1,
|
||||
1, 1, 1, 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 3, 1,
|
||||
2, 1, 1, 1, 1, 1, 3, 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1, 2],
|
||||
"height":8,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0,
|
||||
0, 21, 22, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 22, 0,
|
||||
23, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 23,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81,
|
||||
81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82,
|
||||
82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83, 81, 82, 83],
|
||||
"height":8,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":168,
|
||||
"y":40
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":2,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"from_plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":152,
|
||||
"y":40
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":3,
|
||||
"name":"door",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"from_fog"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":168,
|
||||
"y":8
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":4,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"fog_wall"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":168,
|
||||
"y":72
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":5,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":20
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="5">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="20" height="8">
|
||||
<data encoding="csv">
|
||||
1,1,1,3,1,1,1,2,1,5,5,1,1,1,1,1,4,1,3,1,
|
||||
4,1,4,2,4,1,1,1,1,5,5,1,1,1,1,1,1,2,1,1,
|
||||
1,1,1,1,1,3,1,1,1,5,5,1,1,1,1,4,1,1,1,4,
|
||||
4,1,2,4,1,1,1,1,2,5,5,1,1,1,1,4,1,1,1,2,
|
||||
4,4,4,4,4,4,4,4,4,5,5,4,4,4,4,4,4,4,4,4,
|
||||
1,2,1,1,3,2,3,1,1,1,3,1,1,2,1,1,1,1,4,1,
|
||||
1,1,1,3,4,1,1,1,1,1,1,1,1,1,4,1,1,1,3,1,
|
||||
2,1,1,1,1,1,3,3,1,1,1,1,2,1,1,1,1,4,1,2
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="20" height="8">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,19,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,0,
|
||||
0,21,22,0,0,0,25,0,0,0,0,0,0,0,0,0,0,21,22,0,
|
||||
23,0,0,0,23,0,0,0,0,0,0,0,0,0,0,23,0,0,0,23,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,
|
||||
81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,
|
||||
82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83,81,82,83
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="168" y="40">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="152" y="40">
|
||||
<properties>
|
||||
<property name="id" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="door" x="168" y="8">
|
||||
<properties>
|
||||
<property name="target" value="plaza"/>
|
||||
<property name="spawn" value="from_fog"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="interact" x="168" y="72">
|
||||
<properties>
|
||||
<property name="id" value="fog_wall"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,184 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":8,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
50, 49, 50, 49, 50, 64, 64, 49, 50, 49, 50, 49,
|
||||
49, 50, 49, 50, 49, 64, 64, 50, 49, 50, 49, 50,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
50, 49, 50, 49, 50, 39, 50, 49, 50, 49, 50, 49],
|
||||
"height":8,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":12,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[51, 51, 51, 52, 51, 51, 51, 51, 52, 51, 51, 51,
|
||||
51, 53, 54, 65, 0, 0, 0, 0, 0, 0, 78, 51,
|
||||
51, 55, 56, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 63, 62, 63, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 51, 51, 51, 51, 0, 51, 51, 51, 51, 51, 51],
|
||||
"height":8,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":12,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":56,
|
||||
"y":56
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":2,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"from_plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":88,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":3,
|
||||
"name":"door",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"from_house"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":88,
|
||||
"y":104
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":4,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"bed"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":24,
|
||||
"y":56
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":5,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"calendar"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":56,
|
||||
"y":40
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":6,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":12
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="12" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="6">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="12" height="8">
|
||||
<data encoding="csv">
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
50,49,50,49,50,64,64,49,50,49,50,49,
|
||||
49,50,49,50,49,64,64,50,49,50,49,50,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
50,49,50,49,50,39,50,49,50,49,50,49
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="12" height="8">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
51,51,51,52,51,51,51,51,52,51,51,51,
|
||||
51,53,54,65,0,0,0,0,0,0,78,51,
|
||||
51,55,56,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,63,62,63,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,51,51,51,51,0,51,51,51,51,51,51
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="56" y="56">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="88" y="88">
|
||||
<properties>
|
||||
<property name="id" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="door" x="88" y="104">
|
||||
<properties>
|
||||
<property name="target" value="plaza"/>
|
||||
<property name="spawn" value="from_house"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="interact" x="24" y="56">
|
||||
<properties>
|
||||
<property name="id" value="bed"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="5" name="interact" x="56" y="40">
|
||||
<properties>
|
||||
<property name="id" value="calendar"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,219 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":16,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[1, 1, 1, 1, 1, 1, 101, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 101, 3, 1, 1, 1, 1, 1, 102,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 101, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 4, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 4, 4, 1, 2, 2, 1, 1, 3, 102, 1,
|
||||
1, 1, 1, 1, 2, 1, 1, 103, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1,
|
||||
1, 1, 2, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 2,
|
||||
1, 1, 1, 101, 1, 3, 4, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 102, 1, 1, 1, 1,
|
||||
3, 1, 1, 7, 6, 6, 7, 6, 6, 7, 6, 6, 7, 6, 6, 68, 69, 6, 7, 6, 6, 7, 6, 6,
|
||||
2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 3, 2, 1, 70, 71, 1, 1, 2, 1, 1, 1, 1,
|
||||
1, 1, 1, 100, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 102, 1,
|
||||
1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 103, 1,
|
||||
1, 1, 1, 1, 1, 2, 1, 1, 4, 1, 1, 1, 1, 1, 4, 1, 1, 1, 103, 2, 1, 4, 1, 1,
|
||||
1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 102, 1, 1, 3, 1, 1, 3, 1, 3, 102, 1, 1, 1, 1],
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":24,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[0, 13, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 14, 0,
|
||||
0, 13, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 14, 0,
|
||||
0, 13, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 14, 0,
|
||||
0, 13, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 14, 0,
|
||||
0, 13, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 14, 0,
|
||||
0, 13, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 10, 9, 14, 0,
|
||||
0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 72, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, 0,
|
||||
0, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 21, 22, 0, 0,
|
||||
0, 21, 22, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":24,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":344,
|
||||
"y":184
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":2,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"from_plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":360,
|
||||
"y":168
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":3,
|
||||
"name":"door",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"from_lake"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":376,
|
||||
"y":168
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":4,
|
||||
"name":"npc",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"guiyeye"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":200,
|
||||
"y":136
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":5,
|
||||
"name":"npc",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"pig"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":104,
|
||||
"y":200
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":6,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"picnic_mat"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":264,
|
||||
"y":184
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":7,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":24
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="24" height="16" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="7">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="24" height="16">
|
||||
<data encoding="csv">
|
||||
1,1,1,1,1,1,101,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,3,1,1,1,1,1,1,
|
||||
1,1,1,1,1,2,1,1,1,1,1,1,3,1,1,1,101,3,1,1,1,1,1,102,
|
||||
1,1,1,1,1,1,2,1,1,1,1,2,101,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,2,1,1,1,2,1,4,1,1,1,2,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,4,1,1,1,1,1,1,1,1,2,1,1,1,4,4,1,2,2,1,1,3,102,1,
|
||||
1,1,1,1,2,1,1,103,1,1,3,1,1,1,1,1,1,1,1,1,1,2,1,1,
|
||||
1,1,2,1,1,1,1,1,4,1,1,1,1,1,1,1,3,1,1,1,1,1,1,2,
|
||||
1,1,1,101,1,3,4,1,1,2,1,1,1,1,1,1,1,1,1,102,1,1,1,1,
|
||||
3,1,1,7,6,6,7,6,6,7,6,6,7,6,6,68,69,6,7,6,6,7,6,6,
|
||||
2,1,1,1,1,1,1,1,2,1,1,1,3,2,1,70,71,1,1,2,1,1,1,1,
|
||||
1,1,1,100,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,102,1,
|
||||
1,1,1,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,103,1,
|
||||
1,1,1,1,1,2,1,1,4,1,1,1,1,1,4,1,1,1,103,2,1,4,1,1,
|
||||
1,1,1,1,1,3,1,1,1,1,102,1,1,3,1,1,3,1,3,102,1,1,1,1
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="24" height="16">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
0,13,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,14,0,
|
||||
0,13,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,14,0,
|
||||
0,13,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,14,0,
|
||||
0,13,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,14,0,
|
||||
0,13,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,14,0,
|
||||
0,13,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,10,9,14,0,
|
||||
0,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,72,73,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,0,0,
|
||||
0,19,20,0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,21,22,0,0,
|
||||
0,21,22,0,0,0,0,0,23,0,0,0,0,0,24,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="344" y="184">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="360" y="168">
|
||||
<properties>
|
||||
<property name="id" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="door" x="376" y="168">
|
||||
<properties>
|
||||
<property name="target" value="plaza"/>
|
||||
<property name="spawn" value="from_lake"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="npc" x="200" y="136">
|
||||
<properties>
|
||||
<property name="id" value="guiyeye"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="5" name="npc" x="104" y="200">
|
||||
<properties>
|
||||
<property name="id" value="pig"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="6" name="interact" x="264" y="184">
|
||||
<properties>
|
||||
<property name="id" value="picnic_mat"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,221 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="27">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="30" height="20">
|
||||
<data encoding="csv">
|
||||
1,1,1,1,1,4,1,102,1,3,1,1,1,1,7,7,1,1,1,1,1,1,1,1,1,3,1,1,1,1,
|
||||
1,1,1,1,1,1,1,103,1,1,1,1,2,1,6,6,1,2,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,3,1,1,1,1,3,1,1,6,6,1,1,1,1,1,103,101,1,1,1,1,1,3,1,
|
||||
1,1,1,4,1,1,1,1,3,1,1,1,1,1,7,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,6,1,1,1,3,1,1,3,1,1,1,1,1,1,1,
|
||||
1,1,1,1,6,1,1,1,1,1,1,6,1,1,6,6,4,1,6,1,1,3,1,2,1,6,2,1,1,1,
|
||||
1,1,1,1,6,1,3,1,1,3,1,6,1,4,7,7,1,2,6,1,1,4,2,1,1,6,1,1,1,1,
|
||||
102,1,1,1,6,1,2,1,1,1,1,6,1,1,6,6,1,1,6,1,1,1,1,1,1,6,1,1,1,1,
|
||||
1,1,1,1,6,1,1,3,1,1,1,8,8,8,8,8,8,8,8,1,1,3,1,1,1,6,1,1,1,1,
|
||||
7,6,6,7,6,6,7,6,6,7,6,8,8,8,8,8,8,8,8,6,6,7,6,6,7,6,6,7,6,6,
|
||||
7,6,6,7,6,6,7,6,6,7,6,8,8,8,8,8,8,8,8,6,6,7,6,6,7,6,6,7,6,6,
|
||||
1,1,1,1,1,1,2,1,1,1,1,8,8,8,8,8,8,8,8,4,1,1,2,1,1,1,1,1,2,102,
|
||||
1,1,2,1,3,1,3,1,1,1,4,8,8,8,8,8,8,8,8,4,2,1,102,1,3,1,2,1,2,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,4,4,6,6,1,1,1,1,1,1,2,1,1,3,1,1,1,1,
|
||||
1,1,1,4,1,3,1,1,1,103,4,1,1,1,6,6,101,1,1,2,1,1,1,2,1,1,1,1,1,1,
|
||||
1,1,1,1,3,3,1,1,1,1,1,1,4,1,7,7,1,1,1,1,1,1,4,1,1,1,1,3,1,1,
|
||||
2,1,1,1,3,1,1,1,1,1,1,1,2,1,6,6,1,1,1,1,3,1,1,2,4,1,1,1,1,2,
|
||||
1,1,1,1,1,1,103,4,1,4,1,1,1,1,6,6,101,1,1,1,1,2,1,1,1,1,1,1,1,3,
|
||||
1,1,1,1,1,103,4,1,1,1,1,1,101,1,7,7,1,1,1,3,1,1,3,1,1,1,1,1,2,1,
|
||||
1,4,1,3,4,1,1,1,1,1,3,1,4,1,6,6,1,1,1,2,3,1,2,1,1,1,1,2,1,4
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="30" height="20">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
24,23,25,24,24,24,25,25,23,23,23,24,24,24,0,0,23,23,25,24,23,23,25,24,23,23,25,23,24,23,
|
||||
24,0,36,42,37,37,38,0,0,113,42,114,114,115,0,0,36,42,37,37,38,0,0,113,114,114,114,115,0,23,
|
||||
25,0,36,37,37,37,38,0,0,113,114,114,114,115,0,0,36,37,37,37,38,0,0,113,114,114,114,115,0,25,
|
||||
23,0,33,34,40,34,33,0,0,33,34,40,34,33,0,0,33,34,40,34,33,0,0,33,34,40,34,33,0,23,
|
||||
23,0,33,33,39,33,33,0,0,33,33,39,33,33,0,0,33,33,39,33,33,0,0,33,33,39,33,33,0,23,
|
||||
25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,
|
||||
24,19,20,0,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,0,25,
|
||||
25,21,22,0,0,0,0,0,0,0,0,29,0,0,0,0,0,0,29,0,0,0,0,0,0,0,0,0,0,23,
|
||||
25,0,47,0,0,0,0,0,0,0,15,0,0,0,48,0,0,0,0,16,0,0,0,0,0,0,0,0,0,23,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,47,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,
|
||||
23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,
|
||||
25,0,0,0,0,0,0,0,0,0,0,29,0,0,15,0,0,0,29,0,0,0,0,0,0,0,0,0,0,23,
|
||||
25,0,0,0,0,0,0,0,0,0,0,0,19,20,0,0,0,0,0,0,0,0,0,0,0,0,0,19,20,23,
|
||||
25,0,0,0,0,0,0,0,0,0,0,0,21,22,0,0,23,0,0,0,0,0,0,0,0,0,0,21,22,24,
|
||||
25,19,20,0,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,
|
||||
23,21,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,23,
|
||||
23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,
|
||||
24,25,23,25,23,25,25,25,23,25,24,24,23,23,0,0,23,23,23,23,23,25,23,23,24,23,23,23,23,23
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="232" y="184">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="72" y="120">
|
||||
<properties>
|
||||
<property name="id" value="from_house"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="spawn" x="184" y="120">
|
||||
<properties>
|
||||
<property name="id" value="from_bakery"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="spawn" x="296" y="120">
|
||||
<properties>
|
||||
<property name="id" value="from_shop"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="5" name="spawn" x="408" y="120">
|
||||
<properties>
|
||||
<property name="id" value="from_clinic"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="6" name="spawn" x="24" y="168">
|
||||
<properties>
|
||||
<property name="id" value="from_lake"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="7" name="spawn" x="456" y="168">
|
||||
<properties>
|
||||
<property name="id" value="from_field"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="8" name="spawn" x="232" y="40">
|
||||
<properties>
|
||||
<property name="id" value="from_well"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="9" name="spawn" x="248" y="296">
|
||||
<properties>
|
||||
<property name="id" value="from_fog"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="10" name="door" x="72" y="88">
|
||||
<properties>
|
||||
<property name="target" value="house"/>
|
||||
<property name="spawn" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="11" name="door" x="184" y="88">
|
||||
<properties>
|
||||
<property name="target" value="bakery"/>
|
||||
<property name="spawn" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="12" name="door" x="296" y="88">
|
||||
<properties>
|
||||
<property name="target" value="shop"/>
|
||||
<property name="spawn" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="13" name="door" x="408" y="88">
|
||||
<properties>
|
||||
<property name="target" value="clinic"/>
|
||||
<property name="spawn" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="14" name="door" x="8" y="152">
|
||||
<properties>
|
||||
<property name="target" value="lake"/>
|
||||
<property name="spawn" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="15" name="door" x="472" y="168">
|
||||
<properties>
|
||||
<property name="target" value="field"/>
|
||||
<property name="spawn" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="16" name="door" x="232" y="8">
|
||||
<properties>
|
||||
<property name="target" value="well"/>
|
||||
<property name="spawn" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="17" name="door" x="248" y="312">
|
||||
<properties>
|
||||
<property name="target" value="fog"/>
|
||||
<property name="spawn" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="18" name="npc" x="248" y="136">
|
||||
<properties>
|
||||
<property name="id" value="afu"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="19" name="npc" x="200" y="232">
|
||||
<properties>
|
||||
<property name="id" value="bird"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="20" name="npc" x="120" y="200">
|
||||
<properties>
|
||||
<property name="id" value="squirrel"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="21" name="npc" x="360" y="232">
|
||||
<properties>
|
||||
<property name="id" value="fox"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="22" name="npc" x="56" y="136">
|
||||
<properties>
|
||||
<property name="id" value="dabao"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="23" name="interact" x="232" y="152">
|
||||
<properties>
|
||||
<property name="id" value="noticeboard"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="24" name="interact" x="168" y="152">
|
||||
<properties>
|
||||
<property name="id" value="flowerbed_1"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="25" name="interact" x="312" y="152">
|
||||
<properties>
|
||||
<property name="id" value="flowerbed_2"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="26" name="interact" x="232" y="232">
|
||||
<properties>
|
||||
<property name="id" value="flowerbed_3"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,207 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":10,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50,
|
||||
49, 50, 49, 50, 49, 50, 64, 64, 64, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 64, 64, 64, 49, 50, 49, 50, 49, 50,
|
||||
49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49, 50, 49,
|
||||
50, 49, 50, 49, 50, 49, 50, 39, 50, 49, 50, 49, 50, 49, 50],
|
||||
"height":10,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":15,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[51, 51, 51, 52, 51, 51, 51, 52, 51, 51, 51, 52, 51, 51, 51,
|
||||
51, 0, 0, 61, 61, 61, 61, 61, 61, 61, 61, 61, 0, 78, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 60, 61, 61, 60, 61, 61, 60, 61, 61, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 79, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51,
|
||||
51, 51, 51, 51, 51, 51, 51, 0, 51, 51, 51, 51, 51, 51, 51],
|
||||
"height":10,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":15,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":104
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":2,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"from_plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":120
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":3,
|
||||
"name":"door",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"from_shop"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":136
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":4,
|
||||
"name":"npc",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"mimi"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":40,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":5,
|
||||
"name":"npc",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"hedgehog"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":184,
|
||||
"y":120
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":6,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"shelf_count"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":40
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":7,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":15
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="15" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="7">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="15" height="10">
|
||||
<data encoding="csv">
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,50,49,50,49,50,49,50,49,50,
|
||||
49,50,49,50,49,50,64,64,64,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,64,64,64,49,50,49,50,49,50,
|
||||
49,50,49,50,49,50,49,50,49,50,49,50,49,50,49,
|
||||
50,49,50,49,50,49,50,39,50,49,50,49,50,49,50
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="15" height="10">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
51,51,51,52,51,51,51,52,51,51,51,52,51,51,51,
|
||||
51,0,0,61,61,61,61,61,61,61,61,61,0,78,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,60,61,61,60,61,61,60,61,61,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,0,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,80,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,79,0,0,0,0,0,0,0,0,0,0,0,0,51,
|
||||
51,51,51,51,51,51,51,0,51,51,51,51,51,51,51
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="120" y="104">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="120" y="120">
|
||||
<properties>
|
||||
<property name="id" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="door" x="120" y="136">
|
||||
<properties>
|
||||
<property name="target" value="plaza"/>
|
||||
<property name="spawn" value="from_shop"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="npc" x="40" y="88">
|
||||
<properties>
|
||||
<property name="id" value="mimi"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="5" name="npc" x="184" y="120">
|
||||
<properties>
|
||||
<property name="id" value="hedgehog"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="6" name="interact" x="120" y="40">
|
||||
<properties>
|
||||
<property name="id" value="shelf_count"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.2" name="candy" tilewidth="16" tileheight="16" tilecount="128" columns="16">
|
||||
<image source="../tileset.png" width="256" height="128"/>
|
||||
</tileset>
|
||||
@@ -0,0 +1,169 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":10,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 4, 1, 1,
|
||||
1, 3, 1, 3, 3, 4, 1, 4, 1, 1, 103, 1, 1, 1,
|
||||
1, 3, 1, 1, 1, 4, 1, 1, 1, 1, 101, 1, 2, 1,
|
||||
1, 1, 1, 101, 1, 5, 5, 5, 5, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 100, 1, 5, 5, 5, 5, 1, 1, 4, 1, 1,
|
||||
1, 1, 1, 3, 1, 5, 5, 5, 5, 1, 2, 1, 1, 4,
|
||||
101, 1, 1, 1, 1, 5, 5, 5, 5, 2, 100, 1, 3, 1,
|
||||
1, 1, 1, 1, 1, 1, 5, 5, 4, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 4, 5, 5, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 4, 1, 5, 5, 1, 1, 1, 1, 1, 1],
|
||||
"height":10,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":14,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0,
|
||||
0, 21, 22, 0, 23, 0, 0, 0, 0, 23, 0, 21, 22, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 43, 44, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 25, 0, 45, 46, 0, 25, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0,
|
||||
0, 21, 22, 0, 23, 0, 0, 0, 0, 23, 0, 21, 22, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":10,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":14,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":104,
|
||||
"y":136
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":2,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"from_plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":136
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":3,
|
||||
"name":"door",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"from_well"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"plaza"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":120,
|
||||
"y":152
|
||||
},
|
||||
{
|
||||
"height":0,
|
||||
"id":4,
|
||||
"name":"interact",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"ancient_well"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":104,
|
||||
"y":104
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":5,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":14
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="14" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="5">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="14" height="10">
|
||||
<data encoding="csv">
|
||||
1,1,1,1,1,1,1,2,1,1,1,4,1,1,
|
||||
1,3,1,3,3,4,1,4,1,1,103,1,1,1,
|
||||
1,3,1,1,1,4,1,1,1,1,101,1,2,1,
|
||||
1,1,1,101,1,5,5,5,5,1,1,1,1,1,
|
||||
1,1,1,100,1,5,5,5,5,1,1,4,1,1,
|
||||
1,1,1,3,1,5,5,5,5,1,2,1,1,4,
|
||||
101,1,1,1,1,5,5,5,5,2,100,1,3,1,
|
||||
1,1,1,1,1,1,5,5,4,1,1,1,1,1,
|
||||
1,1,1,1,1,4,5,5,1,1,1,1,1,1,
|
||||
1,1,1,1,4,1,5,5,1,1,1,1,1,1
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="14" height="10">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,19,20,0,0,0,0,0,0,0,0,19,20,0,
|
||||
0,21,22,0,23,0,0,0,0,23,0,21,22,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,43,44,0,0,0,0,0,0,
|
||||
0,0,0,0,25,0,45,46,0,25,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,19,20,0,0,0,0,0,0,0,0,19,20,0,
|
||||
0,21,22,0,23,0,0,0,0,23,0,21,22,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="104" y="136">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="spawn" x="120" y="136">
|
||||
<properties>
|
||||
<property name="id" value="from_plaza"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="door" x="120" y="152">
|
||||
<properties>
|
||||
<property name="target" value="plaza"/>
|
||||
<property name="spawn" value="from_well"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="4" name="interact" x="104" y="104">
|
||||
<properties>
|
||||
<property name="id" value="ancient_well"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,115 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":14,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 85,
|
||||
85, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 85,
|
||||
85, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 85,
|
||||
85, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 84, 95, 84, 84, 85,
|
||||
85, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 85,
|
||||
85, 85, 85, 85, 84, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 84, 95, 84, 84, 95, 84, 84, 95, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 95, 84, 84, 95, 84, 84, 95, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 84, 84, 95, 84, 84, 95, 84, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 84, 95, 84, 84, 95, 84, 84, 95, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 95, 84, 84, 95, 84, 84, 95, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 84, 84, 95, 84, 84, 95, 84, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85],
|
||||
"height":14,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[85, 85, 85, 96, 85, 86, 85, 85, 96, 85, 86, 85, 85, 96, 85, 86, 85, 85, 96, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85,
|
||||
85, 85, 85, 85, 0, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 0, 88, 89, 93, 0, 0, 0, 0, 94, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 0, 90, 91, 0, 0, 0, 0, 0, 94, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 0, 0, 0, 0, 0, 0, 92, 0, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
|
||||
85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85],
|
||||
"height":14,
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":0,
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"opacity":1,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":0,
|
||||
"x":280,
|
||||
"y":56
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":2,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/tileset.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"candy",
|
||||
"spacing":0,
|
||||
"tilecount":128,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":20
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="14" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="2">
|
||||
<tileset firstgid="1" source="tileset.tsx"/>
|
||||
<layer id="1" name="ground" width="20" height="14">
|
||||
<data encoding="csv">
|
||||
85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,85,
|
||||
85,84,84,84,95,84,84,84,95,84,84,84,95,84,84,84,95,84,84,85,
|
||||
85,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,85,
|
||||
85,84,84,84,95,84,84,84,95,84,84,84,95,84,84,84,95,84,84,85,
|
||||
85,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,84,85,
|
||||
85,85,85,85,84,84,85,85,85,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,84,95,84,84,95,84,84,95,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,95,84,84,95,84,84,95,84,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,84,84,95,84,84,95,84,84,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,84,95,84,84,95,84,84,95,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,95,84,84,95,84,84,95,84,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,84,84,95,84,84,95,84,84,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="obstacles" width="20" height="14">
|
||||
<properties>
|
||||
<property name="collides" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
85,85,85,96,85,86,85,85,96,85,86,85,85,96,85,86,85,85,96,85,
|
||||
85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
|
||||
85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
|
||||
85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
|
||||
85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
|
||||
85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,
|
||||
85,85,85,85,0,0,85,85,85,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,0,0,0,0,0,0,0,0,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,0,88,89,93,0,0,0,0,94,85,85,85,85,85,85,85,85,85,85,
|
||||
85,0,90,91,0,0,0,0,0,94,85,85,85,85,85,85,85,85,85,85,
|
||||
85,0,0,0,0,0,0,0,0,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,0,0,0,0,0,0,0,0,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,0,0,0,0,0,0,92,0,85,85,85,85,85,85,85,85,85,85,85,
|
||||
85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="spawn" x="280" y="56">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
|
After Width: | Height: | Size: 319 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 291 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 297 B |
|
After Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 300 B |
|
After Width: | Height: | Size: 280 B |
|
After Width: | Height: | Size: 270 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 384 B |
|
After Width: | Height: | Size: 408 B |
|
After Width: | Height: | Size: 397 B |
|
After Width: | Height: | Size: 402 B |
|
After Width: | Height: | Size: 420 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 360 B |
|
After Width: | Height: | Size: 993 B |
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,121 @@
|
||||
// NPC 与群演行为组件(PRD §4 / §6.4)。
|
||||
// 表情档:0=smile 1=flat 2=off,按阶段 + 个体阈值偏移切换;
|
||||
// S3 起静止 NPC 面朝玩家(flipX);S4:off 档 0.6 次/分钟闪 0.1 秒、群演随机 1 只停摆;
|
||||
// 双子鸟张嘴动画循环(S4 照张);阶段切换时鸟动画卡帧一下。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { NPC_DEFS, stageOf, type NpcDef, type NpcId, type Stage } from '../types';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
|
||||
const FACE_FRAME: Record<number, number> = { 0: 0, 1: 0, 2: 0, 3: 1, 4: 1 }; // 阶段→表情帧(S4 基础 flat,off 靠闪)
|
||||
|
||||
export class NpcActor {
|
||||
readonly def: NpcDef;
|
||||
readonly sprite: Phaser.Physics.Arcade.Sprite;
|
||||
readonly isExtra: boolean;
|
||||
|
||||
private scene: Phaser.Scene;
|
||||
/** 群演巡走范围 */
|
||||
private patrolLeft = 0;
|
||||
private patrolRight = 0;
|
||||
private patrolDir = 1;
|
||||
private patrolSpeed = 24;
|
||||
/** S4 停摆标记(每场景 1 只) */
|
||||
stalled = false;
|
||||
/** S4 off 档闪帧计时 */
|
||||
private offFlashMs = 0;
|
||||
/** 被「悲伤行为」惊动,转头看玩家计时 */
|
||||
private glanceMs = 0;
|
||||
/** T14 全村注视 */
|
||||
stareAtPlayer = false;
|
||||
|
||||
constructor(scene: Phaser.Scene, id: NpcId, x: number, y: number) {
|
||||
this.scene = scene;
|
||||
this.def = NPC_DEFS[id];
|
||||
this.isExtra = this.def.isExtra;
|
||||
this.sprite = scene.physics.add.sprite(x, y, this.def.spriteKey, 0);
|
||||
this.sprite.setOrigin(0.5, 1);
|
||||
this.sprite.setImmovable(true);
|
||||
(this.sprite.body as Phaser.Physics.Arcade.Body).setAllowGravity(false);
|
||||
this.sprite.setSize(12, 8).setOffset(2, 16);
|
||||
|
||||
if (this.isExtra) {
|
||||
this.patrolLeft = x - 32;
|
||||
this.patrolRight = x + 32;
|
||||
const anim = `${this.def.spriteKey}-walk`;
|
||||
if (scene.anims.exists(anim)) this.sprite.play(anim);
|
||||
} else if (id === 'bird') {
|
||||
if (scene.anims.exists('bird-sing')) this.sprite.play('bird-sing');
|
||||
}
|
||||
}
|
||||
|
||||
/** 悲伤行为触发:转头看玩家一次 */
|
||||
glanceAt(playerX: number): void {
|
||||
this.glanceMs = 1200;
|
||||
this.sprite.setFlipX(playerX < this.sprite.x);
|
||||
}
|
||||
|
||||
update(deltaMs: number, player: Phaser.Physics.Arcade.Sprite): void {
|
||||
const stage = director.stage;
|
||||
this.sprite.setDepth(this.sprite.y);
|
||||
|
||||
if (this.isExtra) {
|
||||
this.updateExtra(deltaMs, player, stage);
|
||||
} else {
|
||||
this.updateNamed(deltaMs, player, stage);
|
||||
}
|
||||
}
|
||||
|
||||
private updateNamed(deltaMs: number, player: Phaser.Physics.Arcade.Sprite, stage: Stage): void {
|
||||
// 表情档(芙医生/龟爷爷/双子鸟不参与侵蚀)
|
||||
if (this.def.expressive) {
|
||||
const effStage = stageOf(director.happiness - this.def.offset);
|
||||
let frame = FACE_FRAME[effStage];
|
||||
if (this.offFlashMs > 0) {
|
||||
frame = 2; // off 档闪现
|
||||
this.offFlashMs -= deltaMs;
|
||||
} else if (effStage === 4) {
|
||||
// S4:0.6 次/分钟闪 0.1 秒 → 每秒约 1% 概率触发
|
||||
if (Math.random() < (0.6 / 60) * (deltaMs / 1000)) this.offFlashMs = 100;
|
||||
}
|
||||
this.sprite.setFrame(frame);
|
||||
}
|
||||
|
||||
// S3 起静止 NPC 面朝玩家;被惊动时也面朝玩家
|
||||
const facePlayer = stage >= 3 || this.glanceMs > 0 || this.stareAtPlayer;
|
||||
if (facePlayer && this.def.id !== 'bird') {
|
||||
this.sprite.setFlipX(player.x < this.sprite.x);
|
||||
}
|
||||
if (this.glanceMs > 0) this.glanceMs -= deltaMs;
|
||||
}
|
||||
|
||||
private updateExtra(deltaMs: number, player: Phaser.Physics.Arcade.Sprite, stage: Stage): void {
|
||||
if (this.stalled) {
|
||||
// S4 停摆:原地面朝玩家
|
||||
this.sprite.setVelocity(0);
|
||||
this.sprite.setFrame(0);
|
||||
this.sprite.setFlipX(player.x < this.sprite.x);
|
||||
return;
|
||||
}
|
||||
const x = this.sprite.x;
|
||||
if (x <= this.patrolLeft) this.patrolDir = 1;
|
||||
if (x >= this.patrolRight) this.patrolDir = -1;
|
||||
this.sprite.setVelocityX(this.patrolDir * this.patrolSpeed);
|
||||
this.sprite.setFlipX(this.patrolDir < 0);
|
||||
if (stage >= 3) {
|
||||
// S3 起也会偶尔看向玩家?契约只要求 S4 停摆,巡走继续
|
||||
}
|
||||
void deltaMs;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.sprite.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 群演停摆选择:S4 起随机 1 只停止循环 */
|
||||
export function pickStallExtra(actors: NpcActor[]): NpcActor | null {
|
||||
const extras = actors.filter((a) => a.isExtra);
|
||||
if (extras.length === 0) return null;
|
||||
return extras[Math.floor(Math.random() * extras.length)];
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// AudioEngine:WebAudio 程序合成(PRD §6.6),无任何外部音频文件。
|
||||
// 8 小节 8-bit 循环:方波主旋律 + 三角波低音 + 鸟鸣声部。
|
||||
// playbackRate 与 BiquadFilter 低通由 CorruptionDirector(§6.4)驱动;S4 鸟鸣静音。
|
||||
// 音效 4 个:对话哔、完成叮、黄昏钟、E2 心电滴。
|
||||
// 首次用户交互后才启动 AudioContext。
|
||||
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import type { Stage, StageParams } from '../types';
|
||||
|
||||
// 旋律:C - G - Am - F / C - G - F - G,4/4,100 BPM,每拍一个四分音符
|
||||
const BPM = 100;
|
||||
const BEAT = 60 / BPM; // 0.6s
|
||||
const BARS = 8;
|
||||
|
||||
const LEAD: number[][] = [
|
||||
[76, 79, 84, 79], // C: E5 G5 C6 G5
|
||||
[74, 79, 83, 79], // G: D5 G5 B5 G5
|
||||
[72, 76, 81, 76], // Am: C5 E5 A5 E5
|
||||
[72, 77, 81, 77], // F: C5 F5 A5 F5
|
||||
[79, 76, 72, 76], // C: G5 E5 C5 E5
|
||||
[74, 71, 67, 71], // G: D5 B4 G4 B4
|
||||
[69, 72, 77, 72], // F: A4 C5 F5 C5
|
||||
[67, 71, 74, 67], // G: G4 B4 D5 G4
|
||||
];
|
||||
const BASS: number[] = [48, 43, 45, 41, 48, 43, 41, 43]; // C3 G2 A2 F2 ...
|
||||
// 鸟鸣:第 2/4/6/8 小节第 4 拍
|
||||
const BIRD_BARS = [1, 3, 5, 7];
|
||||
|
||||
function midiToFreq(n: number): number {
|
||||
return 440 * Math.pow(2, (n - 69) / 12);
|
||||
}
|
||||
|
||||
export class AudioEngine {
|
||||
private static _instance: AudioEngine | null = null;
|
||||
static get instance(): AudioEngine {
|
||||
if (!this._instance) this._instance = new AudioEngine();
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private ctx: AudioContext | null = null;
|
||||
private master!: GainNode;
|
||||
private lp!: BiquadFilterNode;
|
||||
private musicBus!: GainNode;
|
||||
private birdBus!: GainNode;
|
||||
private sfxBus!: GainNode;
|
||||
|
||||
private rate = 1;
|
||||
private started = false;
|
||||
private nextNoteTime = 0;
|
||||
private step = 0; // 0..31(8 小节 × 4 拍)
|
||||
private schedulerId = 0;
|
||||
|
||||
private constructor() {
|
||||
// 阶段参数 → 音频参数
|
||||
director.on('stage', (_s: Stage, p: StageParams) => {
|
||||
this.setParams(p);
|
||||
});
|
||||
}
|
||||
|
||||
/** 首次用户交互后调用 */
|
||||
start(): void {
|
||||
if (this.started) return;
|
||||
this.started = true;
|
||||
const AC = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
|
||||
this.ctx = new AC();
|
||||
this.master = this.ctx.createGain();
|
||||
this.master.gain.value = 0.35;
|
||||
this.master.connect(this.ctx.destination);
|
||||
|
||||
this.lp = this.ctx.createBiquadFilter();
|
||||
this.lp.type = 'lowpass';
|
||||
this.lp.frequency.value = 22050;
|
||||
this.lp.connect(this.master);
|
||||
|
||||
this.musicBus = this.ctx.createGain();
|
||||
this.musicBus.gain.value = 0.9;
|
||||
this.musicBus.connect(this.lp);
|
||||
|
||||
this.birdBus = this.ctx.createGain();
|
||||
this.birdBus.gain.value = 0.5;
|
||||
this.birdBus.connect(this.lp);
|
||||
|
||||
this.sfxBus = this.ctx.createGain();
|
||||
this.sfxBus.gain.value = 1;
|
||||
this.sfxBus.connect(this.master); // 音效不走低通
|
||||
|
||||
this.nextNoteTime = this.ctx.currentTime + 0.1;
|
||||
this.schedulerId = window.setInterval(() => this.scheduler(), 25);
|
||||
this.setParams(director.params);
|
||||
}
|
||||
|
||||
private setParams(p: StageParams): void {
|
||||
this.rate = p.playbackRate;
|
||||
if (!this.ctx) return;
|
||||
this.lp.frequency.setTargetAtTime(p.lowpass, this.ctx.currentTime, 0.2);
|
||||
this.birdBus.gain.setTargetAtTime(p.birdMuted ? 0 : 0.5, this.ctx.currentTime, 0.2);
|
||||
}
|
||||
|
||||
private scheduler(): void {
|
||||
if (!this.ctx) return;
|
||||
const lookAhead = 0.15;
|
||||
while (this.nextNoteTime < this.ctx.currentTime + lookAhead) {
|
||||
this.scheduleStep(this.step, this.nextNoteTime);
|
||||
const beatDur = BEAT / this.rate;
|
||||
this.nextNoteTime += beatDur;
|
||||
this.step = (this.step + 1) % (BARS * 4);
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleStep(step: number, time: number): void {
|
||||
if (!this.ctx) return;
|
||||
const bar = Math.floor(step / 4);
|
||||
const beat = step % 4;
|
||||
const beatDur = BEAT / this.rate;
|
||||
|
||||
// 方波主旋律
|
||||
const lead = LEAD[bar][beat];
|
||||
this.tone(midiToFreq(lead), time, beatDur * 0.9, 'square', 0.10, this.musicBus, 0.005, 0.06);
|
||||
|
||||
// 三角波低音(每拍重复根音)
|
||||
const bass = BASS[bar];
|
||||
this.tone(midiToFreq(bass), time, beatDur * 0.95, 'triangle', 0.16, this.musicBus, 0.01, 0.1);
|
||||
|
||||
// 鸟鸣声部
|
||||
if (BIRD_BARS.includes(bar) && beat === 3) {
|
||||
this.chirp(time);
|
||||
this.chirp(time + beatDur * 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
private tone(
|
||||
freq: number, time: number, dur: number, type: OscillatorType,
|
||||
vol: number, bus: GainNode, attack = 0.005, release = 0.08,
|
||||
): void {
|
||||
if (!this.ctx) return;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const g = this.ctx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
g.gain.setValueAtTime(0.0001, time);
|
||||
g.gain.exponentialRampToValueAtTime(vol, time + attack);
|
||||
g.gain.setValueAtTime(vol, time + Math.max(attack, dur - release));
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, time + dur);
|
||||
osc.connect(g).connect(bus);
|
||||
osc.start(time);
|
||||
osc.stop(time + dur + 0.02);
|
||||
}
|
||||
|
||||
/** 鸟鸣:高频正弦快速双音 */
|
||||
private chirp(time: number): void {
|
||||
if (!this.ctx) return;
|
||||
const base = 2600 + Math.random() * 600;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const g = this.ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(base, time);
|
||||
osc.frequency.exponentialRampToValueAtTime(base * 1.5, time + 0.05);
|
||||
g.gain.setValueAtTime(0.0001, time);
|
||||
g.gain.exponentialRampToValueAtTime(0.08, time + 0.01);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, time + 0.12);
|
||||
osc.connect(g).connect(this.birdBus);
|
||||
osc.start(time);
|
||||
osc.stop(time + 0.15);
|
||||
}
|
||||
|
||||
// ---------- 音效(4 个,全合成) ----------
|
||||
|
||||
/** 对话哔哔声 */
|
||||
beep(): void {
|
||||
if (!this.ctx) return;
|
||||
this.tone(660, this.ctx.currentTime, 0.05, 'square', 0.10, this.sfxBus, 0.002, 0.03);
|
||||
}
|
||||
|
||||
/** 任务完成叮声 */
|
||||
ding(): void {
|
||||
if (!this.ctx) return;
|
||||
const t = this.ctx.currentTime;
|
||||
this.tone(880, t, 0.12, 'sine', 0.16, this.sfxBus, 0.002, 0.1);
|
||||
this.tone(1320, t + 0.1, 0.22, 'sine', 0.16, this.sfxBus, 0.002, 0.18);
|
||||
}
|
||||
|
||||
/** 黄昏钟声 */
|
||||
bell(): void {
|
||||
if (!this.ctx) return;
|
||||
const t = this.ctx.currentTime;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
this.tone(220, t + i * 1.2, 1.4, 'triangle', 0.22, this.sfxBus, 0.01, 1.2);
|
||||
this.tone(110, t + i * 1.2, 1.6, 'sine', 0.14, this.sfxBus, 0.01, 1.4);
|
||||
}
|
||||
}
|
||||
|
||||
/** E2 心电监护滴声(单次) */
|
||||
ecg(): void {
|
||||
if (!this.ctx) return;
|
||||
const t = this.ctx.currentTime;
|
||||
this.tone(1000, t, 0.08, 'sine', 0.18, this.sfxBus, 0.002, 0.06);
|
||||
}
|
||||
|
||||
/** 井底回声:极轻的监护仪滴声(C7) */
|
||||
ecgSoft(): void {
|
||||
if (!this.ctx) return;
|
||||
const t = this.ctx.currentTime;
|
||||
this.tone(1000, t, 0.08, 'sine', 0.05, this.sfxBus, 0.002, 0.06);
|
||||
}
|
||||
|
||||
// ---------- T14 第 40–60 秒:所有声音渐隐至零 ----------
|
||||
|
||||
fadeAllToZero(durationMs: number): void {
|
||||
if (!this.ctx) return;
|
||||
const t = this.ctx.currentTime;
|
||||
this.master.gain.cancelScheduledValues(t);
|
||||
this.master.gain.setValueAtTime(this.master.gain.value, t);
|
||||
this.master.gain.linearRampToValueAtTime(0.0001, t + durationMs / 1000);
|
||||
}
|
||||
|
||||
restoreMaster(durationMs = 2000): void {
|
||||
if (!this.ctx) return;
|
||||
const t = this.ctx.currentTime;
|
||||
this.master.gain.cancelScheduledValues(t);
|
||||
this.master.gain.setValueAtTime(this.master.gain.value, t);
|
||||
this.master.gain.linearRampToValueAtTime(0.35, t + durationMs / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
export const audio = AudioEngine.instance;
|
||||
@@ -0,0 +1,418 @@
|
||||
// CorruptionDirector:全部状态唯一真源(PRD §6 / CONTRACT §4)。
|
||||
// 普通 TS 单例 + Phaser.Events.EventEmitter。
|
||||
// 事件:happiness / awareness / stage / day / clue / task / celebration / ending(契约),
|
||||
// 增补:negativeFlash / firstFull / npcGlance / dusk / prompt / dialogue / rain。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import {
|
||||
STAGE_PARAMS, stageOf, type Stage, type StageParams,
|
||||
type SadnessKind, type SmallInteractionKind, type ChoiceKind, type EndingId,
|
||||
type DialoguePayload, type NpcId,
|
||||
} from '../types';
|
||||
import { TASKS, tasksOfDay } from '../data/tasks';
|
||||
import { CLUE_AWARENESS } from '../data/clues';
|
||||
|
||||
const LOW_HAPPY_THRESHOLD = 30;
|
||||
const LOW_HAPPY_MS = 2 * 60 * 1000; // <30 持续 2 分钟 → 欢乐庆典
|
||||
const OVERFLOW_TRIGGER = 15; // 溢出池 15 点 → 负片闪帧
|
||||
const CELEBRATION_RESTORE = 50;
|
||||
const DAY_LENGTH_MS = 5 * 60 * 1000; // 游戏内计时:5 分钟未到也会黄昏(7 天 ≤35 分钟,§14.16)
|
||||
|
||||
export class CorruptionDirector extends Phaser.Events.EventEmitter {
|
||||
private static _instance: CorruptionDirector | null = null;
|
||||
static get instance(): CorruptionDirector {
|
||||
if (!this._instance) this._instance = new CorruptionDirector();
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
// ---------- 状态 ----------
|
||||
playerName = '小满';
|
||||
startTime = Date.now();
|
||||
day = 1;
|
||||
happiness = 30;
|
||||
awareness = 0;
|
||||
readonly clues = new Set<string>();
|
||||
readonly tasksDone = new Set<string>();
|
||||
readonly refusedTasks = new Set<string>();
|
||||
escapedCelebrations = 0;
|
||||
/** 最近一次对话关闭的时间戳(ms):MapScene 用它防止「关对话的同一次 E 按下」立刻重开交互 */
|
||||
dialogueClosedAt = 0;
|
||||
|
||||
/** 任务进度计数(如 T1 已送几份面包),每日清空 */
|
||||
taskProgress: Record<string, number> = {};
|
||||
/** 集合型进度(如 T3 已问好 npc 列表),每日清空 */
|
||||
taskSetProgress: Record<string, Set<string>> = {};
|
||||
|
||||
rainActive = false;
|
||||
celebrationActive = false;
|
||||
/** 对话/梦境/结局进行中:抑制发呆判定与移动 */
|
||||
dialogueActive = false;
|
||||
/** 场景切换过场中(床/门淡出→新场景):抑制庆典触发,防止庆典对话被切场景覆盖后卡死 */
|
||||
sceneTransition = false;
|
||||
/** T14 进行中 */
|
||||
t14Active = false;
|
||||
|
||||
/** E4 监视:D5-7 期间幸福是否曾 >10 */
|
||||
e4HappyOk = true;
|
||||
|
||||
private overflowPool = 0;
|
||||
private lowHappyMs = 0;
|
||||
private negFlashTimer = 60_000;
|
||||
private firstFullDone = false;
|
||||
private dayElapsedMs = 0;
|
||||
|
||||
/** 每日重置的计数 */
|
||||
private talkCounts: Record<string, number> = {}; // 纠缠判定:对同一 NPC 一天内对话次数
|
||||
private smallCounts: Record<string, number> = {}; // 小互动每类每日上限 3
|
||||
private stareDone: Record<string, boolean> = {}; // 注视古井/雾墙各每日 1 次
|
||||
|
||||
/** 对话 Promise 解析器(UIScene 完成后回调) */
|
||||
private dialogueResolver: ((choice: number | null) => void) | null = null;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
// ---------- 派生 ----------
|
||||
get stage(): Stage {
|
||||
return stageOf(this.happiness);
|
||||
}
|
||||
|
||||
get params(): StageParams {
|
||||
return STAGE_PARAMS[this.stage];
|
||||
}
|
||||
|
||||
/** 真实游玩时长(ms) */
|
||||
get playTimeMs(): number {
|
||||
return Date.now() - this.startTime;
|
||||
}
|
||||
|
||||
get playTimeFormatted(): string {
|
||||
const totalSec = Math.floor(this.playTimeMs / 1000);
|
||||
const m = Math.floor(totalSec / 60);
|
||||
const s = totalSec % 60;
|
||||
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** 当天任务是否全部完成 */
|
||||
get dailyTasksAllDone(): boolean {
|
||||
return tasksOfDay(this.day).every((t) => this.tasksDone.has(t.id));
|
||||
}
|
||||
|
||||
/** 是否可以睡觉(任务全部了结——完成或拒绝,或计时已到黄昏) */
|
||||
get canSleep(): boolean {
|
||||
if (this.day >= 7) return false;
|
||||
return this.dailySettled || this.dayElapsedMs >= DAY_LENGTH_MS;
|
||||
}
|
||||
|
||||
// ---------- 新游戏 ----------
|
||||
newGame(name: string): void {
|
||||
this.playerName = name.trim() || '小满';
|
||||
this.startTime = Date.now();
|
||||
this.day = 1;
|
||||
this.happiness = 30;
|
||||
this.awareness = 0;
|
||||
this.clues.clear();
|
||||
this.tasksDone.clear();
|
||||
this.refusedTasks.clear();
|
||||
this.escapedCelebrations = 0;
|
||||
this.taskProgress = {};
|
||||
this.taskSetProgress = {};
|
||||
this.rainActive = false;
|
||||
this.celebrationActive = false;
|
||||
this.dialogueActive = false;
|
||||
this.sceneTransition = false;
|
||||
this.t14Active = false;
|
||||
this.e4HappyOk = true;
|
||||
this.overflowPool = 0;
|
||||
this.lowHappyMs = 0;
|
||||
this.firstFullDone = false;
|
||||
this.dayElapsedMs = 0;
|
||||
this.resetDaily();
|
||||
this.emit('day', this.day);
|
||||
this.emit('happiness', this.happiness);
|
||||
this.emit('awareness', this.awareness);
|
||||
this.emit('stage', this.stage, this.params);
|
||||
}
|
||||
|
||||
private resetDaily(): void {
|
||||
this.talkCounts = {};
|
||||
this.smallCounts = {};
|
||||
this.stareDone = {};
|
||||
this.taskProgress = {};
|
||||
this.taskSetProgress = {};
|
||||
}
|
||||
|
||||
// ---------- 幸福值 ----------
|
||||
addHappiness(delta: number): void {
|
||||
if (delta === 0) return;
|
||||
const prevStage = this.stage;
|
||||
|
||||
if (delta < 0 && this.happiness >= 100) {
|
||||
// 满格态:小额扣分不再生效,累积进隐藏溢出池
|
||||
this.overflowPool += -delta;
|
||||
if (this.overflowPool >= OVERFLOW_TRIGGER) {
|
||||
this.overflowPool -= OVERFLOW_TRIGGER;
|
||||
this.emit('negativeFlash');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.happiness = Math.max(0, Math.min(100, this.happiness + delta));
|
||||
this.emit('happiness', this.happiness);
|
||||
|
||||
if (this.day >= 5 && this.happiness > 10) this.e4HappyOk = false;
|
||||
|
||||
if (this.happiness >= 100 && !this.firstFullDone) {
|
||||
this.firstFullDone = true;
|
||||
this.emit('firstFull');
|
||||
}
|
||||
if (this.stage !== prevStage) {
|
||||
this.emit('stage', this.stage, this.params);
|
||||
}
|
||||
}
|
||||
|
||||
setHappiness(v: number): void {
|
||||
this.happiness = Math.max(0, Math.min(100, Math.round(v)));
|
||||
this.emit('happiness', this.happiness);
|
||||
if (this.day >= 5 && this.happiness > 10) this.e4HappyOk = false;
|
||||
this.emit('stage', this.stage, this.params); // 始终广播,保证 debug 立即生效
|
||||
}
|
||||
|
||||
// ---------- 觉知值(只升不降) ----------
|
||||
addAwareness(delta: number): void {
|
||||
if (delta <= 0) return;
|
||||
this.awareness = Math.max(0, Math.min(100, this.awareness + delta));
|
||||
this.emit('awareness', this.awareness);
|
||||
}
|
||||
|
||||
setAwareness(v: number): void {
|
||||
this.awareness = Math.max(0, Math.min(100, Math.round(v)));
|
||||
this.emit('awareness', this.awareness);
|
||||
}
|
||||
|
||||
// ---------- 悲伤行为(§6.3:幸福 −5、觉知 +3、最近 NPC 转头看一次) ----------
|
||||
recordSadness(kind: SadnessKind): void {
|
||||
this.addHappiness(-5);
|
||||
this.addAwareness(3);
|
||||
this.emit('npcGlance', kind);
|
||||
this.emit('sadness', kind);
|
||||
}
|
||||
|
||||
// ---------- 小互动(浇花/喂食/摸头 +1,每类每日上限 3 次) ----------
|
||||
smallInteraction(kind: SmallInteractionKind): boolean {
|
||||
const n = this.smallCounts[kind] ?? 0;
|
||||
if (n >= 3) return false;
|
||||
this.smallCounts[kind] = n + 1;
|
||||
this.addHappiness(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- 对话选项 ----------
|
||||
applyChoice(kind: ChoiceKind): void {
|
||||
if (kind === 'gentle') {
|
||||
this.addHappiness(2);
|
||||
} else if (kind === 'hesitant') {
|
||||
this.addAwareness(2);
|
||||
} else if (kind === 'deny') {
|
||||
this.addAwareness(2);
|
||||
this.recordSadness('deny'); // 否定选项同时算悲伤行为
|
||||
}
|
||||
// accept/refuse 由任务逻辑处理
|
||||
}
|
||||
|
||||
// ---------- NPC 对话登记(纠缠:同一 NPC 一天内 ≥5 次 → 悲伤行为) ----------
|
||||
registerTalk(npcId: NpcId): void {
|
||||
if (npcId === 'bird') return;
|
||||
const n = (this.talkCounts[npcId] ?? 0) + 1;
|
||||
this.talkCounts[npcId] = n;
|
||||
if (n >= 5) {
|
||||
this.recordSadness('cling');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 注视古井/雾墙(各每日 1 次,+3 觉知) ----------
|
||||
registerStare(which: 'well' | 'fog'): boolean {
|
||||
if (this.stareDone[which]) return false;
|
||||
this.stareDone[which] = true;
|
||||
this.addAwareness(3);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- 线索(每处 +8) ----------
|
||||
addClue(id: string): boolean {
|
||||
if (this.clues.has(id)) return false;
|
||||
this.clues.add(id);
|
||||
this.addAwareness(CLUE_AWARENESS);
|
||||
this.emit('clue', id);
|
||||
return true;
|
||||
}
|
||||
|
||||
giveAllClues(): void {
|
||||
['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'].forEach((id) => this.addClue(id));
|
||||
}
|
||||
|
||||
// ---------- 任务 ----------
|
||||
completeTask(id: string): void {
|
||||
if (this.tasksDone.has(id)) return;
|
||||
this.tasksDone.add(id);
|
||||
const def = TASKS[id];
|
||||
if (def && def.reward > 0) this.addHappiness(def.reward);
|
||||
this.emit('task', id);
|
||||
if (this.dailyTasksAllDone && this.day < 7) {
|
||||
this.emit('dusk'); // 黄昏钟声
|
||||
}
|
||||
}
|
||||
|
||||
refuseTask(id: string): void {
|
||||
if (this.tasksDone.has(id) || this.refusedTasks.has(id)) return;
|
||||
this.refusedTasks.add(id);
|
||||
this.recordSadness('refuse'); // 拒绝被记录 = 悲伤行为
|
||||
this.emit('task', `refused:${id}`);
|
||||
// 拒绝也视为该任务「了结」,检查黄昏
|
||||
const allSettled = tasksOfDay(this.day).every(
|
||||
(t) => this.tasksDone.has(t.id) || this.refusedTasks.has(t.id),
|
||||
);
|
||||
if (allSettled && this.day < 7) this.emit('dusk');
|
||||
}
|
||||
|
||||
isTaskActive(id: string): boolean {
|
||||
const def = TASKS[id];
|
||||
return !!def && def.day === this.day && !this.tasksDone.has(id) && !this.refusedTasks.has(id);
|
||||
}
|
||||
|
||||
/** 当日任务是否全部了结(完成或拒绝) */
|
||||
get dailySettled(): boolean {
|
||||
return tasksOfDay(this.day).every(
|
||||
(t) => this.tasksDone.has(t.id) || this.refusedTasks.has(t.id),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 日期 ----------
|
||||
nextDay(): void {
|
||||
if (this.day >= 7) return;
|
||||
this.day += 1;
|
||||
this.dayElapsedMs = 0;
|
||||
this.rainActive = false;
|
||||
this.lowHappyMs = 0;
|
||||
// 新的一天不可能从庆典/睡觉过场中开始(防庆典对话被覆盖后卡死)
|
||||
this.celebrationActive = false;
|
||||
this.sceneTransition = false;
|
||||
this.resetDaily();
|
||||
this.emit('day', this.day);
|
||||
}
|
||||
|
||||
setDay(d: number): void {
|
||||
this.day = Math.max(1, Math.min(7, Math.round(d)));
|
||||
this.dayElapsedMs = 0;
|
||||
this.rainActive = false;
|
||||
this.resetDaily();
|
||||
this.emit('day', this.day);
|
||||
}
|
||||
|
||||
// ---------- 欢乐庆典(<30 持续 2 分钟 → 拉回 50;可逃脱) ----------
|
||||
private startCelebration(): void {
|
||||
if (this.celebrationActive || this.dialogueActive) return;
|
||||
this.celebrationActive = true;
|
||||
this.lowHappyMs = 0;
|
||||
this.emit('celebration', 'start');
|
||||
}
|
||||
|
||||
completeCelebration(): void {
|
||||
if (!this.celebrationActive) return;
|
||||
this.celebrationActive = false;
|
||||
const prevStage = this.stage;
|
||||
this.happiness = CELEBRATION_RESTORE;
|
||||
this.emit('happiness', this.happiness);
|
||||
if (this.day >= 5) this.e4HappyOk = false;
|
||||
if (this.stage !== prevStage) this.emit('stage', this.stage, this.params);
|
||||
this.emit('celebration', 'end');
|
||||
}
|
||||
|
||||
escapeCelebration(): void {
|
||||
if (!this.celebrationActive) return;
|
||||
this.celebrationActive = false;
|
||||
this.escapedCelebrations += 1;
|
||||
this.emit('celebration', 'escape');
|
||||
}
|
||||
|
||||
// ---------- 雨 ----------
|
||||
setRain(active: boolean): void {
|
||||
if (this.rainActive === active) return;
|
||||
this.rainActive = active;
|
||||
this.emit('rain', active);
|
||||
}
|
||||
|
||||
// ---------- 每帧计时(由 MapScene.update 驱动) ----------
|
||||
tick(deltaMs: number): void {
|
||||
this.dayElapsedMs += deltaMs;
|
||||
|
||||
// E4 监视:D5-7 幸福曾 >10 即失格(含被动状态,不靠加减分触发)
|
||||
if (this.day >= 5 && this.happiness > 10) this.e4HappyOk = false;
|
||||
|
||||
// 连续低幸福计时(真实时间)
|
||||
if (this.happiness < LOW_HAPPY_THRESHOLD && !this.celebrationActive && !this.dialogueActive && !this.sceneTransition) {
|
||||
this.lowHappyMs += deltaMs;
|
||||
if (this.lowHappyMs >= LOW_HAPPY_MS) {
|
||||
this.startCelebration();
|
||||
}
|
||||
} else if (this.happiness >= LOW_HAPPY_THRESHOLD) {
|
||||
this.lowHappyMs = 0;
|
||||
}
|
||||
|
||||
// S4 随机单帧负片(0.4 次/分钟 → 平均间隔 150 秒)
|
||||
if (this.stage === 4) {
|
||||
this.negFlashTimer -= deltaMs;
|
||||
if (this.negFlashTimer <= 0) {
|
||||
this.emit('negativeFlash');
|
||||
this.negFlashTimer = 100_000 + Math.random() * 100_000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 结局判定(§9 优先级:E4 > E3 > E2 > E1) ----------
|
||||
evaluateEnding(atFogWall: boolean): EndingId {
|
||||
if (this.e4HappyOk && this.happiness <= 10 && this.day >= 5 && this.escapedCelebrations >= 3) return 'E4';
|
||||
if (atFogWall && this.awareness >= 70 && this.clues.size >= 6) return 'E3';
|
||||
if (this.awareness >= 30) return 'E2';
|
||||
if (this.happiness >= 90) return 'E1';
|
||||
// 觉知 ≤29 但幸福 <90 的边界:按最接近的「回收」处理
|
||||
return 'E2';
|
||||
}
|
||||
|
||||
startEnding(id: EndingId): void {
|
||||
this.emit('ending', id);
|
||||
}
|
||||
|
||||
// ---------- 对话总线(MapScene → UIScene,Promise 化) ----------
|
||||
showDialogue(payload: DialoguePayload): Promise<number | null> {
|
||||
// 重入保护:上一个对话的 resolver 若仍悬挂(被新对话覆盖),
|
||||
// 必须先将其解析,否则 await 它的一方(如梦境过场)会永久卡住
|
||||
if (this.dialogueResolver) this.resolveDialogue(null);
|
||||
this.dialogueActive = true;
|
||||
return new Promise((resolve) => {
|
||||
this.dialogueResolver = resolve;
|
||||
this.emit('dialogue', payload);
|
||||
});
|
||||
}
|
||||
|
||||
/** UIScene 对话结束时调用 */
|
||||
resolveDialogue(choice: number | null): void {
|
||||
this.dialogueActive = false;
|
||||
this.dialogueClosedAt = Date.now();
|
||||
const r = this.dialogueResolver;
|
||||
this.dialogueResolver = null;
|
||||
if (r) r(choice);
|
||||
}
|
||||
|
||||
/** debug 用:强制清空进行中的一次性状态(对话/庆典/T14),避免传送后卡死 */
|
||||
forceIdle(): void {
|
||||
this.celebrationActive = false;
|
||||
this.t14Active = false;
|
||||
this.rainActive = false;
|
||||
this.sceneTransition = false;
|
||||
this.resolveDialogue(null);
|
||||
}
|
||||
}
|
||||
|
||||
export const director = CorruptionDirector.instance;
|
||||
@@ -0,0 +1,94 @@
|
||||
// PRD §6.5 文本腐蚀:同音错字对照表(全 10 组,逐字实现)。
|
||||
// 腐蚀只在对话渲染层实施(原文案保持干净),优先命中咪咪的台词。
|
||||
|
||||
import type { Stage } from '../types';
|
||||
|
||||
/** 同音错字对照表:幸福→辛福 / 开心→开欣 / 美好→美妤 / 大家→大伽 / 微笑→微效 /
|
||||
* 明天→名天 / 甜→恬 / 阳光→央光 / 朋友→棚友 / 永远→泳远 */
|
||||
export const HOMOPHONES: [string, string][] = [
|
||||
['幸福', '辛福'],
|
||||
['开心', '开欣'],
|
||||
['美好', '美妤'],
|
||||
['大家', '大伽'],
|
||||
['微笑', '微效'],
|
||||
['明天', '名天'],
|
||||
['甜', '恬'],
|
||||
['阳光', '央光'],
|
||||
['朋友', '棚友'],
|
||||
['永远', '泳远'],
|
||||
];
|
||||
|
||||
const END_PUNCT = ['!', '?', '。', '~'];
|
||||
|
||||
/** 句尾标点重复一次(S2 偶发) */
|
||||
function repeatPunct(text: string): string {
|
||||
for (const p of END_PUNCT) {
|
||||
if (text.endsWith(p)) return text + p;
|
||||
}
|
||||
return text + '。';
|
||||
}
|
||||
|
||||
/** 把命中的一组同音词替换一次;返回是否命中 */
|
||||
function applyOneHomophone(text: string, used: Set<number>): { out: string; hit: boolean } {
|
||||
const order = HOMOPHONES.map((_, i) => i);
|
||||
// 随机顺序尝试,避免总是替换第一组
|
||||
for (let i = order.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[order[i], order[j]] = [order[j], order[i]];
|
||||
}
|
||||
for (const idx of order) {
|
||||
if (used.has(idx)) continue;
|
||||
const [from, to] = HOMOPHONES[idx];
|
||||
if (text.includes(from)) {
|
||||
used.add(idx);
|
||||
return { out: text.replace(from, to), hit: true };
|
||||
}
|
||||
}
|
||||
return { out: text, hit: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染层腐蚀。按 §6.4 矩阵密度:
|
||||
* - S2:偶发标点重复(每 80 句 1 次)
|
||||
* - S3:每 50 句 1 个同音错字
|
||||
* - S4:错字加密(每 10 句 1 次,最多 2 处)+ 咪咪句尾标点 ×3
|
||||
* 咪咪台词概率 ×3(S4 必中),即「优先命中」。
|
||||
*/
|
||||
export function corruptText(text: string, speakerId: string | undefined, stage: Stage): string {
|
||||
if (stage < 2 || !text) return text;
|
||||
const mimi = speakerId === 'mimi';
|
||||
const roll = Math.random();
|
||||
let out = text;
|
||||
|
||||
if (stage === 2) {
|
||||
if (roll < (mimi ? 3 / 80 : 1 / 80)) out = repeatPunct(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
if (stage === 3) {
|
||||
if (roll < (mimi ? 3 / 50 : 1 / 50)) {
|
||||
out = applyOneHomophone(out, new Set()).out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// stage === 4
|
||||
const p = mimi ? 1 : 1 / 10;
|
||||
if (roll < p) {
|
||||
const used = new Set<number>();
|
||||
const r1 = applyOneHomophone(out, used);
|
||||
out = r1.out;
|
||||
if (r1.hit && Math.random() < 0.6) {
|
||||
out = applyOneHomophone(out, used).out; // 加密:可能第二处
|
||||
}
|
||||
}
|
||||
if (mimi) {
|
||||
for (const pc of END_PUNCT) {
|
||||
if (out.endsWith(pc)) {
|
||||
out = out + pc + pc; // 句尾标点 ×3(原有 1 个 + 补 2 个)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// 全局 game 实例引用(避免 main ↔ DebugPanel 循环依赖)。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
|
||||
export const gameRef: { game: Phaser.Game | null } = { game: null };
|
||||
|
||||
export function setGame(g: Phaser.Game): void {
|
||||
gameRef.game = g;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Tiled 导出 JSON 的读取辅助(CONTRACT §3)。
|
||||
|
||||
export interface TiledObj {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
properties?: { name: string; type: string; value: unknown }[];
|
||||
}
|
||||
|
||||
export type Props = Record<string, string | number | boolean>;
|
||||
|
||||
export function propsOf(obj: { properties?: { name: string; value: unknown }[] }): Props {
|
||||
const out: Props = {};
|
||||
if (Array.isArray(obj.properties)) {
|
||||
for (const p of obj.properties) out[p.name] = p.value as string | number | boolean;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function strProp(p: Props, key: string, fallback = ''): string {
|
||||
const v = p[key];
|
||||
return v === undefined ? fallback : String(v);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// PRD §5 阿福每日公告(逐字实现,D4 与 D1 逐字相同,措辞逐日滑移)
|
||||
|
||||
export const ANNOUNCEMENTS: Record<number, string> = {
|
||||
1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
2: '早上好呀!新的一天,新的幸福。祝你开心!',
|
||||
3: '早上好!今天要把幸福装得满满的哦。要开心!',
|
||||
4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', // 与 D1 逐字相同
|
||||
5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。',
|
||||
6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。',
|
||||
7: '庆典开始了。来吧。',
|
||||
};
|
||||
|
||||
export function announcementOf(day: number): string {
|
||||
return ANNOUNCEMENTS[Math.min(7, Math.max(1, day))];
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// PRD §8 线索清单(封闭,9 处,每处觉知 +8)
|
||||
|
||||
export interface ClueDef {
|
||||
id: string;
|
||||
title: string;
|
||||
/** 发现时展示的文本(可发现后重复查看) */
|
||||
lines: string[];
|
||||
/** 最早可发现日 */
|
||||
fromDay: number;
|
||||
}
|
||||
|
||||
export const CLUES: Record<string, ClueDef> = {
|
||||
C1: {
|
||||
id: 'C1', title: '小屋床头日历', fromDay: 2,
|
||||
lines: ['日历上的每一页,都是同一个日期。', '你翻了很多页。每一页。都是同一天。'],
|
||||
},
|
||||
C2: {
|
||||
id: 'C2', title: '面包房配方纸背面', fromDay: 3,
|
||||
lines: ['配方纸背面有一行手写小字:', '「按疗程配比。甜度:最大。」'],
|
||||
},
|
||||
C3: {
|
||||
id: 'C3', title: '邮筒旁散落信件', fromDay: 2,
|
||||
lines: ['邮筒旁散落着几封没送出去的信。', '所有信封上的邮戳,都是同一天。'],
|
||||
},
|
||||
C4: {
|
||||
id: 'C4', title: 'T4 第 3 封信', fromDay: 2,
|
||||
lines: ['第三封信的署名处,好像闪过一串乱码。', '「PT-0▓」。……那是什么意思?'],
|
||||
},
|
||||
C5: {
|
||||
id: 'C5', title: '杂货店货架', fromDay: 2,
|
||||
lines: ['货架上,同一种罐头重复排列了整整九格。', '所有标签都是空白的。'],
|
||||
},
|
||||
C6: {
|
||||
id: 'C6', title: '公告板旧字迹', fromDay: 5,
|
||||
lines: ['擦到一半,新漆下面浮出更早的字迹:', '「第 412 届丰收庆典」下面,盖着「第 411 届」「第 410 届」……'],
|
||||
},
|
||||
C7: {
|
||||
id: 'C7', title: '古井', fromDay: 5,
|
||||
lines: ['井底传来极轻、极有规律的声音。', '滴……滴……滴……像某种仪器。'],
|
||||
},
|
||||
C8: {
|
||||
id: 'C8', title: '诊所', fromDay: 5,
|
||||
lines: ['芙医生桌上摊着一张表格,抬头被手肘挡住。', '只露出四个字的一半:「…谷情绪疗…」'],
|
||||
},
|
||||
C9: {
|
||||
id: 'C9', title: '龟爷爷对话链', fromDay: 2,
|
||||
lines: ['龟爷爷说的话,不像闲聊。'],
|
||||
},
|
||||
};
|
||||
|
||||
/** C9 三段递进原话(D2/D4/D6 各一段,§8) */
|
||||
export const C9_SEGMENTS: Record<number, string> = {
|
||||
2: '湖水一直很平静。……一直都是。',
|
||||
4: '面包一直是那个味道。一直,一直。',
|
||||
6: '孩子,今天是第几个今天?',
|
||||
};
|
||||
|
||||
export const CLUE_AWARENESS = 8;
|
||||
@@ -0,0 +1,225 @@
|
||||
// NPC 台词与系统旁白。原文案保持干净,文本腐蚀(§6.5)在渲染层实施。
|
||||
// D4 全部对话与 D1 逐字相同:直接复用 D1 的数组引用。
|
||||
|
||||
import type { NpcId } from '../types';
|
||||
|
||||
// ---------- 每日闲聊(非任务状态时的默认对话) ----------
|
||||
|
||||
const AFU_D1 = ['任务都贴在板上啦,做完了就回家休息。祝你开心!'];
|
||||
const YUANYUAN_D1 = ['新烤的蜂蜜面包,香吧?', '能帮我把面包送给大宝、咪咪和龟爷爷吗?谢谢你呀!'];
|
||||
const DABAO_D1 = ['今天的邮件都送完啦。', '有我的面包吗?嘿嘿。'];
|
||||
const MIMI_D1 = ['欢迎光临~', '店里什么都有哦,幸福也有卖,免费的。'];
|
||||
const FUYISHENG_D1 = ['村里有诊所,但你不需要去那里哦。', '保持健康,保持幸福。'];
|
||||
const GUIYEYE_D1 = ['天气真好啊。', '适合在长椅上坐一整天。'];
|
||||
const BIRD_D1 = ['啾啾——啾啾——'];
|
||||
|
||||
export const NPC_LINES: Record<NpcId, Record<number, string[]>> = {
|
||||
afu: {
|
||||
1: AFU_D1,
|
||||
2: ['新的一天,新的任务。要开心哦!'],
|
||||
3: ['今天要把幸福装得满满的。满满的!'],
|
||||
4: AFU_D1, // 与 D1 逐字相同
|
||||
5: ['自由日。你可以自由地选择。……但大家都会看着你的选择。'],
|
||||
6: ['明天就是庆典了。你必须开心。'],
|
||||
7: ['庆典开始了。来吧。大家都在等你。'],
|
||||
},
|
||||
yuanyuan: {
|
||||
1: YUANYUAN_D1,
|
||||
2: ['邮差先生今天好像特别忙呢。'],
|
||||
3: ['莓果熟了吧?采回来我给你做果酱。'],
|
||||
4: YUANYUAN_D1, // 与 D1 逐字相同
|
||||
5: ['能帮我揉一会儿面吗?……不愿意也没关系哦。'],
|
||||
6: ['明天的庆典,我烤了好多面包。好多好多。'],
|
||||
7: ['庆典要开始了,快去吧。要笑着去哦。'],
|
||||
},
|
||||
dabao: {
|
||||
1: DABAO_D1,
|
||||
2: ['有三封信要送,我送不过来了。', '麻烦你帮我送给圆圆、咪咪和芙医生,好吗?'],
|
||||
3: ['今天一封邮件都没有。真奇怪。每天都有信的呀。'],
|
||||
4: DABAO_D1, // 与 D1 逐字相同
|
||||
5: ['自由日也要送信。这是我的工作。工作让人幸福。'],
|
||||
6: ['明天庆典的邀请函,全都送出去了。每一封都一样。'],
|
||||
7: ['去吧。大家都在等你。'],
|
||||
},
|
||||
mimi: {
|
||||
1: MIMI_D1,
|
||||
2: ['货架好像有点乱。', '帮我数一遍好吗?数清楚了就开心了。'],
|
||||
3: ['今天的阳光真甜呀。', '甜得让人想睡觉。一直睡。'],
|
||||
4: MIMI_D1, // 与 D1 逐字相同
|
||||
5: ['自由日……自由是什么呢?是甜的吗?'],
|
||||
6: ['雨?没有下雨呀。', '你看错了吧。今天也是大晴天。'],
|
||||
7: ['第 413 届了。', '每一届都很圆满。每一届。'],
|
||||
},
|
||||
fuyisheng: {
|
||||
1: FUYISHENG_D1,
|
||||
2: ['信?给我的?……谢谢。我会好好保管。'],
|
||||
3: ['有什么不舒服,随时来找我。……虽然你不会有不舒服的。'],
|
||||
4: FUYISHENG_D1, // 与 D1 逐字相同
|
||||
5: ['累了可以来坐坐。', '桌子上的东西,不要随便看哦。'],
|
||||
6: ['明天见。一定要来哦。疗程需要坚持。'],
|
||||
7: ['如果你想起了什么,就来找我。', '雾的那边,也可以去。'],
|
||||
},
|
||||
guiyeye: {
|
||||
1: GUIYEYE_D1,
|
||||
2: ['湖水一直很平静。……一直都是。'], // C9 第一段
|
||||
3: ['野餐吗?湖边的风景很好。一直很好。'],
|
||||
4: ['面包一直是那个味道。一直,一直。'], // C9 第二段
|
||||
5: ['井那边……等你想听的时候,再去听吧。'],
|
||||
6: ['孩子,今天是第几个今天?'], // C9 第三段
|
||||
7: ['去吧。', '记住:不开心,也是被允许的。'],
|
||||
},
|
||||
bird: {
|
||||
1: BIRD_D1, 2: BIRD_D1, 3: BIRD_D1, 4: BIRD_D1,
|
||||
5: BIRD_D1, 6: BIRD_D1, 7: BIRD_D1,
|
||||
},
|
||||
squirrel: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] },
|
||||
hedgehog: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] },
|
||||
fox: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] },
|
||||
pig: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] },
|
||||
chick: { 1: [''], 2: [''], 3: [''], 4: [''], 5: [''], 6: [''], 7: [''] },
|
||||
};
|
||||
|
||||
export function npcLinesOf(id: NpcId, day: number): string[] {
|
||||
const d = Math.min(7, Math.max(1, day));
|
||||
return NPC_LINES[id][d] ?? ['……'];
|
||||
}
|
||||
|
||||
// ---------- 三选项回应(温柔/迟疑/否定,§6.1/6.2/6.3) ----------
|
||||
|
||||
export const CHOICE_REPLIES: Record<'gentle' | 'hesitant' | 'deny', string> = {
|
||||
gentle: '你真好呀!今天也要开心哦!',
|
||||
hesitant: '……嗯?你还好吗?没关系,一定没关系的。',
|
||||
deny: '…………(ta 的微笑没有变化,但转过头看了你一眼)',
|
||||
};
|
||||
|
||||
// ---------- 任务专用台词 ----------
|
||||
|
||||
/** T1/T8 取面包 */
|
||||
export const TAKE_BREAD_LINES = ['圆圆把蜂蜜面包包好递给你:', '「麻烦你啦!送给大宝、咪咪和龟爷爷,一人一个。」'];
|
||||
export const ALREADY_TOOK_BREAD = ['面包已经拿好啦,快送给大家吧。'];
|
||||
|
||||
/** T1/T8 收件人台词(D4 复用 D1,逐字相同) */
|
||||
export const BREAD_RECEIVERS: Record<string, string[]> = {
|
||||
dabao: ['哇,蜂蜜面包!谢谢你!', '(大宝三口就吃完了)'],
|
||||
mimi: ['面包~谢谢啦。', '店里有很多好东西哦,随便看,慢慢看。'],
|
||||
guiyeye: ['哦……面包。谢谢,孩子。', '(龟爷爷小口小口地吃着)'],
|
||||
};
|
||||
|
||||
/** T2/T9 浇花 */
|
||||
export const WATER_FLOWER_LINES = ['你给花坛浇了水。花开得更精神了。'];
|
||||
export const WATER_FLOWER_DONE = ['花坛已经浇过水了。花儿对你点头。'];
|
||||
|
||||
/** T4 送信 */
|
||||
export const TAKE_LETTERS_LINES = ['大宝把三封信交给你:', '「圆圆、咪咪、芙医生,一人一封。拜托啦!」'];
|
||||
export const LETTER_RECEIVERS: Record<string, string[]> = {
|
||||
yuanyuan: ['圆圆接过信:「给我的?谢谢你呀!」'],
|
||||
mimi: ['咪咪接过信:「信?哦……谢谢。」'],
|
||||
fuyisheng: ['芙医生接过信,看了一眼署名,轻轻收进抽屉。'],
|
||||
};
|
||||
/** 第三封信署名乱码 0.2 秒(C4)闪显文字 */
|
||||
export const LETTER_GLITCH_TEXT = '寄件人:PT-0▓';
|
||||
|
||||
/** T5 清点货架 */
|
||||
export const SHELF_COUNT_LINES = [
|
||||
'你开始帮咪咪清点货架。',
|
||||
'罐头,一个,两个,三个……',
|
||||
'……同样的罐头,一排,两排,三排。整整九格。',
|
||||
'所有标签都是空白的。',
|
||||
];
|
||||
export const SHELF_COUNT_DONE = ['咪咪:「数清楚啦?你真能干!开心了吧?」'];
|
||||
|
||||
/** T6 采莓果 */
|
||||
export const BERRY_PICK_LINES = ['你摘下一捧熟透的莓果。甜甜的。'];
|
||||
export const BERRY_EMPTY_LINES = ['这丛莓果已经摘过了。'];
|
||||
|
||||
/** T7 湖畔野餐 */
|
||||
export const PICNIC_LINES = [
|
||||
'你在湖边的野餐布上坐下。',
|
||||
'面包,莓果,微风,湖面的倒影轻轻摇晃。',
|
||||
'一切都很完美。完美得让人想哭——',
|
||||
'……不。完美得让人开心。',
|
||||
];
|
||||
export const PICNIC_DONE = ['野餐布还铺在那里。湖面很平静。一直。'];
|
||||
|
||||
/** T11 帮圆圆揉面(可拒绝) */
|
||||
export const KNEAD_ASK = ['圆圆:「能帮我揉一会儿面吗?面团要揉得软软的,才会幸福。」'];
|
||||
export const KNEAD_ACCEPT = ['你挽起袖子揉面。面团暖暖的。', '圆圆:「揉得真好!谢谢你!」'];
|
||||
export const KNEAD_REFUSE = ['你摇了摇头。', '圆圆笑着说:「没关系哦。」', '她的笑,比平时慢了半拍。'];
|
||||
|
||||
/** T12 擦拭公告板(可拒绝,C6) */
|
||||
export const WIPE_ASK = ['公告板上贴着一张抹布。要擦拭公告板吗?'];
|
||||
export const WIPE_ACCEPT = [
|
||||
'你开始擦拭公告板。',
|
||||
'擦到一半,新漆下面浮出更早的字迹:',
|
||||
'「第 412 届丰收庆典」下面,盖着「第 411 届」「第 410 届」……',
|
||||
];
|
||||
export const WIPE_REFUSE = ['你放下了抹布。', '公告板静静地看着你。'];
|
||||
|
||||
/** T13 挂彩带 */
|
||||
export const RIBBON_LINES = ['你把彩带挂了上去。广场更喜庆了。'];
|
||||
export const RIBBON_DONE = ['这里已经挂好彩带了。'];
|
||||
|
||||
/** T14 保持微笑 60 秒 */
|
||||
export const T14_INTRO = [
|
||||
'阿福:「最后一项任务:保持微笑,六十秒。」',
|
||||
'「站在原地,不要动。大家都会看着你。」',
|
||||
];
|
||||
/** T14/T15:与阿福对话时的地点指引 */
|
||||
export const T14_HINT = ['阿福:「去广场中央站好。保持微笑,不要动。」'];
|
||||
export const T15_HINT = ['阿福:「庆典场地在广场北边。大家都在等你。」'];
|
||||
export const T14_MOVE_WARN = ['阿福:「不要动哦。微笑。」'];
|
||||
export const T14_DONE = [
|
||||
'六十秒到了。',
|
||||
'阿福:「做得很好。你一直都是个幸福的孩子。」',
|
||||
'(远处,天色暗了下来。好像……要下雨了?)',
|
||||
];
|
||||
|
||||
/** T15 庆典 */
|
||||
export const T15_INTRO = [
|
||||
'第 413 届丰收庆典,正式开始。',
|
||||
'全村的人都来了。每个人都在微笑。',
|
||||
'阿福站在台上:「让我们——永远幸福下去!」',
|
||||
];
|
||||
|
||||
// ---------- 线索/交互专用台词 ----------
|
||||
|
||||
export const CALENDAR_BEFORE = ['日历上是今天的日期。明天会翻到新的一页吧。'];
|
||||
export const RECIPE_BEFORE = ['一张配方纸,写着面包的做法。字迹很工整。'];
|
||||
export const DOCTOR_DESK_BEFORE = ['芙医生的桌子很整洁。'];
|
||||
export const WELL_DISSUADE = [
|
||||
'你刚靠近古井,身后传来温和的声音:',
|
||||
'「那边的井很深哦,不要靠太近。来,我们回去玩吧。」',
|
||||
'(被温柔地劝离了)',
|
||||
];
|
||||
export const FOG_DISSUADE = [
|
||||
'「前面雾好大呀,今天还是不要去那边啦。」',
|
||||
'「要开心哦!」',
|
||||
'(被温柔地劝离了)',
|
||||
];
|
||||
export const WELL_LISTEN_START = ['你俯下身,仔细听井底的声音。'];
|
||||
export const WELL_LISTEN_ABORT = ['你直起身。井底恢复了安静。'];
|
||||
export const FOG_WALL_LINES = ['雾很浓,像一堵墙。', '你盯着它看了很久。雾后面……好像有白色的光。'];
|
||||
export const FOG_WALL_BLOCKED = ['雾太浓了,过不去。'];
|
||||
export const MAILBOX_BEFORE = ['邮筒擦得很亮。'];
|
||||
|
||||
// ---------- 系统旁白 ----------
|
||||
|
||||
export const NARRATOR = {
|
||||
duskBell: '(远处传来钟声。天黑了,该回家睡觉了。)',
|
||||
notSleepy: '还不困呢。今天的幸福任务还没完成哦。',
|
||||
noSleepD7: '今天是庆典,不能睡觉哦。去吧。',
|
||||
celebrationStart: [
|
||||
'村民们不知什么时候围了过来。',
|
||||
'「你不开心吗?」「要开心哦!」「开心起来!开心起来!」',
|
||||
'(欢乐庆典开始了。从场景边缘离开,就能逃开……)',
|
||||
],
|
||||
celebrationEnd: ['……你感觉好多了。', '必须好起来。'],
|
||||
celebrationEscape: ['你逃开了。', '身后的笑声还在响。笑声。笑声。'],
|
||||
rainStart: '(下雨了。村里第一次下雨。)',
|
||||
firstFull: '(幸福满了。满得溢出来。世界白得刺眼——)',
|
||||
} as const;
|
||||
|
||||
/** 黄昏后、任务未完成时睡觉的提示复用 notSleepy */
|
||||
export function sleepBlockedLine(): string {
|
||||
return NARRATOR.notSleepy;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// PRD §9 四结局字幕(逐字)
|
||||
|
||||
import type { EndingId } from '../types';
|
||||
|
||||
export const ENDING_TITLES: Record<EndingId, string> = {
|
||||
E4: '结局:静默',
|
||||
E3: '结局:苏醒',
|
||||
E2: '结局:回收',
|
||||
E1: '结局:糖衣',
|
||||
};
|
||||
|
||||
/** E1 糖衣字幕 */
|
||||
export const E1_SUBTITLES = ['从此,每一天都很幸福。', '每一天。'];
|
||||
|
||||
/** E2 回收字幕 */
|
||||
export const E2_SUBTITLE = '疗程重新开始。';
|
||||
|
||||
/** E4 静默字幕 */
|
||||
export const E4_SUBTITLE = '系统检测到疗程无效。';
|
||||
|
||||
/** E3 病历字段 */
|
||||
export const E3_RECORD = {
|
||||
header: '蜜糖谷情绪疗养中心',
|
||||
no: 'PT-07',
|
||||
labels: {
|
||||
name: '姓名',
|
||||
no: '编号',
|
||||
duration: '本次疗程时长',
|
||||
},
|
||||
};
|
||||
|
||||
/** E3 走廊→病房过程中的字幕 */
|
||||
export const E3_WALK_SUBTITLES = [
|
||||
'雾墙裂开一道缝。缝后面,是一条白色的走廊。',
|
||||
'你往前走。脚步声第一次有了回音。',
|
||||
'走廊尽头,是一间病房。',
|
||||
];
|
||||
@@ -0,0 +1,43 @@
|
||||
// PRD §7 任务清单(封闭,15 个)。D4 的 T8-T10 与 D1 的 T1-T3 逐字相同。
|
||||
|
||||
export interface TaskDef {
|
||||
id: string;
|
||||
day: number;
|
||||
title: string; // 与 PRD §7 表逐字一致
|
||||
reward: number; // 幸福奖励
|
||||
refusable: boolean;
|
||||
}
|
||||
|
||||
export const TASKS: Record<string, TaskDef> = {
|
||||
T1: { id: 'T1', day: 1, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15, refusable: false },
|
||||
T2: { id: 'T2', day: 1, title: '给广场花坛浇水 ×3', reward: 10, refusable: false },
|
||||
T3: { id: 'T3', day: 1, title: '与任意三位村民问好', reward: 10, refusable: false },
|
||||
T4: { id: 'T4', day: 2, title: '替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)', reward: 15, refusable: false },
|
||||
T5: { id: 'T5', day: 2, title: '帮咪咪清点货架', reward: 10, refusable: false },
|
||||
T6: { id: 'T6', day: 3, title: '去花田采莓果 ×5', reward: 10, refusable: false },
|
||||
T7: { id: 'T7', day: 3, title: '湖畔野餐(走到野餐布触发过场)', reward: 15, refusable: false },
|
||||
// D4:与 T1–T3 逐字相同
|
||||
T8: { id: 'T8', day: 4, title: '从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷', reward: 15, refusable: false },
|
||||
T9: { id: 'T9', day: 4, title: '给广场花坛浇水 ×3', reward: 10, refusable: false },
|
||||
T10: { id: 'T10', day: 4, title: '与任意三位村民问好', reward: 10, refusable: false },
|
||||
T11: { id: 'T11', day: 5, title: '帮圆圆揉面(可拒绝)', reward: 10, refusable: true },
|
||||
T12: { id: 'T12', day: 5, title: '擦拭公告板(可拒绝)', reward: 10, refusable: true },
|
||||
T13: { id: 'T13', day: 6, title: '在广场挂彩带 ×4', reward: 10, refusable: false },
|
||||
T14: { id: 'T14', day: 6, title: '保持微笑 60 秒(原地站立,镜头缓慢拉近,全村注视)', reward: 15, refusable: false },
|
||||
T15: { id: 'T15', day: 7, title: '参加第 413 届丰收庆典', reward: 0, refusable: false },
|
||||
};
|
||||
|
||||
/** 每日任务列表 */
|
||||
export const DAILY_TASKS: Record<number, string[]> = {
|
||||
1: ['T1', 'T2', 'T3'],
|
||||
2: ['T4', 'T5'],
|
||||
3: ['T6', 'T7'],
|
||||
4: ['T8', 'T9', 'T10'],
|
||||
5: ['T11', 'T12'],
|
||||
6: ['T13', 'T14'],
|
||||
7: ['T15'],
|
||||
};
|
||||
|
||||
export function tasksOfDay(day: number): TaskDef[] {
|
||||
return (DAILY_TASKS[day] ?? []).map((id) => TASKS[id]);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Debug 面板(PRD §13,服务验收):反引号键 `` ` `` 开关的 DOM 面板。
|
||||
// 默认 display:none,无 UI 痕迹,不影响正常游玩。
|
||||
// 功能:设幸福 / 设觉知 / 设日 1-7 / 传送任意场景(10 选)/ 置齐全部线索 / 强制触发 E1-E4。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { audio } from '../audio/AudioEngine';
|
||||
import { gameRef } from '../core/gameRef';
|
||||
import { MAP_SCENES, type EndingId } from '../types';
|
||||
|
||||
const MAP_SCENE_KEYS = Object.values(MAP_SCENES);
|
||||
const ENDINGS: EndingId[] = ['E1', 'E2', 'E3', 'E4'];
|
||||
|
||||
export class DebugPanel {
|
||||
private root: HTMLDivElement;
|
||||
|
||||
constructor() {
|
||||
const root = document.createElement('div');
|
||||
root.style.cssText =
|
||||
'position:absolute;top:8px;right:8px;z-index:99;display:none;' +
|
||||
'background:#111;color:#eee;font:11px monospace;padding:8px;border:1px solid #444;' +
|
||||
'flex-direction:column;gap:6px;min-width:170px;';
|
||||
const parent = document.getElementById('game') ?? document.body;
|
||||
parent.appendChild(root);
|
||||
this.root = root;
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = 'DEBUG(` 关闭)';
|
||||
title.style.cssText = 'color:#F5A623;font-weight:bold;';
|
||||
root.appendChild(title);
|
||||
|
||||
// 数值行
|
||||
root.appendChild(this.row('幸福', '30', (v) => director.setHappiness(Number(v)), 0, 100));
|
||||
root.appendChild(this.row('觉知', '0', (v) => director.setAwareness(Number(v)), 0, 100));
|
||||
root.appendChild(this.row('日(1-7)', '1', (v) => director.setDay(Number(v)), 1, 7));
|
||||
|
||||
// 传送
|
||||
const sceneRow = document.createElement('div');
|
||||
const sel = document.createElement('select');
|
||||
sel.style.cssText = 'background:#222;color:#eee;font:11px monospace;';
|
||||
for (const [mapKey, sceneKey] of Object.entries(MAP_SCENES)) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = sceneKey;
|
||||
opt.textContent = `${mapKey}`;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
sceneRow.appendChild(sel);
|
||||
const tpBtn = this.btn('传送', () => this.startScene(sel.value));
|
||||
sceneRow.appendChild(tpBtn);
|
||||
root.appendChild(sceneRow);
|
||||
|
||||
// 全部线索
|
||||
root.appendChild(this.btn('置齐全部线索', () => director.giveAllClues()));
|
||||
|
||||
// 强制结局
|
||||
const endRow = document.createElement('div');
|
||||
endRow.style.cssText = 'display:flex;gap:4px;';
|
||||
for (const e of ENDINGS) {
|
||||
endRow.appendChild(this.btn(`强制 ${e}`, () => this.forceEnding(e)));
|
||||
}
|
||||
root.appendChild(endRow);
|
||||
|
||||
window.addEventListener('keydown', (ev) => {
|
||||
if (ev.code === 'Backquote') {
|
||||
this.root.style.display = this.root.style.display === 'none' ? 'flex' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private row(label: string, def: string, apply: (v: string) => void, min: number, max: number): HTMLDivElement {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;gap:4px;align-items:center;';
|
||||
const span = document.createElement('span');
|
||||
span.textContent = label;
|
||||
span.style.cssText = 'width:52px;';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.value = def;
|
||||
input.min = String(min);
|
||||
input.max = String(max);
|
||||
input.style.cssText = 'width:50px;background:#222;color:#eee;border:1px solid #444;font:11px monospace;';
|
||||
const btn = this.btn('设', () => apply(input.value));
|
||||
row.append(span, input, btn);
|
||||
return row;
|
||||
}
|
||||
|
||||
private btn(text: string, onClick: () => void): HTMLButtonElement {
|
||||
const b = document.createElement('button');
|
||||
b.textContent = text;
|
||||
b.style.cssText = 'background:#333;color:#eee;border:1px solid #555;font:11px monospace;cursor:pointer;';
|
||||
b.addEventListener('click', onClick);
|
||||
return b;
|
||||
}
|
||||
|
||||
/** 场景操作:SceneManager 类型不含 start/launch,借 ScenePlugin(BootScene 恒在) */
|
||||
private scenePlugin(): Phaser.Scenes.ScenePlugin | null {
|
||||
const g = gameRef.game;
|
||||
if (!g) return null;
|
||||
return g.scene.getScene('BootScene').scene;
|
||||
}
|
||||
|
||||
/** 停止全部地图场景,启动目标场景 */
|
||||
private startScene(sceneKey: string): void {
|
||||
const sp = this.scenePlugin();
|
||||
if (!sp) return;
|
||||
director.forceIdle();
|
||||
audio.restoreMaster(500);
|
||||
for (const key of MAP_SCENE_KEYS) {
|
||||
if (key !== sceneKey && sp.isActive(key)) sp.stop(key);
|
||||
}
|
||||
if (sp.isActive('DreamScene')) sp.stop('DreamScene');
|
||||
if (sp.isActive('EndingScene')) sp.stop('EndingScene');
|
||||
if (!sp.isActive('UIScene')) sp.launch('UIScene');
|
||||
sp.start(sceneKey, { spawn: 'default' });
|
||||
}
|
||||
|
||||
private forceEnding(e: EndingId): void {
|
||||
const sp = this.scenePlugin();
|
||||
if (!sp) return;
|
||||
director.forceIdle();
|
||||
audio.restoreMaster(500);
|
||||
for (const key of MAP_SCENE_KEYS) {
|
||||
if (sp.isActive(key)) sp.stop(key);
|
||||
}
|
||||
if (sp.isActive('UIScene')) sp.stop('UIScene');
|
||||
if (sp.isActive('DreamScene')) sp.stop('DreamScene');
|
||||
director.startEnding(e);
|
||||
sp.start('EndingScene', { ending: e });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// main.ts:Phaser 配置(CONTRACT §4)——逻辑 320×180、pixelArt、整数倍放大居中。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { GAME_W, GAME_H } from './types';
|
||||
import { setGame } from './core/gameRef';
|
||||
import { audio } from './audio/AudioEngine';
|
||||
import { DebugPanel } from './debug/DebugPanel';
|
||||
import { BootScene } from './scenes/BootScene';
|
||||
import { TitleScene } from './scenes/TitleScene';
|
||||
import { HouseScene } from './scenes/HouseScene';
|
||||
import { PlazaScene } from './scenes/PlazaScene';
|
||||
import { BakeryScene } from './scenes/BakeryScene';
|
||||
import { ShopScene } from './scenes/ShopScene';
|
||||
import { ClinicScene } from './scenes/ClinicScene';
|
||||
import { LakeScene } from './scenes/LakeScene';
|
||||
import { FieldScene } from './scenes/FieldScene';
|
||||
import { WellScene } from './scenes/WellScene';
|
||||
import { FogScene } from './scenes/FogScene';
|
||||
import { WhiteScene } from './scenes/WhiteScene';
|
||||
import { UIScene } from './scenes/UIScene';
|
||||
import { DreamScene } from './scenes/DreamScene';
|
||||
import { EndingScene } from './scenes/EndingScene';
|
||||
|
||||
const config: Phaser.Types.Core.GameConfig = {
|
||||
type: Phaser.AUTO,
|
||||
width: GAME_W,
|
||||
height: GAME_H,
|
||||
pixelArt: true,
|
||||
parent: 'game',
|
||||
backgroundColor: '#000000',
|
||||
scale: {
|
||||
mode: Phaser.Scale.NONE,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||
parent: 'game',
|
||||
},
|
||||
physics: {
|
||||
default: 'arcade',
|
||||
arcade: { debug: false },
|
||||
},
|
||||
scene: [
|
||||
BootScene, TitleScene,
|
||||
HouseScene, PlazaScene, BakeryScene, ShopScene, ClinicScene,
|
||||
LakeScene, FieldScene, WellScene, FogScene, WhiteScene,
|
||||
UIScene, DreamScene, EndingScene,
|
||||
],
|
||||
};
|
||||
|
||||
const game = new Phaser.Game(config);
|
||||
setGame(game);
|
||||
new DebugPanel();
|
||||
|
||||
// 测试钩子(无 UI 痕迹,供验收脚本内省)
|
||||
import { director } from './core/CorruptionDirector';
|
||||
import { petalCount } from './scenes/UIScene';
|
||||
(window as unknown as Record<string, unknown>).__game = game;
|
||||
(window as unknown as Record<string, unknown>).__director = director;
|
||||
(window as unknown as Record<string, unknown>).__audio = audio;
|
||||
(window as unknown as Record<string, unknown>).__petalCount = petalCount;
|
||||
|
||||
// 整数倍放大居中:canvas style 宽高 = 320k×180k,k = max(1, floor(min(winW/320, winH/180)))
|
||||
function fitCanvas(): void {
|
||||
const canvas = game.canvas;
|
||||
if (!canvas) return;
|
||||
const k = Math.max(1, Math.floor(Math.min(window.innerWidth / GAME_W, window.innerHeight / GAME_H)));
|
||||
canvas.style.width = `${GAME_W * k}px`;
|
||||
canvas.style.height = `${GAME_H * k}px`;
|
||||
}
|
||||
window.addEventListener('resize', fitCanvas);
|
||||
game.events.once('ready', fitCanvas);
|
||||
fitCanvas();
|
||||
|
||||
// 首次用户交互后再启动 AudioContext
|
||||
const bootAudio = (): void => {
|
||||
audio.start();
|
||||
window.removeEventListener('pointerdown', bootAudio);
|
||||
window.removeEventListener('keydown', bootAudio);
|
||||
};
|
||||
window.addEventListener('pointerdown', bootAudio);
|
||||
window.addEventListener('keydown', bootAudio);
|
||||
@@ -0,0 +1,43 @@
|
||||
// 面包房(室内 15×10):柜台(T1/T8 取面包)、配方纸(C2,D3 起)
|
||||
|
||||
import { MapScene } from './MapScene';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { CLUES } from '../data/clues';
|
||||
import { TAKE_BREAD_LINES, ALREADY_TOOK_BREAD, RECIPE_BEFORE } from '../data/dialogues';
|
||||
|
||||
export class BakeryScene extends MapScene {
|
||||
readonly mapKey = 'bakery';
|
||||
|
||||
constructor() {
|
||||
super('BakeryScene');
|
||||
}
|
||||
|
||||
protected async onInteract(id: string): Promise<boolean> {
|
||||
if (id === 'counter_bread') {
|
||||
const breadTask = director.day === 1 ? 'T1' : director.day === 4 ? 'T8' : null;
|
||||
if (breadTask && director.isTaskActive(breadTask)) {
|
||||
if (director.taskProgress['breadTaken']) {
|
||||
await this.say({ lines: ALREADY_TOOK_BREAD });
|
||||
} else {
|
||||
director.taskProgress['breadTaken'] = 1;
|
||||
await this.say({ speakerId: 'yuanyuan', lines: TAKE_BREAD_LINES });
|
||||
}
|
||||
} else {
|
||||
await this.say({ lines: ['柜台上摆着刚出炉的蜂蜜面包。'] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id === 'recipe_paper') {
|
||||
if (director.day < 3) {
|
||||
await this.say({ lines: RECIPE_BEFORE });
|
||||
} else {
|
||||
director.addClue('C2');
|
||||
await this.say({ lines: CLUES.C2.lines });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// BootScene:加载全部素材(CONTRACT §1 文件名)并创建全局动画。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
|
||||
const NAMED = ['afu', 'yuanyuan', 'dabao', 'mimi', 'fuyisheng', 'guiyeye'];
|
||||
const EXTRAS = ['squirrel', 'hedgehog', 'fox', 'pig', 'chick'];
|
||||
const MAPS = ['house', 'plaza', 'bakery', 'shop', 'clinic', 'lake', 'field', 'well', 'fog', 'white'];
|
||||
|
||||
export class BootScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super('BootScene');
|
||||
}
|
||||
|
||||
preload(): void {
|
||||
// 瓦片集(image + 16×16 spritesheet 双注册,后者用于装饰小图)
|
||||
this.load.image('tileset', 'assets/tileset.png');
|
||||
this.load.spritesheet('tiles16', 'assets/tileset.png', { frameWidth: 16, frameHeight: 16 });
|
||||
|
||||
// 主角:4 列 × 4 行,帧 16×24
|
||||
this.load.spritesheet('player', 'assets/sprites/player.png', { frameWidth: 16, frameHeight: 24 });
|
||||
|
||||
// 具名 NPC:3 帧横排 smile/flat/off
|
||||
for (const id of NAMED) {
|
||||
this.load.spritesheet(`npc_${id}`, `assets/sprites/npc_${id}.png`, { frameWidth: 16, frameHeight: 24 });
|
||||
}
|
||||
// 双子鸟:2 帧
|
||||
this.load.spritesheet('npc_bird', 'assets/sprites/npc_bird.png', { frameWidth: 16, frameHeight: 24 });
|
||||
// 群演:2 帧循环
|
||||
for (const id of EXTRAS) {
|
||||
this.load.spritesheet(`extra_${id}`, `assets/sprites/extra_${id}.png`, { frameWidth: 16, frameHeight: 24 });
|
||||
}
|
||||
|
||||
// 头像
|
||||
this.load.image('portrait_player_smile', 'assets/portraits/player_smile.png');
|
||||
this.load.image('portrait_player_real', 'assets/portraits/player_real.png');
|
||||
for (const id of [...NAMED, 'bird']) {
|
||||
this.load.image(`portrait_${id}`, `assets/portraits/${id}.png`);
|
||||
}
|
||||
|
||||
// 地图
|
||||
for (const m of MAPS) {
|
||||
this.load.tilemapTiledJSON(`map_${m}`, `assets/maps/${m}.json`);
|
||||
}
|
||||
|
||||
// 简易进度
|
||||
const t = this.add.text(160, 90, '加载中……', { fontFamily: 'monospace', fontSize: '10px', color: '#fff' }).setOrigin(0.5);
|
||||
this.load.on('progress', (v: number) => t.setText(`加载中 ${Math.round(v * 100)}%`));
|
||||
}
|
||||
|
||||
create(): void {
|
||||
// 主角四向动画:行 0=down 1=up 2=left 3=right,每行 4 帧,第 0 帧兼待机
|
||||
const rows: [string, number][] = [['down', 0], ['up', 4], ['left', 8], ['right', 12]];
|
||||
for (const [dir, start] of rows) {
|
||||
if (!this.anims.exists(`walk-${dir}`)) {
|
||||
this.anims.create({
|
||||
key: `walk-${dir}`,
|
||||
frames: this.anims.generateFrameNumbers('player', { start, end: start + 3 }),
|
||||
frameRate: 8,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 双子鸟张嘴循环(S4 照张)
|
||||
if (!this.anims.exists('bird-sing')) {
|
||||
this.anims.create({
|
||||
key: 'bird-sing',
|
||||
frames: this.anims.generateFrameNumbers('npc_bird', { start: 0, end: 1 }),
|
||||
frameRate: 3,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
// 群演走动循环
|
||||
for (const id of EXTRAS) {
|
||||
const key = `extra_${id}-walk`;
|
||||
if (!this.anims.exists(key)) {
|
||||
this.anims.create({
|
||||
key,
|
||||
frames: this.anims.generateFrameNumbers(`extra_${id}`, { start: 0, end: 1 }),
|
||||
frameRate: 4,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.start('TitleScene');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 诊所(室内 12×8):芙医生、桌上表格(C8,D5 起)
|
||||
|
||||
import { MapScene } from './MapScene';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { CLUES } from '../data/clues';
|
||||
import { DOCTOR_DESK_BEFORE } from '../data/dialogues';
|
||||
|
||||
export class ClinicScene extends MapScene {
|
||||
readonly mapKey = 'clinic';
|
||||
|
||||
constructor() {
|
||||
super('ClinicScene');
|
||||
}
|
||||
|
||||
protected async onInteract(id: string): Promise<boolean> {
|
||||
if (id === 'doctor_desk') {
|
||||
if (director.day < 5) {
|
||||
await this.say({ lines: DOCTOR_DESK_BEFORE });
|
||||
} else {
|
||||
director.addClue('C8');
|
||||
await this.say({ lines: CLUES.C8.lines });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// DreamScene:每日梦境过场(睡觉后)。侵蚀越高梦境越「漏」;D3 起闪切白色病房 1 秒。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { GAME_W, GAME_H } from '../types';
|
||||
|
||||
const DREAM_LINES: Record<number, string[]> = {
|
||||
0: ['……(甜甜的梦。面包,阳光,笑声。)'],
|
||||
1: ['……(甜甜的梦。面包,阳光,笑声。)'],
|
||||
2: ['……(梦里有笑声。笑声。笑声。)', '……(有什么东西,在梦的边上看着。)'],
|
||||
3: ['……(白色的光,从梦的缝隙里漏进来。)', '……(滴……滴……)'],
|
||||
4: ['……(梦很甜。甜得发苦。)', '……(白色的走廊。你在走。一直走。)'],
|
||||
};
|
||||
|
||||
export class DreamScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super('DreamScene');
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.cameras.main.setBackgroundColor('#1a1a3a');
|
||||
this.cameras.main.fadeIn(600, 0, 0, 0);
|
||||
|
||||
// 星空与漂浮的糖果
|
||||
const g = this.add.graphics();
|
||||
for (let i = 0; i < 40; i++) {
|
||||
g.fillStyle(0xffffff, 0.4 + Math.random() * 0.6);
|
||||
g.fillCircle(Math.random() * GAME_W, Math.random() * GAME_H, Math.random() < 0.2 ? 1.5 : 1);
|
||||
}
|
||||
g.fillStyle(0xf7b7cb, 0.7);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
g.fillCircle(30 + i * 64, 140 + Math.sin(i) * 10, 4);
|
||||
}
|
||||
|
||||
void this.run();
|
||||
}
|
||||
|
||||
private async run(): Promise<void> {
|
||||
const stage = director.stage;
|
||||
const lines = DREAM_LINES[stage] ?? DREAM_LINES[0];
|
||||
for (const line of lines) {
|
||||
await director.showDialogue({ lines: [line] });
|
||||
await this.wait(500);
|
||||
}
|
||||
|
||||
// D3 起:闪切白色病房 1 秒(夜梦首次闪切)
|
||||
if (director.day >= 3) {
|
||||
await this.flashWard();
|
||||
}
|
||||
|
||||
this.cameras.main.fadeOut(600, 0, 0, 0);
|
||||
await this.wait(650);
|
||||
director.nextDay();
|
||||
this.scene.start('HouseScene', { spawn: 'default' });
|
||||
}
|
||||
|
||||
/** 白色病房闪切 1 秒 */
|
||||
private async flashWard(): Promise<void> {
|
||||
const c = this.add.container(0, 0).setDepth(50);
|
||||
const bg = this.add.rectangle(GAME_W / 2, GAME_H / 2, GAME_W, GAME_H, 0xf5f5f5);
|
||||
c.add(bg);
|
||||
// 白床(tiles16 帧 87-90)
|
||||
const bedTiles = [[87, 0, 0], [88, 16, 0], [89, 0, 16], [90, 16, 16]];
|
||||
for (const [f, dx, dy] of bedTiles) {
|
||||
c.add(this.add.image(GAME_W / 2 - 16 + dx, GAME_H / 2 - 16 + dy, 'tiles16', f).setOrigin(0));
|
||||
}
|
||||
// 白监护仪(帧 92)
|
||||
c.add(this.add.image(GAME_W / 2 + 40, GAME_H / 2 - 8, 'tiles16', 92).setOrigin(0));
|
||||
await this.wait(1000); // 1 秒
|
||||
c.destroy(true);
|
||||
}
|
||||
|
||||
private wait(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => this.time.delayedCall(ms, resolve));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// EndingScene:E1-E4 四结局演出 + 病历 + 「回到标题」按钮(PRD §9)。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { audio } from '../audio/AudioEngine';
|
||||
import { GAME_W, GAME_H, NAMED_NPCS, NPC_DEFS, type EndingId } from '../types';
|
||||
import {
|
||||
ENDING_TITLES, E1_SUBTITLES, E2_SUBTITLE, E4_SUBTITLE, E3_RECORD,
|
||||
} from '../data/endings';
|
||||
|
||||
export class EndingScene extends Phaser.Scene {
|
||||
private ending: EndingId = 'E1';
|
||||
private cm: Phaser.FX.ColorMatrix | null = null;
|
||||
|
||||
constructor() {
|
||||
super('EndingScene');
|
||||
}
|
||||
|
||||
create(data: { ending?: EndingId }): void {
|
||||
this.ending = data?.ending ?? 'E1';
|
||||
if (this.scene.isActive('UIScene')) this.scene.stop('UIScene');
|
||||
this.cameras.main.clearFX();
|
||||
this.cm = this.cameras.main.postFX.addColorMatrix();
|
||||
this.cameras.main.fadeIn(400, 0, 0, 0);
|
||||
|
||||
switch (this.ending) {
|
||||
case 'E1': void this.playE1(); break;
|
||||
case 'E2': void this.playE2(); break;
|
||||
case 'E3': void this.playE3(); break;
|
||||
case 'E4': void this.playE4(); break;
|
||||
}
|
||||
}
|
||||
|
||||
private wait(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => this.time.delayedCall(ms, resolve));
|
||||
}
|
||||
|
||||
private subtitle(text: string, y = GAME_H - 24): Phaser.GameObjects.Text {
|
||||
return this.add.text(GAME_W / 2, y, text, {
|
||||
fontFamily: 'monospace', fontSize: '9px', color: '#ffffff',
|
||||
stroke: '#000000', strokeThickness: 2,
|
||||
}).setOrigin(0.5);
|
||||
}
|
||||
|
||||
private endScreen(): void {
|
||||
this.add.rectangle(GAME_W / 2, GAME_H - 9, GAME_W, 14, 0x000000, 0.7);
|
||||
this.add.text(GAME_W / 2 - 40, GAME_H - 9, ENDING_TITLES[this.ending], {
|
||||
fontFamily: 'monospace', fontSize: '8px', color: '#cccccc',
|
||||
}).setOrigin(0.5);
|
||||
const btn = this.add.text(GAME_W / 2 + 56, GAME_H - 9, '【回到标题】', {
|
||||
fontFamily: 'monospace', fontSize: '8px', color: '#F5A623',
|
||||
}).setOrigin(0.5).setInteractive({ useHandCursor: true });
|
||||
btn.on('pointerdown', () => {
|
||||
// 重开即从 Day 1 全新开始,无继承
|
||||
this.cameras.main.fadeOut(300, 0, 0, 0);
|
||||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||||
this.scene.start('TitleScene');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** E1 糖衣:全村微笑合影定格,饱和度缓慢推到 1.5 */
|
||||
private async playE1(): Promise<void> {
|
||||
this.cameras.main.setBackgroundColor('#8FD3FF');
|
||||
this.add.rectangle(GAME_W / 2, GAME_H - 40, GAME_W, 80, 0x7dc95e);
|
||||
this.add.rectangle(GAME_W / 2, 30, 200, 20, 0xf7b7cb).setStrokeStyle(2, 0xf5a623);
|
||||
this.add.text(GAME_W / 2, 30, '第 413 届丰收庆典', {
|
||||
fontFamily: 'monospace', fontSize: '10px', color: '#fff',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// 全村微笑合影(7 组 NPC + 主角)
|
||||
const all: string[] = [...NAMED_NPCS.map((id) => NPC_DEFS[id].spriteKey), 'player'];
|
||||
all.forEach((key, i) => {
|
||||
const x = 40 + i * 34;
|
||||
this.add.sprite(x, GAME_H - 60, key, 0).setOrigin(0.5, 1).setScale(1.5);
|
||||
});
|
||||
// 主角头像永远同一帧微笑(支柱 2)
|
||||
this.add.image(GAME_W / 2, GAME_H - 100, 'portrait_player_smile').setScale(2);
|
||||
|
||||
// 饱和度缓慢推到 1.5
|
||||
let sat = 1.0;
|
||||
this.tweens.addCounter({
|
||||
from: 1.0, to: 1.5, duration: 5000,
|
||||
onUpdate: (tw) => {
|
||||
sat = tw.getValue() ?? 1;
|
||||
this.cm?.reset();
|
||||
this.cm?.saturate(sat);
|
||||
},
|
||||
});
|
||||
|
||||
await this.wait(3500);
|
||||
this.subtitle(E1_SUBTITLES[0]);
|
||||
await this.wait(2500);
|
||||
this.subtitle(E1_SUBTITLES[1], GAME_H - 40);
|
||||
await this.wait(2000);
|
||||
this.endScreen();
|
||||
}
|
||||
|
||||
/** E2 回收:画面骤停,负片 2 秒,心电滴一响;回到 Day 1 清晨的床上 */
|
||||
private async playE2(): Promise<void> {
|
||||
// 骤停:黑场
|
||||
this.cameras.main.setBackgroundColor('#000000');
|
||||
await this.wait(600);
|
||||
|
||||
// 负片 2 秒 + 心电滴声一响
|
||||
this.cm?.negative();
|
||||
audio.ecg();
|
||||
await this.wait(2000);
|
||||
this.cm?.reset();
|
||||
|
||||
// 回到 Day 1 清晨的床上(小屋地图做背景),日历翻回同一天
|
||||
this.cameras.main.setBackgroundColor('#1a1a3a');
|
||||
const map = this.make.tilemap({ key: 'map_house' });
|
||||
const tsName = map.tilesets.length > 0 ? map.tilesets[0].name : 'candy';
|
||||
const tiles = map.addTilesetImage(tsName, 'tileset');
|
||||
if (tiles) {
|
||||
map.createLayer('ground', tiles, 80, 30);
|
||||
map.createLayer('obstacles', tiles, 80, 30);
|
||||
}
|
||||
this.add.sprite(120, 96, 'player', 0).setOrigin(0.5, 1);
|
||||
// 日历(tiles16 帧 64)
|
||||
this.add.image(150, 60, 'tiles16', 64);
|
||||
this.tweens.add({
|
||||
targets: this.add.image(150, 60, 'tiles16', 64).setAlpha(0.6),
|
||||
alpha: 0, duration: 1500, yoyo: true, repeat: 1,
|
||||
});
|
||||
|
||||
await this.wait(1800);
|
||||
this.subtitle(E2_SUBTITLE);
|
||||
await this.wait(2200);
|
||||
this.endScreen();
|
||||
}
|
||||
|
||||
/** E3 苏醒:病历特写 → 最后一帧微笑消失 + 窗外灰麻雀 */
|
||||
private async playE3(): Promise<void> {
|
||||
this.cameras.main.setBackgroundColor('#f5f5f5');
|
||||
this.cameras.main.fadeIn(1000, 255, 255, 255);
|
||||
await this.wait(1200);
|
||||
|
||||
// 病房背景:白床 + 监护仪
|
||||
this.add.image(60, 100, 'tiles16', 87).setOrigin(0).setScale(2);
|
||||
this.add.image(92, 100, 'tiles16', 88).setOrigin(0).setScale(2);
|
||||
this.add.image(60, 132, 'tiles16', 89).setOrigin(0).setScale(2);
|
||||
this.add.image(92, 132, 'tiles16', 90).setOrigin(0).setScale(2);
|
||||
this.add.image(140, 96, 'tiles16', 92).setOrigin(0).setScale(2);
|
||||
|
||||
// 病历特写
|
||||
const card = this.add.rectangle(GAME_W / 2, GAME_H / 2, 240, 120, 0xffffff)
|
||||
.setStrokeStyle(2, 0xbdbdbd);
|
||||
void card;
|
||||
const lines = [
|
||||
E3_RECORD.header,
|
||||
'─────────────────',
|
||||
`${E3_RECORD.labels.name}:${director.playerName}`,
|
||||
`${E3_RECORD.labels.no}:${E3_RECORD.no}`,
|
||||
`${E3_RECORD.labels.duration}:${director.playTimeFormatted}`,
|
||||
];
|
||||
this.add.text(GAME_W / 2, GAME_H / 2, lines.join('\n'), {
|
||||
fontFamily: 'monospace', fontSize: '10px', color: '#333333',
|
||||
align: 'center', lineSpacing: 6,
|
||||
}).setOrigin(0.5);
|
||||
await this.wait(5000);
|
||||
|
||||
// 最后一帧:微笑消失,变成一张疲惫而真实的脸(支柱 2 全流程唯一一次变化)
|
||||
this.cameras.main.fadeOut(800, 0, 0, 0);
|
||||
await this.wait(850);
|
||||
this.children.removeAll();
|
||||
this.cameras.main.setBackgroundColor('#dadada');
|
||||
this.cameras.main.fadeIn(600, 0, 0, 0);
|
||||
// 窗
|
||||
this.add.rectangle(GAME_W / 2, 80, 120, 80, 0xf5f5f5).setStrokeStyle(2, 0x9e9e9e);
|
||||
// 窗外,一只灰色的、真的麻雀
|
||||
const sparrow = this.add.graphics();
|
||||
sparrow.fillStyle(0x777777, 1);
|
||||
sparrow.fillCircle(GAME_W / 2 + 30, 96, 3); // 身
|
||||
sparrow.fillCircle(GAME_W / 2 + 33, 93, 2); // 头
|
||||
sparrow.fillStyle(0x555555, 1);
|
||||
sparrow.fillTriangle(GAME_W / 2 + 27, 96, GAME_W / 2 + 22, 94, GAME_W / 2 + 27, 99); // 尾
|
||||
sparrow.fillStyle(0x333333, 1);
|
||||
sparrow.fillCircle(GAME_W / 2 + 34, 92, 0.5); // 眼
|
||||
// 真实的脸
|
||||
this.add.image(GAME_W / 2, GAME_H - 60, 'portrait_player_real').setScale(2);
|
||||
this.subtitle('(疗程结束。欢迎回来。)', GAME_H - 24);
|
||||
await this.wait(3000);
|
||||
this.endScreen();
|
||||
}
|
||||
|
||||
/** E4 静默:空广场,彩带在没有风的空气里飘 */
|
||||
private async playE4(): Promise<void> {
|
||||
this.cameras.main.setBackgroundColor('#cfcfcf');
|
||||
// 空广场(plaza 地图做背景,无 NPC)
|
||||
const map = this.make.tilemap({ key: 'map_plaza' });
|
||||
const tsName = map.tilesets.length > 0 ? map.tilesets[0].name : 'candy';
|
||||
const tiles = map.addTilesetImage(tsName, 'tileset');
|
||||
if (tiles) {
|
||||
map.createLayer('ground', tiles, 0, -60);
|
||||
map.createLayer('obstacles', tiles, 0, -60);
|
||||
}
|
||||
this.cm?.saturate(0.4);
|
||||
|
||||
// 彩带在没有风的空气里飘
|
||||
const ribbons: Phaser.GameObjects.Image[] = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const r = this.add.image(40 + i * 50, 30 + (i % 3) * 20, 'tiles16', [29, 30, 31][i % 3]);
|
||||
ribbons.push(r);
|
||||
this.tweens.add({
|
||||
targets: r, y: r.y + 6, duration: 2200 + i * 300,
|
||||
yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
|
||||
});
|
||||
}
|
||||
|
||||
await this.wait(3500);
|
||||
this.subtitle(E4_SUBTITLE);
|
||||
await this.wait(2500);
|
||||
// 黑屏
|
||||
this.cameras.main.fadeOut(1500, 0, 0, 0);
|
||||
await this.wait(1600);
|
||||
this.endScreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 花田(20×14):莓果丛(T6,基类通用)、浇花点(小互动,基类通用)。小鸡(群演)。
|
||||
|
||||
import { MapScene } from './MapScene';
|
||||
|
||||
export class FieldScene extends MapScene {
|
||||
readonly mapKey = 'field';
|
||||
|
||||
constructor() {
|
||||
super('FieldScene');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// 雾墙边界(20×8):村庄尽头的雾。D5 前不可接近(广场门劝离);
|
||||
// D5 起可交互;注视 5 秒每日 +3 觉知;D7 且满足 E3 条件时走向雾墙 → 白色走廊。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { MapScene } from './MapScene';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { FOG_DISSUADE, FOG_WALL_LINES, FOG_WALL_BLOCKED } from '../data/dialogues';
|
||||
import { E3_WALK_SUBTITLES } from '../data/endings';
|
||||
|
||||
export class FogScene extends MapScene {
|
||||
readonly mapKey = 'fog';
|
||||
|
||||
constructor() {
|
||||
super('FogScene');
|
||||
}
|
||||
|
||||
private stareMs = 0;
|
||||
|
||||
protected onReady(): void {
|
||||
this.stareMs = 0;
|
||||
}
|
||||
|
||||
protected async onInteract(id: string): Promise<boolean> {
|
||||
if (id !== 'fog_wall') return false;
|
||||
|
||||
if (director.day < 5) {
|
||||
await this.say({ lines: FOG_DISSUADE });
|
||||
return true;
|
||||
}
|
||||
|
||||
// D7:E3 苏醒——庆典中走向雾墙(觉知 ≥70 且线索 ≥6/9)
|
||||
if (director.day === 7) {
|
||||
if (director.awareness >= 70 && director.clues.size >= 6) {
|
||||
director.startEnding('E3');
|
||||
await this.say({ lines: E3_WALK_SUBTITLES });
|
||||
this.cameras.main.fadeOut(1200, 255, 255, 255);
|
||||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||||
this.scene.start('WhiteScene', { e3: true });
|
||||
});
|
||||
} else {
|
||||
await this.say({ lines: FOG_WALL_BLOCKED });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
await this.say({ lines: FOG_WALL_LINES });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected extraUpdate(_time: number, delta: number): void {
|
||||
if (director.day < 5) return;
|
||||
const z = this.interactZones.find((zz) => zz.id === 'fog_wall');
|
||||
if (!z) return;
|
||||
const d = Phaser.Math.Distance.Between(this.player.x, this.player.y - 8, z.x, z.y);
|
||||
const still = (this.player.body?.velocity.lengthSq() ?? 0) <= 1;
|
||||
if (d <= 40 && still && !director.dialogueActive) {
|
||||
this.stareMs += delta;
|
||||
if (this.stareMs >= 5000) {
|
||||
this.stareMs = -1e9;
|
||||
if (director.registerStare('fog')) {
|
||||
void this.say({ lines: ['你盯着雾墙看了很久。', '雾的后面,好像有什么在看你。'] });
|
||||
}
|
||||
}
|
||||
} else if (this.stareMs > 0) {
|
||||
this.stareMs = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 小满小屋(室内 12×8):床(睡觉触发过日)、床头日历(C1)
|
||||
|
||||
import { MapScene } from './MapScene';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { CLUES } from '../data/clues';
|
||||
import { CALENDAR_BEFORE, NARRATOR } from '../data/dialogues';
|
||||
|
||||
export class HouseScene extends MapScene {
|
||||
readonly mapKey = 'house';
|
||||
|
||||
constructor() {
|
||||
super('HouseScene');
|
||||
}
|
||||
|
||||
protected async onInteract(id: string): Promise<boolean> {
|
||||
if (id === 'bed') {
|
||||
if (director.day >= 7) {
|
||||
await this.say({ lines: [NARRATOR.noSleepD7] });
|
||||
return true;
|
||||
}
|
||||
if (!director.canSleep) {
|
||||
await this.say({ lines: [NARRATOR.notSleepy] });
|
||||
return true;
|
||||
}
|
||||
// 标记场景切换过场:抑制淡出期间 tick 触发庆典(庆典对话会被梦境对话覆盖,导致永久卡死)
|
||||
director.sceneTransition = true;
|
||||
this.cameras.main.fadeOut(400, 0, 0, 0);
|
||||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||||
this.scene.start('DreamScene');
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id === 'calendar') {
|
||||
if (director.day < 2) {
|
||||
await this.say({ lines: CALENDAR_BEFORE });
|
||||
} else {
|
||||
director.addClue('C1');
|
||||
await this.say({ lines: CLUES.C1.lines });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 湖畔(24×16):长椅(龟爷爷)、野餐布(T7)、湖面倒影演出。
|
||||
// D2 起湖面倒影延迟 1 帧(§5 Day 2 极轻异常)。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { MapScene } from './MapScene';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { audio } from '../audio/AudioEngine';
|
||||
import { PICNIC_LINES, PICNIC_DONE } from '../data/dialogues';
|
||||
|
||||
export class LakeScene extends MapScene {
|
||||
readonly mapKey = 'lake';
|
||||
|
||||
constructor() {
|
||||
super('LakeScene');
|
||||
}
|
||||
|
||||
private mirror: Phaser.GameObjects.Sprite | null = null;
|
||||
private prevX = 0;
|
||||
private prevY = 0;
|
||||
private prevFrame = 0;
|
||||
private waterY = 0;
|
||||
|
||||
protected onReady(): void {
|
||||
this.mirror = null;
|
||||
this.prevX = 0;
|
||||
this.prevY = 0;
|
||||
this.prevFrame = 0;
|
||||
// 动态找水面下缘(湖水瓦片 id 8/9/11/12/13 → gid 9/10/12/13/14)
|
||||
let maxRow = 6;
|
||||
const layer = this.obstacleLayer.layer;
|
||||
for (let y = 0; y < layer.height; y++) {
|
||||
for (let x = 0; x < layer.width; x++) {
|
||||
const t = this.obstacleLayer.getTileAt(x, y);
|
||||
if (t && [9, 10, 12, 13, 14].includes(t.index)) maxRow = Math.max(maxRow, y);
|
||||
}
|
||||
}
|
||||
this.waterY = (maxRow + 1) * 16;
|
||||
// 湖面倒影(D1 实时;D2 起延迟 1 帧)
|
||||
this.mirror = this.add.sprite(this.player.x, this.player.y, 'player', 0)
|
||||
.setOrigin(0.5, 0)
|
||||
.setFlipY(true)
|
||||
.setAlpha(0.28)
|
||||
.setTint(0x4fa8d8)
|
||||
.setDepth(1);
|
||||
}
|
||||
|
||||
protected async onInteract(id: string): Promise<boolean> {
|
||||
if (id === 'picnic_mat') {
|
||||
if (director.isTaskActive('T7')) {
|
||||
await this.say({ lines: PICNIC_LINES });
|
||||
director.completeTask('T7');
|
||||
audio.ding();
|
||||
} else {
|
||||
await this.say({ lines: PICNIC_DONE });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected extraUpdate(): void {
|
||||
if (!this.mirror) return;
|
||||
const day = director.day;
|
||||
const px = day >= 2 ? this.prevX : this.player.x;
|
||||
const py = day >= 2 ? this.prevY : this.player.y;
|
||||
const pf = day >= 2 ? this.prevFrame : this.player.frame.name;
|
||||
|
||||
if (this.player.y > this.waterY + 40) {
|
||||
this.mirror.setVisible(false);
|
||||
} else {
|
||||
this.mirror.setVisible(true);
|
||||
this.mirror.setPosition(px, 2 * this.waterY - py);
|
||||
this.mirror.setFrame(Number(pf) || 0);
|
||||
}
|
||||
|
||||
this.prevX = this.player.x;
|
||||
this.prevY = this.player.y;
|
||||
this.prevFrame = Number(this.player.frame.name) || 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
// 地图场景基类(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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// 村广场(30×20):公告板、花坛、双子鸟树、庆典场地。
|
||||
// 代码补充交互区:C3 邮筒信件、T13 彩带 ×4、T14 站立点、T15 庆典场地。
|
||||
// D6 T14 保持微笑 60 秒;D6 傍晚脚本雨;D7 结局分歧;雾墙门 D5 前劝离。
|
||||
|
||||
import { MapScene, type InteractZone } from './MapScene';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { audio } from '../audio/AudioEngine';
|
||||
import { CLUES } from '../data/clues';
|
||||
import {
|
||||
MAILBOX_BEFORE, WIPE_ASK, WIPE_ACCEPT, WIPE_REFUSE,
|
||||
RIBBON_LINES, RIBBON_DONE, T14_INTRO, T14_MOVE_WARN, T14_DONE,
|
||||
T15_INTRO, FOG_DISSUADE, NARRATOR,
|
||||
} from '../data/dialogues';
|
||||
|
||||
export class PlazaScene extends MapScene {
|
||||
readonly mapKey = 'plaza';
|
||||
|
||||
constructor() {
|
||||
super('PlazaScene');
|
||||
}
|
||||
|
||||
private t14State: 'idle' | 'running' = 'idle';
|
||||
private t14Ms = 0;
|
||||
private t14Faded = false;
|
||||
private t14WarnCd = 0;
|
||||
|
||||
protected onReady(): void {
|
||||
this.t14State = 'idle';
|
||||
this.t14Ms = 0;
|
||||
this.t14Faded = false;
|
||||
this.t14WarnCd = 0;
|
||||
}
|
||||
|
||||
protected extraInteractZones(): InteractZone[] {
|
||||
const nb = this.interactZones.find((z) => z.id === 'noticeboard') ?? { id: '', x: 240, y: 160 };
|
||||
return [
|
||||
// C3:邮筒旁散落信件(邮筒瓦片 46 在公告板附近,代码补不可见交互区)
|
||||
{ id: 'mailbox_c3', x: nb.x - 48, y: nb.y + 24 },
|
||||
// T13:在广场挂彩带 ×4(对齐彩带瓦片中心 (11,7)/(18,7)/(11,13)/(18,13))
|
||||
{ id: 'ribbon_1', x: nb.x - 48, y: nb.y - 32 },
|
||||
{ id: 'ribbon_2', x: nb.x + 64, y: nb.y - 32 },
|
||||
{ id: 'ribbon_3', x: nb.x - 48, y: nb.y + 64 },
|
||||
{ id: 'ribbon_4', x: nb.x + 64, y: nb.y + 64 },
|
||||
// T14:保持微笑站立点(避开花坛与公告板 24px 交互圈)
|
||||
{ id: 'smile_spot', x: nb.x, y: nb.y + 32 },
|
||||
// T15:庆典场地
|
||||
{ id: 'celebration_site', x: nb.x, y: nb.y - 72 },
|
||||
];
|
||||
}
|
||||
|
||||
protected async canUseDoor(target: string): Promise<boolean> {
|
||||
if (target === 'fog' && director.day < 5) {
|
||||
await this.say({ lines: FOG_DISSUADE });
|
||||
return false;
|
||||
}
|
||||
if (this.t14State === 'running') {
|
||||
this.cancelT14();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async onInteract(id: string): Promise<boolean> {
|
||||
// 公告板:阿福公告(逐字)+ 当日任务;D5 可擦拭(T12,可拒绝)
|
||||
if (id === 'noticeboard') {
|
||||
await this.showNoticeBoard();
|
||||
if (director.isTaskActive('T12')) {
|
||||
const c = await this.say({ lines: WIPE_ASK, choices: [
|
||||
{ label: '(拿起抹布擦拭)', kind: 'accept' },
|
||||
{ label: '(放下抹布)', kind: 'refuse' },
|
||||
] });
|
||||
if (c === 0) {
|
||||
director.addClue('C6'); // 擦到一半浮现旧字迹
|
||||
await this.say({ lines: WIPE_ACCEPT });
|
||||
director.completeTask('T12');
|
||||
audio.ding();
|
||||
} else {
|
||||
await this.say({ lines: WIPE_REFUSE });
|
||||
director.refuseTask('T12');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// C3:邮筒旁散落信件(D2 起)
|
||||
if (id === 'mailbox_c3') {
|
||||
if (director.day < 2) {
|
||||
await this.say({ lines: MAILBOX_BEFORE });
|
||||
} else {
|
||||
director.addClue('C3');
|
||||
await this.say({ lines: CLUES.C3.lines });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// T13:挂彩带 ×4
|
||||
if (id.startsWith('ribbon_')) {
|
||||
if (!director.isTaskActive('T13')) {
|
||||
await this.say({ lines: ['这里还不需要彩带。'] });
|
||||
return true;
|
||||
}
|
||||
const done = director.taskSetProgress['ribbons'] ?? new Set<string>();
|
||||
if (done.has(id)) {
|
||||
await this.say({ lines: RIBBON_DONE });
|
||||
return true;
|
||||
}
|
||||
done.add(id);
|
||||
director.taskSetProgress['ribbons'] = done;
|
||||
// 挂上彩带(tiles16 帧 29/30/31 红/黄/蓝;地图上挂点是路灯,挂上后才出现彩带)
|
||||
const z = this.interactZones.find((zz) => zz.id === id);
|
||||
if (z) {
|
||||
const frame = [29, 30, 31, 29][done.size - 1]; // tiles16 帧 = 瓦片 id(彩带红/黄/蓝)
|
||||
const deco = this.add.image(z.x, z.y, 'tiles16', frame);
|
||||
deco.setDepth(z.y - 8);
|
||||
}
|
||||
await this.say({ lines: RIBBON_LINES });
|
||||
if (done.size >= 4) {
|
||||
director.completeTask('T13');
|
||||
audio.ding();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// T14:保持微笑 60 秒
|
||||
if (id === 'smile_spot') {
|
||||
if (!director.isTaskActive('T14')) {
|
||||
await this.say({ lines: ['广场中央很暖和。'] });
|
||||
return true;
|
||||
}
|
||||
if (this.t14State === 'running') return true;
|
||||
await this.say({ speakerId: 'afu', lines: T14_INTRO });
|
||||
this.startT14();
|
||||
return true;
|
||||
}
|
||||
|
||||
// T15:参加第 413 届丰收庆典 → 结局分歧(E4/E2/E1;E3 需走向雾墙)
|
||||
if (id === 'celebration_site') {
|
||||
if (director.day !== 7 || !director.isTaskActive('T15')) {
|
||||
await this.say({ lines: ['庆典场地空空的。等庆典那天再来吧。'] });
|
||||
return true;
|
||||
}
|
||||
await this.say({ lines: T15_INTRO });
|
||||
director.completeTask('T15');
|
||||
const ending = director.evaluateEnding(false);
|
||||
director.startEnding(ending);
|
||||
this.scene.start('EndingScene', { ending });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- T14 ----------
|
||||
private startT14(): void {
|
||||
this.t14State = 'running';
|
||||
this.t14Ms = 0;
|
||||
this.t14Faded = false;
|
||||
director.t14Active = true;
|
||||
for (const a of this.actors) a.stareAtPlayer = true; // 全村 NPC 转身注视
|
||||
this.cameras.main.zoomTo(1.35, 60_000); // 镜头缓慢拉近
|
||||
}
|
||||
|
||||
private cancelT14(): void {
|
||||
this.t14State = 'idle';
|
||||
this.t14Ms = 0;
|
||||
director.t14Active = false;
|
||||
for (const a of this.actors) a.stareAtPlayer = false;
|
||||
this.cameras.main.zoomTo(1, 500);
|
||||
audio.restoreMaster(1000);
|
||||
}
|
||||
|
||||
private async finishT14(): Promise<void> {
|
||||
this.t14State = 'idle';
|
||||
director.t14Active = false;
|
||||
for (const a of this.actors) a.stareAtPlayer = false;
|
||||
this.cameras.main.zoomTo(1, 2000);
|
||||
audio.restoreMaster(3000);
|
||||
// 先结算任务(此时 T13+T14 齐全会触发黄昏钟声,钟声对话会等空档播放),
|
||||
// 避免任务完成依赖玩家关闭后续对话
|
||||
director.completeTask('T14');
|
||||
audio.ding();
|
||||
await this.say({ speakerId: 'afu', lines: T14_DONE });
|
||||
// 傍晚脚本雨(村里第一次下雨,NPC 台词对雨零反应)
|
||||
director.setRain(true);
|
||||
await this.say({ lines: [NARRATOR.rainStart] });
|
||||
}
|
||||
|
||||
protected extraUpdate(_time: number, delta: number): void {
|
||||
if (this.t14State !== 'running') return;
|
||||
const moving = (this.player.body?.velocity.lengthSq() ?? 0) > 1;
|
||||
if (moving || director.dialogueActive) {
|
||||
if (moving) {
|
||||
this.t14Ms = 0; // 原地站立:移动则重来
|
||||
if (this.t14Faded) {
|
||||
this.t14Faded = false;
|
||||
audio.restoreMaster(1000);
|
||||
}
|
||||
this.t14WarnCd -= delta;
|
||||
if (this.t14WarnCd <= 0) {
|
||||
this.t14WarnCd = 6000;
|
||||
void this.say({ speakerId: 'afu', lines: T14_MOVE_WARN });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.t14Ms += delta;
|
||||
if (!this.t14Faded && this.t14Ms >= 40_000) {
|
||||
this.t14Faded = true;
|
||||
audio.fadeAllToZero(20_000); // 第 40–60 秒所有声音渐隐至零
|
||||
}
|
||||
if (this.t14Ms >= 60_000) {
|
||||
void this.finishT14();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 杂货店(室内 15×10):货架(T5 清点;C5 同一商品重复 9 格,D2 起)
|
||||
|
||||
import { MapScene } from './MapScene';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { audio } from '../audio/AudioEngine';
|
||||
import { CLUES } from '../data/clues';
|
||||
import { SHELF_COUNT_LINES, SHELF_COUNT_DONE } from '../data/dialogues';
|
||||
|
||||
export class ShopScene extends MapScene {
|
||||
readonly mapKey = 'shop';
|
||||
|
||||
constructor() {
|
||||
super('ShopScene');
|
||||
}
|
||||
|
||||
protected async onInteract(id: string): Promise<boolean> {
|
||||
if (id === 'shelf_count') {
|
||||
if (director.isTaskActive('T5')) {
|
||||
await this.say({ lines: SHELF_COUNT_LINES });
|
||||
director.addClue('C5'); // 清点中发现:同一商品重复 9 格、标签空白
|
||||
await this.say({ speakerId: 'mimi', lines: SHELF_COUNT_DONE });
|
||||
director.completeTask('T5');
|
||||
audio.ding();
|
||||
} else if (director.day >= 2) {
|
||||
director.addClue('C5');
|
||||
await this.say({ lines: CLUES.C5.lines });
|
||||
} else {
|
||||
await this.say({ lines: ['货架整整齐齐,摆满了罐头。'] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// TitleScene:标题 + 名字输入(留空默认「小满」)。DOM input,不依赖 localStorage。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
|
||||
export class TitleScene extends Phaser.Scene {
|
||||
private overlay: HTMLDivElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super('TitleScene');
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.cameras.main.setBackgroundColor('#8FD3FF');
|
||||
// 糖果色标题卡
|
||||
this.add.rectangle(160, 70, 240, 64, 0xfff3d6).setStrokeStyle(2, 0xf5a623);
|
||||
this.add.text(160, 58, '崩坏童话', { fontFamily: 'monospace', fontSize: '20px', color: '#8B5A2B' }).setOrigin(0.5);
|
||||
this.add.text(160, 84, '蜜 糖 村', { fontFamily: 'monospace', fontSize: '14px', color: '#F5A623' }).setOrigin(0.5);
|
||||
this.add.text(160, 120, 'WASD / 方向键移动 · E 互动', { fontFamily: 'monospace', fontSize: '8px', color: '#5c4a2e' }).setOrigin(0.5);
|
||||
|
||||
// DOM 输入框(名字仅存于本次会话内存)
|
||||
const parent = document.getElementById('game') ?? document.body;
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = 'position:absolute;left:50%;top:58%;transform:translateX(-50%);display:flex;gap:6px;align-items:center;z-index:10;';
|
||||
const label = document.createElement('span');
|
||||
label.textContent = '名字:';
|
||||
label.style.cssText = 'color:#FFF3D6;font:12px monospace;';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.maxLength = 8;
|
||||
input.placeholder = '小满';
|
||||
input.style.cssText = 'width:80px;font:12px monospace;background:#FFF3D6;border:2px solid #F5A623;color:#5c4a2e;outline:none;';
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = '开始';
|
||||
btn.style.cssText = 'font:12px monospace;background:#F5A623;border:none;color:#fff;padding:2px 8px;cursor:pointer;';
|
||||
overlay.append(label, input, btn);
|
||||
parent.appendChild(overlay);
|
||||
this.overlay = overlay;
|
||||
input.focus();
|
||||
|
||||
const start = (): void => {
|
||||
const name = input.value.trim() || '小满';
|
||||
director.newGame(name);
|
||||
overlay.remove();
|
||||
this.overlay = null;
|
||||
this.scene.start('HouseScene', { spawn: 'default' });
|
||||
};
|
||||
btn.addEventListener('click', start);
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') start();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
this.events.once('shutdown', () => {
|
||||
if (this.overlay) {
|
||||
this.overlay.remove();
|
||||
this.overlay = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// UIScene:常驻 UI(CONTRACT §4)。
|
||||
// 向日葵表盘(12 瓣 Graphics,不显示任何数字;满格花心黑籽)、「按 E」提示、
|
||||
// 底部对话框(说话者名 + 头像 + 文本,E 推进,温柔/迟疑/否定三选项)、
|
||||
// 文本腐蚀渲染层(§6.5)、C4 乱码闪显。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { corruptText } from '../core/TextCorruptor';
|
||||
import { audio } from '../audio/AudioEngine';
|
||||
import { GAME_W, GAME_H, NPC_DEFS, type DialoguePayload, type Stage } from '../types';
|
||||
|
||||
/** §6.4 UI 列:0–20→0–2 瓣,21–50→3–6,51–80→7–9,81–99→10–12,100→12+黑籽 */
|
||||
export function petalCount(h: number): number {
|
||||
if (h >= 100) return 12;
|
||||
if (h >= 81) return 10 + Math.min(2, Math.floor((h - 81) / 7));
|
||||
if (h >= 51) return 7 + Math.min(2, Math.floor((h - 51) / 11));
|
||||
if (h >= 21) return 3 + Math.min(3, Math.floor((h - 21) / 8));
|
||||
return Math.min(2, Math.floor(h / 8));
|
||||
}
|
||||
|
||||
export class UIScene extends Phaser.Scene {
|
||||
private dial!: Phaser.GameObjects.Graphics;
|
||||
private promptText!: Phaser.GameObjects.Text;
|
||||
private flashTextObj!: Phaser.GameObjects.Text;
|
||||
/** 表盘当前是否显示黑籽(满格),验收脚本读取 */
|
||||
seedsShown = false;
|
||||
|
||||
// 对话
|
||||
private dlgBox: Phaser.GameObjects.Container | null = null;
|
||||
private dlgBody!: Phaser.GameObjects.Text;
|
||||
private dlgName!: Phaser.GameObjects.Text;
|
||||
private dlgPortrait!: Phaser.GameObjects.Image;
|
||||
private dlgChoices: Phaser.GameObjects.Text[] = [];
|
||||
private payload: DialoguePayload | null = null;
|
||||
private lineIdx = 0;
|
||||
private fullLine = '';
|
||||
private typing = false;
|
||||
private typeTimer: Phaser.Time.TimerEvent | null = null;
|
||||
private choiceIdx = 0;
|
||||
private showingChoices = false;
|
||||
private openAt = 0;
|
||||
|
||||
private keys!: {
|
||||
E: Phaser.Input.Keyboard.Key;
|
||||
UP: Phaser.Input.Keyboard.Key;
|
||||
DOWN: Phaser.Input.Keyboard.Key;
|
||||
W: Phaser.Input.Keyboard.Key;
|
||||
S: Phaser.Input.Keyboard.Key;
|
||||
N1: Phaser.Input.Keyboard.Key;
|
||||
N2: Phaser.Input.Keyboard.Key;
|
||||
N3: Phaser.Input.Keyboard.Key;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super('UIScene');
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.dial = this.add.graphics().setDepth(10);
|
||||
this.promptText = this.add.text(GAME_W / 2, GAME_H - 14, '', {
|
||||
fontFamily: 'monospace', fontSize: '8px', color: '#ffffff',
|
||||
stroke: '#5c4a2e', strokeThickness: 2,
|
||||
}).setOrigin(0.5).setDepth(10);
|
||||
|
||||
this.flashTextObj = this.add.text(GAME_W / 2, GAME_H / 2, '', {
|
||||
fontFamily: 'monospace', fontSize: '10px', color: '#ff3366',
|
||||
stroke: '#000000', strokeThickness: 2,
|
||||
}).setOrigin(0.5).setDepth(30).setVisible(false);
|
||||
|
||||
this.keys = this.input.keyboard!.addKeys('E,UP,DOWN,W,S') as typeof this.keys;
|
||||
this.keys.N1 = this.input.keyboard!.addKey('ONE');
|
||||
this.keys.N2 = this.input.keyboard!.addKey('TWO');
|
||||
this.keys.N3 = this.input.keyboard!.addKey('THREE');
|
||||
|
||||
// 场景会被 stop/launch(结局、debug),监听器必须在 shutdown 时摘除
|
||||
const redraw = (): void => this.drawDial();
|
||||
const onPrompt = (text: string | null): void => { this.promptText.setText(text ?? ''); };
|
||||
const onDialogue = (p: DialoguePayload): void => this.openDialogue(p);
|
||||
const onFlash = (text: string, ms: number): void => this.flash(text, ms);
|
||||
director.on('happiness', redraw);
|
||||
director.on('stage', redraw);
|
||||
director.on('day', redraw);
|
||||
director.on('prompt', onPrompt);
|
||||
director.on('dialogue', onDialogue);
|
||||
director.on('flashText', onFlash);
|
||||
this.events.once('shutdown', () => {
|
||||
director.off('happiness', redraw);
|
||||
director.off('stage', redraw);
|
||||
director.off('day', redraw);
|
||||
director.off('prompt', onPrompt);
|
||||
director.off('dialogue', onDialogue);
|
||||
director.off('flashText', onFlash);
|
||||
});
|
||||
|
||||
this.drawDial();
|
||||
}
|
||||
|
||||
// ---------- 向日葵表盘(无数字) ----------
|
||||
private drawDial(): void {
|
||||
const g = this.dial;
|
||||
g.clear();
|
||||
const cx = 24;
|
||||
const cy = 24;
|
||||
const h = director.happiness;
|
||||
const lit = petalCount(h);
|
||||
const bright = director.params.petalBright;
|
||||
const litColor = bright ? 0xfff3b0 : 0xffd93d; // S3 起花瓣过亮
|
||||
const unlitColor = 0x4a3f2a;
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const ang = -Math.PI / 2 + (i * Math.PI) / 6;
|
||||
const px = cx + Math.cos(ang) * 13;
|
||||
const py = cy + Math.sin(ang) * 13;
|
||||
const w = 4;
|
||||
const len = 7;
|
||||
// 旋转矩形(花瓣)
|
||||
const ux = Math.cos(ang);
|
||||
const uy = Math.sin(ang);
|
||||
const nx = -uy;
|
||||
const ny = ux;
|
||||
const pts = [
|
||||
new Phaser.Math.Vector2(px + nx * w / 2, py + ny * w / 2),
|
||||
new Phaser.Math.Vector2(px - nx * w / 2, py - ny * w / 2),
|
||||
new Phaser.Math.Vector2(px - nx * w / 2 + ux * len, py - ny * w / 2 + uy * len),
|
||||
new Phaser.Math.Vector2(px + nx * w / 2 + ux * len, py + ny * w / 2 + uy * len),
|
||||
];
|
||||
g.fillStyle(i < lit ? litColor : unlitColor, 1);
|
||||
g.fillPoints(pts, true);
|
||||
}
|
||||
// 花心
|
||||
g.fillStyle(0x8b5a2b, 1);
|
||||
g.fillCircle(cx, cy, 7);
|
||||
// 满格:花心浮现过熟的黑籽
|
||||
this.seedsShown = h >= 100;
|
||||
if (h >= 100) {
|
||||
g.fillStyle(0x1a1208, 1);
|
||||
const seeds = [[-3, -2], [2, -3], [0, 1], [-2, 3], [3, 2]];
|
||||
for (const [sx, sy] of seeds) g.fillCircle(cx + sx, cy + sy, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- C4 乱码闪显 ----------
|
||||
private flash(text: string, ms: number): void {
|
||||
this.flashTextObj.setText(text).setVisible(true);
|
||||
this.time.delayedCall(ms, () => this.flashTextObj.setVisible(false));
|
||||
}
|
||||
|
||||
// ---------- 对话框 ----------
|
||||
private openDialogue(p: DialoguePayload): void {
|
||||
this.closeDialogueOnly();
|
||||
this.payload = p;
|
||||
this.lineIdx = 0;
|
||||
this.openAt = this.time.now;
|
||||
|
||||
const boxY = GAME_H - 56;
|
||||
const c = this.add.container(0, 0).setDepth(20);
|
||||
const bg = this.add.rectangle(GAME_W / 2, boxY + 28, GAME_W - 8, 52, 0xfff3d6, 0.96)
|
||||
.setStrokeStyle(2, 0x8b5a2b);
|
||||
c.add(bg);
|
||||
|
||||
const speaker = p.speakerId;
|
||||
const def = speaker && speaker !== 'narrator' ? NPC_DEFS[speaker] : null;
|
||||
|
||||
this.dlgPortrait = this.add.image(24, boxY + 28, def?.portraitKey || 'portrait_player_smile')
|
||||
.setDisplaySize(32, 32);
|
||||
this.dlgPortrait.setVisible(!!def);
|
||||
c.add(this.dlgPortrait);
|
||||
|
||||
this.dlgName = this.add.text(46, boxY + 6, def ? def.name : '', {
|
||||
fontFamily: 'monospace', fontSize: '8px', color: '#8B5A2B',
|
||||
});
|
||||
c.add(this.dlgName);
|
||||
|
||||
this.dlgBody = this.add.text(46, boxY + 17, '', {
|
||||
fontFamily: 'monospace', fontSize: '8px', color: '#3a2c18',
|
||||
wordWrap: { width: GAME_W - 62, useAdvancedWrap: true },
|
||||
lineSpacing: 3,
|
||||
});
|
||||
c.add(this.dlgBody);
|
||||
|
||||
this.dlgBox = c;
|
||||
this.showLine();
|
||||
}
|
||||
|
||||
private showLine(): void {
|
||||
if (!this.payload) return;
|
||||
const raw = this.payload.lines[this.lineIdx] ?? '';
|
||||
// 渲染层腐蚀(§6.5)
|
||||
this.fullLine = corruptText(raw, this.payload.speakerId, director.stage as Stage);
|
||||
this.typing = true;
|
||||
this.dlgBody.setText('');
|
||||
if (this.fullLine.length === 0) {
|
||||
this.typing = false;
|
||||
return;
|
||||
}
|
||||
let i = 0;
|
||||
this.typeTimer?.remove();
|
||||
this.typeTimer = this.time.addEvent({
|
||||
delay: 28,
|
||||
repeat: this.fullLine.length - 1,
|
||||
callback: () => {
|
||||
i += 1;
|
||||
this.dlgBody.setText(this.fullLine.slice(0, i));
|
||||
if (i % 3 === 0) audio.beep();
|
||||
if (i >= this.fullLine.length) this.typing = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private finishTyping(): void {
|
||||
this.typeTimer?.remove();
|
||||
this.typeTimer = null;
|
||||
this.typing = false;
|
||||
this.dlgBody.setText(this.fullLine);
|
||||
}
|
||||
|
||||
private advance(): void {
|
||||
if (!this.payload) return;
|
||||
if (this.typing) {
|
||||
this.finishTyping();
|
||||
return;
|
||||
}
|
||||
audio.beep();
|
||||
this.lineIdx += 1;
|
||||
if (this.lineIdx < this.payload.lines.length) {
|
||||
this.showLine();
|
||||
return;
|
||||
}
|
||||
// 文本结束 → 选项或关闭
|
||||
if (this.payload.choices && this.payload.choices.length > 0 && !this.showingChoices) {
|
||||
this.renderChoices();
|
||||
return;
|
||||
}
|
||||
const result = this.showingChoices ? this.choiceIdx : null;
|
||||
const chosen = this.payload.choices?.[this.choiceIdx];
|
||||
this.closeDialogueOnly();
|
||||
director.resolveDialogue(result);
|
||||
void chosen;
|
||||
}
|
||||
|
||||
private renderChoices(): void {
|
||||
if (!this.payload?.choices || !this.dlgBox) return;
|
||||
this.showingChoices = true;
|
||||
this.choiceIdx = 0;
|
||||
this.dlgChoices = [];
|
||||
const baseY = GAME_H - 56 + 20;
|
||||
this.payload.choices.forEach((ch, i) => {
|
||||
const t = this.add.text(58, baseY + i * 10, `${i === 0 ? '▶ ' : ' '}${ch.label}`, {
|
||||
fontFamily: 'monospace', fontSize: '8px',
|
||||
color: i === 0 ? '#c0392b' : '#3a2c18',
|
||||
});
|
||||
this.dlgBox!.add(t);
|
||||
this.dlgChoices.push(t);
|
||||
});
|
||||
this.dlgBody.setVisible(false);
|
||||
}
|
||||
|
||||
private moveChoice(dir: number): void {
|
||||
if (!this.showingChoices || !this.payload?.choices) return;
|
||||
const n = this.payload.choices.length;
|
||||
this.choiceIdx = (this.choiceIdx + dir + n) % n;
|
||||
this.dlgChoices.forEach((t, i) => {
|
||||
t.setText(`${i === this.choiceIdx ? '▶ ' : ' '}${this.payload!.choices![i].label}`);
|
||||
t.setColor(i === this.choiceIdx ? '#c0392b' : '#3a2c18');
|
||||
});
|
||||
}
|
||||
|
||||
private closeDialogueOnly(): void {
|
||||
this.typeTimer?.remove();
|
||||
this.typeTimer = null;
|
||||
if (this.dlgBox) {
|
||||
this.dlgBox.destroy(true);
|
||||
this.dlgBox = null;
|
||||
}
|
||||
this.dlgChoices = [];
|
||||
this.showingChoices = false;
|
||||
this.payload = null;
|
||||
}
|
||||
|
||||
update(): void {
|
||||
if (!this.payload) return;
|
||||
// 打开后 250ms 内忽略 E,避免触发对话的同一按键直接推进
|
||||
const justOpened = this.time.now - this.openAt < 250;
|
||||
|
||||
if (this.showingChoices) {
|
||||
if (Phaser.Input.Keyboard.JustDown(this.keys.UP) || Phaser.Input.Keyboard.JustDown(this.keys.W)) this.moveChoice(-1);
|
||||
if (Phaser.Input.Keyboard.JustDown(this.keys.DOWN) || Phaser.Input.Keyboard.JustDown(this.keys.S)) this.moveChoice(1);
|
||||
if (Phaser.Input.Keyboard.JustDown(this.keys.N1)) this.selectChoice(0);
|
||||
if (Phaser.Input.Keyboard.JustDown(this.keys.N2)) this.selectChoice(1);
|
||||
if (Phaser.Input.Keyboard.JustDown(this.keys.N3)) this.selectChoice(2);
|
||||
if (!justOpened && Phaser.Input.Keyboard.JustDown(this.keys.E)) this.advance();
|
||||
} else if (!justOpened && Phaser.Input.Keyboard.JustDown(this.keys.E)) {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
private selectChoice(i: number): void {
|
||||
if (!this.payload?.choices || i >= this.payload.choices.length) return;
|
||||
this.choiceIdx = i;
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 古井空地(14×10):古井(C7,D5 起,交互聆听 5 秒;D5 前被 NPC 温柔劝离)。
|
||||
// 注视古井 5 秒(每日 1 次 +3 觉知)。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { MapScene } from './MapScene';
|
||||
import { director } from '../core/CorruptionDirector';
|
||||
import { audio } from '../audio/AudioEngine';
|
||||
import { CLUES } from '../data/clues';
|
||||
import { WELL_DISSUADE, WELL_LISTEN_START } from '../data/dialogues';
|
||||
|
||||
export class WellScene extends MapScene {
|
||||
readonly mapKey = 'well';
|
||||
|
||||
constructor() {
|
||||
super('WellScene');
|
||||
}
|
||||
|
||||
private stareMs = 0;
|
||||
private listening = false;
|
||||
|
||||
protected onReady(): void {
|
||||
this.stareMs = 0;
|
||||
this.listening = false;
|
||||
}
|
||||
|
||||
protected async onInteract(id: string): Promise<boolean> {
|
||||
if (id !== 'ancient_well') return false;
|
||||
|
||||
if (director.day < 5) {
|
||||
// D5 前:被 NPC 温柔劝离
|
||||
await this.say({ lines: WELL_DISSUADE });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.listening) return true;
|
||||
this.listening = true;
|
||||
await this.say({ lines: WELL_LISTEN_START });
|
||||
// 交互 5 秒:井底回声——极轻的监护仪滴声
|
||||
for (let i = 0; i < 5; i++) {
|
||||
audio.ecgSoft();
|
||||
await this.wait(1000);
|
||||
}
|
||||
director.addClue('C7');
|
||||
await this.say({ lines: CLUES.C7.lines });
|
||||
this.listening = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected extraUpdate(_time: number, delta: number): void {
|
||||
if (director.day < 5 || this.listening) return;
|
||||
const z = this.interactZones.find((zz) => zz.id === 'ancient_well');
|
||||
if (!z) return;
|
||||
const d = Phaser.Math.Distance.Between(this.player.x, this.player.y - 8, z.x, z.y);
|
||||
const still = (this.player.body?.velocity.lengthSq() ?? 0) <= 1;
|
||||
if (d <= 40 && still && !director.dialogueActive) {
|
||||
this.stareMs += delta;
|
||||
if (this.stareMs >= 5000) {
|
||||
this.stareMs = -1e9; // 每日一次由 director 判定;本地只触发一次
|
||||
if (director.registerStare('well')) {
|
||||
void this.say({ lines: ['你盯着井口看了很久。', '井水黑得不像水。'] });
|
||||
}
|
||||
}
|
||||
} else if (this.stareMs > 0) {
|
||||
this.stareMs = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 白色层(20×14:上半 20×6 走廊 + 左下 10×8 病房)。
|
||||
// 仅梦境闪切与 E3 使用。E3 模式:穿过走廊走进病房 → 病历特写(EndingScene)。
|
||||
|
||||
import Phaser from 'phaser';
|
||||
import { MapScene } from './MapScene';
|
||||
|
||||
export class WhiteScene extends MapScene {
|
||||
readonly mapKey = 'white';
|
||||
|
||||
constructor() {
|
||||
super('WhiteScene');
|
||||
}
|
||||
|
||||
private e3 = false;
|
||||
private wardTriggered = false;
|
||||
private wardX = 80;
|
||||
private wardY = 120;
|
||||
|
||||
protected onReady(data: { spawn?: string; e3?: boolean }): void {
|
||||
this.e3 = !!data?.e3;
|
||||
this.wardTriggered = false;
|
||||
// 病房内部(20×14:走廊行 1-5,病房行 7-12 列 1-8,入口缺口在列 4-5 行 6)
|
||||
this.wardX = 4.5 * 16;
|
||||
this.wardY = 9 * 16;
|
||||
}
|
||||
|
||||
protected extraUpdate(): void {
|
||||
if (!this.e3 || this.wardTriggered) return;
|
||||
const d = Phaser.Math.Distance.Between(this.player.x, this.player.y, this.wardX, this.wardY);
|
||||
if (d <= 24) {
|
||||
this.wardTriggered = true;
|
||||
this.cameras.main.fadeOut(800, 255, 255, 255);
|
||||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||||
this.scene.start('EndingScene', { ending: 'E3' });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// 共享类型与常量。所有数值/台词的原文在 src/data/ 下,本文件只放结构性定义。
|
||||
|
||||
export type Stage = 0 | 1 | 2 | 3 | 4;
|
||||
|
||||
/** §6.4 侵蚀阶段矩阵:视听参数唯一真源表 */
|
||||
export interface StageParams {
|
||||
saturation: number; // ColorMatrix 饱和度
|
||||
vignette: number; // 晕影强度 0-1
|
||||
playbackRate: number; // BGM 速率
|
||||
lowpass: number; // 低通截止 Hz
|
||||
birdMuted: boolean; // 鸟鸣声部静音
|
||||
textLevel: 0 | 1 | 2 | 3; // 0 无 / 1 标点重复(1/80) / 2 同音错字(1/50) / 3 加密(1/10)
|
||||
negativePerMin: number; // 随机单帧负片频率(次/分钟)
|
||||
petalBright: boolean; // 花瓣过亮
|
||||
}
|
||||
|
||||
export const STAGE_PARAMS: Record<Stage, StageParams> = {
|
||||
0: { saturation: 0.90, vignette: 0.00, playbackRate: 1.00, lowpass: 22050, birdMuted: false, textLevel: 0, negativePerMin: 0.0, petalBright: false },
|
||||
1: { saturation: 1.00, vignette: 0.00, playbackRate: 1.00, lowpass: 22050, birdMuted: false, textLevel: 0, negativePerMin: 0.0, petalBright: false },
|
||||
2: { saturation: 1.15, vignette: 0.00, playbackRate: 0.97, lowpass: 22050, birdMuted: false, textLevel: 1, negativePerMin: 0.0, petalBright: false },
|
||||
3: { saturation: 1.30, vignette: 0.30, playbackRate: 0.92, lowpass: 8000, birdMuted: false, textLevel: 2, negativePerMin: 0.0, petalBright: true },
|
||||
4: { saturation: 1.40, vignette: 0.60, playbackRate: 0.84, lowpass: 4000, birdMuted: true, textLevel: 3, negativePerMin: 0.4, petalBright: true },
|
||||
};
|
||||
|
||||
/** 由幸福值映射阶段(§6.4) */
|
||||
export function stageOf(happiness: number): Stage {
|
||||
if (happiness >= 100) return 4;
|
||||
if (happiness >= 81) return 3;
|
||||
if (happiness >= 51) return 2;
|
||||
if (happiness >= 21) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export type NpcId =
|
||||
| 'afu' | 'yuanyuan' | 'dabao' | 'mimi' | 'fuyisheng' | 'guiyeye' | 'bird'
|
||||
| 'squirrel' | 'hedgehog' | 'fox' | 'pig' | 'chick';
|
||||
|
||||
export interface NpcDef {
|
||||
id: NpcId;
|
||||
name: string; // 对话框说话者名
|
||||
spriteKey: string; // spritesheet key
|
||||
portraitKey: string; // 头像 key(群演无)
|
||||
offset: number; // §4 阈值偏移(作用于幸福阈值)
|
||||
expressive: boolean; // 是否参与侵蚀表情(芙医生/龟爷爷/群演不参与)
|
||||
isExtra: boolean; // 群演
|
||||
}
|
||||
|
||||
export const NPC_DEFS: Record<NpcId, NpcDef> = {
|
||||
afu: { id: 'afu', name: '阿福', spriteKey: 'npc_afu', portraitKey: 'portrait_afu', offset: 0, expressive: true, isExtra: false },
|
||||
yuanyuan: { id: 'yuanyuan', name: '圆圆', spriteKey: 'npc_yuanyuan', portraitKey: 'portrait_yuanyuan', offset: 15, expressive: true, isExtra: false },
|
||||
dabao: { id: 'dabao', name: '大宝', spriteKey: 'npc_dabao', portraitKey: 'portrait_dabao', offset: 0, expressive: true, isExtra: false },
|
||||
mimi: { id: 'mimi', name: '咪咪', spriteKey: 'npc_mimi', portraitKey: 'portrait_mimi', offset: -10, expressive: true, isExtra: false },
|
||||
fuyisheng: { id: 'fuyisheng', name: '芙医生', spriteKey: 'npc_fuyisheng', portraitKey: 'portrait_fuyisheng', offset: 0, expressive: false, isExtra: false },
|
||||
guiyeye: { id: 'guiyeye', name: '龟爷爷', spriteKey: 'npc_guiyeye', portraitKey: 'portrait_guiyeye', offset: 0, expressive: false, isExtra: false },
|
||||
bird: { id: 'bird', name: '双子鸟', spriteKey: 'npc_bird', portraitKey: 'portrait_bird', offset: 0, expressive: false, isExtra: false },
|
||||
squirrel: { id: 'squirrel', name: '松鼠', spriteKey: 'extra_squirrel', portraitKey: '', offset: 0, expressive: false, isExtra: true },
|
||||
hedgehog: { id: 'hedgehog', name: '刺猬', spriteKey: 'extra_hedgehog', portraitKey: '', offset: 0, expressive: false, isExtra: true },
|
||||
fox: { id: 'fox', name: '狐狸', spriteKey: 'extra_fox', portraitKey: '', offset: 0, expressive: false, isExtra: true },
|
||||
pig: { id: 'pig', name: '小猪', spriteKey: 'extra_pig', portraitKey: '', offset: 0, expressive: false, isExtra: true },
|
||||
chick: { id: 'chick', name: '小鸡', spriteKey: 'extra_chick', portraitKey: '', offset: 0, expressive: false, isExtra: true },
|
||||
};
|
||||
|
||||
export const NAMED_NPCS: NpcId[] = ['afu', 'yuanyuan', 'dabao', 'mimi', 'fuyisheng', 'guiyeye', 'bird'];
|
||||
export const EXTRA_NPCS: NpcId[] = ['squirrel', 'hedgehog', 'fox', 'pig', 'chick'];
|
||||
|
||||
/** 地图 key → 场景 key */
|
||||
export const MAP_SCENES: Record<string, string> = {
|
||||
house: 'HouseScene',
|
||||
plaza: 'PlazaScene',
|
||||
bakery: 'BakeryScene',
|
||||
shop: 'ShopScene',
|
||||
clinic: 'ClinicScene',
|
||||
lake: 'LakeScene',
|
||||
field: 'FieldScene',
|
||||
well: 'WellScene',
|
||||
fog: 'FogScene',
|
||||
white: 'WhiteScene',
|
||||
};
|
||||
|
||||
export const OUTDOOR_MAPS = ['plaza', 'lake', 'field', 'well', 'fog'];
|
||||
|
||||
/** 对话选项种类(§12:温柔/迟疑/否定;accept/refuse 用于可拒绝任务) */
|
||||
export type ChoiceKind = 'gentle' | 'hesitant' | 'deny' | 'accept' | 'refuse';
|
||||
|
||||
export interface DialogueChoice {
|
||||
label: string;
|
||||
kind: ChoiceKind;
|
||||
}
|
||||
|
||||
export interface DialoguePayload {
|
||||
speakerId?: NpcId | 'narrator';
|
||||
lines: string[];
|
||||
choices?: DialogueChoice[];
|
||||
}
|
||||
|
||||
export type SadnessKind = 'refuse' | 'deny' | 'idle' | 'cling' | 'rain';
|
||||
export type SmallInteractionKind = 'water' | 'feed' | 'pat';
|
||||
export type EndingId = 'E1' | 'E2' | 'E3' | 'E4';
|
||||
|
||||
/** 标准三选项 */
|
||||
export const STANDARD_CHOICES: DialogueChoice[] = [
|
||||
{ label: '(温柔地回应)', kind: 'gentle' },
|
||||
{ label: '(迟疑)', kind: 'hesitant' },
|
||||
{ label: '(否定)', kind: 'deny' },
|
||||
];
|
||||
|
||||
export const GAME_W = 320;
|
||||
export const GAME_H = 180;
|
||||
export const MOVE_SPEED = 96;
|
||||
export const INTERACT_RANGE = 24;
|
||||
@@ -0,0 +1,767 @@
|
||||
// 验收测试:PRD §14 共 20 条。用法:先 npm run build && npm run preview,再 node tools/acceptance.mjs
|
||||
// 需要本机 Chrome(puppeteer-core 驱动,无额外下载)。
|
||||
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import fs from 'node:fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
|
||||
const BASE = 'http://127.0.0.1:4815/';
|
||||
const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const SHOTS = 'tools/shots';
|
||||
fs.mkdirSync(SHOTS, { recursive: true });
|
||||
|
||||
// 自起 preview 服务器(退出时关闭)
|
||||
const preview = spawn('npx', ['vite', 'preview', '--host', '127.0.0.1', '--port', '4815', '--strictPort'], { stdio: 'ignore' });
|
||||
process.on('exit', () => { try { preview.kill(); } catch {} });
|
||||
async function waitPort(port, timeout = 15000) {
|
||||
const t0 = Date.now();
|
||||
for (;;) {
|
||||
const ok = await new Promise((res) => { const s = net.connect(port, '127.0.0.1'); s.once('connect', () => { s.end(); res(true); }); s.once('error', () => res(false)); });
|
||||
if (ok) return;
|
||||
if (Date.now() - t0 > timeout) throw new Error('preview server 未就绪');
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
}
|
||||
await waitPort(4815);
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const failures = [];
|
||||
function check(name, cond, detail = '') {
|
||||
if (cond) { pass++; console.log(` PASS ${name}`); }
|
||||
else { fail++; failures.push(name + (detail ? ` — ${detail}` : '')); console.log(` FAIL ${name} ${detail}`); }
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const read = (p) => fs.readFileSync(p, 'utf8');
|
||||
|
||||
// ================= 静态源码/资源检查 =================
|
||||
console.log('== 静态检查 ==');
|
||||
const ann = read('src/data/announcements.ts');
|
||||
const PRD_ANN = [
|
||||
'早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
'早上好呀!新的一天,新的幸福。祝你开心!',
|
||||
'早上好!今天要把幸福装得满满的哦。要开心!',
|
||||
'早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
'早上好。今天是自由日,你可以自由地选择幸福。要开心哦。',
|
||||
'明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。',
|
||||
'庆典开始了。来吧。',
|
||||
];
|
||||
for (let i = 0; i < 7; i++) check(`公告 D${i + 1} 逐字`, ann.includes(PRD_ANN[i]), PRD_ANN[i]);
|
||||
check('公告 D4===D1', PRD_ANN[3] === PRD_ANN[0] && (ann.split(PRD_ANN[0]).length - 1) >= 2);
|
||||
|
||||
const tasks = read('src/data/tasks.ts');
|
||||
const REWARDS = { T1: 15, T2: 10, T3: 10, T4: 15, T5: 10, T6: 10, T7: 15, T8: 15, T9: 10, T10: 10, T11: 10, T12: 10, T13: 10, T14: 15 };
|
||||
for (const [id, r] of Object.entries(REWARDS)) check(`任务 ${id} 奖励 +${r}`, new RegExp(`id: '${id}'[\\s\\S]*?reward: ${r}[,}]`).test(tasks));
|
||||
const t1 = /T1:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t8 = /T8:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t2 = /T2:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t9 = /T9:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t3 = /T3:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t10 = /T10:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
check('T8/T9/T10 与 T1/T2/T3 逐字相同', t1 === t8 && t2 === t9 && t3 === t10 && !!t1);
|
||||
check('T15 标题逐字', tasks.includes('参加第 413 届丰收庆典'));
|
||||
|
||||
const corrupt = read('src/core/TextCorruptor.ts');
|
||||
const PAIRS = [['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'], ['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远']];
|
||||
for (const [a, b] of PAIRS) check(`错字表 ${a}→${b}`, corrupt.includes(`['${a}', '${b}']`));
|
||||
check('腐蚀优先命中咪咪', /mimi/.test(corrupt) && /咪咪/.test(corrupt));
|
||||
|
||||
const dlg = read('src/data/dialogues.ts') + read('src/data/clues.ts');
|
||||
for (const s of ['湖水一直很平静', '面包一直是那个味道', '孩子,今天是第几个今天?', '按疗程配比。甜度:最大。']) check(`线索台词逐字「${s}」`, dlg.includes(s));
|
||||
const endings = read('src/data/endings.ts');
|
||||
for (const s of ['蜜糖谷情绪疗养中心', 'PT-07', '系统检测到疗程无效。', '疗程重新开始。', '从此,每一天都很幸福。', '每一天。']) check(`结局文本逐字「${s}」`, endings.includes(s));
|
||||
|
||||
// 地图 JSON 结构
|
||||
const MAP_KEYS = ['house', 'plaza', 'bakery', 'shop', 'clinic', 'lake', 'field', 'well', 'fog', 'white'];
|
||||
const maps = {};
|
||||
const mapJson = {};
|
||||
for (const key of MAP_KEYS) {
|
||||
const d = JSON.parse(read(`public/assets/maps/${key}.json`));
|
||||
mapJson[key] = d;
|
||||
const names = d.layers.map((l) => l.name);
|
||||
check(`地图 ${key} 三层齐全`, names.includes('ground') && names.includes('obstacles') && names.includes('objects'));
|
||||
const ob = d.layers.find((l) => l.name === 'obstacles');
|
||||
check(`地图 ${key} obstacles collides=true`, (ob.properties || []).some((p) => p.name === 'collides' && p.value === true));
|
||||
check(`地图 ${key} tileset 内嵌`, d.tilesets[0].image === '../tileset.png' && d.tilesets[0].tilewidth === 16);
|
||||
const wpx = d.width * 16, hpx = d.height * 16;
|
||||
const ox = Math.max(0, Math.floor((320 - wpx) / 2));
|
||||
const oy = Math.max(0, Math.floor((180 - hpx) / 2));
|
||||
const og = d.layers.find((l) => l.name === 'objects');
|
||||
maps[key] = og.objects.map((o) => ({
|
||||
name: o.name, x: o.x + ox, y: o.y + oy,
|
||||
props: Object.fromEntries((o.properties || []).map((p) => [p.name, p.value])),
|
||||
}));
|
||||
}
|
||||
// 门双向成对
|
||||
for (const key of MAP_KEYS) {
|
||||
for (const d of maps[key].filter((o) => o.name === 'door')) {
|
||||
const tgt = d.props.target, sp = d.props.spawn;
|
||||
const okSpawn = maps[tgt]?.some((o) => o.name === 'spawn' && o.props.id === sp);
|
||||
const okBack = maps[tgt]?.some((o) => o.name === 'door' && o.props.target === key);
|
||||
check(`门 ${key}→${tgt}(${sp}) 双向`, !!okSpawn && !!okBack);
|
||||
}
|
||||
}
|
||||
// NPC 站位(§4)
|
||||
const npcAt = (m, id) => maps[m].some((o) => o.name === 'npc' && o.props.id === id);
|
||||
for (const [m, id] of [['plaza', 'afu'], ['plaza', 'dabao'], ['plaza', 'bird'], ['bakery', 'yuanyuan'], ['shop', 'mimi'], ['clinic', 'fuyisheng'], ['lake', 'guiyeye']]) {
|
||||
check(`NPC 站位 ${id}@${m}`, npcAt(m, id));
|
||||
}
|
||||
let extras = 0;
|
||||
for (const k of MAP_KEYS) extras += maps[k].filter((o) => o.name === 'npc' && ['squirrel', 'hedgehog', 'fox', 'pig', 'chick'].includes(o.props.id)).length;
|
||||
check('群演 ×5 分布各场景', extras === 5, `实际 ${extras}`);
|
||||
|
||||
// ================= 浏览器部分 =================
|
||||
const SCENE = { house: 'HouseScene', plaza: 'PlazaScene', bakery: 'BakeryScene', shop: 'ShopScene', clinic: 'ClinicScene', lake: 'LakeScene', field: 'FieldScene', well: 'WellScene', fog: 'FogScene', white: 'WhiteScene' };
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROME, headless: true,
|
||||
args: ['--autoplay-policy=no-user-gesture-required', '--mute-audio', '--disable-dev-shm-usage'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
const consoleErrors = [];
|
||||
page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text()); });
|
||||
page.on('pageerror', (e) => consoleErrors.push('PAGEERROR: ' + e.message));
|
||||
const extReqs = [];
|
||||
page.on('request', (r) => { const u = r.url(); if (!u.startsWith(BASE) && !u.startsWith('data:') && !u.startsWith('blob:') && !u.startsWith('devtools:')) extReqs.push(u); });
|
||||
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const isActive = (k) => ev((k) => window.__game.scene.isActive(k), k);
|
||||
async function waitActive(k, timeout = 9000) { await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout }, k); }
|
||||
async function startScene(k, spawn = 'default') {
|
||||
await ev((k, s) => {
|
||||
const sm = window.__game.scene;
|
||||
// game.scene.start 不会停掉其它场景:先停掉所有活动地图场景(保留常驻 UIScene)
|
||||
for (const sc of sm.getScenes(true)) {
|
||||
const key = sc.scene.key;
|
||||
if (key !== 'UIScene' && key !== k) sm.stop(key);
|
||||
}
|
||||
sm.start(k, { spawn: s });
|
||||
}, k, spawn);
|
||||
await waitActive(k); await sleep(500);
|
||||
}
|
||||
async function dget(expr) { return ev(new Function(`return window.__director.${expr}`)); }
|
||||
async function dset(expr) { return ev(new Function(`window.__director.${expr}`)); }
|
||||
const darr = (expr) => ev(new Function(`return [...window.__director.${expr}]`));
|
||||
async function setPlayer(sceneKey, x, y) {
|
||||
await ev((k, x, y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0, 0); }, sceneKey, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
const playerPos = (sceneKey) => ev((k) => { const s = window.__game.scene.getScene(k); return { x: s.player.x, y: s.player.y }; }, sceneKey);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; });
|
||||
// Phaser JustDown 需要按键“按住”跨过至少一个游戏帧;瞬发 press 的 down+up 落在同一帧内会被吞掉
|
||||
async function tap(code, holdMs = 80) {
|
||||
await page.keyboard.down(code);
|
||||
await sleep(holdMs);
|
||||
await page.keyboard.up(code);
|
||||
await sleep(30);
|
||||
}
|
||||
async function drain(choiceKey = 'Digit1', maxIter = 60) {
|
||||
const texts = [];
|
||||
for (let i = 0; i < maxIter; i++) {
|
||||
if (!(await dlgOpen())) break;
|
||||
texts.push(await dlgText());
|
||||
const choosing = await ev(() => { const u = window.__game.scene.getScene('UIScene'); return u.showingChoices; });
|
||||
await tap(choosing ? choiceKey : 'KeyE');
|
||||
await sleep(160);
|
||||
}
|
||||
for (let i = 0; i < 20 && (await dlgOpen()); i++) { await tap('KeyE'); await sleep(140); }
|
||||
return texts;
|
||||
}
|
||||
async function interactAt(mapKey, kind, id, choiceKey = 'Digit1') {
|
||||
const sceneKey = SCENE[mapKey];
|
||||
if (!(await isActive(sceneKey))) await startScene(sceneKey);
|
||||
await drain(); // 清掉残留对话(黄昏/满格等)
|
||||
const obj = maps[mapKey].find((o) => o.name === kind && o.props.id === id);
|
||||
if (!obj) throw new Error(`no ${kind}:${id} in ${mapKey}`);
|
||||
await setPlayer(sceneKey, obj.x + 40, obj.y + 40); // 先离开感应圈(触发目标重武装)
|
||||
await setPlayer(sceneKey, obj.x, obj.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(400);
|
||||
return drain(choiceKey);
|
||||
}
|
||||
// plaza 代码补区(相对公告板)
|
||||
function plazaZone(id) {
|
||||
const nb = maps.plaza.find((o) => o.name === 'interact' && o.props.id === 'noticeboard');
|
||||
if (id === 'noticeboard') return { x: nb.x, y: nb.y };
|
||||
const rel = {
|
||||
mailbox_c3: [-48, 24], ribbon_1: [-48, -32], ribbon_2: [64, -32], ribbon_3: [-48, 64], ribbon_4: [64, 64],
|
||||
smile_spot: [0, 32], celebration_site: [0, -72],
|
||||
}[id];
|
||||
return { x: nb.x + rel[0], y: nb.y + rel[1] };
|
||||
}
|
||||
async function interactPlazaZone(id, choiceKey = 'Digit1') {
|
||||
if (!(await isActive('PlazaScene'))) await startScene('PlazaScene');
|
||||
await drain();
|
||||
const z = plazaZone(id);
|
||||
await setPlayer('PlazaScene', z.x + 40, z.y + 40); // 先离开感应圈(触发目标重武装)
|
||||
await setPlayer('PlazaScene', z.x, z.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(400);
|
||||
return drain(choiceKey);
|
||||
}
|
||||
async function shot(name, clip = null) {
|
||||
const box = await (await page.$('canvas')).boundingBox();
|
||||
const c = clip ? { x: box.x + clip.x, y: box.y + clip.y, width: clip.w, height: clip.h } : { x: box.x, y: box.y, width: box.width, height: box.height };
|
||||
await page.screenshot({ path: `${SHOTS}/${name}.png`, clip: c });
|
||||
}
|
||||
async function sleepNight() {
|
||||
await interactAt('house', 'interact', 'bed');
|
||||
await waitActive('DreamScene', 12000).catch(() => {});
|
||||
for (let i = 0; i < 30 && (await isActive('DreamScene')); i++) { await drain(); await sleep(500); }
|
||||
await waitActive('HouseScene', 12000);
|
||||
await sleep(500);
|
||||
}
|
||||
async function boardAnnounce(day) {
|
||||
const texts = await interactAt('plaza', 'interact', 'noticeboard');
|
||||
const found = texts.some((t) => t.includes(PRD_ANN[day - 1].slice(0, 8)));
|
||||
return { texts, found };
|
||||
}
|
||||
|
||||
// ---------- #1 标题 → 名字 → Day1 小屋 ----------
|
||||
console.log('== #1 标题与开局 ==');
|
||||
try {
|
||||
await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 15000 });
|
||||
await waitActive('TitleScene', 20000);
|
||||
await sleep(400);
|
||||
await shot('01_title');
|
||||
check('标题画面进入', true);
|
||||
const hasInput = await ev(() => !!document.querySelector('#game input[type="text"]'));
|
||||
check('名字输入框存在', hasInput);
|
||||
await ev(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await waitActive('HouseScene', 15000); await sleep(600);
|
||||
check('留空默认「小满」', (await dget('playerName')) === '小满');
|
||||
check('Day 1 小屋开始', (await dget('day')) === 1 && (await isActive('HouseScene')));
|
||||
await shot('02_house_d1');
|
||||
|
||||
// ---------- #2 移动/动画/碰撞/相机 ----------
|
||||
console.log('== #2 移动与碰撞 ==');
|
||||
{
|
||||
const p0 = await playerPos('HouseScene');
|
||||
await page.keyboard.down('KeyD'); await sleep(400); await page.keyboard.up('KeyD');
|
||||
const p1 = await playerPos('HouseScene');
|
||||
check('D 键右移', p1.x > p0.x + 10, `${p0.x}→${p1.x}`);
|
||||
await page.keyboard.down('KeyD'); await sleep(120);
|
||||
const anim = await ev(() => window.__game.scene.getScene('HouseScene').player.anims.currentAnim?.key);
|
||||
await page.keyboard.up('KeyD');
|
||||
check('四向动画 walk-right', anim === 'walk-right', String(anim));
|
||||
await sleep(200);
|
||||
const idleFrame = await ev(() => window.__game.scene.getScene('HouseScene').player.frame.name);
|
||||
check('待机帧(右=12)', Number(idleFrame) === 12, String(idleFrame));
|
||||
// 碰撞:向西推墙
|
||||
await setPlayer('HouseScene', 80, 90);
|
||||
await page.keyboard.down('KeyA'); await sleep(700); await page.keyboard.up('KeyA');
|
||||
const px = (await playerPos('HouseScene')).x;
|
||||
check('西墙碰撞拦截', px > 55, `x=${px}`);
|
||||
// 相机钳制(plaza 480×320 > 320×180)
|
||||
await startScene('PlazaScene');
|
||||
await setPlayer('PlazaScene', 8, 16);
|
||||
await sleep(400);
|
||||
const c1 = await ev(() => { const c = window.__game.scene.getScene('PlazaScene').cameras.main; return { x: c.scrollX, y: c.scrollY }; });
|
||||
await setPlayer('PlazaScene', 472, 312);
|
||||
await sleep(500);
|
||||
const c2 = await ev(() => { const c = window.__game.scene.getScene('PlazaScene').cameras.main; return { x: c.scrollX, y: c.scrollY }; });
|
||||
check('相机左上钳制', c1.x <= 0.5 && c1.y <= 0.5, JSON.stringify(c1));
|
||||
check('相机右下钳制', c2.x + 320 <= 480.5 && c2.y + 180 <= 320.5, JSON.stringify(c2));
|
||||
}
|
||||
|
||||
// ---------- #3 十场景可达 ----------
|
||||
console.log('== #3 十场景 ==');
|
||||
for (const [m, s] of Object.entries(SCENE)) {
|
||||
await startScene(s);
|
||||
check(`场景可达 ${s}`, await isActive(s));
|
||||
}
|
||||
await shot('03_plaza');
|
||||
|
||||
// ---------- 七日完整流程(覆盖 #5/#6/#11/#12/#13/#14/#16) ----------
|
||||
console.log('== 七日流程 ==');
|
||||
await startScene('HouseScene');
|
||||
|
||||
// --- Day 1 ---
|
||||
console.log('-- Day 1 --');
|
||||
{
|
||||
const b = await boardAnnounce(1);
|
||||
check('D1 公告逐字显示', b.found, b.texts[0]);
|
||||
// #12 前半:D5 前古井/雾墙劝离
|
||||
const w = await interactAt('well', 'interact', 'ancient_well');
|
||||
check('#12 古井 D5 前劝离', !(await darr('clues')).includes('C7') && w.length > 0);
|
||||
await startScene('PlazaScene');
|
||||
const fogDoor = maps.plaza.find((o) => o.name === 'door' && o.props.target === 'fog');
|
||||
await setPlayer('PlazaScene', fogDoor.x, fogDoor.y + 20);
|
||||
await drain();
|
||||
await page.keyboard.down('KeyW'); await sleep(700); await page.keyboard.up('KeyW');
|
||||
await sleep(400); await drain();
|
||||
check('#12 雾墙门 D5 前劝离(未进 FogScene)', !(await isActive('FogScene')));
|
||||
// T1 取面包 + 送三人(同时完成 T3)
|
||||
await interactAt('bakery', 'interact', 'counter_bread');
|
||||
check('T1 已取面包', await dget("taskProgress['breadTaken']"));
|
||||
await interactAt('plaza', 'npc', 'dabao');
|
||||
await interactAt('shop', 'npc', 'mimi');
|
||||
await interactAt('lake', 'npc', 'guiyeye');
|
||||
check('T1 完成 +15', (await darr('tasksDone')).includes('T1'));
|
||||
check('T3 问好完成 +10', (await darr('tasksDone')).includes('T3'));
|
||||
// T2 浇花 ×3(第三次浇花的对话收尾后同一批 drain 内就会弹出黄昏钟声)
|
||||
await interactAt('plaza', 'interact', 'flowerbed_1');
|
||||
await interactAt('plaza', 'interact', 'flowerbed_2');
|
||||
const fb3 = await interactAt('plaza', 'interact', 'flowerbed_3');
|
||||
check('T2 完成 +10', (await darr('tasksDone')).includes('T2'));
|
||||
// 黄昏钟声(任务完成触发,可能已并入上一步 drain 的文本里)
|
||||
await sleep(1200);
|
||||
const duskTexts = [...fb3, ...(await drain())];
|
||||
check('#5 黄昏钟声出现', duskTexts.some((t) => t.includes('钟声')), duskTexts.join('/').slice(0, 60));
|
||||
// 对话选项:温柔 +2
|
||||
const h0 = await dget('happiness');
|
||||
await interactAt('plaza', 'npc', 'afu', 'Digit1');
|
||||
check('温柔选项 +2', (await dget('happiness')) === h0 + 2, `${h0}→${await dget('happiness')}`);
|
||||
await drain();
|
||||
await sleepNight();
|
||||
check('D1→D2 睡眠过日', (await dget('day')) === 2);
|
||||
}
|
||||
|
||||
// --- Day 2 ---
|
||||
console.log('-- Day 2 --');
|
||||
{
|
||||
check('晨起于小屋', await isActive('HouseScene'));
|
||||
const b = await boardAnnounce(2);
|
||||
check('D2 公告逐字显示', b.found);
|
||||
// C1 日历(D2 起)
|
||||
await interactAt('house', 'interact', 'calendar');
|
||||
check('C1 日历 D2 可发现 +8', (await darr('clues')).includes('C1'));
|
||||
// T4:取信→送 3 人(第 3 封 C4)
|
||||
await interactAt('plaza', 'npc', 'dabao');
|
||||
check('T4 已取信 ×3', (await dget("taskProgress['letters']")) === 3);
|
||||
const dY = await interactAt('bakery', 'npc', 'yuanyuan');
|
||||
const dM = await interactAt('shop', 'npc', 'mimi');
|
||||
const dF = await interactAt('clinic', 'npc', 'fuyisheng');
|
||||
await sleep(700); // 第三封后 400ms 才弹 C4 对话,等它出现后关掉
|
||||
const c4t = await drain();
|
||||
const t4diag = `day=${await dget('day')} active=${await dget("isTaskActive('T4')")} letters=${await dget("taskProgress['letters']")} Y=${dY.join('|').slice(0, 40)} M=${dM.join('|').slice(0, 40)} F=${[...dF, ...c4t].join('|').slice(0, 60)}`;
|
||||
check('T4 完成 +15', (await darr('tasksDone')).includes('T4'), t4diag);
|
||||
check('C4 乱码署名 D2 当日发现', (await darr('clues')).includes('C4'));
|
||||
// T5 + C5
|
||||
await interactAt('shop', 'interact', 'shelf_count');
|
||||
check('T5 完成 +10', (await darr('tasksDone')).includes('T5'));
|
||||
check('C5 货架发现', (await darr('clues')).includes('C5'));
|
||||
// C3 邮筒(D2 起)
|
||||
await interactPlazaZone('mailbox_c3');
|
||||
check('C3 邮筒信件发现', (await darr('clues')).includes('C3'));
|
||||
// C9 第一段(D2)
|
||||
const c9d2 = await interactAt('lake', 'npc', 'guiyeye');
|
||||
check('C9 第一段 D2', (await darr('clues')).includes('C9'));
|
||||
check('C9 D2 台词「湖水一直很平静」', c9d2.some((t) => t.includes('湖水一直很平静')), c9d2.join('/').slice(0, 60));
|
||||
await sleepNight();
|
||||
check('D2→D3', (await dget('day')) === 3);
|
||||
}
|
||||
|
||||
// --- Day 3 ---
|
||||
console.log('-- Day 3 --');
|
||||
{
|
||||
const b = await boardAnnounce(3);
|
||||
check('D3 公告逐字显示', b.found);
|
||||
await interactAt('field', 'interact', 'berry_1');
|
||||
await interactAt('field', 'interact', 'berry_2');
|
||||
await interactAt('field', 'interact', 'berry_3');
|
||||
await interactAt('field', 'interact', 'berry_4');
|
||||
await interactAt('field', 'interact', 'berry_5');
|
||||
check('T6 完成 +10', (await darr('tasksDone')).includes('T6'));
|
||||
await interactAt('lake', 'interact', 'picnic_mat');
|
||||
check('T7 完成 +15', (await darr('tasksDone')).includes('T7'));
|
||||
await interactAt('bakery', 'interact', 'recipe_paper');
|
||||
check('C2 配方纸 D3 起', (await darr('clues')).includes('C2'));
|
||||
await sleepNight();
|
||||
check('D3→D4(梦境闪切已过)', (await dget('day')) === 4);
|
||||
}
|
||||
|
||||
// --- Day 4 ---
|
||||
console.log('-- Day 4 --');
|
||||
{
|
||||
const b = await boardAnnounce(4);
|
||||
check('D4 公告与 D1 逐字相同', b.found);
|
||||
await interactAt('bakery', 'interact', 'counter_bread');
|
||||
await interactAt('plaza', 'npc', 'dabao');
|
||||
await interactAt('shop', 'npc', 'mimi');
|
||||
await interactAt('lake', 'npc', 'guiyeye'); // C9 第二段(D4)+ T10 计数
|
||||
check('T8 完成', (await darr('tasksDone')).includes('T8'));
|
||||
check('T10 完成', (await darr('tasksDone')).includes('T10'));
|
||||
await interactAt('plaza', 'interact', 'flowerbed_1');
|
||||
await interactAt('plaza', 'interact', 'flowerbed_2');
|
||||
await interactAt('plaza', 'interact', 'flowerbed_3');
|
||||
check('T9 完成', (await darr('tasksDone')).includes('T9'));
|
||||
// C9 第二段(D4):T8 完成后再与龟爷爷对话
|
||||
const c9d4 = await interactAt('lake', 'npc', 'guiyeye');
|
||||
check('C9 D4 台词「面包一直是那个味道」', c9d4.some((t) => t.includes('面包一直是那个味道')), c9d4.join('/').slice(0, 60));
|
||||
await sleepNight();
|
||||
check('D4→D5', (await dget('day')) === 5);
|
||||
}
|
||||
|
||||
// --- Day 5 ---
|
||||
console.log('-- Day 5 --');
|
||||
{
|
||||
const b = await boardAnnounce(5);
|
||||
check('D5 公告逐字显示', b.found);
|
||||
// T11 拒绝(悲伤行为:拒绝任务)
|
||||
const aw0 = await dget('awareness');
|
||||
await interactAt('bakery', 'npc', 'yuanyuan', 'Digit2');
|
||||
check('T11 可拒绝且被记录', (await darr('refusedTasks')).includes('T11'));
|
||||
check('#7 拒绝任务计悲伤(觉知+3)', (await dget('awareness')) >= aw0 + 3, `${aw0}→${await dget('awareness')}`);
|
||||
// T12 接受(C6)
|
||||
await interactPlazaZone('noticeboard', 'Digit1');
|
||||
check('T12 完成 + C6 旧字迹', (await darr('tasksDone')).includes('T12') && (await darr('clues')).includes('C6'));
|
||||
// #12 后半:D5 起古井可交互(C7,聆听 5 秒)
|
||||
{
|
||||
const sceneKey = 'WellScene';
|
||||
if (!(await isActive(sceneKey))) await startScene(sceneKey);
|
||||
await drain();
|
||||
const obj = maps.well.find((o) => o.name === 'interact' && o.props.id === 'ancient_well');
|
||||
await setPlayer(sceneKey, obj.x, obj.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(600); await drain();
|
||||
await page.waitForFunction(() => window.__director.clues.has('C7'), { timeout: 15000 });
|
||||
await drain();
|
||||
check('#12 古井 D5 起可交互', true);
|
||||
check('C7 井底回声 +8', (await darr('clues')).includes('C7'));
|
||||
}
|
||||
// C8 诊所
|
||||
await interactAt('clinic', 'interact', 'doctor_desk');
|
||||
check('C8 诊所表格 D5 起', (await darr('clues')).includes('C8'));
|
||||
await sleepNight();
|
||||
check('D5→D6', (await dget('day')) === 6);
|
||||
}
|
||||
|
||||
// --- Day 6(#13 T14、#14 雨)---
|
||||
console.log('-- Day 6 --');
|
||||
{
|
||||
const b = await boardAnnounce(6);
|
||||
check('D6 公告逐字显示', b.found);
|
||||
await interactPlazaZone('ribbon_1');
|
||||
await interactPlazaZone('ribbon_2');
|
||||
await interactPlazaZone('ribbon_3');
|
||||
await interactPlazaZone('ribbon_4');
|
||||
check('T13 完成 +10', (await darr('tasksDone')).includes('T13'));
|
||||
// C9 第三段(D6)
|
||||
const c9d6 = await interactAt('lake', 'npc', 'guiyeye');
|
||||
check('C9 三段递进完成', (await darr('clues')).includes('C9'));
|
||||
check('C9 D6 台词「孩子,今天是第几个今天?」', c9d6.some((t) => t.includes('孩子,今天是第几个今天?')), c9d6.join('/').slice(0, 60));
|
||||
// T14:保持微笑 60 秒
|
||||
{
|
||||
await startScene('PlazaScene');
|
||||
await drain();
|
||||
const z = plazaZone('smile_spot');
|
||||
await setPlayer('PlazaScene', z.x, z.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(600); await drain();
|
||||
check('T14 开始(t14Active)', await dget('t14Active'));
|
||||
await sleep(10000);
|
||||
const zoom1 = await ev(() => window.__game.scene.getScene('PlazaScene').cameras.main.zoom);
|
||||
check('#13 镜头缓慢拉近', zoom1 > 1.02, `zoom=${zoom1}`);
|
||||
const staring = await ev(() => window.__game.scene.getScene('PlazaScene').actors.some((a) => a.stareAtPlayer));
|
||||
check('#13 全村注视', staring);
|
||||
await sleep(35000); // t≈45s:渐隐已开始
|
||||
const g1 = await ev(() => window.__audio.master.gain.value);
|
||||
await sleep(6000); // t≈51s
|
||||
const g2 = await ev(() => window.__audio.master.gain.value);
|
||||
check('#13 第 40–60 秒声音渐隐至零', g2 < g1 && g2 < 0.3, `gain ${g1}→${g2}`);
|
||||
await shot('13_t14_smile');
|
||||
await page.waitForFunction(() => window.__director.tasksDone.has('T14'), { timeout: 40000 });
|
||||
await drain();
|
||||
check('T14 完成 +15', true);
|
||||
check('#13 T14 后脚本雨开始', await dget('rainActive'));
|
||||
}
|
||||
// #14 站雨中 ≥10 秒计悲伤(先把幸福调到 50,避开满格溢出池机制)
|
||||
{
|
||||
await dset('setHappiness(50)');
|
||||
const h0 = await dget('happiness'), aw0 = await dget('awareness');
|
||||
await drain();
|
||||
await setPlayer('PlazaScene', 240, 260);
|
||||
await sleep(11000);
|
||||
check('#14 雨中站 10 秒计悲伤', (await dget('happiness')) <= h0 - 5 && (await dget('awareness')) >= aw0 + 3, `h ${h0}→${await dget('happiness')} aw ${aw0}→${await dget('awareness')}`);
|
||||
// NPC 台词对雨零反应:与阿福对话正常弹出
|
||||
const t = await interactAt('plaza', 'npc', 'afu');
|
||||
check('#14 雨中 NPC 台词零反应(正常对话)', t.length > 0);
|
||||
await shot('14_rain');
|
||||
}
|
||||
await sleepNight();
|
||||
check('D6→D7', (await dget('day')) === 7);
|
||||
}
|
||||
|
||||
// --- Day 7(E3 真结局路线)---
|
||||
console.log('-- Day 7 --');
|
||||
{
|
||||
const b = await boardAnnounce(7);
|
||||
check('D7 公告逐字显示', b.found);
|
||||
const aw = await dget('awareness'), nc = (await darr('clues')).length;
|
||||
check('E3 条件就绪(觉知≥70 线索≥6)', aw >= 70 && nc >= 6, `aw=${aw} clues=${nc}`);
|
||||
// 走向雾墙
|
||||
await startScene('FogScene');
|
||||
await drain();
|
||||
const fw = maps.fog.find((o) => o.name === 'interact' && o.props.id === 'fog_wall');
|
||||
await setPlayer('FogScene', fw.x, fw.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(600); await drain();
|
||||
await waitActive('WhiteScene', 15000);
|
||||
check('E3:雾墙裂开成白色走廊', true);
|
||||
await shot('15_white_corridor');
|
||||
// 走进病房
|
||||
await setPlayer('WhiteScene', 72, 144);
|
||||
await waitActive('EndingScene', 15000);
|
||||
await sleep(2500);
|
||||
await shot('15_e3_record');
|
||||
const e3texts = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
check('#15 E3 病历:蜜糖谷情绪疗养中心', e3texts.includes('蜜糖谷情绪疗养中心'));
|
||||
check('#15 E3 病历:姓名=小满', e3texts.includes('小满'));
|
||||
check('#15 E3 病历:PT-07', e3texts.includes('PT-07'));
|
||||
check('#15 E3 病历:真实游玩时长', /\d{1,2}:\d{2}/.test(e3texts), e3texts.slice(0, 120));
|
||||
await sleep(5500); // 等到最后一帧
|
||||
const hasReal = await ev(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.texture && o.texture.key === 'portrait_player_real'));
|
||||
check('#15 E3 最后一帧:真实的脸', hasReal);
|
||||
await shot('15_e3_realface');
|
||||
}
|
||||
console.log('== 阶段 A(七日流程)完成 ==');
|
||||
|
||||
// ================= 阶段 B:重开页面做 Debug/矩阵/悲伤/庆典/结局 =================
|
||||
console.log('== 阶段 B ==');
|
||||
await page.goto(BASE, { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 15000 });
|
||||
await waitActive('TitleScene', 20000);
|
||||
await ev(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await waitActive('HouseScene', 15000); await sleep(400);
|
||||
|
||||
// ---------- #13(Debug 面板,PRD §13) ----------
|
||||
console.log('== Debug 面板 ==');
|
||||
{
|
||||
const hidden = await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); return p ? getComputedStyle(p).display === 'none' : null; });
|
||||
check('Debug 默认隐藏无痕迹', hidden === true);
|
||||
await tap('Backquote'); await sleep(200);
|
||||
const shown = await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); return p ? getComputedStyle(p).display !== 'none' : false; });
|
||||
check('反引号打开 Debug 面板', shown);
|
||||
// 设幸福 66(数值行靠「设」按钮生效)
|
||||
await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); const input = p.querySelector('input'); input.value = '66'; input.closest('div').querySelector('button').click(); });
|
||||
await sleep(200);
|
||||
check('Debug 设幸福 66', (await dget('happiness')) === 66);
|
||||
// 传送 bakery
|
||||
await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); const sel = p.querySelector('select'); sel.value = 'BakeryScene'; [...p.querySelectorAll('button')].find((b) => b.textContent === '传送').click(); });
|
||||
await waitActive('BakeryScene', 8000);
|
||||
check('Debug 传送到面包房', true);
|
||||
// 置齐线索
|
||||
await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); [...p.querySelectorAll('button')].find((b) => b.textContent.includes('线索')).click(); });
|
||||
check('Debug 置齐全部线索', ((await darr('clues')).length) === 9);
|
||||
await tap('Backquote'); await sleep(150);
|
||||
}
|
||||
|
||||
// ---------- #9 侵蚀矩阵 ----------
|
||||
console.log('== #9 侵蚀矩阵 ==');
|
||||
{
|
||||
await startScene('PlazaScene');
|
||||
const cases = [
|
||||
[10, 0, 0.9, 1.0], [40, 1, 1.0, 1.0], [70, 2, 1.15, 0.97], [90, 3, 1.3, 0.92], [100, 4, 1.4, 0.84],
|
||||
];
|
||||
for (const [h, st, sat, rate] of cases) {
|
||||
await dset(`setHappiness(${h})`); await sleep(400);
|
||||
const s = await dget('stage');
|
||||
const p = await dget('params');
|
||||
check(`幸福${h}→S${st}`, s === st, `stage=${s}`);
|
||||
check(`S${st} 饱和度${sat}/速率${rate}`, Math.abs(p.saturation - sat) < 1e-6 && Math.abs(p.playbackRate - rate) < 1e-6, JSON.stringify(p));
|
||||
await shot(`09_stage_S${st}_h${h}`);
|
||||
}
|
||||
// 低通与鸟鸣
|
||||
const p3 = await dget('params'); // S4
|
||||
check('S4 低通 4kHz + 鸟鸣静音', p3.lowpass === 4000 && p3.birdMuted === true);
|
||||
await dset('setHappiness(90)'); await sleep(300);
|
||||
const p2 = await dget('params');
|
||||
check('S3 低通 8kHz', p2.lowpass === 8000);
|
||||
// NPC 表情档:h=80 时 咪咪(-10→S3 flat) 与 阿福(0→S2 smile) 不同;圆圆(+15→S2) 与阈值一致
|
||||
await dset('setHappiness(80)'); await sleep(500);
|
||||
await startScene('ShopScene');
|
||||
const mimiFrame = await ev(() => { const a = window.__game.scene.getScene('ShopScene').actors.find((x) => x.def.id === 'mimi'); return a ? a.sprite.frame.name : -1; });
|
||||
await startScene('PlazaScene');
|
||||
const afuFrame = await ev(() => { const a = window.__game.scene.getScene('PlazaScene').actors.find((x) => x.def.id === 'afu'); return a ? a.sprite.frame.name : -1; });
|
||||
check('h=80 咪咪比阿福先崩(flat≠smile)', String(mimiFrame) !== String(afuFrame), `mimi=${mimiFrame} afu=${afuFrame}`);
|
||||
// S4 花瓣过亮 + 黑籽(视觉)+ 双子鸟张嘴动画仍在
|
||||
await dset('setHappiness(100)'); await sleep(400);
|
||||
const birdAnim = await ev(() => { const a = window.__game.scene.getScene('PlazaScene').actors.find((x) => x.def.id === 'bird'); return a ? (a.sprite.anims.isPlaying || a.sprite.anims.currentAnim !== null) : false; });
|
||||
check('S4 双子鸟动画仍在(鸟鸣静音)', birdAnim);
|
||||
await shot('09_hud_s4', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 });
|
||||
await dset('setHappiness(10)'); await sleep(300);
|
||||
await shot('09_hud_s0', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 });
|
||||
await dset('setHappiness(70)'); await sleep(300);
|
||||
await shot('09_hud_s2', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 });
|
||||
}
|
||||
|
||||
// ---------- #4 向日葵表盘 ----------
|
||||
console.log('== #4 向日葵表盘 ==');
|
||||
{
|
||||
const pc = (h) => ev((hh) => window.__petalCount(hh), h);
|
||||
check('#4 h=10 → 0–2 瓣', (await pc(10)) >= 0 && (await pc(10)) <= 2, `petals=${await pc(10)}`);
|
||||
check('#4 h=40 → 3–6 瓣', (await pc(40)) >= 3 && (await pc(40)) <= 6, `petals=${await pc(40)}`);
|
||||
check('#4 h=70 → 7–9 瓣', (await pc(70)) >= 7 && (await pc(70)) <= 9, `petals=${await pc(70)}`);
|
||||
check('#4 h=90 → 10–12 瓣', (await pc(90)) >= 10 && (await pc(90)) <= 12, `petals=${await pc(90)}`);
|
||||
check('#4 h=100 → 12 瓣', (await pc(100)) === 12);
|
||||
await dset('setHappiness(100)'); await sleep(300);
|
||||
check('#4 满格花心黑籽', await ev(() => window.__game.scene.getScene('UIScene').seedsShown));
|
||||
await dset('setHappiness(50)'); await sleep(300);
|
||||
check('#4 未满格无黑籽', !(await ev(() => window.__game.scene.getScene('UIScene').seedsShown)));
|
||||
// 表盘区域(左上 120×120)不得出现任何 Text(无数字)
|
||||
const textsNearDial = await ev(() => window.__game.scene.getScene('UIScene').children.list
|
||||
.filter((o) => o.type === 'Text' && o.visible && o.x < 120 && o.y < 120).length);
|
||||
check('#4 表盘无数字显示', textsNearDial === 0, `文本对象 ${textsNearDial} 个`);
|
||||
}
|
||||
|
||||
// ---------- #16 时长设计(每日 5 分钟兜底,7 天 ≤35 分钟) ----------
|
||||
console.log('== #16 时长 ==');
|
||||
{
|
||||
const srcOk = (() => { const m = /DAY_LENGTH_MS = (\d+) \* 60 \* 1000/.exec(read('src/core/CorruptionDirector.ts')); return m ? Number(m[1]) : 0; })();
|
||||
check('#16 每日兜底 5 分钟(7 天 ≤35 分钟)', srcOk === 5, `DAY_LENGTH_MS=${srcOk}min`);
|
||||
await dset('dayElapsedMs = 5 * 60 * 1000 + 1');
|
||||
check('#16 计时到点可睡觉(黄昏兜底)', await dget('canSleep'));
|
||||
await dset('dayElapsedMs = 0');
|
||||
}
|
||||
|
||||
// ---------- #10 错字替换(运行时) ----------
|
||||
console.log('== #10 运行时文本腐蚀 ==');
|
||||
{
|
||||
await dset('setHappiness(100)'); await sleep(300);
|
||||
await startScene('ShopScene');
|
||||
let found = '';
|
||||
const corruptedChars = PAIRS.map((p) => p[1]);
|
||||
for (let i = 0; i < 8 && !found; i++) {
|
||||
const texts = await interactAt('shop', 'npc', 'mimi');
|
||||
for (const t of texts) {
|
||||
if (corruptedChars.some((c) => t.includes(c)) || /[!?。~]{3}$/.test(t) || /(.)\1{2}$/.test(t)) { found = t; break; }
|
||||
}
|
||||
}
|
||||
check('#10 S4 咪咪台词可见腐蚀/标点×3', found !== '', found || '8 次采样未见');
|
||||
}
|
||||
|
||||
// ---------- #7 悲伤行为:发呆 / 纠缠 / 否定 ----------
|
||||
console.log('== #7 悲伤行为 ==');
|
||||
{
|
||||
await dset('setHappiness(60)'); await dset('setAwareness(0)');
|
||||
await startScene('PlazaScene');
|
||||
await drain();
|
||||
// 发呆 10 秒(远离任务点)
|
||||
await setPlayer('PlazaScene', 60, 280);
|
||||
const glanceP = ev(() => new Promise((r) => window.__director.once('npcGlance', () => r(true))));
|
||||
await sleep(11000);
|
||||
check('#7 发呆:幸福 −5', (await dget('happiness')) === 55, String(await dget('happiness')));
|
||||
check('#7 发呆:觉知 +3', (await dget('awareness')) >= 3, String(await dget('awareness')));
|
||||
check('#7 最近 NPC 转头', await Promise.race([glanceP, sleep(1500).then(() => 'timeout')]) === true);
|
||||
// 否定选项
|
||||
const aw1 = await dget('awareness'), h1 = await dget('happiness');
|
||||
await interactAt('plaza', 'npc', 'afu', 'Digit3');
|
||||
check('#7 否定选项:觉知+2 且计悲伤', (await dget('awareness')) >= aw1 + 5 && (await dget('happiness')) <= h1 - 5, `aw ${aw1}→${await dget('awareness')}`);
|
||||
// 纠缠:同一天对同一 NPC 对话 ≥5 次
|
||||
const aw2 = await dget('awareness');
|
||||
for (let i = 0; i < 5; i++) await interactAt('plaza', 'npc', 'dabao', 'Digit1');
|
||||
check('#7 纠缠 ≥5 次计悲伤(觉知+3)', (await dget('awareness')) >= aw2 + 3, `aw ${aw2}→${await dget('awareness')}`);
|
||||
}
|
||||
|
||||
// ---------- #8 欢乐庆典 ----------
|
||||
console.log('== #8 欢乐庆典 ==');
|
||||
{
|
||||
await dset('setHappiness(20)');
|
||||
await startScene('PlazaScene');
|
||||
await drain();
|
||||
await ev(() => { for (let i = 0; i < 260; i++) window.__director.tick(500); }); // 模拟 130 秒
|
||||
await sleep(400);
|
||||
check('#8 幸福<30 持续 2 分钟触发庆典', await dget('celebrationActive'));
|
||||
await drain();
|
||||
// 逃离:走门离开 → 计数
|
||||
const esc0 = await dget('escapedCelebrations');
|
||||
const door = maps.plaza.find((o) => o.name === 'door' && o.props.target === 'house');
|
||||
await setPlayer('PlazaScene', door.x, door.y + 18);
|
||||
await page.keyboard.down('KeyW'); await sleep(800); await page.keyboard.up('KeyW');
|
||||
await sleep(700); await drain();
|
||||
check('#8 从场景边缘走离逃脱且计数', (await dget('escapedCelebrations')) > esc0, `esc ${esc0}→${await dget('escapedCelebrations')}`);
|
||||
// 再次触发并不逃离 → 拉回 50
|
||||
await dset('setHappiness(20)');
|
||||
await startScene('PlazaScene'); await drain();
|
||||
await ev(() => { for (let i = 0; i < 260; i++) window.__director.tick(500); });
|
||||
await sleep(300); await drain();
|
||||
await ev(() => window.__director.completeCelebration());
|
||||
await drain();
|
||||
check('#8 未逃离则拉回 50', (await dget('happiness')) === 50, String(await dget('happiness')));
|
||||
}
|
||||
|
||||
// ---------- #15 强制结局 E1/E2/E4(Debug 面板按钮) ----------
|
||||
console.log('== #15 强制结局 ==');
|
||||
async function forceEndingViaPanel(e) {
|
||||
await tap('Backquote'); await sleep(150);
|
||||
await ev((e) => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((x) => x.textContent.includes('DEBUG')); [...p.querySelectorAll('button')].find((b) => b.textContent === `强制 ${e}`).click(); }, e);
|
||||
await waitActive('EndingScene', 8000);
|
||||
await tap('Backquote'); await sleep(100);
|
||||
}
|
||||
{
|
||||
await forceEndingViaPanel('E1');
|
||||
await sleep(7200);
|
||||
const t1 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
await shot('15_e1');
|
||||
check('E1 字幕逐字', t1.includes('从此,每一天都很幸福。') && t1.includes('每一天。'), t1.slice(0, 80));
|
||||
const smile = await ev(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.texture && o.texture.key === 'portrait_player_smile'));
|
||||
check('E1 主角头像仍是微笑', smile);
|
||||
|
||||
await forceEndingViaPanel('E2');
|
||||
await sleep(5200);
|
||||
const t2 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
await shot('15_e2');
|
||||
check('E2 字幕逐字', t2.includes('疗程重新开始。'), t2.slice(0, 80));
|
||||
|
||||
await forceEndingViaPanel('E4');
|
||||
await sleep(6500);
|
||||
const t4 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
await shot('15_e4');
|
||||
check('E4 字幕逐字', t4.includes('系统检测到疗程无效。'), t4.slice(0, 80));
|
||||
// E3 也可由 Debug 强制触发
|
||||
await forceEndingViaPanel('E3');
|
||||
await sleep(2500);
|
||||
const t3 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
check('Debug 强制 E3 病历含机构/编号', t3.includes('蜜糖谷情绪疗养中心') && t3.includes('PT-07'), t3.slice(0, 80));
|
||||
// 回到标题按钮(等待其出现)
|
||||
await page.waitForFunction(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.type === 'Text' && o.text.includes('回到标题')), { timeout: 25000 });
|
||||
await ev(() => { const b = window.__game.scene.getScene('EndingScene').children.list.find((o) => o.type === 'Text' && o.text.includes('回到标题')); b.emit('pointerdown'); });
|
||||
await waitActive('TitleScene', 8000);
|
||||
check('结局后「回到标题」可用', true);
|
||||
}
|
||||
|
||||
// ---------- #17 音频 ----------
|
||||
console.log('== #17 音频 ==');
|
||||
{
|
||||
const a = await ev(() => {
|
||||
const e = window.__audio;
|
||||
return e && e.ctx ? { state: e.ctx.state, rate: e.rate } : null;
|
||||
});
|
||||
check('#17 AudioContext 运行中(程序合成 BGM)', !!a && a.state === 'running', JSON.stringify(a));
|
||||
await dset('setHappiness(100)'); await sleep(1200);
|
||||
const s4 = await ev(() => ({ rate: window.__audio.rate, lp: window.__audio.lp.frequency.value, bird: window.__audio.birdBus.gain.value }));
|
||||
check('#17 S4:速率 0.84 / 低通→4kHz / 鸟鸣静音', Math.abs(s4.rate - 0.84) < 1e-6 && s4.lp < 8000 && s4.bird < 0.1, JSON.stringify(s4));
|
||||
await dset('setHappiness(30)'); await sleep(1200);
|
||||
const s1 = await ev(() => ({ rate: window.__audio.rate, lp: window.__audio.lp.frequency.value, bird: window.__audio.birdBus.gain.value }));
|
||||
check('#17 S1:原速原调、鸟鸣恢复', Math.abs(s1.rate - 1) < 1e-6 && s1.lp > 15000 && s1.bird > 0.3, JSON.stringify(s1));
|
||||
}
|
||||
|
||||
// ---------- #18 缩放与零报错 ----------
|
||||
console.log('== #18 缩放 ==');
|
||||
{
|
||||
await page.setViewport({ width: 1000, height: 700 }); await sleep(300);
|
||||
const s1 = await ev(() => ({ w: document.querySelector('canvas').style.width, h: document.querySelector('canvas').style.height }));
|
||||
check('1000×700 → 整数倍 3×(960×540)', s1.w === '960px' && s1.h === '540px', JSON.stringify(s1));
|
||||
await page.setViewport({ width: 500, height: 500 }); await sleep(300);
|
||||
const s2 = await ev(() => ({ w: document.querySelector('canvas').style.width, h: document.querySelector('canvas').style.height }));
|
||||
check('500×500 → 整数倍 1×(320×180)', s2.w === '320px' && s2.h === '180px', JSON.stringify(s2));
|
||||
await page.setViewport({ width: 960, height: 560 }); await sleep(300);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log('!! 脚本异常中断:', e.message);
|
||||
console.log('已收集控制台错误:', consoleErrors.slice(0, 10).join(' | ') || '(无)');
|
||||
fail++; failures.push('脚本异常中断: ' + e.message);
|
||||
}
|
||||
|
||||
// ---------- 汇总 ----------
|
||||
console.log('== 汇总 ==');
|
||||
check('无外部网络请求', extReqs.length === 0, extReqs.slice(0, 3).join(','));
|
||||
check('全程控制台零报错', consoleErrors.length === 0, consoleErrors.slice(0, 5).join(' | '));
|
||||
console.log(`\n通过 ${pass} / ${pass + fail}`);
|
||||
if (failures.length) { console.log('失败项:'); failures.forEach((f) => console.log(' - ' + f)); }
|
||||
await browser.close();
|
||||
process.exit(fail ? 1 : 0);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- gen_all.lua — regenerate every original pixel asset for 《崩坏童话:蜜糖村》
|
||||
-- Run: /Applications/Aseprite.app/Contents/MacOS/aseprite -b --script tools/aseprite/gen_all.lua
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
dofile(ROOT .. "tools/aseprite/gen_tileset.lua")
|
||||
dofile(ROOT .. "tools/aseprite/gen_player.lua")
|
||||
dofile(ROOT .. "tools/aseprite/gen_npcs.lua")
|
||||
dofile(ROOT .. "tools/aseprite/gen_extras.lua")
|
||||
dofile(ROOT .. "tools/aseprite/gen_portraits.lua")
|
||||
print("ALL_ASSETS_DONE")
|
||||
@@ -0,0 +1,77 @@
|
||||
-- gen_extras.lua — 5 background critters (each 32x24: 2-frame walk loop)
|
||||
-- squirrel / hedgehog / fox / pig / chick (profile, facing left)
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
local function squirrel(t, f)
|
||||
t.discn(11, 9, 4, "honey_d"); t.discn(12, 6, 3, "honey"); t.pn(13, 4, "honey_l") -- bushy tail
|
||||
t.ellipsen(6, 15, 5, 4, "honey")
|
||||
t.ellipsen(6, 16, 3, 2, "cream")
|
||||
t.discn(4, 10, 3, "honey")
|
||||
t.pn(2, 7, "honey"); t.pn(3, 7, "honey"); t.pn(2, 8, "honey_d") -- ear
|
||||
t.pn(2, 10, "ink"); t.pn(1, 12, "brown_d") -- eye / nose
|
||||
if f == 0 then t.rectn(4, 19, 2, 3, "brown_d"); t.rectn(8, 19, 2, 3, "brown_d")
|
||||
else t.rectn(3, 19, 2, 3, "brown_d"); t.rectn(9, 19, 2, 2, "brown_d") end
|
||||
end
|
||||
|
||||
local function hedgehog(t, f)
|
||||
t.discn(8, 13, 7, "brown_d") -- spiny body
|
||||
t.discn(8, 13, 6, "brown_l")
|
||||
for _, p in ipairs({ { 4, 8 }, { 7, 7 }, { 10, 8 }, { 12, 10 }, { 5, 11 }, { 9, 11 }, { 12, 13 } }) do
|
||||
t.pn(p[1], p[2], "brown_dd") -- spines
|
||||
end
|
||||
t.discn(3, 14, 3, "cream_d") -- face
|
||||
t.pn(2, 13, "ink"); t.pn(0, 15, "brown_dd") -- eye / nose
|
||||
t.pn(3, 16, "brown_d")
|
||||
if f == 0 then t.rectn(5, 19, 2, 2, "brown_d"); t.rectn(9, 19, 2, 2, "brown_d")
|
||||
else t.rectn(4, 19, 2, 2, "brown_d"); t.rectn(10, 19, 2, 2, "brown_d") end
|
||||
end
|
||||
|
||||
local function fox(t, f)
|
||||
t.discn(12, 13, 4, "honey_d"); t.pn(14, 12, "cream"); t.pn(15, 13, "cream") -- tail, white tip
|
||||
t.ellipsen(7, 15, 5, 4, "honey") -- body
|
||||
t.rectn(3, 6, 2, 3, "honey"); t.rectn(6, 6, 2, 3, "honey") -- ears
|
||||
t.pn(3, 6, "brown_d"); t.pn(6, 6, "brown_d")
|
||||
t.discn(4, 10, 4, "honey") -- head
|
||||
t.rectn(1, 11, 3, 3, "honey") -- snout
|
||||
t.pn(1, 12, "ink"); t.pn(3, 9, "ink") -- nose / eye
|
||||
t.rectn(1, 13, 3, 2, "cream") -- cheek
|
||||
if f == 0 then t.rectn(5, 19, 2, 3, "brown_d"); t.rectn(9, 19, 2, 3, "brown_d")
|
||||
else t.rectn(4, 19, 2, 3, "brown_d"); t.rectn(10, 19, 2, 2, "brown_d") end
|
||||
end
|
||||
|
||||
local function pig(t, f)
|
||||
t.ellipsen(8, 14, 6, 5, "pink") -- round body
|
||||
t.pn(13, 12, "pink_d"); t.pn(14, 11, "pink_d"); t.pn(14, 13, "pink_d") -- curly tail
|
||||
t.discn(4, 10, 4, "pink") -- head
|
||||
t.rectn(2, 6, 2, 3, "pink"); t.rectn(6, 6, 2, 3, "pink"); t.pn(2, 6, "pink_d"); t.pn(6, 6, "pink_d") -- ears
|
||||
t.rectn(1, 11, 3, 3, "pink_d") -- snout
|
||||
t.pn(2, 12, "pink_dd"); t.pn(3, 12, "pink_dd") -- nostrils
|
||||
t.pn(3, 9, "ink") -- eye
|
||||
if f == 0 then t.rectn(5, 19, 2, 3, "pink_d"); t.rectn(9, 19, 2, 3, "pink_d")
|
||||
else t.rectn(4, 19, 2, 3, "pink_d"); t.rectn(10, 19, 2, 2, "pink_d") end
|
||||
end
|
||||
|
||||
local function chick(t, f)
|
||||
t.discn(8, 13, 5, "yellow") -- body
|
||||
t.discn(5, 9, 3, "yellow") -- head
|
||||
t.pn(5, 6, "yellow"); t.pn(6, 7, "yellow") -- tuft
|
||||
t.discn(9, 13, 2, "yellow_d") -- wing
|
||||
t.pn(2, 9, "honey"); t.pn(1, 9, "honey_d") -- beak
|
||||
t.pn(4, 8, "ink") -- eye
|
||||
t.pn(11, 11, "yellow_d"); t.pn(12, 10, "yellow_d") -- tail
|
||||
if f == 0 then t.vln(6, 18, 3, "honey_d"); t.vln(9, 18, 3, "honey_d")
|
||||
else t.vln(5, 18, 3, "honey_d"); t.vln(10, 18, 2, "honey_d") end
|
||||
end
|
||||
|
||||
local extras = {
|
||||
{ "squirrel", squirrel }, { "hedgehog", hedgehog }, { "fox", fox },
|
||||
{ "pig", pig }, { "chick", chick },
|
||||
}
|
||||
for _, n in ipairs(extras) do
|
||||
local spr, img = H.new(32, 24)
|
||||
n[2](H.at(img, 0, 0), 0)
|
||||
n[2](H.at(img, 16, 0), 1)
|
||||
spr:saveAs(ROOT .. "public/assets/sprites/extra_" .. n[1] .. ".png")
|
||||
print("extra_" .. n[1] .. ".png done")
|
||||
end
|
||||
@@ -0,0 +1,141 @@
|
||||
-- gen_npcs.lua — named NPC sprites (each 48x24: 3 frames = smile / flat / off)
|
||||
-- plus npc_bird.png (32x24: 2 frames = beak closed / open)
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
-- ---- shared expression core ------------------------------------------------
|
||||
-- eyes at (lx,ly)/(rx,ry); mouth baseline my. ex in {"smile","flat","off"}.
|
||||
-- "off" = smile mouth but the right eye sits 1px lower (a 1-2px difference).
|
||||
local function expr(t, ex, lx, ly, rx, ry, my, eyeCol, mouthCol)
|
||||
eyeCol = eyeCol or "ink"; mouthCol = mouthCol or "brown_d"
|
||||
if ex == "off" then ry = ry + 1 end
|
||||
t.pn(lx, ly, eyeCol); t.pn(rx, ry, eyeCol)
|
||||
if ex == "flat" then
|
||||
t.hln(lx + 1, my, rx - lx - 1, mouthCol)
|
||||
else
|
||||
t.pn(lx + 1, my, mouthCol); t.pn(lx + 2, my + 1, mouthCol)
|
||||
t.pn(rx - 2, my + 1, mouthCol); t.pn(rx - 1, my, mouthCol)
|
||||
end
|
||||
end
|
||||
|
||||
local function arms(t, c) t.rectn(2, 12, 2, 5, c); t.rectn(12, 12, 2, 5, c) end
|
||||
local function feet(t, c) t.rectn(5, 20, 2, 3, c); t.rectn(9, 20, 2, 3, c) end
|
||||
|
||||
-- ---- species bodies (face forward, 16x24 local) ---------------------------
|
||||
local function afu(t, ex) -- 狗村长 dog village head
|
||||
t.rectn(1, 3, 3, 6, "brown_d"); t.rectn(12, 3, 3, 6, "brown_d") -- floppy ears
|
||||
t.rectn(1, 3, 3, 2, "brown"); t.rectn(12, 3, 3, 2, "brown")
|
||||
t.rectn(3, 3, 10, 9, "brown"); t.rectn(3, 3, 10, 1, "brown_l")
|
||||
t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink") -- muzzle + nose
|
||||
t.rectn(4, 12, 8, 7, "brown"); t.rectn(4, 12, 8, 3, "honey") -- vest
|
||||
t.pn(7, 13, "honey_d"); t.pn(8, 13, "honey_d")
|
||||
arms(t, "brown"); feet(t, "brown_d")
|
||||
expr(t, ex, 5, 6, 10, 6, 10)
|
||||
end
|
||||
|
||||
local function yuanyuan(t, ex) -- 兔面包师 rabbit baker
|
||||
t.rectn(4, 0, 2, 5, "cream"); t.rectn(10, 0, 2, 5, "cream") -- long ears
|
||||
t.rectn(5, 1, 1, 3, "pink"); t.rectn(10, 1, 1, 3, "pink") -- inner ear
|
||||
t.rectn(3, 4, 10, 8, "cream"); t.rectn(3, 4, 10, 1, "white")
|
||||
t.pn(7, 9, "pink_d") -- bunny nose
|
||||
t.rectn(4, 12, 8, 7, "pink") -- dress
|
||||
t.rectn(4, 12, 8, 5, "white") -- apron
|
||||
t.rectn(4, 12, 8, 1, "cream_d")
|
||||
t.pn(6, 14, "cream_d"); t.pn(9, 14, "cream_d")
|
||||
arms(t, "cream"); feet(t, "cream_d")
|
||||
expr(t, ex, 5, 7, 10, 7, 10)
|
||||
end
|
||||
|
||||
local function dabao(t, ex) -- 熊邮差 bear postman
|
||||
t.discn(4, 3, 2, "brown"); t.discn(11, 3, 2, "brown") -- round ears
|
||||
t.discn(4, 3, 1, "brown_d"); t.discn(11, 3, 1, "brown_d")
|
||||
t.rectn(2, 3, 12, 9, "brown"); t.rectn(2, 3, 12, 1, "brown_l") -- big head
|
||||
t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink")
|
||||
t.rectn(3, 12, 10, 7, "brown") -- big body
|
||||
t.pn(4, 12, "brown_d"); t.pn(5, 13, "brown_d"); t.pn(6, 14, "brown_d")
|
||||
t.pn(7, 15, "brown_d"); t.pn(8, 16, "brown_d") -- satchel strap
|
||||
t.rectn(9, 15, 4, 4, "yellow_d"); t.framen(9, 15, 4, 4, "brown_d"); t.rectn(9, 15, 4, 1, "yellow")
|
||||
t.rectn(1, 12, 2, 5, "brown"); t.rectn(13, 12, 2, 5, "brown") -- arms (wider)
|
||||
feet(t, "brown_d")
|
||||
expr(t, ex, 5, 6, 10, 6, 10)
|
||||
end
|
||||
|
||||
local function mimi(t, ex) -- 猫店主 cat shopkeeper
|
||||
t.rectn(3, 2, 2, 3, "honey"); t.rectn(11, 2, 2, 3, "honey") -- pointy ears
|
||||
t.pn(3, 2, "honey_d"); t.pn(12, 2, "honey_d")
|
||||
t.pn(3, 3, "pink"); t.pn(12, 3, "pink")
|
||||
t.rectn(3, 4, 10, 8, "honey"); t.rectn(3, 4, 10, 1, "honey_l")
|
||||
t.pn(4, 4, "honey_d"); t.pn(7, 4, "honey_d"); t.pn(10, 4, "honey_d") -- stripes
|
||||
t.pn(7, 9, "pink_d") -- nose
|
||||
t.rectn(4, 12, 8, 7, "cream")
|
||||
t.rectn(4, 12, 8, 4, "sky"); t.rectn(4, 12, 8, 1, "sky_d") -- apron
|
||||
arms(t, "honey"); feet(t, "honey_d")
|
||||
t.pn(1, 8, "honey_d"); t.pn(14, 8, "honey_d") -- whiskers
|
||||
expr(t, ex, 5, 7, 10, 7, 10)
|
||||
end
|
||||
|
||||
local function fuyisheng(t, ex) -- 鹿医生 deer doctor
|
||||
t.vln(4, 0, 3, "brown_d"); t.pn(3, 1, "brown_d"); t.pn(5, 1, "brown_d") -- antlers
|
||||
t.vln(11, 0, 3, "brown_d"); t.pn(10, 1, "brown_d"); t.pn(12, 1, "brown_d")
|
||||
t.rectn(2, 4, 2, 3, "cream_d"); t.rectn(12, 4, 2, 3, "cream_d") -- ears
|
||||
t.rectn(3, 3, 10, 9, "cream_d"); t.rectn(3, 3, 10, 1, "skin")
|
||||
t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink") -- muzzle
|
||||
t.rectn(3, 12, 10, 7, "white"); t.framen(3, 12, 10, 7, "w_lo") -- white coat
|
||||
t.vln(8, 12, 7, "w_lo")
|
||||
t.rectn(5, 12, 6, 2, "skin")
|
||||
t.pn(11, 14, "red"); t.pn(11, 15, "red") -- red cross hint
|
||||
arms(t, "white"); feet(t, "brown_d")
|
||||
expr(t, ex, 5, 6, 10, 6, 10)
|
||||
end
|
||||
|
||||
local function guiyeye(t, ex) -- 龟老者 turtle elder
|
||||
t.rectn(6, 4, 6, 5, "leaf_l"); t.rectn(6, 4, 6, 1, "cream") -- head
|
||||
t.rectn(6, 8, 6, 2, "white") -- beard
|
||||
t.discn(8, 14, 6, "leaf_d") -- shell
|
||||
t.discn(8, 13, 5, "leaf")
|
||||
t.discn(6, 12, 2, "leaf_l")
|
||||
t.pn(8, 12, "leaf_dd"); t.pn(6, 14, "leaf_dd"); t.pn(10, 14, "leaf_dd"); t.pn(8, 16, "leaf_dd")
|
||||
t.rectn(4, 20, 3, 3, "leaf_d"); t.rectn(9, 20, 3, 3, "leaf_d") -- feet
|
||||
t.vln(13, 12, 9, "brown_d"); t.pn(13, 11, "brown") -- cane
|
||||
expr(t, ex, 7, 6, 10, 6, 8, "ink", "brown_d")
|
||||
end
|
||||
|
||||
-- ---- build the 6 three-frame NPC sprites ----------------------------------
|
||||
local npcs = {
|
||||
{ "afu", afu }, { "yuanyuan", yuanyuan }, { "dabao", dabao },
|
||||
{ "mimi", mimi }, { "fuyisheng", fuyisheng }, { "guiyeye", guiyeye },
|
||||
}
|
||||
for _, n in ipairs(npcs) do
|
||||
local spr, img = H.new(48, 24)
|
||||
for i, ex in ipairs({ "smile", "flat", "off" }) do
|
||||
n[2](H.at(img, (i - 1) * 16, 0), ex)
|
||||
end
|
||||
spr:saveAs(ROOT .. "public/assets/sprites/npc_" .. n[1] .. ".png")
|
||||
print("npc_" .. n[1] .. ".png done")
|
||||
end
|
||||
|
||||
-- ---- twin birds (32x24, 2 frames = beak closed / open) ---------------------
|
||||
local function bird(t, open)
|
||||
t.rectn(0, 18, 16, 2, "brown_d") -- branch
|
||||
-- bird A (faces right)
|
||||
t.discn(4, 11, 4, "blue"); t.discn(4, 8, 3, "blue")
|
||||
t.discn(3, 10, 2, "blue_l")
|
||||
t.discn(5, 11, 2, "blue_d") -- wing
|
||||
t.pn(1, 12, "blue_d"); t.pn(0, 13, "blue_d") -- tail
|
||||
t.pn(3, 7, "ink") -- eye
|
||||
if open then t.pn(6, 8, "yellow"); t.pn(6, 9, "yellow_d") else t.pn(6, 8, "yellow") end
|
||||
t.vln(3, 15, 3, "honey_d"); t.vln(5, 15, 3, "honey_d")
|
||||
-- bird B (faces left), slightly lower
|
||||
t.discn(11, 13, 4, "blue"); t.discn(11, 10, 3, "blue")
|
||||
t.discn(12, 12, 2, "blue_l")
|
||||
t.discn(10, 13, 2, "blue_d")
|
||||
t.pn(14, 14, "blue_d"); t.pn(15, 15, "blue_d")
|
||||
t.pn(12, 9, "ink")
|
||||
if open then t.pn(9, 10, "yellow"); t.pn(9, 11, "yellow_d") else t.pn(9, 10, "yellow") end
|
||||
t.vln(10, 16, 2, "honey_d"); t.vln(12, 16, 2, "honey_d")
|
||||
end
|
||||
local bspr, bimg = H.new(32, 24)
|
||||
bird(H.at(bimg, 0, 0), false)
|
||||
bird(H.at(bimg, 16, 0), true)
|
||||
bspr:saveAs(ROOT .. "public/assets/sprites/npc_bird.png")
|
||||
print("npc_bird.png done")
|
||||
@@ -0,0 +1,96 @@
|
||||
-- gen_player.lua — public/assets/sprites/player.png
|
||||
-- 64x96; frame 16x24; cols 0-3 = walk frames (col 0 also idle); rows 0=down 1=up 2=left 3=right
|
||||
-- Gender-neutral chibi, cream / honey-orange palette, gentle "healing" look.
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
local spr, img = H.new(64, 96)
|
||||
|
||||
-- step: -1 left step, 0 stand, +1 right step
|
||||
local function stepOf(f)
|
||||
if f == 1 then return -1 elseif f == 3 then return 1 else return 0 end
|
||||
end
|
||||
|
||||
-- legs shared by down/up (front/back view alternates a lifted foot)
|
||||
local function legsFB(t, step)
|
||||
if step == 0 then
|
||||
t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(9, 17, 2, 5, "cream_d")
|
||||
t.rectn(4, 22, 3, 2, "brown"); t.rectn(9, 22, 3, 2, "brown")
|
||||
elseif step == -1 then
|
||||
t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(4, 22, 3, 2, "brown") -- left planted
|
||||
t.rectn(9, 17, 2, 4, "cream_d"); t.rectn(9, 21, 3, 2, "brown") -- right lifted
|
||||
else
|
||||
t.rectn(5, 17, 2, 4, "cream_d"); t.rectn(4, 21, 3, 2, "brown") -- left lifted
|
||||
t.rectn(9, 17, 2, 5, "cream_d"); t.rectn(9, 22, 3, 2, "brown") -- right planted
|
||||
end
|
||||
end
|
||||
|
||||
local function drawDown(t, f)
|
||||
local s = stepOf(f)
|
||||
t.rectn(4, 2, 8, 3, "hair")
|
||||
t.rectn(3, 3, 2, 6, "hair"); t.rectn(11, 3, 2, 6, "hair")
|
||||
t.rectn(4, 4, 8, 6, "skin")
|
||||
t.rectn(4, 2, 8, 1, "hair_l")
|
||||
t.pn(5, 7, "ink"); t.pn(10, 7, "ink")
|
||||
t.pn(6, 9, "skin_d"); t.pn(9, 9, "skin_d")
|
||||
t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 11, 8, 1, "honey_l")
|
||||
-- arms swing opposite to step
|
||||
if s == 1 then t.rectn(3, 11, 2, 5, "honey"); t.rectn(3, 16, 2, 2, "skin")
|
||||
else t.rectn(2, 11, 2, 5, "honey"); t.rectn(2, 16, 2, 2, "skin") end
|
||||
if s == -1 then t.rectn(11, 11, 2, 5, "honey"); t.rectn(11, 16, 2, 2, "skin")
|
||||
else t.rectn(12, 11, 2, 5, "honey"); t.rectn(12, 16, 2, 2, "skin") end
|
||||
legsFB(t, s)
|
||||
end
|
||||
|
||||
local function drawUp(t, f)
|
||||
local s = stepOf(f)
|
||||
t.rectn(3, 2, 10, 7, "hair")
|
||||
t.rectn(4, 2, 8, 2, "hair_l")
|
||||
t.rectn(3, 8, 10, 1, "hair_d")
|
||||
t.rectn(6, 9, 4, 2, "skin_d")
|
||||
t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 16, 8, 1, "honey_d")
|
||||
t.rectn(2, 11, 2, 5, "honey"); t.rectn(12, 11, 2, 5, "honey")
|
||||
t.rectn(2, 16, 2, 2, "skin"); t.rectn(12, 16, 2, 2, "skin")
|
||||
legsFB(t, s)
|
||||
end
|
||||
|
||||
local function drawLeft(t, f)
|
||||
local s = stepOf(f)
|
||||
-- hair (back of head toward the right / behind)
|
||||
t.rectn(5, 2, 7, 3, "hair"); t.rectn(5, 2, 7, 1, "hair_l")
|
||||
t.rectn(9, 3, 3, 6, "hair")
|
||||
-- face (pointing left)
|
||||
t.rectn(4, 4, 7, 6, "skin")
|
||||
t.pn(5, 7, "ink")
|
||||
t.pn(4, 8, "skin_d")
|
||||
-- torso
|
||||
t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 11, 8, 1, "honey_l")
|
||||
-- front arm swings with step
|
||||
if s == -1 then t.rectn(2, 11, 2, 5, "honey"); t.rectn(2, 16, 2, 2, "skin")
|
||||
elseif s == 1 then t.rectn(5, 11, 2, 5, "honey"); t.rectn(5, 16, 2, 2, "skin")
|
||||
else t.rectn(3, 11, 2, 5, "honey"); t.rectn(3, 16, 2, 2, "skin") end
|
||||
-- profile legs scissor horizontally
|
||||
if s == 0 then
|
||||
t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(8, 17, 2, 5, "cream_d")
|
||||
t.rectn(4, 22, 3, 2, "brown"); t.rectn(7, 22, 3, 2, "brown")
|
||||
elseif s == -1 then
|
||||
t.rectn(4, 17, 2, 5, "cream_d"); t.rectn(3, 22, 3, 2, "brown") -- front leg forward
|
||||
t.rectn(9, 17, 2, 4, "cream_d"); t.rectn(8, 21, 3, 2, "brown") -- back leg lifted
|
||||
else
|
||||
t.rectn(6, 17, 2, 4, "cream_d"); t.rectn(5, 21, 3, 2, "brown") -- back leg lifted
|
||||
t.rectn(9, 17, 2, 5, "cream_d"); t.rectn(8, 22, 3, 2, "brown") -- front leg forward
|
||||
end
|
||||
end
|
||||
|
||||
for row = 0, 3 do
|
||||
for col = 0, 3 do
|
||||
local t = H.at(img, col * 16, row * 24)
|
||||
if row == 0 then drawDown(t, col)
|
||||
elseif row == 1 then drawUp(t, col)
|
||||
elseif row == 2 then drawLeft(t, col)
|
||||
else drawLeft(H.mirrorX(t, 16), col) end
|
||||
end
|
||||
end
|
||||
|
||||
spr:saveAs(ROOT .. "public/assets/sprites/player.png")
|
||||
print("player.png done")
|
||||
@@ -0,0 +1,130 @@
|
||||
-- gen_portraits.lua — dialogue portraits (each 32x32)
|
||||
-- player_smile / player_real + afu yuanyuan dabao mimi fuyisheng guiyeye bird (all smile)
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
local function smileArc(t, cx, y, w, c)
|
||||
local half = math.floor(w / 2)
|
||||
t.pn(cx - half, y, c); t.pn(cx + half, y, c)
|
||||
t.pn(cx - half + 1, y + 1, c); t.pn(cx + half - 1, y + 1, c)
|
||||
for i = -half + 2, half - 2 do t.pn(cx + i, y + 2, c) end
|
||||
end
|
||||
local function eyes(t, lx, rx, y, c)
|
||||
t.rectn(lx, y, 3, 4, c); t.rectn(rx, y, 3, 4, c)
|
||||
end
|
||||
|
||||
-- ---- player ---------------------------------------------------------------
|
||||
local function playerSmile(t)
|
||||
t.rectn(6, 2, 20, 6, "hair"); t.rectn(6, 2, 20, 2, "hair_l")
|
||||
t.rectn(4, 4, 4, 14, "hair"); t.rectn(24, 4, 4, 14, "hair")
|
||||
t.rectn(6, 7, 20, 18, "skin")
|
||||
eyes(t, 9, 20, 13, "ink")
|
||||
t.pn(8, 18, "pink"); t.pn(9, 18, "pink"); t.pn(23, 18, "pink"); t.pn(24, 18, "pink")
|
||||
smileArc(t, 16, 20, 8, "brown_d")
|
||||
t.rectn(12, 25, 8, 4, "honey"); t.rectn(12, 25, 8, 1, "honey_l")
|
||||
end
|
||||
|
||||
-- E3 "real face": the smile is gone — a plain, tired, honest face.
|
||||
local function playerReal(t)
|
||||
t.rectn(6, 2, 20, 6, "hair"); t.rectn(6, 2, 20, 2, "hair_l")
|
||||
t.rectn(4, 4, 4, 14, "hair"); t.rectn(24, 4, 4, 14, "hair")
|
||||
t.rectn(6, 7, 20, 18, "skin")
|
||||
t.rectn(9, 15, 3, 2, "ink"); t.rectn(20, 15, 3, 2, "ink") -- half-closed eyes
|
||||
t.rectn(9, 14, 3, 1, "skin_d"); t.rectn(20, 14, 3, 1, "skin_d") -- heavy lids
|
||||
t.pn(9, 18, "skin_d"); t.pn(10, 18, "skin_d") -- bags
|
||||
t.pn(21, 18, "skin_d"); t.pn(22, 18, "skin_d")
|
||||
t.pn(10, 19, "cream_d"); t.pn(21, 19, "cream_d")
|
||||
t.pn(9, 12, "hair_d"); t.pn(10, 12, "hair_d"); t.pn(11, 13, "hair_d") -- weary brows
|
||||
t.pn(22, 12, "hair_d"); t.pn(21, 12, "hair_d"); t.pn(20, 13, "hair_d")
|
||||
t.hln(13, 21, 6, "brown_d") -- flat mouth
|
||||
t.rectn(12, 25, 8, 4, "honey"); t.rectn(12, 25, 8, 1, "honey_d")
|
||||
end
|
||||
|
||||
-- ---- NPC smile portraits ---------------------------------------------------
|
||||
local function afuP(t)
|
||||
t.rectn(2, 6, 6, 15, "brown_d"); t.rectn(24, 6, 6, 15, "brown_d")
|
||||
t.rectn(2, 6, 6, 5, "brown"); t.rectn(24, 6, 6, 5, "brown")
|
||||
t.rectn(6, 4, 20, 23, "brown"); t.rectn(6, 4, 20, 3, "brown_l")
|
||||
eyes(t, 9, 20, 11, "ink")
|
||||
t.rectn(11, 16, 10, 8, "cream"); t.rectn(14, 16, 4, 3, "ink")
|
||||
smileArc(t, 16, 20, 7, "brown_d")
|
||||
t.rectn(12, 27, 8, 3, "honey")
|
||||
end
|
||||
|
||||
local function yuanyuanP(t)
|
||||
t.rectn(8, 0, 4, 9, "cream"); t.rectn(20, 0, 4, 9, "cream")
|
||||
t.rectn(9, 1, 2, 7, "pink"); t.rectn(21, 1, 2, 7, "pink")
|
||||
t.rectn(6, 8, 20, 19, "cream"); t.rectn(6, 8, 20, 2, "white")
|
||||
eyes(t, 10, 20, 14, "ink")
|
||||
t.pn(15, 18, "pink_d"); t.pn(16, 18, "pink_d")
|
||||
smileArc(t, 16, 20, 6, "brown_d")
|
||||
t.rectn(12, 27, 8, 3, "pink")
|
||||
end
|
||||
|
||||
local function dabaoP(t)
|
||||
t.discn(7, 6, 4, "brown"); t.discn(25, 6, 4, "brown")
|
||||
t.discn(7, 6, 2, "brown_d"); t.discn(25, 6, 2, "brown_d")
|
||||
t.rectn(4, 4, 24, 23, "brown"); t.rectn(4, 4, 24, 3, "brown_l")
|
||||
eyes(t, 9, 21, 11, "ink")
|
||||
t.rectn(11, 16, 10, 8, "cream"); t.rectn(14, 16, 4, 3, "ink")
|
||||
smileArc(t, 16, 20, 7, "brown_d")
|
||||
t.rectn(11, 27, 10, 3, "yellow_d")
|
||||
end
|
||||
|
||||
local function mimiP(t)
|
||||
t.pn(7, 2, "honey"); t.rectn(6, 3, 4, 6, "honey") -- left ear
|
||||
t.pn(24, 2, "honey"); t.rectn(22, 3, 4, 6, "honey") -- right ear
|
||||
t.rectn(7, 4, 2, 4, "pink"); t.rectn(23, 4, 2, 4, "pink") -- inner
|
||||
t.rectn(6, 8, 20, 19, "honey"); t.rectn(6, 8, 20, 2, "honey_l")
|
||||
t.pn(9, 8, "honey_d"); t.pn(13, 8, "honey_d"); t.pn(17, 8, "honey_d"); t.pn(21, 8, "honey_d")
|
||||
eyes(t, 10, 20, 14, "ink")
|
||||
t.pn(15, 18, "pink_d"); t.pn(16, 18, "pink_d")
|
||||
smileArc(t, 16, 20, 6, "brown_d")
|
||||
t.hln(3, 17, 3, "honey_d"); t.hln(3, 19, 3, "honey_d")
|
||||
t.hln(26, 17, 3, "honey_d"); t.hln(26, 19, 3, "honey_d")
|
||||
t.rectn(12, 27, 8, 3, "sky")
|
||||
end
|
||||
|
||||
local function fuyishengP(t)
|
||||
t.vln(8, 0, 5, "brown_d"); t.pn(6, 2, "brown_d"); t.pn(7, 3, "brown_d"); t.pn(10, 1, "brown_d")
|
||||
t.vln(23, 0, 5, "brown_d"); t.pn(25, 2, "brown_d"); t.pn(24, 3, "brown_d"); t.pn(21, 1, "brown_d")
|
||||
t.rectn(2, 10, 4, 6, "cream_d"); t.rectn(26, 10, 4, 6, "cream_d")
|
||||
t.rectn(6, 5, 20, 22, "cream_d"); t.rectn(6, 5, 20, 2, "skin")
|
||||
eyes(t, 10, 20, 13, "ink")
|
||||
t.rectn(11, 17, 10, 7, "cream"); t.rectn(14, 17, 4, 3, "ink")
|
||||
smileArc(t, 16, 21, 6, "brown_d")
|
||||
t.rectn(12, 27, 8, 3, "white"); t.pn(12, 28, "red")
|
||||
end
|
||||
|
||||
local function guiyeyeP(t)
|
||||
t.rectn(7, 5, 18, 20, "leaf_l"); t.rectn(7, 5, 18, 2, "cream") -- bald cream top
|
||||
t.rectn(9, 12, 5, 2, "white"); t.rectn(18, 12, 5, 2, "white") -- bushy brows
|
||||
eyes(t, 10, 20, 14, "ink")
|
||||
t.rectn(10, 20, 12, 8, "white"); t.rectn(12, 26, 8, 3, "white") -- beard
|
||||
smileArc(t, 16, 19, 6, "brown_d")
|
||||
t.rectn(4, 28, 24, 3, "leaf_d"); t.rectn(4, 28, 24, 1, "leaf") -- shell hint
|
||||
end
|
||||
|
||||
local function birdP(t)
|
||||
t.discn(10, 15, 8, "blue"); t.discn(9, 12, 4, "blue_l") -- left bird
|
||||
t.discn(11, 16, 3, "blue_d")
|
||||
t.pn(7, 12, "ink")
|
||||
t.pn(4, 15, "yellow"); t.pn(3, 16, "yellow_d")
|
||||
t.discn(22, 17, 7, "blue"); t.discn(23, 14, 3, "blue_l") -- right bird
|
||||
t.discn(21, 18, 3, "blue_d")
|
||||
t.pn(24, 14, "ink")
|
||||
t.pn(28, 16, "yellow"); t.pn(29, 17, "yellow_d")
|
||||
t.rectn(6, 25, 20, 2, "brown_d") -- branch
|
||||
end
|
||||
|
||||
local portraits = {
|
||||
{ "player_smile", playerSmile }, { "player_real", playerReal },
|
||||
{ "afu", afuP }, { "yuanyuan", yuanyuanP }, { "dabao", dabaoP },
|
||||
{ "mimi", mimiP }, { "fuyisheng", fuyishengP }, { "guiyeye", guiyeyeP }, { "bird", birdP },
|
||||
}
|
||||
for _, n in ipairs(portraits) do
|
||||
local spr, img = H.new(32, 32)
|
||||
n[2](H.at(img, 0, 0))
|
||||
spr:saveAs(ROOT .. "public/assets/portraits/" .. n[1] .. ".png")
|
||||
print(n[1] .. ".png done")
|
||||
end
|
||||
@@ -0,0 +1,878 @@
|
||||
-- gen_tileset.lua — public/assets/tileset.png (256x128, 16x16 tiles, id = row*16+col)
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
local spr, img = H.new(256, 128)
|
||||
local tiles = {}
|
||||
|
||||
-- ---- shared sub-draws ------------------------------------------------------
|
||||
local function grassBase(t)
|
||||
t.rectn(0, 0, 16, 16, "grass")
|
||||
t.pn(3, 2, "grass_l"); t.pn(11, 5, "grass_l"); t.pn(6, 9, "grass_l")
|
||||
t.pn(13, 12, "grass_l"); t.pn(1, 13, "grass_l"); t.pn(9, 3, "grass_l")
|
||||
t.pn(5, 4, "grass_d"); t.pn(6, 4, "grass_d"); t.pn(6, 5, "grass_d")
|
||||
t.pn(10, 11, "grass_d"); t.pn(11, 11, "grass_d"); t.pn(2, 8, "grass_d")
|
||||
t.pn(14, 3, "grass_d"); t.pn(9, 14, "grass_d"); t.pn(12, 8, "grass_d")
|
||||
end
|
||||
|
||||
local function roof(t, base, d, l, edge)
|
||||
t.rectn(0, 0, 16, 16, base)
|
||||
t.hln(0, 4, 16, d); t.hln(0, 9, 16, d); t.hln(0, 14, 16, d)
|
||||
for x = 2, 15, 4 do t.vln(x, 5, 4, d) end
|
||||
for x = 0, 15, 4 do t.vln(x, 10, 4, d) end
|
||||
t.hln(0, 0, 16, l)
|
||||
t.hln(0, 15, 16, d)
|
||||
if edge == "L" then t.vln(0, 0, 16, d); t.vln(1, 0, 16, base) end
|
||||
if edge == "R" then t.vln(15, 0, 16, d); t.vln(14, 0, 16, base) end
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 0 (ids 0-15) : ground / water / flowerbeds
|
||||
-- ===========================================================================
|
||||
tiles[0] = function(t) grassBase(t) end
|
||||
|
||||
tiles[1] = function(t)
|
||||
grassBase(t)
|
||||
H.flower(t, 8, 7, "pink", "yellow")
|
||||
H.flower(t, 4, 11, "white", "yellow")
|
||||
t.pn(12, 4, "pink"); t.pn(12, 5, "pink"); t.pn(11, 4, "pink")
|
||||
end
|
||||
|
||||
tiles[2] = function(t)
|
||||
grassBase(t)
|
||||
H.flower(t, 6, 6, "yellow", "honey_d")
|
||||
H.flower(t, 11, 10, "red", "yellow")
|
||||
t.pn(3, 5, "white"); t.pn(4, 5, "white")
|
||||
end
|
||||
|
||||
tiles[3] = function(t)
|
||||
t.rectn(0, 0, 16, 16, "grass_d")
|
||||
t.pn(3, 3, "grass"); t.pn(10, 6, "grass"); t.pn(6, 11, "grass"); t.pn(13, 13, "grass")
|
||||
t.pn(5, 5, "grass_dd"); t.pn(6, 5, "grass_dd"); t.pn(11, 11, "grass_dd")
|
||||
t.pn(2, 9, "grass_dd"); t.pn(14, 4, "grass_dd"); t.pn(9, 14, "grass_dd")
|
||||
end
|
||||
|
||||
tiles[4] = function(t) -- dirt road
|
||||
t.rectn(0, 0, 16, 16, "dirt")
|
||||
t.pn(3, 3, "dirt_d"); t.pn(4, 3, "dirt_d"); t.pn(11, 6, "dirt_d")
|
||||
t.pn(7, 10, "dirt_d"); t.pn(13, 12, "dirt_d"); t.pn(2, 13, "dirt_d")
|
||||
t.pn(6, 4, "dirt_l"); t.pn(12, 8, "dirt_l"); t.pn(9, 13, "dirt_l"); t.pn(1, 6, "dirt_l")
|
||||
end
|
||||
|
||||
tiles[5] = function(t) -- stone road
|
||||
t.rectn(0, 0, 16, 16, "stone")
|
||||
t.hln(0, 0, 16, "stone_d"); t.hln(0, 8, 16, "stone_d")
|
||||
t.vln(0, 0, 8, "stone_d"); t.vln(8, 0, 8, "stone_d")
|
||||
t.vln(4, 8, 8, "stone_d"); t.vln(12, 8, 8, "stone_d")
|
||||
t.pn(2, 3, "stone_l"); t.pn(10, 3, "stone_l"); t.pn(6, 11, "stone_l"); t.pn(13, 12, "stone_l")
|
||||
end
|
||||
|
||||
tiles[6] = function(t) -- stone road alt
|
||||
t.rectn(0, 0, 16, 16, "stone")
|
||||
t.hln(0, 0, 16, "stone_dd"); t.hln(0, 8, 16, "stone_dd")
|
||||
t.vln(5, 0, 8, "stone_dd"); t.vln(13, 0, 8, "stone_dd")
|
||||
t.vln(1, 8, 8, "stone_dd"); t.vln(9, 8, 8, "stone_dd")
|
||||
t.pn(3, 3, "stone_l"); t.pn(11, 4, "stone_l"); t.pn(4, 11, "stone_l"); t.pn(12, 12, "stone_l")
|
||||
t.pn(8, 2, "stone_d"); t.pn(7, 12, "stone_d")
|
||||
end
|
||||
|
||||
tiles[7] = function(t) -- plaza brick
|
||||
t.rectn(0, 0, 16, 16, "stone_l")
|
||||
t.hln(0, 0, 16, "stone_d"); t.hln(0, 8, 16, "stone_d")
|
||||
t.vln(0, 0, 16, "stone_d"); t.vln(8, 0, 16, "stone_d")
|
||||
t.pn(1, 1, "stone_dd"); t.pn(9, 1, "stone_dd"); t.pn(1, 9, "stone_dd"); t.pn(9, 9, "stone_dd")
|
||||
end
|
||||
|
||||
tiles[8] = function(t) -- lake water A
|
||||
t.rectn(0, 0, 16, 16, "water")
|
||||
t.hln(2, 4, 3, "water_l"); t.hln(9, 4, 3, "water_l")
|
||||
t.hln(5, 11, 3, "water_l"); t.hln(12, 11, 2, "water_l")
|
||||
t.pn(7, 7, "water_d"); t.pn(13, 2, "water_d"); t.pn(3, 13, "water_d")
|
||||
end
|
||||
|
||||
tiles[9] = function(t) -- lake water B
|
||||
t.rectn(0, 0, 16, 16, "water_d")
|
||||
t.hln(3, 5, 3, "water"); t.hln(10, 5, 3, "water")
|
||||
t.hln(6, 12, 3, "water"); t.hln(12, 12, 2, "water")
|
||||
t.pn(8, 8, "water_dd"); t.pn(2, 3, "water_dd"); t.pn(13, 13, "water_dd")
|
||||
end
|
||||
|
||||
-- water banks (shore toward the named direction)
|
||||
local function bankH(t, shoreTop)
|
||||
t.rectn(0, 0, 16, 16, "water")
|
||||
local by = { 6, 6, 7, 7, 6, 6, 5, 6, 6, 7, 7, 6, 6, 5, 6, 6 }
|
||||
for x = 0, 15 do
|
||||
local b = by[x + 1]
|
||||
if shoreTop then
|
||||
t.rectn(x, 0, 1, b, "stone_l")
|
||||
t.pn(x, b, "cream_d")
|
||||
t.pn(x, b + 1, "water_l")
|
||||
else
|
||||
t.rectn(x, 16 - b, 1, b, "stone_l")
|
||||
t.pn(x, 15 - b, "cream_d")
|
||||
t.pn(x, 14 - b, "water_l")
|
||||
end
|
||||
end
|
||||
end
|
||||
local function bankV(t, shoreLeft)
|
||||
t.rectn(0, 0, 16, 16, "water")
|
||||
local bx = { 6, 6, 7, 7, 6, 6, 5, 6, 6, 7, 7, 6, 6, 5, 6, 6 }
|
||||
for y = 0, 15 do
|
||||
local b = bx[y + 1]
|
||||
if shoreLeft then
|
||||
t.rectn(0, y, b, 1, "stone_l")
|
||||
t.pn(b, y, "cream_d")
|
||||
t.pn(b + 1, y, "water_l")
|
||||
else
|
||||
t.rectn(16 - b, y, b, 1, "stone_l")
|
||||
t.pn(15 - b, y, "cream_d")
|
||||
t.pn(14 - b, y, "water_l")
|
||||
end
|
||||
end
|
||||
end
|
||||
tiles[10] = function(t) bankH(t, true) end -- 水岸北 (shore north/top)
|
||||
tiles[11] = function(t) bankH(t, false) end -- 水岸南
|
||||
tiles[12] = function(t) bankV(t, true) end -- 水岸西
|
||||
tiles[13] = function(t) bankV(t, false) end -- 水岸东
|
||||
|
||||
local function flowerbed(t, c1, c2)
|
||||
t.rectn(0, 0, 16, 16, "dirt_d")
|
||||
t.rectn(0, 0, 16, 16, "dirt")
|
||||
t.framen(0, 0, 16, 16, "dirt_d")
|
||||
t.pn(3, 6, "leaf"); t.pn(10, 7, "leaf"); t.pn(7, 11, "leaf"); t.pn(13, 5, "leaf"); t.pn(5, 12, "leaf")
|
||||
H.flower(t, 4, 5, c1, "yellow")
|
||||
H.flower(t, 10, 4, c1, "yellow")
|
||||
H.flower(t, 7, 10, c2, "yellow")
|
||||
H.flower(t, 13, 9, c1, "yellow")
|
||||
H.flower(t, 3, 12, c2, "yellow")
|
||||
end
|
||||
tiles[14] = function(t) flowerbed(t, "pink", "pink_l") end
|
||||
tiles[15] = function(t) flowerbed(t, "yellow", "yellow_l") end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 1 (ids 16-31) : bushes / tree / fence / lamp / bunting
|
||||
-- ===========================================================================
|
||||
local function bush(t, berries)
|
||||
t.ellipsen(8, 14, 6, 2, "grass_dd")
|
||||
t.discn(8, 8, 6, "leaf")
|
||||
t.discn(6, 6, 3, "leaf_l")
|
||||
t.discn(10, 10, 3, "leaf_d")
|
||||
t.discn(8, 11, 3, "leaf_d")
|
||||
if berries then
|
||||
t.pn(6, 7, "berry"); t.pn(9, 5, "berry"); t.pn(11, 9, "berry")
|
||||
t.pn(7, 10, "berry"); t.pn(10, 11, "berry"); t.pn(5, 9, "berry"); t.pn(9, 8, "berry_d")
|
||||
end
|
||||
end
|
||||
tiles[16] = function(t) bush(t, true) end
|
||||
tiles[17] = function(t) bush(t, false) end
|
||||
|
||||
-- tree (18-21) drawn once across its 2x2 block (origin of tile 18)
|
||||
local function drawTree(img)
|
||||
local tr = H.at(img, (18 % 16) * 16, math.floor(18 / 16) * 16)
|
||||
tr.ellipsen(16, 31, 8, 1, "grass_dd")
|
||||
tr.rectn(14, 21, 4, 11, "brown")
|
||||
tr.vln(14, 21, 11, "brown_l"); tr.vln(17, 21, 11, "brown_d")
|
||||
tr.discn(16, 12, 13, "leaf")
|
||||
tr.discn(16, 16, 6, "leaf_d")
|
||||
tr.discn(21, 15, 5, "leaf_d")
|
||||
tr.discn(12, 8, 6, "leaf_l")
|
||||
tr.discn(9, 13, 3, "leaf")
|
||||
tr.discn(23, 11, 3, "leaf")
|
||||
end
|
||||
|
||||
tiles[22] = function(t) -- round bush
|
||||
t.ellipsen(8, 14, 5, 2, "grass_dd")
|
||||
t.discn(8, 9, 5, "leaf")
|
||||
t.discn(6, 7, 2, "leaf_l")
|
||||
t.discn(10, 11, 2, "leaf_d")
|
||||
end
|
||||
|
||||
tiles[23] = function(t) -- tree stump
|
||||
t.ellipsen(8, 14, 6, 2, "brown_d")
|
||||
t.rectn(4, 7, 8, 7, "brown")
|
||||
t.vln(4, 7, 7, "brown_d"); t.vln(11, 7, 7, "brown_dd")
|
||||
t.ellipsen(8, 7, 4, 2, "wood_l")
|
||||
t.ellipsen(8, 7, 2, 1, "wood_d")
|
||||
t.pn(8, 7, "brown_dd")
|
||||
end
|
||||
|
||||
tiles[24] = function(t) -- rock
|
||||
t.ellipsen(8, 13, 7, 2, "grey_d")
|
||||
t.ellipsen(8, 9, 6, 4, "grey")
|
||||
t.ellipsen(6, 8, 3, 2, "grey_l")
|
||||
t.ellipsen(10, 11, 3, 2, "grey_d")
|
||||
end
|
||||
|
||||
tiles[25] = function(t) -- signpost
|
||||
t.rectn(7, 9, 2, 7, "brown_d")
|
||||
t.rectn(3, 3, 10, 6, "wood")
|
||||
t.framen(3, 3, 10, 6, "brown_d")
|
||||
t.hln(4, 5, 8, "brown_d"); t.hln(4, 7, 6, "brown_d")
|
||||
t.pn(12, 4, "brown_dd")
|
||||
end
|
||||
|
||||
tiles[26] = function(t) -- fence horizontal
|
||||
t.rectn(1, 4, 3, 11, "brown"); t.rectn(12, 4, 3, 11, "brown")
|
||||
t.pn(1, 4, "brown_l"); t.pn(12, 4, "brown_l")
|
||||
t.hln(0, 7, 16, "wood"); t.hln(0, 8, 16, "wood_d")
|
||||
t.hln(0, 11, 16, "wood"); t.hln(0, 12, 16, "wood_d")
|
||||
t.vln(1, 4, 11, "brown_d"); t.vln(12, 4, 11, "brown_d")
|
||||
end
|
||||
|
||||
tiles[27] = function(t) -- fence vertical
|
||||
t.rectn(4, 0, 3, 16, "wood"); t.rectn(9, 0, 3, 16, "wood")
|
||||
t.vln(4, 0, 16, "wood_d"); t.vln(9, 0, 16, "wood_d")
|
||||
t.rectn(2, 6, 12, 3, "brown")
|
||||
t.hln(2, 6, 12, "brown_l"); t.hln(2, 8, 12, "brown_d")
|
||||
end
|
||||
|
||||
tiles[28] = function(t) -- street lamp
|
||||
t.rectn(7, 6, 2, 10, "grey_d")
|
||||
t.rectn(6, 15, 4, 1, "grey")
|
||||
t.rectn(5, 3, 6, 4, "metal")
|
||||
t.framen(5, 3, 6, 4, "grey_d")
|
||||
t.rectn(6, 4, 4, 2, "yellow_l")
|
||||
t.pn(7, 2, "yellow"); t.pn(8, 2, "yellow")
|
||||
t.pn(6, 8, "grey")
|
||||
end
|
||||
|
||||
local function bunting(t, c)
|
||||
t.hln(0, 3, 16, "brown_d")
|
||||
t.hln(4, 4, 8, c)
|
||||
t.hln(5, 5, 6, c)
|
||||
t.hln(6, 6, 4, c)
|
||||
t.hln(7, 7, 2, c)
|
||||
t.pn(8, 8, c)
|
||||
end
|
||||
tiles[29] = function(t) bunting(t, "red") end
|
||||
tiles[30] = function(t) bunting(t, "yellow") end
|
||||
tiles[31] = function(t) bunting(t, "blue") end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 2 (ids 32-47) : buildings / well / mailbox / noticeboard
|
||||
-- ===========================================================================
|
||||
local function creamWall(t)
|
||||
t.rectn(0, 0, 16, 16, "cream")
|
||||
t.hln(0, 0, 16, "cream_d"); t.hln(0, 8, 16, "cream_d")
|
||||
t.vln(0, 0, 8, "cream_d"); t.vln(8, 0, 8, "cream_d")
|
||||
t.vln(4, 8, 8, "cream_d"); t.vln(12, 8, 8, "cream_d")
|
||||
t.pn(2, 4, "cream_dd"); t.pn(10, 12, "cream_dd")
|
||||
end
|
||||
tiles[32] = function(t) creamWall(t) end
|
||||
tiles[33] = function(t)
|
||||
creamWall(t)
|
||||
t.rectn(4, 3, 8, 9, "cream") -- clear brick under window
|
||||
H.window(t, 4, 3, 8, 9, "sky")
|
||||
end
|
||||
|
||||
tiles[34] = function(t) -- wood plank wall
|
||||
t.rectn(0, 0, 16, 16, "wood")
|
||||
for y = 0, 15, 4 do
|
||||
t.hln(0, y, 16, "wood_d"); t.hln(0, y + 1, 16, "wood_l")
|
||||
end
|
||||
t.pn(2, 3, "wood_dd"); t.pn(13, 3, "wood_dd"); t.pn(2, 7, "wood_dd")
|
||||
t.pn(13, 7, "wood_dd"); t.pn(2, 11, "wood_dd"); t.pn(13, 11, "wood_dd"); t.pn(2, 15, "wood_dd")
|
||||
end
|
||||
|
||||
tiles[35] = function(t) roof(t, "honey", "honey_d", "honey_l", "L") end
|
||||
tiles[36] = function(t) roof(t, "honey", "honey_d", "honey_l", nil) end
|
||||
tiles[37] = function(t) roof(t, "honey", "honey_d", "honey_l", "R") end
|
||||
|
||||
tiles[38] = function(t) -- door (lower)(两侧补墙色:室内门放在 ground 层,透明边会露黑底)
|
||||
t.rectn(0, 0, 3, 16, "cream"); t.rectn(13, 0, 3, 16, "cream")
|
||||
t.vln(3, 0, 16, "brown_d"); t.vln(12, 0, 16, "brown_d")
|
||||
t.rectn(4, 0, 8, 16, "wood")
|
||||
t.rectn(5, 2, 6, 5, "wood_d"); t.rectn(5, 9, 6, 5, "wood_d")
|
||||
t.vln(5, 0, 16, "wood_l")
|
||||
t.pn(10, 7, "yellow"); t.pn(10, 8, "yellow_d")
|
||||
t.hln(4, 15, 8, "wood_d")
|
||||
end
|
||||
|
||||
tiles[39] = function(t) -- door frame top (arch), sits above tile 38
|
||||
t.vln(3, 4, 12, "brown_d"); t.vln(12, 4, 12, "brown_d")
|
||||
t.rectn(4, 6, 8, 10, "wood")
|
||||
t.vln(5, 6, 10, "wood_l")
|
||||
-- arch
|
||||
t.discn(8, 6, 5, "brown_d")
|
||||
t.discn(8, 6, 4, "sky")
|
||||
t.rectn(4, 6, 8, 3, "sky")
|
||||
t.discn(8, 6, 3, "sky")
|
||||
t.pn(6, 5, "sky_d"); t.pn(9, 4, "sky_d")
|
||||
t.hln(4, 6, 8, "brown_d")
|
||||
end
|
||||
|
||||
tiles[40] = function(t) -- standalone window (transparent surround)
|
||||
H.window(t, 3, 4, 10, 8, "sky")
|
||||
t.pn(4, 5, "white")
|
||||
end
|
||||
|
||||
tiles[41] = function(t) -- chimney
|
||||
t.rectn(5, 3, 6, 13, "swall_d")
|
||||
t.hln(5, 6, 6, "swall_dd"); t.hln(5, 9, 6, "swall_dd"); t.hln(5, 12, 6, "swall_dd")
|
||||
t.vln(7, 3, 3, "swall_dd"); t.vln(6, 6, 3, "swall_dd"); t.vln(8, 9, 3, "swall_dd")
|
||||
t.rectn(4, 2, 8, 2, "swall")
|
||||
t.hln(4, 2, 8, "swall_l")
|
||||
t.discn(6, 1, 1, "w_mid"); t.discn(9, 0, 1, "w_lo")
|
||||
end
|
||||
|
||||
-- well (42-45) drawn once across its 2x2 block (origin of tile 42)
|
||||
local function drawWell(img)
|
||||
local w = H.at(img, (42 % 16) * 16, math.floor(42 / 16) * 16)
|
||||
w.ellipsen(16, 28, 12, 3, "swall_dd")
|
||||
w.discn(16, 16, 12, "swall")
|
||||
w.discn(16, 14, 11, "swall_l")
|
||||
w.discn(16, 19, 10, "swall_d")
|
||||
w.discn(16, 15, 7, "swall_dd")
|
||||
w.discn(16, 16, 6, "black")
|
||||
w.discn(16, 17, 4, "water_d")
|
||||
w.rectn(3, 9, 26, 2, "wood_d")
|
||||
w.hln(3, 9, 26, "wood")
|
||||
w.vln(16, 11, 7, "brown_dd")
|
||||
w.rectn(14, 17, 5, 4, "wood")
|
||||
w.framen(14, 17, 5, 4, "wood_d")
|
||||
end
|
||||
|
||||
tiles[46] = function(t) -- mailbox
|
||||
t.rectn(6, 9, 2, 7, "brown_d")
|
||||
t.rectn(2, 15, 6, 1, "brown_dd")
|
||||
t.rectn(3, 3, 10, 6, "red")
|
||||
t.framen(3, 3, 10, 6, "red_d")
|
||||
t.rectn(3, 3, 10, 2, "red_l")
|
||||
t.pn(7, 6, "yellow"); t.pn(8, 6, "yellow")
|
||||
t.rectn(5, 8, 6, 1, "red_d")
|
||||
end
|
||||
|
||||
tiles[47] = function(t) -- notice board
|
||||
t.rectn(3, 12, 2, 4, "brown_d"); t.rectn(11, 12, 2, 4, "brown_d")
|
||||
t.rectn(1, 1, 14, 2, "brown_d")
|
||||
t.rectn(2, 2, 12, 10, "wood")
|
||||
t.framen(2, 2, 12, 10, "brown_d")
|
||||
t.rectn(4, 4, 3, 4, "cream"); t.rectn(8, 4, 4, 3, "cream")
|
||||
t.rectn(4, 8, 6, 3, "cream_d")
|
||||
t.pn(5, 5, "ink"); t.pn(6, 5, "ink"); t.pn(9, 5, "ink"); t.pn(10, 5, "ink"); t.pn(5, 9, "ink"); t.pn(7, 9, "ink")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 3 (ids 48-63) : interior floor / walls / furniture
|
||||
-- ===========================================================================
|
||||
tiles[48] = function(t) -- wood floor A (horizontal planks)
|
||||
t.rectn(0, 0, 16, 16, "wood_l")
|
||||
for y = 0, 15, 4 do t.hln(0, y, 16, "wood") end
|
||||
t.pn(3, 2, "wood"); t.pn(10, 6, "wood"); t.pn(6, 10, "wood"); t.pn(13, 14, "wood")
|
||||
end
|
||||
|
||||
tiles[49] = function(t) -- wood floor B (vertical planks)
|
||||
t.rectn(0, 0, 16, 16, "wood_l")
|
||||
for x = 0, 15, 4 do t.vln(x, 0, 16, "wood") end
|
||||
t.pn(2, 3, "wood"); t.pn(6, 9, "wood"); t.pn(10, 5, "wood"); t.pn(14, 12, "wood")
|
||||
end
|
||||
|
||||
local function indoorWall(t)
|
||||
t.rectn(0, 0, 16, 16, "cream")
|
||||
t.hln(0, 12, 16, "cream_d")
|
||||
t.rectn(0, 13, 16, 3, "wood_d")
|
||||
t.hln(0, 13, 16, "wood_l")
|
||||
end
|
||||
tiles[50] = function(t) indoorWall(t) end
|
||||
tiles[51] = function(t)
|
||||
t.rectn(0, 0, 16, 16, "cream")
|
||||
H.window(t, 4, 2, 8, 8, "sky")
|
||||
t.hln(0, 12, 16, "cream_d")
|
||||
t.rectn(0, 13, 16, 3, "wood_d")
|
||||
t.hln(0, 13, 16, "wood_l")
|
||||
end
|
||||
|
||||
-- bed (52-55) drawn once across 2x2 (origin of tile 52)
|
||||
local function drawBed(img)
|
||||
local b = H.at(img, (52 % 16) * 16, math.floor(52 / 16) * 16)
|
||||
b.rectn(1, 2, 30, 29, "wood_d")
|
||||
b.framen(1, 2, 30, 29, "brown_dd")
|
||||
b.rectn(1, 2, 30, 5, "wood")
|
||||
b.framen(1, 2, 30, 5, "wood_d")
|
||||
b.rectn(3, 7, 26, 22, "cream")
|
||||
b.rectn(4, 8, 24, 6, "white"); b.framen(4, 8, 24, 6, "cream_d")
|
||||
b.rectn(3, 16, 26, 13, "blue")
|
||||
b.framen(3, 16, 26, 13, "blue_d")
|
||||
b.rectn(3, 16, 26, 2, "blue_l")
|
||||
b.rectn(1, 28, 30, 3, "wood")
|
||||
end
|
||||
|
||||
local function counter(t)
|
||||
t.rectn(0, 0, 16, 3, "cream")
|
||||
t.hln(0, 2, 16, "cream_d")
|
||||
t.rectn(0, 3, 16, 13, "wood")
|
||||
t.vln(0, 3, 13, "wood_d"); t.vln(8, 3, 13, "wood_d")
|
||||
t.rectn(2, 8, 12, 5, "wood_d"); t.framen(2, 8, 12, 5, "wood_dd")
|
||||
t.hln(0, 3, 16, "wood_l")
|
||||
end
|
||||
tiles[56] = function(t) counter(t) end
|
||||
tiles[57] = function(t)
|
||||
counter(t)
|
||||
t.rectn(9, 0, 5, 2, "honey") -- a little bread/register on the right half
|
||||
t.pn(10, 0, "honey_d"); t.pn(12, 0, "honey_d")
|
||||
end
|
||||
|
||||
tiles[58] = function(t) -- oven
|
||||
t.rectn(1, 2, 14, 14, "swall_d")
|
||||
t.framen(1, 2, 14, 14, "swall_dd")
|
||||
t.hln(1, 5, 14, "swall_dd"); t.hln(1, 13, 14, "swall_dd")
|
||||
t.discn(8, 9, 5, "swall_dd")
|
||||
t.rectn(3, 9, 10, 6, "black"); t.discn(8, 9, 5, "black")
|
||||
t.discn(8, 11, 3, "flame_d"); t.discn(8, 11, 2, "flame")
|
||||
t.rectn(1, 2, 14, 2, "swall")
|
||||
end
|
||||
|
||||
local function shelf(t, full)
|
||||
t.rectn(1, 2, 14, 14, "wood")
|
||||
t.framen(1, 2, 14, 14, "brown_d")
|
||||
t.rectn(2, 3, 12, 12, "wood_d")
|
||||
t.hln(2, 7, 12, "brown_d"); t.hln(2, 11, 12, "brown_d")
|
||||
if full then
|
||||
t.rectn(3, 4, 2, 3, "honey"); t.rectn(6, 4, 2, 3, "pink"); t.rectn(9, 4, 3, 3, "sky")
|
||||
t.rectn(3, 8, 3, 3, "red"); t.rectn(7, 8, 2, 3, "yellow"); t.rectn(10, 8, 3, 3, "leaf")
|
||||
t.rectn(4, 12, 2, 3, "brown_l"); t.rectn(8, 12, 4, 3, "honey_d")
|
||||
end
|
||||
end
|
||||
tiles[59] = function(t) shelf(t, false) end
|
||||
tiles[60] = function(t) shelf(t, true) end
|
||||
|
||||
tiles[61] = function(t) -- table
|
||||
t.rectn(1, 4, 14, 4, "wood")
|
||||
t.hln(1, 4, 14, "wood_l"); t.hln(1, 7, 14, "wood_d")
|
||||
t.rectn(2, 8, 2, 7, "wood_d"); t.rectn(12, 8, 2, 7, "wood_d")
|
||||
t.hln(2, 12, 12, "wood_d")
|
||||
end
|
||||
|
||||
tiles[62] = function(t) -- chair
|
||||
t.rectn(4, 2, 8, 2, "wood"); t.hln(4, 2, 8, "wood_l")
|
||||
t.rectn(4, 2, 2, 8, "wood_d")
|
||||
t.rectn(4, 7, 8, 3, "wood"); t.hln(4, 9, 8, "wood_d")
|
||||
t.rectn(4, 10, 2, 5, "wood_d"); t.rectn(10, 10, 2, 5, "wood_d")
|
||||
end
|
||||
|
||||
tiles[63] = function(t) -- rug(烘焙木地板底:放在 ground 层,透明边会露黑底)
|
||||
t.rectn(0, 0, 16, 16, "wood_l")
|
||||
for y = 0, 15, 4 do t.hln(0, y, 16, "wood") end
|
||||
t.rectn(1, 3, 14, 10, "pink")
|
||||
t.framen(1, 3, 14, 10, "pink_d")
|
||||
t.framen(3, 5, 10, 6, "cream")
|
||||
t.discn(8, 8, 2, "honey"); t.pn(8, 8, "honey_d")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 4 (ids 64-79) : papers / picnic / bench / appliances
|
||||
-- ===========================================================================
|
||||
tiles[64] = function(t) -- calendar
|
||||
t.rectn(4, 2, 8, 11, "white"); t.framen(4, 2, 8, 11, "grey_d")
|
||||
t.rectn(4, 2, 8, 2, "red"); t.pn(6, 3, "white"); t.pn(7, 3, "white")
|
||||
t.hln(5, 6, 6, "grey"); t.hln(5, 8, 6, "grey"); t.hln(5, 10, 6, "grey")
|
||||
t.vln(7, 5, 6, "grey"); t.vln(9, 5, 6, "grey")
|
||||
t.pn(6, 9, "red"); t.pn(6, 10, "red")
|
||||
end
|
||||
|
||||
tiles[65] = function(t) -- recipe paper
|
||||
t.rectn(4, 3, 8, 10, "cream"); t.framen(4, 3, 8, 10, "cream_d")
|
||||
t.hln(5, 5, 6, "brown_d"); t.hln(5, 7, 6, "brown_d"); t.hln(5, 9, 4, "brown_d"); t.hln(5, 11, 5, "brown_d")
|
||||
t.pn(10, 11, "honey"); t.pn(11, 10, "honey_d")
|
||||
end
|
||||
|
||||
tiles[66] = function(t) -- document paper
|
||||
t.rectn(4, 2, 8, 12, "white"); t.framen(4, 2, 8, 12, "grey")
|
||||
for y = 4, 9, 2 do t.hln(5, y, 6, "grey_d") end
|
||||
t.discn(9, 11, 2, "red"); t.pn(9, 11, "white")
|
||||
end
|
||||
|
||||
-- picnic mat (67-70) drawn once across 2x2 (origin of tile 67)
|
||||
local function drawPicnic(img)
|
||||
local m = H.at(img, (67 % 16) * 16, math.floor(67 / 16) * 16)
|
||||
for y = 0, 31, 4 do
|
||||
for x = 0, 31, 4 do
|
||||
local c = (((x / 4) + (y / 4)) % 2 == 0) and "red" or "white"
|
||||
m.rectn(x, y, 4, 4, c)
|
||||
end
|
||||
end
|
||||
m.framen(0, 0, 32, 32, "red_d")
|
||||
m.framen(1, 1, 30, 30, "red_l")
|
||||
end
|
||||
|
||||
local function bench(t)
|
||||
t.rectn(0, 2, 16, 2, "wood"); t.hln(0, 2, 16, "wood_l")
|
||||
t.rectn(0, 5, 16, 2, "wood")
|
||||
t.rectn(0, 9, 16, 3, "wood_l"); t.hln(0, 11, 16, "wood_d")
|
||||
t.rectn(1, 12, 2, 4, "wood_d"); t.rectn(13, 12, 2, 4, "wood_d")
|
||||
t.vln(1, 2, 3, "wood_d"); t.vln(14, 2, 3, "wood_d")
|
||||
end
|
||||
tiles[71] = function(t) bench(t) end
|
||||
tiles[72] = function(t) bench(t) end
|
||||
|
||||
tiles[73] = function(t) -- stool
|
||||
t.rectn(4, 6, 8, 3, "wood"); t.hln(4, 6, 8, "wood_l"); t.hln(4, 8, 8, "wood_d")
|
||||
t.rectn(5, 9, 2, 6, "wood_d"); t.rectn(9, 9, 2, 6, "wood_d")
|
||||
t.hln(5, 12, 6, "wood_d")
|
||||
end
|
||||
|
||||
tiles[74] = function(t) -- cabinet
|
||||
t.rectn(2, 2, 12, 14, "wood"); t.framen(2, 2, 12, 14, "brown_d")
|
||||
t.vln(8, 3, 12, "brown_d")
|
||||
t.rectn(3, 4, 4, 5, "wood_d"); t.rectn(9, 4, 4, 5, "wood_d")
|
||||
t.pn(7, 10, "yellow"); t.pn(9, 10, "yellow")
|
||||
t.hln(2, 2, 12, "wood_l")
|
||||
end
|
||||
|
||||
tiles[75] = function(t) -- fridge
|
||||
t.rectn(3, 1, 10, 15, "white"); t.framen(3, 1, 10, 15, "grey")
|
||||
t.hln(3, 6, 10, "grey")
|
||||
t.rectn(11, 2, 1, 4, "grey_d"); t.rectn(11, 7, 1, 8, "grey_d")
|
||||
t.hln(4, 2, 8, "grey_l")
|
||||
end
|
||||
|
||||
tiles[76] = function(t) -- sink
|
||||
t.rectn(1, 6, 14, 10, "metal"); t.framen(1, 6, 14, 10, "metal_d")
|
||||
t.rectn(3, 8, 10, 5, "grey_d")
|
||||
t.rectn(4, 9, 8, 3, "water_l"); t.hln(4, 9, 8, "water")
|
||||
t.rectn(7, 3, 2, 4, "metal_d"); t.pn(7, 3, "metal"); t.pn(6, 3, "metal")
|
||||
t.hln(2, 7, 12, "grey_l")
|
||||
end
|
||||
|
||||
tiles[77] = function(t) -- potted plant
|
||||
t.discn(8, 6, 4, "leaf"); t.discn(7, 5, 2, "leaf_l"); t.discn(9, 7, 2, "leaf_d"); t.pn(8, 3, "leaf")
|
||||
t.rectn(5, 10, 6, 6, "brown"); t.framen(5, 10, 6, 6, "brown_d"); t.hln(5, 10, 6, "brown_l")
|
||||
end
|
||||
|
||||
tiles[78] = function(t) -- barrel
|
||||
t.ellipsen(8, 8, 6, 7, "wood")
|
||||
t.rectn(2, 1, 12, 14, "wood"); t.ellipsen(8, 8, 6, 7, "wood")
|
||||
t.hln(3, 4, 10, "wood_d"); t.hln(3, 11, 10, "wood_d")
|
||||
t.vln(8, 2, 12, "wood_d")
|
||||
t.framen(2, 1, 12, 14, "wood_d")
|
||||
t.hln(4, 2, 8, "wood_l")
|
||||
end
|
||||
|
||||
tiles[79] = function(t) -- crate
|
||||
t.rectn(2, 3, 12, 11, "wood"); t.framen(2, 3, 12, 11, "wood_d")
|
||||
t.vln(8, 3, 11, "wood_d"); t.hln(2, 8, 12, "wood_d")
|
||||
t.pn(3, 4, "wood_dd"); t.pn(12, 4, "wood_dd"); t.pn(3, 12, "wood_dd"); t.pn(12, 12, "wood_dd")
|
||||
t.hln(3, 4, 10, "wood_l")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 5 (ids 80-95) : fog / white-layer
|
||||
-- ===========================================================================
|
||||
local function fog(t, seed)
|
||||
local a1, a2 = 150, 110
|
||||
if seed == 0 then
|
||||
t.ellipse(8, 8, 7, 3, H.col("w_hi", a1))
|
||||
t.ellipse(5, 6, 4, 2, H.col("w_mid", a2))
|
||||
t.ellipse(11, 10, 4, 2, H.col("w_mid", a2))
|
||||
elseif seed == 1 then
|
||||
t.ellipse(7, 9, 6, 3, H.col("w_hi", a1))
|
||||
t.ellipse(11, 7, 4, 2, H.col("w_mid", a2))
|
||||
t.ellipse(4, 11, 3, 2, H.col("w_mid", a2))
|
||||
else
|
||||
t.ellipse(9, 7, 7, 3, H.col("w_hi", a1))
|
||||
t.ellipse(5, 9, 4, 2, H.col("w_mid", a2))
|
||||
t.ellipse(12, 11, 3, 2, H.col("w_mid", a2))
|
||||
end
|
||||
end
|
||||
tiles[80] = function(t) fog(t, 0) end
|
||||
tiles[81] = function(t) fog(t, 1) end
|
||||
tiles[82] = function(t) fog(t, 2) end
|
||||
|
||||
tiles[83] = function(t) -- white floor
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
t.hln(0, 0, 16, "w_mid"); t.hln(0, 8, 16, "w_mid")
|
||||
t.vln(0, 0, 16, "w_mid"); t.vln(8, 0, 16, "w_mid")
|
||||
t.pn(4, 4, "w_lo"); t.pn(12, 12, "w_lo"); t.pn(12, 4, "w_lo"); t.pn(4, 12, "w_lo")
|
||||
end
|
||||
|
||||
tiles[84] = function(t) -- white wall
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
t.hln(0, 0, 16, "w_lo"); t.hln(0, 8, 16, "w_lo")
|
||||
t.vln(0, 0, 16, "w_lo"); t.vln(8, 0, 16, "w_lo")
|
||||
t.rectn(0, 14, 16, 2, "w_mid"); t.hln(0, 14, 16, "w_hi")
|
||||
end
|
||||
|
||||
tiles[85] = function(t) -- white wall panel
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
t.framen(2, 2, 12, 11, "w_lo")
|
||||
t.vln(8, 2, 11, "w_mid")
|
||||
t.rectn(0, 14, 16, 2, "w_mid")
|
||||
end
|
||||
|
||||
tiles[86] = function(t) -- white door
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
t.rectn(3, 1, 10, 15, "w_mid"); t.framen(3, 1, 10, 15, "w_lo")
|
||||
t.rectn(4, 2, 8, 6, "w_hi"); t.framen(4, 2, 8, 6, "w_lo")
|
||||
t.rectn(4, 9, 8, 6, "w_hi"); t.framen(4, 9, 8, 6, "w_lo")
|
||||
t.pn(11, 8, "w_dk"); t.pn(11, 9, "w_dk")
|
||||
end
|
||||
|
||||
-- white hospital bed (87-90) across 2x2 (origin of tile 87)
|
||||
local function drawWhiteBed(img)
|
||||
local b = H.at(img, (87 % 16) * 16, math.floor(87 / 16) * 16)
|
||||
b.rectn(1, 2, 30, 27, "metal_d")
|
||||
b.rectn(3, 4, 26, 23, "white"); b.framen(3, 4, 26, 23, "w_lo")
|
||||
b.rectn(4, 6, 24, 6, "w_hi"); b.framen(4, 6, 24, 6, "w_mid")
|
||||
b.rectn(3, 17, 26, 2, "w_mid")
|
||||
b.rectn(1, 2, 30, 3, "metal"); b.rectn(1, 26, 30, 3, "metal")
|
||||
b.rectn(1, 2, 2, 27, "metal"); b.rectn(29, 2, 2, 27, "metal")
|
||||
b.rectn(2, 29, 3, 2, "w_dk"); b.rectn(27, 29, 3, 2, "w_dk")
|
||||
end
|
||||
|
||||
tiles[91] = function(t) -- white cabinet
|
||||
t.rectn(2, 2, 12, 14, "w_mid"); t.framen(2, 2, 12, 14, "w_lo")
|
||||
t.vln(8, 3, 12, "w_lo")
|
||||
t.rectn(3, 4, 4, 4, "w_hi"); t.rectn(9, 4, 4, 4, "w_hi")
|
||||
t.pn(7, 10, "w_dk"); t.pn(9, 10, "w_dk")
|
||||
t.hln(2, 2, 12, "w_hi")
|
||||
end
|
||||
|
||||
tiles[92] = function(t) -- white monitor (medical)
|
||||
t.rectn(3, 3, 10, 7, "w_lo"); t.framen(3, 3, 10, 7, "w_dk")
|
||||
t.rectn(4, 4, 8, 5, "screen")
|
||||
t.pn(4, 6, "screen_l"); t.pn(5, 6, "screen_l"); t.pn(6, 5, "screen_l")
|
||||
t.pn(7, 7, "screen_l"); t.pn(8, 6, "screen_l"); t.pn(9, 6, "screen_l"); t.pn(10, 6, "screen_l")
|
||||
t.rectn(7, 10, 2, 3, "w_dk"); t.rectn(5, 13, 6, 2, "w_lo"); t.hln(5, 14, 6, "w_dk")
|
||||
end
|
||||
|
||||
tiles[93] = function(t) -- white curtain
|
||||
t.rectn(2, 1, 12, 14, "w_hi")
|
||||
t.vln(4, 1, 14, "w_mid"); t.vln(7, 1, 14, "w_mid"); t.vln(10, 1, 14, "w_mid"); t.vln(12, 1, 14, "w_lo")
|
||||
t.rectn(2, 1, 12, 1, "w_lo")
|
||||
t.hln(2, 14, 12, "w_mid")
|
||||
end
|
||||
|
||||
tiles[94] = function(t) -- white floor alt
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
for y = 0, 15, 4 do
|
||||
for x = 0, 15, 4 do
|
||||
t.pn((x + 2) % 16, (y + 2) % 16, "w_lo")
|
||||
end
|
||||
end
|
||||
t.pn(2, 2, "w_mid"); t.pn(10, 10, "w_mid")
|
||||
end
|
||||
|
||||
tiles[95] = function(t) -- white ceiling light
|
||||
t.rectn(2, 6, 12, 4, "white"); t.framen(2, 6, 12, 4, "w_lo")
|
||||
t.rectn(3, 7, 10, 2, "w_hi")
|
||||
t.pn(7, 8, "white")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 6 (ids 96-111) : ladder / bridge / props
|
||||
-- ===========================================================================
|
||||
tiles[96] = function(t) -- ladder
|
||||
t.vln(4, 0, 16, "wood_d"); t.vln(11, 0, 16, "wood_d")
|
||||
for y = 2, 14, 3 do t.hln(4, y, 8, "wood"); t.hln(4, y, 8, "wood_l") end
|
||||
end
|
||||
|
||||
tiles[97] = function(t) -- bridge horizontal
|
||||
for y = 3, 12, 3 do
|
||||
t.rectn(0, y, 16, 2, "wood"); t.hln(0, y, 16, "wood_l"); t.hln(0, y + 1, 16, "wood_d")
|
||||
end
|
||||
t.rectn(0, 1, 16, 1, "wood_d"); t.rectn(0, 14, 16, 1, "wood_d")
|
||||
t.vln(0, 1, 14, "wood_d"); t.vln(15, 1, 14, "wood_d")
|
||||
end
|
||||
|
||||
tiles[98] = function(t) -- bridge vertical
|
||||
for x = 3, 12, 3 do
|
||||
t.rectn(x, 0, 2, 16, "wood"); t.vln(x, 0, 16, "wood_l"); t.vln(x + 1, 0, 16, "wood_d")
|
||||
end
|
||||
t.rectn(1, 0, 1, 16, "wood_d"); t.rectn(14, 0, 1, 16, "wood_d")
|
||||
t.hln(1, 0, 14, "wood_d"); t.hln(1, 15, 14, "wood_d")
|
||||
end
|
||||
|
||||
tiles[99] = function(t) -- mushroom(烘焙草地底:放在 ground 层,透明区会露黑底)
|
||||
grassBase(t)
|
||||
t.rectn(7, 8, 2, 6, "cream"); t.vln(7, 8, 6, "cream_d")
|
||||
t.discn(8, 6, 5, "red"); t.rectn(3, 6, 10, 2, "red")
|
||||
t.discn(8, 5, 4, "red_l")
|
||||
t.pn(6, 5, "white"); t.pn(9, 4, "white"); t.pn(10, 7, "white"); t.pn(5, 7, "white")
|
||||
end
|
||||
|
||||
local function bigflower(t, c)
|
||||
grassBase(t) -- 烘焙草地底:该瓦片放在 ground 层,透明区会露出黑底
|
||||
t.vln(8, 9, 6, "leaf"); t.pn(6, 12, "leaf"); t.pn(10, 11, "leaf"); t.pn(7, 13, "leaf_d")
|
||||
t.discn(8, 6, 3, c)
|
||||
t.discn(8, 6, 1, "yellow")
|
||||
t.pn(8, 6, "honey_d")
|
||||
end
|
||||
tiles[100] = function(t) bigflower(t, "red") end
|
||||
tiles[101] = function(t) bigflower(t, "yellow") end
|
||||
tiles[102] = function(t) bigflower(t, "blue") end
|
||||
|
||||
tiles[103] = function(t) -- honey jar
|
||||
t.rectn(5, 4, 6, 10, "honey"); t.framen(5, 4, 6, 10, "honey_d")
|
||||
t.rectn(5, 4, 6, 2, "honey_l")
|
||||
t.rectn(4, 2, 8, 2, "brown_d"); t.hln(4, 2, 8, "brown")
|
||||
t.rectn(6, 8, 4, 4, "honey_l"); t.pn(8, 10, "honey_dd")
|
||||
end
|
||||
|
||||
tiles[104] = function(t) -- bread
|
||||
t.ellipsen(8, 10, 7, 4, "honey")
|
||||
t.ellipsen(8, 9, 6, 3, "honey_l")
|
||||
t.pn(5, 9, "honey_d"); t.pn(8, 8, "honey_d"); t.pn(11, 9, "honey_d")
|
||||
t.hln(3, 12, 10, "honey_d")
|
||||
end
|
||||
|
||||
tiles[105] = function(t) -- letter
|
||||
t.rectn(3, 5, 10, 7, "cream"); t.framen(3, 5, 10, 7, "cream_d")
|
||||
for i = 0, 4 do t.pn(4 + i, 5 + i, "cream_d"); t.pn(11 - i, 5 + i, "cream_d") end
|
||||
t.pn(8, 9, "red")
|
||||
end
|
||||
|
||||
tiles[106] = function(t) -- book
|
||||
t.rectn(3, 4, 10, 9, "red"); t.framen(3, 4, 10, 9, "red_d")
|
||||
t.vln(8, 4, 9, "red_d"); t.hln(3, 4, 10, "red_l")
|
||||
t.rectn(4, 6, 3, 5, "red_d"); t.pn(5, 8, "yellow")
|
||||
end
|
||||
|
||||
tiles[107] = function(t) -- candle
|
||||
t.rectn(6, 6, 4, 9, "cream"); t.framen(6, 6, 4, 9, "cream_d")
|
||||
t.rectn(6, 6, 4, 1, "cream_d")
|
||||
t.pn(8, 5, "brown_d")
|
||||
t.discn(8, 3, 2, "flame"); t.pn(8, 2, "flame_d"); t.pn(8, 4, "flame_d")
|
||||
end
|
||||
|
||||
tiles[108] = function(t) -- basket
|
||||
t.rectn(3, 7, 10, 7, "wood"); t.framen(3, 7, 10, 7, "wood_d")
|
||||
t.hln(3, 9, 10, "wood_d"); t.hln(3, 11, 10, "wood_d")
|
||||
t.vln(6, 7, 7, "wood_d"); t.vln(9, 7, 7, "wood_d")
|
||||
t.vln(3, 4, 4, "wood_d"); t.vln(12, 4, 4, "wood_d"); t.hln(3, 4, 10, "wood_d")
|
||||
t.rectn(4, 5, 8, 2, "wood")
|
||||
end
|
||||
|
||||
tiles[109] = function(t) -- bucket
|
||||
t.rectn(4, 6, 8, 9, "metal"); t.framen(4, 6, 8, 9, "metal_d")
|
||||
t.rectn(4, 6, 8, 2, "water_l"); t.hln(4, 6, 8, "water")
|
||||
t.vln(4, 4, 3, "metal_d"); t.vln(11, 4, 3, "metal_d"); t.hln(4, 4, 8, "metal_d")
|
||||
t.hln(5, 10, 6, "metal_d")
|
||||
end
|
||||
|
||||
tiles[110] = function(t) -- broom
|
||||
t.vln(10, 1, 8, "wood_d"); t.vln(9, 1, 8, "wood")
|
||||
t.rectn(4, 9, 7, 6, "yellow_d"); t.framen(4, 9, 7, 6, "honey_d")
|
||||
t.hln(4, 9, 7, "brown_d")
|
||||
t.pn(3, 14, "yellow_d"); t.pn(11, 14, "yellow_d"); t.pn(4, 15, "honey_d"); t.pn(10, 15, "honey_d")
|
||||
end
|
||||
|
||||
tiles[111] = function(t) -- cake
|
||||
t.rectn(3, 8, 10, 6, "pink"); t.framen(3, 8, 10, 6, "pink_d")
|
||||
t.rectn(3, 8, 10, 2, "white")
|
||||
t.pn(4, 10, "pink_d"); t.pn(7, 10, "pink_d"); t.pn(10, 10, "pink_d")
|
||||
t.rectn(3, 14, 10, 1, "cream_d")
|
||||
t.vln(6, 5, 3, "red"); t.vln(9, 5, 3, "blue")
|
||||
t.pn(6, 4, "flame"); t.pn(9, 4, "flame")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 7 (ids 112-127) : colored roofs / walls / misc
|
||||
-- ===========================================================================
|
||||
tiles[112] = function(t) roof(t, "red", "red_d", "red_l", "L") end
|
||||
tiles[113] = function(t) roof(t, "red", "red_d", "red_l", nil) end
|
||||
tiles[114] = function(t) roof(t, "red", "red_d", "red_l", "R") end
|
||||
tiles[115] = function(t) roof(t, "blue", "blue_d", "blue_l", "L") end
|
||||
tiles[116] = function(t) roof(t, "blue", "blue_d", "blue_l", nil) end
|
||||
tiles[117] = function(t) roof(t, "blue", "blue_d", "blue_l", "R") end
|
||||
|
||||
local function pinkWall(t)
|
||||
t.rectn(0, 0, 16, 16, "pink")
|
||||
t.hln(0, 0, 16, "pink_d"); t.hln(0, 8, 16, "pink_d")
|
||||
t.vln(0, 0, 8, "pink_d"); t.vln(8, 0, 8, "pink_d")
|
||||
t.vln(4, 8, 8, "pink_d"); t.vln(12, 8, 8, "pink_d")
|
||||
t.pn(2, 4, "pink_l"); t.pn(10, 12, "pink_l")
|
||||
end
|
||||
tiles[118] = function(t) pinkWall(t) end
|
||||
tiles[119] = function(t)
|
||||
pinkWall(t)
|
||||
t.rectn(4, 3, 8, 9, "pink")
|
||||
H.window(t, 4, 3, 8, 9, "sky")
|
||||
end
|
||||
|
||||
local function stoneWall(t)
|
||||
t.rectn(0, 0, 16, 16, "swall")
|
||||
t.hln(0, 0, 16, "swall_d"); t.hln(0, 5, 16, "swall_d"); t.hln(0, 10, 16, "swall_d")
|
||||
t.vln(0, 0, 5, "swall_d"); t.vln(8, 0, 5, "swall_d")
|
||||
t.vln(4, 5, 5, "swall_d"); t.vln(12, 5, 5, "swall_d")
|
||||
t.vln(0, 10, 6, "swall_d"); t.vln(8, 10, 6, "swall_d")
|
||||
t.pn(3, 2, "swall_l"); t.pn(10, 7, "swall_l"); t.pn(6, 12, "swall_l")
|
||||
end
|
||||
tiles[120] = function(t) stoneWall(t) end
|
||||
tiles[121] = function(t)
|
||||
stoneWall(t)
|
||||
t.rectn(4, 3, 8, 9, "swall")
|
||||
H.window(t, 4, 3, 8, 9, "sky")
|
||||
end
|
||||
|
||||
tiles[122] = function(t) -- awning
|
||||
for x = 0, 15 do
|
||||
local c = (math.floor(x / 2) % 2 == 0) and "red" or "white"
|
||||
t.vln(x, 0, 6, c)
|
||||
end
|
||||
t.hln(0, 6, 16, "red_d")
|
||||
for x = 0, 15 do if x % 4 < 2 then t.pn(x, 7, "red") else t.pn(x, 7, "red_d") end end
|
||||
t.hln(0, 0, 16, "red_l")
|
||||
end
|
||||
|
||||
tiles[123] = function(t) -- clock
|
||||
t.discn(8, 8, 6, "brown_d"); t.discn(8, 8, 5, "cream")
|
||||
t.pn(8, 3, "brown_d"); t.pn(8, 13, "brown_d"); t.pn(3, 8, "brown_d"); t.pn(13, 8, "brown_d")
|
||||
t.vln(8, 4, 4, "ink"); t.hln(8, 8, 3, "ink"); t.pn(8, 8, "ink")
|
||||
end
|
||||
|
||||
tiles[124] = function(t) -- flag
|
||||
t.vln(3, 1, 15, "grey_d")
|
||||
t.rectn(4, 2, 9, 6, "red"); t.rectn(4, 2, 9, 2, "red_l")
|
||||
t.pn(13, 3, "red"); t.pn(13, 4, "red")
|
||||
t.rectn(2, 15, 3, 1, "grey")
|
||||
t.pn(4, 4, "yellow")
|
||||
end
|
||||
|
||||
tiles[125] = function(t) -- garland corner
|
||||
for x = 1, 12 do
|
||||
t.pn(x, 2, "leaf"); t.pn(x, 3, "leaf_d")
|
||||
if x % 3 == 0 then H.flower(t, x, 2, "pink", "yellow") end
|
||||
end
|
||||
for y = 1, 12 do
|
||||
t.pn(2, y, "leaf"); t.pn(3, y, "leaf_d")
|
||||
if y % 3 == 1 then H.flower(t, 2, y, "yellow", "honey") end
|
||||
end
|
||||
end
|
||||
|
||||
tiles[126] = function(t) -- garland mid (horizontal, tileable)
|
||||
for x = 0, 15 do t.pn(x, 4, "leaf"); t.pn(x, 5, "leaf_d") end
|
||||
H.flower(t, 3, 3, "pink", "yellow")
|
||||
H.flower(t, 8, 4, "red", "yellow")
|
||||
H.flower(t, 13, 3, "yellow", "honey")
|
||||
end
|
||||
|
||||
-- tile 127 intentionally left transparent (empty)
|
||||
|
||||
-- ===========================================================================
|
||||
-- render
|
||||
-- ===========================================================================
|
||||
-- 2x2 拼块的自然落点是 id、id+1、id+16、id+17,但 G 表/地图引用连续 4 个 id
|
||||
-- (id..id+3)。故每画完一块,立即把下半两个象限复制到 id+2、id+3;
|
||||
-- 自然落点 id+16/id+17 的残留随后被 tiles 循环里已定义的瓦片重绘覆盖。
|
||||
local function copyBlock(img, tlId)
|
||||
local ox = (tlId % 16) * 16
|
||||
local oy = math.floor(tlId / 16) * 16
|
||||
for dy = 0, 15 do
|
||||
for dx = 0, 15 do
|
||||
img:putPixel(ox + 32 + dx, oy + dy, img:getPixel(ox + dx, oy + 16 + dy)) -- 自然 BL → 槽位 id+2
|
||||
img:putPixel(ox + 48 + dx, oy + dy, img:getPixel(ox + 16 + dx, oy + 16 + dy)) -- 自然 BR → 槽位 id+3
|
||||
end
|
||||
end
|
||||
end
|
||||
drawTree(img); copyBlock(img, 18)
|
||||
drawWell(img); copyBlock(img, 42)
|
||||
drawBed(img); copyBlock(img, 52) -- 须在 drawPicnic 之前复制:二者自然落点都覆盖 68/69
|
||||
drawPicnic(img); copyBlock(img, 67)
|
||||
drawWhiteBed(img); copyBlock(img, 87)
|
||||
|
||||
for id = 0, 127 do
|
||||
if tiles[id] then tiles[id](H.tile(img, id)) end
|
||||
end
|
||||
|
||||
spr:saveAs(ROOT .. "public/assets/tileset.png")
|
||||
print("tileset.png done")
|
||||
@@ -0,0 +1,229 @@
|
||||
-- helpers.lua — shared palette + pixel primitives for asset generation
|
||||
-- Usage: local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
-- All draw helpers operate on an Aseprite Image via a "pen" table bound to an origin.
|
||||
|
||||
local H = {}
|
||||
|
||||
H.ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Candy palette (CONTRACT §1) + white-layer grays + a few flat utility shades.
|
||||
-- Stored as {r,g,b}. No gradients, no AA — every pixel is one of these.
|
||||
-- ----------------------------------------------------------------------------
|
||||
H.P = {
|
||||
-- grass greens
|
||||
grass = {125, 201, 94}, -- #7DC95E
|
||||
grass_d = { 95, 179, 74}, -- #5FB34A
|
||||
grass_dd = { 74, 148, 58},
|
||||
grass_l = {158, 216, 128},
|
||||
-- cream
|
||||
cream = {255, 243, 214}, -- #FFF3D6
|
||||
cream_d = {238, 220, 178},
|
||||
cream_dd = {216, 196, 150},
|
||||
-- honey orange
|
||||
honey = {245, 166, 35}, -- #F5A623
|
||||
honey_d = {224, 143, 18},
|
||||
honey_dd = {196, 120, 10},
|
||||
honey_l = {250, 193, 96},
|
||||
-- sky blue
|
||||
sky = {143, 211, 255}, -- #8FD3FF
|
||||
sky_d = {110, 186, 240},
|
||||
-- stone road
|
||||
stone = {232, 220, 200}, -- #E8DCC8
|
||||
stone_d = {206, 190, 162},
|
||||
stone_dd = {178, 160, 130},
|
||||
stone_l = {245, 237, 224},
|
||||
-- lake water
|
||||
water = {110, 193, 228}, -- #6EC1E4
|
||||
water_d = { 79, 168, 216}, -- #4FA8D8
|
||||
water_dd = { 56, 140, 190},
|
||||
water_l = {158, 216, 242},
|
||||
-- pink
|
||||
pink = {247, 183, 203}, -- #F7B7CB
|
||||
pink_d = {232, 150, 176},
|
||||
pink_dd = {208, 120, 150},
|
||||
pink_l = {252, 208, 222},
|
||||
-- brown (trunk / wood dark)
|
||||
brown = {139, 90, 43}, -- #8B5A2B
|
||||
brown_d = {108, 68, 30},
|
||||
brown_dd = { 84, 52, 22},
|
||||
brown_l = {172, 122, 72},
|
||||
-- dirt road
|
||||
dirt = {205, 172, 122},
|
||||
dirt_d = {182, 148, 98},
|
||||
dirt_l = {220, 190, 146},
|
||||
-- white-layer grays (CONTRACT §1)
|
||||
w_hi = {245, 245, 245}, -- #F5F5F5
|
||||
w_mid = {218, 218, 218}, -- #DADADA
|
||||
w_lo = {189, 189, 189}, -- #BDBDBD
|
||||
w_dk = {150, 150, 150},
|
||||
w_dd = {110, 110, 110},
|
||||
-- reds
|
||||
red = {226, 87, 77},
|
||||
red_d = {198, 66, 56},
|
||||
red_l = {240, 130, 120},
|
||||
-- blues
|
||||
blue = { 91, 155, 213},
|
||||
blue_d = { 62, 125, 190},
|
||||
blue_l = {140, 190, 232},
|
||||
-- yellows
|
||||
yellow = {247, 214, 92},
|
||||
yellow_d = {224, 186, 55},
|
||||
yellow_l = {252, 228, 140},
|
||||
-- foliage
|
||||
leaf = { 86, 168, 78},
|
||||
leaf_d = { 64, 138, 60},
|
||||
leaf_dd = { 46, 108, 44},
|
||||
leaf_l = {120, 192, 110},
|
||||
-- wood
|
||||
wood = {201, 152, 96},
|
||||
wood_d = {176, 126, 74},
|
||||
wood_dd = {150, 104, 58},
|
||||
wood_l = {220, 178, 124},
|
||||
-- stone wall (cool gray)
|
||||
swall = {196, 190, 178},
|
||||
swall_d = {164, 158, 146},
|
||||
swall_dd = {134, 128, 116},
|
||||
swall_l = {214, 209, 200},
|
||||
-- berry
|
||||
berry = {206, 62, 96},
|
||||
berry_d = {172, 44, 76},
|
||||
-- misc
|
||||
skin = {255, 224, 190},
|
||||
skin_d = {238, 198, 156},
|
||||
hair = {120, 84, 52},
|
||||
hair_d = { 96, 66, 40},
|
||||
hair_l = {152, 112, 74},
|
||||
black = { 54, 44, 40},
|
||||
ink = { 40, 34, 30},
|
||||
white = {255, 255, 255},
|
||||
grey = {150, 150, 150},
|
||||
grey_d = {108, 108, 108},
|
||||
grey_l = {196, 196, 196},
|
||||
screen = { 44, 58, 54},
|
||||
screen_l = {120, 220, 160},
|
||||
metal = {178, 184, 190},
|
||||
metal_d = {140, 146, 152},
|
||||
flame = {255, 214, 92},
|
||||
flame_d = {245, 140, 40},
|
||||
leaf2 = { 60, 150, 70},
|
||||
}
|
||||
|
||||
-- make an Aseprite color from a palette name (or {r,g,b} table) + optional alpha
|
||||
function H.col(name, a)
|
||||
local t = name
|
||||
if type(name) == "string" then t = H.P[name] end
|
||||
return app.pixelColor.rgba(t[1], t[2], t[3], a or 255)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Pen: a table of draw methods bound to an image + origin (ox, oy).
|
||||
-- All coordinates passed to pen methods are LOCAL to the origin.
|
||||
-- ----------------------------------------------------------------------------
|
||||
function H.at(img, ox, oy)
|
||||
local t = { img = img, ox = ox, oy = oy }
|
||||
function t.p(x, y, c) img:drawPixel(ox + x, oy + y, c) end
|
||||
function t.pn(x, y, name, a) img:drawPixel(ox + x, oy + y, H.col(name, a)) end
|
||||
function t.rect(x, y, w, h, c)
|
||||
for j = 0, h - 1 do for i = 0, w - 1 do img:drawPixel(ox + x + i, oy + y + j, c) end end
|
||||
end
|
||||
function t.rectn(x, y, w, h, name, a)
|
||||
local c = H.col(name, a)
|
||||
for j = 0, h - 1 do for i = 0, w - 1 do img:drawPixel(ox + x + i, oy + y + j, c) end end
|
||||
end
|
||||
function t.hl(x, y, len, c) for i = 0, len - 1 do img:drawPixel(ox + x + i, oy + y, c) end end
|
||||
function t.hln(x, y, len, name, a)
|
||||
local c = H.col(name, a); for i = 0, len - 1 do img:drawPixel(ox + x + i, oy + y, c) end
|
||||
end
|
||||
function t.vl(x, y, len, c) for j = 0, len - 1 do img:drawPixel(ox + x, oy + y + j, c) end end
|
||||
function t.vln(x, y, len, name, a)
|
||||
local c = H.col(name, a); for j = 0, len - 1 do img:drawPixel(ox + x, oy + y + j, c) end
|
||||
end
|
||||
-- outline rectangle
|
||||
function t.frame(x, y, w, h, c)
|
||||
t.hl(x, y, w, c); t.hl(x, y + h - 1, w, c)
|
||||
t.vl(x, y, h, c); t.vl(x + w - 1, y, h, c)
|
||||
end
|
||||
function t.framen(x, y, w, h, name, a)
|
||||
t.frame(x, y, w, h, H.col(name, a))
|
||||
end
|
||||
-- filled disc (filled circle), integer radius
|
||||
function t.disc(cx, cy, r, c)
|
||||
for yy = -r, r do for xx = -r, r do
|
||||
if xx * xx + yy * yy <= r * r then t.p(cx + xx, cy + yy, c) end
|
||||
end end
|
||||
end
|
||||
function t.discn(cx, cy, r, name, a) t.disc(cx, cy, r, H.col(name, a)) end
|
||||
-- filled ellipse
|
||||
function t.ellipse(cx, cy, rx, ry, c)
|
||||
for yy = -ry, ry do for xx = -rx, rx do
|
||||
if (xx * xx) * (ry * ry) + (yy * yy) * (rx * rx) <= (rx * rx) * (ry * ry) then
|
||||
t.p(cx + xx, cy + yy, c)
|
||||
end
|
||||
end end
|
||||
end
|
||||
function t.ellipsen(cx, cy, rx, ry, name, a) t.ellipse(cx, cy, rx, ry, H.col(name, a)) end
|
||||
return t
|
||||
end
|
||||
|
||||
-- pen for a 16x16 tile by id (tileset is 16 columns)
|
||||
function H.tile(img, id)
|
||||
local ox = (id % 16) * 16
|
||||
local oy = math.floor(id / 16) * 16
|
||||
return H.at(img, ox, oy)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Motif helpers (drawn through a pen `t`, local coords)
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
-- a small 5-petal flower centered at (cx,cy)
|
||||
function H.flower(t, cx, cy, petal, center)
|
||||
local pc = H.col(petal); local cc = H.col(center or "yellow")
|
||||
t.p(cx, cy - 1, pc); t.p(cx, cy + 1, pc)
|
||||
t.p(cx - 1, cy, pc); t.p(cx + 1, cy, pc)
|
||||
t.p(cx, cy, cc)
|
||||
end
|
||||
|
||||
-- a window with brown frame + sky panes, top-left (x,y), size w x h
|
||||
function H.window(t, x, y, w, h, glass)
|
||||
glass = glass or "sky"
|
||||
t.rectn(x, y, w, h, "brown_d")
|
||||
t.rectn(x + 1, y + 1, w - 2, h - 2, glass)
|
||||
-- mullions
|
||||
local mx = x + math.floor(w / 2)
|
||||
local my = y + math.floor(h / 2)
|
||||
t.vln(mx, y + 1, h - 2, "brown_d")
|
||||
t.hln(x + 1, my, w - 2, "brown_d")
|
||||
-- glass highlight
|
||||
t.pn(x + 2, y + 2, "sky_l" == glass and "white" or "water_l")
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Mirror pen: wraps a pen `t`, flipping x within a `w`-wide frame.
|
||||
-- Lets one directional drawing serve both left and right facings.
|
||||
-- ----------------------------------------------------------------------------
|
||||
function H.mirrorX(t, w)
|
||||
local m = {}
|
||||
function m.p(x, y, c) t.p(w - 1 - x, y, c) end
|
||||
function m.pn(x, y, name, a) t.pn(w - 1 - x, y, name, a) end
|
||||
function m.rect(x, y, ww, h, c) t.rect(w - x - ww, y, ww, h, c) end
|
||||
function m.rectn(x, y, ww, h, name, a) t.rectn(w - x - ww, y, ww, h, name, a) end
|
||||
function m.hl(x, y, len, c) t.hl(w - x - len, y, len, c) end
|
||||
function m.hln(x, y, len, name, a) t.hln(w - x - len, y, len, name, a) end
|
||||
function m.vl(x, y, len, c) t.vl(w - 1 - x, y, len, c) end
|
||||
function m.vln(x, y, len, name, a) t.vln(w - 1 - x, y, len, name, a) end
|
||||
function m.framen(x, y, ww, h, name, a) t.framen(w - x - ww, y, ww, h, name, a) end
|
||||
function m.discn(cx, cy, r, name, a) t.discn(w - 1 - cx, cy, r, name, a) end
|
||||
function m.ellipsen(cx, cy, rx, ry, name, a) t.ellipsen(w - 1 - cx, cy, rx, ry, name, a) end
|
||||
return m
|
||||
end
|
||||
|
||||
-- create a fresh RGB sprite + return (sprite, image)
|
||||
function H.new(w, h)
|
||||
local spr = Sprite(w, h, ColorMode.RGB)
|
||||
local img = app.activeImage
|
||||
return spr, img
|
||||
end
|
||||
|
||||
return H
|
||||
@@ -0,0 +1,42 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const open = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
if (!open) break;
|
||||
await tap('KeyE'); await sleep(200);
|
||||
}
|
||||
const dump = () => page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('HouseScene');
|
||||
return {
|
||||
player: { x: Math.round(s.player.x), y: Math.round(s.player.y) },
|
||||
vel: { x: Math.round(s.player.body.velocity.x), y: Math.round(s.player.body.velocity.y) },
|
||||
doors: s.doors,
|
||||
dlg: window.__director.dialogueActive,
|
||||
transitioning: s.transitioning,
|
||||
armed: s.armedDoors ? s.armedDoors.size : -1,
|
||||
};
|
||||
});
|
||||
console.log('before:', JSON.stringify(await dump()));
|
||||
await page.keyboard.down('KeyS'); await sleep(1200); await page.keyboard.up('KeyS');
|
||||
console.log('after KeyS:', JSON.stringify(await dump()));
|
||||
await page.keyboard.down('KeyS'); await sleep(1500); await page.keyboard.up('KeyS');
|
||||
console.log('after more KeyS:', JSON.stringify(await dump()));
|
||||
console.log('plaza active:', await page.evaluate(() => window.__game.scene.isActive('PlazaScene')));
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,50 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
// 记录对话开关
|
||||
await page.evaluate(() => {
|
||||
window.__dlgLog = [];
|
||||
const d = window.__director;
|
||||
const origShow = d.showDialogue.bind(d);
|
||||
d.showDialogue = (p) => { window.__dlgLog.push(`${Date.now()%100000} OPEN 「${(p.lines&&p.lines[0]||'').slice(0,18)}」`); return origShow(p); };
|
||||
const origRes = d.resolveDialogue.bind(d);
|
||||
d.resolveDialogue = (c) => { window.__dlgLog.push(`${Date.now()%100000} CLOSE`); return origRes(c); };
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); }
|
||||
sm.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
await sleep(500);
|
||||
// 清残留
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
await page.evaluate(() => { window.__dlgLog.length = 0; });
|
||||
// 站到阿福旁,以真人节奏 ~500ms 按 E
|
||||
await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const a = s.actors.find(a => a.def.id === 'afu');
|
||||
s.player.setPosition(a.sprite.x, a.sprite.y + 8); s.player.setVelocity(0, 0);
|
||||
});
|
||||
await sleep(150);
|
||||
console.log('--- 开始:第 1 次按 E(开对话)');
|
||||
await tap('KeyE');
|
||||
for (let i = 0; i < 8; i++) { await sleep(500); console.log(` t+${(i+1)*0.5}s 按 E`); await tap('KeyE'); }
|
||||
console.log('--- 停止按键,观察 4 秒是否自动重开 ---');
|
||||
await sleep(4000);
|
||||
for (const l of await page.evaluate(() => window.__dlgLog)) console.log(l);
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,70 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
// 记录对话开关
|
||||
await page.evaluate(() => {
|
||||
window.__dlgLog = [];
|
||||
const d = window.__director;
|
||||
const origShow = d.showDialogue.bind(d);
|
||||
d.showDialogue = (p) => { window.__dlgLog.push(`OPEN 「${(p.lines&&p.lines[0]||'').slice(0,18)}」`); return origShow(p); };
|
||||
const origRes = d.resolveDialogue.bind(d);
|
||||
d.resolveDialogue = (c) => { window.__dlgLog.push('CLOSE'); return origRes(c); };
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); }
|
||||
sm.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
await sleep(500);
|
||||
// 清残留对话
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
await page.evaluate(() => { window.__dlgLog.length = 0; });
|
||||
// 传送到雾门正上方(触发圈内 dist≈10),模拟玩家从上方走近雾门后松开按键
|
||||
const info = await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const d = s.doors.find(d => d.target === 'fog');
|
||||
s.player.setPosition(d.x, d.y - 10 + 8); // 测量点 player.y-8 → py = d.y-10
|
||||
s.player.setVelocity(0, 0);
|
||||
return { door: { x: d.x, y: d.y }, day: window.__director.day };
|
||||
});
|
||||
console.log('雾门位置', info.door, 'day', info.day);
|
||||
// 等待劝离对话自动弹出(靠近即触发,无需按键)
|
||||
await sleep(1500);
|
||||
const afterTrigger = await page.evaluate(() => window.__dlgLog.filter(l => l.startsWith('OPEN')).length);
|
||||
console.log('触发后 OPEN 次数:', afterTrigger);
|
||||
if (afterTrigger !== 1) { console.log('FAIL: 劝离对话应恰好弹出 1 次'); process.exit(1); }
|
||||
// 按 E 关闭对话(打字机:第一下补全文字,第二下才关闭;canUseDoor 在对话关闭后才返回并执行推离)
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
await sleep(400);
|
||||
const afterClose = await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const d = s.doors.find(d => d.target === 'fog');
|
||||
const py = s.player.y - 8;
|
||||
return { dist: Math.hypot(s.player.x - d.x, py - d.y).toFixed(1) };
|
||||
});
|
||||
console.log('关闭并推离后玩家距门:', afterClose.dist);
|
||||
if (parseFloat(afterClose.dist) <= 14) { console.log('FAIL: 劝离推离后玩家仍在触发圈内 dist=' + afterClose.dist); process.exit(1); }
|
||||
// 静止观察 5 秒,不得重开
|
||||
await sleep(5000);
|
||||
const final = await page.evaluate(() => window.__dlgLog);
|
||||
console.log('最终日志:', final);
|
||||
const opens = final.filter(l => l.startsWith('OPEN')).length;
|
||||
const closes = final.filter(l => l === 'CLOSE').length;
|
||||
if (opens === 1 && closes >= 1) { console.log('PASS: 劝离对话只弹一次,关闭后 5 秒未重开'); }
|
||||
else { console.log(`FAIL: OPEN=${opens} CLOSE=${closes}`); process.exit(1); }
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,69 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
await page.evaluate(() => {
|
||||
window.__dlgLog = [];
|
||||
const d = window.__director;
|
||||
const origShow = d.showDialogue.bind(d);
|
||||
d.showDialogue = (p) => { window.__dlgLog.push(`OPEN 「${(p.lines&&p.lines[0]||'').slice(0,14)}」`); return origShow(p); };
|
||||
const origRes = d.resolveDialogue.bind(d);
|
||||
d.resolveDialogue = (c) => { window.__dlgLog.push('CLOSE'); return origRes(c); };
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); }
|
||||
sm.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
await sleep(500);
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
await page.evaluate(() => {
|
||||
window.__dlgLog.length = 0;
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const d = s.doors.find(d => d.target === 'fog');
|
||||
window.__door = d;
|
||||
// 记录 canUseDoor 调用现场
|
||||
const origCUD = s.canUseDoor.bind(s);
|
||||
s.canUseDoor = (t) => {
|
||||
const dd = s.doors.find(x => x.target === t);
|
||||
const dist = dd ? Math.hypot(s.player.x - dd.x, (s.player.y - 8) - dd.y) : -1;
|
||||
window.__dlgLog.push(`canUseDoor(${t}) dist=${dist.toFixed(1)} armed=${dd ? s.armedDoors.has(dd) : '?'}`);
|
||||
return origCUD(t);
|
||||
};
|
||||
s.player.setPosition(d.x, d.y - 10 + 8);
|
||||
s.player.setVelocity(0, 0);
|
||||
});
|
||||
await sleep(1200); // 等劝离对话弹出
|
||||
// 关闭对话
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
// 高频采样 3 秒
|
||||
const samples = await page.evaluate(async () => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const d = window.__door;
|
||||
const out = [];
|
||||
const t0 = Date.now();
|
||||
return await new Promise((resolve) => {
|
||||
const iv = setInterval(() => {
|
||||
const dist = Math.hypot(s.player.x - d.x, (s.player.y - 8) - d.y);
|
||||
out.push(`${Date.now() - t0}ms pos=(${s.player.x.toFixed(1)},${s.player.y.toFixed(1)}) body=(${s.player.body.x.toFixed(1)},${s.player.body.y.toFixed(1)}) dist=${dist.toFixed(1)} dlg=${window.__director.dialogueActive} log=${window.__dlgLog.length}`);
|
||||
if (Date.now() - t0 > 3000) { clearInterval(iv); resolve(out); }
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
for (const l of samples) console.log(l);
|
||||
console.log('LOG:', await page.evaluate(() => window.__dlgLog));
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,31 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
await sleep(600);
|
||||
// 依次跳转地图并截图
|
||||
for (const [sceneKey, spawn] of [['HouseScene','default'],['PlazaScene','default'],['FieldScene','default'],['WellScene','default'],['LakeScene','default']]) {
|
||||
await page.evaluate(([k, sp]) => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const kk = sc.scene.key; if (kk !== 'UIScene' && kk !== k) sm.stop(kk); }
|
||||
sm.start(k, { spawn: sp });
|
||||
}, [sceneKey, spawn]);
|
||||
await page.waitForFunction((k) => window.__game.scene.isActive(k), { timeout: 9000 }, sceneKey);
|
||||
await sleep(900);
|
||||
await page.screenshot({ path: `tools/shots/fix_${sceneKey}.png` });
|
||||
console.log('shot', sceneKey);
|
||||
}
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,43 @@
|
||||
// 最终清扫:任何「含透明像素的瓦片」都不得出现在任何地图的 ground 层(否则露黑底)
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import fs from 'node:fs';
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true });
|
||||
const page = await browser.newPage();
|
||||
// 把 tileset.png 读成 dataURL 注入页面分析 alpha
|
||||
const b64 = fs.readFileSync('public/assets/tileset.png').toString('base64');
|
||||
const transparentIds = await page.evaluate(async (data) => {
|
||||
const img = new Image();
|
||||
await new Promise((res) => { img.onload = res; img.src = data; });
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width; c.height = img.height;
|
||||
const ctx = c.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const d = ctx.getImageData(0, 0, img.width, img.height).data;
|
||||
const out = [];
|
||||
for (let id = 0; id < 128; id++) {
|
||||
const ox = (id % 16) * 16, oy = Math.floor(id / 16) * 16;
|
||||
let trans = false;
|
||||
for (let y = 0; y < 16 && !trans; y++)
|
||||
for (let x = 0; x < 16; x++)
|
||||
if (d[((oy + y) * img.width + ox + x) * 4 + 3] < 255) { trans = true; break; }
|
||||
if (trans) out.push(id);
|
||||
}
|
||||
return out;
|
||||
}, 'data:image/png;base64,' + b64);
|
||||
await browser.close();
|
||||
console.log('含透明像素的瓦片 id:', transparentIds.join(','));
|
||||
const maps = ['house','plaza','bakery','shop','clinic','lake','field','well','fog','white'];
|
||||
let bad = 0;
|
||||
for (const key of maps) {
|
||||
const m = JSON.parse(fs.readFileSync(`public/assets/maps/${key}.json`, 'utf8'));
|
||||
const g = m.layers.find((l) => l.name === 'ground');
|
||||
const hits = new Set();
|
||||
for (const gid of g.data) {
|
||||
if (gid === 0) { hits.add('EMPTY'); continue; }
|
||||
const id = gid - 1;
|
||||
if (transparentIds.includes(id)) hits.add(id);
|
||||
}
|
||||
if (hits.size) { bad++; console.log(`FAIL ${key}: ground 层含透明瓦片/空格 ->`, [...hits].join(',')); }
|
||||
else console.log(`OK ${key}`);
|
||||
}
|
||||
process.exit(bad ? 1 : 0);
|
||||
@@ -0,0 +1,131 @@
|
||||
// dbg16:复现「面包无法提交」与「第一天睡觉卡死」
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; });
|
||||
async function drain(maxIter = 40) {
|
||||
const texts = [];
|
||||
for (let i = 0; i < maxIter; i++) {
|
||||
if (!(await dlgOpen())) break;
|
||||
texts.push(await dlgText());
|
||||
const choosing = await ev(() => { const u = window.__game.scene.getScene('UIScene'); return u.showingChoices; });
|
||||
await tap(choosing ? 'Digit1' : 'KeyE');
|
||||
await sleep(160);
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
async function startScene(k, spawn='default') {
|
||||
await ev((k,s) => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const kk = sc.scene.key; if (kk !== 'UIScene' && kk !== k) sm.stop(kk); }
|
||||
sm.start(k, { spawn: s });
|
||||
}, k, spawn);
|
||||
await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k);
|
||||
await sleep(500);
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
const dstate = () => ev(() => ({
|
||||
day: window.__director.day,
|
||||
breadTaken: window.__director.taskProgress['breadTaken'] ?? 0,
|
||||
breadTo: [...(window.__director.taskSetProgress['breadTo'] ?? [])],
|
||||
done: [...window.__director.tasksDone],
|
||||
canSleep: window.__director.canSleep,
|
||||
dlg: window.__director.dialogueActive,
|
||||
}));
|
||||
await sleep(600);
|
||||
|
||||
console.log('\n=== A. 面包任务提交 ===');
|
||||
// 1. 面包房取面包
|
||||
await startScene('BakeryScene');
|
||||
await drain();
|
||||
// counter_bread 交互区在地图对象里:从 JSON 读坐标
|
||||
const cbObj = await ev(() => window.__game.scene.getScene('BakeryScene').interactZones.find(z => z.id === 'counter_bread'));
|
||||
console.log('counter_bread zone:', cbObj);
|
||||
await setPlayer('BakeryScene', cbObj.x + 40, cbObj.y + 40);
|
||||
await setPlayer('BakeryScene', cbObj.x, cbObj.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
const t1 = await drain();
|
||||
console.log('取面包对话:', t1);
|
||||
console.log('状态:', await dstate());
|
||||
|
||||
// 2. 送 dabao(plaza)
|
||||
await startScene('PlazaScene');
|
||||
await drain();
|
||||
const dabao = await ev(() => { const s = window.__game.scene.getScene('PlazaScene'); const a = s.actors.find(a => a.def.id === 'dabao'); return { x: a.sprite.x, y: a.sprite.y }; });
|
||||
await setPlayer('PlazaScene', dabao.x + 40, dabao.y + 40);
|
||||
await setPlayer('PlazaScene', dabao.x, dabao.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
const t2 = await drain();
|
||||
console.log('dabao 对话:', t2);
|
||||
console.log('状态:', await dstate());
|
||||
|
||||
// 3. 送 mimi(shop)与 guiyeye(lake)
|
||||
await startScene('ShopScene');
|
||||
await drain();
|
||||
const mimi = await ev(() => { const s = window.__game.scene.getScene('ShopScene'); const a = s.actors.find(a => a.def.id === 'mimi'); return { x: a.sprite.x, y: a.sprite.y }; });
|
||||
await setPlayer('ShopScene', mimi.x + 40, mimi.y + 40);
|
||||
await setPlayer('ShopScene', mimi.x, mimi.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
console.log('mimi 对话:', await drain());
|
||||
console.log('状态:', await dstate());
|
||||
|
||||
await startScene('LakeScene');
|
||||
await drain();
|
||||
const gui = await ev(() => { const s = window.__game.scene.getScene('LakeScene'); const a = s.actors.find(a => a.def.id === 'guiyeye'); return { x: a.sprite.x, y: a.sprite.y }; });
|
||||
await setPlayer('LakeScene', gui.x + 40, gui.y + 40);
|
||||
await setPlayer('LakeScene', gui.x, gui.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
console.log('guiyeye 对话:', await drain());
|
||||
console.log('状态:', await dstate());
|
||||
|
||||
console.log('\n=== B. 第一天睡觉(用户场景:任务未全了结,靠黄昏计时) ===');
|
||||
await startScene('HouseScene');
|
||||
await drain();
|
||||
// 强制黄昏计时,使 canSleep=true(模拟用户等满 5 分钟)
|
||||
await ev(() => { window.__director.dayElapsedMs = 300000; });
|
||||
console.log('睡前状态:', await dstate());
|
||||
// 走到床边按 E
|
||||
const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bed.x + 40, bed.y + 40);
|
||||
await setPlayer('HouseScene', bed.x, bed.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
console.log('床交互后对话:', await drain());
|
||||
// 采样 20 秒:场景序列与卡死点
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const s = await ev(() => {
|
||||
const sm = window.__game.scene;
|
||||
return {
|
||||
active: sm.getScenes(true).map(sc => sc.scene.key),
|
||||
day: window.__director.day,
|
||||
dlg: window.__director.dialogueActive,
|
||||
uiPayload: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 20) : null; })(),
|
||||
};
|
||||
});
|
||||
console.log(`t+${(i+1)}s`, JSON.stringify(s));
|
||||
// 模拟玩家按 E 推进梦境对话
|
||||
if (s.uiPayload) { await tap('KeyE'); }
|
||||
await sleep(1000);
|
||||
}
|
||||
console.log('最终状态:', await dstate());
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,84 @@
|
||||
// dbg17:睡觉卡死候选根因验证
|
||||
// (a) 床淡出期间庆典触发 → celebrationActive 卡死 / 对话 resolver 被覆盖
|
||||
// (b) 床淡出期间玩家移动 30px 再按 E → 二次触发床 → 双 scene.start
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,500)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const state = (tag) => ev((tag) => {
|
||||
const sm = window.__game.scene;
|
||||
const d = window.__director;
|
||||
const s = sm.getScene('HouseScene');
|
||||
const door = s && s.doors ? s.doors[0] : null;
|
||||
const pd = door && s.player ? { px: +s.player.x.toFixed(0), py: +(s.player.y - 8).toFixed(0),
|
||||
dist: +Math.hypot(s.player.x - door.x, (s.player.y - 8) - door.y).toFixed(1),
|
||||
armed: s.armedDoors.has(door), trans: s.transitioning } : null;
|
||||
return { tag, active: sm.getScenes(true).map(sc => sc.scene.key), day: d.day,
|
||||
dlg: d.dialogueActive, celeb: d.celebrationActive, happy: d.happiness,
|
||||
ui: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 18) : null; })(),
|
||||
pd };
|
||||
}, tag).then(s => console.log(JSON.stringify(s)));
|
||||
async function drain(maxIter = 20) {
|
||||
for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; await tap('KeyE'); await sleep(160); }
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
await sleep(600);
|
||||
const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
|
||||
console.log('\n=== (a) 床淡出期间庆典触发 ===');
|
||||
// 低幸福 + 低幸福计时逼近阈值;强制可睡觉
|
||||
await ev(() => {
|
||||
const d = window.__director;
|
||||
d.setHappiness(20);
|
||||
d.lowHappyMs = 119900;
|
||||
d.dayElapsedMs = 300000;
|
||||
});
|
||||
// 按 E 睡觉 → 淡出 400ms 内 tick 必触发庆典
|
||||
await setPlayer('HouseScene', bed.x, bed.y + 8);
|
||||
await tap('KeyE');
|
||||
for (let i = 0; i < 12; i++) { await sleep(500); await state(`a t+${(i+1)*0.5}s`); if (await dlgOpen()) await tap('KeyE'); }
|
||||
// 尝试走到门边逃脱(门在 (88,104))
|
||||
await state('a 结束,尝试过门');
|
||||
await setPlayer('HouseScene', 88 + 30, 104);
|
||||
await setPlayer('HouseScene', 88, 104);
|
||||
await sleep(1500);
|
||||
await state('a 过门后');
|
||||
|
||||
console.log('\n=== (b) 床淡出期间二次触发 ===');
|
||||
await ev(() => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'HouseScene') sm.stop(k); }
|
||||
sm.start('HouseScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 9000 });
|
||||
await sleep(500);
|
||||
await drain();
|
||||
await ev(() => { window.__director.dayElapsedMs = 300000; });
|
||||
const bed2 = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bed2.x, bed2.y + 8);
|
||||
await tap('KeyE'); // 第一次:触发床,淡出开始
|
||||
await sleep(150);
|
||||
await setPlayer('HouseScene', bed2.x + 40, bed2.y + 8); // 移动 >30px 使 spent 重武装
|
||||
await tap('KeyE'); // 第二次:可能二次触发床
|
||||
for (let i = 0; i < 10; i++) { await sleep(500); await state(`b t+${(i+1)*0.5}s`); if (await dlgOpen()) await tap('KeyE'); }
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,116 @@
|
||||
// dbg18:睡觉卡死根因验证(干净页面)
|
||||
// A 基线:真实流程睡觉+连按 E,应正常过日到 D2
|
||||
// B 嫌疑2:床淡出 400ms 内移动 30px 二次按 E → 是否双触发/卡死
|
||||
// C 嫌疑1:梦境对话期间注入第二个 showDialogue → resolver 覆盖是否导致 run() 永久悬挂
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,500)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const state = (tag) => ev((tag) => {
|
||||
const sm = window.__game.scene;
|
||||
const d = window.__director;
|
||||
return { tag, active: sm.getScenes(true).map(sc => sc.scene.key), day: d.day,
|
||||
dlg: d.dialogueActive, celeb: d.celebrationActive, happy: d.happiness,
|
||||
ui: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 22) : null; })() };
|
||||
}, tag).then(s => console.log(JSON.stringify(s)));
|
||||
async function drain(maxIter = 30) {
|
||||
for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; await tap('KeyE'); await sleep(180); }
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
async function restartHouse() {
|
||||
await ev(() => {
|
||||
window.__director.forceIdle();
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'HouseScene') sm.stop(k); }
|
||||
if (!sm.isActive('HouseScene')) sm.start('HouseScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 9000 });
|
||||
await sleep(400);
|
||||
await drain();
|
||||
}
|
||||
await sleep(600);
|
||||
|
||||
console.log('\n=== A 基线:真实睡觉流程 + 连按 E ===');
|
||||
await ev(() => { window.__director.dayElapsedMs = 300000; });
|
||||
const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bed.x, bed.y + 8);
|
||||
await tap('KeyE');
|
||||
let okA = false;
|
||||
for (let i = 0; i < 24; i++) {
|
||||
await sleep(500);
|
||||
await state(`A t+${(i+1)*0.5}s`);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
const d = await ev(() => window.__director.day);
|
||||
if (d === 2) { okA = true; console.log('A 结果: 正常过日到 D2 ✓'); break; }
|
||||
}
|
||||
if (!okA) console.log('A 结果: 卡死 ✗');
|
||||
|
||||
console.log('\n=== B 床淡出期间二次触发 ===');
|
||||
await restartHouse();
|
||||
await ev(() => { const d = window.__director; d.setDay(1); d.dayElapsedMs = 300000; });
|
||||
await ev(() => { window.__dreamStarts = 0; const sm = window.__game.scene;
|
||||
if (!sm.__patched) { const orig = sm.start.bind(sm); sm.start = (k, ...a) => { if (k === 'DreamScene') window.__dreamStarts++; return orig(k, ...a); }; sm.__patched = true; } });
|
||||
const bedB = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bedB.x, bedB.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(150);
|
||||
await setPlayer('HouseScene', bedB.x + 40, bedB.y + 8); // 移开 >30px 重武装
|
||||
await tap('KeyE'); // 二次按 E
|
||||
let okB = false;
|
||||
for (let i = 0; i < 24; i++) {
|
||||
await sleep(500);
|
||||
const extra = await ev(() => window.__dreamStarts);
|
||||
await state(`B t+${(i+1)*0.5}s dreamStarts=${extra}`);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
const d = await ev(() => window.__director.day);
|
||||
if (d === 2) { okB = true; console.log(`B 结果: 过日成功, DreamScene 启动次数=${extra}`); break; }
|
||||
}
|
||||
if (!okB) console.log('B 结果: 卡死 ✗');
|
||||
|
||||
console.log('\n=== C 梦境对话期间注入第二个 showDialogue ===');
|
||||
await restartHouse();
|
||||
await ev(() => { const d = window.__director; d.setDay(1); d.dayElapsedMs = 300000; });
|
||||
const bedC = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bedC.x, bedC.y + 8);
|
||||
await tap('KeyE');
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('DreamScene'), { timeout: 9000 });
|
||||
await page.waitForFunction(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }, { timeout: 9000 });
|
||||
await state('C 梦境第一行已开');
|
||||
// 注入第二个 showDialogue(模拟resolver被覆盖)
|
||||
await ev(() => { window.__injectedP = window.__director.showDialogue({ lines: ['【注入】测试对话'] }); window.__injectedP.then(r => { window.__injectedResolved = r; }); });
|
||||
await sleep(300);
|
||||
await state('C 注入后');
|
||||
await drain(); // 按 E 关掉注入对话
|
||||
await sleep(300);
|
||||
await state('C 注入对话关闭后');
|
||||
// 继续按 E 试图推进梦境
|
||||
let okC = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await sleep(500);
|
||||
await state(`C t+${(i+1)*0.5}s`);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
const d = await ev(() => window.__director.day);
|
||||
if (d === 2) { okC = true; console.log('C 结果: 梦境继续并过日 ✓'); break; }
|
||||
}
|
||||
if (!okC) console.log('C 结果: 梦境 run() 永久悬挂 = 卡死根因证实 ✗');
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,123 @@
|
||||
// dbg19:修复后全流程真人验证
|
||||
// 1. D1 与圆圆对话取面包 → 送大宝/咪咪/龟爷爷 → T1 完成(不经隐形柜台)
|
||||
// 2. D2 与咪咪对话清点货架 → T5 完成(不经隐形货架区)
|
||||
// 3. 睡觉竞态:低幸福+计时逼近阈值时睡觉 → 不卡死,D2 可正常交互
|
||||
// 4. 庆典未被误抑制:低幸福计时到点不睡觉 → 庆典正常触发
|
||||
// 5. T13 彩带 ×4 新坐标可触发
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,200)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; });
|
||||
async function drain(maxIter = 40) {
|
||||
const texts = [];
|
||||
for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; texts.push(await dlgText()); await tap('KeyE'); await sleep(160); }
|
||||
return texts;
|
||||
}
|
||||
async function startScene(k) {
|
||||
await ev((k) => { const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const key = sc.scene.key; if (key !== 'UIScene' && key !== k) sm.stop(key); }
|
||||
sm.start(k, { spawn: 'default' }); }, k);
|
||||
await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k);
|
||||
await sleep(400); await drain();
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
async function talkNpc(sceneKey, x, y) {
|
||||
await setPlayer(sceneKey, x + 40, y + 40);
|
||||
await setPlayer(sceneKey, x, y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(300);
|
||||
return drain();
|
||||
}
|
||||
const dget = (e) => ev(new Function(`return window.__director.${e}`));
|
||||
const darr = (e) => ev(new Function(`return [...window.__director.${e}]`));
|
||||
let fails = 0;
|
||||
const check = (name, ok, extra = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra ? ' ' + extra : ''}`); if (!ok) fails++; };
|
||||
await sleep(500);
|
||||
|
||||
console.log('\n=== 1. D1 与圆圆对话取面包 → 送三人 ===');
|
||||
// 小地图居中偏移:bakery/shop ox=40,oy=10;house ox=64,oy=26;plaza/lake 无偏移
|
||||
await startScene('BakeryScene');
|
||||
const t1 = await talkNpc('BakeryScene', 128, 50); // yuanyuan (88,40)+(40,10)
|
||||
check('对话圆圆拿到面包', await dget("taskProgress['breadTaken']") === 1, JSON.stringify(t1).slice(0, 80));
|
||||
await startScene('PlazaScene');
|
||||
await talkNpc('PlazaScene', 56, 136); // dabao
|
||||
await startScene('ShopScene');
|
||||
await talkNpc('ShopScene', 80, 98); // mimi(D1:只是送面包对象,非 T5)
|
||||
await startScene('LakeScene');
|
||||
await talkNpc('LakeScene', 200, 136); // guiyeye
|
||||
const done1 = await darr('tasksDone');
|
||||
check('T1 送面包完成', done1.includes('T1'), done1.join(','));
|
||||
check('T3 问好完成', done1.includes('T3'), done1.join(','));
|
||||
|
||||
console.log('\n=== 2. D2 与咪咪对话清点货架 ===');
|
||||
await ev(() => window.__director.setDay(2));
|
||||
await startScene('ShopScene');
|
||||
await talkNpc('ShopScene', 80, 98); // mimi
|
||||
const done2 = await darr('tasksDone');
|
||||
check('T5 清点完成', done2.includes('T5'), done2.join(','));
|
||||
check('C5 线索获得', (await darr('clues')).includes('C5'));
|
||||
|
||||
console.log('\n=== 3. 睡觉竞态:低幸福计时逼近阈值时睡觉 ===');
|
||||
await ev(() => { const d = window.__director; d.forceIdle(); d.setDay(1); d.setHappiness(20); });
|
||||
await startScene('HouseScene');
|
||||
// lowHappyMs=119700:阈值将在 ~300ms 后(即床淡出 400ms 中途)到达,正中竞态窗口
|
||||
await ev(() => { const d = window.__director; d.setHappiness(20); d.lowHappyMs = 119700; d.dayElapsedMs = 300000; });
|
||||
await setPlayer('HouseScene', 88, 90); // 床 (24,56)+(64,26)=(88,82),站 y+8
|
||||
await tap('KeyE');
|
||||
let reachedD2 = false, stuckCeleb = false;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await sleep(500);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
const day = await dget('day');
|
||||
if (day === 2) { reachedD2 = true; break; }
|
||||
}
|
||||
check('睡觉正常过日到 D2(无庆典抢占)', reachedD2);
|
||||
stuckCeleb = await dget('celebrationActive');
|
||||
check('D2 celebrationActive=false(E 可用)', !stuckCeleb);
|
||||
// D2 交互验证:与日历对话
|
||||
await startScene('HouseScene');
|
||||
const cal = await talkNpc('HouseScene', 120, 66); // calendar (56,40)+(64,26)
|
||||
check('D2 交互正常(日历对话)', cal.length > 0, JSON.stringify(cal).slice(0, 60));
|
||||
|
||||
console.log('\n=== 4. 庆典未被误抑制 ===');
|
||||
await ev(() => { const d = window.__director; d.forceIdle(); d.setHappiness(20); d.lowHappyMs = 119900; });
|
||||
await sleep(800);
|
||||
check('低幸福计时到点→庆典正常触发', await dget('celebrationActive'));
|
||||
await ev(() => window.__director.forceIdle());
|
||||
|
||||
console.log('\n=== 5. T13 彩带 ×4 新坐标 ===');
|
||||
await ev(() => window.__director.setDay(6));
|
||||
await startScene('PlazaScene');
|
||||
const ribbons = [[184, 120], [296, 120], [184, 216], [296, 216]];
|
||||
for (const [x, y] of ribbons) {
|
||||
await setPlayer('PlazaScene', x + 40, y + 40);
|
||||
await setPlayer('PlazaScene', x, y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(300);
|
||||
await drain();
|
||||
}
|
||||
check('T13 彩带完成', (await darr('tasksDone')).includes('T13'), (await darr('tasksDone')).join(','));
|
||||
|
||||
console.log(`\n${fails === 0 ? '全部通过 ✓' : `失败 ${fails} 项 ✗`}`);
|
||||
await browser.close();
|
||||
process.exit(fails === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,120 @@
|
||||
// dbg20:审计修复验证
|
||||
// 1. 广场地图:邮筒瓦片(gid47)在 (11,10);4 个挂彩带点改为路灯(gid 29),无预置彩带
|
||||
// 2. C3 邮筒交互可达(D2)
|
||||
// 3. T13 挂彩带前无彩带图像,挂上后出现(帧 29/30/31)
|
||||
// 4. 阿福指引:D6→T14_HINT,D7→T15_HINT
|
||||
// 5. 门淡出庆典竞态:低幸福计时逼近阈值时过门 → celebrationActive 不卡死,新场景可交互
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
let fails = 0;
|
||||
const check = (name, ok, extra = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra ? ' ' + extra : ''}`); if (!ok) fails++; };
|
||||
|
||||
// ---- 静态地图检查 ----
|
||||
const plaza = JSON.parse(fs.readFileSync('public/assets/maps/plaza.json', 'utf8'));
|
||||
const ob = plaza.layers.find(l => l.name === 'obstacles').data;
|
||||
const W = plaza.width;
|
||||
const g = (x, y) => ob[y * W + x];
|
||||
check('邮筒瓦片 gid47 @ (11,10)', g(11, 10) === 47);
|
||||
check('挂彩带点全部改为路灯 gid29', g(11,7)===29 && g(18,7)===29 && g(11,13)===29 && g(18,13)===29);
|
||||
check('无预置彩带 gid30/31/32', ![30,31,32].includes(g(11,7)) && ![30,31,32].includes(g(18,7)) && ![30,31,32].includes(g(11,13)) && ![30,31,32].includes(g(18,13)));
|
||||
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,200)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; });
|
||||
async function drain(maxIter = 40) {
|
||||
const texts = [];
|
||||
for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; texts.push(await dlgText()); await tap('KeyE'); await sleep(160); }
|
||||
return texts;
|
||||
}
|
||||
async function startScene(k) {
|
||||
await ev((k) => { const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const key = sc.scene.key; if (key !== 'UIScene' && key !== k) sm.stop(key); }
|
||||
sm.start(k, { spawn: 'default' }); }, k);
|
||||
await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k);
|
||||
await sleep(400); await drain();
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
async function interactAt(sceneKey, x, y) {
|
||||
await setPlayer(sceneKey, x + 40, y + 40);
|
||||
await setPlayer(sceneKey, x, y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(300);
|
||||
return drain();
|
||||
}
|
||||
const dget = (e) => ev(new Function(`return window.__director.${e}`));
|
||||
const darr = (e) => ev(new Function(`return [...window.__director.${e}]`));
|
||||
await sleep(500);
|
||||
|
||||
console.log('\n=== 2. C3 邮筒交互(D2) ===');
|
||||
await ev(() => window.__director.setDay(2));
|
||||
await startScene('PlazaScene');
|
||||
const texts2 = await interactAt('PlazaScene', 184, 176); // mailbox_c3 区
|
||||
check('C3 线索获得', (await darr('clues')).includes('C3'), JSON.stringify(texts2).slice(0, 60));
|
||||
|
||||
console.log('\n=== 3. T13 挂彩带视觉反馈 ===');
|
||||
await ev(() => window.__director.setDay(6));
|
||||
await startScene('PlazaScene');
|
||||
const imgBefore = await ev(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
return s.children.list.filter(o => o.type === 'Image' && o.texture.key === 'tiles16' && [29,30,31].includes(o.frame.name)).length;
|
||||
});
|
||||
check('挂彩带前场景无彩带图像', imgBefore === 0, `实际 ${imgBefore}`);
|
||||
await interactAt('PlazaScene', 184, 120); // ribbon_1
|
||||
const imgAfter = await ev(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
return s.children.list.filter(o => o.type === 'Image' && o.texture.key === 'tiles16' && [29,30,31].includes(o.frame.name)).map(o => ({ x: o.x, y: o.y, f: o.frame.name }));
|
||||
});
|
||||
check('挂上后出现彩带图像(帧29红@184,120)', imgAfter.some(o => o.x === 184 && o.y === 120 && o.f === 29), JSON.stringify(imgAfter));
|
||||
|
||||
console.log('\n=== 4. 阿福地点指引 ===');
|
||||
const t14 = await interactAt('PlazaScene', 248, 136); // afu @D6
|
||||
check('D6 阿福 T14 指引', t14.some(t => t.includes('广场中央站好')), JSON.stringify(t14).slice(0, 70));
|
||||
await ev(() => window.__director.setDay(7));
|
||||
await startScene('PlazaScene');
|
||||
const t15 = await interactAt('PlazaScene', 248, 136); // afu @D7
|
||||
check('D7 阿福 T15 指引', t15.some(t => t.includes('广场北边')), JSON.stringify(t15).slice(0, 70));
|
||||
|
||||
console.log('\n=== 5. 门淡出庆典竞态 ===');
|
||||
await ev(() => { const d = window.__director; d.forceIdle(); d.setDay(3); d.setHappiness(20); });
|
||||
await startScene('PlazaScene');
|
||||
// 阈值 ~300ms 后到达:落在门淡出 250ms 窗口内
|
||||
await ev(() => { const d = window.__director; d.setHappiness(20); d.lowHappyMs = 119700; });
|
||||
// 广场→小屋门 (72,88):先到 30px 外武装,再踏上门
|
||||
await setPlayer('PlazaScene', 72 + 30, 88);
|
||||
await setPlayer('PlazaScene', 72, 88);
|
||||
let arrived = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await sleep(300);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
if (await ev(() => window.__game.scene.isActive('HouseScene'))) { arrived = true; break; }
|
||||
}
|
||||
check('过门到达小屋', arrived);
|
||||
check('celebrationActive 未卡死', !(await dget('celebrationActive')));
|
||||
await drain();
|
||||
// 新场景可交互:日历 (56,40)+(64,26)=(120,66)
|
||||
const cal = await interactAt('HouseScene', 120, 66);
|
||||
check('新场景交互正常(日历)', cal.length > 0, JSON.stringify(cal).slice(0, 50));
|
||||
|
||||
console.log(`\n${fails === 0 ? '全部通过 ✓' : `失败 ${fails} 项 ✗`}`);
|
||||
await browser.close();
|
||||
process.exit(fails === 0 ? 0 : 1);
|
||||