diff --git a/addons/yt_world/build_spec.py b/addons/yt_world/build_spec.py index 10e14af..d6d558c 100644 --- a/addons/yt_world/build_spec.py +++ b/addons/yt_world/build_spec.py @@ -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 diff --git a/addons/yt_world/controllers/world_api.py b/addons/yt_world/controllers/world_api.py index b6ca1b3..90eaa18 100644 --- a/addons/yt_world/controllers/world_api.py +++ b/addons/yt_world/controllers/world_api.py @@ -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 diff --git a/addons/yt_world/models/yt_world.py b/addons/yt_world/models/yt_world.py index 118a68e..e75c4cc 100644 --- a/addons/yt_world/models/yt_world.py +++ b/addons/yt_world/models/yt_world.py @@ -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) diff --git a/addons/yt_world/models/yt_world_build_instance.py b/addons/yt_world/models/yt_world_build_instance.py index 51649af..338b5d8 100644 --- a/addons/yt_world/models/yt_world_build_instance.py +++ b/addons/yt_world/models/yt_world_build_instance.py @@ -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 diff --git a/addons/yt_world/models/yt_world_tile_micro.py b/addons/yt_world/models/yt_world_tile_micro.py index a837ff4..d192a5a 100644 --- a/addons/yt_world/models/yt_world_tile_micro.py +++ b/addons/yt_world/models/yt_world_tile_micro.py @@ -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 {}) diff --git a/bazi.html b/bazi.html deleted file mode 100644 index 5263a5d..0000000 --- a/bazi.html +++ /dev/null @@ -1,556 +0,0 @@ - - - - - -鹏宇 · 命盘 - - - -
- -
三 命 通 览
-
庚辰年 · 庚辰月 · 甲寅日 · 癸酉时
- - -
-
-
年
-
庚
-
辰
-
七杀
-
-
-
月
-
庚
-
辰
-
七杀
-
-
-
日
-
甲
-
寅
-
日主
-
-
-
时
-
癸
-
酉
-
正印
-
-
- - -
- 杀印相生格局 - ·  庚金七杀生癸水正印 · -  癸水正印生甲木日主 -
- - -
-
💡 你说的"两个七杀一个正印"
-
-
-
七杀①
-
年干 庚
-
-
-
七杀②
-
月干 庚
-
-
-
正印
-
时干 癸
-
-
-
- 完美对上。之前我按庚金日主算,七杀变丙、正印变己,自然对不上。
- 甲木日主,七杀=庚,正印=癸,一字不差。 -
-
- - -
-
杀印相生 · 格局评述
-
- 甲木参天生于辰月,暮春土旺木气渐衰。天干双透庚金七杀,克身之力极重,若无救应则身弱杀强、步步维艰。 -
-
-
七杀
庚
- → 生 → -
正印
癸
- → 生 → -
日主
甲
-
-
- 妙在时柱见癸水正印贴身相生—— - 七杀的压力化作正印的学习动力, - 正印再将养分输送给甲木日主。 - 压力→学识→成长,流通无阻。 -
-
- 时支酉金(辛正官)更为点睛—— - 杀印相生之后再生官,压力转化为权威, - 代表最终走向管理岗、有实权有地位。 -
-
格局核心特质:
-
① 权威性格 · 不怒自威,天生有领导气场
-
② 以学促用 · 善于通过学习思考化解问题
-
③ 后发制人 · 前期压力大,但越走越稳
-
④ 管理才能 · 既有权威(杀)又有智慧(印)
-
- - -
-
十神全表
-
-
-
年干
-
庚 · 七杀
-
-
-
年支
-
辰 · 偏财
-
藏戊(偏财) 乙(劫财) 癸(正印)
-
-
-
月干
-
庚 · 七杀
-
-
-
月支
-
辰 · 偏财
-
藏戊(偏财) 乙(劫财) 癸(正印)
-
-
-
日干
-
甲 · 日主
-
-
-
日支
-
寅 · 食神
-
藏甲(比肩) 丙(食神) 戊(偏财)
-
-
-
时干
-
癸 · 正印
-
-
-
时支
-
酉 · 正官
-
藏辛(正官)
-
-
-
- - -
-
地支藏干
-
-
-
辰(年支/月支)
-
- 戊 偏财 - 乙 劫财 - 癸 正印 -
-
-
-
寅(日支)
-
- 甲 比肩 - 丙 食神 - 戊 偏财 -
-
-
-
酉(时支)
-
- 辛 正官 -
-
-
-
- - -
-
纳音五行
-
-
-
年柱
-
庚辰 · 白蜡金
-
-
-
月柱
-
庚辰 · 白蜡金
-
-
-
日柱
-
甲寅 · 大溪水
-
-
-
时柱
-
癸酉 · 剑锋金
-
-
-
-
4金
-
3木
-
3水
-
1火
-
3土
-
-
- 金旺为杀印之源,木有寅根得印生, - 水火调候俱备,土为财星有制 -
-
- - -
-
大运 · 顺排
-
- 阳男顺排 · 约3岁起运 · 每10年一运 -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
03-12辛巳正官2003-2012
13-22壬午偏印2013-2022
23-32癸未正印2023-2032
33-42甲申比肩2033-2042
43-52乙酉劫财2043-2052
53-62丙戌食神2053-2062
-
- 当前 · 癸未大运(正印运,增强印星力量) -
-
- - -
-
2026丙午 · 流年
-
-
- 丙午流年,丙火食神透出,午火为伤官。食神泄秀生财,想法多、行动力强。 -
-
- 大运癸未与流年丙午:丙癸相见,正印配食神,以技术/知识创造价值的一年。食神制杀(丙制庚),压力感明显减轻。 -
-
- 午未合火,加强了全局的火力——食伤力量提升,适合技术产品落地、内容创作、方案输出。 -
-
- 注意:丙辛合(流年丙合时支辛),正官被合,职场/管理上可能有变动,或因新方向冲击原有岗位结构。食伤太旺耗身,注意精力分配。 -
-
-
- - -
-
格局总评
-
- 甲木参天 · 双庚为杀 · 癸水为印 · 杀印相生 -
-
- 三奇得一:年干庚(杀)→ 时干癸(印)→ 日主甲,链式流通。
- 日坐寅木(禄地),身不太弱,能扛住两庚克制。
- 时支酉官收尾,杀印生官——压力→智慧→地位,路数清晰。 -
-
- - 庚辰年 · 庚辰月 · 甲寅日 · 癸酉时 - -
-
- 山东东营广饶 · 真太阳时校正后仍为酉时 -
-
- - - -
- - diff --git a/buildings_default_demo.html b/buildings_default_demo.html deleted file mode 100644 index c052af3..0000000 --- a/buildings_default_demo.html +++ /dev/null @@ -1,256 +0,0 @@ - - - - - -宇森 · 人族 18 帧状态机 Demo - - - -

