mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 10:55:42 +00:00
Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6762be71b4 | ||
|
|
401dbb0ce0 | ||
|
|
e7d76d99b7 | ||
|
|
d4089ae399 | ||
|
|
15955ff139 | ||
|
|
fed04aec7c | ||
|
|
625155a6c6 | ||
|
|
68a6037cc6 | ||
|
|
b186c059f4 | ||
|
|
be275a5aaf | ||
|
|
9977fc965a | ||
|
|
3076a6727e | ||
|
|
d89f2c8894 | ||
|
|
0a7f0e4ad5 | ||
|
|
613cf529bc | ||
|
|
ca38c1df3a | ||
|
|
13777a1e83 | ||
|
|
782dc2135a | ||
|
|
a23ba79190 | ||
|
|
a1dcb8e932 | ||
|
|
328fa44bf4 | ||
|
|
4f0ecf66c6 | ||
|
|
9a014b38c4 | ||
|
|
f6ed3d2bee | ||
|
|
d92d37ea68 | ||
|
|
f5ed0de040 | ||
|
|
8ff56d7d28 | ||
|
|
66e8f2f605 | ||
|
|
9c557873c4 | ||
|
|
ee8dfba000 | ||
|
|
98b456d760 | ||
|
|
413df8bdff | ||
|
|
c1463ca9c5 | ||
|
|
d4572c2108 | ||
|
|
223e059e72 | ||
|
|
5077981d41 | ||
|
|
f352d7aca8 | ||
|
|
c85ce61bef | ||
|
|
1ccd5784ee | ||
|
|
5814fb31e5 | ||
|
|
ddfd735e15 | ||
|
|
d9506fc0a0 | ||
|
|
1f0da81ed3 | ||
|
|
64a15dd616 | ||
|
|
ad58e4cac2 | ||
|
|
d9e5b47e7f | ||
|
|
a8842b0cd3 | ||
|
|
044819cd99 | ||
|
|
6471a889a1 | ||
|
|
115948eea3 | ||
|
|
58e674d3fd | ||
|
|
c94d41952c | ||
|
|
3b536f93e2 | ||
|
|
d0a07febe7 | ||
|
|
10dbff3738 | ||
|
|
78e130b9dd | ||
|
|
e9d724ed88 | ||
|
|
8cf22c0bd0 | ||
|
|
80d8927d6a | ||
|
|
c75f7d8dda | ||
|
|
cc710e0f6b | ||
|
|
377c1fe726 | ||
|
|
17d2a72ea9 | ||
|
|
91dab8a28b | ||
|
|
ebf9a3934f | ||
|
|
5cd1b372cc | ||
|
|
d543967b44 | ||
|
|
69789dd22e | ||
|
|
f5c24e68c8 | ||
|
|
c00acc991b | ||
|
|
1a9479aa17 | ||
|
|
3a5eb1f760 | ||
|
|
3eaeb031b2 | ||
|
|
1f29de9012 | ||
|
|
6640e423b7 | ||
|
|
1b18bb090f | ||
|
|
c6cb8b4c48 | ||
|
|
8f205e5e8e | ||
|
|
b3b0de6b15 | ||
|
|
4605492f32 | ||
|
|
7626a4a958 | ||
|
|
d07c3237f2 | ||
|
|
ad7b3fd0d1 | ||
|
|
8b64872a51 | ||
|
|
17f9521780 | ||
|
|
2a3ad4cad4 | ||
|
|
5dfd9a7515 | ||
|
|
2021ee2ae8 | ||
|
|
37b4039f59 | ||
|
|
6e1a462c2c | ||
|
|
84f165ea1d | ||
|
|
faaac39051 | ||
|
|
57d35e085a |
@@ -5,6 +5,10 @@ common --ui_event_filters=-INFO
|
||||
|
||||
common --enable_bzlmod
|
||||
|
||||
# Use pre-built protoc binary instead of compiling from source
|
||||
common --incompatible_enable_proto_toolchain_resolution
|
||||
common --@com_google_protobuf//bazel/toolchains:prefer_prebuilt_protoc=true
|
||||
|
||||
# Don't use toolchains_llvm for the swift app build
|
||||
common:mactools --ignore_dev_dependency
|
||||
|
||||
|
||||
@@ -34,16 +34,34 @@ jobs:
|
||||
lint:
|
||||
runs-on: [self-hosted, bazel]
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
git sparse-checkout disable 2>/dev/null || true
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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]
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
git sparse-checkout disable 2>/dev/null || true
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -31,13 +31,16 @@ on:
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
# Only allow one deployment at a time to prevent race conditions
|
||||
# Only allow one deployment at a time; queue new ones (never cancel a running deploy).
|
||||
# The build-all job checks if it's still the latest commit on main and skips if not,
|
||||
# so intermediate commits don't waste time building when a newer one is already queued.
|
||||
concurrency:
|
||||
group: docker-build-deploy
|
||||
cancel-in-progress: false # Don't cancel running deployments, queue new ones
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
# Single consolidated build job - builds all images with one bazel invocation
|
||||
@@ -50,12 +53,33 @@ jobs:
|
||||
admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
|
||||
jfr_sidecar_image_tag: ${{ steps.push-images.outputs.jfr_sidecar_image_tag }}
|
||||
steps:
|
||||
- name: Skip if superseded
|
||||
id: check-latest
|
||||
run: |
|
||||
# Check if there's a newer run of this workflow waiting in the queue.
|
||||
# If so, skip this build — the queued run will deploy a newer commit.
|
||||
QUEUED=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/workflows/docker_build.yml/runs?status=queued" \
|
||||
| python3 -c "import sys,json; runs=json.load(sys.stdin).get('workflow_runs',[]); print(len([r for r in runs if r['run_number'] > ${{ github.run_number }}]))")
|
||||
if [ "$QUEUED" -gt 0 ]; then
|
||||
echo "::notice::Skipping build — $QUEUED newer run(s) queued for this workflow"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: actions/checkout@v4
|
||||
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
|
||||
run: |
|
||||
set -ex
|
||||
@@ -92,6 +116,7 @@ jobs:
|
||||
echo "JFR Sidecar: $JFR_PATH"
|
||||
|
||||
- name: Upload warmup binary
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: warmup-binary
|
||||
@@ -99,7 +124,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
if: steps.check-latest.outputs.skip != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
|
||||
env:
|
||||
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
@@ -222,11 +247,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
sparse-checkout: |
|
||||
docker-compose.prod.yml
|
||||
nginx
|
||||
scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
|
||||
@@ -191,8 +191,22 @@ jobs:
|
||||
|
||||
echo "Deploying Shardok ARM64: ${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Pull the new image
|
||||
docker pull "${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
# Pre-warm NAT64 path to DigitalOcean registry (IPv6-only Hetzner → IPv4 registry)
|
||||
curl -sf --max-time 10 https://registry.digitalocean.com/v2/ > /dev/null 2>&1 || true
|
||||
|
||||
# Pull the new image (retry up to 3 times for NAT64 connectivity flakiness)
|
||||
for attempt in 1 2 3; do
|
||||
echo "Pull attempt $attempt..."
|
||||
if docker pull "${{ needs.build-shardok-arm64.outputs.image_tag }}"; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "ERROR: docker pull failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "Pull failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Stop and remove any container using port 40042 or named shardok*
|
||||
docker ps -q --filter "publish=40042" | xargs -r docker stop
|
||||
|
||||
@@ -5,6 +5,7 @@ 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,6 +243,13 @@ 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",
|
||||
@@ -251,6 +258,7 @@ oci_image(
|
||||
tars = [
|
||||
":busybox_layer",
|
||||
":admin_binary_layer",
|
||||
":admin_maps_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
)
|
||||
|
||||
@@ -208,6 +208,8 @@ 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:-}"
|
||||
@@ -215,6 +217,8 @@ 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
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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/...`
|
||||
+13
-3
@@ -35,8 +35,13 @@ SUBDIRS=(
|
||||
/bin/echo "Building C# protobuf sources..."
|
||||
bazel build "${TARGETS[@]}"
|
||||
|
||||
/bin/echo "Copying generated .cs files to $OUTPUT_DIR..."
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
/bin/echo "Syncing generated .cs files to $OUTPUT_DIR..."
|
||||
|
||||
# Stage new files in a temp directory, then rsync to preserve timestamps
|
||||
# on unchanged files. This avoids triggering Unity reimport when protos
|
||||
# haven't changed, saving ~7 minutes of script recompilation.
|
||||
STAGING_DIR=$(mktemp -d)
|
||||
trap "rm -rf '$STAGING_DIR'" EXIT
|
||||
|
||||
for i in "${!TARGETS[@]}"; do
|
||||
target="${TARGETS[$i]}"
|
||||
@@ -50,9 +55,14 @@ for i in "${!TARGETS[@]}"; do
|
||||
name="${target_path##*:}"
|
||||
bin_dir="$REPO_ROOT/bazel-bin/$pkg/$name"
|
||||
|
||||
dest_dir="$OUTPUT_DIR/$subdir"
|
||||
dest_dir="$STAGING_DIR/$subdir"
|
||||
mkdir -p "$dest_dir"
|
||||
find "$bin_dir" -name "*.cs" -exec cp {} "$dest_dir/" \;
|
||||
done
|
||||
|
||||
# Sync only changed files and delete removed ones; --checksum compares
|
||||
# content not timestamps so unchanged files keep their original mtime.
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
rsync -rc --delete "$STAGING_DIR/" "$OUTPUT_DIR/"
|
||||
|
||||
/bin/echo "Done. Generated C# proto sources in $OUTPUT_DIR"
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/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
|
||||
@@ -20,6 +20,9 @@
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AIConfig.hpp"
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
@@ -171,8 +174,117 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
// }
|
||||
// }
|
||||
|
||||
assert(commandCount == realAvailableCommands->size());
|
||||
// Verify that the AI's guessed state produces the same available commands as reality
|
||||
if (commandCount != realAvailableCommands->size()) {
|
||||
fprintf(stderr,
|
||||
"AI command count mismatch: guessed=%zu real=%zu player=%d round=%d\n",
|
||||
commandCount,
|
||||
realAvailableCommands->size(),
|
||||
playerId,
|
||||
guessedState->current_round());
|
||||
|
||||
const auto maxDump = std::max(commandCount, realAvailableCommands->size());
|
||||
for (size_t i = 0; i < maxDump; i++) {
|
||||
const auto *realName = i < realAvailableCommands->size()
|
||||
? net::eagle0::shardok::common::CommandType_Name(
|
||||
(*realAvailableCommands)[i]->GetCommandType())
|
||||
.c_str()
|
||||
: "(none)";
|
||||
const auto *guessedName = i < commandCount
|
||||
? net::eagle0::shardok::common::CommandType_Name(
|
||||
(*guessedCommands)[i]->GetCommandType())
|
||||
.c_str()
|
||||
: "(none)";
|
||||
const int realActor = i < realAvailableCommands->size()
|
||||
? (*realAvailableCommands)[i]->GetActorUnitId()
|
||||
: -1;
|
||||
const int guessedActor =
|
||||
i < commandCount ? (*guessedCommands)[i]->GetActorUnitId() : -1;
|
||||
const int realRow = i < realAvailableCommands->size()
|
||||
? (*realAvailableCommands)[i]->GetTargetRow()
|
||||
: -1;
|
||||
const int realCol = i < realAvailableCommands->size()
|
||||
? (*realAvailableCommands)[i]->GetTargetColumn()
|
||||
: -1;
|
||||
const int guessedRow = i < commandCount ? (*guessedCommands)[i]->GetTargetRow() : -1;
|
||||
const int guessedCol = i < commandCount ? (*guessedCommands)[i]->GetTargetColumn() : -1;
|
||||
fprintf(stderr,
|
||||
" [%zu] real: %s unit=%d (%d,%d) guessed: %s unit=%d (%d,%d)\n",
|
||||
i,
|
||||
realName,
|
||||
realActor,
|
||||
realRow,
|
||||
realCol,
|
||||
guessedName,
|
||||
guessedActor,
|
||||
guessedRow,
|
||||
guessedCol);
|
||||
}
|
||||
|
||||
// Find positions in real commands that are missing from guessed commands
|
||||
// Group by unit to find the missing position per unit
|
||||
std::unordered_map<int, std::set<std::pair<int, int>>> realPositions;
|
||||
std::unordered_map<int, std::set<std::pair<int, int>>> guessedPositions;
|
||||
for (size_t i = 0; i < realAvailableCommands->size(); i++) {
|
||||
const auto &cmd = (*realAvailableCommands)[i];
|
||||
realPositions[cmd->GetActorUnitId()].emplace(
|
||||
cmd->GetTargetRow(),
|
||||
cmd->GetTargetColumn());
|
||||
}
|
||||
for (size_t i = 0; i < commandCount; i++) {
|
||||
const auto &cmd = (*guessedCommands)[i];
|
||||
guessedPositions[cmd->GetActorUnitId()].emplace(
|
||||
cmd->GetTargetRow(),
|
||||
cmd->GetTargetColumn());
|
||||
}
|
||||
for (const auto &[unitId, positions] : realPositions) {
|
||||
for (const auto &[row, col] : positions) {
|
||||
if (!guessedPositions[unitId].contains({row, col})) {
|
||||
fprintf(stderr,
|
||||
" Missing from guessed: unit=%d pos=(%d,%d)",
|
||||
unitId,
|
||||
row,
|
||||
col);
|
||||
const Coords missingCoords(row, col);
|
||||
const auto *occupant = guessedState.GetOccupant(missingCoords);
|
||||
if (occupant) {
|
||||
fprintf(stderr,
|
||||
" -> guessed occupant: unit_id=%d player=%d status=%d "
|
||||
"loc=(%d,%d) hidden=%d battalion_size=%d\n",
|
||||
occupant->unit_id(),
|
||||
occupant->player_id(),
|
||||
static_cast<int>(occupant->status()),
|
||||
occupant->location().row(),
|
||||
occupant->location().column(),
|
||||
occupant->hidden(),
|
||||
occupant->battalion().size());
|
||||
} else {
|
||||
fprintf(stderr, " -> no occupant in guessed state\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dump all guessed state units for full picture
|
||||
fprintf(stderr, " Guessed state units (%u total):\n", guessedState->units()->size());
|
||||
for (size_t i = 0; i < guessedState->units()->size(); i++) {
|
||||
const auto *u = guessedState->units()->Get(static_cast<unsigned int>(i));
|
||||
fprintf(stderr,
|
||||
" unit_id=%d player=%d status=%d loc=(%d,%d) hidden=%d "
|
||||
"starting_pos_idx=%d\n",
|
||||
u->unit_id(),
|
||||
u->player_id(),
|
||||
static_cast<int>(u->status()),
|
||||
u->location().row(),
|
||||
u->location().column(),
|
||||
u->hidden(),
|
||||
u->starting_position_index());
|
||||
}
|
||||
|
||||
throw ShardokInternalErrorException(
|
||||
"AI command count mismatch: guessed=" + std::to_string(commandCount) +
|
||||
" real=" + std::to_string(realAvailableCommands->size()));
|
||||
}
|
||||
for (size_t i = 0; i < commandCount; i++) {
|
||||
CheckCommand((*realAvailableCommands)[i], (*guessedCommands)[i]);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT: break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT: break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
|
||||
throw ShardokInternalErrorException("Unknown unit status");
|
||||
|
||||
@@ -56,13 +56,18 @@ public:
|
||||
commandFactories(CommandFactoriesList(settings).GetFactories()),
|
||||
playerSetupCommandFactory(settings) {}
|
||||
|
||||
[[nodiscard]] auto GetPlayerSetupCommands(const GameStateW &gameState, PlayerId playerId) const
|
||||
[[nodiscard]] auto GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const
|
||||
-> CommandListSPtr override;
|
||||
|
||||
[[nodiscard]] auto GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId player,
|
||||
bool includeFollowUps) const -> CommandListSPtr override;
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const
|
||||
-> CommandListSPtr override;
|
||||
};
|
||||
|
||||
auto AvailableCommandsFactory::MakeAvailableCommandsFactory(const SettingsGetter &settings)
|
||||
@@ -72,9 +77,14 @@ auto AvailableCommandsFactory::MakeAvailableCommandsFactory(const SettingsGetter
|
||||
|
||||
auto AvailableCommandsFactoryImpl::GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId playerId) const -> CommandListSPtr {
|
||||
const PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> CommandListSPtr {
|
||||
CommandList previewCommands{};
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(previewCommands, playerId, gameState);
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(
|
||||
previewCommands,
|
||||
playerId,
|
||||
gameState,
|
||||
reinforcementPositions);
|
||||
|
||||
return make_shared<CommandList>(previewCommands);
|
||||
}
|
||||
@@ -168,12 +178,14 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId,
|
||||
bool includeFollowUps) const -> CommandListSPtr {
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> CommandListSPtr {
|
||||
CommandList commands{};
|
||||
|
||||
vector<AdjacentTile> adjacentTiles;
|
||||
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(commands, playerId, gameState);
|
||||
playerSetupCommandFactory
|
||||
.AddAvailablePlayerSetupCommands(commands, playerId, gameState, reinforcementPositions);
|
||||
if (!commands.empty()) return make_shared<CommandList>(commands);
|
||||
|
||||
for (const Unit *unit : *gameState->units()) {
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
#ifndef AvailableCommandsFactory_hpp
|
||||
#define AvailableCommandsFactory_hpp
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
|
||||
|
||||
@@ -29,12 +33,16 @@ public:
|
||||
|
||||
[[nodiscard]] virtual auto GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId) const -> CommandListSPtr = 0;
|
||||
PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions = std::nullopt) const
|
||||
-> CommandListSPtr = 0;
|
||||
|
||||
[[nodiscard]] virtual auto GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId player,
|
||||
bool includeFollowUps) const -> CommandListSPtr = 0;
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions = std::nullopt) const
|
||||
-> CommandListSPtr = 0;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:new_round_action",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/command_factories:command_factories_list",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/commands:end_turn_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coordinates",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/unit",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
|
||||
@@ -288,13 +288,13 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
|
||||
|
||||
// Check for tutorial events at end of round
|
||||
if (!GameIsOver() && tutorialController_.IsEnabled()) {
|
||||
auto tutorialResults = tutorialController_.CheckAndExecuteEvents(
|
||||
auto tutorialEventResults = tutorialController_.CheckAndExecuteEvents(
|
||||
gameState,
|
||||
settingsGetter,
|
||||
randomGenerator);
|
||||
|
||||
if (!tutorialResults.empty()) {
|
||||
ApplyAndAddActionResults(tutorialResults);
|
||||
if (!tutorialEventResults.actionResults.empty()) {
|
||||
ApplyAndAddActionResults(tutorialEventResults.actionResults);
|
||||
|
||||
// Update game status after tutorial events (e.g., attackers fled, defender wins)
|
||||
ApplyAndAddActionResults(UpdateGameStatusAction(
|
||||
@@ -306,24 +306,29 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
|
||||
|
||||
// If tutorial events ended the game, don't start new round
|
||||
if (GameIsOver()) { return; }
|
||||
|
||||
// If reinforcement placement is needed, pause the turn flow
|
||||
if (tutorialEventResults.reinforcementPlacement.has_value()) {
|
||||
pendingReinforcementPlacement_ = tutorialEventResults.reinforcementPlacement;
|
||||
|
||||
// Set state to REINFORCEMENT_PLACEMENT
|
||||
ActionResultProto placementStateAction{};
|
||||
placementStateAction.set_type(
|
||||
net::eagle0::shardok::common::TUTORIAL_REINFORCEMENTS_ARRIVED);
|
||||
placementStateAction.mutable_game_status()->set_state(
|
||||
GameStatusProto::State::GameStatus_State_REINFORCEMENT_PLACEMENT);
|
||||
*placementStateAction.mutable_game_status()->mutable_description() =
|
||||
"Place your reinforcements!";
|
||||
// Set current player to the reinforcement recipient
|
||||
placementStateAction.mutable_next_player()->set_value(
|
||||
pendingReinforcementPlacement_->playerId);
|
||||
ApplyAndAddActionResult(placementStateAction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
UpdateOpponentKnowledgeAction(settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
NewRoundAction(gameState, settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
StartPlayerTurnAction(gameState, UNCONTROLLED_PLAYER_ID, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(PerformUndeadCommandsAction(gameState, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(StartPlayerTurnAction(gameState, 0, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
ResumeAfterReinforcementPlacement(randomGenerator);
|
||||
|
||||
} else {
|
||||
const auto updateAction = UpdateGameStatusAction(
|
||||
@@ -339,6 +344,27 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
|
||||
}
|
||||
}
|
||||
|
||||
void ShardokEngine::ResumeAfterReinforcementPlacement(
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator) {
|
||||
pendingReinforcementPlacement_ = std::nullopt;
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
UpdateOpponentKnowledgeAction(settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
NewRoundAction(gameState, settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
StartPlayerTurnAction(gameState, UNCONTROLLED_PLAYER_ID, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(PerformUndeadCommandsAction(gameState, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(StartPlayerTurnAction(gameState, 0, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
}
|
||||
|
||||
void ShardokEngine::PostPlacementCommands(
|
||||
const PlayerId player,
|
||||
const vector<UnitPlacementInfo> &placementInfos,
|
||||
@@ -347,8 +373,15 @@ void ShardokEngine::PostPlacementCommands(
|
||||
throw ShardokClientErrorException("Posting for a player who's not the current player ID!");
|
||||
}
|
||||
|
||||
const auto placementCommands =
|
||||
availableCommandsFactory->GetPlayerSetupCommands(gameState, player);
|
||||
const auto reinforcementPositions =
|
||||
pendingReinforcementPlacement_.has_value()
|
||||
? std::optional<std::vector<Coords>>(
|
||||
pendingReinforcementPlacement_->startingPositions)
|
||||
: std::nullopt;
|
||||
const auto placementCommands = availableCommandsFactory->GetPlayerSetupCommands(
|
||||
gameState,
|
||||
player,
|
||||
reinforcementPositions);
|
||||
|
||||
// first make sure they're all valid and there are no duplicates
|
||||
for (size_t i = 0; i < placementInfos.size(); i++) {
|
||||
@@ -388,6 +421,19 @@ void ShardokEngine::PostPlacementCommands(
|
||||
}
|
||||
}
|
||||
|
||||
// If we were in reinforcement placement, check if all units are now placed
|
||||
if (pendingReinforcementPlacement_.has_value()) {
|
||||
const auto reserveUnits =
|
||||
ReserveUnitsForPlayer(gameState->units(), pendingReinforcementPlacement_->playerId);
|
||||
if (reserveUnits.empty()) {
|
||||
// All reinforcements placed — resume the turn flow
|
||||
ResumeAfterReinforcementPlacement(randomGenerator);
|
||||
return;
|
||||
}
|
||||
// Still have units to place — stay in REINFORCEMENT_PLACEMENT state
|
||||
return;
|
||||
}
|
||||
|
||||
const auto updateAction = UpdateGameStatusAction(
|
||||
GetCurrentGameState(),
|
||||
criticalTileCoords,
|
||||
@@ -586,10 +632,16 @@ auto ShardokEngine::GetAvailableCommandProtos(const PlayerId playerId, const boo
|
||||
auto ShardokEngine::UncachedGetAvailableCommands(
|
||||
const PlayerId playerId,
|
||||
const bool includeFollowUps) const -> CommandListSPtr {
|
||||
const auto reinforcementPositions =
|
||||
pendingReinforcementPlacement_.has_value()
|
||||
? std::optional<std::vector<Coords>>(
|
||||
pendingReinforcementPlacement_->startingPositions)
|
||||
: std::nullopt;
|
||||
return availableCommandsFactory->GetAvailableCommands(
|
||||
gameState,
|
||||
playerId,
|
||||
/* includeFollowUps=*/includeFollowUps);
|
||||
/* includeFollowUps=*/includeFollowUps,
|
||||
reinforcementPositions);
|
||||
}
|
||||
|
||||
void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Units &from) {
|
||||
@@ -621,6 +673,7 @@ void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Uni
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
|
||||
ru.set_status(
|
||||
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT);
|
||||
break;
|
||||
|
||||
@@ -55,6 +55,7 @@ private:
|
||||
const CoordsSet criticalTileCoords;
|
||||
|
||||
TutorialBattleController tutorialController_;
|
||||
std::optional<ReinforcementPlacementInfo> pendingReinforcementPlacement_;
|
||||
|
||||
mutable CommandListSPtr cachedAvailableCommands{};
|
||||
|
||||
@@ -76,6 +77,7 @@ private:
|
||||
}
|
||||
|
||||
void HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &randomGenerator);
|
||||
void ResumeAfterReinforcementPlacement(const std::shared_ptr<RandomGenerator> &randomGenerator);
|
||||
[[nodiscard]] auto UncachedGetAvailableCommands(PlayerId playerId, bool includeFollowUps) const
|
||||
-> CommandListSPtr;
|
||||
|
||||
@@ -206,6 +208,12 @@ public:
|
||||
tutorialController_ = TutorialBattleController(config);
|
||||
}
|
||||
|
||||
/// Returns the pending reinforcement placement info, if any.
|
||||
[[nodiscard]] auto GetPendingReinforcementPlacement() const
|
||||
-> const std::optional<ReinforcementPlacementInfo> & {
|
||||
return pendingReinforcementPlacement_;
|
||||
}
|
||||
|
||||
static inline auto GameIsOver(const fb::GameStatus *status) -> bool {
|
||||
return (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:end_player_setup_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:place_hidden_unit_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:place_unit_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coordinates",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:player_utils",
|
||||
|
||||
+22
-3
@@ -66,11 +66,30 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
|
||||
auto PlayerSetupCommandFactory::AddAvailablePlayerSetupCommands(
|
||||
CommandList &existingCommands,
|
||||
PlayerId playerId,
|
||||
const GameStateW &gameState) const -> void {
|
||||
const GameStateW &gameState,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> void {
|
||||
if (playerId < 0) return;
|
||||
if (gameState->status()->state() !=
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP)
|
||||
|
||||
const auto state = gameState->status()->state();
|
||||
|
||||
// Handle reinforcement placement phase
|
||||
if (state == net::eagle0::shardok::storage::fb::GameStatus_::State_REINFORCEMENT_PLACEMENT) {
|
||||
if (!reinforcementPositions.has_value()) return;
|
||||
|
||||
const auto unplacedUnits = ReserveUnitsForPlayer(gameState->units(), playerId);
|
||||
if (unplacedUnits.empty()) return;
|
||||
|
||||
for (const auto &[unitId, unit] : unplacedUnits) {
|
||||
for (const Coords &position : *reinforcementPositions) {
|
||||
if (!gameState.GetOccupant(position)) {
|
||||
existingCommands.push_back(std::make_shared<PlaceUnitCommand>(unit, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) return;
|
||||
|
||||
const bool isDefender = PlayerIsDefender(gameState, playerId);
|
||||
|
||||
|
||||
+7
-1
@@ -5,8 +5,12 @@
|
||||
#ifndef EAGLE0_PLAYERSETUPCOMMANDFACTORY_HPP
|
||||
#define EAGLE0_PLAYERSETUPCOMMANDFACTORY_HPP
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -29,7 +33,9 @@ public:
|
||||
auto AddAvailablePlayerSetupCommands(
|
||||
CommandList& existingCommands,
|
||||
PlayerId playerId,
|
||||
const GameStateW& gameState) const -> void;
|
||||
const GameStateW& gameState,
|
||||
const std::optional<std::vector<Coords>>& reinforcementPositions = std::nullopt) const
|
||||
-> void;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
+1
-1
@@ -331,7 +331,7 @@ void MutatingApplyResult(
|
||||
.size()
|
||||
: changedUnit->battalion().size();
|
||||
|
||||
auto status = net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT;
|
||||
auto status = changedUnit->status();
|
||||
if (IsDestroyed(*changedUnit)) {
|
||||
status =
|
||||
changedUnit->has_attached_hero()
|
||||
|
||||
@@ -34,6 +34,7 @@ auto IsResolved(const net::eagle0::shardok::storage::fb::UnitStatus status) -> b
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT: return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ using net::eagle0::shardok::common::GameStatus;
|
||||
auto actorAfter = *currentState->units()->Get(actorId);
|
||||
actorAfter.mutable_location() = target;
|
||||
actorAfter.mutate_hidden(true);
|
||||
actorAfter.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
MutatingBumpAllKnowledgeToMinimum(&actorAfter, minimumKnowledge);
|
||||
|
||||
ActionResult placeResult{};
|
||||
|
||||
@@ -16,6 +16,7 @@ auto PlaceUnitCommand::InternalExecute(
|
||||
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
|
||||
auto actorAfter = *actor;
|
||||
actorAfter.mutable_location() = target;
|
||||
actorAfter.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
|
||||
ActionResult placeResult{};
|
||||
placeResult.set_type(ActionType::PLACE_UNIT);
|
||||
|
||||
@@ -89,7 +89,9 @@ auto UpdateGameStatusAction::InternalExecute(
|
||||
// If the game has already ended, just return that state
|
||||
if (GameIsOver(gameState->status())) { return results; }
|
||||
if (gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP ||
|
||||
gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_REINFORCEMENT_PLACEMENT) {
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ auto ReinforceCommand::InternalExecute(
|
||||
const Unit* joiningUnit = currentState->units()->Get(joiningUnitId);
|
||||
auto joinerAfter = *joiningUnit;
|
||||
joinerAfter.mutable_location() = location;
|
||||
joinerAfter.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
|
||||
ActionResult placeResult{};
|
||||
placeResult.set_type(ActionType::REINFORCED);
|
||||
|
||||
@@ -176,7 +176,10 @@ auto SetupInitialGameState(
|
||||
modifiedUnit.mutable_battalion().mutate_base_morale(baseMorale);
|
||||
modifiedUnit.mutable_battalion().mutate_morale(baseMorale);
|
||||
|
||||
modifiedUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
|
||||
if (modifiedUnit.status() !=
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT) {
|
||||
modifiedUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
|
||||
}
|
||||
|
||||
unitsVec.push_back(modifiedUnit);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ cc_library(
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:random_generator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coordinates",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_cc_proto",
|
||||
|
||||
@@ -34,10 +34,10 @@ TutorialBattleController::TutorialBattleController(const TutorialBattleConfigPro
|
||||
auto TutorialBattleController::CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) -> std::vector<ActionResult> {
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) -> TutorialEventResults {
|
||||
if (!enabled_) { return {}; }
|
||||
|
||||
std::vector<ActionResult> allResults;
|
||||
TutorialEventResults combined;
|
||||
|
||||
// Check events in config order
|
||||
for (const auto& event : config_.events()) {
|
||||
@@ -50,12 +50,20 @@ auto TutorialBattleController::CheckAndExecuteEvents(
|
||||
firedEventIds_.insert(event.event_id());
|
||||
|
||||
// Execute the action
|
||||
auto results = ExecuteAction(event.action(), state, settings, randomGenerator);
|
||||
allResults.insert(allResults.end(), results.begin(), results.end());
|
||||
auto eventResults = ExecuteAction(event.action(), state, settings, randomGenerator);
|
||||
combined.actionResults.insert(
|
||||
combined.actionResults.end(),
|
||||
eventResults.actionResults.begin(),
|
||||
eventResults.actionResults.end());
|
||||
|
||||
// Only one reinforcement placement can be pending at a time
|
||||
if (eventResults.reinforcementPlacement.has_value()) {
|
||||
combined.reinforcementPlacement = eventResults.reinforcementPlacement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allResults;
|
||||
return combined;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::EvaluateTrigger(
|
||||
@@ -90,15 +98,17 @@ auto TutorialBattleController::EvaluateTrigger(
|
||||
|
||||
auto TutorialBattleController::ExecuteAction(
|
||||
const TutorialActionProto& action,
|
||||
[[maybe_unused]] const GameStateW& state,
|
||||
const GameStateW& state,
|
||||
[[maybe_unused]] const SettingsGetter& settings,
|
||||
[[maybe_unused]] const std::shared_ptr<RandomGenerator>& randomGenerator) const
|
||||
-> std::vector<ActionResult> {
|
||||
-> TutorialEventResults {
|
||||
switch (action.action_type_case()) {
|
||||
case TutorialActionProto::kFlee: return ExecuteFleeAction(action.flee(), state);
|
||||
case TutorialActionProto::kFlee:
|
||||
return {.actionResults = ExecuteFleeAction(action.flee(), state),
|
||||
.reinforcementPlacement = std::nullopt};
|
||||
|
||||
case TutorialActionProto::kReinforcements:
|
||||
return ExecuteReinforcementsAction(action.reinforcements());
|
||||
return ExecuteReinforcementsAction(action.reinforcements(), state);
|
||||
|
||||
case TutorialActionProto::ACTION_TYPE_NOT_SET:
|
||||
default: return {};
|
||||
@@ -116,7 +126,8 @@ auto TutorialBattleController::CountLostUnits(const GameStateW& state, const Pla
|
||||
if (status != UnitStatusFB::UnitStatus_NORMAL_UNIT &&
|
||||
status != UnitStatusFB::UnitStatus_RESERVE_UNIT &&
|
||||
status != UnitStatusFB::UnitStatus_NEVER_ENTERED_UNIT &&
|
||||
status != UnitStatusFB::UnitStatus_RESERVED_SLOT) {
|
||||
status != UnitStatusFB::UnitStatus_RESERVED_SLOT &&
|
||||
status != UnitStatusFB::UnitStatus_PENDING_REINFORCEMENT) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -192,16 +203,25 @@ auto TutorialBattleController::ExecuteFleeAction(
|
||||
results.push_back(fleeNotification);
|
||||
|
||||
// Determine which units should flee
|
||||
std::set<int32_t> specificUnits(fleeAction.unit_ids().begin(), fleeAction.unit_ids().end());
|
||||
const bool fleeAllUnits = specificUnits.empty();
|
||||
std::set<int32_t> specificUnitIds(fleeAction.unit_ids().begin(), fleeAction.unit_ids().end());
|
||||
std::set<int32_t> specificHeroIds(
|
||||
fleeAction.eagle_hero_ids().begin(),
|
||||
fleeAction.eagle_hero_ids().end());
|
||||
const bool fleeAllUnits = specificUnitIds.empty() && specificHeroIds.empty();
|
||||
|
||||
// Find all matching units that are still active and make them flee
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->player_id() != fleeingPlayerId) { continue; }
|
||||
if (unit->status() != UnitStatusFB::UnitStatus_NORMAL_UNIT) { continue; }
|
||||
|
||||
// Check if this is a specific unit to flee, or if we're fleeing all
|
||||
if (!fleeAllUnits && !specificUnits.contains(unit->unit_id())) { continue; }
|
||||
// Check if this unit should flee
|
||||
if (!fleeAllUnits) {
|
||||
const bool matchesUnitId = specificUnitIds.contains(unit->unit_id());
|
||||
const bool matchesHeroId =
|
||||
unit->has_attached_hero() &&
|
||||
specificHeroIds.contains(unit->attached_hero().eagle_hero_id());
|
||||
if (!matchesUnitId && !matchesHeroId) { continue; }
|
||||
}
|
||||
|
||||
// Create flee result for this unit (100% success - guaranteed flee)
|
||||
ActionResult fleeResult{};
|
||||
@@ -220,17 +240,44 @@ auto TutorialBattleController::ExecuteFleeAction(
|
||||
}
|
||||
|
||||
auto TutorialBattleController::ExecuteReinforcementsAction(
|
||||
const ReinforcementsActionProto& reinforcementsAction) const -> std::vector<ActionResult> {
|
||||
std::vector<ActionResult> results;
|
||||
const ReinforcementsActionProto& reinforcementsAction,
|
||||
const GameStateW& state) const -> TutorialEventResults {
|
||||
TutorialEventResults eventResults;
|
||||
|
||||
// Add reinforcements arrival notification if there are units
|
||||
if (!reinforcementsAction.units().empty()) {
|
||||
ActionResult reinforcementsResult{};
|
||||
reinforcementsResult.set_type(ActionType::TUTORIAL_REINFORCEMENTS_ARRIVED);
|
||||
results.push_back(reinforcementsResult);
|
||||
if (reinforcementsAction.units().empty()) { return eventResults; }
|
||||
|
||||
const PlayerId playerId = reinforcementsAction.player_id();
|
||||
|
||||
// Look up starting positions from the hex map's attacker starting positions
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
std::vector<Coords> availablePositions;
|
||||
const int startingPosGroupIndex = reinforcementsAction.attacker_starting_position_index();
|
||||
const auto* attackerPositions = state->hex_map()->attacker_starting_positions();
|
||||
if (startingPosGroupIndex >= 0 &&
|
||||
startingPosGroupIndex < static_cast<int>(attackerPositions->size())) {
|
||||
for (const auto* pos : *attackerPositions->Get(startingPosGroupIndex)->positions()) {
|
||||
if (!state.GetOccupant(*pos)) { availablePositions.push_back(*pos); }
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
// Place PENDING_REINFORCEMENT units directly at available positions as NORMAL_UNIT
|
||||
ActionResult reinforcementsResult{};
|
||||
reinforcementsResult.set_type(ActionType::TUTORIAL_REINFORCEMENTS_ARRIVED);
|
||||
reinforcementsResult.mutable_player()->set_value(playerId);
|
||||
|
||||
size_t posIndex = 0;
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->status() != UnitStatusFB::UnitStatus_PENDING_REINFORCEMENT) { continue; }
|
||||
if (posIndex >= availablePositions.size()) { break; }
|
||||
|
||||
net::eagle0::shardok::storage::fb::Unit changedUnit = *unit;
|
||||
changedUnit.mutate_status(UnitStatusFB::UnitStatus_NORMAL_UNIT);
|
||||
changedUnit.mutable_location() = availablePositions[posIndex++];
|
||||
AddChangedUnit(reinforcementsResult, changedUnit);
|
||||
}
|
||||
|
||||
eventResults.actionResults.push_back(reinforcementsResult);
|
||||
return eventResults;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
#ifndef TutorialBattleController_hpp
|
||||
#define TutorialBattleController_hpp
|
||||
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/tutorial_battle_config.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
|
||||
@@ -27,6 +29,19 @@ using TutorialEventProto = net::eagle0::common::TutorialEvent;
|
||||
using TutorialTriggerProto = net::eagle0::common::TutorialTrigger;
|
||||
using TutorialActionProto = net::eagle0::common::TutorialAction;
|
||||
|
||||
/// Info about a pending reinforcement placement phase.
|
||||
struct ReinforcementPlacementInfo {
|
||||
PlayerId playerId;
|
||||
std::vector<Coords> startingPositions;
|
||||
};
|
||||
|
||||
/// Results from tutorial event processing, including action results and any pending
|
||||
/// reinforcement placement.
|
||||
struct TutorialEventResults {
|
||||
std::vector<ActionResult> actionResults;
|
||||
std::optional<ReinforcementPlacementInfo> reinforcementPlacement;
|
||||
};
|
||||
|
||||
/// Controller for scripted tutorial battles with event-driven triggers and actions.
|
||||
/// Events are checked at the end of each round and fire in config order.
|
||||
/// Each event fires at most once (tracked by event_id).
|
||||
@@ -47,7 +62,7 @@ public:
|
||||
[[nodiscard]] auto CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) -> std::vector<ActionResult>;
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) -> TutorialEventResults;
|
||||
|
||||
/// Gets the set of fired event IDs (for testing/debugging).
|
||||
[[nodiscard]] auto GetFiredEventIds() const -> const std::set<std::string>& {
|
||||
@@ -68,8 +83,7 @@ private:
|
||||
const TutorialActionProto& action,
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) const
|
||||
-> std::vector<ActionResult>;
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) const -> TutorialEventResults;
|
||||
|
||||
// Helper methods for trigger evaluation
|
||||
|
||||
@@ -91,10 +105,10 @@ private:
|
||||
const net::eagle0::common::FleeAction& fleeAction,
|
||||
const GameStateW& state) const -> std::vector<ActionResult>;
|
||||
|
||||
/// Executes a reinforcements action.
|
||||
/// Executes a reinforcements action, placing units directly at available starting positions.
|
||||
[[nodiscard]] auto ExecuteReinforcementsAction(
|
||||
const net::eagle0::common::ReinforcementsAction& reinforcementsAction) const
|
||||
-> std::vector<ActionResult>;
|
||||
const net::eagle0::common::ReinforcementsAction& reinforcementsAction,
|
||||
const GameStateW& state) const -> TutorialEventResults;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -331,7 +331,8 @@ auto PlacedUnitsForPlayer(const Units *units, const PlayerId pid)
|
||||
std::map<UnitId, const Unit *> placedUnits{};
|
||||
for (const Unit *unit : *units) {
|
||||
if (unit->player_id() == pid &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT)
|
||||
placedUnits[unit->unit_id()] = unit;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ void DumpGameStateToFile(const GameStateW& gameState, const std::string& gameId)
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
out << "RETREATED_UNIT\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
|
||||
out << "PENDING_REINFORCEMENT\n";
|
||||
break;
|
||||
default: out << "OTHER (" << (int)unit->status() << ")\n";
|
||||
}
|
||||
out << " Remaining Action Points: " << (int)unit->remaining_action_points() << "\n";
|
||||
|
||||
@@ -49,7 +49,8 @@ auto GameStateFilteredForPlayer(
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT: break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT: break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
|
||||
throw ShardokInternalErrorException("Unknown unit status");
|
||||
@@ -100,6 +101,7 @@ auto GameStateFilteredForPlayer(
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT: break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ cc_library(
|
||||
deps = [
|
||||
":server_configuration",
|
||||
"//src/main/cpp/net/eagle0/common:tsv_parser",
|
||||
"//src/main/cpp/net/eagle0/common:unit_conversions",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
|
||||
"//src/main/cpp/net/eagle0/shardok/controller",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
|
||||
@@ -222,20 +222,14 @@ auto EagleInterfaceImpl::PostCommand(
|
||||
request->game_setup_info().known_result_count(),
|
||||
response);
|
||||
} catch (ShardokClientErrorException &e) {
|
||||
std::cerr << "Bad request from client!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
std::cerr << "Bad request from client: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INVALID_ARGUMENT, e.what());
|
||||
} catch (ShardokInternalErrorException &e) {
|
||||
std::cerr << "ShardokInternalErrorException thrown!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
std::cerr << "Internal error: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << "Unknown exception in client->PostCommand" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
std::cerr << "Unknown exception: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
@@ -274,20 +268,14 @@ auto EagleInterfaceImpl::PostPlacementCommands(
|
||||
request->game_setup_info().known_result_count(),
|
||||
response);
|
||||
} catch (ShardokClientErrorException &e) {
|
||||
std::cerr << "Bad request from client!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
std::cerr << "Bad request from client: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INVALID_ARGUMENT, e.what());
|
||||
} catch (ShardokInternalErrorException &e) {
|
||||
std::cerr << "ShardokInternalErrorException thrown!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
std::cerr << "Internal error: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << "Unknown exception in client->PostCommand" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
std::cerr << "Unknown exception: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "ServerConfiguration.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/controller/ShardokGameController.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings_loader/SettingsLoader.hpp"
|
||||
@@ -151,6 +152,34 @@ auto SetUpController(
|
||||
unplacedUnits.push_back(up);
|
||||
}
|
||||
|
||||
// Add reinforcement units from tutorial config as PENDING_REINFORCEMENT.
|
||||
// These units exist in the game state from the start but are invisible and inactive
|
||||
// until the TutorialBattleController activates them at the triggered round.
|
||||
if (tutorialConfig.enabled()) {
|
||||
vector<PlayerId> allPlayerIds;
|
||||
for (size_t i = 0; i < playerInfos.size(); i++) {
|
||||
allPlayerIds.push_back(static_cast<PlayerId>(i));
|
||||
}
|
||||
|
||||
for (const auto &event : tutorialConfig.events()) {
|
||||
if (!event.has_action() ||
|
||||
event.action().action_type_case() !=
|
||||
net::eagle0::common::TutorialAction::kReinforcements) {
|
||||
continue;
|
||||
}
|
||||
const auto &reinforcements = event.action().reinforcements();
|
||||
const PlayerId playerId = reinforcements.player_id();
|
||||
|
||||
for (const auto &commonUnit : reinforcements.units()) {
|
||||
Unit unit = ConvertUnit(commonUnit, playerId, allPlayerIds);
|
||||
unit.mutate_unit_id(nextUnitId++);
|
||||
unit.mutate_status(
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT);
|
||||
unplacedUnits.push_back(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unique_ptr<ShardokEngine> engine = std::make_unique<ShardokEngine>(
|
||||
gameSettings,
|
||||
std::move(unplacedUnits),
|
||||
|
||||
@@ -29,6 +29,7 @@ ExportedObj/
|
||||
# Unity3D generated meta files
|
||||
*.pidb.meta
|
||||
*.pdb.meta
|
||||
Assets/GeneratedProtos.meta
|
||||
|
||||
# Unity3D Generated File On Crash Reports
|
||||
sysinfo.txt
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -15,7 +15,7 @@ MonoBehaviour:
|
||||
m_DefaultGroup: 4458439bbfafa478b905417eff3c5741
|
||||
m_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: 00000000000000000000000000000000
|
||||
Hash: 0e8900f37a8b7b1c88049de57e371339
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 1
|
||||
m_CatalogRequestsTimeout: 0
|
||||
|
||||
+92
-62
@@ -108,8 +108,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public TextMeshProUGUI connectionStatusText;
|
||||
|
||||
[Header("Connection Panel Environment")]
|
||||
[Tooltip("Environment dropdown in connection panel (fallback if lobby unreachable)")]
|
||||
public TMP_Dropdown connectionEnvironmentDropdown;
|
||||
[Tooltip("Connect to QA button in connection panel (editor-only)")]
|
||||
public Button connectToQAButton;
|
||||
|
||||
public GameObject connectionPanel;
|
||||
public GameObject connectionBackgroundLayer;
|
||||
@@ -121,7 +121,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public Button logoutButton;
|
||||
public Button customBattleButton;
|
||||
public Button cancelCustomBattleButton;
|
||||
public TMP_Dropdown lobbyEnvironmentDropdown;
|
||||
public Button lobbyConnectToQAButton;
|
||||
public TextMeshProUGUI lobbyUserText;
|
||||
public TextMeshProUGUI lobbyStatusText;
|
||||
|
||||
@@ -150,6 +150,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly Object pendingReplyLock = new Object();
|
||||
private int _connectedEnvironmentIndex = -1; // -1 means not connected
|
||||
private bool _isLocalQAConnection = false;
|
||||
private List<StoredAccountButton> _storedAccountButtons = new();
|
||||
private bool _exitedFullscreenForNewUser = false; // Track if we exited fullscreen for login
|
||||
private FullScreenMode _previousFullScreenMode; // Store mode to restore after login
|
||||
@@ -168,7 +169,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
/// Returns the connected environment name ("prod." or "qa.") or null if not connected.
|
||||
/// </summary>
|
||||
public string ConnectedEnvironmentName =>
|
||||
_connectedEnvironmentIndex >= 0 &&
|
||||
_isLocalQAConnection ? "QA (local)"
|
||||
: _connectedEnvironmentIndex >= 0 &&
|
||||
_connectedEnvironmentIndex < EnvironmentDisplayNames.Count
|
||||
? EnvironmentDisplayNames[_connectedEnvironmentIndex]
|
||||
: null;
|
||||
@@ -218,8 +220,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
// Initialize OAuth UI
|
||||
SetupOAuthUI();
|
||||
|
||||
// Initialize connection panel environment dropdown
|
||||
SetupConnectionEnvironmentDropdown();
|
||||
// Initialize connection panel QA button (editor-only)
|
||||
SetupConnectToQAButton();
|
||||
|
||||
// Initialize Lobby UI (logout button, etc.)
|
||||
SetupLobbyUI();
|
||||
@@ -235,27 +237,17 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
TryRestoreSession();
|
||||
}
|
||||
|
||||
private void SetupConnectionEnvironmentDropdown() {
|
||||
if (connectionEnvironmentDropdown == null) return;
|
||||
private void SetupConnectToQAButton() {
|
||||
if (connectToQAButton == null) return;
|
||||
|
||||
// Only show environment dropdown in Unity Editor; always use prod in builds
|
||||
// Only show QA button in Unity Editor; always use prod in builds
|
||||
if (!Application.isEditor) {
|
||||
connectionEnvironmentDropdown.gameObject.SetActive(false);
|
||||
connectToQAButton.gameObject.SetActive(false);
|
||||
PlayerPrefs.SetInt(EnvironmentKey, 0); // Force prod
|
||||
return;
|
||||
}
|
||||
|
||||
connectionEnvironmentDropdown.ClearOptions();
|
||||
connectionEnvironmentDropdown.AddOptions(EnvironmentDisplayNames);
|
||||
connectionEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
connectionEnvironmentDropdown.onValueChanged.AddListener(OnConnectionEnvironmentChanged);
|
||||
}
|
||||
|
||||
private void OnConnectionEnvironmentChanged(int newEnvironmentIndex) {
|
||||
// Just save the preference - it will be used on next connection attempt
|
||||
PlayerPrefs.SetInt(EnvironmentKey, newEnvironmentIndex);
|
||||
Debug.Log(
|
||||
$"[ConnectionHandler] Environment changed to {EnvironmentDisplayNames[newEnvironmentIndex]}");
|
||||
connectToQAButton.onClick.AddListener(OnConnectToQAClicked);
|
||||
}
|
||||
|
||||
private void SetupOAuthUI() {
|
||||
@@ -295,15 +287,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
// Show OAuth panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(true);
|
||||
|
||||
// Sync connection environment dropdown with saved preference
|
||||
if (connectionEnvironmentDropdown != null) {
|
||||
connectionEnvironmentDropdown.onValueChanged.RemoveListener(
|
||||
OnConnectionEnvironmentChanged);
|
||||
connectionEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
connectionEnvironmentDropdown.onValueChanged.AddListener(
|
||||
OnConnectionEnvironmentChanged);
|
||||
}
|
||||
|
||||
// Refresh stored account buttons
|
||||
RefreshStoredAccountButtons();
|
||||
|
||||
@@ -416,35 +399,46 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
tutorialButton.gameObject.SetActive(TokenStorage.DisplayName == "nolen");
|
||||
tutorialButton.onClick.AddListener(CreateTutorialGame);
|
||||
|
||||
// Set up environment dropdown (only in Unity Editor)
|
||||
if (!Application.isEditor) {
|
||||
lobbyEnvironmentDropdown.gameObject.SetActive(false);
|
||||
} else {
|
||||
lobbyEnvironmentDropdown.ClearOptions();
|
||||
lobbyEnvironmentDropdown.AddOptions(EnvironmentDisplayNames);
|
||||
lobbyEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
|
||||
// Set up QA button (only in Unity Editor)
|
||||
if (lobbyConnectToQAButton != null) {
|
||||
if (!Application.isEditor) {
|
||||
lobbyConnectToQAButton.gameObject.SetActive(false);
|
||||
} else {
|
||||
lobbyConnectToQAButton.onClick.AddListener(OnLobbyConnectToQAClicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLobbyStatusDisplays() {
|
||||
// Update environment dropdown selection
|
||||
if (_connectedEnvironmentIndex >= 0) {
|
||||
// Temporarily remove listener to avoid triggering reconnect
|
||||
lobbyEnvironmentDropdown.onValueChanged.RemoveListener(OnLobbyEnvironmentChanged);
|
||||
lobbyEnvironmentDropdown.value = _connectedEnvironmentIndex;
|
||||
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
|
||||
}
|
||||
|
||||
// Show current user from OAuth
|
||||
lobbyUserText.text = TokenStorage.DisplayName ?? "";
|
||||
lobbyUserText.text = _isLocalQAConnection ? "editor" : TokenStorage.DisplayName ?? "";
|
||||
}
|
||||
|
||||
private void OnLobbyEnvironmentChanged(int newEnvironmentIndex) {
|
||||
if (newEnvironmentIndex == _connectedEnvironmentIndex) return;
|
||||
private void OnConnectToQAClicked() {
|
||||
Debug.Log("[ConnectionHandler] Connect to QA clicked");
|
||||
_isLocalQAConnection = true;
|
||||
|
||||
Debug.Log(
|
||||
$"[ConnectionHandler] Switching environment from {ConnectedEnvironmentName} to {EnvironmentDisplayNames[newEnvironmentIndex]}");
|
||||
SetLobbyActive(true);
|
||||
connectionCanvas.enabled = true;
|
||||
|
||||
_createLocalQAConnection();
|
||||
|
||||
if (_persistentClientConnection == null) {
|
||||
Debug.LogError("[ConnectionHandler] QA connection creation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
RequestMaps();
|
||||
StartListeningForLobbyUpdates();
|
||||
} catch (Exception e) {
|
||||
MainQueue.Q.Enqueue(() => { errorHandler.Add(e); });
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLobbyConnectToQAClicked() {
|
||||
if (_isLocalQAConnection) return;
|
||||
|
||||
Debug.Log("[ConnectionHandler] Lobby: switching to QA (local)");
|
||||
|
||||
// Disconnect from current environment
|
||||
_persistentClientConnection?.Dispose();
|
||||
@@ -455,23 +449,54 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
eagleConnection = null;
|
||||
_httpClient = null;
|
||||
|
||||
// Update environment index and reconnect
|
||||
_connectedEnvironmentIndex = newEnvironmentIndex;
|
||||
PlayerPrefs.SetInt(EnvironmentKey, newEnvironmentIndex);
|
||||
_isLocalQAConnection = true;
|
||||
|
||||
// Clear current game lists while reconnecting
|
||||
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
|
||||
// Show loading indicator
|
||||
if (lobbyStatusText != null) { lobbyStatusText.text = "Switching environment..."; }
|
||||
if (lobbyStatusText != null) { lobbyStatusText.text = "Connecting to QA..."; }
|
||||
|
||||
// Reconnect to new environment
|
||||
_createConnection();
|
||||
_createLocalQAConnection();
|
||||
RequestMaps();
|
||||
StartListeningForLobbyUpdates();
|
||||
}
|
||||
|
||||
private void _createLocalQAConnection() {
|
||||
// Dispose existing connections before creating new ones
|
||||
_httpClient?.Dispose();
|
||||
_persistentClientConnection?.Dispose();
|
||||
eagleConnection?.Dispose();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
eagleConnection = EagleConnection.CreateInsecure("http://localhost:40032");
|
||||
|
||||
// Plain HttpClient with no auth for local QA — ResourceFetcher still uses prod assets
|
||||
_httpClient = new HttpClient();
|
||||
|
||||
_persistentClientConnection = new PersistentClientConnection(
|
||||
eagleConnection.EagleGrpcClient,
|
||||
eagleConnection.credentials,
|
||||
EagleConnection.EagleCancellationToken);
|
||||
_persistentClientConnection.OnChannelDead = () => {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
Debug.Log("[ConnectionHandler] QA channel dead, recreating connection");
|
||||
_createLocalQAConnection();
|
||||
});
|
||||
};
|
||||
_persistentClientConnection.OnCommandError = (errorMessage) => {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
Debug.LogError($"[ConnectionHandler] Command rejected by server: {errorMessage}");
|
||||
});
|
||||
};
|
||||
_persistentClientConnection.Connect();
|
||||
|
||||
PersistentClientConnection.Current = _persistentClientConnection;
|
||||
errorHandler.PersistentClientConnection = _persistentClientConnection;
|
||||
}
|
||||
|
||||
public async void OnLogoutClicked() {
|
||||
Debug.Log("[ConnectionHandler] Logout button clicked");
|
||||
|
||||
@@ -489,6 +514,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
_httpClient = null;
|
||||
_cancellationTokenSource = null;
|
||||
_connectedEnvironmentIndex = -1;
|
||||
_isLocalQAConnection = false;
|
||||
|
||||
// Return to connection screen
|
||||
gameSelectionPanel.SetActive(false);
|
||||
@@ -667,6 +693,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
eagleConnection = null;
|
||||
|
||||
_connectedEnvironmentIndex = -1;
|
||||
_isLocalQAConnection = false;
|
||||
}
|
||||
|
||||
public void OnResolutionChanged(int dropdownIndex) {
|
||||
@@ -705,7 +732,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
// Set up running games table
|
||||
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
foreach (var runningGame in lobbyResponse.RunningGames) {
|
||||
foreach (var runningGame in lobbyResponse.RunningGames
|
||||
.OrderBy(g => g.CreatedTimestampMillis)
|
||||
.ThenBy(g => g.GameId)) {
|
||||
var listItem = Instantiate(
|
||||
runningGamesListItemPrefab,
|
||||
runningGamesListArea.transform,
|
||||
@@ -743,8 +772,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
var listItem = Instantiate(
|
||||
availableGamesListItemPrefab,
|
||||
availableGamesListArea.transform,
|
||||
true);
|
||||
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
|
||||
false);
|
||||
var availableGameItem = listItem.GetComponent<AvailableGameItem>();
|
||||
availableGameItem.SetAvailableNewGame(
|
||||
availableGame.GameId,
|
||||
@@ -945,12 +973,14 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private void CreateGame(string leaderNameTextId, int humanPlayerCount, int totalPlayerCount) {
|
||||
// Track multiplayer status for tutorial system
|
||||
TutorialManager.IsMultiplayerGame = humanPlayerCount > 1;
|
||||
TutorialManager.IsTutorialGame = false;
|
||||
var game = _internalCreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount);
|
||||
}
|
||||
|
||||
public void CreateTutorialGame() {
|
||||
// Tutorial is always single-player
|
||||
TutorialManager.IsMultiplayerGame = false;
|
||||
TutorialManager.IsTutorialGame = true;
|
||||
// Eagle will set up the hardcoded tutorial scenario when isTutorial is true
|
||||
var game = _internalCreateGame(
|
||||
desiredLeaderTextId: "",
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace eagle {
|
||||
[Header("Snowflake Settings")]
|
||||
public Texture2D snowflakeTexture;
|
||||
public Material particleMaterial; // Assign in prefab to ensure shader is in build
|
||||
public float snowEmitRate = 40f; // More particles
|
||||
public float snowEmitRate = 45f;
|
||||
public float snowFallSpeed = 50f;
|
||||
public float snowLifetime = 4f; // Longer lifetime
|
||||
public float snowSize = 10f; // Slightly larger
|
||||
@@ -54,7 +54,7 @@ namespace eagle {
|
||||
main.startRotation = new ParticleSystem.MinMaxCurve(0f, Mathf.PI * 2f);
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 300; // More particles for full coverage
|
||||
main.maxParticles = 900;
|
||||
|
||||
var emission = _snowSystem.emission;
|
||||
emission.enabled = true;
|
||||
|
||||
@@ -350,6 +350,12 @@ namespace eagle {
|
||||
"CommandPanel",
|
||||
commandPanel.GetComponent<RectTransform>());
|
||||
}
|
||||
// Register Go to Battle button for dialogue highlight
|
||||
if (TutorialManager.Instance?.TargetRegistry != null && goToBattleButton != null) {
|
||||
TutorialManager.Instance.TargetRegistry.RegisterTarget(
|
||||
"GoToBattleButton",
|
||||
goToBattleButton.GetComponent<RectTransform>());
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.playModeStateChanged += (state) => OnPlayModeStateChanged(state);
|
||||
@@ -580,6 +586,12 @@ namespace eagle {
|
||||
return;
|
||||
}
|
||||
|
||||
// On first model, sync IsTutorialGame from the server-reported GameType.
|
||||
// This covers joining an existing game (where ConnectionHandler doesn't know the type).
|
||||
if (oldModel == null) {
|
||||
TutorialManager.IsTutorialGame = Model.GameType == GameType.Tutorial;
|
||||
}
|
||||
|
||||
// Start onboarding after first model update (UI panels are now populated)
|
||||
if (oldModel == null && TutorialManager.Instance != null &&
|
||||
!TutorialManager.Instance.State.OnboardingCompleted) {
|
||||
@@ -760,6 +772,9 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public void GoToBattle() {
|
||||
// End any active dialogue before transitioning to battle
|
||||
TutorialManager.Instance?.DialogueManager?.EndDialogue();
|
||||
|
||||
// Hide the button now, so if we get an exception below we can at least recover
|
||||
goToBattleButton.gameObject.SetActive(false);
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ namespace eagle {
|
||||
public List<ProvinceId> ProvincesForFaction(FactionId factionId);
|
||||
|
||||
public List<ChronicleEntry> ChronicleEntries { get; }
|
||||
|
||||
GameType GameType { get; }
|
||||
}
|
||||
|
||||
public class GameModelUpdater : IClientConnectionSubscriber, IGameStateProvider {
|
||||
@@ -201,6 +203,8 @@ namespace eagle {
|
||||
|
||||
public List<ChronicleEntry> ChronicleEntries { get; set; }
|
||||
|
||||
public GameType GameType => GsView.GameType;
|
||||
|
||||
public List<AvailableCommand> AvailableCommandsForProvince(ProvinceId pid) {
|
||||
if (AvailableCommandsByProvince.TryGetValue(pid, out var cmds)) {
|
||||
return cmds.Commands.ToList();
|
||||
@@ -298,7 +302,13 @@ namespace eagle {
|
||||
kv => kv.Value.ImagePath),
|
||||
battalionNames: _currentModel.BattalionNames.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value));
|
||||
kv => kv.Value),
|
||||
heroFallbackLookup: hid => {
|
||||
if (_currentModel.Heroes.TryGetValue(hid, out var hero)) {
|
||||
return (hero.NameTextId, hero.ImagePath);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return shardokModel;
|
||||
}
|
||||
@@ -932,6 +942,10 @@ namespace eagle {
|
||||
}
|
||||
|
||||
foreach (var ce in entry.UpdatedChronicleEntries) { UpdateChronicleEntry(ce); }
|
||||
|
||||
if (entry.NewGameType != GameType.UnknownGameType) {
|
||||
_currentModel.GsView.GameType = entry.NewGameType;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNewHistoryEntry(ActionResultView arv) {
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.BlizzardEffect
|
||||
snowflakeTexture: {fileID: 2800000, guid: 9a5b4e8c3d2f6b0a7c1e9d5f8a3b2c4d, type: 3}
|
||||
particleMaterial: {fileID: 2100000, guid: b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6, type: 2}
|
||||
snowEmitRate: 15
|
||||
snowEmitRate: 45
|
||||
snowFallSpeed: 40
|
||||
snowLifetime: 3
|
||||
snowSize: 8
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using ProvinceId = System.Int32;
|
||||
using FactionId = System.Int32;
|
||||
|
||||
@@ -12,6 +14,7 @@ namespace eagle {
|
||||
public EagleGameController eagleGameController;
|
||||
|
||||
public ProvinceColorManager provinceColorManager;
|
||||
public ProvinceBorderManager provinceBorderManager;
|
||||
public ProvinceWeatherController weatherController;
|
||||
public ProvinceFestivalController festivalController;
|
||||
public ProvinceEpidemicController epidemicController;
|
||||
@@ -126,14 +129,33 @@ 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,
|
||||
SelectedBorderCycle = 1.0f;
|
||||
|
||||
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,
|
||||
TargetedBorderCycle = 0.25f;
|
||||
|
||||
private const float CommandBorderMin = 1.5f, CommandBorderMax = 5f,
|
||||
CommandBorderCycle = 0.5f;
|
||||
private static readonly Color CommandBorderColor = new Color(0.3f, 0.3f, 1f, 0.85f);
|
||||
|
||||
private readonly HashSet<ProvinceId> _activeHighlights = new();
|
||||
|
||||
private byte[] _mapIdBytes;
|
||||
ProvinceId? _previouslyHoveredProvinceId;
|
||||
|
||||
private static readonly Color BlizzardTint = new Color(0.82f, 0.88f, 0.95f);
|
||||
|
||||
private Color ColorForProvince(ProvinceView province) {
|
||||
var color =
|
||||
PlayerColors.LightPlayerColor(province.RulingFactionId.GetValueOrDefault(0));
|
||||
@@ -143,9 +165,31 @@ namespace eagle {
|
||||
province.RulingFactionId.GetValueOrDefault(0));
|
||||
}
|
||||
|
||||
if (HasBlizzardEvent(province)) { color *= BlizzardTint; }
|
||||
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Start() {
|
||||
_mapIdBytes = provinceIDLoader.DecompressedBytes;
|
||||
Enabled = true;
|
||||
@@ -183,6 +227,12 @@ namespace eagle {
|
||||
void Update() {
|
||||
if (Model == null || Model.Provinces == null || Model.Provinces.Count == 0) { return; }
|
||||
|
||||
// Clear previous frame's border highlights
|
||||
foreach (var pid in _activeHighlights) {
|
||||
provinceBorderManager.ClearProvinceBorder(pid);
|
||||
}
|
||||
_activeHighlights.Clear();
|
||||
|
||||
var maybeHoveredProvinceId = ProvinceFromPoint(Input.mousePosition);
|
||||
if (Model != null && maybeHoveredProvinceId != _previouslyHoveredProvinceId) {
|
||||
_previouslyHoveredProvinceId = maybeHoveredProvinceId;
|
||||
@@ -197,18 +247,47 @@ namespace eagle {
|
||||
SetDefaultProvinceColor(commandProvince);
|
||||
}
|
||||
|
||||
provinceBorderManager.SetHighlightGroup(OverrideTargetedProvinces);
|
||||
|
||||
foreach (var pid in OverrideTargetedProvinces) {
|
||||
FlashProvince(pid, _targetColor, 0.25f);
|
||||
FlashProvince(pid, OverrideStrobeColor, OverrideBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
pid,
|
||||
OverrideBorderColor,
|
||||
OverrideBorderMin,
|
||||
OverrideBorderMax,
|
||||
OverrideBorderCycle);
|
||||
}
|
||||
} else {
|
||||
provinceBorderManager.ClearHighlightGroup();
|
||||
if (SelectedProvinceId is ProvinceId sPid) {
|
||||
FlashProvince(sPid, _selectionColor, 1.0f);
|
||||
var sBase = ColorForProvince(Model.Provinces[sPid]);
|
||||
var (sStrobe, sBorder) = ContrastingHighlightColors(sBase);
|
||||
FlashProvince(sPid, sStrobe, SelectedBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
sPid,
|
||||
sBorder,
|
||||
SelectedBorderMin,
|
||||
SelectedBorderMax,
|
||||
SelectedBorderCycle);
|
||||
}
|
||||
foreach (ProvinceId tPid in TargetedProvinces) {
|
||||
FlashProvince(tPid, _targetColor, 0.25f);
|
||||
FlashProvince(tPid, TargetedStrobeColor, TargetedBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
tPid,
|
||||
TargetedBorderColor,
|
||||
TargetedBorderMin,
|
||||
TargetedBorderMax,
|
||||
TargetedBorderCycle);
|
||||
}
|
||||
foreach (var commandProvince in ProvincesWithCommands) {
|
||||
FlashProvince(commandProvince, _commandAvailableColor, 0.5f);
|
||||
FlashProvince(commandProvince, _commandAvailableColor, CommandBorderCycle);
|
||||
HighlightProvinceBorder(
|
||||
commandProvince,
|
||||
CommandBorderColor,
|
||||
CommandBorderMin,
|
||||
CommandBorderMax,
|
||||
CommandBorderCycle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,6 +301,18 @@ namespace eagle {
|
||||
Mathf.PingPong(Time.time, duration)));
|
||||
}
|
||||
|
||||
private void HighlightProvinceBorder(
|
||||
ProvinceId provinceId,
|
||||
Color borderColor,
|
||||
float minWidth,
|
||||
float maxWidth,
|
||||
float cycleDuration) {
|
||||
float t = Mathf.PingPong(Time.time, cycleDuration) / cycleDuration;
|
||||
float width = Mathf.Lerp(minWidth, maxWidth, t);
|
||||
provinceBorderManager.SetProvinceBorder(provinceId, borderColor, width);
|
||||
_activeHighlights.Add(provinceId);
|
||||
}
|
||||
|
||||
public void OnLeftClick(Vector2 position) {
|
||||
SelectedProvinceId = ProvinceFromPoint(position);
|
||||
}
|
||||
@@ -235,6 +326,81 @@ namespace eagle {
|
||||
targetedProvincesChangedCallback.Invoke(TargetedProvinces);
|
||||
}
|
||||
|
||||
// ========== Province Highlight Proxy ==========
|
||||
|
||||
private readonly Dictionary<int, Vector4> _provinceBoundsCache =
|
||||
new Dictionary<int, Vector4>();
|
||||
private RectTransform _provinceHighlightProxy;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a RectTransform child of the map panel that overlays the given province.
|
||||
/// Used by the dialogue system to highlight provinces on the strategic map.
|
||||
/// Returns null if the province is not found.
|
||||
/// </summary>
|
||||
public RectTransform GetProvinceHighlightProxy(string provinceName) {
|
||||
if (Model == null) return null;
|
||||
|
||||
var province = Model.Provinces.Values.FirstOrDefault(p => p.Name == provinceName);
|
||||
if (province == null) return null;
|
||||
|
||||
if (!_provinceBoundsCache.TryGetValue(province.Id, out var bounds)) {
|
||||
bounds = ComputeProvinceBounds(province.Id);
|
||||
_provinceBoundsCache[province.Id] = bounds;
|
||||
}
|
||||
|
||||
ClearProvinceHighlightProxy();
|
||||
|
||||
int w = provinceIDLoader.mapWidth;
|
||||
int h = provinceIDLoader.mapHeight;
|
||||
|
||||
var panelPositions = gameObject.GetComponentInParent<PanelPositions>();
|
||||
var proxyGO = new GameObject("DialogueProvinceHighlight");
|
||||
proxyGO.AddComponent<CanvasRenderer>();
|
||||
proxyGO.transform.SetParent(panelPositions.mapPanelTransform, false);
|
||||
_provinceHighlightProxy = proxyGO.AddComponent<RectTransform>();
|
||||
|
||||
// Anchors in RectTransform space: (0,0) = bottom-left, (1,1) = top-right.
|
||||
// Province ID byte array: y=0 = bottom of map (matches RectTransform origin).
|
||||
_provinceHighlightProxy.anchorMin =
|
||||
new Vector2(bounds.x / w, bounds.y / h); // (minX, minY)
|
||||
_provinceHighlightProxy.anchorMax =
|
||||
new Vector2(bounds.z / w, bounds.w / h); // (maxX, maxY)
|
||||
_provinceHighlightProxy.offsetMin = Vector2.zero;
|
||||
_provinceHighlightProxy.offsetMax = Vector2.zero;
|
||||
|
||||
return _provinceHighlightProxy;
|
||||
}
|
||||
|
||||
public void ClearProvinceHighlightProxy() {
|
||||
if (_provinceHighlightProxy != null) {
|
||||
Destroy(_provinceHighlightProxy.gameObject);
|
||||
_provinceHighlightProxy = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the province ID bytes to find the bounding box of a province.
|
||||
/// Returns Vector4(minX, minY, maxX, maxY) in map pixel coordinates.
|
||||
/// </summary>
|
||||
private Vector4 ComputeProvinceBounds(int provinceId) {
|
||||
int w = provinceIDLoader.mapWidth;
|
||||
int h = provinceIDLoader.mapHeight;
|
||||
int minX = w, minY = h, maxX = 0, maxY = 0;
|
||||
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
if (_mapIdBytes[y * w + x] == provinceId) {
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Vector4(minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
void SetUpCenterText(ProvinceId? maybeProvinceId, Color color) {
|
||||
provinceNameText.color = color;
|
||||
provinceOwnerText.color = color;
|
||||
|
||||
+22
@@ -23,10 +23,30 @@ Material:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BorderColorLookup:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BorderDistanceMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BorderWidthLookup:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ColorLookup:
|
||||
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}
|
||||
@@ -43,6 +63,7 @@ Material:
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _Cutoff: 0.308
|
||||
- _DefaultBorderWidth: 0.5
|
||||
- _OceanWaveSpeed: 2
|
||||
- _OceanWaveStrength: 0.0337
|
||||
- _Stencil: 0
|
||||
@@ -51,6 +72,7 @@ Material:
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
m_Colors:
|
||||
- _DefaultBorderColor: {r: 0.15, g: 0.1, b: 0.05, a: 0.8}
|
||||
- _OceanColor: {r: 0.4, g: 0.54901963, b: 0.8, a: 1}
|
||||
- _OceanScrollSpeed: {r: 0.06, g: 0.03, b: 0, a: 0}
|
||||
m_BuildTextureStacks: []
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Generates a distance-to-border texture from the province ID map at startup.
|
||||
/// 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);
|
||||
|
||||
_distanceTexture = new Texture2D(w, h, TextureFormat.R8, false);
|
||||
_distanceTexture.filterMode = FilterMode.Bilinear;
|
||||
_distanceTexture.wrapMode = TextureWrapMode.Clamp;
|
||||
_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) {
|
||||
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++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int idx = y * w + x;
|
||||
byte id = ids[idx];
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Forward pass (top-left to bottom-right)
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int idx = y * w + x;
|
||||
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;
|
||||
}
|
||||
|
||||
dist[idx] = d;
|
||||
if (bestIdx != idx) { crossIds[idx] = crossIds[bestIdx]; }
|
||||
}
|
||||
}
|
||||
|
||||
// Backward pass (bottom-right to top-left)
|
||||
for (int y = h - 1; y >= 0; y--) {
|
||||
for (int x = w - 1; x >= 0; x--) {
|
||||
int idx = y * w + x;
|
||||
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;
|
||||
}
|
||||
|
||||
dist[idx] = d;
|
||||
if (bestIdx != idx) { crossIds[idx] = crossIds[bestIdx]; }
|
||||
}
|
||||
}
|
||||
|
||||
// Pack into byte arrays
|
||||
var distResult = new byte[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
distResult[i] = (byte)Mathf.Min(Mathf.RoundToInt(dist[i]), 255);
|
||||
}
|
||||
return (distResult, crossIds);
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_distanceTexture != null) { Destroy(_distanceTexture); }
|
||||
if (_crossProvinceTexture != null) { Destroy(_crossProvinceTexture); }
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00eaff8c0b6d46d6bf24c2b841376b0f
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// 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 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);
|
||||
_borderColorLookup.filterMode = FilterMode.Point;
|
||||
_borderColorLookup.wrapMode = TextureWrapMode.Clamp;
|
||||
|
||||
_borderWidthLookup = new Texture2D(256, 1, TextureFormat.RGBA32, false);
|
||||
_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) {
|
||||
if (provinceId < 0 || provinceId > 255) return;
|
||||
_borderColors[provinceId] = color;
|
||||
_borderWidths[provinceId] = new Color(widthScreenPx / 255f, 0, 0, 0);
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
public void ClearProvinceBorder(int provinceId) {
|
||||
if (provinceId < 0 || provinceId > 255) return;
|
||||
_borderColors[provinceId] = Color.clear;
|
||||
_borderWidths[provinceId] = Color.clear;
|
||||
_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);
|
||||
_borderColorLookup.Apply();
|
||||
_borderWidthLookup.SetPixels(_borderWidths);
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bce7ac95ae2407096ea1d4af29f04e0
|
||||
+63
-3
@@ -12,6 +12,17 @@ Shader "Eagle/ProvinceMap"
|
||||
_OceanWaveSpeed ("Ocean Wave Speed", Float) = 1.0
|
||||
_Cutoff ("Alpha cutoff", Range (0,1)) = 0.1
|
||||
|
||||
// Province border rendering
|
||||
_BorderDistanceMap ("Border Distance Map", 2D) = "white" {}
|
||||
_BorderColorLookup ("Border Color Lookup (256x1)", 2D) = "black" {}
|
||||
_BorderWidthLookup ("Border Width Lookup (256x1)", 2D) = "black" {}
|
||||
_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
|
||||
@@ -80,13 +91,21 @@ Shader "Eagle/ProvinceMap"
|
||||
sampler2D _ProvinceIDMap;
|
||||
sampler2D _ColorLookup;
|
||||
sampler2D _OceanTex;
|
||||
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;
|
||||
float _OceanWaveSpeed;
|
||||
float _Cutoff;
|
||||
float _DefaultBorderWidth;
|
||||
fixed4 _DefaultBorderColor;
|
||||
float4 _ClipRect;
|
||||
|
||||
v2f vert (appdata v)
|
||||
@@ -131,9 +150,50 @@ Shader "Eagle/ProvinceMap"
|
||||
}
|
||||
else
|
||||
{
|
||||
// Look up color from 256x1 texture
|
||||
// provinceId is already 0-1 normalized, use directly as U coordinate
|
||||
color = tex2D(_ColorLookup, float2(provinceId, 0.5));
|
||||
// Look up fill color from 256x1 texture
|
||||
fixed4 fillColor = tex2D(_ColorLookup, float2(provinceId, 0.5));
|
||||
|
||||
// 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;
|
||||
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)
|
||||
{
|
||||
float edge = 1.0 - saturate((dist - effectiveHighlightWidth + highlightTransition) / highlightTransition);
|
||||
color = lerp(fillColor, highlightColor, highlightColor.a * edge);
|
||||
}
|
||||
else if (dist <= effectiveDefaultWidth)
|
||||
{
|
||||
float edge = 1.0 - saturate((dist - effectiveDefaultWidth + defaultTransition) / defaultTransition);
|
||||
color = lerp(fillColor, _DefaultBorderColor, _DefaultBorderColor.a * edge);
|
||||
}
|
||||
else
|
||||
{
|
||||
color = fillColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Alpha cutoff
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Berkorszag",
|
||||
"centroid_x": 1757.8,
|
||||
"centroid_y": 1231.9,
|
||||
"centroid_x": 1767.0,
|
||||
"centroid_y": 1296.0,
|
||||
"area": 247272,
|
||||
"orientation": -20.1884829564446,
|
||||
"principal_length": 873.5,
|
||||
@@ -14,8 +14,8 @@
|
||||
{
|
||||
"id": 27,
|
||||
"name": "Chapellia",
|
||||
"centroid_x": 3076.8,
|
||||
"centroid_y": 508.9,
|
||||
"centroid_x": 3077.0,
|
||||
"centroid_y": 509.0,
|
||||
"area": 37546,
|
||||
"orientation": -70.52505264723656,
|
||||
"principal_length": 297.6,
|
||||
@@ -25,8 +25,8 @@
|
||||
{
|
||||
"id": 28,
|
||||
"name": "Kezonoria",
|
||||
"centroid_x": 2204.7,
|
||||
"centroid_y": 1263.1,
|
||||
"centroid_x": 2215.0,
|
||||
"centroid_y": 1251.0,
|
||||
"area": 126885,
|
||||
"orientation": -35.386786918677814,
|
||||
"principal_length": 753.6,
|
||||
@@ -36,7 +36,7 @@
|
||||
{
|
||||
"id": 29,
|
||||
"name": "Mesh",
|
||||
"centroid_x": 2832.3,
|
||||
"centroid_x": 2832.0,
|
||||
"centroid_y": 1017.0,
|
||||
"area": 197788,
|
||||
"orientation": -26.173956537897066,
|
||||
@@ -47,8 +47,8 @@
|
||||
{
|
||||
"id": 36,
|
||||
"name": "Pemeria",
|
||||
"centroid_x": 2839.4,
|
||||
"centroid_y": 1290.4,
|
||||
"centroid_x": 2835.0,
|
||||
"centroid_y": 1282.0,
|
||||
"area": 36356,
|
||||
"orientation": -44.16012147839107,
|
||||
"principal_length": 457.1,
|
||||
@@ -58,8 +58,8 @@
|
||||
{
|
||||
"id": 37,
|
||||
"name": "Yuetia",
|
||||
"centroid_x": 896.1,
|
||||
"centroid_y": 1227.1,
|
||||
"centroid_x": 902.0,
|
||||
"centroid_y": 1132.0,
|
||||
"area": 362211,
|
||||
"orientation": 0.6747576339005641,
|
||||
"principal_length": 1261.0,
|
||||
@@ -69,8 +69,8 @@
|
||||
{
|
||||
"id": 39,
|
||||
"name": "Kojaria",
|
||||
"centroid_x": 3233.1,
|
||||
"centroid_y": 678.6,
|
||||
"centroid_x": 3234.0,
|
||||
"centroid_y": 678.0,
|
||||
"area": 37489,
|
||||
"orientation": -25.04939634407932,
|
||||
"principal_length": 301.9,
|
||||
@@ -80,8 +80,8 @@
|
||||
{
|
||||
"id": 43,
|
||||
"name": "Chia",
|
||||
"centroid_x": 1134.5,
|
||||
"centroid_y": 1519.3,
|
||||
"centroid_x": 1177.0,
|
||||
"centroid_y": 1499.0,
|
||||
"area": 75627,
|
||||
"orientation": -35.91298964206631,
|
||||
"principal_length": 441.6,
|
||||
@@ -91,8 +91,8 @@
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Alah",
|
||||
"centroid_x": 3057.6,
|
||||
"centroid_y": 1330.3,
|
||||
"centroid_x": 3056.0,
|
||||
"centroid_y": 1330.0,
|
||||
"area": 35076,
|
||||
"orientation": -25.515287772906877,
|
||||
"principal_length": 285.6,
|
||||
@@ -102,8 +102,8 @@
|
||||
{
|
||||
"id": 34,
|
||||
"name": "Turton",
|
||||
"centroid_x": 2586.7,
|
||||
"centroid_y": 1506.5,
|
||||
"centroid_x": 2598.0,
|
||||
"centroid_y": 1500.0,
|
||||
"area": 37458,
|
||||
"orientation": -89.27834794069042,
|
||||
"principal_length": 295.6,
|
||||
@@ -113,8 +113,8 @@
|
||||
{
|
||||
"id": 41,
|
||||
"name": "Parnia",
|
||||
"centroid_x": 397.9,
|
||||
"centroid_y": 962.3,
|
||||
"centroid_x": 398.0,
|
||||
"centroid_y": 963.0,
|
||||
"area": 184733,
|
||||
"orientation": 80.31028460303715,
|
||||
"principal_length": 816.0,
|
||||
@@ -124,8 +124,8 @@
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Oryslia",
|
||||
"centroid_x": 2494.9,
|
||||
"centroid_y": 1104.4,
|
||||
"centroid_x": 2466.0,
|
||||
"centroid_y": 1058.0,
|
||||
"area": 108679,
|
||||
"orientation": 53.601133897917535,
|
||||
"principal_length": 673.2,
|
||||
@@ -135,7 +135,7 @@
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Soria",
|
||||
"centroid_x": 611.2,
|
||||
"centroid_x": 611.0,
|
||||
"centroid_y": 587.0,
|
||||
"area": 37567,
|
||||
"orientation": 73.04044050523646,
|
||||
@@ -146,7 +146,7 @@
|
||||
{
|
||||
"id": 26,
|
||||
"name": "Reigia",
|
||||
"centroid_x": 1538.6,
|
||||
"centroid_x": 1538.0,
|
||||
"centroid_y": 824.0,
|
||||
"area": 105738,
|
||||
"orientation": -56.461232070203295,
|
||||
@@ -157,8 +157,8 @@
|
||||
{
|
||||
"id": 21,
|
||||
"name": "Sigkarl",
|
||||
"centroid_x": 1776.1,
|
||||
"centroid_y": 688.7,
|
||||
"centroid_x": 1776.0,
|
||||
"centroid_y": 688.0,
|
||||
"area": 52475,
|
||||
"orientation": -80.26596706568765,
|
||||
"principal_length": 411.4,
|
||||
@@ -168,8 +168,8 @@
|
||||
{
|
||||
"id": 42,
|
||||
"name": "Grytrand",
|
||||
"centroid_x": 1064.1,
|
||||
"centroid_y": 369.5,
|
||||
"centroid_x": 1071.0,
|
||||
"centroid_y": 373.0,
|
||||
"area": 37511,
|
||||
"orientation": 61.866124085696136,
|
||||
"principal_length": 315.1,
|
||||
@@ -179,8 +179,8 @@
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Al Raala",
|
||||
"centroid_x": 3164.8,
|
||||
"centroid_y": 1142.9,
|
||||
"centroid_x": 3165.0,
|
||||
"centroid_y": 1142.0,
|
||||
"area": 64624,
|
||||
"orientation": 1.6109698686653782,
|
||||
"principal_length": 417.0,
|
||||
@@ -190,8 +190,8 @@
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Laufarvia",
|
||||
"centroid_x": 770.1,
|
||||
"centroid_y": 437.8,
|
||||
"centroid_x": 770.0,
|
||||
"centroid_y": 437.0,
|
||||
"area": 37521,
|
||||
"orientation": -42.64914861683819,
|
||||
"principal_length": 253.6,
|
||||
@@ -201,7 +201,7 @@
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Atfordia",
|
||||
"centroid_x": 2473.6,
|
||||
"centroid_x": 2474.0,
|
||||
"centroid_y": 527.0,
|
||||
"area": 78637,
|
||||
"orientation": -15.123132625107454,
|
||||
@@ -212,8 +212,8 @@
|
||||
{
|
||||
"id": 33,
|
||||
"name": "Usvol",
|
||||
"centroid_x": 939.2,
|
||||
"centroid_y": 417.9,
|
||||
"centroid_x": 939.0,
|
||||
"centroid_y": 418.0,
|
||||
"area": 37617,
|
||||
"orientation": 72.87731176765703,
|
||||
"principal_length": 329.4,
|
||||
@@ -223,8 +223,8 @@
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Bolve",
|
||||
"centroid_x": 1554.3,
|
||||
"centroid_y": 567.8,
|
||||
"centroid_x": 1554.0,
|
||||
"centroid_y": 568.0,
|
||||
"area": 37519,
|
||||
"orientation": -28.254514479055615,
|
||||
"principal_length": 297.1,
|
||||
@@ -234,8 +234,8 @@
|
||||
{
|
||||
"id": 17,
|
||||
"name": "Hofolen",
|
||||
"centroid_x": 1193.4,
|
||||
"centroid_y": 496.5,
|
||||
"centroid_x": 1192.0,
|
||||
"centroid_y": 497.0,
|
||||
"area": 37530,
|
||||
"orientation": -40.73421792639953,
|
||||
"principal_length": 321.3,
|
||||
@@ -245,8 +245,8 @@
|
||||
{
|
||||
"id": 15,
|
||||
"name": "Pieksa",
|
||||
"centroid_x": 3373.6,
|
||||
"centroid_y": 752.0,
|
||||
"centroid_x": 3372.0,
|
||||
"centroid_y": 753.0,
|
||||
"area": 37474,
|
||||
"orientation": -42.30480488812397,
|
||||
"principal_length": 302.7,
|
||||
@@ -256,8 +256,8 @@
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Shumal",
|
||||
"centroid_x": 2841.2,
|
||||
"centroid_y": 1426.4,
|
||||
"centroid_x": 2841.0,
|
||||
"centroid_y": 1426.0,
|
||||
"area": 35197,
|
||||
"orientation": -26.08271079122629,
|
||||
"principal_length": 286.9,
|
||||
@@ -267,8 +267,8 @@
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Musland",
|
||||
"centroid_x": 2776.1,
|
||||
"centroid_y": 685.8,
|
||||
"centroid_x": 2776.0,
|
||||
"centroid_y": 686.0,
|
||||
"area": 88353,
|
||||
"orientation": -67.33176588364896,
|
||||
"principal_length": 410.3,
|
||||
@@ -278,8 +278,8 @@
|
||||
{
|
||||
"id": 19,
|
||||
"name": "Hella",
|
||||
"centroid_x": 809.4,
|
||||
"centroid_y": 600.1,
|
||||
"centroid_x": 810.0,
|
||||
"centroid_y": 600.0,
|
||||
"area": 37632,
|
||||
"orientation": -29.964829271657674,
|
||||
"principal_length": 292.4,
|
||||
@@ -289,7 +289,7 @@
|
||||
{
|
||||
"id": 24,
|
||||
"name": "Tatoros",
|
||||
"centroid_x": 2452.8,
|
||||
"centroid_x": 2453.0,
|
||||
"centroid_y": 1508.0,
|
||||
"area": 37547,
|
||||
"orientation": 64.24404335610743,
|
||||
@@ -300,8 +300,8 @@
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Vovimaria",
|
||||
"centroid_x": 608.9,
|
||||
"centroid_y": 818.4,
|
||||
"centroid_x": 613.0,
|
||||
"centroid_y": 807.0,
|
||||
"area": 37474,
|
||||
"orientation": 42.325246247463234,
|
||||
"principal_length": 451.9,
|
||||
@@ -311,8 +311,8 @@
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Onmaa",
|
||||
"centroid_x": 3464.3,
|
||||
"centroid_y": 864.9,
|
||||
"centroid_x": 3465.0,
|
||||
"centroid_y": 876.0,
|
||||
"area": 48302,
|
||||
"orientation": -33.08899725807026,
|
||||
"principal_length": 465.4,
|
||||
@@ -323,7 +323,7 @@
|
||||
"id": 32,
|
||||
"name": "Ingia",
|
||||
"centroid_x": 445.0,
|
||||
"centroid_y": 614.2,
|
||||
"centroid_y": 615.0,
|
||||
"area": 37475,
|
||||
"orientation": 40.330942997303495,
|
||||
"principal_length": 270.3,
|
||||
@@ -333,8 +333,8 @@
|
||||
{
|
||||
"id": 11,
|
||||
"name": "Garholtia",
|
||||
"centroid_x": 768.3,
|
||||
"centroid_y": 805.8,
|
||||
"centroid_x": 768.0,
|
||||
"centroid_y": 807.0,
|
||||
"area": 37509,
|
||||
"orientation": 73.25523522382795,
|
||||
"principal_length": 275.4,
|
||||
@@ -344,8 +344,8 @@
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Motcia",
|
||||
"centroid_x": 2989.1,
|
||||
"centroid_y": 672.1,
|
||||
"centroid_x": 2989.0,
|
||||
"centroid_y": 674.0,
|
||||
"area": 37541,
|
||||
"orientation": 71.01019565314077,
|
||||
"principal_length": 340.1,
|
||||
@@ -355,8 +355,8 @@
|
||||
{
|
||||
"id": 20,
|
||||
"name": "Elekes",
|
||||
"centroid_x": 2354.4,
|
||||
"centroid_y": 1346.4,
|
||||
"centroid_x": 2355.0,
|
||||
"centroid_y": 1346.0,
|
||||
"area": 37570,
|
||||
"orientation": -4.693928802826447,
|
||||
"principal_length": 300.8,
|
||||
@@ -366,8 +366,8 @@
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Oscasland",
|
||||
"centroid_x": 2033.5,
|
||||
"centroid_y": 740.9,
|
||||
"centroid_x": 2033.0,
|
||||
"centroid_y": 761.0,
|
||||
"area": 92595,
|
||||
"orientation": -7.875650568974214,
|
||||
"principal_length": 519.3,
|
||||
@@ -377,8 +377,8 @@
|
||||
{
|
||||
"id": 30,
|
||||
"name": "Tegrot",
|
||||
"centroid_x": 1736.5,
|
||||
"centroid_y": 1018.5,
|
||||
"centroid_x": 1736.0,
|
||||
"centroid_y": 1018.0,
|
||||
"area": 122426,
|
||||
"orientation": -20.247381662688625,
|
||||
"principal_length": 568.2,
|
||||
@@ -387,9 +387,9 @@
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "Faluria",
|
||||
"centroid_x": 976.7,
|
||||
"centroid_y": 744.3,
|
||||
"name": "West Faluria",
|
||||
"centroid_x": 976.0,
|
||||
"centroid_y": 746.0,
|
||||
"area": 80344,
|
||||
"orientation": -62.2520755175581,
|
||||
"principal_length": 494.3,
|
||||
@@ -398,9 +398,9 @@
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "Fluria",
|
||||
"centroid_x": 1245.7,
|
||||
"centroid_y": 835.6,
|
||||
"name": "East Faluria",
|
||||
"centroid_x": 1245.0,
|
||||
"centroid_y": 836.0,
|
||||
"area": 155157,
|
||||
"orientation": -66.31259041323624,
|
||||
"principal_length": 561.7,
|
||||
@@ -410,8 +410,8 @@
|
||||
{
|
||||
"id": 35,
|
||||
"name": "Tumala",
|
||||
"centroid_x": 3211.2,
|
||||
"centroid_y": 1409.8,
|
||||
"centroid_x": 3210.0,
|
||||
"centroid_y": 1409.0,
|
||||
"area": 25474,
|
||||
"orientation": 33.16003147165417,
|
||||
"principal_length": 283.2,
|
||||
@@ -421,8 +421,8 @@
|
||||
{
|
||||
"id": 31,
|
||||
"name": "Nikemi",
|
||||
"centroid_x": 3200.5,
|
||||
"centroid_y": 882.0,
|
||||
"centroid_x": 3200.0,
|
||||
"centroid_y": 881.0,
|
||||
"area": 40737,
|
||||
"orientation": 57.09128390939083,
|
||||
"principal_length": 378.6,
|
||||
@@ -432,8 +432,8 @@
|
||||
{
|
||||
"id": 22,
|
||||
"name": "Chipingia",
|
||||
"centroid_x": 2206.4,
|
||||
"centroid_y": 580.2,
|
||||
"centroid_x": 2209.0,
|
||||
"centroid_y": 588.0,
|
||||
"area": 40656,
|
||||
"orientation": -48.11179717863961,
|
||||
"principal_length": 286.5,
|
||||
@@ -443,8 +443,8 @@
|
||||
{
|
||||
"id": 23,
|
||||
"name": "Eivikia",
|
||||
"centroid_x": 1359.5,
|
||||
"centroid_y": 506.6,
|
||||
"centroid_x": 1360.0,
|
||||
"centroid_y": 506.0,
|
||||
"area": 37555,
|
||||
"orientation": -49.357592556477876,
|
||||
"principal_length": 285.3,
|
||||
@@ -454,8 +454,8 @@
|
||||
{
|
||||
"id": 38,
|
||||
"name": "Wichel",
|
||||
"centroid_x": 2399.6,
|
||||
"centroid_y": 792.4,
|
||||
"centroid_x": 2395.0,
|
||||
"centroid_y": 781.0,
|
||||
"area": 122281,
|
||||
"orientation": -26.154786547398853,
|
||||
"principal_length": 627.0,
|
||||
@@ -465,8 +465,8 @@
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Pozia",
|
||||
"centroid_x": 2950.6,
|
||||
"centroid_y": 338.6,
|
||||
"centroid_x": 2960.0,
|
||||
"centroid_y": 410.0,
|
||||
"area": 56289,
|
||||
"orientation": -65.77001402153415,
|
||||
"principal_length": 522.5,
|
||||
@@ -474,4 +474,4 @@
|
||||
"curvature": -9.3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,130 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14b4504cd5b0e466d985c9f7ac241f30
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: iOS
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -96,6 +96,14 @@ public class EagleConnection : IDisposable {
|
||||
return new EagleConnection(url, useJwt: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an insecure connection with no auth for local QA testing.
|
||||
/// Connects over plain HTTP with no interceptor. Use with a server running --skip-auth.
|
||||
/// </summary>
|
||||
public static EagleConnection CreateInsecure(string fullUrl) {
|
||||
return new EagleConnection(fullUrl, insecure: true, jwt: false);
|
||||
}
|
||||
|
||||
private EagleConnection(string url, bool useJwt) {
|
||||
// For JWT auth, get display name from TokenStorage
|
||||
playerName = TokenStorage.DisplayName ?? "Unknown";
|
||||
@@ -108,6 +116,26 @@ public class EagleConnection : IDisposable {
|
||||
EagleGrpcClient = new Eagle.EagleClient(MakeJwtInvoker(url));
|
||||
}
|
||||
|
||||
private EagleConnection(string fullUrl, bool insecure, bool jwt) {
|
||||
playerName = "editor";
|
||||
credentials = new Metadata { { "user", "editor" } };
|
||||
authHeader = null;
|
||||
|
||||
_loggerFactory = LoggerFactory.Create(
|
||||
builder => { builder.AddProvider(new CustomFileLoggerProvider()); });
|
||||
|
||||
_channel = GrpcChannel.ForAddress(
|
||||
fullUrl,
|
||||
new GrpcChannelOptions {
|
||||
HttpHandler = CreateHttpHandler(),
|
||||
DisposeHttpClient = true,
|
||||
LoggerFactory = _loggerFactory,
|
||||
MaxReceiveMessageSize = null
|
||||
});
|
||||
|
||||
EagleGrpcClient = new Eagle.EagleClient(_channel);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
_channel?.Dispose();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cd70b162309940b1bf97c3077e07322
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"scripts": [
|
||||
{
|
||||
"id": "tutorial_placement",
|
||||
"trigger": "shardok_placement_started",
|
||||
"panelPosition": "top",
|
||||
"steps": [
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "There they are. Tarn has brought his Shardok's Guard \u2014 the finest soldiers in the realm \u2014 and knights besides. Do not underestimate them, Sadar. Every one of those men has seen more battle than we care to imagine.",
|
||||
"instructionText": "Hostile forces appear with <color=#CC0000><b>red</b></color> text on the battlefield.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
},
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "We must hold the castles. If we can dig in and weather the assault, we may yet survive this. Position our forces carefully \u2014 every man counts today.",
|
||||
"instructionText": "Friendly units with actions available have <color=#0000FF><b>blue</b></color> text. Units without actions have <b>black</b> text.\n\nClick a unit to select it, then click a flashing eligible position to place it. Click <b>Commit</b> when finished.",
|
||||
"highlightTarget": "CommitButton",
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tutorial_battle_running",
|
||||
"trigger": "shardok_battle_running",
|
||||
"panelPosition": "top",
|
||||
"steps": [
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "Now the real fight begins. Remember, Sadar \u2014 Tarn wins if he wipes us out or seizes every castle on the field. We win if we destroy his forces, or simply hold out for a full month. Time is on our side, if we can endure.",
|
||||
"instructionText": "The <b>attacker</b> wins by eliminating the defender's forces or occupying all castles.\n\nThe <b>defender</b> wins by eliminating the attacker's forces or surviving a full month.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
},
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "For now, our best move is to hold position and let them come to us. The castles give us the advantage \u2014 we'd be fools to abandon that.",
|
||||
"instructionText": "You can move a unit by clicking it, then clicking an eligible destination. But for now, it's best to click <b>End Turn</b> and let the enemy approach.",
|
||||
"highlightTarget": "EndTurnButton",
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tutorial_archery",
|
||||
"trigger": "archery_available",
|
||||
"panelPosition": "top",
|
||||
"steps": [
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "The enemy draws near! Unleash everything we have, Sadar \u2014 every arrow, every bolt! Let them know the Reclamation will not go quietly!",
|
||||
"instructionText": "Click a unit to select it. Eligible archery targets will appear with <color=#CC00FF><b>purple outlines</b></color> \u2014 click one to fire.\n\n<b>Longbow</b> units (marked with a bow symbol) are especially effective against heavily armored targets like knights.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tutorial_charge",
|
||||
"trigger": "ability_charge_available",
|
||||
"panelPosition": "top",
|
||||
"steps": [
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "Sadar, look \u2014 one of our units has a clear line to charge! A charge delivers extra damage and can stun the enemy, especially with cavalry. We must seize the opportunity!",
|
||||
"instructionText": "When a unit can move to a position that allows a follow-up charge, the destination shows a <sprite name=\"Charge\"> icon. Click the unit, then click the marked destination to move and charge in one action.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tutorial_melee",
|
||||
"trigger": "melee_available",
|
||||
"panelPosition": "top",
|
||||
"steps": [
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "They're right on top of us! Steel to steel, then \u2014 give them a fight they won't forget!",
|
||||
"instructionText": "To melee attack, select a unit and <b>Shift-click</b> an adjacent enemy. You can also click the <b>Melee</b> button on the right, then click the target.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tutorial_thunderstorm",
|
||||
"trigger": "thunderstorm",
|
||||
"panelPosition": "top",
|
||||
"steps": [
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "A thunderstorm! The gods have a sense of timing, at least. Our bowmen won't be able to fire in this \u2014 but neither will theirs. And if anything's burning, the rain will put it out.",
|
||||
"instructionText": "<b>Thunderstorms</b> prevent all archery attacks and extinguish fires on the battlefield.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tutorial_start_fire",
|
||||
"trigger": "start_fire_available",
|
||||
"panelPosition": "top",
|
||||
"steps": [
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "We're close enough to set their position ablaze! Fire spreads quickly and can force the enemy to abandon favorable ground \u2014 or burn trying to hold it.",
|
||||
"instructionText": "Select a unit adjacent to the enemy, then click the <b>Start Fire</b> button on the right to set fire to a hex. Fire damages units that enter or remain in it, and spreads over time.",
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": null,
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 026a02f9c79a24ed69130cdb081343d7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"scripts": [
|
||||
{
|
||||
"id": "tutorial_opening",
|
||||
"trigger": "game_started",
|
||||
"steps": [
|
||||
{
|
||||
"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.",
|
||||
"instructionText": null,
|
||||
"highlightTarget": null,
|
||||
"completionEvent": null
|
||||
},
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "But Tarn still commands the royal army, and he is perhaps the most capable soldier this realm has ever seen. Your allies are scattered. You've been pushed back to Onmaa, your back to the sea. The rebellion teeters on the edge of defeat.\n\nThis is the last stand, Sadar. If we fall here, the Reclamation dies with us.",
|
||||
"instructionText": null,
|
||||
"highlightTarget": null,
|
||||
"highlightProvince": "Onmaa",
|
||||
"completionEvent": null
|
||||
},
|
||||
{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "Tarn's forces are already at the gates. If we survive this day... well. Perhaps there is still a way to reach the King's ear. But first, we must survive.\n\nJoin the battle. Let us make our stand.",
|
||||
"instructionText": "Click <b>Battle!</b> to enter tactical combat.",
|
||||
"highlightTarget": "GoToBattleButton",
|
||||
"completionEvent": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3aeca1f3713f4c08848a7bc55846f5a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -66,6 +66,9 @@ 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;
|
||||
@@ -794,7 +797,6 @@ 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)
|
||||
|
||||
+16
-5
@@ -452,7 +452,11 @@ namespace Shardok {
|
||||
Model.Month);
|
||||
}
|
||||
}
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Defer grid setup by one frame so Unity's layout system fully settles
|
||||
// after the Shardok container activation. Without this delay, mapArea may
|
||||
// report a stale size when SetUp() measures it, causing a visible grid
|
||||
// rebuild on the following frame.
|
||||
MainQueue.Q.EnqueueForNextUpdate(() => {
|
||||
hexGrid.height = map.RowCount;
|
||||
hexGrid.width = map.ColumnCount;
|
||||
hexGrid.textures = textures;
|
||||
@@ -472,6 +476,11 @@ namespace Shardok {
|
||||
|
||||
// Initialize tutorial system for tactical combat
|
||||
TutorialManager.Instance?.Initialize(null, this);
|
||||
var endTurnRT = endTurnButton.GetComponent<RectTransform>();
|
||||
TutorialManager.Instance?.TargetRegistry?.RegisterTarget("CommitButton", endTurnRT);
|
||||
TutorialManager.Instance?.TargetRegistry?.RegisterTarget(
|
||||
"EndTurnButton",
|
||||
endTurnRT);
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnBattleEntered(Model);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -1307,7 +1316,9 @@ namespace Shardok {
|
||||
command.Type ==
|
||||
CommandType.PlaceHiddenUnitCommand)
|
||||
.Where(command => command.Actor == unplacedUnit.UnitId);
|
||||
} else if (reservesController.SelectedUnit != null) {
|
||||
} else if (
|
||||
reservesController.SelectedUnit != null && SelectedCoords != null &&
|
||||
Model.UnitAtCoords(SelectedCoords) != null) {
|
||||
UnitViewWithName unplacedUnit = reservesController.SelectedUnit;
|
||||
|
||||
candidateCommands =
|
||||
@@ -2231,15 +2242,15 @@ namespace Shardok {
|
||||
}
|
||||
}
|
||||
|
||||
// Normal behavior: select the reserve unit for placement
|
||||
_selectedGridIndex = null;
|
||||
// Normal behavior: select the reserve unit for placement/reinforce
|
||||
if (Model.InSetUp) { _selectedGridIndex = null; }
|
||||
hexGrid.ClearOverlays();
|
||||
|
||||
var cmdGroup = commandTypeUIManager.CommandGroupForType(
|
||||
Model.InSetUp ? CommandType.PlaceUnitCommand
|
||||
: CommandType.ReinforceCommand);
|
||||
SetDisplayedCommandGroup(cmdGroup.Value);
|
||||
RedrawCommandOverlays(null, null);
|
||||
RedrawCommandOverlays(_selectedGridIndex, null);
|
||||
} else {
|
||||
SetDisplayedCommandGroup(0);
|
||||
hexGrid.ClearOverlays();
|
||||
|
||||
@@ -33,6 +33,7 @@ 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;
|
||||
@@ -56,6 +57,34 @@ 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);
|
||||
@@ -149,7 +178,8 @@ public class ShardokGameModel {
|
||||
List<PlayerWithHostility> players,
|
||||
Dictionary<HeroId, String> heroNameTextIds,
|
||||
Dictionary<HeroId, String> heroImages,
|
||||
Dictionary<BattalionId, String> battalionNames) {
|
||||
Dictionary<BattalionId, String> battalionNames,
|
||||
Func<HeroId, (string nameTextId, string imagePath)?> heroFallbackLookup = null) {
|
||||
EagleGameId = eagleGameId;
|
||||
ShardokGameId = shardokGameId;
|
||||
History = new List<ActionResultView>(StartingHistoryCapacity);
|
||||
@@ -166,6 +196,7 @@ 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();
|
||||
@@ -422,8 +453,7 @@ public class ShardokGameModel {
|
||||
string heroNameTextId = null;
|
||||
string headshotPath = null;
|
||||
if (changedUnit.AttachedHero?.EagleHeroId is HeroId hid) {
|
||||
heroNameTextId = GetHeroNameTextId(hid);
|
||||
HeroImages.TryGetValue(hid, out headshotPath);
|
||||
(heroNameTextId, headshotPath) = LookupHero(hid);
|
||||
}
|
||||
|
||||
string battalionName = null;
|
||||
@@ -457,8 +487,7 @@ public class ShardokGameModel {
|
||||
string heroNameTextId = null;
|
||||
string headshotPath = null;
|
||||
if (changedReserveUnit.AttachedHero?.EagleHeroId is HeroId hid) {
|
||||
heroNameTextId = GetHeroNameTextId(hid);
|
||||
HeroImages.TryGetValue(hid, out headshotPath);
|
||||
(heroNameTextId, headshotPath) = LookupHero(hid);
|
||||
}
|
||||
|
||||
string battalionName = null;
|
||||
|
||||
+54
-16
@@ -3,10 +3,10 @@ guid: 43bb1d63969274224b3b8ed8af7b5822
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 11
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
@@ -20,9 +20,12 @@ TextureImporter:
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
@@ -31,16 +34,16 @@ TextureImporter:
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: -1
|
||||
aniso: -1
|
||||
mipBias: -100
|
||||
wrapU: -1
|
||||
wrapV: -1
|
||||
wrapW: -1
|
||||
nPOTScale: 1
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
@@ -49,17 +52,22 @@ TextureImporter:
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
@@ -69,24 +77,54 @@ TextureImporter:
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: iOS
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
+294
-12
File diff suppressed because one or more lines are too long
+4
@@ -12,6 +12,10 @@ namespace Eagle0.Tutorial {
|
||||
/// Creates and registers all tutorial sequences with the registry.
|
||||
/// </summary>
|
||||
public static void RegisterAll(TutorialManager manager, TutorialTriggerRegistry registry) {
|
||||
// Old tutorial content suppressed — replaced by narrative dialogue system.
|
||||
// Content definitions preserved below for reference/reuse.
|
||||
return;
|
||||
|
||||
// Create and assign onboarding sequence
|
||||
var onboarding = CreateOnboardingSequence();
|
||||
manager.OnboardingSequence = onboarding;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f40360ec96e8643b0a29dc6f12f4aff9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using eagle;
|
||||
using eagle0;
|
||||
using Shardok;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Loads and plays narrative dialogue scripts from JSON.
|
||||
/// Manages the dialogue panel, speaker headshots, element highlighting,
|
||||
/// and step advancement.
|
||||
/// </summary>
|
||||
public class DialogueManager : MonoBehaviour {
|
||||
[Header("UI")]
|
||||
public DialoguePanelController panelController;
|
||||
|
||||
[Header("Highlight")]
|
||||
public Color highlightColor = new Color(1f, 0.85f, 0f);
|
||||
public float highlightPadding = 5f;
|
||||
public float highlightThickness = 3f;
|
||||
public float strobeSpeed = 2f;
|
||||
public float strobeMinAlpha = 0.2f;
|
||||
|
||||
private readonly Dictionary<string, List<DialogueScript>> _triggerMap =
|
||||
new Dictionary<string, List<DialogueScript>>();
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
public bool IsDialogueActive => _activeScript != null;
|
||||
|
||||
private void Awake() { LoadAllScripts(); }
|
||||
|
||||
private void Update() {
|
||||
if (_highlightEdges.Count == 0) return;
|
||||
|
||||
float alpha =
|
||||
Mathf.Lerp(strobeMinAlpha, 1f, Mathf.PingPong(Time.time * strobeSpeed, 1f));
|
||||
var c = new Color(highlightColor.r, highlightColor.g, highlightColor.b, alpha);
|
||||
foreach (var img in _highlightEdges) {
|
||||
if (img != null) img.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the map controller reference for province highlighting.
|
||||
/// Called from TutorialManager.Initialize().
|
||||
/// </summary>
|
||||
public void SetMapController(MapController mapController) {
|
||||
_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) {
|
||||
var collection = JsonUtility.FromJson<DialogueScriptCollection>(file.text);
|
||||
if (collection?.scripts == null) continue;
|
||||
|
||||
foreach (var script in collection.scripts) {
|
||||
if (string.IsNullOrEmpty(script.trigger)) continue;
|
||||
|
||||
if (!_triggerMap.TryGetValue(script.trigger, out var list)) {
|
||||
list = new List<DialogueScript>();
|
||||
_triggerMap[script.trigger] = list;
|
||||
}
|
||||
list.Add(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </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 (!_triggerMap.TryGetValue(trigger, out var scripts)) return;
|
||||
|
||||
foreach (var script in scripts) {
|
||||
if (!_completedScriptIds.Contains(script.id)) {
|
||||
StartScript(script);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartScript(DialogueScript script) {
|
||||
_activeScript = script;
|
||||
_currentStepIndex = 0;
|
||||
panelController.SetPanelPosition(script.panelPosition);
|
||||
ShowCurrentStep();
|
||||
}
|
||||
|
||||
private void ShowCurrentStep() {
|
||||
if (_activeScript == null) return;
|
||||
if (_currentStepIndex >= _activeScript.steps.Count) {
|
||||
EndDialogue();
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
panelController.continueButton.onClick.RemoveAllListeners();
|
||||
panelController.continueButton.onClick.AddListener(AdvanceStep);
|
||||
|
||||
// Wire skip button
|
||||
panelController.skipButton.onClick.RemoveAllListeners();
|
||||
panelController.skipButton.onClick.AddListener(SkipDialogue);
|
||||
|
||||
// Handle highlighting
|
||||
ClearHighlight();
|
||||
|
||||
if (!string.IsNullOrEmpty(step.highlightTarget)) {
|
||||
// Highlight a UI element
|
||||
var target = TutorialManager.Instance?.TargetRegistry?.GetTargetOrFind(
|
||||
step.highlightTarget);
|
||||
if (target != null && target.gameObject.activeInHierarchy) {
|
||||
HighlightRect(target.GetComponent<RectTransform>());
|
||||
}
|
||||
} else if (!string.IsNullOrEmpty(step.highlightProvince) && _mapController != null) {
|
||||
// Highlight a province on the map
|
||||
var proxy = _mapController.GetProvinceHighlightProxy(step.highlightProvince);
|
||||
if (proxy != null) { HighlightRect(proxy); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public void AdvanceStep() {
|
||||
_currentStepIndex++;
|
||||
if (_currentStepIndex >= _activeScript.steps.Count) {
|
||||
EndDialogue();
|
||||
} else {
|
||||
ShowCurrentStep();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips the entire remaining dialogue. Called by the skip button.
|
||||
/// </summary>
|
||||
public void SkipDialogue() { EndDialogue(); }
|
||||
|
||||
/// <summary>
|
||||
/// Ends the current dialogue and cleans up all state.
|
||||
/// Safe to call even if no dialogue is active.
|
||||
/// </summary>
|
||||
public void EndDialogue() {
|
||||
if (_activeScript != null && !string.IsNullOrEmpty(_activeScript.id)) {
|
||||
_completedScriptIds.Add(_activeScript.id);
|
||||
}
|
||||
_activeScript = null;
|
||||
_currentStepIndex = 0;
|
||||
|
||||
panelController.SetPanelPosition(null);
|
||||
panelController.Hide();
|
||||
panelController.continueButton.onClick.RemoveAllListeners();
|
||||
panelController.skipButton.onClick.RemoveAllListeners();
|
||||
|
||||
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 ==========
|
||||
|
||||
/// <summary>
|
||||
/// Creates a border highlight as a direct child of the target RectTransform.
|
||||
/// This avoids cross-canvas coordinate conversion issues — the border inherits
|
||||
/// the target's position automatically.
|
||||
/// </summary>
|
||||
private void HighlightRect(RectTransform target) {
|
||||
if (target == null) return;
|
||||
|
||||
ClearHighlight();
|
||||
|
||||
var go = new GameObject("DialogueHighlight");
|
||||
go.transform.SetParent(target, false);
|
||||
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = new Vector2(-highlightPadding, -highlightPadding);
|
||||
rt.offsetMax = new Vector2(highlightPadding, highlightPadding);
|
||||
|
||||
CreateBorderEdge(
|
||||
rt,
|
||||
"Top",
|
||||
new Vector2(0, 1),
|
||||
Vector2.one,
|
||||
new Vector2(0, -highlightThickness),
|
||||
Vector2.zero);
|
||||
CreateBorderEdge(
|
||||
rt,
|
||||
"Bottom",
|
||||
Vector2.zero,
|
||||
new Vector2(1, 0),
|
||||
Vector2.zero,
|
||||
new Vector2(0, highlightThickness));
|
||||
CreateBorderEdge(
|
||||
rt,
|
||||
"Left",
|
||||
Vector2.zero,
|
||||
new Vector2(0, 1),
|
||||
Vector2.zero,
|
||||
new Vector2(highlightThickness, 0));
|
||||
CreateBorderEdge(
|
||||
rt,
|
||||
"Right",
|
||||
new Vector2(1, 0),
|
||||
Vector2.one,
|
||||
new Vector2(-highlightThickness, 0),
|
||||
Vector2.zero);
|
||||
|
||||
_activeHighlight = go;
|
||||
}
|
||||
|
||||
private void CreateBorderEdge(
|
||||
RectTransform parent,
|
||||
string name,
|
||||
Vector2 anchorMin,
|
||||
Vector2 anchorMax,
|
||||
Vector2 offsetMin,
|
||||
Vector2 offsetMax) {
|
||||
var edge = new GameObject(name);
|
||||
edge.transform.SetParent(parent, false);
|
||||
var rt = edge.AddComponent<RectTransform>();
|
||||
rt.anchorMin = anchorMin;
|
||||
rt.anchorMax = anchorMax;
|
||||
rt.offsetMin = offsetMin;
|
||||
rt.offsetMax = offsetMax;
|
||||
var img = edge.AddComponent<Image>();
|
||||
img.color = highlightColor;
|
||||
img.raycastTarget = false;
|
||||
_highlightEdges.Add(img);
|
||||
}
|
||||
|
||||
private void ClearHighlight() {
|
||||
if (_activeHighlight != null) {
|
||||
Destroy(_activeHighlight);
|
||||
_activeHighlight = null;
|
||||
}
|
||||
_highlightEdges.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87c43c82915f445b3993ea8dc6c39186
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
using common;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// UI controller for the narrative dialogue panel.
|
||||
/// Wired to a Unity panel via Inspector fields.
|
||||
/// DialogueManager calls Show/Hide to drive the display.
|
||||
/// </summary>
|
||||
public class DialoguePanelController : MonoBehaviour {
|
||||
[Header("Speaker")]
|
||||
public RawImage speakerHeadshot;
|
||||
public TMP_Text speakerNameText;
|
||||
|
||||
[Header("Content")]
|
||||
public TMP_Text dialogueText;
|
||||
public TMP_Text instructionText;
|
||||
public GameObject instructionContainer;
|
||||
public TMP_SpriteAsset instructionSpriteAsset;
|
||||
|
||||
[Header("Buttons")]
|
||||
public Button continueButton;
|
||||
public Button skipButton;
|
||||
|
||||
private Vector2 _defaultAnchorMin;
|
||||
private Vector2 _defaultAnchorMax;
|
||||
private Vector2 _defaultPivot;
|
||||
private bool _defaultsSaved;
|
||||
|
||||
/// <summary>
|
||||
/// Repositions the panel vertically. Call before Show().
|
||||
/// "top" anchors to the top of the screen; null/empty restores default.
|
||||
/// </summary>
|
||||
public void SetPanelPosition(string position) {
|
||||
var rt = GetComponent<RectTransform>();
|
||||
|
||||
if (!_defaultsSaved) {
|
||||
_defaultAnchorMin = rt.anchorMin;
|
||||
_defaultAnchorMax = rt.anchorMax;
|
||||
_defaultPivot = rt.pivot;
|
||||
_defaultsSaved = true;
|
||||
}
|
||||
|
||||
if (position == "top") {
|
||||
rt.anchorMin = new Vector2(rt.anchorMin.x, 1f);
|
||||
rt.anchorMax = new Vector2(rt.anchorMax.x, 1f);
|
||||
rt.pivot = new Vector2(rt.pivot.x, 1f);
|
||||
rt.anchoredPosition = new Vector2(rt.anchoredPosition.x, -10f);
|
||||
} else {
|
||||
rt.anchorMin = _defaultAnchorMin;
|
||||
rt.anchorMax = _defaultAnchorMax;
|
||||
rt.pivot = _defaultPivot;
|
||||
rt.anchoredPosition = new Vector2(rt.anchoredPosition.x, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the panel with the given dialogue step content.
|
||||
/// </summary>
|
||||
public void Show(DialogueStep step) {
|
||||
speakerNameText.text = step.speakerName;
|
||||
dialogueText.text = step.dialogueText;
|
||||
|
||||
// Load speaker headshot via ResourceFetcher
|
||||
if (!string.IsNullOrEmpty(step.speakerImagePath)) {
|
||||
ResourceFetcher.headshotFetcher.LoadIntoRawImage(
|
||||
speakerHeadshot,
|
||||
step.speakerImagePath);
|
||||
} else {
|
||||
speakerHeadshot.texture = null;
|
||||
}
|
||||
|
||||
// Show/hide instruction section
|
||||
bool hasInstruction = !string.IsNullOrEmpty(step.instructionText);
|
||||
instructionContainer.SetActive(hasInstruction);
|
||||
if (hasInstruction) {
|
||||
instructionText.spriteAsset = instructionSpriteAsset;
|
||||
instructionText.text = step.instructionText;
|
||||
}
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
// Force layout rebuild so ContentSizeFitter updates panel height
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent<RectTransform>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the dialogue panel.
|
||||
/// </summary>
|
||||
public void Hide() { gameObject.SetActive(false); }
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 918aadf8ef43c4e98baed073d6e6d26a
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
[Serializable]
|
||||
public class DialogueScript {
|
||||
public string id;
|
||||
public string trigger;
|
||||
public string panelPosition; // "top" or null for default
|
||||
public List<DialogueStep> steps;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class DialogueScriptCollection {
|
||||
public List<DialogueScript> scripts;
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4847dd4e788d54c94aa740ffeddbde24
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
[Serializable]
|
||||
public class DialogueStep {
|
||||
public string speakerName;
|
||||
public string speakerImagePath;
|
||||
public string dialogueText;
|
||||
public string instructionText;
|
||||
public string highlightTarget;
|
||||
public string highlightProvince;
|
||||
public string completionEvent;
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55f1c6b28a2ba45389c5c8196e3f54c2
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d830fd74ce914e828514e73ecb6a44d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-6234567890123456789
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: champion Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 6ef78325c5250bc40bb7fff9e01a7dcc, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: champion
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -6234567890123456789}
|
||||
spriteSheet: {fileID: 2800000, guid: 6ef78325c5250bc40bb7fff9e01a7dcc, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: Champion
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: 6ef78325c5250bc40bb7fff9e01a7dcc, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73067d8ba7ef428e833a6d031d83fe66
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-7331928456102843519
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: charge Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 43bb1d63969274224b3b8ed8af7b5822, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: charge
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -7331928456102843519}
|
||||
spriteSheet: {fileID: 2800000, guid: 43bb1d63969274224b3b8ed8af7b5822, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: Charge
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 128
|
||||
m_Height: 74
|
||||
m_HorizontalBearingX: -64
|
||||
m_HorizontalBearingY: 66
|
||||
m_HorizontalAdvance: 128
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 128
|
||||
m_Height: 74
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: 43bb1d63969274224b3b8ed8af7b5822, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f02ce891f1b54a469e6ea565badaa1f5
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-6970199219496148764
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Blacksmith_30_silver_horseshoe Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: eea23a3fb9d954f469eaf4f888930bd4, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: dragoons
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -6970199219496148764}
|
||||
spriteSheet: {fileID: 2800000, guid: eea23a3fb9d954f469eaf4f888930bd4, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: LightCavalry
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: eea23a3fb9d954f469eaf4f888930bd4, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets:
|
||||
- {fileID: 11400000, guid: 4310e5d1bcccc4f5186ea1d41a6f373f, type: 2}
|
||||
- {fileID: 11400000, guid: 5ef94b343124b42cfadc62d8ed46e278, type: 2}
|
||||
- {fileID: 11400000, guid: 9fad0827179504ff1aca13e029f62588, type: 2}
|
||||
- {fileID: 11400000, guid: 309f2f27c278046cbac995e61a2e7956, type: 2}
|
||||
- {fileID: 11400000, guid: c2834fa26a6824606a565da687bf8ff3, type: 2}
|
||||
- {fileID: 11400000, guid: f02ce891f1b54a469e6ea565badaa1f5, type: 2}
|
||||
- {fileID: 11400000, guid: c0f460df446143f9997cada4c7b61a02, type: 2}
|
||||
- {fileID: 11400000, guid: c185f2d3dd7546068a38f29fa716ee95, type: 2}
|
||||
- {fileID: 11400000, guid: af12d2dee1704edbbf19720ab37b8430, type: 2}
|
||||
- {fileID: 11400000, guid: 8b9e2010102a4d3297964885e3b286bb, type: 2}
|
||||
- {fileID: 11400000, guid: 6a6ffb7e8e104563b0e2627285158f74, type: 2}
|
||||
- {fileID: 11400000, guid: 73067d8ba7ef428e833a6d031d83fe66, type: 2}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5170b1976e3ba433986c5c661454f2fb
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-3234567890123456789
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: engineer Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 3d3cbcae4901f3d4d90d57314e7370f7, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: engineer
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -3234567890123456789}
|
||||
spriteSheet: {fileID: 2800000, guid: 3d3cbcae4901f3d4d90d57314e7370f7, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: Engineer
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: 3d3cbcae4901f3d4d90d57314e7370f7, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af12d2dee1704edbbf19720ab37b8430
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-5303636401254955137
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: mace04 Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 9e15847c53afb14448257c32e4cabe00, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: heavy infantry
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -5303636401254955137}
|
||||
spriteSheet: {fileID: 2800000, guid: 9e15847c53afb14448257c32e4cabe00, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: HeavyInfantry
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: 9e15847c53afb14448257c32e4cabe00, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4310e5d1bcccc4f5186ea1d41a6f373f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-6727445534539906575
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: 2_Mail_head Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: d9177ffe2c0b2b64382bd4e5f3b83fc1, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: knights
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -6727445534539906575}
|
||||
spriteSheet: {fileID: 2800000, guid: d9177ffe2c0b2b64382bd4e5f3b83fc1, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: HeavyCavalry
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: d9177ffe2c0b2b64382bd4e5f3b83fc1, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ef94b343124b42cfadc62d8ed46e278
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-166228656761865305
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: sword02 Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 2c1fa953390c43f4e9385328f77ee967, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: light infantry
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -166228656761865305}
|
||||
spriteSheet: {fileID: 2800000, guid: 2c1fa953390c43f4e9385328f77ee967, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: LightInfantry
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: 2c1fa953390c43f4e9385328f77ee967, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fad0827179504ff1aca13e029f62588
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-6519500375026363721
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Bow_39 Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: c6aa4e8cbfbcff240a893a7afe42c83d, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: longbowmen
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -6519500375026363721}
|
||||
spriteSheet: {fileID: 2800000, guid: c6aa4e8cbfbcff240a893a7afe42c83d, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: Longbowmen
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: c6aa4e8cbfbcff240a893a7afe42c83d, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 309f2f27c278046cbac995e61a2e7956
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-1234567890123456789
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: mage Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 3dc4132ad69f42b458976f624ccd771d, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: mage
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -1234567890123456789}
|
||||
spriteSheet: {fileID: 2800000, guid: 3dc4132ad69f42b458976f624ccd771d, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: Mage
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: 3dc4132ad69f42b458976f624ccd771d, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0f460df446143f9997cada4c7b61a02
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-2234567890123456789
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: necromancer Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 1fda22c5bfb926f4cabc0303e9dbc08b, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: necromancer
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -2234567890123456789}
|
||||
spriteSheet: {fileID: 2800000, guid: 1fda22c5bfb926f4cabc0303e9dbc08b, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: Necromancer
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: 1fda22c5bfb926f4cabc0303e9dbc08b, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c185f2d3dd7546068a38f29fa716ee95
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &-4234567890123456789
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: paladin Material
|
||||
m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: ce3bc31ab20668048b10a34c9d8f7c3b, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _ColorMask: 15
|
||||
- _CullMode: 0
|
||||
- _Stencil: 0
|
||||
- _StencilComp: 8
|
||||
- _StencilOp: 0
|
||||
- _StencilReadMask: 255
|
||||
- _StencilWriteMask: 255
|
||||
- _UseUIAlphaClip: 0
|
||||
m_Colors:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 84a92b25f83d49b9bc132d206b370281, type: 3}
|
||||
m_Name: paladin
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_SpriteAsset
|
||||
m_Version: 1.1.0
|
||||
m_FaceInfo:
|
||||
m_FaceIndex: 0
|
||||
m_FamilyName:
|
||||
m_StyleName:
|
||||
m_PointSize: 0
|
||||
m_Scale: 0
|
||||
m_UnitsPerEM: 0
|
||||
m_LineHeight: 0
|
||||
m_AscentLine: 0
|
||||
m_CapLine: 0
|
||||
m_MeanLine: 0
|
||||
m_Baseline: 0
|
||||
m_DescentLine: 0
|
||||
m_SuperscriptOffset: 0
|
||||
m_SuperscriptSize: 0
|
||||
m_SubscriptOffset: 0
|
||||
m_SubscriptSize: 0
|
||||
m_UnderlineOffset: 0
|
||||
m_UnderlineThickness: 0
|
||||
m_StrikethroughOffset: 0
|
||||
m_StrikethroughThickness: 0
|
||||
m_TabWidth: 0
|
||||
m_Material: {fileID: -4234567890123456789}
|
||||
spriteSheet: {fileID: 2800000, guid: ce3bc31ab20668048b10a34c9d8f7c3b, type: 3}
|
||||
m_SpriteCharacterTable:
|
||||
- m_ElementType: 2
|
||||
m_Unicode: 65534
|
||||
m_GlyphIndex: 0
|
||||
m_Scale: 1.25
|
||||
m_Name: Paladin
|
||||
m_GlyphTable:
|
||||
- m_Index: 0
|
||||
m_Metrics:
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_HorizontalBearingX: -128
|
||||
m_HorizontalBearingY: 230
|
||||
m_HorizontalAdvance: 256
|
||||
m_GlyphRect:
|
||||
m_X: 0
|
||||
m_Y: 0
|
||||
m_Width: 256
|
||||
m_Height: 256
|
||||
m_Scale: 1
|
||||
m_AtlasIndex: 0
|
||||
m_ClassDefinitionType: 0
|
||||
sprite: {fileID: 21300000, guid: ce3bc31ab20668048b10a34c9d8f7c3b, type: 3}
|
||||
spriteInfoList: []
|
||||
fallbackSpriteAssets: []
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b9e2010102a4d3297964885e3b286bb
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user