A UI pass: prompts that tell the truth, and a HUD that does not collide
ci / verify (push) Successful in 53s
ci / verify (push) Successful in 53s
Prompts named the default keys whatever was bound. Every action is rebindable, but the HUD said "WASD move", "LMB fire", "hold F" and "1-4 use", the quartermaster said "E talk" and the upgrade screen "Close (E)". All of them now go through Settings.short_label(), and a test rebinds fire, escape, interact and a slot key and checks each prompt follows. The HUD's top-left was a developer's status line -- name, instance id, health as text, frame rate -- with the boss bar drawn over its right end and the level-up notice sharing a row with the escape bar. It is now name and level, where you are, health with its numbers, experience, escape and level-ups, stacked by a running cursor so nothing has a fixed row to collide on. Instance and fps moved behind F1, which is who they were for. Arrival protection is a centred banner rather than a note beside the health bar, because it is the one moment the rules change. A hit used to tint the whole screen red, which in a bullet hell dims the bullets at exactly the moment you are among them. It is an edge now, and the same edge pulses below a third of your health. New: a message feed above the inventory for what the server just confirmed -- items taken, used and dropped, levels gained -- and party health top-right in a dungeon, where the hub shows who is online. The pickup prompt now also names the dungeon under your feet when you stand on its ring, in the same order the server resolves that key: loot first, then the entrance. The boss bar used to announce the boss to anyone within the actor interest radius -- which passes through walls -- so a player in the entrance room was told the Warden was near. It waits until the fog has actually lit the boss once, then stays for the fight. Permadeath was announced only by the roster screen appearing. The client records its own death from the server's event and the roster says who fell, at what level, and where. The connect screen lost a name field that did nothing for a person -- it only named the characters bots create -- gained a proper panel, remembers the last server, and shows the protocol number a mismatch refusal will quote. And screenshot.tscn ran as the player's real account against their real character store, walking their character into dungeons for pictures -- with health now carried between runs, a stray hit would have stuck. It has its own store and account now, like every diagnostic, and a seeded character. The real store was checked: nothing it carries changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
069abdb3fa
commit
19d2295bec
14 files changed
+903
-229
No files matched your search
+96
-1
@@ -14,6 +14,14 @@ extends Node
|
||||
## touching anything under src/ui/ and open the files.
|
||||
|
||||
const GAME := preload("res://src/game.tscn")
|
||||
const GAME_SCENE := preload("res://src/view/game_scene.gd")
|
||||
## Its own store and its own account, like every diagnostic. Without these the
|
||||
## tool hosted a listen server on the player's REAL character store as the
|
||||
## player's REAL account, and walked their character into dungeons to take
|
||||
## pictures -- with health carried between runs, a stray hit would have stuck,
|
||||
## and a death would have retired someone's character for a screenshot.
|
||||
const STORE := "user://screenshot_characters.json"
|
||||
const ACCOUNT := 777001
|
||||
## 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
|
||||
@@ -34,7 +42,9 @@ const AT_DUNGEON := 480
|
||||
const TO_TUNNEL := 490
|
||||
const AT_TUNNEL := 550
|
||||
const AT_TUNNEL_DEBUG := 560
|
||||
const DONE := 570
|
||||
const TO_BOSS := 570
|
||||
const AT_BOSS := 640
|
||||
const DONE := 650
|
||||
|
||||
var _frame: int = 0
|
||||
var _game: Node = null
|
||||
@@ -44,6 +54,9 @@ var _dir: String = "user://"
|
||||
|
||||
func _ready() -> void:
|
||||
GameOpts.parse()
|
||||
GameOpts.store_path = STORE
|
||||
GameOpts.account_override = ACCOUNT
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(STORE))
|
||||
Engine.physics_ticks_per_second = SimConfig.TICK_RATE
|
||||
Settings.load_and_apply()
|
||||
var from_env := OS.get_environment("SHOT_DIR")
|
||||
@@ -78,16 +91,24 @@ func _process(_delta: float) -> void:
|
||||
# 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()
|
||||
# On an entrance for the hub shot, so its prompt is in the picture.
|
||||
if _frame > AT_GAME - 40 and _frame <= AT_GAME:
|
||||
_stand_on_a_portal()
|
||||
# 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()
|
||||
# Held low, so the low-health edge is in the picture -- and held at all,
|
||||
# because the boss is firing and one landed ring would end the shot.
|
||||
if _frame > TO_BOSS and _frame <= AT_BOSS:
|
||||
_hold_health(0.22)
|
||||
match _frame:
|
||||
AT_MENU:
|
||||
_grab("menu")
|
||||
ENTER_GAME:
|
||||
_menu.queue_free()
|
||||
Net.host(27455)
|
||||
_seed_character()
|
||||
Net.start_local_client()
|
||||
_game = GAME.instantiate()
|
||||
add_child(_game)
|
||||
@@ -131,6 +152,7 @@ func _process(_delta: float) -> void:
|
||||
# The fog is what these two are for, and the hub is the worst place to
|
||||
# judge it: one big room with little in it to cast a shadow.
|
||||
_game.set("_roster_open", false)
|
||||
_game.get_node("CharacterSelect").set_fallen("")
|
||||
Net.server._send_to_dungeon(Net.LOCAL_PEER)
|
||||
AT_DUNGEON:
|
||||
_grab("dungeon")
|
||||
@@ -144,6 +166,11 @@ func _process(_delta: float) -> void:
|
||||
_game.get_node("WorldView").debug_hitboxes = true
|
||||
AT_TUNNEL_DEBUG:
|
||||
_grab("tunnel_debug")
|
||||
TO_BOSS:
|
||||
_game.get_node("WorldView").debug_hitboxes = false
|
||||
_stage_a_boss_fight()
|
||||
AT_BOSS:
|
||||
_grab("boss_fight")
|
||||
DONE:
|
||||
Net.shutdown()
|
||||
get_tree().quit(0)
|
||||
@@ -170,6 +197,9 @@ func _show_the_roster() -> void:
|
||||
p.pos = away
|
||||
_game.set("_upgrades_open", false)
|
||||
_game.set("_roster_open", true)
|
||||
# A death, as it is announced on the screen a death sends you to.
|
||||
_game.get_node("CharacterSelect").set_fallen(GAME_SCENE.fallen_line(
|
||||
{"name": "Quillfell", "level": 9, "where": "The Choir Vault"}))
|
||||
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)),
|
||||
@@ -178,6 +208,71 @@ func _show_the_roster() -> void:
|
||||
_game.get_node("CharacterSelect").refresh(roster, "c4")
|
||||
|
||||
|
||||
## A character worth photographing: levelled, carrying things, so the HUD has
|
||||
## something in every slot and a bar that is not empty. Made in the scratch
|
||||
## store before the local client says hello, so it is who the client resumes.
|
||||
func _seed_character() -> void:
|
||||
var store := Net.server.store
|
||||
var c := store.create_character(ACCOUNT, "Ash")
|
||||
store.grant_xp(ACCOUNT, c.id, Progression.total_xp_for_level(6) + 260)
|
||||
var bag: Array[StringName] = [Items.HEALTH_POTION, Items.WARDENS_RATION,
|
||||
Items.HEALTH_POTION, Items.NONE]
|
||||
store.set_inventory(ACCOUNT, c.id, bag)
|
||||
store.flush()
|
||||
|
||||
|
||||
func _stand_on_a_portal() -> void:
|
||||
if Net.client == null or Net.client.portals.is_empty():
|
||||
return
|
||||
var at: Vector2 = Net.client.portals[0]["pos"]
|
||||
Net.client.predicted_pos = at
|
||||
var p := _player()
|
||||
if p != null:
|
||||
p.pos = at
|
||||
|
||||
|
||||
func _player() -> SimPlayer:
|
||||
var inst := Net.server.instance_of(Net.LOCAL_PEER)
|
||||
return inst.world.players.get(Net.LOCAL_PEER) if inst != null else null
|
||||
|
||||
|
||||
func _hold_health(share: float) -> void:
|
||||
var p := _player()
|
||||
if p != null:
|
||||
p.hp = maxi(1, int(float(p.max_hp) * share))
|
||||
|
||||
|
||||
## Into the boss arena, low on health, with a party member beside you and a
|
||||
## few messages in the feed: everything on the HUD that only exists mid-fight,
|
||||
## in one picture.
|
||||
##
|
||||
## The party member is a world player with no socket behind it -- added to the
|
||||
## simulation and the roster, never to the instance's send list, so the server
|
||||
## never tries to talk to it.
|
||||
func _stage_a_boss_fight() -> void:
|
||||
var inst := Net.server.instance_of(Net.LOCAL_PEER)
|
||||
if inst == null:
|
||||
return
|
||||
var at := inst.boss_spawn + Vector2(-200.0, 60.0)
|
||||
Net.client.predicted_pos = at
|
||||
var me := _player()
|
||||
if me != null:
|
||||
me.pos = at
|
||||
me.spawn_grace = 0
|
||||
var friend := inst.world.add_player(999, "Wren")
|
||||
friend.pos = at + Vector2(-40.0, -70.0)
|
||||
friend.colour = Color(1.0, 0.75, 0.45)
|
||||
friend.hp = int(float(friend.max_hp) * 0.6)
|
||||
friend.spawn_grace = 0
|
||||
Net.server.peer_names[999] = "Wren"
|
||||
Net.server.peer_instance[999] = inst.id
|
||||
Net.server._broadcast_roster()
|
||||
var hud := _game.get_node("HUD")
|
||||
hud.notify("+ Health Potion", Items.get_def(Items.HEALTH_POTION).tint)
|
||||
hud.notify("Warden's Ration used", Items.get_def(Items.WARDENS_RATION).tint)
|
||||
hud.notify("LEVEL 7 -- a choice waits at the quartermaster", Color(1.0, 0.85, 0.4))
|
||||
|
||||
|
||||
## Move the player, on both sides, into the tunnel closest to where it spawned:
|
||||
## somewhere with walls close on two sides and a corner in view, which is where
|
||||
## the old fog looked worst.
|
||||
|
||||
Reference in new issue
Block a user