Start encounter feature
This commit is contained in:
465
scripts/encounter_manager.gd
Normal file
465
scripts/encounter_manager.gd
Normal file
@@ -0,0 +1,465 @@
|
||||
## 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
|
||||
Reference in New Issue
Block a user