verify PR creation #33
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': '宇森世界 - 沙盘与微观世界',
|
||||
'version': '0.7',
|
||||
'version': '0.8',
|
||||
'summary': '地表存储(沙盘记忆) + 微观世界(1:1预留) + 资源(框架)',
|
||||
'description': '每个玩家拥有一个确定性生成的世界;仅持久化玩家改过的地表地块(种子+增量覆盖)。微观世界独立为 1:1 模型仅预留,资源模型为框架。',
|
||||
'category': 'Game',
|
||||
|
||||
@@ -916,18 +916,26 @@ class YtWorldApi(http.Controller):
|
||||
'status': c.get('status') or 'idle',
|
||||
'state': c.get('state') or 'idle',
|
||||
'mood': c.get('mood', '😊'), 'job': c.get('job', 'none'),
|
||||
'equip': self._normalize_equip(c.get('equip')),
|
||||
'tile_ref': c.get('tile_ref') or {},
|
||||
}
|
||||
rec = model.search([('world_id', '=', world.id), ('cid', '=', cid)], limit=1) if cid else None
|
||||
if rec:
|
||||
# 装备加成是"临时"的(来自实例,不存基础字段);前端回传的是有效属性,
|
||||
# 落库时回扣装备加成,避免把临时加成固化进基础值导致下次加载重复叠加。
|
||||
bonus = rec._equip_bonus()
|
||||
eff_hp_max = vals.get('hp_max', rec.hp_max)
|
||||
for f in ('attack', 'defense', 'hp_max'):
|
||||
if f in vals and isinstance(vals[f], (int, float)):
|
||||
vals[f] = max(0, int(vals[f]) - bonus.get(f, 0))
|
||||
if 'hp' in vals:
|
||||
vals['hp'] = min(vals['hp'], eff_hp_max)
|
||||
rec.write(vals)
|
||||
else:
|
||||
vals['world_id'] = world.id
|
||||
vals['cid'] = cid
|
||||
rec = model.create(vals)
|
||||
out.append({'oid': rec.id, 'cid': cid, 'level': rec.level,
|
||||
'exp': rec.exp, 'hp': rec.hp, 'equip': rec.equip or {}})
|
||||
'exp': rec.exp, 'hp': rec.hp})
|
||||
# 删除指定 cid
|
||||
deleted = body.get('deleted') or []
|
||||
if deleted:
|
||||
@@ -1085,20 +1093,37 @@ class YtWorldApi(http.Controller):
|
||||
if action == 'equip':
|
||||
slot = body.get('slot')
|
||||
code = body.get('code')
|
||||
if slot not in ('weapon', 'armor', 'accessory'):
|
||||
if slot not in ('weapon', 'armor', 'accessory', 'boots'):
|
||||
return _json({'ok': False, 'error': 'bad_slot'})
|
||||
item = request.env['yt.world.equip.item'].sudo().search([('code', '=', code)], limit=1)
|
||||
if not item:
|
||||
return _json({'ok': False, 'error': 'item_not_found'})
|
||||
equip = dict(rec.equip or {})
|
||||
equip[slot] = code
|
||||
rec.write({'equip': equip})
|
||||
if item.slot != slot:
|
||||
return _json({'ok': False, 'error': 'slot_mismatch'})
|
||||
# 装备改为管理"实例":每只装备是独立实物,支撑未来耐久/强化/交易
|
||||
Inst = request.env['yt.world.equip.instance'].sudo()
|
||||
# 1) 先卸下该槽现有装备(equipped_slot 置空 = 回背包)
|
||||
old = Inst.search([('owner_creature_id', '=', rec.id),
|
||||
('equipped_slot', '=', slot)], limit=1)
|
||||
if old:
|
||||
old.write({'equipped_slot': False})
|
||||
# 2) 优先复用背包中同款未装备实例(避免每次点击都新建,造成实例堆积)
|
||||
reuse = Inst.search([('owner_creature_id', '=', rec.id),
|
||||
('item_id', '=', item.id),
|
||||
('equipped_slot', '=', False)], limit=1)
|
||||
if reuse:
|
||||
reuse.write({'equipped_slot': slot})
|
||||
else:
|
||||
Inst.create({'item_id': item.id, 'owner_creature_id': rec.id,
|
||||
'equipped_slot': slot, 'durability': 100,
|
||||
'max_durability': 100, 'enhance_level': 0})
|
||||
elif action == 'unequip':
|
||||
slot = body.get('slot')
|
||||
equip = dict(rec.equip or {})
|
||||
if slot in equip:
|
||||
equip.pop(slot, None)
|
||||
rec.write({'equip': equip})
|
||||
# 仅把该槽实例的 equipped_slot 置空(回背包),不删除实例
|
||||
inst = request.env['yt.world.equip.instance'].sudo().search(
|
||||
[('owner_creature_id', '=', rec.id), ('equipped_slot', '=', slot)], limit=1)
|
||||
if inst:
|
||||
inst.write({'equipped_slot': False})
|
||||
elif action == 'train':
|
||||
rec.write({
|
||||
'level': rec.level + 1,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<record id="equip_iron_dagger" model="yt.world.equip.item">
|
||||
<field name="code">iron_dagger</field>
|
||||
<field name="name">铁匕首</field>
|
||||
<field name="icon">🗡️</field>
|
||||
<field name="icon">assets/icons/equips/iron_dagger.png</field>
|
||||
<field name="slot">weapon</field>
|
||||
<field name="rarity">common</field>
|
||||
<field name="stats">{"attack": 3}</field>
|
||||
@@ -16,7 +16,7 @@
|
||||
<record id="equip_steel_sword" model="yt.world.equip.item">
|
||||
<field name="code">steel_sword</field>
|
||||
<field name="name">精钢短剑</field>
|
||||
<field name="icon">⚔️</field>
|
||||
<field name="icon">assets/icons/equips/steel_sword.png</field>
|
||||
<field name="slot">weapon</field>
|
||||
<field name="rarity">rare</field>
|
||||
<field name="stats">{"attack": 7, "speed": 2}</field>
|
||||
@@ -26,7 +26,7 @@
|
||||
<record id="equip_rune_blade" model="yt.world.equip.item">
|
||||
<field name="code">rune_blade</field>
|
||||
<field name="name">符文长剑</field>
|
||||
<field name="icon">🗡️</field>
|
||||
<field name="icon">assets/icons/equips/rune_blade.png</field>
|
||||
<field name="slot">weapon</field>
|
||||
<field name="rarity">epic</field>
|
||||
<field name="stats">{"attack": 12, "hp_max": 15}</field>
|
||||
@@ -37,7 +37,7 @@
|
||||
<record id="equip_leather_vest" model="yt.world.equip.item">
|
||||
<field name="code">leather_vest</field>
|
||||
<field name="name">皮背心</field>
|
||||
<field name="icon">🦺</field>
|
||||
<field name="icon">assets/icons/equips/leather_vest.png</field>
|
||||
<field name="slot">armor</field>
|
||||
<field name="rarity">common</field>
|
||||
<field name="stats">{"defense": 2, "hp_max": 5}</field>
|
||||
@@ -64,6 +64,37 @@
|
||||
<field name="desc">传说用古龙鳞片打造,沉重但几乎刀枪不入。</field>
|
||||
</record>
|
||||
|
||||
<!-- 鞋子 -->
|
||||
<record id="equip_straw_sandals" model="yt.world.equip.item">
|
||||
<field name="code">straw_sandals</field>
|
||||
<field name="name">草鞋</field>
|
||||
<field name="icon">🩴</field>
|
||||
<field name="slot">boots</field>
|
||||
<field name="rarity">common</field>
|
||||
<field name="stats">{"speed": 1}</field>
|
||||
<field name="desc">轻便的草编鞋,适合远行。</field>
|
||||
</record>
|
||||
|
||||
<record id="equip_leather_boots" model="yt.world.equip.item">
|
||||
<field name="code">leather_boots</field>
|
||||
<field name="name">皮靴</field>
|
||||
<field name="icon">🥾</field>
|
||||
<field name="slot">boots</field>
|
||||
<field name="rarity">rare</field>
|
||||
<field name="stats">{"speed": 3, "defense": 1}</field>
|
||||
<field name="desc">结实的皮靴,能保护双脚并提升移动速度。</field>
|
||||
</record>
|
||||
|
||||
<record id="equip_wind_boots" model="yt.world.equip.item">
|
||||
<field name="code">wind_boots</field>
|
||||
<field name="name">疾风靴</field>
|
||||
<field name="icon">👟</field>
|
||||
<field name="slot">boots</field>
|
||||
<field name="rarity">epic</field>
|
||||
<field name="stats">{"speed": 6, "energy_max": 10}</field>
|
||||
<field name="desc">刻有风之符文的靴子,穿戴者仿佛能踏风而行。</field>
|
||||
</record>
|
||||
|
||||
<!-- 饰品 -->
|
||||
<record id="equip_lucky_charm" model="yt.world.equip.item">
|
||||
<field name="code">lucky_charm</field>
|
||||
|
||||
@@ -3,6 +3,7 @@ from . import yt_world_tile
|
||||
from . import yt_world_tile_micro
|
||||
from . import yt_world_build_item
|
||||
from . import yt_world_equip_item
|
||||
from . import yt_world_equip_instance
|
||||
from . import yt_world_build_instance
|
||||
from . import yt_world_stock
|
||||
from . import yt_world_game
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""装备实例(玩家拥有的具体一件装备)。
|
||||
|
||||
与图鉴 yt.world.equip.item 的区别:
|
||||
- item 是「模板」:定义「草鞋」长啥样(name/icon/槽位/基础加成)。
|
||||
- instance 是「实物」:A 生物的那只具体草鞋(耐久 80/100、强化 +2、主人是谁)。
|
||||
|
||||
这样设计是为了给未来的三大系统打地基(与主流 RPG 一致):
|
||||
- 耐久:durability / max_durability 独立维护,每只装备各自掉耐久。
|
||||
- 强化:enhance_level 独立,强化不会污染其他同款装备。
|
||||
- 交易:owner_creature_id 改指另一个生物即可转让,顺着
|
||||
生物→世界→账号自动归属到对应玩家。
|
||||
"""
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class YtWorldEquipInstance(models.Model):
|
||||
_name = 'yt.world.equip.instance'
|
||||
_description = '装备实例(玩家拥有的具体一件装备)'
|
||||
_order = 'id desc'
|
||||
|
||||
item_id = fields.Many2one('yt.world.equip.item', string='模板',
|
||||
required=True, ondelete='cascade', index=True)
|
||||
owner_creature_id = fields.Many2one('yt.world.creature', string='拥有者生物',
|
||||
ondelete='cascade', index=True)
|
||||
# 已装备的部位;空值(False)表示在背包中未装备
|
||||
equipped_slot = fields.Selection(
|
||||
[('weapon', '武器'), ('armor', '护甲'), ('accessory', '饰品'), ('boots', '鞋子')],
|
||||
string='已装备部位', index=True)
|
||||
durability = fields.Integer(string='当前耐久', default=100)
|
||||
max_durability = fields.Integer(string='最大耐久', default=100)
|
||||
enhance_level = fields.Integer(string='强化等级', default=0)
|
||||
# 从模板继承的部位,便于按槽位查询(如"列出某生物所有武器")
|
||||
slot = fields.Selection(related='item_id.slot', string='部位',
|
||||
store=True, readonly=True)
|
||||
|
||||
equipped = fields.Boolean(string='已装备', compute='_compute_equipped',
|
||||
store=True, index=True)
|
||||
|
||||
@api.depends('equipped_slot')
|
||||
def _compute_equipped(self):
|
||||
for r in self:
|
||||
r.equipped = bool(r.equipped_slot)
|
||||
|
||||
def to_dict(self):
|
||||
self.ensure_one()
|
||||
item = self.item_id
|
||||
return {
|
||||
'id': self.id,
|
||||
'item_code': item.code,
|
||||
'name': item.name,
|
||||
'icon': item.icon,
|
||||
'slot': self.slot or item.slot,
|
||||
'rarity': item.rarity,
|
||||
'stats': item.stats or {},
|
||||
'bonus': item._bonus_text(),
|
||||
'durability': self.durability,
|
||||
'max_durability': self.max_durability,
|
||||
'enhance_level': self.enhance_level,
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class YtWorldEquipItem(models.Model):
|
||||
name = fields.Char(string='名称', required=True)
|
||||
icon = fields.Char(string='图标', default='⚔️')
|
||||
slot = fields.Selection(
|
||||
[('weapon', '武器'), ('armor', '护甲'), ('accessory', '饰品')],
|
||||
[('weapon', '武器'), ('armor', '护甲'), ('accessory', '饰品'), ('boots', '鞋子')],
|
||||
string='部位', required=True)
|
||||
rarity = fields.Selection(
|
||||
[('common', '普通'), ('rare', '稀有'), ('epic', '史诗'), ('legendary', '传说')],
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
- yt.world.character : 玩家英雄(等级 / 经验 / 金币 / 背包 / 装备),每世界一条
|
||||
- yt.world.creature : 世界生物(居民 / NPC),含等级 / 经验 / 生命 / 装备 / 职业
|
||||
"""
|
||||
import json
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
@@ -224,18 +225,16 @@ class YtWorldCreature(models.Model):
|
||||
help='idle/working/resting/moving')
|
||||
state = fields.Char(string='动作状态', default='idle',
|
||||
help='idle/walk/attack/work/communicate')
|
||||
equip = fields.Json(string='装备', default=dict,
|
||||
help='{weapon:code, armor:code, accessory:code}')
|
||||
equip_display = fields.Text(string='装备(可读)', compute='_compute_displays')
|
||||
tile_ref = fields.Json(string='所在地块', default=dict)
|
||||
tile_ref_display = fields.Text(string='所在地块(可读)', compute='_compute_displays')
|
||||
partner = fields.Json(string='相关对象', default=dict)
|
||||
partner_display = fields.Text(string='相关对象(可读)', compute='_compute_displays')
|
||||
# 该生物拥有的全部装备实例(背包 + 已装备),与 账号→世界→生物 四级树末端直接关联
|
||||
equip_instance_ids = fields.One2many('yt.world.equip.instance', 'owner_creature_id', string='装备实例')
|
||||
|
||||
@api.depends('equip', 'tile_ref', 'partner')
|
||||
@api.depends('tile_ref', 'partner')
|
||||
def _compute_displays(self):
|
||||
for r in self:
|
||||
r.equip_display = _fmt_kv(r.equip)
|
||||
r.tile_ref_display = _fmt_kv(r.tile_ref)
|
||||
r.partner_display = _fmt_kv(r.partner)
|
||||
|
||||
@@ -270,18 +269,48 @@ class YtWorldCreature(models.Model):
|
||||
vals.setdefault('energy', base['energy_max'])
|
||||
return super(YtWorldCreature, self).create(vals_list)
|
||||
|
||||
def _equip_bonus(self):
|
||||
"""装备提供的临时加成(仅攻击/防御/生命上限;其他数值类型暂不加)。
|
||||
|
||||
汇总当前已装备(equipped_slot 非空)实例的模板 stats,不写回基础字段,
|
||||
因此加成天然"临时"——脱装备即消失,刷新/重载都从实例实时汇总。
|
||||
这里直接查实例表,避免父记录 One2many 缓存未刷新导致加成丢失。"""
|
||||
self.ensure_one()
|
||||
bonus = {'attack': 0, 'defense': 0, 'hp_max': 0}
|
||||
Inst = self.env['yt.world.equip.instance'].sudo()
|
||||
for inst in Inst.search([('owner_creature_id', '=', self.id), ('equipped_slot', '!=', False)]):
|
||||
stats = inst.item_id.stats or {}
|
||||
if not isinstance(stats, dict):
|
||||
try:
|
||||
stats = json.loads(stats)
|
||||
except Exception:
|
||||
stats = {}
|
||||
for k in bonus:
|
||||
v = stats.get(k)
|
||||
if isinstance(v, (int, float)):
|
||||
bonus[k] += int(v)
|
||||
return bonus
|
||||
|
||||
def to_panel_dict(self):
|
||||
self.ensure_one()
|
||||
equip = self.equip or {}
|
||||
catalog = {}
|
||||
for it in self.env['yt.world.equip.item'].sudo().search([]):
|
||||
catalog[it.code] = it.to_dict()
|
||||
# 从装备实例表反查"当前已装备"的装备(实例 -> 模板对象),
|
||||
# 保持前端 {slot: 对象} 结构不变,前端渲染零改动。
|
||||
equip_resolved = {}
|
||||
for slot, raw in (equip or {}).items():
|
||||
# 容错:equip 的值可能是装备 code 字符串,也可能是历史遗留的整件装备字典
|
||||
code = raw.get('code') if isinstance(raw, dict) else raw
|
||||
if code and code in catalog:
|
||||
equip_resolved[slot] = catalog[code]
|
||||
for slot in ('weapon', 'armor', 'accessory', 'boots'):
|
||||
inst = self.env['yt.world.equip.instance'].sudo().search(
|
||||
[('owner_creature_id', '=', self.id), ('equipped_slot', '=', slot)], limit=1)
|
||||
if inst:
|
||||
equip_resolved[slot] = inst.item_id.to_dict()
|
||||
# 装备临时加成:有效属性 = 基础字段 + 已装备实例的 stats 汇总
|
||||
bonus = self._equip_bonus()
|
||||
hp = self.hp or 0
|
||||
hp_max = (self.hp_max or 0) + bonus['hp_max']
|
||||
# 当前生命不超过有效生命上限(如刚脱下 +生命装备导致上限回落)
|
||||
if hp > hp_max:
|
||||
hp = hp_max
|
||||
return {
|
||||
'oid': self.id,
|
||||
'cid': self.cid,
|
||||
@@ -290,12 +319,12 @@ class YtWorldCreature(models.Model):
|
||||
'level': self.level,
|
||||
'exp': self.exp,
|
||||
'exp_max': self.exp_max,
|
||||
'hp': self.hp,
|
||||
'hp_max': self.hp_max,
|
||||
'hp': hp,
|
||||
'hp_max': hp_max,
|
||||
'energy': self.energy,
|
||||
'energy_max': self.energy_max,
|
||||
'attack': self.attack,
|
||||
'defense': self.defense,
|
||||
'attack': (self.attack or 0) + bonus['attack'],
|
||||
'defense': (self.defense or 0) + bonus['defense'],
|
||||
'speed': self.speed,
|
||||
'mood': self.mood,
|
||||
'job': self.job,
|
||||
|
||||
@@ -40,6 +40,9 @@ access_yt_world_creature_public,yt.world.creature public,model_yt_world_creature
|
||||
access_yt_world_equip_item_user,yt.world.equip.item user,model_yt_world_equip_item,base.group_user,1,1,1,1
|
||||
access_yt_world_equip_item_public,yt.world.equip.item public,model_yt_world_equip_item,base.group_public,1,0,0,0
|
||||
access_yt_world_equip_item_portal,yt.world.equip.item portal,model_yt_world_equip_item,base.group_portal,1,1,1,1
|
||||
access_yt_world_equip_instance_user,yt.world.equip.instance user,model_yt_world_equip_instance,base.group_user,1,1,1,1
|
||||
access_yt_world_equip_instance_public,yt.world.equip.instance public,model_yt_world_equip_instance,base.group_public,1,0,0,0
|
||||
access_yt_world_equip_instance_portal,yt.world.equip.instance portal,model_yt_world_equip_instance,base.group_portal,1,1,1,1
|
||||
access_yt_world_race_user,yt.world.race user,model_yt_world_race,base.group_user,1,1,1,1
|
||||
access_yt_world_race_public,yt.world.race public,model_yt_world_race,base.group_public,1,1,1,1
|
||||
access_yt_world_race_portal,yt.world.race portal,model_yt_world_race,base.group_portal,1,1,1,1
|
||||
|
||||
|
@@ -364,19 +364,28 @@
|
||||
<field name="job"/>
|
||||
<field name="state"/>
|
||||
</group>
|
||||
<group string="装备 / 所在地块 / 关联">
|
||||
<field name="equip_display" colspan="4" nolabel="1"
|
||||
widget="textarea" readonly="1" class="o_readonly_text_block"/>
|
||||
<group string="所在地块 / 关联">
|
||||
<field name="tile_ref_display" colspan="4" nolabel="1"
|
||||
widget="textarea" readonly="1" class="o_readonly_text_block"/>
|
||||
<field name="partner_display" colspan="4" nolabel="1"
|
||||
widget="textarea" readonly="1" class="o_readonly_text_block"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="equip" widget="json" colspan="2"/>
|
||||
<field name="tile_ref" widget="json" colspan="2"/>
|
||||
<field name="partner" widget="json" colspan="2"/>
|
||||
</group>
|
||||
<group string="装备实例(与生物关联)">
|
||||
<field name="equip_instance_ids" nolabel="1">
|
||||
<list>
|
||||
<field name="item_id"/>
|
||||
<field name="equipped_slot"/>
|
||||
<field name="durability"/>
|
||||
<field name="max_durability"/>
|
||||
<field name="enhance_level"/>
|
||||
<field name="equipped"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
+11
-6
@@ -401,7 +401,8 @@ function applyAttackDamage(attacker) {
|
||||
if (other.id === attacker.id) continue;
|
||||
const d = Math.hypot(other.x - attacker.x, other.y - attacker.y);
|
||||
if (d <= range) {
|
||||
let dmg = damage;
|
||||
// 防御减免:实际伤害 = max(0, 攻击力 - 防御力);攻击 ≤ 防御则免伤(命中但不掉血)
|
||||
let dmg = Math.max(0, damage - (other.defense || 0));
|
||||
// 护盾优先吸收(矮人铁壁)
|
||||
if ((other.shield || 0) > 0 && dmg > 0) {
|
||||
const absorbed = Math.min(other.shield, dmg);
|
||||
@@ -412,13 +413,16 @@ function applyAttackDamage(attacker) {
|
||||
other.dmgPopups.push({ value: fmt(absorbed), t: 0, shield: true }); // 青色护盾吸收
|
||||
}
|
||||
}
|
||||
// 剩余伤害扣血
|
||||
// 剩余伤害扣血(钳制在 [0, 有效生命上限] 内,防止脱装备后上限回落导致越界)
|
||||
if (!other.dmgPopups) other.dmgPopups = [];
|
||||
if (dmg > 0) {
|
||||
other.hp = Math.max(0, (typeof other.hp === 'number' ? other.hp : (other.hpMax || CHAR_HP_MAX)) - dmg);
|
||||
other.hp = Math.min(other.hpMax, Math.max(0, (typeof other.hp === 'number' ? other.hp : (other.hpMax || CHAR_HP_MAX)) - dmg));
|
||||
other.hurtFlash = 0.4; // 受伤闪白
|
||||
other.lastHurt = now; // 记录受伤时间(脱战恢复计时用)
|
||||
if (!other.dmgPopups) other.dmgPopups = [];
|
||||
other.dmgPopups.push({ value: fmt(dmg), t: 0 }); // 红色扣血
|
||||
} else {
|
||||
// 0 伤害也要显示:命中但被防御/护盾完全减免(免伤)
|
||||
other.dmgPopups.push({ value: '0', t: 0, blocked: true }); // 灰色免伤飘字
|
||||
}
|
||||
// 矮人铁壁:血量首次 ≤30% 时获得 10% 最大生命的护盾
|
||||
if (other.enrage && !other.enrageTriggered && other.hp > 0 && other.hp <= other.hpMax * 0.3) {
|
||||
@@ -820,8 +824,8 @@ function renderChars(ctx, time, mW, mH) {
|
||||
const life = 1 - p.t / 0.9;
|
||||
if (life <= 0) continue;
|
||||
ctx.globalAlpha = Math.min(1, life);
|
||||
ctx.fillStyle = p.shield ? '#37c7f0' : '#ff5040';
|
||||
ctx.fillText('-' + p.value, ch.x, ch.y - 46 - p.t * 26);
|
||||
ctx.fillStyle = p.shield ? '#37c7f0' : p.blocked ? '#9aa3b2' : '#ff5040';
|
||||
ctx.fillText(p.blocked ? '0' : '-' + p.value, ch.x, ch.y - 46 - p.t * 26);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -927,6 +931,7 @@ function updateCharPanel() {
|
||||
${shieldHtml}
|
||||
<div style="margin-top:6px;color:#8a9aaa;font-size:11px;">👟 移速 <b style="color:#e8eef7;">${stats.speed}</b></div>
|
||||
<div style="margin-top:5px;color:#8a9aaa;font-size:11px;">⚔ 攻击 <b style="color:#e8eef7;">${fmt(curAtk)}</b>${atkBonus}</div>
|
||||
<div style="margin-top:5px;color:#8a9aaa;font-size:11px;">🛡 防御 <b style="color:#e8eef7;">${fmt(ch.defense)}</b></div>
|
||||
<div style="margin-top:8px;padding:6px 8px;background:#16213a;border-radius:5px;color:#9fb2cc;font-size:11px;line-height:1.5;">📖 ${rc.trait || '暂无介绍'}</div>
|
||||
<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;">${btns}</div>
|
||||
<div style="margin-top:8px;">
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
+50
-9
@@ -153,6 +153,11 @@ body::before{
|
||||
.card.race .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
|
||||
.card.race .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
|
||||
|
||||
/* 装备/天赋/技能书等通用标签 */
|
||||
.card.equip .tags,.card.talent .tags,.card.skill_book .tags{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px;}
|
||||
.card.equip .tag,.card.talent .tag,.card.skill_book .tag{font-size:9px;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.06);color:#8899aa;}
|
||||
.card.equip{border-color:rgba(120,200,255,.12);}
|
||||
|
||||
/* 野怪等级角标 */
|
||||
.card .tierbadge{position:absolute;top:8px;right:8px;font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;letter-spacing:.5px;}
|
||||
.tierbadge.normal{background:rgba(156,163,175,.2);color:#9ca3af;border:1px solid rgba(156,163,175,.3);}
|
||||
@@ -278,6 +283,7 @@ body::before{
|
||||
<span class="rule-tab" data-cat="talent" onclick="setCat('talent')">🌟 天赋</span>
|
||||
<span class="rule-tab" data-cat="skill_book" onclick="setCat('skill_book')">📖 技能书</span>
|
||||
<span class="rule-tab" data-cat="book" onclick="setCat('book')">📚 书籍</span>
|
||||
<span class="rule-tab" data-cat="equip" onclick="setCat('equip')">🛡️ 装备</span>
|
||||
<span class="rule-tab" data-cat="special" onclick="setCat('special')">✨ 特殊</span>
|
||||
</div>
|
||||
<div class="filterRow">
|
||||
@@ -333,7 +339,8 @@ body::before{
|
||||
const QUALITIES = ['common','uncommon','rare','epic','legendary','mythic'];
|
||||
const QUALITY_NAMES = { common:'普通', uncommon:'优秀', rare:'精良', epic:'史诗', legendary:'传说', mythic:'神话' };
|
||||
const TIER_NAMES = { normal:'普通野怪', elite:'精英野怪', boss:'Boss' };
|
||||
const CAT_ORDER = ['material','processed','creature','monster','race','talent','skill_book','book','special'];
|
||||
const CAT_ORDER = ['material','processed','creature','monster','race','talent','skill_book','book','equip','special'];
|
||||
const SLOT_NAMES = { weapon:'武器', armor:'护甲', accessory:'饰品', boots:'鞋子' };
|
||||
|
||||
// 当前玩家已拥有天赋(从后端加载){ code: { level, active } }
|
||||
let MY_TALENTS = {};
|
||||
@@ -413,6 +420,13 @@ function normBook(b){
|
||||
desc:b.description||b.desc||''
|
||||
};
|
||||
}
|
||||
function normEquip(e){
|
||||
return {
|
||||
type:'equip', id:e.code, cat:'equip', name:e.name, icon:e.icon||'⚔️',
|
||||
qual:e.rarity||'common', slot:e.slot||'weapon', slotName:SLOT_NAMES[e.slot]||'装备',
|
||||
stats:e.stats||{}, bonus:e.bonus||'', desc:e.description||e.desc||''
|
||||
};
|
||||
}
|
||||
|
||||
let ALL_ENTRIES = []; // 归一化后的全部条目(来自后端)
|
||||
let currentCat = 'all';
|
||||
@@ -487,8 +501,8 @@ async function loadCodexData(){
|
||||
const grid = document.getElementById('grid');
|
||||
grid.innerHTML = '<div class="empty show">正在从服务器加载图鉴数据…</div>';
|
||||
try{
|
||||
const [res, cre, mon, rac, talents] = await Promise.all([
|
||||
API.codexResources(), API.codexCreatures(), API.codexMonsters(), API.codexRaces(), API.codexTalents()
|
||||
const [res, cre, mon, rac, talents, equips] = await Promise.all([
|
||||
API.codexResources(), API.codexCreatures(), API.codexMonsters(), API.codexRaces(), API.codexTalents(), API.equipCatalog()
|
||||
]);
|
||||
// 同时尝试拉取当前角色天赋状态;未登录或无角色时忽略。
|
||||
MY_TALENTS = {};
|
||||
@@ -508,6 +522,7 @@ async function loadCodexData(){
|
||||
...(talents||[]).map(normTalent),
|
||||
...SKILL_BOOK_DATA.map(normSkillBook),
|
||||
...BOOK_DATA.map(normBook),
|
||||
...((equips && equips.ok && Array.isArray(equips.items) ? equips.items : equips) || []).map(normEquip),
|
||||
];
|
||||
renderGrid();
|
||||
}catch(err){
|
||||
@@ -525,11 +540,12 @@ function renderGrid(){
|
||||
const hay = [e.name, e.desc, e.effect||'', e.trigger||'', e.skillType||'',
|
||||
e.bookType||'', e.author||'', e.cost||'',
|
||||
(e.products||[]).join(' '), (e.drops||[]).join(' '),
|
||||
(e.traits||[]).join(' '), e.hab||'', e.layer||''].join(' ');
|
||||
(e.traits||[]).join(' '), e.hab||'', e.layer||'',
|
||||
e.slotName||'', e.bonus||''].join(' ');
|
||||
if(!hay.toLowerCase().includes(q)) return false;
|
||||
}
|
||||
if(currentQual!=='all'){
|
||||
if(e.type!=='resource' && e.type!=='talent' && e.type!=='skill_book') return false;
|
||||
if(e.type!=='resource' && e.type!=='talent' && e.type!=='skill_book' && e.type!=='equip') return false;
|
||||
if(e.qual!==currentQual) return false;
|
||||
}
|
||||
return true;
|
||||
@@ -611,6 +627,17 @@ function renderGrid(){
|
||||
<span class="tag">Lv ${e.learnLevel||1}</span>
|
||||
</div>
|
||||
`;
|
||||
} else if(e.type==='equip'){
|
||||
div.classList.add('q-'+e.qual, 'equip');
|
||||
div.innerHTML = `
|
||||
<span class="qbadge ${e.qual}">${QUALITY_NAMES[e.qual]}</span>
|
||||
<div class="icon">${iconHTML(e.icon)}</div>
|
||||
<div class="name">${e.name}</div>
|
||||
<div class="tags">
|
||||
<span class="tag">${e.slotName||'装备'}</span>
|
||||
<span class="tag">${e.bonus||''}</span>
|
||||
</div>
|
||||
`;
|
||||
} else if(e.type==='book'){
|
||||
div.classList.add('book');
|
||||
div.innerHTML = `
|
||||
@@ -662,11 +689,11 @@ function showDetail(e){
|
||||
html += `<div class="md-header">`;
|
||||
html += `<div class="md-icon">${iconHTML(e.icon)}</div>`;
|
||||
html += `<div class="md-info"><div class="md-title">${e.name}</div><div class="md-meta">`;
|
||||
if(e.type==='resource' || e.type==='talent' || e.type==='skill_book'){
|
||||
if(e.type==='resource' || e.type==='talent' || e.type==='skill_book' || e.type==='equip'){
|
||||
const c = e.qual==='common'?'156,163,175':e.qual==='uncommon'?'34,197,94':e.qual==='rare'?'59,130,246':e.qual==='epic'?'168,85,247':e.qual==='legendary'?'245,158,11':'220,38,38';
|
||||
const t = e.qual==='common'?'#9ca3af':e.qual==='uncommon'?'#4ade80':e.qual==='rare'?'#60a5fa':e.qual==='epic'?'#c084fc':e.qual==='legendary'?'#fbbf24':'#f87171';
|
||||
html += `<span class="mt" style="background:rgba(${c},.15);color:${t}">${QUALITY_NAMES[e.qual]}</span>`;
|
||||
const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊';
|
||||
const catLabel = e.type==='talent'?'🌟 天赋':e.type==='skill_book'?'📖 技能书':e.type==='equip'?'🛡️ 装备':e.cat==='material'?'材料':e.cat==='processed'?'加工品':'特殊';
|
||||
html += `<span class="mt">${catLabel}</span>`;
|
||||
} else if(e.type==='creature'){
|
||||
html += `<span class="mt">🐾 生物</span><span class="mt">${e.hab||''}</span><span class="mt">${e.diet||''}</span><span class="mt">${e.tame?'✅ 可驯养':'❌ 野性'}</span>`;
|
||||
@@ -680,8 +707,8 @@ function showDetail(e){
|
||||
}
|
||||
html += `</div></div></div>`;
|
||||
|
||||
// 品质等级条(资源 / 天赋 / 技能书)
|
||||
if(e.type==='resource' || e.type==='talent' || e.type==='skill_book'){
|
||||
// 品质等级条(资源 / 天赋 / 技能书 / 装备)
|
||||
if(e.type==='resource' || e.type==='talent' || e.type==='skill_book' || e.type==='equip'){
|
||||
html += `<div class="qualityBar">`;
|
||||
QUALITIES.forEach((q,i)=>{ html += `<div class="qlv ${i<=qi?'on':''}" title="${QUALITY_NAMES[q]}"></div>`; });
|
||||
html += `</div>`;
|
||||
@@ -779,6 +806,20 @@ function showDetail(e){
|
||||
}
|
||||
}
|
||||
|
||||
// 装备详情
|
||||
if(e.type==='equip'){
|
||||
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
|
||||
html += `<div class="gi"><span class="gl">部位</span><span class="gv">${e.slotName||'—'}</span></div>`;
|
||||
html += `<div class="gi"><span class="gl">品质</span><span class="gv">${QUALITY_NAMES[e.qual]||'—'}</span></div>`;
|
||||
if(e.bonus){
|
||||
html += `<div class="gi" style="grid-column:1/-1;"><span class="gl">加成</span><span class="gv">${e.bonus}</span></div>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
if(e.desc){
|
||||
html += `<div class="md-section"><h3>📖 描述</h3><div class="md-desc">${e.desc}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 书籍详情
|
||||
if(e.type==='book'){
|
||||
html += `<div class="md-section"><h3>📋 属性</h3><div class="md-grid">`;
|
||||
|
||||
+113
-29
@@ -942,6 +942,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
border: 1px solid rgba(139,92,246,.35); border-radius: 14px;
|
||||
box-shadow: 0 14px 44px rgba(0,0,0,.55), 0 0 0 1px rgba(139,92,246,.12), inset 0 1px 0 rgba(255,255,255,.06);
|
||||
padding: 16px 18px; max-width: 480px; width: 94%; pointer-events: auto;
|
||||
max-height: 90vh; overflow-y: auto;
|
||||
}
|
||||
/* 头部:标题 + 右上角关闭(MMO 风格) */
|
||||
#unitModal .umHead { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;
|
||||
@@ -953,7 +954,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
#unitModal .umClose:hover { background:rgba(255,80,80,.28); color:#fff; }
|
||||
/* 主体两栏:左侧立绘+环绕装备槽,右侧数值 */
|
||||
#unitModal .umGrid { display:grid; grid-template-columns: 200px 1fr; gap:14px; }
|
||||
#unitModal .umLeft { position:relative; border-radius:12px; padding:14px 10px 10px;
|
||||
#unitModal .umLeft { position:relative; border-radius:12px; padding:14px 10px 90px;
|
||||
display:flex; flex-direction:column; align-items:center;
|
||||
background: radial-gradient(ellipse at 50% 26%, rgba(139,92,246,.14), rgba(255,255,255,.03) 62%);
|
||||
border: 1px solid rgba(139,92,246,.28);
|
||||
@@ -964,16 +965,35 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
#unitModal .umPortraitFallback { font-size:72px; line-height:180px; text-align:center; }
|
||||
#unitModal .umEq { position:absolute; width:46px; height:46px; border-radius:9px;
|
||||
background:rgba(8,14,26,.78); border:1px solid rgba(120,200,255,.3);
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
cursor:pointer; z-index:2; transition:border-color .15s, background .15s, box-shadow .15s; }
|
||||
#unitModal .umEq:hover { border-color:rgba(139,92,246,.75); background:rgba(139,92,246,.2); box-shadow:0 0 10px rgba(139,92,246,.35); }
|
||||
#unitModal .umEq .umEqIcon { font-size:19px; line-height:1; }
|
||||
#unitModal .umEq .umEqName { font-size:9px; color:#8899aa; margin-top:2px; max-width:44px;
|
||||
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
#unitModal .umEq .umEqIcon { font-size:19px; line-height:1; display:flex; align-items:center; justify-content:center; }
|
||||
#unitModal .umEq .umEqIcon img { width:28px; height:28px; object-fit:contain; filter:drop-shadow(0 1px 1px rgba(0,0,0,.5)); }
|
||||
/* 装备名称统一放在框正下方 */
|
||||
#unitModal .umEq .umEqName {
|
||||
position:absolute; top:100%; left:50%; transform:translateX(-50%);
|
||||
margin-top:3px; font-size:9px; color:#a8b8cc; white-space:nowrap;
|
||||
text-shadow:0 1px 2px rgba(0,0,0,.7); pointer-events:none;
|
||||
}
|
||||
#unitModal .umEq.accessory { top:-10px; left:50%; transform:translateX(-50%); }
|
||||
#unitModal .umEq.weapon { left:-12px; top:50%; transform:translateY(-50%); }
|
||||
#unitModal .umEq.armor { right:-12px; top:50%; transform:translateY(-50%); }
|
||||
#unitModal .umEquipPicker { margin-top:10px; width:100%; }
|
||||
#unitModal .umEq.boots { bottom:-24px; left:50%; transform:translateX(-50%); }
|
||||
#unitModal .umEquipPicker {
|
||||
margin-top:92px; width:100%;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
background: rgba(8,14,26,0.96);
|
||||
border: 1px solid rgba(139,92,246,0.32);
|
||||
border-radius: 10px;
|
||||
padding: 7px 5px 7px 7px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
||||
}
|
||||
/* 未展开时隐藏空容器,避免在鞋子槽下方留下黑色长条 */
|
||||
#unitModal .umEquipPicker[data-slot=""] { display:none; }
|
||||
#unitModal .umEquipPicker::-webkit-scrollbar { width: 4px; }
|
||||
#unitModal .umEquipPicker::-webkit-scrollbar-thumb { background: rgba(139,92,246,0.45); border-radius: 3px; }
|
||||
#unitModal .umRight { min-width:0; }
|
||||
#unitModal .umStatGrid { display:grid; grid-template-columns:1fr 1fr; gap:2px 14px; margin-bottom:8px; }
|
||||
#unitModal .umStatGrid .statBar { display:none; }
|
||||
@@ -993,10 +1013,34 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
||||
#unitModal .cmdGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
#unitModal .cmdGrid .cmdBtn { margin-top: 0; }
|
||||
/* 装备候选列表(立绘旁装备槽点击后展开) */
|
||||
#unitModal .equipSlotName { font-size: 11px; color: #8899aa; margin-bottom: 5px; }
|
||||
#unitModal .equipOpt { font-size: 12px; color: #cfe; padding: 5px 6px; margin-top: 4px; background: rgba(100,200,255,0.1); border-radius: 6px; cursor: pointer; }
|
||||
#unitModal .equipOpt:hover { background: rgba(100,200,255,0.22); }
|
||||
#unitModal .equipBonus { color: #8bc34a; font-size: 10px; margin-left: 3px; }
|
||||
#unitModal .equipSlotName {
|
||||
font-size: 11px; color: #a8b8cc;
|
||||
margin-bottom: 6px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
#unitModal .equipSlotName .equipClose {
|
||||
font-size: 15px; line-height: 1; color: #8899aa; cursor: pointer;
|
||||
padding: 0 3px; border-radius: 3px; transition: color .15s, background .15s;
|
||||
}
|
||||
#unitModal .equipSlotName .equipClose:hover { color: #fff; background: rgba(255,80,80,0.25); }
|
||||
#unitModal .equipOpt {
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
font-size: 12px; color: #cfe;
|
||||
padding: 5px 6px; margin-top: 3px;
|
||||
background: rgba(100,200,255,0.08); border-radius: 6px; cursor: pointer;
|
||||
line-height: 1.2;
|
||||
}
|
||||
#unitModal .equipOpt:hover { background: rgba(100,200,255,0.2); }
|
||||
#unitModal .equipOpt.unequip { color: #ff9a9a; background: rgba(255,80,80,.1); }
|
||||
#unitModal .equipOpt.unequip:hover { background: rgba(255,80,80,.2); }
|
||||
#unitModal .equipOpt .umEqIcon {
|
||||
font-size: 16px; display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0; width: 22px; height: 22px;
|
||||
}
|
||||
#unitModal .equipOpt .umEqIcon img { width: 22px; height: 22px; object-fit: contain; filter: drop-shadow(0 1px 1px rgba(0,0,0,.5)); }
|
||||
#unitModal .equipOpt .equipMeta { display: flex; flex-direction: column; min-width: 0; }
|
||||
#unitModal .equipOpt .equipName { font-size: 12px; color: #cfe; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
#unitModal .equipOpt .equipBonus { color: #8bc34a; font-size: 10px; margin-left: 0; }
|
||||
|
||||
/* 微观建筑悬停提示 tooltip */
|
||||
#microTip {
|
||||
@@ -2597,6 +2641,11 @@ const EQUIP_CATALOG_FALLBACK = {
|
||||
{ code: 'power_ring', name: '力量戒指', icon: '💍', bonus: '攻+4 防+2' },
|
||||
{ code: 'amulet_life', name: '生命护符', icon: '📿', bonus: '血+25 能+15' },
|
||||
],
|
||||
boots: [
|
||||
{ code: 'straw_sandals', name: '草鞋', icon: '🩴', bonus: '速+1' },
|
||||
{ code: 'leather_boots', name: '皮靴', icon: '🥾', bonus: '速+3 防+1' },
|
||||
{ code: 'wind_boots', name: '疾风靴', icon: '👟', bonus: '速+6 能+10' },
|
||||
],
|
||||
};
|
||||
let EQUIP_CATALOG_SERVER = null;
|
||||
|
||||
@@ -2652,7 +2701,7 @@ async function microLoadEquipCatalog() {
|
||||
try {
|
||||
const r = await API.equipCatalog();
|
||||
if (r && r.ok && Array.isArray(r.items)) {
|
||||
const bySlot = { weapon: [], armor: [], accessory: [] };
|
||||
const bySlot = { weapon: [], armor: [], accessory: [], boots: [] };
|
||||
r.items.forEach(it => {
|
||||
if (!bySlot[it.slot]) bySlot[it.slot] = [];
|
||||
bySlot[it.slot].push(it);
|
||||
@@ -2672,7 +2721,7 @@ async function microLoadGame() {
|
||||
const [c, cr] = await Promise.all([API.char(), API.creatures(), microLoadEquipCatalog()]);
|
||||
microChar = (c && c.ok !== false) ? c : (c || null);
|
||||
if (microChar) {
|
||||
if (!microChar.equip) microChar.equip = { weapon: null, armor: null, accessory: null };
|
||||
if (!microChar.equip) microChar.equip = { weapon: null, armor: null, accessory: null, boots: null };
|
||||
if (!microChar.inventory) microChar.inventory = {};
|
||||
['gold','copper','silver','diamond'].forEach(k => { if (microChar[k] == null) microChar[k] = 0; });
|
||||
}
|
||||
@@ -2711,7 +2760,7 @@ function microMakeUnitFromCreature(x) {
|
||||
energy: (x.energy != null) ? x.energy : 80, energy_max: x.energy_max || 100,
|
||||
attack: x.attack || 5, defense: x.defense || 3, speed: x.speed || 90,
|
||||
mood: x.mood || '😊', job: x.job || 'none',
|
||||
equip: x.equip || { weapon: null, armor: null, accessory: null },
|
||||
equip: x.equip || { weapon: null, armor: null, accessory: null, boots: null },
|
||||
status: x.state || x.status || 'resting', _buildingRender: true
|
||||
};
|
||||
}
|
||||
@@ -2785,8 +2834,8 @@ function microEnsureCharPlaced(unit) {
|
||||
|
||||
// equip 的值可能是装备对象(后端 to_panel_dict 解析后的结果),回存时统一压缩为 code
|
||||
function _normalizeEquip(eq) {
|
||||
const out = { weapon: null, armor: null, accessory: null };
|
||||
['weapon', 'armor', 'accessory'].forEach(s => {
|
||||
const out = { weapon: null, armor: null, accessory: null, boots: null };
|
||||
['weapon', 'armor', 'accessory', 'boots'].forEach(s => {
|
||||
const v = eq && eq[s];
|
||||
out[s] = (v && typeof v === 'object') ? (v.code || null) : (v || null);
|
||||
});
|
||||
@@ -2805,12 +2854,15 @@ async function microSaveCreatures() {
|
||||
const list = microState.units
|
||||
.filter(u => u._buildingRender)
|
||||
.map(u => ({
|
||||
cid: u.id, name: u.name, species: u.role || 'none',
|
||||
level: u.level, exp: u.exp, hp: u.hp, hp_max: u.hp_max || 120,
|
||||
energy: u.energy, mood: u.mood, job: u.job || 'none',
|
||||
equip: _normalizeEquip(u.equip),
|
||||
cid: u.id, name: u.name, race: u.role || u.species || 'human',
|
||||
level: u.level, exp: u.exp, exp_max: u.exp_max || 100,
|
||||
hp: u.hp, hp_max: u.hp_max || 120,
|
||||
energy: u.energy, energy_max: u.energy_max || 100,
|
||||
attack: u.attack, defense: u.defense, speed: u.speed,
|
||||
mood: u.mood, job: u.job || 'none',
|
||||
tile_ref: microState.tile ? { q: microState.tile.q, r: microState.tile.r } : {},
|
||||
state: u.status || 'resting',
|
||||
status: u.status || 'idle',
|
||||
state: u.state || u.status || 'idle',
|
||||
}));
|
||||
const res = await API.saveCreatures(list);
|
||||
if (res && res.ok && Array.isArray(res.creatures)) {
|
||||
@@ -5563,9 +5615,31 @@ function microSyncCharLocal(char) {
|
||||
const idx = microState.units.findIndex(u => u.id === key || u.cid === key);
|
||||
const icon = char.icon || _raceIconFallback(char.race);
|
||||
const synced = { ...char, icon, cid: key };
|
||||
if (idx >= 0) { Object.assign(microState.units[idx], synced); return microState.units[idx]; }
|
||||
microState.units.push(synced);
|
||||
return synced;
|
||||
if (idx >= 0) { Object.assign(microState.units[idx], synced); }
|
||||
else { microState.units.push(synced); }
|
||||
|
||||
// 同步 characters.js 的 microChars:装备/训练后后端返回的是有效属性,
|
||||
// 必须让 microChars 也持有最新值,否则 characters.js 的自动保存会把旧值
|
||||
//(不含新装备加成)当基础值回写,导致再次打开面板时加成"消失"。
|
||||
if (typeof window !== 'undefined' && window.microChars) {
|
||||
const cidx = window.microChars.findIndex(c => c.id === key || c.cid === key);
|
||||
if (cidx >= 0) {
|
||||
const ch = window.microChars[cidx];
|
||||
if (typeof char.attack === 'number') ch.attack = char.attack;
|
||||
if (typeof char.defense === 'number') ch.defense = char.defense;
|
||||
if (typeof char.speed === 'number') ch.speed = char.speed;
|
||||
if (typeof char.hp === 'number') ch.hp = char.hp;
|
||||
if (typeof char.hp_max === 'number') ch.hpMax = char.hp_max;
|
||||
if (typeof char.energy === 'number') ch.energy = char.energy;
|
||||
if (typeof char.energy_max === 'number') ch.energy_max = char.energy_max;
|
||||
if (typeof char.level === 'number') ch.level = char.level;
|
||||
if (typeof char.exp === 'number') ch.exp = char.exp;
|
||||
if (typeof char.exp_max === 'number') ch.exp_max = char.exp_max;
|
||||
if (char.equip) ch.equip = char.equip;
|
||||
}
|
||||
}
|
||||
|
||||
return idx >= 0 ? microState.units[idx] : synced;
|
||||
}
|
||||
function _localCharFromMicroChars(unitId) {
|
||||
// 兼容 characters.js 精灵图角色:从 microChars 取本地运行时数据转成本地面板对象
|
||||
@@ -5648,7 +5722,7 @@ async function showUnitModal(unitId) {
|
||||
const eqCell = (slot, label) => {
|
||||
const cur = unit.equip && unit.equip[slot];
|
||||
return `<div class="umEq ${slot}" onclick="umTogglePicker('${slot}')" title="${label}${cur ? ':' + cur.name : '(空,点击穿戴)'}">
|
||||
<div class="umEqIcon">${cur ? (cur.icon || '⚔️') : '➕'}</div>
|
||||
<div class="umEqIcon">${cur ? eqIconHTML(cur.icon) : '➕'}</div>
|
||||
<div class="umEqName">${cur ? cur.name : label}</div>
|
||||
</div>`;
|
||||
};
|
||||
@@ -5663,6 +5737,7 @@ async function showUnitModal(unitId) {
|
||||
${eqCell('weapon','武器')}
|
||||
<div class="umPortrait">${portrait}</div>
|
||||
${eqCell('armor','护甲')}
|
||||
${eqCell('boots','鞋子')}
|
||||
</div>
|
||||
<div id="umPicker" class="umEquipPicker" data-slot=""></div>
|
||||
</div>
|
||||
@@ -5719,6 +5794,15 @@ function _drawRacePortrait(src, cv) {
|
||||
img.src = src;
|
||||
}
|
||||
|
||||
// 装备图标渲染:图片路径→<img>,否则 emoji
|
||||
function eqIconHTML(ic){
|
||||
if(!ic) return '➕';
|
||||
if(/^(assets\/|\/|https?:\/\/)/i.test(ic) || /\.(png|webp|jpe?g|gif|svg)(\?.*)?$/i.test(ic)){
|
||||
return `<img src="${ic}" alt="">`;
|
||||
}
|
||||
return ic || '⚔️';
|
||||
}
|
||||
|
||||
// 点击立绘旁装备槽:展开/收起该槽的候选装备列表(穿戴写 Odoo,与原 equipWrap 同源)
|
||||
window.umTogglePicker = function(slot){
|
||||
const box = document.getElementById('umPicker');
|
||||
@@ -5726,17 +5810,17 @@ window.umTogglePicker = function(slot){
|
||||
if (!box || !unit) return;
|
||||
if (box.dataset.slot === slot) { box.innerHTML = ''; box.dataset.slot = ''; return; }
|
||||
box.dataset.slot = slot;
|
||||
const map = { weapon: '武器', armor: '护甲', accessory: '饰品' };
|
||||
const map = { weapon: '武器', armor: '护甲', accessory: '饰品', boots: '鞋子' };
|
||||
const cur = unit.equip && unit.equip[slot];
|
||||
const cid = unit.cid || unit.id;
|
||||
let html = `<div class="equipSlotName">${map[slot]} · 点击穿戴</div>`;
|
||||
let html = `<div class="equipSlotName"><span>${map[slot]} · 点击穿戴</span><span class="equipClose" onclick="umTogglePicker('${slot}');event.stopPropagation();">×</span></div>`;
|
||||
if (unit._localOnly) {
|
||||
html += `<div style="color:#888;font-size:12px;padding:4px 0;">等待服务器同步…</div>`;
|
||||
} else {
|
||||
if (cur) html += `<div class="equipOpt" onclick="microEquip('${cid}','${slot}',null,true)" style="color:#ff9a9a;background:rgba(255,80,80,.12);">✕ 卸下 ${cur.name}</div>`;
|
||||
if (cur) html += `<div class="equipOpt unequip" onclick="microEquip('${cid}','${slot}',null,true)"><span class="umEqIcon">✕</span><span class="equipMeta"><span class="equipName">卸下 ${cur.name}</span></span></div>`;
|
||||
const catalog = microEquipCatalog();
|
||||
(catalog[slot] || []).forEach(e => {
|
||||
html += `<div class="equipOpt" onclick="microEquip('${cid}','${slot}','${e.code}',false)">${e.icon || '⚔️'} ${e.name}<span class="equipBonus">${e.bonus || ''}</span></div>`;
|
||||
html += `<div class="equipOpt" onclick="microEquip('${cid}','${slot}','${e.code}',false)"><span class="umEqIcon">${eqIconHTML(e.icon)}</span><span class="equipMeta"><span class="equipName">${e.name}</span><span class="equipBonus">${e.bonus || ''}</span></span></div>`;
|
||||
});
|
||||
}
|
||||
box.innerHTML = html;
|
||||
@@ -5790,7 +5874,7 @@ async function updateChar() {
|
||||
});
|
||||
if (r && r.ok && r.char) {
|
||||
microChar = r.char;
|
||||
if (!microChar.equip) microChar.equip = { weapon: null, armor: null, accessory: null };
|
||||
if (!microChar.equip) microChar.equip = { weapon: null, armor: null, accessory: null, boots: null };
|
||||
if (!microChar.inventory) microChar.inventory = {};
|
||||
['gold','copper','silver','diamond'].forEach(k => { if (microChar[k] == null) microChar[k] = 0; });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user