// player.js — Player character with stats var TILE_HEIGHT_PLAYER = 32; class Player { constructor(x, y) { this.x = x; this.y = y; this.tileX = 0; this.tileY = 0; this.speed = 2; // Stats this.name = 'Wanderer'; this.class = 'wanderer'; this.level = 1; this.exp = 0; this.expNeeded = 100; this.maxHp = 100; this.hp = 100; this.maxSp = 50; this.sp = 50; this.str = 10; this.agi = 10; this.int = 10; this.vit = 10; this.dex = 10; this.luk = 5; this.atk = 15; this.def = 5; this.gold = 100; // Movement this.path = []; this.targetX = null; this.targetY = null; this.moving = false; // Combat this.attackTarget = null; this.attackRange = 1.5; this.attackCooldown = 0; this.attackSpeed = 30; // frames between attacks // Visual this.facing = 'down'; this.animFrame = 0; this.animTimer = 0; } getScreenPos(map) { const screen = map.tileToScreen(Math.floor(this.x), Math.floor(this.y)); return screen; } update(map) { // Update tile position this.tileX = Math.floor(this.x); this.tileY = Math.floor(this.y); // Movement along path if (this.path.length > 0) { const next = this.path[0]; const dx = next.x - this.x; const dy = next.y - this.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.1) { this.path.shift(); if (this.path.length === 0) { this.moving = false; } } else { this.x += (dx / dist) * this.speed * 0.1; this.y += (dy / dist) * this.speed * 0.1; if (Math.abs(dx) > Math.abs(dy)) { this.facing = dx > 0 ? 'right' : 'left'; } else { this.facing = dy > 0 ? 'down' : 'up'; } } } // Animation if (this.moving) { this.animTimer++; if (this.animTimer > 10) { this.animTimer = 0; this.animFrame = (this.animFrame + 1) % 4; } } // Combat if (this.attackCooldown > 0) this.attackCooldown--; } setPath(path) { this.path = path; this.moving = path.length > 0; } takeDamage(amount) { this.hp -= amount; if (this.hp < 0) this.hp = 0; return this.hp <= 0; } heal(amount) { this.hp = Math.min(this.maxHp, this.hp + amount); } gainExp(amount) { this.exp += amount; if (this.exp >= this.expNeeded) { this.levelUp(); return true; } return false; } levelUp() { this.exp -= this.expNeeded; this.level++; this.expNeeded = Math.floor(100 * Math.pow(this.level, 1.5)); this.maxHp += 20; this.maxSp += 10; this.hp = this.maxHp; this.sp = this.maxSp; this.str += 2; this.agi += 2; this.int += 1; this.vit += 2; this.dex += 1; this.atk += 3; this.def += 1; } render(ctx, map, camera) { const screen = map.tileToScreen(this.x, this.y); const drawX = screen.x - camera.x + 480; const drawY = screen.y - camera.y + 320; // Shadow ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(drawX, drawY + TILE_HEIGHT_PLAYER/2 + 4, 12, 4, 0, 0, Math.PI * 2); ctx.fill(); // Body (simple character) const bobOffset = this.moving ? Math.sin(this.animFrame * Math.PI / 2) * 2 : 0; // Legs ctx.fillStyle = '#4a4a6a'; ctx.fillRect(drawX - 5, drawY + TILE_HEIGHT_PLAYER/2 - 8 + bobOffset, 4, 8); ctx.fillRect(drawX + 1, drawY + TILE_HEIGHT_PLAYER/2 - 8 + bobOffset, 4, 8); // Body ctx.fillStyle = '#5566aa'; ctx.fillRect(drawX - 7, drawY + TILE_HEIGHT_PLAYER/2 - 18 + bobOffset, 14, 12); // Head ctx.fillStyle = '#e0b890'; ctx.beginPath(); ctx.arc(drawX, drawY + TILE_HEIGHT_PLAYER/2 - 22 + bobOffset, 7, 0, Math.PI * 2); ctx.fill(); // Hair (black) ctx.fillStyle = '#1a1a1a'; ctx.beginPath(); ctx.arc(drawX, drawY + TILE_HEIGHT_PLAYER/2 - 24 + bobOffset, 7, Math.PI, Math.PI * 2); ctx.fill(); ctx.fillRect(drawX - 7, drawY + TILE_HEIGHT_PLAYER/2 - 22 + bobOffset, 14, 3); // Name label ctx.fillStyle = '#ffcc44'; ctx.font = '11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(this.name, drawX, drawY + TILE_HEIGHT_PLAYER/2 - 32); // HP bar above character const hpPercent = this.hp / this.maxHp; const barWidth = 24; const barX = drawX - barWidth / 2; const barY = drawY + TILE_HEIGHT_PLAYER/2 - 38; ctx.fillStyle = '#333'; ctx.fillRect(barX, barY, barWidth, 3); ctx.fillStyle = hpPercent > 0.5 ? '#44dd44' : hpPercent > 0.25 ? '#ddaa44' : '#dd4444'; ctx.fillRect(barX, barY, barWidth * hpPercent, 3); } }