Switched to HTML5
@@ -1,11 +1,10 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
# Node dependencies and coverage output
|
||||
/node_modules/
|
||||
/coverage/
|
||||
|
||||
# Build artifacts
|
||||
/exportsw/
|
||||
# Nix build artifacts
|
||||
/result
|
||||
/result-*
|
||||
|
||||
# Nix / direnv
|
||||
# direnv
|
||||
/.direnv/
|
||||
|
||||
@@ -1,38 +1,81 @@
|
||||
# AGENTS.md
|
||||
|
||||
Godot 4.6 GDScript strategy game, early-stage (most gameplay is stubbed).
|
||||
Battle for 'Tismo — HTML5 port of the strategy game. Pure JavaScript: shared
|
||||
game logic runs both in Node (authoritative server) and in the browser (client).
|
||||
There is no build step and no runtime dependency; the client vendors jQuery and
|
||||
everything else (transport included) is hand-written.
|
||||
|
||||
## Commands
|
||||
|
||||
- Validate / reimport after edits: `godot --headless --path . --import`
|
||||
- Run the test suite: `godot --headless --path . res://tests/test_runner.tscn` (prints a summary and exits non-zero on failure)
|
||||
- Parse-check a single script: `godot --headless --path . --check-only --script res://scenes/units/unit.gd`
|
||||
- Run a scene directly: `godot --path . res://scenes/ui/menu_screen.tscn`
|
||||
- Profile the headless server (prints a report every N seconds): `godot --headless --path . res://scenes/server_main.tscn -- --server --profile-interval 5`. A client's `--profile-interval` is forwarded to the server it spawns, and F3 on the client prints/clears its own report. F4 prints the `PerformanceMonitor` report (FPS-dive diagnostics plus the recent action log) without clearing it; the HUD also prints one automatically when the frame rate dives.
|
||||
- There is no linter, formatter, or CI. Do not invent commands beyond the ones listed here.
|
||||
- `godot --path .` with no scene fails: `project.godot` sets no `application/run/main_scene`. Pass a scene path or set the main scene in the editor.
|
||||
- `--check-only` does not load autoloads, so scripts referencing `GameClock`, `GameConfig` or `Network` report false "Identifier not found" errors. Prefer `--import` plus the test suite for real validation.
|
||||
- Install dev dependencies once: `npm install` (only `c8` and `jsdom`, used by
|
||||
the test suite — nothing the game runs needs them).
|
||||
- Run the test suite: `node tests/run_tests.js` (alias `npm test`; prints a
|
||||
summary and exits non-zero on failure).
|
||||
- Coverage report: `npm run coverage` (c8, writes `coverage/`).
|
||||
- Start the game server and client locally:
|
||||
`node server/server.js --port 27015 --bind 127.0.0.1`. Then open
|
||||
`http://127.0.0.1:27015/` (redirects to `/client/index.html`). The browser
|
||||
connects to `ws(s)://<page-origin>/ws`.
|
||||
- Install the versioned git hooks (runs the tests on every commit):
|
||||
`./scripts/install-git-hooks.sh`; uninstall with
|
||||
`git config --unset core.hooksPath`.
|
||||
- Build the deployable packages: `nix build .#server` and `nix build .#web`.
|
||||
- If `node` is not on PATH, use the pinned one: `nix-shell -p nodejs --run "node tests/run_tests.js"`.
|
||||
|
||||
There is no linter, formatter, or CI. Do not invent commands beyond these.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Renderer is `gl_compatibility` (not Forward+) and 3D uses Jolt Physics. GL Compatibility omits Forward+ only features.
|
||||
- `.uid` files are Godot-maintained; keep them paired with their scripts, never hand-edit or delete. `.godot/` is generated and gitignored.
|
||||
- Indent `.gd` files with tabs (Godot default). `.editorconfig` only enforces UTF-8.
|
||||
- `scenes/world_map.tscn` uses `TileMapLayer` (scene format=4), not the deprecated `TileMap`.
|
||||
- Multiplayer uses `WebSocketMultiplayerPeer` (HTTP), not ENet/UDP, so the browser client can reach the same server. The dedicated server listens on `--port` (default 27015) and binds to `--bind` (default `127.0.0.1`, i.e. reverse-proxy only); the client defaults to the page's own origin on the web, `ws://127.0.0.1:27015/ws` in development (editor or debug builds) and `wss://tismo.cfpi-fpsi.eu/ws` in release builds. Join accepts a bare hostname or a full `ws(s)://`/`http(s)://` URL. The join screen opens a lobby connection, asks the server for its game list (`Network.request_game_list` / `GameServer.game_list`), then logs in over the same connection with `Network.join_game_from_lobby`.
|
||||
- Export templates must match the engine: use `godot_4_6` with `godot_4_6-export-templates-bin` from nixpkgs.
|
||||
- Keep the shared modules framework-free: they are imported unchanged by both
|
||||
`server/` (Node, ESM) and `client/js/` (browser). No Node built-ins and no DOM
|
||||
APIs in `shared/` (`server/websocket.js` is the only place Node built-ins are
|
||||
expected).
|
||||
- Do not add JavaScript libraries. The only third-party code allowed is the
|
||||
vendored jQuery under `client/vendor/`; implement everything else by hand
|
||||
(see `server/websocket.js`, a dependency-free RFC 6455 server).
|
||||
- The game server also serves the app as static files from the repository root,
|
||||
so modules resolve as `/client/...` and `/shared/...`. Preserve the
|
||||
`client/` + `shared/` layout when packaging (`nix/package.nix`).
|
||||
- WebSocket state snapshots can be hundreds of KiB; the custom server handles
|
||||
fragmented and large frames (`server/websocket.js`). Do not lower the buffer
|
||||
limits without checking the snapshot path.
|
||||
- Indent `.js` files with 2 spaces. `.editorconfig` only enforces UTF-8.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Game logic is server-authoritative. `scripts/server/game_state.gd` (`GameState`) is a scene-free model that owns the world, units, cities, territory, economy, training and visibility. `scripts/server/game_server.gd` (`GameServer`) is the root of the headless server (`scenes/server_main.tscn`); it owns a `GameState`, validates orders and broadcasts snapshots. `Network` (autoload) carries orders up and snapshots down. `scenes/world_map.gd` is a view only: it rebuilds terrain from the shared seed, renders snapshots and sends orders (falling back to a local `GameState` when run standalone).
|
||||
- Movement is continuous: the server calls `GameState.advance_movement(delta / GameClock.SECONDS_PER_HOUR)` every frame and `tick_hour()` on each hour for training/visibility/economy; `advance_hour()` bundles both for tests and offline play. The client extrapolates each unit along its snapshot path (see `world_map.gd:_apply_unit_position`) so motion stays smooth between hourly snapshots, and predicts move orders locally (`_predict_path`, mirroring `GameState.find_path`) so a click feels instant before the server confirms it.
|
||||
- Data-driven resources: `class_name` scripts in `data/*.gd` (`CivDescription`, `ProtoUnit`) define schemas; concrete instances are `.tres` under `data/civilisations/` and `data/units/`. Add content as new `.tres` files, not hardcoded values.
|
||||
- Scenes are grouped by area: `scenes/`, `scenes/ui/`, `scenes/units/`. Reusable scenes attach a `class_name` script (e.g. `Unit`).
|
||||
- Reusable logic lives in `class_name` scripts under `scripts/classes/` (`MapGenerator`, `MapTopology`, `HexPathfinder`, `TextFormat`); keep new algorithms there so they stay testable without a running scene.
|
||||
- `scenes/map_overlay.gd` (`MapOverlay`) is the shared base for the tile overlays `Territory` and `FogOfWar`; put layer/topology/geometry plumbing there, not in the subclasses.
|
||||
- Tests live in `tests/` as `*_test.gd` files extending the `TestCase` base in `tests/framework/`. `tests/test_runner.gd` (run via `tests/test_runner.tscn`) discovers and runs them. Add a new `tests/<topic>_test.gd` for each new pure or `class_name` behaviour.
|
||||
- Game logic is server-authoritative. `shared/game_state.js` (`GameState`) is a
|
||||
framework-free model that owns the world, units, cities, territory, economy,
|
||||
training and visibility. `server/game_server.js` (`GameServer`) owns a
|
||||
`GameState`, validates orders and broadcasts snapshots over the transport in
|
||||
`server/server.js`. The browser client is a view: `client/js/map_view.js`
|
||||
rebuilds terrain from the shared seed, renders snapshots and sends orders.
|
||||
- Movement is continuous: `server/server.js` advances movement every tick and
|
||||
strikes an in-game hour every `SECONDS_PER_HOUR` real seconds. The client
|
||||
extrapolates each unit along its snapshot path and predicts move orders
|
||||
locally (`map_view.js`), mirroring `GameState.find_path` so a click feels
|
||||
instant. **Any pathfinding or movement-cost change must be mirrored on both
|
||||
sides** or prediction will fight the server.
|
||||
- `shared/` holds the reusable algorithms so they stay testable without a
|
||||
server: `hex.js` (topology/geometry), `map_generator.js`, `hex_pathfinder.js`,
|
||||
`terrain_stats.js`, `rules.js`, `economy` figures in `game_state.js`,
|
||||
`game_clock.js`, `rng.js`, `noise.js`, `text_format.js`, `login_manager.js`.
|
||||
Data-driven content (civilisations, units, buildings, governments,
|
||||
technologies) lives in `shared/data.js`.
|
||||
- `client/js/` splits the UI: `app.js` (screens and flow), `net.js` (transport
|
||||
wrapper), `game_screen.js` (HUD and panels), `map_view.js` (canvas/DOM map),
|
||||
`modals.js` (training, buildings, government/technology modals).
|
||||
- Tests live in `tests/` as `*_test.js` files extending the `TestCase` base in
|
||||
`tests/framework/`; `tests/run_tests.js` discovers and runs them. Add a new
|
||||
`tests/<topic>_test.js` for each new pure behaviour.
|
||||
|
||||
## Deployment
|
||||
|
||||
- `flake.nix` exposes `packages.<system>.server` and `.web`, plus `nixosModules.default`. `nix/module.nix` runs the server as a `services.battle-for-tismo` systemd unit and serves the web client through nginx, proxying `/ws` to the local server. The web client connects to the page's own origin.
|
||||
- Build locally with `nix build .#server` and `nix build .#web`; both use `godot_4_6` and its matching export templates from the flake's pinned nixpkgs.
|
||||
- `flake.nix` exposes `packages.<system>.server` and `.web`, plus
|
||||
`nixosModules.default`. There is no compiler step: `nix/package.nix` copies
|
||||
the app and wraps `node server/server.js`; `web` is the static `client/` and
|
||||
`shared/` tree.
|
||||
- `nix/module.nix` runs the server as a `services.battle-for-tismo` systemd unit
|
||||
and serves the static client through nginx, redirecting `/` to
|
||||
`/client/index.html` and proxying `/ws` to the local Node server. The browser
|
||||
client connects to the page's own origin, so HTTPS gives it `wss://`.
|
||||
- Build locally with `nix build .#server` and `nix build .#web`.
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b2hdr4csim04w"
|
||||
path="res://.godot/imported/aero-panel.png-a23a24f80c773b86dddbfa145cb68b13.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://aero-panel.png"
|
||||
dest_files=["res://.godot/imported/aero-panel.png-a23a24f80c773b86dddbfa145cb68b13.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 591 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 758 B After Width: | Height: | Size: 758 B |
|
Before Width: | Height: | Size: 594 B After Width: | Height: | Size: 594 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 271 B After Width: | Height: | Size: 271 B |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,390 @@
|
||||
:root {
|
||||
--hex-w: 63px;
|
||||
--hex-h: 56px;
|
||||
--panel-bg: linear-gradient(160deg, rgba(40, 52, 70, 0.96), rgba(18, 24, 34, 0.96));
|
||||
--panel-border: rgba(150, 190, 240, 0.35);
|
||||
--text: #e8eef7;
|
||||
--muted: #9fb0c8;
|
||||
--accent: #6cb6ff;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #05070c;
|
||||
color: var(--text);
|
||||
font-family: "Segoe UI", "DejaVu Sans", Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#screens { position: absolute; inset: 0; }
|
||||
|
||||
.screen { position: absolute; inset: 0; display: none; }
|
||||
.screen.active { display: block; }
|
||||
|
||||
/* ---------------------------------------------------------------- menus -- */
|
||||
|
||||
#screen-menu, #screen-new, #screen-join {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background:
|
||||
radial-gradient(circle at 50% 20%, rgba(60, 90, 140, 0.35), transparent 60%),
|
||||
linear-gradient(180deg, #0b1220, #05070c);
|
||||
}
|
||||
#screen-menu.active, #screen-new.active, #screen-join.active { display: flex; }
|
||||
|
||||
.menu-card {
|
||||
width: 380px;
|
||||
padding: 28px 32px;
|
||||
border-radius: 14px;
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--panel-border);
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
text-align: center;
|
||||
}
|
||||
.menu-card.wide { width: 520px; text-align: left; }
|
||||
.menu-card h1 { margin: 0 0 6px; font-size: 30px; letter-spacing: 1px; }
|
||||
.menu-card.wide h1 { text-align: center; }
|
||||
.tagline { color: var(--muted); margin: 0 0 24px; }
|
||||
|
||||
.field { display: block; margin: 14px 0; }
|
||||
.field > span:first-child { display: block; color: var(--muted); margin-bottom: 5px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.field .inline { display: flex; gap: 8px; }
|
||||
.field .inline input { flex: 1; }
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 9px 11px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid rgba(160, 190, 230, 0.3);
|
||||
background: rgba(8, 12, 20, 0.85);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, select:focus { border-color: var(--accent); }
|
||||
|
||||
.row { display: flex; gap: 10px; justify-content: center; margin-top: 18px; }
|
||||
.menu-card.wide .row { justify-content: flex-end; }
|
||||
|
||||
.aero {
|
||||
cursor: pointer;
|
||||
padding: 9px 18px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(150, 190, 240, 0.45);
|
||||
background: linear-gradient(180deg, rgba(90, 130, 190, 0.55), rgba(35, 55, 90, 0.85));
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
transition: filter 0.12s ease, transform 0.05s ease;
|
||||
}
|
||||
.aero:hover { filter: brightness(1.25); }
|
||||
.aero:active { transform: translateY(1px); }
|
||||
.aero:disabled { opacity: 0.4; cursor: default; filter: none; }
|
||||
.aero.primary { background: linear-gradient(180deg, rgba(110, 175, 255, 0.7), rgba(40, 95, 175, 0.9)); }
|
||||
.aero.big { display: block; width: 100%; margin: 10px 0; padding: 14px; font-size: 17px; }
|
||||
|
||||
.status { min-height: 20px; margin: 14px 0 0; color: #ffcf7a; text-align: center; }
|
||||
|
||||
.game-list {
|
||||
list-style: none;
|
||||
margin: 6px 0 14px;
|
||||
padding: 0;
|
||||
max-height: 190px;
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(150, 190, 240, 0.15);
|
||||
}
|
||||
.game-list li { padding: 9px 12px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||
.game-list li:hover { background: rgba(110, 175, 255, 0.15); }
|
||||
.game-list li.selected { background: rgba(110, 175, 255, 0.3); }
|
||||
|
||||
/* ------------------------------------------------------------- world map -- */
|
||||
|
||||
#screen-game.active { display: block; }
|
||||
|
||||
#map-viewport {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background-image: url("../assets/map_background.png");
|
||||
background-size: 512px 512px;
|
||||
background-color: #05070c;
|
||||
cursor: grab;
|
||||
}
|
||||
#map-viewport.dragging { cursor: grabbing; }
|
||||
|
||||
#map-world { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
|
||||
/* Promote the map to its own layer only while panning, so a drag is a GPU
|
||||
translation instead of a repaint of every hex. Keeping it off when idle
|
||||
avoids holding a huge composited layer in memory. */
|
||||
#map-viewport.dragging #map-world { will-change: transform; }
|
||||
.layer { position: absolute; left: 0; top: 0; }
|
||||
|
||||
/* Zoomed-out level of detail: the detailed hex/fog/border layers are hidden and
|
||||
a single simplified canvas is shown instead. */
|
||||
#layer-overview { display: none; }
|
||||
#map-world.lod #layer-overview { display: block; }
|
||||
#map-world.lod .detail-layer { display: none; }
|
||||
.overview-block { position: absolute; }
|
||||
|
||||
.hex {
|
||||
position: absolute;
|
||||
width: var(--hex-w);
|
||||
height: var(--hex-h);
|
||||
clip-path: polygon(100% 50%, 75% 0, 25% 0, 0 50%, 25% 100%, 75% 100%);
|
||||
}
|
||||
.hex.terrain {
|
||||
background-image: url("../assets/terrain.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 441px 56px;
|
||||
}
|
||||
.hex.territory { pointer-events: none; }
|
||||
.hex.fog { pointer-events: none; }
|
||||
.fog-unexplored { background: #04040a; }
|
||||
.fog-explored { background: rgba(4, 6, 18, 0.62); }
|
||||
|
||||
.border-seg {
|
||||
position: absolute;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
transform-origin: 0 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tile-selection {
|
||||
position: absolute;
|
||||
width: var(--hex-w);
|
||||
height: var(--hex-h);
|
||||
clip-path: polygon(100% 50%, 75% 0, 25% 0, 0 50%, 25% 100%, 75% 100%);
|
||||
border: none;
|
||||
box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.85);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.entity { position: absolute; }
|
||||
.unit {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
margin-left: -19px;
|
||||
margin-top: -19px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.unit .body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(0, 0, 0, 0.65);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.6);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.unit .body img { width: 78%; height: 78%; image-rendering: auto; }
|
||||
.unit.selected .body { box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.9), 0 2px 6px rgba(0,0,0,0.6); }
|
||||
.unit .health {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
right: 3px;
|
||||
bottom: -7px;
|
||||
height: 5px;
|
||||
background: rgba(30, 0, 0, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.7);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.unit .health .fill { height: 100%; background: #46d36a; }
|
||||
|
||||
.city {
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
.city .marker {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin: 0 auto;
|
||||
border-radius: 5px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
background-color: #333;
|
||||
background-size: cover;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
.city .label {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px #000, 0 0 4px #000;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.path-seg {
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
transform-origin: 0 50%;
|
||||
pointer-events: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ HUD -- */
|
||||
|
||||
#hud { position: absolute; inset: 0; pointer-events: none; }
|
||||
#hud .panel { pointer-events: auto; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
#top-panel {
|
||||
left: 12px;
|
||||
top: 12px;
|
||||
min-width: 420px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.flag-button {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--panel-border);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.flag-button img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.flag-button:hover { filter: brightness(1.2); }
|
||||
|
||||
.top-text { flex: 0 0 auto; min-width: 150px; }
|
||||
.civ-name { font-size: 19px; font-weight: 600; }
|
||||
.player-name { color: var(--muted); }
|
||||
.admin-label { color: #ffcf7a; font-size: 12px; }
|
||||
|
||||
.top-stats { display: flex; gap: 16px; margin-left: auto; }
|
||||
.stat { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
|
||||
.stat-value { font-size: 15px; font-variant-numeric: tabular-nums; }
|
||||
|
||||
#fps-label {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 12px;
|
||||
font-size: 13px;
|
||||
color: #8fdc8f;
|
||||
text-shadow: 0 1px 2px #000;
|
||||
}
|
||||
|
||||
#bottom-panel {
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
transform: translateX(-50%);
|
||||
min-width: 420px;
|
||||
max-width: 720px;
|
||||
padding: 12px 18px;
|
||||
}
|
||||
.info-section.hidden { display: none !important; }
|
||||
.info-title { font-size: 17px; font-weight: 600; margin-bottom: 4px; }
|
||||
#bottom-panel .row { justify-content: flex-start; margin-top: 10px; }
|
||||
.health-bar { width: 220px; height: 12px; background: rgba(30, 0, 0, 0.8); border-radius: 6px; overflow: hidden; margin-top: 6px; }
|
||||
#unit-health-fill { height: 100%; background: linear-gradient(180deg, #6fe08a, #2fa04d); }
|
||||
|
||||
/* --------------------------------------------------------------- modals -- */
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
}
|
||||
.modal.hidden { display: none; }
|
||||
|
||||
.modal-card {
|
||||
width: 620px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 14px;
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--panel-border);
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.7);
|
||||
overflow: hidden;
|
||||
}
|
||||
.modal-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.modal-sub { padding: 10px 18px; color: var(--muted); }
|
||||
.modal-body { padding: 12px 18px 18px; overflow-y: auto; }
|
||||
|
||||
.modal-close { padding: 4px 11px; }
|
||||
|
||||
.tabs { display: flex; gap: 6px; padding: 10px 18px 0; }
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: var(--muted);
|
||||
}
|
||||
.tab.active { background: rgba(110, 175, 255, 0.22); color: var(--text); }
|
||||
.tab-page { display: none; }
|
||||
.tab-page.active { display: block; }
|
||||
|
||||
.row-card {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 9px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.row-card .info { flex: 1; }
|
||||
.row-card .name { font-weight: 600; }
|
||||
.row-card .desc { color: var(--muted); font-size: 12px; margin: 3px 0; }
|
||||
.row-card .effects { font-size: 12px; color: #bcd4ff; }
|
||||
.row-card .cost { font-size: 12px; color: var(--muted); margin-top: 3px; }
|
||||
.row-card .actions { display: flex; flex-direction: column; gap: 6px; justify-content: center; }
|
||||
.row-card .actions .aero { min-width: 170px; }
|
||||
|
||||
.training-status { padding: 0 18px 16px; }
|
||||
.progress { height: 14px; margin-top: 8px; background: rgba(0, 0, 0, 0.5); border-radius: 7px; overflow: hidden; }
|
||||
#train-progress, #buildings-progress { height: 100%; width: 0; background: linear-gradient(180deg, #7fc4ff, #2f7fd0); }
|
||||
|
||||
.banner {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 12px;
|
||||
transform: translateX(-50%);
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
background: rgba(140, 20, 20, 0.9);
|
||||
border: 1px solid rgba(255, 120, 120, 0.6);
|
||||
z-index: 60;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Battle for 'Tismo</title>
|
||||
<link rel="icon" href="data:," />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="screens">
|
||||
<!-- Main menu -->
|
||||
<section id="screen-menu" class="screen active">
|
||||
<div class="menu-card">
|
||||
<h1>Battle for 'Tismo</h1>
|
||||
<p class="tagline">A persistent, continuous-time strategy game.</p>
|
||||
<button id="menu-create" class="aero big">Create game</button>
|
||||
<button id="menu-join" class="aero big">Join game</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- New game setup -->
|
||||
<section id="screen-new" class="screen">
|
||||
<div class="menu-card">
|
||||
<h1>New game</h1>
|
||||
<label class="field">
|
||||
<span>Your civilisation</span>
|
||||
<select id="new-civ"></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Nations in the world</span>
|
||||
<input id="new-count" type="number" min="1" max="9" value="4" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Player name</span>
|
||||
<input id="new-name" type="text" autocomplete="off" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Password</span>
|
||||
<input id="new-password" type="password" />
|
||||
</label>
|
||||
<div class="row">
|
||||
<button id="new-back" class="aero">Back</button>
|
||||
<button id="new-start" class="aero primary">Start game</button>
|
||||
</div>
|
||||
<p id="new-status" class="status"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Join screen -->
|
||||
<section id="screen-join" class="screen">
|
||||
<div class="menu-card wide">
|
||||
<h1>Join game</h1>
|
||||
<label class="field">
|
||||
<span>Server</span>
|
||||
<span class="inline">
|
||||
<input id="join-server" type="text" autocomplete="off" />
|
||||
<button id="join-refresh" class="aero">Refresh</button>
|
||||
</span>
|
||||
</label>
|
||||
<ul id="join-games" class="game-list"></ul>
|
||||
<label class="field">
|
||||
<span>Player name</span>
|
||||
<input id="join-name" type="text" autocomplete="off" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Password</span>
|
||||
<input id="join-password" type="password" />
|
||||
</label>
|
||||
<div class="row">
|
||||
<button id="join-back" class="aero">Back</button>
|
||||
<button id="join-create" class="aero">Create game</button>
|
||||
<button id="join-go" class="aero primary">Join</button>
|
||||
</div>
|
||||
<p id="join-status" class="status"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- World map -->
|
||||
<section id="screen-game" class="screen">
|
||||
<div id="map-viewport">
|
||||
<div id="map-world">
|
||||
<div class="layer" id="layer-overview"></div>
|
||||
<div class="layer" id="layer-terrain"></div>
|
||||
<div class="layer" id="layer-territory"></div>
|
||||
<div class="layer" id="layer-borders"></div>
|
||||
<div class="layer" id="layer-fog"></div>
|
||||
<div class="layer" id="layer-paths"></div>
|
||||
<div class="layer" id="layer-entities"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hud">
|
||||
<div id="top-panel" class="panel">
|
||||
<button id="civ-flag" class="flag-button" title="Government and technology">
|
||||
<img id="civ-flag-img" alt="" />
|
||||
</button>
|
||||
<div class="top-text">
|
||||
<div id="civ-name" class="civ-name">—</div>
|
||||
<div id="player-name" class="player-name"></div>
|
||||
<div id="admin-label" class="admin-label">Admin</div>
|
||||
</div>
|
||||
<div class="top-stats">
|
||||
<div class="stat"><span class="stat-label">Date</span><span id="stat-date" class="stat-value"></span></div>
|
||||
<div class="stat"><span class="stat-label">Population</span><span id="stat-pop" class="stat-value">0</span></div>
|
||||
<div class="stat"><span class="stat-label">GDP</span><span id="stat-gdp" class="stat-value">€0</span></div>
|
||||
<div class="stat"><span class="stat-label">GDP/capita</span><span id="stat-gdppc" class="stat-value">€0</span></div>
|
||||
<div class="stat"><span class="stat-label">Budget</span><span id="stat-budget" class="stat-value">€0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fps-label">60 FPS</div>
|
||||
|
||||
<div id="bottom-panel" class="panel hidden">
|
||||
<div id="tile-info" class="info-section hidden">
|
||||
<div id="tile-coords" class="info-title"></div>
|
||||
<div id="tile-owner"></div>
|
||||
<div id="tile-terrain"></div>
|
||||
<div id="tile-population"></div>
|
||||
<div id="tile-improvements"></div>
|
||||
</div>
|
||||
<div id="city-info" class="info-section hidden">
|
||||
<div id="city-name" class="info-title"></div>
|
||||
<div id="city-population"></div>
|
||||
<div id="city-improvements"></div>
|
||||
<div class="row">
|
||||
<button id="city-train" class="aero">Train</button>
|
||||
<button id="city-build" class="aero">Buildings</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="unit-info" class="info-section hidden">
|
||||
<div id="unit-name" class="info-title"></div>
|
||||
<div class="health-bar"><div id="unit-health-fill"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Training modal -->
|
||||
<div id="modal-train" class="modal hidden">
|
||||
<div class="modal-card">
|
||||
<div class="modal-head">
|
||||
<span id="train-title">Train</span>
|
||||
<button class="modal-close aero">✕</button>
|
||||
</div>
|
||||
<div id="train-list" class="modal-body"></div>
|
||||
<div id="train-status" class="training-status hidden">
|
||||
<div id="train-label"></div>
|
||||
<div class="progress"><div id="train-progress"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- City buildings modal -->
|
||||
<div id="modal-buildings" class="modal hidden">
|
||||
<div class="modal-card">
|
||||
<div class="modal-head">
|
||||
<span id="buildings-title">Buildings</span>
|
||||
<button class="modal-close aero">✕</button>
|
||||
</div>
|
||||
<div id="buildings-budget" class="modal-sub"></div>
|
||||
<div id="buildings-list" class="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nation modal: government and technology tabs -->
|
||||
<div id="modal-nation" class="modal hidden">
|
||||
<div class="modal-card">
|
||||
<div class="modal-head">
|
||||
<span id="nation-title">Nation</span>
|
||||
<button class="modal-close aero">✕</button>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="government">Government</button>
|
||||
<button class="tab" data-tab="technology">Technology</button>
|
||||
</div>
|
||||
<div class="tab-page active" id="tab-government">
|
||||
<div id="government-sub" class="modal-sub"></div>
|
||||
<div id="government-list" class="modal-body"></div>
|
||||
</div>
|
||||
<div class="tab-page" id="tab-technology">
|
||||
<div id="technology-sub" class="modal-sub"></div>
|
||||
<div id="technology-list" class="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="disconnect-banner" class="banner hidden"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="vendor/jquery.min.js"></script>
|
||||
<script type="module" src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,216 @@
|
||||
// Application bootstrap: screen routing plus the session state that the new
|
||||
// game and join screens fill in (the menu/new-game/join flow).
|
||||
|
||||
import { Network } from "./net.js";
|
||||
import { GameScreen } from "./game_screen.js";
|
||||
import { CIVILISATIONS } from "../../shared/data.js";
|
||||
|
||||
const network = new Network();
|
||||
const config = {
|
||||
playerName: "",
|
||||
password: "",
|
||||
playerCivId: CIVILISATIONS[0].id,
|
||||
civilisations: [],
|
||||
seed: 0,
|
||||
};
|
||||
|
||||
let gameScreen = null;
|
||||
let joining = false;
|
||||
let mode = "join";
|
||||
let games = [];
|
||||
let selectedGame = -1;
|
||||
|
||||
function showScreen(id) {
|
||||
$(".screen").removeClass("active");
|
||||
$(`#${id}`).addClass("active");
|
||||
}
|
||||
|
||||
function status(id, message) {
|
||||
$(`#${id}`).text(message);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- menu ------
|
||||
|
||||
$("#menu-create").on("click", () => showScreen("screen-new"));
|
||||
$("#menu-join").on("click", () => {
|
||||
showScreen("screen-join");
|
||||
refreshGames();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------- new game ------
|
||||
|
||||
const $civSelect = $("#new-civ");
|
||||
for (const civ of CIVILISATIONS) {
|
||||
$civSelect.append($("<option></option>").val(civ.id).text(civ.name));
|
||||
}
|
||||
$("#new-count").attr("max", CIVILISATIONS.length);
|
||||
|
||||
$("#new-back").on("click", () => {
|
||||
network.close();
|
||||
showScreen("screen-menu");
|
||||
});
|
||||
|
||||
$("#new-start").on("click", () => {
|
||||
const civId = $civSelect.val();
|
||||
const count = Math.max(1, Math.min(CIVILISATIONS.length, Number($("#new-count").val()) || 1));
|
||||
const name = $("#new-name").val().trim();
|
||||
const password = $("#new-password").val();
|
||||
if (!name || !password) {
|
||||
status("new-status", "Enter a name and a password.");
|
||||
return;
|
||||
}
|
||||
const player = CIVILISATIONS.find((c) => c.id === civId) || CIVILISATIONS[0];
|
||||
const list = [player];
|
||||
for (const civ of CIVILISATIONS) {
|
||||
if (list.length >= count) break;
|
||||
if (civ.id !== player.id) list.push(civ);
|
||||
}
|
||||
config.playerName = name;
|
||||
config.password = password;
|
||||
config.playerCivId = player.id;
|
||||
config.civilisations = list;
|
||||
config.seed = 0;
|
||||
mode = "create";
|
||||
setNewEnabled(false);
|
||||
status("new-status", "Starting the game...");
|
||||
network.createGame(name, password).catch(() => {
|
||||
setNewEnabled(true);
|
||||
status("new-status", "Could not connect to the game server.");
|
||||
});
|
||||
});
|
||||
|
||||
function setNewEnabled(enabled) {
|
||||
$("#new-start, #new-back").prop("disabled", !enabled);
|
||||
$("#new-count, #new-civ, #new-name, #new-password").prop("disabled", !enabled);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- join ------
|
||||
|
||||
$("#join-server").val(network.defaultServerUrl());
|
||||
|
||||
$("#join-refresh").on("click", () => refreshGames());
|
||||
$("#join-back").on("click", () => {
|
||||
network.close();
|
||||
showScreen("screen-menu");
|
||||
});
|
||||
$("#join-create").on("click", () => showScreen("screen-new"));
|
||||
$("#join-go").on("click", joinSelected);
|
||||
|
||||
function joinServerUrl() {
|
||||
return network.normalizeUrl($("#join-server").val());
|
||||
}
|
||||
|
||||
function refreshGames(message = "") {
|
||||
joining = false;
|
||||
games = [];
|
||||
selectedGame = -1;
|
||||
$("#join-games").empty();
|
||||
setJoinEnabled(false);
|
||||
status("join-status", message || "Looking for games...");
|
||||
network.requestGameList(joinServerUrl()).catch(() => {
|
||||
status("join-status", "Could not reach the game server.");
|
||||
setJoinEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
function setJoinEnabled(enabled) {
|
||||
$("#join-server, #join-name, #join-password").prop("disabled", !enabled);
|
||||
$("#join-refresh, #join-go, #join-create").prop("disabled", !enabled);
|
||||
}
|
||||
|
||||
function renderGames(list) {
|
||||
games = list;
|
||||
selectedGame = -1;
|
||||
const $list = $("#join-games").empty();
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const game = list[i];
|
||||
const parts = [game.name || "Game", game.date || "?", `${game.players || 0} player(s)`];
|
||||
if (game.civs && game.civs.length) parts.push(game.civs.join(", "));
|
||||
const $item = $("<li></li>").text(parts.join(" — "));
|
||||
$item.on("click", () => {
|
||||
selectedGame = i;
|
||||
$("#join-games li").removeClass("selected");
|
||||
$item.addClass("selected");
|
||||
});
|
||||
$list.append($item);
|
||||
}
|
||||
if (list.length === 0) {
|
||||
status("join-status", "No game in progress on this server.");
|
||||
} else {
|
||||
selectedGame = 0;
|
||||
$list.children().first().addClass("selected");
|
||||
status("join-status", "Select a game and enter a name and a password.");
|
||||
}
|
||||
setJoinEnabled(true);
|
||||
}
|
||||
|
||||
function joinSelected() {
|
||||
const name = $("#join-name").val().trim();
|
||||
const password = $("#join-password").val();
|
||||
if (!name || !password) {
|
||||
status("join-status", "Enter a name and a password.");
|
||||
return;
|
||||
}
|
||||
if (selectedGame < 0) {
|
||||
status("join-status", "Select a game to join.");
|
||||
return;
|
||||
}
|
||||
config.playerName = name;
|
||||
config.password = password;
|
||||
config.playerCivId = null;
|
||||
config.civilisations = [];
|
||||
mode = "join";
|
||||
joining = true;
|
||||
setJoinEnabled(false);
|
||||
status("join-status", "Connecting...");
|
||||
network.joinGame(joinServerUrl(), name, password).catch(() => {
|
||||
status("join-status", "Could not reach the game server.");
|
||||
setJoinEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ sessions ----
|
||||
|
||||
network.on("session_started", () => {
|
||||
if (mode === "create") {
|
||||
status("new-status", "");
|
||||
startGameScreen(true);
|
||||
} else {
|
||||
status("join-status", "");
|
||||
startGameScreen(false);
|
||||
}
|
||||
});
|
||||
|
||||
network.on("session_failed", (message) => {
|
||||
if (mode === "create") {
|
||||
setNewEnabled(true);
|
||||
status("new-status", message);
|
||||
} else {
|
||||
setJoinEnabled(true);
|
||||
status("join-status", message);
|
||||
if (joining) refreshGames(message);
|
||||
}
|
||||
});
|
||||
|
||||
network.on("game_list_received", (list) => renderGames(list));
|
||||
network.on("game_state_received", (state) => {
|
||||
if (gameScreen) gameScreen.onState(state);
|
||||
});
|
||||
network.on("server_disconnected", () => {
|
||||
$("#disconnect-banner").text("Disconnected from the server.").removeClass("hidden");
|
||||
});
|
||||
|
||||
function startGameScreen(isAdmin) {
|
||||
if (gameScreen) {
|
||||
gameScreen.leave();
|
||||
$(window).off("keydown.game");
|
||||
}
|
||||
$("#disconnect-banner").addClass("hidden");
|
||||
gameScreen = new GameScreen(network, config);
|
||||
showScreen("screen-game");
|
||||
gameScreen.enter();
|
||||
if (isAdmin) gameScreen.sendSetup();
|
||||
}
|
||||
|
||||
// Reconnect notice clears once the player navigates back to the menu.
|
||||
$("#menu-create, #menu-join").on("click", () => $("#disconnect-banner").addClass("hidden"));
|
||||
@@ -0,0 +1,403 @@
|
||||
// World-map screen controller. It wires the map view, the HUD and the modals
|
||||
// to the snapshots the server broadcasts, and turns player input into orders.
|
||||
// It owns no simulation: movement between snapshots is dead reckoning with the
|
||||
// same cost model as the server.
|
||||
|
||||
import { MapView } from "./map_view.js";
|
||||
import { TrainModal, BuildingsModal, NationModal } from "./modals.js";
|
||||
import { compact, groupDigits, improvements as fmtImprovements } from "../../shared/text_format.js";
|
||||
import {
|
||||
CIVILISATIONS,
|
||||
PROTO_UNITS,
|
||||
TRAINABLE_UNIT_IDS,
|
||||
BUILDINGS,
|
||||
GOVERNMENTS,
|
||||
TECHNOLOGIES,
|
||||
} from "../../shared/data.js";
|
||||
import { dateString } from "../../shared/game_clock.js";
|
||||
|
||||
export class GameScreen {
|
||||
constructor(network, config) {
|
||||
this.network = network;
|
||||
this.config = config;
|
||||
this.$screen = $("#screen-game");
|
||||
this.snapshot = null;
|
||||
this.localCiv = 0;
|
||||
this.civilisations = [];
|
||||
this.protoUnits = PROTO_UNITS;
|
||||
this.selectedUnitId = 0;
|
||||
this.selectedCityId = 0;
|
||||
this._trainingCityId = 0;
|
||||
this._cityBuildings = new Map();
|
||||
this._population = new Map();
|
||||
this._territoryOwner = new Map();
|
||||
this._researched = new Set();
|
||||
this._entered = false;
|
||||
this._lastFrame = 0;
|
||||
this._fpsFrames = 0;
|
||||
this._fpsElapsed = 0;
|
||||
|
||||
this.map = new MapView(
|
||||
$("#map-viewport"),
|
||||
$("#map-world"),
|
||||
{
|
||||
overview: $("#layer-overview"),
|
||||
terrain: $("#layer-terrain"),
|
||||
territory: $("#layer-territory"),
|
||||
borders: $("#layer-borders"),
|
||||
fog: $("#layer-fog"),
|
||||
paths: $("#layer-paths"),
|
||||
entities: $("#layer-entities"),
|
||||
}
|
||||
);
|
||||
this.trainModal = new TrainModal();
|
||||
this.buildingsModal = new BuildingsModal();
|
||||
this.nationModal = new NationModal();
|
||||
|
||||
this._bindMap();
|
||||
this._bindHud();
|
||||
}
|
||||
|
||||
enter() {
|
||||
this.$screen.addClass("active");
|
||||
this._entered = true;
|
||||
this.map._applyCamera();
|
||||
this._lastFrame = performance.now();
|
||||
requestAnimationFrame((t) => this._frame(t));
|
||||
}
|
||||
|
||||
leave() {
|
||||
this._entered = false;
|
||||
this.$screen.removeClass("active");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- orders ---
|
||||
|
||||
sendOrder(order) {
|
||||
this.network.sendOrder(order);
|
||||
}
|
||||
|
||||
sendSetup() {
|
||||
const ids = this.config.civilisations.map((c) => c.id);
|
||||
const playerIndex = Math.max(0, ids.indexOf(this.config.playerCivId));
|
||||
this.network.sendGameSetup({
|
||||
seed: this.config.seed || 0,
|
||||
player_civ: playerIndex,
|
||||
civilisations: ids,
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- snapshots ---
|
||||
|
||||
onState(state) {
|
||||
this.snapshot = state;
|
||||
this.localCiv = state.viewer;
|
||||
this.civilisations = state.civs.map(
|
||||
(c) => CIVILISATIONS.find((full) => full.id === c.id) || { id: c.id, name: c.name, primaryColour: "#888" }
|
||||
);
|
||||
this.protoUnits = state.protos.map(
|
||||
(p) => PROTO_UNITS.find((full) => full.id === p.id) || { id: p.id, name: p.name }
|
||||
);
|
||||
this._population = new Map(state.population.map((e) => [`${e[0]},${e[1]}`, e[2]]));
|
||||
this._territoryOwner = new Map(state.territory.map((e) => [`${e[0]},${e[1]}`, e[2]]));
|
||||
this._cityBuildings = new Map();
|
||||
for (const city of state.cities) {
|
||||
this._cityBuildings.set(city.id, { ...city.buildings });
|
||||
}
|
||||
this._researched = new Set(
|
||||
(state.researched[this.localCiv] || []).map((index) => Number(index))
|
||||
);
|
||||
|
||||
this.map.setCatalogue(this.civilisations, this.protoUnits);
|
||||
this.map.applySnapshot(state);
|
||||
this.map.setSelectedUnit(this.selectedUnitId);
|
||||
this._updateTopPanel(state);
|
||||
this._updateBottomPanel();
|
||||
this._updateTrainModal();
|
||||
this._updateBuildingsModal();
|
||||
this._updateNationModal();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- HUD ----
|
||||
|
||||
_bindHud() {
|
||||
$("#city-train").on("click", () => this._openTrainModal());
|
||||
$("#city-build").on("click", () => this._openBuildingsModal());
|
||||
$("#civ-flag").on("click", () => this._openNationModal());
|
||||
|
||||
this.trainModal.onTrain = (cityId, protoIndex) =>
|
||||
this.sendOrder({ type: "train", city: cityId, proto: protoIndex });
|
||||
this.buildingsModal.onBuild = (cityId, index) =>
|
||||
this.sendOrder({ type: "build", city: cityId, building: index });
|
||||
this.buildingsModal.onDemolish = (cityId, index) =>
|
||||
this.sendOrder({ type: "demolish", city: cityId, building: index });
|
||||
this.nationModal.onGovernment = (index) =>
|
||||
this.sendOrder({ type: "government", government: index });
|
||||
this.nationModal.onTechnology = (index) =>
|
||||
this.sendOrder({ type: "technology", technology: index });
|
||||
|
||||
$(window).on("keydown.game", (event) => this._onKey(event));
|
||||
}
|
||||
|
||||
_bindMap() {
|
||||
this.map.onUnitSelected = (id) => {
|
||||
if (!id) {
|
||||
this.selectedUnitId = 0;
|
||||
this._showUnit(null);
|
||||
return;
|
||||
}
|
||||
const unit = (this.snapshot.units || []).find((u) => u.id === id);
|
||||
if (!unit || unit.civ !== this.localCiv) {
|
||||
this.map.setSelectedUnit(0);
|
||||
this.selectedUnitId = 0;
|
||||
return;
|
||||
}
|
||||
this.selectedUnitId = id;
|
||||
this.selectedCityId = 0;
|
||||
this.map.setSelectedUnit(id);
|
||||
this._showUnit(unit);
|
||||
};
|
||||
this.map.onCitySelected = (id) => {
|
||||
const city = (this.snapshot.cities || []).find((c) => c.id === id);
|
||||
if (!city || city.civ !== this.localCiv) return;
|
||||
this.selectedCityId = id;
|
||||
this.selectedUnitId = 0;
|
||||
this.map.setSelectedUnit(0);
|
||||
this._showCity(city);
|
||||
};
|
||||
this.map.onTileRequested = (coords) => this._showTile(coords);
|
||||
this.map.onMoveOrdered = (unitId, coords) => {
|
||||
const unit = (this.snapshot.units || []).find((u) => u.id === unitId);
|
||||
if (!unit || unit.civ !== this.localCiv) return;
|
||||
this.sendOrder({ type: "move", unit: unitId, coords: [coords.x, coords.y] });
|
||||
this.map.predictMove(unit, coords);
|
||||
};
|
||||
}
|
||||
|
||||
_updateTopPanel(state) {
|
||||
const civ = this.civilisations[this.localCiv];
|
||||
if (civ) {
|
||||
$("#civ-name").text(civ.name);
|
||||
const flag = civ.flag || (CIVILISATIONS.find((c) => c.id === civ.id) || {}).flag;
|
||||
if (flag) $("#civ-flag-img").attr("src", `assets/flags/${flag}`);
|
||||
}
|
||||
$("#player-name").text(this.config.playerName || (this.network.players[this.network.localPlayerId] || {}).name || "");
|
||||
$("#admin-label").toggle(this.network.isAdmin);
|
||||
$("#stat-date").text(dateString(state.totalHours));
|
||||
const stats = state.viewerStats || {};
|
||||
$("#stat-pop").text(compact(stats.population || 0));
|
||||
$("#stat-gdp").text(`€${compact(stats.gdp || 0)}`);
|
||||
$("#stat-gdppc").text(`€${compact(stats.gdpPerCapita || 0)}`);
|
||||
$("#stat-budget").text(`€${compact(stats.budget || 0)}`);
|
||||
}
|
||||
|
||||
_updateBottomPanel() {
|
||||
if (this.selectedUnitId) {
|
||||
const unit = this._unit(this.selectedUnitId);
|
||||
if (unit) this._showUnit(unit);
|
||||
} else if (this.selectedCityId) {
|
||||
const city = this._city(this.selectedCityId);
|
||||
if (city) this._showCity(city);
|
||||
}
|
||||
}
|
||||
|
||||
_showUnit(unit) {
|
||||
if (!unit) {
|
||||
$("#unit-info").addClass("hidden");
|
||||
this._refreshBottomVisibility();
|
||||
return;
|
||||
}
|
||||
const proto = this.protoUnits[unit.proto];
|
||||
$("#unit-name").text(proto ? proto.name : "Unit");
|
||||
const ratio = unit.maxHp > 0 ? (unit.hp / unit.maxHp) * 100 : 0;
|
||||
$("#unit-health-fill").css("width", `${Math.max(0, Math.min(100, ratio))}%`);
|
||||
$("#tile-info, #city-info").addClass("hidden");
|
||||
$("#unit-info").removeClass("hidden");
|
||||
this._refreshBottomVisibility();
|
||||
}
|
||||
|
||||
_showCity(city) {
|
||||
if (!city) {
|
||||
$("#city-info").addClass("hidden");
|
||||
this._refreshBottomVisibility();
|
||||
return;
|
||||
}
|
||||
$("#city-name").text(city.name);
|
||||
$("#city-population").text(`Population: ${groupDigits(city.population)}`);
|
||||
$("#city-improvements").text(`Improvements: ${fmtImprovements(city.improvements)}`);
|
||||
$("#tile-info, #unit-info").addClass("hidden");
|
||||
$("#city-info").removeClass("hidden");
|
||||
this._refreshBottomVisibility();
|
||||
}
|
||||
|
||||
_showTile(coords) {
|
||||
const k = `${coords.x},${coords.y}`;
|
||||
const owner = this._territoryOwner.has(k) ? this._territoryOwner.get(k) : -1;
|
||||
const civ = this.civilisations[owner];
|
||||
const tile = this.map.getTile(coords);
|
||||
const city = (this.snapshot.cities || []).find(
|
||||
(c) => c.coords[0] === coords.x && c.coords[1] === coords.y
|
||||
);
|
||||
$("#tile-coords").text(`Tile (${coords.x}, ${coords.y})`);
|
||||
$("#tile-owner").text(`Owner: ${civ ? civ.name : "Unclaimed"}`);
|
||||
$("#tile-terrain").text(`Terrain: ${tile ? tile.terrainType : "Unknown"}`);
|
||||
$("#tile-population").text(`Population: ${groupDigits(this._population.get(k) || 0)}`);
|
||||
$("#tile-improvements").text(`Improvements: ${fmtImprovements(city ? city.improvements : [])}`);
|
||||
$("#city-info, #unit-info").addClass("hidden");
|
||||
$("#tile-info").removeClass("hidden");
|
||||
this._refreshBottomVisibility();
|
||||
}
|
||||
|
||||
_refreshBottomVisibility() {
|
||||
const visible =
|
||||
!$("#tile-info").hasClass("hidden") ||
|
||||
!$("#city-info").hasClass("hidden") ||
|
||||
!$("#unit-info").hasClass("hidden");
|
||||
$("#bottom-panel").toggleClass("hidden", !visible);
|
||||
}
|
||||
|
||||
_unit(id) {
|
||||
return (this.snapshot.units || []).find((u) => u.id === id) || null;
|
||||
}
|
||||
|
||||
_city(id) {
|
||||
return (this.snapshot.cities || []).find((c) => c.id === id) || null;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- modals ---
|
||||
|
||||
_openTrainModal() {
|
||||
const city = this._city(this.selectedCityId);
|
||||
if (!city) return;
|
||||
const trainable = TRAINABLE_UNIT_IDS.map((id) => {
|
||||
const index = this.protoUnits.findIndex((p) => p.id === id);
|
||||
return index >= 0 ? { ...this.protoUnits[index], serverIndex: index } : null;
|
||||
}).filter(Boolean);
|
||||
this.buildingsModal.close();
|
||||
this.trainModal.show(
|
||||
{ id: city.id, name: city.name },
|
||||
(this.snapshot.viewerStats || {}).gdp || 0,
|
||||
trainable
|
||||
);
|
||||
this._updateTrainModal();
|
||||
}
|
||||
|
||||
_updateTrainModal() {
|
||||
if (!this.snapshot || !this.selectedCityId) return;
|
||||
if (!this.trainModal.isShowing(this.selectedCityId)) {
|
||||
this._trainingCityId = 0;
|
||||
return;
|
||||
}
|
||||
const entry = (this.snapshot.training || {})[this.selectedCityId];
|
||||
if (!entry) {
|
||||
if (this._trainingCityId === this.selectedCityId) {
|
||||
this.trainModal.endTraining();
|
||||
this._trainingCityId = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const proto = this.protoUnits[entry.protoIndex];
|
||||
const name = proto ? proto.name : "unit";
|
||||
if (this._trainingCityId !== this.selectedCityId) {
|
||||
this._trainingCityId = this.selectedCityId;
|
||||
this.trainModal.beginTraining(name, entry.totalHours, entry.elapsedHours);
|
||||
} else {
|
||||
this.trainModal.setTrainingProgress(name, entry.elapsedHours, entry.totalHours);
|
||||
}
|
||||
}
|
||||
|
||||
_openBuildingsModal() {
|
||||
const city = this._city(this.selectedCityId);
|
||||
if (!city) return;
|
||||
this.trainModal.close();
|
||||
this.buildingsModal.show(
|
||||
{ id: city.id, name: city.name },
|
||||
(this.snapshot.viewerStats || {}).gdp || 0,
|
||||
(this.snapshot.viewerStats || {}).budget || 0,
|
||||
BUILDINGS,
|
||||
this._cityBuildings.get(city.id) || {}
|
||||
);
|
||||
}
|
||||
|
||||
_updateBuildingsModal() {
|
||||
if (!this.snapshot || !this.buildingsModal.isShowing(this.selectedCityId)) return;
|
||||
const stats = this.snapshot.viewerStats || {};
|
||||
this.buildingsModal.refresh(
|
||||
stats.gdp || 0,
|
||||
stats.budget || 0,
|
||||
this._cityBuildings.get(this.selectedCityId) || {}
|
||||
);
|
||||
}
|
||||
|
||||
_openNationModal() {
|
||||
this.nationModal.show(
|
||||
GOVERNMENTS,
|
||||
(this.snapshot.viewerStats || {}).government || 0,
|
||||
TECHNOLOGIES,
|
||||
this._researched,
|
||||
(this.snapshot.viewerStats || {}).gdp || 0,
|
||||
(this.snapshot.viewerStats || {}).budget || 0,
|
||||
(this.snapshot.viewerStats || {}).research || 0
|
||||
);
|
||||
}
|
||||
|
||||
_updateNationModal() {
|
||||
if (!this.snapshot || !this.nationModal.isShowing()) return;
|
||||
const stats = this.snapshot.viewerStats || {};
|
||||
this.nationModal.refresh(
|
||||
stats.government || 0,
|
||||
this._researched,
|
||||
stats.gdp || 0,
|
||||
stats.budget || 0,
|
||||
stats.research || 0
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- input ---
|
||||
|
||||
_onKey(event) {
|
||||
if (!this._entered) return;
|
||||
const step = event.shiftKey ? 120 : 40;
|
||||
switch (event.key) {
|
||||
case "w": case "ArrowUp": this.map.panBy(0, step); break;
|
||||
case "s": case "ArrowDown": this.map.panBy(0, -step); break;
|
||||
case "a": case "ArrowLeft": this.map.panBy(step, 0); break;
|
||||
case "d": case "ArrowRight": this.map.panBy(-step, 0); break;
|
||||
case "+": case "=":
|
||||
this.map.camera.zoom = Math.min(2, this.map.camera.zoom * 1.1);
|
||||
this.map._applyCamera();
|
||||
break;
|
||||
case "-":
|
||||
this.map.camera.zoom = Math.max(0.2, this.map.camera.zoom / 1.1);
|
||||
this.map._applyCamera();
|
||||
break;
|
||||
case "Escape":
|
||||
this.trainModal.close();
|
||||
this.buildingsModal.close();
|
||||
this.nationModal.close();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- frame ---
|
||||
|
||||
_frame(now) {
|
||||
if (!this._entered) return;
|
||||
const dt = Math.min(0.1, (now - this._lastFrame) / 1000);
|
||||
this._lastFrame = now;
|
||||
this.map.animate(dt);
|
||||
this._fpsFrames += 1;
|
||||
this._fpsElapsed += dt;
|
||||
if (this._fpsElapsed >= 0.25) {
|
||||
const fps = Math.round(this._fpsFrames / this._fpsElapsed);
|
||||
this._fpsElapsed = 0;
|
||||
this._fpsFrames = 0;
|
||||
const $label = $("#fps-label").text(`${fps} FPS`);
|
||||
$label.css("color", fps < 30 ? "#f24" : fps < 50 ? "#fa4" : "#8fdc8f");
|
||||
}
|
||||
requestAnimationFrame((t) => this._frame(t));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
// The three modal panels of the game HUD: city training, city buildings, and
|
||||
// the nation panel (government + technology). They only display state supplied
|
||||
// by the game screen and forward the player's choices as orders.
|
||||
|
||||
import { groupDigits, hours } from "../../shared/text_format.js";
|
||||
import { ECONOMY } from "../../shared/data.js";
|
||||
import {
|
||||
buildingBuildCost,
|
||||
buildingRefund,
|
||||
buildingUpkeep,
|
||||
governmentChangeCost,
|
||||
prerequisitesMet,
|
||||
} from "../../shared/rules.js";
|
||||
|
||||
function formatEffect(effect, level) {
|
||||
const value = level <= 0 ? 0 : effect.baseValue * Math.pow(level, effect.exponent);
|
||||
switch (effect.stat) {
|
||||
case "gdp":
|
||||
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% GDP`;
|
||||
case "budget_income":
|
||||
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% budget income`;
|
||||
case "research":
|
||||
return `${value >= 0 ? "+" : ""}${value.toFixed(2)} research/h`;
|
||||
case "culture":
|
||||
return `${value >= 0 ? "+" : ""}${value.toFixed(2)} culture/h`;
|
||||
default:
|
||||
return `${effect.stat} ${value.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function effectsText(effects, level) {
|
||||
const parts = effects.map((e) => formatEffect(e, level));
|
||||
return parts.length > 0 ? parts.join(" · ") : "No effect";
|
||||
}
|
||||
|
||||
export class TrainModal {
|
||||
constructor() {
|
||||
this.$modal = $("#modal-train");
|
||||
this.$title = $("#train-title");
|
||||
this.$list = $("#train-list");
|
||||
this.$status = $("#train-status");
|
||||
this.$label = $("#train-label");
|
||||
this.$progress = $("#train-progress");
|
||||
this.cityId = 0;
|
||||
this.gdp = 0;
|
||||
this.onTrain = () => {};
|
||||
this.$modal.find(".modal-close").on("click", () => this.close());
|
||||
}
|
||||
|
||||
isShowing(cityId) {
|
||||
return !this.$modal.hasClass("hidden") && this.cityId === cityId;
|
||||
}
|
||||
|
||||
show(city, gdp, trainableUnits) {
|
||||
this.cityId = city.id;
|
||||
this.gdp = gdp;
|
||||
this.$title.text(`${city.name} — Train`);
|
||||
this.$list.empty();
|
||||
this.$status.addClass("hidden");
|
||||
for (let index = 0; index < trainableUnits.length; index++) {
|
||||
const proto = trainableUnits[index];
|
||||
if (!proto.cost && proto.cost !== 0) continue;
|
||||
const button = $("<button class='aero'></button>");
|
||||
const duration = ECONOMY.trainingHours(gdp, proto.cost);
|
||||
button.text(`${proto.name} — Cost ${groupDigits(proto.cost)} — ${hours(duration)}`);
|
||||
const serverIndex = proto.serverIndex !== undefined ? proto.serverIndex : index;
|
||||
button.on("click", () => this.onTrain(city.id, serverIndex));
|
||||
this.$list.append(button);
|
||||
}
|
||||
this.$modal.removeClass("hidden");
|
||||
}
|
||||
|
||||
beginTraining(name, totalHours, elapsedHours = 0) {
|
||||
this.$label.text(`Training ${name}…`);
|
||||
this.$progress.css("width", `${Math.min(100, (elapsedHours / Math.max(totalHours, 0.0001)) * 100)}%`);
|
||||
this.$status.removeClass("hidden");
|
||||
this.$list.find("button").prop("disabled", true);
|
||||
}
|
||||
|
||||
setTrainingProgress(name, elapsedHours, totalHours) {
|
||||
const remaining = Math.max(0, Math.ceil(totalHours - elapsedHours));
|
||||
this.$label.text(`Training ${name}… (${remaining} h left)`);
|
||||
this.$progress.css("width", `${Math.min(100, (elapsedHours / Math.max(totalHours, 0.0001)) * 100)}%`);
|
||||
}
|
||||
|
||||
endTraining() {
|
||||
this.$label.text("Training complete");
|
||||
this.$progress.css("width", "100%");
|
||||
this.$list.find("button").prop("disabled", false);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$modal.addClass("hidden");
|
||||
this.cityId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class BuildingsModal {
|
||||
constructor() {
|
||||
this.$modal = $("#modal-buildings");
|
||||
this.$title = $("#buildings-title");
|
||||
this.$budget = $("#buildings-budget");
|
||||
this.$list = $("#buildings-list");
|
||||
this.cityId = 0;
|
||||
this.cityName = "";
|
||||
this.buildings = [];
|
||||
this.rows = [];
|
||||
this.onBuild = () => {};
|
||||
this.onDemolish = () => {};
|
||||
this.$modal.find(".modal-close").on("click", () => this.close());
|
||||
}
|
||||
|
||||
isShowing(cityId) {
|
||||
return !this.$modal.hasClass("hidden") && this.cityId === cityId;
|
||||
}
|
||||
|
||||
show(city, gdp, budget, buildings, levels) {
|
||||
this.cityId = city.id;
|
||||
this.cityName = city.name;
|
||||
this.buildings = buildings;
|
||||
this.$title.text(`${city.name} — Buildings`);
|
||||
this.$list.empty();
|
||||
this.rows = buildings.map((proto, index) => this._makeRow(proto, index));
|
||||
this.refresh(gdp, budget, levels);
|
||||
this.$modal.removeClass("hidden");
|
||||
}
|
||||
|
||||
_makeRow(proto, index) {
|
||||
const $card = $("<div class='row-card'></div>");
|
||||
const $info = $("<div class='info'></div>");
|
||||
const $name = $("<div class='name'></div>");
|
||||
const $desc = $("<div class='desc'></div>").text(proto.description);
|
||||
const $effects = $("<div class='effects'></div>");
|
||||
const $cost = $("<div class='cost'></div>");
|
||||
$info.append($name, $desc, $effects, $cost);
|
||||
const $actions = $("<div class='actions'></div>");
|
||||
const $upgrade = $("<button class='aero'></button>");
|
||||
const $downgrade = $("<button class='aero'></button>");
|
||||
$upgrade.on("click", () => this.onBuild(this.cityId, index));
|
||||
$downgrade.on("click", () => this.onDemolish(this.cityId, index));
|
||||
$actions.append($upgrade, $downgrade);
|
||||
$card.append($info, $actions);
|
||||
this.$list.append($card);
|
||||
return { proto, index, $name, $effects, $cost, $upgrade, $downgrade };
|
||||
}
|
||||
|
||||
refresh(gdp, budget, levels) {
|
||||
this.$budget.text(`Budget: €${groupDigits(Math.round(budget))}`);
|
||||
for (const row of this.rows) {
|
||||
const level = levels[row.index] || 0;
|
||||
row.$name.text(`${row.proto.name} — Level ${level} / ${row.proto.maxLevel}`);
|
||||
row.$effects.text(effectsText(row.proto.effects, level > 0 ? level : 1));
|
||||
const parts = [];
|
||||
if (level > 0) parts.push(`Upkeep €${groupDigits(Math.round(buildingUpkeep(row.proto, gdp, level)))}/h`);
|
||||
if (level < row.proto.maxLevel) {
|
||||
parts.push(`Build cost €${groupDigits(Math.round(buildingBuildCost(row.proto, gdp, level)))}`);
|
||||
}
|
||||
row.$cost.text(parts.join(" · "));
|
||||
if (level < row.proto.maxLevel) {
|
||||
const cost = buildingBuildCost(row.proto, gdp, level);
|
||||
row.$upgrade.text(`Upgrade €${groupDigits(Math.round(cost))}`).prop("disabled", budget < cost);
|
||||
} else {
|
||||
row.$upgrade.text("Max level").prop("disabled", true);
|
||||
}
|
||||
if (level > 0) {
|
||||
row.$downgrade
|
||||
.text(`Downgrade +€${groupDigits(Math.round(buildingRefund(row.proto, gdp, level)))}`)
|
||||
.prop("disabled", false);
|
||||
} else {
|
||||
row.$downgrade.text("Not built").prop("disabled", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$modal.addClass("hidden");
|
||||
this.cityId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class NationModal {
|
||||
constructor() {
|
||||
this.$modal = $("#modal-nation");
|
||||
this.$governmentSub = $("#government-sub");
|
||||
this.$governmentList = $("#government-list");
|
||||
this.$technologySub = $("#technology-sub");
|
||||
this.$technologyList = $("#technology-list");
|
||||
this.governments = [];
|
||||
this.technologies = [];
|
||||
this.currentGovernment = 0;
|
||||
this.researched = new Set();
|
||||
this.governmentRows = [];
|
||||
this.technologyRows = [];
|
||||
this.onGovernment = () => {};
|
||||
this.onTechnology = () => {};
|
||||
this.$modal.find(".modal-close").on("click", () => this.close());
|
||||
this.$modal.find(".tab").on("click", (event) => {
|
||||
const tab = $(event.currentTarget).data("tab");
|
||||
this.$modal.find(".tab").removeClass("active");
|
||||
$(event.currentTarget).addClass("active");
|
||||
this.$modal.find(".tab-page").removeClass("active");
|
||||
$(`#tab-${tab}`).addClass("active");
|
||||
});
|
||||
}
|
||||
|
||||
isShowing() {
|
||||
return !this.$modal.hasClass("hidden");
|
||||
}
|
||||
|
||||
show(governments, currentGovernment, technologies, researched, gdp, budget, research) {
|
||||
this.governments = governments;
|
||||
this.technologies = technologies;
|
||||
this.governmentRows = governments.map((proto, index) => this._makeRow(this.$governmentList, proto, index, "government"));
|
||||
this.technologyRows = technologies.map((proto, index) => this._makeRow(this.$technologyList, proto, index, "technology"));
|
||||
this.refresh(currentGovernment, researched, gdp, budget, research);
|
||||
this.$modal.removeClass("hidden");
|
||||
}
|
||||
|
||||
_makeRow($list, proto, index, kind) {
|
||||
const $card = $("<div class='row-card'></div>");
|
||||
const $info = $("<div class='info'></div>");
|
||||
const $name = $("<div class='name'></div>");
|
||||
const $desc = $("<div class='desc'></div>").text(proto.description || "");
|
||||
const $effects = $("<div class='effects'></div>");
|
||||
const $cost = $("<div class='cost'></div>");
|
||||
$info.append($name, $desc, $effects, $cost);
|
||||
const $actions = $("<div class='actions'></div>");
|
||||
const $action = $("<button class='aero'></button>");
|
||||
$action.on("click", () => {
|
||||
if (kind === "government") this.onGovernment(index);
|
||||
else this.onTechnology(index);
|
||||
});
|
||||
$actions.append($action);
|
||||
$card.append($info, $actions);
|
||||
$list.append($card);
|
||||
return { proto, index, kind, $name, $effects, $cost, $action };
|
||||
}
|
||||
|
||||
refresh(currentGovernment, researched, gdp, budget, research) {
|
||||
this.currentGovernment = currentGovernment;
|
||||
this.researched = researched instanceof Set ? researched : new Set(Object.keys(researched).map(Number));
|
||||
this.$governmentSub.text(
|
||||
`Government — ${this.governments[currentGovernment] ? this.governments[currentGovernment].name : "None"} · Budget: €${groupDigits(Math.round(budget))}`
|
||||
);
|
||||
this.$technologySub.text(`Research points: ${groupDigits(Math.round(research))}`);
|
||||
|
||||
for (const row of this.governmentRows) {
|
||||
const proto = row.proto;
|
||||
row.$name.text(proto.name);
|
||||
row.$effects.text(effectsText(proto.effects, 1));
|
||||
if (row.index === currentGovernment) {
|
||||
row.$cost.text("Currently in power");
|
||||
row.$action.text("Current government").prop("disabled", true);
|
||||
} else {
|
||||
const cost = governmentChangeCost(proto, gdp);
|
||||
row.$cost.text(`Adoption cost €${groupDigits(Math.round(cost))}`);
|
||||
row.$action.text(`Adopt €${groupDigits(Math.round(cost))}`).prop("disabled", budget < cost);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of this.technologyRows) {
|
||||
const proto = row.proto;
|
||||
row.$name.text(proto.name);
|
||||
row.$effects.text(effectsText(proto.effects, 1));
|
||||
if (this.researched.has(row.index)) {
|
||||
row.$cost.text("Researched");
|
||||
row.$action.text("Researched").prop("disabled", true);
|
||||
} else if (!prerequisitesMet(proto, this.researched, this.technologies)) {
|
||||
const names = proto.prerequisites
|
||||
.map((id) => {
|
||||
const found = this.technologies.find((t) => t.id === id);
|
||||
return found ? found.name : id;
|
||||
})
|
||||
.join(", ");
|
||||
row.$cost.text(`Requires: ${names}`);
|
||||
row.$action.text("Locked").prop("disabled", true);
|
||||
} else {
|
||||
row.$cost.text(`Cost ${groupDigits(Math.round(proto.cost))} research points`);
|
||||
row.$action.text(`Research ${groupDigits(Math.round(proto.cost))}`).prop("disabled", research < proto.cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$modal.addClass("hidden");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Browser network role: talks to the authoritative Node server over a WebSocket
|
||||
// and exposes high-level events so the screens do not care about the transport.
|
||||
|
||||
export const DEFAULT_PORT = 27015;
|
||||
export const DEFAULT_PATH = "/ws";
|
||||
|
||||
export class Network {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.isAdmin = false;
|
||||
this.localPlayerId = 0;
|
||||
this.players = {};
|
||||
this._handlers = {};
|
||||
this._loginFailure = "";
|
||||
this._connectResolve = null;
|
||||
this._connectReject = null;
|
||||
}
|
||||
|
||||
on(event, handler) {
|
||||
if (!this._handlers[event]) this._handlers[event] = [];
|
||||
this._handlers[event].push(handler);
|
||||
}
|
||||
|
||||
_emit(event, ...args) {
|
||||
for (const handler of this._handlers[event] || []) handler(...args);
|
||||
}
|
||||
|
||||
isClientConnected() {
|
||||
return this.localPlayerId !== 0;
|
||||
}
|
||||
|
||||
defaultServerUrl() {
|
||||
const location = window.location;
|
||||
if (location.protocol === "http:" || location.protocol === "https:") {
|
||||
const scheme = location.protocol === "https:" ? "wss" : "ws";
|
||||
return `${scheme}://${location.host}${DEFAULT_PATH}`;
|
||||
}
|
||||
return `ws://127.0.0.1:${DEFAULT_PORT}${DEFAULT_PATH}`;
|
||||
}
|
||||
|
||||
// Accepts a bare hostname or a full ws(s):// or http(s):// URL.
|
||||
normalizeUrl(host, port = DEFAULT_PORT) {
|
||||
let url = String(host || "").trim().replace(/\/+$/, "");
|
||||
if (url.startsWith("https://")) url = "wss://" + url.slice("https://".length);
|
||||
else if (url.startsWith("http://")) url = "ws://" + url.slice("http://".length);
|
||||
else if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
|
||||
url = `ws://${url}:${port}`;
|
||||
}
|
||||
const afterScheme = url.slice(url.indexOf("://") + 3);
|
||||
if (!afterScheme.includes("/")) url += DEFAULT_PATH;
|
||||
return url;
|
||||
}
|
||||
|
||||
connect(url) {
|
||||
this.close();
|
||||
this._loginFailure = "";
|
||||
this.localPlayerId = 0;
|
||||
this.isAdmin = false;
|
||||
return new Promise((resolve, reject) => {
|
||||
let socket;
|
||||
try {
|
||||
socket = new WebSocket(url);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
this.socket = socket;
|
||||
this._connectResolve = resolve;
|
||||
this._connectReject = reject;
|
||||
socket.onopen = () => {
|
||||
if (this._connectResolve) {
|
||||
this._connectResolve();
|
||||
this._connectResolve = null;
|
||||
this._connectReject = null;
|
||||
}
|
||||
};
|
||||
socket.onerror = () => {
|
||||
if (this._connectReject) {
|
||||
this._connectReject(new Error("connection failed"));
|
||||
this._connectResolve = null;
|
||||
this._connectReject = null;
|
||||
}
|
||||
};
|
||||
socket.onclose = () => {
|
||||
const failure = this._loginFailure;
|
||||
this._loginFailure = "";
|
||||
this.localPlayerId = 0;
|
||||
this.isAdmin = false;
|
||||
if (this._connectReject) {
|
||||
this._connectReject(new Error("connection closed"));
|
||||
this._connectResolve = null;
|
||||
this._connectReject = null;
|
||||
return;
|
||||
}
|
||||
if (failure) this._emit("session_failed", failure);
|
||||
else this._emit("server_disconnected");
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
this._handleMessage(message);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
requestGameList(url) {
|
||||
return this.connect(url).then(() => {
|
||||
this._send({ t: "request_games" });
|
||||
});
|
||||
}
|
||||
|
||||
createGame(name, password) {
|
||||
return this.connect(this.defaultServerUrl()).then(() => {
|
||||
this._send({ t: "login", name, password });
|
||||
});
|
||||
}
|
||||
|
||||
joinGame(url, name, password) {
|
||||
return this.connect(url).then(() => {
|
||||
this._send({ t: "login", name, password });
|
||||
});
|
||||
}
|
||||
|
||||
sendGameSetup(setup) {
|
||||
this._send({ t: "configure_game", setup });
|
||||
}
|
||||
|
||||
sendOrder(order) {
|
||||
this._send({ t: "order", order });
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.socket) return;
|
||||
const socket = this.socket;
|
||||
this.socket = null;
|
||||
socket.onopen = null;
|
||||
socket.onerror = null;
|
||||
socket.onclose = null;
|
||||
socket.onmessage = null;
|
||||
try {
|
||||
socket.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
_send(message) {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
_handleMessage(message) {
|
||||
switch (message.t) {
|
||||
case "assigned":
|
||||
this.localPlayerId = message.playerId;
|
||||
this.isAdmin = !!message.admin;
|
||||
this._emit("session_started", message);
|
||||
break;
|
||||
case "players":
|
||||
this.players = message.players || {};
|
||||
this._emit("players_updated", this.players);
|
||||
break;
|
||||
case "game_list":
|
||||
this._emit("game_list_received", message.games || []);
|
||||
break;
|
||||
case "login_rejected":
|
||||
this._loginFailure = message.reason || "Login failed.";
|
||||
this._emit("session_failed", this._loginFailure);
|
||||
this.close();
|
||||
break;
|
||||
case "game_configured":
|
||||
this._emit("game_configured");
|
||||
break;
|
||||
case "game_state":
|
||||
this._emit("game_state_received", message.state);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
class_name BuildingEffect
|
||||
|
||||
extends Resource
|
||||
|
||||
## One stat a building improves, and how it scales with the building's level.
|
||||
## A building may carry several of these. The value contributed at a given
|
||||
## level is base_value * level^exponent: exponent 1.0 is linear, below 1.0 is
|
||||
## sublinear (diminishing returns), above 1.0 is superlinear.
|
||||
|
||||
## The stat this effect feeds. See the EFFECT_* constants on ProtoBuilding.
|
||||
@export var stat : String = ""
|
||||
|
||||
## Value contributed at level 1.
|
||||
@export var base_value : float = 0.0
|
||||
|
||||
## Level exponent. 1.0 = linear, 0.5 = square-root (sublinear).
|
||||
@export_range(0.0, 2.0) var exponent : float = 1.0
|
||||
|
||||
|
||||
func value_at(level: int) -> float:
|
||||
if level <= 0:
|
||||
return 0.0
|
||||
return base_value * pow(float(level), exponent)
|
||||
@@ -1 +0,0 @@
|
||||
uid://c8lu7oofc64y
|
||||
@@ -1,20 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="ProtoBuilding" format=3 uid="uid://djatijtfq1pgd"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0u6ish14ncb2" path="res://data/proto_building.gd" id="1_pb"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_bank_income"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "budget_income"
|
||||
base_value = 0.05
|
||||
exponent = 0.7
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pb")
|
||||
name = "Bank"
|
||||
description = "Channels private wealth into the state coffers. Raises budget income."
|
||||
cost_gdp_fraction = 0.02
|
||||
cost_alpha = 1.7
|
||||
upkeep_gdp_fraction = 4e-05
|
||||
upkeep_alpha = 1.6
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_bank_income")])
|
||||
@@ -1,20 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="ProtoBuilding" format=3 uid="uid://d35v8a2dbpn6g"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0u6ish14ncb2" path="res://data/proto_building.gd" id="1_pb"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_cinema_culture"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "culture"
|
||||
base_value = 3.0
|
||||
exponent = 0.7
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pb")
|
||||
name = "Cinema studio"
|
||||
description = "Mass media spreads the nation's culture at home and abroad."
|
||||
cost_gdp_fraction = 0.03
|
||||
cost_alpha = 1.75
|
||||
upkeep_gdp_fraction = 0.0001
|
||||
upkeep_alpha = 1.65
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_cinema_culture")])
|
||||
@@ -1,19 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="ProtoBuilding" format=3 uid="uid://cjlcklx5gpj1u"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0u6ish14ncb2" path="res://data/proto_building.gd" id="1_pb"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_market_gdp"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "gdp"
|
||||
base_value = 0.04
|
||||
exponent = 0.75
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pb")
|
||||
name = "Market"
|
||||
description = "A hub of local trade. Raises national GDP, with diminishing returns."
|
||||
cost_gdp_fraction = 0.01
|
||||
cost_alpha = 1.6
|
||||
upkeep_gdp_fraction = 3e-05
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_market_gdp")])
|
||||
@@ -1,20 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="ProtoBuilding" format=3 uid="uid://dxwffj357knng"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0u6ish14ncb2" path="res://data/proto_building.gd" id="1_pb"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_museum_culture"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "culture"
|
||||
base_value = 1.5
|
||||
exponent = 0.85
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pb")
|
||||
name = "Museum"
|
||||
description = "Preserves national heritage, strengthening culture against foreign ideas."
|
||||
cost_gdp_fraction = 0.02
|
||||
cost_alpha = 1.6
|
||||
upkeep_gdp_fraction = 5e-05
|
||||
upkeep_alpha = 1.55
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_museum_culture")])
|
||||
@@ -1,20 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="ProtoBuilding" format=3 uid="uid://cnm334g5hdj2b"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0u6ish14ncb2" path="res://data/proto_building.gd" id="1_pb"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_lab_research"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "research"
|
||||
base_value = 1.0
|
||||
exponent = 0.9
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pb")
|
||||
name = "Research lab"
|
||||
description = "Applies science to industry and government. Produces research points."
|
||||
cost_gdp_fraction = 0.015
|
||||
cost_alpha = 1.65
|
||||
upkeep_gdp_fraction = 5e-05
|
||||
upkeep_alpha = 1.55
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_lab_research")])
|
||||
@@ -1,26 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="ProtoBuilding" format=3 uid="uid://bc4wqs1rr5707"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0u6ish14ncb2" path="res://data/proto_building.gd" id="1_pb"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_university_research"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "research"
|
||||
base_value = 2.0
|
||||
exponent = 0.8
|
||||
|
||||
[sub_resource type="Resource" id="Effect_university_culture"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "culture"
|
||||
base_value = 0.5
|
||||
exponent = 0.7
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pb")
|
||||
name = "University"
|
||||
description = "Higher education produces research and a little culture."
|
||||
cost_gdp_fraction = 0.025
|
||||
cost_alpha = 1.7
|
||||
upkeep_gdp_fraction = 8e-05
|
||||
upkeep_alpha = 1.6
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_university_research"), SubResource("Effect_university_culture")])
|
||||
@@ -1,9 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CityDescription" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/city_description.gd" id="1_city"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_city")
|
||||
name = "London"
|
||||
population = 9500000
|
||||
improvements = PackedStringArray("Big Ben", "Tower Bridge", "Buckingham Palace")
|
||||
@@ -1,9 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CityDescription" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/city_description.gd" id="1_city"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_city")
|
||||
name = "Paris"
|
||||
population = 11200000
|
||||
improvements = PackedStringArray("Eiffel Tower", "Louvre", "Notre-Dame")
|
||||
@@ -1,10 +0,0 @@
|
||||
class_name CityDescription
|
||||
|
||||
extends Resource
|
||||
|
||||
## Authored data for a single city. Generated cities build one of these at
|
||||
## runtime; named capitals are stored as .tres files under res://data/cities.
|
||||
|
||||
@export var name : String
|
||||
@export var population : int = 0
|
||||
@export var improvements : PackedStringArray = PackedStringArray()
|
||||
@@ -1 +0,0 @@
|
||||
uid://cor2vawqxhptw
|
||||
@@ -1,14 +0,0 @@
|
||||
class_name CivDescription
|
||||
|
||||
extends Resource
|
||||
|
||||
## Defines a playable civilisation. Instances live as .tres files under
|
||||
## res://data/civilisations and are also the run-time identity of a nation:
|
||||
## territory and units reference the resource directly when comparing owners.
|
||||
|
||||
@export var name : String
|
||||
@export var primary_colour : Color
|
||||
@export var accent_colour : Color
|
||||
@export var flag : Texture2D
|
||||
## Names used for the civilisation's non-capital cities, in founding order.
|
||||
@export var city_names : PackedStringArray = PackedStringArray()
|
||||
@@ -1 +0,0 @@
|
||||
uid://bbrofteu1a30o
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3 uid="uid://xm08b8g1r773"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_a63p8"]
|
||||
[ext_resource type="Texture2D" uid="uid://io753v4cud2c" path="res://gfx/hud/flags/flag_britain.png" id="2_gb01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_a63p8")
|
||||
name = "Britain"
|
||||
primary_colour = Color(0.6580932, 0.09319074, 0.09283676, 1)
|
||||
accent_colour = Color(0.028559247, 0.13517213, 0.9999997, 1)
|
||||
flag = ExtResource("2_gb01")
|
||||
city_names = PackedStringArray("Birmingham", "Manchester", "Glasgow", "Liverpool", "Leeds", "Sheffield", "Edinburgh", "Bristol", "Cardiff")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3 uid="uid://bnlivue5sumkl"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_n7v6n"]
|
||||
[ext_resource type="Texture2D" uid="uid://bsuer7fwcs6py" path="res://gfx/hud/flags/flag_france.png" id="2_fr01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_n7v6n")
|
||||
name = "France"
|
||||
primary_colour = Color(0.050399996, 0.050399996, 0.36, 1)
|
||||
accent_colour = Color(0.55, 0.54615, 0.5115, 1)
|
||||
flag = ExtResource("2_fr01")
|
||||
city_names = PackedStringArray("Lyon", "Marseille", "Toulouse", "Nice", "Nantes", "Montpellier", "Strasbourg", "Bordeaux", "Lille")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_ga01"]
|
||||
[ext_resource type="Texture2D" uid="uid://co0ufd01torfw" path="res://gfx/hud/flags/flag_galicia.png" id="2_ga01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_ga01")
|
||||
name = "Galicia"
|
||||
primary_colour = Color(0, 0.427451, 0.678431, 1)
|
||||
accent_colour = Color(1, 1, 1, 1)
|
||||
flag = ExtResource("2_ga01")
|
||||
city_names = PackedStringArray("Santiago de Compostela", "Vigo", "A Coruña", "Ourense", "Lugo", "Pontevedra", "Ferrol", "Vilagarcía de Arousa", "Ribeira", "Redondela")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_de01"]
|
||||
[ext_resource type="Texture2D" uid="uid://bga6kymgrsbra" path="res://gfx/hud/flags/flag_germany.png" id="2_de01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_de01")
|
||||
name = "Germany"
|
||||
primary_colour = Color(0.08, 0.08, 0.08, 1)
|
||||
accent_colour = Color(1, 0.807843, 0, 1)
|
||||
flag = ExtResource("2_de01")
|
||||
city_names = PackedStringArray("Berlin", "Hamburg", "Munich", "Cologne", "Frankfurt", "Stuttgart", "Düsseldorf", "Leipzig", "Dortmund", "Essen")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_ir01"]
|
||||
[ext_resource type="Texture2D" uid="uid://biyhusoa6fuq" path="res://gfx/hud/flags/flag_iran.png" id="2_ir01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_ir01")
|
||||
name = "Iran"
|
||||
primary_colour = Color(0.094118, 0.65098, 0.270588, 1)
|
||||
accent_colour = Color(0.941176, 0.109804, 0.129412, 1)
|
||||
flag = ExtResource("2_ir01")
|
||||
city_names = PackedStringArray("Tehran", "Mashhad", "Isfahan", "Karaj", "Shiraz", "Tabriz", "Qom", "Ahvaz", "Kermanshah", "Urmia")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_pl01"]
|
||||
[ext_resource type="Texture2D" uid="uid://d0dubcdyyciw6" path="res://gfx/hud/flags/flag_poland.png" id="2_pl01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pl01")
|
||||
name = "Poland"
|
||||
primary_colour = Color(0.65098, 0, 0.043137, 1)
|
||||
accent_colour = Color(1, 1, 1, 1)
|
||||
flag = ExtResource("2_pl01")
|
||||
city_names = PackedStringArray("Warsaw", "Kraków", "Łódź", "Wrocław", "Poznań", "Gdańsk", "Szczecin", "Bydgoszcz", "Lublin", "Katowice")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3 uid="uid://c8sloven1axv"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_slv01"]
|
||||
[ext_resource type="Texture2D" uid="uid://calm4e82evox7" path="res://gfx/hud/flags/flag_slovenia.png" id="2_slv01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_slv01")
|
||||
name = "Slovenia"
|
||||
primary_colour = Color(0, 0.364706, 0.643137, 1)
|
||||
accent_colour = Color(0.929412, 0.109804, 0.141176, 1)
|
||||
flag = ExtResource("2_slv01")
|
||||
city_names = PackedStringArray("Maribor", "Celje", "Kranj", "Koper", "Velenje", "Novo Mesto", "Ptuj", "Kamnik", "Jesenice")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_ch01"]
|
||||
[ext_resource type="Texture2D" uid="uid://chcpw711rmj15" path="res://gfx/hud/flags/flag_switzerland.png" id="2_ch01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_ch01")
|
||||
name = "Switzerland"
|
||||
primary_colour = Color(0.854902, 0.160784, 0.109804, 1)
|
||||
accent_colour = Color(1, 1, 1, 1)
|
||||
flag = ExtResource("2_ch01")
|
||||
city_names = PackedStringArray("Bern", "Zürich", "Geneva", "Basel", "Lausanne", "Lucerne", "Winterthur", "St. Gallen", "Lugano", "Biel")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,13 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="CivDescription" format=3]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbrofteu1a30o" path="res://data/civ_description.gd" id="1_ti01"]
|
||||
[ext_resource type="Texture2D" uid="uid://coh6nios70sku" path="res://gfx/hud/flags/flag_ticino.png" id="2_ti01"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_ti01")
|
||||
name = "Ticino"
|
||||
primary_colour = Color(0.27451, 0.27451, 0.878431, 1)
|
||||
accent_colour = Color(0.65098, 0, 0.043137, 1)
|
||||
flag = ExtResource("2_ti01")
|
||||
city_names = PackedStringArray("Bellinzona", "Lugano", "Locarno", "Mendrisio", "Chiasso", "Airolo", "Biasca", "Ascona", "Faido", "Giubiasco")
|
||||
metadata/_custom_type_script = "uid://bbrofteu1a30o"
|
||||
@@ -1,7 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="EconomyConfig" format=3 uid="uid://rb6vrlut8rdm"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bm2pvegtcvgww" path="res://data/economy_config.gd" id="1_econ"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_econ")
|
||||
base_gdp_per_capita = 10000
|
||||
@@ -1,69 +0,0 @@
|
||||
class_name EconomyConfig
|
||||
|
||||
extends Resource
|
||||
|
||||
## Tunable economic figures. The live values live in res://data/economy.tres
|
||||
## and can be edited from the Godot inspector without touching any script.
|
||||
|
||||
## GDP produced per person on an ordinary owned tile.
|
||||
@export var base_gdp_per_capita : int = 10000
|
||||
|
||||
## Number of people on an ordinary fully-populated tile. Each terrain type
|
||||
## scales this by its population_multiplier (1.0 = ordinary land).
|
||||
@export var base_population : int = 100000
|
||||
|
||||
## Owned tiles with a city on them produce this multiple of the base GDP per
|
||||
## capita.
|
||||
@export var city_tile_gdp_multiplier : float = 2.0
|
||||
|
||||
## Owned tiles with a capital city on them produce this multiple of the base
|
||||
## GDP per capita instead of city_tile_gdp_multiplier.
|
||||
@export var capital_tile_gdp_multiplier : float = 2.5
|
||||
|
||||
## Owned tiles immediately around one of your cities get this GDP per capita
|
||||
## bonus (0.3 means +30%).
|
||||
@export_range(0.0, 10.0) var city_surrounding_gdp_bonus : float = 0.3
|
||||
|
||||
## How much GDP is worth one production point. A nation's production capacity
|
||||
## (production points per game hour) is its GDP divided by this value, and a
|
||||
## unit's cost is measured in production points.
|
||||
@export var gdp_per_production_point : int = 100000000
|
||||
|
||||
## Fraction of a nation's GDP added to its state budget every game hour
|
||||
## (0.001 means +0.1% of GDP per hour). The economy runs once per in-game day,
|
||||
## so each tick applies this rate over the whole day.
|
||||
@export_range(0.0, 1.0) var budget_gdp_rate : float = 0.001
|
||||
|
||||
## State budget every civilization starts the game with.
|
||||
@export var starting_budget : int = 0
|
||||
|
||||
## Base annual population growth applied to every populated tile (0.005 means
|
||||
## +0.5% per in-game year). The economy runs once per in-game day, so growth is
|
||||
## applied in one-day steps rather than jumping once a year.
|
||||
@export_range(0.0, 1.0) var base_population_growth_rate : float = 0.005
|
||||
|
||||
|
||||
## Growth factor over a step of `hours` in-game hours. Pass hours_per_year and
|
||||
## the length of the step to scale annual growth to that step.
|
||||
func population_growth_factor(hours_per_year: float, hours: float = 1.0) -> float:
|
||||
if base_population_growth_rate <= 0.0 or hours_per_year <= 0.0 or hours <= 0.0:
|
||||
return 0.0
|
||||
return pow(1.0 + base_population_growth_rate, hours / hours_per_year) - 1.0
|
||||
|
||||
|
||||
func production_capacity(gdp: float) -> float:
|
||||
if gdp_per_production_point <= 0:
|
||||
return 0.0
|
||||
return gdp / float(gdp_per_production_point)
|
||||
|
||||
|
||||
func training_hours(gdp: float, cost: int) -> float:
|
||||
var capacity := production_capacity(gdp)
|
||||
if capacity <= 0.0:
|
||||
return 0.0
|
||||
return float(cost) / capacity
|
||||
|
||||
|
||||
## Budget added over a step of `hours` in-game hours at the hourly rate.
|
||||
func budget_income(gdp: float, hours: float = 1.0) -> float:
|
||||
return gdp * budget_gdp_rate * hours
|
||||
@@ -1 +0,0 @@
|
||||
uid://bm2pvegtcvgww
|
||||
@@ -1,38 +0,0 @@
|
||||
class_name Government
|
||||
|
||||
extends Resource
|
||||
|
||||
## A form of government a civilisation can adopt. Concrete instances live under
|
||||
## res://data/governments and are edited in the inspector.
|
||||
##
|
||||
## Unlike buildings, a government has no levels: adopting one replaces the
|
||||
## previous one and applies its effects in full. Switching costs budget, priced
|
||||
## like a building from a flat base plus a fraction of the owner's GDP, so it
|
||||
## stays meaningful as the economy grows.
|
||||
|
||||
@export var name : String
|
||||
@export_multiline var description : String
|
||||
|
||||
## Flat part of the cost to adopt this government, in budget currency.
|
||||
@export var base_cost : float = 0.0
|
||||
## Fraction of the owner's GDP added to the adoption cost.
|
||||
@export var cost_gdp_fraction : float = 0.0
|
||||
|
||||
## Bonuses this government grants while in power. Effects are evaluated at
|
||||
## level 1, the only level a government has. See BuildingEffect for the stats.
|
||||
@export var effects : Array[BuildingEffect] = []
|
||||
|
||||
|
||||
## Budget cost to switch to this government at the given national GDP.
|
||||
func change_cost(gdp: float) -> float:
|
||||
return base_cost + cost_gdp_fraction * maxf(gdp, 0.0)
|
||||
|
||||
|
||||
## Total contribution to `stat` from every effect this government carries. All
|
||||
## effects are evaluated at level 1, the only level a government has.
|
||||
func effect_value(stat: String) -> float:
|
||||
var total := 0.0
|
||||
for effect in effects:
|
||||
if effect.stat == stat:
|
||||
total += effect.value_at(1)
|
||||
return total
|
||||
@@ -1 +0,0 @@
|
||||
uid://c5vv2vs1ugcv
|
||||
@@ -1,21 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="Government" format=3 uid="uid://bpupdoy0qg5p"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c5vv2vs1ugcv" path="res://data/government.gd" id="1_gov"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_monarchy_gdp"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "gdp"
|
||||
base_value = 0.1
|
||||
|
||||
[sub_resource type="Resource" id="Effect_monarchy_culture"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "culture"
|
||||
base_value = 1.0
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_gov")
|
||||
name = "Monarchy"
|
||||
description = "Rule by a hereditary sovereign. The crown favours commerce and tradition, lifting the economy and the nation's cultural output."
|
||||
cost_gdp_fraction = 0.03
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_monarchy_gdp"), SubResource("Effect_monarchy_culture")])
|
||||
@@ -1,26 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="Government" format=3 uid="uid://df6vqrc1yiepv"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c5vv2vs1ugcv" path="res://data/government.gd" id="1_gov"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_republic_gdp"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "gdp"
|
||||
base_value = 0.05
|
||||
|
||||
[sub_resource type="Resource" id="Effect_republic_income"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "budget_income"
|
||||
base_value = 0.1
|
||||
|
||||
[sub_resource type="Resource" id="Effect_republic_research"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "research"
|
||||
base_value = 1.0
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_gov")
|
||||
name = "Republic"
|
||||
description = "Rule by elected representatives. A balanced government that spreads its support across the economy, the treasury and the sciences."
|
||||
cost_gdp_fraction = 0.02
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_republic_gdp"), SubResource("Effect_republic_income"), SubResource("Effect_republic_research")])
|
||||
@@ -1,21 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="Government" format=3 uid="uid://ldjjnm1wgx20"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c5vv2vs1ugcv" path="res://data/government.gd" id="1_gov"]
|
||||
[ext_resource type="Script" uid="uid://c8lu7oofc64y" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_technocracy_research"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "research"
|
||||
base_value = 3.0
|
||||
|
||||
[sub_resource type="Resource" id="Effect_technocracy_gdp"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "gdp"
|
||||
base_value = 0.03
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_gov")
|
||||
name = "Technocracy"
|
||||
description = "Rule by technical experts. A single-minded drive for progress pours resources into research while economic growth takes second place."
|
||||
cost_gdp_fraction = 0.04
|
||||
effects = Array[ExtResource("2_be")]([SubResource("Effect_technocracy_research"), SubResource("Effect_technocracy_gdp")])
|
||||
@@ -1,6 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="MapGenerationConfig" format=3 uid="uid://2gapygilj0of"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dxn6ig8hfgep1" path="res://data/map_generation_config.gd" id="1_mapgen"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_mapgen")
|
||||
@@ -1,104 +0,0 @@
|
||||
class_name MapGenerationConfig
|
||||
|
||||
extends Resource
|
||||
|
||||
## Tunable parameters for the procedurally generated world map. The live
|
||||
## values live in res://data/map_generation.tres and can be edited from the
|
||||
## Godot inspector without touching any script.
|
||||
|
||||
## Map size in tiles. Tiles are generated around the origin, so a size of
|
||||
## (27, 16) spans x = -13..13 and y = -8..7.
|
||||
## A cylindrical map should use an even width so the wrapped rows line up.
|
||||
@export var map_size : Vector2i = Vector2i(64, 40)
|
||||
|
||||
## How the map edges connect. FLAT is an open rectangle; CYLINDRICAL joins the
|
||||
## east and west edges together so the world wraps around like the surface of
|
||||
## a cylinder.
|
||||
enum Topology { FLAT, CYLINDRICAL }
|
||||
@export var topology : Topology = Topology.CYLINDRICAL
|
||||
|
||||
## Seed for the terrain noise. 0 uses a fresh random seed on every run.
|
||||
@export var terrain_seed : int = 0
|
||||
|
||||
@export_group("Terrain")
|
||||
## Noise value at or above which a tile becomes land.
|
||||
@export_range(-1.0, 1.0) var sea_level : float = -0.2
|
||||
|
||||
## Push the map edges towards ocean so the playable land is an island.
|
||||
@export var falloff_enabled : bool = true
|
||||
## Normalised distance from the map centre where the falloff starts.
|
||||
@export_range(0.0, 1.5) var falloff_start : float = 0.55
|
||||
## How far the falloff lowers the noise at the map edges.
|
||||
@export_range(0.0, 2.0) var falloff_strength : float = 0.6
|
||||
|
||||
## Distance from the equator, as a fraction of half the map height, beyond
|
||||
## which every tile is replaced by the tileset's ice terrain. 0.9 freezes only
|
||||
## the last few rows at each pole.
|
||||
@export_range(0.0, 1.0) var ice_latitude : float = 0.9
|
||||
|
||||
@export_group("Noise")
|
||||
@export var noise_type : FastNoiseLite.NoiseType = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||
@export_range(0.0001, 1.0, 0.0001) var frequency : float = 0.06
|
||||
@export var fractal_type : FastNoiseLite.FractalType = FastNoiseLite.FRACTAL_FBM
|
||||
@export_range(1, 8) var octaves : int = 4
|
||||
@export_range(1.0, 4.0) var lacunarity : float = 2.0
|
||||
@export_range(0.0, 1.0) var gain : float = 0.5
|
||||
@export_range(0.0, 1.0) var fractal_weighted_strength : float = 0.0
|
||||
@export var cellular_distance_function : FastNoiseLite.CellularDistanceFunction = FastNoiseLite.DISTANCE_EUCLIDEAN
|
||||
@export var cellular_return_type : FastNoiseLite.CellularReturnType = FastNoiseLite.RETURN_DISTANCE
|
||||
@export_range(0.0, 2.0) var cellular_jitter : float = 1.0
|
||||
@export var domain_warp_enabled : bool = false
|
||||
@export_range(0.0, 100.0) var domain_warp_amplitude : float = 30.0
|
||||
@export_range(0.0, 1.0) var domain_warp_frequency : float = 0.05
|
||||
@export var domain_warp_fractal_type : FastNoiseLite.DomainWarpFractalType = FastNoiseLite.DOMAIN_WARP_FRACTAL_PROGRESSIVE
|
||||
|
||||
@export_group("Capitals")
|
||||
## Minimum number of tiles a capital is kept away from the map edge.
|
||||
@export var capital_margin : int = 2
|
||||
## Minimum distance (in tiles) between two capitals. If the land cannot
|
||||
## satisfy this for every civilisation, the closest remaining sites are used.
|
||||
@export_range(0.0, 64.0) var min_capital_distance : float = 8.0
|
||||
## Capitals only spawn on land masses of at least this many tiles, so they
|
||||
## never end up stranded on tiny islands. Falls back to the largest land mass
|
||||
## when none reaches the threshold.
|
||||
@export var min_capital_landmass : int = 40
|
||||
|
||||
@export_group("Cities")
|
||||
## Number of cities each starting civilisation founds, including its capital.
|
||||
@export var cities_per_civ : int = 10
|
||||
## Minimum distance (in tiles) kept between the centres of any two cities.
|
||||
@export var city_spacing : int = 8
|
||||
|
||||
@export_group("Tiles")
|
||||
@export var tile_source_id : int = 5
|
||||
@export var land_atlas_coords : Vector2i = Vector2i(3, 0)
|
||||
@export var sea_atlas_coords : Vector2i = Vector2i(5, 0)
|
||||
|
||||
|
||||
func is_cylindrical() -> bool:
|
||||
return topology == Topology.CYLINDRICAL
|
||||
|
||||
|
||||
func make_topology() -> MapTopology:
|
||||
var topology_type := MapTopology.Type.CYLINDRICAL if is_cylindrical() else MapTopology.Type.FLAT
|
||||
return MapTopology.new(map_size, topology_type)
|
||||
|
||||
|
||||
func create_noise(seed_value: int) -> FastNoiseLite:
|
||||
var noise := FastNoiseLite.new()
|
||||
noise.seed = seed_value
|
||||
noise.noise_type = noise_type
|
||||
noise.frequency = frequency
|
||||
noise.fractal_type = fractal_type
|
||||
noise.fractal_octaves = octaves
|
||||
noise.fractal_lacunarity = lacunarity
|
||||
noise.fractal_gain = gain
|
||||
noise.fractal_weighted_strength = fractal_weighted_strength
|
||||
noise.cellular_distance_function = cellular_distance_function
|
||||
noise.cellular_return_type = cellular_return_type
|
||||
noise.cellular_jitter = cellular_jitter
|
||||
noise.domain_warp_enabled = domain_warp_enabled
|
||||
noise.domain_warp_amplitude = domain_warp_amplitude
|
||||
noise.domain_warp_frequency = domain_warp_frequency
|
||||
noise.domain_warp_fractal_type = domain_warp_fractal_type
|
||||
return noise
|
||||
@@ -1 +0,0 @@
|
||||
uid://dxn6ig8hfgep1
|
||||
@@ -1,74 +0,0 @@
|
||||
class_name ProtoBuilding
|
||||
|
||||
extends Resource
|
||||
|
||||
## Template for a city building. Concrete instances live under
|
||||
## res://data/buildings and are edited in the inspector.
|
||||
##
|
||||
## Unlike Civilisation's one-off buildings, these can be levelled up and down.
|
||||
## The build cost and the upkeep grow exponentially with the level (alpha^level)
|
||||
## while the effects grow linearly or sublinear (see BuildingEffect), so pushing
|
||||
## a building too high yields diminishing returns for an accelerating bill.
|
||||
##
|
||||
## The build cost is based on the owner's GDP: a flat base_cost plus a fraction
|
||||
## of GDP, both multiplied by cost_alpha^level, so it stays meaningful as the
|
||||
## economy grows.
|
||||
|
||||
## Effect stat keys consumed by GameState.
|
||||
const EFFECT_GDP := "gdp"
|
||||
const EFFECT_RESEARCH := "research"
|
||||
const EFFECT_CULTURE := "culture"
|
||||
const EFFECT_BUDGET_INCOME := "budget_income"
|
||||
|
||||
@export var name : String
|
||||
@export_multiline var description : String
|
||||
|
||||
## Levels the building can reach; 0 means "not built".
|
||||
@export var max_level : int = 10
|
||||
|
||||
## Flat part of the build cost, in budget currency.
|
||||
@export var base_cost : float = 0.0
|
||||
## Fraction of the owner's GDP added to the build cost.
|
||||
@export var cost_gdp_fraction : float = 0.0
|
||||
## Exponential cost growth per level (alpha in alpha^level).
|
||||
@export_range(1.0, 5.0) var cost_alpha : float = 1.5
|
||||
|
||||
## Flat part of the hourly upkeep, in budget currency.
|
||||
@export var base_upkeep : float = 0.0
|
||||
## Fraction of the owner's GDP charged as hourly upkeep.
|
||||
@export var upkeep_gdp_fraction : float = 0.0
|
||||
## Exponential upkeep growth per level (alpha in alpha^level).
|
||||
@export_range(1.0, 5.0) var upkeep_alpha : float = 1.5
|
||||
|
||||
## Fraction of the removed level's build cost refunded when downgrading.
|
||||
@export_range(0.0, 1.0) var refund_fraction : float = 0.5
|
||||
|
||||
@export var effects : Array[BuildingEffect] = []
|
||||
|
||||
|
||||
## Budget cost to raise the building from `level` to `level + 1`.
|
||||
func build_cost(gdp: float, level: int) -> float:
|
||||
return (base_cost + cost_gdp_fraction * maxf(gdp, 0.0)) * pow(cost_alpha, float(maxi(level, 0)))
|
||||
|
||||
|
||||
## Budget refunded when lowering the building from `level` to `level - 1`.
|
||||
func refund(gdp: float, level: int) -> float:
|
||||
return build_cost(gdp, level - 1) * refund_fraction
|
||||
|
||||
|
||||
## Budget upkeep per game hour while the building sits at `level`.
|
||||
func upkeep(gdp: float, level: int) -> float:
|
||||
if level <= 0:
|
||||
return 0.0
|
||||
var base := base_upkeep + upkeep_gdp_fraction * maxf(gdp, 0.0)
|
||||
return base * pow(upkeep_alpha, float(level))
|
||||
|
||||
|
||||
## Total contribution to `stat` at `level`, summed over every effect that feeds
|
||||
## it. Returns 0.0 for an unknown stat or an unbuilt building.
|
||||
func effect_value(stat: String, level: int) -> float:
|
||||
var total := 0.0
|
||||
for effect in effects:
|
||||
if effect.stat == stat:
|
||||
total += effect.value_at(level)
|
||||
return total
|
||||
@@ -1 +0,0 @@
|
||||
uid://c0u6ish14ncb2
|
||||
@@ -1,19 +0,0 @@
|
||||
class_name ProtoUnit
|
||||
|
||||
extends Resource
|
||||
|
||||
## Template for a unit type. Concrete instances live under res://data/units.
|
||||
## Units spawned from the same template share these values but track their own
|
||||
## current HP and position.
|
||||
|
||||
@export var name : String
|
||||
@export var max_hp : int = 100
|
||||
## Production points needed to train the unit.
|
||||
@export var cost : int = 0
|
||||
## Production points consumed per game hour while the unit exists.
|
||||
@export var upkeep : int = 0
|
||||
@export var moveable : bool = true
|
||||
## Tiles travelled per game hour.
|
||||
@export var speed : float = 0.5
|
||||
@export var traversable_terrains : PackedStringArray = PackedStringArray()
|
||||
@export var sprites : SpriteFrames
|
||||
@@ -1 +0,0 @@
|
||||
uid://q2yr30ge5hl3
|
||||
@@ -1,19 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="Technology" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/technology.gd" id="1_tech"]
|
||||
[ext_resource type="Script" path="res://data/building_effect.gd" id="2_be"]
|
||||
[ext_resource type="Resource" path="res://data/technologies/scientific_method.tres" id="3_prev"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_automation_gdp"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "gdp"
|
||||
base_value = 0.08
|
||||
exponent = 1.0
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_tech")
|
||||
name = "Industrial Automation"
|
||||
description = "Robotics and control systems take over the factory floor, lifting national output."
|
||||
cost = 60.0
|
||||
prerequisites = Array[Technology]([ExtResource("3_prev")])
|
||||
effects = Array[BuildingEffect]([SubResource("Effect_automation_gdp")])
|
||||
@@ -1,19 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="Technology" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/technology.gd" id="1_tech"]
|
||||
[ext_resource type="Script" path="res://data/building_effect.gd" id="2_be"]
|
||||
[ext_resource type="Resource" path="res://data/technologies/scientific_method.tres" id="3_prev"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_quantum_research"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "research"
|
||||
base_value = 4.0
|
||||
exponent = 1.0
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_tech")
|
||||
name = "Quantum Computing"
|
||||
description = "Machines that reason in superposition. A step change in the pace of discovery."
|
||||
cost = 120.0
|
||||
prerequisites = Array[Technology]([ExtResource("3_prev")])
|
||||
effects = Array[BuildingEffect]([SubResource("Effect_quantum_research")])
|
||||
@@ -1,25 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="Technology" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/technology.gd" id="1_tech"]
|
||||
[ext_resource type="Script" path="res://data/building_effect.gd" id="2_be"]
|
||||
[ext_resource type="Resource" path="res://data/technologies/scientific_method.tres" id="3_prev"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_renewable_gdp"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "gdp"
|
||||
base_value = 0.03
|
||||
exponent = 1.0
|
||||
|
||||
[sub_resource type="Resource" id="Effect_renewable_income"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "budget_income"
|
||||
base_value = 0.05
|
||||
exponent = 1.0
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_tech")
|
||||
name = "Renewable Energy"
|
||||
description = "Clean generation cuts waste and strengthens the public finances."
|
||||
cost = 80.0
|
||||
prerequisites = Array[Technology]([ExtResource("3_prev")])
|
||||
effects = Array[BuildingEffect]([SubResource("Effect_renewable_gdp"), SubResource("Effect_renewable_income")])
|
||||
@@ -1,23 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="Technology" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/technology.gd" id="1_tech"]
|
||||
[ext_resource type="Script" path="res://data/building_effect.gd" id="2_be"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_method_research"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "research"
|
||||
base_value = 1.0
|
||||
exponent = 1.0
|
||||
|
||||
[sub_resource type="Resource" id="Effect_method_gdp"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "gdp"
|
||||
base_value = 0.02
|
||||
exponent = 1.0
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_tech")
|
||||
name = "Scientific Method"
|
||||
description = "A disciplined approach to experiment and evidence. Lays the groundwork for every later technology."
|
||||
cost = 20.0
|
||||
effects = Array[BuildingEffect]([SubResource("Effect_method_research"), SubResource("Effect_method_gdp")])
|
||||
@@ -1,26 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="Technology" format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/technology.gd" id="1_tech"]
|
||||
[ext_resource type="Script" path="res://data/building_effect.gd" id="2_be"]
|
||||
[ext_resource type="Resource" path="res://data/technologies/industrial_automation.tres" id="3_prev_a"]
|
||||
[ext_resource type="Resource" path="res://data/technologies/quantum_computing.tres" id="4_prev_b"]
|
||||
|
||||
[sub_resource type="Resource" id="Effect_space_culture"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "culture"
|
||||
base_value = 3.0
|
||||
exponent = 1.0
|
||||
|
||||
[sub_resource type="Resource" id="Effect_space_gdp"]
|
||||
script = ExtResource("2_be")
|
||||
stat = "gdp"
|
||||
base_value = 0.05
|
||||
exponent = 1.0
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_tech")
|
||||
name = "Space Program"
|
||||
description = "Reaching orbit inspires the nation and spins off technology across the economy."
|
||||
cost = 300.0
|
||||
prerequisites = Array[Technology]([ExtResource("3_prev_a"), ExtResource("4_prev_b")])
|
||||
effects = Array[BuildingEffect]([SubResource("Effect_space_culture"), SubResource("Effect_space_gdp")])
|
||||
@@ -1,58 +0,0 @@
|
||||
class_name Technology
|
||||
|
||||
extends Resource
|
||||
|
||||
## A technology a civilisation can unlock by spending accumulated research
|
||||
## points. Concrete instances live under res://data/technologies and are edited
|
||||
## in the inspector.
|
||||
##
|
||||
## Technologies form a tree through `prerequisites`: a technology can only be
|
||||
## researched once every prerequisite is already known. A technology has no
|
||||
## levels and applies its effects in full as soon as it is researched.
|
||||
|
||||
@export var name : String
|
||||
@export_multiline var description : String
|
||||
|
||||
## Research points needed to unlock this technology.
|
||||
@export var cost : float = 0.0
|
||||
|
||||
## Technologies that must be researched first. Compared by resource path, so
|
||||
## resources loaded independently still match.
|
||||
@export var prerequisites : Array[Technology] = []
|
||||
|
||||
## Bonuses this technology grants once researched. Effects are evaluated at
|
||||
## level 1, the only level a technology has. See BuildingEffect for the stats.
|
||||
@export var effects : Array[BuildingEffect] = []
|
||||
|
||||
|
||||
## Total contribution to `stat` from every effect this technology carries.
|
||||
func effect_value(stat: String) -> float:
|
||||
var total := 0.0
|
||||
for effect in effects:
|
||||
if effect.stat == stat:
|
||||
total += effect.value_at(1)
|
||||
return total
|
||||
|
||||
|
||||
## Position of this technology within a catalogue, matched by resource path
|
||||
## first so independently loaded instances still resolve. Returns -1 when the
|
||||
## technology is not in the catalogue.
|
||||
func catalogue_index(catalogue: Array) -> int:
|
||||
for i in catalogue.size():
|
||||
var candidate: Variant = catalogue[i]
|
||||
if candidate == self:
|
||||
return i
|
||||
if candidate is Technology and not resource_path.is_empty() \
|
||||
and (candidate as Technology).resource_path == resource_path:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
## True when every prerequisite appears in `researched`, a set of catalogue
|
||||
## indices. Always true when there are no prerequisites.
|
||||
func prerequisites_met(catalogue: Array, researched: Dictionary) -> bool:
|
||||
for prerequisite in prerequisites:
|
||||
var index := prerequisite.catalogue_index(catalogue)
|
||||
if index < 0 or not researched.has(index):
|
||||
return false
|
||||
return true
|
||||
@@ -1 +0,0 @@
|
||||
uid://bdx56cwdgtj2l
|
||||
@@ -1,14 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="ProtoUnit" format=3 uid="uid://b2l3y5q7vbixw"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://q2yr30ge5hl3" path="res://data/proto_unit.gd" id="1_f3bk2"]
|
||||
[ext_resource type="SpriteFrames" uid="uid://cwm6lopvum8y3" path="res://gfx/sprites/modern_infantry_sprite_frames.tres" id="2_03dfi"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_f3bk2")
|
||||
name = "Modern infantry"
|
||||
max_hp = 500
|
||||
cost = 500000
|
||||
upkeep = 50000000
|
||||
traversable_terrains = PackedStringArray("Land")
|
||||
sprites = ExtResource("2_03dfi")
|
||||
metadata/_custom_type_script = "uid://q2yr30ge5hl3"
|
||||
@@ -1,11 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="ProtoUnit" format=3 uid="uid://c11ktkedjkxyk"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://q2yr30ge5hl3" path="res://data/proto_unit.gd" id="1_r1qvx"]
|
||||
[ext_resource type="SpriteFrames" uid="uid://vpl03bqsaio2" path="res://gfx/sprites/worker_sprite_frames.tres" id="2_suh4m"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_r1qvx")
|
||||
name = "Worker"
|
||||
traversable_terrains = PackedStringArray("Land")
|
||||
sprites = ExtResource("2_suh4m")
|
||||
metadata/_custom_type_script = "uid://q2yr30ge5hl3"
|
||||
@@ -1,96 +0,0 @@
|
||||
[preset.0]
|
||||
|
||||
name="Web"
|
||||
platform="Web"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="exportsw/Battle for 'Tismo.html"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.0.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=false
|
||||
variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=false
|
||||
html/export_icon=true
|
||||
html/custom_html_shell="res://web_shell.html"
|
||||
html/head_include=""
|
||||
html/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
html/experimental_virtual_keyboard=false
|
||||
progressive_web_app/enabled=false
|
||||
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||
progressive_web_app/offline_page=""
|
||||
progressive_web_app/display=1
|
||||
progressive_web_app/orientation=0
|
||||
progressive_web_app/icon_144x144=""
|
||||
progressive_web_app/icon_180x180=""
|
||||
progressive_web_app/icon_512x512=""
|
||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||
threads/emscripten_pool_size=8
|
||||
threads/godot_pool_size=4
|
||||
|
||||
[preset.1]
|
||||
|
||||
name="Linux"
|
||||
platform="Linux"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path=""
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.1.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
shader_baker/enabled=false
|
||||
binary_format/architecture="x86_64"
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script="#!/usr/bin/env bash
|
||||
export DISPLAY=:0
|
||||
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
|
||||
\"{temp_dir}/{exe_name}\" {cmd_args}"
|
||||
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
||||
pkill -x -f \"{temp_dir}/{exe_name} {cmd_args}\"
|
||||
rm -rf \"{temp_dir}\""
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
description = "Battle for 'Tismo — dedicated server and web client";
|
||||
description = "Battle for 'Tismo — Node.js game server and HTML5 web client";
|
||||
|
||||
# Must provide Godot 4.6 and matching export templates. nixos-26.05 does.
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
|
||||
|
||||
outputs =
|
||||
@@ -20,7 +19,7 @@
|
||||
built = pkgs.callPackage ./nix/package.nix { };
|
||||
in
|
||||
{
|
||||
inherit (built) server web webTemplates;
|
||||
inherit (built) server web;
|
||||
default = built.server;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,36 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://dcuuinu88yl86"
|
||||
path="res://.godot/imported/USSR STENCIL WEBFONT.ttf-00585b05d441d3a44d1aa09c746b87be.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://fonts/USSR STENCIL WEBFONT.ttf"
|
||||
dest_files=["res://.godot/imported/USSR STENCIL WEBFONT.ttf-00585b05d441d3a44d1aa09c746b87be.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=4
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
@@ -1,36 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://bjpoddqqagwlx"
|
||||
path="res://.godot/imported/USSR STENCIL.otf-63597cdc1b23ccebac932a3c70452844.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://fonts/USSR STENCIL.otf"
|
||||
dest_files=["res://.godot/imported/USSR STENCIL.otf-63597cdc1b23ccebac932a3c70452844.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=4
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
@@ -1,36 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://c58ly4g14o802"
|
||||
path="res://.godot/imported/TitanOne-Regular.ttf-9cdbf1e26e32de0beec39866072264ff.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://fonts/titan_one/TitanOne-Regular.ttf"
|
||||
dest_files=["res://.godot/imported/TitanOne-Regular.ttf-9cdbf1e26e32de0beec39866072264ff.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=4
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
|
Before Width: | Height: | Size: 753 B |
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dwn3htawrebrs"
|
||||
path="res://.godot/imported/hex-borders.png-fd13eb4a3e596c7f6971eccf0b95bfdb.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://gfx/hex-borders.png"
|
||||
dest_files=["res://.godot/imported/hex-borders.png-fd13eb4a3e596c7f6971eccf0b95bfdb.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dy05p4taw2grt"
|
||||
path="res://.godot/imported/coat_of_arms_mask.png-20bfc00212ab4df91abccadc870cd983.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://gfx/hud/coat_of_arms_mask.png"
|
||||
dest_files=["res://.godot/imported/coat_of_arms_mask.png-20bfc00212ab4df91abccadc870cd983.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bqhfv0surkp5h"
|
||||
path="res://.godot/imported/coat_of_arms_outline.png-ae9ce9a4a48544bb9677a2d9931ffe4b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://gfx/hud/coat_of_arms_outline.png"
|
||||
dest_files=["res://.godot/imported/coat_of_arms_outline.png-ae9ce9a4a48544bb9677a2d9931ffe4b.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
|
||||
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bfaw5xu3t3sfl"
|
||||
path="res://.godot/imported/coat_of_arms_texture.png-e7e5ebfcc69289d5abb72b781d38f7d7.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://gfx/hud/flags/coat_of_arms_texture.png"
|
||||
dest_files=["res://.godot/imported/coat_of_arms_texture.png-e7e5ebfcc69289d5abb72b781d38f7d7.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
|
||||
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://io753v4cud2c"
|
||||
path="res://.godot/imported/flag_britain.png-a71e25a81e0c9fb4940bd04658ca0737.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://gfx/hud/flags/flag_britain.png"
|
||||
dest_files=["res://.godot/imported/flag_britain.png-a71e25a81e0c9fb4940bd04658ca0737.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
|
||||
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bsuer7fwcs6py"
|
||||
path="res://.godot/imported/flag_france.png-97c9d58634cc98f8b172afb7d0f7dafd.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://gfx/hud/flags/flag_france.png"
|
||||
dest_files=["res://.godot/imported/flag_france.png-97c9d58634cc98f8b172afb7d0f7dafd.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
|
||||
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://co0ufd01torfw"
|
||||
path="res://.godot/imported/flag_galicia.png-15ccf0de97f65db2565cc578f37d37f0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://gfx/hud/flags/flag_galicia.png"
|
||||
dest_files=["res://.godot/imported/flag_galicia.png-15ccf0de97f65db2565cc578f37d37f0.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
|
||||
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bga6kymgrsbra"
|
||||
path="res://.godot/imported/flag_germany.png-26b43d47962e77919d8682aa2c8f4b16.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://gfx/hud/flags/flag_germany.png"
|
||||
dest_files=["res://.godot/imported/flag_germany.png-26b43d47962e77919d8682aa2c8f4b16.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
|
||||