Compare commits

..

2 Commits

Author SHA1 Message Date
a3963fe36e POI 2026-10-11 21:19:40 +02:00
f4a6efe74f Zmiany 2026-10-11 20:57:00 +02:00
17 changed files with 372 additions and 10 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

@ -47,7 +47,11 @@ func can_combine(other: ItemAffixDefinition) -> bool:
)
func roll_value(level: int) -> float:
var bounds := get_value_range(level)
return randf_range(bounds.x, bounds.y)
func get_value_range(level: int) -> Vector2:
var progression := float(clampi(level, 1, 1000) - 1) / 999.0
var lower := lerpf(minimum_value_level_1, minimum_value_level_1000, progression)
var upper := lerpf(maximum_value_level_1, maximum_value_level_1000, progression)
return randf_range(minf(lower, upper), maxf(lower, upper))
return Vector2(minf(lower, upper), maxf(lower, upper))

View File

@ -5,6 +5,18 @@ class_name ItemStatModifier
@export var value: float = 0.0
@export var affix: ItemAffixDefinition
# Fixed-value affixes and legacy modifiers have no roll quality to highlight.
func get_roll_quality(level: int) -> int:
if affix == null:
return 0
var bounds := affix.get_value_range(level)
if is_equal_approx(bounds.x, bounds.y):
return 0
var fraction := clampf((value - bounds.x) / (bounds.y - bounds.x), 0.0, 1.0)
if fraction >= 0.9 - 0.000001:
return 2
return 1 if fraction >= 0.75 - 0.000001 else 0
func get_display_text() -> String:
var label := affix.display_name if affix else ItemInstance.stat_name(stat_key)
var suffix := affix.unit if affix else ("%" if stat_key.ends_with("_percent") else "/s")

View File

@ -8,7 +8,7 @@ const CARGO_SIZE := 20
const WEAPON_SLOT_COUNT := 3
const MISSILE_SLOT := 2
const STARTING_MISSILE_AMMUNITION := 2
const MISSILE_CRAFT_PARTS_COST := 50.0
const MISSILE_CRAFT_PARTS_COST := 20.0
const MISSILE_BUY_BASE_COST := 10
const MISSILE_DEFINITIONS := [preload("res://resources/items/missile_launcher.tres"), preload("res://resources/items/unguided_missile.tres")]
const MODULE_SLOT_COUNT := 6

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

@ -124,16 +124,30 @@ func _make_custom_tooltip(for_text: String) -> Object:
background.set_border_width_all(1)
background.set_content_margin_all(12)
panel.add_theme_stylebox_override("panel", background)
var label := Label.new()
label.text = for_text
var label := RichTextLabel.new()
label.fit_content = true
label.scroll_active = false
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.custom_minimum_size.x = 330
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.add_theme_font_size_override("font_size", 13)
label.add_theme_font_size_override("normal_font_size", 13)
var item := _inventory.get_item(slot_kind, slot_index)
if item:
background.bg_color = item.get_background_color()
background.border_color = item.get_type_color()
label.add_theme_color_override("font_color", Color("f1f4f8"))
label.add_theme_color_override("default_color", Color("f1f4f8"))
for line in for_text.split("\n"):
var quality := 0
if item:
for modifier in item.additional_modifiers:
if modifier != null and line == modifier.get_display_text():
quality = modifier.get_roll_quality(item.item_level)
break
if quality > 0:
label.push_color(Color("ffd166") if quality == 2 else Color("79e6a0"))
label.add_text(("★ " if quality == 2 else "") + line + "\n")
if quality > 0:
label.pop()
panel.add_child(label)
return panel

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

@ -47,9 +47,9 @@ func _ready() -> void:
weapons.set("missile_cooldown_remaining", 0.0)
assert(not weapons.call("try_fire_missile"))
assert(inventory.weapon_slots[2] == missile and ship.get("energy") == energy_before)
GameState.captain.parts = 49.0
GameState.captain.parts = 19.0
assert(not inventory.craft_missile())
GameState.captain.parts = 50.0
GameState.captain.parts = 20.0
assert(inventory.craft_missile() and GameState.captain.parts == 0.0)
var unguided := ItemInstance.new()
unguided.definition = ShipInventory.MISSILE_DEFINITIONS[1]
@ -86,7 +86,7 @@ func _ready() -> void:
if inventory.cargo[index] == null:
inventory.cargo[index] = missile
var ammo_before := inventory.missile_ammunition
assert(inventory.craft_missile() and GameState.captain.parts == 50.0)
assert(inventory.craft_missile() and GameState.captain.parts == 80.0)
assert(inventory.buy_missile() and GameState.captain.credits == 70)
assert(inventory.missile_ammunition == ammo_before + 2)
addon.value = 100.0

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")