mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 02:48:40 +00:00
Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
415acf27cd | ||
|
|
c4ad4529d8 | ||
|
|
44e87fbe49 | ||
|
|
4b5897210c | ||
|
|
d1191cf494 | ||
|
|
f4250d2b82 | ||
|
|
1a4656992c | ||
|
|
8b67adbe00 | ||
|
|
6e8c683d9f | ||
|
|
fb5ed2365f | ||
|
|
44ae2f36a7 | ||
|
|
41407d4ad0 | ||
|
|
78d92fee37 | ||
|
|
4e32bd7c51 | ||
|
|
2227cce8a6 | ||
|
|
3e618b1e6f | ||
|
|
c392895e12 | ||
|
|
f86fb86870 | ||
|
|
3cacfc5225 | ||
|
|
6e94c82098 | ||
|
|
1a3d0a82a6 | ||
|
|
ea083c8c6c | ||
|
|
dd4ba80a2b | ||
|
|
aea5bc614b | ||
|
|
e006d9e5a0 | ||
|
|
2daf624538 | ||
|
|
7d6d8f3606 | ||
|
|
0bac3ab38f | ||
|
|
7486af58b8 | ||
|
|
475321a86e | ||
|
|
690ae25c88 | ||
|
|
e1e714a818 | ||
|
|
27cd673038 | ||
|
|
1da9fe3405 | ||
|
|
401fdd26cd | ||
|
|
ad142c6f65 | ||
|
|
96d6d64f77 | ||
|
|
5e80ef41d6 | ||
|
|
58553a4de1 | ||
|
|
dc237d6e8f | ||
|
|
42fa4831f0 | ||
|
|
cdab19e920 | ||
|
|
05de5fd007 | ||
|
|
f1429b9560 | ||
|
|
d9452bd763 | ||
|
|
8f24ef1fa7 | ||
|
|
e9a9475deb | ||
|
|
21b56b8098 | ||
|
|
de04afc47f | ||
|
|
1573fc4a82 | ||
|
|
e4a6edf7d7 | ||
|
|
df781f5643 | ||
|
|
6401db7043 | ||
|
|
9cb3a60501 | ||
|
|
0f8880353b | ||
|
|
f1b89de5b3 | ||
|
|
d99d2f5a53 | ||
|
|
72efdff6bc | ||
|
|
9e8a506601 | ||
|
|
77e1dd41cc | ||
|
|
aeb89e265e | ||
|
|
9046e3d7ce | ||
|
|
fc6b48793e | ||
|
|
7d2050db5e | ||
|
|
7e0d8360c4 | ||
|
|
a0912bbc9e | ||
|
|
987390b244 | ||
|
|
e1c7036780 | ||
|
|
da1a1a64c7 | ||
|
|
138c0fcfa9 | ||
|
|
8e7acdfd97 | ||
|
|
e1d8fd7031 | ||
|
|
f56289899b | ||
|
|
9e365b637c | ||
|
|
15d5519634 | ||
|
|
bbf9d23c1f | ||
|
|
7685f4470c | ||
|
|
ed47b5769d | ||
|
|
11d734ce7f | ||
|
|
275a72256f | ||
|
|
13fa2ce8da | ||
|
|
06cd96f921 | ||
|
|
83ea7ce485 | ||
|
|
9bdfad34e1 | ||
|
|
14ba4b3933 | ||
|
|
462bba117e | ||
|
|
0080f17a19 | ||
|
|
aff09aedc3 | ||
|
|
47b813fba8 | ||
|
|
5da3a75e7c |
@@ -31,12 +31,26 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build-auth:
|
||||
runs-on: [self-hosted, bazel, halfdan]
|
||||
runs-on: [self-hosted, bazel]
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-auth.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel, halfdan]
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel, halfdan]
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
# This uses 1 runner slot instead of 4, and Bazel parallelizes internally
|
||||
# NOTE: Must run on a runner with the bazel label to ensure consistent Bazel cache
|
||||
build-all:
|
||||
runs-on: [self-hosted, bazel, halfdan]
|
||||
runs-on: [self-hosted, bazel]
|
||||
outputs:
|
||||
eagle_image_tag: ${{ steps.push-images.outputs.eagle_image_tag }}
|
||||
admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
|
||||
@@ -227,7 +227,7 @@ jobs:
|
||||
echo "=== All images pushed successfully ==="
|
||||
|
||||
deploy:
|
||||
runs-on: [self-hosted, bazel, halfdan]
|
||||
runs-on: [self-hosted, bazel]
|
||||
needs: [build-all]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
@@ -264,12 +264,12 @@ jobs:
|
||||
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
|
||||
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
|
||||
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
|
||||
EAGLE_HISTORY_BACKEND: ${{ secrets.EAGLE_HISTORY_BACKEND }}
|
||||
EAGLE_POSTGRES_HOST: ${{ secrets.EAGLE_POSTGRES_HOST }}
|
||||
EAGLE_POSTGRES_PORT: ${{ secrets.EAGLE_POSTGRES_PORT }}
|
||||
EAGLE_POSTGRES_DATABASE: ${{ secrets.EAGLE_POSTGRES_DATABASE }}
|
||||
EAGLE_POSTGRES_USER: ${{ secrets.EAGLE_POSTGRES_USER }}
|
||||
EAGLE_POSTGRES_PASSWORD: ${{ secrets.EAGLE_POSTGRES_PASSWORD }}
|
||||
EAGLE_POSTGRES_HISTORY_MIRROR: ${{ secrets.EAGLE_POSTGRES_HISTORY_MIRROR }}
|
||||
DO_DROPLET_IP: ${{ secrets.DO_DROPLET_IP }}
|
||||
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
@@ -380,6 +380,10 @@ jobs:
|
||||
validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
|
||||
validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
|
||||
validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_HOST" "${EAGLE_POSTGRES_HOST}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_DATABASE" "${EAGLE_POSTGRES_DATABASE}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_USER" "${EAGLE_POSTGRES_USER}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_PASSWORD" "${EAGLE_POSTGRES_PASSWORD}" "" || VALIDATION_FAILED=1
|
||||
|
||||
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
|
||||
echo ""
|
||||
@@ -427,12 +431,12 @@ jobs:
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
export EAGLE_HISTORY_BACKEND="${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
export EAGLE_POSTGRES_HOST="${EAGLE_POSTGRES_HOST}"
|
||||
export EAGLE_POSTGRES_PORT="${EAGLE_POSTGRES_PORT}"
|
||||
export EAGLE_POSTGRES_DATABASE="${EAGLE_POSTGRES_DATABASE}"
|
||||
export EAGLE_POSTGRES_USER="${EAGLE_POSTGRES_USER}"
|
||||
export EAGLE_POSTGRES_PASSWORD="${EAGLE_POSTGRES_PASSWORD}"
|
||||
export EAGLE_POSTGRES_HISTORY_MIRROR="${EAGLE_POSTGRES_HISTORY_MIRROR:-false}"
|
||||
export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
export NOTIFY_SECRET="${NOTIFY_SECRET}"
|
||||
export GITHUB_TOKEN="${GITHUB_TOKEN_FOR_ADMIN}"
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
build-unity:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.should_build == 'true'
|
||||
runs-on: [self-hosted, macOS, testflight, halfdan]
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
|
||||
archive-and-upload:
|
||||
needs: build-unity
|
||||
runs-on: [self-hosted, macOS, testflight, halfdan]
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
|
||||
@@ -66,7 +66,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-and-sign:
|
||||
runs-on: [self-hosted, macOS, unity-mac, halfdan]
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
outputs:
|
||||
submission_id: ${{ steps.notarize-submit.outputs.submission_id }}
|
||||
should_deploy: ${{ steps.check-deploy.outputs.should_deploy }}
|
||||
@@ -274,7 +274,7 @@ jobs:
|
||||
wait-notarization:
|
||||
needs: build-and-sign
|
||||
if: needs.build-and-sign.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, notarize, halfdan]
|
||||
runs-on: [self-hosted, macOS, notarize]
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
@@ -342,7 +342,7 @@ jobs:
|
||||
deploy:
|
||||
needs: [build-and-sign, wait-notarization]
|
||||
if: needs.build-and-sign.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, notarize, halfdan]
|
||||
runs-on: [self-hosted, macOS, notarize]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
|
||||
|
||||
@@ -401,8 +401,13 @@ jobs:
|
||||
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
# Install dmgbuild (creates .DS_Store programmatically, no AppleScript needed)
|
||||
pip3 install dmgbuild
|
||||
# Install dmgbuild in an isolated venv. Homebrew Python rejects system
|
||||
# installs under PEP 668, and mac_build_handler shells out to dmgbuild.
|
||||
DMGBUILD_VENV="${{ env.EAGLE0_BUILD_DIR }}/dmgbuild-venv"
|
||||
python3 -m venv "$DMGBUILD_VENV"
|
||||
"$DMGBUILD_VENV/bin/python" -m pip install --upgrade pip
|
||||
"$DMGBUILD_VENV/bin/python" -m pip install dmgbuild
|
||||
export PATH="$DMGBUILD_VENV/bin:$PATH"
|
||||
|
||||
# Background image for styled DMG
|
||||
BACKGROUND_PATH="$(pwd)/ci/mac/dmg/background.png"
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel, halfdan]
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Ensure AWS CLI is available
|
||||
|
||||
@@ -30,12 +30,26 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build-shardok-arm64:
|
||||
runs-on: [self-hosted, bazel, halfdan]
|
||||
runs-on: [self-hosted, bazel]
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
@@ -170,7 +184,7 @@ jobs:
|
||||
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
|
||||
|
||||
deploy-hetzner:
|
||||
runs-on: [self-hosted, bazel, halfdan]
|
||||
runs-on: [self-hosted, bazel]
|
||||
needs: [build-shardok-arm64]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
|
||||
@@ -57,7 +57,7 @@ env:
|
||||
|
||||
jobs:
|
||||
windows-unity:
|
||||
runs-on: [self-hosted, macOS, unity-windows, halfdan]
|
||||
runs-on: [self-hosted, macOS, unity-windows]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.get-version.outputs.deployed_version }}
|
||||
|
||||
|
||||
@@ -49,6 +49,17 @@ When code changes appear correct locally, create the PR promptly even if long-ru
|
||||
Opening the PR starts CI on the build machines, uploads results to the Bazel remote cache, and can speed up subsequent
|
||||
local validation. Continue monitoring any already-started local and CI validation after opening the PR.
|
||||
|
||||
## GitHub CLI PR Bodies
|
||||
|
||||
When creating or editing PR descriptions with multiline bodies, write the body to a temporary markdown file and pass it
|
||||
with `gh pr create --body-file <path>` or `gh pr edit <number> --body-file <path>`. Do not use multiline `$'...'`
|
||||
shell quoting for PR bodies; Codex's permission matching may treat that as a complex shell command instead of a clean
|
||||
`gh pr` invocation, causing unnecessary approval prompts.
|
||||
|
||||
Write these temporary body files, and any other scratch files needed for commands, under a known writable location such
|
||||
as `/private/tmp`, `$TMPDIR`, or the current repository workspace. Do not use protected paths that require additional
|
||||
user permission just to create or edit temporary files.
|
||||
|
||||
---
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
@@ -253,7 +264,7 @@ to be used for different players or game situations within the same server proce
|
||||
**C# (Unity Client):**
|
||||
|
||||
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
|
||||
- Uses Unity 6 (6000.4.9f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Uses Unity 6 (6000.4.10f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
|
||||
@@ -228,7 +228,7 @@ to be used for different players or game situations within the same server proce
|
||||
**C# (Unity Client):**
|
||||
|
||||
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
|
||||
- Uses Unity 6 (6000.4.9f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Uses Unity 6 (6000.4.10f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
|
||||
+47
-44
@@ -3,25 +3,29 @@ module(name = "net_eagle0")
|
||||
# Version constants
|
||||
SCALA_VERSION = "3.7.4"
|
||||
|
||||
NETTY_VERSION = "4.1.127.Final"
|
||||
NETTY_VERSION = "4.1.135.Final"
|
||||
|
||||
SCALAPB_VERSION = "1.0.0-alpha.1"
|
||||
SCALAPB_VERSION = "1.0.0-alpha.5"
|
||||
|
||||
AWS_SDK_VERSION = "2.41.18"
|
||||
SCALAPB_JSON4S_VERSION = "1.0.0-alpha.1"
|
||||
|
||||
AWS_SDK_VERSION = "2.46.7"
|
||||
|
||||
SLF4J_VERSION = "2.0.18"
|
||||
|
||||
#
|
||||
# Core Build Tools
|
||||
#
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.0")
|
||||
bazel_dep(name = "platforms", version = "1.0.0")
|
||||
bazel_dep(name = "platforms", version = "1.1.0")
|
||||
bazel_dep(name = "rules_pkg", version = "1.2.0")
|
||||
|
||||
#
|
||||
# Language Support - Scala
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_scala", version = "7.2.1")
|
||||
bazel_dep(name = "rules_scala", version = "7.2.5")
|
||||
|
||||
scala_config = use_extension(
|
||||
"@rules_scala//scala/extensions:config.bzl",
|
||||
@@ -41,21 +45,21 @@ scala_deps.scala_proto()
|
||||
# Language Support - C++
|
||||
#
|
||||
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.6.0")
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.7.0")
|
||||
|
||||
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
|
||||
|
||||
# Native toolchain (macOS -> macOS, Linux -> Linux)
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain",
|
||||
llvm_version = "20.1.4",
|
||||
llvm_version = "20.1.8",
|
||||
)
|
||||
|
||||
# Cross-compilation toolchain (macOS -> Linux x86_64)
|
||||
# Uses the same LLVM distribution but with a Linux sysroot
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain_linux",
|
||||
llvm_version = "20.1.4",
|
||||
llvm_version = "20.1.8",
|
||||
)
|
||||
|
||||
# Linux x86_64 sysroot for cross-compilation
|
||||
@@ -68,7 +72,7 @@ llvm.sysroot(
|
||||
# Cross-compilation toolchain (macOS -> Linux ARM64)
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain_linux_arm64",
|
||||
llvm_version = "20.1.4",
|
||||
llvm_version = "20.1.8",
|
||||
)
|
||||
|
||||
# Linux ARM64 sysroot for cross-compilation
|
||||
@@ -77,7 +81,6 @@ llvm.sysroot(
|
||||
label = "@linux_sysroot_arm64//sysroot",
|
||||
targets = ["linux-aarch64"],
|
||||
)
|
||||
|
||||
use_repo(llvm, "llvm_toolchain", "llvm_toolchain_linux", "llvm_toolchain_linux_arm64")
|
||||
|
||||
# Download the Linux sysroots (Ubuntu 24.04 Noble for C++23 support)
|
||||
@@ -103,11 +106,11 @@ sysroot(
|
||||
# Language Support - Go
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle")
|
||||
bazel_dep(name = "rules_go", version = "0.61.1", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.51.3", repo_name = "bazel_gazelle")
|
||||
|
||||
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
|
||||
go_sdk.download(version = "1.23.3")
|
||||
go_sdk.download(version = "1.25.11")
|
||||
use_repo(go_sdk, "go_default_sdk")
|
||||
|
||||
go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
|
||||
@@ -130,7 +133,7 @@ use_repo(
|
||||
# Language Support - Rust
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_rust", version = "0.68.1")
|
||||
bazel_dep(name = "rules_rust", version = "0.70.0")
|
||||
|
||||
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
|
||||
rust.toolchain(edition = "2021")
|
||||
@@ -150,7 +153,7 @@ use_repo(crate, "map_generator_crates")
|
||||
# Platform Support - Apple/iOS
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", version = "1.24.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "apple_support", version = "1.24.5", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
|
||||
|
||||
# Register Apple CC toolchain for Objective-C compilation
|
||||
@@ -164,7 +167,7 @@ use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_too
|
||||
# Protocol Buffers & RPC
|
||||
#
|
||||
|
||||
bazel_dep(name = "protobuf", version = "33.5", repo_name = "com_google_protobuf")
|
||||
bazel_dep(name = "protobuf", version = "33.6", repo_name = "com_google_protobuf")
|
||||
|
||||
# Use pre-built protoc binaries instead of compiling from source.
|
||||
# See: https://protobuf.dev/reference/cpp/cpp-generated/#invocation
|
||||
@@ -183,7 +186,7 @@ use_repo(
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_proto", version = "7.1.0")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.14")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.19")
|
||||
bazel_dep(name = "grpc", version = "1.78.0")
|
||||
|
||||
# Patch grpc 1.78.0 to fix rules_python incompatibility: grpc requests Python
|
||||
@@ -197,7 +200,7 @@ single_version_override(
|
||||
version = "1.78.0",
|
||||
)
|
||||
|
||||
bazel_dep(name = "grpc-java", version = "1.78.0")
|
||||
bazel_dep(name = "grpc-java", version = "1.78.0.bcr.1")
|
||||
bazel_dep(name = "rules_proto_grpc_csharp", version = "5.8.0")
|
||||
bazel_dep(name = "flatbuffers", version = "25.12.19")
|
||||
|
||||
@@ -205,13 +208,13 @@ bazel_dep(name = "flatbuffers", version = "25.12.19")
|
||||
# Testing
|
||||
#
|
||||
|
||||
bazel_dep(name = "googletest", version = "1.17.0")
|
||||
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
|
||||
|
||||
#
|
||||
# Container Images (OCI)
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_oci", version = "2.2.7")
|
||||
bazel_dep(name = "rules_oci", version = "2.3.0")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
|
||||
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
@@ -261,7 +264,7 @@ use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_25_jd
|
||||
# Java/Scala Dependencies
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_java", version = "9.3.0")
|
||||
bazel_dep(name = "rules_java", version = "9.6.1")
|
||||
bazel_dep(name = "rules_jvm_external", version = "6.10")
|
||||
|
||||
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
|
||||
@@ -281,7 +284,7 @@ maven.install(
|
||||
|
||||
# ScalaPB
|
||||
"com.thesamet.scalapb:lenses_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-json4s_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-json4s_3:%s" % SCALAPB_JSON4S_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-runtime_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-runtime-grpc_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:compilerplugin_3:%s" % SCALAPB_VERSION,
|
||||
@@ -293,10 +296,10 @@ maven.install(
|
||||
"org.json4s:json4s-native_3:4.1.0-M8",
|
||||
|
||||
# Testing
|
||||
"org.scalamock:scalamock_3:7.4.1",
|
||||
"org.scalamock:scalamock_3:7.5.5",
|
||||
|
||||
# Developer tools
|
||||
"org.scalameta:scalafmt-cli_2.13:3.9.9",
|
||||
"org.scalameta:scalafmt-cli_2.13:3.11.1",
|
||||
|
||||
# AWS SDK
|
||||
"software.amazon.awssdk:s3-transfer-manager:%s" % AWS_SDK_VERSION,
|
||||
@@ -308,38 +311,38 @@ maven.install(
|
||||
"software.amazon.awssdk:http-client-spi:%s" % AWS_SDK_VERSION,
|
||||
|
||||
# AWS Lambda
|
||||
"com.amazonaws:aws-lambda-java-core:1.2.3",
|
||||
"com.amazonaws:aws-lambda-java-events:3.13.0",
|
||||
"com.amazonaws:aws-lambda-java-core:1.4.0",
|
||||
"com.amazonaws:aws-lambda-java-events:3.16.1",
|
||||
|
||||
# Logging
|
||||
"org.slf4j:slf4j-api:2.0.16",
|
||||
"org.slf4j:slf4j-simple:2.0.16",
|
||||
"org.slf4j:slf4j-api:%s" % SLF4J_VERSION,
|
||||
"org.slf4j:slf4j-simple:%s" % SLF4J_VERSION,
|
||||
|
||||
# Other
|
||||
"org.reactivestreams:reactive-streams:1.0.4",
|
||||
"javax.xml.bind:jaxb-api:2.3.1",
|
||||
|
||||
# OkHttp (for SSE with read timeout support, OAuth HTTP calls)
|
||||
"com.squareup.okhttp3:okhttp:4.12.0",
|
||||
"com.squareup.okhttp3:okhttp-sse:4.12.0",
|
||||
"com.squareup.okhttp3:okhttp-jvm:5.4.0",
|
||||
"com.squareup.okhttp3:okhttp-sse:5.4.0",
|
||||
|
||||
# JWT (for OAuth token handling)
|
||||
"com.nimbusds:nimbus-jose-jwt:9.37.3",
|
||||
"com.nimbusds:nimbus-jose-jwt:10.9.1",
|
||||
|
||||
# Error tracking
|
||||
"io.sentry:sentry:8.31.0",
|
||||
"io.sentry:sentry:8.43.2",
|
||||
|
||||
# SQLite for client text storage
|
||||
"org.xerial:sqlite-jdbc:3.46.1.0",
|
||||
"org.xerial:sqlite-jdbc:3.53.2.0",
|
||||
|
||||
# Postgres for production history benchmarking and migration work
|
||||
"org.postgresql:postgresql:42.7.4",
|
||||
"org.postgresql:postgresql:42.7.11",
|
||||
],
|
||||
duplicate_version_warning = "error",
|
||||
fail_if_repin_required = True,
|
||||
lock_file = "//:maven_install.json",
|
||||
repositories = [
|
||||
"https://repo1.maven.org/maven2",
|
||||
"https://repo.maven.apache.org/maven2",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -349,7 +352,7 @@ maven.artifact(
|
||||
artifact = "scala3-staging_3",
|
||||
exclusions = ["org.scala-lang:scala3-compiler_3"],
|
||||
group = "org.scala-lang",
|
||||
version = "3.3.6",
|
||||
version = SCALA_VERSION,
|
||||
)
|
||||
|
||||
# Force specific versions for dependencies with conflicts between grpc-java and protobuf
|
||||
@@ -357,21 +360,20 @@ maven.artifact(
|
||||
artifact = "gson",
|
||||
force_version = True,
|
||||
group = "com.google.code.gson",
|
||||
version = "2.11.0",
|
||||
version = "2.14.0",
|
||||
)
|
||||
maven.artifact(
|
||||
artifact = "error_prone_annotations",
|
||||
force_version = True,
|
||||
group = "com.google.errorprone",
|
||||
version = "2.30.0",
|
||||
version = "2.50.0",
|
||||
)
|
||||
maven.artifact(
|
||||
artifact = "guava",
|
||||
force_version = True,
|
||||
group = "com.google.guava",
|
||||
version = "33.3.1-android",
|
||||
version = "33.6.0-android",
|
||||
)
|
||||
|
||||
use_repo(maven, "maven", "unpinned_maven")
|
||||
|
||||
#
|
||||
@@ -379,6 +381,7 @@ use_repo(maven, "maven", "unpinned_maven")
|
||||
#
|
||||
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
|
||||
|
||||
# GTL (for parallel_hashmap)
|
||||
@@ -431,24 +434,24 @@ http_archive(
|
||||
# Fallback: busybox.net (can be unreliable/slow)
|
||||
http_file(
|
||||
name = "busybox_x86_64",
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
sha256 = "6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348",
|
||||
urls = [
|
||||
"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/busybox/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,
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "busybox_aarch64",
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
sha256 = "141adb1b625a6f44c4b114f76b4387b4ea4f7ab802b88eb40e0d2f6adcccb1c3",
|
||||
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,
|
||||
)
|
||||
|
||||
# LLVM MinGW toolchain for Windows cross-compilation from macOS
|
||||
|
||||
Generated
+226
-201
File diff suppressed because one or more lines are too long
@@ -34,6 +34,7 @@ echo "Platform: ${PLATFORM}"
|
||||
# Check if Unity is installed and has required modules
|
||||
check_modules_installed() {
|
||||
local unity_path="${UNITY_INSTALL_PATH}/${UNITY_VERSION}"
|
||||
local unity_app_contents_path="${unity_path}/Unity.app/Contents"
|
||||
|
||||
if [ ! -d "$unity_path" ]; then
|
||||
return 1
|
||||
@@ -49,7 +50,8 @@ check_modules_installed() {
|
||||
;;
|
||||
mac)
|
||||
# Mac IL2CPP module
|
||||
if [ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations/macos_development_il2cpp" ] && \
|
||||
if [ ! -d "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Source/Player/MacPlayer/GameAssembly.xcodeproj" ] && \
|
||||
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations/macos_development_il2cpp" ] && \
|
||||
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
|
||||
echo "✗ Mac IL2CPP module not installed for Unity ${UNITY_VERSION}"
|
||||
return 1
|
||||
@@ -69,7 +71,8 @@ check_modules_installed() {
|
||||
echo "✗ iOS module missing"
|
||||
missing=1
|
||||
fi
|
||||
if [ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
|
||||
if [ ! -d "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Source/Player/MacPlayer/GameAssembly.xcodeproj" ] && \
|
||||
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
|
||||
echo "✗ Mac module missing"
|
||||
missing=1
|
||||
fi
|
||||
@@ -191,8 +194,10 @@ echo "Running: ${INSTALL_CMD[*]}"
|
||||
echo ""
|
||||
|
||||
# Capture output to check for "already installed" messages
|
||||
OUTPUT=$("${INSTALL_CMD[@]}" 2>&1) || true
|
||||
set +e
|
||||
OUTPUT=$("${INSTALL_CMD[@]}" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
echo "$OUTPUT"
|
||||
|
||||
# Check if modules are already installed (Unity Hub returns error but modules are present)
|
||||
@@ -219,13 +224,13 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
# Verify installation and requested modules
|
||||
echo ""
|
||||
if [ -d "${UNITY_INSTALL_PATH}/${UNITY_VERSION}" ]; then
|
||||
echo "✓ Verified: Unity ${UNITY_VERSION} is now installed"
|
||||
if check_modules_installed; then
|
||||
echo "✓ Verified: Unity ${UNITY_VERSION} is now installed with ${PLATFORM} support"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Unity ${UNITY_VERSION} installation could not be verified"
|
||||
echo "✗ Unity ${UNITY_VERSION} installation with ${PLATFORM} support could not be verified"
|
||||
echo " Expected path: ${UNITY_INSTALL_PATH}/${UNITY_VERSION}"
|
||||
echo ""
|
||||
echo "The installation may still be in progress, or may require manual intervention."
|
||||
|
||||
@@ -38,7 +38,7 @@ fi
|
||||
echo "Using git-lfs at $(command -v git-lfs)"
|
||||
git lfs version
|
||||
|
||||
git lfs install
|
||||
git lfs install --force
|
||||
|
||||
echo "LFS objects before pull:"
|
||||
git lfs ls-files | wc -l
|
||||
|
||||
@@ -19,6 +19,18 @@ SERVER_DATA="$UNITY_PROJECT/ServerData/$BUILD_TARGET"
|
||||
DO_ENDPOINT="https://sfo3.digitaloceanspaces.com"
|
||||
DO_BUCKET="eagle0-assets"
|
||||
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
|
||||
if ! command -v aws >/dev/null 2>&1; then
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew install awscli
|
||||
else
|
||||
echo "ERROR: aws CLI is unavailable, and Homebrew is not on PATH"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$SERVER_DATA" ]; then
|
||||
echo "No Addressables bundles found at $SERVER_DATA"
|
||||
echo "Skipping upload (this is expected if Addressables are bundled locally)"
|
||||
|
||||
@@ -32,13 +32,13 @@ services:
|
||||
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:-}"
|
||||
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}"
|
||||
EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}"
|
||||
EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}"
|
||||
EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}"
|
||||
EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}"
|
||||
EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}"
|
||||
EAGLE_POSTGRES_HISTORY_MIRROR: "${EAGLE_POSTGRES_HISTORY_MIRROR:-false}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
# Auth token for Shardok on Hetzner (required)
|
||||
@@ -88,13 +88,13 @@ services:
|
||||
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:-}"
|
||||
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}"
|
||||
EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}"
|
||||
EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}"
|
||||
EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}"
|
||||
EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}"
|
||||
EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}"
|
||||
EAGLE_POSTGRES_HISTORY_MIRROR: "${EAGLE_POSTGRES_HISTORY_MIRROR:-false}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
|
||||
@@ -68,7 +68,7 @@ private GameObject GetEffectPrefab(string beastName) {
|
||||
|
||||
### 4. Wire up in the scene
|
||||
|
||||
In `Gameplay.unity`, find the `ProvinceBeastsController` component and assign your new prefab to the new field.
|
||||
In `Assets/Scenes/Eagle.unity`, find the `ProvinceBeastsController` component on the Eagle map object and assign your new prefab to the new field.
|
||||
|
||||
### 5. Test
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ Create a tool to programmatically regenerate the Eagle strategic map images with
|
||||
| `Assets/Eagle/Materials/*.png` | Replaced province masks |
|
||||
| `Assets/Eagle/map_bw_labels.png` | Either: regenerated, or removed if using dynamic labels |
|
||||
| `province_map.tsv` | Add centroid columns |
|
||||
| `Assets/Gameplay.unity` | Add ProvinceLabelsController, label prefab instances |
|
||||
| `Assets/Scenes/Eagle.unity` | Add ProvinceLabelsController, label prefab instances |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
# Unity Scene Separation Plan
|
||||
|
||||
## Current State
|
||||
|
||||
The Unity client currently has both a large legacy scene and an unfinished split-scene setup.
|
||||
|
||||
- `Assets/Gameplay.unity` is still the production-sized monolithic scene. It contains connection/lobby UI, Eagle strategic UI, Shardok tactical UI, shared UI, camera/event-system objects, and miscellaneous persistent controllers.
|
||||
- `Assets/Scenes/Main.unity`, `Connection.unity`, `Eagle.unity`, `Shardok.unity`, and `Shared.unity` already exist, but they are small shell scenes.
|
||||
- `ProjectSettings/EditorBuildSettings.asset` includes both `Assets/Gameplay.unity` and the split scenes.
|
||||
- `Assets/UI/Scripts/BootstrapManager.cs` is explicitly transitional and currently defaults to loading `Gameplay` additively through `useOriginalGameplayScene = true`.
|
||||
- Additional migration notes exist under `Assets/SCENE_SEPARATION_PLAN.md`, `Assets/Scenes/README.md`, and `Assets/NEXT_STEPS.md`, but there is no repo-level plan in `docs/`.
|
||||
|
||||
This means the project is partway through a scene-separation migration, but runtime still depends on the monolithic scene.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Finish the split-scene architecture. Keep one small bootstrap scene and load mode-specific UI scenes additively.
|
||||
|
||||
The target architecture should be:
|
||||
|
||||
- `Main`: startup/bootstrap only. Owns scene loading and truly persistent app services.
|
||||
- `Shared`: shared UI and app-wide interaction infrastructure, such as the event system, error panels, confirmation modals, connection status overlays, settings, and update notifications.
|
||||
- `Connection`: login, account selection, lobby, custom battle creation, and game-selection UI.
|
||||
- `Eagle`: strategic map, strategic command panels, province/hero/battalion UI, notifications, and Eagle-specific effects.
|
||||
- `Shardok`: tactical battle UI, hex grid, battle command UI, unit rows, tactical effects, and battle-only camera/input helpers.
|
||||
|
||||
Do not treat large prefabs as inherently bad. A large prefab is acceptable when it represents a coherent, reusable feature root. The problematic pattern is a large scene that owns unrelated application states and long-lived object references.
|
||||
|
||||
## Goals
|
||||
|
||||
- Reduce merge conflicts and accidental Unity serialization churn.
|
||||
- Make login/lobby, Eagle, and Shardok changes independently editable.
|
||||
- Make scene lifecycle explicit: load only the mode currently needed.
|
||||
- Avoid cross-scene inspector references that are hard to reason about.
|
||||
- Preserve a safe fallback until each migrated section is validated.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not refactor gameplay logic and scene structure in the same change.
|
||||
- Do not delete `Gameplay.unity` until all migrated scenes are verified in Editor, local builds, and CI.
|
||||
- Do not move large vendor asset packs as part of this work.
|
||||
- Do not split every small widget into its own scene. Prefer prefabs for reusable UI components and scenes for application modes.
|
||||
|
||||
## Target Runtime Flow
|
||||
|
||||
```text
|
||||
Main
|
||||
Load Shared
|
||||
Load Connection
|
||||
|
||||
Connection -> Eagle
|
||||
Unload Connection
|
||||
Ensure Shared remains loaded
|
||||
Load Eagle
|
||||
|
||||
Eagle -> Shardok
|
||||
Keep Eagle loaded unless the battle UI must exclusively own the viewport
|
||||
Load Shardok additively
|
||||
|
||||
Shardok -> Eagle
|
||||
Unload Shardok
|
||||
Keep Eagle and Shared loaded
|
||||
|
||||
Eagle -> Connection
|
||||
Unload Shardok if loaded
|
||||
Unload Eagle
|
||||
Load Connection
|
||||
```
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
Use these rules during migration to avoid ambiguous object lifetimes.
|
||||
|
||||
- `Main` owns `GameSceneManager`, app bootstrap, and any service object that must survive across login, lobby, Eagle, and Shardok.
|
||||
- `Shared` owns exactly one active `EventSystem` and shared modal/overlay UI.
|
||||
- `Connection` owns `ConnectionHandler` and connection/lobby panel instances.
|
||||
- `Eagle` owns `EagleGameController` and strategic-mode scene roots.
|
||||
- `Shardok` owns `ShardokGameController` and tactical-mode scene roots.
|
||||
- Cross-mode communication should happen through model/controller APIs, not direct serialized references to objects in another loadable scene.
|
||||
- If a scene needs an object from another scene, prefer resolving it during scene initialization and failing loudly when required dependencies are missing.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: Make Bootstrap Explicit
|
||||
|
||||
- Make `Main.unity` the intended startup scene.
|
||||
- Update `BootstrapManager` so the split-scene path is the default in development builds.
|
||||
- Keep a temporary fallback flag for loading `Gameplay.unity`.
|
||||
- Ensure build settings order is intentional: `Main` first, then loadable scenes. Leave `Gameplay` present only while fallback is still required.
|
||||
- Add logging that states which startup path was chosen.
|
||||
|
||||
Validation:
|
||||
|
||||
- Enter Play Mode from `Main.unity`.
|
||||
- Confirm `Shared` and `Connection` load without duplicate `EventSystem` objects.
|
||||
- Confirm fallback still loads `Gameplay.unity` if explicitly enabled.
|
||||
|
||||
### Phase 2: Migrate Shared Infrastructure
|
||||
|
||||
- Move shared error UI, update UI, settings UI, connection status overlays, and global modal roots into `Shared.unity`.
|
||||
- Keep only one event system active when additive scenes are loaded.
|
||||
- Remove duplicated shared UI from `Gameplay.unity` only after migrated UI is validated.
|
||||
|
||||
Validation:
|
||||
|
||||
- Trigger an error modal from Connection, Eagle, and Shardok flows.
|
||||
- Open/close settings from each mode that supports it.
|
||||
- Verify no duplicate event-system warnings.
|
||||
|
||||
### Phase 3: Migrate Connection and Lobby
|
||||
|
||||
- Move `ConnectionCanvas`, login controls, account selector, lobby, custom battle UI, and game-selection rows into `Connection.unity`.
|
||||
- Make `ConnectionHandler` initialize after the scene is loaded rather than assuming objects exist in `Gameplay.unity`.
|
||||
- Replace show/hide transitions that activate large inactive subtrees with scene transitions where appropriate.
|
||||
|
||||
Validation:
|
||||
|
||||
- Start unauthenticated, authenticate, list available games, create/drop a custom battle, and enter a game.
|
||||
- Return from an active game to the lobby.
|
||||
- Test stored account flows.
|
||||
|
||||
### Phase 4: Migrate Eagle Strategic UI
|
||||
|
||||
- Move strategic map, command panel roots, hero/battalion/province UI, Eagle notifications, and strategic effects into `Eagle.unity`.
|
||||
- Keep reusable row/panel widgets as prefabs under `Assets/Eagle/`.
|
||||
- Remove direct dependencies on connection-scene objects.
|
||||
|
||||
Validation:
|
||||
|
||||
- Load a game from the lobby.
|
||||
- Select provinces, heroes, battalions, command buttons, and notifications.
|
||||
- Issue at least one representative command from each major command-panel family.
|
||||
- Confirm generated/dynamic text listeners clean up when leaving the scene.
|
||||
|
||||
### Phase 5: Migrate Shardok Tactical UI
|
||||
|
||||
- Move Shardok canvas, hex grid, battle panels, tactical rows, battle effects, and battle-only input/camera roots into `Shardok.unity`.
|
||||
- Decide whether Eagle remains visible/loaded under Shardok. Prefer keeping Eagle loaded if returning from battle must preserve strategic UI state.
|
||||
- Ensure Shardok scene unload disposes generated text listeners, subscriptions, object pools, and pending animation state.
|
||||
|
||||
Validation:
|
||||
|
||||
- Enter battle from Eagle.
|
||||
- Select units, issue movement/attack/fire/extinguish/end-turn commands.
|
||||
- Resolve a battle and return to Eagle.
|
||||
- Start a second battle in the same session to catch stale singleton/listener state.
|
||||
|
||||
### Phase 6: Remove Legacy Scene Dependency
|
||||
|
||||
- Flip the default so `Gameplay.unity` is not loaded during normal startup.
|
||||
- Remove `Gameplay.unity` from build settings once no supported flow needs it.
|
||||
- Keep `Gameplay.unity` in the repo temporarily as a reference snapshot if useful, then delete it in a separate cleanup PR.
|
||||
- Remove transitional helpers whose only purpose was loading or testing the legacy scene.
|
||||
|
||||
Validation:
|
||||
|
||||
- Local Editor smoke test through login, Eagle, Shardok, and return flows.
|
||||
- Unity edit-mode test suite if available.
|
||||
- CI Unity build.
|
||||
- TestFlight build before deleting the legacy scene.
|
||||
|
||||
## Review Checklist For Each Migration PR
|
||||
|
||||
- The PR moves one coherent subsystem, not multiple unrelated UI areas.
|
||||
- Scene files remain small enough to review.
|
||||
- Prefab changes are limited to the migrated subsystem.
|
||||
- No required Inspector field is hidden behind a defensive null check.
|
||||
- No duplicate `EventSystem`, camera, global canvas, or singleton is active.
|
||||
- Leaving and re-entering the migrated scene works in one app session.
|
||||
- The old `Gameplay.unity` object is removed only after the new scene object is verified.
|
||||
|
||||
## Risks
|
||||
|
||||
- Serialized cross-scene references will break when objects move. Prefer runtime lookup/initialization through explicit scene controllers.
|
||||
- Singletons may survive scene unload and hold stale references. Audit `OnDestroy`, `Dispose`, and listener cleanup for every moved controller.
|
||||
- Additive scenes can accidentally create duplicate input/event/camera objects. Keep these centralized in `Shared` or `Main`.
|
||||
- Loading Shardok additively over Eagle can expose hidden assumptions about active canvases, cameras, and selected input targets.
|
||||
- Unity scene YAML changes are easy to corrupt manually. Prefer moving objects in the Unity Editor and reviewing diffs carefully.
|
||||
|
||||
## Open Decisions
|
||||
|
||||
- Whether Shardok should overlay Eagle additively or unload most Eagle UI while battle is active.
|
||||
- Whether the primary persistent game connection object belongs in `Main` or should be recreated per selected game.
|
||||
- Whether `Shared` should always load at startup or only after successful connection.
|
||||
- How long to keep `Gameplay.unity` after the split scene flow is fully validated.
|
||||
|
||||
## Suggested First PR
|
||||
|
||||
The first implementation PR should not move Eagle or Shardok. It should:
|
||||
|
||||
1. Make `Main.unity` the startup scene in build settings.
|
||||
2. Make the split-scene bootstrap path explicit and logged.
|
||||
3. Load `Shared` plus `Connection` by default.
|
||||
4. Keep a fallback flag for loading `Gameplay.unity`.
|
||||
5. Validate login/lobby startup in Editor and CI.
|
||||
|
||||
That gives a controlled base for subsequent subsystem migrations.
|
||||
@@ -22,7 +22,7 @@ This avoids the "everything is pink" problem of switching the pipeline on main b
|
||||
| Shaders | 47 | 7 custom Eagle, 1 hex mesh, 20 Polytope, 13 TextMesh Pro, 6 other third-party |
|
||||
| Materials | 325 | Mix of Standard shader, custom, and third-party |
|
||||
| ShaderGraph files | 4 | All TextMesh Pro (URP + HDRP variants already exist) |
|
||||
| Scenes with baked lighting | 6 | Gameplay, Connection, Eagle, Shardok, Shared, Map Editor |
|
||||
| Scenes with baked lighting | 5 | Connection, Eagle, Shardok, Shared, Map Editor |
|
||||
| Post-processing | 1 profile | PPP_Orc.asset (Post Processing Stack v2, FXAA/TAA, AO) |
|
||||
| C# rendering scripts | 16 | Material/shader property manipulation only |
|
||||
| Rendering path | Forward | Matches URP default |
|
||||
@@ -132,7 +132,7 @@ This is the critical path. Without these, the game is unplayable.
|
||||
- Fix any visual differences from lighting model changes
|
||||
|
||||
**Lighting**
|
||||
- Rebake lightmaps for all 6 scenes (Gameplay, Connection, Eagle, Shardok, Shared, Map Editor)
|
||||
- Rebake lightmaps for all 5 scenes (Connection, Eagle, Shardok, Shared, Map Editor)
|
||||
- Configure URP shadow cascade settings to match current quality levels
|
||||
- Verify HDR rendering
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ Create these prefabs in `Assets/Eagle/Effects/`:
|
||||
|
||||
### Scene Setup
|
||||
|
||||
Wire up in `Gameplay.unity`:
|
||||
Wire up in `Assets/Scenes/Eagle.unity`:
|
||||
1. Add controller components to the Map GameObject
|
||||
2. Assign `mapContainer`, `centroidsJson` (shared with other controllers)
|
||||
3. Assign the effect prefab for each controller
|
||||
|
||||
@@ -2,7 +2,7 @@ module github.com/nolen777/eagle0
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.3
|
||||
toolchain go1.25.11
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.8
|
||||
@@ -12,9 +12,9 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
||||
golang.org/x/sys v0.30.0
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.36.3
|
||||
golang.org/x/sys v0.33.0
|
||||
google.golang.org/grpc v1.71.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -32,5 +32,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 // indirect
|
||||
github.com/aws/smithy-go v1.22.1 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
)
|
||||
|
||||
@@ -43,14 +43,24 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
|
||||
+1256
-684
File diff suppressed because it is too large
Load Diff
+269
-263
File diff suppressed because it is too large
Load Diff
+16
-13
@@ -24,34 +24,35 @@ public class MoveCamera : MonoBehaviour {
|
||||
|
||||
void Update() {
|
||||
// Get the left mouse button
|
||||
if (Input.GetMouseButtonDown(0)) {
|
||||
if (GameInput.PrimaryPointerPressedThisFrame()) {
|
||||
// Get mouse origin
|
||||
mouseOrigin = Input.mousePosition;
|
||||
mouseOrigin = GameInput.PointerPosition();
|
||||
isRotating = true;
|
||||
}
|
||||
|
||||
// Get the right mouse button
|
||||
if (Input.GetMouseButtonDown(1)) {
|
||||
if (GameInput.SecondaryPointerPressedThisFrame()) {
|
||||
// Get mouse origin
|
||||
mouseOrigin = Input.mousePosition;
|
||||
mouseOrigin = GameInput.PointerPosition();
|
||||
isPanning = true;
|
||||
}
|
||||
|
||||
// Get the middle mouse button
|
||||
if (Input.GetMouseButtonDown(2)) {
|
||||
if (GameInput.MiddlePointerPressedThisFrame()) {
|
||||
// Get mouse origin
|
||||
mouseOrigin = Input.mousePosition;
|
||||
mouseOrigin = GameInput.PointerPosition();
|
||||
isZooming = true;
|
||||
}
|
||||
|
||||
// Disable movements on button release
|
||||
if (!Input.GetMouseButton(0)) isRotating = false;
|
||||
if (!Input.GetMouseButton(1)) isPanning = false;
|
||||
if (!Input.GetMouseButton(2)) isZooming = false;
|
||||
if (!GameInput.PrimaryPointerPressed()) isRotating = false;
|
||||
if (!GameInput.SecondaryPointerPressed()) isPanning = false;
|
||||
if (!GameInput.MiddlePointerPressed()) isZooming = false;
|
||||
|
||||
// Rotate camera along X and Y axis
|
||||
if (isRotating) {
|
||||
Vector3 pos = Camera.main.ScreenToViewportPoint(Input.mousePosition - mouseOrigin);
|
||||
Vector3 pos = Camera.main.ScreenToViewportPoint(
|
||||
(Vector3)GameInput.PointerPosition() - mouseOrigin);
|
||||
|
||||
transform.RotateAround(transform.position, transform.right, -pos.y * turnSpeed);
|
||||
transform.RotateAround(transform.position, Vector3.up, pos.x * turnSpeed);
|
||||
@@ -59,7 +60,8 @@ public class MoveCamera : MonoBehaviour {
|
||||
|
||||
// Move the camera on it's XY plane
|
||||
if (isPanning) {
|
||||
Vector3 pos = Camera.main.ScreenToViewportPoint(Input.mousePosition - mouseOrigin);
|
||||
Vector3 pos = Camera.main.ScreenToViewportPoint(
|
||||
(Vector3)GameInput.PointerPosition() - mouseOrigin);
|
||||
|
||||
Vector3 move = new Vector3(pos.x * panSpeed, pos.y * panSpeed, 0);
|
||||
transform.Translate(move, Space.Self);
|
||||
@@ -67,10 +69,11 @@ public class MoveCamera : MonoBehaviour {
|
||||
|
||||
// Move the camera linearly along Z axis
|
||||
if (isZooming) {
|
||||
Vector3 pos = Camera.main.ScreenToViewportPoint(Input.mousePosition - mouseOrigin);
|
||||
Vector3 pos = Camera.main.ScreenToViewportPoint(
|
||||
(Vector3)GameInput.PointerPosition() - mouseOrigin);
|
||||
|
||||
Vector3 move = pos.y * zoomSpeed * transform.forward;
|
||||
transform.Translate(move, Space.World);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+69
-23
@@ -204,6 +204,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
void Start() {
|
||||
MainQueue.EnsureExists();
|
||||
EnsureOAuthManager();
|
||||
|
||||
var currentResolution = Screen.currentResolution;
|
||||
var resolutions = Screen.resolutions;
|
||||
|
||||
@@ -213,6 +216,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
ShowConnectionPanel();
|
||||
|
||||
if (errorHandler == null) { errorHandler = GameSceneManager.Instance?.GetErrorHandler(); }
|
||||
errorHandler.gameObject.SetActive(true);
|
||||
|
||||
// Initialize OAuth UI
|
||||
@@ -235,6 +239,15 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
TryRestoreSession();
|
||||
}
|
||||
|
||||
private static void EnsureOAuthManager() {
|
||||
if (OAuthManager.Instance != null) return;
|
||||
|
||||
if (FindAnyObjectByType<OAuthManager>(FindObjectsInactive.Include) != null) return;
|
||||
|
||||
var oauthManager = new GameObject("OAuth Manager");
|
||||
oauthManager.AddComponent<OAuthManager>();
|
||||
}
|
||||
|
||||
private void SetupConnectToQAButton() {
|
||||
if (connectToQAButton == null) return;
|
||||
|
||||
@@ -321,13 +334,11 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
private void RefreshStoredAccountButtons() {
|
||||
// Clear ALL children of the container
|
||||
// Disable immediately (Destroy is deferred to end of frame)
|
||||
if (storedAccountsContainer != null) {
|
||||
foreach (Transform child in storedAccountsContainer.transform) {
|
||||
child.gameObject.SetActive(false);
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
foreach (var storedAccountButton in _storedAccountButtons) {
|
||||
if (storedAccountButton == null) continue;
|
||||
|
||||
storedAccountButton.gameObject.SetActive(false);
|
||||
Destroy(storedAccountButton.gameObject);
|
||||
}
|
||||
_storedAccountButtons.Clear();
|
||||
|
||||
@@ -1095,8 +1106,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private void ShowConnectionPanel() {
|
||||
connectionCanvas.gameObject.SetActive(true);
|
||||
connectionCanvas.enabled = true;
|
||||
eagleCanvas.gameObject.SetActive(false);
|
||||
shardokContainer.SetActive(false);
|
||||
HideGamePanels();
|
||||
|
||||
connectionPanel.gameObject.SetActive(true);
|
||||
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(true); }
|
||||
@@ -1107,8 +1117,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private void ShowLobbyPanel() {
|
||||
connectionCanvas.gameObject.SetActive(true);
|
||||
connectionCanvas.enabled = true;
|
||||
eagleCanvas.gameObject.SetActive(false);
|
||||
shardokContainer.SetActive(false);
|
||||
HideGamePanels();
|
||||
|
||||
connectionPanel.gameObject.SetActive(false);
|
||||
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(false); }
|
||||
@@ -1119,8 +1128,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
private void ShowCustomBattlePanel() {
|
||||
connectionCanvas.gameObject.SetActive(true);
|
||||
connectionCanvas.enabled = true;
|
||||
eagleCanvas.gameObject.SetActive(false);
|
||||
shardokContainer.SetActive(false);
|
||||
HideGamePanels();
|
||||
|
||||
connectionPanel.gameObject.SetActive(false);
|
||||
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(false); }
|
||||
@@ -1128,24 +1136,62 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
customBattlePanel.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void ShowEaglePanel() {
|
||||
connectionCanvas.enabled = false;
|
||||
connectionCanvas.gameObject.SetActive(false);
|
||||
shardokContainer.SetActive(false);
|
||||
eagleCanvas.gameObject.SetActive(true);
|
||||
private void HideGamePanels() {
|
||||
var loadedEagleCanvas =
|
||||
eagleCanvas != null ? eagleCanvas : GameSceneManager.Instance?.GetEagleCanvas();
|
||||
if (loadedEagleCanvas != null) { loadedEagleCanvas.gameObject.SetActive(false); }
|
||||
|
||||
var loadedShardokContainer = shardokContainer != null
|
||||
? shardokContainer
|
||||
: GameSceneManager.Instance?.GetShardokContainer();
|
||||
if (loadedShardokContainer != null) { loadedShardokContainer.SetActive(false); }
|
||||
}
|
||||
|
||||
private void SelectEagleGame(long gameId) {
|
||||
private void ShowEaglePanel(Canvas loadedEagleCanvas) {
|
||||
connectionCanvas.enabled = false;
|
||||
connectionCanvas.gameObject.SetActive(false);
|
||||
var loadedShardokContainer = shardokContainer != null
|
||||
? shardokContainer
|
||||
: GameSceneManager.Instance?.GetShardokContainer();
|
||||
if (loadedShardokContainer != null) { loadedShardokContainer.SetActive(false); }
|
||||
loadedEagleCanvas.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private async void SelectEagleGame(long gameId) {
|
||||
var gameInfo = fetchedRunningGames.Find(gi => gi.GameId == gameId);
|
||||
|
||||
if (gameInfo != null) {
|
||||
SetLobbyActive(false);
|
||||
eagleCanvas.GetComponent<EagleGameController>().SetUpGame(
|
||||
var selectedPersistentConnection = _persistentClientConnection;
|
||||
var selectedEnvironmentName = ConnectedEnvironmentName;
|
||||
var eagleController = GameSceneManager.Instance == null
|
||||
? eagleCanvas?.GetComponent<EagleGameController>()
|
||||
: await GameSceneManager.Instance.TransitionToEagle();
|
||||
if (eagleController == null) {
|
||||
Debug.LogError(
|
||||
"Cannot select Eagle game because the Eagle scene did not provide an " +
|
||||
"EagleGameController.");
|
||||
SetLobbyActive(true);
|
||||
ShowLobbyPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
eagleCanvas = eagleController.GetComponent<Canvas>();
|
||||
if (eagleCanvas == null) {
|
||||
Debug.LogError(
|
||||
"Cannot select Eagle game because EagleGameController is not attached to " +
|
||||
"a Canvas.");
|
||||
SetLobbyActive(true);
|
||||
ShowLobbyPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
eagleController.SetUpGame(
|
||||
gameInfo.GameId,
|
||||
gameInfo.FactionId,
|
||||
_persistentClientConnection,
|
||||
ConnectedEnvironmentName);
|
||||
ShowEaglePanel();
|
||||
selectedPersistentConnection,
|
||||
selectedEnvironmentName);
|
||||
ShowEaglePanel(eagleCanvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-2
@@ -68,7 +68,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
private bool _youAreDefender;
|
||||
readonly Dictionary<int, string> _heroNames = new();
|
||||
readonly Dictionary<int, string> _heroImages = new();
|
||||
public void HandleCustomBattleResponse(CustomBattleResponse newGameResponse) {
|
||||
public async void HandleCustomBattleResponse(CustomBattleResponse newGameResponse) {
|
||||
Int16 yourPid = (Int16)(_youAreDefender ? 1 : 0);
|
||||
|
||||
HexMap map = _gameRequest.CustomMapBytes.IsEmpty
|
||||
@@ -103,10 +103,26 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
heroImages: _heroImages,
|
||||
battalionNames: new Dictionary<int, string> { { 0, "no name" } });
|
||||
|
||||
shardokContainer = GameSceneManager.Instance == null
|
||||
? shardokContainer
|
||||
: await GameSceneManager.Instance.TransitionToShardok();
|
||||
if (shardokContainer == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Cannot start custom battle because the Shardok scene did not provide a " +
|
||||
"Shardok container.");
|
||||
}
|
||||
|
||||
shardokContainer.SetActive(true);
|
||||
var shardokController = shardokContainer.GetComponent<ShardokGameController>();
|
||||
if (shardokController == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Cannot start custom battle because the Shardok container is missing a " +
|
||||
"ShardokGameController component.");
|
||||
}
|
||||
|
||||
PersistentUIManager.Instance?.connectionStatusUI?.SetShardokGameStateProvider(
|
||||
_shardokModel);
|
||||
shardokContainer.GetComponent<ShardokGameController>().SetUpGame(_shardokModel);
|
||||
shardokController.SetUpGame(_shardokModel);
|
||||
|
||||
GameId = newGameResponse.EagleGameId;
|
||||
Register();
|
||||
|
||||
+2
@@ -9,6 +9,8 @@ public class DropGameConfirmationPanel : MonoBehaviour {
|
||||
|
||||
private long _pendingGameId;
|
||||
|
||||
void Awake() { gameObject.SetActive(false); }
|
||||
|
||||
public void Show(long gameId, string message) {
|
||||
_pendingGameId = gameId;
|
||||
if (messageText != null) { messageText.text = message; }
|
||||
|
||||
+8
-3
@@ -48,9 +48,14 @@ public class RunningGameItem : MonoBehaviour {
|
||||
leaderName,
|
||||
DisplayNames.ProfessionNames[leader.Profession]);
|
||||
|
||||
// Set profession image
|
||||
professionImage.texture = EagleCommonTextures.Instance.Profession(leader.Profession);
|
||||
professionImage.color = professionImage.texture != null ? Color.white : Color.clear;
|
||||
// Set profession image. The connection lobby can render before the Eagle scene has loaded
|
||||
// its shared texture catalog, so the icon is optional here.
|
||||
var professionTexture =
|
||||
EagleCommonTextures.Instance == null
|
||||
? null
|
||||
: EagleCommonTextures.Instance.Profession(leader.Profession);
|
||||
professionImage.texture = professionTexture;
|
||||
professionImage.color = professionTexture != null ? Color.white : Color.clear;
|
||||
|
||||
// Show/hide tutorial indicator
|
||||
if (tutorialIndicator != null) {
|
||||
|
||||
@@ -56,6 +56,8 @@ namespace eagle {
|
||||
private static readonly int EatHash = Animator.StringToHash("eat");
|
||||
private static readonly int AttackHash = Animator.StringToHash("attack");
|
||||
private static readonly int DieHash = Animator.StringToHash("die");
|
||||
private const string BeastSortingLayerName = "UI Base Overlay";
|
||||
private const int BeastCreatureSortingOrder = 103;
|
||||
|
||||
void Start() { SpawnAnimals(); }
|
||||
|
||||
@@ -129,7 +131,8 @@ namespace eagle {
|
||||
instance.transform.localPosition = startPos;
|
||||
|
||||
foreach (var renderer in instance.GetComponentsInChildren<Renderer>()) {
|
||||
renderer.sortingOrder = 103;
|
||||
renderer.sortingLayerName = BeastSortingLayerName;
|
||||
renderer.sortingOrder = BeastCreatureSortingOrder;
|
||||
}
|
||||
foreach (var smr in instance.GetComponentsInChildren<SkinnedMeshRenderer>()) {
|
||||
smr.updateWhenOffscreen = true;
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace eagle {
|
||||
|
||||
private ParticleSystem _birdSystem;
|
||||
private Material _materialInstance;
|
||||
private const string BeastSortingLayerName = "UI Base Overlay";
|
||||
private const int BeastBirdSortingOrder = 102;
|
||||
|
||||
void Start() { SetupParticleSystems(); }
|
||||
|
||||
@@ -140,7 +142,8 @@ namespace eagle {
|
||||
renderer.material = _materialInstance;
|
||||
}
|
||||
}
|
||||
renderer.sortingOrder = 102;
|
||||
renderer.sortingLayerName = BeastSortingLayerName;
|
||||
renderer.sortingOrder = BeastBirdSortingOrder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
public static class ClientTextDiskCache {
|
||||
private const int MaxDirtyEntriesBeforeFlush = 50;
|
||||
private static readonly TimeSpan MaxTimeBetweenFlushes = TimeSpan.FromSeconds(1);
|
||||
|
||||
[Serializable]
|
||||
private class CacheEntry {
|
||||
public string llmId;
|
||||
public string text;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class CacheFile {
|
||||
public List<CacheEntry> entries = new();
|
||||
}
|
||||
|
||||
private class GameCache {
|
||||
public Dictionary<string, string> entries = new();
|
||||
public int dirtyEntries;
|
||||
public DateTime lastFlushUtc = DateTime.MinValue;
|
||||
public bool loaded;
|
||||
}
|
||||
|
||||
private static readonly object Lock = new();
|
||||
private static readonly Dictionary<long, GameCache> Caches = new();
|
||||
private static string _cacheDirectory;
|
||||
|
||||
public static Dictionary<string, string> LoadCompletedTexts(long gameId) {
|
||||
EnsureCacheDirectoryInitialized();
|
||||
|
||||
lock (Lock) {
|
||||
var cache = CacheFor(gameId);
|
||||
EnsureLoaded(gameId, cache);
|
||||
return new Dictionary<string, string>(cache.entries);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveCompletedText(long gameId, string llmId, string text) {
|
||||
if (String.IsNullOrEmpty(llmId)) { return; }
|
||||
|
||||
lock (Lock) {
|
||||
if (_cacheDirectory == null) {
|
||||
Debug.LogWarning(
|
||||
"Cannot save client text cache before the cache directory has " +
|
||||
"been initialized on the main thread.");
|
||||
return;
|
||||
}
|
||||
|
||||
var cache = CacheFor(gameId);
|
||||
EnsureLoaded(gameId, cache);
|
||||
|
||||
var normalizedText = text ?? "";
|
||||
if (cache.entries.TryGetValue(llmId, out var existing) &&
|
||||
existing == normalizedText) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.entries[llmId] = normalizedText;
|
||||
cache.dirtyEntries++;
|
||||
if (ShouldFlush(cache)) { FlushLocked(gameId, cache); }
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlushAll() {
|
||||
lock (Lock) {
|
||||
if (_cacheDirectory == null) { return; }
|
||||
|
||||
foreach (var (gameId, cache) in Caches) {
|
||||
if (cache.dirtyEntries > 0) { FlushLocked(gameId, cache); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static GameCache CacheFor(long gameId) {
|
||||
if (!Caches.TryGetValue(gameId, out var cache)) {
|
||||
cache = new GameCache();
|
||||
Caches[gameId] = cache;
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
private static void EnsureLoaded(long gameId, GameCache gameCache) {
|
||||
if (gameCache.loaded) { return; }
|
||||
|
||||
gameCache.loaded = true;
|
||||
var path = CachePath(gameId);
|
||||
if (!File.Exists(path)) { return; }
|
||||
|
||||
try {
|
||||
var cache = JsonUtility.FromJson<CacheFile>(File.ReadAllText(path));
|
||||
if (cache?.entries == null) { return; }
|
||||
|
||||
foreach (var entry in cache.entries) {
|
||||
if (!String.IsNullOrEmpty(entry.llmId)) {
|
||||
gameCache.entries[entry.llmId] = entry.text ?? "";
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"Failed to load client text cache for game {gameId}: {e}");
|
||||
gameCache.entries.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureCacheDirectoryInitialized() {
|
||||
if (_cacheDirectory != null) { return; }
|
||||
|
||||
_cacheDirectory = Path.Combine(Application.persistentDataPath, "eagle0", "client_text");
|
||||
Application.quitting -= FlushAll;
|
||||
Application.quitting += FlushAll;
|
||||
}
|
||||
|
||||
private static bool ShouldFlush(GameCache cache) => cache.dirtyEntries
|
||||
>= MaxDirtyEntriesBeforeFlush
|
||||
|| DateTime.UtcNow - cache.lastFlushUtc
|
||||
>= MaxTimeBetweenFlushes;
|
||||
|
||||
private static void FlushLocked(long gameId, GameCache gameCache) {
|
||||
try {
|
||||
Directory.CreateDirectory(CacheDirectory);
|
||||
var cache = new CacheFile();
|
||||
foreach (var (id, cachedText) in gameCache.entries) {
|
||||
cache.entries.Add(new CacheEntry { llmId = id, text = cachedText });
|
||||
}
|
||||
|
||||
File.WriteAllText(CachePath(gameId), JsonUtility.ToJson(cache));
|
||||
gameCache.dirtyEntries = 0;
|
||||
gameCache.lastFlushUtc = DateTime.UtcNow;
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"Failed to save client text cache for game {gameId}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string CacheDirectory => _cacheDirectory;
|
||||
|
||||
private static string CachePath(long gameId) =>
|
||||
Path.Combine(CacheDirectory, $"{gameId:X}.json");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f2c53e67d8043d492e8d3277c6e5794
|
||||
guid: 3a7621b5f8514e8d8b2d4d8cbb8ee5d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -52,6 +52,21 @@ namespace eagle {
|
||||
lock (_lock) { return new Dictionary<string, TextEntry>(_streamingTexts); }
|
||||
}
|
||||
|
||||
public void ReplaceWithCachedCompletedTexts(Dictionary<String, String> completedTexts) {
|
||||
if (completedTexts == null) { throw new ArgumentNullException(nameof(completedTexts)); }
|
||||
|
||||
lock (_lock) {
|
||||
_streamingTexts.Clear();
|
||||
_pendingUpdates.Clear();
|
||||
|
||||
foreach (var (llmId, text) in completedTexts) {
|
||||
if (String.IsNullOrEmpty(llmId)) { continue; }
|
||||
_streamingTexts[llmId] = new TextEntry(text ?? "", true);
|
||||
_pendingUpdates.Add(llmId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the text dictionary. Thread-safe - can be called from any thread.
|
||||
/// Listeners are NOT notified here; call ProcessPendingUpdates from main thread.
|
||||
|
||||
+1
-4
@@ -145,10 +145,7 @@ namespace eagle {
|
||||
|
||||
public void Update() {
|
||||
// '?' key (Shift + /) toggles help panel when a command is selected
|
||||
if (Selector != null && Input.GetKeyDown(KeyCode.Slash) &&
|
||||
(Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))) {
|
||||
InfoButtonClicked();
|
||||
}
|
||||
if (Selector != null && GameInput.QuestionMarkPressed()) { InfoButtonClicked(); }
|
||||
}
|
||||
|
||||
public void PointerEnteredCommitButton() {
|
||||
|
||||
+2
@@ -84,6 +84,8 @@ namespace eagle {
|
||||
|
||||
private List<SingleEntryListener> listeners = new();
|
||||
|
||||
private void OnDestroy() { ClearListeners(); }
|
||||
|
||||
private void ClearListeners() {
|
||||
foreach (var listener in listeners) {
|
||||
ClientTextProvider.Provider.RemoveListener(listener);
|
||||
|
||||
+4
-13
@@ -1,19 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
public class KeyModifiedAmount {
|
||||
private static bool Shift() {
|
||||
return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
|
||||
}
|
||||
private static bool Control() {
|
||||
return Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
|
||||
}
|
||||
|
||||
public static int Amount() {
|
||||
if (Shift() && Control()) { return 1000; }
|
||||
if (Shift()) { return 100; }
|
||||
if (Control()) { return 10; }
|
||||
if (GameInput.ShiftPressed() && GameInput.ControlPressed()) { return 1000; }
|
||||
if (GameInput.ShiftPressed()) { return 100; }
|
||||
if (GameInput.ControlPressed()) { return 10; }
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -115,7 +115,7 @@ namespace eagle {
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {
|
||||
if (Input.GetButtonUp("Toggle Dominion Table") && disclosureToggle != null) {
|
||||
if (GameInput.ToggleDominionTableReleased() && disclosureToggle != null) {
|
||||
disclosureToggle.isOn = !disclosureToggle.isOn;
|
||||
}
|
||||
}
|
||||
@@ -140,4 +140,4 @@ namespace eagle {
|
||||
mapController.OverrideTargetedProvinces = new List<ProvinceId>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ namespace eagle {
|
||||
private static readonly int FlyBreathFireHash = Animator.StringToHash("flyBreathFire");
|
||||
private static readonly int FlyFastHash = Animator.StringToHash("flyFast");
|
||||
private static readonly int FlyHitHash = Animator.StringToHash("flyHit");
|
||||
private const string BeastSortingLayerName = "UI Base Overlay";
|
||||
private const int BeastCreatureSortingOrder = 103;
|
||||
|
||||
void Awake() {
|
||||
SpawnDragon();
|
||||
@@ -85,7 +87,8 @@ namespace eagle {
|
||||
FaceCameraAtSpawn(_dragonInstance.transform);
|
||||
|
||||
foreach (var renderer in _dragonInstance.GetComponentsInChildren<Renderer>()) {
|
||||
renderer.sortingOrder = 103;
|
||||
renderer.sortingLayerName = BeastSortingLayerName;
|
||||
renderer.sortingOrder = BeastCreatureSortingOrder;
|
||||
}
|
||||
if (clipToViewport) { RendererViewportClipper.Attach(_dragonInstance); }
|
||||
|
||||
|
||||
+33
-24
@@ -139,6 +139,9 @@ namespace eagle {
|
||||
dominionPanelControllerNarrow.gameObject.SetActive(false);
|
||||
showChronicleButton.gameObject.SetActive(false);
|
||||
|
||||
if (errorHandler == null) {
|
||||
errorHandler = GameSceneManager.Instance?.GetErrorHandler();
|
||||
}
|
||||
errorHandler.gameObject.SetActive(true);
|
||||
|
||||
ArrangeLayout();
|
||||
@@ -286,7 +289,7 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
if (Input.GetButtonUp("Windowed Mode")) {
|
||||
if (GameInput.WindowedModeReleased()) {
|
||||
#if UNITY_EDITOR
|
||||
// do nothing
|
||||
#else
|
||||
@@ -356,8 +359,11 @@ namespace eagle {
|
||||
}
|
||||
|
||||
// Fire-and-forget - subscription is awaited internally and failures are logged
|
||||
MainQueue.Q.EnqueueForNextUpdate(
|
||||
() => { _ = ModelUpdater.StartListeningForUpdates(); });
|
||||
var modelUpdater = ModelUpdater;
|
||||
MainQueue.Q.EnqueueForNextUpdate(() => {
|
||||
if (this == null || ModelUpdater != modelUpdater) { return; }
|
||||
_ = modelUpdater.StartListeningForUpdates();
|
||||
});
|
||||
|
||||
// Initialize tutorial system (onboarding starts after first model update in SwapModel)
|
||||
TutorialManager.Instance?.Initialize(this, null);
|
||||
@@ -820,16 +826,12 @@ namespace eagle {
|
||||
DoCommitCommand();
|
||||
}
|
||||
|
||||
public void GoToBattle() {
|
||||
public async void GoToBattle() {
|
||||
// End any active dialogue before transitioning to battle
|
||||
TutorialManager.Instance?.DialogueManager?.EndDialogueWithoutCompletion();
|
||||
|
||||
if (Model == null) { throw new InvalidOperationException("GoToBattle: Model is null"); }
|
||||
|
||||
if (shardokContainer == null) {
|
||||
throw new InvalidOperationException("GoToBattle: shardokContainer is null");
|
||||
}
|
||||
|
||||
ShardokGameModel selectedModel;
|
||||
|
||||
var runningBattles = Model.RunningShardokGameModels.Values;
|
||||
@@ -844,32 +846,28 @@ namespace eagle {
|
||||
}
|
||||
|
||||
var shardokModel = Model.ShardokGameModels[selectedModel.ShardokGameId];
|
||||
|
||||
shardokContainer.SetActive(true);
|
||||
var shardokController = shardokContainer.GetComponent<ShardokGameController>();
|
||||
if (shardokController == null) {
|
||||
throw new InvalidOperationException(
|
||||
"GoToBattle: shardokContainer missing ShardokGameController component");
|
||||
}
|
||||
|
||||
PersistentUIManager.Instance?.connectionStatusUI?.SetShardokGameStateProvider(
|
||||
shardokModel);
|
||||
shardokController.SetUpGame(shardokModel);
|
||||
gameObject.SetActive(false);
|
||||
await ShowShardokBattle(shardokModel);
|
||||
}
|
||||
|
||||
public void GoToBattle(string shardokGameId) {
|
||||
public async void GoToBattle(string shardokGameId) {
|
||||
TutorialManager.Instance?.DialogueManager?.EndDialogueWithoutCompletion();
|
||||
battleProgressPanel.gameObject.SetActive(false);
|
||||
|
||||
if (Model == null) { throw new InvalidOperationException("GoToBattle: Model is null"); }
|
||||
|
||||
var shardokModel = Model.ShardokGameModels[shardokGameId];
|
||||
await ShowShardokBattle(shardokModel);
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task ShowShardokBattle(ShardokGameModel shardokModel) {
|
||||
shardokContainer = GameSceneManager.Instance == null
|
||||
? shardokContainer
|
||||
: await GameSceneManager.Instance.TransitionToShardok();
|
||||
|
||||
if (shardokContainer == null) {
|
||||
throw new InvalidOperationException("GoToBattle: shardokContainer is null");
|
||||
}
|
||||
|
||||
var shardokModel = Model.ShardokGameModels[shardokGameId];
|
||||
|
||||
shardokContainer.SetActive(true);
|
||||
var shardokController = shardokContainer.GetComponent<ShardokGameController>();
|
||||
if (shardokController == null) {
|
||||
@@ -888,6 +886,17 @@ namespace eagle {
|
||||
textField.text = textField.text == "" ? ModelUpdater.GameId.ToString("X") : "";
|
||||
}
|
||||
|
||||
private void OnDestroy() { factionTextUpdater?.Dispose(); }
|
||||
private void OnDestroy() {
|
||||
if (ModelUpdater != null) {
|
||||
ModelUpdater.StopListeningForUpdates();
|
||||
ModelUpdater.UpdateAction = null;
|
||||
ModelUpdater.ActionResultAction = null;
|
||||
ModelUpdater.NoteRecipient = null;
|
||||
ModelUpdater.ErrorHandler = null;
|
||||
ModelUpdater = null;
|
||||
}
|
||||
|
||||
factionTextUpdater?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace eagle {
|
||||
/// </summary>
|
||||
public Func<long, bool> HasPendingCommandWithToken { get; set; }
|
||||
|
||||
private void Notify(Notification notification) { NoteRecipient(notification); }
|
||||
private void Notify(Notification notification) { NoteRecipient?.Invoke(notification); }
|
||||
|
||||
private readonly ResourceFetcher resourceFetcher;
|
||||
|
||||
@@ -257,6 +257,8 @@ namespace eagle {
|
||||
PersistentClientConnection pers) {
|
||||
GameId = eagleGameId;
|
||||
PersistentConnection = pers;
|
||||
ClientTextProvider.Provider.ReplaceWithCachedCompletedTexts(
|
||||
ClientTextDiskCache.LoadCompletedTexts(GameId));
|
||||
|
||||
_currentModel.LastPostedToken = -1;
|
||||
_currentModel.PlayerId = factionId;
|
||||
@@ -268,6 +270,34 @@ namespace eagle {
|
||||
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
|
||||
|
||||
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
|
||||
|
||||
RestoreCachedStateIfAvailable(factionId);
|
||||
}
|
||||
|
||||
private void RestoreCachedStateIfAvailable(FactionId? factionId) {
|
||||
var cachedState = EagleGameStateDiskCache.Load(GameId, factionId);
|
||||
if (cachedState == null) {
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_CACHE] Cache miss game={GameId} " +
|
||||
$"player={factionId?.ToString() ?? "spectator"} " +
|
||||
$"reason={EagleGameStateDiskCache.LastLoadDiagnostic}");
|
||||
return;
|
||||
}
|
||||
|
||||
_currentModel.GsView = cachedState.GameStateView;
|
||||
_currentModel.BattalionTypes = cachedState.GameStateView.BattalionTypes.ToDictionary(
|
||||
bt => bt.TypeId,
|
||||
bt => bt);
|
||||
_currentModel.ChronicleEntries = cachedState.GameStateView.ChronicleEntries.ToList();
|
||||
|
||||
lock (_resultCountLock) {
|
||||
_lastUnfilteredResultCount = cachedState.UnfilteredResultCount;
|
||||
}
|
||||
_lastAppliedUnfilteredCount = cachedState.UnfilteredResultCount;
|
||||
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_CACHE] Restored game {GameId} at count " +
|
||||
$"{cachedState.UnfilteredResultCount}; Shardok state will refresh from server");
|
||||
}
|
||||
|
||||
private ShardokGameModel MakeGameModel(ShardokGameId shardokGameId) {
|
||||
@@ -379,8 +409,13 @@ namespace eagle {
|
||||
_connectionLogger.LogLine(
|
||||
"[UPDATE] Processed update and invoked UpdateAction");
|
||||
} else {
|
||||
RecordAppliedUnfilteredCount(
|
||||
updateItem.ActionResultResponse.UnfilteredResultCountAfter);
|
||||
SaveCurrentStateToCache(
|
||||
updateItem.ActionResultResponse.UnfilteredResultCountAfter);
|
||||
_connectionLogger.LogLine(
|
||||
"[UPDATE] SKIPPED - no results, has commands, token matches");
|
||||
"[UPDATE] SKIPPED - no results, has commands, token matches; " +
|
||||
"persisted accepted count");
|
||||
}
|
||||
|
||||
// Check for turn state mismatch after processing update
|
||||
@@ -443,6 +478,8 @@ namespace eagle {
|
||||
shardokGameModel;
|
||||
}
|
||||
|
||||
SaveCurrentStateToCache();
|
||||
|
||||
// If we created new models, re-subscribe so the server knows to keep
|
||||
// sending updates for these games. The original subscription didn't
|
||||
// include them because ShardokGameModels was empty at subscribe time.
|
||||
@@ -477,6 +514,7 @@ namespace eagle {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.BattleProgressResponse:
|
||||
_currentModel.BattleDay = updateItem.BattleProgressResponse.CurrentDay;
|
||||
ApplyBattleProgressUpdate(updateItem.BattleProgressResponse.UpdatedBattles);
|
||||
SaveCurrentStateToCache();
|
||||
if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
|
||||
break;
|
||||
|
||||
@@ -689,8 +727,35 @@ namespace eagle {
|
||||
}
|
||||
|
||||
if (countAfter > _lastAppliedUnfilteredCount) {
|
||||
_lastAppliedUnfilteredCount = countAfter;
|
||||
RecordAppliedUnfilteredCount(countAfter);
|
||||
}
|
||||
SaveCurrentStateToCache(countAfter);
|
||||
}
|
||||
|
||||
private void RecordAppliedUnfilteredCount(int countAfter) {
|
||||
if (countAfter <= _lastAppliedUnfilteredCount) { return; }
|
||||
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_CACHE] Accepted Eagle count {_lastAppliedUnfilteredCount} -> {countAfter}");
|
||||
_lastAppliedUnfilteredCount = countAfter;
|
||||
}
|
||||
|
||||
private void SaveCurrentStateToCache(int unfilteredResultCount) {
|
||||
if (unfilteredResultCount <= 0 || _currentModel.GsView == null) { return; }
|
||||
|
||||
EagleGameStateDiskCache.Save(
|
||||
GameId,
|
||||
_currentModel.PlayerId,
|
||||
unfilteredResultCount,
|
||||
_currentModel.GsView);
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_CACHE] Saved game={GameId} count={unfilteredResultCount}");
|
||||
}
|
||||
|
||||
private void SaveCurrentStateToCache() {
|
||||
int count;
|
||||
lock (_resultCountLock) { count = _lastUnfilteredResultCount; }
|
||||
SaveCurrentStateToCache(count);
|
||||
}
|
||||
|
||||
private static HashSet<FactionId> InvitationAcceptedFactionIds(
|
||||
@@ -1080,11 +1145,14 @@ namespace eagle {
|
||||
private void HandleNewStreamingText(StreamingTextResponse str) {
|
||||
if (str == null) return;
|
||||
|
||||
ClientTextProvider.Provider.HandleNewStreamingText(
|
||||
var updatedText = ClientTextProvider.Provider.HandleNewStreamingText(
|
||||
str.LlmIdentifier,
|
||||
str.NewText,
|
||||
str.StartingByteCount,
|
||||
str.Completed);
|
||||
if (str.Completed) {
|
||||
ClientTextDiskCache.SaveCompletedText(GameId, str.LlmIdentifier, updatedText);
|
||||
}
|
||||
}
|
||||
|
||||
private void MaybeSendNotification(
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Google.Protobuf;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
public static class EagleGameStateDiskCache {
|
||||
private const int CurrentSchemaVersion = 3;
|
||||
|
||||
[Serializable]
|
||||
private class CacheFile {
|
||||
public int schemaVersion;
|
||||
public long gameId;
|
||||
public bool hasPlayerId;
|
||||
public int playerId;
|
||||
public int unfilteredResultCount;
|
||||
public string gameStateViewBase64;
|
||||
public string savedAtUtc;
|
||||
}
|
||||
|
||||
public class CachedGameState {
|
||||
public GameStateView GameStateView { get; set; }
|
||||
public int UnfilteredResultCount { get; set; }
|
||||
}
|
||||
|
||||
private static string _cacheDirectory;
|
||||
public static string LastLoadDiagnostic { get; private set; }
|
||||
|
||||
public static CachedGameState Load(long gameId, int? playerId) {
|
||||
EnsureCacheDirectoryInitialized();
|
||||
|
||||
var path = CachePath(gameId, playerId);
|
||||
LastLoadDiagnostic = $"path={path}";
|
||||
if (!File.Exists(path)) {
|
||||
LastLoadDiagnostic = $"file not found at {path}";
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
var cache = JsonUtility.FromJson<CacheFile>(File.ReadAllText(path));
|
||||
if (cache == null) {
|
||||
LastLoadDiagnostic = $"cache JSON parsed to null at {path}";
|
||||
return null;
|
||||
}
|
||||
if (cache.schemaVersion != CurrentSchemaVersion) {
|
||||
LastLoadDiagnostic =
|
||||
$"schema mismatch at {path}: cache={cache.schemaVersion} current={CurrentSchemaVersion}";
|
||||
return null;
|
||||
}
|
||||
if (cache.gameId != gameId) {
|
||||
LastLoadDiagnostic =
|
||||
$"game id mismatch at {path}: cache={cache.gameId} requested={gameId}";
|
||||
return null;
|
||||
}
|
||||
if (cache.hasPlayerId != playerId.HasValue) {
|
||||
LastLoadDiagnostic =
|
||||
$"player presence mismatch at {path}: cacheHas={cache.hasPlayerId} requestedHas={playerId.HasValue}";
|
||||
return null;
|
||||
}
|
||||
if (cache.hasPlayerId && cache.playerId != playerId.Value) {
|
||||
LastLoadDiagnostic =
|
||||
$"player id mismatch at {path}: cache={cache.playerId} requested={playerId.Value}";
|
||||
return null;
|
||||
}
|
||||
if (cache.unfilteredResultCount <= 0) {
|
||||
LastLoadDiagnostic =
|
||||
$"invalid unfiltered count at {path}: {cache.unfilteredResultCount}";
|
||||
return null;
|
||||
}
|
||||
if (String.IsNullOrEmpty(cache.gameStateViewBase64)) {
|
||||
LastLoadDiagnostic = $"missing GameStateView payload at {path}";
|
||||
return null;
|
||||
}
|
||||
|
||||
var bytes = Convert.FromBase64String(cache.gameStateViewBase64);
|
||||
var gameStateView = GameStateView.Parser.ParseFrom(bytes);
|
||||
if (gameStateView.GameId == 0) {
|
||||
gameStateView.GameId = gameId;
|
||||
} else if (gameStateView.GameId != gameId) {
|
||||
LastLoadDiagnostic =
|
||||
$"GameStateView game id mismatch at {path}: view={gameStateView.GameId} requested={gameId}";
|
||||
return null;
|
||||
}
|
||||
|
||||
LastLoadDiagnostic = $"loaded {path} count={cache.unfilteredResultCount}";
|
||||
|
||||
return new CachedGameState {
|
||||
GameStateView = gameStateView,
|
||||
UnfilteredResultCount = cache.unfilteredResultCount
|
||||
};
|
||||
} catch (Exception e) {
|
||||
LastLoadDiagnostic = $"exception loading {path}: {e}";
|
||||
Debug.LogWarning($"Failed to load Eagle game state cache for game {gameId}: {e}");
|
||||
TryDelete(path);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void
|
||||
Save(long gameId, int? playerId, int unfilteredResultCount, GameStateView gameStateView) {
|
||||
if (unfilteredResultCount <= 0 || gameStateView == null) { return; }
|
||||
|
||||
EnsureCacheDirectoryInitialized();
|
||||
|
||||
try {
|
||||
Directory.CreateDirectory(CacheDirectory);
|
||||
var cache = new CacheFile {
|
||||
schemaVersion = CurrentSchemaVersion,
|
||||
gameId = gameId,
|
||||
hasPlayerId = playerId.HasValue,
|
||||
playerId = playerId ?? 0,
|
||||
unfilteredResultCount = unfilteredResultCount,
|
||||
gameStateViewBase64 = Convert.ToBase64String(gameStateView.ToByteArray()),
|
||||
savedAtUtc = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
File.WriteAllText(CachePath(gameId, playerId), JsonUtility.ToJson(cache));
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"Failed to save Eagle game state cache for game {gameId}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureCacheDirectoryInitialized() {
|
||||
if (_cacheDirectory != null) { return; }
|
||||
|
||||
_cacheDirectory =
|
||||
Path.Combine(Application.persistentDataPath, "eagle0", "eagle_game_state");
|
||||
}
|
||||
|
||||
private static string CacheDirectory => _cacheDirectory;
|
||||
|
||||
private static string CachePath(long gameId, int? playerId) {
|
||||
var playerKey = playerId.HasValue ? playerId.Value.ToString() : "spectator";
|
||||
return Path.Combine(CacheDirectory, $"{gameId:X}-{playerKey}.json");
|
||||
}
|
||||
|
||||
private static void TryDelete(string path) {
|
||||
try {
|
||||
File.Delete(path);
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"Failed to delete corrupt Eagle game state cache {path}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d07c0b7fb8a4e45ac80c1479562dd9a
|
||||
guid: 187990bd7c5c4c02a67ee224d3f6e519
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
public static class GeneratedTextGlyphValidator {
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
private static readonly HashSet<string> ReportedMissingGlyphs = new();
|
||||
#endif
|
||||
|
||||
public static void
|
||||
LogMissingGlyphs(TMP_Text textComponent, string renderedText, string context) {
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
if (String.IsNullOrEmpty(renderedText)) { return; }
|
||||
|
||||
var fontAsset = textComponent.font ?? TMP_Settings.defaultFontAsset;
|
||||
if (fontAsset == null) { return; }
|
||||
|
||||
var visibleCharacters = VisibleCharacters(renderedText);
|
||||
if (visibleCharacters.Length == 0) { return; }
|
||||
|
||||
if (fontAsset.HasCharacters(
|
||||
visibleCharacters,
|
||||
out uint[] missingUnicodes,
|
||||
true,
|
||||
false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newlyReported = missingUnicodes
|
||||
.Where(unicode => ReportedMissingGlyphs.Add(
|
||||
ReportKey(fontAsset, unicode)))
|
||||
.Select(DescribeUnicode)
|
||||
.ToList();
|
||||
if (newlyReported.Count == 0) { return; }
|
||||
|
||||
Debug.LogWarning(
|
||||
$"Generated text contains glyphs missing from TMP font asset '{fontAsset.name}' and its fallbacks: " +
|
||||
$"{String.Join(", ", newlyReported)}. Context: {context}. " +
|
||||
"Bake these glyphs into the static TMP fallback font before shipping text that can contain them.",
|
||||
textComponent);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
private static string VisibleCharacters(string text) {
|
||||
var builder = new StringBuilder(text.Length);
|
||||
foreach (var character in text.Where(character => !Char.IsControl(character))) {
|
||||
builder.Append(character);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string ReportKey(TMP_FontAsset fontAsset, uint unicode) {
|
||||
return $"{fontAsset.GetInstanceID()}:{unicode:X}";
|
||||
}
|
||||
|
||||
private static string DescribeUnicode(uint unicode) {
|
||||
return $"U+{unicode:X4} '{Char.ConvertFromUtf32((int)unicode)}'";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 330d986b5aa54ebc81e3518425ce03ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+29
-3
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -45,7 +46,7 @@ namespace eagle {
|
||||
|
||||
private void UpdateView() {
|
||||
if (String.IsNullOrEmpty(TextId)) {
|
||||
if (TextId == "") { GetComponent<TMP_Text>().text = PrefixedText; }
|
||||
if (TextId == "") { SetRenderedText(PrefixedText); }
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +57,32 @@ namespace eagle {
|
||||
for (var i = 0; i < dots; i++) { text += "."; }
|
||||
}
|
||||
|
||||
GetComponent<TMP_Text>().text = PrefixedText + text;
|
||||
SetRenderedText(PrefixedText + text);
|
||||
}
|
||||
|
||||
private void SetRenderedText(string renderedText) {
|
||||
var textComponent = GetComponent<TMP_Text>();
|
||||
GeneratedTextGlyphValidator.LogMissingGlyphs(
|
||||
textComponent,
|
||||
renderedText,
|
||||
TextContext());
|
||||
textComponent.text = renderedText;
|
||||
}
|
||||
|
||||
private string TextContext() {
|
||||
return String.IsNullOrEmpty(TextId) ? GameObjectPath(transform)
|
||||
: $"{TextId} at {GameObjectPath(transform)}";
|
||||
}
|
||||
|
||||
private static string GameObjectPath(Transform current) {
|
||||
var names = new List<string>();
|
||||
while (current != null) {
|
||||
names.Add(current.name);
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
names.Reverse();
|
||||
return String.Join("/", names);
|
||||
}
|
||||
|
||||
private void OnTextUpdate(TextEntry entry) {
|
||||
@@ -88,4 +114,4 @@ namespace eagle {
|
||||
|
||||
private void OnDisable() { ClientTextProvider.Provider.RemoveListener(this); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,16 @@ namespace eagle {
|
||||
void Start() {
|
||||
Enabled = true;
|
||||
_previouslyHoveredProvinceId = null;
|
||||
PlaceClickedProvincePanelAboveMap();
|
||||
}
|
||||
|
||||
private void PlaceClickedProvincePanelAboveMap() {
|
||||
var clickedProvincePanel = provinceNameText.transform.parent;
|
||||
clickedProvincePanel.SetAsLastSibling();
|
||||
|
||||
foreach (var graphic in clickedProvincePanel.GetComponentsInChildren<Graphic>()) {
|
||||
graphic.raycastTarget = false;
|
||||
}
|
||||
}
|
||||
|
||||
int? ProvinceFromPoint(Vector2 point) {
|
||||
@@ -260,7 +270,7 @@ namespace eagle {
|
||||
}
|
||||
_activeHighlights.Clear();
|
||||
|
||||
var maybeHoveredProvinceId = ProvinceFromPoint(Input.mousePosition);
|
||||
var maybeHoveredProvinceId = ProvinceFromPoint(GameInput.PointerPosition());
|
||||
if (Model != null && maybeHoveredProvinceId != _previouslyHoveredProvinceId) {
|
||||
_previouslyHoveredProvinceId = maybeHoveredProvinceId;
|
||||
hoveredProvinceChangedCallback.Invoke(maybeHoveredProvinceId);
|
||||
|
||||
@@ -68,12 +68,12 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void HandleTouchInput() {
|
||||
if (Input.touchCount == 2) {
|
||||
Touch touch0 = Input.GetTouch(0);
|
||||
Touch touch1 = Input.GetTouch(1);
|
||||
if (GameInput.TouchCount() == 2) {
|
||||
var touch0 = GameInput.GetTouch(0);
|
||||
var touch1 = GameInput.GetTouch(1);
|
||||
|
||||
float currentDistance = Vector2.Distance(touch0.position, touch1.position);
|
||||
Vector2 currentCenter = (touch0.position + touch1.position) / 2f;
|
||||
float currentDistance = Vector2.Distance(touch0.Position, touch1.Position);
|
||||
Vector2 currentCenter = (touch0.Position + touch1.Position) / 2f;
|
||||
|
||||
if (!_isPinching) {
|
||||
_isPinching = true;
|
||||
|
||||
@@ -43,6 +43,8 @@ namespace eagle {
|
||||
private static readonly int SpecialATrigger = Animator.StringToHash("SpecialATrigger");
|
||||
private static readonly int MoveStateHash = Animator.StringToHash("Move");
|
||||
private static readonly int IdleStateHash = Animator.StringToHash("Idle");
|
||||
private const string BeastSortingLayerName = "UI Base Overlay";
|
||||
private const int BeastCreatureSortingOrder = 103;
|
||||
|
||||
void Start() { SpawnMonsters(); }
|
||||
|
||||
@@ -61,7 +63,8 @@ namespace eagle {
|
||||
instance.transform.localPosition = startPos;
|
||||
|
||||
foreach (var renderer in instance.GetComponentsInChildren<Renderer>()) {
|
||||
renderer.sortingOrder = 103;
|
||||
renderer.sortingLayerName = BeastSortingLayerName;
|
||||
renderer.sortingOrder = BeastCreatureSortingOrder;
|
||||
}
|
||||
if (clipToViewport) { RendererViewportClipper.Attach(instance); }
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace eagle {
|
||||
private bool _beastEffectsLoading;
|
||||
private string _pendingBeastVisualName;
|
||||
private GameObject _activeBeastVisual;
|
||||
private Coroutine _spawnBeastVisualCoroutine;
|
||||
private bool _isDestroying;
|
||||
private const float BeastVisualForwardZ = -25f;
|
||||
private const float BeastVisualNotificationScale = 2f;
|
||||
|
||||
@@ -31,7 +33,10 @@ namespace eagle {
|
||||
// to skip adding if a dismiss happened since they were enqueued
|
||||
private int _dismissGeneration = 0;
|
||||
|
||||
private void OnNoteTextChanged() { SetPopupInfos(); }
|
||||
private void OnNoteTextChanged() {
|
||||
if (_isDestroying) { return; }
|
||||
SetPopupInfos();
|
||||
}
|
||||
|
||||
private void TrackNote(Notification note) { note.TextChanged += OnNoteTextChanged; }
|
||||
|
||||
@@ -41,6 +46,8 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void SetPopupInfos() {
|
||||
if (_isDestroying) { return; }
|
||||
|
||||
PopupInfos = _notes.Select(note => new PopupInfo {
|
||||
titleText = note.Title,
|
||||
mainText = note.Text,
|
||||
@@ -51,8 +58,6 @@ namespace eagle {
|
||||
})
|
||||
.ToList();
|
||||
|
||||
SetUpPanel();
|
||||
|
||||
// If there are no images, add a spacer to the panel
|
||||
spacer.gameObject.SetActive(!imagesAndNamesContainer.gameObject.activeSelf);
|
||||
if (PopupInfos.Count == 1) {
|
||||
@@ -66,6 +71,15 @@ namespace eagle {
|
||||
|
||||
void Awake() { HideBeastVisual(); }
|
||||
|
||||
void OnDestroy() {
|
||||
_isDestroying = true;
|
||||
_dismissGeneration++;
|
||||
foreach (var note in _notes) { ReleaseNote(note); }
|
||||
_notes.Clear();
|
||||
ClearBeastVisual();
|
||||
OnAllDismissed = null;
|
||||
}
|
||||
|
||||
void OnEnable() {
|
||||
if (HasInfo) { UpdateBeastVisual(); }
|
||||
}
|
||||
@@ -88,6 +102,11 @@ namespace eagle {
|
||||
// we'll skip adding the note
|
||||
var capturedGeneration = _dismissGeneration;
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (_isDestroying || this == null) {
|
||||
ReleaseNote(notification);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if DismissAll was clicked since this was enqueued
|
||||
if (capturedGeneration != _dismissGeneration) {
|
||||
ReleaseNote(notification);
|
||||
@@ -135,7 +154,7 @@ namespace eagle {
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnBeastVisual(beastName);
|
||||
QueueSpawnBeastVisual(beastName);
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator LoadBeastEffects() {
|
||||
@@ -151,7 +170,7 @@ namespace eagle {
|
||||
_beastEffectsLoading = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_pendingBeastVisualName)) {
|
||||
SpawnBeastVisual(_pendingBeastVisualName);
|
||||
QueueSpawnBeastVisual(_pendingBeastVisualName);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
@@ -160,6 +179,22 @@ namespace eagle {
|
||||
});
|
||||
}
|
||||
|
||||
private void QueueSpawnBeastVisual(string beastName) {
|
||||
CancelQueuedBeastVisualSpawn();
|
||||
_spawnBeastVisualCoroutine = StartCoroutine(SpawnBeastVisualWhenReady(beastName));
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator SpawnBeastVisualWhenReady(string beastName) {
|
||||
yield return null;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
_spawnBeastVisualCoroutine = null;
|
||||
|
||||
if (!isActiveAndEnabled || _pendingBeastVisualName != beastName) { yield break; }
|
||||
|
||||
SpawnBeastVisual(beastName);
|
||||
}
|
||||
|
||||
private void SpawnBeastVisual(string beastName) {
|
||||
ClearBeastVisual();
|
||||
|
||||
@@ -180,11 +215,23 @@ namespace eagle {
|
||||
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rectTransform.pivot = new Vector2(0.5f, 0.5f);
|
||||
rectTransform.anchoredPosition3D = new Vector3(0f, 0f, BeastVisualForwardZ);
|
||||
rectTransform.sizeDelta = beastVisualContainer.rect.size;
|
||||
rectTransform.sizeDelta = ResolveBeastVisualSize(beastVisualContainer);
|
||||
rectTransform.localScale = Vector3.one;
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2 ResolveBeastVisualSize(RectTransform container) {
|
||||
var size = container.rect.size;
|
||||
if (size.x >= 1f && size.y >= 1f) { return size; }
|
||||
|
||||
var layoutElement = container.GetComponent<LayoutElement>();
|
||||
if (layoutElement == null) { return size; }
|
||||
|
||||
return new Vector2(
|
||||
layoutElement.preferredWidth > 0f ? layoutElement.preferredWidth : size.x,
|
||||
layoutElement.preferredHeight > 0f ? layoutElement.preferredHeight : size.y);
|
||||
}
|
||||
|
||||
private static void ConfigureContainedSingleBeast(GameObject effect) {
|
||||
var beastsEffect = effect.GetComponent<BeastsEffect>();
|
||||
if (beastsEffect != null) {
|
||||
@@ -241,12 +288,20 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private void ClearBeastVisual() {
|
||||
CancelQueuedBeastVisualSpawn();
|
||||
if (_activeBeastVisual != null) {
|
||||
Destroy(_activeBeastVisual);
|
||||
_activeBeastVisual = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelQueuedBeastVisualSpawn() {
|
||||
if (_spawnBeastVisualCoroutine == null) { return; }
|
||||
|
||||
StopCoroutine(_spawnBeastVisualCoroutine);
|
||||
_spawnBeastVisualCoroutine = null;
|
||||
}
|
||||
|
||||
public void DismissButtonClicked() {
|
||||
ReleaseNote(_notes.Dequeue());
|
||||
SetPopupInfos();
|
||||
|
||||
+13
-2
@@ -285,10 +285,15 @@ namespace eagle {
|
||||
lock (_pendingSubscriptionAcks) { _pendingSubscriptionAcks[gameId] = ackTcs; }
|
||||
|
||||
var shardokCount = shardokStatuses.Count();
|
||||
var streamingTextStatuses = streamGameRequest.StreamingTextStatuses;
|
||||
var completeTextCount = streamingTextStatuses.Count(status => status.KnownComplete);
|
||||
var incompleteTextCount = streamingTextStatuses.Count - completeTextCount;
|
||||
LogFlow($"SUBSCRIBE game={gameId} unfilteredCount={subscriber.LastUnfilteredResultCount} shardokGames={shardokCount}");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Sending StreamGameRequest for game {gameId}, " +
|
||||
$"unfilteredCount={subscriber.LastUnfilteredResultCount}");
|
||||
$"unfilteredCount={subscriber.LastUnfilteredResultCount}, " +
|
||||
$"shardokGames={shardokCount}, completeTexts={completeTextCount}, " +
|
||||
$"incompleteTexts={incompleteTextCount}");
|
||||
|
||||
var sendSuccess = await DoWithStreamingCall(async (sc) => {
|
||||
await sc.RequestStream.WriteAsync(request);
|
||||
@@ -1040,11 +1045,17 @@ namespace eagle {
|
||||
// Listeners are notified later via ProcessPendingUpdates() on main thread.
|
||||
var str = gameUpdate.StreamingTextResponse;
|
||||
if (str != null) {
|
||||
ClientTextProvider.Provider.HandleNewStreamingText(
|
||||
var updatedText = ClientTextProvider.Provider.HandleNewStreamingText(
|
||||
str.LlmIdentifier,
|
||||
str.NewText,
|
||||
str.StartingByteCount,
|
||||
str.Completed);
|
||||
if (str.Completed) {
|
||||
ClientTextDiskCache.SaveCompletedText(
|
||||
gameUpdate.GameId,
|
||||
str.LlmIdentifier,
|
||||
updatedText);
|
||||
}
|
||||
}
|
||||
// No MainQueue enqueue needed - ProcessPendingUpdates handles listener
|
||||
// notification
|
||||
|
||||
+3
-2
@@ -187,8 +187,9 @@ namespace eagle {
|
||||
private static readonly HashSet<string> ButcherBeastNames =
|
||||
new(StringComparer.Ordinal) { "psychopath", "maniac" };
|
||||
|
||||
private static GameObject
|
||||
LookupPrefab(IReadOnlyDictionary<string, GameObject> effectPrefabs, string prefabName) {
|
||||
private static GameObject LookupPrefab(
|
||||
IReadOnlyDictionary<string, GameObject> effectPrefabs,
|
||||
string prefabName) {
|
||||
effectPrefabs.TryGetValue(prefabName, out var prefab);
|
||||
if (prefab == null) {
|
||||
Debug.LogWarning(
|
||||
|
||||
+2
-2
@@ -380,11 +380,11 @@ namespace eagle {
|
||||
panel.SetActive(true);
|
||||
}
|
||||
|
||||
if (!WideScreen && Input.GetButtonUp("Toggle Dominion Table")) {
|
||||
if (!WideScreen && GameInput.ToggleDominionTableReleased()) {
|
||||
dominionViewToggle.isOn = !dominionViewToggle.isOn;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy() { factionTextUpdater?.Dispose(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-388
@@ -1,388 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using common.GUIUtils;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public static class ConnectionSceneMigrationTool {
|
||||
private const string GameplayScenePath = "Assets/Gameplay.unity";
|
||||
private const string ConnectionScenePath = "Assets/Scenes/Connection.unity";
|
||||
private const string EagleScenePath = "Assets/Scenes/Eagle.unity";
|
||||
private const string ShardokScenePath = "Assets/Scenes/Shardok.unity";
|
||||
private const string SharedScenePath = "Assets/Scenes/Shared.unity";
|
||||
private const string ConnectionCanvasName = "ConnectionCanvas";
|
||||
private const string EagleCanvasName = "Eagle Canvas";
|
||||
private const string EaglePlaceholderCanvasName = "EagleCanvas";
|
||||
private const string ErrorCanvasName = "ErrorCanvas";
|
||||
private const string ShardokContainerName = "Shardok Container";
|
||||
private const string ShardokPlaceholderCanvasName = "ShardokCanvas";
|
||||
private const string CopiedSceneIncompleteReason =
|
||||
"Connection scene was copied from Gameplay but still needs cross-scene reference " +
|
||||
"migration before it can replace Gameplay.";
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Rebuild Connection Scene From Gameplay")]
|
||||
public static void RebuildConnectionSceneFromGameplay() {
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log("Connection scene rebuild canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Rebuild Connection Scene",
|
||||
"This will replace the ConnectionCanvas in Assets/Scenes/Connection.unity " +
|
||||
"with a copy from Assets/Gameplay.unity. The copied canvas will stay " +
|
||||
"inactive and marked not production-ready.",
|
||||
"Rebuild",
|
||||
"Cancel")) {
|
||||
Debug.Log("Connection scene rebuild canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
|
||||
var sourceConnectionCanvas = FindRootByName(gameplayScene, ConnectionCanvasName);
|
||||
if (sourceConnectionCanvas == null) {
|
||||
Debug.LogError(
|
||||
$"Could not find root GameObject '{ConnectionCanvasName}' in {GameplayScenePath}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionScene =
|
||||
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Additive);
|
||||
foreach (var existingCanvas in FindRootsByName(connectionScene, ConnectionCanvasName)) {
|
||||
Object.DestroyImmediate(existingCanvas);
|
||||
}
|
||||
|
||||
var copiedCanvas = Object.Instantiate(sourceConnectionCanvas);
|
||||
copiedCanvas.name = ConnectionCanvasName;
|
||||
copiedCanvas.SetActive(false);
|
||||
SceneManager.MoveGameObjectToScene(copiedCanvas, connectionScene);
|
||||
|
||||
MarkSceneNotReady(copiedCanvas);
|
||||
LogConnectionHandlerReferenceReport(copiedCanvas);
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(connectionScene);
|
||||
EditorSceneManager.SaveScene(connectionScene);
|
||||
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Single);
|
||||
|
||||
Debug.Log(
|
||||
$"Rebuilt {ConnectionScenePath} from {GameplayScenePath}. " +
|
||||
$"{ConnectionCanvasName} is inactive and productionReady is false.");
|
||||
}
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Rebuild Eagle And Shardok Scenes From Gameplay")]
|
||||
public static void RebuildEagleAndShardokScenesFromGameplay() {
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log(
|
||||
"Eagle/Shardok scene rebuild canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Rebuild Eagle And Shardok Scenes",
|
||||
"This will replace the placeholder content in Assets/Scenes/Eagle.unity and " +
|
||||
"Assets/Scenes/Shardok.unity with copies from Assets/Gameplay.unity. " +
|
||||
"References that belong to other split scenes will be cleared so they " +
|
||||
"can be wired at runtime.",
|
||||
"Rebuild",
|
||||
"Cancel")) {
|
||||
Debug.Log("Eagle/Shardok scene rebuild canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
|
||||
|
||||
var copiedEagleCanvas = RebuildSceneObjectFromGameplay(
|
||||
gameplayScene,
|
||||
EagleScenePath,
|
||||
EagleCanvasName,
|
||||
EagleCanvasName,
|
||||
EaglePlaceholderCanvasName,
|
||||
EagleCanvasName);
|
||||
if (copiedEagleCanvas == null) return;
|
||||
|
||||
ClearEagleSceneExternalReferences(copiedEagleCanvas);
|
||||
int clearedEagleReferences =
|
||||
ClearExternalSceneReferences(copiedEagleCanvas, EagleScenePath);
|
||||
var eagleScene = SceneManager.GetSceneByPath(EagleScenePath);
|
||||
EditorSceneManager.MarkSceneDirty(eagleScene);
|
||||
EditorSceneManager.SaveScene(eagleScene);
|
||||
|
||||
var copiedShardokContainer = RebuildSceneObjectFromGameplay(
|
||||
gameplayScene,
|
||||
ShardokScenePath,
|
||||
ShardokContainerName,
|
||||
ShardokContainerName,
|
||||
ShardokPlaceholderCanvasName,
|
||||
ShardokContainerName);
|
||||
if (copiedShardokContainer == null) return;
|
||||
|
||||
ClearShardokSceneExternalReferences(copiedShardokContainer);
|
||||
int clearedShardokReferences =
|
||||
ClearExternalSceneReferences(copiedShardokContainer, ShardokScenePath);
|
||||
var shardokScene = SceneManager.GetSceneByPath(ShardokScenePath);
|
||||
EditorSceneManager.MarkSceneDirty(shardokScene);
|
||||
EditorSceneManager.SaveScene(shardokScene);
|
||||
|
||||
EditorSceneManager.OpenScene(EagleScenePath, OpenSceneMode.Single);
|
||||
|
||||
Debug.Log(
|
||||
$"Rebuilt {EagleScenePath} and {ShardokScenePath} from {GameplayScenePath}. " +
|
||||
"EagleGameController.errorHandler, EagleGameController.shardokContainer, and " +
|
||||
"ShardokGameController.eagleCanvas were cleared for runtime scene wiring. " +
|
||||
$"Cleared {clearedEagleReferences} additional Eagle scene references and " +
|
||||
$"{clearedShardokReferences} additional Shardok scene references.");
|
||||
}
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Rebuild Shared Error UI From Gameplay")]
|
||||
public static void RebuildSharedErrorUIFromGameplay() {
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log("Shared error UI rebuild canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Rebuild Shared Error UI",
|
||||
"This will replace the ErrorCanvas in Assets/Scenes/Shared.unity with a copy " +
|
||||
"from Assets/Gameplay.unity.",
|
||||
"Rebuild",
|
||||
"Cancel")) {
|
||||
Debug.Log("Shared error UI rebuild canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
|
||||
var sourceErrorCanvas = FindRootByName(gameplayScene, ErrorCanvasName);
|
||||
if (sourceErrorCanvas == null) {
|
||||
Debug.LogError(
|
||||
$"Could not find root GameObject '{ErrorCanvasName}' in {GameplayScenePath}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var sharedScene = EditorSceneManager.OpenScene(SharedScenePath, OpenSceneMode.Additive);
|
||||
foreach (var existingErrorCanvas in FindRootsByName(sharedScene, ErrorCanvasName)) {
|
||||
Object.DestroyImmediate(existingErrorCanvas);
|
||||
}
|
||||
|
||||
var copiedErrorCanvas = Object.Instantiate(sourceErrorCanvas);
|
||||
copiedErrorCanvas.name = ErrorCanvasName;
|
||||
ClearSharedErrorUIExternalReferences(copiedErrorCanvas);
|
||||
SceneManager.MoveGameObjectToScene(copiedErrorCanvas, sharedScene);
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(sharedScene);
|
||||
EditorSceneManager.SaveScene(sharedScene);
|
||||
EditorSceneManager.OpenScene(SharedScenePath, OpenSceneMode.Single);
|
||||
|
||||
Debug.Log($"Rebuilt {SharedScenePath} {ErrorCanvasName} from {GameplayScenePath}.");
|
||||
}
|
||||
|
||||
private static GameObject RebuildSceneObjectFromGameplay(
|
||||
Scene gameplayScene,
|
||||
string targetScenePath,
|
||||
string sourceObjectName,
|
||||
string copiedObjectName,
|
||||
params string[] objectNamesToReplace) {
|
||||
var sourceObject = FindObjectByName(gameplayScene, sourceObjectName);
|
||||
if (sourceObject == null) {
|
||||
Debug.LogError(
|
||||
$"Could not find GameObject '{sourceObjectName}' in {GameplayScenePath}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var targetScene = EditorSceneManager.OpenScene(targetScenePath, OpenSceneMode.Additive);
|
||||
foreach (var objectNameToReplace in objectNamesToReplace) {
|
||||
var existingObjects = FindObjectsByName(targetScene, objectNameToReplace).ToList();
|
||||
foreach (var existingObject in existingObjects) {
|
||||
Object.DestroyImmediate(existingObject);
|
||||
}
|
||||
}
|
||||
|
||||
var copiedObject = Object.Instantiate(sourceObject);
|
||||
copiedObject.name = copiedObjectName;
|
||||
SceneManager.MoveGameObjectToScene(copiedObject, targetScene);
|
||||
return copiedObject;
|
||||
}
|
||||
|
||||
private static void ClearEagleSceneExternalReferences(GameObject eagleCanvas) {
|
||||
var eagleController = eagleCanvas.GetComponent<eagle.EagleGameController>();
|
||||
if (eagleController == null) return;
|
||||
|
||||
eagleController.errorHandler = null;
|
||||
eagleController.shardokContainer = null;
|
||||
EditorUtility.SetDirty(eagleController);
|
||||
}
|
||||
|
||||
private static void ClearShardokSceneExternalReferences(GameObject shardokContainer) {
|
||||
var shardokController = shardokContainer.GetComponent<Shardok.ShardokGameController>();
|
||||
if (shardokController == null) return;
|
||||
|
||||
shardokController.eagleCanvas = null;
|
||||
EditorUtility.SetDirty(shardokController);
|
||||
}
|
||||
|
||||
private static int ClearExternalSceneReferences(GameObject sceneRoot, string targetScenePath) {
|
||||
int clearedReferences = 0;
|
||||
foreach (var component in sceneRoot.GetComponentsInChildren<Component>(
|
||||
includeInactive: true)) {
|
||||
if (component == null) continue;
|
||||
|
||||
var serializedObject = new SerializedObject(component);
|
||||
var property = serializedObject.GetIterator();
|
||||
bool modified = false;
|
||||
while (property.NextVisible(enterChildren: true)) {
|
||||
if (property.propertyType != SerializedPropertyType.ObjectReference) continue;
|
||||
if (property.objectReferenceValue == null) continue;
|
||||
if (!ReferencesDifferentScene(property.objectReferenceValue, targetScenePath))
|
||||
continue;
|
||||
|
||||
property.objectReferenceValue = null;
|
||||
clearedReferences++;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(component);
|
||||
}
|
||||
}
|
||||
|
||||
return clearedReferences;
|
||||
}
|
||||
|
||||
private static bool ReferencesDifferentScene(Object referencedObject, string targetScenePath) {
|
||||
var referencedGameObject = GetReferencedGameObject(referencedObject);
|
||||
if (referencedGameObject == null) return false;
|
||||
|
||||
var referencedScene = referencedGameObject.scene;
|
||||
return referencedScene.IsValid() && !string.IsNullOrEmpty(referencedScene.path) &&
|
||||
referencedScene.path != targetScenePath;
|
||||
}
|
||||
|
||||
private static void ClearSharedErrorUIExternalReferences(GameObject errorCanvas) {
|
||||
var errorHandler = errorCanvas.GetComponent<ErrorHandler>();
|
||||
if (errorHandler == null) { return; }
|
||||
|
||||
errorHandler.bugReportPanel = null;
|
||||
EditorUtility.SetDirty(errorHandler);
|
||||
}
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Validate Connection Scene Readiness")]
|
||||
public static void ValidateConnectionSceneReadiness() {
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log(
|
||||
"Connection scene validation canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionScene =
|
||||
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Single);
|
||||
var connectionCanvases = FindRootsByName(connectionScene, ConnectionCanvasName).ToList();
|
||||
if (connectionCanvases.Count != 1) {
|
||||
Debug.LogError(
|
||||
$"{ConnectionScenePath} should contain exactly one root named " +
|
||||
$"{ConnectionCanvasName}, but found {connectionCanvases.Count}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionCanvas = connectionCanvases[0];
|
||||
var readiness = connectionCanvas.GetComponent<ConnectionSceneReadiness>();
|
||||
if (readiness == null) {
|
||||
Debug.LogError($"{ConnectionCanvasName} is missing ConnectionSceneReadiness.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (readiness.IsReady(out var reason)) {
|
||||
var handler = connectionCanvas.GetComponentInChildren<ConnectionHandler>(
|
||||
includeInactive: true);
|
||||
if (handler == null) {
|
||||
Debug.LogError(
|
||||
$"{ConnectionScenePath} is marked ready but {ConnectionCanvasName} " +
|
||||
"does not contain a ConnectionHandler.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"{ConnectionScenePath} is marked production-ready.");
|
||||
LogConnectionHandlerReferenceReport(connectionCanvas);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionCanvas.activeSelf) {
|
||||
Debug.LogWarning(
|
||||
$"{ConnectionCanvasName} is active while the scene is not production-ready. " +
|
||||
"Bootstrap will still fall back, but the scene should stay inert until migration " +
|
||||
"is complete.");
|
||||
}
|
||||
|
||||
Debug.Log($"{ConnectionScenePath} is not production-ready: {reason}");
|
||||
LogConnectionHandlerReferenceReport(connectionCanvas);
|
||||
}
|
||||
|
||||
private static GameObject FindRootByName(Scene scene, string rootName) {
|
||||
return FindRootsByName(scene, rootName).FirstOrDefault();
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> FindRootsByName(Scene scene, string rootName) {
|
||||
return scene.GetRootGameObjects().Where(root => root.name == rootName);
|
||||
}
|
||||
|
||||
private static GameObject FindObjectByName(Scene scene, string objectName) {
|
||||
return FindObjectsByName(scene, objectName).FirstOrDefault();
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> FindObjectsByName(Scene scene, string objectName) {
|
||||
return scene.GetRootGameObjects()
|
||||
.SelectMany(root => root.GetComponentsInChildren<Transform>(includeInactive: true))
|
||||
.Select(transform => transform.gameObject)
|
||||
.Where(gameObject => gameObject.name == objectName);
|
||||
}
|
||||
|
||||
private static void MarkSceneNotReady(GameObject connectionCanvas) {
|
||||
var readiness = connectionCanvas.GetComponent<ConnectionSceneReadiness>();
|
||||
if (readiness == null)
|
||||
readiness = connectionCanvas.AddComponent<ConnectionSceneReadiness>();
|
||||
|
||||
var serializedReadiness = new SerializedObject(readiness);
|
||||
serializedReadiness.FindProperty("productionReady").boolValue = false;
|
||||
serializedReadiness.FindProperty("incompleteReason").stringValue =
|
||||
CopiedSceneIncompleteReason;
|
||||
serializedReadiness.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void LogConnectionHandlerReferenceReport(GameObject connectionCanvas) {
|
||||
var handler =
|
||||
connectionCanvas.GetComponentInChildren<ConnectionHandler>(includeInactive: true);
|
||||
if (handler == null) {
|
||||
Debug.LogWarning($"{ConnectionCanvasName} does not contain a ConnectionHandler yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
LogReferenceSceneWarning(handler.eagleCanvas, nameof(handler.eagleCanvas));
|
||||
LogReferenceSceneWarning(handler.shardokContainer, nameof(handler.shardokContainer));
|
||||
LogReferenceSceneWarning(handler.errorHandler, nameof(handler.errorHandler));
|
||||
}
|
||||
|
||||
private static void LogReferenceSceneWarning(Object referencedObject, string fieldName) {
|
||||
if (referencedObject == null) {
|
||||
Debug.LogWarning($"ConnectionHandler.{fieldName} is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
var referencedGameObject = GetReferencedGameObject(referencedObject);
|
||||
if (referencedGameObject == null) return;
|
||||
|
||||
var scene = referencedGameObject.scene;
|
||||
if (scene.IsValid() && scene.path != ConnectionScenePath) {
|
||||
Debug.LogWarning(
|
||||
$"ConnectionHandler.{fieldName} still references '{referencedGameObject.name}' " +
|
||||
$"from {scene.path}. Move or replace this dependency before marking " +
|
||||
"Connection production-ready.");
|
||||
}
|
||||
}
|
||||
|
||||
private static GameObject GetReferencedGameObject(Object referencedObject) {
|
||||
if (referencedObject is GameObject gameObject) return gameObject;
|
||||
if (referencedObject is Component component) return component.gameObject;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using common.GUIUtils;
|
||||
using NUnit.Framework;
|
||||
using Shardok;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace eagle0.Tests {
|
||||
[TestFixture]
|
||||
public class SceneSplitValidationTests {
|
||||
private const string MainScenePath = "Assets/Scenes/Main.unity";
|
||||
private const string ConnectionScenePath = "Assets/Scenes/Connection.unity";
|
||||
private const string EagleScenePath = "Assets/Scenes/Eagle.unity";
|
||||
private const string ShardokScenePath = "Assets/Scenes/Shardok.unity";
|
||||
private const string SharedScenePath = "Assets/Scenes/Shared.unity";
|
||||
|
||||
[Test]
|
||||
public void BuildSettings_UseSplitScenesOnly() {
|
||||
var enabledScenes = EditorBuildSettings.scenes.Where(scene => scene.enabled)
|
||||
.Select(scene => scene.path)
|
||||
.ToList();
|
||||
|
||||
CollectionAssert.AreEqual(
|
||||
new[] {
|
||||
MainScenePath,
|
||||
ConnectionScenePath,
|
||||
EagleScenePath,
|
||||
ShardokScenePath,
|
||||
SharedScenePath,
|
||||
},
|
||||
enabledScenes);
|
||||
|
||||
CollectionAssert.DoesNotContain(enabledScenes, "Assets/Gameplay.unity");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SplitSceneAssetsExist() {
|
||||
foreach (var scenePath in SplitScenePaths()) {
|
||||
Assert.NotNull(
|
||||
AssetDatabase.LoadAssetAtPath<SceneAsset>(scenePath),
|
||||
$"{scenePath} must exist.");
|
||||
}
|
||||
|
||||
Assert.IsFalse(
|
||||
AssetDatabase.IsValidFolder("Assets/Gameplay"),
|
||||
"Retired Gameplay scene assets should not remain in Assets/Gameplay.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MainSceneContainsBootstrapInfrastructure() {
|
||||
var scene = OpenScene(MainScenePath);
|
||||
|
||||
AssertHasComponent<BootstrapManager>(scene);
|
||||
AssertHasComponent<GameSceneManager>(scene);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConnectionSceneContainsConnectionController() {
|
||||
var scene = OpenScene(ConnectionScenePath);
|
||||
var connectionRoot = AssertRoot(scene, "ConnectionCanvas");
|
||||
|
||||
Assert.NotNull(
|
||||
connectionRoot.GetComponent<ConnectionHandler>(),
|
||||
"ConnectionCanvas must contain ConnectionHandler.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EagleSceneContainsEagleController() {
|
||||
var scene = OpenScene(EagleScenePath);
|
||||
var eagleRoot = AssertRoot(scene, "Eagle Canvas");
|
||||
|
||||
Assert.NotNull(
|
||||
eagleRoot.GetComponent<eagle.EagleGameController>(),
|
||||
"Eagle Canvas must contain EagleGameController.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShardokSceneContainsShardokController() {
|
||||
var scene = OpenScene(ShardokScenePath);
|
||||
var shardokRoot = AssertRoot(scene, "Shardok Container");
|
||||
|
||||
Assert.NotNull(
|
||||
shardokRoot.GetComponent<ShardokGameController>(),
|
||||
"Shardok Container must contain ShardokGameController.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SharedSceneContainsSharedUiInfrastructure() {
|
||||
var scene = OpenScene(SharedScenePath);
|
||||
|
||||
AssertHasComponent<SettingsPanelController>(scene);
|
||||
AssertHasComponent<ErrorHandler>(scene);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SplitScenePaths() {
|
||||
yield return MainScenePath;
|
||||
yield return ConnectionScenePath;
|
||||
yield return EagleScenePath;
|
||||
yield return ShardokScenePath;
|
||||
yield return SharedScenePath;
|
||||
}
|
||||
|
||||
private static Scene OpenScene(string scenePath) {
|
||||
return EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
||||
}
|
||||
|
||||
private static GameObject AssertRoot(Scene scene, string rootName) {
|
||||
var root = scene.GetRootGameObjects().FirstOrDefault(obj => obj.name == rootName);
|
||||
Assert.NotNull(root, $"{scene.path} must contain root object '{rootName}'.");
|
||||
return root;
|
||||
}
|
||||
|
||||
private static void AssertHasComponent<T>(Scene scene)
|
||||
where T : Component {
|
||||
var component = Resources.FindObjectsOfTypeAll<T>().FirstOrDefault(
|
||||
item => item.gameObject.scene == scene);
|
||||
Assert.NotNull(component, $"{scene.path} must contain {typeof(T).Name}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f3e0bc72ed64160b35d557710f32f35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class TmpFallbackFontTool {
|
||||
private const string FallbackFontAssetPath =
|
||||
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset";
|
||||
|
||||
[MenuItem("Eagle0/Fonts/Bake TMP Fallback Font")]
|
||||
public static void BakeFallbackFont() {
|
||||
var fontAsset = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(FallbackFontAssetPath);
|
||||
if (fontAsset == null) {
|
||||
throw new InvalidOperationException(
|
||||
$"Could not load TMP fallback font at {FallbackFontAssetPath}");
|
||||
}
|
||||
|
||||
var unicodes = CuratedFallbackUnicodes();
|
||||
var characters = UnicodeString(unicodes);
|
||||
|
||||
fontAsset.atlasPopulationMode = AtlasPopulationMode.Dynamic;
|
||||
fontAsset.TryAddCharacters(unicodes, out _);
|
||||
fontAsset.atlasPopulationMode = AtlasPopulationMode.Static;
|
||||
|
||||
EditorUtility.SetDirty(fontAsset);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
if (fontAsset.HasCharacters(characters, out uint[] missingUnicodes)) {
|
||||
Debug.Log(
|
||||
$"Baked {unicodes.Length} curated glyphs into {FallbackFontAssetPath} and set it to Static.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning(
|
||||
$"Baked TMP fallback font, but the source font does not contain {missingUnicodes.Length} curated glyphs: " +
|
||||
String.Join(", ", missingUnicodes.Select(DescribeUnicode)));
|
||||
}
|
||||
|
||||
private static uint[] CuratedFallbackUnicodes() {
|
||||
var unicodes = new SortedSet<uint>();
|
||||
|
||||
AddRange(unicodes, 0x0020, 0x007E); // Basic Latin, printable ASCII
|
||||
AddRange(unicodes, 0x00A0, 0x00FF); // Latin-1 Supplement
|
||||
AddRange(unicodes, 0x0100, 0x017F); // Latin Extended-A
|
||||
AddRange(unicodes, 0x0180, 0x024F); // Latin Extended-B
|
||||
|
||||
uint[] commonSymbols = {
|
||||
0x2013, // EN DASH
|
||||
0x2014, // EM DASH
|
||||
0x2018, // LEFT SINGLE QUOTATION MARK
|
||||
0x2019, // RIGHT SINGLE QUOTATION MARK
|
||||
0x201C, // LEFT DOUBLE QUOTATION MARK
|
||||
0x201D, // RIGHT DOUBLE QUOTATION MARK
|
||||
0x2020, // DAGGER
|
||||
0x2021, // DOUBLE DAGGER
|
||||
0x2022, // BULLET
|
||||
0x2026, // HORIZONTAL ELLIPSIS
|
||||
0x2030, // PER MILLE SIGN
|
||||
0x2032, // PRIME
|
||||
0x2033, // DOUBLE PRIME
|
||||
0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK
|
||||
0x203A, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
|
||||
0x2044, // FRACTION SLASH
|
||||
0x20AC, // EURO SIGN
|
||||
0x2122, // TRADE MARK SIGN
|
||||
0x2190, // LEFTWARDS ARROW
|
||||
0x2191, // UPWARDS ARROW
|
||||
0x2192, // RIGHTWARDS ARROW
|
||||
0x2193, // DOWNWARDS ARROW
|
||||
0x2194, // LEFT RIGHT ARROW
|
||||
0x2195, // UP DOWN ARROW
|
||||
0x2211, // N-ARY SUMMATION
|
||||
0x2212, // MINUS SIGN
|
||||
0x221A, // SQUARE ROOT
|
||||
0x221E, // INFINITY
|
||||
0x221F, // RIGHT ANGLE
|
||||
0x2229, // INTERSECTION
|
||||
0x222B, // INTEGRAL
|
||||
0x2248, // ALMOST EQUAL TO
|
||||
0x2260, // NOT EQUAL TO
|
||||
0x2261, // IDENTICAL TO
|
||||
0x2264, // LESS-THAN OR EQUAL TO
|
||||
0x2265, // GREATER-THAN OR EQUAL TO
|
||||
0x25A0, // BLACK SQUARE
|
||||
0x25A1, // WHITE SQUARE
|
||||
0x25B2, // BLACK UP-POINTING TRIANGLE
|
||||
0x25BA, // BLACK RIGHT-POINTING POINTER
|
||||
0x25BC, // BLACK DOWN-POINTING TRIANGLE
|
||||
0x25C4, // BLACK LEFT-POINTING POINTER
|
||||
0x25CA, // LOZENGE
|
||||
0x25CB, // WHITE CIRCLE
|
||||
0x25CF, // BLACK CIRCLE
|
||||
0x25E6, // WHITE BULLET
|
||||
};
|
||||
|
||||
foreach (var unicode in commonSymbols) { unicodes.Add(unicode); }
|
||||
|
||||
return unicodes.ToArray();
|
||||
}
|
||||
|
||||
private static void AddRange(ISet<uint> unicodes, uint first, uint last) {
|
||||
for (var unicode = first; unicode <= last; unicode++) { unicodes.Add(unicode); }
|
||||
}
|
||||
|
||||
private static string UnicodeString(IEnumerable<uint> unicodes) {
|
||||
var builder = new StringBuilder();
|
||||
foreach (var unicode in unicodes) { builder.Append(Char.ConvertFromUtf32((int)unicode)); }
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string DescribeUnicode(uint unicode) {
|
||||
return $"U+{unicode:X4} '{Char.ConvertFromUtf32((int)unicode)}'";
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97718fa2c55143a6b9c09a0eba312194
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+2
-2
@@ -22,9 +22,9 @@ public class CtrPanel : MonoBehaviour {
|
||||
void Update() {
|
||||
if (panels.Count <= 0 || !isReady) return;
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.LeftArrow)) {
|
||||
if (GameInput.LeftArrowPressed()) {
|
||||
Click_Prev();
|
||||
} else if (Input.GetKeyDown(KeyCode.RightArrow)) {
|
||||
} else if (GameInput.RightArrowPressed()) {
|
||||
Click_Next();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 265b489d182ad614e940fb7e18012b0d
|
||||
timeCreated: 1513468984
|
||||
licenseType: Free
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f8740f39f9a94ca2b5870243dead5a8
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 1
|
||||
seamlessCubemap: 1
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 2
|
||||
aniso: 0
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 2
|
||||
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: 100
|
||||
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:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -46,7 +46,7 @@ public class HoveringTooltip : MonoBehaviour {
|
||||
// ItemRect is in screen pixels. Position the tooltip to the right of the item.
|
||||
Vector2 screenPos;
|
||||
if (HugsCursor) {
|
||||
screenPos = (Vector2)Input.mousePosition + new Vector2(15, -10);
|
||||
screenPos = GameInput.PointerPosition() + new Vector2(15, -10);
|
||||
} else {
|
||||
screenPos = new Vector2(ItemRect.xMax + 10, ItemRect.center.y);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ public class HoveringTooltip : MonoBehaviour {
|
||||
float screenWidth = rectTransform.rect.width * canvas.scaleFactor;
|
||||
if (screenPos.x + screenWidth > Screen.width) {
|
||||
if (HugsCursor) {
|
||||
screenPos.x = Input.mousePosition.x - screenWidth - 15;
|
||||
screenPos.x = GameInput.PointerPosition().x - screenWidth - 15;
|
||||
} else {
|
||||
screenPos.x = ItemRect.xMin - screenWidth - 10;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,23 @@ public class MainQueue : MonoBehaviour {
|
||||
private MainQueue() {}
|
||||
|
||||
void Awake() {
|
||||
if (__singletonInstance == null) { __singletonInstance = this; }
|
||||
if (__singletonInstance == null) {
|
||||
__singletonInstance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (__singletonInstance != this) {
|
||||
if (IsDedicatedMainQueueObject()) {
|
||||
Destroy(gameObject);
|
||||
} else {
|
||||
Destroy(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsDedicatedMainQueueObject() {
|
||||
return gameObject.name == "Main Queue" && GetComponents<Component>().Length == 2;
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
@@ -76,5 +92,20 @@ public class MainQueue : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy() { __singletonInstance = null; }
|
||||
public static MainQueue EnsureExists() {
|
||||
if (__singletonInstance != null) { return __singletonInstance; }
|
||||
|
||||
var existingMainQueue = FindAnyObjectByType<MainQueue>(FindObjectsInactive.Include);
|
||||
if (existingMainQueue != null) {
|
||||
__singletonInstance = existingMainQueue;
|
||||
return __singletonInstance;
|
||||
}
|
||||
|
||||
var mainQueueObject = new GameObject("Main Queue");
|
||||
return mainQueueObject.AddComponent<MainQueue>();
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (__singletonInstance == this) { __singletonInstance = null; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using UnityEngine.EventSystems;
|
||||
|
||||
public interface IClickReceiver {
|
||||
HexGrid HexGrid { get; }
|
||||
bool CanReceiveGridInput { get; }
|
||||
void HandleLongHover(int? gridIndex, Vector3 mousePosition);
|
||||
void HandleLongUIHover(IEnumerable<GameObject> hoveredObjects);
|
||||
void HandlePrimaryClick(int clickedIndex);
|
||||
@@ -49,22 +50,26 @@ public class MouseHandler : MonoBehaviour {
|
||||
}
|
||||
|
||||
void HandleTouchLongPress() {
|
||||
// Handle touch input for long-press detection
|
||||
if (Input.touchCount > 0) {
|
||||
Touch touch = Input.GetTouch(0);
|
||||
if (clickReceiver?.CanReceiveGridInput != true) { return; }
|
||||
|
||||
if (touch.phase == TouchPhase.Began) {
|
||||
// Handle touch input for long-press detection
|
||||
if (GameInput.TouchCount() > 0) {
|
||||
var touch = GameInput.GetTouch(0);
|
||||
|
||||
if (touch.Phase == TouchPhase.Began) {
|
||||
// Touch started - begin tracking
|
||||
if (!EventSystem.current.IsPointerOverGameObject(touch.fingerId)) {
|
||||
var touchDownGridIndex = currentGridIndex(touch.Position);
|
||||
if (touchDownGridIndex.HasValue ||
|
||||
!IsPointerOverBlockingUI(touch.FingerId, touch.Position)) {
|
||||
_touchDownTime = Time.time;
|
||||
_touchDownPosition = touch.position;
|
||||
_touchDownGridIndex = currentGridIndex();
|
||||
_touchDownPosition = touch.Position;
|
||||
_touchDownGridIndex = touchDownGridIndex;
|
||||
_longPressTriggered = false;
|
||||
}
|
||||
} else if (touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary) {
|
||||
} else if (touch.Phase == TouchPhase.Moved || touch.Phase == TouchPhase.Stationary) {
|
||||
// Touch ongoing - check for long-press
|
||||
if (_touchDownTime.HasValue && !_longPressTriggered) {
|
||||
var moved = Vector2.Distance(touch.position, _touchDownPosition);
|
||||
var moved = Vector2.Distance(touch.Position, _touchDownPosition);
|
||||
if (moved > MovementTolerancePixels) {
|
||||
// Finger moved too much, cancel long-press
|
||||
_touchDownTime = null;
|
||||
@@ -76,20 +81,20 @@ public class MouseHandler : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled) {
|
||||
} else if (touch.Phase == TouchPhase.Ended || touch.Phase == TouchPhase.Canceled) {
|
||||
// Touch ended
|
||||
_touchDownTime = null;
|
||||
_touchDownGridIndex = null;
|
||||
}
|
||||
} else {
|
||||
_touchDownTime = null;
|
||||
_touchDownGridIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<RaycastResult> RaycastMouse() {
|
||||
PointerEventData pointerData = new PointerEventData(EventSystem.current) {
|
||||
pointerId = -1,
|
||||
};
|
||||
|
||||
pointerData.position = Input.mousePosition;
|
||||
private List<RaycastResult> RaycastPointer(Vector2 position, int pointerId) {
|
||||
PointerEventData pointerData = new PointerEventData(
|
||||
EventSystem.current) { pointerId = pointerId, position = position };
|
||||
|
||||
List<RaycastResult> results = new List<RaycastResult>();
|
||||
EventSystem.current.RaycastAll(pointerData, results);
|
||||
@@ -97,19 +102,38 @@ public class MouseHandler : MonoBehaviour {
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<RaycastResult> RaycastMouse() {
|
||||
return RaycastPointer(GameInput.PointerPosition(), -1);
|
||||
}
|
||||
|
||||
private bool IsPointerOverBlockingUI(int pointerId, Vector2 position) {
|
||||
if (EventSystem.current == null || clickReceiver?.HexGrid == null) { return false; }
|
||||
|
||||
var raycastResults = RaycastPointer(position, pointerId);
|
||||
return raycastResults.Any(
|
||||
raycastResult =>
|
||||
!clickReceiver.HexGrid.ContainsMapVisual(raycastResult.gameObject));
|
||||
}
|
||||
|
||||
void HandleMouse() {
|
||||
if (clickReceiver.HexGrid == null) { return; }
|
||||
if (clickReceiver?.CanReceiveGridInput != true) { return; }
|
||||
|
||||
int? possibleGridIndex = currentGridIndex();
|
||||
bool pointerOverBlockingUI = IsPointerOverBlockingUI(-1, GameInput.PointerPosition());
|
||||
|
||||
Vector3 currentPosition = Input.mousePosition;
|
||||
Vector3 currentPosition = GameInput.PointerPosition();
|
||||
if (currentPosition == hoverPosition) {
|
||||
if (Time.time - hoverStartSeconds > HOVER_THRESHOLD_SECONDS) {
|
||||
isInLongHover = true;
|
||||
if (possibleGridIndex.HasValue) {
|
||||
clickReceiver.HandleLongHover(possibleGridIndex.Value, currentPosition);
|
||||
} else if (EventSystem.current.IsPointerOverGameObject()) {
|
||||
var raycastResults = RaycastMouse();
|
||||
} else if (pointerOverBlockingUI) {
|
||||
var raycastResults =
|
||||
RaycastMouse()
|
||||
.Where(raycastResult =>
|
||||
!clickReceiver.HexGrid.ContainsMapVisual(
|
||||
raycastResult.gameObject))
|
||||
.ToList();
|
||||
var gameObjects =
|
||||
raycastResults.Select(raycastResult => raycastResult.gameObject);
|
||||
if (raycastResults.Count > 0) { clickReceiver.HandleLongUIHover(gameObjects); }
|
||||
@@ -126,7 +150,7 @@ public class MouseHandler : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
|
||||
if (!possibleGridIndex.HasValue || EventSystem.current.IsPointerOverGameObject()) {
|
||||
if (!possibleGridIndex.HasValue) {
|
||||
if (lastHoveredIndex is int index) {
|
||||
clickReceiver.HandleStopHover(index);
|
||||
lastHoveredIndex = null;
|
||||
@@ -139,9 +163,9 @@ public class MouseHandler : MonoBehaviour {
|
||||
clickReceiver.HandleStopHover(lhIndex);
|
||||
lastHoveredIndex = null;
|
||||
}
|
||||
if (Input.GetMouseButton(0)) {
|
||||
if (GameInput.PrimaryPointerPressed()) {
|
||||
handleLeftMouseDown(currentMouseGridIndex);
|
||||
} else if (Input.GetMouseButton(1)) {
|
||||
} else if (GameInput.SecondaryPointerPressed()) {
|
||||
handleRightMouseDown(currentMouseGridIndex);
|
||||
} else {
|
||||
if (leftButtonDownIndex.HasValue) {
|
||||
@@ -170,13 +194,6 @@ public class MouseHandler : MonoBehaviour {
|
||||
}
|
||||
|
||||
void handleLeftMouseDown(int gridIndex) {
|
||||
if (EventSystem.current.IsPointerOverGameObject()) {
|
||||
leftButtonDownIndex = null;
|
||||
lastConveyedIndex = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!leftButtonDownIndex.HasValue) { leftButtonDownIndex = gridIndex; }
|
||||
if (gridIndex != lastConveyedIndex) {
|
||||
lastConveyedIndex = gridIndex;
|
||||
@@ -185,13 +202,6 @@ public class MouseHandler : MonoBehaviour {
|
||||
}
|
||||
|
||||
void handleRightMouseDown(int gridIndex) {
|
||||
if (EventSystem.current.IsPointerOverGameObject()) {
|
||||
rightButtonDownIndex = null;
|
||||
lastRightConveyedIndex = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rightButtonDownIndex.HasValue) { rightButtonDownIndex = gridIndex; }
|
||||
if (gridIndex != lastRightConveyedIndex) {
|
||||
lastRightConveyedIndex = gridIndex;
|
||||
@@ -199,7 +209,9 @@ public class MouseHandler : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
|
||||
int? currentGridIndex() {
|
||||
return clickReceiver.HexGrid.GridIndexForScreenPoint(Input.mousePosition);
|
||||
int? currentGridIndex() { return currentGridIndex(GameInput.PointerPosition()); }
|
||||
|
||||
int? currentGridIndex(Vector2 screenPosition) {
|
||||
return clickReceiver.HexGrid.GridIndexForScreenPoint(screenPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +1,23 @@
|
||||
# Next Steps for Scene Separation
|
||||
# Scene Split Status
|
||||
|
||||
## Current Status ✅
|
||||
- Created Main.unity with GameSceneManager
|
||||
- Created empty scenes: Connection.unity, Eagle.unity, Shardok.unity, Shared.unity
|
||||
- Created BootstrapManager for transitional loading
|
||||
- Created SceneMigrationHelper for analysis
|
||||
- Basic scene loading system works
|
||||
## Current Status
|
||||
|
||||
## Immediate Next Steps
|
||||
- `Main.unity` is the startup scene.
|
||||
- `BootstrapManager` starts the client by loading `Shared.unity` and `Connection.unity`.
|
||||
- `Connection.unity`, `Eagle.unity`, `Shardok.unity`, and `Shared.unity` are included in build settings.
|
||||
- Runtime scene references are wired by `GameSceneManager` after additive scene loads.
|
||||
|
||||
### 1. Add New Scenes to Build Settings
|
||||
In Unity:
|
||||
1. File → Build Settings
|
||||
2. Add the new scenes to the build:
|
||||
- Assets/Scenes/Eagle.unity
|
||||
- Assets/Scenes/Shardok.unity
|
||||
- Assets/Scenes/Shared.unity
|
||||
3. Ensure they're all checked for inclusion
|
||||
## Follow-Up Validation
|
||||
|
||||
### 2. Test Scene Loading System
|
||||
1. In Main.unity, add a BootstrapManager component to the SceneManager GameObject
|
||||
2. In the BootstrapManager inspector:
|
||||
- Set `testSceneLoading` to true
|
||||
- Set `useOriginalGameplayScene` to false
|
||||
3. Play the scene and check Console
|
||||
4. Should see: "✓ Connection scene loaded", "✓ Eagle scene loaded", etc.
|
||||
Keep playtesting the scene flow through connection, Eagle, custom battles, Shardok battles, and return-to-lobby.
|
||||
|
||||
### 3. Test with Original Gameplay Scene
|
||||
1. In BootstrapManager inspector:
|
||||
- Set `testSceneLoading` to false
|
||||
- Set `useOriginalGameplayScene` to true
|
||||
2. Add Gameplay.unity to Build Settings if not already there
|
||||
3. Play the scene
|
||||
4. Should load both Main.unity and Gameplay.unity together
|
||||
5. The game should work exactly as before
|
||||
## Validation Checklist
|
||||
|
||||
## Migration Strategy (Once Testing Works)
|
||||
|
||||
### Phase 1: Move Error/Popup UI (Safest)
|
||||
These are the least connected components:
|
||||
1. Find ErrorCanvas in Gameplay.unity hierarchy
|
||||
2. Right-click → Copy
|
||||
3. Open Shared.unity
|
||||
4. Right-click in hierarchy → Paste
|
||||
5. Save Shared.unity
|
||||
6. Test that error dialogs still work
|
||||
7. Delete ErrorCanvas from Gameplay.unity
|
||||
|
||||
### Phase 2: Move Connection UI
|
||||
1. Find ConnectionCanvas in Gameplay.unity
|
||||
2. Move Connection Panel, Game Selection Panel, Custom Battle Panel
|
||||
3. Update ConnectionHandler to work with scene loading
|
||||
|
||||
### Phase 3: Move Eagle UI
|
||||
1. Move EagleCanvas and related UI
|
||||
2. Update EagleGameController
|
||||
|
||||
### Phase 4: Move Shardok UI
|
||||
1. Move ShardokCanvas and battle UI
|
||||
2. Update battle controllers
|
||||
|
||||
## Tools Created to Help
|
||||
|
||||
### SceneMigrationHelper
|
||||
- Attach to any GameObject in Gameplay.unity
|
||||
- Right-click component → "Analyze Scene Structure"
|
||||
- Shows which GameObjects belong to which scene
|
||||
- Right-click → "Find Cross-Scene References" to see dependencies
|
||||
|
||||
### BootstrapManager
|
||||
- Handles loading original scene OR new scene system
|
||||
- Has testing mode to verify all scenes load correctly
|
||||
- Provides smooth transition during migration
|
||||
|
||||
### GameSceneManager
|
||||
- Handles async scene loading/unloading
|
||||
- Prevents duplicate scene loads
|
||||
- Provides transition methods for different game states
|
||||
|
||||
## Safety Features
|
||||
|
||||
1. **Gradual Migration**: Move one component at a time
|
||||
2. **Fallback Option**: Can always switch back to original Gameplay.unity
|
||||
3. **Testing**: Each step can be verified independently
|
||||
4. **No Data Loss**: Original scene preserved until migration complete
|
||||
|
||||
## Expected Benefits Once Complete
|
||||
|
||||
- **Faster Unity startup** (scenes load quickly)
|
||||
- **Better collaboration** (less merge conflicts)
|
||||
- **Easier debugging** (isolate UI issues to specific scenes)
|
||||
- **Memory efficiency** (only load needed UI)
|
||||
- **Cleaner organization** (logical separation of concerns)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### If Scene Loading Fails:
|
||||
1. Check all scenes are in Build Settings
|
||||
2. Check Console for specific errors
|
||||
3. Ensure no duplicate EventSystems (should be only in Shared.unity)
|
||||
|
||||
### If Game Functionality Breaks:
|
||||
1. Switch BootstrapManager back to `useOriginalGameplayScene = true`
|
||||
2. This loads the full original scene as before
|
||||
3. No functionality should be lost
|
||||
|
||||
### If References Break:
|
||||
1. Use SceneMigrationHelper to find cross-scene dependencies
|
||||
2. Consider using ScriptableObjects or singletons for cross-scene communication
|
||||
3. Update scripts to find objects by type instead of direct reference
|
||||
|
||||
This approach ensures we can safely transition without breaking the working game!
|
||||
- Start from `Main.unity`.
|
||||
- Connect with a stored account and with a fresh sign-in.
|
||||
- List running games and enter an Eagle game.
|
||||
- Open and close Settings with the button and Escape.
|
||||
- Enter Shardok from Eagle and return to Eagle.
|
||||
- Start a custom battle and exit back to the lobby.
|
||||
- Confirm no duplicate `EventSystem` or `AudioListener` warnings.
|
||||
- Confirm CI Unity build passes.
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
# Scene Separation Plan
|
||||
|
||||
## Current Situation
|
||||
- Single massive Gameplay.unity scene (7.5MB, 255k lines)
|
||||
- Contains ALL game UI, connection screens, battle UI, etc.
|
||||
- High risk of corruption when editing
|
||||
|
||||
## Proposed Multi-Scene Architecture
|
||||
|
||||
### Core Scenes (Always Loaded)
|
||||
1. **Main.unity** - Minimal bootstrap scene
|
||||
- Scene manager
|
||||
- Persistent game systems
|
||||
- Audio manager
|
||||
- Network connection manager
|
||||
|
||||
### Loadable Scenes
|
||||
2. **Connection.unity** - Login and lobby UI
|
||||
- ConnectionCanvas
|
||||
- Connection Panel
|
||||
- Game Selection Panel
|
||||
- Custom Battle Panel
|
||||
|
||||
3. **Eagle.unity** - Strategic game UI
|
||||
- Eagle Canvas
|
||||
- Map Canvas
|
||||
- Command Canvas
|
||||
- Heroes and Battalions UI
|
||||
|
||||
4. **Shardok.unity** - Tactical battle UI
|
||||
- Shardok Canvas
|
||||
- Battle grid
|
||||
- Unit placement UI
|
||||
- Combat UI
|
||||
|
||||
5. **Shared.unity** - Common UI elements
|
||||
- Error Canvas
|
||||
- Popup Canvas
|
||||
- Confirmation dialogs
|
||||
- Overlay canvases
|
||||
|
||||
## Scene Loading Strategy
|
||||
|
||||
```csharp
|
||||
// Example scene loading flow
|
||||
Main.unity (always loaded)
|
||||
↓
|
||||
Connection.unity (additive load)
|
||||
↓ (on game select)
|
||||
Unload Connection.unity
|
||||
Load Eagle.unity + Shared.unity (additive)
|
||||
↓ (on battle start)
|
||||
Load Shardok.unity (additive)
|
||||
↓ (on battle end)
|
||||
Unload Shardok.unity
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Create Scene Structure
|
||||
1. Create new empty scenes
|
||||
2. Set up basic scene loading system
|
||||
3. Test scene transitions work
|
||||
|
||||
### Phase 2: Move Non-Critical Elements
|
||||
1. Start with Shared UI elements (popups, errors)
|
||||
2. These are least likely to break core gameplay
|
||||
3. Test thoroughly
|
||||
|
||||
### Phase 3: Move Connection UI
|
||||
1. Connection/lobby UI is self-contained
|
||||
2. Only needs to communicate via ConnectionHandler
|
||||
3. Natural separation point
|
||||
|
||||
### Phase 4: Separate Eagle and Shardok
|
||||
1. These are already somewhat separated
|
||||
2. Different canvases for each mode
|
||||
3. Can be loaded/unloaded as needed
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Reduced Complexity**: Each scene is manageable size
|
||||
2. **Parallel Development**: Multiple developers can work on different scenes
|
||||
3. **Memory Efficiency**: Only load what's needed
|
||||
4. **Faster Load Times**: Smaller scenes load faster
|
||||
5. **Version Control**: Fewer merge conflicts
|
||||
6. **Easier Debugging**: Isolate issues to specific scenes
|
||||
|
||||
## Safe Migration Process
|
||||
|
||||
### Step 1: Create SceneManager
|
||||
```csharp
|
||||
public class GameSceneManager : MonoBehaviour
|
||||
{
|
||||
public static GameSceneManager Instance;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadSceneAdditive(string sceneName)
|
||||
{
|
||||
await SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
|
||||
}
|
||||
|
||||
public async Task UnloadScene(string sceneName)
|
||||
{
|
||||
await SceneManager.UnloadSceneAsync(sceneName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Test with Empty Scenes First
|
||||
1. Create Main.unity with just SceneManager
|
||||
2. Create empty Connection.unity
|
||||
3. Test loading Connection additively
|
||||
4. Test unloading Connection
|
||||
5. Verify no crashes or issues
|
||||
|
||||
### Step 3: Move One Component at a Time
|
||||
1. Move ErrorCanvas to Shared.unity
|
||||
2. Test it still works
|
||||
3. Move next component
|
||||
4. Repeat until all moved
|
||||
|
||||
## Scene Settings
|
||||
|
||||
Each scene should have:
|
||||
- Appropriate lighting settings
|
||||
- Required cameras (if any)
|
||||
- Event systems (only one active)
|
||||
- Canvas scalers configured
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| References break between scenes | Use ScriptableObjects or singleton patterns |
|
||||
| UI events don't work | Ensure EventSystem is present |
|
||||
| Performance overhead | Profile scene loading, use async |
|
||||
| Missing dependencies | Document all cross-scene dependencies |
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Main.unity loads without errors
|
||||
- [ ] Can load Connection.unity additively
|
||||
- [ ] Can transition from Connection to Eagle
|
||||
- [ ] Can start battle (load Shardok)
|
||||
- [ ] Can end battle (unload Shardok)
|
||||
- [ ] All UI elements visible and functional
|
||||
- [ ] No memory leaks from scene loading/unloading
|
||||
- [ ] Build succeeds with multi-scene setup
|
||||
|
||||
This approach is MUCH safer than trying to extract prefabs from a massive scene file!
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e0e3b2b2183b4e50bdb154c5a3c9b29
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -11151,29 +11151,13 @@ GameObject:
|
||||
- component: {fileID: 1124833191}
|
||||
- component: {fileID: 1124833190}
|
||||
- component: {fileID: 1124833189}
|
||||
- component: {fileID: 1124833188}
|
||||
m_Layer: 5
|
||||
m_Name: ConnectionCanvas
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
--- !u!114 &1124833188
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1124833187}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5f2c53e67d8043d492e8d3277c6e5794, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::ConnectionSceneReadiness
|
||||
productionReady: 0
|
||||
incompleteReason: Connection scene was copied from Gameplay but still needs cross-scene
|
||||
reference migration before it can replace Gameplay.
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1124833189
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -56483,7 +56483,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 0.392}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 0.5686275}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -57318,7 +57318,7 @@ GameObject:
|
||||
- component: {fileID: 424602579}
|
||||
- component: {fileID: 424602578}
|
||||
m_Layer: 0
|
||||
m_Name: Main Gameplay
|
||||
m_Name: Eagle Play Area
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b736cb7c83db6417cabec00d0bba9b88
|
||||
guid: 460d684f2d3b4e80bc80eb2d682c5fa6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
@@ -212,7 +212,7 @@ Camera:
|
||||
m_Enabled: 1
|
||||
serializedVersion: 2
|
||||
m_ClearFlags: 2
|
||||
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
|
||||
m_BackGroundColor: {r: 0.6471669, g: 0.7315526, b: 0.83823526, a: 0}
|
||||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
@@ -265,10 +265,6 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: cb9eb60c28181495da4ef437c5e787e4, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
useNewSceneSystem: 1
|
||||
useOriginalGameplayScene: 0
|
||||
fallbackToOriginalGameplaySceneWhenSplitSceneIncomplete: 1
|
||||
testSceneLoading: 0
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# Scene Separation Implementation Guide
|
||||
|
||||
## Current Status
|
||||
|
||||
I've created the basic infrastructure for separating the massive Gameplay.unity into multiple scenes:
|
||||
|
||||
### Created Files:
|
||||
1. **Main.unity** - Minimal bootstrap scene with SceneManager
|
||||
2. **Connection.unity** - Empty scene for connection/lobby UI
|
||||
3. **GameSceneManager.cs** - Scene loading/unloading manager
|
||||
4. **SceneLoadTester.cs** - Test script to verify scene loading works
|
||||
|
||||
## Next Steps
|
||||
|
||||
### 1. Test Basic Scene Loading (Do This First!)
|
||||
1. Open Unity
|
||||
2. Open `Assets/Scenes/Main.unity`
|
||||
3. Add the SceneLoadTester component to the SceneManager GameObject (optional, for testing)
|
||||
4. Add both scenes to Build Settings:
|
||||
- File → Build Settings
|
||||
- Add Open Scenes (Main.unity)
|
||||
- Add `Assets/Scenes/Connection.unity`
|
||||
5. Play the scene and check console for any errors
|
||||
|
||||
### 2. If Scene Loading Works:
|
||||
Create the remaining empty scenes:
|
||||
- Eagle.unity
|
||||
- Shardok.unity
|
||||
- Shared.unity
|
||||
|
||||
### 3. Start Moving UI Components
|
||||
|
||||
**Safest Order:**
|
||||
1. **ErrorCanvas** → Shared.unity (least dependencies)
|
||||
2. **PopupCanvas** → Shared.unity
|
||||
3. **ConnectionCanvas + panels** → Connection.unity
|
||||
4. **Eagle UI** → Eagle.unity
|
||||
5. **Shardok UI** → Shardok.unity
|
||||
|
||||
### 4. How to Move Components Safely
|
||||
|
||||
For each component:
|
||||
1. In Gameplay.unity, find the GameObject
|
||||
2. Right-click → Copy
|
||||
3. Open target scene (e.g., Connection.unity)
|
||||
4. Right-click in Hierarchy → Paste
|
||||
5. Save the target scene
|
||||
6. Test that everything still works
|
||||
7. Only then delete from Gameplay.unity
|
||||
|
||||
### 5. Update ConnectionHandler
|
||||
|
||||
Once Connection UI is moved, update ConnectionHandler to work with scene loading:
|
||||
|
||||
```csharp
|
||||
// Instead of:
|
||||
connectionCanvas.gameObject.SetActive(true);
|
||||
|
||||
// Use:
|
||||
await GameSceneManager.Instance.TransitionToConnection();
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Always test after each move** - Don't move multiple components at once
|
||||
2. **Keep Gameplay.unity as backup** - Don't delete it until everything works
|
||||
3. **Check references** - Some scripts may reference GameObjects across scenes
|
||||
4. **One EventSystem** - Ensure only one EventSystem is active across all loaded scenes
|
||||
|
||||
## Benefits Once Complete
|
||||
|
||||
- Faster Unity startup (smaller scenes)
|
||||
- Easier collaboration (less merge conflicts)
|
||||
- Better performance (load only what's needed)
|
||||
- Cleaner organization
|
||||
- Easier to debug specific UI sections
|
||||
|
||||
## If Something Goes Wrong
|
||||
|
||||
1. Check Build Settings - all scenes must be added
|
||||
2. Check for missing references in Inspector
|
||||
3. Ensure EventSystem exists in one of the loaded scenes
|
||||
4. Check console for specific error messages
|
||||
5. Can always revert to using original Gameplay.unity
|
||||
|
||||
This approach is MUCH safer than trying to edit the 7.5MB scene file directly!
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a28b46a2ab5484cb5a6ea85229d2885d
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -11,8 +11,8 @@ public class SettingsKeyListener : MonoBehaviour {
|
||||
void Start() { settingsPanelController.ApplyPersistedSettings(); }
|
||||
|
||||
void Update() {
|
||||
if (Input.GetButtonUp("Cancel")) { settingsPanelController.HandleEscape(); }
|
||||
if (GameInput.CancelReleased()) { settingsPanelController.HandleEscape(); }
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.F11)) { settingsPanelController.HandleF11(); }
|
||||
if (GameInput.F11Pressed()) { settingsPanelController.HandleF11(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,64 +70,77 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
/// controls. Called early from SettingsKeyListener.Start() so settings take effect even while
|
||||
/// the Settings Panel prefab is inactive.
|
||||
/// </summary>
|
||||
private bool _overridesMissing;
|
||||
|
||||
public void ApplyPersistedSettings() {
|
||||
// Diagnostic: detect prefab overrides failing to apply on Windows builds.
|
||||
// Log which fields are null and bail out so the game can still launch.
|
||||
string missing = "";
|
||||
if (soundEffectsSource == null) missing += " soundEffectsSource";
|
||||
if (musicSource == null) missing += " musicSource";
|
||||
if (hoveringTooltip == null) missing += " hoveringTooltip";
|
||||
if (provinceLabelsController == null) missing += " provinceLabelsController";
|
||||
if (missing.Length > 0) {
|
||||
UnityEngine.Debug.LogError(
|
||||
"ApplyPersistedSettings: null Inspector fields (prefab overrides missing?):" +
|
||||
missing);
|
||||
_overridesMissing = true;
|
||||
return;
|
||||
}
|
||||
LogMissingRuntimeReferences();
|
||||
|
||||
soundEffectsSource.volume = PlayerPrefs.GetFloat(SoundEffectsVolumeKey, 0.5f);
|
||||
musicSource.volume = PlayerPrefs.GetFloat(MusicVolumeKey, 0.5f);
|
||||
if (soundEffectsSource != null) {
|
||||
soundEffectsSource.volume = PlayerPrefs.GetFloat(SoundEffectsVolumeKey, 0.5f);
|
||||
}
|
||||
if (musicSource != null) {
|
||||
musicSource.volume = PlayerPrefs.GetFloat(MusicVolumeKey, 0.5f);
|
||||
}
|
||||
AmbientVolume = PlayerPrefs.GetFloat(AmbientVolumeKey, 0.5f);
|
||||
|
||||
hoveringTooltip.HugsCursor = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
|
||||
if (hoveringTooltip != null) {
|
||||
hoveringTooltip.HugsCursor = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
|
||||
}
|
||||
|
||||
provinceLabelsController.baseFontSize = PlayerPrefs.GetFloat(ProvinceLabelFontSizeKey, 11f);
|
||||
if (provinceLabelsController != null) {
|
||||
provinceLabelsController.baseFontSize =
|
||||
PlayerPrefs.GetFloat(ProvinceLabelFontSizeKey, 11f);
|
||||
}
|
||||
}
|
||||
|
||||
void Start() {
|
||||
if (_overridesMissing) return;
|
||||
|
||||
// Sync _active with the panel's current state rather than forcing it hidden.
|
||||
// Start() runs the first time the panel becomes active (e.g. via ToggleActive),
|
||||
// so forcing _active=false here would immediately undo the activation.
|
||||
_active = panel.activeSelf;
|
||||
modalBlocker.SetActive(_active);
|
||||
if (modalBlocker != null) { modalBlocker.SetActive(_active); }
|
||||
|
||||
// Add click-to-dismiss on modal blocker as safety net
|
||||
var blockerButton = modalBlocker.GetComponent<Button>();
|
||||
if (blockerButton == null) { blockerButton = modalBlocker.AddComponent<Button>(); }
|
||||
blockerButton.onClick.AddListener(DismissButtonClicked);
|
||||
if (modalBlocker != null) {
|
||||
var blockerButton = modalBlocker.GetComponent<Button>();
|
||||
if (blockerButton == null) { blockerButton = modalBlocker.AddComponent<Button>(); }
|
||||
blockerButton.onClick.AddListener(DismissButtonClicked);
|
||||
}
|
||||
|
||||
// Sync UI controls to current values (ApplyPersistedSettings already applied
|
||||
// the actual settings at load time via SettingsKeyListener.Start)
|
||||
soundEffectsSlider.value = soundEffectsSource.volume;
|
||||
musicSlider.value = musicSource.volume;
|
||||
if (soundEffectsSlider != null) {
|
||||
soundEffectsSlider.value = soundEffectsSource != null
|
||||
? soundEffectsSource.volume
|
||||
: PlayerPrefs.GetFloat(SoundEffectsVolumeKey, 0.5f);
|
||||
}
|
||||
if (musicSlider != null) {
|
||||
musicSlider.value = musicSource != null ? musicSource.volume
|
||||
: PlayerPrefs.GetFloat(MusicVolumeKey, 0.5f);
|
||||
}
|
||||
ambientSlider.value = AmbientVolume;
|
||||
|
||||
tooltipHugsCursorToggle.isOn = hoveringTooltip.HugsCursor;
|
||||
if (tooltipHugsCursorToggle != null) {
|
||||
tooltipHugsCursorToggle.isOn = hoveringTooltip != null && hoveringTooltip.HugsCursor;
|
||||
}
|
||||
|
||||
showAnimationTestsToggle.gameObject.SetActive(Application.isEditor);
|
||||
showAnimationTestsToggle.isOn = PlayerPrefs.GetInt(AnimationToggleKey, 0) == 1;
|
||||
animationTestController.gameObject.SetActive(showAnimationTestsToggle.isOn);
|
||||
if (showAnimationTestsToggle != null) {
|
||||
bool showAnimationTests = Application.isEditor && animationTestController != null;
|
||||
showAnimationTestsToggle.gameObject.SetActive(showAnimationTests);
|
||||
showAnimationTestsToggle.isOn =
|
||||
showAnimationTests && PlayerPrefs.GetInt(AnimationToggleKey, 0) == 1;
|
||||
if (animationTestController != null) {
|
||||
animationTestController.gameObject.SetActive(showAnimationTestsToggle.isOn);
|
||||
}
|
||||
}
|
||||
|
||||
autoScrollSpeedSlider.value = AutoScrollingText.GlobalScrollSpeedMultiplier;
|
||||
UpdateAutoScrollSpeedLabel(autoScrollSpeedSlider.value);
|
||||
|
||||
provinceLabelFontSizeSlider.value = provinceLabelsController.baseFontSize;
|
||||
UpdateProvinceLabelFontSizeLabel(provinceLabelsController.baseFontSize);
|
||||
float provinceLabelFontSize = provinceLabelsController != null
|
||||
? provinceLabelsController.baseFontSize
|
||||
: PlayerPrefs.GetFloat(ProvinceLabelFontSizeKey, 11f);
|
||||
provinceLabelFontSizeSlider.value = provinceLabelFontSize;
|
||||
provinceLabelFontSizeSlider.interactable = provinceLabelsController != null;
|
||||
UpdateProvinceLabelFontSizeLabel(provinceLabelFontSize);
|
||||
|
||||
fullscreenToggle.isOn = Screen.fullScreen;
|
||||
|
||||
@@ -148,14 +161,12 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
}
|
||||
|
||||
public void HandleEscape() {
|
||||
if (_overridesMissing) return;
|
||||
|
||||
// Close attributions panel if open
|
||||
if (attributionsController.panel.activeSelf) {
|
||||
if (attributionsController != null && attributionsController.panel.activeSelf) {
|
||||
attributionsController.Hide();
|
||||
}
|
||||
// Don't toggle settings if bug report panel is handling escape
|
||||
else if (!bugReportPanel.panel.activeSelf) {
|
||||
else if (bugReportPanel == null || !bugReportPanel.panel.activeSelf) {
|
||||
ToggleActive();
|
||||
}
|
||||
}
|
||||
@@ -165,7 +176,7 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
private void ToggleActive() {
|
||||
_active = !_active;
|
||||
panel.SetActive(_active);
|
||||
modalBlocker.SetActive(_active);
|
||||
if (modalBlocker != null) { modalBlocker.SetActive(_active); }
|
||||
}
|
||||
|
||||
public void SettingsButtonClicked() { ToggleActive(); }
|
||||
@@ -177,18 +188,18 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
public void DismissButtonClicked() {
|
||||
_active = false;
|
||||
panel.SetActive(_active);
|
||||
modalBlocker.SetActive(_active);
|
||||
if (modalBlocker != null) { modalBlocker.SetActive(_active); }
|
||||
}
|
||||
|
||||
private static float VolumeFunc(float val) { return val * val; }
|
||||
|
||||
public void OnSoundEffectSliderChange(float val) {
|
||||
soundEffectsSource.volume = VolumeFunc(val);
|
||||
if (soundEffectsSource != null) { soundEffectsSource.volume = VolumeFunc(val); }
|
||||
PlayerPrefs.SetFloat(SoundEffectsVolumeKey, val);
|
||||
}
|
||||
|
||||
public void OnMusicSliderChange(float val) {
|
||||
musicSource.volume = VolumeFunc(val);
|
||||
if (musicSource != null) { musicSource.volume = VolumeFunc(val); }
|
||||
PlayerPrefs.SetFloat(MusicVolumeKey, val);
|
||||
}
|
||||
|
||||
@@ -229,12 +240,12 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
|
||||
public void OnCursorHugToggleChange(bool val) {
|
||||
PlayerPrefs.SetInt(TooltipHugsCursorKey, val ? 1 : 0);
|
||||
hoveringTooltip.HugsCursor = val;
|
||||
if (hoveringTooltip != null) { hoveringTooltip.HugsCursor = val; }
|
||||
}
|
||||
|
||||
public void OnAnimationTestToggleChange(bool val) {
|
||||
PlayerPrefs.SetInt(AnimationToggleKey, val ? 1 : 0);
|
||||
animationTestController.gameObject.SetActive(val);
|
||||
if (animationTestController != null) { animationTestController.gameObject.SetActive(val); }
|
||||
}
|
||||
|
||||
public void OnAutoScrollSpeedSliderChange(float val) {
|
||||
@@ -251,9 +262,11 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
}
|
||||
|
||||
public void OnProvinceLabelFontSizeSliderChange(float val) {
|
||||
provinceLabelsController.baseFontSize = val;
|
||||
provinceLabelsController.RefreshLabels();
|
||||
PlayerPrefs.SetFloat(ProvinceLabelFontSizeKey, val);
|
||||
if (provinceLabelsController != null) {
|
||||
provinceLabelsController.baseFontSize = val;
|
||||
provinceLabelsController.RefreshLabels();
|
||||
}
|
||||
UpdateProvinceLabelFontSizeLabel(val);
|
||||
}
|
||||
|
||||
@@ -271,6 +284,8 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
}
|
||||
|
||||
public void OnReportBugClick() {
|
||||
if (bugReportPanel == null) { return; }
|
||||
|
||||
// Configure the bug report panel with game state providers
|
||||
bugReportPanel.Configure(
|
||||
gameModelProvider: () => eagleGameController?.GameModel,
|
||||
@@ -280,12 +295,24 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
bugReportPanel.Show();
|
||||
}
|
||||
|
||||
private void HandleLobby() {
|
||||
eagleGameController.StopAll();
|
||||
eagleGameController.gameObject.SetActive(false);
|
||||
ShardokContainer.SetActive(false);
|
||||
connectionHandler.gameObject.SetActive(true);
|
||||
// connectionHandler.StartListeningForLobbyUpdates();
|
||||
private async void HandleLobby() {
|
||||
if (eagleGameController != null) { eagleGameController.StopAll(); }
|
||||
|
||||
var sceneManager = GameSceneManager.Instance;
|
||||
if (sceneManager == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Cannot return to lobby because GameSceneManager is missing.");
|
||||
}
|
||||
|
||||
await sceneManager.TransitionToConnection();
|
||||
|
||||
connectionHandler = sceneManager.GetConnectionHandler();
|
||||
if (connectionHandler == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Cannot return to lobby because Connection scene did not provide a " +
|
||||
"ConnectionHandler.");
|
||||
}
|
||||
|
||||
connectionHandler.ConnectEagle();
|
||||
ClientTextProvider.Provider.Clear();
|
||||
|
||||
@@ -294,6 +321,23 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
modalBlocker.SetActive(_active);
|
||||
}
|
||||
|
||||
private void LogMissingRuntimeReferences() {
|
||||
string missing = "";
|
||||
if (soundEffectsSource == null) missing += " soundEffectsSource";
|
||||
if (musicSource == null) missing += " musicSource";
|
||||
|
||||
var eagleSceneReferencesExpected = eagleGameController != null;
|
||||
if (eagleSceneReferencesExpected) {
|
||||
if (hoveringTooltip == null) missing += " hoveringTooltip";
|
||||
if (provinceLabelsController == null) missing += " provinceLabelsController";
|
||||
}
|
||||
|
||||
if (missing.Length == 0) { return; }
|
||||
|
||||
UnityEngine.Debug.LogWarning(
|
||||
"Settings runtime references are not fully wired yet:" + missing);
|
||||
}
|
||||
|
||||
private void HandleQuit() {
|
||||
ConnectionKiller.ShutdownAll();
|
||||
#if UNITY_EDITOR
|
||||
|
||||
@@ -82,7 +82,9 @@ public class HexGrid : MonoBehaviour {
|
||||
public Color backgroundColor = new(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.35f);
|
||||
|
||||
private Texture2D _softCircleTexture;
|
||||
private RectTransform _foregroundCanvasRoot;
|
||||
private RectTransform _backingsContainer; // Renders behind foreground elements
|
||||
private RectTransform _foregroundContainer;
|
||||
|
||||
readonly List<OverlayCellInfo> overlayCells = new();
|
||||
readonly HashSet<int> _bgHighlightedCells = new();
|
||||
@@ -93,13 +95,23 @@ public class HexGrid : MonoBehaviour {
|
||||
static readonly float BaseBorderWidth = 0.01f;
|
||||
|
||||
private const float OverlayBorderMaxWidth = 0.2f;
|
||||
private const string HexMeshSortingLayer = "UI Base Overlay";
|
||||
private const int HexBaseSortingOrder = 100;
|
||||
private const int HexOverlaySortingOrder = 101;
|
||||
private const int TerrainModifierEffectSortingOrder = 101;
|
||||
private const int ForegroundCanvasSortingOrder = 102;
|
||||
private const string ForegroundGraphicShaderName = "Shardok/Foreground UI Overlay";
|
||||
private const string ForegroundTextShaderName = "TextMeshPro/Distance Field Overlay";
|
||||
|
||||
HexMetrics metrics;
|
||||
Cell[] cells;
|
||||
private Material _foregroundGraphicMaterial;
|
||||
private Material _foregroundTextMaterial;
|
||||
|
||||
// Track mapArea size for resolution change detection
|
||||
private Vector2 _lastMapAreaSize;
|
||||
private bool _isSetUp = false;
|
||||
public bool IsSetUp => _isSetUp && metrics != null;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired after the grid is rebuilt due to resolution change.
|
||||
@@ -115,17 +127,7 @@ public class HexGrid : MonoBehaviour {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create backings container as first child of overlayContent so it renders behind
|
||||
if (_backingsContainer == null) {
|
||||
var backingsObj = new GameObject("BackingsContainer");
|
||||
_backingsContainer = backingsObj.AddComponent<RectTransform>();
|
||||
_backingsContainer.SetParent(overlayContent, false);
|
||||
_backingsContainer.anchorMin = Vector2.zero;
|
||||
_backingsContainer.anchorMax = Vector2.one;
|
||||
_backingsContainer.offsetMin = Vector2.zero;
|
||||
_backingsContainer.offsetMax = Vector2.zero;
|
||||
_backingsContainer.SetAsFirstSibling();
|
||||
}
|
||||
EnsureForegroundContainers();
|
||||
|
||||
cells = new Cell[cellCount];
|
||||
|
||||
@@ -137,10 +139,106 @@ public class HexGrid : MonoBehaviour {
|
||||
|
||||
private void SetUpMeshesIfNecessary() {
|
||||
if (metrics != null) { return; }
|
||||
NormalizeOverlayContent();
|
||||
metrics = new HexMetrics(mapArea: mapArea, columns: width, rows: height);
|
||||
hexMesh.SetUp(m: metrics);
|
||||
hexMesh.GetComponent<MeshRenderer>().sortingLayerName = "UI Base Overlay";
|
||||
overlayMesh.SetUp(m: metrics);
|
||||
SetMeshRendererSorting(hexMesh, HexBaseSortingOrder);
|
||||
SetMeshRendererSorting(overlayMesh, HexOverlaySortingOrder);
|
||||
}
|
||||
|
||||
private void NormalizeOverlayContent() {
|
||||
overlayContent.SetAsLastSibling();
|
||||
|
||||
var overlayCanvas = overlayContent.GetComponent<Canvas>();
|
||||
if (overlayCanvas != null) { overlayCanvas.enabled = false; }
|
||||
|
||||
var overlayRaycaster = overlayContent.GetComponent<GraphicRaycaster>();
|
||||
if (overlayRaycaster != null) { overlayRaycaster.enabled = false; }
|
||||
}
|
||||
|
||||
private Transform ForegroundContentParent() { return _foregroundContainer; }
|
||||
|
||||
private static void IgnoreRaycasts(Graphic graphic) { graphic.raycastTarget = false; }
|
||||
|
||||
private Material ForegroundGraphicMaterial() {
|
||||
if (_foregroundGraphicMaterial != null) { return _foregroundGraphicMaterial; }
|
||||
|
||||
Shader shader = Shader.Find(ForegroundGraphicShaderName);
|
||||
if (shader == null) {
|
||||
throw new InvalidOperationException(
|
||||
message: $"Could not find required shader {ForegroundGraphicShaderName}");
|
||||
}
|
||||
|
||||
_foregroundGraphicMaterial = new Material(shader) {
|
||||
name = "Shardok Foreground UI Overlay",
|
||||
hideFlags = HideFlags.HideAndDontSave,
|
||||
};
|
||||
return _foregroundGraphicMaterial;
|
||||
}
|
||||
|
||||
private Material ForegroundTextMaterial() {
|
||||
if (_foregroundTextMaterial != null) { return _foregroundTextMaterial; }
|
||||
|
||||
Shader shader = Shader.Find(ForegroundTextShaderName);
|
||||
if (shader == null) {
|
||||
throw new InvalidOperationException(
|
||||
message: $"Could not find required shader {ForegroundTextShaderName}");
|
||||
}
|
||||
|
||||
_foregroundTextMaterial = new Material(mainFont.material) {
|
||||
shader = shader,
|
||||
name = "Shardok Foreground TMP Overlay",
|
||||
hideFlags = HideFlags.HideAndDontSave,
|
||||
};
|
||||
return _foregroundTextMaterial;
|
||||
}
|
||||
|
||||
private void UseForegroundMaterial(Graphic graphic) {
|
||||
graphic.material = ForegroundGraphicMaterial();
|
||||
}
|
||||
|
||||
private void UseForegroundTextMaterial(TMP_Text label) {
|
||||
label.fontSharedMaterial = ForegroundTextMaterial();
|
||||
}
|
||||
|
||||
private static void SetMeshRendererSorting(HexMesh mesh, int sortingOrder) {
|
||||
var meshRenderer = mesh.GetComponent<MeshRenderer>();
|
||||
meshRenderer.sortingLayerName = HexMeshSortingLayer;
|
||||
meshRenderer.sortingOrder = sortingOrder;
|
||||
}
|
||||
|
||||
private void EnsureForegroundContainers() {
|
||||
if (_foregroundCanvasRoot != null) { return; }
|
||||
|
||||
var foregroundRootObj = new GameObject("ForegroundCanvas");
|
||||
_foregroundCanvasRoot = foregroundRootObj.AddComponent<RectTransform>();
|
||||
_foregroundCanvasRoot.SetParent(gridTransform, worldPositionStays: false);
|
||||
StretchToParent(_foregroundCanvasRoot);
|
||||
|
||||
var foregroundCanvas = foregroundRootObj.AddComponent<Canvas>();
|
||||
foregroundCanvas.overrideSorting = true;
|
||||
foregroundCanvas.sortingLayerName = HexMeshSortingLayer;
|
||||
foregroundCanvas.sortingOrder = ForegroundCanvasSortingOrder;
|
||||
|
||||
var backingsObj = new GameObject("BackingsContainer");
|
||||
_backingsContainer = backingsObj.AddComponent<RectTransform>();
|
||||
_backingsContainer.SetParent(_foregroundCanvasRoot, worldPositionStays: false);
|
||||
StretchToParent(_backingsContainer);
|
||||
_backingsContainer.SetAsFirstSibling();
|
||||
|
||||
var foregroundObj = new GameObject("ForegroundContent");
|
||||
_foregroundContainer = foregroundObj.AddComponent<RectTransform>();
|
||||
_foregroundContainer.SetParent(_foregroundCanvasRoot, worldPositionStays: false);
|
||||
StretchToParent(_foregroundContainer);
|
||||
_foregroundContainer.SetAsLastSibling();
|
||||
}
|
||||
|
||||
private static void StretchToParent(RectTransform rectTransform) {
|
||||
rectTransform.anchorMin = Vector2.zero;
|
||||
rectTransform.anchorMax = Vector2.one;
|
||||
rectTransform.offsetMin = Vector2.zero;
|
||||
rectTransform.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private Texture2D GetOrCreateSoftCircleTexture() {
|
||||
@@ -240,10 +338,12 @@ public class HexGrid : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy backings container (will be recreated)
|
||||
if (_backingsContainer != null) {
|
||||
Destroy(_backingsContainer.gameObject);
|
||||
// Destroy foreground containers (will be recreated)
|
||||
if (_foregroundCanvasRoot != null) {
|
||||
Destroy(_foregroundCanvasRoot.gameObject);
|
||||
_foregroundCanvasRoot = null;
|
||||
_backingsContainer = null;
|
||||
_foregroundContainer = null;
|
||||
}
|
||||
|
||||
// Clear cells and metrics to force recreation
|
||||
@@ -437,11 +537,13 @@ public class HexGrid : MonoBehaviour {
|
||||
parent: gridTransform,
|
||||
worldPositionStays: false);
|
||||
newImage.gameObject.name = $"Terrain{coordSuffix}";
|
||||
IgnoreRaycasts(newImage);
|
||||
newImage.color = Color.white;
|
||||
newImage.rectTransform.anchoredPosition =
|
||||
new Vector2(x: position.x, y: position.z + metrics.OuterRadius / 2);
|
||||
newImage.rectTransform.sizeDelta =
|
||||
new Vector2(x: 2 * metrics.InnerRadius, y: 3 * metrics.OuterRadius);
|
||||
newImage.rectTransform.SetAsFirstSibling();
|
||||
newImage.gameObject.SetActive(value: false);
|
||||
|
||||
return newImage;
|
||||
@@ -490,9 +592,11 @@ public class HexGrid : MonoBehaviour {
|
||||
foreach (int column in new[] { -1, 1, -3, 3 }) {
|
||||
RawImage newImage = Instantiate(
|
||||
original: followUpImagePrefab,
|
||||
parent: overlayContent,
|
||||
parent: ForegroundContentParent(),
|
||||
worldPositionStays: false);
|
||||
newImage.gameObject.name = $"SecondaryImage{coordSuffix}_{imageIndex}";
|
||||
IgnoreRaycasts(newImage);
|
||||
UseForegroundMaterial(newImage);
|
||||
newImage.rectTransform.anchoredPosition = new Vector2(
|
||||
x: position.x + column * singleWidth * 0.5f,
|
||||
y: position.z + 1 - row * singleHeight);
|
||||
@@ -511,10 +615,13 @@ public class HexGrid : MonoBehaviour {
|
||||
|
||||
RawImage newImage = Instantiate(
|
||||
original: backgroundImagePrefab,
|
||||
parent: overlayContent,
|
||||
parent: ForegroundContentParent(),
|
||||
worldPositionStays: false);
|
||||
newImage.gameObject.name = $"TerrainModifier{coordSuffix}";
|
||||
newImage.rectTransform.SetParent(parent: overlayContent, worldPositionStays: false);
|
||||
IgnoreRaycasts(newImage);
|
||||
newImage.rectTransform.SetParent(
|
||||
parent: ForegroundContentParent(),
|
||||
worldPositionStays: false);
|
||||
newImage.rectTransform.anchoredPosition = new Vector2(x: position.x, y: position.z);
|
||||
newImage.rectTransform.sizeDelta = new Vector2(x: squareSize, y: squareSize);
|
||||
newImage.gameObject.SetActive(value: false);
|
||||
@@ -535,6 +642,8 @@ public class HexGrid : MonoBehaviour {
|
||||
parent: _backingsContainer,
|
||||
worldPositionStays: false);
|
||||
backing.gameObject.name = $"UnitTypeBacking{coordSuffix}";
|
||||
IgnoreRaycasts(backing);
|
||||
UseForegroundMaterial(backing);
|
||||
backing.texture = GetOrCreateSoftCircleTexture();
|
||||
backing.color = imageBackingColor;
|
||||
backing.rectTransform.anchoredPosition = anchoredPos;
|
||||
@@ -544,9 +653,11 @@ public class HexGrid : MonoBehaviour {
|
||||
// Create main image
|
||||
RawImage newImage = Instantiate(
|
||||
original: followUpImagePrefab,
|
||||
parent: overlayContent,
|
||||
parent: ForegroundContentParent(),
|
||||
worldPositionStays: false);
|
||||
newImage.gameObject.name = $"UnitType{coordSuffix}";
|
||||
IgnoreRaycasts(newImage);
|
||||
UseForegroundMaterial(newImage);
|
||||
newImage.rectTransform.anchoredPosition = anchoredPos;
|
||||
newImage.rectTransform.sizeDelta = new Vector2(x: size, y: size);
|
||||
newImage.gameObject.SetActive(value: false);
|
||||
@@ -567,6 +678,8 @@ public class HexGrid : MonoBehaviour {
|
||||
parent: _backingsContainer,
|
||||
worldPositionStays: false);
|
||||
backing.gameObject.name = $"ProfessionBacking{coordSuffix}";
|
||||
IgnoreRaycasts(backing);
|
||||
UseForegroundMaterial(backing);
|
||||
backing.texture = GetOrCreateSoftCircleTexture();
|
||||
backing.color = imageBackingColor;
|
||||
backing.rectTransform.anchoredPosition = anchoredPos;
|
||||
@@ -576,9 +689,11 @@ public class HexGrid : MonoBehaviour {
|
||||
// Create main image
|
||||
RawImage newImage = Instantiate(
|
||||
original: followUpImagePrefab,
|
||||
parent: overlayContent,
|
||||
parent: ForegroundContentParent(),
|
||||
worldPositionStays: false);
|
||||
newImage.gameObject.name = $"Profession{coordSuffix}";
|
||||
IgnoreRaycasts(newImage);
|
||||
UseForegroundMaterial(newImage);
|
||||
newImage.rectTransform.anchoredPosition = anchoredPos;
|
||||
newImage.rectTransform.sizeDelta = new Vector2(x: size, y: size);
|
||||
newImage.gameObject.SetActive(value: false);
|
||||
@@ -598,6 +713,8 @@ public class HexGrid : MonoBehaviour {
|
||||
parent: _backingsContainer,
|
||||
worldPositionStays: false);
|
||||
backing.gameObject.name = backingName;
|
||||
IgnoreRaycasts(backing);
|
||||
UseForegroundMaterial(backing);
|
||||
backing.texture = GetOrCreateSoftCircleTexture();
|
||||
backing.color = labelBackingColor;
|
||||
backing.rectTransform.anchoredPosition = anchoredPos;
|
||||
@@ -608,9 +725,10 @@ public class HexGrid : MonoBehaviour {
|
||||
// Create text label
|
||||
TMP_Text label = Instantiate(
|
||||
original: cellLabelPrefab,
|
||||
parent: overlayContent,
|
||||
parent: ForegroundContentParent(),
|
||||
worldPositionStays: false);
|
||||
label.gameObject.name = name;
|
||||
IgnoreRaycasts(label);
|
||||
// None of the hex-grid labels (hero names, battalion sizes, odds %, action points,
|
||||
// coord debug strings) need rich-text tags. Disabling richText skips TMP's per-update
|
||||
// tag-scanning pass and makes the labels robust against accidental '<' characters in
|
||||
@@ -621,6 +739,7 @@ public class HexGrid : MonoBehaviour {
|
||||
label.rectTransform.anchoredPosition = anchoredPos;
|
||||
label.rectTransform.sizeDelta = labelSize;
|
||||
label.font = mainFont;
|
||||
UseForegroundTextMaterial(label);
|
||||
label.fontSizeMin = 10;
|
||||
label.fontSize = 300;
|
||||
label.fontSizeMax = 300;
|
||||
@@ -673,6 +792,18 @@ public class HexGrid : MonoBehaviour {
|
||||
return GridIndexForPoint(worldPosition);
|
||||
}
|
||||
|
||||
public bool ContainsMapVisual(GameObject gameObject) {
|
||||
if (gameObject == null) { return false; }
|
||||
var visualTransform = gameObject.transform;
|
||||
return IsSameOrChild(visualTransform, gridTransform) ||
|
||||
IsSameOrChild(visualTransform, overlayContent) ||
|
||||
IsSameOrChild(visualTransform, mapArea);
|
||||
}
|
||||
|
||||
private static bool IsSameOrChild(Transform transform, Transform parent) {
|
||||
return transform == parent || transform.IsChildOf(parent);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
@@ -822,6 +953,9 @@ public class HexGrid : MonoBehaviour {
|
||||
newEffect.transform.localPosition =
|
||||
new Vector3(x: imagePosition.x, y: imagePosition.y - h / 4.0f, z: 0);
|
||||
newEffect.transform.localScale = new Vector3(x: 0.35f, y: 0.45f, z: 0.35f);
|
||||
var effectRenderer = newEffect.GetComponent<ParticleSystemRenderer>();
|
||||
effectRenderer.sortingLayerName = HexMeshSortingLayer;
|
||||
effectRenderer.sortingOrder = TerrainModifierEffectSortingOrder;
|
||||
newEffect.gameObject.SetActive(value: true);
|
||||
cells[cellIndex].TerrainModifierEffect = newEffect;
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4b2b2bb94e5a471e86597b8838642c9
|
||||
guid: 3527cb6d31f54c3faf9b729605a92664
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
Shader "Shardok/Foreground UI Overlay" {
|
||||
Properties {
|
||||
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
|
||||
_Color ("Tint", Color) = (1,1,1,1)
|
||||
_StencilComp ("Stencil Comparison", Float) = 8
|
||||
_Stencil ("Stencil ID", Float) = 0
|
||||
_StencilOp ("Stencil Operation", Float) = 0
|
||||
_StencilWriteMask ("Stencil Write Mask", Float) = 255
|
||||
_StencilReadMask ("Stencil Read Mask", Float) = 255
|
||||
_ColorMask ("Color Mask", Float) = 15
|
||||
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
|
||||
}
|
||||
|
||||
SubShader {
|
||||
Tags {
|
||||
"Queue" = "Overlay"
|
||||
"IgnoreProjector" = "True"
|
||||
"RenderType" = "Transparent"
|
||||
"PreviewType" = "Plane"
|
||||
"CanUseSpriteAtlas" = "True"
|
||||
}
|
||||
|
||||
Stencil {
|
||||
Ref [_Stencil]
|
||||
Comp [_StencilComp]
|
||||
Pass [_StencilOp]
|
||||
ReadMask [_StencilReadMask]
|
||||
WriteMask [_StencilWriteMask]
|
||||
}
|
||||
|
||||
Cull Off
|
||||
Lighting Off
|
||||
ZWrite Off
|
||||
ZTest Always
|
||||
Blend SrcAlpha OneMinusSrcAlpha
|
||||
ColorMask [_ColorMask]
|
||||
|
||||
Pass {
|
||||
Name "Default"
|
||||
CGPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
#pragma target 2.0
|
||||
|
||||
#include "UnityCG.cginc"
|
||||
#include "UnityUI.cginc"
|
||||
|
||||
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
|
||||
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
|
||||
|
||||
struct appdata_t {
|
||||
float4 vertex : POSITION;
|
||||
float4 color : COLOR;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
UNITY_VERTEX_INPUT_INSTANCE_ID
|
||||
};
|
||||
|
||||
struct v2f {
|
||||
float4 vertex : SV_POSITION;
|
||||
fixed4 color : COLOR;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
float4 worldPosition : TEXCOORD1;
|
||||
UNITY_VERTEX_OUTPUT_STEREO
|
||||
};
|
||||
|
||||
sampler2D _MainTex;
|
||||
fixed4 _Color;
|
||||
fixed4 _TextureSampleAdd;
|
||||
float4 _ClipRect;
|
||||
float4 _MainTex_ST;
|
||||
|
||||
v2f vert(appdata_t v) {
|
||||
v2f OUT;
|
||||
UNITY_SETUP_INSTANCE_ID(v);
|
||||
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
|
||||
OUT.worldPosition = v.vertex;
|
||||
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
|
||||
OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
|
||||
OUT.color = v.color * _Color;
|
||||
return OUT;
|
||||
}
|
||||
|
||||
fixed4 frag(v2f IN) : SV_Target {
|
||||
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
|
||||
|
||||
#ifdef UNITY_UI_CLIP_RECT
|
||||
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
|
||||
#endif
|
||||
|
||||
#ifdef UNITY_UI_ALPHACLIP
|
||||
clip(color.a - 0.001);
|
||||
#endif
|
||||
|
||||
return color;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ccd58ed8b9624c54a8bbd9554a4a8777
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+43
-45
@@ -90,6 +90,7 @@ namespace Shardok {
|
||||
public class ShardokGameController : MonoBehaviour, IClickReceiver {
|
||||
public HexGrid hexGrid;
|
||||
public HexGrid HexGrid => hexGrid;
|
||||
public bool CanReceiveGridInput => hexGrid.IsSetUp && Model?.Map != null;
|
||||
public Camera mainCamera;
|
||||
public UnitInfoPanelController unitInfoPanel;
|
||||
public EventBasedTable armiesTable;
|
||||
@@ -359,13 +360,12 @@ namespace Shardok {
|
||||
_setupGameGeneration == setupGameGeneration;
|
||||
}
|
||||
|
||||
public void EndTurn() {
|
||||
public async void EndTurn() {
|
||||
endTurnButton.interactable = false;
|
||||
var gameOver = Model.CanExitBattle;
|
||||
if (gameOver) {
|
||||
PersistentUIManager.Instance?.connectionStatusUI?.SetShardokGameStateProvider(null);
|
||||
ClearActiveModel();
|
||||
eagleCanvas.gameObject.SetActive(true);
|
||||
hexGrid.ClearCellLabels();
|
||||
hexGrid.ClearCellModifierImages();
|
||||
hexGrid.ClearCellModifierEffects();
|
||||
@@ -374,6 +374,33 @@ namespace Shardok {
|
||||
_lastRetrievedHistoryCount = 0;
|
||||
turnHistoryPanel.Clear();
|
||||
this.gameObject.SetActive(false);
|
||||
|
||||
var sceneManager = GameSceneManager.Instance;
|
||||
if (sceneManager == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Cannot exit Shardok battle because GameSceneManager is missing.");
|
||||
}
|
||||
|
||||
if (sceneManager.IsSceneLoaded(sceneManager.eagleSceneName)) {
|
||||
eagleCanvas = sceneManager.GetEagleCanvas();
|
||||
if (eagleCanvas == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Cannot exit Shardok battle because Eagle scene is loaded but " +
|
||||
"does not provide an Eagle canvas.");
|
||||
}
|
||||
|
||||
eagleCanvas.gameObject.SetActive(true);
|
||||
} else {
|
||||
await sceneManager.TransitionToConnection();
|
||||
var connectionHandler = sceneManager.GetConnectionHandler();
|
||||
if (connectionHandler == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Cannot exit custom Shardok battle because Connection scene did " +
|
||||
"not provide a ConnectionHandler.");
|
||||
}
|
||||
|
||||
connectionHandler.ConnectEagle();
|
||||
}
|
||||
} else {
|
||||
hexGrid.ClearOverlays();
|
||||
// Notify tutorial system of turn end
|
||||
@@ -1035,12 +1062,15 @@ namespace Shardok {
|
||||
}
|
||||
|
||||
private bool IsPrimaryOrSecondaryPressActive() {
|
||||
if (Input.GetMouseButton(0) || Input.GetMouseButton(1)) { return true; }
|
||||
if (GameInput.PrimaryPointerPressed() || GameInput.SecondaryPointerPressed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Input.touchCount == 0) { return false; }
|
||||
int touchCount = GameInput.TouchCount();
|
||||
if (touchCount == 0) { return false; }
|
||||
|
||||
for (int i = 0; i < Input.touchCount; i++) {
|
||||
var phase = Input.GetTouch(i).phase;
|
||||
for (int i = 0; i < touchCount; i++) {
|
||||
var phase = GameInput.GetTouch(i).Phase;
|
||||
if (phase != TouchPhase.Ended && phase != TouchPhase.Canceled) { return true; }
|
||||
}
|
||||
|
||||
@@ -1446,54 +1476,22 @@ namespace Shardok {
|
||||
}
|
||||
|
||||
void HandleButton() {
|
||||
/* else if (Input.GetButtonUp("Lightning")) {
|
||||
LightningButtonClicked();
|
||||
}
|
||||
else if (Input.GetButtonUp("Meteor")) {
|
||||
StartMeteorButtonClicked();
|
||||
}
|
||||
else if (Input.GetButtonUp("Move")) {
|
||||
SetDisplayActionsUIState(DisplayActionsUIState.standardState);
|
||||
|
||||
handleOverlaysForSelection();
|
||||
}
|
||||
else if (Input.GetButtonUp("Extinguish")) {
|
||||
ExtinguishButtonClicked();
|
||||
}
|
||||
else if (Input.GetButtonUp("StartFire")) {
|
||||
StartFireButtonClicked();
|
||||
}*/
|
||||
|
||||
// if (Input.GetButton("Horizontal"))
|
||||
//{
|
||||
// float horizontalValue = Input.GetAxis("Horizontal");
|
||||
// Vector3 v = mainCamera.transform.position;
|
||||
// v.x -= horizontalValue;
|
||||
// mainCamera.transform.position = v;
|
||||
// }
|
||||
// if (Input.GetButton("Vertical"))
|
||||
//{
|
||||
// float verticalValue = Input.GetAxis("Vertical");
|
||||
// Vector3 v = mainCamera.transform.position;
|
||||
// v.z -= verticalValue;
|
||||
// mainCamera.transform.position = v;
|
||||
// }
|
||||
if (Input.GetButton("Zoom")) {
|
||||
float zoomValue = Input.GetAxis("Zoom");
|
||||
float zoomValue = GameInput.ShardokZoomAxis();
|
||||
if (!Mathf.Approximately(a: zoomValue, b: 0f)) {
|
||||
var transform1 = mainCamera.transform;
|
||||
Vector3 v = transform1.position;
|
||||
v.y += zoomValue;
|
||||
transform1.position = v;
|
||||
}
|
||||
if (Input.GetButtonDown("Hex Coords")) { SetCoordsLabels(); }
|
||||
if (Input.GetButtonUp("Hex Coords")) { SetHeroLabels(); }
|
||||
if (GameInput.HexCoordsPressed()) { SetCoordsLabels(); }
|
||||
if (GameInput.HexCoordsReleased()) { SetHeroLabels(); }
|
||||
|
||||
if (Input.GetButtonDown("Profession Attack")) {
|
||||
if (GameInput.ProfessionAttackPressed()) {
|
||||
var prev = _displayedCommandGroup;
|
||||
SetDisplayedCommandGroup(CommandTypeUIManager.ProfessionAttackGroup);
|
||||
_previouslyDisplayedCommandGroup = prev;
|
||||
}
|
||||
if (Input.GetButtonUp("Profession Attack")) {
|
||||
if (GameInput.ProfessionAttackReleased()) {
|
||||
SetDisplayedCommandGroup(_previouslyDisplayedCommandGroup ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -1809,7 +1807,7 @@ namespace Shardok {
|
||||
|
||||
// Position tooltip to the right of the cursor (pivot is centered,
|
||||
// tooltip is 130 units wide, so offset by ~half-width + padding)
|
||||
var cursorScreenPos = (Vector2)Input.mousePosition + new Vector2(160, -15);
|
||||
var cursorScreenPos = GameInput.PointerPosition() + new Vector2(160, -15);
|
||||
RectTransformUtility.ScreenPointToWorldPointInRectangle(
|
||||
_canvasRect,
|
||||
cursorScreenPos,
|
||||
|
||||
@@ -5,6 +5,7 @@ using Net.Eagle0.Shardok.Common;
|
||||
using UnityEngine;
|
||||
|
||||
public class SoundManager : MonoBehaviour {
|
||||
public AudioSource soundEffectsSource;
|
||||
public AudioSource musicSource;
|
||||
|
||||
private class MusicTypeInfo {
|
||||
@@ -239,7 +240,7 @@ public class SoundManager : MonoBehaviour {
|
||||
};
|
||||
}
|
||||
|
||||
public void Start() {
|
||||
public void Awake() {
|
||||
_musicInfoForType = new Dictionary<MusicType, MusicTypeInfo> {
|
||||
{ MusicType.MusicNone, null },
|
||||
{ MusicType.MusicSpring, _springMusic },
|
||||
@@ -250,7 +251,9 @@ public class SoundManager : MonoBehaviour {
|
||||
{ MusicType.MusicBattle, _battleMusic },
|
||||
{ MusicType.MusicVictory, _victoryMusic }
|
||||
};
|
||||
}
|
||||
|
||||
public void Start() {
|
||||
StartCoroutine(LoadAllMusic());
|
||||
StartCoroutine(LoadSoundEffects());
|
||||
}
|
||||
|
||||
+13536
-20
File diff suppressed because one or more lines are too long
@@ -11,20 +11,20 @@ public class TouchHandler : MonoBehaviour {
|
||||
const float LONG_TOUCH_SECONDS = 2.0f;
|
||||
|
||||
// Use this for initialization
|
||||
void Start() { Input.simulateMouseWithTouches = false; }
|
||||
void Start() { GameInput.DisableMouseSimulationWithTouches(); }
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {
|
||||
if (gameController.hexGrid == null) { return; }
|
||||
|
||||
int numFingersDown = Input.touchCount;
|
||||
int numFingersDown = GameInput.TouchCount();
|
||||
|
||||
if (numFingersDown > 0) {
|
||||
Touch firstFingerTouch = Input.GetTouch(0);
|
||||
var firstFingerTouch = GameInput.GetTouch(0);
|
||||
|
||||
if (EventSystem.current.IsPointerOverGameObject(firstFingerTouch.fingerId)) { return; }
|
||||
if (EventSystem.current.IsPointerOverGameObject(firstFingerTouch.FingerId)) { return; }
|
||||
|
||||
Ray firstFingerInputRay = Camera.main.ScreenPointToRay(firstFingerTouch.position);
|
||||
Ray firstFingerInputRay = Camera.main.ScreenPointToRay(firstFingerTouch.Position);
|
||||
RaycastHit firstFingerHit;
|
||||
if (Physics.Raycast(firstFingerInputRay, out firstFingerHit)) {
|
||||
int? firstFingerGridIndex =
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Safe Scene Refactoring Plan
|
||||
|
||||
## Problem
|
||||
The Gameplay.unity file is massive (255k lines, 7.5MB) making it:
|
||||
- Prone to crashes when edited
|
||||
- Difficult to maintain and review
|
||||
- Risk of corruption when multiple developers work on it
|
||||
|
||||
## Safe Solution Strategy
|
||||
|
||||
Instead of directly editing the massive scene file, we'll use a gradual approach:
|
||||
|
||||
### Phase 1: Create Empty Prefab Structures (SAFE)
|
||||
1. Create new, minimal prefab versions of UI components
|
||||
2. These will be empty initially and won't affect the game
|
||||
3. Test that Unity can load and build successfully
|
||||
|
||||
### Phase 2: Runtime Loading System (SAFE)
|
||||
1. Create a script that can instantiate prefabs at runtime
|
||||
2. Test this system in isolation first
|
||||
3. Gradually migrate components one by one
|
||||
|
||||
### Phase 3: Component Migration (SAFE)
|
||||
1. Start with the simplest components first
|
||||
2. Use A/B testing - keep old version as fallback
|
||||
3. Migrate one component at a time with thorough testing
|
||||
|
||||
## Phase 1 Implementation
|
||||
|
||||
### Step 1.1: Create UI Directory Structure
|
||||
```
|
||||
Assets/UI/
|
||||
├── Prefabs/
|
||||
│ ├── Connection/
|
||||
│ │ ├── ConnectionPanel.prefab
|
||||
│ │ ├── GameSelectionPanel.prefab
|
||||
│ │ └── CustomBattlePanel.prefab
|
||||
│ ├── Game/
|
||||
│ │ ├── CommandCanvas.prefab
|
||||
│ │ ├── PopupCanvas.prefab
|
||||
│ │ └── ReservesCanvas.prefab
|
||||
│ └── Shared/
|
||||
│ ├── ErrorCanvas.prefab
|
||||
│ └── ConfirmationCanvas.prefab
|
||||
└── Scripts/
|
||||
├── PrefabLoader.cs
|
||||
└── UIManager.cs
|
||||
```
|
||||
|
||||
### Step 1.2: Create Empty Prefabs
|
||||
- Start with ConnectionPanel.prefab (simplest connection UI)
|
||||
- Create minimal structure with required components
|
||||
- Test Unity can load without errors
|
||||
|
||||
### Step 1.3: Runtime Loading Test
|
||||
- Create PrefabLoader script that can instantiate prefabs
|
||||
- Test loading prefabs at runtime
|
||||
- Ensure no conflicts with existing UI
|
||||
|
||||
## Benefits of This Approach
|
||||
|
||||
1. **Zero Risk**: Won't affect existing functionality
|
||||
2. **Incremental**: Can be done component by component
|
||||
3. **Testable**: Each step can be verified independently
|
||||
4. **Reversible**: Can rollback any step if needed
|
||||
5. **Collaborative**: Multiple developers can work safely
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. ConnectionPanel (smallest, least complex)
|
||||
2. CustomBattlePanel (isolated functionality)
|
||||
3. GameSelectionPanel (moderate complexity)
|
||||
4. Command-related canvases (high complexity)
|
||||
5. Game UI canvases (highest complexity)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- After each component: Unity loads without errors
|
||||
- After each component: Game functionality unchanged
|
||||
- After each component: Build succeeds
|
||||
- Before final migration: Comprehensive gameplay testing
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
- Keep original Gameplay.unity as Gameplay_backup.unity
|
||||
- Each prefab has a feature flag for enable/disable
|
||||
- Can instantly revert to original scene if issues arise
|
||||
|
||||
This approach ensures we never break the working game while gradually improving the scene structure.
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b097cd2b606a540029692ab16a1fc29a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+10
-166
@@ -1,104 +1,33 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Bootstrap manager that starts the game by loading appropriate scenes.
|
||||
/// Keeps a legacy Gameplay.unity fallback while the split-scene migration is in progress.
|
||||
/// Bootstrap manager that starts the game by loading the shared and connection scenes.
|
||||
/// </summary>
|
||||
public class BootstrapManager : MonoBehaviour {
|
||||
[Header("Startup Configuration")]
|
||||
public bool useNewSceneSystem = true;
|
||||
public bool useOriginalGameplayScene = false;
|
||||
public bool fallbackToOriginalGameplaySceneWhenSplitSceneIncomplete = true;
|
||||
|
||||
[Header("Development Testing")]
|
||||
public bool testSceneLoading = false;
|
||||
|
||||
void Start() { StartCoroutine(InitializeGame()); }
|
||||
|
||||
IEnumerator InitializeGame() {
|
||||
Debug.Log("BootstrapManager: Initializing game...");
|
||||
|
||||
if (testSceneLoading) {
|
||||
yield return StartCoroutine(TestAllScenes());
|
||||
if (GameSceneManager.Instance == null) {
|
||||
Debug.LogError("BootstrapManager: GameSceneManager.Instance is null!");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (useNewSceneSystem) {
|
||||
Debug.Log("BootstrapManager: Loading split-scene startup path (Shared + Connection).");
|
||||
if (GameSceneManager.Instance != null) {
|
||||
yield return StartCoroutine(LoadNewSceneSystemCoroutine());
|
||||
} else {
|
||||
Debug.LogError("BootstrapManager: GameSceneManager.Instance is null!");
|
||||
}
|
||||
Debug.Log("BootstrapManager: Loading startup scenes (Shared + Connection).");
|
||||
yield return StartCoroutine(LoadSplitSceneStartupCoroutine());
|
||||
|
||||
if (!IsConnectionSceneReady(out var incompleteReason)) {
|
||||
Debug.LogWarning(
|
||||
$"BootstrapManager: Split-scene startup incomplete: {incompleteReason}");
|
||||
if (fallbackToOriginalGameplaySceneWhenSplitSceneIncomplete) {
|
||||
yield return StartCoroutine(UnloadSplitStartupScenesCoroutine());
|
||||
yield return StartCoroutine(LoadOriginalGameplaySceneCoroutine());
|
||||
}
|
||||
}
|
||||
} else if (useOriginalGameplayScene) {
|
||||
yield return StartCoroutine(LoadOriginalGameplaySceneCoroutine());
|
||||
} else {
|
||||
Debug.LogWarning(
|
||||
"BootstrapManager: No startup scene path is enabled. Enable split scenes or the legacy fallback.");
|
||||
if (FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include) == null) {
|
||||
Debug.LogError(
|
||||
"BootstrapManager: Connection scene loaded but does not contain a ConnectionHandler.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log("BootstrapManager: Game initialization complete");
|
||||
}
|
||||
|
||||
IEnumerator LoadOriginalGameplaySceneCoroutine() {
|
||||
Debug.Log("BootstrapManager: Loading legacy Gameplay.unity scene through fallback path.");
|
||||
|
||||
CleanupSplitSceneGlobalsBeforeLegacyFallback();
|
||||
|
||||
var asyncLoad = SceneManager.LoadSceneAsync("Gameplay", LoadSceneMode.Additive);
|
||||
while (!asyncLoad.isDone) { yield return null; }
|
||||
|
||||
Debug.Log("BootstrapManager: Legacy Gameplay.unity scene loaded successfully");
|
||||
}
|
||||
|
||||
IEnumerator UnloadSplitStartupScenesCoroutine() {
|
||||
if (GameSceneManager.Instance == null) { yield break; }
|
||||
|
||||
var unloadConnectionTask = GameSceneManager.Instance.UnloadScene("Connection");
|
||||
yield return new WaitUntil(() => unloadConnectionTask.IsCompleted);
|
||||
|
||||
var unloadSharedTask = GameSceneManager.Instance.UnloadScene("Shared");
|
||||
yield return new WaitUntil(() => unloadSharedTask.IsCompleted);
|
||||
}
|
||||
|
||||
bool IsConnectionSceneReady(out string incompleteReason) {
|
||||
var readiness = FindAnyObjectByType<ConnectionSceneReadiness>(FindObjectsInactive.Include);
|
||||
if (readiness != null) { return readiness.IsReady(out incompleteReason); }
|
||||
|
||||
if (FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include) != null) {
|
||||
incompleteReason = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
incompleteReason =
|
||||
"Connection scene has no readiness marker and does not contain a ConnectionHandler.";
|
||||
return false;
|
||||
}
|
||||
|
||||
void CleanupSplitSceneGlobalsBeforeLegacyFallback() {
|
||||
foreach (var eventSystem in FindObjectsByType<EventSystem>(FindObjectsInactive.Include)) {
|
||||
Destroy(eventSystem.gameObject);
|
||||
}
|
||||
|
||||
foreach (var audioListener in FindObjectsByType<AudioListener>(
|
||||
FindObjectsInactive.Include)) {
|
||||
Destroy(audioListener);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator LoadNewSceneSystemCoroutine() {
|
||||
IEnumerator LoadSplitSceneStartupCoroutine() {
|
||||
// Load connection UI first (this is where users start)
|
||||
var connectionTask = GameSceneManager.Instance.TransitionToConnection();
|
||||
yield return new WaitUntil(() => connectionTask.IsCompleted);
|
||||
@@ -110,89 +39,4 @@ public class BootstrapManager : MonoBehaviour {
|
||||
Debug.Log("BootstrapManager: Connection scene loaded successfully");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test all scenes load and unload correctly
|
||||
/// </summary>
|
||||
IEnumerator TestAllScenes() {
|
||||
Debug.Log("BootstrapManager: Testing all scene loading...");
|
||||
|
||||
if (GameSceneManager.Instance == null) {
|
||||
Debug.LogError("BootstrapManager: GameSceneManager.Instance is null!");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Test Connection scene
|
||||
Debug.Log("Testing Connection scene...");
|
||||
var connectionTask = GameSceneManager.Instance.LoadSceneAdditive("Connection");
|
||||
yield return new WaitUntil(() => connectionTask.IsCompleted);
|
||||
|
||||
if (connectionTask.IsFaulted) {
|
||||
Debug.LogError($"Failed to load Connection: {connectionTask.Exception}");
|
||||
} else {
|
||||
Debug.Log("✓ Connection scene loaded");
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
// Test Eagle scene
|
||||
Debug.Log("Testing Eagle scene...");
|
||||
var eagleTask = GameSceneManager.Instance.LoadSceneAdditive("Eagle");
|
||||
yield return new WaitUntil(() => eagleTask.IsCompleted);
|
||||
|
||||
if (eagleTask.IsFaulted) {
|
||||
Debug.LogError($"Failed to load Eagle: {eagleTask.Exception}");
|
||||
} else {
|
||||
Debug.Log("✓ Eagle scene loaded");
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
// Test Shardok scene
|
||||
Debug.Log("Testing Shardok scene...");
|
||||
var shardokTask = GameSceneManager.Instance.LoadSceneAdditive("Shardok");
|
||||
yield return new WaitUntil(() => shardokTask.IsCompleted);
|
||||
|
||||
if (shardokTask.IsFaulted) {
|
||||
Debug.LogError($"Failed to load Shardok: {shardokTask.Exception}");
|
||||
} else {
|
||||
Debug.Log("✓ Shardok scene loaded");
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
// Test Shared scene
|
||||
Debug.Log("Testing Shared scene...");
|
||||
var sharedTask = GameSceneManager.Instance.LoadSceneAdditive("Shared");
|
||||
yield return new WaitUntil(() => sharedTask.IsCompleted);
|
||||
|
||||
if (sharedTask.IsFaulted) {
|
||||
Debug.LogError($"Failed to load Shared: {sharedTask.Exception}");
|
||||
} else {
|
||||
Debug.Log("✓ Shared scene loaded");
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(2f);
|
||||
|
||||
// Test unloading
|
||||
Debug.Log("Testing scene unloading...");
|
||||
|
||||
var unloadShardokTask = GameSceneManager.Instance.UnloadScene("Shardok");
|
||||
yield return new WaitUntil(() => unloadShardokTask.IsCompleted);
|
||||
Debug.Log("✓ Shardok scene unloaded");
|
||||
|
||||
var unloadEagleTask = GameSceneManager.Instance.UnloadScene("Eagle");
|
||||
yield return new WaitUntil(() => unloadEagleTask.IsCompleted);
|
||||
Debug.Log("✓ Eagle scene unloaded");
|
||||
|
||||
var unloadConnectionTask = GameSceneManager.Instance.UnloadScene("Connection");
|
||||
yield return new WaitUntil(() => unloadConnectionTask.IsCompleted);
|
||||
Debug.Log("✓ Connection scene unloaded");
|
||||
|
||||
var unloadSharedTask = GameSceneManager.Instance.UnloadScene("Shared");
|
||||
yield return new WaitUntil(() => unloadSharedTask.IsCompleted);
|
||||
Debug.Log("✓ Shared scene unloaded");
|
||||
|
||||
Debug.Log("🎉 All scene loading tests passed!");
|
||||
}
|
||||
}
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Declares whether the split Connection scene has enough production UI to replace Gameplay.
|
||||
/// </summary>
|
||||
public class ConnectionSceneReadiness : MonoBehaviour {
|
||||
[SerializeField]
|
||||
private bool productionReady = false;
|
||||
|
||||
[SerializeField]
|
||||
private string incompleteReason =
|
||||
"Connection scene is still a shell and does not contain the production ConnectionHandler.";
|
||||
|
||||
public bool IsReady(out string reason) {
|
||||
if (!productionReady) {
|
||||
reason = incompleteReason;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include) == null) {
|
||||
reason = "Connection scene is marked ready but does not contain a ConnectionHandler.";
|
||||
return false;
|
||||
}
|
||||
|
||||
reason = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+330
-14
@@ -1,8 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Auth;
|
||||
using common;
|
||||
using common.GUIUtils;
|
||||
using eagle;
|
||||
using Shardok;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class GameSceneManager : MonoBehaviour {
|
||||
@@ -32,19 +36,33 @@ public class GameSceneManager : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadSceneAdditive(string sceneName) {
|
||||
public async Task<Scene> LoadSceneAdditive(string sceneName) {
|
||||
if (loadedScenes.Contains(sceneName)) {
|
||||
Debug.LogWarning($"Scene {sceneName} is already loaded");
|
||||
return;
|
||||
return SceneManager.GetSceneByName(sceneName);
|
||||
}
|
||||
|
||||
Debug.Log($"Loading scene: {sceneName}");
|
||||
|
||||
var asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
|
||||
while (!asyncLoad.isDone) { await Task.Yield(); }
|
||||
var sceneLoadedTask = new TaskCompletionSource<Scene>();
|
||||
void OnSceneLoaded(Scene scene, LoadSceneMode mode) {
|
||||
if (scene.name != sceneName || mode != LoadSceneMode.Additive) { return; }
|
||||
|
||||
loadedScenes.Add(sceneName);
|
||||
Debug.Log($"Scene {sceneName} loaded successfully");
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
loadedScenes.Add(sceneName);
|
||||
Debug.Log($"Scene {sceneName} loaded successfully");
|
||||
sceneLoadedTask.SetResult(scene);
|
||||
}
|
||||
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
var asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
|
||||
if (asyncLoad == null) {
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
throw new System.InvalidOperationException(
|
||||
$"Failed to start loading scene {sceneName}");
|
||||
}
|
||||
|
||||
return await sceneLoadedTask.Task;
|
||||
}
|
||||
|
||||
public async Task UnloadScene(string sceneName) {
|
||||
@@ -69,44 +87,171 @@ public class GameSceneManager : MonoBehaviour {
|
||||
|
||||
// Load shared infrastructure before the connection UI.
|
||||
await LoadSceneAdditive(sharedSceneName);
|
||||
EnsureSharedInfrastructure();
|
||||
await LoadSceneAdditive(connectionSceneName);
|
||||
ActivateConnectionSceneRoot();
|
||||
WireConnectionSceneReferences();
|
||||
WireSharedUIReferences();
|
||||
}
|
||||
|
||||
public async Task TransitionToEagle() {
|
||||
// Unload connection scene
|
||||
if (loadedScenes.Contains(connectionSceneName)) { await UnloadScene(connectionSceneName); }
|
||||
|
||||
public async Task<EagleGameController> TransitionToEagle() {
|
||||
// Ensure shared scene is loaded
|
||||
if (!loadedScenes.Contains(sharedSceneName)) { await LoadSceneAdditive(sharedSceneName); }
|
||||
EnsureSharedInfrastructure();
|
||||
|
||||
// Load Eagle scene
|
||||
await LoadSceneAdditive(eagleSceneName);
|
||||
var eagleScene = await LoadSceneAdditive(eagleSceneName);
|
||||
var eagleController = GetEagleController(eagleScene);
|
||||
if (eagleController == null) { LogSceneRoots(eagleScene); }
|
||||
WireEagleSceneReferences();
|
||||
WireSharedUIReferences();
|
||||
return eagleController;
|
||||
}
|
||||
|
||||
public async Task TransitionToShardok() {
|
||||
public async Task<GameObject> TransitionToShardok() {
|
||||
// Ensure shared scene is loaded
|
||||
if (!loadedScenes.Contains(sharedSceneName)) { await LoadSceneAdditive(sharedSceneName); }
|
||||
EnsureSharedInfrastructure();
|
||||
|
||||
// Load Shardok scene additively (on top of Eagle)
|
||||
await LoadSceneAdditive(shardokSceneName);
|
||||
WireShardokSceneReferences();
|
||||
WireEagleSceneReferences();
|
||||
WireSharedUIReferences();
|
||||
return GetShardokContainer();
|
||||
}
|
||||
|
||||
public async Task ExitShardok() {
|
||||
// Just unload Shardok scene, keep Eagle loaded
|
||||
if (loadedScenes.Contains(shardokSceneName)) { await UnloadScene(shardokSceneName); }
|
||||
WireSharedUIReferences();
|
||||
}
|
||||
|
||||
public bool IsSceneLoaded(string sceneName) { return loadedScenes.Contains(sceneName); }
|
||||
|
||||
public EagleGameController GetEagleController() {
|
||||
var eagleScene = SceneManager.GetSceneByName(eagleSceneName);
|
||||
if (eagleScene.IsValid() && eagleScene.isLoaded) {
|
||||
var eagleController = GetEagleController(eagleScene);
|
||||
if (eagleController != null) { return eagleController; }
|
||||
}
|
||||
|
||||
return FindAnyObjectByType<EagleGameController>(FindObjectsInactive.Include);
|
||||
}
|
||||
|
||||
public EagleGameController GetEagleController(Scene eagleScene) {
|
||||
var eagleRoot = FindSceneRootByName(eagleScene, "Eagle Canvas");
|
||||
return eagleRoot == null ? null : eagleRoot.GetComponent<EagleGameController>();
|
||||
}
|
||||
|
||||
public Canvas GetEagleCanvas() {
|
||||
var eagleController = GetEagleController();
|
||||
return eagleController == null ? null : eagleController.GetComponent<Canvas>();
|
||||
}
|
||||
|
||||
public GameObject GetShardokContainer() {
|
||||
var shardokController =
|
||||
FindAnyObjectByType<ShardokGameController>(FindObjectsInactive.Include);
|
||||
return shardokController == null ? null : shardokController.gameObject;
|
||||
}
|
||||
|
||||
public ConnectionHandler GetConnectionHandler() {
|
||||
return FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include);
|
||||
}
|
||||
|
||||
public ErrorHandler GetErrorHandler() {
|
||||
return FindAnyObjectByType<ErrorHandler>(FindObjectsInactive.Include);
|
||||
}
|
||||
|
||||
private void ActivateConnectionSceneRoot() {
|
||||
var connectionHandler = GetConnectionHandler();
|
||||
if (connectionHandler != null) { connectionHandler.gameObject.SetActive(true); }
|
||||
}
|
||||
|
||||
private void WireConnectionSceneReferences() {
|
||||
EnsureOAuthManager();
|
||||
|
||||
var connectionHandler = FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include);
|
||||
var connectionHandler = GetConnectionHandler();
|
||||
if (connectionHandler == null) { return; }
|
||||
|
||||
var errorHandler = FindAnyObjectByType<ErrorHandler>(FindObjectsInactive.Include);
|
||||
var errorHandler = GetErrorHandler();
|
||||
if (errorHandler != null) { connectionHandler.errorHandler = errorHandler; }
|
||||
|
||||
var eagleCanvas = GetEagleCanvas();
|
||||
if (eagleCanvas != null) { connectionHandler.eagleCanvas = eagleCanvas; }
|
||||
|
||||
var shardokContainer = GetShardokContainer();
|
||||
if (shardokContainer != null) {
|
||||
connectionHandler.shardokContainer = shardokContainer;
|
||||
if (connectionHandler.customBattleHandler != null) {
|
||||
connectionHandler.customBattleHandler.shardokContainer = shardokContainer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WireEagleSceneReferences() {
|
||||
var eagleController = FindAnyObjectByType<EagleGameController>(FindObjectsInactive.Include);
|
||||
if (eagleController == null) { return; }
|
||||
|
||||
var errorHandler = GetErrorHandler();
|
||||
if (errorHandler != null) { eagleController.errorHandler = errorHandler; }
|
||||
|
||||
var soundManager = GetSoundManager();
|
||||
if (soundManager != null) {
|
||||
eagleController.soundManager = soundManager;
|
||||
WireEagleAudioSources(eagleController, soundManager.soundEffectsSource);
|
||||
}
|
||||
|
||||
var mainCamera = GetMainCamera();
|
||||
if (mainCamera != null) { WireEagleCanvasCameras(eagleController, mainCamera); }
|
||||
|
||||
ApplyEagleSceneLighting();
|
||||
|
||||
var shardokContainer = GetShardokContainer();
|
||||
if (shardokContainer != null) { eagleController.shardokContainer = shardokContainer; }
|
||||
}
|
||||
|
||||
private void WireShardokSceneReferences() {
|
||||
var shardokController =
|
||||
FindAnyObjectByType<ShardokGameController>(FindObjectsInactive.Include);
|
||||
if (shardokController == null) { return; }
|
||||
|
||||
var soundManager = GetSoundManager();
|
||||
if (soundManager != null) {
|
||||
shardokController.soundManager = soundManager;
|
||||
shardokController.audioClipSource = soundManager.soundEffectsSource;
|
||||
}
|
||||
|
||||
var mainCamera = GetMainCamera();
|
||||
if (mainCamera != null) {
|
||||
shardokController.mainCamera = mainCamera;
|
||||
WireShardokCanvasCameras(shardokController, mainCamera);
|
||||
}
|
||||
|
||||
var eagleCanvas = GetEagleCanvas();
|
||||
if (eagleCanvas != null) { shardokController.eagleCanvas = eagleCanvas; }
|
||||
}
|
||||
|
||||
private static void WireEagleAudioSources(
|
||||
EagleGameController eagleController,
|
||||
AudioSource audioSource) {
|
||||
if (audioSource == null) { return; }
|
||||
|
||||
if (eagleController.provinceActionAnimator != null) {
|
||||
eagleController.provinceActionAnimator.audioSource = audioSource;
|
||||
}
|
||||
|
||||
if (eagleController.mapController?.battleInProgressController != null) {
|
||||
eagleController.mapController.battleInProgressController.audioSource = audioSource;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureSharedInfrastructure() {
|
||||
MainQueue.EnsureExists();
|
||||
EnsureOAuthManager();
|
||||
EnsureSoundManager();
|
||||
WireSharedCanvasCameras();
|
||||
WireSharedUIReferences();
|
||||
}
|
||||
|
||||
private static void EnsureOAuthManager() {
|
||||
@@ -117,4 +262,175 @@ public class GameSceneManager : MonoBehaviour {
|
||||
var oauthManager = new GameObject("OAuth Manager");
|
||||
oauthManager.AddComponent<OAuthManager>();
|
||||
}
|
||||
|
||||
private SoundManager GetSoundManager() {
|
||||
return FindAnyObjectByType<SoundManager>(FindObjectsInactive.Include);
|
||||
}
|
||||
|
||||
private Camera GetMainCamera() {
|
||||
return Camera.main ?? FindAnyObjectByType<Camera>(FindObjectsInactive.Include);
|
||||
}
|
||||
|
||||
private void WireSharedCanvasCameras() {
|
||||
var sharedScene = SceneManager.GetSceneByName(sharedSceneName);
|
||||
if (!sharedScene.IsValid() || !sharedScene.isLoaded) { return; }
|
||||
|
||||
var mainCamera = GetMainCamera();
|
||||
if (mainCamera == null) { return; }
|
||||
|
||||
foreach (var root in sharedScene.GetRootGameObjects()) {
|
||||
foreach (var canvas in root.GetComponentsInChildren<Canvas>(includeInactive: true)) {
|
||||
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay ||
|
||||
canvas.renderMode == RenderMode.WorldSpace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
canvas.worldCamera = mainCamera;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WireSharedUIReferences() {
|
||||
var settingsPanel =
|
||||
FindAnyObjectByType<SettingsPanelController>(FindObjectsInactive.Include);
|
||||
if (settingsPanel == null) { return; }
|
||||
|
||||
var soundManager = GetSoundManager();
|
||||
if (soundManager != null) {
|
||||
settingsPanel.soundEffectsSource = soundManager.soundEffectsSource;
|
||||
settingsPanel.musicSource = soundManager.musicSource;
|
||||
}
|
||||
|
||||
settingsPanel.hoveringTooltip =
|
||||
FindAnyObjectByType<HoveringTooltip>(FindObjectsInactive.Include);
|
||||
settingsPanel.animationTestController =
|
||||
FindAnyObjectByType<AnimationTestController>(FindObjectsInactive.Include);
|
||||
settingsPanel.provinceLabelsController =
|
||||
FindAnyObjectByType<ProvinceLabelsController>(FindObjectsInactive.Include);
|
||||
settingsPanel.hexGrid = FindAnyObjectByType<HexGrid>(FindObjectsInactive.Include);
|
||||
settingsPanel.eagleGameController = GetEagleController();
|
||||
settingsPanel.ShardokContainer = GetShardokContainer();
|
||||
settingsPanel.connectionHandler =
|
||||
FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include);
|
||||
settingsPanel.bugReportPanel =
|
||||
FindAnyObjectByType<BugReportPanelController>(FindObjectsInactive.Include);
|
||||
settingsPanel.attributionsController =
|
||||
FindAnyObjectByType<AttributionsController>(FindObjectsInactive.Include);
|
||||
|
||||
if (settingsPanel.bugReportPanel != null) {
|
||||
settingsPanel.bugReportPanel.settingsPanel = settingsPanel.panel;
|
||||
}
|
||||
|
||||
foreach (var keyListener in FindObjectsByType<SettingsKeyListener>(
|
||||
FindObjectsInactive.Include)) {
|
||||
keyListener.settingsPanelController = settingsPanel;
|
||||
}
|
||||
|
||||
settingsPanel.ApplyPersistedSettings();
|
||||
}
|
||||
|
||||
private static void WireShardokCanvasCameras(
|
||||
ShardokGameController shardokController,
|
||||
Camera mainCamera) {
|
||||
foreach (var canvas in shardokController.GetComponentsInChildren<Canvas>(
|
||||
includeInactive: true)) {
|
||||
if (canvas.renderMode == RenderMode.WorldSpace) { continue; }
|
||||
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = mainCamera;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WireEagleCanvasCameras(
|
||||
EagleGameController eagleController,
|
||||
Camera mainCamera) {
|
||||
foreach (var canvas in eagleController.GetComponentsInChildren<Canvas>(
|
||||
includeInactive: true)) {
|
||||
if (canvas.renderMode == RenderMode.WorldSpace) { continue; }
|
||||
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = mainCamera;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyEagleSceneLighting() {
|
||||
RenderSettings.ambientMode = AmbientMode.Flat;
|
||||
RenderSettings.ambientLight = Color.white;
|
||||
RenderSettings.ambientIntensity = 1f;
|
||||
}
|
||||
|
||||
private void EnsureSoundManager() {
|
||||
var existingSoundManager = GetSoundManager();
|
||||
if (existingSoundManager != null) {
|
||||
EnsureSoundManagerAudioSources(existingSoundManager);
|
||||
return;
|
||||
}
|
||||
|
||||
var soundManagerObject = new GameObject("Sound Manager");
|
||||
var soundManager = soundManagerObject.AddComponent<SoundManager>();
|
||||
EnsureSoundManagerAudioSources(soundManager);
|
||||
|
||||
var sharedScene = SceneManager.GetSceneByName(sharedSceneName);
|
||||
if (sharedScene.IsValid() && sharedScene.isLoaded) {
|
||||
SceneManager.MoveGameObjectToScene(soundManagerObject, sharedScene);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureSoundManagerAudioSources(SoundManager soundManager) {
|
||||
if (soundManager.soundEffectsSource == null) {
|
||||
soundManager.soundEffectsSource =
|
||||
CreateAudioSourceChild(soundManager.transform, "Sound Effects Source");
|
||||
}
|
||||
|
||||
if (soundManager.musicSource == null) {
|
||||
soundManager.musicSource =
|
||||
CreateAudioSourceChild(soundManager.transform, "Music Source");
|
||||
}
|
||||
}
|
||||
|
||||
private static AudioSource CreateAudioSourceChild(Transform parent, string name) {
|
||||
var sourceObject = new GameObject(name);
|
||||
sourceObject.transform.SetParent(parent, false);
|
||||
|
||||
return sourceObject.AddComponent<AudioSource>();
|
||||
}
|
||||
|
||||
private static GameObject FindSceneRootByName(string sceneName, string rootName) {
|
||||
var scene = SceneManager.GetSceneByName(sceneName);
|
||||
if (!scene.IsValid() || !scene.isLoaded) { return null; }
|
||||
|
||||
return FindSceneRootByName(scene, rootName);
|
||||
}
|
||||
|
||||
private static GameObject FindSceneRootByName(Scene scene, string rootName) {
|
||||
foreach (var root in scene.GetRootGameObjects()) {
|
||||
if (root.name == rootName) { return root; }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void LogSceneRoots(string sceneName) {
|
||||
var scene = SceneManager.GetSceneByName(sceneName);
|
||||
LogSceneRoots(scene);
|
||||
}
|
||||
|
||||
private static void LogSceneRoots(Scene scene) {
|
||||
if (!scene.IsValid()) {
|
||||
Debug.LogError("Scene is not valid after load.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scene.isLoaded) {
|
||||
Debug.LogError($"Scene {scene.name} is valid but not loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
var rootNames = new List<string>();
|
||||
foreach (var root in scene.GetRootGameObjects()) { rootNames.Add(root.name); }
|
||||
|
||||
Debug.LogError(
|
||||
$"Scene {scene.name} is loaded but no EagleGameController was found. " +
|
||||
$"Root objects: {string.Join(", ", rootNames)}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Test script to verify scene loading works correctly.
|
||||
/// This should be attached to a GameObject in Main.unity for testing.
|
||||
/// </summary>
|
||||
public class SceneLoadTester : MonoBehaviour {
|
||||
void Start() {
|
||||
Debug.Log("SceneLoadTester: Starting scene load test...");
|
||||
StartCoroutine(TestSceneLoading());
|
||||
}
|
||||
|
||||
IEnumerator TestSceneLoading() {
|
||||
// Wait for SceneManager to initialize
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
if (GameSceneManager.Instance == null) {
|
||||
Debug.LogError("SceneLoadTester: GameSceneManager.Instance is null!");
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log("SceneLoadTester: Testing Connection scene load...");
|
||||
|
||||
// Test loading Connection scene
|
||||
var loadTask = GameSceneManager.Instance.LoadSceneAdditive("Connection");
|
||||
yield return new WaitUntil(() => loadTask.IsCompleted);
|
||||
|
||||
if (loadTask.IsFaulted) {
|
||||
Debug.LogError(
|
||||
$"SceneLoadTester: Failed to load Connection scene: {loadTask.Exception}");
|
||||
} else {
|
||||
Debug.Log("SceneLoadTester: Connection scene loaded successfully!");
|
||||
}
|
||||
|
||||
// Wait a bit
|
||||
yield return new WaitForSeconds(2f);
|
||||
|
||||
// Test unloading Connection scene
|
||||
Debug.Log("SceneLoadTester: Testing Connection scene unload...");
|
||||
var unloadTask = GameSceneManager.Instance.UnloadScene("Connection");
|
||||
yield return new WaitUntil(() => unloadTask.IsCompleted);
|
||||
|
||||
if (unloadTask.IsFaulted) {
|
||||
Debug.LogError(
|
||||
$"SceneLoadTester: Failed to unload Connection scene: {unloadTask.Exception}");
|
||||
} else {
|
||||
Debug.Log("SceneLoadTester: Connection scene unloaded successfully!");
|
||||
}
|
||||
|
||||
Debug.Log("SceneLoadTester: Test complete!");
|
||||
}
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3d8f9175cb2b4c4d929c4ee2e8f7058
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to assist with migrating GameObjects from Gameplay.unity to separate scenes.
|
||||
/// This should be run in the Unity Editor with Gameplay.unity loaded.
|
||||
/// </summary>
|
||||
public class SceneMigrationHelper : MonoBehaviour {
|
||||
[Header("Analysis Results")]
|
||||
public List<string> connectionUIObjects = new List<string>();
|
||||
public List<string> eagleUIObjects = new List<string>();
|
||||
public List<string> shardokUIObjects = new List<string>();
|
||||
public List<string> sharedUIObjects = new List<string>();
|
||||
|
||||
[Header("Key GameObjects")]
|
||||
public GameObject connectionCanvas;
|
||||
public GameObject eagleCanvas;
|
||||
public GameObject shardokContainer;
|
||||
public GameObject errorCanvas;
|
||||
|
||||
/// <summary>
|
||||
/// Run this in the Unity Editor to analyze which GameObjects belong to which scene.
|
||||
/// </summary>
|
||||
[ContextMenu("Analyze Scene Structure")]
|
||||
public void AnalyzeSceneStructure() {
|
||||
connectionUIObjects.Clear();
|
||||
eagleUIObjects.Clear();
|
||||
shardokUIObjects.Clear();
|
||||
sharedUIObjects.Clear();
|
||||
|
||||
// Find key canvases
|
||||
Canvas[] allCanvases = FindObjectsByType<Canvas>(FindObjectsInactive.Include);
|
||||
|
||||
foreach (Canvas canvas in allCanvases) {
|
||||
string canvasName = canvas.gameObject.name.ToLower();
|
||||
|
||||
if (canvasName.Contains("connection") || canvasName.Contains("lobby")) {
|
||||
connectionUIObjects.Add($"Canvas: {canvas.gameObject.name}");
|
||||
AddChildrenToList(canvas.transform, connectionUIObjects, " ");
|
||||
} else if (canvasName.Contains("eagle") && !canvasName.Contains("shardok")) {
|
||||
eagleUIObjects.Add($"Canvas: {canvas.gameObject.name}");
|
||||
AddChildrenToList(canvas.transform, eagleUIObjects, " ");
|
||||
} else if (canvasName.Contains("shardok")) {
|
||||
shardokUIObjects.Add($"Canvas: {canvas.gameObject.name}");
|
||||
AddChildrenToList(canvas.transform, shardokUIObjects, " ");
|
||||
} else if (
|
||||
canvasName.Contains("error") || canvasName.Contains("popup") ||
|
||||
canvasName.Contains("confirmation") || canvasName.Contains("overlay")) {
|
||||
sharedUIObjects.Add($"Canvas: {canvas.gameObject.name}");
|
||||
AddChildrenToList(canvas.transform, sharedUIObjects, " ");
|
||||
}
|
||||
}
|
||||
|
||||
// Find panels that might not be under canvases
|
||||
GameObject[] allGameObjects = FindObjectsByType<GameObject>(FindObjectsInactive.Include);
|
||||
|
||||
foreach (GameObject go in allGameObjects) {
|
||||
if (go.GetComponent<Canvas>() != null) continue; // Skip canvases we already processed
|
||||
|
||||
string goName = go.name.ToLower();
|
||||
|
||||
if (goName.Contains("connection panel") || goName.Contains("game selection panel") ||
|
||||
goName.Contains("custom battle panel")) {
|
||||
connectionUIObjects.Add($"Panel: {go.name}");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"Analysis complete!");
|
||||
Debug.Log($"Connection UI Objects: {connectionUIObjects.Count}");
|
||||
Debug.Log($"Eagle UI Objects: {eagleUIObjects.Count}");
|
||||
Debug.Log($"Shardok UI Objects: {shardokUIObjects.Count}");
|
||||
Debug.Log($"Shared UI Objects: {sharedUIObjects.Count}");
|
||||
}
|
||||
|
||||
private void AddChildrenToList(Transform parent, List<string> list, string indent) {
|
||||
foreach (Transform child in parent) {
|
||||
list.Add($"{indent}{child.name}");
|
||||
// Don't recurse too deep to keep the list manageable
|
||||
if (indent.Length < 6) { AddChildrenToList(child, list, indent + " "); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs which components reference GameObjects that might be in different scenes.
|
||||
/// </summary>
|
||||
[ContextMenu("Find Cross-Scene References")]
|
||||
public void FindCrossSceneReferences() {
|
||||
ConnectionHandler connectionHandler = FindAnyObjectByType<ConnectionHandler>();
|
||||
if (connectionHandler != null) {
|
||||
Debug.Log("ConnectionHandler references:");
|
||||
Debug.Log($" - connectionPanel: {connectionHandler.connectionPanel?.name}");
|
||||
Debug.Log($" - gameSelectionPanel: {connectionHandler.gameSelectionPanel?.name}");
|
||||
Debug.Log($" - customBattlePanel: {connectionHandler.customBattlePanel?.name}");
|
||||
Debug.Log($" - connectionCanvas: {connectionHandler.connectionCanvas?.name}");
|
||||
Debug.Log($" - eagleCanvas: {connectionHandler.eagleCanvas?.name}");
|
||||
Debug.Log($" - shardokContainer: {connectionHandler.shardokContainer?.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ee8c3dd45a034f97b05e47387df8c31
|
||||
+2
-3
@@ -4,9 +4,8 @@ using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Simple error handler for the new scene system.
|
||||
/// This provides basic error display functionality without the complexity of the original
|
||||
/// ErrorHandler.
|
||||
/// Simple shared-scene error handler.
|
||||
/// This mirrors Unity errors into the lightweight shared error display.
|
||||
/// </summary>
|
||||
public class SimpleErrorHandler : MonoBehaviour {
|
||||
[Header("UI References")]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user