Asteroids were pure hazard: the only thing one could ever do was cost you a ship. They now take damage like anything else and break open, dropping ore the fleet can fly over and collect - the one resource reachable in open space without conquering anything, and something for the new fire stick to do out there. Two ways to destroy one, deliberately not equivalent. Shot apart yields the ore; flown into does not - the rock shatters against a hull rather than being mined, and a ship or a shield charge has just been spent on it. Without that split a shield vessel could farm asteroids by ramming them. Asteroid.take_damage() matches Enemy's contract exactly, so every weapon that already knew how to hurt an enemy hurts an asteroid through the same branch rather than a second special case: bolts, the laser sweep, and the gas bomb's blast all pick both groups up together. Dropped ore carries the asteroid's own drift, so it reads as debris rather than as a spawned pickup, and it expires after a minute. Rock grew an optional lifetime for that, defaulting to 0 (never) so hand-placed surface rocks are unchanged - a planet surface is bounded and authored, open space is not, and a field shot up and abandoned would otherwise leak nodes for the session. Rock's header note that asteroids and rocks are never on screen together is no longer true and now says so; the hexagon-versus-lump shape language matters more now that they sit side by side, not less. Five weapon tests silenced the asteroid field but never cleared what it had already placed. That was harmless while asteroids were untouchable and is not any more - two of them assert exact hit counts, and a drifting rock in the beam would have made those fail intermittently. Suite 111/111. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
4.6 KiB
GDScript
128 lines
4.6 KiB
GDScript
class_name GasBombController
|
|
extends "res://AimedFleetAbilityController.gd"
|
|
|
|
const BOMB_COLOR := Color(0.96, 0.22, 0.72)
|
|
const TARGET_COLOR := Color(0.78, 0.32, 1.0)
|
|
const MAX_RANGE := 950.0
|
|
const AOE_RADIUS := 180.0
|
|
const DRAG_RADIUS := 260.0
|
|
const PROJECTILE_SPEED := 720.0
|
|
const DAMAGE := 3
|
|
const COOLDOWN := 5.0
|
|
const GAS_PER_SHOT: int = 5
|
|
|
|
var projectiles: Array = []
|
|
var explosions: Array = []
|
|
var last_hit_count: int = 0
|
|
|
|
var projectile_active: bool:
|
|
get: return not projectiles.is_empty()
|
|
|
|
var explosion_time: float:
|
|
get:
|
|
var longest: float = 0.0
|
|
for explosion in explosions:
|
|
longest = maxf(longest, float(explosion.time))
|
|
return longest
|
|
|
|
|
|
func _button_path() -> NodePath: return "../UI/GasBombButton"
|
|
func _status_path() -> NodePath: return "../UI/GasBombStatus"
|
|
func _component_id() -> String: return "gas_bomber"
|
|
func _ammo_resource() -> String: return GameState.RESOURCE_GAS
|
|
func _ammo_cost() -> int: return GAS_PER_SHOT
|
|
func _aim_padding() -> float: return AOE_RADIUS
|
|
func _aim_max_range() -> float: return MAX_RANGE
|
|
func _has_effect_visuals() -> bool: return not projectiles.is_empty() or not explosions.is_empty()
|
|
|
|
|
|
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
|
if drag.length() < 0.001:
|
|
return origin
|
|
var distance: float = minf(drag.length() / DRAG_RADIUS, 1.0) * MAX_RANGE
|
|
return origin + drag.normalized() * distance
|
|
|
|
|
|
func _consume_shot(component: Node) -> bool:
|
|
return component.consume_shot(COOLDOWN)
|
|
|
|
|
|
func _perform_shot(ship: Node2D, _component: Node) -> void:
|
|
projectiles.append({
|
|
"position": ship.global_position,
|
|
"target": aim_target,
|
|
"trail": PackedVector2Array([ship.global_position]),
|
|
})
|
|
|
|
|
|
func _next_ready_bomber() -> Node2D:
|
|
return _next_ready_ship()
|
|
|
|
|
|
func _advance_effects(delta: float) -> void:
|
|
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
|
|
_explode(projectile.target)
|
|
projectiles.remove_at(i)
|
|
else:
|
|
projectile.position = (projectile.position as Vector2).move_toward(
|
|
projectile.target, PROJECTILE_SPEED * delta)
|
|
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)
|
|
|
|
|
|
func _explode(at: Vector2) -> void:
|
|
explosions.append({"position": at, "time": 0.8})
|
|
last_hit_count = 0
|
|
# Asteroids too - a blast that clears everything around it but leaves the
|
|
# rocks untouched would need explaining, and they take damage the same way.
|
|
for enemy in get_tree().get_nodes_in_group("enemies") \
|
|
+ get_tree().get_nodes_in_group("asteroids"):
|
|
if is_instance_valid(enemy) and enemy is Node2D \
|
|
and enemy.global_position.distance_to(at) <= AOE_RADIUS:
|
|
enemy.take_damage(DAMAGE)
|
|
last_hit_count += 1
|
|
|
|
|
|
func _draw() -> void:
|
|
if aiming:
|
|
NeonDraw.draw_glowing_line(self, _aim_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)
|
|
for projectile in projectiles:
|
|
var trail: PackedVector2Array = projectile.trail
|
|
if trail.size() >= 2:
|
|
for i in range(1, trail.size()):
|
|
var alpha: float = float(i) / trail.size()
|
|
draw_line(trail[i - 1], trail[i], Color(BOMB_COLOR, alpha * 0.65),
|
|
4.0 + alpha * 4.0, true)
|
|
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: float = 1.0 - float(explosion.time) / 0.8
|
|
var radius: float = 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)
|
|
for i in range(7):
|
|
var angle: float = TAU * float(i) / 7.0 + progress
|
|
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))
|
|
|
|
|
|
func _draw_ring(center: Vector2, radius: float, color: Color, alpha: float) -> void:
|
|
draw_arc(center, radius, 0.0, TAU, 64, Color(color, 0.16 * alpha), 16.0, true)
|
|
draw_arc(center, radius, 0.0, TAU, 64, Color(color, 0.5 * alpha), 6.0, true)
|
|
draw_arc(center, radius, 0.0, TAU, 64, Color(1.0, 0.86, 0.96, 0.9 * alpha), 2.0, true)
|