1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {})
|
||||
|
||||
@@ -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>
|
||||
· <span class="kill">庚</span>金七杀生<span class="yin">癸</span>水正印 ·
|
||||
<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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user