mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 12:35:41 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de22df3848 | ||
|
|
e281f3b012 | ||
|
|
9695db2536 | ||
|
|
aaba34e7de | ||
|
|
9e37236fe3 |
@@ -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
|
||||
|
||||
@@ -5,28 +5,9 @@
|
||||
*.tif filter=lfs diff=lfs merge=lfs -text
|
||||
*.bytes filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.psb filter=lfs diff=lfs merge=lfs -text
|
||||
# PSB meta files contain bone/sprite import data and can be very large
|
||||
*.psb.meta filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
# Exclude pre-existing font files that were committed as blobs (not LFS pointers)
|
||||
src/main/csharp/**/GUI[[:space:]]Pro[[:space:]]Kit*/**/*.ttf !filter !diff !merge
|
||||
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,10 +23,6 @@ on:
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -34,34 +30,16 @@ jobs:
|
||||
lint:
|
||||
runs-on: [self-hosted, bazel]
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
git sparse-checkout disable 2>/dev/null || true
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
- name: Check BUILD.bazel dependencies
|
||||
run: ./scripts/check_build_deps.sh --strict
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- name: Check JavaScript syntax
|
||||
run: node --check src/main/go/net/eagle0/admin_server/static/map_editor.js
|
||||
|
||||
test:
|
||||
runs-on: [self-hosted, bazel]
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
git sparse-checkout disable 2>/dev/null || true
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Blob Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 04:00 UTC
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Clean up unreferenced blobs
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
bazel run //src/main/go/net/eagle0/build/blob_cleanup:blob_cleanup -- --min-age=1h
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -112,29 +112,14 @@ jobs:
|
||||
- name: Delete all installer artifacts
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Delete ALL eagle-installer artifacts to free up storage
|
||||
echo "Fetching all eagle-installer artifacts..."
|
||||
page=1
|
||||
while true; do
|
||||
response=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?per_page=100&page=$page")
|
||||
ids=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for a in data.get('artifacts', []):
|
||||
if a['name'] == 'eagle-installer':
|
||||
print(a['id'])
|
||||
" 2>/dev/null)
|
||||
if [ -z "$ids" ]; then
|
||||
break
|
||||
fi
|
||||
for id in $ids; do
|
||||
echo "Deleting artifact ID: $id"
|
||||
curl -s -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts/$id" || true
|
||||
done
|
||||
page=$((page + 1))
|
||||
artifact_ids=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.name == "eagle-installer") | .id')
|
||||
for id in $artifact_ids; do
|
||||
echo "Deleting artifact ID: $id"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" || true
|
||||
done
|
||||
echo "Cleanup complete"
|
||||
|
||||
@@ -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 }}
|
||||
@@ -181,3 +218,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:
|
||||
@@ -115,14 +96,14 @@ jobs:
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh StandaloneWindows64
|
||||
- name: Deploy Windows unity
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" "/tmp/unity_manifest.txt"
|
||||
|
||||
- name: Update unified manifest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -145,12 +126,21 @@ jobs:
|
||||
# Cleanup
|
||||
rm -f /tmp/manifest_signing_key
|
||||
|
||||
- name: Export deployed version
|
||||
id: get-version
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
- 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,17 +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: Notify clients of update
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
run: |
|
||||
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/
|
||||
@@ -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,107 +0,0 @@
|
||||
# Adding Beast-Type-Specific Effects
|
||||
|
||||
When a province has a `BeastsEvent`, `ProvinceBeastsController` spawns a visual effect at the province centroid. By default this is the generic `BeastsEffect` (circling vultures/crows). Beast-specific effects can override this based on `BeastInfo.SingularName`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ProvinceBeastsController
|
||||
├── GetBeastName(province) → reads BeastsEvent.BeastInfo.SingularName
|
||||
├── GetEffectPrefab(beastName) → switch on name → returns prefab
|
||||
└── SpawnBeastsEffect(provinceId, beastName) → instantiates at centroid
|
||||
```
|
||||
|
||||
**Key files:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Assets/Eagle/ProvinceBeastsController.cs` | Routes beast names to prefabs, positions effects |
|
||||
| `Assets/Eagle/BeastsEffect.cs` | Generic effect (circling birds via particle system) |
|
||||
| `Assets/Eagle/DragonEffect.cs` | Dragon-specific effect (animated PSD Importer prefab) |
|
||||
| `Assets/Eagle/Effects/BeastsEffect.prefab` | Generic effect prefab |
|
||||
| `Assets/Eagle/Effects/DragonEffect.prefab` | Dragon effect prefab |
|
||||
|
||||
## Adding a new beast type
|
||||
|
||||
### 1. Create the effect MonoBehaviour
|
||||
|
||||
Create `Assets/Eagle/<BeastName>Effect.cs`. Two approaches:
|
||||
|
||||
**Particle-based** (like `BeastsEffect.cs`): Good for simple effects like swarms, packs, or atmospheric particles. Build particle systems programmatically in `Awake()`.
|
||||
|
||||
**Animated prefab** (like `DragonEffect.cs`): Good for a single creature with bone-driven animation. Requires:
|
||||
- A PSD Importer `.psb` file with the creature art
|
||||
- `com.unity.2d.animation` and `com.unity.2d.psdimporter` packages (already installed)
|
||||
- An Animator Controller with animation clips
|
||||
- The effect script instantiates the animated prefab and drives its position/animation
|
||||
|
||||
Your MonoBehaviour should:
|
||||
- Use `[RequireComponent(typeof(RectTransform))]` for UI canvas positioning
|
||||
- Accept tuning parameters as public fields (circle radius, speed, scale, etc.)
|
||||
- Clean up materials in `OnDestroy()` to avoid leaks
|
||||
|
||||
### 2. Create the prefab
|
||||
|
||||
1. Create an empty GameObject in `Assets/Eagle/Effects/`
|
||||
2. Add a `RectTransform` component
|
||||
3. Add your effect MonoBehaviour
|
||||
4. Wire up any references (animated prefab, textures, materials)
|
||||
5. Save as prefab
|
||||
|
||||
### 3. Register in ProvinceBeastsController
|
||||
|
||||
Add a prefab field and a case in `GetEffectPrefab()`:
|
||||
|
||||
```csharp
|
||||
// In ProvinceBeastsController.cs
|
||||
|
||||
// Add inspector field alongside existing ones:
|
||||
public GameObject wolfEffectPrefab;
|
||||
|
||||
// Add case in GetEffectPrefab(), keyed by singularName from beasts.tsv:
|
||||
private GameObject GetEffectPrefab(string beastName) {
|
||||
switch (beastName) {
|
||||
case "dragon": return dragonEffectPrefab;
|
||||
case "wolf": return wolfEffectPrefab;
|
||||
default: return beastsEffectPrefab;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Wire up in the scene
|
||||
|
||||
In `Gameplay.unity`, find the `ProvinceBeastsController` component and assign your new prefab to the new field.
|
||||
|
||||
### 5. Test
|
||||
|
||||
Use `[ContextMenu]` methods on `ProvinceBeastsController` to test:
|
||||
- "Test Dragon in Motcia (ID 2)" spawns a dragon effect
|
||||
- Add similar methods for your beast type
|
||||
- "Clear All Effects" removes all active effects
|
||||
|
||||
## Beast name matching
|
||||
|
||||
Beast names come from `BeastInfo.SingularName` in the proto, which uses the canonical `singularName` from `src/main/resources/net/eagle0/eagle/beasts.tsv`. The switch in `GetEffectPrefab` uses exact string matching on this canonical name (e.g. `"dragon"`, `"wolf"`, `"skeleton"`).
|
||||
|
||||
## Existing beast types in the game
|
||||
|
||||
See `src/main/resources/net/eagle0/eagle/beasts.tsv` for the full list. Some candidates for custom effects:
|
||||
|
||||
| singularName | Notes |
|
||||
|---|---|
|
||||
| dragon | Has custom effect (animated 2D dragon) |
|
||||
| wolf | Common, could use pack/howling effect |
|
||||
| bear | Could use large creature effect |
|
||||
| skeleton | Could reuse DungeonMonsters2D Skeleton asset |
|
||||
| demon | Could reuse DungeonMonsters2D Demon asset |
|
||||
| spider | Could reuse DungeonMonsters2D Spider asset |
|
||||
| rat | Could reuse DungeonMonsters2D Rat asset |
|
||||
| vampire | Could reuse DungeonMonsters2D Vampire asset |
|
||||
| zombie | Could reuse DungeonMonsters2D Zombie asset |
|
||||
|
||||
## Tips
|
||||
|
||||
- Keep effects lightweight; multiple provinces may have beasts simultaneously
|
||||
- Use `ParticleSystemScalingMode.Hierarchy` so effects scale with the map zoom
|
||||
- Set appropriate `sortingOrder` on renderers (existing effects use 102-104)
|
||||
- For animated prefabs, set `AnimatorCullingMode.AlwaysAnimate` since UI elements may be considered offscreen by the culling system
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,7 @@ When a new player starts their first game in tutorial mode, they experience:
|
||||
## Battle Configuration
|
||||
|
||||
### Defender (Player - John Ranil)
|
||||
- **Heroes**: John Ranil + 2 random vassals (3 total)
|
||||
- **Heroes**: John Ranil + 2 random sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 2x Light Infantry (300 troops each, 60 training/armament)
|
||||
- 1x Longbowmen (200 troops, 60 training/armament)
|
||||
@@ -29,7 +29,7 @@ When a new player starts their first game in tutorial mode, they experience:
|
||||
### Attacker (Ikhaan Tarn)
|
||||
- **Heroes**: Ikhaan Tarn + 2 sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 1x Heavy Cavalry (600 troops, 80 training/armament)
|
||||
- 1x Heavy Cavalry (400 troops, 80 training/armament)
|
||||
- 1x Heavy Infantry (500 troops, 80 training/armament)
|
||||
- 1x Longbowmen (300 troops, 80 training/armament)
|
||||
- **Origin Province**: 32
|
||||
@@ -88,16 +88,19 @@ Screen 3: "Defend Your Home" - Call to action
|
||||
|
||||
**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);
|
||||
// Check if attacker should flee
|
||||
bool ShouldTriggerScriptedFlee(const GameStateW& state);
|
||||
|
||||
// Check if player (defender) can flee
|
||||
bool PlayerCanFlee(PlayerId playerId);
|
||||
|
||||
// Execute the scripted flee (100% success rate)
|
||||
std::vector<ActionResult> ExecuteScriptedFlee(...);
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- `ShardokEngine.cpp`: Calls `CheckAndExecuteEvents()` at end of each round in `HandlePlayerTurnEnd()`
|
||||
- Events fire in config order, each event fires at most once
|
||||
**Integration Points (TODO):**
|
||||
- `ShardokEngine.cpp`: Check `ShouldTriggerScriptedFlee()` during turn processing
|
||||
- `FleeCommandFactory.cpp`: Disable flee commands for defender in tutorial mode
|
||||
|
||||
### Phase 4: Mid-Battle Reinforcements
|
||||
|
||||
@@ -129,33 +132,6 @@ std::vector<ActionResult> CheckAndExecuteEvents(
|
||||
- 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
|
||||
@@ -183,6 +159,12 @@ See `TUTORIAL_CONTENT.md` for full dialogue text.
|
||||
|
||||
## Remaining Implementation
|
||||
|
||||
### ShardokEngine Integration
|
||||
Integrate `TutorialBattleController` into the Shardok battle loop:
|
||||
1. Initialize controller from battle config when battle starts
|
||||
2. Check `ShouldTriggerScriptedFlee()` at end of each round
|
||||
3. If true, call `ExecuteScriptedFlee()` and add results to action queue
|
||||
|
||||
### Disable Player Flee
|
||||
In `FleeCommandFactory.cpp`:
|
||||
- Check if tutorial mode is enabled
|
||||
@@ -239,29 +221,10 @@ In Eagle's battle resolution:
|
||||
```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
|
||||
}
|
||||
int32 flee_after_defender_units_lost = 2;
|
||||
int32 flee_after_rounds = 3;
|
||||
int32 attacker_player_id = 4;
|
||||
bool defender_can_flee = 5;
|
||||
repeated string reinforcement_hero_names = 6;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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,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/
|
||||
|
||||
@@ -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,293 +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); }
|
||||
}
|
||||
}
|
||||
|
||||
// Build set of eagle hero IDs for this specific reinforcement event
|
||||
std::set<int32_t> expectedHeroIds;
|
||||
for (const auto& commonUnit : reinforcementsAction.units()) {
|
||||
if (commonUnit.has_hero()) { expectedHeroIds.insert(commonUnit.hero().eagle_hero_id()); }
|
||||
}
|
||||
|
||||
// Place only the PENDING_REINFORCEMENT units that belong to this event
|
||||
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 (!unit->has_attached_hero() ||
|
||||
!expectedHeroIds.contains(unit->attached_hero().eagle_hero_id())) {
|
||||
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,72 +144,17 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For tutorial battles, override settings so duels are always accepted
|
||||
auto effectiveSettings = gameSettings;
|
||||
if (tutorialConfig.enabled()) {
|
||||
effectiveSettings = std::make_shared<GameSettings>(*gameSettings);
|
||||
effectiveSettings->GetSetter().SetInt(kSettingsKeys.duelBaseAcceptOdds, 200);
|
||||
}
|
||||
|
||||
unique_ptr<ShardokEngine> engine = std::make_unique<ShardokEngine>(
|
||||
effectiveSettings,
|
||||
gameSettings,
|
||||
std::move(unplacedUnits),
|
||||
customMapBytes.empty() ? LoadMap(mapName) : LoadMapFromBytes(customMapBytes),
|
||||
month,
|
||||
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>
|
||||
|
||||
+109
-125
@@ -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,
|
||||
@@ -830,7 +806,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-create and launch a 7-faction game for first-time users.
|
||||
/// Auto-create and launch a tutorial game for first-time users.
|
||||
/// Tutorial mode: 3 factions (player, King, weak rival), player spawns at province 14 with
|
||||
/// quest.
|
||||
/// </summary>
|
||||
private void AutoCreateFirstGame() {
|
||||
if (fetchedNewGameLeaders == null || fetchedNewGameLeaders.Count == 0) {
|
||||
@@ -842,9 +820,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
var randomIndex = new System.Random().Next(fetchedNewGameLeaders.Count);
|
||||
var leaderTextId = fetchedNewGameLeaders[randomIndex].NameTextId;
|
||||
|
||||
// Create a 7-faction game (1 human + 6 AI) - same as lobby default
|
||||
Debug.Log($"[ConnectionHandler] Auto-creating 7-faction game with leader {leaderTextId}");
|
||||
CreateGame(leaderTextId, 1, 7);
|
||||
// Create a tutorial game (1 human, 3 total factions - player, King, and weak rival)
|
||||
Debug.Log($"[ConnectionHandler] Auto-creating tutorial game with leader {leaderTextId}");
|
||||
CreateGame(leaderTextId, 1, 3, isTutorialMode: true);
|
||||
}
|
||||
|
||||
private void StartListeningForLobbyUpdates() {
|
||||
@@ -900,7 +878,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
};
|
||||
_persistentClientConnection.Connect();
|
||||
|
||||
PersistentClientConnection.Current = _persistentClientConnection;
|
||||
errorHandler.PersistentClientConnection = _persistentClientConnection;
|
||||
ResourceFetcher.SetUpConnection(_httpClient);
|
||||
}
|
||||
@@ -971,23 +948,25 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
private void ConfirmDropGame(long gameId) { _internalDropGame(gameId); }
|
||||
|
||||
/// <summary>
|
||||
/// 3-parameter overload for CreateCallback delegate compatibility.
|
||||
/// </summary>
|
||||
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);
|
||||
CreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount, isTutorialMode: false);
|
||||
}
|
||||
|
||||
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
|
||||
private void CreateGame(
|
||||
string leaderNameTextId,
|
||||
int humanPlayerCount,
|
||||
int totalPlayerCount,
|
||||
bool isTutorialMode) {
|
||||
// Track multiplayer status for tutorial system
|
||||
TutorialManager.IsMultiplayerGame = humanPlayerCount > 1;
|
||||
var game = _internalCreateGame(
|
||||
desiredLeaderTextId: "",
|
||||
humanPlayerCount: 1,
|
||||
totalPlayerCount: 2,
|
||||
isTutorial: true);
|
||||
leaderNameTextId,
|
||||
humanPlayerCount,
|
||||
totalPlayerCount,
|
||||
isTutorialMode);
|
||||
}
|
||||
|
||||
public void QuitButtonClicked() { Application.Quit(); }
|
||||
@@ -1007,9 +986,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()); }
|
||||
}
|
||||
@@ -1025,7 +1009,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
string desiredLeaderTextId,
|
||||
int humanPlayerCount,
|
||||
int totalPlayerCount,
|
||||
bool isTutorial = false) {
|
||||
bool isTutorialMode = false) {
|
||||
try {
|
||||
return await _persistentClientConnection.SendUpdateStreamRequestAsync(
|
||||
new UpdateStreamRequest {
|
||||
@@ -1034,7 +1018,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
DesiredLeaderTextId = desiredLeaderTextId,
|
||||
TotalPlayerCount = totalPlayerCount,
|
||||
HumanPlayerCount = humanPlayerCount,
|
||||
IsTutorial = isTutorial
|
||||
IsTutorialMode = isTutorialMode
|
||||
}
|
||||
});
|
||||
} catch (WebException e) {
|
||||
|
||||
+3653
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c093deb39df94fbca5faa9f015a2331
|
||||
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) {
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b383d039013397f44bd766e6dcc74420
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86fa980793a53b3489ac5731c86a3152
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-5903
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4020475e6c70aa048a82148d116ce43e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/AttackDragon.anim
|
||||
uploadId: 600989
|
||||
-12825
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e265f65128d6bee49ad37d177488a169
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/ClimbDragon.anim
|
||||
uploadId: 600989
|
||||
-6642
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c09c1592e02e1d4781e2237f587e37b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/DeathDragon.anim
|
||||
uploadId: 600989
|
||||
-782
@@ -1,782 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1101 &-8538785557602796017
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: MoveTrigger
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 7645206152784429428}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.75
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &-7900222903461492083
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: TalkTrigger
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: -2518930950459252685}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.75
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &-6637492386302821641
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: SpecialAttack
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: 9207208706880946675}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 322d2b45c4c3c6f47a34ff8e869b9047, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1107 &-6446312603455984934
|
||||
AnimatorStateMachine:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Base Layer
|
||||
m_ChildStates:
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: -5308216943816245398}
|
||||
m_Position: {x: 220, y: 160, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 3942610697287538840}
|
||||
m_Position: {x: 220, y: 30, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: -1886661871251496921}
|
||||
m_Position: {x: 450, y: 90, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 547046659980487195}
|
||||
m_Position: {x: 220, y: -120, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 6590903340607972877}
|
||||
m_Position: {x: 220, y: 90, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 7645206152784429428}
|
||||
m_Position: {x: 220, y: -30, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: -6637492386302821641}
|
||||
m_Position: {x: 450, y: 160, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: -2643461505716286733}
|
||||
m_Position: {x: 450, y: 20, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: -2518930950459252685}
|
||||
m_Position: {x: 470, y: -50, z: 0}
|
||||
m_ChildStateMachines: []
|
||||
m_AnyStateTransitions:
|
||||
- {fileID: -8538785557602796017}
|
||||
- {fileID: -7900222903461492083}
|
||||
- {fileID: 8652467406494450390}
|
||||
- {fileID: 6622731859202713379}
|
||||
- {fileID: 6100266138257142490}
|
||||
- {fileID: -1424066395699519718}
|
||||
- {fileID: 7279485870587762051}
|
||||
- {fileID: -4202185913788308745}
|
||||
m_EntryTransitions:
|
||||
- {fileID: 2173102097692100907}
|
||||
m_StateMachineTransitions: {}
|
||||
m_StateMachineBehaviours: []
|
||||
m_AnyStatePosition: {x: 440, y: -120, z: 0}
|
||||
m_EntryPosition: {x: -50, y: 40, z: 0}
|
||||
m_ExitPosition: {x: 800, y: 120, z: 0}
|
||||
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
|
||||
m_DefaultState: {fileID: 547046659980487195}
|
||||
--- !u!1102 &-5308216943816245398
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Attack
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: -357944484032141397}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 4020475e6c70aa048a82148d116ce43e, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &-4202185913788308745
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: SpecialATrigger
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: -6637492386302821641}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.063443124
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.9365569
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &-2643461505716286733
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Stuned
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: 1806250671443103744}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 52fa9d3c43342c340a069dd5553ba47e, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1102 &-2518930950459252685
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Talk
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: -1950858028210504261}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: be3b7cfe9b94a2245b65173df04b809e, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &-1950858028210504261
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.88
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &-1886661871251496921
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Death
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: 3074123931297886700}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 7c09c1592e02e1d4781e2237f587e37b, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &-1424066395699519718
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: DeathTrigger
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: -1886661871251496921}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.75
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &-515188086815602027
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.93333334
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &-357944484032141397
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.09746224
|
||||
m_TransitionOffset: 0.008018917
|
||||
m_ExitTime: 0.96503806
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &-148616555394202794
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.8854962
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!91 &9100000
|
||||
AnimatorController:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: DragonAnimation
|
||||
serializedVersion: 5
|
||||
m_AnimatorParameters:
|
||||
- m_Name: MoveTrigger
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: TalkTrigger
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: ClimbTrigger
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: JumpTrigger
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: AttackTrigger
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: StunedTrigger
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: DeathTrigger
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
- m_Name: SpecialATrigger
|
||||
m_Type: 9
|
||||
m_DefaultFloat: 0
|
||||
m_DefaultInt: 0
|
||||
m_DefaultBool: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
m_AnimatorLayers:
|
||||
- serializedVersion: 5
|
||||
m_Name: Base Layer
|
||||
m_StateMachine: {fileID: -6446312603455984934}
|
||||
m_Mask: {fileID: 0}
|
||||
m_Motions: []
|
||||
m_Behaviours: []
|
||||
m_BlendingMode: 0
|
||||
m_SyncedLayerIndex: -1
|
||||
m_DefaultWeight: 0
|
||||
m_IKPass: 0
|
||||
m_SyncedLayerAffectsTiming: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
--- !u!1101 &372893210610477594
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.93333334
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &547046659980487195
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Idle
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions: []
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 6d6c7bc3d845bd94fa5ca340f6e85bab, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &576596260694049973
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.8854962
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &1806250671443103744
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.9112426
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1109 &2173102097692100907
|
||||
AnimatorTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 1
|
||||
--- !u!1101 &3074123931297886700
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.85
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &3942610697287538840
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Climb
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: -515188086815602027}
|
||||
- {fileID: 372893210610477594}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: e265f65128d6bee49ad37d177488a169, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &6100266138257142490
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: JumpTrigger
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 6590903340607972877}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.75
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &6590903340607972877
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Jump
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: -148616555394202794}
|
||||
- {fileID: 576596260694049973}
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 426964a775883a7409ae0d7b17d3b7a6, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &6622731859202713379
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: StunedTrigger
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: -2643461505716286733}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.75
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &7279485870587762051
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: AttackTrigger
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: -5308216943816245398}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.040809453
|
||||
m_TransitionOffset: 0.004183833
|
||||
m_ExitTime: 0.96337444
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1102 &7645206152784429428
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Move
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions: []
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 379b8e5acca19224483e8c9eddeb815a, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1101 &8652467406494450390
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions:
|
||||
- m_ConditionMode: 1
|
||||
m_ConditionEvent: ClimbTrigger
|
||||
m_EventTreshold: 0
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 3942610697287538840}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.25
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.75
|
||||
m_HasExitTime: 0
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
--- !u!1101 &9207208706880946675
|
||||
AnimatorStateTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 547046659980487195}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 3
|
||||
m_TransitionDuration: 0.09670806
|
||||
m_TransitionOffset: 0
|
||||
m_ExitTime: 0.91071427
|
||||
m_HasExitTime: 1
|
||||
m_HasFixedDuration: 1
|
||||
m_InterruptionSource: 0
|
||||
m_OrderedInterruption: 1
|
||||
m_CanTransitionToSelf: 1
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d262a21f8d0526746b3123e549ab5730
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 9100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/DragonAnimation.controller
|
||||
uploadId: 600989
|
||||
-2935
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d6c7bc3d845bd94fa5ca340f6e85bab
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/IdleDragon.anim
|
||||
uploadId: 600989
|
||||
-10369
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 426964a775883a7409ae0d7b17d3b7a6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/JumpDragon.anim
|
||||
uploadId: 600989
|
||||
-9373
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 379b8e5acca19224483e8c9eddeb815a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/MoveDragon.anim
|
||||
uploadId: 600989
|
||||
-9404
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 322d2b45c4c3c6f47a34ff8e869b9047
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/SpecialAttackDragon.anim
|
||||
uploadId: 600989
|
||||
-7171
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52fa9d3c43342c340a069dd5553ba47e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/StunedDragon.anim
|
||||
uploadId: 600989
|
||||
-4935
File diff suppressed because it is too large
Load Diff
-15
@@ -1,15 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be3b7cfe9b94a2245b65173df04b809e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Animation/Dragon/TalkDragon.anim
|
||||
uploadId: 600989
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f069b8d2fd473544bbf97462a3bef6cf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-1471
File diff suppressed because it is too large
Load Diff
-14
@@ -1,14 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed7afb02a2246614da8d0e93b155ef48
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 260797
|
||||
packageName: Animated Dungeon Characters 2D
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/DungeonMonsters2D/Characters/DragonRed.prefab
|
||||
uploadId: 600989
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user