diff --git a/addons/yt_world/controllers/world_api.py b/addons/yt_world/controllers/world_api.py index 7f22b77..495c5f3 100644 --- a/addons/yt_world/controllers/world_api.py +++ b/addons/yt_world/controllers/world_api.py @@ -1049,9 +1049,12 @@ class YtWorldApi(http.Controller): @http.route('/yt_world/api/equip/catalog', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False) def equip_catalog(self, **kw): - """装备目录(单一事实来源)。""" - items = request.env['yt.world.equip.item'].sudo().search([('active', '=', True)]) - return _json({'ok': True, 'items': [it.to_dict() for it in items]}) + """装备目录(单一事实来源)。同时返回 slots 槽位真源列表, + 前端面板据此动态渲染(含尚无装备的空槽位)。""" + Item = request.env['yt.world.equip.item'].sudo() + items = Item.search([('active', '=', True)]) + slots = [{'value': v, 'label': l} for v, l in Item._fields['slot'].selection] + return _json({'ok': True, 'items': [it.to_dict() for it in items], 'slots': slots}) def _creature_by_cid(self, world, cid): if not cid: @@ -1093,7 +1096,8 @@ class YtWorldApi(http.Controller): if action == 'equip': slot = body.get('slot') code = body.get('code') - if slot not in ('weapon', 'armor', 'accessory', 'boots'): + valid_slots = [s[0] for s in request.env['yt.world.equip.item']._fields['slot'].selection] + if slot not in valid_slots: return _json({'ok': False, 'error': 'bad_slot'}) item = request.env['yt.world.equip.item'].sudo().search([('code', '=', code)], limit=1) if not item: diff --git a/addons/yt_world/data/yt_world_equip_item.xml b/addons/yt_world/data/yt_world_equip_item.xml index 30afe69..ecd7e49 100644 --- a/addons/yt_world/data/yt_world_equip_item.xml +++ b/addons/yt_world/data/yt_world_equip_item.xml @@ -125,4 +125,121 @@ {"hp_max": 25, "energy_max": 15} 蕴含生命能量的护符,佩戴者精力更充沛。 + + + + straw_hat + 草帽 + assets/icons/equips/straw_hat.png + hat + common + {"defense": 1} + 编织草帽,遮阳挡雨,略增防护。 + + + iron_helm + 铁盔 + assets/icons/equips/iron_helm.png + hat + rare + {"defense": 3, "hp_max": 8} + 冷锻铁盔,关键时刻护住头脸。 + + + gold_crown + 黄金王冠 + assets/icons/equips/gold_crown.png + hat + legendary + {"defense": 5, "hp_max": 20, "attack": 5} + 象征权柄的黄金王冠,威仪凛然。 + + + + + cloth_pants + 布裤 + assets/icons/equips/cloth_pants.png + pants + common + {"defense": 1} + 轻便布裤,行动无碍。 + + + iron_leggings + 铁护腿 + assets/icons/equips/iron_leggings.png + pants + rare + {"defense": 3, "hp_max": 10} + 裹住小腿的铁片护腿,稳健可靠。 + + + dragon_pants + 龙鳞战裤 + assets/icons/equips/dragon_pants.png + pants + legendary + {"defense": 6, "hp_max": 25} + 龙鳞缀成的战裤,刀枪难入。 + + + + + leather_bracer + 皮护腕 + assets/icons/equips/leather_bracer.png + bracelet + common + {"attack": 2} + 皮质护腕,挥击更稳。 + + + + silver_bracer + 银手镯 + assets/icons/equips/silver_bracer.png + accessory + rare + {"attack": 4, "defense": 1} + 嵌银手镯,攻防兼修。 + + + dragon_bracelet + 龙纹手镯 + assets/icons/equips/dragon_bracelet.png + accessory + epic + {"attack": 8, "hp_max": 10} + 刻龙纹的护腕,蓄力更猛。 + + + + + rope_belt + 麻绳腰带 + assets/icons/equips/rope_belt.png + belt + common + {"hp_max": 5} + 粗麻绳束腰,勉强管用。 + + + iron_belt + 铁腰封 + assets/icons/equips/iron_belt.png + belt + rare + {"hp_max": 12, "defense": 2} + 铁片腰封,护住躯干。 + + + dragon_belt + 龙鳞腰带 + assets/icons/equips/dragon_belt.png + belt + epic + {"hp_max": 25, "defense": 4} + 龙鳞束腰,根骨更稳。 + diff --git a/addons/yt_world/models/yt_world_equip_instance.py b/addons/yt_world/models/yt_world_equip_instance.py index c41c313..ea7174b 100644 --- a/addons/yt_world/models/yt_world_equip_instance.py +++ b/addons/yt_world/models/yt_world_equip_instance.py @@ -12,6 +12,7 @@ 生物→世界→账号自动归属到对应玩家。 """ from odoo import models, fields, api +from odoo.addons.yt_world.models.yt_world_equip_item import YtWorldEquipItem class YtWorldEquipInstance(models.Model): @@ -23,10 +24,10 @@ class YtWorldEquipInstance(models.Model): required=True, ondelete='cascade', index=True) owner_creature_id = fields.Many2one('yt.world.creature', string='拥有者生物', ondelete='cascade', index=True) - # 已装备的部位;空值(False)表示在背包中未装备 + # 已装备的部位;空值(False)表示在背包中未装备。 + # 与 yt.world.equip.item 的 SLOT_SELECTION 共用同一份常量,避免两处各写一份槽位列表。 equipped_slot = fields.Selection( - [('weapon', '武器'), ('armor', '护甲'), ('accessory', '饰品'), ('boots', '鞋子')], - string='已装备部位', index=True) + YtWorldEquipItem.SLOT_SELECTION, string='已装备部位', index=True) durability = fields.Integer(string='当前耐久', default=100) max_durability = fields.Integer(string='最大耐久', default=100) enhance_level = fields.Integer(string='强化等级', default=0) diff --git a/addons/yt_world/models/yt_world_equip_item.py b/addons/yt_world/models/yt_world_equip_item.py index 60fae33..0014b5e 100644 --- a/addons/yt_world/models/yt_world_equip_item.py +++ b/addons/yt_world/models/yt_world_equip_item.py @@ -21,9 +21,13 @@ class YtWorldEquipItem(models.Model): code = fields.Char(string='编码', required=True, index=True) name = fields.Char(string='名称', required=True) icon = fields.Char(string='图标', default='⚔️') - slot = fields.Selection( - [('weapon', '武器'), ('armor', '护甲'), ('accessory', '饰品'), ('boots', '鞋子')], - string='部位', required=True) + # 部位槽位:单一真源。前端面板 / 实例表 / catalog 接口均以本选择项为准, + # 新增槽位只需在此追加(及 yt.world.equip.instance 的 equipped_slot),前端自动渲染。 + SLOT_SELECTION = [ + ('weapon', '武器'), ('armor', '护甲'), ('accessory', '饰品'), ('boots', '鞋子'), + ('hat', '帽子'), ('pants', '裤子'), ('bracelet', '护臂'), ('belt', '腰带'), + ] + slot = fields.Selection(SLOT_SELECTION, string='部位', required=True) rarity = fields.Selection( [('common', '普通'), ('rare', '稀有'), ('epic', '史诗'), ('legendary', '传说')], string='稀有度', default='common') diff --git a/addons/yt_world/models/yt_world_game.py b/addons/yt_world/models/yt_world_game.py index ce2e121..b6ee781 100644 --- a/addons/yt_world/models/yt_world_game.py +++ b/addons/yt_world/models/yt_world_game.py @@ -298,8 +298,10 @@ class YtWorldCreature(models.Model): catalog[it.code] = it.to_dict() # 从装备实例表反查"当前已装备"的装备(实例 -> 模板对象), # 保持前端 {slot: 对象} 结构不变,前端渲染零改动。 + # 槽位列表直接取自装备图鉴模型的 SLOT_SELECTION(单一真源),新增槽位无需改此处。 equip_resolved = {} - for slot in ('weapon', 'armor', 'accessory', 'boots'): + equip_slots = [s[0] for s in self.env['yt.world.equip.item']._fields['slot'].selection] + for slot in equip_slots: inst = self.env['yt.world.equip.instance'].sudo().search( [('owner_creature_id', '=', self.id), ('equipped_slot', '=', slot)], limit=1) if inst: diff --git a/assets/icons/equips/cloth_pants.png b/assets/icons/equips/cloth_pants.png new file mode 100644 index 0000000..eb38bfd Binary files /dev/null and b/assets/icons/equips/cloth_pants.png differ diff --git a/assets/icons/equips/dragon_belt.png b/assets/icons/equips/dragon_belt.png new file mode 100644 index 0000000..d3786dd Binary files /dev/null and b/assets/icons/equips/dragon_belt.png differ diff --git a/assets/icons/equips/dragon_bracelet.png b/assets/icons/equips/dragon_bracelet.png new file mode 100644 index 0000000..a1b8fa1 Binary files /dev/null and b/assets/icons/equips/dragon_bracelet.png differ diff --git a/assets/icons/equips/dragon_pants.png b/assets/icons/equips/dragon_pants.png new file mode 100644 index 0000000..b955c0e Binary files /dev/null and b/assets/icons/equips/dragon_pants.png differ diff --git a/assets/icons/equips/gen/Hearthstone_game_icon__BIG_bol_2026-09-20T09-07-17.png b/assets/icons/equips/gen/Hearthstone_game_icon__BIG_bol_2026-09-20T09-07-17.png new file mode 100644 index 0000000..6b76806 Binary files /dev/null and b/assets/icons/equips/gen/Hearthstone_game_icon__BIG_bol_2026-09-20T09-07-17.png differ diff --git a/assets/icons/equips/gen/Hearthstone_game_icon__BIG_bol_2026-09-20T09-08-41.png b/assets/icons/equips/gen/Hearthstone_game_icon__BIG_bol_2026-09-20T09-08-41.png new file mode 100644 index 0000000..73669a3 Binary files /dev/null and b/assets/icons/equips/gen/Hearthstone_game_icon__BIG_bol_2026-09-20T09-08-41.png differ diff --git a/assets/icons/equips/gen/Hearthstone_game_icon__single__2026-09-20T08-55-08.png b/assets/icons/equips/gen/Hearthstone_game_icon__single__2026-09-20T08-55-08.png new file mode 100644 index 0000000..84faedd Binary files /dev/null and b/assets/icons/equips/gen/Hearthstone_game_icon__single__2026-09-20T08-55-08.png differ diff --git a/assets/icons/equips/gen/cloth_pants_review.png b/assets/icons/equips/gen/cloth_pants_review.png new file mode 100644 index 0000000..eb38bfd Binary files /dev/null and b/assets/icons/equips/gen/cloth_pants_review.png differ diff --git a/assets/icons/equips/gen/dragon_belt_review.png b/assets/icons/equips/gen/dragon_belt_review.png new file mode 100644 index 0000000..d3786dd Binary files /dev/null and b/assets/icons/equips/gen/dragon_belt_review.png differ diff --git a/assets/icons/equips/gen/dragon_bracelet_review.png b/assets/icons/equips/gen/dragon_bracelet_review.png new file mode 100644 index 0000000..a1b8fa1 Binary files /dev/null and b/assets/icons/equips/gen/dragon_bracelet_review.png differ diff --git a/assets/icons/equips/gen/dragon_pants_review.png b/assets/icons/equips/gen/dragon_pants_review.png new file mode 100644 index 0000000..b955c0e Binary files /dev/null and b/assets/icons/equips/gen/dragon_pants_review.png differ diff --git a/assets/icons/equips/gen/gold_crown_review.png b/assets/icons/equips/gen/gold_crown_review.png new file mode 100644 index 0000000..3d01430 Binary files /dev/null and b/assets/icons/equips/gen/gold_crown_review.png differ diff --git a/assets/icons/equips/gen/iron_belt_review.png b/assets/icons/equips/gen/iron_belt_review.png new file mode 100644 index 0000000..799454e Binary files /dev/null and b/assets/icons/equips/gen/iron_belt_review.png differ diff --git a/assets/icons/equips/gen/iron_helm_review.png b/assets/icons/equips/gen/iron_helm_review.png new file mode 100644 index 0000000..341653c Binary files /dev/null and b/assets/icons/equips/gen/iron_helm_review.png differ diff --git a/assets/icons/equips/gen/iron_leggings_review.png b/assets/icons/equips/gen/iron_leggings_review.png new file mode 100644 index 0000000..a72452c Binary files /dev/null and b/assets/icons/equips/gen/iron_leggings_review.png differ diff --git a/assets/icons/equips/gen/leather_bracer_review.png b/assets/icons/equips/gen/leather_bracer_review.png new file mode 100644 index 0000000..8db877b Binary files /dev/null and b/assets/icons/equips/gen/leather_bracer_review.png differ diff --git a/assets/icons/equips/gen/rope_belt_review.png b/assets/icons/equips/gen/rope_belt_review.png new file mode 100644 index 0000000..ddf4038 Binary files /dev/null and b/assets/icons/equips/gen/rope_belt_review.png differ diff --git a/assets/icons/equips/gen/silver_bracer_review.png b/assets/icons/equips/gen/silver_bracer_review.png new file mode 100644 index 0000000..807deb5 Binary files /dev/null and b/assets/icons/equips/gen/silver_bracer_review.png differ diff --git a/assets/icons/equips/gen/straw_hat_review.png b/assets/icons/equips/gen/straw_hat_review.png new file mode 100644 index 0000000..c13e4d8 Binary files /dev/null and b/assets/icons/equips/gen/straw_hat_review.png differ diff --git a/assets/icons/equips/gold_crown.png b/assets/icons/equips/gold_crown.png new file mode 100644 index 0000000..3d01430 Binary files /dev/null and b/assets/icons/equips/gold_crown.png differ diff --git a/assets/icons/equips/iron_belt.png b/assets/icons/equips/iron_belt.png new file mode 100644 index 0000000..799454e Binary files /dev/null and b/assets/icons/equips/iron_belt.png differ diff --git a/assets/icons/equips/iron_helm.png b/assets/icons/equips/iron_helm.png new file mode 100644 index 0000000..341653c Binary files /dev/null and b/assets/icons/equips/iron_helm.png differ diff --git a/assets/icons/equips/iron_leggings.png b/assets/icons/equips/iron_leggings.png new file mode 100644 index 0000000..a72452c Binary files /dev/null and b/assets/icons/equips/iron_leggings.png differ diff --git a/assets/icons/equips/leather_bracer.png b/assets/icons/equips/leather_bracer.png new file mode 100644 index 0000000..8db877b Binary files /dev/null and b/assets/icons/equips/leather_bracer.png differ diff --git a/assets/icons/equips/rope_belt.png b/assets/icons/equips/rope_belt.png new file mode 100644 index 0000000..ddf4038 Binary files /dev/null and b/assets/icons/equips/rope_belt.png differ diff --git a/assets/icons/equips/silver_bracer.png b/assets/icons/equips/silver_bracer.png new file mode 100644 index 0000000..807deb5 Binary files /dev/null and b/assets/icons/equips/silver_bracer.png differ diff --git a/assets/icons/equips/straw_hat.png b/assets/icons/equips/straw_hat.png new file mode 100644 index 0000000..c13e4d8 Binary files /dev/null and b/assets/icons/equips/straw_hat.png differ diff --git a/world.html b/world.html index d5ae29c..8936f85 100644 --- a/world.html +++ b/world.html @@ -1094,21 +1094,31 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa display:flex;align-items:center;justify-content:center; transition:background .15s,color .15s; } #unitModal .umClose:hover { background:rgba(255,80,80,.28); color:#fff; } /* 主体两栏:左侧立绘+环绕装备槽,右侧数值 */ -#unitModal .umGrid { display:grid; grid-template-columns: 260px 1fr; gap:18px; } -#unitModal .umLeft { position:relative; border-radius:12px; padding:18px 12px 110px; +#unitModal .umGrid { display:grid; grid-template-columns: 286px 1fr; gap:18px; } +#unitModal .umLeft { position:relative; border-radius:12px; padding:14px 10px 14px; 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%); + background: radial-gradient(ellipse at 50% 42%, rgba(139,92,246,.14), rgba(255,255,255,.03) 62%); border: 1px solid rgba(139,92,246,.28); box-shadow: inset 0 0 28px rgba(139,92,246,.10); } -#unitModal .umPortraitWrap { position:relative; width:220px; height:250px; margin:10px 0 4px; } -#unitModal .umPortrait { width:100%; height:100%; display:flex; align-items:flex-end; justify-content:center; } +/* 环绕装备槽:上=帽子 / 下=鞋子 / 左=护臂·武器·饰品 / 右=护甲·腰带·裤子 */ +#unitModal .umPortraitWrap { position:relative; margin:6px 0 4px; } +#unitModal .umEqRing { display:flex; flex-direction:column; align-items:center; gap:8px; } +#unitModal .umEqRingTop, #unitModal .umEqRingBottom { display:flex; justify-content:center; } +#unitModal .umEqRingMid { display:flex; align-items:center; justify-content:center; gap:10px; } +#unitModal .umEqCol { display:flex; flex-direction:column; gap:18px; } +/* 立绘区:与位图等比(132×200 = 0.66),避免 canvas 被 CSS 非等比拉伸致人像挤扁 */ +#unitModal .umPortrait { width:132px; height:200px; display:flex; align-items:flex-end; justify-content:center; } #unitModal .umPortrait canvas { width:100%; height:100%; display:block; } -#unitModal .umPortraitFallback { font-size:90px; line-height:250px; text-align:center; } -#unitModal .umEq { position:absolute; width:54px; height:54px; border-radius:11px; +#unitModal .umPortraitFallback { font-size:86px; line-height:200px; text-align:center; } +/* 未落入环绕方位的槽位(后端将来新增)兜底网格,保持在下方,避免丢槽 */ +#unitModal .umEquipGrid { display:grid; grid-template-columns:repeat(4, 56px); gap:20px 14px; justify-content:center; margin-top:10px; } +#unitModal .umEq { position:relative; width:56px; height:56px; border-radius:11px; background:rgba(8,14,26,.78); border:1px solid rgba(120,200,255,.3); 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.umEqGhost { border-style:dashed; border-color:rgba(120,200,255,.12); background:rgba(8,14,26,.35); cursor:default; pointer-events:none; } #unitModal .umEq .umEqIcon { font-size:22px; line-height:1; display:flex; align-items:center; justify-content:center; } #unitModal .umEq .umEqIcon img { width:34px; height:34px; object-fit:contain; filter:drop-shadow(0 1px 1px rgba(0,0,0,.5)); } /* 装备名称统一放在框正下方 */ @@ -1117,12 +1127,8 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa margin-top:3px; font-size:10px; color:#a8b8cc; white-space:nowrap; text-shadow:0 1px 2px rgba(0,0,0,.7); pointer-events:none; } -#unitModal .umEq.accessory { top:-12px; left:50%; transform:translateX(-50%); } -#unitModal .umEq.weapon { left:-16px; top:50%; transform:translateY(-50%); } -#unitModal .umEq.armor { right:-16px; top:50%; transform:translateY(-50%); } -#unitModal .umEq.boots { bottom:-28px; left:50%; transform:translateX(-50%); } #unitModal .umEquipPicker { - margin-top:120px; width:100%; + margin-top:14px; width:100%; max-height: 260px; overflow-y: auto; background: rgba(8,14,26,0.96); @@ -2803,8 +2809,8 @@ const BUILD_INTERACT = { ] } }; -// 装备目录:后端 /yt_world/api/micro/equip/catalog 为单一真源; -// 下面常量仅作极端兜底(未登录/网络不可达时显示)。 +// 装备目录:后端 /yt_world/api/equip/catalog 为单一真源(返回 items + slots 槽位真源列表); +// 下面常量仅作极端兜底(未登录/网络不可达时显示),槽位与后端 SLOT_SELECTION 保持一致。 const EQUIP_CATALOG_FALLBACK = { weapon: [ { code: 'iron_dagger', name: '铁匕首', icon: '🗡️', bonus: '攻+3' }, @@ -2820,14 +2826,54 @@ const EQUIP_CATALOG_FALLBACK = { { code: 'lucky_charm', name: '幸运符', icon: '🍀', bonus: '速+3' }, { code: 'power_ring', name: '力量戒指', icon: '💍', bonus: '攻+4 防+2' }, { code: 'amulet_life', name: '生命护符', icon: '📿', bonus: '血+25 能+15' }, + { code: 'silver_bracer', name: '银手镯', icon: '🔗', bonus: '攻+4 防+1' }, + { code: 'dragon_bracelet', name: '龙纹手镯', icon: '🔗', bonus: '攻+8 血+10' }, ], boots: [ { code: 'straw_sandals', name: '草鞋', icon: '🩴', bonus: '速+1' }, { code: 'leather_boots', name: '皮靴', icon: '🥾', bonus: '速+3 防+1' }, { code: 'wind_boots', name: '疾风靴', icon: '👟', bonus: '速+6 能+10' }, ], + hat: [ + { code: 'straw_hat', name: '草帽', icon: '🎩', bonus: '防+1' }, + { code: 'iron_helm', name: '铁盔', icon: '🎩', bonus: '防+3 血+8' }, + { code: 'gold_crown', name: '黄金王冠', icon: '👑', bonus: '防+5 血+20 攻+5' }, + ], + pants: [ + { code: 'cloth_pants', name: '布裤', icon: '👖', bonus: '防+1' }, + { code: 'iron_leggings', name: '铁护腿', icon: '👖', bonus: '防+3 血+10' }, + { code: 'dragon_pants', name: '龙鳞战裤', icon: '👖', bonus: '防+6 血+25' }, + ], + bracelet: [ + { code: 'leather_bracer', name: '皮护腕', icon: '🔗', bonus: '攻+2' }, + ], + belt: [ + { code: 'rope_belt', name: '麻绳腰带', icon: '🪢', bonus: '血+5' }, + { code: 'iron_belt', name: '铁腰封', icon: '🪢', bonus: '血+12 防+2' }, + { code: 'dragon_belt', name: '龙鳞腰带', icon: '🪢', bonus: '血+25 防+4' }, + ], +}; +// 环绕方位映射(纯布局信息;槽位数据本身仍以后端 catalog.slots 为真源): +// 上=帽子 / 下=鞋子 / 左=护臂·武器·饰品 / 右=护甲·腰带·裤子 +const EQUIP_SLOT_POS = { + hat: 'top', + bracelet: 'left1', weapon: 'left2', accessory: 'left3', + armor: 'right1', belt: 'right2', pants: 'right3', + boots: 'bottom', +}; +// 槽位真源:优先取后端 catalog.slots,离线兜底用此映射(与后端 SLOT_SELECTION 对齐)。 +const EQUIP_SLOT_FALLBACK_LABELS = { + weapon: '武器', armor: '护甲', accessory: '饰品', boots: '鞋子', + hat: '帽子', pants: '裤子', bracelet: '护臂', belt: '腰带', }; let EQUIP_CATALOG_SERVER = null; +let EQUIP_SLOTS = null; // [{value, label}] 来自后端 catalog.slots +// 返回当前可用的槽位列表(部件渲染据此动态生成,不写死) +function microEquipSlots() { + if (EQUIP_SLOTS && EQUIP_SLOTS.length) return EQUIP_SLOTS; + const cat = microEquipCatalog(); + return Object.keys(cat).map(v => ({ value: v, label: EQUIP_SLOT_FALLBACK_LABELS[v] || v })); +} // 全部可建造项(道路/地面 + 建筑/设施),作为面板/渲染的单一来源 function microAllBuildItems() { return MICRO_BUILD_ITEMS.concat(MICRO_BUILD_EXTRA); } @@ -2881,12 +2927,14 @@ async function microLoadEquipCatalog() { try { const r = await API.equipCatalog(); if (r && r.ok && Array.isArray(r.items)) { - const bySlot = { weapon: [], armor: [], accessory: [], boots: [] }; + const bySlot = {}; + (r.slots || []).forEach(s => { bySlot[s.value] = []; }); r.items.forEach(it => { if (!bySlot[it.slot]) bySlot[it.slot] = []; bySlot[it.slot].push(it); }); EQUIP_CATALOG_SERVER = bySlot; + if (Array.isArray(r.slots) && r.slots.length) EQUIP_SLOTS = r.slots; microEquipCatalogLoaded = true; } } catch (e) { /* 保留兜底 */ } @@ -2901,7 +2949,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, boots: null }; + if (!microChar.equip) microChar.equip = {}; if (!microChar.inventory) microChar.inventory = {}; ['gold','copper','silver','diamond'].forEach(k => { if (microChar[k] == null) microChar[k] = 0; }); } @@ -6145,15 +6193,25 @@ async function showUnitModal(unitId) { const raceCfg = (typeof window !== 'undefined' && window.RACE_CONFIG && window.RACE_CONFIG[unit.race]) || null; const sprite = raceCfg ? (raceCfg.idle_attack || raceCfg.walk || raceCfg.work_comm) : ''; const portrait = sprite - ? `` + ? `` : `
${raceIcon}
`; - const eqCell = (slot, label) => { - const cur = unit.equip && unit.equip[slot]; - return `
+ // 装备槽:槽位列表来自后端真源(microEquipSlots),环绕方位由 EQUIP_SLOT_POS 决定; + // 未落入方位的槽位(后端将来新增)自动进入底部兜底网格 —— 不写死、不丢槽。 + const slotList = microEquipSlots(); + const bySlot = {}; + slotList.forEach(s => { bySlot[s.value] = s; }); + const eqCell = (value) => { + const s = bySlot[value]; + if (!s) return `
`; // 槽位缺失 → 占位保持环绕对称 + const cur = (unit.equip && unit.equip[value]) || null; + return `
${cur ? eqIconHTML(cur.icon) : '➕'}
-
${cur ? cur.name : label}
+
${cur ? cur.name : s.label}
`; }; + const extraSlots = slotList.filter(s => !EQUIP_SLOT_POS[s.value]); + const extraGridHTML = extraSlots.length + ? `
${extraSlots.map(s => eqCell(s.value)).join('')}
` : ''; const statusColor = unit.status==='working'?'#8bc34a':unit.status==='resting'?'#ff9800':'#4af'; const statusText = unit.status==='working'?'🟢 工作中':unit.status==='resting'?'🟡 休息中':'🔵 空闲中'; document.getElementById('modalBody').innerHTML = ` @@ -6161,12 +6219,17 @@ async function showUnitModal(unitId) {
- ${eqCell('accessory','饰品')} - ${eqCell('weapon','武器')} -
${portrait}
- ${eqCell('armor','护甲')} - ${eqCell('boots','鞋子')} +
+
${eqCell('hat')}
+
+
${eqCell('bracelet')}${eqCell('weapon')}${eqCell('accessory')}
+
${portrait}
+
${eqCell('armor')}${eqCell('belt')}${eqCell('pants')}
+
+
${eqCell('boots')}
+
+ ${extraGridHTML}
@@ -6185,33 +6248,67 @@ async function showUnitModal(unitId) {
`; window._umUnit = unit; - if (sprite) _drawRacePortrait(sprite, document.getElementById('umPortraitCv')); + if (sprite) _drawRacePortrait(sprite, document.getElementById('umPortraitCv'), (raceCfg && raceCfg.fw) || 0); modal.classList.add('active'); } -// 精灵图第一帧 → 面板立绘 canvas(精确取 48:64 帧比例、居中、脚下光晕底座) -function _drawRacePortrait(src, cv) { +// 精灵图第一帧 → 面板立绘 canvas +// 要点:① 位图尺寸跟随 CSS 显示尺寸(×DPR),杜绝 canvas 被非等比拉伸致人像变形; +// ② 按第一帧的"不透明像素包围盒"等比放大,人像不被帧内透明留白浪费,填满立绘区; +// ③ 帧宽优先取种族配置 fw(宽帧表按 48/64 反推会算错)。 +function _drawRacePortrait(src, cv, frameW) { if (!cv) return; + // 位图尺寸 = 实际显示尺寸 × DPR(拿不到布局时退回 CSS 声明值 132×200) + let rect = { width: 0, height: 0 }; + try { rect = cv.getBoundingClientRect(); } catch (e) {} + const dpr = 2; + const cssW = rect.width > 8 ? rect.width : 132; + const cssH = rect.height > 8 ? rect.height : 200; + cv.width = Math.round(cssW * dpr); + cv.height = Math.round(cssH * dpr); + const ctx = cv.getContext('2d'); const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { const W = cv.width, H = cv.height; ctx.clearRect(0, 0, W, H); - // 脚下光晕底座 - const gy = H * 0.88; - const grd = ctx.createRadialGradient(W / 2, gy, 2, W / 2, gy, W * 0.42); + ctx.imageSmoothingEnabled = false; + const fh = img.naturalHeight; + const fw = frameW || Math.max(1, Math.round(fh * 48 / 64)); + // 量第一帧的不透明包围盒 + let sx = 0, sy = 0, sw = fw, sh = fh; + try { + const off = document.createElement('canvas'); + off.width = fw; off.height = fh; + const octx = off.getContext('2d'); + octx.drawImage(img, 0, 0, fw, fh, 0, 0, fw, fh); + const d = octx.getImageData(0, 0, fw, fh).data; + let x0 = fw, y0 = fh, x1 = -1, y1 = -1; + for (let y = 0; y < fh; y++) { + for (let x = 0; x < fw; x++) { + if (d[(y * fw + x) * 4 + 3] > 12) { + if (x < x0) x0 = x; + if (x > x1) x1 = x; + if (y < y0) y0 = y; + if (y > y1) y1 = y; + } + } + } + if (x1 > x0 && y1 > y0) { sx = x0; sy = y0; sw = x1 - x0 + 1; sh = y1 - y0 + 1; } + } catch (e) { /* 取像素受限(跨域)→ 退回整帧比例 */ } + // 等比缩放:高度优先(立绘区偏瘦高),宽度不足时自动以宽度为准 + const scale = Math.min((W * 0.94) / sw, (H * 0.80) / sh); + const dw = sw * scale, dh = sh * scale; + const gy = H * 0.90; // 脚底基线 + // 脚下光晕底座(随人像宽度自适应) + const gx = Math.max(W * 0.40, dw * 0.62); + const grd = ctx.createRadialGradient(W / 2, gy, 2, W / 2, gy, gx); grd.addColorStop(0, 'rgba(139,92,246,.5)'); grd.addColorStop(1, 'rgba(139,92,246,0)'); ctx.fillStyle = grd; - ctx.beginPath(); ctx.ellipse(W / 2, gy, W * 0.42, H * 0.06, 0, 0, Math.PI * 2); ctx.fill(); - // 统一帧规格 48×64(characters.js CHAR_FW/CHAR_FH),按原图高反推帧宽,只画第一帧 - const fh = img.naturalHeight; - const fw = Math.max(1, Math.round(fh * 48 / 64)); - const scale = Math.min((W * 0.86) / fw, (H * 0.84) / fh); - const dw = fw * scale, dh = fh * scale; - ctx.imageSmoothingEnabled = false; - ctx.drawImage(img, 0, 0, fw, fh, (W - dw) / 2, gy - dh, dw, dh); + ctx.beginPath(); ctx.ellipse(W / 2, gy, gx, H * 0.055, 0, 0, Math.PI * 2); ctx.fill(); + ctx.drawImage(img, sx, sy, sw, sh, (W - dw) / 2, gy - dh, dw, dh); }; img.src = src; } @@ -6232,7 +6329,9 @@ 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: '饰品', boots: '鞋子' }; + const labelMap = {}; + microEquipSlots().forEach(s => { labelMap[s.value] = s.label; }); + const map = labelMap; const cur = unit.equip && unit.equip[slot]; const cid = unit.cid || unit.id; let html = `
${map[slot]} · 点击穿戴×
`; @@ -6296,7 +6395,7 @@ async function updateChar() { }); if (r && r.ok && r.char) { microChar = r.char; - if (!microChar.equip) microChar.equip = { weapon: null, armor: null, accessory: null, boots: null }; + if (!microChar.equip) microChar.equip = {}; if (!microChar.inventory) microChar.inventory = {}; ['gold','copper','silver','diamond'].forEach(k => { if (microChar[k] == null) microChar[k] = 0; }); }