This commit is contained in:
Tomasz Boruc 2026-10-11 21:19:40 +02:00
parent f4a6efe74f
commit a3963fe36e
12 changed files with 333 additions and 1 deletions

View File

@ -7,6 +7,7 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd")
@export var experience: int = 0
@export var credits: int = 0
@export var parts: float = 0.0
@export var minerals: Dictionary = {"titanium": 0, "iron": 0, "aluminium": 0}
@export var learning_points: int = 0
@export var skills: Dictionary = {
"piloting": 0, "gunner": 0, "mechanic": 0, "sharp_eye": 0,

View File

@ -36,6 +36,7 @@ func _ready() -> void:
camera.limit_bottom = int(WORLD_SIZE.y * 0.5)
_spawn_celestial_bodies()
_spawn_enemies()
preload("res://scripts/levels/exploration_generator.gd").spawn(self, system_data, player)
var marker := Control.new()
marker.set_script(PLANET_MARKER)
marker.name = "ProtectedPlanetMarker"

View File

@ -7,6 +7,7 @@ var selected_ship_path: String = ""
var restart_inventory: Dictionary = {}
var system_level: int = 1
var system_seed: int = 0
var explored_pois: Dictionary = {}
func create_system_seed() -> int:
var rng := RandomNumberGenerator.new()
@ -18,6 +19,7 @@ func start_new_run() -> void:
system_level = 1
system_seed = create_system_seed()
restart_inventory.clear()
explored_pois.clear()
func _ready() -> void:
var game_window := get_tree().root

View File

@ -31,7 +31,7 @@ func _ready() -> void:
_label = Label.new()
_label.light_mask = 4
_label.position = Vector2(-160, radius + 14)
_label.size = Vector2(320, 54)
_label.size = Vector2(320, 80)
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.add_theme_font_size_override("font_size", 14)
_update_label()

View File

@ -0,0 +1,54 @@
extends RefCounted
const POI := preload("res://scripts/levels/exploration_poi.gd")
static func spawn(game: Node2D, data: Dictionary, player: CharacterBody2D) -> void:
var rng := RandomNumberGenerator.new()
rng.seed = GameState.system_seed ^ 0x504f49
var occupied: Array[Vector2] = []
var wreck_count := rng.randi_range(2, 4)
for index in wreck_count + rng.randi_range(6, 10):
var kind := "wreck" if index < wreck_count else "asteroid"
var candidate := Vector2.ZERO
var found := false
for attempt in 200:
candidate = Vector2(rng.randf_range(-2300, 2300), rng.randf_range(-1500, 1500))
if candidate.length() < 650 or candidate.distance_to(player.position) < 450:
continue
var clear := true
for planet in data["planets"]:
if candidate.distance_to(planet["position"]) < float(planet["radius"]) + 220:
clear = false
for point in occupied:
if candidate.distance_to(point) < 180:
clear = false
if clear:
found = true
break
if found:
occupied.append(candidate)
_add(game, player, kind, candidate, index, rng.randi())
# Rare per system, at most one ruin on an uninhabited planet.
if rng.randf() < 0.08:
var candidates: Array[int] = []
for index in data["planets"].size():
if not data["planets"][index]["protected"]:
candidates.append(index)
if not candidates.is_empty():
var index := candidates[rng.randi_range(0, candidates.size() - 1)]
var planet := game.get_node("CelestialBodies/Planet%d" % (index + 1))
_add(game, player, "ruin", planet.position, 100, rng.randi(), float(planet.get("radius")) + 100.0)
static func _add(game: Node2D, player: CharacterBody2D, kind: String, position: Vector2, index: int, reward_seed: int, approach_radius: float = 115.0) -> void:
var id := "%d:%d" % [GameState.system_seed, index]
if GameState.explored_pois.has(id):
return
var poi := StaticBody2D.new()
poi.set_script(POI)
poi.set("kind", kind)
poi.set("poi_id", id)
poi.set("reward_seed", reward_seed)
poi.set("player", player)
poi.set("approach_radius", approach_radius)
poi.position = position
game.add_child(poi)

View File

@ -0,0 +1 @@
uid://cx2fohii03ed6

View File

@ -0,0 +1,188 @@
extends StaticBody2D
const GENERATOR := preload("res://scripts/items/item_generator.gd")
const LOOT := preload("res://scripts/items/loot_pickup.gd")
var kind := "wreck"
var poi_id := ""
var reward_seed := 1
var player: CharacterBody2D
var approach_radius := 115.0
var hull := 20.0
var _claimed := false
var _module_left_outside := false
var _label: Label
var _rng := RandomNumberGenerator.new()
var _mineral := "iron"
var _points := PackedVector2Array()
func _ready() -> void:
add_to_group("exploration_pois")
_rng.seed = reward_seed
_mineral = ["iron", "aluminium", "titanium"][_rng.randi_range(0, 2)]
hull = 20.0 * reward_scale()
collision_layer = 4 if kind == "asteroid" else 0
collision_mask = 0
if kind == "asteroid":
var shape := CollisionShape2D.new()
var circle := CircleShape2D.new()
circle.radius = 30
shape.shape = circle
add_child(shape)
for index in 10:
_points.append(Vector2.RIGHT.rotated(TAU * index / 10) * _rng.randf_range(23, 35))
_label = Label.new()
_label.light_mask = 4
_label.position = Vector2(-160, 40)
_label.size = Vector2(320, 70)
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.add_theme_font_size_override("font_size", 12)
if kind == "ruin":
z_index = 20
_label.position = Vector2(-40, -35)
_label.size = Vector2(80, 70)
_label.add_theme_font_size_override("font_size", 52)
_label.add_theme_color_override("font_color", Color("ffd166"))
_label.add_theme_color_override("font_outline_color", Color("151625"))
_label.add_theme_constant_override("outline_size", 8)
add_child(_label)
_update_label(false)
queue_redraw()
func reward_scale() -> float:
return 1.0 + 0.05 * (clampi(GameState.system_level, 1, 1000) - 1)
func _draw() -> void:
if kind == "asteroid":
draw_colored_polygon(_points, Color("655e56"))
var outline := _points.duplicate()
outline.append(_points[0])
draw_polyline(outline, Color("a39a88"), 2, true)
draw_circle(Vector2(-8, -5), 7, Color("49443f"))
elif kind == "wreck":
draw_colored_polygon(PackedVector2Array([Vector2(-38, 20), Vector2(-16, -30), Vector2(7, -12), Vector2(22, 27), Vector2(-5, 14)]), Color("626b78"))
draw_line(Vector2(-12, -20), Vector2(4, 15), Color("df9c67"), 3, true)
draw_colored_polygon(PackedVector2Array([Vector2(28, -16), Vector2(42, -5), Vector2(29, 8)]), Color("434e60"))
func _update_label(near: bool) -> void:
if kind == "ruin":
_label.text = "?"
elif kind == "asteroid":
_label.text = "Asteroida · %s\nHP: %.0f" % [{"iron": "Żelazo", "aluminium": "Aluminium", "titanium": "Tytan"}[_mineral], hull]
else:
_label.text = ("Wrak statku" if kind == "wreck" else "Antyczne znalezisko") + ("\n[E] Przeszukaj" if near else "\nPodleć, aby przeszukać")
func _physics_process(_delta: float) -> void:
if _claimed or not is_instance_valid(player) or bool(player.get("_is_destroyed")):
return
var near := global_position.distance_to(player.global_position) <= approach_radius
_update_label(near)
if near and (kind == "ruin" or (kind == "wreck" and Input.is_physical_key_pressed(KEY_E))):
claim()
func take_weapon_hit(amount: float, _critical: bool = false) -> void:
take_damage(amount)
func take_damage(amount: float) -> void:
if kind != "asteroid" or _claimed:
return
hull -= maxf(0, amount)
if hull <= 0:
claim()
else:
_update_label(false)
func claim() -> bool:
if _claimed or not is_instance_valid(player) or bool(player.get("_is_destroyed")):
return false
_claimed = true
GameState.explored_pois[poi_id] = true
var inventory := player.get_node("ShipInventory") as ShipInventory
var summary := ""
var scale := reward_scale()
if kind == "asteroid":
var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale))
GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount
summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu"}[_mineral]]
elif kind == "ruin":
var xp := roundi(_rng.randi_range(10, 20) * scale)
GameState.captain.add_rewards(xp, 0)
var module := _drop_module(true)
summary = "%d XP oraz %s (poziom %d)" % [xp, module.definition.display_name, module.item_level]
_show_discovery(summary)
else:
match _rng.randi_range(0, 3):
0:
var amount := roundi(_rng.randi_range(3, 8) * scale)
GameState.captain.parts += amount
summary = "+%d części" % amount
1:
_drop_module(false)
summary = "Znaleziono moduł"
2:
var amount := maxi(1, roundi(scale))
inventory.missile_ammunition += amount
summary = "+%d rakiet" % amount
3:
var amount := roundi(_rng.randi_range(5, 15) * scale)
inventory.ammunition += amount
summary = "+%d amunicji" % amount
inventory.ammunition_changed.emit()
GameState.captain.emit_changed()
if kind != "ruin":
var popup := preload("res://scenes/game/damage_popup.tscn").instantiate() as Label
popup.text = summary
popup.set("text_color", Color("c9e8ff"))
popup.position = position + Vector2(-80, -65)
get_parent().add_child.call_deferred(popup)
set_physics_process(false)
queue_free()
return true
func _show_discovery(reward: String) -> void:
var layer := CanvasLayer.new()
layer.layer = 30
layer.name = "AncientDiscoveryMessage"
get_parent().add_child(layer)
var panel := PanelContainer.new()
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
layer.add_child(panel)
panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
panel.offset_left = -300
panel.offset_right = 300
panel.offset_top = 70
var label := Label.new()
label.custom_minimum_size = Vector2(600, 95)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", 18)
label.add_theme_color_override("font_color", Color("ffd166"))
label.text = "Znalazłeś ślady starożytnej upadłej cywilizacji!\nZdobyłeś: " + reward
if _module_left_outside:
label.text += "\nMagazyn pełny — moduł czeka obok planety."
panel.add_child(label)
var tween := layer.create_tween()
tween.tween_interval(7.0)
tween.tween_property(panel, "modulate:a", 0.0, 0.5)
tween.tween_callback(layer.queue_free)
func _drop_module(ancient: bool) -> ItemInstance:
var definitions: Array[ItemDefinition] = []
for definition in GENERATOR.DEFINITIONS:
if definition.category != ItemDefinition.Category.WEAPON:
definitions.append(definition)
var item := GENERATOR.generate_for_definition(definitions[_rng.randi_range(0, definitions.size() - 1)], mini(1000, GameState.system_level + (1 if ancient else 0)), 2 if ancient else 0)
if ancient:
for modifier in item.additional_modifiers:
var bounds := modifier.affix.get_value_range(item.item_level)
modifier.value = lerpf(bounds.x, bounds.y, _rng.randf_range(0.9, 1.0))
if player.get_node("ShipInventory").add_item(item):
return item
_module_left_outside = true
var loot := Node2D.new()
loot.set_script(LOOT)
loot.set("item", item)
loot.position = position + Vector2(0, 70)
get_parent().add_child.call_deferred(loot)
return item

