mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 05:15:44 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b2cf908d1 | ||
|
|
496545b006 |
@@ -46,12 +46,6 @@ jobs:
|
||||
lfs: false
|
||||
- name: Check BUILD.bazel dependencies
|
||||
run: ./scripts/check_build_deps.sh --strict
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- name: Check JavaScript syntax
|
||||
run: node --check src/main/go/net/eagle0/admin_server/static/map_editor.js
|
||||
|
||||
test:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
@@ -74,10 +74,6 @@ jobs:
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Fetch LFS files needed for admin server
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
run: git lfs pull --include="src/main/go/net/eagle0/admin_server/static/tiles/*"
|
||||
|
||||
- name: Build all Docker images
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
id: build-all
|
||||
|
||||
@@ -5,7 +5,6 @@ repos:
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: no-commit-to-branch
|
||||
args: [--branch, main]
|
||||
- repo: https://github.com/pocc/pre-commit-hooks
|
||||
|
||||
@@ -243,13 +243,6 @@ pkg_tar(
|
||||
package_dir = "/app",
|
||||
)
|
||||
|
||||
# Package the .e0mj map files for the map editor
|
||||
pkg_tar(
|
||||
name = "admin_maps_layer",
|
||||
srcs = ["//src/main/resources/net/eagle0/shardok/maps:maps_json"],
|
||||
package_dir = "/app/maps",
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "admin_server_image",
|
||||
base = "@alpine_linux_linux_amd64",
|
||||
@@ -258,7 +251,6 @@ oci_image(
|
||||
tars = [
|
||||
":busybox_layer",
|
||||
":admin_binary_layer",
|
||||
":admin_maps_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
)
|
||||
|
||||
@@ -208,8 +208,6 @@ services:
|
||||
- "${JFR_SIDECAR_ADDR:-jfr-sidecar:8081}" # Can be switched for blue-green
|
||||
- "--http-port"
|
||||
- "8080"
|
||||
- "--maps-dir"
|
||||
- "/app/maps"
|
||||
environment:
|
||||
# Secret for CI to authenticate client update notifications
|
||||
NOTIFY_SECRET: "${NOTIFY_SECRET:-}"
|
||||
@@ -217,8 +215,6 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${ADMIN_S3_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${ADMIN_S3_SECRET_KEY:-}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# No external port - accessed via nginx at admin.eagle0.net
|
||||
depends_on:
|
||||
- auth
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# Renaming a Province
|
||||
|
||||
This document explains the steps required to rename a province in Eagle0.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### 1. Province Map TSV (Server Source of Truth)
|
||||
|
||||
**File:** `src/main/resources/net/eagle0/eagle/province_map.tsv`
|
||||
|
||||
This is the primary source of province data. Each row contains:
|
||||
- Province ID
|
||||
- Province name
|
||||
- Neighbor IDs
|
||||
- Neighbor directions
|
||||
- Province name (repeated)
|
||||
- Neighbor names (dot-separated)
|
||||
- Starting food
|
||||
|
||||
You need to:
|
||||
1. Change the province name in its own row (columns 2 and 5)
|
||||
2. Update the neighbor name references in all neighboring provinces (column 6)
|
||||
|
||||
Example: To rename "Fluria" to "NewName", you would need to update:
|
||||
- Line 26: The Fluria row itself
|
||||
- Lines 5, 10, 18, 24, 27, 38, 41: All provinces that list Fluria as a neighbor
|
||||
|
||||
### 2. LLM Map Description
|
||||
|
||||
**File:** `src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators/MapDescription.scala`
|
||||
|
||||
This file contains a hardcoded geographic description of the map used in LLM prompts for generating narrative text. Update any references to the old province name.
|
||||
|
||||
### 3. Client Centroids JSON
|
||||
|
||||
**File:** `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/centroids.json`
|
||||
|
||||
This JSON file contains province metadata used for map rendering (centroid positions, areas, orientations). Each entry has a `name` field that should be updated to match.
|
||||
|
||||
Note: The actual province name displayed to players comes from the server via the `ProvinceView` proto, not from this file. However, this file should be kept in sync for consistency and because it may be used for map label positioning.
|
||||
|
||||
### 4. Test Files
|
||||
|
||||
**File:** `src/test/scala/net/eagle0/eagle/library/util/validations/RuntimeValidatorTest.scala`
|
||||
|
||||
Update any test strings that reference the old province name.
|
||||
|
||||
## How Province Names Flow to the Client
|
||||
|
||||
Province names are sent from the Eagle server to the Unity client via the `ProvinceView` protobuf message:
|
||||
|
||||
```protobuf
|
||||
message ProvinceView {
|
||||
int32 id = 1;
|
||||
string name = 6; // <-- Province name sent from server
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The server reads province names from `province_map.tsv` at startup. The client receives names through the proto and displays them via `province.Name` in C# code. This means:
|
||||
|
||||
- Changing the TSV automatically updates what clients see
|
||||
- No C# code changes are needed (the code uses `province.Name` generically)
|
||||
- The `centroids.json` name field is for map rendering/positioning, not display
|
||||
|
||||
## Files That Do NOT Need Changes
|
||||
|
||||
- **Hero backstory TSV files** (`heroes.tsv`, `generated_heroes.tsv`) - These don't contain province name references
|
||||
- **C# client code** - Uses `province.Name` from the server proto, not hardcoded names
|
||||
- **Proto definitions** - No province names are hardcoded in protos
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Update `province_map.tsv` - province's own row
|
||||
- [ ] Update `province_map.tsv` - all neighbor references
|
||||
- [ ] Update `MapDescription.scala`
|
||||
- [ ] Update `centroids.json`
|
||||
- [ ] Update any test files with province name references
|
||||
- [ ] Build and run tests: `bazel test //src/test/scala/...`
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restart the active Eagle instance.
|
||||
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
|
||||
#
|
||||
# This is useful when you need Eagle to reload game state from persistence
|
||||
# (e.g., after a rewind) without doing a full blue-green deployment.
|
||||
#
|
||||
# Usage:
|
||||
# eagle-restart # Restart active instance, wait for healthy
|
||||
# eagle-restart --no-wait # Restart without waiting for health check
|
||||
#
|
||||
# To create an alias, add to ~/.bashrc:
|
||||
# alias eagle-restart='/opt/eagle0/scripts/eagle-restart.sh'
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/eagle0}"
|
||||
ACTIVE_FILE="${APP_DIR}/.active-instance"
|
||||
HEALTH_TIMEOUT=120 # seconds
|
||||
|
||||
# Read active instance from file, with fallback
|
||||
if [ -f "$ACTIVE_FILE" ]; then
|
||||
ACTIVE=$(cat "$ACTIVE_FILE")
|
||||
else
|
||||
# Fallback: check which container is actually running
|
||||
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-blue"
|
||||
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-green"
|
||||
else
|
||||
ACTIVE=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ACTIVE" ]; then
|
||||
echo "Error: No active Eagle instance found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NO_WAIT=false
|
||||
if [ "${1:-}" = "--no-wait" ]; then
|
||||
NO_WAIT=true
|
||||
fi
|
||||
|
||||
echo "Restarting ${ACTIVE}..."
|
||||
docker restart "$ACTIVE"
|
||||
|
||||
if [ "$NO_WAIT" = true ]; then
|
||||
echo "Restart initiated. Not waiting for health check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Waiting for ${ACTIVE} to become healthy (timeout: ${HEALTH_TIMEOUT}s)..."
|
||||
elapsed=0
|
||||
while [ $elapsed -lt $HEALTH_TIMEOUT ]; do
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' "$ACTIVE" 2>/dev/null || echo "unknown")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
echo "${ACTIVE} is healthy after ${elapsed}s."
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
# Print progress every 10 seconds
|
||||
if [ $((elapsed % 10)) -eq 0 ]; then
|
||||
echo " ...${elapsed}s (status: ${health})"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Warning: ${ACTIVE} did not become healthy within ${HEALTH_TIMEOUT}s (status: ${health})" >&2
|
||||
echo "Check logs with: eagle-logs" >&2
|
||||
exit 1
|
||||
@@ -302,13 +302,7 @@ namespace eagle {
|
||||
kv => kv.Value.ImagePath),
|
||||
battalionNames: _currentModel.BattalionNames.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value),
|
||||
heroFallbackLookup: hid => {
|
||||
if (_currentModel.Heroes.TryGetValue(hid, out var hero)) {
|
||||
return (hero.NameTextId, hero.ImagePath);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
kv => kv.Value));
|
||||
|
||||
return shardokModel;
|
||||
}
|
||||
|
||||
@@ -129,23 +129,20 @@ namespace eagle {
|
||||
|
||||
public ProvinceIDLoader provinceIDLoader;
|
||||
|
||||
private readonly Color _selectionColor = Color.grey;
|
||||
private readonly Color _targetColor = Color.red;
|
||||
private readonly Color _commandAvailableColor = Color.blue;
|
||||
private readonly Color _hoverColor = new Color(0, 0, 0.5f, 0.5f);
|
||||
|
||||
private const float SelectedBorderMin = 2f, SelectedBorderMax = 6f,
|
||||
private const float SelectedBorderMin = 4f, SelectedBorderMax = 12f,
|
||||
SelectedBorderCycle = 1.0f;
|
||||
private static readonly Color SelectedBorderColor = new Color(0f, 0f, 0f, 0.95f);
|
||||
|
||||
private static readonly Color OverrideStrobeColor = Color.black;
|
||||
private static readonly Color OverrideBorderColor = new Color(0.05f, 0.05f, 0.05f, 0.95f);
|
||||
private const float OverrideBorderMin = 3f, OverrideBorderMax = 8f,
|
||||
OverrideBorderCycle = 0.25f;
|
||||
|
||||
private static readonly Color TargetedStrobeColor = new Color(0.55f, 0.05f, 0.15f);
|
||||
private static readonly Color TargetedBorderColor = new Color(0.55f, 0.05f, 0.15f, 0.95f);
|
||||
private const float TargetedBorderMin = 3f, TargetedBorderMax = 8f,
|
||||
private const float TargetedBorderMin = 6f, TargetedBorderMax = 15f,
|
||||
TargetedBorderCycle = 0.25f;
|
||||
private static readonly Color TargetedBorderColor = new Color(1f, 0.2f, 0.2f, 0.95f);
|
||||
|
||||
private const float CommandBorderMin = 1.5f, CommandBorderMax = 5f,
|
||||
private const float CommandBorderMin = 3f, CommandBorderMax = 10f,
|
||||
CommandBorderCycle = 0.5f;
|
||||
private static readonly Color CommandBorderColor = new Color(0.3f, 0.3f, 1f, 0.85f);
|
||||
|
||||
@@ -170,17 +167,6 @@ namespace eagle {
|
||||
return color;
|
||||
}
|
||||
|
||||
private static float Luminance(Color c) {
|
||||
return 0.299f * c.r + 0.587f * c.g + 0.114f * c.b;
|
||||
}
|
||||
|
||||
private static (Color strobe, Color border) ContrastingHighlightColors(Color baseColor) {
|
||||
if (Luminance(baseColor) > 0.5f) {
|
||||
return (Color.black, new Color(0.05f, 0.05f, 0.05f, 0.95f));
|
||||
}
|
||||
return (Color.white, new Color(1f, 1f, 1f, 0.95f));
|
||||
}
|
||||
|
||||
private static bool HasBlizzardEvent(ProvinceView province) {
|
||||
foreach (var evt in province.KnownEvents) {
|
||||
if (evt.SealedValueCase == ProvinceEvent.SealedValueOneofCase.BlizzardEvent) {
|
||||
@@ -247,32 +233,27 @@ namespace eagle {
|
||||
SetDefaultProvinceColor(commandProvince);
|
||||
}
|
||||
|
||||
provinceBorderManager.SetHighlightGroup(OverrideTargetedProvinces);
|
||||
|
||||
foreach (var pid in OverrideTargetedProvinces) {
|
||||
FlashProvince(pid, OverrideStrobeColor, OverrideBorderCycle);
|
||||
FlashProvince(pid, _targetColor, TargetedBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
pid,
|
||||
OverrideBorderColor,
|
||||
OverrideBorderMin,
|
||||
OverrideBorderMax,
|
||||
OverrideBorderCycle);
|
||||
TargetedBorderColor,
|
||||
TargetedBorderMin,
|
||||
TargetedBorderMax,
|
||||
TargetedBorderCycle);
|
||||
}
|
||||
} else {
|
||||
provinceBorderManager.ClearHighlightGroup();
|
||||
if (SelectedProvinceId is ProvinceId sPid) {
|
||||
var sBase = ColorForProvince(Model.Provinces[sPid]);
|
||||
var (sStrobe, sBorder) = ContrastingHighlightColors(sBase);
|
||||
FlashProvince(sPid, sStrobe, SelectedBorderCycle);
|
||||
FlashProvince(sPid, _selectionColor, SelectedBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
sPid,
|
||||
sBorder,
|
||||
SelectedBorderColor,
|
||||
SelectedBorderMin,
|
||||
SelectedBorderMax,
|
||||
SelectedBorderCycle);
|
||||
}
|
||||
foreach (ProvinceId tPid in TargetedProvinces) {
|
||||
FlashProvince(tPid, TargetedStrobeColor, TargetedBorderCycle);
|
||||
FlashProvince(tPid, _targetColor, TargetedBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
tPid,
|
||||
TargetedBorderColor,
|
||||
|
||||
+1
-9
@@ -39,14 +39,6 @@ Material:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _CrossProvinceIDMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _HighlightGroupLookup:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
@@ -63,7 +55,7 @@ Material:
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _Cutoff: 0.308
|
||||
- _DefaultBorderWidth: 0.5
|
||||
- _DefaultBorderWidth: 0.35
|
||||
- _OceanWaveSpeed: 2
|
||||
- _OceanWaveStrength: 0.0337
|
||||
- _Stencil: 0
|
||||
|
||||
+14
-65
@@ -6,24 +6,19 @@ namespace eagle {
|
||||
/// Each pixel stores the Euclidean distance (Chamfer approximation, in texels) to
|
||||
/// the nearest province boundary, clamped to 255. The shader uses this to render
|
||||
/// variable-width borders with a single texture sample per fragment.
|
||||
///
|
||||
/// Also generates a cross-province ID map: for each pixel, the province ID on the
|
||||
/// other side of the nearest border. The shader uses this to suppress highlight
|
||||
/// borders between provinces in the same highlight group.
|
||||
/// </summary>
|
||||
public class ProvinceBorderDistanceGenerator : MonoBehaviour {
|
||||
public ProvinceIDLoader provinceIDLoader;
|
||||
public Material provinceMapMaterial;
|
||||
|
||||
private Texture2D _distanceTexture;
|
||||
private Texture2D _crossProvinceTexture;
|
||||
|
||||
void Start() {
|
||||
var mapBytes = provinceIDLoader.DecompressedBytes;
|
||||
int w = provinceIDLoader.mapWidth;
|
||||
int h = provinceIDLoader.mapHeight;
|
||||
|
||||
var (dist, crossIds) = ComputeDistanceMap(mapBytes, w, h);
|
||||
var dist = ComputeDistanceMap(mapBytes, w, h);
|
||||
|
||||
_distanceTexture = new Texture2D(w, h, TextureFormat.R8, false);
|
||||
_distanceTexture.filterMode = FilterMode.Bilinear;
|
||||
@@ -31,21 +26,13 @@ namespace eagle {
|
||||
_distanceTexture.LoadRawTextureData(dist);
|
||||
_distanceTexture.Apply();
|
||||
|
||||
_crossProvinceTexture = new Texture2D(w, h, TextureFormat.R8, false);
|
||||
_crossProvinceTexture.filterMode = FilterMode.Point;
|
||||
_crossProvinceTexture.wrapMode = TextureWrapMode.Clamp;
|
||||
_crossProvinceTexture.LoadRawTextureData(crossIds);
|
||||
_crossProvinceTexture.Apply();
|
||||
|
||||
provinceMapMaterial.SetTexture("_BorderDistanceMap", _distanceTexture);
|
||||
provinceMapMaterial.SetTexture("_CrossProvinceIDMap", _crossProvinceTexture);
|
||||
}
|
||||
|
||||
private static (byte[] dist, byte[] crossIds) ComputeDistanceMap(byte[] ids, int w, int h) {
|
||||
private static byte[] ComputeDistanceMap(byte[] ids, int w, int h) {
|
||||
const float DIAG = 1.414f;
|
||||
int size = w * h;
|
||||
var dist = new float[size];
|
||||
var crossIds = new byte[size];
|
||||
|
||||
// Init: border pixels = 0, interior = large value
|
||||
for (int y = 0; y < h; y++) {
|
||||
@@ -56,29 +43,24 @@ namespace eagle {
|
||||
// Ocean/invalid pixels are always border
|
||||
if (id == 0 || id == 0xFF) {
|
||||
dist[idx] = 0f;
|
||||
crossIds[idx] = id;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check 8 neighbors for different province ID
|
||||
bool isBorder = false;
|
||||
byte crossId = id;
|
||||
for (int dy = -1; dy <= 1 && !isBorder; dy++) {
|
||||
for (int dx = -1; dx <= 1 && !isBorder; dx++) {
|
||||
if (dx == 0 && dy == 0) continue;
|
||||
int nx = x + dx, ny = y + dy;
|
||||
if (nx < 0 || nx >= w || ny < 0 || ny >= h) {
|
||||
isBorder = true;
|
||||
crossId = 0;
|
||||
} else if (ids[ny * w + nx] != id) {
|
||||
isBorder = true;
|
||||
crossId = ids[ny * w + nx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dist[idx] = isBorder ? 0f : 255f;
|
||||
crossIds[idx] = crossId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,27 +71,11 @@ namespace eagle {
|
||||
if (dist[idx] == 0f) continue;
|
||||
|
||||
float d = dist[idx];
|
||||
int bestIdx = idx;
|
||||
|
||||
if (x > 0 && dist[idx - 1] + 1f < d) {
|
||||
d = dist[idx - 1] + 1f;
|
||||
bestIdx = idx - 1;
|
||||
}
|
||||
if (y > 0 && dist[(y - 1) * w + x] + 1f < d) {
|
||||
d = dist[(y - 1) * w + x] + 1f;
|
||||
bestIdx = (y - 1) * w + x;
|
||||
}
|
||||
if (x > 0 && y > 0 && dist[(y - 1) * w + x - 1] + DIAG < d) {
|
||||
d = dist[(y - 1) * w + x - 1] + DIAG;
|
||||
bestIdx = (y - 1) * w + x - 1;
|
||||
}
|
||||
if (x < w - 1 && y > 0 && dist[(y - 1) * w + x + 1] + DIAG < d) {
|
||||
d = dist[(y - 1) * w + x + 1] + DIAG;
|
||||
bestIdx = (y - 1) * w + x + 1;
|
||||
}
|
||||
|
||||
if (x > 0) d = Mathf.Min(d, dist[idx - 1] + 1f);
|
||||
if (y > 0) d = Mathf.Min(d, dist[(y - 1) * w + x] + 1f);
|
||||
if (x > 0 && y > 0) d = Mathf.Min(d, dist[(y - 1) * w + x - 1] + DIAG);
|
||||
if (x < w - 1 && y > 0) d = Mathf.Min(d, dist[(y - 1) * w + x + 1] + DIAG);
|
||||
dist[idx] = d;
|
||||
if (bestIdx != idx) { crossIds[idx] = crossIds[bestIdx]; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,41 +86,24 @@ namespace eagle {
|
||||
if (dist[idx] == 0f) continue;
|
||||
|
||||
float d = dist[idx];
|
||||
int bestIdx = idx;
|
||||
|
||||
if (x < w - 1 && dist[idx + 1] + 1f < d) {
|
||||
d = dist[idx + 1] + 1f;
|
||||
bestIdx = idx + 1;
|
||||
}
|
||||
if (y < h - 1 && dist[(y + 1) * w + x] + 1f < d) {
|
||||
d = dist[(y + 1) * w + x] + 1f;
|
||||
bestIdx = (y + 1) * w + x;
|
||||
}
|
||||
if (x < w - 1 && y < h - 1 && dist[(y + 1) * w + x + 1] + DIAG < d) {
|
||||
d = dist[(y + 1) * w + x + 1] + DIAG;
|
||||
bestIdx = (y + 1) * w + x + 1;
|
||||
}
|
||||
if (x > 0 && y < h - 1 && dist[(y + 1) * w + x - 1] + DIAG < d) {
|
||||
d = dist[(y + 1) * w + x - 1] + DIAG;
|
||||
bestIdx = (y + 1) * w + x - 1;
|
||||
}
|
||||
|
||||
if (x < w - 1) d = Mathf.Min(d, dist[idx + 1] + 1f);
|
||||
if (y < h - 1) d = Mathf.Min(d, dist[(y + 1) * w + x] + 1f);
|
||||
if (x < w - 1 && y < h - 1) d = Mathf.Min(d, dist[(y + 1) * w + x + 1] + DIAG);
|
||||
if (x > 0 && y < h - 1) d = Mathf.Min(d, dist[(y + 1) * w + x - 1] + DIAG);
|
||||
dist[idx] = d;
|
||||
if (bestIdx != idx) { crossIds[idx] = crossIds[bestIdx]; }
|
||||
}
|
||||
}
|
||||
|
||||
// Pack into byte arrays
|
||||
var distResult = new byte[size];
|
||||
// Pack into byte array
|
||||
var result = new byte[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
distResult[i] = (byte)Mathf.Min(Mathf.RoundToInt(dist[i]), 255);
|
||||
result[i] = (byte)Mathf.Min(Mathf.RoundToInt(dist[i]), 255);
|
||||
}
|
||||
return (distResult, crossIds);
|
||||
return result;
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_distanceTexture != null) { Destroy(_distanceTexture); }
|
||||
if (_crossProvinceTexture != null) { Destroy(_crossProvinceTexture); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-38
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
@@ -6,25 +5,20 @@ namespace eagle {
|
||||
/// Manages 256x1 lookup textures for per-province border highlighting.
|
||||
/// Border color and width are set per province and uploaded each frame
|
||||
/// when dirty, mirroring ProvinceColorManager's batched approach.
|
||||
/// Also manages a highlight-group lookup so the shader can suppress
|
||||
/// highlight borders between provinces in the same group (e.g. same faction).
|
||||
/// </summary>
|
||||
public class ProvinceBorderManager : MonoBehaviour {
|
||||
[Header("Material")]
|
||||
public Material provinceMapMaterial;
|
||||
|
||||
[Header("Default Borders")]
|
||||
public float defaultBorderWidth = 0.5f;
|
||||
public float defaultBorderWidth = 0.7f;
|
||||
public Color defaultBorderColor = new Color(0.15f, 0.1f, 0.05f, 0.8f);
|
||||
|
||||
private Texture2D _borderColorLookup;
|
||||
private Texture2D _borderWidthLookup;
|
||||
private Texture2D _highlightGroupLookup;
|
||||
private Color[] _borderColors;
|
||||
private Color[] _borderWidths;
|
||||
private Color[] _highlightGroups;
|
||||
private bool _dirty;
|
||||
private bool _groupDirty;
|
||||
|
||||
void Awake() {
|
||||
_borderColorLookup = new Texture2D(256, 1, TextureFormat.RGBA32, false);
|
||||
@@ -35,37 +29,28 @@ namespace eagle {
|
||||
_borderWidthLookup.filterMode = FilterMode.Point;
|
||||
_borderWidthLookup.wrapMode = TextureWrapMode.Clamp;
|
||||
|
||||
_highlightGroupLookup = new Texture2D(256, 1, TextureFormat.RGBA32, false);
|
||||
_highlightGroupLookup.filterMode = FilterMode.Point;
|
||||
_highlightGroupLookup.wrapMode = TextureWrapMode.Clamp;
|
||||
|
||||
_borderColors = new Color[256];
|
||||
_borderWidths = new Color[256];
|
||||
_highlightGroups = new Color[256];
|
||||
for (int i = 0; i < 256; i++) {
|
||||
_borderColors[i] = Color.clear;
|
||||
_borderWidths[i] = Color.clear;
|
||||
_highlightGroups[i] = Color.clear;
|
||||
}
|
||||
|
||||
_borderColorLookup.SetPixels(_borderColors);
|
||||
_borderColorLookup.Apply();
|
||||
_borderWidthLookup.SetPixels(_borderWidths);
|
||||
_borderWidthLookup.Apply();
|
||||
_highlightGroupLookup.SetPixels(_highlightGroups);
|
||||
_highlightGroupLookup.Apply();
|
||||
|
||||
provinceMapMaterial.SetTexture("_BorderColorLookup", _borderColorLookup);
|
||||
provinceMapMaterial.SetTexture("_BorderWidthLookup", _borderWidthLookup);
|
||||
provinceMapMaterial.SetTexture("_HighlightGroupLookup", _highlightGroupLookup);
|
||||
provinceMapMaterial.SetFloat("_DefaultBorderWidth", defaultBorderWidth);
|
||||
provinceMapMaterial.SetColor("_DefaultBorderColor", defaultBorderColor);
|
||||
}
|
||||
|
||||
public void SetProvinceBorder(int provinceId, Color color, float widthScreenPx) {
|
||||
public void SetProvinceBorder(int provinceId, Color color, float widthTexels) {
|
||||
if (provinceId < 0 || provinceId > 255) return;
|
||||
_borderColors[provinceId] = color;
|
||||
_borderWidths[provinceId] = new Color(widthScreenPx / 255f, 0, 0, 0);
|
||||
_borderWidths[provinceId] = new Color(widthTexels / 255f, 0, 0, 0);
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
@@ -76,20 +61,6 @@ namespace eagle {
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
public void SetHighlightGroup(IList<int> provinceIds) {
|
||||
for (int i = 0; i < 256; i++) { _highlightGroups[i] = Color.clear; }
|
||||
var groupColor = new Color(1f / 255f, 0, 0, 0);
|
||||
foreach (var id in provinceIds) {
|
||||
if (id >= 0 && id <= 255) { _highlightGroups[id] = groupColor; }
|
||||
}
|
||||
_groupDirty = true;
|
||||
}
|
||||
|
||||
public void ClearHighlightGroup() {
|
||||
for (int i = 0; i < 256; i++) { _highlightGroups[i] = Color.clear; }
|
||||
_groupDirty = true;
|
||||
}
|
||||
|
||||
void LateUpdate() {
|
||||
if (_dirty) {
|
||||
_borderColorLookup.SetPixels(_borderColors);
|
||||
@@ -98,17 +69,11 @@ namespace eagle {
|
||||
_borderWidthLookup.Apply();
|
||||
_dirty = false;
|
||||
}
|
||||
if (_groupDirty) {
|
||||
_highlightGroupLookup.SetPixels(_highlightGroups);
|
||||
_highlightGroupLookup.Apply();
|
||||
_groupDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_borderColorLookup != null) Destroy(_borderColorLookup);
|
||||
if (_borderWidthLookup != null) Destroy(_borderWidthLookup);
|
||||
if (_highlightGroupLookup != null) Destroy(_highlightGroupLookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-33
@@ -19,10 +19,6 @@ Shader "Eagle/ProvinceMap"
|
||||
_DefaultBorderWidth ("Default Border Width", Float) = 0.35
|
||||
_DefaultBorderColor ("Default Border Color", Color) = (0.15, 0.1, 0.05, 0.8)
|
||||
|
||||
// Same-faction border suppression
|
||||
_CrossProvinceIDMap ("Cross Province ID Map", 2D) = "black" {}
|
||||
_HighlightGroupLookup ("Highlight Group Lookup (256x1)", 2D) = "black" {}
|
||||
|
||||
// UI clipping support
|
||||
_StencilComp ("Stencil Comparison", Float) = 8
|
||||
_Stencil ("Stencil ID", Float) = 0
|
||||
@@ -94,11 +90,8 @@ Shader "Eagle/ProvinceMap"
|
||||
sampler2D _BorderDistanceMap;
|
||||
sampler2D _BorderColorLookup;
|
||||
sampler2D _BorderWidthLookup;
|
||||
sampler2D _CrossProvinceIDMap;
|
||||
sampler2D _HighlightGroupLookup;
|
||||
float4 _ProvinceIDMap_ST;
|
||||
float4 _OceanTex_ST;
|
||||
float4 _BorderDistanceMap_TexelSize; // Unity auto-provides (1/w, 1/h, w, h)
|
||||
fixed4 _OceanColor;
|
||||
float4 _OceanScrollSpeed;
|
||||
float _OceanWaveStrength;
|
||||
@@ -156,38 +149,18 @@ Shader "Eagle/ProvinceMap"
|
||||
// Distance to nearest province boundary (0 = border, 255 = deep interior)
|
||||
float dist = tex2D(_BorderDistanceMap, i.uv).r * 255.0;
|
||||
|
||||
// Resolution-independent scaling: how many texels span one screen pixel
|
||||
float texelsPerPixel = fwidth(i.uv.x) * _BorderDistanceMap_TexelSize.z;
|
||||
|
||||
// Convert screen-pixel widths to texel widths
|
||||
float effectiveDefaultWidth = _DefaultBorderWidth * texelsPerPixel;
|
||||
|
||||
// Per-province highlight border (animated width from C#, in screen pixels)
|
||||
float highlightWidthPx = tex2D(_BorderWidthLookup, float2(provinceId, 0.5)).r * 255.0;
|
||||
float effectiveHighlightWidth = highlightWidthPx * texelsPerPixel;
|
||||
// Per-province highlight border (animated width from C#)
|
||||
float highlightWidth = tex2D(_BorderWidthLookup, float2(provinceId, 0.5)).r * 255.0;
|
||||
fixed4 highlightColor = tex2D(_BorderColorLookup, float2(provinceId, 0.5));
|
||||
|
||||
// Check if the province across the nearest border is in the
|
||||
// same highlight group (e.g. same faction). When both sides
|
||||
// belong to the same group the highlight border is suppressed
|
||||
// so the faction's territory looks like one contiguous region.
|
||||
float crossProvId = tex2D(_CrossProvinceIDMap, i.uv).r;
|
||||
float myGroup = tex2D(_HighlightGroupLookup, float2(provinceId, 0.5)).r;
|
||||
float crossGroup = tex2D(_HighlightGroupLookup, float2(crossProvId, 0.5)).r;
|
||||
bool sameGroup = myGroup > 0.001 && abs(myGroup - crossGroup) < 0.001;
|
||||
|
||||
// Scale anti-aliasing transitions
|
||||
float defaultTransition = 0.5 * texelsPerPixel;
|
||||
float highlightTransition = 1.5 * texelsPerPixel;
|
||||
|
||||
if (effectiveHighlightWidth > 0.5 && dist <= effectiveHighlightWidth && !sameGroup)
|
||||
if (highlightWidth > 0.5 && dist <= highlightWidth)
|
||||
{
|
||||
float edge = 1.0 - saturate((dist - effectiveHighlightWidth + highlightTransition) / highlightTransition);
|
||||
float edge = 1.0 - saturate((dist - highlightWidth + 1.5) / 1.5);
|
||||
color = lerp(fillColor, highlightColor, highlightColor.a * edge);
|
||||
}
|
||||
else if (dist <= effectiveDefaultWidth)
|
||||
else if (dist <= _DefaultBorderWidth)
|
||||
{
|
||||
float edge = 1.0 - saturate((dist - effectiveDefaultWidth + defaultTransition) / defaultTransition);
|
||||
float edge = 1.0 - saturate((dist - _DefaultBorderWidth + 0.5) / 0.5);
|
||||
color = lerp(fillColor, _DefaultBorderColor, _DefaultBorderColor.a * edge);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Berkorszag",
|
||||
"centroid_x": 1767.0,
|
||||
"centroid_y": 1296.0,
|
||||
"centroid_x": 1757.8,
|
||||
"centroid_y": 1231.9,
|
||||
"area": 247272,
|
||||
"orientation": -20.1884829564446,
|
||||
"principal_length": 873.5,
|
||||
@@ -14,8 +14,8 @@
|
||||
{
|
||||
"id": 27,
|
||||
"name": "Chapellia",
|
||||
"centroid_x": 3077.0,
|
||||
"centroid_y": 509.0,
|
||||
"centroid_x": 3076.8,
|
||||
"centroid_y": 508.9,
|
||||
"area": 37546,
|
||||
"orientation": -70.52505264723656,
|
||||
"principal_length": 297.6,
|
||||
@@ -25,8 +25,8 @@
|
||||
{
|
||||
"id": 28,
|
||||
"name": "Kezonoria",
|
||||
"centroid_x": 2215.0,
|
||||
"centroid_y": 1251.0,
|
||||
"centroid_x": 2204.7,
|
||||
"centroid_y": 1263.1,
|
||||
"area": 126885,
|
||||
"orientation": -35.386786918677814,
|
||||
"principal_length": 753.6,
|
||||
@@ -36,7 +36,7 @@
|
||||
{
|
||||
"id": 29,
|
||||
"name": "Mesh",
|
||||
"centroid_x": 2832.0,
|
||||
"centroid_x": 2832.3,
|
||||
"centroid_y": 1017.0,
|
||||
"area": 197788,
|
||||
"orientation": -26.173956537897066,
|
||||
@@ -47,8 +47,8 @@
|
||||
{
|
||||
"id": 36,
|
||||
"name": "Pemeria",
|
||||
"centroid_x": 2835.0,
|
||||
"centroid_y": 1282.0,
|
||||
"centroid_x": 2839.4,
|
||||
"centroid_y": 1290.4,
|
||||
"area": 36356,
|
||||
"orientation": -44.16012147839107,
|
||||
"principal_length": 457.1,
|
||||
@@ -58,8 +58,8 @@
|
||||
{
|
||||
"id": 37,
|
||||
"name": "Yuetia",
|
||||
"centroid_x": 902.0,
|
||||
"centroid_y": 1132.0,
|
||||
"centroid_x": 896.1,
|
||||
"centroid_y": 1227.1,
|
||||
"area": 362211,
|
||||
"orientation": 0.6747576339005641,
|
||||
"principal_length": 1261.0,
|
||||
@@ -69,8 +69,8 @@
|
||||
{
|
||||
"id": 39,
|
||||
"name": "Kojaria",
|
||||
"centroid_x": 3234.0,
|
||||
"centroid_y": 678.0,
|
||||
"centroid_x": 3233.1,
|
||||
"centroid_y": 678.6,
|
||||
"area": 37489,
|
||||
"orientation": -25.04939634407932,
|
||||
"principal_length": 301.9,
|
||||
@@ -80,8 +80,8 @@
|
||||
{
|
||||
"id": 43,
|
||||
"name": "Chia",
|
||||
"centroid_x": 1177.0,
|
||||
"centroid_y": 1499.0,
|
||||
"centroid_x": 1134.5,
|
||||
"centroid_y": 1519.3,
|
||||
"area": 75627,
|
||||
"orientation": -35.91298964206631,
|
||||
"principal_length": 441.6,
|
||||
@@ -91,8 +91,8 @@
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Alah",
|
||||
"centroid_x": 3056.0,
|
||||
"centroid_y": 1330.0,
|
||||
"centroid_x": 3057.6,
|
||||
"centroid_y": 1330.3,
|
||||
"area": 35076,
|
||||
"orientation": -25.515287772906877,
|
||||
"principal_length": 285.6,
|
||||
@@ -102,8 +102,8 @@
|
||||
{
|
||||
"id": 34,
|
||||
"name": "Turton",
|
||||
"centroid_x": 2598.0,
|
||||
"centroid_y": 1500.0,
|
||||
"centroid_x": 2586.7,
|
||||
"centroid_y": 1506.5,
|
||||
"area": 37458,
|
||||
"orientation": -89.27834794069042,
|
||||
"principal_length": 295.6,
|
||||
@@ -113,8 +113,8 @@
|
||||
{
|
||||
"id": 41,
|
||||
"name": "Parnia",
|
||||
"centroid_x": 398.0,
|
||||
"centroid_y": 963.0,
|
||||
"centroid_x": 397.9,
|
||||
"centroid_y": 962.3,
|
||||
"area": 184733,
|
||||
"orientation": 80.31028460303715,
|
||||
"principal_length": 816.0,
|
||||
@@ -124,8 +124,8 @@
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Oryslia",
|
||||
"centroid_x": 2466.0,
|
||||
"centroid_y": 1058.0,
|
||||
"centroid_x": 2494.9,
|
||||
"centroid_y": 1104.4,
|
||||
"area": 108679,
|
||||
"orientation": 53.601133897917535,
|
||||
"principal_length": 673.2,
|
||||
@@ -135,7 +135,7 @@
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Soria",
|
||||
"centroid_x": 611.0,
|
||||
"centroid_x": 611.2,
|
||||
"centroid_y": 587.0,
|
||||
"area": 37567,
|
||||
"orientation": 73.04044050523646,
|
||||
@@ -146,7 +146,7 @@
|
||||
{
|
||||
"id": 26,
|
||||
"name": "Reigia",
|
||||
"centroid_x": 1538.0,
|
||||
"centroid_x": 1538.6,
|
||||
"centroid_y": 824.0,
|
||||
"area": 105738,
|
||||
"orientation": -56.461232070203295,
|
||||
@@ -157,8 +157,8 @@
|
||||
{
|
||||
"id": 21,
|
||||
"name": "Sigkarl",
|
||||
"centroid_x": 1776.0,
|
||||
"centroid_y": 688.0,
|
||||
"centroid_x": 1776.1,
|
||||
"centroid_y": 688.7,
|
||||
"area": 52475,
|
||||
"orientation": -80.26596706568765,
|
||||
"principal_length": 411.4,
|
||||
@@ -168,8 +168,8 @@
|
||||
{
|
||||
"id": 42,
|
||||
"name": "Grytrand",
|
||||
"centroid_x": 1071.0,
|
||||
"centroid_y": 373.0,
|
||||
"centroid_x": 1064.1,
|
||||
"centroid_y": 369.5,
|
||||
"area": 37511,
|
||||
"orientation": 61.866124085696136,
|
||||
"principal_length": 315.1,
|
||||
@@ -179,8 +179,8 @@
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Al Raala",
|
||||
"centroid_x": 3165.0,
|
||||
"centroid_y": 1142.0,
|
||||
"centroid_x": 3164.8,
|
||||
"centroid_y": 1142.9,
|
||||
"area": 64624,
|
||||
"orientation": 1.6109698686653782,
|
||||
"principal_length": 417.0,
|
||||
@@ -190,8 +190,8 @@
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Laufarvia",
|
||||
"centroid_x": 770.0,
|
||||
"centroid_y": 437.0,
|
||||
"centroid_x": 770.1,
|
||||
"centroid_y": 437.8,
|
||||
"area": 37521,
|
||||
"orientation": -42.64914861683819,
|
||||
"principal_length": 253.6,
|
||||
@@ -201,7 +201,7 @@
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Atfordia",
|
||||
"centroid_x": 2474.0,
|
||||
"centroid_x": 2473.6,
|
||||
"centroid_y": 527.0,
|
||||
"area": 78637,
|
||||
"orientation": -15.123132625107454,
|
||||
@@ -212,8 +212,8 @@
|
||||
{
|
||||
"id": 33,
|
||||
"name": "Usvol",
|
||||
"centroid_x": 939.0,
|
||||
"centroid_y": 418.0,
|
||||
"centroid_x": 939.2,
|
||||
"centroid_y": 417.9,
|
||||
"area": 37617,
|
||||
"orientation": 72.87731176765703,
|
||||
"principal_length": 329.4,
|
||||
@@ -223,8 +223,8 @@
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Bolve",
|
||||
"centroid_x": 1554.0,
|
||||
"centroid_y": 568.0,
|
||||
"centroid_x": 1554.3,
|
||||
"centroid_y": 567.8,
|
||||
"area": 37519,
|
||||
"orientation": -28.254514479055615,
|
||||
"principal_length": 297.1,
|
||||
@@ -234,8 +234,8 @@
|
||||
{
|
||||
"id": 17,
|
||||
"name": "Hofolen",
|
||||
"centroid_x": 1192.0,
|
||||
"centroid_y": 497.0,
|
||||
"centroid_x": 1193.4,
|
||||
"centroid_y": 496.5,
|
||||
"area": 37530,
|
||||
"orientation": -40.73421792639953,
|
||||
"principal_length": 321.3,
|
||||
@@ -245,8 +245,8 @@
|
||||
{
|
||||
"id": 15,
|
||||
"name": "Pieksa",
|
||||
"centroid_x": 3372.0,
|
||||
"centroid_y": 753.0,
|
||||
"centroid_x": 3373.6,
|
||||
"centroid_y": 752.0,
|
||||
"area": 37474,
|
||||
"orientation": -42.30480488812397,
|
||||
"principal_length": 302.7,
|
||||
@@ -256,8 +256,8 @@
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Shumal",
|
||||
"centroid_x": 2841.0,
|
||||
"centroid_y": 1426.0,
|
||||
"centroid_x": 2841.2,
|
||||
"centroid_y": 1426.4,
|
||||
"area": 35197,
|
||||
"orientation": -26.08271079122629,
|
||||
"principal_length": 286.9,
|
||||
@@ -267,8 +267,8 @@
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Musland",
|
||||
"centroid_x": 2776.0,
|
||||
"centroid_y": 686.0,
|
||||
"centroid_x": 2776.1,
|
||||
"centroid_y": 685.8,
|
||||
"area": 88353,
|
||||
"orientation": -67.33176588364896,
|
||||
"principal_length": 410.3,
|
||||
@@ -278,8 +278,8 @@
|
||||
{
|
||||
"id": 19,
|
||||
"name": "Hella",
|
||||
"centroid_x": 810.0,
|
||||
"centroid_y": 600.0,
|
||||
"centroid_x": 809.4,
|
||||
"centroid_y": 600.1,
|
||||
"area": 37632,
|
||||
"orientation": -29.964829271657674,
|
||||
"principal_length": 292.4,
|
||||
@@ -289,7 +289,7 @@
|
||||
{
|
||||
"id": 24,
|
||||
"name": "Tatoros",
|
||||
"centroid_x": 2453.0,
|
||||
"centroid_x": 2452.8,
|
||||
"centroid_y": 1508.0,
|
||||
"area": 37547,
|
||||
"orientation": 64.24404335610743,
|
||||
@@ -300,8 +300,8 @@
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Vovimaria",
|
||||
"centroid_x": 613.0,
|
||||
"centroid_y": 807.0,
|
||||
"centroid_x": 608.9,
|
||||
"centroid_y": 818.4,
|
||||
"area": 37474,
|
||||
"orientation": 42.325246247463234,
|
||||
"principal_length": 451.9,
|
||||
@@ -311,8 +311,8 @@
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Onmaa",
|
||||
"centroid_x": 3465.0,
|
||||
"centroid_y": 876.0,
|
||||
"centroid_x": 3464.3,
|
||||
"centroid_y": 864.9,
|
||||
"area": 48302,
|
||||
"orientation": -33.08899725807026,
|
||||
"principal_length": 465.4,
|
||||
@@ -323,7 +323,7 @@
|
||||
"id": 32,
|
||||
"name": "Ingia",
|
||||
"centroid_x": 445.0,
|
||||
"centroid_y": 615.0,
|
||||
"centroid_y": 614.2,
|
||||
"area": 37475,
|
||||
"orientation": 40.330942997303495,
|
||||
"principal_length": 270.3,
|
||||
@@ -333,8 +333,8 @@
|
||||
{
|
||||
"id": 11,
|
||||
"name": "Garholtia",
|
||||
"centroid_x": 768.0,
|
||||
"centroid_y": 807.0,
|
||||
"centroid_x": 768.3,
|
||||
"centroid_y": 805.8,
|
||||
"area": 37509,
|
||||
"orientation": 73.25523522382795,
|
||||
"principal_length": 275.4,
|
||||
@@ -344,8 +344,8 @@
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Motcia",
|
||||
"centroid_x": 2989.0,
|
||||
"centroid_y": 674.0,
|
||||
"centroid_x": 2989.1,
|
||||
"centroid_y": 672.1,
|
||||
"area": 37541,
|
||||
"orientation": 71.01019565314077,
|
||||
"principal_length": 340.1,
|
||||
@@ -355,8 +355,8 @@
|
||||
{
|
||||
"id": 20,
|
||||
"name": "Elekes",
|
||||
"centroid_x": 2355.0,
|
||||
"centroid_y": 1346.0,
|
||||
"centroid_x": 2354.4,
|
||||
"centroid_y": 1346.4,
|
||||
"area": 37570,
|
||||
"orientation": -4.693928802826447,
|
||||
"principal_length": 300.8,
|
||||
@@ -366,8 +366,8 @@
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Oscasland",
|
||||
"centroid_x": 2033.0,
|
||||
"centroid_y": 761.0,
|
||||
"centroid_x": 2033.5,
|
||||
"centroid_y": 740.9,
|
||||
"area": 92595,
|
||||
"orientation": -7.875650568974214,
|
||||
"principal_length": 519.3,
|
||||
@@ -377,8 +377,8 @@
|
||||
{
|
||||
"id": 30,
|
||||
"name": "Tegrot",
|
||||
"centroid_x": 1736.0,
|
||||
"centroid_y": 1018.0,
|
||||
"centroid_x": 1736.5,
|
||||
"centroid_y": 1018.5,
|
||||
"area": 122426,
|
||||
"orientation": -20.247381662688625,
|
||||
"principal_length": 568.2,
|
||||
@@ -387,9 +387,9 @@
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "West Faluria",
|
||||
"centroid_x": 976.0,
|
||||
"centroid_y": 746.0,
|
||||
"name": "Faluria",
|
||||
"centroid_x": 976.7,
|
||||
"centroid_y": 744.3,
|
||||
"area": 80344,
|
||||
"orientation": -62.2520755175581,
|
||||
"principal_length": 494.3,
|
||||
@@ -398,9 +398,9 @@
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "East Faluria",
|
||||
"centroid_x": 1245.0,
|
||||
"centroid_y": 836.0,
|
||||
"name": "Fluria",
|
||||
"centroid_x": 1245.7,
|
||||
"centroid_y": 835.6,
|
||||
"area": 155157,
|
||||
"orientation": -66.31259041323624,
|
||||
"principal_length": 561.7,
|
||||
@@ -410,8 +410,8 @@
|
||||
{
|
||||
"id": 35,
|
||||
"name": "Tumala",
|
||||
"centroid_x": 3210.0,
|
||||
"centroid_y": 1409.0,
|
||||
"centroid_x": 3211.2,
|
||||
"centroid_y": 1409.8,
|
||||
"area": 25474,
|
||||
"orientation": 33.16003147165417,
|
||||
"principal_length": 283.2,
|
||||
@@ -421,8 +421,8 @@
|
||||
{
|
||||
"id": 31,
|
||||
"name": "Nikemi",
|
||||
"centroid_x": 3200.0,
|
||||
"centroid_y": 881.0,
|
||||
"centroid_x": 3200.5,
|
||||
"centroid_y": 882.0,
|
||||
"area": 40737,
|
||||
"orientation": 57.09128390939083,
|
||||
"principal_length": 378.6,
|
||||
@@ -432,8 +432,8 @@
|
||||
{
|
||||
"id": 22,
|
||||
"name": "Chipingia",
|
||||
"centroid_x": 2209.0,
|
||||
"centroid_y": 588.0,
|
||||
"centroid_x": 2206.4,
|
||||
"centroid_y": 580.2,
|
||||
"area": 40656,
|
||||
"orientation": -48.11179717863961,
|
||||
"principal_length": 286.5,
|
||||
@@ -443,8 +443,8 @@
|
||||
{
|
||||
"id": 23,
|
||||
"name": "Eivikia",
|
||||
"centroid_x": 1360.0,
|
||||
"centroid_y": 506.0,
|
||||
"centroid_x": 1359.5,
|
||||
"centroid_y": 506.6,
|
||||
"area": 37555,
|
||||
"orientation": -49.357592556477876,
|
||||
"principal_length": 285.3,
|
||||
@@ -454,8 +454,8 @@
|
||||
{
|
||||
"id": 38,
|
||||
"name": "Wichel",
|
||||
"centroid_x": 2395.0,
|
||||
"centroid_y": 781.0,
|
||||
"centroid_x": 2399.6,
|
||||
"centroid_y": 792.4,
|
||||
"area": 122281,
|
||||
"orientation": -26.154786547398853,
|
||||
"principal_length": 627.0,
|
||||
@@ -465,8 +465,8 @@
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Pozia",
|
||||
"centroid_x": 2960.0,
|
||||
"centroid_y": 410.0,
|
||||
"centroid_x": 2950.6,
|
||||
"centroid_y": 338.6,
|
||||
"area": 56289,
|
||||
"orientation": -65.77001402153415,
|
||||
"principal_length": 522.5,
|
||||
@@ -474,4 +474,4 @@
|
||||
"curvature": -9.3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -3079,8 +3079,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -274.30908}
|
||||
m_SizeDelta: {x: 400, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 200, y: -284.7687}
|
||||
m_SizeDelta: {x: 400, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &13687162
|
||||
MonoBehaviour:
|
||||
@@ -10491,8 +10491,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 300, y: -280.0067}
|
||||
m_SizeDelta: {x: 400, y: 560.0134}
|
||||
m_AnchoredPosition: {x: 300, y: -289.90192}
|
||||
m_SizeDelta: {x: 400, y: 579.80383}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &73634731
|
||||
MonoBehaviour:
|
||||
@@ -11588,8 +11588,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -69.16998}
|
||||
m_SizeDelta: {x: 400, y: 138.33997}
|
||||
m_AnchoredPosition: {x: 200, y: -71.807495}
|
||||
m_SizeDelta: {x: 400, y: 143.61499}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &80678997
|
||||
MonoBehaviour:
|
||||
@@ -14523,8 +14523,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 250, y: -432.6577}
|
||||
m_SizeDelta: {x: 500, y: 560.0134}
|
||||
m_AnchoredPosition: {x: 250, y: -408.7557}
|
||||
m_SizeDelta: {x: 500, y: 579.80383}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &104905777
|
||||
MonoBehaviour:
|
||||
@@ -17846,8 +17846,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 835, y: -540}
|
||||
m_SizeDelta: {x: 1170, y: 1080}
|
||||
m_AnchoredPosition: {x: 1140, y: -540}
|
||||
m_SizeDelta: {x: 1880, y: 1080}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &120552947
|
||||
MonoBehaviour:
|
||||
@@ -24513,8 +24513,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 125, y: 0}
|
||||
m_SizeDelta: {x: 250, y: 1080}
|
||||
m_AnchoredPosition: {x: 100, y: 0}
|
||||
m_SizeDelta: {x: 200, y: 1080}
|
||||
m_Pivot: {x: 0.5, y: 1}
|
||||
--- !u!114 &164094084
|
||||
MonoBehaviour:
|
||||
@@ -24531,7 +24531,7 @@ MonoBehaviour:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 250
|
||||
m_PreferredWidth: 200
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: 0
|
||||
m_FlexibleHeight: 1
|
||||
@@ -25976,8 +25976,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 41.67, y: -9.311031}
|
||||
m_SizeDelta: {x: 9.34, y: 18.622063}
|
||||
m_AnchoredPosition: {x: 41.67, y: -9.856724}
|
||||
m_SizeDelta: {x: 9.34, y: 19.713448}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &176965333
|
||||
MonoBehaviour:
|
||||
@@ -27960,8 +27960,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -183.66301}
|
||||
m_SizeDelta: {x: 400, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 200, y: -190.66623}
|
||||
m_SizeDelta: {x: 400, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &197596693
|
||||
MonoBehaviour:
|
||||
@@ -40899,8 +40899,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -83.485794}
|
||||
m_SizeDelta: {x: 100, y: 14.320574}
|
||||
m_AnchoredPosition: {x: 50, y: -86.66917}
|
||||
m_SizeDelta: {x: 100, y: 14.866628}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &311690665
|
||||
MonoBehaviour:
|
||||
@@ -57080,8 +57080,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -38.162754}
|
||||
m_SizeDelta: {x: 80, y: 76.32551}
|
||||
m_AnchoredPosition: {x: 50, y: -39.617928}
|
||||
m_SizeDelta: {x: 80, y: 79.235855}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &434123775
|
||||
MonoBehaviour:
|
||||
@@ -62598,8 +62598,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 28.331665, y: -29.29331}
|
||||
m_SizeDelta: {x: 34.99, y: 8.586619}
|
||||
m_AnchoredPosition: {x: 28.331665, y: -29.457016}
|
||||
m_SizeDelta: {x: 34.99, y: 8.914034}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &472645058
|
||||
MonoBehaviour:
|
||||
@@ -77779,8 +77779,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 28.331665, y: -29.29331}
|
||||
m_SizeDelta: {x: 34.99, y: 8.586619}
|
||||
m_AnchoredPosition: {x: 28.331665, y: -29.457016}
|
||||
m_SizeDelta: {x: 34.99, y: 8.914034}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &563565458
|
||||
MonoBehaviour:
|
||||
@@ -77939,8 +77939,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 211.66833, y: -21.79331}
|
||||
m_SizeDelta: {x: 56.66333, y: 33.58662}
|
||||
m_AnchoredPosition: {x: 211.66833, y: -21.957018}
|
||||
m_SizeDelta: {x: 56.66333, y: 33.914036}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &563674213
|
||||
MonoBehaviour:
|
||||
@@ -81317,8 +81317,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -38.162754}
|
||||
m_SizeDelta: {x: 80, y: 76.32551}
|
||||
m_AnchoredPosition: {x: 50, y: -39.617928}
|
||||
m_SizeDelta: {x: 80, y: 79.235855}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &569915235
|
||||
MonoBehaviour:
|
||||
@@ -81633,8 +81633,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -24.79331}
|
||||
m_SizeDelta: {x: 400, y: 43.58662}
|
||||
m_AnchoredPosition: {x: 200, y: -24.957018}
|
||||
m_SizeDelta: {x: 400, y: 43.914036}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &572500977
|
||||
MonoBehaviour:
|
||||
@@ -87274,8 +87274,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 21, y: -9.311031}
|
||||
m_SizeDelta: {x: 22, y: 18.622063}
|
||||
m_AnchoredPosition: {x: 21, y: -9.856724}
|
||||
m_SizeDelta: {x: 22, y: 19.713448}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &616739380
|
||||
MonoBehaviour:
|
||||
@@ -91248,8 +91248,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 268.33167, y: -21.79331}
|
||||
m_SizeDelta: {x: 56.66333, y: 33.58662}
|
||||
m_AnchoredPosition: {x: 268.33167, y: -21.957018}
|
||||
m_SizeDelta: {x: 56.66333, y: 33.914036}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &653005024
|
||||
MonoBehaviour:
|
||||
@@ -97122,8 +97122,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -126.4141}
|
||||
m_SizeDelta: {x: 400, y: 23.851719}
|
||||
m_AnchoredPosition: {x: 200, y: -131.23439}
|
||||
m_SizeDelta: {x: 400, y: 24.761206}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &675359729
|
||||
MonoBehaviour:
|
||||
@@ -103291,8 +103291,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 155.005, y: -21.79331}
|
||||
m_SizeDelta: {x: 56.66333, y: 33.58662}
|
||||
m_AnchoredPosition: {x: 155.005, y: -21.957018}
|
||||
m_SizeDelta: {x: 56.66333, y: 33.914036}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &716946128
|
||||
MonoBehaviour:
|
||||
@@ -108957,8 +108957,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 250, y: -896.3322}
|
||||
m_SizeDelta: {x: 500, y: 367.33557}
|
||||
m_AnchoredPosition: {x: 250, y: -889.3288}
|
||||
m_SizeDelta: {x: 500, y: 381.3424}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &752509117
|
||||
MonoBehaviour:
|
||||
@@ -109433,8 +109433,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -38.162754}
|
||||
m_SizeDelta: {x: 80, y: 76.32551}
|
||||
m_AnchoredPosition: {x: 50, y: -39.617928}
|
||||
m_SizeDelta: {x: 80, y: 79.235855}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &756711068
|
||||
MonoBehaviour:
|
||||
@@ -117498,8 +117498,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 324.995, y: -21.79331}
|
||||
m_SizeDelta: {x: 56.66333, y: 33.58662}
|
||||
m_AnchoredPosition: {x: 324.995, y: -21.957018}
|
||||
m_SizeDelta: {x: 56.66333, y: 33.914036}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &828107775
|
||||
MonoBehaviour:
|
||||
@@ -118295,7 +118295,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 374.16333, y: -21.79331}
|
||||
m_AnchoredPosition: {x: 374.16333, y: -21.957018}
|
||||
m_SizeDelta: {x: 41.673332, y: 20}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &838043085
|
||||
@@ -124625,8 +124625,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -38.162754}
|
||||
m_SizeDelta: {x: 80, y: 76.32551}
|
||||
m_AnchoredPosition: {x: 50, y: -39.617928}
|
||||
m_SizeDelta: {x: 80, y: 79.235855}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &875425552
|
||||
MonoBehaviour:
|
||||
@@ -127894,8 +127894,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -83.485794}
|
||||
m_SizeDelta: {x: 100, y: 14.320574}
|
||||
m_AnchoredPosition: {x: 50, y: -86.66917}
|
||||
m_SizeDelta: {x: 100, y: 14.866628}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &893106133
|
||||
MonoBehaviour:
|
||||
@@ -134073,7 +134073,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 1670, y: -540}
|
||||
m_AnchoredPosition: {x: 2330, y: -540}
|
||||
m_SizeDelta: {x: 500, y: 1080}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &921718188
|
||||
@@ -140441,8 +140441,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 105, y: -452.49}
|
||||
m_SizeDelta: {x: 200, y: 30.01}
|
||||
m_AnchoredPosition: {x: 100, y: -487.495}
|
||||
m_SizeDelta: {x: 190, y: 30.01}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &964809175
|
||||
MonoBehaviour:
|
||||
@@ -146820,8 +146820,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -280.0067}
|
||||
m_SizeDelta: {x: 100, y: 560.0134}
|
||||
m_AnchoredPosition: {x: 50, y: -289.90192}
|
||||
m_SizeDelta: {x: 100, y: 579.80383}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1010256813
|
||||
MonoBehaviour:
|
||||
@@ -151227,8 +151227,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 350, y: -45.32304}
|
||||
m_SizeDelta: {x: 100, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 350, y: -47.051243}
|
||||
m_SizeDelta: {x: 100, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1034585958
|
||||
MonoBehaviour:
|
||||
@@ -154428,8 +154428,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -273.7928}
|
||||
m_SizeDelta: {x: 400, y: 133.56963}
|
||||
m_AnchoredPosition: {x: 200, y: -282.66937}
|
||||
m_SizeDelta: {x: 400, y: 138.66275}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1062810388
|
||||
MonoBehaviour:
|
||||
@@ -158727,7 +158727,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 65.83667, y: -21.79331}
|
||||
m_AnchoredPosition: {x: 65.83667, y: -21.957018}
|
||||
m_SizeDelta: {x: 121.67333, y: 18}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1092485181
|
||||
@@ -165981,8 +165981,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 371.83002, y: -14.311031}
|
||||
m_SizeDelta: {x: 56.34, y: 18.622063}
|
||||
m_AnchoredPosition: {x: 371.83002, y: -14.856724}
|
||||
m_SizeDelta: {x: 56.34, y: 19.713448}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1154149037
|
||||
MonoBehaviour:
|
||||
@@ -168546,8 +168546,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 21, y: -9.311031}
|
||||
m_SizeDelta: {x: 22, y: 18.622063}
|
||||
m_AnchoredPosition: {x: 21, y: -9.856724}
|
||||
m_SizeDelta: {x: 22, y: 19.713448}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1171242893
|
||||
MonoBehaviour:
|
||||
@@ -169302,8 +169302,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -57.24412}
|
||||
m_SizeDelta: {x: 120, y: 114.48824}
|
||||
m_AnchoredPosition: {x: 200, y: -59.426895}
|
||||
m_SizeDelta: {x: 120, y: 118.85379}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1173952401
|
||||
MonoBehaviour:
|
||||
@@ -171795,7 +171795,6 @@ MonoBehaviour:
|
||||
mainFont: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
mapArea: {fileID: 1854993665}
|
||||
textures: []
|
||||
bridgeScale: 22
|
||||
labelOutlineColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
labelOutlineWidth: 0.2
|
||||
labelBackingColor: {r: 1, g: 1, b: 0.7411765, a: 1}
|
||||
@@ -180291,8 +180290,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 28.331665, y: -29.29331}
|
||||
m_SizeDelta: {x: 34.99, y: 8.586619}
|
||||
m_AnchoredPosition: {x: 28.331665, y: -29.457016}
|
||||
m_SizeDelta: {x: 34.99, y: 8.914034}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1256975681
|
||||
MonoBehaviour:
|
||||
@@ -181549,8 +181548,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -38.162754}
|
||||
m_SizeDelta: {x: 80, y: 76.32551}
|
||||
m_AnchoredPosition: {x: 50, y: -39.617928}
|
||||
m_SizeDelta: {x: 80, y: 79.235855}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1264915347
|
||||
MonoBehaviour:
|
||||
@@ -182482,8 +182481,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 48.69, y: -9.311031}
|
||||
m_SizeDelta: {x: 23.38, y: 18.622063}
|
||||
m_AnchoredPosition: {x: 48.69, y: -9.856724}
|
||||
m_SizeDelta: {x: 23.38, y: 19.713448}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1271230046
|
||||
MonoBehaviour:
|
||||
@@ -185652,8 +185651,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 250, y: -45.32304}
|
||||
m_SizeDelta: {x: 100, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 250, y: -47.051243}
|
||||
m_SizeDelta: {x: 100, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1285014549
|
||||
MonoBehaviour:
|
||||
@@ -188751,8 +188750,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -354.88864}
|
||||
m_SizeDelta: {x: 400, y: 28.622063}
|
||||
m_AnchoredPosition: {x: 200, y: -366.85745}
|
||||
m_SizeDelta: {x: 400, y: 29.713448}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1313226903
|
||||
MonoBehaviour:
|
||||
@@ -189165,8 +189164,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 5, y: -213.7425}
|
||||
m_SizeDelta: {x: 0, y: 427.485}
|
||||
m_AnchoredPosition: {x: 5, y: -251.25}
|
||||
m_SizeDelta: {x: 0, y: 422.47998}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1315206790
|
||||
MonoBehaviour:
|
||||
@@ -191617,10 +191616,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 921718187}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -19.081375}
|
||||
m_SizeDelta: {x: 0, y: 38.16275}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1338589763
|
||||
MonoBehaviour:
|
||||
@@ -191679,8 +191678,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -128.2973}
|
||||
m_SizeDelta: {x: 400, y: 157.42136}
|
||||
m_AnchoredPosition: {x: 200, y: -131.62602}
|
||||
m_SizeDelta: {x: 400, y: 163.42397}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1339265654
|
||||
MonoBehaviour:
|
||||
@@ -198097,8 +198096,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 136.64, y: -14.311031}
|
||||
m_SizeDelta: {x: 273.28, y: 18.622063}
|
||||
m_AnchoredPosition: {x: 136.64, y: -14.856724}
|
||||
m_SizeDelta: {x: 273.28, y: 19.713448}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1384218058
|
||||
MonoBehaviour:
|
||||
@@ -209537,8 +209536,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -83.485794}
|
||||
m_SizeDelta: {x: 100, y: 14.320574}
|
||||
m_AnchoredPosition: {x: 50, y: -86.66917}
|
||||
m_SizeDelta: {x: 100, y: 14.866628}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1445542184
|
||||
MonoBehaviour:
|
||||
@@ -213833,8 +213832,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -38.162754}
|
||||
m_SizeDelta: {x: 80, y: 76.32551}
|
||||
m_AnchoredPosition: {x: 50, y: -39.617928}
|
||||
m_SizeDelta: {x: 80, y: 79.235855}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1476057067
|
||||
MonoBehaviour:
|
||||
@@ -219616,8 +219615,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -69.16998}
|
||||
m_SizeDelta: {x: 400, y: 138.33997}
|
||||
m_AnchoredPosition: {x: 200, y: -71.807495}
|
||||
m_SizeDelta: {x: 400, y: 143.61499}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1515258299
|
||||
MonoBehaviour:
|
||||
@@ -224394,8 +224393,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -45.32304}
|
||||
m_SizeDelta: {x: 100, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 50, y: -47.051243}
|
||||
m_SizeDelta: {x: 100, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1534189738
|
||||
MonoBehaviour:
|
||||
@@ -229132,8 +229131,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 350, y: -45.32304}
|
||||
m_SizeDelta: {x: 100, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 350, y: -47.051243}
|
||||
m_SizeDelta: {x: 100, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1555765162
|
||||
MonoBehaviour:
|
||||
@@ -234044,8 +234043,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -45.32304}
|
||||
m_SizeDelta: {x: 100, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 50, y: -47.051243}
|
||||
m_SizeDelta: {x: 100, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1594383387
|
||||
MonoBehaviour:
|
||||
@@ -236796,8 +236795,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -343.48386}
|
||||
m_SizeDelta: {x: 400, y: 47.703438}
|
||||
m_AnchoredPosition: {x: 200, y: -356.58115}
|
||||
m_SizeDelta: {x: 400, y: 49.52241}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1615139386
|
||||
MonoBehaviour:
|
||||
@@ -240920,8 +240919,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 250, y: -95.406876}
|
||||
m_SizeDelta: {x: 500, y: 114.48825}
|
||||
m_AnchoredPosition: {x: 250, y: -59.426895}
|
||||
m_SizeDelta: {x: 500, y: 118.85379}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1645185338
|
||||
MonoBehaviour:
|
||||
@@ -241709,8 +241708,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 125, y: -507.5}
|
||||
m_SizeDelta: {x: 240, y: 60.01}
|
||||
m_AnchoredPosition: {x: 100, y: -527.505}
|
||||
m_SizeDelta: {x: 190, y: 30.01}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1658453644
|
||||
MonoBehaviour:
|
||||
@@ -241772,7 +241771,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: Round 1, Thunderstorm
|
||||
m_text: Round 1
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
@@ -243466,8 +243465,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -83.485794}
|
||||
m_SizeDelta: {x: 100, y: 14.320574}
|
||||
m_AnchoredPosition: {x: 50, y: -86.66917}
|
||||
m_SizeDelta: {x: 100, y: 14.866628}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1675788065
|
||||
MonoBehaviour:
|
||||
@@ -247278,7 +247277,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.ProvinceBorderManager
|
||||
provinceMapMaterial: {fileID: 2100000, guid: 2f6dd8226ea6b4be4936cb7658b59fb4, type: 2}
|
||||
defaultBorderWidth: 0.5
|
||||
defaultBorderWidth: 0.7
|
||||
defaultBorderColor: {r: 0.15, g: 0.1, b: 0.05, a: 0.8}
|
||||
--- !u!1 &1712004404
|
||||
GameObject:
|
||||
@@ -252552,8 +252551,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 308.47, y: -14.311031}
|
||||
m_SizeDelta: {x: 70.38, y: 18.622063}
|
||||
m_AnchoredPosition: {x: 308.47, y: -14.856724}
|
||||
m_SizeDelta: {x: 70.38, y: 19.713448}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1754933203
|
||||
MonoBehaviour:
|
||||
@@ -255033,7 +255032,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 250, y: -712.6644}
|
||||
m_AnchoredPosition: {x: 250, y: -698.6576}
|
||||
m_SizeDelta: {x: 500, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1776620757
|
||||
@@ -258758,8 +258757,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -83.485794}
|
||||
m_SizeDelta: {x: 100, y: 14.320574}
|
||||
m_AnchoredPosition: {x: 50, y: -86.66917}
|
||||
m_SizeDelta: {x: 100, y: 14.866628}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1809783484
|
||||
MonoBehaviour:
|
||||
@@ -260702,8 +260701,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -83.485794}
|
||||
m_SizeDelta: {x: 100, y: 14.320574}
|
||||
m_AnchoredPosition: {x: 50, y: -86.66917}
|
||||
m_SizeDelta: {x: 100, y: 14.866628}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1822541216
|
||||
MonoBehaviour:
|
||||
@@ -263927,8 +263926,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 587.5, y: -537.5}
|
||||
m_SizeDelta: {x: 1165, y: 1075}
|
||||
m_AnchoredPosition: {x: 942.5, y: -537.5}
|
||||
m_SizeDelta: {x: 1875, y: 1075}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1854993666
|
||||
MonoBehaviour:
|
||||
@@ -266725,8 +266724,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 150, y: -45.32304}
|
||||
m_SizeDelta: {x: 100, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 150, y: -47.051243}
|
||||
m_SizeDelta: {x: 100, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1875155214
|
||||
MonoBehaviour:
|
||||
@@ -269018,8 +269017,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -24.79331}
|
||||
m_SizeDelta: {x: 400, y: 49.58662}
|
||||
m_AnchoredPosition: {x: 200, y: -24.957018}
|
||||
m_SizeDelta: {x: 400, y: 49.914036}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1893805460
|
||||
MonoBehaviour:
|
||||
@@ -269361,8 +269360,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -83.485794}
|
||||
m_SizeDelta: {x: 100, y: 14.320574}
|
||||
m_AnchoredPosition: {x: 50, y: -86.66917}
|
||||
m_SizeDelta: {x: 100, y: 14.866628}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1895539477
|
||||
MonoBehaviour:
|
||||
@@ -270033,8 +270032,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 300, y: -183.66779}
|
||||
m_SizeDelta: {x: 400, y: 367.33557}
|
||||
m_AnchoredPosition: {x: 300, y: -190.6712}
|
||||
m_SizeDelta: {x: 400, y: 381.3424}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1902923752
|
||||
MonoBehaviour:
|
||||
@@ -273147,8 +273146,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 105, y: -1029.995}
|
||||
m_SizeDelta: {x: 200, y: 90.01}
|
||||
m_AnchoredPosition: {x: 100, y: -1029.995}
|
||||
m_SizeDelta: {x: 190, y: 90.01}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1927059661
|
||||
MonoBehaviour:
|
||||
@@ -276820,8 +276819,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -38.162754}
|
||||
m_SizeDelta: {x: 80, y: 76.32551}
|
||||
m_AnchoredPosition: {x: 50, y: -39.617928}
|
||||
m_SizeDelta: {x: 80, y: 79.235855}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1958376224
|
||||
MonoBehaviour:
|
||||
@@ -283011,8 +283010,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -38.162754}
|
||||
m_SizeDelta: {x: 80, y: 76.32551}
|
||||
m_AnchoredPosition: {x: 50, y: -39.617928}
|
||||
m_SizeDelta: {x: 80, y: 79.235855}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2003668274
|
||||
MonoBehaviour:
|
||||
@@ -287651,8 +287650,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 5, y: -761.2475}
|
||||
m_SizeDelta: {x: 0, y: 427.485}
|
||||
m_AnchoredPosition: {x: 5, y: -763.75}
|
||||
m_SizeDelta: {x: 0, y: 422.47998}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2049862804
|
||||
MonoBehaviour:
|
||||
@@ -288822,8 +288821,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -83.485794}
|
||||
m_SizeDelta: {x: 100, y: 14.320574}
|
||||
m_AnchoredPosition: {x: 50, y: -86.66917}
|
||||
m_SizeDelta: {x: 100, y: 14.866628}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2063183115
|
||||
MonoBehaviour:
|
||||
@@ -290085,8 +290084,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 28.331665, y: -29.29331}
|
||||
m_SizeDelta: {x: 34.99, y: 8.586619}
|
||||
m_AnchoredPosition: {x: 28.331665, y: -29.457016}
|
||||
m_SizeDelta: {x: 34.99, y: 8.914034}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2076969231
|
||||
MonoBehaviour:
|
||||
@@ -293744,8 +293743,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 250, y: -45.32304}
|
||||
m_SizeDelta: {x: 100, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 250, y: -47.051243}
|
||||
m_SizeDelta: {x: 100, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2092022490
|
||||
MonoBehaviour:
|
||||
@@ -294732,8 +294731,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 200, y: -464.60657}
|
||||
m_SizeDelta: {x: 400, y: 190.81375}
|
||||
m_AnchoredPosition: {x: 200, y: -480.759}
|
||||
m_SizeDelta: {x: 400, y: 198.08965}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2100095381
|
||||
MonoBehaviour:
|
||||
@@ -297245,8 +297244,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 150, y: -45.32304}
|
||||
m_SizeDelta: {x: 100, y: 90.64608}
|
||||
m_AnchoredPosition: {x: 150, y: -47.051243}
|
||||
m_SizeDelta: {x: 100, y: 94.102486}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2107423179
|
||||
MonoBehaviour:
|
||||
|
||||
+2
-45
@@ -17,8 +17,8 @@
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "Let me take stock. We have <b>light infantry</b> and <b>longbowmen</b> \u2014 not much, but the longbows can strike from range and they're deadly against Tarn's armored <b>knights</b> and <b>heavy infantry</b>. And he's brought <b>dragoons</b> \u2014 fast cavalry, hard to pin down. We'll need every advantage we can get.",
|
||||
"instructionText": "<sprite name=\"LightInfantry\"> <b>Light Infantry</b> \u2014 Fast, lightly armored\n<sprite name=\"Longbowmen\"> <b>Longbowmen</b> \u2014 Ranged attack, strong vs. armor\n<sprite name=\"HeavyInfantry\"> <b>Heavy Infantry</b> \u2014 Slow, heavily armored (enemy)\n<sprite name=\"HeavyCavalry\"> <b>Knights</b> \u2014 Mounted, powerful charge (enemy)\n<sprite name=\"LightCavalry\"> <b>Dragoons</b> \u2014 Fast cavalry, charge and reposition (enemy)",
|
||||
"dialogueText": "Let me take stock. We have <b>light infantry</b> and <b>longbowmen</b> \u2014 not much, but the longbows can strike from range and they're deadly against Tarn's armored <b>knights</b> and <b>heavy infantry</b>. We'll need them.",
|
||||
"instructionText": "<sprite name=\"LightInfantry\"> <b>Light Infantry</b> \u2014 Fast, lightly armored\n<sprite name=\"Longbowmen\"> <b>Longbowmen</b> \u2014 Ranged attack, strong vs. armor\n<sprite name=\"HeavyInfantry\"> <b>Heavy Infantry</b> \u2014 Slow, heavily armored (enemy)\n<sprite name=\"HeavyCavalry\"> <b>Knights</b> \u2014 Mounted, powerful charge (enemy)",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
@@ -138,49 +138,6 @@
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tutorial_reinforcements",
|
||||
"trigger": "tutorial_reinforcements_arrived",
|
||||
"panelPosition": "top",
|
||||
"steps": [
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "Ah \u2014 Ranil, Elena, Hedrick. I wasn't sure you'd come. Desperate times, it seems, make for unlikely alliances.",
|
||||
"instructionText": null,
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
},
|
||||
{
|
||||
"speakerName": "John Ranil",
|
||||
"speakerImagePath": null,
|
||||
"dialogueText": "I sat out as long as I could, Sadar. Tried to keep my people safe and stay out of it. But Tarn's forces pushed through my province last month and didn't much care who was in the way. So here I am \u2014 and I brought my tools. Point me at something that needs building or breaking.",
|
||||
"instructionText": "<sprite name=\"Engineer\"> <b>Engineer</b> \u2014 Can repair or build bridges and castles, and fortify a position. While fortified, can bombard enemy-held castles from range.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
},
|
||||
{
|
||||
"speakerName": "Elena Fyar",
|
||||
"speakerImagePath": null,
|
||||
"dialogueText": "I warned my father about Tarn years ago, and no one listened. Well \u2014 here we are. I've brought what's left of the Order with me. If your men are wounded, I can help with that. And if Tarn has brought anything... unnatural... I can help with that too.",
|
||||
"instructionText": "<sprite name=\"Paladin\"> <b>Paladin</b> \u2014 Can unleash a Holy Wave that restores vigor to adjacent friendly units and destroys any undead among them.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
},
|
||||
{
|
||||
"speakerName": "Hedrick the Hedge-merchant",
|
||||
"speakerImagePath": null,
|
||||
"dialogueText": "I was trying to sell shrubberies in Ingia when your man found me, Marek. Business has been terrible \u2014 turns out nobody buys decorative hedges during a civil war. But I know every back trail and hollow between here and the coast, and I know how to make myself scarce when I need to.",
|
||||
"instructionText": "<sprite name=\"Ranger\"> <b>Ranger</b> \u2014 Can hide in forests and swamps, ambush enemies emerging from concealment, and scout to reveal hidden enemy positions.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "Sadar Rakon. I had hoped we would never see this day.\n\nYou were once Ikhaan Tarn's most trusted lieutenant \u2014 the right hand of the Shardok himself. But over the past year, Tarn's behavior grew ever more erratic. The paranoia, the inscrutable orders, those strange training exercises no one could explain... and King Bregos would hear none of it.\n\nSo you did what had to be done. You and a handful of others raised the banner of the Reclamation \u2014 not against the Crown, but against the man who wields its sword and can no longer be trusted with it.",
|
||||
"dialogueText": "Sadar Rakon. I had hoped we would never see this day.\n\nYou were once Ikhaan Tarn's most trusted lieutenant \u2014 the right hand of the Shardok himself. But over the past year, Tarn's behavior grew ever more erratic. The cruelty, the paranoia... and King Bregos would hear none of it.\n\nSo you did what had to be done. You and a handful of others raised the banner of the Reclamation \u2014 not against the Crown, but against the madman who wields its sword.",
|
||||
"instructionText": null,
|
||||
"highlightTarget": null,
|
||||
"completionEvent": null
|
||||
|
||||
@@ -66,9 +66,6 @@ public class HexGrid : MonoBehaviour {
|
||||
|
||||
public Texture[] textures;
|
||||
|
||||
[Header("3D Object Settings")]
|
||||
public float bridgeScale = 22f;
|
||||
|
||||
[Header("Label Settings")]
|
||||
public Color labelOutlineColor = Color.white;
|
||||
public float labelOutlineWidth = 0.25f;
|
||||
@@ -797,6 +794,7 @@ public class HexGrid : MonoBehaviour {
|
||||
Vector2 cellCenter = cell.Geometry.AnchoredPosition;
|
||||
GameObject newObject = Instantiate(original: prefab, parent: gridTransform);
|
||||
newObject.transform.localPosition = new Vector3(x: cellCenter.x, y: cellCenter.y, z: -5f);
|
||||
float bridgeScale = 15f;
|
||||
newObject.transform.localScale =
|
||||
new Vector3(x: bridgeScale, y: bridgeScale, z: bridgeScale);
|
||||
// rotationDegrees is already the X rotation (0, 56, or 124)
|
||||
|
||||
@@ -33,7 +33,6 @@ public class ShardokGameModel {
|
||||
|
||||
private readonly Dictionary<HeroId, string> _heroNameTextIds;
|
||||
private readonly List<HeroNameListener> _heroNameListeners = new();
|
||||
private readonly Func<HeroId, (string nameTextId, string imagePath)?> _heroFallbackLookup;
|
||||
|
||||
public struct PlayerWithHostility {
|
||||
public PlayerId playerId;
|
||||
@@ -57,34 +56,6 @@ public class ShardokGameModel {
|
||||
return textId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up hero name text ID and headshot path, falling back to the eagle model
|
||||
/// if the hero wasn't known at battle creation (e.g. mid-battle reinforcements).
|
||||
/// Caches the result locally and registers a ClientTextProvider listener.
|
||||
/// </summary>
|
||||
private (string heroNameTextId, string headshotPath) LookupHero(HeroId hid) {
|
||||
var nameTextId = GetHeroNameTextId(hid);
|
||||
HeroImages.TryGetValue(hid, out var imagePath);
|
||||
|
||||
if (nameTextId == null && imagePath == null && _heroFallbackLookup != null) {
|
||||
var fallback = _heroFallbackLookup(hid);
|
||||
if (fallback.HasValue) {
|
||||
nameTextId = fallback.Value.nameTextId;
|
||||
imagePath = fallback.Value.imagePath;
|
||||
|
||||
if (nameTextId != null) {
|
||||
_heroNameTextIds[hid] = nameTextId;
|
||||
var listener = new HeroNameListener(nameTextId, this);
|
||||
_heroNameListeners.Add(listener);
|
||||
ClientTextProvider.Provider.AddListener(listener);
|
||||
}
|
||||
if (imagePath != null) { HeroImages[hid] = imagePath; }
|
||||
}
|
||||
}
|
||||
|
||||
return (nameTextId, imagePath);
|
||||
}
|
||||
|
||||
public void CleanupListeners() {
|
||||
foreach (var listener in _heroNameListeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
@@ -178,8 +149,7 @@ public class ShardokGameModel {
|
||||
List<PlayerWithHostility> players,
|
||||
Dictionary<HeroId, String> heroNameTextIds,
|
||||
Dictionary<HeroId, String> heroImages,
|
||||
Dictionary<BattalionId, String> battalionNames,
|
||||
Func<HeroId, (string nameTextId, string imagePath)?> heroFallbackLookup = null) {
|
||||
Dictionary<BattalionId, String> battalionNames) {
|
||||
EagleGameId = eagleGameId;
|
||||
ShardokGameId = shardokGameId;
|
||||
History = new List<ActionResultView>(StartingHistoryCapacity);
|
||||
@@ -196,7 +166,6 @@ public class ShardokGameModel {
|
||||
_heroNameTextIds = heroNameTextIds;
|
||||
HeroImages = heroImages;
|
||||
BattalionNames = battalionNames;
|
||||
_heroFallbackLookup = heroFallbackLookup;
|
||||
UnitPlayers = new Dictionary<UnitId, PlayerId>();
|
||||
|
||||
PlayerTotals = players.Select(p => new PlayerTotals { PlayerId = p.playerId }).ToList();
|
||||
@@ -453,7 +422,8 @@ public class ShardokGameModel {
|
||||
string heroNameTextId = null;
|
||||
string headshotPath = null;
|
||||
if (changedUnit.AttachedHero?.EagleHeroId is HeroId hid) {
|
||||
(heroNameTextId, headshotPath) = LookupHero(hid);
|
||||
heroNameTextId = GetHeroNameTextId(hid);
|
||||
HeroImages.TryGetValue(hid, out headshotPath);
|
||||
}
|
||||
|
||||
string battalionName = null;
|
||||
@@ -487,7 +457,8 @@ public class ShardokGameModel {
|
||||
string heroNameTextId = null;
|
||||
string headshotPath = null;
|
||||
if (changedReserveUnit.AttachedHero?.EagleHeroId is HeroId hid) {
|
||||
(heroNameTextId, headshotPath) = LookupHero(hid);
|
||||
heroNameTextId = GetHeroNameTextId(hid);
|
||||
HeroImages.TryGetValue(hid, out headshotPath);
|
||||
}
|
||||
|
||||
string battalionName = null;
|
||||
|
||||
+9
-36
File diff suppressed because one or more lines are too long
+2
-61
@@ -1,9 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using eagle;
|
||||
using eagle0;
|
||||
using Shardok;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
@@ -30,11 +26,9 @@ namespace Eagle0.Tutorial {
|
||||
private DialogueScript _activeScript;
|
||||
private int _currentStepIndex;
|
||||
private MapController _mapController;
|
||||
private ShardokGameController _shardokController;
|
||||
private GameObject _activeHighlight;
|
||||
private readonly List<Image> _highlightEdges = new List<Image>();
|
||||
private readonly HashSet<string> _completedScriptIds = new HashSet<string>();
|
||||
private readonly Queue<string> _pendingTriggers = new Queue<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether a dialogue is currently being displayed.
|
||||
@@ -62,14 +56,6 @@ namespace Eagle0.Tutorial {
|
||||
_mapController = mapController;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Shardok controller reference for resolving hero headshots.
|
||||
/// Called from TutorialManager.Initialize().
|
||||
/// </summary>
|
||||
public void SetShardokController(ShardokGameController shardokController) {
|
||||
_shardokController = shardokController;
|
||||
}
|
||||
|
||||
private void LoadAllScripts() {
|
||||
var jsonFiles = Resources.LoadAll<TextAsset>("Dialogues");
|
||||
foreach (var file in jsonFiles) {
|
||||
@@ -90,21 +76,11 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
/// <summary>
|
||||
/// Fires a trigger. If any loaded dialogue script matches, plays the first one.
|
||||
/// Only fires in tutorial games. If a dialogue is already active, queues the trigger.
|
||||
/// Only fires in tutorial games.
|
||||
/// </summary>
|
||||
public void TriggerDialogue(string trigger) {
|
||||
if (!TutorialManager.IsTutorialGame) return;
|
||||
if (!_triggerMap.ContainsKey(trigger)) return;
|
||||
|
||||
if (_activeScript != null) {
|
||||
_pendingTriggers.Enqueue(trigger);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayTrigger(trigger);
|
||||
}
|
||||
|
||||
private void PlayTrigger(string trigger) {
|
||||
if (_activeScript != null) return;
|
||||
if (!_triggerMap.TryGetValue(trigger, out var scripts)) return;
|
||||
|
||||
foreach (var script in scripts) {
|
||||
@@ -131,16 +107,6 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
var step = _activeScript.steps[_currentStepIndex];
|
||||
|
||||
// Resolve speaker info from game model when headshot not specified in JSON
|
||||
if (string.IsNullOrEmpty(step.speakerImagePath) &&
|
||||
!string.IsNullOrEmpty(step.speakerName)) {
|
||||
var resolved = ResolveSpeaker(step.speakerName);
|
||||
if (resolved.HasValue) {
|
||||
step.speakerImagePath = resolved.Value.headshotPath;
|
||||
step.speakerName = resolved.Value.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
panelController.Show(step);
|
||||
|
||||
// Wire continue button
|
||||
@@ -168,24 +134,6 @@ namespace Eagle0.Tutorial {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a hero's display name and headshot path by name (case-insensitive)
|
||||
/// from the active Shardok game model. Returns null if no match is found.
|
||||
/// </summary>
|
||||
private (string displayName, string headshotPath)? ResolveSpeaker(string speakerName) {
|
||||
var model = _shardokController?.Model;
|
||||
if (model == null) return null;
|
||||
|
||||
var unit = model.UnitsById.Values.Concat(model.ReserveUnitsById.Values)
|
||||
.FirstOrDefault(
|
||||
u => string.Equals(
|
||||
u.Name,
|
||||
speakerName,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
if (unit == null) return null;
|
||||
return (unit.Name, unit.HeadshotPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances to the next dialogue step. Called by the continue button.
|
||||
/// </summary>
|
||||
@@ -221,13 +169,6 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
ClearHighlight();
|
||||
_mapController?.ClearProvinceHighlightProxy();
|
||||
|
||||
// Play next queued dialogue if any
|
||||
while (_pendingTriggers.Count > 0) {
|
||||
var next = _pendingTriggers.Dequeue();
|
||||
PlayTrigger(next);
|
||||
if (_activeScript != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Highlight as child of target ==========
|
||||
|
||||
-2
@@ -68,8 +68,6 @@ namespace Eagle0.Tutorial {
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
speakerHeadshot,
|
||||
step.speakerImagePath);
|
||||
} else {
|
||||
speakerHeadshot.texture = null;
|
||||
}
|
||||
|
||||
// Show/hide instruction section
|
||||
|
||||
+3
-11
@@ -554,11 +554,6 @@ namespace Eagle0.Tutorial {
|
||||
// Combat tutorials
|
||||
case ActionType.ChargeAttack: OnGameEvent("ability_charge_used", result); break;
|
||||
|
||||
// Reinforcement tutorial
|
||||
case ActionType.TutorialReinforcementsArrived:
|
||||
OnGameEvent("tutorial_reinforcements_arrived", result);
|
||||
break;
|
||||
|
||||
// Terrain tutorials
|
||||
case ActionType.FireDamage:
|
||||
case ActionType.FireSpread: OnGameEvent("terrain_fire_encountered", result); break;
|
||||
@@ -591,17 +586,14 @@ namespace Eagle0.Tutorial {
|
||||
case CommandType.RaiseDeadCommand:
|
||||
OnGameEvent("spell_raisedead_available");
|
||||
break;
|
||||
case CommandType.MoveCommand:
|
||||
if (cmd.FollowUpCommandTypes.Contains(CommandType.ChargeCommand)) {
|
||||
OnGameEvent("ability_charge_available");
|
||||
}
|
||||
break;
|
||||
case CommandType.ChargeCommand: OnGameEvent("ability_charge_available"); break;
|
||||
case CommandType.ArcheryCommand: OnGameEvent("archery_available"); break;
|
||||
case CommandType.MeleeCommand: OnGameEvent("melee_available"); break;
|
||||
case CommandType.StartFireCommand:
|
||||
// Only trigger when the target cell has an enemy unit
|
||||
var targetUnit = model.UnitAtCoords(cmd.Target);
|
||||
if (targetUnit != null &&
|
||||
targetUnit.HostilityStatus == Hostility.EnemyHostility) {
|
||||
targetUnit.HostilityStatus != Hostility.SelfHostility) {
|
||||
OnGameEvent("start_fire_available");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -120,7 +120,6 @@ namespace Eagle0.Tutorial {
|
||||
public void Initialize(EagleGameController eagle, ShardokGameController shardok) {
|
||||
_triggerRegistry?.Initialize(eagle, shardok);
|
||||
DialogueManager?.SetMapController(eagle?.mapController);
|
||||
DialogueManager?.SetShardokController(shardok);
|
||||
Log($"TutorialManager initialized with controllers - Eagle: {eagle != null}, Shardok: {shardok != null}");
|
||||
|
||||
// Reset tutorial progress when entering a tutorial game so onboarding
|
||||
|
||||
+19
-19
@@ -6,29 +6,29 @@ EditorUserSettings:
|
||||
serializedVersion: 4
|
||||
m_ConfigSettings:
|
||||
RecentlyUsedSceneGuid-0:
|
||||
value: 540206545d510c590f595d2143770f45444f1b73757e72357c791836e7e5373a
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-1:
|
||||
value: 06525702540c0f595c57547545770c48104e487d7d787062787e4864b7b7606b
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-2:
|
||||
value: 5a000356000d085a555c0d7343770b4241151e2b7e7070647f2d4e35bae46339
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-3:
|
||||
value: 0507035e5c0558095e5a5974477b064047454b7e2d2a2534282f4d62b0b3606d
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-4:
|
||||
value: 0703555650055e0b58565d7a41770643441641797b7b7263757a4931b1e46c6a
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-5:
|
||||
value: 015207020753505e590c597411770a4215164d282a7e7633757f496be4b0316c
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-6:
|
||||
value: 5b02025f520c5a5f0b0d552640770c4946151a2e7d7a75697c7e4f66e1b2306c
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-7:
|
||||
RecentlyUsedSceneGuid-1:
|
||||
value: 540206545d510c590f595d2143770f45444f1b73757e72357c791836e7e5373a
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-2:
|
||||
value: 06525702540c0f595c57547545770c48104e487d7d787062787e4864b7b7606b
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-3:
|
||||
value: 5a000356000d085a555c0d7343770b4241151e2b7e7070647f2d4e35bae46339
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-4:
|
||||
value: 0507035e5c0558095e5a5974477b064047454b7e2d2a2534282f4d62b0b3606d
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-5:
|
||||
value: 0703555650055e0b58565d7a41770643441641797b7b7263757a4931b1e46c6a
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-6:
|
||||
value: 51070104510d505f5c565e2314750e44134e4c7a2a2a71357c714d62b0e2643c
|
||||
flags: 0
|
||||
RecentlyUsedSceneGuid-7:
|
||||
value: 015207020753505e590c597411770a4215164d282a7e7633757f496be4b0316c
|
||||
flags: 0
|
||||
RecentlyUsedScenePath-0:
|
||||
value: 224247031146467c0c0309321c22465e0319113e35
|
||||
flags: 0
|
||||
|
||||
@@ -2,12 +2,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "admin_server_lib",
|
||||
srcs = [
|
||||
"admin_server.go",
|
||||
"map_editor.go",
|
||||
],
|
||||
srcs = ["admin_server.go"],
|
||||
embedsrcs = glob([
|
||||
"static/**",
|
||||
"static/*",
|
||||
"templates/*.html",
|
||||
]),
|
||||
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/admin_server",
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -36,7 +35,7 @@ import (
|
||||
//go:embed templates/*.html
|
||||
var templatesFS embed.FS
|
||||
|
||||
//go:embed all:static
|
||||
//go:embed static/*
|
||||
var staticFS embed.FS
|
||||
|
||||
// Build info variables - set via ldflags
|
||||
@@ -58,15 +57,12 @@ func getBuildInfo() BuildInfo {
|
||||
}
|
||||
}
|
||||
|
||||
var eagleContainerName string
|
||||
|
||||
var (
|
||||
eagleAddr = flag.String("eagle-addr", "localhost:40032", "Eagle gRPC server address")
|
||||
authAddr = flag.String("auth-addr", "localhost:40033", "Auth gRPC server address")
|
||||
authTLS = flag.Bool("auth-tls", false, "Use TLS for auth server connection (required for remote auth servers)")
|
||||
jfrSidecarAddr = flag.String("jfr-sidecar-addr", "localhost:8081", "JFR sidecar HTTP address")
|
||||
httpPort = flag.Int("http-port", 8080, "HTTP server port")
|
||||
mapsFlag = flag.String("maps-dir", "", "Directory containing .e0mj map files (enables map editor)")
|
||||
gameAdminClient gameadminpb.GameAdminClient
|
||||
adminClient adminpb.AdminClient
|
||||
authClient authpb.AuthClient
|
||||
@@ -179,10 +175,6 @@ func main() {
|
||||
log.Fatalf("Failed to parse llm_stats template: %v", err)
|
||||
}
|
||||
|
||||
// Extract container name from eagle-addr (e.g. "eagle-blue:40032" -> "eagle-blue")
|
||||
eagleContainerName = strings.Split(*eagleAddr, ":")[0]
|
||||
log.Printf("Eagle container name: %s", eagleContainerName)
|
||||
|
||||
// Log configuration at startup
|
||||
log.Printf("Configuration: eagle-addr=%s, auth-addr=%s, auth-tls=%v", *eagleAddr, *authAddr, *authTLS)
|
||||
|
||||
@@ -226,12 +218,6 @@ func main() {
|
||||
}
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticContent))))
|
||||
|
||||
// Map editor (optional, requires --maps-dir flag)
|
||||
if *mapsFlag != "" {
|
||||
initMapEditor(*mapsFlag)
|
||||
log.Printf("Map editor enabled, serving maps from %s", *mapsFlag)
|
||||
}
|
||||
|
||||
// Set up HTTP routes
|
||||
// All admin routes require authentication except login/logout/health
|
||||
// accounts.eagle0.net routes for user self-service
|
||||
@@ -264,7 +250,6 @@ func main() {
|
||||
http.HandleFunc("/jfr/start", requireAuth(handleJfrStart))
|
||||
http.HandleFunc("/jfr/stop", requireAuth(handleJfrStop))
|
||||
http.HandleFunc("/jfr/download", requireAuth(handleJfrDownload))
|
||||
http.HandleFunc("/server/restart", requireAuth(handleEagleRestart))
|
||||
http.HandleFunc("/health", handleHealth)
|
||||
http.HandleFunc("/health/status", handleHealthStatus)
|
||||
http.HandleFunc("/notify-update", handleNotifyUpdate)
|
||||
@@ -370,58 +355,6 @@ func jfrDisabled() bool {
|
||||
return *jfrSidecarAddr == ""
|
||||
}
|
||||
|
||||
// dockerClient returns an HTTP client that communicates via the Docker Unix socket.
|
||||
func dockerClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", "/var/run/docker.sock")
|
||||
},
|
||||
},
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// handleEagleRestart restarts the Eagle container via the Docker Engine API.
|
||||
func handleEagleRestart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Eagle restart requested from %s", r.RemoteAddr)
|
||||
|
||||
client := dockerClient()
|
||||
url := fmt.Sprintf("http://localhost/containers/%s/restart?t=30", eagleContainerName)
|
||||
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create restart request: %v", err)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<span class="error">Error: %s</span>`, template.HTMLEscapeString(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Failed to restart Eagle container: %v", err)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<span class="error">Docker unavailable: %s</span>`, template.HTMLEscapeString(err.Error()))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
log.Printf("Eagle container %s restart initiated successfully", eagleContainerName)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprint(w, `<span class="success">Restarting...</span>`)
|
||||
} else {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("Docker API error restarting Eagle: status=%d body=%s", resp.StatusCode, body)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<span class="error">Restart failed (HTTP %d)</span>`, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// handleJfrStatus proxies a JFR status request to the JFR sidecar container
|
||||
func handleJfrStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if jfrDisabled() {
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Map data structures matching the .e0mj JSON schema
|
||||
|
||||
type StructureIntegrity struct {
|
||||
Integrity float64 `json:"integrity"`
|
||||
}
|
||||
|
||||
type TerrainModifier struct {
|
||||
Castle *StructureIntegrity `json:"castle,omitempty"`
|
||||
Bridge *StructureIntegrity `json:"bridge,omitempty"`
|
||||
}
|
||||
|
||||
type TerrainTile struct {
|
||||
Type string `json:"type"`
|
||||
Modifier *TerrainModifier `json:"modifier,omitempty"`
|
||||
}
|
||||
|
||||
type HexPosition struct {
|
||||
Row *int `json:"row,omitempty"`
|
||||
Column *int `json:"column,omitempty"`
|
||||
}
|
||||
|
||||
type StartingPositions struct {
|
||||
Positions []HexPosition `json:"positions,omitempty"`
|
||||
}
|
||||
|
||||
type WeatherData struct {
|
||||
AverageTemperature *int `json:"averageTemperature,omitempty"`
|
||||
SunChance *int `json:"sunChance,omitempty"`
|
||||
CloudsChance *int `json:"cloudsChance,omitempty"`
|
||||
RainChance *int `json:"rainChance,omitempty"`
|
||||
SnowChance *int `json:"snowChance,omitempty"`
|
||||
ThunderstormChance *int `json:"thunderstormChance,omitempty"`
|
||||
BlizzardChance *int `json:"blizzardChance,omitempty"`
|
||||
MaxWindMph *int `json:"maxWindMph,omitempty"`
|
||||
}
|
||||
|
||||
type MapFile struct {
|
||||
RowCount int `json:"rowCount"`
|
||||
ColumnCount int `json:"columnCount"`
|
||||
Terrain []TerrainTile `json:"terrain"`
|
||||
DefenderStartingPositions *StartingPositions `json:"defenderStartingPositions"`
|
||||
AttackerStartingPositions []StartingPositions `json:"attackerStartingPositions"`
|
||||
MonthlyWeather []WeatherData `json:"monthlyWeather"`
|
||||
}
|
||||
|
||||
// Template and page data
|
||||
|
||||
var (
|
||||
mapsDir string
|
||||
mapsTemplate *template.Template
|
||||
mapEditorTemplate *template.Template
|
||||
)
|
||||
|
||||
type MapsPageData struct {
|
||||
Title string
|
||||
BuildInfo BuildInfo
|
||||
Maps []string
|
||||
}
|
||||
|
||||
type MapEditorPageData struct {
|
||||
Title string
|
||||
BuildInfo BuildInfo
|
||||
MapName string
|
||||
}
|
||||
|
||||
func initMapEditor(dir string) {
|
||||
mapsDir = dir
|
||||
|
||||
var err error
|
||||
mapsTemplate, err = template.ParseFS(templatesFS, "templates/layout.html", "templates/maps.html")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse maps template: %v", err)
|
||||
}
|
||||
|
||||
mapEditorTemplate, err = template.ParseFS(templatesFS, "templates/layout.html", "templates/map_editor.html")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse map_editor template: %v", err)
|
||||
}
|
||||
|
||||
http.HandleFunc("/maps", requireAuth(handleMapsPage))
|
||||
http.HandleFunc("/maps/", requireAuth(handleMapEditorRoutes))
|
||||
http.HandleFunc("/api/maps", requireAuth(handleMapsAPI))
|
||||
http.HandleFunc("/api/maps/", requireAuth(handleMapAPI))
|
||||
}
|
||||
|
||||
// Validation
|
||||
|
||||
var validTerrainTypes = map[string]bool{
|
||||
"PLAINS": true,
|
||||
"FOREST": true,
|
||||
"HILL": true,
|
||||
"MOUNTAIN": true,
|
||||
"RIVER": true,
|
||||
"SWAMP": true,
|
||||
"STILL_WATER": true,
|
||||
}
|
||||
|
||||
func validateMap(m *MapFile) error {
|
||||
expectedTiles := m.RowCount * m.ColumnCount
|
||||
if len(m.Terrain) != expectedTiles {
|
||||
return fmt.Errorf("expected %d terrain tiles (%dx%d), got %d", expectedTiles, m.RowCount, m.ColumnCount, len(m.Terrain))
|
||||
}
|
||||
|
||||
for i, tile := range m.Terrain {
|
||||
if !validTerrainTypes[tile.Type] {
|
||||
return fmt.Errorf("tile %d: invalid terrain type %q", i, tile.Type)
|
||||
}
|
||||
if tile.Modifier != nil {
|
||||
if tile.Modifier.Castle != nil && tile.Modifier.Castle.Integrity <= 0 {
|
||||
return fmt.Errorf("tile %d: castle integrity must be > 0", i)
|
||||
}
|
||||
if tile.Modifier.Bridge != nil && tile.Modifier.Bridge.Integrity <= 0 {
|
||||
return fmt.Errorf("tile %d: bridge integrity must be > 0", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.DefenderStartingPositions != nil {
|
||||
if len(m.DefenderStartingPositions.Positions) != 10 {
|
||||
return fmt.Errorf("defender must have exactly 10 starting positions, got %d", len(m.DefenderStartingPositions.Positions))
|
||||
}
|
||||
for i, pos := range m.DefenderStartingPositions.Positions {
|
||||
if err := validatePosition(pos, m.RowCount, m.ColumnCount, "defender", i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.AttackerStartingPositions) != 8 {
|
||||
return fmt.Errorf("must have exactly 8 attacker groups, got %d", len(m.AttackerStartingPositions))
|
||||
}
|
||||
for g, group := range m.AttackerStartingPositions {
|
||||
if len(group.Positions) != 0 && len(group.Positions) != 10 {
|
||||
return fmt.Errorf("attacker group %d must have 0 or 10 positions, got %d", g, len(group.Positions))
|
||||
}
|
||||
for i, pos := range group.Positions {
|
||||
if err := validatePosition(pos, m.RowCount, m.ColumnCount, fmt.Sprintf("attacker group %d", g), i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.MonthlyWeather) != 12 {
|
||||
return fmt.Errorf("must have exactly 12 monthly weather entries, got %d", len(m.MonthlyWeather))
|
||||
}
|
||||
for i, w := range m.MonthlyWeather {
|
||||
sum := intVal(w.SunChance) + intVal(w.CloudsChance) + intVal(w.RainChance) +
|
||||
intVal(w.SnowChance) + intVal(w.ThunderstormChance) + intVal(w.BlizzardChance)
|
||||
if sum != 100 {
|
||||
return fmt.Errorf("month %d: weather chances must sum to 100, got %d", i+1, sum)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePosition(pos HexPosition, rows, cols int, label string, index int) error {
|
||||
if pos.Row != nil && (*pos.Row < 0 || *pos.Row >= rows) {
|
||||
return fmt.Errorf("%s position %d: row %d out of bounds (0-%d)", label, index, *pos.Row, rows-1)
|
||||
}
|
||||
if pos.Column != nil && (*pos.Column < 0 || *pos.Column >= cols) {
|
||||
return fmt.Errorf("%s position %d: column %d out of bounds (0-%d)", label, index, *pos.Column, cols-1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func intVal(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
// File I/O
|
||||
|
||||
func listMapFiles() ([]string, error) {
|
||||
entries, err := os.ReadDir(mapsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".e0mj") {
|
||||
names = append(names, strings.TrimSuffix(e.Name(), ".e0mj"))
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func readMapFile(name string) (*MapFile, error) {
|
||||
data, err := os.ReadFile(mapFilePath(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m MapFile
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func writeMapFile(name string, m *MapFile) error {
|
||||
data, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
return os.WriteFile(mapFilePath(name), data, 0644)
|
||||
}
|
||||
|
||||
func mapFilePath(name string) string {
|
||||
return filepath.Join(mapsDir, name+".e0mj")
|
||||
}
|
||||
|
||||
func sanitizeMapName(name string) string {
|
||||
// Only allow alphanumeric, underscore, hyphen, and spaces
|
||||
var b strings.Builder
|
||||
for _, r := range name {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' || r == ' ' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func isValidMapName(name string) bool {
|
||||
// Reject empty, path traversal, and names with path separators
|
||||
if name == "" || strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
|
||||
return false
|
||||
}
|
||||
return sanitizeMapName(name) == name
|
||||
}
|
||||
|
||||
// HTTP handlers
|
||||
|
||||
func handleMapsPage(w http.ResponseWriter, r *http.Request) {
|
||||
maps, err := listMapFiles()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to list maps: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data := MapsPageData{
|
||||
Title: "Maps",
|
||||
BuildInfo: getBuildInfo(),
|
||||
Maps: maps,
|
||||
}
|
||||
if err := mapsTemplate.Execute(w, data); err != nil {
|
||||
log.Printf("Error rendering maps page: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleMapEditorRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/maps/")
|
||||
if name == "" {
|
||||
http.Redirect(w, r, "/maps", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if !isValidMapName(name) {
|
||||
http.Error(w, "Invalid map name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check map exists
|
||||
if _, err := os.Stat(mapFilePath(name)); os.IsNotExist(err) {
|
||||
http.Error(w, "Map not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
data := MapEditorPageData{
|
||||
Title: "Edit: " + name,
|
||||
BuildInfo: getBuildInfo(),
|
||||
MapName: name,
|
||||
}
|
||||
if err := mapEditorTemplate.Execute(w, data); err != nil {
|
||||
log.Printf("Error rendering map editor page: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleMapsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new blank map
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name := sanitizeMapName(req.Name)
|
||||
if name == "" {
|
||||
http.Error(w, "Invalid map name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mapFilePath(name)); err == nil {
|
||||
http.Error(w, "Map already exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
m := newBlankMap()
|
||||
if err := writeMapFile(name, m); err != nil {
|
||||
http.Error(w, "Failed to create map: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"name": name})
|
||||
}
|
||||
|
||||
func handleMapAPI(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/api/maps/")
|
||||
if name == "" {
|
||||
http.Error(w, "Map name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !isValidMapName(name) {
|
||||
http.Error(w, "Invalid map name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
m, err := readMapFile(name)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "Map not found", http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, "Failed to read map: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(m)
|
||||
|
||||
case http.MethodPut:
|
||||
var m MapFile
|
||||
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
|
||||
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validateMap(&m); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := writeMapFile(name, &m); err != nil {
|
||||
http.Error(w, "Failed to save map: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func newBlankMap() *MapFile {
|
||||
rows := 12
|
||||
cols := 14
|
||||
terrain := make([]TerrainTile, rows*cols)
|
||||
for i := range terrain {
|
||||
terrain[i] = TerrainTile{Type: "PLAINS"}
|
||||
}
|
||||
|
||||
defPositions := make([]HexPosition, 10)
|
||||
for i := range defPositions {
|
||||
r := i
|
||||
c := 0
|
||||
defPositions[i] = HexPosition{Row: &r, Column: &c}
|
||||
}
|
||||
|
||||
attackerGroups := make([]StartingPositions, 8)
|
||||
// First attacker group gets 10 positions, rest empty
|
||||
atkPositions := make([]HexPosition, 10)
|
||||
for i := range atkPositions {
|
||||
r := i
|
||||
c := cols - 1
|
||||
atkPositions[i] = HexPosition{Row: &r, Column: &c}
|
||||
}
|
||||
attackerGroups[0] = StartingPositions{Positions: atkPositions}
|
||||
|
||||
weather := make([]WeatherData, 12)
|
||||
for i := range weather {
|
||||
sun := 40
|
||||
clouds := 30
|
||||
rain := 20
|
||||
thunder := 10
|
||||
wind := 25
|
||||
temp := 60
|
||||
weather[i] = WeatherData{
|
||||
AverageTemperature: &temp,
|
||||
SunChance: &sun,
|
||||
CloudsChance: &clouds,
|
||||
RainChance: &rain,
|
||||
ThunderstormChance: &thunder,
|
||||
MaxWindMph: &wind,
|
||||
}
|
||||
}
|
||||
|
||||
return &MapFile{
|
||||
RowCount: rows,
|
||||
ColumnCount: cols,
|
||||
Terrain: terrain,
|
||||
DefenderStartingPositions: &StartingPositions{Positions: defPositions},
|
||||
AttackerStartingPositions: attackerGroups,
|
||||
MonthlyWeather: weather,
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.editor-breadcrumb {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.editor-actions button {
|
||||
margin: 0;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
#save-status {
|
||||
font-size: 0.85rem;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.editor-layout {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.editor-sidebar {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.editor-sidebar details {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.editor-sidebar summary {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
|
||||
.tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
.tool-btn {
|
||||
padding: 0.3rem 0.2rem;
|
||||
font-size: 0.75rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
background: var(--pico-secondary-background);
|
||||
color: var(--pico-secondary-inverse);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.tool-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.tool-btn.active {
|
||||
border-color: var(--pico-primary);
|
||||
box-shadow: 0 0 0 2px var(--pico-primary);
|
||||
}
|
||||
|
||||
.startpos-tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.startpos-btn {
|
||||
padding: 0.3rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.startpos-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.startpos-btn.active {
|
||||
border-color: var(--pico-primary);
|
||||
box-shadow: 0 0 0 2px var(--pico-primary);
|
||||
}
|
||||
|
||||
.modifier-integrity {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.modifier-integrity input {
|
||||
padding: 0.3rem;
|
||||
margin: 0;
|
||||
}
|
||||
.modifier-integrity label {
|
||||
font-size: 0.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tool-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--pico-muted-color);
|
||||
margin: 0.3rem 0 0;
|
||||
}
|
||||
|
||||
.editor-canvas-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: 4px;
|
||||
background: #f8f9fa;
|
||||
min-height: 500px;
|
||||
}
|
||||
#hex-canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Weather table */
|
||||
.weather-table-container {
|
||||
overflow-x: auto;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.weather-table {
|
||||
font-size: 0.7rem;
|
||||
width: 100%;
|
||||
}
|
||||
.weather-table th,
|
||||
.weather-table td {
|
||||
padding: 2px 3px;
|
||||
text-align: center;
|
||||
}
|
||||
.weather-table input {
|
||||
width: 40px;
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
font-size: 0.7rem;
|
||||
text-align: center;
|
||||
}
|
||||
.weather-sum-ok {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
.weather-sum-bad {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Map list page */
|
||||
.map-create-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.map-create-section fieldset {
|
||||
margin: 0;
|
||||
}
|
||||
.map-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.map-card {
|
||||
margin: 0;
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
.map-card a {
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1,887 +0,0 @@
|
||||
// Map Editor - Hex grid canvas editor for .e0mj files
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────
|
||||
|
||||
const HEX_SIZE = 30;
|
||||
const HEX_W = HEX_SIZE * Math.sqrt(3); // pointy-top hex width
|
||||
const HEX_H = HEX_SIZE * 2; // pointy-top hex height
|
||||
const PADDING = 20;
|
||||
|
||||
const TERRAIN_COLORS = {
|
||||
PLAINS: "#c8e6a0",
|
||||
HILL: "#a0b070",
|
||||
FOREST: "#2d8f2d",
|
||||
MOUNTAIN: "#8a8a8a",
|
||||
SWAMP: "#6b8e6b",
|
||||
STILL_WATER: "#4a90d9",
|
||||
RIVER: "#2070c0",
|
||||
};
|
||||
|
||||
const TERRAIN_LABELS = {
|
||||
PLAINS: "Plains",
|
||||
HILL: "Hill",
|
||||
FOREST: "Forest",
|
||||
MOUNTAIN: "Mtn",
|
||||
SWAMP: "Swamp",
|
||||
STILL_WATER: "Water",
|
||||
RIVER: "River",
|
||||
};
|
||||
|
||||
const MODIFIER_TYPES = [
|
||||
{ key: "castle", label: "Castle" },
|
||||
{ key: "bridge", label: "Bridge" },
|
||||
{ key: "none", label: "Clear Mod" },
|
||||
];
|
||||
|
||||
const STARTPOS_COLORS = [
|
||||
"#2563eb", // defender - blue
|
||||
"#dc2626", // atk 0 - red
|
||||
"#ea580c", // atk 1 - orange
|
||||
"#ca8a04", // atk 2 - yellow
|
||||
"#16a34a", // atk 3 - green
|
||||
"#0891b2", // atk 4 - cyan
|
||||
"#7c3aed", // atk 5 - purple
|
||||
"#db2777", // atk 6 - pink
|
||||
"#78716c", // atk 7 - gray
|
||||
];
|
||||
|
||||
const MONTH_NAMES = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
|
||||
// ── Tile images ───────────────────────────────────────────────────────
|
||||
|
||||
const TILE_NAMES = {
|
||||
PLAINS: "plains",
|
||||
HILL: "hill",
|
||||
FOREST: "forest",
|
||||
MOUNTAIN: "mountain",
|
||||
SWAMP: "swamp",
|
||||
STILL_WATER: "water",
|
||||
RIVER: "river",
|
||||
CASTLE: "castle",
|
||||
};
|
||||
const TERRAIN_IMAGES = {};
|
||||
let imagesLoaded = false;
|
||||
|
||||
function preloadImages(callback) {
|
||||
const keys = Object.keys(TILE_NAMES);
|
||||
let loaded = 0;
|
||||
keys.forEach(function (key) {
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
TERRAIN_IMAGES[key] = img;
|
||||
loaded++;
|
||||
if (loaded === keys.length) {
|
||||
imagesLoaded = true;
|
||||
callback();
|
||||
}
|
||||
};
|
||||
img.onerror = function () {
|
||||
loaded++;
|
||||
if (loaded === keys.length) {
|
||||
imagesLoaded = true;
|
||||
callback();
|
||||
}
|
||||
};
|
||||
img.src = "/static/tiles/" + TILE_NAMES[key] + ".png";
|
||||
});
|
||||
}
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────
|
||||
|
||||
let mapData = null;
|
||||
let selectedTool = "terrain"; // 'terrain' | 'modifier' | 'startpos'
|
||||
let selectedTerrain = "PLAINS";
|
||||
let selectedModifier = "castle";
|
||||
let modifierIntegrity = 100;
|
||||
let selectedStartPosGroup = -1; // -1 = defender, 0-7 = attacker
|
||||
let hoveredTile = -1;
|
||||
let isDirty = false;
|
||||
let isPainting = false;
|
||||
let undoStack = [];
|
||||
let redoStack = [];
|
||||
|
||||
const canvas = document.getElementById("hex-canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// ── Hex geometry (pointy-top, odd-row offset) ─────────────────────────
|
||||
|
||||
function hexCenter(row, col) {
|
||||
const x =
|
||||
PADDING + HEX_W / 2 + col * HEX_W + (row % 2 === 1 ? HEX_W / 2 : 0);
|
||||
const y = PADDING + HEX_SIZE + row * HEX_SIZE * 1.5;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function hexCorners(cx, cy) {
|
||||
const corners = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const angle = (Math.PI / 180) * (30 + 60 * i);
|
||||
corners.push({
|
||||
x: cx + HEX_SIZE * Math.cos(angle),
|
||||
y: cy + HEX_SIZE * Math.sin(angle),
|
||||
});
|
||||
}
|
||||
return corners;
|
||||
}
|
||||
|
||||
function pointInHex(px, py, cx, cy) {
|
||||
// Simple distance check using hex geometry
|
||||
const dx = Math.abs(px - cx);
|
||||
const dy = Math.abs(py - cy);
|
||||
if (dx > HEX_W / 2 || dy > HEX_SIZE) return false;
|
||||
// More precise check using hex boundary
|
||||
return HEX_SIZE * HEX_W / 2 - HEX_SIZE * dx - HEX_W / 2 * dy >= 0;
|
||||
}
|
||||
|
||||
function tileAtPixel(px, py) {
|
||||
if (!mapData) return -1;
|
||||
for (let row = 0; row < mapData.rowCount; row++) {
|
||||
for (let col = 0; col < mapData.columnCount; col++) {
|
||||
const c = hexCenter(row, col);
|
||||
if (pointInHex(px, py, c.x, c.y)) {
|
||||
return row * mapData.columnCount + col;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Drawing ────────────────────────────────────────────────────────────
|
||||
|
||||
function draw() {
|
||||
if (!mapData) return;
|
||||
|
||||
const w =
|
||||
PADDING * 2 +
|
||||
HEX_W * mapData.columnCount +
|
||||
HEX_W / 2;
|
||||
const h = PADDING * 2 + HEX_SIZE * 1.5 * (mapData.rowCount - 1) + HEX_H;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw all hexes
|
||||
for (let row = 0; row < mapData.rowCount; row++) {
|
||||
for (let col = 0; col < mapData.columnCount; col++) {
|
||||
const idx = row * mapData.columnCount + col;
|
||||
const tile = mapData.terrain[idx];
|
||||
const center = hexCenter(row, col);
|
||||
const corners = hexCorners(center.x, center.y);
|
||||
|
||||
// Clip to hex and draw tile image
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(corners[0].x, corners[0].y);
|
||||
for (let i = 1; i < 6; i++) ctx.lineTo(corners[i].x, corners[i].y);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
|
||||
const tileKey = (tile.modifier && tile.modifier.castle) ? "CASTLE" : tile.type;
|
||||
const img = TERRAIN_IMAGES[tileKey];
|
||||
if (img) {
|
||||
// Tile images contain flat-top hexes with 3D depth below.
|
||||
// Crop to just the hex body and scale to fill our pointy-top hex.
|
||||
var srcHexH = Math.sqrt(3) * img.width / 2;
|
||||
var sy = img.height / 2 - srcHexH / 2;
|
||||
var scale = HEX_H / srcHexH;
|
||||
var dw = img.width * scale;
|
||||
ctx.drawImage(img, 0, sy, img.width, srcHexH,
|
||||
center.x - dw / 2, center.y - HEX_SIZE, dw, HEX_H);
|
||||
} else {
|
||||
ctx.fillStyle = TERRAIN_COLORS[tile.type] || "#cccccc";
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Hover highlight
|
||||
if (idx === hoveredTile) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(corners[0].x, corners[0].y);
|
||||
for (let i = 1; i < 6; i++) ctx.lineTo(corners[i].x, corners[i].y);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.3)";
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Border
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(corners[0].x, corners[0].y);
|
||||
for (let i = 1; i < 6; i++) ctx.lineTo(corners[i].x, corners[i].y);
|
||||
ctx.closePath();
|
||||
ctx.strokeStyle = "#555";
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Modifier icon
|
||||
drawModifier(center, tile.modifier);
|
||||
|
||||
// Coordinate label
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.font = "9px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.fillText(row + "," + col, center.x, center.y + HEX_SIZE - 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw starting positions on top
|
||||
drawStartingPositions();
|
||||
}
|
||||
|
||||
function drawModifier(center, modifier) {
|
||||
if (!modifier) return;
|
||||
const s = HEX_SIZE * 0.25;
|
||||
|
||||
if (modifier.castle) {
|
||||
// Castle rendered as tile image; no overlay needed
|
||||
} else if (modifier.bridge) {
|
||||
// Bridge: horizontal line with endcaps
|
||||
ctx.strokeStyle = "#8B4513";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(center.x - s * 1.5, center.y - 4);
|
||||
ctx.lineTo(center.x + s * 1.5, center.y - 4);
|
||||
ctx.stroke();
|
||||
ctx.lineWidth = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function drawStartingPositions() {
|
||||
if (!mapData) return;
|
||||
const r = HEX_SIZE * 0.3;
|
||||
|
||||
// Defender positions
|
||||
if (mapData.defenderStartingPositions && mapData.defenderStartingPositions.positions) {
|
||||
mapData.defenderStartingPositions.positions.forEach(function (pos, i) {
|
||||
drawPosMarker(pos, i, STARTPOS_COLORS[0], r);
|
||||
});
|
||||
}
|
||||
|
||||
// Attacker positions
|
||||
if (mapData.attackerStartingPositions) {
|
||||
mapData.attackerStartingPositions.forEach(function (group, g) {
|
||||
if (group.positions) {
|
||||
group.positions.forEach(function (pos, i) {
|
||||
drawPosMarker(pos, i, STARTPOS_COLORS[g + 1], r);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function drawPosMarker(pos, index, color, r) {
|
||||
const row = pos.row != null ? pos.row : 0;
|
||||
const col = pos.column != null ? pos.column : 0;
|
||||
const center = hexCenter(row, col);
|
||||
|
||||
// Offset slightly so multiple markers on same hex don't fully overlap
|
||||
const ox = (index % 3 - 1) * r * 0.6;
|
||||
const oy = (Math.floor(index / 3) - 1) * r * 0.6;
|
||||
const cx = center.x + ox;
|
||||
const cy = center.y + oy;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#fff";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "bold 8px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(String(index + 1), cx, cy);
|
||||
}
|
||||
|
||||
// ── Editing ────────────────────────────────────────────────────────────
|
||||
|
||||
function applyTool(tileIdx) {
|
||||
if (tileIdx < 0 || !mapData) return;
|
||||
const row = Math.floor(tileIdx / mapData.columnCount);
|
||||
const col = tileIdx % mapData.columnCount;
|
||||
|
||||
if (selectedTool === "terrain") {
|
||||
const old = mapData.terrain[tileIdx].type;
|
||||
if (old === selectedTerrain) return;
|
||||
pushUndo({ type: "terrain", idx: tileIdx, old: old });
|
||||
mapData.terrain[tileIdx].type = selectedTerrain;
|
||||
markDirty();
|
||||
} else if (selectedTool === "modifier") {
|
||||
const oldMod = mapData.terrain[tileIdx].modifier
|
||||
? JSON.parse(JSON.stringify(mapData.terrain[tileIdx].modifier))
|
||||
: null;
|
||||
|
||||
if (selectedModifier === "none") {
|
||||
if (!oldMod) return;
|
||||
pushUndo({ type: "modifier", idx: tileIdx, old: oldMod });
|
||||
mapData.terrain[tileIdx].modifier = undefined;
|
||||
} else {
|
||||
var newMod = {};
|
||||
newMod[selectedModifier] = { integrity: modifierIntegrity };
|
||||
pushUndo({ type: "modifier", idx: tileIdx, old: oldMod });
|
||||
mapData.terrain[tileIdx].modifier = newMod;
|
||||
}
|
||||
markDirty();
|
||||
} else if (selectedTool === "startpos") {
|
||||
toggleStartPos(row, col);
|
||||
}
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
function toggleStartPos(row, col) {
|
||||
var group;
|
||||
if (selectedStartPosGroup === -1) {
|
||||
if (!mapData.defenderStartingPositions) {
|
||||
mapData.defenderStartingPositions = { positions: [] };
|
||||
}
|
||||
group = mapData.defenderStartingPositions;
|
||||
} else {
|
||||
group = mapData.attackerStartingPositions[selectedStartPosGroup];
|
||||
if (!group.positions) {
|
||||
group.positions = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if position already exists
|
||||
var existing = -1;
|
||||
for (var i = 0; i < group.positions.length; i++) {
|
||||
var p = group.positions[i];
|
||||
var pr = p.row != null ? p.row : 0;
|
||||
var pc = p.column != null ? p.column : 0;
|
||||
if (pr === row && pc === col) {
|
||||
existing = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var label =
|
||||
selectedStartPosGroup === -1
|
||||
? "defender"
|
||||
: "attacker " + selectedStartPosGroup;
|
||||
if (existing >= 0) {
|
||||
pushUndo({
|
||||
type: "startpos_remove",
|
||||
group: selectedStartPosGroup,
|
||||
posIndex: existing,
|
||||
old: JSON.parse(JSON.stringify(group.positions[existing])),
|
||||
});
|
||||
group.positions.splice(existing, 1);
|
||||
markDirty();
|
||||
} else if (group.positions.length < 10) {
|
||||
pushUndo({
|
||||
type: "startpos_add",
|
||||
group: selectedStartPosGroup,
|
||||
posIndex: group.positions.length,
|
||||
});
|
||||
group.positions.push({ row: row, column: col });
|
||||
markDirty();
|
||||
}
|
||||
updateStartPosHint();
|
||||
}
|
||||
|
||||
// ── Undo / Redo ────────────────────────────────────────────────────────
|
||||
|
||||
function pushUndo(delta) {
|
||||
undoStack.push(delta);
|
||||
redoStack = [];
|
||||
updateUndoButtons();
|
||||
}
|
||||
|
||||
function editorUndoAction(delta, isRedo) {
|
||||
if (delta.type === "terrain") {
|
||||
var cur = mapData.terrain[delta.idx].type;
|
||||
mapData.terrain[delta.idx].type = delta.old;
|
||||
delta.old = cur;
|
||||
} else if (delta.type === "modifier") {
|
||||
var curMod = mapData.terrain[delta.idx].modifier
|
||||
? JSON.parse(JSON.stringify(mapData.terrain[delta.idx].modifier))
|
||||
: null;
|
||||
mapData.terrain[delta.idx].modifier = delta.old || undefined;
|
||||
delta.old = curMod;
|
||||
} else if (delta.type === "startpos_add") {
|
||||
var g1 = getStartPosGroup(delta.group);
|
||||
delta.old = JSON.parse(
|
||||
JSON.stringify(g1.positions[g1.positions.length - 1])
|
||||
);
|
||||
g1.positions.splice(g1.positions.length - 1, 1);
|
||||
delta.type = "startpos_remove";
|
||||
} else if (delta.type === "startpos_remove") {
|
||||
var g2 = getStartPosGroup(delta.group);
|
||||
g2.positions.splice(delta.posIndex, 0, delta.old);
|
||||
delta.type = "startpos_add";
|
||||
}
|
||||
}
|
||||
|
||||
function getStartPosGroup(groupIdx) {
|
||||
if (groupIdx === -1) return mapData.defenderStartingPositions;
|
||||
return mapData.attackerStartingPositions[groupIdx];
|
||||
}
|
||||
|
||||
window.editorUndo = function () {
|
||||
if (undoStack.length === 0) return;
|
||||
var delta = undoStack.pop();
|
||||
editorUndoAction(delta, false);
|
||||
redoStack.push(delta);
|
||||
markDirty();
|
||||
updateUndoButtons();
|
||||
updateStartPosHint();
|
||||
draw();
|
||||
};
|
||||
|
||||
window.editorRedo = function () {
|
||||
if (redoStack.length === 0) return;
|
||||
var delta = redoStack.pop();
|
||||
editorUndoAction(delta, true);
|
||||
undoStack.push(delta);
|
||||
markDirty();
|
||||
updateUndoButtons();
|
||||
updateStartPosHint();
|
||||
draw();
|
||||
};
|
||||
|
||||
function updateUndoButtons() {
|
||||
document.getElementById("btn-undo").disabled = undoStack.length === 0;
|
||||
document.getElementById("btn-redo").disabled = redoStack.length === 0;
|
||||
}
|
||||
|
||||
// ── Dirty tracking ────────────────────────────────────────────────────
|
||||
|
||||
function markDirty() {
|
||||
isDirty = true;
|
||||
document.getElementById("save-status").textContent = "Unsaved changes";
|
||||
document.getElementById("save-status").style.color = "#ca8a04";
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
if (isDirty) {
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
}
|
||||
});
|
||||
|
||||
// ── Save ───────────────────────────────────────────────────────────────
|
||||
|
||||
window.editorDownload = function () {
|
||||
if (!mapData) return;
|
||||
var json = JSON.stringify(mapData, null, 2) + "\n";
|
||||
var blob = new Blob([json], { type: "application/json" });
|
||||
var a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = MAP_NAME + ".e0mj";
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
};
|
||||
|
||||
window.editorSave = function () {
|
||||
if (!mapData) return;
|
||||
document.getElementById("save-status").textContent = "Saving...";
|
||||
document.getElementById("save-status").style.color = "";
|
||||
|
||||
fetch("/api/maps/" + MAP_NAME, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(mapData),
|
||||
})
|
||||
.then(function (r) {
|
||||
return r.json().then(function (body) {
|
||||
return { ok: r.ok, body: body };
|
||||
});
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.ok) {
|
||||
isDirty = false;
|
||||
document.getElementById("save-status").textContent = "Saved";
|
||||
document.getElementById("save-status").style.color = "green";
|
||||
} else {
|
||||
document.getElementById("save-status").textContent =
|
||||
"Error: " + (result.body.error || "unknown");
|
||||
document.getElementById("save-status").style.color = "red";
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
document.getElementById("save-status").textContent =
|
||||
"Error: " + err.message;
|
||||
document.getElementById("save-status").style.color = "red";
|
||||
});
|
||||
};
|
||||
|
||||
// ── Canvas interaction ─────────────────────────────────────────────────
|
||||
|
||||
canvas.addEventListener("mousedown", function (e) {
|
||||
var rect = canvas.getBoundingClientRect();
|
||||
var tile = tileAtPixel(e.clientX - rect.left, e.clientY - rect.top);
|
||||
if (tile >= 0) {
|
||||
isPainting = true;
|
||||
applyTool(tile);
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousemove", function (e) {
|
||||
var rect = canvas.getBoundingClientRect();
|
||||
var tile = tileAtPixel(e.clientX - rect.left, e.clientY - rect.top);
|
||||
if (tile !== hoveredTile) {
|
||||
hoveredTile = tile;
|
||||
draw();
|
||||
}
|
||||
if (isPainting && tile >= 0 && selectedTool !== "startpos") {
|
||||
applyTool(tile);
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("mouseup", function () {
|
||||
isPainting = false;
|
||||
});
|
||||
|
||||
canvas.addEventListener("mouseleave", function () {
|
||||
isPainting = false;
|
||||
if (hoveredTile !== -1) {
|
||||
hoveredTile = -1;
|
||||
draw();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "z") {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
editorRedo();
|
||||
} else {
|
||||
editorUndo();
|
||||
}
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
editorSave();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Build sidebar tools ────────────────────────────────────────────────
|
||||
|
||||
function buildTerrainTools() {
|
||||
var container = document.getElementById("terrain-tools");
|
||||
Object.keys(TERRAIN_COLORS).forEach(function (key) {
|
||||
var btn = document.createElement("div");
|
||||
btn.className = "tool-btn" + (key === selectedTerrain ? " active" : "");
|
||||
btn.style.backgroundColor = TERRAIN_COLORS[key];
|
||||
btn.style.color =
|
||||
key === "FOREST" || key === "MOUNTAIN" || key === "RIVER" || key === "STILL_WATER"
|
||||
? "#fff"
|
||||
: "#333";
|
||||
btn.textContent = TERRAIN_LABELS[key];
|
||||
btn.dataset.terrain = key;
|
||||
btn.addEventListener("click", function () {
|
||||
selectedTool = "terrain";
|
||||
selectedTerrain = key;
|
||||
refreshToolSelection();
|
||||
});
|
||||
container.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
function buildModifierTools() {
|
||||
var container = document.getElementById("modifier-tools");
|
||||
MODIFIER_TYPES.forEach(function (mod) {
|
||||
var btn = document.createElement("div");
|
||||
btn.className = "tool-btn";
|
||||
btn.textContent = mod.label;
|
||||
btn.dataset.modifier = mod.key;
|
||||
btn.addEventListener("click", function () {
|
||||
selectedTool = "modifier";
|
||||
selectedModifier = mod.key;
|
||||
refreshToolSelection();
|
||||
});
|
||||
container.appendChild(btn);
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("modifier-integrity")
|
||||
.addEventListener("change", function () {
|
||||
modifierIntegrity = Math.max(1, parseInt(this.value) || 100);
|
||||
});
|
||||
}
|
||||
|
||||
function buildStartPosTools() {
|
||||
var container = document.getElementById("startpos-tools");
|
||||
|
||||
// Defender button
|
||||
var defBtn = document.createElement("div");
|
||||
defBtn.className = "startpos-btn";
|
||||
defBtn.style.backgroundColor = STARTPOS_COLORS[0];
|
||||
defBtn.style.color = "#fff";
|
||||
defBtn.textContent = "Defender (0/10)";
|
||||
defBtn.dataset.group = "-1";
|
||||
defBtn.addEventListener("click", function () {
|
||||
selectedTool = "startpos";
|
||||
selectedStartPosGroup = -1;
|
||||
refreshToolSelection();
|
||||
updateStartPosHint();
|
||||
});
|
||||
container.appendChild(defBtn);
|
||||
|
||||
// Attacker buttons
|
||||
for (var i = 0; i < 8; i++) {
|
||||
(function (idx) {
|
||||
var btn = document.createElement("div");
|
||||
btn.className = "startpos-btn";
|
||||
btn.style.backgroundColor = STARTPOS_COLORS[idx + 1];
|
||||
btn.style.color = "#fff";
|
||||
btn.textContent = "Attacker " + idx + " (0/10)";
|
||||
btn.dataset.group = String(idx);
|
||||
btn.addEventListener("click", function () {
|
||||
selectedTool = "startpos";
|
||||
selectedStartPosGroup = idx;
|
||||
refreshToolSelection();
|
||||
updateStartPosHint();
|
||||
});
|
||||
container.appendChild(btn);
|
||||
})(i);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStartPosCounts() {
|
||||
if (!mapData) return;
|
||||
var btns = document.querySelectorAll(".startpos-btn");
|
||||
btns.forEach(function (btn) {
|
||||
var g = parseInt(btn.dataset.group);
|
||||
var count = 0;
|
||||
if (g === -1) {
|
||||
count =
|
||||
mapData.defenderStartingPositions &&
|
||||
mapData.defenderStartingPositions.positions
|
||||
? mapData.defenderStartingPositions.positions.length
|
||||
: 0;
|
||||
btn.textContent = "Defender (" + count + "/10)";
|
||||
} else {
|
||||
var group = mapData.attackerStartingPositions[g];
|
||||
count = group && group.positions ? group.positions.length : 0;
|
||||
btn.textContent = "Attacker " + g + " (" + count + "/10)";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateStartPosHint() {
|
||||
updateStartPosCounts();
|
||||
var hint = document.getElementById("startpos-hint");
|
||||
if (selectedTool !== "startpos") {
|
||||
hint.textContent = "";
|
||||
return;
|
||||
}
|
||||
hint.textContent = "Click hex to add/remove position";
|
||||
}
|
||||
|
||||
function refreshToolSelection() {
|
||||
// Terrain buttons
|
||||
document.querySelectorAll("#terrain-tools .tool-btn").forEach(function (b) {
|
||||
b.classList.toggle(
|
||||
"active",
|
||||
selectedTool === "terrain" && b.dataset.terrain === selectedTerrain
|
||||
);
|
||||
});
|
||||
|
||||
// Modifier buttons
|
||||
document
|
||||
.querySelectorAll("#modifier-tools .tool-btn")
|
||||
.forEach(function (b) {
|
||||
b.classList.toggle(
|
||||
"active",
|
||||
selectedTool === "modifier" && b.dataset.modifier === selectedModifier
|
||||
);
|
||||
});
|
||||
|
||||
// Integrity input visibility
|
||||
document.getElementById("integrity-section").style.display =
|
||||
selectedTool === "modifier" && selectedModifier !== "none"
|
||||
? "block"
|
||||
: "none";
|
||||
|
||||
// Startpos buttons
|
||||
document.querySelectorAll(".startpos-btn").forEach(function (b) {
|
||||
b.classList.toggle(
|
||||
"active",
|
||||
selectedTool === "startpos" &&
|
||||
parseInt(b.dataset.group) === selectedStartPosGroup
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Weather table ──────────────────────────────────────────────────────
|
||||
|
||||
function buildWeatherTable() {
|
||||
var tbody = document.getElementById("weather-body");
|
||||
tbody.innerHTML = "";
|
||||
if (!mapData || !mapData.monthlyWeather) return;
|
||||
|
||||
mapData.monthlyWeather.forEach(function (w, mi) {
|
||||
var tr = document.createElement("tr");
|
||||
|
||||
// Month name
|
||||
var tdMonth = document.createElement("td");
|
||||
tdMonth.textContent = MONTH_NAMES[mi];
|
||||
tr.appendChild(tdMonth);
|
||||
|
||||
var fields = [
|
||||
"sunChance",
|
||||
"cloudsChance",
|
||||
"rainChance",
|
||||
"snowChance",
|
||||
"thunderstormChance",
|
||||
"blizzardChance",
|
||||
];
|
||||
fields.forEach(function (field) {
|
||||
var td = document.createElement("td");
|
||||
var input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.min = "0";
|
||||
input.max = "100";
|
||||
input.value = w[field] != null ? w[field] : 0;
|
||||
input.dataset.month = mi;
|
||||
input.dataset.field = field;
|
||||
input.addEventListener("change", function () {
|
||||
var val = parseInt(this.value) || 0;
|
||||
if (val < 0) val = 0;
|
||||
if (val > 100) val = 100;
|
||||
this.value = val;
|
||||
pushUndo({
|
||||
type: "weather",
|
||||
month: mi,
|
||||
field: field,
|
||||
old: mapData.monthlyWeather[mi][field],
|
||||
});
|
||||
mapData.monthlyWeather[mi][field] = val > 0 ? val : undefined;
|
||||
markDirty();
|
||||
updateWeatherSums();
|
||||
});
|
||||
td.appendChild(input);
|
||||
tr.appendChild(td);
|
||||
});
|
||||
|
||||
// Sum column
|
||||
var tdSum = document.createElement("td");
|
||||
tdSum.className = "weather-sum";
|
||||
tdSum.dataset.month = mi;
|
||||
tr.appendChild(tdSum);
|
||||
|
||||
// Temperature
|
||||
var tdTemp = document.createElement("td");
|
||||
var tempInput = document.createElement("input");
|
||||
tempInput.type = "number";
|
||||
tempInput.value = w.averageTemperature != null ? w.averageTemperature : 0;
|
||||
tempInput.dataset.month = mi;
|
||||
tempInput.dataset.field = "averageTemperature";
|
||||
tempInput.addEventListener("change", function () {
|
||||
var val = parseInt(this.value) || 0;
|
||||
pushUndo({
|
||||
type: "weather",
|
||||
month: mi,
|
||||
field: "averageTemperature",
|
||||
old: mapData.monthlyWeather[mi].averageTemperature,
|
||||
});
|
||||
mapData.monthlyWeather[mi].averageTemperature =
|
||||
val !== 0 ? val : undefined;
|
||||
markDirty();
|
||||
});
|
||||
tdTemp.appendChild(tempInput);
|
||||
tr.appendChild(tdTemp);
|
||||
|
||||
// Wind
|
||||
var tdWind = document.createElement("td");
|
||||
var windInput = document.createElement("input");
|
||||
windInput.type = "number";
|
||||
windInput.min = "0";
|
||||
windInput.value = w.maxWindMph != null ? w.maxWindMph : 0;
|
||||
windInput.dataset.month = mi;
|
||||
windInput.dataset.field = "maxWindMph";
|
||||
windInput.addEventListener("change", function () {
|
||||
var val = parseInt(this.value) || 0;
|
||||
if (val < 0) val = 0;
|
||||
pushUndo({
|
||||
type: "weather",
|
||||
month: mi,
|
||||
field: "maxWindMph",
|
||||
old: mapData.monthlyWeather[mi].maxWindMph,
|
||||
});
|
||||
mapData.monthlyWeather[mi].maxWindMph = val > 0 ? val : undefined;
|
||||
markDirty();
|
||||
});
|
||||
tdWind.appendChild(windInput);
|
||||
tr.appendChild(tdWind);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
updateWeatherSums();
|
||||
}
|
||||
|
||||
function updateWeatherSums() {
|
||||
if (!mapData) return;
|
||||
mapData.monthlyWeather.forEach(function (w, mi) {
|
||||
var sum =
|
||||
(w.sunChance || 0) +
|
||||
(w.cloudsChance || 0) +
|
||||
(w.rainChance || 0) +
|
||||
(w.snowChance || 0) +
|
||||
(w.thunderstormChance || 0) +
|
||||
(w.blizzardChance || 0);
|
||||
var td = document.querySelector(
|
||||
'.weather-sum[data-month="' + mi + '"]'
|
||||
);
|
||||
if (td) {
|
||||
td.textContent = sum;
|
||||
td.className =
|
||||
"weather-sum " + (sum === 100 ? "weather-sum-ok" : "weather-sum-bad");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────────────
|
||||
|
||||
function loadMap() {
|
||||
fetch("/api/maps/" + MAP_NAME)
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("Failed to load map");
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
mapData = data;
|
||||
// Ensure attacker groups have positions arrays
|
||||
if (mapData.attackerStartingPositions) {
|
||||
mapData.attackerStartingPositions.forEach(function (g) {
|
||||
if (!g.positions) g.positions = [];
|
||||
});
|
||||
}
|
||||
buildWeatherTable();
|
||||
updateStartPosCounts();
|
||||
preloadImages(draw);
|
||||
})
|
||||
.catch(function (err) {
|
||||
alert("Failed to load map: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
buildTerrainTools();
|
||||
buildModifierTools();
|
||||
buildStartPosTools();
|
||||
refreshToolSelection();
|
||||
loadMap();
|
||||
})();
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,7 +16,6 @@
|
||||
<li><a href="/accounts">Accounts</a></li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li><a href="/llm-stats">LLM Stats</a></li>
|
||||
<li><a href="/maps">Maps</a></li>
|
||||
<li><a href="/whats-new">What's New</a></li>
|
||||
<li id="health-status" hx-get="/health/status" hx-trigger="load, every 10s" hx-swap="innerHTML">
|
||||
<span class="health-loading">Health: ...</span>
|
||||
@@ -24,10 +23,6 @@
|
||||
<li id="jfr-controls" hx-get="/jfr/status" hx-trigger="load" hx-swap="innerHTML">
|
||||
<span class="jfr-loading">JFR: ...</span>
|
||||
</li>
|
||||
<li>
|
||||
<button onclick="confirmEagleRestart()" class="btn-small secondary outline">Restart Eagle</button>
|
||||
<span id="eagle-restart-result"></span>
|
||||
</li>
|
||||
<li><a href="/logout" class="logout-link">Logout</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -35,18 +30,6 @@
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
|
||||
<!-- Eagle Restart Dialog -->
|
||||
<dialog id="eagle-restart-dialog">
|
||||
<article style="max-width: 400px;">
|
||||
<h3>Restart Eagle Server</h3>
|
||||
<p>This will restart the active Eagle instance. Players may be briefly disconnected.</p>
|
||||
<footer style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
||||
<button class="secondary outline" onclick="document.getElementById('eagle-restart-dialog').close()">Cancel</button>
|
||||
<button onclick="doEagleRestart()">Restart</button>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
|
||||
<!-- JFR Stop Dialog -->
|
||||
<dialog id="jfr-stop-dialog">
|
||||
<article style="max-width: 400px;">
|
||||
@@ -124,16 +107,6 @@
|
||||
function jfrStopCancel() {
|
||||
document.getElementById('jfr-stop-dialog').close();
|
||||
}
|
||||
|
||||
function confirmEagleRestart() {
|
||||
document.getElementById('eagle-restart-dialog').showModal();
|
||||
}
|
||||
|
||||
function doEagleRestart() {
|
||||
document.getElementById('eagle-restart-dialog').close();
|
||||
document.getElementById('eagle-restart-result').innerHTML = 'Restarting...';
|
||||
htmx.ajax('POST', '/server/restart', {target: '#eagle-restart-result', swap: 'innerHTML'});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{{define "content"}}
|
||||
<link rel="stylesheet" href="/static/map_editor.css">
|
||||
<div class="editor-header">
|
||||
<div class="editor-breadcrumb">
|
||||
<a href="/maps">Maps</a> › <strong>{{.MapName}}</strong>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<button id="btn-undo" onclick="editorUndo()" disabled>Undo</button>
|
||||
<button id="btn-redo" onclick="editorRedo()" disabled>Redo</button>
|
||||
<button id="btn-save" onclick="editorSave()" class="contrast">Save</button>
|
||||
<button id="btn-download" onclick="editorDownload()" class="secondary outline">Download</button>
|
||||
<span id="save-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-layout">
|
||||
<aside class="editor-sidebar">
|
||||
<details open>
|
||||
<summary>Terrain</summary>
|
||||
<div class="tool-grid" id="terrain-tools"></div>
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>Modifier</summary>
|
||||
<div class="tool-grid" id="modifier-tools"></div>
|
||||
<div class="modifier-integrity" id="integrity-section" style="display:none;">
|
||||
<label>Integrity
|
||||
<input type="number" id="modifier-integrity" value="100" min="1" step="1">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>Starting Positions</summary>
|
||||
<div class="startpos-tools" id="startpos-tools"></div>
|
||||
<p class="tool-hint" id="startpos-hint"></p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Weather</summary>
|
||||
<div class="weather-table-container">
|
||||
<table class="weather-table" id="weather-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Month</th>
|
||||
<th>Sun</th>
|
||||
<th>Cloud</th>
|
||||
<th>Rain</th>
|
||||
<th>Snow</th>
|
||||
<th>Storm</th>
|
||||
<th>Bliz</th>
|
||||
<th>Sum</th>
|
||||
<th>Temp</th>
|
||||
<th>Wind</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="weather-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
|
||||
<div class="editor-canvas-container">
|
||||
<canvas id="hex-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const MAP_NAME = "{{.MapName}}";
|
||||
</script>
|
||||
<script src="/static/map_editor.js"></script>
|
||||
{{end}}
|
||||
@@ -1,45 +0,0 @@
|
||||
{{define "content"}}
|
||||
<div class="page-header">
|
||||
<h1>Maps</h1>
|
||||
<span>{{len .Maps}} map{{if ne (len .Maps) 1}}s{{end}}</span>
|
||||
</div>
|
||||
|
||||
<div class="map-create-section">
|
||||
<form id="create-map-form" onsubmit="createMap(event)">
|
||||
<fieldset role="group">
|
||||
<input type="text" id="new-map-name" placeholder="New map name..." required>
|
||||
<button type="submit">Create Map</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="map-list">
|
||||
{{range .Maps}}
|
||||
<article class="map-card">
|
||||
<a href="/maps/{{.}}">{{.}}</a>
|
||||
</article>
|
||||
{{else}}
|
||||
<p>No maps found. Create one or check the --maps-dir flag.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function createMap(e) {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('new-map-name').value.trim();
|
||||
if (!name) return;
|
||||
fetch('/api/maps', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name: name})
|
||||
}).then(r => {
|
||||
if (r.ok) return r.json();
|
||||
return r.text().then(t => { throw new Error(t); });
|
||||
}).then(data => {
|
||||
window.location.href = '/maps/' + data.name;
|
||||
}).catch(err => {
|
||||
alert('Failed to create map: ' + err.message);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -1,44 +1,44 @@
|
||||
id name neighbors neighborPositions hexMapName neighborNames castleCount
|
||||
1 Shumal 18.36 7.5 Shumal Alah.Pemeria 3
|
||||
2 Motcia 12.31.27.29.5.39 5.2.4.1.7.0 Motcia Pozia.Nikemi.Chapellia.Mesh.Musland.Kojaria 3
|
||||
3 Laufarvia 40.6.19.33 3.7.1.4 Laufarvia West Faluria.Soria.Hella.Usvol 3
|
||||
4 Berkorszag 28.37.26.30.16.25.38 2.0.6.5.4.7.3 Berkorszag Kezonoria.Yuetia.Reigia.Tegrot.Oscasland.East Faluria.Wichel 3
|
||||
3 Laufarvia 40.6.19.33 3.7.1.4 Laufarvia Faluria.Soria.Hella.Usvol 3
|
||||
4 Berkorszag 28.37.26.30.16.25.38 2.0.6.5.4.7.3 Berkorszag Kezonoria.Yuetia.Reigia.Tegrot.Oscasland.Fluria.Wichel 3
|
||||
5 Musland 38.8.2.12.29 7.6.3.4.1 Musland Wichel.Atfordia.Motcia.Pozia.Mesh 3
|
||||
6 Soria 32.10.11.19.3 7.1.2.3.4 Soria Ingia.Vovimaria.Garholtia.Hella.Laufarvia 2
|
||||
7 Oryslia 29.36.28.34.38 3.2.7.1.5 Oryslia Mesh.Pemeria.Kezonoria.Turton.Wichel 3
|
||||
8 Atfordia 5.38.22.16 3.1.7.0 Atfordia Musland.Wichel.Chipingia.Oscasland 3
|
||||
9 Bolve 26.25.23 1.0.7 Bolve Reigia.East Faluria.Eivikia 3
|
||||
9 Bolve 26.25.23 1.0.7 Bolve Reigia.Fluria.Eivikia 3
|
||||
10 Vovimaria 41.11.6.32.37 7.3.4.5.1 Vovimaria Parnia.Garholtia.Soria.Ingia.Yuetia 5
|
||||
11 Garholtia 37.40.6.10.19 1.3.6.7.5 Garholtia Yuetia.West Faluria.Soria.Vovimaria.Hella 4
|
||||
11 Garholtia 37.40.6.10.19 1.3.6.7.5 Garholtia Yuetia.Faluria.Soria.Vovimaria.Hella 4
|
||||
12 Pozia 5.2.27 0.1.2 Pozia Musland.Motcia.Chapellia 3
|
||||
13 Al Raala 31.18.29.36 4.1.6.7 AlRaala Nikemi.Alah.Mesh.Pemeria 3
|
||||
14 Onmaa 31.15 7.6 Onmaa Nikemi.Pieksa 3
|
||||
15 Pieksa 31.14.39 7.2.5 Pieksa Nikemi.Onmaa.Kojaria 3
|
||||
16 Oscasland 21.8.38.22.4.30 7.3.2.4.1.0 Oscasland Sigkarl.Atfordia.Wichel.Chipingia.Berkorszag.Tegrot 4
|
||||
17 Hofolen 42.40.23.25 6.7.3.1 Hofolen Grytrand.West Faluria.Eivikia.East Faluria 3
|
||||
17 Hofolen 42.40.23.25 6.7.3.1 Hofolen Grytrand.Faluria.Eivikia.Fluria 3
|
||||
18 Alah 13.35.1.36 5.3.0.6 Alah Al Raala.Tumala.Shumal.Pemeria 3
|
||||
19 Hella 11.3.40.6.33 1.5.3.7.2 Hella Garholtia.Laufarvia.West Faluria.Soria.Usvol 3
|
||||
19 Hella 11.3.40.6.33 1.5.3.7.2 Hella Garholtia.Laufarvia.Faluria.Soria.Usvol 3
|
||||
20 Elekes 24.34.28 1.3.5 Elekes Tatoros.Turton.Kezonoria 3
|
||||
21 Sigkarl 16.30.26 3.1.7 Sigkarl Oscasland.Tegrot.Reigia 3
|
||||
22 Chipingia 16.8 7.3 Chipingia Oscasland.Atfordia 3
|
||||
23 Eivikia 17.25.9 7.1.3 Eivikia Hofolen.East Faluria.Bolve 2
|
||||
23 Eivikia 17.25.9 7.1.3 Eivikia Hofolen.Fluria.Bolve 2
|
||||
24 Tatoros 20.34 5.3 Tatoros Elekes.Turton 3
|
||||
25 East Faluria 9.40.23.26.4.37.17 4.7.5.3.2.1.6 East_Faluria Bolve.West Faluria.Eivikia.Reigia.Berkorszag.Yuetia.Hofolen 5
|
||||
26 Reigia 25.21.9.4.30 7.4.5.1.2 Reigia East Faluria.Sigkarl.Bolve.Berkorszag.Tegrot 4
|
||||
25 Fluria 9.40.23.26.4.37.17 4.7.5.3.2.1.6 Fluria Bolve.Faluria.Eivikia.Reigia.Berkorszag.Yuetia.Hofolen 5
|
||||
26 Reigia 25.21.9.4.30 7.4.5.1.2 Reigia Fluria.Sigkarl.Bolve.Berkorszag.Tegrot 4
|
||||
27 Chapellia 2.12.39 7.5.4 Chapellia Motcia.Pozia.Kojaria 4
|
||||
28 Kezonoria 20.4.7.34.38 1.7.3.2.5 Kezonoria Elekes.Berkorszag.Oryslia.Turton.Wichel 3
|
||||
29 Mesh 13.2.5.7.36.31.38 2.4.5.7.1.3.6 Mesh Al Raala.Motcia.Musland.Oryslia.Pemeria.Nikemi.Wichel 7
|
||||
30 Tegrot 16.21.4.26 4.5.1.7 Tegrot Oscasland.Sigkarl.Berkorszag.Reigia 4
|
||||
31 Nikemi 15.39.13.2.29.14 3.4.1.7.0.2 Nikemi Pieksa.Kojaria.Al Raala.Motcia.Mesh.Onmaa 4
|
||||
32 Ingia 6.10.41 3.1.7 Ingia Soria.Vovimaria.Parnia 3
|
||||
33 Usvol 40.3.19.42 1.3.5.4 Usvol West Faluria.Laufarvia.Hella.Grytrand 2
|
||||
33 Usvol 40.3.19.42 1.3.5.4 Usvol Faluria.Laufarvia.Hella.Grytrand 2
|
||||
34 Turton 36.24.7.28.20 3.0.4.5.7 Turton Pemeria.Tatoros.Oryslia.Kezonoria.Elekes 3
|
||||
35 Tumala 18 7 Tumala Alah 3
|
||||
36 Pemeria 7.34.1.18.13.29 6.0.1.2.3.5 Pemeria Oryslia.Turton.Shumal.Alah.Al Raala.Mesh 4
|
||||
37 Yuetia 10.25.40.41.11.4.43 7.4.5.0.6.3.1 Yuetia Vovimaria.East Faluria.West Faluria.Parnia.Garholtia.Berkorszag.Chia 4
|
||||
37 Yuetia 10.25.40.41.11.4.43 7.4.5.0.6.3.1 Yuetia Vovimaria.Fluria.Faluria.Parnia.Garholtia.Berkorszag.Chia 4
|
||||
38 Wichel 29.4.7.28.16.5.8 2.7.1.0.6.3.5 Wichel Mesh.Berkorszag.Oryslia.Kezonoria.Oscasland.Musland.Atfordia 6
|
||||
39 Kojaria 15.31.27.2 2.0.1.3 Kojaria Pieksa.Nikemi.Chapellia.Motcia 4
|
||||
40 West Faluria 37.25.17.42.3.33.19.11 1.2.3.4.6.5.7.0 West_Faluria Yuetia.East Faluria.Hofolen.Grytrand.Laufarvia.Usvol.Hella.Garholtia 3
|
||||
40 Faluria 37.25.17.42.3.33.19.11 1.2.3.4.6.5.7.0 Faluria Yuetia.Fluria.Hofolen.Grytrand.Laufarvia.Usvol.Hella.Garholtia 3
|
||||
41 Parnia 32.37.10 4.2.3 Parnia Ingia.Yuetia.Vovimaria 3
|
||||
42 Grytrand 40.17.33 1.2.3 Grytrand West Faluria.Hofolen.Usvol 2
|
||||
42 Grytrand 40.17.33 1.2.3 Grytrand Faluria.Hofolen.Usvol 2
|
||||
43 Chia 37 5 Chia Yuetia 3
|
||||
|
||||
|
@@ -144,12 +144,11 @@
|
||||
"gold": 50,
|
||||
"food": 2000,
|
||||
"orders": "ENTRUST",
|
||||
"rulingPlayerHeroNames": ["Ikhaan Tarn", "Tall Edgtheow", "Waylaid Julius", "Silvio the Smooth", "Aldric the Overlooked"],
|
||||
"rulingPlayerHeroNames": ["Ikhaan Tarn", "Tall Edgtheow", "Waylaid Julius", "Aldric the Overlooked"],
|
||||
"battalions": [
|
||||
{ "type": "HEAVY_CAVALRY", "size": 592, "training": 80, "armament": 80, "name": "Doomriders" },
|
||||
{ "type": "HEAVY_INFANTRY", "size": 507, "training": 80, "armament": 80, "name": "The Shardok's Guard" },
|
||||
{ "type": "LONGBOWMEN", "size": 291, "training": 80, "armament": 80, "name": "Bowmen of Nikemi" },
|
||||
{ "type": "LIGHT_CAVALRY", "size": 450, "training": 80, "armament": 80, "name": "Swift Sabres" }
|
||||
{ "type": "HEAVY_CAVALRY", "size": 592, "training": 80, "armament": 80, "name": "Heavy Cavalry" },
|
||||
{ "type": "HEAVY_INFANTRY", "size": 507, "training": 80, "armament": 80, "name": "Heavy Infantry" },
|
||||
{ "type": "LONGBOWMEN", "size": 291, "training": 80, "armament": 80, "name": "Longbowmen" }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -83,21 +83,10 @@ case class ResolveBattleAction(
|
||||
battleResolution: BattleResolution
|
||||
): ChangedProvinceC = {
|
||||
val defenderProvince = startingGameState.provinces(battle.defenderProvince)
|
||||
|
||||
// Reinforcement units that survived the battle and should be added to the province
|
||||
val survivingReinforcementUnits = (for {
|
||||
rsp <- battleResolution.resolvedPlayers
|
||||
if defenderProvince.rulingFactionId.contains(rsp.eagleFid)
|
||||
rse <- rsp.units
|
||||
if battle.reinforcementHeroIds.contains(rse.hero.id)
|
||||
if rse.status != UnitStatus.Fled && rse.status != UnitStatus.Outlawed
|
||||
} yield rse).toVector
|
||||
|
||||
ChangedProvinceC(
|
||||
provinceId = battle.defenderProvince,
|
||||
newUnaffiliatedHeroes = newOutlaws(battleResolution),
|
||||
removedHostileArmyFactionIds = battle.players.filterNot(_.isDefender).map(_.eagleFid),
|
||||
newRulingFactionHeroIds = survivingReinforcementUnits.map(_.hero.id),
|
||||
removedRulingFactionHeroIds = (for {
|
||||
rsp <- battleResolution.resolvedPlayers
|
||||
if defenderProvince.rulingFactionId.contains(rsp.eagleFid)
|
||||
@@ -109,7 +98,6 @@ case class ResolveBattleAction(
|
||||
out = rsp
|
||||
) || rse.status == UnitStatus.Outlawed
|
||||
} yield rse.hero.id).toVector,
|
||||
newBattalionIds = survivingReinforcementUnits.flatMap(_.optionalBattalionId),
|
||||
removedBattalionIds = (for {
|
||||
rsp <- battleResolution.resolvedPlayers
|
||||
if defenderProvince.rulingFactionId.contains(rsp.eagleFid)
|
||||
|
||||
+6
-6
@@ -37,9 +37,9 @@ object MapDescription {
|
||||
|Eivikia, southwest of Bolve
|
||||
|Hofolen, west of Eivikia
|
||||
|Grytrand, southwest of Hofolen
|
||||
|West Faluria, northwest of Grytrand and south of Yuetia
|
||||
|Usvol, with a coast on the south but otherwise surrounded by West Faluria
|
||||
|Laufarvia, southwest of West Faluria
|
||||
|Faluria, northwest of Grytrand and south of Yuetia
|
||||
|Usvol, with a coast on the south but otherwise surrounded by Faluria
|
||||
|Laufarvia, southwest of Faluria
|
||||
|Soria, northwest of Laufarvia
|
||||
|Ingia, west of Soria and southeast of Parnia
|
||||
|
|
||||
@@ -48,9 +48,9 @@ object MapDescription {
|
||||
|
|
||||
|IN THE INTERIOR
|
||||
|Vovimaria in the west, bordering Parnia, Yuetia, Garholtia, Soria, and Ingia
|
||||
|Garholtia, east of Vovimaria and also bordering Yuetia, West Faluria, Hella, and Soria
|
||||
|Hella, south of Garholtia and also bordering West Faluria, Laufarvia, and Soria
|
||||
|East Faluria, northeast of West Faluria and also bordering Yuetia, Berkorszag, Reigia, Bolve, Eivikia, and Hofolen
|
||||
|Garholtia, east of Vovimaria and also bordering Yuetia, Faluria, Hella, and Soria
|
||||
|Hella, south of Garholtia and also bordering Faluria, Laufarvia, and Soria
|
||||
|Fluria, northeast of Faluria and also bordering Yuetia, Berkorszag, Reigia, Bolve, Eivikia, and Hofolen
|
||||
|Tegrot, northeast of Reigia and also bordering Berkorszag, Oscasland, and Sigkarl
|
||||
|Wichel, north of Atfordia and also bordering Berkorszag, Kezonoria, Oryslia, Mesh, Musland, and Oscasland
|
||||
|Oryslia, northeast of Wichel and also bordering Kezonoria, Turton, Pemeria and Mesh
|
||||
|
||||
@@ -222,22 +222,11 @@ class LlmResolver(updateReceiver: LlmUpdateQueuingProxy, isGameValid: GameId =>
|
||||
.flatMap(p => providerCallers.get(p).toVector.flatten.map(c => (p, c)))
|
||||
}
|
||||
|
||||
/** Select a caller with capacity, preferring the primary provider */
|
||||
/** Select a caller with capacity, preferring healthy providers */
|
||||
private def selectCaller: Option[(String, ExternalTextGenerationCaller)] = {
|
||||
val primary = LlmProvider.stringValue.toLowerCase
|
||||
val available = getHealthyCallers
|
||||
|
||||
// First, try to find a caller from the primary provider with capacity
|
||||
val primaryWithCapacity = available.filter {
|
||||
case (p, c) => p == primary && c.getInProgressCount < c.maxConcurrentStreams
|
||||
}.minByOption { case (_, c) => c.getInProgressCount }
|
||||
|
||||
// Only fall back to other providers if primary has no capacity
|
||||
val selected = primaryWithCapacity.orElse {
|
||||
available.filter { case (_, c) => c.getInProgressCount < c.maxConcurrentStreams }.minByOption {
|
||||
case (_, c) => c.getInProgressCount
|
||||
}
|
||||
}
|
||||
val available = getHealthyCallers
|
||||
val withCapacity = available.filter { case (_, c) => c.getInProgressCount < c.maxConcurrentStreams }
|
||||
val selected = withCapacity.minByOption { case (_, c) => c.getInProgressCount }
|
||||
|
||||
if selected.isEmpty && available.nonEmpty then {
|
||||
val capacityInfo = available
|
||||
|
||||
@@ -370,21 +370,6 @@ object PersistedHistory {
|
||||
persister.delete(key)
|
||||
}
|
||||
|
||||
/** Delete .e0s files for battles not in the valid set (cleanup after rewind). */
|
||||
private def deleteOrphanedShardokFiles(
|
||||
persister: Persister,
|
||||
validBattleIds: Set[String]
|
||||
): Unit =
|
||||
persister.existingKeys
|
||||
.filter(_.endsWith(persistence.shardokExtension))
|
||||
.foreach { key =>
|
||||
val shardokId = key.dropRight(persistence.shardokExtension.length)
|
||||
if !validBattleIds.contains(shardokId) then {
|
||||
println(s"[Rewind] Deleting orphaned shardok battle file: $key")
|
||||
val _ = persister.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
/** Load any orphaned individual action results (for crash recovery). */
|
||||
private def loadIndividualResults(
|
||||
persister: Persister,
|
||||
@@ -697,9 +682,8 @@ final case class PersistedHistory(
|
||||
)
|
||||
|
||||
if targetActionCount == count then this
|
||||
else if targetActionCount == 0 then {
|
||||
else if targetActionCount == 0 then
|
||||
// Reset to initial state
|
||||
PersistedHistory.deleteOrphanedShardokFiles(persister, Set.empty)
|
||||
PersistedHistory(
|
||||
startingState = GameStateProto(
|
||||
runStatus = RunStatus.RUN_STATUS_RUNNING,
|
||||
@@ -712,33 +696,23 @@ final case class PersistedHistory(
|
||||
persistedCount = 0,
|
||||
persister = persister
|
||||
)
|
||||
} else if targetActionCount > persistedCount then {
|
||||
else if targetActionCount > persistedCount then
|
||||
// Target is within recent history, just truncate recentHistory
|
||||
val truncatedHistory = recentHistory.take(targetActionCount - persistedCount)
|
||||
val validBattleIds = truncatedHistory.last.scalaGameState.outstandingBattles.map(_.shardokGameId).toSet
|
||||
PersistedHistory.deleteOrphanedShardokFiles(persister, validBattleIds)
|
||||
copy(
|
||||
recentHistory = truncatedHistory,
|
||||
shardokHistory = Vector()
|
||||
recentHistory = recentHistory.take(targetActionCount - persistedCount),
|
||||
shardokHistory = Vector() // Clear Shardok battles since they may be invalid
|
||||
)
|
||||
} else {
|
||||
else {
|
||||
// Target is within persisted history - need to reload from disk
|
||||
val targetStartingIndex = (targetActionCount / resultsPerSaveFile) * resultsPerSaveFile
|
||||
val pgs = PartialGameUtils.partialGames(persister, targetStartingIndex, persistedCount)
|
||||
val newStartingState = pgs.head.startingState.get
|
||||
val allResults = pgs.flatMap(_.results)
|
||||
val resultsToKeep = allResults.take(targetActionCount - targetStartingIndex)
|
||||
val newRecentHistory = PersistedHistory.formAwrs(newStartingState, resultsToKeep)
|
||||
|
||||
val finalState =
|
||||
if newRecentHistory.nonEmpty then newRecentHistory.last.scalaGameState
|
||||
else GameStateConverter.fromProto(newStartingState)
|
||||
val validBattleIds = finalState.outstandingBattles.map(_.shardokGameId).toSet
|
||||
PersistedHistory.deleteOrphanedShardokFiles(persister, validBattleIds)
|
||||
|
||||
copy(
|
||||
startingState = newStartingState,
|
||||
recentHistory = newRecentHistory,
|
||||
recentHistory = PersistedHistory.formAwrs(newStartingState, resultsToKeep),
|
||||
shardokHistory = Vector(),
|
||||
persistedCount = targetStartingIndex
|
||||
)
|
||||
|
||||
@@ -168,7 +168,6 @@ scala_library(
|
||||
deps = [
|
||||
":game_parameters_utils",
|
||||
":great_person",
|
||||
":loaded_hero_conversion",
|
||||
":start_date",
|
||||
":start_game_action_result_utils",
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_scala_proto",
|
||||
@@ -185,12 +184,10 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:order_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:army",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:game_type",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:supplies",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/battalion/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
|
||||
+40
-60
@@ -11,7 +11,6 @@ import net.eagle0.eagle.model.proto_converters.hero.ProfessionConverter
|
||||
import net.eagle0.eagle.model.proto_converters.province.ProvinceOrderTypeConverter
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.model.state.*
|
||||
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
|
||||
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
|
||||
import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
import net.eagle0.eagle.model.state.hero.concrete.HeroC
|
||||
@@ -243,39 +242,6 @@ object TutorialGameCreation {
|
||||
)
|
||||
)
|
||||
|
||||
// Create reinforcement heroes and battalions for the tutorial
|
||||
// Use heroIds/battalionIds starting at 100+ to avoid conflicts with regular game entities
|
||||
val hedrickHero = findHero("Hedrick the Hedge-Merchant")
|
||||
val johnRanilHero = findHero("John Ranil")
|
||||
val elenaFyarHero = findHero("Elena Fyar")
|
||||
|
||||
// Define reinforcement data: (LoadedHero, heroId, battalionId, battalionTypeId, size, training, armament)
|
||||
val reinforcementData = Vector(
|
||||
(hedrickHero, 100, 100, BattalionTypeId.Longbowmen, 600, 75.0, 75.0),
|
||||
(johnRanilHero, 101, 101, BattalionTypeId.HeavyCavalry, 500, 80.0, 80.0),
|
||||
(elenaFyarHero, 102, 102, BattalionTypeId.HeavyInfantry, 600, 75.0, 75.0)
|
||||
)
|
||||
|
||||
// Create HeroC objects for reinforcement heroes (assigned to player faction but not to any province)
|
||||
val reinforcementHeroes = reinforcementData.map {
|
||||
case (loadedHero, heroId, _, _, _, _, _) =>
|
||||
LoadedHeroConversion
|
||||
.toHeroC(loadedHero.withFactionId(playerFid))
|
||||
.copy(id = heroId)
|
||||
}
|
||||
|
||||
// Create BattalionC objects for reinforcement battalions
|
||||
val reinforcementBattalions = reinforcementData.map {
|
||||
case (_, _, battalionId, battalionTypeId, size, training, armament) =>
|
||||
BattalionC(
|
||||
id = battalionId,
|
||||
typeId = battalionTypeId,
|
||||
size = size,
|
||||
training = training,
|
||||
armament = armament
|
||||
)
|
||||
}
|
||||
|
||||
val actionResult = withBattleSetup.copy(
|
||||
newBattalionTypes = battalionTypes,
|
||||
newChronicleEntry = Some(
|
||||
@@ -283,10 +249,7 @@ object TutorialGameCreation {
|
||||
date = StartDate.startDate,
|
||||
generatedTextId = "initial_chronicle_entry"
|
||||
)
|
||||
),
|
||||
// Add reinforcement heroes and battalions to the game state
|
||||
newHeroes = withBattleSetup.newHeroes ++ reinforcementHeroes,
|
||||
newBattalions = withBattleSetup.newBattalions ++ reinforcementBattalions
|
||||
)
|
||||
)
|
||||
|
||||
val heroGenerator = HeroGenerator(
|
||||
@@ -309,28 +272,45 @@ object TutorialGameCreation {
|
||||
results = Vector(actionResult)
|
||||
)
|
||||
|
||||
// Create CommonUnit objects for Shardok (uses the same data as the Eagle state objects above)
|
||||
val reinforcementUnits = reinforcementData.map {
|
||||
case (loadedHero, heroId, battalionId, battalionTypeId, size, training, armament) =>
|
||||
val commonBattalionType = battalionTypeId match {
|
||||
case BattalionTypeId.Longbowmen => CommonBattalionTypeId.LONGBOWMEN
|
||||
case BattalionTypeId.HeavyCavalry => CommonBattalionTypeId.HEAVY_CAVALRY
|
||||
case BattalionTypeId.HeavyInfantry => CommonBattalionTypeId.HEAVY_INFANTRY
|
||||
case BattalionTypeId.LightCavalry => CommonBattalionTypeId.LIGHT_CAVALRY
|
||||
case BattalionTypeId.LightInfantry => CommonBattalionTypeId.LIGHT_INFANTRY
|
||||
case _ => CommonBattalionTypeId.LIGHT_INFANTRY
|
||||
}
|
||||
createReinforcementUnit(
|
||||
loadedHero = loadedHero,
|
||||
heroId = heroId,
|
||||
battalionId = battalionId,
|
||||
factionId = playerFid,
|
||||
battalionType = commonBattalionType,
|
||||
battalionSize = size,
|
||||
training = training,
|
||||
armament = armament
|
||||
)
|
||||
}
|
||||
// Create reinforcement units for the player
|
||||
// Use heroIds starting after existing heroes (100+ to avoid conflicts)
|
||||
// Use battalionIds starting after existing battalions (100+ to avoid conflicts)
|
||||
val hedrickHero = findHero("Hedrick the Hedge-Merchant")
|
||||
val johnRanilHero = findHero("John Ranil")
|
||||
val elenaFyarHero = findHero("Elena Fyar")
|
||||
|
||||
val reinforcementUnits = Seq(
|
||||
createReinforcementUnit(
|
||||
loadedHero = hedrickHero,
|
||||
heroId = 100,
|
||||
battalionId = 100,
|
||||
factionId = playerFid,
|
||||
battalionType = CommonBattalionTypeId.LONGBOWMEN,
|
||||
battalionSize = 600,
|
||||
training = 75.0,
|
||||
armament = 75.0
|
||||
),
|
||||
createReinforcementUnit(
|
||||
loadedHero = johnRanilHero,
|
||||
heroId = 101,
|
||||
battalionId = 101,
|
||||
factionId = playerFid,
|
||||
battalionType = CommonBattalionTypeId.HEAVY_CAVALRY,
|
||||
battalionSize = 500,
|
||||
training = 80.0,
|
||||
armament = 80.0
|
||||
),
|
||||
createReinforcementUnit(
|
||||
loadedHero = elenaFyarHero,
|
||||
heroId = 102,
|
||||
battalionId = 102,
|
||||
factionId = playerFid,
|
||||
battalionType = CommonBattalionTypeId.HEAVY_INFANTRY,
|
||||
battalionSize = 600,
|
||||
training = 75.0,
|
||||
armament = 75.0
|
||||
)
|
||||
)
|
||||
|
||||
val tutorialConfig = TutorialBattleConfig(
|
||||
enabled = true,
|
||||
|
||||
@@ -231,12 +231,12 @@ class RuntimeValidatorTest extends AnyFlatSpec with Matchers {
|
||||
}
|
||||
|
||||
it should "throw if the price index is negative" in {
|
||||
val province = ProvinceC(id = 17, priceIndex = -0.4, name = "East Faluria")
|
||||
val province = ProvinceC(id = 17, priceIndex = -0.4, name = "Fluria")
|
||||
|
||||
val ex = the[EagleValidationException] thrownBy
|
||||
RuntimeValidator.validate(province, RoundPhase.PlayerCommands)
|
||||
|
||||
ex.getMessage shouldBe s"validation failed: Invalid price index -0.4 in province 17 (East Faluria)"
|
||||
ex.getMessage shouldBe s"validation failed: Invalid price index -0.4 in province 17 (Fluria)"
|
||||
}
|
||||
|
||||
it should "not throw if the province id is valid and the priceIndex is valid" in {
|
||||
|
||||
@@ -9,7 +9,6 @@ Usage:
|
||||
python generate_map.py --analyze # Analyze current map, show province sizes
|
||||
python generate_map.py --generate # Generate new equalized map
|
||||
python generate_map.py --visualize # Generate visualization of current vs new
|
||||
python generate_map.py --update-centroids # Update label positions in centroids.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -90,39 +89,6 @@ def load_province_data() -> dict[int, dict]:
|
||||
return provinces
|
||||
|
||||
|
||||
def find_label_position(mask: np.ndarray) -> tuple[float, float]:
|
||||
"""Find the best label position for a province.
|
||||
|
||||
Finds all pixels with at least 70% of the province's maximum possible
|
||||
clearance from edges, then picks the one closest to the geometric centroid.
|
||||
This guarantees good distance from borders/coastlines while keeping labels
|
||||
as visually centered as possible.
|
||||
|
||||
Returns (x, y) in pixel coordinates.
|
||||
"""
|
||||
# Geometric centroid (visual center of mass)
|
||||
y_coords, x_coords = np.where(mask)
|
||||
mean_x = np.mean(x_coords)
|
||||
mean_y = np.mean(y_coords)
|
||||
|
||||
# Distance from nearest edge for every province pixel
|
||||
dist = ndimage.distance_transform_edt(mask)
|
||||
max_clearance = np.max(dist)
|
||||
|
||||
# Require at least 70% of the best possible clearance
|
||||
candidates = dist >= max_clearance * 0.7
|
||||
|
||||
if np.any(candidates):
|
||||
cand_ys, cand_xs = np.where(candidates)
|
||||
dists_to_centroid = (cand_xs - mean_x)**2 + (cand_ys - mean_y)**2
|
||||
best = np.argmin(dists_to_centroid)
|
||||
return float(cand_xs[best]), float(cand_ys[best])
|
||||
|
||||
# Fallback: pole of inaccessibility
|
||||
max_idx = np.unravel_index(np.argmax(dist), dist.shape)
|
||||
return float(max_idx[1]), float(max_idx[0])
|
||||
|
||||
|
||||
def analyze_provinces(raw_gray: np.ndarray, province_data: dict) -> list[Province]:
|
||||
"""Calculate centroid and area for each province."""
|
||||
provinces = []
|
||||
@@ -136,7 +102,10 @@ def analyze_provinces(raw_gray: np.ndarray, province_data: dict) -> list[Provinc
|
||||
print(f"Warning: Province {province_id} ({data['name']}) has no pixels")
|
||||
continue
|
||||
|
||||
centroid_x, centroid_y = find_label_position(mask)
|
||||
# Calculate centroid (center of mass)
|
||||
y_coords, x_coords = np.where(mask)
|
||||
centroid_x = np.mean(x_coords)
|
||||
centroid_y = np.mean(y_coords)
|
||||
|
||||
provinces.append(Province(
|
||||
id=province_id,
|
||||
@@ -561,85 +530,26 @@ def generate_visualization(
|
||||
|
||||
|
||||
def save_centroids(provinces: list[Province], output_path: Path):
|
||||
"""Save province centroids to JSON for Unity label positioning.
|
||||
|
||||
Preserves hand-tuned fields (orientation, principal_length, etc.)
|
||||
from existing centroids.json if present.
|
||||
"""
|
||||
# Load existing data to preserve hand-tuned fields
|
||||
existing_by_id = {}
|
||||
for candidate in [output_path, UNITY_ASSETS / "centroids.json"]:
|
||||
if candidate.exists():
|
||||
with open(candidate, 'r') as f:
|
||||
existing_by_id = {p['id']: p for p in json.load(f)['provinces']}
|
||||
break
|
||||
|
||||
province_list = []
|
||||
for p in provinces:
|
||||
entry = {
|
||||
"id": int(p.id),
|
||||
"name": p.name,
|
||||
"centroid_x": round(float(p.centroid[0]), 1),
|
||||
"centroid_y": round(float(p.centroid[1]), 1),
|
||||
"area": int(p.area)
|
||||
}
|
||||
# Preserve hand-tuned fields from existing file
|
||||
if p.id in existing_by_id:
|
||||
for field in ("orientation", "principal_length", "perpendicular_width", "curvature"):
|
||||
if field in existing_by_id[p.id]:
|
||||
entry[field] = existing_by_id[p.id][field]
|
||||
province_list.append(entry)
|
||||
|
||||
data = {"provinces": province_list}
|
||||
"""Save province centroids to JSON for Unity label positioning."""
|
||||
data = {
|
||||
"provinces": [
|
||||
{
|
||||
"id": int(p.id),
|
||||
"name": p.name,
|
||||
"centroid_x": float(p.centroid[0]),
|
||||
"centroid_y": float(p.centroid[1]),
|
||||
"area": int(p.area)
|
||||
}
|
||||
for p in provinces
|
||||
]
|
||||
}
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write('\n')
|
||||
|
||||
print(f"Saved centroids to {output_path}")
|
||||
|
||||
|
||||
def update_centroids(raw_gray: np.ndarray, province_data: dict):
|
||||
"""Update centroids.json with pole-of-inaccessibility positions.
|
||||
|
||||
Only updates centroid_x and centroid_y, preserving all hand-tuned fields
|
||||
(orientation, principal_length, perpendicular_width, curvature).
|
||||
"""
|
||||
centroids_path = UNITY_ASSETS / "centroids.json"
|
||||
|
||||
with open(centroids_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
existing_by_id = {p['id']: p for p in data['provinces']}
|
||||
|
||||
updated = 0
|
||||
for province_id, pdata in province_data.items():
|
||||
mask = raw_gray == province_id
|
||||
area = np.sum(mask)
|
||||
if area == 0:
|
||||
print(f"Warning: Province {province_id} ({pdata['name']}) has no pixels")
|
||||
continue
|
||||
|
||||
lx, ly = find_label_position(mask)
|
||||
new_x = round(lx, 1)
|
||||
new_y = round(ly, 1)
|
||||
|
||||
if province_id in existing_by_id:
|
||||
entry = existing_by_id[province_id]
|
||||
old_x, old_y = entry['centroid_x'], entry['centroid_y']
|
||||
if old_x != new_x or old_y != new_y:
|
||||
print(f" {pdata['name']:20s}: ({old_x}, {old_y}) -> ({new_x}, {new_y})")
|
||||
entry['centroid_x'] = new_x
|
||||
entry['centroid_y'] = new_y
|
||||
updated += 1
|
||||
|
||||
with open(centroids_path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write('\n')
|
||||
|
||||
print(f"Updated {updated} centroids in {centroids_path}")
|
||||
|
||||
|
||||
def save_raw_gray(province_map: np.ndarray, output_path: Path, compress: bool = True):
|
||||
"""Save province map as raw bytes (optionally gzip compressed)."""
|
||||
# Flip for Unity (bottom-left origin)
|
||||
@@ -662,8 +572,6 @@ def main():
|
||||
parser.add_argument('--analyze', action='store_true', help='Analyze current map')
|
||||
parser.add_argument('--generate', action='store_true', help='Generate new equalized map')
|
||||
parser.add_argument('--visualize', action='store_true', help='Generate visualization')
|
||||
parser.add_argument('--update-centroids', action='store_true',
|
||||
help='Update centroid positions in centroids.json (preserves hand-tuned fields)')
|
||||
parser.add_argument('--target-ratio', type=float, default=0.5,
|
||||
help='Target minimum province size as ratio of average (default: 0.5)')
|
||||
parser.add_argument('--output-dir', type=Path, default=SCRIPT_DIR / 'output',
|
||||
@@ -671,7 +579,7 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not any([args.analyze, args.generate, args.visualize, args.update_centroids]):
|
||||
if not any([args.analyze, args.generate, args.visualize]):
|
||||
args.analyze = True # Default to analyze
|
||||
|
||||
print("Loading province data...")
|
||||
@@ -682,11 +590,6 @@ def main():
|
||||
raw_gray = load_raw_gray()
|
||||
print(f"Map dimensions: {raw_gray.shape[1]} x {raw_gray.shape[0]}")
|
||||
|
||||
if args.update_centroids:
|
||||
print("Updating centroids with pole-of-inaccessibility positions...")
|
||||
update_centroids(raw_gray, province_data)
|
||||
return
|
||||
|
||||
print("Analyzing provinces...")
|
||||
provinces = analyze_provinces(raw_gray, province_data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user