#80 - desktop combat. The mouse aims, the left button fires, and the number keys pick the weapon: 1 the fleet's guns, 2 and up the abilities. The twin sticks are built for thumbs; on a desktop they mean dragging a virtual stick with a pointer that could simply BE the crosshair. The pointer path hands its heading to FireStickController rather than driving weapons itself, so exactly one thing in the scene aims a gun, and a held stick wins - a finger on it is a deliberate act, a mouse resting somewhere is not. Abilities gained fire_at(), which aims and fires in one call instead of pretending to be a press-drag-release touch. It is gated on the absence of a touchscreen, because Godot emulates a mouse button from every touch and without that a tap on a phone would fire the guns. The gate has an explicit override, and not for tidiness: headless reports a touchscreen, so with the automatic answer alone the aiming could never be exercised by a test and the only covered behaviour would be the guard. #81 - the bottom-left control hint is gone, from both scenes and from GameController. #82 - a chaser is destroyed by the impact when it reaches a vessel. It used to pass through the wreck untouched and line up another pass, which made a ramming enemy strictly better than any other. Data-driven per enemy (dies_on_contact), because a sniper that happens to drift into a ship has not spent itself doing it - and routed through take_damage, so it dies exactly as it dies to gunfire. The enemy is told about the impact BEFORE the ship resolves its own damage, since resolving it can free the ship. #83 - the research tree's buy button is gone; tapping a node twice buys it. Deliberately "the same node twice" rather than a timed double-tap: no window to miss on a slow phone, no accidental purchase while browsing, and the first tap always does something visible, so the second is never a blind commitment. The status line now says "Tap again to research" - with the button gone it is the only thing distinguishing an affordable node from an unaffordable one. test_enemy_collision asserted the exact behaviour #82 reversed, and now checks the ram costs both. Suite 117/117. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
54287d27ea
commit
baac11ae8b
19 changed files with 677 additions and 62 deletions
138
tests/test_chaser_rams.gd
Normal file
138
tests/test_chaser_rams.gd
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_chaser_rams.gd
|
||||
# Forgejo #82: a chaser is destroyed by the impact when it reaches a player
|
||||
# vessel. It used to pass straight through the wreck untouched and line up
|
||||
# another pass, which made a ramming enemy strictly better than any other.
|
||||
#
|
||||
# Covers:
|
||||
# A. the chaser dies on contact, and dies the same way it dies to gunfire -
|
||||
# through take_damage, so the explosion and the destroyed signal happen
|
||||
# B. the vessel it hit still dies too. Trading one for one is the point;
|
||||
# the chaser must not become harmless
|
||||
# C. an enemy NOT marked dies_on_contact survives the same collision, so
|
||||
# this is a per-enemy decision and not a blanket rule about touching
|
||||
# D. a SHIELDED vessel survives, and still destroys the chaser
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var chaser: Node2D
|
||||
var sniper: Node2D
|
||||
var victim: Node2D
|
||||
var destroyed_fired: bool = false
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.reset_progression()
|
||||
game_state.vessel_levels["unlock_shield"] = 1
|
||||
main = load("res://Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
fleet = main.get_node("Fleet")
|
||||
|
||||
|
||||
func _spawn_enemy(enemy_id: String, at: Vector2) -> Node2D:
|
||||
var enemy: Node2D = load("res://Enemy.tscn").instantiate()
|
||||
enemy.enemy_id = enemy_id
|
||||
enemy.position = at
|
||||
main.add_child(enemy)
|
||||
enemy.speed = 0.0
|
||||
return enemy
|
||||
|
||||
|
||||
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)
|
||||
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(hazard):
|
||||
hazard.queue_free()
|
||||
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||||
|
||||
var definitions: Script = load("res://EnemyDefinitions.gd")
|
||||
if not bool(definitions.get_definition("chaser").dies_on_contact):
|
||||
return _fail("the chaser is not marked dies_on_contact")
|
||||
if bool(definitions.get_definition("sniper").dies_on_contact):
|
||||
return _fail("the sniper is marked dies_on_contact, so case C proves nothing")
|
||||
|
||||
# Held as an instance, not read back off fleet.ships: the
|
||||
# wiped-out-fleet safety net rebuilds a vessel within a frame, so
|
||||
# "the fleet is empty" is never true for long enough to observe.
|
||||
victim = fleet.ships[0]
|
||||
chaser = _spawn_enemy("chaser", victim.global_position)
|
||||
chaser.destroyed.connect(func(): destroyed_fired = true)
|
||||
stage = 1
|
||||
elapsed = 0.0
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.5:
|
||||
return false
|
||||
# --- A/B. one for one ------------------------------------------
|
||||
if is_instance_valid(chaser) and not chaser.is_queued_for_deletion():
|
||||
return _fail("the chaser survived ramming a vessel")
|
||||
if not destroyed_fired:
|
||||
return _fail("the chaser vanished without firing `destroyed` - it was not routed through take_damage")
|
||||
if is_instance_valid(victim) and not victim.is_queued_for_deletion():
|
||||
return _fail("the rammed vessel survived, so the chaser cost nothing to stop")
|
||||
print("chaser and vessel destroyed each other, through the normal death path")
|
||||
|
||||
# --- C. not a blanket rule -------------------------------------
|
||||
fleet.spawn_ships(1, Vector2(3000.0, 0.0), PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||||
sniper = _spawn_enemy("sniper", fleet.ships[0].global_position)
|
||||
stage = 2
|
||||
elapsed = 0.0
|
||||
return false
|
||||
2:
|
||||
if elapsed < 0.5:
|
||||
return false
|
||||
if not is_instance_valid(sniper) or sniper.is_queued_for_deletion():
|
||||
return _fail("a sniper died on contact too - the rule is meant to be per-enemy")
|
||||
print("a sniper survives the same collision")
|
||||
sniper.queue_free()
|
||||
|
||||
# --- D. a shield saves the vessel, not the chaser --------------
|
||||
fleet.spawn_ships(1, Vector2(-3000.0, 0.0), PackedStringArray([GameState.VESSEL_SHIELD]))
|
||||
stage = 3
|
||||
elapsed = 0.0
|
||||
return false
|
||||
3:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
victim = fleet.ships[0]
|
||||
var shielded: Node2D = victim
|
||||
if not shielded.has_shield():
|
||||
return _fail("the shield vessel spawned with no shield, so this case proves nothing")
|
||||
chaser = _spawn_enemy("chaser", shielded.global_position)
|
||||
stage = 4
|
||||
elapsed = 0.0
|
||||
return false
|
||||
4:
|
||||
if elapsed < 0.5:
|
||||
return false
|
||||
if is_instance_valid(chaser) and not chaser.is_queued_for_deletion():
|
||||
return _fail("the chaser survived ramming a shielded vessel")
|
||||
if not is_instance_valid(victim) or victim.is_queued_for_deletion():
|
||||
return _fail("the shielded vessel died anyway")
|
||||
print("a shielded vessel absorbs the ram and the chaser still dies")
|
||||
|
||||
print("TEST_CHASER_RAMS: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_CHASER_RAMS: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
174
tests/test_desktop_combat.gd
Normal file
174
tests/test_desktop_combat.gd
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_desktop_combat.gd
|
||||
# Forgejo #80: on a computer the mouse aims and the left button fires, and the
|
||||
# number keys pick the weapon - 1 the fleet's guns, 2 and up the abilities.
|
||||
#
|
||||
# Covers:
|
||||
# A. the weapon numbering is FIXED and named, so "2" is the gas bomb in
|
||||
# every scene rather than whatever node order happens to produce
|
||||
# B. holding the button with weapon 1 selected points the fleet's guns at
|
||||
# the cursor, through the same controller the stick uses - not a second
|
||||
# thing that aims
|
||||
# C. releasing stops it
|
||||
# D. the stick WINS while it is held, so a mouse resting somewhere cannot
|
||||
# override a finger
|
||||
# E. selecting an ability highlights its button, so what a click will do is
|
||||
# on screen rather than remembered
|
||||
# F. it is inert on a touch device. Godot emulates a mouse button from every
|
||||
# touch, so without that guard a tap on a phone would fire the guns
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var mouse: Node
|
||||
var fire_stick: Node
|
||||
var stick: Control
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.reset_progression()
|
||||
main = load("res://Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
fleet = main.get_node("Fleet")
|
||||
mouse = main.get_node("MouseCombatController")
|
||||
# Checked here rather than at first use: a script that fails to PARSE
|
||||
# leaves a bare Node2D in the scene, and every call against it becomes a
|
||||
# per-frame "nonexistent function" error that runs until the harness
|
||||
# times the test out three minutes later instead of failing in one line.
|
||||
if not mouse.has_method("weapon_count"):
|
||||
print("TEST_DESKTOP_COMBAT: FAIL (MouseCombatController.gd did not load - see the parse error above)")
|
||||
quit(1)
|
||||
return
|
||||
fire_stick = main.get_node("FireStickController")
|
||||
stick = main.get_node("UI/FireStick")
|
||||
|
||||
|
||||
func _weapon_heading() -> Vector2:
|
||||
return fire_stick.aim_direction
|
||||
|
||||
|
||||
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)
|
||||
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(hazard):
|
||||
hazard.queue_free()
|
||||
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||||
|
||||
# --- A. fixed, named ordering ---------------------------------
|
||||
if mouse.weapon_count() != mouse.abilities.size() + 1:
|
||||
return _fail("weapon count %d does not match 1 + %d abilities" \
|
||||
% [mouse.weapon_count(), mouse.abilities.size()])
|
||||
if mouse.abilities.is_empty():
|
||||
return _fail("no ability controllers were resolved, so the number keys address nothing")
|
||||
if mouse.abilities[0] != main.get_node("GasBombController"):
|
||||
return _fail("weapon 2 is not the gas bomb - the ticket names it explicitly")
|
||||
if not mouse.select_weapon(0):
|
||||
return _fail("could not select the default weapon")
|
||||
if mouse.select_weapon(mouse.weapon_count()):
|
||||
return _fail("selecting a weapon past the end was accepted")
|
||||
print("weapon 1 is the fleet's guns, weapon 2 is the gas bomb, %d in total" \
|
||||
% mouse.weapon_count())
|
||||
|
||||
# --- F. inert where a touchscreen exists -----------------------
|
||||
# Checked against the real capability rather than forced, so this
|
||||
# reports what will actually happen on the machine it runs on.
|
||||
print("touchscreen present: %s -> mouse combat enabled: %s" \
|
||||
% [DisplayServer.is_touchscreen_available(), mouse.is_enabled()])
|
||||
if mouse.pointer_mode != mouse.PointerMode.AUTO:
|
||||
return _fail("the scene ships with an overridden pointer mode, so what a real device does is not what was tested")
|
||||
if mouse.is_enabled() == DisplayServer.is_touchscreen_available():
|
||||
return _fail("mouse combat is not gated on the absence of a touchscreen")
|
||||
print("gated correctly on the display's own answer")
|
||||
|
||||
# Everything below is about the AIMING, not the guard. Headless
|
||||
# reports a touchscreen, so the mode is forced on here - otherwise
|
||||
# these cases would silently never run and this file would test
|
||||
# nothing but the line above.
|
||||
mouse.pointer_mode = mouse.PointerMode.ON
|
||||
if not mouse.is_enabled():
|
||||
return _fail("forcing the pointer mode on did not enable it")
|
||||
stage = 1
|
||||
elapsed = 0.0
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.1:
|
||||
return false
|
||||
# --- B. holding the button aims the guns at the cursor ---------
|
||||
mouse.select_weapon(0)
|
||||
mouse.aim_world = fleet.get_centroid() + Vector2(0.0, -500.0)
|
||||
mouse.firing = true
|
||||
mouse._process(0.0)
|
||||
fire_stick._process(0.0)
|
||||
var heading: Vector2 = _weapon_heading().normalized()
|
||||
if heading.distance_to(Vector2(0.0, -1.0)) > 0.05:
|
||||
return _fail("holding the button aimed along %s, but the cursor was straight up" % heading)
|
||||
print("holding the button aims the guns at the cursor")
|
||||
|
||||
# --- C. releasing stops -----------------------------------------
|
||||
mouse.firing = false
|
||||
mouse._process(0.0)
|
||||
fire_stick._process(0.0)
|
||||
if _weapon_heading() != Vector2.ZERO:
|
||||
return _fail("releasing the button left the guns aimed at %s" % _weapon_heading())
|
||||
print("releasing stops the guns")
|
||||
|
||||
# --- D. a held stick wins ---------------------------------------
|
||||
var centre: Vector2 = stick.global_position + stick.size * 0.5
|
||||
var press := InputEventScreenTouch.new()
|
||||
press.index = 9
|
||||
press.pressed = true
|
||||
press.position = centre
|
||||
stick.handle_touch(press)
|
||||
var drag := InputEventScreenDrag.new()
|
||||
drag.index = 9
|
||||
drag.position = centre + Vector2(stick.max_radius, 0.0)
|
||||
stick.handle_drag(drag)
|
||||
mouse.aim_world = fleet.get_centroid() + Vector2(0.0, -500.0)
|
||||
mouse.firing = true
|
||||
mouse._process(0.0)
|
||||
fire_stick._process(0.0)
|
||||
var contested: Vector2 = _weapon_heading().normalized()
|
||||
if contested.distance_to(Vector2(1.0, 0.0)) > 0.05:
|
||||
return _fail("with the stick pushed right and the mouse aiming up, the guns went %s - the stick must win" % contested)
|
||||
print("a held stick overrides the mouse")
|
||||
var release := InputEventScreenTouch.new()
|
||||
release.index = 9
|
||||
release.pressed = false
|
||||
release.position = centre
|
||||
stick.handle_touch(release)
|
||||
mouse.firing = false
|
||||
|
||||
# --- E. the chosen ability is shown ----------------------------
|
||||
mouse.select_weapon(1)
|
||||
var bomb_button: Node = mouse.abilities[0].button
|
||||
if not bomb_button.highlighted:
|
||||
return _fail("selecting the gas bomb did not highlight its button")
|
||||
mouse.select_weapon(0)
|
||||
if bomb_button.highlighted:
|
||||
return _fail("going back to the guns left the gas bomb still highlighted")
|
||||
print("the selected ability's own button carries the selection")
|
||||
|
||||
print("TEST_DESKTOP_COMBAT: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_DESKTOP_COMBAT: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -2,9 +2,14 @@ extends SceneTree
|
|||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_enemy_collision.gd
|
||||
# Verifies that an enemy touching a ship destroys that ship, is removed
|
||||
# from the fleet, while the enemy itself survives the touch (enemies only
|
||||
# die from being shot, not from ramming).
|
||||
# Verifies that an enemy touching a ship destroys that ship and takes it out
|
||||
# of the fleet.
|
||||
#
|
||||
# This used to also assert the enemy SURVIVED the touch. #82 reversed that for
|
||||
# the chaser - which is the enemy this spawns - so a ram now costs both. The
|
||||
# rule itself, including which enemies it applies to and what a shield does
|
||||
# about it, is tests/test_chaser_rams.gd; here it is only checked far enough
|
||||
# to confirm this test is exercising the ramming enemy it thinks it is.
|
||||
|
||||
const CHECK_DELAY: float = 0.05
|
||||
|
||||
|
|
@ -58,13 +63,14 @@ func _finish() -> void:
|
|||
var ship_destroyed: bool = not is_instance_valid(target_ship)
|
||||
var ship_removed_from_fleet: bool = fleet.ships.find(target_ship) == -1
|
||||
var fleet_count_ok: bool = fleet.ships.size() == 4
|
||||
var enemy_survived: bool = is_instance_valid(enemy) and enemy.hp > 0
|
||||
# The chaser is destroyed by the impact too (#82).
|
||||
var enemy_died: bool = not is_instance_valid(enemy) or enemy.is_queued_for_deletion()
|
||||
|
||||
print("ship_destroyed=%s ship_removed_from_fleet=%s ships_remaining=%d enemy_survived=%s" % [
|
||||
ship_destroyed, ship_removed_from_fleet, fleet.ships.size(), enemy_survived
|
||||
print("ship_destroyed=%s ship_removed_from_fleet=%s ships_remaining=%d enemy_died=%s" % [
|
||||
ship_destroyed, ship_removed_from_fleet, fleet.ships.size(), enemy_died
|
||||
])
|
||||
|
||||
if ship_destroyed and ship_removed_from_fleet and fleet_count_ok and enemy_survived:
|
||||
if ship_destroyed and ship_removed_from_fleet and fleet_count_ok and enemy_died:
|
||||
print("TEST_ENEMY_COLLISION: PASS")
|
||||
quit(0)
|
||||
else:
|
||||
|
|
|
|||
1
tests/test_gas_burner_pause.gd.uid
Normal file
1
tests/test_gas_burner_pause.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bu5mnp245dht7
|
||||
1
tests/test_pickup_feedback.gd.uid
Normal file
1
tests/test_pickup_feedback.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://du732a85tmlhl
|
||||
140
tests/test_research_double_tap.gd
Normal file
140
tests/test_research_double_tap.gd
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_research_double_tap.gd
|
||||
# Forgejo #83: the research tree's separate buy button is gone. Tapping a node
|
||||
# selects it; tapping the SAME node again buys it.
|
||||
#
|
||||
# Covers:
|
||||
# A. the buy button really is gone from the scene, not merely bypassed - a
|
||||
# leftover would still be tappable and would still be the thing a player
|
||||
# reaches for
|
||||
# B. the first tap selects and does NOT buy, so browsing the tree is free
|
||||
# C. a second tap on the same node buys it
|
||||
# D. moving to a DIFFERENT node resets that - two taps on two nodes must
|
||||
# never add up to a purchase
|
||||
# E. tapping an unaffordable node twice buys nothing and says why
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
var tree_view: Node
|
||||
var controller: Node
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
const NODE_ID := "shipyards"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.reset_progression()
|
||||
main = load("res://Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
controller = main.get_node("BuildController")
|
||||
tree_view = main.get_node("UI/SkillMenu/Tree")
|
||||
|
||||
|
||||
func _tap(id: String) -> void:
|
||||
# Straight at the node's own position in the tree's local space, so this
|
||||
# exercises the same hit test a finger does.
|
||||
var node: Dictionary = game_state.skill_node(id)
|
||||
var event := InputEventScreenTouch.new()
|
||||
event.index = 1
|
||||
event.pressed = true
|
||||
event.position = tree_view.global_position + tree_view.node_screen_pos(node)
|
||||
tree_view.handle_touch(event)
|
||||
var release := InputEventScreenTouch.new()
|
||||
release.index = 1
|
||||
release.pressed = false
|
||||
release.position = event.position
|
||||
tree_view.handle_touch(release)
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
if stage != 0:
|
||||
return false
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
stage = 1
|
||||
|
||||
# --- A. the button is gone ---------------------------------------------
|
||||
if main.get_node_or_null("UI/SkillMenu/UnlockButton") != null:
|
||||
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()
|
||||
game_state.unlocked.clear()
|
||||
game_state.research_points = 99
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 500)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
|
||||
var cost: int = int(game_state.skill_node(NODE_ID).cost)
|
||||
var before: int = game_state.available_ore()
|
||||
|
||||
# --- B. the first tap only selects -------------------------------------
|
||||
_tap(NODE_ID)
|
||||
if tree_view.selected_id != NODE_ID:
|
||||
return _fail("the first tap did not select the node")
|
||||
if game_state.is_unlocked(NODE_ID):
|
||||
return _fail("the first tap bought the research - browsing the tree must be free")
|
||||
if game_state.available_ore() != before:
|
||||
return _fail("the first tap spent %d" % (before - game_state.available_ore()))
|
||||
print("first tap selects and spends nothing")
|
||||
|
||||
# --- C. the second tap buys --------------------------------------------
|
||||
_tap(NODE_ID)
|
||||
if not game_state.is_unlocked(NODE_ID):
|
||||
return _fail("a second tap on the same node did not buy it")
|
||||
if game_state.available_ore() != before - cost:
|
||||
return _fail("bought for %d, expected %d" % [before - game_state.available_ore(), cost])
|
||||
print("second tap on the same node buys it (%d ore)" % cost)
|
||||
|
||||
# --- D. two taps on two nodes are not a purchase -----------------------
|
||||
var first: String = ""
|
||||
var second: String = ""
|
||||
for node in GameState.SKILL_TREE:
|
||||
var id: String = String(node.id)
|
||||
if game_state.is_unlocked(id) or not game_state.prerequisite_met(id):
|
||||
continue
|
||||
if first == "":
|
||||
first = id
|
||||
elif second == "":
|
||||
second = id
|
||||
break
|
||||
if second == "":
|
||||
return _fail("could not find two unlocked-but-available nodes for the cross-node case")
|
||||
var ore_before: int = game_state.available_ore()
|
||||
_tap(first)
|
||||
_tap(second)
|
||||
if game_state.is_unlocked(first) or game_state.is_unlocked(second):
|
||||
return _fail("tapping two different nodes bought one of them")
|
||||
if game_state.available_ore() != ore_before:
|
||||
return _fail("tapping two different nodes spent %d" % (ore_before - game_state.available_ore()))
|
||||
print("one tap each on two nodes buys neither")
|
||||
|
||||
# --- E. unaffordable stays unaffordable --------------------------------
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
_tap(second)
|
||||
_tap(second)
|
||||
if game_state.is_unlocked(second):
|
||||
return _fail("a node was bought with no ore at all")
|
||||
controller._refresh_skill_menu(second)
|
||||
if controller.skill_status.text == "":
|
||||
return _fail("an unaffordable node was refused silently")
|
||||
print("unaffordable node refused, and says so: '%s'" % controller.skill_status.text)
|
||||
|
||||
print("TEST_RESEARCH_DOUBLE_TAP: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_RESEARCH_DOUBLE_TAP: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
Loading…
Reference in a new issue