From 03ddbf08b214d50fb8328f124499a12b8d88285b Mon Sep 17 00:00:00 2001 From: Erzangel Date: Fri, 29 May 2026 18:06:20 +0200 Subject: [PATCH] feat: more items, keyboard controls --- backend.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++------- index.html | 55 +++++++++++++++++++++++++++++++++---------- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/backend.py b/backend.py index 2f5d2bd..e3112c2 100644 --- a/backend.py +++ b/backend.py @@ -34,6 +34,10 @@ ATTACK_RANGE_BASE = 4 WEAPON_SLOT_COUNT = 5 BLOCK_BUILD_COST_WOOD = 2 BLOCK_HEALTH = 5 +PLAYER_BASE_ATTACK = 5 +PLAYER_BASE_DEFENSE = 2 +PLAYER_BASE_MAX_HEALTH = 100 +PLAYER_BASE_MOVE_CAPACITY = 1 # Data Models @dataclass @@ -248,6 +252,12 @@ class GameWorld: "aura_radius": 5, "aura_bonuses": {"free_move_chance": 0.7}, }, + { + "name": "Traveler Ring", + "attack_bonus": 0, + "range_bonus": 0, + "movement_bonus": 1, + }, ] self.chests[chest_id] = Chest( id=chest_id, @@ -274,8 +284,13 @@ class GameWorld: continue player.level = 1 player.exp = 0 + player.attack = PLAYER_BASE_ATTACK + player.defense = PLAYER_BASE_DEFENSE + player.max_health = PLAYER_BASE_MAX_HEALTH + player.max_action_points = ACTION_POINTS_MAX + player.movement_capacity = PLAYER_BASE_MOVE_CAPACITY player.health = player.max_health - player.action_points = ACTION_POINTS_MAX + player.action_points = player.max_action_points player.inventory = {"wood": 0, "stone": 0} player.weapon_slots = [None] * WEAPON_SLOT_COUNT player.equipped_weapon_slot = -1 @@ -402,6 +417,43 @@ def _player_effective_range(player: Player) -> float: return player.attack_range + float(w.get("range_bonus", 0)) return player.attack_range +def _player_effective_movement(player: Player) -> int: + if 0 <= player.equipped_weapon_slot < len(player.weapon_slots): + w = player.weapon_slots[player.equipped_weapon_slot] + if w: + return max(1, player.movement_capacity + int(w.get("movement_bonus", 0))) + return player.movement_capacity + +def _recompute_player_stats_for_level(player: Player) -> None: + # Slow progression so levels matter without runaway scaling. + level = max(1, int(player.level)) + old_max_health = player.max_health + old_max_ap = player.max_action_points + + player.attack = PLAYER_BASE_ATTACK + (level - 1) // 3 + player.defense = PLAYER_BASE_DEFENSE + (level - 1) // 4 + player.max_health = PLAYER_BASE_MAX_HEALTH + ((level - 1) // 2) * 5 + player.max_action_points = ACTION_POINTS_MAX + (level - 1) // 4 + + if player.max_health > old_max_health: + player.health = min(player.max_health, player.health + (player.max_health - old_max_health)) + else: + player.health = min(player.max_health, player.health) + + if player.max_action_points > old_max_ap: + player.action_points = min(player.max_action_points, player.action_points + (player.max_action_points - old_max_ap)) + else: + player.action_points = min(player.max_action_points, player.action_points) + +def _recompute_player_level_from_exp(player: Player) -> None: + level = 1 + xp_needed = 30 + while player.exp >= xp_needed and level < 100: + level += 1 + xp_needed += 20 + player.level = level + _recompute_player_stats_for_level(player) + def _player_nearby_bonuses(player: Player) -> Dict: if "game_world" not in globals(): return {} @@ -442,6 +494,7 @@ def _serialize_player(player: Player) -> Dict: bonuses = _player_nearby_bonuses(player) effective_defense = _player_effective_defense(player) effective_max_health = _player_effective_max_health(player) + effective_movement = _player_effective_movement(player) if player.health > effective_max_health: player.health = effective_max_health @@ -451,6 +504,8 @@ def _serialize_player(player: Player) -> Dict: data["base_max_health"] = player.max_health data["max_health"] = effective_max_health data["health"] = player.health + data["base_movement_capacity"] = player.movement_capacity + data["movement_capacity"] = effective_movement data["nearby_bonuses"] = bonuses return data @@ -589,7 +644,8 @@ async def move_player(player_id: str, request: Request): # Calculate distance distance = (dx ** 2 + dy ** 2) ** 0.5 - if distance > player.movement_capacity: + effective_movement = _player_effective_movement(player) + if distance > effective_movement: raise HTTPException(status_code=400, detail="Movement exceeds capacity") next_x = max(-GRID_SIZE // 2, min(GRID_SIZE // 2, player.position.x + dx)) @@ -705,13 +761,7 @@ def _handle_attack(player: Player, target_id: str) -> Dict: if monster.health <= 0: del game_world.monsters[target_id] player.exp += monster.level * 10 - # Calculate level based on XP: 30 for level 1, +20 per additional level - # Level N requires: 30 + 20*(N-1) total XP - player.level = 1 - xp_needed = 30 - while player.exp >= xp_needed and player.level < 100: - player.level += 1 - xp_needed += 20 + _recompute_player_level_from_exp(player) return {"success": True, "damage": damage, "exp": monster.level * 10, "monster_killed": True} retaliation = apply_retaliation(monster.attack) @@ -744,6 +794,7 @@ def _handle_attack(player: Player, target_id: str) -> Dict: if p.active: p.level += 5 p.exp += 500 + _recompute_player_stats_for_level(p) game_world.boss = None return {"success": True, "damage": damage, "boss_killed": True} diff --git a/index.html b/index.html index 9eb47ef..0b17f24 100644 --- a/index.html +++ b/index.html @@ -498,7 +498,7 @@
Controls
-
Each action costs 1 AP. Hover buttons to see details.
+
Keys: Z/Q/S/D move, A attack, E gather, R loot, 1/2/3/4 build. Arrows keep camera orbit.
@@ -639,13 +639,14 @@ 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`); + if (w.movement_bonus) auraParts.push(`+${w.movement_bonus} move range`); const auraLine = auraParts.length > 0 ? `
Aura (${w.aura_radius || 0}): ${auraParts.join(', ')}
` : ''; return `
${icon}${w.name} - +${w.attack_bonus}atk +${w.range_bonus}rng + +${w.attack_bonus}atk +${w.range_bonus}rng${w.movement_bonus ? ` +${w.movement_bonus}mov` : ''} ${auraLine}
`; }).join(''); @@ -1521,21 +1522,49 @@ const key = event.key.toLowerCase(); const rotationStep = 0.12; - const panStep = 1.0; + const hotkeys = ["arrowleft", "arrowright", "arrowup", "arrowdown", "z", "q", "s", "d", "a", "e", "r", "1", "2", "3", "4"]; - if (["arrowleft", "arrowright", "arrowup", "arrowdown", "z", "q", "s", "d"].includes(key)) { + if (hotkeys.includes(key)) { event.preventDefault(); - cameraAutoFollow = false; } - if (key === 'arrowleft') rotateCameraBy(rotationStep, 0, 0); - else if (key === 'arrowright') rotateCameraBy(-rotationStep, 0, 0); - else if (key === 'arrowup') rotateCameraBy(0, -rotationStep * 0.45, 0); - else if (key === 'arrowdown') rotateCameraBy(0, rotationStep * 0.45, 0); - else if (key === 'z') panCameraBy(0, panStep); - else if (key === 's') panCameraBy(0, -panStep); - else if (key === 'q') panCameraBy(-panStep, 0); - else if (key === 'd') panCameraBy(panStep, 0); + // Camera controls stay on arrows only. + if (key === 'arrowleft') { + cameraAutoFollow = false; + rotateCameraBy(rotationStep, 0, 0); + } else if (key === 'arrowright') { + cameraAutoFollow = false; + rotateCameraBy(-rotationStep, 0, 0); + } else if (key === 'arrowup') { + cameraAutoFollow = false; + rotateCameraBy(0, -rotationStep * 0.45, 0); + } else if (key === 'arrowdown') { + cameraAutoFollow = false; + rotateCameraBy(0, rotationStep * 0.45, 0); + // Movement + actions on nearby keys. + } else if (key === 'z') { + document.getElementById('moveUpBtn').click(); + } else if (key === 's') { + document.getElementById('moveDownBtn').click(); + } else if (key === 'q') { + document.getElementById('moveLeftBtn').click(); + } else if (key === 'd') { + document.getElementById('moveRightBtn').click(); + } else if (key === 'a') { + document.getElementById('attackBtn').click(); + } else if (key === 'e') { + document.getElementById('gatherBtn').click(); + } else if (key === 'r') { + document.getElementById('lootBtn').click(); + } else if (key === '1') { + document.getElementById('buildHouseBtn').click(); + } else if (key === '2') { + document.getElementById('buildFarmBtn').click(); + } else if (key === '3') { + document.getElementById('buildTowerBtn').click(); + } else if (key === '4') { + document.getElementById('buildBlockBtn').click(); + } }); }