Add game mode categories, character creation, on-foot ship/hub mode, RAM currency, and in-ship navigation
Bundles several sessions' worth of previously uncommitted work: map categories with Domination/Conquest/King of the Hill mode stubs and a server-list mode filter; a procedurally-drawn character-creation screen replacing the old callsign-only PROFILE overlay; the on-foot groundwork (walkable station hub, ship interior, character controller) plus the RAM currency backend; and today's addition, an in-ship Helldivers-2-style navigation table that QUICK PLAY's queue/connect flow and the Belters/ Military hub travel now live behind, with hub-and-ship return paths and a context-aware pause menu usable both in-match and inside the ship. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@@ -8,7 +8,7 @@
|
|||||||
|------|----------|
|
|------|----------|
|
||||||
| `overview.md` | Concept, core loop, game flow, match sizes |
|
| `overview.md` | Concept, core loop, game flow, match sizes |
|
||||||
| `racesclasses.md` | 2 chosen factions, 3 ship classes each |
|
| `racesclasses.md` | 2 chosen factions, 3 ship classes each |
|
||||||
| `multiplayer.md` | Maps, game modes (design ideas — casual's Sector Control etc. not yet built), galaxy war meta |
|
| `multiplayer.md` | Maps, game modes (4 casual categories exist as `GameMode` subclasses — Team Deathmatch is real, Domination/Conquest/King of the Hill are scoring stubs, see `CLAUDE.md` item 27), galaxy war meta |
|
||||||
| `bots.md` | Bot fill rules for casual matches |
|
| `bots.md` | Bot fill rules for casual matches |
|
||||||
| `tech.md` | Engine, networking architecture, checklist |
|
| `tech.md` | Engine, networking architecture, checklist |
|
||||||
| `money.md` | Monetization strategy |
|
| `money.md` | Monetization strategy |
|
||||||
@@ -71,13 +71,24 @@ Added the other half of that same knob: a VSYNC dropdown (Enabled/Adaptive/Disab
|
|||||||
25. **ISN flagship formation mixes in a Stealth Corvette hull** — item 24's 3-ship flagship formation was 3 identical `flagship_colossus.png` copies per faction; ISN's formation is now 2 Colossus + 1 `stealth_corvette.png` (a new hull processed from a user-supplied render: background matted out via border flood-fill, a disconnected decorative sparkle dropped, rotated 90° to this project's nose-up convention — see `assets/images/ships/isn/source/isn_stealth_corvette_obsidian_source.jpeg` for the original). `team_select.gd`'s ISN entry gained a `flagship_paths` array (list of `{path, scale}`, cycled by formation-slot index in `world.gd`'s `_spawn_flagship()`); a race with no `flagship_paths` (ORC, so far) falls through to the original singular `flagship_path`/`flagship_scale` for every slot, so this is additive and backward compatible. The Corvette's `scale: 0.68` was calibrated against its native resolution (516×1019, notably taller than Colossus's 535×692) so all 3 formation members read at the same on-screen size despite the size mismatch in source art.
|
25. **ISN flagship formation mixes in a Stealth Corvette hull** — item 24's 3-ship flagship formation was 3 identical `flagship_colossus.png` copies per faction; ISN's formation is now 2 Colossus + 1 `stealth_corvette.png` (a new hull processed from a user-supplied render: background matted out via border flood-fill, a disconnected decorative sparkle dropped, rotated 90° to this project's nose-up convention — see `assets/images/ships/isn/source/isn_stealth_corvette_obsidian_source.jpeg` for the original). `team_select.gd`'s ISN entry gained a `flagship_paths` array (list of `{path, scale}`, cycled by formation-slot index in `world.gd`'s `_spawn_flagship()`); a race with no `flagship_paths` (ORC, so far) falls through to the original singular `flagship_path`/`flagship_scale` for every slot, so this is additive and backward compatible. The Corvette's `scale: 0.68` was calibrated against its native resolution (516×1019, notably taller than Colossus's 535×692) so all 3 formation members read at the same on-screen size despite the size mismatch in source art.
|
||||||
26. **Match structure — pre-match countdown, timer, kills/deaths, scoreboard, winner banner, persistent stats, reusable GameMode/map framework** (see `overview/map1.md`) — matches now have real phases instead of ships spawning the instant a peer connects. New `MatchManager` autoload (`autoload/match_manager.gd`) runs a server-authoritative `PRE_MATCH` (30s countdown, ships held invisible/uncollidable via `ship_movement.gd`'s `server_hold_for_match_start()`/`server_release_from_hold()` — reuses the existing dead-ship machinery rather than a parallel state, so prediction/reconciliation are untouched; flagships cosmetically tween in from a random offset via `flagship.gd`'s `play_creep_in()`) → `IN_PROGRESS` (the active `GameMode`'s 7-minute clock runs, ships released) → `POST_MATCH` (winner banner, result reported to matchmaking-api) → loops back into a fresh `PRE_MATCH` forever, matching the always-on server-pool model from item 16 rather than kicking players to the menu. Phase state is replicated with the same "decide once on the server, RPC-broadcast, late joiners pull it" pattern `world.gd`'s `decide_offered_races()` already established. Win-condition logic lives entirely in a new `GameMode` abstraction (`world/game_modes/game_mode.gd` base + `team_deathmatch_mode.gd`, the only mode implemented so far — most kills when the clock runs out wins, draw on a tie) built identically on every peer off a shared `GameConfig.default_game_mode_id` constant, so a future mode (`multiplayer.md`'s Flagship Assault/Last Ship Standing/Capture and Hold ideas) is a new subclass plus one factory branch in `MatchManager`, with zero changes to the timer/scoreboard/banner code around it — none of that ever hardcodes "kills," only ever calling `GameMode.get_score_for_team()`/`get_score_label()`. `world.gd`'s hardcoded `MAP_SCENE` const became a `MAPS` array + `_pick_map()` (same static-array convention as `TeamSelect.RACES`) so a second map is an append, not a rewrite — still only `map_01` exists today. New `MatchStats` autoload (`autoload/match_stats.gd`) tracks per-match (not persistent) kills/deaths by peer_id, including bots, hooked into the single existing death-detection call site (`kill_feed_manager.gd`'s `report_kill()`) rather than duplicating it in `ship_movement.gd`. Three new signal-driven HUD `CanvasLayer`s follow every existing HUD file's autonomous-`_build_ui()` convention: `hud/match_timer.gd` (top-center countdown/clock), `hud/scoreboard.gd` (hold-Tab CS:GO-style two-team panel, new `scoreboard` input action bound to Tab), `hud/match_banner.gd` (full-screen winner banner, `layer=30` — highest in the project). Banner art (`assets/images/banners/`) was renamed from opaque UUID filenames to `isn_banner.jpeg`/`orc_banner.jpeg` (the third, Apex Dynamics, stays orphaned per item 19's precedent of leaving unused assets on disk) and wired via a new `banner_path` key on `team_select.gd`'s ISN/ORC `RACES` entries, same pattern as the existing `flagship_path` key. Persistent per-player history (total kills, deaths, wins, losses, hours played) is a new `matchmaking-api` capability: `Player` gained `kills`/`deaths`/`hours_played` columns, a new `POST /matches/report` endpoint (`app/routers/matches.py`) is called once by the hosting server's `MatchManager` (never a client, same server-only trust model as `/servers/register`) at `POST_MATCH`, upserting each real (non-bot) player's totals by callsign via a shared `app/crud.py` helper also now used by the matchmaking-queue join path; `GET /stats/{callsign}` returns the 3 new fields alongside the existing `mmr`/`wins`/`losses`. No migrations tooling exists in this API (schema changes just land in `models.py` for `create_all()` to pick up on a fresh DB) — a `docker compose down -v` is needed once to add the new columns to an already-existing local `players` table, verified end-to-end via a real `docker compose up` round-trip during implementation.
|
26. **Match structure — pre-match countdown, timer, kills/deaths, scoreboard, winner banner, persistent stats, reusable GameMode/map framework** (see `overview/map1.md`) — matches now have real phases instead of ships spawning the instant a peer connects. New `MatchManager` autoload (`autoload/match_manager.gd`) runs a server-authoritative `PRE_MATCH` (30s countdown, ships held invisible/uncollidable via `ship_movement.gd`'s `server_hold_for_match_start()`/`server_release_from_hold()` — reuses the existing dead-ship machinery rather than a parallel state, so prediction/reconciliation are untouched; flagships cosmetically tween in from a random offset via `flagship.gd`'s `play_creep_in()`) → `IN_PROGRESS` (the active `GameMode`'s 7-minute clock runs, ships released) → `POST_MATCH` (winner banner, result reported to matchmaking-api) → loops back into a fresh `PRE_MATCH` forever, matching the always-on server-pool model from item 16 rather than kicking players to the menu. Phase state is replicated with the same "decide once on the server, RPC-broadcast, late joiners pull it" pattern `world.gd`'s `decide_offered_races()` already established. Win-condition logic lives entirely in a new `GameMode` abstraction (`world/game_modes/game_mode.gd` base + `team_deathmatch_mode.gd`, the only mode implemented so far — most kills when the clock runs out wins, draw on a tie) built identically on every peer off a shared `GameConfig.default_game_mode_id` constant, so a future mode (`multiplayer.md`'s Flagship Assault/Last Ship Standing/Capture and Hold ideas) is a new subclass plus one factory branch in `MatchManager`, with zero changes to the timer/scoreboard/banner code around it — none of that ever hardcodes "kills," only ever calling `GameMode.get_score_for_team()`/`get_score_label()`. `world.gd`'s hardcoded `MAP_SCENE` const became a `MAPS` array + `_pick_map()` (same static-array convention as `TeamSelect.RACES`) so a second map is an append, not a rewrite — still only `map_01` exists today. New `MatchStats` autoload (`autoload/match_stats.gd`) tracks per-match (not persistent) kills/deaths by peer_id, including bots, hooked into the single existing death-detection call site (`kill_feed_manager.gd`'s `report_kill()`) rather than duplicating it in `ship_movement.gd`. Three new signal-driven HUD `CanvasLayer`s follow every existing HUD file's autonomous-`_build_ui()` convention: `hud/match_timer.gd` (top-center countdown/clock), `hud/scoreboard.gd` (hold-Tab CS:GO-style two-team panel, new `scoreboard` input action bound to Tab), `hud/match_banner.gd` (full-screen winner banner, `layer=30` — highest in the project). Banner art (`assets/images/banners/`) was renamed from opaque UUID filenames to `isn_banner.jpeg`/`orc_banner.jpeg` (the third, Apex Dynamics, stays orphaned per item 19's precedent of leaving unused assets on disk) and wired via a new `banner_path` key on `team_select.gd`'s ISN/ORC `RACES` entries, same pattern as the existing `flagship_path` key. Persistent per-player history (total kills, deaths, wins, losses, hours played) is a new `matchmaking-api` capability: `Player` gained `kills`/`deaths`/`hours_played` columns, a new `POST /matches/report` endpoint (`app/routers/matches.py`) is called once by the hosting server's `MatchManager` (never a client, same server-only trust model as `/servers/register`) at `POST_MATCH`, upserting each real (non-bot) player's totals by callsign via a shared `app/crud.py` helper also now used by the matchmaking-queue join path; `GET /stats/{callsign}` returns the 3 new fields alongside the existing `mmr`/`wins`/`losses`. No migrations tooling exists in this API (schema changes just land in `models.py` for `create_all()` to pick up on a fresh DB) — a `docker compose down -v` is needed once to add the new columns to an already-existing local `players` table, verified end-to-end via a real `docker compose up` round-trip during implementation.
|
||||||
|
|
||||||
|
27. **Map categories + 3 new GameMode stubs (Domination, Conquest, King of the Hill)** — maps now declare which game modes they support, the way CS's de_/cs_ prefixes tie a map to a mode, except a map can list more than one category rather than being locked to exactly one. `world.gd`'s `MAPS` entries gained a `"categories"` array; `World.pick_map()` (static, public) filters `MAPS` down to whichever list a given mode id and picks the first match, falling back to the full list if nothing matches (documented as *not* randomized among ties — every caller runs this independently with no shared seed, unlike `_spawn_flagships()`'s `_match_seed`, so a random pick here would desync which map each peer loads the moment a second candidate exists; real map rotation needs that seed-sync treatment first). Three new `GameMode` subclasses (`world/game_modes/domination_mode.gd`/`conquest_mode.gd`/`king_of_the_hill_mode.gd`) round out `MatchManager._create_mode()`'s factory alongside `team_deathmatch_mode.gd` — each only overrides `get_mode_name()` for now and shares a new `GameMode._kills_based_win_condition()` helper (factored out of `TeamDeathmatchMode.check_win_condition()`, which now just calls it) for scoring, since none of the three have real zone/capture-point mechanics yet. `overview/multiplayer.md`'s casual mode-ideas table was renamed to match this session's chosen names (Domination/Conquest/King of the Hill), replacing the old Sector Control/Annihilation/Base Assault/Convoy Escort ideas that had no code behind them either way. **map_01 only lists `"team_deathmatch"`** in its `categories` — Domination/Conquest/King of the Hill exist as `GameMode` code but no map is actually built/tuned for them yet, so map_01 doesn't claim to support modes it can't really run.
|
||||||
|
|
||||||
|
**Server list now reports the real game mode + map** — `menu/server_select.gd` was previously showing only the matchmaking *queue* mode ("CASUAL", the only value `Mode` has since item 15) with no indication of what's actually being played. `GameServer` (matchmaking-api) gained `game_mode`/`map_name` string columns (plain strings, not a `Mode`-style enum — the API has no reason to know the client's `GameMode` id catalog), set via new fields on `POST /servers/register`'s `ServerRegisterRequest` and returned by `GET /servers`; `NetworkManager._register_with_matchmaking_api()` now calls the static `World.pick_map(GameConfig.default_game_mode_id)` to get these before every heartbeat — safe to call before `world.tscn` even loads (the heartbeat starts on server boot) since it's a pure function of shared static data, same "decide independently, same result on every caller" pattern as `TeamSelect.RACES`. `world.gd` gained a `class_name World` so this static call is reachable from `NetworkManager`. Each server row now reads e.g. `"TEAM DEATHMATCH · Sector Alpha 127.0.0.1:7777"` (`_format_mode_name()` mirrors `GameMode.get_mode_name()`'s formatting so the two never spell a mode differently). Verified end-to-end: rebuilt the local matchmaking-api DB (`docker compose down -v && up -d --build`, needed for the new columns per this API's no-migrations convention — see item 26/`matchmaking-api/README.md`), hosted a real headless server, and confirmed `GET /servers` showed `game_mode: "team_deathmatch"`, `map_name: "Sector Alpha"` for the live row.
|
||||||
|
|
||||||
|
**Game mode became its own toggleable filter, not just row text** — `server_select.gd` gained an "ALL MODES" + one-per-`GameConfig.GAME_MODE_IDS` toggle bar (radio-style via a `ButtonGroup`, same pattern the server rows below already use) above the list; picking one narrows the rendered rows to that `game_mode` without re-hitting the API. `GameConfig.GAME_MODE_IDS` is a new canonical ordered array (`["team_deathmatch", "domination", "conquest", "king_of_the_hill"]`) backing both this filter bar and `default_game_mode_id`'s valid values. Required splitting what was one `_refresh_servers()` (fetch + render) into `_refresh_servers()` (network fetch only, sets `_all_servers`) and `_apply_filter()` (narrows to `_selected_category`, rebuilds rows into `_servers` — the array rows/ping-probes/CONNECT actually index into) so toggling a filter doesn't need a fresh network round-trip.
|
||||||
|
|
||||||
|
28. **Character creation screen (Stardew-Valley-style callsign + portrait picker)** — the main menu's PROFILE overlay (item 14) was just a callsign `LineEdit`; it now also offers a portrait pick, over the existing space-themed starfield background (`main_menu.gd`'s `_add_bg()`, unchanged, already reused here). No portrait art exists in the project (no `assets/images/portraits/` or similar), so portraits are procedurally drawn rather than sourced as new images: new `menu/pilot_portrait.gd` (`class_name PilotPortrait`, `extends Control`) `_draw()`s a simple pilot bust (shoulders + helmet + visor) from a `PRESETS` array of grayscale suit shades paired with a colored visor accent (Ash/Slate/Steel/Iron/Fog/Graphite) — kept monochrome/gray per this feature's art direction, with the visor as the only pop of color per preset. `_add_profile_overlay()`'s panel grew (400×240 → 420×450) to fit a centered portrait frame flanked by `<`/`>` `Button`s (`_on_portrait_cycle()`, cycling `PilotPortrait.preset_index` which wraps via `wrapi()`) above the existing callsign field; title changed from "PROFILE" to "CREATE YOUR PILOT". New `GameConfig.player_portrait_index: int` stores the choice with the same lifetime as the existing (not-persisted-to-disk, resets on relaunch) `player_name` — only written on SAVE, matching the callsign's existing commit-on-save behavior rather than live-updating as the player cycles. The chosen portrait is also now shown as a small icon in the main menu's top-right badge (`_badge_portrait`, refreshed in `_refresh_profile_badge()`) next to the callsign/rank text, so the pick is visible outside the creation screen too, not just while editing it.
|
||||||
|
|
||||||
|
29. **On-foot station hub — first sprite import + walkable room (new gameplay mode, groundwork only)** — kicks off a Stardew-Valley-style on-foot mode alongside the ship combat, using 4 AI-generated reference sheets dropped at `assets/images/worldsprites/orc/` (a multi-NPC character roster, a messy "concept + swatches" corridor sheet, a clean labeled tileset reference sheet, and a furniture/props sheet). No `godot4` binary was assumed available per prior session notes, but this sandbox does have one at the `godot4`-aliased path — used here to actually headless-import and screenshot-verify everything below, not just hand-write `.import` files blind. Individual sprites were hand-cropped out of the sheets with ImageMagick (no PIL/pip in this sandbox) since none of the 4 sheets are uniform game-ready spritesheets — grid boundaries were found by overlaying a labeled pixel grid and reading it back with the Read tool, iterating per-crop; backgrounds matted to transparent via corner-seeded flood fill (`-fuzz`/`-draw "alpha X,Y floodfill"`), same technique as the existing ship sprites. New assets: `assets/images/worldsprites/character/soldier_{idle,walk}.png` (one NPC variant's front-idle + side-walking pose — the sheet has no full 4-directional walk cycle, just these two poses per outfit, so `on_foot_character.gd` swaps between them by movement state rather than animating frames, and flips `Sprite2D.flip_h` for left/right, mirroring `ship_movement.gd`'s thrust-sprite-swap pattern from item 18); `assets/images/worldtiles/station_{floor,walls,door}.png` (3 floor variants + 2 wall variants cropped to a uniform 72×72 grid from the clean labeled sheet, plus one door graphic used as a decorative `Sprite2D`, not a tile); `assets/images/worldsprites/furniture/{locker,cabinet,plant,server_rack}.png`. Each folder keeps a `source/` copy of its origin sheet, same convention as the race art in item 17. New `world/station_tileset.tres` (2 `TileSetAtlasSource`s, `tile_size = Vector2i(72, 72)`) follows `world/world_tileset.tres`'s existing walls+asteroids split pattern. New `world/levels/station_hub.tscn` + `station_hub.gd` (`class_name StationHub`) procedurally builds a 12×9 room via `TileMapLayer.set_cell()` in `_ready()` (hand-authoring the raw `tile_data` `PackedInt32Array` format was judged too risky to get right blind, so the layout is code-generated instead of painted) with a door-sized gap in the wall ring, spawns `StaticBody2D` wall colliders per used cell (same "tiles have no collision shapes of their own" approach as `world.gd`'s ship-map collider builder, but a separate implementation local to this scene, not a shared one) and places the 4 furniture sprites as static decoration. New `world/levels/on_foot_character.gd` (`class_name OnFootCharacter`, `CharacterBody2D`) is a 4-directional top-down walker (reuses the existing `move_up/down/left/right` input actions literally instead of the ships' rotate+thrust scheme) with idle/walk sprite swapping and a `Camera2D` using the same `make_current()` fix from the networking memory notes. Verified end-to-end, not just import-clean: launched the real scene headlessly under a throwaway Xvfb display (`:133`, distinct from the user's actual running editor/display — left both untouched), screenshotted the room (floor variety, both wall types, door gap, all 4 furniture pieces, and the player all render correctly), then drove movement via a temporary `HubAutopilot` autoload (removed after) simulating a held `move_right` and confirmed the walk sprite/camera-follow render correctly and that the character stops cleanly at the rock wall's collider instead of clipping through. **This is groundwork only** — no main-menu entry point, no NPC interaction/dialogue, no second room/transition, and the character roster/tileset/furniture sheets still have far more content un-cropped than used (only 1 of ~10 character variants, 5 of ~40 tileset tiles, 4 of ~30 furniture items) — see Current Tasks.
|
||||||
|
|
||||||
## Current Tasks
|
## Current Tasks
|
||||||
|
|
||||||
- [x] ~~Asteroids and environment hazards~~ — done, undocumented until now: `world.gd`'s collider builder gives the map's `Asteroids` TileMapLayer round hitboxes in an `environment_hazard` group distinct from `environment_wall`, and deals impact damage (`GameConfig.ship_wall_damage`) on top of the bounce every wall/asteroid tile already causes
|
- [x] ~~Asteroids and environment hazards~~ — done, undocumented until now: `world.gd`'s collider builder gives the map's `Asteroids` TileMapLayer round hitboxes in an `environment_hazard` group distinct from `environment_wall`, and deals impact damage (`GameConfig.ship_wall_damage`) on top of the bounce every wall/asteroid tile already causes
|
||||||
- [ ] Sound effects — shoot (per-ship, `world.gd`'s `_play_shoot_sound()`) and menu music (`autoload/music_manager.gd`) exist; thrust, explosion, and UI-click sounds are still missing
|
- [ ] Sound effects — shoot (per-ship, `world.gd`'s `_play_shoot_sound()`) and menu music (`autoload/music_manager.gd`) exist; thrust, explosion, and UI-click sounds are still missing
|
||||||
- [ ] Graphics quality presets / resolution scale in Options (audio, window mode, colorblind, and key rebinding are now done — see item 21)
|
- [ ] Graphics quality presets / resolution scale in Options (audio, window mode, colorblind, and key rebinding are now done — see item 21)
|
||||||
- [ ] Real rank/level backend — main menu's top-right badge and RANK_DATA are still placeholders (`CURRENT_RANK`/`CURRENT_LEVEL` constants in `main_menu.gd`)
|
- [ ] Real rank/level backend — main menu's top-right badge and RANK_DATA are still placeholders (`CURRENT_RANK`/`CURRENT_LEVEL` constants in `main_menu.gd`)
|
||||||
- [ ] Additional game modes (Flagship Assault, Last Ship Standing, Capture and Hold — see `multiplayer.md`) via item 26's new `GameMode` framework (`world/game_modes/`); only Team Deathmatch is implemented so far
|
- [ ] Real scoring for Domination/Conquest/King of the Hill (see item 27) — all 3 exist as `GameMode` subclasses with map-category tags wired up, but still score by kills like Team Deathmatch until each grows its own zone/point/capture mechanic
|
||||||
- [ ] Galaxy war meta / sector control for casual (see `multiplayer.md`) — Sector Control specifically would also need its own `GameMode` subclass, see above
|
- [ ] Galaxy war meta / sector control for casual (see `multiplayer.md`) — ties most naturally to Domination once it has real per-sector scoring, see above
|
||||||
- [ ] Real account-linked identity — GodotSteam auth + VAC still not started; `callsign` is the only player identity today
|
- [ ] Real account-linked identity — GodotSteam auth + VAC still not started; `callsign` is the only player identity today
|
||||||
- [ ] Lag compensation (basic rewind) — matters once testing moves beyond localhost
|
- [ ] Lag compensation (basic rewind) — matters once testing moves beyond localhost
|
||||||
|
- [ ] On-foot station hub (see item 29) — currently a standalone unlinked scene (`world/levels/station_hub.tscn`) with one hand-built room; needs a main-menu entry point, decide whether it's multiplayer (networked like ship combat) or single-player/local, NPC interaction, and cropping more of the still-mostly-unused character/tileset/furniture sheets
|
||||||
|
|||||||
@@ -106,6 +106,28 @@ add them to a database that already has a `players` table from before this
|
|||||||
change. Run `docker compose down -v` once to pick them up on an existing local
|
change. Run `docker compose down -v` once to pick them up on an existing local
|
||||||
dev database.
|
dev database.
|
||||||
|
|
||||||
|
## RAM (in-game currency)
|
||||||
|
|
||||||
|
See `overview/onfoot.md` for the full design — this is just the backend
|
||||||
|
piece. `Player.ram_kb` is a single `BigInteger` balance, always stored and
|
||||||
|
transmitted as kilobytes (the smallest denomination); the Godot client
|
||||||
|
(`spacewar/autoload/currency.gd`) formats it up into KB/MB/GB/TB for
|
||||||
|
display, 1000 per step. 100% separate from real-money monetization
|
||||||
|
(`overview/money.md`) — this is purely an in-game economy.
|
||||||
|
|
||||||
|
`POST /matches/report` now also credits RAM to every reported player:
|
||||||
|
`ram_payout_participation_kb` just for being in the match, plus
|
||||||
|
`ram_payout_per_kill_kb` per kill, plus `ram_payout_win_bonus_kb` if they
|
||||||
|
won (all three tunable in `app/config.py`, currently 50/15/200 — placeholder
|
||||||
|
numbers, not balanced against anything). `GET /stats/{callsign}` returns the
|
||||||
|
running total as `ram_kb`.
|
||||||
|
|
||||||
|
There is no spend endpoint yet — nothing in the game can spend RAM until the
|
||||||
|
ship interior/hubs from `overview/onfoot.md` exist. `Player.ram_kb` is a new
|
||||||
|
column on an existing table, same no-migrations caveat as above — covered by
|
||||||
|
the same `docker compose down -v` if you're picking this up on an existing
|
||||||
|
local dev database.
|
||||||
|
|
||||||
## Client integration
|
## Client integration
|
||||||
|
|
||||||
The Godot client is wired up (`spacewar/autoload/matchmaking_client.gd`):
|
The Godot client is wired up (`spacewar/autoload/matchmaking_client.gd`):
|
||||||
|
|||||||
@@ -15,5 +15,15 @@ class Settings(BaseSettings):
|
|||||||
# to "offline" and back.
|
# to "offline" and back.
|
||||||
server_stale_seconds: int = 20
|
server_stale_seconds: int = 20
|
||||||
|
|
||||||
|
# RAM payout formula for POST /matches/report (see overview/onfoot.md) --
|
||||||
|
# every reported player gets ram_payout_participation_kb just for being
|
||||||
|
# in the match, plus ram_payout_per_kill_kb per kill, plus
|
||||||
|
# ram_payout_win_bonus_kb if they won. Placeholder numbers, not balanced
|
||||||
|
# against anything -- tune freely, nothing else in the schema depends on
|
||||||
|
# the specific values.
|
||||||
|
ram_payout_participation_kb: int = 50
|
||||||
|
ram_payout_per_kill_kb: int = 15
|
||||||
|
ram_payout_win_bonus_kb: int = 200
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ async def _seed_demo_servers() -> None:
|
|||||||
status=ServerStatus.available,
|
status=ServerStatus.available,
|
||||||
player_count=player_count,
|
player_count=player_count,
|
||||||
max_players=50,
|
max_players=50,
|
||||||
|
game_mode="team_deathmatch",
|
||||||
|
map_name="Sector Alpha",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import enum
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String
|
from sqlalchemy import BigInteger, DateTime, Float, ForeignKey, Integer, String
|
||||||
from sqlalchemy import Enum as SAEnum
|
from sqlalchemy import Enum as SAEnum
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
@@ -38,6 +38,13 @@ class Player(Base):
|
|||||||
kills: Mapped[int] = mapped_column(Integer, default=0)
|
kills: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
deaths: Mapped[int] = mapped_column(Integer, default=0)
|
deaths: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
hours_played: Mapped[float] = mapped_column(Float, default=0.0)
|
hours_played: Mapped[float] = mapped_column(Float, default=0.0)
|
||||||
|
# In-game currency, see overview/onfoot.md -- named RAM, stored as a
|
||||||
|
# single integer count of kilobytes (the smallest denomination); the
|
||||||
|
# client formats it up into KB/MB/GB/TB (1000 per step, not 1024) for
|
||||||
|
# display. BigInteger since a long-lived player's total is expected to
|
||||||
|
# climb well past 32-bit Integer's ~2.1 billion ceiling (2.1 billion KB
|
||||||
|
# is only ~2.1 TB).
|
||||||
|
ram_kb: Mapped[int] = mapped_column(BigInteger, default=0)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +64,16 @@ class GameServer(Base):
|
|||||||
# once that probe answers.
|
# once that probe answers.
|
||||||
player_count: Mapped[int] = mapped_column(Integer, default=0)
|
player_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
max_players: Mapped[int] = mapped_column(Integer, default=50)
|
max_players: Mapped[int] = mapped_column(Integer, default=50)
|
||||||
|
# The actual in-match GameMode id (e.g. "team_deathmatch") and map
|
||||||
|
# display name (e.g. "Sector Alpha") this server is running -- distinct
|
||||||
|
# from `mode` above, which is only the matchmaking queue mode ("casual").
|
||||||
|
# Plain strings, not a Mode-style enum: the set of GameMode ids lives in
|
||||||
|
# the Godot client (spacewar/world/game_modes/, world.gd's MAPS), which
|
||||||
|
# this API has no reason to duplicate/validate against. Reported by the
|
||||||
|
# hosting server on every register/heartbeat call (see
|
||||||
|
# spacewar/autoload/network_manager.gd's host_server()).
|
||||||
|
game_mode: Mapped[str] = mapped_column(String(64), default="team_deathmatch")
|
||||||
|
map_name: Mapped[str] = mapped_column(String(64), default="Sector Alpha")
|
||||||
|
|
||||||
|
|
||||||
class Match(Base):
|
class Match(Base):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app import crud
|
from app import crud
|
||||||
|
from app.config import settings
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models import GameServer, Match, MatchPlayer
|
from app.models import GameServer, Match, MatchPlayer
|
||||||
from app.schemas import MatchReportRequest
|
from app.schemas import MatchReportRequest
|
||||||
@@ -48,6 +49,11 @@ async def report_match(req: MatchReportRequest, db: AsyncSession = Depends(get_d
|
|||||||
player.kills += result.kills
|
player.kills += result.kills
|
||||||
player.deaths += result.deaths
|
player.deaths += result.deaths
|
||||||
player.hours_played += result.seconds_played / 3600.0
|
player.hours_played += result.seconds_played / 3600.0
|
||||||
|
player.ram_kb += (
|
||||||
|
settings.ram_payout_participation_kb
|
||||||
|
+ result.kills * settings.ram_payout_per_kill_kb
|
||||||
|
+ (settings.ram_payout_win_bonus_kb if result.is_winner else 0)
|
||||||
|
)
|
||||||
if result.is_winner:
|
if result.is_winner:
|
||||||
player.wins += 1
|
player.wins += 1
|
||||||
elif req.winner_team is not None:
|
elif req.winner_team is not None:
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ async def register_server(req: ServerRegisterRequest, db: AsyncSession = Depends
|
|||||||
existing.player_count = req.player_count
|
existing.player_count = req.player_count
|
||||||
existing.max_players = req.max_players
|
existing.max_players = req.max_players
|
||||||
existing.last_heartbeat = datetime.utcnow()
|
existing.last_heartbeat = datetime.utcnow()
|
||||||
|
existing.game_mode = req.game_mode
|
||||||
|
existing.map_name = req.map_name
|
||||||
else:
|
else:
|
||||||
db.add(
|
db.add(
|
||||||
GameServer(
|
GameServer(
|
||||||
@@ -43,6 +45,8 @@ async def register_server(req: ServerRegisterRequest, db: AsyncSession = Depends
|
|||||||
status=status,
|
status=status,
|
||||||
player_count=req.player_count,
|
player_count=req.player_count,
|
||||||
max_players=req.max_players,
|
max_players=req.max_players,
|
||||||
|
game_mode=req.game_mode,
|
||||||
|
map_name=req.map_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -80,6 +84,8 @@ async def list_servers(db: AsyncSession = Depends(get_db)) -> list[dict]:
|
|||||||
"player_count": s.player_count,
|
"player_count": s.player_count,
|
||||||
"max_players": s.max_players,
|
"max_players": s.max_players,
|
||||||
"last_heartbeat": s.last_heartbeat,
|
"last_heartbeat": s.last_heartbeat,
|
||||||
|
"game_mode": s.game_mode,
|
||||||
|
"map_name": s.map_name,
|
||||||
}
|
}
|
||||||
for s in servers
|
for s in servers
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ async def get_stats(callsign: str, db: AsyncSession = Depends(get_db)) -> Player
|
|||||||
kills=player.kills,
|
kills=player.kills,
|
||||||
deaths=player.deaths,
|
deaths=player.deaths,
|
||||||
hours_played=player.hours_played,
|
hours_played=player.hours_played,
|
||||||
|
ram_kb=player.ram_kb,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ class ServerRegisterRequest(BaseModel):
|
|||||||
mode: Mode
|
mode: Mode
|
||||||
player_count: int = 0
|
player_count: int = 0
|
||||||
max_players: int = 50
|
max_players: int = 50
|
||||||
|
# See GameServer.game_mode/map_name in app/models.py -- the in-match
|
||||||
|
# GameMode id and map display name, distinct from `mode` (queue mode).
|
||||||
|
game_mode: str = "team_deathmatch"
|
||||||
|
map_name: str = "Sector Alpha"
|
||||||
|
|
||||||
|
|
||||||
class PlayerStatsResponse(BaseModel):
|
class PlayerStatsResponse(BaseModel):
|
||||||
@@ -39,6 +43,7 @@ class PlayerStatsResponse(BaseModel):
|
|||||||
kills: int
|
kills: int
|
||||||
deaths: int
|
deaths: int
|
||||||
hours_played: float
|
hours_played: float
|
||||||
|
ram_kb: int
|
||||||
|
|
||||||
|
|
||||||
# Reported by the hosting game server (never a client directly -- see
|
# Reported by the hosting game server (never a client directly -- see
|
||||||
|
|||||||
@@ -41,28 +41,30 @@ Competitive, skill-based, MMR/ELO ladder. Penalty for leaving mid-match. Small
|
|||||||
|
|
||||||
No penalty for leaving. Larger maps. Chaotic and fun — you can drop in mid-match.
|
No penalty for leaving. Larger maps. Chaotic and fun — you can drop in mid-match.
|
||||||
|
|
||||||
> **Status:** **Team Deathmatch** (the first row below) is implemented — a
|
> **Status:** **Team Deathmatch** is implemented for real — a 7-minute timed
|
||||||
> 7-minute timed version of Annihilation's "pure kills" idea (most kills when
|
> "most kills when the clock runs out wins" mode, plus a 30s pre-match
|
||||||
> the clock runs out wins, not last-team-standing), plus a 30s pre-match
|
|
||||||
> countdown, live kill/death tracking, a Tab scoreboard, and a winner banner
|
> countdown, live kill/death tracking, a Tab scoreboard, and a winner banner
|
||||||
> (see `overview/map1.md`, `CLAUDE.md` item 26). It's built through a
|
> (see `overview/map1.md`, `CLAUDE.md` item 26). It's built through a
|
||||||
> `GameMode` abstraction (`spacewar/world/game_modes/`) specifically so the
|
> `GameMode` abstraction (`spacewar/world/game_modes/`) specifically so the
|
||||||
> other rows here — Sector Control, King of the Hill, Base Assault, Convoy
|
> other 3 rows below — Domination, Conquest, King of the Hill — can each
|
||||||
> Escort — can each become a new `GameMode` subclass later without changing
|
> become a fully-realized `GameMode` subclass later without changing the
|
||||||
> the match-phase/timer/scoreboard/banner machinery around them.
|
> match-phase/timer/scoreboard/banner machinery around them. As of
|
||||||
|
> `CLAUDE.md` item 27, all 4 modes below exist as real `GameMode` subclasses
|
||||||
|
> and are wired up end-to-end via a "category" tag on each map
|
||||||
|
> (`world.gd`'s `MAPS`, `World._pick_map()`) — but Domination/Conquest/King
|
||||||
|
> of the Hill are still scoring stubs (kills, same as Team Deathmatch) until
|
||||||
|
> each grows its own zone/point/capture mechanic.
|
||||||
|
|
||||||
### Casual Mode Ideas
|
### Casual Mode Ideas
|
||||||
|
|
||||||
| Mode | Description |
|
| Mode | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| **Team Deathmatch** *(implemented)* | Most kills when the match timer runs out wins. Timed variant of the Annihilation idea below. |
|
| **Team Deathmatch** *(implemented)* | Most kills when the match timer runs out wins. |
|
||||||
| **Sector Control** | Map divided into sectors. Hold more sectors at time limit. Ties to galactic war meta. |
|
| **Domination** *(category stub)* | Map divided into multiple capture points. Hold more of them than the enemy at the time limit. Ties to galactic war meta. |
|
||||||
| **King of the Hill** | One contested zone in the center. Hold it to rack up points. |
|
| **Conquest** *(category stub)* | Contested capture points bleed the losing team's respawn "tickets"; first team to zero tickets (or fewer tickets at the time limit) loses. |
|
||||||
| **Annihilation** | Pure kills. Last team standing or first to X kills. Chaos mode. |
|
| **King of the Hill** *(category stub)* | One contested zone in the center. Hold it to rack up points. |
|
||||||
| **Base Assault** | One team attacks a fortified base, one defends. Roles swap each round. |
|
|
||||||
| **Convoy Escort** | One team escorts a slow-moving freighter across the map, other team destroys it. |
|
|
||||||
|
|
||||||
> **Suggestion:** Start with **Sector Control** for casual. It ties into the galaxy meta (Helldivers-style), encourages teamwork without hard requirements, and naturally scales to 25v25.
|
> **Suggestion:** Build out **Domination** next — it ties into the galaxy meta (Helldivers-style), encourages teamwork without hard requirements, and naturally scales to 25v25.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,475 @@
|
|||||||
|
# On-Foot / Ship & Solar System Layer
|
||||||
|
|
||||||
|
Planning doc for the Stardew-Valley-style on-foot mode: a walkable layer
|
||||||
|
(your ship, solar-system locations, 2 multiplayer hubs) alongside the
|
||||||
|
existing ship-combat game. This is the working doc for turning the item 29
|
||||||
|
groundwork (see `CLAUDE.md`) into something actually playable. Sections
|
||||||
|
below are meant to get filled in / checked off as decisions land — not a
|
||||||
|
finished spec yet, and the vision itself has already changed shape more than
|
||||||
|
once, so treat this as a living document rather than a locked design.
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
|
||||||
|
Two intertwined layers (current thinking, as of this doc's last edit):
|
||||||
|
|
||||||
|
1. **Space combat (multiplayer, existing/live)** — the ship-vs-ship battles
|
||||||
|
already built (`world/world.tscn`, `GameMode` subclasses, matchmaking).
|
||||||
|
This is the money-earning loop: winning/playing matches pays out
|
||||||
|
**RAM**, the in-game currency (named after EVE Online's ISK — 100%
|
||||||
|
separate from `overview/money.md`'s real-world monetization strategy,
|
||||||
|
confirmed, that doc is about pricing the game itself). Balance is
|
||||||
|
**server-authoritative**, tracked in `matchmaking-api` alongside the
|
||||||
|
existing `Player.kills/deaths/wins` stats rather than trusted client-side.
|
||||||
|
2. **The Stardew side (new, this doc's scope)** — spending RAM and time in a
|
||||||
|
walkable, on-foot layer:
|
||||||
|
- **Your ship, walked around inside** — this is the Stardew *farm*
|
||||||
|
equivalent, and also the **home base** (see "Where does it fit in the
|
||||||
|
flow" below — space combat and hub travel both launch from here).
|
||||||
|
Maintenance is broad, not just crops: growing crops (the hydroponics
|
||||||
|
plant-rack furniture already cropped for `station_hub.tscn` is a
|
||||||
|
natural seed for this), cosmetic decoration, room-based upgrades that
|
||||||
|
matter in combat (see "Ship upgrades = new rooms" below — ties the two
|
||||||
|
layers together mechanically, not just economically), and repairing/
|
||||||
|
restocking hull, guns, fuel, oxygen, and ammo after combat wear (see
|
||||||
|
"Ship wear, supplies & repair" below). That's 4 systems, not 1 — see
|
||||||
|
the phased plan below for suggested sequencing rather than building all
|
||||||
|
4 at once.
|
||||||
|
- **2 main hubs** — Belters (ORC) and Military (ISN). Multiplayer: other
|
||||||
|
players' chosen character sprites (from character creation) are
|
||||||
|
visible walking around, same as the ship-combat side. Also where you
|
||||||
|
buy the supplies that repair ship wear (see below) — this is what ties
|
||||||
|
hub visits into the ship-maintenance loop mechanically, not just
|
||||||
|
thematically.
|
||||||
|
- **Solar system locations** — additional single-player/instanced places
|
||||||
|
to explore beyond the 2 hubs. **This is v2, not v1** — see Version
|
||||||
|
Scope below.
|
||||||
|
|
||||||
|
## Version scope
|
||||||
|
|
||||||
|
- **V1** — ship interiors + the 2 main hubs (Belters, Military). Nothing
|
||||||
|
else on foot. This is the whole on-foot surface area for v1: no solar
|
||||||
|
system exploration locations.
|
||||||
|
- **V2** — additional single-player "cool places to explore" solar-system
|
||||||
|
locations, reachable from the in-ship nav table alongside the 2 hubs (see
|
||||||
|
"Where does it fit in the flow" below). Not scoped further than that yet —
|
||||||
|
revisit once v1 is actually playable.
|
||||||
|
|
||||||
|
## Multiplayer scope, by location
|
||||||
|
|
||||||
|
| Location | Multiplayer? |
|
||||||
|
|---|---|
|
||||||
|
| Ship combat (existing) | Yes — already networked |
|
||||||
|
| Your ship interior | No — private, single-player/instanced |
|
||||||
|
| Belters hub (`station_hub.tscn`, ORC) | **Yes** — networked, other players' sprites visible |
|
||||||
|
| Military hub (`military_hub.tscn`, ISN art pending — see phased-plan step 3/4) | **Yes** — same as above |
|
||||||
|
| Solar system locations (v2, not built) | No — single-player/instanced |
|
||||||
|
|
||||||
|
This means `on_foot_character.gd` needs to grow two separate modes rather
|
||||||
|
than one: a simple local-only controller (ship interior — what exists
|
||||||
|
today, basically) and a properly networked one for the 2 hub locations
|
||||||
|
(`MultiplayerSpawner`, position replication, the same predict/reconcile
|
||||||
|
shape `ship_movement.gd` already has for combat). Worth building the
|
||||||
|
local-only path first regardless (ship interior), then reusing whatever
|
||||||
|
interaction/movement code it establishes for the networked hub pass.
|
||||||
|
|
||||||
|
**Resolved:** `station_hub.tscn` *is* the first of the 2 main hubs — the
|
||||||
|
**Belters hub** (Outer Rim Collective's home turf, see
|
||||||
|
`overview/racesclasses.md`'s ISN-military/ORC-belters split). Its gritty,
|
||||||
|
industrial, patchwork-metal art direction (see Asset Inventory below) fits
|
||||||
|
ORC's established visual identity directly. A second hub for **the
|
||||||
|
military** (ISN) will be built later — likely wants its own, cleaner/more
|
||||||
|
disciplined tileset+furniture pass to match ISN's gunmetal-gray aesthetic
|
||||||
|
rather than reusing the Belters hub's art wholesale, though the underlying
|
||||||
|
room-building/collider/furniture-placement code (`station_hub.gd`'s
|
||||||
|
approach) should carry over directly.
|
||||||
|
|
||||||
|
The **ship interior** (the Stardew-farm equivalent) is a separate, new,
|
||||||
|
single-player scene — not `station_hub.tscn`, and not built yet.
|
||||||
|
|
||||||
|
## Where does it fit in the flow?
|
||||||
|
|
||||||
|
**Resolved: the ship is the home base**, and the specific interaction is
|
||||||
|
settled too. `main_menu.gd` gets a new entry point (name TBD, e.g. "MY
|
||||||
|
SHIP") that drops the player into their ship interior. Inside, there's a
|
||||||
|
**navigation table** — Helldivers 2 style: walk up to it, it shows a graph
|
||||||
|
of the solar system, and picking a node is how you travel. For v1 that
|
||||||
|
graph has 3 destinations: space combat (queue/connect, today's QUICK
|
||||||
|
PLAY/SERVER SELECT behavior moves here), the Belters hub, and the Military
|
||||||
|
hub. V2's additional explorable locations (see Version Scope above) are
|
||||||
|
just more nodes on the same graph later — the table doesn't need to change
|
||||||
|
shape, only grow more destinations.
|
||||||
|
|
||||||
|
This is a real restructure of the existing menu flow, not an addition
|
||||||
|
alongside it — QUICK PLAY/SERVER SELECT's current straight-into-a-match
|
||||||
|
behavior becomes something reached via the nav table, not a main-menu
|
||||||
|
button. `main_menu.gd`'s QUICK PLAY/SERVER SELECT code itself likely stays
|
||||||
|
close to as-is functionally (still queues/connects the same way); what
|
||||||
|
changes is *what calls it* — a nav-table node interaction instead of a nav
|
||||||
|
button.
|
||||||
|
|
||||||
|
## Current implementation state
|
||||||
|
|
||||||
|
Everything below predates the "ship interior + solar system + 2 hubs"
|
||||||
|
framing above — it was built as a generic "station hub" proof of concept
|
||||||
|
(item 29) and then given a real character-creation entry point in a later
|
||||||
|
session, before `station_hub.tscn` was pinned down as specifically the
|
||||||
|
Belters hub (see Vision above). What's actually built and working today
|
||||||
|
(verified via headless screenshot):
|
||||||
|
|
||||||
|
- **Character creation** (`menu/character_preview.gd`,
|
||||||
|
`menu/character_presets.gd`) — 7 pre-composed archetype sprites (Soldier,
|
||||||
|
Marine, Engineer, Laborer, Scavenger, Officer, Heavy Trooper), each with
|
||||||
|
idle + walk frames, picked on the main-menu PROFILE overlay with a live
|
||||||
|
idle/walk preview loop. Stored in `GameConfig.player_character_index`.
|
||||||
|
This is the *only* piece with a real menu entry point today.
|
||||||
|
- **`world/levels/station_hub.tscn`** — one hand-built 12×9 room
|
||||||
|
(`station_hub.gd`, `class_name StationHub`), procedurally tiled floor +
|
||||||
|
wall ring with a door gap, 4 static furniture pieces (locker, cabinet,
|
||||||
|
hydroponics plant rack, server console), wall colliders auto-built from
|
||||||
|
whatever's painted on the `Walls` `TileMapLayer`.
|
||||||
|
- **`world/levels/on_foot_character.gd`** (`CharacterBody2D`) — 4-directional
|
||||||
|
top-down walker, idle/walk sprite swap + `flip_h`, camera follow. Not
|
||||||
|
networked — single local `CharacterBody2D`, no `MultiplayerSpawner`, no
|
||||||
|
server authority. Loads whichever archetype was chosen at character
|
||||||
|
creation via `CharacterPresets.get_preset(GameConfig.player_character_index)`.
|
||||||
|
- **Not reachable from any menu.** The only way to see this scene today is
|
||||||
|
launching `station_hub.tscn` directly (editor Play, or the `--hub-test`
|
||||||
|
throwaway autopilot used to verify it — deleted after use, not in the
|
||||||
|
repo).
|
||||||
|
- **No NPCs, no interaction, no dialogue, no second room/transition.**
|
||||||
|
- **Ship interior (phased-plan step 2 — done)** — `world/levels/ship_interior.tscn`
|
||||||
|
+ `ship_interior.gd` (`class_name ShipInterior`), a new single-player/
|
||||||
|
local-only scene distinct from `station_hub.tscn` (which stays spoken for
|
||||||
|
as the Belters hub, see Vision above). Reuses `on_foot_character.gd`
|
||||||
|
as-is for the player controller and `station_hub.gd`'s wall-collider
|
||||||
|
pattern, but not that scene's assets wholesale — no dedicated
|
||||||
|
ship-interior art exists yet, so it's built from the same ORC station
|
||||||
|
tileset/furniture as a placeholder (locker, cabinet, server rack; the
|
||||||
|
plant/hydroponics furniture was deliberately left out, since hydroponics
|
||||||
|
is a purchasable room per the decisions log below, not present on the
|
||||||
|
starter ship). A smaller 8×6 room (vs. the hub's 12×9) to read as a
|
||||||
|
personal ship rather than a public hub. Built procedurally in `_ready()`
|
||||||
|
(floor/wall `TileMapLayer.set_cell()` calls, one door gap in the wall
|
||||||
|
ring) rather than hand-authored into the `.tscn`'s `tile_map_data`, since
|
||||||
|
`station_hub.tscn`'s own layout was originally produced the same
|
||||||
|
code-generated way (see item 29) rather than painted in an interactive
|
||||||
|
editor session. Verified via a throwaway autopilot + headless Xvfb
|
||||||
|
screenshot (same technique as item 29): floor tile variety, the wall
|
||||||
|
ring's door gap, all 3 furniture pieces, and wall-collision (player stops
|
||||||
|
cleanly at the east wall instead of clipping through) all confirmed
|
||||||
|
rendering/working correctly; autopilot and its temporary `project.godot`
|
||||||
|
autoload entry were removed after. **Now reachable from the main menu** —
|
||||||
|
see the CONTINUE/NEW entry point below (originally slated for step 3,
|
||||||
|
landed early alongside this step in practice).
|
||||||
|
- **In-ship navigation table (phased-plan step 3 — done)** —
|
||||||
|
`world/levels/nav_table.gd` (`class_name NavTable`, an `Area2D` placed in
|
||||||
|
`ship_interior.tscn`) is a Helldivers-2-style interactable: walking into
|
||||||
|
its range shows a "PRESS E" prompt (new `interact` input action, E key),
|
||||||
|
and pressing it opens `world/levels/nav_table_ui.gd`
|
||||||
|
(`class_name NavTableUI`, `CanvasLayer`, `layer = 20` — same layering
|
||||||
|
convention as `TeamSelect`/`SettingsPanel`) — a graph of 3 nodes (SPACE
|
||||||
|
COMBAT, BELTERS HUB, MILITARY HUB) radiating from a center "YOUR SHIP"
|
||||||
|
marker, connected by `Line2D` routes. A new `GameConfig.nav_table_focused`
|
||||||
|
flag (same pattern as `chat_focused`/`team_select_focused`/
|
||||||
|
`settings_focused`) gates `OnFootCharacter` movement while it's open.
|
||||||
|
**SPACE COMBAT** is QUICK PLAY's old queue/connect flow, moved here
|
||||||
|
verbatim from `main_menu.gd` (which lost its QUICK PLAY nav button
|
||||||
|
entirely — `NAV_ITEMS` is now just `["OPTIONS", "PROFILE", "QUIT"]`):
|
||||||
|
`NavTableUI` owns the `NetworkManager.connection_succeeded/failed` and
|
||||||
|
`MatchmakingClient.match_found/search_failed` signal wiring and shows
|
||||||
|
queueing/connecting status inline in the graph panel, then swaps to
|
||||||
|
`world.tscn` on success. **BELTERS HUB**/**MILITARY HUB** are straight
|
||||||
|
`change_scene_to_file` calls to `station_hub.tscn`/`military_hub.tscn`.
|
||||||
|
Caught and fixed one real bug via the autopilot verification below:
|
||||||
|
`nav_table_focused` was only ever cleared by `close()`, so picking a
|
||||||
|
destination (which changes scene without closing the panel) left it stuck
|
||||||
|
`true` forever, silently freezing player movement in every future ship
|
||||||
|
visit — all 3 destination handlers now clear the flag before changing
|
||||||
|
scene.
|
||||||
|
- **Military hub is a functional placeholder, not a stub** (see Decisions
|
||||||
|
below) — `world/levels/military_hub.tscn` reuses `station_hub.gd`
|
||||||
|
(`class_name StationHub`, generic — no ORC-specific logic) and the same
|
||||||
|
ORC tileset/furniture art as `station_hub.tscn` outright, as its own
|
||||||
|
distinct scene/instance rather than literally reopening the Belters hub.
|
||||||
|
A real ISN art pass (phased-plan step 4) swaps the art later without
|
||||||
|
touching the nav-table wiring.
|
||||||
|
- **Return paths**: a new reusable `world/levels/exit_zone.gd`
|
||||||
|
(`class_name ExitZone`, `Area2D`, exported `target_scene`) triggers a
|
||||||
|
scene change the instant the player's body enters it — unlike
|
||||||
|
`NavTable`, which needs an explicit interact press, this matches walking
|
||||||
|
through a doorway. Both hub scenes got one positioned at their existing
|
||||||
|
door sprite, routing back to `ship_interior.tscn`. `menu/pause_menu.gd`'s
|
||||||
|
"QUIT TO MENU" became **"QUIT TO SHIP"**, changing scene to
|
||||||
|
`ship_interior.tscn` instead of `main_menu.tscn` (still disconnects via
|
||||||
|
`NetworkManager.disconnect_from_game()` first) — the ship is now the
|
||||||
|
only way out of a match, matching its status as home base.
|
||||||
|
- Verified end-to-end with a throwaway `--navtest`-gated autopilot
|
||||||
|
autoload (removed after use, same technique as this doc's other
|
||||||
|
verification notes): a real headless server was hosted and registered
|
||||||
|
with `matchmaking-api` (confirmed `status: "available"` via
|
||||||
|
`GET /servers`), then a windowed client under Xvfb was driven through
|
||||||
|
the full loop — main menu → ship → open nav table (screenshotted, see
|
||||||
|
below) → Belters hub → back to ship → Military hub → back to ship →
|
||||||
|
Space Combat → real matchmaking + connect → in `world.tscn` → pause
|
||||||
|
menu's quit-to-ship → back in the ship interior — with no errors and
|
||||||
|
the `nav_table_focused` fix confirmed (`focused=false` after each hub
|
||||||
|
return, where it previously stuck `true`).
|
||||||
|
- **RAM currency backend (phased-plan step 1 — done)** — `Player.ram_kb`
|
||||||
|
(BigInteger) in `matchmaking-api`, credited on `POST /matches/report` via
|
||||||
|
a tunable participation/per-kill/win-bonus formula (`app/config.py`,
|
||||||
|
currently 50/15/200 KB — placeholder numbers), returned by
|
||||||
|
`GET /stats/{callsign}`. Client side: `autoload/currency.gd`
|
||||||
|
(`class_name Currency`) formats KB up into KB/MB/GB/TB for display (1000
|
||||||
|
per step), `MatchmakingClient.get_stats()` fetches a callsign's balance,
|
||||||
|
and the main-menu profile badge shows it live next to rank/level as a
|
||||||
|
working end-to-end proof (verified via real match-report calls + a
|
||||||
|
headless screenshot — a fresh callsign correctly shows nothing until it
|
||||||
|
has a stats row, an existing one rolls over KB→MB→GB correctly at the
|
||||||
|
1000 boundary). No spend path yet — nothing to spend it on until the ship
|
||||||
|
interior/hubs below exist. See `matchmaking-api/README.md`'s "RAM
|
||||||
|
(in-game currency)" section for the backend details.
|
||||||
|
|
||||||
|
## Ship wear, supplies & repair
|
||||||
|
|
||||||
|
Combat leaves your ship **visibly broken** when you're back in the
|
||||||
|
interior — not just a hidden stat. Two related but distinct systems:
|
||||||
|
|
||||||
|
- **Repairable damage** — things that break and need fixing:
|
||||||
|
- **Hull** — takes damage in combat, needs repair.
|
||||||
|
- **Guns/weapons** — also take damage in combat, repaired separately from
|
||||||
|
the hull.
|
||||||
|
- **Consumable supplies** — things that deplete through play and need
|
||||||
|
restocking, not "broken" so much as "used up":
|
||||||
|
- **Fuel**
|
||||||
|
- **Oxygen**
|
||||||
|
- **Ammo**
|
||||||
|
|
||||||
|
Both repairs and supplies are **bought at the hubs with RAM** — no separate
|
||||||
|
"supplies" currency, it's a RAM purchase like anything else there. This is
|
||||||
|
the mechanic that makes hub visits load-bearing rather than optional: you
|
||||||
|
fight → come home with a damaged hull/guns and depleted fuel/oxygen/ammo →
|
||||||
|
go to a hub and spend RAM → return and repair/restock. **Repairing and
|
||||||
|
restocking both take time** — not instant on purchase (consistent with
|
||||||
|
crops also being a wait → harvest loop, see Vision above; the ship
|
||||||
|
interior's whole maintenance layer runs on "spend, then wait" pacing, not
|
||||||
|
"spend, get instantly").
|
||||||
|
|
||||||
|
**Damage formula (resolved):** every death in combat randomly drains either
|
||||||
|
the hull or damages the weapons (one or the other per death, picked
|
||||||
|
randomly — not both every time). This accumulates across matches; **50
|
||||||
|
deaths' worth** of accumulated damage fully depletes that system, at which
|
||||||
|
point RAM has to be spent at a hub to repair it. Purely deaths-driven, not
|
||||||
|
tied to damage-taken or match outcome.
|
||||||
|
|
||||||
|
Still needs, before it can be built:
|
||||||
|
|
||||||
|
- How fast fuel/oxygen/ammo deplete (a separate formula from the
|
||||||
|
deaths-based hull/gun one above — not yet specified), and what happens if
|
||||||
|
they run out (blocked from queueing another match? a gameplay penalty
|
||||||
|
mid-match?)
|
||||||
|
|
||||||
|
This depends on combat generating *some* persistent-across-matches wear/
|
||||||
|
consumption state, which does not exist today — `ship_movement.gd`
|
||||||
|
currently resets health/state fully on every respawn and between matches,
|
||||||
|
nothing survives to be "broken" or "depleted" afterward, and there's no
|
||||||
|
existing fuel/oxygen/ammo concept in combat at all (weapons currently cost
|
||||||
|
*energy*, which already regenerates freely mid-match per `CLAUDE.md` item
|
||||||
|
23 — that's a separate system from this new persistent ammo-supply concept,
|
||||||
|
not to be conflated). That plumbing has to be built as part of this, not
|
||||||
|
assumed already there.
|
||||||
|
|
||||||
|
## Ship upgrades = new rooms
|
||||||
|
|
||||||
|
Upgrades aren't menu-purchased stat tweaks — they're **new rooms added onto
|
||||||
|
your ship**, walked into like everything else. This is a meatier build than
|
||||||
|
a flat stat upgrade: it means the ship interior can't be one fixed
|
||||||
|
`station_hub.tscn`-style single room — it needs to support **growing its
|
||||||
|
own floor plan** as rooms get added, which is a real architectural
|
||||||
|
difference from every other on-foot space in this doc (the 2 hubs and any
|
||||||
|
v2 solar-system stops are all still just fixed single rooms/scenes).
|
||||||
|
|
||||||
|
**Resolved:**
|
||||||
|
- A room's only function is granting access to the system it houses — it's
|
||||||
|
a gate, not also a stacking stat/capacity bonus on top of granting access.
|
||||||
|
- Hydroponics (crops) is not a starter feature. It's a purchasable room like
|
||||||
|
every other upgrade — the plant-rack furniture already cropped for
|
||||||
|
`station_hub.tscn` (see Asset Inventory below) is useful reference art for
|
||||||
|
it, but the starter ship doesn't have it until bought.
|
||||||
|
|
||||||
|
**Still open:**
|
||||||
|
- What a room being "added" looks like mechanically — a new connected room
|
||||||
|
revealed/unlocked in a pre-built larger ship shell (simpler: author the
|
||||||
|
full possible floor plan up front, gate rooms behind a
|
||||||
|
locked-door-until-purchased flag), vs. actually procedurally
|
||||||
|
attaching/generating new room geometry at purchase time (much harder, and
|
||||||
|
nothing in the codebase does anything like this today — closest analog is
|
||||||
|
`station_hub.gd`'s procedural single-room tile painting, which doesn't
|
||||||
|
generalize to "graft a new room onto an existing layout").
|
||||||
|
|
||||||
|
## Asset inventory
|
||||||
|
|
||||||
|
Source: `world/levels/spritesheet.png` (a single AI-generated reference
|
||||||
|
sheet — has a `source/` jpeg convention like the race art, see `CLAUDE.md`
|
||||||
|
item 17). Cropped into `assets/images/worldsprites/character/`,
|
||||||
|
`assets/images/worldtiles/`, `assets/images/worldsprites/furniture/`.
|
||||||
|
|
||||||
|
| Category | Used | Still on the sheet, uncropped |
|
||||||
|
|---|---|---|
|
||||||
|
| Characters | 7 archetypes × (idle+walk) — appears to be the full set the sheet has | — (all 5 character rows are used) |
|
||||||
|
| Floor tiles | 11 variants → padded/cycled to the 14-slot atlas | — (Flooring block fully used) |
|
||||||
|
| Wall tiles | 6 clean panel variants → cycled to 14 | rock/asteroid-wall chunks, 2 blend-mask swatches (not directly usable as tiles as-is) |
|
||||||
|
| Doors | 1 (plain sliding door) | 2 more door variants (open, blast-door), several branded panel doors |
|
||||||
|
| Furniture | 4 (locker, cabinet, plant rack, server console) | bunks (3 variants), desks + chairs, monitor stations, weapon/gear lockers (cage shelving), crates, workbenches, gas canisters, more potted plants, a fish-tank-style prop |
|
||||||
|
| Lighting fixtures | 0 | ~10 wall/ceiling light variants, fully uncropped |
|
||||||
|
|
||||||
|
Furniture/lighting has a lot of unused headroom for filling out a bigger or
|
||||||
|
second room later — this is not a blocker for anything above, just noting
|
||||||
|
it's there.
|
||||||
|
|
||||||
|
## Rough phased plan (v1 = ship interior + 2 hubs, nothing else — see Version Scope)
|
||||||
|
|
||||||
|
1. ~~Currency: server-authoritative RAM hookup~~ — **done**, see "Current
|
||||||
|
implementation state" above and `matchmaking-api/README.md`. Payout
|
||||||
|
happens via the existing `POST /matches/report` call
|
||||||
|
(`MatchManager`'s `POST_MATCH` phase, `CLAUDE.md` item 26) rather than a
|
||||||
|
new endpoint. Note this landed ahead of step 2 (ship interior) in
|
||||||
|
practice — reasonable since it has no dependency on the ship existing,
|
||||||
|
just needed *a* balance to exist before anything can spend it later.
|
||||||
|
2. ~~Ship interior as its own new scene~~ — **done**, see
|
||||||
|
`world/levels/ship_interior.tscn`/`ship_interior.gd` and "Current
|
||||||
|
implementation state" above. Single-player, local-only, no networking —
|
||||||
|
reused `on_foot_character.gd` and `station_hub.gd`'s room-building/
|
||||||
|
collider approach as the starting pattern (not the `station_hub.tscn`
|
||||||
|
instance itself, which is spoken for as the Belters hub — see Vision
|
||||||
|
above).
|
||||||
|
|
||||||
|
**Main-menu entry point into the ship (part of step 3, landed early,
|
||||||
|
same "no dependency" reasoning as step 1 landing ahead of step 2) —
|
||||||
|
done.** `main_menu.gd`'s nav gained a dynamic top item, CONTINUE (a
|
||||||
|
callsign/character already exists) or NEW (none yet), replacing the
|
||||||
|
old static `NAV_ITEMS` first slot; both drop the player straight into
|
||||||
|
`ship_interior.tscn` (`_enter_ship()`). NEW reuses the existing PROFILE
|
||||||
|
overlay (`_open_profile()`'s `pending_action` param generalized from the
|
||||||
|
old `pending_quick_play` bool to also carry `"enter_ship"`, alongside
|
||||||
|
the pre-existing `"quick_play"` case) so a fresh player creates their
|
||||||
|
pilot first, then lands in the ship on Save — same forced-open pattern
|
||||||
|
QUICK PLAY already used for a missing callsign, just a second pending
|
||||||
|
action instead of a second overlay. **`SERVER SELECT` was removed from
|
||||||
|
the main-menu nav entirely** (`menu/server_select.gd`/`.tscn` left on
|
||||||
|
disk unreferenced, same precedent as the orphaned `terran/mech/vorg`/
|
||||||
|
`apex` asset folders from items 17/19) — no replacement entry point for
|
||||||
|
it yet, since server browsing doesn't have a home on the nav table
|
||||||
|
either until that's built next. QUICK PLAY/OPTIONS/PROFILE/QUIT are
|
||||||
|
otherwise unchanged. Verified via a throwaway autopilot driving
|
||||||
|
`main_menu.gd`'s own `_on_continue_pressed()`/`_on_profile_save()`
|
||||||
|
methods directly (same legitimate `_`-prefixed-access technique as
|
||||||
|
prior sessions) plus headless Xvfb screenshots: NEW label + profile
|
||||||
|
overlay opening with an "enter_ship"-specific hint text, landing in the
|
||||||
|
ship on Save, and the label correctly flipping to CONTINUE back on the
|
||||||
|
menu once a callsign exists.
|
||||||
|
3. ~~Navigation table inside the ship~~ — **done**, see "Current
|
||||||
|
implementation state" above (`world/levels/nav_table.gd`/
|
||||||
|
`nav_table_ui.gd`). QUICK PLAY's queue/connect logic now lives behind the
|
||||||
|
SPACE COMBAT node; Belters/Military hub nodes are the other two. A real
|
||||||
|
server-browsing entry point to replace the removed SERVER SELECT is still
|
||||||
|
not built — no node claims that slot yet, it just isn't reachable from
|
||||||
|
anywhere right now.
|
||||||
|
4. Military (ISN) hub — **placeholder done, real art pass still open.**
|
||||||
|
`military_hub.tscn` exists and is reachable today, but reuses the
|
||||||
|
Belters hub's ORC art/tileset outright rather than a dedicated
|
||||||
|
gunmetal-gray/disciplined ISN look (see Vision above) — that art pass,
|
||||||
|
and re-skinning this scene with it, is the remaining work here. The
|
||||||
|
room-building code (`station_hub.gd`) already carries over directly, as
|
||||||
|
expected — no code changes needed once the art exists, just new
|
||||||
|
textures swapped into a cloned `.tscn`.
|
||||||
|
5. Networked pass on both hubs — `MultiplayerSpawner`, position replication
|
||||||
|
for on-foot characters, reusing whatever the local-only ship-interior
|
||||||
|
controller established in step 2.
|
||||||
|
6. Ship wear, supplies & repair loop (see dedicated section above) — the
|
||||||
|
mechanic that actually ties hubs into ship maintenance. Needs combat to
|
||||||
|
generate persistent hull/gun-damage and fuel/oxygen/ammo-consumption
|
||||||
|
state first (none of which exists today — and is distinct from the
|
||||||
|
existing per-shot *energy* system, `CLAUDE.md` item 23, which stays
|
||||||
|
as-is), then the broken/depleted states in the ship interior, then
|
||||||
|
repairs/supplies for sale at both hubs, then the wait-time pacing on top.
|
||||||
|
7. Cosmetic decoration — the one maintenance system that doesn't depend on
|
||||||
|
the room-upgrade architecture (step 8) or the wear loop (step 6). Can be
|
||||||
|
built any time after the ship interior (step 2) exists.
|
||||||
|
8. Room-based upgrades (see "Ship upgrades = new rooms" above) — the
|
||||||
|
heaviest remaining lift, since it needs the ship interior to support a
|
||||||
|
growable floor plan rather than the fixed-single-room approach every
|
||||||
|
other on-foot scene uses. Do this after the fixed-room approach has
|
||||||
|
already shipped for decoration/hubs, so there's a working baseline before
|
||||||
|
taking on the harder architectural problem. **Crops live here too now**,
|
||||||
|
not as a separate item — hydroponics is a purchasable room like any other
|
||||||
|
upgrade (see "Ship upgrades = new rooms" above), not a starter feature,
|
||||||
|
so it can't ship before the room-upgrade system exists.
|
||||||
|
9. **V2, not v1:** additional solar-system exploration locations as more
|
||||||
|
nav-table nodes. NPCs/dialogue, if still wanted — needs a minimal
|
||||||
|
dialogue/prompt UI that doesn't exist anywhere in the project yet.
|
||||||
|
|
||||||
|
## Decisions log
|
||||||
|
|
||||||
|
- [x] `station_hub.tscn` is the Belters (ORC) hub, not the ship interior and
|
||||||
|
not a generic solar-system stop.
|
||||||
|
- [x] In-game currency is 100% separate from `overview/money.md`'s
|
||||||
|
real-money monetization.
|
||||||
|
- [x] Currency is named **RAM** (EVE Online ISK-style), server-authoritative
|
||||||
|
in `matchmaking-api`.
|
||||||
|
- [x] Ship is the home base — combat and hub travel launch from inside it,
|
||||||
|
not straight off the main menu, via an in-ship navigation table.
|
||||||
|
- [x] The nav table is a Helldivers-2-style solar-system graph; v1 has 3
|
||||||
|
destinations (combat, Belters hub, Military hub).
|
||||||
|
- [x] **V1 scope is ship interior + the 2 hubs only.** Solar-system
|
||||||
|
exploration locations are v2.
|
||||||
|
- [x] Ship wear covers hull damage and gun damage (repairable), plus fuel,
|
||||||
|
oxygen, and ammo (consumable supplies) — bought at the hubs, RAM only
|
||||||
|
(no separate supplies currency), and both repairs and restocking take
|
||||||
|
time rather than being instant.
|
||||||
|
- [x] Ship maintenance covers crops + cosmetic decoration + room-based
|
||||||
|
upgrades + repairs/supplies (all 4 planned for v1, sequenced in the
|
||||||
|
phased plan above — not simultaneous, and not blocking the
|
||||||
|
hub/nav-table work).
|
||||||
|
- [x] Upgrades are new rooms added onto the ship, not a stat menu — the ship
|
||||||
|
interior needs a growable floor plan, not a fixed single room (see
|
||||||
|
"Ship upgrades = new rooms" above).
|
||||||
|
- [x] A room's only function is granting access to the system it houses —
|
||||||
|
not also a stacking stat/capacity bonus.
|
||||||
|
- [x] Hydroponics (crops) is a purchasable room, not present on the starter
|
||||||
|
ship — so it's part of the room-upgrade system (phased-plan step 8),
|
||||||
|
not shippable before that system exists.
|
||||||
|
- [x] Hull/gun damage formula: every combat death randomly drains the hull
|
||||||
|
*or* damages the weapons (one or the other, random per death); 50
|
||||||
|
deaths' worth fully depletes that system and requires a RAM repair at
|
||||||
|
a hub. Purely deaths-driven, not tied to damage taken or match result.
|
||||||
|
- [x] NPCs are deferred past the first playable version.
|
||||||
|
- [x] Military hub is a functional placeholder (its own scene, reusing the
|
||||||
|
Belters hub's ORC art/script wholesale) rather than locked/coming-soon
|
||||||
|
— playable now, real ISN art swapped in later without touching the
|
||||||
|
nav-table wiring.
|
||||||
|
- [x] Belters/Military hub exits and a match's pause-menu quit both route
|
||||||
|
back to `ship_interior.tscn` (not the main menu) — the nav table is a
|
||||||
|
real hub-and-spoke loop, not a one-way trip. Pause menu's "QUIT TO
|
||||||
|
MENU" is now "QUIT TO SHIP".
|
||||||
|
|
||||||
|
## Still open
|
||||||
|
|
||||||
|
- [ ] A dedicated ISN art pass for the Military hub (gunmetal-gray,
|
||||||
|
disciplined, distinct from the Belters hub's gritty patchwork look) —
|
||||||
|
`military_hub.tscn` exists and is playable today but is reusing ORC
|
||||||
|
art as a stand-in (see phased-plan step 4).
|
||||||
|
- [ ] A real server-browsing entry point (the old, now-removed SERVER
|
||||||
|
SELECT) — no nav-table node or other menu surface reaches it yet.
|
||||||
|
- [ ] Fuel/oxygen/ammo depletion rate/formula (separate from the
|
||||||
|
deaths-based hull/gun formula above), and what happens if they run
|
||||||
|
out (blocked from queueing? a mid-match penalty?).
|
||||||
|
- [ ] How a purchased room actually gets added to the ship — pre-built
|
||||||
|
shell with locked-until-bought rooms, vs. real procedural
|
||||||
|
attach/generate at purchase time (see "Ship upgrades = new rooms"
|
||||||
|
above; leaning toward the pre-built/locked-room approach as the
|
||||||
|
cheaper build, not decided).
|
||||||
|
- [ ] Exact list of cosmetic decorations for a first pass — not needed until
|
||||||
|
step 7 of the phased plan.
|
||||||
|
- [ ] Exact list of purchasable rooms beyond hydroponics (armory for
|
||||||
|
ammo/guns? a fuel-tank room? an oxygen/life-support room?) — not
|
||||||
|
needed until step 8 of the phased plan.
|
||||||
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 204 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://btbs07a8uljj1"
|
||||||
|
path="res://.godot/imported/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg-0011f14f0b2893e5f3e09723d59e6d89.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/ships/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg-0011f14f0b2893e5f3e09723d59e6d89.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
|
||||||
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://defa378daeqjh"
|
||||||
|
path="res://.godot/imported/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg-121eb26e72146fe5f9d2445e45188ecf.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/ships/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg-121eb26e72146fe5f9d2445e45188ecf.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
|
||||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dfoqqkd4kv3w1"
|
||||||
|
path="res://.godot/imported/armor_idle.png-2f82efbe1bff7cd562a9173ea0893e3b.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/armor_idle.png"
|
||||||
|
dest_files=["res://.godot/imported/armor_idle.png-2f82efbe1bff7cd562a9173ea0893e3b.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
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cwfl33285ksde"
|
||||||
|
path="res://.godot/imported/armor_walk.png-5057d2cd82915e9deff684d008ca6fc4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/armor_walk.png"
|
||||||
|
dest_files=["res://.godot/imported/armor_walk.png-5057d2cd82915e9deff684d008ca6fc4.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
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dgahd7dstvrc4"
|
||||||
|
path="res://.godot/imported/engineer_idle.png-87ca66b9480bc65728973a02c059059c.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/engineer_idle.png"
|
||||||
|
dest_files=["res://.godot/imported/engineer_idle.png-87ca66b9480bc65728973a02c059059c.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
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cdwl43yw80m7m"
|
||||||
|
path="res://.godot/imported/engineer_walk.png-b5d6ffa913fd64fce8fd2898e6152e44.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/engineer_walk.png"
|
||||||
|
dest_files=["res://.godot/imported/engineer_walk.png-b5d6ffa913fd64fce8fd2898e6152e44.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
|
||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://seojffyrfioi"
|
||||||
|
path="res://.godot/imported/laborer_idle.png-50de51857529f064913edcf5174d3e60.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/laborer_idle.png"
|
||||||
|
dest_files=["res://.godot/imported/laborer_idle.png-50de51857529f064913edcf5174d3e60.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
|
||||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bm1bxsvjjq5pe"
|
||||||
|
path="res://.godot/imported/laborer_walk.png-202645da273b382efcffb72df37335da.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/laborer_walk.png"
|
||||||
|
dest_files=["res://.godot/imported/laborer_walk.png-202645da273b382efcffb72df37335da.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
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bhvn82imgrrdv"
|
||||||
|
path="res://.godot/imported/marine_idle.png-decba940b1e070cab7f32d48cd47e614.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/marine_idle.png"
|
||||||
|
dest_files=["res://.godot/imported/marine_idle.png-decba940b1e070cab7f32d48cd47e614.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
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bnlivrk518dxg"
|
||||||
|
path="res://.godot/imported/marine_walk.png-f42f087095ba6b04b0f2e990676140eb.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/marine_walk.png"
|
||||||
|
dest_files=["res://.godot/imported/marine_walk.png-f42f087095ba6b04b0f2e990676140eb.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
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://5ve0wtgcmqo6"
|
||||||
|
path="res://.godot/imported/officer_idle.png-2be41b8a497f2290de12d9f2ef404925.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/officer_idle.png"
|
||||||
|
dest_files=["res://.godot/imported/officer_idle.png-2be41b8a497f2290de12d9f2ef404925.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
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://djpyvqnbs5nm3"
|
||||||
|
path="res://.godot/imported/officer_walk.png-4cc5308e92afd612845edb4aefa4970f.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/officer_walk.png"
|
||||||
|
dest_files=["res://.godot/imported/officer_walk.png-4cc5308e92afd612845edb4aefa4970f.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
|
||||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://os8r8wgciiax"
|
||||||
|
path="res://.godot/imported/scavenger_idle.png-8132b845cfa209806000561b8cc987f2.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/scavenger_idle.png"
|
||||||
|
dest_files=["res://.godot/imported/scavenger_idle.png-8132b845cfa209806000561b8cc987f2.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
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c0g2587hjurhl"
|
||||||
|
path="res://.godot/imported/scavenger_walk.png-474d74921a3b5034f6c9bf4d7d8d2471.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/scavenger_walk.png"
|
||||||
|
dest_files=["res://.godot/imported/scavenger_walk.png-474d74921a3b5034f6c9bf4d7d8d2471.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
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dok0oj2avem2g"
|
||||||
|
path="res://.godot/imported/soldier_idle.png-4709623b32bb1409bf1d754ddcc1e291.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/soldier_idle.png"
|
||||||
|
dest_files=["res://.godot/imported/soldier_idle.png-4709623b32bb1409bf1d754ddcc1e291.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
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://u415d3kddeh0"
|
||||||
|
path="res://.godot/imported/soldier_walk.png-ab5b08a73427300f5908eabf04977357.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/character/soldier_walk.png"
|
||||||
|
dest_files=["res://.godot/imported/soldier_walk.png-ab5b08a73427300f5908eabf04977357.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
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://beiyomi67nvln"
|
||||||
|
path="res://.godot/imported/cabinet.png-81b9230125498c42eff4c5e2a89664f8.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/furniture/cabinet.png"
|
||||||
|
dest_files=["res://.godot/imported/cabinet.png-81b9230125498c42eff4c5e2a89664f8.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
|
||||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c5m6vma51lbxd"
|
||||||
|
path="res://.godot/imported/locker.png-8cca49f3e3733cf983c09e978919473a.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/furniture/locker.png"
|
||||||
|
dest_files=["res://.godot/imported/locker.png-8cca49f3e3733cf983c09e978919473a.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
|
||||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dtvfg5q2o0xnx"
|
||||||
|
path="res://.godot/imported/plant.png-a10ef1c7abb27caf4d26dd43a1187291.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/furniture/plant.png"
|
||||||
|
dest_files=["res://.godot/imported/plant.png-a10ef1c7abb27caf4d26dd43a1187291.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
|
||||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://br7bwwcuaikdb"
|
||||||
|
path="res://.godot/imported/server_rack.png-14c0c511f56b8aa64850b6c93eaeda9a.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/furniture/server_rack.png"
|
||||||
|
dest_files=["res://.godot/imported/server_rack.png-14c0c511f56b8aa64850b6c93eaeda9a.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
|
||||||
|
After Width: | Height: | Size: 141 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c3lroomgvih1i"
|
||||||
|
path="res://.godot/imported/orc_character_roster_source.jpeg-4a9ed72bb7d862488a0185bc8d540042.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/orc/orc_character_roster_source.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/orc_character_roster_source.jpeg-4a9ed72bb7d862488a0185bc8d540042.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
|
||||||
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bi6720yj2f5im"
|
||||||
|
path="res://.godot/imported/orc_furniture_sheet_source.jpeg-7ddc98842928a9efcb921df3e9a65438.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/orc/orc_furniture_sheet_source.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/orc_furniture_sheet_source.jpeg-7ddc98842928a9efcb921df3e9a65438.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
|
||||||
|
After Width: | Height: | Size: 2.2 MiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c4pv2och0ovpu"
|
||||||
|
path="res://.godot/imported/spritesheet.png-47f1727966668bc132dd8ee0982756e6.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldsprites/orc/spritesheet.png"
|
||||||
|
dest_files=["res://.godot/imported/spritesheet.png-47f1727966668bc132dd8ee0982756e6.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
|
||||||
|
After Width: | Height: | Size: 99 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ciu4tyyv8h3i1"
|
||||||
|
path="res://.godot/imported/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg-3c3e1a0cb799bac42f4bd3cb72ca8f25.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldtiles/source/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg-3c3e1a0cb799bac42f4bd3cb72ca8f25.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
|
||||||
|
After Width: | Height: | Size: 175 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ce7eu3mgcow85"
|
||||||
|
path="res://.godot/imported/4b196425-afc8-407a-aed3-bec051324333.jpeg-e562b7030f906c9cf524497417cda608.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldtiles/source/4b196425-afc8-407a-aed3-bec051324333.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/4b196425-afc8-407a-aed3-bec051324333.jpeg-e562b7030f906c9cf524497417cda608.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
|
||||||
|
After Width: | Height: | Size: 163 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b336dmxpyg2gh"
|
||||||
|
path="res://.godot/imported/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg-3aecddbe3da5ada37efd3b3ddfb1a2f4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldtiles/source/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg-3aecddbe3da5ada37efd3b3ddfb1a2f4.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
|
||||||
|
After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dlvc4kixq56mt"
|
||||||
|
path="res://.godot/imported/orc_station_tileset_reference_source.jpeg-957d05efb5712c2f55eec8156a465142.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldtiles/source/orc_station_tileset_reference_source.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/orc_station_tileset_reference_source.jpeg-957d05efb5712c2f55eec8156a465142.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
|
||||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bpdb1ou43elae"
|
||||||
|
path="res://.godot/imported/station_door.png-d50d127a344c0b29259c58e6ff93180f.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldtiles/station_door.png"
|
||||||
|
dest_files=["res://.godot/imported/station_door.png-d50d127a344c0b29259c58e6ff93180f.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
|
||||||
|
After Width: | Height: | Size: 73 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bndn236w4pj0x"
|
||||||
|
path="res://.godot/imported/station_floor.png-4efe9ad5dc167e01b6f1ce970a24cfa0.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldtiles/station_floor.png"
|
||||||
|
dest_files=["res://.godot/imported/station_floor.png-4efe9ad5dc167e01b6f1ce970a24cfa0.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
|
||||||
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dvb5ni8hxhrmk"
|
||||||
|
path="res://.godot/imported/station_walls.png-3b0fea72982af3446ef23cc9f1e1d700.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/worldtiles/station_walls.png"
|
||||||
|
dest_files=["res://.godot/imported/station_walls.png-3b0fea72982af3446ef23cc9f1e1d700.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
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends Node
|
||||||
|
class_name Currency
|
||||||
|
|
||||||
|
# RAM is the in-game currency (see overview/onfoot.md) -- server-authoritative,
|
||||||
|
# stored and transmitted as a single integer count of kilobytes
|
||||||
|
# (matchmaking-api's Player.ram_kb). This class is purely the client-side
|
||||||
|
# display formatter: KB -> KB/MB/GB/TB, 1000 per step (not 1024).
|
||||||
|
|
||||||
|
const STEP := 1000.0
|
||||||
|
const UNITS := ["KB", "MB", "GB", "TB"]
|
||||||
|
|
||||||
|
|
||||||
|
static func format_ram(kb: int) -> String:
|
||||||
|
var value := float(kb)
|
||||||
|
var unit_index := 0
|
||||||
|
while value >= STEP and unit_index < UNITS.size() - 1:
|
||||||
|
value /= STEP
|
||||||
|
unit_index += 1
|
||||||
|
if unit_index == 0:
|
||||||
|
return "%d %s" % [kb, UNITS[0]]
|
||||||
|
return "%.2f %s" % [value, UNITS[unit_index]]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ch23k14liwu2i
|
||||||
@@ -305,6 +305,12 @@ var pre_match_camera_zoom: float = 0.5
|
|||||||
# Race/ship/speed are per-peer now — see PlayerRegistry.
|
# Race/ship/speed are per-peer now — see PlayerRegistry.
|
||||||
var player_name: String = ""
|
var player_name: String = ""
|
||||||
|
|
||||||
|
# Index into CharacterPresets.PRESETS, chosen on the character-creation
|
||||||
|
# screen (main_menu.gd's PROFILE overlay). Drives both the menu badge and the
|
||||||
|
# on-foot walker's sprite (world/levels/on_foot_character.gd); same
|
||||||
|
# not-persisted lifetime as player_name above.
|
||||||
|
var player_character_index: int = 0
|
||||||
|
|
||||||
# Current map's play area, in world coordinates. Set by world.gd on load.
|
# Current map's play area, in world coordinates. Set by world.gd on load.
|
||||||
var world_bounds: Rect2 = Rect2(0, 0, 1152, 648)
|
var world_bounds: Rect2 = Rect2(0, 0, 1152, 648)
|
||||||
|
|
||||||
@@ -428,6 +434,12 @@ var team_select_focused: bool = false
|
|||||||
# menu underneath it (pause_menu.gd's _input() checks this too).
|
# menu underneath it (pause_menu.gd's _input() checks this too).
|
||||||
var settings_focused: bool = false
|
var settings_focused: bool = false
|
||||||
|
|
||||||
|
# True while NavTableUI is open (world/levels/nav_table_ui.gd) — same purpose
|
||||||
|
# as chat_focused/team_select_focused/settings_focused, gates
|
||||||
|
# OnFootCharacter movement so walking into the console doesn't also drive
|
||||||
|
# the character underneath the graph overlay.
|
||||||
|
var nav_table_focused: bool = false
|
||||||
|
|
||||||
# Match structure (autoload/match_manager.gd) — see overview/map1.md.
|
# Match structure (autoload/match_manager.gd) — see overview/map1.md.
|
||||||
# default_game_mode_id picks a GameMode subclass (world/game_modes/) via
|
# default_game_mode_id picks a GameMode subclass (world/game_modes/) via
|
||||||
# MatchManager's factory; every peer instantiates the same mode independently
|
# MatchManager's factory; every peer instantiates the same mode independently
|
||||||
@@ -435,6 +447,16 @@ var settings_focused: bool = false
|
|||||||
# replication. match_duration is the "7 minutes" a match's clock counts down
|
# replication. match_duration is the "7 minutes" a match's clock counts down
|
||||||
# once ships are released; match_pre_match_duration/match_post_match_duration
|
# once ships are released; match_pre_match_duration/match_post_match_duration
|
||||||
# are the countdown-before and banner-after windows around it.
|
# are the countdown-before and banner-after windows around it.
|
||||||
|
#
|
||||||
|
# GAME_MODE_IDS is the canonical ordered catalog behind default_game_mode_id
|
||||||
|
# (see MatchManager._create_mode()) — these double as the category tags on
|
||||||
|
# world.gd's MAPS entries, which is what a map's "categories" list is
|
||||||
|
# filtered against in World.pick_map(), and as the toggle filters on
|
||||||
|
# menu/server_select.gd's mode-category bar. Only Team Deathmatch has real
|
||||||
|
# scoring so far; the other three are stub GameMode subclasses that score
|
||||||
|
# like Team Deathmatch until they grow real zone/point mechanics — see
|
||||||
|
# CLAUDE.md item 27.
|
||||||
|
const GAME_MODE_IDS := ["team_deathmatch", "domination", "conquest", "king_of_the_hill"]
|
||||||
var default_game_mode_id: String = "team_deathmatch"
|
var default_game_mode_id: String = "team_deathmatch"
|
||||||
var match_pre_match_duration: float = 30.0
|
var match_pre_match_duration: float = 30.0
|
||||||
var match_duration: float = 420.0
|
var match_duration: float = 420.0
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ func _create_mode(mode_id: String) -> GameMode:
|
|||||||
match mode_id:
|
match mode_id:
|
||||||
"team_deathmatch":
|
"team_deathmatch":
|
||||||
return TeamDeathmatchMode.new()
|
return TeamDeathmatchMode.new()
|
||||||
|
"domination":
|
||||||
|
return DominationMode.new()
|
||||||
|
"conquest":
|
||||||
|
return ConquestMode.new()
|
||||||
|
"king_of_the_hill":
|
||||||
|
return KingOfTheHillMode.new()
|
||||||
_:
|
_:
|
||||||
return TeamDeathmatchMode.new()
|
return TeamDeathmatchMode.new()
|
||||||
|
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ func start_matchmaking(callsign: String, mode: String) -> void:
|
|||||||
|
|
||||||
|
|
||||||
# Returns {"ok": bool, "servers": Array} — one dict per row from GET /servers
|
# Returns {"ok": bool, "servers": Array} — one dict per row from GET /servers
|
||||||
# (ip, port, mode, status, player_count, max_players, last_heartbeat). "ok"
|
# (ip, port, mode, status, player_count, max_players, last_heartbeat,
|
||||||
# is false if the API couldn't be reached at all, distinct from a reachable
|
# game_mode, map_name). "ok" is false if the API couldn't be reached at all,
|
||||||
# API returning an empty list.
|
# distinct from a reachable API returning an empty list.
|
||||||
func list_servers() -> Dictionary:
|
func list_servers() -> Dictionary:
|
||||||
var result := await _request("GET", "/servers", null)
|
var result := await _request("GET", "/servers", null)
|
||||||
if result.get("code", 0) != 200:
|
if result.get("code", 0) != 200:
|
||||||
@@ -82,14 +82,19 @@ func list_servers() -> Dictionary:
|
|||||||
# creates a row, a recognized one refreshes it. Returns true on success;
|
# creates a row, a recognized one refreshes it. Returns true on success;
|
||||||
# NetworkManager doesn't hard-fail on a missed heartbeat (the API's own
|
# NetworkManager doesn't hard-fail on a missed heartbeat (the API's own
|
||||||
# stale-server sweep just marks it offline until the next one lands), so
|
# stale-server sweep just marks it offline until the next one lands), so
|
||||||
# callers can fire-and-forget this.
|
# callers can fire-and-forget this. game_mode/map_name are the actual
|
||||||
func register_server(ip: String, port: int, mode: String, player_count: int, max_players: int) -> bool:
|
# in-match GameMode id (e.g. "team_deathmatch") and map display name (e.g.
|
||||||
|
# "Sector Alpha") — distinct from `mode`, which is the matchmaking queue
|
||||||
|
# mode ("casual"); see menu/server_select.gd, which shows both.
|
||||||
|
func register_server(ip: String, port: int, mode: String, player_count: int, max_players: int, game_mode: String, map_name: String) -> bool:
|
||||||
var result := await _request("POST", "/servers/register", {
|
var result := await _request("POST", "/servers/register", {
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"port": port,
|
"port": port,
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
"player_count": player_count,
|
"player_count": player_count,
|
||||||
"max_players": max_players,
|
"max_players": max_players,
|
||||||
|
"game_mode": game_mode,
|
||||||
|
"map_name": map_name,
|
||||||
}, _heartbeat_http)
|
}, _heartbeat_http)
|
||||||
return result.get("code", 0) == 200
|
return result.get("code", 0) == 200
|
||||||
|
|
||||||
@@ -113,6 +118,18 @@ func report_match_result(server_ip: String, server_port: int, mode: String, winn
|
|||||||
return result.get("code", 0) == 200
|
return result.get("code", 0) == 200
|
||||||
|
|
||||||
|
|
||||||
|
# Returns {"ok": bool, "stats": Dictionary} -- "stats" has callsign/mmr/
|
||||||
|
# wins/losses/kills/deaths/hours_played/ram_kb (GET /stats/{callsign}).
|
||||||
|
# "ok" is false if the API couldn't be reached OR the callsign has no
|
||||||
|
# recorded stats yet (a brand-new player who hasn't queued/finished a match
|
||||||
|
# returns 404 -- not an error, just "nothing to show yet").
|
||||||
|
func get_stats(callsign: String) -> Dictionary:
|
||||||
|
var result := await _request("GET", "/stats/" + callsign.uri_encode(), null)
|
||||||
|
if result.get("code", 0) != 200:
|
||||||
|
return {"ok": false, "stats": {}}
|
||||||
|
return {"ok": true, "stats": result.body}
|
||||||
|
|
||||||
|
|
||||||
func cancel_matchmaking() -> void:
|
func cancel_matchmaking() -> void:
|
||||||
if not _searching:
|
if not _searching:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -111,8 +111,15 @@ func _stop_heartbeat() -> void:
|
|||||||
# Fire-and-forget: a missed heartbeat just leaves the API's last_heartbeat
|
# Fire-and-forget: a missed heartbeat just leaves the API's last_heartbeat
|
||||||
# stale, which its own sweep turns into "offline" after server_stale_seconds
|
# stale, which its own sweep turns into "offline" after server_stale_seconds
|
||||||
# (see matchmaking-api/app/routers/servers.py) — no retry logic needed here.
|
# (see matchmaking-api/app/routers/servers.py) — no retry logic needed here.
|
||||||
|
# World.pick_map() is called statically here (world.tscn hasn't even loaded
|
||||||
|
# yet at boot, when the first heartbeat fires) rather than read off a live
|
||||||
|
# World instance -- it's a pure function of GameConfig.default_game_mode_id
|
||||||
|
# and World.MAPS, so this independently lands on the exact same map World
|
||||||
|
# itself picks once it loads, with no replication needed (same pattern as
|
||||||
|
# TeamSelect.RACES).
|
||||||
func _register_with_matchmaking_api() -> void:
|
func _register_with_matchmaking_api() -> void:
|
||||||
await MatchmakingClient.register_server(_register_ip, _hosted_port, "casual", PlayerRegistry.players.size(), MAX_PLAYERS)
|
var map_info: Dictionary = World.pick_map(GameConfig.default_game_mode_id)
|
||||||
|
await MatchmakingClient.register_server(_register_ip, _hosted_port, "casual", PlayerRegistry.players.size(), MAX_PLAYERS, GameConfig.default_game_mode_id, map_info.name)
|
||||||
|
|
||||||
|
|
||||||
func _start_query_responder(query_port: int) -> void:
|
func _start_query_responder(query_port: int) -> void:
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
# Top-right static readout of this match's map name. World.pick_map() is a
|
||||||
|
# pure function of GameConfig.default_game_mode_id (see world/world.gd's
|
||||||
|
# MAPS), so every peer resolves the same value independently with no RPC
|
||||||
|
# needed -- same trick NetworkManager's server registration and
|
||||||
|
# menu/server_select.gd's mode filter already rely on. Sits directly above
|
||||||
|
# ping_display.gd's readout, which starts lower to leave room for this.
|
||||||
|
|
||||||
|
const MARGIN_X := 16.0
|
||||||
|
const MARGIN_TOP := 20.0
|
||||||
|
const WIDTH := 220.0
|
||||||
|
const HEIGHT := 26.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = 4
|
||||||
|
_build_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
var label := Label.new()
|
||||||
|
label.anchor_left = 1.0
|
||||||
|
label.anchor_right = 1.0
|
||||||
|
label.anchor_top = 0.0
|
||||||
|
label.anchor_bottom = 0.0
|
||||||
|
label.offset_left = -(MARGIN_X + WIDTH)
|
||||||
|
label.offset_right = -MARGIN_X
|
||||||
|
label.offset_top = MARGIN_TOP
|
||||||
|
label.offset_bottom = MARGIN_TOP + HEIGHT
|
||||||
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
label.add_theme_font_size_override("font_size", 15)
|
||||||
|
label.add_theme_color_override("font_color", Color(0.85, 0.87, 0.92))
|
||||||
|
label.text = World.pick_map(GameConfig.default_game_mode_id).name.to_upper()
|
||||||
|
add_child(label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dcs037f6tynnw
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://hud/map_name_display.gd" id="1_script"]
|
||||||
|
|
||||||
|
[node name="MapNameDisplay" type="CanvasLayer"]
|
||||||
|
script = ExtResource("1_script")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bbf4sob5sgkqt
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cor1re65vyu2t
|
||||||
@@ -5,10 +5,11 @@ extends CanvasLayer
|
|||||||
# menu/server_select.gd's pre-connect probe, which has no ENet connection
|
# menu/server_select.gd's pre-connect probe, which has no ENet connection
|
||||||
# yet to read a statistic from). Meaningless for the server itself (peer id
|
# yet to read a statistic from). Meaningless for the server itself (peer id
|
||||||
# 1 is the local server, RTT to itself is always ~0), so it stays hidden
|
# 1 is the local server, RTT to itself is always ~0), so it stays hidden
|
||||||
# there.
|
# there. MARGIN_TOP starts below map_name_display.gd's readout, which owns
|
||||||
|
# the top of this same corner.
|
||||||
|
|
||||||
const MARGIN_X := 16.0
|
const MARGIN_X := 16.0
|
||||||
const MARGIN_TOP := 20.0
|
const MARGIN_TOP := 54.0
|
||||||
const WIDTH := 100.0
|
const WIDTH := 100.0
|
||||||
const HEIGHT := 30.0
|
const HEIGHT := 30.0
|
||||||
const UPDATE_INTERVAL := 0.5
|
const UPDATE_INTERVAL := 0.5
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
extends Node
|
||||||
|
class_name CharacterPresets
|
||||||
|
|
||||||
|
# On-foot character archetypes, cropped from the unified station spritesheet
|
||||||
|
# (world/levels/spritesheet.png -> assets/images/worldsprites/character/).
|
||||||
|
# Each is a pre-composed idle+walk sprite pair, not separate skin/hair/clothing
|
||||||
|
# layers, so "character creation" here means picking one of these rather than
|
||||||
|
# a Stardew-style layered build-a-look. Shared by the main-menu creation
|
||||||
|
# screen (menu/pilot_portrait.gd) and the on-foot walker
|
||||||
|
# (world/levels/on_foot_character.gd), same pattern as TeamSelect.RACES.
|
||||||
|
|
||||||
|
const PRESETS := [
|
||||||
|
{"name": "Soldier", "idle": "res://assets/images/worldsprites/character/soldier_idle.png", "walk": "res://assets/images/worldsprites/character/soldier_walk.png"},
|
||||||
|
{"name": "Marine", "idle": "res://assets/images/worldsprites/character/marine_idle.png", "walk": "res://assets/images/worldsprites/character/marine_walk.png"},
|
||||||
|
{"name": "Engineer", "idle": "res://assets/images/worldsprites/character/engineer_idle.png", "walk": "res://assets/images/worldsprites/character/engineer_walk.png"},
|
||||||
|
{"name": "Laborer", "idle": "res://assets/images/worldsprites/character/laborer_idle.png", "walk": "res://assets/images/worldsprites/character/laborer_walk.png"},
|
||||||
|
{"name": "Scavenger", "idle": "res://assets/images/worldsprites/character/scavenger_idle.png", "walk": "res://assets/images/worldsprites/character/scavenger_walk.png"},
|
||||||
|
{"name": "Officer", "idle": "res://assets/images/worldsprites/character/officer_idle.png", "walk": "res://assets/images/worldsprites/character/officer_walk.png"},
|
||||||
|
{"name": "Heavy Trooper", "idle": "res://assets/images/worldsprites/character/armor_idle.png", "walk": "res://assets/images/worldsprites/character/armor_walk.png"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
static func get_preset(index: int) -> Dictionary:
|
||||||
|
return PRESETS[wrapi(index, 0, PRESETS.size())]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d0xf7c88agixx
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
extends Control
|
||||||
|
class_name CharacterPreview
|
||||||
|
|
||||||
|
# Sprite-based stand-in for the old procedurally-drawn helmet bust: shows the
|
||||||
|
# actual idle/walk archetype sprite from CharacterPresets, with a gentle
|
||||||
|
# auto-alternating idle<->walk loop so the creation screen (and the menu
|
||||||
|
# badge) reads as "alive" rather than a static thumbnail.
|
||||||
|
|
||||||
|
const STEP_INTERVAL := 0.6
|
||||||
|
|
||||||
|
var _texture_rect: TextureRect
|
||||||
|
var _idle_tex: Texture2D
|
||||||
|
var _walk_tex: Texture2D
|
||||||
|
var _step_elapsed: float = 0.0
|
||||||
|
var _showing_walk: bool = false
|
||||||
|
|
||||||
|
var preset_index: int = 0:
|
||||||
|
set(v):
|
||||||
|
preset_index = wrapi(v, 0, CharacterPresets.PRESETS.size())
|
||||||
|
_load_textures()
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_texture_rect = TextureRect.new()
|
||||||
|
_texture_rect.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_texture_rect.expand_mode = TextureRect.EXPAND_FIT_HEIGHT_PROPORTIONAL
|
||||||
|
_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
_texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
add_child(_texture_rect)
|
||||||
|
_load_textures()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_step_elapsed += delta
|
||||||
|
if _step_elapsed >= STEP_INTERVAL:
|
||||||
|
_step_elapsed = 0.0
|
||||||
|
_showing_walk = not _showing_walk
|
||||||
|
_texture_rect.texture = _walk_tex if _showing_walk else _idle_tex
|
||||||
|
|
||||||
|
|
||||||
|
func preset_name() -> String:
|
||||||
|
return CharacterPresets.get_preset(preset_index).name
|
||||||
|
|
||||||
|
|
||||||
|
func _load_textures() -> void:
|
||||||
|
var preset: Dictionary = CharacterPresets.get_preset(preset_index)
|
||||||
|
_idle_tex = load(preset.idle)
|
||||||
|
_walk_tex = load(preset.walk)
|
||||||
|
_showing_walk = false
|
||||||
|
_step_elapsed = 0.0
|
||||||
|
if _texture_rect:
|
||||||
|
_texture_rect.texture = _idle_tex
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cg8sr5iuw0gai
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
# HL2-style menu: no big mode tiles, just a plain vertical text nav over a
|
# HL2-style menu: no big mode tiles, just a plain vertical text nav over a
|
||||||
# space backdrop. Quick Play always queues casual — ranked is gone until the
|
# space backdrop. Combat queueing (formerly QUICK PLAY here) now lives behind
|
||||||
# matchmaking/MMR work in overview/task.md lands.
|
# the in-ship nav table's SPACE COMBAT node, see world/levels/nav_table_ui.gd
|
||||||
|
# and overview/onfoot.md's "Where does it fit in the flow" section.
|
||||||
|
|
||||||
var _status_lbl: Label
|
|
||||||
var _name_lbl: Label
|
var _name_lbl: Label
|
||||||
var _rank_lbl: Label
|
var _rank_lbl: Label
|
||||||
|
var _ram_lbl: Label
|
||||||
|
|
||||||
var _profile_overlay: Control
|
var _profile_overlay: Control
|
||||||
var _profile_input: LineEdit
|
var _profile_input: LineEdit
|
||||||
var _profile_hint_lbl: Label
|
var _profile_hint_lbl: Label
|
||||||
|
var _profile_character: CharacterPreview
|
||||||
|
var _profile_character_name_lbl: Label
|
||||||
|
|
||||||
|
var _badge_character: CharacterPreview
|
||||||
|
var _continue_btn: Button
|
||||||
|
|
||||||
const SETTINGS_PANEL_SCENE := preload("res://menu/settings_panel.tscn")
|
const SETTINGS_PANEL_SCENE := preload("res://menu/settings_panel.tscn")
|
||||||
var _settings_panel: SettingsPanel
|
var _settings_panel: SettingsPanel
|
||||||
|
|
||||||
var _connecting: bool = false
|
# "" (no pending action) or "enter_ship" -- which action to resume once the
|
||||||
var _pending_quick_play: bool = false # profile overlay was forced open by Quick Play
|
# profile overlay's forced-open Save completes.
|
||||||
|
var _pending_action: String = ""
|
||||||
|
|
||||||
const RANK_DATA := [
|
const RANK_DATA := [
|
||||||
{"label": "CADET", "color": Color(0.72, 0.48, 0.22)},
|
{"label": "CADET", "color": Color(0.72, 0.48, 0.22)},
|
||||||
@@ -29,7 +36,7 @@ const RANK_DATA := [
|
|||||||
const CURRENT_RANK := 0 # placeholder until backend exists
|
const CURRENT_RANK := 0 # placeholder until backend exists
|
||||||
const CURRENT_LEVEL := 1 # placeholder until backend exists
|
const CURRENT_LEVEL := 1 # placeholder until backend exists
|
||||||
|
|
||||||
const NAV_ITEMS := ["QUICK PLAY", "SERVER SELECT", "OPTIONS", "PROFILE", "QUIT"]
|
const NAV_ITEMS := ["OPTIONS", "PROFILE", "QUIT"]
|
||||||
const NAV_LEFT := 64.0
|
const NAV_LEFT := 64.0
|
||||||
const NAV_WIDTH := 360.0
|
const NAV_WIDTH := 360.0
|
||||||
const NAV_TOP := 250.0
|
const NAV_TOP := 250.0
|
||||||
@@ -40,10 +47,6 @@ const NAV_GAP := 4.0
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_build_ui()
|
_build_ui()
|
||||||
MusicManager.play_menu_music()
|
MusicManager.play_menu_music()
|
||||||
NetworkManager.connection_succeeded.connect(_on_connected)
|
|
||||||
NetworkManager.connection_failed.connect(_on_connect_failed)
|
|
||||||
MatchmakingClient.match_found.connect(_on_match_found)
|
|
||||||
MatchmakingClient.search_failed.connect(_on_search_failed)
|
|
||||||
|
|
||||||
|
|
||||||
func _build_ui() -> void:
|
func _build_ui() -> void:
|
||||||
@@ -51,7 +54,6 @@ func _build_ui() -> void:
|
|||||||
_add_logo()
|
_add_logo()
|
||||||
_add_nav_menu()
|
_add_nav_menu()
|
||||||
_add_profile_badge()
|
_add_profile_badge()
|
||||||
_add_status_label()
|
|
||||||
_add_version_label()
|
_add_version_label()
|
||||||
_add_profile_overlay()
|
_add_profile_overlay()
|
||||||
_add_settings_panel()
|
_add_settings_panel()
|
||||||
@@ -97,24 +99,22 @@ func _add_logo() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _add_nav_menu() -> void:
|
func _add_nav_menu() -> void:
|
||||||
|
# Dynamic top item: CONTINUE (character already created) or NEW
|
||||||
|
# (no callsign/character yet) - both drop the player straight into the
|
||||||
|
# ship interior, see _on_continue_pressed(). Positioned as slot 0, ahead
|
||||||
|
# of the static NAV_ITEMS below.
|
||||||
|
_continue_btn = _make_nav_btn(_continue_label(), false)
|
||||||
|
_position_nav_btn(_continue_btn, 0)
|
||||||
|
_continue_btn.pressed.connect(_on_continue_pressed)
|
||||||
|
add_child(_continue_btn)
|
||||||
|
|
||||||
for i in NAV_ITEMS.size():
|
for i in NAV_ITEMS.size():
|
||||||
var label_text: String = NAV_ITEMS[i]
|
var label_text: String = NAV_ITEMS[i]
|
||||||
var btn := _make_nav_btn(label_text, false)
|
var btn := _make_nav_btn(label_text, false)
|
||||||
btn.anchor_left = 0.0
|
_position_nav_btn(btn, i + 1)
|
||||||
btn.anchor_top = 0.0
|
|
||||||
btn.anchor_right = 0.0
|
|
||||||
btn.anchor_bottom = 0.0
|
|
||||||
btn.offset_left = NAV_LEFT
|
|
||||||
btn.offset_right = NAV_LEFT + NAV_WIDTH
|
|
||||||
btn.offset_top = NAV_TOP + i * (NAV_BTN_H + NAV_GAP)
|
|
||||||
btn.offset_bottom = btn.offset_top + NAV_BTN_H
|
|
||||||
add_child(btn)
|
add_child(btn)
|
||||||
|
|
||||||
match label_text:
|
match label_text:
|
||||||
"QUICK PLAY":
|
|
||||||
btn.pressed.connect(_on_quick_play)
|
|
||||||
"SERVER SELECT":
|
|
||||||
btn.pressed.connect(_on_server_select)
|
|
||||||
"OPTIONS":
|
"OPTIONS":
|
||||||
btn.pressed.connect(_on_options)
|
btn.pressed.connect(_on_options)
|
||||||
"PROFILE":
|
"PROFILE":
|
||||||
@@ -123,6 +123,25 @@ func _add_nav_menu() -> void:
|
|||||||
btn.pressed.connect(_on_quit)
|
btn.pressed.connect(_on_quit)
|
||||||
|
|
||||||
|
|
||||||
|
func _position_nav_btn(btn: Button, index: int) -> void:
|
||||||
|
btn.anchor_left = 0.0
|
||||||
|
btn.anchor_top = 0.0
|
||||||
|
btn.anchor_right = 0.0
|
||||||
|
btn.anchor_bottom = 0.0
|
||||||
|
btn.offset_left = NAV_LEFT
|
||||||
|
btn.offset_right = NAV_LEFT + NAV_WIDTH
|
||||||
|
btn.offset_top = NAV_TOP + index * (NAV_BTN_H + NAV_GAP)
|
||||||
|
btn.offset_bottom = btn.offset_top + NAV_BTN_H
|
||||||
|
|
||||||
|
|
||||||
|
func _continue_label() -> String:
|
||||||
|
return "CONTINUE" if not GameConfig.player_name.strip_edges().is_empty() else "NEW"
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_continue_button() -> void:
|
||||||
|
_continue_btn.text = _continue_label()
|
||||||
|
|
||||||
|
|
||||||
func _make_nav_btn(label_text: String, stub: bool) -> Button:
|
func _make_nav_btn(label_text: String, stub: bool) -> Button:
|
||||||
var btn := Button.new()
|
var btn := Button.new()
|
||||||
btn.text = label_text
|
btn.text = label_text
|
||||||
@@ -154,6 +173,17 @@ func _make_nav_btn(label_text: String, stub: bool) -> Button:
|
|||||||
|
|
||||||
|
|
||||||
func _add_profile_badge() -> void:
|
func _add_profile_badge() -> void:
|
||||||
|
_badge_character = CharacterPreview.new()
|
||||||
|
_badge_character.anchor_left = 1.0
|
||||||
|
_badge_character.anchor_top = 0.0
|
||||||
|
_badge_character.anchor_right = 1.0
|
||||||
|
_badge_character.anchor_bottom = 0.0
|
||||||
|
_badge_character.offset_left = -396.0
|
||||||
|
_badge_character.offset_top = 12.0
|
||||||
|
_badge_character.offset_right = -348.0
|
||||||
|
_badge_character.offset_bottom = 76.0
|
||||||
|
add_child(_badge_character)
|
||||||
|
|
||||||
_name_lbl = Label.new()
|
_name_lbl = Label.new()
|
||||||
_name_lbl.anchor_left = 1.0
|
_name_lbl.anchor_left = 1.0
|
||||||
_name_lbl.anchor_top = 0.0
|
_name_lbl.anchor_top = 0.0
|
||||||
@@ -181,35 +211,52 @@ func _add_profile_badge() -> void:
|
|||||||
_rank_lbl.add_theme_font_size_override("font_size", 12)
|
_rank_lbl.add_theme_font_size_override("font_size", 12)
|
||||||
add_child(_rank_lbl)
|
add_child(_rank_lbl)
|
||||||
|
|
||||||
|
_ram_lbl = Label.new()
|
||||||
|
_ram_lbl.anchor_left = 1.0
|
||||||
|
_ram_lbl.anchor_top = 0.0
|
||||||
|
_ram_lbl.anchor_right = 1.0
|
||||||
|
_ram_lbl.anchor_bottom = 0.0
|
||||||
|
_ram_lbl.offset_left = -340.0
|
||||||
|
_ram_lbl.offset_top = 68.0
|
||||||
|
_ram_lbl.offset_right = -24.0
|
||||||
|
_ram_lbl.offset_bottom = 86.0
|
||||||
|
_ram_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
_ram_lbl.add_theme_font_size_override("font_size", 12)
|
||||||
|
_ram_lbl.add_theme_color_override("font_color", Color(0.4, 0.85, 0.6))
|
||||||
|
add_child(_ram_lbl)
|
||||||
|
|
||||||
|
|
||||||
func _refresh_profile_badge() -> void:
|
func _refresh_profile_badge() -> void:
|
||||||
|
_refresh_continue_button()
|
||||||
|
_badge_character.preset_index = GameConfig.player_character_index
|
||||||
if GameConfig.player_name.strip_edges().is_empty():
|
if GameConfig.player_name.strip_edges().is_empty():
|
||||||
_name_lbl.text = "GUEST"
|
_name_lbl.text = "GUEST"
|
||||||
_name_lbl.add_theme_color_override("font_color", Color(0.7, 0.72, 0.78))
|
_name_lbl.add_theme_color_override("font_color", Color(0.7, 0.72, 0.78))
|
||||||
_rank_lbl.text = "SET A CALLSIGN IN PROFILE"
|
_rank_lbl.text = "SET A CALLSIGN IN PROFILE"
|
||||||
_rank_lbl.add_theme_color_override("font_color", Color(0.85, 0.55, 0.3))
|
_rank_lbl.add_theme_color_override("font_color", Color(0.85, 0.55, 0.3))
|
||||||
|
_ram_lbl.text = ""
|
||||||
else:
|
else:
|
||||||
_name_lbl.text = GameConfig.player_name.to_upper()
|
_name_lbl.text = GameConfig.player_name.to_upper()
|
||||||
_name_lbl.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
_name_lbl.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
||||||
var rank: Dictionary = RANK_DATA[CURRENT_RANK]
|
var rank: Dictionary = RANK_DATA[CURRENT_RANK]
|
||||||
_rank_lbl.text = "%s • LEVEL %d" % [rank.label, CURRENT_LEVEL]
|
_rank_lbl.text = "%s • LEVEL %d" % [rank.label, CURRENT_LEVEL]
|
||||||
_rank_lbl.add_theme_color_override("font_color", rank.color)
|
_rank_lbl.add_theme_color_override("font_color", rank.color)
|
||||||
|
_ram_lbl.text = "..."
|
||||||
|
_refresh_ram_balance()
|
||||||
|
|
||||||
|
|
||||||
func _add_status_label() -> void:
|
# Async, fire-and-forget from _refresh_profile_badge() -- a brand-new
|
||||||
_status_lbl = Label.new()
|
# callsign that hasn't queued/finished a match yet has no stats row at all
|
||||||
_status_lbl.text = ""
|
# (get_stats() returns ok=false, a 404, not an error), so the label just
|
||||||
_status_lbl.anchor_left = 0.0
|
# stays blank rather than showing a wrong/misleading value.
|
||||||
_status_lbl.anchor_top = 1.0
|
func _refresh_ram_balance() -> void:
|
||||||
_status_lbl.anchor_right = 0.0
|
var result := await MatchmakingClient.get_stats(GameConfig.player_name)
|
||||||
_status_lbl.anchor_bottom = 1.0
|
if GameConfig.player_name.strip_edges().is_empty():
|
||||||
_status_lbl.offset_left = NAV_LEFT
|
return # profile was cleared while the request was in flight
|
||||||
_status_lbl.offset_right = NAV_LEFT + 460.0
|
if not result.ok:
|
||||||
_status_lbl.offset_top = -64.0
|
_ram_lbl.text = ""
|
||||||
_status_lbl.offset_bottom = -36.0
|
return
|
||||||
_status_lbl.add_theme_font_size_override("font_size", 13)
|
_ram_lbl.text = Currency.format_ram(int(result.stats.ram_kb)) + " RAM"
|
||||||
_status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86))
|
|
||||||
add_child(_status_lbl)
|
|
||||||
|
|
||||||
|
|
||||||
func _add_version_label() -> void:
|
func _add_version_label() -> void:
|
||||||
@@ -242,15 +289,15 @@ func _add_profile_overlay() -> void:
|
|||||||
|
|
||||||
var panel := Panel.new()
|
var panel := Panel.new()
|
||||||
panel.set_anchors_preset(Control.PRESET_CENTER)
|
panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
panel.offset_left = -200
|
panel.offset_left = -210
|
||||||
panel.offset_top = -120
|
panel.offset_top = -225
|
||||||
panel.offset_right = 200
|
panel.offset_right = 210
|
||||||
panel.offset_bottom = 120
|
panel.offset_bottom = 225
|
||||||
var ps := StyleBoxFlat.new()
|
var ps := StyleBoxFlat.new()
|
||||||
ps.bg_color = Color(0.05, 0.05, 0.07, 0.97)
|
ps.bg_color = Color(0.09, 0.09, 0.11, 0.97)
|
||||||
ps.set_corner_radius_all(6)
|
ps.set_corner_radius_all(6)
|
||||||
ps.set_border_width_all(1)
|
ps.set_border_width_all(1)
|
||||||
ps.border_color = Color(1.0, 1.0, 1.0, 0.25)
|
ps.border_color = Color(0.6, 0.62, 0.68, 0.4)
|
||||||
panel.add_theme_stylebox_override("panel", ps)
|
panel.add_theme_stylebox_override("panel", ps)
|
||||||
_profile_overlay.add_child(panel)
|
_profile_overlay.add_child(panel)
|
||||||
|
|
||||||
@@ -264,7 +311,7 @@ func _add_profile_overlay() -> void:
|
|||||||
panel.add_child(vb)
|
panel.add_child(vb)
|
||||||
|
|
||||||
var title := Label.new()
|
var title := Label.new()
|
||||||
title.text = "PROFILE"
|
title.text = "CREATE YOUR PILOT"
|
||||||
title.add_theme_font_size_override("font_size", 20)
|
title.add_theme_font_size_override("font_size", 20)
|
||||||
title.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
title.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
||||||
vb.add_child(title)
|
vb.add_child(title)
|
||||||
@@ -279,6 +326,40 @@ func _add_profile_overlay() -> void:
|
|||||||
_profile_hint_lbl.visible = false
|
_profile_hint_lbl.visible = false
|
||||||
vb.add_child(_profile_hint_lbl)
|
vb.add_child(_profile_hint_lbl)
|
||||||
|
|
||||||
|
var portrait_row := HBoxContainer.new()
|
||||||
|
portrait_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
portrait_row.add_theme_constant_override("separation", 18)
|
||||||
|
portrait_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
vb.add_child(portrait_row)
|
||||||
|
|
||||||
|
var prev_btn := _make_portrait_arrow_btn("<")
|
||||||
|
prev_btn.pressed.connect(_on_portrait_cycle.bind(-1))
|
||||||
|
portrait_row.add_child(prev_btn)
|
||||||
|
|
||||||
|
var portrait_frame := Panel.new()
|
||||||
|
portrait_frame.custom_minimum_size = Vector2(120, 140)
|
||||||
|
var pfs := StyleBoxFlat.new()
|
||||||
|
pfs.bg_color = Color(0.03, 0.03, 0.04, 0.6)
|
||||||
|
pfs.set_corner_radius_all(4)
|
||||||
|
pfs.set_border_width_all(1)
|
||||||
|
pfs.border_color = Color(0.5, 0.52, 0.58, 0.5)
|
||||||
|
portrait_frame.add_theme_stylebox_override("panel", pfs)
|
||||||
|
portrait_row.add_child(portrait_frame)
|
||||||
|
|
||||||
|
_profile_character = CharacterPreview.new()
|
||||||
|
_profile_character.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
portrait_frame.add_child(_profile_character)
|
||||||
|
|
||||||
|
var next_btn := _make_portrait_arrow_btn(">")
|
||||||
|
next_btn.pressed.connect(_on_portrait_cycle.bind(1))
|
||||||
|
portrait_row.add_child(next_btn)
|
||||||
|
|
||||||
|
_profile_character_name_lbl = Label.new()
|
||||||
|
_profile_character_name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_profile_character_name_lbl.add_theme_font_size_override("font_size", 13)
|
||||||
|
_profile_character_name_lbl.add_theme_color_override("font_color", Color(0.75, 0.78, 0.84))
|
||||||
|
vb.add_child(_profile_character_name_lbl)
|
||||||
|
|
||||||
var lbl := Label.new()
|
var lbl := Label.new()
|
||||||
lbl.text = "CALLSIGN"
|
lbl.text = "CALLSIGN"
|
||||||
lbl.add_theme_font_size_override("font_size", 12)
|
lbl.add_theme_font_size_override("font_size", 12)
|
||||||
@@ -325,11 +406,28 @@ func _add_profile_overlay() -> void:
|
|||||||
btn_row.add_child(save_btn)
|
btn_row.add_child(save_btn)
|
||||||
|
|
||||||
|
|
||||||
func _open_profile(pending_quick_play: bool) -> void:
|
func _make_portrait_arrow_btn(label_text: String) -> Button:
|
||||||
_pending_quick_play = pending_quick_play
|
var btn := Button.new()
|
||||||
|
btn.text = label_text
|
||||||
|
btn.custom_minimum_size = Vector2(36, 36)
|
||||||
|
btn.add_theme_font_size_override("font_size", 18)
|
||||||
|
btn.add_theme_color_override("font_color", Color(0.82, 0.84, 0.9))
|
||||||
|
btn.add_theme_color_override("font_hover_color", Color(1.0, 1.0, 1.0))
|
||||||
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
func _on_portrait_cycle(delta: int) -> void:
|
||||||
|
_profile_character.preset_index += delta
|
||||||
|
_profile_character_name_lbl.text = _profile_character.preset_name().to_upper()
|
||||||
|
|
||||||
|
|
||||||
|
func _open_profile(pending_action: String) -> void:
|
||||||
|
_pending_action = pending_action
|
||||||
_profile_input.text = GameConfig.player_name
|
_profile_input.text = GameConfig.player_name
|
||||||
_profile_hint_lbl.visible = pending_quick_play
|
_profile_hint_lbl.visible = not pending_action.is_empty()
|
||||||
_profile_hint_lbl.text = "Enter a callsign to play."
|
_profile_hint_lbl.text = "Create your pilot to continue."
|
||||||
|
_profile_character.preset_index = GameConfig.player_character_index
|
||||||
|
_profile_character_name_lbl.text = _profile_character.preset_name().to_upper()
|
||||||
_profile_overlay.visible = true
|
_profile_overlay.visible = true
|
||||||
_profile_input.grab_focus()
|
_profile_input.grab_focus()
|
||||||
|
|
||||||
@@ -339,36 +437,29 @@ func _on_profile_save() -> void:
|
|||||||
if new_name.is_empty():
|
if new_name.is_empty():
|
||||||
return
|
return
|
||||||
GameConfig.player_name = new_name
|
GameConfig.player_name = new_name
|
||||||
|
GameConfig.player_character_index = _profile_character.preset_index
|
||||||
_refresh_profile_badge()
|
_refresh_profile_badge()
|
||||||
_profile_overlay.visible = false
|
_profile_overlay.visible = false
|
||||||
if _pending_quick_play:
|
var pending := _pending_action
|
||||||
_pending_quick_play = false
|
_pending_action = ""
|
||||||
_start_quick_play()
|
if pending == "enter_ship":
|
||||||
|
_enter_ship()
|
||||||
|
|
||||||
|
|
||||||
func _on_profile_cancel() -> void:
|
func _on_profile_cancel() -> void:
|
||||||
_pending_quick_play = false
|
_pending_action = ""
|
||||||
_profile_overlay.visible = false
|
_profile_overlay.visible = false
|
||||||
|
|
||||||
|
|
||||||
func _on_quick_play() -> void:
|
func _on_continue_pressed() -> void:
|
||||||
if _connecting:
|
|
||||||
return
|
|
||||||
if GameConfig.player_name.strip_edges().is_empty():
|
if GameConfig.player_name.strip_edges().is_empty():
|
||||||
_open_profile(true)
|
_open_profile("enter_ship")
|
||||||
return
|
return
|
||||||
_start_quick_play()
|
_enter_ship()
|
||||||
|
|
||||||
|
|
||||||
func _start_quick_play() -> void:
|
func _enter_ship() -> void:
|
||||||
_connecting = true
|
get_tree().change_scene_to_file("res://world/levels/ship_interior.tscn")
|
||||||
_status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86))
|
|
||||||
_status_lbl.text = "Searching for a match..."
|
|
||||||
MatchmakingClient.start_matchmaking(GameConfig.player_name, "casual")
|
|
||||||
|
|
||||||
|
|
||||||
func _on_server_select() -> void:
|
|
||||||
get_tree().change_scene_to_file("res://menu/server_select.tscn")
|
|
||||||
|
|
||||||
|
|
||||||
func _on_options() -> void:
|
func _on_options() -> void:
|
||||||
@@ -381,29 +472,8 @@ func _add_settings_panel() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_profile() -> void:
|
func _on_profile() -> void:
|
||||||
_open_profile(false)
|
_open_profile("")
|
||||||
|
|
||||||
|
|
||||||
func _on_quit() -> void:
|
func _on_quit() -> void:
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
func _on_match_found(server_ip: String, server_port: int) -> void:
|
|
||||||
_status_lbl.text = "Match found — connecting..."
|
|
||||||
NetworkManager.join_server(server_ip, server_port)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_connected() -> void:
|
|
||||||
get_tree().change_scene_to_file("res://world/world.tscn")
|
|
||||||
|
|
||||||
|
|
||||||
func _on_connect_failed() -> void:
|
|
||||||
_connecting = false
|
|
||||||
_status_lbl.add_theme_color_override("font_color", Color(0.85, 0.4, 0.4))
|
|
||||||
_status_lbl.text = "Connection failed — is the game server reachable?"
|
|
||||||
|
|
||||||
|
|
||||||
func _on_search_failed(reason: String) -> void:
|
|
||||||
_connecting = false
|
|
||||||
_status_lbl.add_theme_color_override("font_color", Color(0.85, 0.4, 0.4))
|
|
||||||
_status_lbl.text = "Matchmaking failed: %s" % reason
|
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
extends CanvasLayer
|
extends CanvasLayer
|
||||||
|
|
||||||
|
# Shared between world.tscn (in-match) and ship_interior.tscn (home base) —
|
||||||
|
# the two contexts differ only in whether SELECT TEAM makes sense and where
|
||||||
|
# "quit" goes, so this reads its own parent scene rather than needing two
|
||||||
|
# separate menu scripts. Ship interior isn't networked (see
|
||||||
|
# overview/onfoot.md), so its quit path skips NetworkManager entirely.
|
||||||
|
const SHIP_SCENE := "res://world/levels/ship_interior.tscn"
|
||||||
|
|
||||||
var _root: Control
|
var _root: Control
|
||||||
|
var _in_ship: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
layer = 10
|
layer = 10
|
||||||
|
_in_ship = get_tree().current_scene.scene_file_path == SHIP_SCENE
|
||||||
_build_ui()
|
_build_ui()
|
||||||
_root.visible = false
|
_root.visible = false
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if event.is_action_pressed("toggle_pause") and not GameConfig.chat_focused \
|
if event.is_action_pressed("toggle_pause") and not GameConfig.chat_focused \
|
||||||
and not GameConfig.team_select_focused and not GameConfig.settings_focused:
|
and not GameConfig.team_select_focused and not GameConfig.settings_focused \
|
||||||
|
and not GameConfig.nav_table_focused:
|
||||||
_toggle()
|
_toggle()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
@@ -76,11 +86,15 @@ func _build_ui() -> void:
|
|||||||
|
|
||||||
_add_btn(vb, "RESUME", true).pressed.connect(_toggle)
|
_add_btn(vb, "RESUME", true).pressed.connect(_toggle)
|
||||||
_add_btn(vb, "SETTINGS", true).pressed.connect(_on_settings)
|
_add_btn(vb, "SETTINGS", true).pressed.connect(_on_settings)
|
||||||
|
if not _in_ship:
|
||||||
_add_btn(vb, "SELECT TEAM", true).pressed.connect(_on_select_team)
|
_add_btn(vb, "SELECT TEAM", true).pressed.connect(_on_select_team)
|
||||||
|
|
||||||
vb.add_child(_hsep())
|
vb.add_child(_hsep())
|
||||||
|
|
||||||
_add_btn(vb, "QUIT TO MENU", true).pressed.connect(_on_quit_to_menu)
|
if _in_ship:
|
||||||
|
_add_btn(vb, "QUIT TO MAIN MENU", true).pressed.connect(_on_quit_to_main_menu)
|
||||||
|
else:
|
||||||
|
_add_btn(vb, "QUIT TO SHIP", true).pressed.connect(_on_quit_to_ship)
|
||||||
_add_btn(vb, "QUIT TO DESKTOP", true).pressed.connect(_on_quit_to_desktop)
|
_add_btn(vb, "QUIT TO DESKTOP", true).pressed.connect(_on_quit_to_desktop)
|
||||||
|
|
||||||
|
|
||||||
@@ -122,20 +136,24 @@ func _flat(col: Color, radius: int = 0) -> StyleBoxFlat:
|
|||||||
|
|
||||||
|
|
||||||
func _on_settings() -> void:
|
func _on_settings() -> void:
|
||||||
var settings := get_node_or_null("/root/World/SettingsPanel") as SettingsPanel
|
var settings := get_tree().current_scene.get_node_or_null("SettingsPanel") as SettingsPanel
|
||||||
if settings:
|
if settings:
|
||||||
settings.open()
|
settings.open()
|
||||||
|
|
||||||
|
|
||||||
func _on_select_team() -> void:
|
func _on_select_team() -> void:
|
||||||
_root.visible = false
|
_root.visible = false
|
||||||
var team_select := get_node_or_null("/root/World/TeamSelect") as TeamSelect
|
var team_select := get_tree().current_scene.get_node_or_null("TeamSelect") as TeamSelect
|
||||||
if team_select:
|
if team_select:
|
||||||
team_select.reopen()
|
team_select.reopen()
|
||||||
|
|
||||||
|
|
||||||
func _on_quit_to_menu() -> void:
|
func _on_quit_to_ship() -> void:
|
||||||
NetworkManager.disconnect_from_game()
|
NetworkManager.disconnect_from_game()
|
||||||
|
get_tree().change_scene_to_file(SHIP_SCENE)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_quit_to_main_menu() -> void:
|
||||||
get_tree().change_scene_to_file("res://menu/main_menu.tscn")
|
get_tree().change_scene_to_file("res://menu/main_menu.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ const PING_COLOR_GOOD := Color(0.4, 0.9, 0.5)
|
|||||||
const PING_COLOR_OK := Color(0.95, 0.75, 0.3)
|
const PING_COLOR_OK := Color(0.95, 0.75, 0.3)
|
||||||
const PING_COLOR_BAD := Color(0.9, 0.4, 0.4)
|
const PING_COLOR_BAD := Color(0.9, 0.4, 0.4)
|
||||||
|
|
||||||
var _servers: Array = []
|
var _all_servers: Array = [] # every row from the last GET /servers, unfiltered
|
||||||
|
var _servers: Array = [] # _all_servers narrowed to _selected_category -- what's actually rendered/connectable
|
||||||
|
var _selected_category: String = "" # "" = ALL MODES; otherwise one of GameConfig.GAME_MODE_IDS
|
||||||
|
var _category_group: ButtonGroup
|
||||||
var _selected_index: int = -1
|
var _selected_index: int = -1
|
||||||
var _row_group: ButtonGroup
|
var _row_group: ButtonGroup
|
||||||
var _list_vbox: VBoxContainer
|
var _list_vbox: VBoxContainer
|
||||||
@@ -64,10 +67,78 @@ func _process(_delta: float) -> void:
|
|||||||
func _build_ui() -> void:
|
func _build_ui() -> void:
|
||||||
_add_bg()
|
_add_bg()
|
||||||
_add_header()
|
_add_header()
|
||||||
|
_add_category_filters()
|
||||||
_add_list_panel()
|
_add_list_panel()
|
||||||
_add_bottom_bar()
|
_add_bottom_bar()
|
||||||
|
|
||||||
|
|
||||||
|
# Game-mode toggle bar (ALL MODES + one per GameConfig.GAME_MODE_IDS) -- a
|
||||||
|
# separate, filterable category from the map/address text each row already
|
||||||
|
# shows, so a player can narrow the list to e.g. just Team Deathmatch servers
|
||||||
|
# instead of reading mode off every row by eye. A ButtonGroup makes these
|
||||||
|
# mutually exclusive (radio-style), same pattern _row_group already uses for
|
||||||
|
# the server rows below.
|
||||||
|
func _add_category_filters() -> void:
|
||||||
|
var bar := HBoxContainer.new()
|
||||||
|
bar.anchor_left = 0.0
|
||||||
|
bar.anchor_top = 0.0
|
||||||
|
bar.anchor_right = 1.0
|
||||||
|
bar.anchor_bottom = 0.0
|
||||||
|
bar.offset_left = 60.0
|
||||||
|
bar.offset_top = 100.0
|
||||||
|
bar.offset_right = -60.0
|
||||||
|
bar.offset_bottom = 130.0
|
||||||
|
bar.add_theme_constant_override("separation", 8)
|
||||||
|
add_child(bar)
|
||||||
|
|
||||||
|
_category_group = ButtonGroup.new()
|
||||||
|
var categories: Array = [""]
|
||||||
|
categories.append_array(GameConfig.GAME_MODE_IDS)
|
||||||
|
for cat_id in categories:
|
||||||
|
bar.add_child(_make_category_button(cat_id))
|
||||||
|
|
||||||
|
|
||||||
|
func _make_category_button(cat_id: String) -> Button:
|
||||||
|
var btn := Button.new()
|
||||||
|
btn.text = "ALL MODES" if cat_id == "" else _format_mode_name(cat_id)
|
||||||
|
btn.toggle_mode = true
|
||||||
|
btn.button_pressed = cat_id == _selected_category
|
||||||
|
btn.button_group = _category_group
|
||||||
|
btn.custom_minimum_size = Vector2(0, 30)
|
||||||
|
btn.add_theme_font_size_override("font_size", 13)
|
||||||
|
|
||||||
|
var sn := StyleBoxFlat.new()
|
||||||
|
sn.bg_color = Color(1.0, 1.0, 1.0, 0.06)
|
||||||
|
sn.set_corner_radius_all(4)
|
||||||
|
sn.content_margin_left = 12.0
|
||||||
|
sn.content_margin_right = 12.0
|
||||||
|
var sh := StyleBoxFlat.new()
|
||||||
|
sh.bg_color = Color(1.0, 1.0, 1.0, 0.14)
|
||||||
|
sh.set_corner_radius_all(4)
|
||||||
|
sh.content_margin_left = 12.0
|
||||||
|
sh.content_margin_right = 12.0
|
||||||
|
var sp := StyleBoxFlat.new()
|
||||||
|
sp.bg_color = Color(1.0, 1.0, 1.0, 0.9)
|
||||||
|
sp.set_corner_radius_all(4)
|
||||||
|
sp.content_margin_left = 12.0
|
||||||
|
sp.content_margin_right = 12.0
|
||||||
|
btn.add_theme_stylebox_override("normal", sn)
|
||||||
|
btn.add_theme_stylebox_override("hover", sh)
|
||||||
|
btn.add_theme_stylebox_override("pressed", sp)
|
||||||
|
btn.add_theme_color_override("font_color", Color(0.85, 0.87, 0.92))
|
||||||
|
btn.add_theme_color_override("font_color_pressed", Color(0.05, 0.05, 0.08))
|
||||||
|
|
||||||
|
btn.pressed.connect(_on_category_selected.bind(cat_id))
|
||||||
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
func _on_category_selected(cat_id: String) -> void:
|
||||||
|
if _selected_category == cat_id:
|
||||||
|
return
|
||||||
|
_selected_category = cat_id
|
||||||
|
_apply_filter()
|
||||||
|
|
||||||
|
|
||||||
func _add_bg() -> void:
|
func _add_bg() -> void:
|
||||||
var bg := TextureRect.new()
|
var bg := TextureRect.new()
|
||||||
bg.texture = load("res://assets/images/background/skybox/1.png")
|
bg.texture = load("res://assets/images/background/skybox/1.png")
|
||||||
@@ -105,9 +176,9 @@ func _add_list_panel() -> void:
|
|||||||
col_hdr.anchor_right = 1.0
|
col_hdr.anchor_right = 1.0
|
||||||
col_hdr.anchor_bottom = 0.0
|
col_hdr.anchor_bottom = 0.0
|
||||||
col_hdr.offset_left = 60.0
|
col_hdr.offset_left = 60.0
|
||||||
col_hdr.offset_top = 108.0
|
col_hdr.offset_top = 142.0
|
||||||
col_hdr.offset_right = -60.0
|
col_hdr.offset_right = -60.0
|
||||||
col_hdr.offset_bottom = 132.0
|
col_hdr.offset_bottom = 166.0
|
||||||
add_child(col_hdr)
|
add_child(col_hdr)
|
||||||
|
|
||||||
var h_name := _field_label("MODE / ADDRESS")
|
var h_name := _field_label("MODE / ADDRESS")
|
||||||
@@ -132,9 +203,9 @@ func _add_list_panel() -> void:
|
|||||||
sep.anchor_right = 1.0
|
sep.anchor_right = 1.0
|
||||||
sep.anchor_bottom = 0.0
|
sep.anchor_bottom = 0.0
|
||||||
sep.offset_left = 60.0
|
sep.offset_left = 60.0
|
||||||
sep.offset_top = 136.0
|
sep.offset_top = 170.0
|
||||||
sep.offset_right = -60.0
|
sep.offset_right = -60.0
|
||||||
sep.offset_bottom = 140.0
|
sep.offset_bottom = 174.0
|
||||||
sep.add_theme_color_override("color", Color(1.0, 1.0, 1.0, 0.2))
|
sep.add_theme_color_override("color", Color(1.0, 1.0, 1.0, 0.2))
|
||||||
add_child(sep)
|
add_child(sep)
|
||||||
|
|
||||||
@@ -144,7 +215,7 @@ func _add_list_panel() -> void:
|
|||||||
scroll.anchor_right = 1.0
|
scroll.anchor_right = 1.0
|
||||||
scroll.anchor_bottom = 1.0
|
scroll.anchor_bottom = 1.0
|
||||||
scroll.offset_left = 60.0
|
scroll.offset_left = 60.0
|
||||||
scroll.offset_top = 148.0
|
scroll.offset_top = 182.0
|
||||||
scroll.offset_right = -60.0
|
scroll.offset_right = -60.0
|
||||||
scroll.offset_bottom = -84.0
|
scroll.offset_bottom = -84.0
|
||||||
add_child(scroll)
|
add_child(scroll)
|
||||||
@@ -236,11 +307,28 @@ func _add_bottom_bar() -> void:
|
|||||||
add_child(_connect_btn)
|
add_child(_connect_btn)
|
||||||
|
|
||||||
|
|
||||||
|
# Fetches the full unfiltered server list from the API. Re-rendering for a
|
||||||
|
# category toggle (_on_category_selected()) doesn't need to hit the network
|
||||||
|
# again -- that's _apply_filter()'s job, working off _all_servers.
|
||||||
func _refresh_servers() -> void:
|
func _refresh_servers() -> void:
|
||||||
_selected_index = -1
|
|
||||||
_connect_btn.disabled = true
|
|
||||||
_status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86))
|
_status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86))
|
||||||
_status_lbl.text = "Loading servers..."
|
_status_lbl.text = "Loading servers..."
|
||||||
|
|
||||||
|
var result: Dictionary = await MatchmakingClient.list_servers()
|
||||||
|
if not result.ok:
|
||||||
|
_all_servers = []
|
||||||
|
_clear_rows()
|
||||||
|
_status_lbl.add_theme_color_override("font_color", Color(0.85, 0.4, 0.4))
|
||||||
|
_status_lbl.text = "Couldn't reach matchmaking service."
|
||||||
|
return
|
||||||
|
|
||||||
|
_all_servers = result.servers
|
||||||
|
_apply_filter()
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_rows() -> void:
|
||||||
|
_selected_index = -1
|
||||||
|
_connect_btn.disabled = true
|
||||||
for child in _list_vbox.get_children():
|
for child in _list_vbox.get_children():
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
for probe in _ping_probes:
|
for probe in _ping_probes:
|
||||||
@@ -250,15 +338,22 @@ func _refresh_servers() -> void:
|
|||||||
_ping_labels.clear()
|
_ping_labels.clear()
|
||||||
_ping_probes.clear()
|
_ping_probes.clear()
|
||||||
|
|
||||||
var result: Dictionary = await MatchmakingClient.list_servers()
|
|
||||||
if not result.ok:
|
|
||||||
_status_lbl.add_theme_color_override("font_color", Color(0.85, 0.4, 0.4))
|
|
||||||
_status_lbl.text = "Couldn't reach matchmaking service."
|
|
||||||
return
|
|
||||||
|
|
||||||
_servers = result.servers
|
# Narrows _all_servers down to _selected_category ("" = no filter, show
|
||||||
|
# everything) and rebuilds the row list from that -- the only place _servers
|
||||||
|
# (what rows/ping-probes/CONNECT actually index into) gets assigned.
|
||||||
|
func _apply_filter() -> void:
|
||||||
|
_clear_rows()
|
||||||
|
_status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86))
|
||||||
|
|
||||||
|
_servers = _all_servers.filter(
|
||||||
|
func(s): return _selected_category == "" or str(s.get("game_mode", "")) == _selected_category
|
||||||
|
)
|
||||||
if _servers.is_empty():
|
if _servers.is_empty():
|
||||||
_status_lbl.text = "No servers online right now."
|
_status_lbl.text = (
|
||||||
|
"No servers online right now." if _selected_category == ""
|
||||||
|
else "No %s servers online right now." % _format_mode_name(_selected_category)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
_status_lbl.text = ""
|
_status_lbl.text = ""
|
||||||
@@ -300,6 +395,13 @@ func _apply_ping_result(index: int, response: String, rtt_msec: int) -> void:
|
|||||||
ping_lbl.add_theme_color_override("font_color", Color(0.5, 0.53, 0.6))
|
ping_lbl.add_theme_color_override("font_color", Color(0.5, 0.53, 0.6))
|
||||||
|
|
||||||
|
|
||||||
|
# "team_deathmatch" -> "TEAM DEATHMATCH" -- matches GameMode.get_mode_name()'s
|
||||||
|
# own formatting (world/game_modes/*.gd) so the server list and the in-match
|
||||||
|
# scoreboard/banner never show two different spellings of the same mode.
|
||||||
|
func _format_mode_name(mode_id: String) -> String:
|
||||||
|
return mode_id.replace("_", " ").to_upper()
|
||||||
|
|
||||||
|
|
||||||
func _make_row(server: Dictionary, index: int) -> Button:
|
func _make_row(server: Dictionary, index: int) -> Button:
|
||||||
var row := Button.new()
|
var row := Button.new()
|
||||||
row.toggle_mode = true
|
row.toggle_mode = true
|
||||||
@@ -324,9 +426,10 @@ func _make_row(server: Dictionary, index: int) -> Button:
|
|||||||
hb.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
hb.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
row.add_child(hb)
|
row.add_child(hb)
|
||||||
|
|
||||||
var mode: String = str(server.get("mode", "?")).to_upper()
|
var game_mode: String = _format_mode_name(str(server.get("game_mode", "team_deathmatch")))
|
||||||
|
var map_name: String = str(server.get("map_name", "?"))
|
||||||
var addr_lbl := Label.new()
|
var addr_lbl := Label.new()
|
||||||
addr_lbl.text = "%s %s:%s" % [mode, server.get("ip", "?"), str(server.get("port", "?"))]
|
addr_lbl.text = "%s · %s %s:%s" % [game_mode, map_name, server.get("ip", "?"), str(server.get("port", "?"))]
|
||||||
addr_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
addr_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
addr_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
addr_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
addr_lbl.add_theme_color_override("font_color", Color(0.85, 0.87, 0.92))
|
addr_lbl.add_theme_color_override("font_color", Color(0.85, 0.87, 0.92))
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ scoreboard={
|
|||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
interact={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
class_name ConquestMode
|
||||||
|
extends GameMode
|
||||||
|
|
||||||
|
# Category-tagged stub -- see world.gd's MAPS "categories" field and
|
||||||
|
# CLAUDE.md item 27. Conquest's real capture/ticket scoring isn't built yet,
|
||||||
|
# so this scores identically to TeamDeathmatchMode (kills) for now; a map
|
||||||
|
# tagging "conquest" among its supported categories is already playable
|
||||||
|
# under this mode id today. Give this its own check_win_condition()/
|
||||||
|
# get_score_for_team() when that mechanic exists -- GameMode's contract is
|
||||||
|
# what keeps MatchManager/Scoreboard/MatchBanner unaware of the difference
|
||||||
|
# either way.
|
||||||
|
|
||||||
|
|
||||||
|
func get_mode_name() -> String:
|
||||||
|
return "CONQUEST"
|
||||||
|
|
||||||
|
|
||||||
|
func check_win_condition(world: Node) -> Dictionary:
|
||||||
|
return _kills_based_win_condition(world)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://mchyeuh3sq2d
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
class_name DominationMode
|
||||||
|
extends GameMode
|
||||||
|
|
||||||
|
# Category-tagged stub -- see world.gd's MAPS "categories" field and
|
||||||
|
# CLAUDE.md item 27. Domination's real hold-multiple-zones scoring isn't
|
||||||
|
# built yet, so this scores identically to TeamDeathmatchMode (kills) for
|
||||||
|
# now; a map tagging "domination" among its supported categories is already
|
||||||
|
# playable under this mode id today. Give this its own zone-hold
|
||||||
|
# check_win_condition()/get_score_for_team() when that mechanic exists --
|
||||||
|
# GameMode's contract is what keeps MatchManager/Scoreboard/MatchBanner
|
||||||
|
# unaware of the difference either way.
|
||||||
|
|
||||||
|
|
||||||
|
func get_mode_name() -> String:
|
||||||
|
return "DOMINATION"
|
||||||
|
|
||||||
|
|
||||||
|
func check_win_condition(world: Node) -> Dictionary:
|
||||||
|
return _kills_based_win_condition(world)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cn2darjojw0gq
|
||||||
@@ -45,3 +45,23 @@ func get_score_label() -> String:
|
|||||||
# Hold's held-sector points), which is what keeps that UI mode-agnostic.
|
# Hold's held-sector points), which is what keeps that UI mode-agnostic.
|
||||||
func get_score_for_team(_world: Node, race_id: int) -> int:
|
func get_score_for_team(_world: Node, race_id: int) -> int:
|
||||||
return MatchStats.get_team_kills(race_id)
|
return MatchStats.get_team_kills(race_id)
|
||||||
|
|
||||||
|
|
||||||
|
# Shared by TeamDeathmatchMode and by the category-tagged stub modes
|
||||||
|
# (domination_mode.gd/conquest_mode.gd/king_of_the_hill_mode.gd) that don't
|
||||||
|
# have real zone/point scoring yet -- see world.gd's MAPS "categories" field
|
||||||
|
# and CLAUDE.md item 27. Once a mode grows its own scoring, it overrides
|
||||||
|
# check_win_condition()/get_score_for_team() instead of calling this.
|
||||||
|
func _kills_based_win_condition(world: Node) -> Dictionary:
|
||||||
|
var race_ids: Array = world.decide_offered_races()
|
||||||
|
if race_ids.size() != 2:
|
||||||
|
return {"winner_race_id": 0, "is_draw": true}
|
||||||
|
|
||||||
|
var kills_a := MatchStats.get_team_kills(race_ids[0])
|
||||||
|
var kills_b := MatchStats.get_team_kills(race_ids[1])
|
||||||
|
if kills_a == kills_b:
|
||||||
|
return {"winner_race_id": 0, "is_draw": true}
|
||||||
|
return {
|
||||||
|
"winner_race_id": race_ids[0] if kills_a > kills_b else race_ids[1],
|
||||||
|
"is_draw": false,
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
class_name KingOfTheHillMode
|
||||||
|
extends GameMode
|
||||||
|
|
||||||
|
# Category-tagged stub -- see world.gd's MAPS "categories" field and
|
||||||
|
# CLAUDE.md item 27. King of the Hill's real contested-zone scoring isn't
|
||||||
|
# built yet, so this scores identically to TeamDeathmatchMode (kills) for
|
||||||
|
# now; a map tagging "king_of_the_hill" among its supported categories is
|
||||||
|
# already playable under this mode id today. Give this its own
|
||||||
|
# check_win_condition()/get_score_for_team() when that mechanic exists --
|
||||||
|
# GameMode's contract is what keeps MatchManager/Scoreboard/MatchBanner
|
||||||
|
# unaware of the difference either way.
|
||||||
|
|
||||||
|
|
||||||
|
func get_mode_name() -> String:
|
||||||
|
return "KING OF THE HILL"
|
||||||
|
|
||||||
|
|
||||||
|
func check_win_condition(world: Node) -> Dictionary:
|
||||||
|
return _kills_based_win_condition(world)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bsl7r3s8oey64
|
||||||
@@ -11,15 +11,4 @@ func get_mode_name() -> String:
|
|||||||
|
|
||||||
|
|
||||||
func check_win_condition(world: Node) -> Dictionary:
|
func check_win_condition(world: Node) -> Dictionary:
|
||||||
var race_ids: Array = world.decide_offered_races()
|
return _kills_based_win_condition(world)
|
||||||
if race_ids.size() != 2:
|
|
||||||
return {"winner_race_id": 0, "is_draw": true}
|
|
||||||
|
|
||||||
var kills_a := MatchStats.get_team_kills(race_ids[0])
|
|
||||||
var kills_b := MatchStats.get_team_kills(race_ids[1])
|
|
||||||
if kills_a == kills_b:
|
|
||||||
return {"winner_race_id": 0, "is_draw": true}
|
|
||||||
return {
|
|
||||||
"winner_race_id": race_ids[0] if kills_a > kills_b else race_ids[1],
|
|
||||||
"is_draw": false,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
extends Area2D
|
||||||
|
class_name ExitZone
|
||||||
|
|
||||||
|
# Walk-through scene transition, e.g. a hub's door back to the ship interior
|
||||||
|
# (see overview/onfoot.md's "Where does it fit in the flow" section) --
|
||||||
|
# unlike NavTable, which requires an explicit interact press, this triggers
|
||||||
|
# the instant the player's body enters it, matching a doorway rather than a
|
||||||
|
# console.
|
||||||
|
|
||||||
|
@export var target_scene: String
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
body_entered.connect(_on_body_entered)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_body_entered(_body: Node2D) -> void:
|
||||||
|
get_tree().change_scene_to_file(target_scene)
|
||||||