Resolve pending gameplay tickets
This commit is contained in:
parent
ec20778215
commit
c2ebbfaa2d
28 changed files with 618 additions and 188 deletions
|
|
@ -15,54 +15,11 @@ extends Area2D
|
|||
# for.
|
||||
|
||||
const STRIKE_INTERVAL: float = 0.7
|
||||
# #165: how fast a friendly vessel has to be going before touching a structure
|
||||
# counts as hitting it. Comfortably above the speed a fleet drifts onto its own
|
||||
# mothership to unload at, and below cruise - so ordinary docking is free and
|
||||
# flying INTO a building is not.
|
||||
const RAM_SPEED: float = 420.0
|
||||
const RAM_DAMAGE: int = 4
|
||||
const RAM_DAMAGE_MAX: int = 12
|
||||
|
||||
var _strike_timer: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player_buildings")
|
||||
area_entered.connect(_on_area_entered)
|
||||
|
||||
|
||||
# #165: a vessel of the player's own, arriving fast enough, damages what it
|
||||
# hits.
|
||||
#
|
||||
# On ENTRY, not on the strike tick above. The tick is right for a siege - an
|
||||
# enemy parked on a structure is besieging it - but wrong for a collision: a
|
||||
# vessel is only moving fast for the instant it arrives, and by the time a
|
||||
# 0.7s tick came round it had already braked to nothing. Measured: a ship sent
|
||||
# in at 900 did no damage at all until this moved to entry.
|
||||
#
|
||||
# Below the threshold it is docking, not a collision. The fleet sits on its own
|
||||
# mothership constantly to bank ore, and if resting there hurt, the economy
|
||||
# loop would grind the player's own buildings down.
|
||||
func _on_area_entered(area: Area2D) -> void:
|
||||
if not is_instance_valid(area) or not area.is_in_group("player_ships"):
|
||||
return
|
||||
# A vessel stationed at an outpost is not ramming it. It launches from
|
||||
# there, patrols, and comes back - repeatedly, and at speed. Only the
|
||||
# fleet can collide with a structure.
|
||||
if area.is_in_group("garrison_ships"):
|
||||
return
|
||||
var speed: float = float(area.velocity.length()) if "velocity" in area else 0.0
|
||||
if speed < RAM_SPEED:
|
||||
return
|
||||
var id: int = building_id()
|
||||
if id == 0:
|
||||
return
|
||||
# Scaled by how much faster than the threshold it was going, so a glancing
|
||||
# touch above the line is not a full-speed charge, and capped so no engine
|
||||
# upgrade turns a vessel into a wrecking ball.
|
||||
var impact: int = mini(int(round(RAM_DAMAGE * speed / RAM_SPEED)), RAM_DAMAGE_MAX)
|
||||
GameState.damage_building(id, impact)
|
||||
area.take_damage(1)
|
||||
|
||||
|
||||
func building_id() -> int:
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ func _show_building_info(type: String) -> void:
|
|||
GameState.resource_name(planet.resource_type), planet.planet_name])
|
||||
elif can_build and definition.unique and GameState.has_building_of_type(type):
|
||||
lines.append("WARNING: only one may exist - this destroys your current one.")
|
||||
if can_build and not GameState.can_afford_resources(costs):
|
||||
if can_build and not GameState.can_afford_resources(costs, fleet.get_centroid()):
|
||||
can_build = false
|
||||
pending_build_type = type if can_build else ""
|
||||
build_menu.show_info(definition.display_name, "\n\n".join(lines), costs, can_build)
|
||||
|
|
@ -416,7 +416,7 @@ func _confirm_build() -> void:
|
|||
if String(planet.resource_type) == "":
|
||||
build_menu.set_status("%s has nothing to extract" % planet.planet_name)
|
||||
return
|
||||
if not GameState.spend_resources(GameState.building_costs(type)):
|
||||
if not GameState.spend_resources(GameState.building_costs(type), fleet.get_centroid()):
|
||||
build_menu.set_status(_cannot_afford(type))
|
||||
return
|
||||
GameState.add_extractor(planet.planet_name, planet.position,
|
||||
|
|
@ -424,7 +424,7 @@ func _confirm_build() -> void:
|
|||
build_menu.close_menu()
|
||||
return
|
||||
if definition.unique and GameState.has_building_of_type(type):
|
||||
if not GameState.can_afford_resources(GameState.building_costs(type)):
|
||||
if not GameState.can_afford_resources(GameState.building_costs(type), fleet.get_centroid()):
|
||||
build_menu.set_status(_cannot_afford(type))
|
||||
return
|
||||
GameState.remove_buildings_of_type(type)
|
||||
|
|
@ -437,14 +437,14 @@ func _cannot_afford(type: String) -> String:
|
|||
for resource_type in GameState.building_costs(type):
|
||||
var id: String = String(resource_type)
|
||||
var want: int = int(GameState.building_costs(type)[resource_type])
|
||||
var have: int = GameState.available_resource(id)
|
||||
var have: int = GameState.available_resource_at(id, fleet.get_centroid())
|
||||
if have < want:
|
||||
missing.append("%s (need %d, have %d)" % [GameState.resource_name(id), want, have])
|
||||
return "" if missing.is_empty() else "Not enough %s" % " and ".join(missing)
|
||||
|
||||
|
||||
func _try_place(type: String) -> void:
|
||||
if not GameState.spend_resources(GameState.building_costs(type)):
|
||||
if not GameState.spend_resources(GameState.building_costs(type), fleet.get_centroid()):
|
||||
build_menu.set_status(_cannot_afford(type))
|
||||
return
|
||||
building_field.place(type, fleet.get_centroid())
|
||||
|
|
@ -452,7 +452,12 @@ func _try_place(type: String) -> void:
|
|||
|
||||
|
||||
func _on_unlock_requested(id: String) -> void:
|
||||
if not GameState.unlock(id):
|
||||
var research_building: Dictionary = GameState.find_building(skill_menu.building_id)
|
||||
if research_building.is_empty():
|
||||
skill_menu.set_status("The research building is no longer available")
|
||||
return
|
||||
var payment_position: Variant = research_building.get("position", null)
|
||||
if not GameState.unlock(id, payment_position):
|
||||
skill_menu.refresh(id)
|
||||
return
|
||||
if int(GameState.skill_node(id).get("fleet_bonus", 0)) > 0:
|
||||
|
|
|
|||
66
controllers/DashController.gd
Normal file
66
controllers/DashController.gd
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
class_name DashController
|
||||
extends "res://controllers/AimedFleetAbilityController.gd"
|
||||
|
||||
# Energy Propulser active ability (#179): hold, aim a direction, release, and
|
||||
# the complete fleet surges along that heading. Cooldown belongs to the
|
||||
# particular Propulser selected by the shared ability controller, so adding a
|
||||
# second hull adds a second independently recharging dash.
|
||||
|
||||
const DASH_COLOR := Color(0.85, 0.95, 0.15)
|
||||
const DASH_DISTANCE: float = 520.0
|
||||
const ENERGY_PER_DASH: int = 15
|
||||
const TRAIL_SECONDS: float = 0.45
|
||||
|
||||
var trail_from: Vector2 = Vector2.ZERO
|
||||
var trail_time: float = 0.0
|
||||
|
||||
|
||||
func _button_path() -> NodePath: return "../UI/DashButton"
|
||||
func _status_path() -> NodePath: return "../UI/DashStatus"
|
||||
func _component_id() -> String: return "distorder"
|
||||
func _ammo_resource() -> String: return GameState.RESOURCE_ENERGY
|
||||
func _ammo_cost() -> int: return ENERGY_PER_DASH
|
||||
func _aim_padding() -> float: return 36.0
|
||||
func _aim_max_range() -> float: return DASH_DISTANCE
|
||||
func _has_effect_visuals() -> bool: return trail_time > 0.0 or fleet.is_dashing()
|
||||
|
||||
|
||||
func _calculate_aim_target(drag: Vector2, origin: Vector2) -> Vector2:
|
||||
var direction: Vector2 = drag.normalized() if drag.length() > 0.001 else Vector2.UP
|
||||
return origin + direction * DASH_DISTANCE
|
||||
|
||||
|
||||
func _consume_shot(component: Node) -> bool:
|
||||
return component.consume_dash()
|
||||
|
||||
|
||||
func _perform_shot(_ship: Node2D, _component: Node) -> void:
|
||||
var direction: Vector2 = aim_target - _aim_origin()
|
||||
if direction.length() < 0.001:
|
||||
return
|
||||
trail_from = fleet.get_centroid()
|
||||
trail_time = TRAIL_SECONDS
|
||||
fleet.dash(direction, DASH_DISTANCE)
|
||||
|
||||
|
||||
func _advance_effects(delta: float) -> void:
|
||||
trail_time = maxf(trail_time - delta, 0.0)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if aiming:
|
||||
var origin: Vector2 = _aim_origin()
|
||||
NeonDraw.draw_glowing_line(self, origin, aim_target, DASH_COLOR, 0.65, 0.65)
|
||||
var direction: Vector2 = (aim_target - origin).normalized()
|
||||
var side: Vector2 = direction.orthogonal() * 18.0
|
||||
NeonDraw.draw_glowing_path(self, PackedVector2Array([
|
||||
aim_target - direction * 38.0 + side, aim_target,
|
||||
aim_target - direction * 38.0 - side,
|
||||
]), DASH_COLOR, 0.9, 0.55)
|
||||
if trail_time > 0.0:
|
||||
var alpha: float = trail_time / TRAIL_SECONDS
|
||||
var now: Vector2 = fleet.get_centroid()
|
||||
for offset in [-20.0, 0.0, 20.0]:
|
||||
var across: Vector2 = (now - trail_from).normalized().orthogonal() * offset
|
||||
NeonDraw.draw_glowing_line(self, trail_from + across, now + across,
|
||||
DASH_COLOR, alpha * 0.6, 0.35)
|
||||
1
controllers/DashController.gd.uid
Normal file
1
controllers/DashController.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bk38ws0glrkyk
|
||||
|
|
@ -31,6 +31,7 @@ extends Node2D
|
|||
# indirectly - e.g. Main.tscn loaded from inside a _screenshot_tool.gd
|
||||
# preview - for a class_name this new. Node + duck-typed calls sidesteps it.
|
||||
@onready var laser_controller: Node = get_node_or_null("LaserController")
|
||||
@onready var dash_controller: Node = get_node_or_null("DashController")
|
||||
@onready var speed_fader: Node = get_node_or_null("UI/SpeedFader")
|
||||
|
||||
var touches: Dictionary = {}
|
||||
|
|
@ -367,6 +368,8 @@ func _handle_touch(event: InputEventScreenTouch) -> void:
|
|||
return
|
||||
if laser_controller != null and laser_controller.handle_touch(event):
|
||||
return
|
||||
if dash_controller != null and dash_controller.handle_touch(event):
|
||||
return
|
||||
if speed_fader != null and speed_fader.handle_touch(event):
|
||||
return
|
||||
if _handle_pivot_touch(event):
|
||||
|
|
@ -399,6 +402,8 @@ func _handle_drag(event: InputEventScreenDrag) -> void:
|
|||
return
|
||||
if laser_controller != null and laser_controller.handle_drag(event):
|
||||
return
|
||||
if dash_controller != null and dash_controller.handle_drag(event):
|
||||
return
|
||||
if speed_fader != null and speed_fader.handle_drag(event):
|
||||
return
|
||||
if joystick != null and joystick.visible and joystick.handle_drag(event):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ extends Node2D
|
|||
# A literal, not PackedStringArray(...): a constructor call is not a constant
|
||||
# expression, and the script fails to PARSE rather than to run - which shows up
|
||||
# as "nonexistent function" on a node that looks fine in the scene.
|
||||
const ABILITY_ORDER: PackedStringArray = ["GasBombController", "LaserController"]
|
||||
const ABILITY_ORDER: PackedStringArray = ["GasBombController", "LaserController", "DashController"]
|
||||
|
||||
@onready var fleet: Node2D = get_node("../Fleet")
|
||||
@onready var camera: Camera2D = get_node("../Camera2D")
|
||||
|
|
|
|||
|
|
@ -657,10 +657,10 @@ func skill_costs(id: String) -> Dictionary:
|
|||
return {RESOURCE_ORE: int(node.cost)} if int(node.cost) > 0 else {}
|
||||
|
||||
|
||||
func can_unlock(id: String) -> bool:
|
||||
func can_unlock(id: String, payment_position: Variant = null) -> bool:
|
||||
var node: Dictionary = skill_node(id)
|
||||
return not is_unlocked(id) and prerequisite_met(id) \
|
||||
and can_afford_resources(skill_costs(id)) \
|
||||
and can_afford_resources(skill_costs(id), payment_position) \
|
||||
and research_points >= skill_research_point_cost(id)
|
||||
|
||||
|
||||
|
|
@ -668,11 +668,11 @@ func skill_research_point_cost(id: String) -> int:
|
|||
return int(skill_node(id).get("research_points", 0))
|
||||
|
||||
|
||||
func unlock(id: String) -> bool:
|
||||
if not can_unlock(id):
|
||||
func unlock(id: String, payment_position: Variant = null) -> bool:
|
||||
if not can_unlock(id, payment_position):
|
||||
return false
|
||||
var node: Dictionary = skill_node(id)
|
||||
if not spend_resources(skill_costs(id)):
|
||||
if not spend_resources(skill_costs(id), payment_position):
|
||||
return false
|
||||
var points_needed: int = skill_research_point_cost(id)
|
||||
if points_needed > 0:
|
||||
|
|
@ -1314,13 +1314,13 @@ func vessel_for_unlock(unlock_id: String) -> Resource:
|
|||
return null
|
||||
|
||||
|
||||
func unlock_vessel(vessel: String) -> bool:
|
||||
func unlock_vessel(vessel: String, payment_position: Variant = null) -> bool:
|
||||
var definition: Resource = vessel_definition(vessel)
|
||||
if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab():
|
||||
return false
|
||||
var costs: Dictionary = research_costs(definition.unlock_id)
|
||||
var points_needed: int = int(definition.research_point_cost)
|
||||
if research_points < points_needed or not spend_resources(costs):
|
||||
if research_points < points_needed or not spend_resources(costs, payment_position):
|
||||
return false
|
||||
if points_needed > 0:
|
||||
research_points -= points_needed
|
||||
|
|
@ -1370,15 +1370,16 @@ func upgrade_cost(id: String) -> int:
|
|||
return int(d.cost) * (upgrade_level(id) + 1)
|
||||
|
||||
|
||||
func can_buy_upgrade(id: String) -> bool:
|
||||
return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id)) \
|
||||
func can_buy_upgrade(id: String, payment_position: Variant = null) -> bool:
|
||||
return has_ship_lab() and not upgrade_maxed(id) \
|
||||
and can_afford(upgrade_cost(id), payment_position) \
|
||||
and research_points >= research_point_cost(id)
|
||||
|
||||
|
||||
func buy_upgrade(id: String) -> bool:
|
||||
if not can_buy_upgrade(id):
|
||||
func buy_upgrade(id: String, payment_position: Variant = null) -> bool:
|
||||
if not can_buy_upgrade(id, payment_position):
|
||||
return false
|
||||
if not spend(upgrade_cost(id)):
|
||||
if not spend(upgrade_cost(id), payment_position):
|
||||
return false
|
||||
var points_needed: int = research_point_cost(id)
|
||||
if points_needed > 0:
|
||||
|
|
@ -1601,7 +1602,12 @@ const LOCAL_PAY_RANGE: float = 150.0
|
|||
func fleet_is_at(building: Dictionary) -> bool:
|
||||
if building.is_empty():
|
||||
return false
|
||||
return fleet_position.distance_to(building.position) <= LOCAL_PAY_RANGE
|
||||
return fleet_can_pay_at(building.position)
|
||||
|
||||
|
||||
func fleet_can_pay_at(payment_position: Variant) -> bool:
|
||||
return payment_position is Vector2 \
|
||||
and fleet_position.distance_to(payment_position as Vector2) <= LOCAL_PAY_RANGE
|
||||
|
||||
|
||||
func local_payment_available(building: Dictionary, inventory: RefCounted,
|
||||
|
|
@ -3332,10 +3338,10 @@ func _advance_quests() -> void:
|
|||
|
||||
# --- paying for things ------------------------------------------------------
|
||||
#
|
||||
# EVERY payment draws on the bank AND on what the ships are carrying. The
|
||||
# player can already move resources between the two by hand at any building
|
||||
# that takes them, so refusing a purchase that a couple of taps would have
|
||||
# made affordable was pure ceremony.
|
||||
# Payments draw on the bank and may also draw on what the ships are carrying
|
||||
# when the caller supplies the concrete place being paid. The player can move
|
||||
# resources between the two by hand, so using nearby cargo avoids ceremony;
|
||||
# #180 makes "nearby" part of the API instead of an assumption in the caller.
|
||||
#
|
||||
# In one case it was worse than ceremony. A fresh game starts with an empty
|
||||
# bank, ore is mined into the fleet hold, and the only way to bank it is to
|
||||
|
|
@ -3343,9 +3349,8 @@ func _advance_quests() -> void:
|
|||
# made the first mothership cost ore that could not yet exist. Mine forever,
|
||||
# build nothing.
|
||||
#
|
||||
# Bank first, hold second. The hold is also the fleet's mining capacity, so
|
||||
# leaving it as full as possible means the next trip out can still carry
|
||||
# something back.
|
||||
# Bank first, nearby hold second. A call with no position is deliberately
|
||||
# bank-only: missing context must fail safe instead of teleporting cargo.
|
||||
#
|
||||
# The one deliberate exception is AMMO - see AimedFleetAbilityController,
|
||||
# which checks fleet_resource() directly. A gas bomb is fired from what the
|
||||
|
|
@ -3357,36 +3362,50 @@ func available_resource(resource_type: String) -> int:
|
|||
return bank_inventory.amount(resource_type) + fleet_inventory.amount(resource_type)
|
||||
|
||||
|
||||
# What can pay at one concrete place. Banked resources remain globally
|
||||
# available; cargo in the fleet hold joins them only when the fleet is close
|
||||
# enough to physically hand it to the building or construction site (#180).
|
||||
# A null position deliberately means "no local payer", so a forgotten context
|
||||
# can never reach into the hold from across the map.
|
||||
func available_resource_at(resource_type: String, payment_position: Variant = null) -> int:
|
||||
var amount: int = bank_inventory.amount(resource_type)
|
||||
if fleet_can_pay_at(payment_position):
|
||||
amount += fleet_inventory.amount(resource_type)
|
||||
return amount
|
||||
|
||||
|
||||
func available_ore() -> int:
|
||||
return available_resource(RESOURCE_ORE)
|
||||
|
||||
|
||||
func can_afford(cost: int) -> bool:
|
||||
return available_ore() >= cost
|
||||
func can_afford(cost: int, payment_position: Variant = null) -> bool:
|
||||
return available_resource_at(RESOURCE_ORE, payment_position) >= cost
|
||||
|
||||
|
||||
func can_afford_resources(costs: Dictionary) -> bool:
|
||||
func can_afford_resources(costs: Dictionary, payment_position: Variant = null) -> bool:
|
||||
for resource_type in costs:
|
||||
if available_resource(String(resource_type)) < int(costs[resource_type]):
|
||||
if available_resource_at(String(resource_type), payment_position) \
|
||||
< int(costs[resource_type]):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func spend(cost: int) -> bool:
|
||||
return spend_resources({RESOURCE_ORE: cost})
|
||||
func spend(cost: int, payment_position: Variant = null) -> bool:
|
||||
return spend_resources({RESOURCE_ORE: cost}, payment_position)
|
||||
|
||||
|
||||
func spend_resources(costs: Dictionary) -> bool:
|
||||
func spend_resources(costs: Dictionary, payment_position: Variant = null) -> bool:
|
||||
# Affordability is settled in full BEFORE anything is taken. Charging for
|
||||
# part of a purchase that then fails is worse than refusing it, and with
|
||||
# several resource types in one price that is a real possibility rather
|
||||
# than a theoretical one.
|
||||
if not can_afford_resources(costs):
|
||||
if not can_afford_resources(costs, payment_position):
|
||||
return false
|
||||
var fleet_can_pay: bool = fleet_can_pay_at(payment_position)
|
||||
for resource_type in costs:
|
||||
var id: String = String(resource_type)
|
||||
var owed: int = int(costs[resource_type])
|
||||
owed -= bank_inventory.withdraw(id, owed)
|
||||
if owed > 0:
|
||||
if owed > 0 and fleet_can_pay:
|
||||
fleet_inventory.withdraw(id, owed)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ enum State { NEUTRAL, BLOCKED, CAPTURING, CAPTURED }
|
|||
|
||||
@export var point_index: int = 0
|
||||
@export var capture_radius: float = 220.0
|
||||
@export var capture_time: float = 8.0
|
||||
# #177: holding an already-cleared point should resolve briskly. Eight seconds
|
||||
# made every point feel like waiting after the fight was over.
|
||||
@export var capture_time: float = 4.0
|
||||
@export var enemy_clear_radius: float = 700.0
|
||||
@export var drain_rate: float = 0.5
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=19 format=3]
|
||||
[gd_scene load_steps=20 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://controllers/GameController.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://surface/SurfaceBackground.tscn" id="3"]
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
[ext_resource type="Script" path="res://controllers/FireStickController.gd" id="fire_ctrl"]
|
||||
[ext_resource type="Script" path="res://controllers/GasBombController.gd" id="14"]
|
||||
[ext_resource type="Script" path="res://controllers/LaserController.gd" id="16"]
|
||||
[ext_resource type="Script" path="res://controllers/DashController.gd" id="dash_ctrl"]
|
||||
[ext_resource type="Script" path="res://ui/SpeedFader.gd" id="17"]
|
||||
[ext_resource type="Script" path="res://surface/CaptureLabel.gd" id="15"]
|
||||
[ext_resource type="Script" path="res://ui/NarrationBanner.gd" id="banner"]
|
||||
|
|
@ -39,6 +40,9 @@ script = ExtResource("mouse_ctrl")
|
|||
[node name="LaserController" type="Node2D" parent="."]
|
||||
script = ExtResource("16")
|
||||
|
||||
[node name="DashController" type="Node2D" parent="."]
|
||||
script = ExtResource("dash_ctrl")
|
||||
|
||||
[node name="Camera2D" type="Camera2D" parent="."]
|
||||
position = Vector2(0, 0)
|
||||
zoom = Vector2(1, 1)
|
||||
|
|
@ -205,8 +209,25 @@ anchor_top = 1.0
|
|||
anchor_bottom = 1.0
|
||||
offset_left = -196
|
||||
offset_right = -100
|
||||
offset_top = -492
|
||||
offset_bottom = -396
|
||||
offset_top = -548
|
||||
offset_bottom = -452
|
||||
|
||||
[node name="DashButton" type="Control" parent="UI"]
|
||||
visible = false
|
||||
mouse_filter = 2
|
||||
script = ExtResource("6")
|
||||
round_frame = true
|
||||
icon = 45
|
||||
glow_color = Color(0.54, 0.62, 0.05, 1)
|
||||
core_color = Color(0.02, 0.09, 0.15, 1)
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -452
|
||||
offset_right = -356
|
||||
offset_top = -548
|
||||
offset_bottom = -452
|
||||
|
||||
[node name="PivotCCWButton" type="Control" parent="UI"]
|
||||
mouse_filter = 2
|
||||
|
|
@ -219,8 +240,8 @@ anchor_left = 1.0
|
|||
anchor_right = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -468
|
||||
offset_right = -364
|
||||
offset_left = -620
|
||||
offset_right = -516
|
||||
offset_top = -130
|
||||
offset_bottom = -26
|
||||
|
||||
|
|
@ -238,8 +259,8 @@ anchor_top = 1.0
|
|||
anchor_bottom = 1.0
|
||||
offset_left = -324
|
||||
offset_right = -228
|
||||
offset_top = -424
|
||||
offset_bottom = -328
|
||||
offset_top = -548
|
||||
offset_bottom = -452
|
||||
|
||||
[node name="GasBombStatus" type="Label" parent="UI"]
|
||||
mouse_filter = 2
|
||||
|
|
@ -273,6 +294,24 @@ offset_right = -24
|
|||
offset_top = -616
|
||||
offset_bottom = -568
|
||||
|
||||
[node name="DashStatus" type="Label" parent="UI"]
|
||||
mouse_filter = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_colors/font_color = Color(0.54, 0.62, 0.05, 1)
|
||||
theme_override_colors/font_outline_color = Color(0.95, 0.95, 0.96, 0.85)
|
||||
theme_override_constants/outline_size = 4
|
||||
autowrap_mode = 2
|
||||
horizontal_alignment = 1
|
||||
text = ""
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -500
|
||||
offset_right = -104
|
||||
offset_top = -672
|
||||
offset_bottom = -624
|
||||
|
||||
[node name="PivotCWButton" type="Control" parent="UI"]
|
||||
mouse_filter = 2
|
||||
script = ExtResource("6")
|
||||
|
|
@ -284,8 +323,8 @@ anchor_left = 1.0
|
|||
anchor_right = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -356
|
||||
offset_right = -252
|
||||
offset_left = -508
|
||||
offset_right = -404
|
||||
offset_top = -130
|
||||
offset_bottom = -26
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ func _ready() -> void:
|
|||
GameState.extractors.clear()
|
||||
GameState.vessel_levels["unlock_laser"] = 1
|
||||
GameState.vessel_levels["unlock_gas_bomber"] = 1
|
||||
GameState.vessel_levels["unlock_distorder"] = 1
|
||||
GameState.unlocked.append("wing")
|
||||
GameState.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 200)
|
||||
GameState.deposit_fleet_resource(GameState.RESOURCE_GAS, 200)
|
||||
var main: Node = load("res://world/Main.tscn").instantiate()
|
||||
|
|
@ -18,10 +20,12 @@ 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([
|
||||
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_LASER, GameState.VESSEL_GAS_BOMBER]))
|
||||
fleet.spawn_ships(4, Vector2.ZERO, PackedStringArray([
|
||||
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_LASER,
|
||||
GameState.VESSEL_GAS_BOMBER, GameState.VESSEL_DISTORDER]))
|
||||
main.get_node("LaserController")._process(0.0)
|
||||
main.get_node("GasBombController")._process(0.0)
|
||||
main.get_node("DashController")._process(0.0)
|
||||
# Push the fire stick, so its knob shows deflected the way it looks in use.
|
||||
var stick: Control = main.get_node("UI/FireStick")
|
||||
var press := InputEventScreenTouch.new()
|
||||
|
|
|
|||
|
|
@ -2,19 +2,18 @@ extends SceneTree
|
|||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_building_collateral.gd
|
||||
# Forgejo #165: a gas bomb and a vessel flying fast enough both damage a
|
||||
# structure. The blast used to stop politely at anything the player owned,
|
||||
# which made a bomb dropped on a besieged mothership a free way to sweep the
|
||||
# attackers off it.
|
||||
# Forgejo #176: the player's own ships and their weapons do not damage the
|
||||
# player's buildings. Friendly fire on structures made defending them with a
|
||||
# gas bomb or crossing them at speed punish ordinary play.
|
||||
#
|
||||
# Covers:
|
||||
# A. a bomb landing on a structure damages it
|
||||
# A. a bomb landing on a structure leaves it unharmed
|
||||
# B. a bomb landing clear of it does not - the radius is the radius, and
|
||||
# this is a weapon that has to be aimed
|
||||
# C. a vessel resting against a structure does NOT damage it. The fleet
|
||||
# sits on its own mothership constantly to bank ore; if that hurt, the
|
||||
# game's own economy loop would grind its buildings down
|
||||
# D. a vessel arriving fast does
|
||||
# D. a vessel arriving fast still does not damage it or itself
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
|
|
@ -23,6 +22,7 @@ var building_id: int = 0
|
|||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var hp_before: int = 0
|
||||
var ships_before: int = 0
|
||||
const BUILDING_AT := Vector2(1200.0, 0.0)
|
||||
|
||||
|
||||
|
|
@ -78,11 +78,11 @@ func _process(delta: float) -> bool:
|
|||
return _fail("a bomb 900 away still damaged the structure")
|
||||
print("a bomb clear of the structure leaves it alone")
|
||||
|
||||
# --- A. a bomb on top of it does --------------------------------
|
||||
# --- A. a friendly bomb on top of it is safe ----------------------
|
||||
_detonate_at(BUILDING_AT)
|
||||
if _hp() >= hp_before:
|
||||
return _fail("a bomb landing on the structure did not damage it (hp %d)" % _hp())
|
||||
print("a bomb on the structure took it from %d to %d" % [hp_before, _hp()])
|
||||
if _hp() != hp_before:
|
||||
return _fail("a friendly bomb damaged the structure (%d -> %d)" % [hp_before, _hp()])
|
||||
print("a friendly bomb on the structure leaves it at %d hp" % _hp())
|
||||
|
||||
# --- C. resting against it is free ------------------------------
|
||||
hp_before = _hp()
|
||||
|
|
@ -104,8 +104,9 @@ func _process(delta: float) -> bool:
|
|||
% [hp_before, _hp()])
|
||||
print("a vessel parked on the structure for %.1fs did nothing to it" % elapsed)
|
||||
|
||||
# --- D. arriving fast does --------------------------------------
|
||||
# --- D. arriving fast is still friendly -------------------------
|
||||
hp_before = _hp()
|
||||
ships_before = fleet.ships.size()
|
||||
for ship in fleet.ships:
|
||||
# Placed clear and sent IN, because the impact is detected on
|
||||
# entry - a ship spawned already overlapping never enters.
|
||||
|
|
@ -118,9 +119,11 @@ func _process(delta: float) -> bool:
|
|||
3:
|
||||
if elapsed < 1.2:
|
||||
return false
|
||||
if _hp() >= hp_before:
|
||||
return _fail("a vessel arriving at 900 did not damage the structure")
|
||||
print("a vessel arriving fast took it from %d to %d" % [hp_before, _hp()])
|
||||
if _hp() != hp_before:
|
||||
return _fail("a fast friendly vessel damaged the structure (%d -> %d)" % [hp_before, _hp()])
|
||||
if fleet.ships.size() != ships_before:
|
||||
return _fail("a friendly building destroyed a vessel crossing it")
|
||||
print("a vessel crossing the structure at speed leaves both unharmed")
|
||||
|
||||
print("TEST_BUILDING_COLLATERAL: PASS")
|
||||
quit(0)
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ func _process(delta: float) -> bool:
|
|||
|
||||
point = load("res://surface/CapturePoint.tscn").instantiate()
|
||||
point.point_index = 0
|
||||
if point.capture_time > 4.0:
|
||||
return _fail("default capture still takes %.1fs" % point.capture_time)
|
||||
point.capture_time = 1.0
|
||||
point.capture_radius = 200.0
|
||||
point.enemy_clear_radius = 300.0
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ extends SceneTree
|
|||
# E. it types while the game is stopped, being the one node still running
|
||||
# F. the sci-fi part: the panel is drawn differently while decoding, and
|
||||
# settles when the line is complete
|
||||
# G. #168 - a KEY does exactly what a click does, both halves of it, since
|
||||
# the game is played on a desktop with WASD and a mouse and pressing
|
||||
# space at a dialog reached nothing at all. Modifiers alone and held-key
|
||||
# echoes are not presses, or a line would be run out by a held key.
|
||||
# G. #175 - keyboard presses do not finish or dismiss a dialog; only a
|
||||
# deliberate tap/click advances it
|
||||
# H. #178 - revealed text is seated at the left edge rather than growing
|
||||
# outward from the middle
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
|
|
@ -90,6 +90,8 @@ func _process(delta: float) -> bool:
|
|||
return _fail("the label holds a partial string - the line will re-wrap as it types")
|
||||
if narration.label.visible_characters < 0:
|
||||
return _fail("the whole line is visible already")
|
||||
if narration.label.horizontal_alignment != HORIZONTAL_ALIGNMENT_LEFT:
|
||||
return _fail("dialog text is not left-aligned - partial text will grow from the middle")
|
||||
seen.append(narration.label.visible_characters)
|
||||
|
||||
# Hand-ticked, so the reveal is measured rather than raced. The
|
||||
|
|
@ -151,37 +153,34 @@ func _process(delta: float) -> bool:
|
|||
return _fail("dismissing landed on \"%s\"" % narration.current)
|
||||
print("the press after that moves on to \"%s\"" % narration.current)
|
||||
|
||||
# --- G. #168: a key is a press too -----------------------
|
||||
# --- G. #175: keys never advance a dialog ----------------
|
||||
narration.clear()
|
||||
narration.say(first_line, "TEST")
|
||||
narration.say("Second.", "TEST")
|
||||
if not narration.is_typing():
|
||||
return _fail("the fresh line was not arriving")
|
||||
# Not everything that reaches the banner is a press.
|
||||
_key(KEY_SHIFT)
|
||||
_key(KEY_SPACE, true)
|
||||
if not narration.is_typing():
|
||||
return _fail("a modifier or a held-key echo finished the line")
|
||||
# Still arriving: the key shows the whole of it, and no more.
|
||||
_key(KEY_SPACE)
|
||||
if narration.is_typing():
|
||||
return _fail("a key did not finish a line that was still arriving")
|
||||
if narration.current != first_line:
|
||||
return _fail("a key on an arriving line dismissed it - the next line would be skipped unread")
|
||||
# Whole: the key moves on, the same as a click.
|
||||
_key(KEY_SPACE)
|
||||
if narration.current != "Second.":
|
||||
return _fail("a key on a finished line landed on \"%s\"" % narration.current)
|
||||
# Any key, not one chosen key - the player has no reason to guess.
|
||||
_key(KEY_W)
|
||||
if narration.is_typing():
|
||||
return _fail("only some keys finish a line")
|
||||
_key(KEY_ENTER)
|
||||
if not narration.is_typing() or narration.current != first_line:
|
||||
return _fail("a keyboard press changed a dialog that was still arriving")
|
||||
narration.finish_typing()
|
||||
_key(KEY_W)
|
||||
_key(KEY_ENTER)
|
||||
if narration.current != first_line or narration.is_typing():
|
||||
return _fail("a keyboard press dismissed a complete dialog")
|
||||
_click()
|
||||
if narration.current != "Second.":
|
||||
return _fail("clicking after ignored keys did not reach the second line")
|
||||
narration.finish_typing()
|
||||
_click()
|
||||
if narration.is_speaking():
|
||||
return _fail("a key on the last line did not close the dialog")
|
||||
return _fail("clicking the last complete line did not close the dialog")
|
||||
if paused:
|
||||
return _fail("keying through every line left the game stopped")
|
||||
print("space, W and enter all finish then advance a line; shift and echoes do not")
|
||||
return _fail("clicking through every line left the game stopped")
|
||||
print("space, W and enter leave dialogs untouched; clicks still finish and advance")
|
||||
|
||||
print("TEST_DIALOG_TYPING: PASS")
|
||||
quit(0)
|
||||
|
|
|
|||
137
tests/test_energy_propulser_dash.gd
Normal file
137
tests/test_energy_propulser_dash.gd
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
extends SceneTree
|
||||
|
||||
# Forgejo #179: an Energy Propulser in the fleet grants a hold-aim-release
|
||||
# dash. Covers visibility, aimed movement, energy cost, formation integrity,
|
||||
# and one cooldown per Propulser.
|
||||
|
||||
const DASH_COST: int = 15
|
||||
const DASH_DISTANCE: float = 520.0
|
||||
|
||||
var state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var dash: Node
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var first_origin: Vector2
|
||||
var second_origin: Vector2
|
||||
var offsets_before: Array = []
|
||||
var first_component: Node
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
state = root.get_node("GameState")
|
||||
state.reset_new_game()
|
||||
state.intro_active = false
|
||||
state.reset_quests()
|
||||
state.vessel_levels["unlock_distorder"] = 1
|
||||
state.fleet_inventory.total_capacity = -1
|
||||
state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, DASH_COST * 3)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
fleet = main.get_node("Fleet")
|
||||
dash = main.get_node("DashController")
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
if main.get_node("UI/DashButton").visible:
|
||||
return _fail("dash button appeared without an Energy Propulser")
|
||||
# The carrier keeps enough hold capacity for both dashes. Two
|
||||
# Propulsers prove that cooldown belongs to a vessel, not the fleet.
|
||||
fleet.spawn_ships(3, Vector2.ZERO, PackedStringArray([
|
||||
GameState.VESSEL_CARRIER, GameState.VESSEL_DISTORDER,
|
||||
GameState.VESSEL_DISTORDER,
|
||||
]))
|
||||
dash._process(0.0)
|
||||
if not main.get_node("UI/DashButton").visible:
|
||||
return _fail("an Energy Propulser did not reveal the dash button")
|
||||
first_component = dash._next_ready_ship().get_component("distorder")
|
||||
first_origin = fleet.get_centroid()
|
||||
for ship in fleet.ships:
|
||||
offsets_before.append(ship.position - first_origin)
|
||||
_aim_and_release(Vector2.RIGHT, 11)
|
||||
if not fleet.is_dashing():
|
||||
return _fail("releasing a rightward aim did not begin a dash")
|
||||
if state.fleet_resource(GameState.RESOURCE_ENERGY) != DASH_COST * 2:
|
||||
return _fail("first dash did not consume %d energy" % DASH_COST)
|
||||
if first_component.is_ready():
|
||||
return _fail("the Propulser that dashed did not enter cooldown")
|
||||
dash._process(0.0)
|
||||
if not main.get_node("UI/DashButton").enabled:
|
||||
return _fail("one cooling Propulser disabled a second ready one")
|
||||
stage = 1
|
||||
elapsed = 0.0
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.7:
|
||||
return false
|
||||
var moved: Vector2 = fleet.get_centroid() - first_origin
|
||||
if absf(moved.x - DASH_DISTANCE) > 8.0 or absf(moved.y) > 8.0:
|
||||
return _fail("rightward aim moved the fleet by %s" % moved)
|
||||
for i in range(fleet.ships.size()):
|
||||
var offset: Vector2 = fleet.ships[i].position - fleet.get_centroid()
|
||||
if offset.distance_to(offsets_before[i]) > 1.0:
|
||||
return _fail("dash distorted formation slot %d by %.2f" \
|
||||
% [i, offset.distance_to(offsets_before[i])])
|
||||
second_origin = fleet.get_centroid()
|
||||
_aim_and_release(Vector2.UP, 12)
|
||||
if state.fleet_resource(GameState.RESOURCE_ENERGY) != DASH_COST:
|
||||
return _fail("second Propulser did not pay for its own dash")
|
||||
dash._process(0.0)
|
||||
if main.get_node("UI/DashButton").enabled:
|
||||
return _fail("dash remained enabled after both Propulsers fired")
|
||||
stage = 2
|
||||
elapsed = 0.0
|
||||
return false
|
||||
2:
|
||||
if elapsed < 0.7:
|
||||
return false
|
||||
var moved: Vector2 = fleet.get_centroid() - second_origin
|
||||
if absf(moved.y + DASH_DISTANCE) > 8.0 or absf(moved.x) > 8.0:
|
||||
return _fail("upward aim moved the fleet by %s" % moved)
|
||||
first_component.tick(first_component.DASH_COOLDOWN)
|
||||
dash._process(0.0)
|
||||
if not main.get_node("UI/DashButton").enabled:
|
||||
return _fail("button did not re-enable when one Propulser recharged")
|
||||
var surface: Node = load("res://surface/PlanetSurface.tscn").instantiate()
|
||||
if not surface.has_node("DashController") or not surface.has_node("UI/DashButton"):
|
||||
return _fail("planet surface does not carry the dash ability")
|
||||
surface.free()
|
||||
print("two aimed dashes moved %.0f units, preserved formation, and cooled down per Propulser" \
|
||||
% DASH_DISTANCE)
|
||||
print("TEST_ENERGY_PROPULSER_DASH: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _aim_and_release(direction: Vector2, touch_index: int) -> void:
|
||||
var button: Control = main.get_node("UI/DashButton")
|
||||
var press := InputEventScreenTouch.new()
|
||||
press.index = touch_index
|
||||
press.pressed = true
|
||||
press.position = button.global_position + button.size * 0.5
|
||||
main._handle_touch(press)
|
||||
var drag := InputEventScreenDrag.new()
|
||||
drag.index = touch_index
|
||||
drag.position = press.position + direction * 240.0
|
||||
main._handle_drag(drag)
|
||||
var release := InputEventScreenTouch.new()
|
||||
release.index = touch_index
|
||||
release.pressed = false
|
||||
release.position = drag.position
|
||||
main._handle_touch(release)
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_ENERGY_PROPULSER_DASH: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -15,6 +15,8 @@ extends SceneTree
|
|||
# 5. #169 - a second press while a load is running is ignored, or an
|
||||
# impatient double tap builds the world twice (on the web a button fires
|
||||
# on PRESS, so that tap costs nothing to make)
|
||||
# 6. #174 - the opaque loading cover fills the viewport and puts its
|
||||
# activity bar at the middle of the screen
|
||||
|
||||
var menu: Node
|
||||
var game_state: Node
|
||||
|
|
@ -61,7 +63,19 @@ func _process(delta: float) -> bool:
|
|||
return _fail("the press started a load with no loading screen up")
|
||||
if current_scene != menu:
|
||||
return _fail("the scene swapped inside the press, so nothing was ever shown")
|
||||
var viewport_size: Vector2 = menu.get_viewport().get_visible_rect().size
|
||||
if menu.loading_screen.size.distance_to(viewport_size) > 1.0:
|
||||
return _fail("loading cover is %s, viewport is %s" \
|
||||
% [menu.loading_screen.size, viewport_size])
|
||||
if menu.loading_screen.BACKDROP.a < 0.999:
|
||||
return _fail("loading cover is translucent (alpha %.2f)" \
|
||||
% menu.loading_screen.BACKDROP.a)
|
||||
var bar: Rect2 = menu.loading_screen._bar_rect()
|
||||
if absf(bar.get_center().y - viewport_size.y * 0.5) > 1.0:
|
||||
return _fail("loading bar centre %.1f is not screen centre %.1f" \
|
||||
% [bar.get_center().y, viewport_size.y * 0.5])
|
||||
print("pressing an entry raises the loading screen and leaves the menu up")
|
||||
print("loading cover fills %s and centres its bar" % viewport_size)
|
||||
|
||||
# --- 5. and a second press changes nothing --------------------
|
||||
var screen: Node = menu.loading_screen
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ extends SceneTree
|
|||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_payments_use_fleet_hold.gd
|
||||
# Every payment draws on the bank AND on what the ships are carrying. The
|
||||
# player can already shuttle resources between the two by hand, so refusing a
|
||||
# purchase that a couple of taps would have made affordable was ceremony.
|
||||
# A payment draws on the bank and, only at the place being paid, what the
|
||||
# ships are carrying. Fleet cargo is physical: a building across the map may
|
||||
# not reach into it (#180).
|
||||
#
|
||||
# It was also a deadlock. A new game starts with an empty bank, ore is mined
|
||||
# into the fleet hold, and the only way to bank it is to deliver it to a
|
||||
|
|
@ -22,8 +22,10 @@ extends SceneTree
|
|||
# are physically carrying
|
||||
# F. end to end: an empty bank and ore in the hold actually gets a
|
||||
# mothership built through BuildController
|
||||
# G. no payment position, or a distant one, cannot drain the fleet hold
|
||||
|
||||
const HOLD_ORE: int = 40
|
||||
const PAYMENT_AT := Vector2(120.0, 60.0)
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
|
|
@ -37,6 +39,7 @@ func _initialize() -> void:
|
|||
game_state.reset_progression()
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.fleet_position = PAYMENT_AT
|
||||
|
||||
var cost: int = int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE])
|
||||
|
||||
|
|
@ -50,13 +53,16 @@ func _initialize() -> void:
|
|||
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) >= cost:
|
||||
_fail("the bank was not actually empty, so nothing below tests the deadlock")
|
||||
return
|
||||
if not game_state.can_afford(cost):
|
||||
if game_state.can_afford(cost):
|
||||
_fail("a location-free affordability check reached into the fleet hold")
|
||||
return
|
||||
if not game_state.can_afford(cost, PAYMENT_AT):
|
||||
_fail("a fleet holding %d ore could not afford a %d purchase" % [HOLD_ORE, cost])
|
||||
return
|
||||
print("bank 0 + hold %d can afford a %d purchase" % [HOLD_ORE, cost])
|
||||
|
||||
# --- B. bank first, hold for the shortfall -------------------------------
|
||||
if not game_state.spend(cost):
|
||||
if not game_state.spend(cost, PAYMENT_AT):
|
||||
_fail("spend refused a purchase it had just called affordable")
|
||||
return
|
||||
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 0 \
|
||||
|
|
@ -68,7 +74,7 @@ func _initialize() -> void:
|
|||
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 8)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 30)
|
||||
if not game_state.spend(20):
|
||||
if not game_state.spend(20, PAYMENT_AT):
|
||||
_fail("a 20 purchase was refused with 8 banked and 30 held")
|
||||
return
|
||||
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 0 \
|
||||
|
|
@ -82,7 +88,7 @@ func _initialize() -> void:
|
|||
# --- C. no partial charge -----------------------------------------------
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 5)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 5)
|
||||
if game_state.spend(50):
|
||||
if game_state.spend(50, PAYMENT_AT):
|
||||
_fail("a purchase costing more than everything owned was allowed")
|
||||
return
|
||||
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 5 \
|
||||
|
|
@ -93,6 +99,22 @@ func _initialize() -> void:
|
|||
return
|
||||
print("a purchase nobody can afford takes nothing")
|
||||
|
||||
# --- G. cargo does not teleport to a remote purchase ------------------
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, cost)
|
||||
game_state.fleet_position = PAYMENT_AT + Vector2(9000.0, 0.0)
|
||||
if game_state.spend(cost, PAYMENT_AT):
|
||||
_fail("a remote purchase took resources from the fleet hold")
|
||||
return
|
||||
if game_state.fleet_resource(GameState.RESOURCE_ORE) != cost:
|
||||
_fail("a refused remote payment still drained the hold")
|
||||
return
|
||||
if game_state.spend(cost):
|
||||
_fail("a purchase with no location reached into the fleet hold")
|
||||
return
|
||||
game_state.fleet_position = PAYMENT_AT
|
||||
print("distant and location-free payments leave fleet cargo untouched")
|
||||
|
||||
# The hold has a real capacity (one starting interceptor carries 20), and
|
||||
# set_amount() is clamped by it - which silently gave the checks below
|
||||
# less than they asked for and made an affordable price look unaffordable.
|
||||
|
|
@ -125,12 +147,12 @@ func _check_multi_resource_atomicity() -> bool:
|
|||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 10)
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 1)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_GAS, 1)
|
||||
if game_state.spend_resources({GameState.RESOURCE_ORE: 15, GameState.RESOURCE_GAS: 40}):
|
||||
if game_state.spend_resources({GameState.RESOURCE_ORE: 15, GameState.RESOURCE_GAS: 40}, PAYMENT_AT):
|
||||
return _fail("a two-resource price was charged with only one of them affordable")
|
||||
if game_state.available_ore() != 20 or game_state.available_resource(GameState.RESOURCE_GAS) != 2:
|
||||
return _fail("a refused two-resource purchase still took some ore or gas (ore %d, gas %d)" \
|
||||
% [game_state.available_ore(), game_state.available_resource(GameState.RESOURCE_GAS)])
|
||||
if not game_state.spend_resources({GameState.RESOURCE_ORE: 15, GameState.RESOURCE_GAS: 2}):
|
||||
if not game_state.spend_resources({GameState.RESOURCE_ORE: 15, GameState.RESOURCE_GAS: 2}, PAYMENT_AT):
|
||||
return _fail("an affordable two-resource price was refused")
|
||||
if game_state.available_ore() != 5 or game_state.available_resource(GameState.RESOURCE_GAS) != 0:
|
||||
return _fail("a two-resource purchase charged wrongly (ore %d left, gas %d left)" \
|
||||
|
|
@ -147,9 +169,9 @@ func _check_skill_tree() -> bool:
|
|||
game_state.research_points = 999
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, cost)
|
||||
if not game_state.can_unlock(id):
|
||||
if not game_state.can_unlock(id, PAYMENT_AT):
|
||||
return _fail("the skill tree refused '%s' (cost %d) with the whole price in the fleet hold" % [id, cost])
|
||||
if not game_state.unlock(id):
|
||||
if not game_state.unlock(id, PAYMENT_AT):
|
||||
return _fail("unlocking '%s' failed despite can_unlock saying yes" % id)
|
||||
if game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != 0:
|
||||
return _fail("the skill unlock did not charge the hold: %d left of %d" \
|
||||
|
|
@ -177,9 +199,9 @@ func _check_ship_lab() -> bool:
|
|||
game_state.research_points = 999
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, cost)
|
||||
if not game_state.can_buy_upgrade(id):
|
||||
if not game_state.can_buy_upgrade(id, PAYMENT_AT):
|
||||
return _fail("the ship lab refused upgrade '%s' (cost %d) with the whole price in the hold" % [id, cost])
|
||||
if not game_state.buy_upgrade(id):
|
||||
if not game_state.buy_upgrade(id, PAYMENT_AT):
|
||||
return _fail("buying '%s' failed despite can_buy_upgrade saying yes" % id)
|
||||
if game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != 0:
|
||||
return _fail("the upgrade did not charge the hold: %d left of %d" \
|
||||
|
|
@ -206,7 +228,7 @@ func _check_vessel_research() -> bool:
|
|||
for resource_type in costs:
|
||||
game_state.bank_inventory.set_amount(String(resource_type), 0)
|
||||
game_state.fleet_inventory.set_amount(String(resource_type), int(costs[resource_type]))
|
||||
if not game_state.unlock_vessel(vessel):
|
||||
if not game_state.unlock_vessel(vessel, PAYMENT_AT):
|
||||
return _fail("researching '%s' was refused with its whole price (%s) in the fleet hold" % [vessel, str(costs)])
|
||||
for resource_type in costs:
|
||||
if game_state.available_resource(String(resource_type)) != 0:
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ func _initialize() -> void:
|
|||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.reset_progression()
|
||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
|
|
@ -92,9 +93,17 @@ func _process(delta: float) -> bool:
|
|||
return _fail("the research panel still has an UnlockButton node")
|
||||
print("the separate buy button is gone from the panel")
|
||||
|
||||
main.get_node("UI/SkillMenu").visible = true
|
||||
tree_view.visible = true
|
||||
tree_view.reset_scroll()
|
||||
# Open through the menu's public API so research has the mothership as its
|
||||
# payment site. Since fleet-held resources became location-aware, merely
|
||||
# forcing the Control visible no longer represents a reachable game state.
|
||||
var mothership_id: int = 0
|
||||
for building in game_state.buildings:
|
||||
if String(building.type) == GameState.BUILDING_MOTHERSHIP:
|
||||
mothership_id = int(building.id)
|
||||
break
|
||||
if mothership_id == 0:
|
||||
return _fail("the test has no mothership research site")
|
||||
main.get_node("UI/SkillMenu").open_for(mothership_id)
|
||||
game_state.unlocked.clear()
|
||||
game_state.research_points = 99
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 500)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ enum Icon {
|
|||
# inserting anywhere earlier renumbers every icon int already stored in
|
||||
# every scene.
|
||||
QUEST,
|
||||
# #179: a fleet-wide thrust burst, distinct from the Propulser vessel icon.
|
||||
DASH,
|
||||
}
|
||||
|
||||
# Size the glyphs were originally drawn against. Every glyph dimension is
|
||||
|
|
@ -319,6 +321,24 @@ func _draw() -> void:
|
|||
_draw_repair_drone_icon(center, pulse, s)
|
||||
Icon.QUEST:
|
||||
_draw_quest_icon(center, pulse, s)
|
||||
Icon.DASH:
|
||||
_draw_dash_icon(center, pulse, s)
|
||||
|
||||
|
||||
func _draw_dash_icon(center: Vector2, pulse: float, s: float) -> void:
|
||||
var tip: Vector2 = center + Vector2(12.0, 0.0) * s
|
||||
var tail: Vector2 = center + Vector2(-5.0, 0.0) * s
|
||||
NeonDraw.draw_glowing_line(self, tail, tip, _active_color(), pulse,
|
||||
0.7 * s, core_color)
|
||||
NeonDraw.draw_glowing_path(self, PackedVector2Array([
|
||||
tip + Vector2(-7.0, -7.0) * s, tip,
|
||||
tip + Vector2(-7.0, 7.0) * s,
|
||||
]), _active_color(), pulse, 0.7 * s, core_color)
|
||||
for y in [-7.0, 0.0, 7.0]:
|
||||
NeonDraw.draw_glowing_line(self,
|
||||
center + Vector2(-13.0, y) * s,
|
||||
center + Vector2(-7.0, y) * s,
|
||||
_active_color(), pulse * 0.7, 0.45 * s, core_color)
|
||||
|
||||
|
||||
# #140: a checklist - a page with a tick against the top line. Reads as
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ extends Control
|
|||
# with nothing to author, and building it here keeps MainMenu.tscn - which the
|
||||
# player's own editor touches - out of the change.
|
||||
|
||||
const BACKDROP: Color = Color(0.01, 0.02, 0.06, 0.94)
|
||||
const BACKDROP: Color = Color(0.01, 0.02, 0.06, 1.0)
|
||||
const GLOW: Color = Color(0.42, 0.86, 0.95)
|
||||
const TEXT: Color = Color(0.82, 0.94, 1.0)
|
||||
const MUTED: Color = Color(0.52, 0.66, 0.76)
|
||||
|
|
@ -33,7 +33,6 @@ const TITLE_SIZE: int = 44
|
|||
const SUBTITLE_SIZE: int = 24
|
||||
const BAR_WIDTH: float = 520.0
|
||||
const BAR_HEIGHT: float = 16.0
|
||||
const BAR_GAP: float = 46.0
|
||||
# The travelling band inside the bar, and how much of the bar it covers.
|
||||
const SWEEP_SECONDS: float = 1.2
|
||||
const SWEEP_WIDTH: float = 0.22
|
||||
|
|
@ -55,12 +54,22 @@ func _ready() -> void:
|
|||
# MainMenu refuses a second press while a load is running anyway.
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_fit_viewport()
|
||||
get_viewport().size_changed.connect(_fit_viewport)
|
||||
title_label = _make_label("LOADING", TITLE_SIZE, TEXT)
|
||||
subtitle_label = _make_label("", SUBTITLE_SIZE, MUTED)
|
||||
resized.connect(_layout)
|
||||
_layout()
|
||||
|
||||
|
||||
# A Control parent would resolve full-rect anchors for us. This node sits
|
||||
# directly under a CanvasLayer, so its first layout pass can otherwise be
|
||||
# 0x0 - exactly the frame that is supposed to cover the menu before loading.
|
||||
func _fit_viewport() -> void:
|
||||
position = Vector2.ZERO
|
||||
size = get_viewport_rect().size
|
||||
|
||||
|
||||
func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text_value
|
||||
|
|
@ -83,16 +92,17 @@ func begin(what: String) -> void:
|
|||
|
||||
func _bar_rect() -> Rect2:
|
||||
var width: float = minf(BAR_WIDTH, maxf(size.x - 120.0, 40.0))
|
||||
return Rect2(Vector2((size.x - width) * 0.5, size.y * 0.5 + BAR_GAP),
|
||||
return Rect2(Vector2((size.x - width) * 0.5, (size.y - BAR_HEIGHT) * 0.5),
|
||||
Vector2(width, BAR_HEIGHT))
|
||||
|
||||
|
||||
func _layout() -> void:
|
||||
if title_label == null:
|
||||
return
|
||||
title_label.position = Vector2(0.0, size.y * 0.5 - float(TITLE_SIZE) * 1.4)
|
||||
var bar: Rect2 = _bar_rect()
|
||||
title_label.position = Vector2(0.0, bar.position.y - float(TITLE_SIZE) * 1.8)
|
||||
title_label.size = Vector2(size.x, float(TITLE_SIZE) * 1.4)
|
||||
subtitle_label.position = Vector2(0.0, size.y * 0.5)
|
||||
subtitle_label.position = Vector2(0.0, bar.end.y + 18.0)
|
||||
subtitle_label.size = Vector2(size.x, float(SUBTITLE_SIZE) * 1.6)
|
||||
queue_redraw()
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ extends Control
|
|||
# lines never sees the second one at all, and the whole point of #127 was
|
||||
# that lines wait for the reader.
|
||||
#
|
||||
# #168 says the same rule out loud - still arriving, show it all; already
|
||||
# whole, move on - and extends it to the KEYBOARD, which had no way in at
|
||||
# all. See _is_advance_press.
|
||||
# A dialog is advanced deliberately with a tap or click. Keyboard presses are
|
||||
# ignored while it is open: movement keys are too easy to hold when a line
|
||||
# arrives, and must never turn into an accidental skip (#175).
|
||||
|
||||
signal dismissed
|
||||
|
||||
|
|
@ -139,7 +139,9 @@ func _ensure_label() -> void:
|
|||
label.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
label.add_theme_color_override("font_outline_color", Color(0.01, 0.02, 0.06, 0.95))
|
||||
label.add_theme_constant_override("outline_size", 4)
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
# #178: decoded text grows from a stable left edge. Centring a partially
|
||||
# revealed line made it appear in the middle and expand in both directions.
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
add_child(label)
|
||||
|
|
@ -268,28 +270,18 @@ func _process(delta: float) -> void:
|
|||
queue_redraw()
|
||||
|
||||
|
||||
# #168: what counts as "go on". A tap, a left click, or a key.
|
||||
# What counts as "go on": a deliberate tap or left click.
|
||||
#
|
||||
# The rule the ticket asks for was already the rule for a click (#149) - a
|
||||
# press finishes a line still arriving, a press on a finished line moves to
|
||||
# the next - but the game is played on a desktop with WASD and a mouse, and
|
||||
# a keyboard press reached nothing at all. Sitting behind a dialog pressing
|
||||
# space is the most obvious thing a player can do, so the keyboard now goes
|
||||
# through the same one door every other press does (dismiss()).
|
||||
#
|
||||
# Modifiers alone are not a press: holding shift before typing must not eat
|
||||
# a line. Neither is an auto-repeat echo - a held key would otherwise run
|
||||
# the whole queue out in a second.
|
||||
# A press finishes a line still arriving; a press on a finished line moves to
|
||||
# the next (#149). #175 deliberately limits that gesture to pointer input: the
|
||||
# player may still be holding a
|
||||
# movement key when a transmission arrives, and that must leave the line
|
||||
# untouched rather than finishing or dismissing it.
|
||||
func _is_advance_press(event: InputEvent) -> bool:
|
||||
if event is InputEventScreenTouch:
|
||||
return event.pressed
|
||||
if event is InputEventMouseButton:
|
||||
return event.pressed and event.button_index == MOUSE_BUTTON_LEFT
|
||||
if event is InputEventKey:
|
||||
if not event.pressed or event.echo:
|
||||
return false
|
||||
return not (event.keycode in [KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_META,
|
||||
KEY_CAPSLOCK, KEY_NUMLOCK, KEY_SCROLLLOCK])
|
||||
return false
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ var _entries: Array = []
|
|||
var _shown_costs: Dictionary = {}
|
||||
var _shown_points: int = 0
|
||||
var _built: bool = false
|
||||
# Optional site whose purchase this tag describes. With one, the displayed
|
||||
# "have" includes fleet cargo only while the fleet is close enough to hand it
|
||||
# over. Most tags describe construction at the fleet itself and leave this
|
||||
# null, retaining the ordinary combined inventory readout.
|
||||
var payment_position: Variant = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -89,7 +94,7 @@ func show_price(costs: Dictionary, points: int = 0) -> void:
|
|||
if amount <= 0:
|
||||
continue
|
||||
_add_entry(String(resource_type), "", amount,
|
||||
GameState.available_resource(String(resource_type)),
|
||||
_available(String(resource_type)),
|
||||
GameState.resource_color(resource_type))
|
||||
if points > 0:
|
||||
_add_entry("", POINT_MARK, points, GameState.research_points, POINT_COLOR)
|
||||
|
|
@ -102,12 +107,22 @@ func set_costs(costs: Dictionary) -> void:
|
|||
show_price(costs)
|
||||
|
||||
|
||||
func set_payment_position(at: Variant) -> void:
|
||||
payment_position = at
|
||||
_refresh_held()
|
||||
|
||||
|
||||
func _available(resource_type: String) -> int:
|
||||
return GameState.available_resource(resource_type) if payment_position == null \
|
||||
else GameState.available_resource_at(resource_type, payment_position)
|
||||
|
||||
|
||||
# What the player holds, re-read. The price does not move; the wallet does.
|
||||
func _refresh_held() -> void:
|
||||
var relayout: bool = false
|
||||
for entry in _entries:
|
||||
var have: int = GameState.research_points if String(entry.resource) == "" \
|
||||
else GameState.available_resource(String(entry.resource))
|
||||
else _available(String(entry.resource))
|
||||
if have == int(entry.have):
|
||||
continue
|
||||
entry.have = have
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ signal research_requested(id: String)
|
|||
@onready var status: Label = $StatusLabel
|
||||
@onready var resource_bar: Control = $Bar
|
||||
|
||||
var building_id: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
|
|
@ -15,13 +17,22 @@ func _ready() -> void:
|
|||
tree.node_confirmed.connect(func(id: String): research_requested.emit(id))
|
||||
|
||||
|
||||
func open_for(building_id: int) -> void:
|
||||
func open_for(next_building_id: int) -> void:
|
||||
building_id = next_building_id
|
||||
resource_bar.building_id = building_id
|
||||
var building: Dictionary = GameState.find_building(building_id)
|
||||
cost.set_payment_position(building.get("position", null))
|
||||
open_menu()
|
||||
tree.reset_scroll()
|
||||
refresh()
|
||||
|
||||
|
||||
func close_menu() -> void:
|
||||
super.close_menu()
|
||||
building_id = 0
|
||||
cost.set_payment_position(null)
|
||||
|
||||
|
||||
func selected_id() -> String:
|
||||
return String(tree.selected_id)
|
||||
|
||||
|
|
@ -31,6 +42,10 @@ func research_completed(id: String) -> void:
|
|||
refresh(id)
|
||||
|
||||
|
||||
func set_status(message: String) -> void:
|
||||
status.text = message
|
||||
|
||||
|
||||
func refresh(id: String = "") -> void:
|
||||
if id == "":
|
||||
id = String(tree.selected_id)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
script = ExtResource("1")
|
||||
definition_id = "distorder"
|
||||
display_name = "Energy Propulser"
|
||||
description = "Unarmed propulsion hull that burns energy to drive the whole fleet faster. While one flies with you, a fader lets you push cruise speed past its default."
|
||||
description = "Unarmed propulsion hull that burns energy to drive the whole fleet faster. It also grants an aimed fleet dash; each Propulser carries its own recharge."
|
||||
build_cost = 14
|
||||
build_resource_costs = {"ore": 14, "energy": 8}
|
||||
build_time = 16.0
|
||||
|
|
@ -18,7 +18,7 @@ component_scenes = Array[PackedScene]([ExtResource("2")])
|
|||
base_stats = {"max_speed": 645.0, "acceleration": 1845.0, "delivery_speed": 180.0}
|
||||
unlock_id = "unlock_distorder"
|
||||
unlock_name = "Unlock Energy Propulser"
|
||||
unlock_description = "Research a propulsion hull that burns energy to drive the whole fleet faster - while one flies with you, a fader lets you push cruise speed past its default."
|
||||
unlock_description = "Research a propulsion hull that burns energy to drive the whole fleet faster and grants an aimed fleet dash."
|
||||
unlock_cost = 0
|
||||
unlock_resource_costs = {"energy": 50}
|
||||
research_point_cost = 1
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ var fleet_target: Vector2 = Vector2.ZERO
|
|||
# travelling along. Zero heading means the fleet is not being steered.
|
||||
var anchor: Vector2 = Vector2.ZERO
|
||||
var steer_direction: Vector2 = Vector2.ZERO
|
||||
# An aimed Propulser burst translates the complete formation as one body.
|
||||
var dash_direction: Vector2 = Vector2.ZERO
|
||||
var dash_remaining: float = 0.0
|
||||
|
||||
const DASH_SPEED: float = 1400.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -227,12 +232,29 @@ func move_in_direction(direction: Vector2) -> void:
|
|||
steer_direction = direction.normalized()
|
||||
|
||||
|
||||
func dash(direction: Vector2, distance: float) -> bool:
|
||||
if direction.length() < 0.001 or distance <= 0.0 or ships.is_empty():
|
||||
return false
|
||||
if dash_remaining <= 0.0:
|
||||
anchor = get_formation_origin()
|
||||
dash_direction = direction.normalized()
|
||||
dash_remaining = distance
|
||||
return true
|
||||
|
||||
|
||||
func is_dashing() -> bool:
|
||||
return dash_remaining > 0.0
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# #158: a building may pay a local cost out of the hold when the fleet is
|
||||
# actually standing at it, and the autoload has no other way to know
|
||||
# where that is. Written before the early return below - a parked fleet
|
||||
# is exactly the case that matters.
|
||||
GameState.fleet_position = get_centroid()
|
||||
if is_dashing():
|
||||
_advance_dash(delta)
|
||||
return
|
||||
if steer_direction == Vector2.ZERO:
|
||||
return
|
||||
# The anchor - the fleet's formation centre - travels at cruise_speed.
|
||||
|
|
@ -244,6 +266,29 @@ func _process(delta: float) -> void:
|
|||
_assign_slot_targets()
|
||||
|
||||
|
||||
func _advance_dash(delta: float) -> void:
|
||||
var step: float = minf(DASH_SPEED * delta, dash_remaining)
|
||||
var displacement: Vector2 = dash_direction * step
|
||||
dash_remaining -= step
|
||||
anchor += displacement
|
||||
fleet_target += displacement
|
||||
for ship in ships:
|
||||
if not is_instance_valid(ship):
|
||||
continue
|
||||
# Move the hull and its destination together. That preserves every
|
||||
# formation offset and prevents normal steering from trying to "repair"
|
||||
# the burst by pulling ships back toward their pre-dash targets.
|
||||
ship.position += displacement
|
||||
ship.move_target += displacement
|
||||
ship.velocity = Vector2.ZERO
|
||||
ship.fleet_velocity = Vector2.ZERO
|
||||
ship.rotation = dash_direction.angle() + PI / 2.0
|
||||
if dash_remaining <= 0.001:
|
||||
dash_remaining = 0.0
|
||||
if steer_direction != Vector2.ZERO:
|
||||
_assign_slot_targets()
|
||||
|
||||
|
||||
func _effective_cruise_speed() -> float:
|
||||
return cruise_speed * GameState.fleet_speed_multiplier
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,25 @@
|
|||
class_name VesselDistorderComponent
|
||||
extends Node
|
||||
extends "res://vessels/VesselCooldownFillComponent.gd"
|
||||
|
||||
# Pure presence marker (#59) - no per-shot behaviour, no tick(). Its only
|
||||
# job is to make Ship.has_capability("distorder") true, which is what
|
||||
# SpeedFader.gd/Fleet.gd key off to reveal and apply the speed boost.
|
||||
# Energy drain while boosted is a later ticket (#62).
|
||||
# The presence marker used by the speed fader, plus the Energy Propulser's
|
||||
# independently recharging dash (#179). Keeping both on one component means
|
||||
# a vessel is the source of both propulsion capabilities and carries its own
|
||||
# cooldown just like gas bombers and laser vessels do.
|
||||
|
||||
var capability_id := "distorder"
|
||||
|
||||
const DASH_COOLDOWN: float = 5.0
|
||||
const READY_COLOR := Color(0.85, 0.95, 0.15)
|
||||
const COOLING_COLOR := Color(0.28, 0.31, 0.08)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
cooldown_duration = DASH_COOLDOWN
|
||||
|
||||
|
||||
func consume_dash() -> bool:
|
||||
return _begin_cooldown(DASH_COOLDOWN)
|
||||
|
||||
|
||||
func _ready_color() -> Color: return READY_COLOR
|
||||
func _cooling_color() -> Color: return COOLING_COLOR
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
[ext_resource type="Script" path="res://vessels/VesselDistorderComponent.gd" id="1"]
|
||||
|
||||
[node name="DistorderComponent" type="Node"]
|
||||
[node name="DistorderComponent" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
|
|||
|
|
@ -70,13 +70,10 @@ func _explode() -> void:
|
|||
# Asteroids too - a blast that clears everything around it but leaves the
|
||||
# rocks untouched would need explaining, and they take damage the same way.
|
||||
#
|
||||
# #165: and STRUCTURES. The blast used to stop politely at anything the
|
||||
# player owned, which made a bomb dropped on a besieged mothership a free
|
||||
# way to clear the attackers off it. Everything inside the radius is
|
||||
# inside the radius; where it lands is the decision the weapon asks for.
|
||||
# #176: player weapons never damage player buildings. A bomb may be thrown
|
||||
# into a siege without punishing the structure it is meant to defend.
|
||||
for target_node in get_tree().get_nodes_in_group("enemies") \
|
||||
+ get_tree().get_nodes_in_group("asteroids") \
|
||||
+ get_tree().get_nodes_in_group("player_buildings"):
|
||||
+ get_tree().get_nodes_in_group("asteroids"):
|
||||
if is_instance_valid(target_node) and target_node is Node2D \
|
||||
and target_node.global_position.distance_to(target) <= AOE_RADIUS:
|
||||
target_node.take_damage(damage)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=43 format=3]
|
||||
[gd_scene load_steps=44 format=3]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/BuildMenu.tscn" id="menu_build"]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/HangarMenu.tscn" id="menu_hangar"]
|
||||
[ext_resource type="PackedScene" path="res://ui/menus/LabMenu.tscn" id="menu_lab"]
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
[ext_resource type="Script" path="res://controllers/FireStickController.gd" id="fire_ctrl"]
|
||||
[ext_resource type="Script" path="res://controllers/GasBombController.gd" id="29"]
|
||||
[ext_resource type="Script" path="res://controllers/LaserController.gd" id="30"]
|
||||
[ext_resource type="Script" path="res://controllers/DashController.gd" id="dash_ctrl"]
|
||||
[ext_resource type="Script" path="res://ui/SpeedFader.gd" id="31"]
|
||||
[ext_resource type="Script" path="res://world/QuestDirector.gd" id="quest_dir"]
|
||||
[ext_resource type="Script" path="res://ui/QuestPanel.gd" id="quest_panel"]
|
||||
|
|
@ -82,6 +83,9 @@ script = ExtResource("mouse_ctrl")
|
|||
[node name="LaserController" type="Node2D" parent="."]
|
||||
script = ExtResource("30")
|
||||
|
||||
[node name="DashController" type="Node2D" parent="."]
|
||||
script = ExtResource("dash_ctrl")
|
||||
|
||||
[node name="Camera2D" type="Camera2D" parent="."]
|
||||
position = Vector2(0, 0)
|
||||
zoom = Vector2(1, 1)
|
||||
|
|
@ -343,6 +347,22 @@ offset_right = -100
|
|||
offset_top = -548
|
||||
offset_bottom = -452
|
||||
|
||||
[node name="DashButton" type="Control" parent="UI"]
|
||||
visible = false
|
||||
mouse_filter = 2
|
||||
script = ExtResource("10")
|
||||
round_frame = true
|
||||
icon = 45
|
||||
glow_color = Color(0.85, 0.95, 0.15, 1)
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -452
|
||||
offset_right = -356
|
||||
offset_top = -548
|
||||
offset_bottom = -452
|
||||
|
||||
[node name="PivotCCWButton" type="Control" parent="UI"]
|
||||
mouse_filter = 2
|
||||
script = ExtResource("10")
|
||||
|
|
@ -405,6 +425,22 @@ offset_right = -24
|
|||
offset_top = -616
|
||||
offset_bottom = -568
|
||||
|
||||
[node name="DashStatus" type="Label" parent="UI"]
|
||||
mouse_filter = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_colors/font_color = Color(0.85, 0.95, 0.15, 1)
|
||||
autowrap_mode = 2
|
||||
horizontal_alignment = 1
|
||||
text = ""
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -500
|
||||
offset_right = -104
|
||||
offset_top = -672
|
||||
offset_bottom = -624
|
||||
|
||||
[node name="PivotCWButton" type="Control" parent="UI"]
|
||||
mouse_filter = 2
|
||||
script = ExtResource("10")
|
||||
|
|
|
|||
Loading…
Reference in a new issue