Merge branch 'main' into feature/player-controller-v01
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
# 策划 Agent Memory Index
|
# 策划 Agent Memory Index
|
||||||
|
|
||||||
- [千禧年怀旧文化灵感](nostalgia-culture-inspirations.md) — 电视剧/广播体操/广场舞/电话热线游戏的灵感记录与共识
|
- [千禧年怀旧文化灵感](nostalgia-culture-inspirations.md) — 电视剧/广播体操/广场舞/电话热线/零食/小孩游戏/乡下活动灵感汇总
|
||||||
|
- [建筑与区域规划](building-area-plan.md) — 城市主场景完整建筑清单、可进入室内、新增设施共识
|
||||||
- [v0.1执行案状态](v01-execution-status.md) — 三份执行案已通过二次质量门审查,流水线可推进至④程序设计
|
- [v0.1执行案状态](v01-execution-status.md) — 三份执行案已通过二次质量门审查,流水线可推进至④程序设计
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: building-area-plan
|
||||||
|
description: 城市主场景建筑与区域规划共识,包含完整建筑清单、可进入室内、新增设施
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
## 城市主场景建筑清单(2026-06-10 确认)
|
||||||
|
|
||||||
|
来源:头脑风暴-2026-06-10-建筑与区域规划.md
|
||||||
|
|
||||||
|
### 关键变更(vs 原场景设计)
|
||||||
|
|
||||||
|
- 小伙伴家:原"全部不可进入" → 4家可进入(简化室内)
|
||||||
|
- 包机房:合并进街机店后屋
|
||||||
|
- 废品回收:固定摊位 + 流动收废品车
|
||||||
|
- 秘密基地:多处散布(工地围挡后、平房区死角等)
|
||||||
|
- 新增店铺:粮油店、服装店/裁缝铺、鞋店(主街东侧)、储蓄所(西侧)
|
||||||
|
- 新增设施:棋牌室(纯氛围)、公交站牌(纯装饰)、公共厕所、自行车棚、信报箱
|
||||||
|
|
||||||
|
### 主街布局(东西排列)
|
||||||
|
|
||||||
|
东→:学校操场 | 小卖部 | 粮油店 | 书店 | 服装店 | 鞋店 · 公交站 · 棋牌室 | 储蓄所 | 录像出租 | 网吧 | 街机店 ←西
|
||||||
|
|
||||||
|
### 可进入室内(~12个)
|
||||||
|
|
||||||
|
完整:主角家、合并小卖部、书店、街机店、网吧、录像出租点
|
||||||
|
简化:死党A家(小区内)、死党B家(平房区)、老板儿子家(小卖部后屋)、女班长家(主角家同楼)
|
||||||
|
|
||||||
|
### 不做的元素
|
||||||
|
|
||||||
|
菜地(留乡下)、装修、游泳(河边不下水)
|
||||||
|
|
||||||
|
### How to apply:
|
||||||
|
- 场景与世界观.md 已更新为最新清单
|
||||||
|
- 新增店铺和设施在后续版本逐步添加(见版本映射)
|
||||||
|
- 概念草案不变(冻结),变更记录在系统设计案中
|
||||||
@@ -54,3 +54,34 @@ metadata:
|
|||||||
- 广播体操和广场舞适合作为场景氛围事件
|
- 广播体操和广场舞适合作为场景氛围事件
|
||||||
- 迈皋桥考据直接用于场景设计的"半城半乡"氛围塑造
|
- 迈皋桥考据直接用于场景设计的"半城半乡"氛围塑造
|
||||||
- 华电子弟学校、工厂宿舍区、菜地、老街都是高还原度的场景元素
|
- 华电子弟学校、工厂宿舍区、菜地、老街都是高还原度的场景元素
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-10 灵感补充
|
||||||
|
|
||||||
|
来源:头脑风暴-2026-06-10-建筑与区域规划.md
|
||||||
|
|
||||||
|
### 重点新发现(⭐⭐⭐)
|
||||||
|
|
||||||
|
1. **钻狗洞的秘密基地** — 多处散布(工地围挡后、平房区死角),高探索叙事价值
|
||||||
|
2. **叠纸飞机** — 纸张收集+折叠选择+飞行结果三层玩法
|
||||||
|
3. **不知道名字的小哥哥** — 一次性邂逅NPC,极高情感潜力微型叙事线
|
||||||
|
4. **体力系统+家庭动态事件** — 吃饭补充体力→不吃炒饭→换速冻水饺→给饭钱
|
||||||
|
5. **爷爷奶奶家亲戚小孩** — 乡下NPC角色体系(之前乡下NPC完全空白)
|
||||||
|
|
||||||
|
### 新零食/小吃(具体品牌品种)
|
||||||
|
|
||||||
|
大大泡泡糖、秀逗(超酸糖)、无花果丝、老鼠屎(盐津葡萄)、跳跳糖、烤鱿鱼、烤里脊
|
||||||
|
|
||||||
|
### 新小孩游戏
|
||||||
|
|
||||||
|
双人拍手/互推游戏、猜在哪个手里、跑步比赛、叠纸飞机
|
||||||
|
|
||||||
|
### 新人物/社交
|
||||||
|
|
||||||
|
- 北方来南方的同学(与"转学生"角色互补)
|
||||||
|
- 等着去别人家(串门文化)
|
||||||
|
|
||||||
|
### 新乡下活动
|
||||||
|
|
||||||
|
钓鱼(鱼竿)、游泳、挤牛奶、喂鸡掏鸡蛋、菜地浇水、打水漂
|
||||||
|
|||||||
+215
-24
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 完整游戏世界的空间、时代、氛围设计。
|
> 完整游戏世界的空间、时代、氛围设计。
|
||||||
> 覆盖城市主场景 + 乡下场景,以及场景与各系统的关联。
|
> 覆盖城市主场景 + 乡下场景,以及场景与各系统的关联。
|
||||||
> 最后更新:2026-06-01
|
> 最后更新:2026-06-10
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| 尺度 | ~80m × 60m | ~150m × 120m |
|
| 尺度 | ~80m × 60m | ~150m × 120m |
|
||||||
| 区域数 | 6 个核心 + 1 背景 | 主街 + 生活区 + 边缘三大板块 |
|
| 区域数 | 6 个核心 + 1 背景 | 主街 + 生活区 + 边缘三大板块 |
|
||||||
| 可进入室内 | 0-1 个 | 5-6 个(家 + 店铺 + 网吧) |
|
| 可进入室内 | 0-1 个 | ~12 个(家 + 6 店铺 + 4 小伙伴家 + 网吧) |
|
||||||
| 店铺 | 仅小卖部 | 小卖部、书店、街机店、网吧、录像出租点 |
|
| 店铺 | 仅小卖部 | 小卖部、书店、粮油店、服装店、鞋店、街机店、网吧、录像出租点、储蓄所 |
|
||||||
| 居住形态 | 平房带院子 | 事业单位楼房 + 小区 + 平房区 |
|
| 居住形态 | 平房带院子 | 事业单位楼房 + 小区 + 平房区 |
|
||||||
|
|
||||||
### 2.3 总体布局
|
### 2.3 总体布局
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
|
|
||||||
## 三、商业主街
|
## 三、商业主街
|
||||||
|
|
||||||
主街约 70m 长、5-6m 宽的水泥路,是整个场景最繁华的区域。以中间为界,**西侧是商业娱乐区**(街机、网吧、录像出租),**东侧是传统生活区**(小卖部、书店、学校)。街边散布着各类摊贩。
|
主街约 70m 长、5-6m 宽的水泥路,是整个场景最繁华的区域。以中间为界,**西侧是商业娱乐区**(街机、网吧、录像出租、储蓄所、棋牌室),**东侧是传统生活区**(小卖部、书店、粮油店、服装店、鞋店、学校)。街边散布着各类摊贩和公交站牌。
|
||||||
|
|
||||||
### 3.1 东侧(传统生活)
|
### 3.1 东侧(传统生活)
|
||||||
|
|
||||||
@@ -201,7 +201,65 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 3.1.3 学校
|
#### 3.1.3 粮油店
|
||||||
|
|
||||||
|
**位置**:主街东部,书店旁边
|
||||||
|
**面积**:小型(~3m × 3m)
|
||||||
|
**可进入**:简化(门口交互为主)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 窄门面,门口摆着几袋大米和油桶
|
||||||
|
- 招牌手写"粮油"二字,简单直接
|
||||||
|
- 门口有电子秤
|
||||||
|
|
||||||
|
**时代物件**:散装大米、5L 塑料油桶、酱油醋大缸(散装打)、挂面
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 氛围营造(城郊生活必需品店铺)
|
||||||
|
- 可能作为零花钱系统的消费点
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.4 服装店/裁缝铺
|
||||||
|
|
||||||
|
**位置**:主街东部,粮油店旁边
|
||||||
|
**面积**:小型(~3m × 3m)
|
||||||
|
**可进入**:简化(门口交互为主)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 门口挂着几件成衣样品
|
||||||
|
- 招牌写着"XX 服装"或"裁缝"
|
||||||
|
- 老板娘坐在门口
|
||||||
|
|
||||||
|
**时代物件**:成衣、布匹、缝纫机、量衣尺
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 氛围营造(裁缝铺是城郊标配)
|
||||||
|
- 简化对话交互
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.5 鞋店
|
||||||
|
|
||||||
|
**位置**:主街东部,服装店旁边
|
||||||
|
**面积**:小型(~3m × 3m)
|
||||||
|
**可进入**:简化(门口交互为主)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 门口摆着几排鞋(运动鞋为主)
|
||||||
|
- 招牌写着"XX 鞋业"
|
||||||
|
- 墙上挂着运动鞋海报(安踏/李宁风格)
|
||||||
|
|
||||||
|
**时代物件**:运动鞋(安踏/李宁早期款式)、塑料凉鞋、布鞋
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 氛围营造(运动鞋是小孩的梦想)
|
||||||
|
- 简化对话交互
|
||||||
|
- 可能与"换装"概念联动
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.6 学校
|
||||||
|
|
||||||
**位置**:主街最东端
|
**位置**:主街最东端
|
||||||
**朝向**:正门面向主街,侧门(小铁门)面向生活区巷子
|
**朝向**:正门面向主街,侧门(小铁门)面向生活区巷子
|
||||||
@@ -279,6 +337,7 @@
|
|||||||
- 经济系统(柜台换币消费)
|
- 经济系统(柜台换币消费)
|
||||||
- 小霸王常驻于此(格斗高手)
|
- 小霸王常驻于此(格斗高手)
|
||||||
- 西侧的"磁石"——孩子们被声音和灯光吸引过来
|
- 西侧的"磁石"——孩子们被声音和灯光吸引过来
|
||||||
|
- **包机房后屋**:门帘隔开的内部空间,提供主机游戏(PS/SS 风格),比街机更沉浸但更贵
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -362,6 +421,58 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### 3.2.4 储蓄所
|
||||||
|
|
||||||
|
**位置**:主街西部,录像出租点旁边
|
||||||
|
**面积**:小型(~4m × 3m)
|
||||||
|
**可进入**:简化(柜台交互)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 正式的门面,比周围店铺更"正经"
|
||||||
|
- 招牌写着"XX 储蓄所"或"XX 信用社"
|
||||||
|
- 门上有铁栅栏门(下班后拉上)
|
||||||
|
|
||||||
|
**时代物件**:存折、手写利率牌、算盘、老式点钞机
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 零花钱系统(存取零花钱?待定)
|
||||||
|
- 时代氛围(储蓄所是那个年代的金融地标)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.2.5 棋牌室
|
||||||
|
|
||||||
|
**位置**:主街西部
|
||||||
|
**面积**:小型
|
||||||
|
**可进入**:❌ 不可进入(大人的场所)
|
||||||
|
|
||||||
|
**外观**:
|
||||||
|
- 半开的门,里面烟雾缭绕
|
||||||
|
- 门口可能坐着抽烟的大人
|
||||||
|
- 招牌写着"棋牌"或"茶室"
|
||||||
|
- 能听到麻将牌碰撞的声音
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 纯氛围营造(大人的世界,小孩好奇但不能进)
|
||||||
|
- 声音景观(麻将声作为主街西侧的背景音)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.2.6 主街设施
|
||||||
|
|
||||||
|
**公交站牌**:
|
||||||
|
- 主街中段的简易公交站牌(铁杆+铁皮站牌)
|
||||||
|
- 公交车会经过但不可乘坐
|
||||||
|
- 纯装饰,增强城郊出行氛围
|
||||||
|
|
||||||
|
**废品回收点**:
|
||||||
|
- 主街路边固定摊位
|
||||||
|
- 一把秤 + 几个编织袋 + 地上分类的废品
|
||||||
|
- 老板娘坐镇,可交互卖废品
|
||||||
|
- 与流动收废品车互补(固定点方便,流动车价格可能不同)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 3.3 街边摊贩
|
### 3.3 街边摊贩
|
||||||
|
|
||||||
**分布**:散布在主街各处 + 生活区巷子中
|
**分布**:散布在主街各处 + 生活区巷子中
|
||||||
@@ -372,6 +483,8 @@
|
|||||||
|----------|---------|---------|------|
|
|----------|---------|---------|------|
|
||||||
| 冰棍推车 | 主街阴凉处、学校门口 | 泡沫箱+棉被保温,绿豆冰/红豆冰/白糖冰棍 | 夏天标配 |
|
| 冰棍推车 | 主街阴凉处、学校门口 | 泡沫箱+棉被保温,绿豆冰/红豆冰/白糖冰棍 | 夏天标配 |
|
||||||
| 烤肠机 | 小卖部门口、网吧门口 | 电烤机,1-2 元一根 | 简单经济触点 |
|
| 烤肠机 | 小卖部门口、网吧门口 | 电烤机,1-2 元一根 | 简单经济触点 |
|
||||||
|
| 烤鱿鱼 | 主街散布 | 铁板,酱料 | 城郊特色小吃 |
|
||||||
|
| 烤里脊 | 主街散布 | 铁板串烤 | 城郊特色小吃 |
|
||||||
| 糖葫芦 | 随机出现 | 插在草把子上,山楂/草莓/橘子 | 随机事件 |
|
| 糖葫芦 | 随机出现 | 插在草把子上,山楂/草莓/橘子 | 随机事件 |
|
||||||
| 爆米花 | 偶尔出现在主街/空地附近 | 老式黑铁葫芦爆米花机,"嘭"一声 | 随机事件,声音记忆 |
|
| 爆米花 | 偶尔出现在主街/空地附近 | 老式黑铁葫芦爆米花机,"嘭"一声 | 随机事件,声音记忆 |
|
||||||
| 棉花糖 | 偶尔出现在空地/学校附近 | 手摇式棉花糖机 | 随机事件 |
|
| 棉花糖 | 偶尔出现在空地/学校附近 | 手摇式棉花糖机 | 随机事件 |
|
||||||
@@ -396,15 +509,28 @@
|
|||||||
|---|----------|------|--------|---------|
|
|---|----------|------|--------|---------|
|
||||||
| 1 | 合并小卖部 | 8m×5m | ✅ | 经济核心、收集入口、街坊聚集 |
|
| 1 | 合并小卖部 | 8m×5m | ✅ | 经济核心、收集入口、街坊聚集 |
|
||||||
| 2 | 书店 | 5m×4m | ✅ | 漫画/书籍、叙事触点、时代氛围 |
|
| 2 | 书店 | 5m×4m | ✅ | 漫画/书籍、叙事触点、时代氛围 |
|
||||||
| 3 | 学校(仅操场) | 25m×18m | — | 肢体活动、剧情场景 |
|
| 3 | 粮油店 | ~3m×3m | 简化 | 氛围营造、城郊生活必需品 |
|
||||||
|
| 4 | 服装店/裁缝铺 | ~3m×3m | 简化 | 氛围营造、简化对话 |
|
||||||
|
| 5 | 鞋店 | ~3m×3m | 简化 | 氛围营造、运动鞋梦想 |
|
||||||
|
| 6 | 学校(仅操场) | 25m×18m | — | 肢体活动、剧情场景 |
|
||||||
|
|
||||||
**西侧(商业娱乐)**:
|
**西侧(商业娱乐)**:
|
||||||
|
|
||||||
| # | 店铺/设施 | 面积 | 可进入 | 核心功能 |
|
| # | 店铺/设施 | 面积 | 可进入 | 核心功能 |
|
||||||
|---|----------|------|--------|---------|
|
|---|----------|------|--------|---------|
|
||||||
| 4 | 街机店 | 8m×6m | ✅ | 街机小游戏、社交核心 |
|
| 7 | 街机店(含包机房后屋) | 8m×6m | ✅ | 街机小游戏、社交核心、包机房 |
|
||||||
| 5 | 网吧 | 6m×5m | ✅ | PC 小游戏、社交、时代氛围 |
|
| 8 | 网吧 | 6m×5m | ✅ | PC 小游戏、社交、时代氛围 |
|
||||||
| 6 | 录像出租点 | 5m×4m | ✅ | 录像带/磁带、叙事触点、收集 |
|
| 9 | 录像出租点 | 5m×4m | ✅ | 录像带/磁带、叙事触点、收集 |
|
||||||
|
| 10 | 储蓄所 | ~4m×3m | 简化 | 存取零花钱?时代氛围 |
|
||||||
|
| 11 | 棋牌室 | 小型 | ❌ | 纯氛围(大人的场所) |
|
||||||
|
|
||||||
|
**主街设施**:
|
||||||
|
|
||||||
|
| # | 设施 | 性质 |
|
||||||
|
|---|------|------|
|
||||||
|
| 12 | 公交站牌 | 纯装饰 |
|
||||||
|
| 13 | 废品回收点 | 路边固定摊位,可交互卖废品 |
|
||||||
|
| 14 | 街边摊贩 × N | 散布式(烤鱿鱼、烤里脊、冰棍推车、烤肠等) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -430,9 +556,15 @@
|
|||||||
- 楼道:水泥楼梯,声控灯,墙上贴着小广告和水电费通知
|
- 楼道:水泥楼梯,声控灯,墙上贴着小广告和水电费通知
|
||||||
|
|
||||||
**死党 A 的家**:
|
**死党 A 的家**:
|
||||||
- 在某栋楼的某层(不可进入,但外观有标识——比如阳台上挂着四驱车赛道模型)
|
- 在某栋楼的某层(简化室内,可进入)
|
||||||
|
- 外观有标识——比如阳台上挂着四驱车赛道模型
|
||||||
|
- 室内简化:小空间 + 对话为主,展示家境好的氛围(大电视、四驱车跑道、电脑)
|
||||||
- 从楼下能看到他家阳台上摆着的东西
|
- 从楼下能看到他家阳台上摆着的东西
|
||||||
|
|
||||||
|
**小区设施**:
|
||||||
|
- **自行车棚**:水泥柱 + 石棉瓦顶,停着几辆老式自行车(永久牌/凤凰牌)
|
||||||
|
- 信报箱区(铁皮信报箱,嵌在楼道口墙上)
|
||||||
|
|
||||||
**系统关联**:
|
**系统关联**:
|
||||||
- 四驱车系统(死党 A 在小区空地展示/比拼四驱车)
|
- 四驱车系统(死党 A 在小区空地展示/比拼四驱车)
|
||||||
- 社交场所(小区空地是另一个孩子聚集点)
|
- 社交场所(小区空地是另一个孩子聚集点)
|
||||||
@@ -498,6 +630,15 @@
|
|||||||
- 墙上贴着水电费通知、小广告
|
- 墙上贴着水电费通知、小广告
|
||||||
- 楼道里偶尔碰到邻居(背景 NPC)
|
- 楼道里偶尔碰到邻居(背景 NPC)
|
||||||
|
|
||||||
|
**女班长家**:
|
||||||
|
- 同栋楼不同层(简化室内,可进入)
|
||||||
|
- 室内简化:小空间 + 对话为主
|
||||||
|
- 女孩的房间视角,与主角家形成对照
|
||||||
|
|
||||||
|
**楼下设施**:
|
||||||
|
- **信报箱**:铁皮信报箱嵌在楼道口,可能收到通知/信件
|
||||||
|
- **自行车棚**:楼旁小型车棚(如有独立空间)
|
||||||
|
|
||||||
**系统关联**:
|
**系统关联**:
|
||||||
- FC 家用机小游戏(坐在客厅电视前玩)
|
- FC 家用机小游戏(坐在客厅电视前玩)
|
||||||
- 暑假作业叙事线(书桌上的作业)
|
- 暑假作业叙事线(书桌上的作业)
|
||||||
@@ -535,11 +676,16 @@
|
|||||||
| 修车铺 | 修自行车的,门口散落零件 | 扳手、打气筒、旧轮胎 |
|
| 修车铺 | 修自行车的,门口散落零件 | 扳手、打气筒、旧轮胎 |
|
||||||
| 小吃摊 | 下午出摊,卖凉皮、凉面 | 铁皮推车、调料瓶一字排开 |
|
| 小吃摊 | 下午出摊,卖凉皮、凉面 | 铁皮推车、调料瓶一字排开 |
|
||||||
|
|
||||||
这些小店大多不可进入(仅在门口交互),主要是氛围营造和日常事件触发。
|
这些小店大多简化交互(门口对话为主),主要是氛围营造和日常事件触发。其中早餐店可交互买东西。
|
||||||
|
|
||||||
|
**平房区设施**:
|
||||||
|
- **公共厕所**:平房区内的旱厕,城郊生活的标志性设施
|
||||||
|
- **公共水龙头**:巷子角落
|
||||||
|
|
||||||
**死党 B 的家**:
|
**死党 B 的家**:
|
||||||
- 平房区某一间(外观有标识——门口可能放着弹珠罐子或自制玩具)
|
- 平房区某一间(简化室内,可进入)
|
||||||
- 不可进入,但门口有时可以和死党 B 互动
|
- 外观有标识——门口可能放着弹珠罐子或自制玩具
|
||||||
|
- 室内简化:小空间 + 对话为主,展示温馨但简朴的氛围
|
||||||
|
|
||||||
**系统关联**:
|
**系统关联**:
|
||||||
- 对比叙事(小区 vs 平房区的物质差异)
|
- 对比叙事(小区 vs 平房区的物质差异)
|
||||||
@@ -574,11 +720,16 @@
|
|||||||
|
|
||||||
| # | 区域 | 位置 | 类型 | 核心功能 |
|
| # | 区域 | 位置 | 类型 | 核心功能 |
|
||||||
|---|------|------|------|---------|
|
|---|------|------|------|---------|
|
||||||
| 1 | 小区 | 靠东侧 | 正经住宅(3-4 栋 6 层楼 + 中庭) | 死党 A 的家、四驱车场地、对比叙事 |
|
| 1 | 小区 | 靠东侧 | 正经住宅(3-4 栋 6 层楼 + 中庭) | 死党 A 的家(简化室内)、四驱车场地、对比叙事 |
|
||||||
| 2 | 主角家 | 中部 | 事业单位家属楼(独栋 6 层) | 起点/终点、FC 游戏、叙事 |
|
| 2 | 主角家 | 中部 | 事业单位家属楼(独栋 6 层) | 起点/终点、FC 游戏、叙事 |
|
||||||
| 3 | 平房区 | 靠西侧 | 自建平房群 + 生活小店 | 死党 B 的家、弹珠场地、对比叙事 |
|
| 2b | 女班长家 | 同主角家楼 | 同栋不同层(简化室内) | 女班长家、社交 |
|
||||||
|
| 3 | 平房区 | 靠西侧 | 自建平房群 + 生活小店 | 死党 B 的家(简化室内)、弹珠场地、对比叙事 |
|
||||||
| 4 | 巷子通道 | 贯穿 | 2-3 个巷子口 + 内部连接路 | 穿行、日常事件、氛围 |
|
| 4 | 巷子通道 | 贯穿 | 2-3 个巷子口 + 内部连接路 | 穿行、日常事件、氛围 |
|
||||||
|
|
||||||
|
**小区设施**:自行车棚、信报箱
|
||||||
|
**平房区设施**:公共厕所、公共水龙头
|
||||||
|
**平房区小店**:早餐店(可交互)、理发店/修车铺/缝纫店(背景+对话)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、边缘区域
|
## 五、边缘区域
|
||||||
@@ -644,6 +795,24 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 5.3 秘密基地(隐藏区域)
|
||||||
|
|
||||||
|
**设计理念**:散布在地图各处的小型隐藏空间,每个都有独特的"钻进去"方式。代表童年探索的乐趣——发现只有小孩才知道的秘密角落。
|
||||||
|
|
||||||
|
| # | 名称 | 位置 | 进入方式 | 氛围 |
|
||||||
|
|---|------|------|---------|------|
|
||||||
|
| H1 | 秘密基地 A | 工地蓝色围挡后面 | 围挡底部有个洞,蹲下钻进去 | 废弃建材、涂鸦、偷藏的零食 |
|
||||||
|
| H2 | 秘密基地 B | 平房区深处的死角 | 穿过一条很窄的巷子 | 老式家具、墙角涂鸦、安静 |
|
||||||
|
| H3 | 秘密基地 C | 待定 | 待定 | 待定 |
|
||||||
|
|
||||||
|
**系统关联**:
|
||||||
|
- 探索奖励(发现秘密基地本身就是一个成就)
|
||||||
|
- 收集品散落(秘密基地里有独特收集品)
|
||||||
|
- 叙事触点(秘密基地里发现前人留下的东西、和朋友在这里约定)
|
||||||
|
- 氛围营造("这是我们的秘密"的专属感)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 六、乡下场景(奶奶家)
|
## 六、乡下场景(奶奶家)
|
||||||
|
|
||||||
### 6.1 定位与尺度
|
### 6.1 定位与尺度
|
||||||
@@ -785,20 +954,29 @@
|
|||||||
```
|
```
|
||||||
┌──────────────────────────────────┐
|
┌──────────────────────────────────┐
|
||||||
│ 大型空地/工地 河边/河堤 │
|
│ 大型空地/工地 河边/河堤 │
|
||||||
|
│ [秘密基地A] │
|
||||||
└──────────┬───────────┬───────────┘
|
└──────────┬───────────┬───────────┘
|
||||||
│ │
|
│ │
|
||||||
┌──────────┴───────────┴───────────┐
|
┌──────────┴───────────┴───────────┐
|
||||||
│ 生活区巷子 │
|
│ 生活区巷子 │
|
||||||
│ [平房区] ── [主角家] ── [小区] │
|
│ [平房区] ── [主角家+女班长家] ── [小区] │
|
||||||
|
│ [秘密基地B] │
|
||||||
|
│ [早餐店·理发店·修车铺·缝纫店] │
|
||||||
|
│ [公共厕所·自行车棚·信报箱] │
|
||||||
└──┬──────────┬──────────┬─────────┘
|
└──┬──────────┬──────────┬─────────┘
|
||||||
│巷子口① │巷子口② │巷子口③
|
│巷子口① │巷子口② │巷子口③
|
||||||
┌──┴──────────┴──────────┴──────────┐
|
┌──┴──────────┴──────────┴──────────┐
|
||||||
│ 商业主街 │
|
│ 商业主街 │
|
||||||
│ │
|
│ │
|
||||||
│ 西侧(娱乐) 东侧(生活) │
|
│ 西侧(娱乐) 东侧(生活) │
|
||||||
│ [街机店] [小卖部] │
|
│ [棋牌室] [小卖部] │
|
||||||
│ [网吧] [书店] │
|
│ [储蓄所] [粮油店] │
|
||||||
│ [录像出租] [学校] │
|
│ [录像出租] [书店] │
|
||||||
|
│ [网吧] [服装店] │
|
||||||
|
│ [街机店] [鞋店] │
|
||||||
|
│ [学校] │
|
||||||
|
│ │
|
||||||
|
│ [公交站牌] [废品回收点] [摊贩×N] │
|
||||||
└────────────────────────────────────┘
|
└────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -858,7 +1036,7 @@
|
|||||||
| **女班长** | 学校操场(跳绳/跳皮筋) | 主街、小卖部门口 | 上午活跃 |
|
| **女班长** | 学校操场(跳绳/跳皮筋) | 主街、小卖部门口 | 上午活跃 |
|
||||||
| **转学生** | 小卖部门口(初来时) | 逐渐扩展到空地、街机店 | 中后期活动范围扩大 |
|
| **转学生** | 小卖部门口(初来时) | 逐渐扩展到空地、街机店 | 中后期活动范围扩大 |
|
||||||
| **残障儿童** | 平房区巷子口(家附近) | 小卖部门口(坐着看大家玩) | 下午出现 |
|
| **残障儿童** | 平房区巷子口(家附近) | 小卖部门口(坐着看大家玩) | 下午出现 |
|
||||||
| **小卖部老板儿子** | 合并小卖部 | 街机店(偷溜去玩) | 上午在店里、下午偷溜 |
|
| **小卖部老板儿子** | 合并小卖部(后屋是家) | 街机店(偷溜去玩) | 上午在店里、下午偷溜 |
|
||||||
| **小霸王** | 街机店(格斗高手) | 空地(找人挑衅) | 下午到傍晚 |
|
| **小霸王** | 街机店(格斗高手) | 空地(找人挑衅) | 下午到傍晚 |
|
||||||
|
|
||||||
### 8.2 设计原则
|
### 8.2 设计原则
|
||||||
@@ -994,10 +1172,17 @@
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 合并小卖部 | 主街东侧 | 买零食、买文具、买四驱车配件、开卡片 |
|
| 合并小卖部 | 主街东侧 | 买零食、买文具、买四驱车配件、开卡片 |
|
||||||
| 书店 | 主街东侧 | 租借漫画/书籍 |
|
| 书店 | 主街东侧 | 租借漫画/书籍 |
|
||||||
| 街机店 | 主街西侧 | 柜台换币、投币游戏 |
|
| 粮油店 | 主街东侧 | 氛围 + 可能消费 |
|
||||||
|
| 服装店/裁缝铺 | 主街东侧 | 氛围 + 简化对话 |
|
||||||
|
| 鞋店 | 主街东侧 | 氛围 + 简化对话 |
|
||||||
|
| 街机店 | 主街西侧 | 柜台换币、投币游戏、包机房 |
|
||||||
| 网吧 | 主街西侧 | 上网收费 |
|
| 网吧 | 主街西侧 | 上网收费 |
|
||||||
| 录像出租点 | 主街西侧 | 租借录像带/磁带 |
|
| 录像出租点 | 主街西侧 | 租借录像带/磁带 |
|
||||||
|
| 储蓄所 | 主街西侧 | 存取零花钱?待定 |
|
||||||
|
| 废品回收点 | 主街路边 | 卖废品换钱 |
|
||||||
|
| 早餐店 | 平房区 | 买早点(包子油条豆浆) |
|
||||||
| 街边摊贩 | 主街 + 巷子 | 买冰棍、买小吃(比店铺便宜) |
|
| 街边摊贩 | 主街 + 巷子 | 买冰棍、买小吃(比店铺便宜) |
|
||||||
|
| 流动收废品车 | 巷子巡游 | 卖废品换钱(流动价格) |
|
||||||
| 村口小卖部 | 乡下 | 乡下唯一消费点 |
|
| 村口小卖部 | 乡下 | 乡下唯一消费点 |
|
||||||
|
|
||||||
### 10.4 叙事系统 × 场景触发
|
### 10.4 叙事系统 × 场景触发
|
||||||
@@ -1086,9 +1271,9 @@
|
|||||||
| **v0.2** | NPC 放置 + 弹珠场地标识 + NPC 常驻点标识 |
|
| **v0.2** | NPC 放置 + 弹珠场地标识 + NPC 常驻点标识 |
|
||||||
| **v0.3** | 时段光影 + 天气初步 + 学校操场 |
|
| **v0.3** | 时段光影 + 天气初步 + 学校操场 |
|
||||||
| **v0.4** | 街机店室内 + 录像出租点室内 + 叙事触发点布设 + 小区外观 |
|
| **v0.4** | 街机店室内 + 录像出租点室内 + 叙事触发点布设 + 小区外观 |
|
||||||
| **v0.5** | 网吧室内 + 书店室内 + 平房区 + 生活小店 + 工地围挡 + 摊贩体系 |
|
| **v0.5** | 网吧室内 + 书店室内 + 粮油店/服装店/鞋店 + 储蓄所 + 平房区 + 生活小店 + 工地围挡 + 摊贩体系 + 废品回收点 |
|
||||||
| **v0.6** | **乡下场景全部**(奶奶家、小溪、田间路、村口小卖部) |
|
| **v0.6** | **乡下场景全部**(奶奶家、小溪、田间路、村口小卖部) |
|
||||||
| **v0.7** | 全场景整合 + 城乡切换 + 细节打磨 |
|
| **v0.7** | 全场景整合 + 城乡切换 + 4 家小伙伴家(简化室内)+ 秘密基地 + 棋牌室 + 公交站牌 + 细节打磨 |
|
||||||
| **v1.0** | 氛围精修(声音景观、天气效果、光影优化) |
|
| **v1.0** | 氛围精修(声音景观、天气效果、光影优化) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1100,7 +1285,13 @@
|
|||||||
- [ ] 乡下阶段的 NPC 具体设计(村里有几个人?什么性格?)
|
- [ ] 乡下阶段的 NPC 具体设计(村里有几个人?什么性格?)
|
||||||
- [ ] 天气系统的详细设计(v0.3 前确定范围)
|
- [ ] 天气系统的详细设计(v0.3 前确定范围)
|
||||||
- [ ] 室内场景的光照方案(自然光 vs 灯光)
|
- [ ] 室内场景的光照方案(自然光 vs 灯光)
|
||||||
- [ ] 平房区生活小店哪些可以进入、哪些仅门口交互
|
|
||||||
- [ ] 网吧的 PC 小游戏内容和家里 FC 的体验差异如何区分
|
- [ ] 网吧的 PC 小游戏内容和家里 FC 的体验差异如何区分
|
||||||
- [ ] 街边摊贩的出现逻辑(固定位置+固定时段?还是随机事件?)
|
- [ ] 街边摊贩的出现逻辑(固定位置+固定时段?还是随机事件?)
|
||||||
- [ ] 主街东西两侧是否有视觉上的渐变差异(东侧更传统朴素、西侧更鲜亮热闹?)
|
- [ ] 主街东西两侧是否有视觉上的渐变差异(东侧更传统朴素、西侧更鲜亮热闹?)
|
||||||
|
- [x] ~~平房区生活小店哪些可以进入、哪些仅门口交互~~ → 早餐店可交互,其他简化对话
|
||||||
|
- [x] ~~小伙伴家是否可进入~~ → 4 家可进入(全部简化室内):死党A家、死党B家、老板儿子家(小卖部后屋)、女班长家(主角家同楼)
|
||||||
|
- [x] ~~包机房安排~~ → 合并进街机店后屋
|
||||||
|
- [x] ~~废品回收点~~ → 主街固定摊位 + 流动收废品车
|
||||||
|
- [ ] 秘密基地 C 的具体位置和进入方式
|
||||||
|
- [ ] 储蓄所是否真正参与零花钱系统(还是纯氛围)
|
||||||
|
- [ ] 服装店/鞋店是否有实际交互(买衣服/换装?还是纯氛围)
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# 头脑风暴:灵感补充 + 城郊建筑与区域规划
|
||||||
|
|
||||||
|
**日期**:2026-06-10
|
||||||
|
**参与**:starmen1265 × 策划 Agent
|
||||||
|
**状态**:✅ 方向确认完毕
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 讨论一:灵感列表对比与新发现
|
||||||
|
|
||||||
|
用户提供了一份童年记忆灵感列表,对照已有三份头脑风暴记录进行逐一对比。
|
||||||
|
|
||||||
|
### 已有记录(不重复)
|
||||||
|
|
||||||
|
抓虫(乡下抓知了)、跳皮筋、呼啦圈、跳房子、石头剪刀布、警察抓小偷、一二三木头人、天线调台、日立电视、收音机、ADSL拨号上网、排球女将、大哥大、BP机、广播体操、广场舞、废旧报纸卖废品、少年包青天、菜地(迈皋桥地理元素)
|
||||||
|
|
||||||
|
### ⭐ 新发现元素
|
||||||
|
|
||||||
|
#### 乡下乡土活动(奶奶家阶段素材)
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 1 | 钓鱼(鱼竿,区别于"摸鱼") | ⭐⭐ 乡村独立小游戏潜力 |
|
||||||
|
| 2 | 游泳(夏天河里/水塘) | ⭐⭐ 场景事件+氛围 |
|
||||||
|
| 3 | 挤牛奶 | ⭐ 互动体验 |
|
||||||
|
| 4 | 喂鸡、掏鸡蛋 | ⭐⭐⭐ 高辨识度、互动感强 |
|
||||||
|
| 5 | 菜地浇水 | ⭐ 零花钱/好感度任务 |
|
||||||
|
| 6 | 打水漂 | ⭐⭐ 简单小互动,有物理乐趣 |
|
||||||
|
|
||||||
|
#### 小孩日常小游戏
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 7 | 双人拍手小游戏(互推游戏) | ⭐⭐ 简单但互动感极强 |
|
||||||
|
| 8 | 钻狗洞的秘密基地 | ⭐⭐⭐ **极佳的探索叙事素材** |
|
||||||
|
| 9 | 猜在哪个手里 | ⭐ 最简单的NPC互动小游戏 |
|
||||||
|
| 10 | 叠纸飞机(不同纸、不同技法) | ⭐⭐⭐ 收集+小游戏双潜力 |
|
||||||
|
| 11 | 跑步比赛 | ⭐ 简单的NPC互动触发 |
|
||||||
|
|
||||||
|
#### 人物与社交
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 12 | 爷爷奶奶家的各种年龄段亲戚小孩 | ⭐⭐⭐ 乡下NPC角色体系 |
|
||||||
|
| 13 | 北方来南方的同学 | ⭐⭐⭐ 与"转学生"角色互补 |
|
||||||
|
| 14 | 等着去别人家(串门文化) | ⭐⭐ 日常叙事素材 |
|
||||||
|
| 15 | 不知道名字的小哥哥跟你玩了一下午 | ⭐⭐⭐ **极高情感潜力的微型叙事线** |
|
||||||
|
|
||||||
|
#### 食物与零食
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 16 | 大大泡泡糖 | ⭐⭐ 品牌辨识度极高 |
|
||||||
|
| 17 | 秀逗(超酸的糖) | ⭐⭐ 互动素材——表情反应 |
|
||||||
|
| 18 | 无花果(丝) | ⭐ 零花钱消费场景填充 |
|
||||||
|
| 19 | 老鼠屎(盐津葡萄/话梅粒) | ⭐⭐ 名字本身就是小孩文化缩影 |
|
||||||
|
| 20 | 跳跳糖 | ⭐⭐ 视觉效果潜力 |
|
||||||
|
| 21 | 烤鱿鱼 | ⭐ 街边饮食扩展 |
|
||||||
|
| 22 | 烤里脊 | ⭐ 街边饮食扩展 |
|
||||||
|
| 23 | 炒饭和速冻水饺 | ⭐⭐ 与体力系统直接联动 |
|
||||||
|
|
||||||
|
#### 日常生活与物品
|
||||||
|
|
||||||
|
| # | 元素 | 灵感价值 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 24 | 每天都有吃饭聊天时间 | ⭐⭐ 氛围事件+叙事窗口 |
|
||||||
|
| 25 | 小天鹅(洗衣机品牌) | ⭐ 时代品牌元素 |
|
||||||
|
| 26 | MP3(2000年前后刚开始出现) | ⭐⭐ 时代物品——比随身听更"潮" |
|
||||||
|
| 27 | 装修(邻居家在装修) | ⭐⭐ 场景细节(讨论后决定不做) |
|
||||||
|
| 28 | 墙上涂鸦 | ⭐ 收集/互动素材 |
|
||||||
|
| 29 | 十二生肖 | 待确认具体指什么 |
|
||||||
|
| 30 | 吃遍附近的美食 | ⭐⭐ 探索型目标 |
|
||||||
|
| 31 | 纸壳 | ⭐ 与叠纸飞机联动 |
|
||||||
|
| 32 | 西游记后传(2000年播出) | ⭐⭐ 电视内容补充 |
|
||||||
|
| 33 | 玉河摩托 | 待确认具体指什么 |
|
||||||
|
| 34 | 公交 | ⭐ 场景/移动手段 |
|
||||||
|
|
||||||
|
#### 游戏机制灵感
|
||||||
|
|
||||||
|
**体力系统雏形**:
|
||||||
|
> 吃饭补充体力,饭点之前体力无限。如果不吃饭,体力没跑几步就没了。
|
||||||
|
|
||||||
|
**动态家庭事件链**:
|
||||||
|
> 爸爸妈妈早上会给留两顿炒饭 → 如果都不吃,连续三天,会换成速冻水饺 → 如果还不吃,每天会多给两顿饭钱。
|
||||||
|
|
||||||
|
这两个设计同时做到了:还原真实 × 驱动行为 × 叙事表达。
|
||||||
|
|
||||||
|
#### 重点标记 ⭐⭐⭐
|
||||||
|
|
||||||
|
1. **钻狗洞的秘密基地** — 探索叙事
|
||||||
|
2. **叠纸飞机** — 收集+小游戏
|
||||||
|
3. **爷爷奶奶家的亲戚小孩** — 乡下NPC体系
|
||||||
|
4. **不知道名字的小哥哥** — 情感微型叙事
|
||||||
|
5. **体力系统+家庭动态事件** — 核心玩法机制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 讨论二:城郊区域建筑与区域规划
|
||||||
|
|
||||||
|
基于全部已记录的活动/事件,讨论城市主场景应有哪些建筑与区域(不含乡下)。
|
||||||
|
|
||||||
|
### 确认的变更(相比现有场景设计)
|
||||||
|
|
||||||
|
| # | 变更项 | 原设计 | 新决定 |
|
||||||
|
|---|--------|--------|--------|
|
||||||
|
| 1 | 小伙伴家 | 全部不可进入 | 4 家可进入(简化室内) |
|
||||||
|
| 2 | 包机房 | 无 | 合并进街机店后屋 |
|
||||||
|
| 3 | 废品回收 | 无 | 固定点 + 流动收废品车 |
|
||||||
|
| 4 | 公交站 | 无 | 主街公交站牌(纯装饰) |
|
||||||
|
| 5 | 秘密基地 | 无 | 多处散布 |
|
||||||
|
| 6 | 棋牌室 | 无 | 新增 |
|
||||||
|
| 7 | 储蓄所 | 无 | 新增 |
|
||||||
|
| 8 | 菜地 | 无 | 不做(留乡下) |
|
||||||
|
| 9 | 装修 | 无 | 不做 |
|
||||||
|
| 10 | 游泳 | 无 | 河边不能下水 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 完整建筑与区域清单
|
||||||
|
|
||||||
|
#### 一、商业主街(东西走向,~70m x 5-6m)
|
||||||
|
|
||||||
|
**东侧(传统生活)**
|
||||||
|
|
||||||
|
| # | 名称 | 尺寸 | 可进入 | 核心功能 | 备注 |
|
||||||
|
|---|------|------|--------|---------|------|
|
||||||
|
| S1 | 合并小卖部 | 8m x 5m | ✅ | 零食/文具/四驱配件/经济核心 | 后半部分是老板儿子家(简化室内) |
|
||||||
|
| S2 | 书店 | 5m x 4m | ✅ | 漫画/书籍租借 | |
|
||||||
|
| S3 | 粮油店 | 小型 | ✅ | 米面油酱醋 | |
|
||||||
|
| S4 | 服装店/裁缝铺 | 小型 | 简化 | 氛围 + 可能买衣服 | |
|
||||||
|
| S5 | 鞋店 | 小型 | 简化 | 氛围 + 运动鞋梦想 | |
|
||||||
|
|
||||||
|
**西侧(娱乐商业)**
|
||||||
|
|
||||||
|
| # | 名称 | 尺寸 | 可进入 | 核心功能 | 备注 |
|
||||||
|
|---|------|------|--------|---------|------|
|
||||||
|
| S6 | 街机店 | 8m x 6m | ✅ | 街机小游戏 + 社交 | 含包机房后屋(门帘隔着) |
|
||||||
|
| S7 | 网吧 | 6m x 5m | ✅ | PC 小游戏 + 社交 | |
|
||||||
|
| S8 | 录像出租点 | 5m x 4m | ✅ | 录像带/磁带租借 | 只出租,无观影厅 |
|
||||||
|
| S9 | 储蓄所 | 小型 | 简化 | 存取零花钱? | |
|
||||||
|
| S10 | 棋牌室 | 小型 | ❌ | 纯氛围(大人的场所) | |
|
||||||
|
|
||||||
|
**主街设施(散布)**
|
||||||
|
|
||||||
|
| # | 名称 | 性质 |
|
||||||
|
|---|------|------|
|
||||||
|
| S11 | 公交站牌 | 纯装饰,公交经过不可乘坐 |
|
||||||
|
| S12 | 废品回收点 | 路边固定摊位,可交互卖废品 |
|
||||||
|
| S13 | 街边摊贩 × N | 散布式(烤鱿鱼、烤里脊、冰棍推车、烤肠等) |
|
||||||
|
|
||||||
|
**主街尽端**
|
||||||
|
|
||||||
|
| # | 名称 | 尺寸 | 可进入 |
|
||||||
|
|---|------|------|--------|
|
||||||
|
| S14 | 学校操场 | 25m x 18m | 仅室外 |
|
||||||
|
|
||||||
|
#### 二、生活区巷子(主街北侧,南北纵深)
|
||||||
|
|
||||||
|
**中部 — 事业单位家属楼**
|
||||||
|
|
||||||
|
| # | 名称 | 性质 | 备注 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R1 | 主角家 | 6 层家属楼,完整室内(两室一厅) | 起点终点、FC 游戏、电视、电话 |
|
||||||
|
| R2 | 女班长家 | 同栋楼,简化室内 | 同楼不同层 |
|
||||||
|
| R3 | 信报箱 | 楼下设施 | 氛围 + 可能收信/通知 |
|
||||||
|
|
||||||
|
**东侧 — 小区**
|
||||||
|
|
||||||
|
| # | 名称 | 性质 | 备注 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R4 | 小区 | 3-4 栋 6 层楼 + 中庭空地 | 死党 A 住这里 |
|
||||||
|
| R5 | 死党 A 家 | 小区内某栋,简化室内 | 家境好,有四驱车跑道 |
|
||||||
|
| R6 | 自行车棚 | 小区内设施 | 氛围 |
|
||||||
|
|
||||||
|
**西侧 — 平房区**
|
||||||
|
|
||||||
|
| # | 名称 | 性质 | 可交互 | 备注 |
|
||||||
|
|---|------|------|--------|------|
|
||||||
|
| R7 | 平房区 | 十几二十间平房 + 窄巷子 | — | 死党 B 住这里 |
|
||||||
|
| R8 | 死党 B 家 | 平房区内某间,简化室内 | ✅ | 家境差,温馨 |
|
||||||
|
| R9 | 早餐店 | 平房区沿街 | ✅ 买早点 | 包子油条豆浆 |
|
||||||
|
| R10 | 理发店 | 平房区小店 | 简化对话 | 老式转椅+镜子 |
|
||||||
|
| R11 | 修车铺 | 平房区小店 | 简化对话 | 门口堆轮胎零件 |
|
||||||
|
| R12 | 缝纫店 | 平房区小店 | 简化对话 | 缝纫机响 |
|
||||||
|
| R13 | 公共厕所 | 平房区内 | ❌ | 城郊生活标志 |
|
||||||
|
|
||||||
|
**巷子通道**
|
||||||
|
|
||||||
|
| # | 名称 | 备注 |
|
||||||
|
|---|------|------|
|
||||||
|
| R14 | 巷子口 × 2-3 | 主街与生活区的连接口 |
|
||||||
|
|
||||||
|
#### 三、边缘区域(北端)
|
||||||
|
|
||||||
|
| # | 名称 | 尺寸 | 备注 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| E1 | 大型空地/工地 | ~40m x 30m | 一半空地(弹珠主战场),一半工地(围挡) |
|
||||||
|
| E2 | 河边/河堤 | ~50m x 8m | 不能下水,氛围+打水漂 |
|
||||||
|
|
||||||
|
#### 四、隐藏区域
|
||||||
|
|
||||||
|
| # | 名称 | 位置 | 进入方式 |
|
||||||
|
|---|------|------|---------|
|
||||||
|
| H1 | 秘密基地 A | 工地围挡后面 | 钻洞 |
|
||||||
|
| H2 | 秘密基地 B | 平房区死角 | 穿窄巷 |
|
||||||
|
| H3 | 秘密基地 C | 待定 | 待定 |
|
||||||
|
|
||||||
|
#### 五、移动元素
|
||||||
|
|
||||||
|
| # | 名称 | 路线 | 功能 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| M1 | 流动收废品车 | 巷子内巡游 | 卖废品 |
|
||||||
|
| M2 | 街边摊贩 | 主街散布 | 零食/小吃 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 主街店铺分布(东西排列)
|
||||||
|
|
||||||
|
```
|
||||||
|
← 东(生活) 西(娱乐)→
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [学校操场] [小卖部] [粮油店] [书店] [服装店] [鞋店] · 公交站 · [棋牌室] [储蓄所] [录像出租] [网吧] [街机店] │
|
||||||
|
│ ↑ 废品回收点(路边) ↑ 摊贩散布 │
|
||||||
|
└──────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 可进入室内完整清单
|
||||||
|
|
||||||
|
| # | 地点 | 规模 | 关联 NPC |
|
||||||
|
|---|------|------|---------|
|
||||||
|
| 1 | 主角家 | 完整(两室一厅) | — |
|
||||||
|
| 2 | 合并小卖部 | 完整 | 老板儿子 |
|
||||||
|
| 3 | 书店 | 中等 | — |
|
||||||
|
| 4 | 街机店(含包机房后屋) | 中等 | 小霸王 |
|
||||||
|
| 5 | 网吧 | 中等 | — |
|
||||||
|
| 6 | 录像出租点 | 中等 | — |
|
||||||
|
| 7 | 死党 A 家 | 简化(小空间+对话) | 死党 A |
|
||||||
|
| 8 | 死党 B 家 | 简化 | 死党 B |
|
||||||
|
| 9 | 老板儿子家 | 简化(小卖部后屋) | 老板儿子 |
|
||||||
|
| 10 | 女班长家 | 简化(主角家同楼) | 女班长 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 讨论共识记录
|
||||||
|
|
||||||
|
1. **菜地**:城市场景不做,留给乡下阶段
|
||||||
|
2. **秘密基地**:多处散布,小型空间(工地围挡后、平房区死角、至少 2-3 处)
|
||||||
|
3. **小伙伴家**:4 家可进入,全部简化室内(小空间 + 对话为主)
|
||||||
|
- 死党 A 家 → 小区内
|
||||||
|
- 死党 B 家 → 平房区内
|
||||||
|
- 老板儿子家 → 小卖部后屋
|
||||||
|
- 女班长家 → 主角家同楼不同层
|
||||||
|
4. **录像厅**:只做出租点,不做观影厅
|
||||||
|
5. **包机房**:合并进街机店后屋
|
||||||
|
6. **废品回收**:主街固定摊位 + 流动收废品车(巷子巡游)
|
||||||
|
7. **装修**:不做
|
||||||
|
8. **公交站**:纯装饰站牌
|
||||||
|
9. **摊贩**:散布式
|
||||||
|
10. **游泳**:河边不能下水
|
||||||
|
11. **新增店铺**:粮油店、服装店/裁缝铺、鞋店 → 都放主街
|
||||||
|
12. **新增设施**:棋牌室(大人场所/纯氛围)、储蓄所、公共厕所、自行车棚、信报箱
|
||||||
|
13. **平房区小店**:早餐店(可交互)、理发店/修车铺/缝纫店(背景+对话)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 统计
|
||||||
|
|
||||||
|
| 类别 | 数量 |
|
||||||
|
|------|------|
|
||||||
|
| 商业主街店铺/设施 | 14 个 |
|
||||||
|
| 生活区建筑/设施 | 14 个 |
|
||||||
|
| 边缘区域 | 2 个 |
|
||||||
|
| 隐藏区域 | 多处 |
|
||||||
|
| 移动元素 | 2 类 |
|
||||||
|
| **可进入室内** | ~12 个 |
|
||||||
|
|
||||||
|
### 与概念草案的关系
|
||||||
|
|
||||||
|
概念草案定义「可进入室内:主角家 + 4 家店铺」。新设计变更为 6 家店铺 + 4 家小伙伴家(简化)+ 主角家 = ~12 个可进入室内。这是扩展,不与概念草案冲突。概念草案冻结,变更记录在系统设计案中。
|
||||||
+130
-56
@@ -3,17 +3,66 @@
|
|||||||
> 在场景概念与关卡执行案之间,提供可视化布局沟通工具。
|
> 在场景概念与关卡执行案之间,提供可视化布局沟通工具。
|
||||||
> 用颜色画笔绘制世界布局 → AI 解读 → 双方确认 → 产出执行案。
|
> 用颜色画笔绘制世界布局 → AI 解读 → 双方确认 → 产出执行案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
1. 双击 `sketch.html` 在浏览器中打开(推荐 Chrome / Edge)
|
### 方式一:本地服务器(推荐)
|
||||||
2. 左侧设置场景名称和世界尺寸(米)
|
|
||||||
3. 选择画笔,在画布上绘制
|
保存/加载直接读写 `output/` 目录,无需手动下载。
|
||||||
4. **双击** 已绘制区域 → 弹出命名对话框:
|
|
||||||
|
1. 双击 `start.bat`
|
||||||
|
2. 浏览器自动打开 `http://localhost:8765/sketch.html`
|
||||||
|
3. 开始绘制,点"保存"直接存入 `output/`
|
||||||
|
|
||||||
|
> 需要 Python 3。关闭终端窗口即可停止服务器。
|
||||||
|
|
||||||
|
### 方式二:直接打开 HTML
|
||||||
|
|
||||||
|
双击 `sketch.html` 在浏览器中打开(推荐 Chrome / Edge)。
|
||||||
|
|
||||||
|
- 保存时通过浏览器下载 ZIP 文件
|
||||||
|
- 加载时通过浏览器文件选择器选择 ZIP
|
||||||
|
|
||||||
|
> **注意**:直接打开 HTML 时加载独立的 `.json` 文件**不会恢复画布位图**——位图数据存储在 ZIP 包内的 PNG 文件中,必须加载 `.zip` 文件才能完整恢复。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
world-sketcher/
|
||||||
|
├── start.bat ← 启动脚本(双击运行)
|
||||||
|
├── server.py ← 本地保存/加载服务器
|
||||||
|
├── sketch.html ← 主工具(单文件,浏览器运行)
|
||||||
|
├── README.md
|
||||||
|
└── output/ ← 保存/加载目录
|
||||||
|
├── 迈皋桥-scene.zip ← 完整场景包(JSON + 所有 PNG)
|
||||||
|
├── 迈皋桥-scene.json ← 结构化数据(不含位图)
|
||||||
|
├── 迈皋桥-L1-drawing.png # L1 画布像素数据
|
||||||
|
├── 迈皋桥-L1-overview-preview.png # L1 合并预览
|
||||||
|
└── 迈皋桥-L2-*-drawing.png # L2 各区域画布数据
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 基本操作
|
||||||
|
|
||||||
|
1. 左侧设置**场景名称**和**世界尺寸**(米)
|
||||||
|
2. 选择画笔,在画布上绘制
|
||||||
|
3. **双击** 已绘制区域 → 弹出命名对话框:
|
||||||
- 输入名称(如"家"、"小卖部")
|
- 输入名称(如"家"、"小卖部")
|
||||||
- 设置**高度**(建筑/区域/不可通行类型时显示,默认 3m)
|
- **L1 区域**:设置**地形标高**(米,可为负值表示洼地)
|
||||||
- 勾选**连通到**其他已命名区域(建立区域间连接关系)
|
- **L2 建筑**:设置**建筑高度**(米)
|
||||||
|
- 勾选**连通到**其他已命名区域(建立连接关系)
|
||||||
- 勾选「可进入」→ 创建下级详图(L1 → L2)
|
- 勾选「可进入」→ 创建下级详图(L1 → L2)
|
||||||
5. 保存 ZIP(含 PNG + JSON)
|
4. 保存 → 存入 `output/`(服务器模式)或浏览器下载(离线模式)
|
||||||
|
|
||||||
|
### 画布标签
|
||||||
|
|
||||||
|
- **L1 层**:显示区域名称 + 地形标高(如 `棚户区 (标高2m)`)
|
||||||
|
- **L2 层**:显示建筑名称 + 建筑高度(如 `棚户1 (3m)`)
|
||||||
|
- 标高为 0 时不显示高度
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,36 +79,30 @@
|
|||||||
- L1 画布上,双击已命名的区域并勾选「可进入」
|
- L1 画布上,双击已命名的区域并勾选「可进入」
|
||||||
- 左侧元素列表中点击 **"进入 →"**
|
- 左侧元素列表中点击 **"进入 →"**
|
||||||
- 面包屑导航点击 **"场景总览"** 返回上级
|
- 面包屑导航点击 **"场景总览"** 返回上级
|
||||||
|
- 返回 L1 时,L2 的绘制内容会自动合成回 L1 对应区域
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 画笔图例
|
## 画笔图例
|
||||||
|
|
||||||
| 画笔 | 颜色 | 用途 | 对应 UE 材质 | 默认高度 |
|
| 画笔 | 颜色 | 用途 | 对应 UE 材质 | 可用层级 | 高度含义 |
|
||||||
|------|------|------|-------------|---------|
|
|------|------|------|-------------|---------|---------|
|
||||||
| 道路 Road | 灰 `#909090` | 道路、马路、小路 | MI_DevGray | — |
|
| 道路 Road | 灰 `#909090` | 道路、马路、小路 | MI_DevGray | L1/L2 | — |
|
||||||
| 建筑 Building | 橙 `#E07020` | 建筑物 | MI_DevOrange | 3m |
|
| 建筑 Building | 橙 `#E07020` | 建筑物 | MI_DevOrange | L2 | 建筑高度 |
|
||||||
| 水域 Water | 天蓝 `#40A0D0` | 河流、水塘、水体 | MI_DevSkyBlue | — |
|
| 水域 Water | 天蓝 `#40A0D0` | 河流、水塘、水体 | MI_DevSkyBlue | L1/L2 | — |
|
||||||
| 植被 Greenery | 绿 `#40B040` | 树木、草地、绿化带 | MI_DevGreen | 2m |
|
| 植被 Greenery | 绿 `#40B040` | 树木、草地、绿化带 | MI_DevGreen | L2 | 2m |
|
||||||
| 不可通行 Blocked | 棕 `#A07040` | 山体、围墙、不可达区域 | MI_DevBrown | 3m |
|
| 不可通行 Blocked | 棕 `#A07040` | 山体、围墙、不可达区域 | MI_DevBrown | L1/L2 | 3m |
|
||||||
| 区域 Zone | 红 `#D04040` | 区域边界、地块划分 | MI_DevRed | 3m |
|
| 区域 Zone | 红 `#D04040` | 区域边界、地块划分 | MI_DevRed | L1 | 地形标高 |
|
||||||
| 橡皮擦 Eraser | — | 擦除 | — | — |
|
| 橡皮擦 Eraser | — | 擦除 | — | L1/L2 | — |
|
||||||
|
|
||||||
**L1 层**主要用:区域、道路、水域
|
|
||||||
**L2 层**主要用:建筑、道路、植被、水域、不可通行
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 绘图工具
|
## 快捷键
|
||||||
|
|
||||||
| 工具 | 快捷键 | 说明 |
|
|
||||||
|------|--------|------|
|
|
||||||
| 画笔 | P | 自由绘制,按住鼠标拖动 |
|
|
||||||
| 矩形 | R | 绘制矩形区域,拖动定义两角 |
|
|
||||||
| 橡皮擦 | E | 擦除画布内容 |
|
|
||||||
|
|
||||||
| 操作 | 快捷键 |
|
| 操作 | 快捷键 |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
|
| 画笔工具 | P |
|
||||||
|
| 橡皮擦工具 | E |
|
||||||
| 撤销 | Ctrl+Z |
|
| 撤销 | Ctrl+Z |
|
||||||
| 重做 | Ctrl+Y |
|
| 重做 | Ctrl+Y |
|
||||||
| 网格开关 | G |
|
| 网格开关 | G |
|
||||||
@@ -74,57 +117,87 @@
|
|||||||
- **单位**:厘米(与 UE5 一致,1 uu = 1 cm)
|
- **单位**:厘米(与 UE5 一致,1 uu = 1 cm)
|
||||||
- 画布顶部 = 世界最大 Y,画布底部 = 世界 Y=0
|
- 画布顶部 = 世界最大 Y,画布底部 = 世界 Y=0
|
||||||
|
|
||||||
状态栏实时显示鼠标位置的世界坐标。
|
状态栏实时显示鼠标位置的像素坐标和世界坐标。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 输出文件
|
## 输出格式
|
||||||
|
|
||||||
保存全部时生成:
|
|
||||||
|
|
||||||
```
|
|
||||||
<场景名>-L1-overview.png # L1 画布截图
|
|
||||||
<场景名>-scene.json # 完整场景数据(所有层级)
|
|
||||||
```
|
|
||||||
|
|
||||||
### scene.json 结构
|
### scene.json 结构
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"scene_name": "城郊小街",
|
"scene_name": "迈皋桥",
|
||||||
"version": "v02",
|
"version": "v02",
|
||||||
|
"created": "2026-06-09T16:46:32",
|
||||||
"origin": "SW corner, X+ East, Y+ North",
|
"origin": "SW corner, X+ East, Y+ North",
|
||||||
"brush_legend": { "色值": { "type": "类型", "mi_material": "MI_Dev*" } },
|
"brush_legend": {
|
||||||
|
"#D04040": { "type": "zone", "label": "区域 Zone", "mi_material": "MI_DevRed" }
|
||||||
|
},
|
||||||
"levels": {
|
"levels": {
|
||||||
"L1": {
|
"L1": {
|
||||||
"world_size_m": [500, 400],
|
"world_size_m": [500, 400],
|
||||||
"canvas_size_px": [1000, 800],
|
"canvas_size_px": [1000, 800],
|
||||||
"scale_m_per_px": 0.5,
|
"scale_m_per_px": 0.5,
|
||||||
"elements": [
|
"elements": [
|
||||||
{ "id": "el_001", "type": "zone", "name": "家", "height_m": 3,
|
{
|
||||||
"connections": ["el_003"], "has_detail": false },
|
"id": "el_001", "type": "zone", "name": "棚户区",
|
||||||
{ "id": "el_002", "type": "zone", "name": "小卖部", "height_m": 3,
|
"height_m": 2, "connections": ["el_002"],
|
||||||
"connections": ["el_003"], "has_detail": false },
|
"has_detail": true, "detail_ref": "L2-el_001"
|
||||||
{ "id": "el_003", "type": "zone", "name": "主街道", "height_m": 3,
|
}
|
||||||
"connections": ["el_001", "el_002", "el_004"], "has_detail": true, "detail_ref": "L2-el_003" }
|
|
||||||
],
|
],
|
||||||
"regions": [
|
"regions": [
|
||||||
{ "id": "r_001", "type": "zone", "name": "家",
|
{
|
||||||
"bounds_world_m": [5.0, 5.0, 12.5, 15.0], "area_px": 15000 }
|
"id": "r_001", "type": "zone", "name": "棚户区",
|
||||||
]
|
"bounds_world_m": [5.0, 5.0, 120.0, 95.0],
|
||||||
|
"area_px": 15000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"image_file": "迈皋桥-L1-drawing.png",
|
||||||
|
"l1_base_file": null
|
||||||
},
|
},
|
||||||
"L2-el_003": {
|
"L2-el_001": {
|
||||||
"world_size_m": [80, 60],
|
"world_size_m": [80, 60],
|
||||||
"elements": [
|
"elements": [
|
||||||
{ "id": "el_yyy", "type": "building", "name": "家", "height_m": 3, "connections": [], "has_detail": false }
|
{ "id": "el_010", "type": "building", "name": "棚户1", "height_m": 3 }
|
||||||
],
|
],
|
||||||
|
"image_file": "迈皋桥-L2-el_001-drawing.png",
|
||||||
|
"l1_base_file": "迈皋桥-L2-el_001-l1base.png",
|
||||||
"parent_level": "L1",
|
"parent_level": "L1",
|
||||||
"parent_element": "el_003"
|
"parent_element": "el_001"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 高度字段语义
|
||||||
|
|
||||||
|
| 层级 | 元素类型 | `height_m` 含义 | 用途 |
|
||||||
|
|------|---------|----------------|------|
|
||||||
|
| L1 | zone(区域) | **地形标高** | 地面海拔相对高度,0=基准面,负值=洼地 |
|
||||||
|
| L2 | building(建筑) | **建筑高度** | 建筑物垂直高度 |
|
||||||
|
| L1/L2 | blocked(不可通行) | **障碍高度** | 围墙/山体等垂直尺度 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 本地服务器
|
||||||
|
|
||||||
|
`server.py` 提供三个 HTTP 端点,供 `sketch.html` 直接读写 `output/` 目录:
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/list` | GET | 列出 `output/` 下所有 `.json` / `.zip` 文件 |
|
||||||
|
| `/load/<filename>` | GET | 从 `output/` 读取指定文件 |
|
||||||
|
| `/save` | POST (multipart) | 接收文件并保存到 `output/` |
|
||||||
|
|
||||||
|
服务器运行时,sketch.html 自动检测并使用服务器模式;未运行时自动回退到浏览器下载/文件选择。
|
||||||
|
|
||||||
|
手动启动:
|
||||||
|
```bash
|
||||||
|
python server.py # 默认端口 8765
|
||||||
|
python server.py 9000 # 自定义端口
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 工作流集成
|
## 工作流集成
|
||||||
@@ -145,20 +218,21 @@ AI 布局解读文档 (场景名-layout-vXX.md)
|
|||||||
|
|
||||||
### AI 解读流程
|
### AI 解读流程
|
||||||
|
|
||||||
1. 用户在工具中画好布局,命名区域,设置高度和连通关系,保存 ZIP
|
1. 用户在工具中画好布局,命名区域,设置标高/高度和连通关系,保存
|
||||||
2. AI 读取 JSON 布局数据(elements + regions + connections + height_m)
|
2. AI 读取 JSON 中的 elements + regions + connections + height_m
|
||||||
3. AI 交叉参考场景设计案的氛围描述,将 JSON 数据与场景概念建立映射
|
3. AI 交叉参考场景设计案的氛围描述,将 JSON 数据与场景概念建立映射
|
||||||
4. AI 产出结构化布局解读 Markdown(区域表、连通关系、高度、待确认问题)
|
4. AI 产出结构化布局解读 Markdown(区域表、连通关系、标高、待确认问题)
|
||||||
5. 用户确认后,策划基于解读文档产出关卡执行案
|
5. 用户确认后,策划基于解读文档产出关卡执行案
|
||||||
|
|
||||||
> **说明**:PNG 文件保存在 ZIP 中用于画布的保存/加载,AI 分析以 JSON 结构化数据为准,不依赖 PNG 像素分析。
|
> PNG 文件用于画布的保存/加载和人工预览。AI 分析以 JSON 结构化数据为准,不依赖 PNG 像素分析。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 设计原则
|
## 设计原则
|
||||||
|
|
||||||
- **强制使用预定义画笔**:无自由拾色器,保证每个像素都映射到已知地形类型
|
- **预定义画笔**:无自由拾色器,保证每个像素都映射到已知地形类型
|
||||||
- **结构化数据优先**:高度和连通关系由用户直接输入,AI 无需从图像推断
|
- **结构化数据优先**:标高、高度和连通关系由用户直接输入,AI 无需从图像推断
|
||||||
- **层次化设计**:L1 定大局,L2 定细节,L3 预留
|
- **层次化设计**:L1 定大局,L2 定细节,L3 预留
|
||||||
- **人机共读**:PNG 供人视觉确认,JSON 供 AI 和程序化解析
|
- **人机共读**:PNG 供人视觉确认,JSON 供 AI 和程序化解析
|
||||||
- **与 Level Builder 一致**:画笔颜色直接映射 MI_Dev* 标识色材质
|
- **材质一致**:画笔颜色直接映射 UE5 的 MI_Dev* 标识色材质
|
||||||
|
- **离线优先**:单 HTML 文件即可使用,服务器仅为便利增强
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""
|
||||||
|
World Sketcher 本地保存服务器
|
||||||
|
用法: python server.py [port]
|
||||||
|
然后浏览器打开 http://localhost:<port>/sketch.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
import http.server, json, os, sys, urllib.parse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
|
||||||
|
SERVE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
OUTPUT_DIR = os.path.join(SERVE_DIR, "output")
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, directory=SERVE_DIR, **kwargs)
|
||||||
|
|
||||||
|
# ── POST /save ── 保存文件到 output/
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path != "/save":
|
||||||
|
self.send_error(404); return
|
||||||
|
|
||||||
|
ctype = self.headers.get("Content-Type", "")
|
||||||
|
if ctype.startswith("multipart/form-data"):
|
||||||
|
boundary = ctype.split("boundary=")[-1].encode()
|
||||||
|
data = self.rfile.read(int(self.headers["Content-Length"]))
|
||||||
|
|
||||||
|
# 简易 multipart 解析
|
||||||
|
parts = data.split(b"--" + boundary)
|
||||||
|
saved = []
|
||||||
|
for part in parts:
|
||||||
|
if not part or part.strip() in (b"", b"--\r\n", b"--"):
|
||||||
|
continue
|
||||||
|
header_end = part.find(b"\r\n\r\n")
|
||||||
|
if header_end < 0:
|
||||||
|
continue
|
||||||
|
header = part[:header_end].decode("utf-8", errors="replace")
|
||||||
|
body = part[header_end + 4:]
|
||||||
|
if body.endswith(b"\r\n"):
|
||||||
|
body = body[:-2]
|
||||||
|
|
||||||
|
# 提取文件名
|
||||||
|
fname = None
|
||||||
|
for seg in header.split(";"):
|
||||||
|
seg = seg.strip()
|
||||||
|
if seg.startswith("filename="):
|
||||||
|
fname = seg.split("=", 1)[1].strip('"')
|
||||||
|
break
|
||||||
|
if not fname:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 安全:只取文件名,禁止路径遍历
|
||||||
|
fname = os.path.basename(fname)
|
||||||
|
fpath = os.path.join(OUTPUT_DIR, fname)
|
||||||
|
with open(fpath, "wb") as f:
|
||||||
|
f.write(body)
|
||||||
|
saved.append(fname)
|
||||||
|
|
||||||
|
resp = json.dumps({"ok": True, "saved": saved}, ensure_ascii=False)
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(resp.encode("utf-8"))
|
||||||
|
print(f" [{datetime.now():%H:%M:%S}] Saved: {', '.join(saved)}")
|
||||||
|
else:
|
||||||
|
self.send_error(400, "Expected multipart/form-data")
|
||||||
|
|
||||||
|
# ── GET 路由 ──
|
||||||
|
def do_GET(self):
|
||||||
|
# /list → 列出 output/ 下可用文件
|
||||||
|
if self.path == "/list":
|
||||||
|
files = sorted(
|
||||||
|
f for f in os.listdir(OUTPUT_DIR)
|
||||||
|
if f.endswith((".json", ".zip"))
|
||||||
|
)
|
||||||
|
resp = json.dumps({"files": files}, ensure_ascii=False)
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(resp.encode("utf-8"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# /load/<file> → 从 output/ 读取文件
|
||||||
|
if self.path.startswith("/load/"):
|
||||||
|
fname = os.path.basename(urllib.parse.unquote(self.path[6:], encoding="utf-8"))
|
||||||
|
fpath = os.path.join(OUTPUT_DIR, fname)
|
||||||
|
if not os.path.isfile(fpath):
|
||||||
|
self.send_error(404); return
|
||||||
|
self.send_response(200)
|
||||||
|
if fname.endswith(".zip"):
|
||||||
|
self.send_header("Content-Type", "application/zip")
|
||||||
|
elif fname.endswith(".json"):
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
with open(fpath, "rb") as f:
|
||||||
|
self.wfile.write(f.read())
|
||||||
|
return
|
||||||
|
|
||||||
|
# 其他 → 静态文件(sketch.html 等)
|
||||||
|
super().do_GET()
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
# 只显示非静态资源的请求日志
|
||||||
|
if "/save" in str(args) or "/list" in str(args) or "/load/" in str(args):
|
||||||
|
super().log_message(format, *args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with http.server.HTTPServer(("", PORT), Handler) as httpd:
|
||||||
|
url = f"http://localhost:{PORT}/sketch.html"
|
||||||
|
print(f"World Sketcher server running")
|
||||||
|
print(f" URL: {url}")
|
||||||
|
print(f" Output: {OUTPUT_DIR}")
|
||||||
|
print(f" Press Ctrl+C to stop.\n")
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nServer stopped.")
|
||||||
@@ -147,7 +147,7 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
|
|||||||
<button class="tbtn" id="btn-clear">清空</button>
|
<button class="tbtn" id="btn-clear">清空</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tool-group">
|
<div class="tool-group">
|
||||||
<button class="tbtn" id="btn-save-all">保存 ZIP</button>
|
<button class="tbtn" id="btn-save-all">保存</button>
|
||||||
<button class="tbtn" id="btn-load">加载</button>
|
<button class="tbtn" id="btn-load">加载</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,8 +174,8 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
|
|||||||
<input id="name-input" type="text" placeholder="输入名称(如"家"、"小卖部")">
|
<input id="name-input" type="text" placeholder="输入名称(如"家"、"小卖部")">
|
||||||
<label><input type="checkbox" id="name-enterable"> 可进入(有下级详图)</label>
|
<label><input type="checkbox" id="name-enterable"> 可进入(有下级详图)</label>
|
||||||
<div id="name-height-row" style="display:none;">
|
<div id="name-height-row" style="display:none;">
|
||||||
<label style="color:var(--text-dim);">高度</label>
|
<label id="name-height-label" style="color:var(--text-dim);">高度</label>
|
||||||
<input id="name-height" type="number" min="0" step="0.5" value="3">
|
<input id="name-height" type="number" step="0.5" value="3">
|
||||||
<span style="color:var(--text-dim);">m</span>
|
<span style="color:var(--text-dim);">m</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="name-connections-row" style="display:none;">
|
<div id="name-connections-row" style="display:none;">
|
||||||
@@ -191,6 +191,17 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
|
|||||||
|
|
||||||
<input type="file" id="file-input" accept=".json,.zip" style="display:none">
|
<input type="file" id="file-input" accept=".json,.zip" style="display:none">
|
||||||
|
|
||||||
|
<!-- 从 output/ 加载的文件选择弹窗 -->
|
||||||
|
<div id="output-modal" style="display:none;position:fixed;z-index:100;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);">
|
||||||
|
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#2d2d2d;border:1px solid #666;border-radius:8px;padding:16px;min-width:320px;max-height:60vh;display:flex;flex-direction:column;">
|
||||||
|
<h4 style="margin-bottom:10px;font-size:13px;color:#ddd;">从 output/ 加载</h4>
|
||||||
|
<div id="output-file-list" style="flex:1;overflow-y:auto;max-height:300px;"></div>
|
||||||
|
<div style="display:flex;justify-content:flex-end;margin-top:10px;">
|
||||||
|
<button class="tbtn" id="output-modal-close">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
@@ -256,6 +267,8 @@ const App = {
|
|||||||
this.initKeyboard();
|
this.initKeyboard();
|
||||||
this.initLevel("L1", 500, 400, this.defaultL1.canvasW, this.defaultL1.canvasH);
|
this.initLevel("L1", 500, 400, this.defaultL1.canvasW, this.defaultL1.canvasH);
|
||||||
this.switchToLevel("L1");
|
this.switchToLevel("L1");
|
||||||
|
// 缓存服务器可用性(首次加载时检测)
|
||||||
|
this._serverOk = this.serverAvailable();
|
||||||
},
|
},
|
||||||
|
|
||||||
initLevel(id, wW, wH, cW, cH) {
|
initLevel(id, wW, wH, cW, cH) {
|
||||||
@@ -303,6 +316,7 @@ const App = {
|
|||||||
this.renderBrushPalette();
|
this.renderBrushPalette();
|
||||||
this.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
this.updateElementsList();
|
this.updateElementsList();
|
||||||
|
this.drawElementLabels();
|
||||||
this.updateScaleDisplay();
|
this.updateScaleDisplay();
|
||||||
this.updateStatus();
|
this.updateStatus();
|
||||||
},
|
},
|
||||||
@@ -356,7 +370,10 @@ const App = {
|
|||||||
document.getElementById("btn-redo").addEventListener("click", () => this.redo());
|
document.getElementById("btn-redo").addEventListener("click", () => this.redo());
|
||||||
document.getElementById("btn-clear").addEventListener("click", () => this.clearCanvas());
|
document.getElementById("btn-clear").addEventListener("click", () => this.clearCanvas());
|
||||||
document.getElementById("btn-save-all").addEventListener("click", () => this.saveAll());
|
document.getElementById("btn-save-all").addEventListener("click", () => this.saveAll());
|
||||||
document.getElementById("btn-load").addEventListener("click", () => document.getElementById("file-input").click());
|
document.getElementById("btn-load").addEventListener("click", () => this.smartLoad());
|
||||||
|
document.getElementById("output-modal-close").addEventListener("click", () => {
|
||||||
|
document.getElementById("output-modal").style.display = "none";
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ---- Canvas 事件 ---- */
|
/* ---- Canvas 事件 ---- */
|
||||||
@@ -619,7 +636,7 @@ const App = {
|
|||||||
|
|
||||||
this.undoStack = []; this.redoStack = []; this.updateUndoButtons();
|
this.undoStack = []; this.redoStack = []; this.updateUndoButtons();
|
||||||
this.wW.value = l2d.worldW; this.wH.value = l2d.worldH;
|
this.wW.value = l2d.worldW; this.wH.value = l2d.worldH;
|
||||||
this.drawGrid(); this.renderBrushPalette(); this.updateBreadcrumb(); this.updateElementsList(); this.updateScaleDisplay(); this.updateStatus();
|
this.drawGrid(); this.renderBrushPalette(); this.updateBreadcrumb(); this.updateElementsList(); this.drawElementLabels(); this.updateScaleDisplay(); this.updateStatus();
|
||||||
},
|
},
|
||||||
|
|
||||||
compositeL2BackToL1() {
|
compositeL2BackToL1() {
|
||||||
@@ -685,9 +702,19 @@ const App = {
|
|||||||
|
|
||||||
// 高度输入:building / blocked / zone 类型显示
|
// 高度输入:building / blocked / zone 类型显示
|
||||||
const showHeight = ["building", "blocked", "zone"].includes(brushType.id);
|
const showHeight = ["building", "blocked", "zone"].includes(brushType.id);
|
||||||
|
const heightLabel = document.getElementById("name-height-label");
|
||||||
heightRow.style.display = showHeight ? "flex" : "none";
|
heightRow.style.display = showHeight ? "flex" : "none";
|
||||||
if (showHeight) {
|
if (showHeight) {
|
||||||
heightInp.value = existing && existing.height_m != null ? existing.height_m : this.defaultHeightForType(brushType.id);
|
// L1 zone = 地形标高(允许负值);L2 building = 建筑高度
|
||||||
|
if (this.currentLevel === "L1" && brushType.id === "zone") {
|
||||||
|
heightLabel.textContent = "地形标高";
|
||||||
|
heightInp.min = "-10";
|
||||||
|
heightInp.value = existing && existing.height_m != null ? existing.height_m : 0;
|
||||||
|
} else {
|
||||||
|
heightLabel.textContent = "建筑高度";
|
||||||
|
heightInp.min = "0";
|
||||||
|
heightInp.value = existing && existing.height_m != null ? existing.height_m : this.defaultHeightForType(brushType.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连通多选:列出同层级其他已命名元素
|
// 连通多选:列出同层级其他已命名元素
|
||||||
@@ -791,14 +818,26 @@ const App = {
|
|||||||
drawElementLabels() {
|
drawElementLabels() {
|
||||||
const ctx = this.uc.getContext("2d"), d = this.levelData[this.currentLevel];
|
const ctx = this.uc.getContext("2d"), d = this.levelData[this.currentLevel];
|
||||||
ctx.clearRect(0, 0, d.canvasW, d.canvasH);
|
ctx.clearRect(0, 0, d.canvasW, d.canvasH);
|
||||||
|
const isL1 = this.currentLevel === "L1";
|
||||||
d.elements.forEach(el => {
|
d.elements.forEach(el => {
|
||||||
|
// L1 只显示区域(zone)名,L2 只显示建筑(building)名
|
||||||
|
if (isL1 && el.type !== "zone") return;
|
||||||
|
if (!isL1 && el.type !== "building") return;
|
||||||
|
|
||||||
ctx.font = "bold 12px sans-serif";
|
ctx.font = "bold 12px sans-serif";
|
||||||
const tw = ctx.measureText(el.name).width + 8, th = 18;
|
// L1: height_m = 地形标高;L2: height_m = 建筑高度
|
||||||
|
let label = el.name;
|
||||||
|
if (el.height_m != null && el.height_m !== 0) {
|
||||||
|
label = isL1
|
||||||
|
? `${el.name} (标高${el.height_m}m)`
|
||||||
|
: `${el.name} (${el.height_m}m)`;
|
||||||
|
}
|
||||||
|
const tw = ctx.measureText(label).width + 8, th = 18;
|
||||||
const tx = el.px - tw/2, ty = el.py - 28;
|
const tx = el.px - tw/2, ty = el.py - 28;
|
||||||
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||||
ctx.fillRect(tx, ty, tw, th);
|
ctx.fillRect(tx, ty, tw, th);
|
||||||
ctx.fillStyle = "#fff"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
ctx.fillStyle = "#fff"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
||||||
ctx.fillText(el.name, el.px, ty + th/2);
|
ctx.fillText(label, el.px, ty + th/2);
|
||||||
if (el.hasDetail) {
|
if (el.hasDetail) {
|
||||||
ctx.fillStyle = "rgba(74,158,255,0.8)";
|
ctx.fillStyle = "rgba(74,158,255,0.8)";
|
||||||
ctx.beginPath(); ctx.arc(el.px + tw/2 - 2, ty + 2, 5, 0, Math.PI*2); ctx.fill();
|
ctx.beginPath(); ctx.arc(el.px + tw/2 - 2, ty + 2, 5, 0, Math.PI*2); ctx.fill();
|
||||||
@@ -929,8 +968,31 @@ const App = {
|
|||||||
zip.file(`${name}-scene.json`, JSON.stringify(sceneData, null, 2));
|
zip.file(`${name}-scene.json`, JSON.stringify(sceneData, null, 2));
|
||||||
|
|
||||||
const blob = await zip.generateAsync({ type: "blob" });
|
const blob = await zip.generateAsync({ type: "blob" });
|
||||||
this.downloadBlob(blob, `${name}-scene.zip`);
|
|
||||||
this.showSaveFeedback("ZIP 已保存 ✓");
|
// 优先 POST 到本地服务器,失败则回退浏览器下载
|
||||||
|
const savedToServer = await this.saveToServer(`${name}-scene.zip`, blob);
|
||||||
|
if (!savedToServer) {
|
||||||
|
this.downloadBlob(blob, `${name}-scene.zip`);
|
||||||
|
}
|
||||||
|
this.showSaveFeedback(savedToServer ? `已保存到 output/ ✓` : "ZIP 已下载 ✓");
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 尝试通过本地服务器保存到 output/ 目录 */
|
||||||
|
async saveToServer(filename, blob) {
|
||||||
|
try {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", blob, filename);
|
||||||
|
const resp = await fetch("/save", { method: "POST", body: fd });
|
||||||
|
return resp.ok;
|
||||||
|
} catch { return false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 服务器是否可用 */
|
||||||
|
async serverAvailable() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/list", { method: "GET" });
|
||||||
|
return resp.ok;
|
||||||
|
} catch { return false; }
|
||||||
},
|
},
|
||||||
|
|
||||||
showSaveFeedback(msg) {
|
showSaveFeedback(msg) {
|
||||||
@@ -961,6 +1023,69 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 弹出 output/ 文件选择列表 */
|
||||||
|
async showOutputModal() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/list");
|
||||||
|
const { files } = await resp.json();
|
||||||
|
const modal = document.getElementById("output-modal");
|
||||||
|
const list = document.getElementById("output-file-list");
|
||||||
|
list.innerHTML = "";
|
||||||
|
if (files.length === 0) {
|
||||||
|
list.innerHTML = '<div style="color:#999;font-size:12px;padding:8px;">output/ 目录为空</div>';
|
||||||
|
} else {
|
||||||
|
files.forEach(f => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.cssText = "padding:6px 8px;cursor:pointer;border-radius:3px;font-size:12px;display:flex;align-items:center;gap:8px;";
|
||||||
|
row.addEventListener("mouseenter", () => row.style.background = "#3a3a3a");
|
||||||
|
row.addEventListener("mouseleave", () => row.style.background = "");
|
||||||
|
const icon = f.endsWith(".zip") ? "📦" : "📄";
|
||||||
|
row.innerHTML = `<span>${icon}</span><span style="flex:1;color:#ddd;">${f}</span>`;
|
||||||
|
row.addEventListener("click", async () => {
|
||||||
|
modal.style.display = "none";
|
||||||
|
await this.loadFromServer(f);
|
||||||
|
});
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
modal.style.display = "block";
|
||||||
|
} catch {
|
||||||
|
alert("无法连接本地服务器,请确认 server.py 正在运行");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 从服务器 output/ 加载文件 */
|
||||||
|
async loadFromServer(filename) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/load/" + encodeURIComponent(filename));
|
||||||
|
if (!resp.ok) throw new Error("文件不存在");
|
||||||
|
if (filename.endsWith(".zip")) {
|
||||||
|
const ab = await resp.arrayBuffer();
|
||||||
|
const zip = await JSZip.loadAsync(ab);
|
||||||
|
const jsonFile = Object.keys(zip.files).find(n => n.endsWith("-scene.json"));
|
||||||
|
if (!jsonFile) { alert("ZIP 中未找到 scene.json"); return; }
|
||||||
|
const txt = await zip.file(jsonFile).async("string");
|
||||||
|
await this.restoreScene(JSON.parse(txt), zip);
|
||||||
|
} else {
|
||||||
|
const txt = await resp.text();
|
||||||
|
await this.restoreScene(JSON.parse(txt), null);
|
||||||
|
}
|
||||||
|
this.showSaveFeedback(`已加载 ${filename} ✓`);
|
||||||
|
} catch (err) {
|
||||||
|
alert("加载失败:" + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 智能加载:服务器在则从 output/ 读,否则回退浏览器文件选择 */
|
||||||
|
async smartLoad() {
|
||||||
|
const ok = await this._serverOk;
|
||||||
|
if (ok) {
|
||||||
|
this.showOutputModal();
|
||||||
|
} else {
|
||||||
|
document.getElementById("file-input").click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/** 从 PNG Blob 恢复 ImageData */
|
/** 从 PNG Blob 恢复 ImageData */
|
||||||
blobToImageData(blob) {
|
blobToImageData(blob) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@echo off
|
||||||
|
title World Sketcher
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [ERROR] Python not found. Install Python 3 first.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set PORT=8765
|
||||||
|
if not "%~1"=="" set PORT=%~1
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo World Sketcher
|
||||||
|
echo Port: %PORT%
|
||||||
|
echo Save dir: output\
|
||||||
|
echo Close this window to stop.
|
||||||
|
echo.
|
||||||
|
|
||||||
|
start "" cmd /c "timeout /t 2 /nobreak >nul & start http://localhost:%PORT%/sketch.html"
|
||||||
|
|
||||||
|
python "%~dp0server.py" %PORT%
|
||||||
Reference in New Issue
Block a user