diff --git a/CLAUDE.md b/CLAUDE.md index 9895863..7440612 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ |------|----------| | `overview.md` | Concept, core loop, game flow, match sizes | | `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 | | `tech.md` | Engine, networking architecture, checklist | | `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. 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 - [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 - [ ] 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`) -- [ ] 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 -- [ ] Galaxy war meta / sector control for casual (see `multiplayer.md`) — Sector Control specifically would also need its own `GameMode` subclass, see above +- [ ] 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`) — 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 - [ ] 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 diff --git a/matchmaking-api/README.md b/matchmaking-api/README.md index 6454797..4d85833 100644 --- a/matchmaking-api/README.md +++ b/matchmaking-api/README.md @@ -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 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 The Godot client is wired up (`spacewar/autoload/matchmaking_client.gd`): diff --git a/matchmaking-api/app/config.py b/matchmaking-api/app/config.py index ff3dde8..ddae80d 100644 --- a/matchmaking-api/app/config.py +++ b/matchmaking-api/app/config.py @@ -15,5 +15,15 @@ class Settings(BaseSettings): # to "offline" and back. 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() diff --git a/matchmaking-api/app/main.py b/matchmaking-api/app/main.py index a33ec9f..34f7f7e 100644 --- a/matchmaking-api/app/main.py +++ b/matchmaking-api/app/main.py @@ -45,6 +45,8 @@ async def _seed_demo_servers() -> None: status=ServerStatus.available, player_count=player_count, max_players=50, + game_mode="team_deathmatch", + map_name="Sector Alpha", ) ) await db.commit() diff --git a/matchmaking-api/app/models.py b/matchmaking-api/app/models.py index 66d8e62..e343b48 100644 --- a/matchmaking-api/app/models.py +++ b/matchmaking-api/app/models.py @@ -2,7 +2,7 @@ import enum import uuid 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.orm import Mapped, mapped_column, relationship @@ -38,6 +38,13 @@ class Player(Base): kills: 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) + # 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) @@ -57,6 +64,16 @@ class GameServer(Base): # once that probe answers. player_count: Mapped[int] = mapped_column(Integer, default=0) 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): diff --git a/matchmaking-api/app/routers/matches.py b/matchmaking-api/app/routers/matches.py index b5946c4..cbf60a5 100644 --- a/matchmaking-api/app/routers/matches.py +++ b/matchmaking-api/app/routers/matches.py @@ -3,6 +3,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app import crud +from app.config import settings from app.database import get_db from app.models import GameServer, Match, MatchPlayer 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.deaths += result.deaths 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: player.wins += 1 elif req.winner_team is not None: diff --git a/matchmaking-api/app/routers/servers.py b/matchmaking-api/app/routers/servers.py index 71dd7d5..86e1a83 100644 --- a/matchmaking-api/app/routers/servers.py +++ b/matchmaking-api/app/routers/servers.py @@ -34,6 +34,8 @@ async def register_server(req: ServerRegisterRequest, db: AsyncSession = Depends existing.player_count = req.player_count existing.max_players = req.max_players existing.last_heartbeat = datetime.utcnow() + existing.game_mode = req.game_mode + existing.map_name = req.map_name else: db.add( GameServer( @@ -43,6 +45,8 @@ async def register_server(req: ServerRegisterRequest, db: AsyncSession = Depends status=status, player_count=req.player_count, max_players=req.max_players, + game_mode=req.game_mode, + map_name=req.map_name, ) ) await db.commit() @@ -80,6 +84,8 @@ async def list_servers(db: AsyncSession = Depends(get_db)) -> list[dict]: "player_count": s.player_count, "max_players": s.max_players, "last_heartbeat": s.last_heartbeat, + "game_mode": s.game_mode, + "map_name": s.map_name, } for s in servers ] diff --git a/matchmaking-api/app/routers/stats.py b/matchmaking-api/app/routers/stats.py index 1894f2b..afb95da 100644 --- a/matchmaking-api/app/routers/stats.py +++ b/matchmaking-api/app/routers/stats.py @@ -22,4 +22,5 @@ async def get_stats(callsign: str, db: AsyncSession = Depends(get_db)) -> Player kills=player.kills, deaths=player.deaths, hours_played=player.hours_played, + ram_kb=player.ram_kb, ) diff --git a/matchmaking-api/app/schemas.py b/matchmaking-api/app/schemas.py index 0415aba..4be4e76 100644 --- a/matchmaking-api/app/schemas.py +++ b/matchmaking-api/app/schemas.py @@ -29,6 +29,10 @@ class ServerRegisterRequest(BaseModel): mode: Mode player_count: int = 0 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): @@ -39,6 +43,7 @@ class PlayerStatsResponse(BaseModel): kills: int deaths: int hours_played: float + ram_kb: int # Reported by the hosting game server (never a client directly -- see diff --git a/overview/multiplayer.md b/overview/multiplayer.md index aa3220b..616e5d0 100644 --- a/overview/multiplayer.md +++ b/overview/multiplayer.md @@ -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. -> **Status:** **Team Deathmatch** (the first row below) is implemented — a -> 7-minute timed version of Annihilation's "pure kills" idea (most kills when -> the clock runs out wins, not last-team-standing), plus a 30s pre-match +> **Status:** **Team Deathmatch** is implemented for real — a 7-minute timed +> "most kills when the clock runs out wins" mode, plus a 30s pre-match > 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 > `GameMode` abstraction (`spacewar/world/game_modes/`) specifically so the -> other rows here — Sector Control, King of the Hill, Base Assault, Convoy -> Escort — can each become a new `GameMode` subclass later without changing -> the match-phase/timer/scoreboard/banner machinery around them. +> other 3 rows below — Domination, Conquest, King of the Hill — can each +> become a fully-realized `GameMode` subclass later without changing the +> 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 | Mode | Description | |------|-------------| -| **Team Deathmatch** *(implemented)* | Most kills when the match timer runs out wins. Timed variant of the Annihilation idea below. | -| **Sector Control** | Map divided into sectors. Hold more sectors at time limit. Ties to galactic war meta. | -| **King of the Hill** | One contested zone in the center. Hold it to rack up points. | -| **Annihilation** | Pure kills. Last team standing or first to X kills. Chaos mode. | -| **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. | +| **Team Deathmatch** *(implemented)* | Most kills when the match timer runs out wins. | +| **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. | +| **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. | +| **King of the Hill** *(category stub)* | One contested zone in the center. Hold it to rack up points. | -> **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. --- diff --git a/overview/onfoot.md b/overview/onfoot.md new file mode 100644 index 0000000..ea3d0c3 --- /dev/null +++ b/overview/onfoot.md @@ -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. diff --git a/spacewar/assets/images/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg b/spacewar/assets/images/ships/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg similarity index 100% rename from spacewar/assets/images/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg rename to spacewar/assets/images/ships/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg diff --git a/spacewar/assets/images/ships/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg.import b/spacewar/assets/images/ships/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg.import new file mode 100644 index 0000000..aadd3af --- /dev/null +++ b/spacewar/assets/images/ships/6a228cb9-6d56-49ca-b1c8-118fd7b1eff3.jpeg.import @@ -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 diff --git a/spacewar/assets/images/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg b/spacewar/assets/images/ships/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg similarity index 100% rename from spacewar/assets/images/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg rename to spacewar/assets/images/ships/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg diff --git a/spacewar/assets/images/ships/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg.import b/spacewar/assets/images/ships/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg.import new file mode 100644 index 0000000..7b76703 --- /dev/null +++ b/spacewar/assets/images/ships/ae9283e6-0a7c-4a98-a2da-e4d0956a6b8b.jpeg.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/armor_idle.png b/spacewar/assets/images/worldsprites/character/armor_idle.png new file mode 100644 index 0000000..b537d47 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/armor_idle.png differ diff --git a/spacewar/assets/images/worldsprites/character/armor_idle.png.import b/spacewar/assets/images/worldsprites/character/armor_idle.png.import new file mode 100644 index 0000000..c41a386 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/armor_idle.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/armor_walk.png b/spacewar/assets/images/worldsprites/character/armor_walk.png new file mode 100644 index 0000000..615c230 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/armor_walk.png differ diff --git a/spacewar/assets/images/worldsprites/character/armor_walk.png.import b/spacewar/assets/images/worldsprites/character/armor_walk.png.import new file mode 100644 index 0000000..4f0adce --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/armor_walk.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/engineer_idle.png b/spacewar/assets/images/worldsprites/character/engineer_idle.png new file mode 100644 index 0000000..a3ab5f1 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/engineer_idle.png differ diff --git a/spacewar/assets/images/worldsprites/character/engineer_idle.png.import b/spacewar/assets/images/worldsprites/character/engineer_idle.png.import new file mode 100644 index 0000000..c5ac3f4 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/engineer_idle.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/engineer_walk.png b/spacewar/assets/images/worldsprites/character/engineer_walk.png new file mode 100644 index 0000000..b814c72 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/engineer_walk.png differ diff --git a/spacewar/assets/images/worldsprites/character/engineer_walk.png.import b/spacewar/assets/images/worldsprites/character/engineer_walk.png.import new file mode 100644 index 0000000..97f06ee --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/engineer_walk.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/laborer_idle.png b/spacewar/assets/images/worldsprites/character/laborer_idle.png new file mode 100644 index 0000000..3990865 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/laborer_idle.png differ diff --git a/spacewar/assets/images/worldsprites/character/laborer_idle.png.import b/spacewar/assets/images/worldsprites/character/laborer_idle.png.import new file mode 100644 index 0000000..93dad3d --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/laborer_idle.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/laborer_walk.png b/spacewar/assets/images/worldsprites/character/laborer_walk.png new file mode 100644 index 0000000..1cd64d6 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/laborer_walk.png differ diff --git a/spacewar/assets/images/worldsprites/character/laborer_walk.png.import b/spacewar/assets/images/worldsprites/character/laborer_walk.png.import new file mode 100644 index 0000000..4977c02 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/laborer_walk.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/marine_idle.png b/spacewar/assets/images/worldsprites/character/marine_idle.png new file mode 100644 index 0000000..66f855d Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/marine_idle.png differ diff --git a/spacewar/assets/images/worldsprites/character/marine_idle.png.import b/spacewar/assets/images/worldsprites/character/marine_idle.png.import new file mode 100644 index 0000000..ab99c66 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/marine_idle.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/marine_walk.png b/spacewar/assets/images/worldsprites/character/marine_walk.png new file mode 100644 index 0000000..50e9fd9 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/marine_walk.png differ diff --git a/spacewar/assets/images/worldsprites/character/marine_walk.png.import b/spacewar/assets/images/worldsprites/character/marine_walk.png.import new file mode 100644 index 0000000..4e6dd18 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/marine_walk.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/officer_idle.png b/spacewar/assets/images/worldsprites/character/officer_idle.png new file mode 100644 index 0000000..c619063 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/officer_idle.png differ diff --git a/spacewar/assets/images/worldsprites/character/officer_idle.png.import b/spacewar/assets/images/worldsprites/character/officer_idle.png.import new file mode 100644 index 0000000..bb9e1ec --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/officer_idle.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/officer_walk.png b/spacewar/assets/images/worldsprites/character/officer_walk.png new file mode 100644 index 0000000..c619063 Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/officer_walk.png differ diff --git a/spacewar/assets/images/worldsprites/character/officer_walk.png.import b/spacewar/assets/images/worldsprites/character/officer_walk.png.import new file mode 100644 index 0000000..0a2d8c1 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/officer_walk.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/scavenger_idle.png b/spacewar/assets/images/worldsprites/character/scavenger_idle.png new file mode 100644 index 0000000..acaff0c Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/scavenger_idle.png differ diff --git a/spacewar/assets/images/worldsprites/character/scavenger_idle.png.import b/spacewar/assets/images/worldsprites/character/scavenger_idle.png.import new file mode 100644 index 0000000..4436545 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/scavenger_idle.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/scavenger_walk.png b/spacewar/assets/images/worldsprites/character/scavenger_walk.png new file mode 100644 index 0000000..91612ab Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/scavenger_walk.png differ diff --git a/spacewar/assets/images/worldsprites/character/scavenger_walk.png.import b/spacewar/assets/images/worldsprites/character/scavenger_walk.png.import new file mode 100644 index 0000000..d0509f0 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/scavenger_walk.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/soldier_idle.png b/spacewar/assets/images/worldsprites/character/soldier_idle.png new file mode 100644 index 0000000..cecdbfd Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/soldier_idle.png differ diff --git a/spacewar/assets/images/worldsprites/character/soldier_idle.png.import b/spacewar/assets/images/worldsprites/character/soldier_idle.png.import new file mode 100644 index 0000000..53d8dbc --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/soldier_idle.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/character/soldier_walk.png b/spacewar/assets/images/worldsprites/character/soldier_walk.png new file mode 100644 index 0000000..6098aab Binary files /dev/null and b/spacewar/assets/images/worldsprites/character/soldier_walk.png differ diff --git a/spacewar/assets/images/worldsprites/character/soldier_walk.png.import b/spacewar/assets/images/worldsprites/character/soldier_walk.png.import new file mode 100644 index 0000000..8969671 --- /dev/null +++ b/spacewar/assets/images/worldsprites/character/soldier_walk.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/furniture/cabinet.png b/spacewar/assets/images/worldsprites/furniture/cabinet.png new file mode 100644 index 0000000..59f2bd8 Binary files /dev/null and b/spacewar/assets/images/worldsprites/furniture/cabinet.png differ diff --git a/spacewar/assets/images/worldsprites/furniture/cabinet.png.import b/spacewar/assets/images/worldsprites/furniture/cabinet.png.import new file mode 100644 index 0000000..4d84134 --- /dev/null +++ b/spacewar/assets/images/worldsprites/furniture/cabinet.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/furniture/locker.png b/spacewar/assets/images/worldsprites/furniture/locker.png new file mode 100644 index 0000000..8e101fd Binary files /dev/null and b/spacewar/assets/images/worldsprites/furniture/locker.png differ diff --git a/spacewar/assets/images/worldsprites/furniture/locker.png.import b/spacewar/assets/images/worldsprites/furniture/locker.png.import new file mode 100644 index 0000000..2d763d8 --- /dev/null +++ b/spacewar/assets/images/worldsprites/furniture/locker.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/furniture/plant.png b/spacewar/assets/images/worldsprites/furniture/plant.png new file mode 100644 index 0000000..c640972 Binary files /dev/null and b/spacewar/assets/images/worldsprites/furniture/plant.png differ diff --git a/spacewar/assets/images/worldsprites/furniture/plant.png.import b/spacewar/assets/images/worldsprites/furniture/plant.png.import new file mode 100644 index 0000000..d595c8b --- /dev/null +++ b/spacewar/assets/images/worldsprites/furniture/plant.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/furniture/server_rack.png b/spacewar/assets/images/worldsprites/furniture/server_rack.png new file mode 100644 index 0000000..23c06e9 Binary files /dev/null and b/spacewar/assets/images/worldsprites/furniture/server_rack.png differ diff --git a/spacewar/assets/images/worldsprites/furniture/server_rack.png.import b/spacewar/assets/images/worldsprites/furniture/server_rack.png.import new file mode 100644 index 0000000..6add43f --- /dev/null +++ b/spacewar/assets/images/worldsprites/furniture/server_rack.png.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/orc/orc_character_roster_source.jpeg b/spacewar/assets/images/worldsprites/orc/orc_character_roster_source.jpeg new file mode 100644 index 0000000..2c55da4 Binary files /dev/null and b/spacewar/assets/images/worldsprites/orc/orc_character_roster_source.jpeg differ diff --git a/spacewar/assets/images/worldsprites/orc/orc_character_roster_source.jpeg.import b/spacewar/assets/images/worldsprites/orc/orc_character_roster_source.jpeg.import new file mode 100644 index 0000000..d6f9632 --- /dev/null +++ b/spacewar/assets/images/worldsprites/orc/orc_character_roster_source.jpeg.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/orc/orc_furniture_sheet_source.jpeg b/spacewar/assets/images/worldsprites/orc/orc_furniture_sheet_source.jpeg new file mode 100644 index 0000000..a56b244 Binary files /dev/null and b/spacewar/assets/images/worldsprites/orc/orc_furniture_sheet_source.jpeg differ diff --git a/spacewar/assets/images/worldsprites/orc/orc_furniture_sheet_source.jpeg.import b/spacewar/assets/images/worldsprites/orc/orc_furniture_sheet_source.jpeg.import new file mode 100644 index 0000000..d8c93c0 --- /dev/null +++ b/spacewar/assets/images/worldsprites/orc/orc_furniture_sheet_source.jpeg.import @@ -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 diff --git a/spacewar/assets/images/worldsprites/orc/spritesheet.png b/spacewar/assets/images/worldsprites/orc/spritesheet.png new file mode 100644 index 0000000..71ba92f Binary files /dev/null and b/spacewar/assets/images/worldsprites/orc/spritesheet.png differ diff --git a/spacewar/assets/images/worldsprites/orc/spritesheet.png.import b/spacewar/assets/images/worldsprites/orc/spritesheet.png.import new file mode 100644 index 0000000..35026af --- /dev/null +++ b/spacewar/assets/images/worldsprites/orc/spritesheet.png.import @@ -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 diff --git a/spacewar/assets/images/worldtiles/source/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg b/spacewar/assets/images/worldtiles/source/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg new file mode 100644 index 0000000..e47c218 Binary files /dev/null and b/spacewar/assets/images/worldtiles/source/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg differ diff --git a/spacewar/assets/images/worldtiles/source/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg.import b/spacewar/assets/images/worldtiles/source/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg.import new file mode 100644 index 0000000..eccda11 --- /dev/null +++ b/spacewar/assets/images/worldtiles/source/0ac1a9f7-311e-4e64-8e76-8e8a6903ed32.jpeg.import @@ -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 diff --git a/spacewar/assets/images/worldtiles/source/4b196425-afc8-407a-aed3-bec051324333.jpeg b/spacewar/assets/images/worldtiles/source/4b196425-afc8-407a-aed3-bec051324333.jpeg new file mode 100644 index 0000000..e6c937a Binary files /dev/null and b/spacewar/assets/images/worldtiles/source/4b196425-afc8-407a-aed3-bec051324333.jpeg differ diff --git a/spacewar/assets/images/worldtiles/source/4b196425-afc8-407a-aed3-bec051324333.jpeg.import b/spacewar/assets/images/worldtiles/source/4b196425-afc8-407a-aed3-bec051324333.jpeg.import new file mode 100644 index 0000000..2a1ec05 --- /dev/null +++ b/spacewar/assets/images/worldtiles/source/4b196425-afc8-407a-aed3-bec051324333.jpeg.import @@ -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 diff --git a/spacewar/assets/images/worldtiles/source/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg b/spacewar/assets/images/worldtiles/source/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg new file mode 100644 index 0000000..b3985cb Binary files /dev/null and b/spacewar/assets/images/worldtiles/source/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg differ diff --git a/spacewar/assets/images/worldtiles/source/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg.import b/spacewar/assets/images/worldtiles/source/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg.import new file mode 100644 index 0000000..e629beb --- /dev/null +++ b/spacewar/assets/images/worldtiles/source/f4ad5065-9e36-4c9e-a0a8-2aa0d3ea0352.jpeg.import @@ -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 diff --git a/spacewar/assets/images/worldtiles/source/orc_station_tileset_reference_source.jpeg b/spacewar/assets/images/worldtiles/source/orc_station_tileset_reference_source.jpeg new file mode 100644 index 0000000..cb8c475 Binary files /dev/null and b/spacewar/assets/images/worldtiles/source/orc_station_tileset_reference_source.jpeg differ diff --git a/spacewar/assets/images/worldtiles/source/orc_station_tileset_reference_source.jpeg.import b/spacewar/assets/images/worldtiles/source/orc_station_tileset_reference_source.jpeg.import new file mode 100644 index 0000000..f3011e6 --- /dev/null +++ b/spacewar/assets/images/worldtiles/source/orc_station_tileset_reference_source.jpeg.import @@ -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 diff --git a/spacewar/assets/images/worldtiles/station_door.png b/spacewar/assets/images/worldtiles/station_door.png new file mode 100644 index 0000000..bcda0aa Binary files /dev/null and b/spacewar/assets/images/worldtiles/station_door.png differ diff --git a/spacewar/assets/images/worldtiles/station_door.png.import b/spacewar/assets/images/worldtiles/station_door.png.import new file mode 100644 index 0000000..fbb2b8a --- /dev/null +++ b/spacewar/assets/images/worldtiles/station_door.png.import @@ -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 diff --git a/spacewar/assets/images/worldtiles/station_floor.png b/spacewar/assets/images/worldtiles/station_floor.png new file mode 100644 index 0000000..0cb2640 Binary files /dev/null and b/spacewar/assets/images/worldtiles/station_floor.png differ diff --git a/spacewar/assets/images/worldtiles/station_floor.png.import b/spacewar/assets/images/worldtiles/station_floor.png.import new file mode 100644 index 0000000..bcc8f50 --- /dev/null +++ b/spacewar/assets/images/worldtiles/station_floor.png.import @@ -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 diff --git a/spacewar/assets/images/worldtiles/station_walls.png b/spacewar/assets/images/worldtiles/station_walls.png new file mode 100644 index 0000000..f0182cf Binary files /dev/null and b/spacewar/assets/images/worldtiles/station_walls.png differ diff --git a/spacewar/assets/images/worldtiles/station_walls.png.import b/spacewar/assets/images/worldtiles/station_walls.png.import new file mode 100644 index 0000000..22dc126 --- /dev/null +++ b/spacewar/assets/images/worldtiles/station_walls.png.import @@ -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 diff --git a/spacewar/autoload/currency.gd b/spacewar/autoload/currency.gd new file mode 100644 index 0000000..e14ac15 --- /dev/null +++ b/spacewar/autoload/currency.gd @@ -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]] diff --git a/spacewar/autoload/currency.gd.uid b/spacewar/autoload/currency.gd.uid new file mode 100644 index 0000000..d7ed70a --- /dev/null +++ b/spacewar/autoload/currency.gd.uid @@ -0,0 +1 @@ +uid://ch23k14liwu2i diff --git a/spacewar/autoload/game_config.gd b/spacewar/autoload/game_config.gd index ec8f193..0403b3d 100644 --- a/spacewar/autoload/game_config.gd +++ b/spacewar/autoload/game_config.gd @@ -305,6 +305,12 @@ var pre_match_camera_zoom: float = 0.5 # Race/ship/speed are per-peer now — see PlayerRegistry. 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. 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). 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. # default_game_mode_id picks a GameMode subclass (world/game_modes/) via # 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 # once ships are released; match_pre_match_duration/match_post_match_duration # 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 match_pre_match_duration: float = 30.0 var match_duration: float = 420.0 diff --git a/spacewar/autoload/match_manager.gd b/spacewar/autoload/match_manager.gd index 5a1347e..e632596 100644 --- a/spacewar/autoload/match_manager.gd +++ b/spacewar/autoload/match_manager.gd @@ -36,6 +36,12 @@ func _create_mode(mode_id: String) -> GameMode: match mode_id: "team_deathmatch": return TeamDeathmatchMode.new() + "domination": + return DominationMode.new() + "conquest": + return ConquestMode.new() + "king_of_the_hill": + return KingOfTheHillMode.new() _: return TeamDeathmatchMode.new() diff --git a/spacewar/autoload/matchmaking_client.gd b/spacewar/autoload/matchmaking_client.gd index fdb325f..e0f7be3 100644 --- a/spacewar/autoload/matchmaking_client.gd +++ b/spacewar/autoload/matchmaking_client.gd @@ -67,9 +67,9 @@ func start_matchmaking(callsign: String, mode: String) -> void: # Returns {"ok": bool, "servers": Array} — one dict per row from GET /servers -# (ip, port, mode, status, player_count, max_players, last_heartbeat). "ok" -# is false if the API couldn't be reached at all, distinct from a reachable -# API returning an empty list. +# (ip, port, mode, status, player_count, max_players, last_heartbeat, +# game_mode, map_name). "ok" is false if the API couldn't be reached at all, +# distinct from a reachable API returning an empty list. func list_servers() -> Dictionary: var result := await _request("GET", "/servers", null) 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; # 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 -# callers can fire-and-forget this. -func register_server(ip: String, port: int, mode: String, player_count: int, max_players: int) -> bool: +# callers can fire-and-forget this. game_mode/map_name are the actual +# 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", { "ip": ip, "port": port, "mode": mode, "player_count": player_count, "max_players": max_players, + "game_mode": game_mode, + "map_name": map_name, }, _heartbeat_http) 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 +# 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: if not _searching: return diff --git a/spacewar/autoload/network_manager.gd b/spacewar/autoload/network_manager.gd index 79a6965..5a4e8d0 100644 --- a/spacewar/autoload/network_manager.gd +++ b/spacewar/autoload/network_manager.gd @@ -111,8 +111,15 @@ func _stop_heartbeat() -> void: # 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 # (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: - 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: diff --git a/spacewar/hud/map_name_display.gd b/spacewar/hud/map_name_display.gd new file mode 100644 index 0000000..e3f63ff --- /dev/null +++ b/spacewar/hud/map_name_display.gd @@ -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) diff --git a/spacewar/hud/map_name_display.gd.uid b/spacewar/hud/map_name_display.gd.uid new file mode 100644 index 0000000..bee89ea --- /dev/null +++ b/spacewar/hud/map_name_display.gd.uid @@ -0,0 +1 @@ +uid://dcs037f6tynnw diff --git a/spacewar/hud/map_name_display.tscn b/spacewar/hud/map_name_display.tscn new file mode 100644 index 0000000..9a650c5 --- /dev/null +++ b/spacewar/hud/map_name_display.tscn @@ -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") diff --git a/spacewar/hud/match_banner.gd.uid b/spacewar/hud/match_banner.gd.uid new file mode 100644 index 0000000..836e749 --- /dev/null +++ b/spacewar/hud/match_banner.gd.uid @@ -0,0 +1 @@ +uid://bbf4sob5sgkqt diff --git a/spacewar/hud/match_timer.gd.uid b/spacewar/hud/match_timer.gd.uid new file mode 100644 index 0000000..9b12918 --- /dev/null +++ b/spacewar/hud/match_timer.gd.uid @@ -0,0 +1 @@ +uid://cor1re65vyu2t diff --git a/spacewar/hud/ping_display.gd b/spacewar/hud/ping_display.gd index 85cc2c3..01388ae 100644 --- a/spacewar/hud/ping_display.gd +++ b/spacewar/hud/ping_display.gd @@ -5,10 +5,11 @@ extends CanvasLayer # 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 # 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_TOP := 20.0 +const MARGIN_TOP := 54.0 const WIDTH := 100.0 const HEIGHT := 30.0 const UPDATE_INTERVAL := 0.5 diff --git a/spacewar/menu/character_presets.gd b/spacewar/menu/character_presets.gd new file mode 100644 index 0000000..e6e7f96 --- /dev/null +++ b/spacewar/menu/character_presets.gd @@ -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())] diff --git a/spacewar/menu/character_presets.gd.uid b/spacewar/menu/character_presets.gd.uid new file mode 100644 index 0000000..5605590 --- /dev/null +++ b/spacewar/menu/character_presets.gd.uid @@ -0,0 +1 @@ +uid://d0xf7c88agixx diff --git a/spacewar/menu/character_preview.gd b/spacewar/menu/character_preview.gd new file mode 100644 index 0000000..fb5fc39 --- /dev/null +++ b/spacewar/menu/character_preview.gd @@ -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 diff --git a/spacewar/menu/character_preview.gd.uid b/spacewar/menu/character_preview.gd.uid new file mode 100644 index 0000000..84b94d1 --- /dev/null +++ b/spacewar/menu/character_preview.gd.uid @@ -0,0 +1 @@ +uid://cg8sr5iuw0gai diff --git a/spacewar/menu/main_menu.gd b/spacewar/menu/main_menu.gd index ad18777..c7870ba 100644 --- a/spacewar/menu/main_menu.gd +++ b/spacewar/menu/main_menu.gd @@ -1,22 +1,29 @@ extends Control # 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 -# matchmaking/MMR work in overview/task.md lands. +# space backdrop. Combat queueing (formerly QUICK PLAY here) now lives behind +# 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 _rank_lbl: Label +var _ram_lbl: Label var _profile_overlay: Control var _profile_input: LineEdit 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") var _settings_panel: SettingsPanel -var _connecting: bool = false -var _pending_quick_play: bool = false # profile overlay was forced open by Quick Play +# "" (no pending action) or "enter_ship" -- which action to resume once the +# profile overlay's forced-open Save completes. +var _pending_action: String = "" const RANK_DATA := [ {"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_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_WIDTH := 360.0 const NAV_TOP := 250.0 @@ -40,10 +47,6 @@ const NAV_GAP := 4.0 func _ready() -> void: _build_ui() 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: @@ -51,7 +54,6 @@ func _build_ui() -> void: _add_logo() _add_nav_menu() _add_profile_badge() - _add_status_label() _add_version_label() _add_profile_overlay() _add_settings_panel() @@ -97,24 +99,22 @@ func _add_logo() -> 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(): var label_text: String = NAV_ITEMS[i] var btn := _make_nav_btn(label_text, false) - 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 + i * (NAV_BTN_H + NAV_GAP) - btn.offset_bottom = btn.offset_top + NAV_BTN_H + _position_nav_btn(btn, i + 1) add_child(btn) match label_text: - "QUICK PLAY": - btn.pressed.connect(_on_quick_play) - "SERVER SELECT": - btn.pressed.connect(_on_server_select) "OPTIONS": btn.pressed.connect(_on_options) "PROFILE": @@ -123,6 +123,25 @@ func _add_nav_menu() -> void: 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: var btn := Button.new() btn.text = label_text @@ -154,6 +173,17 @@ func _make_nav_btn(label_text: String, stub: bool) -> Button: 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.anchor_left = 1.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) 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: + _refresh_continue_button() + _badge_character.preset_index = GameConfig.player_character_index if GameConfig.player_name.strip_edges().is_empty(): _name_lbl.text = "GUEST" _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.add_theme_color_override("font_color", Color(0.85, 0.55, 0.3)) + _ram_lbl.text = "" else: _name_lbl.text = GameConfig.player_name.to_upper() _name_lbl.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0)) var rank: Dictionary = RANK_DATA[CURRENT_RANK] _rank_lbl.text = "%s • LEVEL %d" % [rank.label, CURRENT_LEVEL] _rank_lbl.add_theme_color_override("font_color", rank.color) + _ram_lbl.text = "..." + _refresh_ram_balance() -func _add_status_label() -> void: - _status_lbl = Label.new() - _status_lbl.text = "" - _status_lbl.anchor_left = 0.0 - _status_lbl.anchor_top = 1.0 - _status_lbl.anchor_right = 0.0 - _status_lbl.anchor_bottom = 1.0 - _status_lbl.offset_left = NAV_LEFT - _status_lbl.offset_right = NAV_LEFT + 460.0 - _status_lbl.offset_top = -64.0 - _status_lbl.offset_bottom = -36.0 - _status_lbl.add_theme_font_size_override("font_size", 13) - _status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86)) - add_child(_status_lbl) +# Async, fire-and-forget from _refresh_profile_badge() -- a brand-new +# callsign that hasn't queued/finished a match yet has no stats row at all +# (get_stats() returns ok=false, a 404, not an error), so the label just +# stays blank rather than showing a wrong/misleading value. +func _refresh_ram_balance() -> void: + var result := await MatchmakingClient.get_stats(GameConfig.player_name) + if GameConfig.player_name.strip_edges().is_empty(): + return # profile was cleared while the request was in flight + if not result.ok: + _ram_lbl.text = "" + return + _ram_lbl.text = Currency.format_ram(int(result.stats.ram_kb)) + " RAM" func _add_version_label() -> void: @@ -242,15 +289,15 @@ func _add_profile_overlay() -> void: var panel := Panel.new() panel.set_anchors_preset(Control.PRESET_CENTER) - panel.offset_left = -200 - panel.offset_top = -120 - panel.offset_right = 200 - panel.offset_bottom = 120 + panel.offset_left = -210 + panel.offset_top = -225 + panel.offset_right = 210 + panel.offset_bottom = 225 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_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) _profile_overlay.add_child(panel) @@ -264,7 +311,7 @@ func _add_profile_overlay() -> void: panel.add_child(vb) var title := Label.new() - title.text = "PROFILE" + title.text = "CREATE YOUR PILOT" title.add_theme_font_size_override("font_size", 20) title.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0)) vb.add_child(title) @@ -279,6 +326,40 @@ func _add_profile_overlay() -> void: _profile_hint_lbl.visible = false 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() lbl.text = "CALLSIGN" lbl.add_theme_font_size_override("font_size", 12) @@ -325,11 +406,28 @@ func _add_profile_overlay() -> void: btn_row.add_child(save_btn) -func _open_profile(pending_quick_play: bool) -> void: - _pending_quick_play = pending_quick_play +func _make_portrait_arrow_btn(label_text: String) -> Button: + 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_hint_lbl.visible = pending_quick_play - _profile_hint_lbl.text = "Enter a callsign to play." + _profile_hint_lbl.visible = not pending_action.is_empty() + _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_input.grab_focus() @@ -339,36 +437,29 @@ func _on_profile_save() -> void: if new_name.is_empty(): return GameConfig.player_name = new_name + GameConfig.player_character_index = _profile_character.preset_index _refresh_profile_badge() _profile_overlay.visible = false - if _pending_quick_play: - _pending_quick_play = false - _start_quick_play() + var pending := _pending_action + _pending_action = "" + if pending == "enter_ship": + _enter_ship() func _on_profile_cancel() -> void: - _pending_quick_play = false + _pending_action = "" _profile_overlay.visible = false -func _on_quick_play() -> void: - if _connecting: - return +func _on_continue_pressed() -> void: if GameConfig.player_name.strip_edges().is_empty(): - _open_profile(true) + _open_profile("enter_ship") return - _start_quick_play() + _enter_ship() -func _start_quick_play() -> void: - _connecting = true - _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 _enter_ship() -> void: + get_tree().change_scene_to_file("res://world/levels/ship_interior.tscn") func _on_options() -> void: @@ -381,29 +472,8 @@ func _add_settings_panel() -> void: func _on_profile() -> void: - _open_profile(false) + _open_profile("") func _on_quit() -> void: 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 diff --git a/spacewar/menu/pause_menu.gd b/spacewar/menu/pause_menu.gd index e28fc4c..2319dd0 100644 --- a/spacewar/menu/pause_menu.gd +++ b/spacewar/menu/pause_menu.gd @@ -1,17 +1,27 @@ 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 _in_ship: bool = false func _ready() -> void: layer = 10 + _in_ship = get_tree().current_scene.scene_file_path == SHIP_SCENE _build_ui() _root.visible = false func _input(event: InputEvent) -> void: 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() 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, "SETTINGS", true).pressed.connect(_on_settings) - _add_btn(vb, "SELECT TEAM", true).pressed.connect(_on_select_team) + if not _in_ship: + _add_btn(vb, "SELECT TEAM", true).pressed.connect(_on_select_team) 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) @@ -122,20 +136,24 @@ func _flat(col: Color, radius: int = 0) -> StyleBoxFlat: 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: settings.open() func _on_select_team() -> void: _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: team_select.reopen() -func _on_quit_to_menu() -> void: +func _on_quit_to_ship() -> void: 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") diff --git a/spacewar/menu/server_select.gd b/spacewar/menu/server_select.gd index 08c09b1..cbc653a 100644 --- a/spacewar/menu/server_select.gd +++ b/spacewar/menu/server_select.gd @@ -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_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 _row_group: ButtonGroup var _list_vbox: VBoxContainer @@ -64,10 +67,78 @@ func _process(_delta: float) -> void: func _build_ui() -> void: _add_bg() _add_header() + _add_category_filters() _add_list_panel() _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: var bg := TextureRect.new() 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_bottom = 0.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_bottom = 132.0 + col_hdr.offset_bottom = 166.0 add_child(col_hdr) var h_name := _field_label("MODE / ADDRESS") @@ -132,9 +203,9 @@ func _add_list_panel() -> void: sep.anchor_right = 1.0 sep.anchor_bottom = 0.0 sep.offset_left = 60.0 - sep.offset_top = 136.0 + sep.offset_top = 170.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)) add_child(sep) @@ -144,7 +215,7 @@ func _add_list_panel() -> void: scroll.anchor_right = 1.0 scroll.anchor_bottom = 1.0 scroll.offset_left = 60.0 - scroll.offset_top = 148.0 + scroll.offset_top = 182.0 scroll.offset_right = -60.0 scroll.offset_bottom = -84.0 add_child(scroll) @@ -236,11 +307,28 @@ func _add_bottom_bar() -> void: 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: - _selected_index = -1 - _connect_btn.disabled = true _status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86)) _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(): child.queue_free() for probe in _ping_probes: @@ -250,15 +338,22 @@ func _refresh_servers() -> void: _ping_labels.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(): - _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 _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)) +# "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: var row := Button.new() row.toggle_mode = true @@ -324,9 +426,10 @@ func _make_row(server: Dictionary, index: int) -> Button: hb.mouse_filter = Control.MOUSE_FILTER_IGNORE 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() - 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.vertical_alignment = VERTICAL_ALIGNMENT_CENTER addr_lbl.add_theme_color_override("font_color", Color(0.85, 0.87, 0.92)) diff --git a/spacewar/project.godot b/spacewar/project.godot index 0d375c3..01bfb1b 100644 --- a/spacewar/project.godot +++ b/spacewar/project.godot @@ -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) ] } +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] diff --git a/spacewar/world/game_modes/conquest_mode.gd b/spacewar/world/game_modes/conquest_mode.gd new file mode 100644 index 0000000..66c14cb --- /dev/null +++ b/spacewar/world/game_modes/conquest_mode.gd @@ -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) diff --git a/spacewar/world/game_modes/conquest_mode.gd.uid b/spacewar/world/game_modes/conquest_mode.gd.uid new file mode 100644 index 0000000..bf60329 --- /dev/null +++ b/spacewar/world/game_modes/conquest_mode.gd.uid @@ -0,0 +1 @@ +uid://mchyeuh3sq2d diff --git a/spacewar/world/game_modes/domination_mode.gd b/spacewar/world/game_modes/domination_mode.gd new file mode 100644 index 0000000..18f9dac --- /dev/null +++ b/spacewar/world/game_modes/domination_mode.gd @@ -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) diff --git a/spacewar/world/game_modes/domination_mode.gd.uid b/spacewar/world/game_modes/domination_mode.gd.uid new file mode 100644 index 0000000..4c1d704 --- /dev/null +++ b/spacewar/world/game_modes/domination_mode.gd.uid @@ -0,0 +1 @@ +uid://cn2darjojw0gq diff --git a/spacewar/world/game_modes/game_mode.gd b/spacewar/world/game_modes/game_mode.gd index 93566d9..cad3a46 100644 --- a/spacewar/world/game_modes/game_mode.gd +++ b/spacewar/world/game_modes/game_mode.gd @@ -45,3 +45,23 @@ func get_score_label() -> String: # Hold's held-sector points), which is what keeps that UI mode-agnostic. func get_score_for_team(_world: Node, race_id: int) -> int: 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, + } diff --git a/spacewar/world/game_modes/king_of_the_hill_mode.gd b/spacewar/world/game_modes/king_of_the_hill_mode.gd new file mode 100644 index 0000000..6d4178e --- /dev/null +++ b/spacewar/world/game_modes/king_of_the_hill_mode.gd @@ -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) diff --git a/spacewar/world/game_modes/king_of_the_hill_mode.gd.uid b/spacewar/world/game_modes/king_of_the_hill_mode.gd.uid new file mode 100644 index 0000000..d1b50b3 --- /dev/null +++ b/spacewar/world/game_modes/king_of_the_hill_mode.gd.uid @@ -0,0 +1 @@ +uid://bsl7r3s8oey64 diff --git a/spacewar/world/game_modes/team_deathmatch_mode.gd b/spacewar/world/game_modes/team_deathmatch_mode.gd index 45091e4..56255a1 100644 --- a/spacewar/world/game_modes/team_deathmatch_mode.gd +++ b/spacewar/world/game_modes/team_deathmatch_mode.gd @@ -11,15 +11,4 @@ func get_mode_name() -> String: func check_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, - } + return _kills_based_win_condition(world) diff --git a/spacewar/world/levels/exit_zone.gd b/spacewar/world/levels/exit_zone.gd new file mode 100644 index 0000000..d3a41ce --- /dev/null +++ b/spacewar/world/levels/exit_zone.gd @@ -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) diff --git a/spacewar/world/levels/exit_zone.gd.uid b/spacewar/world/levels/exit_zone.gd.uid new file mode 100644 index 0000000..02d0394 --- /dev/null +++ b/spacewar/world/levels/exit_zone.gd.uid @@ -0,0 +1 @@ +uid://b33v18lu67xre diff --git a/spacewar/world/levels/military_hub.tscn b/spacewar/world/levels/military_hub.tscn new file mode 100644 index 0000000..2c6d3c9 --- /dev/null +++ b/spacewar/world/levels/military_hub.tscn @@ -0,0 +1,74 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://world/levels/station_hub.gd" id="1_hub"] +[ext_resource type="TileSet" path="res://world/station_tileset.tres" id="2_tileset"] +[ext_resource type="Script" path="res://world/levels/on_foot_character.gd" id="3_player"] +[ext_resource type="Texture2D" path="res://assets/images/worldtiles/station_door.png" id="4_door"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/locker.png" id="5_locker"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/cabinet.png" id="6_cabinet"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/plant.png" id="7_plant"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/server_rack.png" id="8_server"] +[ext_resource type="Script" path="res://world/levels/exit_zone.gd" id="9_exit"] + +[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_player"] +height = 20.0 + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_exit"] +size = Vector2(64, 48) + +[node name="MilitaryHub" type="Node2D"] +script = ExtResource("1_hub") + +[node name="Floor" type="TileMapLayer" parent="."] +tile_map_data = PackedByteArray("AAABAAEAAAAAAAAAAAABAAIAAAAAAAAAAAABAAMAAAAAAAAAAAABAAQAAAABAAAAAAABAAUAAAAAAAAAAAABAAYAAAACAAAAAAABAAcAAAAAAAAAAAACAAEAAAAAAAAAAAACAAIAAAAAAAAAAAACAAMAAAABAAAAAAACAAQAAAAAAAAAAAACAAUAAAACAAAAAAACAAYAAAAAAAAAAAACAAcAAAAAAAAAAAADAAEAAAAAAAAAAAADAAIAAAABAAAAAAADAAMAAAAAAAAAAAADAAQAAAACAAAAAAADAAUAAAAAAAAAAAADAAYAAAAAAAAAAAADAAcAAAABAAAAAAAEAAEAAAABAAAAAAAEAAIAAAAAAAAAAAAEAAMAAAACAAAAAAAEAAQAAAAAAAAAAAAEAAUAAAAAAAAAAAAEAAYAAAABAAAAAAAEAAcAAAAAAAAAAAAFAAEAAAAAAAAAAAAFAAIAAAACAAAAAAAFAAMAAAAAAAAAAAAFAAQAAAAAAAAAAAAFAAUAAAABAAAAAAAFAAYAAAAAAAAAAAAFAAcAAAAAAAAAAAAGAAEAAAACAAAAAAAGAAIAAAAAAAAAAAAGAAMAAAAAAAAAAAAGAAQAAAABAAAAAAAGAAUAAAAAAAAAAAAGAAYAAAAAAAAAAAAGAAcAAAAAAAAAAAAGAAgAAAACAAAAAAAHAAEAAAAAAAAAAAAHAAIAAAAAAAAAAAAHAAMAAAABAAAAAAAHAAQAAAAAAAAAAAAHAAUAAAAAAAAAAAAHAAYAAAAAAAAAAAAHAAcAAAACAAAAAAAIAAEAAAAAAAAAAAAIAAIAAAABAAAAAAAIAAMAAAAAAAAAAAAIAAQAAAAAAAAAAAAIAAUAAAAAAAAAAAAIAAYAAAACAAAAAAAIAAcAAAABAAAAAAAJAAEAAAABAAAAAAAJAAIAAAAAAAAAAAAJAAMAAAAAAAAAAAAJAAQAAAAAAAAAAAAJAAUAAAACAAAAAAAJAAYAAAABAAAAAAAJAAcAAAAAAAAAAAAKAAEAAAAAAAAAAAAKAAIAAAAAAAAAAAAKAAMAAAAAAAAAAAAKAAQAAAACAAAAAAAKAAUAAAABAAAAAAAKAAYAAAAAAAAAAAAKAAcAAAAAAAAAAAA=") +tile_set = ExtResource("2_tileset") + +[node name="Walls" type="TileMapLayer" parent="."] +tile_map_data = PackedByteArray("AAAAAAAAAQAAAAAAAAAAAAEAAQAAAAAAAAAAAAIAAQAAAAAAAAAAAAMAAQAAAAAAAAAAAAQAAQAAAAAAAAAAAAUAAQAAAAAAAAAAAAYAAQAAAAAAAAAAAAcAAQAAAAAAAAAAAAgAAQAAAAAAAAABAAAAAQABAAAAAAABAAgAAQABAAAAAAACAAAAAQABAAAAAAACAAgAAQABAAAAAAADAAAAAQABAAAAAAADAAgAAQABAAAAAAAEAAAAAQABAAAAAAAEAAgAAQABAAAAAAAFAAAAAQABAAAAAAAFAAgAAQABAAAAAAAGAAAAAQABAAAAAAAHAAAAAQABAAAAAAAHAAgAAQABAAAAAAAIAAAAAQABAAAAAAAIAAgAAQABAAAAAAAJAAAAAQABAAAAAAAJAAgAAQABAAAAAAAKAAAAAQABAAAAAAAKAAgAAQABAAAAAAALAAAAAQAAAAAAAAALAAEAAQAAAAAAAAALAAIAAQAAAAAAAAALAAMAAQAAAAAAAAALAAQAAQAAAAAAAAALAAUAAQAAAAAAAAALAAYAAQAAAAAAAAALAAcAAQAAAAAAAAALAAgAAQAAAAAAAAA=") +tile_set = ExtResource("2_tileset") + +[node name="Door" type="Sprite2D" parent="."] +position = Vector2(468, 612) +texture = ExtResource("4_door") + +[node name="Furniture" type="Node2D" parent="."] + +[node name="Locker" type="Sprite2D" parent="Furniture"] +position = Vector2(180, 180) +texture = ExtResource("5_locker") + +[node name="Cabinet" type="Sprite2D" parent="Furniture"] +position = Vector2(684, 180) +texture = ExtResource("6_cabinet") + +[node name="Plant" type="Sprite2D" parent="Furniture"] +position = Vector2(180, 468) +texture = ExtResource("7_plant") + +[node name="ServerRack" type="Sprite2D" parent="Furniture"] +position = Vector2(684, 468) +texture = ExtResource("8_server") + +[node name="Player" type="CharacterBody2D" parent="."] +position = Vector2(468, 324) +collision_layer = 2 +script = ExtResource("3_player") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"] +shape = SubResource("CapsuleShape2D_player") + +[node name="Sprite2D" type="Sprite2D" parent="Player"] + +[node name="Camera2D" type="Camera2D" parent="Player"] +position_smoothing_enabled = true +position_smoothing_speed = 8.0 + +[node name="ExitZone" type="Area2D" parent="."] +position = Vector2(468, 612) +collision_layer = 0 +collision_mask = 2 +script = ExtResource("9_exit") +target_scene = "res://world/levels/ship_interior.tscn" + +[node name="CollisionShape2D" type="CollisionShape2D" parent="ExitZone"] +shape = SubResource("RectangleShape2D_exit") diff --git a/spacewar/world/levels/nav_table.gd b/spacewar/world/levels/nav_table.gd new file mode 100644 index 0000000..be9ed19 --- /dev/null +++ b/spacewar/world/levels/nav_table.gd @@ -0,0 +1,36 @@ +extends Area2D +class_name NavTable + +# Helldivers-2-style interactable: walk up to it, a "PRESS E" prompt appears, +# interact opens the solar-system graph (nav_table_ui.gd). No dedicated +# console art exists yet (see overview/onfoot.md's asset inventory), so the +# visual is just a Label over whatever sprite the scene places here. + +@export var ui_path: NodePath + +@onready var _ui: NavTableUI = get_node(ui_path) +@onready var _prompt: Label = $Prompt + +var _player_in_range: bool = false + + +func _ready() -> void: + _prompt.visible = false + body_entered.connect(_on_body_entered) + body_exited.connect(_on_body_exited) + + +func _unhandled_input(event: InputEvent) -> void: + if _player_in_range and event.is_action_pressed("interact"): + _ui.open() + get_viewport().set_input_as_handled() + + +func _on_body_entered(_body: Node2D) -> void: + _player_in_range = true + _prompt.visible = true + + +func _on_body_exited(_body: Node2D) -> void: + _player_in_range = false + _prompt.visible = false diff --git a/spacewar/world/levels/nav_table.gd.uid b/spacewar/world/levels/nav_table.gd.uid new file mode 100644 index 0000000..cf600f1 --- /dev/null +++ b/spacewar/world/levels/nav_table.gd.uid @@ -0,0 +1 @@ +uid://kbh4kto8nhpy diff --git a/spacewar/world/levels/nav_table_ui.gd b/spacewar/world/levels/nav_table_ui.gd new file mode 100644 index 0000000..3b4ddd4 --- /dev/null +++ b/spacewar/world/levels/nav_table_ui.gd @@ -0,0 +1,254 @@ +extends CanvasLayer +class_name NavTableUI + +# The in-ship navigation table (see overview/onfoot.md's "Where does it fit +# in the flow" section) -- Helldivers-2-style solar-system graph, opened by +# walking up to nav_table.gd's console and pressing interact. V1 has 3 +# destinations: Space Combat (the matchmaking queue/connect flow that used +# to live directly on the main menu's QUICK PLAY button), the Belters hub +# (ORC, station_hub.tscn), and the Military hub (ISN, military_hub.tscn -- +# a functional placeholder reusing the Belters hub's art/script until a +# dedicated ISN art pass exists, see onfoot.md's phased-plan step 4). + +const BELTERS_HUB_SCENE := "res://world/levels/station_hub.tscn" +const MILITARY_HUB_SCENE := "res://world/levels/military_hub.tscn" +const WORLD_SCENE := "res://world/world.tscn" + +const PANEL_SIZE := Vector2(700, 460) +const CENTER_POINT := Vector2(350, 210) +const COMBAT_POINT := Vector2(350, 70) +const BELTERS_POINT := Vector2(160, 330) +const MILITARY_POINT := Vector2(540, 330) +const NODE_RADIUS := 42.0 + +var _root: Control +var _status_lbl: Label +var _combat_btn: Button +var _belters_btn: Button +var _military_btn: Button +var _connecting: bool = false + + +func _ready() -> void: + layer = 20 + _build_ui() + _root.visible = false + 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 _input(event: InputEvent) -> void: + if _root.visible and not _connecting and event.is_action_pressed("toggle_pause"): + close() + get_viewport().set_input_as_handled() + + +func open() -> void: + if _root.visible: + return + _root.visible = true + GameConfig.nav_table_focused = true + _status_lbl.text = "" + + +func close() -> void: + _root.visible = false + GameConfig.nav_table_focused = false + + +func _build_ui() -> void: + _root = Control.new() + _root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(_root) + + var dim := ColorRect.new() + dim.color = Color(0.0, 0.0, 0.0, 0.6) + dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + dim.mouse_filter = Control.MOUSE_FILTER_STOP + _root.add_child(dim) + + var panel := Panel.new() + panel.set_anchors_preset(Control.PRESET_CENTER) + panel.offset_left = -PANEL_SIZE.x / 2.0 + panel.offset_top = -PANEL_SIZE.y / 2.0 + panel.offset_right = PANEL_SIZE.x / 2.0 + panel.offset_bottom = PANEL_SIZE.y / 2.0 + var ps := StyleBoxFlat.new() + ps.bg_color = Color(0.04, 0.07, 0.16, 0.97) + ps.set_corner_radius_all(10) + ps.set_border_width_all(1) + ps.border_color = Color(0.18, 0.38, 0.65, 0.9) + panel.add_theme_stylebox_override("panel", ps) + _root.add_child(panel) + + var title := Label.new() + title.text = "NAVIGATION" + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title.offset_left = 0 + title.offset_top = 18 + title.offset_right = PANEL_SIZE.x + title.offset_bottom = 50 + title.add_theme_font_size_override("font_size", 24) + title.add_theme_color_override("font_color", Color(0.4, 0.85, 1.0)) + panel.add_child(title) + + _add_route_line(panel, CENTER_POINT, COMBAT_POINT) + _add_route_line(panel, CENTER_POINT, BELTERS_POINT) + _add_route_line(panel, CENTER_POINT, MILITARY_POINT) + + _add_center_marker(panel) + + _combat_btn = _add_node_button(panel, COMBAT_POINT, "SPACE COMBAT", Color(0.85, 0.4, 0.4)) + _combat_btn.pressed.connect(_on_combat_pressed) + + _belters_btn = _add_node_button(panel, BELTERS_POINT, "BELTERS HUB", Color(0.55, 0.75, 0.4)) + _belters_btn.pressed.connect(_on_belters_pressed) + + _military_btn = _add_node_button(panel, MILITARY_POINT, "MILITARY HUB", Color(0.4, 0.65, 0.85)) + _military_btn.pressed.connect(_on_military_pressed) + + _status_lbl = Label.new() + _status_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _status_lbl.offset_left = 0 + _status_lbl.offset_top = PANEL_SIZE.y - 74 + _status_lbl.offset_right = PANEL_SIZE.x + _status_lbl.offset_bottom = PANEL_SIZE.y - 50 + _status_lbl.add_theme_font_size_override("font_size", 14) + _status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86)) + panel.add_child(_status_lbl) + + var hint := Label.new() + hint.text = "ESC TO CLOSE" + hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + hint.offset_left = 0 + hint.offset_top = PANEL_SIZE.y - 36 + hint.offset_right = PANEL_SIZE.x + hint.offset_bottom = PANEL_SIZE.y - 14 + hint.add_theme_font_size_override("font_size", 11) + hint.add_theme_color_override("font_color", Color(0.4, 0.42, 0.48)) + panel.add_child(hint) + + +func _add_route_line(panel: Panel, from: Vector2, to: Vector2) -> void: + var line := Line2D.new() + line.add_point(from) + line.add_point(to) + line.width = 2.0 + line.default_color = Color(0.3, 0.5, 0.65, 0.6) + panel.add_child(line) + + +func _add_center_marker(panel: Panel) -> void: + var dot := Panel.new() + dot.offset_left = CENTER_POINT.x - 8 + dot.offset_top = CENTER_POINT.y - 8 + dot.offset_right = CENTER_POINT.x + 8 + dot.offset_bottom = CENTER_POINT.y + 8 + var ds := StyleBoxFlat.new() + ds.bg_color = Color(1.0, 1.0, 1.0, 0.9) + ds.set_corner_radius_all(8) + dot.add_theme_stylebox_override("panel", ds) + panel.add_child(dot) + + var lbl := Label.new() + lbl.text = "YOUR SHIP" + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + lbl.offset_left = CENTER_POINT.x - 60 + lbl.offset_top = CENTER_POINT.y + 12 + lbl.offset_right = CENTER_POINT.x + 60 + lbl.offset_bottom = CENTER_POINT.y + 30 + lbl.add_theme_font_size_override("font_size", 11) + lbl.add_theme_color_override("font_color", Color(0.7, 0.72, 0.78)) + panel.add_child(lbl) + + +func _add_node_button(panel: Panel, pos: Vector2, label_text: String, color: Color) -> Button: + var btn := Button.new() + btn.offset_left = pos.x - NODE_RADIUS + btn.offset_top = pos.y - NODE_RADIUS + btn.offset_right = pos.x + NODE_RADIUS + btn.offset_bottom = pos.y + NODE_RADIUS + + var sn := StyleBoxFlat.new() + sn.bg_color = Color(color.r, color.g, color.b, 0.22) + sn.set_corner_radius_all(int(NODE_RADIUS)) + sn.set_border_width_all(2) + sn.border_color = color + var sh := StyleBoxFlat.new() + sh.bg_color = Color(color.r, color.g, color.b, 0.4) + sh.set_corner_radius_all(int(NODE_RADIUS)) + sh.set_border_width_all(2) + sh.border_color = color + btn.add_theme_stylebox_override("normal", sn) + btn.add_theme_stylebox_override("hover", sh) + btn.add_theme_stylebox_override("pressed", sh) + panel.add_child(btn) + + var lbl := Label.new() + lbl.text = label_text + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + lbl.offset_left = pos.x - 80 + lbl.offset_top = pos.y + NODE_RADIUS + 6 + lbl.offset_right = pos.x + 80 + lbl.offset_bottom = pos.y + NODE_RADIUS + 24 + lbl.add_theme_font_size_override("font_size", 13) + lbl.add_theme_color_override("font_color", Color(0.9, 0.92, 0.96)) + panel.add_child(lbl) + + return btn + + +func _set_buttons_enabled(enabled: bool) -> void: + _combat_btn.disabled = not enabled + _belters_btn.disabled = not enabled + _military_btn.disabled = not enabled + + +func _on_combat_pressed() -> void: + if _connecting: + return + _connecting = true + _set_buttons_enabled(false) + _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_belters_pressed() -> void: + if _connecting: + return + close() + get_tree().change_scene_to_file(BELTERS_HUB_SCENE) + + +func _on_military_pressed() -> void: + if _connecting: + return + close() + get_tree().change_scene_to_file(MILITARY_HUB_SCENE) + + +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: + GameConfig.nav_table_focused = false + get_tree().change_scene_to_file(WORLD_SCENE) + + +func _on_connect_failed() -> void: + _connecting = false + _set_buttons_enabled(true) + _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 + _set_buttons_enabled(true) + _status_lbl.add_theme_color_override("font_color", Color(0.85, 0.4, 0.4)) + _status_lbl.text = "Matchmaking failed: %s" % reason diff --git a/spacewar/world/levels/nav_table_ui.gd.uid b/spacewar/world/levels/nav_table_ui.gd.uid new file mode 100644 index 0000000..fc6ebb3 --- /dev/null +++ b/spacewar/world/levels/nav_table_ui.gd.uid @@ -0,0 +1 @@ +uid://cadbqsy1ft5il diff --git a/spacewar/world/levels/nav_table_ui.tscn b/spacewar/world/levels/nav_table_ui.tscn new file mode 100644 index 0000000..6f70bda --- /dev/null +++ b/spacewar/world/levels/nav_table_ui.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3 uid="uid://cnavtableui01"] + +[ext_resource type="Script" path="res://world/levels/nav_table_ui.gd" id="1_script"] + +[node name="NavTableUI" type="CanvasLayer"] +script = ExtResource("1_script") diff --git a/spacewar/world/levels/on_foot_character.gd b/spacewar/world/levels/on_foot_character.gd new file mode 100644 index 0000000..c55326b --- /dev/null +++ b/spacewar/world/levels/on_foot_character.gd @@ -0,0 +1,35 @@ +extends CharacterBody2D +class_name OnFootCharacter + +const SPEED := 140.0 + +@onready var _sprite: Sprite2D = $Sprite2D +@onready var _camera: Camera2D = $Camera2D +var _idle_texture: Texture2D +var _walk_texture: Texture2D + + +func _ready() -> void: + var preset: Dictionary = CharacterPresets.get_preset(GameConfig.player_character_index) + _idle_texture = load(preset.idle) + _walk_texture = load(preset.walk) + _sprite.texture = _idle_texture + _camera.make_current() + + +func _physics_process(_delta: float) -> void: + var input_dir := Vector2.ZERO + if not GameConfig.nav_table_focused: + input_dir = Vector2( + Input.get_action_strength("move_right") - Input.get_action_strength("move_left"), + Input.get_action_strength("move_down") - Input.get_action_strength("move_up") + ).normalized() + velocity = input_dir * SPEED + move_and_slide() + _update_sprite(input_dir) + + +func _update_sprite(input_dir: Vector2) -> void: + _sprite.texture = _walk_texture if input_dir.length() > 0.0 else _idle_texture + if input_dir.x != 0.0: + _sprite.flip_h = input_dir.x < 0.0 diff --git a/spacewar/world/levels/on_foot_character.gd.uid b/spacewar/world/levels/on_foot_character.gd.uid new file mode 100644 index 0000000..002bc10 --- /dev/null +++ b/spacewar/world/levels/on_foot_character.gd.uid @@ -0,0 +1 @@ +uid://bljg5f4v4pnuu diff --git a/spacewar/world/levels/ship_interior.gd b/spacewar/world/levels/ship_interior.gd new file mode 100644 index 0000000..de70758 --- /dev/null +++ b/spacewar/world/levels/ship_interior.gd @@ -0,0 +1,73 @@ +extends Node2D +class_name ShipInterior + +# The Stardew-farm-equivalent home base (see overview/onfoot.md, phased-plan +# step 2). Single-player/local-only - no MultiplayerSpawner, no server +# authority, same as on_foot_character.gd's current controller. Distinct +# from world/levels/station_hub.tscn, which is spoken for as the Belters hub +# (see onfoot.md's Vision section) - this is the player's own ship, reusing +# station_hub.gd's tile-layer + wall-collider pattern rather than the scene +# itself. No dedicated ship-interior art exists yet, so this reuses the same +# ORC station tileset/furniture as a placeholder until item 29's asset +# inventory grows a set for it. +# +# The room is built procedurally in _ready() rather than hand-authored into +# the .tscn's tile_map_data, since there's no interactive editor pass to +# paint it with in this environment (see station_hub.tscn's own history: +# CLAUDE.md item 29 notes its layout was code-generated for the same reason). + +const TILE_SIZE := 72 +const ROOM_WIDTH := 8 # tiles, including the wall ring +const ROOM_HEIGHT := 6 +const DOOR_CELL := Vector2i(4, ROOM_HEIGHT - 1) + +const FLOOR_SOURCE_ID := 0 +const WALL_SOURCE_ID := 1 +const ATLAS_VARIANTS := 14 # station_tileset.tres has 14 columns per source + +@onready var _floor: TileMapLayer = $Floor +@onready var _walls: TileMapLayer = $Walls + + +func _ready() -> void: + _build_floor() + _build_walls() + _add_wall_colliders() + + +func _build_floor() -> void: + for x in range(1, ROOM_WIDTH - 1): + for y in range(1, ROOM_HEIGHT - 1): + var atlas_x := (x + y) % ATLAS_VARIANTS + _floor.set_cell(Vector2i(x, y), FLOOR_SOURCE_ID, Vector2i(atlas_x, 0)) + + +func _build_walls() -> void: + for x in range(ROOM_WIDTH): + _set_wall_cell(Vector2i(x, 0)) + _set_wall_cell(Vector2i(x, ROOM_HEIGHT - 1)) + for y in range(1, ROOM_HEIGHT - 1): + _set_wall_cell(Vector2i(0, y)) + _set_wall_cell(Vector2i(ROOM_WIDTH - 1, y)) + + +func _set_wall_cell(cell: Vector2i) -> void: + if cell == DOOR_CELL: + return + var atlas_x := (cell.x + cell.y) % ATLAS_VARIANTS + _walls.set_cell(cell, WALL_SOURCE_ID, Vector2i(atlas_x, 0)) + + +# Tiles have no collision shapes of their own (same approach as +# world.gd's ship-map collider builder and station_hub.gd) - spawn one +# StaticBody2D per used wall cell. +func _add_wall_colliders() -> void: + for cell in _walls.get_used_cells(): + var rect := RectangleShape2D.new() + rect.size = Vector2(TILE_SIZE, TILE_SIZE) + var shape := CollisionShape2D.new() + shape.shape = rect + var body := StaticBody2D.new() + body.position = _walls.map_to_local(cell) + body.add_child(shape) + _walls.add_child(body) diff --git a/spacewar/world/levels/ship_interior.gd.uid b/spacewar/world/levels/ship_interior.gd.uid new file mode 100644 index 0000000..8e2e382 --- /dev/null +++ b/spacewar/world/levels/ship_interior.gd.uid @@ -0,0 +1 @@ +uid://do1q5mh81f6uo diff --git a/spacewar/world/levels/ship_interior.tscn b/spacewar/world/levels/ship_interior.tscn new file mode 100644 index 0000000..82119c1 --- /dev/null +++ b/spacewar/world/levels/ship_interior.tscn @@ -0,0 +1,90 @@ +[gd_scene format=4] + +[ext_resource type="Script" path="res://world/levels/ship_interior.gd" id="1_interior"] +[ext_resource type="TileSet" path="res://world/station_tileset.tres" id="2_tileset"] +[ext_resource type="Script" path="res://world/levels/on_foot_character.gd" id="3_player"] +[ext_resource type="Texture2D" path="res://assets/images/worldtiles/station_door.png" id="4_door"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/locker.png" id="5_locker"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/cabinet.png" id="6_cabinet"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/server_rack.png" id="7_server"] +[ext_resource type="Script" path="res://world/levels/nav_table_ui.gd" id="8_nav_ui"] +[ext_resource type="Script" path="res://world/levels/nav_table.gd" id="9_nav_table"] +[ext_resource type="PackedScene" uid="uid://cpausemenu01a" path="res://menu/pause_menu.tscn" id="10_pm"] +[ext_resource type="PackedScene" uid="uid://csettingspanel1" path="res://menu/settings_panel.tscn" id="11_settings"] + +[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_player"] +height = 20.0 + +[sub_resource type="CircleShape2D" id="CircleShape2D_navtable"] +radius = 56.0 + +[node name="ShipInterior" type="Node2D"] +script = ExtResource("1_interior") + +[node name="Floor" type="TileMapLayer" parent="."] +tile_set = ExtResource("2_tileset") + +[node name="Walls" type="TileMapLayer" parent="."] +tile_set = ExtResource("2_tileset") + +[node name="Door" type="Sprite2D" parent="."] +position = Vector2(324, 396) +texture = ExtResource("4_door") + +[node name="Furniture" type="Node2D" parent="."] + +[node name="Locker" type="Sprite2D" parent="Furniture"] +position = Vector2(108, 108) +texture = ExtResource("5_locker") + +[node name="Cabinet" type="Sprite2D" parent="Furniture"] +position = Vector2(468, 108) +texture = ExtResource("6_cabinet") + +[node name="ServerRack" type="Sprite2D" parent="Furniture"] +position = Vector2(108, 324) +texture = ExtResource("7_server") + +[node name="Player" type="CharacterBody2D" parent="."] +position = Vector2(288, 216) +collision_layer = 2 +script = ExtResource("3_player") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"] +shape = SubResource("CapsuleShape2D_player") + +[node name="Sprite2D" type="Sprite2D" parent="Player"] + +[node name="Camera2D" type="Camera2D" parent="Player"] +position_smoothing_enabled = true +position_smoothing_speed = 8.0 + +[node name="NavTableUI" type="CanvasLayer" parent="."] +script = ExtResource("8_nav_ui") + +[node name="NavTable" type="Area2D" parent="."] +position = Vector2(468, 324) +collision_layer = 0 +collision_mask = 2 +script = ExtResource("9_nav_table") +ui_path = NodePath("../NavTableUI") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="NavTable"] +shape = SubResource("CircleShape2D_navtable") + +[node name="Sprite2D" type="Sprite2D" parent="NavTable"] +texture = ExtResource("7_server") + +[node name="Prompt" type="Label" parent="NavTable"] +offset_left = -50.0 +offset_top = -64.0 +offset_right = 50.0 +offset_bottom = -42.0 +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 13 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "PRESS E" + +[node name="PauseMenu" parent="." instance=ExtResource("10_pm")] + +[node name="SettingsPanel" parent="." instance=ExtResource("11_settings")] diff --git a/spacewar/world/levels/spritesheet.png b/spacewar/world/levels/spritesheet.png new file mode 100644 index 0000000..71ba92f Binary files /dev/null and b/spacewar/world/levels/spritesheet.png differ diff --git a/spacewar/world/levels/spritesheet.png.import b/spacewar/world/levels/spritesheet.png.import new file mode 100644 index 0000000..f0f9b03 --- /dev/null +++ b/spacewar/world/levels/spritesheet.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddjbejhsv471e" +path="res://.godot/imported/spritesheet.png-677537dde210d2ae043b3f0939c2aee0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://world/levels/spritesheet.png" +dest_files=["res://.godot/imported/spritesheet.png-677537dde210d2ae043b3f0939c2aee0.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 diff --git a/spacewar/world/levels/station_hub.gd b/spacewar/world/levels/station_hub.gd new file mode 100644 index 0000000..c748faa --- /dev/null +++ b/spacewar/world/levels/station_hub.gd @@ -0,0 +1,26 @@ +extends Node2D +class_name StationHub + +const TILE_SIZE := 72 + +@onready var _walls: TileMapLayer = $Walls + + +func _ready() -> void: + _add_wall_colliders() + + +# Tiles have no collision shapes of their own (same approach as world.gd's +# ship-map collider builder) - spawn one StaticBody2D per used wall cell. +# Reads whatever is actually painted on the Walls layer, so hand-edits made +# in the Tile Editor get colliders for free without touching this script. +func _add_wall_colliders() -> void: + for cell in _walls.get_used_cells(): + var rect := RectangleShape2D.new() + rect.size = Vector2(TILE_SIZE, TILE_SIZE) + var shape := CollisionShape2D.new() + shape.shape = rect + var body := StaticBody2D.new() + body.position = _walls.map_to_local(cell) + body.add_child(shape) + _walls.add_child(body) diff --git a/spacewar/world/levels/station_hub.gd.uid b/spacewar/world/levels/station_hub.gd.uid new file mode 100644 index 0000000..3c64df8 --- /dev/null +++ b/spacewar/world/levels/station_hub.gd.uid @@ -0,0 +1 @@ +uid://2rm5jqaxt3g3t diff --git a/spacewar/world/levels/station_hub.tscn b/spacewar/world/levels/station_hub.tscn new file mode 100644 index 0000000..aae25e7 --- /dev/null +++ b/spacewar/world/levels/station_hub.tscn @@ -0,0 +1,74 @@ +[gd_scene format=4] + +[ext_resource type="Script" path="res://world/levels/station_hub.gd" id="1_hub"] +[ext_resource type="TileSet" path="res://world/station_tileset.tres" id="2_tileset"] +[ext_resource type="Script" path="res://world/levels/on_foot_character.gd" id="3_player"] +[ext_resource type="Texture2D" path="res://assets/images/worldtiles/station_door.png" id="4_door"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/locker.png" id="5_locker"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/cabinet.png" id="6_cabinet"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/plant.png" id="7_plant"] +[ext_resource type="Texture2D" path="res://assets/images/worldsprites/furniture/server_rack.png" id="8_server"] +[ext_resource type="Script" path="res://world/levels/exit_zone.gd" id="9_exit"] + +[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_player"] +height = 20.0 + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_exit"] +size = Vector2(64, 48) + +[node name="StationHub" type="Node2D" unique_id=796862515] +script = ExtResource("1_hub") + +[node name="Floor" type="TileMapLayer" parent="." unique_id=1609840949] +tile_map_data = PackedByteArray("AAABAAEAAAAAAAAAAAABAAIAAAAAAAAAAAABAAMAAAAAAAAAAAABAAQAAAABAAAAAAABAAUAAAAAAAAAAAABAAYAAAACAAAAAAABAAcAAAAAAAAAAAACAAEAAAAAAAAAAAACAAIAAAAAAAAAAAACAAMAAAABAAAAAAACAAQAAAAAAAAAAAACAAUAAAACAAAAAAACAAYAAAAAAAAAAAACAAcAAAAAAAAAAAADAAEAAAAAAAAAAAADAAIAAAABAAAAAAADAAMAAAAAAAAAAAADAAQAAAACAAAAAAADAAUAAAAAAAAAAAADAAYAAAAAAAAAAAADAAcAAAABAAAAAAAEAAEAAAABAAAAAAAEAAIAAAAAAAAAAAAEAAMAAAACAAAAAAAEAAQAAAAAAAAAAAAEAAUAAAAAAAAAAAAEAAYAAAABAAAAAAAEAAcAAAAAAAAAAAAFAAEAAAAAAAAAAAAFAAIAAAACAAAAAAAFAAMAAAAAAAAAAAAFAAQAAAAAAAAAAAAFAAUAAAABAAAAAAAFAAYAAAAAAAAAAAAFAAcAAAAAAAAAAAAGAAEAAAACAAAAAAAGAAIAAAAAAAAAAAAGAAMAAAAAAAAAAAAGAAQAAAABAAAAAAAGAAUAAAAAAAAAAAAGAAYAAAAAAAAAAAAGAAcAAAAAAAAAAAAGAAgAAAACAAAAAAAHAAEAAAAAAAAAAAAHAAIAAAAAAAAAAAAHAAMAAAABAAAAAAAHAAQAAAAAAAAAAAAHAAUAAAAAAAAAAAAHAAYAAAAAAAAAAAAHAAcAAAACAAAAAAAIAAEAAAAAAAAAAAAIAAIAAAABAAAAAAAIAAMAAAAAAAAAAAAIAAQAAAAAAAAAAAAIAAUAAAAAAAAAAAAIAAYAAAACAAAAAAAIAAcAAAABAAAAAAAJAAEAAAABAAAAAAAJAAIAAAAAAAAAAAAJAAMAAAAAAAAAAAAJAAQAAAAAAAAAAAAJAAUAAAACAAAAAAAJAAYAAAABAAAAAAAJAAcAAAAAAAAAAAAKAAEAAAAAAAAAAAAKAAIAAAAAAAAAAAAKAAMAAAAAAAAAAAAKAAQAAAACAAAAAAAKAAUAAAABAAAAAAAKAAYAAAAAAAAAAAAKAAcAAAAAAAAAAAA=") +tile_set = ExtResource("2_tileset") + +[node name="Walls" type="TileMapLayer" parent="." unique_id=372888637] +tile_map_data = PackedByteArray("AAAAAAAAAQAAAAAAAAAAAAEAAQAAAAAAAAAAAAIAAQAAAAAAAAAAAAMAAQAAAAAAAAAAAAQAAQAAAAAAAAAAAAUAAQAAAAAAAAAAAAYAAQAAAAAAAAAAAAcAAQAAAAAAAAAAAAgAAQAAAAAAAAABAAAAAQABAAAAAAABAAgAAQABAAAAAAACAAAAAQABAAAAAAACAAgAAQABAAAAAAADAAAAAQABAAAAAAADAAgAAQABAAAAAAAEAAAAAQABAAAAAAAEAAgAAQABAAAAAAAFAAAAAQABAAAAAAAFAAgAAQABAAAAAAAGAAAAAQABAAAAAAAHAAAAAQABAAAAAAAHAAgAAQABAAAAAAAIAAAAAQABAAAAAAAIAAgAAQABAAAAAAAJAAAAAQABAAAAAAAJAAgAAQABAAAAAAAKAAAAAQABAAAAAAAKAAgAAQABAAAAAAALAAAAAQAAAAAAAAALAAEAAQAAAAAAAAALAAIAAQAAAAAAAAALAAMAAQAAAAAAAAALAAQAAQAAAAAAAAALAAUAAQAAAAAAAAALAAYAAQAAAAAAAAALAAcAAQAAAAAAAAALAAgAAQAAAAAAAAA=") +tile_set = ExtResource("2_tileset") + +[node name="Door" type="Sprite2D" parent="." unique_id=78571898] +position = Vector2(468, 612) +texture = ExtResource("4_door") + +[node name="Furniture" type="Node2D" parent="." unique_id=189395795] + +[node name="Locker" type="Sprite2D" parent="Furniture" unique_id=1181709526] +position = Vector2(180, 180) +texture = ExtResource("5_locker") + +[node name="Cabinet" type="Sprite2D" parent="Furniture" unique_id=1072766267] +position = Vector2(684, 180) +texture = ExtResource("6_cabinet") + +[node name="Plant" type="Sprite2D" parent="Furniture" unique_id=1750488056] +position = Vector2(180, 468) +texture = ExtResource("7_plant") + +[node name="ServerRack" type="Sprite2D" parent="Furniture" unique_id=1803790291] +position = Vector2(684, 468) +texture = ExtResource("8_server") + +[node name="Player" type="CharacterBody2D" parent="." unique_id=1269016771] +position = Vector2(468, 324) +collision_layer = 2 +script = ExtResource("3_player") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Player" unique_id=219190785] +shape = SubResource("CapsuleShape2D_player") + +[node name="Sprite2D" type="Sprite2D" parent="Player" unique_id=1615587378] + +[node name="Camera2D" type="Camera2D" parent="Player" unique_id=304299667] +position_smoothing_enabled = true +position_smoothing_speed = 8.0 + +[node name="ExitZone" type="Area2D" parent="."] +position = Vector2(468, 612) +collision_layer = 0 +collision_mask = 2 +script = ExtResource("9_exit") +target_scene = "res://world/levels/ship_interior.tscn" + +[node name="CollisionShape2D" type="CollisionShape2D" parent="ExitZone"] +shape = SubResource("RectangleShape2D_exit") diff --git a/spacewar/world/maps/map_02.tscn b/spacewar/world/maps/map_02.tscn new file mode 100644 index 0000000..509ba1e --- /dev/null +++ b/spacewar/world/maps/map_02.tscn @@ -0,0 +1,32 @@ +[gd_scene format=4 uid="uid://hbrpoig8f1cbf"] + +[ext_resource type="TileSet" uid="uid://cufng7112p6a7" path="res://world/world_tileset.tres" id="1_tileset"] +[ext_resource type="Shader" path="res://world/starfield.gdshader" id="2_starfield"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_starfield"] +shader = ExtResource("2_starfield") + +[node name="Map02" type="Node2D" unique_id=1843060858] + +[node name="Background" type="ColorRect" parent="." unique_id=1935208902] +material = SubResource("ShaderMaterial_starfield") +offset_left = -7008.0 +offset_top = -3000.0 +offset_right = 7008.0 +offset_bottom = 3000.0 + +[node name="Walls" type="TileMapLayer" parent="." unique_id=1965607015] +scale = Vector2(0.15, 0.15) +tile_set = ExtResource("1_tileset") + +[node name="Asteroids" type="TileMapLayer" parent="." unique_id=1745417804] +scale = Vector2(0.15, 0.15) +tile_set = ExtResource("1_tileset") + +[node name="SpawnPoints" type="Node2D" parent="." unique_id=1668900369] + +[node name="TeamASpawn1" type="Marker2D" parent="SpawnPoints" unique_id=946644648 groups=["team_a_spawn"]] +position = Vector2(5304, 0) + +[node name="TeamBSpawn1" type="Marker2D" parent="SpawnPoints" unique_id=449830332 groups=["team_b_spawn"]] +position = Vector2(-5304, 0) diff --git a/spacewar/world/maps/map_03.tscn b/spacewar/world/maps/map_03.tscn new file mode 100644 index 0000000..c1eb21c --- /dev/null +++ b/spacewar/world/maps/map_03.tscn @@ -0,0 +1,32 @@ +[gd_scene format=4 uid="uid://no6b9m80o2rak"] + +[ext_resource type="TileSet" uid="uid://cufng7112p6a7" path="res://world/world_tileset.tres" id="1_tileset"] +[ext_resource type="Shader" path="res://world/starfield.gdshader" id="2_starfield"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_starfield"] +shader = ExtResource("2_starfield") + +[node name="Map03" type="Node2D" unique_id=1843060859] + +[node name="Background" type="ColorRect" parent="." unique_id=1935208903] +material = SubResource("ShaderMaterial_starfield") +offset_left = -7008.0 +offset_top = -3000.0 +offset_right = 7008.0 +offset_bottom = 3000.0 + +[node name="Walls" type="TileMapLayer" parent="." unique_id=1965607016] +scale = Vector2(0.15, 0.15) +tile_set = ExtResource("1_tileset") + +[node name="Asteroids" type="TileMapLayer" parent="." unique_id=1745417805] +scale = Vector2(0.15, 0.15) +tile_set = ExtResource("1_tileset") + +[node name="SpawnPoints" type="Node2D" parent="." unique_id=1668900370] + +[node name="TeamASpawn1" type="Marker2D" parent="SpawnPoints" unique_id=946644649 groups=["team_a_spawn"]] +position = Vector2(5304, 0) + +[node name="TeamBSpawn1" type="Marker2D" parent="SpawnPoints" unique_id=449830333 groups=["team_b_spawn"]] +position = Vector2(-5304, 0) diff --git a/spacewar/world/maps/map_04.tscn b/spacewar/world/maps/map_04.tscn new file mode 100644 index 0000000..457afa4 --- /dev/null +++ b/spacewar/world/maps/map_04.tscn @@ -0,0 +1,32 @@ +[gd_scene format=4 uid="uid://1vrjnvgfygwwq"] + +[ext_resource type="TileSet" uid="uid://cufng7112p6a7" path="res://world/world_tileset.tres" id="1_tileset"] +[ext_resource type="Shader" path="res://world/starfield.gdshader" id="2_starfield"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_starfield"] +shader = ExtResource("2_starfield") + +[node name="Map04" type="Node2D" unique_id=1843060860] + +[node name="Background" type="ColorRect" parent="." unique_id=1935208904] +material = SubResource("ShaderMaterial_starfield") +offset_left = -7008.0 +offset_top = -3000.0 +offset_right = 7008.0 +offset_bottom = 3000.0 + +[node name="Walls" type="TileMapLayer" parent="." unique_id=1965607017] +scale = Vector2(0.15, 0.15) +tile_set = ExtResource("1_tileset") + +[node name="Asteroids" type="TileMapLayer" parent="." unique_id=1745417806] +scale = Vector2(0.15, 0.15) +tile_set = ExtResource("1_tileset") + +[node name="SpawnPoints" type="Node2D" parent="." unique_id=1668900371] + +[node name="TeamASpawn1" type="Marker2D" parent="SpawnPoints" unique_id=946644650 groups=["team_a_spawn"]] +position = Vector2(5304, 0) + +[node name="TeamBSpawn1" type="Marker2D" parent="SpawnPoints" unique_id=449830334 groups=["team_b_spawn"]] +position = Vector2(-5304, 0) diff --git a/spacewar/world/maps/map_05.tscn b/spacewar/world/maps/map_05.tscn new file mode 100644 index 0000000..8ac766e --- /dev/null +++ b/spacewar/world/maps/map_05.tscn @@ -0,0 +1,32 @@ +[gd_scene format=4 uid="uid://c38hyf9sxmeco"] + +[ext_resource type="TileSet" uid="uid://cufng7112p6a7" path="res://world/world_tileset.tres" id="1_tileset"] +[ext_resource type="Shader" path="res://world/starfield.gdshader" id="2_starfield"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_starfield"] +shader = ExtResource("2_starfield") + +[node name="Map05" type="Node2D" unique_id=1843060861] + +[node name="Background" type="ColorRect" parent="." unique_id=1935208905] +material = SubResource("ShaderMaterial_starfield") +offset_left = -7008.0 +offset_top = -3000.0 +offset_right = 7008.0 +offset_bottom = 3000.0 + +[node name="Walls" type="TileMapLayer" parent="." unique_id=1965607018] +scale = Vector2(0.15, 0.15) +tile_set = ExtResource("1_tileset") + +[node name="Asteroids" type="TileMapLayer" parent="." unique_id=1745417807] +scale = Vector2(0.15, 0.15) +tile_set = ExtResource("1_tileset") + +[node name="SpawnPoints" type="Node2D" parent="." unique_id=1668900372] + +[node name="TeamASpawn1" type="Marker2D" parent="SpawnPoints" unique_id=946644651 groups=["team_a_spawn"]] +position = Vector2(5304, 0) + +[node name="TeamBSpawn1" type="Marker2D" parent="SpawnPoints" unique_id=449830335 groups=["team_b_spawn"]] +position = Vector2(-5304, 0) diff --git a/spacewar/world/station_tileset.tres b/spacewar/world/station_tileset.tres new file mode 100644 index 0000000..0cd8785 --- /dev/null +++ b/spacewar/world/station_tileset.tres @@ -0,0 +1,46 @@ +[gd_resource type="TileSet" format=3 uid="uid://btddb33sj5rii"] + +[ext_resource type="Texture2D" uid="uid://dclgeqk1vui3x" path="res://assets/images/worldtiles/station_floor.png" id="1_floor"] +[ext_resource type="Texture2D" uid="uid://b88x7j2bou4yt" path="res://assets/images/worldtiles/station_walls.png" id="2_walls"] + +[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_floor"] +texture = ExtResource("1_floor") +texture_region_size = Vector2i(72, 72) +0:0/0 = 0 +1:0/0 = 0 +2:0/0 = 0 +3:0/0 = 0 +4:0/0 = 0 +5:0/0 = 0 +6:0/0 = 0 +7:0/0 = 0 +8:0/0 = 0 +9:0/0 = 0 +10:0/0 = 0 +11:0/0 = 0 +12:0/0 = 0 +13:0/0 = 0 + +[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_walls"] +texture = ExtResource("2_walls") +texture_region_size = Vector2i(72, 72) +0:0/0 = 0 +1:0/0 = 0 +2:0/0 = 0 +3:0/0 = 0 +4:0/0 = 0 +5:0/0 = 0 +6:0/0 = 0 +7:0/0 = 0 +8:0/0 = 0 +9:0/0 = 0 +10:0/0 = 0 +11:0/0 = 0 +12:0/0 = 0 +13:0/0 = 0 + +[resource] +tile_size = Vector2i(72, 72) +physics_layer_0/collision_layer = 1 +sources/0 = SubResource("TileSetAtlasSource_floor") +sources/1 = SubResource("TileSetAtlasSource_walls") diff --git a/spacewar/world/world.gd b/spacewar/world/world.gd index c4d3dfb..0607dc9 100644 --- a/spacewar/world/world.gd +++ b/spacewar/world/world.gd @@ -1,12 +1,54 @@ +class_name World extends Node2D -# Only one map exists today; structured as pick-from-list (same static-array -# convention as TeamSelect.RACES) so a second .tscn -- and later, per-mode or -# rotation selection driven by MatchManager -- is just appending here and -# changing _pick_map()'s selection logic. World's own load path never needs -# to change again. +# Structured as pick-from-list (same static-array convention as +# TeamSelect.RACES) so a new .tscn -- and later, per-mode or rotation +# selection driven by MatchManager -- is just appending here and changing +# pick_map()'s selection logic. World's own load path never needs to change +# again. +# +# "categories" is the set of GameMode ids (see world/game_modes/, +# GameConfig.default_game_mode_id) this map is actually built and tuned for +# -- same idea as CS's de_/cs_ map pools, but a map can list more than one +# category rather than being locked to exactly one. map_01 only lists +# "team_deathmatch" -- Domination/Conquest/King of the Hill exist as +# GameMode stubs (see CLAUDE.md item 27) but no map has real layout/mechanics +# support for them yet, so advertising a map for those modes would be a lie +# a player could actually queue into. Add a category here once a map is +# verified to actually play well under that mode. map_02-05 are untouched +# copies of map_01 (empty geometry, no categories) waiting to be hand-built +# in the Godot editor into their own distinct layouts. const MAPS := [ - {"id": "map_01", "name": "Sector Alpha", "scene_path": "res://world/maps/map_01.tscn"}, + { + "id": "map_01", + "name": "Sector Alpha", + "scene_path": "res://world/maps/map_01.tscn", + "categories": ["team_deathmatch"], + }, + { + "id": "map_02", + "name": "Sector Beta", + "scene_path": "res://world/maps/map_02.tscn", + "categories": [], + }, + { + "id": "map_03", + "name": "Sector Gamma", + "scene_path": "res://world/maps/map_03.tscn", + "categories": [], + }, + { + "id": "map_04", + "name": "Sector Delta", + "scene_path": "res://world/maps/map_04.tscn", + "categories": [], + }, + { + "id": "map_05", + "name": "Sector Epsilon", + "scene_path": "res://world/maps/map_05.tscn", + "categories": [], + }, ] const SHIP_SCENE: PackedScene = preload("res://ships/ship.tscn") @@ -50,7 +92,7 @@ var _flagships_spawned: bool = false func _ready() -> void: - var map: Node = load(_pick_map().scene_path).instantiate() + var map: Node = load(pick_map(GameConfig.default_game_mode_id).scene_path).instantiate() _map_container.add_child(map) var background := map.get_node_or_null("Background") as Control if background: @@ -73,10 +115,28 @@ func _ready() -> void: MatchStats.request_match_stats.rpc_id(1) -# Only one map exists today (see MAPS above); this is the seam a future -# map/mode-rotation selection hooks into without World's load path changing. -func _pick_map() -> Dictionary: - return MAPS[0] +# Filters MAPS down to whichever support the given GameMode id (see MAPS' +# "categories" field) and picks the first match. map_02-05 have no +# categories yet (untouched map_01 copies, not tuned for any mode), so this +# still always resolves to map_01 today -- but the filtering is real, and a +# map that doesn't list the active mode's category is correctly skipped. +# Deliberately not randomized among ties: every peer/caller runs this +# independently with no shared seed (unlike _spawn_flagships()' _match_seed), +# so a random pick here would desync which map each peer loads the instant a +# second candidate exists. Randomized map rotation needs that seed-sync +# treatment first, not just this filter. +# +# static + public so NetworkManager can look up the match's map/mode for +# matchmaking-api server registration (see host_server()'s heartbeat) without +# needing a live World instance -- the heartbeat starts on server boot, +# before world.tscn is even loaded. +static func pick_map(mode_id: String) -> Dictionary: + var candidates: Array = MAPS.filter( + func(m): return mode_id in m.categories + ) + if candidates.is_empty(): + candidates = MAPS + return candidates[0] # Picks the match's offered races once and caches them, so every caller diff --git a/spacewar/world/world.tscn b/spacewar/world/world.tscn index 69c9cb2..2580a63 100644 --- a/spacewar/world/world.tscn +++ b/spacewar/world/world.tscn @@ -13,6 +13,7 @@ [ext_resource type="PackedScene" path="res://hud/match_timer.tscn" id="12_mtimer"] [ext_resource type="PackedScene" path="res://hud/scoreboard.tscn" id="13_sboard"] [ext_resource type="PackedScene" path="res://hud/match_banner.tscn" id="14_mbanner"] +[ext_resource type="PackedScene" path="res://hud/map_name_display.tscn" id="15_mname"] [node name="World" type="Node2D" unique_id=1962020789] script = ExtResource("4_world") @@ -45,6 +46,8 @@ spawn_path = NodePath("../Bullets") [node name="PingDisplay" parent="." instance=ExtResource("9_ping")] +[node name="MapNameDisplay" parent="." instance=ExtResource("15_mname")] + [node name="HealthBar" parent="." instance=ExtResource("10_ebar")] margin_top = 20.0 fill_color = Color(0.3, 0.85, 0.35, 0.95)