宇森 · 人族 18 帧状态机 Demo

-

点击地面 放置角色 · 拖动或点击其它格子 让它走过去 · 动作按钮 / 键盘 A 攻击 · W 工作 · C 交流 · I 待机 · 也可用 window.triggerAction('attack') 控制 · 空格暂停 · R 重置

-
- -
载入素材中…
-
-
- - - - - - - - - - -
- - - - diff --git a/deploy_server.sh b/deploy_server.sh deleted file mode 100644 index b94d330..0000000 --- a/deploy_server.sh +++ /dev/null @@ -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 即可,无需构建。" diff --git a/world.html b/world.html index bf80432..2e2e766 100644 --- a/world.html +++ b/world.html @@ -4905,7 +4905,7 @@ function updateSidebar() { ${resNames[k] || k}
${Math.floor(s.amount)} - +${fmtNum(s.rate_per_min)}/分 + ${(s.rate_per_min > 0.0001) ? `+${fmtNum(s.rate_per_min)}/分` : ''}
`).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') ? `` : ''; - const yld = (it.statYield && typeof it.statYield === 'object' && Object.keys(it.statYield).length) - ? Object.entries(it.statYield) - .map(([k, v]) => `
产出·${k}+${v}
`).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) => `
${label}+${v * _mNow}
`; + 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 ? `
费用:${costEntries.map(([k, v]) => (MICRO_RES_NAMES[k] || k) + ' ×' + v).join(' ')}
` : ''; - return `
${a.icon} ${a.label}${costHtml}
${a.desc}
`; + // 升级按钮体现实际提升:当前产出 → 升级后产出(与后端等级倍率一致) + 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}:${parts.join(',')}`; + } + } + return `
${a.icon} ${a.label}${costHtml}
${descHtml}
`; }).join(''); document.getElementById('modalTitle').textContent = `${name} · Lv.${b.level}`; const body = document.getElementById('modalBody'); + // 经验/耐久暂无后台逻辑支撑,按用户要求不在面板显示;无动作时整个交互区块隐藏 body.innerHTML = `
${img}
${it.desc ? `
${it.desc}
` : ''}
类别${microCatName(it.cat)}
等级Lv.${b.level} ${'★'.repeat(Math.min(b.level, 5))}
-
经验
${b.exp}
-
耐久
${b.hp}
${yld} -
${cfg.tagline || '交互'}
-
${acts || '
暂无可交互动作
'}
-
+ ${acts ? `
交互
+
${acts}
` : ''} `; 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) {