Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions godot/autoloads/EventBus.gd
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ signal unit_died(unit_id: StringName, owner_id: StringName, hex: Vector2i)
## Emitted when a unit receives a promotion or upgrade.
signal unit_promoted(unit_id: StringName, promotion_id: StringName, bonuses: Dictionary)

## unit_xp_gained(unit_id: StringName, amount: int, hex: Vector2i)
## Emitted when a unit earns experience from combat, exploration, or objectives.
signal unit_xp_gained(unit_id: StringName, amount: int, hex: Vector2i)


# City events

Expand All @@ -50,6 +54,10 @@ signal city_captured(
## Emitted when a city's production queue completes an item.
signal production_complete(city_id: StringName, production_id: StringName, result: Dictionary)

## resource_discovered(resource_id: StringName, hex: Vector2i, metadata: Dictionary)
## Emitted when a hidden resource or discovery tile is revealed.
signal resource_discovered(resource_id: StringName, hex: Vector2i, metadata: Dictionary)


# Turn events

Expand Down Expand Up @@ -137,6 +145,15 @@ func emit_unit_promoted(
unit_promoted.emit(unit_id, promotion_id, bonuses)


func emit_unit_xp_gained(
unit_id: StringName,
amount: int,
hex: Vector2i = Vector2i.ZERO
) -> void:
_log_event("unit_xp_gained", [unit_id, amount, hex])
unit_xp_gained.emit(unit_id, amount, hex)


func emit_city_built(city_id: StringName, owner_id: StringName, hex: Vector2i) -> void:
_log_event("city_built", [city_id, owner_id, hex])
city_built.emit(city_id, owner_id, hex)
Expand All @@ -160,6 +177,15 @@ func emit_production_complete(
production_complete.emit(city_id, production_id, result)


func emit_resource_discovered(
resource_id: StringName,
hex: Vector2i,
metadata: Dictionary = {}
) -> void:
_log_event("resource_discovered", [resource_id, hex, metadata])
resource_discovered.emit(resource_id, hex, metadata)


func emit_turn_started(turn_number: int, player_id: StringName) -> void:
_log_event("turn_started", [turn_number, player_id])
turn_started.emit(turn_number, player_id)
Expand Down
170 changes: 170 additions & 0 deletions godot/autoloads/ParticleManager.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
extends Node

const EFFECT_COMBAT_EXPLOSION := &"combat_explosion"
const EFFECT_CITY_BUILD := &"city_build"
const EFFECT_XP_GAIN := &"xp_gain"
const EFFECT_LEVEL_UP := &"level_up"
const EFFECT_RESOURCE_DISCOVERY := &"resource_discovery"
const EFFECT_FLAG_CAPTURE := &"flag_capture"

const DEFAULT_HEX_WIDTH := 72.0
const DEFAULT_HEX_HEIGHT := 62.0
const DEFAULT_HEX_ROW_OFFSET := 36.0

var _unit_positions: Dictionary = {}
var _city_positions: Dictionary = {}
var _hex_positions: Dictionary = {}


func _ready() -> void:
_connect_events()


func register_unit_position(unit_id: StringName, world_position: Vector2) -> void:
_unit_positions[unit_id] = world_position


func unregister_unit(unit_id: StringName) -> void:
_unit_positions.erase(unit_id)


func register_city_position(city_id: StringName, world_position: Vector2) -> void:
_city_positions[city_id] = world_position


func unregister_city(city_id: StringName) -> void:
_city_positions.erase(city_id)


func register_hex_position(hex: Vector2i, world_position: Vector2) -> void:
_hex_positions[hex] = world_position


func clear_registered_positions() -> void:
_unit_positions.clear()
_city_positions.clear()
_hex_positions.clear()


func _connect_events() -> void:
_connect_event(EventBus.unit_attacked, _on_unit_attacked)
_connect_event(EventBus.unit_died, _on_unit_died)
_connect_event(EventBus.unit_promoted, _on_unit_promoted)
_connect_event(EventBus.unit_xp_gained, _on_unit_xp_gained)
_connect_event(EventBus.city_built, _on_city_built)
_connect_event(EventBus.city_captured, _on_city_captured)
_connect_event(EventBus.production_complete, _on_production_complete)
_connect_event(EventBus.resource_discovered, _on_resource_discovered)


func _connect_event(event_signal: Signal, callback: Callable) -> void:
if not event_signal.is_connected(callback):
event_signal.connect(callback)


func _on_unit_attacked(
_attacker_id: StringName,
defender_id: StringName,
_damage: int,
_remaining_health: int
) -> void:
_play(EFFECT_COMBAT_EXPLOSION, _position_for_unit(defender_id))


func _on_unit_died(_unit_id: StringName, _owner_id: StringName, hex: Vector2i) -> void:
_play(EFFECT_COMBAT_EXPLOSION, _position_for_hex(hex))


func _on_unit_promoted(
unit_id: StringName,
_promotion_id: StringName,
_bonuses: Dictionary
) -> void:
_play(EFFECT_LEVEL_UP, _position_for_unit(unit_id))


func _on_unit_xp_gained(unit_id: StringName, _amount: int, hex: Vector2i) -> void:
_play(EFFECT_XP_GAIN, _position_for_unit_or_hex(unit_id, hex))


func _on_city_built(city_id: StringName, _owner_id: StringName, hex: Vector2i) -> void:
var position := _position_for_hex(hex)
register_city_position(city_id, position)
_play(EFFECT_CITY_BUILD, position)


func _on_city_captured(
city_id: StringName,
_previous_owner_id: StringName,
_new_owner_id: StringName
) -> void:
_play(EFFECT_FLAG_CAPTURE, _position_for_city(city_id))


func _on_production_complete(
city_id: StringName,
_production_id: StringName,
result: Dictionary
) -> void:
_play(EFFECT_CITY_BUILD, _position_from_result(result, _position_for_city(city_id)))


func _on_resource_discovered(
_resource_id: StringName,
hex: Vector2i,
_metadata: Dictionary
) -> void:
_play(EFFECT_RESOURCE_DISCOVERY, _position_for_hex(hex))


func _play(effect_type: StringName, world_position: Vector2) -> void:
ParticlePool.play(effect_type, world_position)


func _position_for_unit(unit_id: StringName) -> Vector2:
if _unit_positions.has(unit_id):
return _unit_positions[unit_id]
return Vector2.ZERO


func _position_for_city(city_id: StringName) -> Vector2:
if _city_positions.has(city_id):
return _city_positions[city_id]
return Vector2.ZERO


func _position_for_unit_or_hex(unit_id: StringName, hex: Vector2i) -> Vector2:
if _unit_positions.has(unit_id):
return _unit_positions[unit_id]
return _position_for_hex(hex)


func _position_for_hex(hex: Vector2i) -> Vector2:
if _hex_positions.has(hex):
return _hex_positions[hex]

var row_offset := DEFAULT_HEX_ROW_OFFSET if abs(hex.y) % 2 == 1 else 0.0
return Vector2(
float(hex.x) * DEFAULT_HEX_WIDTH + row_offset,
float(hex.y) * DEFAULT_HEX_HEIGHT
)


func _position_from_result(result: Dictionary, fallback: Vector2) -> Vector2:
if result.has("world_position"):
return _coerce_world_position(result["world_position"], fallback)
if result.has("position"):
return _coerce_world_position(result["position"], fallback)
if result.has("hex") and result["hex"] is Vector2i:
return _position_for_hex(result["hex"])
return fallback


func _coerce_world_position(value: Variant, fallback: Vector2) -> Vector2:
if value is Vector2:
return value
if value is Vector2i:
return Vector2(value)
if typeof(value) == TYPE_DICTIONARY and value.has("x") and value.has("y"):
return Vector2(float(value["x"]), float(value["y"]))
return fallback
123 changes: 123 additions & 0 deletions godot/autoloads/ParticlePool.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
extends Node

const POOL_SIZE := 8

const EFFECT_SCENES := {
&"combat_explosion": preload("res://scenes/fx_combat_explosion.tscn"),
&"city_build": preload("res://scenes/fx_city_build.tscn"),
&"xp_gain": preload("res://scenes/fx_xp_gain.tscn"),
&"level_up": preload("res://scenes/fx_level_up.tscn"),
&"resource_discovery": preload("res://scenes/fx_resource_discovery.tscn"),
&"flag_capture": preload("res://scenes/fx_flag_capture.tscn"),
}

var _available: Dictionary = {}
var _active: Dictionary = {}


func _ready() -> void:
_warm_pools()


func get(effect_type: StringName) -> GPUParticles2D:
if not EFFECT_SCENES.has(effect_type):
push_warning("Unknown particle effect type: %s" % effect_type)
return null

var available: Array = _available[effect_type]
if available.is_empty():
_reclaim_oldest(effect_type)

if available.is_empty():
return null

var effect: GPUParticles2D = available.pop_back()
_active[effect_type].append(effect)
return effect


func release(effect: GPUParticles2D) -> void:
if effect == null or not is_instance_valid(effect):
return

var effect_type: StringName = effect.get_meta("particle_pool_type", &"")
if not EFFECT_SCENES.has(effect_type):
effect.queue_free()
return

_active[effect_type].erase(effect)
if _available[effect_type].has(effect):
return

effect.emitting = false
effect.visible = false
effect.position = Vector2.ZERO

var parent := effect.get_parent()
if parent != null and parent != self:
parent.remove_child(effect)
if effect.get_parent() == null:
add_child(effect)

_available[effect_type].append(effect)


func play(
effect_type: StringName,
world_position: Vector2,
target_parent: Node = null
) -> GPUParticles2D:
var effect := get(effect_type)
if effect == null:
return null

var parent: Node = target_parent
if parent == null:
parent = get_tree().current_scene
if parent == null:
parent = get_tree().root

var current_parent := effect.get_parent()
if current_parent != parent:
if current_parent != null:
current_parent.remove_child(effect)
parent.add_child(effect)

effect.global_position = world_position
effect.visible = true
effect.restart()
effect.emitting = true
return effect


func _warm_pools() -> void:
for effect_type in EFFECT_SCENES.keys():
_available[effect_type] = []
_active[effect_type] = []
for index in range(POOL_SIZE):
var effect := _create_effect(effect_type, index)
add_child(effect)
_available[effect_type].append(effect)


func _create_effect(effect_type: StringName, index: int) -> GPUParticles2D:
var effect: GPUParticles2D = EFFECT_SCENES[effect_type].instantiate()
effect.name = "%s_%02d" % [String(effect_type), index]
effect.visible = false
effect.emitting = false
effect.set_meta("particle_pool_type", effect_type)
effect.finished.connect(_on_effect_finished.bind(effect))
return effect


func _reclaim_oldest(effect_type: StringName) -> void:
var active: Array = _active[effect_type]
if active.is_empty():
return

var effect: GPUParticles2D = active.pop_front()
release(effect)


func _on_effect_finished(effect: GPUParticles2D) -> void:
release(effect)
Loading
Loading