Add scaling Alien Grunt boss waves and native simulation; publish Web release
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
extends SceneTree
|
||||
## Persist native scenes: both native simulation and future Web exports need no MDL plugin.
|
||||
func _initialize() -> void:
|
||||
for stem in ["agrunt", "hornet"]:
|
||||
var path := "res://assets/boss/%s_textured" % stem
|
||||
var scene: PackedScene = load(path + ".mdl")
|
||||
assert(scene != null)
|
||||
var model := scene.instantiate()
|
||||
var surfaces := 0
|
||||
var bounds := AABB()
|
||||
for mesh in model.find_children("*", "MeshInstance3D", true, false):
|
||||
bounds = bounds.merge(mesh.transform * mesh.get_aabb())
|
||||
for i in mesh.mesh.get_surface_count():
|
||||
var material = mesh.mesh.surface_get_material(i)
|
||||
assert(material is StandardMaterial3D and material.albedo_texture != null)
|
||||
surfaces += 1
|
||||
assert(surfaces > 0)
|
||||
var anim: AnimationPlayer = model.get_node_or_null("AnimationPlayer")
|
||||
if anim != null:
|
||||
for label in anim.get_animation_list():
|
||||
var clip := anim.get_animation(label)
|
||||
clip.loop_mode = Animation.LOOP_LINEAR if label in ["idle1", "idle", "walk", "run"] else Animation.LOOP_NONE
|
||||
print(stem, " animations=", anim.get_animation_list())
|
||||
var packed := PackedScene.new()
|
||||
assert(packed.pack(model) == OK)
|
||||
assert(ResourceSaver.save(packed, path + ".scn") == OK)
|
||||
print("NATIVE BOSS VERIFIED ", stem, " surfaces=", surfaces, " bounds=", bounds)
|
||||
model.free()
|
||||
quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://3kptgkh7bn63
|
||||
@@ -1,9 +1,10 @@
|
||||
extends SceneTree
|
||||
func _initialize() -> void:
|
||||
for path in ["res://assets/weapons/v_9mmar", "res://assets/zombies/zombie_textured", "res://assets/weapons/grenade"]:
|
||||
for path in ["res://assets/weapons/v_9mmar", "res://assets/zombies/zombie_textured",
|
||||
"res://assets/weapons/grenade", "res://assets/boss/agrunt_textured",
|
||||
"res://assets/boss/hornet_textured"]:
|
||||
var scene: PackedScene = load(path + ".mdl")
|
||||
assert(scene != null)
|
||||
assert(ResourceSaver.save(scene, path + ".scn") == OK)
|
||||
var node := scene.instantiate()
|
||||
var textured := 0
|
||||
for mesh in node.find_children("*", "MeshInstance3D", true, false):
|
||||
@@ -12,6 +13,19 @@ func _initialize() -> void:
|
||||
assert(material is StandardMaterial3D and material.albedo_texture != null)
|
||||
textured += 1
|
||||
assert(textured > 0)
|
||||
if path.contains("/boss/"):
|
||||
var anim := node.get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||
if anim != null:
|
||||
for label in anim.get_animation_list():
|
||||
anim.get_animation(label).loop_mode = Animation.LOOP_LINEAR if label in [
|
||||
"idle1", "idle", "walk", "run"] else Animation.LOOP_NONE
|
||||
if path.contains("/boss/"):
|
||||
var packed := PackedScene.new()
|
||||
assert(packed.pack(node) == OK)
|
||||
assert(ResourceSaver.save(packed, path + ".scn") == OK)
|
||||
else:
|
||||
# Preserve the established export of unchanged weapons/zombie scenes.
|
||||
assert(ResourceSaver.save(scene, path + ".scn") == OK)
|
||||
print("NATIVE SCENE VERIFIED ", path, " surfaces=", textured)
|
||||
node.free()
|
||||
quit()
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Prepare original Alien Grunt / hornet assets, without changing Steam files.
|
||||
|
||||
Embed the companion texture MDL; keep only self-contained sequence-group-zero
|
||||
animations. External group 1 contains cinematic sequences, not the combat set.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = Path.home() / '.steam/steam/steamapps/common/Half-Life/valve'
|
||||
DEST = ROOT / 'assets/boss'
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def prepare_model(name, companion=None):
|
||||
src = SOURCE / 'models' / (name + '.mdl')
|
||||
body = bytearray(src.read_bytes())
|
||||
assert body[:4] == b'IDST' and struct.unpack_from('<i', body, 4)[0] == 10
|
||||
sources = [{'path': 'models/' + src.name, 'sha256': digest(src)}]
|
||||
if companion:
|
||||
texpath = SOURCE / 'models' / companion
|
||||
tex = bytearray(texpath.read_bytes())
|
||||
base = len(body)
|
||||
count, records, data, refs, families, skins = struct.unpack_from('<6i', tex, 180)
|
||||
assert count > 0
|
||||
for i in range(count):
|
||||
pos = records + i * 80
|
||||
pixel = struct.unpack_from('<i', tex, pos + 76)[0]
|
||||
struct.pack_into('<i', tex, pos + 76, pixel + base)
|
||||
struct.pack_into('<6i', body, 180, count, records + base, data + base,
|
||||
refs, families, skins + base)
|
||||
body.extend(tex)
|
||||
sources.append({'path': 'models/' + companion, 'sha256': digest(texpath)})
|
||||
count, offset = struct.unpack_from('<2i', body, 164)
|
||||
sequences, omitted, used = [], [], set()
|
||||
for i in range(count):
|
||||
record = bytearray(body[offset + 176 * i:offset + 176 * (i + 1)])
|
||||
label = bytes(record[:32]).split(b'\0')[0].decode('ascii')
|
||||
group = struct.unpack_from('<i', record, 156)[0]
|
||||
if group:
|
||||
omitted.append({'index': i, 'label': label, 'external_group': group})
|
||||
continue
|
||||
unique = label
|
||||
suffix = 2
|
||||
while unique in used:
|
||||
unique = label + '_' + str(suffix)
|
||||
suffix += 1
|
||||
used.add(unique)
|
||||
record[:32] = unique.encode().ljust(32, b'\0')
|
||||
sequences.append(record)
|
||||
# All sequence offsets still address the original, unchanged body data.
|
||||
struct.pack_into('<2i', body, 164, len(sequences), len(body))
|
||||
body.extend(b''.join(sequences))
|
||||
struct.pack_into('<i', body, 172, 1)
|
||||
struct.pack_into('<i', body, 72, len(body))
|
||||
out = DEST / (name + '_textured.mdl')
|
||||
out.write_bytes(body)
|
||||
return {'sources': sources, 'destination': str(out.relative_to(ROOT)),
|
||||
'sha256': digest(out), 'animations': sorted(used), 'omitted_sequences': omitted}
|
||||
|
||||
|
||||
def main():
|
||||
DEST.mkdir(parents=True, exist_ok=True)
|
||||
records = [prepare_model('agrunt', 'agruntt.mdl'), prepare_model('hornet')]
|
||||
sounds = ['agrunt/ag_alert1.wav', 'agrunt/ag_attack1.wav', 'agrunt/ag_fire1.wav',
|
||||
'agrunt/ag_idle1.wav', 'agrunt/ag_pain1.wav', 'agrunt/ag_die1.wav',
|
||||
'ambience/port_suckin1.wav', 'ambience/port_suckout1.wav']
|
||||
for relative in sounds:
|
||||
src = SOURCE / 'sound' / relative
|
||||
out = ROOT / 'assets/audio' / relative
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(src, out)
|
||||
assert digest(src) == digest(out)
|
||||
records.append({'source': 'sound/' + relative,
|
||||
'destination': str(out.relative_to(ROOT)), 'sha256': digest(src)})
|
||||
(DEST / 'source_manifest.json').write_text(json.dumps(records, indent=2) + '\n')
|
||||
for r in records:
|
||||
print(r.get('destination'), r['sha256'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# Interactive native-only sandbox: never submits scores or changes the saved name.
|
||||
set -eu
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
GODOT_BIN=${GODOT_BIN:-"$HOME/.local/opt/godot-4.7.1/Godot_v4.7.1-stable_linux.x86_64"}
|
||||
WAVE=${1:-10}
|
||||
if [ "$#" -gt 0 ]; then shift; fi
|
||||
case "$WAVE" in ''|*[!0-9]*) echo 'Usage: tools/run_boss_simulation.sh [10|20|30|...] [--boss-god]' >&2; exit 2;; esac
|
||||
if [ "$WAVE" -lt 10 ] || [ $((WAVE % 10)) -ne 0 ]; then
|
||||
echo 'Boss wave must be a positive multiple of ten (10, 20, 30, ...).' >&2
|
||||
exit 2
|
||||
fi
|
||||
exec "$GODOT_BIN" --path "$ROOT" --rendering-method gl_compatibility -- --boss-wave="$WAVE" "$@"
|
||||
@@ -0,0 +1,47 @@
|
||||
extends SceneTree
|
||||
## Deterministic visual review in the real Compatibility renderer.
|
||||
const Boss = preload("res://scripts/alien_grunt.gd")
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
func run_test() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var world = load("res://scenes/main.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
world.start_screen.hide()
|
||||
world.spawning = true
|
||||
world.player.set_physics_process(false)
|
||||
world.player.position = Vector3(0, 0.1, 3.5)
|
||||
world.player.camera.look_at(Vector3(0, 1.2, -3))
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
await physics_frame
|
||||
var actor := Boss.new()
|
||||
actor.configure(10)
|
||||
actor.player = world.player
|
||||
actor.navigation = world.navigation
|
||||
actor.position = Vector3(0, 0.05, -3)
|
||||
actor.rotation.y = PI
|
||||
world.add_child(actor)
|
||||
actor.set_physics_process(false)
|
||||
world.boss = actor
|
||||
world.wave = 10
|
||||
var portal = load("res://scripts/boss_teleport.gd").new()
|
||||
portal.position = actor.position
|
||||
portal.materialized.connect(actor.materialize)
|
||||
world.add_child(portal)
|
||||
await create_timer(1.0).timeout
|
||||
await RenderingServer.frame_post_draw
|
||||
root.get_texture().get_image().save_png("/tmp/zombie-boss-portal.png")
|
||||
await create_timer(0.8).timeout
|
||||
await RenderingServer.frame_post_draw
|
||||
root.get_texture().get_image().save_png("/tmp/zombie-boss-arrival.png")
|
||||
await create_timer(1.8).timeout
|
||||
actor.anim.play("hornetattack")
|
||||
await create_timer(0.3).timeout
|
||||
actor._fire_hornet()
|
||||
await RenderingServer.frame_post_draw
|
||||
root.get_texture().get_image().save_png("/tmp/zombie-boss-combat.png")
|
||||
print("SHOT: /tmp/zombie-boss-{portal,arrival,combat}.png")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bifk8i65ile77
|
||||
@@ -0,0 +1,223 @@
|
||||
extends SceneTree
|
||||
const Boss = preload("res://scripts/alien_grunt.gd")
|
||||
const Hornet = preload("res://scripts/boss_hornet.gd")
|
||||
const Main = preload("res://scripts/main.gd")
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
var killed_events := 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
func check(ok: bool, label: String) -> void:
|
||||
checks += 1
|
||||
if not ok:
|
||||
failures += 1
|
||||
print("PASS: " if ok else "FAIL: ", label)
|
||||
|
||||
func make_world() -> Node3D:
|
||||
var world = load("res://scenes/main.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
world.player.hp = 100000
|
||||
world.player.set_physics_process(false)
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
return world
|
||||
|
||||
func target_body(world: Node3D, at: Vector3, size: Vector3) -> StaticBody3D:
|
||||
var body := StaticBody3D.new()
|
||||
body.position = at
|
||||
var collider := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = size
|
||||
collider.shape = shape
|
||||
body.add_child(collider)
|
||||
world.add_child(body)
|
||||
return body
|
||||
|
||||
func profile_snapshot() -> String:
|
||||
if not FileAccess.file_exists(Main.Profile.PATH):
|
||||
return "ABSENT"
|
||||
return FileAccess.get_sha256(Main.Profile.PATH)
|
||||
|
||||
func run_test() -> void:
|
||||
var profile_before := profile_snapshot()
|
||||
check(not Main.is_boss_wave(0) and not Main.is_boss_wave(9) and Main.is_boss_wave(10)
|
||||
and not Main.is_boss_wave(11) and Main.is_boss_wave(20), "boss solo ogni dieci ondate")
|
||||
var a := Boss.stats_for_wave(10)
|
||||
var b := Boss.stats_for_wave(20)
|
||||
var c := Boss.stats_for_wave(30)
|
||||
check(a.rank == 1 and b.rank == 2 and c.rank == 3 and a.hp == 800 and b.hp == 1200
|
||||
and c.hp == 1600, "grado e salute crescono a ogni comparsa")
|
||||
check(a.melee < b.melee and b.melee < c.melee and a.ranged < b.ranged
|
||||
and b.ranged < c.ranged and a.speed < b.speed and b.interval < a.interval,
|
||||
"danni, velocita' e cadenza crescono")
|
||||
var late := Boss.stats_for_wave(10000)
|
||||
check(late.speed <= 3.4 and late.interval >= 1.3, "mobilita' e cadenza hanno limiti giocabili")
|
||||
var world = make_world()
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
check(world.zombies_for_wave(9) == 22 and world.zombies_for_wave(10) == 0
|
||||
and world.zombies_for_wave(11) == 26, "ondata boss sostituisce gli zombie, non si somma")
|
||||
for i in range(16):
|
||||
var at: Vector3 = world._boss_spawn_position()
|
||||
check(at.is_finite() and at.distance_to(world.player.global_position) >= 5,
|
||||
"portale libero e distante: estrazione %d" % (i + 1))
|
||||
# Real 9 -> 10 transition: finish all 22 ordinary enemies before the boss.
|
||||
world.wave = 8
|
||||
world.start_game("Test", false)
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
check(world.wave == 9 and world.boss == null and world.spawning,
|
||||
"ondata 9 ancora ordinaria")
|
||||
await create_timer(18.0).timeout
|
||||
check(world.alive == 22 and not world.spawning, "ondata 9 completa: 22 zombie, nessun boss")
|
||||
for zombie in get_nodes_in_group("live_zombies"):
|
||||
zombie.take_damage(1000000, zombie.global_position.y + 0.9)
|
||||
await create_timer(3.2).timeout
|
||||
var actor = world.boss
|
||||
var score_before_boss: int = world.score
|
||||
check(world.wave == 10 and world.alive == 1 and not world.spawning
|
||||
and get_nodes_in_group("live_zombies").size() == 1
|
||||
and actor.is_in_group("bosses"), "ondata 10: esattamente un boss e nessuno zombie ordinario")
|
||||
check(actor.entering and not actor.model.visible and actor.collision_layer == 0,
|
||||
"preavviso teletrasporto: boss non ancora attivo")
|
||||
var hp_before: float = actor.hp
|
||||
actor.take_damage(100, actor.position.y + 1)
|
||||
world._check_wave()
|
||||
check(actor.hp == hp_before and not world.wave_pending, "invulnerabile durante arrivo; niente salto ondata")
|
||||
await create_timer(1.65).timeout
|
||||
actor.set_physics_process(false)
|
||||
check(not actor.entering and actor.model.visible and actor.collision_layer == 4,
|
||||
"teletrasporto materializza modello e hitbox")
|
||||
check(actor.anim.has_animation("walk") and actor.anim.has_animation("hornetattack")
|
||||
and actor.anim.has_animation("mattack2") and actor.anim.has_animation("diesimple"),
|
||||
"animazioni di combattimento e morte originali presenti")
|
||||
var surfaces := 0
|
||||
var textured := true
|
||||
for mesh in actor.model.find_children("*", "MeshInstance3D", true, false):
|
||||
for i in mesh.mesh.get_surface_count():
|
||||
var material = mesh.mesh.surface_get_material(i)
|
||||
textured = textured and material is StandardMaterial3D and material.albedo_texture != null
|
||||
surfaces += 1
|
||||
check(textured and surfaces > 0, "modello originale con texture incorporate, scena nativa")
|
||||
# Stable open lane, actual hitscan and grenade blast (same live_zombies interface).
|
||||
actor.position = Vector3(0, 0.05, -3)
|
||||
world.player.position = Vector3(0, 0.1, 2)
|
||||
world.player.camera.look_at(actor.global_position + Vector3.UP * 1.2)
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
hp_before = actor.hp
|
||||
world.player.shoot()
|
||||
check(actor.hp < hp_before, "fucile: il boss riceve i colpi tramite la normale hitbox")
|
||||
var grenade = load("res://scripts/grenade.gd").new()
|
||||
grenade.thrower = world.player
|
||||
grenade.position = actor.position + Vector3(0, 0.4, 1)
|
||||
world.add_child(grenade)
|
||||
grenade.freeze = true
|
||||
hp_before = actor.hp
|
||||
grenade._request_detonation(grenade.global_position)
|
||||
await process_frame
|
||||
await process_frame
|
||||
check(actor.hp < hp_before, "granata: il boss riceve il danno esplosivo")
|
||||
world.hud.update_boss(actor)
|
||||
check(world.hud.boss_panel.visible and world.hud.boss_health.value == actor.hp
|
||||
and world.hud.boss_health.max_value == actor.max_hp, "barra vita del boss segue i danni")
|
||||
# Real hornet flight towards player, with a separate obstruction regression.
|
||||
var player_hp: float = world.player.hp
|
||||
actor._fire_hornet()
|
||||
check(get_nodes_in_group("boss_projectiles").size() == 1, "attacco alieno crea un proiettile visibile")
|
||||
await create_timer(0.8).timeout
|
||||
check(world.player.hp < player_hp, "proiettile alieno colpisce e danneggia il giocatore")
|
||||
var wall := target_body(world, Vector3(0, 1.4, -0.5), Vector3(3, 3, 0.15))
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
player_hp = world.player.hp
|
||||
actor._fire_hornet()
|
||||
await create_timer(0.8).timeout
|
||||
check(world.player.hp == player_hp and get_nodes_in_group("boss_projectiles").is_empty(),
|
||||
"parete ferma i proiettili: niente colpi attraverso gli oggetti")
|
||||
wall.queue_free()
|
||||
await process_frame
|
||||
# Melee cannot reach through the existing desk.
|
||||
world.player.position = Vector3(-5, 0.1, -3.2)
|
||||
actor.position = Vector3(-5, 0.05, -4.8)
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
player_hp = world.player.hp
|
||||
actor._attack()
|
||||
check(world.player.hp == player_hp, "corpo a corpo bloccato dalla scrivania")
|
||||
actor.position = Vector3(0, 0.05, -1.5)
|
||||
world.player.position = Vector3(0, 0.1, 0)
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
actor._attack()
|
||||
check(world.player.hp < player_hp, "corpo a corpo infligge danno a distanza utile")
|
||||
# AI pathfinding actually moves around an office desk, without teleport tricks.
|
||||
var routes := [
|
||||
[Vector3(-5, 0.05, -6.5), Vector3(-5, 0.1, 0)],
|
||||
[Vector3(5, 0.05, 1.5), Vector3(5, 0.1, 7.5)],
|
||||
[Vector3(-6, 0.05, 4), Vector3(-2, 0.1, 4)],
|
||||
[Vector3(6.8, 0.05, -6), Vector3(6.8, 0.1, -2)],
|
||||
]
|
||||
for route in routes:
|
||||
world.player.position = route[1]
|
||||
actor.position = route[0]
|
||||
actor.cooldown = 100
|
||||
actor.attack_lock = 0
|
||||
actor._nav_started = false
|
||||
actor._repath = 0
|
||||
actor.set_physics_process(true)
|
||||
var reached := false
|
||||
for i in range(650):
|
||||
await physics_frame
|
||||
if actor.position.distance_to(world.player.position) < 2.3:
|
||||
reached = true
|
||||
break
|
||||
actor.set_physics_process(false)
|
||||
check(reached, "boss aggira gli arredi: %s -> %s" % [route[0], route[1]])
|
||||
# A queued burst must never fire after death.
|
||||
actor._start_burst()
|
||||
actor.killed.connect(func(_head): killed_events += 1)
|
||||
actor.take_damage(1000000, actor.global_position.y + 1)
|
||||
actor.take_damage(1000000, actor.global_position.y + 1)
|
||||
check(killed_events == 1 and world.alive == 0 and world.score == score_before_boss + actor.reward
|
||||
and world.wave_pending, "morte unica, ricompensa unica e completamento ondata boss")
|
||||
world.hud.update_boss(actor)
|
||||
check(not world.hud.boss_panel.visible and actor.collision_layer == 0
|
||||
and not actor.is_in_group("live_zombies"), "morto: HUD nascosto, corpo non blocca e non conta vivo")
|
||||
await create_timer(0.8).timeout
|
||||
check(get_nodes_in_group("boss_projectiles").is_empty(), "raffica pendente annullata dalla morte")
|
||||
await create_timer(2.5).timeout
|
||||
check(world.wave == 11 and world.boss == null and world.alive > 0
|
||||
and get_nodes_in_group("bosses").is_empty(), "10 -> 11: riprendono gli zombie normali una sola volta")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
# Native sandbox: no start menu/network/profile; stops after the selected boss.
|
||||
world = load("res://scenes/main.tscn").instantiate()
|
||||
world.boss_simulation = true
|
||||
world.simulation_wave = 20
|
||||
root.add_child(world)
|
||||
world.player.hp = 100000
|
||||
world.player.set_physics_process(false)
|
||||
await create_timer(1.8).timeout
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
actor = world.boss
|
||||
actor.set_physics_process(false)
|
||||
check(world.wave == 20 and actor.rank == 2 and actor.max_hp == 1200
|
||||
and world.start_screen == null and world.leaderboard.disabled
|
||||
and world.leaderboard._pending.is_empty(), "simulazione diretta grado 2, senza menu/classifica")
|
||||
actor.take_damage(1000000, actor.position.y + 1)
|
||||
await create_timer(3.2).timeout
|
||||
check(world.simulation_finished and world.simulation_retry.visible and world.wave == 20
|
||||
and world.alive == 0, "simulazione termina nella singola ondata; pulsante RIPROVA")
|
||||
world.player.invulnerable = true
|
||||
world.player.take_damage(1000000)
|
||||
check(world.player.hp > 0, "opzione invulnerabile per prove native")
|
||||
world.player.invulnerable = false
|
||||
world.player.take_damage(1000000)
|
||||
check(world.leaderboard._pending.is_empty() and not world.score_submitted
|
||||
and world.profile.best == 0, "morte in simulazione non invia punteggi e non aggiorna il record")
|
||||
check(profile_before == profile_snapshot(), "profilo persistente byte-identico dopo i test")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
print(checks - failures, " controlli superati, ", failures, " falliti (", checks, " totali)")
|
||||
quit(1 if failures else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmkobdmpjqiai
|
||||
Reference in New Issue
Block a user