View File

@ -0,0 +1 @@
uid://dmmyfie4rijcn

View File

@ -13,6 +13,7 @@ var _inventory: ShipInventory
var _drawer_open := false
var _slide_tween: Tween
var _missile_button: Button
var _minerals_label: Label
var drawer_width := 780.0
func _ready() -> void:
@ -29,6 +30,10 @@ func _ready() -> void:
_missile_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_missile_button.pressed.connect(func() -> void: _inventory.craft_missile())
missile_row.add_child(_missile_button)
_minerals_label = Label.new()
_minerals_label.add_theme_font_size_override("font_size", 12)
$Contents.add_child(_minerals_label)
$Contents.move_child(_minerals_label, $Contents/CargoGrid.get_index())
_inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory
if _inventory:
_inventory.inventory_changed.connect(_refresh_title)
@ -42,6 +47,7 @@ func _ready() -> void:
_refresh_title()
func _refresh_title() -> void:
_minerals_label.text = "SUROWCE · Tytan: %d · Żelazo: %d · Aluminium: %d" % [GameState.captain.minerals.get("titanium", 0), GameState.captain.minerals.get("iron", 0), GameState.captain.minerals.get("aluminium", 0)]
craft_button.disabled = _inventory == null or not _inventory.can_craft_ammunition()
_missile_button.disabled = _inventory == null or not _inventory.can_craft_missile()
if _inventory:

