Balans
Balans
This commit is contained in:
parent
f4b3fe79ae
commit
5b1d284440
BIN
artifacts/enemy_mine_preview.png
Normal file
BIN
artifacts/enemy_mine_preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
40
artifacts/enemy_mine_preview.png.import
Normal file
40
artifacts/enemy_mine_preview.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cia7d4gk8betx"
|
||||
path="res://.godot/imported/enemy_mine_preview.png-ca9fee582f6cc4883f4e3d003a47c2b2.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://artifacts/enemy_mine_preview.png"
|
||||
dest_files=["res://.godot/imported/enemy_mine_preview.png-ca9fee582f6cc4883f4e3d003a47c2b2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/other/enemy_mine.png
Normal file
BIN
assets/other/enemy_mine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
40
assets/other/enemy_mine.png.import
Normal file
40
assets/other/enemy_mine.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dgdqtbrkivkpt"
|
||||
path="res://.godot/imported/enemy_mine.png-cbe7ead53abadbefaca3c3e913b8b0c4.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/other/enemy_mine.png"
|
||||
dest_files=["res://.godot/imported/enemy_mine.png-cbe7ead53abadbefaca3c3e913b8b0c4.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
5
assets/other/enemy_mine_artwork.md
Normal file
5
assets/other/enemy_mine_artwork.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Enemy mine artwork
|
||||
Generated with the built-in image_gen tool, transparent background.
|
||||
Saved asset: assets/other/enemy_mine.png
|
||||
Final prompt:
|
||||
Use case: stylized-concept. Asset type: production sprite for enemy proximity mine in a top-down realistic sci-fi space shooter. One single symmetrical hostile alien mechanical mine, orthographic directly overhead view, no perspective. Compact circular obsidian gunmetal armored hub, six sharp swept titanium spikes arranged radially, crimson red glowing reactor lens in center and small red luminous nodes at the six spike tips. Premium painted 3D game sprite with convincing machined panels, bevels, restrained scratches, rich deep steel violet metal, crisp readable silhouette when rendered 50 pixels wide. Main hull opaque; completely transparent exterior and gaps between spikes, clean antialiased alpha with restrained tiny red light spill only. Centered square composition with 15% empty transparent margin around entire object. No background, no stars, no ground, no cast shadow outside object, no text, no letters, no frame, no multiple objects, no presentation mockup.
|
||||
@ -7,7 +7,7 @@ radius = 6.0
|
||||
|
||||
[node name="WeaponProjectile" type="Area2D"]
|
||||
collision_layer = 0
|
||||
collision_mask = 13
|
||||
collision_mask = 77
|
||||
monitoring = true
|
||||
monitorable = false
|
||||
script = ExtResource("1_projectile")
|
||||
|
||||
@ -3,19 +3,32 @@ extends Node2D
|
||||
const ARM_TIME := 0.7
|
||||
const LIFETIME := 60.0
|
||||
const TRIGGER_RADIUS := 54.0
|
||||
const DAMAGE_FRACTION := 0.5
|
||||
const MINE_TEXTURE := preload("res://assets/other/enemy_mine.png")
|
||||
var player: CharacterBody2D
|
||||
var drift := Vector2.ZERO
|
||||
var _age := 0.0
|
||||
var _triggered := false
|
||||
var _sprite: Sprite2D
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("enemy_mines")
|
||||
z_index = 12
|
||||
_sprite = Sprite2D.new()
|
||||
_sprite.name = "MineArtwork"
|
||||
_sprite.texture = MINE_TEXTURE
|
||||
_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||||
_sprite.scale = Vector2.ONE * 72.0 / maxf(MINE_TEXTURE.get_width(), MINE_TEXTURE.get_height())
|
||||
add_child(_sprite)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_age += delta
|
||||
position += drift * delta
|
||||
drift = drift.move_toward(Vector2.ZERO, 100.0 * delta)
|
||||
if is_instance_valid(_sprite):
|
||||
_sprite.rotation = _age * 0.16
|
||||
_sprite.modulate = Color.WHITE if _age >= ARM_TIME else Color(0.65, 0.65, 0.7)
|
||||
_sprite.modulate.a = clampf(LIFETIME - _age, 0.0, 1.0)
|
||||
queue_redraw()
|
||||
if _age >= LIFETIME:
|
||||
queue_free()
|
||||
@ -24,7 +37,7 @@ func _physics_process(delta: float) -> void:
|
||||
return
|
||||
if global_position.distance_to(player.global_position) <= TRIGGER_RADIUS:
|
||||
_triggered = true
|
||||
player.call("take_damage", float(player.get("max_hull")) * 0.05, false, DamageTypes.Type.KINETIC)
|
||||
player.call("take_damage", float(player.get("max_hull")) * DAMAGE_FRACTION, false, DamageTypes.Type.KINETIC)
|
||||
var explosion := Node2D.new()
|
||||
explosion.set_script(preload("res://scripts/game/deployment_flash.gd"))
|
||||
explosion.set("color", Color("ff4265"))
|
||||
@ -38,18 +51,8 @@ func _draw() -> void:
|
||||
var pulse := 0.65 + 0.35 * sin(_age * 7.0)
|
||||
var accent := Color("ff4265") if armed else Color("ffb45c")
|
||||
var fade := clampf(LIFETIME - _age, 0.0, 1.0)
|
||||
draw_circle(Vector2.ZERO, 30.0, Color(accent, 0.08 * pulse * fade))
|
||||
var shell := PackedVector2Array()
|
||||
for index in 16:
|
||||
var radius := 24.0 if index % 2 == 0 else 13.0
|
||||
shell.append(Vector2.from_angle(TAU * index / 16.0) * radius)
|
||||
draw_colored_polygon(shell, Color("292236") * Color(1, 1, 1, fade))
|
||||
var outline := shell.duplicate()
|
||||
outline.append(shell[0])
|
||||
draw_polyline(outline, Color(accent, 0.9 * fade), 1.5, true)
|
||||
draw_circle(Vector2.ZERO, 10.0, Color("13101e") * Color(1, 1, 1, fade))
|
||||
draw_arc(Vector2.ZERO, 11.0, 0.0, TAU, 32, Color(accent, fade), 2.0, true)
|
||||
for index in 3:
|
||||
var direction := Vector2.from_angle(-PI * 0.5 + TAU * index / 3.0)
|
||||
draw_line(direction * 4.0, direction * 8.0, Color(accent, pulse * fade), 2.5, true)
|
||||
draw_circle(Vector2.ZERO, 3.0, Color(accent, pulse * fade))
|
||||
# The artwork carries the hull; soft warning rings remain visible on dark space.
|
||||
draw_circle(Vector2.ZERO, 33.0, Color(accent, 0.07 * pulse * fade))
|
||||
draw_circle(Vector2.ZERO, 25.0, Color(accent, 0.1 * pulse * fade))
|
||||
var progress := clampf(_age / ARM_TIME, 0.0, 1.0)
|
||||
draw_arc(Vector2.ZERO, 31.0, -PI * 0.5, -PI * 0.5 + TAU * progress, 64, Color(accent, (0.22 + 0.28 * pulse) * fade), 1.0, true)
|
||||
|
||||
@ -243,13 +243,6 @@ func select_combat_target() -> Node2D:
|
||||
if distance <= maxf(activation_range, weapon_range) and distance < closest_distance:
|
||||
closest = flare
|
||||
closest_distance = distance
|
||||
for ally in get_tree().get_nodes_in_group("virus_allies"):
|
||||
if ally.is_queued_for_deletion() or float(ally.get("hull")) <= 0.0:
|
||||
continue
|
||||
var distance := global_position.distance_to(ally.global_position)
|
||||
if distance <= activation_range and distance < closest_distance:
|
||||
closest = ally
|
||||
closest_distance = distance
|
||||
if closest != null:
|
||||
return closest
|
||||
if is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked")):
|
||||
|
||||
@ -3,7 +3,7 @@ extends Node
|
||||
const PROJECTILE_SCENE := preload("res://scenes/game/weapon_projectile.tscn")
|
||||
const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.tscn")
|
||||
const WEAPON_AUDIO_SCRIPT := preload("res://scripts/game/weapon_audio.gd")
|
||||
const RAY_COLLISION_MASK := 13 # World, enemies, and the weapon test target.
|
||||
const RAY_COLLISION_MASK := 77 # World, enemies, weapon test targets and infected ships.
|
||||
|
||||
@onready var ship: CharacterBody2D = get_parent()
|
||||
@onready var inventory: ShipInventory = ship.get_node("ShipInventory")
|
||||
|
||||
@ -61,8 +61,6 @@ func configure(
|
||||
func _physics_process(delta: float) -> void:
|
||||
var travel_velocity := direction * projectile_speed
|
||||
if weapon_kind == ItemDefinition.WeaponKind.MISSILE and missile_homing:
|
||||
if is_instance_valid(homing_target) and homing_target.is_in_group("virus_allies") and not enemy_shot:
|
||||
homing_target = null
|
||||
_flight_time += delta
|
||||
if not is_instance_valid(homing_target):
|
||||
_has_locked_target = false
|
||||
@ -281,9 +279,9 @@ func _explode_at_target(impact_target: Node) -> void:
|
||||
var query := PhysicsShapeQueryParameters2D.new()
|
||||
query.shape = explosion_shape
|
||||
query.transform = Transform2D(0.0, global_position)
|
||||
# Converted ships fire at hostiles; hostile blasts can also hit converted ships.
|
||||
# Player blasts can damage infected ships; converted fire still targets hostiles.
|
||||
var converted_shot := enemy_shot and (collision_mask & 4) != 0
|
||||
query.collision_mask = 4 if converted_shot or not enemy_shot else 66
|
||||
query.collision_mask = 4 if converted_shot else (66 if enemy_shot else 68)
|
||||
for result in get_world_2d().direct_space_state.intersect_shape(query, 32):
|
||||
_apply_damage_once(result.get("collider"))
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ func get_passive_description() -> String:
|
||||
var value := get_passive_value()
|
||||
match definition.passive_effect:
|
||||
"recycler": return "Każdy zebrany odłamek asteroidy odnawia %.1f%% maksymalnych tarcz i energii." % value
|
||||
"virus": return "Przejmuje uciekających wrogów (poniżej 10%% kadłuba). Zamiast uciekać atakują swoich dawnych sojuszników. Limit jednocześnie kontrolowanych: %d." % int(value)
|
||||
"virus": return "Przejmuje uciekających wrogów (poniżej 10%% kadłuba). Zamiast uciekać atakują swoich dawnych sojuszników. Wrogowie nie wybierają ich jako celu. Nadal możesz uszkodzić i zniszczyć je własną bronią. Limit jednocześnie kontrolowanych: %d." % int(value)
|
||||
"titanium_armor": return "Pochłania 50%% otrzymywanych obrażeń, gdy kadłub ma nie więcej niż %d%% maksimum. Próg sprawdzany przed trafieniem." % int(value)
|
||||
return ""
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
extends StaticBody2D
|
||||
|
||||
const GENERATOR := preload("res://scripts/items/item_generator.gd")
|
||||
const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
|
||||
const ASTEROID_TEXTURES := {
|
||||
"iron": preload("res://assets/icons/resources/iron.png"),
|
||||
"titanium": preload("res://assets/icons/resources/titanium.png"),
|
||||
@ -80,6 +81,16 @@ func _process(delta: float) -> void:
|
||||
if is_instance_valid(_asteroid_sprite) and not _claimed:
|
||||
_asteroid_sprite.rotation += _asteroid_spin * delta
|
||||
|
||||
# One mutually exclusive reward: 65% minerals, 20% ammunition/rockets, 15% module.
|
||||
static func wreck_reward_from_roll(roll: int) -> String:
|
||||
if roll < 65:
|
||||
return "minerals"
|
||||
if roll < 75:
|
||||
return "ammunition"
|
||||
if roll < 85:
|
||||
return "missile"
|
||||
return "module"
|
||||
|
||||
func reward_scale() -> float:
|
||||
return 1.0 + 0.05 * (clampi(GameState.system_level, 1, 1000) - 1)
|
||||
|
||||
@ -137,19 +148,19 @@ func claim() -> bool:
|
||||
summary = "+%d doświadczenia · +1 moduł" % xp
|
||||
_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:
|
||||
match wreck_reward_from_roll(_rng.randi_range(0, 99)):
|
||||
"minerals":
|
||||
var amount := _rng.randi_range(3, 8)
|
||||
GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount
|
||||
summary = "+%d · %s" % [amount, RECIPES.NAMES[_mineral]]
|
||||
"module":
|
||||
_drop_module(false)
|
||||
summary = "Znaleziono moduł"
|
||||
2:
|
||||
"missile":
|
||||
var amount := maxi(1, roundi(scale))
|
||||
inventory.missile_ammunition += amount
|
||||
summary = "+%d rakiet" % amount
|
||||
3:
|
||||
"ammunition":
|
||||
var amount := _rng.randi_range(40, 60)
|
||||
inventory.ammunition += amount
|
||||
summary = "+%d amunicji" % amount
|
||||
|
||||
@ -6,7 +6,7 @@ const PAGES := [
|
||||
["Pierwsza wyprawa", "01", "[font_size=26][color=#f1bd79]Twój pierwszy układ[/color][/font_size]\n\nWybierz statek przyciskiem [b]Nowa wyprawa[/b]. Każdy kadłub ma inne parametry — na początek wybierz ten, który pasuje do Twojego stylu.\n\n[color=#91dbea][b]1. Pokonaj zwykłych wrogów[/b][/color]\nOdszukuj przeciwników, rozwijaj wyposażenie i pilnuj zasobów. Aktualny cel znajdziesz w lewym dolnym rogu ekranu.\n\n[color=#91dbea][b]2. Pokonaj bossa[/b][/color]\nGdy zniszczysz pozostałych wrogów, w układzie pojawi się boss. Przygotuj zapas amunicji i sprawdź kadłub przed walką.\n\n[color=#91dbea][b]3. Znajdź zamieszkaną planetę[/b][/color]\nPo zwycięstwie podleć do zamieszkanej planety. Skorzystaj z usług i otwórz wormhole. Wleć w jego środek, żeby przejść do następnego, trudniejszego układu."],
|
||||
["Sterowanie", "02", "[font_size=26][color=#f1bd79]Opanuj statek[/color][/font_size]\n\n[color=#91dbea][b]W[/b][/color] — ciąg do przodu.\n[color=#91dbea][b]S[/b][/color] — hamowanie; po zatrzymaniu lot wstecz.\n[color=#91dbea][b]A / D[/b][/color] — obracanie statku.\n[color=#91dbea][b]Q / E[/b][/color] — ruch boczny w lewo / prawo.\n[color=#91dbea][b]TAB[/b][/color] — dopalanie: większa prędkość kosztem energii.\n\n[color=#91dbea][b]Kursor myszy[/b][/color] — celowanie.\n[color=#91dbea][b]LPM / PPM / ŚPM[/b][/color] — strzelanie z trzech slotów broni.\n[color=#91dbea][b]1–5[/b][/color] — użycie zamontowanych rozszerzeń.\n[color=#91dbea][b]F1–F5[/b][/color] — aktywne umiejętności kapitana.\n[color=#91dbea][b]R[/b][/color] — użycie powerbanku, jeśli masz go w zapasach.\n\nStatek ma bezwładność. Obrót nie zatrzymuje ruchu — hamuj wcześniej i wykorzystuj ruch boczny, żeby unikać ostrzału."],
|
||||
["Walka i przetrwanie", "03", "[font_size=26][color=#f1bd79]Wróć z wyprawy[/color][/font_size]\n\n[color=#91dbea][b]Tarcze, kadłub i energia[/b][/color]\nTarcze pochłaniają obrażenia przed kadłubem. Utrata całego kadłuba oznacza zniszczenie statku. Energia zasila część broni oraz dopalanie; odnawia się z czasem, szybciej przy gwieździe.\n\n[color=#91dbea][b]Dobieraj broń do zapasów[/b][/color]\nBroń energetyczna zużywa energię. Działko maszynowe potrzebuje zwykłej amunicji, gamma — napromieniowanej, a wyrzutnie — rakiet. Puste zapasy oznaczają brak strzału. Absorpcja ogranicza obrażenia danego typu.\n\n[color=#91dbea][b]Korzystaj z rozszerzeń[/b][/color]\nMiny atakują obszarowo, flary odciągają uwagę, kamuflaż pomaga zerwać kontakt, teleport przenosi w bezpieczne miejsce, a drony wspierają ostrzał. Doładowanie tarcz działa przy gwieździe. Sprawdzaj koszt i cooldown w opisie.\n\n[color=#f1bd79][b]Uważaj na uciekających przeciwników[/b][/color]\nWróg z kadłubem poniżej 10% ucieka wolniej, ale może zostawić minę. Nie leć bezmyślnie prosto za nim."],
|
||||
["Łup i wytwarzanie", "04", "[font_size=26][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Możesz znaleźć zapasy lub wyposażenie.\n\n[color=#91dbea][b]Warsztat — przycisk TWÓRZ[/b][/color]\nZ minerałów wytworzysz części, amunicję, rakiety i powerbanki. Receptura pokazuje wymagane składniki oraz ilość produktu.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
|
||||
["Łup i wytwarzanie", "04", "[font_size=26][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Wraki dają surowce (65%), amunicję lub rakiety (łącznie 20%) albo moduł (15%).\n\n[color=#91dbea][b]Warsztat — przycisk TWÓRZ[/b][/color]\nZ minerałów wytworzysz części, amunicję, rakiety i powerbanki. Receptura pokazuje wymagane składniki oraz ilość produktu.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
|
||||
["Statek i kapitan", "05", "[font_size=26][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: 3 bronie, 5 modułów i 5 rozszerzeń. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Trudniejsze układy dają więcej bazowego XP. Jeśli kapitan przewyższa poziom układu o ponad 2 poziomy, XP za przeciwników i ruiny maleje; przy przewadze 8 poziomów spada do zera. Kredyty i surowce nadal zbierasz. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."]
|
||||
]
|
||||
var _panel: PanelContainer
|
||||
|
||||
@ -113,7 +113,7 @@ func _ready() -> void:
|
||||
assert(first.virus_controlled and not second.virus_controlled)
|
||||
assert(not first.is_in_group("enemies") and first.is_in_group("virus_allies"))
|
||||
assert(not first.is_retreating() and first.select_combat_target() != ship)
|
||||
assert(second.select_combat_target() == first)
|
||||
assert(second.select_combat_target() == ship)
|
||||
await get_tree().physics_frame
|
||||
assert(first.collision_layer == 64)
|
||||
# Controlled fire uses enemy damage, cannot hit the player and can hit hostiles.
|
||||
|
||||
75
tests/abilities/virus_player_damage_test.gd
Normal file
75
tests/abilities/virus_player_damage_test.gd
Normal file
@ -0,0 +1,75 @@
|
||||
extends Node2D
|
||||
const ENEMY = preload("res://scenes/game/enemy_ship.tscn")
|
||||
const SHOT = preload("res://scenes/game/weapon_projectile.tscn")
|
||||
var ship: CharacterBody2D
|
||||
|
||||
func make_enemy(point: Vector2) -> CharacterBody2D:
|
||||
var enemy := ENEMY.instantiate() as CharacterBody2D
|
||||
enemy.position = point
|
||||
add_child(enemy)
|
||||
enemy.set_physics_process(false)
|
||||
enemy.set("_player", ship)
|
||||
return enemy
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.start_new_run()
|
||||
ship = preload("res://scenes/game/player_ship.tscn").instantiate()
|
||||
add_child(ship)
|
||||
ship.set_physics_process(false)
|
||||
ship.get_node("WeaponController").set_physics_process(false)
|
||||
ship.get_node("ExtensionSystems").set_physics_process(false)
|
||||
var infected := make_enemy(Vector2(200, 0))
|
||||
var hostile := make_enemy(Vector2(400, 0))
|
||||
infected.set_virus_controlled(true)
|
||||
infected.hull = infected.max_hull
|
||||
infected.shields = infected.max_shields
|
||||
assert(hostile.select_combat_target() == ship)
|
||||
assert(infected.select_combat_target() == hostile)
|
||||
var xp_after_conversion := GameState.captain.experience
|
||||
await get_tree().physics_frame
|
||||
await get_tree().physics_frame
|
||||
# Actual physics ray used by the player laser detects the converted ship.
|
||||
var query := PhysicsRayQueryParameters2D.create(Vector2(100, 0), Vector2(250, 0), preload("res://scripts/game/weapon_controller.gd").RAY_COLLISION_MASK)
|
||||
var hit := get_world_2d().direct_space_state.intersect_ray(query)
|
||||
assert(hit.get("collider") == infected)
|
||||
# Real swept projectile collision: plasma, bullets and gamma all hit layer 64.
|
||||
for kind in [ItemDefinition.WeaponKind.PLASMA, ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.GAMMA]:
|
||||
infected.hull = infected.max_hull
|
||||
infected.shields = infected.max_shields
|
||||
var shot := SHOT.instantiate()
|
||||
shot.configure(Vector2(100, 0), Vector2.RIGHT, kind, 1000.0, 5.0, 400.0)
|
||||
add_child(shot)
|
||||
shot.set_physics_process(false)
|
||||
var before: float = infected.hull + infected.shields
|
||||
shot._physics_process(0.15)
|
||||
assert(infected.hull + infected.shields < before)
|
||||
# Player rocket splash reaches infected ships; converted rockets cannot hurt player.
|
||||
infected.shields = infected.max_shields
|
||||
var rocket := SHOT.instantiate()
|
||||
rocket.configure(Vector2(220, 0), Vector2.RIGHT, ItemDefinition.WeaponKind.MISSILE, 100.0, 5.0, 400.0)
|
||||
add_child(rocket)
|
||||
rocket.set_physics_process(false)
|
||||
var before: float = infected.hull + infected.shields
|
||||
rocket._explode_at_target(hostile)
|
||||
assert(infected.hull + infected.shields < before)
|
||||
rocket.queue_free()
|
||||
# A missile already tracking this ship retains it after infection.
|
||||
var seeker := SHOT.instantiate()
|
||||
seeker.configure(Vector2(-500, 0), Vector2.RIGHT, ItemDefinition.WeaponKind.MISSILE, 10.0, 5.0, 1000.0, infected)
|
||||
add_child(seeker)
|
||||
seeker.set_physics_process(false)
|
||||
seeker._physics_process(0.01)
|
||||
assert(seeker.homing_target == infected)
|
||||
seeker.queue_free()
|
||||
# Destruction remains possible, with no second XP payout after conversion.
|
||||
infected.shields = 0.0
|
||||
infected.hull = 1.0
|
||||
var lethal := SHOT.instantiate()
|
||||
lethal.configure(Vector2(100, 0), Vector2.RIGHT, ItemDefinition.WeaponKind.MACHINE_GUN, 1000.0, 1000.0, 400.0)
|
||||
add_child(lethal)
|
||||
lethal.set_physics_process(false)
|
||||
lethal._physics_process(0.15)
|
||||
assert(infected.is_queued_for_deletion())
|
||||
assert(GameState.captain.experience == xp_after_conversion)
|
||||
print("VIRUS_PLAYER_DAMAGE_TEST: PASS target selection, laser ray, plasma/bullet/gamma collisions, rocket splash, retained seeker target, destruction and single XP payout")
|
||||
get_tree().quit()
|
||||
1
tests/abilities/virus_player_damage_test.gd.uid
Normal file
1
tests/abilities/virus_player_damage_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cikioub3c81bc
|
||||
4
tests/abilities/virus_player_damage_test.tscn
Normal file
4
tests/abilities/virus_player_damage_test.tscn
Normal file
@ -0,0 +1,4 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://tests/abilities/virus_player_damage_test.gd" id="1"]
|
||||
[node name="VirusPlayerDamageTest" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
@ -53,7 +53,7 @@ func _ready() -> void:
|
||||
var ammo_wrecks := 0
|
||||
for level in [1, 50]:
|
||||
GameState.system_level = level
|
||||
for reward_seed in range(1, 50):
|
||||
for reward_seed in range(1, 101):
|
||||
var poi := StaticBody2D.new()
|
||||
poi.set_script(POI)
|
||||
poi.kind = "wreck"
|
||||
@ -62,9 +62,9 @@ func _ready() -> void:
|
||||
poi.poi_id = "balance:%d:%d" % [level, reward_seed]
|
||||
add_child(poi)
|
||||
var state: int = poi._rng.state
|
||||
var choice: int = poi._rng.randi_range(0, 3)
|
||||
var choice: String = POI.wreck_reward_from_roll(poi._rng.randi_range(0, 99))
|
||||
poi._rng.state = state
|
||||
if choice == 3:
|
||||
if choice == "ammunition":
|
||||
var before := inv.ammunition
|
||||
assert(poi.claim())
|
||||
assert(inv.ammunition - before >= 40 and inv.ammunition - before <= 60)
|
||||
|
||||
@ -89,11 +89,13 @@ func supplies(level: int) -> void:
|
||||
for index in wrecks:
|
||||
if randf() > collect:
|
||||
continue
|
||||
match randi_range(0, 3):
|
||||
0: GameState.captain.parts += roundi(randi_range(3, 8) * scale)
|
||||
1: loot(level, 0, true)
|
||||
2: inv.missile_ammunition += maxi(1, roundi(scale))
|
||||
3: inv.ammunition += roundi(randi_range(40, 60))
|
||||
match preload("res://scripts/levels/exploration_poi.gd").wreck_reward_from_roll(randi_range(0, 99)):
|
||||
"minerals":
|
||||
var mineral: String = RECIPES.MINERALS.pick_random()
|
||||
GameState.captain.minerals[mineral] += randi_range(3, 8)
|
||||
"module": loot(level, 0, true)
|
||||
"missile": inv.missile_ammunition += maxi(1, roundi(scale))
|
||||
"ammunition": inv.ammunition += randi_range(40, 60)
|
||||
# Rare ruins: mean XP, module generation uses the real loot rules.
|
||||
if randf() < 0.08 * collect:
|
||||
GameState.captain.add_rewards(GameState.captain.encounter_xp(15 * scale, level), 0)
|
||||
|
||||
52
tests/levels/enemy_mine_test.gd
Normal file
52
tests/levels/enemy_mine_test.gd
Normal file
@ -0,0 +1,52 @@
|
||||
extends Node2D
|
||||
const MINE = preload("res://scripts/game/enemy_mine.gd")
|
||||
|
||||
func create_mine(ship: CharacterBody2D) -> Node2D:
|
||||
var mine := Node2D.new()
|
||||
mine.set_script(MINE)
|
||||
mine.player = ship
|
||||
mine.position = Vector2.ZERO
|
||||
add_child(mine)
|
||||
mine.set_physics_process(false)
|
||||
return mine
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.start_new_run()
|
||||
var ship := preload("res://scenes/game/player_ship.tscn").instantiate()
|
||||
add_child(ship)
|
||||
ship.set_physics_process(false)
|
||||
ship.get_node("WeaponController").set_physics_process(false)
|
||||
ship.get_node("ExtensionSystems").set_physics_process(false)
|
||||
ship.max_hull = 300.0
|
||||
ship.hull = 210.0
|
||||
ship.shields = 0.0
|
||||
var mine := create_mine(ship)
|
||||
assert(mine.has_node("MineArtwork"))
|
||||
var image := MINE.MINE_TEXTURE.get_image()
|
||||
if image.is_compressed():
|
||||
image.decompress()
|
||||
assert(image.get_pixel(0, 0).a == 0.0)
|
||||
assert(image.get_pixel(image.get_width() / 2, image.get_height() / 2).a > 0.9)
|
||||
mine._physics_process(0.69)
|
||||
assert(ship.hull == 210.0 and not mine._triggered)
|
||||
ship.position = Vector2(MINE.TRIGGER_RADIUS + 1.0, 0.0)
|
||||
mine._physics_process(0.02)
|
||||
assert(ship.hull == 210.0 and not mine._triggered)
|
||||
ship.position = Vector2(MINE.TRIGGER_RADIUS, 0.0)
|
||||
mine._physics_process(0.01)
|
||||
assert(mine._triggered and mine.is_queued_for_deletion())
|
||||
assert(is_equal_approx(ship.hull, 60.0), "Damage must use 50% of maximum hull (150), not current hull")
|
||||
mine._physics_process(0.01)
|
||||
assert(ship.hull == 60.0, "Mine must hit only once")
|
||||
ship.position = Vector2.ZERO
|
||||
ship.max_hull = 200.0
|
||||
ship.hull = 150.0
|
||||
ship.shields = 100.0
|
||||
var shielded_mine := create_mine(ship)
|
||||
shielded_mine._physics_process(0.8)
|
||||
assert(ship.shields == 0.0 and ship.hull == 150.0, "Normal shield absorption remains in effect")
|
||||
var expired := create_mine(ship)
|
||||
expired._physics_process(MINE.LIFETIME)
|
||||
assert(expired.is_queued_for_deletion() and not expired._triggered)
|
||||
print("ENEMY_MINE_TEST: PASS transparent artwork, 50% maximum hull, shields, arming, proximity, one explosion and expiry")
|
||||
get_tree().quit()
|
||||
1
tests/levels/enemy_mine_test.gd.uid
Normal file
1
tests/levels/enemy_mine_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dgiintcrvlgxq
|
||||
4
tests/levels/enemy_mine_test.tscn
Normal file
4
tests/levels/enemy_mine_test.tscn
Normal file
@ -0,0 +1,4 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://tests/levels/enemy_mine_test.gd" id="1"]
|
||||
[node name="EnemyMineTest" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
55
tests/levels/wreck_rewards_test.gd
Normal file
55
tests/levels/wreck_rewards_test.gd
Normal file
@ -0,0 +1,55 @@
|
||||
extends Node2D
|
||||
const POI = preload("res://scripts/levels/exploration_poi.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
var weights := {"minerals": 0, "ammunition": 0, "missile": 0, "module": 0}
|
||||
for roll in 100:
|
||||
weights[POI.wreck_reward_from_roll(roll)] += 1
|
||||
assert(weights == {"minerals": 65, "ammunition": 10, "missile": 10, "module": 15})
|
||||
GameState.start_new_run()
|
||||
GameState.system_level = 1
|
||||
var ship := preload("res://scenes/game/player_ship.tscn").instantiate()
|
||||
add_child(ship)
|
||||
ship.set_physics_process(false)
|
||||
ship.get_node("WeaponController").set_physics_process(false)
|
||||
ship.get_node("ExtensionSystems").set_physics_process(false)
|
||||
var inv: ShipInventory = ship.get_node("ShipInventory")
|
||||
var counts := {"minerals": 0, "ammunition": 0, "missile": 0, "module": 0}
|
||||
for reward_seed in range(1, 251):
|
||||
var poi := StaticBody2D.new()
|
||||
poi.set_script(POI)
|
||||
poi.kind = "wreck"
|
||||
poi.player = ship
|
||||
poi.reward_seed = reward_seed
|
||||
poi.poi_id = "wreck-rewards:%d" % reward_seed
|
||||
add_child(poi)
|
||||
poi.set_physics_process(false)
|
||||
var state: int = poi._rng.state
|
||||
var reward: String = POI.wreck_reward_from_roll(poi._rng.randi_range(0, 99))
|
||||
poi._rng.state = state
|
||||
counts[reward] += 1
|
||||
var minerals_before: Dictionary = GameState.captain.minerals.duplicate()
|
||||
var ammo_before := inv.ammunition
|
||||
var rockets_before := inv.missile_ammunition
|
||||
var mineral: String = poi._mineral
|
||||
assert(poi.claim())
|
||||
assert(not poi.claim(), "A wreck can be claimed only once")
|
||||
assert(GameState.captain.parts == 0.0)
|
||||
if reward == "minerals":
|
||||
var delta: int = GameState.captain.minerals[mineral] - minerals_before[mineral]
|
||||
assert(delta >= 3 and delta <= 8)
|
||||
for key in minerals_before:
|
||||
if key != mineral:
|
||||
assert(GameState.captain.minerals[key] == minerals_before[key])
|
||||
else:
|
||||
assert(GameState.captain.minerals == minerals_before)
|
||||
if reward == "ammunition":
|
||||
assert(inv.ammunition - ammo_before >= 40 and inv.ammunition - ammo_before <= 60)
|
||||
else:
|
||||
assert(inv.ammunition == ammo_before)
|
||||
assert(inv.missile_ammunition - rockets_before == (1 if reward == "missile" else 0))
|
||||
await get_tree().process_frame
|
||||
assert(get_tree().get_nodes_in_group("loot").size() == counts.module, "Every module reward must physically drop one item")
|
||||
assert(counts.minerals > counts.ammunition + counts.missile)
|
||||
print("WRECK_REWARDS_TEST: PASS exact 65/10/10/15 weights, 250 actual wrecks, mineral/ammo amounts, single exclusive rewards, physical modules and no duplicate claims")
|
||||
get_tree().quit()
|
||||
4
tests/levels/wreck_rewards_test.tscn
Normal file
4
tests/levels/wreck_rewards_test.tscn
Normal file
@ -0,0 +1,4 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://tests/levels/wreck_rewards_test.gd" id="1"]
|
||||
[node name="WreckRewardsTest" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
24
tests/previews/enemy_mine_preview.gd
Normal file
24
tests/previews/enemy_mine_preview.gd
Normal file
@ -0,0 +1,24 @@
|
||||
extends "res://scripts/game/game.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
set_physics_process(false)
|
||||
player.set_physics_process(false)
|
||||
player.get_node("WeaponController").set_physics_process(false)
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
enemy.set_physics_process(false)
|
||||
var index := 0
|
||||
for offset in [Vector2(-135, -70), Vector2(130, -90), Vector2(160, 105)]:
|
||||
var mine := Node2D.new()
|
||||
mine.set_script(preload("res://scripts/game/enemy_mine.gd"))
|
||||
mine.position = player.position + offset
|
||||
add_child(mine)
|
||||
mine.set_physics_process(false)
|
||||
mine._physics_process(0.3 if index == 0 else 1.0 + index * 0.25)
|
||||
index += 1
|
||||
if "--capture-preview" in OS.get_cmdline_user_args():
|
||||
await get_tree().create_timer(0.6).timeout
|
||||
await RenderingServer.frame_post_draw
|
||||
get_viewport().get_texture().get_image().save_png("res://artifacts/enemy_mine_preview.png")
|
||||
print("ENEMY_MINE_PREVIEW: captured")
|
||||
get_tree().quit()
|
||||
1
tests/previews/enemy_mine_preview.gd.uid
Normal file
1
tests/previews/enemy_mine_preview.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://8s6rjx62wqdl
|
||||
5
tests/previews/enemy_mine_preview.tscn
Normal file
5
tests/previews/enemy_mine_preview.tscn
Normal file
@ -0,0 +1,5 @@
|
||||
[gd_scene load_steps=3 format=3]
|
||||
[ext_resource type="PackedScene" path="res://scenes/game/game.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://tests/previews/enemy_mine_preview.gd" id="2"]
|
||||
[node name="EnemyMinePreview" instance=ExtResource("1")]
|
||||
script = ExtResource("2")
|
||||
Loading…
x
Reference in New Issue
Block a user