Changes #5

Merged
Ghost merged 1 commits from why_ga into main 2026-08-25 11:07:25 +08:00
6 changed files with 310 additions and 52 deletions
+38 -1
View File
@@ -199,13 +199,14 @@ class YtWorldApi(http.Controller):
seen = []
out = []
for r in items.read(['code', 'name', 'category', 'icon', 'footprint_w',
'footprint_h', 'terrains', 'desc', 'flat',
'footprint_h', 'cost_gold', 'terrains', 'desc', 'flat',
'stat_level', 'stat_hp', 'stat_yield', 'actions']):
if r['category'] not in seen:
seen.append(r['category'])
out.append({
'code': r['code'], 'name': r['name'], 'category': r['category'],
'icon': r['icon'], 'w': r['footprint_w'], 'h': r['footprint_h'],
'costGold': r['cost_gold'] or 0,
'terrains': r['terrains'] or [],
'desc': r['desc'] or '',
'flat': bool(r['flat']),
@@ -217,6 +218,42 @@ class YtWorldApi(http.Controller):
cats = [{'id': c, 'name': CAT_LABELS.get(c, c)} for c in seen]
return _json({'cats': cats, 'items': out})
@http.route('/yt_world/api/micro/build/charge', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False)
def charge_build(self, **kw):
"""建造扣费(落库前调用)。body: {"code": "cottage", "count": 1}
校验建筑存在并按 cost_gold * count 扣减玩家金币;余额不足明确报错。"""
auth_err = self._require_user()
if auth_err:
return _json(auth_err)
world = self._get_world()
if not world:
return _json({'ok': False, 'error': 'no_world'})
body = _post_body()
code = (body.get('code') or '').strip()
count = body.get('count') or 1
try:
count = int(count)
except (TypeError, ValueError):
count = 1
if count < 1:
count = 1
if not code:
return _json({'ok': False, 'error': 'bad_params'})
item = request.env['yt.world.build.item'].sudo().search(
[('code', '=', code), ('active', '=', True)], limit=1)
if not item:
return _json({'ok': False, 'error': 'unknown_build', 'code': code})
cost = (item.cost_gold or 0) * count
char = self._get_char(world)
gold = char.gold or 0
if gold < cost:
return _json({'ok': False, 'error': 'no_gold', 'need': cost, 'have': gold, 'code': code})
char.sudo().write({'gold': gold - cost})
_logger.info('yt.world build charge uid=%s code=%s count=%s cost=%s remain=%s',
request.session.uid, code, count, cost, gold - cost)
return _json({'ok': True, 'code': code, 'count': count, 'cost': cost, 'gold': gold - cost})
# ========== 图鉴 API ==========
def _records_json(self, model, fields):
+66 -17
View File
@@ -1,29 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- ===== 微观世界建造素材目录:道路/地面贴图,仅平原、森林可用 ===== -->
<!-- 道路 -->
<!-- 道路:road_auto 为前端"自动道路刷子"对应的计费项(前端单独渲染,不进面板条) -->
<record id="bi_road_auto" model="yt.world.build.item">
<field name="code">road_auto</field><field name="name">道路</field>
<field name="category">road</field><field name="icon">assets/roads/5.png</field>
<field name="footprint_w">1</field><field name="footprint_h">1</field><field name="sequence">5</field>
<field name="cost_gold">1</field>
<field name="terrains">["plain","forest"]</field>
</record>
<record id="bi_road_t" model="yt.world.build.item">
<field name="code">road_t</field><field name="name">T字路口</field>
<field name="category">road</field><field name="icon">assets/roads/1.png</field>
<field name="footprint_w">1</field><field name="footprint_h">1</field><field name="sequence">10</field>
<field name="cost_gold">1</field>
<field name="terrains">["plain","forest"]</field>
</record>
<record id="bi_road_cross" model="yt.world.build.item">
<field name="code">road_cross</field><field name="name">十字路口</field>
<field name="category">road</field><field name="icon">assets/roads/2.png</field>
<field name="footprint_w">1</field><field name="footprint_h">1</field><field name="sequence">20</field>
<field name="cost_gold">1</field>
<field name="terrains">["plain","forest"]</field>
</record>
<record id="bi_road_corner" model="yt.world.build.item">
<field name="code">road_corner</field><field name="name">弯道</field>
<field name="category">road</field><field name="icon">assets/roads/4.png</field>
<field name="footprint_w">1</field><field name="footprint_h">1</field><field name="sequence">30</field>
<field name="cost_gold">1</field>
<field name="terrains">["plain","forest"]</field>
</record>
<record id="bi_road_straight" model="yt.world.build.item">
<field name="code">road_straight</field><field name="name">直路</field>
<field name="category">road</field><field name="icon">assets/roads/5.png</field>
<field name="footprint_w">1</field><field name="footprint_h">1</field><field name="sequence">40</field>
<field name="cost_gold">1</field>
<field name="terrains">["plain","forest"]</field>
</record>
<!-- 地面 -->
@@ -31,18 +42,21 @@
<field name="code">ground_grass_a</field><field name="name">草地A</field>
<field name="category">ground</field><field name="icon">assets/roads/3.png</field>
<field name="footprint_w">1</field><field name="footprint_h">1</field><field name="sequence">110</field>
<field name="cost_gold">0</field>
<field name="terrains">["plain","forest"]</field>
</record>
<record id="bi_ground_grass_b" model="yt.world.build.item">
<field name="code">ground_grass_b</field><field name="name">草地B</field>
<field name="category">ground</field><field name="icon">assets/roads/6.png</field>
<field name="footprint_w">1</field><field name="footprint_h">1</field><field name="sequence">120</field>
<field name="cost_gold">0</field>
<field name="terrains">["plain","forest"]</field>
</record>
<record id="bi_ground_grass_c" model="yt.world.build.item">
<field name="code">ground_grass_c</field><field name="name">草地C</field>
<field name="category">ground</field><field name="icon">assets/roads/7.png</field>
<field name="footprint_w">1</field><field name="footprint_h">1</field><field name="sequence">130</field>
<field name="cost_gold">0</field>
<field name="terrains">["plain","forest"]</field>
</record>
@@ -51,6 +65,7 @@
<field name="code">cottage</field><field name="name">农舍</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/cottage/cottage_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">210</field>
<field name="cost_gold">20</field>
<field name="desc">村民起居的基本居所,提供人口上限与夜间休息。</field>
<field name="stat_level">1</field><field name="stat_hp">120</field>
<field name="stat_yield" eval="{'population': 4}"/>
@@ -63,7 +78,8 @@
<record id="bi_windmill" model="yt.world.build.item">
<field name="code">windmill</field><field name="name">风车</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/windmill/windmill_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">220</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">220</field>
<field name="cost_gold">60</field>
<field name="desc">利用风力磨粉与提水,提升粮食加工效率。</field>
<field name="stat_level">1</field><field name="stat_hp">110</field>
<field name="stat_yield" eval="{'flour': 2}"/>
@@ -71,7 +87,8 @@
<record id="bi_blacksmith" model="yt.world.build.item">
<field name="code">blacksmith</field><field name="name">铁匠铺</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/blacksmith/blacksmith_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">230</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">230</field>
<field name="cost_gold">80</field>
<field name="desc">锻造工具与武器,产出金属制品。</field>
<field name="stat_level">1</field><field name="stat_hp">130</field>
<field name="stat_yield" eval="{'metal': 2}"/>
@@ -79,7 +96,8 @@
<record id="bi_grocery" model="yt.world.build.item">
<field name="code">grocery</field><field name="name">杂货铺</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/grocery/grocery_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">240</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">240</field>
<field name="cost_gold">50</field>
<field name="desc">交易日用物资的小型商铺,促进经济流通。</field>
<field name="stat_level">1</field><field name="stat_hp">90</field>
<field name="stat_yield" eval="{'coin': 2}"/>
@@ -87,7 +105,8 @@
<record id="bi_temple" model="yt.world.build.item">
<field name="code">temple</field><field name="name">神庙</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/temple/temple_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">250</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">250</field>
<field name="cost_gold">100</field>
<field name="desc">供奉信仰的场所,提升居民精神状态与领地声望。</field>
<field name="stat_level">1</field><field name="stat_hp">160</field>
<field name="stat_yield" eval="{'prestige': 3}"/>
@@ -95,7 +114,8 @@
<record id="bi_clinic" model="yt.world.build.item">
<field name="code">clinic</field><field name="name">医务室</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/clinic/clinic_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">260</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">260</field>
<field name="cost_gold">55</field>
<field name="desc">诊治伤病,恢复居民生命值。</field>
<field name="stat_level">1</field><field name="stat_hp">100</field>
<field name="stat_yield" eval="{'heal': 2}"/>
@@ -103,7 +123,8 @@
<record id="bi_restaurant" model="yt.world.build.item">
<field name="code">restaurant</field><field name="name">酒馆</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/restaurant/restaurant_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">270</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">270</field>
<field name="cost_gold">45</field>
<field name="desc">烹制料理,为居民提供饱食与心情加成。</field>
<field name="stat_level">1</field><field name="stat_hp">95</field>
<field name="stat_yield" eval="{'satiety': 3}"/>
@@ -111,7 +132,8 @@
<record id="bi_pasture" model="yt.world.build.item">
<field name="code">pasture</field><field name="name">牧场</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/pasture/pasture_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">280</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">280</field>
<field name="cost_gold">70</field>
<field name="desc">放养牲畜,稳定产出肉奶类资源。</field>
<field name="stat_level">1</field><field name="stat_hp">105</field>
<field name="stat_yield" eval="{'meat': 2, 'milk': 1}"/>
@@ -119,7 +141,8 @@
<record id="bi_blackmarket" model="yt.world.build.item">
<field name="code">blackmarket</field><field name="name">商城</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/blackmarket/blackmarket_r.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">290</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">290</field>
<field name="cost_gold">120</field>
<field name="desc">灰色交易点,可换取稀有物资但伴随风险。</field>
<field name="stat_level">1</field><field name="stat_hp">80</field>
<field name="stat_yield" eval="{'rare': 1}"/>
@@ -129,7 +152,8 @@
<record id="bi_farmland" model="yt.world.build.item">
<field name="code">farmland</field><field name="name">农田</field>
<field name="category">facility</field><field name="icon">assets/buildings/spring/farmland/farmland.png</field>
<field name="footprint_w">2</field><field name="footprint_h">2</field><field name="sequence">310</field>
<field name="footprint_w">2</field> <field name="footprint_h">2</field><field name="sequence">310</field>
<field name="cost_gold">10</field>
<field name="flat">1</field>
<field name="desc">春季播种的耕地,定期产出粮食作物。</field>
<field name="stat_level">1</field><field name="stat_hp">60</field>
@@ -138,7 +162,8 @@
<record id="bi_fence" model="yt.world.build.item">
<field name="code">fence</field><field name="name">栅栏</field>
<field name="category">facility</field><field name="icon">assets/buildings/spring/fence/fence.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">320</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">320</field>
<field name="cost_gold">8</field>
<field name="flat">1</field>
<field name="desc">圈定范围的木质围栏,划分地块边界。</field>
<field name="stat_level">1</field><field name="stat_hp">50</field>
@@ -147,7 +172,8 @@
<record id="bi_wall" model="yt.world.build.item">
<field name="code">wall</field><field name="name">围墙</field>
<field name="category">facility</field><field name="icon">assets/buildings/spring/wall/wall.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">330</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">330</field>
<field name="cost_gold">15</field>
<field name="flat">1</field>
<field name="desc">石砌围墙,提供防御与领地分隔。</field>
<field name="stat_level">1</field><field name="stat_hp">200</field>
@@ -156,7 +182,8 @@
<record id="bi_storage" model="yt.world.build.item">
<field name="code">storage</field><field name="name">存储箱</field>
<field name="category">facility</field><field name="icon">assets/buildings/spring/storage/storage.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">340</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">340</field>
<field name="cost_gold">12</field>
<field name="flat">1</field>
<field name="desc">存放物资的木箱,扩充本地仓储上限。</field>
<field name="stat_level">1</field><field name="stat_hp">70</field>
@@ -165,7 +192,8 @@
<record id="bi_well" model="yt.world.build.item">
<field name="code">well</field><field name="name">水井</field>
<field name="category">build</field><field name="icon">assets/buildings/default/props/well_r.png</field>
<field name="footprint_w">2</field><field name="footprint_h">2</field><field name="sequence">350</field>
<field name="footprint_w">2</field> <field name="footprint_h">2</field><field name="sequence">350</field>
<field name="cost_gold">15</field>
<field name="flat">0</field>
<field name="desc">汲取地下水的圆井,保障饮水与灌溉。_r/_l 为同一水井的两种朝向。</field>
<field name="stat_level">1</field><field name="stat_hp">90</field>
@@ -174,7 +202,8 @@
<record id="bi_school" model="yt.world.build.item">
<field name="code">school</field><field name="name">学堂</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/school/school_l.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">380</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">380</field>
<field name="cost_gold">90</field>
<field name="flat">0</field>
<field name="desc">授业解惑之所,研读典籍提升领地科研与基础教学产出。</field>
<field name="stat_level">1</field><field name="stat_hp">100</field>
@@ -183,7 +212,8 @@
<record id="bi_library" model="yt.world.build.item">
<field name="code">library</field><field name="name">图书馆</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/library/library_l.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">360</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">360</field>
<field name="cost_gold">110</field>
<field name="flat">0</field>
<field name="desc">收藏典籍与卷轴,提升领地科研与知识产出。</field>
<field name="stat_level">1</field><field name="stat_hp">110</field>
@@ -192,10 +222,29 @@
<record id="bi_barracks" model="yt.world.build.item">
<field name="code">barracks</field><field name="name">兵营</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/barracks/barracks_l.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">370</field>
<field name="footprint_w">3</field> <field name="footprint_h">3</field><field name="sequence">370</field>
<field name="cost_gold">130</field>
<field name="flat">0</field>
<field name="desc">训练士兵与储备武备的军事建筑,提升领地军事产出。</field>
<field name="stat_level">1</field><field name="stat_hp">130</field>
<field name="stat_yield" eval="{'military': 4}"/>
</record>
<record id="bi_guild" model="yt.world.build.item">
<field name="code">guild</field><field name="name">公会</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/guild/guild_l.png</field>
<field name="footprint_w">2</field><field name="footprint_h">2</field><field name="sequence">390</field>
<field name="cost_gold">100</field>
<field name="desc">冒险者与住民集结之所,可发布委托与招募成员。</field>
<field name="stat_level">1</field><field name="stat_hp">140</field>
<field name="stat_yield" eval="{'prestige': 2, 'coin': 1}"/>
</record>
<record id="bi_village" model="yt.world.build.item">
<field name="code">village</field><field name="name">村庄</field>
<field name="category">build</field><field name="icon">assets/buildings/spring/village/village_l.png</field>
<field name="footprint_w">3</field><field name="footprint_h">3</field><field name="sequence">400</field>
<field name="cost_gold">200</field>
<field name="desc">族群聚落的核心,提升人口上限与基础产出。</field>
<field name="stat_level">1</field><field name="stat_hp">150</field>
<field name="stat_yield" eval="{'population': 6, 'food': 1}"/>
</record>
</odoo>
@@ -32,6 +32,8 @@ class YtWorldBuildItem(models.Model):
help='建造时占据的网格列数')
footprint_h = fields.Integer(string='占位高(格)', default=1,
help='建造时占据的网格行数')
cost_gold = fields.Integer(string='金币花费', default=0,
help='建造一次该建筑所需金币;0 表示免费')
sequence = fields.Integer(string='排序', default=100)
active = fields.Boolean(string='启用', default=True)
world_id = fields.Many2one('yt.world', string='关联世界',
Binary file not shown.
+18
View File
@@ -162,6 +162,24 @@ const API = {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// 建造扣费(落库前调用):扣成功才允许放置建筑。
// 返回 {ok:true, cost, gold} 或 {ok:false, error:'no_gold'|'auth'|'network', need, have}
async chargeBuild(code, count = 1) {
try {
const r0 = await fetch(_worldUrl('/yt_world/api/micro/build/charge'), {
method: 'POST', credentials: _wcred,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, count }),
});
if (r0.redirected || (r0.url && r0.url.indexOf('/web/login') >= 0)) {
return { ok: false, error: 'auth' };
}
const j = await r0.json();
return j && j.ok === false ? j : (j || { ok: true });
} catch (e) {
return { ok: false, error: 'network', detail: String(e && e.message || e) };
}
},
// ---------- 世界数据 / 包裹(后端接口预留:yt_world 暂未实现路由,前端 mock 兜底) ----------
async worldData() {
try {
+186 -34
View File
@@ -378,6 +378,8 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
.mv-cell .mv-ico { font-size: 30px; line-height: 1; pointer-events: none; }
.mv-cell .mv-ico-img { width: 44px; height: 44px; object-fit: contain; pointer-events: none; border-radius: 6px; }
.mv-cell .mv-nm { font-size: 11px; color: #9fb3c8; pointer-events: none; }
.mv-cost { font-size: 10px; color: #ffd54f; pointer-events: none; font-weight: 600; }
.mv-cost.free { color: #7fdca0; }
.mv-empty-cat { height: 100%; display: flex; align-items: center; justify-content: center; padding: 0 14px; color: #6c8098; font-size: 13px; text-align: center; }
.mv-clear {
background: rgba(255,90,90,.1); border: 1px solid rgba(255,90,90,.35);
@@ -587,6 +589,10 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
#appModal .mailTitle { color: #7cc7ff; font-weight: 600; margin-bottom: 4px; }
#appModal .mailMeta { color: #778; font-size: 11px; margin-bottom: 4px; }
#appModal .settingRow { display: flex; justify-content: space-between; align-items: center; margin: 10px 0; font-size: 13px; }
#appModal .toggleSwitch { position: relative; width: 42px; height: 22px; border-radius: 11px; background: rgba(255,255,255,.12); cursor: pointer; transition: background .2s; }
#appModal .toggleSwitch.active { background: linear-gradient(135deg,#8b5cf6,#4aafff); }
#appModal .toggleSwitch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); transition: transform .2s; }
#appModal .toggleSwitch.active::after { transform: translateX(20px); }
#appModal .mailTabs { display: flex; gap: 6px; margin-bottom: 12px; }
#appModal .mailTab { flex: 1; text-align: center; padding: 6px 0; font-size: 13px; color: #8899aa; background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.1); border-radius: 8px; cursor: pointer; transition: .15s; }
#appModal .mailTab.active { color: #fff; background: rgba(139,92,246,.25); border-color: rgba(139,92,246,.5); }
@@ -1152,6 +1158,8 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
</div>
</div>
<audio id="bgmAudio" src="assets/audio/bgm.mp3" loop preload="auto" style="display:none;"></audio>
<script>
// ============ 配置 ============
const MAP_RADIUS = 2; // 主世界六边形半径:比原始(3)缩小一圈,最外层锁定为边界环
@@ -1752,7 +1760,7 @@ const MICRO_BUILD_CATS = [
{ id: 'nature', name: '自然' },
];
const MICRO_BUILD_ITEMS = [
{ id: 'road_auto', cat: 'road', icon: 'assets/roads/5.png', name: '道路', autoRoad: true },
{ id: 'road_auto', cat: 'road', icon: 'assets/roads/5.png', name: '道路', autoRoad: true, costGold: 1 },
{ id: 'road_t', cat: 'road', icon: 'assets/roads/1.png', name: 'T字路口', hidden: true },
{ id: 'road_cross', cat: 'road', icon: 'assets/roads/2.png', name: '十字路口', hidden: true },
{ id: 'road_corner', cat: 'road', icon: 'assets/roads/4.png', name: '弯道', hidden: true },
@@ -1764,22 +1772,22 @@ const MICRO_BUILD_ITEMS = [
// 建筑 / 设施 目录(兜底:未登录或 Odoo 未提供时直接用此;登录后由 /yt_world/api/micro/catalog 覆盖)。
// flat=true 表示平面俯视,岛上压成菱形贴地;否则为 3/4 立绘,竖直站立。
const MICRO_BUILD_EXTRA = [
{ id: 'cottage', cat: 'build', icon: 'assets/buildings/spring/cottage/cottage_l.png?v=6', name: '农舍', desc: '村民起居的基本居所,提供人口上限与夜间休息。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 120, statYield: { population: 4 } },
{ id: 'windmill', cat: 'build', icon: 'assets/buildings/spring/windmill/windmill_l.png?v=5', name: '风车', desc: '利用风力磨粉与提水,提升粮食加工效率。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 110, statYield: { flour: 2 } },
{ id: 'blacksmith', cat: 'build', icon: 'assets/buildings/spring/blacksmith/blacksmith_l.png?v=5',name: '铁匠铺', desc: '锻造工具与武器,产出金属制品。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 130, statYield: { metal: 2 } },
{ id: 'grocery', cat: 'build', icon: 'assets/buildings/spring/grocery/grocery_l.png?v=5', name: '杂货铺', desc: '交易日用物资的小型商铺,促进经济流通。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 90, statYield: { coin: 2 } },
{ id: 'temple', cat: 'build', icon: 'assets/buildings/spring/temple/temple_l.png?v=4', name: '神庙', desc: '供奉信仰的场所,提升居民精神状态与领地声望。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 160, statYield: { prestige: 3 } },
{ id: 'clinic', cat: 'build', icon: 'assets/buildings/spring/clinic/clinic_l.png?v=5', name: '医务室', desc: '诊治伤病,恢复居民生命值。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 100, statYield: { heal: 2 } },
{ id: 'restaurant', cat: 'build', icon: 'assets/buildings/spring/restaurant/restaurant_l.png?v=5',name: '酒馆', desc: '烹制料理,为居民提供饱食与心情加成。', flat: false, w: 2, h: 3, statLevel: 1, statHp: 95, statYield: { satiety: 3 } },
{ id: 'pasture', cat: 'business', icon: 'assets/buildings/spring/pasture/pasture_l.png?v=13', name: '牧场', desc: '放养牲畜,稳定产出肉奶类资源。', flat: false, w: 4, h: 4, statLevel: 1, statHp: 105, statYield: { meat: 2, milk: 1 } },
{ id: 'blackmarket', cat: 'build', icon: 'assets/buildings/spring/blackmarket/blackmarket_l.png?v=7',name:'商城', desc: '灰色交易点,可换取稀有物资但伴随风险。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 80, statYield: { rare: 1 } },
{ id: 'farmland', cat: 'business', icon: 'assets/buildings/spring/farmland/farmland.png', name: '农田', desc: '春季播种的耕地,定期产出粮食作物。', flat: true, w: 2, h: 2, statLevel: 1, statHp: 60, statYield: { food: 3 } },
{ id: 'well', cat: 'build', icon: 'assets/buildings/default/props/well_l.png', name: '水井', desc: '汲取地下水的圆井,保障饮水与灌溉。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 90, statYield: { water: 2 } },
{ id: 'school', cat: 'build', icon: 'assets/buildings/spring/school/school_l.png?v=6', name: '学堂', desc: '授业解惑之所,研读典籍提升领地科研与基础教学产出。', flat: false, w: 3, h: 2, statLevel: 1, statHp: 100, statYield: { research: 3, prestige: 1 } },
{ id: 'library', cat: 'build', icon: 'assets/buildings/spring/library/library_l.png?v=5', name: '图书馆', desc: '收藏典籍与卷轴,提升领地科研与知识产出。', flat: false, w: 2, h: 3, statLevel: 1, statHp: 110, statYield: { research: 4 } },
{ id: 'barracks', cat: 'build', icon: 'assets/buildings/spring/barracks/barracks_l.png?v=6', name: '兵营', desc: '训练士兵与储备武备的军事建筑,提升领地军事产出。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 130, statYield: { military: 4 } },
{ id: 'guild', cat: 'build', icon: 'assets/buildings/spring/guild/guild_l.png?v=4', name: '公会', desc: '冒险者与住民集结之所,可发布委托与招募成员。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 140, statYield: { prestige: 2, coin: 1 } },
{ id: 'village', cat: 'build', icon: 'assets/buildings/spring/village/village_l.png?v=5', name: '村庄', desc: '族群聚落的核心,提升人口上限与基础产出。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 150, statYield: { population: 6, food: 1 } },
{ id: 'cottage', cat: 'build', icon: 'assets/buildings/spring/cottage/cottage_l.png?v=6', name: '农舍', desc: '村民起居的基本居所,提供人口上限与夜间休息。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 120, statYield: { population: 4 }, costGold: 20 },
{ id: 'windmill', cat: 'build', icon: 'assets/buildings/spring/windmill/windmill_l.png?v=5', name: '风车', desc: '利用风力磨粉与提水,提升粮食加工效率。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 110, statYield: { flour: 2 }, costGold: 60 },
{ id: 'blacksmith', cat: 'build', icon: 'assets/buildings/spring/blacksmith/blacksmith_l.png?v=5',name: '铁匠铺', desc: '锻造工具与武器,产出金属制品。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 130, statYield: { metal: 2 }, costGold: 80 },
{ id: 'grocery', cat: 'build', icon: 'assets/buildings/spring/grocery/grocery_l.png?v=5', name: '杂货铺', desc: '交易日用物资的小型商铺,促进经济流通。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 90, statYield: { coin: 2 }, costGold: 50 },
{ id: 'temple', cat: 'build', icon: 'assets/buildings/spring/temple/temple_l.png?v=4', name: '神庙', desc: '供奉信仰的场所,提升居民精神状态与领地声望。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 160, statYield: { prestige: 3 }, costGold: 100 },
{ id: 'clinic', cat: 'build', icon: 'assets/buildings/spring/clinic/clinic_l.png?v=5', name: '医务室', desc: '诊治伤病,恢复居民生命值。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 100, statYield: { heal: 2 }, costGold: 55 },
{ id: 'restaurant', cat: 'build', icon: 'assets/buildings/spring/restaurant/restaurant_l.png?v=5',name: '酒馆', desc: '烹制料理,为居民提供饱食与心情加成。', flat: false, w: 2, h: 3, statLevel: 1, statHp: 95, statYield: { satiety: 3 }, costGold: 45 },
{ id: 'pasture', cat: 'business', icon: 'assets/buildings/spring/pasture/pasture_l.png?v=13', name: '牧场', desc: '放养牲畜,稳定产出肉奶类资源。', flat: false, w: 4, h: 4, statLevel: 1, statHp: 105, statYield: { meat: 2, milk: 1 }, costGold: 70 },
{ id: 'blackmarket', cat: 'build', icon: 'assets/buildings/spring/blackmarket/blackmarket_l.png?v=7',name:'商城', desc: '灰色交易点,可换取稀有物资但伴随风险。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 80, statYield: { rare: 1 }, costGold: 120 },
{ id: 'farmland', cat: 'business', icon: 'assets/buildings/spring/farmland/farmland.png', name: '农田', desc: '春季播种的耕地,定期产出粮食作物。', flat: true, w: 2, h: 2, statLevel: 1, statHp: 60, statYield: { food: 3 }, costGold: 10 },
{ id: 'well', cat: 'build', icon: 'assets/buildings/default/props/well_l.png', name: '水井', desc: '汲取地下水的圆井,保障饮水与灌溉。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 90, statYield: { water: 2 }, costGold: 15 },
{ id: 'school', cat: 'build', icon: 'assets/buildings/spring/school/school_l.png?v=6', name: '学堂', desc: '授业解惑之所,研读典籍提升领地科研与基础教学产出。', flat: false, w: 3, h: 2, statLevel: 1, statHp: 100, statYield: { research: 3, prestige: 1 }, costGold: 90 },
{ id: 'library', cat: 'build', icon: 'assets/buildings/spring/library/library_l.png?v=5', name: '图书馆', desc: '收藏典籍与卷轴,提升领地科研与知识产出。', flat: false, w: 2, h: 3, statLevel: 1, statHp: 110, statYield: { research: 4 }, costGold: 110 },
{ id: 'barracks', cat: 'build', icon: 'assets/buildings/spring/barracks/barracks_l.png?v=6', name: '兵营', desc: '训练士兵与储备武备的军事建筑,提升领地军事产出。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 130, statYield: { military: 4 }, costGold: 130 },
{ id: 'guild', cat: 'build', icon: 'assets/buildings/spring/guild/guild_l.png?v=4', name: '公会', desc: '冒险者与住民集结之所,可发布委托与招募成员。', flat: false, w: 2, h: 2, statLevel: 1, statHp: 140, statYield: { prestige: 2, coin: 1 }, costGold: 100 },
{ id: 'village', cat: 'build', icon: 'assets/buildings/spring/village/village_l.png?v=5', name: '村庄', desc: '族群聚落的核心,提升人口上限与基础产出。', flat: false, w: 3, h: 3, statLevel: 1, statHp: 150, statYield: { population: 6, food: 1 }, costGold: 200 },
// 生物分类:9 种族角色(点击进入"点地面放置"模式,不进入建筑模式)
{ id: 'race_human', cat: 'bio', icon: 'assets/characters/human/human_thumb.png', name: '人族', isChar: true, race: 'human' },
{ id: 'race_demon', cat: 'bio', icon: 'assets/characters/demon/demon_thumb.png', name: '妖族', isChar: true, race: 'demon' },
@@ -1933,6 +1941,7 @@ async function microLoadCatalog() {
if (it.flat != null) t.flat = it.flat;
if (it.statLevel) t.statLevel = it.statLevel;
if (it.statHp) t.statHp = it.statHp;
if (it.costGold != null) t.costGold = it.costGold; // 后端价格优先(单一数据源)
if (it.statYield && Object.keys(it.statYield).length) t.statYield = it.statYield;
if (Array.isArray(it.actions) && it.actions.length) t.actions = it.actions; // 后端动作优先
});
@@ -2477,6 +2486,7 @@ let microActionHover = null; // 当前悬停的浮动按钮 id
let microRoadPainting = false; // 自动道路刷子左键拖拽铺路中
let microRoadErasing = false; // 自动道路刷子右键拖拽擦除中
let microRoadLastCell = null; // 上一次铺/擦的格子 key(避免同格重复)
let microRoadPaintBefore = 0; // 本次道路刷子绘画前的 road_auto 数量(用于结算费用与撤销)
let microGhostFrozen = false; // 普通素材放置:点击后 ghost 是否已冻结(静止),需点按钮操作
// ============ 地图生成 ============
@@ -3780,7 +3790,10 @@ function onMicroDown(e) {
// 自动道路/栅栏刷子:左键拖拽连续铺
if (microBuildSel === 'road_auto') {
const cell = microCellAt(p);
if (cell) { microRoadPainting = true; microRoadLastCell = null; microPaintRoadCell(cell.i, cell.j); }
if (cell) {
microRoadPaintBefore = microBuildList().filter(b => microIsRoadAuto(b)).length;
microRoadPainting = true; microRoadLastCell = null; microPaintRoadCell(cell.i, cell.j);
}
return;
}
// 普通素材:点击 → 先冻结(静止住)在点击格;已冻结时点击空白则重新固定到该格
@@ -3952,13 +3965,39 @@ function onMicroMove(e) {
microSwipe.x = microSwipe.x0 + ox;
microSwipe.y = microSwipe.y0 + oy;
}
function onMicroUp(e) {
async function onMicroUp(e) {
if (!microState) return;
// 建造模式:松手判定——拖动移动已放置建筑 / 点击落地 / 点击选中编辑 / 平移
if (microBuildMode) {
const p = getMicroPoint(e);
const moved = microPanStart ? Math.hypot(p.x - microPanStart.x, p.y - microPanStart.y) : 0;
if (microRoadPainting) { microRoadPainting = false; microRoadLastCell = null; scheduleBuildSave(); microToast('道路已铺设'); }
if (microRoadPainting) {
microRoadPainting = false; microRoadLastCell = null;
const list = microBuildList();
const after = list.filter(b => microIsRoadAuto(b)).length;
const added = after - microRoadPaintBefore;
if (added > 0) {
const res = await API.chargeBuild('road_auto', added);
if (!res || !res.ok) {
// 扣费失败:撤销本次新增的道路(从末尾移除 added 个 road_auto)
let toRemove = added;
for (let k = list.length - 1; k >= 0 && toRemove > 0; k--) {
if (microIsRoadAuto(list[k])) { list.splice(k, 1); toRemove--; }
}
microAutoRoadCells();
if (res && res.error === 'no_gold') microToast('金币不足,道路未铺设(需 🪙' + res.need + ')');
else if (res && res.error === 'auth') microToast('铺设需登录后扣费');
else microToast('扣费失败,道路未铺设');
microRoadPaintBefore = 0;
return;
}
if (microChar) microChar.gold = res.gold;
microRenderCharHud();
}
microRoadPaintBefore = 0;
scheduleBuildSave(); microToast('道路已铺设');
return;
}
else if (microRoadErasing) { microRoadErasing = false; microRoadLastCell = null; scheduleBuildSave(); }
else if (microEditDrag) {
microEditDrag = false; scheduleBuildSave(); // 已实时跟随,松手即落定
@@ -5437,6 +5476,75 @@ function applyWorldState(ws) {
}
if (microState && microState.tile) { microAutoRoadCells(); microSanitizeBuildings(); } // 加载后重算道路并清理越界建筑
}
// 背景音乐本地偏好(localStorage)
function loadWorldBGMSetting() {
try { return localStorage.getItem('world_bgm_enabled') !== 'false'; }
catch (e) { return true; }
}
function saveWorldBGMSetting(enabled) {
try { localStorage.setItem('world_bgm_enabled', enabled ? 'true' : 'false'); } catch (e) {}
}
function applyBGMSetting(showToastOnError = false) {
const audio = document.getElementById('bgmAudio');
if (!audio) return;
const enabled = loadWorldBGMSetting();
if (enabled) {
if (audio.paused) {
const p = audio.play();
if (p && typeof p.catch === 'function') {
p.catch(err => {
console.warn('BGM 播放失败:', err.name || err);
// 自动播放被浏览器阻止时提示用户
if (showToastOnError) toast('音频尚未就绪,请稍后再试或点击屏幕任意位置');
});
}
}
} else {
audio.pause();
}
}
let bgmInteracted = false;
function tryPlayBGMOnce() {
if (bgmInteracted) return;
bgmInteracted = true;
const audio = document.getElementById('bgmAudio');
if (!audio) return;
// 必须在用户手势(首次 click/keydown/touchstart)内同步触发播放,
// 不可推迟到异步事件(canplaythrough 等已脱离手势上下文,会被浏览器拦截)。
if (loadWorldBGMSetting()) {
const p = audio.play();
if (p && typeof p.catch === 'function') {
p.catch(() => {
// 首次手势已解锁播放权限;若因资源尚未就绪被拒,资源就绪后自动开始
if (audio.readyState < 3) {
const onReady = () => {
audio.removeEventListener('canplay', onReady);
if (loadWorldBGMSetting() && audio.paused) audio.play().catch(() => {});
};
audio.addEventListener('canplay', onReady, { once: true });
audio.load();
}
});
}
}
}
function initBGM() {
const audio = document.getElementById('bgmAudio');
if (!audio) return;
audio.volume = 0.35;
// 预加载音频
try { audio.load(); } catch (e) {}
// 进入页面立即尝试自动播放:浏览器若已对该域名授予自动播放许可(用户此前有交互),
// 这里会直接出声;无需等任何点击。
if (loadWorldBGMSetting()) {
applyBGMSetting();
}
// 兜底:若上述自动播放被浏览器拦截(如 Safari 严格策略),则在用户首次交互时补播。
['click', 'keydown', 'touchstart'].forEach(evt => {
document.addEventListener(evt, tryPlayBGMOnce, { once: true, passive: true });
});
}
// 相机/UI 偏好收集 + 防抖保存
function collectWorldSettings() {
return { camera: { x: camera.x, y: camera.y, zoom: camera.zoom } };
@@ -5759,7 +5867,7 @@ function microHitBuilding(b, cell) {
return cell.i >= b.i && cell.i < b.i + c.ew && cell.j >= b.j && cell.j < b.j + c.eh;
}
// 左键点击落地:在 ghost 锚点处放置新副本(仅当合法)
function microCommitPlace() {
async function microCommitPlace() {
if (!microBuildSel) return;
const g = microComputeGhost();
if (!g || !g.valid) { microToast('这里不能建造(红色区域)'); return; }
@@ -5779,11 +5887,29 @@ function microCommitPlace() {
microBuildList().push({ i: g.i, j: g.j, type: microBuildSel, rot: microBuildRot, w: it.w || 1, h: it.h || 1, level: 1, exp: 0, hp: (it.statHp || 100), _charCid: cid });
microSaveCreatures();
scheduleBuildSave();
return;
return true;
}
// 非生物建筑:落库前先扣金币(后端校验余额,失败则不放置)
const cost = (it && (it.costGold || 0)) || 0;
if (cost > 0) {
const res = await API.chargeBuild(microBuildSel, 1);
if (!res || !res.ok) {
if (res && res.error === 'no_gold') {
microToast('金币不足(需 🪙' + res.need + ',现有 🪙' + res.have + ')');
} else if (res && res.error === 'auth') {
microToast('建造需登录后扣费');
} else {
microToast('扣费失败,无法建造');
}
return;
}
if (microChar) microChar.gold = res.gold;
microRenderCharHud();
}
microBuildList().push({ i: g.i, j: g.j, type: microBuildSel, rot: microBuildRot, w: it.w || 1, h: it.h || 1, level: 1, exp: 0, hp: (it.statHp || 100) });
emitTaskEvent('build_placed', { code: microBuildSel });
scheduleBuildSave();
return true;
}
// 右键:命中建筑→删除;否则取消当前选择/编辑
function microBuildRightClick(p) {
@@ -5936,18 +6062,20 @@ function microActionHit(p) {
return null;
}
// 执行浮动按钮动作
function microDoAction(id) {
async function microDoAction(id) {
if (id === 'rotL') { microRotateGhost(-90); return; }
if (id === 'rotR') { microRotateGhost(90); return; }
if (id === 'move') { microGhostFrozen = false; microToast('移动模式:移动鼠标,点击再次固定'); return; }
if (id === 'confirm') {
if (microEditRef) { microEditRef = null; microToast('已确认'); }
else if (microBuildSel) {
// 放置后同样回到面板重选,不跟手(与取消一致)
microCommitPlace();
microGhostFrozen = false;
microBuildSel = null; clearBuildSelUI(); microRenderBuildPanel();
microToast('已放置,重新选择建筑');
// 放置后同样回到面板重选,不跟手(与取消一致);先扣费放置,成功才清选择
const ok = await microCommitPlace();
if (ok) {
microGhostFrozen = false;
microBuildSel = null; clearBuildSelUI(); microRenderBuildPanel();
microToast('已放置,重新选择建筑');
}
}
return;
}
@@ -6026,8 +6154,11 @@ function microRenderBuildPanel() {
gridEl.innerHTML = items.length ? items.map(it => {
const isImg = typeof it.icon === 'string' && /\.(png|webp|jpg|jpeg)(\?.*)?$/i.test(it.icon);
const ico = isImg ? `<img src="${it.icon}" class="mv-ico-img" alt="">` : `<span class="mv-ico">${it.icon}</span>`;
const cost = (!it.isChar) ? (it.costGold || 0) : null;
const costHtml = cost == null ? '' :
(cost > 0 ? `<span class="mv-cost">🪙 ${cost}</span>` : `<span class="mv-cost free">免费</span>`);
return `<button type="button" class="mv-cell${it.id === microBuildSel ? ' active' : ''}" data-build="${it.id}" title="${it.name}">
${ico}<span class="mv-nm">${it.name}</span>
${ico}<span class="mv-nm">${it.name}</span>${costHtml}
</button>`;
}).join('') : '<div class="mv-empty-cat">当前地块不可用此类素材</div>';
gridEl.querySelectorAll('.mv-cell').forEach(el => {
@@ -6438,6 +6569,7 @@ function setupMicroNameEdit() {
}
function init() {
initBGM();
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight - BOTTOM_BAR_H;
canvas.classList.add('canDrag');
@@ -6888,12 +7020,13 @@ if (typeof fitWorld === 'function') fitWorld();
// 设置
const btnSettings = document.getElementById('btnSettings');
if (btnSettings) btnSettings.addEventListener('click', () => {
const bgmEnabled = loadWorldBGMSetting();
openAppModal('⚙️ 设置', `
<div class="settingRow"><span>背景音乐</span><span>开</span></div>
<div class="settingRow"><span>音效</span><span>开</span></div>
<div class="settingRow"><span>通知推送</span><span>开</span></div>
<div class="settingRow"><span>画质</span><span>高</span></div>
<div class="settingRow"><span>语言</span><span>简体中文</span></div>
<div class="settingRow"><span>背景音乐</span><span class="toggleSwitch${bgmEnabled ? ' active' : ''}" id="bgmToggle"></span></div>
<div class="settingRow"><span>音效</span><span class="toggleSwitch active" id="sfxToggle"></span></div>
<div class="settingRow"><span>通知推送</span><span class="toggleSwitch active" id="notifyToggle"></span></div>
<div class="settingRow"><span>画质</span><span style="color:#e0e0e0;">高</span></div>
<div class="settingRow"><span>语言</span><span style="color:#e0e0e0;">简体中文</span></div>
<div style="height:1px;background:rgba(255,255,255,.08);margin:12px 0;"></div>
<div style="font-size:13px;color:#8899aa;margin-bottom:6px;">兑换码</div>
<div style="display:flex;gap:8px;">
@@ -6906,6 +7039,25 @@ if (typeof fitWorld === 'function') fitWorld();
<div style="height:1px;background:rgba(255,255,255,.08);margin:12px 0;"></div>
<button id="uaWebsiteBtn" style="width:100%;padding:8px;background:rgba(139,92,246,.15);border:1px solid rgba(139,92,246,.35);color:#cbb4ff;border-radius:8px;cursor:pointer;font-size:13px;">🌐 官网 / 社区</button>
`);
const bgmToggle = document.getElementById('bgmToggle');
if (bgmToggle) bgmToggle.onclick = () => {
const now = bgmToggle.classList.toggle('active');
saveWorldBGMSetting(now);
const audio = document.getElementById('bgmAudio');
if (now && audio) {
// 开启时先确保音频已加载,再尝试播放
audio.load();
const p = audio.play();
if (p && typeof p.catch === 'function') {
p.catch(err => {
console.warn('BGM 开关播放失败:', err.name || err);
toast('音频加载中,请稍后再试');
});
}
} else if (audio) {
audio.pause();
}
};
const redeemBtn = document.getElementById('redeemBtn');
const redeemInput = document.getElementById('redeemInput');
const redeemMsg = document.getElementById('redeemMsg');