This commit is contained in:
李鹏宇
2026-09-24 10:19:53 +08:00
parent ef9a32f954
commit e93621ba02
9 changed files with 307 additions and 885 deletions
+18
View File
@@ -44,6 +44,24 @@ BUILD_SPECS = {
}
def level_mult(level):
"""建筑等级产出倍率:L1=1倍、L2=2倍……(线性,升级即等比提升)。
所有结算/对账/速率计算都必须乘上此倍率,保证升级带来实际收益。"""
try:
lv = int(level or 1)
except (TypeError, ValueError):
lv = 1
return max(1, lv)
def scaled(d, level):
"""把基础产出字典按等级缩放:{k: v} → {k: v * level_mult(level)}。"""
if not d:
return {}
m = level_mult(level)
return {k: (v or 0) * m for k, v in d.items()}
def spec_defined(code):
"""某建筑编码是否在 BUILD_SPECS 中有明确定义。"""
return code in BUILD_SPECS
+4
View File
@@ -984,6 +984,10 @@ class YtWorldApi(http.Controller):
b['level'] = (b.get('level') or 1) + 1
b['exp'] = (b.get('exp') or 0) + 50
tile.micro_world_id.sudo().write({'build_items': items})
# 升级改变实际产出(人口/每分钟速率)→ 立即重算世界库存,
# 让前端侧栏"资源产出"马上体现提升(幂等 SET,不怕重复调用)
world.sudo()._recalc_rates()
world.sudo()._reconcile_instant()
return {'level': b['level'], 'exp': b['exp']}
return None
+6 -6
View File
@@ -110,8 +110,8 @@ class YtWorld(models.Model):
def _recalc_rates(self):
"""重新计算世界各资源总产出速率(每分钟),写回 stock.rate_per_min。
速率直接从代码规格 BUILD_SPECS 读取,不依赖 noupdate 的 XML 字段。"""
from odoo.addons.yt_world.build_spec import get_spec
速率直接从代码规格 BUILD_SPECS 读取并按建筑等级缩放,不依赖 noupdate 的 XML 字段。"""
from odoo.addons.yt_world.build_spec import get_spec, level_mult
Stock = self.env['yt.world.stock'].sudo()
Instance = self.env['yt.world.build.instance'].sudo()
for world in self:
@@ -124,7 +124,7 @@ class YtWorld(models.Model):
for micro in micros:
for inst in Instance.search([('micro_id', '=', micro.id)]):
for code, rate in (get_spec(inst.code).get('stat_yield_tick') or {}).items():
rates[code] = rates.get(code, 0.0) + (rate or 0)
rates[code] = rates.get(code, 0.0) + (rate or 0) * level_mult(inst.level)
# 确保每种有产出的资源都有 stock 行
for code, rate in rates.items():
stock = Stock.get_or_create(world.id, code)
@@ -136,9 +136,9 @@ class YtWorld(models.Model):
return True
def _reconcile_instant(self):
"""对账即时属性(如 population):把世界库存 amount 设为当前建筑实例的合计。
"""对账即时属性(如 population):把世界库存 amount 设为当前建筑实例的合计(按等级缩放)。
幂等 SET,避免与实例 create/unlink 钩子的增量 ± 叠加产生漂移。"""
from odoo.addons.yt_world.build_spec import get_spec
from odoo.addons.yt_world.build_spec import get_spec, scaled
Stock = self.env['yt.world.stock'].sudo()
Instance = self.env['yt.world.build.instance'].sudo()
for world in self:
@@ -149,7 +149,7 @@ class YtWorld(models.Model):
micros._sync_build_instances()
for micro in micros:
for inst in Instance.search([('micro_id', '=', micro.id)]):
for code, amt in (get_spec(inst.code).get('stat_yield_instant') or {}).items():
for code, amt in scaled(get_spec(inst.code).get('stat_yield_instant'), inst.level).items():
totals[code] = totals.get(code, 0.0) + (amt or 0)
for code, amt in totals.items():
stock = Stock.get_or_create(world.id, code)
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.addons.yt_world.build_spec import get_spec
from odoo.addons.yt_world.build_spec import get_spec, scaled
class YtWorldBuildInstance(models.Model):
@@ -47,32 +47,33 @@ class YtWorldBuildInstance(models.Model):
for rec in self:
world = rec.micro_id.tile_id.world_id
if world:
captured.append((world.id, get_spec(rec.code)))
captured.append((world.id, get_spec(rec.code), rec.level))
res = super().unlink()
Stock = self.env['yt.world.stock'].sudo()
for world_id, spec in captured:
for code, rate in (spec.get('stat_yield_tick') or {}).items():
for world_id, spec, level in captured:
for code, rate in scaled(spec.get('stat_yield_tick'), level).items():
if rate:
stock = Stock.get_or_create(world_id, code)
stock.rate_per_min = max(0.0, (stock.rate_per_min or 0.0) - rate)
for code, amt in (spec.get('stat_yield_instant') or {}).items():
for code, amt in scaled(spec.get('stat_yield_instant'), level).items():
if amt:
stock = Stock.get_or_create(world_id, code)
stock.amount = max(0.0, (stock.amount or 0.0) - amt)
return res
def _apply_spec(self, sign):
"""建造/恢复时 sign=+1 把产出加进世界库存;删除前 sign=-1 扣减。"""
"""建造/恢复时 sign=+1 把产出加进世界库存;删除前 sign=-1 扣减。
产出按建筑等级缩放(L1=1倍、L2=2倍……)。"""
world = self.micro_id.tile_id.world_id
if not world:
return
spec = get_spec(self.code)
Stock = self.env['yt.world.stock'].sudo()
for code, rate in (spec.get('stat_yield_tick') or {}).items():
for code, rate in scaled(spec.get('stat_yield_tick'), self.level).items():
if rate:
stock = Stock.get_or_create(world.id, code)
stock.rate_per_min = (stock.rate_per_min or 0.0) + sign * rate
for code, amt in (spec.get('stat_yield_instant') or {}).items():
for code, amt in scaled(spec.get('stat_yield_instant'), self.level).items():
if amt:
stock = Stock.get_or_create(world.id, code)
stock.amount = (stock.amount or 0.0) + sign * amt
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.addons.yt_world.build_spec import get_spec
from odoo.addons.yt_world.build_spec import get_spec, level_mult
class YtWorldTileMicro(models.Model):
@@ -112,7 +112,7 @@ class YtWorldTileMicro(models.Model):
for micro in self:
instances = Instance.search([('micro_id', '=', micro.id)])
for inst in instances:
# 直接从代码规格读取产出,永远最新,不依赖 noupdate 的 XML 字段
# 直接从代码规格读取产出(按建筑等级缩放),永远最新,不依赖 noupdate 的 XML 字段
tick = get_spec(inst.code).get('stat_yield_tick') or {}
if not tick:
continue
@@ -125,7 +125,7 @@ class YtWorldTileMicro(models.Model):
effective = capped * offline_efficiency
this_call = {} # 本轮新增量(在线增量)
for code, rate in tick.items():
add = rate * effective
add = rate * level_mult(inst.level) * effective
if add <= 0:
continue
stored = dict(inst.stored or {})
-556
View File
@@ -1,556 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>鹏宇 · 命盘</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{
background:#0d0b12;
color:#d4c9a8;
font-family:'PingFang SC','Noto Sans SC','Microsoft YaHei',sans-serif;
min-height:100dvh;
overflow-x:hidden;
padding:38px 16px 60px;
display:flex;flex-direction:column;align-items:center
}
.container{max-width:420px;width:100%}
/* header */
.title{
text-align:center;
font-size:22px;
font-weight:600;
letter-spacing:4px;
color:#f0e6c5;
margin-bottom:6px
}
.subtitle{
text-align:center;
font-size:13px;
color:#8a7f68;
letter-spacing:2px;
margin-bottom:28px
}
/* four pillars */
.pillars{
display:flex;
justify-content:center;
gap:10px;
margin-bottom:24px
}
.pillar{
background:linear-gradient(180deg,#1e1927,#13101b);
border:1px solid #3a2f4a;
border-radius:10px;
width:80px;
text-align:center;
padding:10px 0 12px;
position:relative
}
.pillar .p-label{
font-size:12px;
color:#6a5f7a;
letter-spacing:2px;
margin-bottom:4px
}
.pillar .gan{
font-size:32px;
font-weight:700;
color:#f0e6c5;
line-height:1.1
}
.pillar .zhi{
font-size:28px;
font-weight:600;
color:#c9b896;
line-height:1.2;
margin-top:2px
}
.pillar .p-shen{
font-size:12px;
color:#b5975a;
margin-top:4px;
letter-spacing:1px
}
.pillar.self{background:linear-gradient(180deg,#2a1f2e,#1a1420);border-color:#5a3f5a}
.pillar.self .gan{color:#ffd966}
.pillar.self .zhi{color:#e6c88a}
/* highlight bar */
.highlight{
background:linear-gradient(90deg,transparent,#2a2030,transparent);
border:1px solid #3a2f4a;
border-radius:8px;
padding:12px 16px;
text-align:center;
margin-bottom:20px;
font-size:14px;
line-height:1.7;
color:#c9b896
}
.highlight strong{color:#f0e6c5}
.highlight .kill{color:#e05858}
.highlight .yin{color:#58aee0}
/* section */
.sec{
background:linear-gradient(180deg,#1a1522,#100d18);
border:1px solid #2a203a;
border-radius:10px;
padding:16px 14px;
margin-bottom:14px
}
.sec-title{
font-size:15px;
font-weight:600;
color:#f0e6c5;
text-align:center;
letter-spacing:3px;
margin-bottom:12px;
padding-bottom:8px;
border-bottom:1px solid #2a203a
}
/* ten gods grid */
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
.grid-item{
background:#1a1522;
border:1px solid #2a203a;
border-radius:6px;
padding:8px 6px;
text-align:center;
font-size:13px;
line-height:1.6
}
.grid-item .label{color:#6a5f7a;font-size:11px}
.grid-item .val{color:#d4c9a8;font-size:13px}
.grid-item .val.kill{color:#e05858}
.grid-item .val.yin{color:#58aee0}
.grid-item .val.guan{color:#e0a058}
.grid-item .val.cai{color:#58d0a0}
.grid-item .val.shang{color:#c0a0e0}
/* flow */
.flow{
display:flex;
align-items:center;
justify-content:center;
gap:4px;
margin:12px 0 8px;
font-size:15px;
flex-wrap:wrap
}
.flow-item{
background:#1a1522;
border:1px solid #2a203a;
border-radius:6px;
padding:6px 12px;
text-align:center
}
.flow-item .label{font-size:11px;color:#6a5f7a}
.flow-item .val{font-size:16px;font-weight:600}
.flow-item .val.k{color:#e05858}
.flow-item .val.y{color:#58aee0}
.flow-item .val.r{color:#ffd966}
.flow-arrow{color:#3a2f4a;font-size:16px;margin:0 4px}
/* dayun table */
.dayun-table{
width:100%;
border-collapse:collapse;
font-size:13px
}
.dayun-table td{
text-align:center;
padding:7px 4px;
border-bottom:1px solid #1a1522
}
.dayun-table tr:last-child td{border-bottom:none}
.dayun-table .dy-gan{font-weight:600;font-size:16px}
.dayun-table .dy-shen{color:#b5975a;font-size:11px}
.dayun-table .dy-age{color:#6a5f7a;font-size:11px}
.dayun-table tr.current td{background:rgba(88,174,224,.08);border-radius:4px;color:#f0e6c5}
.dayun-table .dy-year{color:#6a5f7a;font-size:11px}
/* hidden stems */
.hidden-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.hidden-item{
background:#1a1522;
border:1px solid #2a203a;
border-radius:6px;
padding:8px;
text-align:center;
font-size:13px;
line-height:1.5
}
.hidden-item .zhi-name{color:#c9b896;font-size:12px;margin-bottom:2px}
.hidden-item .gz{display:flex;justify-content:center;gap:8px;font-size:13px}
.hidden-item .g{color:#d4c9a8}
.hidden-item .s{color:#b5975a;font-size:11px}
/* wuxing */
.wuxing{
display:flex;
justify-content:center;
gap:12px;
margin:8px 0
}
.wx-item{
width:52px;
height:52px;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
flex-direction:column;
font-size:12px;
font-weight:600;
border:2px solid rgba(255,255,255,.08)
}
.wx-item .count{font-size:16px}
.wx-item.jin{background:rgba(224,88,88,.18);border-color:#e05858;color:#e05858}
.wx-item.mu{background:rgba(88,208,160,.18);border-color:#58d0a0;color:#58d0a0}
.wx-item.shui{background:rgba(88,174,224,.18);border-color:#58aee0;color:#58aee0}
.wx-item.huo{background:rgba(224,160,88,.18);border-color:#e0a058;color:#e0a058}
.wx-item.tu{background:rgba(192,160,224,.18);border-color:#c0a0e0;color:#c0a0e0}
/* text paragraph */
.para{font-size:13.5px;line-height:1.9;color:#c9b896;margin-bottom:8px}
.para:last-child{margin-bottom:0}
.para strong{color:#f0e6c5}
.para .kill{color:#e05858}
.para .yin{color:#58aee0}
.para .guan{color:#e0a058}
.para .cai{color:#58d0a0}
/* flow-year section */
.liunian-box{
background:rgba(88,174,224,.04);
border:1px solid rgba(88,174,224,.15);
border-radius:6px;
padding:10px;
margin-top:6px;
font-size:13px;
line-height:1.8;
color:#c9b896
}
/* footer */
.footer{
text-align:center;
font-size:11px;
color:#3a2f4a;
letter-spacing:2px;
margin-top:8px;
padding-top:16px;
border-top:1px solid #1a1522
}
@media(max-width:400px){
.pillars{gap:6px}
.pillar{width:70px;padding:8px 0 10px}
.pillar .gan{font-size:26px}
.pillar .zhi{font-size:22px}
body{padding:28px 10px 40px}
}
</style>
</head>
<body>
<div class="container">
<div class="title">三 命 通 览</div>
<div class="subtitle">庚辰年 · 庚辰月 · 甲寅日 · 癸酉时</div>
<!-- 四柱 -->
<div class="pillars">
<div class="pillar">
<div class="p-label">年</div>
<div class="gan" style="color:#e05858">庚</div>
<div class="zhi">辰</div>
<div class="p-shen">七杀</div>
</div>
<div class="pillar">
<div class="p-label">月</div>
<div class="gan" style="color:#e05858">庚</div>
<div class="zhi">辰</div>
<div class="p-shen">七杀</div>
</div>
<div class="pillar self">
<div class="p-label">日</div>
<div class="gan">甲</div>
<div class="zhi">寅</div>
<div class="p-shen">日主</div>
</div>
<div class="pillar">
<div class="p-label">时</div>
<div class="gan" style="color:#58aee0">癸</div>
<div class="zhi">酉</div>
<div class="p-shen">正印</div>
</div>
</div>
<!-- 杀印相生 -->
<div class="highlight">
<strong>杀印相生格局</strong>
· &nbsp;<span class="kill">庚</span>金七杀生<span class="yin">癸</span>水正印 ·
&nbsp;<span class="yin">癸</span>水正印生<span style="color:#ffd966">甲</span>木日主
</div>
<!-- 你的记忆 -->
<div class="sec">
<div class="sec-title">💡 你说的"两个七杀一个正印"</div>
<div class="grid-3">
<div class="grid-item">
<div class="label">七杀①</div>
<div class="val kill">年干 庚</div>
</div>
<div class="grid-item">
<div class="label">七杀②</div>
<div class="val kill">月干 庚</div>
</div>
<div class="grid-item">
<div class="label">正印</div>
<div class="val yin">时干 癸</div>
</div>
</div>
<div class="para" style="margin-top:8px;text-align:center">
完美对上。之前我按庚金日主算,七杀变丙、正印变己,自然对不上。<br>
<strong>甲木日主,七杀=庚,正印=癸,一字不差</strong>。
</div>
</div>
<!-- 格局详解 -->
<div class="sec">
<div class="sec-title">杀印相生 · 格局评述</div>
<div class="para">
<strong>甲木参天</strong>生于辰月,暮春土旺木气渐衰。天干双透<span class="kill">庚金七杀</span>,克身之力极重,若无救应则身弱杀强、步步维艰。
</div>
<div class="flow">
<div class="flow-item"><div class="label">七杀</div><div class="val k">庚</div></div>
<span class="flow-arrow">→ 生 →</span>
<div class="flow-item"><div class="label">正印</div><div class="val y">癸</div></div>
<span class="flow-arrow">→ 生 →</span>
<div class="flow-item"><div class="label">日主</div><div class="val r">甲</div></div>
</div>
<div class="para">
妙在时柱见<span class="yin">癸水正印贴身相生</span>——
<span class="kill">七杀的压力</span>化作<span class="yin">正印的学习动力</span>,
正印再将养分输送给<span style="color:#ffd966">甲木日主</span>。
压力→学识→成长,<strong>流通无阻</strong>。
</div>
<div class="para">
时支<span class="guan">酉金(辛正官)</span>更为点睛——
杀印相生之后再生官,<strong>压力转化为权威</strong>,
代表最终走向管理岗、有实权有地位。
</div>
<div class="para"><strong>格局核心特质:</strong></div>
<div class="para">① 权威性格 · 不怒自威,天生有领导气场</div>
<div class="para">② 以学促用 · 善于通过学习思考化解问题</div>
<div class="para">③ 后发制人 · 前期压力大,但越走越稳</div>
<div class="para">④ 管理才能 · 既有权威(杀)又有智慧(印)</div>
</div>
<!-- 十神全表 -->
<div class="sec">
<div class="sec-title">十神全表</div>
<div class="grid-2">
<div class="grid-item">
<div class="label">年干</div>
<div class="val kill">庚 · 七杀</div>
</div>
<div class="grid-item">
<div class="label">年支</div>
<div class="val cai">辰 · 偏财</div>
<div style="font-size:11px;color:#6a5f7a">藏戊(偏财) 乙(劫财) 癸(正印)</div>
</div>
<div class="grid-item">
<div class="label">月干</div>
<div class="val kill">庚 · 七杀</div>
</div>
<div class="grid-item">
<div class="label">月支</div>
<div class="val cai">辰 · 偏财</div>
<div style="font-size:11px;color:#6a5f7a">藏戊(偏财) 乙(劫财) 癸(正印)</div>
</div>
<div class="grid-item">
<div class="label">日干</div>
<div class="val" style="color:#ffd966">甲 · 日主</div>
</div>
<div class="grid-item">
<div class="label">日支</div>
<div class="val shang">寅 · 食神</div>
<div style="font-size:11px;color:#6a5f7a">藏甲(比肩) 丙(食神) 戊(偏财)</div>
</div>
<div class="grid-item">
<div class="label">时干</div>
<div class="val yin">癸 · 正印</div>
</div>
<div class="grid-item">
<div class="label">时支</div>
<div class="val guan">酉 · 正官</div>
<div style="font-size:11px;color:#6a5f7a">藏辛(正官)</div>
</div>
</div>
</div>
<!-- 藏干 -->
<div class="sec">
<div class="sec-title">地支藏干</div>
<div class="hidden-grid">
<div class="hidden-item">
<div class="zhi-name">辰(年支/月支)</div>
<div class="gz">
<span class="g"><span style="color:#58d0a0">戊</span> 偏财</span>
<span class="g"><span style="color:#58d0a0">乙</span> 劫财</span>
<span class="g"><span style="color:#58aee0">癸</span> 正印</span>
</div>
</div>
<div class="hidden-item">
<div class="zhi-name">寅(日支)</div>
<div class="gz">
<span class="g"><span style="color:#58d0a0">甲</span> 比肩</span>
<span class="g"><span style="color:#e0a058">丙</span> 食神</span>
<span class="g"><span style="color:#58d0a0">戊</span> 偏财</span>
</div>
</div>
<div class="hidden-item">
<div class="zhi-name">酉(时支)</div>
<div class="gz">
<span class="g"><span style="color:#e0a058">辛</span> 正官</span>
</div>
</div>
</div>
</div>
<!-- 纳音五行 -->
<div class="sec">
<div class="sec-title">纳音五行</div>
<div class="grid-2">
<div class="grid-item">
<div class="label">年柱</div>
<div class="val" style="color:#e05858">庚辰 · 白蜡金</div>
</div>
<div class="grid-item">
<div class="label">月柱</div>
<div class="val" style="color:#e05858">庚辰 · 白蜡金</div>
</div>
<div class="grid-item">
<div class="label">日柱</div>
<div class="val" style="color:#58aee0">甲寅 · 大溪水</div>
</div>
<div class="grid-item">
<div class="label">时柱</div>
<div class="val" style="color:#e05858">癸酉 · 剑锋金</div>
</div>
</div>
<div class="wuxing">
<div class="wx-item jin"><span class="count">4</span>金</div>
<div class="wx-item mu"><span class="count">3</span>木</div>
<div class="wx-item shui"><span class="count">3</span>水</div>
<div class="wx-item huo"><span class="count">1</span>火</div>
<div class="wx-item tu"><span class="count">3</span>土</div>
</div>
<div class="para" style="text-align:center;font-size:12px;margin-top:4px">
<span style="color:#e05858">金旺</span>为杀印之源,<span style="color:#58d0a0">木有寅根</span>得印生,
<span style="color:#58aee0">水火调候</span>俱备,<span style="color:#c0a0e0">土</span>为财星有制
</div>
</div>
<!-- 大运 -->
<div class="sec">
<div class="sec-title">大运 · 顺排</div>
<div class="para" style="text-align:center;font-size:12px;color:#6a5f7a">
阳男顺排 · 约3岁起运 · 每10年一运
</div>
<table class="dayun-table">
<tr>
<td><span class="dy-age">03-12</span></td>
<td><span class="dy-gan" style="color:#e05858">辛</span><span style="color:#c0a0e0">巳</span></td>
<td class="dy-shen">正官</td>
<td class="dy-year">2003-2012</td>
</tr>
<tr>
<td><span class="dy-age">13-22</span></td>
<td><span class="dy-gan" style="color:#e05858">壬</span><span style="color:#58aee0">午</span></td>
<td class="dy-shen">偏印</td>
<td class="dy-year">2013-2022</td>
</tr>
<tr class="current">
<td><span class="dy-age">23-32</span></td>
<td><span class="dy-gan" style="color:#58aee0">癸</span><span style="color:#6a5f7a">未</span></td>
<td class="dy-shen">正印</td>
<td class="dy-year">2023-2032</td>
</tr>
<tr>
<td><span class="dy-age">33-42</span></td>
<td><span style="color:#ffd966">甲</span><span style="color:#e0a058">申</span></td>
<td class="dy-shen">比肩</td>
<td class="dy-year">2033-2042</td>
</tr>
<tr>
<td><span class="dy-age">43-52</span></td>
<td><span style="color:#58d0a0">乙</span><span style="color:#e0a058">酉</span></td>
<td class="dy-shen">劫财</td>
<td class="dy-year">2043-2052</td>
</tr>
<tr>
<td><span class="dy-age">53-62</span></td>
<td><span style="color:#e05858">丙</span><span style="color:#c0a0e0">戌</span></td>
<td class="dy-shen">食神</td>
<td class="dy-year">2053-2062</td>
</tr>
</table>
<div class="para" style="font-size:12px;text-align:center;color:#6a5f7a;margin-top:4px">
当前 · <strong style="color:#58aee0">癸未大运</strong>(正印运,增强印星力量)
</div>
</div>
<!-- 2026流年 -->
<div class="sec">
<div class="sec-title">2026丙午 · 流年</div>
<div class="liunian-box">
<div class="para">
<strong style="color:#e0a058">丙午</strong>流年,丙火食神透出,午火为伤官。食神泄秀生财,<strong>想法多、行动力强</strong>。
</div>
<div class="para">
大运癸未与流年丙午:丙癸相见,<span class="yin">正印</span>配<span style="color:#e0a058">食神</span>,<strong>以技术/知识创造价值</strong>的一年。食神制杀(丙制庚),压力感明显减轻。
</div>
<div class="para">
午未合火,加强了全局的火力——<strong>食伤力量提升</strong>,适合技术产品落地、内容创作、方案输出。
</div>
<div class="para">
<strong style="color:#e05858">注意:</strong>丙辛合(流年丙合时支辛),正官被合,职场/管理上可能有变动,或因新方向冲击原有岗位结构。食伤太旺耗身,注意精力分配。
</div>
</div>
</div>
<!-- 总结 -->
<div class="sec">
<div class="sec-title">格局总评</div>
<div class="para">
<strong>甲木参天 · 双庚为杀 · 癸水为印 · 杀印相生</strong>
</div>
<div class="para">
三奇得一:年干庚(杀)→ 时干癸(印)→ 日主甲,链式流通。<br>
日坐寅木(禄地),身不太弱,能扛住两庚克制。<br>
时支酉官收尾,杀印生官——<strong>压力→智慧→地位</strong>,路数清晰。
</div>
<div class="para" style="text-align:center;margin-top:6px">
<span style="font-size:12px;color:#6a5f7a;letter-spacing:2px">
庚辰年 · 庚辰月 · 甲寅日 · 癸酉时
</span>
</div>
<div class="para" style="text-align:center;font-size:12px;color:#6a5f7a">
山东东营广饶 · 真太阳时校正后仍为酉时
</div>
</div>
<div class="footer">杀印相生 · 权印双收</div>
</div>
</body>
</html>
File diff suppressed because one or more lines are too long
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# 宇森游戏 · 线上服务器一键更新(请在 81.70.146.51 上执行)
# 用法:sudo bash deploy_server.sh
set -e
# ===== 按实际情况修改这几个变量 =====
REPO_DIR="/opt/yuthon/game" # git 仓库目录(含 world.html 与 addons/)
ODOO_CONF="/etc/odoo/odoo.conf" # odoo.conf 路径
ODOO_DB="yt_game" # 线上数据库名(game.pengyuthon.cn 对应 yt_game;勿改,避免影响 school)
ODOO_SERVICE="odoo" # systemctl 服务名
ODOO_BIN="/usr/bin/odoo" # odoo-bin 路径(或 which odoo)
# ===================================
echo "== [1/5] 拉取最新代码 =="
cd "$REPO_DIR"
git pull --ff-only origin main
echo "== [2/5] 钉死 odoo.conf 数据库 =="
if grep -q "^[[:space:]]*db_name" "$ODOO_CONF"; then
sed -i "s/^[[:space:]]*db_name.*/db_name = $ODOO_DB/" "$ODOO_CONF"
else
echo "db_name = $ODOO_DB" >> "$ODOO_CONF"
fi
if ! grep -q "^[[:space:]]*dbfilter" "$ODOO_CONF"; then
echo "dbfilter = ^$ODOO_DB\$" >> "$ODOO_CONF"
fi
echo "当前 db_name=$(grep '^db_name' "$ODOO_CONF")"
echo "== [3/5] 升级模块(数据库 $ODOO_DB)=="
"$ODOO_BIN" -c "$ODOO_CONF" -d "$ODOO_DB" -u game_base,yt_world --stop-after-init
# 若模块从未安装过,把上行的 -u 改成 -i 执行一次即可
echo "== [4/5] 重启 Odoo =="
systemctl restart "$ODOO_SERVICE"
sleep 3
systemctl is-active "$ODOO_SERVICE" || { echo "Odoo 未起来,检查: journalctl -u $ODOO_SERVICE -n 50"; exit 1; }
echo "== [5/5] 冒烟验证 =="
curl -s -o /dev/null -w "codex 接口 HTTP %{http_code}\n" "https://game.pengyuthon.cn/game/api/codex?db=yt_game"
echo "完成。前端为静态文件,nginx 指向 $REPO_DIR 即可,无需构建。"
+267 -16
View File
@@ -4905,7 +4905,7 @@ function updateSidebar() {
<span style="font-size:12px;color:#ccd;">${resNames[k] || k}</span>
<div style="display:flex;align-items:baseline;gap:10px;">
<span style="font-size:13px;color:#8bc34a;font-weight:bold;">${Math.floor(s.amount)}</span>
<span style="font-size:10px;color:#7fdca0;white-space:nowrap;">+${fmtNum(s.rate_per_min)}/分</span>
${(s.rate_per_min > 0.0001) ? `<span style="font-size:10px;color:#7fdca0;white-space:nowrap;">+${fmtNum(s.rate_per_min)}/分</span>` : ''}
</div>
</div>`).join('');
} else {
@@ -5150,6 +5150,8 @@ function startMicroRender() {
// 建造模式浮动操作按钮(旋转/确认/取消),在屏幕坐标系绘制
if (microBuildMode) microDrawActionBtns();
// 农舍升级特效反馈层(屏幕空间叠加,纯表现层)
microUpgradeFxDraw(microCtx, now);
const Rx = mW * 0.50, Ry = mH * 0.43;
const cx = mW / 2, cy = mH / 2;
@@ -6435,7 +6437,8 @@ async function updateChar() {
}
}
function closeUnitModal() { microOpenUnitId = null; document.getElementById('unitModal').classList.remove('active'); }
function closeUnitModalDom() { microOpenUnitId = null; document.getElementById('unitModal').classList.remove('active'); }
function closeUnitModal() { closeUnitModalDom(); microUpgradeFxReset(); }
// ===== 微观建筑:悬停 tooltip + 点击弹窗(与 Odoo 后台目录联动)=====
let microTipEl = null;
@@ -6474,6 +6477,212 @@ function microCatName(cat) {
return c ? c.name : (cat || '其他');
}
// 点击已放置建筑 → 复用 unitModal 展示默认数据(demo 交互)
// ================= 农舍升级特效反馈层(纯表现层;不改动升级数据逻辑/存档/接口签名) =================
// 真实状态机入口:microBuildingAct。完成特效只在 res.level != null(后端落库成功)时触发;
// 资源不足 / 网络异常等失败分支只 toast 并中断,绝不播放完成特效(满足"异常分支不误播")。
const UPGRADE_FX = {
enabled: true, // 总开关(关闭 = 全部静默降级,不影响升级结果)
forceLowEnd: false, // 强制低端降级(无粒子,仅保留环/光/数字)
autoDetectLowEnd: true, // 移动端 / 低核数 / 低内存自动降级
durations: { confirm: 220, progress: 1100, complete: 760 }, // 各阶段时长(ms)
particleCount: 24, // 高端粒子数;低端自动为 0
haloRings: 3, // 扩散光环数量
colors: { core: '#ffd54f', spark: '#ffe9a8', halo: '#ffb74d', ring: '#4fc3f7', reward: '#7fdca0' },
sound: { enabled: true, completeFreq: 880, gain: 0.07 },
};
function _ufLowEnd() {
if (UPGRADE_FX.forceLowEnd) return true;
if (!UPGRADE_FX.autoDetectLowEnd) return false;
try {
const c = navigator.hardwareConcurrency || 4;
const mem = navigator.deviceMemory || 4;
const mob = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent || '');
return mob || c <= 2 || mem <= 2;
} catch (e) { return false; }
}
function _ufNow() { return performance.now(); }
function _ufEaseOutCubic(p) { return 1 - Math.pow(1 - p, 3); }
function _ufClamp01(p) { return p < 0 ? 0 : (p > 1 ? 1 : p); }
let UF = null; // 升级特效运行态:null = 空闲
function microUpgradeFxReset() {
UF = null;
document.querySelectorAll('#modalBody .cmdBtn[data-baction="upgrade"]').forEach(b => {
b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = '';
});
}
function microUpgradeFxBtnDisable() {
// 仅作"确认反馈"视觉提示(变暗),保持可点击以支持"进行中再次点击 = 跳过/快速完成"
const btn = document.querySelector('#modalBody .cmdBtn[data-baction="upgrade"]');
if (btn) { btn.dataset.fx = '1'; btn.style.opacity = '0.6'; }
}
function microUpgradeFxCostOf(type) {
if (!UPGRADE_FX.enabled) return null;
const it = microBuildItemById(type);
const up = it && (it.actions || []).find(a => a.action === 'upgrade');
return ((up && up.cost) || (MICRO_BUILD_EFFECTS[(type || '') + ':upgrade'] || {}).cost || {});
}
function microUpgradeFxBuildingCenter(b) {
const it = microBuildItemById(b.type);
const ew = b.w || (it && it.w) || 1, eh = b.h || (it && it.h) || 1;
return microCellToScreen(b.i + ew / 2, b.j + eh / 2);
}
function _ufSpawnParticles(uf, n, rising) {
const c = microUpgradeFxBuildingCenter(uf.building);
uf.particles.length = 0; // 复用数组,避免每帧分配 → 低 GC
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const sp = rising ? (18 + Math.random() * 36) : (70 + Math.random() * 130);
uf.particles.push({
x0: c.x, y0: c.y - 12,
vx: rising ? Math.cos(a) * sp * 0.35 : Math.cos(a) * sp,
vy: rising ? -(38 + Math.random() * 58) : Math.sin(a) * sp,
t0: _ufNow(), life: rising ? 680 : 560 + Math.random() * 320, size: 2 + Math.random() * 2.6,
});
}
}
function _ufPushRings(uf, n) {
for (let r = 0; r < n; r++) uf.rings.push({ t0: _ufNow() + r * 110, life: 640 });
}
function _ufEnterComplete(uf) {
uf.phase = 'complete'; uf.t0 = _ufNow();
uf.floats.push({ kind: 'level', text: 'Lv.' + uf.toLevel, color: '#ffffff', t0: _ufNow(), life: UPGRADE_FX.durations.complete, vy: -0.022 });
if (uf.reward) uf.floats.push({ kind: 'reward', text: uf.reward, color: UPGRADE_FX.colors.reward, t0: _ufNow(), life: UPGRADE_FX.durations.complete + 220, vy: -0.04 });
if (!_ufLowEnd()) _ufSpawnParticles(uf, UPGRADE_FX.particleCount, false);
_ufPushRings(uf, UPGRADE_FX.haloRings);
_ufPlaySound(UPGRADE_FX.sound.completeFreq);
}
// 点击升级:进入"确认"预演(按钮态 + 资源扣除飘字)。此时数据尚未落库,仅为表现层。
function microUpgradeFxStart(building, cost) {
if (!UPGRADE_FX.enabled || !building) return;
microUpgradeFxReset(); // 重复触发先清掉旧的,避免残留/重复播放
UF = { phase: 'confirm', t0: _ufNow(), building, fromLevel: building.level, toLevel: building.level,
reward: '', particles: [], rings: [], floats: [] };
microUpgradeFxBtnDisable();
if (cost) {
const parts = Object.entries(cost).map(([k, v]) => (MICRO_RES_NAMES[k] || k) + ' ' + v);
if (parts.length) UF.floats.push({ kind: 'cost', text: '-' + parts.join(' '), color: '#ffd54f', t0: _ufNow(), life: 900, vy: -0.03 });
}
}
// 后端落库成功:进入"进行中"过程反馈(进度环 + 上升火花 + 光环)。
function microUpgradeFxProgress() {
if (!UF) return;
UF.phase = 'progress'; UF.t0 = _ufNow();
if (!_ufLowEnd()) _ufSpawnParticles(UF, Math.round(UPGRADE_FX.particleCount * 0.6), true);
_ufPushRings(UF, UPGRADE_FX.haloRings);
}
// 失败 / 打断:中断进行中动画(绝不进入完成阶段)。
function microUpgradeFxAbort() { microUpgradeFxReset(); }
// 跳过 / 快速完成:进行中立即跳到完成结算。
function microUpgradeFxSkip() {
if (!UF) return;
if (UF.phase === 'confirm' || UF.phase === 'progress') {
UF.toLevel = UF.toLevel || (UF.fromLevel + 1);
_ufEnterComplete(UF);
}
}
let _ufAudioCtx = null;
function _ufPlaySound(freq) {
if (!UPGRADE_FX.enabled || !UPGRADE_FX.sound.enabled) return;
try {
if (!_ufAudioCtx) _ufAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
const ac = _ufAudioCtx;
if (ac.state === 'suspended') ac.resume();
const t = ac.currentTime;
const mk = (f, g0, dt) => {
const o = ac.createOscillator(), g = ac.createGain();
o.type = 'sine'; o.frequency.value = f; o.connect(g); g.connect(ac.destination);
g.gain.setValueAtTime(g0, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dt);
o.start(t); o.stop(t + dt + 0.02);
};
mk(freq, UPGRADE_FX.sound.gain, 0.18);
mk(freq * 1.25, UPGRADE_FX.sound.gain * 0.7, 0.26); // 上行小三度,强化"完成"感
} catch (e) { /* 资源 / 环境缺失:静默降级 */ }
}
// 每帧由微观渲染循环调用(屏幕空间叠加,不影响建筑数据绘制)。
function microUpgradeFxDraw(ctx, now) {
if (!UPGRADE_FX.enabled || !UF) return;
const g = microBuildGeom();
const c = microUpgradeFxBuildingCenter(UF.building);
const baseR = g.cell * 0.95;
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0); // 强制屏幕空间(绝对坐标)
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
const el = now - UF.t0;
// 阶段推进:进行中结束 → 自动进入完成结算
if (UF.phase === 'progress' && el >= UPGRADE_FX.durations.progress) {
UF.toLevel = UF.toLevel || (UF.fromLevel + 1);
_ufEnterComplete(UF);
}
// 光环(confirm/progress/complete 共用)
UF.rings = UF.rings.filter(r => now - r.t0 < r.life);
UF.rings.forEach(r => {
const age = _ufClamp01((now - r.t0) / r.life);
const rad = baseR * (0.6 + age * 1.4);
ctx.globalAlpha = (1 - age) * 0.5;
ctx.strokeStyle = UPGRADE_FX.colors.halo; ctx.lineWidth = 3 * (1 - age) + 1;
ctx.beginPath(); ctx.arc(c.x, c.y, rad, 0, Math.PI * 2); ctx.stroke();
});
// 粒子
if (UF.particles.length) {
UF.particles = UF.particles.filter(p => (now - p.t0) < p.life);
UF.particles.forEach(p => {
const age = _ufClamp01((now - p.t0) / p.life);
const x = p.x0 + p.vx * age, y = p.y0 + p.vy * age + 120 * age * age; // 轻微重力下坠
ctx.globalAlpha = (1 - age) * 0.9;
ctx.fillStyle = age < 0.5 ? UPGRADE_FX.colors.spark : UPGRADE_FX.colors.core;
ctx.beginPath(); ctx.arc(x, y, p.size * (1 - age * 0.4), 0, Math.PI * 2); ctx.fill();
});
}
// 进度环(仅 progress 阶段)
if (UF.phase === 'progress') {
const p = _ufClamp01(el / UPGRADE_FX.durations.progress);
ctx.globalAlpha = 0.95; ctx.strokeStyle = UPGRADE_FX.colors.ring; ctx.lineWidth = 5;
ctx.beginPath(); ctx.arc(c.x, c.y, baseR * 0.78, -Math.PI / 2, -Math.PI / 2 + p * Math.PI * 2); ctx.stroke();
}
// 完成:建筑弹跳 + 径向辉光(外观过渡)
if (UF.phase === 'complete') {
const p = _ufClamp01(el / UPGRADE_FX.durations.complete);
const pop = 1 + 0.18 * Math.sin(p * Math.PI);
const glow = 1 - p;
const grad = ctx.createRadialGradient(c.x, c.y, baseR * 0.2, c.x, c.y, baseR * (1.1 + p));
grad.addColorStop(0, 'rgba(255,213,79,' + (0.45 * glow).toFixed(3) + ')');
grad.addColorStop(1, 'rgba(255,183,77,0)');
ctx.globalAlpha = 1; ctx.fillStyle = grad;
ctx.beginPath(); ctx.arc(c.x, c.y, baseR * (1.1 + p), 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = glow; ctx.strokeStyle = UPGRADE_FX.colors.core; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(c.x, c.y, baseR * 0.95 * pop, 0, Math.PI * 2); ctx.stroke();
}
// 飘字(成本 / 等级 / 奖励)
UF.floats = UF.floats.filter(f => (now - f.t0) < f.life);
UF.floats.forEach(f => {
const age = _ufClamp01((now - f.t0) / f.life);
const y = c.y - 30 + f.vy * age * 160;
let alpha = 1, scale = 1;
if (f.kind === 'level') { scale = 0.7 + 0.5 * _ufEaseOutCubic(Math.min(1, age * 3)); alpha = age > 0.7 ? (1 - (age - 0.7) / 0.3) : 1; }
else { alpha = age > 0.75 ? (1 - (age - 0.75) / 0.25) : 1; }
ctx.globalAlpha = _ufClamp01(alpha);
ctx.fillStyle = f.color;
ctx.font = (f.kind === 'level' ? 'bold ' : '') + Math.round((f.kind === 'level' ? 30 : 18) * scale) + 'px system-ui, sans-serif';
ctx.fillText(f.text, c.x, y);
});
ctx.globalAlpha = 1; ctx.restore();
// 结束:完成阶段播完即清理(按钮已由 showBuildModal 重渲染恢复)
if (UF && UF.phase === 'complete' && (now - UF.t0) >= UPGRADE_FX.durations.complete) {
microUpgradeFxReset();
}
}
function showBuildModal(b) {
if (!b) return;
const it = microBuildItemById(b.type);
@@ -6494,16 +6703,22 @@ function showBuildModal(b) {
const panelIcon = (it.levels && it.levels[b.level]) || it.icon;
const img = (typeof panelIcon === 'string')
? `<img src="${panelIcon}" alt="" style="width:64px;height:64px;object-fit:contain;margin:2px auto 10px;display:block;">` : '';
const yld = (it.statYield && typeof it.statYield === 'object' && Object.keys(it.statYield).length)
? Object.entries(it.statYield)
.map(([k, v]) => `<div class="statRow"><span class="statLabel">产出·${k}</span><span class="statValue">+${v}</span></div>`).join('') : '';
const need = b.level * 100;
const expPct = Math.max(0, Math.min(100, Math.round((b.exp % need) / need * 100)));
const hpPct = Math.max(0, Math.min(100, Math.round(b.hp / (it.statHp || 100) * 100)));
const hpColor = hpPct > 50 ? '#8bc34a' : hpPct > 25 ? '#ff9800' : '#f44336';
// 产出按建筑等级缩放(与后端 level_mult 一致:L1=1倍、L2=2倍…)
const _lvlMult = (lv) => Math.max(1, parseInt(lv, 10) || 1);
const _baseInstant = (it.statYieldInstant && Object.keys(it.statYieldInstant).length)
? it.statYieldInstant : (it.statYield || {});
const _baseTick = it.statYieldTick || {};
const _mNow = _lvlMult(b.level);
const _yRow = (label, v) => `<div class="statRow"><span class="statLabel">${label}</span><span class="statValue">+${v * _mNow}</span></div>`;
const yld = [
...Object.entries(_baseInstant).map(([k, v]) => _yRow(`产出·${MICRO_RES_NAMES[k] || k}`, v)),
...Object.entries(_baseTick).map(([k, v]) => _yRow(`每分·${MICRO_RES_NAMES[k] || k}`, v)),
].join('');
const acts = (cfg.actions || []).filter(a =>
// 只保留有真实后台逻辑的动作(升级);发布任职/招募村民等占位动作暂不上面板
a.action === 'upgrade' &&
// 满级隐藏升级按钮:b.level 已达 maxLevel(后端真源)时不再渲染
!(a.action === 'upgrade' && b.level >= (it.maxLevel || 1))
!(b.level >= (it.maxLevel || 1))
).map(a => {
// 费用标注:优先用后端 catalog actions 自带的 cost;否则查效果表(key = "code:action")
const eff = MICRO_BUILD_EFFECTS[b.type + ':' + a.action] || {};
@@ -6512,21 +6727,33 @@ function showBuildModal(b) {
const costHtml = costEntries.length
? `<div style="font-size:11px;margin-top:3px;color:#ffd54f;">费用:${costEntries.map(([k, v]) => (MICRO_RES_NAMES[k] || k) + ' ×' + v).join(' ')}</div>`
: '';
return `<div class="cmdBtn" data-btype="${b.type}" data-baction="${a.action}" data-blabel="${a.label}">${a.icon} ${a.label}${costHtml}<div style="font-size:10px;opacity:.7;margin-top:2px;white-space:normal;">${a.desc}</div></div>`;
// 升级按钮体现实际提升:当前产出 → 升级后产出(与后端等级倍率一致)
let descHtml = a.desc;
if (a.action === 'upgrade') {
const _mNext = _lvlMult(b.level + 1);
const parts = [
...Object.entries(_baseInstant).map(([k, v]) =>
`${MICRO_RES_NAMES[k] || k} +${v * _mNow} → +${v * _mNext}`),
...Object.entries(_baseTick).map(([k, v]) =>
`${MICRO_RES_NAMES[k] || k}/分 +${v * _mNow} → +${v * _mNext}`),
];
if (parts.length) {
descHtml = `提升至 Lv.${b.level + 1}:<span style="color:#7fdca0;font-weight:bold;">${parts.join(',')}</span>`;
}
}
return `<div class="cmdBtn" data-btype="${b.type}" data-baction="${a.action}" data-blabel="${a.label}">${a.icon} ${a.label}${costHtml}<div style="font-size:10px;opacity:.7;margin-top:2px;white-space:normal;">${descHtml}</div></div>`;
}).join('');
document.getElementById('modalTitle').textContent = `${name} · Lv.${b.level}`;
const body = document.getElementById('modalBody');
// 经验/耐久暂无后台逻辑支撑,按用户要求不在面板显示;无动作时整个交互区块隐藏
body.innerHTML = `
<div style="text-align:center">${img}</div>
${it.desc ? `<div style="color:rgba(234,242,255,0.78);font-size:12px;line-height:1.5;margin:0 0 12px;">${it.desc}</div>` : ''}
<div class="statRow"><span class="statLabel">类别</span><span class="statValue">${microCatName(it.cat)}</span></div>
<div class="statRow"><span class="statLabel">等级</span><span class="statValue">Lv.${b.level} ${'★'.repeat(Math.min(b.level, 5))}</span></div>
<div class="statRow"><span class="statLabel">经验</span><div class="statBar"><div class="statFill" style="width:${expPct}%;background:#64b5f6;"></div></div><span class="statValue">${b.exp}</span></div>
<div class="statRow"><span class="statLabel">耐久</span><div class="statBar"><div class="statFill" style="width:${hpPct}%;background:${hpColor};"></div></div><span class="statValue">${b.hp}</span></div>
${yld}
<div style="margin-top:16px;"><div class="panel-label">${cfg.tagline || '交互'}</div>
<div class="cmdGrid">${acts || '<div style="color:#8899aa;font-size:12px;">暂无可交互动作</div>'}</div>
</div>
${acts ? `<div style="margin-top:16px;"><div class="panel-label">交互</div>
<div class="cmdGrid">${acts}</div></div>` : ''}
`;
body.querySelectorAll('.cmdBtn').forEach(btn => {
btn.addEventListener('click', () => {
@@ -6541,8 +6768,19 @@ async function microBuildingAct(type, action, label) {
const tile = microState && microState.tile;
if (!tile) return;
const bpos = microOpenBuilding || {};
// 升级:点击即进入"确认"预演(表现层;数据尚未落库,仅按钮态 + 资源扣除动效)
if (action === 'upgrade' && microOpenBuilding) {
if (UF && (UF.phase === 'confirm' || UF.phase === 'progress')) {
microUpgradeFxSkip(); // 进行中再次点击 = 跳过 / 快速完成(不重复调用后端,杜绝重复扣费)
return;
}
microUpgradeFxStart(microOpenBuilding, microUpgradeFxCostOf(type));
// 面板会挡住画布上的特效 → 点升级即收起面板(只关 DOM,不 Reset 特效;失败时有 toast 提示)
closeUnitModalDom();
}
const res = await API.buildingAction({ code: type, action, q: tile.q, r: tile.r, i: bpos.i, j: bpos.j });
if (!res || res.ok === false) {
if (action === 'upgrade') microUpgradeFxAbort(); // 异常分支:中断进行中动画,绝不播放完成特效
const msg = res && res.error === 'no_gold' ? '金币不足'
: (res && res.error === 'no_res' ? ('资源不足:' + (res.res || '')) : '操作失败(未登录或网络异常)');
microToast(msg); return;
@@ -6561,6 +6799,7 @@ async function microBuildingAct(type, action, label) {
if (_res) emitTaskEvent('resource_collected', { res: _res });
// 升级:用后端落库后的权威等级/经验回写并持久化
if (action === 'upgrade' && microOpenBuilding && res.level != null) {
const _was = microOpenBuilding.level;
microOpenBuilding.level = res.level;
microOpenBuilding.exp = res.exp;
// 升级即时换肤:主动确保新等级贴图(含左右视角)已加载,不依赖进入微观时的预加载时机
@@ -6572,6 +6811,18 @@ async function microBuildingAct(type, action, label) {
else if (base.includes('_r.png')) microEnsureIcon(base.replace('_r.png', '_l.png'));
}
scheduleBuildSave();
// 升级改变实际产出(人口上限/每分钟速率)→ 立即刷新侧栏"资源产出"
microRefreshResources();
// 表现层反馈:仅后端落库成功才进入"进行中 → 完成"特效(升级数据逻辑本身不动)
if (UF) {
UF.toLevel = res.level;
const _itU = microBuildItemById(microOpenBuilding.type);
const _inst = (_itU && (_itU.statYieldInstant || _itU.statYield)) || {};
UF.reward = Object.entries(_inst).map(([k, v]) => '+' + (v * (res.level - _was)) + ' ' + (MICRO_RES_NAMES[k] || k)).join(' ');
// 面板会挡住画布上的特效 → 落库成功后自动收起面板(只关 DOM,不 Reset 特效)
closeUnitModalDom();
microUpgradeFxProgress();
}
}
// 治疗:回满本村所有生物生命
if (action === 'heal' && microState && microState.units) {