Complete lab, quest, garrison, and energy tickets

This commit is contained in:
Loïc Wisniewski 2026-09-03 00:12:08 +02:00
commit 0f7edff15a
20 changed files with 238 additions and 45 deletions

View file

@ -1,5 +1,7 @@
extends Area2D
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
# #86: faster. The bolt used to crawl far enough behind a moving target that
# leading a shot was guesswork rather than skill.
# #155: was 850, which took 1.1s to cross the gun's own range - long enough
@ -54,9 +56,9 @@ func _physics_process(delta: float) -> void:
func _on_area_entered(area: Area2D) -> void:
# Asteroids answer take_damage() exactly as enemies do, so both are hit by
# the same branch rather than by a second special case.
if area.is_in_group("enemies") or area.is_in_group("asteroids"):
# Anything that opted into the weapon-target contract answers take_damage(),
# so adding a new target composition needs no projectile type check.
if area.is_in_group(COMBAT_TARGET.GROUP):
area.take_damage(damage)
queue_free()

View file

@ -3,6 +3,7 @@ extends Area2D
signal destroyed
const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd")
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
@export var max_speed: float = 460.0
@export var acceleration: float = 450.0
@ -200,17 +201,14 @@ func _breakaway_direction() -> Vector2:
func _on_area_entered(area: Area2D) -> void:
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
if not area.is_in_group(COMBAT_TARGET.GROUP):
return
# The enemy is told about the impact BEFORE this ship resolves its own
# damage, because resolving it can free this node - and a ship that dies
# to a ramming chaser must still take the chaser with it. Whether the
# impact actually destroys the enemy is the enemy's decision (#82).
if area.is_in_group("enemies") and area.has_method("collide_with_player_vessel"):
# A target is told about the impact BEFORE this ship resolves its own
# damage, because resolving it can free this node. Whether the impact does
# anything to the target is part of that target's composition (#82).
if area.has_method("collide_with_player_vessel"):
area.collide_with_player_vessel()
var absorbed: bool = take_damage(1)
if absorbed and area.is_in_group("asteroids") and is_instance_valid(area):
area.queue_free()
take_damage(1)
func _draw() -> void:

View file

@ -1,6 +1,8 @@
class_name VesselGarrisonComponent
extends Node
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
# #105 (#97g): "When a vessel is assigned to an outpost, it navigates around
# the outpost by itself, attacking nearby ennemies if any are detected."
#
@ -66,7 +68,7 @@ func is_engaging() -> bool:
func tick(delta: float) -> void:
var ship: Node2D = get_parent()
var target: Node2D = _nearest_enemy(ship)
var target: Node2D = _nearest_target(ship)
_engaging = target != null
if target == null:
_patrol(ship, delta)
@ -176,20 +178,27 @@ func _hold_fire(ship: Node2D) -> void:
weapon.set_fire_direction(Vector2.ZERO)
func _nearest_enemy(ship: Node2D) -> Node2D:
func _nearest_target(ship: Node2D) -> Node2D:
var nearest: Node2D = null
var best: float = detection_range
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy) or enemy.is_queued_for_deletion():
var best_distance: float = INF
var best_priority: int = -1
for target_node in get_tree().get_nodes_in_group(COMBAT_TARGET.GROUP):
if not is_instance_valid(target_node) or not (target_node is Node2D) \
or target_node.is_queued_for_deletion():
continue
var at: Vector2 = (enemy as Node2D).global_position
var target: Node2D = target_node as Node2D
var at: Vector2 = target.global_position
# Judged from the POST, not from the vessel: otherwise a garrison
# that had drifted out to its leash would see further still, and
# could be walked away from its outpost one enemy at a time.
if station.distance_to(at) > leash_range + engage_distance:
continue
var d: float = ship.global_position.distance_to(at)
if d < best:
best = d
nearest = enemy
if d > detection_range:
continue
var priority: int = COMBAT_TARGET.priority(target)
if priority > best_priority or (priority == best_priority and d < best_distance):
best_priority = priority
best_distance = d
nearest = target
return nearest

View file

@ -36,6 +36,7 @@ var fire_timer: float = 0.0
var fire_target: Vector2 = Vector2.ZERO
var has_fire_target: bool = false
var projectile_scene: PackedScene = preload("res://vessels/Projectile.tscn")
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
# By path, not by class name - see the note at the top of MuzzleSparkVFX.
const MUZZLE_SPARK = preload("res://vfx/MuzzleSparkVFX.gd")
@ -58,7 +59,6 @@ const ASSIST_CONE_DEGREES: float = 28.0
# smaller than the directional cone at ordinary weapon range: it catches a
# pointer placed beside something without making the whole screen magnetic.
const ASSIST_CURSOR_RADIUS: float = 180.0
const ASSIST_TARGET_GROUP: StringName = &"player_aim_targets"
# The concrete point an aim should stick to. A target can qualify in two ways:
@ -81,7 +81,7 @@ static func assisted_point(tree: SceneTree, origin: Vector2, point: Vector2,
var cursor_distance: float = INF
var direction_pick: Node2D = null
var direction_distance: float = INF
for target_node in tree.get_nodes_in_group(ASSIST_TARGET_GROUP):
for target_node in tree.get_nodes_in_group(COMBAT_TARGET.GROUP):
if not is_instance_valid(target_node) or not (target_node is Node2D) \
or target_node.is_queued_for_deletion():
continue