mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 04:56:26 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deb0e33c5a | ||
|
|
0191455d5d | ||
|
|
98ed70c2e0 |
@@ -5,10 +5,6 @@ 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
|
||||
|
||||
@@ -46,7 +42,5 @@ common --tool_java_language_version=25
|
||||
common --tool_java_runtime_version=remotejdk_25
|
||||
|
||||
# Workspace status for build stamping (git commit, timestamp)
|
||||
# Only targets with stamp=1 will use these values; we intentionally
|
||||
# do NOT set "common --stamp" so non-stamped targets can share the
|
||||
# remote cache across CI workflows.
|
||||
common --workspace_status_command=tools/workspace_status.sh
|
||||
common --stamp
|
||||
|
||||
@@ -11,19 +11,3 @@ src/main/csharp/**/GUI[[:space:]]Pro[[:space:]]Kit*/**/*.ttf !filter !diff !merg
|
||||
src/main/csharp/**/Modern[[:space:]]UI[[:space:]]Pack/**/*.ttf !filter !diff !merge
|
||||
*.herodata filter=lfs diff=lfs merge=lfs -text
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Unity Smart Merge - use UnityYAMLMerge for Unity files
|
||||
# Requires configuring unityyamlmerge in ~/.gitconfig:
|
||||
# [merge]
|
||||
# tool = unityyamlmerge
|
||||
# [mergetool "unityyamlmerge"]
|
||||
# trustExitCode = false
|
||||
# cmd = '/Applications/Unity/Hub/Editor/*/Unity.app/Contents/Tools/UnityYAMLMerge' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"
|
||||
*.unity merge=unityyamlmerge
|
||||
*.prefab merge=unityyamlmerge
|
||||
*.asset merge=unityyamlmerge
|
||||
*.mat merge=unityyamlmerge
|
||||
*.anim merge=unityyamlmerge
|
||||
*.controller merge=unityyamlmerge
|
||||
*.physicsMaterial2D merge=unityyamlmerge
|
||||
*.physicMaterial merge=unityyamlmerge
|
||||
|
||||
@@ -41,8 +41,7 @@ jobs:
|
||||
set -ex
|
||||
|
||||
# Build auth server image (Go binary has explicit goos/goarch in BUILD.bazel)
|
||||
# --stamp is needed for authservice x_defs (git commit, build time)
|
||||
bazel build --stamp //ci:auth_server_image
|
||||
bazel build //ci:auth_server_image
|
||||
|
||||
# Save the resolved path before any other bazel command changes bazel-bin symlink
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/auth_server_image)
|
||||
|
||||
@@ -13,8 +13,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Show disk usage before cleanup
|
||||
run: |
|
||||
|
||||
@@ -23,49 +23,20 @@ on:
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
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:
|
||||
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:
|
||||
lfs: false
|
||||
- name: Run tests
|
||||
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
|
||||
- name: Collect failed test logs
|
||||
|
||||
@@ -31,16 +31,13 @@ on:
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
# 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.
|
||||
# Only allow one deployment at a time to prevent race conditions
|
||||
concurrency:
|
||||
group: docker-build-deploy
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: false # Don't cancel running deployments, queue new ones
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
# Single consolidated build job - builds all images with one bazel invocation
|
||||
@@ -53,43 +50,20 @@ 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
|
||||
|
||||
# Build ALL images in a single bazel command - Bazel parallelizes internally
|
||||
# Note: Shardok is built separately for ARM64 and deployed to Hetzner
|
||||
# --stamp is needed for admin_server x_defs (git commit, build time)
|
||||
echo "=== Building Docker images ==="
|
||||
bazel build \
|
||||
--stamp \
|
||||
--platforms=//:linux_x86_64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:all \
|
||||
//ci:eagle_server_image \
|
||||
@@ -115,16 +89,8 @@ jobs:
|
||||
echo "Admin: $ADMIN_PATH"
|
||||
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
|
||||
path: scripts/bin/warmup
|
||||
retention-days: 1
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: steps.check-latest.outputs.skip != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
@@ -144,59 +110,40 @@ jobs:
|
||||
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
|
||||
# Get crane (cached across runs to avoid rebuilding bazel target)
|
||||
CRANE_VERSION="v0.20.2"
|
||||
CRANE_DIR="${HOME}/.local/bin"
|
||||
CRANE="${CRANE_DIR}/crane"
|
||||
if [ ! -x "$CRANE" ] || ! "$CRANE" version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane ${CRANE_VERSION}..."
|
||||
mkdir -p "$CRANE_DIR"
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Darwin_arm64.tar.gz" | tar xzf - -C "$CRANE_DIR" crane
|
||||
chmod +x "$CRANE"
|
||||
else
|
||||
echo "Using cached crane at $CRANE"
|
||||
fi
|
||||
# Get crane from push target runfiles
|
||||
bazel build //ci:eagle_server_push
|
||||
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
|
||||
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
|
||||
|
||||
# Set up image paths and tags
|
||||
EAGLE_IMAGE="${{ steps.build-all.outputs.eagle_path }}"
|
||||
EAGLE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
|
||||
ADMIN_IMAGE="${{ steps.build-all.outputs.admin_path }}"
|
||||
ADMIN_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
|
||||
JFR_IMAGE="${{ steps.build-all.outputs.jfr_path }}"
|
||||
JFR_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
|
||||
|
||||
# Push all three images in parallel
|
||||
echo "Pushing Eagle: $EAGLE_TAG"
|
||||
$CRANE push "$EAGLE_IMAGE" "$EAGLE_TAG" &
|
||||
PID_EAGLE=$!
|
||||
|
||||
echo "Pushing Admin: $ADMIN_TAG"
|
||||
$CRANE push "$ADMIN_IMAGE" "$ADMIN_TAG" &
|
||||
PID_ADMIN=$!
|
||||
|
||||
echo "Pushing JFR Sidecar: $JFR_TAG"
|
||||
$CRANE push "$JFR_IMAGE" "$JFR_TAG" &
|
||||
PID_JFR=$!
|
||||
|
||||
# Wait for all pushes to complete
|
||||
PUSH_FAILED=0
|
||||
wait $PID_EAGLE || PUSH_FAILED=1
|
||||
wait $PID_ADMIN || PUSH_FAILED=1
|
||||
wait $PID_JFR || PUSH_FAILED=1
|
||||
if [ $PUSH_FAILED -ne 0 ]; then
|
||||
echo "ERROR: One or more image pushes failed"
|
||||
if [ -z "$CRANE" ] || [ ! -x "$CRANE" ]; then
|
||||
echo "ERROR: crane not found in runfiles"
|
||||
find "$RUNFILES" -name crane 2>/dev/null || echo "No crane found at all"
|
||||
exit 1
|
||||
fi
|
||||
echo "All images pushed, tagging :latest..."
|
||||
|
||||
# Tag :latest in parallel
|
||||
$CRANE copy "$EAGLE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest" &
|
||||
$CRANE copy "$ADMIN_TAG" "registry.digitalocean.com/eagle0/admin-server:latest" &
|
||||
$CRANE copy "$JFR_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest" &
|
||||
wait
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push Eagle image
|
||||
EAGLE_IMAGE="${{ steps.build-all.outputs.eagle_path }}"
|
||||
EAGLE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
|
||||
echo "Pushing Eagle: $EAGLE_TAG"
|
||||
$CRANE push "$EAGLE_IMAGE" "$EAGLE_TAG"
|
||||
$CRANE copy "$EAGLE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
|
||||
echo "eagle_image_tag=$EAGLE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Push Admin image
|
||||
ADMIN_IMAGE="${{ steps.build-all.outputs.admin_path }}"
|
||||
ADMIN_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
|
||||
echo "Pushing Admin: $ADMIN_TAG"
|
||||
$CRANE push "$ADMIN_IMAGE" "$ADMIN_TAG"
|
||||
$CRANE copy "$ADMIN_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
|
||||
echo "admin_image_tag=$ADMIN_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Push JFR Sidecar image
|
||||
JFR_IMAGE="${{ steps.build-all.outputs.jfr_path }}"
|
||||
JFR_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
|
||||
echo "Pushing JFR Sidecar: $JFR_TAG"
|
||||
$CRANE push "$JFR_IMAGE" "$JFR_TAG"
|
||||
$CRANE copy "$JFR_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
|
||||
echo "jfr_sidecar_image_tag=$JFR_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "=== All images pushed successfully ==="
|
||||
@@ -242,12 +189,9 @@ jobs:
|
||||
DO_DROPLET_IP: ${{ secrets.DO_DROPLET_IP }}
|
||||
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
GITHUB_TOKEN_FOR_ADMIN: ${{ secrets.ADMIN_GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
@@ -256,11 +200,11 @@ jobs:
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Download warmup binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: warmup-binary
|
||||
path: scripts/bin/
|
||||
- name: Build warmup tool
|
||||
run: |
|
||||
bazel build //ci:warmup_tar
|
||||
mkdir -p scripts/bin
|
||||
tar -xf bazel-bin/ci/warmup_tar.tar -C scripts/bin --strip-components=1
|
||||
|
||||
- name: Copy config files to droplet
|
||||
run: |
|
||||
@@ -367,7 +311,6 @@ jobs:
|
||||
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
export NOTIFY_SECRET="${NOTIFY_SECRET}"
|
||||
export GITHUB_TOKEN="${GITHUB_TOKEN_FOR_ADMIN}"
|
||||
|
||||
# Check Docker has IPv6 support
|
||||
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
|
||||
@@ -379,34 +322,29 @@ jobs:
|
||||
|
||||
echo "Using images: \$EAGLE_IMAGE, \$ADMIN_IMAGE, \$JFR_SIDECAR_IMAGE"
|
||||
|
||||
# Install crane for pulling OCI images (cached across deploys)
|
||||
CRANE_VERSION="v0.20.2"
|
||||
if [ ! -x crane ] || ! ./crane version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane \${CRANE_VERSION}..."
|
||||
rm -f crane
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/\${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" | tar xzf - crane
|
||||
chmod +x crane
|
||||
else
|
||||
echo "Using cached crane"
|
||||
fi
|
||||
# Install crane for pulling OCI images
|
||||
echo "Installing crane..."
|
||||
rm -f crane
|
||||
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
|
||||
chmod +x crane
|
||||
|
||||
# Pull all images in parallel, then load sequentially
|
||||
echo "Pulling all images in parallel..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar &
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar &
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar &
|
||||
docker pull nginx:alpine &
|
||||
docker pull certbot/certbot &
|
||||
wait
|
||||
# Pull and load all images
|
||||
echo "Pulling Eagle image..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar && docker load -i eagle.tar && rm eagle.tar
|
||||
|
||||
echo "Loading images..."
|
||||
docker load -i eagle.tar && rm eagle.tar
|
||||
docker load -i admin.tar && rm admin.tar
|
||||
echo "Pulling Admin image..."
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar && docker load -i admin.tar && rm admin.tar
|
||||
# Tag as :latest locally so docker-compose fallback uses correct image
|
||||
docker tag "\${ADMIN_IMAGE}" registry.digitalocean.com/eagle0/admin-server:latest
|
||||
docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
|
||||
|
||||
echo "All images pulled and loaded successfully"
|
||||
echo "Pulling JFR Sidecar image..."
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar && docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
|
||||
|
||||
# Pull other compose images
|
||||
docker pull nginx:alpine || true
|
||||
docker pull certbot/certbot || true
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# =================================================================
|
||||
# Verify Shardok connectivity before proceeding with deployment
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
name: Eagle Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by docker_build.yml which builds the same target
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/eagle_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
@@ -13,10 +23,6 @@ on:
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/eagle_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ on:
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -50,12 +46,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
|
||||
clean: true
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
@@ -65,9 +56,20 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build iOS Addressables
|
||||
run: ./ci/github_actions/build_ios_addressables.sh "$EAGLE0_BUILD_DIR/editor_ios_addressables.log"
|
||||
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
@@ -75,17 +77,6 @@ jobs:
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh iOS
|
||||
|
||||
- name: Purge CDN cache for iOS addressables
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["addressables/iOS/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
env:
|
||||
# Runner-specific build directory to allow parallel builds on multiple runners
|
||||
@@ -20,7 +21,9 @@ env:
|
||||
|
||||
jobs:
|
||||
build-unity:
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
outputs:
|
||||
xcode_project_path: ${{ steps.build.outputs.xcode_project_path }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -37,14 +40,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
clean: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
@@ -53,10 +51,22 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build iOS Unity Project
|
||||
id: build
|
||||
run: |
|
||||
./ci/github_actions/build_unity_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS"
|
||||
echo "xcode_project_path=$EAGLE0_BUILD_DIR/eagle0iOS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success()
|
||||
@@ -65,16 +75,18 @@ jobs:
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh iOS
|
||||
|
||||
- name: Purge CDN cache for iOS addressables
|
||||
if: success()
|
||||
env:
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
- name: Zip Xcode project for artifact
|
||||
run: |
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["addressables/iOS/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
cd $EAGLE0_BUILD_DIR
|
||||
# Use tar for speed - Xcode projects have many small files
|
||||
tar -czf eagle0iOS.tar.gz eagle0iOS
|
||||
|
||||
- name: Upload Xcode project
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xcode-project-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0iOS.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
@@ -83,12 +95,37 @@ jobs:
|
||||
name: editor_ios.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios.log
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
archive-and-upload:
|
||||
needs: build-unity
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
ci
|
||||
scripts
|
||||
|
||||
- name: Clean download directory
|
||||
run: rm -rf $EAGLE0_BUILD_DIR/eagle0iOS
|
||||
|
||||
- name: Download Xcode project
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: xcode-project-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}
|
||||
|
||||
- name: Extract Xcode project
|
||||
run: |
|
||||
cd $EAGLE0_BUILD_DIR
|
||||
tar -xzf eagle0iOS.tar.gz
|
||||
rm eagle0iOS.tar.gz
|
||||
ls -la eagle0iOS/
|
||||
|
||||
- name: Install Signing Certificate
|
||||
env:
|
||||
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
|
||||
@@ -147,23 +184,11 @@ jobs:
|
||||
- name: Upload to TestFlight
|
||||
if: ${{ github.event.inputs.skip_upload != 'true' }}
|
||||
env:
|
||||
# App Store Connect API Key (replaces deprecated altool with Apple ID)
|
||||
# Create at: https://appstoreconnect.apple.com/access/api
|
||||
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
|
||||
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
|
||||
run: |
|
||||
# Write API key to file (xcodebuild needs a file path)
|
||||
API_KEY_PATH=$(mktemp)
|
||||
echo "$APP_STORE_CONNECT_API_KEY" > "$API_KEY_PATH"
|
||||
export APP_STORE_CONNECT_API_KEY_PATH="$API_KEY_PATH"
|
||||
|
||||
chmod +x ./ci/github_actions/upload_testflight.sh
|
||||
./ci/github_actions/upload_testflight.sh "$EAGLE0_BUILD_DIR/archive/eagle0.xcarchive" "$APPLE_TEAM_ID" "$PROFILE_UUID"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$API_KEY_PATH"
|
||||
./ci/github_actions/upload_testflight.sh "$EAGLE0_BUILD_DIR/archive/eagle0.ipa"
|
||||
|
||||
- name: Upload IPA artifact
|
||||
# Only keep artifact if we skipped TestFlight upload (for debugging)
|
||||
@@ -181,3 +206,21 @@ jobs:
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
cleanup:
|
||||
needs: [build-unity, archive-and-upload]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delete intermediate artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
artifact_name="xcode-project-${{ github.run_id }}"
|
||||
echo "Deleting artifact: $artifact_name"
|
||||
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
|
||||
if [ -n "$artifact_id" ]; then
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
|
||||
fi
|
||||
|
||||
@@ -25,19 +25,25 @@ on:
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "ci/mac/**"
|
||||
pull_request:
|
||||
# On PRs, only build Mac when Mac-specific files change. Shared C#/proto
|
||||
# changes are covered by the Windows build — if it passes, Mac will too.
|
||||
paths:
|
||||
- ".github/workflows/mac_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "src/main/protobuf/net/eagle0/common/**"
|
||||
- "src/main/protobuf/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/api/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/common/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/views/**"
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/build_sparkle_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_submit.sh"
|
||||
- "scripts/notarize_wait.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "ci/mac/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -47,10 +53,6 @@ on:
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # Required to delete artifacts after deploy
|
||||
@@ -83,30 +85,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
clean: true # Remove untracked files like old SparklePlugin.bundle
|
||||
fetch-depth: 0 # For version numbering from git history
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA — clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA — keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ — clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
@@ -115,12 +96,19 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh mac
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build Mac Unity
|
||||
run: ./ci/github_actions/build_unity_mac.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
@@ -297,8 +285,6 @@ jobs:
|
||||
needs: [build-and-sign, wait-notarization]
|
||||
if: needs.build-and-sign.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -321,7 +307,6 @@ jobs:
|
||||
rm eagle0.app.zip
|
||||
|
||||
- name: Deploy Mac Build
|
||||
id: deploy-mac
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -352,37 +337,44 @@ jobs:
|
||||
|
||||
rm "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
# Export version for notify step
|
||||
echo "DEPLOYED_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
# Artifact cleanup is handled by the cleanup job on ubuntu-latest
|
||||
- name: Notify clients of update
|
||||
if: success()
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
run: |
|
||||
# Wait for CDN cache to clear
|
||||
sleep 60
|
||||
|
||||
# Notify via admin server (required=false for normal deploys)
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=$DEPLOYED_VERSION&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
- name: Delete this run's Mac app artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Delete this run's artifacts (names include run ID to avoid conflicts)
|
||||
for artifact_name in signed-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }}; do
|
||||
echo "Deleting artifact: $artifact_name"
|
||||
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
|
||||
if [ -n "$artifact_id" ]; then
|
||||
echo "Deleting artifact ID: $artifact_id"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
|
||||
fi
|
||||
done
|
||||
echo "Cleanup complete"
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
notify-mac:
|
||||
needs: deploy
|
||||
if: needs.deploy.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Purge CDN cache and notify clients
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
# Purge CDN cache so clients get the latest files immediately
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["mac/*", "addressables/StandaloneOSX/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=${{ needs.deploy.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
# Cleanup job runs regardless of success/failure to prevent artifact accumulation
|
||||
cleanup:
|
||||
needs: [build-and-sign, wait-notarization, deploy, notify-mac]
|
||||
needs: [build-and-sign, wait-notarization, deploy]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ on:
|
||||
default: 'true'
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: shardok-arm64-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -64,14 +60,12 @@ jobs:
|
||||
if [ "$MAGIC" = "7f454c46" ]; then
|
||||
echo "SUCCESS: Binary is ELF format (Linux)"
|
||||
# Check if it's ARM64 (e_machine = 0xB7 = 183 for aarch64)
|
||||
# od -tx2 reads as a 16-bit value in host byte order (little-endian on macOS),
|
||||
# so the little-endian ELF bytes b7 00 are displayed as 00b7.
|
||||
E_MACHINE=$(od -An -j18 -N2 -tx2 "$LINUX_BIN" | tr -d ' ')
|
||||
echo "ELF e_machine: $E_MACHINE"
|
||||
if [ "$E_MACHINE" = "00b7" ]; then
|
||||
if [ "$E_MACHINE" = "b700" ]; then
|
||||
echo "SUCCESS: Binary is ARM64 (aarch64)"
|
||||
else
|
||||
echo "WARNING: Binary e_machine is $E_MACHINE (expected 00b7 for aarch64)"
|
||||
echo "WARNING: Binary e_machine is $E_MACHINE (expected b700 for aarch64)"
|
||||
fi
|
||||
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
|
||||
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
|
||||
@@ -134,19 +128,20 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get crane (cached across runs to avoid rebuilding bazel target,
|
||||
# which would discard the ARM64 analysis cache due to platform change)
|
||||
CRANE_VERSION="v0.20.2"
|
||||
CRANE_DIR="${HOME}/.local/bin"
|
||||
CRANE="${CRANE_DIR}/crane"
|
||||
if [ ! -x "$CRANE" ] || ! "$CRANE" version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane ${CRANE_VERSION}..."
|
||||
mkdir -p "$CRANE_DIR"
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Darwin_arm64.tar.gz" | tar xzf - -C "$CRANE_DIR" crane
|
||||
chmod +x "$CRANE"
|
||||
else
|
||||
echo "Using cached crane at $CRANE"
|
||||
# Build a push target to get crane in runfiles
|
||||
bazel build //ci:eagle_server_push
|
||||
|
||||
# Find the Darwin crane binary
|
||||
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
|
||||
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
|
||||
if [ -z "$CRANE" ]; then
|
||||
CRANE=$(find "$RUNFILES" -name crane 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
|
||||
echo "ERROR: crane not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push with arm64-prefixed SHA tag (same repo as x86, different tag)
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
@@ -191,22 +186,8 @@ jobs:
|
||||
|
||||
echo "Deploying Shardok ARM64: ${{ 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
|
||||
# Pull the new image
|
||||
docker pull "${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Stop and remove any container using port 40042 or named shardok*
|
||||
docker ps -q --filter "publish=40042" | xargs -r docker stop
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
name: Shardok Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by shardok_arm64_build.yml which builds the same target
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/proto/net/eagle0/shardok/**'
|
||||
- 'src/main/proto/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
@@ -14,10 +25,6 @@ on:
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ on:
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/restore_library.sh"
|
||||
- "ci/github_actions/persist_library.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
@@ -34,16 +36,14 @@ on:
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/restore_library.sh"
|
||||
- "ci/github_actions/persist_library.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "MODULE.bazel"
|
||||
- "WORKSPACE"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -54,8 +54,6 @@ env:
|
||||
jobs:
|
||||
windows-unity:
|
||||
runs-on: [self-hosted, macOS, unity-windows]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.get-version.outputs.deployed_version }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -72,28 +70,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA — clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA — keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ — clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
clean: true # Remove untracked files from previous builds
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
@@ -101,13 +78,17 @@ jobs:
|
||||
git lfs pull
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
- name: Build Windows unity
|
||||
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
@@ -145,12 +126,21 @@ jobs:
|
||||
# Cleanup
|
||||
rm -f /tmp/manifest_signing_key
|
||||
|
||||
- name: Export deployed version
|
||||
id: get-version
|
||||
- name: Notify clients of update
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
run: |
|
||||
# Wait for CDN cache to clear
|
||||
sleep 60
|
||||
|
||||
# Get version from manifest
|
||||
VERSION=$(grep "^version=" /tmp/unity_manifest.txt | cut -d= -f2 || date +%Y.%m.%d)
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Notify via admin server (required=false for normal deploys)
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=$VERSION&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
- name: Archive build log
|
||||
if: success() || failure()
|
||||
@@ -161,25 +151,4 @@ jobs:
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
notify-windows:
|
||||
needs: windows-unity
|
||||
if: needs.windows-unity.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Purge CDN cache and notify clients
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
# Purge CDN cache so clients get the latest files immediately
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["windows/*", "addressables/StandaloneWindows64/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=${{ needs.windows-unity.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
@@ -37,8 +37,5 @@ scripts/refresh_name_layers/refresh_name_layers.zip
|
||||
.metals
|
||||
api_keys.txt
|
||||
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/GeneratedProtos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
|
||||
node_modules/
|
||||
tools/map_generator/output/
|
||||
|
||||
@@ -5,7 +5,6 @@ repos:
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: no-commit-to-branch
|
||||
args: [--branch, main]
|
||||
- repo: https://github.com/pocc/pre-commit-hooks
|
||||
|
||||
@@ -225,9 +225,6 @@ to be used for different players or game situations within the same server proce
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
- Seamless transition between strategic gameplay and hex-based tactical combat
|
||||
- **NEVER add defensive null checks on Unity Inspector fields** - these hide configuration bugs. If a field isn't
|
||||
linked in the editor, it should throw a NullReferenceException so the problem is immediately obvious. Silently
|
||||
skipping code when a required field is null makes bugs harder to find.
|
||||
|
||||
**Go (Build Tools):**
|
||||
|
||||
@@ -336,8 +333,6 @@ dotty.tools.dotc.core.MissingType: Cannot resolve reference to type net.eagle0.e
|
||||
**Configuration:** Game parameters in `/src/main/resources/net/eagle0/eagle/game_parameters.json`
|
||||
**Data Files:** TSV format for battalions, heroes, and other game data
|
||||
|
||||
**NEVER modify hero names.** Do not change names in `heroes.tsv`, `generated_heroes`, or any other hero data files. The names are carefully chosen and are not to be altered.
|
||||
|
||||
## Deployment
|
||||
|
||||
- Bazel handles multi-language builds and dependencies
|
||||
|
||||
+2
-31
@@ -126,26 +126,6 @@ use_repo(
|
||||
"org_golang_x_sys",
|
||||
)
|
||||
|
||||
#
|
||||
# Language Support - Rust
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_rust", version = "0.68.1")
|
||||
|
||||
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
|
||||
rust.toolchain(edition = "2021")
|
||||
use_repo(rust, "rust_toolchains")
|
||||
|
||||
register_toolchains("@rust_toolchains//:all")
|
||||
|
||||
crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate")
|
||||
crate.from_cargo(
|
||||
name = "map_generator_crates",
|
||||
cargo_lockfile = "//src/main/rust/net/eagle0/eagle/map_generator:Cargo.lock",
|
||||
manifests = ["//src/main/rust/net/eagle0/eagle/map_generator:Cargo.toml"],
|
||||
)
|
||||
use_repo(crate, "map_generator_crates")
|
||||
|
||||
#
|
||||
# Platform Support - Apple/iOS
|
||||
#
|
||||
@@ -154,7 +134,7 @@ use_repo(crate, "map_generator_crates")
|
||||
# in a separate workspace at sparkle_workspace/ to isolate the conflict.
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", version = "1.24.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "apple_support", version = "1.23.1", repo_name = "build_bazel_apple_support")
|
||||
|
||||
# rules_swift is a transitive dep of grpc and flatbuffers with conflicting versions:
|
||||
# - grpc 1.76.0.bcr.1 requires rules_swift 3.x (compatibility level 3)
|
||||
@@ -180,9 +160,8 @@ use_repo(apple_cc_configure, "local_config_apple_cc")
|
||||
bazel_dep(name = "protobuf", version = "33.5", repo_name = "com_google_protobuf")
|
||||
bazel_dep(name = "rules_proto", version = "7.1.0")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.14")
|
||||
bazel_dep(name = "grpc", version = "1.74.1")
|
||||
bazel_dep(name = "grpc", version = "1.74.0")
|
||||
bazel_dep(name = "grpc-java", version = "1.78.0")
|
||||
bazel_dep(name = "rules_proto_grpc_csharp", version = "5.8.0")
|
||||
bazel_dep(name = "flatbuffers", version = "25.12.19")
|
||||
|
||||
#
|
||||
@@ -201,12 +180,8 @@ bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
|
||||
# Base image for Eagle (Java 25 JRE - smaller runtime, no dev tools needed)
|
||||
# Digest pinned for Bazel repository cache hit; update with:
|
||||
# TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/eclipse-temurin:pull" | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
|
||||
# curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" "https://registry-1.docker.io/v2/library/eclipse-temurin/manifests/25-jre" | python3 -c "import json,sys; m=json.load(sys.stdin); [print(p['digest']) for p in m.get('manifests',[]) if p['platform']['architecture']=='amd64']"
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jre",
|
||||
digest = "sha256:98236ffdfb61cff3fc4f8e40e5b80460a5a4c35a0f56a5b4182a926844a7db49",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "25-jre",
|
||||
@@ -215,7 +190,6 @@ oci.pull(
|
||||
# Base image for JFR sidecar (Java 25 JDK - includes jcmd for JFR dumps)
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jdk",
|
||||
digest = "sha256:bc2d562355813350f47bc472b9721b0b84761866671ad95d9a9f1151fa69da6e",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "25-jdk",
|
||||
@@ -309,9 +283,6 @@ maven.install(
|
||||
|
||||
# Error tracking
|
||||
"io.sentry:sentry:8.31.0",
|
||||
|
||||
# SQLite for client text storage
|
||||
"org.xerial:sqlite-jdbc:3.46.1.0",
|
||||
],
|
||||
duplicate_version_warning = "error",
|
||||
fail_if_repin_required = True,
|
||||
|
||||
Generated
+10
-2083
File diff suppressed because one or more lines are too long
@@ -243,13 +243,6 @@ pkg_tar(
|
||||
package_dir = "/app",
|
||||
)
|
||||
|
||||
# Package the .e0mj map files for the map editor
|
||||
pkg_tar(
|
||||
name = "admin_maps_layer",
|
||||
srcs = ["//src/main/resources/net/eagle0/shardok/maps:maps_json"],
|
||||
package_dir = "/app/maps",
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "admin_server_image",
|
||||
base = "@alpine_linux_linux_amd64",
|
||||
@@ -258,7 +251,6 @@ oci_image(
|
||||
tars = [
|
||||
":busybox_layer",
|
||||
":admin_binary_layer",
|
||||
":admin_maps_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
)
|
||||
|
||||
@@ -31,17 +31,6 @@ fi
|
||||
|
||||
echo "Found Xcode project: $XCODEPROJ"
|
||||
|
||||
# Ensure export compliance key is set in Info.plist
|
||||
# This bypasses the "Missing Compliance" prompt in App Store Connect.
|
||||
# Set here rather than relying solely on Unity's DeepLinkPostProcessor,
|
||||
# which depends on UNITY_IOS being defined at script compilation time.
|
||||
INFO_PLIST="$XCODE_PROJECT_PATH/Info.plist"
|
||||
if [ -f "$INFO_PLIST" ]; then
|
||||
/usr/libexec/PlistBuddy -c "Set :ITSAppUsesNonExemptEncryption false" "$INFO_PLIST" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool false" "$INFO_PLIST"
|
||||
echo "Set ITSAppUsesNonExemptEncryption=false in Info.plist"
|
||||
fi
|
||||
|
||||
# Unity always generates "Unity-iPhone" as the main app scheme
|
||||
SCHEME="Unity-iPhone"
|
||||
echo "Using scheme: $SCHEME"
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Persist Unity Library/ cache to persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
#
|
||||
# Note: Library/Bee/ is excluded because it contains DAG files with hardcoded
|
||||
# file paths that become stale when project files change. This prevents
|
||||
# "Data at the root level is invalid" XML errors from stale references.
|
||||
|
||||
set -uxo pipefail
|
||||
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "persist Library/ to $CACHE_DIR (excluding Bee/)"
|
||||
|
||||
# rsync may exit with code 23 ("partial transfer due to error") if Unity's
|
||||
# temporary files vanish during the copy. This is acceptable for a cache.
|
||||
/usr/bin/rsync -rtlDvq --exclude='Bee/' src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ "$CACHE_DIR/"
|
||||
rsync_exit=$?
|
||||
|
||||
if [ $rsync_exit -eq 0 ]; then
|
||||
exit 0
|
||||
elif [ $rsync_exit -eq 23 ]; then
|
||||
echo "Warning: rsync exited with 23 (some files vanished during copy). This is expected for Unity temp files."
|
||||
exit 0
|
||||
else
|
||||
echo "Error: rsync failed with exit code $rsync_exit"
|
||||
exit $rsync_exit
|
||||
fi
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restore Unity Library/ cache from persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "restore Library/ from $CACHE_DIR"
|
||||
/bin/mkdir -p "$CACHE_DIR"
|
||||
/usr/bin/rsync -rtlDvq "$CACHE_DIR/" src/main/csharp/net/eagle0/clients/unity/eagle0/Library/
|
||||
@@ -1,102 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Upload xcarchive to TestFlight using App Store Connect API Key
|
||||
# This replaces the deprecated altool which was removed in Xcode 14+
|
||||
#
|
||||
# Uses xcodebuild -exportArchive with destination=upload, which is Apple's
|
||||
# recommended approach for CI/CD pipelines.
|
||||
# Upload IPA to TestFlight using Apple ID credentials (same as Mac notarization)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
# Ensure xcodebuild uses Xcode.app, not Command Line Tools
|
||||
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
IPA_PATH=${1:?Usage: upload_testflight.sh <ipa_path>}
|
||||
|
||||
ARCHIVE_PATH=${1:?Usage: upload_testflight.sh <xcarchive_path> <team_id> <profile_uuid>}
|
||||
TEAM_ID=${2:?Missing team ID}
|
||||
PROFILE_UUID=${3:?Missing provisioning profile UUID}
|
||||
|
||||
if [ ! -d "$ARCHIVE_PATH" ]; then
|
||||
echo "Error: xcarchive not found: $ARCHIVE_PATH"
|
||||
if [ ! -f "$IPA_PATH" ]; then
|
||||
echo "Error: IPA file not found: $IPA_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Uploading to TestFlight: $ARCHIVE_PATH"
|
||||
echo "Uploading to TestFlight: $IPA_PATH"
|
||||
|
||||
# Requires App Store Connect API Key:
|
||||
# - APP_STORE_CONNECT_API_KEY_ID: Key ID from App Store Connect
|
||||
# - APP_STORE_CONNECT_API_ISSUER_ID: Issuer ID from App Store Connect
|
||||
# - APP_STORE_CONNECT_API_KEY_PATH: Path to .p8 private key file
|
||||
#
|
||||
# To create an API key:
|
||||
# 1. Go to https://appstoreconnect.apple.com/access/api
|
||||
# 2. Click the + button to create a new key
|
||||
# 3. Give it a name and Admin or App Manager access
|
||||
# 4. Download the .p8 file (you can only download it once!)
|
||||
# 5. Note the Key ID and Issuer ID shown on the page
|
||||
# Uses same credentials as Mac notarization:
|
||||
# - APPLE_ID: Your Apple ID email
|
||||
# - APP_SPECIFIC_PASSWORD: App-specific password from appleid.apple.com
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_KEY_ID:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_KEY_ID environment variable not set"
|
||||
echo "Create an API key at https://appstoreconnect.apple.com/access/api"
|
||||
if [ -z "${APPLE_ID:-}" ]; then
|
||||
echo "Error: APPLE_ID environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_ISSUER_ID:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_ISSUER_ID environment variable not set"
|
||||
if [ -z "${APP_SPECIFIC_PASSWORD:-}" ]; then
|
||||
echo "Error: APP_SPECIFIC_PASSWORD environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_KEY_PATH:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_KEY_PATH environment variable not set"
|
||||
echo "Uploading with xcrun altool..."
|
||||
|
||||
# Capture output to check for errors (altool may return 0 even on failure)
|
||||
OUTPUT=$(xcrun altool --upload-app \
|
||||
--type ios \
|
||||
--file "$IPA_PATH" \
|
||||
--username "$APPLE_ID" \
|
||||
--password "$APP_SPECIFIC_PASSWORD" 2>&1) || true
|
||||
|
||||
echo "$OUTPUT"
|
||||
|
||||
# Check for error indicators in output
|
||||
if echo "$OUTPUT" | grep -q "ERROR:"; then
|
||||
echo "ERROR: Upload failed. See error messages above."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$APP_STORE_CONNECT_API_KEY_PATH" ]; then
|
||||
echo "Error: API key file not found: $APP_STORE_CONNECT_API_KEY_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create export options plist with upload destination
|
||||
EXPORT_OPTIONS_PLIST=$(mktemp)
|
||||
trap "rm -f $EXPORT_OPTIONS_PLIST" EXIT
|
||||
|
||||
cat > "$EXPORT_OPTIONS_PLIST" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>teamID</key>
|
||||
<string>$TEAM_ID</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>signingCertificate</key>
|
||||
<string>Apple Distribution: Daniel Crosby (UWJ88DX8WQ)</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>net.eagle0.eagle</key>
|
||||
<string>$PROFILE_UUID</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "Export options:"
|
||||
cat "$EXPORT_OPTIONS_PLIST"
|
||||
|
||||
echo "Uploading to App Store Connect..."
|
||||
|
||||
# Use xcodebuild to upload with App Store Connect API authentication
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
|
||||
-authenticationKeyPath "$APP_STORE_CONNECT_API_KEY_PATH" \
|
||||
-authenticationKeyID "$APP_STORE_CONNECT_API_KEY_ID" \
|
||||
-authenticationKeyIssuerID "$APP_STORE_CONNECT_API_ISSUER_ID"
|
||||
|
||||
echo "Upload complete! Check App Store Connect for processing status."
|
||||
echo "The build should appear in TestFlight within 15-30 minutes after processing."
|
||||
|
||||
@@ -208,8 +208,6 @@ services:
|
||||
- "${JFR_SIDECAR_ADDR:-jfr-sidecar:8081}" # Can be switched for blue-green
|
||||
- "--http-port"
|
||||
- "8080"
|
||||
- "--maps-dir"
|
||||
- "/app/maps"
|
||||
environment:
|
||||
# Secret for CI to authenticate client update notifications
|
||||
NOTIFY_SECRET: "${NOTIFY_SECRET:-}"
|
||||
@@ -217,10 +215,6 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${ADMIN_S3_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${ADMIN_S3_SECRET_KEY:-}"
|
||||
# GitHub PAT for map editor PR creation (scoped to contents:write + pull_requests:write)
|
||||
GITHUB_TOKEN: "${GITHUB_TOKEN:-}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# No external port - accessed via nginx at admin.eagle0.net
|
||||
depends_on:
|
||||
- auth
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
# Programmatic Map Image Generation
|
||||
|
||||
## Overview
|
||||
|
||||
Create a tool to programmatically regenerate the Eagle strategic map images with more equalized province sizes while preserving neighbor relationships. Also implement dynamic text rendering for province names that scales with zoom.
|
||||
|
||||
## Current Map Assets
|
||||
|
||||
| File | Description | Dimensions |
|
||||
|------|-------------|------------|
|
||||
| `Assets/Eagle/rawGray.gz.bytes` | Province ID lookup map (gzip compressed) | 3786 x 1834 |
|
||||
| `Assets/Eagle/map_bw_labels.png` | B&W map with baked-in province labels | 3786 x 1834, RGBA |
|
||||
| `Assets/Eagle/Materials/1.png` - `43.png` | Individual province mask images | 3786 x 1834, grayscale |
|
||||
|
||||
## Province Data
|
||||
|
||||
- 43 provinces defined in `src/main/resources/net/eagle0/eagle/province_map.tsv`
|
||||
- Each has: id, name, neighbors, neighborPositions, hexMapName
|
||||
- Problem provinces (too small): **Shumal** (id=1), **Kojaria** (id=39), **Usvol** (id=33), **Tumala** (id=35), **Chia** (id=43)
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Equalize province sizes** - Reduce disparity so small provinces are easier to click
|
||||
2. **Preserve topology** - Maintain all neighbor relationships
|
||||
3. **Preserve coastline** - Keep ocean/land boundaries
|
||||
4. **Dynamic text rendering** - Province names scale with zoom level
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Province Size Equalization Tool
|
||||
|
||||
### Algorithm: Constrained Voronoi Relaxation
|
||||
|
||||
1. **Extract current state from rawGray:**
|
||||
- Calculate centroid of each province
|
||||
- Calculate area (pixel count) of each province
|
||||
- Identify ocean pixels (value 0 or 255)
|
||||
|
||||
2. **Iterative relaxation:**
|
||||
```
|
||||
For each iteration:
|
||||
For each province:
|
||||
Calculate target area (average of all provinces)
|
||||
If province is too small:
|
||||
Move centroid away from larger neighbors
|
||||
If province is too large:
|
||||
Move centroid toward smaller neighbors
|
||||
Constraint: Don't break neighbor relationships
|
||||
```
|
||||
|
||||
3. **Generate new boundaries:**
|
||||
- Use weighted Voronoi diagram from relaxed centroids
|
||||
- Weight by target area
|
||||
- Mask with original coastline
|
||||
|
||||
4. **Output new images:**
|
||||
- New rawGray (province ID map)
|
||||
- New province masks (1.png - 43.png)
|
||||
- New B&W base map (derived from boundaries)
|
||||
|
||||
### Implementation Options
|
||||
|
||||
**Option A: Python Script (Recommended)**
|
||||
- Use numpy, scipy, PIL/Pillow
|
||||
- Can run offline, generate assets once
|
||||
- Easier to iterate and debug
|
||||
- Output: New image files to replace existing
|
||||
|
||||
**Option B: Unity Editor Script**
|
||||
- C# with Unity's Texture2D
|
||||
- Integrated into build process
|
||||
- Slower, harder to debug
|
||||
|
||||
### Tool Location
|
||||
`tools/map_generator/` or `scripts/generate_map.py`
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Dynamic Province Name Rendering
|
||||
|
||||
### Current State
|
||||
- Province names are baked into `map_bw_labels.png`
|
||||
- Not visible when zoomed out, too small when zoomed in
|
||||
- `SetUpCenterText()` in MapController shows selected/hovered province name in a fixed UI panel
|
||||
|
||||
### New Approach
|
||||
|
||||
1. **Create province label GameObjects:**
|
||||
- One TextMeshPro label per province
|
||||
- Position at province centroid
|
||||
- Child of map content (moves with zoom/pan)
|
||||
|
||||
2. **Dynamic scaling:**
|
||||
- Scale inversely with zoom level
|
||||
- At zoom 1.0: Show major provinces only (or none)
|
||||
- At zoom 2.0+: Show all province names
|
||||
- Font size adjusts to fit province area
|
||||
|
||||
3. **Implementation:**
|
||||
```csharp
|
||||
public class ProvinceLabelsController : MonoBehaviour {
|
||||
public MapPinchZoomHandler zoomHandler;
|
||||
public TextMeshProUGUI labelPrefab;
|
||||
|
||||
private Dictionary<int, TextMeshProUGUI> _labels;
|
||||
|
||||
void Update() {
|
||||
float zoom = zoomHandler.CurrentZoom;
|
||||
foreach (var label in _labels.Values) {
|
||||
label.transform.localScale = Vector3.one / zoom;
|
||||
label.gameObject.SetActive(zoom >= 1.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Province centroid data:**
|
||||
- Add `centroid_x`, `centroid_y` columns to `province_map.tsv`
|
||||
- Or calculate at runtime from rawGray
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
### New Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `tools/map_generator/generate_map.py` | Python script to generate new map images |
|
||||
| `tools/map_generator/requirements.txt` | Python dependencies |
|
||||
| `Assets/Eagle/ProvinceLabelsController.cs` | Dynamic province name rendering |
|
||||
|
||||
### Modified Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `Assets/Eagle/rawGray.gz.bytes` | Replaced with equalized version |
|
||||
| `Assets/Eagle/Materials/*.png` | Replaced province masks |
|
||||
| `Assets/Eagle/map_bw_labels.png` | Either: regenerated, or removed if using dynamic labels |
|
||||
| `province_map.tsv` | Add centroid columns |
|
||||
| `Assets/Gameplay.unity` | Add ProvinceLabelsController, label prefab instances |
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Visual inspection:**
|
||||
- Compare old vs new province sizes
|
||||
- Verify small provinces (Shumal, Kojaria, Usvol) are larger
|
||||
- Verify neighbor relationships preserved
|
||||
|
||||
2. **Functional testing:**
|
||||
- Click on each province at various zoom levels
|
||||
- Verify correct province is selected
|
||||
- Verify province colors/weather still work
|
||||
|
||||
3. **Label testing:**
|
||||
- Zoom in/out, verify labels scale appropriately
|
||||
- Pan around, verify labels move with map
|
||||
- Verify labels don't overlap excessively
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
1. **Equalization approach:** Reduce disparity only
|
||||
- Make small provinces (Shumal, Kojaria, Usvol, etc.) 2-3x larger
|
||||
- Minimal changes to large provinces
|
||||
- Not targeting perfectly equal areas
|
||||
|
||||
2. **Coastline:** Allow simplification
|
||||
- Coastline can be smoothed/simplified as part of generation
|
||||
- Voronoi edges are acceptable
|
||||
|
||||
3. **Map style:** Solid fills with borders
|
||||
- Clean, simple look
|
||||
- Easier to generate programmatically
|
||||
- Province boundaries rendered as dark lines
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Python Map Generator Tool
|
||||
|
||||
```
|
||||
tools/map_generator/
|
||||
├── generate_map.py # Main script
|
||||
├── requirements.txt # numpy, scipy, Pillow
|
||||
└── README.md # Usage instructions
|
||||
```
|
||||
|
||||
**Algorithm:**
|
||||
1. Load current rawGray.gz.bytes, decompress
|
||||
2. Calculate each province's centroid and area
|
||||
3. Identify land mask (non-ocean pixels)
|
||||
4. Run constrained Voronoi relaxation:
|
||||
- Small provinces push neighbors away
|
||||
- Large provinces pull neighbors in
|
||||
- Stop when min province is >= 50% of average
|
||||
5. Generate weighted Voronoi from relaxed centroids
|
||||
6. Output:
|
||||
- `rawGray_new.bytes` (province ID map)
|
||||
- `1.png` - `43.png` (province masks)
|
||||
- `map_bw.png` (solid fill + borders)
|
||||
- `centroids.json` (for label positioning)
|
||||
|
||||
### Step 2: Dynamic Province Labels (Unity)
|
||||
|
||||
1. Create `ProvinceLabelsController.cs`
|
||||
2. Load centroids from JSON or calculate from rawGray
|
||||
3. Spawn TextMeshPro labels at each centroid
|
||||
4. Update label scale/visibility based on zoom
|
||||
|
||||
### Step 3: Integration
|
||||
|
||||
1. Replace asset files with generated versions
|
||||
2. Update `MapController` to use new assets
|
||||
3. Add `ProvinceLabelsController` to scene
|
||||
4. Remove baked label image dependency
|
||||
@@ -1,79 +0,0 @@
|
||||
# Renaming a Province
|
||||
|
||||
This document explains the steps required to rename a province in Eagle0.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### 1. Province Map TSV (Server Source of Truth)
|
||||
|
||||
**File:** `src/main/resources/net/eagle0/eagle/province_map.tsv`
|
||||
|
||||
This is the primary source of province data. Each row contains:
|
||||
- Province ID
|
||||
- Province name
|
||||
- Neighbor IDs
|
||||
- Neighbor directions
|
||||
- Province name (repeated)
|
||||
- Neighbor names (dot-separated)
|
||||
- Starting food
|
||||
|
||||
You need to:
|
||||
1. Change the province name in its own row (columns 2 and 5)
|
||||
2. Update the neighbor name references in all neighboring provinces (column 6)
|
||||
|
||||
Example: To rename "Fluria" to "NewName", you would need to update:
|
||||
- Line 26: The Fluria row itself
|
||||
- Lines 5, 10, 18, 24, 27, 38, 41: All provinces that list Fluria as a neighbor
|
||||
|
||||
### 2. LLM Map Description
|
||||
|
||||
**File:** `src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators/MapDescription.scala`
|
||||
|
||||
This file contains a hardcoded geographic description of the map used in LLM prompts for generating narrative text. Update any references to the old province name.
|
||||
|
||||
### 3. Client Centroids JSON
|
||||
|
||||
**File:** `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/centroids.json`
|
||||
|
||||
This JSON file contains province metadata used for map rendering (centroid positions, areas, orientations). Each entry has a `name` field that should be updated to match.
|
||||
|
||||
Note: The actual province name displayed to players comes from the server via the `ProvinceView` proto, not from this file. However, this file should be kept in sync for consistency and because it may be used for map label positioning.
|
||||
|
||||
### 4. Test Files
|
||||
|
||||
**File:** `src/test/scala/net/eagle0/eagle/library/util/validations/RuntimeValidatorTest.scala`
|
||||
|
||||
Update any test strings that reference the old province name.
|
||||
|
||||
## How Province Names Flow to the Client
|
||||
|
||||
Province names are sent from the Eagle server to the Unity client via the `ProvinceView` protobuf message:
|
||||
|
||||
```protobuf
|
||||
message ProvinceView {
|
||||
int32 id = 1;
|
||||
string name = 6; // <-- Province name sent from server
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The server reads province names from `province_map.tsv` at startup. The client receives names through the proto and displays them via `province.Name` in C# code. This means:
|
||||
|
||||
- Changing the TSV automatically updates what clients see
|
||||
- No C# code changes are needed (the code uses `province.Name` generically)
|
||||
- The `centroids.json` name field is for map rendering/positioning, not display
|
||||
|
||||
## Files That Do NOT Need Changes
|
||||
|
||||
- **Hero backstory TSV files** (`heroes.tsv`, `generated_heroes.tsv`) - These don't contain province name references
|
||||
- **C# client code** - Uses `province.Name` from the server proto, not hardcoded names
|
||||
- **Proto definitions** - No province names are hardcoded in protos
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Update `province_map.tsv` - province's own row
|
||||
- [ ] Update `province_map.tsv` - all neighbor references
|
||||
- [ ] Update `MapDescription.scala`
|
||||
- [ ] Update `centroids.json`
|
||||
- [ ] Update any test files with province name references
|
||||
- [ ] Build and run tests: `bazel test //src/test/scala/...`
|
||||
@@ -77,4 +77,3 @@ Larger set of goals in [The Big Eagle TODO](https://docs.google.com/document/d/1
|
||||
## Nice to have
|
||||
|
||||
- [x] "What's new" changelog (fetch JSON, show entries since last launch)
|
||||
- [ ] Windows code signing ([Azure Artifact Signing](https://azure.microsoft.com/en-us/products/artifact-signing/), ~$10/mo) to eliminate SmartScreen "unknown publisher" warning
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
# Tutorial Battle System
|
||||
|
||||
This document describes the tutorial battle system for first-time players. The system provides a scripted introductory battle that teaches combat basics while telling a story.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
When a new player starts their first game in tutorial mode, they experience:
|
||||
|
||||
1. **Narrative Intro** - Story screens introducing the scenario
|
||||
2. **Immediate Battle** - Skip strategic map, start directly in combat
|
||||
3. **Scripted Flee** - Enemy flees when player is "on the ropes"
|
||||
4. **Reinforcements** - Allied heroes arrive mid-battle
|
||||
5. **Heroes Join** - Reinforcement heroes join player's faction after battle
|
||||
|
||||
---
|
||||
|
||||
## Battle Configuration
|
||||
|
||||
### Defender (Player - John Ranil)
|
||||
- **Heroes**: John Ranil + 2 random vassals (3 total)
|
||||
- **Units**:
|
||||
- 2x Light Infantry (300 troops each, 60 training/armament)
|
||||
- 1x Longbowmen (200 troops, 60 training/armament)
|
||||
- **Province**: 14 (Onmaa) with 40 support, 500 gold, 3000 food
|
||||
- **Restriction**: Cannot flee (must defend)
|
||||
|
||||
### Attacker (Ikhaan Tarn)
|
||||
- **Heroes**: Ikhaan Tarn + 2 sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 1x Heavy Cavalry (600 troops, 80 training/armament)
|
||||
- 1x Heavy Infantry (500 troops, 80 training/armament)
|
||||
- 1x Longbowmen (300 troops, 80 training/armament)
|
||||
- **Origin Province**: 32
|
||||
|
||||
### Flee Trigger
|
||||
Attacker flees when ANY of these conditions are met:
|
||||
- Player loses 1 unit
|
||||
- 5 rounds have passed
|
||||
|
||||
### Reinforcements
|
||||
When Tarn flees, these heroes arrive as player reinforcements:
|
||||
- Elena Fyar
|
||||
- Hedrick
|
||||
- The Boulder
|
||||
|
||||
---
|
||||
|
||||
## Implementation Architecture
|
||||
|
||||
### Phase 1: Narrative Intro System
|
||||
|
||||
**Proto Changes:**
|
||||
- `game_state_view.proto`: Added `TutorialNarrativeScreen` message and `pending_narrative_screens` field
|
||||
- `game_parameters.proto`: Added `TutorialNarrativeScreen` and `tutorial_narrative_screens` field
|
||||
|
||||
**Unity Client:**
|
||||
- `NarrativeScreenController.cs`: Modal screen with title, body text, optional image
|
||||
- `EagleGameController.cs`: Checks for pending narrative screens on game start
|
||||
|
||||
**Narrative Content** (defined in `tutorial_game_parameters.json`):
|
||||
```
|
||||
Screen 1: "The Engineer of Onmaa" - Player backstory
|
||||
Screen 2: "The Traitor Arrives" - Tarn's attack
|
||||
Screen 3: "Defend Your Home" - Call to action
|
||||
```
|
||||
|
||||
### Phase 2: Auto-Start Battle
|
||||
|
||||
**Configuration:**
|
||||
- `tutorial_game_parameters.json`: Contains `tutorialBattle` config with attacker, target, flee conditions
|
||||
- `game_parameters.proto`: `TutorialBattleConfig` message
|
||||
|
||||
**Game Creation:**
|
||||
- `NewGameCreation.scala`: `setupTutorialBattle()` method creates:
|
||||
- `HostileArmyGroup` with `Attacking` status (bypasses decision phase)
|
||||
- `Army` for defender
|
||||
- Both added to target province
|
||||
|
||||
**Battle Creation:**
|
||||
- When round advances to `BattleRequest` phase, `RequestBattlesAction` automatically creates the `ShardokBattle`
|
||||
|
||||
### Phase 3: Scripted Flee Mechanism
|
||||
|
||||
**C++ Component:**
|
||||
- `TutorialBattleController.hpp/cpp`: Controller for scripted battle logic
|
||||
|
||||
**Key Methods:**
|
||||
```cpp
|
||||
// Check all events and execute triggered actions (called at end of each round)
|
||||
std::vector<ActionResult> CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator);
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- `ShardokEngine.cpp`: Calls `CheckAndExecuteEvents()` at end of each round in `HandlePlayerTurnEnd()`
|
||||
- Events fire in config order, each event fires at most once
|
||||
|
||||
### Phase 4: Mid-Battle Reinforcements
|
||||
|
||||
**Action Types** (in `action_type.proto`):
|
||||
- `TUTORIAL_ENEMY_FLED = 78`
|
||||
- `TUTORIAL_REINFORCEMENTS_ARRIVED = 79`
|
||||
|
||||
**Implementation (TODO):**
|
||||
- When scripted flee triggers, create reinforcement units in defender's reserves
|
||||
- Units have `UnitStatus_RESERVE_UNIT` status
|
||||
- Reinforcements enter battle on subsequent turns
|
||||
|
||||
### Phase 5: Tutorial Popups
|
||||
|
||||
**Content Definitions** (in `TutorialContentDefinitions.cs`):
|
||||
- `tutorial_battle_started`: "Defend your province!"
|
||||
- `tutorial_enemy_fled`: "The enemy retreats!"
|
||||
- `tutorial_reinforcements_arrived`: "Allies have arrived!"
|
||||
- `tutorial_battle_victory`: Victory celebration
|
||||
|
||||
**Trigger Integration:**
|
||||
- `TutorialTriggerRegistry.cs`: Handles tutorial action types from `ActionResultView`
|
||||
- `ShardokGameController.cs`: Detects tutorial events and triggers popups
|
||||
|
||||
### Phase 6: Post-Battle Hero Joining
|
||||
|
||||
**Implementation (TODO):**
|
||||
- `ResolveBattleAction.scala`: Check if battle was tutorial and defender won
|
||||
- Create `TutorialHeroJoined` action results adding reinforcement heroes to player faction
|
||||
- Set appropriate loyalty, vigor, location for joined heroes
|
||||
|
||||
### Phase 7: Post-Battle Strategic Dialogue
|
||||
|
||||
After the battle ends and the player returns to the strategic map, narrative dialogues guide them through the next steps.
|
||||
|
||||
**Dialogue 1: Battle Aftermath** (trigger: `tutorial_battle_ended`)
|
||||
- Old Marek reflects on the close call
|
||||
- Tarn has vanished — rumors of sorcery or escape, no one knows
|
||||
- Captured lieutenants: they followed orders, not to blame for Tarn's madness
|
||||
- Player should try to recruit them or hold them
|
||||
- No instruction text needed — Handle Captured Heroes UI is self-explanatory
|
||||
|
||||
**Dialogue 2: Rebuild Support** (trigger: `tutorial_rebuild_support`)
|
||||
- Fires when available commands no longer include HandleCapturedHeroCommand
|
||||
- Marek explains the province needs support rebuilt after the battle
|
||||
- Engineers (John Ranil) can **Improve** the province — build infrastructure, develop economy
|
||||
- Paladins (Elena Fyar) can **Give Alms** — distribute food to win hearts
|
||||
- Instructions highlight **Improve** and **Give Alms** buttons
|
||||
- Goal: get Support to **40** before January for tax revenue
|
||||
|
||||
**Implementation:**
|
||||
- Dialogues defined in `tutorial_strategic.json`
|
||||
- `tutorial_battle_ended`: fire when tutorial battle is removed from `RunningShardokGameModels`
|
||||
- `tutorial_rebuild_support`: fire when available commands no longer include `HandleCapturedHeroCommand`
|
||||
- Register `ImproveButton` and `AlmsButton` as highlight targets in `TutorialTargetRegistry`
|
||||
|
||||
See `TUTORIAL_CONTENT.md` for full dialogue text.
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
|
||||
### New Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `TutorialBattleController.hpp/cpp` | C++ scripted flee and reinforcement logic |
|
||||
| `NarrativeScreenController.cs` | Unity narrative screen display |
|
||||
|
||||
### Modified Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `tutorial_game_parameters.json` | Battle configuration and narrative content |
|
||||
| `game_state_view.proto` | Narrative screens field |
|
||||
| `game_parameters.proto` | Tutorial battle config |
|
||||
| `player_info.proto` | Tutorial battle flags for Shardok |
|
||||
| `action_type.proto` | Tutorial action types |
|
||||
| `NewGameCreation.scala` | Auto-start battle setup |
|
||||
| `EagleGameController.cs` | Narrative display integration |
|
||||
| `TutorialContentDefinitions.cs` | Battle tutorial content |
|
||||
| `TutorialTriggerRegistry.cs` | Tutorial event triggers |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Implementation
|
||||
|
||||
### Disable Player Flee
|
||||
In `FleeCommandFactory.cpp`:
|
||||
- Check if tutorial mode is enabled
|
||||
- If defender, don't offer flee commands
|
||||
|
||||
### Reinforcement Units
|
||||
In `TutorialBattleController::ExecuteScriptedFlee()`:
|
||||
- Create `Unit` objects for Elena Fyar, Hedrick, The Boulder
|
||||
- Add to defender's reserve units
|
||||
- Generate `TUTORIAL_REINFORCEMENTS_ARRIVED` action
|
||||
|
||||
### Post-Battle Hero Joining
|
||||
In Eagle's battle resolution:
|
||||
- Detect tutorial battle completion
|
||||
- Create heroes in player's faction with proper stats
|
||||
- Generate notification action results
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] New user sees narrative screens before battle
|
||||
- [ ] Battle starts immediately after narratives (no strategic map)
|
||||
- [ ] Player has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Attacker has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Player cannot flee
|
||||
- [ ] Tarn flees after player loses 1 unit
|
||||
- [ ] Tarn flees after 5 rounds (if no units lost)
|
||||
- [ ] Reinforcements appear when Tarn flees
|
||||
- [ ] Tutorial popups appear at correct moments
|
||||
- [ ] Reinforcement heroes join faction after battle victory
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### tutorial_game_parameters.json
|
||||
```json
|
||||
{
|
||||
"tutorialBattle": {
|
||||
"attackerFactionHead": "Ikhaan Tarn",
|
||||
"targetProvinceId": 14,
|
||||
"fleeAfterDefenderUnitsLost": 1,
|
||||
"fleeAfterRounds": 5,
|
||||
"reinforcements": ["Elena Fyar", "Hedrick", "The Boulder"]
|
||||
},
|
||||
"tutorialNarrativeScreens": [
|
||||
{ "title": "...", "bodyText": "...", "imagePath": "" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### TutorialBattleConfig Proto (Shardok)
|
||||
```protobuf
|
||||
message TutorialBattleConfig {
|
||||
bool enabled = 1;
|
||||
repeated TutorialEvent events = 2;
|
||||
repeated UnitInitialSize initial_sizes = 3; // For damage tracking
|
||||
}
|
||||
|
||||
message TutorialEvent {
|
||||
string event_id = 1;
|
||||
TutorialTrigger trigger = 2;
|
||||
TutorialAction action = 3;
|
||||
}
|
||||
|
||||
message TutorialTrigger {
|
||||
oneof trigger_type {
|
||||
RoundTrigger after_round = 1;
|
||||
UnitsLostTrigger units_lost = 2;
|
||||
DamageTakenTrigger damage_taken = 3;
|
||||
UnitKilledTrigger unit_killed = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message TutorialAction {
|
||||
oneof action_type {
|
||||
FleeAction flee = 1;
|
||||
ReinforcementsAction reinforcements = 2; // Uses CommonUnit
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -149,52 +149,6 @@ Triggered during battles when players encounter spells, terrain, or abilities.
|
||||
|
||||
---
|
||||
|
||||
## Post-Battle Dialogue (Narrative Dialogues)
|
||||
|
||||
These fire after the first tutorial battle ends and the player returns to the strategic map. They use the narrative dialogue system (`DialogueManager` / `tutorial_strategic.json`) rather than the modal tutorial system.
|
||||
|
||||
### Battle Aftermath
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ID | `post_battle_aftermath` |
|
||||
| Trigger | `tutorial_battle_ended` (battle removed from RunningShardokGameModels) |
|
||||
| Speaker | Old Marek the Learned |
|
||||
| Panel Position | top |
|
||||
|
||||
**Dialogue (Marek):**
|
||||
> That was closer than I'd like to admit. We held — barely. But Tarn... Tarn is gone. Not retreated — *gone*. The men say he vanished from the field. Some claim sorcery. Others say he slipped away in the chaos. Whatever the truth, no one can find him.
|
||||
>
|
||||
> But we captured some of his lieutenants. These are soldiers, Sadar — they followed orders, same as we once did. They're not to blame for Tarn's madness. See if you can bring them to our cause, or at least hold them until they come around.
|
||||
|
||||
**Instructions:** *(none — Handle Captured Heroes UI is self-explanatory)*
|
||||
|
||||
### Rebuild Support
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ID | `post_battle_rebuild` |
|
||||
| Trigger | `tutorial_rebuild_support` (fires when available commands no longer include HandleCapturedHeroCommand) |
|
||||
| Speaker | Old Marek the Learned |
|
||||
| Panel Position | top |
|
||||
|
||||
**Dialogue (Marek):**
|
||||
> Good. Now we need to rebuild our support here in Onmaa. The people are shaken — a battle on their doorstep will do that. We need them behind us if we're going to hold this province.
|
||||
>
|
||||
> Any of our heroes can develop the land or give alms to the people — nothing wins hearts faster than a full belly. Engineers like John Ranil are especially effective at improving a province, and paladins like Elena Fyar are the best at winning hearts. Either way, we need the people's support before the tax collectors come round in January.
|
||||
|
||||
**Instructions:** Use **Improve** and **Give Alms** to raise Support. You must have at least **40** support in the province by January in order to collect gold and food in taxes.
|
||||
|
||||
**Highlight targets:** `SupportField`
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- Both dialogues go in `tutorial_strategic.json`
|
||||
- Need new trigger events: `tutorial_battle_ended` and `tutorial_rebuild_support`
|
||||
- `tutorial_battle_ended` should fire when the tutorial battle is removed from `RunningShardokGameModels` in `EagleGameModel.ApplyGameStateViewDiff`
|
||||
- `tutorial_rebuild_support` should fire when available commands no longer include `HandleCapturedHeroCommand` (i.e. the captured heroes phase is done and the player has regular commands available)
|
||||
- The "rebuild" dialogue should highlight the Improve and Give Alms buttons in the command panel (requires registering them as highlight targets)
|
||||
|
||||
---
|
||||
|
||||
## Display Modes
|
||||
|
||||
| Mode | Description | Use For |
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
# AI Quest Completion Behavior
|
||||
|
||||
This document describes which quests the AI attempts to complete proactively to recruit unaffiliated heroes.
|
||||
|
||||
## Overview
|
||||
|
||||
The AI attempts to complete quests via `FulfillQuestsCommandSelector`, which is invoked by `MidGameAIClient.chosenFulfillEasyQuestsCommand`. The AI only considers quests from unaffiliated heroes in provinces ruled by a faction leader.
|
||||
|
||||
## Quests the AI Actively Completes
|
||||
|
||||
### Diplomacy Quests
|
||||
|
||||
| Quest | Handler | Conditions |
|
||||
|-------|---------|------------|
|
||||
| `AllianceQuest` | `AllianceQuestCommandChooser` | Target faction must meet trust conditions for alliance and not already be in an alliance |
|
||||
| `TruceWithFactionQuest` | `TruceWithFactionQuestCommandChooser` | Target faction must meet trust conditions for truce |
|
||||
| `TruceCountQuest` | `TruceCountQuestCommandChooser` | Picks a random faction that meets trust conditions and isn't already in a truce/alliance |
|
||||
|
||||
### Resource Giving Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `AlmsToProvinceQuest` | `AlmsToProvinceQuestCommandChooser` | Gives food to the specified province |
|
||||
| `AlmsAcrossRealmQuest` | `AlmsAcrossRealmQuestCommandChooser` | Gives food from the province with the largest surplus |
|
||||
| `GiveToHeroesInProvinceQuest` | `GiveToHeroesInProvinceQuestCommandChooser` | Gives gold to the hero with the lowest loyalty in the specified province |
|
||||
| `GiveToHeroesAcrossRealmQuest` | `GiveToHeroesAcrossRealmQuestCommandChooser` | Gives gold from the province with the most gold available |
|
||||
|
||||
### Province Development Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `ImproveAgricultureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Agriculture type |
|
||||
| `ImproveEconomyQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Economy type |
|
||||
| `ImproveInfrastructureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Infrastructure type |
|
||||
|
||||
### Other Quests
|
||||
|
||||
| Quest | Handler | Conditions |
|
||||
|-------|---------|------------|
|
||||
| `DismissSpecificVassalQuest` | `DismissSpecificVassalCommandChooser` | Only if province has more than 2 heroes AND the unaffiliated hero's power >= target hero's power * `RequiredPowerMultiplierForDismiss` |
|
||||
|
||||
## Quests the AI Does Not Attempt to Complete
|
||||
|
||||
The following quests have no handler in `FulfillQuestsCommandSelector` and must be completed naturally through gameplay:
|
||||
|
||||
### Combat/Military Quests
|
||||
- `DefeatFactionQuest` - Defeat a specific faction
|
||||
- `GrandArmyQuest` - Accumulate a large number of troops
|
||||
- `UpgradeBattalionQuest` - Upgrade a battalion to minimum armament/training
|
||||
- `WinBattleOutnumberedQuest` - Win a battle while outnumbered
|
||||
- `WinBattlesQuest` - Win a number of battles
|
||||
- `RescueImprisonedLeaderQuest` - Rescue an imprisoned leader from another faction
|
||||
|
||||
### Expansion Quests
|
||||
- `ExpandToProvincesQuest` - Expand to control a certain number of provinces
|
||||
- `SpecificExpansionQuest` - Conquer a specific province
|
||||
- `BorderSecurityQuest` - Have troops in a border province
|
||||
|
||||
### Prisoner Quests
|
||||
- `ExecutePrisonerQuest` - Execute a specific prisoner
|
||||
- `ExilePrisonerQuest` - Exile a specific prisoner
|
||||
- `ReleasePrisonerQuest` - Release a specific prisoner
|
||||
- `ReturnPrisonerQuest` - Return a prisoner to their faction
|
||||
- `ReleaseAllPrisonersQuest` - Release all prisoners
|
||||
|
||||
### Province Order Quests
|
||||
- `DevelopProvincesQuest` - Maintain provinces in Develop order for months
|
||||
- `MobilizeProvincesQuest` - Maintain provinces in Mobilize order for months
|
||||
- `RestProvinceQuest` - Use the Rest command in a specific province
|
||||
|
||||
### Reconnaissance Quests
|
||||
- `ReconProvincesQuest` - Reconnoiter a number of provinces
|
||||
- `ReconSpecificProvincesQuest` - Reconnoiter specific provinces
|
||||
|
||||
### Economic Quests
|
||||
- `TotalDevelopmentQuest` - Achieve total development level in a province
|
||||
- `WealthQuest` - Accumulate gold and food
|
||||
- `SpendOnFeastsQuest` - Spend gold on feasts
|
||||
- `SendSuppliesQuest` - Send food to a specific province
|
||||
- `RepairDevastationQuest` - Repair devastation
|
||||
|
||||
### Special Event Quests
|
||||
- `SuppressRiotByForceQuest` - Suppress a riot by force
|
||||
- `FightBeastsAloneQuest` - Fight beasts alone
|
||||
- `StartBlizzardQuest` - Start a blizzard in a province
|
||||
- `StartEpidemicQuest` - Start an epidemic in a province
|
||||
- `ApprehendOutlawQuest` - Apprehend an outlaw hero
|
||||
|
||||
### Miscellaneous
|
||||
- `BattalionDiversityQuest` - Have diverse battalion types
|
||||
- `SwearBrotherhoodWithHeroQuest` - Swear brotherhood with a specific hero
|
||||
- `BetrayAllyQuest` - Betray an allied faction
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The quest completion logic is located in:
|
||||
- `FulfillQuestsCommandSelector.scala` - Main entry point, iterates through choosers
|
||||
- `quest_command_selectors/` - Individual quest handlers
|
||||
|
||||
Each chooser extends either:
|
||||
- `QuestCommandChooser` - For quests requiring randomness
|
||||
- `DeterministicQuestCommandChooser` - For quests with deterministic command selection
|
||||
|
||||
The AI prioritizes quests in the order they appear in `FulfillQuestsCommandSelector.choosers`:
|
||||
1. Alliance
|
||||
2. TruceWithFaction
|
||||
3. Improve (Agriculture/Economy/Infrastructure)
|
||||
4. AlmsToProvince
|
||||
5. GiveToHeroesInProvince
|
||||
6. AlmsAcrossRealm
|
||||
7. GiveToHeroesAcrossRealm
|
||||
8. TruceCount
|
||||
9. DismissSpecificVassal
|
||||
@@ -1,230 +0,0 @@
|
||||
# Plan: Eliminate Battalion Backstory LLM Updates
|
||||
|
||||
## Goal
|
||||
|
||||
Dramatically reduce LLM request volume by eliminating battalion backstory updates. Battalions will retain:
|
||||
- An **initial backstory** (generated once when created)
|
||||
- A **history of events** (structured data, not LLM-generated prose)
|
||||
|
||||
The history will be fed into other LLM updates (hero backstories, chronicle) but will no longer be regenerated into prose for each battalion.
|
||||
|
||||
## Current System Overview
|
||||
|
||||
### Data Flow
|
||||
```
|
||||
Battalion Created → Initial Backstory LLM Request → Text stored
|
||||
↓
|
||||
Battalion participates in events → Events accumulated → Update LLM Request → New text version
|
||||
↓
|
||||
BattalionView.backstoryTextId → Client displays on hover
|
||||
```
|
||||
|
||||
### Event Types (8 total)
|
||||
Events are structured data already captured in `EventForBattalionBackstory`:
|
||||
- OrganizedTroops, SuppressedBeasts, ApprehendedOutlaws, FoughtInBattle
|
||||
- SuppressedRiot, Trained, Armed, SurvivedStarvation
|
||||
|
||||
Each event has date, province, hero, and event-specific details.
|
||||
|
||||
### Key Files
|
||||
|
||||
| Component | File |
|
||||
|-----------|------|
|
||||
| Unity Display | `Assets/Eagle/HeroesAndBattalionsPanelController.cs` |
|
||||
| View Proto | `views/battalion_view.proto` |
|
||||
| View Filter | `BattalionViewFilter.scala` |
|
||||
| Update Action | `BattalionBackstoryUpdateAction.scala` |
|
||||
| Update Generator | `BattalionBackstoryUpdateActionGenerator.scala` |
|
||||
| Update Prompt | `BattalionBackstoryUpdatePromptGenerator.scala` |
|
||||
| Event-to-Text | `BattalionBackstoryUpdatePromptGenerator.textForEvent()` |
|
||||
| BattalionDescriptions | `BattalionDescriptions.scala` (used in other LLM prompts) |
|
||||
|
||||
---
|
||||
|
||||
## Stage 1: Stop Displaying Battalion Backstories in Client
|
||||
|
||||
**Goal:** Remove the UI that shows battalion backstory text on hover.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`Assets/Eagle/HeroesAndBattalionsPanelController.cs`**
|
||||
- In `BattalionLongHoverRowChanged()` (lines 143-169):
|
||||
- Remove or comment out the backstory text population
|
||||
- Keep the header/name display, remove `battalionPopupPanelBackstory.TextId = ...`
|
||||
|
||||
**Unity Scene/Prefab**
|
||||
- Hide or remove the `battalionPopupPanelBackstory` UI element
|
||||
|
||||
### Verification
|
||||
- [ ] Battalion hover popup no longer shows backstory text
|
||||
- [ ] No errors when hovering over battalions
|
||||
- [ ] Other battalion info (name, type, size) still displays
|
||||
|
||||
---
|
||||
|
||||
## Stage 2: Stop Populating Backstory in BattalionView
|
||||
|
||||
**Goal:** Stop sending backstory text ID to the client.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionViewFilter.scala`**
|
||||
- In `filteredBattalionViewBelongingToPlayer()`:
|
||||
- Set `backstoryTextId = ""` instead of `battalion.backstoryTextId`
|
||||
- In `limitedBattalionView()`:
|
||||
- Same change
|
||||
|
||||
### Verification
|
||||
- [ ] BattalionView messages have empty backstoryTextId
|
||||
- [ ] Client handles empty backstoryTextId gracefully (Stage 1 should have removed the display)
|
||||
|
||||
---
|
||||
|
||||
## Stage 3: Remove backstory_text_id from BattalionView Proto
|
||||
|
||||
**Goal:** Clean up the proto schema.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`views/battalion_view.proto`**
|
||||
- Remove field: `string backstory_text_id = 7;`
|
||||
- Mark field 7 as reserved to prevent reuse
|
||||
|
||||
**`Assets/Eagle/HeroesAndBattalionsPanelController.cs`**
|
||||
- Remove any remaining references to `BackstoryTextId`
|
||||
|
||||
**`BattalionViewFilter.scala`**
|
||||
- Remove `backstoryTextId` from view construction
|
||||
|
||||
### Verification
|
||||
- [ ] Proto compiles
|
||||
- [ ] C# client compiles without BackstoryTextId references
|
||||
- [ ] Full test suite passes
|
||||
|
||||
---
|
||||
|
||||
## Stage 4: Replace Backstory Usage in BattalionDescriptions
|
||||
|
||||
**Goal:** When other LLM prompts need battalion context, use initial backstory + structured history instead of the latest LLM-generated prose.
|
||||
|
||||
### Current Usage in `BattalionDescriptions.scala`
|
||||
|
||||
```scala
|
||||
def backstoryText(): TextGenerationResult =
|
||||
battalion.backstoryVersions.lastOption
|
||||
.map(v => clientTextStore.getText(v.textId))
|
||||
.getOrElse(TextGenerationSuccess(""))
|
||||
|
||||
def fullDescription(): String =
|
||||
s"${battalion.name} is $description. $backstoryText"
|
||||
```
|
||||
|
||||
### New Approach
|
||||
|
||||
Create `historyDescription()` that:
|
||||
1. Gets initial backstory (first version's text, if available)
|
||||
2. Appends structured event history using the same `textForEvent()` logic from `BattalionBackstoryUpdatePromptGenerator`
|
||||
|
||||
```scala
|
||||
def historyDescription(): String = {
|
||||
val initialBackstory = battalion.backstoryVersions.headOption
|
||||
.map(v => clientTextStore.getText(v.textId))
|
||||
.collect { case TextGenerationSuccess(text) => text }
|
||||
.getOrElse("")
|
||||
|
||||
val eventHistory = battalion.backstoryEvents
|
||||
.map(textForEvent) // Reuse existing event-to-text conversion
|
||||
.mkString(" ")
|
||||
|
||||
if eventHistory.isEmpty then initialBackstory
|
||||
else s"$initialBackstory $eventHistory"
|
||||
}
|
||||
```
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionDescriptions.scala`**
|
||||
- Add `historyDescription()` method
|
||||
- Update `fullDescription()` to use `historyDescription()` instead of `backstoryText()`
|
||||
- Keep `textForEvent()` logic (move from `BattalionBackstoryUpdatePromptGenerator` if needed)
|
||||
|
||||
**Move/refactor `textForEvent()`**
|
||||
- Currently in `BattalionBackstoryUpdatePromptGenerator.scala` (lines 78-262)
|
||||
- Move to `BattalionDescriptions.scala` or a shared utility
|
||||
- This converts events to human-readable text without LLM
|
||||
|
||||
### Verification
|
||||
- [ ] Other LLM prompts that use `BattalionDescriptions` still get meaningful context
|
||||
- [ ] Chronicle updates include battalion history
|
||||
- [ ] Hero backstory updates include relevant battalion history
|
||||
|
||||
---
|
||||
|
||||
## Stage 5: Stop Generating Battalion Backstory Updates
|
||||
|
||||
**Goal:** Remove the LLM request generation entirely.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionBackstoryUpdateActionGenerator.scala`**
|
||||
- Return empty Vector instead of generating actions
|
||||
- Or delete the file entirely
|
||||
|
||||
**`BattalionBackstoryUpdateAction.scala`**
|
||||
- Delete or mark as deprecated
|
||||
|
||||
**`BattalionBackstoryUpdatePromptGenerator.scala`**
|
||||
- Delete or keep only `textForEvent()` if moved
|
||||
|
||||
**`LlmResolver.scala`**
|
||||
- Remove case for `BattalionBackstoryUpdateRequest`
|
||||
- Optionally throw error if somehow called
|
||||
|
||||
**`EndPlayerCommandsPhaseAction.scala`** (and other callers)
|
||||
- Find where `BattalionBackstoryUpdateActionGenerator` is called
|
||||
- Remove those calls
|
||||
|
||||
### Files to Search for Callers
|
||||
```bash
|
||||
grep -r "BattalionBackstoryUpdateAction" src/
|
||||
```
|
||||
|
||||
### Verification
|
||||
- [ ] No `BattalionBackstoryUpdateRequest` LLM requests generated
|
||||
- [ ] Battalions still accumulate events (for history description)
|
||||
- [ ] Full test suite passes
|
||||
- [ ] Game runs without errors
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Keep or Remove Initial Backstory Generation?
|
||||
The initial backstory (`BattalionInitialBackstoryRequest`) is generated once per battalion. Options:
|
||||
1. **Keep it** - Low volume, provides flavor text for history
|
||||
2. **Remove it** - Use a template-based approach instead
|
||||
|
||||
Recommendation: Keep for now, evaluate later based on volume.
|
||||
|
||||
### Event History Cleanup
|
||||
Events currently accumulate forever on battalions. Consider:
|
||||
- Capping event history length
|
||||
- Summarizing old events
|
||||
- Only keeping "significant" events
|
||||
|
||||
### Affected Tests
|
||||
Search for tests that verify backstory update behavior:
|
||||
```bash
|
||||
bazel query 'tests(//src/test/scala/...) intersect rdeps(//src/test/scala/..., //src/main/scala/net/eagle0/eagle/library/actions/impl/action:battalion_backstory_update_action)'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of LLM Request Reduction
|
||||
|
||||
| Request Type | Current Volume | After Change |
|
||||
|--------------|---------------|--------------|
|
||||
| BattalionInitialBackstoryRequest | 1 per battalion | 1 per battalion (unchanged) |
|
||||
| BattalionBackstoryUpdateRequest | ~N per battalion per game | **0** |
|
||||
|
||||
For a typical game with ~50 battalions and ~20 rounds, this could eliminate **hundreds** of LLM requests.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Province Event 3D Effects
|
||||
|
||||
Ideas for replacing 2D shader-based province event effects with 3D particle systems.
|
||||
|
||||
## Current State
|
||||
|
||||
| Event | Effect Type | Notes |
|
||||
|-------|-------------|-------|
|
||||
| Festival | 3D particles | Complete |
|
||||
| Epidemic | 3D particles | Complete (rising skulls + green haze) |
|
||||
| Blizzard | 2D shader | Not visible on unowned (white) provinces |
|
||||
| Drought | 2D shader | |
|
||||
| Flood | 2D shader | |
|
||||
| Beasts | None | Tricky - can be any animal or human |
|
||||
|
||||
## Proposed 3D Effects
|
||||
|
||||
### Blizzard
|
||||
|
||||
- Falling snowflakes with slight blue tint (ensures visibility on white backgrounds)
|
||||
- Diagonal wind-blown particles to convey harsh weather
|
||||
- Swirling vortex effect near ground level
|
||||
- Frosty sparkle/glitter particles for magical winter feel
|
||||
|
||||
### Drought
|
||||
|
||||
- Rising dust/sand particles (tan/brown coloring)
|
||||
- Small dust devils (spinning particle columns)
|
||||
- Shimmering golden particles rising to suggest heat distortion
|
||||
- Dried leaves/debris blowing across the province
|
||||
|
||||
### Flood
|
||||
|
||||
- Rain falling (blue-tinted streaks or droplets)
|
||||
- Water splashes/ripples rising from ground
|
||||
- Mist/spray particles hovering low
|
||||
- Floating debris particles (leaves, sticks)
|
||||
|
||||
### Beasts
|
||||
|
||||
This is challenging since "beasts" can represent any dangerous creature. Universal approaches:
|
||||
|
||||
- **Circling vultures/crows overhead** - Predators attract scavengers; works for any beast type and is visually distinctive
|
||||
- Dust clouds suggesting movement at province edges
|
||||
- Glowing eyes appearing/disappearing in shadows
|
||||
- Stylized claw/scratch mark effects
|
||||
- Paw prints materializing on the ground
|
||||
- Red "danger" aura pulsing from ground level
|
||||
|
||||
The vultures/crows approach is recommended as the most universal solution.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Controllers have been created for all 4 event types:
|
||||
- `ProvinceBlizzardController.cs` - ready, needs prefab
|
||||
- `ProvinceDroughtController.cs` - ready, needs prefab
|
||||
- `ProvinceFloodController.cs` - ready, needs prefab
|
||||
- `ProvinceBeastsController.cs` - ready, needs prefab
|
||||
|
||||
### Prefabs Needed
|
||||
|
||||
Create these prefabs in `Assets/Eagle/Effects/`:
|
||||
|
||||
1. **BlizzardEffect.prefab**
|
||||
- Particle System with falling snowflakes
|
||||
- Blue-tinted particles for visibility on white
|
||||
- Diagonal velocity for wind effect
|
||||
- RectTransform for UI positioning
|
||||
|
||||
2. **DroughtEffect.prefab**
|
||||
- Particle System with rising dust particles
|
||||
- Tan/brown coloring
|
||||
- Upward velocity
|
||||
- Optional spinning sub-emitters for dust devils
|
||||
|
||||
3. **FloodEffect.prefab**
|
||||
- Particle System with falling rain streaks
|
||||
- Blue tint
|
||||
- Fast downward velocity
|
||||
- Optional splash sub-emitter
|
||||
|
||||
4. **BeastsEffect.prefab**
|
||||
- Particle System with bird silhouettes
|
||||
- Circular orbit motion around center
|
||||
- Black/dark particles
|
||||
- Could use a crow/vulture sprite texture
|
||||
|
||||
### Scene Setup
|
||||
|
||||
Wire up in `Gameplay.unity`:
|
||||
1. Add controller components to the Map GameObject
|
||||
2. Assign `mapContainer`, `centroidsJson` (shared with other controllers)
|
||||
3. Assign the effect prefab for each controller
|
||||
@@ -60,7 +60,6 @@
|
||||
"org.scalamock:scalamock_3": -610483078,
|
||||
"org.slf4j:slf4j-api": 1609745898,
|
||||
"org.slf4j:slf4j-simple": 1319506696,
|
||||
"org.xerial:sqlite-jdbc": -1707012205,
|
||||
"repositories": -1949687017,
|
||||
"software.amazon.awssdk:aws-core": 1764800397,
|
||||
"software.amazon.awssdk:http-client-spi": 1697477859,
|
||||
@@ -174,7 +173,6 @@
|
||||
"org.scalamock:scalamock_3": -1778657085,
|
||||
"org.slf4j:slf4j-api": -771685699,
|
||||
"org.slf4j:slf4j-simple": -776509812,
|
||||
"org.xerial:sqlite-jdbc": 1311600081,
|
||||
"software.amazon.awssdk:annotations": -324257201,
|
||||
"software.amazon.awssdk:apache-client": 2066024214,
|
||||
"software.amazon.awssdk:arns": -1240703529,
|
||||
@@ -818,12 +816,6 @@
|
||||
},
|
||||
"version": "2.0.16"
|
||||
},
|
||||
"org.xerial:sqlite-jdbc": {
|
||||
"shasums": {
|
||||
"jar": "6dc7464e3803648d3ff18a7359bab6adf079fcd8495b18991f6f5edcb8ac6e3b"
|
||||
},
|
||||
"version": "3.46.1.0"
|
||||
},
|
||||
"software.amazon.awssdk:annotations": {
|
||||
"shasums": {
|
||||
"jar": "ed03bb4ff78900307dc96bacdec70ddf80c17df3eb6761219573025649f08ff3"
|
||||
@@ -1360,9 +1352,6 @@
|
||||
"org.slf4j:slf4j-simple": [
|
||||
"org.slf4j:slf4j-api"
|
||||
],
|
||||
"org.xerial:sqlite-jdbc": [
|
||||
"org.slf4j:slf4j-api"
|
||||
],
|
||||
"software.amazon.awssdk:apache-client": [
|
||||
"commons-codec:commons-codec",
|
||||
"org.apache.httpcomponents:httpclient",
|
||||
@@ -2415,16 +2404,6 @@
|
||||
"org.slf4j:slf4j-simple": [
|
||||
"org.slf4j.simple"
|
||||
],
|
||||
"org.xerial:sqlite-jdbc": [
|
||||
"org.sqlite",
|
||||
"org.sqlite.core",
|
||||
"org.sqlite.date",
|
||||
"org.sqlite.javax",
|
||||
"org.sqlite.jdbc3",
|
||||
"org.sqlite.jdbc4",
|
||||
"org.sqlite.nativeimage",
|
||||
"org.sqlite.util"
|
||||
],
|
||||
"software.amazon.awssdk:annotations": [
|
||||
"software.amazon.awssdk.annotations"
|
||||
],
|
||||
@@ -2835,7 +2814,6 @@
|
||||
"org.scalamock:scalamock_3",
|
||||
"org.slf4j:slf4j-api",
|
||||
"org.slf4j:slf4j-simple",
|
||||
"org.xerial:sqlite-jdbc",
|
||||
"software.amazon.awssdk:annotations",
|
||||
"software.amazon.awssdk:apache-client",
|
||||
"software.amazon.awssdk:arns",
|
||||
@@ -2912,11 +2890,6 @@
|
||||
"org.slf4j.simple.SimpleServiceProvider"
|
||||
]
|
||||
},
|
||||
"org.xerial:sqlite-jdbc": {
|
||||
"java.sql.Driver": [
|
||||
"org.sqlite.JDBC"
|
||||
]
|
||||
},
|
||||
"software.amazon.awssdk:apache-client": {
|
||||
"software.amazon.awssdk.http.SdkHttpService": [
|
||||
"software.amazon.awssdk.http.apache.ApacheSdkHttpService"
|
||||
|
||||
+4
-64
@@ -2,67 +2,7 @@
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
UNITY_ROOT="$REPO_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
OUTPUT_DIR="$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
|
||||
# All C# protobuf generation targets
|
||||
TARGETS=(
|
||||
"//src/main/protobuf/net/eagle0/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:storage_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:api_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:views_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_csharp_grpc_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:util_csharp_proto_srcs"
|
||||
)
|
||||
|
||||
# Subdirectory for each target (avoids filename collisions across packages)
|
||||
SUBDIRS=(
|
||||
"net/eagle0/common"
|
||||
"net/eagle0/shardok/storage"
|
||||
"net/eagle0/shardok/common"
|
||||
"net/eagle0/shardok/api"
|
||||
"net/eagle0/eagle/common"
|
||||
"net/eagle0/eagle/views"
|
||||
"net/eagle0/eagle/api"
|
||||
"net/eagle0/eagle/api"
|
||||
"net/eagle0/eagle/api/command/util"
|
||||
)
|
||||
|
||||
/bin/echo "Building C# protobuf sources..."
|
||||
bazel build "${TARGETS[@]}"
|
||||
|
||||
/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]}"
|
||||
subdir="${SUBDIRS[$i]}"
|
||||
|
||||
# Convert target label to bazel-bin path
|
||||
# //src/main/protobuf/net/eagle0/common:common_csharp_proto_srcs
|
||||
# -> bazel-bin/src/main/protobuf/net/eagle0/common/common_csharp_proto_srcs
|
||||
target_path="${target#//}"
|
||||
pkg="${target_path%%:*}"
|
||||
name="${target_path##*:}"
|
||||
bin_dir="$REPO_ROOT/bazel-bin/$pkg/$name"
|
||||
|
||||
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"
|
||||
/bin/echo "build protos"
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
dotnet build src/main/csharp/net/eagle0/clients/unity/eagle0/protos/protos.csproj \
|
||||
-o src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
|
||||
@@ -22,31 +22,11 @@ NC='\033[0m' # No Color
|
||||
MODE="${1:-check}"
|
||||
EXIT_CODE=0
|
||||
|
||||
# Cache for expensive bazel query results
|
||||
LIBRARY_DEPS_CACHE=""
|
||||
MAIN_DEPS_CACHE=""
|
||||
|
||||
# Get deps of library/ (cached)
|
||||
get_library_deps() {
|
||||
if [ -z "$LIBRARY_DEPS_CACHE" ]; then
|
||||
LIBRARY_DEPS_CACHE=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...)' 2>/dev/null || true)
|
||||
fi
|
||||
echo "$LIBRARY_DEPS_CACHE"
|
||||
}
|
||||
|
||||
# Get deps of src/main (cached)
|
||||
get_main_deps() {
|
||||
if [ -z "$MAIN_DEPS_CACHE" ]; then
|
||||
MAIN_DEPS_CACHE=$(bazel query 'deps(//src/main/...)' 2>/dev/null || true)
|
||||
fi
|
||||
echo "$MAIN_DEPS_CACHE"
|
||||
}
|
||||
|
||||
# Rule 1: src/main should not depend on src/test
|
||||
check_main_depends_on_test() {
|
||||
echo -e "${YELLOW}Checking: src/main should not depend on src/test...${NC}"
|
||||
|
||||
violations=$(get_main_deps | grep "^//src/test/" || true)
|
||||
violations=$(bazel query 'deps(//src/main/...) intersect //src/test/...' 2>/dev/null || true)
|
||||
|
||||
if [ -n "$violations" ]; then
|
||||
echo -e "${RED}VIOLATION: src/main depends on src/test:${NC}"
|
||||
@@ -63,7 +43,7 @@ check_main_depends_on_test() {
|
||||
check_library_depends_on_scala_proto() {
|
||||
echo -e "${YELLOW}Checking: library/ should not depend on Scala proto types...${NC}"
|
||||
|
||||
violations=$(get_library_deps | grep "^//src/main/protobuf/.*_scala_proto$" || true)
|
||||
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
|
||||
if [ -z "$violations" ]; then
|
||||
count=0
|
||||
else
|
||||
@@ -85,7 +65,7 @@ check_library_depends_on_scala_proto() {
|
||||
check_library_depends_on_proto_converters() {
|
||||
echo -e "${YELLOW}Checking: library/ should not depend on proto_converters...${NC}"
|
||||
|
||||
violations=$(get_library_deps | grep "^//src/main/scala/net/eagle0/eagle/model/proto_converters/" || true)
|
||||
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/scala/net/eagle0/eagle/model/proto_converters/...' 2>/dev/null | grep "^//" || true)
|
||||
|
||||
if [ -n "$violations" ]; then
|
||||
count=$(echo "$violations" | wc -l | tr -d ' ')
|
||||
@@ -105,8 +85,7 @@ check_library_depends_on_proto_converters() {
|
||||
count_proto_deps() {
|
||||
echo -e "${YELLOW}=== Proto dependency counts ===${NC}"
|
||||
|
||||
library_deps=$(get_library_deps)
|
||||
scala_proto_results=$(echo "$library_deps" | grep "^//src/main/protobuf/.*_scala_proto$" || true)
|
||||
scala_proto_results=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
|
||||
if [ -z "$scala_proto_results" ]; then
|
||||
scala_proto_count=0
|
||||
else
|
||||
@@ -115,7 +94,7 @@ count_proto_deps() {
|
||||
echo "library/ Scala proto deps: $scala_proto_count"
|
||||
|
||||
# C++/Go proto deps are expected (map generation tools)
|
||||
all_proto_count=$(echo "$library_deps" | grep -c "^//src/main/protobuf/" || echo "0")
|
||||
all_proto_count=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo "library/ all proto deps (includes C++/Go build tools): $all_proto_count"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1DHEsiv4cY4gE6AX3sVH82K__mpBD1aznIYCQwQxA_F0/export?gid=0&format=tsv" | tr -d '\r' > /tmp/names.tsv
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_checker -- /tmp/names.tsv > /dev/null
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_checker -- /tmp/names.tsv > src/main/resources/net/eagle0/names.tsv
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_json_maker -- /tmp/names.tsv > src/main/resources/net/eagle0/names.json
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1NhvG73HKyVE36yGpkV2oJiSIXoNqQOYTr5ArLnucYL0/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/shardok/battalionTypes.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1pNWiyxIks2wJ1v7jRLFD24zrKHG2AfhC-nkWmQKQGN4/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/eagle/heroes.tsv
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restart the active Eagle instance.
|
||||
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
|
||||
#
|
||||
# This is useful when you need Eagle to reload game state from persistence
|
||||
# (e.g., after a rewind) without doing a full blue-green deployment.
|
||||
#
|
||||
# Usage:
|
||||
# eagle-restart # Restart active instance, wait for healthy
|
||||
# eagle-restart --no-wait # Restart without waiting for health check
|
||||
#
|
||||
# To create an alias, add to ~/.bashrc:
|
||||
# alias eagle-restart='/opt/eagle0/scripts/eagle-restart.sh'
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/eagle0}"
|
||||
ACTIVE_FILE="${APP_DIR}/.active-instance"
|
||||
HEALTH_TIMEOUT=120 # seconds
|
||||
|
||||
# Read active instance from file, with fallback
|
||||
if [ -f "$ACTIVE_FILE" ]; then
|
||||
ACTIVE=$(cat "$ACTIVE_FILE")
|
||||
else
|
||||
# Fallback: check which container is actually running
|
||||
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-blue"
|
||||
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-green"
|
||||
else
|
||||
ACTIVE=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ACTIVE" ]; then
|
||||
echo "Error: No active Eagle instance found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NO_WAIT=false
|
||||
if [ "${1:-}" = "--no-wait" ]; then
|
||||
NO_WAIT=true
|
||||
fi
|
||||
|
||||
echo "Restarting ${ACTIVE}..."
|
||||
docker restart "$ACTIVE"
|
||||
|
||||
if [ "$NO_WAIT" = true ]; then
|
||||
echo "Restart initiated. Not waiting for health check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Waiting for ${ACTIVE} to become healthy (timeout: ${HEALTH_TIMEOUT}s)..."
|
||||
elapsed=0
|
||||
while [ $elapsed -lt $HEALTH_TIMEOUT ]; do
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' "$ACTIVE" 2>/dev/null || echo "unknown")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
echo "${ACTIVE} is healthy after ${elapsed}s."
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
# Print progress every 10 seconds
|
||||
if [ $((elapsed % 10)) -eq 0 ]; then
|
||||
echo " ...${elapsed}s (status: ${health})"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Warning: ${ACTIVE} did not become healthy within ${HEALTH_TIMEOUT}s (status: ${health})" >&2
|
||||
echo "Check logs with: eagle-logs" >&2
|
||||
exit 1
|
||||
@@ -95,16 +95,9 @@ auto ConvertUnit(
|
||||
shardokUnit.mutate_stun_rounds_remaining(0);
|
||||
|
||||
for (const PlayerId pid : allPlayerIds) {
|
||||
int8_t initialKnowledge = 0;
|
||||
for (const auto visibleToPid : unit.visible_to_player_ids()) {
|
||||
if (visibleToPid == pid) {
|
||||
initialKnowledge = 100;
|
||||
break;
|
||||
}
|
||||
}
|
||||
shardokUnit.mutable_opponent_knowledge()->Mutate(
|
||||
static_cast<flatbuffers::uoffset_t>(pid),
|
||||
initialKnowledge);
|
||||
0);
|
||||
}
|
||||
|
||||
shardokUnit.mutate_has_moved_in_zoc(false);
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AIConfig.hpp"
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
@@ -174,117 +171,8 @@ 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,8 +93,7 @@ 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:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT: break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT: break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
|
||||
throw ShardokInternalErrorException("Unknown unit status");
|
||||
|
||||
@@ -56,18 +56,13 @@ public:
|
||||
commandFactories(CommandFactoriesList(settings).GetFactories()),
|
||||
playerSetupCommandFactory(settings) {}
|
||||
|
||||
[[nodiscard]] auto GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const
|
||||
[[nodiscard]] auto GetPlayerSetupCommands(const GameStateW &gameState, PlayerId playerId) const
|
||||
-> CommandListSPtr override;
|
||||
|
||||
[[nodiscard]] auto GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId player,
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const
|
||||
-> CommandListSPtr override;
|
||||
bool includeFollowUps) const -> CommandListSPtr override;
|
||||
};
|
||||
|
||||
auto AvailableCommandsFactory::MakeAvailableCommandsFactory(const SettingsGetter &settings)
|
||||
@@ -77,14 +72,9 @@ auto AvailableCommandsFactory::MakeAvailableCommandsFactory(const SettingsGetter
|
||||
|
||||
auto AvailableCommandsFactoryImpl::GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> CommandListSPtr {
|
||||
const PlayerId playerId) const -> CommandListSPtr {
|
||||
CommandList previewCommands{};
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(
|
||||
previewCommands,
|
||||
playerId,
|
||||
gameState,
|
||||
reinforcementPositions);
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(previewCommands, playerId, gameState);
|
||||
|
||||
return make_shared<CommandList>(previewCommands);
|
||||
}
|
||||
@@ -178,14 +168,12 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId,
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> CommandListSPtr {
|
||||
bool includeFollowUps) const -> CommandListSPtr {
|
||||
CommandList commands{};
|
||||
|
||||
vector<AdjacentTile> adjacentTiles;
|
||||
|
||||
playerSetupCommandFactory
|
||||
.AddAvailablePlayerSetupCommands(commands, playerId, gameState, reinforcementPositions);
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(commands, playerId, gameState);
|
||||
if (!commands.empty()) return make_shared<CommandList>(commands);
|
||||
|
||||
for (const Unit *unit : *gameState->units()) {
|
||||
|
||||
@@ -9,12 +9,8 @@
|
||||
#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"
|
||||
|
||||
@@ -33,16 +29,12 @@ public:
|
||||
|
||||
[[nodiscard]] virtual auto GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions = std::nullopt) const
|
||||
-> CommandListSPtr = 0;
|
||||
PlayerId playerId) const -> CommandListSPtr = 0;
|
||||
|
||||
[[nodiscard]] virtual auto GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId player,
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions = std::nullopt) const
|
||||
-> CommandListSPtr = 0;
|
||||
bool includeFollowUps) const -> CommandListSPtr = 0;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:update_game_status_action",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:update_opponent_knowledge_action",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:game_state_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/tutorial:tutorial_battle_controller",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:game_state_validator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:action_result_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
|
||||
@@ -58,7 +57,6 @@ 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",
|
||||
|
||||
@@ -286,49 +286,21 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
|
||||
true /* atEndOfRound */)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
// Check for tutorial events at end of round
|
||||
if (!GameIsOver() && tutorialController_.IsEnabled()) {
|
||||
auto tutorialEventResults = tutorialController_.CheckAndExecuteEvents(
|
||||
gameState,
|
||||
settingsGetter,
|
||||
randomGenerator);
|
||||
ApplyAndAddActionResults(
|
||||
UpdateOpponentKnowledgeAction(settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
if (!tutorialEventResults.actionResults.empty()) {
|
||||
ApplyAndAddActionResults(tutorialEventResults.actionResults);
|
||||
ApplyAndAddActionResults(
|
||||
NewRoundAction(gameState, settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
// Update game status after tutorial events (e.g., attackers fled, defender wins)
|
||||
ApplyAndAddActionResults(UpdateGameStatusAction(
|
||||
GetCurrentGameState(),
|
||||
criticalTileCoords,
|
||||
settingsGetter,
|
||||
true /* atEndOfRound */)
|
||||
.Execute(gameState, randomGenerator));
|
||||
ApplyAndAddActionResults(
|
||||
StartPlayerTurnAction(gameState, UNCONTROLLED_PLAYER_ID, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
// If tutorial events ended the game, don't start new round
|
||||
if (GameIsOver()) { return; }
|
||||
ApplyAndAddActionResults(PerformUndeadCommandsAction(gameState, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResumeAfterReinforcementPlacement(randomGenerator);
|
||||
ApplyAndAddActionResults(StartPlayerTurnAction(gameState, 0, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
} else {
|
||||
const auto updateAction = UpdateGameStatusAction(
|
||||
@@ -344,27 +316,6 @@ 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,
|
||||
@@ -373,15 +324,8 @@ void ShardokEngine::PostPlacementCommands(
|
||||
throw ShardokClientErrorException("Posting for a player who's not the current player ID!");
|
||||
}
|
||||
|
||||
const auto reinforcementPositions =
|
||||
pendingReinforcementPlacement_.has_value()
|
||||
? std::optional<std::vector<Coords>>(
|
||||
pendingReinforcementPlacement_->startingPositions)
|
||||
: std::nullopt;
|
||||
const auto placementCommands = availableCommandsFactory->GetPlayerSetupCommands(
|
||||
gameState,
|
||||
player,
|
||||
reinforcementPositions);
|
||||
const auto placementCommands =
|
||||
availableCommandsFactory->GetPlayerSetupCommands(gameState, player);
|
||||
|
||||
// first make sure they're all valid and there are no duplicates
|
||||
for (size_t i = 0; i < placementInfos.size(); i++) {
|
||||
@@ -421,19 +365,6 @@ 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,
|
||||
@@ -632,16 +563,10 @@ 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,
|
||||
reinforcementPositions);
|
||||
/* includeFollowUps=*/includeFollowUps);
|
||||
}
|
||||
|
||||
void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Units &from) {
|
||||
@@ -673,7 +598,6 @@ 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;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/tutorial/TutorialBattleController.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
|
||||
@@ -37,7 +36,6 @@ using net::eagle0::shardok::api::UnitView;
|
||||
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
|
||||
using net::eagle0::shardok::storage::ShardokActionWithResultingState;
|
||||
using HexMapProto = net::eagle0::shardok::common::HexMap;
|
||||
using TutorialBattleConfigProto = net::eagle0::common::TutorialBattleConfig;
|
||||
|
||||
class ShardokEngine {
|
||||
private:
|
||||
@@ -54,9 +52,6 @@ private:
|
||||
|
||||
const CoordsSet criticalTileCoords;
|
||||
|
||||
TutorialBattleController tutorialController_;
|
||||
std::optional<ReinforcementPlacementInfo> pendingReinforcementPlacement_;
|
||||
|
||||
mutable CommandListSPtr cachedAvailableCommands{};
|
||||
|
||||
void ApplyAndAddActionResult(const ActionResult &result);
|
||||
@@ -77,7 +72,6 @@ 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;
|
||||
|
||||
@@ -203,17 +197,6 @@ public:
|
||||
|
||||
[[nodiscard]] auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }
|
||||
|
||||
/// Configure the tutorial battle controller (call after construction if needed)
|
||||
void SetTutorialBattleConfig(const TutorialBattleConfigProto &config) {
|
||||
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,7 +90,6 @@ 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",
|
||||
|
||||
+3
-22
@@ -66,30 +66,11 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
|
||||
auto PlayerSetupCommandFactory::AddAvailablePlayerSetupCommands(
|
||||
CommandList &existingCommands,
|
||||
PlayerId playerId,
|
||||
const GameStateW &gameState,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> void {
|
||||
const GameStateW &gameState) const -> void {
|
||||
if (playerId < 0) return;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gameState->status()->state() !=
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP)
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) return;
|
||||
|
||||
const bool isDefender = PlayerIsDefender(gameState, playerId);
|
||||
|
||||
|
||||
+1
-7
@@ -5,12 +5,8 @@
|
||||
#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 {
|
||||
@@ -33,9 +29,7 @@ public:
|
||||
auto AddAvailablePlayerSetupCommands(
|
||||
CommandList& existingCommands,
|
||||
PlayerId playerId,
|
||||
const GameStateW& gameState,
|
||||
const std::optional<std::vector<Coords>>& reinforcementPositions = std::nullopt) const
|
||||
-> void;
|
||||
const GameStateW& gameState) const -> void;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
+1
-1
@@ -331,7 +331,7 @@ void MutatingApplyResult(
|
||||
.size()
|
||||
: changedUnit->battalion().size();
|
||||
|
||||
auto status = changedUnit->status();
|
||||
auto status = net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT;
|
||||
if (IsDestroyed(*changedUnit)) {
|
||||
status =
|
||||
changedUnit->has_attached_hero()
|
||||
|
||||
@@ -34,7 +34,6 @@ 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,7 +18,6 @@ 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,7 +16,6 @@ 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,9 +89,7 @@ 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 ||
|
||||
gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_REINFORCEMENT_PLACEMENT) {
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ 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,10 +176,7 @@ auto SetupInitialGameState(
|
||||
modifiedUnit.mutable_battalion().mutate_base_morale(baseMorale);
|
||||
modifiedUnit.mutable_battalion().mutate_morale(baseMorale);
|
||||
|
||||
if (modifiedUnit.status() !=
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT) {
|
||||
modifiedUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
|
||||
}
|
||||
modifiedUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
|
||||
|
||||
unitsVec.push_back(modifiedUnit);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "tutorial_battle_controller",
|
||||
srcs = ["TutorialBattleController.cpp"],
|
||||
hdrs = ["TutorialBattleController.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
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",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:action_type_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -1,283 +0,0 @@
|
||||
//
|
||||
// TutorialBattleController.cpp
|
||||
// eagle0
|
||||
//
|
||||
// Created for tutorial battle system.
|
||||
// Copyright 2025 none. All rights reserved.
|
||||
//
|
||||
|
||||
#include "TutorialBattleController.hpp"
|
||||
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using UnitStatusFB = net::eagle0::shardok::storage::fb::UnitStatus;
|
||||
using ActionType = net::eagle0::shardok::common::ActionType;
|
||||
using FleeActionProto = net::eagle0::common::FleeAction;
|
||||
using ReinforcementsActionProto = net::eagle0::common::ReinforcementsAction;
|
||||
using UnitKilledTriggerProto = net::eagle0::common::UnitKilledTrigger;
|
||||
|
||||
TutorialBattleController::TutorialBattleController()
|
||||
: enabled_(false),
|
||||
config_(),
|
||||
firedEventIds_() {}
|
||||
|
||||
TutorialBattleController::TutorialBattleController(const TutorialBattleConfigProto& protoConfig)
|
||||
: enabled_(protoConfig.enabled()),
|
||||
config_(protoConfig),
|
||||
firedEventIds_() {}
|
||||
|
||||
auto TutorialBattleController::CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) -> TutorialEventResults {
|
||||
if (!enabled_) { return {}; }
|
||||
|
||||
TutorialEventResults combined;
|
||||
|
||||
// Check events in config order
|
||||
for (const auto& event : config_.events()) {
|
||||
// Skip already-fired events
|
||||
if (firedEventIds_.contains(event.event_id())) { continue; }
|
||||
|
||||
// Check if trigger condition is met
|
||||
if (EvaluateTrigger(event.trigger(), state)) {
|
||||
// Mark as fired before executing (to prevent re-triggering)
|
||||
firedEventIds_.insert(event.event_id());
|
||||
|
||||
// Execute the action
|
||||
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 combined;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::EvaluateTrigger(
|
||||
const TutorialTriggerProto& trigger,
|
||||
const GameStateW& state) const -> bool {
|
||||
switch (trigger.trigger_type_case()) {
|
||||
case TutorialTriggerProto::kAfterRound: {
|
||||
const int currentRound = state->current_round();
|
||||
return currentRound >= trigger.after_round().round_number();
|
||||
}
|
||||
|
||||
case TutorialTriggerProto::kUnitsLost: {
|
||||
const auto& unitsLost = trigger.units_lost();
|
||||
const int lostCount = CountLostUnits(state, unitsLost.player_id());
|
||||
return lostCount >= unitsLost.threshold();
|
||||
}
|
||||
|
||||
case TutorialTriggerProto::kDamageTaken: {
|
||||
const auto& damageTaken = trigger.damage_taken();
|
||||
const int totalDamage = GetTotalDamageTaken(state, damageTaken.player_id());
|
||||
return totalDamage >= damageTaken.threshold();
|
||||
}
|
||||
|
||||
case TutorialTriggerProto::kUnitKilled: {
|
||||
return IsUnitKilled(state, trigger.unit_killed());
|
||||
}
|
||||
|
||||
case TutorialTriggerProto::TRIGGER_TYPE_NOT_SET:
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto TutorialBattleController::ExecuteAction(
|
||||
const TutorialActionProto& action,
|
||||
const GameStateW& state,
|
||||
[[maybe_unused]] const SettingsGetter& settings,
|
||||
[[maybe_unused]] const std::shared_ptr<RandomGenerator>& randomGenerator) const
|
||||
-> TutorialEventResults {
|
||||
switch (action.action_type_case()) {
|
||||
case TutorialActionProto::kFlee:
|
||||
return {.actionResults = ExecuteFleeAction(action.flee(), state),
|
||||
.reinforcementPlacement = std::nullopt};
|
||||
|
||||
case TutorialActionProto::kReinforcements:
|
||||
return ExecuteReinforcementsAction(action.reinforcements(), state);
|
||||
|
||||
case TutorialActionProto::ACTION_TYPE_NOT_SET:
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
auto TutorialBattleController::CountLostUnits(const GameStateW& state, const PlayerId playerId)
|
||||
const -> int {
|
||||
int count = 0;
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->player_id() != playerId) { continue; }
|
||||
|
||||
// Count units that are no longer in battle (destroyed, captured, fled, retreated)
|
||||
const auto status = unit->status();
|
||||
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_PENDING_REINFORCEMENT) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::GetTotalDamageTaken(const GameStateW& state, const PlayerId playerId)
|
||||
const -> int {
|
||||
int totalDamage = 0;
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->player_id() != playerId) { continue; }
|
||||
|
||||
// Calculate damage as difference between initial and current size
|
||||
// Initial sizes are stored in config since flatbuffers don't track starting sizes
|
||||
int initialSize = 0;
|
||||
for (const auto& initial : config_.initial_sizes()) {
|
||||
if (initial.unit_id() == unit->unit_id()) {
|
||||
initialSize = initial.size();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (initialSize == 0) { continue; }
|
||||
|
||||
const int currentSize = unit->battalion().size();
|
||||
if (initialSize > currentSize) { totalDamage += (initialSize - currentSize); }
|
||||
}
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::IsUnitKilled(
|
||||
const GameStateW& state,
|
||||
const UnitKilledTriggerProto& trigger) const -> bool {
|
||||
for (const auto* unit : *state->units()) {
|
||||
// Check if this unit matches the identifier
|
||||
bool matches = false;
|
||||
|
||||
switch (trigger.unit_identifier_case()) {
|
||||
case UnitKilledTriggerProto::kUnitId:
|
||||
matches = (unit->unit_id() == trigger.unit_id());
|
||||
break;
|
||||
|
||||
case UnitKilledTriggerProto::kEagleHeroId:
|
||||
if (unit->has_attached_hero()) {
|
||||
matches = (unit->attached_hero().eagle_hero_id() == trigger.eagle_hero_id());
|
||||
}
|
||||
break;
|
||||
|
||||
case UnitKilledTriggerProto::UNIT_IDENTIFIER_NOT_SET:
|
||||
default: continue;
|
||||
}
|
||||
|
||||
if (!matches) { continue; }
|
||||
|
||||
// Check if the unit is "killed" (destroyed/captured status)
|
||||
const auto status = unit->status();
|
||||
if (status == UnitStatusFB::UnitStatus_DESTROYED_SUMMONED_UNIT ||
|
||||
status == UnitStatusFB::UnitStatus_CAPTURED_UNIT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::ExecuteFleeAction(
|
||||
const FleeActionProto& fleeAction,
|
||||
const GameStateW& state) const -> std::vector<ActionResult> {
|
||||
std::vector<ActionResult> results;
|
||||
const PlayerId fleeingPlayerId = fleeAction.player_id();
|
||||
|
||||
// First, create a "tutorial enemy fled" notification
|
||||
ActionResult fleeNotification{};
|
||||
fleeNotification.set_type(ActionType::TUTORIAL_ENEMY_FLED);
|
||||
results.push_back(fleeNotification);
|
||||
|
||||
// Determine which units should flee
|
||||
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 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{};
|
||||
fleeResult.set_type(ActionType::FLED);
|
||||
fleeResult.mutable_player()->set_value(fleeingPlayerId);
|
||||
fleeResult.mutable_actor()->set_value(unit->unit_id());
|
||||
|
||||
auto* resolvedUnit = fleeResult.add_resolved_units();
|
||||
AddUnitToResolved(*resolvedUnit, *unit);
|
||||
resolvedUnit->set_status(net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT);
|
||||
|
||||
results.push_back(fleeResult);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::ExecuteReinforcementsAction(
|
||||
const ReinforcementsActionProto& reinforcementsAction,
|
||||
const GameStateW& state) const -> TutorialEventResults {
|
||||
TutorialEventResults eventResults;
|
||||
|
||||
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); }
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1,116 +0,0 @@
|
||||
//
|
||||
// TutorialBattleController.hpp
|
||||
// eagle0
|
||||
//
|
||||
// Created for tutorial battle system.
|
||||
// Copyright 2025 none. All rights reserved.
|
||||
//
|
||||
|
||||
#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"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using ActionResult = net::eagle0::shardok::storage::ActionResult;
|
||||
using TutorialBattleConfigProto = net::eagle0::common::TutorialBattleConfig;
|
||||
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).
|
||||
class TutorialBattleController {
|
||||
public:
|
||||
/// Creates a disabled controller.
|
||||
TutorialBattleController();
|
||||
|
||||
/// Creates a controller from proto configuration.
|
||||
explicit TutorialBattleController(const TutorialBattleConfigProto& protoConfig);
|
||||
|
||||
/// Returns true if the controller is enabled for this battle.
|
||||
[[nodiscard]] auto IsEnabled() const -> bool { return enabled_; }
|
||||
|
||||
/// Check all events at end of round, return actions for triggered events.
|
||||
/// Events fire in config order - first triggered event executes, then next.
|
||||
/// Each event fires at most once.
|
||||
[[nodiscard]] auto CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
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>& {
|
||||
return firedEventIds_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool enabled_ = false;
|
||||
TutorialBattleConfigProto config_;
|
||||
std::set<std::string> firedEventIds_; // Track which events have fired
|
||||
|
||||
/// Evaluates whether a trigger condition is met.
|
||||
[[nodiscard]] auto EvaluateTrigger(const TutorialTriggerProto& trigger, const GameStateW& state)
|
||||
const -> bool;
|
||||
|
||||
/// Executes an action and returns the resulting action results.
|
||||
[[nodiscard]] auto ExecuteAction(
|
||||
const TutorialActionProto& action,
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) const -> TutorialEventResults;
|
||||
|
||||
// Helper methods for trigger evaluation
|
||||
|
||||
/// Counts units lost (destroyed, captured, fled, retreated) for a player.
|
||||
[[nodiscard]] auto CountLostUnits(const GameStateW& state, PlayerId playerId) const -> int;
|
||||
|
||||
/// Gets total damage taken (cumulative size reduction) for a player.
|
||||
[[nodiscard]] auto GetTotalDamageTaken(const GameStateW& state, PlayerId playerId) const -> int;
|
||||
|
||||
/// Checks if a specific unit has been killed.
|
||||
[[nodiscard]] auto IsUnitKilled(
|
||||
const GameStateW& state,
|
||||
const net::eagle0::common::UnitKilledTrigger& trigger) const -> bool;
|
||||
|
||||
// Helper methods for action execution
|
||||
|
||||
/// Executes a flee action for specified units.
|
||||
[[nodiscard]] auto ExecuteFleeAction(
|
||||
const net::eagle0::common::FleeAction& fleeAction,
|
||||
const GameStateW& state) const -> std::vector<ActionResult>;
|
||||
|
||||
/// Executes a reinforcements action, placing units directly at available starting positions.
|
||||
[[nodiscard]] auto ExecuteReinforcementsAction(
|
||||
const net::eagle0::common::ReinforcementsAction& reinforcementsAction,
|
||||
const GameStateW& state) const -> TutorialEventResults;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif /* TutorialBattleController_hpp */
|
||||
@@ -331,8 +331,7 @@ 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_PENDING_REINFORCEMENT)
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
|
||||
placedUnits[unit->unit_id()] = unit;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,9 +73,6 @@ 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,8 +49,7 @@ 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:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT: break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT: break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
|
||||
throw ShardokInternalErrorException("Unknown unit status");
|
||||
@@ -101,7 +100,6 @@ 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,7 +12,6 @@ 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",
|
||||
@@ -20,7 +19,6 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings_loader",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:battalion_type_registrar",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/common:victory_condition_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -193,7 +193,6 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
|
||||
mapName,
|
||||
customMapBytes,
|
||||
month,
|
||||
request.tutorial_battle_config(),
|
||||
request.SerializeAsString());
|
||||
}
|
||||
|
||||
@@ -222,14 +221,20 @@ auto EagleInterfaceImpl::PostCommand(
|
||||
request->game_setup_info().known_result_count(),
|
||||
response);
|
||||
} catch (ShardokClientErrorException &e) {
|
||||
std::cerr << "Bad request from client: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INVALID_ARGUMENT, e.what());
|
||||
std::cerr << "Bad request from client!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
} catch (ShardokInternalErrorException &e) {
|
||||
std::cerr << "Internal error: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
std::cerr << "ShardokInternalErrorException thrown!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << "Unknown exception: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
std::cerr << "Unknown exception in client->PostCommand" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
@@ -268,14 +273,20 @@ auto EagleInterfaceImpl::PostPlacementCommands(
|
||||
request->game_setup_info().known_result_count(),
|
||||
response);
|
||||
} catch (ShardokClientErrorException &e) {
|
||||
std::cerr << "Bad request from client: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INVALID_ARGUMENT, e.what());
|
||||
std::cerr << "Bad request from client!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
} catch (ShardokInternalErrorException &e) {
|
||||
std::cerr << "Internal error: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
std::cerr << "ShardokInternalErrorException thrown!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << "Unknown exception: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
std::cerr << "Unknown exception in client->PostCommand" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#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"
|
||||
@@ -31,7 +30,6 @@ auto SetUpController(
|
||||
int month,
|
||||
const vector<Unit> &units,
|
||||
const vector<PlayerInfoProto> &playerSetupInfos,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> shared_ptr<ShardokGameController>;
|
||||
|
||||
ShardokGamesManager::ShardokGamesManager(const std::vector<std::string> &extraSettings) {
|
||||
@@ -79,7 +77,6 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
|
||||
const string &mapName,
|
||||
const string &customMapBytes,
|
||||
const int month,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> GameId {
|
||||
// Check for existing game with this ID and early-exit
|
||||
std::shared_lock<std::shared_mutex> lk(runningControllersLock);
|
||||
@@ -114,7 +111,6 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
|
||||
month,
|
||||
units,
|
||||
playerInfos,
|
||||
tutorialConfig,
|
||||
serializedRequest);
|
||||
|
||||
return LockedCreateGame(controller, playerSetupInfos, serializedRequest);
|
||||
@@ -141,7 +137,6 @@ auto SetUpController(
|
||||
const int month,
|
||||
const vector<Unit> &units,
|
||||
const vector<PlayerInfoProto> &playerInfos,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> shared_ptr<ShardokGameController> {
|
||||
auto unplacedUnits = std::vector<Unit>();
|
||||
|
||||
@@ -149,55 +144,9 @@ auto SetUpController(
|
||||
for (auto up : units) {
|
||||
up.mutate_unit_id(nextUnitId++);
|
||||
up.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
|
||||
|
||||
// Apply initial visibility settings from tutorial config
|
||||
if (tutorialConfig.enabled() && up.has_attached_hero()) {
|
||||
const int heroId = up.attached_hero().eagle_hero_id();
|
||||
for (const auto &visibility : tutorialConfig.initial_visibilities()) {
|
||||
if (visibility.eagle_hero_id() == heroId) {
|
||||
for (const int playerId : visibility.visible_to_player_ids()) {
|
||||
if (playerId >= 0 && playerId < 10) {
|
||||
up.mutable_opponent_knowledge()->Mutate(
|
||||
static_cast<flatbuffers::uoffset_t>(playerId),
|
||||
100);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
@@ -206,8 +155,6 @@ auto SetUpController(
|
||||
gameId,
|
||||
playerInfos);
|
||||
|
||||
engine->SetTutorialBattleConfig(tutorialConfig);
|
||||
|
||||
return std::make_shared<ShardokGameController>(std::move(engine), mapName, serializedRequest);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.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/common/game_status.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/victory_condition.pb.h"
|
||||
@@ -26,7 +25,6 @@ class GameStatePersister;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
|
||||
using TutorialBattleConfigProto = net::eagle0::common::TutorialBattleConfig;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using VictoryConditionProto = net::eagle0::shardok::common::VictoryCondition;
|
||||
@@ -103,7 +101,6 @@ public:
|
||||
const string &mapName,
|
||||
const string &customMapBytes,
|
||||
int month,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> GameId;
|
||||
|
||||
auto UnlockedCreateFromGameStateBytes(
|
||||
|
||||
@@ -29,7 +29,6 @@ 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: 0e8900f37a8b7b1c88049de57e371339
|
||||
Hash: 00000000000000000000000000000000
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 1
|
||||
m_CatalogRequestsTimeout: 0
|
||||
|
||||
@@ -272,8 +272,9 @@ namespace Auth {
|
||||
|
||||
/// <summary>
|
||||
/// Start OAuth login with specified provider.
|
||||
/// In editor: Opens browser and polls for completion.
|
||||
/// In standalone: Opens browser, quits app, deep link relaunches with token.
|
||||
/// Opens system browser for user consent, then quits the app.
|
||||
/// The browser's "Return to Eagle" deep link will relaunch the app
|
||||
/// with the auth session, which HandleSessionTransferAsync will process.
|
||||
/// </summary>
|
||||
public async Task<CheckOAuthStatusResponse> LoginAsync(OAuthProvider provider) {
|
||||
EnsureConfigured();
|
||||
@@ -287,13 +288,6 @@ namespace Auth {
|
||||
Debug.Log($"[OAuthManager] Opening browser for OAuth: {authUrl}");
|
||||
Application.OpenURL(authUrl);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// In editor: poll for OAuth completion since deep links don't work
|
||||
Debug.Log("[OAuthManager] Editor mode - polling for OAuth completion");
|
||||
var response = await _authClient.PollForOAuthCompletionAsync(state);
|
||||
HandleOAuthResponse(response);
|
||||
return response;
|
||||
#else
|
||||
// Quit the app - the browser's "Return to Eagle" deep link will
|
||||
// relaunch a fresh instance with the auth session code.
|
||||
// This avoids the dual-instance problem where both the original
|
||||
@@ -305,7 +299,6 @@ namespace Auth {
|
||||
// to satisfy the compiler. The new instance will handle auth
|
||||
// via OnDeepLinkActivated -> HandleSessionTransferAsync.
|
||||
return null;
|
||||
#endif
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[OAuthManager] Login failed: {ex.Message}");
|
||||
OnLoginFailed?.Invoke(ex.Message);
|
||||
@@ -313,28 +306,6 @@ namespace Auth {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle OAuth response from polling (editor mode).
|
||||
/// </summary>
|
||||
private void HandleOAuthResponse(CheckOAuthStatusResponse response) {
|
||||
var providerName = response.User.Provider.ToString().ToLowerInvariant();
|
||||
TokenStorage.StoreTokens(
|
||||
response.AccessToken,
|
||||
response.RefreshToken,
|
||||
response.ExpiresAt,
|
||||
response.User.UserId,
|
||||
response.User.DisplayName,
|
||||
providerName);
|
||||
|
||||
if (response.IsNewUser || string.IsNullOrEmpty(response.User.DisplayName)) {
|
||||
Debug.Log("[OAuthManager] New user needs to set display name");
|
||||
OnNewUserNeedsDisplayName?.Invoke(true);
|
||||
} else {
|
||||
Debug.Log($"[OAuthManager] OAuth successful for {response.User.DisplayName}");
|
||||
OnLoginSuccess?.Invoke(response.User);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set display name for new user after OAuth.
|
||||
/// </summary>
|
||||
|
||||
+89
-126
@@ -108,8 +108,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public TextMeshProUGUI connectionStatusText;
|
||||
|
||||
[Header("Connection Panel Environment")]
|
||||
[Tooltip("Connect to QA button in connection panel (editor-only)")]
|
||||
public Button connectToQAButton;
|
||||
[Tooltip("Environment dropdown in connection panel (fallback if lobby unreachable)")]
|
||||
public TMP_Dropdown connectionEnvironmentDropdown;
|
||||
|
||||
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 Button lobbyConnectToQAButton;
|
||||
public TMP_Dropdown lobbyEnvironmentDropdown;
|
||||
public TextMeshProUGUI lobbyUserText;
|
||||
public TextMeshProUGUI lobbyStatusText;
|
||||
|
||||
@@ -131,8 +131,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public GameObject availableGamesListArea;
|
||||
public GameObject availableGamesListItemPrefab;
|
||||
public GameObject waitingGamesListItemPrefab;
|
||||
public CreateGameItem createGameItem;
|
||||
public Button tutorialButton;
|
||||
public GameObject createGameListItemPrefab;
|
||||
|
||||
public DropGameConfirmationPanel dropGameConfirmationPanel;
|
||||
|
||||
@@ -143,6 +142,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
List<GameInfo> fetchedRunningGames;
|
||||
List<AvailableLeader> fetchedNewGameLeaders;
|
||||
int fetchedMaxPlayerCount;
|
||||
private EagleConnection eagleConnection;
|
||||
private PersistentClientConnection _persistentClientConnection;
|
||||
private HttpClient _httpClient;
|
||||
@@ -150,7 +150,6 @@ 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
|
||||
@@ -169,8 +168,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
/// Returns the connected environment name ("prod." or "qa.") or null if not connected.
|
||||
/// </summary>
|
||||
public string ConnectedEnvironmentName =>
|
||||
_isLocalQAConnection ? "QA (local)"
|
||||
: _connectedEnvironmentIndex >= 0 &&
|
||||
_connectedEnvironmentIndex >= 0 &&
|
||||
_connectedEnvironmentIndex < EnvironmentDisplayNames.Count
|
||||
? EnvironmentDisplayNames[_connectedEnvironmentIndex]
|
||||
: null;
|
||||
@@ -220,8 +218,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
// Initialize OAuth UI
|
||||
SetupOAuthUI();
|
||||
|
||||
// Initialize connection panel QA button (editor-only)
|
||||
SetupConnectToQAButton();
|
||||
// Initialize connection panel environment dropdown
|
||||
SetupConnectionEnvironmentDropdown();
|
||||
|
||||
// Initialize Lobby UI (logout button, etc.)
|
||||
SetupLobbyUI();
|
||||
@@ -237,17 +235,27 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
TryRestoreSession();
|
||||
}
|
||||
|
||||
private void SetupConnectToQAButton() {
|
||||
if (connectToQAButton == null) return;
|
||||
private void SetupConnectionEnvironmentDropdown() {
|
||||
if (connectionEnvironmentDropdown == null) return;
|
||||
|
||||
// Only show QA button in Unity Editor; always use prod in builds
|
||||
// Only show environment dropdown in Unity Editor; always use prod in builds
|
||||
if (!Application.isEditor) {
|
||||
connectToQAButton.gameObject.SetActive(false);
|
||||
connectionEnvironmentDropdown.gameObject.SetActive(false);
|
||||
PlayerPrefs.SetInt(EnvironmentKey, 0); // Force prod
|
||||
return;
|
||||
}
|
||||
|
||||
connectToQAButton.onClick.AddListener(OnConnectToQAClicked);
|
||||
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]}");
|
||||
}
|
||||
|
||||
private void SetupOAuthUI() {
|
||||
@@ -287,6 +295,15 @@ 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();
|
||||
|
||||
@@ -394,52 +411,42 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void SetupLobbyUI() {
|
||||
customBattleButton.onClick.AddListener(CustomBattle);
|
||||
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
|
||||
tutorialButton.gameObject.SetActive(
|
||||
TokenStorage.DisplayName == "nolen" || TokenStorage.DisplayName == "tarn");
|
||||
tutorialButton.onClick.AddListener(CreateTutorialGame);
|
||||
if (customBattleButton != null) { customBattleButton.onClick.AddListener(CustomBattle); }
|
||||
if (cancelCustomBattleButton != null) {
|
||||
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
|
||||
}
|
||||
|
||||
// Set up QA button (only in Unity Editor)
|
||||
if (lobbyConnectToQAButton != null) {
|
||||
// Set up environment dropdown (only in Unity Editor)
|
||||
if (lobbyEnvironmentDropdown != null) {
|
||||
if (!Application.isEditor) {
|
||||
lobbyConnectToQAButton.gameObject.SetActive(false);
|
||||
lobbyEnvironmentDropdown.gameObject.SetActive(false);
|
||||
} else {
|
||||
lobbyConnectToQAButton.onClick.AddListener(OnLobbyConnectToQAClicked);
|
||||
lobbyEnvironmentDropdown.ClearOptions();
|
||||
lobbyEnvironmentDropdown.AddOptions(EnvironmentDisplayNames);
|
||||
lobbyEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLobbyStatusDisplays() {
|
||||
lobbyUserText.text = _isLocalQAConnection ? "editor" : TokenStorage.DisplayName ?? "";
|
||||
}
|
||||
|
||||
private void OnConnectToQAClicked() {
|
||||
Debug.Log("[ConnectionHandler] Connect to QA clicked");
|
||||
_isLocalQAConnection = true;
|
||||
|
||||
SetLobbyActive(true);
|
||||
connectionCanvas.enabled = true;
|
||||
|
||||
_createLocalQAConnection();
|
||||
|
||||
if (_persistentClientConnection == null) {
|
||||
Debug.LogError("[ConnectionHandler] QA connection creation failed");
|
||||
return;
|
||||
// Update environment dropdown selection
|
||||
if (lobbyEnvironmentDropdown != null && _connectedEnvironmentIndex >= 0) {
|
||||
// Temporarily remove listener to avoid triggering reconnect
|
||||
lobbyEnvironmentDropdown.onValueChanged.RemoveListener(OnLobbyEnvironmentChanged);
|
||||
lobbyEnvironmentDropdown.value = _connectedEnvironmentIndex;
|
||||
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
|
||||
}
|
||||
|
||||
try {
|
||||
RequestMaps();
|
||||
StartListeningForLobbyUpdates();
|
||||
} catch (Exception e) {
|
||||
MainQueue.Q.Enqueue(() => { errorHandler.Add(e); });
|
||||
}
|
||||
// Show current user from OAuth
|
||||
if (lobbyUserText != null) { lobbyUserText.text = TokenStorage.DisplayName ?? ""; }
|
||||
}
|
||||
|
||||
private void OnLobbyConnectToQAClicked() {
|
||||
if (_isLocalQAConnection) return;
|
||||
private void OnLobbyEnvironmentChanged(int newEnvironmentIndex) {
|
||||
if (newEnvironmentIndex == _connectedEnvironmentIndex) return;
|
||||
|
||||
Debug.Log("[ConnectionHandler] Lobby: switching to QA (local)");
|
||||
Debug.Log(
|
||||
$"[ConnectionHandler] Switching environment from {ConnectedEnvironmentName} to {EnvironmentDisplayNames[newEnvironmentIndex]}");
|
||||
|
||||
// Disconnect from current environment
|
||||
_persistentClientConnection?.Dispose();
|
||||
@@ -450,54 +457,23 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
eagleConnection = null;
|
||||
_httpClient = null;
|
||||
|
||||
_isLocalQAConnection = true;
|
||||
// Update environment index and reconnect
|
||||
_connectedEnvironmentIndex = newEnvironmentIndex;
|
||||
PlayerPrefs.SetInt(EnvironmentKey, newEnvironmentIndex);
|
||||
|
||||
// Clear current game lists while reconnecting
|
||||
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
|
||||
if (lobbyStatusText != null) { lobbyStatusText.text = "Connecting to QA..."; }
|
||||
// Show loading indicator
|
||||
if (lobbyStatusText != null) { lobbyStatusText.text = "Switching environment..."; }
|
||||
|
||||
_createLocalQAConnection();
|
||||
// Reconnect to new environment
|
||||
_createConnection();
|
||||
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");
|
||||
|
||||
@@ -515,7 +491,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
_httpClient = null;
|
||||
_cancellationTokenSource = null;
|
||||
_connectedEnvironmentIndex = -1;
|
||||
_isLocalQAConnection = false;
|
||||
|
||||
// Return to connection screen
|
||||
gameSelectionPanel.SetActive(false);
|
||||
@@ -694,7 +669,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
eagleConnection = null;
|
||||
|
||||
_connectedEnvironmentIndex = -1;
|
||||
_isLocalQAConnection = false;
|
||||
}
|
||||
|
||||
public void OnResolutionChanged(int dropdownIndex) {
|
||||
@@ -713,14 +687,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
fetchedNewGameLeaders = new List<AvailableLeader>();
|
||||
fetchedNewGameLeaders.AddRange(lobbyResponse.NewGameOptions.AvailableLeaders);
|
||||
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Skip lobby UI updates if we're already in an Eagle game.
|
||||
// The lobby response may arrive after the game has started (e.g., when creating a new
|
||||
// game), and the connection canvas is inactive, causing GetComponentInParent to fail.
|
||||
// This check must be inside the queue callback since the state may change between
|
||||
// when the response arrives and when this callback executes.
|
||||
if (eagleCanvas.gameObject.activeSelf) return;
|
||||
fetchedMaxPlayerCount = lobbyResponse.NewGameOptions.MaxSupportedPlayers;
|
||||
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
ResourceFetcher.headshotFetcher.Prefetch(
|
||||
fetchedNewGameLeaders.Select(a => a.ImagePath));
|
||||
|
||||
@@ -733,13 +702,12 @@ 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
|
||||
.OrderBy(g => g.CreatedTimestampMillis)
|
||||
.ThenBy(g => g.GameId)) {
|
||||
foreach (var runningGame in lobbyResponse.RunningGames) {
|
||||
var listItem = Instantiate(
|
||||
runningGamesListItemPrefab,
|
||||
runningGamesListArea.transform,
|
||||
false);
|
||||
true);
|
||||
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
|
||||
var runningGameItem = listItem.GetComponent<RunningGameItem>();
|
||||
runningGameItem.SetRunningGame(
|
||||
runningGame.GameId,
|
||||
@@ -756,7 +724,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
var listItem = Instantiate(
|
||||
waitingGamesListItemPrefab,
|
||||
availableGamesListArea.transform,
|
||||
false);
|
||||
true);
|
||||
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
|
||||
listItem.GetComponent<WaitingGameItem>().SetWaitingGame(
|
||||
waitingGame.GameId,
|
||||
string.Format(
|
||||
@@ -766,14 +735,21 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
waitingGame.SelectedLeader);
|
||||
}
|
||||
|
||||
createGameItem.SetCreationOptions(fetchedNewGameLeaders);
|
||||
createGameItem.CreateCallback = this.CreateGame;
|
||||
var createGameItem =
|
||||
Instantiate(createGameListItemPrefab, availableGamesListArea.transform, true);
|
||||
// var singlePlayerListItem = Instantiate(availableGamesListItemPrefab);
|
||||
createGameItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
|
||||
createGameItem.GetComponent<CreateGameItem>().SetCreationOptions(
|
||||
fetchedNewGameLeaders,
|
||||
fetchedMaxPlayerCount);
|
||||
createGameItem.GetComponent<CreateGameItem>().CreateCallback = this.CreateGame;
|
||||
|
||||
foreach (var availableGame in lobbyResponse.AvailableGames) {
|
||||
var listItem = Instantiate(
|
||||
availableGamesListItemPrefab,
|
||||
availableGamesListArea.transform,
|
||||
false);
|
||||
true);
|
||||
listItem.transform.localScale = new Vector3 { x = 1.0f, y = 1.0f, z = 1.0f };
|
||||
var availableGameItem = listItem.GetComponent<AvailableGameItem>();
|
||||
availableGameItem.SetAvailableNewGame(
|
||||
availableGame.GameId,
|
||||
@@ -900,7 +876,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
};
|
||||
_persistentClientConnection.Connect();
|
||||
|
||||
PersistentClientConnection.Current = _persistentClientConnection;
|
||||
errorHandler.PersistentClientConnection = _persistentClientConnection;
|
||||
ResourceFetcher.SetUpConnection(_httpClient);
|
||||
}
|
||||
@@ -974,22 +949,9 @@ 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: "",
|
||||
humanPlayerCount: 1,
|
||||
totalPlayerCount: 2,
|
||||
isTutorial: true);
|
||||
}
|
||||
|
||||
public void QuitButtonClicked() { Application.Quit(); }
|
||||
|
||||
public void HandleJoinGameResponse(JoinGameResponse response) {
|
||||
@@ -1007,9 +969,14 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public void HandleCreateGameResponse(CreateGameResponse response) {
|
||||
try {
|
||||
switch (response.Result) {
|
||||
case JoinGameResult.SuccessJoinGameResult: SelectEagleGame(response.GameId); break;
|
||||
case JoinGameResult.SuccessJoinGameResult:
|
||||
SelectEagleGame(response.GameId);
|
||||
// Automatically start the game if it's one player?
|
||||
break;
|
||||
|
||||
default: Debug.LogError($"Game creation failed: {response.Result}"); break;
|
||||
default:
|
||||
// FIXME: display something
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) { Console.WriteLine(e.ToString()); }
|
||||
}
|
||||
@@ -1021,11 +988,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
} catch (Exception e) { Console.WriteLine(e.ToString()); }
|
||||
}
|
||||
|
||||
private async Task<bool> _internalCreateGame(
|
||||
string desiredLeaderTextId,
|
||||
int humanPlayerCount,
|
||||
int totalPlayerCount,
|
||||
bool isTutorial = false) {
|
||||
private async Task<bool>
|
||||
_internalCreateGame(string desiredLeaderTextId, int humanPlayerCount, int totalPlayerCount) {
|
||||
try {
|
||||
return await _persistentClientConnection.SendUpdateStreamRequestAsync(
|
||||
new UpdateStreamRequest {
|
||||
@@ -1033,8 +997,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
new CreateGameRequest {
|
||||
DesiredLeaderTextId = desiredLeaderTextId,
|
||||
TotalPlayerCount = totalPlayerCount,
|
||||
HumanPlayerCount = humanPlayerCount,
|
||||
IsTutorial = isTutorial
|
||||
HumanPlayerCount = humanPlayerCount
|
||||
}
|
||||
});
|
||||
} catch (WebException e) {
|
||||
|
||||
+3653
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5315bdd018d9e420dbbf55a0508a7f14
|
||||
guid: 81b03eaddf4de4a4aa385617e4881511
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
+23
-8
@@ -11,10 +11,9 @@ using UnityEngine.UI;
|
||||
using Random = System.Random;
|
||||
|
||||
public class CreateGameItem : MonoBehaviour {
|
||||
private const int TotalPlayerCount = 7;
|
||||
|
||||
public HeroDropdownController heroDropdownController;
|
||||
public TMP_Dropdown totalHumansDropdown;
|
||||
public TMP_Dropdown totalPlayersDropdown;
|
||||
public Button createButton;
|
||||
public TextMeshProUGUI buttonText;
|
||||
|
||||
@@ -33,20 +32,30 @@ public class CreateGameItem : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
|
||||
// Use this for initialization
|
||||
void Start() {}
|
||||
|
||||
public void LeaderDropdownSelected(TMP_Dropdown dropdown) {}
|
||||
|
||||
public void TotalPlayersDropdownSelected(TMP_Dropdown dropdown) { SetHumanPlayersDropdown(); }
|
||||
|
||||
public void CreateClicked() {
|
||||
CreateCallback(SelectedLeaderTextId, totalHumansDropdown.value + 1, TotalPlayerCount);
|
||||
CreateCallback(
|
||||
SelectedLeaderTextId,
|
||||
totalHumansDropdown.value + 1,
|
||||
totalPlayersDropdown.value + 1);
|
||||
}
|
||||
|
||||
public void SetHumanPlayersDropdown() {
|
||||
var currentValue = totalHumansDropdown.value;
|
||||
totalHumansDropdown.ClearOptions();
|
||||
totalHumansDropdown.AddOptions(Enumerable.Range(1, TotalPlayerCount)
|
||||
.Select(i => i == 1 ? "1 player" : $"{i} players")
|
||||
totalHumansDropdown.AddOptions(Enumerable.Range(1, totalPlayersDropdown.value + 1)
|
||||
.Select(i => i.ToString())
|
||||
.ToList());
|
||||
totalHumansDropdown.value = Math.Min(TotalPlayerCount - 1, currentValue);
|
||||
totalHumansDropdown.value = Math.Min(totalPlayersDropdown.value, currentValue);
|
||||
}
|
||||
|
||||
public void SetCreationOptions(List<AvailableLeader> leaders) {
|
||||
public void SetCreationOptions(List<AvailableLeader> leaders, int maxPlayerCount) {
|
||||
this.availableLeaders = leaders;
|
||||
|
||||
buttonText.text = "CREATE";
|
||||
@@ -68,10 +77,16 @@ public class CreateGameItem : MonoBehaviour {
|
||||
heroDropdownController.PreResolvedNames =
|
||||
leaders.Where(l => !string.IsNullOrEmpty(l.Name))
|
||||
.ToDictionary(l => l.NameTextId, l => l.Name);
|
||||
heroDropdownController.FallbackText = "Random Warlord";
|
||||
heroDropdownController.FallbackText = "Random";
|
||||
heroDropdownController.AvailableHeroes = heroOptions;
|
||||
heroDropdownController.HeadshotImage.gameObject.SetActive(false);
|
||||
|
||||
totalPlayersDropdown.ClearOptions();
|
||||
totalPlayersDropdown.AddOptions(
|
||||
Enumerable.Range(1, maxPlayerCount).Select(i => i.ToString()).ToList());
|
||||
// Default to 7 total players (index 6 = 7 players, so 1 human + 6 AI)
|
||||
totalPlayersDropdown.value = Math.Min(6, maxPlayerCount - 1);
|
||||
|
||||
SetHumanPlayersDropdown();
|
||||
// Default to 1 human (index 0)
|
||||
totalHumansDropdown.value = 0;
|
||||
|
||||
+4
-98
@@ -631,99 +631,6 @@ MonoBehaviour:
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &5462510071559864326
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7404235247014569559}
|
||||
- component: {fileID: 7896497708396401296}
|
||||
- component: {fileID: 6993077635068771622}
|
||||
- component: {fileID: 1252979331277989500}
|
||||
m_Layer: 5
|
||||
m_Name: Profession Image
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7404235247014569559
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5462510071559864326}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 909271087739711220}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7896497708396401296
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5462510071559864326}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &6993077635068771622
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5462510071559864326}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Texture: {fileID: 2800000, guid: d19d0fed4745dc14fa93e86993f9f2d6, type: 3}
|
||||
m_UVRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1
|
||||
height: 1
|
||||
--- !u!114 &1252979331277989500
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5462510071559864326}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 32
|
||||
m_PreferredHeight: 32
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &5643565463360785033
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -759,7 +666,6 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 2008335870574158510}
|
||||
- {fileID: 7404235247014569559}
|
||||
- {fileID: 6674238761151365000}
|
||||
- {fileID: 5589171621630283209}
|
||||
- {fileID: 1348356180262758599}
|
||||
@@ -768,8 +674,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: -600}
|
||||
m_SizeDelta: {x: 0, y: -1080}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1107237753715810996
|
||||
CanvasRenderer:
|
||||
@@ -827,7 +733,7 @@ MonoBehaviour:
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 3
|
||||
m_Spacing: 5
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 1
|
||||
@@ -851,7 +757,7 @@ MonoBehaviour:
|
||||
gameIdField: {fileID: 8058295588038032232}
|
||||
leaderField: {fileID: 3311450659768657245}
|
||||
lastPlayedField: {fileID: 5482253986454480250}
|
||||
professionImage: {fileID: 6993077635068771622}
|
||||
goButton: {fileID: 0}
|
||||
dropButton: {fileID: 4081637582368108930}
|
||||
--- !u!114 &2208043217657811503
|
||||
MonoBehaviour:
|
||||
|
||||
+1
-6
@@ -10,7 +10,7 @@ public class RunningGameItem : MonoBehaviour {
|
||||
public TextMeshProUGUI gameIdField;
|
||||
public TextMeshProUGUI leaderField;
|
||||
public TextMeshProUGUI lastPlayedField;
|
||||
public RawImage professionImage;
|
||||
public Button goButton;
|
||||
public Button dropButton;
|
||||
|
||||
public Action<long> GoCallback;
|
||||
@@ -43,11 +43,6 @@ public class RunningGameItem : MonoBehaviour {
|
||||
leaderName,
|
||||
DisplayNames.ProfessionNames[leader.Profession]);
|
||||
|
||||
// Set profession image
|
||||
var textures = gameObject.GetComponentInParent<EagleCommonTextures>();
|
||||
professionImage.texture = textures.Profession(leader.Profession);
|
||||
professionImage.color = professionImage.texture != null ? Color.white : Color.clear;
|
||||
|
||||
// Display last played time in user's local timezone
|
||||
if (lastPlayedField != null) {
|
||||
if (lastPlayedTimestampMillis > 0) {
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates a beasts/wildlife particle effect with circling birds (vultures/crows).
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class BeastsEffect : MonoBehaviour {
|
||||
[Header("Bird Settings")]
|
||||
public Texture2D birdTexture;
|
||||
public Material birdMaterial; // Assign in prefab to ensure shader is included in build
|
||||
public float birdCount = 5f;
|
||||
public float birdLifetime = 8f;
|
||||
public float birdSize = 12f;
|
||||
public float circleRadius = 35f;
|
||||
public float circleSpeed = 1f;
|
||||
public Color birdColor = Color.white;
|
||||
|
||||
[Header("Flight Pattern")]
|
||||
public float verticalOscillation = 10f;
|
||||
public float radiusVariation = 10f;
|
||||
|
||||
private ParticleSystem _birdSystem;
|
||||
private Material _materialInstance;
|
||||
|
||||
void Awake() { SetupParticleSystems(); }
|
||||
|
||||
private void SetupParticleSystems() {
|
||||
var birdObj = new GameObject("BirdEffect");
|
||||
birdObj.transform.SetParent(transform, false);
|
||||
_birdSystem = birdObj.AddComponent<ParticleSystem>();
|
||||
SetupBirdSystem();
|
||||
}
|
||||
|
||||
private void SetupBirdSystem() {
|
||||
var main = _birdSystem.main;
|
||||
main.playOnAwake = true;
|
||||
main.loop = true;
|
||||
main.startLifetime = birdLifetime;
|
||||
main.startSpeed = 0f;
|
||||
main.startSize = birdSize;
|
||||
main.startColor = birdColor;
|
||||
main.startRotation3D = true;
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 10;
|
||||
|
||||
var emission = _birdSystem.emission;
|
||||
emission.enabled = true;
|
||||
// Emit in bursts to maintain constant population
|
||||
emission.rateOverTime = 0f;
|
||||
emission.SetBursts(
|
||||
new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, (short)birdCount) });
|
||||
|
||||
// Start on circle perimeter
|
||||
var shape = _birdSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Circle;
|
||||
shape.radius = circleRadius;
|
||||
shape.radiusThickness = 0f; // Only emit on edge
|
||||
shape.rotation = new Vector3(-90f, 0f, 0f);
|
||||
shape.arc = 360f;
|
||||
shape.arcMode = ParticleSystemShapeMultiModeValue.Random;
|
||||
|
||||
// Orbital velocity for circling motion
|
||||
var velocityOverLifetime = _birdSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.space = ParticleSystemSimulationSpace.Local;
|
||||
// All axes must use same curve mode (MinMaxCurve)
|
||||
velocityOverLifetime.orbitalX = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
velocityOverLifetime.orbitalY =
|
||||
new ParticleSystem.MinMaxCurve(circleSpeed, circleSpeed);
|
||||
velocityOverLifetime.orbitalZ = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
velocityOverLifetime.radial = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
|
||||
// Add vertical bobbing with noise
|
||||
var noise = _birdSystem.noise;
|
||||
noise.enabled = true;
|
||||
noise.separateAxes = true;
|
||||
noise.strengthX = radiusVariation;
|
||||
noise.strengthY = verticalOscillation;
|
||||
noise.strengthZ = 0f;
|
||||
noise.frequency = 0.3f;
|
||||
noise.scrollSpeed = 0.5f;
|
||||
noise.damping = false;
|
||||
noise.octaveCount = 1;
|
||||
|
||||
// Rotate to face direction of travel
|
||||
var rotationOverLifetime = _birdSystem.rotationOverLifetime;
|
||||
rotationOverLifetime.enabled = true;
|
||||
rotationOverLifetime.separateAxes = true;
|
||||
// Slight banking as they turn
|
||||
rotationOverLifetime.z = new ParticleSystem.MinMaxCurve(-0.1f, 0.1f);
|
||||
|
||||
// No fade - birds stay visible while circling
|
||||
var colorOverLifetime = _birdSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
var gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(Color.white, 0f),
|
||||
new GradientColorKey(Color.white, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(0f, 0f),
|
||||
new GradientAlphaKey(1f, 0.05f),
|
||||
new GradientAlphaKey(1f, 0.95f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
});
|
||||
colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient);
|
||||
|
||||
var renderer = _birdSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
// Use billboard facing camera
|
||||
renderer.renderMode = ParticleSystemRenderMode.Billboard;
|
||||
renderer.alignment = ParticleSystemRenderSpace.View;
|
||||
|
||||
// Use provided material if available (ensures shader is in build)
|
||||
if (birdMaterial != null) {
|
||||
_materialInstance = new Material(birdMaterial);
|
||||
if (birdTexture != null) { _materialInstance.mainTexture = birdTexture; }
|
||||
renderer.material = _materialInstance;
|
||||
} else {
|
||||
// Fallback for editor testing
|
||||
var particleShader = Shader.Find("Particles/Standard Unlit");
|
||||
if (particleShader != null) {
|
||||
_materialInstance = new Material(particleShader);
|
||||
_materialInstance.SetFloat("_ColorMode", 1);
|
||||
_materialInstance.SetFloat("_Surface", 1);
|
||||
_materialInstance.SetFloat("_Blend", 0);
|
||||
_materialInstance.SetInt(
|
||||
"_SrcBlend",
|
||||
(int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
_materialInstance.SetInt(
|
||||
"_DstBlend",
|
||||
(int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
_materialInstance.EnableKeyword("_ALPHABLEND_ON");
|
||||
_materialInstance.renderQueue = 3000;
|
||||
if (birdTexture != null) { _materialInstance.mainTexture = birdTexture; }
|
||||
renderer.material = _materialInstance;
|
||||
}
|
||||
}
|
||||
renderer.sortingOrder = 102;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_materialInstance != null) { Destroy(_materialInstance); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5ee82ebc029341c4858ef5b7e7cf201
|
||||
@@ -1,220 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates a blizzard particle effect with falling snowflakes.
|
||||
/// Uses province masking shader to clip particles to province boundaries.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class BlizzardEffect : MonoBehaviour {
|
||||
[Header("Snowflake Settings")]
|
||||
public Texture2D snowflakeTexture;
|
||||
public Material particleMaterial; // Assign in prefab to ensure shader is in build
|
||||
public float snowEmitRate = 45f;
|
||||
public float snowFallSpeed = 50f;
|
||||
public float snowLifetime = 4f; // Longer lifetime
|
||||
public float snowSize = 10f; // Slightly larger
|
||||
public float snowDrift = 15f; // More drift
|
||||
public Color snowColor = Color.white;
|
||||
|
||||
[Header("Wind Settings")]
|
||||
public float windStrength = 5f;
|
||||
public float windVariation = 3f;
|
||||
|
||||
[Header("Province Masking")]
|
||||
public Texture2D provinceIdMap;
|
||||
public int targetProvinceId;
|
||||
|
||||
private ParticleSystem _snowSystem;
|
||||
private Material _material;
|
||||
private RectTransform _mapContainer;
|
||||
private Canvas _canvas;
|
||||
private Vector3 _baseShapeScale;
|
||||
private Vector3 _baseShapePosition;
|
||||
private float _baseEmitRate;
|
||||
private float _lastZoom = 1f;
|
||||
|
||||
void Awake() { SetupParticleSystems(); }
|
||||
|
||||
private void SetupParticleSystems() {
|
||||
var snowObj = new GameObject("SnowEffect");
|
||||
snowObj.transform.SetParent(transform, false);
|
||||
_snowSystem = snowObj.AddComponent<ParticleSystem>();
|
||||
SetupSnowSystem();
|
||||
}
|
||||
|
||||
private void SetupSnowSystem() {
|
||||
var main = _snowSystem.main;
|
||||
main.playOnAwake = true;
|
||||
main.loop = true;
|
||||
main.startLifetime = snowLifetime;
|
||||
main.startSpeed = 0f;
|
||||
main.startSize = new ParticleSystem.MinMaxCurve(snowSize * 0.5f, snowSize);
|
||||
main.startColor = snowColor;
|
||||
main.startRotation = new ParticleSystem.MinMaxCurve(0f, Mathf.PI * 2f);
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 900;
|
||||
|
||||
var emission = _snowSystem.emission;
|
||||
emission.enabled = true;
|
||||
emission.rateOverTime = snowEmitRate;
|
||||
_baseEmitRate = snowEmitRate;
|
||||
|
||||
// Emit across full province height - particles spawn at varying Y positions
|
||||
var shape = _snowSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Box;
|
||||
shape.scale = new Vector3(150f, 150f, 1f); // Wide and very tall emission area
|
||||
shape.position = new Vector3(0f, 40f, 0f); // Centered above effect origin
|
||||
|
||||
// Store base values for zoom compensation
|
||||
_baseShapeScale = shape.scale;
|
||||
_baseShapePosition = shape.position;
|
||||
|
||||
// Fall downward with horizontal drift
|
||||
var velocityOverLifetime = _snowSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.space = ParticleSystemSimulationSpace.Local;
|
||||
velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(
|
||||
-snowDrift - windStrength,
|
||||
snowDrift + windStrength);
|
||||
velocityOverLifetime.y = new ParticleSystem.MinMaxCurve(-snowFallSpeed, -snowFallSpeed);
|
||||
velocityOverLifetime.z = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
|
||||
// Gentle rotation as they fall
|
||||
var rotationOverLifetime = _snowSystem.rotationOverLifetime;
|
||||
rotationOverLifetime.enabled = true;
|
||||
rotationOverLifetime.z = new ParticleSystem.MinMaxCurve(-0.5f, 0.5f);
|
||||
|
||||
// Use noise for fluttering motion
|
||||
var noise = _snowSystem.noise;
|
||||
noise.enabled = true;
|
||||
noise.strength = windVariation;
|
||||
noise.frequency = 0.5f;
|
||||
noise.scrollSpeed = 0.2f;
|
||||
noise.damping = false;
|
||||
|
||||
// Fade in and out
|
||||
var colorOverLifetime = _snowSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
var gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(Color.white, 0f),
|
||||
new GradientColorKey(Color.white, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(0f, 0f),
|
||||
new GradientAlphaKey(1f, 0.1f),
|
||||
new GradientAlphaKey(1f, 0.8f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
});
|
||||
colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient);
|
||||
|
||||
var renderer = _snowSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
// Use provided material if available (ensures shader is in build)
|
||||
if (particleMaterial != null) {
|
||||
_material = new Material(particleMaterial);
|
||||
_material.renderQueue = 3000;
|
||||
if (snowflakeTexture != null) { _material.mainTexture = snowflakeTexture; }
|
||||
renderer.material = _material;
|
||||
} else {
|
||||
// Fallback for editor testing
|
||||
string[] shaderNames = {
|
||||
"Eagle/ProvinceParticle",
|
||||
"Universal Render Pipeline/Particles/Unlit",
|
||||
"Particles/Standard Unlit",
|
||||
"Legacy Shaders/Particles/Alpha Blended"
|
||||
};
|
||||
|
||||
Shader shader = null;
|
||||
foreach (var name in shaderNames) {
|
||||
shader = Shader.Find(name);
|
||||
if (shader != null) break;
|
||||
}
|
||||
|
||||
if (shader != null) {
|
||||
_material = new Material(shader);
|
||||
_material.renderQueue = 3000;
|
||||
if (snowflakeTexture != null) { _material.mainTexture = snowflakeTexture; }
|
||||
renderer.material = _material;
|
||||
} else {
|
||||
Debug.LogWarning("BlizzardEffect: No particle shader found");
|
||||
}
|
||||
}
|
||||
renderer.sortingOrder = 100;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetProvinceId(int provinceId) {
|
||||
targetProvinceId = provinceId;
|
||||
if (_material != null) { _material.SetFloat("_TargetProvinceId", provinceId); }
|
||||
}
|
||||
|
||||
public void SetProvinceIdMap(Texture2D idMap) {
|
||||
provinceIdMap = idMap;
|
||||
if (_material != null) { _material.SetTexture("_ProvinceIDMap", idMap); }
|
||||
}
|
||||
|
||||
public void SetMapContainer(RectTransform container) {
|
||||
_mapContainer = container;
|
||||
_canvas = container?.GetComponentInParent<Canvas>();
|
||||
}
|
||||
|
||||
void LateUpdate() {
|
||||
UpdateMapBounds();
|
||||
UpdateZoomCompensation();
|
||||
}
|
||||
|
||||
private void UpdateMapBounds() {
|
||||
if (_mapContainer == null || _material == null) { return; }
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
_mapContainer.GetWorldCorners(corners);
|
||||
|
||||
Camera cam = _canvas?.worldCamera ?? Camera.main;
|
||||
Vector3 screenBL, screenTR;
|
||||
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay) {
|
||||
screenBL = corners[0];
|
||||
screenTR = corners[2];
|
||||
} else {
|
||||
screenBL = cam != null ? cam.WorldToScreenPoint(corners[0]) : corners[0];
|
||||
screenTR = cam != null ? cam.WorldToScreenPoint(corners[2]) : corners[2];
|
||||
}
|
||||
|
||||
var mapBounds = new Vector4(
|
||||
screenBL.x,
|
||||
screenBL.y,
|
||||
screenTR.x - screenBL.x,
|
||||
screenTR.y - screenBL.y);
|
||||
_material.SetVector("_MapBounds", mapBounds);
|
||||
}
|
||||
|
||||
private void UpdateZoomCompensation() {
|
||||
if (_mapContainer == null || _snowSystem == null) { return; }
|
||||
|
||||
float zoom = _mapContainer.localScale.x;
|
||||
if (Mathf.Approximately(zoom, _lastZoom)) { return; }
|
||||
_lastZoom = zoom;
|
||||
|
||||
// Counter-scale particle system to maintain screen-space appearance
|
||||
float inverseZoom = 1f / zoom;
|
||||
_snowSystem.transform.localScale = Vector3.one * inverseZoom;
|
||||
|
||||
// Expand emission area to still cover the province
|
||||
var shape = _snowSystem.shape;
|
||||
shape.scale = _baseShapeScale * zoom;
|
||||
shape.position = _baseShapePosition * zoom;
|
||||
|
||||
// Increase emission rate to maintain screen-space density (area scales as zoom²)
|
||||
var emission = _snowSystem.emission;
|
||||
emission.rateOverTime = _baseEmitRate * zoom * zoom;
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_material != null) { Destroy(_material); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c15980e88e57845ce920e43f29d35fcc
|
||||
+4
-5
@@ -264,16 +264,15 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private int Cost(List<int> armaments) {
|
||||
int totalCost = 0;
|
||||
double totalCost = 0.0;
|
||||
|
||||
for (int i = 0; i < armaments.Count; i++) {
|
||||
var battalion = _battalions[i];
|
||||
var armamentDelta = Math.Max(armaments[i] - battalion.Armament.Value, 0.0);
|
||||
// Ceiling per-battalion to match server calculation
|
||||
totalCost += (int)Math.Ceiling(_costs[i] * armamentDelta);
|
||||
|
||||
totalCost += _costs[i] * Math.Max(armaments[i] - battalion.Armament.Value, 0.0);
|
||||
}
|
||||
|
||||
return totalCost;
|
||||
return (int)Math.Ceiling(Math.Max(totalCost, 0.0));
|
||||
}
|
||||
|
||||
private int Cost() { return Cost(_newArmaments); }
|
||||
|
||||
+13
-92
@@ -15,6 +15,7 @@ namespace eagle {
|
||||
|
||||
public class ImproveCommandSelector : CommandSelector {
|
||||
public HeroDropdownController heroDropdownController;
|
||||
public Toggle lockToggle;
|
||||
|
||||
// Toggle buttons for each improvement type (assign to same ToggleGroup in Unity)
|
||||
public Toggle economyToggle;
|
||||
@@ -23,12 +24,6 @@ namespace eagle {
|
||||
public Toggle devastationToggle;
|
||||
public ToggleGroup improvementToggleGroup;
|
||||
|
||||
// Lock toggles - one per improvement type
|
||||
public Toggle economyLockToggle;
|
||||
public Toggle agricultureLockToggle;
|
||||
public Toggle infrastructureLockToggle;
|
||||
public Toggle devastationLockToggle;
|
||||
|
||||
// Labels showing type name and value
|
||||
public TMP_Text economyLabel;
|
||||
public TMP_Text agricultureLabel;
|
||||
@@ -125,38 +120,6 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
private Toggle GetLockToggleForType(ImprovementType type) {
|
||||
switch (type) {
|
||||
case ImprovementType.Economy: return economyLockToggle;
|
||||
case ImprovementType.Agriculture: return agricultureLockToggle;
|
||||
case ImprovementType.Infrastructure: return infrastructureLockToggle;
|
||||
case ImprovementType.Devastation: return devastationLockToggle;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLockToggleVisibility() {
|
||||
var selectedType = SelectedType;
|
||||
var types = new[] {
|
||||
ImprovementType.Economy,
|
||||
ImprovementType.Agriculture,
|
||||
ImprovementType.Infrastructure,
|
||||
ImprovementType.Devastation
|
||||
};
|
||||
foreach (var type in types) {
|
||||
var lockToggle = GetLockToggleForType(type);
|
||||
if (lockToggle != null) {
|
||||
// Only show lock for the selected type, hide all others
|
||||
lockToggle.gameObject.SetActive(type == selectedType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnImprovementToggleChanged(bool isOn) {
|
||||
// Only respond when a toggle is turned on (ToggleGroup handles turning off others)
|
||||
if (isOn) { UpdateLockToggleVisibility(); }
|
||||
}
|
||||
|
||||
private TMP_Text GetLabelForType(ImprovementType type) {
|
||||
switch (type) {
|
||||
case ImprovementType.Economy: return economyLabel;
|
||||
@@ -242,43 +205,23 @@ namespace eagle {
|
||||
ImprovementType.Devastation,
|
||||
improveCommand.AvailableTypes.Contains(ImprovementType.Devastation));
|
||||
|
||||
// Wire up toggle change listeners for lock visibility updates
|
||||
if (economyToggle != null) {
|
||||
economyToggle.onValueChanged.AddListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (agricultureToggle != null) {
|
||||
agricultureToggle.onValueChanged.AddListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (infrastructureToggle != null) {
|
||||
infrastructureToggle.onValueChanged.AddListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (devastationToggle != null) {
|
||||
devastationToggle.onValueChanged.AddListener(OnImprovementToggleChanged);
|
||||
}
|
||||
|
||||
// Initialize all lock toggles to off
|
||||
if (economyLockToggle != null) economyLockToggle.isOn = false;
|
||||
if (agricultureLockToggle != null) agricultureLockToggle.isOn = false;
|
||||
if (infrastructureLockToggle != null) infrastructureLockToggle.isOn = false;
|
||||
if (devastationLockToggle != null) devastationLockToggle.isOn = false;
|
||||
|
||||
// Determine which type to select and whether to lock it
|
||||
// Determine which type to select
|
||||
ImprovementType selectedType;
|
||||
if (improveCommand.LockedType != ImprovementType.None) {
|
||||
var lockedType = improveCommand.LockedType;
|
||||
if (improveCommand.AvailableTypes.Contains(lockedType)) {
|
||||
selectedType = lockedType;
|
||||
var lockToggle = GetLockToggleForType(lockedType);
|
||||
if (lockToggle != null) lockToggle.isOn = true;
|
||||
lockToggle.isOn = true;
|
||||
} else {
|
||||
selectedType = GetDefaultImprovementType();
|
||||
lockToggle.isOn = false;
|
||||
}
|
||||
} else {
|
||||
selectedType = GetDefaultImprovementType();
|
||||
lockToggle.isOn = false;
|
||||
}
|
||||
|
||||
SelectType(selectedType);
|
||||
UpdateLockToggleVisibility();
|
||||
}
|
||||
|
||||
public override AvailableCommand.SealedValueOneofCase CommandType =>
|
||||
@@ -297,35 +240,13 @@ namespace eagle {
|
||||
heroDropdownController.SelectedHeroId = heroId;
|
||||
}
|
||||
|
||||
public override SelectedCommand Command {
|
||||
get {
|
||||
var selectedType = SelectedType;
|
||||
var lockToggle = GetLockToggleForType(selectedType);
|
||||
return new SelectedCommand {
|
||||
ImproveCommand =
|
||||
new ImproveSelectedCommand {
|
||||
ImprovementType = selectedType,
|
||||
ActingHeroId = heroDropdownController.SelectedHeroId,
|
||||
LockType = lockToggle != null && lockToggle.isOn
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
// Clean up listeners
|
||||
if (economyToggle != null) {
|
||||
economyToggle.onValueChanged.RemoveListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (agricultureToggle != null) {
|
||||
agricultureToggle.onValueChanged.RemoveListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (infrastructureToggle != null) {
|
||||
infrastructureToggle.onValueChanged.RemoveListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (devastationToggle != null) {
|
||||
devastationToggle.onValueChanged.RemoveListener(OnImprovementToggleChanged);
|
||||
}
|
||||
}
|
||||
public override SelectedCommand Command => new SelectedCommand {
|
||||
ImproveCommand =
|
||||
new ImproveSelectedCommand {
|
||||
ImprovementType = SelectedType,
|
||||
ActingHeroId = heroDropdownController.SelectedHeroId,
|
||||
LockType = lockToggle.isOn
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+5
-32
@@ -71,33 +71,6 @@ namespace eagle {
|
||||
public override string CommitWarningText =>
|
||||
$"This will create a treaty with {_model.FactionName(SelectedHero.FactionId.Value)}";
|
||||
|
||||
private List<PrisonerManagementOption> MoveOptions =>
|
||||
SelectedPrisonerToManage.AvailableOptions
|
||||
.Where(opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Move)
|
||||
.ToList();
|
||||
|
||||
protected override ProvinceId? TargetedProvince {
|
||||
get {
|
||||
if (MoveToggle.isOn && moveToDropdown.value >= 0 &&
|
||||
moveToDropdown.value < MoveOptions.Count) {
|
||||
return MoveOptions[moveToDropdown.value].Move.ToProvinceId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set {
|
||||
if (!value.HasValue) return;
|
||||
|
||||
var moveOptions = MoveOptions;
|
||||
int index = moveOptions.FindIndex(opt => opt.Move.ToProvinceId == value.Value);
|
||||
if (index != -1) {
|
||||
MoveToggle.isOn = true;
|
||||
moveToDropdown.value = index;
|
||||
moveToDropdown.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PrisonerManagementOption SelectedOption {
|
||||
get {
|
||||
if (ExecuteToggle.isOn)
|
||||
@@ -219,7 +192,7 @@ namespace eagle {
|
||||
if (!matches) continue;
|
||||
|
||||
var heroName = GetHeroDisplayName(uh.HeroId);
|
||||
questMessages.Add($"{heroName} wants you to {actionVerb}.");
|
||||
questMessages.Add($"{heroName} wants you to {actionVerb} this prisoner.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,24 +204,24 @@ namespace eagle {
|
||||
switch (details.SealedValueCase) {
|
||||
case QuestDetails.SealedValueOneofCase.ExecutePrisonerQuest:
|
||||
if (details.ExecutePrisonerQuest.PrisonerHeroId == prisonerId) {
|
||||
return (true, "execute this prisoner");
|
||||
return (true, "execute");
|
||||
}
|
||||
break;
|
||||
case QuestDetails.SealedValueOneofCase.ExilePrisonerQuest:
|
||||
if (details.ExilePrisonerQuest.PrisonerHeroId == prisonerId) {
|
||||
return (true, "exile this prisoner");
|
||||
return (true, "exile");
|
||||
}
|
||||
break;
|
||||
case QuestDetails.SealedValueOneofCase.FreePrisonerQuest:
|
||||
if (details.FreePrisonerQuest.PrisonerHeroId == prisonerId) {
|
||||
return (true, "release this prisoner");
|
||||
return (true, "release");
|
||||
}
|
||||
break;
|
||||
case QuestDetails.SealedValueOneofCase.ReturnPrisonerQuest:
|
||||
if (details.ReturnPrisonerQuest.PrisonerHeroId == prisonerId) {
|
||||
var factionName =
|
||||
_model.FactionName(details.ReturnPrisonerQuest.ToFactionId);
|
||||
return (true, $"return this prisoner to {factionName}");
|
||||
return (true, $"return to {factionName}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
+3
-6
@@ -40,10 +40,9 @@ namespace eagle {
|
||||
public TMP_Dropdown fromDropdown;
|
||||
public TMP_Dropdown toDropdown;
|
||||
|
||||
// Columns to show/hide based on layout mode
|
||||
// Columns to hide in narrow layout mode (users select via side panels instead)
|
||||
public GameObject heroesColumn;
|
||||
public GameObject battalionsColumn;
|
||||
public GameObject battalionsColumnNarrow; // Alternate battalions view for narrow layout
|
||||
public Slider goldSlider;
|
||||
public Slider foodSlider;
|
||||
public TMP_Text goldTakenLabel;
|
||||
@@ -303,11 +302,9 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public override void OnLayoutModeChanged() {
|
||||
// Heroes always visible (user may march from different province than the viewed one)
|
||||
if (heroesColumn != null) heroesColumn.SetActive(true);
|
||||
// Toggle between wide and narrow battalion views
|
||||
// Update hero/battalion column visibility when layout changes
|
||||
if (heroesColumn != null) heroesColumn.SetActive(!IsNarrowLayout);
|
||||
if (battalionsColumn != null) battalionsColumn.SetActive(!IsNarrowLayout);
|
||||
if (battalionsColumnNarrow != null) battalionsColumnNarrow.SetActive(IsNarrowLayout);
|
||||
}
|
||||
|
||||
protected override void SetUpUI() {
|
||||
|
||||
+7
-14
@@ -16,7 +16,6 @@ namespace eagle {
|
||||
public class EventBasedUnitSelector : MonoBehaviour {
|
||||
public GameObject availableHeroesContent;
|
||||
public GameObject availableBattalionsContent;
|
||||
public GameObject availableBattalionsContentNarrow; // Optional alternate for narrow layout
|
||||
public GameObject selectedUnitsContent;
|
||||
|
||||
public GameObject availableHeroRow;
|
||||
@@ -199,20 +198,10 @@ namespace eagle {
|
||||
if (i % 2 == 1) { newRow.GetComponent<Image>().color = oddColor; }
|
||||
}
|
||||
|
||||
PopulateBattalionsContent(availableBattalionsContent, oddColor);
|
||||
if (availableBattalionsContentNarrow != null) {
|
||||
PopulateBattalionsContent(availableBattalionsContentNarrow, oddColor);
|
||||
foreach (Transform row in availableBattalionsContent.transform) {
|
||||
Destroy(row.gameObject);
|
||||
}
|
||||
|
||||
heroesAndBattalionsPanelController.UpdateTables();
|
||||
|
||||
selectionChanged.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateBattalionsContent(GameObject content, Color oddColor) {
|
||||
foreach (Transform row in content.transform) { Destroy(row.gameObject); }
|
||||
|
||||
var parentTransform = content.transform;
|
||||
parentTransform = availableBattalionsContent.transform;
|
||||
for (int i = 0; i < AvailableBattalions.Count; i++) {
|
||||
var battalion = AvailableBattalions[i];
|
||||
|
||||
@@ -228,6 +217,10 @@ namespace eagle {
|
||||
|
||||
if (i % 2 == 1) { newRow.GetComponent<Image>().color = oddColor; }
|
||||
}
|
||||
|
||||
heroesAndBattalionsPanelController.UpdateTables();
|
||||
|
||||
selectionChanged.Invoke();
|
||||
}
|
||||
|
||||
public void availableHeroButtonClicked(int index) {
|
||||
|
||||
+12
-11
@@ -23,9 +23,7 @@ namespace eagle {
|
||||
/// Attach to a TextMeshProUGUI component to display status.
|
||||
/// </summary>
|
||||
public class ConnectionStatusUI : MonoBehaviour {
|
||||
[Header("UI References")]
|
||||
public TextMeshProUGUI statusText;
|
||||
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private PersistentClientConnection _connection;
|
||||
private IGameStateProvider _gameStateProvider;
|
||||
private string _environmentName;
|
||||
@@ -41,8 +39,10 @@ namespace eagle {
|
||||
private float _timeSinceLastUpdate = 0f;
|
||||
|
||||
void Start() {
|
||||
if (statusText == null) {
|
||||
Debug.LogError("ConnectionStatusUI: statusText reference not assigned");
|
||||
_textComponent = GetComponent<TextMeshProUGUI>();
|
||||
if (_textComponent == null) {
|
||||
Debug.LogError(
|
||||
"ConnectionStatusUI must be attached to a TextMeshProUGUI component");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
@@ -78,7 +78,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
void Update() {
|
||||
if (_connection == null || statusText == null) { return; }
|
||||
if (_connection == null || _textComponent == null) { return; }
|
||||
|
||||
_timeSinceLastUpdate += Time.deltaTime;
|
||||
if (_timeSinceLastUpdate < UpdateInterval) { return; }
|
||||
@@ -97,15 +97,16 @@ namespace eagle {
|
||||
var timeUntilTest = nextTest.Value - DateTime.UtcNow;
|
||||
if (timeUntilTest.TotalSeconds > 0) {
|
||||
int seconds = Math.Min(10, (int)Math.Ceiling(timeUntilTest.TotalSeconds));
|
||||
statusText.text = $"<color=red>●</color> Server down. Retry in {seconds}s";
|
||||
_textComponent.text =
|
||||
$"<color=red>●</color> Server down. Retry in {seconds}s";
|
||||
return;
|
||||
}
|
||||
}
|
||||
statusText.text = "<color=red>●</color> Server unavailable";
|
||||
_textComponent.text = "<color=red>●</color> Server unavailable";
|
||||
return;
|
||||
} else if (circuitState == ConnectionCircuitBreaker.State.HalfOpen) {
|
||||
SetRetryButtonVisible(false);
|
||||
statusText.text = "<color=yellow>●</color> Testing connection...";
|
||||
_textComponent.text = "<color=yellow>●</color> Testing connection...";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ namespace eagle {
|
||||
(nextAttempt.Value - DateTime.UtcNow).TotalSeconds > 0;
|
||||
SetRetryButtonVisible(isCountingDown);
|
||||
|
||||
string displayText = state switch {
|
||||
string statusText = state switch {
|
||||
ConnectionState.Connected => GetConnectedStatusText(),
|
||||
ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
|
||||
ConnectionState.Disconnected => "<color=red>●</color> Disconnected",
|
||||
@@ -127,7 +128,7 @@ namespace eagle {
|
||||
_ => "<color=gray>●</color> Unknown"
|
||||
};
|
||||
|
||||
statusText.text = displayText;
|
||||
_textComponent.text = statusText;
|
||||
}
|
||||
|
||||
private string GetConnectedStatusText() {
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Curves TextMeshPro text along an arc.
|
||||
/// Attach to a GameObject with TextMeshProUGUI component.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(TextMeshProUGUI))]
|
||||
public class CurvedText : MonoBehaviour {
|
||||
[Tooltip(
|
||||
"Curvature amount. Positive curves upward (convex), negative curves downward (concave). 0 = no curve.")]
|
||||
public float curvature = 0f;
|
||||
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private bool _needsUpdate = true;
|
||||
|
||||
void Awake() { _textComponent = GetComponent<TextMeshProUGUI>(); }
|
||||
|
||||
void OnEnable() {
|
||||
TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChanged);
|
||||
_needsUpdate = true;
|
||||
}
|
||||
|
||||
void OnDisable() { TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChanged); }
|
||||
|
||||
void OnTextChanged(Object obj) {
|
||||
if (obj == _textComponent) { _needsUpdate = true; }
|
||||
}
|
||||
|
||||
void LateUpdate() {
|
||||
if (_needsUpdate && _textComponent != null) {
|
||||
_needsUpdate = false;
|
||||
ApplyCurve();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCurvature(float newCurvature) {
|
||||
curvature = newCurvature;
|
||||
_needsUpdate = true;
|
||||
}
|
||||
|
||||
private void ApplyCurve() {
|
||||
if (Mathf.Approximately(curvature, 0f)) { return; }
|
||||
|
||||
_textComponent.ForceMeshUpdate();
|
||||
|
||||
TMP_TextInfo textInfo = _textComponent.textInfo;
|
||||
if (textInfo == null || textInfo.characterCount == 0) { return; }
|
||||
|
||||
// Find the bounds of all characters
|
||||
float minX = float.MaxValue;
|
||||
float maxX = float.MinValue;
|
||||
|
||||
for (int i = 0; i < textInfo.characterCount; i++) {
|
||||
if (!textInfo.characterInfo[i].isVisible) { continue; }
|
||||
|
||||
var charInfo = textInfo.characterInfo[i];
|
||||
minX = Mathf.Min(minX, charInfo.bottomLeft.x);
|
||||
maxX = Mathf.Max(maxX, charInfo.bottomRight.x);
|
||||
}
|
||||
|
||||
float textWidth = maxX - minX;
|
||||
if (textWidth <= 0) { return; }
|
||||
|
||||
float centerX = (minX + maxX) / 2f;
|
||||
|
||||
// Apply curve to each character
|
||||
for (int i = 0; i < textInfo.characterCount; i++) {
|
||||
if (!textInfo.characterInfo[i].isVisible) { continue; }
|
||||
|
||||
var charInfo = textInfo.characterInfo[i];
|
||||
int materialIndex = charInfo.materialReferenceIndex;
|
||||
int vertexIndex = charInfo.vertexIndex;
|
||||
|
||||
Vector3[] vertices = textInfo.meshInfo[materialIndex].vertices;
|
||||
|
||||
// Get character center X position
|
||||
float charCenterX = (vertices[vertexIndex].x + vertices[vertexIndex + 2].x) / 2f;
|
||||
|
||||
// Calculate normalized position (-1 to 1) from center
|
||||
float normalizedX = (charCenterX - centerX) / (textWidth / 2f);
|
||||
|
||||
// Calculate Y offset using parabola: y = -curvature * (1 - x^2)
|
||||
// Negated because Unity Y is up, but our curvature convention has positive = convex
|
||||
// down. Scale by 0.25 to reduce intensity.
|
||||
float scaledCurvature = curvature * 0.25f;
|
||||
float yOffset = -scaledCurvature * (1f - normalizedX * normalizedX);
|
||||
|
||||
// Calculate rotation angle based on tangent of the curve
|
||||
// Derivative of y = -c * (1 - x^2) is dy/dx = 2cx
|
||||
float tangentSlope = 2f * scaledCurvature * normalizedX / (textWidth / 2f);
|
||||
float angle = Mathf.Atan(tangentSlope) * Mathf.Rad2Deg;
|
||||
|
||||
// Apply transformation to all 4 vertices of the character
|
||||
Vector3 charCenter = new Vector3(
|
||||
charCenterX,
|
||||
(vertices[vertexIndex].y + vertices[vertexIndex + 2].y) / 2f,
|
||||
0);
|
||||
|
||||
for (int j = 0; j < 4; j++) {
|
||||
Vector3 offset = vertices[vertexIndex + j] - charCenter;
|
||||
|
||||
// Rotate around character center
|
||||
float rad = angle * Mathf.Deg2Rad;
|
||||
float rotatedX = offset.x * Mathf.Cos(rad) - offset.y * Mathf.Sin(rad);
|
||||
float rotatedY = offset.x * Mathf.Sin(rad) + offset.y * Mathf.Cos(rad);
|
||||
|
||||
vertices[vertexIndex + j] =
|
||||
charCenter + new Vector3(rotatedX, rotatedY + yOffset, offset.z);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the mesh
|
||||
for (int i = 0; i < textInfo.meshInfo.Length; i++) {
|
||||
textInfo.meshInfo[i].mesh.vertices = textInfo.meshInfo[i].vertices;
|
||||
_textComponent.UpdateGeometry(textInfo.meshInfo[i].mesh, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 299f66fc896a34cdeb95e92e927a6f10
|
||||
@@ -15,7 +15,6 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public static readonly Dictionary<Profession, string> ProfessionNames = new() {
|
||||
{ Profession.UnknownProfession, "Unknown" },
|
||||
{ Profession.NoProfession, "None" },
|
||||
{ Profession.Engineer, "Engineer" },
|
||||
{ Profession.Mage, "Mage" },
|
||||
@@ -26,7 +25,6 @@ namespace eagle {
|
||||
};
|
||||
|
||||
public static readonly Dictionary<Profession, string> ShortProfessionNames = new() {
|
||||
{ Profession.UnknownProfession, "?" },
|
||||
{ Profession.NoProfession, "None" },
|
||||
{ Profession.Engineer, "Engr" },
|
||||
{ Profession.Mage, "Mage" },
|
||||
@@ -106,9 +104,6 @@ namespace eagle {
|
||||
return $"{name} is unlikely to stay here long enough to impress with a quest.";
|
||||
|
||||
case RecruitmentStatus.Sworn: return $"{name} is dedicated to another faction.";
|
||||
|
||||
case RecruitmentStatus.WouldNeverJoin:
|
||||
return $"{name} would never join your faction.";
|
||||
}
|
||||
|
||||
return $"Unknown!";
|
||||
@@ -133,8 +128,6 @@ namespace eagle {
|
||||
case RecruitmentStatus.Traveler: return "Traveler";
|
||||
|
||||
case RecruitmentStatus.Sworn: return "Dedicated to another faction";
|
||||
|
||||
case RecruitmentStatus.WouldNeverJoin: return "Would never join";
|
||||
}
|
||||
|
||||
return $"Unknown!";
|
||||
@@ -182,7 +175,6 @@ namespace eagle {
|
||||
case SealedValueOneofCase.WinBattlesQuest: return "Win Battles";
|
||||
case SealedValueOneofCase.ApprehendOutlawQuest: return "Apprehend Outlaw";
|
||||
case SealedValueOneofCase.StartBlizzardQuest: return "Start Blizzard";
|
||||
case SealedValueOneofCase.StartDroughtQuest: return "Start Drought";
|
||||
case SealedValueOneofCase.StartEpidemicQuest: return "Start Epidemic";
|
||||
case SealedValueOneofCase.SpendOnFeastsQuest: return "Host Feasts";
|
||||
case SealedValueOneofCase.SendSuppliesQuest: return "Send Supplies";
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates a drought effect with a hot sun and heat shimmer overlay.
|
||||
/// The heat shimmer distorts the province using a shader that masks to province boundaries.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class DroughtEffect : MonoBehaviour {
|
||||
[Header("Sun Settings")]
|
||||
public Sprite sunSprite;
|
||||
public float sunSize = 40f;
|
||||
public float sunVerticalOffset = 40f;
|
||||
public Color sunColor = new Color(1f, 0.9f, 0.3f, 0.9f);
|
||||
public float sunPulseSpeed = 1.5f;
|
||||
public float sunPulseAmount = 0.15f;
|
||||
public float sunWaveAmountX = 8f; // Horizontal wave distance
|
||||
public float sunWaveAmountY = 5f; // Vertical wave distance
|
||||
public float sunWaveSpeedX = 0.4f; // Horizontal wave speed
|
||||
public float sunWaveSpeedY =
|
||||
0.6f; // Vertical wave speed (slightly different for organic feel)
|
||||
|
||||
[Header("Heat Shimmer Settings")]
|
||||
public Material shimmerMaterial;
|
||||
public float shimmerSize = 300f; // Large enough to cover province
|
||||
|
||||
[Header("Province Masking")]
|
||||
public Texture2D provinceIdMap;
|
||||
public int targetProvinceId;
|
||||
public Vector4 mapBounds; // x=minX, y=minY, z=width, w=height
|
||||
|
||||
private GameObject _sunObject;
|
||||
private Image _sunImage;
|
||||
private GameObject _shimmerObject;
|
||||
private RawImage _shimmerImage; // RawImage renders without sprite
|
||||
private Material _shimmerMatInstance;
|
||||
private Texture2D _whiteTexture;
|
||||
private float _baseSunScale;
|
||||
private Vector2 _baseSunPosition;
|
||||
|
||||
void Start() { SetupEffect(); }
|
||||
|
||||
private void SetupEffect() {
|
||||
SetupSun();
|
||||
// Note: Shimmer disabled - GrabPass doesn't work well with UI Canvas
|
||||
// TODO: Consider alternative shimmer approach (animated texture overlay)
|
||||
// SetupShimmer();
|
||||
}
|
||||
|
||||
private void SetupSun() {
|
||||
_sunObject = new GameObject("Sun");
|
||||
_sunObject.transform.SetParent(transform, false);
|
||||
|
||||
_sunImage = _sunObject.AddComponent<Image>();
|
||||
if (sunSprite != null) { _sunImage.sprite = sunSprite; }
|
||||
_sunImage.color = sunColor;
|
||||
|
||||
var rectTransform = _sunObject.GetComponent<RectTransform>();
|
||||
rectTransform.sizeDelta = new Vector2(sunSize, sunSize);
|
||||
_baseSunPosition = new Vector2(0f, sunVerticalOffset);
|
||||
rectTransform.anchoredPosition = _baseSunPosition;
|
||||
|
||||
_baseSunScale = 1f;
|
||||
}
|
||||
|
||||
private void SetupShimmer() {
|
||||
// Try to find the shader and create material
|
||||
var shader = Shader.Find("Eagle/HeatShimmer");
|
||||
if (shader == null) {
|
||||
Debug.LogWarning("DroughtEffect: Could not find Eagle/HeatShimmer shader");
|
||||
return;
|
||||
}
|
||||
|
||||
_shimmerObject = new GameObject("HeatShimmer");
|
||||
_shimmerObject.transform.SetParent(transform, false);
|
||||
// Put shimmer behind sun
|
||||
_shimmerObject.transform.SetAsFirstSibling();
|
||||
|
||||
// Create a white texture for RawImage (required for rendering)
|
||||
_whiteTexture = new Texture2D(1, 1);
|
||||
_whiteTexture.SetPixel(0, 0, Color.white);
|
||||
_whiteTexture.Apply();
|
||||
|
||||
// Use RawImage which renders without needing a sprite
|
||||
_shimmerImage = _shimmerObject.AddComponent<RawImage>();
|
||||
_shimmerImage.texture = _whiteTexture;
|
||||
_shimmerMatInstance = new Material(shader);
|
||||
_shimmerImage.material = _shimmerMatInstance;
|
||||
_shimmerImage.color = Color.white;
|
||||
|
||||
var rectTransform = _shimmerObject.GetComponent<RectTransform>();
|
||||
rectTransform.sizeDelta = new Vector2(shimmerSize, shimmerSize);
|
||||
rectTransform.anchoredPosition = Vector2.zero;
|
||||
|
||||
// Set shader properties
|
||||
if (provinceIdMap != null) {
|
||||
_shimmerMatInstance.SetTexture("_ProvinceIDMap", provinceIdMap);
|
||||
}
|
||||
_shimmerMatInstance.SetFloat("_TargetProvinceId", targetProvinceId);
|
||||
_shimmerMatInstance.SetVector("_MapBounds", mapBounds);
|
||||
}
|
||||
|
||||
public void SetProvinceId(int provinceId) {
|
||||
targetProvinceId = provinceId;
|
||||
if (_shimmerMatInstance != null) {
|
||||
_shimmerMatInstance.SetFloat("_TargetProvinceId", provinceId);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetProvinceIdMap(Texture2D idMap) {
|
||||
provinceIdMap = idMap;
|
||||
if (_shimmerMatInstance != null) {
|
||||
_shimmerMatInstance.SetTexture("_ProvinceIDMap", idMap);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMapBounds(Vector4 bounds) {
|
||||
mapBounds = bounds;
|
||||
if (_shimmerMatInstance != null) {
|
||||
_shimmerMatInstance.SetVector("_MapBounds", bounds);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetShimmerSize(float size) {
|
||||
shimmerSize = size;
|
||||
if (_shimmerObject != null) {
|
||||
var rt = _shimmerObject.GetComponent<RectTransform>();
|
||||
rt.sizeDelta = new Vector2(size, size);
|
||||
}
|
||||
}
|
||||
|
||||
void Update() {
|
||||
if (_sunObject != null) {
|
||||
// Pulse the sun size
|
||||
float pulse = 1f + Mathf.Sin(Time.time * sunPulseSpeed) * sunPulseAmount;
|
||||
_sunObject.transform.localScale = Vector3.one * (_baseSunScale * pulse);
|
||||
|
||||
// Wave the sun position slowly
|
||||
var rectTransform = _sunObject.GetComponent<RectTransform>();
|
||||
float waveX = Mathf.Sin(Time.time * sunWaveSpeedX) * sunWaveAmountX;
|
||||
float waveY = Mathf.Sin(Time.time * sunWaveSpeedY) * sunWaveAmountY;
|
||||
rectTransform.anchoredPosition = _baseSunPosition + new Vector2(waveX, waveY);
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_shimmerMatInstance != null) { Destroy(_shimmerMatInstance); }
|
||||
if (_whiteTexture != null) { Destroy(_whiteTexture); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f64938ef171de43dba1b0a865fc86f60
|
||||
@@ -21,15 +21,12 @@ public class EagleCommonTextures : MonoBehaviour {
|
||||
public Texture2D Profession(Profession profession) {
|
||||
if (profession == Net.Eagle0.Eagle.Common.Profession.NoProfession)
|
||||
return professionTextures.Last();
|
||||
if (profession == Net.Eagle0.Eagle.Common.Profession.UnknownProfession) return null;
|
||||
return professionTextures[(int)profession];
|
||||
}
|
||||
|
||||
public Texture2D Profession(HeroView.Types.ProfessionSpecificInfo.Types.Profession profession) {
|
||||
if (profession == HeroView.Types.ProfessionSpecificInfo.Types.Profession.NoProfession)
|
||||
return professionTextures.Last();
|
||||
if (profession == HeroView.Types.ProfessionSpecificInfo.Types.Profession.Unknown)
|
||||
return null;
|
||||
return professionTextures[(int)profession];
|
||||
}
|
||||
|
||||
|
||||
+16
-27
@@ -33,6 +33,8 @@ namespace eagle {
|
||||
|
||||
[Header("Status Labels")]
|
||||
public TextMeshProUGUI roundStatusLabel;
|
||||
public TextMeshProUGUI connectionStatusLabel;
|
||||
private bool _connectionStatusUIInitialized = false;
|
||||
|
||||
[Header("Main Panels")]
|
||||
public ProvinceInfoPanelController provinceInfoPanelController;
|
||||
@@ -143,6 +145,16 @@ namespace eagle {
|
||||
|
||||
if (newWidth == _lastWidth && newHeight == _lastHeight) { return; }
|
||||
|
||||
// Initialize ConnectionStatusUI component once when connection is available
|
||||
if (!_connectionStatusUIInitialized &&
|
||||
errorHandler.PersistentClientConnection != null) {
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) {
|
||||
statusUI.SetConnection(errorHandler.PersistentClientConnection);
|
||||
_connectionStatusUIInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
_lastWidth = newWidth;
|
||||
_lastHeight = newHeight;
|
||||
|
||||
@@ -213,21 +225,12 @@ namespace eagle {
|
||||
}
|
||||
SwapModel();
|
||||
Model = null;
|
||||
|
||||
// Clear stale province state from the previous game so it doesn't
|
||||
// get used with the next game's hero/battalion maps.
|
||||
provinceInfoPanelController.Province = null;
|
||||
freeHeroesTableController.CurrentProvince = null;
|
||||
movingArmiesTableControllerNarrow.CurrentProvince = null;
|
||||
movingArmiesTableControllerWide.CurrentProvince = null;
|
||||
factionsTableControllerNarrow.CurrentProvince = null;
|
||||
factionsTableControllerWide.CurrentProvince = null;
|
||||
heroesAndBattalionsPanelController.CurrentProvince = null;
|
||||
chronicleCanvasController.Entries = new List<ChronicleEntry>();
|
||||
SetMusic();
|
||||
|
||||
// Clear game state provider when leaving game
|
||||
PersistentUIManager.Instance?.connectionStatusUI?.SetGameStateProvider(null);
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(null); }
|
||||
}
|
||||
|
||||
void MapControllerChangedTarget(List<ProvinceId> newTarget) {
|
||||
@@ -332,7 +335,7 @@ namespace eagle {
|
||||
ModelUpdater.ErrorHandler = errorHandler;
|
||||
|
||||
// Set up game state provider and environment for connection status UI
|
||||
var statusUI = PersistentUIManager.Instance?.connectionStatusUI;
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) {
|
||||
statusUI.SetGameStateProvider(ModelUpdater);
|
||||
statusUI.SetEnvironment(environmentName);
|
||||
@@ -350,12 +353,6 @@ 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);
|
||||
@@ -586,12 +583,6 @@ 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) {
|
||||
@@ -772,9 +763,6 @@ 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);
|
||||
|
||||
@@ -807,6 +795,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
shardokController.SetUpGame(shardokModel);
|
||||
shardokController.SetConnection(errorHandler.PersistentClientConnection);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,6 @@ namespace eagle {
|
||||
public List<ProvinceId> ProvincesForFaction(FactionId factionId);
|
||||
|
||||
public List<ChronicleEntry> ChronicleEntries { get; }
|
||||
|
||||
GameType GameType { get; }
|
||||
}
|
||||
|
||||
public class GameModelUpdater : IClientConnectionSubscriber, IGameStateProvider {
|
||||
@@ -203,8 +201,6 @@ 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();
|
||||
@@ -264,7 +260,7 @@ namespace eagle {
|
||||
if (battleView == null) {
|
||||
// Battle was removed (e.g., it ended) before we could create the model.
|
||||
// This is expected when Eagle's RemovedBattleIds update arrives before a
|
||||
// pending Shardok update - the UI already shows "Exit Battle" via
|
||||
// pending Shardok update - the UI already shows "Back to Eagle" via
|
||||
// MarkBattleEnded(), so we just skip this stale update.
|
||||
return null;
|
||||
}
|
||||
@@ -302,13 +298,7 @@ namespace eagle {
|
||||
kv => kv.Value.ImagePath),
|
||||
battalionNames: _currentModel.BattalionNames.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value),
|
||||
heroFallbackLookup: hid => {
|
||||
if (_currentModel.Heroes.TryGetValue(hid, out var hero)) {
|
||||
return (hero.NameTextId, hero.ImagePath);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
kv => kv.Value));
|
||||
|
||||
return shardokModel;
|
||||
}
|
||||
@@ -942,10 +932,6 @@ 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) {
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: 618063871244848148}
|
||||
m_Layer: 0
|
||||
m_Name: BeastsEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &618063871244848148
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a5ee82ebc029341c4858ef5b7e7cf201, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.BeastsEffect
|
||||
birdTexture: {fileID: 2800000, guid: 7b6ab1e235eea4e4e9319c88bb1cdc70, type: 3}
|
||||
birdMaterial: {fileID: 2100000, guid: 67dcb765c9c324ff9a103624340bc9b5, type: 2}
|
||||
birdCount: 5
|
||||
birdLifetime: 8
|
||||
birdSize: 12
|
||||
circleRadius: 35
|
||||
circleSpeed: 1
|
||||
birdColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
verticalOscillation: 10
|
||||
radiusVariation: 10
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c093deb39df94fbca5faa9f015a2331
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: 3572684436619911535}
|
||||
m_Layer: 0
|
||||
m_Name: BlizzardEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &3572684436619911535
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c15980e88e57845ce920e43f29d35fcc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.BlizzardEffect
|
||||
snowflakeTexture: {fileID: 2800000, guid: 9a5b4e8c3d2f6b0a7c1e9d5f8a3b2c4d, type: 3}
|
||||
particleMaterial: {fileID: 2100000, guid: b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6, type: 2}
|
||||
snowEmitRate: 45
|
||||
snowFallSpeed: 40
|
||||
snowLifetime: 3
|
||||
snowSize: 8
|
||||
snowDrift: 10
|
||||
snowColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
windStrength: 5
|
||||
windVariation: 3
|
||||
provinceIdMap: {fileID: 0}
|
||||
targetProvinceId: 0
|
||||
mapBounds: {x: 0, y: 0, z: 0, w: 0}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cf4724a235af4fb8abab6456789936a
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: -4077358403987153371}
|
||||
m_Layer: 0
|
||||
m_Name: DroughtEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &-4077358403987153371
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f64938ef171de43dba1b0a865fc86f60, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.DroughtEffect
|
||||
sunSprite: {fileID: 21300000, guid: ecdaab62f65f19745aee51dc3e5be2fc, type: 3}
|
||||
sunSize: 20
|
||||
sunVerticalOffset: 30
|
||||
sunColor: {r: 1, g: 0.9019608, b: 0.29803923, a: 0.68235296}
|
||||
sunPulseSpeed: 1.5
|
||||
sunPulseAmount: 0.1
|
||||
shimmerMaterial: {fileID: 10303, guid: 0000000000000000f000000000000000, type: 0}
|
||||
shimmerSize: 15
|
||||
provinceIdMap: {fileID: 0}
|
||||
targetProvinceId: 0
|
||||
mapBounds: {x: 0, y: 0, z: 0, w: 0}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1be06ed8f9be949478a81914e2479d4d
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: 8607881802583290962}
|
||||
m_Layer: 0
|
||||
m_Name: EpidemicEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &8607881802583290962
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 727093fab7ebc43dc94ee59675164706, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.EpidemicEffect
|
||||
skullMaterial: {fileID: 2100000, guid: c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7, type: 2}
|
||||
skullEmitRate: 1.5
|
||||
skullRiseSpeed: 25
|
||||
skullLifetime: 3
|
||||
skullSize: 25
|
||||
skullSway: 8
|
||||
skullColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
hazeMaterial: {fileID: 2100000, guid: c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7, type: 2}
|
||||
hazeEmitRate: 10
|
||||
hazeSpeed: 8
|
||||
hazeLifetime: 4
|
||||
hazeSize: 10
|
||||
hazeColor: {r: 0.4, g: 0.54901963, b: 0.34901962, a: 0.50980395}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user