feat: revise monster scaling

This commit is contained in:
2026-05-29 17:38:51 +02:00
parent b3b0d41f0b
commit 085678955d
2 changed files with 161 additions and 119 deletions
+119 -90
View File
@@ -560,16 +560,28 @@
setTimeout(() => el.classList.remove('flash'), 600);
}
function updateUI() {
if (!currentPlayer) return;
function updateUI() {
if (!currentPlayer) return;
document.getElementById('statLevel').textContent = currentPlayer.level;
document.getElementById('statExp').textContent = currentPlayer.exp;
const levelProgress = ((currentPlayer.exp || 0) % 100);
document.getElementById('expBar').style.width = `${Math.max(0, Math.min(100, levelProgress))}%`;
document.getElementById('statHealth').textContent = `${currentPlayer.health}/${currentPlayer.max_health}`;
document.getElementById('healthBar').style.width = `${(currentPlayer.health / currentPlayer.max_health) * 100}%`;
document.getElementById('statActionPoints').textContent = currentPlayer.action_points;
document.getElementById('statLevel').textContent = currentPlayer.level;
document.getElementById('statExp').textContent = currentPlayer.exp;
// Calculate XP progress for new level: 30 for level 1, +20 per level after
// XP needed to reach this level: 30 + 20*(level-2) for level >= 2, or 0 for level 1
const currentLevel = currentPlayer.level;
let xpForCurrentLevel = 0;
let xpForNextLevel = 30;
if (currentLevel > 1) {
xpForCurrentLevel = 30 + 20 * (currentLevel - 2);
xpForNextLevel = 30 + 20 * (currentLevel - 1);
}
const xpIntoLevel = currentPlayer.exp - xpForCurrentLevel;
const xpNeededForLevel = xpForNextLevel - xpForCurrentLevel;
const levelProgress = Math.max(0, Math.min(100, (xpIntoLevel / xpNeededForLevel) * 100));
document.getElementById('expBar').style.width = `${levelProgress}%`;
document.getElementById('statHealth').textContent = `${currentPlayer.health}/${currentPlayer.max_health}`;
document.getElementById('healthBar').style.width = `${(currentPlayer.health / currentPlayer.max_health) * 100}%`;
document.getElementById('statActionPoints').textContent = currentPlayer.action_points;
document.getElementById('actionBar').style.width = `${(currentPlayer.action_points / currentPlayer.max_action_points) * 100}%`;
document.getElementById('statAttack').textContent = currentPlayer.effective_attack ?? currentPlayer.attack;
document.getElementById('statRange').textContent = (currentPlayer.attack_range ?? 4).toFixed(1);
@@ -605,40 +617,40 @@
if (newWood !== prevWood) flashEl('invWood');
if (newStone !== prevStone) flashEl('invStone');
// Weapon slots
const slots = currentPlayer.weapon_slots || [];
const equippedIdx = currentPlayer.equipped_weapon_slot ?? -1;
const slotEl = document.getElementById('weaponSlots');
if (slotEl) {
const filled = slots.filter(Boolean);
if (filled.length === 0) {
slotEl.innerHTML = '<span style="color:#555">No weapons</span>';
} else {
slotEl.innerHTML = slots.map((w, i) => {
if (!w) return '';
const eq = i === equippedIdx;
const border = eq ? 'border:1px solid #f1c40f' : 'border:1px solid #444';
const bg = eq ? 'background:#2a2600' : 'background:#1e1e1e';
const icon = eq ? '⚔️ ' : '· ';
const aura = w.aura_bonuses || {};
const auraParts = [];
if (aura.action_regen) auraParts.push(`+${aura.action_regen} AP regen`);
if (aura.hp_regen) auraParts.push(`+${aura.hp_regen} HP regen`);
if (aura.defense) auraParts.push(`+${aura.defense} def`);
if (aura.max_health) auraParts.push(`+${aura.max_health} max HP`);
if (aura.free_move_chance) auraParts.push(`${Math.round(aura.free_move_chance * 100)}% free moves`);
const auraLine = auraParts.length > 0
? `<div style="color:#7fd6ff; margin-top:2px;">Aura (${w.aura_radius || 0}): ${auraParts.join(', ')}</div>`
: '';
return `<div data-slot="${i}" style="padding:3px 6px;margin:2px 0;border-radius:4px;${border};${bg};cursor:pointer;"
title="Click to equip">
${icon}<b>${w.name}</b>
<span style="color:#aaa"> +${w.attack_bonus}atk +${w.range_bonus}rng</span>
${auraLine}
</div>`;
}).join('');
}
}
// Weapon slots
const slots = currentPlayer.weapon_slots || [];
const equippedIdx = currentPlayer.equipped_weapon_slot ?? -1;
const slotEl = document.getElementById('weaponSlots');
if (slotEl) {
const filled = slots.filter(Boolean);
if (filled.length === 0) {
slotEl.innerHTML = '<span style="color:#555">No weapons</span>';
} else {
slotEl.innerHTML = slots.map((w, i) => {
if (!w) return '';
const eq = i === equippedIdx;
const border = eq ? 'border:1px solid #f1c40f' : 'border:1px solid #444';
const bg = eq ? 'background:#2a2600' : 'background:#1e1e1e';
const icon = eq ? '⚔️ ' : '· ';
const aura = w.aura_bonuses || {};
const auraParts = [];
if (aura.action_regen) auraParts.push(`+${aura.action_regen} AP regen`);
if (aura.hp_regen) auraParts.push(`+${aura.hp_regen} HP regen`);
if (aura.defense) auraParts.push(`+${aura.defense} def`);
if (aura.max_health) auraParts.push(`+${aura.max_health} max HP`);
if (aura.free_move_chance) auraParts.push(`${Math.round(aura.free_move_chance * 100)}% free moves`);
const auraLine = auraParts.length > 0
? `<div style="color:#7fd6ff; margin-top:2px; pointer-events:none;">Aura (${w.aura_radius || 0}): ${auraParts.join(', ')}</div>`
: '';
return `<div data-slot="${i}" style="padding:3px 6px;margin:2px 0;border-radius:4px;${border};${bg};cursor:pointer;"
title="Click to equip" class="weapon-slot">
${icon}<b>${w.name}</b>
<span style="color:#aaa"> +${w.attack_bonus}atk +${w.range_bonus}rng</span>
${auraLine}
</div>`;
}).join('');
}
}
if (gameState) {
const activePlayerCount = Object.values(gameState.players).filter(p => p.active).length;
@@ -808,22 +820,26 @@
controls.update();
}
function createPlayerSphere(player) {
const geometry = new THREE.SphereGeometry(0.5, 16, 16);
const isCurrentPlayer = player.id === currentPlayerId;
const material = new THREE.MeshPhongMaterial({
color: player.color,
emissive: isCurrentPlayer ? 0x222222 : 0x000000,
shininess: isCurrentPlayer ? 80 : 30,
});
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set(player.position.x, 0.5, player.position.y);
if (isCurrentPlayer) {
sphere.scale.set(1.15, 1.15, 1.15);
}
sphere.castShadow = true;
sphere.receiveShadow = true;
scene.add(sphere);
function createPlayerSphere(player) {
const geometry = new THREE.SphereGeometry(0.5, 16, 16);
const isCurrentPlayer = player.id === currentPlayerId;
const material = new THREE.MeshPhongMaterial({
color: player.color,
emissive: isCurrentPlayer ? 0x222222 : 0x000000,
shininess: isCurrentPlayer ? 80 : 30,
});
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set(player.position.x, 0.5, player.position.y);
// Scale sphere based on level: 1.0 at level 1, increases with level
const levelScale = 1.0 + (player.level - 1) * 0.08;
sphere.scale.set(levelScale, levelScale, levelScale);
if (isCurrentPlayer) {
// Current player slightly larger
sphere.scale.multiplyScalar(1.15);
}
sphere.castShadow = true;
sphere.receiveShadow = true;
scene.add(sphere);
// Compact nameplate — canvas is exactly as tall as the pill so no
// transparent headroom inflates the visible sprite quad.
@@ -880,16 +896,20 @@
}
}
function updatePlayerSphere(player) {
if (playerSpheres[player.id]) {
playerSpheres[player.id].mesh.position.set(player.position.x, 0.5, player.position.y);
playerSpheres[player.id].sprite.position.set(player.position.x, 2.5, player.position.y);
if (playerSpheres[player.id].northArrow) {
playerSpheres[player.id].northArrow.position.set(player.position.x, 0.5, player.position.y - 1.1);
}
setPlayerVisualState(player);
}
}
function updatePlayerSphere(player) {
if (playerSpheres[player.id]) {
playerSpheres[player.id].mesh.position.set(player.position.x, 0.5, player.position.y);
// Update scale based on level
const levelScale = 1.0 + (player.level - 1) * 0.08;
const baseScale = player.id === currentPlayerId ? levelScale * 1.15 : levelScale;
playerSpheres[player.id].mesh.scale.set(baseScale, baseScale, baseScale);
playerSpheres[player.id].sprite.position.set(player.position.x, 2.5, player.position.y);
if (playerSpheres[player.id].northArrow) {
playerSpheres[player.id].northArrow.position.set(player.position.x, 0.5, player.position.y - 1.1);
}
setPlayerVisualState(player);
}
}
function createTextSprite(initialText, textColor = '#ffffff', bgColor = 'rgba(0, 0, 0, 0.7)') {
const canvas = document.createElement('canvas');
@@ -966,13 +986,19 @@
texture.needsUpdate = true;
}
function createMonsterCube(monster) {
const geometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);
const material = new THREE.MeshPhongMaterial({ color: 0xff4444 });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(monster.position.x, 0.3, monster.position.y);
cube.castShadow = true;
scene.add(cube);
function createMonsterCube(monster) {
const geometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);
// Color by level: green (1-3), yellow (4-6), orange (7-9), red (10+)
let monsterColor = 0xff4444; // red default
if (monster.level <= 3) monsterColor = 0x44ff44; // green
else if (monster.level <= 6) monsterColor = 0xffff44; // yellow
else if (monster.level <= 9) monsterColor = 0xff8844; // orange
else monsterColor = 0xff4444; // red for 10+
const material = new THREE.MeshPhongMaterial({ color: monsterColor });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(monster.position.x, 0.3, monster.position.y);
cube.castShadow = true;
scene.add(cube);
const label = createTextSprite(`Lv ${monster.level} HP ${monster.health}/${monster.max_health}`, '#ffdede');
label.sprite.position.set(monster.position.x, 1.25, monster.position.y);
@@ -1554,20 +1580,23 @@
};
// Click a weapon in the inventory panel to equip it (no AP cost).
document.getElementById('weaponSlots').addEventListener('click', async (e) => {
const targetEl = e.target instanceof Element ? e.target : e.target?.parentElement;
if (!targetEl) return;
const div = targetEl.closest('[data-slot]');
if (!div) return;
const slotIndex = parseInt(div.dataset.slot);
if (Number.isNaN(slotIndex)) return;
const equipData = await performAction('equip', null, null, null, null, slotIndex);
if (equipData && equipData.success) {
currentPlayer.equipped_weapon_slot = equipData.equipped_weapon_slot;
showMessage(`Equipped: ${currentPlayer.weapon_slots[slotIndex]?.name}`);
updateUI();
}
});
document.addEventListener('click', async (e) => {
const weaponSlot = e.target.closest('.weapon-slot');
if (!weaponSlot) return;
const slotIndex = parseInt(weaponSlot.getAttribute('data-slot'), 10);
if (!Number.isFinite(slotIndex) || slotIndex < 0) return;
if (!currentPlayer.weapon_slots || !currentPlayer.weapon_slots[slotIndex]) return;
const weaponName = currentPlayer.weapon_slots[slotIndex]?.name || 'weapon';
const equipData = await performAction('equip', null, null, null, null, slotIndex);
if (equipData && equipData.success) {
currentPlayer.equipped_weapon_slot = equipData.equipped_weapon_slot;
showMessage(`Equipped: ${weaponName}`);
updateUI();
}
}, true); // Use capture phase for maximum reliability
}
// ==================== LOGIN FORM ====================