feat: beam of light above boss

This commit is contained in:
2026-05-29 17:49:30 +02:00
parent 085678955d
commit f1e4c03ea5
2 changed files with 82 additions and 45 deletions
+40 -35
View File
@@ -23,7 +23,7 @@ HP_REGEN_INTERVAL = float(os.getenv("HP_REGEN_INTERVAL", "2" if DEBUG_MODE else
HP_REGEN_AMOUNT = 1 # HP healed per interval HP_REGEN_AMOUNT = 1 # HP healed per interval
MONSTER_SPAWN_RATE = 0.05 # Probability per game tick MONSTER_SPAWN_RATE = 0.05 # Probability per game tick
MAX_MONSTERS = 50 MAX_MONSTERS = 50
BOSS_HEALTH_BASE = 1000 BOSS_FIXED_HEALTH = 6000
MIN_LEVEL_FOR_BOSS = 10 MIN_LEVEL_FOR_BOSS = 10
BOSS_STRUCTURE_BONUS_MULTIPLIER = 0.1 BOSS_STRUCTURE_BONUS_MULTIPLIER = 0.1
RESOURCE_NODE_COUNT = 540 RESOURCE_NODE_COUNT = 540
@@ -187,6 +187,7 @@ class GameWorld:
self.resources: Dict[str, Resource] = {} self.resources: Dict[str, Resource] = {}
self.chests: Dict[str, Chest] = {} self.chests: Dict[str, Chest] = {}
self.boss: Monster = None self.boss: Monster = None
self.boss_spawned: bool = False
self.game_start_time: float = 0 self.game_start_time: float = 0
self.is_game_active: bool = False self.is_game_active: bool = False
self.connected_sessions: Dict[str, str] = {} # session_id -> player_id self.connected_sessions: Dict[str, str] = {} # session_id -> player_id
@@ -196,6 +197,8 @@ class GameWorld:
# Auto-start a game session immediately so the timer runs from launch. # Auto-start a game session immediately so the timer runs from launch.
self.game_start_time = datetime.now().timestamp() self.game_start_time = datetime.now().timestamp()
self.is_game_active = True self.is_game_active = True
self._spawn_boss()
self.boss_spawned = True
def _init_resources(self): def _init_resources(self):
"""Initialize static resources on the map""" """Initialize static resources on the map"""
@@ -221,18 +224,18 @@ class GameWorld:
x = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2) x = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)
y = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2) y = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)
weapon_templates = [ weapon_templates = [
{"name": "Long Spear", "attack_bonus": 2, "range_bonus": 2}, {"name": "Oversized Shrimp", "attack_bonus": 2, "range_bonus": 2},
{"name": "Hunting Bow", "attack_bonus": 1, "range_bonus": 3}, {"name": "Hunting Bow", "attack_bonus": 1, "range_bonus": 5},
{"name": "Battle Axe", "attack_bonus": 3, "range_bonus": 1}, {"name": "Big Ass Axe", "attack_bonus": 3, "range_bonus": 1},
{ {
"name": "Banner of Vigor", "name": "Yaoi Paddle",
"attack_bonus": 1, "attack_bonus": 1,
"range_bonus": 1, "range_bonus": 1,
"aura_radius": 6, "aura_radius": 6,
"aura_bonuses": {"action_regen": 1, "hp_regen": 1}, "aura_bonuses": {"action_regen": 1, "hp_regen": 1},
}, },
{ {
"name": "Warden Standard", "name": "Warden Guitar",
"attack_bonus": 0, "attack_bonus": 0,
"range_bonus": 1, "range_bonus": 1,
"aura_radius": 6, "aura_radius": 6,
@@ -243,7 +246,7 @@ class GameWorld:
"attack_bonus": 0, "attack_bonus": 0,
"range_bonus": 0, "range_bonus": 0,
"aura_radius": 5, "aura_radius": 5,
"aura_bonuses": {"free_move_chance": 0.5}, "aura_bonuses": {"free_move_chance": 0.7},
}, },
] ]
self.chests[chest_id] = Chest( self.chests[chest_id] = Chest(
@@ -257,6 +260,7 @@ class GameWorld:
self.game_start_time = datetime.now().timestamp() self.game_start_time = datetime.now().timestamp()
self.is_game_active = True self.is_game_active = True
self.boss = None self.boss = None
self.boss_spawned = False
self.monsters.clear() self.monsters.clear()
self.chests.clear() self.chests.clear()
self.resources.clear() self.resources.clear()
@@ -282,6 +286,9 @@ class GameWorld:
0, 0,
) )
self._spawn_boss()
self.boss_spawned = True
def get_elapsed_time(self) -> float: def get_elapsed_time(self) -> float:
if not self.is_game_active: if not self.is_game_active:
return 0 return 0
@@ -300,11 +307,10 @@ class GameWorld:
"""Called periodically to update game state""" """Called periodically to update game state"""
current_time = datetime.now().timestamp() current_time = datetime.now().timestamp()
# Spawn boss if conditions are met # Spawn boss once at the beginning of each session.
if self.boss is None and len(self.players) > 0: if self.boss is None and not self.boss_spawned:
active_players = [p for p in self.players.values() if p.active] self._spawn_boss()
if active_players and any(p.level >= MIN_LEVEL_FOR_BOSS for p in active_players): self.boss_spawned = True
self._spawn_boss()
# Spawn monsters # Spawn monsters
if random.random() < MONSTER_SPAWN_RATE and len(self.monsters) < MAX_MONSTERS: if random.random() < MONSTER_SPAWN_RATE and len(self.monsters) < MAX_MONSTERS:
@@ -357,31 +363,30 @@ class GameWorld:
) )
def _spawn_boss(self): def _spawn_boss(self):
"""Spawn the boss monster""" """Spawn the boss monster."""
boss_id = f"boss_{self.generation}" boss_id = f"boss_{self.generation}"
active_players = [p for p in self.players.values() if p.active] active_players = [p for p in self.players.values() if p.active]
avg_level = sum(p.level for p in active_players) / len(active_players) avg_level = (sum(p.level for p in active_players) / len(active_players)) if active_players else 1
health = int(BOSS_HEALTH_BASE + avg_level * 500)
# Spawn boss at a distance from players # Spawn boss at a distance from players if possible, otherwise at map center.
if active_players: if active_players:
player_pos = active_players[0].position player_pos = active_players[0].position
angle = random.random() * 2 * 3.14159 angle = random.random() * 2 * 3.14159
x = player_pos.x + BOSS_SPAWN_DISTANCE * (angle ** 0.5) * (1 if random.random() > 0.5 else -1) x = player_pos.x + BOSS_SPAWN_DISTANCE * (angle ** 0.5) * (1 if random.random() > 0.5 else -1)
y = player_pos.y + BOSS_SPAWN_DISTANCE * (1 - angle ** 0.5) * (1 if random.random() > 0.5 else -1) y = player_pos.y + BOSS_SPAWN_DISTANCE * (1 - angle ** 0.5) * (1 if random.random() > 0.5 else -1)
else: else:
x = y = 0 x = y = 0
self.boss = Monster( self.boss = Monster(
id=boss_id, id=boss_id,
position=Position(x, y, 0), position=Position(x, y, 0),
health=health, health=BOSS_FIXED_HEALTH,
max_health=health, max_health=BOSS_FIXED_HEALTH,
level=int(avg_level) + 5, level=max(10, int(avg_level) + 5),
attack=15 + int(avg_level), attack=15 + int(avg_level),
defense=4 + int(avg_level // 2), defense=4 + int(avg_level // 2),
is_boss=True, is_boss=True,
) )
def _player_effective_attack(player: Player) -> int: def _player_effective_attack(player: Player) -> int:
if 0 <= player.equipped_weapon_slot < len(player.weapon_slots): if 0 <= player.equipped_weapon_slot < len(player.weapon_slots):
+42 -10
View File
@@ -263,16 +263,16 @@
.boss-bar-container { .boss-bar-container {
position: absolute; position: absolute;
top: 50%; top: 10px;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translateX(-50%);
background: rgba(0, 0, 0, 0.9); background: rgba(0, 0, 0, 0.9);
border: 2px solid #ff4444; border: 2px solid #ff4444;
border-radius: 5px; border-radius: 5px;
padding: 20px; padding: 10px 14px;
text-align: center; text-align: center;
display: none; display: none;
z-index: 10; z-index: 30;
} }
.boss-bar-container.active { .boss-bar-container.active {
@@ -467,11 +467,11 @@
</div> </div>
<div class="boss-bar-container" id="bossBar"> <div class="boss-bar-container" id="bossBar">
<div style="color: #ff4444; font-weight: bold; margin-bottom: 10px;">⚔️ BOSS APPEARED ⚔️</div> <div style="color: #ff4444; font-weight: bold; margin-bottom: 6px;">Boss</div>
<div class="stat-bar"> <div class="stat-bar">
<div class="stat-bar-fill" id="bossHealthBar" style="width: 100%; background: #ff4444;"></div> <div class="stat-bar-fill" id="bossHealthBar" style="width: 100%; background: #ff4444;"></div>
</div> </div>
<div style="margin-top: 10px;"> <div style="margin-top: 6px;">
<span id="bossHealth">Loading...</span> <span id="bossHealth">Loading...</span>
</div> </div>
</div> </div>
@@ -1066,13 +1066,45 @@
} }
function createBossMesh(monster) { function createBossMesh(monster) {
const group = new THREE.Group();
const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5); const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
const material = new THREE.MeshPhongMaterial({ color: 0xff0000 }); const material = new THREE.MeshPhongMaterial({ color: 0xff0000 });
const cube = new THREE.Mesh(geometry, material); const cube = new THREE.Mesh(geometry, material);
cube.position.set(monster.position.x, 0.75, monster.position.y); cube.position.set(0, 0.75, 0);
cube.castShadow = true; cube.castShadow = true;
scene.add(cube); group.add(cube);
ossBossObject = cube;
// Vertical light ray so the boss is visible from far away.
const beamGeo = new THREE.CylinderGeometry(0.18, 1.4, 18, 20, 1, true);
const beamMat = new THREE.MeshBasicMaterial({
color: 0xfff2a8,
transparent: true,
opacity: 0.32,
side: THREE.DoubleSide,
depthWrite: false,
});
const beam = new THREE.Mesh(beamGeo, beamMat);
beam.position.set(0, 9, 0);
group.add(beam);
const halo = new THREE.Mesh(
new THREE.RingGeometry(1.1, 1.7, 28),
new THREE.MeshBasicMaterial({
color: 0xffe680,
transparent: true,
opacity: 0.38,
side: THREE.DoubleSide,
depthWrite: false,
})
);
halo.rotation.x = -Math.PI / 2;
halo.position.set(0, 0.08, 0);
group.add(halo);
group.position.set(monster.position.x, 0, monster.position.y);
scene.add(group);
ossBossObject = group;
} }
function createStructureMesh(structure) { function createStructureMesh(structure) {
@@ -1215,7 +1247,7 @@
createBossMesh(gameState.boss); createBossMesh(gameState.boss);
document.getElementById('bossBar').classList.add('active'); document.getElementById('bossBar').classList.add('active');
} else { } else {
ossBossObject.position.set(gameState.boss.position.x, 0.75, gameState.boss.position.y); ossBossObject.position.set(gameState.boss.position.x, 0, gameState.boss.position.y);
ossBossObject.rotation.y += 0.01; ossBossObject.rotation.y += 0.01;
} }
document.getElementById('bossHealthBar').style.width = `${gameState.boss.boss_progress}%`; document.getElementById('bossHealthBar').style.width = `${gameState.boss.boss_progress}%`;