Skip to content

Commit

Permalink
"Heat" mechanic, where overheating disables ship controls
Browse files Browse the repository at this point in the history
  • Loading branch information
jspahrsummers authored Sep 2, 2024
2 parents 41870a6 + d952e39 commit 5492b96
Show file tree
Hide file tree
Showing 27 changed files with 280 additions and 44 deletions.
32 changes: 17 additions & 15 deletions actors/ai/ai_navigation.gd
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
extends Node3D
class_name AINavigation

## Automatically navigates a [RigidBody3D] to a particular position using a [RigidBodyThruster] and a [RigidBodyDirection].
## Automatically navigates a [Ship] to a particular position using a [RigidBodyThruster] and a [RigidBodyDirection].
##
## [b]This script expects the parent node to be a [Ship].[/b]

@export var rigid_body_thruster: RigidBodyThruster
@export var rigid_body_direction: RigidBodyDirection
Expand Down Expand Up @@ -43,7 +45,7 @@ var navigating: bool:
else:
self._state = State.IDLE

@onready var _rigid_body := rigid_body_thruster.get_parent() as RigidBody3D
@onready var _ship := get_parent() as Ship

var _state: State = State.IDLE
var _target_direction: Vector3
Expand All @@ -56,7 +58,7 @@ func _ready() -> void:
self.navigating = true

func _physics_process(_delta: float) -> void:
if not self.navigating:
if not self.navigating or self._ship.controls_disabled():
return

match self._state:
Expand All @@ -76,7 +78,7 @@ func _idle() -> void:
self.rigid_body_thruster.throttle = 0.0

func _rotate_to_accelerate() -> void:
self._target_direction = (self.destination - self._rigid_body.global_position).normalized()
self._target_direction = (self.destination - self._ship.global_position).normalized()
self.rigid_body_direction.direction = self._target_direction
self.rigid_body_thruster.throttle = 0.0

Expand All @@ -86,52 +88,52 @@ func _rotate_to_accelerate() -> void:
func _accelerate_toward_destination() -> void:
self.rigid_body_thruster.throttle = 1.0

var distance_to_destination := self._rigid_body.global_position.distance_to(self.destination)
var stopping_distance := self._calculate_stopping_distance(self._rigid_body.linear_velocity.length())
var distance_to_destination := self._ship.global_position.distance_to(self.destination)
var stopping_distance := self._calculate_stopping_distance(self._ship.linear_velocity.length())

# Account for rotation time in stopping distance
var rotation_time := self._estimate_rotation_time(-self._rigid_body.linear_velocity.normalized())
var rotation_distance := self._rigid_body.linear_velocity.length() * rotation_time
var rotation_time := self._estimate_rotation_time(-self._ship.linear_velocity.normalized())
var rotation_distance := self._ship.linear_velocity.length() * rotation_time
stopping_distance += rotation_distance

if stopping_distance >= distance_to_destination:
self._state = State.ROTATING_TO_DECELERATE

func _rotate_to_decelerate() -> void:
self._target_direction = -self._rigid_body.linear_velocity.normalized()
self._target_direction = -self._ship.linear_velocity.normalized()
self.rigid_body_direction.direction = self._target_direction
self.rigid_body_thruster.throttle = 0.0

if self._pointing_in_direction(self._target_direction):
self._state = State.DECELERATING_TO_STOP

func _decelerate_to_stop() -> void:
self._target_direction = -self._rigid_body.linear_velocity.normalized()
self._target_direction = -self._ship.linear_velocity.normalized()
self.rigid_body_direction.direction = self._target_direction

if self._pointing_in_direction(self._target_direction):
self.rigid_body_thruster.throttle = 1.0
else:
self.rigid_body_thruster.throttle = 0.0

var distance_to_destination := self._rigid_body.global_position.distance_to(self.destination)
if distance_to_destination <= self.arrival_distance_tolerance and self._rigid_body.linear_velocity.length() <= self.stopping_velocity_tolerance:
var distance_to_destination := self._ship.global_position.distance_to(self.destination)
if distance_to_destination <= self.arrival_distance_tolerance and self._ship.linear_velocity.length() <= self.stopping_velocity_tolerance:
self.navigating = false
self.rigid_body_thruster.throttle = 0.0

