mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 07:15:58 +00:00
Compare commits
124
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7b92ae927 | ||
|
|
23ab715ffc | ||
|
|
73f42d5b51 | ||
|
|
3cb61f69f4 | ||
|
|
62fff3c0ea | ||
|
|
ca86926a25 | ||
|
|
1d334c91ae | ||
|
|
64e1e46e0a | ||
|
|
4722a40384 | ||
|
|
6653a14660 | ||
|
|
056571651e | ||
|
|
d8224e7886 | ||
|
|
9cc9a65e4a | ||
|
|
025563b607 | ||
|
|
030f6b2c2e | ||
|
|
1d5dd7b971 | ||
|
|
649e80c4ac | ||
|
|
a1b4e41553 | ||
|
|
1c24474a0b | ||
|
|
26eec6cabc | ||
|
|
d134f40438 | ||
|
|
a638dd26ca | ||
|
|
5e93b0eaa0 | ||
|
|
6940312d3f | ||
|
|
243f0d43e7 | ||
|
|
1ddd015449 | ||
|
|
da85d0983a | ||
|
|
ae574e6ff5 | ||
|
|
c1ac57b929 | ||
|
|
be51daa148 | ||
|
|
97605d0a63 | ||
|
|
9cca922b70 | ||
|
|
6eab9ffc3c | ||
|
|
08eaffb927 | ||
|
|
a2bc55e0f5 | ||
|
|
19861377a6 | ||
|
|
cc70087efc | ||
|
|
b2383c8384 | ||
|
|
03958b6914 | ||
|
|
4cbf3ea39f | ||
|
|
3dbbe74830 | ||
|
|
dab4365f16 | ||
|
|
0b3cd4f1c6 | ||
|
|
49d42a599a | ||
|
|
5ae7d8a9cc | ||
|
|
ee47f7d6bc | ||
|
|
fe3c2d3e79 | ||
|
|
de21646a37 | ||
|
|
040f2d55b9 | ||
|
|
5b32712b0f | ||
|
|
bdec4a0015 | ||
|
|
c2c7b85da5 | ||
|
|
9e287ba3bb | ||
|
|
1a0cfe8578 | ||
|
|
a841720c75 | ||
|
|
064a606d13 | ||
|
|
b84af5798b | ||
|
|
440af18210 | ||
|
|
722e107d53 | ||
|
|
9cc83fa2f5 | ||
|
|
cd9db60b80 | ||
|
|
5c0be5e3a3 | ||
|
|
fdc6c7f8cf | ||
|
|
37ac0dd19f | ||
|
|
13037aec56 | ||
|
|
2acfec76a9 | ||
|
|
ad105d60d9 | ||
|
|
a8048fbef3 | ||
|
|
ad72f11e29 | ||
|
|
14c03f34ef | ||
|
|
eb63977ca2 | ||
|
|
1cd23b566b | ||
|
|
779e7eceec | ||
|
|
1ed5252d44 | ||
|
|
836c09c357 | ||
|
|
6f73ac71ab | ||
|
|
5e68fc344d | ||
|
|
2690332b10 | ||
|
|
99109e7e60 | ||
|
|
bd9b850d45 | ||
|
|
867b4ac66e | ||
|
|
752f078c74 | ||
|
|
4348579000 | ||
|
|
a3dfaf6445 | ||
|
|
aadb517771 | ||
|
|
ff74504a26 | ||
|
|
1e72e53f0e | ||
|
|
66419dee0b | ||
|
|
0726c41609 | ||
|
|
472b33e8de | ||
|
|
908716c8af | ||
|
|
1b52c48e5b | ||
|
|
f6e5235382 | ||
|
|
138a8a76cd | ||
|
|
ffd2b63397 | ||
|
|
8400b44acd | ||
|
|
06db8a9059 | ||
|
|
0262aa5a87 | ||
|
|
60e0c8fc96 | ||
|
|
d5a836db79 | ||
|
|
20f3d14721 | ||
|
|
4d7824656b | ||
|
|
5b18e275af | ||
|
|
ca39d16710 | ||
|
|
6e47e836c2 | ||
|
|
75668cdc58 | ||
|
|
fad349bf35 | ||
|
|
956728670a | ||
|
|
d6bb7a23f2 | ||
|
|
241426c0fa | ||
|
|
69923e12b9 | ||
|
|
3f921c663f | ||
|
|
d49b402988 | ||
|
|
898af3b858 | ||
|
|
043302ed6a | ||
|
|
c010206094 | ||
|
|
ecbca95051 | ||
|
|
ac92f4c112 | ||
|
|
0a8f43f548 | ||
|
|
3722c120a7 | ||
|
|
841ebd8ae4 | ||
|
|
be83fc882e | ||
|
|
425fa082ab | ||
|
|
268f28e755 |
@@ -105,10 +105,23 @@ jobs:
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
|
||||
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
|
||||
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
|
||||
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Copy update-env script to server
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
source: "deploy/update-env.sh,deploy/env.template"
|
||||
target: /opt/eagle0/
|
||||
strip_components: 1
|
||||
|
||||
- name: Deploy auth service to production
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
@@ -116,34 +129,23 @@ jobs:
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
script_stop: true
|
||||
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,JWT_PRIVATE_KEY
|
||||
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
|
||||
script: |
|
||||
set -x
|
||||
cd /opt/eagle0
|
||||
|
||||
# Update AUTH_IMAGE in .env file (preserve other vars)
|
||||
if [ -f .env ]; then
|
||||
# Remove old AUTH_IMAGE line and add new one
|
||||
grep -v '^AUTH_IMAGE=' .env > .env.tmp || true
|
||||
echo "AUTH_IMAGE=${AUTH_IMAGE}" >> .env.tmp
|
||||
# Also update OAuth credentials
|
||||
grep -v '^DISCORD_CLIENT_ID=' .env.tmp > .env.tmp2 || true
|
||||
echo "DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID:-}" >> .env.tmp2
|
||||
grep -v '^DISCORD_CLIENT_SECRET=' .env.tmp2 > .env.tmp3 || true
|
||||
echo "DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET:-}" >> .env.tmp3
|
||||
grep -v '^GOOGLE_CLIENT_ID=' .env.tmp3 > .env.tmp4 || true
|
||||
echo "GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}" >> .env.tmp4
|
||||
grep -v '^GOOGLE_CLIENT_SECRET=' .env.tmp4 > .env.tmp5 || true
|
||||
echo "GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}" >> .env.tmp5
|
||||
# Update JWT private key (JWK format for auth service bootstrap)
|
||||
grep -v '^JWT_PRIVATE_KEY=' .env.tmp5 > .env || true
|
||||
echo "JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:-}" >> .env
|
||||
rm -f .env.tmp .env.tmp2 .env.tmp3 .env.tmp4 .env.tmp5
|
||||
chmod 600 .env
|
||||
else
|
||||
echo "ERROR: .env file not found. Run main deploy first."
|
||||
exit 1
|
||||
fi
|
||||
# Update env vars using shared script (preserves vars set by other workflows)
|
||||
chmod +x update-env.sh
|
||||
./update-env.sh \
|
||||
"AUTH_IMAGE=${AUTH_IMAGE}" \
|
||||
"DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}" \
|
||||
"DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET}" \
|
||||
"GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}" \
|
||||
"GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}" \
|
||||
"JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}" \
|
||||
"FASTMAIL_API_TOKEN=${FASTMAIL_API_TOKEN}" \
|
||||
"FASTMAIL_FROM_EMAIL=${FASTMAIL_FROM_EMAIL}" \
|
||||
"FASTMAIL_FROM_NAME=${FASTMAIL_FROM_NAME}"
|
||||
|
||||
# Login to registry
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
|
||||
|
||||
@@ -53,26 +53,26 @@ jobs:
|
||||
run: |
|
||||
# Upload sysroot tarball to DO Spaces
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
# Upload sha256 file
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
echo ""
|
||||
echo "=== AMD64 Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz"
|
||||
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz"
|
||||
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)"
|
||||
echo ""
|
||||
echo "Update MODULE.bazel with:"
|
||||
echo "sysroot("
|
||||
echo " name = \"linux_sysroot\","
|
||||
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)\","
|
||||
echo " urls = [\"https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz\"],"
|
||||
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz\"],"
|
||||
echo ")"
|
||||
|
||||
build-sysroot-arm64:
|
||||
@@ -114,24 +114,24 @@ jobs:
|
||||
run: |
|
||||
# Upload sysroot tarball to DO Spaces
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.tar.xz \
|
||||
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
# Upload sha256 file
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256 \
|
||||
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.sha256 \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.sha256 \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
echo ""
|
||||
echo "=== ARM64 Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz"
|
||||
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz"
|
||||
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)"
|
||||
echo ""
|
||||
echo "Update MODULE.bazel with:"
|
||||
echo "sysroot("
|
||||
echo " name = \"linux_sysroot_arm64\","
|
||||
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)\","
|
||||
echo " urls = [\"https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz\"],"
|
||||
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz\"],"
|
||||
echo ")"
|
||||
|
||||
+193
-118
@@ -42,6 +42,13 @@ jobs:
|
||||
set -ex
|
||||
bazel build --platforms=//:linux_x86_64 //ci:eagle_server_image
|
||||
|
||||
# Also build the warmup tool for Linux
|
||||
bazel build //src/main/go/net/eagle0/warmup:warmup_linux_amd64
|
||||
|
||||
# Copy warmup binary to scripts/ so it gets deployed with other scripts
|
||||
mkdir -p scripts/bin
|
||||
cp bazel-bin/src/main/go/net/eagle0/warmup/warmup_linux_amd64_/warmup_linux_amd64 scripts/bin/warmup
|
||||
|
||||
# Save the resolved path before any other bazel command changes bazel-bin symlink
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/eagle_server_image)
|
||||
echo "Image path: $IMAGE_PATH"
|
||||
@@ -421,7 +428,7 @@ jobs:
|
||||
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
needs: [build-eagle, build-shardok, build-admin, build-jfr-sidecar]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
@@ -443,135 +450,203 @@ jobs:
|
||||
SHARDOK_ADDRESS: ${{ secrets.SHARDOK_ADDRESS }}
|
||||
SHARDOK_AUTH_TOKEN: ${{ secrets.SHARDOK_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
|
||||
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
|
||||
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
|
||||
DO_DROPLET_IP: ${{ secrets.DO_DROPLET_IP }}
|
||||
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.DO_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Build warmup tool
|
||||
run: |
|
||||
bazel build //src/main/go/net/eagle0/warmup:warmup_linux_amd64
|
||||
mkdir -p scripts/bin
|
||||
cp bazel-bin/src/main/go/net/eagle0/warmup/warmup_linux_amd64_/warmup_linux_amd64 scripts/bin/warmup
|
||||
|
||||
- name: Copy config files to droplet
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
source: "docker-compose.prod.yml,nginx/nginx.conf"
|
||||
target: "/opt/eagle0"
|
||||
run: |
|
||||
# Create directory structure on remote
|
||||
# Use -p to create parents, and test write access before copying
|
||||
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << 'SETUP_DIRS'
|
||||
set -e
|
||||
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx /opt/eagle0/deploy
|
||||
|
||||
# Remove existing warmup binary (it may have read-only permissions from bazel)
|
||||
rm -f /opt/eagle0/scripts/bin/warmup
|
||||
SETUP_DIRS
|
||||
|
||||
# Copy files preserving structure
|
||||
scp -i ~/.ssh/deploy_key docker-compose.prod.yml deploy@"$DO_DROPLET_IP":/opt/eagle0/
|
||||
scp -i ~/.ssh/deploy_key nginx/nginx.conf deploy@"$DO_DROPLET_IP":/opt/eagle0/nginx/
|
||||
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
|
||||
scp -i ~/.ssh/deploy_key scripts/bin/warmup deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/bin/
|
||||
scp -i ~/.ssh/deploy_key deploy/env.template deploy/update-env.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/deploy/
|
||||
|
||||
- name: Deploy to production droplet
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
script_stop: true
|
||||
envs: EAGLE_IMAGE,SHARDOK_IMAGE,ADMIN_IMAGE,JFR_SIDECAR_IMAGE,OPENAI_API_KEY,GPT_MODEL_NAME,EAGLE_ENABLE_S3,DO_SPACES_ACCESS_KEY,DO_SPACES_SECRET_KEY,JWT_PRIVATE_KEY,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,SHARDOK_ADDRESS,SHARDOK_AUTH_TOKEN,SENTRY_DSN
|
||||
script: |
|
||||
set -x
|
||||
cd /opt/eagle0
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << DEPLOY_SCRIPT
|
||||
set -ex
|
||||
cd /opt/eagle0
|
||||
|
||||
# Check Docker has IPv6 support for connecting to Hetzner Shardok
|
||||
# (One-time setup: sudo tee /etc/docker/daemon.json <<< '{"ipv6": true, "ip6tables": true, "experimental": true, "fixed-cidr-v6": "fd00::/80"}' && sudo systemctl restart docker)
|
||||
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
|
||||
echo "WARNING: Docker IPv6 not configured. Eagle may not reach Hetzner Shardok."
|
||||
echo "Run: sudo tee /etc/docker/daemon.json <<< '{\"ipv6\": true, \"ip6tables\": true, \"experimental\": true, \"fixed-cidr-v6\": \"fd00::/80\"}' && sudo systemctl restart docker"
|
||||
# Environment variables passed via heredoc
|
||||
EAGLE_IMAGE="${EAGLE_IMAGE}"
|
||||
SHARDOK_IMAGE="${SHARDOK_IMAGE}"
|
||||
ADMIN_IMAGE="${ADMIN_IMAGE}"
|
||||
JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
|
||||
OPENAI_API_KEY="${OPENAI_API_KEY}"
|
||||
GPT_MODEL_NAME="${GPT_MODEL_NAME}"
|
||||
EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3}"
|
||||
DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
|
||||
DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
|
||||
JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
|
||||
DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
|
||||
DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
|
||||
GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
|
||||
GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
|
||||
SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
|
||||
SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
|
||||
SENTRY_DSN="${SENTRY_DSN}"
|
||||
FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
|
||||
# Check Docker has IPv6 support for connecting to Hetzner Shardok
|
||||
# (One-time setup: sudo tee /etc/docker/daemon.json <<< '{"ipv6": true, "ip6tables": true, "experimental": true, "fixed-cidr-v6": "fd00::/80"}' && sudo systemctl restart docker)
|
||||
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
|
||||
echo "WARNING: Docker IPv6 not configured. Eagle may not reach Hetzner Shardok."
|
||||
echo "Run: sudo tee /etc/docker/daemon.json <<< '{\"ipv6\": true, \"ip6tables\": true, \"experimental\": true, \"fixed-cidr-v6\": \"fd00::/80\"}' && sudo systemctl restart docker"
|
||||
fi
|
||||
|
||||
# Update env vars using shared script (preserves vars set by other workflows)
|
||||
chmod +x update-env.sh
|
||||
./update-env.sh \
|
||||
"EAGLE_IMAGE=\${EAGLE_IMAGE}" \
|
||||
"SHARDOK_IMAGE=\${SHARDOK_IMAGE}" \
|
||||
"ADMIN_IMAGE=\${ADMIN_IMAGE}" \
|
||||
"JFR_SIDECAR_IMAGE=\${JFR_SIDECAR_IMAGE}" \
|
||||
"OPENAI_API_KEY=\${OPENAI_API_KEY}" \
|
||||
"GPT_MODEL_NAME=\${GPT_MODEL_NAME:-gpt-4o}" \
|
||||
"EAGLE_ENABLE_S3=\${EAGLE_ENABLE_S3:-false}" \
|
||||
"DO_SPACES_ACCESS_KEY=\${DO_SPACES_ACCESS_KEY}" \
|
||||
"DO_SPACES_SECRET_KEY=\${DO_SPACES_SECRET_KEY}" \
|
||||
"JWT_PRIVATE_KEY=\${JWT_PRIVATE_KEY}" \
|
||||
"DISCORD_CLIENT_ID=\${DISCORD_CLIENT_ID}" \
|
||||
"DISCORD_CLIENT_SECRET=\${DISCORD_CLIENT_SECRET}" \
|
||||
"GOOGLE_CLIENT_ID=\${GOOGLE_CLIENT_ID}" \
|
||||
"GOOGLE_CLIENT_SECRET=\${GOOGLE_CLIENT_SECRET}" \
|
||||
"SHARDOK_ADDRESS=\${SHARDOK_ADDRESS:-shardok:40042}" \
|
||||
"SHARDOK_AUTH_TOKEN=\${SHARDOK_AUTH_TOKEN}" \
|
||||
"SENTRY_DSN=\${SENTRY_DSN}" \
|
||||
"FASTMAIL_API_TOKEN=\${FASTMAIL_API_TOKEN}" \
|
||||
"FASTMAIL_FROM_EMAIL=\${FASTMAIL_FROM_EMAIL}" \
|
||||
"FASTMAIL_FROM_NAME=\${FASTMAIL_FROM_NAME}"
|
||||
|
||||
# Login to registry
|
||||
echo "\$DO_REGISTRY_TOKEN" | docker login registry.digitalocean.com -u "\$DO_REGISTRY_TOKEN" --password-stdin
|
||||
|
||||
# Use exact image tags passed from build jobs (no :latest fallback)
|
||||
# Note: AUTH_IMAGE is managed separately by auth_build.yml
|
||||
echo "Using images: \$EAGLE_IMAGE, \$SHARDOK_IMAGE, \$ADMIN_IMAGE, \$JFR_SIDECAR_IMAGE"
|
||||
|
||||
# Use crane to pull images (handles OCI format correctly) then load into Docker
|
||||
# This avoids digest mismatch from DO registry's OCI->Docker format conversion
|
||||
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
|
||||
if [ ! -x crane ]; then
|
||||
echo "ERROR: Failed to install crane"
|
||||
ls -la crane || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Crane installed"
|
||||
ls -la crane
|
||||
|
||||
# crane uses Docker config for auth
|
||||
echo "Pulling Eagle image with crane..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar || { echo "ERROR: Failed to pull eagle image"; exit 1; }
|
||||
echo "Loading Eagle image into Docker..."
|
||||
docker load -i eagle.tar
|
||||
rm eagle.tar
|
||||
|
||||
echo "Pulling Shardok image with crane..."
|
||||
./crane pull "\${SHARDOK_IMAGE}" shardok.tar || { echo "ERROR: Failed to pull shardok image"; exit 1; }
|
||||
echo "Loading Shardok image into Docker..."
|
||||
docker load -i shardok.tar
|
||||
rm shardok.tar
|
||||
|
||||
echo "Pulling Admin image with crane..."
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar || { echo "ERROR: Failed to pull admin image"; exit 1; }
|
||||
echo "Loading Admin image into Docker..."
|
||||
docker load -i admin.tar
|
||||
rm admin.tar
|
||||
|
||||
echo "Pulling JFR Sidecar image with crane..."
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar || { echo "ERROR: Failed to pull jfr-sidecar image"; exit 1; }
|
||||
echo "Loading JFR Sidecar image into Docker..."
|
||||
docker load -i jfr-sidecar.tar
|
||||
rm jfr-sidecar.tar
|
||||
|
||||
# Keep crane for blue-green deploys (don't delete it)
|
||||
echo "Crane kept at ./crane for future blue-green deploys"
|
||||
|
||||
# Also pull other compose images
|
||||
docker pull nginx:alpine || true
|
||||
docker pull certbot/certbot || true
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# Recreate non-Eagle services (not auth - managed by auth_build.yml)
|
||||
# Note: jfr-sidecar must start after eagle-blue due to PID namespace sharing
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate shardok admin
|
||||
|
||||
# Deploy Eagle with blue-green (zero-downtime) if scripts are available
|
||||
if [ -x "/opt/eagle0/scripts/deploy-blue-green.sh" ]; then
|
||||
echo "Using blue-green deployment for Eagle..."
|
||||
chmod +x /opt/eagle0/scripts/*.sh
|
||||
# Make warmup binary executable if present
|
||||
if [ -f "/opt/eagle0/scripts/bin/warmup" ]; then
|
||||
chmod +x /opt/eagle0/scripts/bin/warmup
|
||||
fi
|
||||
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
|
||||
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}" || {
|
||||
echo "Blue-green deployment failed, falling back to direct restart"
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
|
||||
}
|
||||
else
|
||||
echo "Blue-green scripts not found, using direct restart..."
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
|
||||
fi
|
||||
|
||||
# Write env vars to .env file for docker-compose
|
||||
# Preserve AUTH_IMAGE if it exists (managed by auth_build.yml)
|
||||
EXISTING_AUTH_IMAGE=""
|
||||
if [ -f .env ]; then
|
||||
EXISTING_AUTH_IMAGE=$(grep '^AUTH_IMAGE=' .env | cut -d= -f2- || true)
|
||||
fi
|
||||
# Start jfr-sidecar after eagle-blue (shares PID namespace with eagle-blue)
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate jfr-sidecar
|
||||
|
||||
rm -f .env 2>/dev/null || true
|
||||
cat > .env << EOF
|
||||
EAGLE_IMAGE=${EAGLE_IMAGE}
|
||||
SHARDOK_IMAGE=${SHARDOK_IMAGE}
|
||||
ADMIN_IMAGE=${ADMIN_IMAGE}
|
||||
JFR_SIDECAR_IMAGE=${JFR_SIDECAR_IMAGE}
|
||||
AUTH_IMAGE=${EXISTING_AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest}
|
||||
OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||
GPT_MODEL_NAME=${GPT_MODEL_NAME:-gpt-4o}
|
||||
EAGLE_ENABLE_S3=${EAGLE_ENABLE_S3:-false}
|
||||
DO_SPACES_ACCESS_KEY=${DO_SPACES_ACCESS_KEY:-}
|
||||
DO_SPACES_SECRET_KEY=${DO_SPACES_SECRET_KEY:-}
|
||||
JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:-}
|
||||
DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID:-}
|
||||
DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET:-}
|
||||
GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
||||
GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
||||
SHARDOK_ADDRESS=${SHARDOK_ADDRESS:-shardok:40042}
|
||||
SHARDOK_AUTH_TOKEN=${SHARDOK_AUTH_TOKEN:-}
|
||||
SENTRY_DSN=${SENTRY_DSN:-}
|
||||
EOF
|
||||
chmod 600 .env
|
||||
# Ensure auth is running (but don't force-recreate it)
|
||||
docker compose -f docker-compose.prod.yml up -d auth
|
||||
|
||||
# Login to registry
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
|
||||
# Restart nginx to pick up new container IPs
|
||||
# (nginx caches DNS at startup, so it needs restart after eagle/shardok)
|
||||
docker compose -f docker-compose.prod.yml up -d --force-recreate nginx
|
||||
|
||||
# Use exact image tags passed from build jobs (no :latest fallback)
|
||||
# Note: AUTH_IMAGE is managed separately by auth_build.yml
|
||||
echo "Using images: $EAGLE_IMAGE, $SHARDOK_IMAGE, $ADMIN_IMAGE, $JFR_SIDECAR_IMAGE"
|
||||
# Wait for health checks
|
||||
sleep 10
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Use crane to pull images (handles OCI format correctly) then load into Docker
|
||||
# This avoids digest mismatch from DO registry's OCI->Docker format conversion
|
||||
echo "Installing 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
|
||||
# Verify containers are using correct images
|
||||
echo "=== Verifying container image tags ==="
|
||||
docker compose -f docker-compose.prod.yml images
|
||||
|
||||
# crane uses Docker config for auth
|
||||
echo "Pulling Eagle image with crane..."
|
||||
./crane pull "${EAGLE_IMAGE}" eagle.tar || { echo "ERROR: Failed to pull eagle image"; exit 1; }
|
||||
echo "Loading Eagle image into Docker..."
|
||||
docker load -i eagle.tar
|
||||
rm eagle.tar
|
||||
|
||||
echo "Pulling Shardok image with crane..."
|
||||
./crane pull "${SHARDOK_IMAGE}" shardok.tar || { echo "ERROR: Failed to pull shardok image"; exit 1; }
|
||||
echo "Loading Shardok image into Docker..."
|
||||
docker load -i shardok.tar
|
||||
rm shardok.tar
|
||||
|
||||
echo "Pulling Admin image with crane..."
|
||||
./crane pull "${ADMIN_IMAGE}" admin.tar || { echo "ERROR: Failed to pull admin image"; exit 1; }
|
||||
echo "Loading Admin image into Docker..."
|
||||
docker load -i admin.tar
|
||||
rm admin.tar
|
||||
|
||||
echo "Pulling JFR Sidecar image with crane..."
|
||||
./crane pull "${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar || { echo "ERROR: Failed to pull jfr-sidecar image"; exit 1; }
|
||||
echo "Loading JFR Sidecar image into Docker..."
|
||||
docker load -i jfr-sidecar.tar
|
||||
rm jfr-sidecar.tar
|
||||
|
||||
rm ./crane
|
||||
|
||||
# Also pull other compose images
|
||||
docker pull nginx:alpine || true
|
||||
docker pull certbot/certbot || true
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# Recreate only the services we're updating (not auth - managed by auth_build.yml)
|
||||
# This prevents restarting auth service during every Eagle deploy
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle shardok admin jfr-sidecar
|
||||
|
||||
# Ensure auth is running (but don't force-recreate it)
|
||||
docker compose -f docker-compose.prod.yml up -d auth
|
||||
|
||||
# Restart nginx to pick up new container IPs
|
||||
# (nginx caches DNS at startup, so it needs restart after eagle/shardok)
|
||||
docker compose -f docker-compose.prod.yml up -d --force-recreate nginx
|
||||
|
||||
# Cleanup orphaned containers
|
||||
docker compose -f docker-compose.prod.yml up -d --remove-orphans
|
||||
|
||||
# Wait for health checks
|
||||
sleep 10
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Verify containers are using correct images
|
||||
echo "=== Verifying container image tags ==="
|
||||
docker compose -f docker-compose.prod.yml images
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f
|
||||
# Cleanup stopped containers and old images
|
||||
docker container prune -f
|
||||
docker image prune -f
|
||||
DEPLOY_SCRIPT
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
name: Mac Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/mac_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "src/main/proto/**"
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_mac_app.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/mac/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/mac_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_mac_app.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/mac/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_notarization:
|
||||
description: 'Skip notarization (for testing)'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
mac-unity:
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
clean: false
|
||||
fetch-depth: 0 # For version numbering from git history
|
||||
|
||||
- name: Pull LFS files
|
||||
run: git lfs pull
|
||||
|
||||
- name: Restore Library/
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build Mac Unity
|
||||
run: ./ci/github_actions/build_unity_mac.sh "/tmp/eagle0/eagle0MAC"
|
||||
|
||||
- name: Persist Library/
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Inject Sparkle Framework
|
||||
if: success()
|
||||
env:
|
||||
SPARKLE_EDDSA_PUBLIC_KEY: ${{ secrets.SPARKLE_EDDSA_PUBLIC_KEY }}
|
||||
run: |
|
||||
chmod +x ./scripts/inject_sparkle.sh
|
||||
./scripts/inject_sparkle.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Import Code Signing Certificate
|
||||
if: success() && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
|
||||
run: |
|
||||
# Generate random keychain password (only used within this workflow run)
|
||||
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> $GITHUB_ENV
|
||||
|
||||
# Decode certificate
|
||||
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
|
||||
|
||||
# Create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain || true
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Import certificate
|
||||
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
|
||||
|
||||
# Allow codesign to access keychain
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Clean up
|
||||
rm certificate.p12
|
||||
|
||||
- name: Code Sign App
|
||||
if: success() && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
env:
|
||||
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
|
||||
run: |
|
||||
chmod +x ./scripts/codesign_mac_app.sh
|
||||
./scripts/codesign_mac_app.sh "/tmp/eagle0/eagle0MAC/eagle0.app" "ci/mac/eagle0.entitlements"
|
||||
|
||||
- name: Notarize App
|
||||
if: success() && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.event.inputs.skip_notarization != 'true'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
chmod +x ./scripts/notarize_mac_app.sh
|
||||
./scripts/notarize_mac_app.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Deploy Mac Build
|
||||
if: success() && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
SPARKLE_EDDSA_PRIVATE_KEY: ${{ secrets.SPARKLE_EDDSA_PRIVATE_KEY }}
|
||||
run: |
|
||||
# Write private key to temp file for signing
|
||||
SPARKLE_PRIVATE_KEY_PATH="/tmp/sparkle_private_key"
|
||||
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
VERSION=$(git describe --tags --always)
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD)
|
||||
|
||||
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
|
||||
"/tmp/eagle0/eagle0MAC/eagle0.app" \
|
||||
"$VERSION" \
|
||||
"$BUILD_NUMBER" \
|
||||
"$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
rm "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
- name: Cleanup Keychain
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain build.keychain 2>/dev/null || true
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: editor_mac.log
|
||||
path: /tmp/eagle0/editor_mac.log
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Mac History Editor Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/mac_history_build.yml"
|
||||
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/mac_history_build.yml"
|
||||
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
mac-history-build:
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
clean: false
|
||||
- name: Build the mac history
|
||||
run: ./ci/github_actions/build_mac_history.sh
|
||||
@@ -40,13 +40,15 @@ jobs:
|
||||
echo ""
|
||||
|
||||
# List of repositories to clean
|
||||
REPOS=$(doctl registry repository list-v2 --format Name --no-header)
|
||||
# Use tail to skip header row in case --no-header doesn't work
|
||||
REPOS=$(doctl registry repository list-v2 --format Name --no-header | grep -v '^Name$' | grep -v '^$')
|
||||
|
||||
for REPO in $REPOS; do
|
||||
echo "=== Processing repository: ${REPO} ==="
|
||||
|
||||
# Get all manifests with their tags and dates
|
||||
MANIFESTS=$(doctl registry repository list-manifests "${REPO}" --format Digest,UpdatedAt,Tags --no-header 2>/dev/null || echo "")
|
||||
# Filter out header row and empty lines
|
||||
MANIFESTS=$(doctl registry repository list-manifests "${REPO}" --format Digest,UpdatedAt,Tags --no-header 2>/dev/null | grep -v '^Digest' | grep -v '^$' || echo "")
|
||||
|
||||
if [ -z "$MANIFESTS" ]; then
|
||||
echo " No manifests found"
|
||||
@@ -54,8 +56,8 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "$MANIFESTS" | while read -r DIGEST UPDATED_AT TAGS; do
|
||||
# Skip if no digest
|
||||
if [ -z "$DIGEST" ]; then
|
||||
# Skip if no digest or if it doesn't look like a valid digest (sha256:...)
|
||||
if [ -z "$DIGEST" ] || ! echo "$DIGEST" | grep -q '^sha256:'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
|
||||
@@ -157,3 +157,65 @@ jobs:
|
||||
echo "=== Push complete ==="
|
||||
echo "Image: $IMAGE_TAG"
|
||||
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
|
||||
|
||||
deploy-hetzner:
|
||||
runs-on: self-hosted
|
||||
needs: [build-shardok-arm64]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
env:
|
||||
SHARDOK_IMAGE: ${{ needs.build-shardok-arm64.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.HETZNER_SSH_KEY }}" > ~/.ssh/hetzner_deploy
|
||||
chmod 600 ~/.ssh/hetzner_deploy
|
||||
# Add host key to known_hosts to avoid prompt
|
||||
ssh-keyscan -H ${{ secrets.HETZNER_IP }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Deploy to Hetzner
|
||||
run: |
|
||||
ssh -i ~/.ssh/hetzner_deploy -o StrictHostKeyChecking=accept-new deploy@${{ secrets.HETZNER_IP }} << 'ENDSSH'
|
||||
set -ex
|
||||
cd /opt/eagle0
|
||||
|
||||
# Login to registry
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
|
||||
|
||||
echo "Deploying Shardok ARM64: ${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Pull the new image
|
||||
docker pull "${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Stop and remove any container using port 40042 or named shardok*
|
||||
docker ps -q --filter "publish=40042" | xargs -r docker stop
|
||||
docker ps -aq --filter "name=shardok" | xargs -r docker rm -f
|
||||
docker ps -aq --filter "publish=40042" | xargs -r docker rm -f
|
||||
|
||||
# Run new container
|
||||
docker run -d \
|
||||
--name shardok-ai \
|
||||
--restart unless-stopped \
|
||||
-p 40042:40042 \
|
||||
-v /opt/eagle0/data:/data \
|
||||
-v /etc/shardok:/etc/shardok:ro \
|
||||
-v /etc/letsencrypt:/etc/letsencrypt:ro \
|
||||
-v /usr/local/share/eagle0:/usr/local/share/eagle0:ro \
|
||||
-e SHARDOK_RESOURCES_PATH=/app/resources \
|
||||
-e SHARDOK_MAPS_PATH=/app/resources/maps \
|
||||
"${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Wait and verify
|
||||
sleep 5
|
||||
docker ps | grep shardok-ai
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f
|
||||
|
||||
echo "=== Hetzner deployment complete ==="
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup SSH key
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/hetzner_deploy
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## CRITICAL GIT RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
|
||||
|
||||
**ALWAYS use this workflow:**
|
||||
1. Create a feature branch from origin/main
|
||||
2. Commit to that branch
|
||||
3. Create a PR with `gh pr create`
|
||||
4. Wait for user to merge
|
||||
|
||||
If you catch yourself about to run `git push origin main` or `git push origin <branch>:main`, STOP. You are about to violate a critical rule. Create a PR instead.
|
||||
|
||||
---
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
+14
-7
@@ -88,14 +88,14 @@ sysroot = use_repo_rule("@toolchains_llvm//toolchain:sysroot.bzl", "sysroot")
|
||||
sysroot(
|
||||
name = "linux_sysroot",
|
||||
sha256 = "a06475004fe8003ae7ccb4fe1d5511feb9b27cce4a8826eb1dfd686ed83f3dba",
|
||||
urls = ["https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/v3/ubuntu_noble_amd64_sysroot.tar.xz"],
|
||||
urls = ["https://eagle0-sysroot.sfo3.digitaloceanspaces.com/v3/ubuntu_noble_amd64_sysroot.tar.xz"],
|
||||
)
|
||||
|
||||
# ARM64 sysroot
|
||||
sysroot(
|
||||
name = "linux_sysroot_arm64",
|
||||
sha256 = "87469137737e09bc73855007dab835477eb10a7b3ce3f725f93f64e25747f3f9",
|
||||
urls = ["https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/v4/ubuntu_noble_arm64_sysroot.tar.xz"],
|
||||
urls = ["https://eagle0-sysroot.sfo3.digitaloceanspaces.com/v4/ubuntu_noble_arm64_sysroot.tar.xz"],
|
||||
)
|
||||
|
||||
#
|
||||
@@ -127,8 +127,8 @@ use_repo(
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "3.16.1", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "rules_swift", version = "2.3.1", repo_name = "build_bazel_rules_swift")
|
||||
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "rules_swift", version = "2.4.0", repo_name = "build_bazel_rules_swift")
|
||||
|
||||
#
|
||||
# Protocol Buffers & RPC
|
||||
@@ -294,11 +294,15 @@ http_archive(
|
||||
)
|
||||
|
||||
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
|
||||
# https://busybox.net/downloads/binaries/
|
||||
# Primary: GitHub release mirror (reliable)
|
||||
# Fallback: busybox.net (can be unreliable/slow)
|
||||
http_file(
|
||||
name = "busybox_x86_64",
|
||||
sha256 = "6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348",
|
||||
urls = ["https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"],
|
||||
urls = [
|
||||
"https://github.com/nolen777/eagle0/releases/download/busybox-1.35.0/busybox-1.35.0-x86_64-linux-musl",
|
||||
"https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox",
|
||||
],
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
)
|
||||
@@ -306,7 +310,10 @@ http_file(
|
||||
http_file(
|
||||
name = "busybox_aarch64",
|
||||
sha256 = "141adb1b625a6f44c4b114f76b4387b4ea4f7ab802b88eb40e0d2f6adcccb1c3",
|
||||
urls = ["https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox"],
|
||||
urls = [
|
||||
# TODO: Upload aarch64 binary to GitHub release when needed
|
||||
"https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox",
|
||||
],
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
)
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
. ./ci/unity_version.sh
|
||||
|
||||
WORKSPACE=$(pwd)
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
BUILD_DIR=$1
|
||||
LOG_PATH=$2
|
||||
|
||||
echo "Building Mac in $BUILD_DIR"
|
||||
|
||||
echo "Cleaning up $BUILD_DIR"
|
||||
/bin/rm -rf "$BUILD_DIR"
|
||||
/bin/mkdir -p "$BUILD_DIR"
|
||||
|
||||
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-nographics \
|
||||
-batchmode \
|
||||
-quit \
|
||||
-buildOSXUniversalPlayer "$BUILD_DIR/eagle0.app" \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
COMMIT=$(/usr/bin/git rev-parse --short HEAD)
|
||||
|
||||
/bin/echo "build protos"
|
||||
./scripts/build_protos.sh
|
||||
|
||||
/bin/echo "build Mac plugin"
|
||||
./scripts/build_mac_plugin.sh
|
||||
|
||||
git log -3
|
||||
|
||||
/bin/echo "build Mac"
|
||||
LOG_PATH="/tmp/eagle0/editor_mac.log"
|
||||
BUILD_DIR=$1
|
||||
|
||||
./ci/github_actions/build_mac.sh "$BUILD_DIR" "$LOG_PATH"
|
||||
@@ -0,0 +1,18 @@
|
||||
<?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>
|
||||
<!-- Allow JIT compilation (required for Mono/IL2CPP) -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<!-- Allow unsigned executable memory (required for Unity) -->
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<!-- Disable library validation (required for plugins) -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<!-- Allow outgoing network connections -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,40 @@
|
||||
# Environment template for production deployment
|
||||
# This file defines all env vars used by docker-compose.prod.yml
|
||||
# Workflows should update their specific vars without overwriting others
|
||||
|
||||
# Container images (managed by respective build workflows)
|
||||
EAGLE_IMAGE=registry.digitalocean.com/eagle0/eagle-server:latest
|
||||
SHARDOK_IMAGE=registry.digitalocean.com/eagle0/shardok-server:latest
|
||||
ADMIN_IMAGE=registry.digitalocean.com/eagle0/admin-server:latest
|
||||
JFR_SIDECAR_IMAGE=registry.digitalocean.com/eagle0/jfr-sidecar:latest
|
||||
AUTH_IMAGE=registry.digitalocean.com/eagle0/auth-server:latest
|
||||
|
||||
# OpenAI / LLM
|
||||
OPENAI_API_KEY=
|
||||
GPT_MODEL_NAME=gpt-4o
|
||||
|
||||
# DigitalOcean Spaces (S3-compatible storage)
|
||||
EAGLE_ENABLE_S3=false
|
||||
DO_SPACES_ACCESS_KEY=
|
||||
DO_SPACES_SECRET_KEY=
|
||||
|
||||
# JWT authentication
|
||||
JWT_PRIVATE_KEY=
|
||||
|
||||
# OAuth providers
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Shardok connection
|
||||
SHARDOK_ADDRESS=shardok:40042
|
||||
SHARDOK_AUTH_TOKEN=
|
||||
|
||||
# Monitoring
|
||||
SENTRY_DSN=
|
||||
|
||||
# Email (Fastmail JMAP)
|
||||
FASTMAIL_API_TOKEN=
|
||||
FASTMAIL_FROM_EMAIL=
|
||||
FASTMAIL_FROM_NAME=
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# Update .env file without losing other variables
|
||||
# Usage: ./update-env.sh KEY1=value1 KEY2=value2 ...
|
||||
#
|
||||
# This script:
|
||||
# 1. Creates .env from template if it doesn't exist
|
||||
# 2. Updates only the specified KEY=value pairs
|
||||
# 3. Preserves all other existing values
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="${ENV_FILE:-/opt/eagle0/.env}"
|
||||
TEMPLATE_FILE="${TEMPLATE_FILE:-$SCRIPT_DIR/env.template}"
|
||||
|
||||
# Create .env from template if it doesn't exist
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$TEMPLATE_FILE" ]; then
|
||||
echo "Creating .env from template..."
|
||||
grep -v '^#' "$TEMPLATE_FILE" | grep -v '^$' > "$ENV_FILE"
|
||||
else
|
||||
echo "Creating empty .env..."
|
||||
touch "$ENV_FILE"
|
||||
fi
|
||||
chmod 600 "$ENV_FILE"
|
||||
fi
|
||||
|
||||
# Process each KEY=VALUE argument
|
||||
for arg in "$@"; do
|
||||
# Skip empty args
|
||||
[ -z "$arg" ] && continue
|
||||
|
||||
# Parse KEY=VALUE
|
||||
KEY="${arg%%=*}"
|
||||
VALUE="${arg#*=}"
|
||||
|
||||
# Skip if no key
|
||||
[ -z "$KEY" ] && continue
|
||||
|
||||
# Skip setting empty values (keeps existing value)
|
||||
if [ -z "$VALUE" ]; then
|
||||
echo "Skipping $KEY (empty value)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Remove existing line for this key and add new one
|
||||
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
|
||||
# Key exists, update it
|
||||
sed -i "s|^${KEY}=.*|${KEY}=${VALUE}|" "$ENV_FILE"
|
||||
echo "Updated $KEY"
|
||||
else
|
||||
# Key doesn't exist, add it
|
||||
echo "${KEY}=${VALUE}" >> "$ENV_FILE"
|
||||
echo "Added $KEY"
|
||||
fi
|
||||
done
|
||||
|
||||
chmod 600 "$ENV_FILE"
|
||||
echo "Done updating $ENV_FILE"
|
||||
+68
-8
@@ -8,9 +8,13 @@
|
||||
# Run: docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
services:
|
||||
eagle:
|
||||
# Blue-green deployment: eagle-blue is the primary (production) instance
|
||||
# eagle-green is the staging instance for zero-downtime deployments
|
||||
# See scripts/deploy-blue-green.sh for deployment workflow
|
||||
|
||||
eagle-blue:
|
||||
image: ${EAGLE_IMAGE:-registry.digitalocean.com/eagle0/eagle-server:latest}
|
||||
container_name: eagle-server
|
||||
container_name: eagle-blue
|
||||
command:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
@@ -39,7 +43,7 @@ services:
|
||||
volumes:
|
||||
- ./saves:/app/saves # Game saves and user database
|
||||
- ./archived:/app/archived # Archived completed games
|
||||
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-server jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
|
||||
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-blue jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
|
||||
- jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
|
||||
depends_on:
|
||||
@@ -58,6 +62,58 @@ services:
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
eagle-green:
|
||||
image: ${EAGLE_IMAGE_NEW:-registry.digitalocean.com/eagle0/eagle-server:latest}
|
||||
container_name: eagle-green
|
||||
profiles: ["blue-green"] # Only started during blue-green deployment
|
||||
command:
|
||||
- "--gpt-model-name"
|
||||
- "${GPT_MODEL_NAME:-gpt-5.1}"
|
||||
- "--shardok-interface-remote-address"
|
||||
- "${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
- "--auth-service-url"
|
||||
- "auth:40033"
|
||||
ports:
|
||||
- "40034:40032" # Different host port for staging
|
||||
environment:
|
||||
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
|
||||
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
|
||||
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
|
||||
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
EAGLE_SAVE_DIR: "/app/saves"
|
||||
EAGLE_ARCHIVE_DIR: "/app/archived"
|
||||
SENTRY_DSN: "${SENTRY_DSN:-}"
|
||||
SENTRY_ENVIRONMENT: "production"
|
||||
volumes:
|
||||
- ./saves:/app/saves # Same save directory as blue
|
||||
- ./archived:/app/archived # Same archive directory as blue
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
|
||||
depends_on:
|
||||
- shardok
|
||||
- auth
|
||||
restart: "no" # Don't auto-restart during deployment
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "nc -z localhost 40032 || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
start_period: 60s
|
||||
|
||||
# Backward compatibility alias - for scripts that reference 'eagle' service
|
||||
eagle:
|
||||
extends:
|
||||
service: eagle-blue
|
||||
|
||||
auth:
|
||||
image: ${AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest}
|
||||
container_name: auth-server
|
||||
@@ -80,6 +136,10 @@ services:
|
||||
# JWT keys - PEM files in volume, bootstrapped from JWK on first run
|
||||
JWT_KEYS_PATH: "/etc/eagle0/keys"
|
||||
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
|
||||
# Fastmail JMAP API for sending invitation emails
|
||||
FASTMAIL_API_TOKEN: "${FASTMAIL_API_TOKEN:-}"
|
||||
FASTMAIL_FROM_EMAIL: "${FASTMAIL_FROM_EMAIL:-}"
|
||||
FASTMAIL_FROM_NAME: "${FASTMAIL_FROM_NAME:-}"
|
||||
# Note: port 40033 is exposed via nginx, not directly
|
||||
volumes:
|
||||
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with Eagle
|
||||
@@ -136,7 +196,7 @@ services:
|
||||
- ./certbot/www:/var/www/certbot:ro
|
||||
- ./auth:/etc/nginx/auth:ro
|
||||
depends_on:
|
||||
- eagle
|
||||
- eagle-blue
|
||||
- admin
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
@@ -150,7 +210,7 @@ services:
|
||||
container_name: admin-server
|
||||
command:
|
||||
- "--eagle-addr"
|
||||
- "eagle:40032"
|
||||
- "${EAGLE_ADDR:-eagle-blue:40032}" # Can be switched for blue-green
|
||||
- "--auth-addr"
|
||||
- "auth:40033"
|
||||
- "--jfr-sidecar-addr"
|
||||
@@ -159,7 +219,7 @@ services:
|
||||
- "8080"
|
||||
# No external port - accessed via nginx at admin.eagle0.net
|
||||
depends_on:
|
||||
- eagle
|
||||
- eagle-blue
|
||||
- auth
|
||||
- jfr-sidecar
|
||||
restart: unless-stopped
|
||||
@@ -179,11 +239,11 @@ services:
|
||||
image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest}
|
||||
container_name: jfr-sidecar
|
||||
# Share PID namespace with Eagle to access its JVM via jcmd
|
||||
pid: "service:eagle"
|
||||
pid: "service:eagle-blue"
|
||||
volumes:
|
||||
- jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files
|
||||
depends_on:
|
||||
- eagle
|
||||
- eagle-blue
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
|
||||
+32
-1
@@ -231,6 +231,21 @@ All action files have been migrated to use Scala types:
|
||||
- ✅ `CommandChoiceHelpers` migrated to Scala types
|
||||
- ✅ `ResolveBattleAction` refactored to use Scala GameState and ActionResultApplier (PR #5048)
|
||||
|
||||
### Enum Type Migrations
|
||||
|
||||
Proto enums are being converted to Scala sealed traits with converters at boundaries:
|
||||
|
||||
| Enum | Scala Type | Status | Notes |
|
||||
|------|------------|--------|-------|
|
||||
| `DiplomacyOfferStatus` | `Status` sealed trait | ✅ **Complete** | PR #5093 - `EligibleDiplomacyStatuses` uses Scala types internally |
|
||||
| `RoundPhase` | `RoundPhase` sealed trait | Partial | Some usages converted |
|
||||
| `BattalionType` | `BattalionType` sealed trait | Partial | Some usages converted |
|
||||
|
||||
**DiplomacyOfferStatus Migration (PR #5093):**
|
||||
- `EligibleDiplomacyStatuses.scala` now returns `Vector[Status]` instead of `Vector[DiplomacyOfferStatus]`
|
||||
- Call sites in `AvailableResolve*CommandFactory` files convert to proto via `StatusConverter.toProto` at the boundary
|
||||
- This pattern should be applied to other proto enums
|
||||
|
||||
### CommandChoiceHelpers Migration Status
|
||||
|
||||
Several command selectors have already been converted to use Scala types:
|
||||
@@ -257,7 +272,23 @@ Several command selectors have already been converted to use Scala types:
|
||||
|--------|-------|
|
||||
| Action files fully protoless | 52 / 52 (100%) ✅ |
|
||||
| Proto usages in remaining actions | 0 |
|
||||
| Next target | History APIs (InMemoryHistory, PersistedHistory) |
|
||||
| Next target | See "Next Candidates" section below |
|
||||
|
||||
### Next Candidates
|
||||
|
||||
Priority candidates for further deproto work:
|
||||
|
||||
1. **More Enum Migrations** - Apply the `DiplomacyOfferStatus` pattern to other proto enums:
|
||||
- Files importing `net.eagle0.eagle.common.round_phase.RoundPhase` (proto) could use Scala `RoundPhase`
|
||||
- Files importing `net.eagle0.eagle.common.battalion_type.BattalionType` (proto) could use Scala `BattalionType`
|
||||
|
||||
2. **AvailableCommandsFactory Files** - Many still use proto `GameState` internally:
|
||||
- These files build proto `AvailableCommand` messages but could use Scala types for internal logic
|
||||
- Convert to accept Scala `GameState`, only convert fields to proto when building the response
|
||||
|
||||
3. **History APIs** - `InMemoryHistory` and `PersistedHistory`:
|
||||
- Change to vend Scala `GameState` and `ActionResultT` instead of proto versions
|
||||
- `PersistedHistory` converts to proto internally for disk persistence
|
||||
|
||||
### Validation
|
||||
- [x] `ActionResultApplier` created and tested
|
||||
|
||||
+10
-1
@@ -25,8 +25,10 @@ http {
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
# Upstream for Eagle gRPC server
|
||||
# For blue-green deployments, this is switched between eagle-blue:40032 and eagle-green:40032
|
||||
# by scripts/deploy-blue-green.sh, then nginx is reloaded with 'nginx -s reload'
|
||||
upstream eagle_grpc {
|
||||
server eagle:40032;
|
||||
server eagle-blue:40032;
|
||||
keepalive 100;
|
||||
}
|
||||
|
||||
@@ -106,6 +108,13 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Invitation landing page (proxied to Go auth service)
|
||||
location /invite/ {
|
||||
proxy_pass http://auth:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Code sign a macOS .app bundle for distribution
|
||||
# Usage: codesign_mac_app.sh <app_path> [entitlements_path]
|
||||
#
|
||||
# Environment variables:
|
||||
# SIGNING_IDENTITY - The signing identity (default: "Developer ID Application")
|
||||
# KEYCHAIN_PASSWORD - Password to unlock the build keychain (optional)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
APP_PATH="$1"
|
||||
ENTITLEMENTS_PATH="${2:-}"
|
||||
SIGNING_IDENTITY="${SIGNING_IDENTITY:-Developer ID Application}"
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "ERROR: App not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Unlock keychain if password provided
|
||||
if [ -n "${KEYCHAIN_PASSWORD:-}" ]; then
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain || true
|
||||
fi
|
||||
|
||||
echo "=== Signing nested components first ==="
|
||||
|
||||
# Sign all dylibs
|
||||
find "$APP_PATH" -name "*.dylib" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing dylib: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign all bundles (plugins)
|
||||
find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing bundle: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign XPC services (inside Sparkle framework)
|
||||
find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing XPC service: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign nested apps (like Sparkle's Updater.app)
|
||||
find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing nested app: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign standalone executables inside frameworks (like Autoupdate)
|
||||
find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing executable: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
# Sign all frameworks (after their contents are signed)
|
||||
find "$APP_PATH" -name "*.framework" -print0 | while IFS= read -r -d '' item; do
|
||||
echo "Signing framework: $item"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$item"
|
||||
done
|
||||
|
||||
echo "=== Signing main app bundle ==="
|
||||
|
||||
if [ -n "$ENTITLEMENTS_PATH" ] && [ -f "$ENTITLEMENTS_PATH" ]; then
|
||||
echo "Using entitlements: $ENTITLEMENTS_PATH"
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--entitlements "$ENTITLEMENTS_PATH" \
|
||||
--sign "$SIGNING_IDENTITY" "$APP_PATH"
|
||||
else
|
||||
codesign --force --verify --verbose --timestamp --options runtime \
|
||||
--sign "$SIGNING_IDENTITY" "$APP_PATH"
|
||||
fi
|
||||
|
||||
echo "=== Verifying signature ==="
|
||||
codesign --verify --verbose=4 "$APP_PATH"
|
||||
|
||||
echo "=== Checking Gatekeeper assessment ==="
|
||||
spctl --assess --type exec -v "$APP_PATH" || echo "Note: Gatekeeper may reject until notarized"
|
||||
|
||||
echo "Code signing complete: $APP_PATH"
|
||||
Executable
+247
@@ -0,0 +1,247 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Blue-Green Deployment Script for Eagle Server
|
||||
#
|
||||
# This script performs a zero-downtime deployment by:
|
||||
# 1. Starting the new version on a staging port (green)
|
||||
# 2. Waiting for it to become healthy
|
||||
# 3. Running warmup traffic to pre-heat the JIT
|
||||
# 4. Stopping the old version (blue) - which flushes state to disk
|
||||
# 5. Telling green to reload games from disk
|
||||
# 6. Switching nginx to route traffic to green
|
||||
# 7. Cleaning up
|
||||
#
|
||||
# Usage: ./deploy-blue-green.sh [NEW_IMAGE_TAG]
|
||||
#
|
||||
# Example:
|
||||
# ./deploy-blue-green.sh latest
|
||||
# ./deploy-blue-green.sh sha-abc123
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_DIR="${APP_DIR:-/opt/eagle0}"
|
||||
NGINX_CONF="${APP_DIR}/nginx/nginx.conf"
|
||||
COMPOSE_FILE="${APP_DIR}/docker-compose.prod.yml"
|
||||
WARMUP_SCRIPT="${SCRIPT_DIR}/warmup-eagle.sh"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Determine which instance is currently active
|
||||
get_active_instance() {
|
||||
if grep -q "eagle-blue:40032" "${NGINX_CONF}"; then
|
||||
echo "blue"
|
||||
elif grep -q "eagle-green:40032" "${NGINX_CONF}"; then
|
||||
echo "green"
|
||||
else
|
||||
log_error "Cannot determine active instance from nginx config"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Pull image with retry using crane (handles OCI/Docker digest mismatch)
|
||||
pull_with_retry() {
|
||||
local image=$1
|
||||
local max_attempts=${2:-3}
|
||||
local attempt=1
|
||||
|
||||
# Use crane if available (handles OCI format correctly)
|
||||
if [ -x "${APP_DIR}/crane" ]; then
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
log_info "Pulling image with crane (attempt ${attempt}/${max_attempts})..."
|
||||
if "${APP_DIR}/crane" pull "${image}" /tmp/image.tar && docker load -i /tmp/image.tar; then
|
||||
rm -f /tmp/image.tar
|
||||
log_info "Image pulled and loaded successfully"
|
||||
return 0
|
||||
fi
|
||||
rm -f /tmp/image.tar
|
||||
log_warn "Pull failed, retrying in 5 seconds..."
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
else
|
||||
# Fallback to docker pull if crane not available
|
||||
log_warn "crane not found at ${APP_DIR}/crane, falling back to docker pull"
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
log_info "Pulling image (attempt ${attempt}/${max_attempts})..."
|
||||
if docker pull "${image}"; then
|
||||
log_info "Image pulled successfully"
|
||||
return 0
|
||||
fi
|
||||
log_warn "Pull failed, retrying in 5 seconds..."
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
fi
|
||||
log_error "Failed to pull image after ${max_attempts} attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Wait for a container to be healthy
|
||||
wait_for_healthy() {
|
||||
local container=$1
|
||||
local max_attempts=${2:-60}
|
||||
local attempt=1
|
||||
|
||||
log_info "Waiting for ${container} to become healthy..."
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "unknown")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
log_info "${container} is healthy"
|
||||
return 0
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
echo ""
|
||||
log_error "${container} did not become healthy after $((max_attempts * 2)) seconds"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main deployment logic
|
||||
main() {
|
||||
local new_tag="${1:-latest}"
|
||||
local registry="registry.digitalocean.com/eagle0/eagle-server"
|
||||
local new_image="${registry}:${new_tag}"
|
||||
|
||||
log_info "Starting blue-green deployment"
|
||||
log_info "New image: ${new_image}"
|
||||
|
||||
cd "${APP_DIR}"
|
||||
|
||||
# Determine current active instance
|
||||
local active=$(get_active_instance)
|
||||
local staging
|
||||
if [ "$active" = "blue" ]; then
|
||||
staging="green"
|
||||
else
|
||||
staging="blue"
|
||||
fi
|
||||
|
||||
log_info "Active instance: eagle-${active}"
|
||||
log_info "Staging instance: eagle-${staging}"
|
||||
|
||||
# Pull the new image (with retry for intermittent registry issues)
|
||||
if ! pull_with_retry "${new_image}" 3; then
|
||||
log_error "Failed to pull new image, aborting deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start staging instance with new image
|
||||
log_info "Starting eagle-${staging} with new image..."
|
||||
if [ "$staging" = "green" ]; then
|
||||
EAGLE_IMAGE_NEW="${new_image}" docker compose -f "${COMPOSE_FILE}" --profile blue-green up -d eagle-green
|
||||
else
|
||||
EAGLE_IMAGE="${new_image}" docker compose -f "${COMPOSE_FILE}" up -d eagle-blue
|
||||
fi
|
||||
|
||||
# Wait for staging to be healthy
|
||||
if ! wait_for_healthy "eagle-${staging}" 90; then
|
||||
log_error "Staging instance failed health check, aborting deployment"
|
||||
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
|
||||
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${staging}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run warmup/smoke test
|
||||
local staging_port
|
||||
if [ "$staging" = "green" ]; then
|
||||
staging_port=40034
|
||||
else
|
||||
staging_port=40032
|
||||
fi
|
||||
|
||||
log_info "Running warmup against eagle-${staging}..."
|
||||
if [ -x "${WARMUP_SCRIPT}" ]; then
|
||||
if ! "${WARMUP_SCRIPT}" "localhost:${staging_port}"; then
|
||||
log_error "Warmup/smoke test failed, aborting deployment"
|
||||
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
|
||||
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${staging}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_warn "Warmup script not found at ${WARMUP_SCRIPT}, skipping warmup"
|
||||
log_warn "JIT will be cold on first requests"
|
||||
fi
|
||||
|
||||
# Stop the active instance (this flushes state to disk)
|
||||
log_info "Stopping eagle-${active} (flushing state to disk)..."
|
||||
docker compose -f "${COMPOSE_FILE}" stop "eagle-${active}"
|
||||
|
||||
# Tell staging to reload games from disk
|
||||
log_info "Telling eagle-${staging} to reload games from disk..."
|
||||
if command -v grpcurl &> /dev/null; then
|
||||
grpcurl -plaintext -d '{}' "localhost:${staging_port}" net.eagle0.eagle.api.Eagle/ReloadGames || true
|
||||
else
|
||||
log_warn "grpcurl not installed, skipping game reload"
|
||||
log_warn "New instance will use games loaded at startup"
|
||||
fi
|
||||
|
||||
# Switch nginx upstream
|
||||
log_info "Switching nginx upstream to eagle-${staging}..."
|
||||
if [ "$staging" = "green" ]; then
|
||||
sed -i.bak 's/eagle-blue:40032/eagle-green:40032/g' "${NGINX_CONF}"
|
||||
else
|
||||
sed -i.bak 's/eagle-green:40032/eagle-blue:40032/g' "${NGINX_CONF}"
|
||||
fi
|
||||
|
||||
# Reload nginx
|
||||
log_info "Reloading nginx..."
|
||||
docker compose -f "${COMPOSE_FILE}" exec nginx nginx -s reload
|
||||
|
||||
# Clean up old instance
|
||||
log_info "Removing old eagle-${active} container..."
|
||||
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${active}"
|
||||
|
||||
# Update the staging instance's restart policy and image var
|
||||
# For blue, we need to update EAGLE_IMAGE; for green, update EAGLE_IMAGE_NEW
|
||||
if [ "$staging" = "blue" ]; then
|
||||
log_info "Updating EAGLE_IMAGE to ${new_image} for future restarts"
|
||||
# User should update their .env file
|
||||
else
|
||||
log_info "Green is now active. Consider switching to blue on next deployment."
|
||||
fi
|
||||
|
||||
log_info "Deployment complete!"
|
||||
log_info "Active instance: eagle-${staging}"
|
||||
log_info ""
|
||||
log_info "Note: Update your .env file with EAGLE_IMAGE=${new_image}"
|
||||
log_info " if you want future 'docker compose up' to use this version."
|
||||
}
|
||||
|
||||
# Check for required tools
|
||||
check_requirements() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "docker is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v sed &> /dev/null; then
|
||||
log_error "sed is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${NGINX_CONF}" ]; then
|
||||
log_error "nginx config not found at ${NGINX_CONF}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${COMPOSE_FILE}" ]; then
|
||||
log_error "docker-compose file not found at ${COMPOSE_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run
|
||||
check_requirements
|
||||
main "$@"
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Inject Sparkle framework into a macOS .app bundle for auto-updates
|
||||
# Usage: inject_sparkle.sh <app_path>
|
||||
#
|
||||
# Environment variables (required):
|
||||
# SPARKLE_EDDSA_PUBLIC_KEY - EdDSA public key for verifying updates
|
||||
#
|
||||
# Optional environment variables:
|
||||
# SPARKLE_FEED_URL - Appcast URL (default: https://assets.eagle0.net/mac/appcast.xml)
|
||||
# SPARKLE_VERSION - Sparkle version to use (default: 2.6.4)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
APP_PATH="$1"
|
||||
SPARKLE_VERSION="${SPARKLE_VERSION:-2.6.4}"
|
||||
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://assets.eagle0.net/mac/appcast.xml}"
|
||||
SPARKLE_CACHE_DIR="/tmp/sparkle-cache"
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "ERROR: App not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
|
||||
echo "ERROR: SPARKLE_EDDSA_PUBLIC_KEY environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download Sparkle if not cached
|
||||
SPARKLE_DIR="$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION"
|
||||
if [ ! -d "$SPARKLE_DIR/Sparkle.framework" ]; then
|
||||
echo "=== Downloading Sparkle $SPARKLE_VERSION ==="
|
||||
mkdir -p "$SPARKLE_CACHE_DIR"
|
||||
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
|
||||
curl -L "$SPARKLE_URL" | tar -xJ -C "$SPARKLE_CACHE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION" "$SPARKLE_DIR" 2>/dev/null || true
|
||||
# If the extracted directory doesn't match version pattern, it may just be "Sparkle"
|
||||
if [ ! -d "$SPARKLE_DIR" ]; then
|
||||
mkdir -p "$SPARKLE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle.framework" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
mv "$SPARKLE_CACHE_DIR/bin" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Injecting Sparkle framework ==="
|
||||
FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
|
||||
mkdir -p "$FRAMEWORKS_DIR"
|
||||
|
||||
# Copy Sparkle framework
|
||||
cp -R "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/"
|
||||
|
||||
# Also copy the XPC services if present
|
||||
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
|
||||
echo "Sparkle XPC services present"
|
||||
fi
|
||||
|
||||
echo "=== Updating Info.plist ==="
|
||||
PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
||||
|
||||
# Add Sparkle configuration to Info.plist
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUFeedURL string '$SPARKLE_FEED_URL'" "$PLIST_PATH"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string '$SPARKLE_EDDSA_PUBLIC_KEY'" "$PLIST_PATH"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUEnableAutomaticChecks bool true" "$PLIST_PATH"
|
||||
|
||||
# Set bundle version from git for Sparkle version comparison
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0")
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
|
||||
|
||||
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string '$VERSION'" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$PLIST_PATH" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleVersion string '$BUILD_NUMBER'" "$PLIST_PATH"
|
||||
|
||||
# Add URL scheme for invitation codes (eagle0://invite?code=XXXX)
|
||||
echo "=== Adding URL scheme for invitation codes ==="
|
||||
/usr/libexec/PlistBuddy -c "Delete :CFBundleURLTypes" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0 dict" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string 'com.Shardok-Games.eagle0'" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string 'eagle0'" "$PLIST_PATH"
|
||||
|
||||
echo "=== Sparkle injection complete ==="
|
||||
echo "App: $APP_PATH"
|
||||
echo "Feed URL: $SPARKLE_FEED_URL"
|
||||
echo "Version: $VERSION (build $BUILD_NUMBER)"
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Notarize a macOS .app bundle with Apple
|
||||
# Usage: notarize_mac_app.sh <app_path>
|
||||
#
|
||||
# Environment variables (required):
|
||||
# APPLE_ID - Apple Developer account email
|
||||
# APP_SPECIFIC_PASSWORD - App-specific password for notarytool
|
||||
# TEAM_ID - Apple Developer Team ID
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
APP_PATH="$1"
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "ERROR: App not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${APPLE_ID:-}" ] || [ -z "${APP_SPECIFIC_PASSWORD:-}" ] || [ -z "${TEAM_ID:-}" ]; then
|
||||
echo "ERROR: Required environment variables not set"
|
||||
echo " APPLE_ID: ${APPLE_ID:-<not set>}"
|
||||
echo " APP_SPECIFIC_PASSWORD: ${APP_SPECIFIC_PASSWORD:+<set>}"
|
||||
echo " TEAM_ID: ${TEAM_ID:-<not set>}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create ZIP for notarization submission
|
||||
ZIP_PATH="${APP_PATH%.app}.zip"
|
||||
echo "=== Creating ZIP for notarization: $ZIP_PATH ==="
|
||||
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
|
||||
|
||||
echo "=== Submitting to Apple for notarization ==="
|
||||
xcrun notarytool submit "$ZIP_PATH" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APP_SPECIFIC_PASSWORD" \
|
||||
--team-id "$TEAM_ID" \
|
||||
--wait
|
||||
|
||||
# Clean up the zip
|
||||
rm "$ZIP_PATH"
|
||||
|
||||
echo "=== Stapling notarization ticket to app ==="
|
||||
xcrun stapler staple "$APP_PATH"
|
||||
|
||||
echo "=== Verifying notarization ==="
|
||||
xcrun stapler validate "$APP_PATH"
|
||||
spctl --assess --type exec -v "$APP_PATH"
|
||||
|
||||
echo "Notarization complete: $APP_PATH"
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Warmup Script for Eagle Server
|
||||
#
|
||||
# This script warms up the JIT compiler before switching traffic to a new instance.
|
||||
# It uses the Go warmup tool which:
|
||||
# 1. Creates a test game via bidirectional streaming
|
||||
# 2. Posts an Improve command
|
||||
# 3. Verifies action results and new commands
|
||||
# 4. Cleans up the test game
|
||||
#
|
||||
# Usage: ./warmup-eagle.sh HOST:PORT
|
||||
#
|
||||
# Example:
|
||||
# ./warmup-eagle.sh localhost:40032
|
||||
# ./warmup-eagle.sh localhost:40034
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
HOST="${1:-localhost:40032}"
|
||||
|
||||
log_info "Warming up Eagle server at ${HOST}..."
|
||||
|
||||
# Try to find the Go warmup tool
|
||||
WARMUP_TOOL=""
|
||||
|
||||
# Check if we're in the project directory with bazel
|
||||
if [ -f "${PROJECT_ROOT}/WORKSPACE" ] || [ -f "${PROJECT_ROOT}/WORKSPACE.bazel" ]; then
|
||||
# Try to find the pre-built binary
|
||||
BAZEL_BIN="${PROJECT_ROOT}/bazel-bin/src/main/go/net/eagle0/warmup/warmup_/warmup"
|
||||
if [ -x "${BAZEL_BIN}" ]; then
|
||||
WARMUP_TOOL="${BAZEL_BIN}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for the warmup tool in common locations (for deployed environments)
|
||||
if [ -z "${WARMUP_TOOL}" ]; then
|
||||
for path in \
|
||||
"${SCRIPT_DIR}/bin/warmup" \
|
||||
"/opt/eagle0/scripts/bin/warmup" \
|
||||
"/opt/eagle0/bin/warmup" \
|
||||
"/usr/local/bin/eagle-warmup" \
|
||||
"${SCRIPT_DIR}/warmup"; do
|
||||
if [ -x "${path}" ]; then
|
||||
WARMUP_TOOL="${path}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# If we found the Go tool, use it
|
||||
if [ -n "${WARMUP_TOOL}" ]; then
|
||||
log_info "Using Go warmup tool: ${WARMUP_TOOL}"
|
||||
if "${WARMUP_TOOL}" --address="${HOST}" --timeout=60s; then
|
||||
log_info "Warmup complete!"
|
||||
exit 0
|
||||
else
|
||||
log_error "Go warmup tool failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback to grpcurl-based warmup
|
||||
log_warn "Go warmup tool not found, falling back to grpcurl"
|
||||
|
||||
# Check for grpcurl
|
||||
if ! command -v grpcurl &> /dev/null; then
|
||||
log_error "Neither Go warmup tool nor grpcurl is available"
|
||||
log_error "Build the warmup tool with: bazel build //src/main/go/net/eagle0/warmup"
|
||||
log_error "Or install grpcurl: brew install grpcurl (macOS)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Warmup iterations
|
||||
WARMUP_ITERATIONS=3
|
||||
|
||||
# 1. Call GetRunningGames multiple times - this exercises the gRPC layer and basic game access
|
||||
log_info "Warming up GetRunningGames..."
|
||||
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
|
||||
RESULT=$(grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetRunningGames 2>&1) || true
|
||||
if echo "$RESULT" | grep -q "games\|{}"; then
|
||||
echo -n "."
|
||||
else
|
||||
log_error "GetRunningGames failed on iteration $i"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo " done"
|
||||
|
||||
# 2. Call GetSettings - exercises settings loading
|
||||
log_info "Warming up GetSettings..."
|
||||
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
|
||||
if grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetSettings > /dev/null 2>&1; then
|
||||
echo -n "."
|
||||
else
|
||||
log_warn "GetSettings failed on iteration $i (non-fatal)"
|
||||
fi
|
||||
done
|
||||
echo " done"
|
||||
|
||||
# 3. Call AddSettings with empty list - exercises settings path
|
||||
log_info "Warming up AddSettings..."
|
||||
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
|
||||
if grpcurl -plaintext -d '{"settings": []}' "${HOST}" net.eagle0.eagle.api.Eagle/AddSettings > /dev/null 2>&1; then
|
||||
echo -n "."
|
||||
else
|
||||
log_warn "AddSettings failed on iteration $i (non-fatal)"
|
||||
fi
|
||||
done
|
||||
echo " done"
|
||||
|
||||
# Final health check
|
||||
log_info "Verifying server health..."
|
||||
if grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetRunningGames > /dev/null 2>&1; then
|
||||
log_info "Health check passed"
|
||||
else
|
||||
log_error "Health check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info ""
|
||||
log_info "Warmup complete (basic mode - bidirectional streaming warmup not available)!"
|
||||
log_info "The JIT should be warmed for:"
|
||||
log_info " - gRPC layer and protobuf parsing"
|
||||
log_info " - Settings loading and management"
|
||||
log_info ""
|
||||
log_warn "Note: For full warmup including game creation and command processing,"
|
||||
log_warn " build and use the Go warmup tool: bazel build //src/main/go/net/eagle0/warmup"
|
||||
@@ -107,6 +107,23 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
|
||||
alCache,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost));
|
||||
} else if (!defenderPositions.empty()) {
|
||||
// Defenders exist but none are on castles - they're scattering/fleeing.
|
||||
// Chase them down rather than holding empty castles, since eliminating
|
||||
// all defenders also wins the battle via LAST_PLAYER_STANDING.
|
||||
chosenStrategy = AttackUnitsStrategy(GenerateTargetPriorities(
|
||||
Occupants(
|
||||
*gameState->units(),
|
||||
gameState->hex_map()->row_count(),
|
||||
gameState->hex_map()->column_count()),
|
||||
gameState->hex_map(),
|
||||
defenderPositions,
|
||||
attackerPid,
|
||||
attackerUnits,
|
||||
apdCache,
|
||||
alCache,
|
||||
battalionTypeGetter,
|
||||
braveWaterCost));
|
||||
} else {
|
||||
chosenStrategy = HoldCastlesStrategy;
|
||||
}
|
||||
|
||||
@@ -222,7 +222,6 @@ double AIHeuristicWeighting::GetCommandWeight(
|
||||
case CommandType::RELEASE_UNIT_COMMAND: return 0.0;
|
||||
|
||||
case CommandType::REINFORCE_COMMAND: return 10.0;
|
||||
case CommandType::MANAGE_PRISONER: return 1.0;
|
||||
|
||||
// === ZERO WEIGHT - NEVER SELECT (0.0) ===
|
||||
// Explicitly bad actions
|
||||
|
||||
@@ -85,7 +85,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
|
||||
e->GetCurrentGameState()->hex_map(),
|
||||
e->GetGameSettings()->GetGetter(),
|
||||
AIAlgorithmType::ITERATIVE_DEEPENING,
|
||||
ScoringCalculatorType::MCTS_OPTIMIZED,
|
||||
ScoringCalculatorType::STANDARD,
|
||||
mctsConfig);
|
||||
|
||||
// MCTS config is dynamically adjusted in ShardokAIClient based on proximity:
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
<Compile Include="Assets/Eagle/ConnectionStatusUI.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerIconEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/NotificationPanel.cs" />
|
||||
<Compile Include="Assets/Shardok/FreezeAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/DemoListShadow.cs" />
|
||||
<Compile Include="Assets/HoveringTooltipTextProvider.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/util/KeyModifiedAmount.cs" />
|
||||
@@ -107,6 +108,7 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/FreeForAllDecisionCommandSelector.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerButtonEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandWarningPanelController.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialModalPanel.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Context Menu/ContextMenuManager.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoopEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/BattalionUtils.cs" />
|
||||
@@ -134,7 +136,9 @@
|
||||
<Compile Include="Assets/Eagle/TextureList.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/BattalionRowController.cs" />
|
||||
<Compile Include="Assets/Shardok/DismissAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Toggle/ToggleAnim.cs" />
|
||||
<Compile Include="Assets/Shardok/HolyWaveAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerInputField.cs" />
|
||||
<Compile Include="Assets/Shardok/ActionResultTypeManager.cs" />
|
||||
<Compile Include="Assets/MainQueue.cs" />
|
||||
@@ -151,6 +155,7 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
|
||||
<Compile Include="Assets/common/ResourceFetcher.cs" />
|
||||
<Compile Include="Assets/Tutorial/Content/TutorialStep.cs" />
|
||||
<Compile Include="Assets/Auth/AuthClient.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceRejectedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
|
||||
@@ -197,10 +202,12 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceAcceptedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Progress Bar/ProgressBarEditor.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Dropdown/CustomDropdownEditor.cs" />
|
||||
<Compile Include="Assets/Shardok/WaterEffectAnimator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/RansomRejectedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/FactionsTableController.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RangeSlider.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ExileVassalCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/TutorialState.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIconEditor.cs" />
|
||||
<Compile Include="Assets/Bluetooth/DiceVectors.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoop.cs" />
|
||||
@@ -218,8 +225,10 @@
|
||||
<Compile Include="Assets/GUI Pro Kit Fantasy RPG/Extensions/UIParticle/UIParticleSystem.cs" />
|
||||
<Compile Include="Assets/Shardok/SoundManager.cs" />
|
||||
<Compile Include="Assets/Shardok/HexMesh.cs" />
|
||||
<Compile Include="Assets/Tutorial/Content/TutorialSequence.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/InvitationRejectedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/DiplomacyCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/Triggers/TutorialTriggerRegistry.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveBreakAllianceCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveAllianceCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/WithdrewForTruceDetailsNotificationGenerator.cs" />
|
||||
@@ -228,6 +237,7 @@
|
||||
<Compile Include="Assets/Eagle/CustomFileLogger.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/InvitationAcceptedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ProvinceEventsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Tutorial/TutorialManager.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/FeastCommandSelector.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Animated Icon/AnimatedIconHandler.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/GenericNotificationGenerator.cs" />
|
||||
@@ -236,6 +246,7 @@
|
||||
<Compile Include="Assets/ConnectionKiller.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RadialSliderEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/MovingArmyPopupRowController.cs" />
|
||||
<Compile Include="Assets/Shardok/DuelAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Dropdown/CustomDropdown.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/NotificationDispatcher.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/DemoTopButton.cs" />
|
||||
@@ -256,6 +267,7 @@
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/TradeCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/DominionTableRowController.cs" />
|
||||
<Compile Include="Assets/Bluetooth/RollPanelController.cs" />
|
||||
<Compile Include="Assets/Shardok/ChargeAnimator.cs" />
|
||||
<Compile Include="Assets/Eagle/ClientTextProvider.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManager.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Progress Bar/ProgressBar.cs" />
|
||||
@@ -277,6 +289,8 @@
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Fixes/LayoutGroupPositionFix.cs" />
|
||||
<Compile Include="Assets/common/Logger.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/AllianceAcceptedNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/ToolAnimator.cs" />
|
||||
<Compile Include="Assets/Shardok/ScoutAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerModalWindow.cs" />
|
||||
<Compile Include="Assets/Eagle/HeroDetailsController.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerEditor.cs" />
|
||||
@@ -284,6 +298,7 @@
|
||||
<Compile Include="Assets/Eagle/Table Rows/UnitSelectorHeroRowController.cs" />
|
||||
<Compile Include="Assets/ConnectionHandler/CustomBattleHandler.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/RansomPaidDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/ControlAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Horizontal Selector/HorizontalSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsResultRow.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManagerEditor.cs" />
|
||||
@@ -299,14 +314,17 @@
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroReturnedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/DivineCommandSelector.cs" />
|
||||
<Compile Include="Assets/Shardok/Unit.cs" />
|
||||
<Compile Include="Assets/Shardok/FearAnimator.cs" />
|
||||
<Compile Include="Assets/Eagle/ProvinceInfoPanelController.cs" />
|
||||
<Compile Include="Assets/Shardok/ShardokGameModel.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/GeneralClickDetector.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerContextMenu.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceAmbassadorImprisonedDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIcon.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialHintIndicator.cs" />
|
||||
<Compile Include="Assets/Bluetooth/UnityDieColors.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/OrganizeTroopsCommandSelector.cs" />
|
||||
<Compile Include="Assets/Shardok/ExtinguishAnimator.cs" />
|
||||
<Compile Include="Assets/Eagle/PanelPositions.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/NotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/DynamicTextNotification.cs" />
|
||||
@@ -342,6 +360,7 @@
|
||||
<Compile Include="Assets/Eagle/IClientConnectionSubscriber.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/AlmsCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ResolveDiplomacyCommandSelector.cs" />
|
||||
<Compile Include="Assets/Shardok/FleeAnimator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerWithIcon.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Horizontal Selector/HorizontalSelectorEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/PopupPanelController.cs" />
|
||||
@@ -351,7 +370,9 @@
|
||||
<Compile Include="Assets/Terrain Hexes/Example Scene/BasicHexArranger.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/ManagePrisonersCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManager.cs" />
|
||||
<Compile Include="Assets/Auth/InvitationCodeManager.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroExiledDetailsNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsSucceededNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/ReservesTableController.cs" />
|
||||
@@ -365,11 +386,13 @@
|
||||
<Compile Include="Assets/ConnectionHandler/CreateGameItem.cs" />
|
||||
<Compile Include="Assets/Eagle/EagleGameController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/FailedSwearBrotherhoodNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Shardok/FireEffectAnimator.cs" />
|
||||
<Compile Include="Assets/common/GUIUtils/EventBasedTable.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManagerEditor.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/FactionDestroyedNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerNotification.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/HandleCapturedHeroesCommandSelector.cs" />
|
||||
<Compile Include="Assets/Tutorial/UI/TutorialOverlayController.cs" />
|
||||
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ARNNotificationGenerator.cs" />
|
||||
<Compile Include="Assets/Eagle/CommandSelectors/AttackDecisionCommandSelector.cs" />
|
||||
<Compile Include="Assets/Eagle/Table Rows/AvailableHeroTableRow.cs" />
|
||||
|
||||
@@ -58,10 +58,19 @@ namespace Auth {
|
||||
/// Routed to Go auth service.
|
||||
/// </summary>
|
||||
/// <param name="provider">Discord or Google</param>
|
||||
/// <returns>Tuple of (URL to open in browser, state token for polling)</returns>
|
||||
public async Task<(string authUrl, string state)> GetOAuthUrlAsync(OAuthProvider provider) {
|
||||
/// <param name="invitationCode">Optional invitation code for new account
|
||||
/// registration</param> <returns>Tuple of (URL to open in browser, state token for
|
||||
/// polling)</returns>
|
||||
public async Task<(string authUrl, string state)> GetOAuthUrlAsync(
|
||||
OAuthProvider provider,
|
||||
string invitationCode = null) {
|
||||
var request = new GetOAuthUrlRequest { Provider = provider };
|
||||
|
||||
if (!string.IsNullOrEmpty(invitationCode)) {
|
||||
request.InvitationCode = invitationCode;
|
||||
Debug.Log("[AuthClient] Including invitation code in OAuth request");
|
||||
}
|
||||
|
||||
var response = await _authServiceClient.GetOAuthUrlAsync(request);
|
||||
|
||||
Debug.Log($"[AuthClient] Got OAuth URL for {provider}, state={response.State}");
|
||||
@@ -94,6 +103,10 @@ namespace Auth {
|
||||
case OAuthStatus.Expired:
|
||||
throw new Exception("OAuth session expired. Please try again.");
|
||||
|
||||
case OAuthStatus.InvitationRequired:
|
||||
Debug.Log("[AuthClient] Server requires invitation code for new account");
|
||||
return response; // Return so OAuthManager can handle this
|
||||
|
||||
case OAuthStatus.Pending:
|
||||
// Check timeout
|
||||
if ((DateTime.UtcNow - startTime).TotalMilliseconds > PollTimeoutMs) {
|
||||
@@ -170,18 +183,16 @@ namespace Auth {
|
||||
|
||||
/// <summary>
|
||||
/// Logout - invalidates refresh token on server.
|
||||
/// Does NOT clear local tokens (OAuthManager handles that decision).
|
||||
/// Routed to Eagle.
|
||||
/// </summary>
|
||||
public async Task LogoutAsync() {
|
||||
try {
|
||||
await _eagleClient.LogoutAsync(new LogoutRequest());
|
||||
Debug.Log("[AuthClient] Server logout successful");
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[AuthClient] Logout RPC failed (may be expected): {ex.Message}");
|
||||
}
|
||||
|
||||
// Clear local tokens regardless of server response
|
||||
TokenStorage.Clear();
|
||||
Debug.Log("[AuthClient] Logged out, tokens cleared");
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Auth {
|
||||
/// <summary>
|
||||
/// Manages invitation codes for new account registration.
|
||||
/// Reads codes from installer-provided file or allows manual entry.
|
||||
/// </summary>
|
||||
public static class InvitationCodeManager {
|
||||
private const string InvitationFileName = "invitation.json";
|
||||
private const string PlayerPrefsKey = "InvitationCode";
|
||||
|
||||
// Cache the code to avoid repeated file reads
|
||||
private static string _cachedCode;
|
||||
private static bool _cacheInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Get the invitation code, if available.
|
||||
/// Checks: 1) Cached value, 2) PlayerPrefs, 3) File from installer
|
||||
/// </summary>
|
||||
public static string GetInvitationCode() {
|
||||
if (_cacheInitialized) { return _cachedCode; }
|
||||
|
||||
// Check PlayerPrefs first (manual entry takes priority)
|
||||
string prefsCode = PlayerPrefs.GetString(PlayerPrefsKey, null);
|
||||
if (!string.IsNullOrEmpty(prefsCode)) {
|
||||
_cachedCode = prefsCode;
|
||||
_cacheInitialized = true;
|
||||
Debug.Log("[InvitationCodeManager] Using invitation code from PlayerPrefs");
|
||||
return _cachedCode;
|
||||
}
|
||||
|
||||
// Try to read from installer file
|
||||
string fileCode = ReadFromFile();
|
||||
if (!string.IsNullOrEmpty(fileCode)) {
|
||||
_cachedCode = fileCode;
|
||||
_cacheInitialized = true;
|
||||
Debug.Log("[InvitationCodeManager] Using invitation code from installer file");
|
||||
return _cachedCode;
|
||||
}
|
||||
|
||||
_cacheInitialized = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set invitation code manually (e.g., from UI input).
|
||||
/// </summary>
|
||||
public static void SetInvitationCode(string code) {
|
||||
if (string.IsNullOrEmpty(code)) {
|
||||
PlayerPrefs.DeleteKey(PlayerPrefsKey);
|
||||
_cachedCode = null;
|
||||
} else {
|
||||
PlayerPrefs.SetString(PlayerPrefsKey, code);
|
||||
_cachedCode = code;
|
||||
}
|
||||
_cacheInitialized = true;
|
||||
PlayerPrefs.Save();
|
||||
Debug.Log(
|
||||
$"[InvitationCodeManager] Invitation code {(string.IsNullOrEmpty(code) ? "cleared" : "set")}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the invitation code after successful account creation.
|
||||
/// </summary>
|
||||
public static void ClearInvitationCode() {
|
||||
PlayerPrefs.DeleteKey(PlayerPrefsKey);
|
||||
_cachedCode = null;
|
||||
_cacheInitialized = true;
|
||||
PlayerPrefs.Save();
|
||||
|
||||
// Also delete the installer file if it exists
|
||||
DeleteInstallerFile();
|
||||
|
||||
Debug.Log("[InvitationCodeManager] Invitation code cleared");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if an invitation code is available.
|
||||
/// </summary>
|
||||
public static bool HasInvitationCode => !string.IsNullOrEmpty(GetInvitationCode());
|
||||
|
||||
private static string ReadFromFile() {
|
||||
try {
|
||||
string installDir = GetInstallDirectory();
|
||||
if (string.IsNullOrEmpty(installDir)) { return null; }
|
||||
|
||||
string filePath = Path.Combine(installDir, InvitationFileName);
|
||||
if (!File.Exists(filePath)) { return null; }
|
||||
|
||||
string json = File.ReadAllText(filePath);
|
||||
// Simple JSON parsing for {"invitation_code":"XXXX"}
|
||||
var parsed = JsonUtility.FromJson<InvitationFile>(json);
|
||||
return parsed?.invitation_code;
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning(
|
||||
$"[InvitationCodeManager] Failed to read invitation file: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteInstallerFile() {
|
||||
try {
|
||||
string installDir = GetInstallDirectory();
|
||||
if (string.IsNullOrEmpty(installDir)) { return; }
|
||||
|
||||
string filePath = Path.Combine(installDir, InvitationFileName);
|
||||
if (File.Exists(filePath)) {
|
||||
File.Delete(filePath);
|
||||
Debug.Log("[InvitationCodeManager] Deleted installer invitation file");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning(
|
||||
$"[InvitationCodeManager] Failed to delete invitation file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetInstallDirectory() {
|
||||
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
|
||||
// Windows: %LOCALAPPDATA%\eagle0
|
||||
string localAppData =
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
return Path.Combine(localAppData, "eagle0");
|
||||
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
|
||||
// Mac: ~/Library/Application Support/eagle0
|
||||
string appSupport =
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
return Path.Combine(appSupport, "eagle0");
|
||||
#else
|
||||
// Other platforms not yet supported
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class InvitationFile {
|
||||
public string invitation_code;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2a19f11d1f82412e8e2811b366113ac
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Net.Eagle0.Eagle.Api.Auth;
|
||||
using UnityEngine;
|
||||
@@ -9,6 +10,7 @@ namespace Auth {
|
||||
/// - Opens system browser for OAuth consent
|
||||
/// - Polls server for OAuth completion (no deep links needed)
|
||||
/// - Token storage and refresh
|
||||
/// - Multi-account support
|
||||
/// </summary>
|
||||
public class OAuthManager : MonoBehaviour {
|
||||
public static OAuthManager Instance { get; private set; }
|
||||
@@ -16,12 +18,14 @@ namespace Auth {
|
||||
private AuthClient _authClient;
|
||||
private string _currentAuthServiceUrl;
|
||||
private string _currentEagleUrl;
|
||||
private OAuthProvider _currentLoginProvider; // Track provider during login
|
||||
|
||||
// Events for UI updates
|
||||
public event Action<UserInfo> OnLoginSuccess;
|
||||
public event Action<string> OnLoginFailed;
|
||||
public event Action OnLogout;
|
||||
public event Action<bool> OnNewUserNeedsDisplayName; // true if new user
|
||||
public event Action OnInvitationRequired; // new user needs invitation code
|
||||
|
||||
public bool IsAuthenticated => TokenStorage.HasValidToken;
|
||||
public string DisplayName => TokenStorage.DisplayName;
|
||||
@@ -80,9 +84,14 @@ namespace Auth {
|
||||
/// </summary>
|
||||
public async Task<CheckOAuthStatusResponse> LoginAsync(OAuthProvider provider) {
|
||||
EnsureConfigured();
|
||||
_currentLoginProvider = provider; // Track for later storage
|
||||
|
||||
try {
|
||||
// Get invitation code if available (for new account registration)
|
||||
string invitationCode = InvitationCodeManager.GetInvitationCode();
|
||||
|
||||
// Get OAuth URL and state from server
|
||||
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider);
|
||||
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider, invitationCode);
|
||||
|
||||
// Open system browser
|
||||
Debug.Log($"[OAuthManager] Opening browser: {authUrl}");
|
||||
@@ -91,16 +100,26 @@ namespace Auth {
|
||||
// Poll for OAuth completion (server handles the callback)
|
||||
var response = await _authClient.PollForOAuthCompletionAsync(state);
|
||||
|
||||
// Store tokens
|
||||
// Handle invitation required status
|
||||
if (response.Status == OAuthStatus.InvitationRequired) {
|
||||
Debug.Log("[OAuthManager] Server requires invitation code for new account");
|
||||
OnInvitationRequired?.Invoke();
|
||||
throw new Exception("An invitation code is required to create a new account");
|
||||
}
|
||||
|
||||
// Store tokens with provider info
|
||||
var providerName = provider.ToString().ToLowerInvariant();
|
||||
TokenStorage.StoreTokens(
|
||||
response.AccessToken,
|
||||
response.RefreshToken,
|
||||
response.ExpiresAt,
|
||||
response.User.UserId,
|
||||
response.User.DisplayName ?? "");
|
||||
response.User.DisplayName ?? "",
|
||||
providerName);
|
||||
|
||||
// Notify listeners
|
||||
// Clear invitation code after successful new account creation
|
||||
if (response.IsNewUser) {
|
||||
InvitationCodeManager.ClearInvitationCode();
|
||||
OnNewUserNeedsDisplayName?.Invoke(true);
|
||||
} else {
|
||||
OnLoginSuccess?.Invoke(response.User);
|
||||
@@ -156,6 +175,15 @@ namespace Auth {
|
||||
// Validate session with server
|
||||
try {
|
||||
var response = await _authClient.GetCurrentUserAsync();
|
||||
|
||||
// Check if user still needs to set display name (e.g., closed app before setting
|
||||
// it)
|
||||
if (string.IsNullOrEmpty(response.User.DisplayName)) {
|
||||
Debug.Log("[OAuthManager] Session restored but user needs display name");
|
||||
OnNewUserNeedsDisplayName?.Invoke(false); // false = not a brand new user
|
||||
return true;
|
||||
}
|
||||
|
||||
Debug.Log($"[OAuthManager] Session restored for {response.User.DisplayName}");
|
||||
OnLoginSuccess?.Invoke(response.User);
|
||||
return true;
|
||||
@@ -178,16 +206,89 @@ namespace Auth {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logout and clear stored tokens.
|
||||
/// Logout - clears current session but preserves stored tokens for quick re-login.
|
||||
/// </summary>
|
||||
public async Task LogoutAsync() {
|
||||
// LogoutAsync can work even without server connection - just clear local tokens
|
||||
// Notify server of logout if connected
|
||||
if (_authClient != null) {
|
||||
await _authClient.LogoutAsync();
|
||||
} else {
|
||||
TokenStorage.Clear();
|
||||
try {
|
||||
await _authClient.LogoutAsync();
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[OAuthManager] Server logout failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Clear current account selection but keep tokens stored
|
||||
TokenStorage.ClearCurrentAccount();
|
||||
OnLogout?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all stored accounts for display in UI.
|
||||
/// </summary>
|
||||
public IReadOnlyList<StoredAccount> GetStoredAccounts() {
|
||||
return TokenStorage.GetAllAccounts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select and connect with a stored account.
|
||||
/// If token is expired or about to expire, refreshes it first.
|
||||
/// </summary>
|
||||
public async Task<bool> ConnectWithStoredAccountAsync(StoredAccount account) {
|
||||
EnsureConfigured();
|
||||
|
||||
// Select this account as current
|
||||
TokenStorage.SelectAccount(account.AccountKey);
|
||||
|
||||
// Check if token needs refresh
|
||||
if (!account.HasValidToken || account.NeedsRefresh) {
|
||||
if (account.HasRefreshToken) {
|
||||
try {
|
||||
await _authClient.RefreshTokenAsync();
|
||||
Debug.Log($"[OAuthManager] Refreshed token for {account.DisplayName}");
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[OAuthManager] Token refresh failed: {ex.Message}");
|
||||
// Token refresh failed - need to re-authenticate
|
||||
// Parse provider from account key
|
||||
var provider = account.Provider.ToLowerInvariant() == "google"
|
||||
? OAuthProvider.Google
|
||||
: OAuthProvider.Discord;
|
||||
OnLoginFailed?.Invoke("Session expired. Please sign in again.");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
OnLoginFailed?.Invoke("Session expired. Please sign in again.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate session with server
|
||||
try {
|
||||
var response = await _authClient.GetCurrentUserAsync();
|
||||
|
||||
// Check if user still needs to set display name
|
||||
if (string.IsNullOrEmpty(response.User.DisplayName)) {
|
||||
Debug.Log("[OAuthManager] Stored account needs display name");
|
||||
OnNewUserNeedsDisplayName?.Invoke(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"[OAuthManager] Connected with stored account: {response.User.DisplayName}");
|
||||
OnLoginSuccess?.Invoke(response.User);
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[OAuthManager] Session validation failed: {ex.Message}");
|
||||
OnLoginFailed?.Invoke("Session invalid. Please sign in again.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a stored account permanently.
|
||||
/// </summary>
|
||||
public void RemoveStoredAccount(StoredAccount account) {
|
||||
TokenStorage.RemoveAccount(account.AccountKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Auth {
|
||||
/// <summary>
|
||||
/// Represents a stored OAuth account with tokens.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StoredAccount {
|
||||
public string Provider; // "discord" or "google"
|
||||
public string UserId; // OAuth provider user ID
|
||||
public string DisplayName; // User's display name
|
||||
public string AccessToken;
|
||||
public string RefreshToken;
|
||||
public long ExpiresAt; // Unix timestamp
|
||||
|
||||
public string AccountKey => $"{Provider}:{UserId}";
|
||||
|
||||
public bool HasValidToken => !string.IsNullOrEmpty(AccessToken) &&
|
||||
ExpiresAt > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
public bool HasRefreshToken => !string.IsNullOrEmpty(RefreshToken);
|
||||
|
||||
public bool NeedsRefresh {
|
||||
get {
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
return ExpiresAt > 0 && ExpiresAt - now < 300; // 5 minutes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Container for all stored accounts, serialized to JSON.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StoredAccountsData {
|
||||
public List<StoredAccount> Accounts = new();
|
||||
public string CurrentAccountKey; // Provider:UserId of active account
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Secure storage for OAuth tokens using Unity's PlayerPrefs.
|
||||
/// Supports multiple accounts - tokens are preserved on logout.
|
||||
/// Values are cached in memory to allow access from background threads.
|
||||
/// In production, consider using platform-specific secure storage
|
||||
/// (Keychain on iOS, Keystore on Android).
|
||||
/// </summary>
|
||||
public static class TokenStorage {
|
||||
private const string AccessTokenKey = "eagle0_access_token";
|
||||
private const string RefreshTokenKey = "eagle0_refresh_token";
|
||||
private const string ExpiresAtKey = "eagle0_expires_at";
|
||||
private const string UserIdKey = "eagle0_user_id";
|
||||
private const string DisplayNameKey = "eagle0_display_name";
|
||||
private const string AccountsDataKey = "eagle0_accounts_data";
|
||||
|
||||
// In-memory cache for thread-safe access from background threads
|
||||
// PlayerPrefs can only be accessed from the main thread
|
||||
private static string _cachedAccessToken;
|
||||
private static string _cachedRefreshToken;
|
||||
private static long _cachedExpiresAt;
|
||||
private static string _cachedUserId;
|
||||
private static string _cachedDisplayName;
|
||||
// Legacy keys for migration
|
||||
private const string LegacyAccessTokenKey = "eagle0_access_token";
|
||||
private const string LegacyRefreshTokenKey = "eagle0_refresh_token";
|
||||
private const string LegacyExpiresAtKey = "eagle0_expires_at";
|
||||
private const string LegacyUserIdKey = "eagle0_user_id";
|
||||
private const string LegacyDisplayNameKey = "eagle0_display_name";
|
||||
|
||||
// In-memory cache
|
||||
private static StoredAccountsData _accountsData;
|
||||
private static bool _cacheInitialized = false;
|
||||
|
||||
/// <summary>
|
||||
@@ -29,147 +65,245 @@ namespace Auth {
|
||||
/// Call this early in app startup (e.g., in Awake or Start).
|
||||
/// </summary>
|
||||
public static void InitializeCache() {
|
||||
_cachedAccessToken = PlayerPrefs.GetString(AccessTokenKey, null);
|
||||
_cachedRefreshToken = PlayerPrefs.GetString(RefreshTokenKey, null);
|
||||
_cachedExpiresAt =
|
||||
long.TryParse(PlayerPrefs.GetString(ExpiresAtKey, "0"), out var val) ? val : 0;
|
||||
_cachedUserId = PlayerPrefs.GetString(UserIdKey, null);
|
||||
_cachedDisplayName = PlayerPrefs.GetString(DisplayNameKey, null);
|
||||
_cacheInitialized = true;
|
||||
var json = PlayerPrefs.GetString(AccountsDataKey, "");
|
||||
|
||||
Debug.Log(
|
||||
$"[TokenStorage] Cache initialized, hasToken={!string.IsNullOrEmpty(_cachedAccessToken)}");
|
||||
}
|
||||
|
||||
public static bool HasValidToken {
|
||||
get {
|
||||
var token = AccessToken;
|
||||
var expiresAt = ExpiresAt;
|
||||
return !string.IsNullOrEmpty(token) &&
|
||||
expiresAt > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasRefreshToken => !string.IsNullOrEmpty(RefreshToken);
|
||||
|
||||
public static string AccessToken {
|
||||
get {
|
||||
if (!_cacheInitialized) {
|
||||
Debug.LogWarning(
|
||||
"[TokenStorage] Cache not initialized, returning cached value anyway");
|
||||
if (!string.IsNullOrEmpty(json)) {
|
||||
try {
|
||||
_accountsData = JsonUtility.FromJson<StoredAccountsData>(json);
|
||||
} catch (Exception ex) {
|
||||
Debug.LogWarning($"[TokenStorage] Failed to parse accounts data: {ex.Message}");
|
||||
_accountsData = new StoredAccountsData();
|
||||
}
|
||||
return _cachedAccessToken;
|
||||
} else {
|
||||
_accountsData = new StoredAccountsData();
|
||||
// Try to migrate legacy single-account data
|
||||
MigrateLegacyData();
|
||||
}
|
||||
private
|
||||
set {
|
||||
_cachedAccessToken = value;
|
||||
PlayerPrefs.SetString(AccessTokenKey, value ?? "");
|
||||
|
||||
_cacheInitialized = true;
|
||||
Debug.Log(
|
||||
$"[TokenStorage] Cache initialized, {_accountsData.Accounts.Count} accounts stored");
|
||||
}
|
||||
|
||||
private static void MigrateLegacyData() {
|
||||
var legacyAccessToken = PlayerPrefs.GetString(LegacyAccessTokenKey, "");
|
||||
if (string.IsNullOrEmpty(legacyAccessToken)) return;
|
||||
|
||||
var legacyRefreshToken = PlayerPrefs.GetString(LegacyRefreshTokenKey, "");
|
||||
var legacyExpiresAt =
|
||||
long.TryParse(PlayerPrefs.GetString(LegacyExpiresAtKey, "0"), out var val) ? val
|
||||
: 0;
|
||||
var legacyUserId = PlayerPrefs.GetString(LegacyUserIdKey, "");
|
||||
var legacyDisplayName = PlayerPrefs.GetString(LegacyDisplayNameKey, "");
|
||||
|
||||
if (!string.IsNullOrEmpty(legacyUserId)) {
|
||||
// Assume discord for legacy data (most common)
|
||||
var account = new StoredAccount {
|
||||
Provider = "discord",
|
||||
UserId = legacyUserId,
|
||||
DisplayName = legacyDisplayName,
|
||||
AccessToken = legacyAccessToken,
|
||||
RefreshToken = legacyRefreshToken,
|
||||
ExpiresAt = legacyExpiresAt
|
||||
};
|
||||
_accountsData.Accounts.Add(account);
|
||||
_accountsData.CurrentAccountKey = account.AccountKey;
|
||||
SaveAccountsData();
|
||||
|
||||
// Clear legacy keys
|
||||
PlayerPrefs.DeleteKey(LegacyAccessTokenKey);
|
||||
PlayerPrefs.DeleteKey(LegacyRefreshTokenKey);
|
||||
PlayerPrefs.DeleteKey(LegacyExpiresAtKey);
|
||||
PlayerPrefs.DeleteKey(LegacyUserIdKey);
|
||||
PlayerPrefs.DeleteKey(LegacyDisplayNameKey);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
Debug.Log($"[TokenStorage] Migrated legacy account: {account.DisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
public static string RefreshToken {
|
||||
get => _cachedRefreshToken;
|
||||
private
|
||||
set {
|
||||
_cachedRefreshToken = value;
|
||||
PlayerPrefs.SetString(RefreshTokenKey, value ?? "");
|
||||
}
|
||||
private static void SaveAccountsData() {
|
||||
var json = JsonUtility.ToJson(_accountsData);
|
||||
PlayerPrefs.SetString(AccountsDataKey, json);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public static long ExpiresAt {
|
||||
get => _cachedExpiresAt;
|
||||
private
|
||||
set {
|
||||
_cachedExpiresAt = value;
|
||||
PlayerPrefs.SetString(ExpiresAtKey, value.ToString());
|
||||
}
|
||||
/// <summary>
|
||||
/// Get all stored accounts.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<StoredAccount> GetAllAccounts() {
|
||||
EnsureInitialized();
|
||||
return _accountsData.Accounts.AsReadOnly();
|
||||
}
|
||||
|
||||
public static string UserId {
|
||||
get => _cachedUserId;
|
||||
private
|
||||
set {
|
||||
_cachedUserId = value;
|
||||
PlayerPrefs.SetString(UserIdKey, value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public static string DisplayName {
|
||||
get => _cachedDisplayName;
|
||||
private
|
||||
set {
|
||||
_cachedDisplayName = value;
|
||||
PlayerPrefs.SetString(DisplayNameKey, value ?? "");
|
||||
/// <summary>
|
||||
/// Get the currently active account, or null if none selected.
|
||||
/// </summary>
|
||||
public static StoredAccount CurrentAccount {
|
||||
get {
|
||||
EnsureInitialized();
|
||||
if (string.IsNullOrEmpty(_accountsData.CurrentAccountKey)) return null;
|
||||
return _accountsData.Accounts.Find(
|
||||
a => a.AccountKey == _accountsData.CurrentAccountKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store tokens received from OAuth exchange.
|
||||
/// Select an account as the current active account.
|
||||
/// </summary>
|
||||
public static void SelectAccount(string accountKey) {
|
||||
EnsureInitialized();
|
||||
var account = _accountsData.Accounts.Find(a => a.AccountKey == accountKey);
|
||||
if (account != null) {
|
||||
_accountsData.CurrentAccountKey = accountKey;
|
||||
SaveAccountsData();
|
||||
Debug.Log($"[TokenStorage] Selected account: {account.DisplayName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the current account selection (logout without deleting tokens).
|
||||
/// </summary>
|
||||
public static void ClearCurrentAccount() {
|
||||
EnsureInitialized();
|
||||
_accountsData.CurrentAccountKey = null;
|
||||
SaveAccountsData();
|
||||
Debug.Log("[TokenStorage] Cleared current account selection");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a specific account and its tokens.
|
||||
/// </summary>
|
||||
public static void RemoveAccount(string accountKey) {
|
||||
EnsureInitialized();
|
||||
var removed = _accountsData.Accounts.RemoveAll(a => a.AccountKey == accountKey);
|
||||
if (_accountsData.CurrentAccountKey == accountKey) {
|
||||
_accountsData.CurrentAccountKey = null;
|
||||
}
|
||||
if (removed > 0) {
|
||||
SaveAccountsData();
|
||||
Debug.Log($"[TokenStorage] Removed account: {accountKey}");
|
||||
}
|
||||
}
|
||||
|
||||
// Compatibility properties that reference the current account
|
||||
|
||||
public static bool HasValidToken => CurrentAccount?.HasValidToken ?? false;
|
||||
|
||||
public static bool HasRefreshToken => CurrentAccount?.HasRefreshToken ?? false;
|
||||
|
||||
public static string AccessToken => CurrentAccount?.AccessToken;
|
||||
|
||||
public static string RefreshToken => CurrentAccount?.RefreshToken;
|
||||
|
||||
public static long ExpiresAt => CurrentAccount?.ExpiresAt ?? 0;
|
||||
|
||||
public static string UserId => CurrentAccount?.UserId;
|
||||
|
||||
public static string DisplayName => CurrentAccount?.DisplayName;
|
||||
|
||||
public static bool NeedsRefresh => CurrentAccount?.NeedsRefresh ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Store tokens for an account. Creates new or updates existing.
|
||||
/// </summary>
|
||||
public static void StoreTokens(
|
||||
string accessToken,
|
||||
string refreshToken,
|
||||
long expiresAt,
|
||||
string userId,
|
||||
string displayName) {
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
ExpiresAt = expiresAt;
|
||||
UserId = userId;
|
||||
DisplayName = displayName;
|
||||
PlayerPrefs.Save();
|
||||
string displayName,
|
||||
string provider = "discord") {
|
||||
EnsureInitialized();
|
||||
|
||||
Debug.Log(
|
||||
$"[TokenStorage] Stored tokens for user {displayName} (expires at {expiresAt})");
|
||||
var accountKey = $"{provider}:{userId}";
|
||||
var existingAccount = _accountsData.Accounts.Find(a => a.AccountKey == accountKey);
|
||||
|
||||
if (existingAccount != null) {
|
||||
existingAccount.AccessToken = accessToken;
|
||||
existingAccount.RefreshToken = refreshToken;
|
||||
existingAccount.ExpiresAt = expiresAt;
|
||||
existingAccount.DisplayName = displayName;
|
||||
} else {
|
||||
var newAccount = new StoredAccount {
|
||||
Provider = provider,
|
||||
UserId = userId,
|
||||
DisplayName = displayName,
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken,
|
||||
ExpiresAt = expiresAt
|
||||
};
|
||||
_accountsData.Accounts.Add(newAccount);
|
||||
}
|
||||
|
||||
// Set as current account
|
||||
_accountsData.CurrentAccountKey = accountKey;
|
||||
SaveAccountsData();
|
||||
|
||||
Debug.Log($"[TokenStorage] Stored tokens for {displayName} ({provider})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update access token after refresh.
|
||||
/// Update access token after refresh for the current account.
|
||||
/// </summary>
|
||||
public static void UpdateAccessToken(string accessToken, long expiresAt) {
|
||||
AccessToken = accessToken;
|
||||
ExpiresAt = expiresAt;
|
||||
PlayerPrefs.Save();
|
||||
|
||||
Debug.Log($"[TokenStorage] Updated access token (expires at {expiresAt})");
|
||||
EnsureInitialized();
|
||||
var account = CurrentAccount;
|
||||
if (account != null) {
|
||||
account.AccessToken = accessToken;
|
||||
account.ExpiresAt = expiresAt;
|
||||
SaveAccountsData();
|
||||
Debug.Log($"[TokenStorage] Updated access token (expires at {expiresAt})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update display name after user sets it.
|
||||
/// Update access token for a specific account (by key).
|
||||
/// </summary>
|
||||
public static void
|
||||
UpdateAccessTokenForAccount(string accountKey, string accessToken, long expiresAt) {
|
||||
EnsureInitialized();
|
||||
var account = _accountsData.Accounts.Find(a => a.AccountKey == accountKey);
|
||||
if (account != null) {
|
||||
account.AccessToken = accessToken;
|
||||
account.ExpiresAt = expiresAt;
|
||||
SaveAccountsData();
|
||||
Debug.Log(
|
||||
$"[TokenStorage] Updated access token for {account.DisplayName} (expires at {expiresAt})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update display name for the current account.
|
||||
/// </summary>
|
||||
public static void UpdateDisplayName(string displayName) {
|
||||
DisplayName = displayName;
|
||||
PlayerPrefs.Save();
|
||||
EnsureInitialized();
|
||||
var account = CurrentAccount;
|
||||
if (account != null) {
|
||||
account.DisplayName = displayName;
|
||||
SaveAccountsData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all stored tokens (logout).
|
||||
/// Clear all stored accounts (full reset).
|
||||
/// </summary>
|
||||
public static void Clear() {
|
||||
_cachedAccessToken = null;
|
||||
_cachedRefreshToken = null;
|
||||
_cachedExpiresAt = 0;
|
||||
_cachedUserId = null;
|
||||
_cachedDisplayName = null;
|
||||
|
||||
PlayerPrefs.DeleteKey(AccessTokenKey);
|
||||
PlayerPrefs.DeleteKey(RefreshTokenKey);
|
||||
PlayerPrefs.DeleteKey(ExpiresAtKey);
|
||||
PlayerPrefs.DeleteKey(UserIdKey);
|
||||
PlayerPrefs.DeleteKey(DisplayNameKey);
|
||||
public static void ClearAll() {
|
||||
_accountsData = new StoredAccountsData();
|
||||
PlayerPrefs.DeleteKey(AccountsDataKey);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
Debug.Log("[TokenStorage] Cleared all tokens");
|
||||
Debug.Log("[TokenStorage] Cleared all accounts");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if token needs refresh (expires within 5 minutes).
|
||||
/// Legacy Clear method - now just clears current selection, not tokens.
|
||||
/// </summary>
|
||||
public static bool NeedsRefresh {
|
||||
get {
|
||||
var expiresAt = ExpiresAt;
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
return expiresAt > 0 && expiresAt - now < 300; // 5 minutes
|
||||
public static void Clear() { ClearCurrentAccount(); }
|
||||
|
||||
private static void EnsureInitialized() {
|
||||
if (!_cacheInitialized) {
|
||||
Debug.LogWarning("[TokenStorage] Cache not initialized, initializing now");
|
||||
InitializeCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+184
-85
@@ -80,9 +80,6 @@ public class AuthInterceptor : Interceptor {
|
||||
};
|
||||
|
||||
public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public TMP_Dropdown environmentDropdown;
|
||||
public TMP_InputField nameField;
|
||||
public TMP_InputField passwordField;
|
||||
public TMP_Dropdown resolutionDropdown;
|
||||
|
||||
[Header("OAuth UI")]
|
||||
@@ -91,16 +88,23 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public Button googleLoginButton;
|
||||
public TextMeshProUGUI oauthStatusText;
|
||||
|
||||
[Header("Stored Accounts")]
|
||||
public GameObject storedAccountsContainer;
|
||||
public GameObject storedAccountButtonPrefab;
|
||||
public Sprite discordProviderIcon;
|
||||
public Sprite googleProviderIcon;
|
||||
|
||||
[Header("Display Name Setup")]
|
||||
public GameObject displayNamePanel;
|
||||
public TMP_InputField displayNameField;
|
||||
public Button setDisplayNameButton;
|
||||
public TextMeshProUGUI displayNameErrorText;
|
||||
|
||||
[Header("Legacy Auth (for testing)")]
|
||||
public GameObject legacyAuthPanel;
|
||||
public Button useLegacyAuthButton;
|
||||
public TextMeshProUGUI useLegacyAuthButtonText;
|
||||
[Header("Invitation Code Entry")]
|
||||
public GameObject invitationCodePanel;
|
||||
public TMP_InputField invitationCodeField;
|
||||
public Button submitInvitationCodeButton;
|
||||
public TextMeshProUGUI invitationCodeErrorText;
|
||||
|
||||
[Header("Status Display")]
|
||||
public TextMeshProUGUI connectionStatusText;
|
||||
@@ -114,7 +118,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
public Button logoutButton;
|
||||
public Button customBattleButton;
|
||||
public Button cancelCustomBattleButton;
|
||||
public TextMeshProUGUI lobbyEnvironmentText;
|
||||
public TMP_Dropdown lobbyEnvironmentDropdown;
|
||||
public TextMeshProUGUI lobbyUserText;
|
||||
|
||||
public GameObject runningGamesListArea;
|
||||
@@ -142,7 +146,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly Object pendingReplyLock = new Object();
|
||||
private int _connectedEnvironmentIndex = -1; // -1 means not connected
|
||||
private bool _useOAuth = false; // Default to legacy until server-side OAuth is ready
|
||||
private List<GameObject> _storedAccountButtons = new List<GameObject>();
|
||||
|
||||
public ClientPregeneratedText clientPregeneratedText;
|
||||
|
||||
@@ -166,7 +170,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
: null;
|
||||
|
||||
private string GetUrlFromEnvironment() {
|
||||
int envIndex = environmentDropdown.value;
|
||||
int envIndex = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
string prefix = EnvironmentOptions[envIndex];
|
||||
return prefix + BaseDomain;
|
||||
}
|
||||
@@ -199,14 +203,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
resolutionDropdown.AddOptions(resolutions.Select(r => $"{r.width} x {r.height}").ToList());
|
||||
resolutionDropdown.value = resolutions.ToList().IndexOf(currentResolution);
|
||||
|
||||
// Set up environment dropdown
|
||||
environmentDropdown.ClearOptions();
|
||||
environmentDropdown.AddOptions(EnvironmentDisplayNames);
|
||||
environmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
|
||||
nameField.text = PlayerPrefs.GetString(NameKey, "sample_name");
|
||||
passwordField.text = PlayerPrefs.GetString(PasswordKey, "sample_password");
|
||||
|
||||
connectionCanvas.gameObject.SetActive(true);
|
||||
eagleCanvas.gameObject.SetActive(false);
|
||||
shardokCanvas.gameObject.SetActive(false);
|
||||
@@ -232,7 +228,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void SetupOAuthUI() {
|
||||
// Set up OAuth button click handlers
|
||||
// Set up OAuth button click handlers for new sign-ins
|
||||
if (discordLoginButton != null) {
|
||||
discordLoginButton.onClick.AddListener(
|
||||
() => OnOAuthLoginClicked(OAuthProvider.Discord));
|
||||
@@ -243,8 +239,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
if (setDisplayNameButton != null) {
|
||||
setDisplayNameButton.onClick.AddListener(OnSetDisplayNameClicked);
|
||||
}
|
||||
if (useLegacyAuthButton != null) {
|
||||
useLegacyAuthButton.onClick.AddListener(OnUseLegacyAuthClicked);
|
||||
if (submitInvitationCodeButton != null) {
|
||||
submitInvitationCodeButton.onClick.AddListener(OnSubmitInvitationCodeClicked);
|
||||
}
|
||||
|
||||
// Subscribe to OAuthManager events
|
||||
@@ -253,26 +249,83 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
OAuthManager.Instance.OnLoginFailed += OnOAuthLoginFailed;
|
||||
OAuthManager.Instance.OnNewUserNeedsDisplayName += OnNewUserNeedsDisplayName;
|
||||
OAuthManager.Instance.OnLogout += OnOAuthLogout;
|
||||
OAuthManager.Instance.OnInvitationRequired += OnInvitationRequired;
|
||||
}
|
||||
|
||||
// Show appropriate panel based on auth state
|
||||
// Show auth panel with stored accounts
|
||||
ShowAuthPanel();
|
||||
}
|
||||
|
||||
private void ShowAuthPanel() {
|
||||
// Hide all auth panels first
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
// Hide other panels
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(false);
|
||||
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(false);
|
||||
if (invitationCodePanel != null) invitationCodePanel.SetActive(false);
|
||||
|
||||
// Show appropriate panel based on _useOAuth
|
||||
if (_useOAuth) {
|
||||
if (oauthPanel != null) oauthPanel.SetActive(true);
|
||||
if (useLegacyAuthButtonText != null)
|
||||
useLegacyAuthButtonText.text = "Use Classic Sign-in";
|
||||
// Show OAuth panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(true);
|
||||
|
||||
// Refresh stored account buttons
|
||||
RefreshStoredAccountButtons();
|
||||
}
|
||||
|
||||
private void RefreshStoredAccountButtons() {
|
||||
// Clear existing buttons
|
||||
foreach (var btn in _storedAccountButtons) {
|
||||
if (btn != null) Destroy(btn);
|
||||
}
|
||||
_storedAccountButtons.Clear();
|
||||
|
||||
if (storedAccountsContainer == null || storedAccountButtonPrefab == null) return;
|
||||
|
||||
// Get stored accounts
|
||||
var accounts = OAuthManager.Instance?.GetStoredAccounts();
|
||||
if (accounts == null || accounts.Count == 0) return;
|
||||
|
||||
// Create button for each stored account
|
||||
foreach (var account in accounts) {
|
||||
var buttonObj =
|
||||
Instantiate(storedAccountButtonPrefab, storedAccountsContainer.transform);
|
||||
buttonObj.transform.localScale = Vector3.one;
|
||||
|
||||
// Set button text (just display name, icon shows provider)
|
||||
var buttonText = buttonObj.GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (buttonText != null) { buttonText.text = account.DisplayName; }
|
||||
|
||||
// Set provider icon (look for child named "ProviderIcon")
|
||||
var providerIconTransform = buttonObj.transform.Find("ProviderIcon");
|
||||
if (providerIconTransform != null) {
|
||||
var providerImage = providerIconTransform.GetComponent<Image>();
|
||||
if (providerImage != null) {
|
||||
var isGoogle = account.Provider.ToLowerInvariant() == "google";
|
||||
providerImage.sprite = isGoogle ? googleProviderIcon : discordProviderIcon;
|
||||
}
|
||||
}
|
||||
|
||||
// Set click handler
|
||||
var button = buttonObj.GetComponent<Button>();
|
||||
if (button != null) {
|
||||
var accountCopy = account; // Capture for lambda
|
||||
button.onClick.AddListener(() => OnStoredAccountClicked(accountCopy));
|
||||
}
|
||||
|
||||
_storedAccountButtons.Add(buttonObj);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnStoredAccountClicked(StoredAccount account) {
|
||||
// Configure OAuthManager with URLs
|
||||
OAuthManager.Instance.SetServerUrls(GetAuthServiceUrl(), GetEagleUrl());
|
||||
|
||||
if (oauthStatusText != null) {
|
||||
oauthStatusText.text = $"Connecting as {account.DisplayName}...";
|
||||
}
|
||||
|
||||
var success = await OAuthManager.Instance.ConnectWithStoredAccountAsync(account);
|
||||
if (success) {
|
||||
// OnLoginSuccess will handle connection to lobby
|
||||
} else {
|
||||
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(true);
|
||||
if (useLegacyAuthButtonText != null) useLegacyAuthButtonText.text = "Use OAuth Sign-in";
|
||||
// Failed - might need re-auth, refresh the buttons
|
||||
RefreshStoredAccountButtons();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,22 +335,56 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
if (cancelCustomBattleButton != null) {
|
||||
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
|
||||
}
|
||||
|
||||
// Set up environment dropdown
|
||||
if (lobbyEnvironmentDropdown != null) {
|
||||
lobbyEnvironmentDropdown.ClearOptions();
|
||||
lobbyEnvironmentDropdown.AddOptions(EnvironmentDisplayNames);
|
||||
lobbyEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLobbyStatusDisplays() {
|
||||
// Show environment (prod/qa)
|
||||
if (lobbyEnvironmentText != null) {
|
||||
lobbyEnvironmentText.text = ConnectedEnvironmentName ?? "";
|
||||
// Update environment dropdown selection
|
||||
if (lobbyEnvironmentDropdown != null && _connectedEnvironmentIndex >= 0) {
|
||||
// Temporarily remove listener to avoid triggering reconnect
|
||||
lobbyEnvironmentDropdown.onValueChanged.RemoveListener(OnLobbyEnvironmentChanged);
|
||||
lobbyEnvironmentDropdown.value = _connectedEnvironmentIndex;
|
||||
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
|
||||
}
|
||||
|
||||
// Show current user - prefer OAuth display name, fall back to classic login name
|
||||
if (lobbyUserText != null) {
|
||||
if (_useOAuth && !string.IsNullOrEmpty(TokenStorage.DisplayName)) {
|
||||
lobbyUserText.text = TokenStorage.DisplayName;
|
||||
} else {
|
||||
lobbyUserText.text = nameField.text;
|
||||
}
|
||||
}
|
||||
// Show current user from OAuth
|
||||
if (lobbyUserText != null) { lobbyUserText.text = TokenStorage.DisplayName ?? ""; }
|
||||
}
|
||||
|
||||
private void OnLobbyEnvironmentChanged(int newEnvironmentIndex) {
|
||||
if (newEnvironmentIndex == _connectedEnvironmentIndex) return;
|
||||
|
||||
Debug.Log(
|
||||
$"[ConnectionHandler] Switching environment from {ConnectedEnvironmentName} to {EnvironmentDisplayNames[newEnvironmentIndex]}");
|
||||
|
||||
// Disconnect from current environment
|
||||
_persistentClientConnection?.Dispose();
|
||||
eagleConnection?.Dispose();
|
||||
_httpClient?.Dispose();
|
||||
|
||||
_persistentClientConnection = null;
|
||||
eagleConnection = null;
|
||||
_httpClient = null;
|
||||
|
||||
// Update environment index and reconnect
|
||||
_connectedEnvironmentIndex = newEnvironmentIndex;
|
||||
PlayerPrefs.SetInt(EnvironmentKey, newEnvironmentIndex);
|
||||
|
||||
// Clear current game lists while reconnecting
|
||||
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }
|
||||
|
||||
// Reconnect to new environment
|
||||
_createConnection();
|
||||
RequestMaps();
|
||||
StartListeningForLobbyUpdates();
|
||||
}
|
||||
|
||||
private async void OnLogoutClicked() {
|
||||
@@ -378,11 +465,53 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Show display name panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (invitationCodePanel != null) invitationCodePanel.SetActive(false);
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(true);
|
||||
if (displayNameErrorText != null) displayNameErrorText.text = "";
|
||||
});
|
||||
}
|
||||
|
||||
private void OnInvitationRequired() {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
// Show invitation code entry panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (displayNamePanel != null) displayNamePanel.SetActive(false);
|
||||
if (invitationCodePanel != null) invitationCodePanel.SetActive(true);
|
||||
if (invitationCodeErrorText != null) {
|
||||
invitationCodeErrorText.text =
|
||||
"An invitation code is required to create a new account.";
|
||||
}
|
||||
if (invitationCodeField != null) invitationCodeField.text = "";
|
||||
});
|
||||
}
|
||||
|
||||
private void OnSubmitInvitationCodeClicked() {
|
||||
if (invitationCodeField == null) return;
|
||||
|
||||
var code = invitationCodeField.text.Trim();
|
||||
if (string.IsNullOrEmpty(code)) {
|
||||
if (invitationCodeErrorText != null) {
|
||||
invitationCodeErrorText.text = "Please enter an invitation code";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the code and return to login screen to retry
|
||||
InvitationCodeManager.SetInvitationCode(code);
|
||||
|
||||
if (invitationCodeErrorText != null) {
|
||||
invitationCodeErrorText.text = "Code saved. Please sign in again.";
|
||||
}
|
||||
|
||||
// Return to auth panel after a short delay
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (oauthStatusText != null) {
|
||||
oauthStatusText.text = "Invitation code saved. Please sign in to continue.";
|
||||
}
|
||||
ShowAuthPanel();
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnSetDisplayNameClicked() {
|
||||
if (displayNameField == null || OAuthManager.Instance == null) return;
|
||||
|
||||
@@ -410,27 +539,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUseLegacyAuthClicked() {
|
||||
_useOAuth = !_useOAuth; // Toggle instead of just setting false
|
||||
|
||||
if (_useOAuth) {
|
||||
// Show OAuth panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(true);
|
||||
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(false);
|
||||
if (useLegacyAuthButtonText != null)
|
||||
useLegacyAuthButtonText.text = "Use Classic Sign-in";
|
||||
} else {
|
||||
// Show legacy panel
|
||||
if (oauthPanel != null) oauthPanel.SetActive(false);
|
||||
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(true);
|
||||
if (useLegacyAuthButtonText != null) useLegacyAuthButtonText.text = "Use OAuth Sign-in";
|
||||
}
|
||||
}
|
||||
|
||||
private void ConnectWithOAuth() {
|
||||
_useOAuth = true;
|
||||
_internalConnectEagle();
|
||||
}
|
||||
private void ConnectWithOAuth() { _internalConnectEagle(); }
|
||||
|
||||
private float _statusUpdateTimer = 0f;
|
||||
private const float StatusUpdateInterval = 0.5f;
|
||||
@@ -634,10 +743,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void _createConnection() {
|
||||
_connectedEnvironmentIndex = environmentDropdown.value;
|
||||
PlayerPrefs.SetInt(EnvironmentKey, _connectedEnvironmentIndex);
|
||||
PlayerPrefs.SetString(NameKey, nameField.text);
|
||||
PlayerPrefs.SetString(PasswordKey, passwordField.text);
|
||||
_connectedEnvironmentIndex = PlayerPrefs.GetInt(EnvironmentKey, 0);
|
||||
|
||||
// Dispose existing connections before creating new ones
|
||||
_httpClient?.Dispose();
|
||||
@@ -650,25 +756,18 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
string url = GetUrlFromEnvironment();
|
||||
|
||||
if (_useOAuth && TokenStorage.HasValidToken) {
|
||||
// Use JWT-based connection
|
||||
eagleConnection = EagleConnection.CreateWithJwt(url);
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", TokenStorage.AccessToken);
|
||||
} else {
|
||||
// Legacy Basic Auth connection
|
||||
PlayerPrefs.SetString(NameKey, nameField.text);
|
||||
PlayerPrefs.SetString(PasswordKey, passwordField.text);
|
||||
|
||||
eagleConnection = new EagleConnection(nameField.text, passwordField.text, url);
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Basic", eagleConnection.authHeader);
|
||||
// OAuth JWT-based connection
|
||||
if (!TokenStorage.HasValidToken) {
|
||||
Debug.LogError("[ConnectionHandler] No valid token available for connection");
|
||||
return;
|
||||
}
|
||||
|
||||
eagleConnection = EagleConnection.CreateWithJwt(url);
|
||||
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", TokenStorage.AccessToken);
|
||||
|
||||
_persistentClientConnection = new PersistentClientConnection(
|
||||
eagleConnection.EagleGrpcClient,
|
||||
eagleConnection.credentials,
|
||||
|
||||
LFS
BIN
Binary file not shown.
+117
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af9f637b3a1f6433f819a0bcdb326453
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &1262787222422545696
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6242087783640990751}
|
||||
- component: {fileID: 5444015614922696687}
|
||||
- component: {fileID: 8871991993173924918}
|
||||
- component: {fileID: 6965305925514130928}
|
||||
m_Layer: 5
|
||||
m_Name: ProviderIcon
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &6242087783640990751
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
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: 157686408557551303}
|
||||
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 &5444015614922696687
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &8871991993173924918
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
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: 80
|
||||
m_PreferredHeight: 50
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &6965305925514130928
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1262787222422545696}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
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_Sprite: {fileID: 21300000, guid: b6b68c8c37d5cf1419267a80c426ac83, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 1
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &5166012670666351313
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 157686408557551303}
|
||||
- component: {fileID: 3634401029247236684}
|
||||
- component: {fileID: 7790188191322585408}
|
||||
- component: {fileID: 646268409010340088}
|
||||
- component: {fileID: 3097176747511135857}
|
||||
- component: {fileID: 7691398338434978342}
|
||||
m_Layer: 5
|
||||
m_Name: StoredAccountButton
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &157686408557551303
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5166012670666351313}
|
||||
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:
|
||||
- {fileID: 7974514965529143181}
|
||||
- {fileID: 6242087783640990751}
|
||||
m_Father: {fileID: 0}
|
||||
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 &3634401029247236684
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5166012670666351313}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &7790188191322585408
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5166012670666351313}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
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_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &646268409010340088
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5166012670666351313}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_WrapAround: 0
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 7790188191322585408}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!114 &3097176747511135857
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5166012670666351313}
|
||||
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: 400
|
||||
m_PreferredHeight: 60
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &7691398338434978342
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5166012670666351313}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
|
||||
m_Padding:
|
||||
m_Left: 0
|
||||
m_Right: 0
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 5
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 1
|
||||
m_ChildControlHeight: 1
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!1 &5617793748733146591
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7974514965529143181}
|
||||
- component: {fileID: 4513536456760004536}
|
||||
- component: {fileID: 6210669843299902677}
|
||||
- component: {fileID: 572895541873054588}
|
||||
m_Layer: 5
|
||||
m_Name: Label
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7974514965529143181
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5617793748733146591}
|
||||
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: 157686408557551303}
|
||||
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 &4513536456760004536
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5617793748733146591}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &6210669843299902677
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5617793748733146591}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
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_text: nolen (dan@danielcrosby.net)
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4281479730
|
||||
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 18
|
||||
m_fontSizeBase: 18
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!114 &572895541873054588
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5617793748733146591}
|
||||
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: -1
|
||||
m_PreferredHeight: 60
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6440904d10a244010b3a91e0aa247749
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+25
-3
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using common;
|
||||
using common.GUIUtils;
|
||||
using Eagle0.Tutorial;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Common;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
@@ -158,9 +159,11 @@ namespace eagle {
|
||||
|
||||
public void StopAll() {
|
||||
_newModel = null;
|
||||
ModelUpdater.StopListeningForUpdates();
|
||||
ModelUpdater.UpdateAction = null;
|
||||
ModelUpdater = null;
|
||||
if (ModelUpdater != null) {
|
||||
ModelUpdater.StopListeningForUpdates();
|
||||
ModelUpdater.UpdateAction = null;
|
||||
ModelUpdater = null;
|
||||
}
|
||||
SwapModel();
|
||||
Model = null;
|
||||
chronicleCanvasController.Entries = new List<ChronicleEntry>();
|
||||
@@ -281,6 +284,13 @@ namespace eagle {
|
||||
MainQueue.Q.EnqueueForNextUpdate(
|
||||
() => { _ = ModelUpdater.StartListeningForUpdates(); });
|
||||
|
||||
// Initialize tutorial system
|
||||
TutorialManager.Instance?.Initialize(this, null);
|
||||
if (TutorialManager.Instance != null &&
|
||||
!TutorialManager.Instance.State.OnboardingCompleted) {
|
||||
TutorialManager.Instance.StartOnboarding();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.playModeStateChanged += (state) => OnPlayModeStateChanged(state);
|
||||
#endif
|
||||
@@ -362,6 +372,12 @@ namespace eagle {
|
||||
SetNextActiveProvinceButton();
|
||||
_dominionPanelController.ForceUpdate();
|
||||
SetMusic();
|
||||
|
||||
// Notify tutorial system of province selection
|
||||
if (pid.HasValue) {
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnProvinceSelected(
|
||||
Model.Provinces[pid.Value]);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrefetchHeadshotForHeroes(List<HeroView> heroViews) {
|
||||
@@ -477,6 +493,9 @@ namespace eagle {
|
||||
var oldModel = Model;
|
||||
Model = _newModel;
|
||||
|
||||
// Notify tutorial system of model change
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnModelUpdated(Model, oldModel);
|
||||
|
||||
provinceInfoPanelController.Model = _newModel;
|
||||
freeHeroesTableController.Model = _newModel;
|
||||
movingArmiesTableController.Model = _newModel;
|
||||
@@ -625,6 +644,9 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void PostCommittedCommand(ProvinceId provinceId, SelectedCommand selectedCommand) {
|
||||
// Notify tutorial system of command
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnCommandIssued(selectedCommand);
|
||||
|
||||
ModelUpdater.PostCommand(provinceId: provinceId, command: selectedCommand)
|
||||
.ContinueWith(response => {
|
||||
if (response.IsFaulted) { errorHandler.Add(response.Exception); }
|
||||
|
||||
+8
-8
@@ -6,7 +6,7 @@ TextureImporter:
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
@@ -37,13 +37,13 @@ TextureImporter:
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
@@ -52,9 +52,9 @@ TextureImporter:
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
@@ -100,7 +100,7 @@ TextureImporter:
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
|
||||
+8
-8
@@ -6,7 +6,7 @@ TextureImporter:
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
@@ -37,13 +37,13 @@ TextureImporter:
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
@@ -52,9 +52,9 @@ TextureImporter:
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
@@ -100,7 +100,7 @@ TextureImporter:
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,9 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using common;
|
||||
using eagle;
|
||||
using Eagle0.Tutorial;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using Shardok;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
@@ -28,6 +30,9 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
public Toggle tooltipHugsCursorToggle;
|
||||
public HoveringTooltip hoveringTooltip;
|
||||
|
||||
public Toggle showAnimationTestsToggle;
|
||||
public AnimationTestController animationTestController;
|
||||
|
||||
public Slider autoScrollSpeedSlider;
|
||||
public TMP_Text autoScrollSpeedLabel;
|
||||
|
||||
@@ -39,11 +44,14 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
private const string MusicVolumeKey = "musicVolume";
|
||||
public const string UseGoDiceKey = "useGoDice";
|
||||
private const string TooltipHugsCursorKey = "tooltipHugsCursor";
|
||||
private const string AnimationToggleKey = "animationToggle";
|
||||
|
||||
public EagleGameController eagleGameController;
|
||||
public GameObject ShardokCanvas;
|
||||
public ConnectionHandler connectionHandler;
|
||||
|
||||
public Button resetTutorialsButton;
|
||||
|
||||
void Start() {
|
||||
_active = false;
|
||||
panel.SetActive(false);
|
||||
@@ -59,6 +67,9 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
tooltipHugsCursorToggle.isOn = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
|
||||
hoveringTooltip.HugsCursor = tooltipHugsCursorToggle.isOn;
|
||||
|
||||
showAnimationTestsToggle.isOn = PlayerPrefs.GetInt(AnimationToggleKey, 0) == 1;
|
||||
animationTestController.gameObject.SetActive(showAnimationTestsToggle.isOn);
|
||||
|
||||
if (autoScrollSpeedSlider != null) {
|
||||
autoScrollSpeedSlider.value = AutoScrollingText.GlobalScrollSpeedMultiplier;
|
||||
UpdateAutoScrollSpeedLabel(autoScrollSpeedSlider.value);
|
||||
@@ -116,6 +127,11 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
hoveringTooltip.HugsCursor = val;
|
||||
}
|
||||
|
||||
public void OnAnimationTestToggleChange(bool val) {
|
||||
PlayerPrefs.SetInt(AnimationToggleKey, val ? 1 : 0);
|
||||
animationTestController.gameObject.SetActive(val);
|
||||
}
|
||||
|
||||
public void OnAutoScrollSpeedSliderChange(float val) {
|
||||
AutoScrollingText.GlobalScrollSpeedMultiplier = val;
|
||||
UpdateAutoScrollSpeedLabel(val);
|
||||
@@ -135,6 +151,11 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
Process.Start(Path.Combine(Application.persistentDataPath, "eagle0", "Resources"));
|
||||
}
|
||||
|
||||
public void OnResetTutorialsClick() {
|
||||
TutorialManager.Instance?.ResetAllProgress();
|
||||
UnityEngine.Debug.Log("Tutorial progress reset");
|
||||
}
|
||||
|
||||
private void HandleLobby() {
|
||||
eagleGameController.StopAll();
|
||||
eagleGameController.gameObject.SetActive(false);
|
||||
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Shardok/Animation Sprites/Gradient Circle.png
LFS
BIN
Binary file not shown.
+233
@@ -0,0 +1,233 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00630274bf61c4899aa743c3fc3d7ccc
|
||||
TextureImporter:
|
||||
internalIDToNameTable:
|
||||
- first:
|
||||
213: -2413806693520163455
|
||||
second: Circle
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 2
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 256
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: 0
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: iOS
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites:
|
||||
- serializedVersion: 2
|
||||
name: Circle
|
||||
rect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 256
|
||||
height: 256
|
||||
alignment: 0
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
customData:
|
||||
outline:
|
||||
- - {x: 33, y: 128}
|
||||
- {x: -33, y: 128}
|
||||
- {x: -95, y: 95}
|
||||
- {x: -128, y: 33}
|
||||
- {x: -128, y: -33}
|
||||
- {x: -95, y: -95}
|
||||
- {x: -33, y: -128}
|
||||
- {x: 33, y: -128}
|
||||
- {x: 95, y: -95}
|
||||
- {x: 128, y: -33}
|
||||
- {x: 128, y: 33}
|
||||
- {x: 95, y: 95}
|
||||
physicsShape:
|
||||
- - {x: 0, y: 128}
|
||||
- {x: -39, y: 121}
|
||||
- {x: -75, y: 103}
|
||||
- {x: -103, y: 75}
|
||||
- {x: -121, y: 39}
|
||||
- {x: -128, y: 0}
|
||||
- {x: -121, y: -39}
|
||||
- {x: -103, y: -75}
|
||||
- {x: -75, y: -103}
|
||||
- {x: -39, y: -121}
|
||||
- {x: 0, y: -128}
|
||||
- {x: 39, y: -121}
|
||||
- {x: 75, y: -103}
|
||||
- {x: 103, y: -75}
|
||||
- {x: 121, y: -39}
|
||||
- {x: 128, y: 0}
|
||||
- {x: 121, y: 39}
|
||||
- {x: 103, y: 75}
|
||||
- {x: 75, y: 103}
|
||||
- {x: 39, y: 121}
|
||||
tessellationDetail: 0
|
||||
bones: []
|
||||
spriteID: 18d3544e99f608ed0800000000000000
|
||||
internalID: -2413806693520163455
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
outline:
|
||||
- - {x: 33, y: 128}
|
||||
- {x: -33, y: 128}
|
||||
- {x: -95, y: 95}
|
||||
- {x: -128, y: 33}
|
||||
- {x: -128, y: -33}
|
||||
- {x: -95, y: -95}
|
||||
- {x: -33, y: -128}
|
||||
- {x: 33, y: -128}
|
||||
- {x: 95, y: -95}
|
||||
- {x: 128, y: -33}
|
||||
- {x: 128, y: 33}
|
||||
- {x: 95, y: 95}
|
||||
customData:
|
||||
physicsShape:
|
||||
- - {x: 0, y: 128}
|
||||
- {x: -39, y: 121}
|
||||
- {x: -75, y: 103}
|
||||
- {x: -103, y: 75}
|
||||
- {x: -121, y: 39}
|
||||
- {x: -128, y: 0}
|
||||
- {x: -121, y: -39}
|
||||
- {x: -103, y: -75}
|
||||
- {x: -75, y: -103}
|
||||
- {x: -39, y: -121}
|
||||
- {x: 0, y: -128}
|
||||
- {x: 39, y: -121}
|
||||
- {x: 75, y: -103}
|
||||
- {x: 103, y: -75}
|
||||
- {x: 121, y: -39}
|
||||
- {x: 128, y: 0}
|
||||
- {x: 121, y: 39}
|
||||
- {x: 103, y: 75}
|
||||
- {x: 75, y: 103}
|
||||
- {x: 39, y: 121}
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable:
|
||||
Circle: -2413806693520163455
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
+169
@@ -0,0 +1,169 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32f7f3d380ec1413590676a13c22324c
|
||||
TextureImporter:
|
||||
internalIDToNameTable:
|
||||
- first:
|
||||
213: 7482667652216324306
|
||||
second: Square
|
||||
externalObjects: {}
|
||||
serializedVersion: 11
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMasterTextureLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 2
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 256
|
||||
spriteBorder: {x: 4, y: 4, z: 4, w: 4}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: 0
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites:
|
||||
- serializedVersion: 2
|
||||
name: Triangle
|
||||
rect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 256
|
||||
height: 256
|
||||
alignment: 9
|
||||
pivot: {x: 0.5, y: 0.28866667}
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
outline:
|
||||
- - {x: 0, y: 93.7025033688}
|
||||
- {x: -128, y: -128}
|
||||
- {x: 128, y: -128}
|
||||
physicsShape:
|
||||
- - {x: 0, y: 93.7025033688}
|
||||
- {x: -128, y: -128}
|
||||
- {x: 128, y: -128}
|
||||
tessellationDetail: 0
|
||||
bones: []
|
||||
spriteID: 2d009a6b596c7d760800000000000000
|
||||
internalID: 7482667652216324306
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
outline: []
|
||||
physicsShape:
|
||||
- - {x: -128, y: 128}
|
||||
- {x: -128, y: -128}
|
||||
- {x: 128, y: -128}
|
||||
- {x: 128, y: 128}
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable:
|
||||
Triangle: 7482667652216324306
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
+117
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7fee21f21c4f146f69dd2eae14a94b4b
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
+117
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfa8cdc5e73d54ed1876c17a47ff7346
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
+117
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd23b09c6f27b4754a769858a2020668
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
LFS
BIN
Binary file not shown.
+117
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fe839b58f5fd46d695ece8ccf2412a5
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+235
-3
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
@@ -24,6 +25,19 @@ namespace Shardok {
|
||||
private CatapultAnimator _catapultAnimator;
|
||||
private RaiseDeadAnimator _raiseDeadAnimator;
|
||||
private MeteorAnimator _meteorAnimator;
|
||||
private HolyWaveAnimator _holyWaveAnimator;
|
||||
private ChargeAnimator _chargeAnimator;
|
||||
private ToolAnimator _toolAnimator;
|
||||
private FearAnimator _fearAnimator;
|
||||
private ControlAnimator _controlAnimator;
|
||||
private FireEffectAnimator _fireEffectAnimator;
|
||||
private ExtinguishAnimator _extinguishAnimator;
|
||||
private FreezeAnimator _freezeAnimator;
|
||||
private WaterEffectAnimator _waterEffectAnimator;
|
||||
private ScoutAnimator _scoutAnimator;
|
||||
private DismissAnimator _dismissAnimator;
|
||||
private FleeAnimator _fleeAnimator;
|
||||
private DuelAnimator _duelAnimator;
|
||||
private AudioSource _audioSource;
|
||||
private SoundManager _soundManager;
|
||||
|
||||
@@ -40,6 +54,19 @@ namespace Shardok {
|
||||
_catapultAnimator = gameController.catapultAnimator;
|
||||
_raiseDeadAnimator = gameController.raiseDeadAnimator;
|
||||
_meteorAnimator = gameController.meteorAnimator;
|
||||
_holyWaveAnimator = gameController.holyWaveAnimator;
|
||||
_chargeAnimator = gameController.chargeAnimator;
|
||||
_toolAnimator = gameController.toolAnimator;
|
||||
_fearAnimator = gameController.fearAnimator;
|
||||
_controlAnimator = gameController.controlAnimator;
|
||||
_fireEffectAnimator = gameController.fireEffectAnimator;
|
||||
_extinguishAnimator = gameController.extinguishAnimator;
|
||||
_freezeAnimator = gameController.freezeAnimator;
|
||||
_waterEffectAnimator = gameController.waterEffectAnimator;
|
||||
_scoutAnimator = gameController.scoutAnimator;
|
||||
_dismissAnimator = gameController.dismissAnimator;
|
||||
_fleeAnimator = gameController.fleeAnimator;
|
||||
_duelAnimator = gameController.duelAnimator;
|
||||
_audioSource = gameController.audioClipSource;
|
||||
_soundManager = gameController.soundManager;
|
||||
} else {
|
||||
@@ -99,13 +126,30 @@ namespace Shardok {
|
||||
Debug.Log("Melee sequence complete!");
|
||||
}
|
||||
|
||||
public void TestMove() {
|
||||
Debug.Log($"Testing Move: {sourceCell} -> {targetCell}");
|
||||
public void TestMove() { StartCoroutine(TestMoveSequence()); }
|
||||
|
||||
private IEnumerator TestMoveSequence() {
|
||||
Debug.Log($"Testing Move (Infantry): {sourceCell} -> {targetCell}");
|
||||
if (_moveAnimator != null) {
|
||||
_moveAnimator.AnimateMove(sourceCell, targetCell);
|
||||
_moveAnimator.AnimateMove(
|
||||
sourceCell,
|
||||
targetCell,
|
||||
Net.Eagle0.Shardok.Api.BattalionView.Types.BattalionTypeId.LightInfantry);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.Move);
|
||||
} else {
|
||||
Debug.LogWarning("MoveAnimator not available");
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
Debug.Log($"Testing Move (Cavalry): {sourceCell} -> {targetCell}");
|
||||
if (_moveAnimator != null) {
|
||||
_moveAnimator.AnimateMove(
|
||||
sourceCell,
|
||||
targetCell,
|
||||
Net.Eagle0.Shardok.Api.BattalionView.Types.BattalionTypeId.LightCavalry);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.Move);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +193,152 @@ namespace Shardok {
|
||||
}
|
||||
}
|
||||
|
||||
public void TestHolyWave() {
|
||||
Debug.Log($"Testing Holy Wave from: {sourceCell}");
|
||||
if (_holyWaveAnimator != null) {
|
||||
// Test with some simulated undead positions nearby
|
||||
var undeadCells = new System.Collections.Generic.List<int> { targetCell };
|
||||
_holyWaveAnimator.AnimateHolyWave(sourceCell, undeadCells);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.InspiredTroops);
|
||||
} else {
|
||||
Debug.LogWarning("HolyWaveAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestCharge() {
|
||||
Debug.Log($"Testing Charge: {sourceCell} -> {targetCell}");
|
||||
if (_chargeAnimator != null) {
|
||||
_chargeAnimator.AnimateCharge(
|
||||
sourceCell,
|
||||
targetCell,
|
||||
Net.Eagle0.Shardok.Api.BattalionView.Types.BattalionTypeId.HeavyCavalry,
|
||||
Net.Eagle0.Shardok.Api.BattalionView.Types.BattalionTypeId.HeavyInfantry);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.ChargeAttack);
|
||||
} else {
|
||||
Debug.LogWarning("ChargeAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestRepair() {
|
||||
Debug.Log($"Testing Repair at: {targetCell}");
|
||||
if (_toolAnimator != null) {
|
||||
_toolAnimator.AnimateRepair(targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.Repair);
|
||||
} else {
|
||||
Debug.LogWarning("ToolAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestBuildBridge() {
|
||||
Debug.Log($"Testing BuildBridge at: {targetCell}");
|
||||
if (_toolAnimator != null) {
|
||||
_toolAnimator.AnimateBuildBridge(targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.BuildBridge);
|
||||
} else {
|
||||
Debug.LogWarning("ToolAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestFear() {
|
||||
Debug.Log($"Testing Fear: {sourceCell} -> {targetCell}");
|
||||
if (_fearAnimator != null) {
|
||||
_fearAnimator.AnimateFear(sourceCell, targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.FearAttack);
|
||||
} else {
|
||||
Debug.LogWarning("FearAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestControl() {
|
||||
Debug.Log($"Testing Control: {sourceCell} -> {targetCell}");
|
||||
if (_controlAnimator != null) {
|
||||
_controlAnimator.AnimateControl(sourceCell, targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.Controlled);
|
||||
} else {
|
||||
Debug.LogWarning("ControlAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestFire() {
|
||||
Debug.Log($"Testing Fire at: {targetCell}");
|
||||
if (_fireEffectAnimator != null) {
|
||||
_fireEffectAnimator.AnimateFire(targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.StartFire);
|
||||
} else {
|
||||
Debug.LogWarning("FireEffectAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestExtinguish() {
|
||||
Debug.Log($"Testing Extinguish at: {targetCell}");
|
||||
if (_extinguishAnimator != null) {
|
||||
_extinguishAnimator.AnimateExtinguish(targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.ExtinguishFire);
|
||||
} else {
|
||||
Debug.LogWarning("ExtinguishAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestFreeze() {
|
||||
Debug.Log($"Testing Freeze at: {targetCell}");
|
||||
if (_freezeAnimator != null) {
|
||||
_freezeAnimator.AnimateFreeze(targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.FreezeWater);
|
||||
} else {
|
||||
Debug.LogWarning("FreezeAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestWaterSplash() {
|
||||
Debug.Log($"Testing Water Splash at: {targetCell}");
|
||||
if (_waterEffectAnimator != null) {
|
||||
_waterEffectAnimator.AnimateWaterSplash(targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.CrossedWater);
|
||||
} else {
|
||||
Debug.LogWarning("WaterEffectAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestScout() {
|
||||
Debug.Log($"Testing Scout: {sourceCell} -> {targetCell}");
|
||||
if (_scoutAnimator != null) {
|
||||
_scoutAnimator.AnimateScout(sourceCell, targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.Scouted);
|
||||
} else {
|
||||
Debug.LogWarning("ScoutAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestDismiss() {
|
||||
Debug.Log($"Testing Dismiss at: {targetCell}");
|
||||
if (_dismissAnimator != null) {
|
||||
_dismissAnimator.AnimateDismiss(targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.DismissUnit);
|
||||
} else {
|
||||
Debug.LogWarning("DismissAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestFlee() {
|
||||
Debug.Log($"Testing Flee: {sourceCell} -> {targetCell}");
|
||||
if (_fleeAnimator != null) {
|
||||
_fleeAnimator.AnimateFlee(sourceCell, targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.Move);
|
||||
} else {
|
||||
Debug.LogWarning("FleeAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestDuel() {
|
||||
Debug.Log($"Testing Duel: {sourceCell} -> {targetCell}");
|
||||
if (_duelAnimator != null) {
|
||||
_duelAnimator.AnimateDuel(sourceCell, targetCell);
|
||||
PlaySound(Net.Eagle0.Shardok.Common.ActionType.MeleeAttack);
|
||||
} else {
|
||||
Debug.LogWarning("DuelAnimator not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void TestAll() {
|
||||
Debug.Log("Testing all animations in sequence...");
|
||||
StartCoroutine(TestAllSequence());
|
||||
@@ -174,6 +364,48 @@ namespace Shardok {
|
||||
yield return new WaitForSeconds(2.0f);
|
||||
|
||||
TestMeteor();
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
TestHolyWave();
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
TestCharge();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
TestRepair();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
TestBuildBridge();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
TestFear();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
TestControl();
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
TestFire();
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
TestExtinguish();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
TestFreeze();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
TestWaterSplash();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
TestScout();
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
TestDismiss();
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
TestFlee();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
TestDuel();
|
||||
Debug.Log("All animations tested!");
|
||||
}
|
||||
|
||||
|
||||
+7
-16
@@ -38,15 +38,10 @@ namespace Shardok {
|
||||
[Tooltip("Time spread for staggered launch")]
|
||||
public float launchSpread = 0.1f;
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
private readonly List<Coroutine> _activeCoroutines = new List<Coroutine>();
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
}
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts an arrow volley animation between two hex cells.
|
||||
@@ -59,13 +54,13 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("ArrowVolleyAnimator: No hexGrid or gridCanvas available");
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("ArrowVolleyAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = hexGrid.GetCellLocalPosition(sourceCellIndex);
|
||||
Vector2? targetPos = hexGrid.GetCellLocalPosition(targetCellIndex);
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
@@ -73,14 +68,10 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"ArrowVolleyAnimator: Starting volley from cell {sourceCellIndex} to {targetCellIndex}");
|
||||
StartCoroutine(SpawnArrowVolley(sourcePos.Value, targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator SpawnArrowVolley(Vector2 sourcePosition, Vector2 targetPosition) {
|
||||
Debug.Log(
|
||||
$"ArrowVolleyAnimator: Spawning {arrowCount} arrows from {sourcePosition} to {targetPosition}");
|
||||
var arrows = new List<GameObject>();
|
||||
|
||||
for (int i = 0; i < arrowCount; i++) {
|
||||
@@ -122,7 +113,7 @@ namespace Shardok {
|
||||
// Create arrow as child of gridCanvas, using SpriteRenderer for world-space rendering
|
||||
// This matches how particle effects work on the rotated canvas
|
||||
GameObject arrow = new GameObject("Arrow");
|
||||
arrow.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
arrow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
// Use SpriteRenderer instead of UI Image
|
||||
SpriteRenderer renderer = arrow.AddComponent<SpriteRenderer>();
|
||||
|
||||
@@ -40,13 +40,9 @@ namespace Shardok {
|
||||
[Tooltip("Duration of impact explosion in seconds")]
|
||||
public float impactDuration = 0.3f;
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
}
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a catapult animation between two hex cells.
|
||||
@@ -58,13 +54,13 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("CatapultAnimator: No hexGrid or gridCanvas available");
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("CatapultAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = hexGrid.GetCellLocalPosition(sourceCellIndex);
|
||||
Vector2? targetPos = hexGrid.GetCellLocalPosition(targetCellIndex);
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
@@ -78,7 +74,7 @@ namespace Shardok {
|
||||
private IEnumerator AnimateProjectile(Vector2 source, Vector2 target) {
|
||||
// Create rock projectile
|
||||
GameObject rock = new GameObject("CatapultRock");
|
||||
rock.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
rock.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer rockRenderer = rock.AddComponent<SpriteRenderer>();
|
||||
rockRenderer.sprite = rockSprite;
|
||||
@@ -115,7 +111,7 @@ namespace Shardok {
|
||||
// Impact explosion
|
||||
if (explosionSprite != null) {
|
||||
GameObject explosion = new GameObject("CatapultExplosion");
|
||||
explosion.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
explosion.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
explosion.transform.localPosition = new Vector3(target.x, target.y, 5f);
|
||||
|
||||
SpriteRenderer explosionRenderer = explosion.AddComponent<SpriteRenderer>();
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
using System.Collections;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using UnityEngine;
|
||||
|
||||
using static Net.Eagle0.Shardok.Api.BattalionView.Types;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates charge attacks between two hexes.
|
||||
/// Shows attacker's weapon thrusting toward defender with accelerating motion
|
||||
/// and a violent flash on impact.
|
||||
/// </summary>
|
||||
public class ChargeAnimator : MonoBehaviour {
|
||||
[Header("Weapon Sprites")]
|
||||
[Tooltip("Sword sprite for light infantry charge")]
|
||||
public Sprite swordSprite;
|
||||
|
||||
[Tooltip("Mace/hammer sprite for heavy infantry charge")]
|
||||
public Sprite maceSprite;
|
||||
|
||||
[Tooltip("Small spear sprite for light cavalry charge")]
|
||||
public Sprite smallSpearSprite;
|
||||
|
||||
[Tooltip("Large lance sprite for heavy cavalry charge")]
|
||||
public Sprite largeLanceSprite;
|
||||
|
||||
[Tooltip("Dagger sprite for longbowmen charge")]
|
||||
public Sprite daggerSprite;
|
||||
|
||||
[Tooltip("Bone/claw sprite for undead charge")]
|
||||
public Sprite boneSprite;
|
||||
|
||||
[Header("Impact Flash Settings")]
|
||||
[Tooltip("Sprite for impact flash")]
|
||||
public Sprite flashSprite;
|
||||
|
||||
[Tooltip("Color of impact flash")]
|
||||
public Color flashColor = new Color(1f, 1f, 0.8f, 0.9f);
|
||||
|
||||
[Tooltip("Scale of impact flash")]
|
||||
public float flashScale = 35f;
|
||||
|
||||
[Tooltip("Duration of flash")]
|
||||
public float flashDuration = 0.1f;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Base scale of weapon sprites")]
|
||||
public float weaponScale = 25.0f;
|
||||
|
||||
[Tooltip("Number of thrusts")]
|
||||
public int thrustCount = 2;
|
||||
|
||||
[Tooltip("Duration of each thrust (slow to fast)")]
|
||||
public float thrustDuration = 0.25f;
|
||||
|
||||
[Tooltip("Duration of pullback between thrusts")]
|
||||
public float pullbackDuration = 0.15f;
|
||||
|
||||
[Tooltip("Starting position offset from midpoint (in local units)")]
|
||||
public float startOffset = 60f;
|
||||
|
||||
[Tooltip("End position offset from midpoint (in local units)")]
|
||||
public float endOffset = 15f;
|
||||
|
||||
[Tooltip("Base rotation offset for diagonal sprites (degrees)")]
|
||||
public float baseRotationOffset = -45f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
private Coroutine _activeAnimation;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a charge animation between two hex cells.
|
||||
/// </summary>
|
||||
public void AnimateCharge(
|
||||
int attackerCellIndex,
|
||||
int defenderCellIndex,
|
||||
BattalionTypeId attackerType,
|
||||
BattalionTypeId defenderType) {
|
||||
if (swordSprite == null && maceSprite == null && smallSpearSprite == null &&
|
||||
largeLanceSprite == null && daggerSprite == null && boneSprite == null) {
|
||||
Debug.LogWarning("ChargeAnimator: No weapon sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("ChargeAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? attackerPos = __hexGrid.GetCellCenterPosition(attackerCellIndex);
|
||||
Vector2? defenderPos = __hexGrid.GetCellCenterPosition(defenderCellIndex);
|
||||
|
||||
if (attackerPos == null || defenderPos == null) {
|
||||
Debug.LogWarning(
|
||||
$"ChargeAnimator: Invalid cell indices {attackerCellIndex} or {defenderCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_activeAnimation != null) { StopCoroutine(_activeAnimation); }
|
||||
|
||||
_activeAnimation = StartCoroutine(
|
||||
AnimateChargeThrusts(attackerPos.Value, defenderPos.Value, attackerType));
|
||||
}
|
||||
|
||||
private Sprite GetWeaponSprite(BattalionTypeId type) {
|
||||
switch (type) {
|
||||
case BattalionTypeId.LightInfantry: return swordSprite;
|
||||
case BattalionTypeId.HeavyInfantry: return maceSprite ?? swordSprite;
|
||||
case BattalionTypeId.LightCavalry:
|
||||
return smallSpearSprite ?? largeLanceSprite ?? swordSprite;
|
||||
case BattalionTypeId.HeavyCavalry:
|
||||
return largeLanceSprite ?? smallSpearSprite ?? swordSprite;
|
||||
case BattalionTypeId.Longbowmen: return daggerSprite ?? swordSprite;
|
||||
case BattalionTypeId.Undead: return boneSprite ?? swordSprite;
|
||||
default: return swordSprite;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetScaleMultiplier(BattalionTypeId type) {
|
||||
switch (type) {
|
||||
case BattalionTypeId.HeavyInfantry: return 1.2f;
|
||||
case BattalionTypeId.LightCavalry: return 1.1f;
|
||||
case BattalionTypeId.HeavyCavalry: return 1.4f;
|
||||
case BattalionTypeId.Longbowmen: return 0.8f;
|
||||
default: return 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator AnimateChargeThrusts(
|
||||
Vector2 attackerPos,
|
||||
Vector2 defenderPos,
|
||||
BattalionTypeId attackerType) {
|
||||
// Calculate direction toward defender
|
||||
Vector2 midpoint = (attackerPos + defenderPos) / 2f;
|
||||
Vector2 direction = (defenderPos - attackerPos).normalized;
|
||||
float baseAngle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
|
||||
|
||||
// Create attacker weapon
|
||||
Sprite weaponSprite = GetWeaponSprite(attackerType);
|
||||
float scale = weaponScale * GetScaleMultiplier(attackerType);
|
||||
|
||||
GameObject weapon = new GameObject("ChargeWeapon");
|
||||
weapon.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = weapon.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = weaponSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
|
||||
weapon.transform.localRotation = Quaternion.Euler(0, 0, baseAngle + baseRotationOffset);
|
||||
weapon.transform.localScale = new Vector3(scale, scale, scale);
|
||||
|
||||
// Calculate positions
|
||||
Vector3 startPos = new Vector3(
|
||||
midpoint.x - direction.x * startOffset,
|
||||
midpoint.y - direction.y * startOffset,
|
||||
10f);
|
||||
Vector3 endPos = new Vector3(
|
||||
midpoint.x + direction.x * endOffset,
|
||||
midpoint.y + direction.y * endOffset,
|
||||
10f);
|
||||
|
||||
weapon.transform.localPosition = startPos;
|
||||
|
||||
// Animate thrusts
|
||||
for (int i = 0; i < thrustCount; i++) {
|
||||
// Thrust forward - starts slow, accelerates
|
||||
float elapsed = 0f;
|
||||
while (elapsed < thrustDuration) {
|
||||
if (weapon == null) yield break;
|
||||
|
||||
float t = elapsed / thrustDuration;
|
||||
// Cubic ease-in for accelerating thrust
|
||||
float easeT = t * t * t;
|
||||
|
||||
weapon.transform.localPosition = Vector3.Lerp(startPos, endPos, easeT);
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
weapon.transform.localPosition = endPos;
|
||||
|
||||
// Impact flash
|
||||
if (flashSprite != null) {
|
||||
yield return StartCoroutine(ShowImpactFlash(endPos, direction));
|
||||
}
|
||||
|
||||
// Pullback - smooth ease out
|
||||
elapsed = 0f;
|
||||
while (elapsed < pullbackDuration) {
|
||||
if (weapon == null) yield break;
|
||||
|
||||
float t = elapsed / pullbackDuration;
|
||||
// Ease out for smooth pullback
|
||||
float easeT = t * (2f - t);
|
||||
|
||||
weapon.transform.localPosition = Vector3.Lerp(endPos, startPos, easeT);
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
weapon.transform.localPosition = startPos;
|
||||
|
||||
// Brief pause between thrusts
|
||||
if (i < thrustCount - 1) { yield return new WaitForSeconds(0.05f); }
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (weapon != null) { Destroy(weapon); }
|
||||
_activeAnimation = null;
|
||||
}
|
||||
|
||||
private IEnumerator ShowImpactFlash(Vector3 position, Vector2 direction) {
|
||||
GameObject flash = new GameObject("ChargeFlash");
|
||||
flash.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
flash.transform.localPosition = position;
|
||||
|
||||
SpriteRenderer renderer = flash.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = flashSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = flashColor;
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < flashDuration) {
|
||||
float t = elapsed / flashDuration;
|
||||
|
||||
// Quick expand then fade
|
||||
float expandT = t < 0.3f ? t / 0.3f : 1f;
|
||||
float scale = flashScale * (0.5f + 0.5f * expandT);
|
||||
flash.transform.localScale = Vector3.one * scale;
|
||||
|
||||
// Fade out
|
||||
Color c = flashColor;
|
||||
c.a = flashColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Destroy(flash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any active charge animation.
|
||||
/// </summary>
|
||||
public void CancelAnimation() {
|
||||
if (_activeAnimation != null) {
|
||||
StopCoroutine(_activeAnimation);
|
||||
_activeAnimation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52d8c0b9135074d4eaa11cb4457185e3
|
||||
@@ -0,0 +1,274 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates mind control with a pulsing beam from source to target.
|
||||
/// Creates a mystical connection effect with spiraling particles.
|
||||
/// </summary>
|
||||
public class ControlAnimator : MonoBehaviour {
|
||||
[Header("Beam Settings")]
|
||||
[Tooltip("Sprite for the beam (elongated gradient works well)")]
|
||||
public Sprite beamSprite;
|
||||
|
||||
[Tooltip("Color of the control beam")]
|
||||
public Color beamColor = new Color(0.8f, 0.2f, 0.9f, 0.7f);
|
||||
|
||||
[Tooltip("Width of the beam")]
|
||||
public float beamWidth = 8f;
|
||||
|
||||
[Header("Particle Settings")]
|
||||
[Tooltip("Sprite for spiraling particles")]
|
||||
public Sprite particleSprite;
|
||||
|
||||
[Tooltip("Number of particles along the beam")]
|
||||
public int particleCount = 6;
|
||||
|
||||
[Tooltip("Color of particles")]
|
||||
public Color particleColor = new Color(1f, 0.5f, 1f, 0.9f);
|
||||
|
||||
[Header("Impact Settings")]
|
||||
[Tooltip("Sprite for impact ring at target")]
|
||||
public Sprite impactSprite;
|
||||
|
||||
[Tooltip("Color of impact")]
|
||||
public Color impactColor = new Color(0.9f, 0.3f, 1f, 0.8f);
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration of the beam formation")]
|
||||
public float beamDuration = 0.4f;
|
||||
|
||||
[Tooltip("Duration of the pulse effect")]
|
||||
public float pulseDuration = 0.3f;
|
||||
|
||||
[Tooltip("Number of pulse waves")]
|
||||
public int pulseCount = 2;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a mind control animation from source to target hex.
|
||||
/// </summary>
|
||||
public void AnimateControl(int sourceCellIndex, int targetCellIndex) {
|
||||
if (beamSprite == null && particleSprite == null) {
|
||||
Debug.LogWarning("ControlAnimator: No sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("ControlAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
$"ControlAnimator: Invalid cell indices {sourceCellIndex} or {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateControlBeam(sourcePos.Value, targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateControlBeam(Vector2 source, Vector2 target) {
|
||||
Vector2 direction = (target - source).normalized;
|
||||
float distance = Vector2.Distance(source, target);
|
||||
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
|
||||
|
||||
// Create beam
|
||||
GameObject beam = null;
|
||||
SpriteRenderer beamRenderer = null;
|
||||
|
||||
if (beamSprite != null) {
|
||||
beam = new GameObject("ControlBeam");
|
||||
beam.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
beamRenderer = beam.AddComponent<SpriteRenderer>();
|
||||
beamRenderer.sprite = beamSprite;
|
||||
beamRenderer.sortingLayerName = "Effects";
|
||||
beamRenderer.color = beamColor;
|
||||
|
||||
// Position at midpoint, rotate toward target
|
||||
Vector2 midpoint = (source + target) / 2f;
|
||||
beam.transform.localPosition = new Vector3(midpoint.x, midpoint.y, 6f);
|
||||
beam.transform.localRotation = Quaternion.Euler(0, 0, angle);
|
||||
beam.transform.localScale = new Vector3(0f, beamWidth, 1f);
|
||||
}
|
||||
|
||||
// Create spiraling particles
|
||||
GameObject[] particles = new GameObject[particleCount];
|
||||
float[] particlePhases = new float[particleCount];
|
||||
|
||||
for (int i = 0; i < particleCount; i++) {
|
||||
if (particleSprite == null) break;
|
||||
|
||||
GameObject particle = new GameObject($"ControlParticle_{i}");
|
||||
particle.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = particle.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = particleSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = particleColor;
|
||||
particle.transform.localScale = Vector3.one * 6f;
|
||||
|
||||
particlePhases[i] = (float)i / particleCount * Mathf.PI * 2f;
|
||||
particles[i] = particle;
|
||||
}
|
||||
|
||||
// Beam formation phase
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < beamDuration) {
|
||||
float t = elapsed / beamDuration;
|
||||
float easeT = t * t * (3f - 2f * t);
|
||||
|
||||
// Extend beam
|
||||
if (beam != null) {
|
||||
float beamLength = distance * easeT;
|
||||
beam.transform.localScale = new Vector3(beamLength, beamWidth, 1f);
|
||||
|
||||
// Slight pulse in width
|
||||
float pulse = 1f + Mathf.Sin(t * Mathf.PI * 4) * 0.1f;
|
||||
beam.transform.localScale = new Vector3(beamLength, beamWidth * pulse, 1f);
|
||||
}
|
||||
|
||||
// Animate particles spiraling along beam
|
||||
for (int i = 0; i < particles.Length; i++) {
|
||||
if (particles[i] == null) continue;
|
||||
|
||||
float particleT = (float)i / particleCount * easeT;
|
||||
Vector2 pos = Vector2.Lerp(source, target, particleT);
|
||||
|
||||
// Spiral offset
|
||||
float spiralAngle = particlePhases[i] + elapsed * 15f;
|
||||
float spiralRadius = 10f * (1f - Mathf.Abs(particleT - 0.5f) * 2f);
|
||||
pos += new Vector2(
|
||||
Mathf.Cos(spiralAngle) * spiralRadius * -direction.y +
|
||||
Mathf.Sin(spiralAngle) * spiralRadius * direction.x,
|
||||
Mathf.Cos(spiralAngle) * spiralRadius * direction.x +
|
||||
Mathf.Sin(spiralAngle) * spiralRadius * direction.y);
|
||||
|
||||
particles[i].transform.localPosition = new Vector3(pos.x, pos.y, 5f);
|
||||
|
||||
// Fade based on position
|
||||
var renderer = particles[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = particleColor;
|
||||
c.a = particleColor.a * easeT;
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Create impact effect at target
|
||||
GameObject impact = null;
|
||||
SpriteRenderer impactRenderer = null;
|
||||
|
||||
if (impactSprite != null) {
|
||||
impact = new GameObject("ControlImpact");
|
||||
impact.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
impact.transform.localPosition = new Vector3(target.x, target.y, 5f);
|
||||
|
||||
impactRenderer = impact.AddComponent<SpriteRenderer>();
|
||||
impactRenderer.sprite = impactSprite;
|
||||
impactRenderer.sortingLayerName = "Effects";
|
||||
impactRenderer.color = impactColor;
|
||||
impact.transform.localScale = Vector3.one * 10f;
|
||||
}
|
||||
|
||||
// Pulse phase
|
||||
for (int p = 0; p < pulseCount; p++) {
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < pulseDuration) {
|
||||
float t = elapsed / pulseDuration;
|
||||
|
||||
// Beam pulsing
|
||||
if (beam != null) {
|
||||
float pulse = 1f + Mathf.Sin(t * Mathf.PI * 2) * 0.3f;
|
||||
beam.transform.localScale = new Vector3(distance, beamWidth * pulse, 1f);
|
||||
|
||||
Color c = beamColor;
|
||||
c.a = beamColor.a * (0.7f + Mathf.Sin(t * Mathf.PI * 2) * 0.3f);
|
||||
beamRenderer.color = c;
|
||||
}
|
||||
|
||||
// Impact pulsing
|
||||
if (impact != null) {
|
||||
float impactPulse = 10f + Mathf.Sin(t * Mathf.PI * 2) * 5f;
|
||||
impact.transform.localScale = Vector3.one * impactPulse;
|
||||
}
|
||||
|
||||
// Continue particle spiral
|
||||
for (int i = 0; i < particles.Length; i++) {
|
||||
if (particles[i] == null) continue;
|
||||
|
||||
float particleT = (float)i / particleCount;
|
||||
Vector2 pos = Vector2.Lerp(source, target, particleT);
|
||||
|
||||
float spiralAngle = particlePhases[i] +
|
||||
(beamDuration + p * pulseDuration + elapsed) * 15f;
|
||||
float spiralRadius = 10f * (1f - Mathf.Abs(particleT - 0.5f) * 2f);
|
||||
pos += new Vector2(
|
||||
Mathf.Cos(spiralAngle) * spiralRadius * -direction.y +
|
||||
Mathf.Sin(spiralAngle) * spiralRadius * direction.x,
|
||||
Mathf.Cos(spiralAngle) * spiralRadius * direction.x +
|
||||
Mathf.Sin(spiralAngle) * spiralRadius * direction.y);
|
||||
|
||||
particles[i].transform.localPosition = new Vector3(pos.x, pos.y, 5f);
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fade out
|
||||
float fadeDuration = 0.2f;
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < fadeDuration) {
|
||||
float t = elapsed / fadeDuration;
|
||||
|
||||
if (beam != null) {
|
||||
Color c = beamColor;
|
||||
c.a = beamColor.a * (1f - t);
|
||||
beamRenderer.color = c;
|
||||
}
|
||||
|
||||
if (impact != null) {
|
||||
Color c = impactColor;
|
||||
c.a = impactColor.a * (1f - t);
|
||||
impactRenderer.color = c;
|
||||
}
|
||||
|
||||
for (int i = 0; i < particles.Length; i++) {
|
||||
if (particles[i] == null) continue;
|
||||
var renderer = particles[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = particleColor;
|
||||
c.a = particleColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (beam != null) { Destroy(beam); }
|
||||
if (impact != null) { Destroy(impact); }
|
||||
foreach (var particle in particles) {
|
||||
if (particle != null) { Destroy(particle); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25a8c1a83921d4859b6bd40fd9a8b6e4
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates unit dismissal with a fade-out and dissolve effect.
|
||||
/// Creates particles that drift away as the unit disappears.
|
||||
/// </summary>
|
||||
public class DismissAnimator : MonoBehaviour {
|
||||
[Header("Dissolve Settings")]
|
||||
[Tooltip("Sprite for dissolve particles")]
|
||||
public Sprite dissolveSprite;
|
||||
|
||||
[Tooltip("Number of dissolve particles")]
|
||||
public int particleCount = 12;
|
||||
|
||||
[Tooltip("Color of dissolve particles")]
|
||||
public Color dissolveColor = new Color(0.8f, 0.8f, 0.8f, 0.7f);
|
||||
|
||||
[Header("Glow Settings")]
|
||||
[Tooltip("Sprite for fading glow")]
|
||||
public Sprite glowSprite;
|
||||
|
||||
[Tooltip("Color of the glow")]
|
||||
public Color glowColor = new Color(1f, 1f, 0.9f, 0.5f);
|
||||
|
||||
[Tooltip("Scale of the glow")]
|
||||
public float glowScale = 30f;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration of the dissolve")]
|
||||
public float dissolveDuration = 0.6f;
|
||||
|
||||
[Tooltip("Spread distance of particles")]
|
||||
public float dissolveSpread = 40f;
|
||||
|
||||
[Tooltip("Rise height of particles")]
|
||||
public float riseHeight = 30f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a dismiss animation at the target hex.
|
||||
/// </summary>
|
||||
public void AnimateDismiss(int targetCellIndex) {
|
||||
if (dissolveSprite == null && glowSprite == null) {
|
||||
Debug.LogWarning("DismissAnimator: No sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("DismissAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"DismissAnimator: Invalid cell index {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateDismissEffect(targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateDismissEffect(Vector2 target) {
|
||||
// Create glow
|
||||
GameObject glow = null;
|
||||
SpriteRenderer glowRenderer = null;
|
||||
|
||||
if (glowSprite != null) {
|
||||
glow = new GameObject("DismissGlow");
|
||||
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
glow.transform.localPosition = new Vector3(target.x, target.y, 6f);
|
||||
glow.transform.localScale = Vector3.one * glowScale;
|
||||
|
||||
glowRenderer = glow.AddComponent<SpriteRenderer>();
|
||||
glowRenderer.sprite = glowSprite;
|
||||
glowRenderer.sortingLayerName = "Effects";
|
||||
glowRenderer.color = glowColor;
|
||||
}
|
||||
|
||||
// Create dissolve particles
|
||||
GameObject[] particles = new GameObject[particleCount];
|
||||
Vector2[] particleDirections = new Vector2[particleCount];
|
||||
float[] particleSpeeds = new float[particleCount];
|
||||
float[] particleDelays = new float[particleCount];
|
||||
|
||||
for (int i = 0; i < particleCount; i++) {
|
||||
if (dissolveSprite == null) break;
|
||||
|
||||
GameObject particle = new GameObject($"Dissolve_{i}");
|
||||
particle.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
particle.transform.localPosition = new Vector3(target.x, target.y, 5f);
|
||||
particle.transform.localScale = Vector3.one * Random.Range(4f, 8f);
|
||||
|
||||
SpriteRenderer renderer = particle.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = dissolveSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = dissolveColor;
|
||||
|
||||
float angle = (float)i / particleCount * Mathf.PI * 2f + Random.Range(-0.3f, 0.3f);
|
||||
particleDirections[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
|
||||
particleSpeeds[i] = dissolveSpread * Random.Range(0.5f, 1.2f);
|
||||
particleDelays[i] = Random.Range(0f, dissolveDuration * 0.3f);
|
||||
|
||||
particles[i] = particle;
|
||||
}
|
||||
|
||||
// Animate dissolve
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < dissolveDuration) {
|
||||
float t = elapsed / dissolveDuration;
|
||||
|
||||
// Pulse and fade glow
|
||||
if (glow != null) {
|
||||
float pulse = 1f + Mathf.Sin(t * Mathf.PI * 3) * 0.2f;
|
||||
glow.transform.localScale = Vector3.one * glowScale * pulse * (1f - t * 0.5f);
|
||||
|
||||
Color c = glowColor;
|
||||
c.a = glowColor.a * (1f - t);
|
||||
glowRenderer.color = c;
|
||||
}
|
||||
|
||||
// Animate particles drifting outward and upward
|
||||
for (int i = 0; i < particles.Length; i++) {
|
||||
if (particles[i] == null) continue;
|
||||
|
||||
float particleT = Mathf.Clamp01(
|
||||
(elapsed - particleDelays[i]) / (dissolveDuration - particleDelays[i]));
|
||||
if (particleT <= 0) continue;
|
||||
|
||||
float easeT = particleT * (2f - particleT); // Ease out
|
||||
|
||||
Vector2 pos = target + particleDirections[i] * particleSpeeds[i] * easeT;
|
||||
pos.y += riseHeight * particleT;
|
||||
|
||||
particles[i].transform.localPosition = new Vector3(pos.x, pos.y, 5f);
|
||||
|
||||
// Fade and shrink
|
||||
var renderer = particles[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = dissolveColor;
|
||||
c.a = dissolveColor.a * (1f - particleT);
|
||||
renderer.color = c;
|
||||
}
|
||||
|
||||
float scale = Mathf.Lerp(Random.Range(4f, 8f), 0f, particleT);
|
||||
particles[i].transform.localScale = Vector3.one * scale;
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (glow != null) { Destroy(glow); }
|
||||
foreach (var particle in particles) {
|
||||
if (particle != null) { Destroy(particle); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 153258e70f7a4f468e2ddbb00c8eb625
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,289 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates a duel with crossed weapons clashing dramatically.
|
||||
/// Creates weapon sprites that meet in the middle with impact effects.
|
||||
/// </summary>
|
||||
public class DuelAnimator : MonoBehaviour {
|
||||
[Header("Weapon Sprites")]
|
||||
[Tooltip("Sword sprite for the duel")]
|
||||
public Sprite swordSprite;
|
||||
|
||||
[Tooltip("Alternate weapon sprite")]
|
||||
public Sprite alternateWeaponSprite;
|
||||
|
||||
[Header("Impact Settings")]
|
||||
[Tooltip("Sprite for clash impact")]
|
||||
public Sprite impactSprite;
|
||||
|
||||
[Tooltip("Color of impact flash")]
|
||||
public Color impactColor = new Color(1f, 1f, 0.8f, 0.9f);
|
||||
|
||||
[Tooltip("Sprite for sparks")]
|
||||
public Sprite sparkSprite;
|
||||
|
||||
[Tooltip("Number of sparks on clash")]
|
||||
public int sparkCount = 4;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Scale of weapon sprites")]
|
||||
public float weaponScale = 22f;
|
||||
|
||||
[Tooltip("Distance weapons start from clash point")]
|
||||
public float startDistance = 45f;
|
||||
|
||||
[Tooltip("Duration of weapon swing")]
|
||||
public float swingDuration = 0.2f;
|
||||
|
||||
[Tooltip("Number of clashes")]
|
||||
public int clashCount = 3;
|
||||
|
||||
[Tooltip("Pause between clashes")]
|
||||
public float clashPause = 0.1f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a duel animation between two hex cells.
|
||||
/// </summary>
|
||||
public void AnimateDuel(int attackerCellIndex, int defenderCellIndex) {
|
||||
if (swordSprite == null && alternateWeaponSprite == null) {
|
||||
Debug.LogWarning("DuelAnimator: No weapon sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("DuelAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? attackerPos = __hexGrid.GetCellCenterPosition(attackerCellIndex);
|
||||
Vector2? defenderPos = __hexGrid.GetCellCenterPosition(defenderCellIndex);
|
||||
|
||||
if (attackerPos == null || defenderPos == null) {
|
||||
Debug.LogWarning(
|
||||
$"DuelAnimator: Invalid cell indices {attackerCellIndex} or {defenderCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateDuelEffect(attackerPos.Value, defenderPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateDuelEffect(Vector2 attacker, Vector2 defender) {
|
||||
Vector2 midpoint = (attacker + defender) / 2f;
|
||||
Vector2 direction = (defender - attacker).normalized;
|
||||
float baseAngle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
|
||||
|
||||
Sprite weapon1Sprite = swordSprite ?? alternateWeaponSprite;
|
||||
Sprite weapon2Sprite = alternateWeaponSprite ?? swordSprite;
|
||||
|
||||
// Create weapons
|
||||
GameObject weapon1 = new GameObject("DuelWeapon1");
|
||||
weapon1.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
SpriteRenderer weapon1Renderer = weapon1.AddComponent<SpriteRenderer>();
|
||||
weapon1Renderer.sprite = weapon1Sprite;
|
||||
weapon1Renderer.sortingLayerName = "Effects";
|
||||
weapon1.transform.localScale = Vector3.one * weaponScale;
|
||||
|
||||
GameObject weapon2 = new GameObject("DuelWeapon2");
|
||||
weapon2.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
SpriteRenderer weapon2Renderer = weapon2.AddComponent<SpriteRenderer>();
|
||||
weapon2Renderer.sprite = weapon2Sprite;
|
||||
weapon2Renderer.sortingLayerName = "Effects";
|
||||
weapon2.transform.localScale = Vector3.one * weaponScale;
|
||||
|
||||
// Starting positions
|
||||
Vector3 weapon1Start = new Vector3(
|
||||
midpoint.x - direction.x * startDistance,
|
||||
midpoint.y - direction.y * startDistance,
|
||||
5f);
|
||||
Vector3 weapon2Start = new Vector3(
|
||||
midpoint.x + direction.x * startDistance,
|
||||
midpoint.y + direction.y * startDistance,
|
||||
5f);
|
||||
|
||||
Vector3 clashPoint = new Vector3(midpoint.x, midpoint.y, 5f);
|
||||
|
||||
// Duel clashes
|
||||
for (int clash = 0; clash < clashCount; clash++) {
|
||||
// Alternate swing direction
|
||||
float swingOffset = (clash % 2 == 0) ? 1f : -1f;
|
||||
|
||||
// Swing in toward clash
|
||||
weapon1.transform.localPosition = weapon1Start;
|
||||
weapon2.transform.localPosition = weapon2Start;
|
||||
|
||||
// Crossed swords angle
|
||||
float weapon1Angle = baseAngle + 45f * swingOffset;
|
||||
float weapon2Angle = baseAngle + 180f - 45f * swingOffset;
|
||||
|
||||
weapon1.transform.localRotation = Quaternion.Euler(0, 0, weapon1Angle);
|
||||
weapon2.transform.localRotation = Quaternion.Euler(0, 0, weapon2Angle);
|
||||
|
||||
float elapsed = 0f;
|
||||
|
||||
// Swing toward clash point
|
||||
while (elapsed < swingDuration) {
|
||||
float t = elapsed / swingDuration;
|
||||
float easeT = t * t * (3f - 2f * t);
|
||||
|
||||
weapon1.transform.localPosition = Vector3.Lerp(weapon1Start, clashPoint, easeT);
|
||||
weapon2.transform.localPosition = Vector3.Lerp(weapon2Start, clashPoint, easeT);
|
||||
|
||||
// Rotate during swing
|
||||
float rot1 = Mathf.Lerp(weapon1Angle + 30f * swingOffset, weapon1Angle, easeT);
|
||||
float rot2 = Mathf.Lerp(weapon2Angle - 30f * swingOffset, weapon2Angle, easeT);
|
||||
weapon1.transform.localRotation = Quaternion.Euler(0, 0, rot1);
|
||||
weapon2.transform.localRotation = Quaternion.Euler(0, 0, rot2);
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Impact effect
|
||||
yield return StartCoroutine(CreateClashImpact(midpoint));
|
||||
|
||||
// Brief shake
|
||||
float shakeDuration = 0.05f;
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < shakeDuration) {
|
||||
Vector3 shake = new Vector3(Random.Range(-5f, 5f), Random.Range(-5f, 5f), 0f);
|
||||
weapon1.transform.localPosition = clashPoint + shake;
|
||||
weapon2.transform.localPosition = clashPoint - shake;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Pull back
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < swingDuration * 0.5f) {
|
||||
float t = elapsed / (swingDuration * 0.5f);
|
||||
float easeT = t * (2f - t);
|
||||
|
||||
weapon1.transform.localPosition = Vector3.Lerp(clashPoint, weapon1Start, easeT);
|
||||
weapon2.transform.localPosition = Vector3.Lerp(clashPoint, weapon2Start, easeT);
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (clash < clashCount - 1) { yield return new WaitForSeconds(clashPause); }
|
||||
}
|
||||
|
||||
// Final fade out
|
||||
float fadeDuration = 0.15f;
|
||||
float fadeElapsed = 0f;
|
||||
|
||||
while (fadeElapsed < fadeDuration) {
|
||||
float t = fadeElapsed / fadeDuration;
|
||||
|
||||
Color c1 = weapon1Renderer.color;
|
||||
c1.a = 1f - t;
|
||||
weapon1Renderer.color = c1;
|
||||
|
||||
Color c2 = weapon2Renderer.color;
|
||||
c2.a = 1f - t;
|
||||
weapon2Renderer.color = c2;
|
||||
|
||||
fadeElapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Destroy(weapon1);
|
||||
Destroy(weapon2);
|
||||
}
|
||||
|
||||
private IEnumerator CreateClashImpact(Vector2 position) {
|
||||
// Create impact flash
|
||||
GameObject impact = null;
|
||||
SpriteRenderer impactRenderer = null;
|
||||
|
||||
if (impactSprite != null) {
|
||||
impact = new GameObject("DuelImpact");
|
||||
impact.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
impact.transform.localPosition = new Vector3(position.x, position.y, 4f);
|
||||
impact.transform.localScale = Vector3.one * 8f;
|
||||
|
||||
impactRenderer = impact.AddComponent<SpriteRenderer>();
|
||||
impactRenderer.sprite = impactSprite;
|
||||
impactRenderer.sortingLayerName = "Effects";
|
||||
impactRenderer.color = impactColor;
|
||||
}
|
||||
|
||||
// Create sparks
|
||||
GameObject[] sparks = new GameObject[sparkCount];
|
||||
Vector2[] sparkDirections = new Vector2[sparkCount];
|
||||
float[] sparkSpeeds = new float[sparkCount];
|
||||
|
||||
for (int i = 0; i < sparkCount; i++) {
|
||||
if (sparkSprite == null) break;
|
||||
|
||||
GameObject spark = new GameObject($"DuelSpark_{i}");
|
||||
spark.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
spark.transform.localPosition = new Vector3(position.x, position.y, 3f);
|
||||
spark.transform.localScale = Vector3.one * 4f;
|
||||
|
||||
SpriteRenderer renderer = spark.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = sparkSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = impactColor;
|
||||
|
||||
float angle = (float)i / sparkCount * Mathf.PI * 2f + Random.Range(-0.5f, 0.5f);
|
||||
sparkDirections[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
|
||||
sparkSpeeds[i] = Random.Range(20f, 40f);
|
||||
|
||||
sparks[i] = spark;
|
||||
}
|
||||
|
||||
// Animate impact and sparks
|
||||
float impactDuration = 0.15f;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < impactDuration) {
|
||||
float t = elapsed / impactDuration;
|
||||
|
||||
// Flash expand and fade
|
||||
if (impact != null) {
|
||||
float scale = 8f + t * 12f;
|
||||
impact.transform.localScale = Vector3.one * scale;
|
||||
|
||||
Color c = impactColor;
|
||||
c.a = impactColor.a * (1f - t);
|
||||
impactRenderer.color = c;
|
||||
}
|
||||
|
||||
// Sparks fly out
|
||||
for (int i = 0; i < sparks.Length; i++) {
|
||||
if (sparks[i] == null) continue;
|
||||
|
||||
Vector2 pos = position + sparkDirections[i] * sparkSpeeds[i] * t;
|
||||
sparks[i].transform.localPosition = new Vector3(pos.x, pos.y, 3f);
|
||||
|
||||
var renderer = sparks[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = impactColor;
|
||||
c.a = impactColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (impact != null) { Destroy(impact); }
|
||||
foreach (var spark in sparks) {
|
||||
if (spark != null) { Destroy(spark); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb7274736d0c47e697b1dbf7153f3433
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,244 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates extinguishing fire with water spray/splash effect.
|
||||
/// Creates a burst of water droplets descending on the target.
|
||||
/// </summary>
|
||||
public class ExtinguishAnimator : MonoBehaviour {
|
||||
[Header("Water Settings")]
|
||||
[Tooltip("Sprite for water droplets")]
|
||||
public Sprite dropletSprite;
|
||||
|
||||
[Tooltip("Number of droplets")]
|
||||
public int dropletCount = 25;
|
||||
|
||||
[Tooltip("Minimum droplet scale")]
|
||||
public float dropletScaleMin = 5f;
|
||||
|
||||
[Tooltip("Maximum droplet scale")]
|
||||
public float dropletScaleMax = 15f;
|
||||
|
||||
[Tooltip("Color of water")]
|
||||
public Color waterColor = new Color(0.3f, 0.6f, 0.9f, 0.8f);
|
||||
|
||||
[Header("Steam Settings")]
|
||||
[Tooltip("Sprite for steam puffs")]
|
||||
public Sprite steamSprite;
|
||||
|
||||
[Tooltip("Number of steam puffs")]
|
||||
public int steamCount = 8;
|
||||
|
||||
[Tooltip("Color of steam")]
|
||||
public Color steamColor = new Color(0.9f, 0.9f, 0.9f, 0.6f);
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Height water falls from")]
|
||||
public float fallHeight = 100f;
|
||||
|
||||
[Tooltip("Spread radius of the water")]
|
||||
public float waterSpread = 40f;
|
||||
|
||||
[Tooltip("Duration of the water fall")]
|
||||
public float fallDuration = 0.5f;
|
||||
|
||||
[Tooltip("Time spread for staggered droplet launch")]
|
||||
public float launchSpread = 0.25f;
|
||||
|
||||
[Tooltip("Horizontal chaos during fall")]
|
||||
public float fallChaos = 15f;
|
||||
|
||||
[Tooltip("Duration of the steam effect")]
|
||||
public float steamDuration = 0.5f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts an extinguish animation at the target hex.
|
||||
/// </summary>
|
||||
public void AnimateExtinguish(int targetCellIndex) {
|
||||
if (dropletSprite == null && steamSprite == null) {
|
||||
Debug.LogWarning("ExtinguishAnimator: No sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("ExtinguishAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"ExtinguishAnimator: Invalid cell index {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateExtinguishEffect(targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateExtinguishEffect(Vector2 target) {
|
||||
// Create droplets
|
||||
GameObject[] droplets = new GameObject[dropletCount];
|
||||
Vector2[] dropletOffsets = new Vector2[dropletCount];
|
||||
Vector2[] dropletChaos = new Vector2[dropletCount];
|
||||
float[] dropletDelays = new float[dropletCount];
|
||||
|
||||
for (int i = 0; i < dropletCount; i++) {
|
||||
if (dropletSprite == null) break;
|
||||
|
||||
GameObject droplet = new GameObject($"Droplet_{i}");
|
||||
droplet.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = droplet.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = dropletSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = waterColor;
|
||||
droplet.transform.localScale =
|
||||
Vector3.one * Random.Range(dropletScaleMin, dropletScaleMax);
|
||||
|
||||
float angle = Random.Range(0f, Mathf.PI * 2f);
|
||||
float radius = waterSpread * Random.Range(0f, 1f);
|
||||
dropletOffsets[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
|
||||
dropletDelays[i] = Random.Range(0f, launchSpread);
|
||||
|
||||
// Random horizontal chaos direction for each droplet
|
||||
float chaosAngle = Random.Range(0f, Mathf.PI * 2f);
|
||||
dropletChaos[i] =
|
||||
new Vector2(Mathf.Cos(chaosAngle), Mathf.Sin(chaosAngle)) * fallChaos;
|
||||
|
||||
// Start above with some horizontal spread
|
||||
droplet.transform.localPosition = new Vector3(
|
||||
target.x + dropletOffsets[i].x * 0.3f,
|
||||
target.y + fallHeight + Random.Range(-10f, 10f),
|
||||
5f);
|
||||
|
||||
droplets[i] = droplet;
|
||||
}
|
||||
|
||||
// Animate falling water
|
||||
float elapsed = 0f;
|
||||
float totalDuration = fallDuration + launchSpread;
|
||||
|
||||
while (elapsed < totalDuration) {
|
||||
for (int i = 0; i < droplets.Length; i++) {
|
||||
if (droplets[i] == null) continue;
|
||||
|
||||
float dropletT = Mathf.Clamp01(
|
||||
(elapsed - dropletDelays[i]) /
|
||||
(fallDuration - dropletDelays[i] * 0.5f));
|
||||
if (dropletT <= 0) continue;
|
||||
|
||||
// Accelerating fall
|
||||
float fallT = dropletT * dropletT;
|
||||
|
||||
// Add chaotic horizontal movement (sine wave pattern)
|
||||
float chaosT = Mathf.Sin(dropletT * Mathf.PI * 2f) * (1f - dropletT);
|
||||
Vector2 chaos = dropletChaos[i] * chaosT;
|
||||
|
||||
float x = target.x + dropletOffsets[i].x + chaos.x;
|
||||
float y = target.y + fallHeight * (1f - fallT);
|
||||
droplets[i].transform.localPosition = new Vector3(x, y, 5f);
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Splash effect - droplets spread out
|
||||
float splashDuration = 0.15f;
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < splashDuration) {
|
||||
float t = elapsed / splashDuration;
|
||||
|
||||
for (int i = 0; i < droplets.Length; i++) {
|
||||
if (droplets[i] == null) continue;
|
||||
|
||||
Vector2 splashOffset = dropletOffsets[i] * (1f + t * 0.5f);
|
||||
droplets[i].transform.localPosition =
|
||||
new Vector3(target.x + splashOffset.x, target.y + t * 5f, 5f);
|
||||
|
||||
var renderer = droplets[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = waterColor;
|
||||
c.a = waterColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup droplets
|
||||
foreach (var droplet in droplets) {
|
||||
if (droplet != null) { Destroy(droplet); }
|
||||
}
|
||||
|
||||
// Create steam
|
||||
GameObject[] steams = new GameObject[steamCount];
|
||||
Vector2[] steamOffsets = new Vector2[steamCount];
|
||||
|
||||
for (int i = 0; i < steamCount; i++) {
|
||||
if (steamSprite == null) break;
|
||||
|
||||
GameObject steam = new GameObject($"Steam_{i}");
|
||||
steam.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
steam.transform.localPosition = new Vector3(target.x, target.y, 4f);
|
||||
|
||||
SpriteRenderer renderer = steam.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = steamSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = steamColor;
|
||||
steam.transform.localScale = Vector3.one * 10f;
|
||||
|
||||
float angle = (float)i / steamCount * Mathf.PI * 2f;
|
||||
steamOffsets[i] =
|
||||
new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * waterSpread * 0.5f;
|
||||
|
||||
steams[i] = steam;
|
||||
}
|
||||
|
||||
// Animate rising steam
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < steamDuration) {
|
||||
float t = elapsed / steamDuration;
|
||||
|
||||
for (int i = 0; i < steams.Length; i++) {
|
||||
if (steams[i] == null) continue;
|
||||
|
||||
float riseT = t * (2f - t); // Ease out
|
||||
|
||||
Vector2 pos = target + steamOffsets[i] * (1f + t * 0.3f);
|
||||
pos.y += fallHeight * 0.8f * riseT;
|
||||
|
||||
steams[i].transform.localPosition = new Vector3(pos.x, pos.y, 4f);
|
||||
|
||||
// Expand and fade
|
||||
float scale = 10f * (1f + t * 2f);
|
||||
steams[i].transform.localScale = Vector3.one * scale;
|
||||
|
||||
var renderer = steams[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = steamColor;
|
||||
c.a = steamColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup steam
|
||||
foreach (var steam in steams) {
|
||||
if (steam != null) { Destroy(steam); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4b21a8ead0c49478d937a08fe1fd31b
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,217 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates a fear attack with a dark wave emanating from source toward target.
|
||||
/// Creates an ominous expanding dark pulse effect.
|
||||
/// </summary>
|
||||
public class FearAnimator : MonoBehaviour {
|
||||
[Header("Wave Settings")]
|
||||
[Tooltip("Sprite for the fear wave (circular gradient works well)")]
|
||||
public Sprite waveSprite;
|
||||
|
||||
[Tooltip("Color of the fear wave")]
|
||||
public Color waveColor = new Color(0.3f, 0.1f, 0.4f, 0.8f);
|
||||
|
||||
[Tooltip("Starting scale of the wave")]
|
||||
public float waveStartScale = 5f;
|
||||
|
||||
[Tooltip("Ending scale of the wave")]
|
||||
public float waveEndScale = 40f;
|
||||
|
||||
[Header("Tendril Settings")]
|
||||
[Tooltip("Sprite for dark tendrils")]
|
||||
public Sprite tendrilSprite;
|
||||
|
||||
[Tooltip("Number of tendrils in the wave")]
|
||||
public int tendrilCount = 5;
|
||||
|
||||
[Tooltip("Color of tendrils")]
|
||||
public Color tendrilColor = new Color(0.2f, 0f, 0.3f, 0.9f);
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration of the fear wave animation")]
|
||||
public float waveDuration = 0.5f;
|
||||
|
||||
[Tooltip("Duration of the impact effect on target")]
|
||||
public float impactDuration = 0.3f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a fear animation from source to target hex.
|
||||
/// </summary>
|
||||
public void AnimateFear(int sourceCellIndex, int targetCellIndex) {
|
||||
if (waveSprite == null && tendrilSprite == null) {
|
||||
Debug.LogWarning("FearAnimator: No sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("FearAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
$"FearAnimator: Invalid cell indices {sourceCellIndex} or {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateFearWave(sourcePos.Value, targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateFearWave(Vector2 source, Vector2 target) {
|
||||
Vector2 direction = (target - source).normalized;
|
||||
float distance = Vector2.Distance(source, target);
|
||||
|
||||
// Create main wave
|
||||
GameObject wave = null;
|
||||
SpriteRenderer waveRenderer = null;
|
||||
|
||||
if (waveSprite != null) {
|
||||
wave = new GameObject("FearWave");
|
||||
wave.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
wave.transform.localPosition = new Vector3(source.x, source.y, 6f);
|
||||
wave.transform.localScale = Vector3.one * waveStartScale;
|
||||
|
||||
waveRenderer = wave.AddComponent<SpriteRenderer>();
|
||||
waveRenderer.sprite = waveSprite;
|
||||
waveRenderer.sortingLayerName = "Effects";
|
||||
waveRenderer.color = waveColor;
|
||||
}
|
||||
|
||||
// Create tendrils
|
||||
GameObject[] tendrils = new GameObject[tendrilCount];
|
||||
float[] tendrilAngles = new float[tendrilCount];
|
||||
float[] tendrilSpeeds = new float[tendrilCount];
|
||||
|
||||
float baseAngle = Mathf.Atan2(direction.y, direction.x);
|
||||
|
||||
for (int i = 0; i < tendrilCount; i++) {
|
||||
if (tendrilSprite == null) break;
|
||||
|
||||
GameObject tendril = new GameObject($"FearTendril_{i}");
|
||||
tendril.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
tendril.transform.localPosition = new Vector3(source.x, source.y, 5f);
|
||||
|
||||
SpriteRenderer renderer = tendril.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = tendrilSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = tendrilColor;
|
||||
tendril.transform.localScale = Vector3.one * 8f;
|
||||
|
||||
// Fan out from center toward target direction
|
||||
float spreadAngle = (i - (tendrilCount - 1) / 2f) * 0.3f;
|
||||
tendrilAngles[i] = baseAngle + spreadAngle;
|
||||
tendrilSpeeds[i] = 1f + Random.Range(-0.2f, 0.2f);
|
||||
|
||||
tendrils[i] = tendril;
|
||||
}
|
||||
|
||||
// Animate wave traveling toward target
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < waveDuration) {
|
||||
float t = elapsed / waveDuration;
|
||||
float easeT = t * t * (3f - 2f * t); // Smooth step
|
||||
|
||||
// Move and expand wave
|
||||
if (wave != null) {
|
||||
Vector2 wavePos = Vector2.Lerp(source, target, easeT);
|
||||
wave.transform.localPosition = new Vector3(wavePos.x, wavePos.y, 6f);
|
||||
|
||||
float scale = Mathf.Lerp(waveStartScale, waveEndScale, easeT);
|
||||
wave.transform.localScale = Vector3.one * scale;
|
||||
|
||||
// Fade as it expands
|
||||
Color c = waveColor;
|
||||
c.a = waveColor.a * (1f - t * 0.5f);
|
||||
waveRenderer.color = c;
|
||||
}
|
||||
|
||||
// Animate tendrils
|
||||
for (int i = 0; i < tendrils.Length; i++) {
|
||||
if (tendrils[i] == null) continue;
|
||||
|
||||
float tendrilT = Mathf.Clamp01(t * tendrilSpeeds[i]);
|
||||
float tendrilDist = distance * tendrilT;
|
||||
|
||||
Vector2 tendrilPos =
|
||||
source +
|
||||
new Vector2(Mathf.Cos(tendrilAngles[i]), Mathf.Sin(tendrilAngles[i])) *
|
||||
tendrilDist;
|
||||
|
||||
// Add waviness
|
||||
float wave_offset = Mathf.Sin(t * 10f + i) * 5f;
|
||||
tendrilPos += new Vector2(-direction.y, direction.x) * wave_offset;
|
||||
|
||||
tendrils[i].transform.localPosition =
|
||||
new Vector3(tendrilPos.x, tendrilPos.y, 5f);
|
||||
|
||||
// Rotate to face direction
|
||||
tendrils[i].transform.localRotation =
|
||||
Quaternion.Euler(0, 0, tendrilAngles[i] * Mathf.Rad2Deg - 90f);
|
||||
|
||||
// Scale and fade
|
||||
var renderer = tendrils[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = tendrilColor;
|
||||
c.a = tendrilColor.a * (1f - tendrilT * 0.7f);
|
||||
renderer.color = c;
|
||||
|
||||
float tendrilScale = 8f * (1f + tendrilT * 0.5f);
|
||||
tendrils[i].transform.localScale = Vector3.one * tendrilScale;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Impact effect at target
|
||||
GameObject impact = null;
|
||||
if (waveSprite != null) {
|
||||
impact = new GameObject("FearImpact");
|
||||
impact.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
impact.transform.localPosition = new Vector3(target.x, target.y, 6f);
|
||||
|
||||
SpriteRenderer impactRenderer = impact.AddComponent<SpriteRenderer>();
|
||||
impactRenderer.sprite = waveSprite;
|
||||
impactRenderer.sortingLayerName = "Effects";
|
||||
impactRenderer.color = waveColor;
|
||||
impact.transform.localScale = Vector3.one * waveEndScale;
|
||||
|
||||
// Pulse and fade impact
|
||||
elapsed = 0f;
|
||||
while (elapsed < impactDuration) {
|
||||
float t = elapsed / impactDuration;
|
||||
|
||||
float pulseScale = waveEndScale * (1f + Mathf.Sin(t * Mathf.PI * 3) * 0.1f);
|
||||
impact.transform.localScale = Vector3.one * pulseScale;
|
||||
|
||||
Color c = waveColor;
|
||||
c.a = waveColor.a * (1f - t);
|
||||
impactRenderer.color = c;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Destroy(impact);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (wave != null) { Destroy(wave); }
|
||||
foreach (var tendril in tendrils) {
|
||||
if (tendril != null) { Destroy(tendril); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3af84325bf53c44c48b14a508d38c14e
|
||||
@@ -0,0 +1,191 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates fire effects for StartFire and FireDamage actions.
|
||||
/// Creates rising flames with flickering particles.
|
||||
/// </summary>
|
||||
public class FireEffectAnimator : MonoBehaviour {
|
||||
[Header("Flame Settings")]
|
||||
[Tooltip("Sprite for flame particles")]
|
||||
public Sprite flameSprite;
|
||||
|
||||
[Tooltip("Number of flame particles")]
|
||||
public int flameCount = 5;
|
||||
|
||||
[Tooltip("Base color of flames")]
|
||||
public Color flameColor = new Color(1f, 0.5f, 0.1f, 0.9f);
|
||||
|
||||
[Tooltip("Hot color for inner flames")]
|
||||
public Color hotColor = new Color(1f, 0.9f, 0.3f, 1f);
|
||||
|
||||
[Header("Ember Settings")]
|
||||
[Tooltip("Sprite for rising embers")]
|
||||
public Sprite emberSprite;
|
||||
|
||||
[Tooltip("Number of embers")]
|
||||
public int emberCount = 8;
|
||||
|
||||
[Tooltip("Color of embers")]
|
||||
public Color emberColor = new Color(1f, 0.6f, 0.2f, 0.8f);
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Scale of flame sprites")]
|
||||
public float flameScale = 15f;
|
||||
|
||||
[Tooltip("Height flames rise")]
|
||||
public float flameHeight = 40f;
|
||||
|
||||
[Tooltip("Duration of the fire effect")]
|
||||
public float fireDuration = 0.8f;
|
||||
|
||||
[Tooltip("Spread radius of the fire")]
|
||||
public float fireSpread = 20f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a fire animation at the target hex.
|
||||
/// </summary>
|
||||
public void AnimateFire(int targetCellIndex) {
|
||||
if (flameSprite == null && emberSprite == null) {
|
||||
Debug.LogWarning("FireEffectAnimator: No sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("FireEffectAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"FireEffectAnimator: Invalid cell index {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateFireEffect(targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateFireEffect(Vector2 target) {
|
||||
// Create flames
|
||||
GameObject[] flames = new GameObject[flameCount];
|
||||
Vector2[] flameOffsets = new Vector2[flameCount];
|
||||
float[] flamePhases = new float[flameCount];
|
||||
|
||||
for (int i = 0; i < flameCount; i++) {
|
||||
if (flameSprite == null) break;
|
||||
|
||||
GameObject flame = new GameObject($"Flame_{i}");
|
||||
flame.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = flame.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = flameSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = i < flameCount / 2 ? hotColor : flameColor;
|
||||
flame.transform.localScale = Vector3.one * flameScale * Random.Range(0.8f, 1.2f);
|
||||
|
||||
float angle = (float)i / flameCount * Mathf.PI * 2f;
|
||||
float radius = fireSpread * Random.Range(0.3f, 0.8f);
|
||||
flameOffsets[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
|
||||
flamePhases[i] = Random.Range(0f, Mathf.PI * 2f);
|
||||
|
||||
flames[i] = flame;
|
||||
}
|
||||
|
||||
// Create embers
|
||||
GameObject[] embers = new GameObject[emberCount];
|
||||
Vector2[] emberOffsets = new Vector2[emberCount];
|
||||
float[] emberSpeeds = new float[emberCount];
|
||||
float[] emberPhases = new float[emberCount];
|
||||
|
||||
for (int i = 0; i < emberCount; i++) {
|
||||
if (emberSprite == null) break;
|
||||
|
||||
GameObject ember = new GameObject($"Ember_{i}");
|
||||
ember.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = ember.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = emberSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = emberColor;
|
||||
ember.transform.localScale = Vector3.one * 4f;
|
||||
|
||||
float angle = Random.Range(0f, Mathf.PI * 2f);
|
||||
emberOffsets[i] =
|
||||
new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * fireSpread * 0.5f;
|
||||
emberSpeeds[i] = Random.Range(0.5f, 1.5f);
|
||||
emberPhases[i] = Random.Range(0f, Mathf.PI * 2f);
|
||||
|
||||
embers[i] = ember;
|
||||
}
|
||||
|
||||
// Animate
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < fireDuration) {
|
||||
float t = elapsed / fireDuration;
|
||||
|
||||
// Animate flames (rise and flicker)
|
||||
for (int i = 0; i < flames.Length; i++) {
|
||||
if (flames[i] == null) continue;
|
||||
|
||||
float flicker = Mathf.Sin(elapsed * 20f + flamePhases[i]) * 0.3f + 0.7f;
|
||||
float rise = Mathf.Sin(elapsed * 8f + flamePhases[i]) * 5f;
|
||||
|
||||
Vector2 pos = target + flameOffsets[i];
|
||||
pos.y += rise + flameHeight * Mathf.Sin(t * Mathf.PI);
|
||||
|
||||
flames[i].transform.localPosition = new Vector3(pos.x, pos.y, 5f);
|
||||
flames[i].transform.localScale =
|
||||
Vector3.one * flameScale * flicker * (1f - t * 0.3f);
|
||||
|
||||
var renderer = flames[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color baseColor = i < flameCount / 2 ? hotColor : flameColor;
|
||||
Color c = baseColor;
|
||||
c.a = baseColor.a * flicker * (1f - t * 0.5f);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
// Animate embers (rise and drift)
|
||||
for (int i = 0; i < embers.Length; i++) {
|
||||
if (embers[i] == null) continue;
|
||||
|
||||
float emberT = t * emberSpeeds[i];
|
||||
float drift = Mathf.Sin(elapsed * 5f + emberPhases[i]) * 10f;
|
||||
|
||||
Vector2 pos = target + emberOffsets[i];
|
||||
pos.x += drift;
|
||||
pos.y += flameHeight * 1.5f * emberT;
|
||||
|
||||
embers[i].transform.localPosition = new Vector3(pos.x, pos.y, 4f);
|
||||
|
||||
var renderer = embers[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = emberColor;
|
||||
c.a = emberColor.a * (1f - emberT);
|
||||
renderer.color = c;
|
||||
embers[i].transform.localScale = Vector3.one * 4f * (1f - emberT * 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
foreach (var flame in flames) {
|
||||
if (flame != null) { Destroy(flame); }
|
||||
}
|
||||
foreach (var ember in embers) {
|
||||
if (ember != null) { Destroy(ember); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16506a5d765243e9badade12973e848d
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,198 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates unit fleeing with a quick dash away effect.
|
||||
/// Creates dust clouds and motion blur trailing behind.
|
||||
/// </summary>
|
||||
public class FleeAnimator : MonoBehaviour {
|
||||
[Header("Dust Settings")]
|
||||
[Tooltip("Sprite for dust cloud")]
|
||||
public Sprite dustSprite;
|
||||
|
||||
[Tooltip("Number of dust puffs")]
|
||||
public int dustCount = 4;
|
||||
|
||||
[Tooltip("Color of dust")]
|
||||
public Color dustColor = new Color(0.8f, 0.7f, 0.5f, 0.6f);
|
||||
|
||||
[Header("Motion Blur Settings")]
|
||||
[Tooltip("Sprite for motion blur/afterimage")]
|
||||
public Sprite blurSprite;
|
||||
|
||||
[Tooltip("Number of blur images")]
|
||||
public int blurCount = 3;
|
||||
|
||||
[Tooltip("Color of blur (typically unit-colored)")]
|
||||
public Color blurColor = new Color(0.5f, 0.5f, 0.5f, 0.5f);
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Distance of the flee movement")]
|
||||
public float fleeDistance = 50f;
|
||||
|
||||
[Tooltip("Duration of the flee animation")]
|
||||
public float fleeDuration = 0.3f;
|
||||
|
||||
[Tooltip("Duration of dust settling")]
|
||||
public float dustDuration = 0.4f;
|
||||
|
||||
[Tooltip("Scale of dust sprites")]
|
||||
public float dustScale = 12f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a flee animation from source away from threat.
|
||||
/// </summary>
|
||||
public void AnimateFlee(int sourceCellIndex, int threatCellIndex) {
|
||||
if (dustSprite == null && blurSprite == null) {
|
||||
Debug.LogWarning("FleeAnimator: No sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("FleeAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? threatPos = __hexGrid.GetCellCenterPosition(threatCellIndex);
|
||||
|
||||
if (sourcePos == null || threatPos == null) {
|
||||
Debug.LogWarning(
|
||||
$"FleeAnimator: Invalid cell indices {sourceCellIndex} or {threatCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateFleeEffect(sourcePos.Value, threatPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateFleeEffect(Vector2 source, Vector2 threat) {
|
||||
// Direction is AWAY from threat
|
||||
Vector2 fleeDirection = (source - threat).normalized;
|
||||
Vector2 fleeTarget = source + fleeDirection * fleeDistance;
|
||||
|
||||
// Create motion blur afterimages
|
||||
GameObject[] blurs = new GameObject[blurCount];
|
||||
|
||||
for (int i = 0; i < blurCount; i++) {
|
||||
if (blurSprite == null) break;
|
||||
|
||||
GameObject blur = new GameObject($"FleeBlur_{i}");
|
||||
blur.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
blur.transform.localPosition = new Vector3(source.x, source.y, 5f);
|
||||
blur.transform.localScale = Vector3.one * 15f;
|
||||
|
||||
SpriteRenderer renderer = blur.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = blurSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
Color c = blurColor;
|
||||
c.a = blurColor.a * (1f - (float)i / blurCount * 0.5f);
|
||||
renderer.color = c;
|
||||
|
||||
blurs[i] = blur;
|
||||
}
|
||||
|
||||
// Animate flee movement with blur trail
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < fleeDuration) {
|
||||
float t = elapsed / fleeDuration;
|
||||
float easeT = 1f - (1f - t) * (1f - t); // Ease out
|
||||
|
||||
Vector2 currentPos = Vector2.Lerp(source, fleeTarget, easeT);
|
||||
|
||||
// Update blur trail positions (staggered behind)
|
||||
for (int i = 0; i < blurs.Length; i++) {
|
||||
if (blurs[i] == null) continue;
|
||||
|
||||
float blurT = Mathf.Max(0f, easeT - (float)(i + 1) / blurCount * 0.3f);
|
||||
Vector2 blurPos = Vector2.Lerp(source, fleeTarget, blurT);
|
||||
blurs[i].transform.localPosition =
|
||||
new Vector3(blurPos.x, blurPos.y, 5f + i * 0.1f);
|
||||
|
||||
// Fade as they get further behind
|
||||
var renderer = blurs[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = blurColor;
|
||||
c.a = blurColor.a * (1f - t) * (1f - (float)i / blurCount * 0.5f);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup blurs
|
||||
foreach (var blur in blurs) {
|
||||
if (blur != null) { Destroy(blur); }
|
||||
}
|
||||
|
||||
// Create dust clouds at flee destination
|
||||
GameObject[] dusts = new GameObject[dustCount];
|
||||
Vector2[] dustOffsets = new Vector2[dustCount];
|
||||
|
||||
for (int i = 0; i < dustCount; i++) {
|
||||
if (dustSprite == null) break;
|
||||
|
||||
GameObject dust = new GameObject($"FleeDust_{i}");
|
||||
dust.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = dust.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = dustSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = dustColor;
|
||||
dust.transform.localScale = Vector3.one * dustScale * 0.5f;
|
||||
|
||||
// Position dust behind the flee path
|
||||
float offset = (float)i / dustCount * fleeDistance * 0.8f;
|
||||
Vector2 dustPos = fleeTarget - fleeDirection * offset;
|
||||
dustPos += new Vector2(Random.Range(-10f, 10f), Random.Range(-5f, 5f));
|
||||
dustOffsets[i] = new Vector2(Random.Range(-5f, 5f), Random.Range(5f, 15f));
|
||||
|
||||
dust.transform.localPosition = new Vector3(dustPos.x, dustPos.y, 6f);
|
||||
dusts[i] = dust;
|
||||
}
|
||||
|
||||
// Animate dust settling
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < dustDuration) {
|
||||
float t = elapsed / dustDuration;
|
||||
|
||||
for (int i = 0; i < dusts.Length; i++) {
|
||||
if (dusts[i] == null) continue;
|
||||
|
||||
// Expand and rise
|
||||
float scale = dustScale * (0.5f + t * 1.5f);
|
||||
dusts[i].transform.localScale = Vector3.one * scale;
|
||||
|
||||
Vector3 pos = dusts[i].transform.localPosition;
|
||||
pos.x += dustOffsets[i].x * Time.deltaTime;
|
||||
pos.y += dustOffsets[i].y * Time.deltaTime * (1f - t);
|
||||
dusts[i].transform.localPosition = pos;
|
||||
|
||||
// Fade
|
||||
var renderer = dusts[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = dustColor;
|
||||
c.a = dustColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup dust
|
||||
foreach (var dust in dusts) {
|
||||
if (dust != null) { Destroy(dust); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 692443d7e72c42c99ddec3400c3d4d75
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,221 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates freezing water with ice crystal formation effect.
|
||||
/// Creates expanding ice crystals with frost particles.
|
||||
/// </summary>
|
||||
public class FreezeAnimator : MonoBehaviour {
|
||||
[Header("Ice Crystal Settings")]
|
||||
[Tooltip("Sprite for ice crystals")]
|
||||
public Sprite crystalSprite;
|
||||
|
||||
[Tooltip("Number of crystals")]
|
||||
public int crystalCount = 6;
|
||||
|
||||
[Tooltip("Color of ice")]
|
||||
public Color iceColor = new Color(0.7f, 0.9f, 1f, 0.9f);
|
||||
|
||||
[Header("Frost Settings")]
|
||||
[Tooltip("Sprite for frost particles")]
|
||||
public Sprite frostSprite;
|
||||
|
||||
[Tooltip("Number of frost particles")]
|
||||
public int frostCount = 10;
|
||||
|
||||
[Tooltip("Color of frost")]
|
||||
public Color frostColor = new Color(0.9f, 0.95f, 1f, 0.7f);
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Scale of crystal sprites")]
|
||||
public float crystalScale = 12f;
|
||||
|
||||
[Tooltip("Spread radius of ice effect")]
|
||||
public float iceSpread = 25f;
|
||||
|
||||
[Tooltip("Duration of crystal formation")]
|
||||
public float formDuration = 0.4f;
|
||||
|
||||
[Tooltip("Duration of shimmer effect")]
|
||||
public float shimmerDuration = 0.3f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a freeze animation at the target hex.
|
||||
/// </summary>
|
||||
public void AnimateFreeze(int targetCellIndex) {
|
||||
if (crystalSprite == null && frostSprite == null) {
|
||||
Debug.LogWarning("FreezeAnimator: No sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("FreezeAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"FreezeAnimator: Invalid cell index {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateFreezeEffect(targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateFreezeEffect(Vector2 target) {
|
||||
// Create crystals
|
||||
GameObject[] crystals = new GameObject[crystalCount];
|
||||
Vector2[] crystalOffsets = new Vector2[crystalCount];
|
||||
float[] crystalAngles = new float[crystalCount];
|
||||
|
||||
for (int i = 0; i < crystalCount; i++) {
|
||||
if (crystalSprite == null) break;
|
||||
|
||||
GameObject crystal = new GameObject($"IceCrystal_{i}");
|
||||
crystal.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
crystal.transform.localPosition = new Vector3(target.x, target.y, 5f);
|
||||
crystal.transform.localScale = Vector3.zero;
|
||||
|
||||
SpriteRenderer renderer = crystal.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = crystalSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = iceColor;
|
||||
|
||||
float angle = (float)i / crystalCount * Mathf.PI * 2f;
|
||||
crystalAngles[i] = angle * Mathf.Rad2Deg;
|
||||
float radius = iceSpread * Random.Range(0.5f, 1f);
|
||||
crystalOffsets[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
|
||||
|
||||
crystal.transform.localRotation = Quaternion.Euler(0, 0, crystalAngles[i] + 90f);
|
||||
|
||||
crystals[i] = crystal;
|
||||
}
|
||||
|
||||
// Create frost particles
|
||||
GameObject[] frosts = new GameObject[frostCount];
|
||||
Vector2[] frostOffsets = new Vector2[frostCount];
|
||||
|
||||
for (int i = 0; i < frostCount; i++) {
|
||||
if (frostSprite == null) break;
|
||||
|
||||
GameObject frost = new GameObject($"Frost_{i}");
|
||||
frost.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
frost.transform.localPosition = new Vector3(target.x, target.y, 4f);
|
||||
frost.transform.localScale = Vector3.one * 3f;
|
||||
|
||||
SpriteRenderer renderer = frost.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = frostSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = frostColor;
|
||||
|
||||
float angle = Random.Range(0f, Mathf.PI * 2f);
|
||||
float radius = iceSpread * Random.Range(0.2f, 1.2f);
|
||||
frostOffsets[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
|
||||
|
||||
frosts[i] = frost;
|
||||
}
|
||||
|
||||
// Crystal formation phase
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < formDuration) {
|
||||
float t = elapsed / formDuration;
|
||||
float easeT = t * t * (3f - 2f * t);
|
||||
|
||||
// Grow crystals outward
|
||||
for (int i = 0; i < crystals.Length; i++) {
|
||||
if (crystals[i] == null) continue;
|
||||
|
||||
Vector2 pos = target + crystalOffsets[i] * easeT;
|
||||
crystals[i].transform.localPosition = new Vector3(pos.x, pos.y, 5f);
|
||||
crystals[i].transform.localScale = Vector3.one * crystalScale * easeT;
|
||||
|
||||
var renderer = crystals[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = iceColor;
|
||||
c.a = iceColor.a * easeT;
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
// Spread frost
|
||||
for (int i = 0; i < frosts.Length; i++) {
|
||||
if (frosts[i] == null) continue;
|
||||
|
||||
float frostT = Mathf.Clamp01(t * 1.5f);
|
||||
Vector2 pos = target + frostOffsets[i] * frostT;
|
||||
frosts[i].transform.localPosition = new Vector3(pos.x, pos.y, 4f);
|
||||
|
||||
var renderer = frosts[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = frostColor;
|
||||
c.a = frostColor.a * Mathf.Sin(frostT * Mathf.PI);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Shimmer phase
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < shimmerDuration) {
|
||||
float t = elapsed / shimmerDuration;
|
||||
|
||||
// Crystal shimmer
|
||||
for (int i = 0; i < crystals.Length; i++) {
|
||||
if (crystals[i] == null) continue;
|
||||
|
||||
var renderer = crystals[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
float shimmer = 0.8f + Mathf.Sin(t * Mathf.PI * 4f + i) * 0.2f;
|
||||
Color c = iceColor;
|
||||
c.a = iceColor.a * shimmer * (1f - t * 0.3f);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Fade out
|
||||
float fadeDuration = 0.2f;
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < fadeDuration) {
|
||||
float t = elapsed / fadeDuration;
|
||||
|
||||
for (int i = 0; i < crystals.Length; i++) {
|
||||
if (crystals[i] == null) continue;
|
||||
|
||||
var renderer = crystals[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = iceColor;
|
||||
c.a = iceColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
foreach (var crystal in crystals) {
|
||||
if (crystal != null) { Destroy(crystal); }
|
||||
}
|
||||
foreach (var frost in frosts) {
|
||||
if (frost != null) { Destroy(frost); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7b7ff29594e436dab2ff82a41d6a4b7
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -357,12 +357,29 @@ public class HexGrid : MonoBehaviour {
|
||||
/// <summary>
|
||||
/// Gets the local position of a cell for use with effects (arrows, particles).
|
||||
/// Returns the anchoredPosition which maps to localPosition in the rotated canvas.
|
||||
/// Note: This returns the terrain image position, which is offset from the true hex center.
|
||||
/// For the actual hex center, use GetCellCenterPosition.
|
||||
/// </summary>
|
||||
public Vector2? GetCellLocalPosition(int cellIndex) {
|
||||
if (cells == null || cellIndex < 0 || cellIndex >= cells.Length) { return null; }
|
||||
return cells[cellIndex].TerrainImage.rectTransform.anchoredPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the true center position of a hex cell.
|
||||
/// Unlike GetCellLocalPosition, this returns the actual geometric center of the hex.
|
||||
/// </summary>
|
||||
public Vector2? GetCellCenterPosition(int cellIndex) {
|
||||
if (cells == null || cellIndex < 0 || cellIndex >= cells.Length) { return null; }
|
||||
return cells[cellIndex].Geometry.AnchoredPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inner radius of a hex (distance from center to edge midpoint).
|
||||
/// Useful for scaling effects relative to hex size.
|
||||
/// </summary>
|
||||
public float GetHexInnerRadius() { return metrics?.InnerRadius ?? 0f; }
|
||||
|
||||
public void OverlayCell(
|
||||
int cellIndex,
|
||||
Color mainColor,
|
||||
@@ -445,11 +462,14 @@ public class HexGrid : MonoBehaviour {
|
||||
}
|
||||
|
||||
public void SetProfessionImage(int cellIndex, Texture image) {
|
||||
var professionImage = cells[cellIndex].ProfessionImage;
|
||||
if (professionImage == null) return;
|
||||
|
||||
if (image == null) {
|
||||
cells[cellIndex].ProfessionImage.gameObject.SetActive(value: false);
|
||||
professionImage.gameObject.SetActive(value: false);
|
||||
} else {
|
||||
cells[cellIndex].ProfessionImage.texture = image;
|
||||
cells[cellIndex].ProfessionImage.gameObject.SetActive(value: true);
|
||||
professionImage.texture = image;
|
||||
professionImage.gameObject.SetActive(value: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,11 +478,14 @@ public class HexGrid : MonoBehaviour {
|
||||
}
|
||||
|
||||
public void SetUnitTypeImage(int cellIndex, Texture image) {
|
||||
var unitTypeImage = cells[cellIndex].UnitTypeImage;
|
||||
if (unitTypeImage == null) return;
|
||||
|
||||
if (image == null) {
|
||||
cells[cellIndex].UnitTypeImage.gameObject.SetActive(value: false);
|
||||
unitTypeImage.gameObject.SetActive(value: false);
|
||||
} else {
|
||||
cells[cellIndex].UnitTypeImage.texture = image;
|
||||
cells[cellIndex].UnitTypeImage.gameObject.SetActive(value: true);
|
||||
unitTypeImage.texture = image;
|
||||
unitTypeImage.gameObject.SetActive(value: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates a holy wave expanding from the source hex outward.
|
||||
/// Creates a white glow that spreads to surrounding hexes, with violent
|
||||
/// damage effects on hexes containing undead units.
|
||||
/// </summary>
|
||||
public class HolyWaveAnimator : MonoBehaviour {
|
||||
[Header("Glow Settings")]
|
||||
[Tooltip("Sprite for the expanding glow")]
|
||||
public Sprite glowSprite;
|
||||
|
||||
[Tooltip("Color of the holy glow")]
|
||||
public Color glowColor = new Color(1f, 1f, 0.9f, 0.7f);
|
||||
|
||||
[Tooltip("Initial scale as fraction of hex radius")]
|
||||
public float glowStartRadii = 0.1f;
|
||||
|
||||
[Tooltip("Final scale as number of hex radii (2.5 = extends well into neighbors)")]
|
||||
public float glowEndRadii = 2.5f;
|
||||
|
||||
[Header("Damage Effect Settings")]
|
||||
[Tooltip("Sprite for damage effect on undead")]
|
||||
public Sprite damageSprite;
|
||||
|
||||
[Tooltip("Color of the damage effect")]
|
||||
public Color damageColor = new Color(1f, 1f, 0.5f, 1f);
|
||||
|
||||
[Tooltip("Scale of damage effect")]
|
||||
public float damageScale = 25f;
|
||||
|
||||
[Tooltip("Number of damage flashes")]
|
||||
public int damageFlashCount = 3;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration of the wave expansion")]
|
||||
public float expansionDuration = 0.8f;
|
||||
|
||||
[Tooltip("Duration the wave holds at full size")]
|
||||
public float holdDuration = 0.3f;
|
||||
|
||||
[Tooltip("Duration of fade out")]
|
||||
public float fadeOutDuration = 0.3f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a holy wave animation from the source hex.
|
||||
/// </summary>
|
||||
/// <param name="sourceCellIndex">The hex where the wave originates</param>
|
||||
/// <param name="undeadCellIndices">Cells containing undead that will show damage
|
||||
/// effects</param>
|
||||
public void AnimateHolyWave(int sourceCellIndex, List<int> undeadCellIndices = null) {
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("HolyWaveAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use GetCellCenterPosition for the true hex center
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
|
||||
if (sourcePos == null) {
|
||||
Debug.LogWarning($"HolyWaveAnimator: Invalid cell index {sourceCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get positions of undead cells (use center positions)
|
||||
List<Vector2> undeadPositions = new List<Vector2>();
|
||||
if (undeadCellIndices != null) {
|
||||
foreach (int cellIndex in undeadCellIndices) {
|
||||
Vector2? pos = __hexGrid.GetCellCenterPosition(cellIndex);
|
||||
if (pos != null) { undeadPositions.Add(pos.Value); }
|
||||
}
|
||||
}
|
||||
|
||||
// Compute scale based on hex size
|
||||
float hexRadius = __hexGrid.GetHexInnerRadius();
|
||||
float startScale = hexRadius * glowStartRadii;
|
||||
float endScale = hexRadius * glowEndRadii * 2f; // diameter = 2 * radius
|
||||
|
||||
StartCoroutine(AnimateWave(sourcePos.Value, undeadPositions, startScale, endScale));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateWave(
|
||||
Vector2 source,
|
||||
List<Vector2> undeadPositions,
|
||||
float startScale,
|
||||
float endScale) {
|
||||
List<GameObject> objects = new List<GameObject>();
|
||||
|
||||
// Create main glow
|
||||
GameObject glow = null;
|
||||
SpriteRenderer glowRenderer = null;
|
||||
|
||||
if (glowSprite != null) {
|
||||
glow = new GameObject("HolyWaveGlow");
|
||||
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
glow.transform.localPosition = new Vector3(source.x, source.y, 6f);
|
||||
glow.transform.localScale = Vector3.one * startScale;
|
||||
|
||||
glowRenderer = glow.AddComponent<SpriteRenderer>();
|
||||
glowRenderer.sprite = glowSprite;
|
||||
glowRenderer.sortingLayerName = "Effects";
|
||||
glowRenderer.color = glowColor;
|
||||
objects.Add(glow);
|
||||
}
|
||||
|
||||
// Track which undead have been hit (for triggering damage effects)
|
||||
HashSet<int> hitUndead = new HashSet<int>();
|
||||
|
||||
// Expansion phase
|
||||
float elapsed = 0f;
|
||||
while (elapsed < expansionDuration) {
|
||||
float t = elapsed / expansionDuration;
|
||||
float easeT = t * (2f - t); // Ease out
|
||||
|
||||
if (glow != null) {
|
||||
float scale = Mathf.Lerp(startScale, endScale, easeT);
|
||||
glow.transform.localScale = Vector3.one * scale;
|
||||
|
||||
// Pulse the alpha slightly for ethereal effect
|
||||
Color c = glowColor;
|
||||
c.a = glowColor.a * (0.8f + 0.2f * Mathf.Sin(elapsed * 10f));
|
||||
glowRenderer.color = c;
|
||||
}
|
||||
|
||||
// Check if wave has reached any undead positions
|
||||
float currentRadius = Mathf.Lerp(startScale, endScale, easeT) * 0.5f;
|
||||
for (int i = 0; i < undeadPositions.Count; i++) {
|
||||
if (hitUndead.Contains(i)) continue;
|
||||
|
||||
float distance = Vector2.Distance(source, undeadPositions[i]);
|
||||
if (distance <= currentRadius) {
|
||||
hitUndead.Add(i);
|
||||
// Start damage effect coroutine (manages its own cleanup)
|
||||
StartCoroutine(PlayDamageEffect(undeadPositions[i]));
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Ensure all undead are hit
|
||||
for (int i = 0; i < undeadPositions.Count; i++) {
|
||||
if (!hitUndead.Contains(i)) {
|
||||
StartCoroutine(PlayDamageEffect(undeadPositions[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// Hold at full size
|
||||
if (glow != null) { glow.transform.localScale = Vector3.one * endScale; }
|
||||
yield return new WaitForSeconds(holdDuration);
|
||||
|
||||
// Fade out
|
||||
elapsed = 0f;
|
||||
while (elapsed < fadeOutDuration) {
|
||||
float t = elapsed / fadeOutDuration;
|
||||
|
||||
if (glowRenderer != null) {
|
||||
Color c = glowColor;
|
||||
c.a = glowColor.a * (1f - t);
|
||||
glowRenderer.color = c;
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
foreach (var obj in objects) {
|
||||
if (obj != null) { Destroy(obj); }
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator PlayDamageEffect(Vector2 position) {
|
||||
if (damageSprite == null) yield break;
|
||||
|
||||
GameObject damageEffect = new GameObject("HolyWaveDamage");
|
||||
damageEffect.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
damageEffect.transform.localPosition = new Vector3(position.x, position.y, 5f);
|
||||
|
||||
SpriteRenderer renderer = damageEffect.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = damageSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
|
||||
// Flash effect
|
||||
float flashDuration = 0.15f;
|
||||
for (int i = 0; i < damageFlashCount; i++) {
|
||||
// Flash on
|
||||
renderer.color = damageColor;
|
||||
damageEffect.transform.localScale = Vector3.one * damageScale;
|
||||
yield return new WaitForSeconds(flashDuration * 0.6f);
|
||||
|
||||
// Flash off
|
||||
renderer.color = new Color(damageColor.r, damageColor.g, damageColor.b, 0.3f);
|
||||
damageEffect.transform.localScale = Vector3.one * damageScale * 0.8f;
|
||||
yield return new WaitForSeconds(flashDuration * 0.4f);
|
||||
}
|
||||
|
||||
// Final burst
|
||||
float burstDuration = 0.2f;
|
||||
float burstElapsed = 0f;
|
||||
while (burstElapsed < burstDuration) {
|
||||
float t = burstElapsed / burstDuration;
|
||||
|
||||
damageEffect.transform.localScale = Vector3.one * damageScale * (1f + t * 0.5f);
|
||||
Color c = damageColor;
|
||||
c.a = 1f - t;
|
||||
renderer.color = c;
|
||||
|
||||
burstElapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Destroy(damageEffect);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7925cd01f3b44fb1a3048f9fb5bfa6c
|
||||
+45
-30
@@ -4,15 +4,19 @@ using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates a lightning bolt striking from source to target hex.
|
||||
/// Creates a jagged electric arc with multiple segments and flicker effect.
|
||||
/// Animates lightning bolts striking from source to target hex.
|
||||
/// Creates multiple jagged electric arcs that all start at source center
|
||||
/// but end at random points within the target hex.
|
||||
/// </summary>
|
||||
public class LightningAnimator : MonoBehaviour {
|
||||
[Header("Lightning Settings")]
|
||||
[Tooltip("Sprite for lightning bolt segment")]
|
||||
public Sprite boltSegmentSprite;
|
||||
|
||||
[Tooltip("Number of segments in the bolt")]
|
||||
[Tooltip("Number of lightning bolts")]
|
||||
public int boltCount = 3;
|
||||
|
||||
[Tooltip("Number of segments per bolt")]
|
||||
public int segmentCount = 8;
|
||||
|
||||
[Tooltip("Maximum perpendicular offset for jags")]
|
||||
@@ -22,29 +26,28 @@ namespace Shardok {
|
||||
public float segmentScale = 3f;
|
||||
|
||||
[Tooltip("Bolt segment thickness multiplier")]
|
||||
public float thicknessMultiplier = 2f;
|
||||
public float thicknessMultiplier = 1f;
|
||||
|
||||
[Tooltip("How far endpoints spread within target hex (0-1, fraction of hex radius)")]
|
||||
public float targetSpread = 0.6f;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Total duration of lightning effect")]
|
||||
public float duration = 0.25f;
|
||||
|
||||
[Tooltip("Number of times the bolt flickers")]
|
||||
[Tooltip("Number of times the bolts flicker")]
|
||||
public int flickerCount = 3;
|
||||
|
||||
[Tooltip("Color of the lightning")]
|
||||
public Color boltColor = new Color(0.8f, 0.9f, 1f, 1f);
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
}
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a lightning animation between two hex cells.
|
||||
/// Multiple animations can run simultaneously.
|
||||
/// Multiple bolts originate from source center and strike random points in target hex.
|
||||
/// </summary>
|
||||
public void AnimateLightning(int sourceCellIndex, int targetCellIndex) {
|
||||
if (boltSegmentSprite == null) {
|
||||
@@ -52,13 +55,13 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("LightningAnimator: No hexGrid or gridCanvas available");
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("LightningAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = hexGrid.GetCellLocalPosition(sourceCellIndex);
|
||||
Vector2? targetPos = hexGrid.GetCellLocalPosition(targetCellIndex);
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
@@ -66,40 +69,52 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateBolt(sourcePos.Value, targetPos.Value));
|
||||
float hexRadius = __hexGrid.GetHexInnerRadius();
|
||||
StartCoroutine(AnimateBolts(sourcePos.Value, targetPos.Value, hexRadius));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateBolt(Vector2 source, Vector2 target) {
|
||||
List<GameObject> segments = new List<GameObject>();
|
||||
private IEnumerator AnimateBolts(Vector2 source, Vector2 targetCenter, float hexRadius) {
|
||||
List<GameObject> allSegments = new List<GameObject>();
|
||||
|
||||
// Generate random endpoints for each bolt within the target hex
|
||||
Vector2[] boltEndpoints = new Vector2[boltCount];
|
||||
for (int b = 0; b < boltCount; b++) {
|
||||
float angle = Random.Range(0f, Mathf.PI * 2f);
|
||||
float radius = Random.Range(0f, hexRadius * targetSpread);
|
||||
boltEndpoints[b] =
|
||||
targetCenter + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
|
||||
}
|
||||
|
||||
for (int flicker = 0; flicker < flickerCount; flicker++) {
|
||||
// Clear previous flicker segments
|
||||
foreach (var seg in segments) {
|
||||
foreach (var seg in allSegments) {
|
||||
if (seg != null) { Destroy(seg); }
|
||||
}
|
||||
segments.Clear();
|
||||
allSegments.Clear();
|
||||
|
||||
// Generate jagged path
|
||||
List<Vector2> points = GenerateBoltPath(source, target);
|
||||
// Generate and create all bolts
|
||||
for (int b = 0; b < boltCount; b++) {
|
||||
List<Vector2> points = GenerateBoltPath(source, boltEndpoints[b]);
|
||||
|
||||
// Create segment objects
|
||||
for (int i = 0; i < points.Count - 1; i++) {
|
||||
var segment = CreateBoltSegment(points[i], points[i + 1]);
|
||||
segments.Add(segment);
|
||||
// Create segment objects for this bolt
|
||||
for (int i = 0; i < points.Count - 1; i++) {
|
||||
var segment = CreateBoltSegment(points[i], points[i + 1]);
|
||||
allSegments.Add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// Show briefly
|
||||
yield return new WaitForSeconds(duration / flickerCount * 0.6f);
|
||||
|
||||
// Hide briefly (flicker effect)
|
||||
foreach (var seg in segments) {
|
||||
foreach (var seg in allSegments) {
|
||||
if (seg != null) { seg.SetActive(false); }
|
||||
}
|
||||
yield return new WaitForSeconds(duration / flickerCount * 0.4f);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
foreach (var seg in segments) {
|
||||
foreach (var seg in allSegments) {
|
||||
if (seg != null) { Destroy(seg); }
|
||||
}
|
||||
}
|
||||
@@ -128,7 +143,7 @@ namespace Shardok {
|
||||
|
||||
private GameObject CreateBoltSegment(Vector2 start, Vector2 end) {
|
||||
GameObject segment = new GameObject("LightningSegment");
|
||||
segment.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
segment.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = segment.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = boltSegmentSprite;
|
||||
|
||||
@@ -18,11 +18,11 @@ namespace Shardok {
|
||||
[Tooltip("Mace/hammer sprite for heavy infantry")]
|
||||
public Sprite maceSprite;
|
||||
|
||||
[Tooltip("Small spear sprite for light cavalry")]
|
||||
public Sprite smallSpearSprite;
|
||||
[Tooltip("Falchion sprite for light cavalry")]
|
||||
public Sprite falchionSprite;
|
||||
|
||||
[Tooltip("Large spear/lance sprite for heavy cavalry")]
|
||||
public Sprite largeSpearSprite;
|
||||
[Tooltip("Two-handed sword sprite for heavy cavalry")]
|
||||
public Sprite twoHandedSwordSprite;
|
||||
|
||||
[Tooltip("Dagger sprite for longbowmen")]
|
||||
public Sprite daggerSprite;
|
||||
@@ -30,15 +30,57 @@ namespace Shardok {
|
||||
[Tooltip("Bone/fist sprite for undead")]
|
||||
public Sprite boneSprite;
|
||||
|
||||
[Header("Weapon Orientation - Sword")]
|
||||
[Tooltip("Base rotation offset for sword (degrees)")]
|
||||
public float swordRotationOffset = -45f;
|
||||
|
||||
[Tooltip("Flip sword horizontally")]
|
||||
public bool swordFlipHorizontal = false;
|
||||
|
||||
[Header("Weapon Orientation - Mace")]
|
||||
[Tooltip("Base rotation offset for mace (degrees)")]
|
||||
public float maceRotationOffset = -45f;
|
||||
|
||||
[Tooltip("Flip mace horizontally")]
|
||||
public bool maceFlipHorizontal = false;
|
||||
|
||||
[Header("Weapon Orientation - Falchion")]
|
||||
[Tooltip("Base rotation offset for falchion (degrees)")]
|
||||
public float falchionRotationOffset = -45f;
|
||||
|
||||
[Tooltip("Flip falchion horizontally")]
|
||||
public bool falchionFlipHorizontal = false;
|
||||
|
||||
[Header("Weapon Orientation - Two-Handed Sword")]
|
||||
[Tooltip("Base rotation offset for two-handed sword (degrees)")]
|
||||
public float twoHandedSwordRotationOffset = -45f;
|
||||
|
||||
[Tooltip("Flip two-handed sword horizontally")]
|
||||
public bool twoHandedSwordFlipHorizontal = false;
|
||||
|
||||
[Header("Weapon Orientation - Dagger")]
|
||||
[Tooltip("Base rotation offset for dagger (degrees)")]
|
||||
public float daggerRotationOffset = -45f;
|
||||
|
||||
[Tooltip("Flip dagger horizontally")]
|
||||
public bool daggerFlipHorizontal = false;
|
||||
|
||||
[Header("Weapon Orientation - Bone")]
|
||||
[Tooltip("Base rotation offset for bone (degrees)")]
|
||||
public float boneRotationOffset = -45f;
|
||||
|
||||
[Tooltip("Flip bone horizontally")]
|
||||
public bool boneFlipHorizontal = false;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Base scale of weapon sprites")]
|
||||
public float weaponScale = 20.0f;
|
||||
|
||||
[Tooltip("Duration of a single clash cycle in seconds")]
|
||||
public float clashDuration = 0.12f;
|
||||
[Tooltip("Duration of a single swing cycle in seconds")]
|
||||
public float clashDuration = 0.3f;
|
||||
|
||||
[Tooltip("Number of clash cycles")]
|
||||
public int clashCount = 3;
|
||||
[Tooltip("Number of swing cycles")]
|
||||
public int clashCount = 2;
|
||||
|
||||
[Tooltip("Distance weapons travel toward each other (in local units)")]
|
||||
public float thrustDistance = 25f;
|
||||
@@ -46,23 +88,28 @@ namespace Shardok {
|
||||
[Tooltip("Offset from center point along attack axis (in local units)")]
|
||||
public float weaponOffset = 40f;
|
||||
|
||||
[Tooltip("Base rotation offset for diagonal sprites (degrees)")]
|
||||
public float baseRotationOffset = -45f;
|
||||
|
||||
[Tooltip("Swing arc angle for swinging weapons (degrees)")]
|
||||
public float swingArc = 60f;
|
||||
public float swingArc = 90f;
|
||||
|
||||
[Tooltip("Amount of random shake (in local units)")]
|
||||
public float shakeIntensity = 8f;
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
[Tooltip("Amount of shake at impact only (in local units)")]
|
||||
public float shakeIntensity = 3f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
private Coroutine _activeAnimation;
|
||||
private GameObject _attackerWeapon;
|
||||
private GameObject _defenderWeapon;
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
private void CleanupWeapons() {
|
||||
if (_attackerWeapon != null) {
|
||||
Destroy(_attackerWeapon);
|
||||
_attackerWeapon = null;
|
||||
}
|
||||
if (_defenderWeapon != null) {
|
||||
Destroy(_defenderWeapon);
|
||||
_defenderWeapon = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,19 +124,19 @@ namespace Shardok {
|
||||
int defenderCellIndex,
|
||||
BattalionTypeId attackerType,
|
||||
BattalionTypeId defenderType) {
|
||||
if (swordSprite == null && maceSprite == null && smallSpearSprite == null &&
|
||||
largeSpearSprite == null && daggerSprite == null && boneSprite == null) {
|
||||
if (swordSprite == null && maceSprite == null && falchionSprite == null &&
|
||||
twoHandedSwordSprite == null && daggerSprite == null && boneSprite == null) {
|
||||
Debug.LogWarning("MeleeAnimator: No weapon sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MeleeAnimator: No hexGrid or gridCanvas available");
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MeleeAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? attackerPos = hexGrid.GetCellLocalPosition(attackerCellIndex);
|
||||
Vector2? defenderPos = hexGrid.GetCellLocalPosition(defenderCellIndex);
|
||||
Vector2? attackerPos = __hexGrid.GetCellCenterPosition(attackerCellIndex);
|
||||
Vector2? defenderPos = __hexGrid.GetCellCenterPosition(defenderCellIndex);
|
||||
|
||||
if (attackerPos == null || defenderPos == null) {
|
||||
Debug.LogWarning(
|
||||
@@ -97,10 +144,11 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_activeAnimation != null) { StopCoroutine(_activeAnimation); }
|
||||
if (_activeAnimation != null) {
|
||||
StopCoroutine(_activeAnimation);
|
||||
CleanupWeapons();
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"MeleeAnimator: Starting melee from cell {attackerCellIndex} ({attackerType}) to {defenderCellIndex} ({defenderType})");
|
||||
_activeAnimation = StartCoroutine(
|
||||
AnimateClash(attackerPos.Value, defenderPos.Value, attackerType, defenderType));
|
||||
}
|
||||
@@ -109,7 +157,8 @@ namespace Shardok {
|
||||
public Sprite sprite;
|
||||
public bool swings; // true = swing arc, false = thrust
|
||||
public float scaleMultiplier;
|
||||
public float violenceMultiplier; // shake intensity multiplier
|
||||
public float rotationOffset;
|
||||
public bool flipHorizontal;
|
||||
}
|
||||
|
||||
private WeaponConfig GetWeaponConfig(BattalionTypeId type) {
|
||||
@@ -119,49 +168,66 @@ namespace Shardok {
|
||||
sprite = swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.0f,
|
||||
violenceMultiplier = 0.3f
|
||||
rotationOffset = swordRotationOffset,
|
||||
flipHorizontal = swordFlipHorizontal
|
||||
};
|
||||
case BattalionTypeId.HeavyInfantry:
|
||||
return new WeaponConfig {
|
||||
sprite = maceSprite ?? swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.2f,
|
||||
violenceMultiplier = 0.4f
|
||||
rotationOffset =
|
||||
maceSprite != null ? maceRotationOffset : swordRotationOffset,
|
||||
flipHorizontal =
|
||||
maceSprite != null ? maceFlipHorizontal : swordFlipHorizontal
|
||||
};
|
||||
case BattalionTypeId.LightCavalry:
|
||||
return new WeaponConfig {
|
||||
sprite = smallSpearSprite ?? largeSpearSprite ?? swordSprite,
|
||||
swings = false,
|
||||
scaleMultiplier = 0.9f,
|
||||
violenceMultiplier = 1.0f
|
||||
sprite = falchionSprite ?? swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.0f,
|
||||
rotationOffset = falchionSprite != null ? falchionRotationOffset
|
||||
: swordRotationOffset,
|
||||
flipHorizontal = falchionSprite != null ? falchionFlipHorizontal
|
||||
: swordFlipHorizontal
|
||||
};
|
||||
case BattalionTypeId.HeavyCavalry:
|
||||
return new WeaponConfig {
|
||||
sprite = largeSpearSprite ?? smallSpearSprite ?? swordSprite,
|
||||
swings = false,
|
||||
sprite = twoHandedSwordSprite ?? swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.4f,
|
||||
violenceMultiplier = 1.8f
|
||||
rotationOffset = twoHandedSwordSprite != null ? twoHandedSwordRotationOffset
|
||||
: swordRotationOffset,
|
||||
flipHorizontal = twoHandedSwordSprite != null ? twoHandedSwordFlipHorizontal
|
||||
: swordFlipHorizontal
|
||||
};
|
||||
case BattalionTypeId.Longbowmen:
|
||||
return new WeaponConfig {
|
||||
sprite = daggerSprite ?? swordSprite,
|
||||
swings = false,
|
||||
scaleMultiplier = 0.7f,
|
||||
violenceMultiplier = 0.8f
|
||||
rotationOffset =
|
||||
daggerSprite != null ? daggerRotationOffset : swordRotationOffset,
|
||||
flipHorizontal =
|
||||
daggerSprite != null ? daggerFlipHorizontal : swordFlipHorizontal
|
||||
};
|
||||
case BattalionTypeId.Undead:
|
||||
return new WeaponConfig {
|
||||
sprite = boneSprite ?? swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.0f,
|
||||
violenceMultiplier = 0.5f
|
||||
rotationOffset =
|
||||
boneSprite != null ? boneRotationOffset : swordRotationOffset,
|
||||
flipHorizontal =
|
||||
boneSprite != null ? boneFlipHorizontal : swordFlipHorizontal
|
||||
};
|
||||
default:
|
||||
return new WeaponConfig {
|
||||
sprite = swordSprite,
|
||||
swings = true,
|
||||
scaleMultiplier = 1.0f,
|
||||
violenceMultiplier = 0.3f
|
||||
rotationOffset = swordRotationOffset,
|
||||
flipHorizontal = swordFlipHorizontal
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -180,14 +246,12 @@ namespace Shardok {
|
||||
WeaponConfig attackerConfig = GetWeaponConfig(attackerType);
|
||||
WeaponConfig defenderConfig = GetWeaponConfig(defenderType);
|
||||
|
||||
// Create weapons with type-specific scaling
|
||||
GameObject attackerWeapon = CreateWeapon(attackerConfig, baseAngle);
|
||||
GameObject defenderWeapon = CreateWeapon(defenderConfig, baseAngle + 180f);
|
||||
// Create weapons with type-specific scaling (stored in class fields for cleanup)
|
||||
_attackerWeapon = CreateWeapon(attackerConfig, baseAngle);
|
||||
_defenderWeapon = CreateWeapon(defenderConfig, baseAngle + 180f);
|
||||
|
||||
bool attackerSwings = attackerConfig.swings;
|
||||
bool defenderSwings = defenderConfig.swings;
|
||||
float attackerViolence = shakeIntensity * attackerConfig.violenceMultiplier;
|
||||
float defenderViolence = shakeIntensity * defenderConfig.violenceMultiplier;
|
||||
|
||||
// Starting positions offset from midpoint
|
||||
Vector3 attackerStart = new Vector3(
|
||||
@@ -209,91 +273,86 @@ namespace Shardok {
|
||||
midpoint.y + direction.y * (weaponOffset - thrustDistance),
|
||||
10f);
|
||||
|
||||
attackerWeapon.transform.localPosition = attackerStart;
|
||||
defenderWeapon.transform.localPosition = defenderStart;
|
||||
_attackerWeapon.transform.localPosition = attackerStart;
|
||||
_defenderWeapon.transform.localPosition = defenderStart;
|
||||
|
||||
float attackerBaseAngle = baseAngle + baseRotationOffset;
|
||||
float defenderBaseAngle = baseAngle + 180f + baseRotationOffset;
|
||||
float attackerBaseAngle = baseAngle + attackerConfig.rotationOffset;
|
||||
float defenderBaseAngle = baseAngle + 180f + defenderConfig.rotationOffset;
|
||||
|
||||
// Animate clash cycles with alternating swing directions
|
||||
// Animate swing cycles with alternating swing directions
|
||||
for (int i = 0; i < clashCount; i++) {
|
||||
// Alternate swing direction each cycle
|
||||
float swingDirection = (i % 2 == 0) ? 1f : -1f;
|
||||
|
||||
// Swing/thrust toward clash point
|
||||
// Swing toward clash point
|
||||
yield return StartCoroutine(AnimateWeaponSwing(
|
||||
attackerWeapon,
|
||||
_attackerWeapon,
|
||||
attackerStart,
|
||||
attackerClash,
|
||||
attackerBaseAngle,
|
||||
attackerSwings ? -swingArc * swingDirection : 0f,
|
||||
attackerSwings ? 0f : 0f,
|
||||
clashDuration / 2f,
|
||||
true,
|
||||
attackerViolence));
|
||||
true));
|
||||
|
||||
yield return StartCoroutine(AnimateWeaponSwing(
|
||||
defenderWeapon,
|
||||
_defenderWeapon,
|
||||
defenderStart,
|
||||
defenderClash,
|
||||
defenderBaseAngle,
|
||||
defenderSwings ? swingArc * swingDirection : 0f,
|
||||
defenderSwings ? 0f : 0f,
|
||||
clashDuration / 2f,
|
||||
true,
|
||||
defenderViolence));
|
||||
true));
|
||||
|
||||
// Brief violent shake at impact (use max violence of both)
|
||||
float impactViolence = Mathf.Max(attackerViolence, defenderViolence);
|
||||
// Brief shake at impact
|
||||
yield return StartCoroutine(ImpactShake(
|
||||
attackerWeapon,
|
||||
defenderWeapon,
|
||||
_attackerWeapon,
|
||||
_defenderWeapon,
|
||||
attackerClash,
|
||||
defenderClash,
|
||||
0.05f,
|
||||
impactViolence));
|
||||
shakeIntensity));
|
||||
|
||||
// Swing/pull back
|
||||
// Swing back
|
||||
yield return StartCoroutine(AnimateWeaponSwing(
|
||||
attackerWeapon,
|
||||
_attackerWeapon,
|
||||
attackerClash,
|
||||
attackerStart,
|
||||
attackerBaseAngle,
|
||||
0f,
|
||||
attackerSwings ? -swingArc * swingDirection : 0f,
|
||||
clashDuration / 2f,
|
||||
false,
|
||||
attackerViolence));
|
||||
false));
|
||||
|
||||
yield return StartCoroutine(AnimateWeaponSwing(
|
||||
defenderWeapon,
|
||||
_defenderWeapon,
|
||||
defenderClash,
|
||||
defenderStart,
|
||||
defenderBaseAngle,
|
||||
0f,
|
||||
defenderSwings ? swingArc * swingDirection : 0f,
|
||||
clashDuration / 2f,
|
||||
false,
|
||||
defenderViolence));
|
||||
false));
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (attackerWeapon != null) { Destroy(attackerWeapon); }
|
||||
if (defenderWeapon != null) { Destroy(defenderWeapon); }
|
||||
CleanupWeapons();
|
||||
_activeAnimation = null;
|
||||
}
|
||||
|
||||
private GameObject CreateWeapon(WeaponConfig config, float angle) {
|
||||
GameObject weapon = new GameObject("MeleeWeapon");
|
||||
weapon.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
weapon.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = weapon.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = config.sprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
|
||||
weapon.transform.localRotation = Quaternion.Euler(0, 0, angle + baseRotationOffset);
|
||||
weapon.transform.localRotation = Quaternion.Euler(0, 0, angle + config.rotationOffset);
|
||||
float scale = weaponScale * config.scaleMultiplier;
|
||||
weapon.transform.localScale = new Vector3(scale, scale, scale);
|
||||
float xScale = config.flipHorizontal ? -scale : scale;
|
||||
weapon.transform.localScale = new Vector3(xScale, scale, scale);
|
||||
|
||||
return weapon;
|
||||
}
|
||||
@@ -306,25 +365,20 @@ namespace Shardok {
|
||||
float fromAngleOffset,
|
||||
float toAngleOffset,
|
||||
float duration,
|
||||
bool isAttacking,
|
||||
float violence) {
|
||||
bool isAttacking) {
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration) {
|
||||
if (weapon == null) { yield break; }
|
||||
|
||||
float t = elapsed / duration;
|
||||
// Aggressive ease for attack, smoother for retreat
|
||||
// Smooth ease for deliberate swing motion
|
||||
float smoothT = isAttacking ? t * t * (3f - 2f * t) : t * (2f - t);
|
||||
|
||||
// Position with shake (scaled by violence)
|
||||
Vector3 shake = new Vector3(
|
||||
Random.Range(-violence, violence),
|
||||
Random.Range(-violence, violence),
|
||||
0f);
|
||||
weapon.transform.localPosition = Vector3.Lerp(fromPos, toPos, smoothT) + shake;
|
||||
// Clean position interpolation - no shake during swing
|
||||
weapon.transform.localPosition = Vector3.Lerp(fromPos, toPos, smoothT);
|
||||
|
||||
// Rotation (for swinging weapons)
|
||||
// Rotation arc for swinging weapons
|
||||
float currentAngle =
|
||||
Mathf.Lerp(baseAngle + fromAngleOffset, baseAngle + toAngleOffset, smoothT);
|
||||
weapon.transform.localRotation = Quaternion.Euler(0, 0, currentAngle);
|
||||
@@ -377,6 +431,7 @@ namespace Shardok {
|
||||
StopCoroutine(_activeAnimation);
|
||||
_activeAnimation = null;
|
||||
}
|
||||
CleanupWeapons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,60 +28,118 @@ namespace Shardok {
|
||||
[Tooltip("Sprite for trail particles")]
|
||||
public Sprite trailSprite;
|
||||
|
||||
[Tooltip("Number of trail particles")]
|
||||
public int trailParticleCount = 8;
|
||||
[Tooltip("Number of trail particles in continuous trail")]
|
||||
public int trailParticleCount = 20;
|
||||
|
||||
[Tooltip("Scale of trail particles")]
|
||||
public float trailScale = 8f;
|
||||
public float trailScale = 50f;
|
||||
|
||||
[Tooltip("Color of trail")]
|
||||
public Color trailColor = new Color(1f, 0.7f, 0.3f, 0.8f);
|
||||
[Tooltip("Color of trail at front (hottest)")]
|
||||
public Color trailColorHot = new Color(1f, 0.9f, 0.6f, 0.9f);
|
||||
|
||||
[Tooltip("Color of trail at back (cooler)")]
|
||||
public Color trailColorCool = new Color(1f, 0.4f, 0.1f, 0.6f);
|
||||
|
||||
[Tooltip("Trail spread width")]
|
||||
public float trailSpread = 12f;
|
||||
|
||||
[Header("Explosion Settings")]
|
||||
[Tooltip("Sprite for explosion")]
|
||||
public Sprite explosionSprite;
|
||||
|
||||
[Tooltip("Initial scale of explosion")]
|
||||
public float explosionStartScale = 15f;
|
||||
public float explosionStartScale = 20f;
|
||||
|
||||
[Tooltip("Final scale of explosion")]
|
||||
public float explosionEndScale = 50f;
|
||||
public float explosionEndScale = 250f;
|
||||
|
||||
[Tooltip("Color of explosion")]
|
||||
public Color explosionColor = new Color(1f, 0.4f, 0.1f, 1f);
|
||||
[Tooltip("Color of explosion core")]
|
||||
public Color explosionColor = new Color(1f, 0.6f, 0.2f, 1f);
|
||||
|
||||
[Tooltip("Number of sparks")]
|
||||
public int sparkCount = 6;
|
||||
[Tooltip("Number of fire sparks")]
|
||||
public int sparkCount = 8;
|
||||
|
||||
[Tooltip("Spark spread distance")]
|
||||
public float sparkSpread = 40f;
|
||||
public float sparkSpread = 50f;
|
||||
|
||||
[Header("Debris Settings")]
|
||||
[Tooltip("Sprite for rock debris")]
|
||||
public Sprite debrisSprite;
|
||||
|
||||
[Tooltip("Number of debris rocks")]
|
||||
public int debrisCount = 6;
|
||||
|
||||
[Tooltip("Scale of debris rocks")]
|
||||
public float debrisScale = 6f;
|
||||
|
||||
[Tooltip("Debris spread distance")]
|
||||
public float debrisSpread = 60f;
|
||||
|
||||
[Tooltip("Color of debris")]
|
||||
public Color debrisColor = new Color(0.4f, 0.3f, 0.2f, 1f);
|
||||
|
||||
[Header("Meteor Rotation")]
|
||||
[Tooltip("Rotation speed in degrees per second")]
|
||||
public float rotationSpeed = 360f;
|
||||
|
||||
[Header("Charging Settings (MeteorStart)")]
|
||||
[Tooltip("Sprite for charging glow")]
|
||||
public Sprite chargeSprite;
|
||||
|
||||
[Tooltip("Color of charging effect")]
|
||||
public Color chargeColor = new Color(1f, 0.6f, 0.2f, 0.7f);
|
||||
|
||||
[Tooltip("Starting scale of charge glow")]
|
||||
public float chargeStartScale = 5f;
|
||||
|
||||
[Tooltip("Ending scale of charge glow")]
|
||||
public float chargeEndScale = 25f;
|
||||
|
||||
[Tooltip("Duration of charge animation")]
|
||||
public float chargeDuration = 0.5f;
|
||||
|
||||
[Header("Targeting Settings (MeteorTarget)")]
|
||||
[Tooltip("Sprite for target indicator")]
|
||||
public Sprite targetSprite;
|
||||
|
||||
[Tooltip("Color of target indicator")]
|
||||
public Color targetColor = new Color(1f, 0.3f, 0.1f, 0.6f);
|
||||
|
||||
[Tooltip("Scale of target indicator")]
|
||||
public float targetScale = 30f;
|
||||
|
||||
[Tooltip("Duration of target animation")]
|
||||
public float targetDuration = 0.4f;
|
||||
|
||||
[Header("Cancel Settings (MeteorCancel)")]
|
||||
[Tooltip("Color of fizzle effect")]
|
||||
public Color cancelColor = new Color(0.5f, 0.5f, 0.5f, 0.8f);
|
||||
|
||||
[Tooltip("Duration of cancel animation")]
|
||||
public float cancelDuration = 0.3f;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration of meteor fall")]
|
||||
public float fallDuration = 0.4f;
|
||||
public float fallDuration = 0.8f;
|
||||
|
||||
[Tooltip("Duration of explosion")]
|
||||
public float explosionDuration = 0.5f;
|
||||
public float explosionDuration = 0.6f;
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
}
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a meteor animation at the target hex cell.
|
||||
/// Multiple animations can run simultaneously.
|
||||
/// </summary>
|
||||
public void AnimateMeteorCast(int targetCellIndex) {
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MeteorAnimator: No hexGrid or gridCanvas available");
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MeteorAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = hexGrid.GetCellLocalPosition(targetCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"MeteorAnimator: Invalid cell index {targetCellIndex}");
|
||||
@@ -91,6 +149,315 @@ namespace Shardok {
|
||||
StartCoroutine(AnimateMeteor(targetPos.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates meteor charging effect at the caster's position.
|
||||
/// Shows a growing fiery glow gathering energy.
|
||||
/// </summary>
|
||||
public void AnimateMeteorStart(int sourceCellIndex) {
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MeteorAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
|
||||
if (sourcePos == null) {
|
||||
Debug.LogWarning($"MeteorAnimator: Invalid cell index {sourceCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateCharge(sourcePos.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates meteor targeting indicator at destination.
|
||||
/// Shows a pulsing circle/crosshairs marking the target.
|
||||
/// </summary>
|
||||
public void AnimateMeteorTarget(int targetCellIndex) {
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MeteorAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"MeteorAnimator: Invalid cell index {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateTarget(targetPos.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates meteor cancel/fizzle effect at the caster's position.
|
||||
/// Shows the charging energy dissipating.
|
||||
/// </summary>
|
||||
public void AnimateMeteorCancel(int sourceCellIndex) {
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MeteorAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
|
||||
if (sourcePos == null) {
|
||||
Debug.LogWarning($"MeteorAnimator: Invalid cell index {sourceCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateCancel(sourcePos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateCharge(Vector2 source) {
|
||||
Sprite sprite = chargeSprite ?? explosionSprite;
|
||||
if (sprite == null) yield break;
|
||||
|
||||
// Create charging glow
|
||||
GameObject glow = new GameObject("MeteorCharge");
|
||||
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
glow.transform.localPosition = new Vector3(source.x, source.y, 5f);
|
||||
|
||||
SpriteRenderer renderer = glow.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = sprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = chargeColor;
|
||||
|
||||
// Create rising particles
|
||||
int particleCount = 5;
|
||||
GameObject[] particles = new GameObject[particleCount];
|
||||
Vector2[] particleOffsets = new Vector2[particleCount];
|
||||
|
||||
for (int i = 0; i < particleCount; i++) {
|
||||
if (trailSprite == null) break;
|
||||
|
||||
GameObject particle = new GameObject($"ChargeParticle_{i}");
|
||||
particle.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer pRenderer = particle.AddComponent<SpriteRenderer>();
|
||||
pRenderer.sprite = trailSprite;
|
||||
pRenderer.sortingLayerName = "Effects";
|
||||
pRenderer.color = chargeColor;
|
||||
particle.transform.localScale = Vector3.one * trailScale;
|
||||
|
||||
float angle = (float)i / particleCount * Mathf.PI * 2f;
|
||||
particleOffsets[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * 20f;
|
||||
particles[i] = particle;
|
||||
}
|
||||
|
||||
// Animation
|
||||
float elapsed = 0f;
|
||||
while (elapsed < chargeDuration) {
|
||||
float t = elapsed / chargeDuration;
|
||||
float easeT = t * (2f - t); // Ease out
|
||||
|
||||
// Grow glow
|
||||
float scale = Mathf.Lerp(chargeStartScale, chargeEndScale, easeT);
|
||||
glow.transform.localScale = Vector3.one * scale;
|
||||
|
||||
// Pulse alpha
|
||||
Color c = chargeColor;
|
||||
c.a = chargeColor.a * (0.7f + 0.3f * Mathf.Sin(elapsed * 12f));
|
||||
renderer.color = c;
|
||||
|
||||
// Particles spiral inward and upward
|
||||
for (int i = 0; i < particles.Length; i++) {
|
||||
if (particles[i] == null) continue;
|
||||
|
||||
float particleT = (t + (float)i / particleCount) % 1f;
|
||||
float radius = 20f * (1f - particleT);
|
||||
float angle =
|
||||
particleT * Mathf.PI * 4f + (float)i / particleCount * Mathf.PI * 2f;
|
||||
Vector2 offset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
|
||||
|
||||
particles[i].transform.localPosition = new Vector3(
|
||||
source.x + offset.x,
|
||||
source.y + offset.y + particleT * 15f,
|
||||
4f);
|
||||
|
||||
var pRenderer = particles[i].GetComponent<SpriteRenderer>();
|
||||
if (pRenderer != null) {
|
||||
Color pc = chargeColor;
|
||||
pc.a = chargeColor.a * (1f - particleT * 0.5f);
|
||||
pRenderer.color = pc;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Brief flash at end
|
||||
glow.transform.localScale = Vector3.one * chargeEndScale * 1.2f;
|
||||
Color flashColor = chargeColor;
|
||||
flashColor.a = 1f;
|
||||
renderer.color = flashColor;
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
|
||||
// Cleanup
|
||||
Destroy(glow);
|
||||
foreach (var p in particles) {
|
||||
if (p != null) Destroy(p);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator AnimateTarget(Vector2 target) {
|
||||
Sprite sprite = targetSprite ?? explosionSprite;
|
||||
if (sprite == null) yield break;
|
||||
|
||||
// Create target indicator
|
||||
GameObject indicator = new GameObject("MeteorTarget");
|
||||
indicator.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
indicator.transform.localPosition = new Vector3(target.x, target.y, 5f);
|
||||
|
||||
SpriteRenderer renderer = indicator.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = sprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = targetColor;
|
||||
|
||||
// Create outer ring that contracts
|
||||
GameObject ring = null;
|
||||
SpriteRenderer ringRenderer = null;
|
||||
if (sprite != null) {
|
||||
ring = new GameObject("TargetRing");
|
||||
ring.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
ring.transform.localPosition = new Vector3(target.x, target.y, 6f);
|
||||
|
||||
ringRenderer = ring.AddComponent<SpriteRenderer>();
|
||||
ringRenderer.sprite = sprite;
|
||||
ringRenderer.sortingLayerName = "Effects";
|
||||
Color ringColor = targetColor;
|
||||
ringColor.a = targetColor.a * 0.5f;
|
||||
ringRenderer.color = ringColor;
|
||||
}
|
||||
|
||||
// Animation - indicator appears, ring contracts to it
|
||||
float elapsed = 0f;
|
||||
while (elapsed < targetDuration) {
|
||||
float t = elapsed / targetDuration;
|
||||
|
||||
// Main indicator pulses
|
||||
float pulse = 0.8f + 0.2f * Mathf.Sin(elapsed * 15f);
|
||||
indicator.transform.localScale = Vector3.one * targetScale * pulse;
|
||||
|
||||
Color c = targetColor;
|
||||
c.a = targetColor.a * pulse;
|
||||
renderer.color = c;
|
||||
|
||||
// Ring contracts inward
|
||||
if (ring != null) {
|
||||
float ringScale = targetScale * (2f - t);
|
||||
ring.transform.localScale = Vector3.one * ringScale;
|
||||
|
||||
Color rc = targetColor;
|
||||
rc.a = targetColor.a * 0.5f * (1f - t);
|
||||
ringRenderer.color = rc;
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Final pulse and fade
|
||||
float fadeDuration = 0.15f;
|
||||
elapsed = 0f;
|
||||
while (elapsed < fadeDuration) {
|
||||
float t = elapsed / fadeDuration;
|
||||
|
||||
indicator.transform.localScale = Vector3.one * targetScale * (1f + t * 0.3f);
|
||||
|
||||
Color c = targetColor;
|
||||
c.a = targetColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Destroy(indicator);
|
||||
if (ring != null) Destroy(ring);
|
||||
}
|
||||
|
||||
private IEnumerator AnimateCancel(Vector2 source) {
|
||||
Sprite sprite = chargeSprite ?? explosionSprite;
|
||||
if (sprite == null) yield break;
|
||||
|
||||
// Create fizzling glow (starts at charge end size)
|
||||
GameObject glow = new GameObject("MeteorCancel");
|
||||
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
glow.transform.localPosition = new Vector3(source.x, source.y, 5f);
|
||||
glow.transform.localScale = Vector3.one * chargeEndScale;
|
||||
|
||||
SpriteRenderer renderer = glow.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = sprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = cancelColor;
|
||||
|
||||
// Create dispersing particles
|
||||
int particleCount = 6;
|
||||
GameObject[] particles = new GameObject[particleCount];
|
||||
Vector2[] directions = new Vector2[particleCount];
|
||||
|
||||
for (int i = 0; i < particleCount; i++) {
|
||||
if (trailSprite == null) break;
|
||||
|
||||
GameObject particle = new GameObject($"CancelParticle_{i}");
|
||||
particle.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
particle.transform.localPosition = new Vector3(source.x, source.y, 4f);
|
||||
|
||||
SpriteRenderer pRenderer = particle.AddComponent<SpriteRenderer>();
|
||||
pRenderer.sprite = trailSprite;
|
||||
pRenderer.sortingLayerName = "Effects";
|
||||
pRenderer.color = cancelColor;
|
||||
particle.transform.localScale = Vector3.one * trailScale;
|
||||
|
||||
float angle = (float)i / particleCount * Mathf.PI * 2f + Random.Range(-0.2f, 0.2f);
|
||||
directions[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
|
||||
particles[i] = particle;
|
||||
}
|
||||
|
||||
// Animation - shrink and fade while particles disperse
|
||||
float elapsed = 0f;
|
||||
while (elapsed < cancelDuration) {
|
||||
float t = elapsed / cancelDuration;
|
||||
|
||||
// Shrink and fade glow
|
||||
float scale = chargeEndScale * (1f - t * 0.7f);
|
||||
glow.transform.localScale = Vector3.one * scale;
|
||||
|
||||
Color c = cancelColor;
|
||||
c.a = cancelColor.a * (1f - t);
|
||||
renderer.color = c;
|
||||
|
||||
// Particles drift outward and fade
|
||||
for (int i = 0; i < particles.Length; i++) {
|
||||
if (particles[i] == null) continue;
|
||||
|
||||
Vector2 pos = source + directions[i] * 30f * t;
|
||||
particles[i].transform.localPosition = new Vector3(pos.x, pos.y, 4f);
|
||||
|
||||
var pRenderer = particles[i].GetComponent<SpriteRenderer>();
|
||||
if (pRenderer != null) {
|
||||
Color pc = cancelColor;
|
||||
pc.a = cancelColor.a * (1f - t);
|
||||
pRenderer.color = pc;
|
||||
particles[i].transform.localScale =
|
||||
Vector3.one * trailScale * (1f - t * 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Destroy(glow);
|
||||
foreach (var p in particles) {
|
||||
if (p != null) Destroy(p);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator AnimateMeteor(Vector2 target) {
|
||||
List<GameObject> objects = new List<GameObject>();
|
||||
|
||||
@@ -99,87 +466,103 @@ namespace Shardok {
|
||||
Vector2 fallDirection = new Vector2(Mathf.Sin(angleRad), Mathf.Cos(angleRad));
|
||||
Vector2 startPos = target + fallDirection * fallDistance;
|
||||
|
||||
// Create meteor
|
||||
// Create meteor rock
|
||||
GameObject meteor = null;
|
||||
SpriteRenderer meteorRenderer = null;
|
||||
float currentRotation = 0f;
|
||||
if (meteorSprite != null) {
|
||||
meteor = new GameObject("Meteor");
|
||||
meteor.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
meteor.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer meteorRenderer = meteor.AddComponent<SpriteRenderer>();
|
||||
meteorRenderer = meteor.AddComponent<SpriteRenderer>();
|
||||
meteorRenderer.sprite = meteorSprite;
|
||||
meteorRenderer.sortingLayerName = "Effects";
|
||||
meteorRenderer.color = meteorColor;
|
||||
meteor.transform.localScale = Vector3.one * meteorScale;
|
||||
|
||||
// Rotate meteor to face direction of travel
|
||||
float rotAngle = Mathf.Atan2(-fallDirection.x, -fallDirection.y) * Mathf.Rad2Deg;
|
||||
meteor.transform.localRotation = Quaternion.Euler(0, 0, rotAngle);
|
||||
meteor.transform.localPosition = new Vector3(startPos.x, startPos.y, 3f);
|
||||
objects.Add(meteor);
|
||||
}
|
||||
|
||||
// Trail particles
|
||||
// Pre-create trail particles (continuous trail behind meteor)
|
||||
List<TrailParticle> trailParticles = new List<TrailParticle>();
|
||||
for (int i = 0; i < trailParticleCount; i++) {
|
||||
if (trailSprite == null) break;
|
||||
|
||||
GameObject particle = new GameObject($"TrailParticle_{i}");
|
||||
particle.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
particle.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = particle.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = trailSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = trailColor;
|
||||
particle.transform.localScale = Vector3.one * trailScale;
|
||||
particle.SetActive(false);
|
||||
particle.transform.localPosition = new Vector3(startPos.x, startPos.y, 4f);
|
||||
|
||||
// Particles further back are smaller and cooler colored
|
||||
float trailT = (float)i / trailParticleCount;
|
||||
float particleScale = trailScale * (1f - trailT * 0.6f);
|
||||
particle.transform.localScale = Vector3.one * particleScale;
|
||||
|
||||
Color particleColor = Color.Lerp(trailColorHot, trailColorCool, trailT);
|
||||
renderer.color = particleColor;
|
||||
|
||||
// Random offset perpendicular to fall direction
|
||||
float perpOffset = Random.Range(-trailSpread, trailSpread) * trailT;
|
||||
|
||||
trailParticles.Add(new TrailParticle {
|
||||
gameObject = particle,
|
||||
delay = (float)i / trailParticleCount * 0.15f,
|
||||
offset = Random.Range(-5f, 5f)
|
||||
delay = trailT * 0.3f, // How far behind the meteor this particle trails
|
||||
offset = perpOffset
|
||||
});
|
||||
objects.Add(particle);
|
||||
}
|
||||
|
||||
// Fall phase
|
||||
// Fall phase with rotation and continuous trail
|
||||
float elapsed = 0f;
|
||||
int nextTrailIndex = 0;
|
||||
|
||||
while (elapsed < fallDuration) {
|
||||
float t = elapsed / fallDuration;
|
||||
float easeT = t * t; // Accelerating fall
|
||||
// Ease-in for accelerating fall (starts slow, speeds up)
|
||||
float easeT = t * t;
|
||||
|
||||
Vector2 currentPos = Vector2.Lerp(startPos, target, easeT);
|
||||
|
||||
// Update meteor position and rotation
|
||||
if (meteor != null) {
|
||||
meteor.transform.localPosition = new Vector3(currentPos.x, currentPos.y, 3f);
|
||||
|
||||
// Continuous rotation
|
||||
currentRotation += rotationSpeed * Time.deltaTime;
|
||||
meteor.transform.localRotation = Quaternion.Euler(0, 0, currentRotation);
|
||||
|
||||
// Slight glow intensification as it approaches
|
||||
Color c = meteorColor;
|
||||
c.r = Mathf.Min(1f, meteorColor.r + t * 0.2f);
|
||||
c.g = Mathf.Min(1f, meteorColor.g + t * 0.1f);
|
||||
meteorRenderer.color = c;
|
||||
}
|
||||
|
||||
// Spawn trail particles
|
||||
while (nextTrailIndex < trailParticles.Count &&
|
||||
elapsed >= trailParticles[nextTrailIndex].delay) {
|
||||
var tp = trailParticles[nextTrailIndex];
|
||||
tp.gameObject.SetActive(true);
|
||||
tp.spawnPosition =
|
||||
currentPos + new Vector2(tp.offset, tp.offset * 0.5f) * fallDirection.x;
|
||||
tp.gameObject.transform.localPosition =
|
||||
new Vector3(tp.spawnPosition.x, tp.spawnPosition.y, 4f);
|
||||
nextTrailIndex++;
|
||||
}
|
||||
// Update trail particles to follow behind meteor
|
||||
Vector2 perpDir = new Vector2(-fallDirection.y, fallDirection.x);
|
||||
for (int i = 0; i < trailParticles.Count; i++) {
|
||||
var tp = trailParticles[i];
|
||||
if (tp.gameObject == null) continue;
|
||||
|
||||
// Fade trail particles
|
||||
foreach (var tp in trailParticles) {
|
||||
if (!tp.gameObject.activeSelf) continue;
|
||||
float age = elapsed - tp.delay;
|
||||
if (age > 0) {
|
||||
float fadeT = Mathf.Clamp01(age / 0.2f);
|
||||
var renderer = tp.gameObject.GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = trailColor;
|
||||
c.a = trailColor.a * (1f - fadeT);
|
||||
renderer.color = c;
|
||||
tp.gameObject.transform.localScale =
|
||||
Vector3.one * trailScale * (1f - fadeT * 0.5f);
|
||||
}
|
||||
// Calculate position along the trail (behind the meteor)
|
||||
float trailT = (float)i / trailParticleCount;
|
||||
float trailDistance = trailT * 50f; // How far behind
|
||||
|
||||
// Position is behind meteor along fall direction
|
||||
Vector2 trailPos = currentPos + fallDirection * trailDistance;
|
||||
// Add perpendicular wobble
|
||||
trailPos += perpDir * tp.offset * Mathf.Sin(elapsed * 8f + i);
|
||||
|
||||
tp.gameObject.transform.localPosition = new Vector3(trailPos.x, trailPos.y, 4f);
|
||||
|
||||
// Flicker effect
|
||||
var renderer = tp.gameObject.GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color baseColor = Color.Lerp(trailColorHot, trailColorCool, trailT);
|
||||
baseColor.a *= 0.8f + 0.2f * Mathf.Sin(elapsed * 15f + i * 0.5f);
|
||||
renderer.color = baseColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,29 +577,30 @@ namespace Shardok {
|
||||
}
|
||||
objects.Clear();
|
||||
|
||||
// Explosion phase
|
||||
// Explosion phase - bigger and with debris
|
||||
GameObject explosion = null;
|
||||
SpriteRenderer explosionRenderer = null;
|
||||
|
||||
if (explosionSprite != null) {
|
||||
explosion = new GameObject("MeteorExplosion");
|
||||
explosion.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
explosion.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
explosion.transform.localPosition = new Vector3(target.x, target.y, 5f);
|
||||
|
||||
explosionRenderer = explosion.AddComponent<SpriteRenderer>();
|
||||
explosionRenderer.sprite = explosionSprite;
|
||||
explosionRenderer.sortingLayerName = "Effects";
|
||||
explosionRenderer.color = explosionColor;
|
||||
explosion.transform.localScale = Vector3.one * explosionStartScale;
|
||||
objects.Add(explosion);
|
||||
}
|
||||
|
||||
// Sparks
|
||||
// Fire sparks (fast moving, fiery)
|
||||
List<SparkParticle> sparks = new List<SparkParticle>();
|
||||
for (int i = 0; i < sparkCount; i++) {
|
||||
if (trailSprite == null) break;
|
||||
|
||||
GameObject spark = new GameObject($"Spark_{i}");
|
||||
spark.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
spark.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
spark.transform.localPosition = new Vector3(target.x, target.y, 4f);
|
||||
|
||||
SpriteRenderer renderer = spark.AddComponent<SpriteRenderer>();
|
||||
@@ -229,27 +613,61 @@ namespace Shardok {
|
||||
sparks.Add(new SparkParticle {
|
||||
gameObject = spark,
|
||||
direction = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)),
|
||||
speed = sparkSpread* Random.Range(0.7f, 1.3f)
|
||||
speed = sparkSpread* Random.Range(0.8f, 1.4f)
|
||||
});
|
||||
objects.Add(spark);
|
||||
}
|
||||
|
||||
// Rock debris (slower, with gravity arc, rotating)
|
||||
List<DebrisParticle> debris = new List<DebrisParticle>();
|
||||
Sprite actualDebrisSprite = debrisSprite ?? meteorSprite;
|
||||
if (actualDebrisSprite != null) {
|
||||
for (int i = 0; i < debrisCount; i++) {
|
||||
GameObject rock = new GameObject($"Debris_{i}");
|
||||
rock.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
rock.transform.localPosition = new Vector3(target.x, target.y, 3f);
|
||||
|
||||
SpriteRenderer renderer = rock.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = actualDebrisSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = debrisColor;
|
||||
|
||||
float randomScale = debrisScale * Random.Range(0.6f, 1.4f);
|
||||
rock.transform.localScale = Vector3.one * randomScale;
|
||||
|
||||
// Debris flies outward in all directions
|
||||
float angle =
|
||||
(float)i / debrisCount * Mathf.PI * 2f + Random.Range(-0.4f, 0.4f);
|
||||
Vector2 dir = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
|
||||
|
||||
debris.Add(new DebrisParticle {
|
||||
gameObject = rock,
|
||||
direction = dir,
|
||||
speed = debrisSpread * Random.Range(0.6f, 1.2f),
|
||||
rotationSpeed = Random.Range(-400f, 400f),
|
||||
currentRotation = Random.Range(0f, 360f)
|
||||
});
|
||||
objects.Add(rock);
|
||||
}
|
||||
}
|
||||
|
||||
// Explosion animation
|
||||
elapsed = 0f;
|
||||
while (elapsed < explosionDuration) {
|
||||
float t = elapsed / explosionDuration;
|
||||
|
||||
// Expand and fade explosion
|
||||
// Expand and fade explosion with initial flash
|
||||
if (explosion != null) {
|
||||
float scale = Mathf.Lerp(explosionStartScale, explosionEndScale, t);
|
||||
float flashT = t < 0.1f ? 1f + (0.1f - t) * 5f : 1f;
|
||||
float scale = Mathf.Lerp(explosionStartScale, explosionEndScale, t) * flashT;
|
||||
explosion.transform.localScale = Vector3.one * scale;
|
||||
|
||||
Color c = explosionColor;
|
||||
c.a = 1f - t;
|
||||
c.a = (1f - t * t); // Slower fade at start
|
||||
explosionRenderer.color = c;
|
||||
}
|
||||
|
||||
// Animate sparks
|
||||
// Animate fire sparks (fast, straight lines)
|
||||
foreach (var spark in sparks) {
|
||||
if (spark.gameObject == null) continue;
|
||||
|
||||
@@ -262,7 +680,31 @@ namespace Shardok {
|
||||
c.a = 1f - t;
|
||||
renderer.color = c;
|
||||
spark.gameObject.transform.localScale =
|
||||
Vector3.one * trailScale * 1.5f * (1f - t * 0.5f);
|
||||
Vector3.one * trailScale * 1.5f * (1f - t * 0.6f);
|
||||
}
|
||||
}
|
||||
|
||||
// Animate debris (flies outward in all directions, rotating)
|
||||
for (int di = 0; di < debris.Count; di++) {
|
||||
var d = debris[di];
|
||||
if (d.gameObject == null) continue;
|
||||
|
||||
// Debris flies outward, slowing down over time
|
||||
float easeT = t * (2f - t); // Ease out - fast start, slow end
|
||||
Vector2 pos = target + d.direction * d.speed * easeT;
|
||||
d.gameObject.transform.localPosition = new Vector3(pos.x, pos.y, 3f);
|
||||
|
||||
// Spin the debris
|
||||
d.currentRotation += d.rotationSpeed * Time.deltaTime;
|
||||
d.gameObject.transform.localRotation =
|
||||
Quaternion.Euler(0, 0, d.currentRotation);
|
||||
debris[di] = d; // Write back modified struct
|
||||
|
||||
var renderer = d.gameObject.GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = debrisColor;
|
||||
c.a = 1f - t * 0.8f; // Debris fades slower
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,5 +730,13 @@ namespace Shardok {
|
||||
public Vector2 direction;
|
||||
public float speed;
|
||||
}
|
||||
|
||||
private struct DebrisParticle {
|
||||
public GameObject gameObject;
|
||||
public Vector2 direction;
|
||||
public float speed;
|
||||
public float rotationSpeed;
|
||||
public float currentRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,72 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using static Net.Eagle0.Shardok.Api.BattalionView.Types;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates unit movement from source to target hex with smooth interpolation.
|
||||
/// Creates a temporary indicator that slides across the grid.
|
||||
/// Animates unit movement between hex cells using a trail of footprints/hoofprints.
|
||||
/// Boot prints for infantry, horseshoe prints for cavalry.
|
||||
/// Prints appear sequentially then fade out FIFO.
|
||||
/// </summary>
|
||||
public class MoveAnimator : MonoBehaviour {
|
||||
[Header("Print Sprites")]
|
||||
[Tooltip("Boot print sprite (will be flipped for left/right)")]
|
||||
public Sprite bootPrintSprite;
|
||||
|
||||
[Tooltip("Paired horseshoe prints sprite")]
|
||||
public Sprite hoofPrintSprite;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration of the movement in seconds")]
|
||||
public float moveDuration = 0.3f;
|
||||
[Tooltip("Number of prints between hex centers")]
|
||||
public int printsPerHex = 5;
|
||||
|
||||
[Tooltip("Optional sprite to show during movement")]
|
||||
public Sprite moveIndicatorSprite;
|
||||
[Tooltip("Time between each print appearing (seconds)")]
|
||||
public float printInterval = 0.06f;
|
||||
|
||||
[Tooltip("Scale of the movement indicator")]
|
||||
public float indicatorScale = 15f;
|
||||
[Tooltip("How long each print stays fully visible before fading (seconds)")]
|
||||
public float printLingerTime = 0.3f;
|
||||
|
||||
[Tooltip("Height offset for arc movement (0 = straight line)")]
|
||||
public float arcHeight = 0.05f;
|
||||
[Tooltip("Duration of each print's fade out (seconds)")]
|
||||
public float fadeDuration = 0.4f;
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
[Tooltip("Scale of print sprites")]
|
||||
public float printScale = 1.5f;
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
}
|
||||
[Tooltip("Lateral offset for alternating prints")]
|
||||
public float lateralOffset = 6f;
|
||||
|
||||
[Tooltip("Color tint for boot prints")]
|
||||
public Color bootPrintColor = new Color(0.3f, 0.2f, 0.1f, 0.8f);
|
||||
|
||||
[Tooltip("Color tint for hoof prints")]
|
||||
public Color hoofPrintColor = new Color(0.25f, 0.15f, 0.05f, 0.8f);
|
||||
|
||||
private HexGrid _hexGrid;
|
||||
private readonly List<Coroutine> _activeCoroutines = new List<Coroutine>();
|
||||
|
||||
private void Start() { _hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a move animation between two hex cells.
|
||||
/// Multiple animations can run simultaneously.
|
||||
/// Animate movement from source to target hex.
|
||||
/// </summary>
|
||||
public void AnimateMove(int sourceCellIndex, int targetCellIndex) {
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MoveAnimator: No hexGrid or gridCanvas available");
|
||||
/// <param name="sourceCellIndex">Starting hex cell index</param>
|
||||
/// <param name="targetCellIndex">Destination hex cell index</param>
|
||||
/// <param name="unitType">Type of battalion (determines boot vs hoof prints)</param>
|
||||
public void
|
||||
AnimateMove(int sourceCellIndex, int targetCellIndex, BattalionTypeId unitType) {
|
||||
if (bootPrintSprite == null && hoofPrintSprite == null) {
|
||||
Debug.LogWarning("MoveAnimator: No print sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = hexGrid.GetCellLocalPosition(sourceCellIndex);
|
||||
Vector2? targetPos = hexGrid.GetCellLocalPosition(targetCellIndex);
|
||||
if (_hexGrid == null || _hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("MoveAnimator: No HexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = _hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = _hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
@@ -47,45 +74,122 @@ namespace Shardok {
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateMovement(sourcePos.Value, targetPos.Value));
|
||||
bool isMounted = unitType == BattalionTypeId.LightCavalry ||
|
||||
unitType == BattalionTypeId.HeavyCavalry;
|
||||
|
||||
StartCoroutine(SpawnPrintTrail(sourcePos.Value, targetPos.Value, isMounted));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateMovement(Vector2 source, Vector2 target) {
|
||||
// Create movement indicator if sprite assigned
|
||||
GameObject indicator = null;
|
||||
if (moveIndicatorSprite != null) {
|
||||
indicator = new GameObject("MoveIndicator");
|
||||
indicator.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
/// <summary>
|
||||
/// Overload for when unit type is not available - defaults to boot prints.
|
||||
/// </summary>
|
||||
public void AnimateMove(int sourceCellIndex, int targetCellIndex) {
|
||||
AnimateMove(sourceCellIndex, targetCellIndex, BattalionTypeId.LightInfantry);
|
||||
}
|
||||
|
||||
var renderer = indicator.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = moveIndicatorSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
indicator.transform.localScale = Vector3.one * indicatorScale;
|
||||
private IEnumerator SpawnPrintTrail(Vector2 source, Vector2 target, bool isMounted) {
|
||||
var prints = new List<GameObject>();
|
||||
Vector2 direction = (target - source).normalized;
|
||||
float totalDistance = Vector2.Distance(source, target);
|
||||
|
||||
// Calculate rotation angle for prints to face direction of travel
|
||||
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg - 90f;
|
||||
|
||||
Sprite printSprite = isMounted ? (hoofPrintSprite ?? bootPrintSprite)
|
||||
: (bootPrintSprite ?? hoofPrintSprite);
|
||||
Color printColor = isMounted ? hoofPrintColor : bootPrintColor;
|
||||
|
||||
// Spawn prints along the path
|
||||
for (int i = 0; i < printsPerHex; i++) {
|
||||
float t = (i + 1f) / (printsPerHex + 1f);
|
||||
Vector2 position = Vector2.Lerp(source, target, t);
|
||||
|
||||
// Alternate left/right for boot prints (flip horizontally)
|
||||
bool flipX = !isMounted && (i % 2 == 1);
|
||||
|
||||
// Lateral offset for alternating prints (feet or front/rear hooves)
|
||||
Vector2 perpendicular = new Vector2(-direction.y, direction.x);
|
||||
float offset = (i % 2 == 0) ? -lateralOffset : lateralOffset;
|
||||
position += perpendicular * offset;
|
||||
|
||||
GameObject print = CreatePrint(position, angle, printSprite, printColor, flipX);
|
||||
prints.Add(print);
|
||||
|
||||
// Start FIFO fade coroutine for this print
|
||||
var fadeCoroutine =
|
||||
StartCoroutine(FadePrintFIFO(print, printLingerTime + i * printInterval));
|
||||
_activeCoroutines.Add(fadeCoroutine);
|
||||
|
||||
yield return new WaitForSeconds(printInterval);
|
||||
}
|
||||
|
||||
// Wait for all fades to complete
|
||||
float totalWaitTime = printLingerTime + fadeDuration + 0.1f;
|
||||
yield return new WaitForSeconds(totalWaitTime);
|
||||
|
||||
// Cleanup any remaining prints
|
||||
foreach (var print in prints) {
|
||||
if (print != null) { Destroy(print); }
|
||||
}
|
||||
|
||||
_activeCoroutines.Clear();
|
||||
}
|
||||
|
||||
private GameObject
|
||||
CreatePrint(Vector2 position, float angle, Sprite sprite, Color color, bool flipX) {
|
||||
GameObject print = new GameObject("Footprint");
|
||||
print.transform.SetParent(_hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer renderer = print.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = sprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.sortingOrder = -1; // Below other effects
|
||||
renderer.color = color;
|
||||
|
||||
print.transform.localPosition = new Vector3(position.x, position.y, 0);
|
||||
print.transform.localRotation = Quaternion.Euler(0, 0, angle);
|
||||
|
||||
Vector3 scale = new Vector3(flipX ? -printScale : printScale, printScale, printScale);
|
||||
print.transform.localScale = scale;
|
||||
|
||||
return print;
|
||||
}
|
||||
|
||||
private IEnumerator FadePrintFIFO(GameObject print, float delay) {
|
||||
// Wait before starting fade (FIFO - earlier prints fade first)
|
||||
yield return new WaitForSeconds(delay);
|
||||
|
||||
if (print == null) yield break;
|
||||
|
||||
SpriteRenderer renderer = print.GetComponent<SpriteRenderer>();
|
||||
if (renderer == null) yield break;
|
||||
|
||||
Color startColor = renderer.color;
|
||||
float elapsed = 0f;
|
||||
float distance = Vector2.Distance(source, target);
|
||||
|
||||
while (elapsed < moveDuration) {
|
||||
float t = elapsed / moveDuration;
|
||||
float smoothT = t * t * (3f - 2f * t); // Smooth step
|
||||
while (elapsed < fadeDuration) {
|
||||
if (print == null || renderer == null) yield break;
|
||||
|
||||
Vector3 pos = Vector3.Lerp(
|
||||
new Vector3(source.x, source.y, 5f),
|
||||
new Vector3(target.x, target.y, 5f),
|
||||
smoothT);
|
||||
|
||||
// Add slight arc on Y axis for visible height in top-down view
|
||||
float arc = 4f * arcHeight * distance * t * (1f - t);
|
||||
pos.y += arc;
|
||||
|
||||
if (indicator != null) { indicator.transform.localPosition = pos; }
|
||||
float t = elapsed / fadeDuration;
|
||||
Color newColor = startColor;
|
||||
newColor.a = Mathf.Lerp(startColor.a, 0f, t);
|
||||
renderer.color = newColor;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (indicator != null) { Destroy(indicator); }
|
||||
if (print != null) { Destroy(print); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all active print animations.
|
||||
/// </summary>
|
||||
public void CancelAllAnimations() {
|
||||
foreach (var coroutine in _activeCoroutines) {
|
||||
if (coroutine != null) { StopCoroutine(coroutine); }
|
||||
}
|
||||
_activeCoroutines.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-11
@@ -44,25 +44,21 @@ namespace Shardok {
|
||||
[Tooltip("Duration of rising phase")]
|
||||
public float riseDuration = 0.8f;
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("HexGrid to get canvas reference from (auto-found if not set)")]
|
||||
public HexGrid hexGrid;
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() {
|
||||
if (hexGrid == null) { hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
}
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a raise dead animation at the target hex cell.
|
||||
/// Multiple animations can run simultaneously.
|
||||
/// </summary>
|
||||
public void AnimateRaiseDead(int targetCellIndex) {
|
||||
if (hexGrid == null || hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("RaiseDeadAnimator: No hexGrid or gridCanvas available");
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("RaiseDeadAnimator: No __hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = hexGrid.GetCellLocalPosition(targetCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"RaiseDeadAnimator: Invalid cell index {targetCellIndex}");
|
||||
@@ -81,7 +77,7 @@ namespace Shardok {
|
||||
|
||||
if (glowSprite != null) {
|
||||
glow = new GameObject("RaiseDeadGlow");
|
||||
glow.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
glow.transform.localPosition = new Vector3(target.x, target.y, 8f);
|
||||
glow.transform.localScale = Vector3.zero;
|
||||
|
||||
@@ -112,7 +108,7 @@ namespace Shardok {
|
||||
if (figureSprite == null) break;
|
||||
|
||||
GameObject figure = new GameObject($"RisingUndead_{i}");
|
||||
figure.transform.SetParent(hexGrid.gridCanvas.transform, false);
|
||||
figure.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer figureRenderer = figure.AddComponent<SpriteRenderer>();
|
||||
figureRenderer.sprite = figureSprite;
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates scouting action with an eye at source, a vision cone extending
|
||||
/// toward target, and a glow that travels with the cone tip and grows to
|
||||
/// cover the target hex and its neighbors.
|
||||
/// </summary>
|
||||
public class ScoutAnimator : MonoBehaviour {
|
||||
[Header("Eye Settings")]
|
||||
[Tooltip("Sprite for the eye/binoculars icon")]
|
||||
public Sprite eyeSprite;
|
||||
|
||||
[Tooltip("Color of the eye")]
|
||||
public Color eyeColor = new Color(0.9f, 0.9f, 0.3f, 0.9f);
|
||||
|
||||
[Tooltip("Scale of the eye sprite")]
|
||||
public float eyeScale = 15f;
|
||||
|
||||
[Header("Vision Cone Settings")]
|
||||
[Tooltip("Sprite for vision cone/beam (triangle pointing right)")]
|
||||
public Sprite coneSprite;
|
||||
|
||||
[Tooltip("Color of the vision cone")]
|
||||
public Color coneColor = new Color(1f, 1f, 0.5f, 0.3f);
|
||||
|
||||
[Tooltip("Width of the vision cone at base")]
|
||||
public float coneWidth = 30f;
|
||||
|
||||
[Tooltip("Rotation offset for cone sprite (degrees). +90 for upward-pointing triangle")]
|
||||
public float coneRotationOffset = 90f;
|
||||
|
||||
[Header("Glow Settings")]
|
||||
[Tooltip("Sprite for the traveling glow (circle)")]
|
||||
public Sprite glowSprite;
|
||||
|
||||
[Tooltip("Color of the glow")]
|
||||
public Color glowColor = new Color(1f, 1f, 0.7f, 0.5f);
|
||||
|
||||
[Tooltip("Starting scale of glow (at source)")]
|
||||
public float glowStartScale = 10f;
|
||||
|
||||
[Tooltip("Final scale of glow in hex radii (2.5 = covers 7-hex area)")]
|
||||
public float glowEndRadii = 2.5f;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration of cone extension")]
|
||||
public float extendDuration = 0.5f;
|
||||
|
||||
[Tooltip("Duration the glow holds at target")]
|
||||
public float holdDuration = 0.4f;
|
||||
|
||||
[Tooltip("Duration of fade out")]
|
||||
public float fadeDuration = 0.3f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a scout animation from source toward target hex.
|
||||
/// </summary>
|
||||
public void AnimateScout(int sourceCellIndex, int targetCellIndex) {
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("ScoutAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (sourcePos == null || targetPos == null) {
|
||||
Debug.LogWarning(
|
||||
$"ScoutAnimator: Invalid cell indices {sourceCellIndex} or {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
float hexRadius = __hexGrid.GetHexInnerRadius();
|
||||
float glowEndScale = hexRadius * glowEndRadii * 2f;
|
||||
|
||||
StartCoroutine(AnimateScoutEffect(sourcePos.Value, targetPos.Value, glowEndScale));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateScoutEffect(Vector2 source, Vector2 target, float glowEndScale) {
|
||||
Vector2 direction = (target - source).normalized;
|
||||
float baseAngle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
|
||||
float distance = Vector2.Distance(source, target);
|
||||
|
||||
// Create eye at source
|
||||
GameObject eye = null;
|
||||
SpriteRenderer eyeRenderer = null;
|
||||
if (eyeSprite != null) {
|
||||
eye = new GameObject("ScoutEye");
|
||||
eye.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
eye.transform.localPosition = new Vector3(source.x, source.y, 5f);
|
||||
eye.transform.localScale = Vector3.zero;
|
||||
|
||||
eyeRenderer = eye.AddComponent<SpriteRenderer>();
|
||||
eyeRenderer.sprite = eyeSprite;
|
||||
eyeRenderer.sortingLayerName = "Effects";
|
||||
eyeRenderer.color = eyeColor;
|
||||
}
|
||||
|
||||
// Create vision cone
|
||||
GameObject cone = null;
|
||||
SpriteRenderer coneRenderer = null;
|
||||
if (coneSprite != null) {
|
||||
cone = new GameObject("VisionCone");
|
||||
cone.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
cone.transform.localPosition = new Vector3(source.x, source.y, 6f);
|
||||
cone.transform.localRotation =
|
||||
Quaternion.Euler(0, 0, baseAngle + coneRotationOffset);
|
||||
cone.transform.localScale = new Vector3(coneWidth, 0f, 1f);
|
||||
|
||||
coneRenderer = cone.AddComponent<SpriteRenderer>();
|
||||
coneRenderer.sprite = coneSprite;
|
||||
coneRenderer.sortingLayerName = "Effects";
|
||||
coneRenderer.color = coneColor;
|
||||
}
|
||||
|
||||
// Create traveling glow
|
||||
GameObject glow = null;
|
||||
SpriteRenderer glowRenderer = null;
|
||||
if (glowSprite != null) {
|
||||
glow = new GameObject("ScoutGlow");
|
||||
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
glow.transform.localPosition = new Vector3(source.x, source.y, 7f);
|
||||
glow.transform.localScale = Vector3.one * glowStartScale;
|
||||
|
||||
glowRenderer = glow.AddComponent<SpriteRenderer>();
|
||||
glowRenderer.sprite = glowSprite;
|
||||
glowRenderer.sortingLayerName = "Effects";
|
||||
glowRenderer.color = glowColor;
|
||||
}
|
||||
|
||||
// Eye appearance phase
|
||||
float appearDuration = 0.15f;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < appearDuration) {
|
||||
float t = elapsed / appearDuration;
|
||||
|
||||
if (eye != null) {
|
||||
float scale = eyeScale * t * (2f - t);
|
||||
eye.transform.localScale = Vector3.one * scale;
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (eye != null) { eye.transform.localScale = Vector3.one * eyeScale; }
|
||||
|
||||
// Cone extension phase - cone extends, glow travels and grows
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < extendDuration) {
|
||||
float t = elapsed / extendDuration;
|
||||
float easeT = t * (2f - t); // Ease out
|
||||
|
||||
// Extend cone - Y axis is length after rotation, X is width
|
||||
if (cone != null) {
|
||||
float length = distance * easeT;
|
||||
Vector2 conePos = source + direction * (length * 0.5f);
|
||||
cone.transform.localPosition = new Vector3(conePos.x, conePos.y, 6f);
|
||||
cone.transform.localScale =
|
||||
new Vector3(coneWidth * (1f - easeT * 0.3f), length, 1f);
|
||||
|
||||
Color c = coneColor;
|
||||
c.a = coneColor.a * (0.8f + 0.2f * Mathf.Sin(elapsed * 15f));
|
||||
coneRenderer.color = c;
|
||||
}
|
||||
|
||||
// Move and grow glow at cone tip
|
||||
if (glow != null) {
|
||||
Vector2 glowPos = source + direction * distance * easeT;
|
||||
glow.transform.localPosition = new Vector3(glowPos.x, glowPos.y, 7f);
|
||||
|
||||
float glowScale = Mathf.Lerp(glowStartScale, glowEndScale, easeT);
|
||||
glow.transform.localScale = Vector3.one * glowScale;
|
||||
|
||||
Color c = glowColor;
|
||||
c.a = glowColor.a * (0.7f + 0.3f * Mathf.Sin(elapsed * 12f));
|
||||
glowRenderer.color = c;
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Ensure final positions
|
||||
if (cone != null) {
|
||||
Vector2 conePos = source + direction * (distance * 0.5f);
|
||||
cone.transform.localPosition = new Vector3(conePos.x, conePos.y, 6f);
|
||||
cone.transform.localScale = new Vector3(coneWidth * 0.7f, distance, 1f);
|
||||
}
|
||||
if (glow != null) {
|
||||
glow.transform.localPosition = new Vector3(target.x, target.y, 7f);
|
||||
glow.transform.localScale = Vector3.one * glowEndScale;
|
||||
}
|
||||
|
||||
// Hold phase - glow pulses at target
|
||||
elapsed = 0f;
|
||||
while (elapsed < holdDuration) {
|
||||
float t = elapsed / holdDuration;
|
||||
|
||||
if (glowRenderer != null) {
|
||||
Color c = glowColor;
|
||||
c.a = glowColor.a * (0.8f + 0.2f * Mathf.Sin(elapsed * 10f));
|
||||
glowRenderer.color = c;
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Fade out phase
|
||||
elapsed = 0f;
|
||||
while (elapsed < fadeDuration) {
|
||||
float t = elapsed / fadeDuration;
|
||||
|
||||
if (eyeRenderer != null) {
|
||||
Color c = eyeColor;
|
||||
c.a = eyeColor.a * (1f - t);
|
||||
eyeRenderer.color = c;
|
||||
}
|
||||
|
||||
if (coneRenderer != null) {
|
||||
Color c = coneColor;
|
||||
c.a = coneColor.a * (1f - t);
|
||||
coneRenderer.color = c;
|
||||
}
|
||||
|
||||
if (glowRenderer != null) {
|
||||
Color c = glowColor;
|
||||
c.a = glowColor.a * (1f - t);
|
||||
glowRenderer.color = c;
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (eye != null) { Destroy(eye); }
|
||||
if (cone != null) { Destroy(cone); }
|
||||
if (glow != null) { Destroy(glow); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: daed3cae2aa74ac2a50c1b70232beb33
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+268
-15
@@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using common;
|
||||
using eagle0;
|
||||
using Eagle0.Tutorial;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using TMPro;
|
||||
@@ -35,7 +36,10 @@ namespace Shardok {
|
||||
Fear,
|
||||
// Magic
|
||||
LightningBolt,
|
||||
MeteorStart,
|
||||
MeteorTarget,
|
||||
MeteorCast,
|
||||
MeteorCancel,
|
||||
HolyWave,
|
||||
RaiseDead,
|
||||
Control,
|
||||
@@ -115,6 +119,19 @@ namespace Shardok {
|
||||
public CatapultAnimator catapultAnimator;
|
||||
public RaiseDeadAnimator raiseDeadAnimator;
|
||||
public MeteorAnimator meteorAnimator;
|
||||
public HolyWaveAnimator holyWaveAnimator;
|
||||
public ChargeAnimator chargeAnimator;
|
||||
public ToolAnimator toolAnimator;
|
||||
public FearAnimator fearAnimator;
|
||||
public ControlAnimator controlAnimator;
|
||||
public FireEffectAnimator fireEffectAnimator;
|
||||
public ExtinguishAnimator extinguishAnimator;
|
||||
public FreezeAnimator freezeAnimator;
|
||||
public WaterEffectAnimator waterEffectAnimator;
|
||||
public ScoutAnimator scoutAnimator;
|
||||
public DismissAnimator dismissAnimator;
|
||||
public FleeAnimator fleeAnimator;
|
||||
public DuelAnimator duelAnimator;
|
||||
|
||||
public CommandTypeUIManager commandTypeUIManager;
|
||||
public ActionResultTypeManager actionResultTypeManager;
|
||||
@@ -228,6 +245,8 @@ namespace Shardok {
|
||||
turnHistoryPanel.Clear();
|
||||
this.gameObject.SetActive(false);
|
||||
} else {
|
||||
// Notify tutorial system of turn end
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnTurnEnded();
|
||||
Model.EndTurn();
|
||||
}
|
||||
}
|
||||
@@ -378,8 +397,6 @@ namespace Shardok {
|
||||
|
||||
Model = shardokGameModel;
|
||||
|
||||
Model.UpdateAction = ModelUpdated;
|
||||
|
||||
HexMap map = Model.Map;
|
||||
Texture[] textures = new Texture[map.RowCount * map.ColumnCount];
|
||||
Coords coords = new Coords();
|
||||
@@ -405,10 +422,17 @@ namespace Shardok {
|
||||
hexGrid.SetUp();
|
||||
hexGrid.gameObject.SetActive(true);
|
||||
|
||||
// Set UpdateAction after hexGrid is ready to avoid race condition
|
||||
Model.UpdateAction = ModelUpdated;
|
||||
|
||||
SetHeroLabels();
|
||||
|
||||
MainQueue.Q.EnqueueForNextUpdate(() => { ModelUpdated(); });
|
||||
|
||||
// Initialize tutorial system for tactical combat
|
||||
TutorialManager.Instance?.Initialize(null, this);
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnBattleEntered(Model);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.playModeStateChanged += (state) => OnPlayModeStateChanged(state);
|
||||
#endif
|
||||
@@ -528,6 +552,9 @@ namespace Shardok {
|
||||
var historyEntry = Model.History[i];
|
||||
turnHistoryPanel.AddLine(GetActionResultDescription(historyEntry));
|
||||
|
||||
// Notify tutorial system of battle action
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnBattleAction(historyEntry);
|
||||
|
||||
ActionType type = historyEntry.Type;
|
||||
AnimationType animationType = AnimationTypeForAction(type);
|
||||
|
||||
@@ -552,7 +579,12 @@ namespace Shardok {
|
||||
if (Model.UnitsById.TryGetValue(
|
||||
historyEntry.Actor.Value,
|
||||
out var actorUnit)) {
|
||||
int sourceIndex = MapCoordsToGridIndex(actorUnit.Location);
|
||||
// For Move actions, use stored source coords (unit's pre-move location)
|
||||
// For other actions, use current unit location
|
||||
int sourceIndex =
|
||||
Model.MoveSourceCoords.TryGetValue(i, out var sourceCoords)
|
||||
? MapCoordsToGridIndex(sourceCoords)
|
||||
: MapCoordsToGridIndex(actorUnit.Location);
|
||||
|
||||
// Get target - either coords (archery) or unit location (melee)
|
||||
int targetIndex = -1;
|
||||
@@ -1165,6 +1197,9 @@ namespace Shardok {
|
||||
}
|
||||
_selectedGridIndex = selectedIndex;
|
||||
RedrawCommandOverlays(selectedIndex, selectedIndex);
|
||||
|
||||
// Notify tutorial system of unit selection
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnUnitSelected(selectedIndex);
|
||||
}
|
||||
|
||||
public void HandleActionClick(int clickedIndex) {
|
||||
@@ -1330,8 +1365,8 @@ namespace Shardok {
|
||||
case CommandType.MoveCommand: return AnimationType.Move;
|
||||
// Magic
|
||||
case CommandType.LightningBoltCommand: return AnimationType.LightningBolt;
|
||||
case CommandType.MeteorStartCommand:
|
||||
case CommandType.MeteorTargetCommand: return AnimationType.MeteorCast;
|
||||
case CommandType.MeteorStartCommand: return AnimationType.MeteorStart;
|
||||
case CommandType.MeteorTargetCommand: return AnimationType.MeteorTarget;
|
||||
case CommandType.HolyWaveCommand: return AnimationType.HolyWave;
|
||||
case CommandType.RaiseDeadCommand: return AnimationType.RaiseDead;
|
||||
case CommandType.ControlCommand: return AnimationType.Control;
|
||||
@@ -1370,9 +1405,10 @@ namespace Shardok {
|
||||
case ActionType.FearAttack: return AnimationType.Fear;
|
||||
// Magic
|
||||
case ActionType.LightningBolt: return AnimationType.LightningBolt;
|
||||
case ActionType.MeteorStart:
|
||||
case ActionType.MeteorTarget:
|
||||
case ActionType.MeteorStart: return AnimationType.MeteorStart;
|
||||
case ActionType.MeteorTarget: return AnimationType.MeteorTarget;
|
||||
case ActionType.MeteorCast: return AnimationType.MeteorCast;
|
||||
case ActionType.MeteorCancel: return AnimationType.MeteorCancel;
|
||||
case ActionType.HolyWave:
|
||||
case ActionType.HolyWaveDamage: return AnimationType.HolyWave;
|
||||
case ActionType.RaisedUndead: return AnimationType.RaiseDead;
|
||||
@@ -1394,9 +1430,7 @@ namespace Shardok {
|
||||
case ActionType.Reduce: return AnimationType.Reduce;
|
||||
case ActionType.Fled: return AnimationType.Flee;
|
||||
// Other
|
||||
case ActionType.DuelChallenged:
|
||||
case ActionType.DuelAccepted:
|
||||
case ActionType.DuelDeclined: return AnimationType.Duel;
|
||||
case ActionType.DuelAccepted: return AnimationType.Duel;
|
||||
default: return AnimationType.None;
|
||||
}
|
||||
}
|
||||
@@ -1414,7 +1448,10 @@ namespace Shardok {
|
||||
case AnimationType.Fear: return ActionType.FearAttack;
|
||||
// Magic
|
||||
case AnimationType.LightningBolt: return ActionType.LightningBolt;
|
||||
case AnimationType.MeteorStart: return ActionType.MeteorStart;
|
||||
case AnimationType.MeteorTarget: return ActionType.MeteorTarget;
|
||||
case AnimationType.MeteorCast: return ActionType.MeteorCast;
|
||||
case AnimationType.MeteorCancel: return ActionType.MeteorCancel;
|
||||
case AnimationType.HolyWave: return ActionType.HolyWave;
|
||||
case AnimationType.RaiseDead: return ActionType.RaisedUndead;
|
||||
case AnimationType.Control: return ActionType.Controlled;
|
||||
@@ -1435,7 +1472,7 @@ namespace Shardok {
|
||||
case AnimationType.Reduce: return ActionType.Reduce;
|
||||
case AnimationType.Flee: return ActionType.Fled;
|
||||
// Other
|
||||
case AnimationType.Duel: return ActionType.DuelChallenged;
|
||||
case AnimationType.Duel: return ActionType.DuelAccepted;
|
||||
default: return ActionType.UnknownAction;
|
||||
}
|
||||
}
|
||||
@@ -1538,8 +1575,11 @@ namespace Shardok {
|
||||
}
|
||||
break;
|
||||
case AnimationType.Move:
|
||||
if (moveAnimator != null) {
|
||||
moveAnimator.AnimateMove(sourceGridIndex, targetGridIndex);
|
||||
if (moveAnimator != null && attackerUnit != null) {
|
||||
moveAnimator.AnimateMove(
|
||||
sourceGridIndex,
|
||||
targetGridIndex,
|
||||
attackerUnit.Battalion.Type);
|
||||
}
|
||||
break;
|
||||
case AnimationType.LightningBolt:
|
||||
@@ -1557,11 +1597,97 @@ namespace Shardok {
|
||||
raiseDeadAnimator.AnimateRaiseDead(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.MeteorStart:
|
||||
if (meteorAnimator != null) {
|
||||
meteorAnimator.AnimateMeteorStart(sourceGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.MeteorTarget:
|
||||
if (meteorAnimator != null) {
|
||||
meteorAnimator.AnimateMeteorTarget(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.MeteorCast:
|
||||
if (meteorAnimator != null) {
|
||||
meteorAnimator.AnimateMeteorCast(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.MeteorCancel:
|
||||
if (meteorAnimator != null) {
|
||||
meteorAnimator.AnimateMeteorCancel(sourceGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.HolyWave:
|
||||
if (holyWaveAnimator != null) {
|
||||
var undeadCells = FindNearbyUndeadCells(sourceGridIndex, 2);
|
||||
holyWaveAnimator.AnimateHolyWave(sourceGridIndex, undeadCells);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Charge:
|
||||
if (chargeAnimator != null && attackerUnit != null && defenderUnit != null) {
|
||||
chargeAnimator.AnimateCharge(
|
||||
sourceGridIndex,
|
||||
targetGridIndex,
|
||||
attackerUnit.Battalion.Type,
|
||||
defenderUnit.Battalion.Type);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Fear:
|
||||
if (fearAnimator != null) {
|
||||
fearAnimator.AnimateFear(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Control:
|
||||
if (controlAnimator != null) {
|
||||
controlAnimator.AnimateControl(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.StartFire:
|
||||
case AnimationType.FireDamage:
|
||||
if (fireEffectAnimator != null) {
|
||||
fireEffectAnimator.AnimateFire(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.ExtinguishFire:
|
||||
if (extinguishAnimator != null) {
|
||||
extinguishAnimator.AnimateExtinguish(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.FreezeWater:
|
||||
if (freezeAnimator != null) { freezeAnimator.AnimateFreeze(targetGridIndex); }
|
||||
break;
|
||||
case AnimationType.BuildBridge:
|
||||
if (toolAnimator != null) { toolAnimator.AnimateBuildBridge(targetGridIndex); }
|
||||
break;
|
||||
case AnimationType.Repair:
|
||||
if (toolAnimator != null) { toolAnimator.AnimateRepair(targetGridIndex); }
|
||||
break;
|
||||
case AnimationType.BraveWater:
|
||||
case AnimationType.WaterDamage:
|
||||
if (waterEffectAnimator != null) {
|
||||
waterEffectAnimator.AnimateWaterSplash(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Scout:
|
||||
if (scoutAnimator != null) {
|
||||
scoutAnimator.AnimateScout(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.DismissUnit:
|
||||
if (dismissAnimator != null) {
|
||||
dismissAnimator.AnimateDismiss(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Flee:
|
||||
if (fleeAnimator != null) {
|
||||
fleeAnimator.AnimateFlee(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Duel:
|
||||
if (duelAnimator != null) {
|
||||
duelAnimator.AnimateDuel(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1612,8 +1738,11 @@ namespace Shardok {
|
||||
}
|
||||
break;
|
||||
case AnimationType.Move:
|
||||
if (moveAnimator != null) {
|
||||
moveAnimator.AnimateMove(sourceGridIndex, targetGridIndex);
|
||||
if (moveAnimator != null && attackerUnit != null) {
|
||||
moveAnimator.AnimateMove(
|
||||
sourceGridIndex,
|
||||
targetGridIndex,
|
||||
attackerUnit.Battalion.Type);
|
||||
}
|
||||
break;
|
||||
case AnimationType.LightningBolt:
|
||||
@@ -1631,14 +1760,138 @@ namespace Shardok {
|
||||
raiseDeadAnimator.AnimateRaiseDead(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.MeteorStart:
|
||||
if (meteorAnimator != null) {
|
||||
meteorAnimator.AnimateMeteorStart(sourceGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.MeteorTarget:
|
||||
if (meteorAnimator != null) {
|
||||
meteorAnimator.AnimateMeteorTarget(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.MeteorCast:
|
||||
if (meteorAnimator != null) {
|
||||
meteorAnimator.AnimateMeteorCast(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.MeteorCancel:
|
||||
if (meteorAnimator != null) {
|
||||
meteorAnimator.AnimateMeteorCancel(sourceGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.HolyWave:
|
||||
if (holyWaveAnimator != null) {
|
||||
var undeadCells = FindNearbyUndeadCells(sourceGridIndex, 2);
|
||||
holyWaveAnimator.AnimateHolyWave(sourceGridIndex, undeadCells);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Charge:
|
||||
if (chargeAnimator != null && attackerUnit != null && defenderUnit != null) {
|
||||
chargeAnimator.AnimateCharge(
|
||||
sourceGridIndex,
|
||||
targetGridIndex,
|
||||
attackerUnit.Battalion.Type,
|
||||
defenderUnit.Battalion.Type);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Fear:
|
||||
if (fearAnimator != null) {
|
||||
fearAnimator.AnimateFear(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Control:
|
||||
if (controlAnimator != null) {
|
||||
controlAnimator.AnimateControl(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.StartFire:
|
||||
case AnimationType.FireDamage:
|
||||
if (fireEffectAnimator != null) {
|
||||
fireEffectAnimator.AnimateFire(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.ExtinguishFire:
|
||||
if (extinguishAnimator != null) {
|
||||
extinguishAnimator.AnimateExtinguish(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.FreezeWater:
|
||||
if (freezeAnimator != null) { freezeAnimator.AnimateFreeze(targetGridIndex); }
|
||||
break;
|
||||
case AnimationType.BuildBridge:
|
||||
if (toolAnimator != null) { toolAnimator.AnimateBuildBridge(targetGridIndex); }
|
||||
break;
|
||||
case AnimationType.Repair:
|
||||
if (toolAnimator != null) { toolAnimator.AnimateRepair(targetGridIndex); }
|
||||
break;
|
||||
case AnimationType.BraveWater:
|
||||
case AnimationType.WaterDamage:
|
||||
if (waterEffectAnimator != null) {
|
||||
waterEffectAnimator.AnimateWaterSplash(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Scout:
|
||||
if (scoutAnimator != null) {
|
||||
scoutAnimator.AnimateScout(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.DismissUnit:
|
||||
if (dismissAnimator != null) {
|
||||
dismissAnimator.AnimateDismiss(targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Flee:
|
||||
if (fleeAnimator != null) {
|
||||
fleeAnimator.AnimateFlee(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
case AnimationType.Duel:
|
||||
if (duelAnimator != null) {
|
||||
duelAnimator.AnimateDuel(sourceGridIndex, targetGridIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds grid indices of cells containing undead units within a hex radius of the source.
|
||||
/// </summary>
|
||||
List<int> FindNearbyUndeadCells(int sourceCellIndex, int maxDistance) {
|
||||
var result = new List<int>();
|
||||
Coords sourceCoords = GridIndexToMapCoords(sourceCellIndex);
|
||||
|
||||
foreach (var unit in Model.Units) {
|
||||
if (unit.Battalion.Type != BattalionTypeId.Undead) continue;
|
||||
|
||||
int distance = HexDistance(sourceCoords, unit.Location);
|
||||
|
||||
if (distance > 0 && distance <= maxDistance) {
|
||||
result.Add(MapCoordsToGridIndex(unit.Location));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates hex distance between two offset coordinates.
|
||||
/// Converts to cube coordinates for accurate hex distance calculation.
|
||||
/// </summary>
|
||||
static int HexDistance(Coords a, Coords b) {
|
||||
// Convert offset coordinates to cube coordinates (odd-q vertical layout)
|
||||
int ax = a.Column;
|
||||
int az = a.Row - (a.Column - (a.Column & 1)) / 2;
|
||||
int ay = -ax - az;
|
||||
|
||||
int bx = b.Column;
|
||||
int bz = b.Row - (b.Column - (b.Column & 1)) / 2;
|
||||
int by = -bx - bz;
|
||||
|
||||
// Cube distance
|
||||
return (Math.Abs(ax - bx) + Math.Abs(ay - by) + Math.Abs(az - bz)) / 2;
|
||||
}
|
||||
|
||||
void PerformAction(int startGridIndex, int finishGridIndex) {
|
||||
Coords startCoords = GridIndexToMapCoords(startGridIndex);
|
||||
Coords finishCoords = GridIndexToMapCoords(finishGridIndex);
|
||||
|
||||
@@ -75,6 +75,13 @@ public class ShardokGameModel {
|
||||
ReserveUnitsById.Values.FirstOrDefault(u => u.Location.Equals(location));
|
||||
|
||||
public List<ActionResultView> History { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the source coordinates for Move actions, keyed by history index.
|
||||
/// Used for animations since the model is fully updated before animations play.
|
||||
/// </summary>
|
||||
public Dictionary<int, Coords> MoveSourceCoords { get; } = new();
|
||||
|
||||
public PlayerId PlayerId { get; private set; }
|
||||
public GameStatus GameStatus { get; private set; }
|
||||
public RoundId CurrentRound { get; private set; }
|
||||
@@ -402,6 +409,14 @@ public class ShardokGameModel {
|
||||
}
|
||||
|
||||
private void HandleNewHistoryEntry(ActionResultView entry) {
|
||||
// For Move actions, record the source coordinates BEFORE applying the diff
|
||||
// This is needed for animations since all diffs are applied before animations play
|
||||
if (entry.Type == ActionType.Move && entry.Actor != null) {
|
||||
if (UnitsById.TryGetValue(entry.Actor.Value, out var actorUnit)) {
|
||||
MoveSourceCoords[History.Count] = actorUnit.Location;
|
||||
}
|
||||
}
|
||||
|
||||
History.Add(entry);
|
||||
var gsvDiff = entry.GameStateViewDiff;
|
||||
if (gsvDiff == null) {
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates construction/repair actions with a hammer/tool striking effect.
|
||||
/// Used for Repair and BuildBridge commands.
|
||||
/// </summary>
|
||||
public class ToolAnimator : MonoBehaviour {
|
||||
[Header("Tool Sprites")]
|
||||
[Tooltip("Hammer sprite for repair actions")]
|
||||
public Sprite hammerSprite;
|
||||
|
||||
[Tooltip("Alternate tool sprite (wrench, etc)")]
|
||||
public Sprite alternateToolSprite;
|
||||
|
||||
[Header("Spark Settings")]
|
||||
[Tooltip("Sprite for sparks on impact")]
|
||||
public Sprite sparkSprite;
|
||||
|
||||
[Tooltip("Number of sparks per strike")]
|
||||
public int sparkCount = 3;
|
||||
|
||||
[Tooltip("Spark spread distance")]
|
||||
public float sparkSpread = 15f;
|
||||
|
||||
[Tooltip("Color of sparks")]
|
||||
public Color sparkColor = new Color(1f, 0.9f, 0.3f, 1f);
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Scale of the tool sprite")]
|
||||
public float toolScale = 20f;
|
||||
|
||||
[Tooltip("Number of hammer strikes")]
|
||||
public int strikeCount = 3;
|
||||
|
||||
[Tooltip("Duration of each strike cycle")]
|
||||
public float strikeDuration = 0.2f;
|
||||
|
||||
[Tooltip("Height of tool swing arc")]
|
||||
public float swingHeight = 30f;
|
||||
|
||||
[Tooltip("Rotation arc of the swing (degrees)")]
|
||||
public float swingArc = 45f;
|
||||
|
||||
[Tooltip("Base rotation offset for hammer (degrees)")]
|
||||
public float hammerBaseRotation = 180f;
|
||||
|
||||
[Tooltip("Flip hammer horizontally")]
|
||||
public bool hammerFlipHorizontal = true;
|
||||
|
||||
[Tooltip("Base rotation offset for alternate tool (degrees)")]
|
||||
public float alternateBaseRotation = 0f;
|
||||
|
||||
[Tooltip("Flip alternate tool horizontally")]
|
||||
public bool alternateFlipHorizontal = false;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a repair/construction animation at the target hex.
|
||||
/// </summary>
|
||||
public void AnimateRepair(int targetCellIndex) { AnimateTool(targetCellIndex, false); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a bridge building animation at the target hex.
|
||||
/// </summary>
|
||||
public void AnimateBuildBridge(int targetCellIndex) { AnimateTool(targetCellIndex, true); }
|
||||
|
||||
private void AnimateTool(int targetCellIndex, bool useBridgeStyle) {
|
||||
if (hammerSprite == null && alternateToolSprite == null) {
|
||||
Debug.LogWarning("ToolAnimator: No tool sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("ToolAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"ToolAnimator: Invalid cell index {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateStrikes(targetPos.Value, useBridgeStyle));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateStrikes(Vector2 target, bool useBridgeStyle) {
|
||||
bool useAlternate = useBridgeStyle && alternateToolSprite != null;
|
||||
Sprite toolSprite =
|
||||
useAlternate ? alternateToolSprite : hammerSprite ?? alternateToolSprite;
|
||||
float baseRotation = useAlternate ? alternateBaseRotation : hammerBaseRotation;
|
||||
bool flipHorizontal = useAlternate ? alternateFlipHorizontal : hammerFlipHorizontal;
|
||||
|
||||
// Create tool
|
||||
GameObject tool = new GameObject("Tool");
|
||||
tool.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
|
||||
SpriteRenderer toolRenderer = tool.AddComponent<SpriteRenderer>();
|
||||
toolRenderer.sprite = toolSprite;
|
||||
toolRenderer.sortingLayerName = "Effects";
|
||||
float xScale = flipHorizontal ? -toolScale : toolScale;
|
||||
tool.transform.localScale = new Vector3(xScale, toolScale, toolScale);
|
||||
|
||||
// Starting position (raised above target)
|
||||
Vector3 strikePoint = new Vector3(target.x, target.y, 5f);
|
||||
Vector3 raisePoint = new Vector3(target.x, target.y + swingHeight, 5f);
|
||||
|
||||
// Animate strike cycles
|
||||
for (int i = 0; i < strikeCount; i++) {
|
||||
// Raise tool
|
||||
tool.transform.localPosition = raisePoint;
|
||||
tool.transform.localRotation = Quaternion.Euler(0, 0, baseRotation - swingArc);
|
||||
|
||||
float elapsed = 0f;
|
||||
float downDuration = strikeDuration * 0.4f;
|
||||
|
||||
// Swing down
|
||||
while (elapsed < downDuration) {
|
||||
float t = elapsed / downDuration;
|
||||
float easeT = t * t; // Accelerating
|
||||
|
||||
tool.transform.localPosition = Vector3.Lerp(raisePoint, strikePoint, easeT);
|
||||
float angle = Mathf.Lerp(-swingArc, 0f, easeT);
|
||||
tool.transform.localRotation = Quaternion.Euler(0, 0, baseRotation + angle);
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
tool.transform.localPosition = strikePoint;
|
||||
tool.transform.localRotation = Quaternion.Euler(0, 0, baseRotation);
|
||||
|
||||
// Spawn sparks on impact
|
||||
yield return StartCoroutine(SpawnSparks(target));
|
||||
|
||||
// Brief pause at impact
|
||||
yield return new WaitForSeconds(strikeDuration * 0.1f);
|
||||
|
||||
// Raise back up
|
||||
elapsed = 0f;
|
||||
float upDuration = strikeDuration * 0.5f;
|
||||
|
||||
while (elapsed < upDuration) {
|
||||
float t = elapsed / upDuration;
|
||||
float easeT = t * (2f - t); // Decelerating
|
||||
|
||||
tool.transform.localPosition = Vector3.Lerp(strikePoint, raisePoint, easeT);
|
||||
float angle = Mathf.Lerp(0f, -swingArc, easeT);
|
||||
tool.transform.localRotation = Quaternion.Euler(0, 0, baseRotation + angle);
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (tool != null) { Destroy(tool); }
|
||||
}
|
||||
|
||||
private IEnumerator SpawnSparks(Vector2 target) {
|
||||
if (sparkSprite == null) { yield break; }
|
||||
|
||||
GameObject[] sparks = new GameObject[sparkCount];
|
||||
|
||||
for (int i = 0; i < sparkCount; i++) {
|
||||
GameObject spark = new GameObject($"Spark_{i}");
|
||||
spark.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
spark.transform.localPosition = new Vector3(target.x, target.y, 4f);
|
||||
|
||||
SpriteRenderer renderer = spark.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = sparkSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = sparkColor;
|
||||
spark.transform.localScale = Vector3.one * 5f;
|
||||
|
||||
sparks[i] = spark;
|
||||
}
|
||||
|
||||
// Animate sparks flying outward
|
||||
float sparkDuration = 0.15f;
|
||||
float elapsed = 0f;
|
||||
|
||||
Vector2[] directions = new Vector2[sparkCount];
|
||||
float[] speeds = new float[sparkCount];
|
||||
|
||||
for (int i = 0; i < sparkCount; i++) {
|
||||
float angle = Random.Range(0f, Mathf.PI * 2f);
|
||||
directions[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
|
||||
speeds[i] = sparkSpread * Random.Range(0.7f, 1.3f);
|
||||
}
|
||||
|
||||
while (elapsed < sparkDuration) {
|
||||
float t = elapsed / sparkDuration;
|
||||
|
||||
for (int i = 0; i < sparks.Length; i++) {
|
||||
if (sparks[i] == null) continue;
|
||||
|
||||
Vector2 pos = target + directions[i] * speeds[i] * t;
|
||||
// Sparks rise slightly as they spread
|
||||
pos.y += t * 10f;
|
||||
sparks[i].transform.localPosition = new Vector3(pos.x, pos.y, 4f);
|
||||
|
||||
// Fade out
|
||||
var renderer = sparks[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = sparkColor;
|
||||
c.a = 1f - t;
|
||||
renderer.color = c;
|
||||
sparks[i].transform.localScale = Vector3.one * 5f * (1f - t * 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup sparks
|
||||
foreach (var spark in sparks) {
|
||||
if (spark != null) { Destroy(spark); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b131f94a99ad146afa8636a4f80c23b1
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Shardok {
|
||||
/// <summary>
|
||||
/// Animates water splash effects for BraveWater and WaterDamage actions.
|
||||
/// Creates splashing water with ripples.
|
||||
/// </summary>
|
||||
public class WaterEffectAnimator : MonoBehaviour {
|
||||
[Header("Splash Settings")]
|
||||
[Tooltip("Sprite for splash droplets")]
|
||||
public Sprite splashSprite;
|
||||
|
||||
[Tooltip("Number of splash droplets")]
|
||||
public int splashCount = 8;
|
||||
|
||||
[Tooltip("Color of water")]
|
||||
public Color waterColor = new Color(0.2f, 0.5f, 0.9f, 0.8f);
|
||||
|
||||
[Header("Ripple Settings")]
|
||||
[Tooltip("Sprite for ripple rings")]
|
||||
public Sprite rippleSprite;
|
||||
|
||||
[Tooltip("Number of ripple rings")]
|
||||
public int rippleCount = 3;
|
||||
|
||||
[Tooltip("Color of ripples")]
|
||||
public Color rippleColor = new Color(0.4f, 0.7f, 1f, 0.6f);
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Height of splash")]
|
||||
public float splashHeight = 30f;
|
||||
|
||||
[Tooltip("Spread radius of splash")]
|
||||
public float splashSpread = 25f;
|
||||
|
||||
[Tooltip("Duration of splash animation")]
|
||||
public float splashDuration = 0.4f;
|
||||
|
||||
[Tooltip("Duration of ripple animation")]
|
||||
public float rippleDuration = 0.5f;
|
||||
|
||||
private HexGrid __hexGrid;
|
||||
|
||||
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
|
||||
|
||||
/// <summary>
|
||||
/// Starts a water splash animation at the target hex.
|
||||
/// </summary>
|
||||
public void AnimateWaterSplash(int targetCellIndex) {
|
||||
if (splashSprite == null && rippleSprite == null) {
|
||||
Debug.LogWarning("WaterEffectAnimator: No sprites assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
|
||||
Debug.LogWarning("WaterEffectAnimator: No _hexGrid or gridCanvas available");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
|
||||
|
||||
if (targetPos == null) {
|
||||
Debug.LogWarning($"WaterEffectAnimator: Invalid cell index {targetCellIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(AnimateSplashEffect(targetPos.Value));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateSplashEffect(Vector2 target) {
|
||||
// Create splash droplets
|
||||
GameObject[] splashes = new GameObject[splashCount];
|
||||
Vector2[] splashDirections = new Vector2[splashCount];
|
||||
float[] splashSpeeds = new float[splashCount];
|
||||
|
||||
for (int i = 0; i < splashCount; i++) {
|
||||
if (splashSprite == null) break;
|
||||
|
||||
GameObject splash = new GameObject($"Splash_{i}");
|
||||
splash.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
splash.transform.localPosition = new Vector3(target.x, target.y, 5f);
|
||||
|
||||
SpriteRenderer renderer = splash.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = splashSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = waterColor;
|
||||
splash.transform.localScale = Vector3.one * Random.Range(5f, 10f);
|
||||
|
||||
float angle = (float)i / splashCount * Mathf.PI * 2f + Random.Range(-0.3f, 0.3f);
|
||||
splashDirections[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
|
||||
splashSpeeds[i] = splashSpread * Random.Range(0.7f, 1.3f);
|
||||
|
||||
splashes[i] = splash;
|
||||
}
|
||||
|
||||
// Create ripples
|
||||
GameObject[] ripples = new GameObject[rippleCount];
|
||||
float[] rippleDelays = new float[rippleCount];
|
||||
|
||||
for (int i = 0; i < rippleCount; i++) {
|
||||
if (rippleSprite == null) break;
|
||||
|
||||
GameObject ripple = new GameObject($"Ripple_{i}");
|
||||
ripple.transform.SetParent(__hexGrid.gridCanvas.transform, false);
|
||||
ripple.transform.localPosition = new Vector3(target.x, target.y, 6f);
|
||||
ripple.transform.localScale = Vector3.zero;
|
||||
|
||||
SpriteRenderer renderer = ripple.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = rippleSprite;
|
||||
renderer.sortingLayerName = "Effects";
|
||||
renderer.color = rippleColor;
|
||||
|
||||
rippleDelays[i] = (float)i / rippleCount * rippleDuration * 0.5f;
|
||||
ripples[i] = ripple;
|
||||
}
|
||||
|
||||
// Animate splash
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < splashDuration) {
|
||||
float t = elapsed / splashDuration;
|
||||
|
||||
for (int i = 0; i < splashes.Length; i++) {
|
||||
if (splashes[i] == null) continue;
|
||||
|
||||
// Parabolic arc
|
||||
float x = splashDirections[i].x * splashSpeeds[i] * t;
|
||||
float y = splashDirections[i].y * splashSpeeds[i] * 0.3f * t +
|
||||
splashHeight * (t - t * t) * 4f; // Arc up then down
|
||||
|
||||
splashes[i].transform.localPosition =
|
||||
new Vector3(target.x + x, target.y + y, 5f);
|
||||
|
||||
var renderer = splashes[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = waterColor;
|
||||
c.a = waterColor.a * (1f - t * 0.7f);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup splashes
|
||||
foreach (var splash in splashes) {
|
||||
if (splash != null) { Destroy(splash); }
|
||||
}
|
||||
|
||||
// Animate ripples
|
||||
elapsed = 0f;
|
||||
|
||||
while (elapsed < rippleDuration) {
|
||||
float t = elapsed / rippleDuration;
|
||||
|
||||
for (int i = 0; i < ripples.Length; i++) {
|
||||
if (ripples[i] == null) continue;
|
||||
|
||||
float rippleT =
|
||||
Mathf.Clamp01((elapsed - rippleDelays[i]) / (rippleDuration * 0.7f));
|
||||
if (rippleT <= 0) continue;
|
||||
|
||||
float scale = splashSpread * 2f * rippleT;
|
||||
ripples[i].transform.localScale = Vector3.one * scale;
|
||||
|
||||
var renderer = ripples[i].GetComponent<SpriteRenderer>();
|
||||
if (renderer != null) {
|
||||
Color c = rippleColor;
|
||||
c.a = rippleColor.a * (1f - rippleT);
|
||||
renderer.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Cleanup ripples
|
||||
foreach (var ripple in ripples) {
|
||||
if (ripple != null) { Destroy(ripple); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af4ca8a26ea34d32986d26f4199791f5
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0deea4d267414bcd9421d74e9c1d0d7e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f80e1b28dcb84478b8a97821d82dbde7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Ordered collection of tutorial steps forming a complete tutorial.
|
||||
/// Can be used for onboarding or contextual tutorials.
|
||||
/// Create instances via Assets > Create > Eagle0 > Tutorial Sequence.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "TutorialSequence", menuName = "Eagle0/Tutorial Sequence")]
|
||||
public class TutorialSequence : ScriptableObject {
|
||||
[Header("Identity")]
|
||||
[Tooltip("Unique identifier for this tutorial sequence")]
|
||||
public string SequenceId;
|
||||
|
||||
[Tooltip("Display name shown to user")]
|
||||
public string DisplayName;
|
||||
|
||||
[Tooltip("Whether this is the main onboarding sequence")]
|
||||
public bool IsOnboarding;
|
||||
|
||||
[Header("Steps")]
|
||||
[Tooltip("Ordered list of tutorial steps")]
|
||||
public List<TutorialStep> Steps = new List<TutorialStep>();
|
||||
|
||||
[Header("Prerequisites")]
|
||||
[Tooltip("Tutorial IDs that must be completed before this one can trigger")]
|
||||
public List<string> RequiredCompletedTutorials = new List<string>();
|
||||
|
||||
[Header("Events")]
|
||||
[Tooltip("Called when this sequence starts")]
|
||||
public UnityEvent OnSequenceStart;
|
||||
|
||||
[Tooltip("Called when this sequence completes normally")]
|
||||
public UnityEvent OnSequenceComplete;
|
||||
|
||||
[Tooltip("Called when user skips this sequence")]
|
||||
public UnityEvent OnSequenceSkipped;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a step by index, or null if out of range.
|
||||
/// </summary>
|
||||
public TutorialStep GetStep(int index) {
|
||||
if (index < 0 || index >= Steps.Count) return null;
|
||||
return Steps[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of a step by its ID, or -1 if not found.
|
||||
/// </summary>
|
||||
public int IndexOfStep(string stepId) {
|
||||
if (string.IsNullOrEmpty(stepId)) return -1;
|
||||
|
||||
for (int i = 0; i < Steps.Count; i++) {
|
||||
if (Steps[i].StepId == stepId) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total number of steps in this sequence.
|
||||
/// </summary>
|
||||
public int StepCount => Steps?.Count ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all prerequisites are met.
|
||||
/// </summary>
|
||||
public bool ArePrerequisitesMet(TutorialState state) {
|
||||
if (RequiredCompletedTutorials == null || RequiredCompletedTutorials.Count == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var reqId in RequiredCompletedTutorials) {
|
||||
if (!state.HasCompletedTutorial(reqId)) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Editor validation to catch common issues.
|
||||
/// </summary>
|
||||
private void OnValidate() {
|
||||
// Ensure SequenceId is set
|
||||
if (string.IsNullOrEmpty(SequenceId)) { SequenceId = name; }
|
||||
|
||||
// Check for duplicate step IDs
|
||||
var seenIds = new HashSet<string>();
|
||||
foreach (var step in Steps) {
|
||||
if (!string.IsNullOrEmpty(step.StepId)) {
|
||||
if (!seenIds.Add(step.StepId)) {
|
||||
Debug.LogWarning(
|
||||
$"TutorialSequence '{SequenceId}': Duplicate step ID '{step.StepId}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97da9ce2a5d7418998c026d559ad0a13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// How the tutorial step should be displayed to the user.
|
||||
/// </summary>
|
||||
public enum TutorialDisplayMode {
|
||||
/// <summary>Full modal window blocking game interaction (for important concepts).</summary>
|
||||
Modal,
|
||||
|
||||
/// <summary>Semi-transparent overlay highlighting a specific UI element.</summary>
|
||||
Overlay,
|
||||
|
||||
/// <summary>Small tooltip positioned near target element.</summary>
|
||||
Tooltip,
|
||||
|
||||
/// <summary>Subtle pulsing indicator only (no text popup).</summary>
|
||||
Hint,
|
||||
|
||||
/// <summary>Invisible step for waiting on game events.</summary>
|
||||
None
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How the tutorial step is completed/advanced.
|
||||
/// </summary>
|
||||
public enum TutorialCompletionType {
|
||||
/// <summary>User clicks Continue/Got it button.</summary>
|
||||
ButtonClick,
|
||||
|
||||
/// <summary>Specific game event triggers completion.</summary>
|
||||
GameEvent,
|
||||
|
||||
/// <summary>User interacts with the highlighted UI element.</summary>
|
||||
UIInteraction,
|
||||
|
||||
/// <summary>Auto-advances after a delay.</summary>
|
||||
Timer,
|
||||
|
||||
/// <summary>Custom condition checked each frame.</summary>
|
||||
Condition
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single step within a tutorial sequence.
|
||||
/// Supports multiple display modes and completion conditions.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TutorialStep {
|
||||
[Header("Identity")]
|
||||
[Tooltip("Unique identifier for this step")]
|
||||
public string StepId;
|
||||
|
||||
[Header("Display Content")]
|
||||
[Tooltip("Title shown in modal or tooltip")]
|
||||
public string Title;
|
||||
|
||||
[Tooltip("Main description text")]
|
||||
[TextArea(3, 10)]
|
||||
public string Description;
|
||||
|
||||
[Tooltip("Optional icon/image to display")]
|
||||
public Sprite Icon;
|
||||
|
||||
[Tooltip("How this step should be displayed")]
|
||||
public TutorialDisplayMode DisplayMode = TutorialDisplayMode.Modal;
|
||||
|
||||
[Header("UI Targeting")]
|
||||
[Tooltip("Path to GameObject to highlight (e.g., 'Canvas/CommandPanel/MarchButton')")]
|
||||
public string TargetGameObjectPath;
|
||||
|
||||
[Tooltip("Offset from target element for tooltip/arrow positioning")]
|
||||
public Vector2 HighlightOffset;
|
||||
|
||||
[Tooltip("Whether the highlight should pulse")]
|
||||
public bool HighlightPulsing = true;
|
||||
|
||||
[Header("Completion")]
|
||||
[Tooltip("How this step is completed")]
|
||||
public TutorialCompletionType CompletionType = TutorialCompletionType.ButtonClick;
|
||||
|
||||
[Tooltip("Event ID to wait for (when CompletionType is GameEvent)")]
|
||||
public string CompletionEventId;
|
||||
|
||||
[Tooltip("Delay in seconds before auto-advance (when CompletionType is Timer)")]
|
||||
public float AutoAdvanceDelay;
|
||||
|
||||
[Header("Flow Control")]
|
||||
[Tooltip("Whether user can skip this step")]
|
||||
public bool AllowSkip = true;
|
||||
|
||||
[Tooltip("Jump to specific step ID instead of sequential (empty = next step)")]
|
||||
public string NextStepOverride;
|
||||
|
||||
[Header("Audio")]
|
||||
[Tooltip("Optional narration audio")]
|
||||
public AudioClip NarrationClip;
|
||||
|
||||
[Tooltip("Optional sound effect when step appears")]
|
||||
public AudioClip EffectClip;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad37a68e71e24b6a88ace636f74020dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,139 @@
|
||||
# Tutorial System for Eagle0
|
||||
|
||||
## Overview
|
||||
|
||||
A modular tutorial system for the Eagle0 Unity client supporting:
|
||||
- **Onboarding**: Full guided playthrough of first several turns
|
||||
- **Contextual tutorials**: First-encounter and first-attempt triggers with subtle hints
|
||||
- **Mixed UI**: Modals for concepts, overlays for UI guidance, hint indicators
|
||||
- **Local persistence**: PlayerPrefs-based state tracking
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
|
||||
### Completed
|
||||
- [x] **Phase 1: Foundation** - TutorialState, TutorialManager, TutorialStep/Sequence
|
||||
- [x] **Phase 2: UI (partial)** - IMGUI fallback modal with Stoke font
|
||||
- [x] **Phase 3: Triggers (partial)** - Basic trigger system with hooks in game controllers
|
||||
- [x] **Test setup** - TutorialTestSetup for province selection, battle entry, command triggers
|
||||
- [x] **Settings integration** - Reset Tutorials button in Settings panel
|
||||
|
||||
### Remaining
|
||||
- [ ] **Canvas UI prefabs** - Replace IMGUI fallback with styled Canvas-based modal
|
||||
- [ ] **Overlay system** - TutorialOverlayController for highlighting UI elements
|
||||
- [ ] **Hint indicators** - TutorialHintIndicator for subtle pulsing dots
|
||||
- [ ] **Real tutorial content** - Replace test tutorials with actual onboarding sequence
|
||||
- [ ] **More contextual triggers** - Diplomacy, hero recruitment, spells, terrain, etc.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
TutorialManager (Singleton, DontDestroyOnLoad)
|
||||
├── TutorialState (PlayerPrefs persistence)
|
||||
├── TutorialTriggerRegistry
|
||||
│ ├── OnboardingTriggers
|
||||
│ └── ContextualTriggers
|
||||
└── TutorialUIManager
|
||||
├── TutorialModalPanel (TODO)
|
||||
├── TutorialOverlayController (TODO)
|
||||
├── TutorialHintIndicator (TODO)
|
||||
└── IMGUI Fallback (working)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure (Current)
|
||||
|
||||
```
|
||||
Assets/Tutorial/
|
||||
├── TutorialManager.cs ✓ Singleton, coordinates everything
|
||||
├── TutorialState.cs ✓ PlayerPrefs persistence
|
||||
├── TutorialTestSetup.cs ✓ Test component for validation
|
||||
├── Content/
|
||||
│ ├── TutorialStep.cs ✓ Step data structure
|
||||
│ └── TutorialSequence.cs ✓ Sequence ScriptableObject
|
||||
├── Triggers/
|
||||
│ └── TutorialTriggerRegistry.cs ✓ Event routing
|
||||
└── UI/
|
||||
├── TutorialUIManager.cs ✓ UI coordination + IMGUI fallback
|
||||
├── TutorialModalPanel.cs ✓ Stub (needs Canvas implementation)
|
||||
├── TutorialOverlayController.cs ✓ Stub
|
||||
└── TutorialHintIndicator.cs ✓ Stub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Points (Implemented)
|
||||
|
||||
### EagleGameController.cs
|
||||
- `SetUpGame()`: Initializes TutorialManager, starts onboarding
|
||||
- `SwapModel()`: Calls `OnModelUpdated()`
|
||||
- `ProvinceWasSelected()`: Calls `OnProvinceSelected()`
|
||||
- `PostCommittedCommand()`: Calls `OnCommandIssued()`
|
||||
|
||||
### ShardokGameController.cs
|
||||
- `SetUpGame()`: Initializes TutorialManager for battle
|
||||
- `ModelUpdated()`: Calls `OnBattleAction()`
|
||||
- `OnTurnEnded()`: Calls `OnTurnEnded()`
|
||||
- Unit selection: Calls `OnUnitSelected()`
|
||||
|
||||
### SettingsPanelController.cs
|
||||
- Reset Tutorials button: Calls `TutorialManager.Instance.ResetAllProgress()`
|
||||
|
||||
---
|
||||
|
||||
## Onboarding Flow (Planned)
|
||||
|
||||
| Step | Type | Content | Completion |
|
||||
|------|------|---------|------------|
|
||||
| 1 | Modal | Welcome to Eagle0 | Button click |
|
||||
| 2 | Overlay | Map overview - "Select a province" | Province selected |
|
||||
| 3 | Overlay | Province info panel explanation | Button click |
|
||||
| 4 | Overlay | Command buttons - "Try March" | March issued |
|
||||
| 5 | Modal | Turn cycle explanation | Button click |
|
||||
| 6 | Hidden | Wait for battle available | Model update |
|
||||
| 7 | Modal | Battle introduction | Button click |
|
||||
| 8 | Overlay | "Battle!" button highlight | Battle entered |
|
||||
| 9 | Modal | Tactical combat overview | Button click |
|
||||
| 10 | Overlay | Select and move a unit | Move issued |
|
||||
| 11 | Overlay | Attack an enemy | Attack issued |
|
||||
| 12 | Overlay | End Turn button | Turn ended |
|
||||
| 13 | Modal | Onboarding complete! | Button click |
|
||||
|
||||
---
|
||||
|
||||
## Contextual Tutorials (Planned)
|
||||
|
||||
### Strategic (First Encounter)
|
||||
| ID | Trigger | Display |
|
||||
|----|---------|---------|
|
||||
| `diplomacy_offer` | First diplomacy command available | Modal |
|
||||
| `hero_recruitment` | First recruitment available | Modal |
|
||||
| `province_riot` | First riot in owned province | Modal |
|
||||
| `weather_control` | First weather command available | Overlay |
|
||||
| `prisoner_capture` | First prisoner captured | Modal |
|
||||
|
||||
### Tactical (First Encounter/Attempt)
|
||||
| ID | Trigger | Display |
|
||||
|----|---------|---------|
|
||||
| `spell_lightning` | Lightning spell available | Tooltip |
|
||||
| `spell_meteor` | Meteor spell available | Modal |
|
||||
| `spell_holywave` | Holy Wave available | Tooltip |
|
||||
| `spell_raisedead` | Raise Dead available | Modal |
|
||||
| `terrain_fire` | Fire hex encountered | Tooltip |
|
||||
| `terrain_water` | Water adjacent to unit | Tooltip |
|
||||
| `ability_charge` | Charge command available | Overlay |
|
||||
| `flanking` | Flanking opportunity | Hint |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
1. Add `TutorialTestSetup` component to TutorialManager GameObject
|
||||
2. Assign `Stoke-Regular.ttf` to `FallbackFont` on TutorialUIManager
|
||||
3. Enable `Debug Logging` on TutorialManager for console output
|
||||
4. Play game → select province → see test tutorial modal
|
||||
5. Use Settings → Reset Tutorials to test again
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e2fbfb348eb443cb91395b3712870da
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
using System.Collections.Generic;
|
||||
using eagle;
|
||||
using Shardok;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Registry of tutorial triggers that respond to game events.
|
||||
/// Integrates with game controllers to detect first-encounter situations.
|
||||
/// </summary>
|
||||
public class TutorialTriggerRegistry {
|
||||
private readonly TutorialManager _manager;
|
||||
private EagleGameController _eagleController;
|
||||
private ShardokGameController _shardokController;
|
||||
|
||||
// Registered contextual tutorials by ID
|
||||
private Dictionary<string, TutorialSequence> _tutorialSequences =
|
||||
new Dictionary<string, TutorialSequence>();
|
||||
|
||||
// Event-to-tutorial mapping
|
||||
private Dictionary<string, List<string>> _eventTriggers =
|
||||
new Dictionary<string, List<string>>();
|
||||
|
||||
public TutorialTriggerRegistry(TutorialManager manager) { _manager = manager; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialize with game controller references.
|
||||
/// </summary>
|
||||
public void Initialize(EagleGameController eagle, ShardokGameController shardok) {
|
||||
if (eagle != null) { _eagleController = eagle; }
|
||||
if (shardok != null) { _shardokController = shardok; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a tutorial sequence that can be triggered by events.
|
||||
/// </summary>
|
||||
public void RegisterTutorial(TutorialSequence sequence, params string[] triggerEvents) {
|
||||
if (sequence == null || string.IsNullOrEmpty(sequence.SequenceId)) return;
|
||||
|
||||
_tutorialSequences[sequence.SequenceId] = sequence;
|
||||
|
||||
foreach (var eventId in triggerEvents) {
|
||||
if (!_eventTriggers.ContainsKey(eventId)) {
|
||||
_eventTriggers[eventId] = new List<string>();
|
||||
}
|
||||
_eventTriggers[eventId].Add(sequence.SequenceId);
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"TutorialTriggerRegistry: Registered '{sequence.SequenceId}' for events: {string.Join(", ", triggerEvents)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a tutorial sequence by ID.
|
||||
/// </summary>
|
||||
public TutorialSequence GetSequenceById(string tutorialId) {
|
||||
_tutorialSequences.TryGetValue(tutorialId, out var sequence);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a game event occurs.
|
||||
/// Checks for tutorials that should be triggered.
|
||||
/// </summary>
|
||||
public void OnGameEvent(string eventId, object context = null) {
|
||||
if (!_eventTriggers.TryGetValue(eventId, out var tutorialIds)) return;
|
||||
|
||||
foreach (var tutorialId in tutorialIds) {
|
||||
if (_manager.State.HasCompletedTutorial(tutorialId)) continue;
|
||||
|
||||
var sequence = GetSequenceById(tutorialId);
|
||||
if (sequence != null && sequence.ArePrerequisitesMet(_manager.State)) {
|
||||
_manager.TriggerContextualTutorial(tutorialId);
|
||||
break; // Only trigger one tutorial per event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Strategic Layer Events ==========
|
||||
|
||||
/// <summary>
|
||||
/// Called when the game model is updated.
|
||||
/// </summary>
|
||||
public void OnModelUpdated(IGameModel model, IGameModel previousModel) {
|
||||
if (model == null) return;
|
||||
|
||||
// Check for first battle available
|
||||
if (model.RunningShardokGameModels?.Count > 0) {
|
||||
if (previousModel?.RunningShardokGameModels == null ||
|
||||
previousModel.RunningShardokGameModels.Count == 0) {
|
||||
OnGameEvent("first_battle_available");
|
||||
}
|
||||
}
|
||||
|
||||
// Check for diplomacy offers
|
||||
// Check for riots
|
||||
// Check for hero recruitment opportunities
|
||||
// etc. - These would check model state changes
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a province is selected.
|
||||
/// </summary>
|
||||
public void OnProvinceSelected(object province) {
|
||||
// Track first province selection for onboarding
|
||||
OnGameEvent("province_selected", province);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a command is issued.
|
||||
/// </summary>
|
||||
public void OnCommandIssued(object command) {
|
||||
// Track first command for onboarding
|
||||
OnGameEvent("command_issued", command);
|
||||
|
||||
// Could also check command type for specific tutorials
|
||||
// e.g., first diplomacy command, first weather control, etc.
|
||||
}
|
||||
|
||||
// ========== Tactical Layer Events ==========
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering a battle.
|
||||
/// </summary>
|
||||
public void OnBattleEntered(object battleModel) {
|
||||
OnGameEvent("battle_entered", battleModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a battle action result is received.
|
||||
/// </summary>
|
||||
public void OnBattleAction(object actionResult) {
|
||||
OnGameEvent("battle_action", actionResult);
|
||||
|
||||
// Check for specific action types that need tutorials
|
||||
// e.g., first spell cast, first charge, etc.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a unit is selected in tactical view.
|
||||
/// </summary>
|
||||
public void OnUnitSelected(int cellIndex) { OnGameEvent("unit_selected", cellIndex); }
|
||||
|
||||
/// <summary>
|
||||
/// Called when turn ends in tactical combat.
|
||||
/// </summary>
|
||||
public void OnTurnEnded() { OnGameEvent("turn_ended"); }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23dab249a548427091dc94baadedcebd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 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