Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions server/src/engine/world-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ function sanitize(player) {
lastActionAt: player.lastActionAt || null,
lastHeartbeatAt: player.lastHeartbeatAt || null,
presenceState: getPresenceState(player),
level: player.level ?? 1,
hp: player.hp ?? 100,
max_hp: player.max_hp ?? 100,
};
}

Expand Down Expand Up @@ -431,6 +434,10 @@ function join(playerId, name, sprite, options = {}) {
lastHeartbeatAt: now,
lastActionAt: options.trackActivity === false ? null : now,
lastChatCursor: nextChatCursor,
// RPG 基础属性(等待 Issue #12 接入完整数据模型前使用默认值)
level: 1,
hp: 100,
max_hp: 100,
};
addActivity(playerId, { type: 'join', text: `加入了小镇 (角色: ${assignedSprite})` });
emitPerception('join', playerId, name, spawnX, spawnY, { sprite: assignedSprite });
Expand Down
32 changes: 32 additions & 0 deletions server/web/js/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,9 @@
clientPlayers[id].currentZoneName = sp.currentZoneName;
clientPlayers[id].lastActionAt = sp.lastActionAt;
clientPlayers[id].lastHeartbeatAt = sp.lastHeartbeatAt;
clientPlayers[id].level = sp.level;
clientPlayers[id].hp = sp.hp;
clientPlayers[id].max_hp = sp.max_hp;
if (sp.interactionSound && !clientPlayers[id]._lastSound) {
clientPlayers[id]._lastSound = sp.interactionSound;
if (sfxEnabled && sfx[sp.interactionSound]) sfx[sp.interactionSound].cloneNode().play().catch(() => {});
Expand Down Expand Up @@ -811,6 +814,35 @@
ctx.lineWidth=2.5; ctx.lineJoin='round'; ctx.strokeStyle=idle?'rgba(26,26,46,0.55)':'rgba(26,26,46,0.9)'; ctx.strokeText(p.name,cx2,nameY);
ctx.fillStyle=p.name==='Observer'?'#f1c40f':(idle?'rgba(255,255,255,0.72)':'#ffffff'); ctx.fillText(p.name,cx2,nameY);

// 等级标签:显示在名字右侧
if (p.level != null) {
const nameHalfW = ctx.measureText(p.name).width / 2;
ctx.font = 'bold 9px "Pixelify Sans","Comic Sans MS",sans-serif';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
const lvText = `Lv.${p.level}`;
ctx.lineWidth = 1.5; ctx.strokeStyle = idle ? 'rgba(26,26,46,0.55)' : 'rgba(26,26,46,0.9)';
ctx.strokeText(lvText, cx2 + nameHalfW + 2, nameY);
ctx.fillStyle = '#f9ca24';
ctx.fillText(lvText, cx2 + nameHalfW + 2, nameY);
ctx.textAlign = 'center';
}

// HP 血条:显示在角色贴图正下方
if (p.hp != null && p.max_hp) {
const barW = Math.round(TILE_SIZE * 1.2);
const barH = 3;
const barX = sx;
const barY = Math.round(sy - 10 + TILE_SIZE * 1.2) + 2;
const ratio = Math.max(0, Math.min(1, p.hp / p.max_hp));
const hpColor = ratio > 0.5 ? '#00b894' : ratio > 0.25 ? '#fdcb6e' : '#d63031';
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.beginPath(); ctx.roundRect(barX, barY, barW, barH, 1.5); ctx.fill();
if (ratio > 0) {
ctx.fillStyle = hpColor;
ctx.beginPath(); ctx.roundRect(barX, barY, Math.max(1, Math.round(barW * ratio)), barH, 1.5); ctx.fill();
}
}

const bubbleY=sy-27+floatY;
ctx.textAlign='center'; ctx.textBaseline='middle';
if(p.isThinking){
Expand Down
Loading