## 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 # @onready var camera: Camera3D = $Camera3D # 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_node_or_null("Board") #_combat_system = get_node_or_null("CombatSystem") #_synergy_system = get_node_or_null("SynergySystem") #_enemy_spawner = get_node_or_null("EnemySpawner") #_ui_manager = 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 _input(event: InputEvent) -> void: if _board_manager.selected_tetromino and event is InputEventMouseMotion: var mouse_pos = get_viewport().get_mouse_position() var ray_origin = camera.project_ray_origin(mouse_pos) var ray_direction = camera.project_ray_normal(mouse_pos) # Cast ray to find ground position (Y = 0 plane) var t = -ray_origin.y / ray_direction.y if ray_direction.y != 0 else 0.0 var world_hit = ray_origin + ray_direction * t # Convert to grid position var new_grid_pos = _board_manager.world_to_grid(world_hit) _board_manager.move_tetromino(_board_manager.selected_tetromino, new_grid_pos) func _on_tetromino_selected(tetromino: Tetromino): print("Tetromino selected") _board_manager.selected_tetromino = tetromino func _on_tetromino_deselected(tetromino: Tetromino): print("Tetromino deselected") _board_manager.selected_tetromino = null 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