v0.1.0 — M1: Project Setup + Tilemap + Combat + Backend
Frontend: - HTML5 Canvas isometric tilemap renderer - Eichenhafen starting city (procedural) - Click-to-move A* pathfinding - Player stats, HP/SP/EXP bars, skill bar, minimap - 3 NPCs (merchant, class master, innkeeper) - 6 monster spawns with aggro AI - Combat: auto-attack, damage numbers, EXP/gold - Player death + respawn Backend: - FastAPI server with SQLite - Ollama LLM bridge for events + dialogue - Event validation (no soft-lock) - NPC dialogue generation Design: - Full DESIGN.md with architecture, formulas, milestones - 6 base classes + 6 advanced classes - 3 fixed cities, dynamic events
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
// player.js — Player character with stats
|
||||
|
||||
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/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/2 - 8 + bobOffset, 4, 8);
|
||||
ctx.fillRect(drawX + 1, drawY + TILE_HEIGHT/2 - 8 + bobOffset, 4, 8);
|
||||
|
||||
// Body
|
||||
ctx.fillStyle = '#5566aa';
|
||||
ctx.fillRect(drawX - 7, drawY + TILE_HEIGHT/2 - 18 + bobOffset, 14, 12);
|
||||
|
||||
// Head
|
||||
ctx.fillStyle = '#e0b890';
|
||||
ctx.beginPath();
|
||||
ctx.arc(drawX, drawY + TILE_HEIGHT/2 - 22 + bobOffset, 7, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Hair (black)
|
||||
ctx.fillStyle = '#1a1a1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(drawX, drawY + TILE_HEIGHT/2 - 24 + bobOffset, 7, Math.PI, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillRect(drawX - 7, drawY + TILE_HEIGHT/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/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/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);
|
||||
}
|
||||
}
|
||||
|
||||
// TILE_WIDTH constant for player rendering
|
||||
const TILE_HEIGHT = 32;
|
||||
Reference in New Issue
Block a user