From 2d4002bd3f74056991e068d30dd01b82ed51896e Mon Sep 17 00:00:00 2001 From: Marek Le Date: Sat, 9 May 2026 23:37:26 +0200 Subject: [PATCH] refactor --- CLAUDE.md | 152 ++--- autoloads/boss_stats.gd | 82 --- autoloads/boss_stats.gd.uid | 1 - autoloads/enemy_stats.gd | 82 --- autoloads/enemy_stats.gd.uid | 1 - autoloads/event_bus.gd | 113 ++-- autoloads/event_bus.gd.uid | 2 +- autoloads/game_state.gd | 45 +- autoloads/game_state.gd.uid | 2 +- autoloads/net.gd | 148 +++++ autoloads/net.gd.uid | 1 + autoloads/player_stats.gd | 195 ------- autoloads/player_stats.gd.uid | 1 - autoloads/portal_stats.gd | 45 -- autoloads/portal_stats.gd.uid | 1 - autoloads/save_load.gd | 46 ++ autoloads/save_load.gd.uid | 1 + autoloads/stats.gd | 124 ++++ autoloads/stats.gd.uid | 1 + autoloads/stats/base_stats.gd | 8 - autoloads/stats/base_stats.gd.uid | 1 - autoloads/tavern_stats.gd | 38 -- autoloads/tavern_stats.gd.uid | 1 - doc/core/data.md | 4 - doc/core/detail.md | 0 doc/data.md | 16 - doc/detail.md | 25 - doc/scenes/player/features.md | 12 - doc/scenes/player/specs.md | 11 - export_presets.cfg | 46 ++ plan.md | 58 +- project.godot | 92 ++- resources/abilities/ability.gd | 20 + resources/abilities/ability.gd.uid | 1 + .../abilities}/ability_set.gd | 8 +- resources/abilities/ability_set.gd.uid | 1 + resources/buildings/building.gd | 13 + resources/buildings/building.gd.uid | 1 + resources/effects/effect.gd | 15 + resources/effects/effect.gd.uid | 1 + resources/effects/element.gd | 19 + resources/effects/element.gd.uid | 1 + resources/items/item.gd | 12 + resources/items/item.gd.uid | 1 + resources/items/recipe.gd | 9 + resources/items/recipe.gd.uid | 1 + resources/npcs/npc_profile.gd | 10 + resources/npcs/npc_profile.gd.uid | 1 + resources/stats/base_stats.gd | 8 + resources/stats/base_stats.gd.uid | 1 + .../enemy => resources/stats}/boss_stats.gd | 4 +- resources/stats/boss_stats.gd.uid | 1 + resources/stats/building_stats.gd | 2 + resources/stats/building_stats.gd.uid | 1 + resources/stats/enemy_stats.gd | 11 + resources/stats/enemy_stats.gd.uid | 1 + resources/stats/gate_stats.gd | 7 + resources/stats/gate_stats.gd.uid | 1 + resources/stats/player_stats.gd | 18 + resources/stats/player_stats.gd.uid | 1 + resources/stats/portal_stats.gd | 5 + resources/stats/portal_stats.gd.uid | 1 + resources/stats/village_stats.gd | 2 + resources/stats/village_stats.gd.uid | 1 + scenes/dungeon/dungeon.tscn | 345 ++++------- scenes/dungeon/dungeon_generator.gd | 93 +++ scenes/dungeon/dungeon_generator.gd.uid | 1 + scenes/dungeon/dungeon_manager.gd | 92 ++- scenes/dungeon/dungeon_manager.gd.uid | 2 +- scenes/enemy/boss_stats.gd.uid | 1 - scenes/enemy/boss_stats.tres | 20 - scenes/enemy/detection.gd | 11 - scenes/enemy/detection.gd.uid | 1 - scenes/enemy/enemy.tscn | 96 --- scenes/enemy/enemy_stats.gd | 12 - scenes/enemy/enemy_stats.gd.uid | 1 - scenes/enemy/enemy_stats.tres | 7 - scenes/enemy/init.gd | 101 ---- scenes/enemy/init.gd.uid | 1 - scenes/entities/building/building.gd | 36 ++ scenes/entities/building/building.gd.uid | 1 + scenes/entities/building/building.tscn | 22 + scenes/entities/enemy/enemy.gd | 170 ++++++ scenes/entities/enemy/enemy.gd.uid | 1 + scenes/entities/enemy/enemy.tscn | 76 +++ scenes/entities/gate/gate.gd | 80 +++ scenes/entities/gate/gate.gd.uid | 1 + scenes/entities/gate/gate.tscn | 53 ++ scenes/entities/loot/loot_drop.gd | 39 ++ scenes/entities/loot/loot_drop.gd.uid | 1 + scenes/entities/loot/loot_drop.tscn | 35 ++ scenes/entities/npc/npc.gd | 52 ++ scenes/entities/npc/npc.gd.uid | 1 + scenes/entities/npc/npc.tscn | 53 ++ scenes/entities/player/player.gd | 242 ++++++++ scenes/entities/player/player.gd.uid | 1 + scenes/entities/player/player.tscn | 58 ++ scenes/entities/portal/portal.gd | 62 ++ scenes/entities/portal/portal.gd.uid | 1 + scenes/entities/portal/portal.tscn | 49 ++ scenes/entities/village/village.gd | 36 ++ scenes/entities/village/village.gd.uid | 1 + scenes/entities/village/village.tscn | 47 ++ scenes/hud/hud.gd | 497 ++++++++++++++++ scenes/hud/hud.gd.uid | 1 + scenes/hud/hud.tscn | 552 ++++++++++++------ scenes/hud/minimap.gd | 40 ++ scenes/hud/minimap.gd.uid | 1 + scenes/menu/game_over_overlay.gd | 21 - scenes/menu/game_over_overlay.gd.uid | 1 - scenes/menu/game_over_overlay.tscn | 41 -- scenes/menu/lobby.gd | 55 ++ scenes/menu/lobby.gd.uid | 1 + scenes/menu/lobby.tscn | 58 ++ scenes/menu/main_menu.gd | 63 +- scenes/menu/main_menu.gd.uid | 2 +- scenes/menu/main_menu.tscn | 128 ++-- scenes/menu/options_menu.gd | 49 ++ scenes/menu/options_menu.gd.uid | 1 + scenes/menu/options_menu.tscn | 108 ++++ scenes/player/ability.gd | 9 - scenes/player/ability.gd.uid | 1 - scenes/player/camera.gd | 30 - scenes/player/camera.gd.uid | 1 - scenes/player/init.gd | 59 -- scenes/player/init.gd.uid | 1 - scenes/player/movement.gd | 37 -- scenes/player/movement.gd.uid | 1 - scenes/player/player.tscn | 45 -- scenes/player/player_stats.gd | 10 - scenes/player/player_stats.gd.uid | 1 - scenes/player/player_stats.tres | 8 - scenes/player/role/ability.gd | 16 - scenes/player/role/ability.gd.uid | 1 - scenes/player/role/ability_set.gd.uid | 1 - scenes/player/role/damage/abilities/aoe.tres | 13 - .../player/role/damage/abilities/passive.tres | 12 - .../player/role/damage/abilities/single.tres | 12 - scenes/player/role/damage/abilities/ult.tres | 14 - .../player/role/damage/abilities/utility.tres | 12 - scenes/player/role/damage/set.tres | 14 - scenes/player/role/healer/abilities/aoe.tres | 13 - .../player/role/healer/abilities/passive.tres | 13 - .../player/role/healer/abilities/single.tres | 12 - scenes/player/role/healer/abilities/ult.tres | 14 - .../player/role/healer/abilities/utility.tres | 12 - scenes/player/role/healer/set.tres | 16 - scenes/player/role/role.gd | 14 - scenes/player/role/role.gd.uid | 1 - scenes/player/role/tank/abilities/aoe.tres | 12 - .../player/role/tank/abilities/passive.tres | 13 - scenes/player/role/tank/abilities/single.tres | 11 - scenes/player/role/tank/abilities/ult.tres | 11 - .../player/role/tank/abilities/utility.tres | 12 - scenes/player/role/tank/set.tres | 15 - scenes/player/targeting.gd | 60 -- scenes/player/targeting.gd.uid | 1 - scenes/portal/gate.gd | 36 -- scenes/portal/gate.gd.uid | 1 - scenes/portal/gate.tscn | 36 -- scenes/portal/init.gd | 37 -- scenes/portal/init.gd.uid | 1 - scenes/portal/portal.tscn | 82 --- scenes/portal/portal_stats.gd | 8 - scenes/portal/portal_stats.gd.uid | 1 - scenes/portal/portal_stats.tres | 7 - scenes/portal/red_portal_stats.tres | 9 - scenes/tavern/init.gd | 10 - scenes/tavern/init.gd.uid | 1 - scenes/tavern/tavern.tscn | 51 -- scenes/tavern/tavern_stats.gd | 2 - scenes/tavern/tavern_stats.gd.uid | 1 - scenes/tavern/tavern_stats.tres | 7 - scenes/world/portal_spawner.gd | 69 --- scenes/world/portal_spawner.gd.uid | 1 - scenes/world/world.tscn | 311 +++++----- scenes/world/world_manager.gd | 87 ++- scenes/world/world_manager.gd.uid | 2 +- systems/ability_system.gd | 129 ---- systems/ability_system.gd.uid | 1 - systems/aggro/aggro_config.gd | 8 - systems/aggro/aggro_config.gd.uid | 1 - systems/aggro/aggro_config.tres | 6 - systems/aggro/aggro_decay.gd | 73 --- systems/aggro/aggro_decay.gd.uid | 1 - systems/aggro/aggro_events.gd | 53 -- systems/aggro/aggro_events.gd.uid | 1 - systems/aggro/aggro_system.gd | 17 - systems/aggro/aggro_system.gd.uid | 1 - systems/aggro/aggro_tracker.gd | 96 --- systems/aggro/aggro_tracker.gd.uid | 1 - systems/aggro_system.gd | 110 ++++ systems/aggro_system.gd.uid | 1 + systems/ai_system.gd | 121 ---- systems/ai_system.gd.uid | 1 - systems/attack_system.gd | 27 - systems/attack_system.gd.uid | 1 - systems/audio_system.gd | 161 ++--- systems/audio_system.gd.uid | 2 +- systems/aura_system.gd | 62 -- systems/aura_system.gd.uid | 1 - systems/buff_system.gd | 142 ----- systems/buff_system.gd.uid | 1 - systems/building_system.gd | 92 +++ systems/building_system.gd.uid | 1 + systems/chat_system.gd | 16 + systems/chat_system.gd.uid | 1 + systems/combat/ability_system.gd | 166 ++++++ systems/combat/ability_system.gd.uid | 1 + systems/combat/auto_attack_system.gd | 74 +++ systems/combat/auto_attack_system.gd.uid | 1 + systems/cooldown_system.gd | 87 ++- systems/cooldown_system.gd.uid | 2 +- systems/crafting_system.gd | 49 ++ systems/crafting_system.gd.uid | 1 + systems/damage_system.gd | 44 -- systems/damage_system.gd.uid | 1 - systems/debuff_system.gd | 65 --- systems/debuff_system.gd.uid | 1 - systems/dialog_system.gd | 92 +++ systems/dialog_system.gd.uid | 1 + systems/dungeon_system.gd | 13 - systems/dungeon_system.gd.uid | 1 - systems/effect.gd | 14 - systems/effect.gd.uid | 1 - systems/effect_system.gd | 138 +++++ systems/effect_system.gd.uid | 1 + systems/element_system.gd | 65 +-- systems/element_system.gd.uid | 2 +- systems/heal_system.gd | 22 - systems/heal_system.gd.uid | 1 - systems/health_system.gd | 83 ++- systems/health_system.gd.uid | 2 +- systems/hud_system.gd | 294 ---------- systems/hud_system.gd.uid | 1 - systems/invasion_system.gd | 101 +--- systems/invasion_system.gd.uid | 2 +- systems/inventory_system.gd | 57 ++ systems/inventory_system.gd.uid | 1 + systems/loot_system.gd | 27 + systems/loot_system.gd.uid | 1 + systems/map_system.gd | 26 + systems/map_system.gd.uid | 1 + systems/nameplate_system.gd | 237 -------- systems/nameplate_system.gd.uid | 1 - systems/npc_system.gd | 39 ++ systems/npc_system.gd.uid | 1 + systems/portal_system.gd | 19 - systems/portal_system.gd.uid | 1 - systems/respawn_system.gd | 77 +-- systems/respawn_system.gd.uid | 2 +- systems/role_system.gd | 100 +++- systems/role_system.gd.uid | 2 +- systems/shield_system.gd | 95 +-- systems/shield_system.gd.uid | 2 +- systems/spawn_system.gd | 132 +++-- systems/spawn_system.gd.uid | 2 +- systems/targeting_system.gd | 69 --- systems/targeting_system.gd.uid | 1 - systems/wave_system.gd | 98 ++-- systems/wave_system.gd.uid | 2 +- systems/xp_system.gd | 55 +- systems/xp_system.gd.uid | 2 +- 263 files changed, 5250 insertions(+), 4597 deletions(-) delete mode 100644 autoloads/boss_stats.gd delete mode 100644 autoloads/boss_stats.gd.uid delete mode 100644 autoloads/enemy_stats.gd delete mode 100644 autoloads/enemy_stats.gd.uid create mode 100644 autoloads/net.gd create mode 100644 autoloads/net.gd.uid delete mode 100644 autoloads/player_stats.gd delete mode 100644 autoloads/player_stats.gd.uid delete mode 100644 autoloads/portal_stats.gd delete mode 100644 autoloads/portal_stats.gd.uid create mode 100644 autoloads/save_load.gd create mode 100644 autoloads/save_load.gd.uid create mode 100644 autoloads/stats.gd create mode 100644 autoloads/stats.gd.uid delete mode 100644 autoloads/stats/base_stats.gd delete mode 100644 autoloads/stats/base_stats.gd.uid delete mode 100644 autoloads/tavern_stats.gd delete mode 100644 autoloads/tavern_stats.gd.uid delete mode 100644 doc/core/data.md delete mode 100644 doc/core/detail.md delete mode 100644 doc/data.md delete mode 100644 doc/detail.md delete mode 100644 doc/scenes/player/features.md delete mode 100644 doc/scenes/player/specs.md create mode 100644 export_presets.cfg create mode 100644 resources/abilities/ability.gd create mode 100644 resources/abilities/ability.gd.uid rename {scenes/player/role => resources/abilities}/ability_set.gd (57%) create mode 100644 resources/abilities/ability_set.gd.uid create mode 100644 resources/buildings/building.gd create mode 100644 resources/buildings/building.gd.uid create mode 100644 resources/effects/effect.gd create mode 100644 resources/effects/effect.gd.uid create mode 100644 resources/effects/element.gd create mode 100644 resources/effects/element.gd.uid create mode 100644 resources/items/item.gd create mode 100644 resources/items/item.gd.uid create mode 100644 resources/items/recipe.gd create mode 100644 resources/items/recipe.gd.uid create mode 100644 resources/npcs/npc_profile.gd create mode 100644 resources/npcs/npc_profile.gd.uid create mode 100644 resources/stats/base_stats.gd create mode 100644 resources/stats/base_stats.gd.uid rename {scenes/enemy => resources/stats}/boss_stats.gd (50%) create mode 100644 resources/stats/boss_stats.gd.uid create mode 100644 resources/stats/building_stats.gd create mode 100644 resources/stats/building_stats.gd.uid create mode 100644 resources/stats/enemy_stats.gd create mode 100644 resources/stats/enemy_stats.gd.uid create mode 100644 resources/stats/gate_stats.gd create mode 100644 resources/stats/gate_stats.gd.uid create mode 100644 resources/stats/player_stats.gd create mode 100644 resources/stats/player_stats.gd.uid create mode 100644 resources/stats/portal_stats.gd create mode 100644 resources/stats/portal_stats.gd.uid create mode 100644 resources/stats/village_stats.gd create mode 100644 resources/stats/village_stats.gd.uid create mode 100644 scenes/dungeon/dungeon_generator.gd create mode 100644 scenes/dungeon/dungeon_generator.gd.uid delete mode 100644 scenes/enemy/boss_stats.gd.uid delete mode 100644 scenes/enemy/boss_stats.tres delete mode 100644 scenes/enemy/detection.gd delete mode 100644 scenes/enemy/detection.gd.uid delete mode 100644 scenes/enemy/enemy.tscn delete mode 100644 scenes/enemy/enemy_stats.gd delete mode 100644 scenes/enemy/enemy_stats.gd.uid delete mode 100644 scenes/enemy/enemy_stats.tres delete mode 100644 scenes/enemy/init.gd delete mode 100644 scenes/enemy/init.gd.uid create mode 100644 scenes/entities/building/building.gd create mode 100644 scenes/entities/building/building.gd.uid create mode 100644 scenes/entities/building/building.tscn create mode 100644 scenes/entities/enemy/enemy.gd create mode 100644 scenes/entities/enemy/enemy.gd.uid create mode 100644 scenes/entities/enemy/enemy.tscn create mode 100644 scenes/entities/gate/gate.gd create mode 100644 scenes/entities/gate/gate.gd.uid create mode 100644 scenes/entities/gate/gate.tscn create mode 100644 scenes/entities/loot/loot_drop.gd create mode 100644 scenes/entities/loot/loot_drop.gd.uid create mode 100644 scenes/entities/loot/loot_drop.tscn create mode 100644 scenes/entities/npc/npc.gd create mode 100644 scenes/entities/npc/npc.gd.uid create mode 100644 scenes/entities/npc/npc.tscn create mode 100644 scenes/entities/player/player.gd create mode 100644 scenes/entities/player/player.gd.uid create mode 100644 scenes/entities/player/player.tscn create mode 100644 scenes/entities/portal/portal.gd create mode 100644 scenes/entities/portal/portal.gd.uid create mode 100644 scenes/entities/portal/portal.tscn create mode 100644 scenes/entities/village/village.gd create mode 100644 scenes/entities/village/village.gd.uid create mode 100644 scenes/entities/village/village.tscn create mode 100644 scenes/hud/hud.gd create mode 100644 scenes/hud/hud.gd.uid create mode 100644 scenes/hud/minimap.gd create mode 100644 scenes/hud/minimap.gd.uid delete mode 100644 scenes/menu/game_over_overlay.gd delete mode 100644 scenes/menu/game_over_overlay.gd.uid delete mode 100644 scenes/menu/game_over_overlay.tscn create mode 100644 scenes/menu/lobby.gd create mode 100644 scenes/menu/lobby.gd.uid create mode 100644 scenes/menu/lobby.tscn create mode 100644 scenes/menu/options_menu.gd create mode 100644 scenes/menu/options_menu.gd.uid create mode 100644 scenes/menu/options_menu.tscn delete mode 100644 scenes/player/ability.gd delete mode 100644 scenes/player/ability.gd.uid delete mode 100644 scenes/player/camera.gd delete mode 100644 scenes/player/camera.gd.uid delete mode 100644 scenes/player/init.gd delete mode 100644 scenes/player/init.gd.uid delete mode 100644 scenes/player/movement.gd delete mode 100644 scenes/player/movement.gd.uid delete mode 100644 scenes/player/player.tscn delete mode 100644 scenes/player/player_stats.gd delete mode 100644 scenes/player/player_stats.gd.uid delete mode 100644 scenes/player/player_stats.tres delete mode 100644 scenes/player/role/ability.gd delete mode 100644 scenes/player/role/ability.gd.uid delete mode 100644 scenes/player/role/ability_set.gd.uid delete mode 100644 scenes/player/role/damage/abilities/aoe.tres delete mode 100644 scenes/player/role/damage/abilities/passive.tres delete mode 100644 scenes/player/role/damage/abilities/single.tres delete mode 100644 scenes/player/role/damage/abilities/ult.tres delete mode 100644 scenes/player/role/damage/abilities/utility.tres delete mode 100644 scenes/player/role/damage/set.tres delete mode 100644 scenes/player/role/healer/abilities/aoe.tres delete mode 100644 scenes/player/role/healer/abilities/passive.tres delete mode 100644 scenes/player/role/healer/abilities/single.tres delete mode 100644 scenes/player/role/healer/abilities/ult.tres delete mode 100644 scenes/player/role/healer/abilities/utility.tres delete mode 100644 scenes/player/role/healer/set.tres delete mode 100644 scenes/player/role/role.gd delete mode 100644 scenes/player/role/role.gd.uid delete mode 100644 scenes/player/role/tank/abilities/aoe.tres delete mode 100644 scenes/player/role/tank/abilities/passive.tres delete mode 100644 scenes/player/role/tank/abilities/single.tres delete mode 100644 scenes/player/role/tank/abilities/ult.tres delete mode 100644 scenes/player/role/tank/abilities/utility.tres delete mode 100644 scenes/player/role/tank/set.tres delete mode 100644 scenes/player/targeting.gd delete mode 100644 scenes/player/targeting.gd.uid delete mode 100644 scenes/portal/gate.gd delete mode 100644 scenes/portal/gate.gd.uid delete mode 100644 scenes/portal/gate.tscn delete mode 100644 scenes/portal/init.gd delete mode 100644 scenes/portal/init.gd.uid delete mode 100644 scenes/portal/portal.tscn delete mode 100644 scenes/portal/portal_stats.gd delete mode 100644 scenes/portal/portal_stats.gd.uid delete mode 100644 scenes/portal/portal_stats.tres delete mode 100644 scenes/portal/red_portal_stats.tres delete mode 100644 scenes/tavern/init.gd delete mode 100644 scenes/tavern/init.gd.uid delete mode 100644 scenes/tavern/tavern.tscn delete mode 100644 scenes/tavern/tavern_stats.gd delete mode 100644 scenes/tavern/tavern_stats.gd.uid delete mode 100644 scenes/tavern/tavern_stats.tres delete mode 100644 scenes/world/portal_spawner.gd delete mode 100644 scenes/world/portal_spawner.gd.uid delete mode 100644 systems/ability_system.gd delete mode 100644 systems/ability_system.gd.uid delete mode 100644 systems/aggro/aggro_config.gd delete mode 100644 systems/aggro/aggro_config.gd.uid delete mode 100644 systems/aggro/aggro_config.tres delete mode 100644 systems/aggro/aggro_decay.gd delete mode 100644 systems/aggro/aggro_decay.gd.uid delete mode 100644 systems/aggro/aggro_events.gd delete mode 100644 systems/aggro/aggro_events.gd.uid delete mode 100644 systems/aggro/aggro_system.gd delete mode 100644 systems/aggro/aggro_system.gd.uid delete mode 100644 systems/aggro/aggro_tracker.gd delete mode 100644 systems/aggro/aggro_tracker.gd.uid create mode 100644 systems/aggro_system.gd create mode 100644 systems/aggro_system.gd.uid delete mode 100644 systems/ai_system.gd delete mode 100644 systems/ai_system.gd.uid delete mode 100644 systems/attack_system.gd delete mode 100644 systems/attack_system.gd.uid delete mode 100644 systems/aura_system.gd delete mode 100644 systems/aura_system.gd.uid delete mode 100644 systems/buff_system.gd delete mode 100644 systems/buff_system.gd.uid create mode 100644 systems/building_system.gd create mode 100644 systems/building_system.gd.uid create mode 100644 systems/chat_system.gd create mode 100644 systems/chat_system.gd.uid create mode 100644 systems/combat/ability_system.gd create mode 100644 systems/combat/ability_system.gd.uid create mode 100644 systems/combat/auto_attack_system.gd create mode 100644 systems/combat/auto_attack_system.gd.uid create mode 100644 systems/crafting_system.gd create mode 100644 systems/crafting_system.gd.uid delete mode 100644 systems/damage_system.gd delete mode 100644 systems/damage_system.gd.uid delete mode 100644 systems/debuff_system.gd delete mode 100644 systems/debuff_system.gd.uid create mode 100644 systems/dialog_system.gd create mode 100644 systems/dialog_system.gd.uid delete mode 100644 systems/dungeon_system.gd delete mode 100644 systems/dungeon_system.gd.uid delete mode 100644 systems/effect.gd delete mode 100644 systems/effect.gd.uid create mode 100644 systems/effect_system.gd create mode 100644 systems/effect_system.gd.uid delete mode 100644 systems/heal_system.gd delete mode 100644 systems/heal_system.gd.uid delete mode 100644 systems/hud_system.gd delete mode 100644 systems/hud_system.gd.uid create mode 100644 systems/inventory_system.gd create mode 100644 systems/inventory_system.gd.uid create mode 100644 systems/loot_system.gd create mode 100644 systems/loot_system.gd.uid create mode 100644 systems/map_system.gd create mode 100644 systems/map_system.gd.uid delete mode 100644 systems/nameplate_system.gd delete mode 100644 systems/nameplate_system.gd.uid create mode 100644 systems/npc_system.gd create mode 100644 systems/npc_system.gd.uid delete mode 100644 systems/portal_system.gd delete mode 100644 systems/portal_system.gd.uid delete mode 100644 systems/targeting_system.gd delete mode 100644 systems/targeting_system.gd.uid diff --git a/CLAUDE.md b/CLAUDE.md index db0d8ae..bcc50d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,92 +1,98 @@ # MMO Projekt ## Überblick -Community-MMO in Godot 4.6 (Forward+, Jolt Physics). Ziel: Einsamkeit bekämpfen durch geographische Nähe der Spieler. Stilles Designprinzip — nicht als "Anti-Einsamkeits-Spiel" vermarktet. +Community-MMO in Godot 4.6 (Forward+, Jolt Physics). Vision in `doc/_core.md`, Features in `doc/features.md`. ## Sprache Der User kommuniziert auf Deutsch. Code und Variablen auf Englisch. Kommentare nur wo nötig. ## Code-Style -- GDScript mit 2 Spaces Einrückung (Godot Editor auf Spaces/2 eingestellt) -- Explizite Typen bei Variablen die von Variant-Funktionen kommen (z.B. `var dist: float = ...` statt `var dist := ...` bei `distance_to()`, `min()`, `get_node_or_null()`) -- Keine Debug-Prints im finalen Code (nur temporär zum Testen) +- GDScript mit 2 Spaces Einrückung +- Explizite Typen wo der Inferenzer scheitert (Ergebnisse von Variant-Funktionen, Helper-Returns die Node sein könnten) +- Keine Debug-Prints im finalen Code +- Keine emoji ## Architektur -- **Stats (Model)**: Autoload, zentrale Datenhaltung aller Entity-Attribute. Basiswerte aus Resources. -- **Systeme (Controller)**: Scene-Nodes in world.tscn/dungeon.tscn, lesen/schreiben über Stats. -- **Szenen (Views)**: Rendern, Input senden, Events empfangen. Kein Gameplay-State. -- **EventBus (Signals)**: Autoload, Kommunikation zwischen Szenen und Systemen. -- **Event-Flow**: Szene → Input → EventBus → System → Stats → EventBus → Szene -- **Zwischen Szenen**: Kommunikation über EventBus. Szenen kennen sich nicht. -- **Innerhalb einer Szene**: Zugriff auf Geschwister-Nodes erlaubt. -- **Autoloads**: EventBus (Signals), Stats (Entity-Daten), GameState (Szene + Position) -- **Gruppen**: "player", "enemies", "portals", "boss", "cooldown_system" -- **Resources** für Basiswerte (Stats, Abilities), **Stats Autoload** für Laufzeitwerte -## Projektstruktur -Drei Grundordner nach Verantwortung. Resources liegen bei ihren Skripten. -- `scenes/` — Darstellung + Input - - `player/` — Spieler + player_stats + role/ (Rollen + Abilities) - - `enemy/` — Gegner + enemy_stats + boss/ (Boss + boss_stats) - - `portal/` — Portal + Gate + portal_stats - - `dungeon/` — Dungeon + dungeon_manager - - `hud/` — HUD - - `world/` — Hauptszene + portal_spawner - - `effect_icon_factory.gd` — Shared Utility (Effekt-Icons) - - `healthbar*.gd` — Healthbar-Komponenten (health, shield, status, effects) -- `systems/` — Spiellogik - - 12 Systeme (health, shield, ability, auto_attack, cooldown, enemy_ai, respawn, spawn, effect, element, aura, buff_calc) - - `effect.gd` — Effect Resource (Buff/Debuff/Aura Daten) - - `aggro/` — AggroSystem (system, tracker, decay, events) + aggro_config -- `autoloads/` — Globaler Zustand - - event_bus, game_state - - `stats/` — stats + base_stats +### Autoloads (`autoloads/`) +- `EventBus` — alle Signals (Combat, Wave, Inventory, Dialog, Chat, …) +- `Net` — ENet P2P Multiplayer-Manager (Singleplayer = OfflineMultiplayerPeer) +- `Stats` — zentrale Registry: `register(entity, base_resource)` / `get_stat` / `set_stat` +- `GameState` — Wave#, Scene-Pfade, Run-Seed, Pause, Dungeon-Seed +- `SaveLoad` — JSON-basierte Persistenz (Stub für später) -## Planungsdokument -`plan.md` enthält die vollständige Projektstruktur: Szenenbaum, Szenen mit Nodes, Skripte, Components, Stats, Aggro-Regeln, Abilities und Events. Dieses Dokument ist die Wahrheit für den Soll-Zustand. +### Resources (`resources/`) +- `stats/` — BaseStats + Player/Enemy/Boss/Portal/Gate/Village/Building Stats +- `abilities/` — Ability + AbilitySet (3 Klassen × 5 Abilities, im RoleSystem in Code aufgebaut) +- `items/` — Item, Recipe (Crafting in CraftingSystem in Code) +- `buildings/` — Building Resource (Blueprints in BuildingSystem in Code) +- `npcs/` — NpcProfile (Lore + Personality für Ollama) +- `effects/` — Effect Resource + Element (NONE, FIRE) -## Core Loop -1. Portale spawnen dynamisch auf der Karte (PortalSpawner, max 3, 20-40m vom Zentrum) -2. Spieler greift Portal an → Gegner spawnen bei Lebensschwellen (85%/70%/55%/40%/25%/10%) -3. Spieler bekämpft Gegner mit Abilities (Single, AOE, Utility, Ult) + Auto-Attack -4. Portal bei 0 HP → Gate spawnt, Gegner werden entfernt -5. Spieler betritt Gate → Dungeon (separate Szene, 4 Gegnergruppen + Boss) -6. Spieler kann zwischen Welt und Dungeon hin und her (beide Gates aktiv solange Boss lebt) -7. Boss stirbt → 2s Delay → Spieler wird zur Taverne teleportiert, Gates verschwinden -8. Tod → 3s Respawn bei Taverne +### Szenen (`scenes/`) +- `menu/` — main_menu, lobby, options_menu +- `world/` — world.tscn (Dorf-Welt mit Village + alle Systeme als Children) +- `dungeon/` — dungeon.tscn (procedural generation via dungeon_generator.gd) +- `entities/` — player, enemy, gate, portal, building, loot, npc, village +- `hud/` — hud.tscn (alles-in-einem: vitals, abilities, chat, minimap, inventory, crafting, build, dialog, map, pause, game over) -## Kampfsystem -- Auto-Attack: Rollenspezifisch (D: 10 Schaden/10m, T: 5 Schaden/3m, H: 1 Heilung/20m), 0.5s CD -- 5 Abilities pro Rolle: Single, AOE, Utility, Ult, Passive — jede Rolle hat eigene Werte -- GCD 0.5s (gilt für 1, 2, 4), Utility ignoriert GCD -- Heilung: Heiler heilt sich selbst (Singleplayer), is_heal Flag auf Ability -- Passive: Schadens-Boost (D), Schild-Boost (T), Heal-Boost (H) — je 50%, als Aura (50m Radius) -- Targeting: Klick, TAB (cyclet "enemies" + "portals"), Auto-Target (Gegner > Portal) -- Aggro-System: 1:1 Schaden, Tank 2x, Heilung 0.5x, verfällt -1/s, exponentiell außerhalb Portal-Radius +### Systeme (`systems/`) +Alle als Children unter `World/Systems` und `Dungeon/Systems` instanziert: +- Combat: HealthSystem, ShieldSystem, RespawnSystem, CooldownSystem, AbilitySystem, AutoAttackSystem +- Effects: EffectSystem, ElementSystem, AggroSystem, RoleSystem +- World: SpawnSystem, WaveSystem, InvasionSystem, XpSystem, LootSystem +- Player: InventorySystem, CraftingSystem, BuildingSystem +- Social: NpcSystem, DialogSystem (Ollama HTTP), ChatSystem, MapSystem, AudioSystem -## Rollen -- Tank (T), Schaden (D), Heiler (H) — wechselbar mit ALT+1/2/3 -- Jede Rolle hat eigenes AbilitySet mit unterschiedlichen Werten und Mechaniken -- Tank: Weniger Schaden, Nahkampf, Schild-Ult (300%), Schild-Passive -- Heiler: Heilt statt schadet (Single, AOE, Ult), Heal-Passive, AOE macht Schaden +### Multiplayer-Pattern +- ENet P2P, Host-Authoritative +- Singleplayer = `Net.host_singleplayer()` (OfflineMultiplayerPeer) +- Player-Entities tragen Authority des jeweiligen Peers (`_enter_tree() → set_multiplayer_authority(name.to_int())`) +- Stats/Damage/Spawn/Wave/XP/Inventory laufen nur am Host (`if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: return`) +- Client-Aktionen via RPC zum Host: `_request_*.rpc_id(1, ...)` → Host validiert + sync via `MultiplayerSpawner` und `MultiplayerSynchronizer` +- Sync-Felder pro Entity über `MultiplayerSynchronizer` mit `SceneReplicationConfig` -## Effekte & Elemente -- **EffectSystem**: Verwaltet Buffs, Debuffs, Auras auf allen Entities. Kein Stacking (gleicher Name → Refresh) -- **Effect Resource**: effect_name, type (BUFF/DEBUFF/AURA), stat, value, duration (-1=permanent), is_multiplier, aura_radius, tick_interval, element -- **Auras**: Passive-Abilities werden als AURA-Effekte erstellt. Propagieren Buffs auf Spieler im aura_radius. Verlässt man Radius → Buff sofort weg -- **ElementSystem**: Verwaltet Elementar-Zustände auf Enemies + Portalen. Aktuell: Feuer (DoT 3/Tick, 2s Interval, 6s) -- **Abilities**: `element` Feld (0=NONE, 1=FIRE). Schadens-Abilities der Damage-Rolle haben element=1 (Feuer) -- **Effekt-Icons**: Auf Healthbars (Enemies, Boss, Portal) und Spieler-HUD. Aura=blau, Buff=grün, Debuff=rot +### Input-Map +- WASD + Space: Move + Jump +- 1–4: Abilities (im Build-Mode: Bauteil-Auswahl) +- ALT+1/2/3: Tank/Damage/Healer +- Tab: Cycle target | LMB: Click target | RMB: Camera drag (capture mouse) +- I: Inventory | C: Crafting | B: Build mode | M: Map | Y: Chat | E: Interact (NPC) +- R: Rotate building preview +- Escape: Pause / Cancel UI -## Szenenwechsel -- Stats Autoload cached Spieler-Werte automatisch bei Szenenwechsel -- GameState speichert Rolle + Position -- Gate (Eingang): save_player → Dungeon laden -- Gate (Exit): returning_from_dungeon → Welt laden, Spieler bei Gate-Position -- Boss-Tod: dungeon_cleared → Welt laden, Cache geleert, Spieler bei Taverne mit vollen HP +### Dialog (Ollama) +- HTTP `localhost:11434/api/generate`, Modell `llama3.2` +- Pro NPC: System-Prompt aus `lore` + `personality` +- Fallback: `npc.fallback_text` wenn Ollama nicht erreichbar — Spiel bleibt voll spielbar + +### Bauen +- Grid-Snap 1m, 4 Bauteile (Floor, Wall, Door, Roof) +- LMB im Build-Mode platziert, MMB entfernt +- Material-Verbrauch aus Crafting-Items (alle aus `wood`, das aus `essence` gecrafted wird) +- Persistenz via Host (Buildings sind echte Nodes unter `EntityRoot/Buildings`, MultiplayerSpawner repliziert) + +### Wave-Loop +1. WaveSystem startet 10-min Timer, spawnt 3 normale + 1 rotes Gate +2. Gate stirbt → spawnt Portal an gleicher Position +3. Spieler betritt Portal → Dungeon (procedural, 5–8 Räume + Boss) +4. Boss-Tod → Welt-Rückkehr; rotes Portal → Wave++ + alle Stats skalieren +5. Timer 0 ohne rotes Portal → Invasion (8+wave×2 Mobs zur Village) +6. Village 0 HP → Game Over → Hauptmenü ## Workflow mit dem User -- **plan.md ist zentral** — User will Änderungen zuerst in plan.md dokumentiert haben, dann implementieren -- **Modularer Aufbau** — Jede Szene/Skript hat eine Aufgabe -- **Schrittweise** — Erst planen, dann Code zeigen, dann implementieren -- **Godot-Eigenheiten**: Nach Änderungen an Autoloads/Scripts Godot neu starten. Godot überschreibt .tscn beim Speichern — Szene im Editor schließen vor externen Edits. +- Erst planen (`doc/` lesen), dann implementieren +- Schrittweise, modulares Aufbauen — nicht alles auf einmal umbauen +- Nach Änderungen: `godot-4 --headless --quit-after 60 res://scenes/world/world.tscn` um Parse-/Runtime-Fehler zu sehen +- `doc/` nicht anfassen — das ist die User-Spec + +## Smoke-Test +- `godot-4 --headless --import` → Kein Error +- `godot-4 --headless --quit-after 600 res://scenes/world/world.tscn` → Kein Error +- `godot-4 --headless --quit-after 600 res://scenes/dungeon/dungeon.tscn` → Kein Error + +## Bekannte Lücken / TODOs +- Ollama muss installiert werden (Modell `llama3.2` erwartet) — Stub-Fallback funktioniert sonst +- Save/Load Slots im Hauptmenü (Autoload existiert, UI fehlt) +- Multiplayer Late-Join: Building-/Wave-State wird beim spätem Beitritt nicht synchronisiert +- Damage Numbers / Hit FX / Particle Effects fehlen (rein Polish) diff --git a/autoloads/boss_stats.gd b/autoloads/boss_stats.gd deleted file mode 100644 index 4e1a774..0000000 --- a/autoloads/boss_stats.gd +++ /dev/null @@ -1,82 +0,0 @@ -extends Node - -var entities: Dictionary = {} - -func register(entity: Node, base: EnemyStats, scale: float = 1.0) -> void: - var max_hp: float = base.max_health * scale - var max_sh: float = base.max_shield * scale - entities[entity] = { - "base": base, - "scale": scale, - "health": max_hp, - "max_health": max_hp, - "health_regen": base.health_regen * scale, - "shield": max_sh, - "max_shield": max_sh, - "shield_regen_delay": base.shield_regen_delay, - "shield_regen_time": base.shield_regen_time, - "shield_regen_timer": 0.0, - "alive": true, - "buff_damage": 1.0, - "buff_heal": 1.0, - "buff_shield": 1.0, - "state": 0, - "target": null, - "spawn_position": Vector3.ZERO, - "portal": null, - "attack_timer": 0.0, - } - -func apply_scale(entity: Node, scale: float) -> void: - if entity not in entities: - return - var data: Dictionary = entities[entity] - var base: EnemyStats = data["base"] - data["scale"] = scale - data["max_health"] = base.max_health * scale - data["health"] = data["max_health"] - data["health_regen"] = base.health_regen * scale - data["max_shield"] = base.max_shield * scale - data["shield"] = data["max_shield"] - EventBus.health_changed.emit(entity, data["health"], data["max_health"]) - if base.max_shield > 0: - EventBus.shield_changed.emit(entity, data["shield"], data["max_shield"]) - -func deregister(entity: Node) -> void: - entities.erase(entity) - -func get_stat(entity: Node, key: String) -> Variant: - if entity in entities: - return entities[entity].get(key) - return null - -func set_stat(entity: Node, key: String, value: Variant) -> void: - if entity in entities: - entities[entity][key] = value - -func get_base(entity: Node) -> EnemyStats: - if entity in entities: - return entities[entity]["base"] - return null - -func is_alive(entity: Node) -> bool: - if entity in entities: - return entities[entity]["alive"] - return false - -func set_health(entity: Node, value: float) -> void: - if entity not in entities: - return - entities[entity]["health"] = value - var max_health: float = entities[entity]["max_health"] - EventBus.health_changed.emit(entity, value, max_health) - if value <= 0 and entities[entity]["alive"]: - entities[entity]["alive"] = false - EventBus.entity_died.emit(entity) - -func set_shield(entity: Node, value: float) -> void: - if entity not in entities: - return - entities[entity]["shield"] = value - var max_shield: float = entities[entity]["max_shield"] - EventBus.shield_changed.emit(entity, value, max_shield) diff --git a/autoloads/boss_stats.gd.uid b/autoloads/boss_stats.gd.uid deleted file mode 100644 index 4e97697..0000000 --- a/autoloads/boss_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dbr02t7pt4vcn diff --git a/autoloads/enemy_stats.gd b/autoloads/enemy_stats.gd deleted file mode 100644 index 4e1a774..0000000 --- a/autoloads/enemy_stats.gd +++ /dev/null @@ -1,82 +0,0 @@ -extends Node - -var entities: Dictionary = {} - -func register(entity: Node, base: EnemyStats, scale: float = 1.0) -> void: - var max_hp: float = base.max_health * scale - var max_sh: float = base.max_shield * scale - entities[entity] = { - "base": base, - "scale": scale, - "health": max_hp, - "max_health": max_hp, - "health_regen": base.health_regen * scale, - "shield": max_sh, - "max_shield": max_sh, - "shield_regen_delay": base.shield_regen_delay, - "shield_regen_time": base.shield_regen_time, - "shield_regen_timer": 0.0, - "alive": true, - "buff_damage": 1.0, - "buff_heal": 1.0, - "buff_shield": 1.0, - "state": 0, - "target": null, - "spawn_position": Vector3.ZERO, - "portal": null, - "attack_timer": 0.0, - } - -func apply_scale(entity: Node, scale: float) -> void: - if entity not in entities: - return - var data: Dictionary = entities[entity] - var base: EnemyStats = data["base"] - data["scale"] = scale - data["max_health"] = base.max_health * scale - data["health"] = data["max_health"] - data["health_regen"] = base.health_regen * scale - data["max_shield"] = base.max_shield * scale - data["shield"] = data["max_shield"] - EventBus.health_changed.emit(entity, data["health"], data["max_health"]) - if base.max_shield > 0: - EventBus.shield_changed.emit(entity, data["shield"], data["max_shield"]) - -func deregister(entity: Node) -> void: - entities.erase(entity) - -func get_stat(entity: Node, key: String) -> Variant: - if entity in entities: - return entities[entity].get(key) - return null - -func set_stat(entity: Node, key: String, value: Variant) -> void: - if entity in entities: - entities[entity][key] = value - -func get_base(entity: Node) -> EnemyStats: - if entity in entities: - return entities[entity]["base"] - return null - -func is_alive(entity: Node) -> bool: - if entity in entities: - return entities[entity]["alive"] - return false - -func set_health(entity: Node, value: float) -> void: - if entity not in entities: - return - entities[entity]["health"] = value - var max_health: float = entities[entity]["max_health"] - EventBus.health_changed.emit(entity, value, max_health) - if value <= 0 and entities[entity]["alive"]: - entities[entity]["alive"] = false - EventBus.entity_died.emit(entity) - -func set_shield(entity: Node, value: float) -> void: - if entity not in entities: - return - entities[entity]["shield"] = value - var max_shield: float = entities[entity]["max_shield"] - EventBus.shield_changed.emit(entity, value, max_shield) diff --git a/autoloads/enemy_stats.gd.uid b/autoloads/enemy_stats.gd.uid deleted file mode 100644 index 6ab7faf..0000000 --- a/autoloads/enemy_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bvxn6y15tvidu diff --git a/autoloads/event_bus.gd b/autoloads/event_bus.gd index b95a1d7..1120630 100644 --- a/autoloads/event_bus.gd +++ b/autoloads/event_bus.gd @@ -1,73 +1,60 @@ extends Node -# Intentionen (Input → System) -signal ability_use(player, ability_index) -signal role_change_requested(player, role) -signal target_requested(player, target) -signal enemy_detected(enemy, player) -signal enemy_lost(enemy, player) -signal portal_entered(portal, player) +signal entity_registered(entity: Node) +signal entity_deregistered(entity: Node) -# Kampf -signal attack_executed(attacker, position, direction, damage) -signal damage_dealt(attacker, target, damage) -signal damage_requested(attacker, target, amount) -signal heal_requested(healer, target, amount) +signal damage_requested(attacker: Node, target: Node, amount: float, element: int) +signal heal_requested(healer: Node, target: Node, amount: float) +signal damage_dealt(attacker: Node, target: Node, amount: float) +signal entity_died(entity: Node) +signal entity_respawned(entity: Node) +signal health_changed(entity: Node, current: float, max: float) +signal shield_changed(entity: Node, current: float, max: float) +signal shield_broken(entity: Node) -# Entity -signal entity_died(entity) -signal health_changed(entity, current, max_val) -signal shield_changed(entity, current, max_val) -signal shield_broken(entity) -signal shield_regenerated(entity) +signal ability_use_requested(player: Node, ability_index: int) +signal ability_used(player: Node, ability_index: int, ability: Resource) +signal cooldown_tick(entity: Node, cds: PackedFloat32Array, max_cds: PackedFloat32Array, gcd: float) +signal role_changed(player: Node, role: int) +signal target_changed(player: Node, target: Node) -# Spieler -signal target_changed(player, target) -signal player_respawned(player) -signal role_changed(player, role_type) -signal respawn_tick(timer) -signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer) +signal effect_applied(target: Node, effect: Resource, source: Node) +signal effect_expired(target: Node, effect: Resource) +signal element_applied(target: Node, element: int) +signal buff_changed(entity: Node, stat: StringName, value: float) -# Buff -signal buff_changed(entity, stat, value) +signal enemy_detected(enemy: Node, player: Node) +signal enemy_lost(enemy: Node, player: Node) +signal enemy_engaged(enemy: Node, target: Node) -# Gegner -signal enemy_engaged(enemy, target) +signal gate_destroyed(gate: Node) +signal portal_spawned(portal: Node) +signal portal_entered(portal: Node, player: Node) +signal dungeon_cleared(seed: int) +signal boss_defeated(boss: Node) -# Portal -signal portal_spawn(portal, enemies) -signal portal_defeated(portal) +signal loot_dropped(items: Array, position: Vector3) +signal item_picked_up(player: Node, item: Resource) +signal inventory_changed(player: Node) +signal item_crafted(player: Node, item: Resource) +signal building_placed(building: Node) +signal building_removed(building: Node) -# Dungeon -signal dungeon_cleared() - -# Effects -signal effect_requested(target, effect, source) -signal effect_applied(target, effect) -signal effect_expired(target, effect) - -# Elements -signal element_damage_dealt(attacker, target, amount, element) -signal element_applied(target, element) -signal element_reaction(target, element_a, element_b, reaction_name) - -# Wave -signal run_started(wave_number) -signal wave_started(wave_number) -signal wave_timer_tick(seconds_remaining) -signal wave_ended(wave_number, success) - -# XP / Level -signal xp_gained(player, amount) -signal level_up(player, new_level) - -# Taverne -signal tavern_damaged(current, max_val) -signal tavern_destroyed() - -# Invasion -signal invasion_started(enemies) -signal invasion_ended(success) - -# Game-Over +signal wave_started(wave_number: int) +signal wave_timer_tick(seconds_remaining: float) +signal wave_ended(wave_number: int, success: bool) +signal invasion_started() +signal invasion_ended(success: bool) +signal village_damaged(current: float, max: float) +signal village_destroyed() signal game_over() +signal run_started(wave_number: int) + +signal xp_gained(player: Node, amount: float) +signal level_up(player: Node, new_level: int) + +signal dialog_opened(player: Node, npc: Node) +signal dialog_closed(player: Node) +signal chat_message(peer_id: int, sender_name: String, text: String) + +signal scene_change_requested(scene_path: String) diff --git a/autoloads/event_bus.gd.uid b/autoloads/event_bus.gd.uid index 5fb252b..39a2c78 100644 --- a/autoloads/event_bus.gd.uid +++ b/autoloads/event_bus.gd.uid @@ -1 +1 @@ -uid://g7a7xkg1pgb4 +uid://361x7bdk2j6v diff --git a/autoloads/game_state.gd b/autoloads/game_state.gd index 1e65473..f1390b7 100644 --- a/autoloads/game_state.gd +++ b/autoloads/game_state.gd @@ -1,19 +1,36 @@ extends Node -# Run-Zustand +const ROLE_TANK: int = 0 +const ROLE_DAMAGE: int = 1 +const ROLE_HEALER: int = 2 + +const SCENE_MAIN_MENU: String = "res://scenes/menu/main_menu.tscn" +const SCENE_LOBBY: String = "res://scenes/menu/lobby.tscn" +const SCENE_WORLD: String = "res://scenes/world/world.tscn" +const SCENE_DUNGEON: String = "res://scenes/dungeon/dungeon.tscn" +const SCENE_OPTIONS: String = "res://scenes/menu/options_menu.tscn" + +var current_scene: String = SCENE_MAIN_MENU +var paused: bool = false +var run_seed: int = 0 +var dungeon_seed: int = 0 +var dungeon_red: bool = false var current_wave: int = 1 -var wave_timer_remaining: float = 0.0 -var run_initialized: bool = false +var portal_return_position: Vector3 = Vector3.ZERO -# Dungeon-Kontext (für XP-Zuordnung nach Clear) -var last_dungeon_variant: int = 0 - -# Flag für Forced Return (Timer läuft ab während Spieler im Dungeon) -var force_return_to_world: bool = false - -func reset() -> void: +func reset_run() -> void: + run_seed = randi() current_wave = 1 - wave_timer_remaining = 0.0 - run_initialized = false - last_dungeon_variant = 0 - force_return_to_world = false + dungeon_seed = 0 + dungeon_red = false + paused = false + Stats.clear_all() + +func change_scene(path: String) -> void: + current_scene = path + EventBus.scene_change_requested.emit(path) + get_tree().change_scene_to_file(path) + +func set_paused(value: bool) -> void: + paused = value + get_tree().paused = value diff --git a/autoloads/game_state.gd.uid b/autoloads/game_state.gd.uid index 4cf2998..647312e 100644 --- a/autoloads/game_state.gd.uid +++ b/autoloads/game_state.gd.uid @@ -1 +1 @@ -uid://c3jq4raqs0onf +uid://dettmu50fjtvc diff --git a/autoloads/net.gd b/autoloads/net.gd new file mode 100644 index 0000000..4e34a0d --- /dev/null +++ b/autoloads/net.gd @@ -0,0 +1,148 @@ +extends Node + +const DEFAULT_PORT: int = 7777 +const MAX_PLAYERS: int = 4 + +signal peer_connected(id: int) +signal peer_disconnected(id: int) +signal connected_to_server() +signal connection_failed() +signal server_disconnected() +signal world_ready() +signal peer_world_loaded(peer_id: int) + +var player_names: Dictionary = {} +var local_name: String = "Player" +var _expected_peers: Array = [] +var _ready_peers: Array = [] + +func _ready() -> void: + multiplayer.peer_connected.connect(_on_peer_connected) + multiplayer.peer_disconnected.connect(_on_peer_disconnected) + multiplayer.connected_to_server.connect(_on_connected_to_server) + multiplayer.connection_failed.connect(_on_connection_failed) + multiplayer.server_disconnected.connect(_on_server_disconnected) + +func host(port: int = DEFAULT_PORT, max_clients: int = MAX_PLAYERS - 1) -> bool: + var peer := ENetMultiplayerPeer.new() + var err := peer.create_server(port, max_clients) + if err != OK: + push_error("Net: failed to host on port %d (err=%d)" % [port, err]) + return false + multiplayer.multiplayer_peer = peer + player_names[1] = local_name + return true + +func join(address: String, port: int = DEFAULT_PORT) -> bool: + var peer := ENetMultiplayerPeer.new() + var err := peer.create_client(address, port) + if err != OK: + push_error("Net: failed to join %s:%d (err=%d)" % [address, port, err]) + return false + multiplayer.multiplayer_peer = peer + return true + +func host_singleplayer() -> bool: + if multiplayer.multiplayer_peer != null: + multiplayer.multiplayer_peer.close() + var peer := OfflineMultiplayerPeer.new() + multiplayer.multiplayer_peer = peer + player_names.clear() + player_names[1] = local_name + return true + +func disconnect_net() -> void: + if multiplayer.multiplayer_peer != null: + multiplayer.multiplayer_peer.close() + multiplayer.multiplayer_peer = null + player_names.clear() + +func is_host() -> bool: + return multiplayer.multiplayer_peer == null or multiplayer.is_server() + +func is_authority_for(node: Node) -> bool: + return node.is_multiplayer_authority() + +func local_id() -> int: + if multiplayer.multiplayer_peer == null: + return 1 + return multiplayer.get_unique_id() + +func _on_peer_connected(id: int) -> void: + peer_connected.emit(id) + if is_host(): + _register_name.rpc_id(id, 1, local_name) + +func _on_peer_disconnected(id: int) -> void: + player_names.erase(id) + peer_disconnected.emit(id) + +func _on_connected_to_server() -> void: + player_names[1] = "Host" + player_names[multiplayer.get_unique_id()] = local_name + _register_name.rpc(multiplayer.get_unique_id(), local_name) + connected_to_server.emit() + +func _on_connection_failed() -> void: + multiplayer.multiplayer_peer = null + connection_failed.emit() + +func _on_server_disconnected() -> void: + multiplayer.multiplayer_peer = null + player_names.clear() + server_disconnected.emit() + +@rpc("any_peer", "reliable", "call_local") +func _register_name(peer_id: int, name: String) -> void: + player_names[peer_id] = name + +func mark_world_loaded() -> void: + if multiplayer.multiplayer_peer == null: + _expected_peers = [1] + _ready_peers = [1] + world_ready.emit() + return + if is_host(): + _expected_peers = [1] + for p in multiplayer.get_peers(): + _expected_peers.append(p) + _ready_peers = [1] + if _all_ready(): + _broadcast_ready.rpc() + else: + _client_world_loaded.rpc_id(1, multiplayer.get_unique_id()) + +@rpc("any_peer", "reliable") +func _client_world_loaded(peer_id: int) -> void: + if not is_host(): + return + if not peer_id in _expected_peers: + _expected_peers.append(peer_id) + if not peer_id in _ready_peers: + _ready_peers.append(peer_id) + peer_world_loaded.emit(peer_id) + if _all_ready(): + _broadcast_ready.rpc() + +@rpc("authority", "reliable") +func _load_scene_for_peer(path: String) -> void: + GameState.change_scene(path) + +func tell_peer_to_load_scene(peer_id: int, path: String) -> void: + if not is_host(): + return + _load_scene_for_peer.rpc_id(peer_id, path) + +@rpc("authority", "reliable", "call_local") +func _broadcast_ready() -> void: + world_ready.emit() + +func _all_ready() -> bool: + for p in _expected_peers: + if not p in _ready_peers: + return false + return true + +func reset_world_ready() -> void: + _expected_peers.clear() + _ready_peers.clear() diff --git a/autoloads/net.gd.uid b/autoloads/net.gd.uid new file mode 100644 index 0000000..79abdfa --- /dev/null +++ b/autoloads/net.gd.uid @@ -0,0 +1 @@ +uid://1k1cufc2skfr diff --git a/autoloads/player_stats.gd b/autoloads/player_stats.gd deleted file mode 100644 index 60d31bd..0000000 --- a/autoloads/player_stats.gd +++ /dev/null @@ -1,195 +0,0 @@ -extends Node - -enum Role { TANK, DAMAGE, HEALER } - -# Basis (aus Resource geladen) -var base: PlayerStats -var speed := 5.0 -var jump_velocity := 4.5 -var target_range := 20.0 -var combat_timeout := 3.0 -var respawn_time := 3.0 -var gcd_time := 0.5 -var aa_cooldown := 0.5 - -# Laufzeit -var health := 100.0 -var max_health := 100.0 -var health_regen := 0.0 -var shield := 0.0 -var max_shield := 0.0 -var shield_regen_delay := 3.0 -var shield_regen_time := 5.0 -var shield_regen_timer := 0.0 -var alive := true - -# Buffs -var buff_damage := 1.0 -var buff_heal := 1.0 -var buff_shield := 1.0 - -# Level / XP -const XP_PER_LEVEL: int = 50 -var level: int = 1 -var xp: int = 0 -var xp_to_next: int = XP_PER_LEVEL -var level_scale: float = 1.0 - -# Rolle -var current_role: int = Role.DAMAGE -var ability_set: AbilitySet = null - -# Kampf -var target: Node3D = null -var in_combat := false -var combat_timer := 0.0 - -# Cooldowns -var cooldowns: Array[float] = [] -var max_cooldowns: Array[float] = [] -var gcd := 0.0 -var aa_timer := 0.0 - -# Szenenwechsel -var portal_position := Vector3.ZERO -var returning_from_dungeon := false -var dungeon_cleared := false - -# Cache für Szenenwechsel -var _cache: Dictionary = {} - -func init_from_resource(res: PlayerStats) -> void: - base = res - speed = res.speed - jump_velocity = res.jump_velocity - target_range = res.target_range - combat_timeout = res.combat_timeout - respawn_time = res.respawn_time - gcd_time = res.gcd_time - aa_cooldown = res.aa_cooldown - if _cache.is_empty(): - max_health = res.max_health * level_scale - health = max_health - health_regen = res.health_regen * level_scale - max_shield = res.max_shield * level_scale - shield = max_shield - shield_regen_delay = res.shield_regen_delay - shield_regen_time = res.shield_regen_time - shield_regen_timer = 0.0 - alive = true - buff_damage = 1.0 - buff_heal = 1.0 - buff_shield = 1.0 - else: - _restore_cache() - cooldowns.resize(5) - cooldowns.fill(0.0) - max_cooldowns.resize(5) - max_cooldowns.fill(0.0) - gcd = 0.0 - aa_timer = 0.0 - -func set_health(value: float) -> void: - health = value - EventBus.health_changed.emit(self, health, max_health) - if health <= 0 and alive: - alive = false - EventBus.entity_died.emit(self) - -func set_shield(value: float) -> void: - shield = value - EventBus.shield_changed.emit(self, shield, max_shield) - -func set_role(role: int) -> void: - current_role = role - EventBus.role_changed.emit(self, current_role) - -func set_target(new_target: Node3D) -> void: - target = new_target - EventBus.target_changed.emit(self, target) - -func respawn() -> void: - health = max_health - shield = max_shield - alive = true - EventBus.health_changed.emit(self, health, max_health) - EventBus.shield_changed.emit(self, shield, max_shield) - EventBus.player_respawned.emit(self) - -func save_cache() -> void: - _cache = { - "health": health, - "max_health": max_health, - "health_regen": health_regen, - "shield": shield, - "max_shield": max_shield, - "shield_regen_delay": shield_regen_delay, - "shield_regen_time": shield_regen_time, - "alive": alive, - "buff_damage": buff_damage, - "buff_heal": buff_heal, - "buff_shield": buff_shield, - } - -func clear_cache() -> void: - _cache.clear() - portal_position = Vector3.ZERO - returning_from_dungeon = false - dungeon_cleared = false - -func reset_run() -> void: - clear_cache() - level = 1 - xp = 0 - xp_to_next = XP_PER_LEVEL - level_scale = 1.0 - -func add_xp(amount: int) -> void: - xp += amount - EventBus.xp_gained.emit(self, amount) - while xp >= xp_to_next: - xp -= xp_to_next - level_up() - -func level_up() -> void: - level += 1 - level_scale = float(_fibonacci(level)) - xp_to_next = XP_PER_LEVEL * _fibonacci(level) - if base: - max_health = base.max_health * level_scale - max_shield = base.max_shield * level_scale - else: - max_health = 100.0 * level_scale - max_shield = 50.0 * level_scale - health = max_health - shield = max_shield - EventBus.health_changed.emit(self, health, max_health) - EventBus.shield_changed.emit(self, shield, max_shield) - EventBus.level_up.emit(self, level) - -func _fibonacci(n: int) -> int: - if n <= 1: - return 1 - if n == 2: - return 2 - var a := 1 - var b := 2 - for i in range(3, n + 1): - var c := a + b - a = b - b = c - return b - -func _restore_cache() -> void: - health = _cache.get("health", max_health) - max_health = _cache.get("max_health", max_health) - health_regen = _cache.get("health_regen", 0.0) - shield = _cache.get("shield", 0.0) - max_shield = _cache.get("max_shield", 0.0) - shield_regen_delay = _cache.get("shield_regen_delay", 3.0) - shield_regen_time = _cache.get("shield_regen_time", 5.0) - alive = _cache.get("alive", true) - buff_damage = _cache.get("buff_damage", 1.0) - buff_heal = _cache.get("buff_heal", 1.0) - buff_shield = _cache.get("buff_shield", 1.0) - _cache.clear() diff --git a/autoloads/player_stats.gd.uid b/autoloads/player_stats.gd.uid deleted file mode 100644 index 9417cb1..0000000 --- a/autoloads/player_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://blmuqkl3aro5w diff --git a/autoloads/portal_stats.gd b/autoloads/portal_stats.gd deleted file mode 100644 index 8e67f41..0000000 --- a/autoloads/portal_stats.gd +++ /dev/null @@ -1,45 +0,0 @@ -extends Node - -var entities: Dictionary = {} - -func register(entity: Node, base: PortalStats) -> void: - var thresholds: Array[float] = base.thresholds.duplicate() - var triggered: Array[bool] = [] - triggered.resize(thresholds.size()) - triggered.fill(false) - entities[entity] = { - "base": base, - "health": base.max_health, - "max_health": base.max_health, - "alive": true, - "spawn_count": base.spawn_count, - "thresholds": thresholds, - "triggered": triggered, - } - -func deregister(entity: Node) -> void: - entities.erase(entity) - -func get_stat(entity: Node, key: String) -> Variant: - if entity in entities: - return entities[entity].get(key) - return null - -func set_stat(entity: Node, key: String, value: Variant) -> void: - if entity in entities: - entities[entity][key] = value - -func is_alive(entity: Node) -> bool: - if entity in entities: - return entities[entity]["alive"] - return false - -func set_health(entity: Node, value: float) -> void: - if entity not in entities: - return - entities[entity]["health"] = value - var max_health: float = entities[entity]["max_health"] - EventBus.health_changed.emit(entity, value, max_health) - if value <= 0 and entities[entity]["alive"]: - entities[entity]["alive"] = false - EventBus.entity_died.emit(entity) diff --git a/autoloads/portal_stats.gd.uid b/autoloads/portal_stats.gd.uid deleted file mode 100644 index 6356dbf..0000000 --- a/autoloads/portal_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://doullpjapcsk1 diff --git a/autoloads/save_load.gd b/autoloads/save_load.gd new file mode 100644 index 0000000..10e27c6 --- /dev/null +++ b/autoloads/save_load.gd @@ -0,0 +1,46 @@ +extends Node + +const SAVE_DIR: String = "user://saves/" + +func ensure_dir() -> void: + DirAccess.make_dir_recursive_absolute(SAVE_DIR) + +func save_path(slot: String) -> String: + return SAVE_DIR + slot + ".save" + +func save_run(slot: String, payload: Dictionary) -> bool: + ensure_dir() + var f := FileAccess.open(save_path(slot), FileAccess.WRITE) + if f == null: + push_error("SaveLoad: cannot open %s for write" % slot) + return false + f.store_string(JSON.stringify(payload, " ")) + f.close() + return true + +func load_run(slot: String) -> Dictionary: + var path := save_path(slot) + if not FileAccess.file_exists(path): + return {} + var f := FileAccess.open(path, FileAccess.READ) + var text := f.get_as_text() + f.close() + var data: Variant = JSON.parse_string(text) + if typeof(data) != TYPE_DICTIONARY: + return {} + return data + +func list_slots() -> Array[String]: + ensure_dir() + var out: Array[String] = [] + var d := DirAccess.open(SAVE_DIR) + if d == null: + return out + d.list_dir_begin() + var fname := d.get_next() + while fname != "": + if not d.current_is_dir() and fname.ends_with(".save"): + out.append(fname.trim_suffix(".save")) + fname = d.get_next() + d.list_dir_end() + return out diff --git a/autoloads/save_load.gd.uid b/autoloads/save_load.gd.uid new file mode 100644 index 0000000..6b965af --- /dev/null +++ b/autoloads/save_load.gd.uid @@ -0,0 +1 @@ +uid://bstx6urqlutmq diff --git a/autoloads/stats.gd b/autoloads/stats.gd new file mode 100644 index 0000000..e45a3d9 --- /dev/null +++ b/autoloads/stats.gd @@ -0,0 +1,124 @@ +extends Node + +const SYNCED_STATS: Array = [ + "health", "shield", "max_health", "max_shield", + "role", "level", "xp", "xp_to_next", + "buff_damage", "buff_heal", "buff_shield", +] +const SYNC_INTERVAL: float = 0.10 + +var _entities: Dictionary = {} +var _player_cache: Dictionary = {} +var _dirty: Dictionary = {} +var _accum: float = 0.0 + +func _ready() -> void: + set_process(true) + +func _process(delta: float) -> void: + _accum += delta + if _accum < SYNC_INTERVAL: + return + _accum = 0.0 + if multiplayer.multiplayer_peer == null or multiplayer.multiplayer_peer is OfflineMultiplayerPeer or not multiplayer.is_server(): + _dirty.clear() + return + if _dirty.is_empty(): + return + for entity in _dirty.keys(): + if not is_instance_valid(entity) or not entity.is_inside_tree(): + continue + _sync_stats_batch.rpc(String(entity.get_path()), _dirty[entity]) + _dirty.clear() + +@rpc("authority", "unreliable_ordered") +func _sync_stats_batch(path_str: String, changes: Dictionary) -> void: + var entity := get_node_or_null(NodePath(path_str)) + if entity == null or not entity in _entities: + return + for stat in changes.keys(): + _entities[entity][stat] = changes[stat] + match stat: + "health": + EventBus.health_changed.emit(entity, changes[stat], _entities[entity].get("max_health", 100.0)) + "shield": + EventBus.shield_changed.emit(entity, changes[stat], _entities[entity].get("max_shield", 0.0)) + "role": + EventBus.role_changed.emit(entity, changes[stat]) + "level": + EventBus.level_up.emit(entity, changes[stat]) + "buff_damage", "buff_heal", "buff_shield": + EventBus.buff_changed.emit(entity, StringName(stat), changes[stat]) + +func register(entity: Node, base_resource: Resource) -> void: + if not is_instance_valid(entity): + return + var data: Dictionary = {} + for prop in base_resource.get_property_list(): + var name: String = prop.name + if not (prop.usage & PROPERTY_USAGE_STORAGE): + continue + if name in ["resource_local_to_scene", "resource_path", "resource_name", "resource_scene_unique_id", "script"]: + continue + data[name] = base_resource.get(name) + data["health"] = data.get("max_health", 100.0) + data["shield"] = data.get("max_shield", 0.0) + data["shield_regen_timer"] = 0.0 + data["base_resource"] = base_resource + _entities[entity] = data + EventBus.entity_registered.emit(entity) + +func deregister(entity: Node) -> void: + if entity in _entities: + _entities.erase(entity) + EventBus.entity_deregistered.emit(entity) + +func has(entity: Node) -> bool: + return entity in _entities + +func get_stat(entity: Node, stat: String, default: Variant = 0.0) -> Variant: + if entity in _entities: + return _entities[entity].get(stat, default) + return default + +func set_stat(entity: Node, stat: String, value: Variant) -> void: + if not entity in _entities: + return + var prev: Variant = _entities[entity].get(stat) + _entities[entity][stat] = value + if stat in SYNCED_STATS and prev != value and multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.is_server() and is_instance_valid(entity) and entity.is_inside_tree(): + if not entity in _dirty: + _dirty[entity] = {} + _dirty[entity][stat] = value + + +func get_all(entity: Node) -> Dictionary: + return _entities.get(entity, {}) + +func entities() -> Array: + return _entities.keys() + +func entities_in_group(group: StringName) -> Array: + var out: Array = [] + for e in _entities.keys(): + if is_instance_valid(e) and e.is_in_group(group): + out.append(e) + return out + +func cache_player(peer_id: int, entity: Node) -> void: + if entity in _entities: + _player_cache[peer_id] = _entities[entity].duplicate(true) + +func restore_player(peer_id: int, entity: Node) -> void: + if peer_id in _player_cache: + _entities[entity] = _player_cache[peer_id].duplicate(true) + +func clear_player_cache(peer_id: int = -1) -> void: + if peer_id == -1: + _player_cache.clear() + else: + _player_cache.erase(peer_id) + +func clear_all() -> void: + _entities.clear() + _player_cache.clear() diff --git a/autoloads/stats.gd.uid b/autoloads/stats.gd.uid new file mode 100644 index 0000000..4516ce6 --- /dev/null +++ b/autoloads/stats.gd.uid @@ -0,0 +1 @@ +uid://cdrii2l4sefow diff --git a/autoloads/stats/base_stats.gd b/autoloads/stats/base_stats.gd deleted file mode 100644 index f5d073a..0000000 --- a/autoloads/stats/base_stats.gd +++ /dev/null @@ -1,8 +0,0 @@ -extends Resource -class_name BaseStats - -@export var max_health := 100.0 -@export var health_regen := 0.0 -@export var max_shield := 0.0 -@export var shield_regen_delay := 3.0 -@export var shield_regen_time := 5.0 diff --git a/autoloads/stats/base_stats.gd.uid b/autoloads/stats/base_stats.gd.uid deleted file mode 100644 index b7ecabe..0000000 --- a/autoloads/stats/base_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cet184f878lb8 diff --git a/autoloads/tavern_stats.gd b/autoloads/tavern_stats.gd deleted file mode 100644 index bc28eed..0000000 --- a/autoloads/tavern_stats.gd +++ /dev/null @@ -1,38 +0,0 @@ -extends Node - -var entities: Dictionary = {} - -func register(entity: Node, base: Resource) -> void: - entities[entity] = { - "base": base, - "health": base.max_health, - "max_health": base.max_health, - "alive": true, - } - -func deregister(entity: Node) -> void: - entities.erase(entity) - -func get_stat(entity: Node, key: String) -> Variant: - if entity in entities: - return entities[entity].get(key) - return null - -func set_stat(entity: Node, key: String, value: Variant) -> void: - if entity in entities: - entities[entity][key] = value - -func is_alive(entity: Node) -> bool: - if entity in entities: - return entities[entity]["alive"] - return false - -func set_health(entity: Node, value: float) -> void: - if entity not in entities: - return - entities[entity]["health"] = value - var max_health: float = entities[entity]["max_health"] - EventBus.tavern_damaged.emit(value, max_health) - if value <= 0 and entities[entity]["alive"]: - entities[entity]["alive"] = false - EventBus.tavern_destroyed.emit() diff --git a/autoloads/tavern_stats.gd.uid b/autoloads/tavern_stats.gd.uid deleted file mode 100644 index 975fe5e..0000000 --- a/autoloads/tavern_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://822h8c1pur1a diff --git a/doc/core/data.md b/doc/core/data.md deleted file mode 100644 index 75d5982..0000000 --- a/doc/core/data.md +++ /dev/null @@ -1,4 +0,0 @@ -- player/ - - player szene -- enemy/ - - enemy szene \ No newline at end of file diff --git a/doc/core/detail.md b/doc/core/detail.md deleted file mode 100644 index e69de29..0000000 diff --git a/doc/data.md b/doc/data.md deleted file mode 100644 index 5f09356..0000000 --- a/doc/data.md +++ /dev/null @@ -1,16 +0,0 @@ -- core/ - - persistenz der daten - - enthält die wahrheit zum zustand der welt - - model -- scenes/ - - verwaltet die darstellung der szene - - dürfen berechnen, aber nicht den zustand ändern - - logik wird an systeme gesendet und von diesen empfangen - - view -- systems/ - - verarbeiten logik von szenen - - schnittstelle von szenen und core - - dürfen berechnungen durchführen - - controller -- assets/ - - objekte für szenen diff --git a/doc/detail.md b/doc/detail.md deleted file mode 100644 index 2cd4e23..0000000 --- a/doc/detail.md +++ /dev/null @@ -1,25 +0,0 @@ -ziel -- dorf verteidigen -- hohe welle erreichen -sieg möglichkeiten -- gegner durch angriff besiegen -- gegner durch verteidigung besiegen -core loop -- gegner besiegen -- stärker werden -- welle überleben -meta loop -- viele wellen überleben -- presige sammeln -- pernamente verbesserungen -kern -- dungeons besiegen um erfahrung und ressourcen zu bekommen -- dorf kann durch das bausystem mit ressourcen aus den portalen verbessert werden - -typischer ablauf (angriff) -- dorf verteidigen -- portale spawnen -- protale besiegen -> stärker werden -- rotes protal besigen -> welle geschafft -- rotes portal bricht nach timer aus -> greift tarverne an -- tarverne tod -> game over \ No newline at end of file diff --git a/doc/scenes/player/features.md b/doc/scenes/player/features.md deleted file mode 100644 index 5d87edd..0000000 --- a/doc/scenes/player/features.md +++ /dev/null @@ -1,12 +0,0 @@ -- WASD bewegung -- Leertaste springen mit gravitation - -- gruppe "player" -- thirs-person camera -- RMB+Hold kamera bewegung, player ausrichtung -- leertaste springen -- listener - - player stirbt - - player wird gespawnt -- events - - fähigkeit verwenden \ No newline at end of file diff --git a/doc/scenes/player/specs.md b/doc/scenes/player/specs.md deleted file mode 100644 index 54e4e22..0000000 --- a/doc/scenes/player/specs.md +++ /dev/null @@ -1,11 +0,0 @@ -- CharacterBody3D - - CollisionShape3D - capsule - - - -- init.gd - daten initialisieren -- listener.gd - listener registrieren -- movement.gd - player bewegung -- camera.gd - kamera bewegung -- ability.gd - -- targeting.gd -- role.gd \ No newline at end of file diff --git a/export_presets.cfg b/export_presets.cfg new file mode 100644 index 0000000..e180437 --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,46 @@ +[preset.0] + +name="Linux" +platform="Linux" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=false +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +shader_baker/enabled=false +binary_format/architecture="x86_64" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/usr/bin/env bash +export DISPLAY=:0 +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +\"{temp_dir}/{exe_name}\" {cmd_args}" +ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash +pkill -x -f \"{temp_dir}/{exe_name} {cmd_args}\" +rm -rf \"{temp_dir}\"" diff --git a/plan.md b/plan.md index 1850867..0f79b37 100644 --- a/plan.md +++ b/plan.md @@ -22,12 +22,12 @@ scenes/ — Darstellung + Input healthbar_status.gd — Target-Border + Aggro-Farbwechsel healthbar_effects.gd — Effekt-Icons auf Healthbar player/ — Spieler + player_stats - role/ — Rollenwechsel + Ability/AbilitySet-Klassen - damage/ — set.tres + abilities/ - tank/ — set.tres + abilities/ - healer/ — set.tres + abilities/ + role/ — Rollenwechsel + Ability/AbilitySet-Klassen + damage/ — set.tres + abilities/ + tank/ — set.tres + abilities/ + healer/ — set.tres + abilities/ enemy/ — Gegner + enemy_stats - boss/ — Boss + boss_stats + boss/ — Boss + boss_stats portal/ — Portal + Gate + portal_stats dungeon/ — Dungeon + dungeon_manager hud/ — HUD (4 Skripte: vitals, respawn, abilities, effects) @@ -49,7 +49,7 @@ autoloads/ — Globaler Zustand - Taverne - Player - Portale (dynamisch) - - Gegner + - Gegner - HUD ## Architektur @@ -254,14 +254,14 @@ autoloads/ — Globaler Zustand - world.tscn — Hauptszene (100x100m) - Systems (alle Systeme als Child-Nodes, siehe Systeme-Sektion) - NavigationRegion3D - - Boden (MeshInstance3D, 100x100m PlaneMesh) + - Boden (MeshInstance3D, 100x100m PlaneMesh) - Kollision (StaticBody3D, WorldBoundaryShape3D) - Licht (DirectionalLight3D, 45°, Schatten) - Taverne (StaticBody3D, BoxMesh, Mitte der Karte) - - Gruppe (tavern) - - HitArea (Area3D) — nimmt Invasions-Schaden entgegen - - Healthbar (Sprite3D + SubViewport, groß, nur Health — kein Schild) - - Registriert bei Stats mit TavernStats Resource + - Gruppe (tavern) + - HitArea (Area3D) — nimmt Invasions-Schaden entgegen + - Healthbar (Sprite3D + SubViewport, groß, nur Health — kein Schild) + - Registriert bei Stats mit TavernStats Resource - Spieler (Instanz von player.tscn) - HUD (Instanz von hud.tscn) - PortalSpawner (Node, portal_spawner.gd) @@ -272,7 +272,7 @@ autoloads/ — Globaler Zustand - Kollision (CapsuleShape3D, 1.8m x 0.3m) - Mesh (CapsuleMesh) - CameraPivot (Node3D, camera.gd) - - Camera3D + - Camera3D - Movement (Node, movement.gd) — WASD + Springen, liest Werte von Stats - Ability (Node, ability.gd) — Input-Handler 1/2/3/4, emittiert ability_use_requested - Role (Node, role/role.gd) — Rollenwechsel ALT+1/2/3, emittiert role_changed (auch bei _ready) @@ -291,25 +291,25 @@ autoloads/ — Globaler Zustand - NavigationAgent3D - Bewegung wird vom AI-System (ai_system.gd) gesteuert — Enemy hat keinen eigenen Movement-Node - Healthbar (Sprite3D + SubViewport, healthbar.gd) — Health-Anzeige - - HealthbarShield (Node, healthbar_shield.gd) — Shield-Anzeige - - HealthbarStatus (Node, healthbar_status.gd) — Target-Border + Aggro-Farbe - - HealthbarEffects (Node, healthbar_effects.gd) — Effekt-Icons + - HealthbarShield (Node, healthbar_shield.gd) — Shield-Anzeige + - HealthbarStatus (Node, healthbar_status.gd) — Target-Border + Aggro-Farbe + - HealthbarEffects (Node, healthbar_effects.gd) — Effekt-Icons - init.gd — Registriert bei Stats mit EnemyStats Resource, Detection-Area Signal - Aggro-Regeln (Werte in AggroConfig Resource): - Aufbau: - - Schaden = Aggro (1:1), Tank 2x Multiplikator - - Heilung = 0.5x Aggro auf alle Gegner die Heiler kennen - - Aggro-Spread: 50% des Aggro an Gegner im alert_radius (10m) - - Detection-Area (10m): +1 Aggro, Alert an Nachbarn im alert_radius + - Schaden = Aggro (1:1), Tank 2x Multiplikator + - Heilung = 0.5x Aggro auf alle Gegner die Heiler kennen + - Aggro-Spread: 50% des Aggro an Gegner im alert_radius (10m) + - Detection-Area (10m): +1 Aggro, Alert an Nachbarn im alert_radius - Kampfstatus: - - Spieler in DetectionArea → immer im Kampf (kein Decay) - - Spieler verlässt DetectionArea → 5s Combat-Timeout, dann Decay - - Schaden verursachen setzt Combat-Timer zurück + - Spieler in DetectionArea → immer im Kampf (kein Decay) + - Spieler verlässt DetectionArea → 5s Combat-Timeout, dann Decay + - Schaden verursachen setzt Combat-Timer zurück - Abbau (nach Combat-Timeout): - - Basis: -aggro_decay/s (default 1.0) - - Exponentieller Decay basierend auf Zeit seit Kampfende (1%·2^sekunden) - - Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert - - Bei Spieler-Tod → Aggro auf 0 + - Basis: -aggro_decay/s (default 1.0) + - Exponentieller Decay basierend auf Zeit seit Kampfende (1%·2^sekunden) + - Ohne Aggro: Gegner kehrt zum Portal zurück, regeneriert + - Bei Spieler-Tod → Aggro auf 0 ## Boss (enemy/) - boss.tscn — wie enemy.tscn aber größer (Mesh lila, 1.5x) @@ -324,9 +324,9 @@ autoloads/ — Globaler Zustand - HitArea (Area3D) - DetectionArea (Area3D, Auto-Targeting bei Betreten) - Healthbar (Sprite3D + SubViewport, healthbar.gd) - - HealthbarShield (Node, healthbar_shield.gd) - - HealthbarStatus (Node, healthbar_status.gd) - - HealthbarEffects (Node, healthbar_effects.gd) + - HealthbarShield (Node, healthbar_shield.gd) + - HealthbarStatus (Node, healthbar_status.gd) + - HealthbarEffects (Node, healthbar_effects.gd) - init.gd — Registriert bei Stats mit PortalStats Resource - Spawnt Gegner bei HP-Schwellen (→ SpawnSystem) diff --git a/project.godot b/project.godot index 382b8fd..b475f1c 100644 --- a/project.godot +++ b/project.godot @@ -18,12 +18,10 @@ config/icon="res://icon.svg" [autoload] EventBus="*res://autoloads/event_bus.gd" +Net="*res://autoloads/net.gd" +Stats="*res://autoloads/stats.gd" GameState="*res://autoloads/game_state.gd" -PlayerData="*res://autoloads/player_stats.gd" -EnemyData="*res://autoloads/enemy_stats.gd" -BossData="*res://autoloads/boss_stats.gd" -PortalData="*res://autoloads/portal_stats.gd" -TavernData="*res://autoloads/tavern_stats.gd" +SaveLoad="*res://autoloads/save_load.gd" [dotnet] @@ -51,51 +49,109 @@ move_right={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) ] } +jump={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +] +} ability_1={ -"deadzone": 0.2, +"deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":49,"location":0,"echo":false,"script":null) ] } ability_2={ -"deadzone": 0.2, +"deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":50,"location":0,"echo":false,"script":null) ] } ability_3={ -"deadzone": 0.2, +"deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":51,"location":0,"echo":false,"script":null) ] } ability_4={ -"deadzone": 0.2, +"deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":52,"key_label":0,"unicode":52,"location":0,"echo":false,"script":null) ] } -ability_5={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":53,"key_label":0,"unicode":53,"location":0,"echo":false,"script":null) -] -} target_next={ -"deadzone": 0.2, +"deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } class_tank={ -"deadzone": 0.2, +"deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":49,"location":0,"echo":false,"script":null) ] } class_damage={ -"deadzone": 0.2, +"deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":50,"location":0,"echo":false,"script":null) ] } class_healer={ -"deadzone": 0.2, +"deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":51,"location":0,"echo":false,"script":null) ] } +interact={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +] +} +inventory={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":105,"location":0,"echo":false,"script":null) +] +} +crafting={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null) +] +} +build_mode={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":66,"key_label":0,"unicode":98,"location":0,"echo":false,"script":null) +] +} +chat={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":89,"key_label":0,"unicode":121,"location":0,"echo":false,"script":null) +] +} +map={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":77,"key_label":0,"unicode":109,"location":0,"echo":false,"script":null) +] +} +pause={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +rotate_build={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null) +] +} +click_select={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) +] +} +camera_drag={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null) +] +} + +[layer_names] + +3d_physics/layer_1="World" +3d_physics/layer_2="Player" +3d_physics/layer_3="Enemy" +3d_physics/layer_4="Hitbox" +3d_physics/layer_5="Building" [physics] diff --git a/resources/abilities/ability.gd b/resources/abilities/ability.gd new file mode 100644 index 0000000..e9ad6f5 --- /dev/null +++ b/resources/abilities/ability.gd @@ -0,0 +1,20 @@ +class_name Ability +extends Resource + +enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE } + +@export var ability_name: StringName = &"" +@export var type: Type = Type.SINGLE +@export var damage: float = 0.0 +@export var ability_range: float = 5.0 +@export var cooldown: float = 2.0 +@export var uses_gcd: bool = true +@export var aoe_radius: float = 0.0 +@export var is_heal: bool = false +@export var shield_value: float = 0.0 +@export var shield_multiplier: float = 0.0 +@export var passive_stat: StringName = &"" +@export var passive_value: float = 0.5 +@export var passive_radius: float = 50.0 +@export var element: int = Element.NONE +@export var icon: Texture2D diff --git a/resources/abilities/ability.gd.uid b/resources/abilities/ability.gd.uid new file mode 100644 index 0000000..f499d43 --- /dev/null +++ b/resources/abilities/ability.gd.uid @@ -0,0 +1 @@ +uid://ma4hkxsfia27 diff --git a/scenes/player/role/ability_set.gd b/resources/abilities/ability_set.gd similarity index 57% rename from scenes/player/role/ability_set.gd rename to resources/abilities/ability_set.gd index 1e16b78..d7c28df 100644 --- a/scenes/player/role/ability_set.gd +++ b/resources/abilities/ability_set.gd @@ -1,7 +1,9 @@ -extends Resource class_name AbilitySet +extends Resource -@export var abilities: Array[Ability] = [] -@export var aa_damage: float = 10.0 +@export var role_name: StringName = &"" +@export var aa_damage: float = 5.0 @export var aa_range: float = 10.0 @export var aa_is_heal: bool = false +@export var abilities: Array[Ability] = [] +@export var color: Color = Color.WHITE diff --git a/resources/abilities/ability_set.gd.uid b/resources/abilities/ability_set.gd.uid new file mode 100644 index 0000000..4154069 --- /dev/null +++ b/resources/abilities/ability_set.gd.uid @@ -0,0 +1 @@ +uid://bn4hk5bimojsi diff --git a/resources/buildings/building.gd b/resources/buildings/building.gd new file mode 100644 index 0000000..ccd24ad --- /dev/null +++ b/resources/buildings/building.gd @@ -0,0 +1,13 @@ +class_name Building +extends Resource + +enum Type { FLOOR, WALL, DOOR, ROOF } + +@export var building_id: StringName = &"" +@export var display_name: String = "" +@export var type: Type = Type.WALL +@export var size: Vector3 = Vector3(1, 1, 1) +@export var color: Color = Color.GRAY +@export var icon: Texture2D +@export var material_item: Item +@export var material_cost: int = 1 diff --git a/resources/buildings/building.gd.uid b/resources/buildings/building.gd.uid new file mode 100644 index 0000000..c3d9dd4 --- /dev/null +++ b/resources/buildings/building.gd.uid @@ -0,0 +1 @@ +uid://cb61cxd4li4ck diff --git a/resources/effects/effect.gd b/resources/effects/effect.gd new file mode 100644 index 0000000..e8898cf --- /dev/null +++ b/resources/effects/effect.gd @@ -0,0 +1,15 @@ +class_name Effect +extends Resource + +enum Type { BUFF, DEBUFF, AURA, DOT, HOT } + +@export var effect_name: StringName = &"" +@export var type: Type = Type.BUFF +@export var stat: StringName = &"" +@export var value: float = 0.0 +@export var is_multiplier: bool = false +@export var duration: float = -1.0 +@export var aura_radius: float = 0.0 +@export var tick_interval: float = 0.0 +@export var element: int = Element.NONE +@export var icon: Texture2D diff --git a/resources/effects/effect.gd.uid b/resources/effects/effect.gd.uid new file mode 100644 index 0000000..0e55c59 --- /dev/null +++ b/resources/effects/effect.gd.uid @@ -0,0 +1 @@ +uid://dvgpetv33u1a8 diff --git a/resources/effects/element.gd b/resources/effects/element.gd new file mode 100644 index 0000000..869db23 --- /dev/null +++ b/resources/effects/element.gd @@ -0,0 +1,19 @@ +class_name Element +extends RefCounted + +const NONE: int = 0 +const FIRE: int = 1 + +static func name_of(e: int) -> String: + match e: + FIRE: + return "Fire" + _: + return "None" + +static func color_of(e: int) -> Color: + match e: + FIRE: + return Color(1.0, 0.4, 0.1) + _: + return Color.WHITE diff --git a/resources/effects/element.gd.uid b/resources/effects/element.gd.uid new file mode 100644 index 0000000..0f8eaf2 --- /dev/null +++ b/resources/effects/element.gd.uid @@ -0,0 +1 @@ +uid://duxxvs1ild4we diff --git a/resources/items/item.gd b/resources/items/item.gd new file mode 100644 index 0000000..d9a48f9 --- /dev/null +++ b/resources/items/item.gd @@ -0,0 +1,12 @@ +class_name Item +extends Resource + +enum Category { ESSENCE, MATERIAL, EQUIPMENT, CONSUMABLE } + +@export var item_id: StringName = &"" +@export var display_name: String = "" +@export var category: Category = Category.MATERIAL +@export var max_stack: int = 99 +@export var icon: Texture2D +@export var equip_slot: StringName = &"" +@export var stat_bonus: Dictionary = {} diff --git a/resources/items/item.gd.uid b/resources/items/item.gd.uid new file mode 100644 index 0000000..4d5a06b --- /dev/null +++ b/resources/items/item.gd.uid @@ -0,0 +1 @@ +uid://cp3sxpqnv31o2 diff --git a/resources/items/recipe.gd b/resources/items/recipe.gd new file mode 100644 index 0000000..7486e08 --- /dev/null +++ b/resources/items/recipe.gd @@ -0,0 +1,9 @@ +class_name Recipe +extends Resource + +@export var recipe_id: StringName = &"" +@export var display_name: String = "" +@export var output_item: Item +@export var output_count: int = 1 +@export var inputs: Array[Item] = [] +@export var input_counts: Array[int] = [] diff --git a/resources/items/recipe.gd.uid b/resources/items/recipe.gd.uid new file mode 100644 index 0000000..4695a44 --- /dev/null +++ b/resources/items/recipe.gd.uid @@ -0,0 +1 @@ +uid://ddntj0p320vkq diff --git a/resources/npcs/npc_profile.gd b/resources/npcs/npc_profile.gd new file mode 100644 index 0000000..5a987ab --- /dev/null +++ b/resources/npcs/npc_profile.gd @@ -0,0 +1,10 @@ +class_name NpcProfile +extends Resource + +@export var npc_id: StringName = &"" +@export var display_name: String = "" +@export_multiline var lore: String = "" +@export_multiline var personality: String = "" +@export_multiline var fallback_text: String = "..." +@export var color: Color = Color(0.8, 0.7, 0.5) +@export var greeting: String = "Hallo Reisender." diff --git a/resources/npcs/npc_profile.gd.uid b/resources/npcs/npc_profile.gd.uid new file mode 100644 index 0000000..37b767c --- /dev/null +++ b/resources/npcs/npc_profile.gd.uid @@ -0,0 +1 @@ +uid://bcqtqp3jfkfm1 diff --git a/resources/stats/base_stats.gd b/resources/stats/base_stats.gd new file mode 100644 index 0000000..dafadb1 --- /dev/null +++ b/resources/stats/base_stats.gd @@ -0,0 +1,8 @@ +class_name BaseStats +extends Resource + +@export var max_health: float = 100.0 +@export var health_regen: float = 0.0 +@export var max_shield: float = 0.0 +@export var shield_regen: float = 0.0 +@export var shield_regen_delay: float = 5.0 diff --git a/resources/stats/base_stats.gd.uid b/resources/stats/base_stats.gd.uid new file mode 100644 index 0000000..092f156 --- /dev/null +++ b/resources/stats/base_stats.gd.uid @@ -0,0 +1 @@ +uid://rotp80banlee diff --git a/scenes/enemy/boss_stats.gd b/resources/stats/boss_stats.gd similarity index 50% rename from scenes/enemy/boss_stats.gd rename to resources/stats/boss_stats.gd index b7c8a45..5dfa9e1 100644 --- a/scenes/enemy/boss_stats.gd +++ b/resources/stats/boss_stats.gd @@ -1,2 +1,4 @@ -extends EnemyStats class_name BossStats +extends EnemyStats + +@export var enrage_time: float = 60.0 diff --git a/resources/stats/boss_stats.gd.uid b/resources/stats/boss_stats.gd.uid new file mode 100644 index 0000000..c98763b --- /dev/null +++ b/resources/stats/boss_stats.gd.uid @@ -0,0 +1 @@ +uid://cdyoffdfaor5m diff --git a/resources/stats/building_stats.gd b/resources/stats/building_stats.gd new file mode 100644 index 0000000..eb941ae --- /dev/null +++ b/resources/stats/building_stats.gd @@ -0,0 +1,2 @@ +class_name BuildingStats +extends BaseStats diff --git a/resources/stats/building_stats.gd.uid b/resources/stats/building_stats.gd.uid new file mode 100644 index 0000000..bffcd57 --- /dev/null +++ b/resources/stats/building_stats.gd.uid @@ -0,0 +1 @@ +uid://le02tb117uh diff --git a/resources/stats/enemy_stats.gd b/resources/stats/enemy_stats.gd new file mode 100644 index 0000000..75b6de3 --- /dev/null +++ b/resources/stats/enemy_stats.gd @@ -0,0 +1,11 @@ +class_name EnemyStats +extends BaseStats + +@export var speed: float = 3.0 +@export var attack_range: float = 2.0 +@export var attack_damage: float = 5.0 +@export var attack_cooldown: float = 1.5 +@export var aggro_radius: float = 12.0 +@export var leash_radius: float = 25.0 +@export var xp_value: float = 5.0 +@export var loot_chance: float = 1.0 diff --git a/resources/stats/enemy_stats.gd.uid b/resources/stats/enemy_stats.gd.uid new file mode 100644 index 0000000..c21e45e --- /dev/null +++ b/resources/stats/enemy_stats.gd.uid @@ -0,0 +1 @@ +uid://bch6113rtsk05 diff --git a/resources/stats/gate_stats.gd b/resources/stats/gate_stats.gd new file mode 100644 index 0000000..648db2b --- /dev/null +++ b/resources/stats/gate_stats.gd @@ -0,0 +1,7 @@ +class_name GateStats +extends BaseStats + +@export var spawn_count: int = 5 +@export var spawn_interval: float = 4.0 +@export var aggro_radius: float = 15.0 +@export var is_red: bool = false diff --git a/resources/stats/gate_stats.gd.uid b/resources/stats/gate_stats.gd.uid new file mode 100644 index 0000000..687de4b --- /dev/null +++ b/resources/stats/gate_stats.gd.uid @@ -0,0 +1 @@ +uid://cyroet7ce02ew diff --git a/resources/stats/player_stats.gd b/resources/stats/player_stats.gd new file mode 100644 index 0000000..9c2fcb6 --- /dev/null +++ b/resources/stats/player_stats.gd @@ -0,0 +1,18 @@ +class_name PlayerStats +extends BaseStats + +@export var speed: float = 5.0 +@export var jump_velocity: float = 4.5 +@export var target_range: float = 20.0 +@export var combat_timeout: float = 5.0 +@export var respawn_time: float = 3.0 +@export var gcd_time: float = 0.5 +@export var aa_cooldown: float = 0.5 + +@export var level: int = 1 +@export var xp: float = 0.0 +@export var xp_to_next: float = 50.0 + +@export var buff_damage: float = 1.0 +@export var buff_heal: float = 1.0 +@export var buff_shield: float = 1.0 diff --git a/resources/stats/player_stats.gd.uid b/resources/stats/player_stats.gd.uid new file mode 100644 index 0000000..abf6c75 --- /dev/null +++ b/resources/stats/player_stats.gd.uid @@ -0,0 +1 @@ +uid://c42fif5nbdvur diff --git a/resources/stats/portal_stats.gd b/resources/stats/portal_stats.gd new file mode 100644 index 0000000..9e2ef34 --- /dev/null +++ b/resources/stats/portal_stats.gd @@ -0,0 +1,5 @@ +class_name PortalStats +extends BaseStats + +@export var lifetime: float = 0.0 +@export var is_red: bool = false diff --git a/resources/stats/portal_stats.gd.uid b/resources/stats/portal_stats.gd.uid new file mode 100644 index 0000000..0601ac4 --- /dev/null +++ b/resources/stats/portal_stats.gd.uid @@ -0,0 +1 @@ +uid://beg4u508o3lcv diff --git a/resources/stats/village_stats.gd b/resources/stats/village_stats.gd new file mode 100644 index 0000000..2e694e2 --- /dev/null +++ b/resources/stats/village_stats.gd @@ -0,0 +1,2 @@ +class_name VillageStats +extends BaseStats diff --git a/resources/stats/village_stats.gd.uid b/resources/stats/village_stats.gd.uid new file mode 100644 index 0000000..6318bbe --- /dev/null +++ b/resources/stats/village_stats.gd.uid @@ -0,0 +1 @@ +uid://dpyr5lcc45eub diff --git a/scenes/dungeon/dungeon.tscn b/scenes/dungeon/dungeon.tscn index 72a7a50..a439730 100644 --- a/scenes/dungeon/dungeon.tscn +++ b/scenes/dungeon/dungeon.tscn @@ -1,266 +1,133 @@ -[gd_scene format=3] +[gd_scene load_steps=23 format=3 uid="uid://b0dungeon0001"] -[ext_resource type="PackedScene" path="res://scenes/player/player.tscn" id="player"] -[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] -[ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"] -[ext_resource type="Resource" path="res://scenes/enemy/boss_stats.tres" id="boss_stats"] -[ext_resource type="Script" path="res://systems/dungeon_system.gd" id="dungeon_system"] -[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="dungeon_manager"] -[ext_resource type="Script" path="res://systems/audio_system.gd" id="audio_system"] -[ext_resource type="Script" path="res://systems/xp_system.gd" id="xp_system"] -[ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"] -[ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"] -[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"] -[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"] -[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"] -[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"] -[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"] -[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"] -[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"] -[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"] -[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"] -[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"] -[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"] -[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"] -[ext_resource type="Script" path="res://systems/ai_system.gd" id="ai_system"] -[ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"] -[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"] -[ext_resource type="Script" path="res://systems/aura_system.gd" id="aura_system"] -[ext_resource type="Script" path="res://systems/buff_system.gd" id="buff_system"] -[ext_resource type="Script" path="res://systems/debuff_system.gd" id="debuff_system"] -[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"] -[ext_resource type="Script" path="res://systems/hud_system.gd" id="hud_system"] -[ext_resource type="Script" path="res://systems/nameplate_system.gd" id="nameplate_system"] -[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"] -[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"] -[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"] +[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="1"] +[ext_resource type="PackedScene" uid="uid://b0player00001" path="res://scenes/entities/player/player.tscn" id="2"] +[ext_resource type="Script" path="res://systems/health_system.gd" id="4"] +[ext_resource type="Script" path="res://systems/shield_system.gd" id="5"] +[ext_resource type="Script" path="res://systems/respawn_system.gd" id="6"] +[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="7"] +[ext_resource type="Script" path="res://systems/role_system.gd" id="8"] +[ext_resource type="Script" path="res://systems/effect_system.gd" id="9"] +[ext_resource type="Script" path="res://systems/element_system.gd" id="10"] +[ext_resource type="Script" path="res://systems/aggro_system.gd" id="11"] +[ext_resource type="Script" path="res://systems/combat/ability_system.gd" id="12"] +[ext_resource type="Script" path="res://systems/combat/auto_attack_system.gd" id="13"] +[ext_resource type="Script" path="res://systems/spawn_system.gd" id="14"] +[ext_resource type="Script" path="res://systems/xp_system.gd" id="17"] +[ext_resource type="Script" path="res://systems/loot_system.gd" id="18"] +[ext_resource type="Script" path="res://systems/inventory_system.gd" id="19"] +[ext_resource type="Script" path="res://systems/chat_system.gd" id="24"] +[ext_resource type="Script" path="res://systems/map_system.gd" id="25"] +[ext_resource type="PackedScene" uid="uid://b0hud00001" path="res://scenes/hud/hud.tscn" id="26"] +[ext_resource type="Script" path="res://systems/audio_system.gd" id="27"] -[sub_resource type="NavigationMesh" id="NavigationMesh_1"] -vertices = PackedVector3Array(-7.0, 0.5, -7.0, -7.0, 0.5, 87.0, 7.0, 0.5, 87.0, 7.0, 0.5, -7.0) -polygons = [PackedInt32Array(3, 2, 0), PackedInt32Array(0, 2, 1)] +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_1"] +sky_top_color = Color(0.1, 0.05, 0.1, 1) +sky_horizon_color = Color(0.15, 0.05, 0.05, 1) +ground_horizon_color = Color(0.1, 0.05, 0.05, 1) +ground_bottom_color = Color(0.0, 0.0, 0.0, 1) -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"] -albedo_color = Color(0.2, 0.18, 0.15, 1) +[sub_resource type="Sky" id="Sky_1"] +sky_material = SubResource("ProceduralSkyMaterial_1") -[sub_resource type="PlaneMesh" id="PlaneMesh_1"] -material = SubResource("StandardMaterial3D_floor") -size = Vector2(15, 90) - -[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_1"] - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wall"] -albedo_color = Color(0.25, 0.22, 0.2, 1) - -[sub_resource type="BoxMesh" id="BoxMesh_north_south"] -material = SubResource("StandardMaterial3D_wall") -size = Vector3(15, 3, 0.5) - -[sub_resource type="BoxShape3D" id="BoxShape3D_north_south"] -size = Vector3(15, 3, 0.5) - -[sub_resource type="BoxMesh" id="BoxMesh_east_west"] -material = SubResource("StandardMaterial3D_wall") -size = Vector3(0.5, 3, 90) - -[sub_resource type="BoxShape3D" id="BoxShape3D_east_west"] -size = Vector3(0.5, 3, 90) +[sub_resource type="Environment" id="Environment_1"] +background_mode = 2 +sky = SubResource("Sky_1") +ambient_light_source = 3 +ambient_light_color = Color(0.2, 0.18, 0.2, 1) +ambient_light_energy = 0.4 [node name="Dungeon" type="Node3D"] +script = ExtResource("1") + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_1") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 30, 0) +light_energy = 0.7 + +[node name="DungeonGeometry" type="Node3D" parent="."] [node name="Systems" type="Node" parent="."] [node name="HealthSystem" type="Node" parent="Systems"] -script = ExtResource("health_system") - -[node name="DamageSystem" type="Node" parent="Systems"] -script = ExtResource("damage_system") - -[node name="HealSystem" type="Node" parent="Systems"] -script = ExtResource("heal_system") +script = ExtResource("4") [node name="ShieldSystem" type="Node" parent="Systems"] -script = ExtResource("shield_system") - -[node name="RoleSystem" type="Node" parent="Systems"] -script = ExtResource("role_system") -tank_set = ExtResource("tank_set") -damage_set = ExtResource("damage_set") -healer_set = ExtResource("healer_set") - -[node name="AbilitySystem" type="Node" parent="Systems"] -script = ExtResource("ability_system") - -[node name="AttackSystem" type="Node" parent="Systems"] -script = ExtResource("attack_system") - -[node name="CooldownSystem" type="Node" parent="Systems"] -script = ExtResource("cooldown_system") - -[node name="TargetingSystem" type="Node" parent="Systems"] -script = ExtResource("targeting_system") - -[node name="AggroSystem" type="Node" parent="Systems"] -script = ExtResource("aggro_system") - -[node name="AggroTracker" type="Node" parent="Systems/AggroSystem"] -script = ExtResource("aggro_tracker") - -[node name="AggroDecay" type="Node" parent="Systems/AggroSystem"] -script = ExtResource("aggro_decay") - -[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"] -script = ExtResource("aggro_events") - -[node name="AISystem" type="Node" parent="Systems"] -script = ExtResource("ai_system") +script = ExtResource("5") [node name="RespawnSystem" type="Node" parent="Systems"] -script = ExtResource("respawn_system") +script = ExtResource("6") -[node name="SpawnSystem" type="Node" parent="Systems"] -script = ExtResource("spawn_system") +[node name="CooldownSystem" type="Node" parent="Systems"] +script = ExtResource("7") -[node name="AuraSystem" type="Node" parent="Systems"] -script = ExtResource("aura_system") +[node name="RoleSystem" type="Node" parent="Systems"] +script = ExtResource("8") -[node name="BuffSystem" type="Node" parent="Systems"] -script = ExtResource("buff_system") - -[node name="DebuffSystem" type="Node" parent="Systems"] -script = ExtResource("debuff_system") +[node name="EffectSystem" type="Node" parent="Systems"] +script = ExtResource("9") [node name="ElementSystem" type="Node" parent="Systems"] -script = ExtResource("element_system") +script = ExtResource("10") -[node name="HudSystem" type="Node" parent="Systems"] -script = ExtResource("hud_system") +[node name="AggroSystem" type="Node" parent="Systems"] +script = ExtResource("11") -[node name="NameplateSystem" type="Node" parent="Systems"] -script = ExtResource("nameplate_system") +[node name="AbilitySystem" type="Node" parent="Systems"] +script = ExtResource("12") -[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] -navigation_mesh = SubResource("NavigationMesh_1") +[node name="AutoAttackSystem" type="Node" parent="Systems"] +script = ExtResource("13") -[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 40) -mesh = SubResource("PlaneMesh_1") +[node name="SpawnSystem" type="Node" parent="Systems"] +script = ExtResource("14") -[node name="BodenCollision" type="StaticBody3D" parent="."] - -[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"] -shape = SubResource("WorldBoundaryShape3D_1") - -[node name="WallSouth" type="StaticBody3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -5.25) - -[node name="Mesh" type="MeshInstance3D" parent="WallSouth"] -mesh = SubResource("BoxMesh_north_south") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="WallSouth"] -shape = SubResource("BoxShape3D_north_south") - -[node name="WallNorth" type="StaticBody3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 85.25) - -[node name="Mesh" type="MeshInstance3D" parent="WallNorth"] -mesh = SubResource("BoxMesh_north_south") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="WallNorth"] -shape = SubResource("BoxShape3D_north_south") - -[node name="WallEast" type="StaticBody3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.75, 1.5, 40) - -[node name="Mesh" type="MeshInstance3D" parent="WallEast"] -mesh = SubResource("BoxMesh_east_west") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="WallEast"] -shape = SubResource("BoxShape3D_east_west") - -[node name="WallWest" type="StaticBody3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.75, 1.5, 40) - -[node name="Mesh" type="MeshInstance3D" parent="WallWest"] -mesh = SubResource("BoxMesh_east_west") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="WallWest"] -shape = SubResource("BoxShape3D_east_west") - -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 40) -light_energy = 0.6 -shadow_enabled = true - -[node name="Player" parent="." instance=ExtResource("player")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -3) - -[node name="HUD" parent="." instance=ExtResource("hud")] - -[node name="Enemy1a" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 15) - -[node name="Enemy1b" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 15) - -[node name="Enemy1c" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 15) - -[node name="Enemy1d" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 15) - -[node name="Enemy2a" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 30) - -[node name="Enemy2b" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 30) - -[node name="Enemy2c" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 30) - -[node name="Enemy2d" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 30) - -[node name="Enemy3a" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 45) - -[node name="Enemy3b" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 45) - -[node name="Enemy3c" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 45) - -[node name="Enemy3d" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 45) - -[node name="Enemy4a" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 60) - -[node name="Enemy4b" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 60) - -[node name="Enemy4c" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 60) - -[node name="Enemy4d" parent="." instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60) - -[node name="Boss" parent="." groups=["boss"] instance=ExtResource("enemy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 75) -stats = ExtResource("boss_stats") - -[node name="ExitGate" parent="." instance=ExtResource("gate")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4) -target_scene = "res://scenes/world/world.tscn" -is_exit = true - -[node name="DungeonSystem" type="Node" parent="Systems"] -script = ExtResource("dungeon_system") - -[node name="AudioSystem" type="Node" parent="Systems"] -script = ExtResource("audio_system") +[node name="LootSystem" type="Node" parent="Systems"] +script = ExtResource("18") [node name="XpSystem" type="Node" parent="Systems"] -script = ExtResource("xp_system") +script = ExtResource("17") -[node name="DungeonManager" type="Node" parent="."] -script = ExtResource("dungeon_manager") +[node name="InventorySystem" type="Node" parent="Systems"] +script = ExtResource("19") + +[node name="ChatSystem" type="Node" parent="Systems"] +script = ExtResource("24") + +[node name="MapSystem" type="Node" parent="Systems"] +script = ExtResource("25") + +[node name="AudioSystem" type="Node" parent="Systems"] +script = ExtResource("27") + +[node name="EntityRoot" type="Node3D" parent="."] + +[node name="Players" type="Node3D" parent="EntityRoot"] + +[node name="Enemies" type="Node3D" parent="EntityRoot"] + +[node name="Loot" type="Node3D" parent="EntityRoot"] + +[node name="Portals" type="Node3D" parent="EntityRoot"] + +[node name="Buildings" type="Node3D" parent="EntityRoot"] + +[node name="Gates" type="Node3D" parent="EntityRoot"] + +[node name="Npcs" type="Node3D" parent="EntityRoot"] + +[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/player/player.tscn") +spawn_path = NodePath("../EntityRoot/Players") + +[node name="EnemySpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/enemy/enemy.tscn") +spawn_path = NodePath("../EntityRoot/Enemies") + +[node name="LootSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/loot/loot_drop.tscn") +spawn_path = NodePath("../EntityRoot/Loot") + +[node name="HUD" parent="." instance=ExtResource("26")] diff --git a/scenes/dungeon/dungeon_generator.gd b/scenes/dungeon/dungeon_generator.gd new file mode 100644 index 0000000..5d659da --- /dev/null +++ b/scenes/dungeon/dungeon_generator.gd @@ -0,0 +1,93 @@ +extends Node + +const ROOM_HEIGHT: float = 4.0 +const WALL_THICKNESS: float = 0.4 +const CORRIDOR_WIDTH: float = 4.0 + +var rng: RandomNumberGenerator +var rooms: Array = [] + +func generate(parent: Node3D, seed: int, scale_difficulty: float = 1.0) -> Dictionary: + rng = RandomNumberGenerator.new() + rng.seed = seed + rooms.clear() + var room_count: int = rng.randi_range(5, 8) + var pos := Vector3(0, 0, 0) + var dir := Vector3(0, 0, -1) + for i in range(room_count): + var w: float = rng.randf_range(8.0, 14.0) + var d: float = rng.randf_range(8.0, 14.0) + rooms.append({"pos": pos, "size": Vector3(w, ROOM_HEIGHT, d)}) + if i == room_count - 1: + break + var corridor_len: float = rng.randf_range(4.0, 8.0) + var step: Vector3 = dir * (max(w, d) * 0.5 + corridor_len + 4.0) + pos += step + if rng.randf() < 0.5: + var rotate_left: bool = rng.randf() < 0.5 + dir = dir.rotated(Vector3.UP, PI * 0.5 * (1 if rotate_left else -1)) + _build_geometry(parent) + return {"rooms": rooms, "spawn": rooms[0].pos + Vector3(0, 1, 0), "boss": rooms[-1].pos} + +func _build_geometry(parent: Node3D) -> void: + for r in rooms: + _build_room(parent, r.pos, r.size) + for i in range(rooms.size() - 1): + _build_corridor(parent, rooms[i].pos, rooms[i + 1].pos) + +func _build_room(parent: Node3D, center: Vector3, size: Vector3) -> void: + _add_floor(parent, center, Vector2(size.x, size.z)) + var hw: float = size.x * 0.5 + var hd: float = size.z * 0.5 + _add_wall(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, -hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS)) + _add_wall(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS)) + _add_wall(parent, center + Vector3(-hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z)) + _add_wall(parent, center + Vector3(hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z)) + +func _build_corridor(parent: Node3D, from: Vector3, to: Vector3) -> void: + var mid: Vector3 = (from + to) * 0.5 + var d: float = from.distance_to(to) + var dir: Vector3 = (to - from).normalized() + _add_floor(parent, Vector3(mid.x, 0, mid.z), Vector2(d, CORRIDOR_WIDTH) if abs(dir.x) > abs(dir.z) else Vector2(CORRIDOR_WIDTH, d)) + +func _add_floor(parent: Node3D, center: Vector3, size: Vector2) -> void: + var body := StaticBody3D.new() + body.collision_layer = 1 + body.collision_mask = 0 + var mesh := MeshInstance3D.new() + var box := BoxMesh.new() + box.size = Vector3(size.x, 0.4, size.y) + mesh.mesh = box + var mat := StandardMaterial3D.new() + mat.albedo_color = Color(0.25, 0.22, 0.2) + mesh.material_override = mat + mesh.position = Vector3(0, -0.2, 0) + var col := CollisionShape3D.new() + var shape := BoxShape3D.new() + shape.size = Vector3(size.x, 0.4, size.y) + col.shape = shape + col.position = Vector3(0, -0.2, 0) + body.add_child(mesh) + body.add_child(col) + body.position = center + parent.add_child(body) + +func _add_wall(parent: Node3D, center: Vector3, size: Vector3) -> void: + var body := StaticBody3D.new() + body.collision_layer = 1 + body.collision_mask = 0 + var mesh := MeshInstance3D.new() + var box := BoxMesh.new() + box.size = size + mesh.mesh = box + var mat := StandardMaterial3D.new() + mat.albedo_color = Color(0.35, 0.32, 0.28) + mesh.material_override = mat + var col := CollisionShape3D.new() + var shape := BoxShape3D.new() + shape.size = size + col.shape = shape + body.add_child(mesh) + body.add_child(col) + body.position = center + parent.add_child(body) diff --git a/scenes/dungeon/dungeon_generator.gd.uid b/scenes/dungeon/dungeon_generator.gd.uid new file mode 100644 index 0000000..019da42 --- /dev/null +++ b/scenes/dungeon/dungeon_generator.gd.uid @@ -0,0 +1 @@ +uid://d1u4odursm3m4 diff --git a/scenes/dungeon/dungeon_manager.gd b/scenes/dungeon/dungeon_manager.gd index a7c01ba..ed1f66d 100644 --- a/scenes/dungeon/dungeon_manager.gd +++ b/scenes/dungeon/dungeon_manager.gd @@ -1,16 +1,82 @@ -extends Node +extends Node3D + +const PLAYER_SCENE: PackedScene = preload("res://scenes/entities/player/player.tscn") +const ENEMY_SCENE: PackedScene = preload("res://scenes/entities/enemy/enemy.tscn") +const GENERATOR: GDScript = preload("res://scenes/dungeon/dungeon_generator.gd") + +@onready var players_root: Node3D = $EntityRoot/Players +@onready var dungeon_root: Node3D = $DungeonGeometry +@onready var spawn_system: Node = $Systems/SpawnSystem + +var generator: Node +var data: Dictionary func _ready() -> void: - call_deferred("_scale_dungeon") + add_to_group("dungeon") + generator = GENERATOR.new() + add_child(generator) + data = generator.generate(dungeon_root, GameState.dungeon_seed, GameState.current_wave * (5.0 if GameState.dungeon_red else 1.0)) + EventBus.boss_defeated.connect(_on_boss_defeated) + Net.world_ready.connect(_on_world_ready) + Net.peer_world_loaded.connect(_on_peer_world_loaded) + if Net.is_host(): + multiplayer.peer_connected.connect(_on_peer_connected) + multiplayer.peer_disconnected.connect(_on_peer_disconnected) + Net.reset_world_ready() + Net.mark_world_loaded() -func _scale_dungeon() -> void: - var variant_multiplier: float = 10.0 if GameState.last_dungeon_variant == 1 else 1.0 - var total_scale: float = PlayerData.level_scale * variant_multiplier - var parent: Node = get_parent() - for child in parent.get_children(): - if not child.is_in_group("enemies"): - continue - if child.is_in_group("boss"): - BossData.apply_scale(child, total_scale) - else: - EnemyData.apply_scale(child, total_scale) +func _exit_tree() -> void: + if Net.world_ready.is_connected(_on_world_ready): + Net.world_ready.disconnect(_on_world_ready) + if Net.peer_world_loaded.is_connected(_on_peer_world_loaded): + Net.peer_world_loaded.disconnect(_on_peer_world_loaded) + +func _on_world_ready() -> void: + if not Net.is_host(): + return + _spawn_player(1) + for id in multiplayer.get_peers(): + _spawn_player(id) + _populate_dungeon() + +func _populate_dungeon() -> void: + var difficulty: float = GameState.current_wave * (3.0 if GameState.dungeon_red else 1.0) + for i in range(data.rooms.size() - 1): + var room: Dictionary = data.rooms[i] + var n: int = 2 + (1 if GameState.dungeon_red else 0) + for j in range(n): + var off := Vector3(randf_range(-room.size.x * 0.3, room.size.x * 0.3), 0.5, randf_range(-room.size.z * 0.3, room.size.z * 0.3)) + spawn_system.spawn_enemy_at(room.pos + off, GameState.dungeon_red, difficulty * 0.5) + spawn_system.spawn_boss_at(data.boss + Vector3(0, 0.5, 0), difficulty) + +func _spawn_player(peer_id: int) -> void: + if players_root.get_node_or_null(str(peer_id)) != null: + return + var p: CharacterBody3D = PLAYER_SCENE.instantiate() + p.name = str(peer_id) + players_root.add_child(p, true) + p.global_position = data.spawn + Vector3(randf_range(-1, 1), 0, randf_range(-1, 1)) + +func _on_peer_connected(id: int) -> void: + Net.tell_peer_to_load_scene(id, GameState.SCENE_DUNGEON) + +func _on_peer_world_loaded(peer_id: int) -> void: + if not Net.is_host(): + return + _spawn_player(peer_id) + +func _on_peer_disconnected(id: int) -> void: + var node := players_root.get_node_or_null(str(id)) + if node: + node.queue_free() + +func _on_boss_defeated(_b: Node) -> void: + if Net.is_host(): + var t := get_tree().create_timer(2.0) + t.timeout.connect(func(): + GameState.dungeon_seed = 0 + _return.rpc()) + +@rpc("authority", "reliable", "call_local") +func _return() -> void: + GameState.change_scene(GameState.SCENE_WORLD) diff --git a/scenes/dungeon/dungeon_manager.gd.uid b/scenes/dungeon/dungeon_manager.gd.uid index 799400b..6b8dfba 100644 --- a/scenes/dungeon/dungeon_manager.gd.uid +++ b/scenes/dungeon/dungeon_manager.gd.uid @@ -1 +1 @@ -uid://bfkxrflfn5qx4 +uid://civwsilci7nlx diff --git a/scenes/enemy/boss_stats.gd.uid b/scenes/enemy/boss_stats.gd.uid deleted file mode 100644 index 8592b7c..0000000 --- a/scenes/enemy/boss_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dlawq281oesnf diff --git a/scenes/enemy/boss_stats.tres b/scenes/enemy/boss_stats.tres deleted file mode 100644 index e15d7c7..0000000 --- a/scenes/enemy/boss_stats.tres +++ /dev/null @@ -1,20 +0,0 @@ -[gd_resource type="Resource" script_class="BossStats" load_steps=2 format=3] - -[ext_resource type="Script" path="res://scenes/enemy/boss_stats.gd" id="1"] - -[resource] -script = ExtResource("1") -max_health = 500.0 -health_regen = 0.0 -max_shield = 100.0 -shield_regen_delay = 5.0 -shield_regen_time = 8.0 -speed = 3.0 -attack_range = 2.0 -attack_cooldown = 1.5 -attack_damage = 5.0 -regen_fast = 0.1 -regen_slow = 0.01 -aggro_decay = 1.0 -portal_radius = 10.0 -alert_radius = 10.0 diff --git a/scenes/enemy/detection.gd b/scenes/enemy/detection.gd deleted file mode 100644 index 23efbde..0000000 --- a/scenes/enemy/detection.gd +++ /dev/null @@ -1,11 +0,0 @@ -extends Node - -@onready var entity: CharacterBody3D = get_parent() - -func _on_detection_area_body_entered(body: Node3D) -> void: - if body is CharacterBody3D and body.name == "Player": - EventBus.enemy_detected.emit(entity, body) - -func _on_detection_area_body_exited(body: Node3D) -> void: - if body is CharacterBody3D and body.name == "Player": - EventBus.enemy_lost.emit(entity, body) diff --git a/scenes/enemy/detection.gd.uid b/scenes/enemy/detection.gd.uid deleted file mode 100644 index 9236bb4..0000000 --- a/scenes/enemy/detection.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b07aajhufqvb3 diff --git a/scenes/enemy/enemy.tscn b/scenes/enemy/enemy.tscn deleted file mode 100644 index 1af9693..0000000 --- a/scenes/enemy/enemy.tscn +++ /dev/null @@ -1,96 +0,0 @@ -[gd_scene format=3 uid="uid://db8pa55ev4l4a"] - -[ext_resource type="Script" uid="uid://vy6hyqok0p8b" path="res://scenes/enemy/init.gd" id="1"] -[ext_resource type="Script" uid="uid://b07aajhufqvb3" path="res://scenes/enemy/detection.gd" id="2"] -[ext_resource type="Resource" uid="uid://cj1shmjwf0xeo" path="res://scenes/enemy/enemy_stats.tres" id="8"] -[ext_resource type="PackedScene" path="res://assets/models/characters/Skeleton_Minion.glb" id="9"] - -[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] -radius = 0.4 -height = 1.5 - -[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"] -radius = 0.4 -height = 1.5 - -[sub_resource type="SphereShape3D" id="SphereShape3D_1"] -radius = 10.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"] -bg_color = Color(0.3, 0.1, 0.1, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"] -bg_color = Color(0.2, 0.8, 0.2, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"] -bg_color = Color(0.1, 0.1, 0.3, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"] -bg_color = Color(0.2, 0.5, 0.9, 1) - -[node name="Enemy" type="CharacterBody3D" unique_id=1724620529] -script = ExtResource("1") -stats = ExtResource("8") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1011138038] -shape = SubResource("CapsuleShape3D_1") - -[node name="Mesh" type="Node3D" parent="." unique_id=1598094615] - -[node name="Model" parent="Mesh" instance=ExtResource("9")] -transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, -0.75, 0) - -[node name="HitArea" type="Area3D" parent="." unique_id=893463784] -collision_layer = 4 -collision_mask = 0 - -[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=984781962] -shape = SubResource("CapsuleShape3D_2") - -[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=440641945] - -[node name="Detection" type="Node" parent="." unique_id=534240144] -script = ExtResource("2") - -[node name="DetectionArea" type="Area3D" parent="." unique_id=1955178598] -collision_layer = 0 - -[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea" unique_id=557461347] -shape = SubResource("SphereShape3D_1") - -[node name="Healthbar" type="Sprite3D" parent="." unique_id=1008728031] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0) -billboard = 1 - -[node name="SubViewport" type="SubViewport" parent="Healthbar" unique_id=1219060718] -transparent_bg = true -size = Vector2i(104, 29) - -[node name="Border" type="ColorRect" parent="Healthbar/SubViewport" unique_id=848146848] -offset_right = 104.0 -offset_bottom = 29.0 -color = Color(1, 0.9, 0.2, 1) - -[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1206434403] -offset_left = 2.0 -offset_top = 2.0 -offset_right = 102.0 -offset_bottom = 12.0 -theme_override_styles/background = SubResource("StyleBoxFlat_health_bg") -theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill") -value = 100.0 -show_percentage = false - -[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1891108036] -offset_left = 2.0 -offset_top = 15.0 -offset_right = 102.0 -offset_bottom = 27.0 -theme_override_styles/background = SubResource("StyleBoxFlat_shield_bg") -theme_override_styles/fill = SubResource("StyleBoxFlat_shield_fill") -max_value = 50.0 -value = 50.0 -show_percentage = false - -[connection signal="body_entered" from="DetectionArea" to="Detection" method="_on_detection_area_body_entered"] -[connection signal="body_exited" from="DetectionArea" to="Detection" method="_on_detection_area_body_exited"] diff --git a/scenes/enemy/enemy_stats.gd b/scenes/enemy/enemy_stats.gd deleted file mode 100644 index 439cf90..0000000 --- a/scenes/enemy/enemy_stats.gd +++ /dev/null @@ -1,12 +0,0 @@ -extends BaseStats -class_name EnemyStats - -@export var speed := 3.0 -@export var attack_range := 2.0 -@export var attack_cooldown := 1.5 -@export var attack_damage := 5.0 -@export var regen_fast := 0.10 -@export var regen_slow := 0.01 -@export var aggro_decay := 1.0 -@export var portal_radius := 10.0 -@export var alert_radius := 10.0 diff --git a/scenes/enemy/enemy_stats.gd.uid b/scenes/enemy/enemy_stats.gd.uid deleted file mode 100644 index d4cd96d..0000000 --- a/scenes/enemy/enemy_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bh2uuuvl30y0x diff --git a/scenes/enemy/enemy_stats.tres b/scenes/enemy/enemy_stats.tres deleted file mode 100644 index cd0eb0d..0000000 --- a/scenes/enemy/enemy_stats.tres +++ /dev/null @@ -1,7 +0,0 @@ -[gd_resource type="Resource" script_class="EnemyStats" format=3 uid="uid://cj1shmjwf0xeo"] - -[ext_resource type="Script" uid="uid://bh2uuuvl30y0x" path="res://scenes/enemy/enemy_stats.gd" id="1"] - -[resource] -script = ExtResource("1") -max_shield = 50.0 diff --git a/scenes/enemy/init.gd b/scenes/enemy/init.gd deleted file mode 100644 index 5c4a32c..0000000 --- a/scenes/enemy/init.gd +++ /dev/null @@ -1,101 +0,0 @@ -extends CharacterBody3D - -const SKELETON_WARRIOR: PackedScene = preload("res://assets/models/characters/Skeleton_Warrior.glb") -const SKELETON_MAGE: PackedScene = preload("res://assets/models/characters/Skeleton_Mage.glb") - -@export var stats: EnemyStats - -var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") -var spawn_scale: float = 1.0 -var anim_player: AnimationPlayer = null -var current_anim: String = "" -var attack_lock_until: float = 0.0 -var is_dying: bool = false - -func _ready() -> void: - add_to_group("enemies") - if is_in_group("boss"): - BossData.register(self, stats, spawn_scale) - BossData.set_stat(self, "spawn_position", global_position) - _swap_model(SKELETON_MAGE, 1.3) - else: - EnemyData.register(self, stats, spawn_scale) - EnemyData.set_stat(self, "spawn_position", global_position) - EventBus.entity_died.connect(_on_entity_died) - EventBus.attack_executed.connect(_on_attack_executed) - call_deferred("_check_variant") - call_deferred("_init_anim") - -func _init_anim() -> void: - anim_player = get_node_or_null("Mesh/Model/AnimationPlayer") - _play_anim("Idle") - -func _check_variant() -> void: - if is_in_group("boss"): - return - if is_in_group("red_enemies") or is_in_group("invasion"): - _swap_model(SKELETON_WARRIOR, 1.0) - anim_player = get_node_or_null("Mesh/Model/AnimationPlayer") - _play_anim("Idle") - -func _swap_model(new_scene: PackedScene, scale_factor: float = 1.0) -> void: - var mesh: Node3D = get_node_or_null("Mesh") - if not mesh: - return - var old: Node = mesh.get_node_or_null("Model") - if old: - old.queue_free() - var new_model: Node3D = new_scene.instantiate() - new_model.name = "Model" - new_model.scale = Vector3(scale_factor, scale_factor, scale_factor) - new_model.position = Vector3(0, -0.75, 0) - new_model.rotation.y = PI - mesh.add_child(new_model) - -func _exit_tree() -> void: - if is_in_group("boss"): - BossData.deregister(self) - else: - EnemyData.deregister(self) - -func _on_entity_died(entity: Node) -> void: - if entity != self: - return - is_dying = true - _play_anim("Death_A", false) - get_tree().create_timer(1.0).timeout.connect(queue_free) - -func _on_attack_executed(attacker: Node, _pos: Vector3, _dir: Vector3, _damage: float) -> void: - if attacker != self: - return - _play_anim("1H_Melee_Attack_Chop", false) - attack_lock_until = Time.get_ticks_msec() / 1000.0 + 0.5 - -func _process(_delta: float) -> void: - if is_dying: - return - var now: float = Time.get_ticks_msec() / 1000.0 - if now < attack_lock_until: - return - if velocity.length() > 0.1: - _play_anim("Running_A") - else: - _play_anim("Idle") - -func _play_anim(anim_name: String, loop: bool = true) -> void: - if not anim_player: - return - if current_anim == anim_name: - return - if not anim_player.has_animation(anim_name): - return - var anim: Animation = anim_player.get_animation(anim_name) - if anim: - anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE - anim_player.play(anim_name) - current_anim = anim_name - -func _physics_process(delta: float) -> void: - if not is_on_floor(): - velocity.y -= gravity * delta - move_and_slide() diff --git a/scenes/enemy/init.gd.uid b/scenes/enemy/init.gd.uid deleted file mode 100644 index 14f802f..0000000 --- a/scenes/enemy/init.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://vy6hyqok0p8b diff --git a/scenes/entities/building/building.gd b/scenes/entities/building/building.gd new file mode 100644 index 0000000..3c34f37 --- /dev/null +++ b/scenes/entities/building/building.gd @@ -0,0 +1,36 @@ +extends StaticBody3D + +@export var building_id: StringName = &"" + +@onready var mesh: MeshInstance3D = $Mesh +@onready var collision: CollisionShape3D = $Collision + +func _enter_tree() -> void: + set_multiplayer_authority(1) + +func _ready() -> void: + add_to_group("buildings") + apply_building(building_id) + +func apply_building(id: StringName) -> void: + building_id = id + var data: Building = _load_building(id) + if data == null: + return + var box: BoxMesh = mesh.mesh as BoxMesh + if box: + box.size = data.size + var shape: BoxShape3D = collision.shape as BoxShape3D + if shape: + shape.size = data.size + collision.position = Vector3(0.0, data.size.y * 0.5, 0.0) + mesh.position = Vector3(0.0, data.size.y * 0.5, 0.0) + var mat: StandardMaterial3D = StandardMaterial3D.new() + mat.albedo_color = data.color + mesh.material_override = mat + +static func _load_building(id: StringName) -> Building: + var path := "res://resources/buildings/%s.tres" % str(id) + if ResourceLoader.exists(path): + return load(path) as Building + return null diff --git a/scenes/entities/building/building.gd.uid b/scenes/entities/building/building.gd.uid new file mode 100644 index 0000000..1ed9365 --- /dev/null +++ b/scenes/entities/building/building.gd.uid @@ -0,0 +1 @@ +uid://cq63wtaqo3ebt diff --git a/scenes/entities/building/building.tscn b/scenes/entities/building/building.tscn new file mode 100644 index 0000000..0823b0e --- /dev/null +++ b/scenes/entities/building/building.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=4 format=3 uid="uid://b0building001"] + +[ext_resource type="Script" path="res://scenes/entities/building/building.gd" id="1"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_1"] +size = Vector3(1, 1, 1) + +[sub_resource type="BoxMesh" id="BoxMesh_1"] +size = Vector3(1, 1, 1) + +[node name="Building" type="StaticBody3D"] +collision_layer = 16 +collision_mask = 0 +script = ExtResource("1") + +[node name="Collision" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) +shape = SubResource("BoxShape3D_1") + +[node name="Mesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) +mesh = SubResource("BoxMesh_1") diff --git a/scenes/entities/enemy/enemy.gd b/scenes/entities/enemy/enemy.gd new file mode 100644 index 0000000..123759a --- /dev/null +++ b/scenes/entities/enemy/enemy.gd @@ -0,0 +1,170 @@ +extends CharacterBody3D + +const GRAVITY: float = 18.0 + +@export var stats_resource: EnemyStats +@export var is_boss: bool = false + +@onready var nav: NavigationAgent3D = $NavAgent +@onready var mesh_holder: Node3D = $MeshHolder +@onready var collision: CollisionShape3D = $Collision +@onready var detection: Area3D = $DetectionArea +@onready var sync: MultiplayerSynchronizer = $Synchronizer +@onready var healthbar: MeshInstance3D = $Healthbar +@onready var name_label: Label3D = $NameLabel + +var origin: Vector3 = Vector3.ZERO +var attack_cd: float = 0.0 +var dead: bool = false +var invasion_target: Node = null + +@export var sync_position: Vector3 = Vector3.ZERO +@export var sync_yaw: float = 0.0 + +func _enter_tree() -> void: + set_multiplayer_authority(1) + +func _ready() -> void: + if is_boss: + add_to_group("boss") + add_to_group("enemies") + if stats_resource == null: + stats_resource = EnemyStats.new() + Stats.register(self, stats_resource) + origin = global_position + detection.body_entered.connect(_on_body_entered) + EventBus.health_changed.connect(_on_health_changed) + EventBus.entity_died.connect(_on_entity_died) + name_label.text = "Boss" if is_boss else "Enemy" + if is_boss: + name_label.text = "Boss" + var mesh: MeshInstance3D = mesh_holder.get_node("Mesh") + mesh.scale = Vector3(1.5, 1.5, 1.5) + var mat: StandardMaterial3D = StandardMaterial3D.new() + mat.albedo_color = Color(0.6, 0.2, 0.8) + mesh.material_override = mat + +func _exit_tree() -> void: + Stats.deregister(self) + +func _physics_process(delta: float) -> void: + if not is_multiplayer_authority(): + global_position = global_position.lerp(sync_position, clamp(delta * 20.0, 0.0, 1.0)) + rotation.y = lerp_angle(rotation.y, sync_yaw, clamp(delta * 20.0, 0.0, 1.0)) + return + if dead: + return + if not is_on_floor(): + velocity.y -= GRAVITY * delta + attack_cd = max(0.0, attack_cd - delta) + var target: Node = _get_target() + if target == null: + _return_to_origin(delta) + else: + _chase_or_attack(target, delta) + move_and_slide() + sync_position = global_position + sync_yaw = rotation.y + +func _get_target() -> Node: + var aggro: Node = get_node_or_null("/root/World/Systems/AggroSystem") + if aggro == null: + aggro = get_node_or_null("/root/Dungeon/Systems/AggroSystem") + if aggro and aggro.has_method("target_for"): + var t: Node = aggro.target_for(self) + if t: + return t + if invasion_target and is_instance_valid(invasion_target): + return invasion_target + return null + +func _chase_or_attack(target: Node, delta: float) -> void: + var t_pos: Vector3 = (target as Node3D).global_position + var d: float = global_position.distance_to(t_pos) + var attack_range: float = float(Stats.get_stat(self, "attack_range", 2.0)) + if d <= attack_range: + velocity.x = move_toward(velocity.x, 0.0, 20.0 * delta) + velocity.z = move_toward(velocity.z, 0.0, 20.0 * delta) + var look := Vector3(t_pos.x - global_position.x, 0.0, t_pos.z - global_position.z) + if look.length() > 0.01: + rotation.y = atan2(look.x, look.z) + if attack_cd <= 0.0: + attack_cd = float(Stats.get_stat(self, "attack_cooldown", 1.5)) + var dmg: float = float(Stats.get_stat(self, "attack_damage", 5.0)) + EventBus.damage_requested.emit(self, target, dmg, Element.NONE) + else: + var dir := Vector3(t_pos.x - global_position.x, 0.0, t_pos.z - global_position.z).normalized() + var speed: float = float(Stats.get_stat(self, "speed", 3.0)) + velocity.x = dir.x * speed + velocity.z = dir.z * speed + if dir.length() > 0.01: + rotation.y = atan2(dir.x, dir.z) + +func _return_to_origin(delta: float) -> void: + var d: float = global_position.distance_to(origin) + if d < 0.5: + velocity.x = move_toward(velocity.x, 0.0, 20.0 * delta) + velocity.z = move_toward(velocity.z, 0.0, 20.0 * delta) + var max_hp: float = float(Stats.get_stat(self, "max_health", 100.0)) + var hp: float = float(Stats.get_stat(self, "health", 0.0)) + if hp > 0.0 and hp < max_hp: + Stats.set_stat(self, "health", min(max_hp, hp + max_hp * 0.20 * delta)) + EventBus.health_changed.emit(self, Stats.get_stat(self, "health"), max_hp) + else: + var dir: Vector3 = Vector3(origin.x - global_position.x, 0.0, origin.z - global_position.z).normalized() + var speed: float = float(Stats.get_stat(self, "speed", 3.0)) * 1.5 + velocity.x = dir.x * speed + velocity.z = dir.z * speed + if dir.length() > 0.01: + rotation.y = atan2(dir.x, dir.z) + +func _on_body_entered(body: Node) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + if body.is_in_group("player") and not dead: + EventBus.enemy_detected.emit(self, body) + +func _on_health_changed(entity: Node, current: float, max: float) -> void: + if entity != self: + return + var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0) + if healthbar: + healthbar.scale.x = max(0.01, ratio) + +func _on_entity_died(entity: Node) -> void: + if entity != self or dead: + return + dead = true + if multiplayer.is_server() or multiplayer.multiplayer_peer == null: + _on_death.rpc() + var loot: Node = get_node_or_null("/root/World/Systems/LootSystem") + if loot == null: + loot = get_node_or_null("/root/Dungeon/Systems/LootSystem") + if loot and loot.has_method("drop_loot_for"): + loot.drop_loot_for(self, global_position) + var xp_sys: Node = get_node_or_null("/root/World/Systems/XpSystem") + if xp_sys == null: + xp_sys = get_node_or_null("/root/Dungeon/Systems/XpSystem") + if xp_sys and xp_sys.has_method("award_for_enemy"): + xp_sys.award_for_enemy(self) + if is_boss: + EventBus.boss_defeated.emit(self) + var t := get_tree().create_timer(2.0) + t.timeout.connect(func(): + if is_instance_valid(self): + queue_free()) + +@rpc("any_peer", "reliable", "call_local") +func _on_death() -> void: + if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1: + return + dead = true + collision.disabled = true + modulate_alpha(0.4) + +func modulate_alpha(a: float) -> void: + for child in mesh_holder.get_children(): + if child is MeshInstance3D and child.material_override: + var c: Color = child.material_override.albedo_color + c.a = a + child.material_override.albedo_color = c diff --git a/scenes/entities/enemy/enemy.gd.uid b/scenes/entities/enemy/enemy.gd.uid new file mode 100644 index 0000000..754cb70 --- /dev/null +++ b/scenes/entities/enemy/enemy.gd.uid @@ -0,0 +1 @@ +uid://t28sckpnef15 diff --git a/scenes/entities/enemy/enemy.tscn b/scenes/entities/enemy/enemy.tscn new file mode 100644 index 0000000..276acd1 --- /dev/null +++ b/scenes/entities/enemy/enemy.tscn @@ -0,0 +1,76 @@ +[gd_scene load_steps=8 format=3 uid="uid://b0enemy00001"] + +[ext_resource type="Script" path="res://scenes/entities/enemy/enemy.gd" id="1"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] +height = 1.6 +radius = 0.4 + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"] +height = 1.6 +radius = 0.4 + +[sub_resource type="StandardMaterial3D" id="Mat_1"] +albedo_color = Color(0.6, 0.6, 0.6, 1) + +[sub_resource type="SphereShape3D" id="SphereShape3D_1"] +radius = 12.0 + +[sub_resource type="QuadMesh" id="QuadMesh_1"] +size = Vector2(1.0, 0.12) + +[sub_resource type="StandardMaterial3D" id="HBMat"] +shading_mode = 0 +no_depth_test = true +albedo_color = Color(0.9, 0.2, 0.2, 1) + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1"] +properties/0/path = NodePath(".:sync_position") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath(".:sync_yaw") +properties/1/spawn = true +properties/1/replication_mode = 1 + +[node name="Enemy" type="CharacterBody3D"] +collision_layer = 4 +collision_mask = 1 +script = ExtResource("1") + +[node name="Collision" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0) +shape = SubResource("CapsuleShape3D_1") + +[node name="MeshHolder" type="Node3D" parent="."] + +[node name="Mesh" type="MeshInstance3D" parent="MeshHolder"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0) +mesh = SubResource("CapsuleMesh_1") +material_override = SubResource("Mat_1") + +[node name="NameLabel" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0) +billboard = 1 +text = "Enemy" +font_size = 20 +outline_size = 3 + +[node name="Healthbar" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.95, 0) +mesh = SubResource("QuadMesh_1") +material_override = SubResource("HBMat") +gi_mode = 0 + +[node name="DetectionArea" type="Area3D" parent="."] +collision_layer = 0 +collision_mask = 2 + +[node name="DetectionShape" type="CollisionShape3D" parent="DetectionArea"] +shape = SubResource("SphereShape3D_1") + +[node name="NavAgent" type="NavigationAgent3D" parent="."] +path_desired_distance = 0.5 +target_desired_distance = 0.5 + +[node name="Synchronizer" type="MultiplayerSynchronizer" parent="."] +replication_config = SubResource("SceneReplicationConfig_1") diff --git a/scenes/entities/gate/gate.gd b/scenes/entities/gate/gate.gd new file mode 100644 index 0000000..0505d93 --- /dev/null +++ b/scenes/entities/gate/gate.gd @@ -0,0 +1,80 @@ +extends StaticBody3D + +@export var stats_resource: GateStats +@export var is_red: bool = false + +@onready var mesh: MeshInstance3D = $Mesh +@onready var healthbar: MeshInstance3D = $Healthbar +@onready var name_label: Label3D = $NameLabel +@onready var spawn_point: Node3D = $SpawnPoint + +var spawn_timer: float = 0.0 +var spawned_count: int = 0 +var dead: bool = false + +func _enter_tree() -> void: + set_multiplayer_authority(1) + +func _ready() -> void: + add_to_group("gates") + if is_red: + add_to_group("red_gate") + if stats_resource == null: + stats_resource = GateStats.new() + if is_red: + stats_resource.max_health = 600.0 + stats_resource.spawn_count = 8 + else: + stats_resource.max_health = 200.0 + stats_resource.spawn_count = 5 + stats_resource.is_red = is_red + Stats.register(self, stats_resource) + EventBus.health_changed.connect(_on_health_changed) + EventBus.entity_died.connect(_on_entity_died) + if is_red: + var mat: StandardMaterial3D = StandardMaterial3D.new() + mat.albedo_color = Color(0.95, 0.2, 0.15) + mat.emission_enabled = true + mat.emission = Color(1.0, 0.4, 0.2) + mat.emission_energy_multiplier = 0.6 + mesh.material_override = mat + name_label.text = "Red Gate" + else: + name_label.text = "Gate" + set_physics_process(true) + +func _exit_tree() -> void: + Stats.deregister(self) + +func _physics_process(delta: float) -> void: + if dead: + return + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + spawn_timer = max(0.0, spawn_timer - delta) + if spawn_timer <= 0.0 and spawned_count < int(Stats.get_stat(self, "spawn_count", 5)): + var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem") + if spawn_sys and spawn_sys.has_method("spawn_enemy_at"): + spawn_sys.spawn_enemy_at(spawn_point.global_position + Vector3(randf_range(-1.5, 1.5), 0.0, randf_range(-1.5, 1.5)), is_red) + spawned_count += 1 + spawn_timer = float(Stats.get_stat(self, "spawn_interval", 4.0)) + +func _on_health_changed(entity: Node, current: float, max: float) -> void: + if entity != self: + return + var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0) + healthbar.scale.x = max(0.01, ratio * 2.0) + +func _on_entity_died(entity: Node) -> void: + if entity != self or dead: + return + dead = true + if multiplayer.is_server() or multiplayer.multiplayer_peer == null: + var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem") + if spawn_sys and spawn_sys.has_method("spawn_portal_at"): + spawn_sys.spawn_portal_at(global_position, is_red) + EventBus.gate_destroyed.emit(self) + var t := get_tree().create_timer(0.5) + t.timeout.connect(func(): + if is_instance_valid(self): + queue_free()) diff --git a/scenes/entities/gate/gate.gd.uid b/scenes/entities/gate/gate.gd.uid new file mode 100644 index 0000000..3da786c --- /dev/null +++ b/scenes/entities/gate/gate.gd.uid @@ -0,0 +1 @@ +uid://yrf2o25mrihx diff --git a/scenes/entities/gate/gate.tscn b/scenes/entities/gate/gate.tscn new file mode 100644 index 0000000..d522306 --- /dev/null +++ b/scenes/entities/gate/gate.tscn @@ -0,0 +1,53 @@ +[gd_scene load_steps=7 format=3 uid="uid://b0gate0001"] + +[ext_resource type="Script" path="res://scenes/entities/gate/gate.gd" id="1"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_1"] +size = Vector3(2, 3, 2) + +[sub_resource type="BoxMesh" id="BoxMesh_1"] +size = Vector3(2, 3, 2) + +[sub_resource type="StandardMaterial3D" id="GateMat"] +albedo_color = Color(0.4, 0.4, 0.55, 1) +emission_enabled = true +emission = Color(0.2, 0.2, 0.4, 1) +emission_energy_multiplier = 0.4 + +[sub_resource type="QuadMesh" id="QuadMesh_HB"] +size = Vector2(1.0, 0.18) + +[sub_resource type="StandardMaterial3D" id="HBMat"] +shading_mode = 0 +no_depth_test = true +albedo_color = Color(0.95, 0.4, 0.2, 1) + +[node name="Gate" type="StaticBody3D"] +collision_layer = 4 +collision_mask = 0 +script = ExtResource("1") + +[node name="Collision" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +shape = SubResource("BoxShape3D_1") + +[node name="Mesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +mesh = SubResource("BoxMesh_1") +material_override = SubResource("GateMat") + +[node name="NameLabel" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.0, 0) +billboard = 1 +text = "Gate" +font_size = 22 +outline_size = 3 + +[node name="Healthbar" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.5, 0) +mesh = SubResource("QuadMesh_HB") +material_override = SubResource("HBMat") +gi_mode = 0 + +[node name="SpawnPoint" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3) diff --git a/scenes/entities/loot/loot_drop.gd b/scenes/entities/loot/loot_drop.gd new file mode 100644 index 0000000..402f992 --- /dev/null +++ b/scenes/entities/loot/loot_drop.gd @@ -0,0 +1,39 @@ +extends Area3D + +@onready var mesh: MeshInstance3D = $Mesh +@onready var label: Label3D = $Label + +@export var item_id: StringName = &"" +@export var amount: int = 1 + +var rotation_speed: float = 1.5 +var bob: float = 0.0 + +func _enter_tree() -> void: + set_multiplayer_authority(1) + +func _ready() -> void: + add_to_group("loot") + body_entered.connect(_on_body_entered) + if item_id != &"": + label.text = "%s x%d" % [str(item_id), amount] + else: + label.text = "Loot" + +func _process(delta: float) -> void: + bob += delta + mesh.rotation.y += rotation_speed * delta + mesh.position.y = 0.5 + sin(bob * 2.0) * 0.1 + +func _on_body_entered(body: Node) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + if not body.is_in_group("player"): + return + var inv: Node = get_node_or_null("/root/World/Systems/InventorySystem") + if inv == null: + inv = get_node_or_null("/root/Dungeon/Systems/InventorySystem") + if inv and inv.has_method("add_item"): + inv.add_item(body, item_id, amount) + EventBus.item_picked_up.emit(body, item_id) + queue_free() diff --git a/scenes/entities/loot/loot_drop.gd.uid b/scenes/entities/loot/loot_drop.gd.uid new file mode 100644 index 0000000..4927d46 --- /dev/null +++ b/scenes/entities/loot/loot_drop.gd.uid @@ -0,0 +1 @@ +uid://begk6dfrigj12 diff --git a/scenes/entities/loot/loot_drop.tscn b/scenes/entities/loot/loot_drop.tscn new file mode 100644 index 0000000..e0f2e23 --- /dev/null +++ b/scenes/entities/loot/loot_drop.tscn @@ -0,0 +1,35 @@ +[gd_scene load_steps=4 format=3 uid="uid://b0loot0001"] + +[ext_resource type="Script" path="res://scenes/entities/loot/loot_drop.gd" id="1"] + +[sub_resource type="BoxMesh" id="BoxMesh_1"] +size = Vector3(0.4, 0.4, 0.4) + +[sub_resource type="StandardMaterial3D" id="LootMat"] +albedo_color = Color(0.95, 0.85, 0.2, 1) +emission_enabled = true +emission = Color(1.0, 0.9, 0.4, 1) +emission_energy_multiplier = 0.6 + +[sub_resource type="SphereShape3D" id="SphereShape3D_1"] +radius = 1.2 + +[node name="LootDrop" type="Area3D"] +collision_layer = 0 +collision_mask = 2 +script = ExtResource("1") + +[node name="Mesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) +mesh = SubResource("BoxMesh_1") +material_override = SubResource("LootMat") + +[node name="Label" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +billboard = 1 +text = "Loot" +font_size = 18 +outline_size = 3 + +[node name="PickupShape" type="CollisionShape3D" parent="."] +shape = SubResource("SphereShape3D_1") diff --git a/scenes/entities/npc/npc.gd b/scenes/entities/npc/npc.gd new file mode 100644 index 0000000..827cd86 --- /dev/null +++ b/scenes/entities/npc/npc.gd @@ -0,0 +1,52 @@ +extends StaticBody3D + +@export var profile: NpcProfile +@export var profile_id: StringName = &"" + +@onready var mesh: MeshInstance3D = $Mesh +@onready var label: Label3D = $Label +@onready var prompt: Label3D = $Prompt +@onready var interact_area: Area3D = $InteractArea + +var nearby_player: Node = null + +func _enter_tree() -> void: + set_multiplayer_authority(1) + +func _ready() -> void: + add_to_group("npc") + if profile == null and profile_id != &"": + var path := "res://resources/npcs/%s.tres" % str(profile_id) + if ResourceLoader.exists(path): + profile = load(path) as NpcProfile + if profile == null: + profile = NpcProfile.new() + profile.display_name = "Villager" + profile.lore = "A simple villager." + profile.personality = "Friendly, curious." + profile.fallback_text = "Hello there!" + label.text = profile.display_name + prompt.visible = false + if profile.color != Color.WHITE: + var mat: StandardMaterial3D = StandardMaterial3D.new() + mat.albedo_color = profile.color + mesh.material_override = mat + interact_area.body_entered.connect(_on_player_near) + interact_area.body_exited.connect(_on_player_left) + +func _process(_delta: float) -> void: + if nearby_player and nearby_player.is_multiplayer_authority(): + prompt.visible = true + if Input.is_action_just_pressed("interact"): + EventBus.dialog_opened.emit(nearby_player, self) + else: + prompt.visible = false + +func _on_player_near(body: Node) -> void: + if body.is_in_group("player"): + if body.is_multiplayer_authority(): + nearby_player = body + +func _on_player_left(body: Node) -> void: + if body == nearby_player: + nearby_player = null diff --git a/scenes/entities/npc/npc.gd.uid b/scenes/entities/npc/npc.gd.uid new file mode 100644 index 0000000..af41187 --- /dev/null +++ b/scenes/entities/npc/npc.gd.uid @@ -0,0 +1 @@ +uid://chul3jysytie2 diff --git a/scenes/entities/npc/npc.tscn b/scenes/entities/npc/npc.tscn new file mode 100644 index 0000000..adc9b09 --- /dev/null +++ b/scenes/entities/npc/npc.tscn @@ -0,0 +1,53 @@ +[gd_scene load_steps=6 format=3 uid="uid://b0npc0001"] + +[ext_resource type="Script" path="res://scenes/entities/npc/npc.gd" id="1"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] +height = 1.7 +radius = 0.35 + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"] +height = 1.7 +radius = 0.35 + +[sub_resource type="StandardMaterial3D" id="NpcMat"] +albedo_color = Color(0.85, 0.7, 0.5, 1) + +[sub_resource type="SphereShape3D" id="InteractShape"] +radius = 2.5 + +[node name="Npc" type="StaticBody3D"] +collision_layer = 0 +collision_mask = 0 +script = ExtResource("1") + +[node name="Collision" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0) +shape = SubResource("CapsuleShape3D_1") + +[node name="Mesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0) +mesh = SubResource("CapsuleMesh_1") +material_override = SubResource("NpcMat") + +[node name="Label" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0) +billboard = 1 +text = "Villager" +font_size = 22 +outline_size = 3 +modulate = Color(0.95, 0.85, 0.5, 1) + +[node name="Prompt" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.55, 0) +billboard = 1 +text = "[E] Talk" +font_size = 16 +outline_size = 2 + +[node name="InteractArea" type="Area3D" parent="."] +collision_layer = 0 +collision_mask = 2 + +[node name="InteractShape" type="CollisionShape3D" parent="InteractArea"] +shape = SubResource("InteractShape") diff --git a/scenes/entities/player/player.gd b/scenes/entities/player/player.gd new file mode 100644 index 0000000..41f0a4b --- /dev/null +++ b/scenes/entities/player/player.gd @@ -0,0 +1,242 @@ +extends CharacterBody3D + +const GRAVITY: float = 18.0 +const MOUSE_SENS: float = 0.0035 + +@export var stats_resource: PlayerStats + +@onready var pivot: Node3D = $Pivot +@onready var pitch_pivot: Node3D = $Pivot/PitchPivot +@onready var camera: Camera3D = $Pivot/PitchPivot/Camera +@onready var mesh_holder: Node3D = $MeshHolder +@onready var collision: CollisionShape3D = $Collision +@onready var sync: MultiplayerSynchronizer = $Synchronizer +@onready var name_label: Label3D = $NameLabel + +var peer_id: int = 1 +var role: int = GameState.ROLE_DAMAGE +var look_dragging: bool = false +var current_target: Node = null +var dead: bool = false +var ui_capturing: bool = false +var build_mode: bool = false + +@export var sync_position: Vector3 = Vector3.ZERO +@export var sync_velocity: Vector3 = Vector3.ZERO +@export var sync_yaw: float = 0.0 +@export var sync_role: int = GameState.ROLE_DAMAGE + +func _enter_tree() -> void: + peer_id = name.to_int() + set_multiplayer_authority(peer_id) + +func _ready() -> void: + add_to_group("player") + if stats_resource == null: + stats_resource = PlayerStats.new() + Stats.register(self, stats_resource) + Stats.set_stat(self, "role", role) + name_label.text = Net.player_names.get(peer_id, "P%d" % peer_id) + EventBus.entity_died.connect(_on_entity_died_clear_target) + if is_multiplayer_authority(): + camera.current = true + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + else: + camera.current = false + _apply_role_visual(role) + +func _on_entity_died_clear_target(entity: Node) -> void: + if entity == current_target: + current_target = null + +func _set_target(t: Node) -> void: + current_target = t + EventBus.target_changed.emit(self, t) + if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and not multiplayer.is_server(): + _request_target.rpc_id(1, String(t.get_path()) if t else "") + +@rpc("any_peer", "reliable") +func _request_target(path_str: String) -> void: + if not multiplayer.is_server(): + return + var t: Node = get_node_or_null(NodePath(path_str)) if path_str != "" else null + current_target = t + +func _exit_tree() -> void: + Stats.deregister(self) + +func _input(event: InputEvent) -> void: + if not is_multiplayer_authority(): + return + if ui_capturing: + return + if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT: + look_dragging = event.pressed + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if look_dragging else Input.MOUSE_MODE_VISIBLE + elif event is InputEventMouseMotion and look_dragging: + pivot.rotate_y(-event.relative.x * MOUSE_SENS) + pitch_pivot.rotate_x(-event.relative.y * MOUSE_SENS) + pitch_pivot.rotation.x = clamp(pitch_pivot.rotation.x, -1.2, 0.2) + +func _unhandled_input(event: InputEvent) -> void: + if not is_multiplayer_authority(): + return + if dead: + return + if build_mode: + return + if event.is_action_pressed("class_tank"): + _request_role(GameState.ROLE_TANK) + elif event.is_action_pressed("class_damage"): + _request_role(GameState.ROLE_DAMAGE) + elif event.is_action_pressed("class_healer"): + _request_role(GameState.ROLE_HEALER) + elif event.is_action_pressed("ability_1"): + EventBus.ability_use_requested.emit(self, 0) + elif event.is_action_pressed("ability_2"): + EventBus.ability_use_requested.emit(self, 1) + elif event.is_action_pressed("ability_3"): + EventBus.ability_use_requested.emit(self, 2) + elif event.is_action_pressed("ability_4"): + EventBus.ability_use_requested.emit(self, 3) + elif event.is_action_pressed("target_next"): + var nt := _cycle_target() + _set_target(nt) + elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed and not look_dragging: + var t := _pick_target_under_mouse() + if t: + _set_target(t) + +func _physics_process(delta: float) -> void: + if is_multiplayer_authority(): + if not dead: + _process_local(delta) + sync_position = global_position + sync_velocity = velocity + sync_yaw = pivot.rotation.y + sync_role = role + else: + global_position = global_position.lerp(sync_position, clamp(delta * 20.0, 0.0, 1.0)) + pivot.rotation.y = lerp_angle(pivot.rotation.y, sync_yaw, clamp(delta * 20.0, 0.0, 1.0)) + if sync_role != role: + role = sync_role + _apply_role_visual(role) + +func _process_local(delta: float) -> void: + if not is_on_floor(): + velocity.y -= GRAVITY * delta + var move_dir: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_back") if not ui_capturing else Vector2.ZERO + var speed: float = float(Stats.get_stat(self, "speed", 5.0)) + var basis_y := Basis(Vector3.UP, pivot.rotation.y) + var direction := basis_y * Vector3(move_dir.x, 0.0, move_dir.y) + if direction.length() > 0.01: + velocity.x = direction.x * speed + velocity.z = direction.z * speed + var look_dir := Vector3(velocity.x, 0.0, velocity.z).normalized() + var target_basis := Basis.looking_at(look_dir, Vector3.UP) + mesh_holder.basis = mesh_holder.basis.slerp(target_basis, clamp(delta * 12.0, 0.0, 1.0)) + else: + velocity.x = move_toward(velocity.x, 0.0, speed * 6.0 * delta) + velocity.z = move_toward(velocity.z, 0.0, speed * 6.0 * delta) + if Input.is_action_just_pressed("jump") and is_on_floor() and not ui_capturing: + velocity.y = float(Stats.get_stat(self, "jump_velocity", 4.5)) + move_and_slide() + +func _request_role(new_role: int) -> void: + _set_role.rpc_id(1, new_role) + +@rpc("any_peer", "reliable", "call_local") +func _set_role(new_role: int) -> void: + if not multiplayer.is_server(): + return + if new_role == role: + return + role = new_role + Stats.set_stat(self, "role", role) + _apply_role.rpc(role) + +@rpc("any_peer", "reliable", "call_local") +func _apply_role(new_role: int) -> void: + if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1: + return + role = new_role + if Stats.has(self): + Stats.set_stat(self, "role", role) + _apply_role_visual(role) + EventBus.role_changed.emit(self, role) + +func _apply_role_visual(r: int) -> void: + var mesh: MeshInstance3D = mesh_holder.get_node("Mesh") + var mat: StandardMaterial3D = mesh.get_active_material(0).duplicate() if mesh.get_active_material(0) else StandardMaterial3D.new() + match r: + GameState.ROLE_TANK: + mat.albedo_color = Color(0.3, 0.5, 0.95) + GameState.ROLE_DAMAGE: + mat.albedo_color = Color(0.95, 0.3, 0.3) + GameState.ROLE_HEALER: + mat.albedo_color = Color(0.4, 0.85, 0.4) + mesh.material_override = mat + +@rpc("any_peer", "reliable", "call_local") +func set_dead(value: bool) -> void: + if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1: + return + dead = value + visible = not value + collision.disabled = value + if value: + velocity = Vector3.ZERO + +@rpc("any_peer", "reliable", "call_local") +func teleport_to(pos: Vector3) -> void: + if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1: + return + global_position = pos + sync_position = pos + +func set_ui_capturing(v: bool) -> void: + ui_capturing = v + +func set_build_mode(v: bool) -> void: + build_mode = v + +func _cycle_target() -> Node: + if current_target != null and not is_instance_valid(current_target): + current_target = null + var candidates: Array = [] + for n in get_tree().get_nodes_in_group("enemies"): + if is_instance_valid(n): + candidates.append(n) + for n in get_tree().get_nodes_in_group("portals"): + if is_instance_valid(n): + candidates.append(n) + for n in get_tree().get_nodes_in_group("gates"): + if is_instance_valid(n): + candidates.append(n) + if candidates.is_empty(): + current_target = null + return null + candidates.sort_custom(func(a, b): + return (a as Node3D).global_position.distance_to(global_position) < (b as Node3D).global_position.distance_to(global_position)) + if current_target == null or not current_target in candidates: + current_target = candidates[0] + else: + var idx: int = candidates.find(current_target) + current_target = candidates[(idx + 1) % candidates.size()] + return current_target + +func _pick_target_under_mouse() -> Node: + var mouse := get_viewport().get_mouse_position() + var from := camera.project_ray_origin(mouse) + var to := from + camera.project_ray_normal(mouse) * 100.0 + var space := get_world_3d().direct_space_state + var query := PhysicsRayQueryParameters3D.create(from, to) + query.collision_mask = 0xFFFFFFFF + query.exclude = [self] + var hit := space.intersect_ray(query) + if hit.is_empty(): + return null + var n: Node = hit.collider + while n != null and not (n.is_in_group("enemies") or n.is_in_group("portals") or n.is_in_group("gates") or n.is_in_group("npc")): + n = n.get_parent() + return n diff --git a/scenes/entities/player/player.gd.uid b/scenes/entities/player/player.gd.uid new file mode 100644 index 0000000..588b8e3 --- /dev/null +++ b/scenes/entities/player/player.gd.uid @@ -0,0 +1 @@ +uid://w43lhoat7ccq diff --git a/scenes/entities/player/player.tscn b/scenes/entities/player/player.tscn new file mode 100644 index 0000000..efebb1d --- /dev/null +++ b/scenes/entities/player/player.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=8 format=3 uid="uid://b0player00001"] + +[ext_resource type="Script" path="res://scenes/entities/player/player.gd" id="1"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] +height = 1.8 +radius = 0.35 + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"] +height = 1.8 +radius = 0.35 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +albedo_color = Color(0.3, 0.55, 0.95, 1) + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1"] +properties/0/path = NodePath(".:sync_position") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath(".:sync_yaw") +properties/1/spawn = true +properties/1/replication_mode = 1 + +[node name="Player" type="CharacterBody3D"] +collision_layer = 2 +collision_mask = 1 +script = ExtResource("1") + +[node name="Collision" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0) +shape = SubResource("CapsuleShape3D_1") + +[node name="MeshHolder" type="Node3D" parent="."] + +[node name="Mesh" type="MeshInstance3D" parent="MeshHolder"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0) +mesh = SubResource("CapsuleMesh_1") +surface_material_override/0 = SubResource("StandardMaterial3D_1") + +[node name="NameLabel" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0, 0) +billboard = 1 +text = "Player" +font_size = 24 +outline_size = 4 + +[node name="Pivot" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) + +[node name="PitchPivot" type="Node3D" parent="Pivot"] + +[node name="Camera" type="Camera3D" parent="Pivot/PitchPivot"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 5) +current = false +fov = 70.0 + +[node name="Synchronizer" type="MultiplayerSynchronizer" parent="."] +replication_config = SubResource("SceneReplicationConfig_1") diff --git a/scenes/entities/portal/portal.gd b/scenes/entities/portal/portal.gd new file mode 100644 index 0000000..32b7017 --- /dev/null +++ b/scenes/entities/portal/portal.gd @@ -0,0 +1,62 @@ +extends StaticBody3D + +@export var stats_resource: PortalStats +@export var is_red: bool = false + +@onready var mesh: MeshInstance3D = $Mesh +@onready var name_label: Label3D = $NameLabel +@onready var enter_area: Area3D = $EnterArea + +var triggered: bool = false + +func _enter_tree() -> void: + set_multiplayer_authority(1) + +func _ready() -> void: + add_to_group("portals") + if is_red: + add_to_group("red_portal") + if stats_resource == null: + stats_resource = PortalStats.new() + stats_resource.max_health = 1.0 + stats_resource.is_red = is_red + Stats.register(self, stats_resource) + enter_area.body_entered.connect(_on_body_entered) + if is_red: + var mat: StandardMaterial3D = StandardMaterial3D.new() + mat.albedo_color = Color(0.95, 0.2, 0.2) + mat.emission_enabled = true + mat.emission = Color(1.0, 0.4, 0.3) + mat.emission_energy_multiplier = 1.5 + mesh.material_override = mat + name_label.text = "Red Portal" + else: + name_label.text = "Portal" + +func _exit_tree() -> void: + Stats.deregister(self) + +func _on_body_entered(body: Node) -> void: + if not body.is_in_group("player"): + return + if not body.is_multiplayer_authority(): + return + if triggered: + return + triggered = true + EventBus.portal_entered.emit(self, body) + _request_enter.rpc_id(1, is_red, global_position) + +@rpc("any_peer", "reliable", "call_local") +func _request_enter(red: bool, return_pos: Vector3) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + var seed: int = randi() + _do_enter.rpc(seed, red, return_pos) + +@rpc("authority", "reliable", "call_local") +func _do_enter(seed: int, red: bool, return_pos: Vector3) -> void: + GameState.dungeon_seed = seed + GameState.dungeon_red = red + GameState.portal_return_position = return_pos + GameState.change_scene(GameState.SCENE_DUNGEON) diff --git a/scenes/entities/portal/portal.gd.uid b/scenes/entities/portal/portal.gd.uid new file mode 100644 index 0000000..b8a136c --- /dev/null +++ b/scenes/entities/portal/portal.gd.uid @@ -0,0 +1 @@ +uid://cny0k5s884uco diff --git a/scenes/entities/portal/portal.tscn b/scenes/entities/portal/portal.tscn new file mode 100644 index 0000000..82b5913 --- /dev/null +++ b/scenes/entities/portal/portal.tscn @@ -0,0 +1,49 @@ +[gd_scene load_steps=6 format=3 uid="uid://b0portal0001"] + +[ext_resource type="Script" path="res://scenes/entities/portal/portal.gd" id="1"] + +[sub_resource type="CylinderShape3D" id="CylShape_1"] +height = 0.4 +radius = 1.5 + +[sub_resource type="CylinderMesh" id="CylMesh_1"] +top_radius = 1.5 +bottom_radius = 1.5 +height = 3.0 + +[sub_resource type="StandardMaterial3D" id="PortalMat"] +albedo_color = Color(0.3, 0.5, 0.95, 1) +emission_enabled = true +emission = Color(0.5, 0.7, 1.0, 1) +emission_energy_multiplier = 1.5 + +[sub_resource type="SphereShape3D" id="SphereEnter"] +radius = 1.8 + +[node name="Portal" type="StaticBody3D"] +collision_layer = 8 +collision_mask = 0 +script = ExtResource("1") + +[node name="Collision" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.2, 0) +shape = SubResource("CylShape_1") + +[node name="Mesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +mesh = SubResource("CylMesh_1") +material_override = SubResource("PortalMat") + +[node name="NameLabel" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.0, 0) +billboard = 1 +text = "Portal" +font_size = 22 +outline_size = 3 + +[node name="EnterArea" type="Area3D" parent="."] +collision_layer = 0 +collision_mask = 2 + +[node name="EnterShape" type="CollisionShape3D" parent="EnterArea"] +shape = SubResource("SphereEnter") diff --git a/scenes/entities/village/village.gd b/scenes/entities/village/village.gd new file mode 100644 index 0000000..3954330 --- /dev/null +++ b/scenes/entities/village/village.gd @@ -0,0 +1,36 @@ +extends StaticBody3D + +@export var stats_resource: VillageStats + +@onready var mesh: MeshInstance3D = $Mesh +@onready var label: Label3D = $Label +@onready var healthbar: MeshInstance3D = $Healthbar + +func _enter_tree() -> void: + set_multiplayer_authority(1) + +func _ready() -> void: + add_to_group("village") + if stats_resource == null: + stats_resource = VillageStats.new() + stats_resource.max_health = 1000.0 + Stats.register(self, stats_resource) + EventBus.health_changed.connect(_on_health_changed) + EventBus.entity_died.connect(_on_entity_died) + label.text = "Village" + +func _exit_tree() -> void: + Stats.deregister(self) + +func _on_health_changed(entity: Node, current: float, max: float) -> void: + if entity != self: + return + EventBus.village_damaged.emit(current, max) + var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0) + healthbar.scale.x = max(0.01, ratio * 4.0) + +func _on_entity_died(entity: Node) -> void: + if entity != self: + return + EventBus.village_destroyed.emit() + EventBus.game_over.emit() diff --git a/scenes/entities/village/village.gd.uid b/scenes/entities/village/village.gd.uid new file mode 100644 index 0000000..ee5e51f --- /dev/null +++ b/scenes/entities/village/village.gd.uid @@ -0,0 +1 @@ +uid://n5yrsrsav4yx diff --git a/scenes/entities/village/village.tscn b/scenes/entities/village/village.tscn new file mode 100644 index 0000000..5789c08 --- /dev/null +++ b/scenes/entities/village/village.tscn @@ -0,0 +1,47 @@ +[gd_scene load_steps=6 format=3 uid="uid://b0village001"] + +[ext_resource type="Script" path="res://scenes/entities/village/village.gd" id="1"] + +[sub_resource type="BoxShape3D" id="BoxShape_1"] +size = Vector3(6, 4, 6) + +[sub_resource type="BoxMesh" id="BoxMesh_1"] +size = Vector3(6, 4, 6) + +[sub_resource type="StandardMaterial3D" id="VillageMat"] +albedo_color = Color(0.55, 0.4, 0.25, 1) + +[sub_resource type="QuadMesh" id="QuadMesh_HB"] +size = Vector2(1.0, 0.25) + +[sub_resource type="StandardMaterial3D" id="HBMat"] +shading_mode = 0 +no_depth_test = true +albedo_color = Color(0.4, 0.85, 0.4, 1) + +[node name="Village" type="StaticBody3D"] +collision_layer = 1 +collision_mask = 0 +script = ExtResource("1") + +[node name="Collision" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0) +shape = SubResource("BoxShape_1") + +[node name="Mesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0) +mesh = SubResource("BoxMesh_1") +material_override = SubResource("VillageMat") + +[node name="Label" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.5, 0) +billboard = 1 +text = "Village" +font_size = 28 +outline_size = 4 + +[node name="Healthbar" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.0, 0) +mesh = SubResource("QuadMesh_HB") +material_override = SubResource("HBMat") +gi_mode = 0 diff --git a/scenes/hud/hud.gd b/scenes/hud/hud.gd new file mode 100644 index 0000000..dd04adb --- /dev/null +++ b/scenes/hud/hud.gd @@ -0,0 +1,497 @@ +extends CanvasLayer + +@onready var hp_bar: ProgressBar = %HpBar +@onready var hp_label: Label = %HpLabel +@onready var shield_bar: ProgressBar = %ShieldBar +@onready var shield_label: Label = %ShieldLabel +@onready var xp_bar: ProgressBar = %XpBar +@onready var level_label: Label = %LevelLabel +@onready var wave_label: Label = %WaveLabel +@onready var timer_label: Label = %WaveTimer +@onready var village_bar: ProgressBar = %VillageBar +@onready var role_icon: Panel = %RoleIcon +@onready var role_label: Label = %RoleLabel +@onready var ability_box: HBoxContainer = %AbilityBox +@onready var death_overlay: Control = %DeathOverlay +@onready var death_label: Label = %DeathLabel +@onready var chat_log: RichTextLabel = %ChatLog +@onready var chat_input: LineEdit = %ChatInput +@onready var inventory_panel: Control = %InventoryPanel +@onready var inventory_list: VBoxContainer = %InventoryList +@onready var crafting_panel: Control = %CraftingPanel +@onready var crafting_list: VBoxContainer = %CraftingList +@onready var build_panel: Control = %BuildPanel +@onready var build_list: HBoxContainer = %BuildList +@onready var dialog_panel: Control = %DialogPanel +@onready var dialog_npc: Label = %DialogNpc +@onready var dialog_log: RichTextLabel = %DialogLog +@onready var dialog_input: LineEdit = %DialogInput +@onready var map_panel: Control = %MapPanel +@onready var map_canvas: Control = %MapCanvas +@onready var minimap_canvas: Control = %MinimapCanvas +@onready var pause_panel: Control = %PausePanel +@onready var game_over_overlay: Control = %GameOverOverlay +@onready var ability_buttons: Array = [] + +var local_player: Node = null +var dialog_npc_node: Node = null +var build_selected: int = 0 +var build_rotation: float = 0.0 +var build_preview: MeshInstance3D = null +var build_active: bool = false +var _minimap_accum: float = 0.0 + +func _ready() -> void: + add_to_group("dialog_ui") + EventBus.health_changed.connect(_on_health_changed) + EventBus.shield_changed.connect(_on_shield_changed) + EventBus.cooldown_tick.connect(_on_cooldown_tick) + EventBus.role_changed.connect(_on_role_changed) + EventBus.entity_died.connect(_on_entity_died) + EventBus.entity_respawned.connect(_on_respawned) + EventBus.wave_started.connect(_on_wave_started) + EventBus.wave_timer_tick.connect(_on_wave_tick) + EventBus.village_damaged.connect(_on_village_damaged) + EventBus.village_destroyed.connect(_on_village_destroyed) + EventBus.invasion_started.connect(_on_invasion_started) + EventBus.xp_gained.connect(_on_xp_gained) + EventBus.level_up.connect(_on_level_up) + EventBus.dialog_opened.connect(_on_dialog_opened) + EventBus.inventory_changed.connect(_on_inventory_changed) + EventBus.chat_message.connect(_on_chat) + chat_input.text_submitted.connect(_on_chat_submitted) + dialog_input.text_submitted.connect(_on_dialog_submitted) + death_overlay.visible = false + inventory_panel.visible = false + crafting_panel.visible = false + build_panel.visible = false + dialog_panel.visible = false + map_panel.visible = false + pause_panel.visible = false + game_over_overlay.visible = false + set_process(true) + _wire_ability_buttons() + call_deferred("_populate_build_list") + +func _process(_delta: float) -> void: + if local_player == null: + local_player = _find_local_player() + if local_player: + var role: int = int(Stats.get_stat(local_player, "role", GameState.ROLE_DAMAGE)) + _on_role_changed(local_player, role) + _refresh_vitals() + if build_active: + _update_build_preview() + _update_minimap() + +func _unhandled_input(event: InputEvent) -> void: + if event.is_action_pressed("inventory"): + _toggle_panel(inventory_panel) + _refresh_inventory() + elif event.is_action_pressed("crafting"): + _toggle_panel(crafting_panel) + _refresh_crafting() + elif event.is_action_pressed("build_mode"): + _toggle_build_mode() + elif event.is_action_pressed("map"): + _toggle_panel(map_panel) + elif event.is_action_pressed("chat"): + chat_input.grab_focus() + _capture_ui(true) + elif event.is_action_pressed("pause"): + if dialog_panel.visible: + dialog_panel.visible = false + _capture_ui(false) + elif build_active: + _toggle_build_mode() + elif inventory_panel.visible or crafting_panel.visible or map_panel.visible: + inventory_panel.visible = false + crafting_panel.visible = false + map_panel.visible = false + _capture_ui(false) + else: + _toggle_pause() + elif build_active: + if event.is_action_pressed("rotate_build"): + build_rotation = wrapf(build_rotation + PI * 0.5, 0.0, TAU) + elif event.is_action_pressed("ability_1"): + _select_build(0) + elif event.is_action_pressed("ability_2"): + _select_build(1) + elif event.is_action_pressed("ability_3"): + _select_build(2) + elif event.is_action_pressed("ability_4"): + _select_build(3) + elif event is InputEventMouseButton and event.pressed: + if event.button_index == MOUSE_BUTTON_LEFT: + _try_place() + elif event.button_index == MOUSE_BUTTON_MIDDLE: + _try_remove() + +func _wire_ability_buttons() -> void: + for c in ability_box.get_children(): + if c is Button: + ability_buttons.append(c) + +func _toggle_panel(panel: Control) -> void: + panel.visible = not panel.visible + _capture_ui(_any_panel_visible()) + +func _any_panel_visible() -> bool: + return inventory_panel.visible or crafting_panel.visible or dialog_panel.visible or pause_panel.visible + +func _capture_ui(v: bool) -> void: + if local_player and local_player.has_method("set_ui_capturing"): + local_player.set_ui_capturing(v) + +func _find_local_player() -> Node: + for p in get_tree().get_nodes_in_group("player"): + if p.is_multiplayer_authority(): + return p + return null + +func _refresh_vitals() -> void: + if local_player == null: + return + var hp: float = float(Stats.get_stat(local_player, "health", 0.0)) + var max_hp: float = float(Stats.get_stat(local_player, "max_health", 1.0)) + _on_health_changed(local_player, hp, max_hp) + var shield: float = float(Stats.get_stat(local_player, "shield", 0.0)) + var max_shield: float = float(Stats.get_stat(local_player, "max_shield", 0.0)) + _on_shield_changed(local_player, shield, max_shield) + var xp: float = float(Stats.get_stat(local_player, "xp", 0.0)) + var to_next: float = float(Stats.get_stat(local_player, "xp_to_next", 50.0)) + xp_bar.max_value = to_next + xp_bar.value = xp + level_label.text = "Lv %d" % int(Stats.get_stat(local_player, "level", 1)) + +func _on_health_changed(entity: Node, current: float, max: float) -> void: + if entity != local_player: + return + hp_bar.max_value = max + hp_bar.value = current + hp_label.text = "%d / %d" % [int(current), int(max)] + +func _on_shield_changed(entity: Node, current: float, max: float) -> void: + if entity != local_player: + return + shield_bar.max_value = max if max > 0 else 1 + shield_bar.value = current + shield_label.text = "%d / %d" % [int(current), int(max)] + +func _on_cooldown_tick(entity: Node, cds: PackedFloat32Array, _max_cds: PackedFloat32Array, gcd: float) -> void: + if entity != local_player: + return + for i in range(min(ability_buttons.size(), cds.size())): + var btn: Button = ability_buttons[i] + if cds[i] > 0.0: + btn.text = "%d\n%.1f" % [i + 1, cds[i]] + btn.disabled = true + elif gcd > 0.0: + btn.text = "%d\nGCD" % (i + 1) + btn.disabled = true + else: + btn.text = "%d" % (i + 1) + btn.disabled = false + +func _on_role_changed(player: Node, role: int) -> void: + if player != local_player and player != _find_local_player(): + return + if local_player == null: + local_player = player + match role: + GameState.ROLE_TANK: + role_label.text = "T" + role_icon.modulate = Color(0.3, 0.5, 0.95) + GameState.ROLE_DAMAGE: + role_label.text = "D" + role_icon.modulate = Color(0.95, 0.3, 0.3) + GameState.ROLE_HEALER: + role_label.text = "H" + role_icon.modulate = Color(0.4, 0.85, 0.4) + +func _on_entity_died(entity: Node) -> void: + if entity != local_player: + return + death_overlay.visible = true + death_label.text = "Respawning..." + +func _on_respawned(entity: Node) -> void: + if entity != local_player: + return + death_overlay.visible = false + +func _on_wave_started(wave: int) -> void: + wave_label.text = "Wave %d" % wave + +func _on_wave_tick(seconds: float) -> void: + var m := int(seconds) / 60 + var s := int(seconds) % 60 + timer_label.text = "%02d:%02d" % [m, s] + +func _on_village_damaged(current: float, max: float) -> void: + village_bar.max_value = max + village_bar.value = current + +func _on_village_destroyed() -> void: + game_over_overlay.visible = true + +func _on_invasion_started() -> void: + timer_label.modulate = Color(1.0, 0.4, 0.3) + +func _on_xp_gained(player: Node, _amount: float) -> void: + if player != local_player: + return + var xp: float = float(Stats.get_stat(player, "xp", 0.0)) + var to_next: float = float(Stats.get_stat(player, "xp_to_next", 50.0)) + xp_bar.max_value = to_next + xp_bar.value = xp + +func _on_level_up(player: Node, new_level: int) -> void: + if player != local_player: + return + level_label.text = "Lv %d" % new_level + _refresh_vitals() + +func _on_dialog_opened(player: Node, npc: Node) -> void: + if player != local_player: + return + dialog_panel.visible = true + dialog_npc_node = npc + dialog_npc.text = npc.profile.display_name + dialog_log.text = "[i]" + npc.profile.greeting + "[/i]\n" + dialog_input.text = "" + dialog_input.grab_focus() + _capture_ui(true) + +func _on_dialog_submitted(text: String) -> void: + if dialog_npc_node == null: + return + var dialog_sys: Node = _find_system("DialogSystem") + if dialog_sys == null: + return + dialog_log.append_text("[b]Du:[/b] " + text + "\n[i]...[/i]\n") + dialog_input.text = "" + dialog_sys.ask(dialog_npc_node, local_player, text) + +func show_answer(text: String) -> void: + if dialog_npc_node == null: + return + dialog_log.text = dialog_log.text.replace("[i]...[/i]\n", "") + dialog_log.append_text("[b]" + dialog_npc_node.profile.display_name + ":[/b] " + text + "\n") + +func _on_inventory_changed(player: Node) -> void: + if player != local_player: + return + _refresh_inventory() + +func _refresh_inventory() -> void: + for c in inventory_list.get_children(): + c.queue_free() + if local_player == null: + return + var inv_sys: Node = _find_system("InventorySystem") + if inv_sys == null: + return + var inv: Dictionary = inv_sys.get_inventory(local_player) + if inv.is_empty(): + var lbl := Label.new() + lbl.text = "(empty)" + inventory_list.add_child(lbl) + return + for k in inv.keys(): + var lbl := Label.new() + lbl.text = "%s: %d" % [str(k), inv[k]] + inventory_list.add_child(lbl) + +func _refresh_crafting() -> void: + for c in crafting_list.get_children(): + c.queue_free() + if local_player == null: + return + var c_sys: Node = _find_system("CraftingSystem") + var inv_sys: Node = _find_system("InventorySystem") + if c_sys == null or inv_sys == null: + return + for r in c_sys.get_recipes(): + var btn := Button.new() + var inputs_str: String = "" + for k in r.inputs.keys(): + inputs_str += "%s x%d " % [str(k), r.inputs[k]] + btn.text = "%s (%s)" % [r.name, inputs_str.strip_edges()] + btn.disabled = not c_sys.can_craft(local_player, r) + btn.pressed.connect(func(): c_sys.craft(local_player, r.id); _refresh_crafting()) + crafting_list.add_child(btn) + +func _populate_build_list() -> void: + for c in build_list.get_children(): + c.queue_free() + var b_sys: Node = _find_system("BuildingSystem") + if b_sys == null: + return + var bps: Array = b_sys.get_blueprints() + for i in range(bps.size()): + var btn := Button.new() + btn.text = "%d %s\n%s x%d" % [i + 1, bps[i].name, str(bps[i].material), bps[i].cost] + btn.toggle_mode = true + btn.button_pressed = (i == 0) + btn.pressed.connect(func(): _select_build(i)) + build_list.add_child(btn) + +func _select_build(idx: int) -> void: + build_selected = idx + var btns := build_list.get_children() + for i in range(btns.size()): + if btns[i] is Button: + (btns[i] as Button).button_pressed = (i == idx) + if build_preview: + _update_preview_mesh() + +func _toggle_build_mode() -> void: + build_active = not build_active + build_panel.visible = build_active + if local_player and local_player.has_method("set_build_mode"): + local_player.set_build_mode(build_active) + if build_active: + _create_build_preview() + elif build_preview: + build_preview.queue_free() + build_preview = null + +func _create_build_preview() -> void: + if build_preview: + build_preview.queue_free() + var world: Node = get_tree().current_scene + if world == null: + return + build_preview = MeshInstance3D.new() + build_preview.cast_shadow = MeshInstance3D.SHADOW_CASTING_SETTING_OFF + world.add_child(build_preview) + _update_preview_mesh() + +func _update_preview_mesh() -> void: + if build_preview == null: + return + var b_sys: Node = _find_system("BuildingSystem") + if b_sys == null: + return + var bp: Dictionary = b_sys.get_blueprints()[build_selected] + var box := BoxMesh.new() + box.size = bp.size + build_preview.mesh = box + var mat := StandardMaterial3D.new() + var c: Color = bp.color + c.a = 0.5 + mat.albedo_color = c + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.flags_unshaded = true + build_preview.material_override = mat + +func _update_build_preview() -> void: + if build_preview == null or local_player == null: + return + var b_sys: Node = _find_system("BuildingSystem") + if b_sys == null: + return + var pos: Vector3 = _ground_under_cursor() + var snapped: Vector3 = b_sys.snap_position(pos) + var bp: Dictionary = b_sys.get_blueprints()[build_selected] + build_preview.global_position = snapped + Vector3(0, bp.size.y * 0.5, 0) + build_preview.rotation.y = build_rotation + +func _ground_under_cursor() -> Vector3: + var cam: Camera3D = local_player.camera if local_player.has_method("get") else null + if cam == null: + return Vector3.ZERO + var mouse := get_viewport().get_mouse_position() + var from := cam.project_ray_origin(mouse) + var dir := cam.project_ray_normal(mouse) + if abs(dir.y) < 0.001: + return from + var t: float = -from.y / dir.y + if t <= 0: + return from + return from + dir * t + +func _try_place() -> void: + if local_player == null: + return + var b_sys: Node = _find_system("BuildingSystem") + if b_sys == null: + return + var bps: Array = b_sys.get_blueprints() + var bp: Dictionary = bps[build_selected] + var pos: Vector3 = _ground_under_cursor() + b_sys.place(local_player, bp.id, pos, build_rotation) + +func _try_remove() -> void: + if local_player == null: + return + var cam: Camera3D = local_player.camera if local_player.has_method("get") else null + if cam == null: + return + var mouse := get_viewport().get_mouse_position() + var from := cam.project_ray_origin(mouse) + var to := from + cam.project_ray_normal(mouse) * 100.0 + var space: PhysicsDirectSpaceState3D = local_player.get_world_3d().direct_space_state + var query := PhysicsRayQueryParameters3D.create(from, to) + query.collision_mask = 16 + var hit: Dictionary = space.intersect_ray(query) + if hit.is_empty(): + return + var node: Node = hit.collider + while node and not node.is_in_group("buildings"): + node = node.get_parent() + if node: + var b_sys: Node = _find_system("BuildingSystem") + if b_sys: + b_sys.remove(local_player, node.get_path()) + +func _on_chat(_peer_id: int, sender: String, text: String) -> void: + chat_log.append_text("[b]%s:[/b] %s\n" % [sender, text]) + +func _on_chat_submitted(text: String) -> void: + var c_sys: Node = _find_system("ChatSystem") + if c_sys: + c_sys.send(text) + chat_input.text = "" + chat_input.release_focus() + _capture_ui(_any_panel_visible()) + +func _toggle_pause() -> void: + pause_panel.visible = not pause_panel.visible + if pause_panel.visible and multiplayer.multiplayer_peer is OfflineMultiplayerPeer: + GameState.set_paused(true) + else: + GameState.set_paused(false) + _capture_ui(_any_panel_visible()) + +func _on_resume_pressed() -> void: + pause_panel.visible = false + GameState.set_paused(false) + _capture_ui(_any_panel_visible()) + +func _on_quit_pressed() -> void: + Net.disconnect_net() + GameState.set_paused(false) + GameState.change_scene(GameState.SCENE_MAIN_MENU) + +func _on_game_over_restart() -> void: + Net.disconnect_net() + GameState.set_paused(false) + GameState.change_scene(GameState.SCENE_MAIN_MENU) + +func _update_minimap() -> void: + _minimap_accum += get_process_delta_time() + if _minimap_accum < 0.20: + return + _minimap_accum = 0.0 + minimap_canvas.queue_redraw() + if map_panel.visible: + map_canvas.queue_redraw() + +func _find_system(name: String) -> Node: + var n: Node = get_tree().root.get_node_or_null("World/Systems/" + name) + if n == null: + n = get_tree().root.get_node_or_null("Dungeon/Systems/" + name) + return n diff --git a/scenes/hud/hud.gd.uid b/scenes/hud/hud.gd.uid new file mode 100644 index 0000000..7c21997 --- /dev/null +++ b/scenes/hud/hud.gd.uid @@ -0,0 +1 @@ +uid://dimp3y1yydhw2 diff --git a/scenes/hud/hud.tscn b/scenes/hud/hud.tscn index d24a6af..8ca3f44 100644 --- a/scenes/hud/hud.tscn +++ b/scenes/hud/hud.tscn @@ -1,237 +1,411 @@ -[gd_scene format=3] +[gd_scene load_steps=3 format=3 uid="uid://b0hud00001"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_active"] -bg_color = Color(0.2, 0.2, 0.2, 0.8) -border_width_bottom = 2 -border_width_left = 2 -border_width_right = 2 -border_width_top = 2 -border_color = Color(0.8, 0.8, 0.8, 1) +[ext_resource type="Script" path="res://scenes/hud/hud.gd" id="1"] +[ext_resource type="Script" path="res://scenes/hud/minimap.gd" id="2"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_round"] -bg_color = Color(0.2, 0.2, 0.2, 0.8) -border_width_bottom = 2 -border_width_left = 2 -border_width_right = 2 -border_width_top = 2 -border_color = Color(0.8, 0.8, 0.8, 1) -corner_radius_top_left = 22 -corner_radius_top_right = 22 -corner_radius_bottom_right = 22 -corner_radius_bottom_left = 22 +[node name="HUD" type="CanvasLayer"] +script = ExtResource("1") -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"] -bg_color = Color(0.3, 0.1, 0.1, 1) +[node name="VitalsPanel" type="PanelContainer" parent="."] +offset_left = 12.0 +offset_top = 12.0 +offset_right = 312.0 +offset_bottom = 130.0 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"] -bg_color = Color(0.2, 0.8, 0.2, 1) +[node name="VBox" type="VBoxContainer" parent="VitalsPanel"] +layout_mode = 2 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"] -bg_color = Color(0.1, 0.1, 0.3, 1) +[node name="HpRow" type="HBoxContainer" parent="VitalsPanel/VBox"] +layout_mode = 2 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"] -bg_color = Color(0.2, 0.5, 0.9, 1) - -[node name="HUD" type="CanvasLayer" groups=["hud"]] - -[node name="HealthBar" type="ProgressBar" parent="."] -offset_left = 10.0 -offset_top = 10.0 -offset_right = 210.0 -offset_bottom = 30.0 -theme_override_styles/background = SubResource("StyleBoxFlat_health_bg") -theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill") +[node name="HpBar" type="ProgressBar" parent="VitalsPanel/VBox/HpRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(220, 18) +layout_mode = 2 max_value = 100.0 value = 100.0 -show_percentage = false -[node name="HealthLabel" type="Label" parent="HealthBar"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_font_sizes/font_size = 12 +[node name="HpLabel" type="Label" parent="VitalsPanel/VBox/HpRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 text = "100/100" horizontal_alignment = 1 -vertical_alignment = 1 -[node name="ShieldBar" type="ProgressBar" parent="."] -offset_left = 10.0 -offset_top = 35.0 -offset_right = 210.0 -offset_bottom = 55.0 -theme_override_styles/background = SubResource("StyleBoxFlat_shield_bg") -theme_override_styles/fill = SubResource("StyleBoxFlat_shield_fill") +[node name="ShieldRow" type="HBoxContainer" parent="VitalsPanel/VBox"] +layout_mode = 2 + +[node name="ShieldBar" type="ProgressBar" parent="VitalsPanel/VBox/ShieldRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(220, 14) +layout_mode = 2 +max_value = 100.0 +value = 0.0 + +[node name="ShieldLabel" type="Label" parent="VitalsPanel/VBox/ShieldRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "0/0" +horizontal_alignment = 1 + +[node name="XpRow" type="HBoxContainer" parent="VitalsPanel/VBox"] +layout_mode = 2 + +[node name="LevelLabel" type="Label" parent="VitalsPanel/VBox/XpRow"] +unique_name_in_owner = true +layout_mode = 2 +text = "Lv 1" + +[node name="XpBar" type="ProgressBar" parent="VitalsPanel/VBox/XpRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(180, 12) +layout_mode = 2 +size_flags_horizontal = 3 max_value = 50.0 -value = 50.0 -show_percentage = false +value = 0.0 -[node name="ShieldLabel" type="Label" parent="ShieldBar"] +[node name="VillageRow" type="HBoxContainer" parent="VitalsPanel/VBox"] +layout_mode = 2 + +[node name="VillageLabelStatic" type="Label" parent="VitalsPanel/VBox/VillageRow"] +layout_mode = 2 +text = "Village" + +[node name="VillageBar" type="ProgressBar" parent="VitalsPanel/VBox/VillageRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(180, 12) +layout_mode = 2 +size_flags_horizontal = 3 +max_value = 1000.0 +value = 1000.0 + +[node name="WaveBox" type="PanelContainer" parent="."] +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -120.0 +offset_top = 12.0 +offset_right = 120.0 +offset_bottom = 64.0 + +[node name="VBox" type="VBoxContainer" parent="WaveBox"] +layout_mode = 2 + +[node name="WaveLabel" type="Label" parent="WaveBox/VBox"] +unique_name_in_owner = true +layout_mode = 2 +horizontal_alignment = 1 +text = "Wave 1" +theme_override_font_sizes/font_size = 18 + +[node name="WaveTimer" type="Label" parent="WaveBox/VBox"] +unique_name_in_owner = true +layout_mode = 2 +horizontal_alignment = 1 +text = "10:00" + +[node name="MinimapPanel" type="PanelContainer" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -212.0 +offset_top = 12.0 +offset_right = -12.0 +offset_bottom = 212.0 + +[node name="MinimapCanvas" type="Control" parent="MinimapPanel"] +unique_name_in_owner = true +layout_mode = 2 +script = ExtResource("2") +world_size = 80.0 + +[node name="AbilityBox" type="HBoxContainer" parent="."] +unique_name_in_owner = true +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = -200.0 +offset_top = -76.0 +offset_right = 280.0 +offset_bottom = -12.0 +theme_override_constants/separation = 6 + +[node name="RoleIcon" type="Panel" parent="AbilityBox"] +unique_name_in_owner = true +custom_minimum_size = Vector2(56, 56) +layout_mode = 2 + +[node name="RoleLabel" type="Label" parent="AbilityBox/RoleIcon"] +unique_name_in_owner = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +text = "D" +horizontal_alignment = 1 +vertical_alignment = 1 +theme_override_font_sizes/font_size = 28 + +[node name="A1" type="Button" parent="AbilityBox"] +custom_minimum_size = Vector2(56, 56) +layout_mode = 2 +text = "1" + +[node name="A2" type="Button" parent="AbilityBox"] +custom_minimum_size = Vector2(56, 56) +layout_mode = 2 +text = "2" + +[node name="A3" type="Button" parent="AbilityBox"] +custom_minimum_size = Vector2(56, 56) +layout_mode = 2 +text = "3" + +[node name="A4" type="Button" parent="AbilityBox"] +custom_minimum_size = Vector2(56, 56) +layout_mode = 2 +text = "4" + +[node name="ChatPanel" type="PanelContainer" parent="."] +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 12.0 +offset_top = -200.0 +offset_right = 360.0 +offset_bottom = -12.0 + +[node name="VBox" type="VBoxContainer" parent="ChatPanel"] +layout_mode = 2 + +[node name="ChatLog" type="RichTextLabel" parent="ChatPanel/VBox"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 140) +layout_mode = 2 +bbcode_enabled = true +scroll_following = true +fit_content = true + +[node name="ChatInput" type="LineEdit" parent="ChatPanel/VBox"] +unique_name_in_owner = true +layout_mode = 2 +placeholder_text = "Press Y to chat, Enter to send" + +[node name="DeathOverlay" type="Control" parent="."] +unique_name_in_owner = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Background" type="ColorRect" parent="DeathOverlay"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_font_sizes/font_size = 12 -text = "50/50" +color = Color(0.0, 0.0, 0.0, 0.5) + +[node name="DeathLabel" type="Label" parent="DeathOverlay"] +unique_name_in_owner = true +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -200.0 +offset_top = -40.0 +offset_right = 200.0 +offset_bottom = 40.0 +text = "Respawning..." horizontal_alignment = 1 vertical_alignment = 1 +theme_override_font_sizes/font_size = 32 -[node name="RespawnTimer" type="Label" parent="."] -anchors_preset = 8 +[node name="InventoryPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -50.0 -offset_top = -30.0 -offset_right = 50.0 -offset_bottom = 30.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_font_sizes/font_size = 48 -horizontal_alignment = 1 -vertical_alignment = 1 +offset_left = -200.0 +offset_top = -200.0 +offset_right = 200.0 +offset_bottom = 200.0 -[node name="AbilityBar" type="HBoxContainer" parent="."] -anchors_preset = 7 +[node name="VBox" type="VBoxContainer" parent="InventoryPanel"] +layout_mode = 2 + +[node name="Title" type="Label" parent="InventoryPanel/VBox"] +layout_mode = 2 +text = "Inventory (I)" +horizontal_alignment = 1 + +[node name="InventoryList" type="VBoxContainer" parent="InventoryPanel/VBox"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="CraftingPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true anchor_left = 0.5 -anchor_top = 1.0 +anchor_top = 0.5 anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -260.0 +offset_top = -240.0 +offset_right = 260.0 +offset_bottom = 240.0 + +[node name="VBox" type="VBoxContainer" parent="CraftingPanel"] +layout_mode = 2 + +[node name="Title" type="Label" parent="CraftingPanel/VBox"] +layout_mode = 2 +text = "Crafting (C)" +horizontal_alignment = 1 + +[node name="CraftingList" type="VBoxContainer" parent="CraftingPanel/VBox"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="BuildPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true +anchor_top = 1.0 anchor_bottom = 1.0 -offset_left = -130.0 -offset_top = -60.0 -offset_right = 130.0 -offset_bottom = -10.0 -grow_horizontal = 2 -grow_vertical = 0 -theme_override_constants/separation = 5 +offset_left = 12.0 +offset_top = -260.0 +offset_right = 360.0 +offset_bottom = -210.0 -[node name="ClassIcon" type="Panel" parent="AbilityBar"] -custom_minimum_size = Vector2(45, 45) -theme_override_styles/panel = SubResource("StyleBoxFlat_ability_round") +[node name="VBox" type="VBoxContainer" parent="BuildPanel"] +layout_mode = 2 -[node name="Label" type="Label" parent="AbilityBar/ClassIcon"] +[node name="Title" type="Label" parent="BuildPanel/VBox"] +layout_mode = 2 +text = "Build Mode (B)" +horizontal_alignment = 1 + +[node name="BuildList" type="HBoxContainer" parent="BuildPanel/VBox"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="DialogPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -300.0 +offset_top = -200.0 +offset_right = 300.0 +offset_bottom = 200.0 + +[node name="VBox" type="VBoxContainer" parent="DialogPanel"] +layout_mode = 2 + +[node name="DialogNpc" type="Label" parent="DialogPanel/VBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "NPC" +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 22 + +[node name="DialogLog" type="RichTextLabel" parent="DialogPanel/VBox"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 280) +layout_mode = 2 +bbcode_enabled = true +scroll_following = true +fit_content = true + +[node name="DialogInput" type="LineEdit" parent="DialogPanel/VBox"] +unique_name_in_owner = true +layout_mode = 2 +placeholder_text = "Frage stellen, Enter senden" + +[node name="MapPanel" type="Control" parent="."] +unique_name_in_owner = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Background" type="ColorRect" parent="MapPanel"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_font_sizes/font_size = 20 -text = "D" +color = Color(0.0, 0.0, 0.0, 0.7) + +[node name="MapCanvas" type="Control" parent="MapPanel"] +unique_name_in_owner = true +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -300.0 +offset_top = -300.0 +offset_right = 300.0 +offset_bottom = 300.0 +script = ExtResource("2") +world_size = 200.0 +draw_labels = true + +[node name="PausePanel" type="PanelContainer" parent="."] +unique_name_in_owner = true +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -150.0 +offset_top = -120.0 +offset_right = 150.0 +offset_bottom = 120.0 + +[node name="VBox" type="VBoxContainer" parent="PausePanel"] +layout_mode = 2 + +[node name="Title" type="Label" parent="PausePanel/VBox"] +layout_mode = 2 +text = "Paused" horizontal_alignment = 1 -vertical_alignment = 1 +theme_override_font_sizes/font_size = 28 -[node name="Ability1" type="Panel" parent="AbilityBar"] -custom_minimum_size = Vector2(45, 45) -theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active") +[node name="Resume" type="Button" parent="PausePanel/VBox"] +layout_mode = 2 +text = "Resume" -[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability1"] -layout_mode = 1 +[node name="QuitToMenu" type="Button" parent="PausePanel/VBox"] +layout_mode = 2 +text = "Quit to Menu" + +[node name="GameOverOverlay" type="Control" parent="."] +unique_name_in_owner = true +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -color = Color(0, 0, 0, 0.6) -visible = false -[node name="Label" type="Label" parent="AbilityBar/Ability1"] +[node name="Background" type="ColorRect" parent="GameOverOverlay"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -text = "1" +color = Color(0.2, 0.0, 0.0, 0.8) + +[node name="VBox" type="VBoxContainer" parent="GameOverOverlay"] +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -200.0 +offset_top = -100.0 +offset_right = 200.0 +offset_bottom = 100.0 + +[node name="Title" type="Label" parent="GameOverOverlay/VBox"] +layout_mode = 2 +text = "GAME OVER" horizontal_alignment = 1 -vertical_alignment = 1 +theme_override_font_sizes/font_size = 48 -[node name="Ability2" type="Panel" parent="AbilityBar"] -custom_minimum_size = Vector2(45, 45) -theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active") +[node name="Restart" type="Button" parent="GameOverOverlay/VBox"] +layout_mode = 2 +text = "Back to Main Menu" -[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability2"] -layout_mode = 1 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0, 0, 0, 0.6) -visible = false - -[node name="Label" type="Label" parent="AbilityBar/Ability2"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -text = "2" -horizontal_alignment = 1 -vertical_alignment = 1 - -[node name="Ability3" type="Panel" parent="AbilityBar"] -custom_minimum_size = Vector2(45, 45) -theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active") - -[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability3"] -layout_mode = 1 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0, 0, 0, 0.6) -visible = false - -[node name="Label" type="Label" parent="AbilityBar/Ability3"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -text = "3" -horizontal_alignment = 1 -vertical_alignment = 1 - -[node name="Ability4" type="Panel" parent="AbilityBar"] -custom_minimum_size = Vector2(45, 45) -theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active") - -[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability4"] -layout_mode = 1 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0, 0, 0, 0.6) -visible = false - -[node name="Label" type="Label" parent="AbilityBar/Ability4"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -text = "4" -horizontal_alignment = 1 -vertical_alignment = 1 - -[node name="Ability5" type="Panel" parent="AbilityBar"] -custom_minimum_size = Vector2(45, 45) -theme_override_styles/panel = SubResource("StyleBoxFlat_ability_round") - -[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability5"] -layout_mode = 1 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0, 0, 0, 0.6) -visible = false - -[node name="Label" type="Label" parent="AbilityBar/Ability5"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -text = "P" -horizontal_alignment = 1 -vertical_alignment = 1 +[connection signal="pressed" from="PausePanel/VBox/Resume" to="." method="_on_resume_pressed"] +[connection signal="pressed" from="PausePanel/VBox/QuitToMenu" to="." method="_on_quit_pressed"] +[connection signal="pressed" from="GameOverOverlay/VBox/Restart" to="." method="_on_game_over_restart"] diff --git a/scenes/hud/minimap.gd b/scenes/hud/minimap.gd new file mode 100644 index 0000000..6a727b9 --- /dev/null +++ b/scenes/hud/minimap.gd @@ -0,0 +1,40 @@ +extends Control + +@export var world_size: float = 200.0 +@export var label_text: String = "" +@export var draw_labels: bool = false + +func _ready() -> void: + set_process(false) + +func _draw() -> void: + var view_size: Vector2 = get_size() + draw_rect(Rect2(Vector2.ZERO, view_size), Color(0.05, 0.05, 0.08, 0.85)) + draw_rect(Rect2(Vector2.ZERO, view_size), Color(0.6, 0.6, 0.6, 0.5), false, 2.0) + var local: Node = _local_player() + var center_offset: Vector2 = Vector2.ZERO + if local: + center_offset = Vector2((local as Node3D).global_position.x, (local as Node3D).global_position.z) + var map_sys: Node = get_tree().root.get_node_or_null("World/Systems/MapSystem") + if map_sys == null: + return + for entry in map_sys.get_marker_data(): + var pos: Vector3 = entry.pos + var px: Vector2 = _to_pixel(Vector2(pos.x, pos.z), view_size, center_offset) + if px.x < 0 or px.x > view_size.x or px.y < 0 or px.y > view_size.y: + continue + draw_circle(px, 3.0, entry.color) + if draw_labels and entry.label != "": + var f: Font = ThemeDB.fallback_font + draw_string(f, px + Vector2(5, -5), entry.label, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, Color.WHITE) + +func _to_pixel(world: Vector2, view_size: Vector2, center: Vector2) -> Vector2: + var rel: Vector2 = world - center + var s: float = view_size.x / world_size + return view_size * 0.5 + rel * s + +func _local_player() -> Node: + for p in get_tree().get_nodes_in_group("player"): + if p.is_multiplayer_authority(): + return p + return null diff --git a/scenes/hud/minimap.gd.uid b/scenes/hud/minimap.gd.uid new file mode 100644 index 0000000..0ade730 --- /dev/null +++ b/scenes/hud/minimap.gd.uid @@ -0,0 +1 @@ +uid://bt0i0umsod51f diff --git a/scenes/menu/game_over_overlay.gd b/scenes/menu/game_over_overlay.gd deleted file mode 100644 index 707f167..0000000 --- a/scenes/menu/game_over_overlay.gd +++ /dev/null @@ -1,21 +0,0 @@ -extends CanvasLayer - -@onready var label: Label = $Center/VBox/Label -@onready var button: Button = $Center/VBox/Button - -func _ready() -> void: - visible = false - button.pressed.connect(_on_button) - -func show_overlay(wave: int) -> void: - label.text = "GAME OVER — Welle %d erreicht" % wave - visible = true - -func _on_button() -> void: - GameState.reset() - PlayerData.reset_run() - EnemyData.entities.clear() - BossData.entities.clear() - PortalData.entities.clear() - TavernData.entities.clear() - get_tree().change_scene_to_file("res://scenes/menu/main_menu.tscn") diff --git a/scenes/menu/game_over_overlay.gd.uid b/scenes/menu/game_over_overlay.gd.uid deleted file mode 100644 index a77f403..0000000 --- a/scenes/menu/game_over_overlay.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dm00anoh5wtyu diff --git a/scenes/menu/game_over_overlay.tscn b/scenes/menu/game_over_overlay.tscn deleted file mode 100644 index 20a2c11..0000000 --- a/scenes/menu/game_over_overlay.tscn +++ /dev/null @@ -1,41 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="res://scenes/menu/game_over_overlay.gd" id="1"] - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button"] -bg_color = Color(0.2, 0.2, 0.25, 0.9) -border_width_bottom = 2 -border_width_left = 2 -border_width_right = 2 -border_width_top = 2 -border_color = Color(0.7, 0.7, 0.7, 1) - -[node name="GameOverOverlay" type="CanvasLayer"] -layer = 10 -script = ExtResource("1") - -[node name="Background" type="ColorRect" parent="."] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0, 0, 0, 0.75) - -[node name="Center" type="CenterContainer" parent="."] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="VBox" type="VBoxContainer" parent="Center"] -custom_minimum_size = Vector2(400, 0) -theme_override_constants/separation = 30 - -[node name="Label" type="Label" parent="Center/VBox"] -text = "GAME OVER" -horizontal_alignment = 1 -theme_override_font_sizes/font_size = 48 -theme_override_colors/font_color = Color(1, 0.3, 0.3, 1) - -[node name="Button" type="Button" parent="Center/VBox"] -custom_minimum_size = Vector2(0, 48) -text = "Zurück zum Menü" -theme_override_styles/normal = SubResource("StyleBoxFlat_button") diff --git a/scenes/menu/lobby.gd b/scenes/menu/lobby.gd new file mode 100644 index 0000000..47f9d8d --- /dev/null +++ b/scenes/menu/lobby.gd @@ -0,0 +1,55 @@ +extends Control + +@onready var player_list: ItemList = %PlayerList +@onready var status_label: Label = %StatusLabel +@onready var start_button: Button = %StartButton + +func _ready() -> void: + Net.peer_connected.connect(_refresh) + Net.peer_disconnected.connect(_refresh) + Net.server_disconnected.connect(_on_disconnect) + start_button.visible = Net.is_host() + if Net.is_host(): + status_label.text = "Hosting on port %d — press Start when ready" % Net.DEFAULT_PORT + else: + status_label.text = "Joined — waiting for host to start" + _refresh(0) + +func _refresh(_id: int) -> void: + player_list.clear() + var ids: Array = [] + if multiplayer.multiplayer_peer == null: + ids.append(1) + else: + ids.append(multiplayer.get_unique_id()) + for p in multiplayer.get_peers(): + if not p in ids: + ids.append(p) + ids.sort() + for id in ids: + var n: String = Net.player_names.get(id, "Player %d" % id) + player_list.add_item("[%d] %s" % [id, n]) + +func _on_start_pressed() -> void: + if not Net.is_host(): + return + _start_world.rpc() + +@rpc("authority", "call_local", "reliable") +func _start_world() -> void: + GameState.change_scene(GameState.SCENE_WORLD) + +var _leaving: bool = false + +func _on_back_pressed() -> void: + if _leaving: + return + _leaving = true + Net.disconnect_net() + GameState.change_scene(GameState.SCENE_MAIN_MENU) + +func _on_disconnect() -> void: + if _leaving: + return + _leaving = true + GameState.change_scene(GameState.SCENE_MAIN_MENU) diff --git a/scenes/menu/lobby.gd.uid b/scenes/menu/lobby.gd.uid new file mode 100644 index 0000000..aaa62d4 --- /dev/null +++ b/scenes/menu/lobby.gd.uid @@ -0,0 +1 @@ +uid://d3gurtlgr2a6o diff --git a/scenes/menu/lobby.tscn b/scenes/menu/lobby.tscn new file mode 100644 index 0000000..131e374 --- /dev/null +++ b/scenes/menu/lobby.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=2 format=3 uid="uid://b0lobby0001"] + +[ext_resource type="Script" path="res://scenes/menu/lobby.gd" id="1"] + +[node name="Lobby" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1") + +[node name="Background" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.08, 0.08, 0.12, 1) + +[node name="Center" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="VBox" type="VBoxContainer" parent="Center"] +layout_mode = 2 +custom_minimum_size = Vector2(420, 0) + +[node name="Title" type="Label" parent="Center/VBox"] +layout_mode = 2 +text = "Lobby" +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 36 + +[node name="StatusLabel" type="Label" parent="Center/VBox"] +unique_name_in_owner = true +layout_mode = 2 +horizontal_alignment = 1 +text = "" + +[node name="PlayerList" type="ItemList" parent="Center/VBox"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(0, 180) + +[node name="StartButton" type="Button" parent="Center/VBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "Start" +custom_minimum_size = Vector2(0, 40) + +[node name="BackButton" type="Button" parent="Center/VBox"] +layout_mode = 2 +text = "Back" +custom_minimum_size = Vector2(0, 40) + +[connection signal="pressed" from="Center/VBox/StartButton" to="." method="_on_start_pressed"] +[connection signal="pressed" from="Center/VBox/BackButton" to="." method="_on_back_pressed"] diff --git a/scenes/menu/main_menu.gd b/scenes/menu/main_menu.gd index f430346..34d2f72 100644 --- a/scenes/menu/main_menu.gd +++ b/scenes/menu/main_menu.gd @@ -1,32 +1,47 @@ -extends CanvasLayer +extends Control -@onready var singleplayer_button: Button = $Center/VBox/SingleplayerButton -@onready var host_button: Button = $Center/VBox/HostButton -@onready var join_button: Button = $Center/VBox/JoinButton -@onready var quit_button: Button = $Center/VBox/QuitButton +@onready var name_input: LineEdit = %NameInput +@onready var ip_input: LineEdit = %IpInput +@onready var status_label: Label = %StatusLabel func _ready() -> void: - singleplayer_button.pressed.connect(_on_singleplayer) - host_button.pressed.connect(_on_host) - join_button.pressed.connect(_on_join) - quit_button.pressed.connect(_on_quit) - host_button.disabled = true - join_button.disabled = true + name_input.text = Net.local_name + Net.connected_to_server.connect(_on_connected) + Net.connection_failed.connect(_on_failed) -func _on_singleplayer() -> void: - GameState.reset() - PlayerData.reset_run() - EnemyData.entities.clear() - BossData.entities.clear() - PortalData.entities.clear() - TavernData.entities.clear() - get_tree().change_scene_to_file("res://scenes/world/world.tscn") +func _on_singleplayer_pressed() -> void: + Net.local_name = name_input.text.strip_edges() if name_input.text.strip_edges() != "" else "Player" + GameState.reset_run() + if Net.host_singleplayer(): + GameState.change_scene(GameState.SCENE_WORLD) -func _on_host() -> void: - pass +func _on_host_pressed() -> void: + Net.local_name = name_input.text.strip_edges() if name_input.text.strip_edges() != "" else "Host" + GameState.reset_run() + if Net.host(): + status_label.text = "Hosting on port %d" % Net.DEFAULT_PORT + GameState.change_scene(GameState.SCENE_LOBBY) + else: + status_label.text = "Hosting failed." -func _on_join() -> void: - pass +func _on_join_pressed() -> void: + Net.local_name = name_input.text.strip_edges() if name_input.text.strip_edges() != "" else "Client" + var addr := ip_input.text.strip_edges() + if addr == "": + addr = "127.0.0.1" + status_label.text = "Connecting to %s..." % addr + if not Net.join(addr): + status_label.text = "Join failed." -func _on_quit() -> void: +func _on_options_pressed() -> void: + GameState.change_scene(GameState.SCENE_OPTIONS) + +func _on_quit_pressed() -> void: get_tree().quit() + +func _on_connected() -> void: + status_label.text = "Connected." + GameState.change_scene(GameState.SCENE_LOBBY) + +func _on_failed() -> void: + status_label.text = "Connection failed." diff --git a/scenes/menu/main_menu.gd.uid b/scenes/menu/main_menu.gd.uid index ac165e6..d209911 100644 --- a/scenes/menu/main_menu.gd.uid +++ b/scenes/menu/main_menu.gd.uid @@ -1 +1 @@ -uid://b4m6byh4k2mg7 +uid://d0k5a5qreg1uu diff --git a/scenes/menu/main_menu.tscn b/scenes/menu/main_menu.tscn index 1e7b6ff..7a4ec5a 100644 --- a/scenes/menu/main_menu.tscn +++ b/scenes/menu/main_menu.tscn @@ -1,57 +1,115 @@ -[gd_scene format=3] +[gd_scene format=3 uid="uid://bm5gsrlveyvnk"] -[ext_resource type="Script" path="res://scenes/menu/main_menu.gd" id="1"] +[ext_resource type="Script" uid="uid://d0k5a5qreg1uu" path="res://scenes/menu/main_menu.gd" id="1"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button"] -bg_color = Color(0.2, 0.2, 0.25, 0.9) -border_width_bottom = 2 -border_width_left = 2 -border_width_right = 2 -border_width_top = 2 -border_color = Color(0.7, 0.7, 0.7, 1) - -[node name="MainMenu" type="CanvasLayer"] +[node name="MainMenu" type="Control" unique_id=282057399] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 script = ExtResource("1") -[node name="Background" type="ColorRect" parent="."] +[node name="Background" type="ColorRect" parent="." unique_id=1892952438] +layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -color = Color(0.08, 0.08, 0.12, 1) +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.1, 0.1, 0.15, 1) -[node name="Center" type="CenterContainer" parent="."] +[node name="Center" type="CenterContainer" parent="." unique_id=303933157] +layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 -[node name="VBox" type="VBoxContainer" parent="Center"] -custom_minimum_size = Vector2(320, 0) -theme_override_constants/separation = 20 +[node name="VBox" type="VBoxContainer" parent="Center" unique_id=576802246] +custom_minimum_size = Vector2(420, 0) +layout_mode = 2 -[node name="Title" type="Label" parent="Center/VBox"] +[node name="Title" type="Label" parent="Center/VBox" unique_id=1058778255] +layout_mode = 2 +theme_override_font_sizes/font_size = 56 text = "MMO" horizontal_alignment = 1 -theme_override_font_sizes/font_size = 64 -[node name="Spacer" type="Control" parent="Center/VBox"] +[node name="Spacer1" type="Control" parent="Center/VBox" unique_id=707057681] +custom_minimum_size = Vector2(0, 24) +layout_mode = 2 + +[node name="NameRow" type="HBoxContainer" parent="Center/VBox" unique_id=1693567332] +layout_mode = 2 + +[node name="NameLabel" type="Label" parent="Center/VBox/NameRow" unique_id=1012504876] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Name:" + +[node name="NameInput" type="LineEdit" parent="Center/VBox/NameRow" unique_id=318387796] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Player" + +[node name="IpRow" type="HBoxContainer" parent="Center/VBox" unique_id=674693362] +layout_mode = 2 + +[node name="IpLabel" type="Label" parent="Center/VBox/IpRow" unique_id=222908556] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Server IP:" + +[node name="IpInput" type="LineEdit" parent="Center/VBox/IpRow" unique_id=99390081] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "127.0.0.1" + +[node name="Spacer2" type="Control" parent="Center/VBox" unique_id=979762154] +custom_minimum_size = Vector2(0, 16) +layout_mode = 2 + +[node name="Singleplayer" type="Button" parent="Center/VBox" unique_id=156537849] custom_minimum_size = Vector2(0, 40) - -[node name="SingleplayerButton" type="Button" parent="Center/VBox"] -custom_minimum_size = Vector2(0, 48) +layout_mode = 2 text = "Singleplayer" -theme_override_styles/normal = SubResource("StyleBoxFlat_button") -[node name="HostButton" type="Button" parent="Center/VBox"] -custom_minimum_size = Vector2(0, 48) -text = "Host (bald)" -theme_override_styles/normal = SubResource("StyleBoxFlat_button") +[node name="Host" type="Button" parent="Center/VBox" unique_id=1117468477] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +text = "Host" -[node name="JoinButton" type="Button" parent="Center/VBox"] -custom_minimum_size = Vector2(0, 48) -text = "Join (bald)" -theme_override_styles/normal = SubResource("StyleBoxFlat_button") +[node name="Join" type="Button" parent="Center/VBox" unique_id=1669660415] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +text = "Join" -[node name="QuitButton" type="Button" parent="Center/VBox"] -custom_minimum_size = Vector2(0, 48) +[node name="Options" type="Button" parent="Center/VBox" unique_id=1739276223] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +text = "Options" + +[node name="Quit" type="Button" parent="Center/VBox" unique_id=182300116] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 text = "Quit" -theme_override_styles/normal = SubResource("StyleBoxFlat_button") + +[node name="Spacer3" type="Control" parent="Center/VBox" unique_id=1580062158] +custom_minimum_size = Vector2(0, 16) +layout_mode = 2 + +[node name="StatusLabel" type="Label" parent="Center/VBox" unique_id=643981255] +unique_name_in_owner = true +layout_mode = 2 +horizontal_alignment = 1 + +[connection signal="pressed" from="Center/VBox/Singleplayer" to="." method="_on_singleplayer_pressed"] +[connection signal="pressed" from="Center/VBox/Host" to="." method="_on_host_pressed"] +[connection signal="pressed" from="Center/VBox/Join" to="." method="_on_join_pressed"] +[connection signal="pressed" from="Center/VBox/Options" to="." method="_on_options_pressed"] +[connection signal="pressed" from="Center/VBox/Quit" to="." method="_on_quit_pressed"] diff --git a/scenes/menu/options_menu.gd b/scenes/menu/options_menu.gd new file mode 100644 index 0000000..6aa7fa8 --- /dev/null +++ b/scenes/menu/options_menu.gd @@ -0,0 +1,49 @@ +extends Control + +const SETTINGS_PATH: String = "user://settings.cfg" + +@onready var master: HSlider = %MasterSlider +@onready var music: HSlider = %MusicSlider +@onready var sfx: HSlider = %SfxSlider +@onready var sens: HSlider = %SensSlider + +func _ready() -> void: + load_settings() + master.value_changed.connect(_on_master_changed) + music.value_changed.connect(_on_music_changed) + sfx.value_changed.connect(_on_sfx_changed) + +func load_settings() -> void: + var cfg := ConfigFile.new() + if cfg.load(SETTINGS_PATH) == OK: + master.value = cfg.get_value("audio", "master", 1.0) + music.value = cfg.get_value("audio", "music", 1.0) + sfx.value = cfg.get_value("audio", "sfx", 1.0) + sens.value = cfg.get_value("input", "sens", 1.0) + _apply_audio() + +func save_settings() -> void: + var cfg := ConfigFile.new() + cfg.set_value("audio", "master", master.value) + cfg.set_value("audio", "music", music.value) + cfg.set_value("audio", "sfx", sfx.value) + cfg.set_value("input", "sens", sens.value) + cfg.save(SETTINGS_PATH) + +func _on_master_changed(_v: float) -> void: + _apply_audio() + save_settings() + +func _on_music_changed(_v: float) -> void: + _apply_audio() + save_settings() + +func _on_sfx_changed(_v: float) -> void: + _apply_audio() + save_settings() + +func _apply_audio() -> void: + AudioServer.set_bus_volume_db(0, linear_to_db(max(0.001, master.value))) + +func _on_back_pressed() -> void: + GameState.change_scene(GameState.SCENE_MAIN_MENU) diff --git a/scenes/menu/options_menu.gd.uid b/scenes/menu/options_menu.gd.uid new file mode 100644 index 0000000..6aac01d --- /dev/null +++ b/scenes/menu/options_menu.gd.uid @@ -0,0 +1 @@ +uid://fpukylp3yy1c diff --git a/scenes/menu/options_menu.tscn b/scenes/menu/options_menu.tscn new file mode 100644 index 0000000..49cc432 --- /dev/null +++ b/scenes/menu/options_menu.tscn @@ -0,0 +1,108 @@ +[gd_scene load_steps=2 format=3 uid="uid://b0options0001"] + +[ext_resource type="Script" path="res://scenes/menu/options_menu.gd" id="1"] + +[node name="OptionsMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1") + +[node name="Background" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.1, 0.1, 0.15, 1) + +[node name="Center" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="VBox" type="VBoxContainer" parent="Center"] +layout_mode = 2 +custom_minimum_size = Vector2(420, 0) + +[node name="Title" type="Label" parent="Center/VBox"] +layout_mode = 2 +text = "Options" +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 36 + +[node name="MasterRow" type="HBoxContainer" parent="Center/VBox"] +layout_mode = 2 + +[node name="MasterLabel" type="Label" parent="Center/VBox/MasterRow"] +layout_mode = 2 +custom_minimum_size = Vector2(110, 0) +text = "Master" + +[node name="MasterSlider" type="HSlider" parent="Center/VBox/MasterRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 0.0 +max_value = 1.0 +step = 0.05 +value = 1.0 + +[node name="MusicRow" type="HBoxContainer" parent="Center/VBox"] +layout_mode = 2 + +[node name="MusicLabel" type="Label" parent="Center/VBox/MusicRow"] +layout_mode = 2 +custom_minimum_size = Vector2(110, 0) +text = "Music" + +[node name="MusicSlider" type="HSlider" parent="Center/VBox/MusicRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 0.0 +max_value = 1.0 +step = 0.05 +value = 1.0 + +[node name="SfxRow" type="HBoxContainer" parent="Center/VBox"] +layout_mode = 2 + +[node name="SfxLabel" type="Label" parent="Center/VBox/SfxRow"] +layout_mode = 2 +custom_minimum_size = Vector2(110, 0) +text = "SFX" + +[node name="SfxSlider" type="HSlider" parent="Center/VBox/SfxRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 0.0 +max_value = 1.0 +step = 0.05 +value = 1.0 + +[node name="SensRow" type="HBoxContainer" parent="Center/VBox"] +layout_mode = 2 + +[node name="SensLabel" type="Label" parent="Center/VBox/SensRow"] +layout_mode = 2 +custom_minimum_size = Vector2(110, 0) +text = "Mouse Sens" + +[node name="SensSlider" type="HSlider" parent="Center/VBox/SensRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 0.1 +max_value = 3.0 +step = 0.1 +value = 1.0 + +[node name="Back" type="Button" parent="Center/VBox"] +layout_mode = 2 +text = "Back" +custom_minimum_size = Vector2(0, 40) + +[connection signal="pressed" from="Center/VBox/Back" to="." method="_on_back_pressed"] diff --git a/scenes/player/ability.gd b/scenes/player/ability.gd deleted file mode 100644 index 28f1d87..0000000 --- a/scenes/player/ability.gd +++ /dev/null @@ -1,9 +0,0 @@ -extends Node - -@onready var player: CharacterBody3D = get_parent() - -func _unhandled_input(event: InputEvent) -> void: - for i in range(5): - if event.is_action_pressed("ability_%s" % (i + 1)): - EventBus.ability_use.emit(player, i) - return diff --git a/scenes/player/ability.gd.uid b/scenes/player/ability.gd.uid deleted file mode 100644 index 37b43ab..0000000 --- a/scenes/player/ability.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://hh5yw7vcjdqr diff --git a/scenes/player/camera.gd b/scenes/player/camera.gd deleted file mode 100644 index a01980b..0000000 --- a/scenes/player/camera.gd +++ /dev/null @@ -1,30 +0,0 @@ -extends Node3D - -const SENSITIVITY := 0.003 -const PITCH_MIN := -80.0 -const PITCH_MAX := 80.0 - -var pitch := -0.3 -var camera_yaw := 0.0 -var player_yaw := -2.356 - -func _ready() -> void: - get_parent().rotation.y = player_yaw - rotation = Vector3(pitch, camera_yaw, 0) - -func _unhandled_input(event: InputEvent) -> void: - if event is InputEventMouseMotion: - var lmb := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) - var rmb := Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) - - if lmb or rmb: - pitch -= event.relative.y * SENSITIVITY - pitch = clamp(pitch, deg_to_rad(PITCH_MIN), deg_to_rad(PITCH_MAX)) - - if rmb: - player_yaw -= event.relative.x * SENSITIVITY - get_parent().rotation.y = player_yaw - else: - camera_yaw -= event.relative.x * SENSITIVITY - - rotation = Vector3(pitch, camera_yaw, 0) diff --git a/scenes/player/camera.gd.uid b/scenes/player/camera.gd.uid deleted file mode 100644 index 7319a55..0000000 --- a/scenes/player/camera.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cohjyjge1kqxb diff --git a/scenes/player/init.gd b/scenes/player/init.gd deleted file mode 100644 index 16d0b11..0000000 --- a/scenes/player/init.gd +++ /dev/null @@ -1,59 +0,0 @@ -extends CharacterBody3D - -@export var stats: PlayerStats - -var anim_player: AnimationPlayer = null -var current_anim: String = "" -var attack_lock_until: float = 0.0 - -func _ready() -> void: - add_to_group("player") - PlayerData.init_from_resource(stats) - if PlayerData.returning_from_dungeon: - global_position = PlayerData.portal_position + Vector3(0, 1, -5) - PlayerData.returning_from_dungeon = false - elif PlayerData.dungeon_cleared: - PlayerData.clear_cache() - anim_player = get_node_or_null("Mesh/Model/AnimationPlayer") - _play_anim("Idle") - EventBus.attack_executed.connect(_on_attack_executed) - EventBus.entity_died.connect(_on_entity_died) - EventBus.player_respawned.connect(_on_player_respawned) - -func _process(_delta: float) -> void: - if not PlayerData.alive: - return - var now: float = Time.get_ticks_msec() / 1000.0 - if now < attack_lock_until: - return - if velocity.length() > 0.1: - _play_anim("Running_A") - else: - _play_anim("Idle") - -func _play_anim(anim_name: String, loop: bool = true) -> void: - if not anim_player: - return - if current_anim == anim_name: - return - if not anim_player.has_animation(anim_name): - return - var anim: Animation = anim_player.get_animation(anim_name) - if anim: - anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE - anim_player.play(anim_name) - current_anim = anim_name - -func _on_attack_executed(attacker: Node, _pos: Vector3, _dir: Vector3, _damage: float) -> void: - if attacker != self: - return - _play_anim("1H_Melee_Attack_Chop", false) - attack_lock_until = Time.get_ticks_msec() / 1000.0 + 0.5 - -func _on_entity_died(entity: Node) -> void: - if entity != PlayerData: - return - _play_anim("Death_A", false) - -func _on_player_respawned(_player: Node) -> void: - _play_anim("Idle") diff --git a/scenes/player/init.gd.uid b/scenes/player/init.gd.uid deleted file mode 100644 index 9e283c7..0000000 --- a/scenes/player/init.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cx6k5473yxno diff --git a/scenes/player/movement.gd b/scenes/player/movement.gd deleted file mode 100644 index ffeafab..0000000 --- a/scenes/player/movement.gd +++ /dev/null @@ -1,37 +0,0 @@ -extends Node - -var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") - -@onready var player: CharacterBody3D = get_parent() - -func _physics_process(delta: float) -> void: - if not player.is_on_floor(): - player.velocity.y -= gravity * delta - - if Input.is_action_just_pressed("ui_accept") and player.is_on_floor(): - player.velocity.y = PlayerData.jump_velocity - - var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back") - var camera_pivot := player.get_node("CameraPivot") as Node3D - var forward := -camera_pivot.global_transform.basis.z - forward.y = 0 - forward = forward.normalized() - var right := camera_pivot.global_transform.basis.x - right.y = 0 - right = right.normalized() - - var direction := (forward * -input_dir.y + right * input_dir.x).normalized() - - if direction: - player.velocity.x = direction.x * PlayerData.speed - player.velocity.z = direction.z * PlayerData.speed - var world_yaw: float = atan2(-direction.x, -direction.z) - var local_yaw: float = world_yaw - player.rotation.y - var mesh: Node3D = player.get_node_or_null("Mesh") as Node3D - if mesh: - mesh.rotation.y = lerp_angle(mesh.rotation.y, local_yaw, 10.0 * delta) - else: - player.velocity.x = move_toward(player.velocity.x, 0, PlayerData.speed) - player.velocity.z = move_toward(player.velocity.z, 0, PlayerData.speed) - - player.move_and_slide() diff --git a/scenes/player/movement.gd.uid b/scenes/player/movement.gd.uid deleted file mode 100644 index 2e48f19..0000000 --- a/scenes/player/movement.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://fg87dh8fbc8 diff --git a/scenes/player/player.tscn b/scenes/player/player.tscn deleted file mode 100644 index b828482..0000000 --- a/scenes/player/player.tscn +++ /dev/null @@ -1,45 +0,0 @@ -[gd_scene format=3 uid="uid://cdnkbt1f0db7e"] - -[ext_resource type="Script" uid="uid://cx6k5473yxno" path="res://scenes/player/init.gd" id="1"] -[ext_resource type="Script" uid="uid://cohjyjge1kqxb" path="res://scenes/player/camera.gd" id="2"] -[ext_resource type="Script" uid="uid://fg87dh8fbc8" path="res://scenes/player/movement.gd" id="3"] -[ext_resource type="Script" uid="uid://hh5yw7vcjdqr" path="res://scenes/player/ability.gd" id="4"] -[ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scenes/player/targeting.gd" id="8"] -[ext_resource type="Script" uid="uid://dhomrampxola4" path="res://scenes/player/role/role.gd" id="10"] -[ext_resource type="Resource" uid="uid://btd0g0oiulssq" path="res://scenes/player/player_stats.tres" id="14"] -[ext_resource type="PackedScene" path="res://assets/models/characters/Knight.glb" id="15"] - -[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"] -radius = 0.3 -height = 1.8 - -[node name="Player" type="CharacterBody3D" unique_id=197716516] -script = ExtResource("1") -stats = ExtResource("14") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=311205642] -shape = SubResource("CapsuleShape3D_1") - -[node name="Mesh" type="Node3D" parent="." unique_id=1514179122] - -[node name="Model" parent="Mesh" instance=ExtResource("15")] -transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, -0.9, 0) - -[node name="CameraPivot" type="Node3D" parent="." unique_id=1881685457] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) -script = ExtResource("2") - -[node name="Camera3D" type="Camera3D" parent="CameraPivot" unique_id=2062990383] -transform = Transform3D(1, 0, 0, 0, 0.966, 0.259, 0, -0.259, 0.966, 0, 2, 5) - -[node name="Movement" type="Node" parent="." unique_id=811179177] -script = ExtResource("3") - -[node name="Ability" type="Node" parent="." unique_id=1184596245] -script = ExtResource("4") - -[node name="Targeting" type="Node" parent="." unique_id=1974574662] -script = ExtResource("8") - -[node name="Role" type="Node" parent="." unique_id=1637643687] -script = ExtResource("10") diff --git a/scenes/player/player_stats.gd b/scenes/player/player_stats.gd deleted file mode 100644 index 63e7769..0000000 --- a/scenes/player/player_stats.gd +++ /dev/null @@ -1,10 +0,0 @@ -extends BaseStats -class_name PlayerStats - -@export var speed := 5.0 -@export var jump_velocity := 4.5 -@export var target_range := 20.0 -@export var combat_timeout := 3.0 -@export var respawn_time := 3.0 -@export var gcd_time := 0.5 -@export var aa_cooldown := 0.5 diff --git a/scenes/player/player_stats.gd.uid b/scenes/player/player_stats.gd.uid deleted file mode 100644 index b768b97..0000000 --- a/scenes/player/player_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ypyntbavbsto diff --git a/scenes/player/player_stats.tres b/scenes/player/player_stats.tres deleted file mode 100644 index 61822e2..0000000 --- a/scenes/player/player_stats.tres +++ /dev/null @@ -1,8 +0,0 @@ -[gd_resource type="Resource" script_class="PlayerStats" format=3 uid="uid://btd0g0oiulssq"] - -[ext_resource type="Script" uid="uid://ypyntbavbsto" path="res://scenes/player/player_stats.gd" id="1"] - -[resource] -script = ExtResource("1") -health_regen = 1.0 -max_shield = 50.0 diff --git a/scenes/player/role/ability.gd b/scenes/player/role/ability.gd deleted file mode 100644 index 432c681..0000000 --- a/scenes/player/role/ability.gd +++ /dev/null @@ -1,16 +0,0 @@ -extends Resource -class_name Ability - -enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE } - -@export var ability_name: String = "" -@export var type: Type = Type.SINGLE -@export var damage: float = 0.0 -@export var ability_range: float = 3.0 -@export var cooldown: float = 0.0 -@export var uses_gcd: bool = true -@export var aoe_radius: float = 0.0 -@export var icon: String = "" -@export var is_heal: bool = false -@export var passive_stat: String = "damage" -@export var element: int = 0 diff --git a/scenes/player/role/ability.gd.uid b/scenes/player/role/ability.gd.uid deleted file mode 100644 index a2c213e..0000000 --- a/scenes/player/role/ability.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c03xbbf3yhfl3 diff --git a/scenes/player/role/ability_set.gd.uid b/scenes/player/role/ability_set.gd.uid deleted file mode 100644 index 9a11572..0000000 --- a/scenes/player/role/ability_set.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://voedgs25cwrb diff --git a/scenes/player/role/damage/abilities/aoe.tres b/scenes/player/role/damage/abilities/aoe.tres deleted file mode 100644 index 8cf6b23..0000000 --- a/scenes/player/role/damage/abilities/aoe.tres +++ /dev/null @@ -1,13 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://bpx3l13iuynfv"] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "AOE" -type = 1 -damage = 20.0 -ability_range = 5.0 -cooldown = 3.0 -icon = "2" -element = 1 diff --git a/scenes/player/role/damage/abilities/passive.tres b/scenes/player/role/damage/abilities/passive.tres deleted file mode 100644 index 19c730b..0000000 --- a/scenes/player/role/damage/abilities/passive.tres +++ /dev/null @@ -1,12 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://dadpl32yujwhe"] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Damage Boost" -type = 4 -damage = 50.0 -ability_range = 0.0 -uses_gcd = false -icon = "P" diff --git a/scenes/player/role/damage/abilities/single.tres b/scenes/player/role/damage/abilities/single.tres deleted file mode 100644 index 921e429..0000000 --- a/scenes/player/role/damage/abilities/single.tres +++ /dev/null @@ -1,12 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://dwvc8b3cmce8l"] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Single" -damage = 30.0 -ability_range = 20.0 -cooldown = 2.0 -icon = "1" -element = 1 diff --git a/scenes/player/role/damage/abilities/ult.tres b/scenes/player/role/damage/abilities/ult.tres deleted file mode 100644 index 2be8c99..0000000 --- a/scenes/player/role/damage/abilities/ult.tres +++ /dev/null @@ -1,14 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://s32wvlww2ls2"] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Burst" -type = 3 -damage = 10.0 -ability_range = 20.0 -cooldown = 15.0 -aoe_radius = 3.0 -icon = "4" -element = 1 diff --git a/scenes/player/role/damage/abilities/utility.tres b/scenes/player/role/damage/abilities/utility.tres deleted file mode 100644 index d11f2c0..0000000 --- a/scenes/player/role/damage/abilities/utility.tres +++ /dev/null @@ -1,12 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Shield Reset" -type = 2 -ability_range = 0.0 -cooldown = 5.0 -uses_gcd = false -icon = "3" diff --git a/scenes/player/role/damage/set.tres b/scenes/player/role/damage/set.tres deleted file mode 100644 index 06d958d..0000000 --- a/scenes/player/role/damage/set.tres +++ /dev/null @@ -1,14 +0,0 @@ -[gd_resource type="Resource" script_class="AbilitySet" format=3 uid="uid://beodknb6i1pm4"] - -[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scenes/player/role/ability_set.gd" id="1"] -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1_ability"] -[ext_resource type="Resource" uid="uid://dwvc8b3cmce8l" path="res://scenes/player/role/damage/abilities/single.tres" id="2"] -[ext_resource type="Resource" uid="uid://bpx3l13iuynfv" path="res://scenes/player/role/damage/abilities/aoe.tres" id="3"] -[ext_resource type="Resource" path="res://scenes/player/role/damage/abilities/utility.tres" id="4"] -[ext_resource type="Resource" uid="uid://s32wvlww2ls2" path="res://scenes/player/role/damage/abilities/ult.tres" id="5"] -[ext_resource type="Resource" uid="uid://dadpl32yujwhe" path="res://scenes/player/role/damage/abilities/passive.tres" id="6"] - -[resource] -script = ExtResource("1") -abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")]) -aa_damage = 25.0 diff --git a/scenes/player/role/healer/abilities/aoe.tres b/scenes/player/role/healer/abilities/aoe.tres deleted file mode 100644 index 3c679fd..0000000 --- a/scenes/player/role/healer/abilities/aoe.tres +++ /dev/null @@ -1,13 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://m1kgk2uugnex"] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Circle of Healing" -type = 1 -damage = 10.0 -ability_range = 20.0 -cooldown = 3.0 -icon = "2" -is_heal = true diff --git a/scenes/player/role/healer/abilities/passive.tres b/scenes/player/role/healer/abilities/passive.tres deleted file mode 100644 index bb9be18..0000000 --- a/scenes/player/role/healer/abilities/passive.tres +++ /dev/null @@ -1,13 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Heal Aura" -type = 4 -damage = 50.0 -ability_range = 0.0 -uses_gcd = false -icon = "P" -passive_stat = "heal" diff --git a/scenes/player/role/healer/abilities/single.tres b/scenes/player/role/healer/abilities/single.tres deleted file mode 100644 index cd94eba..0000000 --- a/scenes/player/role/healer/abilities/single.tres +++ /dev/null @@ -1,12 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://cqw1jy6kqvmnj"] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Heal" -damage = 15.0 -ability_range = 20.0 -cooldown = 2.0 -icon = "1" -is_heal = true diff --git a/scenes/player/role/healer/abilities/ult.tres b/scenes/player/role/healer/abilities/ult.tres deleted file mode 100644 index 32b83cf..0000000 --- a/scenes/player/role/healer/abilities/ult.tres +++ /dev/null @@ -1,14 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://d04nu1leyki16"] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Divine Light" -type = 3 -damage = 25.0 -ability_range = 20.0 -cooldown = 15.0 -aoe_radius = 3.0 -icon = "4" -is_heal = true diff --git a/scenes/player/role/healer/abilities/utility.tres b/scenes/player/role/healer/abilities/utility.tres deleted file mode 100644 index d11f2c0..0000000 --- a/scenes/player/role/healer/abilities/utility.tres +++ /dev/null @@ -1,12 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Shield Reset" -type = 2 -ability_range = 0.0 -cooldown = 5.0 -uses_gcd = false -icon = "3" diff --git a/scenes/player/role/healer/set.tres b/scenes/player/role/healer/set.tres deleted file mode 100644 index cc3eefd..0000000 --- a/scenes/player/role/healer/set.tres +++ /dev/null @@ -1,16 +0,0 @@ -[gd_resource type="Resource" script_class="AbilitySet" format=3 uid="uid://kcwuhnqy34mj"] - -[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scenes/player/role/ability_set.gd" id="1"] -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1_ability"] -[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/single.tres" id="2"] -[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/aoe.tres" id="3"] -[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/utility.tres" id="4"] -[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/ult.tres" id="5"] -[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/passive.tres" id="6"] - -[resource] -script = ExtResource("1") -abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")]) -aa_damage = 1.0 -aa_range = 20.0 -aa_is_heal = true diff --git a/scenes/player/role/role.gd b/scenes/player/role/role.gd deleted file mode 100644 index 05c21df..0000000 --- a/scenes/player/role/role.gd +++ /dev/null @@ -1,14 +0,0 @@ -extends Node - -@onready var player: CharacterBody3D = get_parent() - -func _ready() -> void: - EventBus.role_change_requested.emit(player, PlayerData.current_role) - -func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("class_tank"): - EventBus.role_change_requested.emit(player, PlayerData.Role.TANK) - elif event.is_action_pressed("class_damage"): - EventBus.role_change_requested.emit(player, PlayerData.Role.DAMAGE) - elif event.is_action_pressed("class_healer"): - EventBus.role_change_requested.emit(player, PlayerData.Role.HEALER) diff --git a/scenes/player/role/role.gd.uid b/scenes/player/role/role.gd.uid deleted file mode 100644 index b4cc9da..0000000 --- a/scenes/player/role/role.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dhomrampxola4 diff --git a/scenes/player/role/tank/abilities/aoe.tres b/scenes/player/role/tank/abilities/aoe.tres deleted file mode 100644 index 4472d0c..0000000 --- a/scenes/player/role/tank/abilities/aoe.tres +++ /dev/null @@ -1,12 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Tank AOE" -type = 1 -damage = 10.0 -ability_range = 10.0 -cooldown = 3.0 -icon = "2" diff --git a/scenes/player/role/tank/abilities/passive.tres b/scenes/player/role/tank/abilities/passive.tres deleted file mode 100644 index 1babc70..0000000 --- a/scenes/player/role/tank/abilities/passive.tres +++ /dev/null @@ -1,13 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Shield Aura" -type = 4 -damage = 50.0 -ability_range = 0.0 -uses_gcd = false -icon = "P" -passive_stat = "shield" diff --git a/scenes/player/role/tank/abilities/single.tres b/scenes/player/role/tank/abilities/single.tres deleted file mode 100644 index 749d28e..0000000 --- a/scenes/player/role/tank/abilities/single.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Tank Strike" -damage = 15.0 -ability_range = 3.0 -cooldown = 2.0 -icon = "1" diff --git a/scenes/player/role/tank/abilities/ult.tres b/scenes/player/role/tank/abilities/ult.tres deleted file mode 100644 index 6e6057e..0000000 --- a/scenes/player/role/tank/abilities/ult.tres +++ /dev/null @@ -1,11 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Fortress" -type = 2 -damage = 300.0 -cooldown = 20.0 -icon = "4" diff --git a/scenes/player/role/tank/abilities/utility.tres b/scenes/player/role/tank/abilities/utility.tres deleted file mode 100644 index d11f2c0..0000000 --- a/scenes/player/role/tank/abilities/utility.tres +++ /dev/null @@ -1,12 +0,0 @@ -[gd_resource type="Resource" script_class="Ability" format=3] - -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"] - -[resource] -script = ExtResource("1") -ability_name = "Shield Reset" -type = 2 -ability_range = 0.0 -cooldown = 5.0 -uses_gcd = false -icon = "3" diff --git a/scenes/player/role/tank/set.tres b/scenes/player/role/tank/set.tres deleted file mode 100644 index 3ff166c..0000000 --- a/scenes/player/role/tank/set.tres +++ /dev/null @@ -1,15 +0,0 @@ -[gd_resource type="Resource" script_class="AbilitySet" format=3 uid="uid://cgxtn7dfs40bh"] - -[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scenes/player/role/ability_set.gd" id="1"] -[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1_ability"] -[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/single.tres" id="2"] -[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/aoe.tres" id="3"] -[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/utility.tres" id="4"] -[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/ult.tres" id="5"] -[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/passive.tres" id="6"] - -[resource] -script = ExtResource("1") -abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")]) -aa_damage = 5.0 -aa_range = 3.0 diff --git a/scenes/player/targeting.gd b/scenes/player/targeting.gd deleted file mode 100644 index f3da88e..0000000 --- a/scenes/player/targeting.gd +++ /dev/null @@ -1,60 +0,0 @@ -extends Node - -const TARGET_RANGE := 100.0 - -var mouse_press_pos: Vector2 = Vector2.ZERO - -@onready var player: CharacterBody3D = get_parent() -@onready var camera: Camera3D = get_parent().get_node("CameraPivot/Camera3D") - -func _unhandled_input(event: InputEvent) -> void: - if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: - if event.pressed: - mouse_press_pos = event.position - else: - var drag: float = event.position.distance_to(mouse_press_pos) - if drag < 5.0: - _try_target_under_mouse(event.position) - if event.is_action_pressed("target_next"): - _cycle_target() - -func _try_target_under_mouse(mouse_pos: Vector2) -> void: - var from := camera.project_ray_origin(mouse_pos) - var to := from + camera.project_ray_normal(mouse_pos) * TARGET_RANGE - var space := player.get_world_3d().direct_space_state - var query := PhysicsRayQueryParameters3D.create(from, to) - query.collision_mask = 4 - query.collide_with_areas = true - query.collide_with_bodies = false - var result := space.intersect_ray(query) - if result: - var hit_target := result.collider.get_parent() as Node3D - if hit_target and hit_target.is_in_group("tavern"): - EventBus.target_requested.emit(player, null) - return - if hit_target and (hit_target.is_in_group("enemies") or hit_target.is_in_group("portals")): - EventBus.target_requested.emit(player, hit_target) - return - EventBus.target_requested.emit(player, null) - -func _cycle_target() -> void: - var enemies := get_tree().get_nodes_in_group("enemies") - var portals := get_tree().get_nodes_in_group("portals") - var targets: Array = [] - for e in enemies: - if is_instance_valid(e): - targets.append(e) - for p in portals: - if is_instance_valid(p): - targets.append(p) - if targets.is_empty(): - EventBus.target_requested.emit(player, null) - return - targets.sort_custom(func(a, b): return player.global_position.distance_squared_to(a.global_position) < player.global_position.distance_squared_to(b.global_position)) - var current: Node3D = PlayerData.target - if current == null or current not in targets: - EventBus.target_requested.emit(player, targets[0]) - return - var idx := targets.find(current) - var next_idx := (idx + 1) % targets.size() - EventBus.target_requested.emit(player, targets[next_idx]) diff --git a/scenes/player/targeting.gd.uid b/scenes/player/targeting.gd.uid deleted file mode 100644 index 5bf1d42..0000000 --- a/scenes/player/targeting.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b05nkuryipwny diff --git a/scenes/portal/gate.gd b/scenes/portal/gate.gd deleted file mode 100644 index ee35323..0000000 --- a/scenes/portal/gate.gd +++ /dev/null @@ -1,36 +0,0 @@ -extends StaticBody3D - -@export var target_scene: String = "res://scenes/dungeon/dungeon.tscn" -@export var is_exit: bool = false - -var active := false -var dungeon_variant: int = 0 - -func _ready() -> void: - if not is_exit: - if PlayerData.dungeon_cleared: - queue_free() - return - get_tree().create_timer(0.5).timeout.connect(_check_overlapping) - else: - get_tree().create_timer(1.0).timeout.connect(func() -> void: active = true) - -func _check_overlapping() -> void: - active = true - for body in $GateArea.get_overlapping_bodies(): - _on_gate_area_body_entered(body) - -func _on_gate_area_body_entered(body: Node3D) -> void: - if not active: - return - if body is CharacterBody3D and body.name == "Player": - PlayerData.save_cache() - if is_exit: - PlayerData.returning_from_dungeon = true - else: - PlayerData.portal_position = global_position - GameState.last_dungeon_variant = dungeon_variant - call_deferred("_change_scene") - -func _change_scene() -> void: - get_tree().change_scene_to_file(target_scene) diff --git a/scenes/portal/gate.gd.uid b/scenes/portal/gate.gd.uid deleted file mode 100644 index 76d88b1..0000000 --- a/scenes/portal/gate.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ctci5mc3cd2ck diff --git a/scenes/portal/gate.tscn b/scenes/portal/gate.tscn deleted file mode 100644 index 09d221d..0000000 --- a/scenes/portal/gate.tscn +++ /dev/null @@ -1,36 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="res://scenes/portal/gate.gd" id="1"] - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] -albedo_color = Color(0.1, 0.9, 0.3, 1) -emission_enabled = true -emission = Color(0.1, 0.9, 0.3, 1) -emission_energy_multiplier = 0.5 - -[sub_resource type="CylinderMesh" id="CylinderMesh_1"] -top_radius = 1.0 -bottom_radius = 1.0 -height = 2.0 -material = SubResource("StandardMaterial3D_1") - -[sub_resource type="SphereShape3D" id="SphereShape3D_1"] -radius = 3.0 - -[node name="Gate" type="StaticBody3D"] -script = ExtResource("1") -collision_layer = 0 -collision_mask = 0 - -[node name="Mesh" type="MeshInstance3D" parent="."] -mesh = SubResource("CylinderMesh_1") - -[node name="GateArea" type="Area3D" parent="."] -collision_layer = 0 -collision_mask = 1 -monitoring = true - -[node name="CollisionShape3D" type="CollisionShape3D" parent="GateArea"] -shape = SubResource("SphereShape3D_1") - -[connection signal="body_entered" from="GateArea" to="." method="_on_gate_area_body_entered"] diff --git a/scenes/portal/init.gd b/scenes/portal/init.gd deleted file mode 100644 index 3820fb5..0000000 --- a/scenes/portal/init.gd +++ /dev/null @@ -1,37 +0,0 @@ -extends StaticBody3D - -@export var stats: PortalStats - -func _ready() -> void: - add_to_group("portals") - if stats.variant == PortalStats.Kind.RED: - add_to_group("red_portal") - _apply_appearance() - PortalData.register(self, stats) - -func _exit_tree() -> void: - PortalData.deregister(self) - -func _process(delta: float) -> void: - var mesh: Node3D = get_node_or_null("Mesh") - if mesh: - mesh.rotate_y(delta * 1.5) - -func _apply_appearance() -> void: - var mesh: MeshInstance3D = get_node_or_null("Mesh") - if not mesh: - return - var mat := StandardMaterial3D.new() - mat.emission_enabled = true - mat.emission_energy_multiplier = 0.8 - if stats.variant == PortalStats.Kind.RED: - mat.albedo_color = Color(0.9, 0.1, 0.1, 1) - mat.emission = Color(1.0, 0.25, 0.25, 1) - else: - mat.albedo_color = Color(0.4, 0.2, 0.85, 1) - mat.emission = Color(0.6, 0.35, 1.0, 1) - mesh.material_override = mat - -func _on_detection_area_body_entered(body: Node3D) -> void: - if body is CharacterBody3D and body.name == "Player": - EventBus.portal_entered.emit(self, body) diff --git a/scenes/portal/init.gd.uid b/scenes/portal/init.gd.uid deleted file mode 100644 index 1a86cac..0000000 --- a/scenes/portal/init.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dlexijybbqxop diff --git a/scenes/portal/portal.tscn b/scenes/portal/portal.tscn deleted file mode 100644 index a039e73..0000000 --- a/scenes/portal/portal.tscn +++ /dev/null @@ -1,82 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="res://scenes/portal/init.gd" id="1"] -[ext_resource type="Resource" path="res://scenes/portal/portal_stats.tres" id="6"] - -[sub_resource type="SphereShape3D" id="SphereShape3D_1"] -radius = 10.0 - -[sub_resource type="CylinderShape3D" id="CylinderShape3D_1"] -radius = 1.0 -height = 2.0 - -[sub_resource type="CylinderShape3D" id="CylinderShape3D_2"] -radius = 1.0 -height = 2.0 - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] -albedo_color = Color(0.5, 0.1, 0.8, 1) - -[sub_resource type="CylinderMesh" id="CylinderMesh_1"] -top_radius = 1.0 -bottom_radius = 1.0 -height = 2.0 -material = SubResource("StandardMaterial3D_1") - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"] -bg_color = Color(0.3, 0.1, 0.1, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"] -bg_color = Color(0.2, 0.8, 0.2, 1) - -[node name="Portal" type="StaticBody3D"] -script = ExtResource("1") -stats = ExtResource("6") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="."] -shape = SubResource("CylinderShape3D_1") - -[node name="Mesh" type="MeshInstance3D" parent="."] -mesh = SubResource("CylinderMesh_1") - -[node name="HitArea" type="Area3D" parent="."] -collision_layer = 4 -collision_mask = 0 - -[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"] -shape = SubResource("CylinderShape3D_2") - -[node name="DetectionArea" type="Area3D" parent="."] -collision_layer = 0 -collision_mask = 1 -monitoring = true - -[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea"] -shape = SubResource("SphereShape3D_1") - -[node name="Healthbar" type="Sprite3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) -billboard = 1 -pixel_size = 0.01 - -[node name="SubViewport" type="SubViewport" parent="Healthbar"] -transparent_bg = true -size = Vector2i(104, 15) - -[node name="Border" type="ColorRect" parent="Healthbar/SubViewport"] -offset_right = 104.0 -offset_bottom = 15.0 -color = Color(1, 0.9, 0.2, 1) - -[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport"] -offset_left = 2.0 -offset_top = 2.0 -offset_right = 102.0 -offset_bottom = 13.0 -theme_override_styles/background = SubResource("StyleBoxFlat_health_bg") -theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill") -max_value = 500.0 -value = 500.0 -show_percentage = false - -[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"] diff --git a/scenes/portal/portal_stats.gd b/scenes/portal/portal_stats.gd deleted file mode 100644 index e0e18ed..0000000 --- a/scenes/portal/portal_stats.gd +++ /dev/null @@ -1,8 +0,0 @@ -extends BaseStats -class_name PortalStats - -enum Kind { NORMAL, RED } - -@export var variant: Kind = Kind.NORMAL -@export var spawn_count := 3 -@export var thresholds: Array[float] = [0.85, 0.70, 0.55, 0.40, 0.25, 0.10] diff --git a/scenes/portal/portal_stats.gd.uid b/scenes/portal/portal_stats.gd.uid deleted file mode 100644 index 49367e2..0000000 --- a/scenes/portal/portal_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bioid3s5oftxs diff --git a/scenes/portal/portal_stats.tres b/scenes/portal/portal_stats.tres deleted file mode 100644 index 24454bf..0000000 --- a/scenes/portal/portal_stats.tres +++ /dev/null @@ -1,7 +0,0 @@ -[gd_resource type="Resource" script_class="PortalStats" format=3 uid="uid://be2vv5u0jw0yw"] - -[ext_resource type="Script" uid="uid://bioid3s5oftxs" path="res://scenes/portal/portal_stats.gd" id="1"] - -[resource] -script = ExtResource("1") -max_health = 500.0 diff --git a/scenes/portal/red_portal_stats.tres b/scenes/portal/red_portal_stats.tres deleted file mode 100644 index 24a5be4..0000000 --- a/scenes/portal/red_portal_stats.tres +++ /dev/null @@ -1,9 +0,0 @@ -[gd_resource type="Resource" script_class="PortalStats" format=3] - -[ext_resource type="Script" path="res://scenes/portal/portal_stats.gd" id="1"] - -[resource] -script = ExtResource("1") -variant = 1 -max_health = 5000.0 -spawn_count = 5 diff --git a/scenes/tavern/init.gd b/scenes/tavern/init.gd deleted file mode 100644 index 3ced402..0000000 --- a/scenes/tavern/init.gd +++ /dev/null @@ -1,10 +0,0 @@ -extends StaticBody3D - -@export var stats: TavernStats - -func _ready() -> void: - add_to_group("tavern") - TavernData.register(self, stats) - -func _exit_tree() -> void: - TavernData.deregister(self) diff --git a/scenes/tavern/init.gd.uid b/scenes/tavern/init.gd.uid deleted file mode 100644 index 24284c9..0000000 --- a/scenes/tavern/init.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dd103qxf2s5i5 diff --git a/scenes/tavern/tavern.tscn b/scenes/tavern/tavern.tscn deleted file mode 100644 index 49206a0..0000000 --- a/scenes/tavern/tavern.tscn +++ /dev/null @@ -1,51 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="res://scenes/tavern/init.gd" id="1"] -[ext_resource type="Resource" path="res://scenes/tavern/tavern_stats.tres" id="2"] -[ext_resource type="PackedScene" path="res://assets/models/buildings/building_home_A_blue.gltf" id="3"] - -[sub_resource type="BoxShape3D" id="BoxShape3D_tavern"] -size = Vector3(5, 3, 5) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"] -bg_color = Color(0.3, 0.1, 0.1, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"] -bg_color = Color(0.9, 0.7, 0.2, 1) - -[node name="Tavern" type="StaticBody3D"] -script = ExtResource("1") -stats = ExtResource("2") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="."] -shape = SubResource("BoxShape3D_tavern") - -[node name="Mesh" type="Node3D" parent="."] - -[node name="Model" parent="Mesh" instance=ExtResource("3")] -transform = Transform3D(2.5, 0, 0, 0, 2.5, 0, 0, 0, 2.5, 0, -1.5, 0) - -[node name="Healthbar" type="Sprite3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0) -billboard = 1 -pixel_size = 0.015 - -[node name="SubViewport" type="SubViewport" parent="Healthbar"] -transparent_bg = true -size = Vector2i(204, 25) - -[node name="Border" type="ColorRect" parent="Healthbar/SubViewport"] -offset_right = 204.0 -offset_bottom = 25.0 -color = Color(0.1, 0.1, 0.1, 1) - -[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport"] -offset_left = 2.0 -offset_top = 2.0 -offset_right = 202.0 -offset_bottom = 23.0 -theme_override_styles/background = SubResource("StyleBoxFlat_health_bg") -theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill") -max_value = 5000.0 -value = 5000.0 -show_percentage = false diff --git a/scenes/tavern/tavern_stats.gd b/scenes/tavern/tavern_stats.gd deleted file mode 100644 index fa2326c..0000000 --- a/scenes/tavern/tavern_stats.gd +++ /dev/null @@ -1,2 +0,0 @@ -extends BaseStats -class_name TavernStats diff --git a/scenes/tavern/tavern_stats.gd.uid b/scenes/tavern/tavern_stats.gd.uid deleted file mode 100644 index 72c740b..0000000 --- a/scenes/tavern/tavern_stats.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://duw4m3mhgmixk diff --git a/scenes/tavern/tavern_stats.tres b/scenes/tavern/tavern_stats.tres deleted file mode 100644 index b7973e5..0000000 --- a/scenes/tavern/tavern_stats.tres +++ /dev/null @@ -1,7 +0,0 @@ -[gd_resource type="Resource" script_class="TavernStats" format=3] - -[ext_resource type="Script" path="res://scenes/tavern/tavern_stats.gd" id="1"] - -[resource] -script = ExtResource("1") -max_health = 5000.0 diff --git a/scenes/world/portal_spawner.gd b/scenes/world/portal_spawner.gd deleted file mode 100644 index 18dc73c..0000000 --- a/scenes/world/portal_spawner.gd +++ /dev/null @@ -1,69 +0,0 @@ -extends Node - -const PORTAL_SCENE: PackedScene = preload("res://scenes/portal/portal.tscn") -const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn") -const RED_PORTAL_STATS: Resource = preload("res://scenes/portal/red_portal_stats.tres") -const MAX_NORMAL_PORTALS := 3 -const MIN_DISTANCE := 20.0 -const MAX_DISTANCE := 40.0 -const RESPAWN_DELAY := 1.0 - -var portals: Array[Node] = [] - -func _ready() -> void: - EventBus.portal_defeated.connect(_on_portal_defeated) - EventBus.wave_started.connect(_on_wave_started) - if PlayerData.portal_position != Vector3.ZERO and not PlayerData.dungeon_cleared: - call_deferred("_restore_gate") - else: - if PlayerData.dungeon_cleared: - PlayerData.clear_cache() - call_deferred("_ensure_portals") - -func _restore_gate() -> void: - var gate: Node3D = GATE_SCENE.instantiate() - get_parent().add_child(gate) - gate.global_position = PlayerData.portal_position - -func _ensure_portals() -> void: - _cleanup_dead() - while portals.size() < MAX_NORMAL_PORTALS: - _spawn_portal() - -func _on_portal_defeated(portal: Node) -> void: - if portal.is_in_group("red_portal"): - return - portals.erase(portal) - await get_tree().create_timer(RESPAWN_DELAY).timeout - _ensure_portals() - -func _on_wave_started(_wave_number: int) -> void: - _spawn_red_portal() - -func _spawn_red_portal() -> void: - for p in get_tree().get_nodes_in_group("red_portal"): - if is_instance_valid(p): - return - var angle: float = randf() * TAU - var distance: float = randf_range(MIN_DISTANCE, MAX_DISTANCE) - var pos := Vector3(cos(angle) * distance, 0, sin(angle) * distance) - var portal: Node3D = PORTAL_SCENE.instantiate() - portal.stats = RED_PORTAL_STATS - get_parent().add_child(portal) - portal.global_position = pos - -func _spawn_portal() -> void: - var angle: float = randf() * TAU - var distance: float = randf_range(MIN_DISTANCE, MAX_DISTANCE) - var pos := Vector3(cos(angle) * distance, 0, sin(angle) * distance) - var portal: Node3D = PORTAL_SCENE.instantiate() - get_parent().add_child(portal) - portal.global_position = pos - portals.append(portal) - -func _cleanup_dead() -> void: - var valid: Array[Node] = [] - for p in portals: - if is_instance_valid(p): - valid.append(p) - portals = valid diff --git a/scenes/world/portal_spawner.gd.uid b/scenes/world/portal_spawner.gd.uid deleted file mode 100644 index 5b5028e..0000000 --- a/scenes/world/portal_spawner.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cskx6o07iukwh diff --git a/scenes/world/world.tscn b/scenes/world/world.tscn index ca8d061..04ced95 100644 --- a/scenes/world/world.tscn +++ b/scenes/world/world.tscn @@ -1,193 +1,200 @@ -[gd_scene format=3 uid="uid://dy1icabu2ssbw"] +[gd_scene load_steps=10 format=3 uid="uid://b0world00001"] -[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"] -[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"] -[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"] -[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"] -[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"] -[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"] -[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"] -[ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"] -[ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"] -[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"] -[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"] -[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"] -[ext_resource type="Script" path="res://systems/portal_system.gd" id="portal_system"] -[ext_resource type="Script" path="res://systems/aura_system.gd" id="aura_system"] -[ext_resource type="Script" path="res://systems/buff_system.gd" id="buff_system"] -[ext_resource type="Script" path="res://systems/debuff_system.gd" id="debuff_system"] -[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"] -[ext_resource type="Script" path="res://systems/hud_system.gd" id="hud_system"] -[ext_resource type="Script" path="res://systems/nameplate_system.gd" id="nameplate_system"] -[ext_resource type="Script" path="res://systems/ai_system.gd" id="ai_system"] -[ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"] -[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"] -[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"] -[ext_resource type="Script" path="res://systems/wave_system.gd" id="wave_system"] -[ext_resource type="Script" path="res://systems/xp_system.gd" id="xp_system"] -[ext_resource type="Script" path="res://systems/invasion_system.gd" id="invasion_system"] -[ext_resource type="Script" path="res://systems/audio_system.gd" id="audio_system"] -[ext_resource type="Script" path="res://scenes/world/world_manager.gd" id="world_manager"] -[ext_resource type="PackedScene" path="res://scenes/menu/game_over_overlay.tscn" id="game_over_overlay"] -[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] -[ext_resource type="PackedScene" path="res://scenes/tavern/tavern.tscn" id="tavern"] -[ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"] -[ext_resource type="Script" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"] -[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"] -[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"] -[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"] +[ext_resource type="Script" path="res://scenes/world/world_manager.gd" id="1"] +[ext_resource type="PackedScene" uid="uid://b0player00001" path="res://scenes/entities/player/player.tscn" id="2"] +[ext_resource type="PackedScene" uid="uid://b0village001" path="res://scenes/entities/village/village.tscn" id="3"] +[ext_resource type="Script" path="res://systems/health_system.gd" id="4"] +[ext_resource type="Script" path="res://systems/shield_system.gd" id="5"] +[ext_resource type="Script" path="res://systems/respawn_system.gd" id="6"] +[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="7"] +[ext_resource type="Script" path="res://systems/role_system.gd" id="8"] +[ext_resource type="Script" path="res://systems/effect_system.gd" id="9"] +[ext_resource type="Script" path="res://systems/element_system.gd" id="10"] +[ext_resource type="Script" path="res://systems/aggro_system.gd" id="11"] +[ext_resource type="Script" path="res://systems/combat/ability_system.gd" id="12"] +[ext_resource type="Script" path="res://systems/combat/auto_attack_system.gd" id="13"] +[ext_resource type="Script" path="res://systems/spawn_system.gd" id="14"] +[ext_resource type="Script" path="res://systems/wave_system.gd" id="15"] +[ext_resource type="Script" path="res://systems/invasion_system.gd" id="16"] +[ext_resource type="Script" path="res://systems/xp_system.gd" id="17"] +[ext_resource type="Script" path="res://systems/loot_system.gd" id="18"] +[ext_resource type="Script" path="res://systems/inventory_system.gd" id="19"] +[ext_resource type="Script" path="res://systems/crafting_system.gd" id="20"] +[ext_resource type="Script" path="res://systems/building_system.gd" id="21"] +[ext_resource type="Script" path="res://systems/npc_system.gd" id="22"] +[ext_resource type="Script" path="res://systems/dialog_system.gd" id="23"] +[ext_resource type="Script" path="res://systems/chat_system.gd" id="24"] +[ext_resource type="Script" path="res://systems/map_system.gd" id="25"] +[ext_resource type="PackedScene" uid="uid://b0hud00001" path="res://scenes/hud/hud.tscn" id="26"] +[ext_resource type="Script" path="res://systems/audio_system.gd" id="27"] -[sub_resource type="NavigationMesh" id="NavigationMesh_1"] -vertices = PackedVector3Array(-49.5, 0.5, -49.5, -49.5, 0.5, 49.5, 49.5, 0.5, 49.5, 49.5, 0.5, -49.5) -polygons = [PackedInt32Array(3, 2, 0), PackedInt32Array(0, 2, 1)] +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_1"] +sky_top_color = Color(0.4, 0.55, 0.8, 1) +sky_horizon_color = Color(0.7, 0.7, 0.6, 1) +ground_horizon_color = Color(0.65, 0.55, 0.4, 1) +ground_bottom_color = Color(0.2, 0.2, 0.2, 1) -[sub_resource type="Gradient" id="Gradient_1"] -colors = PackedColorArray(0.15, 0.35, 0.05, 1, 0.3, 0.55, 0.1, 1) +[sub_resource type="Sky" id="Sky_1"] +sky_material = SubResource("ProceduralSkyMaterial_1") -[sub_resource type="FastNoiseLite" id="FastNoiseLite_1"] -frequency = 0.05 - -[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_1"] -noise = SubResource("FastNoiseLite_1") -color_ramp = SubResource("Gradient_1") -seamless = true - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ground"] -albedo_texture = SubResource("NoiseTexture2D_1") -uv1_scale = Vector3(15, 15, 1) +[sub_resource type="Environment" id="Environment_1"] +background_mode = 2 +sky = SubResource("Sky_1") +ambient_light_source = 3 +ambient_light_color = Color(0.6, 0.6, 0.7, 1) +ambient_light_energy = 0.5 +tonemap_mode = 2 [sub_resource type="PlaneMesh" id="PlaneMesh_1"] -material = SubResource("StandardMaterial3D_ground") -size = Vector2(100, 100) +size = Vector2(200, 200) +subdivide_width = 4 +subdivide_depth = 4 -[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_1"] - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tavern"] -albedo_color = Color(0.45, 0.3, 0.15, 1) - -[sub_resource type="BoxMesh" id="BoxMesh_tavern"] -material = SubResource("StandardMaterial3D_tavern") -size = Vector3(5, 3, 5) - -[sub_resource type="BoxShape3D" id="BoxShape3D_tavern"] -size = Vector3(5, 3, 5) +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_Ground"] +albedo_color = Color(0.35, 0.55, 0.3, 1) [node name="World" type="Node3D"] +script = ExtResource("1") + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_1") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 30, 0) +light_energy = 1.2 +shadow_enabled = true + +[node name="Ground" type="StaticBody3D" parent="."] +collision_layer = 1 +collision_mask = 0 + +[node name="GroundMesh" type="MeshInstance3D" parent="Ground"] +mesh = SubResource("PlaneMesh_1") +surface_material_override/0 = SubResource("StandardMaterial3D_Ground") + +[node name="GroundShape" type="CollisionShape3D" parent="Ground"] + +[node name="NavigationRegion" type="NavigationRegion3D" parent="."] [node name="Systems" type="Node" parent="."] [node name="HealthSystem" type="Node" parent="Systems"] -script = ExtResource("health_system") - -[node name="DamageSystem" type="Node" parent="Systems"] -script = ExtResource("damage_system") - -[node name="HealSystem" type="Node" parent="Systems"] -script = ExtResource("heal_system") +script = ExtResource("4") [node name="ShieldSystem" type="Node" parent="Systems"] -script = ExtResource("shield_system") - -[node name="RoleSystem" type="Node" parent="Systems"] -script = ExtResource("role_system") -tank_set = ExtResource("tank_set") -damage_set = ExtResource("damage_set") -healer_set = ExtResource("healer_set") - -[node name="AbilitySystem" type="Node" parent="Systems"] -script = ExtResource("ability_system") - -[node name="AttackSystem" type="Node" parent="Systems"] -script = ExtResource("attack_system") - -[node name="CooldownSystem" type="Node" parent="Systems"] -script = ExtResource("cooldown_system") - -[node name="TargetingSystem" type="Node" parent="Systems"] -script = ExtResource("targeting_system") - -[node name="AggroSystem" type="Node" parent="Systems"] -script = ExtResource("aggro_system") - -[node name="AggroTracker" type="Node" parent="Systems/AggroSystem"] -script = ExtResource("aggro_tracker") - -[node name="AggroDecay" type="Node" parent="Systems/AggroSystem"] -script = ExtResource("aggro_decay") - -[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"] -script = ExtResource("aggro_events") - -[node name="AISystem" type="Node" parent="Systems"] -script = ExtResource("ai_system") +script = ExtResource("5") [node name="RespawnSystem" type="Node" parent="Systems"] -script = ExtResource("respawn_system") +script = ExtResource("6") -[node name="SpawnSystem" type="Node" parent="Systems"] -script = ExtResource("spawn_system") +[node name="CooldownSystem" type="Node" parent="Systems"] +script = ExtResource("7") -[node name="PortalSystem" type="Node" parent="Systems"] -script = ExtResource("portal_system") +[node name="RoleSystem" type="Node" parent="Systems"] +script = ExtResource("8") -[node name="AuraSystem" type="Node" parent="Systems"] -script = ExtResource("aura_system") - -[node name="BuffSystem" type="Node" parent="Systems"] -script = ExtResource("buff_system") - -[node name="DebuffSystem" type="Node" parent="Systems"] -script = ExtResource("debuff_system") +[node name="EffectSystem" type="Node" parent="Systems"] +script = ExtResource("9") [node name="ElementSystem" type="Node" parent="Systems"] -script = ExtResource("element_system") +script = ExtResource("10") -[node name="HudSystem" type="Node" parent="Systems"] -script = ExtResource("hud_system") +[node name="AggroSystem" type="Node" parent="Systems"] +script = ExtResource("11") -[node name="NameplateSystem" type="Node" parent="Systems"] -script = ExtResource("nameplate_system") +[node name="AbilitySystem" type="Node" parent="Systems"] +script = ExtResource("12") -[node name="WaveSystem" type="Node" parent="Systems"] -script = ExtResource("wave_system") +[node name="AutoAttackSystem" type="Node" parent="Systems"] +script = ExtResource("13") + +[node name="SpawnSystem" type="Node" parent="Systems"] +script = ExtResource("14") + +[node name="LootSystem" type="Node" parent="Systems"] +script = ExtResource("18") [node name="XpSystem" type="Node" parent="Systems"] -script = ExtResource("xp_system") +script = ExtResource("17") -[node name="InvasionSystem" type="Node" parent="Systems"] -script = ExtResource("invasion_system") +[node name="InventorySystem" type="Node" parent="Systems"] +script = ExtResource("19") + +[node name="CraftingSystem" type="Node" parent="Systems"] +script = ExtResource("20") + +[node name="BuildingSystem" type="Node" parent="Systems"] +script = ExtResource("21") + +[node name="NpcSystem" type="Node" parent="Systems"] +script = ExtResource("22") + +[node name="DialogSystem" type="Node" parent="Systems"] +script = ExtResource("23") + +[node name="ChatSystem" type="Node" parent="Systems"] +script = ExtResource("24") + +[node name="MapSystem" type="Node" parent="Systems"] +script = ExtResource("25") [node name="AudioSystem" type="Node" parent="Systems"] -script = ExtResource("audio_system") +script = ExtResource("27") -[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0027503967, 0.014227867, 0.023231506) -navigation_mesh = SubResource("NavigationMesh_1") +[node name="WaveSystem" type="Node" parent="Systems"] +script = ExtResource("15") -[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D"] -mesh = SubResource("PlaneMesh_1") +[node name="InvasionSystem" type="Node" parent="Systems"] +script = ExtResource("16") -[node name="BodenCollision" type="StaticBody3D" parent="."] +[node name="VillageRoot" type="Node3D" parent="."] -[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"] -shape = SubResource("WorldBoundaryShape3D_1") +[node name="Village" parent="VillageRoot" instance=ExtResource("3")] -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0) -shadow_enabled = true +[node name="EntityRoot" type="Node3D" parent="."] -[node name="Tavern" parent="." instance=ExtResource("tavern")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +[node name="Players" type="Node3D" parent="EntityRoot"] -[node name="Player" parent="." instance=ExtResource("player")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5) +[node name="Enemies" type="Node3D" parent="EntityRoot"] -[node name="HUD" parent="." instance=ExtResource("hud")] +[node name="Bosses" type="Node3D" parent="EntityRoot"] -[node name="PortalSpawner" type="Node" parent="."] -script = ExtResource("portal_spawner") +[node name="Gates" type="Node3D" parent="EntityRoot"] -[node name="WorldManager" type="Node" parent="."] -script = ExtResource("world_manager") +[node name="Portals" type="Node3D" parent="EntityRoot"] -[node name="GameOverOverlay" parent="." instance=ExtResource("game_over_overlay")] +[node name="Buildings" type="Node3D" parent="EntityRoot"] + +[node name="Loot" type="Node3D" parent="EntityRoot"] + +[node name="Npcs" type="Node3D" parent="EntityRoot"] + +[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/player/player.tscn") +spawn_path = NodePath("../EntityRoot/Players") + +[node name="EnemySpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/enemy/enemy.tscn") +spawn_path = NodePath("../EntityRoot/Enemies") + +[node name="GateSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/gate/gate.tscn") +spawn_path = NodePath("../EntityRoot/Gates") + +[node name="PortalSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/portal/portal.tscn") +spawn_path = NodePath("../EntityRoot/Portals") + +[node name="BuildingSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/building/building.tscn") +spawn_path = NodePath("../EntityRoot/Buildings") + +[node name="LootSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/loot/loot_drop.tscn") +spawn_path = NodePath("../EntityRoot/Loot") + +[node name="NpcSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/entities/npc/npc.tscn") +spawn_path = NodePath("../EntityRoot/Npcs") + +[node name="HUD" parent="." instance=ExtResource("26")] diff --git a/scenes/world/world_manager.gd b/scenes/world/world_manager.gd index 2789ba4..0f6a3b2 100644 --- a/scenes/world/world_manager.gd +++ b/scenes/world/world_manager.gd @@ -1,21 +1,74 @@ -extends Node +extends Node3D + +const PLAYER_SCENE: PackedScene = preload("res://scenes/entities/player/player.tscn") + +@onready var players_root: Node3D = $EntityRoot/Players +@onready var ground_shape: CollisionShape3D = $Ground/GroundShape +@onready var npc_system: Node = $Systems/NpcSystem +@onready var wave_system: Node = $Systems/WaveSystem func _ready() -> void: - EventBus.game_over.connect(_on_game_over) - if GameState.force_return_to_world: - call_deferred("_handle_force_return") + add_to_group("world") + _setup_ground_collision() + Net.world_ready.connect(_on_world_ready) + Net.peer_world_loaded.connect(_on_peer_world_loaded) + if Net.is_host(): + multiplayer.peer_connected.connect(_on_peer_connected) + multiplayer.peer_disconnected.connect(_on_peer_disconnected) + Net.reset_world_ready() + Net.mark_world_loaded() -func _handle_force_return() -> void: - GameState.force_return_to_world = false - var player: Node3D = get_tree().get_first_node_in_group("player") - var tavern: Node3D = get_tree().get_first_node_in_group("tavern") - if player and tavern: - player.global_position = tavern.global_position + Vector3(0, 1, -6) - var invasion: Node = get_node_or_null("../Systems/InvasionSystem") - if invasion: - invasion.trigger() +func _exit_tree() -> void: + if Net.is_host(): + if multiplayer.peer_connected.is_connected(_on_peer_connected): + multiplayer.peer_connected.disconnect(_on_peer_connected) + if multiplayer.peer_disconnected.is_connected(_on_peer_disconnected): + multiplayer.peer_disconnected.disconnect(_on_peer_disconnected) + if Net.world_ready.is_connected(_on_world_ready): + Net.world_ready.disconnect(_on_world_ready) + if Net.peer_world_loaded.is_connected(_on_peer_world_loaded): + Net.peer_world_loaded.disconnect(_on_peer_world_loaded) -func _on_game_over() -> void: - var overlay: CanvasLayer = get_node_or_null("../GameOverOverlay") - if overlay and overlay.has_method("show_overlay"): - overlay.show_overlay(GameState.current_wave) +func _setup_ground_collision() -> void: + var shape := WorldBoundaryShape3D.new() + shape.plane = Plane(Vector3.UP, 0.0) + ground_shape.shape = shape + +func _on_world_ready() -> void: + if not Net.is_host(): + return + _spawn_player(1) + for id in multiplayer.get_peers(): + _spawn_player(id) + npc_system.spawn_default_npcs() + if wave_system and wave_system.has_method("start_wave"): + wave_system.start_wave(GameState.current_wave) + +func _on_peer_connected(id: int) -> void: + Net.tell_peer_to_load_scene(id, GameState.SCENE_WORLD) + +func _on_peer_world_loaded(peer_id: int) -> void: + if not Net.is_host(): + return + _spawn_player(peer_id) + +func _on_peer_disconnected(id: int) -> void: + var node := players_root.get_node_or_null(str(id)) + if node: + node.queue_free() + +func _spawn_player(peer_id: int) -> void: + if players_root.get_node_or_null(str(peer_id)) != null: + return + var p: CharacterBody3D = PLAYER_SCENE.instantiate() + p.name = str(peer_id) + var spawn := _spawn_position_for(peer_id) + players_root.add_child(p, true) + p.global_position = spawn + +func _spawn_position_for(peer_id: int) -> Vector3: + if GameState.portal_return_position != Vector3.ZERO: + var off: float = float(peer_id) * 0.7 + return GameState.portal_return_position + Vector3(cos(off) * 1.2, 1.5, sin(off) * 1.2) + var angle: float = wrapf(float(peer_id) * 1.7, 0.0, TAU) + return Vector3(cos(angle) * 5.0, 1.5, sin(angle) * 5.0) diff --git a/scenes/world/world_manager.gd.uid b/scenes/world/world_manager.gd.uid index 583038f..17536ec 100644 --- a/scenes/world/world_manager.gd.uid +++ b/scenes/world/world_manager.gd.uid @@ -1 +1 @@ -uid://cejlqodm01ob3 +uid://c88xbvl82lrtx diff --git a/systems/ability_system.gd b/systems/ability_system.gd deleted file mode 100644 index ea34af2..0000000 --- a/systems/ability_system.gd +++ /dev/null @@ -1,129 +0,0 @@ -extends Node - -func _ready() -> void: - EventBus.ability_use.connect(_on_ability_use) - -func _on_ability_use(_player: Node, ability_index: int) -> void: - if not PlayerData.alive: - return - var ability_set: AbilitySet = PlayerData.ability_set - if not ability_set or ability_index >= ability_set.abilities.size(): - return - var ability: Ability = ability_set.abilities[ability_index] - if not ability or ability.type == Ability.Type.PASSIVE: - return - if PlayerData.cooldowns[ability_index] > 0: - return - if ability.uses_gcd and PlayerData.gcd > 0: - return - var success: bool = _execute_ability(ability) - if not success: - return - var gcd: float = PlayerData.gcd_time if ability.uses_gcd else 0.0 - PlayerData.cooldowns[ability_index] = ability.cooldown - PlayerData.max_cooldowns[ability_index] = max(ability.cooldown, gcd) - if gcd > 0: - PlayerData.gcd = gcd - -func _execute_ability(ability: Ability) -> bool: - var stat: String = "heal" if ability.is_heal else "damage" - var dmg: float = _apply_passive(ability.damage, stat) - var player: Node = get_tree().get_first_node_in_group("player") - match ability.type: - Ability.Type.SINGLE: - return _execute_single(player, ability, dmg) - Ability.Type.AOE: - return _execute_aoe(player, ability, dmg) - Ability.Type.UTILITY: - return _execute_utility(ability) - Ability.Type.ULT: - return _execute_ult(player, ability, dmg) - return false - -func _apply_passive(base: float, stat: String) -> float: - var mult: float = 1.0 - match stat: - "damage": mult = PlayerData.buff_damage - "heal": mult = PlayerData.buff_heal - return base * mult * PlayerData.level_scale - -func _in_range(ability: Ability) -> bool: - if ability.ability_range <= 0 or ability.is_heal: - return true - if not is_instance_valid(PlayerData.target): - return false - var player: Node = get_tree().get_first_node_in_group("player") - var dist: float = player.global_position.distance_to(PlayerData.target.global_position) - return dist <= ability.ability_range - -func _execute_single(player: Node, ability: Ability, dmg: float) -> bool: - if ability.is_heal: - EventBus.heal_requested.emit(player, player, dmg) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return true - if not _in_range(ability): - return false - if not is_instance_valid(PlayerData.target): - return false - EventBus.damage_requested.emit(player, PlayerData.target, dmg) - if ability.element != 0: - EventBus.element_damage_dealt.emit(player, PlayerData.target, dmg, ability.element) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return true - -func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool: - if ability.is_heal: - EventBus.heal_requested.emit(player, player, dmg) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return true - var hit := false - var targets: Array = get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals") - for target in targets: - var dist: float = player.global_position.distance_to(target.global_position) - if dist <= ability.ability_range: - EventBus.damage_requested.emit(player, target, dmg) - if ability.element != 0: - EventBus.element_damage_dealt.emit(player, target, dmg, ability.element) - hit = true - if hit: - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return hit - -func _execute_utility(ability: Ability) -> bool: - if PlayerData.max_shield <= 0: - return false - var shield: float = PlayerData.shield - if ability.damage > 0: - shield = PlayerData.max_shield * (ability.damage / 100.0) - else: - if shield >= PlayerData.max_shield: - return false - shield = PlayerData.max_shield - PlayerData.set_shield(shield) - return true - -func _execute_ult(player: Node, ability: Ability, dmg: float) -> bool: - if ability.is_heal: - EventBus.heal_requested.emit(player, player, dmg) - var aoe_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg) - return true - if not _in_range(ability): - return false - if not is_instance_valid(PlayerData.target): - return false - var target: Node3D = PlayerData.target - EventBus.damage_requested.emit(player, target, dmg * 5.0) - if ability.element != 0: - EventBus.element_damage_dealt.emit(player, target, dmg * 5.0, ability.element) - var splash_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range - var splash_targets: Array = get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals") - for other in splash_targets: - if other != target and is_instance_valid(other): - var other_dist: float = target.global_position.distance_to(other.global_position) - if other_dist <= splash_range: - EventBus.damage_requested.emit(player, other, dmg * 2.0) - if ability.element != 0: - EventBus.element_damage_dealt.emit(player, other, dmg * 2.0, ability.element) - EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 5.0) - return true diff --git a/systems/ability_system.gd.uid b/systems/ability_system.gd.uid deleted file mode 100644 index e0d7add..0000000 --- a/systems/ability_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://h0hts425epc6 diff --git a/systems/aggro/aggro_config.gd b/systems/aggro/aggro_config.gd deleted file mode 100644 index bb6ad14..0000000 --- a/systems/aggro/aggro_config.gd +++ /dev/null @@ -1,8 +0,0 @@ -extends Resource -class_name AggroConfig - -@export var combat_timeout := 5.0 -@export var tank_multiplier := 2.0 -@export var heal_multiplier := 0.5 -@export var spread_multiplier := 0.5 -@export var exponential_decay_factor := 0.01 diff --git a/systems/aggro/aggro_config.gd.uid b/systems/aggro/aggro_config.gd.uid deleted file mode 100644 index 7892d27..0000000 --- a/systems/aggro/aggro_config.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b3gwl1wweld2x diff --git a/systems/aggro/aggro_config.tres b/systems/aggro/aggro_config.tres deleted file mode 100644 index bf7380d..0000000 --- a/systems/aggro/aggro_config.tres +++ /dev/null @@ -1,6 +0,0 @@ -[gd_resource type="Resource" script_class="AggroConfig" format=3] - -[ext_resource type="Script" path="res://systems/aggro/aggro_config.gd" id="1"] - -[resource] -script = ExtResource("1") diff --git a/systems/aggro/aggro_decay.gd b/systems/aggro/aggro_decay.gd deleted file mode 100644 index e810a64..0000000 --- a/systems/aggro/aggro_decay.gd +++ /dev/null @@ -1,73 +0,0 @@ -extends Node - -var tracker: Node -var config: AggroConfig -var last_damage_time: Dictionary = {} - -func process(delta: float) -> void: - _update_combat_timers(delta) - for enemy in tracker.aggro_tables.keys(): - if not is_instance_valid(enemy): - tracker.aggro_tables.erase(enemy) - tracker.players_in_range.erase(enemy) - continue - _decay_aggro(enemy, delta) - tracker.update_target(enemy) - -func _update_combat_timers(delta: float) -> void: - for player in last_damage_time.keys(): - if not is_instance_valid(player): - last_damage_time.erase(player) - else: - last_damage_time[player] += delta - -func _decay_aggro(enemy: Node, delta: float) -> void: - var table: Dictionary = tracker.aggro_tables[enemy] - var data_source: Node = tracker._get_data_source(enemy) - var aggro_decay: float = 1.0 - if data_source: - var base: EnemyStats = data_source.get_base(enemy) - if base: - aggro_decay = base.aggro_decay - for player in table.keys(): - if is_in_combat(player): - continue - var time_since_combat: float = last_damage_time.get(player, config.combat_timeout) - config.combat_timeout - var decay: float = aggro_decay * delta - decay += _exponential_decay(table[player], time_since_combat, delta) - table[player] -= decay - if table[player] <= 0: - table.erase(player) - -func reset_combat_timer(player: Node) -> void: - last_damage_time[player] = 0.0 - -func is_in_combat(player: Node) -> bool: - if tracker.is_player_in_any_range(player): - return true - return last_damage_time.get(player, config.combat_timeout + 1.0) < config.combat_timeout - -func _exponential_decay(aggro: float, time_outside: float, delta: float) -> float: - if time_outside <= 0: - return 0.0 - return aggro * config.exponential_decay_factor * pow(2, time_outside) * delta - -func spread_aggro(source: Node, attacker: Node, amount: float) -> void: - if not is_instance_valid(source): - return - var radius: float = tracker.get_alert_radius(source) - for enemy in tracker.get_enemies_in_radius(source, radius): - tracker.add_aggro(enemy, attacker, amount) - -func alert_nearby(enemy: Node, target: Node) -> void: - var radius: float = tracker.get_alert_radius(enemy) - for other in tracker.get_enemies_in_radius(enemy, radius): - var data_source: Node = tracker._get_data_source(other) - if data_source and data_source.get_stat(other, "state") == 0: - tracker.add_aggro(other, target, 1.0) - data_source.set_stat(other, "target", target) - data_source.set_stat(other, "state", 1) - EventBus.enemy_engaged.emit(other, target) - -func erase_entity(entity: Node) -> void: - last_damage_time.erase(entity) diff --git a/systems/aggro/aggro_decay.gd.uid b/systems/aggro/aggro_decay.gd.uid deleted file mode 100644 index 90e41dd..0000000 --- a/systems/aggro/aggro_decay.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cysg30lud2ta2 diff --git a/systems/aggro/aggro_events.gd b/systems/aggro/aggro_events.gd deleted file mode 100644 index 270e39e..0000000 --- a/systems/aggro/aggro_events.gd +++ /dev/null @@ -1,53 +0,0 @@ -extends Node - -var tracker: Node -var decay: Node -var config: AggroConfig - -func _ready() -> void: - EventBus.damage_dealt.connect(_on_damage_dealt) - EventBus.heal_requested.connect(_on_heal_requested) - EventBus.entity_died.connect(_on_entity_died) - EventBus.enemy_detected.connect(_on_enemy_detected) - EventBus.enemy_lost.connect(_on_enemy_lost) - -func _on_enemy_detected(enemy: Node, player: Node) -> void: - if not enemy.is_in_group("enemies"): - return - var data_source: Node = tracker._get_data_source(enemy) - if data_source: - var state: int = data_source.get_stat(enemy, "state") - if state == 1 or state == 2: - return - tracker.add_player_in_range(enemy, player) - tracker.add_aggro(enemy, player, 1.0) - if data_source: - data_source.set_stat(enemy, "target", player) - data_source.set_stat(enemy, "state", 1) - EventBus.enemy_engaged.emit(enemy, player) - decay.alert_nearby(enemy, player) - -func _on_enemy_lost(enemy: Node, player: Node) -> void: - tracker.remove_player_in_range(enemy, player) - -func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void: - if not target.is_in_group("enemies") and not target.is_in_group("portals"): - return - decay.reset_combat_timer(attacker) - var multiplier := 1.0 - if PlayerData.current_role == PlayerData.Role.TANK: - multiplier = config.tank_multiplier - var aggro: float = amount * multiplier - tracker.add_aggro(target, attacker, aggro) - decay.spread_aggro(target, attacker, aggro * config.spread_multiplier) - -func _on_heal_requested(healer: Node, _target: Node, amount: float) -> void: - if not healer.is_in_group("player"): - return - for enemy in tracker.aggro_tables: - if is_instance_valid(enemy) and healer in tracker.aggro_tables[enemy]: - tracker.add_aggro(enemy, healer, amount * config.heal_multiplier) - -func _on_entity_died(entity: Node) -> void: - tracker.erase_entity(entity) - decay.erase_entity(entity) diff --git a/systems/aggro/aggro_events.gd.uid b/systems/aggro/aggro_events.gd.uid deleted file mode 100644 index 9f4f901..0000000 --- a/systems/aggro/aggro_events.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cyffo1g4uhmwh diff --git a/systems/aggro/aggro_system.gd b/systems/aggro/aggro_system.gd deleted file mode 100644 index cb0fb62..0000000 --- a/systems/aggro/aggro_system.gd +++ /dev/null @@ -1,17 +0,0 @@ -extends Node - -@export var config: AggroConfig = preload("res://systems/aggro/aggro_config.tres") - -@onready var tracker: Node = $AggroTracker -@onready var decay: Node = $AggroDecay -@onready var events: Node = $AggroEvents - -func _ready() -> void: - decay.tracker = tracker - decay.config = config - events.tracker = tracker - events.decay = decay - events.config = config - -func _process(delta: float) -> void: - decay.process(delta) diff --git a/systems/aggro/aggro_system.gd.uid b/systems/aggro/aggro_system.gd.uid deleted file mode 100644 index 95f3ef4..0000000 --- a/systems/aggro/aggro_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cm7ehl2pexcst diff --git a/systems/aggro/aggro_tracker.gd b/systems/aggro/aggro_tracker.gd deleted file mode 100644 index 0a5b6e6..0000000 --- a/systems/aggro/aggro_tracker.gd +++ /dev/null @@ -1,96 +0,0 @@ -extends Node - -var aggro_tables: Dictionary = {} -var players_in_range: Dictionary = {} - -func add_aggro(enemy: Node, player: Node, amount: float) -> void: - if enemy not in aggro_tables: - aggro_tables[enemy] = {} - if player in aggro_tables[enemy]: - aggro_tables[enemy][player] += amount - else: - aggro_tables[enemy][player] = amount - -func remove_aggro(enemy: Node, player: Node, amount: float) -> void: - if enemy in aggro_tables and player in aggro_tables[enemy]: - aggro_tables[enemy][player] -= amount - if aggro_tables[enemy][player] <= 0: - aggro_tables[enemy].erase(player) - -func add_player_in_range(enemy: Node, player: Node) -> void: - if enemy not in players_in_range: - players_in_range[enemy] = [] - if player not in players_in_range[enemy]: - players_in_range[enemy].append(player) - -func remove_player_in_range(enemy: Node, player: Node) -> void: - if enemy in players_in_range: - players_in_range[enemy].erase(player) - -func is_player_in_any_range(player: Node) -> bool: - for enemy in players_in_range: - if is_instance_valid(enemy) and player in players_in_range[enemy]: - return true - return false - -func get_top_target(table: Dictionary) -> Node: - var top: Node = null - var top_val := 0.0 - for player in table: - if is_instance_valid(player) and table[player] > top_val: - top_val = table[player] - top = player - return top - -func update_target(enemy: Node) -> void: - var table: Dictionary = aggro_tables[enemy] - var top: Node = get_top_target(table) - var data_source: Node = _get_data_source(enemy) - if not data_source: - return - var state: int = data_source.get_stat(enemy, "state") - if top and top != data_source.get_stat(enemy, "target"): - data_source.set_stat(enemy, "target", top) - if state == 0 or state == 3: - data_source.set_stat(enemy, "state", 1) - elif not top and state != 0 and state != 3: - data_source.set_stat(enemy, "target", null) - data_source.set_stat(enemy, "state", 3) - -func get_enemies_in_radius(source: Node, radius: float) -> Array: - var result: Array = [] - for enemy in get_tree().get_nodes_in_group("enemies"): - if enemy != source and is_instance_valid(enemy): - var dist: float = source.global_position.distance_to(enemy.global_position) - if dist <= radius: - result.append(enemy) - return result - -func get_alert_radius(entity: Node) -> float: - var data_source: Node = _get_data_source(entity) - if data_source: - var base: EnemyStats = data_source.get_base(entity) - if base: - return base.alert_radius - return 10.0 - -func erase_entity(entity: Node) -> void: - aggro_tables.erase(entity) - players_in_range.erase(entity) - for enemy in aggro_tables: - if is_instance_valid(enemy): - aggro_tables[enemy].erase(entity) - var data_source: Node = _get_data_source(enemy) - if data_source and data_source.get_stat(enemy, "target") == entity: - data_source.set_stat(enemy, "target", null) - data_source.set_stat(enemy, "state", 3) - for enemy in players_in_range: - if is_instance_valid(enemy): - players_in_range[enemy].erase(entity) - -func _get_data_source(entity: Node) -> Node: - if entity.is_in_group("boss"): - return BossData - elif entity.is_in_group("enemies"): - return EnemyData - return null diff --git a/systems/aggro/aggro_tracker.gd.uid b/systems/aggro/aggro_tracker.gd.uid deleted file mode 100644 index 6b96555..0000000 --- a/systems/aggro/aggro_tracker.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c7gsu2qddsor6 diff --git a/systems/aggro_system.gd b/systems/aggro_system.gd new file mode 100644 index 0000000..b7ab153 --- /dev/null +++ b/systems/aggro_system.gd @@ -0,0 +1,110 @@ +extends Node + +const COMBAT_TIMEOUT: float = 5.0 +const TANK_MULT: float = 2.0 +const HEAL_MULT: float = 0.5 +const SPREAD: float = 0.5 +const DECAY_PER_SEC: float = 1.0 + +var aggro: Dictionary = {} +var combat_timers: Dictionary = {} + +func _ready() -> void: + EventBus.damage_dealt.connect(_on_damage_dealt) + EventBus.heal_requested.connect(_on_heal_requested) + EventBus.entity_died.connect(_on_died) + EventBus.entity_deregistered.connect(_on_dereg) + EventBus.enemy_detected.connect(_on_enemy_detected) + if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer): + set_physics_process(false) + else: + set_physics_process(true) + +var _accum: float = 0.0 + +func _physics_process(delta: float) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + _accum += delta + if _accum < 0.20: + return + var dt: float = _accum + _accum = 0.0 + for enemy in aggro.keys(): + if not is_instance_valid(enemy): + continue + var t: float = combat_timers.get(enemy, 0.0) + if t > 0.0: + combat_timers[enemy] = max(0.0, t - dt) + else: + var table: Dictionary = aggro[enemy] + var to_remove: Array = [] + for player in table.keys(): + table[player] = max(0.0, table[player] - DECAY_PER_SEC * dt) + if table[player] <= 0.0: + to_remove.append(player) + for p in to_remove: + table.erase(p) + if not aggro[enemy].is_empty(): + EventBus.enemy_engaged.emit(enemy, _top_target(enemy)) + +func _on_damage_dealt(attacker: Node, target: Node, amount: float) -> void: + if not is_instance_valid(target) or not is_instance_valid(attacker): + return + if target.is_in_group("enemies"): + var role: int = int(Stats.get_stat(attacker, "role", GameState.ROLE_DAMAGE)) + var mult: float = TANK_MULT if role == GameState.ROLE_TANK else 1.0 + _add(target, attacker, amount * mult) + _spread(target, attacker, amount * SPREAD) + combat_timers[target] = COMBAT_TIMEOUT + +func _on_heal_requested(healer: Node, _target: Node, amount: float) -> void: + if not is_instance_valid(healer): + return + for enemy in aggro.keys(): + if not is_instance_valid(enemy): + continue + if healer in aggro[enemy]: + _add(enemy, healer, amount * HEAL_MULT) + +func _on_enemy_detected(enemy: Node, player: Node) -> void: + _add(enemy, player, 1.0) + combat_timers[enemy] = COMBAT_TIMEOUT + +func _on_died(entity: Node) -> void: + aggro.erase(entity) + combat_timers.erase(entity) + for enemy in aggro.keys(): + if entity in aggro[enemy]: + aggro[enemy].erase(entity) + +func _on_dereg(entity: Node) -> void: + _on_died(entity) + +func _add(enemy: Node, player: Node, amount: float) -> void: + if not enemy in aggro: + aggro[enemy] = {} + aggro[enemy][player] = aggro[enemy].get(player, 0.0) + amount + +func _spread(enemy: Node, player: Node, amount: float) -> void: + for other in aggro.keys(): + if other == enemy or not is_instance_valid(other): + continue + if (other as Node3D).global_position.distance_to((enemy as Node3D).global_position) <= 10.0: + _add(other, player, amount) + +func _top_target(enemy: Node) -> Node: + if not enemy in aggro: + return null + var best: Node = null + var best_v: float = -1.0 + for p in aggro[enemy].keys(): + if not is_instance_valid(p): + continue + if aggro[enemy][p] > best_v: + best_v = aggro[enemy][p] + best = p + return best + +func target_for(enemy: Node) -> Node: + return _top_target(enemy) diff --git a/systems/aggro_system.gd.uid b/systems/aggro_system.gd.uid new file mode 100644 index 0000000..47aaf50 --- /dev/null +++ b/systems/aggro_system.gd.uid @@ -0,0 +1 @@ +uid://6mabhel6qdl4 diff --git a/systems/ai_system.gd b/systems/ai_system.gd deleted file mode 100644 index d3c5213..0000000 --- a/systems/ai_system.gd +++ /dev/null @@ -1,121 +0,0 @@ -extends Node - -enum State { IDLE, CHASE, ATTACK, RETURN } - -func _physics_process(delta: float) -> void: - _process_group(delta, EnemyData) - _process_group(delta, BossData) - -func _process_group(delta: float, data_source: Node) -> void: - for entity in data_source.entities: - if not is_instance_valid(entity) or not data_source.is_alive(entity): - continue - var data: Dictionary = data_source.entities[entity] - if entity.is_in_group("invasion"): - _force_invasion_target(entity, data) - var state: int = data["state"] - match state: - State.IDLE: - entity.velocity.x = 0 - entity.velocity.z = 0 - State.CHASE: - _chase(entity, data, data_source) - State.ATTACK: - _attack(entity, data, data_source, delta) - State.RETURN: - _return_to_spawn(entity, data, data_source, delta) - -func _force_invasion_target(entity: Node, data: Dictionary) -> void: - var tavern: Node = get_tree().get_first_node_in_group("tavern") - if not tavern: - return - data["target"] = tavern - if data["state"] == State.IDLE or data["state"] == State.RETURN: - data["state"] = State.CHASE - -func _chase(entity: Node, data: Dictionary, data_source: Node) -> void: - if not is_instance_valid(data["target"]): - data["state"] = State.RETURN - return - var base: EnemyStats = data_source.get_base(entity) - var attack_range: float = base.attack_range - if data["target"].is_in_group("tavern"): - attack_range += 3.0 - var dist: float = entity.global_position.distance_to(data["target"].global_position) - if dist <= attack_range: - data["state"] = State.ATTACK - return - var nav_agent: NavigationAgent3D = entity.get_node_or_null("NavigationAgent3D") - if not nav_agent: - return - nav_agent.target_position = data["target"].global_position - var next_pos := nav_agent.get_next_path_position() - var direction: Vector3 = (next_pos - entity.global_position).normalized() - direction.y = 0 - entity.velocity.x = direction.x * base.speed - entity.velocity.z = direction.z * base.speed - _face_target(entity, data["target"]) - -func _attack(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void: - data["attack_timer"] -= delta - if not is_instance_valid(data["target"]): - data["state"] = State.RETURN - return - var base: EnemyStats = data_source.get_base(entity) - var attack_range: float = base.attack_range - if data["target"].is_in_group("tavern"): - attack_range += 3.0 - var dist: float = entity.global_position.distance_to(data["target"].global_position) - if dist > attack_range: - data["state"] = State.CHASE - return - if data["attack_timer"] <= 0: - data["attack_timer"] = base.attack_cooldown - var scale: float = data.get("scale", 1.0) - EventBus.damage_requested.emit(entity, data["target"], base.attack_damage * scale) - EventBus.attack_executed.emit(entity, entity.global_position, -entity.global_transform.basis.z, base.attack_damage * scale) - entity.velocity.x = 0 - entity.velocity.z = 0 - _face_target(entity, data["target"]) - -func _face_target(entity: Node3D, target: Node3D) -> void: - if not is_instance_valid(target): - return - var to_target: Vector3 = target.global_position - entity.global_position - to_target.y = 0 - if to_target.length() < 0.01: - return - var yaw: float = atan2(-to_target.x, -to_target.z) - var delta: float = get_physics_process_delta_time() - entity.rotation.y = lerp_angle(entity.rotation.y, yaw, 8.0 * delta) - -func _return_to_spawn(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void: - var spawn_pos: Vector3 = data["spawn_position"] - var dist: float = entity.global_position.distance_to(spawn_pos) - if dist < 1.0: - data["state"] = State.IDLE - entity.velocity.x = 0 - entity.velocity.z = 0 - return - var base: EnemyStats = data_source.get_base(entity) - var nav_agent: NavigationAgent3D = entity.get_node_or_null("NavigationAgent3D") - if not nav_agent: - return - nav_agent.target_position = spawn_pos - var next_pos := nav_agent.get_next_path_position() - var direction: Vector3 = (next_pos - entity.global_position).normalized() - direction.y = 0 - entity.velocity.x = direction.x * base.speed - entity.velocity.z = direction.z * base.speed - _regenerate(entity, data, data_source, delta) - -func _regenerate(entity: Node, data: Dictionary, data_source: Node, delta: float) -> void: - var health: float = data["health"] - var max_health: float = data["max_health"] - if health < max_health: - var base: EnemyStats = data_source.get_base(entity) - var rate: float = base.regen_fast - if health >= max_health * 0.99: - rate = base.regen_slow - health = min(health + max_health * rate * delta, max_health) - data_source.set_health(entity, health) diff --git a/systems/ai_system.gd.uid b/systems/ai_system.gd.uid deleted file mode 100644 index f54ccbd..0000000 --- a/systems/ai_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dokr1ut7ea541 diff --git a/systems/attack_system.gd b/systems/attack_system.gd deleted file mode 100644 index bae1d91..0000000 --- a/systems/attack_system.gd +++ /dev/null @@ -1,27 +0,0 @@ -extends Node - -func _process(_delta: float) -> void: - if not PlayerData.alive or not PlayerData.in_combat: - return - if not is_instance_valid(PlayerData.target): - return - if PlayerData.aa_timer > 0: - return - var ability_set: AbilitySet = PlayerData.ability_set - if not ability_set: - return - var player: Node = get_tree().get_first_node_in_group("player") - if not player: - return - var aa_damage: float = ability_set.aa_damage - var aa_range: float = ability_set.aa_range - var aa_is_heal: bool = ability_set.aa_is_heal - var dmg: float = aa_damage * (PlayerData.buff_heal if aa_is_heal else PlayerData.buff_damage) * PlayerData.level_scale - if aa_is_heal: - EventBus.heal_requested.emit(player, player, dmg) - else: - var dist: float = player.global_position.distance_to(PlayerData.target.global_position) - if dist > aa_range: - return - EventBus.damage_requested.emit(player, PlayerData.target, dmg) - PlayerData.aa_timer = PlayerData.aa_cooldown diff --git a/systems/attack_system.gd.uid b/systems/attack_system.gd.uid deleted file mode 100644 index 3766e79..0000000 --- a/systems/attack_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dvuds0uuffj6t diff --git a/systems/audio_system.gd b/systems/audio_system.gd index b01b97a..affa5d9 100644 --- a/systems/audio_system.gd +++ b/systems/audio_system.gd @@ -1,113 +1,80 @@ extends Node -const SFX_PATHS := { - "hit": "res://assets/audio/sfx/hit.wav", - "death": "res://assets/audio/sfx/death.wav", - "level_up": "res://assets/audio/sfx/level_up.wav", - "ability_cast": "res://assets/audio/sfx/ability_cast.wav", - "portal_spawn": "res://assets/audio/sfx/portal_spawn.wav", - "invasion_alarm": "res://assets/audio/sfx/invasion_alarm.wav", - "tavern_damage": "res://assets/audio/sfx/tavern_damage.wav", -} +const SND_HIT: AudioStream = preload("res://assets/audio/sfx/hit.wav") +const SND_DEATH: AudioStream = preload("res://assets/audio/sfx/death.wav") +const SND_ABILITY: AudioStream = preload("res://assets/audio/sfx/ability_cast.wav") +const SND_PORTAL: AudioStream = preload("res://assets/audio/sfx/portal_spawn.wav") +const SND_LEVEL: AudioStream = preload("res://assets/audio/sfx/level_up.wav") +const SND_INVASION: AudioStream = preload("res://assets/audio/sfx/invasion_alarm.wav") +const SND_VILLAGE: AudioStream = preload("res://assets/audio/sfx/tavern_damage.wav") +const MUS_TAVERN: AudioStream = preload("res://assets/audio/music/tavern.wav") +const MUS_BATTLE: AudioStream = preload("res://assets/audio/music/battle.wav") +const MUS_INVASION: AudioStream = preload("res://assets/audio/music/invasion.wav") -const MUSIC_PATHS := { - "tavern": "res://assets/audio/music/tavern.wav", - "battle": "res://assets/audio/music/battle.wav", - "invasion": "res://assets/audio/music/invasion.wav", -} - -const SFX_POOL_SIZE := 8 - -var sfx_cache: Dictionary = {} -var music_cache: Dictionary = {} -var sfx_players: Array[AudioStreamPlayer] = [] -var music_player: AudioStreamPlayer = null -var current_music: String = "" +var music_player: AudioStreamPlayer func _ready() -> void: - for i in range(SFX_POOL_SIZE): - var p := AudioStreamPlayer.new() - p.volume_db = -6.0 - add_child(p) - sfx_players.append(p) music_player = AudioStreamPlayer.new() - music_player.volume_db = -12.0 - music_player.finished.connect(_on_music_finished) + music_player.bus = "Master" + music_player.volume_db = -10.0 add_child(music_player) - _preload_audio() - EventBus.attack_executed.connect(_on_attack_executed) - EventBus.entity_died.connect(_on_entity_died) + EventBus.damage_dealt.connect(_on_damage_dealt) + EventBus.entity_died.connect(_on_died) + EventBus.ability_used.connect(_on_ability_used) + EventBus.portal_spawned.connect(_on_portal_spawned) EventBus.level_up.connect(_on_level_up) - EventBus.portal_spawn.connect(_on_portal_spawn) - EventBus.tavern_damaged.connect(_on_tavern_damaged) EventBus.invasion_started.connect(_on_invasion_started) - EventBus.invasion_ended.connect(_on_invasion_ended) + EventBus.village_damaged.connect(_on_village_damaged) EventBus.wave_started.connect(_on_wave_started) - _play_music("tavern") + call_deferred("_play_music", MUS_TAVERN) -func _preload_audio() -> void: - for key in SFX_PATHS: - var path: String = SFX_PATHS[key] - if ResourceLoader.exists(path): - var stream: AudioStream = load(path) - if stream: - sfx_cache[key] = stream - for key in MUSIC_PATHS: - var path: String = MUSIC_PATHS[key] - if ResourceLoader.exists(path): - var stream: AudioStream = load(path) - if stream is AudioStreamWAV: - (stream as AudioStreamWAV).loop_mode = AudioStreamWAV.LOOP_FORWARD - (stream as AudioStreamWAV).loop_end = (stream as AudioStreamWAV).data.size() / 2 - if stream: - music_cache[key] = stream +func _on_damage_dealt(_a: Node, target: Node, _amount: float) -> void: + _play_at(target, SND_HIT, -8.0) -func play_sfx(key: String) -> void: - if not sfx_cache.has(key): - return - for p in sfx_players: - if not p.playing: - p.stream = sfx_cache[key] - p.play() - return +func _on_died(entity: Node) -> void: + _play_at(entity, SND_DEATH, -4.0) -func _play_music(key: String) -> void: - if current_music == key and music_player.playing: +func _on_ability_used(player: Node, _i: int, _a: Resource) -> void: + _play_at(player, SND_ABILITY, -10.0) + +func _on_portal_spawned(p: Node) -> void: + _play_at(p, SND_PORTAL, -4.0) + +func _on_level_up(_p: Node, _l: int) -> void: + _play_global(SND_LEVEL, -4.0) + +func _on_invasion_started() -> void: + _play_global(SND_INVASION, -2.0) + _play_music(MUS_INVASION) + +func _on_village_damaged(_c: float, _m: float) -> void: + _play_global(SND_VILLAGE, -8.0) + +func _on_wave_started(_n: int) -> void: + _play_music(MUS_BATTLE) + +func _play_at(node: Node, stream: AudioStream, vol: float) -> void: + if not is_instance_valid(node) or not (node is Node3D): return - if not music_cache.has(key): + var p := AudioStreamPlayer3D.new() + p.stream = stream + p.volume_db = vol + p.max_distance = 30.0 + get_tree().current_scene.add_child(p) + p.global_position = (node as Node3D).global_position + p.play() + p.finished.connect(func(): p.queue_free()) + +func _play_global(stream: AudioStream, vol: float) -> void: + var p := AudioStreamPlayer.new() + p.stream = stream + p.volume_db = vol + add_child(p) + p.play() + p.finished.connect(func(): p.queue_free()) + +func _play_music(stream: AudioStream) -> void: + if music_player.stream == stream and music_player.playing: return - music_player.stream = music_cache[key] + music_player.stream = stream music_player.play() - current_music = key - -func _on_music_finished() -> void: - if current_music != "" and music_cache.has(current_music): - music_player.play() - -func _on_attack_executed(_attacker, _pos, _dir, _dmg) -> void: - play_sfx("hit") - -func _on_entity_died(entity: Node) -> void: - if entity == PlayerData: - return - play_sfx("death") - -func _on_level_up(_player, _level) -> void: - play_sfx("level_up") - -func _on_portal_spawn(_portal, _enemies) -> void: - play_sfx("portal_spawn") - -func _on_tavern_damaged(_current, _max_val) -> void: - play_sfx("tavern_damage") - -func _on_invasion_started(_enemies) -> void: - play_sfx("invasion_alarm") - _play_music("invasion") - -func _on_invasion_ended(_success) -> void: - _play_music("battle") - -func _on_wave_started(_wave) -> void: - if current_music != "invasion": - _play_music("battle") diff --git a/systems/audio_system.gd.uid b/systems/audio_system.gd.uid index c4ab9eb..bf89c95 100644 --- a/systems/audio_system.gd.uid +++ b/systems/audio_system.gd.uid @@ -1 +1 @@ -uid://cbfc1ys0i4svm +uid://h2qcbg7iv2yn diff --git a/systems/aura_system.gd b/systems/aura_system.gd deleted file mode 100644 index ad2b8c2..0000000 --- a/systems/aura_system.gd +++ /dev/null @@ -1,62 +0,0 @@ -extends Node - -const AURA_REFRESH := 0.5 - -var active_auras: Dictionary = {} - -func _ready() -> void: - EventBus.role_changed.connect(_on_role_changed) - EventBus.entity_died.connect(_on_entity_died) - -func _process(_delta: float) -> void: - for entity in active_auras.keys(): - if not is_instance_valid(entity): - active_auras.erase(entity) - continue - for aura in active_auras[entity]: - _propagate(entity, aura) - -func _propagate(source_entity: Node, aura: Effect) -> void: - if not source_entity is Node3D: - return - var buff_system: Node = get_node("../BuffSystem") - var players := get_tree().get_nodes_in_group("player") - for player in players: - if not is_instance_valid(player) or not PlayerData.alive: - continue - var dist: float = source_entity.global_position.distance_to(player.global_position) - if dist > aura.aura_radius: - continue - if buff_system.has_aura_buff(player, aura.effect_name, source_entity): - buff_system.refresh_aura_buff(player, aura.effect_name, source_entity, AURA_REFRESH) - else: - var buff := Effect.new() - buff.effect_name = aura.effect_name - buff.type = Effect.Type.BUFF - buff.stat = aura.stat - buff.value = aura.value - buff.duration = AURA_REFRESH - buff.is_multiplier = aura.is_multiplier - buff_system.apply_aura_buff(player, buff, source_entity) - -func _on_role_changed(player: Node, _role_type: int) -> void: - active_auras.erase(player) - var ability_set: AbilitySet = PlayerData.ability_set - if not ability_set: - return - for ability in ability_set.abilities: - if ability and ability.type == Ability.Type.PASSIVE: - var effect := Effect.new() - effect.effect_name = ability.ability_name - effect.type = Effect.Type.AURA - effect.stat = ability.passive_stat - effect.value = ability.damage / 100.0 - effect.duration = -1.0 - effect.is_multiplier = true - effect.aura_radius = ability.ability_range - if not active_auras.has(player): - active_auras[player] = [] - active_auras[player].append(effect) - -func _on_entity_died(entity: Node) -> void: - active_auras.erase(entity) diff --git a/systems/aura_system.gd.uid b/systems/aura_system.gd.uid deleted file mode 100644 index 98a7a87..0000000 --- a/systems/aura_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b17o3hfdm8uo6 diff --git a/systems/buff_system.gd b/systems/buff_system.gd deleted file mode 100644 index 01dc477..0000000 --- a/systems/buff_system.gd +++ /dev/null @@ -1,142 +0,0 @@ -extends Node - -var active_buffs: Dictionary = {} - -func _ready() -> void: - EventBus.effect_requested.connect(_on_effect_requested) - EventBus.role_changed.connect(_on_role_changed) - EventBus.entity_died.connect(_on_entity_died) - -func _process(delta: float) -> void: - for entity in active_buffs.keys(): - if not is_instance_valid(entity): - active_buffs.erase(entity) - continue - var entries: Array = active_buffs[entity] - var i: int = entries.size() - 1 - while i >= 0: - var entry: Dictionary = entries[i] - var effect: Effect = entry["effect"] - if effect.duration > 0: - entry["remaining"] -= delta - if entry["remaining"] <= 0: - var is_aura_buff: bool = entry.get("is_aura_buff", false) - entries.remove_at(i) - if not is_aura_buff: - EventBus.effect_expired.emit(entity, effect) - _recalc(entity) - i -= 1 - continue - if effect.tick_interval > 0: - entry["tick_timer"] -= delta - if entry["tick_timer"] <= 0: - entry["tick_timer"] += effect.tick_interval - if not effect.is_multiplier and effect.type == Effect.Type.BUFF: - var source: Node = entry["source"] - if not is_instance_valid(source): - source = entity - EventBus.heal_requested.emit(source, entity, effect.value) - i -= 1 - -func apply(target: Node, effect: Effect, source: Node) -> void: - if effect.type != Effect.Type.BUFF and effect.type != Effect.Type.AURA: - return - if not active_buffs.has(target): - active_buffs[target] = [] - var replaced := false - var entries: Array = active_buffs[target] - for i in range(entries.size()): - if entries[i]["effect"].effect_name == effect.effect_name: - entries[i]["effect"] = effect - entries[i]["source"] = source - entries[i]["remaining"] = effect.duration - entries[i]["tick_timer"] = effect.tick_interval - replaced = true - break - if not replaced: - entries.append({ - "effect": effect, - "source": source, - "remaining": effect.duration, - "tick_timer": effect.tick_interval, - }) - EventBus.effect_applied.emit(target, effect) - if effect.is_multiplier: - _recalc(target) - -func apply_aura_buff(target: Node, effect: Effect, source: Node) -> void: - if not active_buffs.has(target): - active_buffs[target] = [] - var entry := { - "effect": effect, - "source": source, - "remaining": effect.duration, - "tick_timer": effect.tick_interval, - "aura_source": source, - "is_aura_buff": true, - } - active_buffs[target].append(entry) - if effect.is_multiplier: - _recalc(target) - -func has_aura_buff(target: Node, aura_name: String, source: Node) -> bool: - if not active_buffs.has(target): - return false - for entry in active_buffs[target]: - if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source: - return true - return false - -func refresh_aura_buff(target: Node, aura_name: String, source: Node, duration: float) -> void: - if not active_buffs.has(target): - return - for entry in active_buffs[target]: - if entry["effect"].effect_name == aura_name and entry.get("aura_source") == source: - entry["remaining"] = duration - return - -func clear(entity: Node) -> void: - active_buffs.erase(entity) - _recalc(entity) - -func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void: - if effect.type == Effect.Type.BUFF or effect.type == Effect.Type.AURA: - apply(target, effect, source) - -func _on_entity_died(entity: Node) -> void: - active_buffs.erase(entity) - _recalc(entity) - -func _on_role_changed(player: Node, _role_type: int) -> void: - _remove_permanent(player) - -func _remove_permanent(entity: Node) -> void: - if not active_buffs.has(entity): - return - var entries: Array = active_buffs[entity] - var i: int = entries.size() - 1 - while i >= 0: - if entries[i]["effect"].duration < 0: - EventBus.effect_expired.emit(entity, entries[i]["effect"]) - entries.remove_at(i) - i -= 1 - _recalc(entity) - -func _recalc(entity: Node) -> void: - var mults := { "damage": 1.0, "heal": 1.0, "shield": 1.0 } - if active_buffs.has(entity): - for entry in active_buffs[entity]: - var effect: Effect = entry["effect"] - if effect.is_multiplier and effect.stat in mults: - mults[effect.stat] += effect.value - var player: Node = get_tree().get_first_node_in_group("player") - if entity == player: - PlayerData.buff_damage = mults["damage"] - PlayerData.buff_heal = mults["heal"] - PlayerData.buff_shield = mults["shield"] - if PlayerData.base: - var new_max: float = PlayerData.base.max_shield * mults["shield"] - PlayerData.max_shield = new_max - PlayerData.shield = min(PlayerData.shield, new_max) - PlayerData.set_shield(PlayerData.shield) - EventBus.buff_changed.emit(entity, "damage", mults["damage"]) diff --git a/systems/buff_system.gd.uid b/systems/buff_system.gd.uid deleted file mode 100644 index d11e7f8..0000000 --- a/systems/buff_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://y2bm5ssu77wp diff --git a/systems/building_system.gd b/systems/building_system.gd new file mode 100644 index 0000000..2c7a881 --- /dev/null +++ b/systems/building_system.gd @@ -0,0 +1,92 @@ +extends Node + +const BUILDING_SCENE: PackedScene = preload("res://scenes/entities/building/building.tscn") +const GRID_SIZE: float = 1.0 + +@onready var inventory_system: Node = get_node("../InventorySystem") + +var blueprints: Array = [] + +func _ready() -> void: + blueprints = [ + {"id": &"floor", "name": "Floor", "size": Vector3(1, 0.2, 1), "color": Color(0.55, 0.4, 0.25), "material": &"wood", "cost": 1}, + {"id": &"wall", "name": "Wall", "size": Vector3(1, 2, 0.2), "color": Color(0.7, 0.6, 0.45), "material": &"wood", "cost": 1}, + {"id": &"door", "name": "Door", "size": Vector3(1, 2, 0.15), "color": Color(0.4, 0.25, 0.15), "material": &"wood", "cost": 2}, + {"id": &"roof", "name": "Roof", "size": Vector3(1, 0.2, 1), "color": Color(0.5, 0.3, 0.2), "material": &"wood", "cost": 1} + ] + +func _building_root() -> Node3D: + var n: Node = get_node_or_null("/root/World/EntityRoot/Buildings") + if n == null: + n = get_node_or_null("/root/Dungeon/EntityRoot/Buildings") + return n + +func get_blueprints() -> Array: + return blueprints + +func find_blueprint(id: StringName) -> Dictionary: + for b in blueprints: + if b.id == id: + return b + return {} + +func snap_position(pos: Vector3) -> Vector3: + return Vector3(round(pos.x / GRID_SIZE) * GRID_SIZE, max(0.0, pos.y), round(pos.z / GRID_SIZE) * GRID_SIZE) + +func place(player: Node, id: StringName, pos: Vector3, rot: float) -> bool: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + request_place.rpc_id(1, player.get_path(), id, pos, rot) + return false + var bp: Dictionary = find_blueprint(id) + if bp.is_empty(): + return false + var mat: StringName = bp.material + var cost: int = bp.cost + if inventory_system.get_amount(player, mat) < cost: + return false + inventory_system.remove_item(player, mat, cost) + var root := _building_root() + if root == null: + return false + var b: StaticBody3D = BUILDING_SCENE.instantiate() + b.building_id = id + b.name = "B_%s_%d" % [str(id), Time.get_ticks_msec() + randi() % 1000] + root.add_child(b, true) + b.global_position = snap_position(pos) + b.rotation.y = rot + b.apply_building(id) + _apply_building_visual.rpc(b.get_path(), id) + EventBus.building_placed.emit(b) + return true + +func remove(player: Node, b_path: NodePath) -> bool: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + request_remove.rpc_id(1, player.get_path(), b_path) + return false + var node: Node = get_node_or_null(b_path) + if node == null: + return false + var bp: Dictionary = find_blueprint(node.building_id) + if not bp.is_empty(): + inventory_system.add_item(player, bp.material, max(1, int(float(bp.cost) * 0.5))) + EventBus.building_removed.emit(node) + node.queue_free() + return true + +@rpc("any_peer", "reliable") +func request_place(player_path: NodePath, id: StringName, pos: Vector3, rot: float) -> void: + var p: Node = get_node_or_null(player_path) + if p: + place(p, id, pos, rot) + +@rpc("any_peer", "reliable") +func request_remove(player_path: NodePath, b_path: NodePath) -> void: + var p: Node = get_node_or_null(player_path) + if p: + remove(p, b_path) + +@rpc("authority", "reliable", "call_local") +func _apply_building_visual(b_path: NodePath, id: StringName) -> void: + var node: Node = get_node_or_null(b_path) + if node and node.has_method("apply_building"): + node.apply_building(id) diff --git a/systems/building_system.gd.uid b/systems/building_system.gd.uid new file mode 100644 index 0000000..6e05507 --- /dev/null +++ b/systems/building_system.gd.uid @@ -0,0 +1 @@ +uid://wt6sjhxolm4g diff --git a/systems/chat_system.gd b/systems/chat_system.gd new file mode 100644 index 0000000..5f6deca --- /dev/null +++ b/systems/chat_system.gd @@ -0,0 +1,16 @@ +extends Node + +func send(text: String) -> void: + var msg: String = text.strip_edges() + if msg == "": + return + var sender: String = Net.local_name + var id: int = Net.local_id() + if multiplayer.multiplayer_peer == null: + EventBus.chat_message.emit(id, sender, msg) + return + _broadcast.rpc(id, sender, msg) + +@rpc("any_peer", "reliable", "call_local") +func _broadcast(peer_id: int, sender: String, msg: String) -> void: + EventBus.chat_message.emit(peer_id, sender, msg) diff --git a/systems/chat_system.gd.uid b/systems/chat_system.gd.uid new file mode 100644 index 0000000..5c50ffc --- /dev/null +++ b/systems/chat_system.gd.uid @@ -0,0 +1 @@ +uid://c035shi1tlmql diff --git a/systems/combat/ability_system.gd b/systems/combat/ability_system.gd new file mode 100644 index 0000000..17d1357 --- /dev/null +++ b/systems/combat/ability_system.gd @@ -0,0 +1,166 @@ +extends Node + +@onready var role_system: Node = get_node("../RoleSystem") +@onready var cooldown_system: Node = get_node("../CooldownSystem") + +func _ready() -> void: + EventBus.ability_use_requested.connect(_on_ability_request) + +func _on_ability_request(player: Node, index: int) -> void: + if not is_instance_valid(player) or not Stats.has(player): + return + var target_str: String = "" + var tv: Variant = (player as Node).get("current_target") + if tv != null and is_instance_valid(tv) and tv is Node: + target_str = String((tv as Node).get_path()) + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + _request_use.rpc_id(1, String(player.get_path()), index, target_str) + return + _execute(player, index, target_str) + +@rpc("any_peer", "reliable") +func _request_use(path_str: String, index: int, target_str: String) -> void: + var p: Node = get_node_or_null(NodePath(path_str)) + if p == null: + return + if target_str != "": + var t := get_node_or_null(NodePath(target_str)) + if t and Stats.has(t): + p.set("current_target", t) + _execute(p, index, target_str) + +func _execute(player: Node, index: int, _target_str: String = "") -> void: + if index < 0 or index > 3: + return + var role: int = int(Stats.get_stat(player, "role", GameState.ROLE_DAMAGE)) + var set: AbilitySet = role_system.get_set(role) + if set == null or index >= set.abilities.size(): + return + var ability: Ability = set.abilities[index] + if ability == null: + return + if not cooldown_system.is_ready(player, index): + return + if ability.uses_gcd and not cooldown_system.is_gcd_ready(player): + return + cooldown_system.set_cooldown(player, index, ability.cooldown, float(Stats.get_stat(player, "gcd_time", 0.5)) if ability.uses_gcd else 0.0) + EventBus.ability_used.emit(player, index, ability) + _apply_ability(player, ability) + +func _apply_ability(player: Node, ability: Ability) -> void: + match ability.type: + Ability.Type.SINGLE: + _apply_single(player, ability) + Ability.Type.AOE: + _apply_aoe(player, ability) + Ability.Type.UTILITY: + _apply_utility(player, ability) + Ability.Type.ULT: + _apply_ult(player, ability) + _: + pass + +func _apply_single(player: Node, ability: Ability) -> void: + var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0)) + var heal_mult: float = float(Stats.get_stat(player, "buff_heal", 1.0)) + var target: Node = _resolve_target(player, ability) + if target == null: + return + if (player as Node3D).global_position.distance_to((target as Node3D).global_position) > ability.ability_range: + return + if ability.is_heal: + EventBus.heal_requested.emit(player, target, ability.damage * heal_mult) + else: + EventBus.damage_requested.emit(player, target, ability.damage * dmg_mult, ability.element) + if ability.shield_value > 0.0: + _apply_shield(player, ability.shield_value) + +func _apply_aoe(player: Node, ability: Ability) -> void: + var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0)) + var heal_mult: float = float(Stats.get_stat(player, "buff_heal", 1.0)) + var origin: Vector3 = (player as Node3D).global_position + var center: Vector3 = origin + var target: Node = _resolve_target(player, ability) + if target and (target as Node3D).global_position.distance_to(origin) <= ability.ability_range: + center = (target as Node3D).global_position + if ability.is_heal: + for ally in get_tree().get_nodes_in_group("player"): + if (ally as Node3D).global_position.distance_to(center) <= ability.aoe_radius: + EventBus.heal_requested.emit(player, ally, ability.damage * heal_mult) + else: + var hits: int = 0 + for foe in Stats.entities_in_group(&"enemies"): + if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius: + EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element) + hits += 1 + for foe in Stats.entities_in_group(&"portals"): + if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius: + EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element) + hits += 1 + for foe in Stats.entities_in_group(&"gates"): + if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius: + EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element) + hits += 1 + if ability.shield_value > 0.0: + _apply_shield(player, ability.shield_value * hits) + +func _apply_utility(player: Node, ability: Ability) -> void: + if ability.shield_multiplier > 0.0: + var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0)) + var add: float = max_shield * ability.shield_multiplier + Stats.set_stat(player, "shield", min(max_shield, float(Stats.get_stat(player, "shield", 0.0)) + add)) + EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield) + +func _apply_ult(player: Node, ability: Ability) -> void: + if ability.is_heal: + _apply_aoe(player, ability) + elif ability.shield_multiplier > 0.0: + var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0)) + Stats.set_stat(player, "shield", min(max_shield * (1.0 + ability.shield_multiplier), float(Stats.get_stat(player, "shield", 0.0)) + max_shield * ability.shield_multiplier)) + EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield) + else: + var target: Node = _resolve_target(player, ability) + if target == null: + return + var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0)) + EventBus.damage_requested.emit(player, target, ability.damage * dmg_mult, ability.element) + if ability.aoe_radius > 0.0: + var center: Vector3 = (target as Node3D).global_position + for foe in Stats.entities_in_group(&"enemies"): + if foe == target: + continue + if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius: + EventBus.damage_requested.emit(player, foe, ability.damage * 0.5 * dmg_mult, ability.element) + +func _apply_shield(player: Node, amount: float) -> void: + var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0)) + Stats.set_stat(player, "shield", min(max_shield, float(Stats.get_stat(player, "shield", 0.0)) + amount)) + EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield) + +func _resolve_target(player: Node, ability: Ability) -> Node: + if ability.is_heal: + var lowest: Node = null + var lowest_pct: float = 2.0 + for ally in get_tree().get_nodes_in_group("player"): + if not Stats.has(ally): + continue + var hp: float = float(Stats.get_stat(ally, "health", 0.0)) + var max_hp: float = float(Stats.get_stat(ally, "max_health", 1.0)) + if max_hp <= 0.0 or hp <= 0.0: + continue + var pct: float = hp / max_hp + if pct < lowest_pct: + lowest = ally + lowest_pct = pct + return lowest + var tv: Variant = (player as Node).get("current_target") + if tv != null and is_instance_valid(tv) and tv is Node and Stats.has(tv): + return tv as Node + var nearest: Node = null + var nearest_dist: float = INF + for foe in Stats.entities_in_group(&"enemies") + Stats.entities_in_group(&"portals") + Stats.entities_in_group(&"gates"): + var d: float = (foe as Node3D).global_position.distance_to((player as Node3D).global_position) + if d < nearest_dist and d <= ability.ability_range: + nearest = foe + nearest_dist = d + return nearest diff --git a/systems/combat/ability_system.gd.uid b/systems/combat/ability_system.gd.uid new file mode 100644 index 0000000..8f58bb7 --- /dev/null +++ b/systems/combat/ability_system.gd.uid @@ -0,0 +1 @@ +uid://e47e8o4ofgtg diff --git a/systems/combat/auto_attack_system.gd b/systems/combat/auto_attack_system.gd new file mode 100644 index 0000000..bf5c2df --- /dev/null +++ b/systems/combat/auto_attack_system.gd @@ -0,0 +1,74 @@ +extends Node + +@onready var role_system: Node = get_node("../RoleSystem") +@onready var cooldown_system: Node = get_node("../CooldownSystem") + +var _accum: float = 0.0 + +func _ready() -> void: + if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer): + set_physics_process(false) + else: + set_physics_process(true) + +func _physics_process(delta: float) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + _accum += delta + if _accum < 0.10: + return + _accum = 0.0 + for player in get_tree().get_nodes_in_group("player"): + if not Stats.has(player): + continue + if int(Stats.get_stat(player, "health", 0.0)) <= 0: + continue + if not cooldown_system.is_aa_ready(player): + continue + var role: int = int(Stats.get_stat(player, "role", GameState.ROLE_DAMAGE)) + var set: AbilitySet = role_system.get_set(role) + if set == null: + continue + var target: Node = _pick_target(player, set) + if target == null: + continue + var dist: float = (player as Node3D).global_position.distance_to((target as Node3D).global_position) + if dist > set.aa_range: + continue + var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0)) + var heal_mult: float = float(Stats.get_stat(player, "buff_heal", 1.0)) + if set.aa_is_heal: + EventBus.heal_requested.emit(player, target, set.aa_damage * heal_mult) + else: + EventBus.damage_requested.emit(player, target, set.aa_damage * dmg_mult, Element.NONE) + cooldown_system.set_aa(player, float(Stats.get_stat(player, "aa_cooldown", 0.5))) + +func _pick_target(player: Node, set: AbilitySet) -> Node: + if set.aa_is_heal: + var lowest: Node = null + var lowest_pct: float = 2.0 + for ally in get_tree().get_nodes_in_group("player"): + if not Stats.has(ally): + continue + var hp: float = float(Stats.get_stat(ally, "health", 0.0)) + var max_hp: float = float(Stats.get_stat(ally, "max_health", 1.0)) + if hp <= 0.0: + continue + var pct: float = hp / max_hp + if pct < lowest_pct and (player as Node3D).global_position.distance_to((ally as Node3D).global_position) <= set.aa_range: + lowest = ally + lowest_pct = pct + return lowest + var current_var: Variant = (player as Node).get("current_target") + if current_var != null and is_instance_valid(current_var) and current_var is Node and Stats.has(current_var): + return current_var + var nearest: Node = null + var nearest_dist: float = INF + for foe in Stats.entities_in_group(&"enemies"): + if not is_instance_valid(foe): + continue + var d: float = (foe as Node3D).global_position.distance_to((player as Node3D).global_position) + if d < nearest_dist and d <= set.aa_range: + nearest = foe + nearest_dist = d + return nearest diff --git a/systems/combat/auto_attack_system.gd.uid b/systems/combat/auto_attack_system.gd.uid new file mode 100644 index 0000000..149addf --- /dev/null +++ b/systems/combat/auto_attack_system.gd.uid @@ -0,0 +1 @@ +uid://comfiqsxl5t1e diff --git a/systems/cooldown_system.gd b/systems/cooldown_system.gd index 8f0cc02..6d5d7cc 100644 --- a/systems/cooldown_system.gd +++ b/systems/cooldown_system.gd @@ -1,19 +1,80 @@ extends Node +const ABILITY_COUNT: int = 4 + +var _cds: Dictionary = {} +var _max_cds: Dictionary = {} +var _gcd: Dictionary = {} +var _aa: Dictionary = {} + func _ready() -> void: + EventBus.entity_deregistered.connect(_on_dereg) EventBus.role_changed.connect(_on_role_changed) + set_physics_process(true) -func _process(delta: float) -> void: - if PlayerData.gcd > 0: - PlayerData.gcd -= delta - if PlayerData.aa_timer > 0: - PlayerData.aa_timer -= delta - for i in range(PlayerData.cooldowns.size()): - if PlayerData.cooldowns[i] > 0: - PlayerData.cooldowns[i] -= delta - EventBus.cooldown_tick.emit(PlayerData.cooldowns, PlayerData.max_cooldowns, PlayerData.gcd) +func register(entity: Node) -> void: + _cds[entity] = PackedFloat32Array([0, 0, 0, 0]) + _max_cds[entity] = PackedFloat32Array([0, 0, 0, 0]) + _gcd[entity] = 0.0 + _aa[entity] = 0.0 -func _on_role_changed(_player: Node, _role_type: int) -> void: - PlayerData.cooldowns.fill(0.0) - PlayerData.max_cooldowns.fill(0.0) - PlayerData.gcd = 0.0 +func _on_dereg(entity: Node) -> void: + _cds.erase(entity) + _max_cds.erase(entity) + _gcd.erase(entity) + _aa.erase(entity) + +func _on_role_changed(player: Node, _role: int) -> void: + if player in _cds: + var z: PackedFloat32Array = PackedFloat32Array([0, 0, 0, 0]) + _cds[player] = z + _max_cds[player] = z.duplicate() + _gcd[player] = 0.0 + _aa[player] = 0.0 + +func is_ready(entity: Node, index: int) -> bool: + if not entity in _cds: + return true + if index < 0 or index >= ABILITY_COUNT: + return false + return _cds[entity][index] <= 0.0 + +func is_gcd_ready(entity: Node) -> bool: + return _gcd.get(entity, 0.0) <= 0.0 + +func is_aa_ready(entity: Node) -> bool: + return _aa.get(entity, 0.0) <= 0.0 + +func set_cooldown(entity: Node, index: int, cd: float, gcd: float = 0.0) -> void: + if not entity in _cds: + register(entity) + _cds[entity][index] = cd + _max_cds[entity][index] = cd + if gcd > 0.0: + _gcd[entity] = gcd + +func set_aa(entity: Node, value: float) -> void: + _aa[entity] = value + +var _emit_accum: float = 0.0 + +func _physics_process(delta: float) -> void: + for entity in _cds.keys(): + if not is_instance_valid(entity): + continue + var arr: PackedFloat32Array = _cds[entity] + for i in range(arr.size()): + if arr[i] > 0.0: + arr[i] = max(0.0, arr[i] - delta) + _cds[entity] = arr + if _gcd.get(entity, 0.0) > 0.0: + _gcd[entity] = max(0.0, _gcd[entity] - delta) + if _aa.get(entity, 0.0) > 0.0: + _aa[entity] = max(0.0, _aa[entity] - delta) + _emit_accum += delta + if _emit_accum < 0.10: + return + _emit_accum = 0.0 + for entity in _cds.keys(): + if is_instance_valid(entity) and entity.is_in_group("player") and entity.is_multiplayer_authority(): + EventBus.cooldown_tick.emit(entity, _cds[entity], _max_cds[entity], _gcd.get(entity, 0.0)) diff --git a/systems/cooldown_system.gd.uid b/systems/cooldown_system.gd.uid index f0728cc..6e6b781 100644 --- a/systems/cooldown_system.gd.uid +++ b/systems/cooldown_system.gd.uid @@ -1 +1 @@ -uid://ddos7mo8rahou +uid://cr6jycuq6udgf diff --git a/systems/crafting_system.gd b/systems/crafting_system.gd new file mode 100644 index 0000000..7c85d9b --- /dev/null +++ b/systems/crafting_system.gd @@ -0,0 +1,49 @@ +extends Node + +@onready var inventory_system: Node = get_node("../InventorySystem") + +var recipes: Array = [] + +func _ready() -> void: + recipes = [ + {"id": &"wood", "name": "Wood", "out_count": 1, "inputs": {&"essence": 1}}, + {"id": &"stone", "name": "Stone", "out_count": 1, "inputs": {&"essence": 2}}, + {"id": &"iron", "name": "Iron", "out_count": 1, "inputs": {&"essence": 5}}, + {"id": &"sword", "name": "Sword", "out_count": 1, "inputs": {&"iron": 3, &"wood": 1}}, + ] + +func get_recipes() -> Array: + return recipes + +func can_craft(player: Node, recipe: Dictionary) -> bool: + for input_id in recipe.inputs.keys(): + if inventory_system.get_amount(player, input_id) < recipe.inputs[input_id]: + return false + return true + +func craft(player: Node, recipe_id: StringName) -> bool: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + request_craft.rpc_id(1, player.get_path(), recipe_id) + return false + var recipe: Dictionary = _find_recipe(recipe_id) + if recipe.is_empty(): + return false + if not can_craft(player, recipe): + return false + for input_id in recipe.inputs.keys(): + inventory_system.remove_item(player, input_id, recipe.inputs[input_id]) + inventory_system.add_item(player, recipe.id, recipe.out_count) + EventBus.item_crafted.emit(player, recipe.id) + return true + +@rpc("any_peer", "reliable") +func request_craft(path: NodePath, recipe_id: StringName) -> void: + var player: Node = get_node_or_null(path) + if player: + craft(player, recipe_id) + +func _find_recipe(id: StringName) -> Dictionary: + for r in recipes: + if r.id == id: + return r + return {} diff --git a/systems/crafting_system.gd.uid b/systems/crafting_system.gd.uid new file mode 100644 index 0000000..f433966 --- /dev/null +++ b/systems/crafting_system.gd.uid @@ -0,0 +1 @@ +uid://c4rcp5bw5rjr1 diff --git a/systems/damage_system.gd b/systems/damage_system.gd deleted file mode 100644 index 84a07af..0000000 --- a/systems/damage_system.gd +++ /dev/null @@ -1,44 +0,0 @@ -extends Node - -func _ready() -> void: - EventBus.damage_requested.connect(_on_damage_requested) - -func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void: - var remaining: float = amount - if not target.is_in_group("tavern"): - var shield_system: Node = get_node_or_null("../ShieldSystem") - if shield_system: - remaining = shield_system.absorb(target, remaining) - EventBus.damage_dealt.emit(attacker, target, amount) - if remaining > 0: - _apply_damage(target, remaining) - -func _apply_damage(entity: Node, amount: float) -> void: - if entity == _get_player(): - var health: float = PlayerData.health - amount - if health < 0: - health = 0 - PlayerData.set_health(health) - elif entity.is_in_group("boss"): - var health: float = BossData.get_stat(entity, "health") - amount - if health < 0: - health = 0 - BossData.set_health(entity, health) - elif entity.is_in_group("enemies"): - var health: float = EnemyData.get_stat(entity, "health") - amount - if health < 0: - health = 0 - EnemyData.set_health(entity, health) - elif entity.is_in_group("portals"): - var health: float = PortalData.get_stat(entity, "health") - amount - if health < 0: - health = 0 - PortalData.set_health(entity, health) - elif entity.is_in_group("tavern"): - var health: float = TavernData.get_stat(entity, "health") - amount - if health < 0: - health = 0 - TavernData.set_health(entity, health) - -func _get_player() -> Node: - return get_tree().get_first_node_in_group("player") diff --git a/systems/damage_system.gd.uid b/systems/damage_system.gd.uid deleted file mode 100644 index fcb128f..0000000 --- a/systems/damage_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cmy1kqo1pk1q8 diff --git a/systems/debuff_system.gd b/systems/debuff_system.gd deleted file mode 100644 index 45042cd..0000000 --- a/systems/debuff_system.gd +++ /dev/null @@ -1,65 +0,0 @@ -extends Node - -var active_debuffs: Dictionary = {} - -func _ready() -> void: - EventBus.effect_requested.connect(_on_effect_requested) - EventBus.entity_died.connect(_on_entity_died) - -func _process(delta: float) -> void: - for entity in active_debuffs.keys(): - if not is_instance_valid(entity): - active_debuffs.erase(entity) - continue - var entries: Array = active_debuffs[entity] - var i: int = entries.size() - 1 - while i >= 0: - var entry: Dictionary = entries[i] - var effect: Effect = entry["effect"] - if effect.duration > 0: - entry["remaining"] -= delta - if entry["remaining"] <= 0: - entries.remove_at(i) - EventBus.effect_expired.emit(entity, effect) - i -= 1 - continue - if effect.tick_interval > 0: - entry["tick_timer"] -= delta - if entry["tick_timer"] <= 0: - entry["tick_timer"] += effect.tick_interval - var source: Node = entry["source"] - if not is_instance_valid(source): - source = entity - EventBus.damage_requested.emit(source, entity, effect.value) - i -= 1 - -func apply(target: Node, effect: Effect, source: Node) -> void: - if effect.type != Effect.Type.DEBUFF: - return - if not active_debuffs.has(target): - active_debuffs[target] = [] - var replaced := false - var entries: Array = active_debuffs[target] - for i in range(entries.size()): - if entries[i]["effect"].effect_name == effect.effect_name: - entries[i]["effect"] = effect - entries[i]["source"] = source - entries[i]["remaining"] = effect.duration - entries[i]["tick_timer"] = effect.tick_interval - replaced = true - break - if not replaced: - entries.append({ - "effect": effect, - "source": source, - "remaining": effect.duration, - "tick_timer": effect.tick_interval, - }) - EventBus.effect_applied.emit(target, effect) - -func _on_effect_requested(target: Node, effect: Effect, source: Node) -> void: - if effect.type == Effect.Type.DEBUFF: - apply(target, effect, source) - -func _on_entity_died(entity: Node) -> void: - active_debuffs.erase(entity) diff --git a/systems/debuff_system.gd.uid b/systems/debuff_system.gd.uid deleted file mode 100644 index 517bb53..0000000 --- a/systems/debuff_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ce12ledregjqx diff --git a/systems/dialog_system.gd b/systems/dialog_system.gd new file mode 100644 index 0000000..5c182e9 --- /dev/null +++ b/systems/dialog_system.gd @@ -0,0 +1,92 @@ +extends Node + +const OLLAMA_URL: String = "http://127.0.0.1:11434/api/generate" +const DEFAULT_MODEL: String = "qwen2.5:0.5b" +const REQUEST_TIMEOUT: float = 25.0 + +var _http: HTTPRequest +var _pending: Dictionary = {} +var _pending_target: Node = null + +func _ready() -> void: + _http = HTTPRequest.new() + _http.timeout = REQUEST_TIMEOUT + add_child(_http) + _http.request_completed.connect(_on_response) + +func ask(npc: Node, player: Node, question: String) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + request_ask.rpc_id(1, npc.get_path(), player.get_path(), question) + return + var profile: NpcProfile = npc.profile + var system_prompt: String = "Du bist %s, eine NPC in einem mittelalterlichen Dorf. Lore: %s. Persönlichkeit: %s. Antworte knapp (max 2 Sätze) auf Deutsch und bleibe immer in deiner Rolle. Erfinde keine Fakten über die Welt." % [profile.display_name, profile.lore, profile.personality] + var prompt: String = "Spieler: %s\n%s:" % [question, profile.display_name] + _send_request(npc, player, system_prompt, prompt) + +@rpc("any_peer", "reliable") +func request_ask(npc_path: NodePath, player_path: NodePath, question: String) -> void: + var npc: Node = get_node_or_null(npc_path) + var player: Node = get_node_or_null(player_path) + if npc and player: + ask(npc, player, question) + +func _send_request(npc: Node, player: Node, system_prompt: String, prompt: String) -> void: + var body: Dictionary = { + "model": DEFAULT_MODEL, + "prompt": prompt, + "system": system_prompt, + "stream": false, + "options": {"temperature": 0.7, "num_predict": 120} + } + var headers := PackedStringArray(["Content-Type: application/json"]) + var json := JSON.stringify(body) + var token: int = randi() + _pending[token] = {"npc": npc, "player": player} + _pending_target = player + var err := _http.request(OLLAMA_URL, headers, HTTPClient.METHOD_POST, json) + if err != OK: + _send_fallback(npc, player) + +func _on_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + var entry_keys: Array = _pending.keys() + if entry_keys.is_empty(): + return + var token: int = entry_keys[0] + var entry: Dictionary = _pending[token] + _pending.erase(token) + var npc: Node = entry.npc + var player: Node = entry.player + if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: + _send_fallback(npc, player) + return + var text: String = body.get_string_from_utf8() + var data: Variant = JSON.parse_string(text) + if typeof(data) != TYPE_DICTIONARY or not "response" in data: + _send_fallback(npc, player) + return + var answer: String = (data.response as String).strip_edges() + if answer == "": + _send_fallback(npc, player) + return + _deliver(player, npc, answer) + +func _send_fallback(npc: Node, player: Node) -> void: + if not is_instance_valid(npc): + return + var profile: NpcProfile = npc.profile + _deliver(player, npc, profile.fallback_text) + +func _deliver(player: Node, npc: Node, text: String) -> void: + if player == null or not is_instance_valid(player): + return + var auth: int = player.get_multiplayer_authority() + _on_dialog_answer.rpc_id(auth, npc.get_path(), text) + +@rpc("authority", "reliable", "call_local") +func _on_dialog_answer(npc_path: NodePath, text: String) -> void: + var npc: Node = get_node_or_null(npc_path) + if npc: + EventBus.chat_message.emit(0, npc.profile.display_name if npc.has_method("get") else "NPC", text) + for hud in get_tree().get_nodes_in_group("dialog_ui"): + if hud.has_method("show_answer"): + hud.show_answer(text) diff --git a/systems/dialog_system.gd.uid b/systems/dialog_system.gd.uid new file mode 100644 index 0000000..c1d7e53 --- /dev/null +++ b/systems/dialog_system.gd.uid @@ -0,0 +1 @@ +uid://brxmr4qquuu5o diff --git a/systems/dungeon_system.gd b/systems/dungeon_system.gd deleted file mode 100644 index f6a9f94..0000000 --- a/systems/dungeon_system.gd +++ /dev/null @@ -1,13 +0,0 @@ -extends Node - -func _ready() -> void: - EventBus.entity_died.connect(_on_entity_died) - -func _on_entity_died(entity: Node) -> void: - if entity.is_in_group("boss"): - await get_tree().create_timer(2.0).timeout - PlayerData.dungeon_cleared = true - PlayerData.returning_from_dungeon = false - PlayerData.clear_cache() - EventBus.dungeon_cleared.emit() - get_tree().change_scene_to_file("res://scenes/world/world.tscn") diff --git a/systems/dungeon_system.gd.uid b/systems/dungeon_system.gd.uid deleted file mode 100644 index e963515..0000000 --- a/systems/dungeon_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://lc5n3uxi4fho diff --git a/systems/effect.gd b/systems/effect.gd deleted file mode 100644 index 7b91f89..0000000 --- a/systems/effect.gd +++ /dev/null @@ -1,14 +0,0 @@ -extends Resource -class_name Effect - -enum Type { BUFF, DEBUFF, AURA } - -@export var effect_name: String = "" -@export var type: Type = Type.BUFF -@export var stat: String = "" -@export var value: float = 0.0 -@export var duration: float = -1.0 -@export var is_multiplier: bool = true -@export var aura_radius: float = 0.0 -@export var tick_interval: float = 0.0 -@export var element: int = 0 diff --git a/systems/effect.gd.uid b/systems/effect.gd.uid deleted file mode 100644 index 6897369..0000000 --- a/systems/effect.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://djbni7iy5pw2m diff --git a/systems/effect_system.gd b/systems/effect_system.gd new file mode 100644 index 0000000..0c09aa1 --- /dev/null +++ b/systems/effect_system.gd @@ -0,0 +1,138 @@ +extends Node + +var _active: Dictionary = {} +var _passive_auras: Dictionary = {} + +func _ready() -> void: + EventBus.entity_died.connect(_on_died) + EventBus.entity_deregistered.connect(_on_dereg) + if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer): + set_physics_process(false) + else: + set_physics_process(true) + +var _heavy_accum: float = 0.0 + +func _physics_process(delta: float) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + _tick_durations(delta) + _heavy_accum += delta + if _heavy_accum < 0.20: + return + _heavy_accum = 0.0 + _propagate_auras() + _recompute_buffs() + +func apply(target: Node, effect: Effect, source: Node = null) -> void: + if not is_instance_valid(target): + return + if not target in _active: + _active[target] = [] + for entry in _active[target]: + if entry.effect.effect_name == effect.effect_name and entry.source == source: + entry.remaining = effect.duration + entry.tick_timer = effect.tick_interval + EventBus.effect_applied.emit(target, effect, source) + return + _active[target].append({ + "effect": effect, + "source": source, + "remaining": effect.duration, + "tick_timer": effect.tick_interval + }) + EventBus.effect_applied.emit(target, effect, source) + +func remove_by_source(target: Node, source: Node) -> void: + if not target in _active: + return + var keep: Array = [] + for entry in _active[target]: + if entry.source == source: + EventBus.effect_expired.emit(target, entry.effect) + else: + keep.append(entry) + _active[target] = keep + +func apply_passive_aura(player: Node, ability: Ability) -> void: + if ability == null or ability.passive_stat == &"": + return + _passive_auras[player] = ability + for target in get_tree().get_nodes_in_group("player"): + remove_by_source(target, player) + +func _propagate_auras() -> void: + for source in _passive_auras.keys(): + if not is_instance_valid(source): + _passive_auras.erase(source) + continue + var ability: Ability = _passive_auras[source] + var effect := Effect.new() + effect.effect_name = StringName("aura_%s_%s" % [ability.passive_stat, source.name]) + effect.type = Effect.Type.AURA + effect.stat = ability.passive_stat + effect.value = ability.passive_value + effect.is_multiplier = true + effect.duration = 0.5 + effect.aura_radius = ability.passive_radius + var src_pos: Vector3 = (source as Node3D).global_position + for target in get_tree().get_nodes_in_group("player"): + if not Stats.has(target): + continue + var d: float = (target as Node3D).global_position.distance_to(src_pos) + if d <= ability.passive_radius: + apply(target, effect, source) + +func _tick_durations(delta: float) -> void: + for target in _active.keys(): + if not is_instance_valid(target): + _active.erase(target) + continue + var keep: Array = [] + for entry in _active[target]: + if entry.effect.duration > 0.0: + entry.remaining -= delta + if entry.remaining <= 0.0: + EventBus.effect_expired.emit(target, entry.effect) + continue + if entry.effect.tick_interval > 0.0: + entry.tick_timer -= delta + if entry.tick_timer <= 0.0: + entry.tick_timer = entry.effect.tick_interval + if entry.effect.type == Effect.Type.DOT: + EventBus.damage_requested.emit(entry.source, target, entry.effect.value, entry.effect.element) + elif entry.effect.type == Effect.Type.HOT: + EventBus.heal_requested.emit(entry.source, target, entry.effect.value) + keep.append(entry) + _active[target] = keep + +func _recompute_buffs() -> void: + for target in _active.keys(): + if not is_instance_valid(target) or not Stats.has(target): + continue + var bonus: Dictionary = {} + var mult: Dictionary = {} + for entry in _active[target]: + var e: Effect = entry.effect + if e.stat == &"": + continue + if e.is_multiplier: + mult[e.stat] = mult.get(e.stat, 0.0) + e.value + else: + bonus[e.stat] = bonus.get(e.stat, 0.0) + e.value + for stat in [&"buff_damage", &"buff_heal", &"buff_shield"]: + var base: float = 1.0 + var v: float = base + mult.get(stat, 0.0) + bonus.get(stat, 0.0) + Stats.set_stat(target, stat, v) + EventBus.buff_changed.emit(target, stat, v) + +func _on_died(entity: Node) -> void: + if entity in _active: + for entry in _active[entity]: + EventBus.effect_expired.emit(entity, entry.effect) + _active.erase(entity) + _passive_auras.erase(entity) + +func _on_dereg(entity: Node) -> void: + _active.erase(entity) + _passive_auras.erase(entity) diff --git a/systems/effect_system.gd.uid b/systems/effect_system.gd.uid new file mode 100644 index 0000000..1c1902d --- /dev/null +++ b/systems/effect_system.gd.uid @@ -0,0 +1 @@ +uid://dai1m8j8conpk diff --git a/systems/element_system.gd b/systems/element_system.gd index e4fba33..7e63b5c 100644 --- a/systems/element_system.gd +++ b/systems/element_system.gd @@ -1,53 +1,38 @@ extends Node -enum Element { NONE, FIRE } +@onready var effect_system: Node = get_node("../EffectSystem") -var applied_elements: Dictionary = {} +var _applied: Dictionary = {} func _ready() -> void: - EventBus.element_damage_dealt.connect(_on_element_damage_dealt) - EventBus.entity_died.connect(_on_entity_died) - EventBus.effect_expired.connect(_on_effect_expired) + EventBus.damage_requested.connect(_on_damage_requested) + EventBus.entity_died.connect(_on_died) -func _on_element_damage_dealt(attacker: Node, target: Node, _amount: float, element: int) -> void: +func _on_damage_requested(attacker: Node, target: Node, _amount: float, element: int) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return if element == Element.NONE: return - if not target.is_in_group("enemies") and not target.is_in_group("portals"): + if not is_instance_valid(target): return - var current: int = applied_elements.get(target, Element.NONE) - if current != Element.NONE and current != element: - _trigger_reaction(attacker, target, current, element) + if not (target.is_in_group("enemies") or target.is_in_group("portals") or target.is_in_group("gates")): return - _apply_element(attacker, target, element) + apply_element(attacker, target, element) -func _apply_element(source: Node, target: Node, element: int) -> void: - applied_elements[target] = element +func apply_element(source: Node, target: Node, element: int) -> void: + if element == Element.NONE or not is_instance_valid(target): + return + _applied[target] = element EventBus.element_applied.emit(target, element) - match element: - Element.FIRE: - _apply_fire(source, target) + if element == Element.FIRE: + var dot := Effect.new() + dot.effect_name = &"fire_dot" + dot.type = Effect.Type.DOT + dot.value = 3.0 + dot.duration = 6.0 + dot.tick_interval = 2.0 + dot.element = Element.FIRE + effect_system.apply(target, dot, source) -func _apply_fire(source: Node, target: Node) -> void: - var fire_dot := Effect.new() - fire_dot.effect_name = "Burning" - fire_dot.type = Effect.Type.DEBUFF - fire_dot.stat = "damage" - fire_dot.value = 3.0 - fire_dot.duration = 6.0 - fire_dot.is_multiplier = false - fire_dot.tick_interval = 2.0 - fire_dot.element = Element.FIRE - EventBus.effect_requested.emit(target, fire_dot, source) - -func _trigger_reaction(_attacker: Node, target: Node, _elem_a: int, _elem_b: int) -> void: - applied_elements.erase(target) - EventBus.element_reaction.emit(target, _elem_a, _elem_b, "") - -func _on_entity_died(entity: Node) -> void: - applied_elements.erase(entity) - -func _on_effect_expired(target: Node, effect: Effect) -> void: - if effect.element != Element.NONE: - var current: int = applied_elements.get(target, Element.NONE) - if current == effect.element: - applied_elements.erase(target) +func _on_died(entity: Node) -> void: + _applied.erase(entity) diff --git a/systems/element_system.gd.uid b/systems/element_system.gd.uid index e6bfc53..9f31661 100644 --- a/systems/element_system.gd.uid +++ b/systems/element_system.gd.uid @@ -1 +1 @@ -uid://bqebxfvticxto +uid://cndhsw6vikcd1 diff --git a/systems/heal_system.gd b/systems/heal_system.gd deleted file mode 100644 index dc6f72f..0000000 --- a/systems/heal_system.gd +++ /dev/null @@ -1,22 +0,0 @@ -extends Node - -func _ready() -> void: - EventBus.heal_requested.connect(_on_heal_requested) - -func _on_heal_requested(_healer: Node, target: Node, amount: float) -> void: - if target == _get_player(): - var health: float = min(PlayerData.health + amount, PlayerData.max_health) - PlayerData.set_health(health) - elif target.is_in_group("boss"): - var health: float = BossData.get_stat(target, "health") - var max_health: float = BossData.get_stat(target, "max_health") - health = min(health + amount, max_health) - BossData.set_health(target, health) - elif target.is_in_group("enemies"): - var health: float = EnemyData.get_stat(target, "health") - var max_health: float = EnemyData.get_stat(target, "max_health") - health = min(health + amount, max_health) - EnemyData.set_health(target, health) - -func _get_player() -> Node: - return get_tree().get_first_node_in_group("player") diff --git a/systems/heal_system.gd.uid b/systems/heal_system.gd.uid deleted file mode 100644 index bf85d80..0000000 --- a/systems/heal_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://8jyik37e4tjw diff --git a/systems/health_system.gd b/systems/health_system.gd index 6726a2f..a15ac73 100644 --- a/systems/health_system.gd +++ b/systems/health_system.gd @@ -1,32 +1,69 @@ extends Node func _ready() -> void: - _emit_initial.call_deferred() + EventBus.damage_requested.connect(_on_damage_requested) + EventBus.heal_requested.connect(_on_heal_requested) + if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer): + set_physics_process(false) + else: + set_physics_process(true) -func _process(delta: float) -> void: - _regen_player(delta) - _regen_entities(delta, EnemyData.entities) - _regen_entities(delta, BossData.entities) +var _regen_accum: float = 0.0 -func _regen_player(delta: float) -> void: - if not PlayerData.alive or PlayerData.health_regen <= 0: +func _physics_process(delta: float) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: return - if PlayerData.health < PlayerData.max_health: - var health: float = min(PlayerData.health + PlayerData.health_regen * delta, PlayerData.max_health) - PlayerData.set_health(health) - -func _regen_entities(delta: float, entities: Dictionary) -> void: - for entity in entities: + _regen_accum += delta + if _regen_accum < 0.20: + return + var dt: float = _regen_accum + _regen_accum = 0.0 + for entity in Stats.entities(): if not is_instance_valid(entity): continue - var data: Dictionary = entities[entity] - if not data["alive"]: - continue - var regen: float = data["health_regen"] - if regen > 0 and data["health"] < data["max_health"]: - data["health"] = min(data["health"] + regen * delta, data["max_health"]) - EventBus.health_changed.emit(entity, data["health"], data["max_health"]) + var hp: float = float(Stats.get_stat(entity, "health", 0.0)) + var max_hp: float = float(Stats.get_stat(entity, "max_health", 0.0)) + var regen: float = float(Stats.get_stat(entity, "health_regen", 0.0)) + if regen > 0.0 and hp > 0.0 and hp < max_hp: + var new_hp: float = min(max_hp, hp + regen * dt) + Stats.set_stat(entity, "health", new_hp) + EventBus.health_changed.emit(entity, new_hp, max_hp) -func _emit_initial() -> void: - EventBus.health_changed.emit(PlayerData, PlayerData.health, PlayerData.max_health) - EventBus.shield_changed.emit(PlayerData, PlayerData.shield, PlayerData.max_shield) +func _on_damage_requested(attacker: Node, target: Node, amount: float, element: int) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + if not is_instance_valid(target) or not Stats.has(target): + return + var hp: float = float(Stats.get_stat(target, "health", 0.0)) + if hp <= 0.0: + return + var remaining := amount + var shield: float = float(Stats.get_stat(target, "shield", 0.0)) + if shield > 0.0: + var absorbed: float = min(shield, remaining) + Stats.set_stat(target, "shield", shield - absorbed) + Stats.set_stat(target, "shield_regen_timer", float(Stats.get_stat(target, "shield_regen_delay", 5.0))) + remaining -= absorbed + EventBus.shield_changed.emit(target, Stats.get_stat(target, "shield"), Stats.get_stat(target, "max_shield", 0.0)) + if shield - absorbed <= 0.0: + EventBus.shield_broken.emit(target) + if remaining > 0.0: + var new_hp: float = max(0.0, hp - remaining) + Stats.set_stat(target, "health", new_hp) + EventBus.health_changed.emit(target, new_hp, Stats.get_stat(target, "max_health")) + EventBus.damage_dealt.emit(attacker, target, amount) + if new_hp <= 0.0: + EventBus.entity_died.emit(target) + +func _on_heal_requested(healer: Node, target: Node, amount: float) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return + if not is_instance_valid(target) or not Stats.has(target): + return + var hp: float = float(Stats.get_stat(target, "health", 0.0)) + if hp <= 0.0: + return + var max_hp: float = float(Stats.get_stat(target, "max_health", 0.0)) + var new_hp: float = min(max_hp, hp + amount) + Stats.set_stat(target, "health", new_hp) + EventBus.health_changed.emit(target, new_hp, max_hp) diff --git a/systems/health_system.gd.uid b/systems/health_system.gd.uid index 3feac41..87baaa3 100644 --- a/systems/health_system.gd.uid +++ b/systems/health_system.gd.uid @@ -1 +1 @@ -uid://h362ftxb0cns +uid://bm2g2nxog0nxu diff --git a/systems/hud_system.gd b/systems/hud_system.gd deleted file mode 100644 index 50a94ff..0000000 --- a/systems/hud_system.gd +++ /dev/null @@ -1,294 +0,0 @@ -extends Node - -const GCD_TIME := 0.5 -const ICON_SIZE := 20 -const FONT_SIZE := 14 -const BORDER_WIDTH := 2 -const MARGIN := 2 - -var ability_labels: Array[String] = ["1", "2", "3", "4", "P"] -var effect_container: HBoxContainer = null -var tavern_bar: ProgressBar = null -var tavern_label: Label = null -var wave_label: Label = null -var level_label: Label = null -var xp_bar: ProgressBar = null -var xp_label: Label = null - -func _ready() -> void: - EventBus.health_changed.connect(_on_health_changed) - EventBus.shield_changed.connect(_on_shield_changed) - EventBus.entity_died.connect(_on_entity_died) - EventBus.player_respawned.connect(_on_player_respawned) - EventBus.respawn_tick.connect(_on_respawn_tick) - EventBus.role_changed.connect(_on_role_changed) - EventBus.cooldown_tick.connect(_on_cooldown_tick) - EventBus.effect_applied.connect(_on_effect_applied) - EventBus.effect_expired.connect(_on_effect_expired) - EventBus.tavern_damaged.connect(_on_tavern_damaged) - EventBus.wave_started.connect(_on_wave_started) - EventBus.wave_timer_tick.connect(_on_wave_timer_tick) - EventBus.xp_gained.connect(_on_xp_gained) - EventBus.level_up.connect(_on_level_up) - _init_hud.call_deferred() - -func _init_hud() -> void: - var hud: CanvasLayer = _get_hud() - if not hud: - return - hud.get_node("RespawnTimer").visible = false - effect_container = HBoxContainer.new() - effect_container.name = "EffectContainer" - effect_container.position = Vector2(10, 60) - effect_container.add_theme_constant_override("separation", 3) - hud.add_child(effect_container) - _init_tavern_bar(hud) - _init_xp_bar(hud) - -func _init_tavern_bar(hud: CanvasLayer) -> void: - var container := VBoxContainer.new() - container.name = "TavernContainer" - container.anchor_left = 0.5 - container.anchor_right = 0.5 - container.offset_left = -150 - container.offset_right = 150 - container.offset_top = 10 - container.add_theme_constant_override("separation", 2) - wave_label = Label.new() - wave_label.text = "Welle 1 — 60:00" - wave_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - wave_label.add_theme_font_size_override("font_size", 18) - container.add_child(wave_label) - var title := Label.new() - title.text = "Taverne" - title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - title.add_theme_font_size_override("font_size", 14) - container.add_child(title) - tavern_bar = ProgressBar.new() - tavern_bar.custom_minimum_size = Vector2(300, 22) - tavern_bar.show_percentage = false - var bg := StyleBoxFlat.new() - bg.bg_color = Color(0.3, 0.1, 0.1, 1) - var fill := StyleBoxFlat.new() - fill.bg_color = Color(0.9, 0.7, 0.2, 1) - tavern_bar.add_theme_stylebox_override("background", bg) - tavern_bar.add_theme_stylebox_override("fill", fill) - tavern_bar.max_value = 5000.0 - tavern_bar.value = 5000.0 - container.add_child(tavern_bar) - tavern_label = Label.new() - tavern_label.text = "5000/5000" - tavern_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - tavern_label.anchor_left = 0.0 - tavern_label.anchor_right = 1.0 - tavern_bar.add_child(tavern_label) - hud.add_child(container) - -func _on_tavern_damaged(current: float, max_val: float) -> void: - if tavern_bar: - tavern_bar.max_value = max_val - tavern_bar.value = current - if tavern_label: - tavern_label.text = "%d/%d" % [current, max_val] - -func _init_xp_bar(hud: CanvasLayer) -> void: - var container := VBoxContainer.new() - container.name = "LevelContainer" - container.anchor_left = 1.0 - container.anchor_top = 1.0 - container.anchor_right = 1.0 - container.anchor_bottom = 1.0 - container.offset_left = -260 - container.offset_top = -80 - container.offset_right = -10 - container.offset_bottom = -10 - container.add_theme_constant_override("separation", 2) - level_label = Label.new() - level_label.text = "Level 1" - level_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT - level_label.add_theme_font_size_override("font_size", 20) - container.add_child(level_label) - xp_bar = ProgressBar.new() - xp_bar.custom_minimum_size = Vector2(250, 22) - xp_bar.max_value = 1 - xp_bar.value = 0 - xp_bar.show_percentage = false - var bg := StyleBoxFlat.new() - bg.bg_color = Color(0.1, 0.2, 0.1, 1) - var fill := StyleBoxFlat.new() - fill.bg_color = Color(0.3, 0.9, 0.3, 1) - xp_bar.add_theme_stylebox_override("background", bg) - xp_bar.add_theme_stylebox_override("fill", fill) - xp_label = Label.new() - xp_label.text = "0/1" - xp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - xp_label.anchor_left = 0.0 - xp_label.anchor_right = 1.0 - xp_bar.add_child(xp_label) - container.add_child(xp_bar) - hud.add_child(container) - _update_xp_ui() - -func _on_xp_gained(_player: Node, _amount: int) -> void: - _update_xp_ui() - -func _on_level_up(_player: Node, _new_level: int) -> void: - _update_xp_ui() - -func _update_xp_ui() -> void: - if level_label: - level_label.text = "Level %d" % PlayerData.level - if xp_bar: - xp_bar.max_value = PlayerData.xp_to_next - xp_bar.value = PlayerData.xp - if xp_label: - xp_label.text = "%d/%d" % [PlayerData.xp, PlayerData.xp_to_next] - -func _on_wave_started(_wave_number: int) -> void: - _update_wave_label() - -func _on_wave_timer_tick(_seconds_remaining: float) -> void: - _update_wave_label() - -func _update_wave_label() -> void: - if not wave_label: - return - var secs: int = int(max(0.0, GameState.wave_timer_remaining)) - var mm: int = secs / 60 - var ss: int = secs % 60 - wave_label.text = "Welle %d — %02d:%02d" % [GameState.current_wave, mm, ss] - -func _on_health_changed(entity: Node, current: float, max_val: float) -> void: - if entity != PlayerData: - return - var hud: CanvasLayer = _get_hud() - if not hud: - return - var bar: ProgressBar = hud.get_node("HealthBar") - bar.max_value = max_val - bar.value = current - hud.get_node("HealthBar/HealthLabel").text = "%d/%d" % [current, max_val] - -func _on_shield_changed(entity: Node, current: float, max_val: float) -> void: - if entity != PlayerData: - return - var hud: CanvasLayer = _get_hud() - if not hud: - return - var bar: ProgressBar = hud.get_node("ShieldBar") - bar.max_value = max_val - bar.value = current - hud.get_node("ShieldBar/ShieldLabel").text = "%d/%d" % [current, max_val] - -func _on_entity_died(entity: Node) -> void: - if entity != PlayerData: - return - var hud: CanvasLayer = _get_hud() - if hud: - hud.get_node("RespawnTimer").visible = true - -func _on_player_respawned(_player: Node) -> void: - var hud: CanvasLayer = _get_hud() - if hud: - hud.get_node("RespawnTimer").visible = false - -func _on_respawn_tick(timer: float) -> void: - var hud: CanvasLayer = _get_hud() - if hud: - hud.get_node("RespawnTimer").text = str(ceil(timer)) - -func _on_role_changed(_player: Node, role_type: int) -> void: - var hud: CanvasLayer = _get_hud() - if not hud: - return - var icon: Label = hud.get_node("AbilityBar/ClassIcon/Label") - match role_type: - 0: icon.text = "T" - 1: icon.text = "D" - 2: icon.text = "H" - -func _on_cooldown_tick(cooldowns: Array, max_cooldowns: Array, gcd_timer: float) -> void: - var hud: CanvasLayer = _get_hud() - if not hud: - return - var panels: Array = [ - hud.get_node("AbilityBar/Ability1"), - hud.get_node("AbilityBar/Ability2"), - hud.get_node("AbilityBar/Ability3"), - hud.get_node("AbilityBar/Ability4"), - hud.get_node("AbilityBar/Ability5"), - ] - for i in range(min(panels.size(), cooldowns.size())): - var panel: Panel = panels[i] - var label: Label = panel.get_node("Label") - var overlay: ColorRect = panel.get_node("CooldownOverlay") - var cd: float = cooldowns[i] - var gcd: float = gcd_timer if i != 2 and i != 4 else 0.0 - var active_cd: float = max(cd, gcd) - var max_cd: float = max_cooldowns[i] if max_cooldowns[i] > 0 else GCD_TIME - if active_cd > 0: - var ratio: float = clamp(active_cd / max_cd, 0.0, 1.0) - overlay.visible = true - overlay.anchor_bottom = ratio - label.text = str(ceil(active_cd)) - else: - overlay.visible = false - label.text = ability_labels[i] - -func _on_effect_applied(target: Node, effect: Effect) -> void: - if target != PlayerData: - return - if effect_container: - _add_icon(effect) - -func _on_effect_expired(target: Node, effect: Effect) -> void: - if target != PlayerData: - return - if effect_container: - _remove_icon(effect) - -func _add_icon(effect: Effect) -> void: - var panel := PanelContainer.new() - var style := StyleBoxFlat.new() - match effect.type: - Effect.Type.AURA: - style.bg_color = Color(0.15, 0.15, 0.3, 1) - style.border_color = Color(0.3, 0.5, 1.0, 1) - Effect.Type.BUFF: - style.bg_color = Color(0.15, 0.3, 0.15, 1) - style.border_color = Color(0.3, 1.0, 0.3, 1) - Effect.Type.DEBUFF: - style.bg_color = Color(0.3, 0.15, 0.15, 1) - style.border_color = Color(1.0, 0.3, 0.3, 1) - style.set_border_width_all(BORDER_WIDTH) - style.set_content_margin_all(MARGIN) - panel.add_theme_stylebox_override("panel", style) - var label := Label.new() - label.text = effect.effect_name.left(1) - label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - label.add_theme_font_size_override("font_size", FONT_SIZE) - label.add_theme_color_override("font_color", Color.WHITE) - label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE) - panel.add_child(label) - panel.custom_minimum_size = Vector2(ICON_SIZE + BORDER_WIDTH * 2, ICON_SIZE + BORDER_WIDTH * 2) - panel.set_meta("effect_type", effect.type) - panel.set_meta("effect_name", effect.effect_name) - var insert_idx := 0 - for child in effect_container.get_children(): - if child.has_meta("effect_type") and child.get_meta("effect_type") <= effect.type: - insert_idx += 1 - else: - break - effect_container.add_child(panel) - effect_container.move_child(panel, insert_idx) - -func _remove_icon(effect: Effect) -> void: - for child in effect_container.get_children(): - if child.has_meta("effect_type") and child.has_meta("effect_name"): - if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name: - child.queue_free() - return - -func _get_hud() -> CanvasLayer: - return get_tree().get_first_node_in_group("hud") as CanvasLayer diff --git a/systems/hud_system.gd.uid b/systems/hud_system.gd.uid deleted file mode 100644 index 919bef8..0000000 --- a/systems/hud_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://da87wrxxuhws1 diff --git a/systems/invasion_system.gd b/systems/invasion_system.gd index 94b147a..ee6cbef 100644 --- a/systems/invasion_system.gd +++ b/systems/invasion_system.gd @@ -1,88 +1,41 @@ extends Node -const ENEMY_SCENE: PackedScene = preload("res://scenes/enemy/enemy.tscn") -const BOSS_STATS: Resource = preload("res://scenes/enemy/boss_stats.tres") -const INVASION_COUNT := 16 -const SPAWN_RADIUS := 45.0 +@onready var spawn_system: Node = get_node("../SpawnSystem") +var active_enemies: Array = [] var active: bool = false -var invasion_enemies: Array[Node] = [] func _ready() -> void: - EventBus.wave_timer_tick.connect(_on_wave_timer_tick) - EventBus.entity_died.connect(_on_entity_died) - EventBus.tavern_destroyed.connect(_on_tavern_destroyed) + EventBus.entity_died.connect(_on_died) -func _on_wave_timer_tick(seconds_remaining: float) -> void: - if active: +func start(wave: int) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): return - if seconds_remaining > 0: - return - var has_alive_red := false - for p in get_tree().get_nodes_in_group("red_portal"): - if is_instance_valid(p) and PortalData.is_alive(p): - has_alive_red = true - break - if not has_alive_red: - return - trigger() - -func trigger() -> void: active = true - invasion_enemies.clear() - var tavern: Node = get_tree().get_first_node_in_group("tavern") - if not tavern: - active = false - return - var world: Node = get_tree().current_scene - var scale: float = PlayerData.level_scale * 10.0 - for enemy in get_tree().get_nodes_in_group("red_enemies"): - if is_instance_valid(enemy): - _convert_to_invasion(enemy, tavern) - for i in range(INVASION_COUNT): + active_enemies.clear() + var count: int = 6 + wave * 2 + for i in range(count): var angle: float = randf() * TAU - var pos := Vector3(cos(angle) * SPAWN_RADIUS, 0, sin(angle) * SPAWN_RADIUS) - var enemy: Node = ENEMY_SCENE.instantiate() - enemy.spawn_scale = scale - world.add_child(enemy) - enemy.global_position = pos - _convert_to_invasion(enemy, tavern) - var boss_angle: float = randf() * TAU - var boss_pos := Vector3(cos(boss_angle) * SPAWN_RADIUS, 0, sin(boss_angle) * SPAWN_RADIUS) - var boss: Node = ENEMY_SCENE.instantiate() - boss.add_to_group("boss") - boss.stats = BOSS_STATS - boss.spawn_scale = scale - world.add_child(boss) - boss.global_position = boss_pos - _convert_to_invasion(boss, tavern) - EventBus.invasion_started.emit(invasion_enemies) + var pos := Vector3(cos(angle) * 50.0, 0.5, sin(angle) * 50.0) + var e: Node = spawn_system.spawn_enemy_at(pos, true, 1.0 + wave * 0.5) + if e: + active_enemies.append(e) + e.set("origin", Vector3.ZERO) + var v: Node = get_tree().get_first_node_in_group("village") + if v: + e.set("invasion_target", v) -func _convert_to_invasion(enemy: Node, tavern: Node) -> void: - enemy.add_to_group("invasion") - var data_source: Node = BossData if enemy.is_in_group("boss") else EnemyData - data_source.set_stat(enemy, "target", tavern) - data_source.set_stat(enemy, "state", 1) - invasion_enemies.append(enemy) - -func _on_entity_died(entity: Node) -> void: +func _on_died(entity: Node) -> void: if not active: return - if entity not in invasion_enemies: + if entity in active_enemies: + active_enemies.erase(entity) + if active_enemies.is_empty(): + active = false + EventBus.invasion_ended.emit(true) + +func damage_village(amount: float) -> void: + var v: Node = get_tree().get_first_node_in_group("village") + if v == null: return - invasion_enemies.erase(entity) - var alive_count := 0 - for e in invasion_enemies: - if is_instance_valid(e): - alive_count += 1 - if alive_count == 0: - _end_invasion(true) - -func _end_invasion(success: bool) -> void: - active = false - invasion_enemies.clear() - EventBus.invasion_ended.emit(success) - -func _on_tavern_destroyed() -> void: - active = false - EventBus.game_over.emit() + EventBus.damage_requested.emit(v, v, amount, Element.NONE) diff --git a/systems/invasion_system.gd.uid b/systems/invasion_system.gd.uid index 80ffe88..7f7373a 100644 --- a/systems/invasion_system.gd.uid +++ b/systems/invasion_system.gd.uid @@ -1 +1 @@ -uid://841gb4nrydai +uid://ka51njikkl diff --git a/systems/inventory_system.gd b/systems/inventory_system.gd new file mode 100644 index 0000000..0ff4053 --- /dev/null +++ b/systems/inventory_system.gd @@ -0,0 +1,57 @@ +extends Node + +var inventories: Dictionary = {} +var equipment: Dictionary = {} + +func _ready() -> void: + EventBus.entity_deregistered.connect(_on_dereg) + +func _on_dereg(entity: Node) -> void: + inventories.erase(entity) + equipment.erase(entity) + +func add_item(player: Node, item_id: StringName, amount: int) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + return + if not player in inventories: + inventories[player] = {} + inventories[player][item_id] = inventories[player].get(item_id, 0) + amount + EventBus.inventory_changed.emit(player) + if not player.is_multiplayer_authority(): + _sync_inventory.rpc_id(player.get_multiplayer_authority(), inventories[player]) + +func remove_item(player: Node, item_id: StringName, amount: int) -> bool: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + return false + if not player in inventories: + return false + if inventories[player].get(item_id, 0) < amount: + return false + inventories[player][item_id] -= amount + if inventories[player][item_id] <= 0: + inventories[player].erase(item_id) + EventBus.inventory_changed.emit(player) + if not player.is_multiplayer_authority(): + _sync_inventory.rpc_id(player.get_multiplayer_authority(), inventories[player]) + return true + +func get_amount(player: Node, item_id: StringName) -> int: + if not player in inventories: + return 0 + return inventories[player].get(item_id, 0) + +func get_inventory(player: Node) -> Dictionary: + return inventories.get(player, {}) + +@rpc("authority", "reliable", "call_local") +func _sync_inventory(data: Dictionary) -> void: + var local: Node = _find_local_player() + if local: + inventories[local] = data + EventBus.inventory_changed.emit(local) + +func _find_local_player() -> Node: + for p in get_tree().get_nodes_in_group("player"): + if p.is_multiplayer_authority(): + return p + return null diff --git a/systems/inventory_system.gd.uid b/systems/inventory_system.gd.uid new file mode 100644 index 0000000..e06164d --- /dev/null +++ b/systems/inventory_system.gd.uid @@ -0,0 +1 @@ +uid://ctqu2tiv8jw3a diff --git a/systems/loot_system.gd b/systems/loot_system.gd new file mode 100644 index 0000000..cd45ce3 --- /dev/null +++ b/systems/loot_system.gd @@ -0,0 +1,27 @@ +extends Node + +const LOOT_SCENE: PackedScene = preload("res://scenes/entities/loot/loot_drop.tscn") + +func _loot_root() -> Node3D: + var n: Node = get_node_or_null("/root/World/EntityRoot/Loot") + if n == null: + n = get_node_or_null("/root/Dungeon/EntityRoot/Loot") + return n + +func drop_loot_for(entity: Node, pos: Vector3) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + return + var root := _loot_root() + if root == null: + return + var amount: int = 1 + var item_id: StringName = &"essence" + if entity.is_in_group("boss"): + amount = 5 + randi() % 5 + var loot: Area3D = LOOT_SCENE.instantiate() + loot.item_id = item_id + loot.amount = amount + loot.name = "Loot_%d" % (Time.get_ticks_msec() + randi() % 1000) + root.add_child(loot, true) + loot.global_position = pos + Vector3(randf_range(-0.5, 0.5), 0.4, randf_range(-0.5, 0.5)) + EventBus.loot_dropped.emit([item_id], loot.global_position) diff --git a/systems/loot_system.gd.uid b/systems/loot_system.gd.uid new file mode 100644 index 0000000..60ba6b5 --- /dev/null +++ b/systems/loot_system.gd.uid @@ -0,0 +1 @@ +uid://dh3ctjeb17wc5 diff --git a/systems/map_system.gd b/systems/map_system.gd new file mode 100644 index 0000000..b117c15 --- /dev/null +++ b/systems/map_system.gd @@ -0,0 +1,26 @@ +extends Node + +func get_marker_data() -> Array: + var out: Array = [] + for p in get_tree().get_nodes_in_group("player"): + if not (p is Node3D): + continue + out.append({"pos": (p as Node3D).global_position, "color": Color(0.3, 0.6, 1.0), "label": Net.player_names.get(p.peer_id if p.has_method("get") else 1, "P")}) + for e in get_tree().get_nodes_in_group("enemies"): + if e is Node3D: + out.append({"pos": (e as Node3D).global_position, "color": Color(0.9, 0.2, 0.2), "label": ""}) + for g in get_tree().get_nodes_in_group("gates"): + if g is Node3D: + var c: Color = Color(0.95, 0.2, 0.15) if g.is_in_group("red_gate") else Color(0.85, 0.55, 0.2) + out.append({"pos": (g as Node3D).global_position, "color": c, "label": "Gate"}) + for portal in get_tree().get_nodes_in_group("portals"): + if portal is Node3D: + var c2: Color = Color(0.95, 0.2, 0.15) if portal.is_in_group("red_portal") else Color(0.3, 0.6, 1.0) + out.append({"pos": (portal as Node3D).global_position, "color": c2, "label": "Portal"}) + for n in get_tree().get_nodes_in_group("npc"): + if n is Node3D: + out.append({"pos": (n as Node3D).global_position, "color": Color(0.9, 0.85, 0.5), "label": ""}) + for v in get_tree().get_nodes_in_group("village"): + if v is Node3D: + out.append({"pos": (v as Node3D).global_position, "color": Color(0.4, 0.85, 0.4), "label": "Village"}) + return out diff --git a/systems/map_system.gd.uid b/systems/map_system.gd.uid new file mode 100644 index 0000000..0e62905 --- /dev/null +++ b/systems/map_system.gd.uid @@ -0,0 +1 @@ +uid://kafmxo67e5xt diff --git a/systems/nameplate_system.gd b/systems/nameplate_system.gd deleted file mode 100644 index c417c5b..0000000 --- a/systems/nameplate_system.gd +++ /dev/null @@ -1,237 +0,0 @@ -extends Node - -const ICON_SIZE := 10 -const FONT_SIZE := 7 -const BORDER_WIDTH := 1 -const ICON_MARGIN := 0 -const BASE_HEIGHT := 29 - -var styles: Dictionary = {} - -func _ready() -> void: - EventBus.health_changed.connect(_on_health_changed) - EventBus.shield_changed.connect(_on_shield_changed) - EventBus.target_changed.connect(_on_target_changed) - EventBus.entity_died.connect(_on_entity_died) - EventBus.effect_applied.connect(_on_effect_applied) - EventBus.effect_expired.connect(_on_effect_expired) - EventBus.portal_spawn.connect(_on_portal_spawn) - EventBus.invasion_started.connect(_on_invasion_started) - _init_nameplates.call_deferred() - -func _on_invasion_started(enemies: Array) -> void: - for enemy in enemies: - if is_instance_valid(enemy): - _setup_nameplate.call_deferred(enemy) - -func _init_nameplates() -> void: - for enemy in get_tree().get_nodes_in_group("enemies"): - _setup_nameplate(enemy) - for portal in get_tree().get_nodes_in_group("portals"): - _setup_nameplate(portal) - -func _setup_nameplate(entity: Node) -> void: - var nameplate: Sprite3D = entity.get_node_or_null("Healthbar") - if not nameplate: - return - var viewport: SubViewport = nameplate.get_node("SubViewport") - nameplate.texture = viewport.get_texture() - var border: ColorRect = viewport.get_node_or_null("Border") - if border: - border.visible = false - -func _process(_delta: float) -> void: - var player: Node = get_tree().get_first_node_in_group("player") - for enemy in get_tree().get_nodes_in_group("enemies"): - if not is_instance_valid(enemy): - continue - var nameplate: Sprite3D = enemy.get_node_or_null("Healthbar") - if not nameplate: - continue - var health_bar: ProgressBar = nameplate.get_node("SubViewport/HealthBar") - var data_source: Node = _get_data_source(enemy) - if not data_source: - continue - if enemy not in styles: - var style_normal: StyleBoxFlat = health_bar.get_theme_stylebox("fill").duplicate() - var style_aggro: StyleBoxFlat = style_normal.duplicate() - style_aggro.bg_color = Color(0.2, 0.4, 0.9, 1) - styles[enemy] = { "normal": style_normal, "aggro": style_aggro } - var s: Dictionary = styles[enemy] - var enemy_target: Variant = data_source.get_stat(enemy, "target") - if player and enemy_target == player: - health_bar.add_theme_stylebox_override("fill", s["aggro"]) - else: - health_bar.add_theme_stylebox_override("fill", s["normal"]) - -func _on_health_changed(entity: Node, current: float, max_val: float) -> void: - if entity == PlayerData: - return - if not is_instance_valid(entity): - return - var nameplate: Sprite3D = entity.get_node_or_null("Healthbar") - if not nameplate: - return - if not nameplate.texture: - _setup_nameplate(entity) - var bar: ProgressBar = nameplate.get_node("SubViewport/HealthBar") - bar.max_value = max_val - bar.value = current - -func _on_shield_changed(entity: Node, current: float, max_val: float) -> void: - if entity == PlayerData: - return - if not is_instance_valid(entity): - return - var nameplate: Sprite3D = entity.get_node_or_null("Healthbar") - if not nameplate: - return - if not nameplate.texture: - _setup_nameplate(entity) - var bar: ProgressBar = nameplate.get_node_or_null("SubViewport/ShieldBar") - if not bar: - return - if max_val <= 0: - bar.visible = false - return - bar.visible = true - bar.max_value = max_val - bar.value = current - -func _on_target_changed(_player: Node, target: Node) -> void: - for enemy in get_tree().get_nodes_in_group("enemies"): - if not is_instance_valid(enemy): - continue - var nameplate: Sprite3D = enemy.get_node_or_null("Healthbar") - if nameplate: - if not nameplate.texture: - _setup_nameplate(enemy) - nameplate.get_node("SubViewport/Border").visible = (target == enemy) - for portal in get_tree().get_nodes_in_group("portals"): - if not is_instance_valid(portal): - continue - var nameplate: Sprite3D = portal.get_node_or_null("Healthbar") - if nameplate: - if not nameplate.texture: - _setup_nameplate(portal) - nameplate.get_node("SubViewport/Border").visible = (target == portal) - -func _on_entity_died(entity: Node) -> void: - if entity != PlayerData and is_instance_valid(entity): - styles.erase(entity) - -func _on_effect_applied(target: Node, effect: Effect) -> void: - if target == PlayerData: - return - if not is_instance_valid(target): - return - var nameplate: Sprite3D = target.get_node_or_null("Healthbar") - if not nameplate: - return - var container: HBoxContainer = _get_or_create_effect_container(nameplate) - _add_icon(container, effect) - _resize_viewport(nameplate) - -func _on_effect_expired(target: Node, effect: Effect) -> void: - if target == PlayerData: - return - if not is_instance_valid(target): - return - var nameplate: Sprite3D = target.get_node_or_null("Healthbar") - if not nameplate: - return - var container: HBoxContainer = nameplate.get_node_or_null("SubViewport/EffectContainer") - if container: - _remove_icon(container, effect) - _resize_viewport.call_deferred(nameplate) - -func _on_portal_spawn(_portal: Node, enemies: Array) -> void: - for enemy in enemies: - _setup_nameplate.call_deferred(enemy) - -func _get_or_create_effect_container(nameplate: Sprite3D) -> HBoxContainer: - var viewport: SubViewport = nameplate.get_node("SubViewport") - var container: HBoxContainer = viewport.get_node_or_null("EffectContainer") - if container: - return container - container = HBoxContainer.new() - container.name = "EffectContainer" - var health_bar: ProgressBar = viewport.get_node("HealthBar") - var shield_bar: ProgressBar = viewport.get_node_or_null("ShieldBar") - var y_pos: float = 0.0 - if shield_bar and shield_bar.visible: - y_pos = shield_bar.offset_bottom + 2 - else: - y_pos = health_bar.offset_bottom + 2 - container.position = Vector2(2, y_pos) - container.add_theme_constant_override("separation", 1) - viewport.add_child(container) - return container - -func _resize_viewport(nameplate: Sprite3D) -> void: - var viewport: SubViewport = nameplate.get_node("SubViewport") - var border: ColorRect = viewport.get_node("Border") - var container: HBoxContainer = viewport.get_node_or_null("EffectContainer") - if not container: - return - var icon_count := 0 - for child in container.get_children(): - if not child.is_queued_for_deletion(): - icon_count += 1 - if icon_count > 0: - var needed: int = int(container.position.y) + ICON_SIZE + 4 - viewport.size.y = max(BASE_HEIGHT, needed) - border.offset_bottom = viewport.size.y - else: - viewport.size.y = BASE_HEIGHT - border.offset_bottom = BASE_HEIGHT - -func _add_icon(container: HBoxContainer, effect: Effect) -> void: - var panel := PanelContainer.new() - var style := StyleBoxFlat.new() - match effect.type: - Effect.Type.AURA: - style.bg_color = Color(0.15, 0.15, 0.3, 1) - style.border_color = Color(0.3, 0.5, 1.0, 1) - Effect.Type.BUFF: - style.bg_color = Color(0.15, 0.3, 0.15, 1) - style.border_color = Color(0.3, 1.0, 0.3, 1) - Effect.Type.DEBUFF: - style.bg_color = Color(0.3, 0.15, 0.15, 1) - style.border_color = Color(1.0, 0.3, 0.3, 1) - style.set_border_width_all(BORDER_WIDTH) - style.set_content_margin_all(ICON_MARGIN) - panel.add_theme_stylebox_override("panel", style) - var label := Label.new() - label.text = effect.effect_name.left(1) - label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - label.add_theme_font_size_override("font_size", FONT_SIZE) - label.add_theme_color_override("font_color", Color.WHITE) - label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE) - panel.add_child(label) - panel.custom_minimum_size = Vector2(ICON_SIZE + BORDER_WIDTH * 2, ICON_SIZE + BORDER_WIDTH * 2) - panel.set_meta("effect_type", effect.type) - panel.set_meta("effect_name", effect.effect_name) - var insert_idx := 0 - for child in container.get_children(): - if child.has_meta("effect_type") and child.get_meta("effect_type") <= effect.type: - insert_idx += 1 - else: - break - container.add_child(panel) - container.move_child(panel, insert_idx) - -func _remove_icon(container: HBoxContainer, effect: Effect) -> void: - for child in container.get_children(): - if child.has_meta("effect_type") and child.has_meta("effect_name"): - if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name: - child.queue_free() - return - -func _get_data_source(entity: Node) -> Node: - if entity.is_in_group("boss"): - return BossData - elif entity.is_in_group("enemies"): - return EnemyData - return null diff --git a/systems/nameplate_system.gd.uid b/systems/nameplate_system.gd.uid deleted file mode 100644 index 6d2d652..0000000 --- a/systems/nameplate_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://yijhaxo8anul diff --git a/systems/npc_system.gd b/systems/npc_system.gd new file mode 100644 index 0000000..e920b1e --- /dev/null +++ b/systems/npc_system.gd @@ -0,0 +1,39 @@ +extends Node + +const NPC_SCENE: PackedScene = preload("res://scenes/entities/npc/npc.tscn") + +func _npc_root() -> Node3D: + var n: Node = get_node_or_null("/root/World/EntityRoot/Npcs") + return n + +func spawn_default_npcs() -> void: + var root := _npc_root() + if root == null: + return + var profiles: Array = [ + _make_profile(&"barkeep", "Brena", "Barkeep at the Crooked Wheel tavern.", "Warm, gossipy, has lived here her whole life."), + _make_profile(&"smith", "Halvor", "Village blacksmith. Lost an eye to a portal monster.", "Gruff, terse, cares deeply for the village."), + _make_profile(&"sage", "Eyrie", "Village sage. Reads the portals, claims to remember the time before.", "Cryptic, slow-spoken, weary."), + _make_profile(&"farmer", "Rolf", "Tends the small fields east of the village.", "Anxious, practical, wants the wars to end."), + ] + var radius: float = 6.0 + for i in range(profiles.size()): + var angle: float = float(i) * TAU / float(profiles.size()) + 0.3 + var pos := Vector3(cos(angle) * radius, 0.0, sin(angle) * radius) + var npc: StaticBody3D = NPC_SCENE.instantiate() + npc.profile = profiles[i] + npc.profile_id = profiles[i].npc_id + npc.name = "Npc_%s" % str(profiles[i].npc_id) + root.add_child(npc, true) + npc.global_position = pos + +func _make_profile(id: StringName, name: String, lore: String, pers: String) -> NpcProfile: + var p := NpcProfile.new() + p.npc_id = id + p.display_name = name + p.lore = lore + p.personality = pers + p.fallback_text = "..." + p.greeting = "Hallo, Reisender." + p.color = Color(0.55 + randf() * 0.35, 0.5 + randf() * 0.3, 0.4 + randf() * 0.3) + return p diff --git a/systems/npc_system.gd.uid b/systems/npc_system.gd.uid new file mode 100644 index 0000000..3fb5c1c --- /dev/null +++ b/systems/npc_system.gd.uid @@ -0,0 +1 @@ +uid://pk3bpy5mo0vb diff --git a/systems/portal_system.gd b/systems/portal_system.gd deleted file mode 100644 index 9a3e65d..0000000 --- a/systems/portal_system.gd +++ /dev/null @@ -1,19 +0,0 @@ -extends Node - -const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn") - -func _ready() -> void: - EventBus.entity_died.connect(_on_entity_died) - -func _on_entity_died(entity: Node) -> void: - if not entity.is_in_group("portals"): - return - if not entity.is_inside_tree(): - return - var pos: Vector3 = entity.global_position - var gate: Node3D = GATE_SCENE.instantiate() - entity.get_parent().add_child(gate) - gate.global_position = pos - gate.dungeon_variant = entity.stats.variant - EventBus.portal_defeated.emit(entity) - entity.queue_free() diff --git a/systems/portal_system.gd.uid b/systems/portal_system.gd.uid deleted file mode 100644 index acb14b0..0000000 --- a/systems/portal_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c5sqw08twxtnh diff --git a/systems/respawn_system.gd b/systems/respawn_system.gd index 69b1c1b..939a096 100644 --- a/systems/respawn_system.gd +++ b/systems/respawn_system.gd @@ -1,45 +1,54 @@ extends Node -var respawn_timer := 0.0 -var is_dead := false +var _dead: Dictionary = {} func _ready() -> void: EventBus.entity_died.connect(_on_entity_died) + if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer): + set_physics_process(false) + else: + set_physics_process(true) -func _process(delta: float) -> void: - if not is_dead: +func _physics_process(delta: float) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: return - respawn_timer -= delta - EventBus.respawn_tick.emit(respawn_timer) - if respawn_timer <= 0: - _respawn() + var to_respawn: Array = [] + for entity in _dead.keys(): + if not is_instance_valid(entity): + to_respawn.append(entity) + continue + _dead[entity] -= delta + if _dead[entity] <= 0.0: + to_respawn.append(entity) + for e in to_respawn: + _dead.erase(e) + if is_instance_valid(e) and e.is_in_group("player"): + _respawn_player(e) func _on_entity_died(entity: Node) -> void: - if entity != PlayerData: + if not is_instance_valid(entity): return - if is_dead: - return - is_dead = true - respawn_timer = PlayerData.respawn_time - var player: Node = get_tree().get_first_node_in_group("player") - if not player: - return - player.velocity = Vector3.ZERO - player.get_node("Mesh").visible = false - player.get_node("CollisionShape3D").disabled = true - player.get_node("Movement").set_physics_process(false) - player.get_node("Ability").set_process_unhandled_input(false) - player.get_node("Targeting").set_process_unhandled_input(false) + if entity.is_in_group("player"): + var t: float = float(Stats.get_stat(entity, "respawn_time", 3.0)) + _dead[entity] = t + if entity.has_method("set_dead"): + entity.set_dead.rpc(true) -func _respawn() -> void: - is_dead = false - var player: Node = get_tree().get_first_node_in_group("player") - if not player: - return - player.global_position = Vector3(0, 1, -5) - player.get_node("Mesh").visible = true - player.get_node("CollisionShape3D").disabled = false - player.get_node("Movement").set_physics_process(true) - player.get_node("Ability").set_process_unhandled_input(true) - player.get_node("Targeting").set_process_unhandled_input(true) - PlayerData.respawn() +func _respawn_player(entity: Node) -> void: + var max_hp: float = float(Stats.get_stat(entity, "max_health", 100.0)) + var max_shield: float = float(Stats.get_stat(entity, "max_shield", 0.0)) + Stats.set_stat(entity, "health", max_hp) + Stats.set_stat(entity, "shield", max_shield) + EventBus.health_changed.emit(entity, max_hp, max_hp) + EventBus.shield_changed.emit(entity, max_shield, max_shield) + EventBus.entity_respawned.emit(entity) + if entity.has_method("set_dead"): + entity.set_dead.rpc(false) + if entity.has_method("teleport_to"): + entity.teleport_to.rpc(_village_spawn_point()) + +func _village_spawn_point() -> Vector3: + var v: Variant = get_tree().get_first_node_in_group("village") + if v is Node3D: + return (v as Node3D).global_position + Vector3(0, 1, 0) + return Vector3(0, 1, 0) diff --git a/systems/respawn_system.gd.uid b/systems/respawn_system.gd.uid index 8b9fca0..1b9ff31 100644 --- a/systems/respawn_system.gd.uid +++ b/systems/respawn_system.gd.uid @@ -1 +1 @@ -uid://b1qkvoqvmd21h +uid://84nufoafsg60 diff --git a/systems/role_system.gd b/systems/role_system.gd index 9b70c3c..cf0fdce 100644 --- a/systems/role_system.gd +++ b/systems/role_system.gd @@ -1,23 +1,89 @@ extends Node -@export var tank_set: AbilitySet -@export var damage_set: AbilitySet -@export var healer_set: AbilitySet +var ability_sets: Dictionary = {} func _ready() -> void: - EventBus.role_change_requested.connect(_on_role_change_requested) - _apply_role.call_deferred(PlayerData.current_role) + ability_sets[GameState.ROLE_TANK] = _build_tank_set() + ability_sets[GameState.ROLE_DAMAGE] = _build_damage_set() + ability_sets[GameState.ROLE_HEALER] = _build_healer_set() + EventBus.role_changed.connect(_on_role_changed) -func _on_role_change_requested(_player: Node, role: int) -> void: - _apply_role(role) +func get_set(role: int) -> AbilitySet: + return ability_sets.get(role, ability_sets[GameState.ROLE_DAMAGE]) -func _apply_role(role: int) -> void: - PlayerData.current_role = role - match role: - PlayerData.Role.TANK: - PlayerData.ability_set = tank_set - PlayerData.Role.DAMAGE: - PlayerData.ability_set = damage_set - PlayerData.Role.HEALER: - PlayerData.ability_set = healer_set - PlayerData.set_role(role) +func _on_role_changed(player: Node, role: int) -> void: + var set: AbilitySet = get_set(role) + var passive: Ability = null + for a in set.abilities: + if a.type == Ability.Type.PASSIVE: + passive = a + break + var effects: Node = get_node_or_null("../EffectSystem") + if effects and passive and effects.has_method("apply_passive_aura"): + effects.apply_passive_aura(player, passive) + +func _make_ability(name: String, type: int, damage: float, ability_range: float, cooldown: float, opts: Dictionary = {}) -> Ability: + var a := Ability.new() + a.ability_name = StringName(name) + a.type = type + a.damage = damage + a.ability_range = ability_range + a.cooldown = cooldown + a.uses_gcd = opts.get("uses_gcd", true) + a.aoe_radius = opts.get("aoe_radius", 0.0) + a.is_heal = opts.get("is_heal", false) + a.shield_value = opts.get("shield_value", 0.0) + a.shield_multiplier = opts.get("shield_multiplier", 0.0) + a.passive_stat = opts.get("passive_stat", &"") + a.passive_value = opts.get("passive_value", 0.5) + a.passive_radius = opts.get("passive_radius", 50.0) + a.element = opts.get("element", Element.NONE) + return a + +func _build_tank_set() -> AbilitySet: + var s := AbilitySet.new() + s.role_name = &"Tank" + s.color = Color(0.3, 0.5, 0.95) + s.aa_damage = 5.0 + s.aa_range = 3.0 + s.aa_is_heal = false + s.abilities = [ + _make_ability("Smash", Ability.Type.SINGLE, 15.0, 3.0, 2.0, {"shield_value": 10.0}), + _make_ability("Sweep", Ability.Type.AOE, 10.0, 3.0, 3.0, {"aoe_radius": 10.0, "shield_value": 5.0}), + _make_ability("Bulwark", Ability.Type.UTILITY, 0.0, 0.0, 5.0, {"uses_gcd": false, "shield_multiplier": 1.0}), + _make_ability("Aegis", Ability.Type.ULT, 0.0, 0.0, 20.0, {"shield_multiplier": 3.0}), + _make_ability("Fortify", Ability.Type.PASSIVE, 0.0, 0.0, 0.0, {"passive_stat": &"buff_shield", "passive_value": 0.5, "passive_radius": 50.0}) + ] + return s + +func _build_damage_set() -> AbilitySet: + var s := AbilitySet.new() + s.role_name = &"Damage" + s.color = Color(0.95, 0.3, 0.3) + s.aa_damage = 10.0 + s.aa_range = 10.0 + s.aa_is_heal = false + s.abilities = [ + _make_ability("Bolt", Ability.Type.SINGLE, 30.0, 20.0, 2.0, {"element": Element.FIRE}), + _make_ability("Inferno", Ability.Type.AOE, 20.0, 20.0, 3.0, {"aoe_radius": 5.0, "element": Element.FIRE}), + _make_ability("Barrier", Ability.Type.UTILITY, 0.0, 0.0, 5.0, {"uses_gcd": false, "shield_multiplier": 1.0}), + _make_ability("Rain", Ability.Type.ULT, 50.0, 20.0, 15.0, {"aoe_radius": 3.0, "element": Element.FIRE}), + _make_ability("Power", Ability.Type.PASSIVE, 0.0, 0.0, 0.0, {"passive_stat": &"buff_damage", "passive_value": 0.5, "passive_radius": 50.0}) + ] + return s + +func _build_healer_set() -> AbilitySet: + var s := AbilitySet.new() + s.role_name = &"Healer" + s.color = Color(0.4, 0.85, 0.4) + s.aa_damage = 1.0 + s.aa_range = 20.0 + s.aa_is_heal = true + s.abilities = [ + _make_ability("Mend", Ability.Type.SINGLE, 15.0, 20.0, 2.0, {"is_heal": true}), + _make_ability("Bloom", Ability.Type.AOE, 10.0, 20.0, 3.0, {"aoe_radius": 20.0, "is_heal": true}), + _make_ability("Ward", Ability.Type.UTILITY, 0.0, 0.0, 5.0, {"uses_gcd": false, "shield_multiplier": 1.0}), + _make_ability("Sanctuary", Ability.Type.ULT, 25.0, 20.0, 15.0, {"aoe_radius": 3.0, "is_heal": true}), + _make_ability("Devotion", Ability.Type.PASSIVE, 0.0, 0.0, 0.0, {"passive_stat": &"buff_heal", "passive_value": 0.5, "passive_radius": 50.0}) + ] + return s diff --git a/systems/role_system.gd.uid b/systems/role_system.gd.uid index ee53254..7cf3d85 100644 --- a/systems/role_system.gd.uid +++ b/systems/role_system.gd.uid @@ -1 +1 @@ -uid://cuwueo5v43kap +uid://ci60ni5o8h0ie diff --git a/systems/shield_system.gd b/systems/shield_system.gd index 84e21e8..d57470f 100644 --- a/systems/shield_system.gd +++ b/systems/shield_system.gd @@ -1,74 +1,39 @@ extends Node -func _process(delta: float) -> void: - _process_player(delta) - _process_entities(delta, EnemyData.entities) - _process_entities(delta, BossData.entities) +func _ready() -> void: + if multiplayer.multiplayer_peer != null and not multiplayer.is_server() and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer): + set_physics_process(false) + else: + set_physics_process(true) -func _process_player(delta: float) -> void: - if not PlayerData.alive or PlayerData.max_shield <= 0: +var _regen_accum: float = 0.0 + +func _physics_process(delta: float) -> void: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: return - if PlayerData.shield < PlayerData.max_shield: - PlayerData.shield_regen_timer += delta - if PlayerData.shield_regen_timer >= PlayerData.shield_regen_delay: - var regen_rate: float = PlayerData.max_shield / PlayerData.shield_regen_time - var shield: float = PlayerData.shield + regen_rate * delta - if shield >= PlayerData.max_shield: - shield = PlayerData.max_shield - EventBus.shield_regenerated.emit(get_tree().get_first_node_in_group("player")) - PlayerData.set_shield(shield) - -func _process_entities(delta: float, entities: Dictionary) -> void: - for entity in entities: + _regen_accum += delta + if _regen_accum < 0.20: + return + var dt: float = _regen_accum + _regen_accum = 0.0 + for entity in Stats.entities(): if not is_instance_valid(entity): continue - var data: Dictionary = entities[entity] - if not data["alive"]: + var hp: float = float(Stats.get_stat(entity, "health", 0.0)) + if hp <= 0.0: continue - var max_shield: float = data["max_shield"] - if max_shield <= 0: + var max_shield: float = float(Stats.get_stat(entity, "max_shield", 0.0)) + if max_shield <= 0.0: + continue + var shield: float = float(Stats.get_stat(entity, "shield", 0.0)) + var timer: float = float(Stats.get_stat(entity, "shield_regen_timer", 0.0)) + if timer > 0.0: + Stats.set_stat(entity, "shield_regen_timer", max(0.0, timer - dt)) continue - var shield: float = data["shield"] if shield < max_shield: - data["shield_regen_timer"] += delta - if data["shield_regen_timer"] >= data["shield_regen_delay"]: - var regen_rate: float = max_shield / data["shield_regen_time"] - shield += regen_rate * delta - if shield >= max_shield: - shield = max_shield - EventBus.shield_regenerated.emit(entity) - data["shield"] = shield - EventBus.shield_changed.emit(entity, shield, max_shield) - -func absorb(target: Node, amount: float) -> float: - var player: Node = get_tree().get_first_node_in_group("player") - if target == player: - if PlayerData.shield <= 0: - return amount - PlayerData.shield_regen_timer = 0.0 - var absorbed: float = min(amount, PlayerData.shield) - var shield: float = PlayerData.shield - absorbed - PlayerData.set_shield(shield) - if shield <= 0: - EventBus.shield_broken.emit(target) - return amount - absorbed - var data_source: Node = _get_data_source(target) - if not data_source: - return amount - var shield: float = data_source.get_stat(target, "shield") - if shield == null or shield <= 0: - return amount - data_source.set_stat(target, "shield_regen_timer", 0.0) - var absorbed: float = min(amount, shield) - shield -= absorbed - data_source.set_shield(target, shield) - if shield <= 0: - EventBus.shield_broken.emit(target) - return amount - absorbed - -func _get_data_source(entity: Node) -> Node: - if entity.is_in_group("boss"): - return BossData - elif entity.is_in_group("enemies"): - return EnemyData - return null + var regen: float = float(Stats.get_stat(entity, "shield_regen", 0.0)) + if regen <= 0.0: + regen = max_shield * 0.10 + var new_shield: float = min(max_shield, shield + regen * dt) + Stats.set_stat(entity, "shield", new_shield) + EventBus.shield_changed.emit(entity, new_shield, max_shield) diff --git a/systems/shield_system.gd.uid b/systems/shield_system.gd.uid index a8ea48c..68217b7 100644 --- a/systems/shield_system.gd.uid +++ b/systems/shield_system.gd.uid @@ -1 +1 @@ -uid://rsnpuf77o0sn +uid://dhgary4qpucki diff --git a/systems/spawn_system.gd b/systems/spawn_system.gd index 0193a4b..aa6b52e 100644 --- a/systems/spawn_system.gd +++ b/systems/spawn_system.gd @@ -1,51 +1,93 @@ extends Node -const ENEMY_SCENE: PackedScene = preload("res://scenes/enemy/enemy.tscn") +const ENEMY_SCENE: PackedScene = preload("res://scenes/entities/enemy/enemy.tscn") +const PORTAL_SCENE: PackedScene = preload("res://scenes/entities/portal/portal.tscn") +const GATE_SCENE: PackedScene = preload("res://scenes/entities/gate/gate.tscn") -func _ready() -> void: - EventBus.health_changed.connect(_on_health_changed) - EventBus.entity_died.connect(_on_entity_died) +func _enemy_root() -> Node3D: + var n: Node = get_node_or_null("/root/World/EntityRoot/Enemies") + if n == null: + n = get_node_or_null("/root/Dungeon/EntityRoot/Enemies") + return n -func _on_health_changed(entity: Node, current: float, max_val: float) -> void: - if not entity.is_in_group("portals"): - return - if current <= 0: - return - var data: Dictionary = PortalData.entities.get(entity, {}) - if data.is_empty(): - return - var ratio: float = current / max_val - var thresholds: Array = data["thresholds"] - var triggered: Array = data["triggered"] - var spawn_count: int = data["spawn_count"] - for i in range(thresholds.size()): - if not triggered[i] and ratio <= thresholds[i]: - triggered[i] = true - _spawn_enemies(entity, spawn_count) +func _portal_root() -> Node3D: + var n: Node = get_node_or_null("/root/World/EntityRoot/Portals") + if n == null: + n = get_node_or_null("/root/Dungeon/EntityRoot/Portals") + return n -func _spawn_enemies(portal: Node, count: int) -> void: - var spawned: Array = [] - var is_red: bool = portal.is_in_group("red_portal") - var portal_bonus: float = 10.0 if is_red else 1.0 - var total_scale: float = PlayerData.level_scale * portal_bonus - for j in range(count): - var entity: Node = ENEMY_SCENE.instantiate() - entity.spawn_scale = total_scale - var offset := Vector3(randf_range(-2, 2), 0, randf_range(-2, 2)) - portal.get_parent().add_child(entity) - if is_red: - entity.add_to_group("red_enemies") - entity.global_position = portal.global_position + offset - EnemyData.set_stat(entity, "portal", portal) - spawned.append(entity) - var player: Node = get_tree().get_first_node_in_group("player") - if player: - var dist: float = portal.global_position.distance_to(player.global_position) - if dist <= 10.0: - for entity in spawned: - EventBus.enemy_detected.emit(entity, player) - EventBus.portal_spawn.emit(portal, spawned) +func _gate_root() -> Node3D: + var n: Node = get_node_or_null("/root/World/EntityRoot/Gates") + if n == null: + n = get_node_or_null("/root/Dungeon/EntityRoot/Gates") + return n -func _on_entity_died(entity: Node) -> void: - if entity.is_in_group("portals"): - PortalData.deregister(entity) +func spawn_enemy_at(pos: Vector3, strong: bool = false, mult: float = 1.0) -> Node: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return null + var root := _enemy_root() + if root == null: + return null + var e: CharacterBody3D = ENEMY_SCENE.instantiate() + var stats := EnemyStats.new() + var s: float = mult * (3.0 if strong else 1.0) + stats.max_health = 30.0 * s + stats.attack_damage = 5.0 * s + stats.attack_range = 2.0 + stats.attack_cooldown = 1.5 + stats.speed = 3.0 + stats.aggro_radius = 12.0 + stats.xp_value = 5.0 * s + e.stats_resource = stats + e.name = "Enemy_%d" % (Time.get_ticks_msec() + randi() % 1000) + root.add_child(e, true) + e.global_position = pos + return e + +func spawn_boss_at(pos: Vector3, mult: float = 1.0) -> Node: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return null + var root := _enemy_root() + if root == null: + return null + var b: CharacterBody3D = ENEMY_SCENE.instantiate() + b.is_boss = true + var stats := BossStats.new() + stats.max_health = 250.0 * mult + stats.attack_damage = 12.0 * mult + stats.attack_range = 3.0 + stats.attack_cooldown = 1.6 + stats.speed = 3.5 + stats.xp_value = 100.0 * mult + b.stats_resource = stats + b.name = "Boss_%d" % Time.get_ticks_msec() + root.add_child(b, true) + b.global_position = pos + return b + +func spawn_portal_at(pos: Vector3, is_red: bool) -> Node: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return null + var root := _portal_root() + if root == null: + return null + var p: StaticBody3D = PORTAL_SCENE.instantiate() + p.is_red = is_red + p.name = "Portal_%d" % Time.get_ticks_msec() + root.add_child(p, true) + p.global_position = pos + EventBus.portal_spawned.emit(p) + return p + +func spawn_gate_at(pos: Vector3, is_red: bool) -> Node: + if not multiplayer.is_server() and multiplayer.multiplayer_peer != null: + return null + var root := _gate_root() + if root == null: + return null + var g: StaticBody3D = GATE_SCENE.instantiate() + g.is_red = is_red + g.name = "Gate_%s_%d" % ["red" if is_red else "n", Time.get_ticks_msec() + randi() % 1000] + root.add_child(g, true) + g.global_position = pos + return g diff --git a/systems/spawn_system.gd.uid b/systems/spawn_system.gd.uid index 45c450e..7ba79bc 100644 --- a/systems/spawn_system.gd.uid +++ b/systems/spawn_system.gd.uid @@ -1 +1 @@ -uid://c84voxmnaifyt +uid://k0yi8mwpdhgy diff --git a/systems/targeting_system.gd b/systems/targeting_system.gd deleted file mode 100644 index 177317c..0000000 --- a/systems/targeting_system.gd +++ /dev/null @@ -1,69 +0,0 @@ -extends Node - -func _ready() -> void: - EventBus.target_requested.connect(_on_target_requested) - EventBus.entity_died.connect(_on_entity_died) - EventBus.enemy_engaged.connect(_on_enemy_engaged) - EventBus.damage_dealt.connect(_on_damage_dealt) - -func _process(delta: float) -> void: - if PlayerData.in_combat: - PlayerData.combat_timer -= delta - if PlayerData.combat_timer <= 0: - PlayerData.in_combat = false - -func _on_target_requested(_player: Node, target: Node3D) -> void: - PlayerData.set_target(target) - if target and (target.is_in_group("enemies") or target.is_in_group("portals")): - PlayerData.in_combat = true - PlayerData.combat_timer = PlayerData.combat_timeout - -func _on_entity_died(entity: Node) -> void: - if entity == PlayerData.target: - PlayerData.set_target(null) - if PlayerData.in_combat: - _auto_target(entity) - -func _on_enemy_engaged(_enemy: Node, target: Node) -> void: - var player: Node = get_tree().get_first_node_in_group("player") - if target == player: - PlayerData.combat_timer = PlayerData.combat_timeout - PlayerData.in_combat = true - if PlayerData.target == null: - _auto_target() - -func _on_damage_dealt(attacker: Node, target: Node, _amount: float) -> void: - var player: Node = get_tree().get_first_node_in_group("player") - if target == player: - PlayerData.combat_timer = PlayerData.combat_timeout - if not PlayerData.in_combat: - PlayerData.in_combat = true - if PlayerData.target == null: - _auto_target() - elif attacker == player: - PlayerData.in_combat = true - PlayerData.combat_timer = PlayerData.combat_timeout - -func _auto_target(exclude: Node = null) -> void: - var player: Node = get_tree().get_first_node_in_group("player") - if not player: - return - var nearest: Node3D = null - var nearest_dist: float = INF - for enemy in get_tree().get_nodes_in_group("enemies"): - if is_instance_valid(enemy) and enemy != exclude: - var dist: float = player.global_position.distance_to(enemy.global_position) - if dist < nearest_dist: - nearest_dist = dist - nearest = enemy - if nearest: - PlayerData.set_target(nearest) - return - for p in get_tree().get_nodes_in_group("portals"): - if is_instance_valid(p) and p != exclude: - var dist: float = player.global_position.distance_to(p.global_position) - if dist < nearest_dist: - nearest_dist = dist - nearest = p - if nearest: - PlayerData.set_target(nearest) diff --git a/systems/targeting_system.gd.uid b/systems/targeting_system.gd.uid deleted file mode 100644 index 1e68347..0000000 --- a/systems/targeting_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bvjmdmof4vcyr diff --git a/systems/wave_system.gd b/systems/wave_system.gd index 1372f4c..b4a392f 100644 --- a/systems/wave_system.gd +++ b/systems/wave_system.gd @@ -1,47 +1,77 @@ extends Node -const WAVE_DURATION := 60.0 +const NORMAL_GATES: int = 3 +const TIMER_SECONDS: float = 600.0 -var tick_accumulator := 0.0 +@onready var spawn_system: Node = get_node("../SpawnSystem") + +var timer_remaining: float = TIMER_SECONDS +var red_gate: Node = null +var red_portal_done: bool = false +var active: bool = false func _ready() -> void: - EventBus.portal_defeated.connect(_on_portal_defeated) + EventBus.gate_destroyed.connect(_on_gate_destroyed) + EventBus.boss_defeated.connect(_on_boss_defeated) EventBus.invasion_ended.connect(_on_invasion_ended) - call_deferred("_start_run") -func _start_run() -> void: - if not GameState.run_initialized: - GameState.current_wave = 1 - GameState.wave_timer_remaining = WAVE_DURATION - GameState.run_initialized = true - EventBus.run_started.emit(GameState.current_wave) - EventBus.wave_started.emit(GameState.current_wave) - -func _process(delta: float) -> void: - if GameState.wave_timer_remaining <= 0: +func start_wave(n: int) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): return - GameState.wave_timer_remaining -= delta - tick_accumulator += delta - if tick_accumulator >= 1.0: - tick_accumulator -= 1.0 - EventBus.wave_timer_tick.emit(max(0.0, GameState.wave_timer_remaining)) - if GameState.wave_timer_remaining <= 0: - GameState.wave_timer_remaining = 0.0 - EventBus.wave_timer_tick.emit(0.0) + GameState.current_wave = n + red_portal_done = false + red_gate = null + timer_remaining = TIMER_SECONDS + active = true + EventBus.wave_started.emit(n) + _spawn_initial_gates() -func _on_portal_defeated(portal: Node) -> void: - if not portal.is_in_group("red_portal"): +func _spawn_initial_gates() -> void: + for i in range(NORMAL_GATES): + var pos := _random_outer_position(20.0, 40.0) + spawn_system.spawn_gate_at(pos, false) + red_gate = spawn_system.spawn_gate_at(_random_outer_position(30.0, 45.0), true) + +func _random_outer_position(min_d: float, max_d: float) -> Vector3: + var angle: float = randf() * TAU + var d: float = randf_range(min_d, max_d) + return Vector3(cos(angle) * d, 0.5, sin(angle) * d) + +func _physics_process(delta: float) -> void: + if not active: return - _advance_wave() + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + return + timer_remaining = max(0.0, timer_remaining - delta) + EventBus.wave_timer_tick.emit(timer_remaining) + if timer_remaining <= 0.0 and not red_portal_done: + active = false + EventBus.invasion_started.emit() + var inv: Node = get_node_or_null("../InvasionSystem") + if inv and inv.has_method("start"): + inv.start(GameState.current_wave) + +func _on_gate_destroyed(gate: Node) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + return + if not gate.is_in_group("red_gate"): + var pos := _random_outer_position(20.0, 40.0) + spawn_system.spawn_gate_at(pos, false) + +func _on_boss_defeated(_boss: Node) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): + return + if GameState.dungeon_red and not red_portal_done: + red_portal_done = true + active = false + EventBus.wave_ended.emit(GameState.current_wave, true) + var t := get_tree().create_timer(2.0) + t.timeout.connect(func(): start_wave(GameState.current_wave + 1)) func _on_invasion_ended(success: bool) -> void: - if not success: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): return - _advance_wave() - -func _advance_wave() -> void: - EventBus.wave_ended.emit(GameState.current_wave, true) - GameState.current_wave += 1 - GameState.wave_timer_remaining = WAVE_DURATION - tick_accumulator = 0.0 - EventBus.wave_started.emit(GameState.current_wave) + if success: + EventBus.wave_ended.emit(GameState.current_wave, true) + var t := get_tree().create_timer(2.0) + t.timeout.connect(func(): start_wave(GameState.current_wave + 1)) diff --git a/systems/wave_system.gd.uid b/systems/wave_system.gd.uid index 51a390c..93671fa 100644 --- a/systems/wave_system.gd.uid +++ b/systems/wave_system.gd.uid @@ -1 +1 @@ -uid://chfcocmkb0wnp +uid://ccmpr0d2tgs45 diff --git a/systems/xp_system.gd b/systems/xp_system.gd index 53f0e8e..e946d14 100644 --- a/systems/xp_system.gd +++ b/systems/xp_system.gd @@ -1,19 +1,48 @@ extends Node -const XP_PER_ENEMY: int = 3 -const XP_PER_BOSS: int = 30 - func _ready() -> void: - EventBus.entity_died.connect(_on_entity_died) + EventBus.boss_defeated.connect(_on_boss) -func _on_entity_died(entity: Node) -> void: - if not entity or not is_instance_valid(entity): +func _on_boss(boss: Node) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): return - if not entity.is_in_group("enemies"): + var amount: float = float(Stats.get_stat(boss, "xp_value", 100.0)) + if GameState.dungeon_red: + amount *= 5.0 + for player in get_tree().get_nodes_in_group("player"): + award(player, amount) + +func award_for_enemy(enemy: Node) -> void: + if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): return - var is_boss: bool = entity.is_in_group("boss") - var data_source: Node = BossData if is_boss else EnemyData - var scale_val: Variant = data_source.get_stat(entity, "scale") - var scale: float = scale_val if scale_val != null else 1.0 - var base_xp: int = XP_PER_BOSS if is_boss else XP_PER_ENEMY - PlayerData.add_xp(int(base_xp * scale)) + var amount: float = float(Stats.get_stat(enemy, "xp_value", 5.0)) + for player in get_tree().get_nodes_in_group("player"): + var d: float = (player as Node3D).global_position.distance_to((enemy as Node3D).global_position) + if d <= 50.0: + award(player, amount) + +func award(player: Node, amount: float) -> void: + if not Stats.has(player): + return + var xp: float = float(Stats.get_stat(player, "xp", 0.0)) + amount + var to_next: float = float(Stats.get_stat(player, "xp_to_next", 50.0)) + var level: int = int(Stats.get_stat(player, "level", 1)) + while xp >= to_next: + xp -= to_next + level += 1 + to_next = round(to_next * 1.6) + _level_up(player, level) + Stats.set_stat(player, "xp", xp) + Stats.set_stat(player, "xp_to_next", to_next) + Stats.set_stat(player, "level", level) + EventBus.xp_gained.emit(player, amount) + +func _level_up(player: Node, new_level: int) -> void: + for stat in [&"max_health", &"max_shield", &"speed"]: + var cur: float = float(Stats.get_stat(player, stat, 0.0)) + Stats.set_stat(player, stat, cur * 1.4) + Stats.set_stat(player, "health", Stats.get_stat(player, "max_health")) + Stats.set_stat(player, "shield", Stats.get_stat(player, "max_shield")) + EventBus.level_up.emit(player, new_level) + EventBus.health_changed.emit(player, Stats.get_stat(player, "health"), Stats.get_stat(player, "max_health")) + EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), Stats.get_stat(player, "max_shield")) diff --git a/systems/xp_system.gd.uid b/systems/xp_system.gd.uid index 4e97898..fa94695 100644 --- a/systems/xp_system.gd.uid +++ b/systems/xp_system.gd.uid @@ -1 +1 @@ -uid://clivdryqcvfmh +uid://crvitg8qvkip5