print("Destination reached")
self.destination_reached.emit(self)

func _pointing_in_direction(direction: Vector3) -> bool:
var current_direction := -self._rigid_body.global_transform.basis.z
var current_direction := -self._ship.global_transform.basis.z
return current_direction.angle_to(direction) <= self.direction_tolerance

func _calculate_stopping_distance(current_velocity: float) -> float:
var acceleration := self.rigid_body_thruster.thruster.thrust_force / self._rigid_body.mass
var acceleration := self.rigid_body_thruster.thruster.thrust_force / self._ship.mass
return (current_velocity * current_velocity) / (2 * acceleration)

func _estimate_rotation_time(target_direction: Vector3) -> float:
var current_direction := -self._rigid_body.global_transform.basis.z
var current_direction := -self._ship.global_transform.basis.z
var angle_to_rotate := current_direction.angle_to(target_direction)
return angle_to_rotate / self.rigid_body_direction.spin_thruster.turning_rate

Expand Down
18 changes: 9 additions & 9 deletions actors/ai/archetypes/pirate.gd
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ func _select_new_patrol_target() -> void:
self._patrol_target = MathUtils.random_unit_vector() * self.patrol_radius

func _physics_process(delta: float) -> void:
if self._ship.controls_disabled():
self._ship.set_firing(false)
return

if not self._ship.targeting_system.target:
self._current_state = State.PATROL

Expand All @@ -85,7 +89,7 @@ func _pointing_in_direction(direction: Vector3) -> bool:
return current_direction.angle_to(direction) <= self.direction_tolerance

func _patrol_behavior(_delta: float) -> void:
self._set_firing(false)
self._ship.set_firing(false)

var target := self._find_closest_target()
self._ship.targeting_system.target = target
Expand All @@ -105,12 +109,12 @@ func _patrol_behavior(_delta: float) -> void:
func _engage_behavior(_delta: float) -> void:
var target := self._ship.targeting_system.target
if not target:
self._set_firing(false)
self._ship.set_firing(false)
self._current_state = State.PATROL
return

if not self._pointing_in_direction(self._desired_direction()):
self._set_firing(false)
self._ship.set_firing(false)
self._ship.rigid_body_thruster.throttle = 0.0
return

Expand All @@ -127,10 +131,10 @@ func _engage_behavior(_delta: float) -> void:
else:
self._ship.rigid_body_thruster.throttle = 0.0

self._set_firing(distance <= self.fire_range)
self._ship.set_firing(distance <= self.fire_range)

func _retreat_behavior(_delta: float) -> void:
self._set_firing(false)
self._ship.set_firing(false)

var target := self._ship.targeting_system.target
if not target:
Expand All @@ -146,10 +150,6 @@ func _retreat_behavior(_delta: float) -> void:
if distance >= self.preferred_distance:
self._current_state = State.ENGAGE

func _set_firing(firing: bool) -> void:
for weapon_mount in self._ship.weapon_mounts:
weapon_mount.firing = firing

func _find_closest_target() -> CombatObject:
var available_targets := self._ship.targeting_system.get_available_targets()
available_targets.erase(self._ship.combat_object)
Expand Down
21 changes: 16 additions & 5 deletions actors/player.gd
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ signal shield_changed(player: Player, shield: Shield)
## Fires when the ship's power level changes.
signal power_changed(player: Player, battery: Battery)

## Fires when the ship's heat level changes.
signal heat_changed(player: Player, heat_sink: HeatSink)

## Fires when the player's ship is destroyed.
signal ship_destroyed(player: Player)

Expand Down Expand Up @@ -107,13 +110,15 @@ func _ready() -> void:
self.ship.hull.changed.connect(_on_hull_changed)
self.ship.hull.hull_destroyed.connect(_on_hull_destroyed)
self.ship.battery.changed.connect(_on_power_changed)
self.ship.heat_sink.changed.connect(_on_heat_changed)
self.ship.targeting_system.target_changed.connect(_on_target_changed)

InputEventBroadcaster.input_event.connect(_on_broadcasted_input_event)

# Initial notifications so the UI can update.
self._on_hull_changed()
self._on_power_changed()
self._on_heat_changed()