View File

@ -0,0 +1,72 @@
extends Node2D
const POI = preload("res://scripts/levels/exploration_poi.gd")
func _ready() -> void:
GameState.start_new_run()
var ship = preload("res://scenes/game/player_ship.tscn").instantiate()
ship.name = "PlayerShip"
add_child(ship)
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
var inv = ship.get_node("ShipInventory")
for kind in ["wreck", "asteroid", "ruin"]:
for index in 12:
var poi = StaticBody2D.new()
poi.set_script(POI)
poi.kind = kind
poi.player = ship
poi.reward_seed = index + 1
poi.poi_id = kind + str(index)
add_child(poi)
if kind == "asteroid":
poi.take_weapon_hit(poi.hull - 1)
assert(not poi._claimed and is_equal_approx(poi.hull, 1))
poi.take_weapon_hit(1)
assert(poi._claimed)
else:
assert(poi.claim())
assert(not poi.claim())
await get_tree().process_frame
assert(GameState.captain.parts > 0)
assert(inv.ammunition > 50 and inv.missile_ammunition > 2)
assert(GameState.captain.minerals.iron + GameState.captain.minerals.titanium + GameState.captain.minerals.aluminium > 0)
var ancient = 0
var modules: Array = inv.cargo.filter(func(item): return item != null)
for loot in get_tree().get_nodes_in_group("loot"):
modules.append(loot.item)
for item in modules:
if item.item_level == 2:
ancient += 1
assert(item.get_addon_count() >= 4)
for modifier in item.additional_modifiers:
var bounds = modifier.affix.get_value_range(2)
assert(modifier.value >= lerpf(bounds.x, bounds.y, 0.9) - 0.00001)
assert(ancient == 12)
var discovery = StaticBody2D.new()
discovery.set_script(POI)
discovery.kind = "ruin"
discovery.player = ship
discovery.poi_id = "auto-discovery"
discovery.position = Vector2(1000, 0)
discovery.approach_radius = 200
add_child(discovery)
assert(discovery._label.text == "?")
discovery._physics_process(0)
assert(not discovery._claimed)
ship.position = Vector2(810, 0)
discovery._physics_process(0)
assert(discovery._claimed and GameState.explored_pois.has("auto-discovery"))
assert(not discovery.claim())
var data = preload("res://scripts/levels/system_generator.gd").generate(1, 1234)
var bodies = Node2D.new()
bodies.name = "CelestialBodies"
add_child(bodies)
preload("res://scripts/levels/exploration_generator.gd").spawn(self, data, ship)
var generated = get_tree().get_nodes_in_group("exploration_pois").filter(func(p): return not p.is_queued_for_deletion())
assert(generated.size() >= 8 and generated.size() <= 14)
var probe = generated[0]
assert(probe.reward_scale() == 1.0)
GameState.system_level = 10
assert(is_equal_approx(probe.reward_scale(), 1.45))
GameState.system_level = 1
print("EXPLORATION_TEST: PASS rewards, duplicate prevention, minerals, ancient level+1 and high addons, generation")
get_tree().quit()

View File

@ -0,0 +1 @@
uid://bwrnlc3vjc2ty

View File

@ -0,0 +1,5 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/levels/exploration_test.gd" id="1"]
[node name="Test" type="Node2D"]
script = ExtResource("1")