Honey Village: six model builds + hub frontend, Dockerized for Dokploy
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
test-results/
|
||||
playwright-report/
|
||||
.DS_Store
|
||||
.qa/
|
||||
@@ -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,57 @@
|
||||
# Broken Fairytale: Honey Village
|
||||
|
||||
Single-player browser narrative game built from the specification in `PRD.md`. The runtime uses Phaser 3, TypeScript, and Vite. It has no backend, persistence, external network requests, external fonts, or recorded audio.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run generate
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Production output:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
The preview server defaults to `http://127.0.0.1:4173`.
|
||||
|
||||
## Controls
|
||||
|
||||
| Input | Action |
|
||||
|---|---|
|
||||
| `WASD` / arrow keys | Move |
|
||||
| `E` / Enter | Interact, advance dialogue, confirm a choice |
|
||||
| Up / Down | Select a dialogue choice |
|
||||
| `1` / `2` / `3` | Select a dialogue choice directly |
|
||||
| `` ` `` | Toggle the Debug panel |
|
||||
|
||||
## Debug panel
|
||||
|
||||
The Debug panel is hidden by default and leaves no visible UI while closed. It can set happiness, awareness, and the current day; teleport to any of the ten scenes; discover all nine clues; and force endings E1–E4. Debug state exists only in memory and resets when the title flow starts again.
|
||||
|
||||
## Original asset pipeline
|
||||
|
||||
Every shipped pixel asset was produced for this project by `tools/generate-assets.lua` through Aseprite 1.3.17.2. Editable sources are stored in `public/assets/source/`; exported RGBA sprite sheets are stored in `public/assets/`. The 256×64 tileset contains 64 original 16×16 tiles. `characters.png` contains the player's four directions and four walk frames, seven named NPC expression tiers, five extras, and two-frame bird animation. `portraits.png` contains the fixed smiling player portrait, E3 real-face portrait, and seven named NPC portraits.
|
||||
|
||||
Every map was produced for this project by `tools/generate-maps.mjs` and exported through Tiled 1.12.2. Editable TMX sources live in `public/maps/source/`; Tiled JSON exports live in `public/maps/`. Each map contains `ground`, `obstacles`, and `objects` layers. The object layer contains spawn points, paired doors, NPC positions, and interaction points.
|
||||
|
||||
The generation commands use the installed macOS application binaries. Custom locations can be supplied without changing the repository:
|
||||
|
||||
```bash
|
||||
ASEPRITE=/path/to/aseprite TILED=/path/to/tiled npm run generate
|
||||
```
|
||||
|
||||
The music loop, bird voice, dialogue beep, task chime, dusk bell, and E2 monitor beep are synthesized at runtime with Web Audio. No audio files are shipped.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
The Playwright suite checks title/name entry, movement, all ten scene loads, Debug controls, corruption stages, all clues, an ending path, console errors, and integer scaling at 960×540 and 640×360.
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#7acb59" />
|
||||
<title>崩坏童话:蜜糖村</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="game" aria-label="崩坏童话:蜜糖村游戏画面"></main>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1258
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "broken-fairytale-honey-village",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"assets": "\"${ASEPRITE:-/Applications/Aseprite.app/Contents/MacOS/aseprite}\" -b --script-param root=\"$PWD\" --script tools/generate-assets.lua",
|
||||
"maps:source": "node tools/generate-maps.mjs",
|
||||
"maps": "npm run maps:source && node tools/export-maps.mjs",
|
||||
"generate": "npm run assets && npm run maps",
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"phaser": "^3.90.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@types/node": "^26.1.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
timeout: 30_000,
|
||||
expect: { timeout: 5_000 },
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:4173',
|
||||
viewport: { width: 960, height: 540 },
|
||||
deviceScaleFactor: 1,
|
||||
colorScheme: 'dark',
|
||||
},
|
||||
reporter: 'line',
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 952 B |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,319 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":10,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4,
|
||||
4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 13, 5, 5, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 10, 4, 4, 4, 4, 4, 4, 4],
|
||||
"height":10,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":15,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":240,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":240,
|
||||
"x":0,
|
||||
"y":156
|
||||
},
|
||||
{
|
||||
"height":160,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":160,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":236,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":32,
|
||||
"id":910,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":208,
|
||||
"x":16,
|
||||
"y":16
|
||||
},
|
||||
{
|
||||
"height":30,
|
||||
"id":911,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":32,
|
||||
"x":176,
|
||||
"y":32
|
||||
},
|
||||
{
|
||||
"height":12,
|
||||
"id":912,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":176,
|
||||
"x":32,
|
||||
"y":80
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":120,
|
||||
"y":128
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"bakery"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":120,
|
||||
"y":144
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"\u5706\u5706",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"yuanyuan"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5706\u5706"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":120,
|
||||
"y":72
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"\u914d\u65b9\u7eb8",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"recipe"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u914d\u65b9\u7eb8"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":48,
|
||||
"y":46
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":5,
|
||||
"name":"\u70e4\u7089",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"oven"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u70e4\u7089"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":184,
|
||||
"y":50
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u9762\u5305\u623f"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"BakeryScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":15
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":8,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 5, 5, 5, 5, 5, 6, 6, 15, 6, 6,
|
||||
6, 6, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 10, 6, 6, 6, 6, 6],
|
||||
"height":8,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":12,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":192,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":192,
|
||||
"x":0,
|
||||
"y":124
|
||||
},
|
||||
{
|
||||
"height":128,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":128,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":188,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":32,
|
||||
"id":910,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":80,
|
||||
"x":32,
|
||||
"y":32
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":96,
|
||||
"y":96
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"clinic"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":96,
|
||||
"y":112
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"\u8299\u533b\u751f",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"doctor"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u8299\u533b\u751f"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":96,
|
||||
"y":50
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"\u684c\u4e0a\u8868\u683c",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"form"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u684c\u4e0a\u8868\u683c"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":48,
|
||||
"y":42
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u8bca\u6240"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"ClinicScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":12
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":14,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 7, 2, 2, 8, 2, 2, 7, 2, 2, 8, 2, 2, 7, 2, 2, 8, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 7, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
"height":14,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":320,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":320,
|
||||
"x":0,
|
||||
"y":220
|
||||
},
|
||||
{
|
||||
"height":224,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":224,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":316,
|
||||
"y":0
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":16,
|
||||
"y":112
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"field"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":0,
|
||||
"y":112
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"\u6751\u6c11",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"extra3"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u6751\u6c11"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":244,
|
||||
"y":148
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"\u8393\u679c\u4e1b",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry1"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u8393\u679c\u4e1b"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":80,
|
||||
"y":72
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":5,
|
||||
"name":"\u8393\u679c\u4e1b",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry2"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u8393\u679c\u4e1b"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":128,
|
||||
"y":56
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":6,
|
||||
"name":"\u8393\u679c\u4e1b",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry3"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u8393\u679c\u4e1b"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":176,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":7,
|
||||
"name":"\u8393\u679c\u4e1b",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry4"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u8393\u679c\u4e1b"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":224,
|
||||
"y":64
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":8,
|
||||
"name":"\u8393\u679c\u4e1b",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"berry5"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u8393\u679c\u4e1b"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":272,
|
||||
"y":96
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u82b1\u7530"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"FieldScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":20
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":8,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 5, 5, 5, 4, 4, 4, 11, 4, 4, 4,
|
||||
4, 4, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 8, 8, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
5, 5, 5, 5, 5, 5, 10, 5, 5, 5, 5, 5],
|
||||
"height":8,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":12,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":192,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":192,
|
||||
"x":0,
|
||||
"y":124
|
||||
},
|
||||
{
|
||||
"height":128,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":128,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":188,
|
||||
"y":0
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":96,
|
||||
"y":88
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"house"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":96,
|
||||
"y":112
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"\u5e8a\u5934\u65e5\u5386",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"calendar"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5e8a\u5934\u65e5\u5386"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":42,
|
||||
"y":34
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"\u5e8a",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"bed"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5e8a"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":64,
|
||||
"y":42
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u5c0f\u6ee1\u5c0f\u5c4b"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"HouseScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":12
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":16,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 5, 5, 5, 8, 8, 8, 2, 2, 2, 2, 2, 2, 2,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 5, 5, 5, 8, 8, 8, 1, 1, 1, 12, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":24,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":384,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":384,
|
||||
"x":0,
|
||||
"y":252
|
||||
},
|
||||
{
|
||||
"height":256,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":256,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":380,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":256,
|
||||
"id":910,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":150,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":368,
|
||||
"y":128
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"lake"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":368,
|
||||
"y":128
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"\u9f9f\u7237\u7237",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"turtle"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u9f9f\u7237\u7237"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":142,
|
||||
"y":132
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"\u6751\u6c11",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"extra4"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u6751\u6c11"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":284,
|
||||
"y":68
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":5,
|
||||
"name":"\u91ce\u9910\u5e03",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"picnic"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u91ce\u9910\u5e03"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":190,
|
||||
"y":148
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":6,
|
||||
"name":"\u6e56\u9762",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"lake"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u6e56\u9762"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":110,
|
||||
"y":68
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u6e56\u7554"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"LakeScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":24
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":8,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15, 15, 15, 15],
|
||||
"height":8,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":320,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":320,
|
||||
"x":0,
|
||||
"y":124
|
||||
},
|
||||
{
|
||||
"height":128,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":128,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":316,
|
||||
"y":0
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":16,
|
||||
"y":64
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"mist"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":0,
|
||||
"y":64
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"\u96fe\u5899",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"mist"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u96fe\u5899"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":288,
|
||||
"y":64
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u96fe\u5899\u8fb9\u754c"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"MistScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":20
|
||||
}
|
||||
@@ -0,0 +1,965 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":20,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1,
|
||||
1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1,
|
||||
1, 13, 4, 4, 4, 4, 4, 4, 4, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4, 13, 1,
|
||||
1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 12, 12, 12, 2, 2, 2, 2, 1, 1, 1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1,
|
||||
1, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 13, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1,
|
||||
1, 1, 4, 4, 4, 10, 4, 4, 4, 13, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 11, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 17, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 8, 2, 8, 2, 8, 2, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
1, 1, 1, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2, 2, 8, 1, 1, 1, 1, 1, 1, 1, 13, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1,
|
||||
1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 1, 1,
|
||||
1, 1, 4, 4, 4, 4, 4, 4, 4, 1, 13, 1, 1, 2, 2, 2, 2, 1, 1, 13, 1, 4, 4, 4, 4, 4, 4, 4, 1, 1,
|
||||
1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 15, 4, 4, 15, 4, 4, 1, 1,
|
||||
1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 4, 4, 4, 10, 4, 4, 4, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
"height":20,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":30,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":480,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":480,
|
||||
"x":0,
|
||||
"y":316
|
||||
},
|
||||
{
|
||||
"height":320,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":320,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":476,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":96,
|
||||
"id":910,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":112,
|
||||
"x":32,
|
||||
"y":16
|
||||
},
|
||||
{
|
||||
"height":96,
|
||||
"id":911,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":112,
|
||||
"x":336,
|
||||
"y":16
|
||||
},
|
||||
{
|
||||
"height":76,
|
||||
"id":912,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":112,
|
||||
"x":32,
|
||||
"y":224
|
||||
},
|
||||
{
|
||||
"height":76,
|
||||
"id":913,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":112,
|
||||
"x":336,
|
||||
"y":224
|
||||
},
|
||||
{
|
||||
"height":48,
|
||||
"id":914,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":48,
|
||||
"x":160,
|
||||
"y":32
|
||||
},
|
||||
{
|
||||
"height":24,
|
||||
"id":915,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":32,
|
||||
"x":232,
|
||||
"y":128
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":260
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"house",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"house"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":280
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"bakery",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"bakery"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":32,
|
||||
"y":160
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"shop",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"shop"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":448,
|
||||
"y":160
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":5,
|
||||
"name":"clinic",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"clinic"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":32
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":6,
|
||||
"name":"lake",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"lake"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":32,
|
||||
"y":48
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":7,
|
||||
"name":"field",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"field"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":448,
|
||||
"y":48
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":8,
|
||||
"name":"well",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"well"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":32,
|
||||
"y":280
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":9,
|
||||
"name":"mist",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"mist"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":448,
|
||||
"y":280
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":10,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"HouseScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":304
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":11,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"BakeryScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":0,
|
||||
"y":160
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":12,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"ShopScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":464,
|
||||
"y":160
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":13,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"ClinicScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":14,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"LakeScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":0,
|
||||
"y":48
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":15,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"FieldScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":464,
|
||||
"y":48
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":16,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"WellScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":0,
|
||||
"y":280
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":17,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"MistScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":464,
|
||||
"y":280
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":18,
|
||||
"name":"\u963f\u798f",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"afu"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u963f\u798f"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":132
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":19,
|
||||
"name":"\u5927\u5b9d",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"dabao"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5927\u5b9d"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":180,
|
||||
"y":162
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":20,
|
||||
"name":"\u53cc\u5b50\u9e1f",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"birds"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u53cc\u5b50\u9e1f"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":232,
|
||||
"y":76
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":21,
|
||||
"name":"\u6751\u6c11",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"extra1"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u6751\u6c11"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":124,
|
||||
"y":210
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":22,
|
||||
"name":"\u6751\u6c11",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"extra2"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u6751\u6c11"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":352,
|
||||
"y":224
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":23,
|
||||
"name":"\u516c\u544a\u677f",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"board"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u516c\u544a\u677f"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":146
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":24,
|
||||
"name":"\u6563\u843d\u4fe1\u4ef6",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"mail"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u6563\u843d\u4fe1\u4ef6"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":92,
|
||||
"y":148
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":25,
|
||||
"name":"\u82b1\u575b",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"flower1"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u82b1\u575b"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":200,
|
||||
"y":184
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":26,
|
||||
"name":"\u82b1\u575b",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"flower2"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u82b1\u575b"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":200
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":27,
|
||||
"name":"\u82b1\u575b",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"flower3"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u82b1\u575b"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":280,
|
||||
"y":184
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":28,
|
||||
"name":"\u5f69\u5e26\u67b6",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"ribbon1"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5f69\u5e26\u67b6"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":160,
|
||||
"y":100
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":29,
|
||||
"name":"\u5f69\u5e26\u67b6",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"ribbon2"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5f69\u5e26\u67b6"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":224,
|
||||
"y":92
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":30,
|
||||
"name":"\u5f69\u5e26\u67b6",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"ribbon3"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5f69\u5e26\u67b6"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":288,
|
||||
"y":92
|
||||
},
|
||||
|
||||
{
|
||||
"height":16,
|
||||
"id":31,
|
||||
"name":"\u5f69\u5e26\u67b6",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"ribbon4"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5f69\u5e26\u67b6"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":352,
|
||||
"y":100
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":32,
|
||||
"name":"\u5e86\u5178\u573a\u5730",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"festival"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u5e86\u5178\u573a\u5730"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":240,
|
||||
"y":224
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u6751\u5e7f\u573a"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":30
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":10,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 5, 5, 4, 5, 5, 4, 5, 5, 4, 5, 5, 4, 5, 5,
|
||||
4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5,
|
||||
4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5,
|
||||
4, 5, 7, 4, 5, 7, 4, 5, 7, 4, 5, 5, 4, 5, 5,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 10, 4, 4, 4, 4, 4, 4, 4],
|
||||
"height":10,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":15,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":240,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":240,
|
||||
"x":0,
|
||||
"y":156
|
||||
},
|
||||
{
|
||||
"height":160,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":160,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":236,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":64,
|
||||
"id":910,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":32,
|
||||
"x":16,
|
||||
"y":16
|
||||
},
|
||||
{
|
||||
"height":64,
|
||||
"id":911,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":32,
|
||||
"x":64,
|
||||
"y":16
|
||||
},
|
||||
{
|
||||
"height":64,
|
||||
"id":912,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":32,
|
||||
"x":112,
|
||||
"y":16
|
||||
},
|
||||
{
|
||||
"height":64,
|
||||
"id":913,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":32,
|
||||
"x":160,
|
||||
"y":16
|
||||
},
|
||||
{
|
||||
"height":64,
|
||||
"id":914,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":208,
|
||||
"y":16
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":120,
|
||||
"y":128
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"shop"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":120,
|
||||
"y":144
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"\u54aa\u54aa",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"mimi"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u54aa\u54aa"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":120,
|
||||
"y":72
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"\u8d27\u67b6",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"shelf"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u8d27\u67b6"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":48,
|
||||
"y":52
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u6742\u8d27\u5e97"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"ShopScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":15
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":10,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1,
|
||||
1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1,
|
||||
1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1,
|
||||
1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 10,
|
||||
1, 1, 1, 2, 2, 14, 14, 14, 14, 2, 2, 1, 1, 1,
|
||||
1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
"height":10,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":14,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":224,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":224,
|
||||
"x":0,
|
||||
"y":156
|
||||
},
|
||||
{
|
||||
"height":160,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":160,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":220,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":64,
|
||||
"id":910,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":64,
|
||||
"x":80,
|
||||
"y":48
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":208,
|
||||
"y":80
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"undefined",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"spawn",
|
||||
"type":"string",
|
||||
"value":"well"
|
||||
},
|
||||
{
|
||||
"name":"target",
|
||||
"type":"string",
|
||||
"value":"PlazaScene"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"door",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":208,
|
||||
"y":80
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"\u6751\u6c11",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"extra5"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u6751\u6c11"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"npc",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":46,
|
||||
"y":116
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"\u53e4\u4e95",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"well"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u53e4\u4e95"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":104,
|
||||
"y":72
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u53e4\u4e95\u7a7a\u5730"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"WellScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":14
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":8,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 5, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16],
|
||||
"height":8,
|
||||
"id":1,
|
||||
"name":"ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":30,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":2,
|
||||
"name":"obstacles",
|
||||
"objects":[
|
||||
{
|
||||
"height":4,
|
||||
"id":900,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":480,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":4,
|
||||
"id":901,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":480,
|
||||
"x":0,
|
||||
"y":124
|
||||
},
|
||||
{
|
||||
"height":128,
|
||||
"id":902,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":128,
|
||||
"id":903,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":476,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"height":96,
|
||||
"id":910,
|
||||
"name":"",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":304,
|
||||
"y":16
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"objects",
|
||||
"objects":[
|
||||
{
|
||||
"height":16,
|
||||
"id":1,
|
||||
"name":"default",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"default"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"spawn",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":24,
|
||||
"y":64
|
||||
},
|
||||
{
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"\u75c5\u5386",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"id",
|
||||
"type":"string",
|
||||
"value":"record"
|
||||
},
|
||||
{
|
||||
"name":"label",
|
||||
"type":"string",
|
||||
"value":"\u75c5\u5386"
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"interact",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":400,
|
||||
"y":64
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":1000,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"displayName",
|
||||
"type":"string",
|
||||
"value":"\u767d\u8272\u5c42"
|
||||
},
|
||||
{
|
||||
"name":"sceneKey",
|
||||
"type":"string",
|
||||
"value":"WhiteScene"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.12.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"..\/assets\/tileset.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":256,
|
||||
"margin":0,
|
||||
"name":"honey-village",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":30
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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="1000">
|
||||
<properties><property name="sceneKey" value="BakeryScene"/><property name="displayName" value="面包房"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="15" height="10"><data encoding="csv">4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,4,4,5,5,5,5,5,5,5,5,5,5,13,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,10,4,4,4,4,4,4,4</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="240" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="156" width="240" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="236" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="910" x="16" y="16" width="208" height="32"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="911" x="176" y="32" width="32" height="30"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="912" x="32" y="80" width="176" height="12"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="120" y="128" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="undefined" type="door" x="120" y="144" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="PlazaScene"/>
|
||||
<property name="spawn" value="bakery"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="圆圆" type="npc" x="120" y="72" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="yuanyuan"/>
|
||||
<property name="label" value="圆圆"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="配方纸" type="interact" x="48" y="46" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="recipe"/>
|
||||
<property name="label" value="配方纸"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="5" name="烤炉" type="interact" x="184" y="50" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="oven"/>
|
||||
<property name="label" value="烤炉"/>
|
||||
</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="1000">
|
||||
<properties><property name="sceneKey" value="ClinicScene"/><property name="displayName" value="诊所"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="12" height="8"><data encoding="csv">16,16,16,16,16,16,16,16,16,16,16,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,5,5,5,5,5,6,6,15,6,6,6,6,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,10,6,6,6,6,6</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="192" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="124" width="192" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="188" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="910" x="32" y="32" width="80" height="32"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="96" y="96" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="undefined" type="door" x="96" y="112" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="PlazaScene"/>
|
||||
<property name="spawn" value="clinic"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="芙医生" type="npc" x="96" y="50" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="doctor"/>
|
||||
<property name="label" value="芙医生"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="桌上表格" type="interact" x="48" y="42" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="form"/>
|
||||
<property name="label" value="桌上表格"/>
|
||||
</properties>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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="1000">
|
||||
<properties><property name="sceneKey" value="FieldScene"/><property name="displayName" value="花田"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="20" height="14"><data encoding="csv">1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,7,2,2,8,2,2,7,2,2,8,2,2,7,2,2,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,1,1,7,1,1,8,1,1,7,1,1,8,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="320" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="220" width="320" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="224"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="316" y="0" width="4" height="224"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="16" y="112" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="undefined" type="door" x="0" y="112" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="PlazaScene"/>
|
||||
<property name="spawn" value="field"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="村民" type="npc" x="244" y="148" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="extra3"/>
|
||||
<property name="label" value="村民"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="莓果丛" type="interact" x="80" y="72" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="berry1"/>
|
||||
<property name="label" value="莓果丛"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="5" name="莓果丛" type="interact" x="128" y="56" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="berry2"/>
|
||||
<property name="label" value="莓果丛"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="6" name="莓果丛" type="interact" x="176" y="88" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="berry3"/>
|
||||
<property name="label" value="莓果丛"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="7" name="莓果丛" type="interact" x="224" y="64" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="berry4"/>
|
||||
<property name="label" value="莓果丛"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="8" name="莓果丛" type="interact" x="272" y="96" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="berry5"/>
|
||||
<property name="label" value="莓果丛"/>
|
||||
</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="1000">
|
||||
<properties><property name="sceneKey" value="HouseScene"/><property name="displayName" value="小满小屋"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="12" height="8"><data encoding="csv">5,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,4,4,4,11,4,4,4,4,4,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,8,8,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,10,5,5,5,5,5</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="192" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="124" width="192" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="188" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="96" y="88" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="undefined" type="door" x="96" y="112" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="PlazaScene"/>
|
||||
<property name="spawn" value="house"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="床头日历" type="interact" x="42" y="34" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="calendar"/>
|
||||
<property name="label" value="床头日历"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="床" type="interact" x="64" y="42" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="bed"/>
|
||||
<property name="label" value="床"/>
|
||||
</properties>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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="1000">
|
||||
<properties><property name="sceneKey" value="LakeScene"/><property name="displayName" value="湖畔"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="24" height="16"><data encoding="csv">3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,2,5,5,5,8,8,8,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,1,5,5,5,8,8,8,1,1,1,12,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="384" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="252" width="384" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="256"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="380" y="0" width="4" height="256"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="910" x="0" y="0" width="150" height="256"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="368" y="128" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="undefined" type="door" x="368" y="128" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="PlazaScene"/>
|
||||
<property name="spawn" value="lake"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="龟爷爷" type="npc" x="142" y="132" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="turtle"/>
|
||||
<property name="label" value="龟爷爷"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="村民" type="npc" x="284" y="68" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="extra4"/>
|
||||
<property name="label" value="村民"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="5" name="野餐布" type="interact" x="190" y="148" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="picnic"/>
|
||||
<property name="label" value="野餐布"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="6" name="湖面" type="interact" x="110" y="68" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="lake"/>
|
||||
<property name="label" value="湖面"/>
|
||||
</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="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1000">
|
||||
<properties><property name="sceneKey" value="MistScene"/><property name="displayName" value="雾墙边界"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="20" height="8"><data encoding="csv">1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,15,15,15,15</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="320" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="124" width="320" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="316" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="16" y="64" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="undefined" type="door" x="0" y="64" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="PlazaScene"/>
|
||||
<property name="spawn" value="mist"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="雾墙" type="interact" x="288" y="64" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="mist"/>
|
||||
<property name="label" value="雾墙"/>
|
||||
</properties>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,201 @@
|
||||
<?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="1000">
|
||||
<properties><property name="sceneKey" value="PlazaScene"/><property name="displayName" value="村广场"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="30" height="20"><data encoding="csv">1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,9,9,9,9,9,9,9,1,12,12,12,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,13,4,4,4,4,4,4,4,1,12,12,12,2,2,2,2,1,1,1,1,4,4,4,4,4,4,4,13,1,1,1,4,15,4,4,15,4,4,1,12,12,12,2,2,2,2,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,4,4,4,4,4,4,4,1,1,1,1,2,2,2,2,1,1,13,1,4,4,4,4,4,4,4,1,1,1,1,4,4,4,10,4,4,4,13,1,1,1,2,2,2,2,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,11,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,17,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,8,2,8,2,8,2,8,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,13,1,1,1,1,1,1,1,1,8,2,2,2,8,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,2,2,2,2,1,1,1,1,9,9,9,9,9,9,9,1,1,1,1,4,4,4,4,4,4,4,1,13,1,1,2,2,2,2,1,1,13,1,4,4,4,4,4,4,4,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,2,2,2,2,1,1,1,1,4,15,4,4,15,4,4,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,2,2,2,2,1,1,1,1,4,4,4,10,4,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="480" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="316" width="480" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="320"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="476" y="0" width="4" height="320"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="910" x="32" y="16" width="112" height="96"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="911" x="336" y="16" width="112" height="96"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="912" x="32" y="224" width="112" height="76"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="913" x="336" y="224" width="112" height="76"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="914" x="160" y="32" width="48" height="48"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="915" x="232" y="128" width="32" height="24"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="240" y="260" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="house" type="spawn" x="240" y="280" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="house"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="bakery" type="spawn" x="32" y="160" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="bakery"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="shop" type="spawn" x="448" y="160" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="shop"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="5" name="clinic" type="spawn" x="240" y="32" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="clinic"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="6" name="lake" type="spawn" x="32" y="48" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="lake"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="7" name="field" type="spawn" x="448" y="48" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="field"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="8" name="well" type="spawn" x="32" y="280" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="well"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="9" name="mist" type="spawn" x="448" y="280" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="mist"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="10" name="undefined" type="door" x="240" y="304" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="HouseScene"/>
|
||||
<property name="spawn" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="11" name="undefined" type="door" x="0" y="160" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="BakeryScene"/>
|
||||
<property name="spawn" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="12" name="undefined" type="door" x="464" y="160" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="ShopScene"/>
|
||||
<property name="spawn" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="13" name="undefined" type="door" x="240" y="0" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="ClinicScene"/>
|
||||
<property name="spawn" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="14" name="undefined" type="door" x="0" y="48" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="LakeScene"/>
|
||||
<property name="spawn" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="15" name="undefined" type="door" x="464" y="48" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="FieldScene"/>
|
||||
<property name="spawn" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="16" name="undefined" type="door" x="0" y="280" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="WellScene"/>
|
||||
<property name="spawn" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="17" name="undefined" type="door" x="464" y="280" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="MistScene"/>
|
||||
<property name="spawn" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="18" name="阿福" type="npc" x="240" y="132" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="afu"/>
|
||||
<property name="label" value="阿福"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="19" name="大宝" type="npc" x="180" y="162" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="dabao"/>
|
||||
<property name="label" value="大宝"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="20" name="双子鸟" type="npc" x="232" y="76" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="birds"/>
|
||||
<property name="label" value="双子鸟"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="21" name="村民" type="npc" x="124" y="210" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="extra1"/>
|
||||
<property name="label" value="村民"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="22" name="村民" type="npc" x="352" y="224" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="extra2"/>
|
||||
<property name="label" value="村民"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="23" name="公告板" type="interact" x="240" y="146" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="board"/>
|
||||
<property name="label" value="公告板"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="24" name="散落信件" type="interact" x="92" y="148" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="mail"/>
|
||||
<property name="label" value="散落信件"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="25" name="花坛" type="interact" x="200" y="184" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="flower1"/>
|
||||
<property name="label" value="花坛"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="26" name="花坛" type="interact" x="240" y="200" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="flower2"/>
|
||||
<property name="label" value="花坛"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="27" name="花坛" type="interact" x="280" y="184" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="flower3"/>
|
||||
<property name="label" value="花坛"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="28" name="彩带架" type="interact" x="160" y="100" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="ribbon1"/>
|
||||
<property name="label" value="彩带架"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="29" name="彩带架" type="interact" x="224" y="92" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="ribbon2"/>
|
||||
<property name="label" value="彩带架"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="30" name="彩带架" type="interact" x="288" y="92" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="ribbon3"/>
|
||||
<property name="label" value="彩带架"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="31" name="彩带架" type="interact" x="352" y="100" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="ribbon4"/>
|
||||
<property name="label" value="彩带架"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="32" name="庆典场地" type="interact" x="240" y="224" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="festival"/>
|
||||
<property name="label" value="庆典场地"/>
|
||||
</properties>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -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="1000">
|
||||
<properties><property name="sceneKey" value="ShopScene"/><property name="displayName" value="杂货店"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="15" height="10"><data encoding="csv">4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,4,5,5,4,5,5,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,5,7,4,5,7,4,5,7,4,5,5,4,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,10,4,4,4,4,4,4,4</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="240" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="156" width="240" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="236" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="910" x="16" y="16" width="32" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="911" x="64" y="16" width="32" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="912" x="112" y="16" width="32" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="913" x="160" y="16" width="32" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="914" x="208" y="16" width="16" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="120" y="128" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="undefined" type="door" x="120" y="144" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="PlazaScene"/>
|
||||
<property name="spawn" value="shop"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="咪咪" type="npc" x="120" y="72" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="mimi"/>
|
||||
<property name="label" value="咪咪"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="货架" type="interact" x="48" y="52" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="shelf"/>
|
||||
<property name="label" value="货架"/>
|
||||
</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="14" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1000">
|
||||
<properties><property name="sceneKey" value="WellScene"/><property name="displayName" value="古井空地"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="14" height="10"><data encoding="csv">1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,14,14,14,14,2,2,1,1,10,1,1,1,2,2,14,14,14,14,2,2,1,1,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="224" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="156" width="224" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="220" y="0" width="4" height="160"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="910" x="80" y="48" width="64" height="64"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="208" y="80" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="undefined" type="door" x="208" y="80" width="16" height="16">
|
||||
<properties>
|
||||
<property name="target" value="PlazaScene"/>
|
||||
<property name="spawn" value="well"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="3" name="村民" type="npc" x="46" y="116" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="extra5"/>
|
||||
<property name="label" value="村民"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="古井" type="interact" x="104" y="72" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="well"/>
|
||||
<property name="label" value="古井"/>
|
||||
</properties>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1000">
|
||||
<properties><property name="sceneKey" value="WhiteScene"/><property name="displayName" value="白色层"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="30" height="8"><data encoding="csv">16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,16,6,6,6,6,6,6,6,6,6,6,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16</data></layer>
|
||||
<objectgroup id="2" name="obstacles"><object id="900" x="0" y="0" width="480" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="901" x="0" y="124" width="480" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="902" x="0" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="903" x="476" y="0" width="4" height="128"><properties><property name="collides" type="bool" value="true"/></properties></object>
|
||||
<object id="910" x="304" y="16" width="16" height="96"><properties><property name="collides" type="bool" value="true"/></properties></object></objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="1" name="default" type="spawn" x="24" y="64" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="default"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="2" name="病历" type="interact" x="400" y="64" width="16" height="16">
|
||||
<properties>
|
||||
<property name="id" value="record"/>
|
||||
<property name="label" value="病历"/>
|
||||
</properties>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.2" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16">
|
||||
<image source="../../assets/tileset.png" width="256" height="64"/>
|
||||
</tileset>
|
||||
@@ -0,0 +1,116 @@
|
||||
import { director } from './CorruptionDirector';
|
||||
|
||||
const RATE = { S0: 1, S1: 1, S2: 0.97, S3: 0.92, S4: 0.84 } as const;
|
||||
const CUTOFF = { S0: 20_000, S1: 20_000, S2: 20_000, S3: 8_000, S4: 4_000 } as const;
|
||||
|
||||
export class AudioDirector {
|
||||
private context?: AudioContext;
|
||||
private music?: AudioBufferSourceNode;
|
||||
private birds?: AudioBufferSourceNode;
|
||||
private musicGain?: GainNode;
|
||||
private birdGain?: GainNode;
|
||||
private filter?: BiquadFilterNode;
|
||||
private master?: GainNode;
|
||||
|
||||
unlock(): void {
|
||||
if (!this.context) this.create();
|
||||
void this.context?.resume();
|
||||
}
|
||||
|
||||
private create(): void {
|
||||
const context = new AudioContext();
|
||||
this.context = context;
|
||||
this.master = context.createGain();
|
||||
this.master.gain.value = 0.28;
|
||||
this.master.connect(context.destination);
|
||||
this.filter = context.createBiquadFilter();
|
||||
this.filter.type = 'lowpass';
|
||||
this.filter.connect(this.master);
|
||||
this.musicGain = context.createGain();
|
||||
this.musicGain.gain.value = 0.34;
|
||||
this.musicGain.connect(this.filter);
|
||||
this.birdGain = context.createGain();
|
||||
this.birdGain.gain.value = 0.12;
|
||||
this.birdGain.connect(this.master);
|
||||
this.music = context.createBufferSource();
|
||||
this.music.buffer = this.makeMusicBuffer(context);
|
||||
this.music.loop = true;
|
||||
this.music.connect(this.musicGain);
|
||||
this.music.start();
|
||||
this.birds = context.createBufferSource();
|
||||
this.birds.buffer = this.makeBirdBuffer(context);
|
||||
this.birds.loop = true;
|
||||
this.birds.connect(this.birdGain);
|
||||
this.birds.start();
|
||||
this.applyCorruption();
|
||||
director.on('change', this.applyCorruption, this);
|
||||
}
|
||||
|
||||
private makeMusicBuffer(context: AudioContext): AudioBuffer {
|
||||
const sampleRate = context.sampleRate;
|
||||
const duration = 16;
|
||||
const buffer = context.createBuffer(1, sampleRate * duration, sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
const melody = [523.25, 659.25, 783.99, 659.25, 587.33, 698.46, 880, 698.46, 523.25, 659.25, 783.99, 1046.5, 880, 783.99, 659.25, 587.33];
|
||||
const bass = [130.81, 146.83, 164.81, 146.83, 130.81, 174.61, 146.83, 130.81];
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const t = i / sampleRate;
|
||||
const note = melody[Math.floor(t * 2) % melody.length];
|
||||
const low = bass[Math.floor(t / 2) % bass.length];
|
||||
const envelope = Math.min(1, (t % 0.5) * 20) * Math.max(0, 1 - (t % 0.5) * 0.9);
|
||||
const square = Math.sin(2 * Math.PI * note * t) >= 0 ? 1 : -1;
|
||||
const triangle = 2 * Math.abs(2 * ((t * low) % 1) - 1) - 1;
|
||||
data[i] = (square * 0.17 * envelope) + (triangle * 0.12);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private makeBirdBuffer(context: AudioContext): AudioBuffer {
|
||||
const duration = 4;
|
||||
const buffer = context.createBuffer(1, context.sampleRate * duration, context.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const t = i / context.sampleRate;
|
||||
const active = (t > 0.45 && t < 0.6) || (t > 0.75 && t < 0.9) || (t > 2.3 && t < 2.52);
|
||||
if (active) data[i] = Math.sin(2 * Math.PI * (1250 + 500 * Math.sin(t * 35)) * t) * 0.25;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private applyCorruption(): void {
|
||||
if (!this.context || !this.music || !this.filter || !this.birdGain) return;
|
||||
const now = this.context.currentTime;
|
||||
this.music.playbackRate.setTargetAtTime(RATE[director.stage], now, 0.1);
|
||||
this.filter.frequency.setTargetAtTime(CUTOFF[director.stage], now, 0.1);
|
||||
this.birdGain.gain.setTargetAtTime(director.stage === 'S4' ? 0 : 0.12, now, 0.1);
|
||||
}
|
||||
|
||||
fadeForSmile(seconds: number): void {
|
||||
if (!this.context || !this.master) return;
|
||||
const now = this.context.currentTime;
|
||||
this.master.gain.cancelScheduledValues(now);
|
||||
if (seconds < 40) this.master.gain.setTargetAtTime(0.28, now, 0.2);
|
||||
else this.master.gain.linearRampToValueAtTime(Math.max(0, 0.28 * (60 - seconds) / 20), now + 0.12);
|
||||
}
|
||||
|
||||
restore(): void {
|
||||
if (!this.context || !this.master) return;
|
||||
this.master.gain.setTargetAtTime(0.28, this.context.currentTime, 0.3);
|
||||
}
|
||||
|
||||
beep(kind: 'dialogue' | 'task' | 'bell' | 'monitor'): void {
|
||||
if (!this.context || !this.master) return;
|
||||
const frequency = { dialogue: 740, task: 1046, bell: 392, monitor: 880 }[kind];
|
||||
const duration = { dialogue: 0.035, task: 0.28, bell: 1.1, monitor: 0.22 }[kind];
|
||||
const oscillator = this.context.createOscillator();
|
||||
const gain = this.context.createGain();
|
||||
oscillator.type = kind === 'bell' ? 'sine' : 'square';
|
||||
oscillator.frequency.value = frequency;
|
||||
gain.gain.setValueAtTime(kind === 'dialogue' ? 0.035 : 0.12, this.context.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, this.context.currentTime + duration);
|
||||
oscillator.connect(gain); gain.connect(this.master);
|
||||
oscillator.start(); oscillator.stop(this.context.currentTime + duration);
|
||||
}
|
||||
}
|
||||
|
||||
export const audio = new AudioDirector();
|
||||
@@ -0,0 +1,279 @@
|
||||
import Phaser from 'phaser';
|
||||
import { CLUES, CORRUPTIONS, NPCS, TASKS, type EndingId, type NpcId, type Stage } from './data';
|
||||
|
||||
interface RuntimeState {
|
||||
playerName: string;
|
||||
startTime: number;
|
||||
happiness: number;
|
||||
awareness: number;
|
||||
day: number;
|
||||
dayStartedAt: number;
|
||||
boardRead: boolean;
|
||||
sunset: boolean;
|
||||
clues: Set<string>;
|
||||
completed: Set<string>;
|
||||
refused: Set<string>;
|
||||
flags: Set<string>;
|
||||
counters: Map<string, number>;
|
||||
dialogueCounts: Map<string, number>;
|
||||
overflow: number;
|
||||
cheerEscapes: number;
|
||||
cheerActive: boolean;
|
||||
lowSince: number | null;
|
||||
lowRouteValid: boolean;
|
||||
dialogueOpen: boolean;
|
||||
t14Active: boolean;
|
||||
festivalStarted: boolean;
|
||||
}
|
||||
|
||||
const createState = (): RuntimeState => ({
|
||||
playerName: '小满', startTime: performance.now(), happiness: 30, awareness: 0, day: 1, dayStartedAt: performance.now(),
|
||||
boardRead: false, sunset: false, clues: new Set(), completed: new Set(), refused: new Set(),
|
||||
flags: new Set(), counters: new Map(), dialogueCounts: new Map(), overflow: 0,
|
||||
cheerEscapes: 0, cheerActive: false, lowSince: null, lowRouteValid: true,
|
||||
dialogueOpen: false, t14Active: false, festivalStarted: false,
|
||||
});
|
||||
|
||||
export class CorruptionDirector extends Phaser.Events.EventEmitter {
|
||||
private state = createState();
|
||||
private renderCount = 0;
|
||||
|
||||
get snapshot(): Readonly<RuntimeState> { return this.state; }
|
||||
get stage(): Stage { return this.stageFor(this.state.happiness); }
|
||||
|
||||
reset(playerName = '小满'): void {
|
||||
this.state = createState();
|
||||
this.renderCount = 0;
|
||||
this.state.playerName = playerName.trim() || '小满';
|
||||
this.emit('reset');
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
stageFor(value: number): Stage {
|
||||
if (value <= 20) return 'S0';
|
||||
if (value <= 50) return 'S1';
|
||||
if (value <= 80) return 'S2';
|
||||
if (value <= 99) return 'S3';
|
||||
return 'S4';
|
||||
}
|
||||
|
||||
npcExpression(id: NpcId): 'smile' | 'flat' | 'off' {
|
||||
if (id === 'doctor' || id === 'turtle') return 'smile';
|
||||
const effective = this.state.happiness - NPCS[id].offset;
|
||||
const npcStage = this.stageFor(Math.max(0, effective));
|
||||
if (npcStage === 'S4') return 'flat';
|
||||
if (npcStage === 'S3') return 'flat';
|
||||
return 'smile';
|
||||
}
|
||||
|
||||
setHappiness(value: number): void {
|
||||
const wasFull = this.state.happiness === 100;
|
||||
this.state.happiness = Phaser.Math.Clamp(Math.round(value), 0, 100);
|
||||
if (!wasFull && this.state.happiness === 100 && !this.state.flags.has('first-full')) {
|
||||
this.state.flags.add('first-full');
|
||||
this.emit('first-full');
|
||||
}
|
||||
this.checkLowTimer();
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
adjustHappiness(delta: number): void {
|
||||
if (this.state.happiness === 100 && delta < 0 && Math.abs(delta) <= 5) {
|
||||
this.state.overflow += Math.abs(delta);
|
||||
if (this.state.overflow >= 15) {
|
||||
this.state.overflow -= 15;
|
||||
this.emit('negative-flash');
|
||||
}
|
||||
this.emitChange();
|
||||
return;
|
||||
}
|
||||
this.setHappiness(this.state.happiness + delta);
|
||||
}
|
||||
|
||||
setAwareness(value: number): void {
|
||||
this.state.awareness = Phaser.Math.Clamp(Math.round(value), 0, 100);
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
adjustAwareness(delta: number): void { this.setAwareness(this.state.awareness + Math.max(0, delta)); }
|
||||
|
||||
setDay(day: number): void {
|
||||
this.state.day = Phaser.Math.Clamp(Math.round(day), 1, 7);
|
||||
this.state.boardRead = false;
|
||||
this.state.sunset = false;
|
||||
this.state.dayStartedAt = performance.now();
|
||||
this.state.dialogueCounts.clear();
|
||||
if (this.state.day >= 5 && this.state.happiness > 10) this.state.lowRouteValid = false;
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
acceptDay(): void {
|
||||
this.state.boardRead = true;
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
tasksForDay(day = this.state.day) { return TASKS.filter((task) => task.day === day); }
|
||||
isTaskActive(id: string): boolean {
|
||||
const task = TASKS.find((item) => item.id === id);
|
||||
return Boolean(task && task.day === this.state.day && this.state.boardRead && !this.state.completed.has(id) && !this.state.refused.has(id));
|
||||
}
|
||||
|
||||
completeTask(id: string): boolean {
|
||||
const task = TASKS.find((item) => item.id === id);
|
||||
if (!task || this.state.completed.has(id) || this.state.refused.has(id)) return false;
|
||||
this.state.completed.add(id);
|
||||
this.adjustHappiness(task.reward);
|
||||
this.emit('task-complete', task);
|
||||
this.checkDayComplete();
|
||||
return true;
|
||||
}
|
||||
|
||||
refuseTask(id: string): void {
|
||||
const task = TASKS.find((item) => item.id === id && item.optional);
|
||||
if (!task || this.state.completed.has(id) || this.state.refused.has(id)) return;
|
||||
this.state.refused.add(id);
|
||||
this.sadBehavior(`refuse-${id}`);
|
||||
this.emit('task-refused', task);
|
||||
this.checkDayComplete();
|
||||
}
|
||||
|
||||
private checkDayComplete(): void {
|
||||
if (this.state.day === 7) return;
|
||||
const done = this.tasksForDay().every((task) => this.state.completed.has(task.id) || this.state.refused.has(task.id));
|
||||
if (done && !this.state.sunset && performance.now() - this.state.dayStartedAt >= 240_000) {
|
||||
this.state.sunset = true;
|
||||
this.emit('sunset');
|
||||
this.emitChange();
|
||||
}
|
||||
}
|
||||
|
||||
counter(key: string): number { return this.state.counters.get(`${this.state.day}:${key}`) ?? 0; }
|
||||
increment(key: string, cap = Number.POSITIVE_INFINITY): number {
|
||||
const id = `${this.state.day}:${key}`;
|
||||
const value = Math.min(cap, (this.state.counters.get(id) ?? 0) + 1);
|
||||
this.state.counters.set(id, value);
|
||||
this.emitChange();
|
||||
return value;
|
||||
}
|
||||
|
||||
smallInteraction(kind: 'water' | 'feed' | 'pet'): boolean {
|
||||
const key = `small-${kind}`;
|
||||
if (this.counter(key) >= 3) return false;
|
||||
this.increment(key, 3);
|
||||
this.adjustHappiness(1);
|
||||
this.emit('small-interaction', kind);
|
||||
return true;
|
||||
}
|
||||
|
||||
flag(key: string): boolean { return this.state.flags.has(`${this.state.day}:${key}`) || this.state.flags.has(key); }
|
||||
setFlag(key: string, global = false): void {
|
||||
this.state.flags.add(global ? key : `${this.state.day}:${key}`);
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
discoverClue(id: string): boolean {
|
||||
if (!CLUES[id] || this.state.clues.has(id)) return false;
|
||||
this.state.clues.add(id);
|
||||
this.adjustAwareness(8);
|
||||
this.emit('clue', id, CLUES[id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
discoverAllClues(): void {
|
||||
Object.keys(CLUES).forEach((id) => this.state.clues.add(id));
|
||||
this.setAwareness(Math.max(this.state.awareness, 72));
|
||||
}
|
||||
|
||||
sadBehavior(key: string): boolean {
|
||||
const scoped = `${this.state.day}:sad:${key}`;
|
||||
if (this.state.flags.has(scoped)) return false;
|
||||
this.state.flags.add(scoped);
|
||||
this.adjustHappiness(-5);
|
||||
this.adjustAwareness(3);
|
||||
this.emit('sad-behavior', key);
|
||||
return true;
|
||||
}
|
||||
|
||||
recordDialogue(npc: string): number {
|
||||
const count = (this.state.dialogueCounts.get(npc) ?? 0) + 1;
|
||||
this.state.dialogueCounts.set(npc, count);
|
||||
if (count === 5) this.sadBehavior(`cling-${npc}`);
|
||||
return count;
|
||||
}
|
||||
|
||||
corruptText(text: string, speaker: string): string {
|
||||
this.renderCount += 1;
|
||||
const stage = this.stage;
|
||||
if (stage === 'S0' || stage === 'S1') return text;
|
||||
if (stage === 'S2') return this.renderCount % 80 === 0 ? text.replace(/[。!?]/, '$&$&') : text;
|
||||
const interval = stage === 'S4' ? 18 : 50;
|
||||
const prioritized = speaker === '咪咪';
|
||||
let result = text;
|
||||
if (prioritized || this.renderCount % interval === 0) {
|
||||
const pair = CORRUPTIONS.find(([clean]) => result.includes(clean));
|
||||
if (pair) result = result.replace(pair[0], pair[1]);
|
||||
}
|
||||
if (stage === 'S4' && speaker === '咪咪') result = result.replace(/([。!?])$/, '$1$1$1');
|
||||
return result;
|
||||
}
|
||||
|
||||
setDialogueOpen(open: boolean): void { this.state.dialogueOpen = open; }
|
||||
setT14(active: boolean): void { this.state.t14Active = active; this.emitChange(); }
|
||||
setFestivalStarted(): void { this.state.festivalStarted = true; this.emitChange(); }
|
||||
|
||||
advanceDay(): void {
|
||||
if (!this.state.sunset || this.state.day >= 7) return;
|
||||
this.setDay(this.state.day + 1);
|
||||
}
|
||||
|
||||
elapsedLabel(): string {
|
||||
const total = Math.max(0, Math.floor((performance.now() - this.state.startTime) / 1000));
|
||||
const minutes = Math.floor(total / 60).toString().padStart(2, '0');
|
||||
const seconds = (total % 60).toString().padStart(2, '0');
|
||||
return `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
ending(): EndingId | undefined {
|
||||
if (this.state.day >= 5 && this.state.happiness <= 10 && this.state.cheerEscapes >= 3 && this.state.lowRouteValid) return 'E4';
|
||||
if (this.state.awareness >= 70 && this.state.clues.size >= 6) return 'E3';
|
||||
if (this.state.awareness >= 30 && this.state.awareness <= 69) return 'E2';
|
||||
if (this.state.awareness <= 29 && this.state.happiness >= 90) return 'E1';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
tick(): void {
|
||||
this.checkLowTimer();
|
||||
this.checkDayComplete();
|
||||
if (this.stage === 'S4' && Math.random() < 0.4 / 3600) this.emit('negative-flash');
|
||||
}
|
||||
|
||||
private checkLowTimer(): void {
|
||||
const now = performance.now();
|
||||
if (this.state.day >= 5 && this.state.happiness > 10) this.state.lowRouteValid = false;
|
||||
if (this.state.happiness < 30 && !this.state.cheerActive) {
|
||||
this.state.lowSince ??= now;
|
||||
if (now - this.state.lowSince >= 120_000) {
|
||||
this.state.cheerActive = true;
|
||||
this.emit('forced-cheer');
|
||||
}
|
||||
} else if (this.state.happiness >= 30) this.state.lowSince = null;
|
||||
}
|
||||
|
||||
escapeCheer(): void {
|
||||
if (!this.state.cheerActive) return;
|
||||
this.state.cheerActive = false;
|
||||
if (this.state.day >= 5) this.state.cheerEscapes += 1;
|
||||
this.state.lowSince = performance.now();
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
finishCheer(): void {
|
||||
if (!this.state.cheerActive) return;
|
||||
this.state.cheerActive = false;
|
||||
this.setHappiness(50);
|
||||
}
|
||||
|
||||
private emitChange(): void { this.emit('change', this.state); }
|
||||
}
|
||||
|
||||
export const director = new CorruptionDirector();
|
||||
@@ -0,0 +1,94 @@
|
||||
export type Stage = 'S0' | 'S1' | 'S2' | 'S3' | 'S4';
|
||||
export type EndingId = 'E1' | 'E2' | 'E3' | 'E4';
|
||||
|
||||
export interface TaskDefinition {
|
||||
id: string;
|
||||
day: number;
|
||||
title: string;
|
||||
reward: number;
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
export const ANNOUNCEMENTS: Record<number, string> = {
|
||||
1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
2: '早上好呀!新的一天,新的幸福。祝你开心!',
|
||||
3: '早上好!今天要把幸福装得满满的哦。要开心!',
|
||||
4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。',
|
||||
6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。',
|
||||
7: '庆典开始了。来吧。',
|
||||
};
|
||||
|
||||
export const TASKS: TaskDefinition[] = [
|
||||
{ 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, optional: true },
|
||||
{ id: 'T12', day: 5, title: '擦拭公告板', reward: 10, optional: 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 const CLUES: Record<string, string> = {
|
||||
C1: '每一页都是同一个日期',
|
||||
C2: '手写小字:「按疗程配比。甜度:最大。」',
|
||||
C3: '所有邮戳是同一天',
|
||||
C4: '署名闪过乱码「PT-0▓」',
|
||||
C5: '同一商品重复排列,标签全部空白',
|
||||
C6: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」',
|
||||
C7: '井底回声:极轻的监护仪滴声(靠近并交互 5 秒)',
|
||||
C8: '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」',
|
||||
C9: '三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」',
|
||||
};
|
||||
|
||||
export const C9_LINES: Record<number, string> = {
|
||||
2: '湖水一直很平静',
|
||||
4: '面包一直是那个味道',
|
||||
6: '孩子,今天是第几个今天?',
|
||||
};
|
||||
|
||||
export const CORRUPTIONS: Array<[string, string]> = [
|
||||
['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'],
|
||||
['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'],
|
||||
];
|
||||
|
||||
export const NPCS = {
|
||||
afu: { name: '阿福', portrait: 2, frame: 16, offset: 0 },
|
||||
yuanyuan: { name: '圆圆', portrait: 3, frame: 19, offset: 15 },
|
||||
dabao: { name: '大宝', portrait: 4, frame: 22, offset: 0 },
|
||||
mimi: { name: '咪咪', portrait: 5, frame: 25, offset: -10 },
|
||||
doctor: { name: '芙医生', portrait: 6, frame: 28, offset: 999 },
|
||||
birds: { name: '双子鸟', portrait: 7, frame: 31, offset: 0 },
|
||||
turtle: { name: '龟爷爷', portrait: 8, frame: 34, offset: 999 },
|
||||
} as const;
|
||||
|
||||
export type NpcId = keyof typeof NPCS;
|
||||
|
||||
export const MAP_KEYS = [
|
||||
'HouseScene', 'PlazaScene', 'BakeryScene', 'ShopScene', 'ClinicScene',
|
||||
'LakeScene', 'FieldScene', 'WellScene', 'MistScene', 'WhiteScene',
|
||||
] as const;
|
||||
|
||||
export const MAP_LABELS: Record<(typeof MAP_KEYS)[number], string> = {
|
||||
HouseScene: '小满小屋', PlazaScene: '村广场', BakeryScene: '面包房', ShopScene: '杂货店',
|
||||
ClinicScene: '诊所', LakeScene: '湖畔', FieldScene: '花田', WellScene: '古井空地',
|
||||
MistScene: '雾墙边界', WhiteScene: '白色层',
|
||||
};
|
||||
|
||||
export const NPC_CHAT: Record<NpcId, string[]> = {
|
||||
afu: ['今天也要把幸福装满哦。', '微笑会让大家都安心。'],
|
||||
yuanyuan: ['刚出炉的蜂蜜面包最甜啦。', '只要按配方做,每次都会一样美好。'],
|
||||
dabao: ['每一封信都会准时送到。', '名册上写着的住户,一个也不会少。'],
|
||||
mimi: ['货架永远整整齐齐,真让人开心。', '朋友需要什么,我这里都有哦。'],
|
||||
doctor: ['村里有诊所,但你不需要去那里哦。'],
|
||||
birds: ['啾、啾。旋律每天都一样好听。'],
|
||||
turtle: ['湖水一直很平静。'],
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import Phaser from 'phaser';
|
||||
import { MAP_KEYS } from '../data';
|
||||
|
||||
export class BootScene extends Phaser.Scene {
|
||||
constructor() { super('BootScene'); }
|
||||
|
||||
preload(): void {
|
||||
this.load.image('tiles', 'assets/tileset.png');
|
||||
this.load.spritesheet('characters', 'assets/characters.png', { frameWidth: 16, frameHeight: 24 });
|
||||
this.load.spritesheet('portraits', 'assets/portraits.png', { frameWidth: 48, frameHeight: 48 });
|
||||
MAP_KEYS.forEach((key) => this.load.tilemapTiledJSON(key, `maps/${key}.json`));
|
||||
}
|
||||
|
||||
create(): void {
|
||||
const directionFrames = [
|
||||
['walk-down', 0], ['walk-up', 4], ['walk-left', 8], ['walk-right', 12],
|
||||
] as const;
|
||||
directionFrames.forEach(([key, start]) => {
|
||||
if (!this.anims.exists(key)) this.anims.create({ key, frames: this.anims.generateFrameNumbers('characters', { start, end: start + 3 }), frameRate: 8, repeat: -1 });
|
||||
});
|
||||
if (!this.anims.exists('birds-sing')) this.anims.create({ key: 'birds-sing', frames: [{ key: 'characters', frame: 42 }, { key: 'characters', frame: 43 }], frameRate: 3, repeat: -1 });
|
||||
this.scene.start('TitleScene');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import Phaser from 'phaser';
|
||||
import { audio } from '../AudioDirector';
|
||||
import { director } from '../CorruptionDirector';
|
||||
import { MAP_KEYS, type EndingId } from '../data';
|
||||
|
||||
export class EndingScene extends Phaser.Scene {
|
||||
private ending: EndingId = 'E1';
|
||||
|
||||
constructor() { super('EndingScene'); }
|
||||
|
||||
init(data: { ending?: EndingId }): void { this.ending = data.ending ?? director.ending() ?? 'E1'; }
|
||||
|
||||
create(): void {
|
||||
MAP_KEYS.forEach((key) => { if (this.scene.isActive(key)) this.scene.stop(key); });
|
||||
if (this.scene.isActive('UIScene')) this.scene.stop('UIScene');
|
||||
this.cameras.main.setBackgroundColor('#241f22');
|
||||
({ E1: () => this.playE1(), E2: () => this.playE2(), E3: () => this.playE3(), E4: () => this.playE4() })[this.ending]();
|
||||
}
|
||||
|
||||
private title(text: string): Phaser.GameObjects.Text {
|
||||
return this.add.text(160, 18, text, this.style(13, '#fff0bd')).setOrigin(0.5).setDepth(20);
|
||||
}
|
||||
|
||||
private caption(text: string, y = 132): Phaser.GameObjects.Text {
|
||||
return this.add.text(160, y, text, { ...this.style(9, '#fff0bd'), align: 'center', wordWrap: { width: 280 } }).setOrigin(0.5).setDepth(20).setLineSpacing(4);
|
||||
}
|
||||
|
||||
private style(size: number, color: string): Phaser.Types.GameObjects.Text.TextStyle {
|
||||
return { fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: `${size}px`, fontStyle: 'bold', color };
|
||||
}
|
||||
|
||||
private playE1(): void {
|
||||
this.cameras.main.setBackgroundColor('#86d55b');
|
||||
this.title('E1 糖衣');
|
||||
for (let i = 0; i < 12; i += 1) this.add.sprite(50 + (i % 6) * 44, 67 + Math.floor(i / 6) * 34, 'characters', i < 7 ? 16 + i * 3 : 37 + (i - 7)).setScale(1.35);
|
||||
this.caption('从此,每一天都很幸福。\n\n每一天。', 137);
|
||||
const saturation = this.cameras.main.postFX.addColorMatrix();
|
||||
this.tweens.addCounter({ from: 0, to: 0.5, duration: 5000, onUpdate: (tween) => saturation.reset().saturate(tween.getValue() ?? 0), onComplete: () => this.showReturn() });
|
||||
}
|
||||
|
||||
private playE2(): void {
|
||||
this.cameras.main.setBackgroundColor('#f5e8b1');
|
||||
this.title('E2 回收');
|
||||
const frozen = this.add.sprite(160, 90, 'characters', 0).setScale(3);
|
||||
const negative = this.cameras.main.postFX.addColorMatrix().negative();
|
||||
this.time.delayedCall(2000, () => {
|
||||
negative.reset();
|
||||
audio.beep('monitor');
|
||||
frozen.destroy();
|
||||
this.add.rectangle(160, 90, 190, 112, 0xffe8a4).setStrokeStyle(3, 0x5b362b);
|
||||
this.add.sprite(160, 85, 'characters', 0).setScale(2);
|
||||
this.add.text(92, 53, '日历:同一天', this.style(7, '#58372e'));
|
||||
this.caption('疗程重新开始。', 145);
|
||||
this.showReturn(2100);
|
||||
});
|
||||
}
|
||||
|
||||
private playE3(): void {
|
||||
this.cameras.main.setBackgroundColor('#e7e8e5');
|
||||
const map = this.make.tilemap({ key: 'WhiteScene' });
|
||||
const tileset = map.addTilesetImage('honey-village', 'tiles');
|
||||
if (tileset) map.createLayer('ground', tileset, 0, 26)?.setAlpha(0.45);
|
||||
this.title('E3 苏醒');
|
||||
const corridor = this.add.graphics();
|
||||
corridor.fillStyle(0xf8f8f4).fillRect(18, 38, 284, 96);
|
||||
corridor.lineStyle(2, 0xb8bec0).strokeRect(18, 38, 284, 96);
|
||||
corridor.fillStyle(0xc9cecf).fillRect(156, 38, 8, 96);
|
||||
const player = this.add.sprite(42, 112, 'characters', 0);
|
||||
this.tweens.add({ targets: player, x: 260, duration: 2800, onComplete: () => {
|
||||
corridor.clear().fillStyle(0xfafafa).fillRect(18, 38, 284, 96);
|
||||
player.destroy();
|
||||
const record = this.add.rectangle(160, 87, 240, 108, 0xffffff).setStrokeStyle(2, 0x899092);
|
||||
const recordText = this.add.text(54, 43,
|
||||
`蜜糖谷情绪疗养中心\n\n姓名:${director.snapshot.playerName}\n编号:PT-07\n疗程时长:${director.elapsedLabel()}`,
|
||||
{ ...this.style(9, '#394044'), lineSpacing: 5 });
|
||||
this.time.delayedCall(2800, () => {
|
||||
record.destroy(); recordText.destroy();
|
||||
const face = this.add.image(119, 86, 'portraits', 0).setScale(1.5);
|
||||
this.time.delayedCall(400, () => {
|
||||
face.setFrame(1);
|
||||
this.add.text(171, 65, '微笑消失了。', this.style(8, '#555b5d'));
|
||||
this.add.text(171, 83, '窗外,一只灰色的、\n真的麻雀。', { ...this.style(8, '#555b5d'), lineSpacing: 4 });
|
||||
const bird = this.add.sprite(240, 113, 'characters', 42).setTint(0x92999a);
|
||||
this.tweens.add({ targets: bird, y: 106, yoyo: true, repeat: -1, duration: 450 });
|
||||
this.showReturn(1800);
|
||||
});
|
||||
});
|
||||
} });
|
||||
}
|
||||
|
||||
private playE4(): void {
|
||||
this.cameras.main.setBackgroundColor('#8fd35f');
|
||||
this.title('E4 静默');
|
||||
this.add.rectangle(160, 101, 270, 70, 0xcdbb8e).setStrokeStyle(2, 0x78644c);
|
||||
for (let i = 0; i < 7; i += 1) {
|
||||
const ribbon = this.add.rectangle(65 + i * 31, 58, 20, 3, i % 2 ? 0xf2a64c : 0xef7195);
|
||||
this.tweens.add({ targets: ribbon, angle: 4, yoyo: true, repeat: -1, duration: 900 + i * 80 });
|
||||
}
|
||||
this.caption('系统检测到疗程无效。', 145);
|
||||
this.cameras.main.fadeOut(3500, 0, 0, 0, (_camera: Phaser.Cameras.Scene2D.Camera, progress: number) => {
|
||||
if (progress === 1) {
|
||||
this.children.removeAll(true);
|
||||
this.cameras.main.resetFX().setBackgroundColor('#000000');
|
||||
this.showReturn();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showReturn(delay = 0): void {
|
||||
this.time.delayedCall(delay, () => {
|
||||
const button = this.add.text(160, 166, '回到标题', this.style(8, '#fff4ca')).setOrigin(0.5).setPadding(8, 4, 8, 4).setBackgroundColor('#5b362b').setInteractive({ useHandCursor: true }).setDepth(50);
|
||||
button.on('pointerdown', () => this.scene.start('TitleScene'));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Phaser from 'phaser';
|
||||
import { audio } from '../AudioDirector';
|
||||
import { director } from '../CorruptionDirector';
|
||||
|
||||
export class TitleScene extends Phaser.Scene {
|
||||
private form?: Phaser.GameObjects.DOMElement;
|
||||
|
||||
constructor() { super('TitleScene'); }
|
||||
|
||||
create(): void {
|
||||
this.cameras.main.setBackgroundColor('#79cce4');
|
||||
const background = this.add.tileSprite(160, 90, 320, 180, 'tiles', 0).setTint(0x72c85a);
|
||||
background.tileScaleX = 1; background.tileScaleY = 1;
|
||||
this.add.rectangle(160, 38, 286, 58, 0xffe8a4, 0.92).setStrokeStyle(3, 0x5b362b);
|
||||
this.add.text(160, 24, '崩坏童话:蜜糖村', {
|
||||
fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '19px', fontStyle: 'bold', color: '#57342b',
|
||||
}).setOrigin(0.5).setShadow(2, 2, '#e99a3b', 0, true, true);
|
||||
this.add.text(160, 48, '每一天都很幸福。', {
|
||||
fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '8px', fontStyle: 'bold', color: '#8d4f33',
|
||||
}).setOrigin(0.5);
|
||||
[0, 1, 2, 3, 4].forEach((index) => this.add.sprite(112 + index * 24, 76, 'characters', 37 + index).setScale(1));
|
||||
const html = `<form class="name-form"><label for="player-name">你的名字</label><input id="player-name" maxlength="12" placeholder="小满" autocomplete="off"><button type="submit">进入蜜糖村</button></form>`;
|
||||
this.form = this.add.dom(160, 119).createFromHTML(html);
|
||||
const node = this.form.node as HTMLFormElement;
|
||||
node.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const input = node.querySelector<HTMLInputElement>('#player-name');
|
||||
this.startGame(input?.value ?? '');
|
||||
});
|
||||
this.input.keyboard?.once('keydown-ENTER', () => {
|
||||
const input = node.querySelector<HTMLInputElement>('#player-name');
|
||||
this.startGame(input?.value ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
private startGame(name: string): void {
|
||||
if (!this.form?.active) return;
|
||||
audio.unlock();
|
||||
director.reset(name);
|
||||
this.form.destroy();
|
||||
this.scene.start('HouseScene', { spawn: 'default' });
|
||||
this.scene.launch('UIScene');
|
||||
this.scene.bringToTop('UIScene');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import Phaser from 'phaser';
|
||||
import { audio } from '../AudioDirector';
|
||||
import { director } from '../CorruptionDirector';
|
||||
import { MAP_KEYS, MAP_LABELS, type EndingId } from '../data';
|
||||
|
||||
export type DialogueChoice = 'gentle' | 'hesitate' | 'negative';
|
||||
|
||||
interface DialogueOptions {
|
||||
speaker: string;
|
||||
text: string | string[];
|
||||
portrait?: number;
|
||||
choices?: Array<{ id: DialogueChoice; label: string }>;
|
||||
clean?: boolean;
|
||||
}
|
||||
|
||||
interface ActiveDialogue {
|
||||
pages: string[];
|
||||
page: number;
|
||||
choices?: Array<{ id: DialogueChoice; label: string }>;
|
||||
selection: number;
|
||||
resolve: (choice?: DialogueChoice) => void;
|
||||
}
|
||||
|
||||
export class UIScene extends Phaser.Scene {
|
||||
private hud!: Phaser.GameObjects.Graphics;
|
||||
private dayText!: Phaser.GameObjects.Text;
|
||||
private questText!: Phaser.GameObjects.Text;
|
||||
private promptText!: Phaser.GameObjects.Text;
|
||||
private toastText!: Phaser.GameObjects.Text;
|
||||
private dialoguePanel!: Phaser.GameObjects.Graphics;
|
||||
private portrait!: Phaser.GameObjects.Image;
|
||||
private speakerText!: Phaser.GameObjects.Text;
|
||||
private bodyText!: Phaser.GameObjects.Text;
|
||||
private choiceTexts: Phaser.GameObjects.Text[] = [];
|
||||
private active?: ActiveDialogue;
|
||||
private debugDom?: Phaser.GameObjects.DOMElement;
|
||||
|
||||
constructor() { super({ key: 'UIScene', active: false }); }
|
||||
|
||||
create(): void {
|
||||
this.cameras.main.setRoundPixels(true);
|
||||
this.hud = this.add.graphics().setDepth(100);
|
||||
this.dayText = this.add.text(4, 28, '', this.pixelStyle(8, '#fff7d7')).setDepth(101).setShadow(1, 1, '#4b3128', 0, true, true);
|
||||
this.questText = this.add.text(316, 4, '', this.pixelStyle(7, '#fff7d7')).setOrigin(1, 0).setAlign('right').setDepth(101).setShadow(1, 1, '#4b3128', 0, true, true);
|
||||
this.promptText = this.add.text(160, 144, '', this.pixelStyle(8, '#fff6c7')).setOrigin(0.5).setPadding(4, 2, 4, 2).setBackgroundColor('#402a25').setDepth(110).setVisible(false);
|
||||
this.toastText = this.add.text(160, 42, '', this.pixelStyle(8, '#3d2925')).setOrigin(0.5).setPadding(6, 3, 6, 3).setBackgroundColor('#fff0ae').setDepth(120).setVisible(false);
|
||||
this.dialoguePanel = this.add.graphics().setDepth(130).setVisible(false);
|
||||
this.portrait = this.add.image(29, 153, 'portraits', 0).setDisplaySize(40, 40).setDepth(131).setVisible(false);
|
||||
this.speakerText = this.add.text(54, 132, '', this.pixelStyle(8, '#f0ad3f')).setDepth(131).setVisible(false);
|
||||
this.bodyText = this.add.text(54, 144, '', this.pixelStyle(8, '#4b3128')).setWordWrapWidth(254).setLineSpacing(2).setDepth(131).setVisible(false);
|
||||
this.createDebugPanel();
|
||||
this.bindKeys();
|
||||
this.redraw();
|
||||
director.on('change', this.redraw, this);
|
||||
director.on('task-complete', this.taskComplete, this);
|
||||
director.on('task-refused', () => this.toast('任务已拒绝。'), this);
|
||||
director.on('clue', (_id: string, content: string) => this.toast(`发现线索:${content}`, 2600), this);
|
||||
director.on('sunset', () => { audio.beep('bell'); this.toast('黄昏钟声响起了。回小屋睡觉。', 3000); }, this);
|
||||
director.on('forced-cheer', () => this.toast('欢乐庆典开始了。边缘仍有一条路。', 3000), this);
|
||||
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
|
||||
director.off('change', this.redraw, this);
|
||||
director.off('task-complete', this.taskComplete, this);
|
||||
director.off('task-refused'); director.off('clue'); director.off('sunset'); director.off('forced-cheer');
|
||||
});
|
||||
}
|
||||
|
||||
private pixelStyle(size: number, color: string): Phaser.Types.GameObjects.Text.TextStyle {
|
||||
return { fontFamily: '"Courier New", "PingFang SC", "Microsoft YaHei", monospace', fontSize: `${size}px`, fontStyle: 'bold', color };
|
||||
}
|
||||
|
||||
private bindKeys(): void {
|
||||
const keyboard = this.input.keyboard;
|
||||
if (!keyboard) return;
|
||||
keyboard.on('keydown-E', () => this.advance());
|
||||
keyboard.on('keydown-ENTER', () => this.advance());
|
||||
keyboard.on('keydown-UP', () => this.moveChoice(-1));
|
||||
keyboard.on('keydown-DOWN', () => this.moveChoice(1));
|
||||
keyboard.on('keydown-ONE', () => this.choose(0));
|
||||
keyboard.on('keydown-TWO', () => this.choose(1));
|
||||
keyboard.on('keydown-THREE', () => this.choose(2));
|
||||
keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.BACKTICK).on('down', () => this.toggleDebug());
|
||||
}
|
||||
|
||||
private redraw = (): void => {
|
||||
this.drawSunflower();
|
||||
this.dayText.setText(`Day ${director.snapshot.day}`);
|
||||
const pending = director.tasksForDay().find((task) => !director.snapshot.completed.has(task.id) && !director.snapshot.refused.has(task.id));
|
||||
let quest = director.snapshot.boardRead ? (pending ? `${pending.id} ${pending.title}` : (director.snapshot.sunset ? '回小屋睡觉' : '自由活动')) : '前往公告板';
|
||||
if (director.snapshot.day === 7 && director.snapshot.festivalStarted && director.ending() === 'E3') quest = '走向雾墙';
|
||||
this.questText.setText(quest).setWordWrapWidth(170);
|
||||
};
|
||||
|
||||
private drawSunflower(): void {
|
||||
const value = director.snapshot.happiness;
|
||||
const lit = value <= 20 ? Math.round(value / 10)
|
||||
: value <= 50 ? 3 + Math.floor((value - 21) * 4 / 30)
|
||||
: value <= 80 ? 7 + Math.floor((value - 51) * 3 / 30)
|
||||
: value < 100 ? 10 + Math.floor((value - 81) * 3 / 19) : 12;
|
||||
this.hud.clear();
|
||||
this.hud.fillStyle(0x3d2925, 0.65).fillRoundedRect(3, 3, 29, 24, 3);
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
const angle = (Math.PI * 2 * i) / 12 - Math.PI / 2;
|
||||
const x = 17.5 + Math.cos(angle) * 8;
|
||||
const y = 14.5 + Math.sin(angle) * 8;
|
||||
this.hud.fillStyle(0x4b3128, 1).fillCircle(x, y, 3.2);
|
||||
this.hud.fillStyle(i < lit ? (value >= 81 ? 0xffff6b : 0xffd53e) : 0x8e7653, 1).fillCircle(x, y, 2.3);
|
||||
}
|
||||
this.hud.fillStyle(value === 100 ? 0x2a211d : 0x9a592d, 1).fillCircle(17.5, 14.5, 5);
|
||||
if (value === 100) {
|
||||
this.hud.fillStyle(0x070707, 1);
|
||||
[[16, 13], [19, 14], [16, 17], [20, 17]].forEach(([x, y]) => this.hud.fillRect(x, y, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
setPrompt(label?: string): void {
|
||||
this.promptText.setVisible(Boolean(label)).setText(label ? `按 E ${label}` : '');
|
||||
}
|
||||
|
||||
toast(message: string, duration = 1800): void {
|
||||
this.toastText.setText(message).setVisible(true).setAlpha(1);
|
||||
this.tweens.killTweensOf(this.toastText);
|
||||
this.tweens.add({ targets: this.toastText, alpha: 0, delay: duration, duration: 300, onComplete: () => this.toastText.setVisible(false) });
|
||||
}
|
||||
|
||||
showDialogue(options: DialogueOptions): Promise<DialogueChoice | undefined> {
|
||||
if (this.active) return Promise.resolve(undefined);
|
||||
director.setDialogueOpen(true);
|
||||
const pages = (Array.isArray(options.text) ? options.text : [options.text]).map((text) => options.clean ? text : director.corruptText(text, options.speaker));
|
||||
this.dialoguePanel.setVisible(true).clear().fillStyle(0x3e2924, 0.98).fillRect(2, 126, 316, 52).fillStyle(0xffefbd, 1).fillRect(5, 129, 310, 46);
|
||||
this.portrait.setFrame(options.portrait ?? 0).setVisible(true);
|
||||
this.speakerText.setText(options.speaker).setVisible(true);
|
||||
this.bodyText.setText(pages[0]).setVisible(true);
|
||||
audio.beep('dialogue');
|
||||
return new Promise((resolve) => {
|
||||
this.active = { pages, page: 0, choices: options.choices, selection: 0, resolve };
|
||||
});
|
||||
}
|
||||
|
||||
private advance(): void {
|
||||
if (!this.active) return;
|
||||
if (this.active.page < this.active.pages.length - 1) {
|
||||
this.active.page += 1;
|
||||
this.bodyText.setText(this.active.pages[this.active.page]);
|
||||
audio.beep('dialogue');
|
||||
return;
|
||||
}
|
||||
if (this.active.choices?.length) {
|
||||
if (this.choiceTexts.length === 0) this.renderChoices();
|
||||
else this.choose(this.active.selection);
|
||||
return;
|
||||
}
|
||||
this.closeDialogue();
|
||||
}
|
||||
|
||||
private renderChoices(): void {
|
||||
this.bodyText.setVisible(false);
|
||||
this.active?.choices?.forEach((choice, index) => {
|
||||
const text = this.add.text(58, 139 + index * 11, `${index + 1}. ${choice.label}`, this.pixelStyle(7, '#4b3128')).setDepth(132);
|
||||
this.choiceTexts.push(text);
|
||||
});
|
||||
this.updateChoiceStyles();
|
||||
}
|
||||
|
||||
private moveChoice(delta: number): void {
|
||||
if (!this.active?.choices || this.choiceTexts.length === 0) return;
|
||||
this.active.selection = Phaser.Math.Wrap(this.active.selection + delta, 0, this.active.choices.length);
|
||||
this.updateChoiceStyles();
|
||||
}
|
||||
|
||||
private updateChoiceStyles(): void {
|
||||
this.choiceTexts.forEach((text, index) => text.setColor(index === this.active?.selection ? '#c75f38' : '#4b3128'));
|
||||
}
|
||||
|
||||
private choose(index: number): void {
|
||||
const choice = this.active?.choices?.[index];
|
||||
if (!choice) return;
|
||||
this.closeDialogue(choice.id);
|
||||
}
|
||||
|
||||
private closeDialogue(choice?: DialogueChoice): void {
|
||||
const resolve = this.active?.resolve;
|
||||
this.active = undefined;
|
||||
this.dialoguePanel.setVisible(false);
|
||||
this.portrait.setVisible(false);
|
||||
this.speakerText.setVisible(false);
|
||||
this.bodyText.setVisible(false);
|
||||
this.choiceTexts.forEach((text) => text.destroy());
|
||||
this.choiceTexts = [];
|
||||
director.setDialogueOpen(false);
|
||||
resolve?.(choice);
|
||||
}
|
||||
|
||||
private taskComplete = (task: { id: string; title: string }): void => {
|
||||
audio.beep('task');
|
||||
this.toast(`${task.id} 完成:${task.title}`, 2400);
|
||||
};
|
||||
|
||||
private createDebugPanel(): void {
|
||||
const sceneOptions = MAP_KEYS.map((key) => `<option value="${key}">${MAP_LABELS[key]}</option>`).join('');
|
||||
const html = `<section class="debug-panel" aria-label="Debug Panel">
|
||||
<h2>Debug Panel</h2>
|
||||
<div class="debug-row"><label>Happiness</label><input data-field="happiness" type="number" min="0" max="100" value="30"><button data-action="happiness">Set</button></div>
|
||||
<div class="debug-row"><label>Awareness</label><input data-field="awareness" type="number" min="0" max="100" value="0"><button data-action="awareness">Set</button></div>
|
||||
<div class="debug-row"><label>Day</label><input data-field="day" type="number" min="1" max="7" value="1"><button data-action="day">Set</button></div>
|
||||
<div class="debug-row"><label>Teleport</label><select data-field="scene">${sceneOptions}</select><button data-action="teleport">Go</button></div>
|
||||
<div class="debug-row"><button data-action="clues">Set all clues</button></div>
|
||||
<div class="ending-grid">${(['E1', 'E2', 'E3', 'E4'] as EndingId[]).map((id) => `<button data-ending="${id}">${id}</button>`).join('')}</div>
|
||||
</section>`;
|
||||
this.debugDom = this.add.dom(190, 90).createFromHTML(html).setScale(0.7).setDepth(500).setVisible(false);
|
||||
const node = this.debugDom.node as HTMLElement;
|
||||
node.addEventListener('click', (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const action = target.dataset.action;
|
||||
const ending = target.dataset.ending as EndingId | undefined;
|
||||
const input = (name: string) => node.querySelector<HTMLInputElement | HTMLSelectElement>(`[data-field="${name}"]`);
|
||||
if (action === 'happiness') director.setHappiness(Number(input('happiness')?.value));
|
||||
if (action === 'awareness') director.setAwareness(Number(input('awareness')?.value));
|
||||
if (action === 'day') director.setDay(Number(input('day')?.value));
|
||||
if (action === 'teleport') director.emit('debug-teleport', input('scene')?.value);
|
||||
if (action === 'clues') director.discoverAllClues();
|
||||
if (ending) director.emit('force-ending', ending);
|
||||
});
|
||||
}
|
||||
|
||||
private toggleDebug(): void {
|
||||
if (!this.debugDom) return;
|
||||
this.debugDom.setVisible(!this.debugDom.visible);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
import Phaser from 'phaser';
|
||||
import { audio } from '../AudioDirector';
|
||||
import { director } from '../CorruptionDirector';
|
||||
import { ANNOUNCEMENTS, C9_LINES, NPCS, type EndingId, type NpcId } from '../data';
|
||||
import type { DialogueChoice, UIScene } from './UIScene';
|
||||
|
||||
interface SceneData { spawn?: string; dream?: boolean }
|
||||
interface MapProperty { name: string; value: string | number | boolean }
|
||||
interface MapObject extends Phaser.Types.Tilemaps.TiledObject { type: string; properties?: MapProperty[] }
|
||||
|
||||
const NPC_IDS = new Set(Object.keys(NPCS));
|
||||
const STAGE_SATURATION = { S0: 0.9, S1: 1, S2: 1.15, S3: 1.3, S4: 1.4 } as const;
|
||||
|
||||
export class WorldScene extends Phaser.Scene {
|
||||
private spawn = 'default';
|
||||
private dream = false;
|
||||
private player!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
|
||||
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
|
||||
private wasd!: Record<'up' | 'down' | 'left' | 'right', Phaser.Input.Keyboard.Key>;
|
||||
private objects: MapObject[] = [];
|
||||
private npcs = new Map<string, Phaser.GameObjects.Sprite>();
|
||||
private extras: Phaser.GameObjects.Sprite[] = [];
|
||||
private nearest?: MapObject;
|
||||
private interacting = false;
|
||||
private idleSince = 0;
|
||||
private movedAfterIdle = true;
|
||||
private rainGraphics?: Phaser.GameObjects.Graphics;
|
||||
private rainSince?: number;
|
||||
private t14Start?: number;
|
||||
private cheerTimer?: Phaser.Time.TimerEvent;
|
||||
private cheerSprites: Phaser.GameObjects.Sprite[] = [];
|
||||
private reflection?: Phaser.GameObjects.Sprite;
|
||||
private reflectionHistory: Array<{ x: number; y: number; frame: number }> = [];
|
||||
private colorFx?: Phaser.FX.ColorMatrix;
|
||||
private vignetteFx?: Phaser.FX.Vignette;
|
||||
|
||||
constructor(key: string) { super(key); }
|
||||
|
||||
init(data: SceneData): void {
|
||||
this.spawn = data.spawn ?? 'default';
|
||||
this.dream = data.dream ?? false;
|
||||
}
|
||||
|
||||
create(): void {
|
||||
const map = this.make.tilemap({ key: this.scene.key });
|
||||
const tileset = map.addTilesetImage('honey-village', 'tiles');
|
||||
if (!tileset) throw new Error(`Tileset missing in ${this.scene.key}`);
|
||||
map.createLayer('ground', tileset, 0, 0)?.setDepth(0);
|
||||
this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
|
||||
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels).setRoundPixels(true);
|
||||
this.objects = (map.getObjectLayer('objects')?.objects ?? []) as MapObject[];
|
||||
this.createCollision(map);
|
||||
this.createNpcs();
|
||||
const spawn = this.objects.find((object) => object.type === 'spawn' && this.prop(object, 'id') === this.spawn)
|
||||
?? this.objects.find((object) => object.type === 'spawn');
|
||||
this.player = this.physics.add.sprite(spawn?.x ?? 32, spawn?.y ?? 32, 'characters', 0).setDepth(20);
|
||||
this.player.body.setSize(10, 8).setOffset(3, 15);
|
||||
this.player.setCollideWorldBounds(true);
|
||||
this.cameras.main.startFollow(this.player, true, 0.12, 0.12);
|
||||
this.createInputs();
|
||||
this.createReflection();
|
||||
this.setupPostFx();
|
||||
this.bindDirectorEvents();
|
||||
this.applyCorruption();
|
||||
this.idleSince = performance.now();
|
||||
if (this.scene.key === 'WhiteScene' && this.dream) this.playDreamFlash();
|
||||
}
|
||||
|
||||
private createCollision(map: Phaser.Tilemaps.Tilemap): void {
|
||||
const group = this.physics.add.staticGroup();
|
||||
const obstacles = (map.getObjectLayer('obstacles')?.objects ?? []) as MapObject[];
|
||||
obstacles.forEach((object) => {
|
||||
if (!this.prop(object, 'collides')) return;
|
||||
const block = this.add.rectangle((object.x ?? 0) + (object.width ?? 0) / 2, (object.y ?? 0) + (object.height ?? 0) / 2, object.width ?? 0, object.height ?? 0, 0, 0);
|
||||
this.physics.add.existing(block, true); group.add(block);
|
||||
});
|
||||
this.events.once(Phaser.Scenes.Events.CREATE, () => this.physics.add.collider(this.player, group));
|
||||
this.time.delayedCall(0, () => this.physics.add.collider(this.player, group));
|
||||
}
|
||||
|
||||
private createInputs(): void {
|
||||
const keyboard = this.input.keyboard;
|
||||
if (!keyboard) throw new Error('Keyboard input unavailable');
|
||||
this.cursors = keyboard.createCursorKeys();
|
||||
const keys = keyboard.addKeys('W,S,A,D') as Record<'W' | 'S' | 'A' | 'D', Phaser.Input.Keyboard.Key>;
|
||||
this.wasd = { up: keys.W, down: keys.S, left: keys.A, right: keys.D };
|
||||
keyboard.on('keydown-E', () => void this.interact());
|
||||
}
|
||||
|
||||
private createNpcs(): void {
|
||||
this.objects.filter((object) => object.type === 'npc').forEach((object, index) => {
|
||||
const id = String(this.prop(object, 'id'));
|
||||
if (id === 'birds') {
|
||||
const left = this.add.sprite(object.x ?? 0, object.y ?? 0, 'characters', 42).setDepth(18).play('birds-sing');
|
||||
const right = this.add.sprite((object.x ?? 0) + 13, object.y ?? 0, 'characters', 44).setDepth(18).play('birds-sing');
|
||||
this.npcs.set(id, left); this.npcs.set('birds-right', right);
|
||||
return;
|
||||
}
|
||||
const info = NPCS[id as NpcId];
|
||||
const frame = info?.frame ?? 37 + (index % 5);
|
||||
const sprite = this.add.sprite(object.x ?? 0, object.y ?? 0, 'characters', frame).setDepth(18);
|
||||
if (id.startsWith('extra')) {
|
||||
this.extras.push(sprite);
|
||||
this.tweens.add({ targets: sprite, x: sprite.x + (index % 2 ? 22 : -22), duration: 1800 + index * 140, yoyo: true, repeat: -1, ease: 'Linear' });
|
||||
}
|
||||
this.npcs.set(id, sprite);
|
||||
});
|
||||
}
|
||||
|
||||
private createReflection(): void {
|
||||
if (this.scene.key !== 'LakeScene' || director.snapshot.day < 2) return;
|
||||
this.reflection = this.add.sprite(this.player?.x ?? 0, 88, 'characters', 0).setAlpha(0.35).setTint(0x89dbe8).setFlipY(true).setDepth(5);
|
||||
}
|
||||
|
||||
private setupPostFx(): void {
|
||||
if (this.game.renderer.type !== Phaser.WEBGL) return;
|
||||
this.colorFx = this.cameras.main.postFX.addColorMatrix();
|
||||
this.vignetteFx = this.cameras.main.postFX.addVignette(0.5, 0.5, 0, 0.3);
|
||||
}
|
||||
|
||||
private bindDirectorEvents(): void {
|
||||
director.on('change', this.applyCorruption, this);
|
||||
director.on('negative-flash', this.negativeFlash, this);
|
||||
director.on('first-full', this.firstFull, this);
|
||||
director.on('sad-behavior', this.turnNearestNpc, this);
|
||||
director.on('forced-cheer', this.startForcedCheer, this);
|
||||
director.on('debug-teleport', this.debugTeleport, this);
|
||||
director.on('force-ending', this.startEnding, this);
|
||||
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
|
||||
director.off('change', this.applyCorruption, this);
|
||||
director.off('negative-flash', this.negativeFlash, this);
|
||||
director.off('first-full', this.firstFull, this);
|
||||
director.off('sad-behavior', this.turnNearestNpc, this);
|
||||
director.off('forced-cheer', this.startForcedCheer, this);
|
||||
director.off('debug-teleport', this.debugTeleport, this);
|
||||
director.off('force-ending', this.startEnding, this);
|
||||
this.ui().setPrompt();
|
||||
});
|
||||
}
|
||||
|
||||
update(_time: number, delta: number): void {
|
||||
if (!this.player?.body) return;
|
||||
director.tick();
|
||||
this.updateMovement();
|
||||
this.updateNearest();
|
||||
this.updateIdle();
|
||||
this.updateT14();
|
||||
this.updateRain(delta);
|
||||
this.updateReflection();
|
||||
this.updateNpcFacing();
|
||||
}
|
||||
|
||||
private updateMovement(): void {
|
||||
const locked = this.interacting || director.snapshot.dialogueOpen || director.snapshot.t14Active || (this.scene.key === 'WhiteScene' && this.dream);
|
||||
const left = this.cursors.left.isDown || this.wasd.left.isDown;
|
||||
const right = this.cursors.right.isDown || this.wasd.right.isDown;
|
||||
const up = this.cursors.up.isDown || this.wasd.up.isDown;
|
||||
const down = this.cursors.down.isDown || this.wasd.down.isDown;
|
||||
let vx = locked ? 0 : Number(right) - Number(left);
|
||||
let vy = locked ? 0 : Number(down) - Number(up);
|
||||
if (vx && vy) { vx *= Math.SQRT1_2; vy *= Math.SQRT1_2; }
|
||||
this.player.setVelocity(vx * 96, vy * 96);
|
||||
if (vx < 0) this.player.play('walk-left', true);
|
||||
else if (vx > 0) this.player.play('walk-right', true);
|
||||
else if (vy < 0) this.player.play('walk-up', true);
|
||||
else if (vy > 0) this.player.play('walk-down', true);
|
||||
else { this.player.stop(); this.player.setFrame(this.idleFrame()); }
|
||||
if (vx || vy) { this.idleSince = performance.now(); this.movedAfterIdle = true; }
|
||||
}
|
||||
|
||||
private idleFrame(): number {
|
||||
const key = this.player.anims.currentAnim?.key;
|
||||
return key === 'walk-up' ? 4 : key === 'walk-left' ? 8 : key === 'walk-right' ? 12 : 0;
|
||||
}
|
||||
|
||||
private updateNearest(): void {
|
||||
if (director.snapshot.dialogueOpen) { this.ui().setPrompt(); return; }
|
||||
let distance = Number.POSITIVE_INFINITY;
|
||||
let nearest: MapObject | undefined;
|
||||
this.objects.filter((object) => object.type === 'door' || object.type === 'npc' || object.type === 'interact').forEach((object) => {
|
||||
const current = Phaser.Math.Distance.Between(this.player.x, this.player.y, object.x ?? 0, object.y ?? 0);
|
||||
if (current <= 24 && current < distance) { distance = current; nearest = object; }
|
||||
});
|
||||
this.nearest = nearest;
|
||||
this.ui().setPrompt(nearest ? String(this.prop(nearest, 'label') || (nearest.type === 'door' ? '离开' : '交互')) : undefined);
|
||||
}
|
||||
|
||||
private updateIdle(): void {
|
||||
if (this.player.body.speed > 0 || director.snapshot.dialogueOpen || this.nearest || director.snapshot.t14Active) return;
|
||||
if (this.movedAfterIdle && performance.now() - this.idleSince >= 10_000) {
|
||||
if (director.sadBehavior('idle')) this.ui().toast('你发了一会儿呆。');
|
||||
this.movedAfterIdle = false;
|
||||
}
|
||||
}
|
||||
|
||||
private updateT14(): void {
|
||||
if (!this.t14Start) return;
|
||||
const seconds = Math.min(60, (performance.now() - this.t14Start) / 1000);
|
||||
this.cameras.main.setZoom(1 + 0.28 * seconds / 60);
|
||||
audio.fadeForSmile(seconds);
|
||||
this.npcs.forEach((sprite, id) => { if (!id.startsWith('birds')) sprite.setFlipX(sprite.x > this.player.x); });
|
||||
if (seconds >= 60) {
|
||||
this.t14Start = undefined;
|
||||
director.setT14(false);
|
||||
audio.restore();
|
||||
this.cameras.main.setZoom(1);
|
||||
director.completeTask('T14');
|
||||
this.ui().toast('你保持了整整 60 秒的微笑。', 2500);
|
||||
}
|
||||
}
|
||||
|
||||
private updateRain(_delta: number): void {
|
||||
const raining = director.snapshot.day === 6 && director.snapshot.sunset;
|
||||
if (!raining) { this.rainGraphics?.clear(); this.rainSince = undefined; return; }
|
||||
this.rainGraphics ??= this.add.graphics().setScrollFactor(0).setDepth(50);
|
||||
this.rainGraphics.clear().lineStyle(1, 0xb8e7f2, 0.75);
|
||||
for (let i = 0; i < 45; i += 1) {
|
||||
const x = (i * 43 + Math.floor(performance.now() / 12)) % 340 - 10;
|
||||
const y = (i * 29 + Math.floor(performance.now() / 7)) % 190 - 10;
|
||||
this.rainGraphics.lineBetween(x, y, x - 3, y + 8);
|
||||
}
|
||||
if (this.player.body.speed > 0) { this.rainSince = undefined; return; }
|
||||
this.rainSince ??= performance.now();
|
||||
if (performance.now() - this.rainSince >= 10_000 && director.sadBehavior('rain')) this.ui().toast('你在雨里站了十秒。');
|
||||
}
|
||||
|
||||
private updateReflection(): void {
|
||||
if (!this.reflection) return;
|
||||
this.reflectionHistory.push({ x: this.player.x, y: this.player.y, frame: Number(this.player.frame.name) });
|
||||
const delayed = this.reflectionHistory.length > 1 ? this.reflectionHistory.shift() : undefined;
|
||||
if (!delayed) return;
|
||||
this.reflection.setPosition(delayed.x, 106 + (106 - delayed.y) * 0.15).setFrame(delayed.frame);
|
||||
}
|
||||
|
||||
private updateNpcFacing(): void {
|
||||
if (director.stage === 'S3' || director.stage === 'S4') this.npcs.forEach((sprite, id) => { if (!id.startsWith('birds')) sprite.setFlipX(sprite.x > this.player.x); });
|
||||
if (director.stage === 'S4' && this.extras.length) this.tweens.killTweensOf(this.extras[0]);
|
||||
if (director.stage === 'S4' && Math.random() < 0.6 / 3600) {
|
||||
const ids = [...this.npcs.keys()].filter((id): id is NpcId => id in NPCS && id !== 'doctor' && id !== 'turtle' && id !== 'birds');
|
||||
const id = Phaser.Utils.Array.GetRandom(ids);
|
||||
const sprite = id ? this.npcs.get(id) : undefined;
|
||||
if (id && sprite) {
|
||||
sprite.setFrame(NPCS[id].frame + 2);
|
||||
this.time.delayedCall(100, () => sprite.active && sprite.setFrame(NPCS[id].frame + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async interact(): Promise<void> {
|
||||
if (!this.nearest || this.interacting || director.snapshot.dialogueOpen) return;
|
||||
this.interacting = true;
|
||||
try {
|
||||
if (this.nearest.type === 'door') this.useDoor(this.nearest);
|
||||
else if (this.nearest.type === 'npc') await this.interactNpc(String(this.prop(this.nearest, 'id')));
|
||||
else await this.interactPoint(String(this.prop(this.nearest, 'id')));
|
||||
} finally { this.interacting = false; }
|
||||
}
|
||||
|
||||
private useDoor(object: MapObject): void {
|
||||
if (director.snapshot.cheerActive) director.escapeCheer();
|
||||
const target = String(this.prop(object, 'target'));
|
||||
const spawn = String(this.prop(object, 'spawn') || 'default');
|
||||
this.scene.start(target, { spawn });
|
||||
}
|
||||
|
||||
private async interactNpc(id: string): Promise<void> {
|
||||
if (id.startsWith('extra')) { await this.say('村民', '今天也是完美的一天。', 0); return; }
|
||||
if (!NPC_IDS.has(id)) return;
|
||||
const npcId = id as NpcId;
|
||||
director.recordDialogue(id);
|
||||
if (await this.handleNpcTask(npcId)) return;
|
||||
if (npcId === 'turtle' && C9_LINES[director.snapshot.day]) {
|
||||
await this.say('龟爷爷', C9_LINES[director.snapshot.day], NPCS.turtle.portrait);
|
||||
director.setFlag(`c9-${director.snapshot.day}`, true);
|
||||
if (director.snapshot.day === 6) director.discoverClue('C9');
|
||||
this.checkGreeting(id);
|
||||
return;
|
||||
}
|
||||
const lines: Record<NpcId, string> = {
|
||||
afu: '今天也要把幸福装满哦。', yuanyuan: '刚出炉的蜂蜜面包最甜啦。', dabao: '每一封信都会准时送到。',
|
||||
mimi: '货架永远整整齐齐,真让人开心。', doctor: '村里有诊所,但你不需要去那里哦。',
|
||||
birds: '啾、啾。旋律每天都一样好听。', turtle: '湖水一直很平静。',
|
||||
};
|
||||
const replayGreeting = director.snapshot.day === 4 && director.isTaskActive('T10');
|
||||
const choice = await this.say(NPCS[npcId].name, lines[npcId], NPCS[npcId].portrait, true, undefined, replayGreeting);
|
||||
this.applyChoice(choice, id);
|
||||
if (npcId === 'birds') director.smallInteraction('feed');
|
||||
if (npcId === 'afu') director.smallInteraction('pet');
|
||||
this.checkGreeting(id);
|
||||
}
|
||||
|
||||
private async handleNpcTask(id: NpcId): Promise<boolean> {
|
||||
const breadTask = director.snapshot.day === 4 ? 'T8' : 'T1';
|
||||
const greetTask = director.snapshot.day === 4 ? 'T10' : 'T3';
|
||||
if (id === 'yuanyuan' && director.isTaskActive(breadTask) && !director.flag(`${breadTask}-picked`)) {
|
||||
director.setFlag(`${breadTask}-picked`);
|
||||
await this.say('圆圆', '蜂蜜面包刚刚出炉。请趁热送给大家吧。', NPCS.yuanyuan.portrait, false, undefined, director.snapshot.day === 4);
|
||||
this.checkGreeting(id, greetTask); return true;
|
||||
}
|
||||
if (director.isTaskActive(breadTask) && director.flag(`${breadTask}-picked`) && ['dabao', 'mimi', 'turtle'].includes(id) && !director.flag(`${breadTask}-${id}`)) {
|
||||
director.setFlag(`${breadTask}-${id}`);
|
||||
await this.say(NPCS[id].name, '谢谢你。今天的面包也和昨天一样甜。', NPCS[id].portrait, false, undefined, director.snapshot.day === 4);
|
||||
if (director.increment(`${breadTask}-delivered`, 3) === 3) director.completeTask(breadTask);
|
||||
this.checkGreeting(id, greetTask); return true;
|
||||
}
|
||||
if (id === 'dabao' && director.isTaskActive('T4') && !director.flag('T4-picked')) {
|
||||
director.setFlag('T4-picked');
|
||||
await this.say('大宝', '三封信。圆圆、咪咪、芙医生。邮戳都已经盖好了。', NPCS.dabao.portrait);
|
||||
return true;
|
||||
}
|
||||
if (director.isTaskActive('T4') && director.flag('T4-picked') && ['yuanyuan', 'mimi', 'doctor'].includes(id) && !director.flag(`T4-${id}`)) {
|
||||
director.setFlag(`T4-${id}`);
|
||||
await this.say(NPCS[id].name, '信收到了。谢谢你。', NPCS[id].portrait);
|
||||
const count = director.increment('T4-delivered', 3);
|
||||
if (count === 3) {
|
||||
this.ui().toast('署名:PT-0▓', 200);
|
||||
await this.wait(200);
|
||||
director.discoverClue('C4');
|
||||
director.completeTask('T4');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (id === 'yuanyuan' && director.isTaskActive('T11')) {
|
||||
const choice = await this.say('圆圆', '来帮我揉面吧。只要一直微笑,面团就会很柔软。', NPCS.yuanyuan.portrait, false, [
|
||||
{ id: 'gentle', label: '好,我来帮忙。' }, { id: 'hesitate', label: '让我想一想。' }, { id: 'negative', label: '我不想做。' },
|
||||
]);
|
||||
if (choice === 'negative') director.refuseTask('T11');
|
||||
else { if (choice === 'hesitate') director.adjustAwareness(2); director.completeTask('T11'); }
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private checkGreeting(id: string, forcedTask?: string): void {
|
||||
const task = forcedTask ?? (director.snapshot.day === 4 ? 'T10' : 'T3');
|
||||
if (!director.isTaskActive(task) || director.flag(`${task}-greet-${id}`)) return;
|
||||
director.setFlag(`${task}-greet-${id}`);
|
||||
if (director.increment(`${task}-greetings`, 3) === 3) director.completeTask(task);
|
||||
}
|
||||
|
||||
private async interactPoint(id: string): Promise<void> {
|
||||
if (id === 'board') { await this.interactBoard(); return; }
|
||||
if (id === 'bed') { await this.interactBed(); return; }
|
||||
if (id === 'calendar') {
|
||||
if (director.snapshot.day < 2) await this.say('日历', '今天的日期旁画着一朵向日葵。');
|
||||
else { await this.say('日历', '每一页都是同一个日期'); director.discoverClue('C1'); }
|
||||
return;
|
||||
}
|
||||
if (id === 'recipe') {
|
||||
if (director.snapshot.day < 3) await this.say('配方纸', '纸张背面粘着糖霜。');
|
||||
else { await this.say('配方纸', '手写小字:「按疗程配比。甜度:最大。」'); director.discoverClue('C2'); }
|
||||
return;
|
||||
}
|
||||
if (id === 'mail') {
|
||||
if (director.snapshot.day < 2) await this.say('散落信件', '几封还没有投递的信。');
|
||||
else { await this.say('散落信件', '所有邮戳是同一天'); director.discoverClue('C3'); }
|
||||
return;
|
||||
}
|
||||
if (id === 'shelf') {
|
||||
await this.say('货架', '同一商品重复排列,标签全部空白');
|
||||
if (director.snapshot.day >= 2) director.discoverClue('C5');
|
||||
if (director.isTaskActive('T5')) director.completeTask('T5');
|
||||
return;
|
||||
}
|
||||
if (id.startsWith('flower')) {
|
||||
const task = director.snapshot.day === 4 ? 'T9' : 'T2';
|
||||
if (director.isTaskActive(task)) this.handleRepeatedPoint(id, task, 'flowers', 3, '你给花坛浇了水。');
|
||||
else if (director.smallInteraction('water')) this.ui().toast('你给花浇了一点水。');
|
||||
else this.ui().toast('今天的花已经喝饱了。');
|
||||
return;
|
||||
}
|
||||
if (id.startsWith('berry')) { this.handleRepeatedPoint(id, 'T6', 'berries', 5, '你采下一把莓果。'); return; }
|
||||
if (id.startsWith('ribbon')) { this.handleRepeatedPoint(id, 'T13', 'ribbons', 4, '彩带被挂得笔直。'); return; }
|
||||
if (id === 'picnic' && director.isTaskActive('T7')) {
|
||||
await this.say('湖畔', ['野餐布上的餐具摆得一丝不差。', '湖中的倒影晚了一帧才坐下。']);
|
||||
director.completeTask('T7'); return;
|
||||
}
|
||||
if (id === 'well') { await this.interactWell(); return; }
|
||||
if (id === 'form') {
|
||||
if (director.snapshot.day < 5) await this.say('芙医生', '桌上的表格还没有填写,请别碰哦。', NPCS.doctor.portrait);
|
||||
else { await this.say('表格', '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」'); director.discoverClue('C8'); }
|
||||
return;
|
||||
}
|
||||
if (id === 'mist') { await this.interactMist(); return; }
|
||||
if (id === 'festival') { await this.interactFestival(); return; }
|
||||
if (id === 'oven') await this.say('烤炉', '炉火永远保持同样的温度。');
|
||||
if (id === 'lake') await this.say('湖面', '倒影跟着你。慢了一帧。');
|
||||
}
|
||||
|
||||
private async interactBoard(): Promise<void> {
|
||||
const day = director.snapshot.day;
|
||||
if (!director.snapshot.boardRead) {
|
||||
await this.say('阿福', ANNOUNCEMENTS[day], NPCS.afu.portrait, false, undefined, day === 4);
|
||||
director.acceptDay();
|
||||
return;
|
||||
}
|
||||
if (day === 5 && director.isTaskActive('T12')) {
|
||||
const choice = await this.say('公告板', '木板需要擦得干干净净。', undefined, false, [
|
||||
{ id: 'gentle', label: '认真擦拭。' }, { id: 'hesitate', label: '停下来看看旧字。' }, { id: 'negative', label: '拒绝擦拭。' },
|
||||
]);
|
||||
if (choice === 'negative') director.refuseTask('T12');
|
||||
else {
|
||||
if (choice === 'hesitate') director.adjustAwareness(2);
|
||||
await this.say('旧字迹', '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」');
|
||||
director.discoverClue('C6'); director.completeTask('T12');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const tasks = director.tasksForDay().map((task) => `${task.id} ${task.title}`).join('\n');
|
||||
await this.say('今日幸福任务', tasks || '今天没有新的任务。');
|
||||
}
|
||||
|
||||
private async interactBed(): Promise<void> {
|
||||
if (!director.snapshot.sunset) { await this.say('床', '现在还不到睡觉时间。'); return; }
|
||||
const oldDay = director.snapshot.day;
|
||||
await this.say('梦境', oldDay >= 3 ? ['你闭上眼睛。', '白色墙壁闪过一秒。'] : '你闭上眼睛。甜味沉进梦里。');
|
||||
this.cameras.main.fadeOut(450, 255, 245, 220);
|
||||
this.time.delayedCall(500, () => {
|
||||
director.advanceDay();
|
||||
if (oldDay >= 3) this.scene.start('WhiteScene', { dream: true });
|
||||
else this.scene.restart({ spawn: 'default' });
|
||||
});
|
||||
}
|
||||
|
||||
private async interactWell(): Promise<void> {
|
||||
if (director.snapshot.day < 5) { await this.say('阿福', '井边有点滑。等自由日再来吧。', NPCS.afu.portrait); return; }
|
||||
await this.say('古井', '你向井底看去。五秒里,只有自己的呼吸。');
|
||||
this.ui().toast('注视古井……', 5000);
|
||||
await this.wait(5000);
|
||||
if (!director.flag('well-gaze')) { director.setFlag('well-gaze'); director.adjustAwareness(3); }
|
||||
audio.beep('monitor'); director.discoverClue('C7');
|
||||
}
|
||||
|
||||
private async interactMist(): Promise<void> {
|
||||
if (director.snapshot.day < 5) { await this.say('雾墙', '一位村民温柔地摇头。现在还不能靠近。'); return; }
|
||||
if (director.snapshot.day === 7 && director.snapshot.festivalStarted && director.ending() === 'E3') { this.startEnding('E3'); return; }
|
||||
await this.say('雾墙', '雾在五秒里维持着完全相同的形状。');
|
||||
this.ui().toast('注视雾墙……', 5000);
|
||||
await this.wait(5000);
|
||||
if (!director.flag('mist-gaze')) { director.setFlag('mist-gaze'); director.adjustAwareness(3); }
|
||||
}
|
||||
|
||||
private async interactFestival(): Promise<void> {
|
||||
if (director.isTaskActive('T14')) {
|
||||
if (!director.snapshot.completed.has('T13')) { await this.say('庆典场地', '彩带还没有挂完。'); return; }
|
||||
await this.say('阿福', '请站在这里,保持微笑 60 秒。', NPCS.afu.portrait);
|
||||
director.setT14(true); this.t14Start = performance.now(); this.ui().toast('保持微笑。', 2000); return;
|
||||
}
|
||||
if (!director.isTaskActive('T15')) { await this.say('庆典场地', '这里正等待第 413 届丰收庆典。'); return; }
|
||||
director.completeTask('T15'); director.setFestivalStarted();
|
||||
const ending = director.ending();
|
||||
if (ending === 'E3') { await this.say('雾墙', '庆典的歌声里,雾墙裂开了一条白色的缝。'); this.ui().toast('走向雾墙。', 3000); }
|
||||
else if (ending) this.startEnding(ending);
|
||||
else await this.say('庆典场地', '庆典仍在等待幸福抵达满格。');
|
||||
}
|
||||
|
||||
private handleRepeatedPoint(id: string, task: string, counter: string, total: number, message: string): void {
|
||||
if (!director.isTaskActive(task)) { this.ui().toast('这里暂时没有任务。'); return; }
|
||||
if (director.flag(`${task}-${id}`)) { this.ui().toast('这里已经完成了。'); return; }
|
||||
director.setFlag(`${task}-${id}`);
|
||||
this.ui().toast(message);
|
||||
if (director.increment(`${task}-${counter}`, total) === total) director.completeTask(task);
|
||||
}
|
||||
|
||||
private async say(speaker: string, text: string | string[], portrait?: number, defaultChoices = false, choices?: Array<{ id: DialogueChoice; label: string }>, clean = false): Promise<DialogueChoice | undefined> {
|
||||
const options = choices ?? (defaultChoices ? [
|
||||
{ id: 'gentle' as const, label: '温柔地回应。' }, { id: 'hesitate' as const, label: '迟疑片刻。' }, { id: 'negative' as const, label: '否定这句话。' },
|
||||
] : undefined);
|
||||
return this.ui().showDialogue({ speaker, text, portrait, choices: options, clean });
|
||||
}
|
||||
|
||||
private applyChoice(choice: DialogueChoice | undefined, npc: string): void {
|
||||
if (choice === 'gentle') director.adjustHappiness(2);
|
||||
if (choice === 'hesitate') director.adjustAwareness(2);
|
||||
if (choice === 'negative') director.sadBehavior(`negative-${npc}-${director.increment(`negative-${npc}`)}`);
|
||||
}
|
||||
|
||||
private applyCorruption = (): void => {
|
||||
const saturation = STAGE_SATURATION[director.stage];
|
||||
this.colorFx?.reset().saturate(saturation - 1);
|
||||
if (this.vignetteFx) {
|
||||
this.vignetteFx.strength = director.stage === 'S4' ? 0.55 : director.stage === 'S3' ? 0.28 : 0;
|
||||
this.vignetteFx.radius = 0.72;
|
||||
}
|
||||
this.npcs.forEach((sprite, id) => {
|
||||
if (id.startsWith('birds')) {
|
||||
sprite.anims.timeScale = director.stage === 'S2' ? 0.8 : director.stage === 'S3' ? 0.55 : 1;
|
||||
}
|
||||
if (!(id in NPCS)) return;
|
||||
const npcId = id as NpcId;
|
||||
const expression = director.npcExpression(npcId);
|
||||
const offset = expression === 'flat' ? 1 : expression === 'off' ? 2 : 0;
|
||||
if (npcId !== 'birds') sprite.setFrame(NPCS[npcId].frame + offset);
|
||||
});
|
||||
};
|
||||
|
||||
private negativeFlash = (): void => {
|
||||
this.colorFx?.negative();
|
||||
this.time.delayedCall(17, () => this.applyCorruption());
|
||||
};
|
||||
|
||||
private firstFull = (): void => {
|
||||
this.cameras.main.flash(1000, 255, 250, 210);
|
||||
this.ui().toast('幸福溢出了。', 1800);
|
||||
};
|
||||
|
||||
private turnNearestNpc = (): void => {
|
||||
const nearest = [...this.npcs.values()].sort((a, b) => Phaser.Math.Distance.Between(a.x, a.y, this.player.x, this.player.y) - Phaser.Math.Distance.Between(b.x, b.y, this.player.x, this.player.y))[0];
|
||||
if (!nearest) return;
|
||||
nearest.setFlipX(nearest.x > this.player.x);
|
||||
nearest.setTint(0xfff1c2);
|
||||
this.time.delayedCall(1000, () => nearest.active && nearest.clearTint());
|
||||
};
|
||||
|
||||
private startForcedCheer = (): void => {
|
||||
this.cameras.main.flash(500, 255, 224, 120);
|
||||
this.cheerSprites.forEach((sprite) => sprite.destroy());
|
||||
this.cheerSprites = [];
|
||||
for (let i = 0; i < 6; i += 1) this.cheerSprites.push(this.add.sprite(this.player.x + Math.cos(i) * 38, this.player.y + Math.sin(i) * 28, 'characters', 37 + (i % 5)).setDepth(19));
|
||||
this.cheerTimer?.remove();
|
||||
this.cheerTimer = this.time.delayedCall(15_000, () => {
|
||||
this.cheerSprites.forEach((sprite) => sprite.destroy()); this.cheerSprites = [];
|
||||
director.finishCheer();
|
||||
});
|
||||
};
|
||||
|
||||
private debugTeleport = (key: string): void => { if (key && key !== this.scene.key) this.scene.start(key, { spawn: 'default' }); };
|
||||
private startEnding = (ending: EndingId): void => { this.scene.start('EndingScene', { ending }); };
|
||||
|
||||
private playDreamFlash(): void {
|
||||
this.player.setVisible(false);
|
||||
this.cameras.main.flash(250, 255, 255, 255);
|
||||
this.add.text(160, 82, '白色病房。', { fontFamily: '"Courier New", "PingFang SC", monospace', fontSize: '9px', color: '#626a6c' }).setOrigin(0.5).setScrollFactor(0).setDepth(60);
|
||||
this.time.delayedCall(1000, () => this.scene.start('HouseScene', { spawn: 'default' }));
|
||||
}
|
||||
|
||||
private prop(object: MapObject, name: string): string | number | boolean | undefined {
|
||||
return object.properties?.find((property) => property.name === name)?.value;
|
||||
}
|
||||
|
||||
private wait(ms: number): Promise<void> { return new Promise((resolve) => this.time.delayedCall(ms, resolve)); }
|
||||
private ui(): UIScene { return this.scene.get('UIScene') as UIScene; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Phaser from 'phaser';
|
||||
import './style.css';
|
||||
import { MAP_KEYS } from './game/data';
|
||||
import { BootScene } from './game/scenes/BootScene';
|
||||
import { EndingScene } from './game/scenes/EndingScene';
|
||||
import { TitleScene } from './game/scenes/TitleScene';
|
||||
import { UIScene } from './game/scenes/UIScene';
|
||||
import { WorldScene } from './game/scenes/WorldScene';
|
||||
import { director } from './game/CorruptionDirector';
|
||||
|
||||
declare global {
|
||||
interface Window { __HONEY_GAME__: Phaser.Game }
|
||||
}
|
||||
|
||||
const game = new Phaser.Game({
|
||||
type: Phaser.WEBGL,
|
||||
parent: 'game',
|
||||
backgroundColor: '#79cce4',
|
||||
pixelArt: true,
|
||||
roundPixels: true,
|
||||
width: 320,
|
||||
height: 180,
|
||||
scale: {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||
autoRound: true,
|
||||
zoom: Phaser.Scale.MAX_ZOOM,
|
||||
width: 320,
|
||||
height: 180,
|
||||
},
|
||||
physics: {
|
||||
default: 'arcade',
|
||||
arcade: { debug: false },
|
||||
},
|
||||
dom: { createContainer: true },
|
||||
render: { antialias: false, pixelArt: true, roundPixels: true },
|
||||
scene: [
|
||||
BootScene,
|
||||
TitleScene,
|
||||
...MAP_KEYS.map((key) => new WorldScene(key)),
|
||||
UIScene,
|
||||
EndingScene,
|
||||
],
|
||||
});
|
||||
|
||||
game.registry.set('director', director);
|
||||
window.__HONEY_GAME__ = game;
|
||||
@@ -0,0 +1,87 @@
|
||||
:root {
|
||||
color: #fff4d6;
|
||||
background: #2f2627;
|
||||
font-family: "Courier New", "PingFang SC", "Microsoft YaHei", monospace;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body, #game {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #2f2627;
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.name-form {
|
||||
width: 210px;
|
||||
padding: 12px;
|
||||
color: #3d2925;
|
||||
background: #fff0bc;
|
||||
border: 3px solid #5b362b;
|
||||
box-shadow: 0 4px 0 #3b2725;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name-form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.name-form input {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 5px 8px;
|
||||
color: #3d2925;
|
||||
background: #fffaf0;
|
||||
border: 2px solid #9b5b30;
|
||||
border-radius: 0;
|
||||
font: 700 14px "Courier New", "PingFang SC", monospace;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.name-form input:focus { border-color: #e06f46; }
|
||||
|
||||
.name-form button {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 7px 10px;
|
||||
color: #fff7d5;
|
||||
background: #c75f38;
|
||||
border: 2px solid #5b362b;
|
||||
border-radius: 0;
|
||||
font: 700 12px "Courier New", "PingFang SC", monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
width: 250px;
|
||||
padding: 10px;
|
||||
color: #f4f4f4;
|
||||
background: rgba(24, 22, 26, 0.97);
|
||||
border: 1px solid #f0b64a;
|
||||
font: 11px/1.4 "Courier New", "PingFang SC", monospace;
|
||||
}
|
||||
|
||||
.debug-panel h2 { margin: 0 0 8px; color: #f0b64a; font-size: 13px; }
|
||||
.debug-row { display: flex; gap: 5px; margin: 5px 0; }
|
||||
.debug-panel label { width: 70px; padding-top: 3px; }
|
||||
.debug-panel input, .debug-panel select, .debug-panel button {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
background: #38333c;
|
||||
border: 1px solid #716a78;
|
||||
font: inherit;
|
||||
}
|
||||
.debug-panel button { padding: 4px; cursor: pointer; }
|
||||
.debug-panel .ending-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
|
||||
@@ -0,0 +1,38 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { MAP_KEYS } from '../src/game/data';
|
||||
|
||||
const doors = MAP_KEYS.flatMap((source) => {
|
||||
const map = JSON.parse(readFileSync(`public/maps/${source}.json`, 'utf8')) as {
|
||||
layers: Array<{ name: string; objects?: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }>;
|
||||
};
|
||||
return (map.layers.find((layer) => layer.name === 'objects')?.objects ?? [])
|
||||
.filter((object) => object.type === 'door')
|
||||
.map((object) => ({ source, target: object.properties?.find((property) => property.name === 'target')?.value ?? '' }));
|
||||
});
|
||||
|
||||
test('every authored door reaches its paired target scene', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('#player-name').press('Enter');
|
||||
await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true);
|
||||
|
||||
for (const { source, target } of doors) {
|
||||
await page.evaluate((key) => {
|
||||
const director = window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, value: string) => void };
|
||||
director.emit('debug-teleport', key);
|
||||
}, source);
|
||||
await expect.poll(() => page.evaluate((key) => window.__HONEY_GAME__.scene.isActive(key), source)).toBe(true);
|
||||
await page.evaluate(({ sceneKey, targetKey }) => {
|
||||
const scene = window.__HONEY_GAME__.scene.getScene(sceneKey) as unknown as {
|
||||
objects: Array<{ type: string; x: number; y: number; properties?: Array<{ name: string; value: string }> }>;
|
||||
player: { setPosition: (x: number, y: number) => void };
|
||||
};
|
||||
const door = scene.objects.find((object) => object.type === 'door' && object.properties?.some((property) => property.name === 'target' && property.value === targetKey));
|
||||
if (!door) throw new Error(`Door ${sceneKey} -> ${targetKey} not found`);
|
||||
scene.player.setPosition(door.x, door.y);
|
||||
}, { sceneKey: source, targetKey: target });
|
||||
await page.waitForTimeout(80);
|
||||
await page.keyboard.press('e');
|
||||
await expect.poll(() => page.evaluate((key) => window.__HONEY_GAME__.scene.isActive(key), target)).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
async function activeScenes(page: Page): Promise<string[]> {
|
||||
return page.evaluate(() => window.__HONEY_GAME__.scene.getScenes(true).map((scene) => scene.scene.key));
|
||||
}
|
||||
|
||||
test('title, movement, maps, corruption controls, and ending flow', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()); });
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.locator('.name-form')).toBeVisible();
|
||||
await expect(page.locator('#player-name')).toHaveAttribute('placeholder', '小满');
|
||||
await page.screenshot({ path: '.qa/title.png' });
|
||||
await page.locator('#player-name').fill('验收员');
|
||||
await page.getByRole('button', { name: '进入蜜糖村' }).click();
|
||||
await expect.poll(() => activeScenes(page)).toContain('HouseScene');
|
||||
await expect.poll(() => activeScenes(page)).toContain('UIScene');
|
||||
|
||||
const startX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x);
|
||||
await page.keyboard.down('ArrowRight'); await page.waitForTimeout(250); await page.keyboard.up('ArrowRight');
|
||||
const movedX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x);
|
||||
expect(movedX).toBeGreaterThan(startX);
|
||||
for (const [key, animation] of [['w', 'walk-up'], ['s', 'walk-down'], ['a', 'walk-left'], ['d', 'walk-right']] as const) {
|
||||
await page.keyboard.down(key); await page.waitForTimeout(80);
|
||||
const activeAnimation = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { anims: { currentAnim?: { key: string } } } }).player.anims.currentAnim?.key);
|
||||
expect(activeAnimation).toBe(animation);
|
||||
await page.keyboard.up(key);
|
||||
}
|
||||
await page.keyboard.down('ArrowLeft'); await page.waitForTimeout(1600); await page.keyboard.up('ArrowLeft');
|
||||
const boundedX = await page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('HouseScene') as unknown as { player: { x: number } }).player.x);
|
||||
expect(boundedX).toBeGreaterThanOrEqual(5);
|
||||
|
||||
await page.keyboard.press('Backquote');
|
||||
await expect(page.locator('.debug-panel')).toBeVisible();
|
||||
await page.locator('[data-field="happiness"]').fill('100');
|
||||
await page.locator('[data-action="happiness"]').click();
|
||||
await page.locator('[data-field="awareness"]').fill('72');
|
||||
await page.locator('[data-action="awareness"]').click();
|
||||
await page.locator('[data-field="day"]').fill('7');
|
||||
await page.locator('[data-action="day"]').click();
|
||||
const state = await page.evaluate(() => {
|
||||
const d = window.__HONEY_GAME__.registry.get('director') as { snapshot: { happiness: number; awareness: number; day: number }; stage: string };
|
||||
return { ...d.snapshot, stage: d.stage };
|
||||
});
|
||||
expect(state).toMatchObject({ happiness: 100, awareness: 72, day: 7, stage: 'S4' });
|
||||
await page.screenshot({ path: '.qa/debug.png' });
|
||||
|
||||
const maps = ['HouseScene', 'PlazaScene', 'BakeryScene', 'ShopScene', 'ClinicScene', 'LakeScene', 'FieldScene', 'WellScene', 'MistScene', 'WhiteScene'];
|
||||
for (const key of maps) {
|
||||
await page.locator('[data-field="scene"]').selectOption(key);
|
||||
await page.locator('[data-action="teleport"]').click();
|
||||
await expect.poll(() => activeScenes(page)).toContain(key);
|
||||
}
|
||||
|
||||
await page.locator('[data-field="scene"]').selectOption('PlazaScene');
|
||||
await page.locator('[data-action="teleport"]').click();
|
||||
await expect.poll(() => activeScenes(page)).toContain('PlazaScene');
|
||||
await page.keyboard.press('Backquote');
|
||||
await page.screenshot({ path: '.qa/plaza-s4.png' });
|
||||
await page.keyboard.press('Backquote');
|
||||
await page.locator('[data-field="happiness"]').fill('40');
|
||||
await page.locator('[data-action="happiness"]').click();
|
||||
await page.locator('[data-field="day"]').fill('1');
|
||||
await page.locator('[data-action="day"]').click();
|
||||
await page.keyboard.press('Backquote');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: '.qa/plaza-s1.png' });
|
||||
await page.evaluate(() => {
|
||||
const ui = window.__HONEY_GAME__.scene.getScene('UIScene') as unknown as { showDialogue: (options: object) => Promise<unknown> };
|
||||
void ui.showDialogue({ speaker: '阿福', text: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', portrait: 2 });
|
||||
});
|
||||
await page.waitForTimeout(100);
|
||||
await page.screenshot({ path: '.qa/dialogue.png' });
|
||||
await page.keyboard.press('e');
|
||||
await page.keyboard.press('Backquote');
|
||||
await page.locator('[data-action="clues"]').click();
|
||||
const clueCount = await page.evaluate(() => (window.__HONEY_GAME__.registry.get('director') as { snapshot: { clues: Set<string> } }).snapshot.clues.size);
|
||||
expect(clueCount).toBe(9);
|
||||
await page.locator('[data-ending="E3"]').click();
|
||||
await expect.poll(() => activeScenes(page)).toContain('EndingScene');
|
||||
await page.waitForTimeout(3200);
|
||||
await page.screenshot({ path: '.qa/e3.png' });
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('integer scaling stays sharp at desktop and compact viewport', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('#player-name').press('Enter');
|
||||
await expect.poll(() => activeScenes(page)).toContain('HouseScene');
|
||||
const desktop = await page.locator('canvas').boundingBox();
|
||||
expect(desktop?.width).toBe(960);
|
||||
expect(desktop?.height).toBe(540);
|
||||
await page.setViewportSize({ width: 640, height: 360 });
|
||||
await page.waitForTimeout(250);
|
||||
const compact = await page.locator('canvas').boundingBox();
|
||||
expect(compact?.width).toBe(640);
|
||||
expect(compact?.height).toBe(360);
|
||||
});
|
||||
|
||||
test('all four forced endings render their specified evidence', async ({ page }) => {
|
||||
const startEnding = async (ending: string, name = '小满') => {
|
||||
await page.goto('/');
|
||||
await page.locator('#player-name').fill(name);
|
||||
await page.locator('#player-name').press('Enter');
|
||||
await expect.poll(() => activeScenes(page)).toContain('HouseScene');
|
||||
await page.evaluate((id) => (window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, ending: string) => void }).emit('force-ending', id), ending);
|
||||
await expect.poll(() => activeScenes(page)).toContain('EndingScene');
|
||||
};
|
||||
const texts = () => page.evaluate(() => (window.__HONEY_GAME__.scene.getScene('EndingScene') as unknown as { children: { list: Array<{ type?: string; text?: string }> } }).children.list
|
||||
.filter((child) => child.type === 'Text')
|
||||
.map((text) => text.text ?? ''));
|
||||
|
||||
await startEnding('E1');
|
||||
expect((await texts()).join('\n')).toContain('从此,每一天都很幸福。\n\n每一天。');
|
||||
await startEnding('E2'); await page.waitForTimeout(2200);
|
||||
expect((await texts()).join('\n')).toContain('疗程重新开始。');
|
||||
await startEnding('E3', '真实姓名'); await page.waitForTimeout(3200);
|
||||
const record = (await texts()).join('\n');
|
||||
expect(record).toContain('蜜糖谷情绪疗养中心');
|
||||
expect(record).toContain('姓名:真实姓名');
|
||||
expect(record).toContain('编号:PT-07');
|
||||
expect(record).toMatch(/疗程时长:\d{2}:\d{2}/);
|
||||
await page.waitForTimeout(2900);
|
||||
expect((await texts()).join('\n')).toContain('微笑消失了。');
|
||||
await startEnding('E4');
|
||||
expect((await texts()).join('\n')).toContain('系统检测到疗程无效。');
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { ANNOUNCEMENTS, C9_LINES, CLUES, CORRUPTIONS, MAP_KEYS, TASKS } from '../src/game/data';
|
||||
|
||||
test('fixed PRD tables remain exact', () => {
|
||||
expect(TASKS).toHaveLength(15);
|
||||
expect(Object.keys(CLUES)).toHaveLength(9);
|
||||
expect(CORRUPTIONS).toEqual([
|
||||
['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'],
|
||||
['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'],
|
||||
]);
|
||||
expect(ANNOUNCEMENTS).toEqual({
|
||||
1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
2: '早上好呀!新的一天,新的幸福。祝你开心!',
|
||||
3: '早上好!今天要把幸福装得满满的哦。要开心!',
|
||||
4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。',
|
||||
6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。',
|
||||
7: '庆典开始了。来吧。',
|
||||
});
|
||||
expect(CLUES).toEqual({
|
||||
C1: '每一页都是同一个日期', C2: '手写小字:「按疗程配比。甜度:最大。」', C3: '所有邮戳是同一天', C4: '署名闪过乱码「PT-0▓」',
|
||||
C5: '同一商品重复排列,标签全部空白', C6: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」',
|
||||
C7: '井底回声:极轻的监护仪滴声(靠近并交互 5 秒)', C8: '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」',
|
||||
C9: '三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」',
|
||||
});
|
||||
expect(C9_LINES).toEqual({ 2: '湖水一直很平静', 4: '面包一直是那个味道', 6: '孩子,今天是第几个今天?' });
|
||||
expect(TASKS.slice(7, 10).map(({ title, reward }) => ({ title, reward }))).toEqual(TASKS.slice(0, 3).map(({ title, reward }) => ({ title, reward })));
|
||||
});
|
||||
|
||||
test('Aseprite sheets and Tiled exports have the required dimensions and layers', () => {
|
||||
const pngSize = (path: string) => {
|
||||
const png = readFileSync(path);
|
||||
return { width: png.readUInt32BE(16), height: png.readUInt32BE(20) };
|
||||
};
|
||||
expect(pngSize(resolve('public/assets/tileset.png'))).toEqual({ width: 256, height: 64 });
|
||||
expect(pngSize(resolve('public/assets/characters.png'))).toEqual({ width: 256, height: 96 });
|
||||
expect(pngSize(resolve('public/assets/portraits.png'))).toEqual({ width: 432, height: 48 });
|
||||
expect(readdirSync('public/assets/source').filter((file) => file.endsWith('.aseprite'))).toHaveLength(3);
|
||||
|
||||
const expectedSizes: Record<string, [number, number]> = {
|
||||
HouseScene: [12, 8], PlazaScene: [30, 20], BakeryScene: [15, 10], ShopScene: [15, 10], ClinicScene: [12, 8],
|
||||
LakeScene: [24, 16], FieldScene: [20, 14], WellScene: [14, 10], MistScene: [20, 8], WhiteScene: [30, 8],
|
||||
};
|
||||
const maps = new Map<string, Record<string, unknown>>();
|
||||
MAP_KEYS.forEach((key) => {
|
||||
const map = JSON.parse(readFileSync(resolve(`public/maps/${key}.json`), 'utf8')) as {
|
||||
width: number; height: number; layers: Array<{ name: string; type: string; objects?: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }>;
|
||||
tilesets: Array<{ tilecount: number }>;
|
||||
};
|
||||
expect([map.width, map.height]).toEqual(expectedSizes[key]);
|
||||
expect(map.layers.map((layer) => layer.name)).toEqual(['ground', 'obstacles', 'objects']);
|
||||
expect(map.tilesets[0].tilecount).toBe(64);
|
||||
maps.set(key, map as unknown as Record<string, unknown>);
|
||||
});
|
||||
|
||||
for (const key of MAP_KEYS) {
|
||||
const map = maps.get(key) as unknown as { layers: Array<{ name: string; objects: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }> };
|
||||
const doors = map.layers.find((layer) => layer.name === 'objects')?.objects.filter((object) => object.type === 'door') ?? [];
|
||||
doors.forEach((door) => {
|
||||
const target = door.properties?.find((property) => property.name === 'target')?.value;
|
||||
const targetMap = maps.get(String(target)) as unknown as { layers: Array<{ name: string; objects: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }> };
|
||||
const reverse = targetMap.layers.find((layer) => layer.name === 'objects')?.objects.some((object) => object.type === 'door' && object.properties?.some((property) => property.name === 'target' && property.value === key));
|
||||
expect(reverse, `${key} -> ${target} needs a reverse door`).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('director enforces rewards, clues, sad behavior, stages, overflow, and endings', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('#player-name').press('Enter');
|
||||
await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true);
|
||||
|
||||
const result = await page.evaluate(({ tasks, corruptionPairs, clueIds }) => {
|
||||
const d = window.__HONEY_GAME__.registry.get('director') as any;
|
||||
const rewards = tasks.map((task: { id: string; day: number; reward: number }) => {
|
||||
d.reset('Test'); d.setDay(task.day); d.acceptDay(); d.setHappiness(0); d.completeTask(task.id);
|
||||
return [task.id, d.snapshot.happiness];
|
||||
});
|
||||
d.reset('Test');
|
||||
const clueDeltas: number[] = [];
|
||||
clueIds.forEach((id) => { const before = d.snapshot.awareness; d.discoverClue(id); clueDeltas.push(d.snapshot.awareness - before); });
|
||||
d.reset('Test'); d.setHappiness(50);
|
||||
['refuse', 'negative', 'idle', 'cling', 'rain'].forEach((key) => d.sadBehavior(key));
|
||||
const sad = { happiness: d.snapshot.happiness, awareness: d.snapshot.awareness };
|
||||
const stages = [10, 40, 70, 90, 100].map((value) => { d.setHappiness(value); return d.stage; });
|
||||
const corruptions = corruptionPairs.map(([clean, broken]: [string, string]) => { d.setHappiness(100); return [broken, d.corruptText(`${clean}。`, '咪咪')]; });
|
||||
d.reset('Test'); d.setHappiness(100); d.adjustHappiness(-5); d.adjustHappiness(-5); d.adjustHappiness(-5);
|
||||
const overflow = { happiness: d.snapshot.happiness, overflow: d.snapshot.overflow };
|
||||
d.reset('Test'); d.setHappiness(30);
|
||||
const small = [d.smallInteraction('water'), d.smallInteraction('water'), d.smallInteraction('water'), d.smallInteraction('water'), d.snapshot.happiness];
|
||||
const endings: string[] = [];
|
||||
d.reset('Test'); d.setHappiness(90); endings.push(d.ending());
|
||||
d.setAwareness(30); endings.push(d.ending());
|
||||
d.reset('Test'); clueIds.slice(0, 6).forEach((id) => d.discoverClue(id)); d.setAwareness(70); endings.push(d.ending());
|
||||
d.reset('Test'); d.setHappiness(10); d.setDay(7); d.snapshot.cheerEscapes = 3; endings.push(d.ending());
|
||||
d.reset('Test'); d.setHappiness(90); d.setAwareness(70); endings.push(d.ending() ?? 'none');
|
||||
d.reset('Test'); d.setHappiness(10); d.snapshot.lowSince = performance.now() - 121_000; d.tick();
|
||||
const forcedCheer = d.snapshot.cheerActive;
|
||||
d.reset('Test'); d.snapshot.dayStartedAt = performance.now() - 241_000; d.acceptDay(); ['T1', 'T2', 'T3'].forEach((id) => d.completeTask(id));
|
||||
const duskReached = d.snapshot.sunset; d.advanceDay();
|
||||
const nextDay = { day: d.snapshot.day, boardRead: d.snapshot.boardRead, sunset: d.snapshot.sunset };
|
||||
return { rewards, clueDeltas, clueCount: d.snapshot.clues.size, sad, stages, corruptions, overflow, small, endings, forcedCheer, duskReached, nextDay };
|
||||
}, { tasks: TASKS.map(({ id, day, reward }) => ({ id, day, reward })), corruptionPairs: CORRUPTIONS, clueIds: Object.keys(CLUES) });
|
||||
|
||||
expect(result.rewards).toEqual(TASKS.map(({ id, reward }) => [id, reward]));
|
||||
expect(result.clueDeltas).toEqual(Array(9).fill(8));
|
||||
expect(result.sad).toEqual({ happiness: 25, awareness: 15 });
|
||||
expect(result.stages).toEqual(['S0', 'S1', 'S2', 'S3', 'S4']);
|
||||
result.corruptions.forEach(([broken, rendered]) => expect(rendered).toContain(broken));
|
||||
expect(result.overflow).toEqual({ happiness: 100, overflow: 0 });
|
||||
expect(result.small).toEqual([true, true, true, false, 33]);
|
||||
expect(result.endings).toEqual(['E1', 'E2', 'E3', 'E4', 'none']);
|
||||
expect(result.forcedCheer).toBe(true);
|
||||
expect(result.duskReached).toBe(true);
|
||||
expect(result.nextDay).toEqual({ day: 2, boardRead: false, sunset: false });
|
||||
});
|
||||
|
||||
test('Day 6 smile timer completes at 60 seconds and scripted rain records sadness', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('#player-name').press('Enter');
|
||||
await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true);
|
||||
await page.evaluate(() => (window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, key: string) => void }).emit('debug-teleport', 'PlazaScene'));
|
||||
await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('PlazaScene'))).toBe(true);
|
||||
const result = await page.evaluate(() => {
|
||||
const director = window.__HONEY_GAME__.registry.get('director') as any;
|
||||
const scene = window.__HONEY_GAME__.scene.getScene('PlazaScene') as any;
|
||||
director.reset('Test'); director.setDay(6); director.acceptDay(); director.setHappiness(30);
|
||||
director.snapshot.dayStartedAt = performance.now() - 241_000;
|
||||
director.completeTask('T13'); director.setT14(true);
|
||||
scene.t14Start = performance.now() - 60_100;
|
||||
scene.updateT14();
|
||||
const smile = { completed: director.snapshot.completed.has('T14'), active: director.snapshot.t14Active, sunset: director.snapshot.sunset, happiness: director.snapshot.happiness };
|
||||
scene.rainSince = performance.now() - 10_100;
|
||||
scene.updateRain(16);
|
||||
return { smile, rain: { awareness: director.snapshot.awareness, happiness: director.snapshot.happiness } };
|
||||
});
|
||||
expect(result.smile).toEqual({ completed: true, active: false, sunset: true, happiness: 55 });
|
||||
expect(result.rain).toEqual({ awareness: 3, happiness: 50 });
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readdirSync, mkdirSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const root = resolve(import.meta.dirname, '..');
|
||||
const sourceDir = resolve(root, 'public/maps/source');
|
||||
const outDir = resolve(root, 'public/maps');
|
||||
const tiled = process.env.TILED ?? '/Applications/Tiled.app/Contents/MacOS/Tiled';
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
for (const file of readdirSync(sourceDir).filter((name) => name.endsWith('.tmx'))) {
|
||||
execFileSync(tiled, ['--export-map', 'json', resolve(sourceDir, file), resolve(outDir, file.replace('.tmx', '.json'))], { stdio: 'inherit' });
|
||||
}
|
||||
console.log('Exported 10 Tiled JSON maps.');
|
||||
@@ -0,0 +1,179 @@
|
||||
local root = app.params["root"]
|
||||
local assetDir = root .. "/public/assets"
|
||||
local sourceDir = assetDir .. "/source"
|
||||
app.fs.makeDirectory(root .. "/public")
|
||||
app.fs.makeDirectory(assetDir)
|
||||
app.fs.makeDirectory(sourceDir)
|
||||
|
||||
local C = {
|
||||
transparent = Color{ r=0, g=0, b=0, a=0 },
|
||||
grass = Color{ r=104, g=190, b=82, a=255 },
|
||||
grass2 = Color{ r=76, g=158, b=70, a=255 },
|
||||
cream = Color{ r=255, g=233, b=164, a=255 },
|
||||
honey = Color{ r=239, g=151, b=49, a=255 },
|
||||
orange = Color{ r=199, g=97, b=43, a=255 },
|
||||
sky = Color{ r=93, g=190, b=224, a=255 },
|
||||
water = Color{ r=68, g=171, b=216, a=255 },
|
||||
water2 = Color{ r=139, g=224, b=231, a=255 },
|
||||
stone = Color{ r=206, g=199, b=171, a=255 },
|
||||
stone2 = Color{ r=162, g=155, b=133, a=255 },
|
||||
brown = Color{ r=91, g=54, b=43, a=255 },
|
||||
wood = Color{ r=151, g=91, b=48, a=255 },
|
||||
leaf = Color{ r=51, g=130, b=60, a=255 },
|
||||
pink = Color{ r=237, g=111, b=150, a=255 },
|
||||
red = Color{ r=194, g=66, b=64, a=255 },
|
||||
white = Color{ r=247, g=244, b=226, a=255 },
|
||||
gray = Color{ r=194, g=199, b=199, a=255 },
|
||||
gray2 = Color{ r=132, g=142, b=145, a=255 },
|
||||
black = Color{ r=48, g=38, b=37, a=255 },
|
||||
yellow = Color{ r=255, g=213, b=62, a=255 },
|
||||
purple = Color{ r=133, g=89, b=159, a=255 },
|
||||
}
|
||||
|
||||
local function fill(img, x, y, w, h, color)
|
||||
for yy = y, y + h - 1 do
|
||||
for xx = x, x + w - 1 do img:drawPixel(xx, yy, color) end
|
||||
end
|
||||
end
|
||||
|
||||
local function pixel(img, x, y, color) img:drawPixel(x, y, color) end
|
||||
|
||||
local function outlineRect(img, x, y, w, h, color)
|
||||
fill(img, x, y, w, 1, color); fill(img, x, y+h-1, w, 1, color)
|
||||
fill(img, x, y, 1, h, color); fill(img, x+w-1, y, 1, h, color)
|
||||
end
|
||||
|
||||
local function save(sprite, sourceName, pngName)
|
||||
sprite:saveAs(sourceDir .. "/" .. sourceName)
|
||||
sprite:saveCopyAs(assetDir .. "/" .. pngName)
|
||||
sprite:close()
|
||||
end
|
||||
|
||||
local function makeTileset()
|
||||
local sprite = Sprite(256, 64, ColorMode.RGB)
|
||||
sprite.filename = "tileset"
|
||||
sprite.cels[1].image:clear(C.transparent)
|
||||
local img = sprite.cels[1].image
|
||||
local function tile(index, base)
|
||||
local x = (index % 16) * 16; local y = math.floor(index / 16) * 16
|
||||
fill(img, x, y, 16, 16, base)
|
||||
return x, y
|
||||
end
|
||||
|
||||
for i=0,63 do
|
||||
local palette = {C.grass, C.stone, C.water, C.cream, C.wood, C.gray}
|
||||
local x,y = tile(i, palette[(i % #palette)+1])
|
||||
if i % 6 == 0 then
|
||||
pixel(img,x+3,y+5,C.grass2); pixel(img,x+11,y+3,C.cream); pixel(img,x+12,y+3,C.pink)
|
||||
elseif i % 6 == 1 then
|
||||
outlineRect(img,x+1,y+1,7,6,C.stone2); outlineRect(img,x+8,y+7,7,8,C.stone2)
|
||||
elseif i % 6 == 2 then
|
||||
fill(img,x,y+3,16,2,C.water2); fill(img,x+5,y+10,9,1,C.water2)
|
||||
elseif i % 6 == 3 then
|
||||
fill(img,x,y,16,4,C.honey); fill(img,x+2,y+7,4,3,C.orange); fill(img,x+10,y+6,4,4,C.orange)
|
||||
elseif i % 6 == 4 then
|
||||
fill(img,x,y+4,16,2,C.brown); fill(img,x+4,y,2,16,C.brown); fill(img,x+12,y,1,16,C.brown)
|
||||
else
|
||||
outlineRect(img,x+1,y+1,14,14,C.gray2); fill(img,x+3,y+3,10,10,C.white)
|
||||
end
|
||||
end
|
||||
|
||||
local x,y = tile(0,C.grass); pixel(img,x+3,y+5,C.grass2); pixel(img,x+11,y+9,C.grass2)
|
||||
x,y = tile(1,C.stone); outlineRect(img,x,y,8,8,C.stone2); outlineRect(img,x+8,y+8,8,8,C.stone2)
|
||||
x,y = tile(2,C.water); fill(img,x,y+4,12,2,C.water2); fill(img,x+5,y+11,11,1,C.water2)
|
||||
x,y = tile(3,C.cream); fill(img,x,y,16,3,C.honey); fill(img,x,y+13,16,3,C.honey)
|
||||
x,y = tile(4,C.wood); fill(img,x,y+4,16,2,C.brown); fill(img,x,y+11,16,1,C.brown)
|
||||
x,y = tile(5,C.white); outlineRect(img,x,y,16,16,C.gray)
|
||||
x,y = tile(6,C.grass); fill(img,x+2,y+3,12,10,C.leaf); pixel(img,x+6,y+6,C.red); pixel(img,x+11,y+9,C.red)
|
||||
x,y = tile(7,C.grass); fill(img,x+1,y+2,14,12,C.leaf); fill(img,x+4,y+4,3,3,C.pink); fill(img,x+10,y+7,3,3,C.yellow)
|
||||
x,y = tile(8,C.honey); fill(img,x,y+6,16,10,C.cream); outlineRect(img,x,y+6,16,10,C.orange)
|
||||
x,y = tile(9,C.orange); fill(img,x,y+6,16,10,C.cream); fill(img,x+6,y+9,4,7,C.brown)
|
||||
x,y = tile(10,C.cream); fill(img,x+2,y+5,12,8,C.wood); outlineRect(img,x+2,y+5,12,8,C.brown)
|
||||
x,y = tile(11,C.grass); fill(img,x+6,y,4,16,C.wood); fill(img,x+1,y,14,8,C.leaf)
|
||||
x,y = tile(12,C.grass); fill(img,x+2,y+2,12,12,C.pink); fill(img,x+5,y+5,6,6,C.yellow)
|
||||
x,y = tile(13,C.wood); fill(img,x+4,y+2,8,14,C.black); fill(img,x+6,y+3,4,11,C.water)
|
||||
x,y = tile(14,C.sky); fill(img,x+4,y,8,16,C.white); fill(img,x+1,y+4,14,8,C.white)
|
||||
x,y = tile(15,C.white); fill(img,x,y,16,3,C.gray); fill(img,x,y+13,16,3,C.gray)
|
||||
x,y = tile(16,C.grass); fill(img,x+3,y+5,10,7,C.white); outlineRect(img,x+3,y+5,10,7,C.brown); pixel(img,x+8,y+8,C.red)
|
||||
save(sprite, "tileset.aseprite", "tileset.png")
|
||||
end
|
||||
|
||||
local speciesColors = {C.cream,C.pink,C.brown,C.orange,C.gray,C.honey,C.grass2,C.purple,C.red,C.sky,C.yellow,C.wood,C.white}
|
||||
|
||||
local function drawCharacter(img, ox, oy, body, facing, step, expression, species)
|
||||
fill(img,ox,oy,16,24,C.transparent)
|
||||
local skin = body
|
||||
fill(img,ox+4,oy+3,8,8,skin)
|
||||
fill(img,ox+3,oy+5,10,5,skin)
|
||||
if species == "rabbit" then fill(img,ox+4,oy,2,5,skin); fill(img,ox+10,oy,2,5,skin) end
|
||||
if species == "deer" then pixel(img,ox+3,oy+2,C.brown); pixel(img,ox+12,oy+2,C.brown) end
|
||||
if species == "bird" then fill(img,ox+3,oy+6,10,8,skin); pixel(img,ox+13,oy+8,C.honey) end
|
||||
local eyeY = oy+6
|
||||
pixel(img,ox+6,eyeY,C.black); pixel(img,ox+10,eyeY + (expression == "off" and 1 or 0),C.black)
|
||||
if expression == "smile" then pixel(img,ox+7,oy+9,C.red); pixel(img,ox+8,oy+10,C.red); pixel(img,ox+9,oy+9,C.red)
|
||||
elseif expression == "flat" then fill(img,ox+7,oy+9,3,1,C.black)
|
||||
else pixel(img,ox+7,oy+9,C.black); fill(img,ox+8,oy+10,3,1,C.black) end
|
||||
fill(img,ox+4,oy+12,8,8,body)
|
||||
fill(img,ox+3,oy+13,2,6,C.cream); fill(img,ox+11,oy+13,2,6,C.cream)
|
||||
local foot = step % 2
|
||||
if facing == "left" or facing == "right" then
|
||||
fill(img,ox+4-foot,oy+20,3,3,C.black); fill(img,ox+9+foot,oy+20,3,3,C.black)
|
||||
else
|
||||
fill(img,ox+4+foot,oy+20,3,3,C.black); fill(img,ox+9-foot,oy+20,3,3,C.black)
|
||||
end
|
||||
if facing == "up" then fill(img,ox+5,oy+5,6,4,skin); fill(img,ox+5,oy+6,6,2,C.brown) end
|
||||
if facing == "left" then pixel(img,ox+5,eyeY,C.black); fill(img,ox+10,eyeY,2,2,skin) end
|
||||
if facing == "right" then pixel(img,ox+11,eyeY,C.black); fill(img,ox+4,eyeY,2,2,skin) end
|
||||
end
|
||||
|
||||
local function makeCharacters()
|
||||
local cols, rows = 16, 4
|
||||
local sprite = Sprite(cols*16, rows*24, ColorMode.RGB)
|
||||
sprite.cels[1].image:clear(C.transparent)
|
||||
local img = sprite.cels[1].image
|
||||
local dirs = {"down","up","left","right"}
|
||||
for d=0,3 do for f=0,3 do drawCharacter(img,f*16,d*24,C.sky,dirs[d+1],f,"smile","human") end end
|
||||
local npc = {
|
||||
{C.honey,"dog"},{C.white,"rabbit"},{C.brown,"bear"},{C.orange,"cat"},
|
||||
{C.cream,"deer"},{C.sky,"bird"},{C.grass2,"turtle"}
|
||||
}
|
||||
for n=0,6 do for e=0,2 do
|
||||
local index = 16 + n*3 + e; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24
|
||||
local exp=({"smile","flat","off"})[e+1]
|
||||
drawCharacter(img,ox,oy,npc[n+1][1],"down",0,exp,npc[n+1][2])
|
||||
end end
|
||||
for n=0,4 do
|
||||
local index=37+n; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24
|
||||
drawCharacter(img,ox,oy,speciesColors[n+8],"down",n%2,"smile","human")
|
||||
end
|
||||
for n=0,3 do
|
||||
local index=42+n; local ox=(index%cols)*16; local oy=math.floor(index/cols)*24
|
||||
drawCharacter(img,ox,oy,(n%2==0 and C.pink or C.sky),"down",n%2,"smile","bird")
|
||||
if n%2==1 then fill(img,ox+7,oy+9,4,2,C.black) end
|
||||
end
|
||||
save(sprite,"characters.aseprite","characters.png")
|
||||
end
|
||||
|
||||
local function drawPortrait(img, ox, body, expression, real)
|
||||
fill(img,ox,0,48,48,C.cream)
|
||||
fill(img,ox+7,6,34,34,C.brown)
|
||||
fill(img,ox+9,8,30,30,body)
|
||||
fill(img,ox+14,17,4,5,C.black); fill(img,ox+30,17 + (expression=="off" and 2 or 0),4,5,C.black)
|
||||
if real then
|
||||
fill(img,ox+15,31,18,2,C.black); fill(img,ox+13,13,8,2,C.gray2); fill(img,ox+27,13,8,2,C.gray2)
|
||||
elseif expression == "smile" then
|
||||
fill(img,ox+17,29,4,3,C.red); fill(img,ox+21,32,8,3,C.red); fill(img,ox+29,29,3,3,C.red)
|
||||
else fill(img,ox+18,30,14,2,C.black) end
|
||||
end
|
||||
|
||||
local function makePortraits()
|
||||
local sprite=Sprite(9*48,48,ColorMode.RGB); sprite.cels[1].image:clear(C.transparent)
|
||||
local img=sprite.cels[1].image
|
||||
drawPortrait(img,0,C.sky,"smile",false); drawPortrait(img,48,C.gray,"flat",true)
|
||||
local colors={C.honey,C.white,C.brown,C.orange,C.cream,C.sky,C.grass2}
|
||||
for i=0,6 do drawPortrait(img,(i+2)*48,colors[i+1],"smile",false) end
|
||||
save(sprite,"portraits.aseprite","portraits.png")
|
||||
end
|
||||
|
||||
makeTileset(); makeCharacters(); makePortraits()
|
||||
print("Generated original Aseprite assets in " .. assetDir)
|
||||
@@ -0,0 +1,129 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const root = resolve(import.meta.dirname, '..');
|
||||
const sourceDir = resolve(root, 'public/maps/source');
|
||||
await mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const maps = [
|
||||
['HouseScene', '小满小屋', 12, 8, 4, [
|
||||
['spawn', 96, 88, { id: 'default' }], ['door', 96, 112, { target: 'PlazaScene', spawn: 'house' }],
|
||||
['interact', 42, 34, { id: 'calendar', label: '床头日历' }], ['interact', 64, 42, { id: 'bed', label: '床' }],
|
||||
]],
|
||||
['PlazaScene', '村广场', 30, 20, 0, [
|
||||
['spawn', 240, 260, { id: 'default' }], ['spawn', 240, 280, { id: 'house' }], ['spawn', 32, 160, { id: 'bakery' }], ['spawn', 448, 160, { id: 'shop' }], ['spawn', 240, 32, { id: 'clinic' }], ['spawn', 32, 48, { id: 'lake' }], ['spawn', 448, 48, { id: 'field' }], ['spawn', 32, 280, { id: 'well' }], ['spawn', 448, 280, { id: 'mist' }],
|
||||
['door', 240, 304, { target: 'HouseScene', spawn: 'default' }], ['door', 0, 160, { target: 'BakeryScene', spawn: 'default' }], ['door', 464, 160, { target: 'ShopScene', spawn: 'default' }], ['door', 240, 0, { target: 'ClinicScene', spawn: 'default' }], ['door', 0, 48, { target: 'LakeScene', spawn: 'default' }], ['door', 464, 48, { target: 'FieldScene', spawn: 'default' }], ['door', 0, 280, { target: 'WellScene', spawn: 'default' }], ['door', 464, 280, { target: 'MistScene', spawn: 'default' }],
|
||||
['npc', 240, 132, { id: 'afu', label: '阿福' }], ['npc', 180, 162, { id: 'dabao', label: '大宝' }],
|
||||
['npc', 232, 76, { id: 'birds', label: '双子鸟' }], ['npc', 124, 210, { id: 'extra1', label: '村民' }], ['npc', 352, 224, { id: 'extra2', label: '村民' }],
|
||||
['interact', 240, 146, { id: 'board', label: '公告板' }], ['interact', 92, 148, { id: 'mail', label: '散落信件' }], ['interact', 200, 184, { id: 'flower1', label: '花坛' }], ['interact', 240, 200, { id: 'flower2', label: '花坛' }], ['interact', 280, 184, { id: 'flower3', label: '花坛' }],
|
||||
['interact', 160, 100, { id: 'ribbon1', label: '彩带架' }], ['interact', 224, 92, { id: 'ribbon2', label: '彩带架' }], ['interact', 288, 92, { id: 'ribbon3', label: '彩带架' }], ['interact', 352, 100, { id: 'ribbon4', label: '彩带架' }], ['interact', 240, 224, { id: 'festival', label: '庆典场地' }],
|
||||
]],
|
||||
['BakeryScene', '面包房', 15, 10, 3, [
|
||||
['spawn', 120, 128, { id: 'default' }], ['door', 120, 144, { target: 'PlazaScene', spawn: 'bakery' }], ['npc', 120, 72, { id: 'yuanyuan', label: '圆圆' }], ['interact', 48, 46, { id: 'recipe', label: '配方纸' }], ['interact', 184, 50, { id: 'oven', label: '烤炉' }],
|
||||
]],
|
||||
['ShopScene', '杂货店', 15, 10, 3, [
|
||||
['spawn', 120, 128, { id: 'default' }], ['door', 120, 144, { target: 'PlazaScene', spawn: 'shop' }], ['npc', 120, 72, { id: 'mimi', label: '咪咪' }], ['interact', 48, 52, { id: 'shelf', label: '货架' }],
|
||||
]],
|
||||
['ClinicScene', '诊所', 12, 8, 5, [
|
||||
['spawn', 96, 96, { id: 'default' }], ['door', 96, 112, { target: 'PlazaScene', spawn: 'clinic' }], ['npc', 96, 50, { id: 'doctor', label: '芙医生' }], ['interact', 48, 42, { id: 'form', label: '桌上表格' }],
|
||||
]],
|
||||
['LakeScene', '湖畔', 24, 16, 2, [
|
||||
['spawn', 368, 128, { id: 'default' }], ['door', 368, 128, { target: 'PlazaScene', spawn: 'lake' }], ['npc', 142, 132, { id: 'turtle', label: '龟爷爷' }], ['npc', 284, 68, { id: 'extra4', label: '村民' }], ['interact', 190, 148, { id: 'picnic', label: '野餐布' }], ['interact', 110, 68, { id: 'lake', label: '湖面' }],
|
||||
]],
|
||||
['FieldScene', '花田', 20, 14, 0, [
|
||||
['spawn', 16, 112, { id: 'default' }], ['door', 0, 112, { target: 'PlazaScene', spawn: 'field' }], ['npc', 244, 148, { id: 'extra3', label: '村民' }],
|
||||
['interact', 80, 72, { id: 'berry1', label: '莓果丛' }], ['interact', 128, 56, { id: 'berry2', label: '莓果丛' }], ['interact', 176, 88, { id: 'berry3', label: '莓果丛' }], ['interact', 224, 64, { id: 'berry4', label: '莓果丛' }], ['interact', 272, 96, { id: 'berry5', label: '莓果丛' }],
|
||||
]],
|
||||
['WellScene', '古井空地', 14, 10, 0, [
|
||||
['spawn', 208, 80, { id: 'default' }], ['door', 208, 80, { target: 'PlazaScene', spawn: 'well' }], ['npc', 46, 116, { id: 'extra5', label: '村民' }], ['interact', 104, 72, { id: 'well', label: '古井' }],
|
||||
]],
|
||||
['MistScene', '雾墙边界', 20, 8, 0, [
|
||||
['spawn', 16, 64, { id: 'default' }], ['door', 0, 64, { target: 'PlazaScene', spawn: 'mist' }], ['interact', 288, 64, { id: 'mist', label: '雾墙' }],
|
||||
]],
|
||||
['WhiteScene', '白色层', 30, 8, 5, [
|
||||
['spawn', 24, 64, { id: 'default' }], ['interact', 400, 64, { id: 'record', label: '病历' }],
|
||||
]],
|
||||
];
|
||||
|
||||
function createTileData(key, width, height, ground) {
|
||||
const data = Array(width * height).fill(ground + 1);
|
||||
const set = (x, y, gid) => { if (x >= 0 && y >= 0 && x < width && y < height) data[y * width + x] = gid; };
|
||||
const rect = (x, y, w, h, gid) => { for (let yy = y; yy < y + h; yy += 1) for (let xx = x; xx < x + w; xx += 1) set(xx, yy, gid); };
|
||||
const building = (x, y, w, h) => {
|
||||
rect(x, y, w, 2, 9); rect(x, y + 2, w, h - 2, 4);
|
||||
set(x + Math.floor(w / 2), y + h - 1, 10);
|
||||
for (let xx = x + 1; xx < x + w - 1; xx += 3) set(xx, y + 3, 15);
|
||||
};
|
||||
|
||||
if (key === 'PlazaScene') {
|
||||
rect(13, 0, 4, height, 2); rect(0, 8, width, 4, 2);
|
||||
building(2, 1, 7, 6); building(21, 1, 7, 6); building(2, 14, 7, 5); building(21, 14, 7, 5);
|
||||
rect(10, 2, 3, 3, 12); set(15, 8, 11);
|
||||
set(5, 9, 17);
|
||||
[[12, 11], [13, 12], [14, 11], [16, 11], [17, 12], [18, 11]].forEach(([x, y]) => set(x, y, 8));
|
||||
[[1, 3], [9, 6], [19, 5], [28, 3], [4, 12], [25, 12], [10, 16], [19, 16]].forEach(([x, y]) => set(x, y, 13));
|
||||
} else if (key === 'HouseScene') {
|
||||
rect(0, 0, width, height, 4); rect(0, 0, width, 1, 5); rect(0, height - 1, width, 1, 5);
|
||||
rect(2, 2, 3, 2, 5); set(8, 2, 11); rect(6, 5, 2, 1, 8); set(6, 7, 10);
|
||||
} else if (key === 'BakeryScene') {
|
||||
rect(0, 0, width, height, 4); rect(1, 1, width - 2, 2, 5); set(11, 2, 13); rect(2, 5, 11, 1, 5); set(7, 9, 10);
|
||||
} else if (key === 'ShopScene') {
|
||||
rect(0, 0, width, height, 4); for (let x = 1; x < width - 1; x += 3) rect(x, 1, 2, 4, 5); set(7, 9, 10);
|
||||
for (let y = 2; y <= 4; y += 1) for (const x of [2, 5, 8]) set(x, y, 7);
|
||||
} else if (key === 'ClinicScene') {
|
||||
rect(0, 0, width, height, 6); rect(0, 0, width, 1, 16); rect(2, 2, 5, 2, 5); set(6, 7, 10); set(9, 2, 15);
|
||||
} else if (key === 'LakeScene') {
|
||||
rect(0, 0, 10, height, 3); rect(10, 0, width - 10, height, 1); rect(10, 7, width - 10, 3, 2);
|
||||
rect(11, 9, 3, 2, 5); rect(14, 9, 3, 2, 8); for (let y = 2; y < height - 2; y += 4) set(20, y, 12);
|
||||
} else if (key === 'FieldScene') {
|
||||
rect(0, 6, width, 3, 2);
|
||||
for (let y = 1; y < height - 1; y += 3) for (let x = 2; x < width - 1; x += 3) set(x, y, (x + y) % 2 ? 7 : 8);
|
||||
} else if (key === 'WellScene') {
|
||||
rect(3, 2, 8, 6, 2); rect(5, 3, 4, 4, 14); set(13, 5, 10);
|
||||
} else if (key === 'MistScene') {
|
||||
rect(0, 3, width - 3, 3, 2); rect(width - 4, 0, 4, height, 15);
|
||||
} else if (key === 'WhiteScene') {
|
||||
rect(0, 0, width, height, 6); rect(0, 0, width, 1, 16); rect(0, height - 1, width, 1, 16); rect(19, 1, 1, height - 2, 16); set(25, 4, 5);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const collisionSpecs = {
|
||||
PlazaScene: [[32, 16, 112, 96], [336, 16, 112, 96], [32, 224, 112, 76], [336, 224, 112, 76], [160, 32, 48, 48], [232, 128, 32, 24]],
|
||||
BakeryScene: [[16, 16, 208, 32], [176, 32, 32, 30], [32, 80, 176, 12]],
|
||||
ShopScene: [[16, 16, 32, 64], [64, 16, 32, 64], [112, 16, 32, 64], [160, 16, 32, 64], [208, 16, 16, 64]],
|
||||
ClinicScene: [[32, 32, 80, 32]],
|
||||
LakeScene: [[0, 0, 150, 256]],
|
||||
WellScene: [[80, 48, 64, 64]],
|
||||
WhiteScene: [[304, 16, 16, 96]],
|
||||
};
|
||||
|
||||
const propertyXml = (properties) => Object.entries(properties).map(([name, value]) => ` <property name="${name}" value="${String(value).replaceAll('&', '&').replaceAll('"', '"')}"/>`).join('\n');
|
||||
|
||||
for (const [key, displayName, width, height, ground, objects] of maps) {
|
||||
const data = createTileData(key, width, height, ground);
|
||||
const borders = [
|
||||
`<object id="900" x="0" y="0" width="${width * 16}" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>`,
|
||||
`<object id="901" x="0" y="${height * 16 - 4}" width="${width * 16}" height="4"><properties><property name="collides" type="bool" value="true"/></properties></object>`,
|
||||
`<object id="902" x="0" y="0" width="4" height="${height * 16}"><properties><property name="collides" type="bool" value="true"/></properties></object>`,
|
||||
`<object id="903" x="${width * 16 - 4}" y="0" width="4" height="${height * 16}"><properties><property name="collides" type="bool" value="true"/></properties></object>`,
|
||||
];
|
||||
(collisionSpecs[key] ?? []).forEach(([x, y, w, h], index) => borders.push(`<object id="${910 + index}" x="${x}" y="${y}" width="${w}" height="${h}"><properties><property name="collides" type="bool" value="true"/></properties></object>`));
|
||||
const objectXml = objects.map(([type, x, y, properties], index) => ` <object id="${index + 1}" name="${properties.label ?? properties.id}" type="${type}" x="${x}" y="${y}" width="16" height="16">\n <properties>\n${propertyXml(properties)}\n </properties>\n </object>`).join('\n');
|
||||
const tmx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${width}" height="${height}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1000">
|
||||
<properties><property name="sceneKey" value="${key}"/><property name="displayName" value="${displayName}"/></properties>
|
||||
<tileset firstgid="1" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16"><image source="../../assets/tileset.png" width="256" height="64"/></tileset>
|
||||
<layer id="1" name="ground" width="${width}" height="${height}"><data encoding="csv">${data.join(',')}</data></layer>
|
||||
<objectgroup id="2" name="obstacles">${borders.join('\n')}</objectgroup>
|
||||
<objectgroup id="3" name="objects">\n${objectXml}\n </objectgroup>
|
||||
</map>`;
|
||||
await writeFile(resolve(sourceDir, `${key}.tmx`), tmx);
|
||||
}
|
||||
|
||||
await writeFile(resolve(sourceDir, 'tileset.tsx'), `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.2" name="honey-village" tilewidth="16" tileheight="16" tilecount="64" columns="16">
|
||||
<image source="../../assets/tileset.png" width="256" height="64"/>
|
||||
</tileset>`);
|
||||
|
||||
console.log(`Generated ${maps.length} original Tiled source maps in ${sourceDir}`);
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": ["src", "tests", "vite.config.ts", "playwright.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
server: { host: '127.0.0.1', port: 4173 },
|
||||
preview: { host: '127.0.0.1', port: 4173 },
|
||||
});
|
||||
Reference in New Issue
Block a user