Track gas bomb cooldowns per bomber
This commit is contained in:
parent
1d9afcbc2e
commit
eb79efdafe
6 changed files with 193 additions and 49 deletions
|
|
@ -23,16 +23,20 @@ var aim_screen := Vector2.ZERO
|
|||
var aim_target := Vector2.ZERO
|
||||
var saved_zoom := 1.0
|
||||
var restoring_zoom := false
|
||||
var cooldown_left := 0.0
|
||||
|
||||
var projectile_active := false
|
||||
var projectile_position := Vector2.ZERO
|
||||
var projectile_target := Vector2.ZERO
|
||||
var projectile_trail := PackedVector2Array()
|
||||
var explosion_time := 0.0
|
||||
var explosion_position := Vector2.ZERO
|
||||
var projectiles: Array = []
|
||||
var explosions: Array = []
|
||||
var last_hit_count := 0
|
||||
|
||||
var projectile_active: bool:
|
||||
get: return not projectiles.is_empty()
|
||||
|
||||
var explosion_time: float:
|
||||
get:
|
||||
var longest := 0.0
|
||||
for explosion in explosions:
|
||||
longest = maxf(longest, float(explosion.time))
|
||||
return longest
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
button.visible = false
|
||||
|
|
@ -40,10 +44,9 @@ func _ready() -> void:
|
|||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var was_exploding := explosion_time > 0.0
|
||||
var had_visuals := aiming or not projectiles.is_empty() or not explosions.is_empty()
|
||||
button.visible = fleet.has_capability("gas_bomber")
|
||||
cooldown_left = maxf(cooldown_left - delta, 0.0)
|
||||
button.enabled = cooldown_left <= 0.0
|
||||
button.enabled = _next_ready_bomber() != null
|
||||
if not button.visible and aiming:
|
||||
_cancel_aim()
|
||||
|
||||
|
|
@ -53,27 +56,33 @@ func _process(delta: float) -> void:
|
|||
if is_equal_approx(z, saved_zoom):
|
||||
restoring_zoom = false
|
||||
|
||||
if projectile_active:
|
||||
var distance := projectile_position.distance_to(projectile_target)
|
||||
for i in range(projectiles.size() - 1, -1, -1):
|
||||
var projectile: Dictionary = projectiles[i]
|
||||
var distance: float = (projectile.position as Vector2).distance_to(projectile.target)
|
||||
if distance <= PROJECTILE_SPEED * delta:
|
||||
projectile_position = projectile_target
|
||||
projectile_active = false
|
||||
_explode(projectile_target)
|
||||
projectile.position = projectile.target
|
||||
_explode(projectile.target)
|
||||
projectiles.remove_at(i)
|
||||
else:
|
||||
projectile_position = projectile_position.move_toward(projectile_target,
|
||||
projectile.position = (projectile.position as Vector2).move_toward(projectile.target,
|
||||
PROJECTILE_SPEED * delta)
|
||||
projectile_trail.append(projectile_position)
|
||||
while projectile_trail.size() > 12:
|
||||
projectile_trail.remove_at(0)
|
||||
if explosion_time > 0.0:
|
||||
explosion_time = maxf(explosion_time - delta, 0.0)
|
||||
if aiming or projectile_active or explosion_time > 0.0 or was_exploding:
|
||||
var trail: PackedVector2Array = projectile.trail
|
||||
trail.append(projectile.position)
|
||||
while trail.size() > 12:
|
||||
trail.remove_at(0)
|
||||
projectile.trail = trail
|
||||
for i in range(explosions.size() - 1, -1, -1):
|
||||
var explosion: Dictionary = explosions[i]
|
||||
explosion.time = maxf(float(explosion.time) - delta, 0.0)
|
||||
if float(explosion.time) <= 0.0:
|
||||
explosions.remove_at(i)
|
||||
if aiming or not projectiles.is_empty() or not explosions.is_empty() or had_visuals:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func can_begin_aim() -> bool:
|
||||
return button.visible and button.enabled and not button._blocked_by_modal() \
|
||||
and not aiming and not projectile_active
|
||||
and not aiming
|
||||
|
||||
|
||||
func handle_touch(event: InputEventScreenTouch) -> bool:
|
||||
|
|
@ -143,21 +152,35 @@ func _cancel_aim() -> void:
|
|||
|
||||
|
||||
func _fire() -> void:
|
||||
var bomber: Node2D = fleet.first_ship_with_capability("gas_bomber")
|
||||
var bomber: Node2D = _next_ready_bomber()
|
||||
if bomber == null:
|
||||
_cancel_aim()
|
||||
return
|
||||
projectile_active = true
|
||||
projectile_position = bomber.global_position
|
||||
projectile_target = aim_target
|
||||
projectile_trail = PackedVector2Array([projectile_position])
|
||||
cooldown_left = COOLDOWN
|
||||
var component: Node = bomber.get_component("gas_bomber")
|
||||
if not component.consume_shot(COOLDOWN):
|
||||
_cancel_aim()
|
||||
return
|
||||
projectiles.append({
|
||||
"position": bomber.global_position,
|
||||
"target": aim_target,
|
||||
"trail": PackedVector2Array([bomber.global_position]),
|
||||
})
|
||||
_finish_aim()
|
||||
button.enabled = _next_ready_bomber() != null
|
||||
|
||||
|
||||
func _next_ready_bomber() -> Node2D:
|
||||
for ship in fleet.ships:
|
||||
if not is_instance_valid(ship):
|
||||
continue
|
||||
var component: Node = ship.get_component("gas_bomber")
|
||||
if component != null and component.is_ready():
|
||||
return ship
|
||||
return null
|
||||
|
||||
|
||||
func _explode(at: Vector2) -> void:
|
||||
explosion_position = at
|
||||
explosion_time = 0.8
|
||||
explosions.append({"position": at, "time": 0.8})
|
||||
last_hit_count = 0
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(enemy) and enemy is Node2D \
|
||||
|
|
@ -172,22 +195,23 @@ func _draw() -> void:
|
|||
NeonDraw.draw_glowing_line(self, origin, aim_target, TARGET_COLOR, 0.7, 0.8)
|
||||
_draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8)
|
||||
_draw_ring(aim_target, 18.0, BOMB_COLOR, 1.0)
|
||||
if projectile_trail.size() >= 2 and projectile_active:
|
||||
for i in range(1, projectile_trail.size()):
|
||||
var alpha := float(i) / projectile_trail.size()
|
||||
draw_line(projectile_trail[i - 1], projectile_trail[i],
|
||||
for projectile in projectiles:
|
||||
var trail: PackedVector2Array = projectile.trail
|
||||
if trail.size() >= 2:
|
||||
for i in range(1, trail.size()):
|
||||
var alpha := float(i) / trail.size()
|
||||
draw_line(trail[i - 1], trail[i],
|
||||
Color(BOMB_COLOR, alpha * 0.65), 4.0 + alpha * 4.0, true)
|
||||
if projectile_active:
|
||||
draw_circle(projectile_position, 12.0, Color(BOMB_COLOR, 0.35))
|
||||
NeonDraw.draw_glowing_circle(self, projectile_position, 7.0, BOMB_COLOR, 1.0)
|
||||
if explosion_time > 0.0:
|
||||
var progress := 1.0 - explosion_time / 0.8
|
||||
draw_circle(projectile.position, 12.0, Color(BOMB_COLOR, 0.35))
|
||||
NeonDraw.draw_glowing_circle(self, projectile.position, 7.0, BOMB_COLOR, 1.0)
|
||||
for explosion in explosions:
|
||||
var progress := 1.0 - float(explosion.time) / 0.8
|
||||
var radius := lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0))
|
||||
draw_circle(explosion_position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16))
|
||||
_draw_ring(explosion_position, radius, BOMB_COLOR, 1.0 - progress)
|
||||
draw_circle(explosion.position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16))
|
||||
_draw_ring(explosion.position, radius, BOMB_COLOR, 1.0 - progress)
|
||||
for i in range(7):
|
||||
var angle := TAU * float(i) / 7.0 + progress
|
||||
var cloud_at := explosion_position + Vector2(cos(angle), sin(angle)) * radius * 0.55
|
||||
var cloud_at: Vector2 = explosion.position + Vector2(cos(angle), sin(angle)) * radius * 0.55
|
||||
draw_circle(cloud_at, 18.0 + 14.0 * progress, Color(BOMB_COLOR, (1.0 - progress) * 0.22))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,54 @@
|
|||
extends Node
|
||||
class_name VesselGasBomberComponent
|
||||
extends Node2D
|
||||
|
||||
var capability_id := "gas_bomber"
|
||||
var cooldown_duration := 5.0
|
||||
var cooldown_left := 0.0
|
||||
|
||||
const READY_COLOR := Color(1.0, 0.25, 0.78)
|
||||
const COOLING_COLOR := Color(0.38, 0.16, 0.32)
|
||||
|
||||
|
||||
func setup() -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func is_ready() -> bool:
|
||||
return cooldown_left <= 0.0
|
||||
|
||||
|
||||
func consume_shot(duration: float) -> bool:
|
||||
if not is_ready():
|
||||
return false
|
||||
cooldown_duration = maxf(duration, 0.001)
|
||||
cooldown_left = cooldown_duration
|
||||
queue_redraw()
|
||||
return true
|
||||
|
||||
|
||||
func tick(delta: float) -> void:
|
||||
if cooldown_left <= 0.0:
|
||||
return
|
||||
cooldown_left = maxf(cooldown_left - delta, 0.0)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func cooldown_progress() -> float:
|
||||
return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var parent_ship: Node = get_parent()
|
||||
var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE
|
||||
if is_ready():
|
||||
# Bright, complete halo: this bomber owns one available shot.
|
||||
NeonDraw.draw_glowing_circle(self, Vector2.ZERO, 31.0, READY_COLOR, 0.9, core)
|
||||
draw_circle(Vector2.ZERO, 4.0, Color(READY_COLOR, 0.5))
|
||||
return
|
||||
# A muted full ring says "not ready"; the bright arc fills clockwise so
|
||||
# the individual bomber also communicates how close it is to rearming.
|
||||
draw_arc(Vector2.ZERO, 31.0, 0.0, TAU, 48, Color(COOLING_COLOR, 0.65), 4.0, true)
|
||||
var progress := cooldown_progress()
|
||||
if progress > 0.001:
|
||||
draw_arc(Vector2.ZERO, 31.0, -PI / 2.0,
|
||||
-PI / 2.0 + TAU * progress, 48, Color(READY_COLOR, 0.92), 4.0, true)
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
[ext_resource type="Script" path="res://VesselGasBomberComponent.gd" id="1"]
|
||||
|
||||
[node name="GasBomberComponent" type="Node"]
|
||||
[node name="GasBomberComponent" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
|
|||
|
|
@ -15,11 +15,14 @@ func _ready() -> void:
|
|||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
var fleet: Node = main.get_node("Fleet")
|
||||
fleet.spawn_ships(3, Vector2.ZERO, PackedStringArray([
|
||||
fleet.spawn_ships(4, Vector2.ZERO, PackedStringArray([
|
||||
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||
GameState.VESSEL_GAS_BOMBER]))
|
||||
GameState.VESSEL_GAS_BOMBER, GameState.VESSEL_GAS_BOMBER]))
|
||||
var skill: GasBombController = main.get_node("GasBombController")
|
||||
skill._process(0.0)
|
||||
# Show both per-ship states in one frame: the first bomber is rearming,
|
||||
# while the second has the bright complete ready halo and supplies the aim.
|
||||
fleet.ships[2].get_component("gas_bomber").consume_shot(GasBombController.COOLDOWN)
|
||||
main.get_node("Camera2D").zoom = Vector2(1.8, 1.8)
|
||||
var button: Control = main.get_node("UI/GasBombButton")
|
||||
var press := InputEventScreenTouch.new()
|
||||
|
|
|
|||
65
tests/test_gas_bomber_cooldowns.gd
Normal file
65
tests/test_gas_bomber_cooldowns.gd
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
extends SceneTree
|
||||
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var skill: GasBombController
|
||||
var elapsed := 0.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var state: Node = root.get_node("GameState")
|
||||
state.buildings.clear()
|
||||
state.extractors.clear()
|
||||
state.transporters.clear()
|
||||
state.vessel_levels["unlock_gas_bomber"] = 1
|
||||
main = load("res://Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
skill = main.get_node("GasBombController")
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
if elapsed < 0.2:
|
||||
return false
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([
|
||||
GameState.VESSEL_GAS_BOMBER, GameState.VESSEL_GAS_BOMBER]))
|
||||
skill._process(0.0)
|
||||
var first: Node = fleet.ships[0].get_component("gas_bomber")
|
||||
var second: Node = fleet.ships[1].get_component("gas_bomber")
|
||||
if not first.is_ready() or not second.is_ready() or not skill.button.enabled:
|
||||
return _fail("two fresh bombers did not each begin ready")
|
||||
|
||||
skill.aim_target = Vector2(500, 0)
|
||||
skill._fire()
|
||||
skill._process(0.0)
|
||||
if first.is_ready() or not second.is_ready() or not skill.button.enabled:
|
||||
return _fail("the first shot disabled more than its firing bomber")
|
||||
if skill.projectiles.size() != 1:
|
||||
return _fail("the first bomber did not launch exactly one projectile")
|
||||
|
||||
skill.aim_target = Vector2(-500, 0)
|
||||
skill._fire()
|
||||
skill._process(0.0)
|
||||
if first.is_ready() or second.is_ready() or skill.button.enabled:
|
||||
return _fail("the skill stayed available after both bombers fired")
|
||||
if skill.projectiles.size() != 2:
|
||||
return _fail("independent bombers could not have shots in flight together")
|
||||
|
||||
# Rearming one bomber restores one available shot without touching the
|
||||
# other bomber's cooldown.
|
||||
first.tick(GasBombController.COOLDOWN)
|
||||
skill._process(0.0)
|
||||
if not first.is_ready() or second.is_ready() or not skill.button.enabled:
|
||||
return _fail("one completed cooldown did not restore exactly one shot")
|
||||
print("TEST_GAS_BOMBER_COOLDOWNS: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_GAS_BOMBER_COOLDOWNS: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
1
tests/test_gas_bomber_cooldowns.gd.uid
Normal file
1
tests/test_gas_bomber_cooldowns.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwo6ake5taias
|
||||
Loading…
Reference in a new issue