mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 05:15:44 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ed9982822 | ||
|
|
dc351a48b8 | ||
|
|
cb0111a00f | ||
|
|
508bfabece | ||
|
|
de532a3448 | ||
|
|
29c3d5f2c1 | ||
|
|
7b118f24b6 | ||
|
|
2ddb627c58 | ||
|
|
154fac85a5 | ||
|
|
5e61f43183 | ||
|
|
a79209891d | ||
|
|
cbcad13d23 | ||
|
|
9e7eaa6367 | ||
|
|
3fc8589847 | ||
|
|
abec33585d | ||
|
|
e82c174814 | ||
|
|
38732c9255 | ||
|
|
7cd56e5980 | ||
|
|
454d4e2fc7 | ||
|
|
7ad5ebdb56 | ||
|
|
1e3ae0d82c |
@@ -14,6 +14,7 @@ on:
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_submit.sh"
|
||||
- "scripts/notarize_wait.sh"
|
||||
@@ -31,6 +32,7 @@ on:
|
||||
- "src/main/protobuf/net/eagle0/eagle/views/**"
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_submit.sh"
|
||||
- "scripts/notarize_wait.sh"
|
||||
@@ -66,14 +68,27 @@ jobs:
|
||||
run: git lfs pull
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build Mac Unity
|
||||
run: ./ci/github_actions/build_unity_mac.sh "/tmp/eagle0/eagle0MAC"
|
||||
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Inject Sparkle Framework
|
||||
if: success()
|
||||
env:
|
||||
SPARKLE_EDDSA_PUBLIC_KEY: ${{ secrets.SPARKLE_EDDSA_PUBLIC_KEY }}
|
||||
run: |
|
||||
chmod +x ./scripts/inject_sparkle.sh
|
||||
./scripts/inject_sparkle.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Check if should deploy
|
||||
id: check-deploy
|
||||
run: |
|
||||
@@ -239,11 +254,20 @@ jobs:
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
SPARKLE_EDDSA_PRIVATE_KEY: ${{ secrets.SPARKLE_EDDSA_PRIVATE_KEY }}
|
||||
run: |
|
||||
# Write private key to temp file for signing
|
||||
SPARKLE_PRIVATE_KEY_PATH="/tmp/sparkle_private_key"
|
||||
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
VERSION=$(git describe --tags --always)
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD)
|
||||
|
||||
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
|
||||
"/tmp/eagle0/eagle0MAC/eagle0.app" \
|
||||
"$VERSION" \
|
||||
"$BUILD_NUMBER"
|
||||
"$BUILD_NUMBER" \
|
||||
"$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
rm "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
@@ -49,14 +49,19 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
clean: false
|
||||
clean: true # Remove untracked files from previous builds
|
||||
- name: Pull lfs files
|
||||
run: git lfs pull
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
- name: Build Windows unity
|
||||
run: ./ci/github_actions/build_unity.sh "/tmp/eagle0/eagle0WIN"
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
- name: Deploy Windows unity
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
|
||||
-566
@@ -1,566 +0,0 @@
|
||||
# Deproto Migration Plan
|
||||
|
||||
This document tracks the migration from protobuf types to native Scala models inside the Eagle game engine.
|
||||
|
||||
## Architectural Decisions
|
||||
|
||||
1. **Keep proto for persistence**: Yes - protobuf is used for persisting game state
|
||||
2. **Keep proto for Shardok communication**: Yes - protobuf is used for Eagle-Shardok gRPC communication
|
||||
3. **Use Scala views inside the library**: Yes - use native Scala types like `ProvinceView`, `FactionView`, `HeroView`, etc. within the library code
|
||||
|
||||
## Recent Completed Work
|
||||
|
||||
### GameState Round-Trip Elimination (PRs #4913, #4914, #4915)
|
||||
|
||||
Eliminated wasteful Scala→proto→Scala conversions in the hot path:
|
||||
|
||||
1. **LLM Pipeline** (#4913): `LlmRequestWithGameState` now uses Scala `GameState` instead of proto. All ~38 prompt generators updated to use Scala model types (`FactionT`, `HeroT`, `ProvinceT`).
|
||||
|
||||
2. **ActionWithResultingState Caching** (#4914): Added `precomputedScalaState: Option[GameState]` to cache Scala state when available, avoiding `fromProto()` conversion in `stateAfter()`.
|
||||
|
||||
3. **PostResults Simplification** (#4915): Changed `PostResults.gameState` from proto to `Option[GameState]` (Scala), eliminating `toProto()` calls when creating PostResults.
|
||||
|
||||
## Migration Pattern
|
||||
|
||||
The codebase follows a **Legacy* pattern** for separating proto-dependent and protoless code:
|
||||
|
||||
- **Protoless utilities**: `FactionUtils`, `HeroUtils`, `ProvinceUtils`, `ProvinceDistances`, etc.
|
||||
- **Proto-dependent utilities**: `LegacyFactionUtils`, `LegacyHeroUtils`, `LegacyProvinceUtils`, `LegacyProvinceDistances`, etc.
|
||||
|
||||
When migrating a file:
|
||||
1. Create a `Legacy*` version containing the proto-dependent methods
|
||||
2. Keep the original file name for protoless methods
|
||||
3. Update callers to use the appropriate version based on their context
|
||||
|
||||
## Migration Status
|
||||
|
||||
### Fully Protoless (no proto imports)
|
||||
|
||||
**Utilities:**
|
||||
- [x] `FactionUtils` - has protoless `ownedNeighbors` method
|
||||
- [x] `ProvinceDistances` - split into protoless + `LegacyProvinceDistances`
|
||||
- [x] `SwornBrotherChooser` - fully protoless (removed `bestChoiceProto`)
|
||||
|
||||
**Command Selectors (all use native GameState):**
|
||||
- [x] `AllianceOfferCommandSelector`
|
||||
- [x] `AlmsCommandSelector`
|
||||
- [x] `AttackCommandChooser`
|
||||
- [x] `ExpandCommandSelector`
|
||||
- [x] `HeroGiftCommandSelector`
|
||||
- [x] `ImproveCommandSelector`
|
||||
- [x] `MarchTowardProvinceCommandChooser` - in AI folder, uses native GameState (callers convert)
|
||||
- [x] `OrganizeCommandSelector`
|
||||
- [x] `RansomOfferHelpers`
|
||||
- [x] `SeekMoreLeadersCommandChooser` - in AI folder, uses native GameState
|
||||
- [x] `TruceOfferCommandSelector`
|
||||
- [x] `TrustForDiplomacy`
|
||||
|
||||
**Quest Command Selectors (all protoless):**
|
||||
- [x] `AllianceQuestCommandChooser`
|
||||
- [x] `AlmsAcrossRealmQuestCommandChooser`
|
||||
- [x] `AlmsToProvinceQuestCommandChooser`
|
||||
- [x] `DismissSpecificVassalCommandChooser`
|
||||
- [x] `GiveToHeroesAcrossRealmQuestCommandChooser`
|
||||
- [x] `GiveToHeroesInProvinceQuestCommandChooser`
|
||||
- [x] `ImproveQuestCommandChooser`
|
||||
- [x] `QuestCommandChooser`
|
||||
- [x] `TruceCountQuestCommandChooser`
|
||||
- [x] `TruceWithFactionQuestCommandChooser`
|
||||
|
||||
### Fully Protoless
|
||||
|
||||
- [x] `AIClientUtils` - has protoless overloads (`takenHeroIdsForMarchTowardFocus`, `mostPowerfulHeroes`)
|
||||
- [x] `AttackCommandChooser` - uses Scala `GameState` and `BattalionViewC` (Scala view type)
|
||||
- [x] `BattalionPower` - has `estimatedPower(BattalionViewC)` for recon data with optional stats
|
||||
- [x] `CommandChoiceHelpers` - fully protoless, uses Scala `GameState` throughout
|
||||
- [x] `ProvinceGoldSurplusCalculator` - fully protoless (callers use converters)
|
||||
- [x] `HeroSelector` - fully protoless (removed dead `minimallyFatiguedHeroesProto`)
|
||||
|
||||
### AI Layer ✅ COMPLETE
|
||||
|
||||
All AI and command chooser code is now fully protoless:
|
||||
- [x] `AttackDecisionCommandChooser` - uses Scala GameState
|
||||
- [x] `CommandChooser` - trait uses Scala GameState
|
||||
- [x] `FulfillQuestsCommandSelector` - uses Scala GameState
|
||||
- [x] `MidGameAIClient` - uses Scala GameState internally
|
||||
|
||||
### Still Using Proto GameState (Boundary Code)
|
||||
|
||||
These files use proto GameState because they're at system boundaries:
|
||||
|
||||
**View Filters (client projection):**
|
||||
- `view_filters/GameStateViewFilter` - has Scala overload, uses Scala sub-filters
|
||||
- `view_filters/ProvinceViewFilter` - has Scala overloads for some methods
|
||||
- `view_filters/FactionViewFilter` - has Scala overload
|
||||
- `view_filters/HeroViewFilter` - has Scala overload
|
||||
- `view_filters/BattalionNameFilter` - has Scala overload
|
||||
- `view_filters/BattleFilter` - has Scala overload
|
||||
|
||||
**Legacy Utilities (to be deprecated):**
|
||||
- `LegacyProvinceDistances`, `LegacyFactionUtils`, `LegacyHeroUtils`, etc.
|
||||
- Used by code that still needs proto GameState
|
||||
|
||||
**Persistence/Action System:**
|
||||
- `ActionResultTApplier`, `ActionResultProtoApplier` - apply results to proto state
|
||||
- `ActionWithResultingState` - caches both proto and Scala state
|
||||
|
||||
**Shardok Interface (gRPC boundary):**
|
||||
- `ShardokInterfaceGrpcClient`, `ShardokInterfaceProxy` - must use proto for C++ communication
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 1-3: AI Layer ✅ COMPLETE
|
||||
|
||||
The entire AI decision-making layer is now protoless.
|
||||
|
||||
### Phase 4: View Filters ✅ COMPLETE
|
||||
|
||||
The view_filters package migration is complete:
|
||||
|
||||
**Completed:**
|
||||
- [x] `GameStateViewFilter` - added Scala GameState overload (uses Scala sub-filters)
|
||||
- [x] `ProvinceViewFilter` - already has Scala overloads for server-side views
|
||||
- [x] `HumanPlayerClientConnectionState` - updated to pass Scala GameState directly
|
||||
- [x] `HeroViewFilter` - added Scala overload
|
||||
- [x] `FactionViewFilter` - added Scala overload
|
||||
- [x] `Visibility` - added Scala overloads
|
||||
|
||||
**Still Using Proto:**
|
||||
- [x] `BattalionNameFilter` - has Scala overload
|
||||
- [x] `BattleFilter` - has Scala overload
|
||||
- [ ] `ActionResultFilter` - uses proto internally (boundary code)
|
||||
|
||||
**Strategy:**
|
||||
1. Add Scala GameState overloads to view filter methods
|
||||
2. Update callers to pass Scala GameState where available
|
||||
3. Eventually deprecate proto versions
|
||||
|
||||
### Phase 5: Legacy Utility Cleanup (IN PROGRESS)
|
||||
|
||||
Remove Legacy* utilities by migrating remaining callers:
|
||||
1. Identify callers of each Legacy* util
|
||||
2. Update callers to use protoless versions
|
||||
3. Delete Legacy* files when no longer needed
|
||||
|
||||
**Deleted (no production callers):**
|
||||
- [x] `LegacyProvinceDistances` - deleted (no callers)
|
||||
- [x] `LegacyBattalionSuitability` - deleted (no callers)
|
||||
- [x] `LegacyFoodConsumptionUtils` - deleted (no callers)
|
||||
- [x] `LegacyHandleRiotUtils` - deleted (no callers)
|
||||
|
||||
**Refactored to Thin Wrappers (delegating to protoless versions):**
|
||||
- [x] `LegacyRansomValidity` - already a thin wrapper delegating to `RansomValidity`
|
||||
- [x] `LegacyRecruitmentOdds` - refactored to delegate to `RecruitmentOdds`
|
||||
|
||||
**Parallel Implementations (proto mirrors protoless):**
|
||||
- [x] `FactionUtils` / `LegacyFactionUtils` - both have matching APIs; LegacyFactionUtils used by boundary code (24+ callers)
|
||||
- [x] `HeroUtils` / `LegacyHeroUtils` - both have matching APIs; LegacyHeroUtils used by boundary code (10 callers)
|
||||
- [x] `ProvinceUtils` / `LegacyProvinceUtils` - both have matching APIs; LegacyProvinceUtils used by boundary code (20 callers: availability factories, view filters)
|
||||
|
||||
**Parallel Implementations (awaiting migration of callers):**
|
||||
- [x] `BattalionUtils` / `LegacyBattalionUtils` - both have matching core methods; LegacyBattalionUtils used by boundary code (4 callers)
|
||||
- [x] `BattalionViewFilter` / `LegacyBattalionViewFilter` - protoless version exists; Legacy used by view filters, action appliers (3 callers)
|
||||
- [x] `BattalionTypeFinder` / `LegacyBattalionTypeFinder` - protoless version exists; Legacy used by validators (1 caller: RuntimeValidator)
|
||||
|
||||
### Recent Caller Migration
|
||||
|
||||
**CheckForFulfilledQuestsAction** - migrated to use protoless `BattalionTypeFinder`:
|
||||
- Changed `battalionTypes` parameter from proto `Vector[BattalionType]` to Scala `Vector[BattalionType]`
|
||||
- Updated callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
|
||||
- Eliminated wasteful `BattalionTypeConverter.toProto()` conversions
|
||||
|
||||
**ExpandedUnaffiliatedHeroUtils** - added Scala overload:
|
||||
- New overload takes Scala `GameState` and `UnaffiliatedHeroT` directly
|
||||
- Added `UnaffiliatedHeroConverter.unaffiliatedHeroTypeToProto()` helper for efficient enum conversion
|
||||
- Proto overload retained for backward compatibility
|
||||
|
||||
**AvailablePleaseRecruitMeCommandFactory** - eliminated wasteful proto conversions:
|
||||
- Now uses `ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(ScalaGameState, UnaffiliatedHeroT)` directly
|
||||
- Removed `GameStateConverter.toProto()` and `UnaffiliatedHeroConverter.toProto()` calls
|
||||
- Factory is now fully protoless internally (still returns proto types for API boundary)
|
||||
|
||||
**ProvinceViewFilter** - added Scala overload with faction filtering:
|
||||
- New `filteredProvinceView(province: ProvinceT, gs: ScalaGameState, factionId: FactionId)` overload
|
||||
- Uses protoless `FactionUtils.hasAlliance`, `Visibility.hasFullVisibility`, and `ProvinceUtils.incomingOthers`
|
||||
- Handles reconned provinces directly from Scala `FactionT.reconnedProvinces` (already Scala type)
|
||||
- Added helper methods: `fullProvinceInfoScala`, `maybeIncomingAttackersScala`, `unaffiliatedHeroInfoScala`
|
||||
- Events still converted to proto at the end (ProvinceView.knownEvents uses proto events)
|
||||
|
||||
**GameStateViewFilter** - eliminated GameStateConverter.toProto() call:
|
||||
- Scala overload now fully protoless internally
|
||||
- Uses the new ProvinceViewFilter Scala overload with faction filtering
|
||||
- Converts `battalionTypes` and `chronicleEntries` to proto only at output boundary
|
||||
|
||||
## Key Files
|
||||
|
||||
### Protoless Model Types
|
||||
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/game_state/GameState.scala` - native Scala GameState
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/province/ProvinceView.scala` - province view type
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/faction/FactionView.scala` - faction view type
|
||||
- `src/main/scala/net/eagle0/eagle/model/state/hero/HeroView.scala` - hero view type
|
||||
|
||||
### Proto Converters
|
||||
|
||||
- `src/main/scala/net/eagle0/eagle/model/proto_converters/game_state/` - converts between proto and Scala types
|
||||
|
||||
## Notes
|
||||
|
||||
- The AI client code (`src/main/scala/net/eagle0/eagle/ai/`) is now fully protoless
|
||||
- Proto GameState is still needed at boundaries: persistence, gRPC to Shardok
|
||||
- `PerformUnaffiliatedHeroesAction` and the LLM pipeline use protoless `GameState`
|
||||
- `GameStateViewFilter` Scala overload is now fully protoless internally (converts to proto only at output)
|
||||
- `ProvinceViewFilter` has Scala overloads for all three modes: no filtering, faction filtering, and withdrawn-from view
|
||||
|
||||
---
|
||||
|
||||
## Proto Import Inventory (library/)
|
||||
|
||||
**Total: 149 proto imports across 51 files** (as of 2026-01-13)
|
||||
|
||||
### Proto Dependencies by BUILD.bazel (unique deps)
|
||||
|
||||
#### library/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:game_state_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/shardok/api:action_result_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto`
|
||||
|
||||
#### library/actions/impl/action/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`
|
||||
|
||||
#### library/actions/impl/common/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`
|
||||
|
||||
#### library/actions/llm_prompt_generators/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:event_for_chronicle_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto`
|
||||
|
||||
#### library/actions/llm_request_generators/diplomacy_llm_helpers/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto`
|
||||
|
||||
#### library/settings/loaders/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto`
|
||||
|
||||
#### library/util/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/common:hostility_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api/command/util:army_stats_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:date_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:province_event_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:army_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:battalion_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:faction_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:game_state_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:hero_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:stat_with_condition_scala_proto`
|
||||
|
||||
#### library/util/command_choice_helpers/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/common:hostility_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api/command/util:battalion_with_food_cost_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_scala_proto`
|
||||
|
||||
#### library/util/command_choice_helpers/quest_command_selectors/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:improvement_type_scala_proto`
|
||||
|
||||
#### library/util/faction_utils/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/common:hostility_scala_proto`
|
||||
|
||||
#### library/util/quest_creation/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto`
|
||||
|
||||
#### library/util/quest_fulfillment/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_scala_proto`
|
||||
|
||||
#### library/util/view_filters/BUILD.bazel
|
||||
- `//src/main/protobuf/net/eagle0/common:hostility_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/common:province_event_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:army_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:faction_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:hero_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:army_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:battalion_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:faction_relationship_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:faction_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:game_state_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:hero_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:incoming_army_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:province_view_scala_proto`
|
||||
- `//src/main/protobuf/net/eagle0/eagle/views:shardok_battle_view_scala_proto`
|
||||
|
||||
---
|
||||
|
||||
### Summary by Directory
|
||||
|
||||
| Directory | Files | Imports | Status |
|
||||
|-----------|-------|---------|--------|
|
||||
| `actions/availability/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/impl/action/` | 2 | 3 | Boundary code |
|
||||
| `actions/impl/common/` | 1 | 2 | Boundary code |
|
||||
| `actions/llm_prompt_generators/` | 24 | 46 | LLM request types |
|
||||
| `actions/llm_request_generators/` | 1 | 2 | LLM request types |
|
||||
| `settings/loaders/` | 1 | 1 | Loader (expected) |
|
||||
| `util/` | 9 | 20 | Mixed - cleanup candidates |
|
||||
| `util/command_choice_helpers/` | 17 | 57 | API types |
|
||||
| `util/view_filters/` | 1 | 1 | View boundary |
|
||||
| Root (`library/`) | 3 | 5 | Boundary code |
|
||||
|
||||
---
|
||||
|
||||
### Detailed Inventory
|
||||
|
||||
#### Root library/ (5 imports, 3 files)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `ActionResultFilter.scala` | 3 | `common.action_result_notification_details.Notification` | Action result filtering |
|
||||
| `ActionResultFilter.scala` | 4 | `common.action_result_type.ActionResultType` | |
|
||||
| `ActionResultFilter.scala` | 5 | `common.action_result_type.ActionResultType.*` | |
|
||||
| `Engine.scala` | 6 | `internal.action_result.ActionResult` | Trait interface |
|
||||
| `EngineImpl.scala` | 8 | `internal.action_result.ActionResult` | Returns proto for persistence |
|
||||
|
||||
#### actions/impl/action/ (3 imports, 2 files)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `ChronicleEventGenerator.scala` | 3 | `common.action_result_type.ActionResultType.FACTION_DESTROYED` | Single enum value |
|
||||
| `PerformVassalCommandsPhaseAction.scala` | 5 | `api.available_command.{AvailableCommand, RestAvailableCommand}` | Commands from factory |
|
||||
| `PerformVassalDefenseDecisionsAction.scala` | 4 | `api.available_command.AvailableCommand` | Commands from factory |
|
||||
|
||||
#### actions/impl/common/ (2 imports, 1 file)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `ActionWithResultingState.scala` | 3 | `internal.action_result.ActionResult` | Caches both proto and Scala |
|
||||
| `ActionWithResultingState.scala` | 4 | `internal.game_state.GameState` | |
|
||||
|
||||
#### actions/llm_prompt_generators/ (46 imports, 24 files)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `AllianceOfferMessagePromptGenerator.scala` | 4 | `internal.generated_text_request.AllianceOfferMessage` | LLM request type |
|
||||
| `AllianceResolutionMessagePromptGenerator.scala` | 4 | `common.diplomacy_offer_status.DiplomacyOfferStatus` | |
|
||||
| `AllianceResolutionMessagePromptGenerator.scala` | 5 | `common.diplomacy_offer_status.DiplomacyOfferStatus.{...}` | |
|
||||
| `AllianceResolutionMessagePromptGenerator.scala` | 13 | `internal.generated_text_request.AllianceOfferResolutionMessage` | |
|
||||
| `BattalionDescriptions.scala` | 3 | `common.battalion_type.BattalionTypeId` | |
|
||||
| `BattalionDescriptions.scala` | 4 | `internal.battalion.Battalion` | |
|
||||
| `BreakAllianceMessagePromptGenerator.scala` | 4 | `internal.generated_text_request.BreakAllianceMessage` | |
|
||||
| `BreakAllianceResolutionMessagePromptGenerator.scala` | 4 | `common.diplomacy_offer_status.DiplomacyOfferStatus` | |
|
||||
| `BreakAllianceResolutionMessagePromptGenerator.scala` | 5 | `common.diplomacy_offer_status.DiplomacyOfferStatus.{...}` | |
|
||||
| `BreakAllianceResolutionMessagePromptGenerator.scala` | 13 | `internal.generated_text_request.BreakAllianceResolutionMessage` | |
|
||||
| `CapturedHeroMessagePromptGenerator.scala` | 4 | `internal.generated_text_request.{...}` | |
|
||||
| `ChronicleEventTextGenerator.scala` | 4 | `internal.event_for_chronicle.{...}` | |
|
||||
| `ChronicleEventTextGenerator.scala` | 31 | `internal.event_for_chronicle.ShatteredArmyEvent.Reason.{...}` | |
|
||||
| `ChronicleUpdatePromptGenerator.scala` | 4 | `common.date.Date` | |
|
||||
| `ChronicleUpdatePromptGenerator.scala` | 5 | `internal.event_for_chronicle.EventForChronicle` | |
|
||||
| `ChronicleUpdatePromptGenerator.scala` | 6 | `internal.generated_text_request.ChronicleUpdateMessage` | |
|
||||
| `DivineMessagePromptGenerator.scala` | 4 | `internal.generated_text_request.DivineMessage` | |
|
||||
| `ExileVassalPromptGenerator.scala` | 4 | `internal.generated_text_request.ExileVassalMessage` | |
|
||||
| `GeneratorUtilities.scala` | 13 | `common.date.Date` | |
|
||||
| `HandleCapturedHeroPleaPromptGenerator.scala` | 4 | `internal.generated_text_request.HandleCapturedHeroPlea` | |
|
||||
| `HeroBackstoryUpdatePromptGenerator.scala` | 4 | `common.diplomacy_offer_status.DiplomacyOfferStatus.{...}` | |
|
||||
| `HeroBackstoryUpdatePromptGenerator.scala` | 9 | `internal.event_for_hero_backstory.{...}` | |
|
||||
| `HeroBackstoryUpdatePromptGenerator.scala` | 37 | `internal.event_for_hero_backstory.FoughtInBattleBackstoryEvent.UnitStatus` | |
|
||||
| `HeroBackstoryUpdatePromptGenerator.scala` | 38 | `internal.event_for_hero_backstory.FoughtInBattleBackstoryEvent.UnitStatus.{...}` | |
|
||||
| `HeroBackstoryUpdatePromptGenerator.scala` | 47 | `internal.generated_text_request.HeroBackstoryUpdateRequest` | |
|
||||
| `HeroDeparturePromptGenerator.scala` | 4 | `internal.generated_text_request.HeroDepartureMessage` | |
|
||||
| `HeroInitialBackstoryPromptGenerator.scala` | 4 | `internal.generated_text_request.HeroInitialBackstoryRequest` | |
|
||||
| `InvitationMessagePromptGenerator.scala` | 4 | `internal.generated_text_request.InvitationMessage` | |
|
||||
| `InvitationResolutionMessagePromptGenerator.scala` | 4 | `common.diplomacy_offer_status.DiplomacyOfferStatus` | |
|
||||
| `InvitationResolutionMessagePromptGenerator.scala` | 5 | `common.diplomacy_offer_status.DiplomacyOfferStatus.{...}` | |
|
||||
| `InvitationResolutionMessagePromptGenerator.scala` | 13 | `internal.generated_text_request.InvitationResolutionMessage` | |
|
||||
| `NewFactionHeadPromptGenerator.scala` | 4 | `internal.generated_text_request.NewFactionHeadMessage` | |
|
||||
| `PleaseRecruitMePromptGenerator.scala` | 4 | `internal.generated_text_request.PleaseRecruitMeMessage` | |
|
||||
| `PrisonerExecutedPromptGenerator.scala` | 4 | `internal.generated_text_request.PrisonerExecutedMessage` | |
|
||||
| `PrisonerExiledPromptGenerator.scala` | 4 | `internal.generated_text_request.PrisonerExiledMessage` | |
|
||||
| `PrisonerReleasedPromptGenerator.scala` | 4 | `internal.generated_text_request.PrisonerReleasedMessage` | |
|
||||
| `PrisonerReturnedPromptGenerator.scala` | 4 | `internal.generated_text_request.PrisonerReturnedMessage` | |
|
||||
| `ProfessionGainedPromptGenerator.scala` | 4 | `internal.generated_text_request.ProfessionGainedMessage` | |
|
||||
| `QuestEndedGeneratorUtilities.scala` | 5 | `common.battalion_type.BattalionTypeId` | |
|
||||
| `QuestEndedGeneratorUtilities.scala` | 6 | `common.unaffiliated_hero_quest.{...}` | |
|
||||
| `QuestEndedGeneratorUtilities.scala` | 29 | `common.unaffiliated_hero_quest.QuestDetails.Empty` | |
|
||||
| `QuestFailedPromptGenerator.scala` | 4 | `internal.generated_text_request.QuestFailedMessage` | |
|
||||
| `QuestFulfilledPromptGenerator.scala` | 4 | `internal.generated_text_request.QuestFulfilledMessage` | |
|
||||
| `RansomOfferMessagePromptGenerator.scala` | 4 | `internal.generated_text_request.RansomOfferMessage` | |
|
||||
| `RansomResolutionMessagePromptGenerator.scala` | 4 | `common.diplomacy_offer_status.DiplomacyOfferStatus` | |
|
||||
| `RansomResolutionMessagePromptGenerator.scala` | 5 | `common.diplomacy_offer_status.DiplomacyOfferStatus.{...}` | |
|
||||
| `RansomResolutionMessagePromptGenerator.scala` | 13 | `internal.generated_text_request.RansomResolutionMessage` | |
|
||||
| `RecruitmentRefusedPromptGenerator.scala` | 4 | `internal.generated_text_request.RecruitmentRefusedMessage` | |
|
||||
| `SuppressBeastsPromptGenerator.scala` | 4 | `internal.generated_text_request.{...}` | |
|
||||
| `SwearBrotherhoodPromptGenerator.scala` | 3 | `common.gender.Gender.{...}` | |
|
||||
| `SwearBrotherhoodPromptGenerator.scala` | 4 | `internal.generated_text_request.SwearBrotherhoodMessage` | |
|
||||
| `TruceOfferMessagePromptGenerator.scala` | 4 | `internal.generated_text_request.TruceOfferMessage` | |
|
||||
| `TruceResolutionMessagePromptGenerator.scala` | 4 | `common.diplomacy_offer_status.DiplomacyOfferStatus` | |
|
||||
| `TruceResolutionMessagePromptGenerator.scala` | 5 | `common.diplomacy_offer_status.DiplomacyOfferStatus.{...}` | |
|
||||
| `TruceResolutionMessagePromptGenerator.scala` | 13 | `internal.generated_text_request.TruceResolutionMessage` | |
|
||||
|
||||
#### actions/llm_request_generators/ (2 imports, 1 file)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `DiplomacyResolutionLlmRequestGenerator.scala` | 4 | `common.diplomacy_offer_status.DiplomacyOfferStatus` | |
|
||||
| `DiplomacyResolutionLlmRequestGenerator.scala` | 5 | `internal.generated_text_request.{...}` | |
|
||||
|
||||
#### settings/loaders/ (1 import, 1 file)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `BattalionTypeLoader.scala` | 8 | `common.battalion_type.{BattalionType, BattalionTypeId}` | Loads from file, converts immediately |
|
||||
|
||||
#### util/ (20 imports, 9 files)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `ArmyUtils.scala` | 3 | `internal.army.{Army, HostileArmyGroup, MovingArmy}` | Has Scala overloads |
|
||||
| `ArmyUtils.scala` | 4 | `internal.game_state.GameState` | |
|
||||
| `CommandSelection.scala` | 4 | `api.available_command.AvailableCommand` | Wraps proto command |
|
||||
| `CommandSelection.scala` | 5 | `api.selected_command.SelectedCommand` | |
|
||||
| `DateProtoUtils.scala` | 5 | `common.date.Date` | Conversion utility |
|
||||
| `GameStateViewDiffer.scala` | 3 | `common.round_phase.NewRoundPhase` | View diff for client |
|
||||
| `IDable.scala` | 4 | `internal.battalion.Battalion` | Test helper only |
|
||||
| `IDable.scala` | 5 | `internal.faction.Faction` | |
|
||||
| `IDable.scala` | 6 | `internal.hero.Hero` | |
|
||||
| `IDable.scala` | 7 | `internal.province.Province` | |
|
||||
| `IncomingArmyUtils.scala` | 4 | `api.command.util.army_stats.ArmyStats` | Returns proto type |
|
||||
| `IncomingArmyUtils.scala` | 5 | `internal.army.{Army, MovingArmy}` | Has Scala overloads |
|
||||
| `IncomingArmyUtils.scala` | 6 | `internal.game_state.GameState` | |
|
||||
| `IncomingArmyUtils.scala` | 7 | `internal.province.Province` | |
|
||||
| `MapGenerator.scala` | 6 | `internal.province.{Neighbor, Province}` | Map generation |
|
||||
| `ProvinceEventUtils.scala` | 3 | `common.beast_info.BeastInfo` | |
|
||||
| `ProvinceEventUtils.scala` | 4 | `common.province_event.*` | |
|
||||
|
||||
#### util/command_choice_helpers/ (57 imports, 17 files)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `AllianceOfferCommandSelector.scala` | 4 | `api.available_command.{AvailableCommand, DiplomacyAvailableCommand}` | |
|
||||
| `AllianceOfferCommandSelector.scala` | 5 | `api.command.util.diplomacy_option.{AllianceOption, DiplomacyOption}` | |
|
||||
| `AllianceOfferCommandSelector.scala` | 6 | `api.selected_command.DiplomacySelectedCommand` | |
|
||||
| `AlmsCommandSelector.scala` | 4 | `api.available_command.{AlmsAvailableCommand, AvailableCommand}` | |
|
||||
| `AlmsCommandSelector.scala` | 5 | `api.selected_command.AlmsSelectedCommand` | |
|
||||
| `AttackCommandChooser.scala` | 4 | `api.available_command.*` | |
|
||||
| `AttackCommandChooser.scala` | 5 | `api.selected_command.*` | |
|
||||
| `AttackCommandChooser.scala` | 6 | `common.combat_unit.CombatUnit` | |
|
||||
| `AttackDecisionCommandChooser.scala` | 5 | `api.available_command.{AttackDecisionAvailableCommand, AvailableCommand}` | |
|
||||
| `AttackDecisionCommandChooser.scala` | 6 | `api.command.util.attack_decision_type.{...}` | |
|
||||
| `AttackDecisionCommandChooser.scala` | 12 | `api.selected_command.AttackDecisionSelectedCommand` | |
|
||||
| `AttackDecisionCommandChooser.scala` | 13 | `common.tribute_amount.TributeAmount` | |
|
||||
| `AvailableCommandSelector.scala` | 6 | `api.available_command.AvailableCommand` | |
|
||||
| `CombatUnitSelector.scala` | 5 | `common.combat_unit.CombatUnit` | |
|
||||
| `CommandChoiceHelpers.scala` | 5 | `api.available_command.*` | |
|
||||
| `CommandChoiceHelpers.scala` | 6 | `api.command.util.armed_battalion.ArmedBattalion` | |
|
||||
| `CommandChoiceHelpers.scala` | 7 | `api.command.util.attack_decision_type.{AdvanceDecision, WithdrawDecision}` | |
|
||||
| `CommandChoiceHelpers.scala` | 8 | `api.command.util.captured_hero_option.CapturedHeroOption.{...}` | |
|
||||
| `CommandChoiceHelpers.scala` | 13 | `api.command.util.diplomacy_option.RansomOfferOption` | |
|
||||
| `CommandChoiceHelpers.scala` | 14 | `api.selected_command.*` | |
|
||||
| `CommandChoiceHelpers.scala` | 15 | `api.selected_command.OrganizeTroopsSelectedCommand.{ChangedBattalion, NewBattalion}` | |
|
||||
| `CommandChoiceHelpers.scala` | 16 | `common.battalion_type.BattalionTypeId` | |
|
||||
| `CommandChoiceHelpers.scala` | 17 | `common.combat_unit.CombatUnit` | |
|
||||
| `CommandChoiceHelpers.scala` | 18 | `common.improvement_type.ImprovementType.INFRASTRUCTURE` | |
|
||||
| `CommandChoiceHelpers.scala` | 19 | `common.tribute_amount.TributeAmount` | |
|
||||
| `CommandChooser.scala` | 6 | `api.available_command.AvailableCommand` | |
|
||||
| `ExileVassalCommandSelector.scala` | 4 | `api.available_command.{AvailableCommand, ExileVassalAvailableCommand}` | |
|
||||
| `ExileVassalCommandSelector.scala` | 5 | `api.selected_command.ExileVassalSelectedCommand` | |
|
||||
| `ExpandCommandSelector.scala` | 7 | `api.available_command.{AvailableCommand, MarchAvailableCommand, MarchCommandFromOneProvince}` | |
|
||||
| `ExpandCommandSelector.scala` | 8 | `api.selected_command.MarchSelectedCommand` | |
|
||||
| `ExpandCommandSelector.scala` | 9 | `common.combat_unit.CombatUnit` | |
|
||||
| `FulfillQuestsCommandSelector.scala` | 4 | `api.available_command.AvailableCommand` | |
|
||||
| `HeroGiftCommandSelector.scala` | 4 | `api.available_command.{AvailableCommand, HeroGiftAvailableCommand}` | |
|
||||
| `HeroGiftCommandSelector.scala` | 5 | `api.selected_command.HeroGiftSelectedCommand` | |
|
||||
| `ImproveCommandSelector.scala` | 4 | `api.available_command.{AvailableCommand, ImproveAvailableCommand}` | |
|
||||
| `ImproveCommandSelector.scala` | 5 | `api.selected_command.ImproveSelectedCommand` | |
|
||||
| `ImproveCommandSelector.scala` | 6 | `common.improvement_type.ImprovementType` | |
|
||||
| `ImproveCommandSelector.scala` | 7 | `common.improvement_type.ImprovementType.DEVASTATION` | |
|
||||
| `OrganizeCommandSelector.scala` | 5 | `api.available_command.{AvailableCommand, OrganizeTroopsAvailableCommand}` | |
|
||||
| `OrganizeCommandSelector.scala` | 6 | `api.selected_command.OrganizeTroopsSelectedCommand` | |
|
||||
| `OrganizeCommandSelector.scala` | 7 | `api.selected_command.OrganizeTroopsSelectedCommand.{ChangedBattalion, NewBattalion}` | |
|
||||
| `OrganizeCommandSelector.scala` | 8 | `common.battalion_type.BattalionTypeId` | |
|
||||
| `RansomOfferHelpers.scala` | 3 | `common.diplomacy_offer.RansomOfferDetails` | |
|
||||
| `RansomOfferHelpers.scala` | 4 | `common.diplomacy_offer_status.DiplomacyOfferStatus` | |
|
||||
| `RansomOfferHelpers.scala` | 5 | `common.diplomacy_offer_status.DiplomacyOfferStatus.{...}` | |
|
||||
| `SwearBrotherhoodCommandSelector.scala` | 4 | `api.available_command.{AvailableCommand, SwearBrotherhoodAvailableCommand}` | |
|
||||
| `SwearBrotherhoodCommandSelector.scala` | 5 | `api.selected_command.SwearBrotherhoodSelectedCommand` | |
|
||||
| `TruceOfferCommandSelector.scala` | 4 | `api.available_command.{AvailableCommand, DiplomacyAvailableCommand}` | |
|
||||
| `TruceOfferCommandSelector.scala` | 5 | `api.command.util.diplomacy_option.{DiplomacyOption, TruceOption}` | |
|
||||
| `TruceOfferCommandSelector.scala` | 6 | `api.selected_command.DiplomacySelectedCommand` | |
|
||||
|
||||
#### util/command_choice_helpers/quest_command_selectors/ (12 imports, 10 files)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `AllianceQuestCommandChooser.scala` | 4 | `api.available_command.{AvailableCommand, DiplomacyAvailableCommand}` | |
|
||||
| `AllianceQuestCommandChooser.scala` | 5 | `api.command.util.diplomacy_option.AllianceOption` | |
|
||||
| `AlmsAcrossRealmQuestCommandChooser.scala` | 2 | `api.available_command.AvailableCommand` | |
|
||||
| `AlmsToProvinceQuestCommandChooser.scala` | 3 | `api.available_command.AvailableCommand` | |
|
||||
| `DismissSpecificVassalCommandChooser.scala` | 5 | `api.available_command.AvailableCommand` | |
|
||||
| `GiveToHeroesAcrossRealmQuestCommandChooser.scala` | 2 | `api.available_command.AvailableCommand` | |
|
||||
| `GiveToHeroesInProvinceQuestCommandChooser.scala` | 4 | `api.available_command.AvailableCommand` | |
|
||||
| `ImproveQuestCommandChooser.scala` | 3 | `api.available_command.AvailableCommand` | |
|
||||
| `ImproveQuestCommandChooser.scala` | 4 | `common.improvement_type.ImprovementType.{...}` | |
|
||||
| `QuestCommandChooser.scala` | 4 | `api.available_command.AvailableCommand` | |
|
||||
| `TruceCountQuestCommandChooser.scala` | 4 | `api.available_command.AvailableCommand` | |
|
||||
| `TruceWithFactionQuestCommandChooser.scala` | 3 | `api.available_command.AvailableCommand` | |
|
||||
|
||||
#### util/view_filters/ (1 import, 1 file)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `ProvinceViewFilter.scala` | 3 | `common.province_event.ProvinceEvent` | For knownEvents field |
|
||||
|
||||
#### util/province/ (1 import, 1 file)
|
||||
|
||||
| File | Line | Import | Notes |
|
||||
|------|------|--------|-------|
|
||||
| `ProvinceUtils.scala` | 4 | `common.battalion_type.BattalionType` | Single proto reference |
|
||||
|
||||
---
|
||||
|
||||
### Cleanup Candidates (Priority Order)
|
||||
|
||||
1. **Delete proto overloads where Scala overloads exist and proto unused**
|
||||
- `IncomingArmyUtils.scala` - verify all main callers use Scala overloads
|
||||
- `ArmyUtils.scala` - verify all main callers use Scala overloads
|
||||
|
||||
2. **Move test-only utilities to test code**
|
||||
- `IDable.scala` - only used by tests (except StartGameActionResultUtils)
|
||||
|
||||
3. **Create Scala versions of common enums**
|
||||
- `DiplomacyOfferStatus` → already done in availability package
|
||||
- `ImprovementType` → used by ImproveCommandSelector, CommandChoiceHelpers
|
||||
- `BattalionTypeId` → used by multiple command selectors
|
||||
|
||||
4. **Large refactoring (requires Scala AvailableCommand/SelectedCommand)**
|
||||
- `command_choice_helpers/` package (57 imports) - uses proto API command types
|
||||
- Would need Scala versions of AvailableCommand and SelectedCommand hierarchies
|
||||
@@ -130,6 +130,13 @@ bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_a
|
||||
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "rules_swift", version = "2.4.0", repo_name = "build_bazel_rules_swift")
|
||||
|
||||
# Register Apple CC toolchain for Objective-C compilation
|
||||
apple_cc_configure = use_extension(
|
||||
"@build_bazel_apple_support//crosstool:setup.bzl",
|
||||
"apple_cc_configure_extension",
|
||||
)
|
||||
use_repo(apple_cc_configure, "local_config_apple_cc")
|
||||
|
||||
#
|
||||
# Protocol Buffers & RPC
|
||||
#
|
||||
@@ -293,6 +300,17 @@ http_archive(
|
||||
],
|
||||
)
|
||||
|
||||
# Sparkle framework for macOS auto-updates
|
||||
SPARKLE_VERSION = "2.6.4"
|
||||
|
||||
http_archive(
|
||||
name = "sparkle",
|
||||
build_file = "@//external:BUILD.sparkle",
|
||||
sha256 = "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b",
|
||||
strip_prefix = "",
|
||||
url = "https://github.com/sparkle-project/Sparkle/releases/download/%s/Sparkle-%s.tar.xz" % (SPARKLE_VERSION, SPARKLE_VERSION),
|
||||
)
|
||||
|
||||
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
|
||||
# Primary: DigitalOcean Spaces (public, reliable)
|
||||
# Fallback: busybox.net (can be unreliable/slow)
|
||||
|
||||
Generated
+1
-1
@@ -396,7 +396,7 @@
|
||||
"@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "Z3yAd66IJL0GAZUTSeMOjoHiE1SZPPwiIs/XQui5BvE=",
|
||||
"usagesDigest": "tl3VVeQX3Hzh7FhM2gjnkCwEJpRMlY5S6a850WY/xvc=",
|
||||
"usagesDigest": "DqQsfZN5lA8z+nLEEY+EpKGzQ8M73mDm/A8lofDSyus=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
|
||||
@@ -9,9 +9,6 @@ COMMIT=$(/usr/bin/git rev-parse --short HEAD)
|
||||
/bin/echo "build protos"
|
||||
./scripts/build_protos.sh
|
||||
|
||||
/bin/echo "build plugins"
|
||||
./scripts/build_windows_plugin.sh
|
||||
|
||||
git log -3
|
||||
|
||||
/bin/echo "build Windows"
|
||||
|
||||
@@ -7,8 +7,8 @@ COMMIT=$(/usr/bin/git rev-parse --short HEAD)
|
||||
/bin/echo "build protos"
|
||||
./scripts/build_protos.sh
|
||||
|
||||
/bin/echo "build Mac plugin"
|
||||
./scripts/build_mac_plugin.sh
|
||||
/bin/echo "build Sparkle plugin"
|
||||
./scripts/build_sparkle_plugin.sh
|
||||
|
||||
git log -3
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Persist Unity Library/ cache to persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
#
|
||||
# Note: Library/Bee/ is excluded because it contains DAG files with hardcoded
|
||||
# file paths that become stale when project files change. This prevents
|
||||
# "Data at the root level is invalid" XML errors from stale references.
|
||||
|
||||
set -uxo pipefail
|
||||
|
||||
/bin/echo "persist Library/"
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "persist Library/ to $CACHE_DIR (excluding Bee/)"
|
||||
|
||||
# rsync may exit with code 23 ("partial transfer due to error") if Unity's
|
||||
# temporary files vanish during the copy. This is acceptable for a cache.
|
||||
/usr/bin/rsync -rtlDvq src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ /tmp/eagle0/Library/
|
||||
/usr/bin/rsync -rtlDvq --exclude='Bee/' src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ "$CACHE_DIR/"
|
||||
rsync_exit=$?
|
||||
|
||||
if [ $rsync_exit -eq 0 ]; then
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restore Unity Library/ cache from persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
/bin/echo "restore Library/"
|
||||
/bin/mkdir -p /tmp/eagle0/Library
|
||||
/usr/bin/rsync -rtlDvq /tmp/eagle0/Library/ src/main/csharp/net/eagle0/clients/unity/eagle0/Library/
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "restore Library/ from $CACHE_DIR"
|
||||
/bin/mkdir -p "$CACHE_DIR"
|
||||
/usr/bin/rsync -rtlDvq "$CACHE_DIR/" src/main/csharp/net/eagle0/clients/unity/eagle0/Library/
|
||||
|
||||
+198
-300
@@ -33,7 +33,23 @@
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
## Current State (January 2026)
|
||||
|
||||
### Summary
|
||||
|
||||
| Area | Proto Imports | Status |
|
||||
|------|---------------|--------|
|
||||
| AI (`/ai/`) | **0** | ✅ **Complete** |
|
||||
| Actions (`/library/actions/impl/action/`) | **0** | ✅ **Complete** |
|
||||
| Commands (`/library/actions/impl/command/`) | **0** | ✅ **Complete** |
|
||||
| Availability (`/library/actions/availability/`) | **0** | ✅ **Complete** |
|
||||
| Command Choice Helpers (`/library/util/command_choice_helpers/`) | **3** | ✅ Near Complete (1 file) |
|
||||
| View Filters (`/library/util/view_filters/`) | **4** | ⏳ In progress (3 files) |
|
||||
| LLM Prompt Generators (`/library/actions/llm_prompt_generators/`) | **56** | ⏳ Remaining work (34 files) |
|
||||
| Other Utilities (`/library/util/`) | **13** | ⏳ Remaining work (5 files) |
|
||||
| Root Library (`/library/`) | **9** | Boundary code (3 files) |
|
||||
|
||||
**Total: ~85 proto imports remaining** (down from 149)
|
||||
|
||||
### Completed Phases
|
||||
|
||||
@@ -43,346 +59,228 @@
|
||||
| Phase 2: EngineImpl | **Complete** | Holds Scala `GameState` internally |
|
||||
| Phase 3: GameHistory | **Complete** | `stateAfter` returns Scala GameState |
|
||||
| Phase 4: ActionResultT | **Complete** | All 59 actions return `ActionResultT` |
|
||||
| Phase 5: Action Base Classes | **Complete** | All `RandomSequentialResultsAction` and `DeterministicSingleResultAction` converted to T-type base classes |
|
||||
| Phase 5b: Base Class Cleanup | **Complete** | `RandomSequentialResultsAction` and `DeterministicSingleResultAction` deleted |
|
||||
| Phase 5c: RoundPhaseAdvancer Actions | **Complete** | All actions called by RoundPhaseAdvancer accept Scala GameState |
|
||||
| Phase 5d: RoundPhaseAdvancer Itself | **Complete** | RoundPhaseAdvancer.checkForPhaseAdvancement takes Scala GameState |
|
||||
| Phase 5: Action Base Classes | **Complete** | All actions converted to T-type base classes |
|
||||
| Phase 5b-d: RoundPhaseAdvancer | **Complete** | Uses Scala GameState throughout |
|
||||
| Phase 6: Core Engine | **Complete** | ActionResultApplier, RandomStateSequencer protoless |
|
||||
| Phase 6b: CommandFactory | **Complete** | Uses Scala SelectedCommand and AvailableCommand |
|
||||
| Phase 6c: AI Clients | **Complete** | AIClient, command choosers all protoless |
|
||||
| Phase 6d: CommandSelection | **Complete** | Renamed ScalaCommandSelection → CommandSelection |
|
||||
|
||||
### Phase 5c/5d Progress (Complete)
|
||||
### Recent Completions (PRs #5326, #5330, #5332, #5333, #5334, #5336)
|
||||
|
||||
`RoundPhaseAdvancer.checkForPhaseAdvancement` now accepts Scala `GameState` and `ActionResultApplier` directly (PR #4677).
|
||||
|
||||
| Action | PR | Status |
|
||||
|--------|-----|--------|
|
||||
| `PrisonerExchangeAction` | #4670 | ✅ Merged |
|
||||
| `PerformForcedTurnBackAction` | #4671 | ✅ Merged |
|
||||
| `PerformHeroDeparturesAction` | #4672 | ✅ Merged |
|
||||
| `RequestFreeForAllBattlesAction` | #4673 | ✅ Merged |
|
||||
| `EndPlayerCommandsPhaseAction` | #4674 | ✅ Merged |
|
||||
| `EndDiplomacyResolutionPhaseAction` | #4675 | ✅ Merged |
|
||||
| `RoundPhaseAdvancer` itself | #4677 | ✅ Merged |
|
||||
|
||||
### EngineImpl Progress
|
||||
|
||||
| Change | PR | Status |
|
||||
|--------|-----|--------|
|
||||
| `recursiveTransform` deleted | #4677 | ✅ Merged |
|
||||
| `recursiveTransformT` uses `RandomStateTSequencer` | #4677 | ✅ Merged |
|
||||
|
||||
### Current Architecture
|
||||
|
||||
**ActionResultT Production (100% Complete):**
|
||||
- All actions produce `ActionResultT`
|
||||
- Conversion to `ActionResultProto` happens via `ActionResultProtoConverter.toProto()`
|
||||
- No direct `ActionResultProto` construction outside the converter
|
||||
|
||||
**ActionResultProto Consumption (Next Target):**
|
||||
- `ActionResultProtoApplierImpl` - applies proto results to proto GameState
|
||||
- `RoundPhaseAdvancer` - calls converter, passes protos to applier
|
||||
- `InMemoryHistory` / `PersistedHistory` - stores proto results
|
||||
- Service layer (`GameController`, `GamesManager`, etc.) - uses proto for client communication
|
||||
1. **CommandFactory protoless** - Now accepts/returns Scala `SelectedCommand` types
|
||||
2. **AI clients protoless** - `AIClient`, `MidGameAIClient`, all command choosers use Scala types
|
||||
3. **CommandSelection cleanup** - Deleted legacy proto-based `CommandSelection`, renamed `ScalaCommandSelection` → `CommandSelection`
|
||||
4. **AvailableCommandsFactory protoless** - Returns Scala `OneProvinceAvailableCommands`
|
||||
5. **All CommandChoiceHelpers protoless** - ~25 selector/chooser files converted (only RansomOfferHelpers remains)
|
||||
6. **ChronicleEventGenerator protoless** (PR #5332) - Removed last proto enum import from Actions layer
|
||||
7. **ProvinceUtils protoless** (PR #5333) - Converted to use Scala `BattalionType`
|
||||
8. **FactionViewFilter protoless** (PR #5334) - Returns Scala `FactionView`, converts to proto at edge
|
||||
9. **HeroViewFilter protoless** (PR #5336) - Returns Scala `HeroView`, converts to proto at edge
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Migrate to ActionResultT Consumers
|
||||
## Remaining Work
|
||||
|
||||
### Objective
|
||||
### Phase 7: View Filters (~4 proto imports remaining in 3 files)
|
||||
|
||||
Eliminate internal consumption of `ActionResultProto`. Everything inside the engine should work with `ActionResultT`.
|
||||
| File | Status | Migration Path |
|
||||
|------|--------|----------------|
|
||||
| `FactionViewFilter.scala` | ✅ **Complete** | Returns Scala `FactionView` (PR #5334) |
|
||||
| `HeroViewFilter.scala` | ✅ **Complete** | Returns Scala `HeroView` (PR #5336) |
|
||||
| `GameStateViewFilter.scala` | Partial | Converts views at edge; uses `FactionViewProto`, `GameStateView` |
|
||||
| `ProvinceViewFilter.scala` | Partial | Uses `ProvinceEvent` proto for `knownEvents` field |
|
||||
| `BattleFilter.scala` | Keep | Uses `ShardokBattleView` (boundary code for battles) |
|
||||
|
||||
### Current Flow (Proto-Heavy)
|
||||
```
|
||||
Action.execute()
|
||||
→ ActionResultT
|
||||
→ ActionResultProtoConverter.toProto()
|
||||
→ ActionResultProto
|
||||
→ ActionResultProtoApplierImpl.applyActionResults()
|
||||
→ GameStateProto
|
||||
→ GameStateConverter.fromProto()
|
||||
→ GameStateC
|
||||
```
|
||||
**Pattern established**: Create Scala view type → Create converter → Update filter to return Scala type → Convert at edge in `GameStateViewFilter`.
|
||||
|
||||
### Target Flow (T-Types Throughout)
|
||||
```
|
||||
Action.execute()
|
||||
→ ActionResultT
|
||||
→ ActionResultApplier.applyActionResults()
|
||||
→ GameStateC
|
||||
### Phase 8: LLM Prompt Generators (~56 proto imports in 34 files)
|
||||
|
||||
(Proto conversion only at boundaries)
|
||||
```
|
||||
The LLM prompt generators still use proto types for hero/faction/province data:
|
||||
|
||||
### Key Files to Convert
|
||||
| File Category | Files | Proto Usage |
|
||||
|---------------|-------|-------------|
|
||||
| Diplomacy prompts | 12 | `Hero`, `Faction`, `Province` protos |
|
||||
| Quest prompts | 4 | `Hero`, `Faction` protos |
|
||||
| Chronicle prompts | 3 | `Hero`, `Faction`, `Province` protos |
|
||||
| Other prompts | 15 | Various proto types |
|
||||
|
||||
**Tier 1 - Core Applier:** ✅ **Complete**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/applier/ActionResultApplierImpl.scala
|
||||
```
|
||||
`ActionResultApplier` applies `ActionResultT` directly to Scala `GameState`. The legacy `ActionResultTApplierImpl` wraps it and converts to/from proto for callers that still need proto types.
|
||||
**Migration strategy**: These files generate text for LLM prompts. They can be migrated to accept Scala types (`HeroT`, `FactionT`, `ProvinceT`) with converters at the call sites if needed.
|
||||
|
||||
**Tier 2 - RoundPhaseAdvancer:** ✅ **Complete**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/RoundPhaseAdvancer.scala
|
||||
```
|
||||
Now accepts Scala `GameState` and `ActionResultApplier`. Only converts to proto lazily for `AvailableCommandsFactory` calls.
|
||||
**Priority**: Medium - These don't block other migrations and are isolated.
|
||||
|
||||
**Tier 3 - Sequencers:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateTSequencer.scala
|
||||
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateProtoSequencer.scala
|
||||
```
|
||||
Modify `RandomStateTSequencer` to thread Scala `GameState` throughout (currently converts to proto internally). Then evaluate whether `RandomStateProtoSequencer` is still needed at all.
|
||||
### Phase 9: Remaining Utilities (~13 proto imports in 5 files)
|
||||
|
||||
**Current State**: `RandomStateTSequencer` accepts Scala `GameState` via its `apply()` method but internally converts to proto. All callback methods (`withRandomActionResult`, `withActionResults`, etc.) pass `GameStateProto` to callers, forcing actions that use the sequencer to work with proto types internally.
|
||||
| File | Proto Usage | Migration Path |
|
||||
|------|-------------|----------------|
|
||||
| `ProvinceEventUtils.scala` | 2 imports | Convert to Scala types |
|
||||
| `MapGenerator.scala` | 1 import | Convert to Scala types |
|
||||
| `IncomingArmyUtils.scala` | 1 import | Convert to Scala types |
|
||||
| `GameStateViewDiffer.scala` | 7 imports | Keep - works with client view protos (boundary) |
|
||||
| `StatWithConditionUtils.scala` | 2 imports | Convert to Scala types |
|
||||
|
||||
**Target State**: Create a fully protoless sequencer where:
|
||||
1. `lastState` returns Scala `GameState` (not `lastStateProto`)
|
||||
2. All callback methods pass Scala `GameState` to callers
|
||||
3. Actions using the sequencer can be fully protoless
|
||||
### Phase 10: History APIs
|
||||
|
||||
**Migration Path**:
|
||||
1. Add `lastState: GameState` method alongside `lastStateProto` (non-breaking)
|
||||
2. Add parallel callback methods that pass Scala GameState (e.g., `withScalaActionResult`)
|
||||
3. Migrate actions one by one to use the new Scala-based callbacks
|
||||
4. Once all actions migrated, deprecate/remove proto-based callbacks
|
||||
5. Remove `lastStateProto` once no longer used
|
||||
|
||||
**RandomStateSequencer Migration Progress** (PR #4679 introduced protoless `RandomStateSequencer`):
|
||||
|
||||
| Action | Status |
|
||||
|--------|--------|
|
||||
| `TruceTurnBackPhaseAction` | ✅ Migrated (PR #4680) |
|
||||
| `EndHandleRiotsPhaseAction` | ✅ Migrated (PR #4684) |
|
||||
| `PerformVassalCommandsPhaseAction` | ✅ Migrated |
|
||||
| `PerformVassalDefenseDecisionsAction` | ✅ Migrated |
|
||||
| `EndVassalCommandsPhaseAction` | ✅ Migrated |
|
||||
| `PerformReconResolutionAction` | ✅ Migrated |
|
||||
| `NewRoundAction` | ✅ Migrated (PR #4698) |
|
||||
| `EndBattleAftermathPhaseAction` | ✅ Migrated (PR #4699) |
|
||||
| `EndDiplomacyResolutionPhaseAction` | ✅ Migrated |
|
||||
| `PerformUnaffiliatedHeroesAction` | ✅ Migrated |
|
||||
| `EngineImpl.recursiveTransformT` | ✅ Migrated (PR #4704) |
|
||||
| `ProtolessSequentialResultsActionWrapper` | ✅ Migrated (PR #4705) |
|
||||
| `LegacyRandomStateTSequencer` | ✅ **Deleted** (PR #4705) |
|
||||
|
||||
**TCommandFactory Extraction** (PR #4684):
|
||||
|
||||
To enable lightweight mocking of command creation in tests, `TCommandFactory` trait was extracted from `CommandFactory`. This allows tests to mock just the `makeTCommand` method without pulling in all 40+ command dependencies that `CommandFactory` requires.
|
||||
|
||||
- `TCommandFactory` - lightweight trait with just `makeTCommand`
|
||||
- `CommandFactory extends TCommandFactory` - maintains backward compatibility
|
||||
- Actions accepting command factories now use `TCommandFactory` type for better testability
|
||||
|
||||
**Tier 4 - History APIs:**
|
||||
```
|
||||
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
|
||||
src/main/scala/net/eagle0/eagle/service/PersistedHistory.scala
|
||||
```
|
||||
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions. `PersistedHistory` converts to proto internally for disk persistence; `InMemoryHistory` doesn't need proto at all.
|
||||
|
||||
### ActionResultProto Consumer Inventory
|
||||
|
||||
| File | Usage | Status |
|
||||
|------|-------|--------|
|
||||
| `ActionResultApplierImpl.scala` | Applies ActionResultT to Scala GameState | ✅ **Complete** |
|
||||
| `ActionResultTApplierImpl.scala` | Legacy wrapper - converts to/from proto | Keep until all callers migrated |
|
||||
| `RoundPhaseAdvancer.scala` | Uses Scala GameState | ✅ **Complete** |
|
||||
| `RandomStateSequencer.scala` | Threads Scala GameState | ✅ **Complete** |
|
||||
| `VigorXPApplier.scala` | Has both proto and Scala methods | Scala method exists, delete proto method when unused |
|
||||
| `PerformForcedTurnBackAction.scala` | Fully protoless | ✅ **Complete** |
|
||||
| `ResolveBattleAction.scala` | ✅ Fully protoless | Complete (PR #5048) |
|
||||
| `InMemoryHistory.scala` | Stores proto results | Pending - vend Scala types |
|
||||
| `PersistedHistory.scala` | Stores proto results | Pending - vend Scala types, convert for disk |
|
||||
| `GameController.scala` | Uses proto for client communication | Keep proto (gRPC boundary) |
|
||||
|
||||
### Remaining Proto Usage in Actions
|
||||
|
||||
**Progress: 52 of 52 action files (100%) are fully protoless.** ✅
|
||||
|
||||
All action files have been migrated to use Scala types:
|
||||
|
||||
| Action | Status | Notes |
|
||||
|--------|--------|-------|
|
||||
| `ResolveBattleAction` | ✅ Complete | PR #5048 - uses Scala GameState and ActionResultApplier |
|
||||
| `PerformVassalCommandsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
|
||||
| `EndHandleRiotsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
|
||||
| `PerformVassalDefenseDecisionsAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
|
||||
| `EndVassalCommandsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
|
||||
|
||||
**Note:** `NewRoundAction` is now fully protoless after converting `ChronicleEventGenerator` to return Scala `ChronicleEvent` types directly.
|
||||
|
||||
**Deleted Dead Code:**
|
||||
- `UnaffiliatedHeroMovedAction` - Was never called; `PerformUnaffiliatedHeroesAction.heroMovedResult` constructs `ActionResultC` directly
|
||||
- `HeroBackstoryUpdateActionGenerator.fromGameState` - Dead method that converted proto to Scala; only `apply(GameState)` is used
|
||||
|
||||
**Note**: `PerformReconResolutionAction` and `EndBattleAftermathPhaseAction` are now fully protoless after:
|
||||
1. Migrating `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` to use Scala `ProvinceView`
|
||||
2. Adding Scala overload of `ProvinceViewFilter.withdrawnFromProvinceView`
|
||||
|
||||
### Estimated Effort (Remaining)
|
||||
|
||||
| Component | Lines | Complexity | Blocks |
|
||||
|-----------|-------|------------|--------|
|
||||
| History API updates | ~100 | Low | - |
|
||||
| **Total Remaining** | **~100** | | |
|
||||
|
||||
**Completed:**
|
||||
- ✅ `ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
|
||||
- ✅ `CommandChoiceHelpers` migrated to Scala types
|
||||
- ✅ `ResolveBattleAction` refactored to use Scala GameState and ActionResultApplier (PR #5048)
|
||||
|
||||
### Enum Type Migrations
|
||||
|
||||
Proto enums are being converted to Scala sealed traits with converters at boundaries:
|
||||
|
||||
| Enum | Scala Type | Status | Notes |
|
||||
|------|------------|--------|-------|
|
||||
| `DiplomacyOfferStatus` | `Status` sealed trait | ✅ **Complete** | PR #5093 - `EligibleDiplomacyStatuses` uses Scala types internally |
|
||||
| `RoundPhase` | `RoundPhase` sealed trait | Partial | Some usages converted |
|
||||
| `BattalionType` | `BattalionType` sealed trait | Partial | Some usages converted |
|
||||
|
||||
**DiplomacyOfferStatus Migration (PR #5093):**
|
||||
- `EligibleDiplomacyStatuses.scala` now returns `Vector[Status]` instead of `Vector[DiplomacyOfferStatus]`
|
||||
- Call sites in `AvailableResolve*CommandFactory` files convert to proto via `StatusConverter.toProto` at the boundary
|
||||
- This pattern should be applied to other proto enums
|
||||
|
||||
### CommandChoiceHelpers Migration Status
|
||||
|
||||
Several command selectors have already been converted to use Scala types:
|
||||
|
||||
| File | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| `AttackCommandChooser.scala` | ✅ **Protoless** | Uses Scala `GameState`, `HeroT`, `ProvinceT` |
|
||||
| `AlmsCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `HeroT`, `ProvinceT` |
|
||||
| `FoodConsumptionUtils.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `RoundPhase` |
|
||||
| `MarchSuppliesHelpers.scala` | ✅ **Protoless** | Uses `BattalionT` |
|
||||
| `CombatUnitSelector.scala` | ✅ **Protoless** | Uses `HeroT`, `BattalionT`, `BattalionType` |
|
||||
| `ExpandCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `FactionT` |
|
||||
| `ImproveCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `HeroT` |
|
||||
| `OrganizeCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `BattalionT`, `BattalionType` |
|
||||
| `RansomOfferHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState`, `FactionT` |
|
||||
| `CommandChoiceHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState` throughout |
|
||||
| `ProvinceGoldSurplusCalculator.scala` | ✅ **Protoless** | Uses Scala types |
|
||||
|
||||
**All CommandChoiceHelpers selectors have been migrated to Scala types.** ✅
|
||||
|
||||
### Progress Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Action files fully protoless | 52 / 52 (100%) ✅ |
|
||||
| Proto usages in remaining actions | 0 |
|
||||
| Next target | See "Next Candidates" section below |
|
||||
|
||||
### Next Candidates
|
||||
|
||||
Priority candidates for further deproto work:
|
||||
|
||||
1. **More Enum Migrations** - Apply the `DiplomacyOfferStatus` pattern to other proto enums:
|
||||
- Files importing `net.eagle0.eagle.common.round_phase.RoundPhase` (proto) could use Scala `RoundPhase`
|
||||
- Files importing `net.eagle0.eagle.common.battalion_type.BattalionType` (proto) could use Scala `BattalionType`
|
||||
|
||||
2. **AvailableCommandsFactory Files** - Many still use proto `GameState` internally:
|
||||
- These files build proto `AvailableCommand` messages but could use Scala types for internal logic
|
||||
- Convert to accept Scala `GameState`, only convert fields to proto when building the response
|
||||
|
||||
3. **History APIs** - `InMemoryHistory` and `PersistedHistory`:
|
||||
- Change to vend Scala `GameState` and `ActionResultT` instead of proto versions
|
||||
- `PersistedHistory` converts to proto internally for disk persistence
|
||||
|
||||
### Validation
|
||||
- [x] `ActionResultApplier` created and tested
|
||||
- [x] `RandomStateSequencer` threads Scala GameState throughout
|
||||
- [x] `RoundPhaseAdvancer` uses T-types internally
|
||||
- [x] `ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
|
||||
- [x] `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` use Scala `ProvinceView`
|
||||
- [x] `ProvinceViewFilter.withdrawnFromProvinceView` has Scala overload
|
||||
- [x] `CommandChoiceHelpers` uses Scala types ✅
|
||||
- [x] All action files (52/52) are fully protoless ✅
|
||||
- [ ] `ProvinceViewFilter` faction-filtered views use Scala types
|
||||
- [ ] History APIs vend Scala types
|
||||
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
|
||||
- [ ] All tests pass
|
||||
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Clean Up Legacy Utilities
|
||||
## Architecture Summary
|
||||
|
||||
### Objective
|
||||
Remove remaining direct proto imports from utility classes.
|
||||
### Fully Protoless Areas ✅
|
||||
|
||||
### Files to Modify
|
||||
- **AI layer** (`/ai/`) - All AI clients and command choosers
|
||||
- **Actions** (`/library/actions/impl/action/`) - All 52 action files
|
||||
- **Commands** (`/library/actions/impl/command/`) - CommandFactory and all commands
|
||||
- **Availability** (`/library/actions/availability/`) - AvailableCommandsFactory and all factories
|
||||
- **Command helpers** (`/library/util/command_choice_helpers/`) - All selectors and choosers (except RansomOfferHelpers)
|
||||
- **Core types** - `CommandSelection`, `SelectedCommand`, `AvailableCommand`, `OneProvinceAvailableCommands`
|
||||
- **View types** - `FactionView`, `HeroView` (return Scala, convert at edge)
|
||||
|
||||
| File | Status |
|
||||
|------|--------|
|
||||
| `CommandChoiceHelpers.scala` | Accepts proto `GameState`; blocks full deproto of `PerformVassalCommandsPhaseAction` and `PerformVassalDefenseDecisionsAction` |
|
||||
| `LegacyProvinceUtils.scala` | Replace with `ProvinceUtils.scala` - `hasImminentRiot` added (PR #4683) |
|
||||
| `LegacyFactionUtils.scala` | Replace proto imports with `FactionT` |
|
||||
| `LegacyUnaffiliatedHeroUtils.scala` | Replace proto imports with Scala models |
|
||||
| `BattalionTypeLoader.scala` | Keep proto for file loading, convert immediately after |
|
||||
| `BeastUtils.scala` | **Complete** - now uses Scala `BeastInfo` only |
|
||||
### Proto at Boundaries (Expected) ✅
|
||||
|
||||
### View Filters (Partially Complete)
|
||||
|
||||
The view filter utilities now have Scala overloads for server-side use:
|
||||
|
||||
| File | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| `ProvinceViewFilter.scala` | **Partial** | `filteredProvinceView(ProvinceT, ScalaGameState)` added (PR #4752) |
|
||||
| `ArmyFilter.scala` | **Partial** | `filterArmy(ScalaArmy, Map[BattalionId, BattalionT], Option[FactionId])` added |
|
||||
| `BattalionViewFilter.scala` | **Complete** | Uses Scala `BattalionT` throughout |
|
||||
| `GameStateViewFilter.scala` | Pending | Uses proto types throughout |
|
||||
| `GameStateViewDiffer.scala` | Pending | Works with view protos |
|
||||
|
||||
**Unblocked Actions** (PR #4752):
|
||||
- `EndBattleAftermathPhaseAction` - can now use `filteredProvinceView(province, scalaGameState)`
|
||||
- `PerformReconResolutionAction` - can now use Scala overload
|
||||
- `GameStateFactionExtensions` - can now use `updatedReconnedProvinces` with Scala types
|
||||
|
||||
**Remaining Work**:
|
||||
- Faction-filtered `filteredProvinceView(Province, GameState, FactionId)` still uses proto types
|
||||
- `withdrawnFromProvinceView` still uses proto types
|
||||
- These are needed for client-facing views with visibility restrictions
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Verify Boundaries
|
||||
|
||||
### Objective
|
||||
Confirm protos are used correctly at boundaries — and ONLY there.
|
||||
|
||||
### Expected Proto Usage (Keep)
|
||||
- `EagleServiceImpl.scala` - gRPC boundary
|
||||
- `InMemoryHistory.scala` / `PersistedHistory.scala` - Persistence boundary
|
||||
- `GameController.scala` - Client communication
|
||||
- `GameStateViewFilter.scala` - Converts Scala views to proto for client
|
||||
- `*Converter.scala` - Explicit conversion utilities
|
||||
- `*Loader.scala` - File loading utilities
|
||||
- `PersistedHistory.scala` - Disk persistence
|
||||
|
||||
### Expected No Proto Usage (Verify)
|
||||
- `/library/actions/impl/` - Pure Scala models
|
||||
- `/library/util/` - Pure Scala models (except loaders)
|
||||
- `/model/state/` - Pure Scala models
|
||||
### Remaining Proto Usage ⏳
|
||||
|
||||
- View filters (4 imports in 3 files) - In progress
|
||||
- LLM prompt generators (56 imports in 34 files) - Medium priority
|
||||
- Some utility files (13 imports in 5 files) - Low priority
|
||||
- History API internals - Low priority
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
## Estimated Remaining Effort
|
||||
|
||||
1. **Persistence Format**: Currently game state is persisted as proto. Should we keep proto for persistence (good for schema evolution) or switch to a different format?
|
||||
|
||||
2. **Shardok Integration**: `ResolveBattleAction` communicates with Shardok. Should the Shardok interface use protos (external service) or Scala models?
|
||||
|
||||
3. **View Generation**: `GameStateViewDiffer` works with view protos for client updates. Views need Scala models (`ProvinceViewT`, etc.) to allow actions like `EndBattleAftermathPhaseAction` to be fully protoless. The Scala views would be converted to proto only at the gRPC boundary when sending updates to clients.
|
||||
| Component | Files | Imports | Priority |
|
||||
|-----------|-------|---------|----------|
|
||||
| View Filters | 3 | 4 | High |
|
||||
| LLM Prompt Generators | 34 | 56 | Medium |
|
||||
| Utility files | 5 | 13 | Low |
|
||||
| Root Library (boundary) | 3 | 9 | Keep |
|
||||
| **Total** | **45** | **~82** | |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Code Quality
|
||||
- [ ] Zero proto imports in `/library/actions/` (except boundaries)
|
||||
- [ ] Zero proto imports in `/library/` utilities (except loaders)
|
||||
- [ ] `GameStateT` used throughout engine internals
|
||||
- [ ] Proto usage limited to: `EagleServiceImpl`, loaders, converters, persistence
|
||||
- [x] Zero proto imports in `/ai/`
|
||||
- [x] Zero proto imports in `/library/actions/impl/command/`
|
||||
- [x] Zero proto imports in `/library/actions/availability/`
|
||||
- [x] Zero proto imports in `/library/util/command_choice_helpers/` (except RansomOfferHelpers)
|
||||
- [x] Zero proto imports in `/library/actions/impl/action/`
|
||||
- [ ] Zero proto imports in `/library/util/view_filters/` (except BattleFilter, GameStateViewDiffer)
|
||||
- [ ] Zero proto imports in `/library/actions/llm_prompt_generators/`
|
||||
- [ ] Zero proto imports in `/library/util/` (except view filters)
|
||||
|
||||
### Architecture
|
||||
- [ ] Clear separation: Scala models (internal) vs Proto (boundaries)
|
||||
- [ ] Converters as the only bridge between domains
|
||||
- [x] Clear separation: Scala models (internal) vs Proto (boundaries)
|
||||
- [x] Converters as the only bridge between domains
|
||||
- [x] CommandFactory accepts/returns Scala types
|
||||
- [x] AI layer fully protoless
|
||||
- [x] FactionViewFilter returns Scala types
|
||||
- [x] HeroViewFilter returns Scala types
|
||||
- [ ] All view filters return Scala types
|
||||
- [ ] LLM layer uses Scala types
|
||||
- [ ] No "proto creep" into business logic
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **LLM Prompt Generators**: Should these accept Scala types directly, or is it acceptable to have proto usage here since they're generating text (not core game logic)?
|
||||
|
||||
2. **GameStateViewDiffer**: This works with view protos for client updates. Should it remain proto-based since it's generating client-facing data?
|
||||
|
||||
3. **History Serialization**: Keep proto for persistence (good for schema evolution) or consider alternatives?
|
||||
|
||||
---
|
||||
|
||||
## Proto Import Inventory (Detailed)
|
||||
|
||||
**Current: ~85 proto imports across 45 files** (as of 2026-01-14)
|
||||
|
||||
### Summary by Directory
|
||||
|
||||
| Directory | Files | Imports | Status |
|
||||
|-----------|-------|---------|--------|
|
||||
| `ai/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/availability/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/impl/action/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/impl/command/` | 0 | 0 | ✅ Clean |
|
||||
| `actions/llm_prompt_generators/` | 34 | 56 | LLM request types |
|
||||
| `util/command_choice_helpers/` | 1 | 3 | Near complete |
|
||||
| `util/view_filters/` | 3 | 4 | View boundary |
|
||||
| `util/` (other) | 5 | 13 | Mixed |
|
||||
| Root (`library/`) | 3 | 9 | Boundary code |
|
||||
|
||||
### util/command_choice_helpers/ (3 imports, 1 file)
|
||||
|
||||
| File | Import | Notes |
|
||||
|------|--------|-------|
|
||||
| `RansomOfferHelpers.scala` | `common.diplomacy_offer.RansomOfferDetails` | Diplomacy details |
|
||||
| `RansomOfferHelpers.scala` | `common.diplomacy_offer_status.DiplomacyOfferStatus` | Status enum |
|
||||
| `RansomOfferHelpers.scala` | `common.diplomacy_offer_status.DiplomacyOfferStatus.{...}` | Status variants |
|
||||
|
||||
### util/view_filters/ (4 imports, 3 files)
|
||||
|
||||
| File | Import | Notes |
|
||||
|------|--------|-------|
|
||||
| `ProvinceViewFilter.scala` | `common.province_event.ProvinceEvent` | For knownEvents field |
|
||||
| `GameStateViewFilter.scala` | `views.faction_view.FactionView` | Output type |
|
||||
| `GameStateViewFilter.scala` | `views.game_state_view.GameStateView` | Output type |
|
||||
| `BattleFilter.scala` | `views.shardok_battle_view.{...}` | Battle view boundary |
|
||||
|
||||
### util/ other files (13 imports, 5 files)
|
||||
|
||||
| File | Imports | Notes |
|
||||
|------|---------|-------|
|
||||
| `GameStateViewDiffer.scala` | 7 | View diff for client (boundary code) |
|
||||
| `ProvinceEventUtils.scala` | 2 | Province event handling |
|
||||
| `StatWithConditionUtils.scala` | 2 | Stat condition handling |
|
||||
| `MapGenerator.scala` | 1 | Map generation |
|
||||
| `IncomingArmyUtils.scala` | 1 | Army utilities |
|
||||
|
||||
### Root library/ (9 imports, 3 files)
|
||||
|
||||
| File | Imports | Notes |
|
||||
|------|---------|-------|
|
||||
| `ActionResultFilter.scala` | 3 | Action result filtering (boundary) |
|
||||
| `Engine.scala` | 1 | Trait interface |
|
||||
| `EngineImpl.scala` | 5 | Returns proto for persistence (boundary) |
|
||||
|
||||
### actions/llm_prompt_generators/ (56 imports, 34 files)
|
||||
|
||||
These files generate LLM prompts and primarily use `internal.generated_text_request.*` types plus some common enums like `DiplomacyOfferStatus` and `BattalionTypeId`.
|
||||
|
||||
**Migration strategy**: Can be migrated to Scala types when convenient, but low priority as they're isolated from core game logic.
|
||||
|
||||
---
|
||||
|
||||
## Migration Pattern
|
||||
|
||||
The codebase follows a **Legacy* pattern** for separating proto-dependent and protoless code:
|
||||
|
||||
- **Protoless utilities**: `FactionUtils`, `HeroUtils`, `ProvinceUtils`, `ProvinceDistances`, etc.
|
||||
- **Proto-dependent utilities**: `LegacyFactionUtils`, `LegacyHeroUtils`, `LegacyProvinceUtils`, etc.
|
||||
|
||||
When migrating a file:
|
||||
1. Create a `Legacy*` version containing the proto-dependent methods
|
||||
2. Keep the original file name for protoless methods
|
||||
3. Update callers to use the appropriate version based on their context
|
||||
4. Delete `Legacy*` files when no longer needed
|
||||
|
||||
### Deleted Legacy Files (no production callers)
|
||||
- `LegacyProvinceDistances`
|
||||
- `LegacyBattalionSuitability`
|
||||
- `LegacyFoodConsumptionUtils`
|
||||
- `LegacyHandleRiotUtils`
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
load("@build_bazel_rules_apple//apple:apple.bzl", "apple_dynamic_framework_import")
|
||||
|
||||
# Import pre-built Sparkle framework
|
||||
apple_dynamic_framework_import(
|
||||
name = "Sparkle",
|
||||
framework_imports = glob(["Sparkle.framework/**"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -8,3 +8,6 @@ ZIP_LOCATION=$(bazel cquery --config=mactools --output=files @net_eagle0_unity_g
|
||||
/usr/bin/unzip -o $ZIP_LOCATION -d src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/
|
||||
|
||||
/usr/bin/plutil -convert xml1 src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/DarwinGodiceBundle.bundle/Contents/Info.plist
|
||||
|
||||
/bin/echo "building sparkle plugin"
|
||||
./scripts/build_sparkle_plugin.sh
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the SparklePlugin native library for Unity using Bazel
|
||||
#
|
||||
# Usage: build_sparkle_plugin.sh [output_dir]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
OUTPUT_DIR="${1:-$PROJECT_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/macOS}"
|
||||
|
||||
echo "=== Building SparklePlugin with Bazel ==="
|
||||
|
||||
bazel build --config=mactools //src/main/objc/net/eagle0/sparkle:SparklePlugin
|
||||
|
||||
# Get the zip path from bazel
|
||||
ZIP_PATH=$(bazel cquery --config=mactools --output=files //src/main/objc/net/eagle0/sparkle:SparklePlugin 2>/dev/null)
|
||||
|
||||
echo "=== Extracting SparklePlugin.bundle ==="
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
rm -rf "$OUTPUT_DIR/SparklePlugin.bundle"
|
||||
unzip -o "$ZIP_PATH" -d "$OUTPUT_DIR/"
|
||||
|
||||
# Convert Info.plist from binary to XML format (Unity requires XML)
|
||||
/usr/bin/plutil -convert xml1 "$OUTPUT_DIR/SparklePlugin.bundle/Contents/Info.plist"
|
||||
|
||||
echo "=== SparklePlugin built successfully ==="
|
||||
ls -la "$OUTPUT_DIR/SparklePlugin.bundle/"
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Inject Sparkle framework into a macOS .app bundle for auto-updates
|
||||
# Usage: inject_sparkle.sh <app_path>
|
||||
#
|
||||
# Environment variables (required):
|
||||
# SPARKLE_EDDSA_PUBLIC_KEY - EdDSA public key for verifying updates
|
||||
#
|
||||
# Optional environment variables:
|
||||
# SPARKLE_FEED_URL - Appcast URL (default: https://assets.eagle0.net/mac/appcast.xml)
|
||||
# SPARKLE_VERSION - Sparkle version to use (default: 2.6.4)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
APP_PATH="$1"
|
||||
SPARKLE_VERSION="${SPARKLE_VERSION:-2.6.4}"
|
||||
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://assets.eagle0.net/mac/appcast.xml}"
|
||||
SPARKLE_CACHE_DIR="/tmp/sparkle-cache"
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "ERROR: App not found at $APP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
|
||||
echo "ERROR: SPARKLE_EDDSA_PUBLIC_KEY environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download Sparkle if not cached
|
||||
SPARKLE_DIR="$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION"
|
||||
if [ ! -d "$SPARKLE_DIR/Sparkle.framework" ]; then
|
||||
echo "=== Downloading Sparkle $SPARKLE_VERSION ==="
|
||||
mkdir -p "$SPARKLE_CACHE_DIR"
|
||||
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
|
||||
curl -L "$SPARKLE_URL" | tar -xJ -C "$SPARKLE_CACHE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION" "$SPARKLE_DIR" 2>/dev/null || true
|
||||
# If the extracted directory doesn't match version pattern, it may just be "Sparkle"
|
||||
if [ ! -d "$SPARKLE_DIR" ]; then
|
||||
mkdir -p "$SPARKLE_DIR"
|
||||
mv "$SPARKLE_CACHE_DIR/Sparkle.framework" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
mv "$SPARKLE_CACHE_DIR/bin" "$SPARKLE_DIR/" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Injecting Sparkle framework ==="
|
||||
FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
|
||||
mkdir -p "$FRAMEWORKS_DIR"
|
||||
|
||||
# Copy Sparkle framework
|
||||
cp -R "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/"
|
||||
|
||||
# Also copy the XPC services if present
|
||||
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
|
||||
echo "Sparkle XPC services present"
|
||||
fi
|
||||
|
||||
echo "=== Updating Info.plist ==="
|
||||
PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
||||
|
||||
# Add Sparkle configuration to Info.plist
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUFeedURL string '$SPARKLE_FEED_URL'" "$PLIST_PATH"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string '$SPARKLE_EDDSA_PUBLIC_KEY'" "$PLIST_PATH"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUEnableAutomaticChecks bool true" "$PLIST_PATH"
|
||||
|
||||
# Set bundle version from git for Sparkle version comparison
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0")
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
|
||||
|
||||
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string '$VERSION'" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$PLIST_PATH" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleVersion string '$BUILD_NUMBER'" "$PLIST_PATH"
|
||||
|
||||
# Add URL scheme for invitation codes (eagle0://invite?code=XXXX)
|
||||
echo "=== Adding URL scheme for invitation codes ==="
|
||||
/usr/libexec/PlistBuddy -c "Delete :CFBundleURLTypes" "$PLIST_PATH" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0 dict" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string 'com.Shardok-Games.eagle0'" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$PLIST_PATH"
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string 'eagle0'" "$PLIST_PATH"
|
||||
|
||||
echo "=== Sparkle injection complete ==="
|
||||
echo "App: $APP_PATH"
|
||||
echo "Feed URL: $SPARKLE_FEED_URL"
|
||||
echo "Version: $VERSION (build $BUILD_NUMBER)"
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ddb90d3140384d50a98a19e72539b62
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using common;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityGoDiceInterface;
|
||||
|
||||
public class DiceConfigurationPanelController : MonoBehaviour {
|
||||
private DiceInterface _diceInterface;
|
||||
|
||||
public EventBasedTable diceRollGroup;
|
||||
public PickerRowController pickerRowPrefab;
|
||||
public TMP_Text instructionsLabel;
|
||||
|
||||
public delegate void ConfigurationCallback(DieInfo? tensDieInfo, DieInfo? onesDieInfo);
|
||||
|
||||
private ConfigurationCallback _configurationCallback;
|
||||
|
||||
private DieInfo? _tensDieInfo = null;
|
||||
private DieInfo? _onesDieInfo = null;
|
||||
|
||||
private List<DieInfo> _knownDice = new List<DieInfo>();
|
||||
private HashSet<string> _connectingIdentifiers = new HashSet<string>();
|
||||
|
||||
void Awake() {}
|
||||
|
||||
public void GetConfiguration(ConfigurationCallback cb) {
|
||||
if (_diceInterface == null) { _diceInterface = GetComponentInParent<DiceInterface>(); }
|
||||
_diceInterface.deviceFoundCallback = DeviceFoundCallback;
|
||||
_diceInterface.connectionCallback = ConnectionCallback;
|
||||
_diceInterface.connectionFailedCallback = ConnectionFailedCallback;
|
||||
_diceInterface.disconnectionCallback = DisconnectionCallback;
|
||||
_diceInterface.listenerStoppedCallback = ListenerStoppedCallback;
|
||||
_diceInterface.colorCallback = ReceiveColorCallback;
|
||||
|
||||
string currentPath = NewLogLocation();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(currentPath));
|
||||
_diceInterface.logger = log => {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
Debug.Log(log);
|
||||
File.AppendAllText(currentPath, log);
|
||||
});
|
||||
};
|
||||
|
||||
_diceInterface.StartListening();
|
||||
|
||||
_configurationCallback = cb;
|
||||
_tensDieInfo = null;
|
||||
_onesDieInfo = null;
|
||||
instructionsLabel.text = "Looking for dice...";
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
string NewLogLocation() {
|
||||
return Path.Combine(
|
||||
Application.persistentDataPath,
|
||||
"eagle0",
|
||||
"Resources",
|
||||
"DiceConfigurationLogs",
|
||||
DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss") + ".log");
|
||||
}
|
||||
|
||||
public void RowClicked(int rowIndex) {
|
||||
if (_tensDieInfo == null) {
|
||||
_tensDieInfo = _knownDice[rowIndex];
|
||||
_knownDice.RemoveAt(rowIndex);
|
||||
SetUpTable();
|
||||
} else {
|
||||
_diceInterface.StopListening();
|
||||
|
||||
_onesDieInfo = _knownDice[rowIndex];
|
||||
_knownDice.RemoveAt(rowIndex);
|
||||
_knownDice.ForEach(dieInfo => _diceInterface.Disconnect(dieInfo.identifier));
|
||||
|
||||
// _diceInterface.deviceFoundCallback = null;
|
||||
// _diceInterface.connectionCallback = null;
|
||||
// _diceInterface.disconnectionCallback = null;
|
||||
// _diceInterface.listenerStoppedCallback = null;
|
||||
// _diceInterface.rollCallback = null;
|
||||
// _diceInterface.colorCallback = null;
|
||||
|
||||
_configurationCallback(_tensDieInfo, _onesDieInfo);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable() {}
|
||||
|
||||
private void OnDisable() { diceRollGroup.RowCount = 0; }
|
||||
|
||||
void ListenerStoppedCallback() { Debug.Log("Listener stopped!"); }
|
||||
|
||||
void SetUpTable() {
|
||||
diceRollGroup.RowCount = _knownDice.Count;
|
||||
|
||||
if (_knownDice.Count == 0) {
|
||||
instructionsLabel.text = "Looking for dice...";
|
||||
} else if (_tensDieInfo == null) {
|
||||
instructionsLabel.text = "Select a TENS die:";
|
||||
} else {
|
||||
instructionsLabel.text = "Select an ONES die:";
|
||||
}
|
||||
|
||||
for (int i = 0; i < _knownDice.Count; i++) {
|
||||
var row = diceRollGroup.ComponentAt<PickerRowController>(i);
|
||||
|
||||
row.Text = _knownDice[i].deviceName;
|
||||
row.TextColor = UnityDieColors.ColorFromDieColor(_knownDice[i].color);
|
||||
}
|
||||
}
|
||||
|
||||
void ReceiveColorCallback(string identifier, DieColor dieColor) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) {
|
||||
var colorName = dieColor.ToString();
|
||||
_knownDice[existingIndex] = new DieInfo(
|
||||
identifier: _knownDice[existingIndex].identifier,
|
||||
deviceName: colorName,
|
||||
color: dieColor,
|
||||
connected: true);
|
||||
} else {
|
||||
Debug.Log("This shouldn't happen");
|
||||
}
|
||||
|
||||
SetUpTable();
|
||||
});
|
||||
}
|
||||
|
||||
void DeviceFoundCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (_connectingIdentifiers.Contains(identifier)) { return; }
|
||||
|
||||
_connectingIdentifiers.Add(identifier);
|
||||
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) {
|
||||
_knownDice[existingIndex] =
|
||||
new DieInfo(identifier, "Unknown", DieColor.DieColorBlack);
|
||||
} else {
|
||||
_knownDice.Add(new DieInfo(identifier, "Unknown", DieColor.DieColorBlack));
|
||||
}
|
||||
|
||||
SetUpTable();
|
||||
|
||||
_diceInterface.Connect(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionCallback(string identifier, string deviceName) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_connectingIdentifiers.Remove(identifier);
|
||||
|
||||
if (_tensDieInfo != null && _tensDieInfo.Value.identifier.Equals(identifier)) {
|
||||
return;
|
||||
}
|
||||
if (_onesDieInfo != null && _onesDieInfo.Value.identifier.Equals(identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var shortenedName = deviceName.Split('_')[1];
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) {
|
||||
_knownDice[existingIndex] =
|
||||
new DieInfo(identifier, shortenedName, DieColor.DieColorBlack);
|
||||
} else {
|
||||
_knownDice.Add(new DieInfo(identifier, shortenedName, DieColor.DieColorBlack));
|
||||
}
|
||||
|
||||
SetUpTable();
|
||||
|
||||
_diceInterface.RequestColor(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionFailedCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_connectingIdentifiers.Remove(identifier);
|
||||
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) {
|
||||
_knownDice.RemoveAt(existingIndex);
|
||||
|
||||
SetUpTable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DisconnectionCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
_connectingIdentifiers.Remove(identifier);
|
||||
|
||||
if (_tensDieInfo is {} tensInfo && tensInfo.identifier.Equals(identifier)) {
|
||||
_tensDieInfo = null;
|
||||
_onesDieInfo = null;
|
||||
} else if (_onesDieInfo is {} onesInfo && onesInfo.identifier.Equals(identifier)) {
|
||||
_onesDieInfo = null;
|
||||
}
|
||||
|
||||
var existingIndex =
|
||||
_knownDice.FindIndex(dieInfo => dieInfo.identifier.Equals(identifier));
|
||||
if (existingIndex >= 0) { _knownDice.RemoveAt(existingIndex); }
|
||||
|
||||
SetUpTable();
|
||||
_diceInterface.Connect(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
public void DismissButtonClicked() {
|
||||
gameObject.SetActive(false);
|
||||
_configurationCallback(null, null);
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e694fb18ea1df4034b392352bf3aa04a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,213 +0,0 @@
|
||||
#if UNITY_IOS || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
|
||||
#define USE_SWIFT_INTERFACE
|
||||
#elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || ENABLE_WINMD_SUPPORT
|
||||
#define USE_WINDOWS_INTERFACE
|
||||
#else
|
||||
#endif
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityGoDiceInterface;
|
||||
|
||||
public class DiceInterface : MonoBehaviour {
|
||||
public struct Commands {
|
||||
public static readonly byte PulseLed = 16;
|
||||
public static readonly byte GetColor = 23;
|
||||
}
|
||||
|
||||
readonly NativeDiceInterfaceImports _diceInterfaceImports = new();
|
||||
private static DiceInterface _singleton = null;
|
||||
|
||||
private static Dictionary<string, string> _deviceNames = new Dictionary<string, string>();
|
||||
|
||||
public delegate void DeviceFoundCallback(string identifier);
|
||||
public delegate void ConnectionCallback(string identifier, string deviceName);
|
||||
public delegate void ConnectionFailedCallback(string identifier);
|
||||
public delegate void DisconnectionCallback(string identifier);
|
||||
public delegate void ListenerStoppedCallback();
|
||||
public delegate void RollCallback(string identifier, sbyte x, sbyte y, sbyte z);
|
||||
public delegate void ColorCallback(string identifier, DieColor color);
|
||||
public delegate void LoggerCallback(string log);
|
||||
|
||||
public DeviceFoundCallback deviceFoundCallback = null;
|
||||
public ConnectionCallback connectionCallback = null;
|
||||
public ConnectionFailedCallback connectionFailedCallback = null;
|
||||
public DisconnectionCallback disconnectionCallback = null;
|
||||
public ListenerStoppedCallback listenerStoppedCallback = null;
|
||||
public RollCallback rollCallback = null;
|
||||
public ColorCallback colorCallback = null;
|
||||
public LoggerCallback logger = null;
|
||||
|
||||
public void StartListening() { _diceInterfaceImports.StartListening(); }
|
||||
|
||||
public void StopListening() { _diceInterfaceImports.StopListening(); }
|
||||
|
||||
public void Connect(string identifier) { _diceInterfaceImports.Connect(identifier); }
|
||||
public void Disconnect(string identifier) { _diceInterfaceImports.Disconnect(identifier); }
|
||||
|
||||
public void Reset() {
|
||||
_diceInterfaceImports.Reset();
|
||||
_deviceNames.Clear();
|
||||
}
|
||||
|
||||
public void RequestColor(string identifier) {
|
||||
_diceInterfaceImports.Send(identifier, new List<byte> { Commands.GetColor });
|
||||
}
|
||||
|
||||
public void FlashColor(
|
||||
string identifier,
|
||||
byte pulseCount,
|
||||
byte onTime10ms,
|
||||
byte offTime10ms,
|
||||
byte red,
|
||||
byte green,
|
||||
byte blue) {
|
||||
_diceInterfaceImports.Send(
|
||||
identifier,
|
||||
new List<byte> {
|
||||
Commands.PulseLed,
|
||||
pulseCount,
|
||||
onTime10ms,
|
||||
offTime10ms,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
0x01,
|
||||
0x00
|
||||
});
|
||||
}
|
||||
|
||||
// Start is called before the first frame update
|
||||
void Start() {
|
||||
_singleton = this;
|
||||
|
||||
_diceInterfaceImports.SetDeviceConnectedCallback(DeviceConnectedDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetDeviceConnectionFailedCallback(
|
||||
DeviceConnectionFailedDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetDeviceDisconnectedCallback(
|
||||
DeviceDisconnectedDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetDeviceFoundCallback(DeviceFoundDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetDataCallback(DataDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetListenerStoppedCallback(ListenerStoppedDelegateMessageReceived);
|
||||
_diceInterfaceImports.SetLoggerCallback(LoggerDelegateMessageReceived);
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {}
|
||||
|
||||
private static sbyte[] GetRollVector(List<byte> rawData) {
|
||||
if (rawData.Count < 1) {
|
||||
Debug.Log("rollVector: no data");
|
||||
return null;
|
||||
}
|
||||
byte firstByte = rawData[0];
|
||||
if (firstByte != 83) {
|
||||
Debug.Log("rollVector: first byte is not 83");
|
||||
return null;
|
||||
}
|
||||
if (rawData.Count != 4) {
|
||||
Debug.Log("rollVector: data length is not 4");
|
||||
return null;
|
||||
}
|
||||
return new[] { (sbyte)rawData[1], (sbyte)rawData[2], (sbyte)rawData[3] };
|
||||
}
|
||||
|
||||
private static void DeviceFoundDelegateMessageReceived(string identifier, string deviceName) {
|
||||
if (_singleton != null) {
|
||||
_deviceNames[identifier] = deviceName;
|
||||
if (_singleton.deviceFoundCallback != null) {
|
||||
_singleton.deviceFoundCallback(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeviceConnectedDelegateMessageReceived(string identifier) {
|
||||
if (_singleton != null && _singleton.connectionCallback != null) {
|
||||
_singleton.connectionCallback(identifier, _deviceNames[identifier]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeviceConnectionFailedDelegateMessageReceived(string identifier) {
|
||||
if (_singleton != null && _singleton.connectionFailedCallback != null) {
|
||||
_singleton.connectionFailedCallback(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeviceDisconnectedDelegateMessageReceived(string identifier) {
|
||||
if (_singleton != null && _singleton.disconnectionCallback != null) {
|
||||
_singleton.disconnectionCallback(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DataDelegateMessageReceived(string identifier, List<byte> byteList) {
|
||||
if (_singleton != null) {
|
||||
if (byteList.Count == 0) {
|
||||
if (_singleton.connectionCallback != null) {
|
||||
// I don't think this one should still happen
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
byte firstByte = byteList[0];
|
||||
|
||||
switch (firstByte) {
|
||||
case 82:
|
||||
// Roll started
|
||||
break;
|
||||
case 66:
|
||||
// Battery level
|
||||
break;
|
||||
case (byte)'C':
|
||||
if (byteList[1] == (byte)'o' && byteList[2] == (byte)'l') {
|
||||
byte colorRawValue = byteList[3];
|
||||
|
||||
_singleton.colorCallback(identifier, (DieColor)colorRawValue);
|
||||
}
|
||||
// Color (fetched)
|
||||
break;
|
||||
case 83: {
|
||||
var roll = GetRollVector(byteList);
|
||||
if (roll != null && _singleton.rollCallback != null) {
|
||||
_singleton.rollCallback(identifier, roll[0], roll[1], roll[2]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 70:
|
||||
case 84:
|
||||
case 77: {
|
||||
byteList.RemoveAt(0);
|
||||
var roll = GetRollVector(byteList);
|
||||
if (roll != null && _singleton.rollCallback != null) {
|
||||
_singleton.rollCallback(identifier, roll[0], roll[1], roll[2]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: Debug.Log("Not yet handled"); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ListenerStoppedDelegateMessageReceived() {
|
||||
if (_singleton != null && _singleton.listenerStoppedCallback != null) {
|
||||
_singleton.listenerStoppedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoggerDelegateMessageReceived(string log) {
|
||||
if (_singleton != null && _singleton.logger != null) { _singleton.logger(log); }
|
||||
}
|
||||
|
||||
public void OnDestroy() {
|
||||
_diceInterfaceImports.SetDeviceConnectedCallback(null);
|
||||
_diceInterfaceImports.SetDeviceConnectionFailedCallback(null);
|
||||
_diceInterfaceImports.SetDeviceDisconnectedCallback(null);
|
||||
_diceInterfaceImports.SetDeviceFoundCallback(null);
|
||||
_diceInterfaceImports.SetDataCallback(null);
|
||||
_diceInterfaceImports.SetListenerStoppedCallback(null);
|
||||
_diceInterfaceImports.SetLoggerCallback(null);
|
||||
#if UNITY_EDITOR
|
||||
StopListening();
|
||||
Reset();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityGoDiceInterface {
|
||||
public class DiceVectors {
|
||||
public struct Vector3 {
|
||||
public sbyte x;
|
||||
public sbyte y;
|
||||
public sbyte z;
|
||||
|
||||
public Vector3(sbyte x, sbyte y, sbyte z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
public static int D6Value(sbyte x, sbyte y, sbyte z) {
|
||||
return DieValueForVector(d6, new Vector3(x, y, z));
|
||||
}
|
||||
|
||||
public static int D10Value(sbyte x, sbyte y, sbyte z) {
|
||||
return d10Transform[DieValueForVector(d20, new Vector3(x, y, z))];
|
||||
}
|
||||
|
||||
private static int sq(int x) { return x * x; }
|
||||
|
||||
private static int DieValueForVector(List<Vector3> table, Vector3 vector) {
|
||||
var closestIndex = -1;
|
||||
var closestDistance = int.MaxValue;
|
||||
|
||||
for (int i = 0; i < table.Count; i++) {
|
||||
var entry = table[i];
|
||||
var distance =
|
||||
sq(entry.x - vector.x) + sq(entry.y - vector.y) + sq(entry.z - vector.z);
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex + 1;
|
||||
}
|
||||
|
||||
// The private vectors are 0-based, so we need to add 1 to the index
|
||||
private static List<Vector3> d6 = new List<Vector3> {
|
||||
new Vector3(-64, 0, 0),
|
||||
new Vector3(-0, 0, 64),
|
||||
new Vector3(0, 64, 0),
|
||||
new Vector3(0, -64, 0),
|
||||
new Vector3(0, 0, -64),
|
||||
new Vector3(64, 0, 0)
|
||||
};
|
||||
|
||||
private static List<Vector3> d20 = new List<Vector3> {
|
||||
new Vector3(-64, 0, -22), new Vector3(42, -42, 40), new Vector3(0, 22, -64),
|
||||
new Vector3(0, 22, 64), new Vector3(-42, -42, 42), new Vector3(22, 64, 0),
|
||||
new Vector3(-42, -42, -42), new Vector3(64, 0, -22), new Vector3(-22, 64, 0),
|
||||
new Vector3(42, -42, -42), new Vector3(-42, 42, 42), new Vector3(22, -64, 0),
|
||||
new Vector3(-64, 0, 22), new Vector3(42, 42, 42), new Vector3(-22, -64, 0),
|
||||
new Vector3(42, 42, -42), new Vector3(0, -22, -64), new Vector3(0, -22, 64),
|
||||
new Vector3(-42, 42, -42), new Vector3(64, 0, 22),
|
||||
};
|
||||
|
||||
// static d24Vectors = {
|
||||
// new Vector3(20, -60, -20),
|
||||
// new Vector3(20, 0, 60),
|
||||
// new Vector3(-40, -40, 40),
|
||||
// new Vector3(-60, 0, 20),
|
||||
// new Vector3(40, 20, 40),
|
||||
// new Vector3(-20, -60, -20),
|
||||
// new Vector3(20, 60, 20),
|
||||
// new Vector3(-40, 20, -40),
|
||||
// new Vector3(-40, 40, 40),
|
||||
// new Vector3(-20, 0, 60),
|
||||
// new Vector3(-20, -60, 20),
|
||||
// new Vector3(60, 0, 20),
|
||||
// new Vector3(-60, 0, -20),
|
||||
// new Vector3(20, 60, -20),
|
||||
// new Vector3(20, 0, -60),
|
||||
// new Vector3(40, -20, -40),
|
||||
// new Vector3(-20, 60, -20),
|
||||
// new Vector3(-40, -40, -40),
|
||||
// new Vector3(40, -20, 40),
|
||||
// new Vector3(20, -60, 20),
|
||||
// new Vector3(60, 0, -20),
|
||||
// new Vector3(40, 20, -40),
|
||||
// new Vector3(-20, 0, -60),
|
||||
// new Vector3(-20, 60, 20),
|
||||
// }
|
||||
//
|
||||
// Transforms from each shell type to according number on shell
|
||||
// D20 Transforms
|
||||
private static Dictionary<int, int> d10Transform = new Dictionary<int, int> {
|
||||
{ 1, 8 }, { 2, 2 }, { 3, 6 }, { 4, 1 }, { 5, 4 }, { 6, 3 }, { 7, 9 },
|
||||
{ 8, 0 }, { 9, 7 }, { 10, 5 }, { 11, 5 }, { 12, 7 }, { 13, 0 }, { 14, 9 },
|
||||
{ 15, 3 }, { 16, 4 }, { 17, 1 }, { 18, 6 }, { 19, 2 }, { 20, 8 },
|
||||
};
|
||||
//
|
||||
// static d10XTransform = {
|
||||
// 1: 80,
|
||||
// 2: 20,
|
||||
// 3: 60,
|
||||
// 4: 10,
|
||||
// 5: 40,
|
||||
// 6: 30,
|
||||
// 7: 90,
|
||||
// 8: 0,
|
||||
// 9: 70,
|
||||
// 10: 50,
|
||||
// 11: 50,
|
||||
// 12: 70,
|
||||
// 13: 0,
|
||||
// 14: 90,
|
||||
// 15: 30,
|
||||
// 16: 40,
|
||||
// 17: 10,
|
||||
// 18: 60,
|
||||
// 19: 20,
|
||||
// 20: 80,
|
||||
// }
|
||||
//
|
||||
// // D24 Transforms
|
||||
// static d4Transform = {
|
||||
// 1: 3,
|
||||
// 2: 1,
|
||||
// 3: 4,
|
||||
// 4: 1,
|
||||
// 5: 4,
|
||||
// 6: 4,
|
||||
// 7: 1,
|
||||
// 8: 4,
|
||||
// 9: 2,
|
||||
// 10: 3,
|
||||
// 11: 1,
|
||||
// 12: 1,
|
||||
// 13: 1,
|
||||
// 14: 4,
|
||||
// 15: 2,
|
||||
// 16: 3,
|
||||
// 17: 3,
|
||||
// 18: 2,
|
||||
// 19: 2,
|
||||
// 20: 2,
|
||||
// 21: 4,
|
||||
// 22: 1,
|
||||
// 23: 3,
|
||||
// 24: 2,
|
||||
// }
|
||||
//
|
||||
// static d8Transform = {
|
||||
// 1: 3,
|
||||
// 2: 3,
|
||||
// 3: 6,
|
||||
// 4: 1,
|
||||
// 5: 2,
|
||||
// 6: 8,
|
||||
// 7: 1,
|
||||
// 8: 1,
|
||||
// 9: 4,
|
||||
// 10: 7,
|
||||
// 11: 5,
|
||||
// 12: 5,
|
||||
// 13: 4,
|
||||
// 14: 4,
|
||||
// 15: 2,
|
||||
// 16: 5,
|
||||
// 17: 7,
|
||||
// 18: 7,
|
||||
// 19: 8,
|
||||
// 20: 2,
|
||||
// 21: 8,
|
||||
// 22: 3,
|
||||
// 23: 6,
|
||||
// 24: 6,
|
||||
// }
|
||||
//
|
||||
// static d12Transform = {
|
||||
// 1: 1,
|
||||
// 2: 2,
|
||||
// 3: 3,
|
||||
// 4: 4,
|
||||
// 5: 5,
|
||||
// 6: 6,
|
||||
// 7: 7,
|
||||
// 8: 8,
|
||||
// 9: 9,
|
||||
// 10: 10,
|
||||
// 11: 11,
|
||||
// 12: 12,
|
||||
// 13: 1,
|
||||
// 14: 2,
|
||||
// 15: 3,
|
||||
// 16: 4,
|
||||
// 17: 5,
|
||||
// 18: 6,
|
||||
// 19: 7,
|
||||
// 20: 8,
|
||||
// 21: 9,
|
||||
// 22: 10,
|
||||
// 23: 11,
|
||||
// 24: 12,
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9d67d9a2f8145a999d04de91c27073d
|
||||
timeCreated: 1702612480
|
||||
@@ -1,28 +0,0 @@
|
||||
namespace UnityGoDiceInterface {
|
||||
public enum DieColor : byte {
|
||||
DieColorBlack = 0,
|
||||
DieColorRed = 1,
|
||||
DieColorGreen = 2,
|
||||
DieColorBlue = 3,
|
||||
DieColorYellow = 4,
|
||||
DieColorOrange = 5,
|
||||
}
|
||||
|
||||
public struct DieInfo {
|
||||
public string identifier;
|
||||
public string deviceName;
|
||||
public DieColor color;
|
||||
public bool connected;
|
||||
|
||||
public DieInfo(
|
||||
string identifier,
|
||||
string deviceName,
|
||||
DieColor color,
|
||||
bool connected = false) {
|
||||
this.identifier = identifier;
|
||||
this.deviceName = deviceName;
|
||||
this.color = color;
|
||||
this.connected = connected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e1000782bb845399869ca7910eebcd6
|
||||
timeCreated: 1703172326
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using AOT;
|
||||
|
||||
namespace UnityGoDiceInterface {
|
||||
public class NativeDiceInterfaceImports {
|
||||
public delegate void DeviceFoundDelegateMessage(string identifier, string name);
|
||||
public delegate void DataDelegateMessage(string identifier, List<byte> bytes);
|
||||
public delegate void DeviceConnectedDelegateMessage(string identifier);
|
||||
public delegate void DeviceConnectionFailedDelegateMessage(string identifier);
|
||||
public delegate void DeviceDisconnectedDelegateMessage(string identifier);
|
||||
public delegate void ListenerStoppedDelegateMessage();
|
||||
public delegate void LoggerDelegateMessage(string log);
|
||||
|
||||
private static DeviceFoundDelegateMessage deviceFoundDelegate;
|
||||
private static DataDelegateMessage dataDelegate;
|
||||
private static DeviceConnectedDelegateMessage deviceConnectedDelegate;
|
||||
private static DeviceConnectionFailedDelegateMessage deviceConnectionFailedDelegate;
|
||||
private static DeviceDisconnectedDelegateMessage deviceDisconnectedDelegate;
|
||||
private static ListenerStoppedDelegateMessage listenerStoppedDelegate;
|
||||
private static LoggerDelegateMessage loggerDelegateMessage;
|
||||
|
||||
#if UNITY_IOS
|
||||
private const string BundleName = "__Internal";
|
||||
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
|
||||
private const string BundleName = "DarwinGodiceBundle";
|
||||
#elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
|
||||
private const string BundleName = "GoDiceDll.dll";
|
||||
#endif
|
||||
|
||||
private delegate void MonoDeviceFoundDelegateMessage(string identifier, string name);
|
||||
private delegate void
|
||||
MonoDataDelegateMessage(string identifier, UInt32 byteCount, IntPtr bytePtr);
|
||||
private delegate void MonoDeviceConnectedDelegateMessage(string identifier);
|
||||
private delegate void MonoDeviceConnectionFailedDelegateMessage(string identifier);
|
||||
private delegate void MonoDeviceDisconnectedDelegateMessage(string identifier);
|
||||
private delegate void MonoListenerStoppedDelegateMessage();
|
||||
private delegate void MonoLoggerDelegateMessage(string log);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_start_listening")]
|
||||
private static extern void _NativeBridgeStartListening();
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_stop_listening")]
|
||||
private static extern void _NativeBridgeStopListening();
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_set_callbacks")]
|
||||
private static extern void _NativeBridgeSetCallbacks(
|
||||
MonoDeviceFoundDelegateMessage deviceFoundDelegate,
|
||||
MonoDataDelegateMessage dataDelegate,
|
||||
MonoDeviceConnectedDelegateMessage deviceConnectedDelegate,
|
||||
MonoDeviceConnectionFailedDelegateMessage deviceConnectionFailedDelegateMessage,
|
||||
MonoDeviceDisconnectedDelegateMessage deviceDisconnectedDelegate,
|
||||
MonoListenerStoppedDelegateMessage listenerStoppedDelegate);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_connect")]
|
||||
private static extern void _NativeBridgeConnect(string identifier);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_disconnect")]
|
||||
private static extern void _NativeBridgeDisconnect(string identifier);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_send")]
|
||||
private static extern void
|
||||
_NativeBridgeSend(string identifier, UInt32 byteCount, IntPtr bytePtr);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_set_logger")]
|
||||
private static extern void _NativeBridgeSetLogger(MonoLoggerDelegateMessage loggerDelegate);
|
||||
|
||||
[DllImport(dllName: BundleName, EntryPoint = "godice_reset")]
|
||||
private static extern void _NativeBridgeReset();
|
||||
|
||||
private static List<byte> BytesFromRawPointer(UInt32 byteCount, IntPtr bytes) {
|
||||
byte[] array = new byte[byteCount];
|
||||
if (byteCount > 0) { Marshal.Copy(bytes, array, 0, (int)byteCount); }
|
||||
return new List<byte>(array);
|
||||
}
|
||||
|
||||
/*
|
||||
* Callback methods
|
||||
*/
|
||||
[MonoPInvokeCallback(typeof(MonoDeviceFoundDelegateMessage))]
|
||||
private static void MonoDeviceFoundMessageReceived(string identifier, string name) {
|
||||
if (deviceFoundDelegate != null) { deviceFoundDelegate(identifier, name); }
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoDataDelegateMessage))]
|
||||
private static void
|
||||
MonoDataMessageReceived(string identifier, UInt32 byteCount, IntPtr bytePtr) {
|
||||
if (dataDelegate != null) {
|
||||
dataDelegate(identifier, BytesFromRawPointer(byteCount, bytePtr));
|
||||
}
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoDeviceConnectedDelegateMessage))]
|
||||
private static void MonoDeviceConnected(string identifier) {
|
||||
if (deviceConnectedDelegate != null) { deviceConnectedDelegate(identifier); }
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoDeviceConnectionFailedDelegateMessage))]
|
||||
private static void MonoDeviceConnectionFailed(string identifier) {
|
||||
if (deviceConnectionFailedDelegate != null) {
|
||||
deviceConnectionFailedDelegate(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoDeviceDisconnectedDelegateMessage))]
|
||||
private static void MonoDeviceDisconnected(string identifier) {
|
||||
if (deviceDisconnectedDelegate != null) { deviceDisconnectedDelegate(identifier); }
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoListenerStoppedDelegateMessage))]
|
||||
private static void MonoListenerStopped() {
|
||||
if (listenerStoppedDelegate != null) { listenerStoppedDelegate(); }
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(MonoLoggerDelegateMessage))]
|
||||
private static void MonoLogger(string log) {
|
||||
if (loggerDelegateMessage != null) { loggerDelegateMessage(log); }
|
||||
}
|
||||
|
||||
public void StartListening() {
|
||||
_NativeBridgeSetCallbacks(
|
||||
MonoDeviceFoundMessageReceived,
|
||||
MonoDataMessageReceived,
|
||||
MonoDeviceConnected,
|
||||
MonoDeviceConnectionFailed,
|
||||
MonoDeviceDisconnected,
|
||||
MonoListenerStopped);
|
||||
_NativeBridgeSetLogger(MonoLogger);
|
||||
_NativeBridgeStartListening();
|
||||
}
|
||||
|
||||
public void StopListening() { _NativeBridgeStopListening(); }
|
||||
|
||||
public void Connect(string identifier) { _NativeBridgeConnect(identifier); }
|
||||
|
||||
public void Disconnect(string identifier) { _NativeBridgeDisconnect(identifier); }
|
||||
|
||||
public void Send(string identifier, List<byte> data) {
|
||||
byte[] byteArray = data.ToArray();
|
||||
GCHandle pinnedArray = GCHandle.Alloc(byteArray, GCHandleType.Pinned);
|
||||
IntPtr pointer = pinnedArray.AddrOfPinnedObject();
|
||||
_NativeBridgeSend(identifier, (uint)data.Count, pointer);
|
||||
pinnedArray.Free();
|
||||
}
|
||||
|
||||
public void SetDeviceFoundCallback(DeviceFoundDelegateMessage deviceFound) {
|
||||
deviceFoundDelegate = deviceFound;
|
||||
}
|
||||
|
||||
public void SetDataCallback(DataDelegateMessage data) { dataDelegate = data; }
|
||||
|
||||
public void SetDeviceConnectedCallback(DeviceConnectedDelegateMessage deviceConnected) {
|
||||
deviceConnectedDelegate = deviceConnected;
|
||||
}
|
||||
|
||||
public void SetDeviceConnectionFailedCallback(
|
||||
DeviceConnectionFailedDelegateMessage deviceConnectionFailed) {
|
||||
deviceConnectionFailedDelegate = deviceConnectionFailed;
|
||||
}
|
||||
|
||||
public void SetDeviceDisconnectedCallback(
|
||||
DeviceDisconnectedDelegateMessage deviceDisconnected) {
|
||||
deviceDisconnectedDelegate = deviceDisconnected;
|
||||
}
|
||||
|
||||
public void SetListenerStoppedCallback(ListenerStoppedDelegateMessage listenerStopped) {
|
||||
listenerStoppedDelegate = listenerStopped;
|
||||
}
|
||||
|
||||
public void SetLoggerCallback(LoggerDelegateMessage logger) {
|
||||
loggerDelegateMessage = logger;
|
||||
}
|
||||
|
||||
public void Reset() { _NativeBridgeReset(); }
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 424476bb5b18c47039d649c28a58a8fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-447
@@ -1,447 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &2154262805260441194
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7181142147825592926}
|
||||
- component: {fileID: 4917217417751722011}
|
||||
- component: {fileID: 8876282893591316834}
|
||||
- component: {fileID: 1837103631940551462}
|
||||
- component: {fileID: 7123394496182663843}
|
||||
m_Layer: 0
|
||||
m_Name: OneDiceResult
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7181142147825592926
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 145111913694346175}
|
||||
- {fileID: 6185631331142294830}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &4917217417751722011
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Padding:
|
||||
m_Left: 0
|
||||
m_Right: 0
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 4
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 1
|
||||
m_ChildControlHeight: 1
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!114 &8876282893591316834
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &1837103631940551462
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 680b7d1179eb34a0fa338286236db2e9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
nameLabel: {fileID: 9015301821484574050}
|
||||
d6Label: {fileID: 0}
|
||||
d10Label: {fileID: 4483020407950697354}
|
||||
--- !u!222 &7123394496182663843
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2154262805260441194}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &6943825177879701675
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6185631331142294830}
|
||||
- component: {fileID: 4032026062387785650}
|
||||
- component: {fileID: 4483020407950697354}
|
||||
- component: {fileID: 742703408719749652}
|
||||
- component: {fileID: 4292989854150447437}
|
||||
m_Layer: 0
|
||||
m_Name: result
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &6185631331142294830
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7181142147825592926}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4032026062387785650
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &4483020407950697354
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: 9
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4278190080
|
||||
m_fontColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 48
|
||||
m_fontSizeBase: 48
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!114 &742703408719749652
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &4292989854150447437
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6943825177879701675}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &8198927819804470251
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 145111913694346175}
|
||||
- component: {fileID: 8510898822381381753}
|
||||
- component: {fileID: 9015301821484574050}
|
||||
- component: {fileID: 4918688670369599320}
|
||||
m_Layer: 0
|
||||
m_Name: Name
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &145111913694346175
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8198927819804470251}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7181142147825592926}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8510898822381381753
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8198927819804470251}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &9015301821484574050
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8198927819804470251}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: 'Tens
|
||||
|
||||
'
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4278190080
|
||||
m_fontColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 16
|
||||
m_fontSizeBase: 16
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!114 &4918688670369599320
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8198927819804470251}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 200
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: 1
|
||||
m_LayoutPriority: 1
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9acf3ba64a8c04e6083aae699248b0c4
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
using TMPro;
|
||||
|
||||
public class OneDiceRollController : TableRowController {
|
||||
private string _identifier;
|
||||
|
||||
public TMP_Text nameLabel;
|
||||
public TMP_Text d6Label;
|
||||
public TMP_Text d10Label;
|
||||
|
||||
// Start is called before the first frame update
|
||||
void Start() {}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {}
|
||||
|
||||
public void SetUp(string identifier, string deviceName) {
|
||||
_identifier = identifier;
|
||||
|
||||
nameLabel.text = deviceName;
|
||||
d6Label.text = "?";
|
||||
d10Label.text = "?";
|
||||
}
|
||||
|
||||
public void SetRoll(string identifier, string d6, string d10) {
|
||||
_identifier = identifier;
|
||||
|
||||
d6Label.text = d6;
|
||||
d10Label.text = d10;
|
||||
}
|
||||
|
||||
public string GetIdentifier() { return _identifier; }
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &3756143511112798868
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5260824885446700081}
|
||||
- component: {fileID: 757087991837136050}
|
||||
- component: {fileID: 8474069203316727232}
|
||||
m_Layer: 0
|
||||
m_Name: Text (TMP)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &5260824885446700081
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3756143511112798868}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 3793282440965013679}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 200, y: 50}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &757087991837136050
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3756143511112798868}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &8474069203316727232
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3756143511112798868}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: Orange
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
|
||||
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
|
||||
type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4278229493
|
||||
m_fontColor: {r: 0.9607843, g: 0.6, b: 0, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 18
|
||||
m_fontSizeBase: 18
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &5800426311821892848
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3793282440965013679}
|
||||
- component: {fileID: 6926687573176944590}
|
||||
- component: {fileID: -2050598564750628712}
|
||||
- component: {fileID: 2781819584472077043}
|
||||
- component: {fileID: -2772541110306127347}
|
||||
- component: {fileID: 5171470299130298381}
|
||||
m_Layer: 0
|
||||
m_Name: PickerRow
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &3793282440965013679
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 5260824885446700081}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 200, y: 50}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6926687573176944590
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &-2050598564750628712
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: 30
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: 30
|
||||
m_FlexibleWidth: 1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &2781819584472077043
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &-2772541110306127347
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d7b0b988957134dbc9c5e270bd5e9e70, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
nameLabel: {fileID: 8474069203316727232}
|
||||
--- !u!114 &5171470299130298381
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5800426311821892848}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 876f18cbcbaea4cba9510a977f343c42, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
onClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onRightClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onLeftDown:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onLeftUp:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onRightDown:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onRightUp:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
selectable: 1
|
||||
rightSelectable: 0
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a5c83874e75141728c54f347958d4ae
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class PickerRowController : TableRowController {
|
||||
public TMP_Text nameLabel;
|
||||
|
||||
public string Text {
|
||||
get => nameLabel.text;
|
||||
set => nameLabel.text = value;
|
||||
}
|
||||
|
||||
override public void ColorRow() { gameObject.GetComponent<Image>().color = BackgroundColor; }
|
||||
|
||||
// Start is called before the first frame update
|
||||
void Start() {}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7b0b988957134dbc9c5e270bd5e9e70
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,9 +0,0 @@
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
|
||||
namespace UnityGoDiceInterface {
|
||||
public interface RollFetcher {
|
||||
public delegate void RollResultCallback(int? result);
|
||||
|
||||
public void GetRoll(RollRequest rollType, RollResultCallback cb);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aade83941cf241b398049ff3fe0475cb
|
||||
timeCreated: 1703812608
|
||||
-375
@@ -1,375 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGoDiceInterface;
|
||||
|
||||
public class RollPanelController : MonoBehaviour, RollFetcher {
|
||||
public DiceConfigurationPanelController diceConfigurationPanelController;
|
||||
public Button skipContinueButton;
|
||||
public TMP_Text skipContinueButtonText;
|
||||
private DiceInterface _diceInterface;
|
||||
|
||||
public TMP_Text tensResultLabel;
|
||||
public TMP_Text onesResultLabel;
|
||||
|
||||
public TMP_Text header;
|
||||
public TMP_Text reconnectionLabel;
|
||||
public TMP_Text runningTotalLabel;
|
||||
|
||||
private RollFetcher.RollResultCallback _rollResultCallback;
|
||||
private RollRequest _rollRequest;
|
||||
|
||||
private string logLocation;
|
||||
|
||||
enum OpenEndedState { None, High, Low, RollComplete }
|
||||
private OpenEndedState _openEndedState = OpenEndedState.None;
|
||||
|
||||
private readonly int _d100OpenEndedHighThreshold = 96;
|
||||
private readonly int _d100OpenEndedLowThreshold = 5;
|
||||
|
||||
private DateTime _lastRollTime = DateTime.MinValue;
|
||||
private readonly TimeSpan _disconnectionDelay = TimeSpan.FromMinutes(5);
|
||||
private readonly TimeSpan _displayTime = TimeSpan.FromSeconds(3);
|
||||
private readonly TimeSpan _openEndedResetTime = TimeSpan.FromSeconds(2);
|
||||
|
||||
public void GetRoll(RollRequest rollRequest, RollFetcher.RollResultCallback cb) {
|
||||
// Check for dice enabled
|
||||
if (PlayerPrefs.GetInt(SettingsPanelController.UseGoDiceKey, 0) == 0) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
skipContinueButton.gameObject.SetActive(true);
|
||||
if (_diceInterface == null) { _diceInterface = GetComponentInParent<DiceInterface>(); }
|
||||
|
||||
_onesResult = null;
|
||||
_tensResult = null;
|
||||
_runningTotal = 0;
|
||||
_openEndedState = OpenEndedState.None;
|
||||
|
||||
skipContinueButtonText.text = "Skip";
|
||||
|
||||
_rollResultCallback = cb;
|
||||
_rollRequest = rollRequest;
|
||||
|
||||
SetResultLabels();
|
||||
|
||||
logLocation = NewLogLocation();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(logLocation));
|
||||
|
||||
if (string.IsNullOrEmpty(_tensDieInfo.identifier) ||
|
||||
string.IsNullOrEmpty(_onesDieInfo.identifier)) {
|
||||
GetConfiguration();
|
||||
} else {
|
||||
_diceInterface.deviceFoundCallback = DeviceFoundCallback;
|
||||
_diceInterface.connectionCallback = ConnectionCallback;
|
||||
_diceInterface.rollCallback = RollCallback;
|
||||
_diceInterface.logger = LogFromNative;
|
||||
|
||||
// _diceInterface.StartListening();
|
||||
SetAlphas();
|
||||
|
||||
if (!_tensDieInfo.connected) { _diceInterface.Connect(_tensDieInfo.identifier); }
|
||||
if (!_onesDieInfo.connected) { _diceInterface.Connect(_onesDieInfo.identifier); }
|
||||
}
|
||||
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private DieInfo _tensDieInfo;
|
||||
private DieInfo _onesDieInfo;
|
||||
|
||||
private int? _tensResult = null;
|
||||
private int? _onesResult = null;
|
||||
private int _runningTotal = 0;
|
||||
|
||||
private void SetAlphas() {
|
||||
tensResultLabel.alpha = _tensDieInfo.connected ? 1.0f : 0.25f;
|
||||
onesResultLabel.alpha = _onesDieInfo.connected ? 1.0f : 0.25f;
|
||||
}
|
||||
|
||||
void OnEnable() {}
|
||||
|
||||
private void GetConfiguration() {
|
||||
diceConfigurationPanelController.GetConfiguration(ConfigurationCallback);
|
||||
}
|
||||
|
||||
public void ConfigureClicked() { GetConfiguration(); }
|
||||
|
||||
public void SkipClicked() {
|
||||
gameObject.SetActive(false);
|
||||
_rollResultCallback(_runningTotal);
|
||||
_diceInterface.StopListening();
|
||||
}
|
||||
|
||||
void ConfigurationCallback(DieInfo? tensOpt, DieInfo? onesOpt) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (tensOpt is DieInfo tens && onesOpt is DieInfo ones) {
|
||||
gameObject.SetActive(true);
|
||||
_diceInterface.connectionCallback = ConnectionCallback;
|
||||
_diceInterface.connectionFailedCallback = ConnectionFailedCallback;
|
||||
_diceInterface.disconnectionCallback = DisconnectionCallback;
|
||||
_diceInterface.listenerStoppedCallback = ListenerStoppedCallback;
|
||||
_diceInterface.rollCallback = RollCallback;
|
||||
|
||||
_tensDieInfo = tens;
|
||||
_onesDieInfo = ones;
|
||||
|
||||
reconnectionLabel.gameObject.SetActive(false);
|
||||
|
||||
if (!_tensDieInfo.connected) _diceInterface.Connect(_tensDieInfo.identifier);
|
||||
if (!_onesDieInfo.connected) _diceInterface.Connect(_onesDieInfo.identifier);
|
||||
|
||||
SetAlphas();
|
||||
SetResultLabels();
|
||||
|
||||
tensResultLabel.color = UnityDieColors.ColorFromDieColor(_tensDieInfo.color);
|
||||
onesResultLabel.color = UnityDieColors.ColorFromDieColor(_onesDieInfo.color);
|
||||
} else {
|
||||
gameObject.SetActive(false);
|
||||
_rollResultCallback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DeviceFoundCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if ((_tensDieInfo.identifier == identifier && !_tensDieInfo.connected) ||
|
||||
(_onesDieInfo.identifier == identifier && !_onesDieInfo.connected)) {
|
||||
_diceInterface.Connect(identifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionCallback(string identifier, string deviceName) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (identifier == _tensDieInfo.identifier) {
|
||||
_tensDieInfo.connected = true;
|
||||
} else if (identifier == _onesDieInfo.identifier) {
|
||||
_onesDieInfo.connected = true;
|
||||
}
|
||||
|
||||
SetAlphas();
|
||||
|
||||
if (_tensDieInfo.connected && _onesDieInfo.connected) {
|
||||
reconnectionLabel.gameObject.SetActive(false);
|
||||
_diceInterface.StopListening();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ConnectionFailedCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (identifier == _onesDieInfo.identifier || identifier == _tensDieInfo.identifier) {
|
||||
reconnectionLabel.gameObject.SetActive(true);
|
||||
_diceInterface.StartListening();
|
||||
} else {
|
||||
Debug.Log("Got connection failed for unknown identifier");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DisconnectionCallback(string identifier) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (identifier == _tensDieInfo.identifier) {
|
||||
_tensDieInfo.connected = false;
|
||||
SetAlphas();
|
||||
_diceInterface.Connect(identifier);
|
||||
} else if (identifier == _onesDieInfo.identifier) {
|
||||
_onesDieInfo.connected = false;
|
||||
SetAlphas();
|
||||
_diceInterface.Connect(identifier);
|
||||
} else {
|
||||
Debug.Log("Got disconnection callback for unknown identifier");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ListenerStoppedCallback() {
|
||||
MainQueue.Q.Enqueue(() => { reconnectionLabel.text = "Unable to connect."; });
|
||||
}
|
||||
|
||||
private readonly Dictionary<CommandType, string> _baseHeaderText = new() {
|
||||
{ CommandType.ArcheryCommand, "Archery Attack" },
|
||||
{ CommandType.BuildBridgeCommand, "Build Bridge" },
|
||||
{ CommandType.ChargeCommand, "Charge" },
|
||||
{ CommandType.BraveWaterCommand, "Brave Water" },
|
||||
{ CommandType.MeleeCommand, "Melee Attack" }
|
||||
};
|
||||
|
||||
private string BaseHeaderText(CommandType commandType) {
|
||||
if (_baseHeaderText.ContainsKey(commandType)) { return _baseHeaderText[commandType]; }
|
||||
|
||||
return "Action";
|
||||
}
|
||||
|
||||
void SetResultLabels() {
|
||||
var baseText = BaseHeaderText(_rollRequest.CommandType);
|
||||
string fullText = "";
|
||||
|
||||
if (_openEndedState == OpenEndedState.High) {
|
||||
fullText = $"You rolled OPEN ENDED HIGH! Keep rolling for {baseText}!";
|
||||
} else if (_openEndedState == OpenEndedState.Low) {
|
||||
fullText = $"You rolled OPEN ENDED LOW! Keep rolling for {baseText}!";
|
||||
} else {
|
||||
switch (_rollRequest.RollType) {
|
||||
case RollType.D100: fullText = $"Roll D100 for {baseText}"; break;
|
||||
case RollType.D100OpenEnded:
|
||||
fullText = $"Roll Open Ended D100 for {baseText}";
|
||||
break;
|
||||
case RollType.D100OpenEndedHigh:
|
||||
fullText = $"Roll Open Ended High D100 for {baseText}";
|
||||
break;
|
||||
case RollType.D100OpenEndedLow:
|
||||
fullText = $"Roll Open Ended Low D100 for {baseText}";
|
||||
break;
|
||||
case RollType.Unspecified:
|
||||
default: throw new ArgumentException("Invalid roll type");
|
||||
}
|
||||
}
|
||||
|
||||
if (_rollRequest.Odds is OddsView odds) {
|
||||
int minRoll = 101 - odds.SuccessChance;
|
||||
fullText += $" ({minRoll}+ to succeed)";
|
||||
}
|
||||
|
||||
header.text = fullText;
|
||||
|
||||
if (_tensResult is int tr) {
|
||||
tensResultLabel.text = tr.ToString();
|
||||
} else {
|
||||
tensResultLabel.text = "?";
|
||||
}
|
||||
|
||||
if (_onesResult is int or) {
|
||||
onesResultLabel.text = or.ToString();
|
||||
} else {
|
||||
onesResultLabel.text = "?";
|
||||
}
|
||||
|
||||
runningTotalLabel.text = $"Running total: {_runningTotal}";
|
||||
runningTotalLabel.gameObject.SetActive(_runningTotal != 0);
|
||||
}
|
||||
|
||||
void RollCallback(string identifier, sbyte x, sbyte y, sbyte z) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
if (gameObject.activeSelf) {
|
||||
var d10Roll = DiceVectors.D10Value(x, y, z);
|
||||
|
||||
if (identifier == _tensDieInfo.identifier) {
|
||||
if (_tensResult == null) { _tensResult = d10Roll; }
|
||||
} else if (identifier == _onesDieInfo.identifier) {
|
||||
if (_onesResult == null) { _onesResult = d10Roll; }
|
||||
}
|
||||
|
||||
SetResultLabels();
|
||||
CheckCompletion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HandleOneRoll(int newD100Result) {
|
||||
if (_openEndedState == OpenEndedState.Low) {
|
||||
_runningTotal -= newD100Result;
|
||||
} else {
|
||||
_runningTotal += newD100Result;
|
||||
}
|
||||
|
||||
// Rolls in the middle always end the open ended state.
|
||||
if (newD100Result > _d100OpenEndedLowThreshold &&
|
||||
newD100Result < _d100OpenEndedHighThreshold) {
|
||||
_openEndedState = OpenEndedState.RollComplete;
|
||||
return;
|
||||
}
|
||||
|
||||
// For a LOW roll, if we are currently in a None state, move to OpenEnded
|
||||
// Low so we can roll again, but if we were already there, this ends the roll.
|
||||
if (newD100Result <= _d100OpenEndedLowThreshold) {
|
||||
if ((_rollRequest.RollType == RollType.D100OpenEnded ||
|
||||
_rollRequest.RollType == RollType.D100OpenEndedLow) &&
|
||||
_openEndedState == OpenEndedState.None) {
|
||||
_openEndedState = OpenEndedState.Low;
|
||||
} else {
|
||||
_openEndedState = OpenEndedState.RollComplete;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point we know the current roll is high. If we were already in OpenEndedLow,
|
||||
// stay there and keep subtracting, otherwise move into High.
|
||||
if (_openEndedState == OpenEndedState.Low) {
|
||||
_openEndedState = OpenEndedState.Low;
|
||||
} else {
|
||||
_openEndedState = OpenEndedState.High;
|
||||
}
|
||||
}
|
||||
|
||||
void CheckCompletion() {
|
||||
if (_onesResult is int ones && _tensResult is int tens) {
|
||||
// FIXME: open ended rolls
|
||||
var currentResult = tens * 10 + ones;
|
||||
|
||||
HandleOneRoll(currentResult);
|
||||
// TODO: flash the dice here for high rolls
|
||||
|
||||
if (_openEndedState == OpenEndedState.RollComplete) {
|
||||
skipContinueButtonText.text = "Dismiss";
|
||||
SetResultLabels();
|
||||
StartCoroutine(AfterDelay(_displayTime, () => {
|
||||
if (gameObject.activeSelf) {
|
||||
gameObject.SetActive(false);
|
||||
_rollResultCallback(_runningTotal);
|
||||
}
|
||||
}));
|
||||
|
||||
// Disconnect only after _disconnectionDelay time
|
||||
_lastRollTime = DateTime.Now;
|
||||
StartCoroutine(AfterDelay(_disconnectionDelay, () => {
|
||||
if (DateTime.Now - _lastRollTime > _disconnectionDelay) {
|
||||
_diceInterface.StopListening();
|
||||
|
||||
_tensDieInfo.connected = false;
|
||||
_onesDieInfo.connected = false;
|
||||
_diceInterface.Disconnect(_tensDieInfo.identifier);
|
||||
_diceInterface.Disconnect(_onesDieInfo.identifier);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// We keep rolling, so reset the dice and keep going.
|
||||
StartCoroutine(AfterDelay(_openEndedResetTime, () => {
|
||||
_tensResult = null;
|
||||
_onesResult = null;
|
||||
SetResultLabels();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator AfterDelay(TimeSpan timeSpan, Action action) {
|
||||
yield return new WaitForSeconds(timeSpan.Seconds);
|
||||
action();
|
||||
}
|
||||
|
||||
void LogFromNative(string log) {
|
||||
MainQueue.Q.Enqueue(() => {
|
||||
Debug.Log(log);
|
||||
File.AppendAllText(logLocation, log);
|
||||
});
|
||||
}
|
||||
|
||||
string NewLogLocation() {
|
||||
return Path.Combine(
|
||||
Application.persistentDataPath,
|
||||
"eagle0",
|
||||
"Resources",
|
||||
"RollPanelLogs",
|
||||
DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss") + ".log");
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db0669ab8b7684b2bb7eb30a0e66f5e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityGoDiceInterface {
|
||||
public class UnityDieColors {
|
||||
public static Color ColorFromDieColor(DieColor dieColor) {
|
||||
switch (dieColor) {
|
||||
case DieColor.DieColorBlack: return Color.black;
|
||||
case DieColor.DieColorRed: return Color.red;
|
||||
case DieColor.DieColorGreen: return Color.green;
|
||||
case DieColor.DieColorBlue: return Color.blue;
|
||||
case DieColor.DieColorYellow: return Color.yellow;
|
||||
case DieColor.DieColorOrange: return new Color(1.0f, 0.5f, 0.0f);
|
||||
default: throw new ArgumentOutOfRangeException(nameof(dieColor), dieColor, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d26c35650c67416e86ab5a26906cfb5a
|
||||
timeCreated: 1703173073
|
||||
+1
-4
@@ -20,8 +20,6 @@ using Random = System.Random;
|
||||
using VictoryCondition = Net.Eagle0.Common.VictoryCondition;
|
||||
|
||||
public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
public RollPanelController rollPanelController;
|
||||
|
||||
public GameObject yourArmiesArea;
|
||||
public GameObject aiArmiesArea;
|
||||
public Canvas shardokCanvas;
|
||||
@@ -103,8 +101,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
players: players,
|
||||
heroNameTextIds: _heroNames,
|
||||
heroImages: _heroImages,
|
||||
battalionNames: new Dictionary<int, string> { { 0, "no name" } },
|
||||
rollFetcher: rollPanelController);
|
||||
battalionNames: new Dictionary<int, string> { { 0, "no name" } });
|
||||
|
||||
shardokCanvas.gameObject.SetActive(true);
|
||||
shardokCanvas.GetComponent<ShardokGameController>().SetUpGame(_shardokModel);
|
||||
|
||||
+4
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Eagle0.Tutorial;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using UnityEngine;
|
||||
@@ -88,6 +89,9 @@ namespace eagle {
|
||||
_model = model;
|
||||
_availableCommand = ac;
|
||||
SetUpUI();
|
||||
|
||||
// Trigger tutorial for this command type (fires every time command is selected)
|
||||
TutorialManager.Instance?.TriggerRegistry?.OnCommandPanelShown(CommandType);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-11
@@ -22,8 +22,6 @@ namespace eagle {
|
||||
public class EagleGameController : MonoBehaviour {
|
||||
public SoundManager soundManager;
|
||||
|
||||
public RollPanelController rollPanelController;
|
||||
|
||||
public TextMeshProUGUI roundStatusLabel;
|
||||
public TextMeshProUGUI connectionStatusLabel;
|
||||
private bool _connectionStatusUIInitialized = false;
|
||||
@@ -248,11 +246,7 @@ namespace eagle {
|
||||
int? playerId,
|
||||
PersistentClientConnection persistentClientConnection,
|
||||
string environmentName = null) {
|
||||
ModelUpdater = new GameModelUpdater(
|
||||
gameId,
|
||||
playerId,
|
||||
persistentClientConnection,
|
||||
rollPanelController) {
|
||||
ModelUpdater = new GameModelUpdater(gameId, playerId, persistentClientConnection) {
|
||||
UpdateAction = ModelUpdated,
|
||||
NoteRecipient = (title, text, llmId, pids, displayedHeroes) =>
|
||||
notificationPanel.AddNote(title, text, llmId, pids, displayedHeroes)
|
||||
@@ -284,11 +278,13 @@ namespace eagle {
|
||||
MainQueue.Q.EnqueueForNextUpdate(
|
||||
() => { _ = ModelUpdater.StartListeningForUpdates(); });
|
||||
|
||||
// Initialize tutorial system
|
||||
// Initialize tutorial system (onboarding starts after first model update in SwapModel)
|
||||
TutorialManager.Instance?.Initialize(this, null);
|
||||
if (TutorialManager.Instance != null &&
|
||||
!TutorialManager.Instance.State.OnboardingCompleted) {
|
||||
TutorialManager.Instance.StartOnboarding();
|
||||
// Register command panel for tutorial positioning
|
||||
if (TutorialManager.Instance?.TargetRegistry != null && commandPanel != null) {
|
||||
TutorialManager.Instance.TargetRegistry.RegisterTarget(
|
||||
"CommandPanel",
|
||||
commandPanel.GetComponent<RectTransform>());
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -507,6 +503,12 @@ namespace eagle {
|
||||
|
||||
if (Model == null) { return; }
|
||||
|
||||
// Start onboarding after first model update (UI panels are now populated)
|
||||
if (oldModel == null && TutorialManager.Instance != null &&
|
||||
!TutorialManager.Instance.State.OnboardingCompleted) {
|
||||
TutorialManager.Instance.StartOnboarding();
|
||||
}
|
||||
|
||||
PrefetchHeadshots(Model);
|
||||
|
||||
SetMusic();
|
||||
|
||||
@@ -13,7 +13,6 @@ using Net.Eagle0.Eagle.Views;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityGoDiceInterface;
|
||||
using Logger = common.Logger;
|
||||
using Notification = eagle.Notifications.Notification;
|
||||
|
||||
@@ -58,8 +57,6 @@ namespace eagle {
|
||||
public List<ProvinceId> ProvincesForFaction(FactionId factionId);
|
||||
|
||||
public List<ChronicleEntry> ChronicleEntries { get; }
|
||||
|
||||
RollFetcher RollFetcher { get; }
|
||||
}
|
||||
|
||||
public class GameModelUpdater : IClientConnectionSubscriber, IGameStateProvider {
|
||||
@@ -150,8 +147,6 @@ namespace eagle {
|
||||
private readonly ConcurrentDictionary<string, int> _shardokResultCounts =
|
||||
new ConcurrentDictionary<string, int>();
|
||||
|
||||
private readonly RollFetcher _rollFetcher;
|
||||
|
||||
// State synced with server
|
||||
private struct ConcreteGameModel : IGameModel {
|
||||
public PlayerId? PlayerId { get; set; }
|
||||
@@ -220,8 +215,6 @@ namespace eagle {
|
||||
var faction = MaybeDestroyedFaction(factionId);
|
||||
textUpdater.SetFactionText(textComponent, faction, this, template, fallbackText);
|
||||
}
|
||||
|
||||
public RollFetcher RollFetcher { get; set; }
|
||||
}
|
||||
|
||||
private ConcreteGameModel _currentModel;
|
||||
@@ -229,11 +222,9 @@ namespace eagle {
|
||||
public GameModelUpdater(
|
||||
GameId eagleGameId,
|
||||
FactionId? factionId,
|
||||
PersistentClientConnection pers,
|
||||
RollFetcher rollFetcher) {
|
||||
PersistentClientConnection pers) {
|
||||
GameId = eagleGameId;
|
||||
PersistentConnection = pers;
|
||||
this._rollFetcher = rollFetcher;
|
||||
|
||||
_currentModel.LastPostedToken = -1;
|
||||
_currentModel.PlayerId = factionId;
|
||||
@@ -245,8 +236,6 @@ namespace eagle {
|
||||
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
|
||||
|
||||
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
|
||||
|
||||
_currentModel.RollFetcher = rollFetcher;
|
||||
}
|
||||
|
||||
private ShardokGameModel MakeGameModel(ShardokGameId shardokGameId) {
|
||||
@@ -294,8 +283,7 @@ namespace eagle {
|
||||
kv => kv.Value.ImagePath),
|
||||
battalionNames: _currentModel.BattalionNames.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value),
|
||||
rollFetcher: _rollFetcher);
|
||||
kv => kv.Value));
|
||||
|
||||
return shardokModel;
|
||||
}
|
||||
|
||||
+20
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using common;
|
||||
using Eagle0.Tutorial;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -136,6 +137,25 @@ namespace eagle {
|
||||
row.Hero = hero;
|
||||
SetHeroRowSelections(row, hero);
|
||||
});
|
||||
|
||||
// Register hero rows with tutorial system for highlighting
|
||||
RegisterHeroRowsForTutorial();
|
||||
}
|
||||
|
||||
private void RegisterHeroRowsForTutorial() {
|
||||
var registry = TutorialManager.Instance?.TargetRegistry;
|
||||
if (registry == null) return;
|
||||
|
||||
// Register first row as WarlordRow (index 0)
|
||||
var warlordRow = heroesTable.GetRowRectTransform(0);
|
||||
if (warlordRow != null) { registry.RegisterTarget("WarlordRow", warlordRow); }
|
||||
|
||||
// Register rows 1 and 2 as VassalRows
|
||||
var vassalRow1 = heroesTable.GetRowRectTransform(1);
|
||||
if (vassalRow1 != null) { registry.RegisterTarget("VassalRow1", vassalRow1); }
|
||||
|
||||
var vassalRow2 = heroesTable.GetRowRectTransform(2);
|
||||
if (vassalRow2 != null) { registry.RegisterTarget("VassalRow2", vassalRow2); }
|
||||
}
|
||||
|
||||
private void SetUpBattalionsTable() {
|
||||
|
||||
+44
-7
@@ -196,6 +196,20 @@ namespace eagle {
|
||||
/// </summary>
|
||||
private async Task<bool> StreamOneGameAsync(IClientConnectionSubscriber subscriber) {
|
||||
var gameId = subscriber.GameId;
|
||||
|
||||
// Send JoinGameRequest first to ensure game is loaded on server.
|
||||
// For already-started games, this returns GAME_FULL but still triggers
|
||||
// game loading, which prevents "key not found" errors on subsequent commands.
|
||||
var joinRequest = new UpdateStreamRequest {
|
||||
JoinGameRequest = new JoinGameRequest { GameId = gameId }
|
||||
};
|
||||
await DoWithStreamingCall(async (sc) => {
|
||||
await sc.RequestStream.WriteAsync(joinRequest);
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[SUBSCRIBE] Sent JoinGameRequest for game {gameId} to ensure game is loaded");
|
||||
return true;
|
||||
});
|
||||
|
||||
var streamGameRequest = new StreamGameRequest {
|
||||
GameId = gameId,
|
||||
UnfilteredResultCount = subscriber.LastUnfilteredResultCount
|
||||
@@ -810,12 +824,19 @@ namespace eagle {
|
||||
await Task.WhenAny(actionTask, timeoutTask).ConfigureAwait(false);
|
||||
|
||||
if (completedTask == timeoutTask) {
|
||||
// WriteAsync timed out - connection is likely dead
|
||||
// WriteAsync timed out - connection is dead
|
||||
LogConnectionEvent(
|
||||
"write_timeout",
|
||||
$"WriteAsync timed out after {WriteAsyncTimeoutMs}ms");
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[WRITE_TIMEOUT] WriteAsync timed out after {WriteAsyncTimeoutMs}ms, connection may be dead");
|
||||
$"[WRITE_TIMEOUT] WriteAsync timed out after {WriteAsyncTimeoutMs}ms, triggering reconnect");
|
||||
|
||||
// Dispose the dead connection and schedule reconnect
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
}
|
||||
ScheduleReconnect("WriteAsyncTimeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -823,15 +844,31 @@ namespace eagle {
|
||||
timeoutCts.Cancel();
|
||||
return await actionTask.ConfigureAwait(false);
|
||||
} catch (RpcException e) {
|
||||
if (e.StatusCode == StatusCode.Cancelled) {
|
||||
// This is expected when the connection is closed.
|
||||
return false;
|
||||
} else {
|
||||
throw;
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[WRITE_ERROR] RpcException: {e.StatusCode} - {e.Message}");
|
||||
// Connection is broken - dispose and reconnect
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
}
|
||||
if (e.StatusCode != StatusCode.Cancelled) {
|
||||
// Cancelled is expected during normal shutdown, don't reconnect
|
||||
ScheduleReconnect($"RpcException-{e.StatusCode}");
|
||||
}
|
||||
return false;
|
||||
} catch (OperationCanceledException) {
|
||||
// Timeout was triggered
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[WRITE_ERROR] Exception: {e.GetType().Name} - {e.Message}");
|
||||
// Unknown error - dispose and reconnect
|
||||
lock (this) {
|
||||
_streamingCall?.Dispose();
|
||||
_streamingCall = null;
|
||||
}
|
||||
ScheduleReconnect($"WriteException-{e.GetType().Name}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes Sparkle auto-updater at app startup.
|
||||
/// Uses RuntimeInitializeOnLoadMethod to ensure early initialization.
|
||||
/// </summary>
|
||||
public static class SparkleInitializer {
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void Initialize() {
|
||||
Debug.Log("SparkleInitializer: Initializing Sparkle at startup");
|
||||
SparkleUpdater.Initialize();
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 680b7d1179eb34a0fa338286236db2e9
|
||||
guid: 2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
executionOrder: -100
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// C# wrapper for the native SparklePlugin on macOS.
|
||||
/// Provides auto-update functionality via the Sparkle framework.
|
||||
/// On non-macOS platforms, all methods are no-ops.
|
||||
/// </summary>
|
||||
public static class SparkleUpdater {
|
||||
#if UNITY_STANDALONE_OSX && !UNITY_EDITOR
|
||||
private const string PluginName = "SparklePlugin";
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern void SparklePlugin_Initialize();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern void SparklePlugin_CheckForUpdates();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern void SparklePlugin_CheckForUpdatesInBackground();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern int SparklePlugin_IsCheckingForUpdates();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern int SparklePlugin_GetAutomaticallyChecksForUpdates();
|
||||
|
||||
[DllImport(PluginName)]
|
||||
private static extern void SparklePlugin_SetAutomaticallyChecksForUpdates(int enabled);
|
||||
|
||||
private static bool _initialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the Sparkle updater. Should be called once at app startup.
|
||||
/// This starts automatic background update checks based on Info.plist settings.
|
||||
/// </summary>
|
||||
public static void Initialize() {
|
||||
if (_initialized) {
|
||||
Debug.Log("SparkleUpdater: Already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("SparkleUpdater: Initializing Sparkle");
|
||||
try {
|
||||
SparklePlugin_Initialize();
|
||||
_initialized = true;
|
||||
Debug.Log("SparkleUpdater: Initialization complete");
|
||||
} catch (System.Exception e) {
|
||||
Debug.LogError($"SparkleUpdater: Failed to initialize: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually check for updates. Shows the update UI to the user.
|
||||
/// </summary>
|
||||
public static void CheckForUpdates() {
|
||||
if (!_initialized) {
|
||||
Debug.LogWarning("SparkleUpdater: Not initialized");
|
||||
return;
|
||||
}
|
||||
SparklePlugin_CheckForUpdates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for updates silently in the background.
|
||||
/// Only shows UI if an update is found.
|
||||
/// </summary>
|
||||
public static void CheckForUpdatesInBackground() {
|
||||
if (!_initialized) {
|
||||
Debug.LogWarning("SparkleUpdater: Not initialized");
|
||||
return;
|
||||
}
|
||||
SparklePlugin_CheckForUpdatesInBackground();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an update check is currently in progress.
|
||||
/// </summary>
|
||||
public static bool IsCheckingForUpdates {
|
||||
get {
|
||||
if (!_initialized) return false;
|
||||
return SparklePlugin_IsCheckingForUpdates() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether automatic update checks are enabled.
|
||||
/// </summary>
|
||||
public static bool AutomaticallyChecksForUpdates {
|
||||
get {
|
||||
if (!_initialized) return false;
|
||||
return SparklePlugin_GetAutomaticallyChecksForUpdates() != 0;
|
||||
}
|
||||
set {
|
||||
if (!_initialized) return;
|
||||
SparklePlugin_SetAutomaticallyChecksForUpdates(value? 1: 0);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
// Stub implementations for non-macOS platforms and Editor
|
||||
|
||||
public static void Initialize() { Debug.Log("SparkleUpdater: Not available on this platform"); }
|
||||
|
||||
public static void CheckForUpdates() {
|
||||
Debug.Log("SparkleUpdater: Not available on this platform");
|
||||
}
|
||||
|
||||
public static void CheckForUpdatesInBackground() {
|
||||
Debug.Log("SparkleUpdater: Not available on this platform");
|
||||
}
|
||||
|
||||
public static bool IsCheckingForUpdates => false;
|
||||
|
||||
public static bool AutomaticallyChecksForUpdates {
|
||||
get => false;
|
||||
set {}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75ea5d911cfab4851831d1de4b61f559
|
||||
guid: 8a3b5c7d9e1f2a3b4c5d6e7f8a9b0c1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,6 @@
|
||||
System.Runtime.CompilerServices.Unsafe/**
|
||||
!**/*.psd
|
||||
**/*.dll
|
||||
# Native plugins built at CI time
|
||||
DarwinGodiceBundle.bundle/
|
||||
macOS/SparklePlugin.bundle/
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fc634da07f5047c99dba5ce558b5886
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c5d7e9f1a2b4c6d8e0f2a4b6c8d0e2f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+18
-6
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5b6db3c9b9e34b82ae9163a61276c8a
|
||||
guid: 4d6e8f0a2b3c5d7e9f1a3b5c7d9e1f3a
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -12,22 +12,34 @@ PluginImporter:
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
: Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Editor: 1
|
||||
Exclude Linux64: 1
|
||||
Exclude OSXUniversal: 0
|
||||
Exclude Win: 1
|
||||
Exclude Win64: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 1
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -23,10 +23,6 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
public Slider tileBorderWidthSlider;
|
||||
public TMP_Text tileBorderWidthLabel;
|
||||
|
||||
public Toggle useGoDiceToggle;
|
||||
public Button testGoDice;
|
||||
public RollPanelController rollPanelController;
|
||||
|
||||
public Toggle tooltipHugsCursorToggle;
|
||||
public HoveringTooltip hoveringTooltip;
|
||||
|
||||
@@ -42,7 +38,6 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
|
||||
private const string SoundEffectsVolumeKey = "soundEffectsVolume";
|
||||
private const string MusicVolumeKey = "musicVolume";
|
||||
public const string UseGoDiceKey = "useGoDice";
|
||||
private const string TooltipHugsCursorKey = "tooltipHugsCursor";
|
||||
private const string AnimationToggleKey = "animationToggle";
|
||||
|
||||
@@ -62,7 +57,6 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
|
||||
soundEffectsSlider.value = soundEffectsSource.volume;
|
||||
musicSlider.value = musicSource.volume;
|
||||
useGoDiceToggle.isOn = PlayerPrefs.GetInt(UseGoDiceKey, 0) == 1;
|
||||
|
||||
tooltipHugsCursorToggle.isOn = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
|
||||
hoveringTooltip.HugsCursor = tooltipHugsCursorToggle.isOn;
|
||||
@@ -111,17 +105,6 @@ public class SettingsPanelController : MonoBehaviour {
|
||||
PlayerPrefs.SetFloat(MusicVolumeKey, val);
|
||||
}
|
||||
|
||||
public void OnUseGoDiceToggleChange(bool val) {
|
||||
PlayerPrefs.SetInt(UseGoDiceKey, val ? 1 : 0);
|
||||
testGoDice.interactable = val;
|
||||
}
|
||||
|
||||
public void OnTestGoDiceClick() {
|
||||
rollPanelController.GetRoll(
|
||||
new RollRequest { RollType = RollType.D100OpenEnded },
|
||||
(result) => { Console.Out.WriteLine($"Got ${result}"); });
|
||||
}
|
||||
|
||||
public void OnCursorHugToggleChange(bool val) {
|
||||
PlayerPrefs.SetInt(TooltipHugsCursorKey, val ? 1 : 0);
|
||||
hoveringTooltip.HugsCursor = val;
|
||||
|
||||
@@ -6,7 +6,6 @@ using eagle0;
|
||||
using UnityEngine;
|
||||
using Net.Eagle0.Shardok.Api;
|
||||
using Net.Eagle0.Shardok.Common;
|
||||
using UnityGoDiceInterface;
|
||||
using BattalionId = System.Int32;
|
||||
using UnitId = System.Int32;
|
||||
using PlayerId = System.Int32;
|
||||
@@ -133,8 +132,6 @@ public class ShardokGameModel {
|
||||
|
||||
private const int StartingHistoryCapacity = 100;
|
||||
|
||||
private readonly RollFetcher _rollFetcher;
|
||||
|
||||
public ShardokGameModel(
|
||||
EagleGameId eagleGameId,
|
||||
ShardokGameId shardokGameId,
|
||||
@@ -146,13 +143,11 @@ public class ShardokGameModel {
|
||||
List<PlayerWithHostility> players,
|
||||
Dictionary<HeroId, String> heroNameTextIds,
|
||||
Dictionary<HeroId, String> heroImages,
|
||||
Dictionary<BattalionId, String> battalionNames,
|
||||
RollFetcher rollFetcher) {
|
||||
Dictionary<BattalionId, String> battalionNames) {
|
||||
EagleGameId = eagleGameId;
|
||||
ShardokGameId = shardokGameId;
|
||||
History = new List<ActionResultView>(StartingHistoryCapacity);
|
||||
_persistentClientConnection = persistentClientConnection;
|
||||
this._rollFetcher = rollFetcher;
|
||||
PlayerId = playerId;
|
||||
this.players = new List<PlayerWithHostility>(players);
|
||||
|
||||
@@ -350,25 +345,14 @@ public class ShardokGameModel {
|
||||
var index = AvailableCommands.IndexOf(action);
|
||||
AvailableCommands.Clear();
|
||||
|
||||
if (action.RollRequest != null) {
|
||||
_rollFetcher.GetRoll(action.RollRequest, roll => {
|
||||
_persistentClientConnection.PostShardokCommand(
|
||||
EagleGameId,
|
||||
ShardokGameId,
|
||||
PlayerId,
|
||||
History.Count(),
|
||||
index,
|
||||
roll);
|
||||
});
|
||||
} else {
|
||||
_persistentClientConnection.PostShardokCommand(
|
||||
EagleGameId,
|
||||
ShardokGameId,
|
||||
PlayerId,
|
||||
History.Count(),
|
||||
index,
|
||||
null);
|
||||
}
|
||||
// Physical dice roll support removed - server generates random rolls
|
||||
_persistentClientConnection.PostShardokCommand(
|
||||
EagleGameId,
|
||||
ShardokGameId,
|
||||
PlayerId,
|
||||
History.Count(),
|
||||
index,
|
||||
null);
|
||||
}
|
||||
|
||||
private void PostGameSetupActions() {
|
||||
|
||||
+465
-66
@@ -16,6 +16,9 @@ namespace Eagle0.Tutorial {
|
||||
var onboarding = CreateOnboardingSequence();
|
||||
manager.OnboardingSequence = onboarding;
|
||||
|
||||
// Register command panel tutorials (shown after onboarding)
|
||||
RegisterCommandPanelTutorials(registry);
|
||||
|
||||
// Register strategic contextual tutorials
|
||||
RegisterStrategicTutorials(registry);
|
||||
|
||||
@@ -23,7 +26,7 @@ namespace Eagle0.Tutorial {
|
||||
RegisterTacticalTutorials(registry);
|
||||
}
|
||||
|
||||
// ========== ONBOARDING SEQUENCE (13 steps) ==========
|
||||
// ========== ONBOARDING SEQUENCE ==========
|
||||
|
||||
private static TutorialSequence CreateOnboardingSequence() {
|
||||
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
|
||||
@@ -37,60 +40,146 @@ namespace Eagle0.Tutorial {
|
||||
StepId = "welcome",
|
||||
Title = "Welcome to Eagle0",
|
||||
Description =
|
||||
"Command your armies, recruit heroes, and conquer provinces in this turn-based strategy game.\n\nLet's walk through the basics!",
|
||||
"You are a new ruler in a fractured realm. Build your power, recruit heroes, and expand your domain.\n\n" +
|
||||
"Let's look at your first province!\n\n" +
|
||||
"<size=80%><color=#888888>(You can skip this tutorial and restart it later from Settings.)</color></size>",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 2: Map overview - select a province
|
||||
// Step 2: Province Info Panel - Stats Overview
|
||||
new TutorialStep {
|
||||
StepId = "select_province",
|
||||
Title = "The Strategic Map",
|
||||
StepId = "province_stats",
|
||||
Title = "Province Statistics",
|
||||
Description =
|
||||
"This is your kingdom. Each colored region is a province.\n\nTap a province you control (shown in your color) to see what you can do there.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "province_selected",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 3: Province info panel
|
||||
new TutorialStep {
|
||||
StepId = "province_panel",
|
||||
Title = "Province Information",
|
||||
Description =
|
||||
"This panel shows province details: its name, terrain, any armies present, and the commands available to you.\n\nCommands let you move troops, recruit heroes, and more.",
|
||||
"This panel shows your province's vital statistics. <b>Hover over icons and numbers</b> for detailed tooltips.\n\n" +
|
||||
"• <b>Agriculture</b> produces Food to feed your troops\n" +
|
||||
"• <b>Economy</b> generates Gold for hiring, gifts, and equipment\n" +
|
||||
"• <b>Infrastructure</b> improves troop armament and disaster resilience\n\n" +
|
||||
"All three stats increase your storage capacity.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Right,
|
||||
AdjacentTargetPath = "ProvinceInfoPanel",
|
||||
TargetGameObjectPath = "ProvinceInfoPanel",
|
||||
HighlightPulsing = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 4: Try March command
|
||||
// Step 3: Province Info Panel - Support (CRITICAL)
|
||||
new TutorialStep {
|
||||
StepId = "try_march",
|
||||
Title = "Issue a Command",
|
||||
StepId = "province_support",
|
||||
Title = "Support is Critical!",
|
||||
Description =
|
||||
"Try issuing a March command to move your army to an adjacent province.\n\nSelect a destination and confirm the order.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
// TODO: Add TargetGameObjectPath once we know the actual UI hierarchy
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "command_issued",
|
||||
"<b>Support</b> measures how much your people trust you. This is your most important number right now!\n\n" +
|
||||
"You need <b>40 Support by January</b> to collect taxes. Taxes provide both <b>Gold</b> and <b>Food</b>:\n" +
|
||||
"• <b>Food</b> feeds your troops and lets you Give Alms\n" +
|
||||
"• <b>Gold</b> pays for hiring, hero gifts, equipment, and more\n\n" +
|
||||
"Use <b>Improve</b> and <b>Give Alms</b> to raise Support quickly.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Right,
|
||||
AdjacentTargetPath = "ProvinceInfoPanel",
|
||||
TargetGameObjectPath = "SupportField",
|
||||
HighlightPulsing = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 5: Turn cycle explanation
|
||||
// Step 4: Heroes Panel - Warlord
|
||||
new TutorialStep {
|
||||
StepId = "heroes_warlord",
|
||||
Title = "Your Warlord",
|
||||
Description =
|
||||
"The <b>Resident Heroes</b> panel shows heroes in this province. The first hero is your <b>Warlord</b> - this is essentially <i>you</i>.\n\n" +
|
||||
"<b>Hover over a hero</b> to see their backstory, stats, and abilities. Your Warlord has a special profession that grants unique powers.\n\n" +
|
||||
"<color=#FF6666>Your Warlord is very important - protect them!</color>",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Right,
|
||||
AdjacentTargetPath = "HeroesPanel",
|
||||
TargetGameObjectPath = "WarlordRow",
|
||||
HighlightPulsing = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 5: Heroes Panel - Vassals
|
||||
new TutorialStep {
|
||||
StepId = "heroes_vassals",
|
||||
Title = "Your Vassals",
|
||||
Description =
|
||||
"The other heroes in your service are your <b>Vassals</b>. They lead troops, govern provinces, and perform tasks for you.\n\n" +
|
||||
"<b>Watch their Loyalty!</b> At the end of each year, vassal loyalty may drop. If it falls below <b>50</b>, they might leave your service - or worse.\n\n" +
|
||||
"Keep vassals happy with gifts and feasts!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Right,
|
||||
AdjacentTargetPath = "HeroesPanel",
|
||||
TargetGameObjectPath = "VassalRow1",
|
||||
AdditionalHighlightTargets = new[] { "VassalRow2" },
|
||||
HighlightPulsing = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 6: Command Buttons - Focus on Improve/Alms
|
||||
new TutorialStep {
|
||||
StepId = "command_buttons",
|
||||
Title = "Province Commands",
|
||||
Description =
|
||||
"The buttons below let you issue commands. <b>Feel free to click them</b> - nothing happens until you click <b>Commit</b>!\n\n" +
|
||||
"For now, focus on:\n" +
|
||||
"• <b>Improve</b> - Raises province stats AND Support\n" +
|
||||
"• <b>Give Alms</b> - Quickly boosts Support (costs Food)\n\n" +
|
||||
"Get to <b>40 Support before January</b> to collect taxes!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
PanelAnchor = TutorialPanelAnchor.Top,
|
||||
AdjacentTargetPath = "CommandButtonsPanel",
|
||||
TargetGameObjectPath = "CommandButtonsPanel",
|
||||
HighlightPulsing = true,
|
||||
HighlightBoundsFromChildren = true,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 7: Turn cycle explanation
|
||||
new TutorialStep {
|
||||
StepId = "turn_cycle",
|
||||
Title = "The Turn Cycle",
|
||||
Description =
|
||||
"Eagle0 uses simultaneous turns. All players give orders at the same time, then turns resolve together.\n\nWhen all players are ready, the server processes everyone's commands and shows the results.",
|
||||
"Eagle0 uses <b>simultaneous turns</b>. All players give orders at the same time, then everything resolves together.\n\n" +
|
||||
"Your turn ends when you've given an order in each of your provinces. Once all players are ready, the server processes commands and shows results.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 6: Hidden wait for battle
|
||||
// Step 8: Completion of strategic intro
|
||||
new TutorialStep {
|
||||
StepId = "strategic_complete",
|
||||
Title = "You're Ready to Begin!",
|
||||
Description =
|
||||
"That's the basics of managing your province. Remember:\n\n" +
|
||||
"• Get <b>Support to 40</b> before January for taxes (Gold + Food)\n" +
|
||||
"• <b>Hover over everything</b> for tooltips\n" +
|
||||
"• Protect your <b>Warlord</b> at all costs!\n" +
|
||||
"• Keep your <b>Vassals</b> loyal with gifts\n\n" +
|
||||
"When armies clash, you'll learn tactical combat. Good luck, ruler!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = false,
|
||||
// Mark onboarding complete here so command tutorials can appear
|
||||
// Tactical tutorial continues in background when battle becomes available
|
||||
MarksOnboardingComplete = true
|
||||
},
|
||||
|
||||
// === TACTICAL TUTORIAL (triggered later when battle occurs) ===
|
||||
|
||||
// Step 9: Hidden wait for battle
|
||||
new TutorialStep {
|
||||
StepId = "wait_for_battle",
|
||||
Title = "",
|
||||
@@ -101,103 +190,413 @@ namespace Eagle0.Tutorial {
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 7: Battle introduction
|
||||
// Step 10: Battle introduction
|
||||
new TutorialStep {
|
||||
StepId = "battle_intro",
|
||||
Title = "Battle Time!",
|
||||
Description =
|
||||
"When armies collide, you'll fight tactical battles on a hex grid.\n\nYou command individual units - infantry, cavalry, archers, and heroes with special abilities.",
|
||||
"When armies collide, you fight tactical battles on a hex grid.\n\n" +
|
||||
"You command individual units - infantry, cavalry, archers, and heroes with special abilities.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 8: Battle button highlight
|
||||
// Step 11: Battle button highlight
|
||||
new TutorialStep {
|
||||
StepId = "enter_battle",
|
||||
Title = "Enter the Battle",
|
||||
Description = "Tap the Battle button to enter tactical combat.",
|
||||
Description = "Tap the <b>Battle</b> button to enter tactical combat.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
// TODO: Add TargetGameObjectPath once we know the actual UI hierarchy
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "battle_entered",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 9: Tactical overview
|
||||
// Step 12: Tactical overview
|
||||
new TutorialStep {
|
||||
StepId = "tactical_overview",
|
||||
Title = "Tactical Combat",
|
||||
Description =
|
||||
"Each unit has movement points and attack power. Position your troops wisely!\n\nUnits attack adjacent enemies. Flanking (attacking from multiple sides) deals bonus damage.",
|
||||
"Each unit has movement points and attack power. Position your troops wisely!\n\n" +
|
||||
"Units attack adjacent enemies. <b>Flanking</b> (attacking from multiple sides) deals bonus damage.",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 10: Move a unit
|
||||
// Step 13: Move a unit
|
||||
new TutorialStep {
|
||||
StepId = "move_unit",
|
||||
Title = "Move Your Units",
|
||||
Description =
|
||||
"Tap one of your units to select it, then tap a highlighted hex to move there.\n\nBlue hexes show where you can move.",
|
||||
"Tap one of your units to select it, then tap a highlighted hex to move there.\n\n" +
|
||||
"<b>Blue hexes</b> show where you can move.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "battle_action",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 11: Attack an enemy
|
||||
// Step 14: Attack an enemy
|
||||
new TutorialStep {
|
||||
StepId = "attack_enemy",
|
||||
Title = "Attack!",
|
||||
Description =
|
||||
"Move next to an enemy unit, then tap the enemy to attack.\n\nRed highlights show valid attack targets.",
|
||||
Description = "Move next to an enemy unit, then tap the enemy to attack.\n\n" +
|
||||
"<b>Red highlights</b> show valid attack targets.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "battle_action",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 12: End turn
|
||||
new TutorialStep {
|
||||
StepId = "end_turn",
|
||||
Title = "End Your Turn",
|
||||
Description =
|
||||
"When you've moved all units or want to pass, tap End Turn.\n\nThe enemy will then take their turn.",
|
||||
DisplayMode = TutorialDisplayMode.Overlay,
|
||||
// TODO: Add TargetGameObjectPath once we know the actual UI hierarchy
|
||||
CompletionType = TutorialCompletionType.GameEvent,
|
||||
CompletionEventId = "turn_ended",
|
||||
AllowSkip = true
|
||||
},
|
||||
|
||||
// Step 13: Completion
|
||||
// Step 15: Final completion
|
||||
new TutorialStep {
|
||||
StepId = "complete",
|
||||
Title = "You're Ready!",
|
||||
Title = "Tutorial Complete!",
|
||||
Description =
|
||||
"You now know the basics of Eagle0!\n\nExplore diplomacy, recruit powerful heroes, and conquer the realm. Good luck, commander!",
|
||||
"You now know the basics of Eagle0!\n\n" +
|
||||
"Explore diplomacy, recruit powerful heroes, and conquer the realm. May your reign be long and prosperous!",
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = false // Don't allow skipping the final step
|
||||
AllowSkip = false
|
||||
}
|
||||
};
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// ========== COMMAND PANEL TUTORIALS ==========
|
||||
// These appear the first time a command panel is shown, after onboarding completes.
|
||||
|
||||
private static void RegisterCommandPanelTutorials(TutorialTriggerRegistry registry) {
|
||||
// Improve command - most important early game command
|
||||
var improve = CreateCommandPanelTutorial(
|
||||
"command_improve",
|
||||
"Improve Province",
|
||||
"Use <b>Improve</b> to develop your province and increase Support.\n\n" +
|
||||
"• <b>Agriculture</b> - Increases food production\n" +
|
||||
"• <b>Economy</b> - Increases gold income\n" +
|
||||
"• <b>Infrastructure</b> - Improves armament and resilience\n" +
|
||||
"• <b>Repair Devastation</b> - Restores damaged stats\n\n" +
|
||||
"<b>Tip:</b> Select a hero using the dropdown, or click a hero in the <b>Resident Heroes</b> panel to the left!");
|
||||
registry.RegisterTutorial(improve, "command_panel_ImproveCommand");
|
||||
|
||||
// Alms command
|
||||
var alms = CreateCommandPanelTutorial(
|
||||
"command_alms",
|
||||
"Give Alms",
|
||||
"Give Alms distributes food to the people, quickly raising <b>Support</b>.\n\n" +
|
||||
"This is the fastest way to boost Support, but costs Food from your stores. Use it when you need Support urgently!");
|
||||
registry.RegisterTutorial(alms, "command_panel_AlmsCommand");
|
||||
|
||||
// March command
|
||||
var march = CreateCommandPanelTutorial(
|
||||
"command_march",
|
||||
"March",
|
||||
"March moves your army to another province.\n\n" +
|
||||
"• Select a <b>destination</b> province\n" +
|
||||
"• Choose which <b>heroes and battalions</b> to take\n" +
|
||||
"• Optionally bring <b>supplies</b> (food and gold)\n\n" +
|
||||
"If you march into enemy territory, battle may ensue!");
|
||||
registry.RegisterTutorial(march, "command_panel_MarchCommand");
|
||||
|
||||
// Defend command
|
||||
var defend = CreateCommandPanelTutorial(
|
||||
"command_defend",
|
||||
"Defend",
|
||||
"Defend prepares your forces to repel attackers.\n\n" +
|
||||
"Choose a <b>rally point</b> where your troops will gather. Units defending have advantages in tactical combat.");
|
||||
registry.RegisterTutorial(defend, "command_panel_DefendCommand");
|
||||
|
||||
// Rest command
|
||||
var rest = CreateCommandPanelTutorial(
|
||||
"command_rest",
|
||||
"Rest",
|
||||
"Rest restores your heroes' <b>Vigor</b>.\n\n" +
|
||||
"Heroes need rest to recover from exertion. Use this when your heroes are tired and you don't need to take other actions.");
|
||||
registry.RegisterTutorial(rest, "command_panel_RestCommand");
|
||||
|
||||
// Trade command
|
||||
var trade = CreateCommandPanelTutorial(
|
||||
"command_trade",
|
||||
"Trade",
|
||||
"Trade lets you exchange <b>Food</b> for <b>Gold</b> or vice versa.\n\n" +
|
||||
"The market takes its cut - buy and sell prices differ. Useful when you have surplus of one resource and need the other.");
|
||||
registry.RegisterTutorial(trade, "command_panel_TradeCommand");
|
||||
|
||||
// Feast command
|
||||
var feast = CreateCommandPanelTutorial(
|
||||
"command_feast",
|
||||
"Hold Feast",
|
||||
"Hold a Feast to boost hero <b>Loyalty</b> and <b>Vigor</b>.\n\n" +
|
||||
"The cost depends on how many heroes are in the province. A feast keeps your vassals happy and well-rested!");
|
||||
registry.RegisterTutorial(feast, "command_panel_FeastCommand");
|
||||
|
||||
// Hero Gift command
|
||||
var heroGift = CreateCommandPanelTutorial(
|
||||
"command_hero_gift",
|
||||
"Give Gift",
|
||||
"Give a gift to increase a hero's <b>Loyalty</b>.\n\n" +
|
||||
"Select a hero and choose how much gold to spend. More expensive gifts have greater effect.");
|
||||
registry.RegisterTutorial(heroGift, "command_panel_HeroGiftCommand");
|
||||
|
||||
// Train command
|
||||
var train = CreateCommandPanelTutorial(
|
||||
"command_train",
|
||||
"Train",
|
||||
"Train improves your troops' <b>combat effectiveness</b>.\n\n" +
|
||||
"Select a hero to lead training. Better trainers produce better results!");
|
||||
registry.RegisterTutorial(train, "command_panel_TrainCommand");
|
||||
|
||||
// Arm Troops command
|
||||
var armTroops = CreateCommandPanelTutorial(
|
||||
"command_arm_troops",
|
||||
"Arm Troops",
|
||||
"Arm Troops equips your battalions with better weapons and armor.\n\n" +
|
||||
"Higher <b>Infrastructure</b> allows better equipment. Well-armed troops are more effective in battle.");
|
||||
registry.RegisterTutorial(armTroops, "command_panel_ArmTroopsCommand");
|
||||
|
||||
// Organize Troops command
|
||||
var organizeTroops = CreateCommandPanelTutorial(
|
||||
"command_organize_troops",
|
||||
"Organize Troops",
|
||||
"Organize Troops lets you <b>hire new battalions</b> and restructure existing ones.\n\n" +
|
||||
"Light Infantry and Longbowmen are always available. Other battalion types require minimum <b>Economy</b> or <b>Agriculture</b> levels. You can also combine, split, or reassign troops between battalions.");
|
||||
registry.RegisterTutorial(organizeTroops, "command_panel_OrganizeTroopsCommand");
|
||||
|
||||
// Recruit Heroes command
|
||||
var recruitHeroes = CreateCommandPanelTutorial(
|
||||
"command_recruit_heroes",
|
||||
"Recruit Heroes",
|
||||
"Recruit a free hero to join your faction!\n\n" +
|
||||
"Heroes lead armies, govern provinces, and have special abilities. Choose wisely - their stats and professions vary.");
|
||||
registry.RegisterTutorial(recruitHeroes, "command_panel_RecruitHeroesCommand");
|
||||
|
||||
// Travel command
|
||||
var travel = CreateCommandPanelTutorial(
|
||||
"command_travel",
|
||||
"Travel",
|
||||
"Travel sends your hero to <b>town</b> within the province.\n\n" +
|
||||
"While traveling, you can do as many actions as you want in a turn: trade, arm troops, divine or recruit heroes, and manage prisoners. Use <b>Return</b> when done.");
|
||||
registry.RegisterTutorial(travel, "command_panel_TravelCommand");
|
||||
|
||||
// Return command
|
||||
var returnCmd = CreateCommandPanelTutorial(
|
||||
"command_return",
|
||||
"Return to Camp",
|
||||
"Return your province leader from town back to camp.\n\n" +
|
||||
"This ends <b>Travel</b> mode and completes your turn. Use it when you're done with town activities.");
|
||||
registry.RegisterTutorial(returnCmd, "command_panel_ReturnCommand");
|
||||
|
||||
// Diplomacy command
|
||||
var diplomacy = CreateCommandPanelTutorial(
|
||||
"command_diplomacy",
|
||||
"Diplomacy",
|
||||
"Send a diplomatic envoy to another faction.\n\n" +
|
||||
"Propose truces to pause hostilities, form alliances for mutual defense, or invite factions to join your cause. Select an ambassador - sending your leader is risky!");
|
||||
registry.RegisterTutorial(diplomacy, "command_panel_DiplomacyCommand");
|
||||
|
||||
// Ransom command (uses DiplomacyCommand type but separate selector)
|
||||
// Note: RansomCommandSelector also uses DiplomacyCommand, triggered by same event
|
||||
|
||||
// Send Supplies command
|
||||
var sendSupplies = CreateCommandPanelTutorial(
|
||||
"command_send_supplies",
|
||||
"Send Supplies",
|
||||
"Send gold and food to another province.\n\n" +
|
||||
"Useful for resupplying distant armies or supporting provinces under siege. Select a hero to escort the supplies.");
|
||||
registry.RegisterTutorial(sendSupplies, "command_panel_SendSuppliesCommand");
|
||||
|
||||
// Recon command
|
||||
var recon = CreateCommandPanelTutorial(
|
||||
"command_recon",
|
||||
"Recon",
|
||||
"Send a hero to scout an enemy province.\n\n" +
|
||||
"Gather intelligence on enemy forces, resources, and defenses. Your scout may encounter danger!");
|
||||
registry.RegisterTutorial(recon, "command_panel_ReconCommand");
|
||||
|
||||
// Divine command
|
||||
var divine = CreateCommandPanelTutorial(
|
||||
"command_divine",
|
||||
"Divine Heroes",
|
||||
"Use divination to learn a hero's hidden potential.\n\n" +
|
||||
"Costs gold but reveals the hero's true abilities and stats. Divine all available heroes at once for efficiency.");
|
||||
registry.RegisterTutorial(divine, "command_panel_DivineCommand");
|
||||
|
||||
// Issue Orders command
|
||||
var issueOrders = CreateCommandPanelTutorial(
|
||||
"command_issue_orders",
|
||||
"Issue Orders",
|
||||
"Set standing orders for your provinces.\n\n" +
|
||||
"Orders determine how provinces behave each turn - prioritize military, economy, or defense. You can also designate a Focus Province for special attention.");
|
||||
registry.RegisterTutorial(issueOrders, "command_panel_IssueOrdersCommand");
|
||||
|
||||
// Control Weather command
|
||||
var controlWeather = CreateCommandPanelTutorial(
|
||||
"command_control_weather",
|
||||
"Control Weather",
|
||||
"Use magical power to control the weather.\n\n" +
|
||||
"Create favorable conditions for your forces or harsh weather to hinder enemies. Requires a hero with weather magic.");
|
||||
registry.RegisterTutorial(
|
||||
controlWeather,
|
||||
"command_panel_ControlWeatherAvailableCommand");
|
||||
|
||||
// Swear Brotherhood command
|
||||
var swearBrotherhood = CreateCommandPanelTutorial(
|
||||
"command_swear_brotherhood",
|
||||
"Swear Brotherhood",
|
||||
"Swear an oath of brotherhood with a hero.\n\n" +
|
||||
"Creates a powerful bond that boosts loyalty and grants special bonuses. Choose your sworn brothers wisely - this bond is sacred!");
|
||||
registry.RegisterTutorial(swearBrotherhood, "command_panel_SwearBrotherhoodCommand");
|
||||
|
||||
// Apprehend Outlaw command
|
||||
var apprehendOutlaw = CreateCommandPanelTutorial(
|
||||
"command_apprehend_outlaw",
|
||||
"Apprehend Outlaw",
|
||||
"Capture an outlaw hiding in your province.\n\n" +
|
||||
"Send a hero and optionally a battalion to apprehend the fugitive. Outlaws may be former heroes from defeated factions.");
|
||||
registry.RegisterTutorial(apprehendOutlaw, "command_panel_ApprehendOutlawCommand");
|
||||
|
||||
// Suppress Beasts command
|
||||
var suppressBeasts = CreateCommandPanelTutorial(
|
||||
"command_suppress_beasts",
|
||||
"Suppress Beasts",
|
||||
"Deal with dangerous creatures threatening your province.\n\n" +
|
||||
"Send a hero with a battalion to suppress the beasts. Going without troops is risky - your hero could be killed!");
|
||||
registry.RegisterTutorial(suppressBeasts, "command_panel_SuppressBeastsCommand");
|
||||
|
||||
// Exile Vassal command
|
||||
var exileVassal = CreateCommandPanelTutorial(
|
||||
"command_exile_vassal",
|
||||
"Exile Vassal",
|
||||
"Exile a hero from your faction.\n\n" +
|
||||
"Sometimes a disloyal or troublesome vassal must be removed. The exiled hero becomes an outlaw and may seek revenge!");
|
||||
registry.RegisterTutorial(exileVassal, "command_panel_ExileVassalCommand");
|
||||
|
||||
// Handle Captured Hero command
|
||||
var handleCapturedHero = CreateCommandPanelTutorial(
|
||||
"command_handle_captured_hero",
|
||||
"Handle Captured Hero",
|
||||
"Decide the fate of a captured enemy hero.\n\n" +
|
||||
"You can recruit them to your cause, imprison them for ransom, exile them, execute them, or return them for diplomatic favor.");
|
||||
registry.RegisterTutorial(
|
||||
handleCapturedHero,
|
||||
"command_panel_HandleCapturedHeroCommand");
|
||||
|
||||
// Manage Prisoners command
|
||||
var managePrisoners = CreateCommandPanelTutorial(
|
||||
"command_manage_prisoners",
|
||||
"Manage Prisoners",
|
||||
"Manage heroes you've imprisoned.\n\n" +
|
||||
"Release them, move them between provinces, exile them, execute them, or return them to their faction for diplomatic benefit.");
|
||||
registry.RegisterTutorial(managePrisoners, "command_panel_ManagePrisonerCommand");
|
||||
|
||||
// Decline Quest command
|
||||
var declineQuest = CreateCommandPanelTutorial(
|
||||
"command_decline_quest",
|
||||
"Decline Quest",
|
||||
"Decline a quest offered by a hero.\n\n" +
|
||||
"Some heroes seek specific quests before joining. If you can't fulfill their request, you may decline - but they may leave.");
|
||||
registry.RegisterTutorial(declineQuest, "command_panel_DeclineQuestCommand");
|
||||
|
||||
// Start Epidemic command
|
||||
var startEpidemic = CreateCommandPanelTutorial(
|
||||
"command_start_epidemic",
|
||||
"Start Plague",
|
||||
"Unleash a deadly plague upon an enemy province.\n\n" +
|
||||
"A devastating but dishonorable tactic. Requires a hero with plague magic. The disease will spread and cause great suffering.");
|
||||
registry.RegisterTutorial(startEpidemic, "command_panel_StartEpidemicCommand");
|
||||
|
||||
// Handle Riot - Crack Down command
|
||||
var handleRiotCrackDown = CreateCommandPanelTutorial(
|
||||
"command_handle_riot_crack_down",
|
||||
"Crack Down",
|
||||
"Use force to suppress the rioting populace.\n\n" +
|
||||
"Send a hero with troops to restore order. Effective but may damage your reputation and reduce support.");
|
||||
registry.RegisterTutorial(
|
||||
handleRiotCrackDown,
|
||||
"command_panel_HandleRiotCrackDownAvailableCommand");
|
||||
|
||||
// Handle Riot - Do Nothing command
|
||||
var handleRiotDoNothing = CreateCommandPanelTutorial(
|
||||
"command_handle_riot_do_nothing",
|
||||
"Ignore Riot",
|
||||
"Choose to ignore the riot and let it run its course.\n\n" +
|
||||
"The unrest may subside on its own, or it may worsen. A risky choice that avoids immediate costs.");
|
||||
registry.RegisterTutorial(
|
||||
handleRiotDoNothing,
|
||||
"command_panel_HandleRiotDoNothingAvailableCommand");
|
||||
|
||||
// Handle Riot - Give command
|
||||
var handleRiotGive = CreateCommandPanelTutorial(
|
||||
"command_handle_riot_give",
|
||||
"Give to Populace",
|
||||
"Appease the rioters with gifts of food or gold.\n\n" +
|
||||
"A peaceful solution that costs resources but preserves your reputation. Choose how much to give.");
|
||||
registry.RegisterTutorial(
|
||||
handleRiotGive,
|
||||
"command_panel_HandleRiotGiveAvailableCommand");
|
||||
|
||||
// Attack Decision command
|
||||
var attackDecision = CreateCommandPanelTutorial(
|
||||
"command_attack_decision",
|
||||
"Attack Decision",
|
||||
"Your army has encountered enemy forces! Choose your action.\n\n" +
|
||||
"Advance to attack, demand tribute, request safe passage, or withdraw. Consider your strength before engaging.");
|
||||
registry.RegisterTutorial(attackDecision, "command_panel_AttackDecisionCommand");
|
||||
|
||||
// Free For All Decision command
|
||||
var freeForAllDecision = CreateCommandPanelTutorial(
|
||||
"command_free_for_all_decision",
|
||||
"Free-For-All Battle",
|
||||
"Multiple armies have converged! A chaotic battle is imminent.\n\n" +
|
||||
"You must decide whether to join the fray or attempt to withdraw from this dangerous situation.");
|
||||
registry.RegisterTutorial(
|
||||
freeForAllDecision,
|
||||
"command_panel_FreeForAllDecisionCommand");
|
||||
|
||||
// Resolve Tribute command
|
||||
var resolveTribute = CreateCommandPanelTutorial(
|
||||
"command_resolve_tribute",
|
||||
"Tribute Demanded",
|
||||
"Another faction is demanding tribute from you.\n\n" +
|
||||
"You can pay to avoid conflict, refuse and risk war, or imprison the messenger (a hostile act).");
|
||||
registry.RegisterTutorial(resolveTribute, "command_panel_ResolveTributeCommand");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a command panel tutorial with onboarding as a prerequisite.
|
||||
/// </summary>
|
||||
private static TutorialSequence
|
||||
CreateCommandPanelTutorial(string id, string title, string description) {
|
||||
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
|
||||
sequence.SequenceId = id;
|
||||
sequence.DisplayName = title;
|
||||
sequence.IsOnboarding = false;
|
||||
|
||||
// Require onboarding completion before showing command tutorials
|
||||
sequence.RequiredCompletedTutorials = new List<string> { "onboarding" };
|
||||
|
||||
sequence.Steps = new List<TutorialStep> { new TutorialStep {
|
||||
StepId = id + "_step",
|
||||
Title = title,
|
||||
Description = description,
|
||||
DisplayMode = TutorialDisplayMode.Modal,
|
||||
BlocksInteraction = false,
|
||||
// Position above the command selector panel
|
||||
PanelAnchor = TutorialPanelAnchor.Top,
|
||||
AdjacentTargetPath = "CommandPanel",
|
||||
CompletionType = TutorialCompletionType.ButtonClick,
|
||||
AllowSkip = true
|
||||
} };
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// ========== STRATEGIC CONTEXTUAL TUTORIALS ==========
|
||||
|
||||
private static void RegisterStrategicTutorials(TutorialTriggerRegistry registry) {
|
||||
// Diplomacy introduction
|
||||
var diplomacy = CreateSingleStepTutorial(
|
||||
"diplomacy_intro",
|
||||
"Diplomacy",
|
||||
"You can negotiate with other factions!\n\nOffer alliances, declare war, or propose tribute. Your diplomatic choices shape the realm.",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(diplomacy, "diplomacy_available");
|
||||
|
||||
// Hero recruitment
|
||||
var heroRecruitment = CreateSingleStepTutorial(
|
||||
"hero_recruitment",
|
||||
|
||||
+31
@@ -64,6 +64,37 @@ namespace Eagle0.Tutorial {
|
||||
/// </summary>
|
||||
public int StepCount => Steps?.Count ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Number of visible steps up to and including the first MarksOnboardingComplete step.
|
||||
/// This gives an accurate count for the current "phase" of onboarding.
|
||||
/// </summary>
|
||||
public int VisibleStepCount {
|
||||
get {
|
||||
if (Steps == null) return 0;
|
||||
int count = 0;
|
||||
foreach (var step in Steps) {
|
||||
if (step.DisplayMode != TutorialDisplayMode.None) count++;
|
||||
// Stop counting after the step that marks onboarding complete
|
||||
if (step.MarksOnboardingComplete) break;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible step index for a given actual step index.
|
||||
/// Returns 0-based index among visible steps only.
|
||||
/// </summary>
|
||||
public int GetVisibleStepIndex(int actualIndex) {
|
||||
if (Steps == null || actualIndex < 0) return 0;
|
||||
|
||||
int visibleIndex = 0;
|
||||
for (int i = 0; i < actualIndex && i < Steps.Count; i++) {
|
||||
if (Steps[i].DisplayMode != TutorialDisplayMode.None) visibleIndex++;
|
||||
}
|
||||
return visibleIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all prerequisites are met.
|
||||
/// </summary>
|
||||
|
||||
+39
@@ -22,6 +22,26 @@ namespace Eagle0.Tutorial {
|
||||
None
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where to position the tutorial panel on screen.
|
||||
/// </summary>
|
||||
public enum TutorialPanelAnchor {
|
||||
/// <summary>Centered on screen (default modal behavior).</summary>
|
||||
Center,
|
||||
|
||||
/// <summary>Panel anchored to left side of screen.</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Panel anchored to right side of screen.</summary>
|
||||
Right,
|
||||
|
||||
/// <summary>Panel anchored to top of screen.</summary>
|
||||
Top,
|
||||
|
||||
/// <summary>Panel anchored to bottom of screen.</summary>
|
||||
Bottom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How the tutorial step is completed/advanced.
|
||||
/// </summary>
|
||||
@@ -66,16 +86,31 @@ namespace Eagle0.Tutorial {
|
||||
[Tooltip("How this step should be displayed")]
|
||||
public TutorialDisplayMode DisplayMode = TutorialDisplayMode.Modal;
|
||||
|
||||
[Tooltip("Whether this step blocks game interaction (false = user can interact with game)")]
|
||||
public bool BlocksInteraction = true;
|
||||
|
||||
[Tooltip("Where to position the tutorial panel on screen")]
|
||||
public TutorialPanelAnchor PanelAnchor = TutorialPanelAnchor.Center;
|
||||
|
||||
[Header("UI Targeting")]
|
||||
[Tooltip("Path to GameObject to highlight (e.g., 'Canvas/CommandPanel/MarchButton')")]
|
||||
public string TargetGameObjectPath;
|
||||
|
||||
[Tooltip("Additional targets to highlight together (combined bounding box)")]
|
||||
public string[] AdditionalHighlightTargets;
|
||||
|
||||
[Tooltip("Path to GameObject to position the panel adjacent to (for non-blocking modals)")]
|
||||
public string AdjacentTargetPath;
|
||||
|
||||
[Tooltip("Offset from target element for tooltip/arrow positioning")]
|
||||
public Vector2 HighlightOffset;
|
||||
|
||||
[Tooltip("Whether the highlight should pulse")]
|
||||
public bool HighlightPulsing = true;
|
||||
|
||||
[Tooltip("Compute highlight bounds from active children instead of target's RectTransform")]
|
||||
public bool HighlightBoundsFromChildren;
|
||||
|
||||
[Header("Completion")]
|
||||
[Tooltip("How this step is completed")]
|
||||
public TutorialCompletionType CompletionType = TutorialCompletionType.ButtonClick;
|
||||
@@ -93,6 +128,10 @@ namespace Eagle0.Tutorial {
|
||||
[Tooltip("Jump to specific step ID instead of sequential (empty = next step)")]
|
||||
public string NextStepOverride;
|
||||
|
||||
[Tooltip(
|
||||
"Marks onboarding as complete when this step finishes (for mid-sequence completion)")]
|
||||
public bool MarksOnboardingComplete;
|
||||
|
||||
[Header("Audio")]
|
||||
[Tooltip("Optional narration audio")]
|
||||
public AudioClip NarrationClip;
|
||||
|
||||
+30
@@ -80,6 +80,36 @@ namespace Eagle0.Tutorial {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Command Panel Events ==========
|
||||
|
||||
// Track the last command type shown so we can re-trigger after onboarding
|
||||
private AvailableCommand.SealedValueOneofCase? _lastCommandTypeShown;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a command panel is shown in the strategic view.
|
||||
/// Triggers contextual tutorials for each command type.
|
||||
/// </summary>
|
||||
public void OnCommandPanelShown(AvailableCommand.SealedValueOneofCase commandType) {
|
||||
_lastCommandTypeShown = commandType;
|
||||
string eventId = $"command_panel_{commandType}";
|
||||
OnGameEvent(eventId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-triggers the last command panel tutorial.
|
||||
/// Called after onboarding completes to show tutorial for already-selected command.
|
||||
/// </summary>
|
||||
public void RetriggerLastCommandTutorial() {
|
||||
if (_lastCommandTypeShown.HasValue) {
|
||||
Debug.Log(
|
||||
$"[Tutorial] RetriggerLastCommandTutorial: Triggering for {_lastCommandTypeShown.Value}");
|
||||
string eventId = $"command_panel_{_lastCommandTypeShown.Value}";
|
||||
OnGameEvent(eventId);
|
||||
} else {
|
||||
Debug.Log("[Tutorial] RetriggerLastCommandTutorial: No last command type recorded");
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Strategic Layer Events ==========
|
||||
|
||||
/// <summary>
|
||||
|
||||
+63
-10
@@ -23,6 +23,9 @@ namespace Eagle0.Tutorial {
|
||||
[Tooltip("Reference to the tutorial UI manager")]
|
||||
public TutorialUIManager UIManager;
|
||||
|
||||
[Tooltip("Registry of UI targets for tutorials")]
|
||||
public TutorialTargetRegistry TargetRegistry;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable verbose logging")]
|
||||
public bool DebugLogging;
|
||||
@@ -147,15 +150,48 @@ namespace Eagle0.Tutorial {
|
||||
/// </summary>
|
||||
public void TriggerContextualTutorial(string tutorialId) {
|
||||
if (!TutorialsEnabled || _state.AllTutorialsDisabled) return;
|
||||
if (_state.HasCompletedTutorial(tutorialId)) return;
|
||||
|
||||
// Don't interrupt an active tutorial with a contextual one
|
||||
// (onboarding can be interrupted by explicit StartSequence calls)
|
||||
if (_activeSequence != null) {
|
||||
Log($"TriggerContextualTutorial: Skipping '{tutorialId}' - another tutorial is active");
|
||||
bool isAlreadyCompleted = _state.HasCompletedTutorial(tutorialId);
|
||||
bool isCommandTutorial = tutorialId.StartsWith("command_");
|
||||
bool activeIsCommandTutorial =
|
||||
_activeSequence?.SequenceId.StartsWith("command_") ?? false;
|
||||
|
||||
// If switching to a completed command tutorial while another command tutorial
|
||||
// is showing, just hide the current one (don't mark it complete)
|
||||
if (isAlreadyCompleted && isCommandTutorial && activeIsCommandTutorial) {
|
||||
Log($"TriggerContextualTutorial: '{tutorialId}' already completed, " +
|
||||
$"hiding active command tutorial '{_activeSequence.SequenceId}'");
|
||||
StopCurrentSequence(skipped: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAlreadyCompleted) return;
|
||||
|
||||
// Check if we should allow interruption
|
||||
if (_activeSequence != null) {
|
||||
var currentStep = CurrentStep;
|
||||
bool isHiddenStep = currentStep?.DisplayMode == TutorialDisplayMode.None;
|
||||
|
||||
// Allow interruption if:
|
||||
// 1. Current step is hidden (DisplayMode.None) - waiting for async events
|
||||
// 2. Both tutorials are command tutorials - allow switching between commands
|
||||
bool bothAreCommandTutorials = isCommandTutorial && activeIsCommandTutorial;
|
||||
|
||||
if (!isHiddenStep && !bothAreCommandTutorials) {
|
||||
Log($"TriggerContextualTutorial: Skipping '{tutorialId}' - another tutorial is active");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bothAreCommandTutorials && _activeSequence.SequenceId == tutorialId) {
|
||||
// Same command tutorial already showing, don't restart
|
||||
return;
|
||||
}
|
||||
|
||||
Log($"TriggerContextualTutorial: Allowing '{tutorialId}' to interrupt " +
|
||||
(isHiddenStep ? "hidden step"
|
||||
: $"command tutorial '{_activeSequence.SequenceId}'"));
|
||||
}
|
||||
|
||||
// Look up the tutorial sequence by ID
|
||||
var sequence = _triggerRegistry?.GetSequenceById(tutorialId);
|
||||
if (sequence != null) { StartSequence(sequence); }
|
||||
@@ -168,6 +204,18 @@ namespace Eagle0.Tutorial {
|
||||
if (_activeSequence == null) return;
|
||||
|
||||
var currentStep = CurrentStep;
|
||||
|
||||
// Check if this step marks onboarding as complete (mid-sequence)
|
||||
bool shouldRetriggerCommandTutorial = false;
|
||||
if (currentStep != null && currentStep.MarksOnboardingComplete &&
|
||||
_activeSequence.IsOnboarding) {
|
||||
Log("Marking onboarding complete (mid-sequence)");
|
||||
_state.CompleteOnboarding();
|
||||
_state.MarkTutorialCompleted(_activeSequence.SequenceId);
|
||||
// Defer re-triggering until after we advance to the hidden step
|
||||
shouldRetriggerCommandTutorial = true;
|
||||
}
|
||||
|
||||
string nextStepId = currentStep?.NextStepOverride;
|
||||
|
||||
// Determine next step index
|
||||
@@ -192,6 +240,11 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
Log($"Advanced to step {_currentStepIndex}: '{CurrentStep?.StepId}'");
|
||||
ShowCurrentStep();
|
||||
|
||||
// Now that we've advanced to the hidden step, re-trigger command tutorial
|
||||
if (shouldRetriggerCommandTutorial) {
|
||||
_triggerRegistry?.RetriggerLastCommandTutorial();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -297,12 +350,14 @@ namespace Eagle0.Tutorial {
|
||||
// Handle different display modes
|
||||
switch (step.DisplayMode) {
|
||||
case TutorialDisplayMode.Modal:
|
||||
// Use visible step index/count for better UX (excludes hidden steps)
|
||||
int visibleIndex = _activeSequence.GetVisibleStepIndex(_currentStepIndex);
|
||||
int visibleCount = _activeSequence.VisibleStepCount;
|
||||
UIManager?.ShowModal(
|
||||
step,
|
||||
_currentStepIndex,
|
||||
_activeSequence.StepCount,
|
||||
visibleIndex,
|
||||
visibleCount,
|
||||
OnStepComplete,
|
||||
OnStepSkip,
|
||||
_activeSequence.IsOnboarding);
|
||||
break;
|
||||
|
||||
@@ -343,8 +398,6 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
private void OnStepComplete() { AdvanceStep(); }
|
||||
|
||||
private void OnStepSkip() { SkipCurrentStep(); }
|
||||
|
||||
private void CompleteCurrentSequence() {
|
||||
if (_activeSequence == null) return;
|
||||
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Registry of UI elements that tutorials can reference.
|
||||
/// Assign static targets via Inspector, register dynamic targets via code.
|
||||
/// </summary>
|
||||
public class TutorialTargetRegistry : MonoBehaviour {
|
||||
[Header("Province Info Panel")]
|
||||
[Tooltip("The main Province Info panel")]
|
||||
public RectTransform ProvinceInfoPanel;
|
||||
|
||||
[Tooltip("The Support value display")]
|
||||
public RectTransform SupportField;
|
||||
|
||||
[Tooltip("The Agriculture value display")]
|
||||
public RectTransform AgricultureField;
|
||||
|
||||
[Tooltip("The Economy value display")]
|
||||
public RectTransform EconomyField;
|
||||
|
||||
[Tooltip("The Infrastructure value display")]
|
||||
public RectTransform InfrastructureField;
|
||||
|
||||
[Header("Heroes Panel")]
|
||||
[Tooltip("The Resident Heroes panel")]
|
||||
public RectTransform HeroesPanel;
|
||||
|
||||
[Header("Command Panel")]
|
||||
[Tooltip("The command selector panel (parent of individual command selectors)")]
|
||||
public RectTransform CommandPanel;
|
||||
|
||||
[Header("Command Buttons")]
|
||||
[Tooltip("Container for command buttons")]
|
||||
public RectTransform CommandButtonsPanel;
|
||||
|
||||
[Tooltip("The Commit button")]
|
||||
public RectTransform CommitButton;
|
||||
|
||||
// Dynamic targets registered at runtime (e.g., command buttons from prefabs)
|
||||
private Dictionary<string, RectTransform> _dynamicTargets =
|
||||
new Dictionary<string, RectTransform>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a target at runtime. Use for prefab-instantiated UI elements.
|
||||
/// </summary>
|
||||
public void RegisterTarget(string targetId, RectTransform target) {
|
||||
if (string.IsNullOrEmpty(targetId) || target == null) return;
|
||||
_dynamicTargets[targetId] = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a dynamically registered target.
|
||||
/// </summary>
|
||||
public void UnregisterTarget(string targetId) {
|
||||
if (!string.IsNullOrEmpty(targetId)) { _dynamicTargets.Remove(targetId); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a target by its string ID.
|
||||
/// Checks static Inspector fields first, then dynamic registrations.
|
||||
/// </summary>
|
||||
public RectTransform GetTarget(string targetId) {
|
||||
if (string.IsNullOrEmpty(targetId)) return null;
|
||||
|
||||
// Check static targets first
|
||||
var staticTarget = targetId switch { "ProvinceInfoPanel" => ProvinceInfoPanel,
|
||||
"SupportField" => SupportField,
|
||||
"AgricultureField" => AgricultureField,
|
||||
"EconomyField" => EconomyField,
|
||||
"InfrastructureField" => InfrastructureField,
|
||||
"HeroesPanel" => HeroesPanel,
|
||||
"CommandPanel" => CommandPanel,
|
||||
"CommandButtonsPanel" => CommandButtonsPanel,
|
||||
"CommitButton" => CommitButton,
|
||||
_ => null };
|
||||
|
||||
if (staticTarget != null) return staticTarget;
|
||||
|
||||
// Check dynamic targets
|
||||
if (_dynamicTargets.TryGetValue(targetId, out var dynamicTarget)) {
|
||||
return dynamicTarget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a target, with fallback to GameObject.Find for unregistered paths.
|
||||
/// </summary>
|
||||
public Transform GetTargetOrFind(string targetId) {
|
||||
var registered = GetTarget(targetId);
|
||||
if (registered != null) return registered;
|
||||
|
||||
// Fallback to path-based lookup for unregistered targets
|
||||
var found = GameObject.Find(targetId);
|
||||
if (found != null) {
|
||||
Debug.LogWarning(
|
||||
$"TutorialTargetRegistry: '{targetId}' not registered, using GameObject.Find fallback");
|
||||
return found.transform;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be44269baefd349e99d5ed5f6004234b
|
||||
+16
-28
@@ -85,7 +85,7 @@ namespace Eagle0.Tutorial {
|
||||
RectTransform panelRect = panel.AddComponent<RectTransform>();
|
||||
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
panelRect.sizeDelta = new Vector2(800, 550);
|
||||
panelRect.sizeDelta = new Vector2(540, 500);
|
||||
panelRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
// Panel Background
|
||||
@@ -100,8 +100,8 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
// Add vertical layout group
|
||||
VerticalLayoutGroup layout = panel.AddComponent<VerticalLayoutGroup>();
|
||||
layout.padding = new RectOffset(40, 40, 30, 30);
|
||||
layout.spacing = 20;
|
||||
layout.padding = new RectOffset(20, 20, 15, 15);
|
||||
layout.spacing = 10;
|
||||
layout.childAlignment = TextAnchor.UpperCenter;
|
||||
layout.childControlHeight = false;
|
||||
layout.childControlWidth = true;
|
||||
@@ -134,18 +134,18 @@ namespace Eagle0.Tutorial {
|
||||
titleObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = titleObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(720, 60);
|
||||
rect.sizeDelta = new Vector2(460, 36);
|
||||
|
||||
TextMeshProUGUI text = titleObj.AddComponent<TextMeshProUGUI>();
|
||||
text.text = "Tutorial";
|
||||
text.fontSize = 42;
|
||||
text.fontSize = 28;
|
||||
text.fontStyle = FontStyles.Bold;
|
||||
text.color = TitleColor;
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
if (font != null) text.font = font;
|
||||
|
||||
LayoutElement layoutElement = titleObj.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 60;
|
||||
layoutElement.preferredHeight = 36;
|
||||
}
|
||||
|
||||
private static void CreateIcon(Transform parent) {
|
||||
@@ -172,11 +172,11 @@ namespace Eagle0.Tutorial {
|
||||
descObj.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rect = descObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(720, 200);
|
||||
rect.sizeDelta = new Vector2(460, 180);
|
||||
|
||||
TextMeshProUGUI text = descObj.AddComponent<TextMeshProUGUI>();
|
||||
text.text = "";
|
||||
text.fontSize = 26;
|
||||
text.fontSize = 18;
|
||||
text.color = TextColor;
|
||||
text.alignment = TextAlignmentOptions.TopLeft;
|
||||
text.enableWordWrapping = true;
|
||||
@@ -184,8 +184,8 @@ namespace Eagle0.Tutorial {
|
||||
if (font != null) text.font = font;
|
||||
|
||||
LayoutElement layoutElement = descObj.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 200;
|
||||
layoutElement.minHeight = 100;
|
||||
layoutElement.preferredHeight = 180;
|
||||
layoutElement.minHeight = 80;
|
||||
}
|
||||
|
||||
private static void CreateSpacer(Transform parent, float height) {
|
||||
@@ -233,7 +233,7 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
TextMeshProUGUI progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
|
||||
progressText.text = "Step 1 of 1";
|
||||
progressText.fontSize = 20;
|
||||
progressText.fontSize = 14;
|
||||
progressText.color = TextColor;
|
||||
progressText.alignment = TextAlignmentOptions.Center;
|
||||
if (font != null) progressText.font = font;
|
||||
@@ -308,10 +308,6 @@ namespace Eagle0.Tutorial {
|
||||
panel.ContinueButton.onClick.RemoveAllListeners();
|
||||
panel.ContinueButton.onClick.AddListener(panel.OnContinueClicked);
|
||||
}
|
||||
if (panel.SkipButton != null) {
|
||||
panel.SkipButton.onClick.RemoveAllListeners();
|
||||
panel.SkipButton.onClick.AddListener(panel.OnSkipClicked);
|
||||
}
|
||||
if (panel.SkipAllButton != null) {
|
||||
panel.SkipAllButton.onClick.RemoveAllListeners();
|
||||
panel.SkipAllButton.onClick.AddListener(panel.OnSkipAllClicked);
|
||||
@@ -334,13 +330,10 @@ namespace Eagle0.Tutorial {
|
||||
layout.childForceExpandWidth = false;
|
||||
|
||||
LayoutElement layoutElement = buttonRow.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = 60;
|
||||
layoutElement.preferredHeight = 44;
|
||||
|
||||
// Skip Button (left)
|
||||
CreateButton(buttonRow.transform, "SkipButton", "Skip", 150, 50, font);
|
||||
|
||||
// Skip All Button
|
||||
CreateButton(buttonRow.transform, "SkipAllButton", "Skip All", 150, 50, font);
|
||||
// Skip Tutorial Button (left) - only shown during onboarding
|
||||
CreateButton(buttonRow.transform, "SkipAllButton", "Skip Tutorial", 140, 36, font);
|
||||
|
||||
// Spacer
|
||||
GameObject spacer = new GameObject("ButtonSpacer");
|
||||
@@ -350,7 +343,7 @@ namespace Eagle0.Tutorial {
|
||||
spacerLayout.flexibleWidth = 1;
|
||||
|
||||
// Continue Button (right)
|
||||
CreateButton(buttonRow.transform, "ContinueButton", "Continue", 180, 50, font);
|
||||
CreateButton(buttonRow.transform, "ContinueButton", "Continue", 120, 36, font);
|
||||
}
|
||||
|
||||
private static GameObject CreateButton(
|
||||
@@ -398,7 +391,7 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
TextMeshProUGUI buttonText = textObj.AddComponent<TextMeshProUGUI>();
|
||||
buttonText.text = text;
|
||||
buttonText.fontSize = 24;
|
||||
buttonText.fontSize = 16;
|
||||
buttonText.color = ButtonTextColor;
|
||||
buttonText.alignment = TextAlignmentOptions.Center;
|
||||
if (font != null) buttonText.font = font;
|
||||
@@ -450,11 +443,6 @@ namespace Eagle0.Tutorial {
|
||||
// Button Row
|
||||
Transform buttonRow = containerTransform.Find("ButtonRow");
|
||||
if (buttonRow != null) {
|
||||
Transform skipTransform = buttonRow.Find("SkipButton");
|
||||
if (skipTransform != null) {
|
||||
panel.SkipButton = skipTransform.GetComponent<Button>();
|
||||
}
|
||||
|
||||
Transform skipAllTransform = buttonRow.Find("SkipAllButton");
|
||||
if (skipAllTransform != null) {
|
||||
panel.SkipAllButton = skipAllTransform.GetComponent<Button>();
|
||||
|
||||
+195
-24
@@ -26,10 +26,7 @@ namespace Eagle0.Tutorial {
|
||||
[Tooltip("Text on continue button")]
|
||||
public TextMeshProUGUI ContinueButtonText;
|
||||
|
||||
[Tooltip("Skip this step button")]
|
||||
public Button SkipButton;
|
||||
|
||||
[Tooltip("Skip all tutorials button (onboarding only)")]
|
||||
[Tooltip("Skip tutorial button (onboarding only)")]
|
||||
public Button SkipAllButton;
|
||||
|
||||
[Header("Progress")]
|
||||
@@ -52,12 +49,10 @@ namespace Eagle0.Tutorial {
|
||||
|
||||
// Callbacks
|
||||
private Action _onContinue;
|
||||
private Action _onSkip;
|
||||
|
||||
private void Awake() {
|
||||
// Set up button listeners
|
||||
if (ContinueButton != null) { ContinueButton.onClick.AddListener(OnContinueClicked); }
|
||||
if (SkipButton != null) { SkipButton.onClick.AddListener(OnSkipClicked); }
|
||||
if (SkipAllButton != null) { SkipAllButton.onClick.AddListener(OnSkipAllClicked); }
|
||||
|
||||
// Start hidden
|
||||
@@ -72,10 +67,8 @@ namespace Eagle0.Tutorial {
|
||||
int currentIndex,
|
||||
int totalSteps,
|
||||
Action onContinue,
|
||||
Action onSkip,
|
||||
bool isOnboarding) {
|
||||
_onContinue = onContinue;
|
||||
_onSkip = onSkip;
|
||||
|
||||
// Set content
|
||||
if (TitleText != null) { TitleText.text = step.Title ?? ""; }
|
||||
@@ -96,8 +89,7 @@ namespace Eagle0.Tutorial {
|
||||
ContinueButtonText.text = currentIndex >= totalSteps - 1 ? "Got it!" : "Continue";
|
||||
}
|
||||
|
||||
// Show/hide skip buttons
|
||||
if (SkipButton != null) { SkipButton.gameObject.SetActive(step.AllowSkip); }
|
||||
// Show Skip Tutorial button only during onboarding (and when skipping is allowed)
|
||||
if (SkipAllButton != null) {
|
||||
SkipAllButton.gameObject.SetActive(isOnboarding && step.AllowSkip);
|
||||
}
|
||||
@@ -112,9 +104,19 @@ namespace Eagle0.Tutorial {
|
||||
ProgressSlider.gameObject.SetActive(totalSteps > 1);
|
||||
}
|
||||
|
||||
// Position panel - either adjacent to target or based on anchor setting
|
||||
if (!string.IsNullOrEmpty(step.AdjacentTargetPath)) {
|
||||
PositionAdjacentTo(step.AdjacentTargetPath, step.PanelAnchor);
|
||||
} else {
|
||||
PositionPanel(step.PanelAnchor);
|
||||
}
|
||||
|
||||
// Show panel - ensure all parent containers are active first
|
||||
ActivateParents();
|
||||
if (ModalBlocker != null) { ModalBlocker.SetActive(true); }
|
||||
|
||||
// Only show modal blocker if step blocks interaction
|
||||
if (ModalBlocker != null) { ModalBlocker.SetActive(step.BlocksInteraction); }
|
||||
|
||||
if (PanelContainer != null) { PanelContainer.SetActive(true); }
|
||||
gameObject.SetActive(true);
|
||||
|
||||
@@ -122,6 +124,170 @@ namespace Eagle0.Tutorial {
|
||||
if (PanelAnimator != null) { PanelAnimator.SetTrigger("Show"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the panel based on the anchor setting.
|
||||
/// </summary>
|
||||
private void PositionPanel(TutorialPanelAnchor anchor) {
|
||||
if (PanelContainer == null) return;
|
||||
|
||||
RectTransform rect = PanelContainer.GetComponent<RectTransform>();
|
||||
if (rect == null) return;
|
||||
|
||||
// Store original size
|
||||
Vector2 size = rect.sizeDelta;
|
||||
|
||||
switch (anchor) {
|
||||
case TutorialPanelAnchor.Center:
|
||||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rect.pivot = new Vector2(0.5f, 0.5f);
|
||||
rect.anchoredPosition = Vector2.zero;
|
||||
break;
|
||||
|
||||
case TutorialPanelAnchor.Left:
|
||||
rect.anchorMin = new Vector2(0f, 0.5f);
|
||||
rect.anchorMax = new Vector2(0f, 0.5f);
|
||||
rect.pivot = new Vector2(0f, 0.5f);
|
||||
rect.anchoredPosition = new Vector2(20f, 0f); // 20px padding from edge
|
||||
break;
|
||||
|
||||
case TutorialPanelAnchor.Right:
|
||||
rect.anchorMin = new Vector2(1f, 0.5f);
|
||||
rect.anchorMax = new Vector2(1f, 0.5f);
|
||||
rect.pivot = new Vector2(1f, 0.5f);
|
||||
rect.anchoredPosition = new Vector2(-20f, 0f); // 20px padding from edge
|
||||
break;
|
||||
|
||||
case TutorialPanelAnchor.Top:
|
||||
rect.anchorMin = new Vector2(0.5f, 1f);
|
||||
rect.anchorMax = new Vector2(0.5f, 1f);
|
||||
rect.pivot = new Vector2(0.5f, 1f);
|
||||
rect.anchoredPosition = new Vector2(0f, -20f); // 20px padding from edge
|
||||
break;
|
||||
|
||||
case TutorialPanelAnchor.Bottom:
|
||||
rect.anchorMin = new Vector2(0.5f, 0f);
|
||||
rect.anchorMax = new Vector2(0.5f, 0f);
|
||||
rect.pivot = new Vector2(0.5f, 0f);
|
||||
rect.anchoredPosition = new Vector2(0f, 20f); // 20px padding from edge
|
||||
break;
|
||||
}
|
||||
|
||||
// Restore size after changing anchors
|
||||
rect.sizeDelta = size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the panel adjacent to a target GameObject.
|
||||
/// </summary>
|
||||
private void PositionAdjacentTo(string targetId, TutorialPanelAnchor side) {
|
||||
if (PanelContainer == null) return;
|
||||
|
||||
// Use registry to find target
|
||||
Transform target = GetTarget(targetId);
|
||||
if (target == null) {
|
||||
Debug.LogWarning($"TutorialModalPanel: Could not find target '{targetId}'");
|
||||
PositionPanel(side);
|
||||
return;
|
||||
}
|
||||
|
||||
RectTransform targetRect = target.GetComponent<RectTransform>();
|
||||
if (targetRect == null) {
|
||||
PositionPanel(side);
|
||||
return;
|
||||
}
|
||||
|
||||
RectTransform panelRect = PanelContainer.GetComponent<RectTransform>();
|
||||
if (panelRect == null) return;
|
||||
|
||||
Vector2 size = panelRect.sizeDelta;
|
||||
|
||||
// Get target bounds in screen space
|
||||
Vector3[] targetCorners = new Vector3[4];
|
||||
targetRect.GetWorldCorners(targetCorners);
|
||||
|
||||
// Get canvas for coordinate conversion
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
RectTransform canvasRect = canvas?.GetComponent<RectTransform>();
|
||||
|
||||
if (canvasRect == null) {
|
||||
PositionPanel(side);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert target corners to canvas space
|
||||
for (int i = 0; i < 4; i++) {
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvasRect,
|
||||
RectTransformUtility.WorldToScreenPoint(
|
||||
canvas.worldCamera,
|
||||
targetCorners[i]),
|
||||
canvas.worldCamera,
|
||||
out Vector2 localPoint);
|
||||
targetCorners[i] = localPoint;
|
||||
}
|
||||
|
||||
// Calculate target bounds
|
||||
float targetLeft = Mathf.Min(targetCorners[0].x, targetCorners[1].x);
|
||||
float targetRight = Mathf.Max(targetCorners[2].x, targetCorners[3].x);
|
||||
float targetBottom = Mathf.Min(targetCorners[0].y, targetCorners[3].y);
|
||||
float targetTop = Mathf.Max(targetCorners[1].y, targetCorners[2].y);
|
||||
float targetCenterX = (targetLeft + targetRight) / 2f;
|
||||
float targetCenterY = (targetTop + targetBottom) / 2f;
|
||||
|
||||
// Use center anchor for the panel
|
||||
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
panelRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
|
||||
float padding = 20f;
|
||||
Vector2 position;
|
||||
|
||||
// Position based on which side
|
||||
switch (side) {
|
||||
case TutorialPanelAnchor.Right:
|
||||
// Position panel to the right of target
|
||||
position = new Vector2(targetRight + size.x / 2f + padding, targetCenterY);
|
||||
break;
|
||||
|
||||
case TutorialPanelAnchor.Left:
|
||||
// Position panel to the left of target
|
||||
position = new Vector2(targetLeft - size.x / 2f - padding, targetCenterY);
|
||||
break;
|
||||
|
||||
case TutorialPanelAnchor.Top:
|
||||
// Position panel above target
|
||||
position = new Vector2(targetCenterX, targetTop + size.y / 2f + padding);
|
||||
break;
|
||||
|
||||
case TutorialPanelAnchor.Bottom:
|
||||
// Position panel below target
|
||||
position = new Vector2(targetCenterX, targetBottom - size.y / 2f - padding);
|
||||
break;
|
||||
|
||||
default:
|
||||
// For Center, fall back to standard positioning
|
||||
PositionPanel(side);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp to screen bounds (with padding)
|
||||
float canvasHalfWidth = canvasRect.rect.width / 2f;
|
||||
float canvasHalfHeight = canvasRect.rect.height / 2f;
|
||||
float screenPadding = 10f;
|
||||
|
||||
float minX = -canvasHalfWidth + size.x / 2f + screenPadding;
|
||||
float maxX = canvasHalfWidth - size.x / 2f - screenPadding;
|
||||
float minY = -canvasHalfHeight + size.y / 2f + screenPadding;
|
||||
float maxY = canvasHalfHeight - size.y / 2f - screenPadding;
|
||||
|
||||
position.x = Mathf.Clamp(position.x, minX, maxX);
|
||||
position.y = Mathf.Clamp(position.y, minY, maxY);
|
||||
|
||||
panelRect.anchoredPosition = position;
|
||||
panelRect.sizeDelta = size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates all parent GameObjects to ensure this object can be active in hierarchy.
|
||||
/// </summary>
|
||||
@@ -144,7 +310,6 @@ namespace Eagle0.Tutorial {
|
||||
gameObject.SetActive(false);
|
||||
|
||||
_onContinue = null;
|
||||
_onSkip = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -158,22 +323,28 @@ namespace Eagle0.Tutorial {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when Skip button is clicked.
|
||||
/// Public to allow external setup of button listeners.
|
||||
/// </summary>
|
||||
public void OnSkipClicked() {
|
||||
var callback = _onSkip;
|
||||
Hide();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when Skip All button is clicked.
|
||||
/// Public to allow external setup of button listeners.
|
||||
/// Called when Skip Tutorial button is clicked.
|
||||
/// Skips all remaining tutorial steps. User can restart from Settings.
|
||||
/// </summary>
|
||||
public void OnSkipAllClicked() {
|
||||
Hide();
|
||||
TutorialManager.Instance?.SkipAllOnboarding();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a target Transform by ID, using the registry if available.
|
||||
/// Falls back to GameObject.Find for unregistered targets.
|
||||
/// </summary>
|
||||
private Transform GetTarget(string targetId) {
|
||||
if (string.IsNullOrEmpty(targetId)) return null;
|
||||
|
||||
// Try registry first
|
||||
var registry = TutorialManager.Instance?.TargetRegistry;
|
||||
if (registry != null) { return registry.GetTargetOrFind(targetId); }
|
||||
|
||||
// Fallback to GameObject.Find
|
||||
var targetObj = GameObject.Find(targetId);
|
||||
return targetObj?.transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+235
-14
@@ -112,11 +112,15 @@ namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Shows only the highlight without tooltip (for emphasis).
|
||||
/// </summary>
|
||||
public void HighlightOnly(Transform target) {
|
||||
public void HighlightOnly(Transform target, bool boundsFromChildren = false) {
|
||||
_currentTarget = target;
|
||||
|
||||
if (target != null) {
|
||||
PositionHighlight(target);
|
||||
if (boundsFromChildren) {
|
||||
PositionHighlightFromChildren(target);
|
||||
} else {
|
||||
PositionHighlight(target);
|
||||
}
|
||||
if (HighlightFrame != null) { HighlightFrame.gameObject.SetActive(true); }
|
||||
}
|
||||
|
||||
@@ -130,6 +134,103 @@ namespace Eagle0.Tutorial {
|
||||
_pulseCoroutine = StartCoroutine(PulseHighlight());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows highlight around multiple targets with a combined bounding box.
|
||||
/// </summary>
|
||||
public void HighlightMultiple(Transform[] targets) {
|
||||
if (targets == null || targets.Length == 0) {
|
||||
ClearHighlight();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use first target as "current" for tracking
|
||||
_currentTarget = targets[0];
|
||||
|
||||
PositionHighlightMultiple(targets);
|
||||
if (HighlightFrame != null) { HighlightFrame.gameObject.SetActive(true); }
|
||||
|
||||
// Hide tooltip elements
|
||||
if (TooltipContainer != null) { TooltipContainer.gameObject.SetActive(false); }
|
||||
if (BackgroundDimmer != null) { BackgroundDimmer.gameObject.SetActive(false); }
|
||||
|
||||
// Ensure all parent containers are active first
|
||||
ActivateParents();
|
||||
gameObject.SetActive(true);
|
||||
_pulseCoroutine = StartCoroutine(PulseHighlight());
|
||||
}
|
||||
|
||||
private void PositionHighlightMultiple(Transform[] targets) {
|
||||
if (HighlightFrame == null || targets == null || targets.Length == 0) return;
|
||||
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
RectTransform canvasRect = canvas?.GetComponent<RectTransform>();
|
||||
if (canvasRect == null) {
|
||||
HighlightFrame.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Camera cam = canvas.worldCamera;
|
||||
|
||||
// Initialize bounds with extreme values
|
||||
float minX = float.MaxValue, maxX = float.MinValue;
|
||||
float minY = float.MaxValue, maxY = float.MinValue;
|
||||
|
||||
foreach (var target in targets) {
|
||||
if (target == null) continue;
|
||||
|
||||
RectTransform targetRect = target as RectTransform;
|
||||
if (targetRect == null) { targetRect = target.GetComponent<RectTransform>(); }
|
||||
if (targetRect == null) continue;
|
||||
|
||||
// Get target bounds in world space
|
||||
Vector3[] corners = new Vector3[4];
|
||||
targetRect.GetWorldCorners(corners);
|
||||
|
||||
// Convert to canvas-local and expand bounds
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(cam, corners[i]);
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvasRect,
|
||||
screenPoint,
|
||||
cam,
|
||||
out Vector2 localPoint);
|
||||
|
||||
minX = Mathf.Min(minX, localPoint.x);
|
||||
maxX = Mathf.Max(maxX, localPoint.x);
|
||||
minY = Mathf.Min(minY, localPoint.y);
|
||||
maxY = Mathf.Max(maxY, localPoint.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (minX == float.MaxValue) {
|
||||
HighlightFrame.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp bounds to stay within canvas (accounting for padding that will be added)
|
||||
float canvasHalfWidth = canvasRect.rect.width / 2f;
|
||||
float canvasHalfHeight = canvasRect.rect.height / 2f;
|
||||
float margin = 5f + HighlightPadding;
|
||||
|
||||
minX = Mathf.Max(minX, -canvasHalfWidth + margin);
|
||||
maxX = Mathf.Min(maxX, canvasHalfWidth - margin);
|
||||
minY = Mathf.Max(minY, -canvasHalfHeight + margin);
|
||||
maxY = Mathf.Min(maxY, canvasHalfHeight - margin);
|
||||
|
||||
// Position and size highlight to encompass all targets
|
||||
Vector2 center = new Vector2((minX + maxX) / 2, (minY + maxY) / 2);
|
||||
Vector2 size = new Vector2(
|
||||
maxX - minX + HighlightPadding * 2,
|
||||
maxY - minY + HighlightPadding * 2);
|
||||
|
||||
HighlightFrame.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.pivot = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.anchoredPosition = center;
|
||||
HighlightFrame.sizeDelta = size;
|
||||
HighlightFrame.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears highlight without hiding overlay.
|
||||
/// </summary>
|
||||
@@ -171,35 +272,155 @@ namespace Eagle0.Tutorial {
|
||||
if (targetRect == null) { targetRect = target.GetComponent<RectTransform>(); }
|
||||
|
||||
if (targetRect != null) {
|
||||
// Get target bounds in screen space
|
||||
// Get target bounds in world space
|
||||
Vector3[] corners = new Vector3[4];
|
||||
targetRect.GetWorldCorners(corners);
|
||||
|
||||
// Convert to canvas space
|
||||
// Get canvas for coordinate conversion
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay) {
|
||||
Camera cam = canvas.worldCamera ?? Camera.main;
|
||||
for (int i = 0; i < 4; i++) { corners[i] = cam.WorldToScreenPoint(corners[i]); }
|
||||
RectTransform canvasRect = canvas?.GetComponent<RectTransform>();
|
||||
|
||||
if (canvasRect == null) {
|
||||
HighlightFrame.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate bounds
|
||||
float minX = Mathf.Min(corners[0].x, corners[1].x, corners[2].x, corners[3].x);
|
||||
float maxX = Mathf.Max(corners[0].x, corners[1].x, corners[2].x, corners[3].x);
|
||||
float minY = Mathf.Min(corners[0].y, corners[1].y, corners[2].y, corners[3].y);
|
||||
float maxY = Mathf.Max(corners[0].y, corners[1].y, corners[2].y, corners[3].y);
|
||||
// Convert world corners to screen space, then to canvas-local space
|
||||
Vector2[] localCorners = new Vector2[4];
|
||||
Camera cam = canvas.worldCamera;
|
||||
|
||||
// Position and size highlight
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(cam, corners[i]);
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvasRect,
|
||||
screenPoint,
|
||||
cam,
|
||||
out localCorners[i]);
|
||||
}
|
||||
|
||||
// Calculate bounds in canvas-local space
|
||||
float minX = Mathf.Min(
|
||||
localCorners[0].x,
|
||||
localCorners[1].x,
|
||||
localCorners[2].x,
|
||||
localCorners[3].x);
|
||||
float maxX = Mathf.Max(
|
||||
localCorners[0].x,
|
||||
localCorners[1].x,
|
||||
localCorners[2].x,
|
||||
localCorners[3].x);
|
||||
float minY = Mathf.Min(
|
||||
localCorners[0].y,
|
||||
localCorners[1].y,
|
||||
localCorners[2].y,
|
||||
localCorners[3].y);
|
||||
float maxY = Mathf.Max(
|
||||
localCorners[0].y,
|
||||
localCorners[1].y,
|
||||
localCorners[2].y,
|
||||
localCorners[3].y);
|
||||
|
||||
// Clamp bounds to stay within canvas (accounting for padding that will be added)
|
||||
float canvasHalfWidth = canvasRect.rect.width / 2f;
|
||||
float canvasHalfHeight = canvasRect.rect.height / 2f;
|
||||
float margin = 5f + HighlightPadding;
|
||||
|
||||
minX = Mathf.Max(minX, -canvasHalfWidth + margin);
|
||||
maxX = Mathf.Min(maxX, canvasHalfWidth - margin);
|
||||
minY = Mathf.Max(minY, -canvasHalfHeight + margin);
|
||||
maxY = Mathf.Min(maxY, canvasHalfHeight - margin);
|
||||
|
||||
// Position and size highlight in canvas-local coordinates
|
||||
Vector2 center = new Vector2((minX + maxX) / 2, (minY + maxY) / 2);
|
||||
Vector2 size = new Vector2(
|
||||
maxX - minX + HighlightPadding * 2,
|
||||
maxY - minY + HighlightPadding * 2);
|
||||
|
||||
HighlightFrame.position = center;
|
||||
// Set highlight position and size using anchored position (canvas-local)
|
||||
HighlightFrame.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.pivot = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.anchoredPosition = center;
|
||||
HighlightFrame.sizeDelta = size;
|
||||
HighlightFrame.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions highlight based on active children of the target rather than target's own
|
||||
/// bounds. Useful for containers where the RectTransform is larger than the visible
|
||||
/// content.
|
||||
/// </summary>
|
||||
private void PositionHighlightFromChildren(Transform target) {
|
||||
if (HighlightFrame == null || target == null) return;
|
||||
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
RectTransform canvasRect = canvas?.GetComponent<RectTransform>();
|
||||
if (canvasRect == null) {
|
||||
HighlightFrame.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Camera cam = canvas.worldCamera;
|
||||
|
||||
// Collect bounds from all active children with RectTransforms
|
||||
float minX = float.MaxValue, maxX = float.MinValue;
|
||||
float minY = float.MaxValue, maxY = float.MinValue;
|
||||
|
||||
foreach (Transform child in target) {
|
||||
if (!child.gameObject.activeInHierarchy) continue;
|
||||
|
||||
RectTransform childRect = child.GetComponent<RectTransform>();
|
||||
if (childRect == null) continue;
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
childRect.GetWorldCorners(corners);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(cam, corners[i]);
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvasRect,
|
||||
screenPoint,
|
||||
cam,
|
||||
out Vector2 localPoint);
|
||||
|
||||
minX = Mathf.Min(minX, localPoint.x);
|
||||
maxX = Mathf.Max(maxX, localPoint.x);
|
||||
minY = Mathf.Min(minY, localPoint.y);
|
||||
maxY = Mathf.Max(maxY, localPoint.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (minX == float.MaxValue) {
|
||||
// No active children found, fall back to target bounds
|
||||
PositionHighlight(target);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp bounds to stay within canvas (accounting for padding)
|
||||
float canvasHalfWidth = canvasRect.rect.width / 2f;
|
||||
float canvasHalfHeight = canvasRect.rect.height / 2f;
|
||||
float margin = 5f + HighlightPadding;
|
||||
|
||||
minX = Mathf.Max(minX, -canvasHalfWidth + margin);
|
||||
maxX = Mathf.Min(maxX, canvasHalfWidth - margin);
|
||||
minY = Mathf.Max(minY, -canvasHalfHeight + margin);
|
||||
maxY = Mathf.Min(maxY, canvasHalfHeight - margin);
|
||||
|
||||
// Position and size highlight
|
||||
Vector2 center = new Vector2((minX + maxX) / 2, (minY + maxY) / 2);
|
||||
Vector2 size = new Vector2(
|
||||
maxX - minX + HighlightPadding * 2,
|
||||
maxY - minY + HighlightPadding * 2);
|
||||
|
||||
HighlightFrame.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.pivot = new Vector2(0.5f, 0.5f);
|
||||
HighlightFrame.anchoredPosition = center;
|
||||
HighlightFrame.sizeDelta = size;
|
||||
HighlightFrame.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void PositionTooltip(Transform target, Vector2 offset) {
|
||||
if (TooltipContainer == null) return;
|
||||
|
||||
|
||||
+109
-56
@@ -46,7 +46,6 @@ namespace Eagle0.Tutorial {
|
||||
private bool _fallbackModalVisible;
|
||||
private TutorialStep _fallbackStep;
|
||||
private Action _fallbackOnContinue;
|
||||
private Action _fallbackOnSkip;
|
||||
private int _fallbackCurrentIndex;
|
||||
private int _fallbackTotalSteps;
|
||||
private bool _fallbackIsOnboarding;
|
||||
@@ -106,16 +105,43 @@ namespace Eagle0.Tutorial {
|
||||
private void OnGUI() {
|
||||
if (!_fallbackModalVisible || _fallbackStep == null) return;
|
||||
|
||||
// Semi-transparent background
|
||||
GUI.color = new Color(0, 0, 0, 0.7f);
|
||||
GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), Texture2D.whiteTexture);
|
||||
GUI.color = Color.white;
|
||||
// Only draw background blocker if step blocks interaction
|
||||
if (_fallbackStep.BlocksInteraction) {
|
||||
GUI.color = new Color(0, 0, 0, 0.7f);
|
||||
GUI.DrawTexture(
|
||||
new Rect(0, 0, Screen.width, Screen.height),
|
||||
Texture2D.whiteTexture);
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
// Modal window
|
||||
float windowWidth = Mathf.Min(1200, Screen.width - 40);
|
||||
float windowHeight = 600;
|
||||
float windowX = (Screen.width - windowWidth) / 2;
|
||||
float windowY = (Screen.height - windowHeight) / 2;
|
||||
// Calculate window position based on anchor
|
||||
float windowWidth = Mathf.Min(800, Screen.width - 40);
|
||||
float windowHeight = 500;
|
||||
float windowX, windowY;
|
||||
|
||||
switch (_fallbackStep.PanelAnchor) {
|
||||
case TutorialPanelAnchor.Left:
|
||||
windowX = 20;
|
||||
windowY = (Screen.height - windowHeight) / 2;
|
||||
break;
|
||||
case TutorialPanelAnchor.Right:
|
||||
windowX = Screen.width - windowWidth - 20;
|
||||
windowY = (Screen.height - windowHeight) / 2;
|
||||
break;
|
||||
case TutorialPanelAnchor.Top:
|
||||
windowX = (Screen.width - windowWidth) / 2;
|
||||
windowY = 20;
|
||||
break;
|
||||
case TutorialPanelAnchor.Bottom:
|
||||
windowX = (Screen.width - windowWidth) / 2;
|
||||
windowY = Screen.height - windowHeight - 20;
|
||||
break;
|
||||
case TutorialPanelAnchor.Center:
|
||||
default:
|
||||
windowX = (Screen.width - windowWidth) / 2;
|
||||
windowY = (Screen.height - windowHeight) / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
GUIStyle windowStyle = new GUIStyle(GUI.skin.window);
|
||||
windowStyle.fontSize = 48;
|
||||
@@ -165,26 +191,15 @@ namespace Eagle0.Tutorial {
|
||||
// Buttons
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
if (_fallbackStep.AllowSkip) {
|
||||
// Skip Tutorial button (only for onboarding, when step allows skipping)
|
||||
if (_fallbackIsOnboarding && _fallbackStep.AllowSkip) {
|
||||
if (GUILayout.Button(
|
||||
"Skip",
|
||||
"Skip Tutorial",
|
||||
buttonStyle,
|
||||
GUILayout.Width(200),
|
||||
GUILayout.Width(280),
|
||||
GUILayout.Height(80))) {
|
||||
var callback = _fallbackOnSkip;
|
||||
HideFallbackModal();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
if (_fallbackIsOnboarding) {
|
||||
if (GUILayout.Button(
|
||||
"Skip All",
|
||||
buttonStyle,
|
||||
GUILayout.Width(200),
|
||||
GUILayout.Height(80))) {
|
||||
HideFallbackModal();
|
||||
TutorialManager.Instance?.SkipAllOnboarding();
|
||||
}
|
||||
TutorialManager.Instance?.SkipAllOnboarding();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,13 +226,11 @@ namespace Eagle0.Tutorial {
|
||||
int currentIndex,
|
||||
int totalSteps,
|
||||
Action onContinue,
|
||||
Action onSkip,
|
||||
bool isOnboarding) {
|
||||
_fallbackStep = step;
|
||||
_fallbackCurrentIndex = currentIndex;
|
||||
_fallbackTotalSteps = totalSteps;
|
||||
_fallbackOnContinue = onContinue;
|
||||
_fallbackOnSkip = onSkip;
|
||||
_fallbackIsOnboarding = isOnboarding;
|
||||
_fallbackModalVisible = true;
|
||||
}
|
||||
@@ -226,7 +239,6 @@ namespace Eagle0.Tutorial {
|
||||
_fallbackModalVisible = false;
|
||||
_fallbackStep = null;
|
||||
_fallbackOnContinue = null;
|
||||
_fallbackOnSkip = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -237,16 +249,56 @@ namespace Eagle0.Tutorial {
|
||||
int currentIndex,
|
||||
int totalSteps,
|
||||
Action onComplete,
|
||||
Action onSkip,
|
||||
bool isOnboarding) {
|
||||
// Clear any existing highlight before showing new one
|
||||
OverlayController?.ClearHighlight();
|
||||
|
||||
// Show highlight on target element(s) if specified (works alongside modal)
|
||||
if (!string.IsNullOrEmpty(step.TargetGameObjectPath)) {
|
||||
if (OverlayController == null) {
|
||||
Debug.LogWarning(
|
||||
"TutorialUIManager: OverlayController is null, cannot highlight");
|
||||
} else {
|
||||
// Collect all targets (primary + additional)
|
||||
var targets = new List<Transform>();
|
||||
var primaryTarget = GetTarget(step.TargetGameObjectPath);
|
||||
if (primaryTarget != null) { targets.Add(primaryTarget); }
|
||||
|
||||
if (step.AdditionalHighlightTargets != null) {
|
||||
foreach (var additionalId in step.AdditionalHighlightTargets) {
|
||||
var additionalTarget = GetTarget(additionalId);
|
||||
if (additionalTarget != null) { targets.Add(additionalTarget); }
|
||||
}
|
||||
}
|
||||
|
||||
if (targets.Count > 1) {
|
||||
OverlayController.HighlightMultiple(targets.ToArray());
|
||||
} else if (targets.Count == 1) {
|
||||
OverlayController.HighlightOnly(
|
||||
targets[0],
|
||||
step.HighlightBoundsFromChildren);
|
||||
} else {
|
||||
Debug.LogWarning(
|
||||
$"TutorialUIManager: Target '{step.TargetGameObjectPath}' not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ModalPanel != null) {
|
||||
// Ensure canvas is active
|
||||
if (TutorialCanvas != null && !TutorialCanvas.gameObject.activeSelf) {
|
||||
TutorialCanvas.gameObject.SetActive(true);
|
||||
}
|
||||
ModalPanel.Show(step, currentIndex, totalSteps, onComplete, onSkip, isOnboarding);
|
||||
|
||||
// Wrap callback to clear highlight when modal is dismissed
|
||||
Action wrappedCallback = () => {
|
||||
OverlayController?.ClearHighlight();
|
||||
onComplete?.Invoke();
|
||||
};
|
||||
|
||||
ModalPanel.Show(step, currentIndex, totalSteps, wrappedCallback, isOnboarding);
|
||||
} else if (UseFallbackUI) {
|
||||
ShowFallbackModal(step, currentIndex, totalSteps, onComplete, onSkip, isOnboarding);
|
||||
ShowFallbackModal(step, currentIndex, totalSteps, onComplete, isOnboarding);
|
||||
} else {
|
||||
Debug.LogWarning("TutorialUIManager: No ModalPanel assigned and fallback disabled");
|
||||
onComplete?.Invoke();
|
||||
@@ -263,16 +315,11 @@ namespace Eagle0.Tutorial {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find target element
|
||||
Transform target = null;
|
||||
if (!string.IsNullOrEmpty(step.TargetGameObjectPath)) {
|
||||
var targetObj = GameObject.Find(step.TargetGameObjectPath);
|
||||
if (targetObj != null) {
|
||||
target = targetObj.transform;
|
||||
} else {
|
||||
Debug.LogWarning(
|
||||
$"TutorialUIManager: Could not find target '{step.TargetGameObjectPath}'");
|
||||
}
|
||||
// Find target element using registry
|
||||
Transform target = GetTarget(step.TargetGameObjectPath);
|
||||
if (!string.IsNullOrEmpty(step.TargetGameObjectPath) && target == null) {
|
||||
Debug.LogWarning(
|
||||
$"TutorialUIManager: Could not find target '{step.TargetGameObjectPath}'");
|
||||
}
|
||||
|
||||
OverlayController.ShowOverlay(
|
||||
@@ -305,13 +352,8 @@ namespace Eagle0.Tutorial {
|
||||
// Don't show duplicate hints
|
||||
if (_activeHints.ContainsKey(hintId)) return;
|
||||
|
||||
// Find target
|
||||
Transform target = null;
|
||||
if (!string.IsNullOrEmpty(targetPath)) {
|
||||
var targetObj = GameObject.Find(targetPath);
|
||||
if (targetObj != null) { target = targetObj.transform; }
|
||||
}
|
||||
|
||||
// Find target using registry
|
||||
Transform target = GetTarget(targetPath);
|
||||
if (target == null) {
|
||||
Debug.LogWarning($"TutorialUIManager: Could not find hint target '{targetPath}'");
|
||||
return;
|
||||
@@ -356,15 +398,10 @@ namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Highlights a specific UI element (without showing tutorial content).
|
||||
/// </summary>
|
||||
public void HighlightElement(string gameObjectPath) {
|
||||
public void HighlightElement(string targetId) {
|
||||
if (OverlayController == null) return;
|
||||
|
||||
Transform target = null;
|
||||
if (!string.IsNullOrEmpty(gameObjectPath)) {
|
||||
var targetObj = GameObject.Find(gameObjectPath);
|
||||
if (targetObj != null) { target = targetObj.transform; }
|
||||
}
|
||||
|
||||
Transform target = GetTarget(targetId);
|
||||
if (target != null) { OverlayController.HighlightOnly(target); }
|
||||
}
|
||||
|
||||
@@ -372,5 +409,21 @@ namespace Eagle0.Tutorial {
|
||||
/// Clears any active highlight.
|
||||
/// </summary>
|
||||
public void ClearHighlight() { OverlayController?.ClearHighlight(); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a target Transform by ID, using the registry if available.
|
||||
/// Falls back to GameObject.Find for unregistered targets.
|
||||
/// </summary>
|
||||
private Transform GetTarget(string targetId) {
|
||||
if (string.IsNullOrEmpty(targetId)) return null;
|
||||
|
||||
// Try registry first
|
||||
var registry = TutorialManager.Instance?.TargetRegistry;
|
||||
if (registry != null) { return registry.GetTargetOrFind(targetId); }
|
||||
|
||||
// Fallback to GameObject.Find
|
||||
var targetObj = GameObject.Find(targetId);
|
||||
return targetObj?.transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -223,6 +223,15 @@ namespace common {
|
||||
return RowAt(index).GetComponent<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RectTransform of the row at the specified index.
|
||||
/// Returns null if index is out of bounds.
|
||||
/// </summary>
|
||||
public RectTransform GetRowRectTransform(int index) {
|
||||
if (index < 0 || index >= _rows.Count) return null;
|
||||
return _rows[index].GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
private void Awake() {
|
||||
if (ShadeAlternateRows) {
|
||||
_evenColor = rowPrefab.GetComponent<Image>().color;
|
||||
|
||||
@@ -4,6 +4,7 @@ go_library(
|
||||
name = "authservice_lib",
|
||||
srcs = [
|
||||
"admin_handlers.go",
|
||||
"apple_auth.go",
|
||||
"handlers.go",
|
||||
"invitation_handlers.go",
|
||||
"invitations.go",
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// AppleTokenResponse represents the response from Apple's token endpoint
|
||||
type AppleTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
}
|
||||
|
||||
// AppleIDTokenClaims represents the claims in Apple's id_token
|
||||
type AppleIDTokenClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
Email string `json:"email,omitempty"`
|
||||
EmailVerified string `json:"email_verified,omitempty"`
|
||||
IsPrivateEmail string `json:"is_private_email,omitempty"`
|
||||
RealUserStatus int `json:"real_user_status,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateAppleClientSecret generates a JWT client_secret for Apple Sign-In
|
||||
func GenerateAppleClientSecret() (string, error) {
|
||||
teamID := os.Getenv("APPLE_TEAM_ID")
|
||||
clientID := os.Getenv("APPLE_SIGNIN_CLIENT_ID")
|
||||
keyID := os.Getenv("APPLE_SIGNIN_KEY_ID")
|
||||
privateKeyPEM := os.Getenv("APPLE_SIGNIN_PRIVATE_KEY")
|
||||
|
||||
if teamID == "" || clientID == "" || keyID == "" || privateKeyPEM == "" {
|
||||
return "", fmt.Errorf("Apple Sign-In not fully configured")
|
||||
}
|
||||
|
||||
// Parse the private key
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil {
|
||||
// Try base64 decoding if not PEM format
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(privateKeyPEM)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode private key: %v", err)
|
||||
}
|
||||
block = &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse private key: %v", err)
|
||||
}
|
||||
|
||||
ecdsaKey, ok := privateKey.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("private key is not ECDSA")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := jwt.RegisteredClaims{
|
||||
Issuer: teamID,
|
||||
Subject: clientID,
|
||||
Audience: jwt.ClaimStrings{"https://appleid.apple.com"},
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(6 * 30 * 24 * time.Hour)), // ~6 months
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
|
||||
token.Header["kid"] = keyID
|
||||
|
||||
signedToken, err := token.SignedString(ecdsaKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign token: %v", err)
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
// ExchangeAppleCode exchanges an authorization code for tokens from Apple
|
||||
func (s *OAuthService) ExchangeAppleCode(config OAuthProviderConfig, code string) (*AppleTokenResponse, error) {
|
||||
clientSecret, err := GenerateAppleClientSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("client_id", config.ClientID)
|
||||
data.Set("client_secret", clientSecret)
|
||||
data.Set("grant_type", "authorization_code")
|
||||
data.Set("code", code)
|
||||
data.Set("redirect_uri", s.callbackURL)
|
||||
|
||||
req, err := http.NewRequest("POST", config.TokenEndpoint, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("Apple token exchange failed: %d - %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var tokenResp AppleTokenResponse
|
||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tokenResp, nil
|
||||
}
|
||||
|
||||
// ParseAppleIDToken extracts user info from Apple's id_token JWT
|
||||
func ParseAppleIDToken(idToken string) (*ProviderUserInfo, error) {
|
||||
// Split the JWT into parts
|
||||
parts := strings.Split(idToken, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid id_token format")
|
||||
}
|
||||
|
||||
// Decode the payload (middle part)
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode id_token payload: %v", err)
|
||||
}
|
||||
|
||||
var claims AppleIDTokenClaims
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse id_token claims: %v", err)
|
||||
}
|
||||
|
||||
// Apple user ID is in the 'sub' claim
|
||||
sub := claims.Subject
|
||||
|
||||
// Use email prefix as username fallback since Apple doesn't provide username
|
||||
username := claims.Email
|
||||
if idx := strings.Index(username, "@"); idx > 0 {
|
||||
username = username[:idx]
|
||||
}
|
||||
|
||||
return &ProviderUserInfo{
|
||||
ID: sub,
|
||||
Email: claims.Email,
|
||||
Username: username,
|
||||
AvatarURL: "", // Apple doesn't provide avatar
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleAppleCallback handles the Apple OAuth callback
|
||||
// Note: Apple sends a POST request to the callback URL, not GET
|
||||
func (s *OAuthService) HandleAppleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
// Apple POSTs the callback data
|
||||
if r.Method != "POST" {
|
||||
// Fallback to GET for the state parameter only
|
||||
state := r.URL.Query().Get("state")
|
||||
if state != "" {
|
||||
// This might be an error redirect
|
||||
errorParam := r.URL.Query().Get("error")
|
||||
if errorParam != "" {
|
||||
s.completeWithError(state, fmt.Sprintf("Apple OAuth error: %s", errorParam))
|
||||
http.Error(w, "OAuth failed: "+errorParam, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Error(w, "Apple Sign-In requires POST callback", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.FormValue("code")
|
||||
state := r.FormValue("state")
|
||||
errorParam := r.FormValue("error")
|
||||
|
||||
if errorParam != "" {
|
||||
s.completeWithError(state, fmt.Sprintf("Apple OAuth error: %s", errorParam))
|
||||
http.Error(w, "OAuth failed: "+errorParam, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if code == "" || state == "" {
|
||||
http.Error(w, "Missing code or state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get pending OAuth info
|
||||
stateData, ok := s.pendingOAuth.Load(state)
|
||||
if !ok {
|
||||
http.Error(w, "Invalid or expired state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
oauthState := stateData.(OAuthState)
|
||||
if time.Since(oauthState.CreatedAt) >= stateExpiration {
|
||||
s.pendingOAuth.Delete(state)
|
||||
s.completeWithError(state, "OAuth session expired")
|
||||
http.Error(w, "OAuth session expired", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Exchange code for tokens (Apple requires JWT client_secret)
|
||||
config := s.configs[oauthState.Provider]
|
||||
tokenResp, err := s.ExchangeAppleCode(config, code)
|
||||
if err != nil {
|
||||
s.completeWithError(state, err.Error())
|
||||
http.Error(w, "Token exchange failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse user info from id_token
|
||||
userInfo, err := ParseAppleIDToken(tokenResp.IDToken)
|
||||
if err != nil {
|
||||
s.completeWithError(state, err.Error())
|
||||
http.Error(w, "Failed to parse user info", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Apple may provide user info in the POST body on first auth
|
||||
// This includes the user's name which isn't in the id_token
|
||||
if userName := r.FormValue("user"); userName != "" {
|
||||
var user struct {
|
||||
Name struct {
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
} `json:"name"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(userName), &user); err == nil {
|
||||
if user.Name.FirstName != "" || user.Name.LastName != "" {
|
||||
userInfo.Username = strings.TrimSpace(user.Name.FirstName + " " + user.Name.LastName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as completed
|
||||
s.pendingOAuth.Delete(state)
|
||||
s.completedOAuth.Store(state, OAuthResult{
|
||||
Success: true,
|
||||
UserInfo: userInfo,
|
||||
Provider: oauthState.Provider,
|
||||
InvitationCode: oauthState.InvitationCode,
|
||||
})
|
||||
|
||||
// Redirect or show close message
|
||||
if oauthState.ReturnURL != "" {
|
||||
http.Redirect(w, r, oauthState.ReturnURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Login Successful</title></head>
|
||||
<body>
|
||||
<h1>Login Successful!</h1>
|
||||
<p>You can close this window and return to the game.</p>
|
||||
<script>window.close();</script>
|
||||
</body>
|
||||
</html>`)
|
||||
}
|
||||
@@ -146,6 +146,7 @@ func main() {
|
||||
|
||||
// Start HTTP server for OAuth callbacks and invitation pages
|
||||
http.HandleFunc("/oauth/callback", oauthSvc.HandleCallback)
|
||||
http.HandleFunc("/oauth/apple/callback", oauthSvc.HandleAppleCallback) // Apple uses POST
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "OK")
|
||||
@@ -180,5 +181,22 @@ func getOAuthConfigs() map[string]OAuthProviderConfig {
|
||||
UserInfoEndpoint: "https://www.googleapis.com/oauth2/v2/userinfo",
|
||||
Scopes: []string{"openid", "email", "profile"},
|
||||
},
|
||||
"github": {
|
||||
ClientID: os.Getenv("GH_OAUTH_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("GH_OAUTH_CLIENT_SECRET"),
|
||||
AuthorizationEndpoint: "https://github.com/login/oauth/authorize",
|
||||
TokenEndpoint: "https://github.com/login/oauth/access_token",
|
||||
UserInfoEndpoint: "https://api.github.com/user",
|
||||
Scopes: []string{"read:user", "user:email"},
|
||||
},
|
||||
"apple": {
|
||||
ClientID: os.Getenv("APPLE_SIGNIN_CLIENT_ID"),
|
||||
ClientSecret: "", // Generated dynamically from private key
|
||||
AuthorizationEndpoint: "https://appleid.apple.com/auth/authorize",
|
||||
TokenEndpoint: "https://appleid.apple.com/auth/token",
|
||||
UserInfoEndpoint: "", // Not used - info comes from id_token
|
||||
Scopes: []string{"name", "email"},
|
||||
CallbackPath: "/oauth/apple/callback", // Apple uses POST callback
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ type OAuthProviderConfig struct {
|
||||
TokenEndpoint string
|
||||
UserInfoEndpoint string
|
||||
Scopes []string
|
||||
CallbackPath string // Optional: custom callback path (e.g., "/oauth/apple/callback")
|
||||
}
|
||||
|
||||
// OAuthState represents a pending OAuth session
|
||||
@@ -111,13 +112,29 @@ func (s *OAuthService) GetAuthURL(provider string, returnURL string, invitationC
|
||||
|
||||
log.Printf("OAuth: created state=%s for provider=%s returnURL=%s hasInvitationCode=%v", state, provider, returnURL, invitationCode != "")
|
||||
|
||||
// Use custom callback path if configured, otherwise use default
|
||||
callbackURL := s.callbackURL
|
||||
if config.CallbackPath != "" {
|
||||
// Extract base URL and append custom path
|
||||
baseURL := s.callbackURL
|
||||
if idx := strings.LastIndex(baseURL, "/oauth/callback"); idx > 0 {
|
||||
baseURL = baseURL[:idx]
|
||||
}
|
||||
callbackURL = baseURL + config.CallbackPath
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("client_id", config.ClientID)
|
||||
params.Set("redirect_uri", s.callbackURL)
|
||||
params.Set("redirect_uri", callbackURL)
|
||||
params.Set("response_type", "code")
|
||||
params.Set("scope", strings.Join(config.Scopes, " "))
|
||||
params.Set("state", state)
|
||||
|
||||
// Apple-specific parameters
|
||||
if provider == "apple" {
|
||||
params.Set("response_mode", "form_post") // Apple posts the callback
|
||||
}
|
||||
|
||||
authURL = config.AuthorizationEndpoint + "?" + params.Encode()
|
||||
return authURL, state, nil
|
||||
}
|
||||
@@ -349,6 +366,23 @@ func (s *OAuthService) parseUserInfo(provider string, data []byte) (*ProviderUse
|
||||
AvatarURL: picture,
|
||||
}, nil
|
||||
|
||||
case "github":
|
||||
// GitHub returns id as a number
|
||||
var id string
|
||||
if idNum, ok := result["id"].(float64); ok {
|
||||
id = fmt.Sprintf("%.0f", idNum)
|
||||
}
|
||||
login, _ := result["login"].(string)
|
||||
email, _ := result["email"].(string)
|
||||
avatarURL, _ := result["avatar_url"].(string)
|
||||
|
||||
return &ProviderUserInfo{
|
||||
ID: id,
|
||||
Username: login,
|
||||
Email: email,
|
||||
AvatarURL: avatarURL,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider: %s", provider)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_bundle")
|
||||
|
||||
# Native Sparkle plugin for Unity
|
||||
# This plugin initializes Sparkle auto-updater at runtime
|
||||
objc_library(
|
||||
name = "sparkle_plugin_lib",
|
||||
srcs = ["SparklePlugin.m"],
|
||||
copts = [
|
||||
"-fmodules",
|
||||
"-fobjc-arc",
|
||||
"-fvisibility=default", # Export symbols for Unity P/Invoke
|
||||
],
|
||||
sdk_frameworks = ["Foundation"],
|
||||
deps = ["@sparkle//:Sparkle"],
|
||||
)
|
||||
|
||||
macos_bundle(
|
||||
name = "SparklePlugin",
|
||||
bundle_id = "net.eagle0.SparklePlugin",
|
||||
bundle_name = "SparklePlugin",
|
||||
infoplists = ["Info.plist"],
|
||||
linkopts = [
|
||||
# Export symbols for Unity P/Invoke
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_Initialize",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_CheckForUpdates",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_CheckForUpdatesInBackground",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_IsCheckingForUpdates",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_GetAutomaticallyChecksForUpdates",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_SetAutomaticallyChecksForUpdates",
|
||||
],
|
||||
minimum_os_version = "10.13",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [":sparkle_plugin_lib"],
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>SparklePlugin</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>net.eagle0.SparklePlugin</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>SparklePlugin</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// SparklePlugin.m
|
||||
// Native macOS plugin for Unity to initialize Sparkle auto-updates
|
||||
//
|
||||
// Copyright 2026 Dan Crosby
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Sparkle/Sparkle.h>
|
||||
|
||||
// The updater controller - retained for the lifetime of the app
|
||||
static SPUStandardUpdaterController *updaterController = nil;
|
||||
|
||||
// Export macro for Unity P/Invoke
|
||||
#define EXPORT __attribute__((visibility("default")))
|
||||
|
||||
// Called from Unity to initialize Sparkle
|
||||
// This should be called once at app startup
|
||||
EXPORT void SparklePlugin_Initialize(void) {
|
||||
if (updaterController != nil) {
|
||||
NSLog(@"SparklePlugin: Already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"SparklePlugin: Initializing Sparkle auto-updater");
|
||||
|
||||
// Create the updater controller with automatic update checking enabled
|
||||
// The feed URL and public key are read from Info.plist (SUFeedURL, SUPublicEDKey)
|
||||
updaterController = [[SPUStandardUpdaterController alloc]
|
||||
initWithStartingUpdater:YES
|
||||
updaterDelegate:nil
|
||||
userDriverDelegate:nil];
|
||||
|
||||
if (updaterController != nil) {
|
||||
NSLog(@"SparklePlugin: Sparkle initialized successfully");
|
||||
} else {
|
||||
NSLog(@"SparklePlugin: Failed to initialize Sparkle");
|
||||
}
|
||||
}
|
||||
|
||||
// Manually trigger an update check (shows UI)
|
||||
EXPORT void SparklePlugin_CheckForUpdates(void) {
|
||||
if (updaterController == nil) {
|
||||
NSLog(@"SparklePlugin: Not initialized, call Initialize first");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"SparklePlugin: Checking for updates");
|
||||
[updaterController checkForUpdates:nil];
|
||||
}
|
||||
|
||||
// Check for updates silently in background (no UI unless update found)
|
||||
EXPORT void SparklePlugin_CheckForUpdatesInBackground(void) {
|
||||
if (updaterController == nil) {
|
||||
NSLog(@"SparklePlugin: Not initialized, call Initialize first");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"SparklePlugin: Checking for updates in background");
|
||||
[[updaterController updater] checkForUpdatesInBackground];
|
||||
}
|
||||
|
||||
// Returns 1 if an update check is in progress, 0 otherwise
|
||||
EXPORT int SparklePlugin_IsCheckingForUpdates(void) {
|
||||
if (updaterController == nil) {
|
||||
return 0;
|
||||
}
|
||||
return [[updaterController updater] sessionInProgress] ? 1 : 0;
|
||||
}
|
||||
|
||||
// Returns 1 if automatic update checks are enabled, 0 otherwise
|
||||
EXPORT int SparklePlugin_GetAutomaticallyChecksForUpdates(void) {
|
||||
if (updaterController == nil) {
|
||||
return 0;
|
||||
}
|
||||
return [[updaterController updater] automaticallyChecksForUpdates] ? 1 : 0;
|
||||
}
|
||||
|
||||
// Enable or disable automatic update checks
|
||||
EXPORT void SparklePlugin_SetAutomaticallyChecksForUpdates(int enabled) {
|
||||
if (updaterController == nil) {
|
||||
NSLog(@"SparklePlugin: Not initialized");
|
||||
return;
|
||||
}
|
||||
[[updaterController updater] setAutomaticallyChecksForUpdates:(enabled != 0)];
|
||||
}
|
||||
@@ -39,6 +39,8 @@ enum OAuthProvider {
|
||||
OAUTH_PROVIDER_UNSPECIFIED = 0;
|
||||
OAUTH_PROVIDER_DISCORD = 1;
|
||||
OAUTH_PROVIDER_GOOGLE = 2;
|
||||
OAUTH_PROVIDER_GITHUB = 3;
|
||||
OAUTH_PROVIDER_APPLE = 4;
|
||||
}
|
||||
|
||||
// Request to initiate OAuth flow
|
||||
|
||||
@@ -24,6 +24,7 @@ message IncompleteText {
|
||||
.net.eagle0.eagle.internal.GeneratedTextRequest llm_request = 3;
|
||||
int32 requested_after_history_count = 4;
|
||||
int64 requested_at_millis = 5;
|
||||
int64 last_update_at_millis = 6;
|
||||
}
|
||||
|
||||
message UnrequestedText {
|
||||
|
||||
@@ -4,17 +4,16 @@ import scala.collection.Map
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand as ProtoAvailableCommand
|
||||
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands as ProtoOneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.library.util.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
AlmsCommandSelector,
|
||||
AttackDecisionCommandChooser,
|
||||
CommandChooser
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.*
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.library.Engine
|
||||
import net.eagle0.eagle.model.proto_converters.command.available.OneProvinceAvailableCommandsConverter
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
case class AIClientWithSelectedCommand(
|
||||
@@ -27,14 +26,12 @@ case class AIClient(
|
||||
factionId: FactionId,
|
||||
focusProvinceId: Option[ProvinceId] = None
|
||||
) {
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.*
|
||||
|
||||
val logger = new AIClientLogger(gameId = gameId, factionId = factionId)
|
||||
|
||||
val roundsBetweenRecruitAttempts = 5
|
||||
|
||||
def handleError(
|
||||
selectedCommand: Option[SelectedCommand],
|
||||
selectedCommand: Option[CommandSelection],
|
||||
error: Throwable
|
||||
): Unit = {
|
||||
println(s"Received an error with AI selected command $error")
|
||||
@@ -50,13 +47,8 @@ case class AIClient(
|
||||
|
||||
if scalaCommandsMap.isEmpty then RandomState(AIClientWithSelectedCommand(this, None), functionalRandom)
|
||||
else {
|
||||
// Convert to proto for command choosers (temporary until command choosers are migrated to Scala types)
|
||||
val protoCommandsMap = scalaCommandsMap.map {
|
||||
case (pid, scalaCmds) =>
|
||||
pid -> OneProvinceAvailableCommandsConverter.toProto(scalaCmds, engine.currentState, factionId)
|
||||
}
|
||||
chooseFrom(
|
||||
protoCommandsMap,
|
||||
scalaCommandsMap,
|
||||
engine.currentState,
|
||||
functionalRandom
|
||||
)
|
||||
@@ -65,7 +57,7 @@ case class AIClient(
|
||||
|
||||
private def chooseMidGameCommandFrom(
|
||||
gameState: GameState,
|
||||
oneProvinceAvailableCommands: ProtoOneProvinceAvailableCommands,
|
||||
oneProvinceAvailableCommands: OneProvinceAvailableCommands,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[AIClientWithSelectedCommand] =
|
||||
MidGameAIClient.chosenMidGameCommand(
|
||||
@@ -75,7 +67,7 @@ case class AIClient(
|
||||
functionalRandom = functionalRandom
|
||||
) match {
|
||||
case RandomState(
|
||||
CommandSelection(
|
||||
cs @ CommandSelection(
|
||||
actingFactionId,
|
||||
actingProvinceId,
|
||||
available,
|
||||
@@ -91,7 +83,7 @@ case class AIClient(
|
||||
s"$actingProvinceId (${gameState.provinces(actingProvinceId).name})"
|
||||
)
|
||||
logger.log(
|
||||
s"${selected.asMessage.toProtoString}"
|
||||
s"$selected"
|
||||
)
|
||||
logger.logLine(s" $reason")
|
||||
logger.logLine("")
|
||||
@@ -103,32 +95,23 @@ case class AIClient(
|
||||
factionId = this.factionId,
|
||||
focusProvinceId = focusProvinceId
|
||||
),
|
||||
Some(
|
||||
CommandSelection(
|
||||
actingFactionId,
|
||||
actingProvinceId,
|
||||
available,
|
||||
selected,
|
||||
reason
|
||||
)
|
||||
)
|
||||
Some(cs)
|
||||
),
|
||||
fr
|
||||
)
|
||||
}
|
||||
|
||||
private def chooseFrom(
|
||||
acs: Map[ProvinceId, ProtoOneProvinceAvailableCommands],
|
||||
acs: Map[ProvinceId, OneProvinceAvailableCommands],
|
||||
gs: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[AIClientWithSelectedCommand] = {
|
||||
val opac = acs.head._2
|
||||
val oneProvinceAcs = acs.head._2.commands
|
||||
val opac = acs.head._2
|
||||
|
||||
CommandChooser.choose(
|
||||
actingFactionId = factionId,
|
||||
gameState = gs,
|
||||
availableCommands = oneProvinceAcs.toVector,
|
||||
availableCommands = opac.commands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
handleCapturedHeroesSelectedCommand(
|
||||
@@ -181,7 +164,7 @@ case class AIClient(
|
||||
),
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
resolvePleaseRecruitMeSelectedCommand(
|
||||
resolvePleaseRecruitMeSelected(
|
||||
actingFactionId,
|
||||
gameState,
|
||||
availableCommands
|
||||
@@ -191,7 +174,7 @@ case class AIClient(
|
||||
(
|
||||
fid: FactionId,
|
||||
gameState: GameState,
|
||||
acs: Vector[ProtoAvailableCommand],
|
||||
commandsVector: Vector[AvailableCommand],
|
||||
functionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
@@ -199,7 +182,7 @@ case class AIClient(
|
||||
.chosenAlmsForSupportWithMinimumCommandForProvince(
|
||||
actingFactionId = fid,
|
||||
gameState = gameState,
|
||||
availableCommands = acs,
|
||||
availableCommands = commandsVector,
|
||||
minimumFood = 0,
|
||||
provinceId = opac.provinceId
|
||||
),
|
||||
@@ -225,7 +208,7 @@ case class AIClient(
|
||||
.chooseEarlyGameCommand(
|
||||
factionId,
|
||||
gs,
|
||||
oneProvinceAcs.toVector,
|
||||
opac.commands,
|
||||
fr
|
||||
) match {
|
||||
case RandomState(cs, fr2) =>
|
||||
|
||||
@@ -12,9 +12,6 @@ scala_library(
|
||||
":early_game_ai_client",
|
||||
":mid_game_ai_client",
|
||||
":resolve_diplomacy_command_selector",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:eagle_scala_grpc",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:engine",
|
||||
@@ -24,7 +21,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:alms_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:attack_decision_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:one_province_available_commands_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
|
||||
],
|
||||
)
|
||||
@@ -70,14 +68,16 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":resolve_diplomacy_command_selector",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:invite_gold_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_chance_for_a_i_invite",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:available_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:trust_for_diplomacy",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
@@ -93,7 +93,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
|
||||
@@ -134,13 +133,13 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:available_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
@@ -153,14 +152,14 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
":ai_client_utils",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:province_gold_surplus_calculator",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
)
|
||||
@@ -173,7 +172,6 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
@@ -184,9 +182,6 @@ scala_library(
|
||||
":march_toward_province_command_chooser",
|
||||
":move_leader_to_better_province_command_chooser",
|
||||
":seek_more_leaders_command_chooser",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:more_option",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
@@ -210,6 +205,9 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/food_consumption",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
@@ -232,10 +230,10 @@ scala_library(
|
||||
deps = [
|
||||
":faction_leader_province_ranker",
|
||||
":march_toward_province_command_chooser",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
@@ -251,8 +249,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
@@ -263,8 +259,10 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:available_command_selector",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:ransom_offer_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/faction",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
@@ -279,8 +277,6 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:more_option",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:loyalty_gain_from_feast",
|
||||
@@ -293,6 +289,8 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:sworn_brother_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers.{
|
||||
chosenCommandWhileTraveling,
|
||||
chosenDevelopCommand,
|
||||
@@ -11,6 +10,7 @@ import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooser
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, MarchAvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.randomSelectionForType
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.MarchAvailable
|
||||
import net.eagle0.eagle.model.state.command.selected.CombatUnit
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.MarchSelected
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
@@ -19,7 +20,7 @@ object FixLeaderAloneCommandSelector {
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
val factions = gameState.factions.values.toVector
|
||||
val provinces = gameState.provinces
|
||||
randomSelectionForType[MarchAvailableCommand](
|
||||
randomSelectionForType[MarchAvailable](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
@@ -40,7 +41,7 @@ object FixLeaderAloneCommandSelector {
|
||||
.map { destination =>
|
||||
fr.nextRandomElement(opmc.availableHeroIds)
|
||||
.map { hid =>
|
||||
CombatUnit(factionId = actingFactionId, heroId = hid)
|
||||
CombatUnit(heroId = hid, battalionId = None)
|
||||
}
|
||||
.map { combatUnit =>
|
||||
Some(
|
||||
@@ -48,7 +49,7 @@ object FixLeaderAloneCommandSelector {
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ac.actingProvinceId,
|
||||
available = ac,
|
||||
selected = MarchSelectedCommand(
|
||||
selected = MarchSelected(
|
||||
gold = 0,
|
||||
food = 0,
|
||||
originProvince = opmc.originProvinceId,
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, DiplomacyAvailableCommand}
|
||||
import net.eagle0.eagle.api.command.util.diplomacy_option.InvitationOption
|
||||
import net.eagle0.eagle.api.selected_command.DiplomacySelectedCommand
|
||||
import net.eagle0.eagle.library.settings.MinChanceForAIInvite
|
||||
import net.eagle0.eagle.library.settings.{InviteGoldCost, MinChanceForAIInvite}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{ProvinceGoldSurplusCalculator, TrustForDiplomacy}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.flatSelectionForType
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{DiplomacyAvailable, DiplomacyOption}
|
||||
import net.eagle0.eagle.model.state.command.common.DiplomacyOptionType
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.DiplomacySelected
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object InvitationCommandSelector {
|
||||
private case class InvitationOptionWithChance(
|
||||
invitationOption: InvitationOption,
|
||||
targetFactionId: FactionId,
|
||||
acceptanceChance: Int
|
||||
)
|
||||
|
||||
@@ -21,38 +22,44 @@ object InvitationCommandSelector {
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[DiplomacyAvailableCommand](acs = availableCommands) { diplomacyAvailableCommand =>
|
||||
diplomacyAvailableCommand.options.collect { case io: InvitationOption => io }.filter { invitationOption =>
|
||||
TrustForDiplomacy.meetsConditionsForInvitation(
|
||||
actingFactionId = actingFactionId,
|
||||
targetFactionId = invitationOption.targetFactionId,
|
||||
gameState = gameState
|
||||
)
|
||||
}.filter { invitationOption =>
|
||||
ProvinceGoldSurplusCalculator.provinceGoldSurplus(
|
||||
gameState.provinces(diplomacyAvailableCommand.actingProvinceId)
|
||||
) >= invitationOption.goldCost
|
||||
}.map { invitationOption =>
|
||||
InvitationOptionWithChance(
|
||||
invitationOption = invitationOption,
|
||||
acceptanceChance = ResolveDiplomacyCommandSelector.invitationAcceptanceChance(
|
||||
actingFactionId,
|
||||
invitationOption.targetFactionId,
|
||||
gameState.factions.values.toVector,
|
||||
gameState.provinces.values.toVector
|
||||
flatSelectionForType[DiplomacyAvailable](acs = availableCommands) { diplomacyAvailable =>
|
||||
// Find options that have Invitation as an option type
|
||||
diplomacyAvailable.options
|
||||
.filter(opt => opt.optionTypes.contains(DiplomacyOptionType.Invitation))
|
||||
.filter { diplomacyOption =>
|
||||
TrustForDiplomacy.meetsConditionsForInvitation(
|
||||
actingFactionId = actingFactionId,
|
||||
targetFactionId = diplomacyOption.targetFactionId,
|
||||
gameState = gameState
|
||||
)
|
||||
)
|
||||
}.maxByOption(_.acceptanceChance)
|
||||
}
|
||||
.filter { _ =>
|
||||
ProvinceGoldSurplusCalculator.provinceGoldSurplus(
|
||||
gameState.provinces(diplomacyAvailable.actingProvinceId)
|
||||
) >= InviteGoldCost.intValue
|
||||
}
|
||||
.map { diplomacyOption =>
|
||||
InvitationOptionWithChance(
|
||||
targetFactionId = diplomacyOption.targetFactionId,
|
||||
acceptanceChance = ResolveDiplomacyCommandSelector.invitationAcceptanceChance(
|
||||
actingFactionId,
|
||||
diplomacyOption.targetFactionId,
|
||||
gameState.factions.values.toVector,
|
||||
gameState.provinces.values.toVector
|
||||
)
|
||||
)
|
||||
}
|
||||
.maxByOption(_.acceptanceChance)
|
||||
.filter(_.acceptanceChance >= MinChanceForAIInvite.intValue)
|
||||
.map { iowc =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = diplomacyAvailableCommand.actingProvinceId,
|
||||
available = diplomacyAvailableCommand,
|
||||
selected = DiplomacySelectedCommand(
|
||||
selectedOption = iowc.invitationOption,
|
||||
targetFactionId = iowc.invitationOption.targetFactionId,
|
||||
sentHeroId = diplomacyAvailableCommand.recommendedHeroId
|
||||
actingProvinceId = diplomacyAvailable.actingProvinceId,
|
||||
available = diplomacyAvailable,
|
||||
selected = DiplomacySelected(
|
||||
selectedOption = DiplomacyOptionType.Invitation,
|
||||
targetFactionId = iowc.targetFactionId,
|
||||
sentHeroId = diplomacyAvailable.recommendedHeroId
|
||||
),
|
||||
reason = "maybeInviteOtherFaction"
|
||||
)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{MarchAvailableCommand, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.library.util.{BattalionUtils, CommandSelection, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ProvinceGoldSurplusCalculator.provinceGoldSurplus
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{MarchAvailable, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.model.state.command.selected.CombatUnit
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.MarchSelected
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object MarchTowardProvinceCommandChooser {
|
||||
def marchTowardProvinceCommand(
|
||||
ac: MarchAvailableCommand,
|
||||
ac: MarchAvailable,
|
||||
fid: FactionId,
|
||||
opmc: MarchCommandFromOneProvince,
|
||||
destinationProvinceId: ProvinceId,
|
||||
@@ -49,7 +49,7 @@ object MarchTowardProvinceCommandChooser {
|
||||
AIClientUtils.takenHeroIdsForMarchTowardFocus(
|
||||
originProvince = originProvince,
|
||||
destinationProvince = dest,
|
||||
availableHeroIds = opmc.availableHeroIds.toVector,
|
||||
availableHeroIds = opmc.availableHeroIds,
|
||||
favorLeaders = favorLeaders,
|
||||
factions = gs.factions.values.toVector,
|
||||
heroes = gs.heroes
|
||||
@@ -72,7 +72,7 @@ object MarchTowardProvinceCommandChooser {
|
||||
)
|
||||
.map {
|
||||
case (hid, bid) =>
|
||||
CombatUnit(factionId = fid, heroId = hid, battalionId = bid)
|
||||
CombatUnit(heroId = hid, battalionId = bid)
|
||||
}
|
||||
|
||||
val foodToTake = 2 * BattalionUtils.monthlyConsumedFood(
|
||||
@@ -84,7 +84,7 @@ object MarchTowardProvinceCommandChooser {
|
||||
actingFactionId = fid,
|
||||
actingProvinceId = actingProvinceId,
|
||||
available = ac,
|
||||
selected = MarchSelectedCommand(
|
||||
selected = MarchSelected(
|
||||
marchingUnits = takenUnits, // Check the food cost here?
|
||||
destinationProvinceId = dest.id,
|
||||
originProvince = opmc.originProvinceId,
|
||||
|
||||
@@ -3,11 +3,6 @@ package net.eagle0.eagle.ai
|
||||
import net.eagle0.common.{FunctionalRandom, MoreOption, RandomState}
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.ai.AIClientUtils
|
||||
import net.eagle0.eagle.api.available_command.*
|
||||
import net.eagle0.eagle.api.command.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.api.selected_command.{MarchSelectedCommand, ReconSelectedCommand}
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.common.improvement_type.ImprovementType
|
||||
import net.eagle0.eagle.library.settings.{MinSupportForTaxes, MonthsReconConsideredRecent}
|
||||
import net.eagle0.eagle.library.util.*
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
@@ -30,6 +25,12 @@ import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.util.ProvinceDistances
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.*
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.command.common.ImprovementType
|
||||
import net.eagle0.eagle.model.state.command.selected.CombatUnit
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.{MarchSelected, ReconSelected}
|
||||
import net.eagle0.eagle.model.state.date.Date.given
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship as FactionRelationshipC
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
@@ -66,7 +67,7 @@ object MidGameAIClient {
|
||||
CommandChooser.choose(
|
||||
actingFactionId = factionId,
|
||||
gameState = gs,
|
||||
availableCommands = opac.commands.toVector,
|
||||
availableCommands = opac.commands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
(fid: FactionId, gs: GameState, acs: Vector[AvailableCommand], fr: FunctionalRandom) =>
|
||||
chosenUniversalCommand(fid, gs, acs, fr),
|
||||
@@ -129,14 +130,13 @@ object MidGameAIClient {
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[SendSuppliesAvailableCommand](availableCommands) {
|
||||
case available @ SendSuppliesAvailableCommand(
|
||||
flatSelectionForType[SendSuppliesAvailable](availableCommands) {
|
||||
case available @ SendSuppliesAvailable(
|
||||
goldAvailable,
|
||||
foodAvailable,
|
||||
availableDestinationProvinceIds,
|
||||
actingProvinceId,
|
||||
_ /* availableHeroIds */,
|
||||
_ /* unknownFieldSet */
|
||||
_ /* availableHeroIds */
|
||||
) =>
|
||||
val actingProvince = gameState.provinces(actingProvinceId)
|
||||
val foodMonthsToHoldBack =
|
||||
@@ -546,7 +546,7 @@ object MidGameAIClient {
|
||||
.choose(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gameState,
|
||||
availableCommands = opac.commands.toVector,
|
||||
availableCommands = opac.commands,
|
||||
rankedChoosers = Vector[CommandChooser](
|
||||
(actingFactionId, gameState, availableCommands, functionalRandom) =>
|
||||
RandomState(
|
||||
@@ -683,7 +683,7 @@ object MidGameAIClient {
|
||||
acs: Vector[AvailableCommand],
|
||||
factionId: FactionId
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[ImproveAvailableCommand](acs) { availableCommand =>
|
||||
flatSelectionForType[ImproveAvailable](acs) { availableCommand =>
|
||||
val province = gs.provinces(availableCommand.actingProvinceId)
|
||||
val existingBattalions = province.battalionIds
|
||||
.map(gs.battalions)
|
||||
@@ -706,7 +706,7 @@ object MidGameAIClient {
|
||||
actingFactionId = factionId,
|
||||
actingProvinceId = province.id,
|
||||
availableCommands = acs,
|
||||
improvementType = ImprovementType.DEVASTATION
|
||||
improvementType = ImprovementType.Devastation
|
||||
)
|
||||
.orElse {
|
||||
val neededAgriculture = notAvailable
|
||||
@@ -729,7 +729,7 @@ object MidGameAIClient {
|
||||
actingFactionId = factionId,
|
||||
actingProvinceId = province.id,
|
||||
availableCommands = acs,
|
||||
improvementType = ImprovementType.AGRICULTURE
|
||||
improvementType = ImprovementType.Agriculture
|
||||
)
|
||||
else
|
||||
ImproveCommandSelector
|
||||
@@ -737,7 +737,7 @@ object MidGameAIClient {
|
||||
actingFactionId = factionId,
|
||||
actingProvinceId = province.id,
|
||||
availableCommands = acs,
|
||||
improvementType = ImprovementType.ECONOMY
|
||||
improvementType = ImprovementType.Economy
|
||||
)
|
||||
end if
|
||||
}
|
||||
@@ -856,7 +856,7 @@ object MidGameAIClient {
|
||||
.filter(p => p.rulingFactionHeroIds.size < p.heroCap)
|
||||
val cs = for {
|
||||
(marchCommand, _) <- acs.zipWithIndex.collectFirst {
|
||||
case (ac: MarchAvailableCommand, idx) => (ac, idx)
|
||||
case (ac: MarchAvailable, idx) => (ac, idx)
|
||||
}
|
||||
} yield {
|
||||
// Find the march command origin provinces that contain extra heroes
|
||||
@@ -924,14 +924,14 @@ object MidGameAIClient {
|
||||
private def selectedReconCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
ac: ReconAvailableCommand,
|
||||
ac: ReconAvailable,
|
||||
targetProvinceId: ProvinceId
|
||||
): CommandSelection =
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ac.actingProvinceId,
|
||||
available = ac,
|
||||
selected = ReconSelectedCommand(
|
||||
selected = ReconSelected(
|
||||
actingHeroId = ac.availableHeroIds.maxBy(hid => gameState.heroes(hid).vigor),
|
||||
targetProvinceId = targetProvinceId
|
||||
),
|
||||
@@ -943,7 +943,7 @@ object MidGameAIClient {
|
||||
gameState: GameState,
|
||||
acs: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
flatSelectionForType[ReconAvailableCommand](acs) { ac =>
|
||||
flatSelectionForType[ReconAvailable](acs) { ac =>
|
||||
val targetProvincesNeighboringMe = ac.availableTargetProvinces
|
||||
.map(gameState.provinces)
|
||||
.filter(_.rulingFactionId.exists(_ != actingFactionId))
|
||||
@@ -1013,8 +1013,8 @@ object MidGameAIClient {
|
||||
): Option[CommandSelection] = {
|
||||
val faction = gameState.factions(actingFactionId)
|
||||
val factionLeaders = faction.leaderIds
|
||||
flatSelectionForType[MarchAvailableCommand](acs) { marchAvailableCommand =>
|
||||
marchAvailableCommand.oneProvinceCommands.flatMap { opmc =>
|
||||
flatSelectionForType[MarchAvailable](acs) { marchAvailable =>
|
||||
marchAvailable.oneProvinceCommands.flatMap { opmc =>
|
||||
val originProvince = gameState.provinces(opmc.originProvinceId)
|
||||
val leadersInOriginProvince =
|
||||
originProvince.rulingFactionHeroIds.intersect(factionLeaders)
|
||||
@@ -1042,16 +1042,15 @@ object MidGameAIClient {
|
||||
validDestinations.minByOption(_.rulingFactionHeroIds.size).map { destinationProvince =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = marchAvailableCommand.actingProvinceId,
|
||||
available = marchAvailableCommand,
|
||||
selected = MarchSelectedCommand(
|
||||
actingProvinceId = marchAvailable.actingProvinceId,
|
||||
available = marchAvailable,
|
||||
selected = MarchSelected(
|
||||
gold = 0,
|
||||
food = 0,
|
||||
originProvince = originProvince.id,
|
||||
destinationProvinceId = destinationProvince.id,
|
||||
marchingUnits = Vector(
|
||||
CombatUnit(
|
||||
factionId = actingFactionId,
|
||||
heroId = leaderIdToMove,
|
||||
battalionId = None
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, MarchAvailableCommand, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.library.util.ProvinceDistances
|
||||
import net.eagle0.eagle.library.util.ProvinceDistances.ProvinceAndDistance
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{MarchAvailable, MarchCommandFromOneProvince}
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
@@ -31,11 +32,11 @@ object MoveLeaderToBetterProvinceCommandChooser {
|
||||
)
|
||||
|
||||
private def candidatesForMarchCommand(
|
||||
marchCommand: MarchAvailableCommand,
|
||||
marchCommand: MarchAvailable,
|
||||
gs: GameState,
|
||||
faction: FactionT
|
||||
): Vector[MarchCommandFromOneProvince] =
|
||||
marchCommand.oneProvinceCommands.toVector.filter { opmc =>
|
||||
marchCommand.oneProvinceCommands.filter { opmc =>
|
||||
val originProvince = gs.provinces(opmc.originProvinceId)
|
||||
if opmc.availableHeroIds.intersect(faction.leaderIds).isEmpty then false
|
||||
else if originProvince.rulingFactionHeroIds.length < 2 then false
|
||||
@@ -57,7 +58,7 @@ object MoveLeaderToBetterProvinceCommandChooser {
|
||||
ProvinceDistances
|
||||
.closestProvinceThroughFriendliesOption(
|
||||
desiredPid,
|
||||
mcfop.availableDestinationProvinces.map(_.provinceId).toVector,
|
||||
mcfop.availableDestinationProvinces.map(_.provinceId),
|
||||
actingFactionId,
|
||||
gs.provinces
|
||||
)
|
||||
@@ -78,7 +79,7 @@ object MoveLeaderToBetterProvinceCommandChooser {
|
||||
): Option[CommandSelection] = {
|
||||
val cs = for {
|
||||
faction <- gs.factions.get(actingFactionId)
|
||||
marchCommand <- acs.collectFirst { case ac: MarchAvailableCommand => ac }
|
||||
marchCommand <- acs.collectFirst { case ac: MarchAvailable => ac }
|
||||
} yield {
|
||||
// Find the march command origin provinces that contain a faction leader
|
||||
val candidatesWithDistances = candidateCommandsWithDistances(
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.available_command.{
|
||||
AvailableCommand,
|
||||
ResolveAllianceOfferAvailableCommand,
|
||||
ResolveBreakAllianceAvailableCommand,
|
||||
ResolveInvitationAvailableCommand,
|
||||
ResolveRansomOfferAvailableCommand,
|
||||
ResolveTruceOfferAvailableCommand
|
||||
}
|
||||
import net.eagle0.eagle.api.selected_command.{
|
||||
ResolveAllianceOfferSelectedCommand,
|
||||
ResolveBreakAllianceSelectedCommand,
|
||||
ResolveInvitationSelectedCommand,
|
||||
ResolveRansomOfferSelectedCommand,
|
||||
ResolveTruceOfferSelectedCommand,
|
||||
SelectedCommand
|
||||
}
|
||||
import net.eagle0.eagle.common.diplomacy_offer.RansomOfferDetails
|
||||
import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus.{
|
||||
DIPLOMACY_OFFER_STATUS_ACCEPTED,
|
||||
DIPLOMACY_OFFER_STATUS_IMPRISONED,
|
||||
DIPLOMACY_OFFER_STATUS_REJECTED
|
||||
}
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
AiAllianceAcceptanceBaseChance,
|
||||
AiTruceAcceptanceBaseChance,
|
||||
MinimumPrestigeDifferenceToInvite
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{CommandChooser, RansomOfferHelpers}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.{
|
||||
randomSelectionForType,
|
||||
selectionForType
|
||||
}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooser
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.*
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.*
|
||||
import net.eagle0.eagle.model.state.diplomacy_offer.status.{Accepted, Imprisoned, Rejected, Status}
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
@@ -71,7 +54,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
randomSelectionForType[ResolveInvitationAvailableCommand](
|
||||
randomSelectionForType[ResolveInvitationAvailable](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
@@ -96,7 +79,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
|
||||
private def selectionForResolveInvitationCommand(
|
||||
actingFactionId: FactionId,
|
||||
ac: ResolveInvitationAvailableCommand,
|
||||
ac: ResolveInvitationAvailable,
|
||||
gs: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[SelectedCommand] = functionalRandom.nextDouble.map { roll =>
|
||||
@@ -104,20 +87,20 @@ object ResolveDiplomacyCommandSelector {
|
||||
val factions = gs.factions.values.toVector
|
||||
val provinces = gs.provinces.values.toVector
|
||||
val bestInvitation = ac.invitations
|
||||
.maxBy(inv => invitationAcceptanceChance(inv.originatingFactionId, invitedFid, factions, provinces))
|
||||
.maxBy(inv => invitationAcceptanceChance(inv.offeringFactionId, invitedFid, factions, provinces))
|
||||
|
||||
internalRequire(
|
||||
bestInvitation.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_ACCEPTED),
|
||||
bestInvitation.eligibleStatuses.contains(Accepted),
|
||||
"Cannot handle invitation without ACCEPTED option"
|
||||
)
|
||||
internalRequire(
|
||||
bestInvitation.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_REJECTED),
|
||||
bestInvitation.eligibleStatuses.contains(Rejected),
|
||||
"Cannot handle invitation without REJECTED option"
|
||||
)
|
||||
|
||||
val bestOriginatingFid = bestInvitation.originatingFactionId
|
||||
val bestOriginatingFid = bestInvitation.offeringFactionId
|
||||
|
||||
ResolveInvitationSelectedCommand(
|
||||
ResolveInvitationSelected(
|
||||
originatingFactionId = bestOriginatingFid,
|
||||
resolution =
|
||||
if 100.0 * roll < invitationAcceptanceChance(
|
||||
@@ -126,8 +109,8 @@ object ResolveDiplomacyCommandSelector {
|
||||
factions,
|
||||
provinces
|
||||
)
|
||||
then DIPLOMACY_OFFER_STATUS_ACCEPTED
|
||||
else DIPLOMACY_OFFER_STATUS_REJECTED
|
||||
then Accepted
|
||||
else Rejected
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,7 +133,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
randomSelectionForType[ResolveTruceOfferAvailableCommand](
|
||||
randomSelectionForType[ResolveTruceOfferAvailable](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
@@ -175,7 +158,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
|
||||
private def selectionForResolveTruceOfferSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
ac: ResolveTruceOfferAvailableCommand,
|
||||
ac: ResolveTruceOfferAvailable,
|
||||
gs: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[SelectedCommand] = functionalRandom.nextDouble.map { roll =>
|
||||
@@ -183,20 +166,20 @@ object ResolveDiplomacyCommandSelector {
|
||||
val factions = gs.factions.values.toVector
|
||||
val provinces = gs.provinces.values.toVector
|
||||
val bestOffer = ac.offers
|
||||
.maxBy(inv => truceOfferAcceptanceChance(inv.originatingFactionId, actingFid, factions, provinces))
|
||||
.maxBy(inv => truceOfferAcceptanceChance(inv.offeringFactionId, actingFid, factions, provinces))
|
||||
|
||||
internalRequire(
|
||||
bestOffer.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_ACCEPTED),
|
||||
bestOffer.eligibleStatuses.contains(Accepted),
|
||||
"Cannot handle truce offer without ACCEPTED option"
|
||||
)
|
||||
internalRequire(
|
||||
bestOffer.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_REJECTED),
|
||||
bestOffer.eligibleStatuses.contains(Rejected),
|
||||
"Cannot handle truce offer without REJECTED option"
|
||||
)
|
||||
|
||||
val bestOriginatingFid = bestOffer.originatingFactionId
|
||||
val bestOriginatingFid = bestOffer.offeringFactionId
|
||||
|
||||
ResolveTruceOfferSelectedCommand(
|
||||
ResolveTruceOfferSelected(
|
||||
originatingFactionId = bestOriginatingFid,
|
||||
resolution =
|
||||
if 100.0 * roll < truceOfferAcceptanceChance(
|
||||
@@ -205,8 +188,8 @@ object ResolveDiplomacyCommandSelector {
|
||||
factions = factions,
|
||||
provinces = provinces
|
||||
)
|
||||
then DIPLOMACY_OFFER_STATUS_ACCEPTED
|
||||
else DIPLOMACY_OFFER_STATUS_REJECTED
|
||||
then Accepted
|
||||
else Rejected
|
||||
)
|
||||
}
|
||||
|
||||
@@ -271,7 +254,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand]
|
||||
): Option[CommandSelection] =
|
||||
selectionForType[ResolveRansomOfferAvailableCommand](availableCommands) { resolveRansomAc =>
|
||||
selectionForType[ResolveRansomOfferAvailable](availableCommands) { resolveRansomAc =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = 0,
|
||||
@@ -284,17 +267,18 @@ object ResolveDiplomacyCommandSelector {
|
||||
}
|
||||
|
||||
private def selectionForResolveRansomOfferSelectedCommand(
|
||||
ac: ResolveRansomOfferAvailableCommand
|
||||
): SelectedCommand =
|
||||
ac.offers.head.offerDetails match {
|
||||
case rod: RansomOfferDetails =>
|
||||
ResolveRansomOfferSelectedCommand(
|
||||
offeringFactionId = ac.offers.head.originatingFactionId,
|
||||
ransomOffer = Some(ac.offers.head),
|
||||
resolution = RansomOfferHelpers.chosenResolution(rod)
|
||||
)
|
||||
case _ => throw new EagleInternalException("Not a ransom offer")
|
||||
}
|
||||
ac: ResolveRansomOfferAvailable
|
||||
): SelectedCommand = {
|
||||
val offer = ac.offers.head
|
||||
// TODO: Proper scoring requires ransom details not currently in DiplomacyOfferInfo
|
||||
// For now, accept all ransom offers
|
||||
val resolution = if offer.eligibleStatuses.contains(Accepted) then Accepted else Rejected
|
||||
ResolveRansomOfferSelected(
|
||||
offeringFactionId = offer.offeringFactionId,
|
||||
prisonerHeroId = offer.heroId.get,
|
||||
resolution = resolution
|
||||
)
|
||||
}
|
||||
|
||||
private def resolveAllianceOfferSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
@@ -302,7 +286,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
randomSelectionForType[ResolveAllianceOfferAvailableCommand](
|
||||
randomSelectionForType[ResolveAllianceOfferAvailable](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
@@ -331,7 +315,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
randomSelectionForType[ResolveBreakAllianceAvailableCommand](
|
||||
randomSelectionForType[ResolveBreakAllianceAvailable](
|
||||
availableCommands,
|
||||
functionalRandom
|
||||
) {
|
||||
@@ -354,7 +338,7 @@ object ResolveDiplomacyCommandSelector {
|
||||
|
||||
private def selectionForResolveAllianceOfferSelectedCommand(
|
||||
actingFactionId: FactionId,
|
||||
ac: ResolveAllianceOfferAvailableCommand,
|
||||
ac: ResolveAllianceOfferAvailable,
|
||||
gs: GameState,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[SelectedCommand] = functionalRandom.nextDouble.map { roll =>
|
||||
@@ -362,20 +346,20 @@ object ResolveDiplomacyCommandSelector {
|
||||
val factions = gs.factions.values.toVector
|
||||
val provinces = gs.provinces.values.toVector
|
||||
val bestOffer = ac.offers
|
||||
.maxBy(inv => allianceOfferAcceptanceChance(inv.originatingFactionId, actingFid, factions, provinces))
|
||||
.maxBy(inv => allianceOfferAcceptanceChance(inv.offeringFactionId, actingFid, factions, provinces))
|
||||
|
||||
internalRequire(
|
||||
bestOffer.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_ACCEPTED),
|
||||
bestOffer.eligibleStatuses.contains(Accepted),
|
||||
"Cannot handle alliance offer without ACCEPTED option"
|
||||
)
|
||||
internalRequire(
|
||||
bestOffer.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_REJECTED),
|
||||
bestOffer.eligibleStatuses.contains(Rejected),
|
||||
"Cannot handle alliance offer without REJECTED option"
|
||||
)
|
||||
|
||||
val bestOriginatingFid = bestOffer.originatingFactionId
|
||||
val bestOriginatingFid = bestOffer.offeringFactionId
|
||||
|
||||
ResolveAllianceOfferSelectedCommand(
|
||||
ResolveAllianceOfferSelected(
|
||||
originatingFactionId = bestOriginatingFid,
|
||||
resolution =
|
||||
if 100.0 * roll < allianceOfferAcceptanceChance(
|
||||
@@ -384,26 +368,26 @@ object ResolveDiplomacyCommandSelector {
|
||||
factions = factions,
|
||||
provinces = provinces
|
||||
)
|
||||
then DIPLOMACY_OFFER_STATUS_ACCEPTED
|
||||
else DIPLOMACY_OFFER_STATUS_REJECTED
|
||||
then Accepted
|
||||
else Rejected
|
||||
)
|
||||
}
|
||||
|
||||
// FIXME: always imprison ambassadors for now
|
||||
// Note: doesn't use gameState or functionalRandom roll
|
||||
private def selectionForResolveBreakAllianceSelectedCommand(
|
||||
ac: ResolveBreakAllianceAvailableCommand,
|
||||
ac: ResolveBreakAllianceAvailable,
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[SelectedCommand] = functionalRandom.nextDouble.map { _ =>
|
||||
val offer = ac.offers.head
|
||||
|
||||
ResolveBreakAllianceSelectedCommand(
|
||||
originatingFactionId = ac.offers.head.originatingFactionId,
|
||||
ResolveBreakAllianceSelected(
|
||||
originatingFactionId = offer.offeringFactionId,
|
||||
resolution =
|
||||
if offer.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_IMPRISONED)
|
||||
then DIPLOMACY_OFFER_STATUS_IMPRISONED
|
||||
else if offer.eligibleStatuses.contains(DIPLOMACY_OFFER_STATUS_ACCEPTED)
|
||||
then DIPLOMACY_OFFER_STATUS_ACCEPTED
|
||||
if offer.eligibleStatuses.contains(Imprisoned)
|
||||
then Imprisoned
|
||||
else if offer.eligibleStatuses.contains(Accepted)
|
||||
then Accepted
|
||||
else
|
||||
throw new EagleInternalException(
|
||||
"Unable to handle a break alliance command without IMPRISONED or ACCEPTED options"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package net.eagle0.eagle.ai
|
||||
|
||||
import net.eagle0.common.MoreOption
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, MarchAvailableCommand}
|
||||
import net.eagle0.eagle.api.selected_command.MarchSelectedCommand
|
||||
import net.eagle0.eagle.common.combat_unit.CombatUnit
|
||||
import net.eagle0.eagle.library.settings.{LoyaltyGainFromFeast, MinimumLoyaltyForSwearBrotherhood}
|
||||
import net.eagle0.eagle.library.util.{CommandSelection, ProvinceDistances}
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
@@ -14,6 +11,10 @@ import net.eagle0.eagle.library.util.command_choice_helpers.{
|
||||
}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.hero.HeroUtils
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.MarchAvailable
|
||||
import net.eagle0.eagle.model.state.command.selected.CombatUnit
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.MarchSelected
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
@@ -32,7 +33,7 @@ object SeekMoreLeadersCommandChooser {
|
||||
|
||||
case class Candidate(hero: HeroT, province: ProvinceT)
|
||||
private def maybeMoveTowardDestination(
|
||||
command: MarchAvailableCommand,
|
||||
command: MarchAvailable,
|
||||
hero: HeroT,
|
||||
startingProvince: ProvinceT,
|
||||
factionHeadProvince: ProvinceT,
|
||||
@@ -62,10 +63,9 @@ object SeekMoreLeadersCommandChooser {
|
||||
actingFactionId = factionHeadProvince.rulingFactionId.get,
|
||||
actingProvinceId = command.actingProvinceId,
|
||||
available = command,
|
||||
selected = MarchSelectedCommand(
|
||||
selected = MarchSelected(
|
||||
marchingUnits = Vector(
|
||||
CombatUnit(
|
||||
factionId = factionHeadProvince.rulingFactionId.get,
|
||||
heroId = hero.id,
|
||||
battalionId = None
|
||||
)
|
||||
@@ -106,7 +106,7 @@ object SeekMoreLeadersCommandChooser {
|
||||
.map(hero => Candidate(hero, province))
|
||||
}
|
||||
|
||||
val marchACs = acs.collect { case ac: MarchAvailableCommand => ac }
|
||||
val marchACs = acs.collect { case ac: MarchAvailable => ac }
|
||||
|
||||
val marchableCandidates = candidates.flatMap { c =>
|
||||
marchACs.find { mac =>
|
||||
|
||||
@@ -35,6 +35,7 @@ scala_library(
|
||||
":oauth_config",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:auth_scala_grpc",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"@maven//:com_nimbusds_nimbus_jose_jwt",
|
||||
"@maven//:org_json4s_json4s_ast_3",
|
||||
"@maven//:org_json4s_json4s_core_3",
|
||||
"@maven//:org_json4s_json4s_native_3",
|
||||
|
||||
@@ -31,6 +31,26 @@ object OAuthConfig {
|
||||
scopes = Seq("openid", "email", "profile")
|
||||
)
|
||||
|
||||
def github: OAuthProviderConfig = OAuthProviderConfig(
|
||||
clientId = sys.env.getOrElse("GH_OAUTH_CLIENT_ID", ""),
|
||||
clientSecret = sys.env.getOrElse("GH_OAUTH_CLIENT_SECRET", ""),
|
||||
authorizationEndpoint = "https://github.com/login/oauth/authorize",
|
||||
tokenEndpoint = "https://github.com/login/oauth/access_token",
|
||||
userInfoEndpoint = "https://api.github.com/user",
|
||||
scopes = Seq("read:user", "user:email")
|
||||
)
|
||||
|
||||
// Apple Sign-In uses OIDC - user info comes from JWT id_token, not a userinfo endpoint.
|
||||
// The client_secret is generated dynamically from a private key by the Go authservice.
|
||||
def apple: OAuthProviderConfig = OAuthProviderConfig(
|
||||
clientId = sys.env.getOrElse("APPLE_SIGNIN_CLIENT_ID", ""),
|
||||
clientSecret = "", // Generated dynamically from private key
|
||||
authorizationEndpoint = "https://appleid.apple.com/auth/authorize",
|
||||
tokenEndpoint = "https://appleid.apple.com/auth/token",
|
||||
userInfoEndpoint = "", // Not used - info is in id_token
|
||||
scopes = Seq("name", "email")
|
||||
)
|
||||
|
||||
/** Check if Discord OAuth is configured */
|
||||
def isDiscordConfigured: Boolean =
|
||||
discord.clientId.nonEmpty && discord.clientSecret.nonEmpty
|
||||
@@ -38,4 +58,12 @@ object OAuthConfig {
|
||||
/** Check if Google OAuth is configured */
|
||||
def isGoogleConfigured: Boolean =
|
||||
google.clientId.nonEmpty && google.clientSecret.nonEmpty
|
||||
|
||||
/** Check if GitHub OAuth is configured */
|
||||
def isGitHubConfigured: Boolean =
|
||||
github.clientId.nonEmpty && github.clientSecret.nonEmpty
|
||||
|
||||
/** Check if Apple Sign-In is configured */
|
||||
def isAppleConfigured: Boolean =
|
||||
apple.clientId.nonEmpty && sys.env.getOrElse("APPLE_SIGNIN_PRIVATE_KEY", "").nonEmpty
|
||||
}
|
||||
|
||||
@@ -4,12 +4,18 @@ package net.eagle0.eagle.auth
|
||||
import java.net.{URI, URLEncoder}
|
||||
import java.net.http.{HttpClient, HttpRequest, HttpResponse}
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Duration
|
||||
import java.util.UUID
|
||||
import java.security.interfaces.ECPrivateKey
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.KeyFactory
|
||||
import java.time.{Duration, Instant}
|
||||
import java.util.{Base64, Date, UUID}
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.util.Try
|
||||
|
||||
import com.nimbusds.jose.{JWSAlgorithm, JWSHeader}
|
||||
import com.nimbusds.jose.crypto.ECDSASigner
|
||||
import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT}
|
||||
import net.eagle0.common.SimpleTimedLogger
|
||||
import net.eagle0.eagle.api.auth.auth.OAuthProvider
|
||||
import org.json4s.{DefaultFormats, JObject, JString}
|
||||
@@ -137,25 +143,26 @@ class OAuthServiceImpl(serverBaseUrl: String) extends OAuthService {
|
||||
case Some((provider, timestamp)) if System.currentTimeMillis() - timestamp < stateExpirationMs =>
|
||||
val config = getConfig(provider)
|
||||
|
||||
// Exchange code for access token
|
||||
val tokenResult = exchangeCodeForToken(config, code)
|
||||
tokenResult match {
|
||||
case Left(error) =>
|
||||
// Apple uses a different flow - user info comes from id_token
|
||||
val result = if provider == OAuthProvider.OAUTH_PROVIDER_APPLE then {
|
||||
handleAppleCallback(config, code)
|
||||
} else {
|
||||
// Standard OAuth flow: exchange code for access token, then fetch user info
|
||||
exchangeCodeForToken(provider, config, code).flatMap { accessToken =>
|
||||
fetchUserInfo(provider, config, accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
result match {
|
||||
case Left(error) =>
|
||||
completedOAuth.put(state, OAuthFailure(error))
|
||||
Left(error)
|
||||
case Right(accessToken) =>
|
||||
// Fetch user info
|
||||
fetchUserInfo(provider, config, accessToken) match {
|
||||
case Left(error) =>
|
||||
completedOAuth.put(state, OAuthFailure(error))
|
||||
Left(error)
|
||||
case Right(userInfo) =>
|
||||
completedOAuth.put(state, OAuthSuccess(userInfo, provider))
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"OAuth: handleCallback SUCCESS state=$state user=${userInfo.username} (completed now has ${completedOAuth.size} entries)"
|
||||
)
|
||||
Right(userInfo)
|
||||
}
|
||||
case Right(userInfo) =>
|
||||
completedOAuth.put(state, OAuthSuccess(userInfo, provider))
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"OAuth: handleCallback SUCCESS state=$state user=${userInfo.username} (completed now has ${completedOAuth.size} entries)"
|
||||
)
|
||||
Right(userInfo)
|
||||
}
|
||||
|
||||
case Some(_) =>
|
||||
@@ -189,10 +196,13 @@ class OAuthServiceImpl(serverBaseUrl: String) extends OAuthService {
|
||||
provider match {
|
||||
case OAuthProvider.OAUTH_PROVIDER_DISCORD => OAuthConfig.discord
|
||||
case OAuthProvider.OAUTH_PROVIDER_GOOGLE => OAuthConfig.google
|
||||
case OAuthProvider.OAUTH_PROVIDER_GITHUB => OAuthConfig.github
|
||||
case OAuthProvider.OAUTH_PROVIDER_APPLE => OAuthConfig.apple
|
||||
case _ => throw new IllegalArgumentException(s"Unsupported provider: $provider")
|
||||
}
|
||||
|
||||
private def exchangeCodeForToken(
|
||||
provider: OAuthProvider,
|
||||
config: OAuthProviderConfig,
|
||||
code: String
|
||||
): Either[String, String] =
|
||||
@@ -281,8 +291,142 @@ class OAuthServiceImpl(serverBaseUrl: String) extends OAuthService {
|
||||
val avatarUrl = (data \ "picture").extractOpt[String].getOrElse("")
|
||||
ProviderUserInfo(id, email, avatarUrl, name)
|
||||
|
||||
case OAuthProvider.OAUTH_PROVIDER_GITHUB =>
|
||||
val id = (data \ "id").extract[Long].toString
|
||||
val login = (data \ "login").extract[String]
|
||||
val email = (data \ "email").extractOpt[String].getOrElse("")
|
||||
val avatarUrl = (data \ "avatar_url").extractOpt[String].getOrElse("")
|
||||
ProviderUserInfo(id, email, avatarUrl, login)
|
||||
|
||||
case OAuthProvider.OAUTH_PROVIDER_APPLE =>
|
||||
// Apple user info comes from id_token JWT, not userinfo endpoint.
|
||||
// This case is handled by parseAppleIdToken in handleCallback.
|
||||
throw new IllegalArgumentException("Apple user info should be parsed from id_token")
|
||||
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(s"Unsupported provider: $provider")
|
||||
}
|
||||
}.toEither.left.map(e => s"Failed to parse user info: ${e.getMessage}")
|
||||
|
||||
// Apple Sign-In specific methods
|
||||
|
||||
/** Handle Apple OAuth callback - Apple uses a different flow with JWT client_secret and id_token */
|
||||
private def handleAppleCallback(
|
||||
config: OAuthProviderConfig,
|
||||
code: String
|
||||
): Either[String, ProviderUserInfo] =
|
||||
for {
|
||||
clientSecret <- generateAppleClientSecret()
|
||||
tokenResponse <- exchangeAppleCode(config, code, clientSecret)
|
||||
userInfo <- parseAppleIdToken(tokenResponse)
|
||||
} yield userInfo
|
||||
|
||||
/** Generate Apple client_secret JWT signed with the private key */
|
||||
private def generateAppleClientSecret(): Either[String, String] =
|
||||
Try {
|
||||
val teamId = sys.env.getOrElse("APPLE_TEAM_ID", "")
|
||||
val clientId = sys.env.getOrElse("APPLE_SIGNIN_CLIENT_ID", "")
|
||||
val keyId = sys.env.getOrElse("APPLE_SIGNIN_KEY_ID", "")
|
||||
val privateKey = sys.env.getOrElse("APPLE_SIGNIN_PRIVATE_KEY", "")
|
||||
|
||||
if teamId.isEmpty || clientId.isEmpty || keyId.isEmpty || privateKey.isEmpty then {
|
||||
return Left("Apple Sign-In not fully configured")
|
||||
}
|
||||
|
||||
// Parse the private key (PEM format, EC P-256)
|
||||
val keyLines = privateKey
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s", "")
|
||||
val keyBytes = Base64.getDecoder.decode(keyLines)
|
||||
val keySpec = new PKCS8EncodedKeySpec(keyBytes)
|
||||
val keyFactory = KeyFactory.getInstance("EC")
|
||||
val ecPrivateKey = keyFactory.generatePrivate(keySpec).asInstanceOf[ECPrivateKey]
|
||||
|
||||
val now = Instant.now()
|
||||
val expiresAt = now.plusSeconds(15777000) // ~6 months (Apple max)
|
||||
|
||||
val header = new JWSHeader.Builder(JWSAlgorithm.ES256)
|
||||
.keyID(keyId)
|
||||
.build()
|
||||
|
||||
val claims = new JWTClaimsSet.Builder()
|
||||
.issuer(teamId)
|
||||
.subject(clientId)
|
||||
.audience("https://appleid.apple.com")
|
||||
.issueTime(Date.from(now))
|
||||
.expirationTime(Date.from(expiresAt))
|
||||
.build()
|
||||
|
||||
val signedJWT = new SignedJWT(header, claims)
|
||||
signedJWT.sign(new ECDSASigner(ecPrivateKey))
|
||||
|
||||
Right(signedJWT.serialize())
|
||||
}.toEither.left.map(e => s"Failed to generate Apple client_secret: ${e.getMessage}").flatten
|
||||
|
||||
/** Exchange Apple authorization code for tokens */
|
||||
private def exchangeAppleCode(
|
||||
config: OAuthProviderConfig,
|
||||
code: String,
|
||||
clientSecret: String
|
||||
): Either[String, org.json4s.JValue] =
|
||||
Try {
|
||||
val formData = Map(
|
||||
"client_id" -> config.clientId,
|
||||
"client_secret" -> clientSecret,
|
||||
"grant_type" -> "authorization_code",
|
||||
"code" -> code,
|
||||
"redirect_uri" -> callbackUrl
|
||||
)
|
||||
|
||||
val formBody = formData.map {
|
||||
case (k, v) =>
|
||||
s"${URLEncoder.encode(k, StandardCharsets.UTF_8)}=${URLEncoder.encode(v, StandardCharsets.UTF_8)}"
|
||||
}
|
||||
.mkString("&")
|
||||
|
||||
val request = HttpRequest
|
||||
.newBuilder()
|
||||
.uri(URI.create(config.tokenEndpoint))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Accept", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(formBody))
|
||||
.build()
|
||||
|
||||
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
val body = response.body()
|
||||
|
||||
if response.statusCode() < 200 || response.statusCode() >= 300 then {
|
||||
return Left(s"Apple token exchange failed: ${response.statusCode()} - $body")
|
||||
}
|
||||
|
||||
Right(json.parse(body))
|
||||
}.toEither.left.map(e => s"Apple token exchange error: ${e.getMessage}").flatten
|
||||
|
||||
/** Parse user info from Apple's id_token JWT */
|
||||
private def parseAppleIdToken(tokenResponse: org.json4s.JValue): Either[String, ProviderUserInfo] =
|
||||
Try {
|
||||
val idToken = (tokenResponse \ "id_token").extract[String]
|
||||
|
||||
// Decode JWT payload (middle part) - Apple already verified it, so we just extract claims
|
||||
val parts = idToken.split("\\.")
|
||||
if parts.length != 3 then {
|
||||
return Left("Invalid id_token format")
|
||||
}
|
||||
|
||||
val payloadJson = new String(Base64.getUrlDecoder.decode(parts(1)), StandardCharsets.UTF_8)
|
||||
val payload = json.parse(payloadJson)
|
||||
|
||||
// Apple user ID is in 'sub' claim
|
||||
val sub = (payload \ "sub").extract[String]
|
||||
val email = (payload \ "email").extractOpt[String].getOrElse("")
|
||||
|
||||
// Apple doesn't provide username/avatar in id_token.
|
||||
// User's name is only available on first auth (in the POST body, not token).
|
||||
// We use email prefix as fallback username.
|
||||
val username = email.takeWhile(_ != '@')
|
||||
|
||||
Right(ProviderUserInfo(sub, email, "", username))
|
||||
}.toEither.left.map(e => s"Failed to parse Apple id_token: ${e.getMessage}").flatten
|
||||
}
|
||||
|
||||
@@ -22,10 +22,11 @@ case class IncompleteClientText(
|
||||
partialText: String,
|
||||
requestedAfterHistoryCount: Int,
|
||||
llmRequest: GeneratedTextRequest,
|
||||
requestedAtMillis: Long
|
||||
requestedAtMillis: Long,
|
||||
lastUpdateAtMillis: Long
|
||||
) extends ClientText {
|
||||
def append(newText: String): IncompleteClientText =
|
||||
copy(partialText = partialText + newText)
|
||||
copy(partialText = partialText + newText, lastUpdateAtMillis = System.currentTimeMillis())
|
||||
|
||||
override def text: String = partialText
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ case class ClientTextStoreImpl(
|
||||
unrequestedTexts
|
||||
.get(id)
|
||||
.map { unrequested =>
|
||||
val now = System.currentTimeMillis()
|
||||
copy(
|
||||
incompleteTexts = incompleteTexts + (id ->
|
||||
IncompleteClientText(
|
||||
@@ -70,7 +71,8 @@ case class ClientTextStoreImpl(
|
||||
partialText = "",
|
||||
llmRequest = unrequested.llmRequest,
|
||||
requestedAfterHistoryCount = unrequested.requestedAfterHistoryCount,
|
||||
requestedAtMillis = System.currentTimeMillis()
|
||||
requestedAtMillis = now,
|
||||
lastUpdateAtMillis = now
|
||||
)),
|
||||
unrequestedTexts = unrequestedTexts - id,
|
||||
incompleteTextsAreSaved = false
|
||||
@@ -240,7 +242,8 @@ object ClientTextStoreImpl {
|
||||
partialText = ict.text,
|
||||
llmRequest = Some(ict.llmRequest),
|
||||
requestedAfterHistoryCount = ict.requestedAfterHistoryCount,
|
||||
requestedAtMillis = ict.requestedAtMillis
|
||||
requestedAtMillis = ict.requestedAtMillis,
|
||||
lastUpdateAtMillis = ict.lastUpdateAtMillis
|
||||
)
|
||||
}.toVector,
|
||||
unrequestedTexts = completeSaved.unrequestedTexts.map {
|
||||
@@ -328,6 +331,7 @@ object ClientTextStoreImpl {
|
||||
.map { icts =>
|
||||
(
|
||||
icts.incompleteTexts.map { it =>
|
||||
val now = System.currentTimeMillis()
|
||||
IncompleteClientText(
|
||||
id = it.id,
|
||||
partialText = it.partialText,
|
||||
@@ -337,7 +341,10 @@ object ClientTextStoreImpl {
|
||||
// (for backwards compatibility with old persisted data)
|
||||
requestedAtMillis =
|
||||
if it.requestedAtMillis > 0 then it.requestedAtMillis
|
||||
else System.currentTimeMillis()
|
||||
else now,
|
||||
lastUpdateAtMillis =
|
||||
if it.lastUpdateAtMillis > 0 then it.lastUpdateAtMillis
|
||||
else now
|
||||
)
|
||||
}.toVector,
|
||||
icts.unrequestedTexts.map { it =>
|
||||
|
||||
@@ -128,13 +128,13 @@ scala_library(
|
||||
exports = [
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:game_history",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/date",
|
||||
@@ -460,7 +460,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:available_command_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
@@ -1304,7 +1303,6 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:more_option",
|
||||
@@ -1319,7 +1317,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:available_command_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
@@ -1342,7 +1339,6 @@ scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
|
||||
@@ -1354,7 +1350,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:available_command_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.eagle.common.action_result_type.ActionResultType.FACTION_DESTROYED
|
||||
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
|
||||
import net.eagle0.eagle.library.GameHistory
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.chronicle_event.*
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType
|
||||
import net.eagle0.eagle.model.action_result.NotificationDetails
|
||||
import net.eagle0.eagle.model.proto_converters.date.DateConverter
|
||||
import net.eagle0.eagle.model.proto_converters.NotificationConverter
|
||||
@@ -319,8 +319,8 @@ object ChronicleEventGenerator {
|
||||
private def basicActionResultEntries(
|
||||
awrs: ActionWithResultingState
|
||||
): Vector[ChronicleEvent] =
|
||||
awrs.actionResult.`type` match {
|
||||
case FACTION_DESTROYED =>
|
||||
ActionResultType.fromValue(awrs.actionResult.`type`.value) match {
|
||||
case ActionResultType.FactionDestroyed =>
|
||||
val date = DateConverter.fromProto(awrs.gameState.currentDate)
|
||||
awrs.actionResult.removedFactionIds.map { factionId =>
|
||||
FactionDestroyedChronicleEvent(
|
||||
|
||||
+3
-8
@@ -11,7 +11,6 @@ import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedPro
|
||||
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType.EndHandleRiotsPhase
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.command.available.AvailableCommandConverter
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
@@ -39,19 +38,15 @@ case class EndHandleRiotsPhaseAction(
|
||||
commandsForProvince(province.id).map { opac =>
|
||||
seq.withRandomActionResults { (gs, fr) =>
|
||||
val actingFactionId = province.rulingFactionId.get
|
||||
val protoCommands = opac.commands.map { cmd =>
|
||||
AvailableCommandConverter.toProto(cmd, gs, actingFactionId)
|
||||
}
|
||||
CommandChoiceHelpers
|
||||
.handleRiotSelectedCommand(
|
||||
actingFactionId = actingFactionId,
|
||||
gameState = gs,
|
||||
availableCommands = protoCommands,
|
||||
availableCommands = opac.commands,
|
||||
functionalRandom = fr
|
||||
)
|
||||
.continue {
|
||||
case (Some(cs), nextFr) =>
|
||||
val scs = cs.toScala(opac.commands)
|
||||
case (Some(scs), nextFr) =>
|
||||
val cmd = commandFactory.makeTCommand(
|
||||
actingFactionId = province.rulingFactionId.get,
|
||||
gameState = gs,
|
||||
@@ -66,7 +61,7 @@ case class EndHandleRiotsPhaseAction(
|
||||
case TCommand.Sequential(action) =>
|
||||
RandomState(action.results, nextFr)
|
||||
}
|
||||
case (None, nextFr) =>
|
||||
case (None, nextFr) =>
|
||||
RandomState(Vector.empty[ActionResultT], nextFr)
|
||||
}
|
||||
}
|
||||
|
||||
+45
-50
@@ -2,7 +2,6 @@ package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, MoreOption, RandomState}
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand as ProtoAvailableCommand, RestAvailableCommand}
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
import net.eagle0.eagle.library.actions.impl.command.TCommandFactory
|
||||
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, TCommand}
|
||||
@@ -11,8 +10,8 @@ import net.eagle0.eagle.library.util.command_choice_helpers.CommandChooser
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.command.available.AvailableCommandConverter
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.command.available.{AvailableCommand, OneProvinceAvailableCommands}
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.RestAvailable
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceOrderType.*
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
@@ -47,11 +46,10 @@ case class PerformVassalCommandsPhaseAction(
|
||||
case (sequencer, province) =>
|
||||
commandsForProvince(province.id).map { opac =>
|
||||
sequencer.withRandomActionResults { (gs, fr) =>
|
||||
chooseCommand(gs, province.id, fr).continue {
|
||||
case (Some(cs), nextFr) =>
|
||||
val scs = cs.toScala(opac.commands)
|
||||
chooseCommand(gs, province.id, opac.commands, fr).continue {
|
||||
case (Some(scs), nextFr) =>
|
||||
val cmd = commandFactory.makeTCommand(
|
||||
actingFactionId = cs.actingFactionId,
|
||||
actingFactionId = scs.actingFactionId,
|
||||
gameState = gs,
|
||||
availableCommand = scs.available,
|
||||
selectedCommand = scs.selected
|
||||
@@ -64,7 +62,7 @@ case class PerformVassalCommandsPhaseAction(
|
||||
case TCommand.Sequential(action) =>
|
||||
RandomState(action.results, nextFr)
|
||||
}
|
||||
case (None, nextFr) =>
|
||||
case (None, nextFr) =>
|
||||
RandomState(Vector.empty[ActionResultT], nextFr)
|
||||
}
|
||||
}
|
||||
@@ -76,7 +74,7 @@ case class PerformVassalCommandsPhaseAction(
|
||||
private def maybeRestCommand(
|
||||
actingFactionId: FactionId,
|
||||
gs: GameState,
|
||||
commandOptions: Vector[ProtoAvailableCommand],
|
||||
commandOptions: Vector[AvailableCommand],
|
||||
provinceId: ProvinceId,
|
||||
reason: String
|
||||
): Option[CommandSelection] = {
|
||||
@@ -84,7 +82,7 @@ case class PerformVassalCommandsPhaseAction(
|
||||
gs.provinces(provinceId).rulingFactionHeroIds.map(gs.heroes)
|
||||
MoreOption.flatWhen(
|
||||
commandOptions.collectFirst {
|
||||
case ac: RestAvailableCommand =>
|
||||
case ac: RestAvailable =>
|
||||
ac
|
||||
}.isDefined && shouldRest(heroes)
|
||||
) {
|
||||
@@ -95,7 +93,7 @@ case class PerformVassalCommandsPhaseAction(
|
||||
private def selectedCommandFromOrders(
|
||||
actingFactionId: FactionId,
|
||||
gs: GameState,
|
||||
commandOptions: Vector[ProtoAvailableCommand],
|
||||
commandOptions: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom,
|
||||
provinceId: ProvinceId
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
@@ -145,45 +143,42 @@ case class PerformVassalCommandsPhaseAction(
|
||||
def chooseCommand(
|
||||
gs: GameState,
|
||||
provinceId: ProvinceId,
|
||||
commands: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
commandsForProvince(provinceId).map { oneProvinceAvailableCommands =>
|
||||
val actingFactionId = gs.provinces(provinceId).rulingFactionId.get
|
||||
val protoCommands = oneProvinceAvailableCommands.commands.map { cmd =>
|
||||
AvailableCommandConverter.toProto(cmd, gs, actingFactionId)
|
||||
}
|
||||
CommandChooser.choose(
|
||||
actingFactionId,
|
||||
gs,
|
||||
protoCommands,
|
||||
Vector[CommandChooser](
|
||||
resolveTributeSelectedCommand,
|
||||
defendSelectedCommand,
|
||||
handleRiotSelectedCommand,
|
||||
(
|
||||
fid: FactionId,
|
||||
gsC: GameState,
|
||||
acs: Vector[ProtoAvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
maybeRestCommand(
|
||||
fid,
|
||||
gsC,
|
||||
acs,
|
||||
provinceId,
|
||||
"chosen vassal command: rest"
|
||||
),
|
||||
fr
|
||||
): RandomState[Option[CommandSelection]] = {
|
||||
val actingFactionId = gs.provinces(provinceId).rulingFactionId.get
|
||||
CommandChooser.choose(
|
||||
actingFactionId,
|
||||
gs,
|
||||
commands,
|
||||
Vector[CommandChooser](
|
||||
resolveTributeSelectedCommand,
|
||||
defendSelectedCommand,
|
||||
handleRiotSelectedCommand,
|
||||
(
|
||||
fid: FactionId,
|
||||
gsC: GameState,
|
||||
acs: Vector[AvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) =>
|
||||
RandomState(
|
||||
maybeRestCommand(
|
||||
fid,
|
||||
gsC,
|
||||
acs,
|
||||
provinceId,
|
||||
"chosen vassal command: rest"
|
||||
),
|
||||
(
|
||||
fid: FactionId,
|
||||
gsC: GameState,
|
||||
acs: Vector[ProtoAvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) => selectedCommandFromOrders(fid, gsC, acs, fr, provinceId)
|
||||
),
|
||||
functionalRandom
|
||||
)
|
||||
}.get
|
||||
fr
|
||||
),
|
||||
(
|
||||
fid: FactionId,
|
||||
gsC: GameState,
|
||||
acs: Vector[AvailableCommand],
|
||||
fr: FunctionalRandom
|
||||
) => selectedCommandFromOrders(fid, gsC, acs, fr, provinceId)
|
||||
),
|
||||
functionalRandom
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-13
@@ -1,7 +1,6 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand as ProtoAvailableCommand
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
import net.eagle0.eagle.library.actions.impl.command.TCommandFactory
|
||||
import net.eagle0.eagle.library.actions.impl.common.{ProtolessRandomSequentialResultsAction, TCommand}
|
||||
@@ -10,8 +9,7 @@ import net.eagle0.eagle.library.util.command_choice_helpers.CommandChoiceHelpers
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.proto_converters.command.available.AvailableCommandConverter
|
||||
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
|
||||
import net.eagle0.eagle.model.state.command.available.{AvailableCommand, OneProvinceAvailableCommands}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
@@ -43,15 +41,10 @@ case class PerformVassalDefenseDecisionsAction(
|
||||
case (sequencer, province) =>
|
||||
commandsForProvince(province.id).map { opac =>
|
||||
sequencer.withRandomActionResults { (gs, fr) =>
|
||||
val actingFactionId = province.rulingFactionId.get
|
||||
val protoCommands = opac.commands.map { cmd =>
|
||||
AvailableCommandConverter.toProto(cmd, gs, actingFactionId)
|
||||
}
|
||||
chooseCommand(gs, province.id, protoCommands, fr).continue {
|
||||
case (Some(cs), nextFr) =>
|
||||
val scs = cs.toScala(opac.commands)
|
||||
chooseCommand(gs, province.id, opac.commands, fr).continue {
|
||||
case (Some(scs), nextFr) =>
|
||||
val cmd = commandFactory.makeTCommand(
|
||||
actingFactionId = cs.actingFactionId,
|
||||
actingFactionId = scs.actingFactionId,
|
||||
gameState = gs,
|
||||
availableCommand = scs.available,
|
||||
selectedCommand = scs.selected
|
||||
@@ -64,7 +57,7 @@ case class PerformVassalDefenseDecisionsAction(
|
||||
case TCommand.Sequential(action) =>
|
||||
RandomState(action.results, nextFr)
|
||||
}
|
||||
case (None, nextFr) =>
|
||||
case (None, nextFr) =>
|
||||
RandomState(Vector.empty[ActionResultT], nextFr)
|
||||
}
|
||||
}
|
||||
@@ -76,7 +69,7 @@ case class PerformVassalDefenseDecisionsAction(
|
||||
private def chooseCommand(
|
||||
gs: GameState,
|
||||
provinceId: ProvinceId,
|
||||
commandOptions: Vector[ProtoAvailableCommand],
|
||||
commandOptions: Vector[AvailableCommand],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Option[CommandSelection]] =
|
||||
CommandChoiceHelpers.resolveTributeSelectedCommand(
|
||||
|
||||
@@ -80,6 +80,10 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:alliance_gold_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:break_alliance_gold_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:invite_gold_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:truce_gold_cost",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:quest_fulfillment_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
|
||||
@@ -13,6 +13,7 @@ import net.eagle0.eagle.library.actions.impl.common.{
|
||||
ProtolessSimpleAction,
|
||||
TCommand
|
||||
}
|
||||
import net.eagle0.eagle.library.settings.{AllianceGoldCost, BreakAllianceGoldCost, InviteGoldCost, TruceGoldCost}
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentChecker
|
||||
import net.eagle0.eagle.library.EagleCommandException
|
||||
@@ -120,7 +121,7 @@ class CommandFactory extends TCommandFactory {
|
||||
armedBattalions = sc.armedBattalions.map { ab =>
|
||||
ArmTroopsCommand.ArmedBattalion(
|
||||
id = ab.battalionId,
|
||||
newArmament = ab.battalionTypeId.toDouble
|
||||
newArmament = ab.newArmament.toDouble
|
||||
)
|
||||
},
|
||||
actingProvince = gameState.provinces(ac.actingProvinceId),
|
||||
@@ -780,11 +781,14 @@ class CommandFactory extends TCommandFactory {
|
||||
targetFactionId: FactionId
|
||||
): DiplomacyOption =
|
||||
dot match {
|
||||
// Using goldCost = 0 since availability factory already ensures options are affordable
|
||||
case DiplomacyOptionType.Alliance => DiplomacyOption.AllianceOption(targetFactionId, goldCost = 0)
|
||||
case DiplomacyOptionType.Truce => DiplomacyOption.TruceOption(targetFactionId, goldCost = 0)
|
||||
case DiplomacyOptionType.Invitation => DiplomacyOption.InvitationOption(targetFactionId, goldCost = 0)
|
||||
case DiplomacyOptionType.BreakAlliance => DiplomacyOption.BreakAllianceOption(targetFactionId, goldCost = 0)
|
||||
case DiplomacyOptionType.Alliance =>
|
||||
DiplomacyOption.AllianceOption(targetFactionId, goldCost = AllianceGoldCost.intValue)
|
||||
case DiplomacyOptionType.Truce =>
|
||||
DiplomacyOption.TruceOption(targetFactionId, goldCost = TruceGoldCost.intValue)
|
||||
case DiplomacyOptionType.Invitation =>
|
||||
DiplomacyOption.InvitationOption(targetFactionId, goldCost = InviteGoldCost.intValue)
|
||||
case DiplomacyOptionType.BreakAlliance =>
|
||||
DiplomacyOption.BreakAllianceOption(targetFactionId, goldCost = BreakAllianceGoldCost.intValue)
|
||||
case DiplomacyOptionType.Ransom(details) =>
|
||||
DiplomacyOption.RansomOfferOption(
|
||||
targetFactionId = targetFactionId,
|
||||
|
||||
@@ -101,10 +101,7 @@ scala_library(
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
|
||||
],
|
||||
|
||||
@@ -1,51 +1,16 @@
|
||||
package net.eagle0.eagle.library.util
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.api.available_command.AvailableCommand as ProtoAvailableCommand
|
||||
import net.eagle0.eagle.api.selected_command.SelectedCommand as ProtoSelectedCommand
|
||||
import net.eagle0.eagle.model.proto_converters.command.selected.SelectedCommandConverter
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
|
||||
|
||||
// Proto-based command selection (legacy, for gradual migration)
|
||||
case class CommandSelection(
|
||||
actingFactionId: FactionId,
|
||||
actingProvinceId: ProvinceId,
|
||||
available: ProtoAvailableCommand,
|
||||
selected: ProtoSelectedCommand,
|
||||
reason: String
|
||||
) {
|
||||
def withReason(reason: String): CommandSelection =
|
||||
this.copy(reason = reason)
|
||||
|
||||
/** Convert to ScalaCommandSelection by finding the matching Scala AvailableCommand */
|
||||
def toScala(scalaAvailableCommands: Iterable[AvailableCommand]): ScalaCommandSelection = {
|
||||
val scalaSelectedCommand = SelectedCommandConverter.fromProto(selected)
|
||||
val scalaAvailableCommand = scalaAvailableCommands
|
||||
.find(_.commandType == scalaSelectedCommand.commandType)
|
||||
.getOrElse(
|
||||
throw new IllegalStateException(
|
||||
s"No matching Scala available command for selected command: ${scalaSelectedCommand.commandType}"
|
||||
)
|
||||
)
|
||||
ScalaCommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = actingProvinceId,
|
||||
available = scalaAvailableCommand,
|
||||
selected = scalaSelectedCommand,
|
||||
reason = reason
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Scala-based command selection
|
||||
case class ScalaCommandSelection(
|
||||
actingFactionId: FactionId,
|
||||
actingProvinceId: ProvinceId,
|
||||
available: AvailableCommand,
|
||||
selected: SelectedCommand,
|
||||
reason: String
|
||||
) {
|
||||
def withReason(reason: String): ScalaCommandSelection =
|
||||
def withReason(reason: String): CommandSelection =
|
||||
this.copy(reason = reason)
|
||||
}
|
||||
|
||||
+15
-22
@@ -1,12 +1,13 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId}
|
||||
import net.eagle0.eagle.api.available_command.{AvailableCommand, DiplomacyAvailableCommand}
|
||||
import net.eagle0.eagle.api.command.util.diplomacy_option.{AllianceOption, DiplomacyOption}
|
||||
import net.eagle0.eagle.api.selected_command.DiplomacySelectedCommand
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.AvailableCommandSelector.ofType
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.CommandSelection
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand
|
||||
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{DiplomacyAvailable, DiplomacyOption}
|
||||
import net.eagle0.eagle.model.state.command.common.DiplomacyOptionType
|
||||
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.DiplomacySelected
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
|
||||
object AllianceOfferCommandSelector {
|
||||
@@ -16,13 +17,13 @@ object AllianceOfferCommandSelector {
|
||||
): Vector[HeroId] =
|
||||
hids.filterNot(FactionUtils.isFactionLeader(_, gameState.factions.values.toVector))
|
||||
|
||||
private def allianceOptionWithTarget(
|
||||
private def hasAllianceOptionWithTarget(
|
||||
options: Vector[DiplomacyOption],
|
||||
targetFactionId: FactionId
|
||||
): Option[AllianceOption] =
|
||||
options.collectFirst {
|
||||
case x @ AllianceOption(tfid, _, _ /* unknownFieldSet */ ) if tfid == targetFactionId =>
|
||||
x
|
||||
): Boolean =
|
||||
options.exists { opt =>
|
||||
opt.targetFactionId == targetFactionId &&
|
||||
opt.optionTypes.contains(DiplomacyOptionType.Alliance)
|
||||
}
|
||||
|
||||
def chosenAllianceWithFactionCommand(
|
||||
@@ -31,26 +32,18 @@ object AllianceOfferCommandSelector {
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
targetFactionId: FactionId
|
||||
): Option[CommandSelection] =
|
||||
ofType[DiplomacyAvailableCommand](availableCommands).find { ac =>
|
||||
allianceOptionWithTarget(
|
||||
ac.options.toVector,
|
||||
targetFactionId
|
||||
).isDefined && usableHeroIds(
|
||||
ac.availableHeroIds.toVector,
|
||||
gameState
|
||||
).nonEmpty
|
||||
ofType[DiplomacyAvailable](availableCommands).find { ac =>
|
||||
hasAllianceOptionWithTarget(ac.options, targetFactionId) &&
|
||||
usableHeroIds(ac.availableHeroIds, gameState).nonEmpty
|
||||
}.map { ac =>
|
||||
CommandSelection(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = ac.actingProvinceId,
|
||||
available = ac,
|
||||
selected = DiplomacySelectedCommand(
|
||||
selectedOption = allianceOptionWithTarget(
|
||||
ac.options.toVector,
|
||||
targetFactionId
|
||||
).get,
|
||||
selected = DiplomacySelected(
|
||||
selectedOption = DiplomacyOptionType.Alliance,
|
||||
targetFactionId = targetFactionId,
|
||||
sentHeroId = usableHeroIds(ac.availableHeroIds.toVector, gameState).maxBy { hid =>
|
||||
sentHeroId = usableHeroIds(ac.availableHeroIds, gameState).maxBy { hid =>
|
||||
gameState.heroes(hid).vigor
|
||||
}
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user