a943aa19f6
ci / verify (push) Successful in 47s
Two labelled portals now stand side by side in the hub. The Proving Grounds runs the same generator, the same rooms, the same enemies and the same four-phase Warden -- enemies at a fifth health, the boss at 288 instead of 3600, and trash dropping potions 80% of the time instead of 8%. A manual pass over loot, the inventory, dropping and every boss phase takes a couple of minutes rather than a quarter of an hour. It is multipliers over the shared content rather than a parallel copy: a duplicated Content would drift the first time anything was tuned, and "identical but easier" would quietly stop being true. And it is a portal rather than a launch flag, so the two can be compared back to back without restarting the server -- which is most of the point. Which dungeon you enter is resolved from the player's server-side position, and PORTAL_USED carries the answer. There is deliberately no client message that names a dungeon: one would let any client ask for the generous loot table and bring the results back to the hub. Instance matching compares dungeon ids too, so walking into one entrance can never drop you into the other's run on timing alone. SimWorld.portals replaces portal_pos/portal_enabled, enter_instance carries the portal list and the dungeon id (the client needs the latter to scale the boss bar's ceiling the way the server scaled the boss), and Protocol.VERSION goes to 7. Also pins what happens when two players reach for one item on the same tick: exactly one gets it -- the loop is sequential and the pickup erases the entity before the next player looks. The tie-break is join order rather than distance, which is arbitrary rather than designed, so it is recorded as such. Stale doc fixed while here: MapGen.build() still claimed the client rebuilds the map from the seed, which has not been true since map streaming landed and is the opposite of the rule. check.sh clean, 288 tests, SMOKE PASS (18 assertions, both dungeon kinds opened over a real socket), all three diagnostics green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
2.5 KiB
GDScript
81 lines
2.5 KiB
GDScript
class_name Dungeons
|
|
extends RefCounted
|
|
## The kinds of dungeon run, defined in code like everything else in
|
|
## `src/content/`.
|
|
##
|
|
## [constant ORDER] does double duty: it is the wire order for the dungeon id,
|
|
## and it is the order the hub's portals are assigned. The Nth `P` marker in the
|
|
## lobby stamp (reading order) opens the Nth entry here, so adding a dungeon is
|
|
## one entry plus one marker.
|
|
|
|
const STANDARD := &"warden_descent"
|
|
const PROVING := &"proving_grounds"
|
|
|
|
const ORDER: Array[StringName] = [
|
|
STANDARD,
|
|
PROVING,
|
|
]
|
|
|
|
|
|
static func default_id() -> StringName:
|
|
return STANDARD
|
|
|
|
|
|
static func get_def(id: StringName) -> DungeonDef:
|
|
match id:
|
|
STANDARD: return standard()
|
|
PROVING: return proving_grounds()
|
|
return null
|
|
|
|
|
|
## Falls back to the standard run rather than to null. An id off the wire has to
|
|
## resolve to something playable, and "the normal dungeon" is the safe answer.
|
|
static func get_or_default(id: StringName) -> DungeonDef:
|
|
var d := get_def(id)
|
|
return d if d != null else standard()
|
|
|
|
|
|
static func index_of(id: StringName) -> int:
|
|
return maxi(ORDER.find(id), 0)
|
|
|
|
|
|
static func by_index(index: int) -> StringName:
|
|
if index < 0 or index >= ORDER.size():
|
|
return default_id()
|
|
return ORDER[index]
|
|
|
|
|
|
# --- The dungeons -----------------------------------------------------------
|
|
|
|
## The real thing. Every multiplier is 1.0, which is the point: this is the
|
|
## baseline the other definitions are described against.
|
|
static func standard() -> DungeonDef:
|
|
var d := DungeonDef.new()
|
|
d.id = STANDARD
|
|
d.display_name = "Warden's Descent"
|
|
d.subtitle = "the real run"
|
|
d.tint = Color(0.5, 0.9, 1.0)
|
|
return d
|
|
|
|
|
|
## A test harness you can walk into.
|
|
##
|
|
## Same generator, same rooms, same enemies and the same Warden -- everything
|
|
## dies far faster and drops far more often, so a manual pass over the loot,
|
|
## the inventory and all four boss phases takes a couple of minutes instead of a
|
|
## quarter of an hour. Being reachable from the hub rather than hidden behind a
|
|
## launch flag is most of the value: you can compare the two back to back in one
|
|
## session without restarting the server.
|
|
static func proving_grounds() -> DungeonDef:
|
|
var d := DungeonDef.new()
|
|
d.id = PROVING
|
|
d.display_name = "Proving Grounds"
|
|
d.subtitle = "for testing -- fragile, generous"
|
|
d.enemy_hp_mult = 0.2
|
|
d.boss_hp_mult = 0.08
|
|
# 0.08 -> 0.8 for trash. High enough that a handful of kills fills a bag,
|
|
# which is what makes the four-slot limit and dropping testable at all.
|
|
d.loot_chance_mult = 10.0
|
|
d.tint = Color(1.0, 0.75, 0.35)
|
|
return d
|