装备系统

This commit is contained in:
李鹏宇
2026-09-09 15:04:24 +08:00
parent 0ad531a09b
commit 9b50cd496a
12 changed files with 313 additions and 79 deletions
+117 -29
View File
@@ -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,18 @@ function _drawRacePortrait(src, cv) {
img.src = src;
}
// 装备图标渲染:图片路径→<img>,否则 emoji
function eqIconHTML(ic){
if(!ic) return '➕';
if (typeof ic === 'string' && window.EQUIP_ICONS_B64 && EQUIP_ICONS_B64[ic]) {
return `<img src="${EQUIP_ICONS_B64[ic]}" alt="">`;
}
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 +5813,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 +5877,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; });
}
@@ -8101,6 +8188,7 @@ if (typeof fitWorld === 'function') fitWorld();
</script>
<script src="assets/game-api.js?v=20260825"></script>
<script src="assets/common.js?v=20260825"></script>
<script src="assets/icons/equips/icons_b64.js"></script>
<!-- ===== 我的天赋面板(顶栏 🌟 按钮触发,展示玩家已拥有的天赋) ===== -->
<div id="talentPanel" hidden>