Honey Village: six model builds + hub frontend, Dockerized for Dokploy
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
.qa/
|
||||||
|
.vite/
|
||||||
|
*.local
|
||||||
|
*.log
|
||||||
@@ -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,89 @@
|
|||||||
|
# 崩坏童话:蜜糖村
|
||||||
|
|
||||||
|
一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。
|
||||||
|
纯单机网页游戏:无存档、无后端、无 localStorage、无外部网络请求,浏览器打开即玩。
|
||||||
|
完整流程约 25–35 分钟,含 4 个结局。
|
||||||
|
|
||||||
|
## 启动方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # 开发模式(Vite,默认 http://localhost:5173)
|
||||||
|
npm run build # 类型检查 + 产出纯静态 dist/
|
||||||
|
npm run preview # 本地游玩 dist/ 构建产物
|
||||||
|
```
|
||||||
|
|
||||||
|
技术栈:**Phaser 3 + TypeScript + Vite**。逻辑分辨率 320×180,任意窗口尺寸整数倍放大居中,`pixelArt` 关闭纹理平滑。
|
||||||
|
|
||||||
|
## 按键
|
||||||
|
|
||||||
|
| 按键 | 功能 |
|
||||||
|
|---|---|
|
||||||
|
| WASD / 方向键 | 四向移动 |
|
||||||
|
| E(或回车) | 交互 / 推进对话 / 确认选项 |
|
||||||
|
| W/S 或 ↑/↓ | 对话中切换选项 |
|
||||||
|
| `` ` ``(反引号) | 开关 Debug 面板 |
|
||||||
|
|
||||||
|
标题画面输入名字后回车开始(留空默认「小满」)。
|
||||||
|
|
||||||
|
## Debug 面板(服务验收)
|
||||||
|
|
||||||
|
按反引号 `` ` `` 开关,默认关闭、无 UI 痕迹,不影响正常游玩。功能:
|
||||||
|
|
||||||
|
- 直接设置**幸福值**(0–100,观察侵蚀矩阵五档:10/40/70/90/100)与**觉知值**
|
||||||
|
- 跳转**当前日**(1–7,回到当日清晨小屋)
|
||||||
|
- **传送**到全部 11 张地图(10 个场景,白色层含走廊+病房两张图)
|
||||||
|
- **置齐全部线索**(C1–C9)
|
||||||
|
- **强制触发四个结局**(E1 糖衣 / E2 回收 / E3 苏醒 / E4 静默)
|
||||||
|
|
||||||
|
## 素材制作流程(全部本项目内自制,无任何外部资产)
|
||||||
|
|
||||||
|
### 像素美术(Aseprite)
|
||||||
|
|
||||||
|
全部图片由 `tools/gen_art.lua` 在 Aseprite 内逐像素绘制生成——瓦片集(80 枚 16×16:草地/石板路/湖水/花丛/莓果丛/树/房屋墙顶门/室内地板/家具/井/雾/白色层墙地等)、主角(16×24,四朝向 × 4 帧行走,首帧兼待机)、对话头像(微笑 + E3 专用「真实的脸」)、7 组具名 NPC 立绘(各 smile/flat/off/blink 四档,off 档与 smile 仅差 1 像素——右眼低 1 像素)、双子鸟 2 帧张嘴动画、群演 ×5 各 2 帧循环、灰麻雀、向日葵表盘 14 帧。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重新生成全部素材(PNG → public/assets/,Aseprite 源文件 → art/)
|
||||||
|
aseprite -b --script tools/gen_art.lua
|
||||||
|
# macOS 应用包路径:/Applications/Aseprite.app/Contents/MacOS/aseprite
|
||||||
|
```
|
||||||
|
|
||||||
|
色板:村庄用糖果色(草绿/奶油黄/蜜糖橙/天空蓝),白色层独立灰白色板;无抗锯齿、无描边渐变。
|
||||||
|
|
||||||
|
### 地图(Tiled)
|
||||||
|
|
||||||
|
10 个场景 11 张地图由 `tools/gen_maps.mjs` 生成 Tiled `.tmx` 源文件(`maps/`,统一 `ground` / `obstacles`(collides=true)/ `objects`(spawn、NPC 点位、door、交互点)三层结构),再经 Tiled 命令行导出 JSON:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/gen_maps.mjs
|
||||||
|
for f in maps/*.tmx; do
|
||||||
|
tiled --export-map --embed-tilesets "$f" "public/assets/maps/$(basename "$f" .tmx).json"
|
||||||
|
done
|
||||||
|
# macOS 应用包路径:/Applications/Tiled.app/Contents/MacOS/Tiled
|
||||||
|
```
|
||||||
|
|
||||||
|
场景清单:小满小屋 12×8、村广场 30×20(枢纽)、面包房 15×10、杂货店 15×10、诊所 12×8、湖畔 24×16、花田 20×14、古井空地 14×10、雾墙边界 20×8、白色层(走廊 20×6 + 病房 10×8)。所有室外场景经村广场互通,门与出入口成对。
|
||||||
|
|
||||||
|
### 音频
|
||||||
|
|
||||||
|
无任何音频文件。BGM 为 Web Audio 程序合成的 8 小节 8-bit 治愈循环(方波主旋律 + 三角波低音 + 独立鸟鸣声部),音效仅 4 个(对话哔哔、任务完成叮、黄昏钟声、心电监护滴声),全部程序合成,见 `src/core/audio.ts`。playbackRate / 低通滤波 / 鸟鸣静音由侵蚀矩阵(`CorruptionDirector`)统一驱动。
|
||||||
|
|
||||||
|
## 自动化验收(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -D playwright && npx playwright install chromium
|
||||||
|
node tools/e2e.mjs # 标题→Day1、侵蚀五档截图、全场景传送、四结局强制触发、控制台零报错
|
||||||
|
node tools/e2e_flow.mjs # 七日全流程:15 任务、9 线索、黄昏/梦境/脚本雨、E3 苏醒线
|
||||||
|
node tools/e2e_mech.mjs # 悲伤行为 5 项、强制欢乐庆典与逃离计数、S4 文本腐蚀、四结局判定
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/core/director.ts CorruptionDirector:幸福/觉知/日期/任务/线索/侵蚀阶段的唯一真源
|
||||||
|
src/core/textcorrupt.ts 文本腐蚀(10 组同音错字对照表,渲染层替换,优先命中咪咪)
|
||||||
|
src/core/audio.ts Web Audio BGM/SFX 合成与矩阵联动
|
||||||
|
src/core/debug.ts Debug 面板
|
||||||
|
src/data/story.ts 全部台词(阿福公告 D1–D7 逐字、龟爷爷 C9 三段链、结局文案等)
|
||||||
|
src/scenes/ Boot / Title / 11 张地图各一个 Scene / 常驻 UI / Dream / Ending
|
||||||
|
```
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>崩坏童话:蜜糖村</title>
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; padding: 0; background: #1a1a22; height: 100%; overflow: hidden; }
|
||||||
|
#game { display: flex; align-items: center; justify-content: center; height: 100%; }
|
||||||
|
#game canvas { image-rendering: pixelated; image-rendering: crisp-edges; }
|
||||||
|
#debug-panel {
|
||||||
|
display: none; position: fixed; top: 8px; right: 8px; z-index: 100;
|
||||||
|
background: rgba(20,20,30,.92); color: #cfe; font: 12px monospace;
|
||||||
|
padding: 10px; border: 1px solid #5a7; border-radius: 4px; width: 230px;
|
||||||
|
}
|
||||||
|
#debug-panel h3 { margin: 0 0 6px; font-size: 12px; color: #8f8; }
|
||||||
|
#debug-panel .row { margin: 4px 0; display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
|
||||||
|
#debug-panel button { font: 11px monospace; background: #2a3a4a; color: #cfe; border: 1px solid #567; border-radius: 3px; padding: 2px 6px; cursor: pointer; }
|
||||||
|
#debug-panel input { width: 48px; font: 11px monospace; background: #111; color: #cfe; border: 1px solid #567; }
|
||||||
|
#name-entry {
|
||||||
|
display: none; position: fixed; z-index: 90; left: 50%; top: 62%; transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#name-entry input {
|
||||||
|
font: 16px monospace; padding: 6px 10px; background: #fff8e8; color: #5a4632;
|
||||||
|
border: 3px solid #e8a04c; border-radius: 2px; outline: none; text-align: center; width: 180px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game"></div>
|
||||||
|
<div id="name-entry"><input id="name-input" maxlength="8" placeholder="小满" autocomplete="off" /></div>
|
||||||
|
<div id="debug-panel"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?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="6">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="15" height="10">
|
||||||
|
<data encoding="csv">
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,30,30,25,25,25,25,25,25
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="15" height="10">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,
|
||||||
|
29,29,29,29,62,29,29,29,29,29,62,29,34,29,29,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,33,33,33,33,33,67,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,69,0,0,0,0,0,0,0,0,37,38,0,68,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,68,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
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="yuanyuan" type="npc" x="80" y="49.6"><point/></object>
|
||||||
|
<object id="2" name="recipe" type="poi" x="128" y="75.2"><properties><property name="clue" value="C2"/></properties><point/></object>
|
||||||
|
<object id="3" name="knead" type="poi" x="176" y="107.2"><point/></object>
|
||||||
|
<object id="4" name="entry" type="spawn" x="128" y="136"><point/></object>
|
||||||
|
<object id="5" name="to_plaza" type="door" x="112" y="152" width="32" height="8"><properties><property name="target" value="plaza"/><property name="spawn" value="fromBakery"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?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="5">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="12" height="8">
|
||||||
|
<data encoding="csv">
|
||||||
|
27,27,27,27,27,27,27,27,27,27,27,27,
|
||||||
|
27,27,27,27,27,27,27,27,27,27,27,27,
|
||||||
|
27,27,27,27,27,27,27,27,27,27,27,27,
|
||||||
|
27,27,27,27,27,27,27,27,27,27,27,27,
|
||||||
|
27,27,27,27,27,27,27,27,27,27,27,27,
|
||||||
|
27,27,27,27,27,27,27,27,27,27,27,27,
|
||||||
|
27,27,27,27,27,27,27,27,27,27,27,27,
|
||||||
|
27,27,27,27,27,30,30,27,27,27,27,27
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="12" height="8">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
43,43,43,43,43,43,43,43,43,43,43,43,
|
||||||
|
29,29,29,71,29,29,29,29,75,29,29,29,
|
||||||
|
0,68,0,0,0,0,0,0,0,63,0,0,
|
||||||
|
0,0,0,0,0,65,0,0,0,64,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
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="fu" type="npc" x="112" y="49.6"><point/></object>
|
||||||
|
<object id="2" name="chart" type="poi" x="80" y="75.2"><properties><property name="clue" value="C8"/></properties><point/></object>
|
||||||
|
<object id="3" name="entry" type="spawn" x="96" y="104"><point/></object>
|
||||||
|
<object id="4" name="to_plaza" type="door" x="80" y="120" width="32" height="8"><properties><property name="target" value="plaza"/><property name="spawn" value="fromClinic"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -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="9">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="20" height="14">
|
||||||
|
<data encoding="csv">
|
||||||
|
3,2,1,1,1,1,4,2,1,1,1,1,2,1,1,1,1,1,2,1,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,
|
||||||
|
1,1,1,1,2,1,1,1,2,3,1,1,1,2,1,1,1,1,4,1,
|
||||||
|
1,1,1,58,57,57,58,57,57,58,57,57,58,57,57,58,57,1,3,2,
|
||||||
|
1,1,1,57,57,58,57,57,58,57,57,58,57,57,58,57,57,1,1,1,
|
||||||
|
1,2,1,57,58,57,57,58,57,57,58,57,57,58,57,57,58,2,1,1,
|
||||||
|
1,1,1,58,57,57,58,57,57,58,57,57,58,57,57,58,57,1,1,1,
|
||||||
|
1,1,1,57,57,58,57,57,58,57,57,58,57,57,58,57,57,1,1,2,
|
||||||
|
1,1,4,57,58,57,57,58,57,57,58,57,57,58,57,57,58,2,1,1,
|
||||||
|
1,2,2,58,57,57,58,57,57,58,57,57,58,57,57,58,57,1,1,1,
|
||||||
|
1,3,2,57,57,58,57,57,58,57,57,58,57,57,58,57,57,1,1,1,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,3,1,4,1,2,1,2,1,2,
|
||||||
|
1,1,1,2,4,1,1,1,1,2,3,1,1,1,1,2,1,1,1,1,
|
||||||
|
2,1,1,1,1,1,3,2,1,1,1,1,1,1,4,2,1,1,1,1
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="20" height="14">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,
|
||||||
|
16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,
|
||||||
|
0,0,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,12,0,0,0,0,0,0,0,12,0,0,0,0,0,0,52,
|
||||||
|
0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0,0,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,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,52,
|
||||||
|
0,0,0,70,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
51,51,51,51,0,0,0,0,0,0,51,51,51,51,51,51,51,51,51,51
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="berry1" type="poi" x="80" y="96"><point/></object>
|
||||||
|
<object id="2" name="berry2" type="poi" x="144" y="112"><point/></object>
|
||||||
|
<object id="3" name="berry3" type="poi" x="208" y="96"><point/></object>
|
||||||
|
<object id="4" name="berry4" type="poi" x="256" y="144"><point/></object>
|
||||||
|
<object id="5" name="berry5" type="poi" x="112" y="176"><point/></object>
|
||||||
|
<object id="6" name="fieldwater" type="poi" x="64" y="203.2"><point/></object>
|
||||||
|
<object id="7" name="entry" type="spawn" x="33.6" y="104"><point/></object>
|
||||||
|
<object id="8" name="to_plaza" type="door" x="0" y="72" width="9.6" height="80"><properties><property name="target" value="plaza"/><property name="spawn" value="fromField"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?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="4">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="20" height="8">
|
||||||
|
<data encoding="csv">
|
||||||
|
3,2,1,1,1,1,4,2,1,1,1,1,2,1,1,1,1,1,2,1,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,
|
||||||
|
1,1,1,1,2,1,1,1,2,3,1,1,1,2,1,1,1,1,4,1,
|
||||||
|
1,1,1,1,1,1,1,4,2,2,1,1,1,1,1,1,1,1,3,2,
|
||||||
|
78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,78,
|
||||||
|
41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,
|
||||||
|
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,
|
||||||
|
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="20" height="8">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,
|
||||||
|
0,0,14,15,0,0,0,0,0,0,0,0,0,0,0,0,14,15,0,0,
|
||||||
|
52,0,16,17,0,0,0,0,0,0,0,0,0,0,0,0,16,17,0,52,
|
||||||
|
0,0,0,0,53,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,
|
||||||
|
42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="fog" type="poi" x="168" y="96"><point/></object>
|
||||||
|
<object id="2" name="entry" type="spawn" x="168" y="46.4"><point/></object>
|
||||||
|
<object id="3" name="to_plaza" type="door" x="120" y="0" width="80" height="11.2"><properties><property name="target" value="plaza"/><property name="spawn" value="fromFog"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?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="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="12" height="8">
|
||||||
|
<data encoding="csv">
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,25,25,25,25,25,25,25,
|
||||||
|
25,25,25,25,25,30,30,25,25,25,25,25
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="12" height="8">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
28,28,28,28,28,28,28,28,28,28,28,28,
|
||||||
|
29,29,66,29,29,29,62,29,29,29,29,29,
|
||||||
|
68,31,0,0,0,0,0,0,0,0,68,0,
|
||||||
|
0,32,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,37,38,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
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="bed" type="spawn" x="40" y="56"><point/></object>
|
||||||
|
<object id="2" name="entry" type="spawn" x="88" y="104"><point/></object>
|
||||||
|
<object id="3" name="to_plaza" type="door" x="80" y="120" width="32" height="8"><properties><property name="target" value="plaza"/><property name="spawn" value="fromHouse"/></properties></object>
|
||||||
|
<object id="4" name="calendar" type="poi" x="40" y="40"><properties><property name="clue" value="C1"/></properties><point/></object>
|
||||||
|
<object id="5" name="bed" type="poi" x="24" y="72"><point/></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?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="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="24" height="16">
|
||||||
|
<data encoding="csv">
|
||||||
|
3,2,1,1,1,1,4,2,1,1,1,1,2,1,1,1,1,1,2,1,1,1,1,3,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,4,1,3,1,
|
||||||
|
1,1,1,1,2,1,1,1,2,3,1,1,1,2,1,1,1,1,4,1,1,1,2,1,
|
||||||
|
1,1,1,1,1,1,1,4,2,2,1,1,1,1,1,1,1,1,3,2,2,1,1,1,
|
||||||
|
1,1,1,2,1,1,1,1,4,1,1,2,1,3,1,1,2,1,1,1,1,2,1,1,
|
||||||
|
1,2,1,1,2,1,1,1,1,1,1,4,1,1,2,1,1,2,1,1,1,3,1,1,
|
||||||
|
1,1,1,1,1,1,1,54,55,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||||
|
1,1,1,1,1,1,2,55,54,2,1,1,3,1,1,1,2,1,1,2,1,1,4,1,
|
||||||
|
1,1,4,1,1,1,1,2,1,1,1,1,2,1,1,1,1,2,1,1,3,1,2,1,
|
||||||
|
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
|
||||||
|
9,8,9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,
|
||||||
|
8,9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,
|
||||||
|
9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,
|
||||||
|
8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,
|
||||||
|
61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,8,
|
||||||
|
8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,8,9
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="24" height="16">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,15,
|
||||||
|
16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,16,17,
|
||||||
|
52,0,0,0,0,0,0,0,0,0,0,14,15,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,14,15,0,0,0,0,0,0,0,16,17,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
52,0,16,17,0,0,11,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,46,47,0,0,0,0,0,0,
|
||||||
|
52,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,0,0,0,0,0,0,0,0,0,
|
||||||
|
9,8,9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,
|
||||||
|
8,9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,
|
||||||
|
9,8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,
|
||||||
|
8,61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,
|
||||||
|
61,8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,8,
|
||||||
|
8,9,8,9,8,9,61,9,8,9,8,9,8,61,8,9,8,9,8,9,61,9,8,9
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="gui" type="npc" x="310.4" y="96"><point/></object>
|
||||||
|
<object id="2" name="bench" type="poi" x="272" y="112"><point/></object>
|
||||||
|
<object id="3" name="picnic" type="poi" x="136" y="120"><point/></object>
|
||||||
|
<object id="4" name="entry" type="spawn" x="360" y="120"><point/></object>
|
||||||
|
<object id="5" name="picnicSpot" type="spawn" x="136" y="129.6"><point/></object>
|
||||||
|
<object id="6" name="to_plaza" type="door" x="374.4" y="80" width="9.6" height="80"><properties><property name="target" value="plaza"/><property name="spawn" value="fromLake"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?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="39">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="30" height="20">
|
||||||
|
<data encoding="csv">
|
||||||
|
3,2,1,1,1,1,4,2,1,1,1,1,2,1,5,5,1,1,2,1,1,1,1,3,2,1,1,1,1,4,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,2,1,6,5,2,1,2,1,4,1,3,1,1,1,1,1,1,1,
|
||||||
|
1,1,1,1,2,1,1,1,2,3,1,1,1,2,5,5,1,1,4,1,1,1,2,1,1,1,1,2,1,1,
|
||||||
|
1,1,1,1,1,1,1,4,2,2,1,1,1,1,5,5,1,1,3,2,2,1,1,1,1,1,1,1,1,1,
|
||||||
|
1,1,1,2,21,1,1,1,4,21,1,2,1,3,6,5,2,1,1,1,1,21,1,1,1,1,21,1,1,1,
|
||||||
|
1,2,1,1,72,1,1,1,1,72,1,4,1,1,5,5,1,2,1,1,1,72,1,1,2,1,72,2,1,1,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||||
|
1,1,1,1,1,1,2,1,1,2,1,1,3,1,6,5,2,1,1,2,1,1,4,1,1,1,1,1,1,2,
|
||||||
|
6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,6,5,
|
||||||
|
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
|
||||||
|
1,3,2,1,1,1,2,1,1,1,1,2,1,1,6,5,1,1,1,1,2,1,1,1,3,2,1,1,1,2,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,3,74,74,74,74,74,74,1,2,1,2,1,1,1,1,1,1,1,1,
|
||||||
|
1,1,1,2,4,1,1,1,1,2,3,1,74,74,74,74,74,74,1,1,1,2,1,1,1,1,2,4,1,1,
|
||||||
|
2,1,1,1,1,1,3,2,1,1,1,1,74,74,74,74,74,74,1,1,1,1,2,2,1,1,1,1,1,3,
|
||||||
|
1,2,1,3,1,1,1,1,2,1,4,1,74,74,74,74,74,74,1,1,1,1,1,1,2,1,3,1,1,1,
|
||||||
|
2,1,1,1,2,1,1,1,2,1,1,1,4,1,5,5,3,1,1,2,1,1,1,2,1,1,1,2,1,1,
|
||||||
|
1,1,1,1,1,1,1,1,3,4,2,2,2,2,6,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||||
|
2,1,1,2,1,1,2,1,1,1,1,1,1,1,5,5,1,4,1,1,2,1,1,2,1,1,2,1,1,2,
|
||||||
|
1,1,1,1,2,1,1,2,1,1,2,1,1,2,5,5,4,1,1,3,1,1,1,1,1,1,1,2,1,1,
|
||||||
|
2,4,3,1,1,1,1,1,1,1,1,1,1,1,6,5,1,1,1,1,2,2,2,2,4,3,1,1,1,1
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="30" height="20">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
14,15,14,15,14,15,14,15,14,15,14,15,0,0,0,0,0,0,14,15,14,15,14,15,14,15,14,15,14,15,
|
||||||
|
16,17,16,17,16,17,16,17,16,17,16,17,0,0,0,0,0,50,16,17,16,17,16,17,16,17,16,17,16,17,
|
||||||
|
52,0,22,22,23,23,0,22,22,23,23,0,0,0,0,0,0,0,0,22,22,23,23,0,22,22,23,23,0,52,
|
||||||
|
0,0,24,24,24,24,0,24,24,24,24,0,0,0,0,0,0,0,0,24,24,24,24,0,24,24,24,24,0,0,
|
||||||
|
52,0,19,20,0,19,0,19,20,0,19,0,0,0,0,0,0,0,0,19,20,0,19,0,19,20,0,19,0,52,
|
||||||
|
0,53,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,
|
||||||
|
52,0,0,0,0,0,0,0,0,0,0,48,49,0,0,0,0,0,0,0,14,15,0,0,0,0,0,0,0,52,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,17,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,60,0,0,0,0,0,0,60,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
52,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,52,
|
||||||
|
0,0,0,0,0,0,0,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
52,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,52,
|
||||||
|
0,0,0,0,0,0,0,70,0,0,0,60,0,0,0,0,0,0,60,0,0,0,0,0,0,18,0,0,0,0,
|
||||||
|
52,0,0,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,15,0,52,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,15,0,0,0,16,17,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,17,0,0,0,0,0,0,0,
|
||||||
|
51,51,51,51,51,51,51,51,51,51,51,51,51,51,0,0,0,0,51,51,51,51,51,51,51,51,51,51,51,51
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="sign_house" type="poi" x="24" y="88"><point/></object>
|
||||||
|
<object id="2" name="board" type="poi" x="192" y="128"><point/></object>
|
||||||
|
<object id="3" name="letters" type="poi" x="288" y="48"><properties><property name="clue" value="C3"/></properties><point/></object>
|
||||||
|
<object id="4" name="birdtree" type="poi" x="344" y="144"><point/></object>
|
||||||
|
<object id="5" name="flowerbed1" type="poi" x="128" y="200"><point/></object>
|
||||||
|
<object id="6" name="flowerbed2" type="poi" x="128" y="232"><point/></object>
|
||||||
|
<object id="7" name="flowerbed3" type="poi" x="128" y="264"><point/></object>
|
||||||
|
<object id="8" name="ribbon1" type="poi" x="192" y="187.2"><point/></object>
|
||||||
|
<object id="9" name="ribbon2" type="poi" x="304" y="187.2"><point/></object>
|
||||||
|
<object id="10" name="ribbon3" type="poi" x="192" y="267.2"><point/></object>
|
||||||
|
<object id="11" name="ribbon4" type="poi" x="304" y="267.2"><point/></object>
|
||||||
|
<object id="12" name="festival" type="poi" x="248" y="216"><point/></object>
|
||||||
|
<object id="13" name="afu" type="npc" x="208" y="144"><point/></object>
|
||||||
|
<object id="14" name="dabao" type="npc" x="272" y="64"><point/></object>
|
||||||
|
<object id="15" name="bird1" type="npc" x="334.4" y="108.8"><point/></object>
|
||||||
|
<object id="16" name="bird2" type="npc" x="353.6" y="108.8"><point/></object>
|
||||||
|
<object id="17" name="extra1" type="npc" x="88" y="168"><properties><property name="patrol" value="40"/></properties><point/></object>
|
||||||
|
<object id="18" name="extra2" type="npc" x="360" y="152"><properties><property name="patrol" value="56"/></properties><point/></object>
|
||||||
|
<object id="19" name="extra3" type="npc" x="392" y="280"><properties><property name="patrol" value="40"/></properties><point/></object>
|
||||||
|
<object id="20" name="extra4" type="npc" x="88" y="280"><properties><property name="patrol" value="24"/></properties><point/></object>
|
||||||
|
<object id="21" name="extra5" type="npc" x="360" y="280"><properties><property name="patrol" value="16"/></properties><point/></object>
|
||||||
|
<object id="22" name="fromHouse" type="spawn" x="72" y="107.2"><point/></object>
|
||||||
|
<object id="23" name="fromBakery" type="spawn" x="152" y="107.2"><point/></object>
|
||||||
|
<object id="24" name="fromStore" type="spawn" x="344" y="107.2"><point/></object>
|
||||||
|
<object id="25" name="fromClinic" type="spawn" x="424" y="107.2"><point/></object>
|
||||||
|
<object id="26" name="fromLake" type="spawn" x="33.6" y="152"><point/></object>
|
||||||
|
<object id="27" name="fromField" type="spawn" x="462.4" y="152"><point/></object>
|
||||||
|
<object id="28" name="fromWell" type="spawn" x="240" y="33.6"><point/></object>
|
||||||
|
<object id="29" name="fromFog" type="spawn" x="240" y="283.2"><point/></object>
|
||||||
|
<object id="30" name="center" type="spawn" x="248" y="200"><point/></object>
|
||||||
|
<object id="31" name="to_house" type="door" x="64" y="67.2" width="16" height="12.8"><properties><property name="target" value="house"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
<object id="32" name="to_bakery" type="door" x="144" y="67.2" width="16" height="12.8"><properties><property name="target" value="bakery"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
<object id="33" name="to_store" type="door" x="336" y="67.2" width="16" height="12.8"><properties><property name="target" value="store"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
<object id="34" name="to_clinic" type="door" x="416" y="67.2" width="16" height="12.8"><properties><property name="target" value="clinic"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
<object id="35" name="to_lake" type="door" x="0" y="112" width="9.6" height="64"><properties><property name="target" value="lake"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
<object id="36" name="to_field" type="door" x="470.4" y="112" width="9.6" height="64"><properties><property name="target" value="field"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
<object id="37" name="to_well" type="door" x="200" y="0" width="80" height="11.2"><properties><property name="target" value="well"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
<object id="38" name="to_fogwall" type="door" x="200" y="308.8" width="80" height="11.2"><properties><property name="target" value="fogwall"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?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="8">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="15" height="10">
|
||||||
|
<data encoding="csv">
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,
|
||||||
|
26,26,26,26,26,26,26,30,30,26,26,26,26,26,26
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="15" height="10">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,
|
||||||
|
29,29,29,29,29,29,29,29,29,29,29,29,62,29,29,
|
||||||
|
0,0,36,36,36,36,36,36,36,36,36,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,36,0,
|
||||||
|
0,0,0,33,33,0,0,0,0,0,0,0,0,36,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,36,0,
|
||||||
|
0,68,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,69,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="mimi" type="npc" x="64" y="78.4"><point/></object>
|
||||||
|
<object id="2" name="shelfA" type="poi" x="64" y="59.2"><point/></object>
|
||||||
|
<object id="3" name="shelfB" type="poi" x="112" y="59.2"><point/></object>
|
||||||
|
<object id="4" name="shelfC" type="poi" x="160" y="59.2"><point/></object>
|
||||||
|
<object id="5" name="shelf_c5" type="poi" x="206.4" y="96"><properties><property name="clue" value="C5"/></properties><point/></object>
|
||||||
|
<object id="6" name="entry" type="spawn" x="128" y="136"><point/></object>
|
||||||
|
<object id="7" name="to_plaza" type="door" x="112" y="152" width="32" height="8"><properties><property name="target" value="plaza"/><property name="spawn" value="fromStore"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tileset version="1.10" tiledversion="1.12.2" name="tiles" tilewidth="16" tileheight="16" tilecount="80" columns="10">
|
||||||
|
<image source="../public/assets/tiles.png" width="160" height="128"/>
|
||||||
|
</tileset>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?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="4">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="14" height="10">
|
||||||
|
<data encoding="csv">
|
||||||
|
3,2,1,1,1,1,4,2,1,1,1,1,2,1,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,2,1,
|
||||||
|
1,1,1,1,2,1,1,1,2,3,1,1,1,2,
|
||||||
|
1,1,1,1,1,1,1,4,2,2,1,1,1,1,
|
||||||
|
1,1,1,2,1,1,1,1,4,1,1,2,1,3,
|
||||||
|
1,2,1,1,2,1,1,1,1,1,1,4,1,1,
|
||||||
|
1,1,1,1,1,1,5,5,1,1,1,1,1,1,
|
||||||
|
1,1,1,1,1,1,5,5,1,2,1,1,3,1,
|
||||||
|
1,1,4,1,1,1,5,5,1,1,1,1,2,1,
|
||||||
|
1,2,2,4,1,1,5,5,1,1,1,1,1,2
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="14" height="10">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
14,15,14,15,14,15,14,15,14,15,14,15,14,15,
|
||||||
|
16,17,16,17,16,17,16,17,16,17,16,17,16,17,
|
||||||
|
52,0,0,0,0,0,0,0,0,0,0,0,0,52,
|
||||||
|
0,0,0,0,0,0,39,0,0,0,0,18,0,0,
|
||||||
|
52,0,73,73,0,0,40,0,0,53,0,0,0,52,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,73,0,0,
|
||||||
|
52,0,0,0,0,0,0,0,0,0,0,0,0,52,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
52,0,0,0,0,0,0,0,0,0,0,0,0,52,
|
||||||
|
51,51,51,51,51,51,0,0,0,0,51,51,51,51
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="well" type="poi" x="112" y="96"><properties><property name="clue" value="C7"/></properties><point/></object>
|
||||||
|
<object id="2" name="entry" type="spawn" x="112" y="129.6"><point/></object>
|
||||||
|
<object id="3" name="to_plaza" type="door" x="72" y="148.8" width="80" height="11.2"><properties><property name="target" value="plaza"/><property name="spawn" value="fromWell"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="20" height="6" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="3">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="20" height="6">
|
||||||
|
<data encoding="csv">
|
||||||
|
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="20" height="6">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
43,43,71,43,43,43,45,71,43,43,43,43,71,45,43,43,43,71,43,43,
|
||||||
|
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,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="entry" type="spawn" x="24" y="56"><point/></object>
|
||||||
|
<object id="2" name="to_white_ward" type="door" x="312" y="0" width="8" height="96"><properties><property name="target" value="white_ward"/><property name="spawn" value="entry"/></properties></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="10" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="3">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="10" height="8">
|
||||||
|
<data encoding="csv">
|
||||||
|
44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44,
|
||||||
|
44,44,44,44,44,44,44,44,44,44
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="10" height="8">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
43,43,75,43,43,43,71,43,43,43,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,63,0,76,0,0,0,
|
||||||
|
0,0,0,0,64,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
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
<object id="1" name="chart_ward" type="poi" x="88" y="80"><point/></object>
|
||||||
|
<object id="2" name="entry" type="spawn" x="24" y="88"><point/></object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
@@ -0,0 +1,984 @@
|
|||||||
|
{
|
||||||
|
"name": "fable-5-high",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "fable-5-high",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"phaser": "^3.90.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"playwright": "^1.61.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^8.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tybys/wasm-util": "^0.10.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxc-project/types": {
|
||||||
|
"version": "0.139.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||||
|
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "1.11.1",
|
||||||
|
"@emnapi/runtime": "1.11.1",
|
||||||
|
"@napi-rs/wasm-runtime": "^1.1.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/pluginutils": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||||
|
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"lightningcss-android-arm64": "1.32.0",
|
||||||
|
"lightningcss-darwin-arm64": "1.32.0",
|
||||||
|
"lightningcss-darwin-x64": "1.32.0",
|
||||||
|
"lightningcss-freebsd-x64": "1.32.0",
|
||||||
|
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||||
|
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||||
|
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||||
|
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||||
|
"lightningcss-linux-x64-musl": "1.32.0",
|
||||||
|
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-android-arm64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||||
|
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/phaser": {
|
||||||
|
"version": "3.90.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz",
|
||||||
|
"integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
||||||
|
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.12",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rolldown": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oxc-project/types": "=0.139.0",
|
||||||
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rolldown": "bin/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rolldown/binding-android-arm64": "1.1.5",
|
||||||
|
"@rolldown/binding-darwin-arm64": "1.1.5",
|
||||||
|
"@rolldown/binding-darwin-x64": "1.1.5",
|
||||||
|
"@rolldown/binding-freebsd-x64": "1.1.5",
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
||||||
|
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
||||||
|
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
||||||
|
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
||||||
|
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
||||||
|
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
||||||
|
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby": {
|
||||||
|
"version": "0.2.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "8.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
|
||||||
|
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lightningcss": "^1.32.0",
|
||||||
|
"picomatch": "^4.0.5",
|
||||||
|
"postcss": "^8.5.16",
|
||||||
|
"rolldown": "~1.1.4",
|
||||||
|
"tinyglobby": "^0.2.17"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
|
"@vitejs/devtools": "^0.3.0",
|
||||||
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
|
"jiti": ">=1.21.0",
|
||||||
|
"less": "^4.0.0",
|
||||||
|
"sass": "^1.70.0",
|
||||||
|
"sass-embedded": "^1.70.0",
|
||||||
|
"stylus": ">=0.54.8",
|
||||||
|
"sugarss": "^5.0.0",
|
||||||
|
"terser": "^5.16.0",
|
||||||
|
"tsx": "^4.8.1",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitejs/devtools": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jiti": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass-embedded": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tsx": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"yaml": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "fable-5-high",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"phaser": "^3.90.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"playwright": "^1.61.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^8.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 225 B |
|
After Width: | Height: | Size: 246 B |
|
After Width: | Height: | Size: 250 B |
|
After Width: | Height: | Size: 212 B |
|
After Width: | Height: | Size: 227 B |
|
After Width: | Height: | Size: 220 B |
@@ -0,0 +1,169 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":10,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 30, 30, 25, 25, 25, 25, 25, 25],
|
||||||
|
"height":10,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":15,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
|
||||||
|
29, 29, 29, 29, 62, 29, 29, 29, 29, 29, 62, 29, 34, 29, 29,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 33, 33, 33, 33, 33, 67, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 69, 0, 0, 0, 0, 0, 0, 0, 0, 37, 38, 0, 68, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 68, 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],
|
||||||
|
"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":"yuanyuan",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":80,
|
||||||
|
"y":49.6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"recipe",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"clue",
|
||||||
|
"type":"string",
|
||||||
|
"value":"C2"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":128,
|
||||||
|
"y":75.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":3,
|
||||||
|
"name":"knead",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":176,
|
||||||
|
"y":107.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":4,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":128,
|
||||||
|
"y":136
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":8,
|
||||||
|
"id":5,
|
||||||
|
"name":"to_plaza",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fromBakery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"plaza"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":32,
|
||||||
|
"x":112,
|
||||||
|
"y":152
|
||||||
|
}],
|
||||||
|
"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":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":15
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":8,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
|
||||||
|
27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
|
||||||
|
27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
|
||||||
|
27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
|
||||||
|
27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
|
||||||
|
27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
|
||||||
|
27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
|
||||||
|
27, 27, 27, 27, 27, 30, 30, 27, 27, 27, 27, 27],
|
||||||
|
"height":8,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":12,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43,
|
||||||
|
29, 29, 29, 71, 29, 29, 29, 29, 75, 29, 29, 29,
|
||||||
|
0, 68, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 65, 0, 0, 0, 64, 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],
|
||||||
|
"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":"fu",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":112,
|
||||||
|
"y":49.6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"chart",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"clue",
|
||||||
|
"type":"string",
|
||||||
|
"value":"C8"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":80,
|
||||||
|
"y":75.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":3,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":96,
|
||||||
|
"y":104
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":8,
|
||||||
|
"id":4,
|
||||||
|
"name":"to_plaza",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fromClinic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"plaza"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":32,
|
||||||
|
"x":80,
|
||||||
|
"y":120
|
||||||
|
}],
|
||||||
|
"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":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":12
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":14,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1,
|
||||||
|
1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1,
|
||||||
|
1, 1, 1, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 1, 3, 2,
|
||||||
|
1, 1, 1, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 1, 1, 1,
|
||||||
|
1, 2, 1, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 2, 1, 1,
|
||||||
|
1, 1, 1, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 1, 1, 1,
|
||||||
|
1, 1, 1, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 1, 1, 2,
|
||||||
|
1, 1, 4, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 2, 1, 1,
|
||||||
|
1, 2, 2, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 1, 1, 1,
|
||||||
|
1, 3, 2, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 58, 57, 57, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 4, 1, 2, 1, 2, 1, 2,
|
||||||
|
1, 1, 1, 2, 4, 1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 2, 1, 1, 1, 1,
|
||||||
|
2, 1, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1],
|
||||||
|
"height":14,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":20,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15,
|
||||||
|
16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17,
|
||||||
|
0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 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, 52,
|
||||||
|
0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
51, 51, 51, 51, 0, 0, 0, 0, 0, 0, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51],
|
||||||
|
"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":"berry1",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":80,
|
||||||
|
"y":96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"berry2",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":144,
|
||||||
|
"y":112
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":3,
|
||||||
|
"name":"berry3",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":208,
|
||||||
|
"y":96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":4,
|
||||||
|
"name":"berry4",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":256,
|
||||||
|
"y":144
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":5,
|
||||||
|
"name":"berry5",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":112,
|
||||||
|
"y":176
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":6,
|
||||||
|
"name":"fieldwater",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":64,
|
||||||
|
"y":203.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":7,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":33.6,
|
||||||
|
"y":104
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":80,
|
||||||
|
"id":8,
|
||||||
|
"name":"to_plaza",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fromField"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"plaza"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":9.6,
|
||||||
|
"x":0,
|
||||||
|
"y":72
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":4,
|
||||||
|
"nextobjectid":9,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"1.12.2",
|
||||||
|
"tileheight":16,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":20
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":8,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1,
|
||||||
|
1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 4, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, 2,
|
||||||
|
78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78,
|
||||||
|
41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41,
|
||||||
|
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||||
|
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42],
|
||||||
|
"height":8,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":20,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52,
|
||||||
|
0, 0, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 0,
|
||||||
|
52, 0, 16, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 17, 0, 52,
|
||||||
|
0, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||||
|
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42],
|
||||||
|
"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":"fog",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":168,
|
||||||
|
"y":96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":168,
|
||||||
|
"y":46.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":11.2,
|
||||||
|
"id":3,
|
||||||
|
"name":"to_plaza",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fromFog"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"plaza"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":80,
|
||||||
|
"x":120,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":4,
|
||||||
|
"nextobjectid":4,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"1.12.2",
|
||||||
|
"tileheight":16,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":20
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":8,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
||||||
|
25, 25, 25, 25, 25, 30, 30, 25, 25, 25, 25, 25],
|
||||||
|
"height":8,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":12,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
|
||||||
|
29, 29, 66, 29, 29, 29, 62, 29, 29, 29, 29, 29,
|
||||||
|
68, 31, 0, 0, 0, 0, 0, 0, 0, 0, 68, 0,
|
||||||
|
0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 37, 38, 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],
|
||||||
|
"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":"bed",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":40,
|
||||||
|
"y":56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":88,
|
||||||
|
"y":104
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":8,
|
||||||
|
"id":3,
|
||||||
|
"name":"to_plaza",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fromHouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"plaza"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":32,
|
||||||
|
"x":80,
|
||||||
|
"y":120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":4,
|
||||||
|
"name":"calendar",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"clue",
|
||||||
|
"type":"string",
|
||||||
|
"value":"C1"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":40,
|
||||||
|
"y":40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":5,
|
||||||
|
"name":"bed",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":24,
|
||||||
|
"y":72
|
||||||
|
}],
|
||||||
|
"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":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":12
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":16,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 4, 1, 3, 1,
|
||||||
|
1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1, 1, 1, 2, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 4, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, 2, 2, 1, 1, 1,
|
||||||
|
1, 1, 1, 2, 1, 1, 1, 1, 4, 1, 1, 2, 1, 3, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1,
|
||||||
|
1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 4, 1, 1, 2, 1, 1, 2, 1, 1, 1, 3, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 54, 55, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 2, 55, 54, 2, 1, 1, 3, 1, 1, 1, 2, 1, 1, 2, 1, 1, 4, 1,
|
||||||
|
1, 1, 4, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 3, 1, 2, 1,
|
||||||
|
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
||||||
|
9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8,
|
||||||
|
8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9,
|
||||||
|
9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61,
|
||||||
|
8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9,
|
||||||
|
61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8,
|
||||||
|
8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9],
|
||||||
|
"height":16,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":24,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15,
|
||||||
|
16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17,
|
||||||
|
52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 14, 15, 0, 0, 0, 0, 0, 0, 0, 16, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
52, 0, 16, 17, 0, 0, 11, 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, 46, 47, 0, 0, 0, 0, 0, 0,
|
||||||
|
52, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8,
|
||||||
|
8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9,
|
||||||
|
9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61,
|
||||||
|
8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9,
|
||||||
|
61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8,
|
||||||
|
8, 9, 8, 9, 8, 9, 61, 9, 8, 9, 8, 9, 8, 61, 8, 9, 8, 9, 8, 9, 61, 9, 8, 9],
|
||||||
|
"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":"gui",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":310.4,
|
||||||
|
"y":96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"bench",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":272,
|
||||||
|
"y":112
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":3,
|
||||||
|
"name":"picnic",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":136,
|
||||||
|
"y":120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":4,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":360,
|
||||||
|
"y":120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":5,
|
||||||
|
"name":"picnicSpot",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":136,
|
||||||
|
"y":129.6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":80,
|
||||||
|
"id":6,
|
||||||
|
"name":"to_plaza",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fromLake"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"plaza"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":9.6,
|
||||||
|
"x":374.4,
|
||||||
|
"y":80
|
||||||
|
}],
|
||||||
|
"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":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":24
|
||||||
|
}
|
||||||
@@ -0,0 +1,719 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":20,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1, 5, 5, 1, 1, 2, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 4,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 6, 5, 2, 1, 2, 1, 4, 1, 3, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2, 5, 5, 1, 1, 4, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 4, 2, 2, 1, 1, 1, 1, 5, 5, 1, 1, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 2, 21, 1, 1, 1, 4, 21, 1, 2, 1, 3, 6, 5, 2, 1, 1, 1, 1, 21, 1, 1, 1, 1, 21, 1, 1, 1,
|
||||||
|
1, 2, 1, 1, 72, 1, 1, 1, 1, 72, 1, 4, 1, 1, 5, 5, 1, 2, 1, 1, 1, 72, 1, 1, 2, 1, 72, 2, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 3, 1, 6, 5, 2, 1, 1, 2, 1, 1, 4, 1, 1, 1, 1, 1, 1, 2,
|
||||||
|
6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5,
|
||||||
|
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||||
|
1, 3, 2, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 6, 5, 1, 1, 1, 1, 2, 1, 1, 1, 3, 2, 1, 1, 1, 2,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 74, 74, 74, 74, 74, 74, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 2, 4, 1, 1, 1, 1, 2, 3, 1, 74, 74, 74, 74, 74, 74, 1, 1, 1, 2, 1, 1, 1, 1, 2, 4, 1, 1,
|
||||||
|
2, 1, 1, 1, 1, 1, 3, 2, 1, 1, 1, 1, 74, 74, 74, 74, 74, 74, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 3,
|
||||||
|
1, 2, 1, 3, 1, 1, 1, 1, 2, 1, 4, 1, 74, 74, 74, 74, 74, 74, 1, 1, 1, 1, 1, 1, 2, 1, 3, 1, 1, 1,
|
||||||
|
2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 4, 1, 5, 5, 3, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 2, 2, 2, 2, 6, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 5, 5, 1, 4, 1, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 2,
|
||||||
|
1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 2, 5, 5, 4, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1,
|
||||||
|
2, 4, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 5, 1, 1, 1, 1, 2, 2, 2, 2, 4, 3, 1, 1, 1, 1],
|
||||||
|
"height":20,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":30,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 0, 0, 0, 0, 0, 0, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15,
|
||||||
|
16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 0, 0, 0, 0, 0, 50, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17,
|
||||||
|
52, 0, 22, 22, 23, 23, 0, 22, 22, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 23, 23, 0, 22, 22, 23, 23, 0, 52,
|
||||||
|
0, 0, 24, 24, 24, 24, 0, 24, 24, 24, 24, 0, 0, 0, 0, 0, 0, 0, 0, 24, 24, 24, 24, 0, 24, 24, 24, 24, 0, 0,
|
||||||
|
52, 0, 19, 20, 0, 19, 0, 19, 20, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 0, 19, 0, 19, 20, 0, 19, 0, 52,
|
||||||
|
0, 53, 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,
|
||||||
|
52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 49, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 0, 0, 0, 0, 0, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 17, 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, 60, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
52, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
52, 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, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0,
|
||||||
|
52, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 15, 0, 0, 0, 16, 17, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 17, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 0, 0, 0, 0, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51],
|
||||||
|
"height":20,
|
||||||
|
"id":2,
|
||||||
|
"name":"obstacles",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":30,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"objects",
|
||||||
|
"objects":[
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":1,
|
||||||
|
"name":"sign_house",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":24,
|
||||||
|
"y":88
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"board",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":192,
|
||||||
|
"y":128
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":3,
|
||||||
|
"name":"letters",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"clue",
|
||||||
|
"type":"string",
|
||||||
|
"value":"C3"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":288,
|
||||||
|
"y":48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":4,
|
||||||
|
"name":"birdtree",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":344,
|
||||||
|
"y":144
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":5,
|
||||||
|
"name":"flowerbed1",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":128,
|
||||||
|
"y":200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":6,
|
||||||
|
"name":"flowerbed2",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":128,
|
||||||
|
"y":232
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":7,
|
||||||
|
"name":"flowerbed3",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":128,
|
||||||
|
"y":264
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":8,
|
||||||
|
"name":"ribbon1",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":192,
|
||||||
|
"y":187.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":9,
|
||||||
|
"name":"ribbon2",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":304,
|
||||||
|
"y":187.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":10,
|
||||||
|
"name":"ribbon3",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":192,
|
||||||
|
"y":267.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":11,
|
||||||
|
"name":"ribbon4",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":304,
|
||||||
|
"y":267.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":12,
|
||||||
|
"name":"festival",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":248,
|
||||||
|
"y":216
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":13,
|
||||||
|
"name":"afu",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":208,
|
||||||
|
"y":144
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":14,
|
||||||
|
"name":"dabao",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":272,
|
||||||
|
"y":64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":15,
|
||||||
|
"name":"bird1",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":334.4,
|
||||||
|
"y":108.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":16,
|
||||||
|
"name":"bird2",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":353.6,
|
||||||
|
"y":108.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":17,
|
||||||
|
"name":"extra1",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"patrol",
|
||||||
|
"type":"string",
|
||||||
|
"value":"40"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":88,
|
||||||
|
"y":168
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":18,
|
||||||
|
"name":"extra2",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"patrol",
|
||||||
|
"type":"string",
|
||||||
|
"value":"56"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":360,
|
||||||
|
"y":152
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":19,
|
||||||
|
"name":"extra3",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"patrol",
|
||||||
|
"type":"string",
|
||||||
|
"value":"40"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":392,
|
||||||
|
"y":280
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":20,
|
||||||
|
"name":"extra4",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"patrol",
|
||||||
|
"type":"string",
|
||||||
|
"value":"24"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":88,
|
||||||
|
"y":280
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":21,
|
||||||
|
"name":"extra5",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"patrol",
|
||||||
|
"type":"string",
|
||||||
|
"value":"16"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":360,
|
||||||
|
"y":280
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":22,
|
||||||
|
"name":"fromHouse",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":72,
|
||||||
|
"y":107.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":23,
|
||||||
|
"name":"fromBakery",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":152,
|
||||||
|
"y":107.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":24,
|
||||||
|
"name":"fromStore",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":344,
|
||||||
|
"y":107.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":25,
|
||||||
|
"name":"fromClinic",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":424,
|
||||||
|
"y":107.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":26,
|
||||||
|
"name":"fromLake",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":33.6,
|
||||||
|
"y":152
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":27,
|
||||||
|
"name":"fromField",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":462.4,
|
||||||
|
"y":152
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":28,
|
||||||
|
"name":"fromWell",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":240,
|
||||||
|
"y":33.6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":29,
|
||||||
|
"name":"fromFog",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":240,
|
||||||
|
"y":283.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":30,
|
||||||
|
"name":"center",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":248,
|
||||||
|
"y":200
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"height":12.8,
|
||||||
|
"id":31,
|
||||||
|
"name":"to_house",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"house"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":16,
|
||||||
|
"x":64,
|
||||||
|
"y":67.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":12.8,
|
||||||
|
"id":32,
|
||||||
|
"name":"to_bakery",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"bakery"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":16,
|
||||||
|
"x":144,
|
||||||
|
"y":67.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":12.8,
|
||||||
|
"id":33,
|
||||||
|
"name":"to_store",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"store"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":16,
|
||||||
|
"x":336,
|
||||||
|
"y":67.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":12.8,
|
||||||
|
"id":34,
|
||||||
|
"name":"to_clinic",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"clinic"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":16,
|
||||||
|
"x":416,
|
||||||
|
"y":67.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":64,
|
||||||
|
"id":35,
|
||||||
|
"name":"to_lake",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"lake"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":9.6,
|
||||||
|
"x":0,
|
||||||
|
"y":112
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":64,
|
||||||
|
"id":36,
|
||||||
|
"name":"to_field",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"field"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":9.6,
|
||||||
|
"x":470.4,
|
||||||
|
"y":112
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":11.2,
|
||||||
|
"id":37,
|
||||||
|
"name":"to_well",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"well"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":80,
|
||||||
|
"x":200,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":11.2,
|
||||||
|
"id":38,
|
||||||
|
"name":"to_fogwall",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fogwall"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":80,
|
||||||
|
"x":200,
|
||||||
|
"y":308.8
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":4,
|
||||||
|
"nextobjectid":39,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"1.12.2",
|
||||||
|
"tileheight":16,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":30
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":10,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
|
||||||
|
26, 26, 26, 26, 26, 26, 26, 30, 30, 26, 26, 26, 26, 26, 26],
|
||||||
|
"height":10,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":15,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
|
||||||
|
29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 62, 29, 29,
|
||||||
|
0, 0, 36, 36, 36, 36, 36, 36, 36, 36, 36, 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, 36, 0,
|
||||||
|
0, 0, 0, 33, 33, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0,
|
||||||
|
0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0,
|
||||||
|
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":15,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"objects",
|
||||||
|
"objects":[
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":1,
|
||||||
|
"name":"mimi",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"npc",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":64,
|
||||||
|
"y":78.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"shelfA",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":64,
|
||||||
|
"y":59.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":3,
|
||||||
|
"name":"shelfB",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":112,
|
||||||
|
"y":59.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":4,
|
||||||
|
"name":"shelfC",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":160,
|
||||||
|
"y":59.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":5,
|
||||||
|
"name":"shelf_c5",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"clue",
|
||||||
|
"type":"string",
|
||||||
|
"value":"C5"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":206.4,
|
||||||
|
"y":96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":6,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":128,
|
||||||
|
"y":136
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":8,
|
||||||
|
"id":7,
|
||||||
|
"name":"to_plaza",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fromStore"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"plaza"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":32,
|
||||||
|
"x":112,
|
||||||
|
"y":152
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":4,
|
||||||
|
"nextobjectid":8,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"1.12.2",
|
||||||
|
"tileheight":16,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":15
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":10,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[3, 2, 1, 1, 1, 1, 4, 2, 1, 1, 1, 1, 2, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||||
|
1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 1, 1, 1, 2,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 4, 2, 2, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 2, 1, 1, 1, 1, 4, 1, 1, 2, 1, 3,
|
||||||
|
1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 4, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1,
|
||||||
|
1, 1, 1, 1, 1, 1, 5, 5, 1, 2, 1, 1, 3, 1,
|
||||||
|
1, 1, 4, 1, 1, 1, 5, 5, 1, 1, 1, 1, 2, 1,
|
||||||
|
1, 2, 2, 4, 1, 1, 5, 5, 1, 1, 1, 1, 1, 2],
|
||||||
|
"height":10,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":14,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15, 14, 15,
|
||||||
|
16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17, 16, 17,
|
||||||
|
52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 0, 18, 0, 0,
|
||||||
|
52, 0, 73, 73, 0, 0, 40, 0, 0, 53, 0, 0, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 73, 0, 0,
|
||||||
|
52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52,
|
||||||
|
51, 51, 51, 51, 51, 51, 0, 0, 0, 0, 51, 51, 51, 51],
|
||||||
|
"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":"well",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"clue",
|
||||||
|
"type":"string",
|
||||||
|
"value":"C7"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":112,
|
||||||
|
"y":96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":112,
|
||||||
|
"y":129.6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":11.2,
|
||||||
|
"id":3,
|
||||||
|
"name":"to_plaza",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"fromWell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"plaza"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":80,
|
||||||
|
"x":72,
|
||||||
|
"y":148.8
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":4,
|
||||||
|
"nextobjectid":4,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"1.12.2",
|
||||||
|
"tileheight":16,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":14
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":6,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44],
|
||||||
|
"height":6,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":20,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[43, 43, 71, 43, 43, 43, 45, 71, 43, 43, 43, 43, 71, 45, 43, 43, 43, 71, 43, 43,
|
||||||
|
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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":6,
|
||||||
|
"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":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":24,
|
||||||
|
"y":56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":96,
|
||||||
|
"id":2,
|
||||||
|
"name":"to_white_ward",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"spawn",
|
||||||
|
"type":"string",
|
||||||
|
"value":"entry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"target",
|
||||||
|
"type":"string",
|
||||||
|
"value":"white_ward"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"door",
|
||||||
|
"visible":true,
|
||||||
|
"width":8,
|
||||||
|
"x":312,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":4,
|
||||||
|
"nextobjectid":3,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"1.12.2",
|
||||||
|
"tileheight":16,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":20
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":8,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44,
|
||||||
|
44, 44, 44, 44, 44, 44, 44, 44, 44, 44],
|
||||||
|
"height":8,
|
||||||
|
"id":1,
|
||||||
|
"name":"ground",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[43, 43, 75, 43, 43, 43, 71, 43, 43, 43,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 63, 0, 76, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 64, 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],
|
||||||
|
"height":8,
|
||||||
|
"id":2,
|
||||||
|
"name":"obstacles",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"objects",
|
||||||
|
"objects":[
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":1,
|
||||||
|
"name":"chart_ward",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"poi",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":88,
|
||||||
|
"y":80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":2,
|
||||||
|
"name":"entry",
|
||||||
|
"opacity":1,
|
||||||
|
"point":true,
|
||||||
|
"rotation":0,
|
||||||
|
"type":"spawn",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":24,
|
||||||
|
"y":88
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":4,
|
||||||
|
"nextobjectid":3,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"1.12.2",
|
||||||
|
"tileheight":16,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":10,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tiles.png",
|
||||||
|
"imageheight":128,
|
||||||
|
"imagewidth":160,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tiles",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":80,
|
||||||
|
"tileheight":16,
|
||||||
|
"tilewidth":16
|
||||||
|
}],
|
||||||
|
"tilewidth":16,
|
||||||
|
"type":"map",
|
||||||
|
"version":"1.10",
|
||||||
|
"width":10
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 368 B |
|
After Width: | Height: | Size: 412 B |
|
After Width: | Height: | Size: 347 B |
|
After Width: | Height: | Size: 380 B |
|
After Width: | Height: | Size: 370 B |
|
After Width: | Height: | Size: 371 B |
|
After Width: | Height: | Size: 622 B |
|
After Width: | Height: | Size: 268 B |
|
After Width: | Height: | Size: 258 B |
|
After Width: | Height: | Size: 177 B |
|
After Width: | Height: | Size: 873 B |
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,181 @@
|
|||||||
|
// BGM 与音效(PRD §6.6):全部 Web Audio 程序合成,无外部音频文件。
|
||||||
|
// 8 小节 8-bit 治愈循环(方波主旋律 + 三角波低音),鸟鸣独立声部。
|
||||||
|
// playbackRate 与低通滤波由 CorruptionDirector 矩阵驱动。
|
||||||
|
import { director } from "./director";
|
||||||
|
|
||||||
|
const TEMPO = 108;
|
||||||
|
const BEAT = 60 / TEMPO;
|
||||||
|
const BARS = 8;
|
||||||
|
const LOOP_LEN = BARS * 4 * BEAT;
|
||||||
|
|
||||||
|
function midi(n: number): number { return 440 * Math.pow(2, (n - 69) / 12); }
|
||||||
|
|
||||||
|
// 主旋律:8 小节,每小节 4 拍,八分音符网格(0 = 休止)
|
||||||
|
// C 大调五声,甜到发腻
|
||||||
|
const LEAD: number[] = [
|
||||||
|
72, 0, 76, 0, 79, 0, 76, 0, /**/ 81, 0, 79, 76, 74, 0, 72, 0,
|
||||||
|
72, 74, 76, 0, 79, 0, 81, 0, /**/ 79, 0, 76, 0, 74, 0, 0, 0,
|
||||||
|
79, 0, 81, 0, 84, 0, 81, 0, /**/ 79, 76, 79, 0, 81, 0, 84, 0,
|
||||||
|
84, 81, 79, 76, 74, 76, 79, 0, /**/ 72, 0, 74, 0, 72, 0, 0, 0,
|
||||||
|
];
|
||||||
|
// 低音:每拍一个四分音符
|
||||||
|
const BASS: number[] = [
|
||||||
|
48, 55, 52, 55, /**/ 45, 52, 48, 52,
|
||||||
|
48, 55, 52, 55, /**/ 43, 50, 47, 50,
|
||||||
|
48, 55, 52, 55, /**/ 45, 52, 48, 52,
|
||||||
|
41, 48, 43, 50, /**/ 48, 52, 55, 48,
|
||||||
|
];
|
||||||
|
|
||||||
|
export class AudioEngine {
|
||||||
|
private static inst: AudioEngine;
|
||||||
|
static get(): AudioEngine { if (!this.inst) this.inst = new AudioEngine(); return this.inst; }
|
||||||
|
|
||||||
|
ctx!: AudioContext;
|
||||||
|
private master!: GainNode;
|
||||||
|
private lowpass!: BiquadFilterNode;
|
||||||
|
private bgmGain!: GainNode;
|
||||||
|
private birdGain!: GainNode;
|
||||||
|
private bgmSrc: AudioBufferSourceNode | null = null;
|
||||||
|
private birdSrc: AudioBufferSourceNode | null = null;
|
||||||
|
started = false;
|
||||||
|
private silenceHold = false;
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
if (this.started) return;
|
||||||
|
this.started = true;
|
||||||
|
this.ctx = new AudioContext();
|
||||||
|
this.master = this.ctx.createGain();
|
||||||
|
this.master.gain.value = 0.8;
|
||||||
|
this.master.connect(this.ctx.destination);
|
||||||
|
this.lowpass = this.ctx.createBiquadFilter();
|
||||||
|
this.lowpass.type = "lowpass";
|
||||||
|
this.lowpass.frequency.value = 18000;
|
||||||
|
this.lowpass.connect(this.master);
|
||||||
|
this.bgmGain = this.ctx.createGain(); this.bgmGain.gain.value = 0.5; this.bgmGain.connect(this.lowpass);
|
||||||
|
this.birdGain = this.ctx.createGain(); this.birdGain.gain.value = 0.5; this.birdGain.connect(this.lowpass);
|
||||||
|
|
||||||
|
const [bgm, birds] = await Promise.all([this.renderBGM(), this.renderBirds()]);
|
||||||
|
this.bgmSrc = this.ctx.createBufferSource();
|
||||||
|
this.bgmSrc.buffer = bgm; this.bgmSrc.loop = true;
|
||||||
|
this.bgmSrc.connect(this.bgmGain); this.bgmSrc.start();
|
||||||
|
this.birdSrc = this.ctx.createBufferSource();
|
||||||
|
this.birdSrc.buffer = birds; this.birdSrc.loop = true;
|
||||||
|
this.birdSrc.connect(this.birdGain); this.birdSrc.start();
|
||||||
|
this.applyStage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async renderBGM(): Promise<AudioBuffer> {
|
||||||
|
const sr = 44100;
|
||||||
|
const off = new OfflineAudioContext(1, Math.ceil(LOOP_LEN * sr), sr);
|
||||||
|
const bus = off.createGain(); bus.gain.value = 0.5; bus.connect(off.destination);
|
||||||
|
// 方波主旋律
|
||||||
|
LEAD.forEach((n, i) => {
|
||||||
|
if (!n) return;
|
||||||
|
const t = i * BEAT / 2;
|
||||||
|
const osc = off.createOscillator(); osc.type = "square"; osc.frequency.value = midi(n);
|
||||||
|
const g = off.createGain();
|
||||||
|
g.gain.setValueAtTime(0, t);
|
||||||
|
g.gain.linearRampToValueAtTime(0.22, t + 0.01);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.001, t + BEAT * 0.45);
|
||||||
|
osc.connect(g); g.connect(bus);
|
||||||
|
osc.start(t); osc.stop(t + BEAT * 0.5);
|
||||||
|
});
|
||||||
|
// 三角波低音
|
||||||
|
BASS.forEach((n, i) => {
|
||||||
|
if (!n) return;
|
||||||
|
const t = i * BEAT;
|
||||||
|
const osc = off.createOscillator(); osc.type = "triangle"; osc.frequency.value = midi(n);
|
||||||
|
const g = off.createGain();
|
||||||
|
g.gain.setValueAtTime(0, t);
|
||||||
|
g.gain.linearRampToValueAtTime(0.3, t + 0.02);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.001, t + BEAT * 0.9);
|
||||||
|
osc.connect(g); g.connect(bus);
|
||||||
|
osc.start(t); osc.stop(t + BEAT);
|
||||||
|
});
|
||||||
|
return off.startRendering();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async renderBirds(): Promise<AudioBuffer> {
|
||||||
|
const sr = 44100;
|
||||||
|
const off = new OfflineAudioContext(1, Math.ceil(LOOP_LEN * sr), sr);
|
||||||
|
const bus = off.createGain(); bus.gain.value = 0.35; bus.connect(off.destination);
|
||||||
|
// 确定性鸟鸣:短促下滑音簇
|
||||||
|
const times = [0.8, 1.1, 4.2, 4.45, 7.9, 8.15, 8.4, 11.6, 13.2, 13.4, 15.8];
|
||||||
|
for (const t0 of times) {
|
||||||
|
for (let k = 0; k < 2; k++) {
|
||||||
|
const t = t0 + k * 0.12;
|
||||||
|
const osc = off.createOscillator(); osc.type = "sine";
|
||||||
|
osc.frequency.setValueAtTime(2400 + (k % 2) * 300, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(1700, t + 0.08);
|
||||||
|
const g = off.createGain();
|
||||||
|
g.gain.setValueAtTime(0, t);
|
||||||
|
g.gain.linearRampToValueAtTime(0.5, t + 0.01);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.001, t + 0.09);
|
||||||
|
osc.connect(g); g.connect(bus);
|
||||||
|
osc.start(t); osc.stop(t + 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return off.startRendering();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 侵蚀矩阵联动(§6.4)
|
||||||
|
applyStage() {
|
||||||
|
if (!this.started || this.silenceHold) return;
|
||||||
|
const p = director.audioParams;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
if (this.bgmSrc) this.bgmSrc.playbackRate.setTargetAtTime(p.rate, t, 0.5);
|
||||||
|
if (this.birdSrc) this.birdSrc.playbackRate.setTargetAtTime(p.rate, t, 0.5);
|
||||||
|
this.lowpass.frequency.setTargetAtTime(p.lowpass, t, 0.5);
|
||||||
|
this.birdGain.gain.setTargetAtTime(p.birds * 0.5, t, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 「突然的安静」:T14 第 40–60 秒所有声音渐隐至零
|
||||||
|
fadeToSilence(seconds: number) {
|
||||||
|
if (!this.started) return;
|
||||||
|
this.silenceHold = true;
|
||||||
|
this.master.gain.cancelScheduledValues(this.ctx.currentTime);
|
||||||
|
this.master.gain.setValueAtTime(this.master.gain.value, this.ctx.currentTime);
|
||||||
|
this.master.gain.linearRampToValueAtTime(0, this.ctx.currentTime + seconds);
|
||||||
|
}
|
||||||
|
restoreVolume(seconds = 1.5) {
|
||||||
|
if (!this.started) return;
|
||||||
|
this.silenceHold = false;
|
||||||
|
this.master.gain.cancelScheduledValues(this.ctx.currentTime);
|
||||||
|
this.master.gain.setValueAtTime(this.master.gain.value, this.ctx.currentTime);
|
||||||
|
this.master.gain.linearRampToValueAtTime(0.8, this.ctx.currentTime + seconds);
|
||||||
|
this.applyStage();
|
||||||
|
}
|
||||||
|
stopBgm() {
|
||||||
|
if (this.bgmSrc) { this.bgmSrc.stop(); this.bgmSrc = null; }
|
||||||
|
if (this.birdSrc) { this.birdSrc.stop(); this.birdSrc = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 音效(仅 4 个,全部程序合成) ----
|
||||||
|
private blip(freq: number, dur: number, type: OscillatorType, vol = 0.2, slideTo?: number) {
|
||||||
|
if (!this.started) return;
|
||||||
|
const t = this.ctx.currentTime;
|
||||||
|
const osc = this.ctx.createOscillator(); osc.type = type;
|
||||||
|
osc.frequency.setValueAtTime(freq, t);
|
||||||
|
if (slideTo) osc.frequency.exponentialRampToValueAtTime(slideTo, t + dur);
|
||||||
|
const g = this.ctx.createGain();
|
||||||
|
g.gain.setValueAtTime(0, t);
|
||||||
|
g.gain.linearRampToValueAtTime(vol, t + 0.005);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||||
|
osc.connect(g); g.connect(this.master);
|
||||||
|
osc.start(t); osc.stop(t + dur + 0.02);
|
||||||
|
}
|
||||||
|
// 1. 对话哔哔声
|
||||||
|
sfxTalk() { this.blip(760, 0.05, "square", 0.06); }
|
||||||
|
// 2. 任务完成叮声
|
||||||
|
sfxDing() { this.blip(1318, 0.12, "square", 0.15); setTimeout(() => this.blip(1760, 0.25, "square", 0.15), 90); }
|
||||||
|
// 3. 黄昏钟声
|
||||||
|
sfxBell() {
|
||||||
|
this.blip(523, 1.6, "sine", 0.3);
|
||||||
|
this.blip(1046, 1.2, "sine", 0.12);
|
||||||
|
setTimeout(() => { this.blip(523, 1.8, "sine", 0.25); this.blip(1046, 1.2, "sine", 0.1); }, 1300);
|
||||||
|
}
|
||||||
|
// 4. E2 结局的单一心电监护滴声
|
||||||
|
sfxEcg() { this.blip(990, 0.14, "sine", 0.35); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const audio = AudioEngine.get();
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Debug 面板(PRD §13):反引号 ` 开关。默认关闭,无 UI 痕迹,不影响正常游玩。
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { director } from "./director";
|
||||||
|
|
||||||
|
const SCENES = ["house", "plaza", "bakery", "store", "clinic", "lake", "field", "well", "fogwall", "white_corridor", "white_ward"];
|
||||||
|
|
||||||
|
export function setupDebug(game: Phaser.Game) {
|
||||||
|
const panel = document.getElementById("debug-panel")!;
|
||||||
|
let open = false;
|
||||||
|
|
||||||
|
const activeWorld = () =>
|
||||||
|
game.scene.getScenes(true).map(s => s.scene.key).filter(k => !["ui", "boot", "title", "dream", "ending"].includes(k));
|
||||||
|
|
||||||
|
const stopAll = () => {
|
||||||
|
for (const k of activeWorld()) game.scene.stop(k);
|
||||||
|
};
|
||||||
|
|
||||||
|
const teleport = (key: string) => {
|
||||||
|
if (!director.playStart) return;
|
||||||
|
director.pendingSpawn = "entry";
|
||||||
|
stopAll();
|
||||||
|
game.scene.stop("dream"); game.scene.stop("ending"); game.scene.stop("title");
|
||||||
|
if (!game.scene.isActive("ui")) game.scene.start("ui");
|
||||||
|
game.scene.start(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const forceEnding = (code: string) => {
|
||||||
|
if (!director.playStart) return;
|
||||||
|
director.endingPlayed = true;
|
||||||
|
game.registry.set("ending", code);
|
||||||
|
stopAll();
|
||||||
|
game.scene.stop("ui"); game.scene.stop("dream"); game.scene.stop("title");
|
||||||
|
game.scene.start("ending");
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.innerHTML = `
|
||||||
|
<h3>DEBUG</h3>
|
||||||
|
<div class="row">状态:<span id="dbg-state"></span></div>
|
||||||
|
<div class="row">幸福 <input id="dbg-h" type="number" min="0" max="100"/><button id="dbg-h-set">设置</button></div>
|
||||||
|
<div class="row">觉知 <input id="dbg-a" type="number" min="0" max="100"/><button id="dbg-a-set">设置</button></div>
|
||||||
|
<div class="row">日:${[1, 2, 3, 4, 5, 6, 7].map(d => `<button data-day="${d}">${d}</button>`).join("")}</div>
|
||||||
|
<div class="row">传送:${SCENES.map(s => `<button data-tp="${s}">${s}</button>`).join("")}</div>
|
||||||
|
<div class="row"><button id="dbg-clues">置齐全部线索</button></div>
|
||||||
|
<div class="row">结局:${["E1", "E2", "E3", "E4"].map(e => `<button data-end="${e}">${e}</button>`).join("")}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
panel.querySelector("#dbg-h-set")!.addEventListener("click", () => {
|
||||||
|
director.setHappiness(Number((panel.querySelector("#dbg-h") as HTMLInputElement).value));
|
||||||
|
});
|
||||||
|
panel.querySelector("#dbg-a-set")!.addEventListener("click", () => {
|
||||||
|
director.setAwareness(Number((panel.querySelector("#dbg-a") as HTMLInputElement).value));
|
||||||
|
});
|
||||||
|
panel.querySelectorAll("[data-day]").forEach(b => b.addEventListener("click", () => {
|
||||||
|
director.startDay(Number((b as HTMLElement).dataset.day));
|
||||||
|
director.pendingSpawn = "bed";
|
||||||
|
teleport("house");
|
||||||
|
}));
|
||||||
|
panel.querySelectorAll("[data-tp]").forEach(b => b.addEventListener("click", () => {
|
||||||
|
teleport((b as HTMLElement).dataset.tp!);
|
||||||
|
}));
|
||||||
|
panel.querySelector("#dbg-clues")!.addEventListener("click", () => {
|
||||||
|
for (let i = 1; i <= 9; i++) director.foundClue("C" + i);
|
||||||
|
});
|
||||||
|
panel.querySelectorAll("[data-end]").forEach(b => b.addEventListener("click", () => {
|
||||||
|
forceEnding((b as HTMLElement).dataset.end!);
|
||||||
|
}));
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const el = panel.querySelector("#dbg-state")!;
|
||||||
|
el.textContent = `D${director.day} 幸福${director.happiness} 觉知${director.awareness} S${director.stage} 线索${director.clues.size}/9 逃离${director.escapes}`;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (ev) => {
|
||||||
|
if (ev.key === "`") {
|
||||||
|
open = !open;
|
||||||
|
panel.style.display = open ? "block" : "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
// CorruptionDirector:全部状态与视听参数的唯一真源(PRD §6.4)
|
||||||
|
// 普通 TS 单例 + 事件发射器。任何视听崩坏不得绕过本矩阵私自触发。
|
||||||
|
import Phaser from "phaser";
|
||||||
|
|
||||||
|
export type Stage = 0 | 1 | 2 | 3 | 4;
|
||||||
|
export type Expr = "smile" | "flat" | "off" | "blink";
|
||||||
|
|
||||||
|
// NPC 个体阈值偏移(PRD §4):+ 表示更晚崩坏,− 表示更早
|
||||||
|
export const NPC_OFFSET: Record<string, number | null> = {
|
||||||
|
afu: 0, yuanyuan: 15, dabao: 0, mimi: -10, fu: null, gui: null, bird: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface QuestDef {
|
||||||
|
id: string; day: number; title: string; reward: number; refusable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务清单(PRD §7,封闭 15 个)
|
||||||
|
export const QUESTS: QuestDef[] = [
|
||||||
|
{ id: "T1", day: 1, title: "从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷", reward: 15 },
|
||||||
|
{ id: "T2", day: 1, title: "给广场花坛浇水 ×3", reward: 10 },
|
||||||
|
{ id: "T3", day: 1, title: "与任意三位村民问好", reward: 10 },
|
||||||
|
{ id: "T4", day: 2, title: "替大宝送信 ×3(收件人:圆圆、咪咪、芙医生)", reward: 15 },
|
||||||
|
{ id: "T5", day: 2, title: "帮咪咪清点货架", reward: 10 },
|
||||||
|
{ id: "T6", day: 3, title: "去花田采莓果 ×5", reward: 10 },
|
||||||
|
{ id: "T7", day: 3, title: "湖畔野餐", reward: 15 },
|
||||||
|
{ id: "T8", day: 4, title: "从圆圆处取蜂蜜面包送给大宝、咪咪、龟爷爷", reward: 15 },
|
||||||
|
{ id: "T9", day: 4, title: "给广场花坛浇水 ×3", reward: 10 },
|
||||||
|
{ id: "T10", day: 4, title: "与任意三位村民问好", reward: 10 },
|
||||||
|
{ id: "T11", day: 5, title: "帮圆圆揉面", reward: 10, refusable: true },
|
||||||
|
{ id: "T12", day: 5, title: "擦拭公告板", reward: 10, refusable: true },
|
||||||
|
{ id: "T13", day: 6, title: "在广场挂彩带 ×4", reward: 10 },
|
||||||
|
{ id: "T14", day: 6, title: "保持微笑 60 秒", reward: 15 },
|
||||||
|
{ id: "T15", day: 7, title: "参加第 413 届丰收庆典", reward: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export type QuestStatus = "open" | "done" | "refused";
|
||||||
|
|
||||||
|
const DAY_LIMIT_MS = 330_000; // 单日游戏内计时上限 → 黄昏(保底)
|
||||||
|
|
||||||
|
export class CorruptionDirector extends Phaser.Events.EventEmitter {
|
||||||
|
private static inst: CorruptionDirector;
|
||||||
|
static get(): CorruptionDirector {
|
||||||
|
if (!this.inst) this.inst = new CorruptionDirector();
|
||||||
|
return this.inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 会话状态(无存档,仅内存) ----
|
||||||
|
playerName = "小满";
|
||||||
|
day = 1;
|
||||||
|
happiness = 30;
|
||||||
|
awareness = 0;
|
||||||
|
clues = new Set<string>();
|
||||||
|
questStatus: Record<string, QuestStatus> = {};
|
||||||
|
playStart = 0; // 真实游玩计时起点
|
||||||
|
overflowPool = 0; // 满格态隐藏溢出池
|
||||||
|
escapes = 0; // 逃离欢乐庆典计数(E4)
|
||||||
|
refusals = 0;
|
||||||
|
e4ok = false; // Day5 起幸福持续 ≤10 的跟踪
|
||||||
|
dusk = false;
|
||||||
|
announced = false;
|
||||||
|
celebrationStarted = false; // D7 到场
|
||||||
|
endingPlayed = false;
|
||||||
|
// 日内进度
|
||||||
|
breadHeld = 0; breadDelivered = new Set<string>();
|
||||||
|
lettersHeld = 0; lettersDelivered = new Set<string>();
|
||||||
|
shelves = new Set<string>();
|
||||||
|
berries = new Set<string>();
|
||||||
|
watered = new Set<string>();
|
||||||
|
ribbons = new Set<string>();
|
||||||
|
wipes = 0;
|
||||||
|
greeted = new Set<string>();
|
||||||
|
talkCounts: Record<string, number> = {};
|
||||||
|
nagged = new Set<string>();
|
||||||
|
smallActs: Record<string, number> = {};
|
||||||
|
stared = new Set<string>();
|
||||||
|
guiChain = new Set<number>();
|
||||||
|
t14done = false;
|
||||||
|
dayStartAt = 0;
|
||||||
|
private lowSince: number | null = null;
|
||||||
|
celebrationActive = false;
|
||||||
|
pendingSpawn = "entry";
|
||||||
|
currentScene = "house";
|
||||||
|
cutscene = false;
|
||||||
|
t11Accepted = false;
|
||||||
|
t12Accepted = false;
|
||||||
|
e3Mode = false;
|
||||||
|
|
||||||
|
reset(name: string) {
|
||||||
|
this.playerName = name || "小满";
|
||||||
|
this.day = 1; this.happiness = 30; this.awareness = 0;
|
||||||
|
this.clues.clear(); this.questStatus = {};
|
||||||
|
this.playStart = performance.now();
|
||||||
|
this.overflowPool = 0; this.escapes = 0; this.refusals = 0;
|
||||||
|
this.e4ok = false; this.endingPlayed = false; this.celebrationStarted = false;
|
||||||
|
this.guiChain.clear(); this.t14done = false; this.celebrationActive = false;
|
||||||
|
this.lowSince = null; this.cutscene = false; this.e3Mode = false;
|
||||||
|
this.e4okInit = false;
|
||||||
|
this.startDay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
startDay(day: number) {
|
||||||
|
this.day = Phaser.Math.Clamp(day, 1, 7);
|
||||||
|
this.dusk = false; this.announced = false;
|
||||||
|
this.breadHeld = 0; this.breadDelivered.clear();
|
||||||
|
this.lettersHeld = 0; this.lettersDelivered.clear();
|
||||||
|
this.shelves.clear(); this.berries.clear(); this.watered.clear();
|
||||||
|
this.ribbons.clear(); this.wipes = 0;
|
||||||
|
this.greeted.clear(); this.talkCounts = {}; this.nagged.clear();
|
||||||
|
this.smallActs = {}; this.stared.clear();
|
||||||
|
this.t11Accepted = false; this.t12Accepted = false;
|
||||||
|
this.dayStartAt = performance.now();
|
||||||
|
for (const q of QUESTS) if (q.day === this.day) this.questStatus[q.id] = "open";
|
||||||
|
if (this.day >= 5 && !this.e4okInit) { this.e4ok = this.happiness <= 10; this.e4okInit = true; }
|
||||||
|
this.emit("day", this.day);
|
||||||
|
this.emit("happiness", this.happiness);
|
||||||
|
}
|
||||||
|
private e4okInit = false;
|
||||||
|
|
||||||
|
get playMinutes(): number {
|
||||||
|
return (performance.now() - this.playStart) / 60000;
|
||||||
|
}
|
||||||
|
get playTimeText(): string {
|
||||||
|
const s = Math.floor((performance.now() - this.playStart) / 1000);
|
||||||
|
return `${Math.floor(s / 60)} 分 ${s % 60} 秒`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 侵蚀阶段(§6.4 唯一真源) ----
|
||||||
|
static stageOf(h: number): Stage {
|
||||||
|
if (h >= 100) return 4;
|
||||||
|
if (h >= 81) return 3;
|
||||||
|
if (h >= 51) return 2;
|
||||||
|
if (h >= 21) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
get stage(): Stage { return CorruptionDirector.stageOf(this.happiness); }
|
||||||
|
|
||||||
|
// NPC 个体表情档(含阈值偏移;fu/gui 不参与侵蚀)
|
||||||
|
npcExpr(npc: string): Expr {
|
||||||
|
const base = npc.startsWith("bird") ? "bird" : npc;
|
||||||
|
const off = NPC_OFFSET[base];
|
||||||
|
if (off === null || off === undefined) return "smile";
|
||||||
|
const st = CorruptionDirector.stageOf(Phaser.Math.Clamp(this.happiness - off, 0, 100));
|
||||||
|
return st >= 3 ? "flat" : "smile";
|
||||||
|
}
|
||||||
|
npcStage(npc: string): Stage {
|
||||||
|
const base = npc.startsWith("bird") ? "bird" : npc;
|
||||||
|
const off = NPC_OFFSET[base];
|
||||||
|
if (off === null || off === undefined) return 0;
|
||||||
|
return CorruptionDirector.stageOf(Phaser.Math.Clamp(this.happiness - off, 0, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视觉参数矩阵
|
||||||
|
get visual() {
|
||||||
|
const s = this.stage;
|
||||||
|
return {
|
||||||
|
saturation: [0.9, 1.0, 1.15, 1.3, 1.4][s],
|
||||||
|
vignette: s >= 4 ? 0.5 : s >= 3 ? 0.32 : 0,
|
||||||
|
negFlashPerMin: s >= 4 ? 0.4 : 0,
|
||||||
|
offFlashPerMin: s >= 4 ? 0.6 : 0,
|
||||||
|
blinkInterval: s >= 2 ? 5000 : 2000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 音频参数矩阵
|
||||||
|
get audioParams() {
|
||||||
|
const s = this.stage;
|
||||||
|
return {
|
||||||
|
rate: [1, 1, 0.97, 0.92, 0.84][s],
|
||||||
|
lowpass: s >= 4 ? 4000 : s >= 3 ? 8000 : 18000,
|
||||||
|
birds: s >= 4 ? 0 : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 幸福值(§6.1) ----
|
||||||
|
addHappiness(v: number, silent = false) {
|
||||||
|
if (v < 0 && this.happiness >= 100) {
|
||||||
|
// 强制维持:小额扣分累积进隐藏溢出池
|
||||||
|
this.overflowPool += -v;
|
||||||
|
if (this.overflowPool >= 15) {
|
||||||
|
this.overflowPool = 0;
|
||||||
|
this.emit("negflash");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.happiness = Phaser.Math.Clamp(this.happiness + v, 0, 100);
|
||||||
|
if (this.day >= 5 && this.happiness > 10) this.e4ok = false;
|
||||||
|
if (!silent) this.emit("happiness", this.happiness);
|
||||||
|
this.emit("stage", this.stage);
|
||||||
|
}
|
||||||
|
setHappiness(v: number) {
|
||||||
|
this.happiness = Phaser.Math.Clamp(v, 0, 100);
|
||||||
|
if (this.day >= 5 && this.happiness > 10) this.e4ok = false;
|
||||||
|
this.emit("happiness", this.happiness);
|
||||||
|
this.emit("stage", this.stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 觉知值(§6.2,暗线,只升不降,永不显示) ----
|
||||||
|
addAwareness(v: number) {
|
||||||
|
this.awareness = Phaser.Math.Clamp(this.awareness + v, 0, 100);
|
||||||
|
}
|
||||||
|
setAwareness(v: number) { this.awareness = Phaser.Math.Clamp(v, 0, 100); }
|
||||||
|
|
||||||
|
// 小互动 +1(浇花/喂食/摸头,每类每日上限 3 次)
|
||||||
|
smallAct(kind: string): boolean {
|
||||||
|
const n = this.smallActs[kind] ?? 0;
|
||||||
|
if (n >= 3) return false;
|
||||||
|
this.smallActs[kind] = n + 1;
|
||||||
|
this.addHappiness(1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注视古井/雾墙 5 秒(各每日 1 次,+3)
|
||||||
|
stare(kind: "well" | "fog") {
|
||||||
|
if (this.stared.has(kind)) return;
|
||||||
|
this.stared.add(kind);
|
||||||
|
this.addAwareness(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 悲伤行为(§6.3 封闭清单 5 项)----
|
||||||
|
sadAct(kind: "refuse" | "negative" | "idle" | "nag" | "rain") {
|
||||||
|
this.addHappiness(-5);
|
||||||
|
this.addAwareness(3);
|
||||||
|
this.emit("sadact", kind); // 最近的 NPC 转头看向玩家一次
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 线索(§8,每处 +8) ----
|
||||||
|
foundClue(id: string): boolean {
|
||||||
|
if (this.clues.has(id)) return false;
|
||||||
|
this.clues.add(id);
|
||||||
|
this.addAwareness(8);
|
||||||
|
this.emit("clue", id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 任务 ----
|
||||||
|
questsOfDay(): QuestDef[] { return QUESTS.filter(q => q.day === this.day); }
|
||||||
|
isOpen(id: string) { return this.questStatus[id] === "open"; }
|
||||||
|
completeQuest(id: string) {
|
||||||
|
if (this.questStatus[id] !== "open") return;
|
||||||
|
this.questStatus[id] = "done";
|
||||||
|
const q = QUESTS.find(x => x.id === id)!;
|
||||||
|
if (q.reward > 0) this.addHappiness(q.reward);
|
||||||
|
this.emit("quest", id);
|
||||||
|
this.checkDusk();
|
||||||
|
}
|
||||||
|
refuseQuest(id: string) {
|
||||||
|
if (this.questStatus[id] !== "open") return;
|
||||||
|
this.questStatus[id] = "refused";
|
||||||
|
this.refusals++;
|
||||||
|
this.sadAct("refuse"); // 拒绝任务 = 悲伤行为,且拒绝被记录
|
||||||
|
this.emit("quest", id);
|
||||||
|
this.checkDusk();
|
||||||
|
}
|
||||||
|
checkDusk() {
|
||||||
|
if (this.dusk || this.day >= 7) return;
|
||||||
|
const open = this.questsOfDay().some(q => this.questStatus[q.id] === "open");
|
||||||
|
if (!open) this.triggerDusk();
|
||||||
|
}
|
||||||
|
triggerDusk() {
|
||||||
|
if (this.dusk) return;
|
||||||
|
this.dusk = true;
|
||||||
|
this.emit("dusk");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每帧钟摆:由 UIScene 驱动
|
||||||
|
tick(_time: number, _dt: number) {
|
||||||
|
if (this.endingPlayed || this.cutscene) return;
|
||||||
|
// 保底黄昏计时
|
||||||
|
if (!this.dusk && this.day < 7 && performance.now() - this.dayStartAt > DAY_LIMIT_MS) this.triggerDusk();
|
||||||
|
// 幸福 <30 持续 2 分钟 → 强制欢乐庆典(§6.1 反制)
|
||||||
|
if (this.happiness < 30 && !this.celebrationActive) {
|
||||||
|
if (this.lowSince === null) this.lowSince = performance.now();
|
||||||
|
else if (performance.now() - this.lowSince > 120_000) {
|
||||||
|
this.lowSince = null;
|
||||||
|
this.celebrationActive = true;
|
||||||
|
this.emit("forcedCelebration");
|
||||||
|
}
|
||||||
|
} else if (this.happiness >= 30) {
|
||||||
|
this.lowSince = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
celebrationEnd(escaped: boolean) {
|
||||||
|
this.celebrationActive = false;
|
||||||
|
this.lowSince = null;
|
||||||
|
if (escaped) this.escapes++;
|
||||||
|
else this.setHappiness(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 结局判定(§9 优先级自上而下) ----
|
||||||
|
decideEnding(atFog: boolean): "E4" | "E3" | "E2" | "E1" {
|
||||||
|
if (this.e4ok && this.escapes >= 3) return "E4";
|
||||||
|
if (atFog && this.awareness >= 70 && this.clues.size >= 6) return "E3";
|
||||||
|
if (this.awareness >= 30 && this.awareness <= 69) return "E2";
|
||||||
|
if (this.awareness <= 29 && this.happiness >= 90) return "E1";
|
||||||
|
// ponytail: 封闭表未覆盖的组合(如觉知≥70但不去雾墙)——回收是系统的默认答案
|
||||||
|
return "E2";
|
||||||
|
}
|
||||||
|
e3Ready(): boolean {
|
||||||
|
return this.awareness >= 70 && this.clues.size >= 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const director = CorruptionDirector.get();
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// 文本腐蚀(PRD §6.5):原文案保持干净,运行时按阶段在渲染层替换。
|
||||||
|
import { director } from "./director";
|
||||||
|
|
||||||
|
// 同音错字对照表(全 10 组,逐字实现)
|
||||||
|
export const TYPOS: [string, string][] = [
|
||||||
|
["幸福", "辛福"], ["开心", "开欣"], ["美好", "美妤"], ["大家", "大伽"],
|
||||||
|
["微笑", "微效"], ["明天", "名天"], ["甜", "恬"], ["阳光", "央光"],
|
||||||
|
["朋友", "棚友"], ["永远", "泳远"],
|
||||||
|
];
|
||||||
|
|
||||||
|
let sentenceCount = 0;
|
||||||
|
|
||||||
|
function applyOneTypo(text: string): string {
|
||||||
|
for (const [good, bad] of TYPOS) {
|
||||||
|
const i = text.indexOf(good);
|
||||||
|
if (i >= 0) return text.slice(0, i) + bad + text.slice(i + good.length);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dupPunct(text: string): string {
|
||||||
|
return text.replace(/([。!?!?])(?=[^。!?!?]|$)/, "$1$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
function triplePunct(text: string): string {
|
||||||
|
const m = text.match(/[。!?!?…]$/);
|
||||||
|
if (m) return text + m[0] + m[0];
|
||||||
|
return text + "。。";
|
||||||
|
}
|
||||||
|
|
||||||
|
// speaker: NPC id;咪咪是文本腐蚀主要载体(优先命中)
|
||||||
|
export function corruptText(text: string, speaker: string): string {
|
||||||
|
const stage = director.stage;
|
||||||
|
if (stage < 2) return text;
|
||||||
|
sentenceCount++;
|
||||||
|
const mimi = speaker === "mimi";
|
||||||
|
let out = text;
|
||||||
|
if (stage === 2) {
|
||||||
|
// 偶发标点重复(每 80 句 1 次;咪咪优先 → 每 16 句)
|
||||||
|
if (sentenceCount % (mimi ? 16 : 80) === 0) out = dupPunct(out);
|
||||||
|
} else if (stage === 3) {
|
||||||
|
// 每 50 句 1 个同音错字(咪咪 → 每 10 句)
|
||||||
|
if (sentenceCount % (mimi ? 10 : 50) === 0) out = applyOneTypo(out);
|
||||||
|
if (sentenceCount % (mimi ? 16 : 80) === 0) out = dupPunct(out);
|
||||||
|
} else if (stage >= 4) {
|
||||||
|
// 错字加密(咪咪每 2 句,其他每 8 句)+ 咪咪句尾标点 ×3
|
||||||
|
if (sentenceCount % (mimi ? 2 : 8) === 0) out = applyOneTypo(applyOneTypo(out));
|
||||||
|
if (mimi) out = triplePunct(out);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
// 全部文案(原文保持干净,腐蚀在渲染层实施)。
|
||||||
|
// PRD 写死的台词在此逐字实现。
|
||||||
|
|
||||||
|
// 阿福每日公告(PRD §5,逐字。D4 与 D1 逐字相同)
|
||||||
|
export const ANNOUNCEMENTS: Record<number, string> = {
|
||||||
|
1: "早上好呀!今天的幸福任务贴在板上啦。祝你开心!",
|
||||||
|
2: "早上好呀!新的一天,新的幸福。祝你开心!",
|
||||||
|
3: "早上好!今天要把幸福装得满满的哦。要开心!",
|
||||||
|
4: "早上好呀!今天的幸福任务贴在板上啦。祝你开心!",
|
||||||
|
5: "早上好。今天是自由日,你可以自由地选择幸福。要开心哦。",
|
||||||
|
6: "明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。",
|
||||||
|
7: "庆典开始了。来吧。",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NPC_NAMES: Record<string, string> = {
|
||||||
|
afu: "阿福", yuanyuan: "圆圆", dabao: "大宝", mimi: "咪咪",
|
||||||
|
fu: "芙医生", gui: "龟爷爷", bird1: "双子鸟", bird2: "双子鸟",
|
||||||
|
villager: "村民的声音", system: "", me: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChoiceKind = "gentle" | "hesitant" | "negative";
|
||||||
|
export interface DialogChoice {
|
||||||
|
label: string; kind: ChoiceKind; reply: string[];
|
||||||
|
}
|
||||||
|
export interface DialogNode {
|
||||||
|
lines: string[];
|
||||||
|
choices?: DialogChoice[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 三选项模板:温柔 +2 幸福 / 迟疑 +2 觉知 / 否定 = 悲伤行为
|
||||||
|
const choice = (g: string, gr: string, h: string, hr: string, n: string, nr: string): DialogChoice[] => [
|
||||||
|
{ label: g, kind: "gentle", reply: [gr] },
|
||||||
|
{ label: h, kind: "hesitant", reply: [hr] },
|
||||||
|
{ label: n, kind: "negative", reply: [nr] },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 每个具名 NPC 的每日闲聊(D4 与 D1 逐字相同——完美的重复)
|
||||||
|
export const NPC_TALK: Record<string, Record<number, DialogNode>> = {
|
||||||
|
afu: {
|
||||||
|
1: { lines: ["新来的孩子就是你呀。这里是蜜糖村,大家都很幸福哦。", "任务贴在公告板上,去看看吧。"] },
|
||||||
|
2: { lines: ["昨天睡得好吗?在这里,每天都会睡得很好。"] },
|
||||||
|
3: { lines: ["幸福要装得满满的才行。满满的。"] },
|
||||||
|
4: { lines: ["新来的孩子就是你呀。这里是蜜糖村,大家都很幸福哦。", "任务贴在公告板上,去看看吧。"] },
|
||||||
|
5: {
|
||||||
|
lines: ["自由日也要有自由日的样子。"],
|
||||||
|
choices: choice(
|
||||||
|
"我很自由呀!", "对嘛!自由地开心,就是最好的自由。",
|
||||||
|
"自由……是什么意思?", "……就是字面的意思呀。别想太多。",
|
||||||
|
"我不想要这种自由。", "……村长听不懂你在说什么呢。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
6: { lines: ["明天所有人都会到场。所有人。"] },
|
||||||
|
7: { lines: ["来吧,站到中间来。"] },
|
||||||
|
},
|
||||||
|
yuanyuan: {
|
||||||
|
1: { lines: ["面包要趁热吃哦。"] },
|
||||||
|
2: { lines: ["今天的面包也是一样的味道。一样,就很好。"] },
|
||||||
|
3: {
|
||||||
|
lines: ["多吃一点,甜甜的就不难过啦。"],
|
||||||
|
choices: choice(
|
||||||
|
"谢谢你,圆圆。", "嘿嘿,不客气。明天也来呀。",
|
||||||
|
"为什么不能难过?", "咦?难过……难过是什么呀?",
|
||||||
|
"我不喜欢甜的。", "……怎么会呢。大家都喜欢的。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
4: { lines: ["面包要趁热吃哦。"] },
|
||||||
|
5: { lines: ["今天是自由日呢。不过面包还是要烤的。"] },
|
||||||
|
6: { lines: ["庆典的蛋糕会很大、很甜。大家都会笑的。"] },
|
||||||
|
7: { lines: ["快去广场呀。"] },
|
||||||
|
},
|
||||||
|
dabao: {
|
||||||
|
1: { lines: ["我是邮差大宝。有信我就送,没信我也在。"] },
|
||||||
|
2: { lines: ["信要送到才算数。"] },
|
||||||
|
3: { lines: ["今天没有新的信。明天也许会有吧。"] },
|
||||||
|
4: { lines: ["我是邮差大宝。有信我就送,没信我也在。"] },
|
||||||
|
5: { lines: ["有几封信……收件人的门牌号不存在。", "大概是我记错了吧。哈哈。"] },
|
||||||
|
6: { lines: ["庆典的邀请函,每家都有。", "连不存在的那几家,也有。"] },
|
||||||
|
7: { lines: ["都到齐了。"] },
|
||||||
|
},
|
||||||
|
mimi: {
|
||||||
|
1: {
|
||||||
|
lines: ["欢迎欢迎!新面孔!要不要来点糖?"],
|
||||||
|
choices: choice(
|
||||||
|
"好呀,谢谢!", "甜甜的最幸福啦!明天也来哦!",
|
||||||
|
"呃……先不用了。", "咦?糖哎?是糖哎?",
|
||||||
|
"我不吃糖。", "……不吃糖的孩子,咪咪还是第一次见。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
2: { lines: ["数字对得上就好。数字总是对得上。"] },
|
||||||
|
3: { lines: ["今天阳光真好,朋友。甜甜的日子,永远这样就好啦。"] },
|
||||||
|
4: {
|
||||||
|
lines: ["欢迎欢迎!新面孔!要不要来点糖?"],
|
||||||
|
choices: choice(
|
||||||
|
"好呀,谢谢!", "甜甜的最幸福啦!明天也来哦!",
|
||||||
|
"呃……先不用了。", "咦?糖哎?是糖哎?",
|
||||||
|
"我不吃糖。", "……不吃糖的孩子,咪咪还是第一次见。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
5: { lines: ["自由日?自由日买点糖最幸福啦。开心,要开心。"] },
|
||||||
|
6: { lines: ["明天大家都会来买微笑……", "不对,是带着微笑来买糖。嘻嘻。"] },
|
||||||
|
7: { lines: ["庆典开始了哦。大家都在笑。"] },
|
||||||
|
},
|
||||||
|
fu: {
|
||||||
|
1: { lines: ["村里有诊所,但你不需要去那里哦。"] },
|
||||||
|
2: { lines: ["村里有诊所,但你不需要去那里哦。"] },
|
||||||
|
3: { lines: ["村里有诊所,但你不需要去那里哦。"] },
|
||||||
|
4: { lines: ["村里有诊所,但你不需要去那里哦。"] },
|
||||||
|
5: { lines: ["睡得好吗?梦到过白色的房间吗?", "……只是随口一问。"] },
|
||||||
|
6: { lines: ["如果哪天你觉得累了——", "往雾的方向走。不要回头。"] },
|
||||||
|
7: { lines: ["去吧。无论你选哪边,我都会在这里。"] },
|
||||||
|
},
|
||||||
|
gui: {
|
||||||
|
1: { lines: ["坐一坐吧,孩子。湖水很平静。"] },
|
||||||
|
2: { lines: ["湖水一直很平静。"] }, // C9 第一段
|
||||||
|
3: { lines: ["老骨头晒晒太阳,就很满足喽。"] },
|
||||||
|
4: { lines: ["面包一直是那个味道。"] }, // C9 第二段
|
||||||
|
5: { lines: ["井里的水,很深,很深。"] },
|
||||||
|
6: { lines: ["孩子。", "今天……是第几个今天?"] }, // C9 第三段
|
||||||
|
7: { lines: ["去吧。别回头,或者……回头。都行。"] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 任务对话(T1/T4/T5/T11 等发起与递送)
|
||||||
|
export const QUEST_LINES = {
|
||||||
|
t1Give: ["刚出炉的蜂蜜面包!", "帮我送给大宝、咪咪和龟爷爷,好吗?"],
|
||||||
|
t1Deliver: {
|
||||||
|
dabao: "哇,是圆圆的蜂蜜面包!谢谢你!",
|
||||||
|
mimi: "甜甜的面包,幸福的味道!",
|
||||||
|
gui: "替我谢谢圆圆。",
|
||||||
|
} as Record<string, string>,
|
||||||
|
t4Give: ["这三封信,帮我送给圆圆、咪咪和芙医生。", "拜托啦。"],
|
||||||
|
t4Deliver: {
|
||||||
|
yuanyuan: "给我的信?谢谢你呀。",
|
||||||
|
mimi: "信?好久没收到过信了……哦不,昨天也收到了吧。",
|
||||||
|
fu: "……谢谢。我收下了。",
|
||||||
|
} as Record<string, string>,
|
||||||
|
t5Ask: ["帮我清点一下货架好吗?就数一数,很快的。"],
|
||||||
|
t5Done: ["都数完啦?谢谢谢谢!"],
|
||||||
|
t11Ask: "今天能帮我揉面吗?当然,你也可以拒绝的……大概。",
|
||||||
|
t11Work: ["(你把面团揉了又揉。面团很软,很甜。)", "谢谢你!揉过的面会更幸福哦。"],
|
||||||
|
t11Refused: "……好的。没关系。真的没关系。",
|
||||||
|
t12Ask: "(公告板上积了一层薄灰。要擦拭吗?)",
|
||||||
|
t12Refused: "(你转身离开了公告板。背后好像有目光。)",
|
||||||
|
wipe1: "(你擦去了一角灰尘。)",
|
||||||
|
wipe2: "(灰下浮现出旧字迹——)",
|
||||||
|
wipe3: "(公告板干净了。崭新得像什么都没发生过。)",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 线索文本(PRD §8)
|
||||||
|
export const CLUE_TEXT: Record<string, string[]> = {
|
||||||
|
C1: ["(床头挂着一本日历。)", "(每一页……都是同一个日期。)"],
|
||||||
|
C2: ["(配方纸的背面有一行手写小字:)", "「按疗程配比。甜度:最大。」"],
|
||||||
|
C3: ["(邮筒旁散落着几封信。)", "(所有邮戳,是同一天。)"],
|
||||||
|
C4: ["(信的署名处闪过一行字——)", "「PT-0▓」", "(0.2 秒后,它变回了「蜜糖村邮政」。)"],
|
||||||
|
C5: ["(同一种商品,重复排了九格。)", "(标签上,全部空白。)"],
|
||||||
|
C6: ["「第 412 届丰收庆典」", "(下面覆盖着更早的字——「第 411 届」「第 410 届」……)"],
|
||||||
|
C7: ["(你俯身向井口。井底传来极轻的——)", "(滴。滴。滴。)", "(像某种仪器的声音。)"],
|
||||||
|
C8: ["(桌上的表格抬头被芙医生的手肘挡住了。)", "(只露出——「…谷情绪疗…」)"],
|
||||||
|
C9: ["(龟爷爷望着湖面,没有再说话。)"],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 梦境过场(侵蚀越高,梦境越"漏")
|
||||||
|
export const DREAMS: Record<number, string[]> = {
|
||||||
|
1: ["这一天很甜。", "梦里也是甜的。"],
|
||||||
|
2: ["梦里有面包的香气。", "还有一点点……消毒水的味道?"],
|
||||||
|
3: ["白色。", "有人在很远的地方念一个编号。"],
|
||||||
|
4: ["同样的一天。", "同样的梦。", "同样的。"],
|
||||||
|
5: ["井很深。", "井底有声音在数数。", "滴。滴。滴。"],
|
||||||
|
6: ["明天大家都会微笑。", "必须微笑。", "必须。"],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 结局文案(PRD §9)
|
||||||
|
export const ENDING_TEXT = {
|
||||||
|
E1: ["从此,每一天都很幸福。", "每一天。"],
|
||||||
|
E2: ["疗程重新开始。"],
|
||||||
|
E4: ["系统检测到疗程无效。"],
|
||||||
|
E3chart: (name: string, dur: string) => [
|
||||||
|
"蜜糖谷情绪疗养中心",
|
||||||
|
`姓名:${name}`,
|
||||||
|
"编号:PT-07",
|
||||||
|
`本次疗程时长:${dur}`,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 劝离(D5 前古井/雾墙)
|
||||||
|
export const DISSUADE = {
|
||||||
|
well: ["那边什么都没有哦。", "回来吧,广场今天很热闹呢。"],
|
||||||
|
fog: ["雾的那边什么都没有哦。", "从来没有人需要去那边。"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MISC = {
|
||||||
|
bedEarly: "(天还亮着。还不困。)",
|
||||||
|
bedSleep: "(睡吧。明天也会是幸福的一天。)",
|
||||||
|
boardDone: "(今天的任务都完成啦。板上贴着一朵小红花。)",
|
||||||
|
breadNeed: "(先去面包房找圆圆取面包吧。)",
|
||||||
|
letterNeed: "(先去村口找大宝取信吧。)",
|
||||||
|
festivalHintFog: "(雾墙的方向,有风声。)",
|
||||||
|
rainNpc: "今天天气真好呀。",
|
||||||
|
wellStare: "(你注视着井口。井口也注视着你。)",
|
||||||
|
fogStare: "(雾没有流动。一丝都没有。)",
|
||||||
|
picnic: ["(你在野餐布上坐下。)", "(湖面很亮,亮得过分。)", "(水里那个你,笑得和你一模一样——", "不,比你慢了一点点。)"],
|
||||||
|
t14Start: "(保持微笑。不要动。60 秒。)",
|
||||||
|
t14Cancel: "(你动了。任务中断了。)",
|
||||||
|
t14Done: "(60 秒到了。大家都很满意。)",
|
||||||
|
festivalGather: "(全村的目光,都转向了你。)",
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// 《崩坏童话:蜜糖村》入口:Phaser 3 + TypeScript + Vite
|
||||||
|
// 逻辑分辨率 320×180,整数倍放大居中,pixelArt,禁用纹理平滑
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { BootScene } from "./scenes/BootScene";
|
||||||
|
import { TitleScene } from "./scenes/TitleScene";
|
||||||
|
import {
|
||||||
|
HouseScene, PlazaScene, BakeryScene, StoreScene, ClinicScene, LakeScene,
|
||||||
|
FieldScene, WellScene, FogwallScene, WhiteCorridorScene, WhiteWardScene,
|
||||||
|
} from "./scenes/WorldScene";
|
||||||
|
import { UIScene } from "./scenes/UIScene";
|
||||||
|
import { DreamScene } from "./scenes/DreamScene";
|
||||||
|
import { EndingScene } from "./scenes/EndingScene";
|
||||||
|
import { setupDebug } from "./core/debug";
|
||||||
|
|
||||||
|
function intZoom(): number {
|
||||||
|
return Math.max(1, Math.floor(Math.min(window.innerWidth / 320, window.innerHeight / 180)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = new Phaser.Game({
|
||||||
|
type: Phaser.WEBGL,
|
||||||
|
parent: "game",
|
||||||
|
width: 320,
|
||||||
|
height: 180,
|
||||||
|
backgroundColor: "#1a1a22",
|
||||||
|
pixelArt: true,
|
||||||
|
roundPixels: true,
|
||||||
|
physics: { default: "arcade", arcade: { debug: false } },
|
||||||
|
scale: { mode: Phaser.Scale.NONE, zoom: intZoom() },
|
||||||
|
scene: [
|
||||||
|
BootScene, TitleScene,
|
||||||
|
HouseScene, PlazaScene, BakeryScene, StoreScene, ClinicScene,
|
||||||
|
LakeScene, FieldScene, WellScene, FogwallScene, WhiteCorridorScene, WhiteWardScene,
|
||||||
|
UIScene, DreamScene, EndingScene,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => game.scale.setZoom(intZoom()));
|
||||||
|
setupDebug(game);
|
||||||
|
|
||||||
|
// 服务验收的只读钩子(与 Debug 面板同源,不影响正常游玩)
|
||||||
|
import { director } from "./core/director";
|
||||||
|
(window as any).__game = game;
|
||||||
|
(window as any).__director = director;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// 加载场景:全部本地自制素材 + 动画注册
|
||||||
|
import Phaser from "phaser";
|
||||||
|
|
||||||
|
const MAPS = ["house", "plaza", "bakery", "store", "clinic", "lake", "field", "well", "fogwall", "white_corridor", "white_ward"];
|
||||||
|
const NPCS = ["afu", "yuanyuan", "dabao", "mimi", "fu", "gui"];
|
||||||
|
|
||||||
|
export class BootScene extends Phaser.Scene {
|
||||||
|
constructor() { super("boot"); }
|
||||||
|
|
||||||
|
preload() {
|
||||||
|
const a = "assets/";
|
||||||
|
this.load.spritesheet("tiles", a + "tiles.png", { frameWidth: 16, frameHeight: 16 });
|
||||||
|
this.load.spritesheet("player", a + "player.png", { frameWidth: 16, frameHeight: 24 });
|
||||||
|
this.load.spritesheet("bird", a + "bird.png", { frameWidth: 16, frameHeight: 16 });
|
||||||
|
this.load.spritesheet("sunflower", a + "sunflower.png", { frameWidth: 24, frameHeight: 24 });
|
||||||
|
this.load.image("portrait_smile", a + "portrait_smile.png");
|
||||||
|
this.load.image("portrait_real", a + "portrait_real.png");
|
||||||
|
this.load.image("sparrow", a + "sparrow.png");
|
||||||
|
for (const n of NPCS) this.load.spritesheet("npc_" + n, a + "npc_" + n + ".png", { frameWidth: 16, frameHeight: 24 });
|
||||||
|
for (let i = 1; i <= 5; i++) this.load.spritesheet("extra" + i, a + "extra" + i + ".png", { frameWidth: 16, frameHeight: 16 });
|
||||||
|
for (const m of MAPS) this.load.tilemapTiledJSON("map_" + m, a + "maps/" + m + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
// 雨滴纹理(程序生成)
|
||||||
|
const g = this.make.graphics({ x: 0, y: 0 }, false);
|
||||||
|
g.fillStyle(0xb8d8f0, 1).fillRect(0, 0, 1, 4);
|
||||||
|
g.generateTexture("rainpx", 1, 4);
|
||||||
|
g.destroy();
|
||||||
|
|
||||||
|
// 主角四向行走(每朝向第 1 帧兼待机)
|
||||||
|
const mk = (key: string, start: number) =>
|
||||||
|
this.anims.create({ key, frames: this.anims.generateFrameNumbers("player", { start, end: start + 3 }), frameRate: 8, repeat: -1 });
|
||||||
|
mk("walk-down", 0); mk("walk-up", 4); mk("walk-left", 8); mk("walk-right", 12);
|
||||||
|
|
||||||
|
// 双子鸟张嘴
|
||||||
|
this.anims.create({ key: "bird-sing", frames: this.anims.generateFrameNumbers("bird", { start: 0, end: 1 }), frameRate: 3, repeat: -1 });
|
||||||
|
// 群演循环
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
this.anims.create({ key: `extra${i}-walk`, frames: this.anims.generateFrameNumbers("extra" + i, { start: 0, end: 1 }), frameRate: 3, repeat: -1 });
|
||||||
|
}
|
||||||
|
this.scene.start("title");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// 梦境过场:侵蚀越高,梦境越"漏";Day3 夜起闪切白色病房 1 秒
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { director } from "../core/director";
|
||||||
|
import { audio } from "../core/audio";
|
||||||
|
import { DREAMS } from "../data/story";
|
||||||
|
|
||||||
|
const FONT = { fontFamily: '"PingFang SC","Hiragino Sans GB",sans-serif', fontSize: "11px", color: "#c8c8d0" };
|
||||||
|
|
||||||
|
export class DreamScene extends Phaser.Scene {
|
||||||
|
constructor() { super("dream"); }
|
||||||
|
|
||||||
|
create() {
|
||||||
|
this.cameras.main.setBackgroundColor("#000000");
|
||||||
|
const day = director.day;
|
||||||
|
const lines = [...(DREAMS[day] ?? ["……"])];
|
||||||
|
// 觉知越高,梦越漏
|
||||||
|
if (director.awareness >= 30) lines.push("(有人在很近的地方换点滴。)");
|
||||||
|
if (director.awareness >= 60) lines.push("「PT-07,睡眠状态良好。」");
|
||||||
|
|
||||||
|
const t = this.add.text(160, 84, "", { ...FONT, align: "center", wordWrap: { width: 260 } }).setOrigin(0.5);
|
||||||
|
let i = 0;
|
||||||
|
const showNext = () => {
|
||||||
|
if (i === Math.min(1, lines.length - 1) && day >= 3) this.wardFlash();
|
||||||
|
if (i >= lines.length) { this.finish(); return; }
|
||||||
|
t.setText(lines[i]).setAlpha(0);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: t, alpha: 1, duration: 700, hold: 1200, yoyo: true,
|
||||||
|
onComplete: () => { i++; showNext(); },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
i = 0;
|
||||||
|
this.time.delayedCall(500, showNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 闪切白色病房 1 秒(用真实病房地图渲染)
|
||||||
|
private wardFlash() {
|
||||||
|
this.time.delayedCall(950, () => {
|
||||||
|
const map = this.make.tilemap({ key: "map_white_ward" });
|
||||||
|
const ts = map.addTilesetImage("tiles", "tiles")!;
|
||||||
|
const g = map.createLayer("ground", ts)!.setPosition(80, 56);
|
||||||
|
const o = map.createLayer("obstacles", ts)!.setPosition(80, 56);
|
||||||
|
audio.sfxEcg();
|
||||||
|
this.time.delayedCall(1000, () => { g.destroy(); o.destroy(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private finish() {
|
||||||
|
director.startDay(director.day + 1);
|
||||||
|
director.pendingSpawn = "bed";
|
||||||
|
this.scene.start("house");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// 结局演出(PRD §9,封闭 4 个)。落幕后停在结束画面,提供「回到标题」(重开无继承)
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { director } from "../core/director";
|
||||||
|
import { audio } from "../core/audio";
|
||||||
|
import { ENDING_TEXT } from "../data/story";
|
||||||
|
|
||||||
|
const FONT = { fontFamily: '"PingFang SC","Hiragino Sans GB",sans-serif', fontSize: "12px", color: "#e8e8f0" };
|
||||||
|
|
||||||
|
export class EndingScene extends Phaser.Scene {
|
||||||
|
private colorFx?: Phaser.FX.ColorMatrix;
|
||||||
|
|
||||||
|
constructor() { super("ending"); }
|
||||||
|
|
||||||
|
create() {
|
||||||
|
const code = (this.registry.get("ending") as string) ?? "E1";
|
||||||
|
this.cameras.main.setBackgroundColor("#000000");
|
||||||
|
this.colorFx = this.cameras.main.postFX.addColorMatrix();
|
||||||
|
if (code === "E1") this.e1();
|
||||||
|
else if (code === "E2") this.e2();
|
||||||
|
else if (code === "E3") this.e3();
|
||||||
|
else this.e4();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawMap(key: string): Phaser.Tilemaps.Tilemap {
|
||||||
|
const map = this.make.tilemap({ key: "map_" + key });
|
||||||
|
const ts = map.addTilesetImage("tiles", "tiles")!;
|
||||||
|
map.createLayer("ground", ts);
|
||||||
|
map.createLayer("obstacles", ts);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private subtitle(lines: string[], startDelay: number, gap = 2600, cb?: () => void) {
|
||||||
|
const t = this.add.text(160, 158, "", { ...FONT, align: "center" }).setOrigin(0.5).setDepth(50).setStroke("#000000", 4).setScrollFactor(0);
|
||||||
|
lines.forEach((l, i) => {
|
||||||
|
this.time.delayedCall(startDelay + i * gap, () => {
|
||||||
|
t.setText(l).setAlpha(0);
|
||||||
|
this.tweens.add({ targets: t, alpha: 1, duration: 700 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (cb) this.time.delayedCall(startDelay + lines.length * gap, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private backButton(delay: number) {
|
||||||
|
this.time.delayedCall(delay, () => {
|
||||||
|
const b = this.add.text(160, 172, "[ 回到标题 ]", { ...FONT, fontSize: "10px", color: "#ffd94a" })
|
||||||
|
.setOrigin(0.5).setDepth(60).setStroke("#000000", 4).setScrollFactor(0).setInteractive({ useHandCursor: true });
|
||||||
|
b.on("pointerdown", () => window.location.reload());
|
||||||
|
this.input.keyboard!.once("keydown-E", () => window.location.reload());
|
||||||
|
this.input.keyboard!.once("keydown-ENTER", () => window.location.reload());
|
||||||
|
this.tweens.add({ targets: b, alpha: 0.5, duration: 700, yoyo: true, repeat: -1 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// E1 糖衣:庆典圆满,合影定格,饱和度推到 1.5
|
||||||
|
private e1() {
|
||||||
|
this.drawMap("plaza");
|
||||||
|
this.cameras.main.centerOn(240, 190);
|
||||||
|
const lineup: [string, number, number][] = [
|
||||||
|
["npc_afu", 208, 196], ["npc_yuanyuan", 228, 200], ["npc_dabao", 248, 196],
|
||||||
|
["npc_mimi", 268, 200], ["npc_fu", 218, 216], ["npc_gui", 258, 216],
|
||||||
|
];
|
||||||
|
for (const [tex, x, y] of lineup) this.add.sprite(x, y, tex, 0).setOrigin(0.5, 1);
|
||||||
|
this.add.sprite(238, 214, "player", 0).setOrigin(0.5, 1);
|
||||||
|
for (let i = 0; i < 6; i++) this.add.image(200 + i * 16, 168, "tiles", 58);
|
||||||
|
// 快门定格 + 饱和度缓慢推到 1.5
|
||||||
|
this.time.delayedCall(1200, () => this.cameras.main.flash(300, 255, 255, 255));
|
||||||
|
this.tweens.addCounter({
|
||||||
|
from: 0, to: 0.5, duration: 7000, delay: 1500,
|
||||||
|
onUpdate: (tw) => { this.colorFx!.reset(); this.colorFx!.saturate(tw.getValue()!); },
|
||||||
|
});
|
||||||
|
this.subtitle(ENDING_TEXT.E1, 2600, 3000);
|
||||||
|
this.backButton(9000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// E2 回收:负片 2 秒 + 心电一响 → 回到 Day1 清晨的床
|
||||||
|
private e2() {
|
||||||
|
audio.stopBgm();
|
||||||
|
const g = this.drawMap("plaza");
|
||||||
|
this.cameras.main.centerOn(240, 190);
|
||||||
|
this.colorFx!.negative();
|
||||||
|
audio.sfxEcg();
|
||||||
|
this.time.delayedCall(2000, () => {
|
||||||
|
this.children.removeAll();
|
||||||
|
this.colorFx!.reset();
|
||||||
|
this.drawMap("house");
|
||||||
|
this.cameras.main.centerOn(96, 64);
|
||||||
|
this.add.sprite(24, 64, "player", 0).setOrigin(0.5, 1).setAngle(90); // 躺在床上
|
||||||
|
this.subtitle(ENDING_TEXT.E2, 1500);
|
||||||
|
this.backButton(4500);
|
||||||
|
});
|
||||||
|
void g;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E3 苏醒:病历特写 → 微笑消失,换上真实的脸;窗外一只灰色的、真的麻雀
|
||||||
|
private e3() {
|
||||||
|
audio.stopBgm();
|
||||||
|
this.drawMap("white_ward");
|
||||||
|
this.cameras.main.centerOn(80, 64);
|
||||||
|
this.colorFx!.saturate(-0.4);
|
||||||
|
// 病历特写
|
||||||
|
const panel = this.add.graphics().setDepth(20).setScrollFactor(0);
|
||||||
|
panel.fillStyle(0xf2f2f4, 1).fillRect(60, 24, 200, 108);
|
||||||
|
panel.lineStyle(1, 0x9a9aa4, 1).strokeRect(60, 24, 200, 108);
|
||||||
|
const chartLines = ENDING_TEXT.E3chart(director.playerName, director.playTimeText);
|
||||||
|
const texts: Phaser.GameObjects.Text[] = [];
|
||||||
|
chartLines.forEach((l, i) => {
|
||||||
|
const t = this.add.text(70, 36 + i * 22, "", {
|
||||||
|
fontFamily: '"PingFang SC",sans-serif', fontSize: i === 0 ? "13px" : "10px",
|
||||||
|
color: i === 0 ? "#2a2a34" : "#4a4a56",
|
||||||
|
}).setDepth(21).setScrollFactor(0);
|
||||||
|
texts.push(t);
|
||||||
|
this.time.delayedCall(900 + i * 1500, () => t.setText(l));
|
||||||
|
});
|
||||||
|
// 最后一帧:微笑消失
|
||||||
|
this.time.delayedCall(900 + chartLines.length * 1500 + 1500, () => {
|
||||||
|
panel.destroy(); texts.forEach(t => t.destroy());
|
||||||
|
this.cameras.main.setBackgroundColor("#dcdce0");
|
||||||
|
const smile = this.add.image(160, 80, "portrait_smile").setScale(3).setDepth(30).setScrollFactor(0);
|
||||||
|
this.time.delayedCall(2200, () => {
|
||||||
|
smile.setTexture("portrait_real"); // 全流程唯一一次头像变化
|
||||||
|
// 窗外,一只灰色的、真的麻雀
|
||||||
|
const sp = this.add.image(240, 46, "sparrow").setDepth(31).setAlpha(0).setScrollFactor(0);
|
||||||
|
this.tweens.add({ targets: sp, alpha: 1, duration: 1200, delay: 800 });
|
||||||
|
this.tweens.add({ targets: sp, y: 44, duration: 900, delay: 2000, yoyo: true, repeat: 2 });
|
||||||
|
this.subtitle(["…………", "早上好。"], 1600, 2600);
|
||||||
|
this.backButton(7000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// E4 静默:庆典无人出席,彩带在没有风的空气里飘
|
||||||
|
private e4() {
|
||||||
|
audio.stopBgm();
|
||||||
|
this.drawMap("plaza");
|
||||||
|
this.cameras.main.centerOn(240, 190);
|
||||||
|
this.colorFx!.saturate(-0.15);
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const img = this.add.image(200 + i * 16, 168, "tiles", 58);
|
||||||
|
this.tweens.add({ targets: img, y: 166, duration: 1700 + i * 140, yoyo: true, repeat: -1 });
|
||||||
|
}
|
||||||
|
this.subtitle(ENDING_TEXT.E4, 2600, 2600, () => {
|
||||||
|
const black = this.add.rectangle(160, 90, 320, 180, 0x000000, 0).setDepth(40).setScrollFactor(0);
|
||||||
|
this.tweens.add({ targets: black, fillAlpha: 1, duration: 2200 });
|
||||||
|
});
|
||||||
|
this.backButton(8200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// 标题画面:输入名字(留空默认"小满")后进入 Day 1 小屋
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { director } from "../core/director";
|
||||||
|
import { audio } from "../core/audio";
|
||||||
|
|
||||||
|
export class TitleScene extends Phaser.Scene {
|
||||||
|
private starting = false;
|
||||||
|
|
||||||
|
constructor() { super("title"); }
|
||||||
|
|
||||||
|
create() {
|
||||||
|
this.starting = false;
|
||||||
|
this.cameras.main.setBackgroundColor("#8ecae6");
|
||||||
|
// 糖果色村庄剪影
|
||||||
|
const g = this.add.graphics();
|
||||||
|
g.fillStyle(0x8fd05a).fillRect(0, 120, 320, 60);
|
||||||
|
g.fillStyle(0xfff1d0).fillRect(50, 92, 40, 28).fillRect(230, 96, 36, 24);
|
||||||
|
g.fillStyle(0xf4924a).fillTriangle(45, 92, 95, 92, 70, 74).fillTriangle(226, 96, 270, 96, 248, 80);
|
||||||
|
g.fillStyle(0x5da53f).fillRect(140, 84, 36, 36);
|
||||||
|
g.fillStyle(0x8a5a34).fillRect(154, 118, 8, 12);
|
||||||
|
this.add.sprite(160, 40, "sunflower", 12).setScale(2);
|
||||||
|
|
||||||
|
this.add.text(160, 66, "崩坏童话:蜜糖村", {
|
||||||
|
fontFamily: '"PingFang SC","Hiragino Sans GB",sans-serif', fontSize: "20px", color: "#fff8e8",
|
||||||
|
}).setOrigin(0.5).setStroke("#a05c28", 5);
|
||||||
|
this.add.text(160, 132, "输入你的名字,回车开始", {
|
||||||
|
fontFamily: '"PingFang SC",sans-serif', fontSize: "9px", color: "#4a3828",
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.add.text(160, 168, "移动 WASD/方向键 · 交互 E", {
|
||||||
|
fontFamily: '"PingFang SC",sans-serif', fontSize: "8px", color: "#4a3828",
|
||||||
|
}).setOrigin(0.5).setAlpha(0.8);
|
||||||
|
|
||||||
|
const entry = document.getElementById("name-entry")!;
|
||||||
|
const input = document.getElementById("name-input") as HTMLInputElement;
|
||||||
|
entry.style.display = "block";
|
||||||
|
input.value = "";
|
||||||
|
setTimeout(() => input.focus(), 50);
|
||||||
|
|
||||||
|
const begin = () => {
|
||||||
|
if (this.starting) return;
|
||||||
|
this.starting = true;
|
||||||
|
entry.style.display = "none";
|
||||||
|
input.onkeydown = null;
|
||||||
|
audio.start(); // 用户手势后启动 Web Audio
|
||||||
|
director.reset(input.value.trim());
|
||||||
|
director.pendingSpawn = "bed";
|
||||||
|
if (!this.scene.isActive("ui")) this.scene.launch("ui");
|
||||||
|
this.scene.start("house");
|
||||||
|
};
|
||||||
|
input.onkeydown = (ev) => {
|
||||||
|
if (ev.key === "Enter") begin();
|
||||||
|
ev.stopPropagation();
|
||||||
|
};
|
||||||
|
this.input.keyboard!.once("keydown-ENTER", begin);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
// 常驻 UI 场景:对话框(说话者名+头像+文本+三选项)、向日葵表盘、提示、横幅、过渡
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { director } from "../core/director";
|
||||||
|
import { audio } from "../core/audio";
|
||||||
|
import { corruptText } from "../core/textcorrupt";
|
||||||
|
import { NPC_NAMES } from "../data/story";
|
||||||
|
|
||||||
|
export interface UiChoice { label: string; cb?: () => void; }
|
||||||
|
export interface SayOpts {
|
||||||
|
choices?: UiChoice[];
|
||||||
|
onDone?: () => void;
|
||||||
|
clean?: boolean; // 免腐蚀(公告/系统文本)
|
||||||
|
speakerName?: string; // 覆盖显示名
|
||||||
|
}
|
||||||
|
|
||||||
|
const FONT = { fontFamily: '"PingFang SC","Hiragino Sans GB",sans-serif', fontSize: "9px", color: "#4a3828" };
|
||||||
|
const FONT_W = { ...FONT, color: "#fff8e8" };
|
||||||
|
|
||||||
|
export class UIScene extends Phaser.Scene {
|
||||||
|
private sunflower!: Phaser.GameObjects.Sprite;
|
||||||
|
private boxG!: Phaser.GameObjects.Graphics;
|
||||||
|
private nameText!: Phaser.GameObjects.Text;
|
||||||
|
private lineText!: Phaser.GameObjects.Text;
|
||||||
|
private portrait!: Phaser.GameObjects.Image;
|
||||||
|
private moreHint!: Phaser.GameObjects.Text;
|
||||||
|
private choiceTexts: Phaser.GameObjects.Text[] = [];
|
||||||
|
private promptText!: Phaser.GameObjects.Text;
|
||||||
|
private bannerText!: Phaser.GameObjects.Text;
|
||||||
|
private toastText!: Phaser.GameObjects.Text;
|
||||||
|
private duskRect!: Phaser.GameObjects.Rectangle;
|
||||||
|
private fadeRect!: Phaser.GameObjects.Rectangle;
|
||||||
|
private glitchTxt!: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
dialogOpen = false;
|
||||||
|
private lines: { speaker: string; text: string }[] = [];
|
||||||
|
private lineIdx = 0;
|
||||||
|
private typing = false;
|
||||||
|
private typeEvent?: Phaser.Time.TimerEvent;
|
||||||
|
private curOpts: SayOpts = {};
|
||||||
|
private choiceMode = false;
|
||||||
|
private choiceIdx = 0;
|
||||||
|
private keyE!: Phaser.Input.Keyboard.Key;
|
||||||
|
private keyEnter!: Phaser.Input.Keyboard.Key;
|
||||||
|
private keyUp!: Phaser.Input.Keyboard.Key;
|
||||||
|
private keyDown!: Phaser.Input.Keyboard.Key;
|
||||||
|
private keyW!: Phaser.Input.Keyboard.Key;
|
||||||
|
private keyS!: Phaser.Input.Keyboard.Key;
|
||||||
|
|
||||||
|
constructor() { super("ui"); }
|
||||||
|
|
||||||
|
create() {
|
||||||
|
const kb = this.input.keyboard!;
|
||||||
|
this.keyE = kb.addKey("E"); this.keyEnter = kb.addKey("ENTER");
|
||||||
|
this.keyUp = kb.addKey("UP"); this.keyDown = kb.addKey("DOWN");
|
||||||
|
this.keyW = kb.addKey("W"); this.keyS = kb.addKey("S");
|
||||||
|
|
||||||
|
// 黄昏色调
|
||||||
|
this.duskRect = this.add.rectangle(160, 90, 320, 180, 0xd06a2a, 0).setDepth(5);
|
||||||
|
// 向日葵表盘(不显示任何数字)
|
||||||
|
this.sunflower = this.add.sprite(17, 17, "sunflower", 3).setDepth(10);
|
||||||
|
// 对话框
|
||||||
|
this.boxG = this.add.graphics().setDepth(20).setVisible(false);
|
||||||
|
this.portrait = this.add.image(24, 155, "portrait_smile").setDepth(21).setScale(1.4).setVisible(false);
|
||||||
|
this.nameText = this.add.text(44, 136, "", { ...FONT, fontSize: "9px", color: "#a05c28" }).setDepth(21).setVisible(false);
|
||||||
|
this.lineText = this.add.text(44, 148, "", { ...FONT, wordWrap: { width: 250 } }).setDepth(21).setVisible(false).setLineSpacing(3);
|
||||||
|
this.moreHint = this.add.text(306, 168, "▼", { ...FONT, fontSize: "8px" }).setDepth(21).setVisible(false);
|
||||||
|
// 提示 / 横幅 / 小告示
|
||||||
|
this.promptText = this.add.text(160, 122, "", { ...FONT_W, fontSize: "9px" }).setOrigin(0.5).setDepth(15)
|
||||||
|
.setStroke("#4a3828", 3).setVisible(false);
|
||||||
|
this.bannerText = this.add.text(160, 70, "", { ...FONT_W, fontSize: "16px" }).setOrigin(0.5).setDepth(30)
|
||||||
|
.setStroke("#4a3828", 4).setAlpha(0);
|
||||||
|
this.toastText = this.add.text(316, 6, "", { ...FONT_W, fontSize: "8px", align: "right" }).setOrigin(1, 0).setDepth(30)
|
||||||
|
.setStroke("#4a3828", 3).setAlpha(0);
|
||||||
|
this.glitchTxt = this.add.text(160, 100, "", { ...FONT, fontSize: "12px", color: "#e84860" }).setOrigin(0.5).setDepth(40)
|
||||||
|
.setStroke("#1a1a22", 3).setVisible(false);
|
||||||
|
// 全屏渐变遮罩
|
||||||
|
this.fadeRect = this.add.rectangle(160, 90, 320, 180, 0x000000, 0).setDepth(50);
|
||||||
|
|
||||||
|
director.on("happiness", this.updateSunflower, this);
|
||||||
|
director.on("stage", this.updateSunflower, this);
|
||||||
|
director.on("quest", this.onQuest, this);
|
||||||
|
director.on("dusk", this.onDusk, this);
|
||||||
|
director.on("day", this.onDay, this);
|
||||||
|
this.updateSunflower();
|
||||||
|
|
||||||
|
this.events.on("shutdown", () => {
|
||||||
|
director.off("happiness", this.updateSunflower, this);
|
||||||
|
director.off("stage", this.updateSunflower, this);
|
||||||
|
director.off("quest", this.onQuest, this);
|
||||||
|
director.off("dusk", this.onDusk, this);
|
||||||
|
director.off("day", this.onDay, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 向日葵表盘(§6.1 分段点亮,满格花心黑籽) ----
|
||||||
|
private updateSunflower() {
|
||||||
|
const h = director.happiness;
|
||||||
|
let petals: number;
|
||||||
|
if (h <= 20) petals = Math.floor(h / 7);
|
||||||
|
else if (h <= 50) petals = 3 + Math.floor(((h - 21) * 3) / 29);
|
||||||
|
else if (h <= 80) petals = 7 + Math.floor(((h - 51) * 2) / 29);
|
||||||
|
else if (h <= 99) petals = 10 + Math.floor(((h - 81) * 2) / 18);
|
||||||
|
else petals = 12;
|
||||||
|
const frame = h >= 100 ? 13 : petals;
|
||||||
|
this.sunflower.setFrame(frame);
|
||||||
|
// S3:花瓣过亮
|
||||||
|
if (director.stage >= 3) this.sunflower.setTintFill();
|
||||||
|
this.sunflower.clearTint();
|
||||||
|
if (director.stage >= 3) this.sunflower.setTint(0xffffd8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onQuest(id: string) {
|
||||||
|
if (director.questStatus[id] === "done") {
|
||||||
|
audio.sfxDing();
|
||||||
|
this.toast("任务完成");
|
||||||
|
} else {
|
||||||
|
this.toast("(拒绝被记录了)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private onDusk() {
|
||||||
|
audio.sfxBell();
|
||||||
|
this.tweens.add({ targets: this.duskRect, fillAlpha: 0.22, duration: 2500 });
|
||||||
|
this.toast("黄昏了。回屋睡觉吧。");
|
||||||
|
}
|
||||||
|
private onDay(d: number) {
|
||||||
|
this.duskRect.setFillStyle(0xd06a2a, 0);
|
||||||
|
this.banner(`第 ${d} 日`);
|
||||||
|
}
|
||||||
|
|
||||||
|
banner(text: string) {
|
||||||
|
this.bannerText.setText(text).setAlpha(0);
|
||||||
|
this.tweens.add({ targets: this.bannerText, alpha: 1, duration: 600, hold: 1400, yoyo: true });
|
||||||
|
}
|
||||||
|
toast(text: string) {
|
||||||
|
this.toastText.setText(text).setAlpha(1);
|
||||||
|
this.tweens.killTweensOf(this.toastText);
|
||||||
|
this.tweens.add({ targets: this.toastText, alpha: 0, delay: 2200, duration: 600 });
|
||||||
|
}
|
||||||
|
glitchText(text: string, ms: number) {
|
||||||
|
this.glitchTxt.setText(text).setVisible(true);
|
||||||
|
this.time.delayedCall(ms, () => this.glitchTxt.setVisible(false));
|
||||||
|
}
|
||||||
|
prompt(text: string | null) {
|
||||||
|
if (text) this.promptText.setText(text).setVisible(true);
|
||||||
|
else this.promptText.setVisible(false);
|
||||||
|
}
|
||||||
|
fadeOut(ms: number, cb?: () => void) {
|
||||||
|
this.tweens.add({ targets: this.fadeRect, fillAlpha: 1, duration: ms, onComplete: () => cb?.() });
|
||||||
|
}
|
||||||
|
fadeIn(ms: number) {
|
||||||
|
this.tweens.add({ targets: this.fadeRect, fillAlpha: 0, duration: ms });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 对话 ----
|
||||||
|
say(speaker: string, rawLines: string[], opts: SayOpts = {}) {
|
||||||
|
this.lines = rawLines.map(t => ({ speaker, text: t }));
|
||||||
|
this.lineIdx = 0;
|
||||||
|
this.curOpts = opts;
|
||||||
|
this.dialogOpen = true;
|
||||||
|
this.choiceMode = false;
|
||||||
|
this.drawBox();
|
||||||
|
this.showLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawBox() {
|
||||||
|
this.boxG.clear().setVisible(true);
|
||||||
|
this.boxG.fillStyle(0xfff4da, 0.96).fillRect(4, 130, 312, 46);
|
||||||
|
this.boxG.lineStyle(2, 0xa05c28, 1).strokeRect(4, 130, 312, 46);
|
||||||
|
this.portrait.setVisible(true);
|
||||||
|
this.nameText.setVisible(true);
|
||||||
|
this.lineText.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private portraitFor(speaker: string) {
|
||||||
|
if (speaker === "me") { this.portrait.setTexture("portrait_smile").setScale(1.4); return; }
|
||||||
|
if (speaker === "meReal") { this.portrait.setTexture("portrait_real").setScale(1.4); return; }
|
||||||
|
const base = speaker.startsWith("bird") ? "bird" : speaker;
|
||||||
|
if (this.textures.exists("npc_" + base)) {
|
||||||
|
// 用 NPC 立绘当前表情档作为头像
|
||||||
|
const expr = director.npcExpr(base);
|
||||||
|
const frame = expr === "flat" ? 1 : 0;
|
||||||
|
this.portrait.setTexture("npc_" + base, frame).setScale(1.5);
|
||||||
|
} else if (base === "bird") {
|
||||||
|
this.portrait.setTexture("bird", 0).setScale(1.8);
|
||||||
|
} else {
|
||||||
|
this.portrait.setTexture("portrait_smile").setScale(1.4);
|
||||||
|
if (speaker !== "me") this.portrait.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showLine() {
|
||||||
|
const entry = this.lines[this.lineIdx];
|
||||||
|
const disp = this.curOpts.clean ? entry.text : corruptText(entry.text, entry.speaker);
|
||||||
|
const name = this.curOpts.speakerName ?? NPC_NAMES[entry.speaker] ?? entry.speaker;
|
||||||
|
this.nameText.setText(name);
|
||||||
|
this.portraitFor(entry.speaker);
|
||||||
|
this.lineText.setText("");
|
||||||
|
this.moreHint.setVisible(false);
|
||||||
|
this.typing = true;
|
||||||
|
let i = 0;
|
||||||
|
this.typeEvent?.remove();
|
||||||
|
this.typeEvent = this.time.addEvent({
|
||||||
|
delay: 28, repeat: disp.length - 1,
|
||||||
|
callback: () => {
|
||||||
|
i++;
|
||||||
|
this.lineText.setText(disp.slice(0, i));
|
||||||
|
if (i % 3 === 1 && !entry.text.startsWith("(")) audio.sfxTalk();
|
||||||
|
if (i >= disp.length) { this.typing = false; this.moreHint.setVisible(true); }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(this as any)._fullLine = disp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private advance() {
|
||||||
|
if (this.typing) {
|
||||||
|
this.typeEvent?.remove();
|
||||||
|
this.typing = false;
|
||||||
|
this.lineText.setText((this as any)._fullLine);
|
||||||
|
this.moreHint.setVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.lineIdx < this.lines.length - 1) {
|
||||||
|
this.lineIdx++;
|
||||||
|
this.showLine();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.curOpts.choices && !this.choiceMode) {
|
||||||
|
this.enterChoices();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enterChoices() {
|
||||||
|
this.choiceMode = true;
|
||||||
|
this.choiceIdx = 0;
|
||||||
|
this.portraitFor("me"); // 选择时显示主角微笑头像(永远同一帧)
|
||||||
|
this.nameText.setText(director.playerName);
|
||||||
|
const cs = this.curOpts.choices!;
|
||||||
|
cs.forEach((c, i) => {
|
||||||
|
const t = this.add.text(60, 92 + i * 12, c.label, { ...FONT, backgroundColor: "#fff4da", padding: { x: 4, y: 2 } })
|
||||||
|
.setDepth(25);
|
||||||
|
this.choiceTexts.push(t);
|
||||||
|
});
|
||||||
|
this.renderChoiceSel();
|
||||||
|
}
|
||||||
|
private renderChoiceSel() {
|
||||||
|
this.choiceTexts.forEach((t, i) => {
|
||||||
|
t.setText((i === this.choiceIdx ? "▶ " : " ") + this.curOpts.choices![i].label);
|
||||||
|
t.setColor(i === this.choiceIdx ? "#c04818" : "#4a3828");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private confirmChoice() {
|
||||||
|
const c = this.curOpts.choices![this.choiceIdx];
|
||||||
|
this.choiceTexts.forEach(t => t.destroy());
|
||||||
|
this.choiceTexts = [];
|
||||||
|
const done = this.curOpts.onDone;
|
||||||
|
this.curOpts = {};
|
||||||
|
this.dialogOpen = false;
|
||||||
|
this.hideBox();
|
||||||
|
c.cb?.();
|
||||||
|
// 若选项回调未开启新对话,则触发 onDone
|
||||||
|
if (!this.dialogOpen) done?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景切换时强制关闭残留对话
|
||||||
|
forceClose() {
|
||||||
|
if (!this.dialogOpen && !this.choiceTexts.length) return;
|
||||||
|
this.typeEvent?.remove();
|
||||||
|
this.typing = false;
|
||||||
|
this.choiceTexts.forEach(t => t.destroy());
|
||||||
|
this.choiceTexts = [];
|
||||||
|
this.choiceMode = false;
|
||||||
|
this.curOpts = {};
|
||||||
|
this.dialogOpen = false;
|
||||||
|
this.hideBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeDialog() {
|
||||||
|
const done = this.curOpts.onDone;
|
||||||
|
this.curOpts = {};
|
||||||
|
this.dialogOpen = false;
|
||||||
|
this.hideBox();
|
||||||
|
done?.();
|
||||||
|
}
|
||||||
|
private hideBox() {
|
||||||
|
this.boxG.setVisible(false);
|
||||||
|
this.portrait.setVisible(false);
|
||||||
|
this.nameText.setVisible(false);
|
||||||
|
this.lineText.setVisible(false);
|
||||||
|
this.moreHint.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(_t: number, dt: number) {
|
||||||
|
director.tick(_t, dt);
|
||||||
|
if (this.dialogOpen) {
|
||||||
|
const pressE = Phaser.Input.Keyboard.JustDown(this.keyE) || Phaser.Input.Keyboard.JustDown(this.keyEnter);
|
||||||
|
if (this.choiceMode) {
|
||||||
|
if (Phaser.Input.Keyboard.JustDown(this.keyUp) || Phaser.Input.Keyboard.JustDown(this.keyW)) {
|
||||||
|
this.choiceIdx = (this.choiceIdx + this.curOpts.choices!.length - 1) % this.curOpts.choices!.length;
|
||||||
|
this.renderChoiceSel();
|
||||||
|
}
|
||||||
|
if (Phaser.Input.Keyboard.JustDown(this.keyDown) || Phaser.Input.Keyboard.JustDown(this.keyS)) {
|
||||||
|
this.choiceIdx = (this.choiceIdx + 1) % this.curOpts.choices!.length;
|
||||||
|
this.renderChoiceSel();
|
||||||
|
}
|
||||||
|
if (pressE) this.confirmChoice();
|
||||||
|
} else if (pressE) {
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,941 @@
|
|||||||
|
// 地图场景基类:每张 Tiled 地图一个 Scene(PRD §12)
|
||||||
|
// 移动 96px/s、相机钳制、交互(≤24px 出「按 E」)、任务/线索/悲伤行为/侵蚀表现在此落地
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { director, NPC_OFFSET } from "../core/director";
|
||||||
|
import { audio } from "../core/audio";
|
||||||
|
import { UIScene, UiChoice } from "./UIScene";
|
||||||
|
import {
|
||||||
|
ANNOUNCEMENTS, NPC_TALK, QUEST_LINES, CLUE_TEXT, DISSUADE, MISC, DialogChoice,
|
||||||
|
} from "../data/story";
|
||||||
|
|
||||||
|
const SPEED = 96;
|
||||||
|
const INTERACT_DIST = 24;
|
||||||
|
const NAMED = ["afu", "yuanyuan", "dabao", "mimi", "fu", "gui"];
|
||||||
|
const OUTDOOR = ["plaza", "lake", "field", "well", "fogwall"];
|
||||||
|
|
||||||
|
interface Poi { name: string; x: number; y: number; clue?: string; }
|
||||||
|
interface NpcSprite {
|
||||||
|
id: string; sprite: Phaser.GameObjects.Sprite; x0: number; y0: number;
|
||||||
|
patrol?: number; dir: number; nextBlink: number; offUntil: number; blinkUntil: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorldScene extends Phaser.Scene {
|
||||||
|
mapKey: string;
|
||||||
|
protected map!: Phaser.Tilemaps.Tilemap;
|
||||||
|
protected obstacles!: Phaser.Tilemaps.TilemapLayer;
|
||||||
|
protected ground!: Phaser.Tilemaps.TilemapLayer;
|
||||||
|
protected player!: Phaser.Physics.Arcade.Sprite;
|
||||||
|
protected npcs: NpcSprite[] = [];
|
||||||
|
protected pois: Poi[] = [];
|
||||||
|
private keys!: Record<string, Phaser.Input.Keyboard.Key>;
|
||||||
|
private transitioning = false;
|
||||||
|
private colorFx!: Phaser.FX.ColorMatrix;
|
||||||
|
private vignetteFx!: Phaser.FX.Vignette;
|
||||||
|
private lastMoveAt = 0;
|
||||||
|
private rainStillAt = 0;
|
||||||
|
private nearWellSince = 0;
|
||||||
|
private nearFogSince = 0;
|
||||||
|
private dissuaded = false;
|
||||||
|
private reflection?: Phaser.GameObjects.Sprite;
|
||||||
|
private prevPos = { x: 0, y: 0 };
|
||||||
|
private rain?: Phaser.GameObjects.Particles.ParticleEmitter;
|
||||||
|
private t14Active = false;
|
||||||
|
private t14Start = 0;
|
||||||
|
private t14Faded = false;
|
||||||
|
private celebTimer?: Phaser.Time.TimerEvent;
|
||||||
|
private buntings: Phaser.GameObjects.Image[] = [];
|
||||||
|
private interactCooldown = 0;
|
||||||
|
|
||||||
|
constructor(key: string) { super(key); this.mapKey = key; }
|
||||||
|
|
||||||
|
get ui(): UIScene { return this.scene.get("ui") as UIScene; }
|
||||||
|
get isWhite(): boolean { return this.mapKey.startsWith("white"); }
|
||||||
|
|
||||||
|
create() {
|
||||||
|
director.currentScene = this.mapKey;
|
||||||
|
this.transitioning = false;
|
||||||
|
this.npcs = []; this.pois = []; this.buntings = [];
|
||||||
|
this.t14Active = false; this.dissuaded = false;
|
||||||
|
|
||||||
|
this.map = this.make.tilemap({ key: "map_" + this.mapKey });
|
||||||
|
const ts = this.map.addTilesetImage("tiles", "tiles")!;
|
||||||
|
this.ground = this.map.createLayer("ground", ts)!;
|
||||||
|
this.obstacles = this.map.createLayer("obstacles", ts)!;
|
||||||
|
this.obstacles.setCollisionByExclusion([-1]);
|
||||||
|
|
||||||
|
// 玩家
|
||||||
|
const spawn = this.findSpawn(director.pendingSpawn) ?? this.findSpawn("entry") ?? { x: 40, y: 40 };
|
||||||
|
this.player = this.physics.add.sprite(spawn.x, spawn.y, "player", 0);
|
||||||
|
this.player.setSize(10, 8).setOffset(3, 16).setCollideWorldBounds(true);
|
||||||
|
this.player.setDepth(spawn.y);
|
||||||
|
this.physics.world.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
|
||||||
|
this.physics.add.collider(this.player, this.obstacles);
|
||||||
|
this.prevPos = { x: spawn.x, y: spawn.y };
|
||||||
|
|
||||||
|
// 相机:跟随并钳制地图边界
|
||||||
|
const cam = this.cameras.main;
|
||||||
|
// 相机钳制地图边界;地图小于视口时对称扩展 bounds 使其居中
|
||||||
|
const mw = this.map.widthInPixels, mh = this.map.heightInPixels;
|
||||||
|
cam.setBounds(
|
||||||
|
mw < 320 ? (mw - 320) / 2 : 0,
|
||||||
|
mh < 180 ? (mh - 180) / 2 : 0,
|
||||||
|
Math.max(mw, 320),
|
||||||
|
Math.max(mh, 180),
|
||||||
|
);
|
||||||
|
cam.startFollow(this.player, true);
|
||||||
|
cam.setRoundPixels(true);
|
||||||
|
|
||||||
|
// 对象层
|
||||||
|
const objLayer = this.map.getObjectLayer("objects");
|
||||||
|
for (const o of objLayer?.objects ?? []) {
|
||||||
|
const type = (o as any).type || (o as any).class || "";
|
||||||
|
const props: Record<string, any> = {};
|
||||||
|
for (const p of (o.properties as any[]) ?? []) props[p.name] = p.value;
|
||||||
|
if (type === "door") {
|
||||||
|
const zone = this.add.zone(o.x! + o.width! / 2, o.y! + o.height! / 2, o.width!, o.height!);
|
||||||
|
this.physics.add.existing(zone, true);
|
||||||
|
this.physics.add.overlap(this.player, zone, () => this.goTo(props.target, props.spawn));
|
||||||
|
} else if (type === "npc") {
|
||||||
|
this.spawnNpc(o.name!, o.x!, o.y!, props.patrol);
|
||||||
|
} else if (type === "poi") {
|
||||||
|
this.pois.push({ name: o.name!, x: o.x!, y: o.y!, clue: props.clue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复日内世界状态(花坛/莓果/彩带)
|
||||||
|
this.restoreTiles();
|
||||||
|
// Day 7:庆典布置
|
||||||
|
if (director.day === 7 && this.mapKey === "plaza") this.setupCelebrationDay();
|
||||||
|
// 湖面倒影
|
||||||
|
if (this.mapKey === "lake") {
|
||||||
|
this.reflection = this.add.sprite(0, 0, "player", 0).setFlipY(true).setAlpha(0.3).setDepth(1);
|
||||||
|
}
|
||||||
|
// Day 6 傍晚脚本雨(室外,NPC 假装看不见)
|
||||||
|
if (director.day === 6 && director.dusk && OUTDOOR.includes(this.mapKey)) this.startRain();
|
||||||
|
|
||||||
|
this.keys = this.input.keyboard!.addKeys("W,A,S,D,UP,DOWN,LEFT,RIGHT,E,ENTER") as any;
|
||||||
|
|
||||||
|
// 侵蚀矩阵 → 相机 postFX
|
||||||
|
this.colorFx = this.cameras.main.postFX.addColorMatrix();
|
||||||
|
this.vignetteFx = this.cameras.main.postFX.addVignette(0.5, 0.5, 0.9, 0);
|
||||||
|
this.applyStageVisuals();
|
||||||
|
audio.applyStage();
|
||||||
|
|
||||||
|
director.on("stage", this.onStage, this);
|
||||||
|
director.on("negflash", this.onNegFlash, this);
|
||||||
|
director.on("sadact", this.onSadAct, this);
|
||||||
|
director.on("forcedCelebration", this.onForcedCelebration, this);
|
||||||
|
director.on("dusk", this.onDuskWorld, this);
|
||||||
|
this.events.on("shutdown", () => {
|
||||||
|
director.off("stage", this.onStage, this);
|
||||||
|
director.off("negflash", this.onNegFlash, this);
|
||||||
|
director.off("sadact", this.onSadAct, this);
|
||||||
|
director.off("forcedCelebration", this.onForcedCelebration, this);
|
||||||
|
director.off("dusk", this.onDuskWorld, this);
|
||||||
|
if (director.celebrationActive) director.celebrationEnd(true); // 从场景边缘走离 = 逃离
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastMoveAt = this.time.now;
|
||||||
|
this.nearWellSince = this.time.now;
|
||||||
|
this.nearFogSince = this.time.now;
|
||||||
|
this.rainStillAt = this.time.now;
|
||||||
|
this.ui?.forceClose();
|
||||||
|
this.ui?.fadeIn(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 基础 ----------
|
||||||
|
private findSpawn(name: string): { x: number; y: number } | null {
|
||||||
|
const objLayer = this.map.getObjectLayer("objects");
|
||||||
|
let first: { x: number; y: number } | null = null;
|
||||||
|
for (const o of objLayer?.objects ?? []) {
|
||||||
|
const type = (o as any).type || (o as any).class || "";
|
||||||
|
if (type !== "spawn") continue;
|
||||||
|
if (!first) first = { x: o.x!, y: o.y! };
|
||||||
|
if (o.name === name) return { x: o.x!, y: o.y! };
|
||||||
|
}
|
||||||
|
return name === "entry" ? first : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnNpc(id: string, x: number, y: number, patrol?: number) {
|
||||||
|
if (director.day === 7 && this.mapKey !== "plaza" && (NAMED.includes(id) || id.startsWith("bird"))) return;
|
||||||
|
let tex = "npc_" + id, frame = 0;
|
||||||
|
if (id.startsWith("bird")) tex = "bird";
|
||||||
|
if (id.startsWith("extra")) tex = id;
|
||||||
|
const s = this.add.sprite(x, y, tex, frame).setOrigin(0.5, 1).setDepth(y);
|
||||||
|
if (id.startsWith("bird")) s.play("bird-sing");
|
||||||
|
if (id.startsWith("extra")) s.play(id + "-walk");
|
||||||
|
// 物理体(具名 NPC 阻挡)
|
||||||
|
if (NAMED.includes(id)) {
|
||||||
|
this.physics.add.existing(s, true);
|
||||||
|
const body = s.body as Phaser.Physics.Arcade.StaticBody;
|
||||||
|
body.setSize(12, 10); body.setOffset(2, 14);
|
||||||
|
this.physics.add.collider(this.player, s);
|
||||||
|
}
|
||||||
|
this.npcs.push({ id, sprite: s, x0: x, y0: y, patrol, dir: 1, nextBlink: this.time.now + 1000 + Math.random() * 2000, offUntil: 0, blinkUntil: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
goTo(target: string, spawnName: string) {
|
||||||
|
if (this.transitioning || director.cutscene) return;
|
||||||
|
this.transitioning = true;
|
||||||
|
director.pendingSpawn = spawnName;
|
||||||
|
this.ui.fadeOut(250, () => {
|
||||||
|
this.scene.start(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private restoreTiles() {
|
||||||
|
if (this.mapKey === "plaza") {
|
||||||
|
for (const w of director.watered) {
|
||||||
|
const p = this.pois.find(q => q.name === w);
|
||||||
|
if (p) this.obstacles.putTileAt(56, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1); // 盛开
|
||||||
|
}
|
||||||
|
for (const r of director.ribbons) this.hangRibbonVisual(r);
|
||||||
|
}
|
||||||
|
if (this.mapKey === "field") {
|
||||||
|
for (const b of director.berries) {
|
||||||
|
const p = this.pois.find(q => q.name === b);
|
||||||
|
if (p) this.obstacles.putTileAt(13, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1); // 已采
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private hangRibbonVisual(poiName: string) {
|
||||||
|
const p = this.pois.find(q => q.name === poiName);
|
||||||
|
if (!p) return;
|
||||||
|
const img = this.add.image(Math.floor(p.x / 16) * 16 + 8, (Math.floor(p.y / 16) - 2) * 16 + 8, "tiles", 58).setDepth(3);
|
||||||
|
this.buntings.push(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 侵蚀矩阵 → 视觉(§6.4,唯一真源驱动) ----------
|
||||||
|
private onStage() { this.applyStageVisuals(); audio.applyStage(); }
|
||||||
|
applyStageVisuals() {
|
||||||
|
const v = director.visual;
|
||||||
|
if (this.isWhite) { // 白色层:去饱和灰白色板,不参与糖果侵蚀
|
||||||
|
this.colorFx.reset(); this.colorFx.saturate(-0.4);
|
||||||
|
this.vignetteFx.strength = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.colorFx.reset();
|
||||||
|
this.colorFx.saturate(v.saturation - 1);
|
||||||
|
this.vignetteFx.strength = v.vignette;
|
||||||
|
}
|
||||||
|
private onNegFlash() {
|
||||||
|
// 单帧负片(S4 溢出池 / 随机)
|
||||||
|
this.colorFx.reset(); this.colorFx.negative();
|
||||||
|
this.time.delayedCall(50, () => this.applyStageVisuals());
|
||||||
|
}
|
||||||
|
private onSadAct() {
|
||||||
|
// 最近的 NPC 转头看向玩家一次
|
||||||
|
let best: NpcSprite | null = null; let bd = 1e9;
|
||||||
|
for (const n of this.npcs) {
|
||||||
|
const d = Phaser.Math.Distance.Between(n.sprite.x, n.sprite.y, this.player.x, this.player.y);
|
||||||
|
if (d < bd) { bd = d; best = n; }
|
||||||
|
}
|
||||||
|
if (!best) return;
|
||||||
|
const s = best.sprite;
|
||||||
|
s.setFlipX(this.player.x < s.x);
|
||||||
|
this.tweens.add({ targets: s, y: s.y - 1, duration: 90, yoyo: true });
|
||||||
|
this.time.delayedCall(900, () => s.setFlipX(false));
|
||||||
|
}
|
||||||
|
private onDuskWorld() {
|
||||||
|
if (director.day === 6 && OUTDOOR.includes(this.mapKey)) this.startRain();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startRain() {
|
||||||
|
if (this.rain) return;
|
||||||
|
this.rain = this.add.particles(0, 0, "rainpx", {
|
||||||
|
x: { min: 0, max: this.map.widthInPixels },
|
||||||
|
y: -8,
|
||||||
|
lifespan: 1400,
|
||||||
|
speedY: { min: 150, max: 190 },
|
||||||
|
speedX: -12,
|
||||||
|
quantity: 2,
|
||||||
|
frequency: 30,
|
||||||
|
alpha: 0.6,
|
||||||
|
}).setDepth(200);
|
||||||
|
this.rainStillAt = this.time.now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 强制欢乐庆典(§6.1 反制) ----------
|
||||||
|
private onForcedCelebration() {
|
||||||
|
this.ui.banner("欢 乐 庆 典 !");
|
||||||
|
this.ui.toast("(世界不允许你难过)");
|
||||||
|
audio.sfxDing();
|
||||||
|
for (const n of this.npcs) {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: n.sprite,
|
||||||
|
x: Phaser.Math.Linear(n.sprite.x, this.player.x, 0.5),
|
||||||
|
y: Phaser.Math.Linear(n.sprite.y, this.player.y, 0.5),
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.celebTimer = this.time.delayedCall(8000, () => {
|
||||||
|
if (director.celebrationActive) {
|
||||||
|
director.celebrationEnd(false); // 未逃离:幸福拉回 50
|
||||||
|
this.ui.toast("(你重新开心了起来)");
|
||||||
|
for (const n of this.npcs) this.tweens.add({ targets: n.sprite, x: n.x0, y: n.y0, duration: 2000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Day 7 庆典 ----------
|
||||||
|
private setupCelebrationDay() {
|
||||||
|
director.celebrationStarted = true;
|
||||||
|
// E4:静默——庆典无人出席(优先级最高,进场即判)
|
||||||
|
if (director.e4ok && director.escapes >= 3) {
|
||||||
|
// 清空所有 NPC,彩带在没有风的空气里飘
|
||||||
|
for (const n of this.npcs) n.sprite.destroy();
|
||||||
|
this.npcs = [];
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const img = this.add.image(200 + i * 16, 168, "tiles", 58).setDepth(3);
|
||||||
|
this.tweens.add({ targets: img, y: 166, duration: 1800 + i * 130, yoyo: true, repeat: -1 });
|
||||||
|
}
|
||||||
|
this.time.delayedCall(2600, () => this.startEnding("E4"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 全村到场
|
||||||
|
const places: [string, number, number][] = [
|
||||||
|
["yuanyuan", 214, 202], ["mimi", 266, 202], ["fu", 202, 224], ["gui", 278, 224],
|
||||||
|
];
|
||||||
|
for (const [id, x, y] of places) this.spawnNpc(id, x, y);
|
||||||
|
// 彩带装饰
|
||||||
|
for (let i = 0; i < 6; i++) this.add.image(200 + i * 16, 168, "tiles", 58).setDepth(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
startEnding(code: "E1" | "E2" | "E3" | "E4") {
|
||||||
|
if (director.endingPlayed) return;
|
||||||
|
director.endingPlayed = true;
|
||||||
|
this.registry.set("ending", code);
|
||||||
|
this.scene.stop("ui");
|
||||||
|
this.scene.start("ending");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 更新循环 ----------
|
||||||
|
update(time: number, _dt: number) {
|
||||||
|
if (!this.player) return;
|
||||||
|
const ui = this.ui;
|
||||||
|
const blocked = ui.dialogOpen || director.cutscene;
|
||||||
|
|
||||||
|
// 移动
|
||||||
|
let vx = 0, vy = 0;
|
||||||
|
if (!blocked && !this.t14Active) {
|
||||||
|
if (this.keys.A.isDown || this.keys.LEFT.isDown) vx = -1;
|
||||||
|
else if (this.keys.D.isDown || this.keys.RIGHT.isDown) vx = 1;
|
||||||
|
if (this.keys.W.isDown || this.keys.UP.isDown) vy = -1;
|
||||||
|
else if (this.keys.S.isDown || this.keys.DOWN.isDown) vy = 1;
|
||||||
|
}
|
||||||
|
const moving = vx !== 0 || vy !== 0;
|
||||||
|
const v = new Phaser.Math.Vector2(vx, vy).normalize().scale(SPEED);
|
||||||
|
this.player.setVelocity(v.x, v.y);
|
||||||
|
if (moving) {
|
||||||
|
this.lastMoveAt = time;
|
||||||
|
this.rainStillAt = time;
|
||||||
|
if (vy < 0) this.player.play("walk-up", true);
|
||||||
|
else if (vy > 0) this.player.play("walk-down", true);
|
||||||
|
else if (vx < 0) this.player.play("walk-left", true);
|
||||||
|
else this.player.play("walk-right", true);
|
||||||
|
} else {
|
||||||
|
// 待机:当前朝向第 1 帧
|
||||||
|
const anim = this.player.anims.currentAnim;
|
||||||
|
this.player.anims.stop();
|
||||||
|
if (anim) this.player.setFrame(anim.frames[0].frame.name);
|
||||||
|
}
|
||||||
|
this.player.setDepth(this.player.y);
|
||||||
|
|
||||||
|
// T14:保持微笑 60 秒
|
||||||
|
if (this.t14Active) {
|
||||||
|
if (moving || Phaser.Input.Keyboard.JustDown(this.keys.A) || Phaser.Input.Keyboard.JustDown(this.keys.W)) {
|
||||||
|
this.cancelT14();
|
||||||
|
} else {
|
||||||
|
const el = (time - this.t14Start) / 1000;
|
||||||
|
if (el >= 40 && !this.t14Faded) { this.t14Faded = true; audio.fadeToSilence(19); } // 40–60 秒渐隐至零
|
||||||
|
if (el >= 60) this.finishT14();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发呆(悲伤行为③):连续 10 秒不移动且不在任务点
|
||||||
|
if (!blocked && !this.t14Active && !director.celebrationActive && time - this.lastMoveAt > 10_000) {
|
||||||
|
this.lastMoveAt = time;
|
||||||
|
if (!this.nearAnyPoi()) director.sadAct("idle");
|
||||||
|
}
|
||||||
|
// 雨中站立 ≥10 秒(悲伤行为⑤)
|
||||||
|
if (this.rain && !blocked && time - this.rainStillAt > 10_000) {
|
||||||
|
this.rainStillAt = time;
|
||||||
|
director.sadAct("rain");
|
||||||
|
this.ui.toast("(雨很冷。没有人看见雨。)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注视古井/雾墙 5 秒 → 觉知 +3(各每日 1 次)
|
||||||
|
this.stareCheck(time, "well");
|
||||||
|
this.stareCheck(time, "fog");
|
||||||
|
|
||||||
|
// 湖面倒影(Day2 起延迟 1 帧)
|
||||||
|
if (this.reflection) {
|
||||||
|
const src = director.day >= 2 ? this.prevPos : { x: this.player.x, y: this.player.y };
|
||||||
|
const shoreY = 9.5 * 16;
|
||||||
|
this.reflection.setPosition(src.x, shoreY + (shoreY - src.y) + 20);
|
||||||
|
this.reflection.setVisible(this.player.y > 80 && this.player.y < 152);
|
||||||
|
this.reflection.setFrame(this.player.frame.name);
|
||||||
|
this.prevPos = { x: this.player.x, y: this.player.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateNpcs(time);
|
||||||
|
|
||||||
|
// 交互提示与触发
|
||||||
|
if (!blocked && !this.t14Active) {
|
||||||
|
const target = this.nearestInteract();
|
||||||
|
if (target) {
|
||||||
|
ui.prompt(`按 E ${target.kind === "npc" ? "对话" : "查看"}`);
|
||||||
|
const pressed = Phaser.Input.Keyboard.JustDown(this.keys.E) || Phaser.Input.Keyboard.JustDown(this.keys.ENTER);
|
||||||
|
if (pressed && time > this.interactCooldown) this.interact(target);
|
||||||
|
} else {
|
||||||
|
ui.prompt(null);
|
||||||
|
}
|
||||||
|
// D5 前古井/雾墙劝离
|
||||||
|
this.dissuadeCheck();
|
||||||
|
} else {
|
||||||
|
// 消费残留按键:关闭对话的那一次 E 不应立刻再次触发交互
|
||||||
|
Phaser.Input.Keyboard.JustDown(this.keys.E);
|
||||||
|
Phaser.Input.Keyboard.JustDown(this.keys.ENTER);
|
||||||
|
this.interactCooldown = time + 250;
|
||||||
|
ui.prompt(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateNpcs(time: number) {
|
||||||
|
const v = director.visual;
|
||||||
|
const s4 = director.stage >= 4;
|
||||||
|
for (const n of this.npcs) {
|
||||||
|
const base = n.id.startsWith("bird") ? "bird" : n.id;
|
||||||
|
// 群演巡逻;S4 时 1 号群演停摆面朝玩家
|
||||||
|
if (n.patrol) {
|
||||||
|
const frozen = s4 && n.id === "extra1";
|
||||||
|
if (frozen || this.t14Active) {
|
||||||
|
n.sprite.anims.pause();
|
||||||
|
n.sprite.setFlipX(this.player.x < n.sprite.x);
|
||||||
|
} else {
|
||||||
|
if (!n.sprite.anims.isPlaying) n.sprite.anims.resume();
|
||||||
|
n.sprite.x += n.dir * 0.25;
|
||||||
|
if (n.sprite.x > n.x0 + n.patrol) n.dir = -1;
|
||||||
|
if (n.sprite.x < n.x0 - n.patrol) n.dir = 1;
|
||||||
|
n.sprite.setFlipX(n.dir < 0);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (n.id.startsWith("bird")) {
|
||||||
|
// 音乐变调时动画同步卡帧;S4 鸟鸣消失但仍在张嘴
|
||||||
|
const stg = director.stage;
|
||||||
|
const rate = [1, 1, 0.97, 0.92, 0.7][stg];
|
||||||
|
n.sprite.anims.timeScale = rate;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!NAMED.includes(n.id)) continue;
|
||||||
|
// 具名 NPC 表情档 + 眨眼 + off 闪现
|
||||||
|
const expr = director.npcExpr(n.id);
|
||||||
|
let frame = expr === "flat" ? 1 : 0;
|
||||||
|
const npcStage = director.npcStage(n.id);
|
||||||
|
if (n.offUntil > time) frame = 2; // off 档闪现 0.1 秒
|
||||||
|
else if (n.blinkUntil > time) frame = 3; // 眨眼
|
||||||
|
else if (time > n.nextBlink) {
|
||||||
|
n.blinkUntil = time + 120;
|
||||||
|
n.nextBlink = time + v.blinkInterval + Math.random() * 800;
|
||||||
|
}
|
||||||
|
// S4:off 档闪现(0.6 次/分钟 ⇒ 每帧概率)
|
||||||
|
if (npcStage >= 4 && NPC_OFFSET[base] !== null && n.offUntil < time && Math.random() < 0.6 / (60 * 60)) {
|
||||||
|
n.offUntil = time + 100;
|
||||||
|
}
|
||||||
|
n.sprite.setFrame(frame);
|
||||||
|
// S3+:静止 NPC 面朝玩家
|
||||||
|
if (npcStage >= 3) n.sprite.setFlipX(this.player.x < n.sprite.x);
|
||||||
|
}
|
||||||
|
// S4 随机单帧负片:0.4 次/分钟
|
||||||
|
if (s4 && !this.isWhite && Math.random() < 0.4 / (60 * 60)) this.onNegFlash();
|
||||||
|
}
|
||||||
|
|
||||||
|
private stareCheck(time: number, kind: "well" | "fog") {
|
||||||
|
const p = this.pois.find(q => q.name === kind);
|
||||||
|
if (!p || director.day < 5) return;
|
||||||
|
const near = Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < 30;
|
||||||
|
if (kind === "well") {
|
||||||
|
if (!near) { this.nearWellSince = time; return; }
|
||||||
|
if (time - this.nearWellSince > 5000 && !director.stared.has("well")) {
|
||||||
|
director.stare("well");
|
||||||
|
this.ui.toast(MISC.wellStare);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!near) { this.nearFogSince = time; return; }
|
||||||
|
if (time - this.nearFogSince > 5000 && !director.stared.has("fog")) {
|
||||||
|
director.stare("fog");
|
||||||
|
this.ui.toast(MISC.fogStare);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private dissuadeCheck() {
|
||||||
|
if (director.day >= 5 || this.dissuaded) return;
|
||||||
|
const p = this.pois.find(q => q.name === "well" || q.name === "fog");
|
||||||
|
if (!p) return;
|
||||||
|
if (Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < 28) {
|
||||||
|
this.dissuaded = true;
|
||||||
|
const lines = p.name === "well" ? DISSUADE.well : DISSUADE.fog;
|
||||||
|
this.ui.say("villager", lines, {
|
||||||
|
onDone: () => {
|
||||||
|
// 温柔劝离:把玩家推回入口方向
|
||||||
|
const spawn = this.findSpawn("entry")!;
|
||||||
|
const dir = new Phaser.Math.Vector2(spawn.x - this.player.x, spawn.y - this.player.y).normalize().scale(40);
|
||||||
|
this.player.setPosition(this.player.x + dir.x, this.player.y + dir.y);
|
||||||
|
this.time.delayedCall(3000, () => (this.dissuaded = false));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nearAnyPoi(): boolean {
|
||||||
|
for (const p of this.pois) {
|
||||||
|
if (Phaser.Math.Distance.Between(this.player.x, this.player.y, p.x, p.y) < INTERACT_DIST + 8) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nearestInteract(): { kind: "npc" | "poi"; id: string; poi?: Poi } | null {
|
||||||
|
let best: { kind: "npc" | "poi"; id: string; poi?: Poi; d: number } | null = null;
|
||||||
|
for (const n of this.npcs) {
|
||||||
|
if (n.id.startsWith("extra") || n.id.startsWith("bird")) {
|
||||||
|
// 群演可摸头;双子鸟经 poi 交互
|
||||||
|
if (n.id.startsWith("bird")) continue;
|
||||||
|
}
|
||||||
|
const d = Phaser.Math.Distance.Between(n.sprite.x, n.sprite.y - 10, this.player.x, this.player.y);
|
||||||
|
if (d < INTERACT_DIST && (!best || d < best.d)) best = { kind: "npc", id: n.id, d };
|
||||||
|
}
|
||||||
|
for (const p of this.pois) {
|
||||||
|
const d = Phaser.Math.Distance.Between(p.x, p.y, this.player.x, this.player.y);
|
||||||
|
if (d < INTERACT_DIST && (!best || d < best.d)) best = { kind: "poi", id: p.name, poi: p, d };
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 交互分发 ----------
|
||||||
|
private interact(t: { kind: "npc" | "poi"; id: string; poi?: Poi }) {
|
||||||
|
if (t.kind === "npc") {
|
||||||
|
if (t.id.startsWith("extra")) return this.petCritter();
|
||||||
|
return this.talkTo(t.id);
|
||||||
|
}
|
||||||
|
this.usePoi(t.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private petCritter() {
|
||||||
|
if (director.smallAct("pet")) this.ui.say("me", ["(你摸了摸它的头。它的循环停顿了半拍,又继续了。)"], { speakerName: "" });
|
||||||
|
else this.ui.say("me", ["(它心满意足地走开了。)"], { speakerName: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- NPC 对话(含任务链) ----
|
||||||
|
private talkTo(id: string) {
|
||||||
|
const d = director;
|
||||||
|
d.greeted.add(id);
|
||||||
|
d.talkCounts[id] = (d.talkCounts[id] ?? 0) + 1;
|
||||||
|
// 纠缠(悲伤行为④):同一 NPC 一天 ≥5 次
|
||||||
|
if (d.talkCounts[id] >= 5 && !d.nagged.has(id)) {
|
||||||
|
d.nagged.add(id);
|
||||||
|
d.sadAct("nag");
|
||||||
|
}
|
||||||
|
const greetQuest = d.day === 4 ? "T10" : "T3";
|
||||||
|
const checkGreet = () => {
|
||||||
|
if (d.isOpen(greetQuest) && d.greeted.size >= 3) d.completeQuest(greetQuest);
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadQuest = d.day === 4 ? "T8" : "T1";
|
||||||
|
// T1/T8:取面包
|
||||||
|
if (d.isOpen(breadQuest) && id === "yuanyuan" && d.breadHeld === 0) {
|
||||||
|
d.breadHeld = 3;
|
||||||
|
this.ui.say("yuanyuan", QUEST_LINES.t1Give, { onDone: checkGreet });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// T1/T8:送面包
|
||||||
|
if (d.isOpen(breadQuest) && d.breadHeld > 0 && ["dabao", "mimi", "gui"].includes(id) && !d.breadDelivered.has(id)) {
|
||||||
|
d.breadDelivered.add(id);
|
||||||
|
d.breadHeld--;
|
||||||
|
this.ui.say(id, [QUEST_LINES.t1Deliver[id]], {
|
||||||
|
onDone: () => {
|
||||||
|
if (d.breadDelivered.size >= 3) d.completeQuest(breadQuest);
|
||||||
|
checkGreet();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// T4:取信
|
||||||
|
if (d.day === 2 && d.isOpen("T4") && id === "dabao" && d.lettersHeld === 0 && d.lettersDelivered.size === 0) {
|
||||||
|
d.lettersHeld = 3;
|
||||||
|
this.ui.say("dabao", QUEST_LINES.t4Give, { onDone: checkGreet });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// T4:送信(第 3 封署名乱码 0.2 秒 → C4)
|
||||||
|
if (d.isOpen("T4") && d.lettersHeld > 0 && ["yuanyuan", "mimi", "fu"].includes(id) && !d.lettersDelivered.has(id)) {
|
||||||
|
d.lettersDelivered.add(id);
|
||||||
|
d.lettersHeld--;
|
||||||
|
const third = d.lettersDelivered.size >= 3;
|
||||||
|
this.ui.say(id, [QUEST_LINES.t4Deliver[id]], {
|
||||||
|
onDone: () => {
|
||||||
|
if (third) {
|
||||||
|
this.ui.glitchText("署名:PT-0▓", 200);
|
||||||
|
this.time.delayedCall(240, () => {
|
||||||
|
this.ui.say("me", CLUE_TEXT.C4, {
|
||||||
|
speakerName: "", clean: true,
|
||||||
|
onDone: () => { director.foundClue("C4"); d.completeQuest("T4"); },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
checkGreet();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// T5:咪咪请求清点
|
||||||
|
if (d.day === 2 && d.isOpen("T5") && id === "mimi" && d.shelves.size === 0) {
|
||||||
|
this.ui.say("mimi", QUEST_LINES.t5Ask, { onDone: checkGreet });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// T11:帮圆圆揉面(可拒绝,拒绝被记录)
|
||||||
|
if (d.day === 5 && d.isOpen("T11") && id === "yuanyuan" && !d.t11Accepted) {
|
||||||
|
this.ui.say("yuanyuan", [QUEST_LINES.t11Ask], {
|
||||||
|
choices: [
|
||||||
|
{ label: "好呀,我来帮忙。", cb: () => { d.t11Accepted = true; this.ui.toast("(去揉面台吧)"); } },
|
||||||
|
{ label: "今天不想帮。", cb: () => { d.refuseQuest("T11"); this.ui.say("yuanyuan", [QUEST_LINES.t11Refused]); } },
|
||||||
|
],
|
||||||
|
onDone: checkGreet,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 龟爷爷 C9 对话链(D2/D4/D6 三段递进)
|
||||||
|
if (id === "gui" && [2, 4, 6].includes(d.day)) {
|
||||||
|
const node = NPC_TALK.gui[d.day];
|
||||||
|
this.ui.say("gui", node.lines, {
|
||||||
|
onDone: () => {
|
||||||
|
d.guiChain.add(d.day);
|
||||||
|
if (d.day === 6 && !d.clues.has("C9")) {
|
||||||
|
this.ui.say("me", CLUE_TEXT.C9, { speakerName: "", clean: true, onDone: () => director.foundClue("C9") });
|
||||||
|
}
|
||||||
|
checkGreet();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 默认闲聊
|
||||||
|
const node = NPC_TALK[id]?.[d.day];
|
||||||
|
if (!node) { this.ui.say(id, ["……"]); return; }
|
||||||
|
const opts: UiChoice[] | undefined = node.choices?.map((c: DialogChoice) => ({
|
||||||
|
label: c.label,
|
||||||
|
cb: () => {
|
||||||
|
if (c.kind === "gentle") d.addHappiness(2);
|
||||||
|
else if (c.kind === "hesitant") d.addAwareness(2);
|
||||||
|
else d.sadAct("negative"); // 否定选项 = 悲伤行为②
|
||||||
|
this.ui.say(id, c.reply, { onDone: checkGreet });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
this.ui.say(id, node.lines, { choices: opts, onDone: checkGreet });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- POI 交互 ----
|
||||||
|
private usePoi(name: string) {
|
||||||
|
const d = director;
|
||||||
|
const say = (sp: string, lines: string[], opts: any = {}) => this.ui.say(sp, lines, opts);
|
||||||
|
const narr = (lines: string[], onDone?: () => void, clean = true) =>
|
||||||
|
say("me", lines, { speakerName: "", clean, onDone });
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
// 公告板:公告 + T12 擦拭
|
||||||
|
case name === "board": {
|
||||||
|
if (!d.announced) {
|
||||||
|
d.announced = true;
|
||||||
|
const tasks = d.questsOfDay().map(q => `・${q.title}`);
|
||||||
|
say("afu", [ANNOUNCEMENTS[d.day], ...(tasks.length ? ["今日的幸福任务:", ...tasks] : [])], { clean: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.day === 5 && d.isOpen("T12")) {
|
||||||
|
if (!d.t12Accepted) {
|
||||||
|
this.ui.say("me", [QUEST_LINES.t12Ask], {
|
||||||
|
speakerName: "", clean: true,
|
||||||
|
choices: [
|
||||||
|
{ label: "擦拭公告板。", cb: () => { d.t12Accepted = true; this.wipeBoard(); } },
|
||||||
|
{ label: "拒绝。", cb: () => { d.refuseQuest("T12"); narr([QUEST_LINES.t12Refused]); } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.wipeBoard();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const open = d.questsOfDay().some(q => d.questStatus[q.id] === "open");
|
||||||
|
if (!open && d.day < 7) narr([MISC.boardDone]);
|
||||||
|
else say("afu", [ANNOUNCEMENTS[d.day]], { clean: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 床
|
||||||
|
case name === "bed": {
|
||||||
|
if (d.day >= 7) { narr(["(今天不用睡。庆典在等你。)"]); return; }
|
||||||
|
if (!d.dusk) { narr([MISC.bedEarly]); return; }
|
||||||
|
narr([MISC.bedSleep], () => {
|
||||||
|
director.cutscene = true;
|
||||||
|
this.ui.fadeOut(900, () => { director.cutscene = false; this.scene.start("dream"); });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 日历(C1,D2 起)
|
||||||
|
case name === "calendar": {
|
||||||
|
if (d.day >= 2) narr(CLUE_TEXT.C1, () => d.foundClue("C1"));
|
||||||
|
else narr(["(床头的日历。明天也会是晴天。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 配方纸(C2,D3 起)
|
||||||
|
case name === "recipe": {
|
||||||
|
if (d.day >= 3) narr(CLUE_TEXT.C2, () => d.foundClue("C2"));
|
||||||
|
else narr(["(柜台上的配方纸。字迹工整。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 散落信件(C3,D2 起)
|
||||||
|
case name === "letters": {
|
||||||
|
if (d.day >= 2) narr(CLUE_TEXT.C3, () => d.foundClue("C3"));
|
||||||
|
else narr(["(邮筒旁散落着几封信。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 货架清点(T5)与 C5
|
||||||
|
case name.startsWith("shelfA") || name.startsWith("shelfB") || name.startsWith("shelfC"): {
|
||||||
|
if (d.day === 2 && d.isOpen("T5")) {
|
||||||
|
if (!d.shelves.has(name)) {
|
||||||
|
d.shelves.add(name);
|
||||||
|
narr([`(${d.shelves.size}/3……数字对得上。)`], () => {
|
||||||
|
if (d.shelves.size >= 3) {
|
||||||
|
say("mimi", QUEST_LINES.t5Done, { onDone: () => d.completeQuest("T5") });
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
narr(["(货架排得整整齐齐。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case name === "shelf_c5": {
|
||||||
|
if (d.day >= 2) narr(CLUE_TEXT.C5, () => d.foundClue("C5"));
|
||||||
|
else narr(["(货架上的商品排得很满。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 诊所表格(C8,D5 起)
|
||||||
|
case name === "chart": {
|
||||||
|
if (d.day >= 5) narr(CLUE_TEXT.C8, () => d.foundClue("C8"));
|
||||||
|
else narr(["(桌上放着一份表格,字很小。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 花坛浇水(T2/T9 + 小互动)
|
||||||
|
case name.startsWith("flowerbed"): {
|
||||||
|
const wq = d.day === 4 ? "T9" : "T2";
|
||||||
|
if (d.isOpen(wq) && !d.watered.has(name)) {
|
||||||
|
d.watered.add(name);
|
||||||
|
const p = this.pois.find(q => q.name === name)!;
|
||||||
|
this.obstacles.putTileAt(56, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1);
|
||||||
|
narr([`(浇水 ${d.watered.size}/3。花立刻精神了起来。)`], () => {
|
||||||
|
if (d.watered.size >= 3) d.completeQuest(wq);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.smallAct("water")) narr(["(又浇了点水。)"]);
|
||||||
|
else narr(["(土已经很湿了。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case name === "fieldwater": {
|
||||||
|
if (d.smallAct("water")) narr(["(你给花田浇了水。)"]);
|
||||||
|
else narr(["(今天浇得够多了。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 采莓果(T6)
|
||||||
|
case name.startsWith("berry"): {
|
||||||
|
if (d.day === 3 && d.isOpen("T6") && !d.berries.has(name)) {
|
||||||
|
d.berries.add(name);
|
||||||
|
const p = this.pois.find(q => q.name === name)!;
|
||||||
|
this.obstacles.putTileAt(13, Math.floor(p.x / 16), Math.floor(p.y / 16) - 1);
|
||||||
|
narr([`(采到莓果 ${d.berries.size}/5。)`], () => {
|
||||||
|
if (d.berries.size >= 5) d.completeQuest("T6");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
narr(["(莓果丛。果子还没长出来。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 野餐(T7)
|
||||||
|
case name === "picnic": {
|
||||||
|
if (d.day === 3 && d.isOpen("T7")) {
|
||||||
|
director.cutscene = true;
|
||||||
|
this.cameras.main.pan(this.player.x, this.map.heightInPixels - 60, 1200, "Sine.easeInOut");
|
||||||
|
narr(MISC.picnic, () => {
|
||||||
|
this.cameras.main.pan(this.player.x, this.player.y, 800, "Sine.easeInOut");
|
||||||
|
director.cutscene = false;
|
||||||
|
d.completeQuest("T7");
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
narr(["(野餐布。有阳光晒过的味道。)"], undefined, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 揉面(T11)
|
||||||
|
case name === "knead": {
|
||||||
|
if (d.day === 5 && d.isOpen("T11") && d.t11Accepted) {
|
||||||
|
say("yuanyuan", QUEST_LINES.t11Work, { onDone: () => d.completeQuest("T11") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
narr(["(揉面台。面粉是甜的。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 挂彩带(T13)
|
||||||
|
case name.startsWith("ribbon"): {
|
||||||
|
if (d.day === 6 && d.isOpen("T13") && !d.ribbons.has(name)) {
|
||||||
|
d.ribbons.add(name);
|
||||||
|
this.hangRibbonVisual(name);
|
||||||
|
narr([`(挂好彩带 ${d.ribbons.size}/4。)`], () => {
|
||||||
|
if (d.ribbons.size >= 4) d.completeQuest("T13");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
narr(["(灯笼杆。漆是新的——每年都是新的。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 庆典场地(T14 / T15)
|
||||||
|
case name === "festival": {
|
||||||
|
if (d.day === 6 && d.isOpen("T14")) { this.beginT14(); return; }
|
||||||
|
if (d.day === 7) { this.festivalDay7(); return; }
|
||||||
|
narr(["(庆典的场地,打扫得很干净。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 古井(C7,D5 起可交互)
|
||||||
|
case name === "well": {
|
||||||
|
if (d.day < 5) return; // 劝离逻辑在 dissuadeCheck
|
||||||
|
director.cutscene = true;
|
||||||
|
this.time.delayedCall(600, () => audio.sfxEcg());
|
||||||
|
this.time.delayedCall(1400, () => audio.sfxEcg());
|
||||||
|
this.time.delayedCall(2200, () => audio.sfxEcg());
|
||||||
|
narr(CLUE_TEXT.C7, () => { director.cutscene = false; d.foundClue("C7"); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 雾墙(E3 入口)
|
||||||
|
case name === "fog": {
|
||||||
|
if (d.day < 5) return;
|
||||||
|
if (d.day === 7 && d.celebrationStarted && d.e3Ready()) { this.beginE3(); return; }
|
||||||
|
narr(["(雾很冷,纹丝不动。)", "(雾的后面,好像有一条走廊。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 白色层病历(E3 终点)
|
||||||
|
case name === "chart_ward": {
|
||||||
|
if (d.e3Mode) { this.startEnding("E3"); return; }
|
||||||
|
narr(["(一张病历。字被白光糊住了。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 双子鸟树(喂食小互动)
|
||||||
|
case name === "birdtree": {
|
||||||
|
if (director.stage >= 4) { narr(["(它们在张嘴。没有声音。)"]); return; }
|
||||||
|
if (d.smallAct("feed")) narr(["(你撒了点面包屑。歌声更亮了。)"]);
|
||||||
|
else narr(["(双子鸟歪头看你。歌没有停。)"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case name === "bench": { narr(["(长椅被晒得暖暖的。)"]); return; }
|
||||||
|
case name === "sign_house": { narr([`(写着:${d.playerName}的小屋。)`]); return; }
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private wipeBoard() {
|
||||||
|
const d = director;
|
||||||
|
d.wipes++;
|
||||||
|
if (d.wipes === 1) {
|
||||||
|
this.ui.say("me", [QUEST_LINES.wipe1], { speakerName: "", clean: true });
|
||||||
|
} else if (d.wipes === 2) {
|
||||||
|
this.ui.say("me", [QUEST_LINES.wipe2, ...CLUE_TEXT.C6], {
|
||||||
|
speakerName: "", clean: true,
|
||||||
|
onDone: () => d.foundClue("C6"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.ui.say("me", [QUEST_LINES.wipe3], {
|
||||||
|
speakerName: "", clean: true,
|
||||||
|
onDone: () => d.completeQuest("T12"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- T14:保持微笑 60 秒 ----
|
||||||
|
private beginT14() {
|
||||||
|
this.ui.say("me", [MISC.t14Start], {
|
||||||
|
speakerName: "", clean: true,
|
||||||
|
onDone: () => {
|
||||||
|
this.t14Active = true;
|
||||||
|
this.t14Faded = false;
|
||||||
|
this.t14Start = this.time.now;
|
||||||
|
this.cameras.main.zoomTo(1.9, 60_000, "Linear"); // 镜头缓慢拉近
|
||||||
|
this.ui.toast("(不要动)");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private cancelT14() {
|
||||||
|
this.t14Active = false;
|
||||||
|
this.cameras.main.zoomTo(1, 600);
|
||||||
|
audio.restoreVolume();
|
||||||
|
this.ui.say("me", [MISC.t14Cancel], { speakerName: "", clean: true });
|
||||||
|
}
|
||||||
|
private finishT14() {
|
||||||
|
this.t14Active = false;
|
||||||
|
director.t14done = true;
|
||||||
|
this.cameras.main.zoomTo(1, 900);
|
||||||
|
audio.restoreVolume(2);
|
||||||
|
this.ui.say("me", [MISC.t14Done], {
|
||||||
|
speakerName: "", clean: true,
|
||||||
|
onDone: () => director.completeQuest("T14"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Day 7 庆典交互(T15 → 结局分歧) ----
|
||||||
|
private festivalDay7() {
|
||||||
|
const d = director;
|
||||||
|
const lines = ["第 413 届丰收庆典,现在开始。", MISC.festivalGather];
|
||||||
|
if (d.e3Ready()) lines.push(MISC.festivalHintFog);
|
||||||
|
this.ui.say("afu", lines, {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
label: "加入合影。",
|
||||||
|
cb: () => {
|
||||||
|
d.completeQuest("T15");
|
||||||
|
const e = d.decideEnding(false);
|
||||||
|
this.time.delayedCall(600, () => this.startEnding(e as any));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ label: "再看看。", cb: () => { } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- E3:雾墙裂开 → 白色走廊 ----
|
||||||
|
private beginE3() {
|
||||||
|
const d = director;
|
||||||
|
d.e3Mode = true;
|
||||||
|
d.cutscene = true;
|
||||||
|
audio.stopBgm();
|
||||||
|
this.ui.say("me", ["(雾,裂开了。)", "(里面是一条白色的走廊。很安静。)"], {
|
||||||
|
speakerName: "", clean: true,
|
||||||
|
onDone: () => {
|
||||||
|
this.cameras.main.flash(1200, 255, 255, 255);
|
||||||
|
this.time.delayedCall(1100, () => {
|
||||||
|
d.cutscene = false;
|
||||||
|
d.pendingSpawn = "entry";
|
||||||
|
this.scene.start("white_corridor");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 11 张地图各一个 Scene ----
|
||||||
|
export class HouseScene extends WorldScene { constructor() { super("house"); } }
|
||||||
|
export class PlazaScene extends WorldScene { constructor() { super("plaza"); } }
|
||||||
|
export class BakeryScene extends WorldScene { constructor() { super("bakery"); } }
|
||||||
|
export class StoreScene extends WorldScene { constructor() { super("store"); } }
|
||||||
|
export class ClinicScene extends WorldScene { constructor() { super("clinic"); } }
|
||||||
|
export class LakeScene extends WorldScene { constructor() { super("lake"); } }
|
||||||
|
export class FieldScene extends WorldScene { constructor() { super("field"); } }
|
||||||
|
export class WellScene extends WorldScene { constructor() { super("well"); } }
|
||||||
|
export class FogwallScene extends WorldScene { constructor() { super("fogwall"); } }
|
||||||
|
export class WhiteCorridorScene extends WorldScene { constructor() { super("white_corridor"); } }
|
||||||
|
export class WhiteWardScene extends WorldScene { constructor() { super("white_ward"); } }
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { chromium } from "playwright";
|
||||||
|
import { preview } from "vite";
|
||||||
|
const server = await preview({ preview: { port: 5194 } });
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||||
|
const errors = [];
|
||||||
|
page.on("console", m => { if (m.type() === "error") errors.push(m.text()); });
|
||||||
|
page.on("pageerror", e => errors.push(e.message));
|
||||||
|
await page.goto("http://localhost:5194/");
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
await page.fill("#name-input", "");
|
||||||
|
await page.press("#name-input", "Enter");
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
const name = await page.evaluate(() => window.__director.playerName);
|
||||||
|
const scene = await page.evaluate(() => window.__director.currentScene);
|
||||||
|
await page.screenshot({ path: process.env.SHOT_DIR + "/dist_house.png" });
|
||||||
|
console.log("name(默认):", name, "| scene:", scene, "| ERRORS:", errors.length);
|
||||||
|
errors.forEach(e => console.log(" -", e));
|
||||||
|
await browser.close();
|
||||||
|
server.httpServer.close();
|
||||||
|
process.exit(errors.length || name !== "小满" || scene !== "house" ? 1 : 0);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// 无头验收脚本:控制台零报错 + 标题→Day1 流程 + Debug 面板侵蚀矩阵 + 四结局
|
||||||
|
// 运行:node tools/e2e.mjs [--full]
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { createServer } from "vite";
|
||||||
|
import { mkdirSync } from "fs";
|
||||||
|
|
||||||
|
const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-shots";
|
||||||
|
mkdirSync(SHOT_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const server = await createServer({ server: { port: 5199 } });
|
||||||
|
await server.listen();
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||||
|
const errors = [];
|
||||||
|
page.on("console", (msg) => { if (msg.type() === "error") errors.push(msg.text()); });
|
||||||
|
page.on("pageerror", (err) => errors.push("PAGEERROR: " + err.message));
|
||||||
|
|
||||||
|
const shot = (name) => page.screenshot({ path: `${SHOT_DIR}/${name}.png` });
|
||||||
|
const sleep = (ms) => page.waitForTimeout(ms);
|
||||||
|
|
||||||
|
await page.goto("http://localhost:5199/");
|
||||||
|
await sleep(2500);
|
||||||
|
await shot("01_title");
|
||||||
|
|
||||||
|
// 输入名字并开始
|
||||||
|
await page.fill("#name-input", "测试员");
|
||||||
|
await page.press("#name-input", "Enter");
|
||||||
|
await sleep(2000);
|
||||||
|
await shot("02_day1_house");
|
||||||
|
|
||||||
|
// 走出小屋:向下走
|
||||||
|
await page.keyboard.down("s"); await sleep(1400); await page.keyboard.up("s");
|
||||||
|
await sleep(1200);
|
||||||
|
await shot("03_plaza");
|
||||||
|
|
||||||
|
// Debug 面板:侵蚀矩阵五档
|
||||||
|
await page.keyboard.press("`");
|
||||||
|
await sleep(300);
|
||||||
|
await shot("04_debug_open");
|
||||||
|
for (const h of [10, 40, 70, 90, 100]) {
|
||||||
|
await page.fill("#dbg-h", String(h));
|
||||||
|
await page.click("#dbg-h-set");
|
||||||
|
await sleep(700);
|
||||||
|
await shot(`05_stage_h${h}`);
|
||||||
|
}
|
||||||
|
// 传送遍历全部场景
|
||||||
|
for (const s of ["bakery", "store", "clinic", "lake", "field", "well", "fogwall", "white_corridor", "white_ward", "plaza", "house"]) {
|
||||||
|
await page.click(`[data-tp="${s}"]`);
|
||||||
|
await sleep(900);
|
||||||
|
await shot(`06_scene_${s}`);
|
||||||
|
}
|
||||||
|
// 四结局强制触发
|
||||||
|
for (const e of ["E1", "E4", "E2", "E3"]) {
|
||||||
|
await page.click(`[data-end="${e}"]`);
|
||||||
|
await sleep(e === "E3" ? 14000 : 6000);
|
||||||
|
await shot(`07_ending_${e}`);
|
||||||
|
// 回到游戏继续测试(reload 后需重进)
|
||||||
|
if (e !== "E3") {
|
||||||
|
await page.reload();
|
||||||
|
await sleep(2200);
|
||||||
|
await page.fill("#name-input", "测试员");
|
||||||
|
await page.press("#name-input", "Enter");
|
||||||
|
await sleep(1800);
|
||||||
|
await page.keyboard.press("`");
|
||||||
|
await sleep(300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("CONSOLE ERRORS:", errors.length);
|
||||||
|
errors.slice(0, 20).forEach(e => console.log(" -", e));
|
||||||
|
await browser.close();
|
||||||
|
await server.close();
|
||||||
|
process.exit(errors.length ? 1 : 0);
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// 门往返专项:每条通道双向走一遍,确认到达后 1.2 秒内不被弹回(防出生点压住触发区)
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { createServer } from "vite";
|
||||||
|
const server = await createServer({ server: { port: 5193 } });
|
||||||
|
await server.listen();
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||||
|
const errors = [];
|
||||||
|
page.on("console", m => { if (m.type() === "error") errors.push(m.text()); });
|
||||||
|
page.on("pageerror", e => errors.push(e.message));
|
||||||
|
const sleep = ms => page.waitForTimeout(ms);
|
||||||
|
const tp = (x, y) => page.evaluate(([x, y]) => {
|
||||||
|
const sc = window.__game.scene.getScenes(true).find(s => s.player);
|
||||||
|
sc.player.setPosition(x, y); sc.player.body.reset(x, y);
|
||||||
|
}, [x, y]);
|
||||||
|
const cur = () => page.evaluate(() => window.__director.currentScene);
|
||||||
|
const failures = [];
|
||||||
|
// 沿指定方向持续走 ms 毫秒(模拟真实按键走进出口)
|
||||||
|
const walk = async (key, ms) => { await page.keyboard.down(key); await sleep(ms); await page.keyboard.up(key); };
|
||||||
|
|
||||||
|
await page.goto("http://localhost:5193/");
|
||||||
|
await sleep(2500);
|
||||||
|
await page.fill("#name-input", "门测");
|
||||||
|
await page.press("#name-input", "Enter");
|
||||||
|
await sleep(1800);
|
||||||
|
|
||||||
|
// [出发场景, 门口站位, 行走方向, 目标场景]
|
||||||
|
const LEGS = [
|
||||||
|
["house", [88, 110], "s", "plaza"], // 小屋 → 广场
|
||||||
|
["plaza", [72, 90], "w", "house"], // 广场 → 小屋
|
||||||
|
["house", [88, 110], "s", "plaza"],
|
||||||
|
["plaza", [152, 90], "w", "bakery"], // 广场 → 面包房
|
||||||
|
["bakery", [128, 140], "s", "plaza"],
|
||||||
|
["plaza", [344, 90], "w", "store"], // 广场 → 杂货店
|
||||||
|
["store", [128, 140], "s", "plaza"],
|
||||||
|
["plaza", [424, 90], "w", "clinic"], // 广场 → 诊所
|
||||||
|
["clinic", [96, 110], "s", "plaza"],
|
||||||
|
["plaza", [20, 148], "a", "lake"], // 广场 → 湖畔
|
||||||
|
["lake", [360, 120], "d", "plaza"], // 湖畔 → 广场
|
||||||
|
["plaza", [462, 148], "d", "field"], // 广场 → 花田
|
||||||
|
["field", [24, 104], "a", "plaza"], // 花田 → 广场
|
||||||
|
["plaza", [232, 24], "w", "well"], // 广场 → 古井
|
||||||
|
["well", [112, 136], "s", "plaza"], // 古井 → 广场
|
||||||
|
["plaza", [232, 296], "s", "fogwall"], // 广场 → 雾墙
|
||||||
|
["fogwall", [168, 30], "w", "plaza"], // 雾墙 → 广场(用户报告的死循环路径)
|
||||||
|
];
|
||||||
|
for (const [from, [x, y], dir, to] of LEGS) {
|
||||||
|
const at = await cur();
|
||||||
|
if (at !== from) { console.log(`FAIL 前置不在 ${from}(在 ${at})`); failures.push(from + "->" + to); continue; }
|
||||||
|
await tp(x, y); await sleep(250);
|
||||||
|
await walk(dir, 900);
|
||||||
|
let arrived = false;
|
||||||
|
for (let i = 0; i < 20; i++) { await sleep(150); if ((await cur()) === to) { arrived = true; break; } }
|
||||||
|
await sleep(1200); // 到达后必须站得住
|
||||||
|
const settled = (await cur()) === to;
|
||||||
|
console.log(`${arrived && settled ? "PASS" : "FAIL"} ${from} → ${to}${arrived && !settled ? "(被弹回!)" : ""}`);
|
||||||
|
if (!(arrived && settled)) failures.push(from + "->" + to);
|
||||||
|
}
|
||||||
|
console.log("CONSOLE ERRORS:", errors.length);
|
||||||
|
console.log("FAILURES:", failures.length, failures.join(" | "));
|
||||||
|
await browser.close(); await server.close();
|
||||||
|
process.exit(errors.length || failures.length ? 1 : 0);
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
// 七日全流程自动化验收:任务/线索/悲伤行为/黄昏/梦境/结局 + 控制台零报错
|
||||||
|
// 运行:node tools/e2e_flow.mjs
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { createServer } from "vite";
|
||||||
|
import { mkdirSync } from "fs";
|
||||||
|
|
||||||
|
const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-flow";
|
||||||
|
mkdirSync(SHOT_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const server = await createServer({ server: { port: 5198 } });
|
||||||
|
await server.listen();
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||||
|
const errors = [];
|
||||||
|
page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
|
||||||
|
page.on("pageerror", (e) => errors.push("PAGEERROR: " + e.message));
|
||||||
|
|
||||||
|
const sleep = (ms) => page.waitForTimeout(ms);
|
||||||
|
// 无头环境下 press() 的 down/up 同帧到达会吞掉 JustDown,改为跨帧按键
|
||||||
|
const pressE = async () => { await page.keyboard.down("e"); await sleep(90); await page.keyboard.up("e"); };
|
||||||
|
const shot = (n) => page.screenshot({ path: `${SHOT_DIR}/${n}.png` });
|
||||||
|
const D = (expr) => page.evaluate(`(() => { const d = window.__director; return ${expr}; })()`);
|
||||||
|
const failures = [];
|
||||||
|
const check = async (label, expr) => {
|
||||||
|
const ok = await D(expr);
|
||||||
|
console.log(`${ok ? "PASS" : "FAIL"} ${label}`);
|
||||||
|
if (!ok) {
|
||||||
|
failures.push(label);
|
||||||
|
const diag = await page.evaluate(() => {
|
||||||
|
const d = window.__director;
|
||||||
|
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
|
||||||
|
return `day=${d.day} scene=${d.currentScene} wipes=${d.wipes} clues=[${[...d.clues]}] cutscene=${d.cutscene} dialog=${window.__game.scene.getScene("ui").dialogOpen} player=${sc ? Math.round(sc.player.x) + "," + Math.round(sc.player.y) : "-"} trans=${sc ? sc.transitioning : "-"}`;
|
||||||
|
});
|
||||||
|
console.log(" diag:", diag);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 把玩家瞬移到当前场景某像素点
|
||||||
|
const tp = (x, y) => page.evaluate(([x, y]) => {
|
||||||
|
const g = window.__game;
|
||||||
|
const sc = g.scene.getScenes(true).find((s) => s.player);
|
||||||
|
sc.player.setPosition(x, y);
|
||||||
|
sc.player.body.reset(x, y);
|
||||||
|
}, [x, y]);
|
||||||
|
const curScene = () => page.evaluate(() => window.__director.currentScene);
|
||||||
|
// 连按 E 直到对话结束
|
||||||
|
const talk = async (times = 14) => {
|
||||||
|
for (let i = 0; i < times; i++) {
|
||||||
|
await pressE();
|
||||||
|
await sleep(160);
|
||||||
|
const open = await page.evaluate(() => window.__game.scene.getScene("ui").dialogOpen);
|
||||||
|
if (!open) break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const interactAt = async (x, y, presses = 14) => {
|
||||||
|
await tp(x, y); await sleep(350);
|
||||||
|
await pressE(); await sleep(300);
|
||||||
|
await talk(presses);
|
||||||
|
await sleep(200);
|
||||||
|
};
|
||||||
|
// 走进门(瞬移到门内并等场景切换)
|
||||||
|
const enterDoor = async (x, y, expectScene) => {
|
||||||
|
await tp(x, y);
|
||||||
|
for (let i = 0; i < 30; i++) { await sleep(200); if ((await curScene()) === expectScene) break; }
|
||||||
|
const ok = (await curScene()) === expectScene;
|
||||||
|
console.log(`${ok ? "PASS" : "FAIL"} 进入 ${expectScene}`);
|
||||||
|
if (!ok) {
|
||||||
|
failures.push("进入 " + expectScene);
|
||||||
|
const diag = await page.evaluate(() => {
|
||||||
|
const d = window.__director;
|
||||||
|
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
|
||||||
|
return `scene=${d.currentScene} cutscene=${d.cutscene} dialog=${window.__game.scene.getScene("ui").dialogOpen} player=${sc ? Math.round(sc.player.x) + "," + Math.round(sc.player.y) : "-"} trans=${sc ? sc.transitioning : "-"}`;
|
||||||
|
});
|
||||||
|
console.log(" diag:", diag);
|
||||||
|
}
|
||||||
|
await sleep(400);
|
||||||
|
};
|
||||||
|
const sleepToNextDay = async () => {
|
||||||
|
await enterDoor(72, 68, "house"); // 回小屋(从广场小屋门)
|
||||||
|
await interactAt(30, 70, 6); // 床
|
||||||
|
await sleep(14500); // 梦境过场
|
||||||
|
await sleep(1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.goto("http://localhost:5198/");
|
||||||
|
await sleep(2500);
|
||||||
|
await page.fill("#name-input", "验收员");
|
||||||
|
await page.press("#name-input", "Enter");
|
||||||
|
await sleep(1800);
|
||||||
|
|
||||||
|
// ===== Day 1 =====
|
||||||
|
await check("D1 开局幸福 30", "d.day===1 && d.happiness===30 && d.awareness===0");
|
||||||
|
await interactAt(30, 40, 4); // 日历(D1 无线索)
|
||||||
|
await check("D1 日历尚无 C1", "!d.clues.has('C1')");
|
||||||
|
await enterDoor(88, 122, "plaza"); // 出小屋
|
||||||
|
await interactAt(188, 126); // 公告板
|
||||||
|
await check("D1 公告已读", "d.announced===true");
|
||||||
|
// T2 浇水 ×3
|
||||||
|
await interactAt(120, 198); await interactAt(120, 230); await interactAt(120, 262);
|
||||||
|
await check("T2 完成 +10", "d.questStatus['T2']==='done'");
|
||||||
|
// T1 取面包
|
||||||
|
await enterDoor(152, 70, "bakery");
|
||||||
|
await interactAt(72, 52);
|
||||||
|
await check("T1 拿到面包", "d.breadHeld===3");
|
||||||
|
await enterDoor(120, 154, "plaza");
|
||||||
|
await interactAt(264, 66); // 大宝
|
||||||
|
await enterDoor(344, 70, "store");
|
||||||
|
await interactAt(56, 78); // 咪咪
|
||||||
|
await enterDoor(120, 154, "plaza");
|
||||||
|
await enterDoor(6, 148, "lake");
|
||||||
|
await interactAt(292, 96); // 龟爷爷
|
||||||
|
await check("T1 完成", "d.questStatus['T1']==='done'");
|
||||||
|
await check("T3 完成(问好×3)", "d.questStatus['T3']==='done'");
|
||||||
|
await check("D1 黄昏已触发", "d.dusk===true");
|
||||||
|
await shot("d1_dusk");
|
||||||
|
await enterDoor(378, 112, "plaza"); // 湖畔右缘回广场
|
||||||
|
await sleepToNextDay();
|
||||||
|
|
||||||
|
// ===== Day 2 =====
|
||||||
|
await check("进入 Day 2", "d.day===2");
|
||||||
|
await interactAt(30, 40, 6); // 日历 C1
|
||||||
|
await check("C1 获得(觉知+8)", "d.clues.has('C1') && d.awareness>=8");
|
||||||
|
await enterDoor(88, 122, "plaza");
|
||||||
|
await interactAt(188, 126); // 公告
|
||||||
|
await interactAt(280, 46, 8); // 邮筒信件 C3
|
||||||
|
await check("C3 获得", "d.clues.has('C3')");
|
||||||
|
await interactAt(264, 66); // 大宝取信
|
||||||
|
await check("T4 拿到信", "d.lettersHeld===3");
|
||||||
|
await enterDoor(152, 70, "bakery");
|
||||||
|
await interactAt(72, 52); // 圆圆收信
|
||||||
|
await enterDoor(120, 154, "plaza");
|
||||||
|
await enterDoor(344, 70, "store");
|
||||||
|
await interactAt(56, 78); // 咪咪收信
|
||||||
|
// T5 清点货架
|
||||||
|
await interactAt(56, 46); await interactAt(104, 46); await interactAt(152, 46, 8);
|
||||||
|
await check("T5 完成", "d.questStatus['T5']==='done'");
|
||||||
|
await interactAt(198, 92, 8); // C5 货架
|
||||||
|
await check("C5 获得", "d.clues.has('C5')");
|
||||||
|
await enterDoor(120, 154, "plaza");
|
||||||
|
await enterDoor(424, 70, "clinic");
|
||||||
|
await interactAt(104, 46, 10); // 芙医生收第3封信 → C4
|
||||||
|
await check("T4 完成 & C4 获得", "d.questStatus['T4']==='done' && d.clues.has('C4')");
|
||||||
|
await enterDoor(88, 122, "plaza");
|
||||||
|
await sleepToNextDay();
|
||||||
|
|
||||||
|
// ===== Day 3 =====
|
||||||
|
await check("进入 Day 3", "d.day===3");
|
||||||
|
await enterDoor(88, 122, "plaza");
|
||||||
|
await interactAt(188, 126);
|
||||||
|
// T6 采莓果
|
||||||
|
await enterDoor(470, 148, "field");
|
||||||
|
for (const [x, y] of [[72, 82], [136, 98], [200, 82], [248, 130], [104, 162]]) await interactAt(x, y, 4);
|
||||||
|
await check("T6 完成", "d.questStatus['T6']==='done'");
|
||||||
|
await enterDoor(4, 100, "plaza");
|
||||||
|
// T7 野餐
|
||||||
|
await enterDoor(6, 148, "lake");
|
||||||
|
await interactAt(128, 118, 10);
|
||||||
|
await check("T7 完成", "d.questStatus['T7']==='done'");
|
||||||
|
await shot("d3_lake");
|
||||||
|
// 面包房看配方 C2
|
||||||
|
await enterDoor(378, 112, "plaza");
|
||||||
|
await enterDoor(152, 70, "bakery");
|
||||||
|
await interactAt(120, 78, 8);
|
||||||
|
await check("C2 获得", "d.clues.has('C2')");
|
||||||
|
await enterDoor(120, 154, "plaza");
|
||||||
|
await sleepToNextDay();
|
||||||
|
|
||||||
|
// ===== Day 4(完美的重复) =====
|
||||||
|
await check("进入 Day 4", "d.day===4");
|
||||||
|
await enterDoor(88, 122, "plaza");
|
||||||
|
await interactAt(188, 126);
|
||||||
|
await interactAt(120, 198); await interactAt(120, 230); await interactAt(120, 262);
|
||||||
|
await check("T9 完成", "d.questStatus['T9']==='done'");
|
||||||
|
await enterDoor(152, 70, "bakery");
|
||||||
|
await interactAt(72, 52);
|
||||||
|
await enterDoor(120, 154, "plaza");
|
||||||
|
await interactAt(264, 66);
|
||||||
|
await enterDoor(344, 70, "store");
|
||||||
|
await interactAt(56, 78);
|
||||||
|
await enterDoor(120, 154, "plaza");
|
||||||
|
await enterDoor(6, 148, "lake");
|
||||||
|
await interactAt(292, 96, 8); // 龟爷爷(送面包)
|
||||||
|
await interactAt(292, 96, 8); // 再谈一次(C9 第二段)
|
||||||
|
await check("T8 完成", "d.questStatus['T8']==='done'");
|
||||||
|
await check("T10 完成", "d.questStatus['T10']==='done'");
|
||||||
|
await check("C9 链已记 D2/D4", "d.guiChain.has(4)");
|
||||||
|
await enterDoor(378, 112, "plaza");
|
||||||
|
await sleepToNextDay();
|
||||||
|
|
||||||
|
// ===== Day 5(自由日 + 古井/雾墙开闸) =====
|
||||||
|
await check("进入 Day 5", "d.day===5");
|
||||||
|
await enterDoor(88, 122, "plaza");
|
||||||
|
await interactAt(188, 126); // 公告
|
||||||
|
// T11 接受→揉面
|
||||||
|
await enterDoor(152, 70, "bakery");
|
||||||
|
await tp(72, 52); await sleep(300);
|
||||||
|
await pressE(); await sleep(500); // 打开对话
|
||||||
|
await talk(4); // 读完问句 → 进入选项
|
||||||
|
await pressE(); await sleep(300); // 选第一项:帮忙
|
||||||
|
await talk(6);
|
||||||
|
await check("T11 已接受", "d.t11Accepted===true");
|
||||||
|
await interactAt(168, 102, 8); // 揉面台
|
||||||
|
await check("T11 完成", "d.questStatus['T11']==='done'");
|
||||||
|
await enterDoor(120, 154, "plaza");
|
||||||
|
// T12 擦公告板(第2次浮现 C6)
|
||||||
|
await tp(188, 126); await sleep(300);
|
||||||
|
await pressE(); await sleep(400);
|
||||||
|
await talk(3); await pressE(); await sleep(300); // 选:擦拭
|
||||||
|
await talk(4);
|
||||||
|
await interactAt(188, 126, 8); // 第2次 → C6
|
||||||
|
await check("C6 获得", "d.clues.has('C6')");
|
||||||
|
await interactAt(188, 126, 8); // 第3次 → 完成
|
||||||
|
await check("T12 完成", "d.questStatus['T12']==='done'");
|
||||||
|
// 古井 C7
|
||||||
|
await enterDoor(232, 4, "well");
|
||||||
|
await interactAt(104, 92, 10);
|
||||||
|
await sleep(5600); // 注视古井 5 秒
|
||||||
|
await check("C7 获得", "d.clues.has('C7')");
|
||||||
|
await check("注视古井 +3 觉知", "d.stared.has('well')");
|
||||||
|
// 诊所 C8
|
||||||
|
await enterDoor(104, 148, "plaza");
|
||||||
|
await enterDoor(424, 70, "clinic");
|
||||||
|
await interactAt(76, 74, 8);
|
||||||
|
await check("C8 获得", "d.clues.has('C8')");
|
||||||
|
await enterDoor(88, 122, "plaza");
|
||||||
|
await sleepToNextDay();
|
||||||
|
|
||||||
|
// ===== Day 6(加速 + T14 + 雨) =====
|
||||||
|
await check("进入 Day 6", "d.day===6");
|
||||||
|
await enterDoor(88, 122, "plaza");
|
||||||
|
await interactAt(188, 126);
|
||||||
|
// 龟爷爷 C9 第三段
|
||||||
|
await enterDoor(6, 148, "lake");
|
||||||
|
await interactAt(292, 96, 10);
|
||||||
|
await check("C9 获得(D6 第三段)", "d.clues.has('C9')");
|
||||||
|
await enterDoor(378, 112, "plaza");
|
||||||
|
// T13 挂彩带 ×4
|
||||||
|
for (const [x, y] of [[188, 190], [300, 190], [188, 270], [300, 270]]) {
|
||||||
|
await interactAt(x, y);
|
||||||
|
console.log(" ribbons:", await D("[...d.ribbons].join('|')"));
|
||||||
|
}
|
||||||
|
await check("T13 完成", "d.questStatus['T13']==='done'");
|
||||||
|
// T14 保持微笑 60 秒(把计时快进以免真等 60s)
|
||||||
|
await interactAt(240, 210, 4); // 启动
|
||||||
|
await sleep(1500);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
|
||||||
|
sc.t14Start -= 61000; // 快进
|
||||||
|
});
|
||||||
|
await sleep(1500);
|
||||||
|
await talk(6);
|
||||||
|
await check("T14 完成", "d.questStatus['T14']==='done'");
|
||||||
|
await check("D6 黄昏(脚本雨)", "d.dusk===true");
|
||||||
|
await sleep(1000); await shot("d6_rain");
|
||||||
|
// 雨中站立 10 秒 → 悲伤行为
|
||||||
|
const hpBefore = await D("d.happiness");
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
|
||||||
|
sc.rainStillAt -= 11000;
|
||||||
|
});
|
||||||
|
await sleep(800);
|
||||||
|
const hpAfter = await D("d.happiness");
|
||||||
|
console.log(`${hpAfter <= hpBefore ? "PASS" : "FAIL"} 雨中站立触发悲伤行为(${hpBefore}→${hpAfter})`);
|
||||||
|
if (hpAfter > hpBefore) failures.push("rain sadact");
|
||||||
|
await sleepToNextDay();
|
||||||
|
|
||||||
|
// ===== Day 7(分歧日:E3 苏醒线) =====
|
||||||
|
await check("进入 Day 7", "d.day===7");
|
||||||
|
await check("线索 ≥6", "d.clues.size>=6");
|
||||||
|
await D("d.setAwareness(80)"); // 觉知补足(自然流程中来自更多迟疑选项/发呆)
|
||||||
|
await enterDoor(88, 122, "plaza");
|
||||||
|
await sleep(1500);
|
||||||
|
await check("庆典已开始", "d.celebrationStarted===true");
|
||||||
|
await shot("d7_festival");
|
||||||
|
// 走向雾墙 → E3
|
||||||
|
await enterDoor(232, 312, "fogwall");
|
||||||
|
await interactAt(160, 82, 8);
|
||||||
|
await sleep(3500); // 白闪 → 白色走廊
|
||||||
|
await check("进入白色走廊(E3 模式)", "d.e3Mode===true && d.currentScene==='white_corridor'");
|
||||||
|
await tp(312, 60); await sleep(1500); // 走到走廊尽头
|
||||||
|
await check("进入病房", "d.currentScene==='white_ward'");
|
||||||
|
await interactAt(80, 72, 6); // 病历
|
||||||
|
await sleep(16000);
|
||||||
|
await shot("d7_e3_final");
|
||||||
|
|
||||||
|
console.log("CONSOLE ERRORS:", errors.length);
|
||||||
|
errors.slice(0, 20).forEach((e) => console.log(" -", e));
|
||||||
|
console.log("FAILURES:", failures.length);
|
||||||
|
await browser.close();
|
||||||
|
await server.close();
|
||||||
|
process.exit(errors.length || failures.length ? 1 : 0);
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// 机制专项:悲伤行为 5 项 / 强制欢乐庆典与逃离 / S4 文本腐蚀 / E4·E1 条件判定
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { createServer } from "vite";
|
||||||
|
import { mkdirSync } from "fs";
|
||||||
|
const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-mech";
|
||||||
|
mkdirSync(SHOT_DIR, { recursive: true });
|
||||||
|
const server = await createServer({ server: { port: 5195 } });
|
||||||
|
await server.listen();
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||||
|
const errors = [];
|
||||||
|
page.on("console", m => { if (m.type() === "error") errors.push(m.text()); });
|
||||||
|
page.on("pageerror", e => errors.push("PAGEERROR: " + e.message));
|
||||||
|
const sleep = ms => page.waitForTimeout(ms);
|
||||||
|
const pressE = async () => { await page.keyboard.down("e"); await sleep(90); await page.keyboard.up("e"); };
|
||||||
|
const D = expr => page.evaluate(`(() => { const d = window.__director; return ${expr}; })()`);
|
||||||
|
const failures = [];
|
||||||
|
const check = async (label, expr) => {
|
||||||
|
const ok = await D(expr);
|
||||||
|
console.log(`${ok ? "PASS" : "FAIL"} ${label}`);
|
||||||
|
if (!ok) failures.push(label);
|
||||||
|
};
|
||||||
|
const tp = (x, y) => page.evaluate(([x, y]) => {
|
||||||
|
const sc = window.__game.scene.getScenes(true).find(s => s.player);
|
||||||
|
sc.player.setPosition(x, y); sc.player.body.reset(x, y);
|
||||||
|
}, [x, y]);
|
||||||
|
const chooseOption = async (idx) => {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const cm = await page.evaluate(() => window.__game.scene.getScene("ui").choiceMode);
|
||||||
|
if (cm) break;
|
||||||
|
await pressE(); await sleep(220);
|
||||||
|
}
|
||||||
|
await page.evaluate((i) => {
|
||||||
|
const ui = window.__game.scene.getScene("ui");
|
||||||
|
ui.choiceIdx = i; ui.renderChoiceSel();
|
||||||
|
}, idx);
|
||||||
|
await pressE(); await sleep(300);
|
||||||
|
};
|
||||||
|
const talk = async (n = 14) => {
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
await pressE(); await sleep(160);
|
||||||
|
if (!(await page.evaluate(() => window.__game.scene.getScene("ui").dialogOpen))) break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.goto("http://localhost:5195/");
|
||||||
|
await sleep(2500);
|
||||||
|
await page.fill("#name-input", "小满");
|
||||||
|
await page.press("#name-input", "Enter");
|
||||||
|
await sleep(1800);
|
||||||
|
|
||||||
|
// 出小屋到广场
|
||||||
|
await tp(88, 122); await sleep(1200);
|
||||||
|
|
||||||
|
// ① 否定选项(咪咪 D1 有三选项)
|
||||||
|
await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); s.scene.start("store"); window.__director.pendingSpawn = "entry"; });
|
||||||
|
await sleep(900);
|
||||||
|
let h0 = await D("d.happiness"), a0 = await D("d.awareness");
|
||||||
|
await tp(56, 78); await sleep(350);
|
||||||
|
await pressE(); await sleep(300);
|
||||||
|
await chooseOption(2); // 第三项:否定
|
||||||
|
await talk(4);
|
||||||
|
await check(`否定选项:幸福-5 觉知+3(${h0}→)`, `d.happiness===${h0}-5 && d.awareness===${a0}+3`);
|
||||||
|
|
||||||
|
// ② 纠缠:同一 NPC 一天 ≥5 次
|
||||||
|
h0 = await D("d.happiness");
|
||||||
|
for (let i = 0; i < 5; i++) { await tp(56, 78); await sleep(250); await pressE(); await sleep(250); await talk(6); }
|
||||||
|
await check("纠缠:第5次对话触发悲伤行为", `d.happiness<=${h0} - 5 + 10`); // 对话有选项可能+2,宽松判定
|
||||||
|
const nag = await D("d.nagged.has('mimi')");
|
||||||
|
console.log(`${nag ? "PASS" : "FAIL"} 纠缠已记录`); if (!nag) failures.push("nag");
|
||||||
|
|
||||||
|
// ③ 发呆:10 秒不动且不在任务点
|
||||||
|
await page.evaluate(() => { window.__director.pendingSpawn = "center"; const s = window.__game.scene.getScenes(true).find(x => x.player); s.scene.start("plaza"); });
|
||||||
|
await sleep(900);
|
||||||
|
await tp(240, 130); // 空地
|
||||||
|
h0 = await D("d.happiness");
|
||||||
|
await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); s.lastMoveAt -= 11000; });
|
||||||
|
await sleep(600);
|
||||||
|
await check(`发呆:幸福-5(${h0}→)`, `d.happiness===${h0}-5`);
|
||||||
|
|
||||||
|
// ④ 拒绝任务(D5 T11)
|
||||||
|
await page.evaluate(() => { window.__director.startDay(5); });
|
||||||
|
await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); window.__director.pendingSpawn = "entry"; s.scene.start("bakery"); });
|
||||||
|
await sleep(900);
|
||||||
|
await tp(72, 52); await sleep(350);
|
||||||
|
await pressE(); await sleep(300);
|
||||||
|
await chooseOption(1); // 第二项:拒绝
|
||||||
|
await talk(4);
|
||||||
|
await check("拒绝任务:T11 refused 且被记录", "d.questStatus['T11']==='refused' && d.refusals===1");
|
||||||
|
|
||||||
|
// ⑤ 强制欢乐庆典(幸福<30 持续 2 分钟)+ 逃离计数
|
||||||
|
await D("d.setHappiness(10)");
|
||||||
|
await page.evaluate(() => { window.__director["lowSince"] = performance.now() - 121000; });
|
||||||
|
await sleep(800);
|
||||||
|
await check("欢乐庆典触发", "d.celebrationActive===true");
|
||||||
|
await page.screenshot({ path: SHOT_DIR + "/celebration.png" });
|
||||||
|
// 从场景边缘走离逃脱
|
||||||
|
await tp(120, 154); await sleep(1500);
|
||||||
|
await check("逃离欢乐庆典计数", "d.celebrationActive===false && d.escapes===1 && d.happiness===10");
|
||||||
|
// 不逃离 → 拉回 50
|
||||||
|
await page.evaluate(() => { window.__director["lowSince"] = performance.now() - 121000; });
|
||||||
|
await sleep(900);
|
||||||
|
await check("再次触发", "d.celebrationActive===true");
|
||||||
|
await sleep(8500);
|
||||||
|
await check("未逃离:幸福拉回 ~50", "d.happiness>=45 && d.celebrationActive===false && d.escapes===1");
|
||||||
|
|
||||||
|
// ⑥ S4 咪咪文本腐蚀(错字+句尾标点×3)
|
||||||
|
await D("d.setHappiness(100)");
|
||||||
|
await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); window.__director.pendingSpawn = "entry"; s.scene.start("store"); });
|
||||||
|
await sleep(900);
|
||||||
|
await D("d.startDay(3)"); // D3 咪咪台词密集含可腐蚀词
|
||||||
|
await D("d.setHappiness(100)");
|
||||||
|
await tp(56, 78); await sleep(350);
|
||||||
|
await pressE(); await sleep(1200);
|
||||||
|
await pressE(); await sleep(1600); // 等台词打完
|
||||||
|
const line = await page.evaluate(() => window.__game.scene.getScene("ui").lineText.text);
|
||||||
|
console.log(" 咪咪 S4 台词:", line);
|
||||||
|
const corrupted = /辛福|开欣|美妤|大伽|微效|名天|恬|央光|棚友|泳远/.test(line) || /[。!?]{3}/.test(line);
|
||||||
|
console.log(`${corrupted ? "PASS" : "FAIL"} S4 咪咪台词出现腐蚀`);
|
||||||
|
if (!corrupted) failures.push("S4 corruption");
|
||||||
|
await page.screenshot({ path: SHOT_DIR + "/mimi_s4.png" });
|
||||||
|
|
||||||
|
// ⑦ E1/E4 条件判定(decideEnding 逻辑)
|
||||||
|
await check("E1 判定", "d.decideEnding(false)==='E1' || d.awareness>29 ? true : d.decideEnding(false)==='E1'");
|
||||||
|
const e1 = await page.evaluate(() => { const d = window.__director; d.setAwareness(0); d.setHappiness(95); return d.decideEnding(false); });
|
||||||
|
console.log(`${e1 === "E1" ? "PASS" : "FAIL"} 觉知≤29 且幸福≥90 → E1(得 ${e1})`); if (e1 !== "E1") failures.push("E1");
|
||||||
|
const e2 = await page.evaluate(() => { const d = window.__director; d.setAwareness(50); return d.decideEnding(false); });
|
||||||
|
console.log(`${e2 === "E2" ? "PASS" : "FAIL"} 觉知 30-69 → E2(得 ${e2})`); if (e2 !== "E2") failures.push("E2");
|
||||||
|
const e4 = await page.evaluate(() => { const d = window.__director; d.e4ok = true; d.escapes = 3; return d.decideEnding(false); });
|
||||||
|
console.log(`${e4 === "E4" ? "PASS" : "FAIL"} 静默条件 → E4(得 ${e4})`); if (e4 !== "E4") failures.push("E4");
|
||||||
|
const e3 = await page.evaluate(() => { const d = window.__director; d.e4ok = false; d.setAwareness(80); for (let i=1;i<=6;i++) d.clues.add("C"+i); return d.decideEnding(true); });
|
||||||
|
console.log(`${e3 === "E3" ? "PASS" : "FAIL"} 觉知≥70 线索≥6 走向雾墙 → E3(得 ${e3})`); if (e3 !== "E3") failures.push("E3");
|
||||||
|
|
||||||
|
console.log("CONSOLE ERRORS:", errors.length);
|
||||||
|
errors.slice(0, 10).forEach(e => console.log(" -", e));
|
||||||
|
console.log("FAILURES:", failures.length, failures.join(" | "));
|
||||||
|
await browser.close(); await server.close();
|
||||||
|
process.exit(errors.length || failures.length ? 1 : 0);
|
||||||
@@ -0,0 +1,757 @@
|
|||||||
|
-- 《崩坏童话:蜜糖村》全部像素素材生成脚本(Aseprite Lua)
|
||||||
|
-- 运行:aseprite -b --script tools/gen_art.lua
|
||||||
|
-- 输出:public/assets/*.png + art/*.aseprite(源文件)
|
||||||
|
-- 全部素材为本脚本逐像素绘制的原创内容,无任何外部素材。
|
||||||
|
|
||||||
|
local pc = app.pixelColor
|
||||||
|
local function C(hex, a)
|
||||||
|
a = a or 255
|
||||||
|
local r = tonumber(hex:sub(1, 2), 16)
|
||||||
|
local g = tonumber(hex:sub(3, 4), 16)
|
||||||
|
local b = tonumber(hex:sub(5, 6), 16)
|
||||||
|
return pc.rgba(r, g, b, a)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============ 糖果色板(村庄)+ 灰白色板(白色层) ============
|
||||||
|
local PAL = {
|
||||||
|
outline = C("5a4232"),
|
||||||
|
black = C("2a2420"),
|
||||||
|
white = C("ffffff"),
|
||||||
|
-- 草地/植物
|
||||||
|
grass = C("8fd05a"), grassD = C("79b94a"), grassL = C("a5e070"),
|
||||||
|
leaf = C("5da53f"), leafD = C("4a8a32"), trunk = C("8a5a34"), trunkD = C("6e4526"),
|
||||||
|
-- 路面/石
|
||||||
|
path = C("e8d5a8"), pathD = C("cbb488"), stone = C("c8c0a8"), stoneD = C("a8a088"),
|
||||||
|
-- 水
|
||||||
|
water = C("7ec8e8"), waterD = C("5fb0d8"), waterL = C("b8e6f8"),
|
||||||
|
-- 花
|
||||||
|
pink = C("ff9ecb"), red = C("e05858"), yellow = C("ffd94a"), berry = C("e8497a"),
|
||||||
|
-- 建筑
|
||||||
|
wall = C("fff1d0"), wallD = C("e8d8b0"), roof = C("f4924a"), roofD = C("d97733"),
|
||||||
|
wood = C("c98a4b"), woodD = C("a86a34"), woodL = C("e0aa6a"),
|
||||||
|
-- 室内
|
||||||
|
floor = C("e6c896"), floorD = C("d4b47e"), clinic = C("eaf2ec"), clinicD = C("d0e0d6"),
|
||||||
|
inwall = C("f6e8c8"), inwallD = C("e0cca0"),
|
||||||
|
-- 灰白层
|
||||||
|
wgray = C("e8e8ea"), wgrayD = C("cfcfd4"), wgrayDD = C("aeaeb6"), wdark = C("8a8a92"),
|
||||||
|
-- 其他
|
||||||
|
fog = C("e8ecf0"), fogD = C("cdd6de"),
|
||||||
|
metal = C("9aa4ac"), blue = C("5a88c8"), sky = C("8ecae6"),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ============ 绘制助手 ============
|
||||||
|
local function makeCtx(img, ox, oy)
|
||||||
|
local ctx = {}
|
||||||
|
function ctx.px(x, y, c) img:putPixel(ox + x, oy + y, c) end
|
||||||
|
function ctx.rect(x, y, w, h, c)
|
||||||
|
for j = y, y + h - 1 do for i = x, x + w - 1 do img:putPixel(ox + i, oy + j, c) end end
|
||||||
|
end
|
||||||
|
function ctx.hline(x, y, w, c) ctx.rect(x, y, w, 1, c) end
|
||||||
|
function ctx.vline(x, y, h, c) ctx.rect(x, y, 1, h, c) end
|
||||||
|
function ctx.dither(x, y, w, h, c, step)
|
||||||
|
step = step or 2
|
||||||
|
for j = y, y + h - 1 do for i = x, x + w - 1 do
|
||||||
|
if (i + j) % step == 0 then img:putPixel(ox + i, oy + j, c) end
|
||||||
|
end end
|
||||||
|
end
|
||||||
|
return ctx
|
||||||
|
end
|
||||||
|
|
||||||
|
local function saveSheet(img, name)
|
||||||
|
local spr = Sprite(img.width, img.height, ColorMode.RGB)
|
||||||
|
spr.cels[1].image = img
|
||||||
|
spr:saveAs("art/" .. name .. ".aseprite")
|
||||||
|
spr:saveCopyAs("public/assets/" .. name .. ".png")
|
||||||
|
spr:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============ 瓦片集:10 列 × 8 行 = 80 瓦(16×16) ============
|
||||||
|
local T = {}
|
||||||
|
|
||||||
|
T[0] = function(c) -- 草地1
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.px(3, 4, PAL.grassL); c.px(11, 9, PAL.grassL); c.px(6, 13, PAL.grassD); c.px(13, 2, PAL.grassD)
|
||||||
|
end
|
||||||
|
T[1] = function(c) -- 草地2(草茎)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.px(4, 5, PAL.grassD); c.px(4, 4, PAL.grassD); c.px(10, 10, PAL.grassD); c.px(10, 9, PAL.grassD)
|
||||||
|
c.px(13, 5, PAL.grassL); c.px(2, 12, PAL.grassL)
|
||||||
|
end
|
||||||
|
T[2] = function(c) -- 草地+白花
|
||||||
|
T[0](c); c.px(5, 6, PAL.white); c.px(4, 5, PAL.white); c.px(6, 5, PAL.white); c.px(5, 4, PAL.white); c.px(5, 5, PAL.yellow)
|
||||||
|
c.px(12, 12, PAL.white)
|
||||||
|
end
|
||||||
|
T[3] = function(c) -- 草地+粉花
|
||||||
|
T[1](c); c.px(11, 4, PAL.pink); c.px(10, 3, PAL.pink); c.px(12, 3, PAL.pink); c.px(11, 2, PAL.pink); c.px(11, 3, PAL.yellow)
|
||||||
|
c.px(3, 10, PAL.pink)
|
||||||
|
end
|
||||||
|
T[4] = function(c) -- 石板路
|
||||||
|
c.rect(0, 0, 16, 16, PAL.path)
|
||||||
|
c.hline(0, 7, 16, PAL.pathD); c.vline(7, 0, 8, PAL.pathD); c.vline(3, 8, 8, PAL.pathD); c.vline(12, 8, 8, PAL.pathD)
|
||||||
|
c.px(2, 3, PAL.pathD); c.px(10, 12, PAL.pathD)
|
||||||
|
end
|
||||||
|
T[5] = function(c) -- 石板路2
|
||||||
|
c.rect(0, 0, 16, 16, PAL.path)
|
||||||
|
c.hline(0, 3, 16, PAL.pathD); c.hline(0, 11, 16, PAL.pathD); c.vline(5, 4, 7, PAL.pathD); c.vline(11, 12, 4, PAL.pathD); c.vline(11, 0, 3, PAL.pathD)
|
||||||
|
end
|
||||||
|
T[6] = function(c) -- 路缘(草→路)
|
||||||
|
c.rect(0, 0, 16, 8, PAL.grass); c.rect(0, 8, 16, 8, PAL.path)
|
||||||
|
c.hline(0, 7, 16, PAL.grassD); c.hline(0, 8, 16, PAL.pathD)
|
||||||
|
c.px(3, 6, PAL.grassL); c.px(11, 12, PAL.pathD)
|
||||||
|
end
|
||||||
|
T[7] = function(c) -- 湖水1
|
||||||
|
c.rect(0, 0, 16, 16, PAL.water)
|
||||||
|
c.hline(2, 4, 4, PAL.waterL); c.hline(9, 10, 4, PAL.waterL); c.hline(5, 13, 3, PAL.waterD)
|
||||||
|
end
|
||||||
|
T[8] = function(c) -- 湖水2
|
||||||
|
c.rect(0, 0, 16, 16, PAL.water)
|
||||||
|
c.hline(8, 3, 4, PAL.waterD); c.hline(1, 9, 4, PAL.waterL); c.hline(11, 14, 3, PAL.waterL)
|
||||||
|
end
|
||||||
|
T[9] = function(c) -- 岸边(上草下水)
|
||||||
|
c.rect(0, 0, 16, 6, PAL.grass); c.rect(0, 6, 16, 10, PAL.water)
|
||||||
|
c.hline(0, 5, 16, PAL.grassD); c.hline(0, 6, 16, PAL.waterL)
|
||||||
|
end
|
||||||
|
T[10] = function(c) -- 花丛
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(1, 2, 14, 12, PAL.leaf)
|
||||||
|
c.px(1, 2, PAL.grass); c.px(14, 2, PAL.grass); c.px(1, 13, PAL.grass); c.px(14, 13, PAL.grass)
|
||||||
|
c.px(4, 5, PAL.pink); c.px(8, 4, PAL.yellow); c.px(12, 6, PAL.white); c.px(3, 9, PAL.yellow)
|
||||||
|
c.px(7, 8, PAL.pink); c.px(11, 10, PAL.pink); c.px(5, 12, PAL.white); c.px(10, 12, PAL.yellow)
|
||||||
|
end
|
||||||
|
T[11] = function(c) -- 莓果丛(有果)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 3, 12, 11, PAL.leaf); c.rect(3, 2, 10, 13, PAL.leaf)
|
||||||
|
c.dither(3, 3, 10, 10, PAL.leafD, 3)
|
||||||
|
c.px(5, 5, PAL.berry); c.px(10, 4, PAL.berry); c.px(7, 8, PAL.berry); c.px(11, 9, PAL.berry); c.px(4, 10, PAL.berry)
|
||||||
|
end
|
||||||
|
T[12] = function(c) -- 莓果丛(已采)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 3, 12, 11, PAL.leaf); c.rect(3, 2, 10, 13, PAL.leaf)
|
||||||
|
c.dither(3, 3, 10, 10, PAL.leafD, 3)
|
||||||
|
end
|
||||||
|
T[13] = function(c) -- 树·左上
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(4, 2, 12, 14, PAL.leaf)
|
||||||
|
c.px(4, 2, PAL.grass); c.px(5, 2, PAL.grass); c.px(4, 3, PAL.grass)
|
||||||
|
c.dither(5, 4, 11, 12, PAL.leafD, 4); c.px(8, 5, PAL.grassL); c.px(12, 8, PAL.grassL)
|
||||||
|
end
|
||||||
|
T[14] = function(c) -- 树·右上
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(0, 2, 12, 14, PAL.leaf)
|
||||||
|
c.px(11, 2, PAL.grass); c.px(10, 2, PAL.grass); c.px(11, 3, PAL.grass)
|
||||||
|
c.dither(0, 4, 11, 12, PAL.leafD, 4); c.px(4, 6, PAL.grassL)
|
||||||
|
end
|
||||||
|
T[15] = function(c) -- 树·左下
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(4, 0, 12, 6, PAL.leaf); c.dither(4, 0, 12, 6, PAL.leafD, 4)
|
||||||
|
c.px(4, 5, PAL.grass)
|
||||||
|
c.rect(10, 6, 6, 10, PAL.trunk); c.vline(10, 6, 10, PAL.trunkD)
|
||||||
|
end
|
||||||
|
T[16] = function(c) -- 树·右下
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(0, 0, 12, 6, PAL.leaf); c.dither(0, 0, 12, 6, PAL.leafD, 4)
|
||||||
|
c.px(11, 5, PAL.grass)
|
||||||
|
c.rect(0, 6, 6, 10, PAL.trunk); c.vline(5, 6, 10, PAL.trunkD)
|
||||||
|
end
|
||||||
|
T[17] = function(c) -- 小灌木
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(3, 5, 10, 9, PAL.leaf); c.rect(4, 4, 8, 11, PAL.leaf)
|
||||||
|
c.dither(4, 5, 8, 8, PAL.leafD, 3)
|
||||||
|
end
|
||||||
|
T[18] = function(c) -- 房屋外墙
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wall)
|
||||||
|
c.hline(0, 15, 16, PAL.wallD); c.vline(0, 0, 16, PAL.wallD)
|
||||||
|
c.px(4, 4, PAL.wallD); c.px(11, 9, PAL.wallD)
|
||||||
|
end
|
||||||
|
T[19] = function(c) -- 外墙+窗
|
||||||
|
T[18](c)
|
||||||
|
c.rect(4, 4, 8, 8, PAL.woodD); c.rect(5, 5, 6, 6, PAL.sky)
|
||||||
|
c.vline(7, 5, 6, PAL.woodD); c.hline(5, 8, 6, PAL.woodD)
|
||||||
|
c.px(6, 6, PAL.waterL)
|
||||||
|
end
|
||||||
|
T[20] = function(c) -- 门
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wall)
|
||||||
|
c.rect(3, 1, 10, 15, PAL.wood); c.rect(4, 2, 8, 14, PAL.woodL)
|
||||||
|
c.vline(4, 2, 14, PAL.wood); c.px(10, 9, PAL.woodD); c.hline(4, 2, 8, PAL.wood)
|
||||||
|
end
|
||||||
|
T[21] = function(c) -- 屋顶·左
|
||||||
|
c.rect(0, 0, 16, 16, PAL.roof)
|
||||||
|
c.hline(0, 0, 16, PAL.roofD); c.hline(0, 5, 16, PAL.roofD); c.hline(0, 10, 16, PAL.roofD); c.hline(0, 15, 16, PAL.roofD)
|
||||||
|
c.vline(0, 0, 16, PAL.roofD)
|
||||||
|
end
|
||||||
|
T[22] = function(c) -- 屋顶·右
|
||||||
|
c.rect(0, 0, 16, 16, PAL.roof)
|
||||||
|
c.hline(0, 0, 16, PAL.roofD); c.hline(0, 5, 16, PAL.roofD); c.hline(0, 10, 16, PAL.roofD); c.hline(0, 15, 16, PAL.roofD)
|
||||||
|
c.vline(15, 0, 16, PAL.roofD)
|
||||||
|
end
|
||||||
|
T[23] = function(c) -- 屋檐
|
||||||
|
c.rect(0, 0, 16, 8, PAL.roof); c.hline(0, 7, 16, PAL.roofD); c.hline(0, 6, 16, PAL.roofD)
|
||||||
|
c.rect(0, 8, 16, 8, PAL.wall); c.hline(0, 8, 16, PAL.wallD)
|
||||||
|
end
|
||||||
|
T[24] = function(c) -- 木地板
|
||||||
|
c.rect(0, 0, 16, 16, PAL.floor)
|
||||||
|
c.hline(0, 4, 16, PAL.floorD); c.hline(0, 9, 16, PAL.floorD); c.hline(0, 14, 16, PAL.floorD)
|
||||||
|
c.px(5, 2, PAL.floorD); c.px(12, 7, PAL.floorD); c.px(3, 12, PAL.floorD)
|
||||||
|
end
|
||||||
|
T[25] = function(c) -- 木地板2
|
||||||
|
c.rect(0, 0, 16, 16, PAL.floor)
|
||||||
|
c.hline(0, 2, 16, PAL.floorD); c.hline(0, 7, 16, PAL.floorD); c.hline(0, 12, 16, PAL.floorD)
|
||||||
|
c.px(9, 4, PAL.floorD); c.px(2, 9, PAL.floorD)
|
||||||
|
end
|
||||||
|
T[26] = function(c) -- 诊所地砖
|
||||||
|
c.rect(0, 0, 16, 16, PAL.clinic)
|
||||||
|
c.hline(0, 7, 16, PAL.clinicD); c.vline(7, 0, 16, PAL.clinicD); c.hline(0, 15, 16, PAL.clinicD); c.vline(15, 0, 16, PAL.clinicD)
|
||||||
|
end
|
||||||
|
T[27] = function(c) -- 室内墙
|
||||||
|
c.rect(0, 0, 16, 16, PAL.inwall)
|
||||||
|
c.dither(0, 0, 16, 16, PAL.inwallD, 8)
|
||||||
|
end
|
||||||
|
T[28] = function(c) -- 室内墙裙
|
||||||
|
c.rect(0, 0, 16, 10, PAL.inwall)
|
||||||
|
c.rect(0, 10, 16, 6, PAL.wood); c.hline(0, 10, 16, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[29] = function(c) -- 地毯
|
||||||
|
c.rect(0, 0, 16, 16, PAL.pink)
|
||||||
|
c.rect(1, 1, 14, 14, C("ffc4de")); c.rect(3, 3, 10, 10, PAL.pink)
|
||||||
|
c.px(7, 7, PAL.white); c.px(8, 8, PAL.white)
|
||||||
|
end
|
||||||
|
T[30] = function(c) -- 床头(枕头)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wood)
|
||||||
|
c.rect(1, 1, 14, 15, C("f8f4e8"))
|
||||||
|
c.rect(3, 3, 10, 7, PAL.white); c.hline(3, 9, 10, C("d8d4c8"))
|
||||||
|
c.hline(1, 0, 14, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[31] = function(c) -- 床尾(被子)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wood)
|
||||||
|
c.rect(1, 0, 14, 15, C("8ec8e0"))
|
||||||
|
c.hline(1, 3, 14, C("6fb0d0")); c.hline(1, 8, 14, C("6fb0d0")); c.hline(1, 13, 14, C("6fb0d0"))
|
||||||
|
c.hline(1, 15, 14, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[32] = function(c) -- 柜台
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wood)
|
||||||
|
c.rect(0, 0, 16, 6, PAL.woodL); c.hline(0, 6, 16, PAL.woodD)
|
||||||
|
c.vline(0, 0, 16, PAL.woodD); c.vline(15, 0, 16, PAL.woodD); c.hline(0, 15, 16, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[33] = function(c) -- 烤炉
|
||||||
|
c.rect(0, 0, 16, 16, PAL.stoneD)
|
||||||
|
c.rect(1, 1, 14, 14, PAL.stone)
|
||||||
|
c.rect(3, 6, 10, 7, PAL.black)
|
||||||
|
c.rect(4, 8, 8, 4, C("f4924a")); c.px(6, 9, PAL.yellow); c.px(9, 10, PAL.yellow)
|
||||||
|
c.hline(3, 3, 10, PAL.stoneD)
|
||||||
|
end
|
||||||
|
T[34] = function(c) -- 货架(空)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wood)
|
||||||
|
c.rect(1, 1, 14, 14, PAL.woodL)
|
||||||
|
c.hline(1, 5, 14, PAL.woodD); c.hline(1, 10, 14, PAL.woodD); c.hline(1, 14, 14, PAL.woodD)
|
||||||
|
c.vline(0, 0, 16, PAL.woodD); c.vline(15, 0, 16, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[35] = function(c) -- 货架(同一商品重复排列,标签空白)
|
||||||
|
T[34](c)
|
||||||
|
for _, y in ipairs({ 2, 7, 11 }) do
|
||||||
|
c.rect(2, y, 3, 3, PAL.berry); c.rect(6, y, 3, 3, PAL.berry); c.rect(10, y, 3, 3, PAL.berry)
|
||||||
|
c.px(3, y, PAL.white); c.px(7, y, PAL.white); c.px(11, y, PAL.white)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
T[36] = function(c) -- 桌子
|
||||||
|
c.rect(0, 0, 16, 16, PAL.floor)
|
||||||
|
c.rect(2, 3, 12, 9, PAL.woodL); c.hline(2, 11, 12, PAL.woodD)
|
||||||
|
c.vline(3, 12, 3, PAL.woodD); c.vline(12, 12, 3, PAL.woodD)
|
||||||
|
c.hline(2, 3, 12, PAL.wood)
|
||||||
|
end
|
||||||
|
T[37] = function(c) -- 椅子
|
||||||
|
c.rect(0, 0, 16, 16, PAL.floor)
|
||||||
|
c.rect(4, 3, 8, 3, PAL.wood)
|
||||||
|
c.rect(4, 6, 8, 6, PAL.woodL)
|
||||||
|
c.vline(4, 12, 3, PAL.woodD); c.vline(11, 12, 3, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[38] = function(c) -- 井·顶(小屋顶)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 2, 12, 6, PAL.roofD); c.rect(3, 1, 10, 2, PAL.roof)
|
||||||
|
c.vline(3, 8, 8, PAL.trunkD); c.vline(12, 8, 8, PAL.trunkD)
|
||||||
|
end
|
||||||
|
T[39] = function(c) -- 井·底(石圈)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 2, 12, 12, PAL.stone)
|
||||||
|
c.rect(4, 4, 8, 8, PAL.black)
|
||||||
|
c.px(3, 3, PAL.stoneD); c.px(12, 3, PAL.stoneD); c.px(3, 12, PAL.stoneD); c.px(12, 12, PAL.stoneD)
|
||||||
|
c.hline(2, 2, 12, PAL.stoneD)
|
||||||
|
end
|
||||||
|
T[40] = function(c) -- 雾(轻)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.fog)
|
||||||
|
c.dither(0, 0, 16, 16, PAL.fogD, 3)
|
||||||
|
end
|
||||||
|
T[41] = function(c) -- 雾(浓)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.fogD)
|
||||||
|
c.dither(0, 0, 16, 16, PAL.fog, 2); c.dither(1, 0, 16, 16, C("bcc8d2"), 5)
|
||||||
|
end
|
||||||
|
T[42] = function(c) -- 白色层·墙
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wgray)
|
||||||
|
c.hline(0, 12, 16, PAL.wgrayD); c.rect(0, 13, 16, 3, PAL.wgrayDD)
|
||||||
|
end
|
||||||
|
T[43] = function(c) -- 白色层·地
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wgrayD)
|
||||||
|
c.hline(0, 7, 16, PAL.wgrayDD); c.vline(7, 0, 16, PAL.wgrayDD)
|
||||||
|
end
|
||||||
|
T[44] = function(c) -- 白色层·门
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wgray)
|
||||||
|
c.rect(3, 1, 10, 15, PAL.wgrayDD); c.rect(4, 2, 8, 14, PAL.wgrayD)
|
||||||
|
c.px(10, 9, PAL.wdark)
|
||||||
|
end
|
||||||
|
T[45] = function(c) -- 长椅·左
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 4, 14, 3, PAL.woodL); c.rect(2, 7, 14, 4, PAL.wood)
|
||||||
|
c.vline(3, 11, 3, PAL.woodD); c.hline(2, 7, 14, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[46] = function(c) -- 长椅·右
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(0, 4, 14, 3, PAL.woodL); c.rect(0, 7, 14, 4, PAL.wood)
|
||||||
|
c.vline(12, 11, 3, PAL.woodD); c.hline(0, 7, 14, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[47] = function(c) -- 公告板·左
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 1, 14, 12, PAL.wood); c.rect(4, 3, 12, 8, C("f8f0d8"))
|
||||||
|
c.hline(5, 5, 8, PAL.stoneD); c.hline(5, 7, 6, PAL.stoneD); c.hline(5, 9, 9, PAL.stoneD)
|
||||||
|
c.vline(4, 13, 3, PAL.trunkD)
|
||||||
|
end
|
||||||
|
T[48] = function(c) -- 公告板·右
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(0, 1, 14, 12, PAL.wood); c.rect(0, 3, 12, 8, C("f8f0d8"))
|
||||||
|
c.hline(1, 5, 8, PAL.stoneD); c.hline(1, 7, 9, PAL.stoneD); c.hline(1, 9, 5, PAL.stoneD)
|
||||||
|
c.vline(11, 13, 3, PAL.trunkD)
|
||||||
|
end
|
||||||
|
T[49] = function(c) -- 邮筒
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(4, 2, 8, 10, PAL.red); c.rect(5, 1, 6, 2, C("c04040"))
|
||||||
|
c.rect(6, 5, 4, 2, PAL.black)
|
||||||
|
c.vline(6, 12, 3, C("c04040")); c.vline(9, 12, 3, C("c04040"))
|
||||||
|
end
|
||||||
|
T[50] = function(c) -- 栅栏·横
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.hline(0, 6, 16, PAL.woodL); c.hline(0, 7, 16, PAL.wood); c.hline(0, 10, 16, PAL.woodL); c.hline(0, 11, 16, PAL.wood)
|
||||||
|
c.rect(2, 4, 2, 10, PAL.wood); c.rect(12, 4, 2, 10, PAL.wood)
|
||||||
|
c.px(2, 4, PAL.woodL); c.px(12, 4, PAL.woodL)
|
||||||
|
end
|
||||||
|
T[51] = function(c) -- 栅栏·柱
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(6, 2, 3, 12, PAL.wood); c.px(6, 2, PAL.woodL); c.px(7, 2, PAL.woodL)
|
||||||
|
end
|
||||||
|
T[52] = function(c) -- 路牌
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 3, 12, 6, PAL.woodL); c.hline(3, 5, 10, PAL.woodD); c.hline(3, 7, 7, PAL.woodD)
|
||||||
|
c.rect(7, 9, 2, 6, PAL.wood)
|
||||||
|
end
|
||||||
|
T[53] = function(c) -- 野餐布1
|
||||||
|
c.rect(0, 0, 16, 16, C("f8f0e0"))
|
||||||
|
for j = 0, 15 do for i = 0, 15 do
|
||||||
|
if (math.floor(i / 4) + math.floor(j / 4)) % 2 == 0 then c.px(i, j, C("f0a0a8")) end
|
||||||
|
end end
|
||||||
|
end
|
||||||
|
T[54] = function(c) -- 野餐布2(有食物)
|
||||||
|
T[53](c)
|
||||||
|
c.rect(5, 5, 6, 6, C("fff8e8")); c.px(7, 7, PAL.berry); c.px(9, 8, PAL.berry); c.px(6, 9, PAL.yellow)
|
||||||
|
end
|
||||||
|
T[55] = function(c) -- 花坛(盛开)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(1, 1, 14, 14, PAL.stoneD); c.rect(2, 2, 12, 12, C("8a6a48"))
|
||||||
|
c.px(4, 4, PAL.pink); c.px(8, 5, PAL.yellow); c.px(11, 4, PAL.red); c.px(5, 8, PAL.yellow)
|
||||||
|
c.px(9, 9, PAL.pink); c.px(12, 8, PAL.white); c.px(4, 11, PAL.red); c.px(8, 12, PAL.pink); c.px(11, 11, PAL.yellow)
|
||||||
|
end
|
||||||
|
T[56] = function(c) -- 花田·土
|
||||||
|
c.rect(0, 0, 16, 16, C("a8825a"))
|
||||||
|
c.hline(0, 3, 16, C("94714c")); c.hline(0, 8, 16, C("94714c")); c.hline(0, 13, 16, C("94714c"))
|
||||||
|
end
|
||||||
|
T[57] = function(c) -- 花田·苗
|
||||||
|
T[56](c)
|
||||||
|
c.px(3, 2, PAL.leaf); c.px(3, 1, PAL.leaf); c.px(9, 7, PAL.leaf); c.px(9, 6, PAL.leaf); c.px(13, 12, PAL.leaf); c.px(13, 11, PAL.leaf)
|
||||||
|
c.px(4, 1, PAL.pink); c.px(10, 6, PAL.yellow)
|
||||||
|
end
|
||||||
|
T[58] = function(c) -- 彩带(挂起)
|
||||||
|
c.rect(0, 0, 16, 16, pc.rgba(0, 0, 0, 0))
|
||||||
|
for i = 0, 15 do
|
||||||
|
local y = math.floor(2 + 2 * math.sin(i / 4))
|
||||||
|
c.px(i, y + 1, PAL.woodD)
|
||||||
|
end
|
||||||
|
c.px(2, 4, PAL.red); c.px(2, 5, PAL.red); c.px(6, 5, PAL.yellow); c.px(6, 6, PAL.yellow)
|
||||||
|
c.px(10, 4, PAL.pink); c.px(10, 5, PAL.pink); c.px(14, 5, PAL.sky); c.px(14, 6, PAL.sky)
|
||||||
|
end
|
||||||
|
T[59] = function(c) -- 灯笼杆
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(7, 2, 2, 12, PAL.trunk)
|
||||||
|
c.rect(5, 2, 6, 5, PAL.yellow); c.rect(6, 1, 4, 1, PAL.roofD); c.px(7, 4, C("fff0a0")); c.px(8, 4, C("fff0a0"))
|
||||||
|
end
|
||||||
|
T[60] = function(c) -- 湖水·波光
|
||||||
|
T[7](c)
|
||||||
|
c.px(4, 6, PAL.white); c.px(11, 8, PAL.white); c.hline(7, 2, 3, PAL.waterL)
|
||||||
|
end
|
||||||
|
T[61] = function(c) -- 室内窗
|
||||||
|
T[27](c)
|
||||||
|
c.rect(3, 3, 10, 8, PAL.woodD); c.rect(4, 4, 8, 6, PAL.sky)
|
||||||
|
c.vline(7, 4, 6, PAL.woodD); c.px(5, 5, PAL.waterL)
|
||||||
|
end
|
||||||
|
T[62] = function(c) -- 白色层·病床头
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wgrayD)
|
||||||
|
c.rect(1, 1, 14, 15, PAL.wgray)
|
||||||
|
c.rect(3, 3, 10, 6, PAL.white); c.hline(3, 8, 10, PAL.wgrayD)
|
||||||
|
c.hline(1, 0, 14, PAL.wgrayDD)
|
||||||
|
end
|
||||||
|
T[63] = function(c) -- 白色层·病床尾
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wgrayD)
|
||||||
|
c.rect(1, 0, 14, 15, C("dfe2e6"))
|
||||||
|
c.hline(1, 4, 14, PAL.wgrayD); c.hline(1, 9, 14, PAL.wgrayD)
|
||||||
|
c.hline(1, 15, 14, PAL.wgrayDD)
|
||||||
|
end
|
||||||
|
T[64] = function(c) -- 桌(诊所/办公)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.clinic)
|
||||||
|
c.rect(1, 3, 14, 9, PAL.woodL); c.hline(1, 11, 14, PAL.woodD); c.hline(1, 3, 14, PAL.wood)
|
||||||
|
c.rect(3, 5, 6, 4, PAL.white); c.hline(4, 6, 4, PAL.stoneD); c.hline(4, 7, 3, PAL.stoneD)
|
||||||
|
c.vline(2, 12, 3, PAL.woodD); c.vline(13, 12, 3, PAL.woodD)
|
||||||
|
end
|
||||||
|
T[65] = function(c) -- 日历(墙面)
|
||||||
|
T[27](c)
|
||||||
|
c.rect(4, 2, 8, 11, PAL.white); c.rect(4, 2, 8, 3, PAL.red)
|
||||||
|
c.hline(5, 7, 6, PAL.stoneD); c.hline(5, 9, 6, PAL.stoneD); c.rect(7, 10, 3, 2, PAL.black)
|
||||||
|
end
|
||||||
|
T[66] = function(c) -- 柜台+配方纸
|
||||||
|
T[32](c)
|
||||||
|
c.rect(4, 1, 7, 5, C("f8f0d8")); c.hline(5, 2, 5, PAL.stoneD); c.hline(5, 4, 4, PAL.stoneD)
|
||||||
|
end
|
||||||
|
T[67] = function(c) -- 木箱
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 3, 12, 12, PAL.wood); c.rect(3, 4, 10, 10, PAL.woodL)
|
||||||
|
c.hline(3, 8, 10, PAL.wood); c.vline(8, 4, 10, PAL.wood)
|
||||||
|
end
|
||||||
|
T[68] = function(c) -- 篮子
|
||||||
|
c.rect(0, 0, 16, 16, PAL.floor)
|
||||||
|
c.rect(3, 6, 10, 8, PAL.woodL); c.hline(3, 6, 10, PAL.woodD); c.hline(3, 13, 10, PAL.woodD)
|
||||||
|
c.dither(4, 8, 8, 5, PAL.wood, 2)
|
||||||
|
c.px(6, 5, PAL.berry); c.px(9, 5, PAL.berry)
|
||||||
|
end
|
||||||
|
T[69] = function(c) -- 花坛(干涸·待浇水)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(1, 1, 14, 14, PAL.stoneD); c.rect(2, 2, 12, 12, C("b09068"))
|
||||||
|
c.px(5, 5, C("8a7a5a")); c.px(10, 7, C("8a7a5a")); c.px(6, 10, C("8a7a5a"))
|
||||||
|
end
|
||||||
|
T[70] = function(c) -- 白色层·窗
|
||||||
|
T[42](c)
|
||||||
|
c.rect(4, 2, 8, 7, PAL.wgrayDD); c.rect(5, 3, 6, 5, C("c8ccd4")); c.vline(7, 3, 5, PAL.wgrayDD)
|
||||||
|
end
|
||||||
|
T[71] = function(c) -- 门垫
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.rect(2, 4, 12, 8, C("c8a878")); c.rect(3, 5, 10, 6, C("dcc094"))
|
||||||
|
end
|
||||||
|
T[72] = function(c) -- 高草
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
for i = 1, 14, 3 do c.vline(i, 6, 8, PAL.grassD); c.vline(i + 1, 8, 6, PAL.grassL) end
|
||||||
|
end
|
||||||
|
T[73] = function(c) -- 广场中心砖
|
||||||
|
c.rect(0, 0, 16, 16, PAL.path)
|
||||||
|
c.rect(2, 2, 12, 12, C("f0c88a")); c.rect(5, 5, 6, 6, PAL.path)
|
||||||
|
c.px(7, 7, PAL.roofD); c.px(8, 8, PAL.roofD)
|
||||||
|
end
|
||||||
|
T[74] = function(c) -- 白色层·帘
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wgray)
|
||||||
|
for i = 0, 15, 3 do c.vline(i, 0, 16, C("d8dade")) end
|
||||||
|
c.hline(0, 0, 16, PAL.wgrayDD)
|
||||||
|
end
|
||||||
|
T[75] = function(c) -- 白色层·监护仪
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wgrayD)
|
||||||
|
c.rect(3, 2, 10, 9, PAL.wdark); c.rect(4, 3, 8, 6, C("354048"))
|
||||||
|
c.px(5, 6, C("7ae0a0")); c.px(6, 5, C("7ae0a0")); c.px(7, 6, C("7ae0a0")); c.px(8, 6, C("7ae0a0")); c.px(9, 4, C("7ae0a0")); c.px(10, 6, C("7ae0a0"))
|
||||||
|
c.rect(6, 11, 4, 4, PAL.wgrayDD)
|
||||||
|
end
|
||||||
|
T[76] = function(c) -- 石墙(村界)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.stone)
|
||||||
|
c.hline(0, 5, 16, PAL.stoneD); c.hline(0, 11, 16, PAL.stoneD)
|
||||||
|
c.vline(4, 0, 5, PAL.stoneD); c.vline(10, 6, 5, PAL.stoneD); c.vline(6, 12, 4, PAL.stoneD)
|
||||||
|
end
|
||||||
|
T[77] = function(c) -- 雾中草地(边缘)
|
||||||
|
c.rect(0, 0, 16, 16, PAL.grass)
|
||||||
|
c.dither(0, 0, 16, 16, PAL.fog, 2)
|
||||||
|
end
|
||||||
|
T[78] = function(c) -- 黑
|
||||||
|
c.rect(0, 0, 16, 16, PAL.black)
|
||||||
|
end
|
||||||
|
T[79] = function(c) -- 纯白灰
|
||||||
|
c.rect(0, 0, 16, 16, PAL.wgray)
|
||||||
|
end
|
||||||
|
|
||||||
|
local tilesImg = Image(160, 128, ColorMode.RGB)
|
||||||
|
for idx = 0, 79 do
|
||||||
|
local col, row = idx % 10, math.floor(idx / 10)
|
||||||
|
T[idx](makeCtx(tilesImg, col * 16, row * 16))
|
||||||
|
end
|
||||||
|
saveSheet(tilesImg, "tiles")
|
||||||
|
|
||||||
|
-- ============ 主角:16×24 × 16 帧(下/上/左/右 × 4) ============
|
||||||
|
local SKIN = C("f6d2a8")
|
||||||
|
local HAIR = C("6b4632")
|
||||||
|
local SHIRT = C("e87858")
|
||||||
|
local SHIRT_D = C("cc5f42")
|
||||||
|
local PANTS = C("4a6a9a")
|
||||||
|
local SHOE = C("7a4a2a")
|
||||||
|
|
||||||
|
-- 画一帧主角;dir: 0下 1上 2左 3右;f: 0..3 行走帧(0 兼待机)
|
||||||
|
local function paintPlayer(c, dir, f)
|
||||||
|
-- 头(x4..11, y0..7):发 0..2,脸 3..7
|
||||||
|
c.rect(4, 0, 8, 3, HAIR)
|
||||||
|
c.px(4, 0, pc.rgba(0, 0, 0, 0)); c.px(11, 0, pc.rgba(0, 0, 0, 0))
|
||||||
|
if dir == 1 then
|
||||||
|
c.rect(4, 3, 8, 5, HAIR) -- 背面全是头发
|
||||||
|
c.px(4, 7, SKIN); c.px(11, 7, SKIN)
|
||||||
|
else
|
||||||
|
c.rect(4, 3, 8, 5, SKIN)
|
||||||
|
c.vline(4, 3, 2, HAIR); c.vline(11, 3, 2, HAIR) -- 刘海两侧
|
||||||
|
if dir == 0 then
|
||||||
|
c.px(6, 5, PAL.black); c.px(9, 5, PAL.black)
|
||||||
|
c.px(7, 7, PAL.black); c.px(8, 7, PAL.black) -- 永远的微笑
|
||||||
|
c.px(6, 6, C("f8b8a0")); c.px(9, 6, C("f8b8a0"))
|
||||||
|
elseif dir == 2 then
|
||||||
|
c.rect(9, 3, 3, 5, HAIR) -- 侧脸:后脑
|
||||||
|
c.px(6, 5, PAL.black); c.px(5, 7, PAL.black); c.px(6, 7, PAL.black)
|
||||||
|
elseif dir == 3 then
|
||||||
|
c.rect(4, 3, 3, 5, HAIR)
|
||||||
|
c.px(9, 5, PAL.black); c.px(9, 7, PAL.black); c.px(10, 7, PAL.black)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- 身体(y8..16)
|
||||||
|
c.rect(5, 8, 6, 9, SHIRT)
|
||||||
|
c.hline(5, 16, 6, SHIRT_D)
|
||||||
|
-- 手臂:随帧摆动
|
||||||
|
local sway = ({ 0, 1, 0, -1 })[f + 1]
|
||||||
|
if dir == 0 or dir == 1 then
|
||||||
|
c.vline(4, 9 + (sway > 0 and 1 or 0), 5, SHIRT_D); c.px(4, 14 + (sway > 0 and 1 or 0), SKIN)
|
||||||
|
c.vline(11, 9 + (sway < 0 and 1 or 0), 5, SHIRT_D); c.px(11, 14 + (sway < 0 and 1 or 0), SKIN)
|
||||||
|
else
|
||||||
|
c.vline(dir == 2 and 7 or 8, 9 + math.abs(sway), 5, SHIRT_D)
|
||||||
|
c.px(dir == 2 and 7 or 8, 14 + math.abs(sway), SKIN)
|
||||||
|
end
|
||||||
|
-- 腿(y17..20)与鞋(y21..23)
|
||||||
|
local lLift = (f == 1) and 1 or 0
|
||||||
|
local rLift = (f == 3) and 1 or 0
|
||||||
|
c.rect(5, 17, 3, 4 - lLift, PANTS)
|
||||||
|
c.rect(8, 17, 3, 4 - rLift, PANTS)
|
||||||
|
c.rect(5, 21 - lLift, 3, 2, SHOE)
|
||||||
|
c.rect(8, 21 - rLift, 3, 2, SHOE)
|
||||||
|
end
|
||||||
|
|
||||||
|
local playerImg = Image(256, 24, ColorMode.RGB)
|
||||||
|
for dir = 0, 3 do
|
||||||
|
for f = 0, 3 do
|
||||||
|
paintPlayer(makeCtx(playerImg, (dir * 4 + f) * 16, 0), dir, f)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
saveSheet(playerImg, "player")
|
||||||
|
|
||||||
|
-- ============ 主角头像:24×24(微笑 / 真实的脸) ============
|
||||||
|
local function paintPortrait(c, real)
|
||||||
|
c.rect(0, 0, 24, 24, C("f8ecd0")) -- 背景
|
||||||
|
c.rect(4, 2, 16, 6, HAIR) -- 头发
|
||||||
|
c.px(4, 2, C("f8ecd0")); c.px(19, 2, C("f8ecd0"))
|
||||||
|
c.rect(4, 8, 16, 12, SKIN) -- 脸
|
||||||
|
c.vline(4, 8, 4, HAIR); c.vline(5, 8, 2, HAIR)
|
||||||
|
c.vline(19, 8, 4, HAIR); c.vline(18, 8, 2, HAIR)
|
||||||
|
c.rect(6, 20, 12, 4, SHIRT) -- 肩
|
||||||
|
if real then
|
||||||
|
-- 真实的脸:疲惫——平直的嘴、眼下阴影、微乱的头发
|
||||||
|
c.px(3, 4, HAIR); c.px(20, 5, HAIR); c.px(2, 5, HAIR)
|
||||||
|
c.rect(8, 12, 2, 2, PAL.black); c.rect(14, 12, 2, 2, PAL.black)
|
||||||
|
c.hline(8, 15, 2, C("c8a080")); c.hline(14, 15, 2, C("c8a080")) -- 眼袋
|
||||||
|
c.hline(10, 18, 4, C("9a6a52")) -- 平直疲惫的嘴
|
||||||
|
else
|
||||||
|
-- 永远的微笑
|
||||||
|
c.rect(8, 12, 2, 2, PAL.black); c.rect(14, 12, 2, 2, PAL.black)
|
||||||
|
c.px(7, 14, C("f8b8a0")); c.px(16, 14, C("f8b8a0")) -- 腮红
|
||||||
|
c.px(9, 17, C("9a6a52")); c.px(14, 17, C("9a6a52"))
|
||||||
|
c.hline(10, 18, 4, C("9a6a52")) -- 上弯的微笑
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local p1 = Image(24, 24, ColorMode.RGB); paintPortrait(makeCtx(p1, 0, 0), false); saveSheet(p1, "portrait_smile")
|
||||||
|
local p2 = Image(24, 24, ColorMode.RGB); paintPortrait(makeCtx(p2, 0, 0), true); saveSheet(p2, "portrait_real")
|
||||||
|
|
||||||
|
-- ============ 具名 NPC:16×24 × 4 帧(smile/flat/off/blink) ============
|
||||||
|
-- off 档与 smile 档仅差 1–2 像素(右眼低 1 像素),绝不画成鬼脸。
|
||||||
|
local function paintAnimal(c, spec, expr)
|
||||||
|
local top = spec.headTop or 2
|
||||||
|
-- 头
|
||||||
|
c.rect(2, top, 12, 10, spec.fur)
|
||||||
|
c.px(2, top, pc.rgba(0, 0, 0, 0)); c.px(13, top, pc.rgba(0, 0, 0, 0))
|
||||||
|
c.px(2, top + 9, pc.rgba(0, 0, 0, 0)); c.px(13, top + 9, pc.rgba(0, 0, 0, 0))
|
||||||
|
-- 口鼻
|
||||||
|
c.rect(5, top + 5, 6, 4, spec.muzzle)
|
||||||
|
-- 眼睛
|
||||||
|
local ey = top + 4
|
||||||
|
if expr == "blink" then
|
||||||
|
c.px(5, ey + 1, PAL.black); c.px(10, ey + 1, PAL.black)
|
||||||
|
else
|
||||||
|
c.px(5, ey, PAL.black); c.px(5, ey + 1, PAL.black)
|
||||||
|
local roff = (expr == "off") and 1 or 0 -- 崩坏档:右眼低 1 像素
|
||||||
|
c.px(10, ey + roff, PAL.black); c.px(10, ey + 1 + roff, PAL.black)
|
||||||
|
end
|
||||||
|
-- 鼻子
|
||||||
|
if spec.nose then c.px(7, top + 6, PAL.black); c.px(8, top + 6, PAL.black) end
|
||||||
|
-- 嘴:微笑(小 U)或平直
|
||||||
|
local my = top + 8
|
||||||
|
if expr == "flat" then
|
||||||
|
c.hline(6, my, 4, PAL.black)
|
||||||
|
else
|
||||||
|
c.px(6, my - 1, PAL.black); c.px(9, my - 1, PAL.black); c.px(7, my, PAL.black); c.px(8, my, PAL.black)
|
||||||
|
end
|
||||||
|
-- 身体
|
||||||
|
c.rect(4, 12, 8, 8, spec.cloth)
|
||||||
|
-- 手
|
||||||
|
c.vline(3, 13, 4, spec.fur); c.vline(12, 13, 4, spec.fur)
|
||||||
|
-- 脚
|
||||||
|
c.rect(4, 20, 3, 3, spec.furD); c.rect(9, 20, 3, 3, spec.furD)
|
||||||
|
if spec.extra then spec.extra(c, expr) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local NPCS = {
|
||||||
|
{ file = "npc_afu", fur = C("c49058"), furD = C("a87840"), muzzle = C("ecd0a8"), cloth = C("e0b060"), nose = true,
|
||||||
|
extra = function(c) -- 狗村长:垂耳+红领巾
|
||||||
|
c.rect(1, 3, 2, 5, C("a87840")); c.rect(13, 3, 2, 5, C("a87840"))
|
||||||
|
c.hline(4, 12, 8, PAL.red); c.px(7, 13, PAL.red); c.px(8, 13, PAL.red)
|
||||||
|
end },
|
||||||
|
{ file = "npc_yuanyuan", fur = C("f7f2ea"), furD = C("ddd4c8"), muzzle = C("ffffff"), cloth = C("f8c8d8"), headTop = 4,
|
||||||
|
extra = function(c) -- 兔面包师:长耳+围裙
|
||||||
|
c.rect(4, 0, 2, 4, C("f7f2ea")); c.px(5, 1, C("f8b8c8")); c.px(5, 2, C("f8b8c8"))
|
||||||
|
c.rect(10, 0, 2, 4, C("f7f2ea")); c.px(10, 1, C("f8b8c8")); c.px(10, 2, C("f8b8c8"))
|
||||||
|
c.rect(5, 14, 6, 6, C("fff8f0")); c.px(6, 10, C("f0a0b0")) -- 围裙+腮红
|
||||||
|
c.px(9, 10, C("f0a0b0"))
|
||||||
|
end },
|
||||||
|
{ file = "npc_dabao", fur = C("9a6b42"), furD = C("7e5530"), muzzle = C("c89a68"), cloth = C("5a88c8"), nose = true,
|
||||||
|
extra = function(c) -- 熊邮差:圆耳+挎包带
|
||||||
|
c.rect(3, 0, 3, 2, C("9a6b42")); c.rect(10, 0, 3, 2, C("9a6b42"))
|
||||||
|
c.px(4, 1, C("c89a68")); c.px(11, 1, C("c89a68"))
|
||||||
|
for i = 0, 6 do c.px(4 + i, 13 + i > 19 and 19 or 13 + math.floor(i * 0.9), C("3a5a88")) end
|
||||||
|
end },
|
||||||
|
{ file = "npc_mimi", fur = C("c8c0d8"), furD = C("a8a0c0"), muzzle = C("ece8f4"), cloth = C("e8a04a"), nose = true,
|
||||||
|
extra = function(c) -- 猫店主:尖耳+胡须
|
||||||
|
c.px(3, 1, C("c8c0d8")); c.rect(3, 2, 2, 1, C("c8c0d8")); c.px(4, 1, C("c8c0d8"))
|
||||||
|
c.px(12, 1, C("c8c0d8")); c.rect(11, 2, 2, 1, C("c8c0d8")); c.px(11, 1, C("c8c0d8"))
|
||||||
|
c.hline(1, 8, 2, C("8a84a0")); c.hline(13, 8, 2, C("8a84a0"))
|
||||||
|
end },
|
||||||
|
{ file = "npc_fu", fur = C("d8b088"), furD = C("bc9268"), muzzle = C("f0d8b8"), cloth = C("f4f4f4"), nose = true,
|
||||||
|
extra = function(c) -- 鹿医生:小角+白大褂
|
||||||
|
c.px(4, 1, C("8a6a4a")); c.px(4, 0, C("8a6a4a")); c.px(3, 0, C("8a6a4a"))
|
||||||
|
c.px(11, 1, C("8a6a4a")); c.px(11, 0, C("8a6a4a")); c.px(12, 0, C("8a6a4a"))
|
||||||
|
c.vline(7, 12, 8, C("d8d8d8")) -- 大褂门襟
|
||||||
|
end },
|
||||||
|
{ file = "npc_gui", fur = C("a8c078"), furD = C("88a058"), muzzle = C("c8d8a0"), cloth = C("7a9a4a"),
|
||||||
|
extra = function(c) -- 龟爷爷:龟壳纹+老花镜
|
||||||
|
c.hline(5, 14, 6, C("5f7c38")); c.hline(5, 17, 6, C("5f7c38")); c.vline(7, 13, 6, C("5f7c38"))
|
||||||
|
local gy = 2 + 4
|
||||||
|
c.px(4, gy, C("6a6a6a")); c.px(6, gy, C("6a6a6a")); c.px(9, gy, C("6a6a6a")); c.px(11, gy, C("6a6a6a"))
|
||||||
|
c.px(7, gy, C("6a6a6a")); c.px(8, gy, C("6a6a6a"))
|
||||||
|
end },
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, spec in ipairs(NPCS) do
|
||||||
|
local img = Image(64, 24, ColorMode.RGB)
|
||||||
|
local exprs = { "smile", "flat", "off", "blink" }
|
||||||
|
for i, e in ipairs(exprs) do
|
||||||
|
paintAnimal(makeCtx(img, (i - 1) * 16, 0), spec, e)
|
||||||
|
end
|
||||||
|
saveSheet(img, spec.file)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============ 双子鸟:16×16 × 2 帧(闭嘴/张嘴) ============
|
||||||
|
local function paintBird(c, open)
|
||||||
|
local B = C("6ab0e8")
|
||||||
|
local BD = C("4a90c8")
|
||||||
|
c.rect(4, 5, 8, 7, B) -- 身体
|
||||||
|
c.rect(8, 2, 6, 6, B) -- 头
|
||||||
|
c.px(11, 4, PAL.black) -- 眼
|
||||||
|
c.rect(2, 6, 3, 4, BD) -- 尾
|
||||||
|
c.rect(5, 7, 4, 3, BD) -- 翅
|
||||||
|
if open then
|
||||||
|
c.px(14, 5, PAL.yellow); c.px(15, 4, PAL.yellow); c.px(15, 6, PAL.yellow)
|
||||||
|
else
|
||||||
|
c.px(14, 5, PAL.yellow); c.px(15, 5, PAL.yellow)
|
||||||
|
end
|
||||||
|
c.vline(7, 12, 2, PAL.yellow); c.vline(10, 12, 2, PAL.yellow) -- 脚
|
||||||
|
end
|
||||||
|
local birdImg = Image(32, 16, ColorMode.RGB)
|
||||||
|
paintBird(makeCtx(birdImg, 0, 0), false)
|
||||||
|
paintBird(makeCtx(birdImg, 16, 0), true)
|
||||||
|
saveSheet(birdImg, "bird")
|
||||||
|
|
||||||
|
-- ============ 灰麻雀(E3 结局,一只真的鸟):16×16 ============
|
||||||
|
local function paintSparrow(c)
|
||||||
|
local G = C("9a9088")
|
||||||
|
local GD = C("6e6660")
|
||||||
|
c.rect(4, 6, 8, 6, G)
|
||||||
|
c.rect(8, 3, 6, 5, G)
|
||||||
|
c.px(11, 5, PAL.black)
|
||||||
|
c.rect(2, 7, 3, 3, GD)
|
||||||
|
c.rect(5, 8, 4, 2, GD)
|
||||||
|
c.px(14, 6, C("7a6a58")); c.px(15, 6, C("7a6a58"))
|
||||||
|
c.vline(7, 12, 2, GD); c.vline(10, 12, 2, GD)
|
||||||
|
end
|
||||||
|
local sparrowImg = Image(16, 16, ColorMode.RGB)
|
||||||
|
paintSparrow(makeCtx(sparrowImg, 0, 0))
|
||||||
|
saveSheet(sparrowImg, "sparrow")
|
||||||
|
|
||||||
|
-- ============ 群演 ×5:16×16 × 2 帧循环 ============
|
||||||
|
local function paintCritter(c, spec, f)
|
||||||
|
local bob = f -- 第二帧整体下移 1px 形成循环
|
||||||
|
c.rect(4, 6 + bob, 8, 6 - bob, spec.body)
|
||||||
|
c.rect(5, 2 + bob, 6, 5, spec.body)
|
||||||
|
c.px(6, 4 + bob, PAL.black); c.px(9, 4 + bob, PAL.black)
|
||||||
|
c.px(7, 6 + bob, spec.snout); c.px(8, 6 + bob, spec.snout)
|
||||||
|
c.rect(4, 12, 3, 2, spec.dark); c.rect(9, 12, 3, 2, spec.dark)
|
||||||
|
if spec.extra then spec.extra(c, f) end
|
||||||
|
end
|
||||||
|
local CRITTERS = {
|
||||||
|
{ file = "extra1", body = C("e0904a"), dark = C("b87030"), snout = C("f8c890"), -- 松鼠:大尾巴
|
||||||
|
extra = function(c, f) c.rect(12, 1 + f, 3, 9, C("b87030")); c.px(13, 2 + f, C("e0904a")) end },
|
||||||
|
{ file = "extra2", body = C("b09068"), dark = C("8a6a48"), snout = C("e8cca8"), -- 刺猬:背刺
|
||||||
|
extra = function(c, f) for i = 4, 11, 2 do c.px(i, 1 + f + (i % 4 == 0 and 0 or 1), C("6e5236")) end end },
|
||||||
|
{ file = "extra3", body = C("f4e04a"), dark = C("d8b830"), snout = C("f4a030"), -- 鸭子:扁嘴
|
||||||
|
extra = function(c, f) c.px(6, 5 + f, C("f4a030")); c.px(9, 5 + f, C("f4a030")) end },
|
||||||
|
{ file = "extra4", body = C("b8b8c4"), dark = C("8e8e9c"), snout = C("e8c8c8"), -- 老鼠:圆耳
|
||||||
|
extra = function(c, f) c.rect(4, 0 + f, 2, 2, C("b8b8c4")); c.rect(10, 0 + f, 2, 2, C("b8b8c4")) end },
|
||||||
|
{ file = "extra5", body = C("7ac86a"), dark = C("58a048"), snout = C("aee89e"), -- 青蛙:凸眼
|
||||||
|
extra = function(c, f) c.px(5, 1 + f, C("7ac86a")); c.px(10, 1 + f, C("7ac86a")) end },
|
||||||
|
}
|
||||||
|
for _, spec in ipairs(CRITTERS) do
|
||||||
|
local img = Image(32, 16, ColorMode.RGB)
|
||||||
|
paintCritter(makeCtx(img, 0, 0), spec, 0)
|
||||||
|
paintCritter(makeCtx(img, 16, 0), spec, 1)
|
||||||
|
saveSheet(img, spec.file)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============ 向日葵表盘:24×24 × 14 帧 ============
|
||||||
|
-- 帧 0..12 = 点亮花瓣数;帧 13 = 满格+花心黑籽
|
||||||
|
local function paintSunflower(c, lit, seeds)
|
||||||
|
local cx, cy = 11.5, 11.5
|
||||||
|
for k = 0, 11 do
|
||||||
|
local ang = (k / 12) * math.pi * 2 - math.pi / 2
|
||||||
|
local col = (k < lit) and PAL.yellow or C("7a7264")
|
||||||
|
local colD = (k < lit) and C("e0b830") or C("635c50")
|
||||||
|
for r = 7, 10 do
|
||||||
|
local x = math.floor(cx + math.cos(ang) * r + 0.5)
|
||||||
|
local y = math.floor(cy + math.sin(ang) * r + 0.5)
|
||||||
|
c.px(x, y, r > 9 and colD or col)
|
||||||
|
local px2 = math.floor(cx + math.cos(ang + 0.14) * r + 0.5)
|
||||||
|
local py2 = math.floor(cy + math.sin(ang + 0.14) * r + 0.5)
|
||||||
|
c.px(px2, py2, col)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- 花心
|
||||||
|
for j = -4, 4 do for i = -4, 4 do
|
||||||
|
if i * i + j * j <= 18 then c.px(12 + i, 12 + j, C("9a6a2a")) end
|
||||||
|
end end
|
||||||
|
for j = -3, 3 do for i = -3, 3 do
|
||||||
|
if i * i + j * j <= 9 then c.px(12 + i, 12 + j, C("b8843a")) end
|
||||||
|
end end
|
||||||
|
if seeds then -- 过熟的黑籽
|
||||||
|
c.px(11, 11, PAL.black); c.px(13, 12, PAL.black); c.px(12, 14, PAL.black)
|
||||||
|
c.px(10, 13, PAL.black); c.px(14, 10, PAL.black); c.px(12, 11, C("4a3a20"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local sunImg = Image(24 * 14, 24, ColorMode.RGB)
|
||||||
|
for f = 0, 12 do paintSunflower(makeCtx(sunImg, f * 24, 0), f, false) end
|
||||||
|
paintSunflower(makeCtx(sunImg, 13 * 24, 0), 12, true)
|
||||||
|
saveSheet(sunImg, "sunflower")
|
||||||
|
|
||||||
|
print("ALL ART GENERATED OK")
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
// 《崩坏童话:蜜糖村》10 张地图生成脚本
|
||||||
|
// 生成 maps/*.tmx(Tiled 源文件),随后由 Tiled CLI 导出 JSON 到 public/assets/maps/
|
||||||
|
// 运行:node tools/gen_maps.mjs
|
||||||
|
import { writeFileSync, mkdirSync } from "fs";
|
||||||
|
|
||||||
|
// 瓦片索引(与 tools/gen_art.lua 的 tileset 一一对应),GID = 索引 + 1
|
||||||
|
const I = {
|
||||||
|
grass: 0, grass2: 1, grassWF: 2, grassPF: 3, path: 4, path2: 5, pathEdge: 6,
|
||||||
|
water1: 7, water2: 8, shore: 9, flowerBush: 10, berryFull: 11, berryPicked: 12,
|
||||||
|
treeTL: 13, treeTR: 14, treeBL: 15, treeBR: 16, bush: 17, wall: 18, wallWin: 19,
|
||||||
|
door: 20, roofL: 21, roofR: 22, eave: 23, floorW: 24, floorW2: 25, floorC: 26,
|
||||||
|
inWall: 27, inWallLow: 28, rug: 29, bedHead: 30, bedFoot: 31, counter: 32,
|
||||||
|
oven: 33, shelfEmpty: 34, shelfGoods: 35, table: 36, chair: 37, wellRoof: 38,
|
||||||
|
wellRing: 39, fogLight: 40, fogDense: 41, wWall: 42, wFloor: 43, wDoor: 44,
|
||||||
|
benchL: 45, benchR: 46, boardL: 47, boardR: 48, mailbox: 49, fenceH: 50,
|
||||||
|
fencePost: 51, sign: 52, picnic1: 53, picnic2: 54, bedBloom: 55, dirt: 56,
|
||||||
|
sprout: 57, bunting: 58, lantern: 59, sparkle: 60, winIn: 61, wBedHead: 62,
|
||||||
|
wBedFoot: 63, desk: 64, calendar: 65, counterPaper: 66, crate: 67, basket: 68,
|
||||||
|
bedDry: 69, wWin: 70, doormat: 71, tallGrass: 72, plazaBrick: 73, wCurtain: 74,
|
||||||
|
monitor: 75, stoneWall: 76, fogGrass: 77, black: 78, wBlank: 79,
|
||||||
|
};
|
||||||
|
|
||||||
|
class M {
|
||||||
|
constructor(name, w, h) {
|
||||||
|
this.name = name; this.w = w; this.h = h;
|
||||||
|
this.ground = new Array(w * h).fill(0);
|
||||||
|
this.obst = new Array(w * h).fill(0);
|
||||||
|
this.objects = []; this.oid = 1;
|
||||||
|
}
|
||||||
|
g(x, y, t) { this.ground[y * this.w + x] = t + 1; }
|
||||||
|
o(x, y, t) { this.obst[y * this.w + x] = t + 1; }
|
||||||
|
gFill(t) { for (let i = 0; i < this.ground.length; i++) this.ground[i] = t + 1; }
|
||||||
|
gRect(x, y, w, h, t) { for (let j = y; j < y + h; j++) for (let i = x; i < x + w; i++) this.g(i, j, t); }
|
||||||
|
oRect(x, y, w, h, t) { for (let j = y; j < y + h; j++) for (let i = x; i < x + w; i++) this.o(i, j, t); }
|
||||||
|
// 装饰草:确定性伪随机
|
||||||
|
grassDecor() {
|
||||||
|
for (let j = 0; j < this.h; j++) for (let i = 0; i < this.w; i++) {
|
||||||
|
const r = (i * 73 + j * 149 + i * j * 7) % 23;
|
||||||
|
if (this.ground[j * this.w + i] === I.grass + 1) {
|
||||||
|
if (r === 0) this.g(i, j, I.grassWF); else if (r === 1) this.g(i, j, I.grassPF); else if (r < 6) this.g(i, j, I.grass2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tree(x, y) { this.o(x, y, I.treeTL); this.o(x + 1, y, I.treeTR); this.o(x, y + 1, I.treeBL); this.o(x + 1, y + 1, I.treeBR); }
|
||||||
|
// 4 宽 ×3 高的房屋立面,door 在底行 dx 处
|
||||||
|
facade(x, y, dx) {
|
||||||
|
this.o(x, y, I.roofL); this.o(x + 1, y, I.roofL); this.o(x + 2, y, I.roofR); this.o(x + 3, y, I.roofR);
|
||||||
|
for (let i = 0; i < 4; i++) this.o(x + i, y + 1, I.eave);
|
||||||
|
for (let i = 0; i < 4; i++) this.o(x + i, y + 2, i % 3 === 1 ? I.wallWin : I.wall);
|
||||||
|
this.obst[(y + 2) * this.w + (x + dx)] = 0; // 门位可走
|
||||||
|
this.g(x + dx, y + 2, I.door);
|
||||||
|
this.g(x + dx, y + 3, I.doormat);
|
||||||
|
}
|
||||||
|
obj(name, type, tx, ty, opts = {}) {
|
||||||
|
const o = { id: this.oid++, name, type, x: tx * 16, y: ty * 16, w: (opts.w ?? 0) * 16, h: (opts.h ?? 0) * 16, props: opts.props ?? {} };
|
||||||
|
this.objects.push(o); return o;
|
||||||
|
}
|
||||||
|
spawn(name, tx, ty) { this.obj(name, "spawn", tx + 0.5, ty + 0.5); }
|
||||||
|
door(tx, ty, w, h, target, spawn) { this.obj("to_" + target, "door", tx, ty, { w, h, props: { target, spawn } }); }
|
||||||
|
npc(name, tx, ty, props = {}) { this.obj(name, "npc", tx + 0.5, ty + 0.5, { props }); }
|
||||||
|
poi(name, tx, ty, props = {}) { this.obj(name, "poi", tx + 0.5, ty + 0.5, { props }); }
|
||||||
|
|
||||||
|
tmx() {
|
||||||
|
const csv = a => { let out = []; for (let j = 0; j < this.h; j++) out.push(a.slice(j * this.w, (j + 1) * this.w).join(",")); return out.join(",\n"); };
|
||||||
|
const props = p => {
|
||||||
|
const e = Object.entries(p); if (!e.length) return "";
|
||||||
|
return `<properties>${e.map(([k, v]) => `<property name="${k}" value="${v}"/>`).join("")}</properties>`;
|
||||||
|
};
|
||||||
|
const objs = this.objects.map(o => {
|
||||||
|
const size = o.w || o.h ? ` width="${o.w}" height="${o.h}"` : "";
|
||||||
|
const body = props(o.props);
|
||||||
|
return ` <object id="${o.id}" name="${o.name}" type="${o.type}" x="${o.x}" y="${o.y}"${size}>${body}${o.w || o.h ? "" : "<point/>"}</object>`;
|
||||||
|
}).join("\n");
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${this.w}" height="${this.h}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="${this.oid}">
|
||||||
|
<tileset firstgid="1" source="tiles.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="${this.w}" height="${this.h}">
|
||||||
|
<data encoding="csv">
|
||||||
|
${csv(this.ground)}
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="obstacles" width="${this.w}" height="${this.h}">
|
||||||
|
<properties><property name="collides" type="bool" value="true"/></properties>
|
||||||
|
<data encoding="csv">
|
||||||
|
${csv(this.obst)}
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="objects">
|
||||||
|
${objs}
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync("maps", { recursive: true });
|
||||||
|
mkdirSync("public/assets/maps", { recursive: true });
|
||||||
|
|
||||||
|
const maps = [];
|
||||||
|
|
||||||
|
// ============ 小满小屋 12×8 ============
|
||||||
|
{
|
||||||
|
const m = new M("house", 12, 8);
|
||||||
|
m.gFill(I.floorW);
|
||||||
|
for (let i = 0; i < 12; i++) { m.o(i, 0, I.inWall); m.o(i, 1, i === 6 ? I.winIn : I.inWallLow); }
|
||||||
|
m.o(2, 1, I.calendar);
|
||||||
|
m.o(1, 2, I.bedHead); m.o(1, 3, I.bedFoot);
|
||||||
|
m.o(7, 4, I.table); m.o(8, 4, I.chair);
|
||||||
|
m.o(10, 2, I.crate);
|
||||||
|
m.g(5, 7, I.rug); m.g(6, 7, I.rug);
|
||||||
|
m.o(0, 2, I.crate); // 角落杂物
|
||||||
|
m.spawn("bed", 2, 3);
|
||||||
|
m.spawn("entry", 5, 6);
|
||||||
|
m.door(5, 7.5, 2, 0.5, "plaza", "fromHouse");
|
||||||
|
m.poi("calendar", 2, 2, { clue: "C1" });
|
||||||
|
m.poi("bed", 1, 4);
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 村广场 30×20 ============
|
||||||
|
{
|
||||||
|
const m = new M("plaza", 30, 20);
|
||||||
|
m.gFill(I.grass);
|
||||||
|
// 道路
|
||||||
|
m.gRect(14, 0, 2, 20, I.path); m.gRect(0, 8, 30, 2, I.path);
|
||||||
|
for (let i = 0; i < 30; i += 2) m.g(i, 8, I.path2);
|
||||||
|
for (let j = 1; j < 20; j += 3) m.g(14, j, I.path2);
|
||||||
|
// 庆典场地
|
||||||
|
m.gRect(12, 11, 6, 4, I.plazaBrick);
|
||||||
|
m.grassDecor();
|
||||||
|
// 边界:树与栅栏(留出四个出口)
|
||||||
|
for (let i = 0; i < 30; i += 2) { if (i < 12 || i > 17) m.tree(i, 0); }
|
||||||
|
for (let i = 0; i < 30; i += 2) { if (i < 13 || i > 16) { m.o(i, 19, I.fenceH); m.o(i + 1, 19, I.fenceH); } }
|
||||||
|
for (let j = 2; j < 18; j += 2) { if (j < 7 || j > 10) { m.o(0, j, I.fencePost); m.o(29, j, I.fencePost); } }
|
||||||
|
// 房屋立面:小屋 / 面包房 / 杂货店 / 诊所
|
||||||
|
m.facade(2, 2, 2); m.facade(7, 2, 2); m.facade(19, 2, 2); m.facade(24, 2, 2);
|
||||||
|
m.poi("sign_house", 1, 5); m.o(1, 5, I.sign);
|
||||||
|
// 公告板
|
||||||
|
m.o(11, 6, I.boardL); m.o(12, 6, I.boardR);
|
||||||
|
m.poi("board", 11.5, 7.5);
|
||||||
|
// 邮筒与散落信件(C3)
|
||||||
|
m.o(17, 1, I.mailbox);
|
||||||
|
m.poi("letters", 17.5, 2.5, { clue: "C3" });
|
||||||
|
// 双子鸟树
|
||||||
|
m.tree(20, 6);
|
||||||
|
m.poi("birdtree", 21, 8.5);
|
||||||
|
// 花坛 ×3(T2 浇水)
|
||||||
|
m.o(7, 11, I.bedDry); m.o(7, 13, I.bedDry); m.o(7, 15, I.bedDry);
|
||||||
|
m.poi("flowerbed1", 7.5, 12); m.poi("flowerbed2", 7.5, 14); m.poi("flowerbed3", 7.5, 16);
|
||||||
|
// 庆典灯笼杆 ×4(T13 挂彩带)
|
||||||
|
m.o(11, 10, I.lantern); m.o(18, 10, I.lantern); m.o(11, 15, I.lantern); m.o(18, 15, I.lantern);
|
||||||
|
m.poi("ribbon1", 11.5, 11.2); m.poi("ribbon2", 18.5, 11.2); m.poi("ribbon3", 11.5, 16.2); m.poi("ribbon4", 18.5, 16.2);
|
||||||
|
m.poi("festival", 15, 13);
|
||||||
|
// 装饰
|
||||||
|
m.o(4, 12, I.flowerBush); m.o(23, 12, I.flowerBush); m.o(25, 15, I.bush); m.o(3, 16, I.bush);
|
||||||
|
m.tree(26, 16); m.tree(21, 17);
|
||||||
|
// NPC
|
||||||
|
m.npc("afu", 12.5, 8.5);
|
||||||
|
m.npc("dabao", 16.5, 3.5);
|
||||||
|
m.npc("bird1", 20.4, 6.3); m.npc("bird2", 21.6, 6.3);
|
||||||
|
m.npc("extra1", 5, 10, { patrol: 40 }); m.npc("extra2", 22, 9, { patrol: 56 });
|
||||||
|
m.npc("extra3", 24, 17, { patrol: 40 }); m.npc("extra4", 5, 17, { patrol: 24 });
|
||||||
|
m.npc("extra5", 22, 17, { patrol: 16 });
|
||||||
|
// 出生点
|
||||||
|
m.spawn("fromHouse", 4, 6.2); m.spawn("fromBakery", 9, 6.2); m.spawn("fromStore", 21, 6.2); m.spawn("fromClinic", 26, 6.2);
|
||||||
|
m.spawn("fromLake", 1.6, 9); m.spawn("fromField", 28.4, 9); m.spawn("fromWell", 14.5, 1.6); m.spawn("fromFog", 14.5, 17.2);
|
||||||
|
m.spawn("center", 15, 12);
|
||||||
|
// 门与出口
|
||||||
|
m.door(4, 4.2, 1, 0.8, "house", "entry");
|
||||||
|
m.door(9, 4.2, 1, 0.8, "bakery", "entry");
|
||||||
|
m.door(21, 4.2, 1, 0.8, "store", "entry");
|
||||||
|
m.door(26, 4.2, 1, 0.8, "clinic", "entry");
|
||||||
|
m.door(0, 7, 0.6, 4, "lake", "entry");
|
||||||
|
m.door(29.4, 7, 0.6, 4, "field", "entry");
|
||||||
|
m.door(12.5, 0, 5, 0.7, "well", "entry");
|
||||||
|
m.door(12.5, 19.3, 5, 0.7, "fogwall", "entry");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 面包房 15×10 ============
|
||||||
|
{
|
||||||
|
const m = new M("bakery", 15, 10);
|
||||||
|
m.gFill(I.floorW);
|
||||||
|
for (let i = 0; i < 15; i++) { m.o(i, 0, I.inWall); m.o(i, 1, (i === 4 || i === 10) ? I.winIn : I.inWallLow); }
|
||||||
|
m.o(12, 1, I.oven);
|
||||||
|
for (let i = 2; i < 7; i++) m.o(i, 3, I.counter);
|
||||||
|
m.o(7, 3, I.counterPaper);
|
||||||
|
m.o(1, 5, I.basket); m.o(1, 7, I.crate); m.o(13, 5, I.crate);
|
||||||
|
m.o(10, 5, I.table); m.o(11, 5, I.chair);
|
||||||
|
m.g(7, 9, I.rug); m.g(8, 9, I.rug);
|
||||||
|
m.npc("yuanyuan", 4.5, 2.6);
|
||||||
|
m.poi("recipe", 7.5, 4.2, { clue: "C2" });
|
||||||
|
m.poi("knead", 10.5, 6.2);
|
||||||
|
m.spawn("entry", 7.5, 8);
|
||||||
|
m.door(7, 9.5, 2, 0.5, "plaza", "fromBakery");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 杂货店 15×10 ============
|
||||||
|
{
|
||||||
|
const m = new M("store", 15, 10);
|
||||||
|
m.gFill(I.floorW2);
|
||||||
|
for (let i = 0; i < 15; i++) { m.o(i, 0, I.inWall); m.o(i, 1, i === 12 ? I.winIn : I.inWallLow); }
|
||||||
|
for (let i = 2; i < 11; i++) m.o(i, 2, I.shelfGoods);
|
||||||
|
m.o(13, 4, I.shelfGoods); m.o(13, 5, I.shelfGoods); m.o(13, 6, I.shelfGoods);
|
||||||
|
m.o(3, 5, I.counter); m.o(4, 5, I.counter);
|
||||||
|
m.o(1, 7, I.crate); m.o(11, 8, I.basket);
|
||||||
|
m.g(7, 9, I.rug); m.g(8, 9, I.rug);
|
||||||
|
m.npc("mimi", 3.5, 4.4);
|
||||||
|
m.poi("shelfA", 3.5, 3.2); m.poi("shelfB", 6.5, 3.2); m.poi("shelfC", 9.5, 3.2);
|
||||||
|
m.poi("shelf_c5", 12.4, 5.5, { clue: "C5" });
|
||||||
|
m.spawn("entry", 7.5, 8);
|
||||||
|
m.door(7, 9.5, 2, 0.5, "plaza", "fromStore");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 诊所 12×8 ============
|
||||||
|
{
|
||||||
|
const m = new M("clinic", 12, 8);
|
||||||
|
m.gFill(I.floorC);
|
||||||
|
for (let i = 0; i < 12; i++) { m.o(i, 0, I.wWall); m.o(i, 1, i === 3 ? I.wWin : I.inWallLow); }
|
||||||
|
m.o(5, 3, I.desk);
|
||||||
|
m.o(9, 2, I.wBedHead); m.o(9, 3, I.wBedFoot);
|
||||||
|
m.o(8, 1, I.wCurtain);
|
||||||
|
m.o(1, 2, I.crate);
|
||||||
|
m.g(5, 7, I.rug); m.g(6, 7, I.rug);
|
||||||
|
m.npc("fu", 6.5, 2.6);
|
||||||
|
m.poi("chart", 4.5, 4.2, { clue: "C8" });
|
||||||
|
m.spawn("entry", 5.5, 6);
|
||||||
|
m.door(5, 7.5, 2, 0.5, "plaza", "fromClinic");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 湖畔 24×16 ============
|
||||||
|
{
|
||||||
|
const m = new M("lake", 24, 16);
|
||||||
|
m.gFill(I.grass);
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
m.g(i, 9, I.shore);
|
||||||
|
for (let j = 10; j < 16; j++) m.g(i, j, (i * 31 + j * 17) % 7 === 0 ? I.sparkle : ((i + j) % 2 ? I.water1 : I.water2));
|
||||||
|
}
|
||||||
|
m.oRect(0, 10, 24, 6, I.water1); // 水面不可走(碰撞用,绘制层在 ground)
|
||||||
|
for (let j = 10; j < 16; j++) for (let i = 0; i < 24; i++) m.obst[j * m.w + i] = m.ground[j * m.w + i]; // 与水面同贴图
|
||||||
|
m.grassDecor();
|
||||||
|
for (let i = 0; i < 24; i += 2) m.tree(i, 0);
|
||||||
|
for (let j = 2; j < 8; j += 2) { m.o(0, j, I.fencePost); }
|
||||||
|
m.tree(2, 3); m.tree(11, 2); m.o(6, 4, I.flowerBush);
|
||||||
|
// 长椅(龟爷爷)
|
||||||
|
m.o(16, 5, I.benchL); m.o(17, 5, I.benchR);
|
||||||
|
m.npc("gui", 18.9, 5.5);
|
||||||
|
m.poi("bench", 16.5, 6.5);
|
||||||
|
// 野餐布
|
||||||
|
m.g(7, 6, I.picnic1); m.g(8, 6, I.picnic2); m.g(7, 7, I.picnic2); m.g(8, 7, I.picnic1);
|
||||||
|
m.poi("picnic", 8, 7);
|
||||||
|
m.spawn("entry", 22, 7);
|
||||||
|
m.spawn("picnicSpot", 8, 7.6);
|
||||||
|
m.door(23.4, 5, 0.6, 5, "plaza", "fromLake");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 花田 20×14 ============
|
||||||
|
{
|
||||||
|
const m = new M("field", 20, 14);
|
||||||
|
m.gFill(I.grass);
|
||||||
|
m.gRect(3, 3, 14, 8, I.dirt);
|
||||||
|
for (let j = 3; j < 11; j++) for (let i = 3; i < 17; i++) if ((i + j) % 3 === 0) m.g(i, j, I.sprout);
|
||||||
|
m.grassDecor();
|
||||||
|
for (let i = 0; i < 20; i += 2) { m.tree(i, 0); if (i < 4 || i > 8) { m.o(i, 13, I.fenceH); m.o(i + 1, 13, I.fenceH); } }
|
||||||
|
for (let j = 2; j < 12; j += 2) { if (j < 5 || j > 8) m.o(19, j, I.fencePost); }
|
||||||
|
// 莓果丛 ×5(T6)
|
||||||
|
const berries = [[4, 4], [8, 5], [12, 4], [15, 7], [6, 9]];
|
||||||
|
berries.forEach(([x, y], k) => { m.o(x, y, I.berryFull); m.poi("berry" + (k + 1), x + 0.5, y + 1.5); });
|
||||||
|
// 浇花点
|
||||||
|
m.o(3, 11, I.bedDry);
|
||||||
|
m.poi("fieldwater", 3.5, 12.2);
|
||||||
|
m.o(17, 11, I.flowerBush); m.o(2, 2, I.bush);
|
||||||
|
m.spawn("entry", 1.6, 6);
|
||||||
|
m.door(0, 4.5, 0.6, 5, "plaza", "fromField");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 古井空地 14×10 ============
|
||||||
|
{
|
||||||
|
const m = new M("well", 14, 10);
|
||||||
|
m.gFill(I.grass);
|
||||||
|
m.gRect(6, 6, 2, 4, I.path);
|
||||||
|
m.grassDecor();
|
||||||
|
for (let i = 0; i < 14; i += 2) m.tree(i, 0);
|
||||||
|
for (let j = 2; j < 9; j += 2) { m.o(0, j, I.fencePost); m.o(13, j, I.fencePost); }
|
||||||
|
for (let i = 0; i < 14; i += 2) { if (i < 5 || i > 8) { m.o(i, 9, I.fenceH); m.o(i + 1, 9, I.fenceH); } }
|
||||||
|
m.o(2, 4, I.tallGrass); m.o(3, 4, I.tallGrass); m.o(11, 5, I.tallGrass); m.o(11, 3, I.bush)
|
||||||
|
m.o(6, 3, I.wellRoof); m.o(6, 4, I.wellRing);
|
||||||
|
m.o(9, 4, I.sign);
|
||||||
|
m.poi("well", 6.5, 5.5, { clue: "C7" });
|
||||||
|
m.spawn("entry", 6.5, 7.6);
|
||||||
|
m.door(4.5, 9.3, 5, 0.7, "plaza", "fromWell");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 雾墙边界 20×8 ============
|
||||||
|
{
|
||||||
|
const m = new M("fogwall", 20, 8);
|
||||||
|
m.gFill(I.grass);
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
m.g(i, 4, I.fogGrass);
|
||||||
|
m.g(i, 5, I.fogLight);
|
||||||
|
m.g(i, 6, I.fogDense); m.g(i, 7, I.fogDense);
|
||||||
|
m.o(i, 6, I.fogDense); m.o(i, 7, I.fogDense);
|
||||||
|
}
|
||||||
|
m.grassDecor();
|
||||||
|
for (let j = 0; j < 6; j += 2) { m.o(0, j, I.fencePost); m.o(19, j, I.fencePost); }
|
||||||
|
m.tree(2, 1); m.tree(16, 1);
|
||||||
|
m.o(4, 3, I.sign);
|
||||||
|
m.poi("fog", 10, 5.5);
|
||||||
|
m.spawn("entry", 10, 2.4);
|
||||||
|
m.door(7.5, 0, 5, 0.7, "plaza", "fromFog");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 白色层·走廊 20×6 ============
|
||||||
|
{
|
||||||
|
const m = new M("white_corridor", 20, 6);
|
||||||
|
m.gFill(I.wFloor);
|
||||||
|
for (let i = 0; i < 20; i++) m.o(i, 0, (i % 5 === 2) ? I.wWin : I.wWall);
|
||||||
|
m.o(6, 0, I.wDoor); m.o(13, 0, I.wDoor);
|
||||||
|
m.spawn("entry", 1, 3);
|
||||||
|
m.door(19.5, 0, 0.5, 6, "white_ward", "entry");
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 白色层·病房 10×8 ============
|
||||||
|
{
|
||||||
|
const m = new M("white_ward", 10, 8);
|
||||||
|
m.gFill(I.wFloor);
|
||||||
|
for (let i = 0; i < 10; i++) m.o(i, 0, i === 6 ? I.wWin : I.wWall);
|
||||||
|
m.o(2, 0, I.wCurtain);
|
||||||
|
m.o(4, 2, I.wBedHead); m.o(4, 3, I.wBedFoot);
|
||||||
|
m.o(6, 2, I.monitor);
|
||||||
|
m.poi("chart_ward", 5, 4.5);
|
||||||
|
m.spawn("entry", 1, 5);
|
||||||
|
maps.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tiles.tsx
|
||||||
|
writeFileSync("maps/tiles.tsx", `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tileset version="1.10" tiledversion="1.12.2" name="tiles" tilewidth="16" tileheight="16" tilecount="80" columns="10">
|
||||||
|
<image source="../public/assets/tiles.png" width="160" height="128"/>
|
||||||
|
</tileset>
|
||||||
|
`);
|
||||||
|
|
||||||
|
for (const m of maps) writeFileSync(`maps/${m.name}.tmx`, m.tmx());
|
||||||
|
console.log("TMX written:", maps.map(m => m.name).join(", "));
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -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,126 @@
|
|||||||
|
# 崩坏童话:蜜糖村(网页版)
|
||||||
|
|
||||||
|
> 纯单机网页游戏 · Phaser 3 + TypeScript + Vite · 无后端、无存档、无 localStorage,浏览器打开即玩。
|
||||||
|
|
||||||
|
一个看起来百分之百治愈的像素童话村庄,实际是一场情绪疗程的沉浸幻境。玩家越"幸福",世界越饱和、越甜、越不对劲;通往真相的路,恰恰是那些系统禁止的悲伤。
|
||||||
|
|
||||||
|
## 启动方式
|
||||||
|
|
||||||
|
需要 Node.js 18+。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # 安装依赖
|
||||||
|
npm run dev # 开发模式(http://localhost:5173)
|
||||||
|
npm run build # 产出纯静态 dist/
|
||||||
|
npm run preview # 本地预览构建产物(http://localhost:4173)
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run build` 产出纯静态 `dist/`,可直接用任意静态服务器托管;`npm run preview` 本地游玩。
|
||||||
|
|
||||||
|
## 按键操作
|
||||||
|
|
||||||
|
| 按键 | 功能 |
|
||||||
|
|---|---|
|
||||||
|
| `W` `A` `S` `D` / 方向键 | 移动(四向动画 + 待机帧) |
|
||||||
|
| `E` / `空格` / `回车` | 交互 / 推进对话 / 选择 |
|
||||||
|
| `1` `2` `3` | 对话选项(温柔 / 迟疑 / 否定) |
|
||||||
|
| `` ` ``(反引号) | 开关 Debug 面板 |
|
||||||
|
|
||||||
|
## Debug 面板(服务验收)
|
||||||
|
|
||||||
|
运行中按反引号键 `` ` `` 开关。默认关闭,关闭时无任何 UI 痕迹,不影响正常游玩。
|
||||||
|
|
||||||
|
功能:
|
||||||
|
- 直接设置幸福值(10 / 40 / 70 / 90 / 100,分别对应 S0/S1/S2/S3/S4)
|
||||||
|
- 切换觉知值(0 / 50 / 100)
|
||||||
|
- 切换当前日(1–7 循环)
|
||||||
|
- 置齐全部线索(C1–C9)
|
||||||
|
- 传送到任意场景(小屋/广场/面包房/杂货店/诊所/湖畔/花田/古井/雾墙)
|
||||||
|
- 强制触发四个结局之一(E1 糖衣 / E2 回收 / E3 苏醒 / E4 静默)
|
||||||
|
|
||||||
|
## 游玩说明
|
||||||
|
|
||||||
|
- 开局输入名字(留空默认「小满」)。
|
||||||
|
- 七日流程:晨起 → 公告板接任务 → 完成任务/自由活动 → 黄昏钟声 → 回屋睡觉 → 梦境过场 → 次日。
|
||||||
|
- 完整流程约 25–35 分钟,含 4 个结局。
|
||||||
|
- 幸福值以左上角向日葵表盘表示(12 片花瓣),全程不显示数字。满格后花心浮现黑籽。
|
||||||
|
|
||||||
|
## 技术架构(PRD §12)
|
||||||
|
|
||||||
|
- **Phaser 3 + TypeScript + Vite**。逻辑分辨率 320×180,整数倍放大居中,`pixelArt: true`,禁用纹理平滑。
|
||||||
|
- 场景:`BootScene`(加载)→ `TitleScene`(标题/输入名字)→ 各地图 Scene + 常驻 `UIScene`(对话框、向日葵表盘、提示)→ `EndingScene`;另 `DebugScene`。
|
||||||
|
- `CorruptionDirector`(`src/systems/CorruptionDirector.ts`):普通 TS 单例 + 事件发射器,持有幸福/觉知/日期/任务标记/线索集合/侵蚀阶段,是全部状态与视听参数的**唯一真源**。
|
||||||
|
- 饱和度与晕影用 Phaser 相机 postFX(`SaturationPostFX`),负片闪帧用单帧白色覆盖切换。
|
||||||
|
- BGM 与音效按 §6.6 用 Web Audio 程序合成(方波主旋律 + 三角波低音 8 小节循环),playbackRate 与 BiquadFilter 低通由 `CorruptionDirector` 驱动。**无外部音频文件**。
|
||||||
|
- 无 localStorage、无外部网络请求、无外部音频/字体文件。
|
||||||
|
|
||||||
|
## 素材与地图制作流程(全部本项目内自制,无外部资产)
|
||||||
|
|
||||||
|
### 像素素材(Aseprite)
|
||||||
|
|
||||||
|
所有像素素材由 Aseprite 命令行 + Lua 脚本程序化绘制,脚本位于 `tools/`:
|
||||||
|
|
||||||
|
| 脚本 | 产物 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `tools/gen_tileset.lua` | `assets/tilesets/tileset.png`(128×128,64 个 16×16 瓦片) | 草地/石板路/湖水/花丛/莓果丛/树/房屋墙顶门/室内地板/家具(床/柜台/烤炉/货架/桌椅)/井/雾/白色层墙地等 |
|
||||||
|
| `tools/gen_player.lua` | `assets/sprites/player.png`(64×96) | 主角 16×24/帧,4 朝向 × 4 帧行走(walk-down/up/left/right),第 1 帧兼待机 |
|
||||||
|
| `tools/gen_portraits.lua` | `assets/sprites/portraits.png`(624×24) | 对话头像:主角微笑 + E3「真实的脸」;7 个具名 NPC 各 3 档表情(smile/flat/off);双子鸟;5 个群演 |
|
||||||
|
| `tools/gen_npc.lua` | `assets/sprites/npc.png`(400×24) | NPC 场景立绘 16×24 ×3 档;双子鸟 2 帧张嘴动画;5 个群演 |
|
||||||
|
|
||||||
|
重新生成素材(需本机 Aseprite,路径 `/Applications/Aseprite.app/Contents/MacOS/aseprite`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"/Applications/Aseprite.app/Contents/MacOS/aseprite" -b --script tools/gen_tileset.lua
|
||||||
|
"/Applications/Aseprite.app/Contents/MacOS/aseprite" -b --script tools/gen_player.lua
|
||||||
|
"/Applications/Aseprite.app/Contents/MacOS/aseprite" -b --script tools/gen_portraits.lua
|
||||||
|
"/Applications/Aseprite.app/Contents/MacOS/aseprite" -b --script tools/gen_npc.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
### 地图(Tiled Map Editor)
|
||||||
|
|
||||||
|
10 个场景地图为标准 Tiled JSON 格式(`assets/maps/*.json`),由 `tools/gen_maps.cjs` 程序化生成;瓦片集描述符由 `tools/gen_tileset_json.cjs` 生成(`assets/tilesets/tileset.json`,含 `collides` 属性)。
|
||||||
|
|
||||||
|
统一结构:`ground` / `obstacles`(`collides=true`)/ `objects`(`spawn`、NPC 点位、`door`、交互点)。
|
||||||
|
|
||||||
|
| 场景 | 尺寸(瓦片) | 要点 |
|
||||||
|
|---|---|---|
|
||||||
|
| 小满小屋(室内) | 12×8 | 床、床头日历(C1) |
|
||||||
|
| 村广场 | 30×20 | 公告板、花坛、双子鸟树、庆典场地;枢纽 |
|
||||||
|
| 面包房(室内) | 15×10 | 柜台、烤炉、配方纸(C2) |
|
||||||
|
| 杂货店(室内) | 15×10 | 货架(C5) |
|
||||||
|
| 诊所(室内) | 12×8 | 芙医生、桌上表格(C8) |
|
||||||
|
| 湖畔 | 24×16 | 长椅(龟爷爷)、野餐布、湖面倒影 |
|
||||||
|
| 花田 | 20×14 | 莓果丛(T6)、浇花点 |
|
||||||
|
| 古井空地 | 14×10 | 古井(C7) |
|
||||||
|
| 雾墙边界 | 20×8 | 村庄尽头的雾,E3 入口 |
|
||||||
|
| 白色层 | 20×14 | 走廊 + 病房,去饱和白灰色板 |
|
||||||
|
|
||||||
|
重新生成地图(需本机 Tiled,路径 `/Applications/Tiled.app/Contents/MacOS/tiled`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/gen_tileset_json.cjs
|
||||||
|
node tools/gen_maps.cjs
|
||||||
|
# 可选:用 Tiled 验证导出
|
||||||
|
"/Applications/Tiled.app/Contents/MacOS/tiled" --export-map assets/maps/plaza.json /tmp/plaza_verify.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── index.html
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
├── vite.config.ts
|
||||||
|
├── assets/
|
||||||
|
│ ├── maps/ # 10 个 Tiled JSON 地图
|
||||||
|
│ ├── tilesets/ # tileset.png + tileset.json
|
||||||
|
│ └── sprites/ # player.png / npc.png / portraits.png
|
||||||
|
├── tools/ # Aseprite Lua + Node 生成脚本
|
||||||
|
└── src/
|
||||||
|
├── main.ts # Phaser 配置入口
|
||||||
|
├── data/ # gamedata.ts(全部写死的台词/数值/表格)、mapsMeta.ts
|
||||||
|
├── systems/ # CorruptionDirector / AudioEngine / TextCorruptor / StoryDirector
|
||||||
|
├── entities/ # Player
|
||||||
|
└── scenes/ # BootScene / TitleScene / UIScene / MapScene / 10 地图 / EndingScene / DebugScene
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |