Merge pull request '等级系统' (#45) from why_ga into main
Reviewed-on: #45
This commit was merged in pull request #45.
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
'data/codex_race.xml',
|
||||
'data/codex_talent.xml',
|
||||
'data/yt_world_build_item.xml',
|
||||
'data/yt_world_build_item_updates.xml',
|
||||
'data/yt_world_equip_item.xml',
|
||||
'data/yt_world_race.xml',
|
||||
'data/yt_world_task.xml',
|
||||
|
||||
@@ -201,7 +201,7 @@ class YtWorldApi(http.Controller):
|
||||
out = []
|
||||
for r in items.read(['code', 'name', 'category', 'icon', 'footprint_w',
|
||||
'footprint_h', 'cost_gold', 'terrains', 'desc', 'flat',
|
||||
'stat_level', 'stat_hp', 'stat_yield', 'stat_yield_instant',
|
||||
'stat_level', 'max_level', 'stat_hp', 'stat_yield', 'stat_yield_instant',
|
||||
'stat_yield_tick', 'actions']):
|
||||
if r['category'] not in seen:
|
||||
seen.append(r['category'])
|
||||
@@ -227,6 +227,7 @@ class YtWorldApi(http.Controller):
|
||||
'desc': r['desc'] or '',
|
||||
'flat': bool(r['flat']),
|
||||
'statLevel': r['stat_level'] or 1,
|
||||
'maxLevel': r['max_level'] or 1,
|
||||
'statHp': r['stat_hp'] or 100,
|
||||
'statYield': r['stat_yield'] or {},
|
||||
'statYieldInstant': instant,
|
||||
@@ -605,6 +606,19 @@ class YtWorldApi(http.Controller):
|
||||
('market', 'upgrade'): {'cost': {'gold': 25}, 'msg': '集市升级申请已提交'},
|
||||
}
|
||||
|
||||
@http.route('/yt_world/api/build/effects', type='http', auth='public',
|
||||
methods=['GET', 'OPTIONS'], csrf=False)
|
||||
def build_effects(self, **kw):
|
||||
"""建筑动作效果表(费用/产出真源),供前端弹窗标注费用。
|
||||
返回 {"cottage:upgrade": {"cost": {"gold": 50}, "gain": {}}, ...}"""
|
||||
effects = {}
|
||||
for (code, action), eff in self.BUILD_EFFECTS.items():
|
||||
effects['%s:%s' % (code, action)] = {
|
||||
'cost': eff.get('cost') or {},
|
||||
'gain': eff.get('gain') or {},
|
||||
}
|
||||
return _json({'ok': True, 'effects': effects})
|
||||
|
||||
def _require_user(self):
|
||||
"""auth='public' 时手动校验登录态;未登录返回统一错误字典。
|
||||
Odoo auth='public' 不会把 session uid 同步到 request.env.user,
|
||||
@@ -961,6 +975,12 @@ class YtWorldApi(http.Controller):
|
||||
items = list(tile.micro_world_id.build_items or [])
|
||||
for b in items:
|
||||
if b.get('type') == code and b.get('i') == i and b.get('j') == j:
|
||||
# 满级守卫:最大等级真源 = yt.world.build.item.max_level
|
||||
item = request.env['yt.world.build.item'].sudo().search(
|
||||
[('code', '=', code)], limit=1)
|
||||
max_lv = (item.max_level or 1) if item else 1
|
||||
if (b.get('level') or 1) >= max_lv:
|
||||
return {'max_reached': True}
|
||||
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})
|
||||
@@ -1022,6 +1042,8 @@ class YtWorldApi(http.Controller):
|
||||
if q is None or r is None or i is None or j is None:
|
||||
return _json({'ok': False, 'error': 'bad_pos'})
|
||||
upgrade_info = self._upgrade_building_instance(world, code, q, r, i, j)
|
||||
if upgrade_info and upgrade_info.get('max_reached'):
|
||||
return _json({'ok': False, 'error': 'max_level'})
|
||||
if not upgrade_info:
|
||||
return _json({'ok': False, 'error': 'building_not_found'})
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
<field name="cost_gold">20</field>
|
||||
<field name="desc">村民起居的基本居所,提供人口上限与夜间休息。</field>
|
||||
<field name="stat_level">1</field><field name="stat_hp">120</field>
|
||||
<field name="max_level">2</field>
|
||||
<field name="stat_yield" eval="{'population': 4}"/>
|
||||
<field name="actions" eval="[
|
||||
{'action': 'employ', 'label': '发布任职', 'icon': '📜', 'desc': '发布村长任职委托,吸引居民就任', 'cost': {}, 'gain': {}, 'msg': '已发布村长任职委托'},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class YtWorldBuildItem(models.Model):
|
||||
@@ -44,6 +44,8 @@ class YtWorldBuildItem(models.Model):
|
||||
flat = fields.Boolean(string='平面俯视', default=False,
|
||||
help='true=顶视图,岛上压成菱形贴地;false=3/4 立绘,竖直站立')
|
||||
stat_level = fields.Integer(string='默认等级', default=1)
|
||||
max_level = fields.Integer(string='最大等级', default=1,
|
||||
help='当前设计支持的最高等级;前端据此隐藏升级按钮,后端据此拒绝升级。')
|
||||
stat_hp = fields.Integer(string='默认耐久', default=100)
|
||||
stat_yield = fields.Json(string='默认产出(兼容)', default=dict,
|
||||
help='兼容旧数据;新产出请使用 stat_yield_instant / stat_yield_tick')
|
||||
@@ -57,3 +59,16 @@ class YtWorldBuildItem(models.Model):
|
||||
_sql_constraints = [
|
||||
('uniq_code', 'unique(code)', '素材编码必须唯一'),
|
||||
]
|
||||
|
||||
# 设计最大等级(随素材制作进度迭代;由 data/yt_world_build_item_updates.xml 的
|
||||
# <function> 在每次模块升级时刷新——noupdate 记录无法用普通 record 覆盖)
|
||||
DESIGN_MAX_LEVELS = {
|
||||
'cottage': 2, # L2 素材已上线
|
||||
}
|
||||
|
||||
@api.model
|
||||
def sync_max_levels(self):
|
||||
for it in self.search([]):
|
||||
lv = self.DESIGN_MAX_LEVELS.get(it.code)
|
||||
if lv and it.max_level != lv:
|
||||
it.max_level = lv
|
||||
|
||||
@@ -279,6 +279,16 @@ const API = {
|
||||
return { ok: false, error: 'network', detail: String(e && e.message || e) };
|
||||
}
|
||||
},
|
||||
// 建筑动作效果表(费用/产出真源在后端 BUILD_EFFECTS)
|
||||
async buildEffects() {
|
||||
try {
|
||||
const r = await fetch(_worldUrl('/yt_world/api/build/effects'), {
|
||||
method: 'GET', credentials: _wcred,
|
||||
});
|
||||
const j = await r.json();
|
||||
return j && j.ok ? j : { ok: false, error: 'empty' };
|
||||
} catch (e) { return { ok: false, error: 'network' }; }
|
||||
},
|
||||
// ---------- 微观世界角色(玩家放置的种族角色,绑定用户持久化) ----------
|
||||
// 以下两个已重定向到世界生物表(yt.world.creature),保留旧名以兼容 characters.js
|
||||
async microChars() {
|
||||
|
||||
+29
-6
@@ -2626,6 +2626,15 @@ let microBuildRevealAt = 0; // 建筑显示时间戳:过渡结束 + 2
|
||||
let microChar = null; // 玩家角色:{name,level,exp,gold,inventory,equip}
|
||||
let microCreatureOverlay = {}; // cid -> 已保存生物状态(等级/装备/经验)
|
||||
let microOpenBuilding = null; // 当前打开的建筑交互对象(用于升级等更新)
|
||||
// 建筑动作效果表(费用/产出)——真源在后端 BUILD_EFFECTS,登录后拉取一次缓存
|
||||
let MICRO_BUILD_EFFECTS = {};
|
||||
const MICRO_RES_NAMES = { food:'🍖 食物', wood:'🪵 木材', metal:'⛏ 金属', gold:'💰 金币', culture:'🏛 文化', research:'🔬 科研', defense:'🛡 防御', military:'⚔ 军事', pop:'👥 人口', population:'👥 人口', water:'💧 水', meat:'🥩 肉', milk:'🥛 奶', coin:'🪙 硬币', flour:'🌾 面粉', stone:'🪨 石材', prestige:'⭐ 声望', rare:'💎 稀有' };
|
||||
async function microLoadBuildEffects() {
|
||||
try {
|
||||
const r = await API.buildEffects();
|
||||
if (r && r.ok && r.effects) MICRO_BUILD_EFFECTS = r.effects;
|
||||
} catch (e) { /* 拉不到:弹窗不显示费用,交互仍走后端校验 */ }
|
||||
}
|
||||
let microCanvas, microCtx;
|
||||
let microAnimId = null;
|
||||
let microGridCache = null; // 微观世界:背景 + 20×20 离屏缓存(静态,逐帧只贴图)
|
||||
@@ -2910,6 +2919,7 @@ async function microLoadCatalog() {
|
||||
if (it.h != null && t.h == null) t.h = it.h;
|
||||
if (it.flat != null) t.flat = it.flat;
|
||||
if (it.statLevel) t.statLevel = it.statLevel;
|
||||
if (it.maxLevel) t.maxLevel = it.maxLevel; // 最大等级真源(满级隐藏升级按钮)
|
||||
if (it.statHp) t.statHp = it.statHp;
|
||||
if (it.costGold != null) t.costGold = it.costGold; // 后端价格优先(单一数据源)
|
||||
if (it.statYield && Object.keys(it.statYield).length) t.statYield = it.statYield;
|
||||
@@ -4687,6 +4697,7 @@ function enterMicroWorld(tile, opts) {
|
||||
updateSidebar();
|
||||
microLoadIcons(); // 进入微观视图即预加载建造贴图,避免已放置建筑先显示文字
|
||||
microLoadCatalog(); // 拉取 Odoo 后台建造目录(未登录时保留前端兜底常量),hover/弹窗即时生效
|
||||
microLoadBuildEffects(); // 拉取建筑动作费用/产出真源(弹窗标注费用)
|
||||
microLoadGame(); // 拉取角色 / 生物 / 订单(Odoo 后端),覆盖单位状态
|
||||
if (!(opts && opts.boss)) {
|
||||
microLoadChests(tile.q, tile.r); // 拉取本地块已生成的宝箱
|
||||
@@ -4884,7 +4895,7 @@ function updateSidebar() {
|
||||
const stocks = microStockCache || {};
|
||||
const stockEntries = Object.entries(stocks).filter(([, s]) => (s.amount > 0.0001) || (s.rate_per_min > 0.0001));
|
||||
if (stockEntries.length > 0) {
|
||||
const resNames = { food:'🍖 食物', wood:'🪵 木材', metal:'⛏ 金属', gold:'💰 金币', culture:'🏛 文化', research:'🔬 科研', defense:'🛡 防御', military:'⚔ 军事', pop:'👥 人口', population:'👥 人口', water:'💧 水', meat:'🥩 肉', milk:'🥛 奶', coin:'🪙 硬币', flour:'🌾 面粉', prestige:'⭐ 声望', rare:'💎 稀有' };
|
||||
const resNames = MICRO_RES_NAMES;
|
||||
const order = ['food','water','wood','meat','milk','metal','gold','coin','culture','research','defense','military','pop','prestige','rare'];
|
||||
stockEntries.sort((a, b) => {
|
||||
const ia = order.indexOf(a[0]), ib = order.indexOf(b[0]);
|
||||
@@ -6479,8 +6490,10 @@ function showBuildModal(b) {
|
||||
? { title: it.name, tagline: it.desc || '', actions: it.actions }
|
||||
: (BUILD_INTERACT[b.type] || { title: it.name, tagline: it.desc || '', actions: [] });
|
||||
const name = it.name || b.type;
|
||||
const img = (typeof it.icon === 'string')
|
||||
? `<img src="${it.icon}" alt="" style="width:64px;height:64px;object-fit:contain;margin:2px auto 10px;display:block;">` : '';
|
||||
// 面板大图跟随实际等级:有等级贴图用等级贴图,否则回退目录图标
|
||||
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('') : '';
|
||||
@@ -6488,9 +6501,19 @@ function showBuildModal(b) {
|
||||
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';
|
||||
const acts = (cfg.actions || []).map(a =>
|
||||
`<div class="cmdBtn" data-btype="${b.type}" data-baction="${a.action}" data-blabel="${a.label}">${a.icon} ${a.label}<div style="font-size:10px;opacity:.7;margin-top:2px;white-space:normal;">${a.desc}</div></div>`
|
||||
).join('');
|
||||
const acts = (cfg.actions || []).filter(a =>
|
||||
// 满级隐藏升级按钮:b.level 已达 maxLevel(后端真源)时不再渲染
|
||||
!(a.action === 'upgrade' && b.level >= (it.maxLevel || 1))
|
||||
).map(a => {
|
||||
// 费用标注:优先用后端 catalog actions 自带的 cost;否则查效果表(key = "code:action")
|
||||
const eff = MICRO_BUILD_EFFECTS[b.type + ':' + a.action] || {};
|
||||
const costMap = (a.cost && Object.keys(a.cost).length) ? a.cost : (eff.cost || {});
|
||||
const costEntries = Object.entries(costMap);
|
||||
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>`;
|
||||
}).join('');
|
||||
document.getElementById('modalTitle').textContent = `${name} · Lv.${b.level}`;
|
||||
const body = document.getElementById('modalBody');
|
||||
body.innerHTML = `
|
||||
|
||||
Reference in New Issue
Block a user