extends Node ## Boot the client windowed, capture a few screens, and quit. ## ## SHOT_DIR=/tmp/shots godot --path . res://tools/screenshot.tscn ## ## This exists because UI is the one part of the game the rest of the tooling ## cannot check. `check.sh` proves it parses, the suite proves the theme ## resolves, the smoke test proves nothing errors -- and all three passed ## while the entire interface rendered unstyled, because Godot's theme ## inheritance breaks at the first non-Control parent and nothing had ever ## looked at the screen. ## ## Needs a real display, so it is not part of any automated gate. Run it after ## touching anything under src/ui/ and open the files. const GAME := preload("res://src/game.tscn") ## Frame numbers, not seconds: the point is a deterministic sequence, and a ## windowed client at 60fps gets there in about four seconds. const AT_MENU := 30 const ENTER_GAME := 34 const AT_GAME := 200 const OPEN_MENU := 210 const AT_INGAME_MENU := 240 const OPEN_SETTINGS := 250 const AT_SETTINGS := 280 const OPEN_CREDITS := 290 const AT_CREDITS := 320 const OPEN_UPGRADES := 330 const AT_UPGRADES := 360 const OPEN_ROSTER := 370 const AT_ROSTER := 400 const DONE := 410 var _frame: int = 0 var _game: Node = null var _menu: Control = null var _dir: String = "user://" func _ready() -> void: GameOpts.parse() Engine.physics_ticks_per_second = SimConfig.TICK_RATE Settings.load_and_apply() var from_env := OS.get_environment("SHOT_DIR") if not from_env.is_empty(): _dir = from_env get_window().size = Vector2i(1280, 720) _menu = preload("res://src/ui/main_menu.gd").new() add_child(_menu) ## Put the player at the quartermaster on BOTH sides. Setting only the client's ## predicted position does not hold: reconciliation pulls it back to whatever ## the server says within a tick or two, the game scene notices it is no longer ## at the NPC, and closes the screen again. func _stand_at_the_npc() -> void: var npc: Vector2 = Net.client.upgrade_npc Net.client.predicted_pos = npc var inst := Net.server.instance_of(Net.LOCAL_PEER) if inst != null: var p: SimPlayer = inst.world.players.get(Net.LOCAL_PEER) if p != null: p.pos = npc _game.set("_upgrades_open", true) func _process(_delta: float) -> void: _frame += 1 # Held until the shot is taken; one frame of it is not enough. if _frame > OPEN_UPGRADES and _frame <= AT_UPGRADES: _stand_at_the_npc() # Same reason: the game scene owns this screen's visibility too and decides # it every frame, so one assignment lasts exactly one frame. if _frame > OPEN_ROSTER and _frame <= AT_ROSTER: _show_the_roster() match _frame: AT_MENU: _grab("menu") ENTER_GAME: _menu.queue_free() Net.host(27455) Net.start_local_client() _game = GAME.instantiate() add_child(_game) AT_GAME: _grab("game") OPEN_MENU: _game.get_node("GameMenu").open() AT_INGAME_MENU: _grab("menu_ingame") OPEN_SETTINGS: _game.get_node("GameMenu").close() _game.get_node("SettingsScreen").open() AT_SETTINGS: _grab("settings") OPEN_CREDITS: _game.get_node("SettingsScreen").visible = false _game.get_node("CreditsScreen").open() AT_CREDITS: _grab("credits") OPEN_UPGRADES: # Driven with a made-up offer rather than by earning one: the point # here is the card layout, and levelling a character up first would # make the shot depend on a dungeon run. _game.get_node("CreditsScreen").visible = false # The game scene owns this screen's visibility and re-asserts it # every frame, closing it unless the player is at the NPC -- so # setting `visible` directly lasted exactly one frame. Stand the # player at the quartermaster and open it the way the game does. _stand_at_the_npc() var offer: Array[StringName] = [ Upgrades.SPLIT_SHOT, Upgrades.POISON, Upgrades.ERASER] var taken: Array[StringName] = [Upgrades.SNIPER, Upgrades.SPREAD] _game.get_node("UpgradeScreen").refresh(2, offer, taken) AT_UPGRADES: _grab("upgrades") OPEN_ROSTER: _show_the_roster() AT_ROSTER: _grab("roster") DONE: Net.shutdown() get_tree().quit(0) ## Put the roster on screen with a made-up set of characters. ## ## Made up for the same reason the upgrade offer is: the point is the layout, ## and earning three characters at three different health levels first would ## make the shot depend on a dungeon run. One whole, one badly hurt, one barely ## alive -- health survives a run now, so the wounded rows are the reason this ## screen is worth a picture at all. ## ## Walking off the quartermaster first is not optional: the game scene re-opens ## the upgrade screen every frame the player is standing on the NPC, and it ## draws over this one. func _show_the_roster() -> void: var away: Vector2 = Net.client.upgrade_npc + Vector2(600.0, 0.0) Net.client.predicted_pos = away var inst := Net.server.instance_of(Net.LOCAL_PEER) if inst != null: var p: SimPlayer = inst.world.players.get(Net.LOCAL_PEER) if p != null: p.pos = away _game.set("_upgrades_open", false) _game.set("_roster_open", true) var roster: Array[Dictionary] = [ _roster_entry("Whole", 4, 1300, 1300, Color(0.5, 0.9, 1.0)), _roster_entry("Bruised", 7, 1600, 940, Color(1.0, 0.8, 0.4)), _roster_entry("Nearly", 11, 2000, 160, Color(1.0, 0.5, 0.5)), ] _game.get_node("CharacterSelect").refresh(roster, "c4") ## One roster row in the shape decode_characters produces. func _roster_entry(who: String, level: int, ceiling: int, current: int, tint: Color) -> Dictionary: return { "id": "c%d" % level, "name": who, "level": level, "xp": 0, "progress": 0.4, "max_hp": ceiling, "hp": current, "active": true, "colour": tint, "inventory": [0, 0, 0, 0] as Array[int], } func _grab(shot_name: String) -> void: var path := "%s/shot_%s.png" % [_dir, shot_name] var err := get_viewport().get_texture().get_image().save_png(path) if err != OK: printerr("could not write %s (error %d)" % [path, err]) return print("saved ", path)