Files
trenchlock/scripts/encounter_manager.gd
2026-01-13 08:31:42 +01:00

466 lines
14 KiB
GDScript

## Core game state machine and encounter loop for Tetra Tactics 3D.
##
## Manages the encounter flow: DRAFT → PLACEMENT → TELEGRAPH → COMBAT → RESOLUTION → ESCALATION.
## Orchestrates turn transitions, wave progression, and win/loss conditions.
class_name EncounterManager
extends Node
## Game state enumeration
enum State {
DRAFT, ## Draft random tetrominoes for placement
PLACEMENT, ## Player places/rotates shapes in 3D board
TELEGRAPH, ## Show enemy spawn points and fall paths (pre-combat warning)
COMBAT, ## Enemies fall from above; towers track and fire projectiles
RESOLUTION, ## Damage calculation, collect rewards, wave outcome
ESCALATION ## Difficulty increase, preview next wave
}
## Signal emitted when state changes
signal state_changed(new_state: State, old_state: State)
## Signal emitted when wave starts/progresses
signal wave_started(wave_number: int)
signal wave_completed(wave_number: int)
signal wave_failed
## Signal emitted for player feedback
signal gold_changed(amount: int)
signal health_changed(amount: int)
# Game flow configuration
@export var waves: Array[Resource] = [] ## Array of WaveConfig resources
@export var starting_health: int = 100
@export var starting_gold: int = 500
# State management
var _current_state: State = State.DRAFT
var _previous_state: State
var _state_timer: float = 0.0
# Game progress
var _current_wave: int = 0
var _total_waves: int = 0
var _current_health: int
var _current_gold: int
# References to systems and managers
var _event_bus: Node
var _board_manager: Node ## BoardManager
var _combat_system: Node ## CombatSystem
var _synergy_system: Node ## SynergySystem
var _enemy_spawner: Node ## EnemySpawner
var _ui_manager: Node
# Draft phase data
var _available_tetrominoes: Array[Resource] = [] ## TetrominoDefinition resources
var _drafted_tetromino: Resource ## Currently selected tetromino
# Combat phase state
var _combat_in_progress: bool = false
var _enemies_remaining: int = 0
func _ready() -> void:
_total_waves = waves.size()
_current_health = starting_health
_current_gold = starting_gold
# Get references to autoload singletons and scene nodes
_event_bus = get_tree().root.get_child(0).get_node_or_null("EventBus")
if not _event_bus:
_event_bus = get_node("/root/EventBus") if "/root/EventBus" in get_tree().root else null
# Find board manager in scene
_board_manager = get_parent().get_node_or_null("Board")
_combat_system = get_parent().get_node_or_null("CombatSystem")
_synergy_system = get_parent().get_node_or_null("SynergySystem")
_enemy_spawner = get_parent().get_node_or_null("EnemySpawner")
_ui_manager = get_parent().get_node_or_null("HUD")
# Connect to signal events if event bus exists
if _event_bus:
if _event_bus.has_signal("enemy_died"):
_event_bus.enemy_died.connect(_on_enemy_died)
if _event_bus.has_signal("tower_damaged"):
_event_bus.tower_damaged.connect(_on_tower_damaged)
# Start first wave's draft phase
_current_wave = 0
_transition_to_state(State.DRAFT)
func _process(delta: float) -> void:
_state_timer += delta
match _current_state:
State.DRAFT:
_update_draft(delta)
State.PLACEMENT:
_update_placement(delta)
State.TELEGRAPH:
_update_telegraph(delta)
State.COMBAT:
_update_combat(delta)
State.RESOLUTION:
_update_resolution(delta)
State.ESCALATION:
_update_escalation(delta)
## Transition to a new state and emit signal
func _transition_to_state(new_state: State) -> void:
if new_state == _current_state:
return
_previous_state = _current_state
_current_state = new_state
_state_timer = 0.0
state_changed.emit(new_state, _previous_state)
# Broadcast to event bus if available
if _event_bus and _event_bus.has_signal("state_changed"):
_event_bus.state_changed.emit(new_state, _previous_state)
# Execute state entry logic
match new_state:
State.DRAFT:
_enter_draft()
State.PLACEMENT:
_enter_placement()
State.TELEGRAPH:
_enter_telegraph()
State.COMBAT:
_enter_combat()
State.RESOLUTION:
_enter_resolution()
State.ESCALATION:
_enter_escalation()
# ============================================================================
# DRAFT PHASE: Present 3 random tetrominoes for player selection
# ============================================================================
func _enter_draft() -> void:
print("Entering DRAFT phase for wave %d" % (_current_wave + 1))
# Load tetromino definitions from resources
_available_tetrominoes.clear()
_load_tetromino_definitions()
# Shuffle and select 3 random tetrominoes for player choice
_available_tetrominoes.shuffle()
if _available_tetrominoes.size() > 3:
_available_tetrominoes = _available_tetrominoes.slice(0, 3)
# Signal UI to show draft carousel
if _ui_manager:
_ui_manager.show_draft_carousel(_available_tetrominoes)
if _event_bus and _event_bus.has_signal("draft_started"):
_event_bus.draft_started.emit(_available_tetrominoes)
func _update_draft(delta: float) -> void:
# Wait for player selection via input or UI callback
# Once tetromino is selected, move to PLACEMENT phase
pass
## Called when player selects a tetromino from the draft carousel
func select_tetromino(tetromino: Resource) -> void:
if _current_state != State.DRAFT:
return
_drafted_tetromino = tetromino
print("Selected tetromino: %s" % tetromino.shape_type)
_transition_to_state(State.PLACEMENT)
## Load tetromino definitions from resource files
func _load_tetromino_definitions() -> void:
# Load from resource directory (placeholder path)
var tetromino_dir = "res://resources/data/tetrominoes/"
var dir = DirAccess.open(tetromino_dir)
if dir:
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
if file_name.ends_with(".tres"):
var resource_path = tetromino_dir + file_name
var tetromino_def = load(resource_path)
if tetromino_def is Resource:
_available_tetrominoes.append(tetromino_def)
file_name = dir.get_next()
else:
# Fallback: create sample tetrominoes if resources unavailable
_create_sample_tetrominoes()
## Create sample tetrominoes if resource files not found
func _create_sample_tetrominoes() -> void:
var shapes = ["I", "O", "T", "S", "Z", "L", "J"]
for shape in shapes:
var tetromino_def = TetrominoDefinition.new()
tetromino_def.shape_type = StringName(shape)
tetromino_def.base_damage = 10 + randi() % 10
tetromino_def.fire_rate = 1.0 + randf_range(0.0, 0.5)
tetromino_def.synergy_tags = ["fire", "ice", "light"].slice(0, randi() % 2 + 1)
_available_tetrominoes.append(tetromino_def)
# ============================================================================
# PLACEMENT PHASE: Player places/rotates tetrominoes in 3D board
# ============================================================================
func _enter_placement() -> void:
print("Entering PLACEMENT phase")
if _board_manager:
_board_manager.clear_preview()
_board_manager.set_placement_mode(true, _drafted_tetromino)
if _ui_manager:
_ui_manager.show_placement_instructions()
if _event_bus and _event_bus.has_signal("placement_started"):
_event_bus.placement_started.emit(_drafted_tetromino)
func _update_placement(delta: float) -> void:
# Wait for player confirmation (Space key)
if Input.is_action_just_pressed("ui_select"):
_on_placement_confirmed()
## Called when player confirms tetromino placement
func _on_placement_confirmed() -> void:
if _current_state != State.PLACEMENT:
return
if _board_manager and _board_manager.place_tetromino(_drafted_tetromino):
print("Tetromino placed successfully")
_transition_to_state(State.TELEGRAPH)
else:
print("Invalid placement - cannot place tetromino")
# ============================================================================
# TELEGRAPH PHASE: Show enemy spawn points and fall paths
# ============================================================================
func _enter_telegraph() -> void:
print("Entering TELEGRAPH phase")
# Show visual indicators of where enemies will spawn and fall
if _enemy_spawner:
_enemy_spawner.telegraph_wave(waves[_current_wave])
if _ui_manager:
_ui_manager.show_telegraph_warning()
if _event_bus and _event_bus.has_signal("telegraph_started"):
_event_bus.telegraph_started.emit(waves[_current_wave])
func _update_telegraph(delta: float) -> void:
# Telegraph phase lasts 2-3 seconds, then transitions to combat
if _state_timer >= 2.5:
_transition_to_state(State.COMBAT)
# ============================================================================
# COMBAT PHASE: Enemies fall; towers track and fire
# ============================================================================
func _enter_combat() -> void:
print("Entering COMBAT phase for wave %d" % (_current_wave + 1))
_combat_in_progress = true
# Spawn enemies for current wave
var wave_config: Resource = waves[_current_wave]
if _enemy_spawner:
_enemies_remaining = _enemy_spawner.spawn_wave(wave_config)
# Enable tower combat systems
if _combat_system:
_combat_system.enable_combat(true)
if _synergy_system:
_synergy_system.recalculate_synergies()
if _board_manager:
_board_manager.set_placement_mode(false) # Disable placement, allow repositioning
wave_started.emit(_current_wave + 1)
if _event_bus and _event_bus.has_signal("combat_started"):
_event_bus.combat_started.emit(_current_wave + 1)
func _update_combat(delta: float) -> void:
# Combat continues until all enemies are defeated or health reaches 0
if _enemies_remaining <= 0:
_transition_to_state(State.RESOLUTION)
elif _current_health <= 0:
wave_failed.emit()
_transition_to_state(State.RESOLUTION)
## Called when an enemy is defeated
func _on_enemy_died() -> void:
_enemies_remaining = max(0, _enemies_remaining - 1)
if _enemies_remaining == 0 and _current_state == State.COMBAT:
_transition_to_state(State.RESOLUTION)
## Called when a tower takes damage from enemy impact
func _on_tower_damaged(amount: int) -> void:
modify_health(-amount)
# ============================================================================
# RESOLUTION PHASE: Damage calculation, collect rewards
# ============================================================================
func _enter_resolution() -> void:
print("Entering RESOLUTION phase")
_combat_in_progress = false
# Disable combat systems
if _combat_system:
_combat_system.enable_combat(false)
if _board_manager:
_board_manager.set_placement_mode(false)
# Calculate and award gold for wave completion
if _enemies_remaining == 0:
var gold_reward = 100 + (_current_wave * 50)
modify_gold(gold_reward)
wave_completed.emit(_current_wave + 1)
#print("Wave %d completed! Earned %d gold" % (_current_wave + 1, gold_reward))
else:
print("Wave %d failed - enemies escaped" % (_current_wave + 1))
if _ui_manager:
_ui_manager.show_resolution_summary(_enemies_remaining == 0)
func _update_resolution(delta: float) -> void:
# Resolution phase lasts 2 seconds, then transitions to escalation or game over
if _state_timer >= 2.0:
if _current_health <= 0:
print("Game Over - Health depleted")
_end_game(false)
else:
_transition_to_state(State.ESCALATION)
# ============================================================================
# ESCALATION PHASE: Difficulty increase, preview next wave
# ============================================================================
func _enter_escalation() -> void:
print("Entering ESCALATION phase")
_current_wave += 1
if _current_wave >= _total_waves:
print("All waves completed - Victory!")
_end_game(true)
return
# Prepare next wave
if _ui_manager:
_ui_manager.show_escalation_preview(_current_wave + 1, waves[_current_wave])
if _event_bus and _event_bus.has_signal("escalation_started"):
_event_bus.escalation_started.emit(_current_wave + 1)
func _update_escalation(delta: float) -> void:
# Escalation phase lasts 2-3 seconds, then returns to draft
if _state_timer >= 2.5:
# Clear previous tetrominoes from board (optional: allow stacking)
# For now, board persists across waves
_transition_to_state(State.DRAFT)
# ============================================================================
# GAME END & UTILITIES
# ============================================================================
## Called when game ends (victory or defeat)
func _end_game(victory: bool) -> void:
if victory:
print("VICTORY - Campaign completed!")
if _event_bus and _event_bus.has_signal("game_won"):
_event_bus.game_won.emit()
else:
print("DEFEAT - Health depleted")
if _event_bus and _event_bus.has_signal("game_lost"):
_event_bus.game_lost.emit()
if _ui_manager:
_ui_manager.show_game_over(victory)
# Pause game or transition to menu
get_tree().paused = true
## Modify player health (positive to heal, negative to damage)
func modify_health(amount: int) -> void:
var old_health = _current_health
_current_health = clampi(_current_health + amount, 0, starting_health)
if _current_health != old_health:
health_changed.emit(_current_health)
if _event_bus and _event_bus.has_signal("health_changed"):
_event_bus.health_changed.emit(_current_health)
## Modify player gold (positive to gain, negative to spend)
func modify_gold(amount: int) -> void:
var old_gold = _current_gold
_current_gold = maxi(_current_gold + amount, 0)
if _current_gold != old_gold:
gold_changed.emit(_current_gold)
if _event_bus and _event_bus.has_signal("gold_changed"):
_event_bus.gold_changed.emit(_current_gold)
## Get current game state
func get_current_state() -> State:
return _current_state
## Get current wave number (1-indexed)
func get_current_wave() -> int:
return _current_wave + 1
## Get total wave count
func get_total_waves() -> int:
return _total_waves
## Get current health
func get_health() -> int:
return _current_health
## Get current gold
func get_gold() -> int:
return _current_gold
## Check if combat is active
func is_combat_active() -> bool:
return _combat_in_progress