mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 04:05:44 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91c990da04 | ||
|
|
f15aab6fe7 |
@@ -42,7 +42,5 @@ common --tool_java_language_version=25
|
||||
common --tool_java_runtime_version=remotejdk_25
|
||||
|
||||
# Workspace status for build stamping (git commit, timestamp)
|
||||
# Only targets with stamp=1 will use these values; we intentionally
|
||||
# do NOT set "common --stamp" so non-stamped targets can share the
|
||||
# remote cache across CI workflows.
|
||||
common --workspace_status_command=tools/workspace_status.sh
|
||||
common --stamp
|
||||
|
||||
@@ -11,19 +11,3 @@ src/main/csharp/**/GUI[[:space:]]Pro[[:space:]]Kit*/**/*.ttf !filter !diff !merg
|
||||
src/main/csharp/**/Modern[[:space:]]UI[[:space:]]Pack/**/*.ttf !filter !diff !merge
|
||||
*.herodata filter=lfs diff=lfs merge=lfs -text
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Unity Smart Merge - use UnityYAMLMerge for Unity files
|
||||
# Requires configuring unityyamlmerge in ~/.gitconfig:
|
||||
# [merge]
|
||||
# tool = unityyamlmerge
|
||||
# [mergetool "unityyamlmerge"]
|
||||
# trustExitCode = false
|
||||
# cmd = '/Applications/Unity/Hub/Editor/*/Unity.app/Contents/Tools/UnityYAMLMerge' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"
|
||||
*.unity merge=unityyamlmerge
|
||||
*.prefab merge=unityyamlmerge
|
||||
*.asset merge=unityyamlmerge
|
||||
*.mat merge=unityyamlmerge
|
||||
*.anim merge=unityyamlmerge
|
||||
*.controller merge=unityyamlmerge
|
||||
*.physicsMaterial2D merge=unityyamlmerge
|
||||
*.physicMaterial merge=unityyamlmerge
|
||||
|
||||
@@ -41,8 +41,7 @@ jobs:
|
||||
set -ex
|
||||
|
||||
# Build auth server image (Go binary has explicit goos/goarch in BUILD.bazel)
|
||||
# --stamp is needed for authservice x_defs (git commit, build time)
|
||||
bazel build --stamp //ci:auth_server_image
|
||||
bazel build //ci:auth_server_image
|
||||
|
||||
# Save the resolved path before any other bazel command changes bazel-bin symlink
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/auth_server_image)
|
||||
|
||||
@@ -13,8 +13,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Show disk usage before cleanup
|
||||
run: |
|
||||
|
||||
@@ -23,16 +23,13 @@ on:
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
test:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -40,14 +37,6 @@ jobs:
|
||||
lfs: false
|
||||
- name: Check BUILD.bazel dependencies
|
||||
run: ./scripts/check_build_deps.sh --strict
|
||||
|
||||
test:
|
||||
runs-on: [self-hosted, bazel]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
- name: Run tests
|
||||
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
|
||||
- name: Collect failed test logs
|
||||
|
||||
@@ -62,10 +62,8 @@ jobs:
|
||||
|
||||
# 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 \
|
||||
@@ -91,13 +89,6 @@ jobs:
|
||||
echo "Admin: $ADMIN_PATH"
|
||||
echo "JFR Sidecar: $JFR_PATH"
|
||||
|
||||
- name: Upload warmup binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: warmup-binary
|
||||
path: scripts/bin/warmup
|
||||
retention-days: 1
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
@@ -119,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 ==="
|
||||
@@ -220,13 +192,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
sparse-checkout: |
|
||||
docker-compose.prod.yml
|
||||
nginx
|
||||
scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
@@ -235,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: |
|
||||
@@ -357,34 +322,29 @@ jobs:
|
||||
|
||||
echo "Using images: \$EAGLE_IMAGE, \$ADMIN_IMAGE, \$JFR_SIDECAR_IMAGE"
|
||||
|
||||
# Install crane for pulling OCI images (cached across deploys)
|
||||
CRANE_VERSION="v0.20.2"
|
||||
if [ ! -x crane ] || ! ./crane version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane \${CRANE_VERSION}..."
|
||||
rm -f crane
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/\${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" | tar xzf - crane
|
||||
chmod +x crane
|
||||
else
|
||||
echo "Using cached crane"
|
||||
fi
|
||||
# Install crane for pulling OCI images
|
||||
echo "Installing crane..."
|
||||
rm -f crane
|
||||
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
|
||||
chmod +x crane
|
||||
|
||||
# Pull all images in parallel, then load sequentially
|
||||
echo "Pulling all images in parallel..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar &
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar &
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar &
|
||||
docker pull nginx:alpine &
|
||||
docker pull certbot/certbot &
|
||||
wait
|
||||
# Pull and load all images
|
||||
echo "Pulling Eagle image..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar && docker load -i eagle.tar && rm eagle.tar
|
||||
|
||||
echo "Loading images..."
|
||||
docker load -i eagle.tar && rm eagle.tar
|
||||
docker load -i admin.tar && rm admin.tar
|
||||
echo "Pulling Admin image..."
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar && docker load -i admin.tar && rm admin.tar
|
||||
# Tag as :latest locally so docker-compose fallback uses correct image
|
||||
docker tag "\${ADMIN_IMAGE}" registry.digitalocean.com/eagle0/admin-server:latest
|
||||
docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
|
||||
|
||||
echo "All images pulled and loaded successfully"
|
||||
echo "Pulling JFR Sidecar image..."
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar && docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
|
||||
|
||||
# Pull other compose images
|
||||
docker pull nginx:alpine || true
|
||||
docker pull certbot/certbot || true
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# =================================================================
|
||||
# Verify Shardok connectivity before proceeding with deployment
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
name: Eagle Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by docker_build.yml which builds the same target
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/eagle_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
@@ -13,10 +23,6 @@ on:
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/eagle_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ on:
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -50,12 +46,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
|
||||
clean: true
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
@@ -65,9 +56,20 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build iOS Addressables
|
||||
run: ./ci/github_actions/build_ios_addressables.sh "$EAGLE0_BUILD_DIR/editor_ios_addressables.log"
|
||||
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
@@ -75,17 +77,6 @@ jobs:
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh iOS
|
||||
|
||||
- name: Purge CDN cache for iOS addressables
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["addressables/iOS/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
env:
|
||||
# Runner-specific build directory to allow parallel builds on multiple runners
|
||||
@@ -20,7 +21,9 @@ env:
|
||||
|
||||
jobs:
|
||||
build-unity:
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
outputs:
|
||||
xcode_project_path: ${{ steps.build.outputs.xcode_project_path }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -37,14 +40,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
clean: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
@@ -53,10 +51,22 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build iOS Unity Project
|
||||
id: build
|
||||
run: |
|
||||
./ci/github_actions/build_unity_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS"
|
||||
echo "xcode_project_path=$EAGLE0_BUILD_DIR/eagle0iOS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success()
|
||||
@@ -65,16 +75,18 @@ jobs:
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh iOS
|
||||
|
||||
- name: Purge CDN cache for iOS addressables
|
||||
if: success()
|
||||
env:
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
- name: Zip Xcode project for artifact
|
||||
run: |
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["addressables/iOS/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
cd $EAGLE0_BUILD_DIR
|
||||
# Use tar for speed - Xcode projects have many small files
|
||||
tar -czf eagle0iOS.tar.gz eagle0iOS
|
||||
|
||||
- name: Upload Xcode project
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xcode-project-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0iOS.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
@@ -83,12 +95,37 @@ jobs:
|
||||
name: editor_ios.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios.log
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
archive-and-upload:
|
||||
needs: build-unity
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
ci
|
||||
scripts
|
||||
|
||||
- name: Clean download directory
|
||||
run: rm -rf $EAGLE0_BUILD_DIR/eagle0iOS
|
||||
|
||||
- name: Download Xcode project
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: xcode-project-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}
|
||||
|
||||
- name: Extract Xcode project
|
||||
run: |
|
||||
cd $EAGLE0_BUILD_DIR
|
||||
tar -xzf eagle0iOS.tar.gz
|
||||
rm eagle0iOS.tar.gz
|
||||
ls -la eagle0iOS/
|
||||
|
||||
- name: Install Signing Certificate
|
||||
env:
|
||||
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
|
||||
@@ -147,23 +184,11 @@ jobs:
|
||||
- name: Upload to TestFlight
|
||||
if: ${{ github.event.inputs.skip_upload != 'true' }}
|
||||
env:
|
||||
# App Store Connect API Key (replaces deprecated altool with Apple ID)
|
||||
# Create at: https://appstoreconnect.apple.com/access/api
|
||||
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
|
||||
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
|
||||
run: |
|
||||
# Write API key to file (xcodebuild needs a file path)
|
||||
API_KEY_PATH=$(mktemp)
|
||||
echo "$APP_STORE_CONNECT_API_KEY" > "$API_KEY_PATH"
|
||||
export APP_STORE_CONNECT_API_KEY_PATH="$API_KEY_PATH"
|
||||
|
||||
chmod +x ./ci/github_actions/upload_testflight.sh
|
||||
./ci/github_actions/upload_testflight.sh "$EAGLE0_BUILD_DIR/archive/eagle0.xcarchive" "$APPLE_TEAM_ID" "$PROFILE_UUID"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$API_KEY_PATH"
|
||||
./ci/github_actions/upload_testflight.sh "$EAGLE0_BUILD_DIR/archive/eagle0.ipa"
|
||||
|
||||
- name: Upload IPA artifact
|
||||
# Only keep artifact if we skipped TestFlight upload (for debugging)
|
||||
@@ -181,3 +206,21 @@ jobs:
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
cleanup:
|
||||
needs: [build-unity, archive-and-upload]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delete intermediate artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
artifact_name="xcode-project-${{ github.run_id }}"
|
||||
echo "Deleting artifact: $artifact_name"
|
||||
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
|
||||
if [ -n "$artifact_id" ]; then
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
|
||||
fi
|
||||
|
||||
@@ -25,19 +25,25 @@ on:
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "ci/mac/**"
|
||||
pull_request:
|
||||
# On PRs, only build Mac when Mac-specific files change. Shared C#/proto
|
||||
# changes are covered by the Windows build — if it passes, Mac will too.
|
||||
paths:
|
||||
- ".github/workflows/mac_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "src/main/protobuf/net/eagle0/common/**"
|
||||
- "src/main/protobuf/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/api/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/common/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/views/**"
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/build_sparkle_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_submit.sh"
|
||||
- "scripts/notarize_wait.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "ci/mac/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -47,10 +53,6 @@ on:
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # Required to delete artifacts after deploy
|
||||
@@ -83,30 +85,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
clean: true # Remove untracked files like old SparklePlugin.bundle
|
||||
fetch-depth: 0 # For version numbering from git history
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA — clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA — keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ — clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
@@ -115,12 +96,19 @@ jobs:
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh mac
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build Mac Unity
|
||||
run: ./ci/github_actions/build_unity_mac.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
@@ -297,8 +285,6 @@ jobs:
|
||||
needs: [build-and-sign, wait-notarization]
|
||||
if: needs.build-and-sign.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -321,7 +307,6 @@ jobs:
|
||||
rm eagle0.app.zip
|
||||
|
||||
- name: Deploy Mac Build
|
||||
id: deploy-mac
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -352,37 +337,44 @@ jobs:
|
||||
|
||||
rm "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
# Export version for notify step
|
||||
echo "DEPLOYED_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
# Artifact cleanup is handled by the cleanup job on ubuntu-latest
|
||||
- name: Notify clients of update
|
||||
if: success()
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
run: |
|
||||
# Wait for CDN cache to clear
|
||||
sleep 60
|
||||
|
||||
# Notify via admin server (required=false for normal deploys)
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=$DEPLOYED_VERSION&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
- name: Delete this run's Mac app artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Delete this run's artifacts (names include run ID to avoid conflicts)
|
||||
for artifact_name in signed-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }}; do
|
||||
echo "Deleting artifact: $artifact_name"
|
||||
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
|
||||
if [ -n "$artifact_id" ]; then
|
||||
echo "Deleting artifact ID: $artifact_id"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
|
||||
fi
|
||||
done
|
||||
echo "Cleanup complete"
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
notify-mac:
|
||||
needs: deploy
|
||||
if: needs.deploy.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Purge CDN cache and notify clients
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
# Purge CDN cache so clients get the latest files immediately
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["mac/*", "addressables/StandaloneOSX/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=${{ needs.deploy.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
# Cleanup job runs regardless of success/failure to prevent artifact accumulation
|
||||
cleanup:
|
||||
needs: [build-and-sign, wait-notarization, deploy, notify-mac]
|
||||
needs: [build-and-sign, wait-notarization, deploy]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ on:
|
||||
default: 'true'
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: shardok-arm64-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -64,14 +60,12 @@ jobs:
|
||||
if [ "$MAGIC" = "7f454c46" ]; then
|
||||
echo "SUCCESS: Binary is ELF format (Linux)"
|
||||
# Check if it's ARM64 (e_machine = 0xB7 = 183 for aarch64)
|
||||
# od -tx2 reads as a 16-bit value in host byte order (little-endian on macOS),
|
||||
# so the little-endian ELF bytes b7 00 are displayed as 00b7.
|
||||
E_MACHINE=$(od -An -j18 -N2 -tx2 "$LINUX_BIN" | tr -d ' ')
|
||||
echo "ELF e_machine: $E_MACHINE"
|
||||
if [ "$E_MACHINE" = "00b7" ]; then
|
||||
if [ "$E_MACHINE" = "b700" ]; then
|
||||
echo "SUCCESS: Binary is ARM64 (aarch64)"
|
||||
else
|
||||
echo "WARNING: Binary e_machine is $E_MACHINE (expected 00b7 for aarch64)"
|
||||
echo "WARNING: Binary e_machine is $E_MACHINE (expected b700 for aarch64)"
|
||||
fi
|
||||
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
|
||||
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
|
||||
@@ -134,19 +128,20 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get crane (cached across runs to avoid rebuilding bazel target,
|
||||
# which would discard the ARM64 analysis cache due to platform change)
|
||||
CRANE_VERSION="v0.20.2"
|
||||
CRANE_DIR="${HOME}/.local/bin"
|
||||
CRANE="${CRANE_DIR}/crane"
|
||||
if [ ! -x "$CRANE" ] || ! "$CRANE" version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane ${CRANE_VERSION}..."
|
||||
mkdir -p "$CRANE_DIR"
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Darwin_arm64.tar.gz" | tar xzf - -C "$CRANE_DIR" crane
|
||||
chmod +x "$CRANE"
|
||||
else
|
||||
echo "Using cached crane at $CRANE"
|
||||
# Build a push target to get crane in runfiles
|
||||
bazel build //ci:eagle_server_push
|
||||
|
||||
# Find the Darwin crane binary
|
||||
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
|
||||
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
|
||||
if [ -z "$CRANE" ]; then
|
||||
CRANE=$(find "$RUNFILES" -name crane 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
|
||||
echo "ERROR: crane not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push with arm64-prefixed SHA tag (same repo as x86, different tag)
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
name: Shardok Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by shardok_arm64_build.yml which builds the same target
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/proto/net/eagle0/shardok/**'
|
||||
- 'src/main/proto/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
@@ -14,10 +25,6 @@ on:
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ on:
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/restore_library.sh"
|
||||
- "ci/github_actions/persist_library.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
@@ -34,16 +36,14 @@ on:
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/restore_library.sh"
|
||||
- "ci/github_actions/persist_library.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "MODULE.bazel"
|
||||
- "WORKSPACE"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -54,8 +54,6 @@ env:
|
||||
jobs:
|
||||
windows-unity:
|
||||
runs-on: [self-hosted, macOS, unity-windows]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.get-version.outputs.deployed_version }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -72,28 +70,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA — clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA — keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ — clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
clean: true # Remove untracked files from previous builds
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
@@ -101,13 +78,17 @@ jobs:
|
||||
git lfs pull
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
- name: Build Windows unity
|
||||
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
@@ -145,12 +126,21 @@ jobs:
|
||||
# Cleanup
|
||||
rm -f /tmp/manifest_signing_key
|
||||
|
||||
- name: Export deployed version
|
||||
id: get-version
|
||||
- name: Notify clients of update
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
run: |
|
||||
# Wait for CDN cache to clear
|
||||
sleep 60
|
||||
|
||||
# Get version from manifest
|
||||
VERSION=$(grep "^version=" /tmp/unity_manifest.txt | cut -d= -f2 || date +%Y.%m.%d)
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Notify via admin server (required=false for normal deploys)
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=$VERSION&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
- name: Archive build log
|
||||
if: success() || failure()
|
||||
@@ -161,25 +151,4 @@ jobs:
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
notify-windows:
|
||||
needs: windows-unity
|
||||
if: needs.windows-unity.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Purge CDN cache and notify clients
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
# Purge CDN cache so clients get the latest files immediately
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["windows/*", "addressables/StandaloneWindows64/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=${{ needs.windows-unity.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
@@ -37,8 +37,5 @@ scripts/refresh_name_layers/refresh_name_layers.zip
|
||||
.metals
|
||||
api_keys.txt
|
||||
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/GeneratedProtos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
|
||||
node_modules/
|
||||
tools/map_generator/output/
|
||||
|
||||
@@ -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
@@ -31,17 +31,6 @@ fi
|
||||
|
||||
echo "Found Xcode project: $XCODEPROJ"
|
||||
|
||||
# Ensure export compliance key is set in Info.plist
|
||||
# This bypasses the "Missing Compliance" prompt in App Store Connect.
|
||||
# Set here rather than relying solely on Unity's DeepLinkPostProcessor,
|
||||
# which depends on UNITY_IOS being defined at script compilation time.
|
||||
INFO_PLIST="$XCODE_PROJECT_PATH/Info.plist"
|
||||
if [ -f "$INFO_PLIST" ]; then
|
||||
/usr/libexec/PlistBuddy -c "Set :ITSAppUsesNonExemptEncryption false" "$INFO_PLIST" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool false" "$INFO_PLIST"
|
||||
echo "Set ITSAppUsesNonExemptEncryption=false in Info.plist"
|
||||
fi
|
||||
|
||||
# Unity always generates "Unity-iPhone" as the main app scheme
|
||||
SCHEME="Unity-iPhone"
|
||||
echo "Using scheme: $SCHEME"
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Persist Unity Library/ cache to persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
#
|
||||
# Note: Library/Bee/ is excluded because it contains DAG files with hardcoded
|
||||
# file paths that become stale when project files change. This prevents
|
||||
# "Data at the root level is invalid" XML errors from stale references.
|
||||
|
||||
set -uxo pipefail
|
||||
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "persist Library/ to $CACHE_DIR (excluding Bee/)"
|
||||
|
||||
# rsync may exit with code 23 ("partial transfer due to error") if Unity's
|
||||
# temporary files vanish during the copy. This is acceptable for a cache.
|
||||
/usr/bin/rsync -rtlDvq --exclude='Bee/' src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ "$CACHE_DIR/"
|
||||
rsync_exit=$?
|
||||
|
||||
if [ $rsync_exit -eq 0 ]; then
|
||||
exit 0
|
||||
elif [ $rsync_exit -eq 23 ]; then
|
||||
echo "Warning: rsync exited with 23 (some files vanished during copy). This is expected for Unity temp files."
|
||||
exit 0
|
||||
else
|
||||
echo "Error: rsync failed with exit code $rsync_exit"
|
||||
exit $rsync_exit
|
||||
fi
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restore Unity Library/ cache from persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "restore Library/ from $CACHE_DIR"
|
||||
/bin/mkdir -p "$CACHE_DIR"
|
||||
/usr/bin/rsync -rtlDvq "$CACHE_DIR/" src/main/csharp/net/eagle0/clients/unity/eagle0/Library/
|
||||
@@ -1,102 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Upload xcarchive to TestFlight using App Store Connect API Key
|
||||
# This replaces the deprecated altool which was removed in Xcode 14+
|
||||
#
|
||||
# Uses xcodebuild -exportArchive with destination=upload, which is Apple's
|
||||
# recommended approach for CI/CD pipelines.
|
||||
# Upload IPA to TestFlight using Apple ID credentials (same as Mac notarization)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
# Ensure xcodebuild uses Xcode.app, not Command Line Tools
|
||||
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
IPA_PATH=${1:?Usage: upload_testflight.sh <ipa_path>}
|
||||
|
||||
ARCHIVE_PATH=${1:?Usage: upload_testflight.sh <xcarchive_path> <team_id> <profile_uuid>}
|
||||
TEAM_ID=${2:?Missing team ID}
|
||||
PROFILE_UUID=${3:?Missing provisioning profile UUID}
|
||||
|
||||
if [ ! -d "$ARCHIVE_PATH" ]; then
|
||||
echo "Error: xcarchive not found: $ARCHIVE_PATH"
|
||||
if [ ! -f "$IPA_PATH" ]; then
|
||||
echo "Error: IPA file not found: $IPA_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Uploading to TestFlight: $ARCHIVE_PATH"
|
||||
echo "Uploading to TestFlight: $IPA_PATH"
|
||||
|
||||
# Requires App Store Connect API Key:
|
||||
# - APP_STORE_CONNECT_API_KEY_ID: Key ID from App Store Connect
|
||||
# - APP_STORE_CONNECT_API_ISSUER_ID: Issuer ID from App Store Connect
|
||||
# - APP_STORE_CONNECT_API_KEY_PATH: Path to .p8 private key file
|
||||
#
|
||||
# To create an API key:
|
||||
# 1. Go to https://appstoreconnect.apple.com/access/api
|
||||
# 2. Click the + button to create a new key
|
||||
# 3. Give it a name and Admin or App Manager access
|
||||
# 4. Download the .p8 file (you can only download it once!)
|
||||
# 5. Note the Key ID and Issuer ID shown on the page
|
||||
# Uses same credentials as Mac notarization:
|
||||
# - APPLE_ID: Your Apple ID email
|
||||
# - APP_SPECIFIC_PASSWORD: App-specific password from appleid.apple.com
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_KEY_ID:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_KEY_ID environment variable not set"
|
||||
echo "Create an API key at https://appstoreconnect.apple.com/access/api"
|
||||
if [ -z "${APPLE_ID:-}" ]; then
|
||||
echo "Error: APPLE_ID environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_ISSUER_ID:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_ISSUER_ID environment variable not set"
|
||||
if [ -z "${APP_SPECIFIC_PASSWORD:-}" ]; then
|
||||
echo "Error: APP_SPECIFIC_PASSWORD environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_KEY_PATH:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_KEY_PATH environment variable not set"
|
||||
echo "Uploading with xcrun altool..."
|
||||
|
||||
# Capture output to check for errors (altool may return 0 even on failure)
|
||||
OUTPUT=$(xcrun altool --upload-app \
|
||||
--type ios \
|
||||
--file "$IPA_PATH" \
|
||||
--username "$APPLE_ID" \
|
||||
--password "$APP_SPECIFIC_PASSWORD" 2>&1) || true
|
||||
|
||||
echo "$OUTPUT"
|
||||
|
||||
# Check for error indicators in output
|
||||
if echo "$OUTPUT" | grep -q "ERROR:"; then
|
||||
echo "ERROR: Upload failed. See error messages above."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$APP_STORE_CONNECT_API_KEY_PATH" ]; then
|
||||
echo "Error: API key file not found: $APP_STORE_CONNECT_API_KEY_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create export options plist with upload destination
|
||||
EXPORT_OPTIONS_PLIST=$(mktemp)
|
||||
trap "rm -f $EXPORT_OPTIONS_PLIST" EXIT
|
||||
|
||||
cat > "$EXPORT_OPTIONS_PLIST" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>teamID</key>
|
||||
<string>$TEAM_ID</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>signingCertificate</key>
|
||||
<string>Apple Distribution: Daniel Crosby (UWJ88DX8WQ)</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>net.eagle0.eagle</key>
|
||||
<string>$PROFILE_UUID</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "Export options:"
|
||||
cat "$EXPORT_OPTIONS_PLIST"
|
||||
|
||||
echo "Uploading to App Store Connect..."
|
||||
|
||||
# Use xcodebuild to upload with App Store Connect API authentication
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
|
||||
-authenticationKeyPath "$APP_STORE_CONNECT_API_KEY_PATH" \
|
||||
-authenticationKeyID "$APP_STORE_CONNECT_API_KEY_ID" \
|
||||
-authenticationKeyIssuerID "$APP_STORE_CONNECT_API_ISSUER_ID"
|
||||
|
||||
echo "Upload complete! Check App Store Connect for processing status."
|
||||
echo "The build should appear in TestFlight within 15-30 minutes after processing."
|
||||
|
||||
@@ -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,150 +0,0 @@
|
||||
# rules_apple Workspace Separation
|
||||
|
||||
This document explains why `rules_apple` is built in a separate Bazel workspace (`sparkle_workspace/`) and what conditions need to be met before it can be reintegrated into the main workspace.
|
||||
|
||||
## Background
|
||||
|
||||
The main workspace cannot include `rules_apple` due to transitive dependency conflicts with `rules_swift`. Multiple dependencies require different major versions of `rules_swift`:
|
||||
|
||||
| Dependency | rules_swift Version | Compatibility Level |
|
||||
|------------|---------------------|---------------------|
|
||||
| grpc | 3.x | 3 |
|
||||
| flatbuffers | 2.x | 2 |
|
||||
| rules_apple | 2.x | 2 |
|
||||
|
||||
Bzlmod's `single_version_override` can force a single version, but compatibility levels 2 and 3 are incompatible. Forcing `rules_swift` 3.x (required for grpc) breaks `rules_apple` and `flatbuffers`.
|
||||
|
||||
## Current Solution
|
||||
|
||||
The `SparklePlugin` (the only component requiring `rules_apple`) is built in an isolated workspace:
|
||||
|
||||
```
|
||||
sparkle_workspace/
|
||||
├── MODULE.bazel # Minimal deps: apple_support, rules_apple, sparkle
|
||||
├── BUILD.bazel # Builds SparklePlugin.bundle
|
||||
├── SparklePlugin.m # Objective-C source
|
||||
├── Info.plist # Bundle metadata
|
||||
└── external/
|
||||
└── BUILD.sparkle # Build file for Sparkle framework
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Build script**: `scripts/build_sparkle_plugin.sh` builds the plugin from the separate workspace
|
||||
2. **Output**: The built `SparklePlugin.bundle` is placed in `Assets/Plugins/macOS/`
|
||||
3. **CI integration**: `mac_build.yml` calls `inject_sparkle.sh` to embed Sparkle into the Mac app
|
||||
|
||||
The main workspace uses `single_version_override` for `rules_swift` 3.x to satisfy grpc, and includes a comment noting that `rules_apple` is intentionally excluded.
|
||||
|
||||
## What SparklePlugin Does
|
||||
|
||||
SparklePlugin is a native macOS library that:
|
||||
- Initializes the [Sparkle](https://sparkle-project.org/) auto-update framework
|
||||
- Exposes C functions for Unity to call via P/Invoke:
|
||||
- `SparklePlugin_Initialize`
|
||||
- `SparklePlugin_CheckForUpdates`
|
||||
- `SparklePlugin_CheckForUpdatesInBackground`
|
||||
- `SparklePlugin_IsCheckingForUpdates`
|
||||
- `SparklePlugin_GetAutomaticallyChecksForUpdates`
|
||||
- `SparklePlugin_SetAutomaticallyChecksForUpdates`
|
||||
- `SparklePlugin_IsRunningFromReadOnlyVolume`
|
||||
- `SparklePlugin_ShowDMGWarning`
|
||||
|
||||
## Conditions for Reintegration
|
||||
|
||||
To move `rules_apple` back into the main workspace, **all** of the following must be true:
|
||||
|
||||
### 1. rules_swift Version Alignment
|
||||
|
||||
Check if grpc, flatbuffers, and rules_apple all support the same `rules_swift` major version:
|
||||
|
||||
```bash
|
||||
# Check what rules_swift version each dependency requires
|
||||
bazel mod graph 2>&1 | grep -A2 "rules_swift"
|
||||
```
|
||||
|
||||
Or check BCR directly:
|
||||
- https://registry.bazel.build/modules/grpc
|
||||
- https://registry.bazel.build/modules/flatbuffers
|
||||
- https://registry.bazel.build/modules/rules_apple
|
||||
|
||||
**Test**: After updating versions, verify you can add this to MODULE.bazel without errors:
|
||||
```python
|
||||
bazel_dep(name = "rules_apple", version = "X.Y.Z")
|
||||
```
|
||||
|
||||
### 2. Build Test
|
||||
|
||||
If the dependency can be added, test that both the main build and SparklePlugin work:
|
||||
|
||||
```bash
|
||||
# Main workspace builds
|
||||
bazel build //src/main/scala/net/eagle0/eagle:eagle_server
|
||||
bazel build //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
bazel build //ci:eagle_server_image
|
||||
|
||||
# SparklePlugin builds (would move to main workspace)
|
||||
bazel build //sparkle:SparklePlugin # After moving files
|
||||
```
|
||||
|
||||
### 3. Files to Move
|
||||
|
||||
When reintegrating, move these from `sparkle_workspace/` to the main workspace:
|
||||
|
||||
| Source | Destination |
|
||||
|--------|-------------|
|
||||
| `sparkle_workspace/SparklePlugin.m` | `src/main/objc/net/eagle0/sparkle/SparklePlugin.m` |
|
||||
| `sparkle_workspace/Info.plist` | `src/main/objc/net/eagle0/sparkle/Info.plist` |
|
||||
| `sparkle_workspace/BUILD.bazel` | `src/main/objc/net/eagle0/sparkle/BUILD.bazel` |
|
||||
| `sparkle_workspace/external/BUILD.sparkle` | (inline into MODULE.bazel http_archive) |
|
||||
|
||||
### 4. MODULE.bazel Changes
|
||||
|
||||
Add to the main MODULE.bazel:
|
||||
|
||||
```python
|
||||
bazel_dep(name = "rules_apple", version = "X.Y.Z", repo_name = "build_bazel_rules_apple")
|
||||
|
||||
# Sparkle framework
|
||||
http_archive(
|
||||
name = "sparkle",
|
||||
build_file_content = """...""", # Contents from external/BUILD.sparkle
|
||||
sha256 = "...",
|
||||
url = "https://github.com/sparkle-project/Sparkle/releases/download/...",
|
||||
)
|
||||
```
|
||||
|
||||
Remove the `single_version_override` for `rules_swift` if no longer needed.
|
||||
|
||||
### 5. Update Build Scripts
|
||||
|
||||
- Update `scripts/build_sparkle_plugin.sh` to build from main workspace
|
||||
- Update `scripts/build_plugins.sh` if it references the separate workspace
|
||||
- Verify `mac_build.yml` still works
|
||||
|
||||
### 6. Cleanup
|
||||
|
||||
After successful integration:
|
||||
```bash
|
||||
rm -rf sparkle_workspace/
|
||||
```
|
||||
|
||||
Update comments in MODULE.bazel that reference the separate workspace.
|
||||
|
||||
## Version History
|
||||
|
||||
| Date | Event |
|
||||
|------|-------|
|
||||
| 2026-02-04 | Separated sparkle_workspace (PR #5882) to enable Bazel 8 upgrade |
|
||||
| 2026-02-05 | Upgraded to Bazel 8.5.1 (PR #5883) |
|
||||
|
||||
## Monitoring
|
||||
|
||||
Periodically check if the upstream dependencies have aligned:
|
||||
|
||||
1. **grpc releases**: https://github.com/grpc/grpc/releases
|
||||
2. **flatbuffers releases**: https://github.com/google/flatbuffers/releases
|
||||
3. **rules_apple releases**: https://github.com/bazelbuild/rules_apple/releases
|
||||
4. **BCR updates**: https://registry.bazel.build/
|
||||
|
||||
When a new version of any of these is released, check if the `rules_swift` requirements have converged.
|
||||
@@ -77,4 +77,3 @@ Larger set of goals in [The Big Eagle TODO](https://docs.google.com/document/d/1
|
||||
## Nice to have
|
||||
|
||||
- [x] "What's new" changelog (fetch JSON, show entries since last launch)
|
||||
- [ ] Windows code signing ([Azure Artifact Signing](https://azure.microsoft.com/en-us/products/artifact-signing/), ~$10/mo) to eliminate SmartScreen "unknown publisher" warning
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
# Tutorial Battle System
|
||||
|
||||
This document describes the tutorial battle system for first-time players. The system provides a scripted introductory battle that teaches combat basics while telling a story.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
When a new player starts their first game in tutorial mode, they experience:
|
||||
|
||||
1. **Narrative Intro** - Story screens introducing the scenario
|
||||
2. **Immediate Battle** - Skip strategic map, start directly in combat
|
||||
3. **Scripted Flee** - Enemy flees when player is "on the ropes"
|
||||
4. **Reinforcements** - Allied heroes arrive mid-battle
|
||||
5. **Heroes Join** - Reinforcement heroes join player's faction after battle
|
||||
|
||||
---
|
||||
|
||||
## Battle Configuration
|
||||
|
||||
### Defender (Player - John Ranil)
|
||||
- **Heroes**: John Ranil + 2 random vassals (3 total)
|
||||
- **Units**:
|
||||
- 2x Light Infantry (300 troops each, 60 training/armament)
|
||||
- 1x Longbowmen (200 troops, 60 training/armament)
|
||||
- **Province**: 14 (Onmaa) with 40 support, 500 gold, 3000 food
|
||||
- **Restriction**: Cannot flee (must defend)
|
||||
|
||||
### Attacker (Ikhaan Tarn)
|
||||
- **Heroes**: Ikhaan Tarn + 2 sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 1x Heavy Cavalry (600 troops, 80 training/armament)
|
||||
- 1x Heavy Infantry (500 troops, 80 training/armament)
|
||||
- 1x Longbowmen (300 troops, 80 training/armament)
|
||||
- **Origin Province**: 32
|
||||
|
||||
### Flee Trigger
|
||||
Attacker flees when ANY of these conditions are met:
|
||||
- Player loses 1 unit
|
||||
- 5 rounds have passed
|
||||
|
||||
### Reinforcements
|
||||
When Tarn flees, these heroes arrive as player reinforcements:
|
||||
- Elena Fyar
|
||||
- Hedrick
|
||||
- The Boulder
|
||||
|
||||
---
|
||||
|
||||
## Implementation Architecture
|
||||
|
||||
### Phase 1: Narrative Intro System
|
||||
|
||||
**Proto Changes:**
|
||||
- `game_state_view.proto`: Added `TutorialNarrativeScreen` message and `pending_narrative_screens` field
|
||||
- `game_parameters.proto`: Added `TutorialNarrativeScreen` and `tutorial_narrative_screens` field
|
||||
|
||||
**Unity Client:**
|
||||
- `NarrativeScreenController.cs`: Modal screen with title, body text, optional image
|
||||
- `EagleGameController.cs`: Checks for pending narrative screens on game start
|
||||
|
||||
**Narrative Content** (defined in `tutorial_game_parameters.json`):
|
||||
```
|
||||
Screen 1: "The Engineer of Onmaa" - Player backstory
|
||||
Screen 2: "The Traitor Arrives" - Tarn's attack
|
||||
Screen 3: "Defend Your Home" - Call to action
|
||||
```
|
||||
|
||||
### Phase 2: Auto-Start Battle
|
||||
|
||||
**Configuration:**
|
||||
- `tutorial_game_parameters.json`: Contains `tutorialBattle` config with attacker, target, flee conditions
|
||||
- `game_parameters.proto`: `TutorialBattleConfig` message
|
||||
|
||||
**Game Creation:**
|
||||
- `NewGameCreation.scala`: `setupTutorialBattle()` method creates:
|
||||
- `HostileArmyGroup` with `Attacking` status (bypasses decision phase)
|
||||
- `Army` for defender
|
||||
- Both added to target province
|
||||
|
||||
**Battle Creation:**
|
||||
- When round advances to `BattleRequest` phase, `RequestBattlesAction` automatically creates the `ShardokBattle`
|
||||
|
||||
### Phase 3: Scripted Flee Mechanism
|
||||
|
||||
**C++ Component:**
|
||||
- `TutorialBattleController.hpp/cpp`: Controller for scripted battle logic
|
||||
|
||||
**Key Methods:**
|
||||
```cpp
|
||||
// Check all events and execute triggered actions (called at end of each round)
|
||||
std::vector<ActionResult> CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator);
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- `ShardokEngine.cpp`: Calls `CheckAndExecuteEvents()` at end of each round in `HandlePlayerTurnEnd()`
|
||||
- Events fire in config order, each event fires at most once
|
||||
|
||||
### Phase 4: Mid-Battle Reinforcements
|
||||
|
||||
**Action Types** (in `action_type.proto`):
|
||||
- `TUTORIAL_ENEMY_FLED = 78`
|
||||
- `TUTORIAL_REINFORCEMENTS_ARRIVED = 79`
|
||||
|
||||
**Implementation (TODO):**
|
||||
- When scripted flee triggers, create reinforcement units in defender's reserves
|
||||
- Units have `UnitStatus_RESERVE_UNIT` status
|
||||
- Reinforcements enter battle on subsequent turns
|
||||
|
||||
### Phase 5: Tutorial Popups
|
||||
|
||||
**Content Definitions** (in `TutorialContentDefinitions.cs`):
|
||||
- `tutorial_battle_started`: "Defend your province!"
|
||||
- `tutorial_enemy_fled`: "The enemy retreats!"
|
||||
- `tutorial_reinforcements_arrived`: "Allies have arrived!"
|
||||
- `tutorial_battle_victory`: Victory celebration
|
||||
|
||||
**Trigger Integration:**
|
||||
- `TutorialTriggerRegistry.cs`: Handles tutorial action types from `ActionResultView`
|
||||
- `ShardokGameController.cs`: Detects tutorial events and triggers popups
|
||||
|
||||
### Phase 6: Post-Battle Hero Joining
|
||||
|
||||
**Implementation (TODO):**
|
||||
- `ResolveBattleAction.scala`: Check if battle was tutorial and defender won
|
||||
- Create `TutorialHeroJoined` action results adding reinforcement heroes to player faction
|
||||
- Set appropriate loyalty, vigor, location for joined heroes
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
|
||||
### New Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `TutorialBattleController.hpp/cpp` | C++ scripted flee and reinforcement logic |
|
||||
| `NarrativeScreenController.cs` | Unity narrative screen display |
|
||||
|
||||
### Modified Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `tutorial_game_parameters.json` | Battle configuration and narrative content |
|
||||
| `game_state_view.proto` | Narrative screens field |
|
||||
| `game_parameters.proto` | Tutorial battle config |
|
||||
| `player_info.proto` | Tutorial battle flags for Shardok |
|
||||
| `action_type.proto` | Tutorial action types |
|
||||
| `NewGameCreation.scala` | Auto-start battle setup |
|
||||
| `EagleGameController.cs` | Narrative display integration |
|
||||
| `TutorialContentDefinitions.cs` | Battle tutorial content |
|
||||
| `TutorialTriggerRegistry.cs` | Tutorial event triggers |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Implementation
|
||||
|
||||
### Disable Player Flee
|
||||
In `FleeCommandFactory.cpp`:
|
||||
- Check if tutorial mode is enabled
|
||||
- If defender, don't offer flee commands
|
||||
|
||||
### Reinforcement Units
|
||||
In `TutorialBattleController::ExecuteScriptedFlee()`:
|
||||
- Create `Unit` objects for Elena Fyar, Hedrick, The Boulder
|
||||
- Add to defender's reserve units
|
||||
- Generate `TUTORIAL_REINFORCEMENTS_ARRIVED` action
|
||||
|
||||
### Post-Battle Hero Joining
|
||||
In Eagle's battle resolution:
|
||||
- Detect tutorial battle completion
|
||||
- Create heroes in player's faction with proper stats
|
||||
- Generate notification action results
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] New user sees narrative screens before battle
|
||||
- [ ] Battle starts immediately after narratives (no strategic map)
|
||||
- [ ] Player has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Attacker has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Player cannot flee
|
||||
- [ ] Tarn flees after player loses 1 unit
|
||||
- [ ] Tarn flees after 5 rounds (if no units lost)
|
||||
- [ ] Reinforcements appear when Tarn flees
|
||||
- [ ] Tutorial popups appear at correct moments
|
||||
- [ ] Reinforcement heroes join faction after battle victory
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### tutorial_game_parameters.json
|
||||
```json
|
||||
{
|
||||
"tutorialBattle": {
|
||||
"attackerFactionHead": "Ikhaan Tarn",
|
||||
"targetProvinceId": 14,
|
||||
"fleeAfterDefenderUnitsLost": 1,
|
||||
"fleeAfterRounds": 5,
|
||||
"reinforcements": ["Elena Fyar", "Hedrick", "The Boulder"]
|
||||
},
|
||||
"tutorialNarrativeScreens": [
|
||||
{ "title": "...", "bodyText": "...", "imagePath": "" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### TutorialBattleConfig Proto (Shardok)
|
||||
```protobuf
|
||||
message TutorialBattleConfig {
|
||||
bool enabled = 1;
|
||||
repeated TutorialEvent events = 2;
|
||||
repeated UnitInitialSize initial_sizes = 3; // For damage tracking
|
||||
}
|
||||
|
||||
message TutorialEvent {
|
||||
string event_id = 1;
|
||||
TutorialTrigger trigger = 2;
|
||||
TutorialAction action = 3;
|
||||
}
|
||||
|
||||
message TutorialTrigger {
|
||||
oneof trigger_type {
|
||||
RoundTrigger after_round = 1;
|
||||
UnitsLostTrigger units_lost = 2;
|
||||
DamageTakenTrigger damage_taken = 3;
|
||||
UnitKilledTrigger unit_killed = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message TutorialAction {
|
||||
oneof action_type {
|
||||
FleeAction flee = 1;
|
||||
ReinforcementsAction reinforcements = 2; // Uses CommonUnit
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,113 +0,0 @@
|
||||
# AI Quest Completion Behavior
|
||||
|
||||
This document describes which quests the AI attempts to complete proactively to recruit unaffiliated heroes.
|
||||
|
||||
## Overview
|
||||
|
||||
The AI attempts to complete quests via `FulfillQuestsCommandSelector`, which is invoked by `MidGameAIClient.chosenFulfillEasyQuestsCommand`. The AI only considers quests from unaffiliated heroes in provinces ruled by a faction leader.
|
||||
|
||||
## Quests the AI Actively Completes
|
||||
|
||||
### Diplomacy Quests
|
||||
|
||||
| Quest | Handler | Conditions |
|
||||
|-------|---------|------------|
|
||||
| `AllianceQuest` | `AllianceQuestCommandChooser` | Target faction must meet trust conditions for alliance and not already be in an alliance |
|
||||
| `TruceWithFactionQuest` | `TruceWithFactionQuestCommandChooser` | Target faction must meet trust conditions for truce |
|
||||
| `TruceCountQuest` | `TruceCountQuestCommandChooser` | Picks a random faction that meets trust conditions and isn't already in a truce/alliance |
|
||||
|
||||
### Resource Giving Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `AlmsToProvinceQuest` | `AlmsToProvinceQuestCommandChooser` | Gives food to the specified province |
|
||||
| `AlmsAcrossRealmQuest` | `AlmsAcrossRealmQuestCommandChooser` | Gives food from the province with the largest surplus |
|
||||
| `GiveToHeroesInProvinceQuest` | `GiveToHeroesInProvinceQuestCommandChooser` | Gives gold to the hero with the lowest loyalty in the specified province |
|
||||
| `GiveToHeroesAcrossRealmQuest` | `GiveToHeroesAcrossRealmQuestCommandChooser` | Gives gold from the province with the most gold available |
|
||||
|
||||
### Province Development Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `ImproveAgricultureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Agriculture type |
|
||||
| `ImproveEconomyQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Economy type |
|
||||
| `ImproveInfrastructureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Infrastructure type |
|
||||
|
||||
### Other Quests
|
||||
|
||||
| Quest | Handler | Conditions |
|
||||
|-------|---------|------------|
|
||||
| `DismissSpecificVassalQuest` | `DismissSpecificVassalCommandChooser` | Only if province has more than 2 heroes AND the unaffiliated hero's power >= target hero's power * `RequiredPowerMultiplierForDismiss` |
|
||||
|
||||
## Quests the AI Does Not Attempt to Complete
|
||||
|
||||
The following quests have no handler in `FulfillQuestsCommandSelector` and must be completed naturally through gameplay:
|
||||
|
||||
### Combat/Military Quests
|
||||
- `DefeatFactionQuest` - Defeat a specific faction
|
||||
- `GrandArmyQuest` - Accumulate a large number of troops
|
||||
- `UpgradeBattalionQuest` - Upgrade a battalion to minimum armament/training
|
||||
- `WinBattleOutnumberedQuest` - Win a battle while outnumbered
|
||||
- `WinBattlesQuest` - Win a number of battles
|
||||
- `RescueImprisonedLeaderQuest` - Rescue an imprisoned leader from another faction
|
||||
|
||||
### Expansion Quests
|
||||
- `ExpandToProvincesQuest` - Expand to control a certain number of provinces
|
||||
- `SpecificExpansionQuest` - Conquer a specific province
|
||||
- `BorderSecurityQuest` - Have troops in a border province
|
||||
|
||||
### Prisoner Quests
|
||||
- `ExecutePrisonerQuest` - Execute a specific prisoner
|
||||
- `ExilePrisonerQuest` - Exile a specific prisoner
|
||||
- `ReleasePrisonerQuest` - Release a specific prisoner
|
||||
- `ReturnPrisonerQuest` - Return a prisoner to their faction
|
||||
- `ReleaseAllPrisonersQuest` - Release all prisoners
|
||||
|
||||
### Province Order Quests
|
||||
- `DevelopProvincesQuest` - Maintain provinces in Develop order for months
|
||||
- `MobilizeProvincesQuest` - Maintain provinces in Mobilize order for months
|
||||
- `RestProvinceQuest` - Use the Rest command in a specific province
|
||||
|
||||
### Reconnaissance Quests
|
||||
- `ReconProvincesQuest` - Reconnoiter a number of provinces
|
||||
- `ReconSpecificProvincesQuest` - Reconnoiter specific provinces
|
||||
|
||||
### Economic Quests
|
||||
- `TotalDevelopmentQuest` - Achieve total development level in a province
|
||||
- `WealthQuest` - Accumulate gold and food
|
||||
- `SpendOnFeastsQuest` - Spend gold on feasts
|
||||
- `SendSuppliesQuest` - Send food to a specific province
|
||||
- `RepairDevastationQuest` - Repair devastation
|
||||
|
||||
### Special Event Quests
|
||||
- `SuppressRiotByForceQuest` - Suppress a riot by force
|
||||
- `FightBeastsAloneQuest` - Fight beasts alone
|
||||
- `StartBlizzardQuest` - Start a blizzard in a province
|
||||
- `StartEpidemicQuest` - Start an epidemic in a province
|
||||
- `ApprehendOutlawQuest` - Apprehend an outlaw hero
|
||||
|
||||
### Miscellaneous
|
||||
- `BattalionDiversityQuest` - Have diverse battalion types
|
||||
- `SwearBrotherhoodWithHeroQuest` - Swear brotherhood with a specific hero
|
||||
- `BetrayAllyQuest` - Betray an allied faction
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The quest completion logic is located in:
|
||||
- `FulfillQuestsCommandSelector.scala` - Main entry point, iterates through choosers
|
||||
- `quest_command_selectors/` - Individual quest handlers
|
||||
|
||||
Each chooser extends either:
|
||||
- `QuestCommandChooser` - For quests requiring randomness
|
||||
- `DeterministicQuestCommandChooser` - For quests with deterministic command selection
|
||||
|
||||
The AI prioritizes quests in the order they appear in `FulfillQuestsCommandSelector.choosers`:
|
||||
1. Alliance
|
||||
2. TruceWithFaction
|
||||
3. Improve (Agriculture/Economy/Infrastructure)
|
||||
4. AlmsToProvince
|
||||
5. GiveToHeroesInProvince
|
||||
6. AlmsAcrossRealm
|
||||
7. GiveToHeroesAcrossRealm
|
||||
8. TruceCount
|
||||
9. DismissSpecificVassal
|
||||
@@ -1,230 +0,0 @@
|
||||
# Plan: Eliminate Battalion Backstory LLM Updates
|
||||
|
||||
## Goal
|
||||
|
||||
Dramatically reduce LLM request volume by eliminating battalion backstory updates. Battalions will retain:
|
||||
- An **initial backstory** (generated once when created)
|
||||
- A **history of events** (structured data, not LLM-generated prose)
|
||||
|
||||
The history will be fed into other LLM updates (hero backstories, chronicle) but will no longer be regenerated into prose for each battalion.
|
||||
|
||||
## Current System Overview
|
||||
|
||||
### Data Flow
|
||||
```
|
||||
Battalion Created → Initial Backstory LLM Request → Text stored
|
||||
↓
|
||||
Battalion participates in events → Events accumulated → Update LLM Request → New text version
|
||||
↓
|
||||
BattalionView.backstoryTextId → Client displays on hover
|
||||
```
|
||||
|
||||
### Event Types (8 total)
|
||||
Events are structured data already captured in `EventForBattalionBackstory`:
|
||||
- OrganizedTroops, SuppressedBeasts, ApprehendedOutlaws, FoughtInBattle
|
||||
- SuppressedRiot, Trained, Armed, SurvivedStarvation
|
||||
|
||||
Each event has date, province, hero, and event-specific details.
|
||||
|
||||
### Key Files
|
||||
|
||||
| Component | File |
|
||||
|-----------|------|
|
||||
| Unity Display | `Assets/Eagle/HeroesAndBattalionsPanelController.cs` |
|
||||
| View Proto | `views/battalion_view.proto` |
|
||||
| View Filter | `BattalionViewFilter.scala` |
|
||||
| Update Action | `BattalionBackstoryUpdateAction.scala` |
|
||||
| Update Generator | `BattalionBackstoryUpdateActionGenerator.scala` |
|
||||
| Update Prompt | `BattalionBackstoryUpdatePromptGenerator.scala` |
|
||||
| Event-to-Text | `BattalionBackstoryUpdatePromptGenerator.textForEvent()` |
|
||||
| BattalionDescriptions | `BattalionDescriptions.scala` (used in other LLM prompts) |
|
||||
|
||||
---
|
||||
|
||||
## Stage 1: Stop Displaying Battalion Backstories in Client
|
||||
|
||||
**Goal:** Remove the UI that shows battalion backstory text on hover.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`Assets/Eagle/HeroesAndBattalionsPanelController.cs`**
|
||||
- In `BattalionLongHoverRowChanged()` (lines 143-169):
|
||||
- Remove or comment out the backstory text population
|
||||
- Keep the header/name display, remove `battalionPopupPanelBackstory.TextId = ...`
|
||||
|
||||
**Unity Scene/Prefab**
|
||||
- Hide or remove the `battalionPopupPanelBackstory` UI element
|
||||
|
||||
### Verification
|
||||
- [ ] Battalion hover popup no longer shows backstory text
|
||||
- [ ] No errors when hovering over battalions
|
||||
- [ ] Other battalion info (name, type, size) still displays
|
||||
|
||||
---
|
||||
|
||||
## Stage 2: Stop Populating Backstory in BattalionView
|
||||
|
||||
**Goal:** Stop sending backstory text ID to the client.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionViewFilter.scala`**
|
||||
- In `filteredBattalionViewBelongingToPlayer()`:
|
||||
- Set `backstoryTextId = ""` instead of `battalion.backstoryTextId`
|
||||
- In `limitedBattalionView()`:
|
||||
- Same change
|
||||
|
||||
### Verification
|
||||
- [ ] BattalionView messages have empty backstoryTextId
|
||||
- [ ] Client handles empty backstoryTextId gracefully (Stage 1 should have removed the display)
|
||||
|
||||
---
|
||||
|
||||
## Stage 3: Remove backstory_text_id from BattalionView Proto
|
||||
|
||||
**Goal:** Clean up the proto schema.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`views/battalion_view.proto`**
|
||||
- Remove field: `string backstory_text_id = 7;`
|
||||
- Mark field 7 as reserved to prevent reuse
|
||||
|
||||
**`Assets/Eagle/HeroesAndBattalionsPanelController.cs`**
|
||||
- Remove any remaining references to `BackstoryTextId`
|
||||
|
||||
**`BattalionViewFilter.scala`**
|
||||
- Remove `backstoryTextId` from view construction
|
||||
|
||||
### Verification
|
||||
- [ ] Proto compiles
|
||||
- [ ] C# client compiles without BackstoryTextId references
|
||||
- [ ] Full test suite passes
|
||||
|
||||
---
|
||||
|
||||
## Stage 4: Replace Backstory Usage in BattalionDescriptions
|
||||
|
||||
**Goal:** When other LLM prompts need battalion context, use initial backstory + structured history instead of the latest LLM-generated prose.
|
||||
|
||||
### Current Usage in `BattalionDescriptions.scala`
|
||||
|
||||
```scala
|
||||
def backstoryText(): TextGenerationResult =
|
||||
battalion.backstoryVersions.lastOption
|
||||
.map(v => clientTextStore.getText(v.textId))
|
||||
.getOrElse(TextGenerationSuccess(""))
|
||||
|
||||
def fullDescription(): String =
|
||||
s"${battalion.name} is $description. $backstoryText"
|
||||
```
|
||||
|
||||
### New Approach
|
||||
|
||||
Create `historyDescription()` that:
|
||||
1. Gets initial backstory (first version's text, if available)
|
||||
2. Appends structured event history using the same `textForEvent()` logic from `BattalionBackstoryUpdatePromptGenerator`
|
||||
|
||||
```scala
|
||||
def historyDescription(): String = {
|
||||
val initialBackstory = battalion.backstoryVersions.headOption
|
||||
.map(v => clientTextStore.getText(v.textId))
|
||||
.collect { case TextGenerationSuccess(text) => text }
|
||||
.getOrElse("")
|
||||
|
||||
val eventHistory = battalion.backstoryEvents
|
||||
.map(textForEvent) // Reuse existing event-to-text conversion
|
||||
.mkString(" ")
|
||||
|
||||
if eventHistory.isEmpty then initialBackstory
|
||||
else s"$initialBackstory $eventHistory"
|
||||
}
|
||||
```
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionDescriptions.scala`**
|
||||
- Add `historyDescription()` method
|
||||
- Update `fullDescription()` to use `historyDescription()` instead of `backstoryText()`
|
||||
- Keep `textForEvent()` logic (move from `BattalionBackstoryUpdatePromptGenerator` if needed)
|
||||
|
||||
**Move/refactor `textForEvent()`**
|
||||
- Currently in `BattalionBackstoryUpdatePromptGenerator.scala` (lines 78-262)
|
||||
- Move to `BattalionDescriptions.scala` or a shared utility
|
||||
- This converts events to human-readable text without LLM
|
||||
|
||||
### Verification
|
||||
- [ ] Other LLM prompts that use `BattalionDescriptions` still get meaningful context
|
||||
- [ ] Chronicle updates include battalion history
|
||||
- [ ] Hero backstory updates include relevant battalion history
|
||||
|
||||
---
|
||||
|
||||
## Stage 5: Stop Generating Battalion Backstory Updates
|
||||
|
||||
**Goal:** Remove the LLM request generation entirely.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionBackstoryUpdateActionGenerator.scala`**
|
||||
- Return empty Vector instead of generating actions
|
||||
- Or delete the file entirely
|
||||
|
||||
**`BattalionBackstoryUpdateAction.scala`**
|
||||
- Delete or mark as deprecated
|
||||
|
||||
**`BattalionBackstoryUpdatePromptGenerator.scala`**
|
||||
- Delete or keep only `textForEvent()` if moved
|
||||
|
||||
**`LlmResolver.scala`**
|
||||
- Remove case for `BattalionBackstoryUpdateRequest`
|
||||
- Optionally throw error if somehow called
|
||||
|
||||
**`EndPlayerCommandsPhaseAction.scala`** (and other callers)
|
||||
- Find where `BattalionBackstoryUpdateActionGenerator` is called
|
||||
- Remove those calls
|
||||
|
||||
### Files to Search for Callers
|
||||
```bash
|
||||
grep -r "BattalionBackstoryUpdateAction" src/
|
||||
```
|
||||
|
||||
### Verification
|
||||
- [ ] No `BattalionBackstoryUpdateRequest` LLM requests generated
|
||||
- [ ] Battalions still accumulate events (for history description)
|
||||
- [ ] Full test suite passes
|
||||
- [ ] Game runs without errors
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Keep or Remove Initial Backstory Generation?
|
||||
The initial backstory (`BattalionInitialBackstoryRequest`) is generated once per battalion. Options:
|
||||
1. **Keep it** - Low volume, provides flavor text for history
|
||||
2. **Remove it** - Use a template-based approach instead
|
||||
|
||||
Recommendation: Keep for now, evaluate later based on volume.
|
||||
|
||||
### Event History Cleanup
|
||||
Events currently accumulate forever on battalions. Consider:
|
||||
- Capping event history length
|
||||
- Summarizing old events
|
||||
- Only keeping "significant" events
|
||||
|
||||
### Affected Tests
|
||||
Search for tests that verify backstory update behavior:
|
||||
```bash
|
||||
bazel query 'tests(//src/test/scala/...) intersect rdeps(//src/test/scala/..., //src/main/scala/net/eagle0/eagle/library/actions/impl/action:battalion_backstory_update_action)'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of LLM Request Reduction
|
||||
|
||||
| Request Type | Current Volume | After Change |
|
||||
|--------------|---------------|--------------|
|
||||
| BattalionInitialBackstoryRequest | 1 per battalion | 1 per battalion (unchanged) |
|
||||
| BattalionBackstoryUpdateRequest | ~N per battalion per game | **0** |
|
||||
|
||||
For a typical game with ~50 battalions and ~20 rounds, this could eliminate **hundreds** of LLM requests.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Province Event 3D Effects
|
||||
|
||||
Ideas for replacing 2D shader-based province event effects with 3D particle systems.
|
||||
|
||||
## Current State
|
||||
|
||||
| Event | Effect Type | Notes |
|
||||
|-------|-------------|-------|
|
||||
| Festival | 3D particles | Complete |
|
||||
| Epidemic | 3D particles | Complete (rising skulls + green haze) |
|
||||
| Blizzard | 2D shader | Not visible on unowned (white) provinces |
|
||||
| Drought | 2D shader | |
|
||||
| Flood | 2D shader | |
|
||||
| Beasts | None | Tricky - can be any animal or human |
|
||||
|
||||
## Proposed 3D Effects
|
||||
|
||||
### Blizzard
|
||||
|
||||
- Falling snowflakes with slight blue tint (ensures visibility on white backgrounds)
|
||||
- Diagonal wind-blown particles to convey harsh weather
|
||||
- Swirling vortex effect near ground level
|
||||
- Frosty sparkle/glitter particles for magical winter feel
|
||||
|
||||
### Drought
|
||||
|
||||
- Rising dust/sand particles (tan/brown coloring)
|
||||
- Small dust devils (spinning particle columns)
|
||||
- Shimmering golden particles rising to suggest heat distortion
|
||||
- Dried leaves/debris blowing across the province
|
||||
|
||||
### Flood
|
||||
|
||||
- Rain falling (blue-tinted streaks or droplets)
|
||||
- Water splashes/ripples rising from ground
|
||||
- Mist/spray particles hovering low
|
||||
- Floating debris particles (leaves, sticks)
|
||||
|
||||
### Beasts
|
||||
|
||||
This is challenging since "beasts" can represent any dangerous creature. Universal approaches:
|
||||
|
||||
- **Circling vultures/crows overhead** - Predators attract scavengers; works for any beast type and is visually distinctive
|
||||
- Dust clouds suggesting movement at province edges
|
||||
- Glowing eyes appearing/disappearing in shadows
|
||||
- Stylized claw/scratch mark effects
|
||||
- Paw prints materializing on the ground
|
||||
- Red "danger" aura pulsing from ground level
|
||||
|
||||
The vultures/crows approach is recommended as the most universal solution.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Controllers have been created for all 4 event types:
|
||||
- `ProvinceBlizzardController.cs` - ready, needs prefab
|
||||
- `ProvinceDroughtController.cs` - ready, needs prefab
|
||||
- `ProvinceFloodController.cs` - ready, needs prefab
|
||||
- `ProvinceBeastsController.cs` - ready, needs prefab
|
||||
|
||||
### Prefabs Needed
|
||||
|
||||
Create these prefabs in `Assets/Eagle/Effects/`:
|
||||
|
||||
1. **BlizzardEffect.prefab**
|
||||
- Particle System with falling snowflakes
|
||||
- Blue-tinted particles for visibility on white
|
||||
- Diagonal velocity for wind effect
|
||||
- RectTransform for UI positioning
|
||||
|
||||
2. **DroughtEffect.prefab**
|
||||
- Particle System with rising dust particles
|
||||
- Tan/brown coloring
|
||||
- Upward velocity
|
||||
- Optional spinning sub-emitters for dust devils
|
||||
|
||||
3. **FloodEffect.prefab**
|
||||
- Particle System with falling rain streaks
|
||||
- Blue tint
|
||||
- Fast downward velocity
|
||||
- Optional splash sub-emitter
|
||||
|
||||
4. **BeastsEffect.prefab**
|
||||
- Particle System with bird silhouettes
|
||||
- Circular orbit motion around center
|
||||
- Black/dark particles
|
||||
- Could use a crow/vulture sprite texture
|
||||
|
||||
### Scene Setup
|
||||
|
||||
Wire up in `Gameplay.unity`:
|
||||
1. Add controller components to the Map GameObject
|
||||
2. Assign `mapContainer`, `centroidsJson` (shared with other controllers)
|
||||
3. Assign the effect prefab for each controller
|
||||
@@ -60,7 +60,6 @@
|
||||
"org.scalamock:scalamock_3": -610483078,
|
||||
"org.slf4j:slf4j-api": 1609745898,
|
||||
"org.slf4j:slf4j-simple": 1319506696,
|
||||
"org.xerial:sqlite-jdbc": -1707012205,
|
||||
"repositories": -1949687017,
|
||||
"software.amazon.awssdk:aws-core": 1764800397,
|
||||
"software.amazon.awssdk:http-client-spi": 1697477859,
|
||||
@@ -174,7 +173,6 @@
|
||||
"org.scalamock:scalamock_3": -1778657085,
|
||||
"org.slf4j:slf4j-api": -771685699,
|
||||
"org.slf4j:slf4j-simple": -776509812,
|
||||
"org.xerial:sqlite-jdbc": 1311600081,
|
||||
"software.amazon.awssdk:annotations": -324257201,
|
||||
"software.amazon.awssdk:apache-client": 2066024214,
|
||||
"software.amazon.awssdk:arns": -1240703529,
|
||||
@@ -818,12 +816,6 @@
|
||||
},
|
||||
"version": "2.0.16"
|
||||
},
|
||||
"org.xerial:sqlite-jdbc": {
|
||||
"shasums": {
|
||||
"jar": "6dc7464e3803648d3ff18a7359bab6adf079fcd8495b18991f6f5edcb8ac6e3b"
|
||||
},
|
||||
"version": "3.46.1.0"
|
||||
},
|
||||
"software.amazon.awssdk:annotations": {
|
||||
"shasums": {
|
||||
"jar": "ed03bb4ff78900307dc96bacdec70ddf80c17df3eb6761219573025649f08ff3"
|
||||
@@ -1360,9 +1352,6 @@
|
||||
"org.slf4j:slf4j-simple": [
|
||||
"org.slf4j:slf4j-api"
|
||||
],
|
||||
"org.xerial:sqlite-jdbc": [
|
||||
"org.slf4j:slf4j-api"
|
||||
],
|
||||
"software.amazon.awssdk:apache-client": [
|
||||
"commons-codec:commons-codec",
|
||||
"org.apache.httpcomponents:httpclient",
|
||||
@@ -2415,16 +2404,6 @@
|
||||
"org.slf4j:slf4j-simple": [
|
||||
"org.slf4j.simple"
|
||||
],
|
||||
"org.xerial:sqlite-jdbc": [
|
||||
"org.sqlite",
|
||||
"org.sqlite.core",
|
||||
"org.sqlite.date",
|
||||
"org.sqlite.javax",
|
||||
"org.sqlite.jdbc3",
|
||||
"org.sqlite.jdbc4",
|
||||
"org.sqlite.nativeimage",
|
||||
"org.sqlite.util"
|
||||
],
|
||||
"software.amazon.awssdk:annotations": [
|
||||
"software.amazon.awssdk.annotations"
|
||||
],
|
||||
@@ -2835,7 +2814,6 @@
|
||||
"org.scalamock:scalamock_3",
|
||||
"org.slf4j:slf4j-api",
|
||||
"org.slf4j:slf4j-simple",
|
||||
"org.xerial:sqlite-jdbc",
|
||||
"software.amazon.awssdk:annotations",
|
||||
"software.amazon.awssdk:apache-client",
|
||||
"software.amazon.awssdk:arns",
|
||||
@@ -2912,11 +2890,6 @@
|
||||
"org.slf4j.simple.SimpleServiceProvider"
|
||||
]
|
||||
},
|
||||
"org.xerial:sqlite-jdbc": {
|
||||
"java.sql.Driver": [
|
||||
"org.sqlite.JDBC"
|
||||
]
|
||||
},
|
||||
"software.amazon.awssdk:apache-client": {
|
||||
"software.amazon.awssdk.http.SdkHttpService": [
|
||||
"software.amazon.awssdk.http.apache.ApacheSdkHttpService"
|
||||
|
||||
+4
-54
@@ -2,57 +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 "Copying generated .cs files to $OUTPUT_DIR..."
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
|
||||
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="$OUTPUT_DIR/$subdir"
|
||||
mkdir -p "$dest_dir"
|
||||
find "$bin_dir" -name "*.cs" -exec cp {} "$dest_dir/" \;
|
||||
done
|
||||
|
||||
/bin/echo "Done. Generated C# proto sources in $OUTPUT_DIR"
|
||||
/bin/echo "build protos"
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
dotnet build src/main/csharp/net/eagle0/clients/unity/eagle0/protos/protos.csproj \
|
||||
-o src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
|
||||
@@ -22,31 +22,11 @@ NC='\033[0m' # No Color
|
||||
MODE="${1:-check}"
|
||||
EXIT_CODE=0
|
||||
|
||||
# Cache for expensive bazel query results
|
||||
LIBRARY_DEPS_CACHE=""
|
||||
MAIN_DEPS_CACHE=""
|
||||
|
||||
# Get deps of library/ (cached)
|
||||
get_library_deps() {
|
||||
if [ -z "$LIBRARY_DEPS_CACHE" ]; then
|
||||
LIBRARY_DEPS_CACHE=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...)' 2>/dev/null || true)
|
||||
fi
|
||||
echo "$LIBRARY_DEPS_CACHE"
|
||||
}
|
||||
|
||||
# Get deps of src/main (cached)
|
||||
get_main_deps() {
|
||||
if [ -z "$MAIN_DEPS_CACHE" ]; then
|
||||
MAIN_DEPS_CACHE=$(bazel query 'deps(//src/main/...)' 2>/dev/null || true)
|
||||
fi
|
||||
echo "$MAIN_DEPS_CACHE"
|
||||
}
|
||||
|
||||
# Rule 1: src/main should not depend on src/test
|
||||
check_main_depends_on_test() {
|
||||
echo -e "${YELLOW}Checking: src/main should not depend on src/test...${NC}"
|
||||
|
||||
violations=$(get_main_deps | grep "^//src/test/" || true)
|
||||
violations=$(bazel query 'deps(//src/main/...) intersect //src/test/...' 2>/dev/null || true)
|
||||
|
||||
if [ -n "$violations" ]; then
|
||||
echo -e "${RED}VIOLATION: src/main depends on src/test:${NC}"
|
||||
@@ -63,7 +43,7 @@ check_main_depends_on_test() {
|
||||
check_library_depends_on_scala_proto() {
|
||||
echo -e "${YELLOW}Checking: library/ should not depend on Scala proto types...${NC}"
|
||||
|
||||
violations=$(get_library_deps | grep "^//src/main/protobuf/.*_scala_proto$" || true)
|
||||
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
|
||||
if [ -z "$violations" ]; then
|
||||
count=0
|
||||
else
|
||||
@@ -85,7 +65,7 @@ check_library_depends_on_scala_proto() {
|
||||
check_library_depends_on_proto_converters() {
|
||||
echo -e "${YELLOW}Checking: library/ should not depend on proto_converters...${NC}"
|
||||
|
||||
violations=$(get_library_deps | grep "^//src/main/scala/net/eagle0/eagle/model/proto_converters/" || true)
|
||||
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/scala/net/eagle0/eagle/model/proto_converters/...' 2>/dev/null | grep "^//" || true)
|
||||
|
||||
if [ -n "$violations" ]; then
|
||||
count=$(echo "$violations" | wc -l | tr -d ' ')
|
||||
@@ -105,8 +85,7 @@ check_library_depends_on_proto_converters() {
|
||||
count_proto_deps() {
|
||||
echo -e "${YELLOW}=== Proto dependency counts ===${NC}"
|
||||
|
||||
library_deps=$(get_library_deps)
|
||||
scala_proto_results=$(echo "$library_deps" | grep "^//src/main/protobuf/.*_scala_proto$" || true)
|
||||
scala_proto_results=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
|
||||
if [ -z "$scala_proto_results" ]; then
|
||||
scala_proto_count=0
|
||||
else
|
||||
@@ -115,7 +94,7 @@ count_proto_deps() {
|
||||
echo "library/ Scala proto deps: $scala_proto_count"
|
||||
|
||||
# C++/Go proto deps are expected (map generation tools)
|
||||
all_proto_count=$(echo "$library_deps" | grep -c "^//src/main/protobuf/" || echo "0")
|
||||
all_proto_count=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo "library/ all proto deps (includes C++/Go build tools): $all_proto_count"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1DHEsiv4cY4gE6AX3sVH82K__mpBD1aznIYCQwQxA_F0/export?gid=0&format=tsv" | tr -d '\r' > /tmp/names.tsv
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_checker -- /tmp/names.tsv > /dev/null
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_checker -- /tmp/names.tsv > src/main/resources/net/eagle0/names.tsv
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_json_maker -- /tmp/names.tsv > src/main/resources/net/eagle0/names.json
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1NhvG73HKyVE36yGpkV2oJiSIXoNqQOYTr5ArLnucYL0/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/shardok/battalionTypes.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1pNWiyxIks2wJ1v7jRLFD24zrKHG2AfhC-nkWmQKQGN4/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/eagle/heroes.tsv
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -286,29 +286,6 @@ 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 tutorialResults = tutorialController_.CheckAndExecuteEvents(
|
||||
gameState,
|
||||
settingsGetter,
|
||||
randomGenerator);
|
||||
|
||||
if (!tutorialResults.empty()) {
|
||||
ApplyAndAddActionResults(tutorialResults);
|
||||
|
||||
// Update game status after tutorial events (e.g., attackers fled, defender wins)
|
||||
ApplyAndAddActionResults(UpdateGameStatusAction(
|
||||
GetCurrentGameState(),
|
||||
criticalTileCoords,
|
||||
settingsGetter,
|
||||
true /* atEndOfRound */)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
// If tutorial events ended the game, don't start new round
|
||||
if (GameIsOver()) { return; }
|
||||
}
|
||||
}
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
UpdateOpponentKnowledgeAction(settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
|
||||
@@ -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,8 +52,6 @@ private:
|
||||
|
||||
const CoordsSet criticalTileCoords;
|
||||
|
||||
TutorialBattleController tutorialController_;
|
||||
|
||||
mutable CommandListSPtr cachedAvailableCommands{};
|
||||
|
||||
void ApplyAndAddActionResult(const ActionResult &result);
|
||||
@@ -201,11 +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);
|
||||
}
|
||||
|
||||
static inline auto GameIsOver(const fb::GameStatus *status) -> bool {
|
||||
return (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY);
|
||||
}
|
||||
|
||||
@@ -1,18 +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/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,236 +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) -> std::vector<ActionResult> {
|
||||
if (!enabled_) { return {}; }
|
||||
|
||||
std::vector<ActionResult> allResults;
|
||||
|
||||
// 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 results = ExecuteAction(event.action(), state, settings, randomGenerator);
|
||||
allResults.insert(allResults.end(), results.begin(), results.end());
|
||||
}
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
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,
|
||||
[[maybe_unused]] const GameStateW& state,
|
||||
[[maybe_unused]] const SettingsGetter& settings,
|
||||
[[maybe_unused]] const std::shared_ptr<RandomGenerator>& randomGenerator) const
|
||||
-> std::vector<ActionResult> {
|
||||
switch (action.action_type_case()) {
|
||||
case TutorialActionProto::kFlee: return ExecuteFleeAction(action.flee(), state);
|
||||
|
||||
case TutorialActionProto::kReinforcements:
|
||||
return ExecuteReinforcementsAction(action.reinforcements());
|
||||
|
||||
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) {
|
||||
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> specificUnits(fleeAction.unit_ids().begin(), fleeAction.unit_ids().end());
|
||||
const bool fleeAllUnits = specificUnits.empty();
|
||||
|
||||
// Find all matching units that are still active and make them flee
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->player_id() != fleeingPlayerId) { continue; }
|
||||
if (unit->status() != UnitStatusFB::UnitStatus_NORMAL_UNIT) { continue; }
|
||||
|
||||
// Check if this is a specific unit to flee, or if we're fleeing all
|
||||
if (!fleeAllUnits && !specificUnits.contains(unit->unit_id())) { continue; }
|
||||
|
||||
// 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 -> std::vector<ActionResult> {
|
||||
std::vector<ActionResult> results;
|
||||
|
||||
// Add reinforcements arrival notification if there are units
|
||||
if (!reinforcementsAction.units().empty()) {
|
||||
ActionResult reinforcementsResult{};
|
||||
reinforcementsResult.set_type(ActionType::TUTORIAL_REINFORCEMENTS_ARRIVED);
|
||||
results.push_back(reinforcementsResult);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,102 +0,0 @@
|
||||
//
|
||||
// TutorialBattleController.hpp
|
||||
// eagle0
|
||||
//
|
||||
// Created for tutorial battle system.
|
||||
// Copyright 2025 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef TutorialBattleController_hpp
|
||||
#define TutorialBattleController_hpp
|
||||
|
||||
#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/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;
|
||||
|
||||
/// 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) -> std::vector<ActionResult>;
|
||||
|
||||
/// 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
|
||||
-> std::vector<ActionResult>;
|
||||
|
||||
// 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.
|
||||
[[nodiscard]] auto ExecuteReinforcementsAction(
|
||||
const net::eagle0::common::ReinforcementsAction& reinforcementsAction) const
|
||||
-> std::vector<ActionResult>;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif /* TutorialBattleController_hpp */
|
||||
@@ -19,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -30,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) {
|
||||
@@ -78,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);
|
||||
@@ -113,7 +111,6 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
|
||||
month,
|
||||
units,
|
||||
playerInfos,
|
||||
tutorialConfig,
|
||||
serializedRequest);
|
||||
|
||||
return LockedCreateGame(controller, playerSetupInfos, serializedRequest);
|
||||
@@ -140,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>();
|
||||
|
||||
@@ -159,8 +155,6 @@ auto SetUpController(
|
||||
gameId,
|
||||
playerInfos);
|
||||
|
||||
engine->SetTutorialBattleConfig(tutorialConfig);
|
||||
|
||||
return std::make_shared<ShardokGameController>(std::move(engine), mapName, serializedRequest);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/tutorial_battle_config.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/game_status.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/victory_condition.pb.h"
|
||||
@@ -26,7 +25,6 @@ class GameStatePersister;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
|
||||
using TutorialBattleConfigProto = net::eagle0::common::TutorialBattleConfig;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using VictoryConditionProto = net::eagle0::shardok::common::VictoryCondition;
|
||||
@@ -103,7 +101,6 @@ public:
|
||||
const string &mapName,
|
||||
const string &customMapBytes,
|
||||
int month,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> GameId;
|
||||
|
||||
auto UnlockedCreateFromGameStateBytes(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
+41
-47
@@ -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;
|
||||
@@ -411,25 +411,27 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void SetupLobbyUI() {
|
||||
customBattleButton.onClick.AddListener(CustomBattle);
|
||||
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
|
||||
tutorialButton.gameObject.SetActive(TokenStorage.DisplayName == "nolen");
|
||||
tutorialButton.onClick.AddListener(CreateTutorialGame);
|
||||
if (customBattleButton != null) { customBattleButton.onClick.AddListener(CustomBattle); }
|
||||
if (cancelCustomBattleButton != null) {
|
||||
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
|
||||
}
|
||||
|
||||
// Set up environment dropdown (only in Unity Editor)
|
||||
if (!Application.isEditor) {
|
||||
lobbyEnvironmentDropdown.gameObject.SetActive(false);
|
||||
} else {
|
||||
lobbyEnvironmentDropdown.ClearOptions();
|
||||
lobbyEnvironmentDropdown.AddOptions(EnvironmentDisplayNames);
|
||||
lobbyEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
|
||||
if (lobbyEnvironmentDropdown != null) {
|
||||
if (!Application.isEditor) {
|
||||
lobbyEnvironmentDropdown.gameObject.SetActive(false);
|
||||
} else {
|
||||
lobbyEnvironmentDropdown.ClearOptions();
|
||||
lobbyEnvironmentDropdown.AddOptions(EnvironmentDisplayNames);
|
||||
lobbyEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLobbyStatusDisplays() {
|
||||
// Update environment dropdown selection
|
||||
if (_connectedEnvironmentIndex >= 0) {
|
||||
if (lobbyEnvironmentDropdown != null && _connectedEnvironmentIndex >= 0) {
|
||||
// Temporarily remove listener to avoid triggering reconnect
|
||||
lobbyEnvironmentDropdown.onValueChanged.RemoveListener(OnLobbyEnvironmentChanged);
|
||||
lobbyEnvironmentDropdown.value = _connectedEnvironmentIndex;
|
||||
@@ -437,7 +439,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
// Show current user from OAuth
|
||||
lobbyUserText.text = TokenStorage.DisplayName ?? "";
|
||||
if (lobbyUserText != null) { lobbyUserText.text = TokenStorage.DisplayName ?? ""; }
|
||||
}
|
||||
|
||||
private void OnLobbyEnvironmentChanged(int newEnvironmentIndex) {
|
||||
@@ -685,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));
|
||||
|
||||
@@ -709,7 +706,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
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,
|
||||
@@ -726,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(
|
||||
@@ -736,8 +735,14 @@ 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(
|
||||
@@ -871,7 +876,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
};
|
||||
_persistentClientConnection.Connect();
|
||||
|
||||
PersistentClientConnection.Current = _persistentClientConnection;
|
||||
errorHandler.PersistentClientConnection = _persistentClientConnection;
|
||||
ResourceFetcher.SetUpConnection(_httpClient);
|
||||
}
|
||||
@@ -948,17 +952,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
var game = _internalCreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount);
|
||||
}
|
||||
|
||||
public void CreateTutorialGame() {
|
||||
// Tutorial is always single-player
|
||||
TutorialManager.IsMultiplayerGame = false;
|
||||
// Eagle will set up the hardcoded tutorial scenario when isTutorial is true
|
||||
var game = _internalCreateGame(
|
||||
desiredLeaderTextId: "",
|
||||
humanPlayerCount: 1,
|
||||
totalPlayerCount: 2,
|
||||
isTutorial: true);
|
||||
}
|
||||
|
||||
public void QuitButtonClicked() { Application.Quit(); }
|
||||
|
||||
public void HandleJoinGameResponse(JoinGameResponse response) {
|
||||
@@ -976,9 +969,14 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public void HandleCreateGameResponse(CreateGameResponse response) {
|
||||
try {
|
||||
switch (response.Result) {
|
||||
case JoinGameResult.SuccessJoinGameResult: SelectEagleGame(response.GameId); break;
|
||||
case JoinGameResult.SuccessJoinGameResult:
|
||||
SelectEagleGame(response.GameId);
|
||||
// Automatically start the game if it's one player?
|
||||
break;
|
||||
|
||||
default: Debug.LogError($"Game creation failed: {response.Result}"); break;
|
||||
default:
|
||||
// FIXME: display something
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) { Console.WriteLine(e.ToString()); }
|
||||
}
|
||||
@@ -990,11 +988,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
} catch (Exception e) { Console.WriteLine(e.ToString()); }
|
||||
}
|
||||
|
||||
private async Task<bool> _internalCreateGame(
|
||||
string desiredLeaderTextId,
|
||||
int humanPlayerCount,
|
||||
int totalPlayerCount,
|
||||
bool isTutorial = false) {
|
||||
private async Task<bool>
|
||||
_internalCreateGame(string desiredLeaderTextId, int humanPlayerCount, int totalPlayerCount) {
|
||||
try {
|
||||
return await _persistentClientConnection.SendUpdateStreamRequestAsync(
|
||||
new UpdateStreamRequest {
|
||||
@@ -1002,8 +997,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
new CreateGameRequest {
|
||||
DesiredLeaderTextId = desiredLeaderTextId,
|
||||
TotalPlayerCount = totalPlayerCount,
|
||||
HumanPlayerCount = humanPlayerCount,
|
||||
IsTutorial = isTutorial
|
||||
HumanPlayerCount = humanPlayerCount
|
||||
}
|
||||
});
|
||||
} catch (WebException e) {
|
||||
|
||||
+3653
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5315bdd018d9e420dbbf55a0508a7f14
|
||||
guid: 81b03eaddf4de4a4aa385617e4881511
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
+23
-8
@@ -11,10 +11,9 @@ using UnityEngine.UI;
|
||||
using Random = System.Random;
|
||||
|
||||
public class CreateGameItem : MonoBehaviour {
|
||||
private const int TotalPlayerCount = 7;
|
||||
|
||||
public HeroDropdownController heroDropdownController;
|
||||
public TMP_Dropdown totalHumansDropdown;
|
||||
public TMP_Dropdown totalPlayersDropdown;
|
||||
public Button createButton;
|
||||
public TextMeshProUGUI buttonText;
|
||||
|
||||
@@ -33,20 +32,30 @@ public class CreateGameItem : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
|
||||
// Use this for initialization
|
||||
void Start() {}
|
||||
|
||||
public void LeaderDropdownSelected(TMP_Dropdown dropdown) {}
|
||||
|
||||
public void TotalPlayersDropdownSelected(TMP_Dropdown dropdown) { SetHumanPlayersDropdown(); }
|
||||
|
||||
public void CreateClicked() {
|
||||
CreateCallback(SelectedLeaderTextId, totalHumansDropdown.value + 1, TotalPlayerCount);
|
||||
CreateCallback(
|
||||
SelectedLeaderTextId,
|
||||
totalHumansDropdown.value + 1,
|
||||
totalPlayersDropdown.value + 1);
|
||||
}
|
||||
|
||||
public void SetHumanPlayersDropdown() {
|
||||
var currentValue = totalHumansDropdown.value;
|
||||
totalHumansDropdown.ClearOptions();
|
||||
totalHumansDropdown.AddOptions(Enumerable.Range(1, TotalPlayerCount)
|
||||
.Select(i => i == 1 ? "1 player" : $"{i} players")
|
||||
totalHumansDropdown.AddOptions(Enumerable.Range(1, totalPlayersDropdown.value + 1)
|
||||
.Select(i => i.ToString())
|
||||
.ToList());
|
||||
totalHumansDropdown.value = Math.Min(TotalPlayerCount - 1, currentValue);
|
||||
totalHumansDropdown.value = Math.Min(totalPlayersDropdown.value, currentValue);
|
||||
}
|
||||
|
||||
public void SetCreationOptions(List<AvailableLeader> leaders) {
|
||||
public void SetCreationOptions(List<AvailableLeader> leaders, int maxPlayerCount) {
|
||||
this.availableLeaders = leaders;
|
||||
|
||||
buttonText.text = "CREATE";
|
||||
@@ -68,10 +77,16 @@ public class CreateGameItem : MonoBehaviour {
|
||||
heroDropdownController.PreResolvedNames =
|
||||
leaders.Where(l => !string.IsNullOrEmpty(l.Name))
|
||||
.ToDictionary(l => l.NameTextId, l => l.Name);
|
||||
heroDropdownController.FallbackText = "Random Warlord";
|
||||
heroDropdownController.FallbackText = "Random";
|
||||
heroDropdownController.AvailableHeroes = heroOptions;
|
||||
heroDropdownController.HeadshotImage.gameObject.SetActive(false);
|
||||
|
||||
totalPlayersDropdown.ClearOptions();
|
||||
totalPlayersDropdown.AddOptions(
|
||||
Enumerable.Range(1, maxPlayerCount).Select(i => i.ToString()).ToList());
|
||||
// Default to 7 total players (index 6 = 7 players, so 1 human + 6 AI)
|
||||
totalPlayersDropdown.value = Math.Min(6, maxPlayerCount - 1);
|
||||
|
||||
SetHumanPlayersDropdown();
|
||||
// Default to 1 human (index 0)
|
||||
totalHumansDropdown.value = 0;
|
||||
|
||||
+4
-98
@@ -631,99 +631,6 @@ MonoBehaviour:
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &5462510071559864326
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7404235247014569559}
|
||||
- component: {fileID: 7896497708396401296}
|
||||
- component: {fileID: 6993077635068771622}
|
||||
- component: {fileID: 1252979331277989500}
|
||||
m_Layer: 5
|
||||
m_Name: Profession Image
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7404235247014569559
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5462510071559864326}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 909271087739711220}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7896497708396401296
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5462510071559864326}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &6993077635068771622
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5462510071559864326}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Texture: {fileID: 2800000, guid: d19d0fed4745dc14fa93e86993f9f2d6, type: 3}
|
||||
m_UVRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1
|
||||
height: 1
|
||||
--- !u!114 &1252979331277989500
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5462510071559864326}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: 32
|
||||
m_PreferredHeight: 32
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &5643565463360785033
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -759,7 +666,6 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 2008335870574158510}
|
||||
- {fileID: 7404235247014569559}
|
||||
- {fileID: 6674238761151365000}
|
||||
- {fileID: 5589171621630283209}
|
||||
- {fileID: 1348356180262758599}
|
||||
@@ -768,8 +674,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: -600}
|
||||
m_SizeDelta: {x: 0, y: -1080}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1107237753715810996
|
||||
CanvasRenderer:
|
||||
@@ -827,7 +733,7 @@ MonoBehaviour:
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 3
|
||||
m_Spacing: 5
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 1
|
||||
@@ -851,7 +757,7 @@ MonoBehaviour:
|
||||
gameIdField: {fileID: 8058295588038032232}
|
||||
leaderField: {fileID: 3311450659768657245}
|
||||
lastPlayedField: {fileID: 5482253986454480250}
|
||||
professionImage: {fileID: 6993077635068771622}
|
||||
goButton: {fileID: 0}
|
||||
dropButton: {fileID: 4081637582368108930}
|
||||
--- !u!114 &2208043217657811503
|
||||
MonoBehaviour:
|
||||
|
||||
+1
-6
@@ -10,7 +10,7 @@ public class RunningGameItem : MonoBehaviour {
|
||||
public TextMeshProUGUI gameIdField;
|
||||
public TextMeshProUGUI leaderField;
|
||||
public TextMeshProUGUI lastPlayedField;
|
||||
public RawImage professionImage;
|
||||
public Button goButton;
|
||||
public Button dropButton;
|
||||
|
||||
public Action<long> GoCallback;
|
||||
@@ -43,11 +43,6 @@ public class RunningGameItem : MonoBehaviour {
|
||||
leaderName,
|
||||
DisplayNames.ProfessionNames[leader.Profession]);
|
||||
|
||||
// Set profession image
|
||||
var textures = gameObject.GetComponentInParent<EagleCommonTextures>();
|
||||
professionImage.texture = textures.Profession(leader.Profession);
|
||||
professionImage.color = professionImage.texture != null ? Color.white : Color.clear;
|
||||
|
||||
// Display last played time in user's local timezone
|
||||
if (lastPlayedField != null) {
|
||||
if (lastPlayedTimestampMillis > 0) {
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates a beasts/wildlife particle effect with circling birds (vultures/crows).
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class BeastsEffect : MonoBehaviour {
|
||||
[Header("Bird Settings")]
|
||||
public Texture2D birdTexture;
|
||||
public Material birdMaterial; // Assign in prefab to ensure shader is included in build
|
||||
public float birdCount = 5f;
|
||||
public float birdLifetime = 8f;
|
||||
public float birdSize = 12f;
|
||||
public float circleRadius = 35f;
|
||||
public float circleSpeed = 1f;
|
||||
public Color birdColor = Color.white;
|
||||
|
||||
[Header("Flight Pattern")]
|
||||
public float verticalOscillation = 10f;
|
||||
public float radiusVariation = 10f;
|
||||
|
||||
private ParticleSystem _birdSystem;
|
||||
private Material _materialInstance;
|
||||
|
||||
void Awake() { SetupParticleSystems(); }
|
||||
|
||||
private void SetupParticleSystems() {
|
||||
var birdObj = new GameObject("BirdEffect");
|
||||
birdObj.transform.SetParent(transform, false);
|
||||
_birdSystem = birdObj.AddComponent<ParticleSystem>();
|
||||
SetupBirdSystem();
|
||||
}
|
||||
|
||||
private void SetupBirdSystem() {
|
||||
var main = _birdSystem.main;
|
||||
main.playOnAwake = true;
|
||||
main.loop = true;
|
||||
main.startLifetime = birdLifetime;
|
||||
main.startSpeed = 0f;
|
||||
main.startSize = birdSize;
|
||||
main.startColor = birdColor;
|
||||
main.startRotation3D = true;
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 10;
|
||||
|
||||
var emission = _birdSystem.emission;
|
||||
emission.enabled = true;
|
||||
// Emit in bursts to maintain constant population
|
||||
emission.rateOverTime = 0f;
|
||||
emission.SetBursts(
|
||||
new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, (short)birdCount) });
|
||||
|
||||
// Start on circle perimeter
|
||||
var shape = _birdSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Circle;
|
||||
shape.radius = circleRadius;
|
||||
shape.radiusThickness = 0f; // Only emit on edge
|
||||
shape.rotation = new Vector3(-90f, 0f, 0f);
|
||||
shape.arc = 360f;
|
||||
shape.arcMode = ParticleSystemShapeMultiModeValue.Random;
|
||||
|
||||
// Orbital velocity for circling motion
|
||||
var velocityOverLifetime = _birdSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.space = ParticleSystemSimulationSpace.Local;
|
||||
// All axes must use same curve mode (MinMaxCurve)
|
||||
velocityOverLifetime.orbitalX = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
velocityOverLifetime.orbitalY =
|
||||
new ParticleSystem.MinMaxCurve(circleSpeed, circleSpeed);
|
||||
velocityOverLifetime.orbitalZ = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
velocityOverLifetime.radial = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
|
||||
// Add vertical bobbing with noise
|
||||
var noise = _birdSystem.noise;
|
||||
noise.enabled = true;
|
||||
noise.separateAxes = true;
|
||||
noise.strengthX = radiusVariation;
|
||||
noise.strengthY = verticalOscillation;
|
||||
noise.strengthZ = 0f;
|
||||
noise.frequency = 0.3f;
|
||||
noise.scrollSpeed = 0.5f;
|
||||
noise.damping = false;
|
||||
noise.octaveCount = 1;
|
||||
|
||||
// Rotate to face direction of travel
|
||||
var rotationOverLifetime = _birdSystem.rotationOverLifetime;
|
||||
rotationOverLifetime.enabled = true;
|
||||
rotationOverLifetime.separateAxes = true;
|
||||
// Slight banking as they turn
|
||||
rotationOverLifetime.z = new ParticleSystem.MinMaxCurve(-0.1f, 0.1f);
|
||||
|
||||
// No fade - birds stay visible while circling
|
||||
var colorOverLifetime = _birdSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
var gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(Color.white, 0f),
|
||||
new GradientColorKey(Color.white, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(0f, 0f),
|
||||
new GradientAlphaKey(1f, 0.05f),
|
||||
new GradientAlphaKey(1f, 0.95f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
});
|
||||
colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient);
|
||||
|
||||
var renderer = _birdSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
// Use billboard facing camera
|
||||
renderer.renderMode = ParticleSystemRenderMode.Billboard;
|
||||
renderer.alignment = ParticleSystemRenderSpace.View;
|
||||
|
||||
// Use provided material if available (ensures shader is in build)
|
||||
if (birdMaterial != null) {
|
||||
_materialInstance = new Material(birdMaterial);
|
||||
if (birdTexture != null) { _materialInstance.mainTexture = birdTexture; }
|
||||
renderer.material = _materialInstance;
|
||||
} else {
|
||||
// Fallback for editor testing
|
||||
var particleShader = Shader.Find("Particles/Standard Unlit");
|
||||
if (particleShader != null) {
|
||||
_materialInstance = new Material(particleShader);
|
||||
_materialInstance.SetFloat("_ColorMode", 1);
|
||||
_materialInstance.SetFloat("_Surface", 1);
|
||||
_materialInstance.SetFloat("_Blend", 0);
|
||||
_materialInstance.SetInt(
|
||||
"_SrcBlend",
|
||||
(int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
_materialInstance.SetInt(
|
||||
"_DstBlend",
|
||||
(int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
_materialInstance.EnableKeyword("_ALPHABLEND_ON");
|
||||
_materialInstance.renderQueue = 3000;
|
||||
if (birdTexture != null) { _materialInstance.mainTexture = birdTexture; }
|
||||
renderer.material = _materialInstance;
|
||||
}
|
||||
}
|
||||
renderer.sortingOrder = 102;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_materialInstance != null) { Destroy(_materialInstance); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5ee82ebc029341c4858ef5b7e7cf201
|
||||
@@ -1,220 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates a blizzard particle effect with falling snowflakes.
|
||||
/// Uses province masking shader to clip particles to province boundaries.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class BlizzardEffect : MonoBehaviour {
|
||||
[Header("Snowflake Settings")]
|
||||
public Texture2D snowflakeTexture;
|
||||
public Material particleMaterial; // Assign in prefab to ensure shader is in build
|
||||
public float snowEmitRate = 40f; // More particles
|
||||
public float snowFallSpeed = 50f;
|
||||
public float snowLifetime = 4f; // Longer lifetime
|
||||
public float snowSize = 10f; // Slightly larger
|
||||
public float snowDrift = 15f; // More drift
|
||||
public Color snowColor = Color.white;
|
||||
|
||||
[Header("Wind Settings")]
|
||||
public float windStrength = 5f;
|
||||
public float windVariation = 3f;
|
||||
|
||||
[Header("Province Masking")]
|
||||
public Texture2D provinceIdMap;
|
||||
public int targetProvinceId;
|
||||
|
||||
private ParticleSystem _snowSystem;
|
||||
private Material _material;
|
||||
private RectTransform _mapContainer;
|
||||
private Canvas _canvas;
|
||||
private Vector3 _baseShapeScale;
|
||||
private Vector3 _baseShapePosition;
|
||||
private float _baseEmitRate;
|
||||
private float _lastZoom = 1f;
|
||||
|
||||
void Awake() { SetupParticleSystems(); }
|
||||
|
||||
private void SetupParticleSystems() {
|
||||
var snowObj = new GameObject("SnowEffect");
|
||||
snowObj.transform.SetParent(transform, false);
|
||||
_snowSystem = snowObj.AddComponent<ParticleSystem>();
|
||||
SetupSnowSystem();
|
||||
}
|
||||
|
||||
private void SetupSnowSystem() {
|
||||
var main = _snowSystem.main;
|
||||
main.playOnAwake = true;
|
||||
main.loop = true;
|
||||
main.startLifetime = snowLifetime;
|
||||
main.startSpeed = 0f;
|
||||
main.startSize = new ParticleSystem.MinMaxCurve(snowSize * 0.5f, snowSize);
|
||||
main.startColor = snowColor;
|
||||
main.startRotation = new ParticleSystem.MinMaxCurve(0f, Mathf.PI * 2f);
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 300; // More particles for full coverage
|
||||
|
||||
var emission = _snowSystem.emission;
|
||||
emission.enabled = true;
|
||||
emission.rateOverTime = snowEmitRate;
|
||||
_baseEmitRate = snowEmitRate;
|
||||
|
||||
// Emit across full province height - particles spawn at varying Y positions
|
||||
var shape = _snowSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Box;
|
||||
shape.scale = new Vector3(150f, 150f, 1f); // Wide and very tall emission area
|
||||
shape.position = new Vector3(0f, 40f, 0f); // Centered above effect origin
|
||||
|
||||
// Store base values for zoom compensation
|
||||
_baseShapeScale = shape.scale;
|
||||
_baseShapePosition = shape.position;
|
||||
|
||||
// Fall downward with horizontal drift
|
||||
var velocityOverLifetime = _snowSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.space = ParticleSystemSimulationSpace.Local;
|
||||
velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(
|
||||
-snowDrift - windStrength,
|
||||
snowDrift + windStrength);
|
||||
velocityOverLifetime.y = new ParticleSystem.MinMaxCurve(-snowFallSpeed, -snowFallSpeed);
|
||||
velocityOverLifetime.z = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
|
||||
// Gentle rotation as they fall
|
||||
var rotationOverLifetime = _snowSystem.rotationOverLifetime;
|
||||
rotationOverLifetime.enabled = true;
|
||||
rotationOverLifetime.z = new ParticleSystem.MinMaxCurve(-0.5f, 0.5f);
|
||||
|
||||
// Use noise for fluttering motion
|
||||
var noise = _snowSystem.noise;
|
||||
noise.enabled = true;
|
||||
noise.strength = windVariation;
|
||||
noise.frequency = 0.5f;
|
||||
noise.scrollSpeed = 0.2f;
|
||||
noise.damping = false;
|
||||
|
||||
// Fade in and out
|
||||
var colorOverLifetime = _snowSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
var gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(Color.white, 0f),
|
||||
new GradientColorKey(Color.white, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(0f, 0f),
|
||||
new GradientAlphaKey(1f, 0.1f),
|
||||
new GradientAlphaKey(1f, 0.8f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
});
|
||||
colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient);
|
||||
|
||||
var renderer = _snowSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
// Use provided material if available (ensures shader is in build)
|
||||
if (particleMaterial != null) {
|
||||
_material = new Material(particleMaterial);
|
||||
_material.renderQueue = 3000;
|
||||
if (snowflakeTexture != null) { _material.mainTexture = snowflakeTexture; }
|
||||
renderer.material = _material;
|
||||
} else {
|
||||
// Fallback for editor testing
|
||||
string[] shaderNames = {
|
||||
"Eagle/ProvinceParticle",
|
||||
"Universal Render Pipeline/Particles/Unlit",
|
||||
"Particles/Standard Unlit",
|
||||
"Legacy Shaders/Particles/Alpha Blended"
|
||||
};
|
||||
|
||||
Shader shader = null;
|
||||
foreach (var name in shaderNames) {
|
||||
shader = Shader.Find(name);
|
||||
if (shader != null) break;
|
||||
}
|
||||
|
||||
if (shader != null) {
|
||||
_material = new Material(shader);
|
||||
_material.renderQueue = 3000;
|
||||
if (snowflakeTexture != null) { _material.mainTexture = snowflakeTexture; }
|
||||
renderer.material = _material;
|
||||
} else {
|
||||
Debug.LogWarning("BlizzardEffect: No particle shader found");
|
||||
}
|
||||
}
|
||||
renderer.sortingOrder = 100;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetProvinceId(int provinceId) {
|
||||
targetProvinceId = provinceId;
|
||||
if (_material != null) { _material.SetFloat("_TargetProvinceId", provinceId); }
|
||||
}
|
||||
|
||||
public void SetProvinceIdMap(Texture2D idMap) {
|
||||
provinceIdMap = idMap;
|
||||
if (_material != null) { _material.SetTexture("_ProvinceIDMap", idMap); }
|
||||
}
|
||||
|
||||
public void SetMapContainer(RectTransform container) {
|
||||
_mapContainer = container;
|
||||
_canvas = container?.GetComponentInParent<Canvas>();
|
||||
}
|
||||
|
||||
void LateUpdate() {
|
||||
UpdateMapBounds();
|
||||
UpdateZoomCompensation();
|
||||
}
|
||||
|
||||
private void UpdateMapBounds() {
|
||||
if (_mapContainer == null || _material == null) { return; }
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
_mapContainer.GetWorldCorners(corners);
|
||||
|
||||
Camera cam = _canvas?.worldCamera ?? Camera.main;
|
||||
Vector3 screenBL, screenTR;
|
||||
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay) {
|
||||
screenBL = corners[0];
|
||||
screenTR = corners[2];
|
||||
} else {
|
||||
screenBL = cam != null ? cam.WorldToScreenPoint(corners[0]) : corners[0];
|
||||
screenTR = cam != null ? cam.WorldToScreenPoint(corners[2]) : corners[2];
|
||||
}
|
||||
|
||||
var mapBounds = new Vector4(
|
||||
screenBL.x,
|
||||
screenBL.y,
|
||||
screenTR.x - screenBL.x,
|
||||
screenTR.y - screenBL.y);
|
||||
_material.SetVector("_MapBounds", mapBounds);
|
||||
}
|
||||
|
||||
private void UpdateZoomCompensation() {
|
||||
if (_mapContainer == null || _snowSystem == null) { return; }
|
||||
|
||||
float zoom = _mapContainer.localScale.x;
|
||||
if (Mathf.Approximately(zoom, _lastZoom)) { return; }
|
||||
_lastZoom = zoom;
|
||||
|
||||
// Counter-scale particle system to maintain screen-space appearance
|
||||
float inverseZoom = 1f / zoom;
|
||||
_snowSystem.transform.localScale = Vector3.one * inverseZoom;
|
||||
|
||||
// Expand emission area to still cover the province
|
||||
var shape = _snowSystem.shape;
|
||||
shape.scale = _baseShapeScale * zoom;
|
||||
shape.position = _baseShapePosition * zoom;
|
||||
|
||||
// Increase emission rate to maintain screen-space density (area scales as zoom²)
|
||||
var emission = _snowSystem.emission;
|
||||
emission.rateOverTime = _baseEmitRate * zoom * zoom;
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_material != null) { Destroy(_material); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c15980e88e57845ce920e43f29d35fcc
|
||||
+4
-5
@@ -264,16 +264,15 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private int Cost(List<int> armaments) {
|
||||
int totalCost = 0;
|
||||
double totalCost = 0.0;
|
||||
|
||||
for (int i = 0; i < armaments.Count; i++) {
|
||||
var battalion = _battalions[i];
|
||||
var armamentDelta = Math.Max(armaments[i] - battalion.Armament.Value, 0.0);
|
||||
// Ceiling per-battalion to match server calculation
|
||||
totalCost += (int)Math.Ceiling(_costs[i] * armamentDelta);
|
||||
|
||||
totalCost += _costs[i] * Math.Max(armaments[i] - battalion.Armament.Value, 0.0);
|
||||
}
|
||||
|
||||
return totalCost;
|
||||
return (int)Math.Ceiling(Math.Max(totalCost, 0.0));
|
||||
}
|
||||
|
||||
private int Cost() { return Cost(_newArmaments); }
|
||||
|
||||
+30
-26
@@ -45,9 +45,14 @@ namespace eagle {
|
||||
public Toggle demandTributeToggle;
|
||||
public Toggle withdrawToggle;
|
||||
public Toggle safePassageToggle;
|
||||
public ToggleGroup decisionToggleGroup;
|
||||
|
||||
public GameObject tributeContainer;
|
||||
public GameObject advanceImage;
|
||||
public GameObject demandTributeImage;
|
||||
public GameObject withdrawImage;
|
||||
public GameObject safePassageImage;
|
||||
|
||||
public Image goldIcon;
|
||||
public Image foodIcon;
|
||||
public Slider goldSlider;
|
||||
public Slider foodSlider;
|
||||
public TMP_Text goldLabel;
|
||||
@@ -135,10 +140,21 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureToggle(advanceToggle, enableAttack);
|
||||
ConfigureToggle(withdrawToggle, enableWithdraw);
|
||||
ConfigureToggle(demandTributeToggle, enableDemandTribute);
|
||||
ConfigureToggle(safePassageToggle, enableSafePassage);
|
||||
advanceToggle.enabled = enableAttack;
|
||||
advanceToggle.gameObject.SetActive(enableAttack);
|
||||
advanceImage.SetActive(enableAttack);
|
||||
|
||||
withdrawToggle.enabled = enableWithdraw;
|
||||
withdrawToggle.gameObject.SetActive(enableWithdraw);
|
||||
withdrawImage.SetActive(enableWithdraw);
|
||||
|
||||
demandTributeToggle.enabled = enableDemandTribute;
|
||||
demandTributeToggle.gameObject.SetActive(enableDemandTribute);
|
||||
demandTributeImage.SetActive(enableDemandTribute);
|
||||
|
||||
safePassageToggle.enabled = enableSafePassage;
|
||||
safePassageToggle.gameObject.SetActive(enableSafePassage);
|
||||
safePassageImage.SetActive(enableSafePassage);
|
||||
|
||||
goldSlider.minValue = 0;
|
||||
goldSlider.maxValue = maxGoldTribute;
|
||||
@@ -186,7 +202,14 @@ namespace eagle {
|
||||
foodLabel.text = ((int)newValue).ToString();
|
||||
}
|
||||
|
||||
private void EnableSliders(bool newEnabled) { tributeContainer.SetActive(newEnabled); }
|
||||
private void EnableSliders(bool newEnabled) {
|
||||
goldIcon.gameObject.SetActive(newEnabled);
|
||||
foodIcon.gameObject.SetActive(newEnabled);
|
||||
goldSlider.gameObject.SetActive(newEnabled);
|
||||
foodSlider.gameObject.SetActive(newEnabled);
|
||||
goldLabel.gameObject.SetActive(newEnabled);
|
||||
foodLabel.gameObject.SetActive(newEnabled);
|
||||
}
|
||||
|
||||
public void ToggleChanged(bool value) {
|
||||
bool enableSliders =
|
||||
@@ -195,24 +218,5 @@ namespace eagle {
|
||||
|
||||
EnableSliders(enableSliders);
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
|
||||
private void ConfigureToggle(Toggle toggle, bool available) {
|
||||
if (toggle == null) return;
|
||||
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
|
||||
if (!available) { toggle.isOn = false; }
|
||||
|
||||
toggle.group = available ? decisionToggleGroup : null;
|
||||
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-26
@@ -23,7 +23,6 @@ namespace eagle {
|
||||
|
||||
public Toggle AdvanceToggle;
|
||||
public Toggle WithdrawToggle;
|
||||
public ToggleGroup decisionToggleGroup;
|
||||
|
||||
private FreeForAllDecisionAvailableCommand FreeForAllDecisionCommand =>
|
||||
_availableCommand.FreeForAllDecisionCommand;
|
||||
@@ -64,32 +63,10 @@ namespace eagle {
|
||||
row.Origin = _model.Provinces[army.OriginProvinceId].Name;
|
||||
});
|
||||
|
||||
bool withdrawAvailable = FreeForAllDecisionCommand.AvailableDecisions.Any(
|
||||
x => x.SealedValueCase == AttackDecisionType.SealedValueOneofCase.Withdraw);
|
||||
|
||||
ConfigureToggle(AdvanceToggle, true);
|
||||
ConfigureToggle(WithdrawToggle, withdrawAvailable);
|
||||
|
||||
AdvanceToggle.isOn = true;
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
|
||||
private void ConfigureToggle(Toggle toggle, bool available) {
|
||||
if (toggle == null) return;
|
||||
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
|
||||
if (!available) { toggle.isOn = false; }
|
||||
|
||||
toggle.group = available ? decisionToggleGroup : null;
|
||||
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
AdvanceToggle.enabled = true;
|
||||
WithdrawToggle.enabled = FreeForAllDecisionCommand.AvailableDecisions.Any(
|
||||
x => x.SealedValueCase == AttackDecisionType.SealedValueOneofCase.Withdraw);
|
||||
}
|
||||
|
||||
// Use this for initialization
|
||||
|
||||
+42
-40
@@ -15,7 +15,12 @@ namespace eagle {
|
||||
public Toggle ExileToggle;
|
||||
public Toggle ExecuteToggle;
|
||||
public Toggle ReturnToggle;
|
||||
public ToggleGroup decisionToggleGroup;
|
||||
|
||||
public RawImage RecruitIcon;
|
||||
public RawImage ImprisonIcon;
|
||||
public RawImage ExileIcon;
|
||||
public RawImage ExecuteIcon;
|
||||
public RawImage ReturnIcon;
|
||||
|
||||
public HeroDetailsController heroDetails;
|
||||
|
||||
@@ -90,54 +95,51 @@ namespace eagle {
|
||||
|
||||
messageUpdater.TextId = SelectedHeroWithOptions.MessageId;
|
||||
|
||||
var options = SelectedHeroWithOptions.Options;
|
||||
ConfigureToggle(
|
||||
RecruitToggle,
|
||||
options.Contains(CapturedHeroOption.RecruitCapturedHeroOption));
|
||||
ConfigureToggle(
|
||||
ImprisonToggle,
|
||||
options.Contains(CapturedHeroOption.ImprisonCapturedHeroOption));
|
||||
ConfigureToggle(
|
||||
ExileToggle,
|
||||
options.Contains(CapturedHeroOption.ExileCapturedHeroOption));
|
||||
ConfigureToggle(
|
||||
// The order here is important: the last available toggle in this list
|
||||
// will be on by default
|
||||
ConditionallyEnable(
|
||||
ExecuteToggle,
|
||||
options.Contains(CapturedHeroOption.ExecuteCapturedHeroOption));
|
||||
ConfigureToggle(
|
||||
ExecuteIcon,
|
||||
CapturedHeroOption.ExecuteCapturedHeroOption);
|
||||
ConditionallyEnable(ExileToggle, ExileIcon, CapturedHeroOption.ExileCapturedHeroOption);
|
||||
ConditionallyEnable(
|
||||
ReturnToggle,
|
||||
options.Contains(CapturedHeroOption.ReturnCapturedHeroOption));
|
||||
|
||||
// Select first available option (priority order: Recruit > Imprison > Exile > Execute >
|
||||
// Return)
|
||||
if (options.Contains(CapturedHeroOption.RecruitCapturedHeroOption))
|
||||
RecruitToggle.isOn = true;
|
||||
else if (options.Contains(CapturedHeroOption.ImprisonCapturedHeroOption))
|
||||
ImprisonToggle.isOn = true;
|
||||
else if (options.Contains(CapturedHeroOption.ExileCapturedHeroOption))
|
||||
ExileToggle.isOn = true;
|
||||
else if (options.Contains(CapturedHeroOption.ExecuteCapturedHeroOption))
|
||||
ExecuteToggle.isOn = true;
|
||||
else if (options.Contains(CapturedHeroOption.ReturnCapturedHeroOption))
|
||||
ReturnToggle.isOn = true;
|
||||
ReturnIcon,
|
||||
CapturedHeroOption.ReturnCapturedHeroOption);
|
||||
ConditionallyEnable(
|
||||
ImprisonToggle,
|
||||
ImprisonIcon,
|
||||
CapturedHeroOption.ImprisonCapturedHeroOption);
|
||||
ConditionallyEnable(
|
||||
RecruitToggle,
|
||||
RecruitIcon,
|
||||
CapturedHeroOption.RecruitCapturedHeroOption);
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
private bool ConditionallyEnable(Toggle toggle, CapturedHeroOption option) {
|
||||
bool enable = SelectedHeroWithOptions.Options.Contains(option);
|
||||
toggle.gameObject.SetActive(enable);
|
||||
|
||||
private void ConfigureToggle(Toggle toggle, bool available) {
|
||||
if (toggle == null) return;
|
||||
if (enable) {
|
||||
ExecuteToggle.isOn = false;
|
||||
ExileToggle.isOn = false;
|
||||
ImprisonToggle.isOn = false;
|
||||
RecruitToggle.isOn = false;
|
||||
ReturnToggle.isOn = false;
|
||||
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
toggle.isOn = true;
|
||||
} else
|
||||
toggle.isOn = false;
|
||||
|
||||
if (!available) { toggle.isOn = false; }
|
||||
return enable;
|
||||
}
|
||||
|
||||
toggle.group = available ? decisionToggleGroup : null;
|
||||
private void ConditionallyEnable(Toggle toggle, RawImage img, CapturedHeroOption option) {
|
||||
bool enable = ConditionallyEnable(toggle, option);
|
||||
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
Color color = img.color;
|
||||
color.a = enable ? 1.0f : 0.25f;
|
||||
img.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-92
@@ -15,6 +15,7 @@ namespace eagle {
|
||||
|
||||
public class ImproveCommandSelector : CommandSelector {
|
||||
public HeroDropdownController heroDropdownController;
|
||||
public Toggle lockToggle;
|
||||
|
||||
// Toggle buttons for each improvement type (assign to same ToggleGroup in Unity)
|
||||
public Toggle economyToggle;
|
||||
@@ -23,12 +24,6 @@ namespace eagle {
|
||||
public Toggle devastationToggle;
|
||||
public ToggleGroup improvementToggleGroup;
|
||||
|
||||
// Lock toggles - one per improvement type
|
||||
public Toggle economyLockToggle;
|
||||
public Toggle agricultureLockToggle;
|
||||
public Toggle infrastructureLockToggle;
|
||||
public Toggle devastationLockToggle;
|
||||
|
||||
// Labels showing type name and value
|
||||
public TMP_Text economyLabel;
|
||||
public TMP_Text agricultureLabel;
|
||||
@@ -125,38 +120,6 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
private Toggle GetLockToggleForType(ImprovementType type) {
|
||||
switch (type) {
|
||||
case ImprovementType.Economy: return economyLockToggle;
|
||||
case ImprovementType.Agriculture: return agricultureLockToggle;
|
||||
case ImprovementType.Infrastructure: return infrastructureLockToggle;
|
||||
case ImprovementType.Devastation: return devastationLockToggle;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLockToggleVisibility() {
|
||||
var selectedType = SelectedType;
|
||||
var types = new[] {
|
||||
ImprovementType.Economy,
|
||||
ImprovementType.Agriculture,
|
||||
ImprovementType.Infrastructure,
|
||||
ImprovementType.Devastation
|
||||
};
|
||||
foreach (var type in types) {
|
||||
var lockToggle = GetLockToggleForType(type);
|
||||
if (lockToggle != null) {
|
||||
// Only show lock for the selected type, hide all others
|
||||
lockToggle.gameObject.SetActive(type == selectedType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnImprovementToggleChanged(bool isOn) {
|
||||
// Only respond when a toggle is turned on (ToggleGroup handles turning off others)
|
||||
if (isOn) { UpdateLockToggleVisibility(); }
|
||||
}
|
||||
|
||||
private TMP_Text GetLabelForType(ImprovementType type) {
|
||||
switch (type) {
|
||||
case ImprovementType.Economy: return economyLabel;
|
||||
@@ -242,43 +205,23 @@ namespace eagle {
|
||||
ImprovementType.Devastation,
|
||||
improveCommand.AvailableTypes.Contains(ImprovementType.Devastation));
|
||||
|
||||
// Wire up toggle change listeners for lock visibility updates
|
||||
if (economyToggle != null) {
|
||||
economyToggle.onValueChanged.AddListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (agricultureToggle != null) {
|
||||
agricultureToggle.onValueChanged.AddListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (infrastructureToggle != null) {
|
||||
infrastructureToggle.onValueChanged.AddListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (devastationToggle != null) {
|
||||
devastationToggle.onValueChanged.AddListener(OnImprovementToggleChanged);
|
||||
}
|
||||
|
||||
// Initialize all lock toggles to off
|
||||
if (economyLockToggle != null) economyLockToggle.isOn = false;
|
||||
if (agricultureLockToggle != null) agricultureLockToggle.isOn = false;
|
||||
if (infrastructureLockToggle != null) infrastructureLockToggle.isOn = false;
|
||||
if (devastationLockToggle != null) devastationLockToggle.isOn = false;
|
||||
|
||||
// Determine which type to select and whether to lock it
|
||||
// Determine which type to select
|
||||
ImprovementType selectedType;
|
||||
if (improveCommand.LockedType != ImprovementType.None) {
|
||||
var lockedType = improveCommand.LockedType;
|
||||
if (improveCommand.AvailableTypes.Contains(lockedType)) {
|
||||
selectedType = lockedType;
|
||||
var lockToggle = GetLockToggleForType(lockedType);
|
||||
if (lockToggle != null) lockToggle.isOn = true;
|
||||
lockToggle.isOn = true;
|
||||
} else {
|
||||
selectedType = GetDefaultImprovementType();
|
||||
lockToggle.isOn = false;
|
||||
}
|
||||
} else {
|
||||
selectedType = GetDefaultImprovementType();
|
||||
lockToggle.isOn = false;
|
||||
}
|
||||
|
||||
SelectType(selectedType);
|
||||
UpdateLockToggleVisibility();
|
||||
}
|
||||
|
||||
public override AvailableCommand.SealedValueOneofCase CommandType =>
|
||||
@@ -297,35 +240,13 @@ namespace eagle {
|
||||
heroDropdownController.SelectedHeroId = heroId;
|
||||
}
|
||||
|
||||
public override SelectedCommand Command {
|
||||
get {
|
||||
var selectedType = SelectedType;
|
||||
var lockToggle = GetLockToggleForType(selectedType);
|
||||
return new SelectedCommand {
|
||||
ImproveCommand =
|
||||
new ImproveSelectedCommand {
|
||||
ImprovementType = selectedType,
|
||||
ActingHeroId = heroDropdownController.SelectedHeroId,
|
||||
LockType = lockToggle != null && lockToggle.isOn
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
// Clean up listeners
|
||||
if (economyToggle != null) {
|
||||
economyToggle.onValueChanged.RemoveListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (agricultureToggle != null) {
|
||||
agricultureToggle.onValueChanged.RemoveListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (infrastructureToggle != null) {
|
||||
infrastructureToggle.onValueChanged.RemoveListener(OnImprovementToggleChanged);
|
||||
}
|
||||
if (devastationToggle != null) {
|
||||
devastationToggle.onValueChanged.RemoveListener(OnImprovementToggleChanged);
|
||||
}
|
||||
}
|
||||
public override SelectedCommand Command => new SelectedCommand {
|
||||
ImproveCommand =
|
||||
new ImproveSelectedCommand {
|
||||
ImprovementType = SelectedType,
|
||||
ActingHeroId = heroDropdownController.SelectedHeroId,
|
||||
LockType = lockToggle.isOn
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+58
-82
@@ -21,7 +21,12 @@ namespace eagle {
|
||||
public Toggle ExileToggle;
|
||||
public Toggle ExecuteToggle;
|
||||
public Toggle ReturnToggle;
|
||||
public ToggleGroup decisionToggleGroup;
|
||||
|
||||
public RawImage ReleaseIcon;
|
||||
public RawImage MoveIcon;
|
||||
public RawImage ExileIcon;
|
||||
public RawImage ExecuteIcon;
|
||||
public RawImage ReturnIcon;
|
||||
|
||||
public TMP_Dropdown moveToDropdown;
|
||||
|
||||
@@ -71,33 +76,6 @@ namespace eagle {
|
||||
public override string CommitWarningText =>
|
||||
$"This will create a treaty with {_model.FactionName(SelectedHero.FactionId.Value)}";
|
||||
|
||||
private List<PrisonerManagementOption> MoveOptions =>
|
||||
SelectedPrisonerToManage.AvailableOptions
|
||||
.Where(opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Move)
|
||||
.ToList();
|
||||
|
||||
protected override ProvinceId? TargetedProvince {
|
||||
get {
|
||||
if (MoveToggle.isOn && moveToDropdown.value >= 0 &&
|
||||
moveToDropdown.value < MoveOptions.Count) {
|
||||
return MoveOptions[moveToDropdown.value].Move.ToProvinceId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set {
|
||||
if (!value.HasValue) return;
|
||||
|
||||
var moveOptions = MoveOptions;
|
||||
int index = moveOptions.FindIndex(opt => opt.Move.ToProvinceId == value.Value);
|
||||
if (index != -1) {
|
||||
MoveToggle.isOn = true;
|
||||
moveToDropdown.value = index;
|
||||
moveToDropdown.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PrisonerManagementOption SelectedOption {
|
||||
get {
|
||||
if (ExecuteToggle.isOn)
|
||||
@@ -165,43 +143,28 @@ namespace eagle {
|
||||
|
||||
MessageText.text = GetQuestMessagesForPrisoner(SelectedHero.Id);
|
||||
|
||||
var options = SelectedPrisonerToManage.AvailableOptions;
|
||||
bool hasRelease = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Release);
|
||||
bool hasMove = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Move);
|
||||
bool hasExile = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Exile);
|
||||
bool hasExecute = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Execute);
|
||||
bool hasReturn = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Return);
|
||||
|
||||
ConfigureToggle(ReleaseToggle, hasRelease);
|
||||
ConfigureToggle(MoveToggle, hasMove);
|
||||
ConfigureToggle(ExileToggle, hasExile);
|
||||
ConfigureToggle(ExecuteToggle, hasExecute);
|
||||
ConfigureToggle(ReturnToggle, hasReturn);
|
||||
|
||||
// Select first available option (priority order: Release > Move > Exile > Execute >
|
||||
// Return)
|
||||
if (hasRelease) ReleaseToggle.isOn = true;
|
||||
else if (hasMove)
|
||||
MoveToggle.isOn = true;
|
||||
else if (hasExile)
|
||||
ExileToggle.isOn = true;
|
||||
else if (hasExecute)
|
||||
ExecuteToggle.isOn = true;
|
||||
else if (hasReturn)
|
||||
ReturnToggle.isOn = true;
|
||||
|
||||
// Update dropdown visibility based on selected toggle
|
||||
moveToDropdown.gameObject.SetActive(MoveToggle.isOn);
|
||||
// The order here is important: the last available toggle in this list
|
||||
// will be on by default
|
||||
ConditionallyEnable(
|
||||
ExecuteToggle,
|
||||
ExecuteIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Execute);
|
||||
ConditionallyEnable(
|
||||
ExileToggle,
|
||||
ExileIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Exile);
|
||||
ConditionallyEnable(
|
||||
ReturnToggle,
|
||||
ReturnIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Return);
|
||||
ConditionallyEnable(
|
||||
MoveToggle,
|
||||
MoveIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Move);
|
||||
ConditionallyEnable(
|
||||
ReleaseToggle,
|
||||
ReleaseIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Release);
|
||||
}
|
||||
|
||||
private string GetQuestMessagesForPrisoner(HeroId prisonerId) {
|
||||
@@ -219,7 +182,7 @@ namespace eagle {
|
||||
if (!matches) continue;
|
||||
|
||||
var heroName = GetHeroDisplayName(uh.HeroId);
|
||||
questMessages.Add($"{heroName} wants you to {actionVerb}.");
|
||||
questMessages.Add($"{heroName} wants you to {actionVerb} this prisoner.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,24 +194,24 @@ namespace eagle {
|
||||
switch (details.SealedValueCase) {
|
||||
case QuestDetails.SealedValueOneofCase.ExecutePrisonerQuest:
|
||||
if (details.ExecutePrisonerQuest.PrisonerHeroId == prisonerId) {
|
||||
return (true, "execute this prisoner");
|
||||
return (true, "execute");
|
||||
}
|
||||
break;
|
||||
case QuestDetails.SealedValueOneofCase.ExilePrisonerQuest:
|
||||
if (details.ExilePrisonerQuest.PrisonerHeroId == prisonerId) {
|
||||
return (true, "exile this prisoner");
|
||||
return (true, "exile");
|
||||
}
|
||||
break;
|
||||
case QuestDetails.SealedValueOneofCase.FreePrisonerQuest:
|
||||
if (details.FreePrisonerQuest.PrisonerHeroId == prisonerId) {
|
||||
return (true, "release this prisoner");
|
||||
return (true, "release");
|
||||
}
|
||||
break;
|
||||
case QuestDetails.SealedValueOneofCase.ReturnPrisonerQuest:
|
||||
if (details.ReturnPrisonerQuest.PrisonerHeroId == prisonerId) {
|
||||
var factionName =
|
||||
_model.FactionName(details.ReturnPrisonerQuest.ToFactionId);
|
||||
return (true, $"return this prisoner to {factionName}");
|
||||
return (true, $"return to {factionName}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -262,23 +225,36 @@ namespace eagle {
|
||||
: "A free hero";
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
private bool ConditionallyEnable(
|
||||
Toggle toggle,
|
||||
PrisonerManagementOption.SealedValueOneofCase option) {
|
||||
bool enable = SelectedPrisonerToManage.AvailableOptions.Any(
|
||||
opt => opt.SealedValueCase == option);
|
||||
toggle.gameObject.SetActive(enable);
|
||||
|
||||
private void ConfigureToggle(Toggle toggle, bool available) {
|
||||
if (toggle == null) return;
|
||||
if (enable) {
|
||||
ExecuteToggle.isOn = false;
|
||||
ExileToggle.isOn = false;
|
||||
MoveToggle.isOn = false;
|
||||
ReleaseToggle.isOn = false;
|
||||
ReturnToggle.isOn = false;
|
||||
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
toggle.isOn = true;
|
||||
} else
|
||||
toggle.isOn = false;
|
||||
|
||||
if (!available) { toggle.isOn = false; }
|
||||
return enable;
|
||||
}
|
||||
|
||||
toggle.group = available ? decisionToggleGroup : null;
|
||||
private void ConditionallyEnable(
|
||||
Toggle toggle,
|
||||
RawImage img,
|
||||
PrisonerManagementOption.SealedValueOneofCase option) {
|
||||
bool enable = ConditionallyEnable(toggle, option);
|
||||
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
Color color = img.color;
|
||||
color.a = enable ? 1.0f : 0.25f;
|
||||
img.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -40,10 +40,9 @@ namespace eagle {
|
||||
public TMP_Dropdown fromDropdown;
|
||||
public TMP_Dropdown toDropdown;
|
||||
|
||||
// Columns to show/hide based on layout mode
|
||||
// Columns to hide in narrow layout mode (users select via side panels instead)
|
||||
public GameObject heroesColumn;
|
||||
public GameObject battalionsColumn;
|
||||
public GameObject battalionsColumnNarrow; // Alternate battalions view for narrow layout
|
||||
public Slider goldSlider;
|
||||
public Slider foodSlider;
|
||||
public TMP_Text goldTakenLabel;
|
||||
@@ -303,11 +302,9 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public override void OnLayoutModeChanged() {
|
||||
// Heroes always visible (user may march from different province than the viewed one)
|
||||
if (heroesColumn != null) heroesColumn.SetActive(true);
|
||||
// Toggle between wide and narrow battalion views
|
||||
// Update hero/battalion column visibility when layout changes
|
||||
if (heroesColumn != null) heroesColumn.SetActive(!IsNarrowLayout);
|
||||
if (battalionsColumn != null) battalionsColumn.SetActive(!IsNarrowLayout);
|
||||
if (battalionsColumnNarrow != null) battalionsColumnNarrow.SetActive(IsNarrowLayout);
|
||||
}
|
||||
|
||||
protected override void SetUpUI() {
|
||||
|
||||
+7
-14
@@ -16,7 +16,6 @@ namespace eagle {
|
||||
public class EventBasedUnitSelector : MonoBehaviour {
|
||||
public GameObject availableHeroesContent;
|
||||
public GameObject availableBattalionsContent;
|
||||
public GameObject availableBattalionsContentNarrow; // Optional alternate for narrow layout
|
||||
public GameObject selectedUnitsContent;
|
||||
|
||||
public GameObject availableHeroRow;
|
||||
@@ -199,20 +198,10 @@ namespace eagle {
|
||||
if (i % 2 == 1) { newRow.GetComponent<Image>().color = oddColor; }
|
||||
}
|
||||
|
||||
PopulateBattalionsContent(availableBattalionsContent, oddColor);
|
||||
if (availableBattalionsContentNarrow != null) {
|
||||
PopulateBattalionsContent(availableBattalionsContentNarrow, oddColor);
|
||||
foreach (Transform row in availableBattalionsContent.transform) {
|
||||
Destroy(row.gameObject);
|
||||
}
|
||||
|
||||
heroesAndBattalionsPanelController.UpdateTables();
|
||||
|
||||
selectionChanged.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateBattalionsContent(GameObject content, Color oddColor) {
|
||||
foreach (Transform row in content.transform) { Destroy(row.gameObject); }
|
||||
|
||||
var parentTransform = content.transform;
|
||||
parentTransform = availableBattalionsContent.transform;
|
||||
for (int i = 0; i < AvailableBattalions.Count; i++) {
|
||||
var battalion = AvailableBattalions[i];
|
||||
|
||||
@@ -228,6 +217,10 @@ namespace eagle {
|
||||
|
||||
if (i % 2 == 1) { newRow.GetComponent<Image>().color = oddColor; }
|
||||
}
|
||||
|
||||
heroesAndBattalionsPanelController.UpdateTables();
|
||||
|
||||
selectionChanged.Invoke();
|
||||
}
|
||||
|
||||
public void availableHeroButtonClicked(int index) {
|
||||
|
||||
+12
-11
@@ -23,9 +23,7 @@ namespace eagle {
|
||||
/// Attach to a TextMeshProUGUI component to display status.
|
||||
/// </summary>
|
||||
public class ConnectionStatusUI : MonoBehaviour {
|
||||
[Header("UI References")]
|
||||
public TextMeshProUGUI statusText;
|
||||
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private PersistentClientConnection _connection;
|
||||
private IGameStateProvider _gameStateProvider;
|
||||
private string _environmentName;
|
||||
@@ -41,8 +39,10 @@ namespace eagle {
|
||||
private float _timeSinceLastUpdate = 0f;
|
||||
|
||||
void Start() {
|
||||
if (statusText == null) {
|
||||
Debug.LogError("ConnectionStatusUI: statusText reference not assigned");
|
||||
_textComponent = GetComponent<TextMeshProUGUI>();
|
||||
if (_textComponent == null) {
|
||||
Debug.LogError(
|
||||
"ConnectionStatusUI must be attached to a TextMeshProUGUI component");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
@@ -78,7 +78,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
void Update() {
|
||||
if (_connection == null || statusText == null) { return; }
|
||||
if (_connection == null || _textComponent == null) { return; }
|
||||
|
||||
_timeSinceLastUpdate += Time.deltaTime;
|
||||
if (_timeSinceLastUpdate < UpdateInterval) { return; }
|
||||
@@ -97,15 +97,16 @@ namespace eagle {
|
||||
var timeUntilTest = nextTest.Value - DateTime.UtcNow;
|
||||
if (timeUntilTest.TotalSeconds > 0) {
|
||||
int seconds = Math.Min(10, (int)Math.Ceiling(timeUntilTest.TotalSeconds));
|
||||
statusText.text = $"<color=red>●</color> Server down. Retry in {seconds}s";
|
||||
_textComponent.text =
|
||||
$"<color=red>●</color> Server down. Retry in {seconds}s";
|
||||
return;
|
||||
}
|
||||
}
|
||||
statusText.text = "<color=red>●</color> Server unavailable";
|
||||
_textComponent.text = "<color=red>●</color> Server unavailable";
|
||||
return;
|
||||
} else if (circuitState == ConnectionCircuitBreaker.State.HalfOpen) {
|
||||
SetRetryButtonVisible(false);
|
||||
statusText.text = "<color=yellow>●</color> Testing connection...";
|
||||
_textComponent.text = "<color=yellow>●</color> Testing connection...";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ namespace eagle {
|
||||
(nextAttempt.Value - DateTime.UtcNow).TotalSeconds > 0;
|
||||
SetRetryButtonVisible(isCountingDown);
|
||||
|
||||
string displayText = state switch {
|
||||
string statusText = state switch {
|
||||
ConnectionState.Connected => GetConnectedStatusText(),
|
||||
ConnectionState.Connecting => "<color=yellow>●</color> Connecting...",
|
||||
ConnectionState.Disconnected => "<color=red>●</color> Disconnected",
|
||||
@@ -127,7 +128,7 @@ namespace eagle {
|
||||
_ => "<color=gray>●</color> Unknown"
|
||||
};
|
||||
|
||||
statusText.text = displayText;
|
||||
_textComponent.text = statusText;
|
||||
}
|
||||
|
||||
private string GetConnectedStatusText() {
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Curves TextMeshPro text along an arc.
|
||||
/// Attach to a GameObject with TextMeshProUGUI component.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(TextMeshProUGUI))]
|
||||
public class CurvedText : MonoBehaviour {
|
||||
[Tooltip(
|
||||
"Curvature amount. Positive curves upward (convex), negative curves downward (concave). 0 = no curve.")]
|
||||
public float curvature = 0f;
|
||||
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private bool _needsUpdate = true;
|
||||
|
||||
void Awake() { _textComponent = GetComponent<TextMeshProUGUI>(); }
|
||||
|
||||
void OnEnable() {
|
||||
TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChanged);
|
||||
_needsUpdate = true;
|
||||
}
|
||||
|
||||
void OnDisable() { TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChanged); }
|
||||
|
||||
void OnTextChanged(Object obj) {
|
||||
if (obj == _textComponent) { _needsUpdate = true; }
|
||||
}
|
||||
|
||||
void LateUpdate() {
|
||||
if (_needsUpdate && _textComponent != null) {
|
||||
_needsUpdate = false;
|
||||
ApplyCurve();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCurvature(float newCurvature) {
|
||||
curvature = newCurvature;
|
||||
_needsUpdate = true;
|
||||
}
|
||||
|
||||
private void ApplyCurve() {
|
||||
if (Mathf.Approximately(curvature, 0f)) { return; }
|
||||
|
||||
_textComponent.ForceMeshUpdate();
|
||||
|
||||
TMP_TextInfo textInfo = _textComponent.textInfo;
|
||||
if (textInfo == null || textInfo.characterCount == 0) { return; }
|
||||
|
||||
// Find the bounds of all characters
|
||||
float minX = float.MaxValue;
|
||||
float maxX = float.MinValue;
|
||||
|
||||
for (int i = 0; i < textInfo.characterCount; i++) {
|
||||
if (!textInfo.characterInfo[i].isVisible) { continue; }
|
||||
|
||||
var charInfo = textInfo.characterInfo[i];
|
||||
minX = Mathf.Min(minX, charInfo.bottomLeft.x);
|
||||
maxX = Mathf.Max(maxX, charInfo.bottomRight.x);
|
||||
}
|
||||
|
||||
float textWidth = maxX - minX;
|
||||
if (textWidth <= 0) { return; }
|
||||
|
||||
float centerX = (minX + maxX) / 2f;
|
||||
|
||||
// Apply curve to each character
|
||||
for (int i = 0; i < textInfo.characterCount; i++) {
|
||||
if (!textInfo.characterInfo[i].isVisible) { continue; }
|
||||
|
||||
var charInfo = textInfo.characterInfo[i];
|
||||
int materialIndex = charInfo.materialReferenceIndex;
|
||||
int vertexIndex = charInfo.vertexIndex;
|
||||
|
||||
Vector3[] vertices = textInfo.meshInfo[materialIndex].vertices;
|
||||
|
||||
// Get character center X position
|
||||
float charCenterX = (vertices[vertexIndex].x + vertices[vertexIndex + 2].x) / 2f;
|
||||
|
||||
// Calculate normalized position (-1 to 1) from center
|
||||
float normalizedX = (charCenterX - centerX) / (textWidth / 2f);
|
||||
|
||||
// Calculate Y offset using parabola: y = -curvature * (1 - x^2)
|
||||
// Negated because Unity Y is up, but our curvature convention has positive = convex
|
||||
// down. Scale by 0.25 to reduce intensity.
|
||||
float scaledCurvature = curvature * 0.25f;
|
||||
float yOffset = -scaledCurvature * (1f - normalizedX * normalizedX);
|
||||
|
||||
// Calculate rotation angle based on tangent of the curve
|
||||
// Derivative of y = -c * (1 - x^2) is dy/dx = 2cx
|
||||
float tangentSlope = 2f * scaledCurvature * normalizedX / (textWidth / 2f);
|
||||
float angle = Mathf.Atan(tangentSlope) * Mathf.Rad2Deg;
|
||||
|
||||
// Apply transformation to all 4 vertices of the character
|
||||
Vector3 charCenter = new Vector3(
|
||||
charCenterX,
|
||||
(vertices[vertexIndex].y + vertices[vertexIndex + 2].y) / 2f,
|
||||
0);
|
||||
|
||||
for (int j = 0; j < 4; j++) {
|
||||
Vector3 offset = vertices[vertexIndex + j] - charCenter;
|
||||
|
||||
// Rotate around character center
|
||||
float rad = angle * Mathf.Deg2Rad;
|
||||
float rotatedX = offset.x * Mathf.Cos(rad) - offset.y * Mathf.Sin(rad);
|
||||
float rotatedY = offset.x * Mathf.Sin(rad) + offset.y * Mathf.Cos(rad);
|
||||
|
||||
vertices[vertexIndex + j] =
|
||||
charCenter + new Vector3(rotatedX, rotatedY + yOffset, offset.z);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the mesh
|
||||
for (int i = 0; i < textInfo.meshInfo.Length; i++) {
|
||||
textInfo.meshInfo[i].mesh.vertices = textInfo.meshInfo[i].vertices;
|
||||
_textComponent.UpdateGeometry(textInfo.meshInfo[i].mesh, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 299f66fc896a34cdeb95e92e927a6f10
|
||||
@@ -15,7 +15,6 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public static readonly Dictionary<Profession, string> ProfessionNames = new() {
|
||||
{ Profession.UnknownProfession, "Unknown" },
|
||||
{ Profession.NoProfession, "None" },
|
||||
{ Profession.Engineer, "Engineer" },
|
||||
{ Profession.Mage, "Mage" },
|
||||
@@ -26,7 +25,6 @@ namespace eagle {
|
||||
};
|
||||
|
||||
public static readonly Dictionary<Profession, string> ShortProfessionNames = new() {
|
||||
{ Profession.UnknownProfession, "?" },
|
||||
{ Profession.NoProfession, "None" },
|
||||
{ Profession.Engineer, "Engr" },
|
||||
{ Profession.Mage, "Mage" },
|
||||
@@ -106,9 +104,6 @@ namespace eagle {
|
||||
return $"{name} is unlikely to stay here long enough to impress with a quest.";
|
||||
|
||||
case RecruitmentStatus.Sworn: return $"{name} is dedicated to another faction.";
|
||||
|
||||
case RecruitmentStatus.WouldNeverJoin:
|
||||
return $"{name} would never join your faction.";
|
||||
}
|
||||
|
||||
return $"Unknown!";
|
||||
@@ -133,8 +128,6 @@ namespace eagle {
|
||||
case RecruitmentStatus.Traveler: return "Traveler";
|
||||
|
||||
case RecruitmentStatus.Sworn: return "Dedicated to another faction";
|
||||
|
||||
case RecruitmentStatus.WouldNeverJoin: return "Would never join";
|
||||
}
|
||||
|
||||
return $"Unknown!";
|
||||
@@ -182,7 +175,6 @@ namespace eagle {
|
||||
case SealedValueOneofCase.WinBattlesQuest: return "Win Battles";
|
||||
case SealedValueOneofCase.ApprehendOutlawQuest: return "Apprehend Outlaw";
|
||||
case SealedValueOneofCase.StartBlizzardQuest: return "Start Blizzard";
|
||||
case SealedValueOneofCase.StartDroughtQuest: return "Start Drought";
|
||||
case SealedValueOneofCase.StartEpidemicQuest: return "Start Epidemic";
|
||||
case SealedValueOneofCase.SpendOnFeastsQuest: return "Host Feasts";
|
||||
case SealedValueOneofCase.SendSuppliesQuest: return "Send Supplies";
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates a drought effect with a hot sun and heat shimmer overlay.
|
||||
/// The heat shimmer distorts the province using a shader that masks to province boundaries.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class DroughtEffect : MonoBehaviour {
|
||||
[Header("Sun Settings")]
|
||||
public Sprite sunSprite;
|
||||
public float sunSize = 40f;
|
||||
public float sunVerticalOffset = 40f;
|
||||
public Color sunColor = new Color(1f, 0.9f, 0.3f, 0.9f);
|
||||
public float sunPulseSpeed = 1.5f;
|
||||
public float sunPulseAmount = 0.15f;
|
||||
public float sunWaveAmountX = 8f; // Horizontal wave distance
|
||||
public float sunWaveAmountY = 5f; // Vertical wave distance
|
||||
public float sunWaveSpeedX = 0.4f; // Horizontal wave speed
|
||||
public float sunWaveSpeedY =
|
||||
0.6f; // Vertical wave speed (slightly different for organic feel)
|
||||
|
||||
[Header("Heat Shimmer Settings")]
|
||||
public Material shimmerMaterial;
|
||||
public float shimmerSize = 300f; // Large enough to cover province
|
||||
|
||||
[Header("Province Masking")]
|
||||
public Texture2D provinceIdMap;
|
||||
public int targetProvinceId;
|
||||
public Vector4 mapBounds; // x=minX, y=minY, z=width, w=height
|
||||
|
||||
private GameObject _sunObject;
|
||||
private Image _sunImage;
|
||||
private GameObject _shimmerObject;
|
||||
private RawImage _shimmerImage; // RawImage renders without sprite
|
||||
private Material _shimmerMatInstance;
|
||||
private Texture2D _whiteTexture;
|
||||
private float _baseSunScale;
|
||||
private Vector2 _baseSunPosition;
|
||||
|
||||
void Start() { SetupEffect(); }
|
||||
|
||||
private void SetupEffect() {
|
||||
SetupSun();
|
||||
// Note: Shimmer disabled - GrabPass doesn't work well with UI Canvas
|
||||
// TODO: Consider alternative shimmer approach (animated texture overlay)
|
||||
// SetupShimmer();
|
||||
}
|
||||
|
||||
private void SetupSun() {
|
||||
_sunObject = new GameObject("Sun");
|
||||
_sunObject.transform.SetParent(transform, false);
|
||||
|
||||
_sunImage = _sunObject.AddComponent<Image>();
|
||||
if (sunSprite != null) { _sunImage.sprite = sunSprite; }
|
||||
_sunImage.color = sunColor;
|
||||
|
||||
var rectTransform = _sunObject.GetComponent<RectTransform>();
|
||||
rectTransform.sizeDelta = new Vector2(sunSize, sunSize);
|
||||
_baseSunPosition = new Vector2(0f, sunVerticalOffset);
|
||||
rectTransform.anchoredPosition = _baseSunPosition;
|
||||
|
||||
_baseSunScale = 1f;
|
||||
}
|
||||
|
||||
private void SetupShimmer() {
|
||||
// Try to find the shader and create material
|
||||
var shader = Shader.Find("Eagle/HeatShimmer");
|
||||
if (shader == null) {
|
||||
Debug.LogWarning("DroughtEffect: Could not find Eagle/HeatShimmer shader");
|
||||
return;
|
||||
}
|
||||
|
||||
_shimmerObject = new GameObject("HeatShimmer");
|
||||
_shimmerObject.transform.SetParent(transform, false);
|
||||
// Put shimmer behind sun
|
||||
_shimmerObject.transform.SetAsFirstSibling();
|
||||
|
||||
// Create a white texture for RawImage (required for rendering)
|
||||
_whiteTexture = new Texture2D(1, 1);
|
||||
_whiteTexture.SetPixel(0, 0, Color.white);
|
||||
_whiteTexture.Apply();
|
||||
|
||||
// Use RawImage which renders without needing a sprite
|
||||
_shimmerImage = _shimmerObject.AddComponent<RawImage>();
|
||||
_shimmerImage.texture = _whiteTexture;
|
||||
_shimmerMatInstance = new Material(shader);
|
||||
_shimmerImage.material = _shimmerMatInstance;
|
||||
_shimmerImage.color = Color.white;
|
||||
|
||||
var rectTransform = _shimmerObject.GetComponent<RectTransform>();
|
||||
rectTransform.sizeDelta = new Vector2(shimmerSize, shimmerSize);
|
||||
rectTransform.anchoredPosition = Vector2.zero;
|
||||
|
||||
// Set shader properties
|
||||
if (provinceIdMap != null) {
|
||||
_shimmerMatInstance.SetTexture("_ProvinceIDMap", provinceIdMap);
|
||||
}
|
||||
_shimmerMatInstance.SetFloat("_TargetProvinceId", targetProvinceId);
|
||||
_shimmerMatInstance.SetVector("_MapBounds", mapBounds);
|
||||
}
|
||||
|
||||
public void SetProvinceId(int provinceId) {
|
||||
targetProvinceId = provinceId;
|
||||
if (_shimmerMatInstance != null) {
|
||||
_shimmerMatInstance.SetFloat("_TargetProvinceId", provinceId);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetProvinceIdMap(Texture2D idMap) {
|
||||
provinceIdMap = idMap;
|
||||
if (_shimmerMatInstance != null) {
|
||||
_shimmerMatInstance.SetTexture("_ProvinceIDMap", idMap);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMapBounds(Vector4 bounds) {
|
||||
mapBounds = bounds;
|
||||
if (_shimmerMatInstance != null) {
|
||||
_shimmerMatInstance.SetVector("_MapBounds", bounds);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetShimmerSize(float size) {
|
||||
shimmerSize = size;
|
||||
if (_shimmerObject != null) {
|
||||
var rt = _shimmerObject.GetComponent<RectTransform>();
|
||||
rt.sizeDelta = new Vector2(size, size);
|
||||
}
|
||||
}
|
||||
|
||||
void Update() {
|
||||
if (_sunObject != null) {
|
||||
// Pulse the sun size
|
||||
float pulse = 1f + Mathf.Sin(Time.time * sunPulseSpeed) * sunPulseAmount;
|
||||
_sunObject.transform.localScale = Vector3.one * (_baseSunScale * pulse);
|
||||
|
||||
// Wave the sun position slowly
|
||||
var rectTransform = _sunObject.GetComponent<RectTransform>();
|
||||
float waveX = Mathf.Sin(Time.time * sunWaveSpeedX) * sunWaveAmountX;
|
||||
float waveY = Mathf.Sin(Time.time * sunWaveSpeedY) * sunWaveAmountY;
|
||||
rectTransform.anchoredPosition = _baseSunPosition + new Vector2(waveX, waveY);
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_shimmerMatInstance != null) { Destroy(_shimmerMatInstance); }
|
||||
if (_whiteTexture != null) { Destroy(_whiteTexture); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f64938ef171de43dba1b0a865fc86f60
|
||||
@@ -21,15 +21,12 @@ public class EagleCommonTextures : MonoBehaviour {
|
||||
public Texture2D Profession(Profession profession) {
|
||||
if (profession == Net.Eagle0.Eagle.Common.Profession.NoProfession)
|
||||
return professionTextures.Last();
|
||||
if (profession == Net.Eagle0.Eagle.Common.Profession.UnknownProfession) return null;
|
||||
return professionTextures[(int)profession];
|
||||
}
|
||||
|
||||
public Texture2D Profession(HeroView.Types.ProfessionSpecificInfo.Types.Profession profession) {
|
||||
if (profession == HeroView.Types.ProfessionSpecificInfo.Types.Profession.NoProfession)
|
||||
return professionTextures.Last();
|
||||
if (profession == HeroView.Types.ProfessionSpecificInfo.Types.Profession.Unknown)
|
||||
return null;
|
||||
return professionTextures[(int)profession];
|
||||
}
|
||||
|
||||
|
||||
+16
-12
@@ -33,6 +33,8 @@ namespace eagle {
|
||||
|
||||
[Header("Status Labels")]
|
||||
public TextMeshProUGUI roundStatusLabel;
|
||||
public TextMeshProUGUI connectionStatusLabel;
|
||||
private bool _connectionStatusUIInitialized = false;
|
||||
|
||||
[Header("Main Panels")]
|
||||
public ProvinceInfoPanelController provinceInfoPanelController;
|
||||
@@ -143,6 +145,16 @@ namespace eagle {
|
||||
|
||||
if (newWidth == _lastWidth && newHeight == _lastHeight) { return; }
|
||||
|
||||
// Initialize ConnectionStatusUI component once when connection is available
|
||||
if (!_connectionStatusUIInitialized &&
|
||||
errorHandler.PersistentClientConnection != null) {
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) {
|
||||
statusUI.SetConnection(errorHandler.PersistentClientConnection);
|
||||
_connectionStatusUIInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
_lastWidth = newWidth;
|
||||
_lastHeight = newHeight;
|
||||
|
||||
@@ -213,21 +225,12 @@ namespace eagle {
|
||||
}
|
||||
SwapModel();
|
||||
Model = null;
|
||||
|
||||
// Clear stale province state from the previous game so it doesn't
|
||||
// get used with the next game's hero/battalion maps.
|
||||
provinceInfoPanelController.Province = null;
|
||||
freeHeroesTableController.CurrentProvince = null;
|
||||
movingArmiesTableControllerNarrow.CurrentProvince = null;
|
||||
movingArmiesTableControllerWide.CurrentProvince = null;
|
||||
factionsTableControllerNarrow.CurrentProvince = null;
|
||||
factionsTableControllerWide.CurrentProvince = null;
|
||||
heroesAndBattalionsPanelController.CurrentProvince = null;
|
||||
chronicleCanvasController.Entries = new List<ChronicleEntry>();
|
||||
SetMusic();
|
||||
|
||||
// Clear game state provider when leaving game
|
||||
PersistentUIManager.Instance?.connectionStatusUI?.SetGameStateProvider(null);
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) { statusUI.SetGameStateProvider(null); }
|
||||
}
|
||||
|
||||
void MapControllerChangedTarget(List<ProvinceId> newTarget) {
|
||||
@@ -332,7 +335,7 @@ namespace eagle {
|
||||
ModelUpdater.ErrorHandler = errorHandler;
|
||||
|
||||
// Set up game state provider and environment for connection status UI
|
||||
var statusUI = PersistentUIManager.Instance?.connectionStatusUI;
|
||||
var statusUI = connectionStatusLabel.GetComponent<ConnectionStatusUI>();
|
||||
if (statusUI != null) {
|
||||
statusUI.SetGameStateProvider(ModelUpdater);
|
||||
statusUI.SetEnvironment(environmentName);
|
||||
@@ -792,6 +795,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
shardokController.SetUpGame(shardokModel);
|
||||
shardokController.SetConnection(errorHandler.PersistentClientConnection);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ namespace eagle {
|
||||
if (battleView == null) {
|
||||
// Battle was removed (e.g., it ended) before we could create the model.
|
||||
// This is expected when Eagle's RemovedBattleIds update arrives before a
|
||||
// pending Shardok update - the UI already shows "Exit Battle" via
|
||||
// pending Shardok update - the UI already shows "Back to Eagle" via
|
||||
// MarkBattleEnded(), so we just skip this stale update.
|
||||
return null;
|
||||
}
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: 618063871244848148}
|
||||
m_Layer: 0
|
||||
m_Name: BeastsEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &618063871244848148
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a5ee82ebc029341c4858ef5b7e7cf201, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.BeastsEffect
|
||||
birdTexture: {fileID: 2800000, guid: 7b6ab1e235eea4e4e9319c88bb1cdc70, type: 3}
|
||||
birdMaterial: {fileID: 2100000, guid: 67dcb765c9c324ff9a103624340bc9b5, type: 2}
|
||||
birdCount: 5
|
||||
birdLifetime: 8
|
||||
birdSize: 12
|
||||
circleRadius: 35
|
||||
circleSpeed: 1
|
||||
birdColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
verticalOscillation: 10
|
||||
radiusVariation: 10
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c093deb39df94fbca5faa9f015a2331
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: 3572684436619911535}
|
||||
m_Layer: 0
|
||||
m_Name: BlizzardEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &3572684436619911535
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c15980e88e57845ce920e43f29d35fcc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.BlizzardEffect
|
||||
snowflakeTexture: {fileID: 2800000, guid: 9a5b4e8c3d2f6b0a7c1e9d5f8a3b2c4d, type: 3}
|
||||
particleMaterial: {fileID: 2100000, guid: b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6, type: 2}
|
||||
snowEmitRate: 15
|
||||
snowFallSpeed: 40
|
||||
snowLifetime: 3
|
||||
snowSize: 8
|
||||
snowDrift: 10
|
||||
snowColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
windStrength: 5
|
||||
windVariation: 3
|
||||
provinceIdMap: {fileID: 0}
|
||||
targetProvinceId: 0
|
||||
mapBounds: {x: 0, y: 0, z: 0, w: 0}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cf4724a235af4fb8abab6456789936a
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: -4077358403987153371}
|
||||
m_Layer: 0
|
||||
m_Name: DroughtEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &-4077358403987153371
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f64938ef171de43dba1b0a865fc86f60, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.DroughtEffect
|
||||
sunSprite: {fileID: 21300000, guid: ecdaab62f65f19745aee51dc3e5be2fc, type: 3}
|
||||
sunSize: 20
|
||||
sunVerticalOffset: 30
|
||||
sunColor: {r: 1, g: 0.9019608, b: 0.29803923, a: 0.68235296}
|
||||
sunPulseSpeed: 1.5
|
||||
sunPulseAmount: 0.1
|
||||
shimmerMaterial: {fileID: 10303, guid: 0000000000000000f000000000000000, type: 0}
|
||||
shimmerSize: 15
|
||||
provinceIdMap: {fileID: 0}
|
||||
targetProvinceId: 0
|
||||
mapBounds: {x: 0, y: 0, z: 0, w: 0}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: 8607881802583290962}
|
||||
m_Layer: 0
|
||||
m_Name: EpidemicEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &8607881802583290962
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 727093fab7ebc43dc94ee59675164706, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.EpidemicEffect
|
||||
skullMaterial: {fileID: 2100000, guid: c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7, type: 2}
|
||||
skullEmitRate: 1.5
|
||||
skullRiseSpeed: 25
|
||||
skullLifetime: 3
|
||||
skullSize: 25
|
||||
skullSway: 8
|
||||
skullColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
hazeMaterial: {fileID: 2100000, guid: c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7, type: 2}
|
||||
hazeEmitRate: 10
|
||||
hazeSpeed: 8
|
||||
hazeLifetime: 4
|
||||
hazeSize: 10
|
||||
hazeColor: {r: 0.4, g: 0.54901963, b: 0.34901962, a: 0.50980395}
|
||||
@@ -1,61 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &529533522556784756
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7895772391506986011}
|
||||
- component: {fileID: 4807333260855875379}
|
||||
m_Layer: 0
|
||||
m_Name: Fireworks
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7895772391506986011
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 529533522556784756}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &4807333260855875379
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 529533522556784756}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 01a76cb7235d04623be14e4b01f18754, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.FireworksEffect
|
||||
burstInterval: 1.5
|
||||
particlesPerBurst: 30
|
||||
particleLifetime: 1.5
|
||||
startSpeed: 200
|
||||
gravity: 100
|
||||
fireworkColors:
|
||||
- {r: 1, g: 0.3, b: 0.3, a: 1}
|
||||
- {r: 1, g: 0.8, b: 0.2, a: 1}
|
||||
- {r: 0.3, g: 0.8, b: 1, a: 1}
|
||||
- {r: 0.3, g: 1, b: 0.3, a: 1}
|
||||
- {r: 1, g: 0.5, b: 1, a: 1}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27861cc746ec947e88dac44d9a058e10
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &7893566172152383399
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 9219784709551883713}
|
||||
- component: {fileID: 8954796869750569591}
|
||||
m_Layer: 0
|
||||
m_Name: FloodEffect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &9219784709551883713
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &8954796869750569591
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7893566172152383399}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 336c92d7cae77487fa6915c9839d38f6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::eagle.FloodEffect
|
||||
rainTexture: {fileID: 2800000, guid: 8f4a3e7b2c1d5a9e6b0f8c4d7a2e1b3c, type: 3}
|
||||
particleMaterial: {fileID: 2100000, guid: b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6, type: 2}
|
||||
rainEmitRate: 40
|
||||
rainFallSpeed: 80
|
||||
rainLifetime: 1.5
|
||||
rainSize: 4
|
||||
rainStretch: 3
|
||||
rainColor: {r: 0.7, g: 0.8, b: 0.9, a: 0.6}
|
||||
splashEmitRate: 10
|
||||
splashLifetime: 0.5
|
||||
splashSize: 8
|
||||
splashColor: {r: 0.7, g: 0.85, b: 1, a: 0.4}
|
||||
provinceIdMap: {fileID: 0}
|
||||
targetProvinceId: 0
|
||||
mapBounds: {x: 0, y: 0, z: 0, w: 0}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 384a5a10723d4475e8dba09c8ef66eca
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: ParticleAlphaBlendMaterial
|
||||
m_Shader: {fileID: 4800000, guid: a60360570f7264533a1146b26d028b00, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords:
|
||||
- _ALPHABLEND_ON
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 5fb52be5b250f5f42ace466a01c19bed, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ProvinceIDMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _DstBlend: 10
|
||||
- _InvFade: 1
|
||||
- _SrcBlend: 5
|
||||
- _TargetProvinceId: 0
|
||||
m_Colors:
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _TintColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: ProvinceParticleMaterial
|
||||
m_Shader: {fileID: 4800000, guid: a60360570f7264533a1146b26d028b00, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: 3000
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ProvinceIDMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _TargetProvinceId: 0
|
||||
m_Colors:
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,206 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates an epidemic particle effect with skulls rising and green haze.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class EpidemicEffect : MonoBehaviour {
|
||||
[Header("Skull Settings")]
|
||||
public Material skullMaterial;
|
||||
public float skullEmitRate = 1.5f;
|
||||
public float skullRiseSpeed = 25f;
|
||||
public float skullLifetime = 3f;
|
||||
public float skullSize = 12f;
|
||||
public float skullSway = 8f;
|
||||
public Color skullColor = Color.white;
|
||||
|
||||
[Header("Haze Settings")]
|
||||
public Material hazeMaterial;
|
||||
public float hazeEmitRate = 5f;
|
||||
public float hazeSpeed = 8f;
|
||||
public float hazeLifetime = 4f;
|
||||
public float hazeSize = 40f;
|
||||
public Color hazeColor = new Color(0.4f, 0.55f, 0.35f, 0.25f);
|
||||
|
||||
private ParticleSystem _skullSystem;
|
||||
private ParticleSystem _hazeSystem;
|
||||
private Material _skullMatInstance;
|
||||
private Material _hazeMatInstance;
|
||||
|
||||
void Awake() { SetupParticleSystems(); }
|
||||
|
||||
private void SetupParticleSystems() {
|
||||
// Create haze first (renders behind skulls)
|
||||
var hazeObj = new GameObject("HazeEffect");
|
||||
hazeObj.transform.SetParent(transform, false);
|
||||
_hazeSystem = hazeObj.AddComponent<ParticleSystem>();
|
||||
SetupHazeSystem();
|
||||
|
||||
// Create skull particle system
|
||||
var skullObj = new GameObject("SkullEffect");
|
||||
skullObj.transform.SetParent(transform, false);
|
||||
_skullSystem = skullObj.AddComponent<ParticleSystem>();
|
||||
SetupSkullSystem();
|
||||
}
|
||||
|
||||
private void SetupSkullSystem() {
|
||||
var main = _skullSystem.main;
|
||||
main.playOnAwake = true;
|
||||
main.loop = true;
|
||||
main.startLifetime = skullLifetime;
|
||||
main.startSpeed = 0f;
|
||||
main.startSize = skullSize;
|
||||
main.startColor = skullColor;
|
||||
main.startRotation = new ParticleSystem.MinMaxCurve(-0.2f, 0.2f); // Slight random tilt
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 20;
|
||||
|
||||
var emission = _skullSystem.emission;
|
||||
emission.enabled = true;
|
||||
emission.rateOverTime = skullEmitRate;
|
||||
|
||||
// Emit from a small area at the base with random direction for sway
|
||||
var shape = _skullSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Circle;
|
||||
shape.radius = 15f;
|
||||
shape.rotation = new Vector3(-90f, 0f, 0f);
|
||||
shape.randomDirectionAmount = 0.3f; // Add horizontal sway variation
|
||||
|
||||
// Rise upward with horizontal drift for swaying motion
|
||||
var velocityOverLifetime = _skullSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.space = ParticleSystemSimulationSpace.Local;
|
||||
// Use constant mode for all axes to avoid mode mismatch
|
||||
velocityOverLifetime.x = skullSway; // More horizontal drift
|
||||
velocityOverLifetime.y = skullRiseSpeed;
|
||||
velocityOverLifetime.z = 0f;
|
||||
|
||||
// Grow as they rise (start at 1/4 size, end at full size)
|
||||
var sizeOverLifetime = _skullSystem.sizeOverLifetime;
|
||||
sizeOverLifetime.enabled = true;
|
||||
sizeOverLifetime.size = new ParticleSystem.MinMaxCurve(
|
||||
1f,
|
||||
new AnimationCurve(new Keyframe(0f, 0.25f), new Keyframe(1f, 1f)));
|
||||
|
||||
// Fade out as they rise
|
||||
var colorOverLifetime = _skullSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
var gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(Color.white, 0f),
|
||||
new GradientColorKey(Color.white, 0.7f),
|
||||
new GradientColorKey(Color.white, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(0f, 0f),
|
||||
new GradientAlphaKey(1f, 0.1f),
|
||||
new GradientAlphaKey(1f, 0.6f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
});
|
||||
colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient);
|
||||
|
||||
// Disable rotation over lifetime (causes spinning)
|
||||
var rotationOverLifetime = _skullSystem.rotationOverLifetime;
|
||||
rotationOverLifetime.enabled = false;
|
||||
|
||||
// Use noise module for gentle rocking/swaying rotation
|
||||
var noise = _skullSystem.noise;
|
||||
noise.enabled = true;
|
||||
noise.separateAxes = true;
|
||||
noise.strengthX = 0f; // No position noise
|
||||
noise.strengthY = 0f;
|
||||
noise.strengthZ = 0f;
|
||||
noise.rotationAmount = 0.2f; // ~11 degrees of rotation sway
|
||||
noise.frequency = 0.5f; // Slow oscillation
|
||||
noise.scrollSpeed = 0f;
|
||||
noise.damping = false;
|
||||
noise.octaveCount = 1;
|
||||
|
||||
var renderer = _skullSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
if (skullMaterial == null) {
|
||||
Debug.LogError("[EpidemicEffect] skullMaterial is not assigned!");
|
||||
return;
|
||||
}
|
||||
_skullMatInstance = new Material(skullMaterial);
|
||||
renderer.material = _skullMatInstance;
|
||||
renderer.sortingOrder = 101;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupHazeSystem() {
|
||||
var main = _hazeSystem.main;
|
||||
main.playOnAwake = true;
|
||||
main.loop = true;
|
||||
main.startLifetime = hazeLifetime;
|
||||
main.startSpeed = hazeSpeed;
|
||||
main.startSize = new ParticleSystem.MinMaxCurve(hazeSize * 0.5f, hazeSize);
|
||||
main.startColor = hazeColor;
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 30;
|
||||
|
||||
var emission = _hazeSystem.emission;
|
||||
emission.enabled = true;
|
||||
emission.rateOverTime = hazeEmitRate;
|
||||
|
||||
// Emit from ground area, spread outward and up
|
||||
var shape = _hazeSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Cone;
|
||||
shape.angle = 40f;
|
||||
shape.radius = 10f;
|
||||
shape.rotation = new Vector3(-90f, 0f, 0f);
|
||||
|
||||
// Slow down over time
|
||||
var velocityOverLifetime = _hazeSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.speedModifier = new ParticleSystem.MinMaxCurve(
|
||||
1f,
|
||||
new AnimationCurve(new Keyframe(0f, 1f), new Keyframe(1f, 0.2f)));
|
||||
|
||||
// Grow and fade
|
||||
var sizeOverLifetime = _hazeSystem.sizeOverLifetime;
|
||||
sizeOverLifetime.enabled = true;
|
||||
sizeOverLifetime.size = new ParticleSystem.MinMaxCurve(
|
||||
1f,
|
||||
new AnimationCurve(new Keyframe(0f, 0.5f), new Keyframe(1f, 2f)));
|
||||
|
||||
var colorOverLifetime = _hazeSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
var gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(Color.white, 0f),
|
||||
new GradientColorKey(Color.white, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(0f, 0f),
|
||||
new GradientAlphaKey(0.6f, 0.2f),
|
||||
new GradientAlphaKey(0.6f, 0.5f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
});
|
||||
colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient);
|
||||
|
||||
var renderer = _hazeSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
if (hazeMaterial == null) {
|
||||
Debug.LogError("[EpidemicEffect] hazeMaterial is not assigned!");
|
||||
return;
|
||||
}
|
||||
_hazeMatInstance = new Material(hazeMaterial);
|
||||
renderer.material = _hazeMatInstance;
|
||||
renderer.sortingOrder = 100;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_skullMatInstance != null) { Destroy(_skullMatInstance); }
|
||||
if (_hazeMatInstance != null) { Destroy(_hazeMatInstance); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 727093fab7ebc43dc94ee59675164706
|
||||
@@ -1,217 +0,0 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates a fireworks particle effect for festival events.
|
||||
/// Rockets fly upward and burst into colorful explosions.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class FireworksEffect : MonoBehaviour {
|
||||
[Header("Rocket Settings")]
|
||||
public float launchInterval = 2f;
|
||||
public float rocketSpeed = 80f;
|
||||
public float rocketLifetime = 0.8f;
|
||||
public float rocketSize = 6f;
|
||||
|
||||
[Header("Burst Settings")]
|
||||
public int burstParticles = 40;
|
||||
public float burstSpeed = 50f;
|
||||
public float burstLifetime = 1.2f;
|
||||
public float burstGravity = 30f;
|
||||
|
||||
[Header("Colors")]
|
||||
public Color[] fireworkColors = {
|
||||
new Color(1f, 0.3f, 0.3f), // Red
|
||||
new Color(1f, 0.8f, 0.2f), // Gold
|
||||
new Color(0.3f, 0.8f, 1f), // Blue
|
||||
new Color(0.3f, 1f, 0.3f), // Green
|
||||
new Color(1f, 0.5f, 1f), // Pink
|
||||
new Color(1f, 1f, 0.4f) // Yellow
|
||||
};
|
||||
|
||||
private ParticleSystem _rocketSystem;
|
||||
private ParticleSystem _burstSystem;
|
||||
private float _timer;
|
||||
private int _colorIndex;
|
||||
|
||||
void Awake() {
|
||||
_colorIndex = Random.Range(0, fireworkColors.Length);
|
||||
SetupParticleSystems();
|
||||
}
|
||||
|
||||
private void SetupParticleSystems() {
|
||||
// Create rocket particle system on child GameObject (not on RectTransform)
|
||||
var rocketObj = new GameObject("RocketEffect");
|
||||
rocketObj.transform.SetParent(transform, false);
|
||||
_rocketSystem = rocketObj.AddComponent<ParticleSystem>();
|
||||
SetupRocketSystem();
|
||||
|
||||
// Create burst particle system (separate, not sub-emitter)
|
||||
var burstObj = new GameObject("BurstEffect");
|
||||
burstObj.transform.SetParent(transform, false);
|
||||
_burstSystem = burstObj.AddComponent<ParticleSystem>();
|
||||
SetupBurstSystem();
|
||||
|
||||
StartCoroutine(LaunchSequence());
|
||||
}
|
||||
|
||||
private void SetupRocketSystem() {
|
||||
var main = _rocketSystem.main;
|
||||
main.playOnAwake = false;
|
||||
main.loop = false;
|
||||
main.startLifetime = rocketLifetime;
|
||||
main.startSpeed = 0f; // We set velocity explicitly in EmitParams
|
||||
main.startSize = rocketSize;
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 10;
|
||||
main.startColor = new Color(1f, 0.9f, 0.6f); // Bright orange/yellow
|
||||
|
||||
var emission = _rocketSystem.emission;
|
||||
emission.enabled = false;
|
||||
|
||||
// Disable shape - we set velocity explicitly in EmitParams to avoid Z movement
|
||||
var shape = _rocketSystem.shape;
|
||||
shape.enabled = false;
|
||||
|
||||
// Add slight arc to rocket path (gravity slowing upward movement)
|
||||
var velocityOverLifetime = _rocketSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.space = ParticleSystemSimulationSpace.Local;
|
||||
velocityOverLifetime.y = new ParticleSystem.MinMaxCurve(-20f); // Slight downward pull
|
||||
|
||||
// Rocket trail
|
||||
var trails = _rocketSystem.trails;
|
||||
trails.enabled = true;
|
||||
trails.ratio = 1f;
|
||||
trails.lifetime = new ParticleSystem.MinMaxCurve(0.4f);
|
||||
trails.minVertexDistance = 2f;
|
||||
trails.worldSpace = false;
|
||||
trails.dieWithParticles = true;
|
||||
trails.widthOverTrail = new ParticleSystem.MinMaxCurve(
|
||||
1f,
|
||||
new AnimationCurve(new Keyframe(0f, 1f), new Keyframe(1f, 0.3f)));
|
||||
trails.colorOverLifetime = new ParticleSystem.MinMaxGradient(
|
||||
new Color(1f, 0.8f, 0.3f),
|
||||
new Color(1f, 0.5f, 0.1f));
|
||||
|
||||
var renderer = _rocketSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
// Use a shader that respects vertex colors
|
||||
var particleShader = Shader.Find("Particles/Standard Unlit");
|
||||
if (particleShader != null) {
|
||||
var mat = new Material(particleShader);
|
||||
mat.SetFloat("_ColorMode", 1); // Multiply mode
|
||||
renderer.material = mat;
|
||||
renderer.trailMaterial = mat;
|
||||
}
|
||||
renderer.sortingOrder = 100;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupBurstSystem() {
|
||||
var main = _burstSystem.main;
|
||||
main.playOnAwake = false;
|
||||
main.loop = false;
|
||||
main.startLifetime = burstLifetime;
|
||||
main.startSpeed = burstSpeed;
|
||||
main.startSize = new ParticleSystem.MinMaxCurve(4f, 7f);
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 200;
|
||||
|
||||
var emission = _burstSystem.emission;
|
||||
emission.enabled = false;
|
||||
|
||||
var shape = _burstSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Sphere;
|
||||
shape.radius = 1f;
|
||||
|
||||
// Gravity pulls burst particles down
|
||||
var velocityOverLifetime = _burstSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.y = new ParticleSystem.MinMaxCurve(-burstGravity);
|
||||
|
||||
// Disable colorOverLifetime to preserve startColor from EmitParams
|
||||
var colorOverLifetime = _burstSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = false;
|
||||
|
||||
// Shrink over lifetime
|
||||
var sizeOverLifetime = _burstSystem.sizeOverLifetime;
|
||||
sizeOverLifetime.enabled = true;
|
||||
sizeOverLifetime.size = new ParticleSystem.MinMaxCurve(
|
||||
1f,
|
||||
new AnimationCurve(new Keyframe(0f, 1f), new Keyframe(1f, 0.2f)));
|
||||
|
||||
// Sparkle trails on burst particles
|
||||
var trails = _burstSystem.trails;
|
||||
trails.enabled = true;
|
||||
trails.ratio = 0.4f;
|
||||
trails.lifetime = new ParticleSystem.MinMaxCurve(0.2f);
|
||||
trails.minVertexDistance = 1f;
|
||||
trails.worldSpace = false;
|
||||
trails.dieWithParticles = true;
|
||||
trails.widthOverTrail = new ParticleSystem.MinMaxCurve(
|
||||
1f,
|
||||
new AnimationCurve(new Keyframe(0f, 0.6f), new Keyframe(1f, 0f)));
|
||||
trails.inheritParticleColor = true;
|
||||
|
||||
var burstRenderer = _burstSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (burstRenderer != null) {
|
||||
// Use a shader that respects vertex colors
|
||||
var particleShader = Shader.Find("Particles/Standard Unlit");
|
||||
if (particleShader != null) {
|
||||
var mat = new Material(particleShader);
|
||||
mat.SetFloat("_ColorMode", 1); // Multiply mode
|
||||
burstRenderer.material = mat;
|
||||
burstRenderer.trailMaterial = mat;
|
||||
}
|
||||
burstRenderer.sortingOrder = 101;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator LaunchSequence() {
|
||||
while (true) {
|
||||
LaunchRocket();
|
||||
yield return new WaitForSeconds(launchInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchRocket() {
|
||||
if (_rocketSystem == null || _burstSystem == null) { return; }
|
||||
|
||||
Color burstColor = fireworkColors[_colorIndex % fireworkColors.Length];
|
||||
_colorIndex++;
|
||||
|
||||
// Launch rocket with explicit velocity and color
|
||||
// X: random variation for slight angle, Y: upward, Z: must be negative to render
|
||||
float xVariation = Random.Range(-15f, 15f);
|
||||
var rocketParams = new ParticleSystem.EmitParams();
|
||||
rocketParams.position = Vector3.zero;
|
||||
rocketParams.velocity = new Vector3(xVariation, rocketSpeed, -1f);
|
||||
rocketParams.startColor = new Color(1f, 0.9f, 0.5f); // Bright yellow-orange
|
||||
_rocketSystem.Emit(rocketParams, 1);
|
||||
|
||||
// Schedule burst at rocket death position
|
||||
StartCoroutine(EmitBurstAfterDelay(burstColor));
|
||||
}
|
||||
|
||||
private IEnumerator EmitBurstAfterDelay(Color color) {
|
||||
// Wait for rocket to reach peak
|
||||
yield return new WaitForSeconds(rocketLifetime);
|
||||
|
||||
// Calculate burst position (where rocket ended)
|
||||
Vector3 burstPosition = new Vector3(0f, rocketSpeed * rocketLifetime, 0f);
|
||||
|
||||
// Move burst system to rocket's end position
|
||||
_burstSystem.transform.localPosition = burstPosition;
|
||||
|
||||
// Emit particles with explicit color using EmitParams
|
||||
var emitParams = new ParticleSystem.EmitParams();
|
||||
emitParams.startColor = color;
|
||||
for (int i = 0; i < burstParticles; i++) { _burstSystem.Emit(emitParams, 1); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01a76cb7235d04623be14e4b01f18754
|
||||
@@ -1,316 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Creates a flood/rain particle effect with falling raindrops and splashes.
|
||||
/// Uses province masking shader to clip particles to province boundaries.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class FloodEffect : MonoBehaviour {
|
||||
[Header("Rain Settings")]
|
||||
public Texture2D rainTexture;
|
||||
public Material particleMaterial; // Assign in prefab to ensure shader is in build
|
||||
public float rainEmitRate = 80f; // More rain
|
||||
public float rainFallSpeed = 100f;
|
||||
public float rainLifetime = 2f; // Longer lifetime
|
||||
public float rainSize = 5f;
|
||||
public float rainStretch = 4f;
|
||||
public Color rainColor = new Color(0.7f, 0.8f, 0.9f, 0.6f);
|
||||
|
||||
[Header("Splash Settings")]
|
||||
public float splashEmitRate = 25f; // More splashes
|
||||
public float splashLifetime = 0.6f;
|
||||
public float splashSize = 10f;
|
||||
public Color splashColor = new Color(0.7f, 0.85f, 1f, 0.4f);
|
||||
|
||||
[Header("Province Masking")]
|
||||
public Texture2D provinceIdMap;
|
||||
public int targetProvinceId;
|
||||
|
||||
private ParticleSystem _rainSystem;
|
||||
private ParticleSystem _splashSystem;
|
||||
private Material _rainMaterial;
|
||||
private Material _splashMaterial;
|
||||
private RectTransform _mapContainer;
|
||||
private Canvas _canvas;
|
||||
private Vector3 _baseRainShapeScale;
|
||||
private Vector3 _baseRainShapePosition;
|
||||
private float _baseSplashRadius;
|
||||
private float _baseRainEmitRate;
|
||||
private float _baseSplashEmitRate;
|
||||
private float _lastZoom = 1f;
|
||||
|
||||
void Awake() { SetupParticleSystems(); }
|
||||
|
||||
private void SetupParticleSystems() {
|
||||
// Create splash first (renders behind rain)
|
||||
var splashObj = new GameObject("SplashEffect");
|
||||
splashObj.transform.SetParent(transform, false);
|
||||
_splashSystem = splashObj.AddComponent<ParticleSystem>();
|
||||
SetupSplashSystem();
|
||||
|
||||
// Create rain particle system
|
||||
var rainObj = new GameObject("RainEffect");
|
||||
rainObj.transform.SetParent(transform, false);
|
||||
_rainSystem = rainObj.AddComponent<ParticleSystem>();
|
||||
SetupRainSystem();
|
||||
}
|
||||
|
||||
private void SetupRainSystem() {
|
||||
var main = _rainSystem.main;
|
||||
main.playOnAwake = true;
|
||||
main.loop = true;
|
||||
main.startLifetime = rainLifetime;
|
||||
main.startSpeed = 0f;
|
||||
main.startSize = rainSize;
|
||||
main.startColor = rainColor;
|
||||
main.startRotation = 0f;
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 400; // More particles for coverage
|
||||
|
||||
var emission = _rainSystem.emission;
|
||||
emission.enabled = true;
|
||||
emission.rateOverTime = rainEmitRate;
|
||||
_baseRainEmitRate = rainEmitRate;
|
||||
|
||||
// Emit across full province height - particles spawn at varying Y positions
|
||||
var shape = _rainSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Box;
|
||||
shape.scale = new Vector3(150f, 150f, 1f); // Wide and very tall emission area
|
||||
shape.position = new Vector3(0f, 40f, 0f); // Centered above effect origin
|
||||
_baseRainShapeScale = shape.scale;
|
||||
_baseRainShapePosition = shape.position;
|
||||
|
||||
// Fall downward with slight angle
|
||||
var velocityOverLifetime = _rainSystem.velocityOverLifetime;
|
||||
velocityOverLifetime.enabled = true;
|
||||
velocityOverLifetime.space = ParticleSystemSimulationSpace.Local;
|
||||
velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(-5f, -5f);
|
||||
velocityOverLifetime.y = new ParticleSystem.MinMaxCurve(-rainFallSpeed, -rainFallSpeed);
|
||||
velocityOverLifetime.z = new ParticleSystem.MinMaxCurve(0f, 0f);
|
||||
|
||||
// Fade in at top, stay visible
|
||||
var colorOverLifetime = _rainSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
var gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(Color.white, 0f),
|
||||
new GradientColorKey(Color.white, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(0f, 0f),
|
||||
new GradientAlphaKey(1f, 0.1f),
|
||||
new GradientAlphaKey(1f, 0.9f),
|
||||
new GradientAlphaKey(0.5f, 1f)
|
||||
});
|
||||
colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient);
|
||||
|
||||
var renderer = _rainSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
renderer.renderMode = ParticleSystemRenderMode.Stretch;
|
||||
renderer.lengthScale = rainStretch;
|
||||
renderer.velocityScale = 0f;
|
||||
|
||||
// Use provided material if available (ensures shader is in build)
|
||||
if (particleMaterial != null) {
|
||||
_rainMaterial = new Material(particleMaterial);
|
||||
_rainMaterial.renderQueue = 3000;
|
||||
if (rainTexture != null) { _rainMaterial.mainTexture = rainTexture; }
|
||||
renderer.material = _rainMaterial;
|
||||
} else {
|
||||
// Fallback for editor testing
|
||||
string[] shaderNames = {
|
||||
"Eagle/ProvinceParticle",
|
||||
"Universal Render Pipeline/Particles/Unlit",
|
||||
"Particles/Standard Unlit",
|
||||
"Legacy Shaders/Particles/Alpha Blended"
|
||||
};
|
||||
|
||||
Shader shader = null;
|
||||
foreach (var name in shaderNames) {
|
||||
shader = Shader.Find(name);
|
||||
if (shader != null) break;
|
||||
}
|
||||
|
||||
if (shader != null) {
|
||||
_rainMaterial = new Material(shader);
|
||||
_rainMaterial.renderQueue = 3000;
|
||||
if (rainTexture != null) { _rainMaterial.mainTexture = rainTexture; }
|
||||
renderer.material = _rainMaterial;
|
||||
} else {
|
||||
Debug.LogWarning("FloodEffect: No particle shader found");
|
||||
}
|
||||
}
|
||||
renderer.sortingOrder = 101;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupSplashSystem() {
|
||||
var main = _splashSystem.main;
|
||||
main.playOnAwake = true;
|
||||
main.loop = true;
|
||||
main.startLifetime = splashLifetime;
|
||||
main.startSpeed = new ParticleSystem.MinMaxCurve(5f, 15f);
|
||||
main.startSize = new ParticleSystem.MinMaxCurve(splashSize * 0.5f, splashSize);
|
||||
main.startColor = splashColor;
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.Local;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
main.maxParticles = 150; // More splashes
|
||||
main.gravityModifier = 2f;
|
||||
|
||||
var emission = _splashSystem.emission;
|
||||
emission.enabled = true;
|
||||
emission.rateOverTime = splashEmitRate;
|
||||
_baseSplashEmitRate = splashEmitRate;
|
||||
|
||||
// Emit from ground area - large radius since shader masks
|
||||
var shape = _splashSystem.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Circle;
|
||||
shape.radius = 60f; // Larger splash area
|
||||
_baseSplashRadius = shape.radius;
|
||||
shape.rotation = new Vector3(-90f, 0f, 0f);
|
||||
shape.arc = 180f;
|
||||
shape.arcMode = ParticleSystemShapeMultiModeValue.Random;
|
||||
|
||||
// Quick fade out
|
||||
var colorOverLifetime = _splashSystem.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
var gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(Color.white, 0f),
|
||||
new GradientColorKey(Color.white, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(0.8f, 0f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
});
|
||||
colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient);
|
||||
|
||||
// Shrink as they fade
|
||||
var sizeOverLifetime = _splashSystem.sizeOverLifetime;
|
||||
sizeOverLifetime.enabled = true;
|
||||
sizeOverLifetime.size = new ParticleSystem.MinMaxCurve(
|
||||
1f,
|
||||
new AnimationCurve(new Keyframe(0f, 1f), new Keyframe(1f, 0.2f)));
|
||||
|
||||
var renderer = _splashSystem.GetComponent<ParticleSystemRenderer>();
|
||||
if (renderer != null) {
|
||||
// Use provided material if available (ensures shader is in build)
|
||||
if (particleMaterial != null) {
|
||||
_splashMaterial = new Material(particleMaterial);
|
||||
_splashMaterial.renderQueue = 3000;
|
||||
renderer.material = _splashMaterial;
|
||||
} else {
|
||||
// Fallback for editor testing
|
||||
string[] shaderNames = {
|
||||
"Eagle/ProvinceParticle",
|
||||
"Universal Render Pipeline/Particles/Unlit",
|
||||
"Particles/Standard Unlit",
|
||||
"Legacy Shaders/Particles/Alpha Blended"
|
||||
};
|
||||
|
||||
Shader shader = null;
|
||||
foreach (var name in shaderNames) {
|
||||
shader = Shader.Find(name);
|
||||
if (shader != null) break;
|
||||
}
|
||||
|
||||
if (shader != null) {
|
||||
_splashMaterial = new Material(shader);
|
||||
_splashMaterial.renderQueue = 3000;
|
||||
renderer.material = _splashMaterial;
|
||||
}
|
||||
}
|
||||
renderer.sortingOrder = 100;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetProvinceId(int provinceId) {
|
||||
targetProvinceId = provinceId;
|
||||
if (_rainMaterial != null) { _rainMaterial.SetFloat("_TargetProvinceId", provinceId); }
|
||||
if (_splashMaterial != null) {
|
||||
_splashMaterial.SetFloat("_TargetProvinceId", provinceId);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetProvinceIdMap(Texture2D idMap) {
|
||||
provinceIdMap = idMap;
|
||||
if (_rainMaterial != null) { _rainMaterial.SetTexture("_ProvinceIDMap", idMap); }
|
||||
if (_splashMaterial != null) { _splashMaterial.SetTexture("_ProvinceIDMap", idMap); }
|
||||
}
|
||||
|
||||
public void SetMapContainer(RectTransform container) {
|
||||
_mapContainer = container;
|
||||
_canvas = container?.GetComponentInParent<Canvas>();
|
||||
}
|
||||
|
||||
void LateUpdate() {
|
||||
UpdateMapBounds();
|
||||
UpdateZoomCompensation();
|
||||
}
|
||||
|
||||
private void UpdateMapBounds() {
|
||||
if (_mapContainer == null) { return; }
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
_mapContainer.GetWorldCorners(corners);
|
||||
|
||||
Camera cam = _canvas?.worldCamera ?? Camera.main;
|
||||
Vector3 screenBL, screenTR;
|
||||
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay) {
|
||||
screenBL = corners[0];
|
||||
screenTR = corners[2];
|
||||
} else {
|
||||
screenBL = cam != null ? cam.WorldToScreenPoint(corners[0]) : corners[0];
|
||||
screenTR = cam != null ? cam.WorldToScreenPoint(corners[2]) : corners[2];
|
||||
}
|
||||
|
||||
var mapBounds = new Vector4(
|
||||
screenBL.x,
|
||||
screenBL.y,
|
||||
screenTR.x - screenBL.x,
|
||||
screenTR.y - screenBL.y);
|
||||
if (_rainMaterial != null) { _rainMaterial.SetVector("_MapBounds", mapBounds); }
|
||||
if (_splashMaterial != null) { _splashMaterial.SetVector("_MapBounds", mapBounds); }
|
||||
}
|
||||
|
||||
private void UpdateZoomCompensation() {
|
||||
if (_mapContainer == null) { return; }
|
||||
|
||||
float zoom = _mapContainer.localScale.x;
|
||||
if (Mathf.Approximately(zoom, _lastZoom)) { return; }
|
||||
_lastZoom = zoom;
|
||||
|
||||
float inverseZoom = 1f / zoom;
|
||||
|
||||
// Counter-scale rain system
|
||||
if (_rainSystem != null) {
|
||||
_rainSystem.transform.localScale = Vector3.one * inverseZoom;
|
||||
var shape = _rainSystem.shape;
|
||||
shape.scale = _baseRainShapeScale * zoom;
|
||||
shape.position = _baseRainShapePosition * zoom;
|
||||
var emission = _rainSystem.emission;
|
||||
emission.rateOverTime = _baseRainEmitRate * zoom * zoom;
|
||||
}
|
||||
|
||||
// Counter-scale splash system
|
||||
if (_splashSystem != null) {
|
||||
_splashSystem.transform.localScale = Vector3.one * inverseZoom;
|
||||
var shape = _splashSystem.shape;
|
||||
shape.radius = _baseSplashRadius * zoom;
|
||||
var emission = _splashSystem.emission;
|
||||
emission.rateOverTime = _baseSplashEmitRate * zoom * zoom;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_rainMaterial != null) { Destroy(_rainMaterial); }
|
||||
if (_splashMaterial != null) { Destroy(_splashMaterial); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 336c92d7cae77487fa6915c9839d38f6
|
||||
+1
-24
@@ -9,11 +9,6 @@ namespace eagle {
|
||||
public class FreeHeroesTableController : MonoBehaviour {
|
||||
public GameObject panel;
|
||||
|
||||
[Header("Hero Backstory Popup")]
|
||||
public GameObject popupPanel;
|
||||
public HeroDetailsController popupPanelDetailsController;
|
||||
public GeneratedTextUpdater popupPanelBackstory;
|
||||
|
||||
private ProvinceView _currentProvince;
|
||||
public ProvinceView CurrentProvince {
|
||||
get => _currentProvince;
|
||||
@@ -78,21 +73,6 @@ namespace eagle {
|
||||
UpdateUnaffiliatedHeroSelections();
|
||||
}
|
||||
|
||||
public void LongHoverRowChanged(int? newIndex) {
|
||||
if (popupPanel == null) return;
|
||||
|
||||
if (newIndex is {} index) {
|
||||
var heroId = UnaffiliatedHeroes.ElementAt(index).HeroId;
|
||||
if (Model.Heroes.TryGetValue(heroId, out var hero)) {
|
||||
popupPanelDetailsController.SetHero(hero, Model);
|
||||
popupPanelBackstory.TextId = hero.BackstoryTextId;
|
||||
popupPanel.SetActive(true);
|
||||
}
|
||||
} else {
|
||||
popupPanel.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setUpUnaffiliatedHeroesTable() {
|
||||
unaffiliatedHeroesTable.RowCount = UnaffiliatedHeroCount;
|
||||
|
||||
@@ -108,10 +88,7 @@ namespace eagle {
|
||||
});
|
||||
}
|
||||
|
||||
void Start() {
|
||||
panel.SetActive(Model != null);
|
||||
if (popupPanel != null) { popupPanel.SetActive(false); }
|
||||
}
|
||||
void Start() { panel.SetActive(Model != null); }
|
||||
|
||||
private bool HasProvinceInfo =>
|
||||
(CurrentProvince != null && CurrentProvince.FullInfo != null);
|
||||
|
||||
+25
-3
@@ -141,9 +141,31 @@ namespace eagle {
|
||||
}
|
||||
|
||||
public void BattalionLongHoverRowChanged(int? newIndex) {
|
||||
// Battalion backstory popups disabled - backstory updates are being eliminated
|
||||
// to reduce LLM request volume. See docs/eliminate-battalion-backstory-updates.md
|
||||
if (battalionPopupPanel != null) { battalionPopupPanel.SetActive(false); }
|
||||
if (battalionPopupPanel == null) return;
|
||||
|
||||
if (newIndex is {} index) {
|
||||
var battalion = Battalions.ElementAt(index);
|
||||
if (!string.IsNullOrEmpty(battalion.BackstoryTextId)) {
|
||||
// Populate header
|
||||
if (battalionPopupPanelName != null) {
|
||||
// Use PrefixedText for plain name; ready for NameTextId when available
|
||||
battalionPopupPanelName.TextId = "";
|
||||
battalionPopupPanelName.PrefixedText = battalion.Name;
|
||||
}
|
||||
if (battalionPopupPanelTypeIcon != null) {
|
||||
var textures = GetComponentInParent<EagleCommonTextures>();
|
||||
battalionPopupPanelTypeIcon.texture =
|
||||
textures?.BattalionType(battalion.Type);
|
||||
}
|
||||
|
||||
battalionPopupPanelBackstory.TextId = battalion.BackstoryTextId;
|
||||
battalionPopupPanel.SetActive(true);
|
||||
} else {
|
||||
battalionPopupPanel.SetActive(false);
|
||||
}
|
||||
} else {
|
||||
battalionPopupPanel.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetUpHeroesTable() {
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using ProvinceId = System.Int32;
|
||||
using FactionId = System.Int32;
|
||||
|
||||
@@ -11,15 +12,9 @@ namespace eagle {
|
||||
public class MapController : MonoBehaviour {
|
||||
public EagleGameController eagleGameController;
|
||||
|
||||
public ProvinceColorManager provinceColorManager;
|
||||
public RawImage mapBWImage;
|
||||
public List<RawImage> provinceImagesList;
|
||||
public ProvinceWeatherController weatherController;
|
||||
public ProvinceFestivalController festivalController;
|
||||
public ProvinceEpidemicController epidemicController;
|
||||
public ProvinceBlizzardController blizzardController;
|
||||
public ProvinceDroughtController droughtController;
|
||||
public ProvinceFloodController floodController;
|
||||
public ProvinceBeastsController beastsController;
|
||||
public MapPinchZoomHandler zoomHandler;
|
||||
|
||||
public TMP_Text provinceNameText;
|
||||
public TMP_Text provinceOwnerText;
|
||||
@@ -39,9 +34,16 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void SetProvinceColor(ProvinceId provinceId, Color color) {
|
||||
if (provinceColorManager != null) {
|
||||
provinceColorManager.SetProvinceColor(provinceId, color);
|
||||
var image = ImageForProvince(provinceId);
|
||||
if (!image.texture) {
|
||||
Texture2D texture = new Texture2D(1, 1);
|
||||
texture.wrapMode = TextureWrapMode.Repeat;
|
||||
image.texture = texture;
|
||||
}
|
||||
|
||||
Texture2D t2d = (Texture2D)image.texture;
|
||||
t2d.SetPixel(0, 0, color);
|
||||
t2d.Apply();
|
||||
}
|
||||
|
||||
private ProvinceId? _selectedProvinceId;
|
||||
@@ -113,18 +115,16 @@ namespace eagle {
|
||||
}
|
||||
|
||||
if (weatherController != null) { weatherController.UpdateWeatherEffects(value); }
|
||||
if (festivalController != null) { festivalController.UpdateFestivalEffects(value); }
|
||||
if (epidemicController != null) { epidemicController.UpdateEpidemicEffects(value); }
|
||||
if (blizzardController != null) { blizzardController.UpdateBlizzardEffects(value); }
|
||||
if (droughtController != null) { droughtController.UpdateDroughtEffects(value); }
|
||||
if (floodController != null) { floodController.UpdateFloodEffects(value); }
|
||||
if (beastsController != null) { beastsController.UpdateBeastsEffects(value); }
|
||||
|
||||
eagleGameController.ProvinceWasSelected(_selectedProvinceId);
|
||||
}
|
||||
}
|
||||
private RawImage ImageForProvince(ProvinceId id) => provinceImagesList[id - 1];
|
||||
|
||||
public ProvinceIDLoader provinceIDLoader;
|
||||
public TextAsset rawGray;
|
||||
|
||||
private int _imageWidth;
|
||||
private int _imageHeight;
|
||||
|
||||
private readonly Color _selectionColor = Color.grey;
|
||||
private readonly Color _targetColor = Color.red;
|
||||
@@ -147,7 +147,9 @@ namespace eagle {
|
||||
}
|
||||
|
||||
void Start() {
|
||||
_mapIdBytes = provinceIDLoader.DecompressedBytes;
|
||||
_mapIdBytes = rawGray.bytes;
|
||||
_imageWidth = mapBWImage.texture.width;
|
||||
_imageHeight = mapBWImage.texture.height;
|
||||
Enabled = true;
|
||||
_previouslyHoveredProvinceId = null;
|
||||
}
|
||||
@@ -156,18 +158,17 @@ namespace eagle {
|
||||
var mousePosition = new Vector2(point.x, Screen.height - point.y);
|
||||
var mapRect = gameObject.GetComponentInParent<PanelPositions>().MapRect;
|
||||
|
||||
int mapWidth = provinceIDLoader.mapWidth;
|
||||
int mapHeight = provinceIDLoader.mapHeight;
|
||||
|
||||
var relativeX = (int)((mousePosition.x - mapRect.x) * mapWidth / mapRect.width);
|
||||
var relativeX = (int)((mousePosition.x - mapRect.x) * _imageWidth / mapRect.width);
|
||||
var relativeY =
|
||||
(int)(mapHeight - (mousePosition.y - mapRect.y) * mapHeight / mapRect.height);
|
||||
(int)(_imageHeight -
|
||||
(mousePosition.y - mapRect.y) * _imageHeight / mapRect.height);
|
||||
|
||||
if (relativeX < 0 || relativeX >= mapWidth || relativeY < 0 || relativeY >= mapHeight) {
|
||||
if (relativeX < 0 || relativeX >= _imageWidth || relativeY < 0 ||
|
||||
relativeY >= _imageHeight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var provinceId = _mapIdBytes[relativeY * mapWidth + relativeX];
|
||||
var provinceId = _mapIdBytes[relativeY * _imageWidth + relativeX];
|
||||
if (provinceId == 0 || provinceId == 0xFF) { return null; }
|
||||
return provinceId;
|
||||
}
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace eagle {
|
||||
/// <summary>
|
||||
/// Handles pinch-to-zoom and scroll-wheel zoom for a map displayed in a ScrollRect.
|
||||
/// Attach this to the same GameObject as the ScrollRect.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(ScrollRect))]
|
||||
public class MapPinchZoomHandler : MonoBehaviour, IScrollHandler {
|
||||
public float minZoom = 1.0f;
|
||||
public float maxZoom = 3.0f;
|
||||
public float scrollZoomSpeed = 0.1f;
|
||||
public float pinchZoomSpeed = 0.01f;
|
||||
public float zoomSmoothTime = 0.1f; // Time to smooth zoom changes
|
||||
|
||||
private ScrollRect _scrollRect;
|
||||
private RectTransform _content;
|
||||
private RectTransform _viewport;
|
||||
private Canvas _canvas;
|
||||
private float _currentZoom = 1.0f;
|
||||
private float _targetZoom = 1.0f;
|
||||
private float _zoomVelocity = 0f;
|
||||
private Vector2 _lastZoomCenter;
|
||||
|
||||
// For coordinate conversion (used by MapController)
|
||||
public float CurrentZoom => _currentZoom;
|
||||
public Vector2 PanOffset {
|
||||
get {
|
||||
if (_content == null) return Vector2.zero;
|
||||
return _content.anchoredPosition;
|
||||
}
|
||||
}
|
||||
|
||||
// Pinch tracking
|
||||
private bool _isPinching;
|
||||
private float _previousPinchDistance;
|
||||
private Vector2 _previousPinchCenter;
|
||||
|
||||
void Awake() {
|
||||
_scrollRect = GetComponent<ScrollRect>();
|
||||
_content = _scrollRect.content;
|
||||
_viewport = _scrollRect.viewport != null ? _scrollRect.viewport
|
||||
: GetComponent<RectTransform>();
|
||||
_canvas = GetComponentInParent<Canvas>();
|
||||
|
||||
// Disable scrolling at zoom level 1 (no panning needed)
|
||||
UpdateScrollability();
|
||||
}
|
||||
|
||||
void Update() {
|
||||
// Handle touch input on iOS
|
||||
if (Application.platform == RuntimePlatform.IPhonePlayer) { HandleTouchInput(); }
|
||||
|
||||
// Smoothly interpolate toward target zoom
|
||||
if (!Mathf.Approximately(_currentZoom, _targetZoom)) {
|
||||
float previousZoom = _currentZoom;
|
||||
_currentZoom = Mathf.SmoothDamp(
|
||||
_currentZoom,
|
||||
_targetZoom,
|
||||
ref _zoomVelocity,
|
||||
zoomSmoothTime);
|
||||
|
||||
// Apply the smoothed zoom
|
||||
ApplyCurrentZoom(_lastZoomCenter, previousZoom);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleTouchInput() {
|
||||
if (Input.touchCount == 2) {
|
||||
Touch touch0 = Input.GetTouch(0);
|
||||
Touch touch1 = Input.GetTouch(1);
|
||||
|
||||
float currentDistance = Vector2.Distance(touch0.position, touch1.position);
|
||||
Vector2 currentCenter = (touch0.position + touch1.position) / 2f;
|
||||
|
||||
if (!_isPinching) {
|
||||
_isPinching = true;
|
||||
_previousPinchDistance = currentDistance;
|
||||
_previousPinchCenter = currentCenter;
|
||||
// Disable scroll rect during pinch to prevent fighting
|
||||
_scrollRect.enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate zoom delta from pinch
|
||||
float distanceDelta = currentDistance - _previousPinchDistance;
|
||||
float zoomDelta = distanceDelta * pinchZoomSpeed;
|
||||
ApplyZoom(zoomDelta, currentCenter);
|
||||
|
||||
_previousPinchDistance = currentDistance;
|
||||
_previousPinchCenter = currentCenter;
|
||||
} else {
|
||||
if (_isPinching) {
|
||||
_isPinching = false;
|
||||
UpdateScrollability();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IScrollHandler - captures scroll wheel events
|
||||
public void OnScroll(PointerEventData eventData) {
|
||||
float scrollDelta = eventData.scrollDelta.y;
|
||||
if (scrollDelta != 0) { ApplyZoom(scrollDelta * scrollZoomSpeed, eventData.position); }
|
||||
}
|
||||
|
||||
private void ApplyZoom(float delta, Vector2 screenCenter) {
|
||||
// Set target zoom - actual zoom is applied smoothly in Update()
|
||||
_targetZoom = Mathf.Clamp(_targetZoom + delta, minZoom, maxZoom);
|
||||
_lastZoomCenter = screenCenter;
|
||||
}
|
||||
|
||||
private void ApplyCurrentZoom(Vector2 screenCenter, float previousZoom) {
|
||||
// Get the cursor position relative to viewport (0,0 = bottom-left, 1,1 = top-right)
|
||||
// Use canvas camera for ScreenSpaceCamera mode, null for ScreenSpaceOverlay
|
||||
Camera cam = _canvas != null ? _canvas.worldCamera : null;
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
_viewport,
|
||||
screenCenter,
|
||||
cam,
|
||||
out Vector2 viewportLocalPoint);
|
||||
|
||||
Vector2 viewportSize = _viewport.rect.size;
|
||||
Vector2 cursorViewportNorm = new Vector2(
|
||||
(viewportLocalPoint.x + viewportSize.x * _viewport.pivot.x) / viewportSize.x,
|
||||
(viewportLocalPoint.y + viewportSize.y * _viewport.pivot.y) / viewportSize.y);
|
||||
|
||||
// Calculate content size before and after zoom
|
||||
Vector2 contentSizeBefore = _content.rect.size * previousZoom;
|
||||
Vector2 contentSizeAfter = _content.rect.size * _currentZoom;
|
||||
|
||||
// Calculate the scrollable range (content size - viewport size)
|
||||
Vector2 scrollRangeBefore = contentSizeBefore - viewportSize;
|
||||
Vector2 scrollRangeAfter = contentSizeAfter - viewportSize;
|
||||
|
||||
// Get current scroll offset in pixels
|
||||
Vector2 scrollOffsetBefore = new Vector2(
|
||||
scrollRangeBefore.x > 0 ? _scrollRect.normalizedPosition.x * scrollRangeBefore.x
|
||||
: 0,
|
||||
scrollRangeBefore.y > 0 ? _scrollRect.normalizedPosition.y * scrollRangeBefore.y
|
||||
: 0);
|
||||
|
||||
// The point under cursor in content space (from bottom-left of content)
|
||||
Vector2 cursorContentPoint = scrollOffsetBefore + cursorViewportNorm * viewportSize;
|
||||
|
||||
// After zoom, where should scroll offset be to keep same content point under cursor?
|
||||
float zoomRatio = _currentZoom / previousZoom;
|
||||
Vector2 cursorContentPointAfter = cursorContentPoint * zoomRatio;
|
||||
Vector2 scrollOffsetAfter = cursorContentPointAfter - cursorViewportNorm * viewportSize;
|
||||
|
||||
// Apply the zoom scale
|
||||
_content.localScale = Vector3.one * _currentZoom;
|
||||
|
||||
// Update scrollability based on new zoom level
|
||||
UpdateScrollability();
|
||||
|
||||
// Convert back to normalized position
|
||||
if (_currentZoom > 1.0f && scrollRangeAfter.x > 0 && scrollRangeAfter.y > 0) {
|
||||
Vector2 newNormalizedPos = new Vector2(
|
||||
Mathf.Clamp01(scrollOffsetAfter.x / scrollRangeAfter.x),
|
||||
Mathf.Clamp01(scrollOffsetAfter.y / scrollRangeAfter.y));
|
||||
_scrollRect.normalizedPosition = newNormalizedPos;
|
||||
} else if (_currentZoom <= 1.0f) {
|
||||
// At zoom 1, center the content
|
||||
_scrollRect.normalizedPosition = new Vector2(0.5f, 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateScrollability() {
|
||||
// Only allow scrolling when zoomed in
|
||||
bool canScroll = _currentZoom > 1.0f;
|
||||
_scrollRect.horizontal = canScroll;
|
||||
_scrollRect.vertical = canScroll;
|
||||
_scrollRect.enabled = canScroll || !_isPinching;
|
||||
}
|
||||
|
||||
public void ResetZoom() {
|
||||
_currentZoom = 1.0f;
|
||||
_targetZoom = 1.0f;
|
||||
_zoomVelocity = 0f;
|
||||
_content.localScale = Vector3.one;
|
||||
_scrollRect.normalizedPosition = new Vector2(0.5f, 0.5f);
|
||||
UpdateScrollability();
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc258a4d89a7a41278242c1405ccebe5
|
||||
Binary file not shown.
+42
-45
@@ -1,13 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44b7f6bedeca4c3a8c2f501e0d5cd8d7
|
||||
guid: 2f6e3de2b19c54f4f831c3b5b58b9910
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
serializedVersion: 10
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
sRGBTexture: 0
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
@@ -20,12 +20,9 @@ TextureImporter:
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
@@ -34,12 +31,12 @@ TextureImporter:
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
filterMode: -1
|
||||
aniso: -1
|
||||
mipBias: -100
|
||||
wrapU: -1
|
||||
wrapV: -1
|
||||
wrapW: -1
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
@@ -51,80 +48,80 @@ TextureImporter:
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaUsage: 2
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
maxTextureSize: 4096
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
compressionQuality: 20
|
||||
crunchedCompression: 1
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
maxTextureSize: 4096
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
compressionQuality: 20
|
||||
crunchedCompression: 1
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: iOS
|
||||
maxTextureSize: 2048
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 4096
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
compressionQuality: 20
|
||||
crunchedCompression: 1
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 4096
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 20
|
||||
crunchedCompression: 1
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
+42
-45
@@ -1,13 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b6ab1e235eea4e4e9319c88bb1cdc70
|
||||
guid: 0bc9ccd5f98af42ed87bbbb4052fd300
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
serializedVersion: 10
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 0
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
@@ -20,12 +20,9 @@ TextureImporter:
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
@@ -34,16 +31,16 @@ TextureImporter:
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
filterMode: -1
|
||||
aniso: -1
|
||||
mipBias: -100
|
||||
wrapU: -1
|
||||
wrapV: -1
|
||||
wrapW: -1
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
@@ -51,80 +48,80 @@ TextureImporter:
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaUsage: 2
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
maxTextureSize: 4096
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
compressionQuality: 20
|
||||
crunchedCompression: 1
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
maxTextureSize: 4096
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
compressionQuality: 20
|
||||
crunchedCompression: 1
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: iOS
|
||||
maxTextureSize: 2048
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 4096
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
compressionQuality: 20
|
||||
crunchedCompression: 1
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 4096
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 20
|
||||
crunchedCompression: 1
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user