if self.ship.shield:
self.ship.shield.changed.connect(_on_shield_changed)
Expand Down Expand Up @@ -145,6 +150,9 @@ func _on_shield_changed() -> void:
func _on_power_changed() -> void:
self.power_changed.emit(self, self.ship.battery)

func _on_heat_changed() -> void:
self.heat_changed.emit(self, self.ship.heat_sink)

func _on_hull_destroyed(hull: Hull) -> void:
assert(hull == self.ship.hull, "Received hull_destroyed signal from incorrect hull")
self.ship_destroyed.emit(self)
Expand Down Expand Up @@ -206,7 +214,7 @@ func _closest_landing_target() -> Celestial:
return nearest_celestial

func _unhandled_input(event: InputEvent) -> void:
if self.ship.hyperdrive_system.jumping:
if self.ship.controls_disabled():
return

var motion_event := event as InputEventMouseMotion
Expand Down Expand Up @@ -321,6 +329,7 @@ func _depart_from_port(port: Port) -> void:
if self.ship.shield:
self.ship.shield.integrity = self.ship.shield.max_integrity
self.ship.hull.integrity = self.ship.hull.max_integrity
self.ship.heat_sink.heat = 0

self._reset_controls()
self._reset_velocity()
Expand Down Expand Up @@ -358,16 +367,18 @@ func _mouse_joystick_input() -> Vector2:
return offset.normalized() * normalized_distance

func _physics_process(_delta: float) -> void:
if not self.ship.hyperdrive_system or self.ship.hyperdrive_system.jumping:
if self.ship.controls_disabled():
self.ship.set_firing(false)
self.ship.rigid_body_thruster.throttle = 0.0
self.ship.rigid_body_direction.direction = Vector3.ZERO
self._rigid_body_turner.turning = 0.0
return

if Input.is_action_pressed("jump"):
self._jump_to_hyperspace()
return

var firing := Input.is_action_pressed("fire")
for weapon_mount in self.ship.weapon_mounts:
weapon_mount.firing = firing
self.ship.set_firing(Input.is_action_pressed("fire"))

match UserPreferences.control_scheme:
UserPreferences.ControlScheme.RELATIVE:
Expand Down
5 changes: 5 additions & 0 deletions mechanics/combat/combat_object.gd
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ var hull: Hull:
## An optional shield protecting the object.
var shield: Shield

## A heat sink to dump heat to.
var heat_sink: HeatSink

## Fires when this object is targeted, or stops being targeted, by a new [TargetingSystem].
##
## See [method get_targeted_by]
Expand Down Expand Up @@ -106,6 +109,8 @@ func damage(dmg: Damage) -> void:

if apply_hull_dmg_pct > 0.0:
self.hull.integrity -= dmg.hull_damage * apply_hull_dmg_pct

self.heat_sink.heat += dmg.heat

## Checks whether [param node] contains a [CombatObject], and damages it if so.
static func damage_combat_object_inside(node: Node, dmg: Damage) -> bool:
Expand Down
5 changes: 5 additions & 0 deletions mechanics/combat/damage.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ class_name Damage
##
## If a damageable object has shields, damage is applied to the shields first, then the hull, in proportion.
@export var hull_damage: float

## How much heat will be inflicted on the target.
##
## Heat is not directly damaging, but can result in a ship becoming disabled.
@export var heat: float
1 change: 1 addition & 0 deletions mechanics/combat/weapons/blaster/blaster.tres
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ script = ExtResource("1_h0068")
fire_interval = 0.2
fire_force = 2.0
power_consumption = 5.0
heat = 10.0
projectile = ExtResource("1_upvwn")
1 change: 1 addition & 0 deletions mechanics/combat/weapons/blaster/blaster_bolt.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
script = ExtResource("2_eoln0")
shield_damage = 80.0
hull_damage = 40.0
heat = 20.0

