Switched to HTML5

This commit is contained in:
2026-09-16 19:59:18 +02:00
parent b6f8fd3504
commit 0b8c5bee68
367 changed files with 10993 additions and 15796 deletions
+5 -6
View File
@@ -1,11 +1,10 @@
# Godot 4+ specific ignores # Node dependencies and coverage output
.godot/ /node_modules/
/android/ /coverage/
# Build artifacts # Nix build artifacts
/exportsw/
/result /result
/result-* /result-*
# Nix / direnv # direnv
/.direnv/ /.direnv/
+67 -24
View File
@@ -1,38 +1,81 @@
# AGENTS.md # 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 ## Commands
- Validate / reimport after edits: `godot --headless --path . --import` - Install dev dependencies once: `npm install` (only `c8` and `jsdom`, used by
- Run the test suite: `godot --headless --path . res://tests/test_runner.tscn` (prints a summary and exits non-zero on failure) the test suite — nothing the game runs needs them).
- Parse-check a single script: `godot --headless --path . --check-only --script res://scenes/units/unit.gd` - Run the test suite: `node tests/run_tests.js` (alias `npm test`; prints a
- Run a scene directly: `godot --path . res://scenes/ui/menu_screen.tscn` summary and exits non-zero on failure).
- 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. - Coverage report: `npm run coverage` (c8, writes `coverage/`).
- There is no linter, formatter, or CI. Do not invent commands beyond the ones listed here. - Start the game server and client locally:
- `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. `node server/server.js --port 27015 --bind 127.0.0.1`. Then open
- `--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. `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 ## Gotchas
- Renderer is `gl_compatibility` (not Forward+) and 3D uses Jolt Physics. GL Compatibility omits Forward+ only features. - Keep the shared modules framework-free: they are imported unchanged by both
- `.uid` files are Godot-maintained; keep them paired with their scripts, never hand-edit or delete. `.godot/` is generated and gitignored. `server/` (Node, ESM) and `client/js/` (browser). No Node built-ins and no DOM
- Indent `.gd` files with tabs (Godot default). `.editorconfig` only enforces UTF-8. APIs in `shared/` (`server/websocket.js` is the only place Node built-ins are
- `scenes/world_map.tscn` uses `TileMapLayer` (scene format=4), not the deprecated `TileMap`. expected).
- 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`. - Do not add JavaScript libraries. The only third-party code allowed is the
- Export templates must match the engine: use `godot_4_6` with `godot_4_6-export-templates-bin` from nixpkgs. 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 ## 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). - Game logic is server-authoritative. `shared/game_state.js` (`GameState`) is a
- 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. framework-free model that owns the world, units, cities, territory, economy,
- 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. training and visibility. `server/game_server.js` (`GameServer`) owns a
- Scenes are grouped by area: `scenes/`, `scenes/ui/`, `scenes/units/`. Reusable scenes attach a `class_name` script (e.g. `Unit`). `GameState`, validates orders and broadcasts snapshots over the transport in
- 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. `server/server.js`. The browser client is a view: `client/js/map_view.js`
- `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. rebuilds terrain from the shared seed, renders snapshots and sends orders.
- 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. - 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 ## 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. - `flake.nix` exposes `packages.<system>.server` and `.web`, plus
- 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. `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`.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

-40
View File
@@ -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

+390
View File
@@ -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;
}
+193
View File
@@ -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>
+216
View File
@@ -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"));
+403
View File
@@ -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));
}
}
File diff suppressed because it is too large Load Diff
+287
View File
@@ -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");
}
}
+186
View File
@@ -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;
}
}
}
+2
View File
File diff suppressed because one or more lines are too long
-23
View File
@@ -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
View File
@@ -1 +0,0 @@
uid://c8lu7oofc64y
-20
View File
@@ -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")])
-20
View File
@@ -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")])
-19
View File
@@ -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")])
-20
View File
@@ -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")])
-20
View File
@@ -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")])
-26
View File
@@ -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")])
-9
View File
@@ -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")
-9
View File
@@ -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")
-10
View File
@@ -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
View File
@@ -1 +0,0 @@
uid://cor2vawqxhptw
-14
View File
@@ -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
View File
@@ -1 +0,0 @@
uid://bbrofteu1a30o
-13
View File
@@ -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"
-13
View File
@@ -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"
-13
View File
@@ -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"
-13
View File
@@ -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"
-13
View File
@@ -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"
-13
View File
@@ -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"
-13
View File
@@ -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"
-13
View File
@@ -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"
-13
View File
@@ -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"
-7
View File
@@ -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
-69
View File
@@ -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
View File
@@ -1 +0,0 @@
uid://bm2pvegtcvgww
-38
View File
@@ -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
View File
@@ -1 +0,0 @@
uid://c5vv2vs1ugcv
-21
View File
@@ -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")])
-26
View File
@@ -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")])
-21
View File
@@ -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")])
-6
View File
@@ -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")
-104
View File
@@ -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
View File
@@ -1 +0,0 @@
uid://dxn6ig8hfgep1
-74
View File
@@ -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
View File
@@ -1 +0,0 @@
uid://c0u6ish14ncb2
-19
View File
@@ -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
View File
@@ -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")])
-19
View File
@@ -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")])
-25
View File
@@ -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")])
-23
View File
@@ -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")])
-26
View File
@@ -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")])
-58
View File
@@ -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
View File
@@ -1 +0,0 @@
uid://bdx56cwdgtj2l
-14
View File
@@ -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"
-11
View File
@@ -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"
-96
View File
@@ -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}\""
+2 -3
View File
@@ -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"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = outputs =
@@ -20,7 +19,7 @@
built = pkgs.callPackage ./nix/package.nix { }; built = pkgs.callPackage ./nix/package.nix { };
in in
{ {
inherit (built) server web webTemplates; inherit (built) server web;
default = built.server; default = built.server;
} }
); );
-91
View File
@@ -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.
Binary file not shown.
Binary file not shown.
-36
View File
@@ -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={}
Binary file not shown.
-36
View File
@@ -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={}
Binary file not shown.
Binary file not shown.
@@ -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={}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 753 B

-40
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

-40
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

-40
View File
@@ -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
-40
View File
@@ -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
-40
View File
@@ -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
-40
View File
@@ -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
-40
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More