[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_5b1vh"]

Expand Down
3 changes: 3 additions & 0 deletions mechanics/combat/weapons/weapon.gd
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ class_name Weapon
## If the containing ship's available power is less than this amount, the weapon won't be able to fire.
@export var power_consumption: float

## The amount of heat generated when firing the weapon.
@export var heat: float

## The projectile that this weapon fires. The root node [b]must[/b] be a [RigidBody3D].
@export var projectile: PackedScene
4 changes: 4 additions & 0 deletions mechanics/combat/weapons/weapon_mount.gd
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class_name WeaponMount
## The [Battery] to power the weapon from.
var battery: Battery

## The [HeatSink] to dump heat into.
var heat_sink: HeatSink

## Set to true when this weapon should automatically fire; set to false to stop firing.
var firing: bool = false:
set(value):
Expand Down Expand Up @@ -67,3 +70,4 @@ func _physics_process(_delta: float) -> void:
projectile_instance.apply_central_impulse(projectile_instance.transform.basis * self.weapon.fire_force * Vector3.FORWARD)

self._last_fired_usec = now
self.heat_sink.heat += self.weapon.heat
27 changes: 27 additions & 0 deletions mechanics/heat/heat_sink.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
extends SaveableResource
class_name HeatSink

## Accumulates heat that is generated by ship functions and inflicted in combat.
##
## When heat exceeds the maximum capacity of the heat sink, the ship's controls become inoperable.

## The maximum heat capacity for this sink.
@export var max_heat: float:
set(value):
assert(value >= 0.0, "Heat capacity must be non-negative")
if is_equal_approx(max_heat, value):
return

max_heat = value
self.emit_changed()

## The current heat level.
##
## Unlike other, similar mechanics, this value can exceed [member max_heat], at which point the ship controls are disabled.
@export var heat: float:
set(value):
if is_equal_approx(heat, value):
return

heat = maxf(value, 0.0)
self.emit_changed()
12 changes: 12 additions & 0 deletions mechanics/heat/radiator.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
extends Node3D
class_name Radiator

## Radiates heat away from a [HeatSink] into vacuum.

## How much heat is radiated away per second.
@export var rate: float

var heat_sink: HeatSink

func _physics_process(delta: float) -> void:
self.heat_sink.heat -= self.rate * delta
15 changes: 15 additions & 0 deletions mechanics/outfitting/outfit.gd
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class_name Outfit
## If negative, the output of the ship's [PowerGenerator] will be reduced by this amount. The outfit cannot be installed if the [PowerGenerator] is insufficient.
@export var modified_power_generation: float = 0

## A modification this outfit makes to heat sink capacity.
##
## If negative, the capacity of the ship's [HeatSink] will be reduced. The outfit cannot be installed if the available heat capacity is insufficient.
@export var modified_heat_capacity: float = 0

## A weapon provided by this outfit.
@export var weapon: Weapon

Expand Down Expand Up @@ -110,6 +115,11 @@ func can_install_onto_ship(ship: Ship) -> bool:

if generator.rate_of_power < self.modified_power_generation:
return false

if not is_zero_approx(self.modified_heat_capacity):
# Include some epsilon so the ship doesn't end up overheated and inoperable
if ship.heat_sink.max_heat + self.modified_heat_capacity < 0.01:
return false

if self.weapon:
var mount_available := false
Expand Down Expand Up @@ -143,6 +153,7 @@ func apply_to_ship(ship: Ship) -> void:
ship.shield.recharge_rate *= self.shield_recharge_multiplier

ship.hull.max_integrity += self.modified_hull_capacity
ship.heat_sink.max_heat += self.modified_heat_capacity

if ship.power_management_unit.power_generator:
ship.power_management_unit.power_generator.rate_of_power += self.modified_power_generation
Expand Down Expand Up @@ -175,6 +186,7 @@ func remove_from_ship(ship: Ship) -> void:
ship.shield.recharge_rate /= self.shield_recharge_multiplier

ship.hull.max_integrity -= self.modified_hull_capacity
ship.heat_sink.max_heat -= self.modified_heat_capacity

if ship.power_management_unit.power_generator:
ship.power_management_unit.power_generator.rate_of_power -= self.modified_power_generation
Expand Down Expand Up @@ -220,6 +232,9 @@ func get_effects() -> PackedStringArray:
effects.push_back("[b]Fire interval:[/b] %ss" % [self.weapon.fire_interval])
effects.push_back("[b]Power consumption per shot:[/b] %s" % [self.weapon.power_consumption])

if not is_zero_approx(self.modified_heat_capacity):
effects.push_back("[b]Heat:[/b] %s" % self._signed_string(-self.modified_heat_capacity))

return effects

func _signed_string(value: float) -> String:
Expand Down
Loading

0 comments on commit 5492b96

Please sign in to comment.