mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 06:55:41 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91c990da04 | ||
|
|
f15aab6fe7 |
@@ -27,8 +27,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
test:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -36,14 +37,6 @@ jobs:
|
||||
lfs: false
|
||||
- name: Check BUILD.bazel dependencies
|
||||
run: ./scripts/check_build_deps.sh --strict
|
||||
|
||||
test:
|
||||
runs-on: [self-hosted, bazel]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
- name: Run tests
|
||||
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
|
||||
- name: Collect failed test logs
|
||||
|
||||
@@ -184,23 +184,11 @@ jobs:
|
||||
- name: Upload to TestFlight
|
||||
if: ${{ github.event.inputs.skip_upload != 'true' }}
|
||||
env:
|
||||
# App Store Connect API Key (replaces deprecated altool with Apple ID)
|
||||
# Create at: https://appstoreconnect.apple.com/access/api
|
||||
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
|
||||
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
|
||||
run: |
|
||||
# Write API key to file (xcodebuild needs a file path)
|
||||
API_KEY_PATH=$(mktemp)
|
||||
echo "$APP_STORE_CONNECT_API_KEY" > "$API_KEY_PATH"
|
||||
export APP_STORE_CONNECT_API_KEY_PATH="$API_KEY_PATH"
|
||||
|
||||
chmod +x ./ci/github_actions/upload_testflight.sh
|
||||
./ci/github_actions/upload_testflight.sh "$EAGLE0_BUILD_DIR/archive/eagle0.xcarchive" "$APPLE_TEAM_ID" "$PROFILE_UUID"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$API_KEY_PATH"
|
||||
./ci/github_actions/upload_testflight.sh "$EAGLE0_BUILD_DIR/archive/eagle0.ipa"
|
||||
|
||||
- name: Upload IPA artifact
|
||||
# Only keep artifact if we skipped TestFlight upload (for debugging)
|
||||
|
||||
@@ -1,102 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Upload xcarchive to TestFlight using App Store Connect API Key
|
||||
# This replaces the deprecated altool which was removed in Xcode 14+
|
||||
#
|
||||
# Uses xcodebuild -exportArchive with destination=upload, which is Apple's
|
||||
# recommended approach for CI/CD pipelines.
|
||||
# Upload IPA to TestFlight using Apple ID credentials (same as Mac notarization)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
# Ensure xcodebuild uses Xcode.app, not Command Line Tools
|
||||
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
IPA_PATH=${1:?Usage: upload_testflight.sh <ipa_path>}
|
||||
|
||||
ARCHIVE_PATH=${1:?Usage: upload_testflight.sh <xcarchive_path> <team_id> <profile_uuid>}
|
||||
TEAM_ID=${2:?Missing team ID}
|
||||
PROFILE_UUID=${3:?Missing provisioning profile UUID}
|
||||
|
||||
if [ ! -d "$ARCHIVE_PATH" ]; then
|
||||
echo "Error: xcarchive not found: $ARCHIVE_PATH"
|
||||
if [ ! -f "$IPA_PATH" ]; then
|
||||
echo "Error: IPA file not found: $IPA_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Uploading to TestFlight: $ARCHIVE_PATH"
|
||||
echo "Uploading to TestFlight: $IPA_PATH"
|
||||
|
||||
# Requires App Store Connect API Key:
|
||||
# - APP_STORE_CONNECT_API_KEY_ID: Key ID from App Store Connect
|
||||
# - APP_STORE_CONNECT_API_ISSUER_ID: Issuer ID from App Store Connect
|
||||
# - APP_STORE_CONNECT_API_KEY_PATH: Path to .p8 private key file
|
||||
#
|
||||
# To create an API key:
|
||||
# 1. Go to https://appstoreconnect.apple.com/access/api
|
||||
# 2. Click the + button to create a new key
|
||||
# 3. Give it a name and Admin or App Manager access
|
||||
# 4. Download the .p8 file (you can only download it once!)
|
||||
# 5. Note the Key ID and Issuer ID shown on the page
|
||||
# Uses same credentials as Mac notarization:
|
||||
# - APPLE_ID: Your Apple ID email
|
||||
# - APP_SPECIFIC_PASSWORD: App-specific password from appleid.apple.com
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_KEY_ID:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_KEY_ID environment variable not set"
|
||||
echo "Create an API key at https://appstoreconnect.apple.com/access/api"
|
||||
if [ -z "${APPLE_ID:-}" ]; then
|
||||
echo "Error: APPLE_ID environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_ISSUER_ID:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_ISSUER_ID environment variable not set"
|
||||
if [ -z "${APP_SPECIFIC_PASSWORD:-}" ]; then
|
||||
echo "Error: APP_SPECIFIC_PASSWORD environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${APP_STORE_CONNECT_API_KEY_PATH:-}" ]; then
|
||||
echo "Error: APP_STORE_CONNECT_API_KEY_PATH environment variable not set"
|
||||
echo "Uploading with xcrun altool..."
|
||||
|
||||
# Capture output to check for errors (altool may return 0 even on failure)
|
||||
OUTPUT=$(xcrun altool --upload-app \
|
||||
--type ios \
|
||||
--file "$IPA_PATH" \
|
||||
--username "$APPLE_ID" \
|
||||
--password "$APP_SPECIFIC_PASSWORD" 2>&1) || true
|
||||
|
||||
echo "$OUTPUT"
|
||||
|
||||
# Check for error indicators in output
|
||||
if echo "$OUTPUT" | grep -q "ERROR:"; then
|
||||
echo "ERROR: Upload failed. See error messages above."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$APP_STORE_CONNECT_API_KEY_PATH" ]; then
|
||||
echo "Error: API key file not found: $APP_STORE_CONNECT_API_KEY_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create export options plist with upload destination
|
||||
EXPORT_OPTIONS_PLIST=$(mktemp)
|
||||
trap "rm -f $EXPORT_OPTIONS_PLIST" EXIT
|
||||
|
||||
cat > "$EXPORT_OPTIONS_PLIST" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>teamID</key>
|
||||
<string>$TEAM_ID</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>signingCertificate</key>
|
||||
<string>Apple Distribution: Daniel Crosby (UWJ88DX8WQ)</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>net.eagle0.eagle</key>
|
||||
<string>$PROFILE_UUID</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "Export options:"
|
||||
cat "$EXPORT_OPTIONS_PLIST"
|
||||
|
||||
echo "Uploading to App Store Connect..."
|
||||
|
||||
# Use xcodebuild to upload with App Store Connect API authentication
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
|
||||
-authenticationKeyPath "$APP_STORE_CONNECT_API_KEY_PATH" \
|
||||
-authenticationKeyID "$APP_STORE_CONNECT_API_KEY_ID" \
|
||||
-authenticationKeyIssuerID "$APP_STORE_CONNECT_API_ISSUER_ID"
|
||||
|
||||
echo "Upload complete! Check App Store Connect for processing status."
|
||||
echo "The build should appear in TestFlight within 15-30 minutes after processing."
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
# rules_apple Workspace Separation
|
||||
|
||||
This document explains why `rules_apple` is built in a separate Bazel workspace (`sparkle_workspace/`) and what conditions need to be met before it can be reintegrated into the main workspace.
|
||||
|
||||
## Background
|
||||
|
||||
The main workspace cannot include `rules_apple` due to transitive dependency conflicts with `rules_swift`. Multiple dependencies require different major versions of `rules_swift`:
|
||||
|
||||
| Dependency | rules_swift Version | Compatibility Level |
|
||||
|------------|---------------------|---------------------|
|
||||
| grpc | 3.x | 3 |
|
||||
| flatbuffers | 2.x | 2 |
|
||||
| rules_apple | 2.x | 2 |
|
||||
|
||||
Bzlmod's `single_version_override` can force a single version, but compatibility levels 2 and 3 are incompatible. Forcing `rules_swift` 3.x (required for grpc) breaks `rules_apple` and `flatbuffers`.
|
||||
|
||||
## Current Solution
|
||||
|
||||
The `SparklePlugin` (the only component requiring `rules_apple`) is built in an isolated workspace:
|
||||
|
||||
```
|
||||
sparkle_workspace/
|
||||
├── MODULE.bazel # Minimal deps: apple_support, rules_apple, sparkle
|
||||
├── BUILD.bazel # Builds SparklePlugin.bundle
|
||||
├── SparklePlugin.m # Objective-C source
|
||||
├── Info.plist # Bundle metadata
|
||||
└── external/
|
||||
└── BUILD.sparkle # Build file for Sparkle framework
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Build script**: `scripts/build_sparkle_plugin.sh` builds the plugin from the separate workspace
|
||||
2. **Output**: The built `SparklePlugin.bundle` is placed in `Assets/Plugins/macOS/`
|
||||
3. **CI integration**: `mac_build.yml` calls `inject_sparkle.sh` to embed Sparkle into the Mac app
|
||||
|
||||
The main workspace uses `single_version_override` for `rules_swift` 3.x to satisfy grpc, and includes a comment noting that `rules_apple` is intentionally excluded.
|
||||
|
||||
## What SparklePlugin Does
|
||||
|
||||
SparklePlugin is a native macOS library that:
|
||||
- Initializes the [Sparkle](https://sparkle-project.org/) auto-update framework
|
||||
- Exposes C functions for Unity to call via P/Invoke:
|
||||
- `SparklePlugin_Initialize`
|
||||
- `SparklePlugin_CheckForUpdates`
|
||||
- `SparklePlugin_CheckForUpdatesInBackground`
|
||||
- `SparklePlugin_IsCheckingForUpdates`
|
||||
- `SparklePlugin_GetAutomaticallyChecksForUpdates`
|
||||
- `SparklePlugin_SetAutomaticallyChecksForUpdates`
|
||||
- `SparklePlugin_IsRunningFromReadOnlyVolume`
|
||||
- `SparklePlugin_ShowDMGWarning`
|
||||
|
||||
## Conditions for Reintegration
|
||||
|
||||
To move `rules_apple` back into the main workspace, **all** of the following must be true:
|
||||
|
||||
### 1. rules_swift Version Alignment
|
||||
|
||||
Check if grpc, flatbuffers, and rules_apple all support the same `rules_swift` major version:
|
||||
|
||||
```bash
|
||||
# Check what rules_swift version each dependency requires
|
||||
bazel mod graph 2>&1 | grep -A2 "rules_swift"
|
||||
```
|
||||
|
||||
Or check BCR directly:
|
||||
- https://registry.bazel.build/modules/grpc
|
||||
- https://registry.bazel.build/modules/flatbuffers
|
||||
- https://registry.bazel.build/modules/rules_apple
|
||||
|
||||
**Test**: After updating versions, verify you can add this to MODULE.bazel without errors:
|
||||
```python
|
||||
bazel_dep(name = "rules_apple", version = "X.Y.Z")
|
||||
```
|
||||
|
||||
### 2. Build Test
|
||||
|
||||
If the dependency can be added, test that both the main build and SparklePlugin work:
|
||||
|
||||
```bash
|
||||
# Main workspace builds
|
||||
bazel build //src/main/scala/net/eagle0/eagle:eagle_server
|
||||
bazel build //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
bazel build //ci:eagle_server_image
|
||||
|
||||
# SparklePlugin builds (would move to main workspace)
|
||||
bazel build //sparkle:SparklePlugin # After moving files
|
||||
```
|
||||
|
||||
### 3. Files to Move
|
||||
|
||||
When reintegrating, move these from `sparkle_workspace/` to the main workspace:
|
||||
|
||||
| Source | Destination |
|
||||
|--------|-------------|
|
||||
| `sparkle_workspace/SparklePlugin.m` | `src/main/objc/net/eagle0/sparkle/SparklePlugin.m` |
|
||||
| `sparkle_workspace/Info.plist` | `src/main/objc/net/eagle0/sparkle/Info.plist` |
|
||||
| `sparkle_workspace/BUILD.bazel` | `src/main/objc/net/eagle0/sparkle/BUILD.bazel` |
|
||||
| `sparkle_workspace/external/BUILD.sparkle` | (inline into MODULE.bazel http_archive) |
|
||||
|
||||
### 4. MODULE.bazel Changes
|
||||
|
||||
Add to the main MODULE.bazel:
|
||||
|
||||
```python
|
||||
bazel_dep(name = "rules_apple", version = "X.Y.Z", repo_name = "build_bazel_rules_apple")
|
||||
|
||||
# Sparkle framework
|
||||
http_archive(
|
||||
name = "sparkle",
|
||||
build_file_content = """...""", # Contents from external/BUILD.sparkle
|
||||
sha256 = "...",
|
||||
url = "https://github.com/sparkle-project/Sparkle/releases/download/...",
|
||||
)
|
||||
```
|
||||
|
||||
Remove the `single_version_override` for `rules_swift` if no longer needed.
|
||||
|
||||
### 5. Update Build Scripts
|
||||
|
||||
- Update `scripts/build_sparkle_plugin.sh` to build from main workspace
|
||||
- Update `scripts/build_plugins.sh` if it references the separate workspace
|
||||
- Verify `mac_build.yml` still works
|
||||
|
||||
### 6. Cleanup
|
||||
|
||||
After successful integration:
|
||||
```bash
|
||||
rm -rf sparkle_workspace/
|
||||
```
|
||||
|
||||
Update comments in MODULE.bazel that reference the separate workspace.
|
||||
|
||||
## Version History
|
||||
|
||||
| Date | Event |
|
||||
|------|-------|
|
||||
| 2026-02-04 | Separated sparkle_workspace (PR #5882) to enable Bazel 8 upgrade |
|
||||
| 2026-02-05 | Upgraded to Bazel 8.5.1 (PR #5883) |
|
||||
|
||||
## Monitoring
|
||||
|
||||
Periodically check if the upstream dependencies have aligned:
|
||||
|
||||
1. **grpc releases**: https://github.com/grpc/grpc/releases
|
||||
2. **flatbuffers releases**: https://github.com/google/flatbuffers/releases
|
||||
3. **rules_apple releases**: https://github.com/bazelbuild/rules_apple/releases
|
||||
4. **BCR updates**: https://registry.bazel.build/
|
||||
|
||||
When a new version of any of these is released, check if the `rules_swift` requirements have converged.
|
||||
@@ -1,230 +0,0 @@
|
||||
# Tutorial Battle System
|
||||
|
||||
This document describes the tutorial battle system for first-time players. The system provides a scripted introductory battle that teaches combat basics while telling a story.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
When a new player starts their first game in tutorial mode, they experience:
|
||||
|
||||
1. **Narrative Intro** - Story screens introducing the scenario
|
||||
2. **Immediate Battle** - Skip strategic map, start directly in combat
|
||||
3. **Scripted Flee** - Enemy flees when player is "on the ropes"
|
||||
4. **Reinforcements** - Allied heroes arrive mid-battle
|
||||
5. **Heroes Join** - Reinforcement heroes join player's faction after battle
|
||||
|
||||
---
|
||||
|
||||
## Battle Configuration
|
||||
|
||||
### Defender (Player - John Ranil)
|
||||
- **Heroes**: John Ranil + 2 random sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 2x Light Infantry (300 troops each, 60 training/armament)
|
||||
- 1x Longbowmen (200 troops, 60 training/armament)
|
||||
- **Province**: 14 (Onmaa) with 40 support, 500 gold, 3000 food
|
||||
- **Restriction**: Cannot flee (must defend)
|
||||
|
||||
### Attacker (Ikhaan Tarn)
|
||||
- **Heroes**: Ikhaan Tarn + 2 sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 1x Heavy Cavalry (400 troops, 80 training/armament)
|
||||
- 1x Heavy Infantry (500 troops, 80 training/armament)
|
||||
- 1x Longbowmen (300 troops, 80 training/armament)
|
||||
- **Origin Province**: 32
|
||||
|
||||
### Flee Trigger
|
||||
Attacker flees when ANY of these conditions are met:
|
||||
- Player loses 1 unit
|
||||
- 5 rounds have passed
|
||||
|
||||
### Reinforcements
|
||||
When Tarn flees, these heroes arrive as player reinforcements:
|
||||
- Elena Fyar
|
||||
- Hedrick
|
||||
- The Boulder
|
||||
|
||||
---
|
||||
|
||||
## Implementation Architecture
|
||||
|
||||
### Phase 1: Narrative Intro System
|
||||
|
||||
**Proto Changes:**
|
||||
- `game_state_view.proto`: Added `TutorialNarrativeScreen` message and `pending_narrative_screens` field
|
||||
- `game_parameters.proto`: Added `TutorialNarrativeScreen` and `tutorial_narrative_screens` field
|
||||
|
||||
**Unity Client:**
|
||||
- `NarrativeScreenController.cs`: Modal screen with title, body text, optional image
|
||||
- `EagleGameController.cs`: Checks for pending narrative screens on game start
|
||||
|
||||
**Narrative Content** (defined in `tutorial_game_parameters.json`):
|
||||
```
|
||||
Screen 1: "The Engineer of Onmaa" - Player backstory
|
||||
Screen 2: "The Traitor Arrives" - Tarn's attack
|
||||
Screen 3: "Defend Your Home" - Call to action
|
||||
```
|
||||
|
||||
### Phase 2: Auto-Start Battle
|
||||
|
||||
**Configuration:**
|
||||
- `tutorial_game_parameters.json`: Contains `tutorialBattle` config with attacker, target, flee conditions
|
||||
- `game_parameters.proto`: `TutorialBattleConfig` message
|
||||
|
||||
**Game Creation:**
|
||||
- `NewGameCreation.scala`: `setupTutorialBattle()` method creates:
|
||||
- `HostileArmyGroup` with `Attacking` status (bypasses decision phase)
|
||||
- `Army` for defender
|
||||
- Both added to target province
|
||||
|
||||
**Battle Creation:**
|
||||
- When round advances to `BattleRequest` phase, `RequestBattlesAction` automatically creates the `ShardokBattle`
|
||||
|
||||
### Phase 3: Scripted Flee Mechanism
|
||||
|
||||
**C++ Component:**
|
||||
- `TutorialBattleController.hpp/cpp`: Controller for scripted battle logic
|
||||
|
||||
**Key Methods:**
|
||||
```cpp
|
||||
// Check if attacker should flee
|
||||
bool ShouldTriggerScriptedFlee(const GameStateW& state);
|
||||
|
||||
// Check if player (defender) can flee
|
||||
bool PlayerCanFlee(PlayerId playerId);
|
||||
|
||||
// Execute the scripted flee (100% success rate)
|
||||
std::vector<ActionResult> ExecuteScriptedFlee(...);
|
||||
```
|
||||
|
||||
**Integration Points (TODO):**
|
||||
- `ShardokEngine.cpp`: Check `ShouldTriggerScriptedFlee()` during turn processing
|
||||
- `FleeCommandFactory.cpp`: Disable flee commands for defender in tutorial mode
|
||||
|
||||
### Phase 4: Mid-Battle Reinforcements
|
||||
|
||||
**Action Types** (in `action_type.proto`):
|
||||
- `TUTORIAL_ENEMY_FLED = 78`
|
||||
- `TUTORIAL_REINFORCEMENTS_ARRIVED = 79`
|
||||
|
||||
**Implementation (TODO):**
|
||||
- When scripted flee triggers, create reinforcement units in defender's reserves
|
||||
- Units have `UnitStatus_RESERVE_UNIT` status
|
||||
- Reinforcements enter battle on subsequent turns
|
||||
|
||||
### Phase 5: Tutorial Popups
|
||||
|
||||
**Content Definitions** (in `TutorialContentDefinitions.cs`):
|
||||
- `tutorial_battle_started`: "Defend your province!"
|
||||
- `tutorial_enemy_fled`: "The enemy retreats!"
|
||||
- `tutorial_reinforcements_arrived`: "Allies have arrived!"
|
||||
- `tutorial_battle_victory`: Victory celebration
|
||||
|
||||
**Trigger Integration:**
|
||||
- `TutorialTriggerRegistry.cs`: Handles tutorial action types from `ActionResultView`
|
||||
- `ShardokGameController.cs`: Detects tutorial events and triggers popups
|
||||
|
||||
### Phase 6: Post-Battle Hero Joining
|
||||
|
||||
**Implementation (TODO):**
|
||||
- `ResolveBattleAction.scala`: Check if battle was tutorial and defender won
|
||||
- Create `TutorialHeroJoined` action results adding reinforcement heroes to player faction
|
||||
- Set appropriate loyalty, vigor, location for joined heroes
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
|
||||
### New Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `TutorialBattleController.hpp/cpp` | C++ scripted flee and reinforcement logic |
|
||||
| `NarrativeScreenController.cs` | Unity narrative screen display |
|
||||
|
||||
### Modified Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `tutorial_game_parameters.json` | Battle configuration and narrative content |
|
||||
| `game_state_view.proto` | Narrative screens field |
|
||||
| `game_parameters.proto` | Tutorial battle config |
|
||||
| `player_info.proto` | Tutorial battle flags for Shardok |
|
||||
| `action_type.proto` | Tutorial action types |
|
||||
| `NewGameCreation.scala` | Auto-start battle setup |
|
||||
| `EagleGameController.cs` | Narrative display integration |
|
||||
| `TutorialContentDefinitions.cs` | Battle tutorial content |
|
||||
| `TutorialTriggerRegistry.cs` | Tutorial event triggers |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Implementation
|
||||
|
||||
### ShardokEngine Integration
|
||||
Integrate `TutorialBattleController` into the Shardok battle loop:
|
||||
1. Initialize controller from battle config when battle starts
|
||||
2. Check `ShouldTriggerScriptedFlee()` at end of each round
|
||||
3. If true, call `ExecuteScriptedFlee()` and add results to action queue
|
||||
|
||||
### Disable Player Flee
|
||||
In `FleeCommandFactory.cpp`:
|
||||
- Check if tutorial mode is enabled
|
||||
- If defender, don't offer flee commands
|
||||
|
||||
### Reinforcement Units
|
||||
In `TutorialBattleController::ExecuteScriptedFlee()`:
|
||||
- Create `Unit` objects for Elena Fyar, Hedrick, The Boulder
|
||||
- Add to defender's reserve units
|
||||
- Generate `TUTORIAL_REINFORCEMENTS_ARRIVED` action
|
||||
|
||||
### Post-Battle Hero Joining
|
||||
In Eagle's battle resolution:
|
||||
- Detect tutorial battle completion
|
||||
- Create heroes in player's faction with proper stats
|
||||
- Generate notification action results
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] New user sees narrative screens before battle
|
||||
- [ ] Battle starts immediately after narratives (no strategic map)
|
||||
- [ ] Player has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Attacker has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Player cannot flee
|
||||
- [ ] Tarn flees after player loses 1 unit
|
||||
- [ ] Tarn flees after 5 rounds (if no units lost)
|
||||
- [ ] Reinforcements appear when Tarn flees
|
||||
- [ ] Tutorial popups appear at correct moments
|
||||
- [ ] Reinforcement heroes join faction after battle victory
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### tutorial_game_parameters.json
|
||||
```json
|
||||
{
|
||||
"tutorialBattle": {
|
||||
"attackerFactionHead": "Ikhaan Tarn",
|
||||
"targetProvinceId": 14,
|
||||
"fleeAfterDefenderUnitsLost": 1,
|
||||
"fleeAfterRounds": 5,
|
||||
"reinforcements": ["Elena Fyar", "Hedrick", "The Boulder"]
|
||||
},
|
||||
"tutorialNarrativeScreens": [
|
||||
{ "title": "...", "bodyText": "...", "imagePath": "" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### TutorialBattleConfig Proto (Shardok)
|
||||
```protobuf
|
||||
message TutorialBattleConfig {
|
||||
bool enabled = 1;
|
||||
int32 flee_after_defender_units_lost = 2;
|
||||
int32 flee_after_rounds = 3;
|
||||
int32 attacker_player_id = 4;
|
||||
bool defender_can_flee = 5;
|
||||
repeated string reinforcement_hero_names = 6;
|
||||
}
|
||||
```
|
||||
@@ -1,113 +0,0 @@
|
||||
# AI Quest Completion Behavior
|
||||
|
||||
This document describes which quests the AI attempts to complete proactively to recruit unaffiliated heroes.
|
||||
|
||||
## Overview
|
||||
|
||||
The AI attempts to complete quests via `FulfillQuestsCommandSelector`, which is invoked by `MidGameAIClient.chosenFulfillEasyQuestsCommand`. The AI only considers quests from unaffiliated heroes in provinces ruled by a faction leader.
|
||||
|
||||
## Quests the AI Actively Completes
|
||||
|
||||
### Diplomacy Quests
|
||||
|
||||
| Quest | Handler | Conditions |
|
||||
|-------|---------|------------|
|
||||
| `AllianceQuest` | `AllianceQuestCommandChooser` | Target faction must meet trust conditions for alliance and not already be in an alliance |
|
||||
| `TruceWithFactionQuest` | `TruceWithFactionQuestCommandChooser` | Target faction must meet trust conditions for truce |
|
||||
| `TruceCountQuest` | `TruceCountQuestCommandChooser` | Picks a random faction that meets trust conditions and isn't already in a truce/alliance |
|
||||
|
||||
### Resource Giving Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `AlmsToProvinceQuest` | `AlmsToProvinceQuestCommandChooser` | Gives food to the specified province |
|
||||
| `AlmsAcrossRealmQuest` | `AlmsAcrossRealmQuestCommandChooser` | Gives food from the province with the largest surplus |
|
||||
| `GiveToHeroesInProvinceQuest` | `GiveToHeroesInProvinceQuestCommandChooser` | Gives gold to the hero with the lowest loyalty in the specified province |
|
||||
| `GiveToHeroesAcrossRealmQuest` | `GiveToHeroesAcrossRealmQuestCommandChooser` | Gives gold from the province with the most gold available |
|
||||
|
||||
### Province Development Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `ImproveAgricultureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Agriculture type |
|
||||
| `ImproveEconomyQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Economy type |
|
||||
| `ImproveInfrastructureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Infrastructure type |
|
||||
|
||||
### Other Quests
|
||||
|
||||
| Quest | Handler | Conditions |
|
||||
|-------|---------|------------|
|
||||
| `DismissSpecificVassalQuest` | `DismissSpecificVassalCommandChooser` | Only if province has more than 2 heroes AND the unaffiliated hero's power >= target hero's power * `RequiredPowerMultiplierForDismiss` |
|
||||
|
||||
## Quests the AI Does Not Attempt to Complete
|
||||
|
||||
The following quests have no handler in `FulfillQuestsCommandSelector` and must be completed naturally through gameplay:
|
||||
|
||||
### Combat/Military Quests
|
||||
- `DefeatFactionQuest` - Defeat a specific faction
|
||||
- `GrandArmyQuest` - Accumulate a large number of troops
|
||||
- `UpgradeBattalionQuest` - Upgrade a battalion to minimum armament/training
|
||||
- `WinBattleOutnumberedQuest` - Win a battle while outnumbered
|
||||
- `WinBattlesQuest` - Win a number of battles
|
||||
- `RescueImprisonedLeaderQuest` - Rescue an imprisoned leader from another faction
|
||||
|
||||
### Expansion Quests
|
||||
- `ExpandToProvincesQuest` - Expand to control a certain number of provinces
|
||||
- `SpecificExpansionQuest` - Conquer a specific province
|
||||
- `BorderSecurityQuest` - Have troops in a border province
|
||||
|
||||
### Prisoner Quests
|
||||
- `ExecutePrisonerQuest` - Execute a specific prisoner
|
||||
- `ExilePrisonerQuest` - Exile a specific prisoner
|
||||
- `ReleasePrisonerQuest` - Release a specific prisoner
|
||||
- `ReturnPrisonerQuest` - Return a prisoner to their faction
|
||||
- `ReleaseAllPrisonersQuest` - Release all prisoners
|
||||
|
||||
### Province Order Quests
|
||||
- `DevelopProvincesQuest` - Maintain provinces in Develop order for months
|
||||
- `MobilizeProvincesQuest` - Maintain provinces in Mobilize order for months
|
||||
- `RestProvinceQuest` - Use the Rest command in a specific province
|
||||
|
||||
### Reconnaissance Quests
|
||||
- `ReconProvincesQuest` - Reconnoiter a number of provinces
|
||||
- `ReconSpecificProvincesQuest` - Reconnoiter specific provinces
|
||||
|
||||
### Economic Quests
|
||||
- `TotalDevelopmentQuest` - Achieve total development level in a province
|
||||
- `WealthQuest` - Accumulate gold and food
|
||||
- `SpendOnFeastsQuest` - Spend gold on feasts
|
||||
- `SendSuppliesQuest` - Send food to a specific province
|
||||
- `RepairDevastationQuest` - Repair devastation
|
||||
|
||||
### Special Event Quests
|
||||
- `SuppressRiotByForceQuest` - Suppress a riot by force
|
||||
- `FightBeastsAloneQuest` - Fight beasts alone
|
||||
- `StartBlizzardQuest` - Start a blizzard in a province
|
||||
- `StartEpidemicQuest` - Start an epidemic in a province
|
||||
- `ApprehendOutlawQuest` - Apprehend an outlaw hero
|
||||
|
||||
### Miscellaneous
|
||||
- `BattalionDiversityQuest` - Have diverse battalion types
|
||||
- `SwearBrotherhoodWithHeroQuest` - Swear brotherhood with a specific hero
|
||||
- `BetrayAllyQuest` - Betray an allied faction
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The quest completion logic is located in:
|
||||
- `FulfillQuestsCommandSelector.scala` - Main entry point, iterates through choosers
|
||||
- `quest_command_selectors/` - Individual quest handlers
|
||||
|
||||
Each chooser extends either:
|
||||
- `QuestCommandChooser` - For quests requiring randomness
|
||||
- `DeterministicQuestCommandChooser` - For quests with deterministic command selection
|
||||
|
||||
The AI prioritizes quests in the order they appear in `FulfillQuestsCommandSelector.choosers`:
|
||||
1. Alliance
|
||||
2. TruceWithFaction
|
||||
3. Improve (Agriculture/Economy/Infrastructure)
|
||||
4. AlmsToProvince
|
||||
5. GiveToHeroesInProvince
|
||||
6. AlmsAcrossRealm
|
||||
7. GiveToHeroesAcrossRealm
|
||||
8. TruceCount
|
||||
9. DismissSpecificVassal
|
||||
@@ -22,31 +22,11 @@ NC='\033[0m' # No Color
|
||||
MODE="${1:-check}"
|
||||
EXIT_CODE=0
|
||||
|
||||
# Cache for expensive bazel query results
|
||||
LIBRARY_DEPS_CACHE=""
|
||||
MAIN_DEPS_CACHE=""
|
||||
|
||||
# Get deps of library/ (cached)
|
||||
get_library_deps() {
|
||||
if [ -z "$LIBRARY_DEPS_CACHE" ]; then
|
||||
LIBRARY_DEPS_CACHE=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...)' 2>/dev/null || true)
|
||||
fi
|
||||
echo "$LIBRARY_DEPS_CACHE"
|
||||
}
|
||||
|
||||
# Get deps of src/main (cached)
|
||||
get_main_deps() {
|
||||
if [ -z "$MAIN_DEPS_CACHE" ]; then
|
||||
MAIN_DEPS_CACHE=$(bazel query 'deps(//src/main/...)' 2>/dev/null || true)
|
||||
fi
|
||||
echo "$MAIN_DEPS_CACHE"
|
||||
}
|
||||
|
||||
# Rule 1: src/main should not depend on src/test
|
||||
check_main_depends_on_test() {
|
||||
echo -e "${YELLOW}Checking: src/main should not depend on src/test...${NC}"
|
||||
|
||||
violations=$(get_main_deps | grep "^//src/test/" || true)
|
||||
violations=$(bazel query 'deps(//src/main/...) intersect //src/test/...' 2>/dev/null || true)
|
||||
|
||||
if [ -n "$violations" ]; then
|
||||
echo -e "${RED}VIOLATION: src/main depends on src/test:${NC}"
|
||||
@@ -63,7 +43,7 @@ check_main_depends_on_test() {
|
||||
check_library_depends_on_scala_proto() {
|
||||
echo -e "${YELLOW}Checking: library/ should not depend on Scala proto types...${NC}"
|
||||
|
||||
violations=$(get_library_deps | grep "^//src/main/protobuf/.*_scala_proto$" || true)
|
||||
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
|
||||
if [ -z "$violations" ]; then
|
||||
count=0
|
||||
else
|
||||
@@ -85,7 +65,7 @@ check_library_depends_on_scala_proto() {
|
||||
check_library_depends_on_proto_converters() {
|
||||
echo -e "${YELLOW}Checking: library/ should not depend on proto_converters...${NC}"
|
||||
|
||||
violations=$(get_library_deps | grep "^//src/main/scala/net/eagle0/eagle/model/proto_converters/" || true)
|
||||
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/scala/net/eagle0/eagle/model/proto_converters/...' 2>/dev/null | grep "^//" || true)
|
||||
|
||||
if [ -n "$violations" ]; then
|
||||
count=$(echo "$violations" | wc -l | tr -d ' ')
|
||||
@@ -105,8 +85,7 @@ check_library_depends_on_proto_converters() {
|
||||
count_proto_deps() {
|
||||
echo -e "${YELLOW}=== Proto dependency counts ===${NC}"
|
||||
|
||||
library_deps=$(get_library_deps)
|
||||
scala_proto_results=$(echo "$library_deps" | grep "^//src/main/protobuf/.*_scala_proto$" || true)
|
||||
scala_proto_results=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
|
||||
if [ -z "$scala_proto_results" ]; then
|
||||
scala_proto_count=0
|
||||
else
|
||||
@@ -115,7 +94,7 @@ count_proto_deps() {
|
||||
echo "library/ Scala proto deps: $scala_proto_count"
|
||||
|
||||
# C++/Go proto deps are expected (map generation tools)
|
||||
all_proto_count=$(echo "$library_deps" | grep -c "^//src/main/protobuf/" || echo "0")
|
||||
all_proto_count=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo "library/ all proto deps (includes C++/Go build tools): $all_proto_count"
|
||||
}
|
||||
|
||||
|
||||
+8
-29
@@ -806,9 +806,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-create and launch a tutorial game for first-time users.
|
||||
/// Tutorial mode: 3 factions (player, King, weak rival), player spawns at province 14 with
|
||||
/// quest.
|
||||
/// Auto-create and launch a 7-faction game for first-time users.
|
||||
/// </summary>
|
||||
private void AutoCreateFirstGame() {
|
||||
if (fetchedNewGameLeaders == null || fetchedNewGameLeaders.Count == 0) {
|
||||
@@ -820,9 +818,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
var randomIndex = new System.Random().Next(fetchedNewGameLeaders.Count);
|
||||
var leaderTextId = fetchedNewGameLeaders[randomIndex].NameTextId;
|
||||
|
||||
// Create a tutorial game (1 human, 3 total factions - player, King, and weak rival)
|
||||
Debug.Log($"[ConnectionHandler] Auto-creating tutorial game with leader {leaderTextId}");
|
||||
CreateGame(leaderTextId, 1, 3, isTutorialMode: true);
|
||||
// Create a 7-faction game (1 human + 6 AI) - same as lobby default
|
||||
Debug.Log($"[ConnectionHandler] Auto-creating 7-faction game with leader {leaderTextId}");
|
||||
CreateGame(leaderTextId, 1, 7);
|
||||
}
|
||||
|
||||
private void StartListeningForLobbyUpdates() {
|
||||
@@ -948,25 +946,10 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
|
||||
private void ConfirmDropGame(long gameId) { _internalDropGame(gameId); }
|
||||
|
||||
/// <summary>
|
||||
/// 3-parameter overload for CreateCallback delegate compatibility.
|
||||
/// </summary>
|
||||
private void CreateGame(string leaderNameTextId, int humanPlayerCount, int totalPlayerCount) {
|
||||
CreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount, isTutorialMode: false);
|
||||
}
|
||||
|
||||
private void CreateGame(
|
||||
string leaderNameTextId,
|
||||
int humanPlayerCount,
|
||||
int totalPlayerCount,
|
||||
bool isTutorialMode) {
|
||||
// Track multiplayer status for tutorial system
|
||||
TutorialManager.IsMultiplayerGame = humanPlayerCount > 1;
|
||||
var game = _internalCreateGame(
|
||||
leaderNameTextId,
|
||||
humanPlayerCount,
|
||||
totalPlayerCount,
|
||||
isTutorialMode);
|
||||
var game = _internalCreateGame(leaderNameTextId, humanPlayerCount, totalPlayerCount);
|
||||
}
|
||||
|
||||
public void QuitButtonClicked() { Application.Quit(); }
|
||||
@@ -1005,11 +988,8 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
} catch (Exception e) { Console.WriteLine(e.ToString()); }
|
||||
}
|
||||
|
||||
private async Task<bool> _internalCreateGame(
|
||||
string desiredLeaderTextId,
|
||||
int humanPlayerCount,
|
||||
int totalPlayerCount,
|
||||
bool isTutorialMode = false) {
|
||||
private async Task<bool>
|
||||
_internalCreateGame(string desiredLeaderTextId, int humanPlayerCount, int totalPlayerCount) {
|
||||
try {
|
||||
return await _persistentClientConnection.SendUpdateStreamRequestAsync(
|
||||
new UpdateStreamRequest {
|
||||
@@ -1017,8 +997,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
|
||||
new CreateGameRequest {
|
||||
DesiredLeaderTextId = desiredLeaderTextId,
|
||||
TotalPlayerCount = totalPlayerCount,
|
||||
HumanPlayerCount = humanPlayerCount,
|
||||
IsTutorialMode = isTutorialMode
|
||||
HumanPlayerCount = humanPlayerCount
|
||||
}
|
||||
});
|
||||
} catch (WebException e) {
|
||||
|
||||
+30
-26
@@ -45,9 +45,14 @@ namespace eagle {
|
||||
public Toggle demandTributeToggle;
|
||||
public Toggle withdrawToggle;
|
||||
public Toggle safePassageToggle;
|
||||
public ToggleGroup decisionToggleGroup;
|
||||
|
||||
public GameObject tributeContainer;
|
||||
public GameObject advanceImage;
|
||||
public GameObject demandTributeImage;
|
||||
public GameObject withdrawImage;
|
||||
public GameObject safePassageImage;
|
||||
|
||||
public Image goldIcon;
|
||||
public Image foodIcon;
|
||||
public Slider goldSlider;
|
||||
public Slider foodSlider;
|
||||
public TMP_Text goldLabel;
|
||||
@@ -135,10 +140,21 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureToggle(advanceToggle, enableAttack);
|
||||
ConfigureToggle(withdrawToggle, enableWithdraw);
|
||||
ConfigureToggle(demandTributeToggle, enableDemandTribute);
|
||||
ConfigureToggle(safePassageToggle, enableSafePassage);
|
||||
advanceToggle.enabled = enableAttack;
|
||||
advanceToggle.gameObject.SetActive(enableAttack);
|
||||
advanceImage.SetActive(enableAttack);
|
||||
|
||||
withdrawToggle.enabled = enableWithdraw;
|
||||
withdrawToggle.gameObject.SetActive(enableWithdraw);
|
||||
withdrawImage.SetActive(enableWithdraw);
|
||||
|
||||
demandTributeToggle.enabled = enableDemandTribute;
|
||||
demandTributeToggle.gameObject.SetActive(enableDemandTribute);
|
||||
demandTributeImage.SetActive(enableDemandTribute);
|
||||
|
||||
safePassageToggle.enabled = enableSafePassage;
|
||||
safePassageToggle.gameObject.SetActive(enableSafePassage);
|
||||
safePassageImage.SetActive(enableSafePassage);
|
||||
|
||||
goldSlider.minValue = 0;
|
||||
goldSlider.maxValue = maxGoldTribute;
|
||||
@@ -186,7 +202,14 @@ namespace eagle {
|
||||
foodLabel.text = ((int)newValue).ToString();
|
||||
}
|
||||
|
||||
private void EnableSliders(bool newEnabled) { tributeContainer.SetActive(newEnabled); }
|
||||
private void EnableSliders(bool newEnabled) {
|
||||
goldIcon.gameObject.SetActive(newEnabled);
|
||||
foodIcon.gameObject.SetActive(newEnabled);
|
||||
goldSlider.gameObject.SetActive(newEnabled);
|
||||
foodSlider.gameObject.SetActive(newEnabled);
|
||||
goldLabel.gameObject.SetActive(newEnabled);
|
||||
foodLabel.gameObject.SetActive(newEnabled);
|
||||
}
|
||||
|
||||
public void ToggleChanged(bool value) {
|
||||
bool enableSliders =
|
||||
@@ -195,24 +218,5 @@ namespace eagle {
|
||||
|
||||
EnableSliders(enableSliders);
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
|
||||
private void ConfigureToggle(Toggle toggle, bool available) {
|
||||
if (toggle == null) return;
|
||||
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
|
||||
if (!available) { toggle.isOn = false; }
|
||||
|
||||
toggle.group = available ? decisionToggleGroup : null;
|
||||
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-26
@@ -23,7 +23,6 @@ namespace eagle {
|
||||
|
||||
public Toggle AdvanceToggle;
|
||||
public Toggle WithdrawToggle;
|
||||
public ToggleGroup decisionToggleGroup;
|
||||
|
||||
private FreeForAllDecisionAvailableCommand FreeForAllDecisionCommand =>
|
||||
_availableCommand.FreeForAllDecisionCommand;
|
||||
@@ -64,32 +63,10 @@ namespace eagle {
|
||||
row.Origin = _model.Provinces[army.OriginProvinceId].Name;
|
||||
});
|
||||
|
||||
bool withdrawAvailable = FreeForAllDecisionCommand.AvailableDecisions.Any(
|
||||
x => x.SealedValueCase == AttackDecisionType.SealedValueOneofCase.Withdraw);
|
||||
|
||||
ConfigureToggle(AdvanceToggle, true);
|
||||
ConfigureToggle(WithdrawToggle, withdrawAvailable);
|
||||
|
||||
AdvanceToggle.isOn = true;
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
|
||||
private void ConfigureToggle(Toggle toggle, bool available) {
|
||||
if (toggle == null) return;
|
||||
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
|
||||
if (!available) { toggle.isOn = false; }
|
||||
|
||||
toggle.group = available ? decisionToggleGroup : null;
|
||||
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
AdvanceToggle.enabled = true;
|
||||
WithdrawToggle.enabled = FreeForAllDecisionCommand.AvailableDecisions.Any(
|
||||
x => x.SealedValueCase == AttackDecisionType.SealedValueOneofCase.Withdraw);
|
||||
}
|
||||
|
||||
// Use this for initialization
|
||||
|
||||
+42
-40
@@ -15,7 +15,12 @@ namespace eagle {
|
||||
public Toggle ExileToggle;
|
||||
public Toggle ExecuteToggle;
|
||||
public Toggle ReturnToggle;
|
||||
public ToggleGroup decisionToggleGroup;
|
||||
|
||||
public RawImage RecruitIcon;
|
||||
public RawImage ImprisonIcon;
|
||||
public RawImage ExileIcon;
|
||||
public RawImage ExecuteIcon;
|
||||
public RawImage ReturnIcon;
|
||||
|
||||
public HeroDetailsController heroDetails;
|
||||
|
||||
@@ -90,54 +95,51 @@ namespace eagle {
|
||||
|
||||
messageUpdater.TextId = SelectedHeroWithOptions.MessageId;
|
||||
|
||||
var options = SelectedHeroWithOptions.Options;
|
||||
ConfigureToggle(
|
||||
RecruitToggle,
|
||||
options.Contains(CapturedHeroOption.RecruitCapturedHeroOption));
|
||||
ConfigureToggle(
|
||||
ImprisonToggle,
|
||||
options.Contains(CapturedHeroOption.ImprisonCapturedHeroOption));
|
||||
ConfigureToggle(
|
||||
ExileToggle,
|
||||
options.Contains(CapturedHeroOption.ExileCapturedHeroOption));
|
||||
ConfigureToggle(
|
||||
// The order here is important: the last available toggle in this list
|
||||
// will be on by default
|
||||
ConditionallyEnable(
|
||||
ExecuteToggle,
|
||||
options.Contains(CapturedHeroOption.ExecuteCapturedHeroOption));
|
||||
ConfigureToggle(
|
||||
ExecuteIcon,
|
||||
CapturedHeroOption.ExecuteCapturedHeroOption);
|
||||
ConditionallyEnable(ExileToggle, ExileIcon, CapturedHeroOption.ExileCapturedHeroOption);
|
||||
ConditionallyEnable(
|
||||
ReturnToggle,
|
||||
options.Contains(CapturedHeroOption.ReturnCapturedHeroOption));
|
||||
|
||||
// Select first available option (priority order: Recruit > Imprison > Exile > Execute >
|
||||
// Return)
|
||||
if (options.Contains(CapturedHeroOption.RecruitCapturedHeroOption))
|
||||
RecruitToggle.isOn = true;
|
||||
else if (options.Contains(CapturedHeroOption.ImprisonCapturedHeroOption))
|
||||
ImprisonToggle.isOn = true;
|
||||
else if (options.Contains(CapturedHeroOption.ExileCapturedHeroOption))
|
||||
ExileToggle.isOn = true;
|
||||
else if (options.Contains(CapturedHeroOption.ExecuteCapturedHeroOption))
|
||||
ExecuteToggle.isOn = true;
|
||||
else if (options.Contains(CapturedHeroOption.ReturnCapturedHeroOption))
|
||||
ReturnToggle.isOn = true;
|
||||
ReturnIcon,
|
||||
CapturedHeroOption.ReturnCapturedHeroOption);
|
||||
ConditionallyEnable(
|
||||
ImprisonToggle,
|
||||
ImprisonIcon,
|
||||
CapturedHeroOption.ImprisonCapturedHeroOption);
|
||||
ConditionallyEnable(
|
||||
RecruitToggle,
|
||||
RecruitIcon,
|
||||
CapturedHeroOption.RecruitCapturedHeroOption);
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
private bool ConditionallyEnable(Toggle toggle, CapturedHeroOption option) {
|
||||
bool enable = SelectedHeroWithOptions.Options.Contains(option);
|
||||
toggle.gameObject.SetActive(enable);
|
||||
|
||||
private void ConfigureToggle(Toggle toggle, bool available) {
|
||||
if (toggle == null) return;
|
||||
if (enable) {
|
||||
ExecuteToggle.isOn = false;
|
||||
ExileToggle.isOn = false;
|
||||
ImprisonToggle.isOn = false;
|
||||
RecruitToggle.isOn = false;
|
||||
ReturnToggle.isOn = false;
|
||||
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
toggle.isOn = true;
|
||||
} else
|
||||
toggle.isOn = false;
|
||||
|
||||
if (!available) { toggle.isOn = false; }
|
||||
return enable;
|
||||
}
|
||||
|
||||
toggle.group = available ? decisionToggleGroup : null;
|
||||
private void ConditionallyEnable(Toggle toggle, RawImage img, CapturedHeroOption option) {
|
||||
bool enable = ConditionallyEnable(toggle, option);
|
||||
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
Color color = img.color;
|
||||
color.a = enable ? 1.0f : 0.25f;
|
||||
img.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-77
@@ -21,7 +21,12 @@ namespace eagle {
|
||||
public Toggle ExileToggle;
|
||||
public Toggle ExecuteToggle;
|
||||
public Toggle ReturnToggle;
|
||||
public ToggleGroup decisionToggleGroup;
|
||||
|
||||
public RawImage ReleaseIcon;
|
||||
public RawImage MoveIcon;
|
||||
public RawImage ExileIcon;
|
||||
public RawImage ExecuteIcon;
|
||||
public RawImage ReturnIcon;
|
||||
|
||||
public TMP_Dropdown moveToDropdown;
|
||||
|
||||
@@ -71,33 +76,6 @@ namespace eagle {
|
||||
public override string CommitWarningText =>
|
||||
$"This will create a treaty with {_model.FactionName(SelectedHero.FactionId.Value)}";
|
||||
|
||||
private List<PrisonerManagementOption> MoveOptions =>
|
||||
SelectedPrisonerToManage.AvailableOptions
|
||||
.Where(opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Move)
|
||||
.ToList();
|
||||
|
||||
protected override ProvinceId? TargetedProvince {
|
||||
get {
|
||||
if (MoveToggle.isOn && moveToDropdown.value >= 0 &&
|
||||
moveToDropdown.value < MoveOptions.Count) {
|
||||
return MoveOptions[moveToDropdown.value].Move.ToProvinceId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set {
|
||||
if (!value.HasValue) return;
|
||||
|
||||
var moveOptions = MoveOptions;
|
||||
int index = moveOptions.FindIndex(opt => opt.Move.ToProvinceId == value.Value);
|
||||
if (index != -1) {
|
||||
MoveToggle.isOn = true;
|
||||
moveToDropdown.value = index;
|
||||
moveToDropdown.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PrisonerManagementOption SelectedOption {
|
||||
get {
|
||||
if (ExecuteToggle.isOn)
|
||||
@@ -165,43 +143,28 @@ namespace eagle {
|
||||
|
||||
MessageText.text = GetQuestMessagesForPrisoner(SelectedHero.Id);
|
||||
|
||||
var options = SelectedPrisonerToManage.AvailableOptions;
|
||||
bool hasRelease = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Release);
|
||||
bool hasMove = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Move);
|
||||
bool hasExile = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Exile);
|
||||
bool hasExecute = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Execute);
|
||||
bool hasReturn = options.Any(
|
||||
opt => opt.SealedValueCase ==
|
||||
PrisonerManagementOption.SealedValueOneofCase.Return);
|
||||
|
||||
ConfigureToggle(ReleaseToggle, hasRelease);
|
||||
ConfigureToggle(MoveToggle, hasMove);
|
||||
ConfigureToggle(ExileToggle, hasExile);
|
||||
ConfigureToggle(ExecuteToggle, hasExecute);
|
||||
ConfigureToggle(ReturnToggle, hasReturn);
|
||||
|
||||
// Select first available option (priority order: Release > Move > Exile > Execute >
|
||||
// Return)
|
||||
if (hasRelease) ReleaseToggle.isOn = true;
|
||||
else if (hasMove)
|
||||
MoveToggle.isOn = true;
|
||||
else if (hasExile)
|
||||
ExileToggle.isOn = true;
|
||||
else if (hasExecute)
|
||||
ExecuteToggle.isOn = true;
|
||||
else if (hasReturn)
|
||||
ReturnToggle.isOn = true;
|
||||
|
||||
// Update dropdown visibility based on selected toggle
|
||||
moveToDropdown.gameObject.SetActive(MoveToggle.isOn);
|
||||
// The order here is important: the last available toggle in this list
|
||||
// will be on by default
|
||||
ConditionallyEnable(
|
||||
ExecuteToggle,
|
||||
ExecuteIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Execute);
|
||||
ConditionallyEnable(
|
||||
ExileToggle,
|
||||
ExileIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Exile);
|
||||
ConditionallyEnable(
|
||||
ReturnToggle,
|
||||
ReturnIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Return);
|
||||
ConditionallyEnable(
|
||||
MoveToggle,
|
||||
MoveIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Move);
|
||||
ConditionallyEnable(
|
||||
ReleaseToggle,
|
||||
ReleaseIcon,
|
||||
PrisonerManagementOption.SealedValueOneofCase.Release);
|
||||
}
|
||||
|
||||
private string GetQuestMessagesForPrisoner(HeroId prisonerId) {
|
||||
@@ -262,23 +225,36 @@ namespace eagle {
|
||||
: "A free hero";
|
||||
}
|
||||
|
||||
private const float DisabledAlpha = 0.35f;
|
||||
private bool ConditionallyEnable(
|
||||
Toggle toggle,
|
||||
PrisonerManagementOption.SealedValueOneofCase option) {
|
||||
bool enable = SelectedPrisonerToManage.AvailableOptions.Any(
|
||||
opt => opt.SealedValueCase == option);
|
||||
toggle.gameObject.SetActive(enable);
|
||||
|
||||
private void ConfigureToggle(Toggle toggle, bool available) {
|
||||
if (toggle == null) return;
|
||||
if (enable) {
|
||||
ExecuteToggle.isOn = false;
|
||||
ExileToggle.isOn = false;
|
||||
MoveToggle.isOn = false;
|
||||
ReleaseToggle.isOn = false;
|
||||
ReturnToggle.isOn = false;
|
||||
|
||||
toggle.gameObject.SetActive(true);
|
||||
toggle.interactable = available;
|
||||
toggle.isOn = true;
|
||||
} else
|
||||
toggle.isOn = false;
|
||||
|
||||
if (!available) { toggle.isOn = false; }
|
||||
return enable;
|
||||
}
|
||||
|
||||
toggle.group = available ? decisionToggleGroup : null;
|
||||
private void ConditionallyEnable(
|
||||
Toggle toggle,
|
||||
RawImage img,
|
||||
PrisonerManagementOption.SealedValueOneofCase option) {
|
||||
bool enable = ConditionallyEnable(toggle, option);
|
||||
|
||||
var canvasGroup = toggle.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null) {
|
||||
canvasGroup = toggle.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
canvasGroup.alpha = available ? 1f : DisabledAlpha;
|
||||
Color color = img.color;
|
||||
color.a = enable ? 1.0f : 0.25f;
|
||||
img.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-37
@@ -77,10 +77,6 @@ namespace eagle {
|
||||
public Button gameIdButton;
|
||||
public Button nextActiveProvinceButton;
|
||||
|
||||
[Header("Tutorial Narrative")]
|
||||
public NarrativeScreenController narrativeScreenController;
|
||||
private bool _narrativeScreensShown = false;
|
||||
|
||||
[Header("Commands")]
|
||||
public MouseHandler mouseHandlerPrefab;
|
||||
public GameObject commandPanel;
|
||||
@@ -528,29 +524,6 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows tutorial narrative screens before gameplay.
|
||||
/// After all screens are dismissed, proceeds to battle if one is waiting.
|
||||
/// </summary>
|
||||
private void ShowNarrativeScreens(IList<TutorialNarrativeScreen> screens) {
|
||||
if (narrativeScreenController == null) {
|
||||
Debug.LogWarning("EagleGameController: NarrativeScreenController not assigned");
|
||||
OnNarrativeComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
narrativeScreenController.Show(screens, OnNarrativeComplete);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when narrative screens are complete.
|
||||
/// Proceeds to tutorial battle or normal game flow.
|
||||
/// </summary>
|
||||
private void OnNarrativeComplete() {
|
||||
// If there's a battle waiting, go directly to it
|
||||
if (Model != null && Model.RunningShardokGameModels.Count > 0) { GoToBattle(); }
|
||||
}
|
||||
|
||||
void SetHeaderString() {
|
||||
var date = Model.CurrentDate;
|
||||
if (date != null) {
|
||||
@@ -610,18 +583,9 @@ namespace eagle {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for tutorial narrative screens on first model load
|
||||
if (oldModel == null && !_narrativeScreensShown &&
|
||||
Model.PendingNarrativeScreens != null && Model.PendingNarrativeScreens.Count > 0) {
|
||||
_narrativeScreensShown = true;
|
||||
ShowNarrativeScreens(Model.PendingNarrativeScreens);
|
||||
return; // Wait for narrative to complete before proceeding
|
||||
}
|
||||
|
||||
// Start onboarding after first model update (UI panels are now populated)
|
||||
// Skip onboarding if we had narrative screens (tutorial battle mode)
|
||||
if (oldModel == null && TutorialManager.Instance != null &&
|
||||
!TutorialManager.Instance.State.OnboardingCompleted && !_narrativeScreensShown) {
|
||||
!TutorialManager.Instance.State.OnboardingCompleted) {
|
||||
TutorialManager.Instance.StartOnboarding();
|
||||
}
|
||||
|
||||
|
||||
@@ -59,11 +59,6 @@ namespace eagle {
|
||||
public List<ProvinceId> ProvincesForFaction(FactionId factionId);
|
||||
|
||||
public List<ChronicleEntry> ChronicleEntries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Narrative screens to display before gameplay (tutorial intro).
|
||||
/// </summary>
|
||||
public IList<TutorialNarrativeScreen> PendingNarrativeScreens { get; }
|
||||
}
|
||||
|
||||
public class GameModelUpdater : IClientConnectionSubscriber, IGameStateProvider {
|
||||
@@ -206,9 +201,6 @@ namespace eagle {
|
||||
|
||||
public List<ChronicleEntry> ChronicleEntries { get; set; }
|
||||
|
||||
public IList<TutorialNarrativeScreen> PendingNarrativeScreens =>
|
||||
GsView.PendingNarrativeScreens;
|
||||
|
||||
public List<AvailableCommand> AvailableCommandsForProvince(ProvinceId pid) {
|
||||
if (AvailableCommandsByProvince.TryGetValue(pid, out var cmds)) {
|
||||
return cmds.Commands.ToList();
|
||||
|
||||
+1
-24
@@ -9,11 +9,6 @@ namespace eagle {
|
||||
public class FreeHeroesTableController : MonoBehaviour {
|
||||
public GameObject panel;
|
||||
|
||||
[Header("Hero Backstory Popup")]
|
||||
public GameObject popupPanel;
|
||||
public HeroDetailsController popupPanelDetailsController;
|
||||
public GeneratedTextUpdater popupPanelBackstory;
|
||||
|
||||
private ProvinceView _currentProvince;
|
||||
public ProvinceView CurrentProvince {
|
||||
get => _currentProvince;
|
||||
@@ -78,21 +73,6 @@ namespace eagle {
|
||||
UpdateUnaffiliatedHeroSelections();
|
||||
}
|
||||
|
||||
public void LongHoverRowChanged(int? newIndex) {
|
||||
if (popupPanel == null) return;
|
||||
|
||||
if (newIndex is {} index) {
|
||||
var heroId = UnaffiliatedHeroes.ElementAt(index).HeroId;
|
||||
if (Model.Heroes.TryGetValue(heroId, out var hero)) {
|
||||
popupPanelDetailsController.SetHero(hero, Model);
|
||||
popupPanelBackstory.TextId = hero.BackstoryTextId;
|
||||
popupPanel.SetActive(true);
|
||||
}
|
||||
} else {
|
||||
popupPanel.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setUpUnaffiliatedHeroesTable() {
|
||||
unaffiliatedHeroesTable.RowCount = UnaffiliatedHeroCount;
|
||||
|
||||
@@ -108,10 +88,7 @@ namespace eagle {
|
||||
});
|
||||
}
|
||||
|
||||
void Start() {
|
||||
panel.SetActive(Model != null);
|
||||
if (popupPanel != null) { popupPanel.SetActive(false); }
|
||||
}
|
||||
void Start() { panel.SetActive(Model != null); }
|
||||
|
||||
private bool HasProvinceInfo =>
|
||||
(CurrentProvince != null && CurrentProvince.FullInfo != null);
|
||||
|
||||
+37
-60
@@ -33,6 +33,7 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 1571934834221197146}
|
||||
m_Father: {fileID: 13469637781584011}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -106,23 +107,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 0
|
||||
m_enableWordWrapping: 0
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -192,6 +190,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4705461973370478516}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -285,6 +284,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 3691368921260708877}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -379,6 +379,7 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 409974592681485098}
|
||||
m_Father: {fileID: 13469637781584011}
|
||||
m_RootOrder: 5
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -452,23 +453,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 0
|
||||
m_enableWordWrapping: 0
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -539,6 +537,7 @@ RectTransform:
|
||||
- {fileID: 8905938057092819695}
|
||||
- {fileID: 4705461973370478516}
|
||||
m_Father: {fileID: 6123654153464171526}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -624,6 +623,7 @@ RectTransform:
|
||||
- {fileID: 6100877526870815819}
|
||||
- {fileID: 5909934266303401356}
|
||||
m_Father: {fileID: 3691368921260708877}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -687,7 +687,7 @@ GameObject:
|
||||
- component: {fileID: 1571934834221197146}
|
||||
- component: {fileID: 2314867006400566416}
|
||||
m_Layer: 0
|
||||
m_Name: 'TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]'
|
||||
m_Name: TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -706,6 +706,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 3080737000364722689}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -753,6 +754,7 @@ RectTransform:
|
||||
- {fileID: 13469637781584011}
|
||||
- {fileID: 6123654154484763561}
|
||||
m_Father: {fileID: 6123654153464171526}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -816,7 +818,7 @@ GameObject:
|
||||
- component: {fileID: 7991069078033516274}
|
||||
- component: {fileID: 3709131301875944532}
|
||||
m_Layer: 0
|
||||
m_Name: 'TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]'
|
||||
m_Name: TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -835,6 +837,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1392924472004276419}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -860,7 +863,7 @@ GameObject:
|
||||
- component: {fileID: 2443631178969516575}
|
||||
- component: {fileID: 8616701387306228446}
|
||||
m_Layer: 0
|
||||
m_Name: 'TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]'
|
||||
m_Name: TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -879,6 +882,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4523044671415516144}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -904,7 +908,7 @@ GameObject:
|
||||
- component: {fileID: 7240827529169758877}
|
||||
- component: {fileID: 4768305797197202640}
|
||||
m_Layer: 0
|
||||
m_Name: 'TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]'
|
||||
m_Name: TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -923,6 +927,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 107607736864462239}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -970,6 +975,7 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 7240827529169758877}
|
||||
m_Father: {fileID: 13469637781584011}
|
||||
m_RootOrder: 3
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -1043,23 +1049,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 0
|
||||
m_enableWordWrapping: 0
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -1112,7 +1115,6 @@ GameObject:
|
||||
- component: {fileID: 7083373564964204245}
|
||||
- component: {fileID: 1611950019350954987}
|
||||
- component: {fileID: 7571393407091744620}
|
||||
- component: {fileID: 839825112791048530}
|
||||
m_Layer: 0
|
||||
m_Name: Unaffiliated Hero Row Prefab
|
||||
m_TagString: Untagged
|
||||
@@ -1135,6 +1137,7 @@ RectTransform:
|
||||
- {fileID: 3691368921260708877}
|
||||
- {fileID: 7084089121705905319}
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
@@ -1221,6 +1224,7 @@ MonoBehaviour:
|
||||
conLabel: {fileID: 663501340024978645}
|
||||
archeryImage: {fileID: 6373503588455539415}
|
||||
fireImage: {fileID: 7060661334717899661}
|
||||
extinguishImage: {fileID: 0}
|
||||
--- !u!114 &1611950019350954987
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1279,30 +1283,6 @@ MonoBehaviour:
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!114 &839825112791048530
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6123654153464171525}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 73953ac09da84b2d96cb90a2ab37ed79, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TableRowHoverDetector
|
||||
onHoverBegin:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onHoverEnd:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onLongHoverBegin:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
onLongHoverEnd:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
--- !u!1 &6123654153502973338
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1336,6 +1316,7 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 6123654154611511663}
|
||||
m_Father: {fileID: 13469637781584011}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -1409,23 +1390,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -1474,7 +1452,7 @@ GameObject:
|
||||
- component: {fileID: 6123654154152031040}
|
||||
- component: {fileID: 6123654154152031042}
|
||||
m_Layer: 0
|
||||
m_Name: 'TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]'
|
||||
m_Name: TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -1493,6 +1471,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6123654154484763561}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -1539,6 +1518,7 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 6123654154152031040}
|
||||
m_Father: {fileID: 7084089121705905319}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -1613,23 +1593,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 25
|
||||
m_TextWrappingMode: 1
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -1658,7 +1635,7 @@ GameObject:
|
||||
- component: {fileID: 6123654154611511663}
|
||||
- component: {fileID: 6123654154611511657}
|
||||
m_Layer: 0
|
||||
m_Name: 'TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]'
|
||||
m_Name: TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -1677,6 +1654,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6123654153502973339}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -1724,6 +1702,7 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 7991069078033516274}
|
||||
m_Father: {fileID: 13469637781584011}
|
||||
m_RootOrder: 2
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -1797,23 +1776,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 0
|
||||
m_enableWordWrapping: 0
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -1884,6 +1860,7 @@ RectTransform:
|
||||
m_Children:
|
||||
- {fileID: 2443631178969516575}
|
||||
m_Father: {fileID: 13469637781584011}
|
||||
m_RootOrder: 4
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -1957,23 +1934,20 @@ MonoBehaviour:
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 0
|
||||
m_enableWordWrapping: 0
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
@@ -2022,7 +1996,7 @@ GameObject:
|
||||
- component: {fileID: 409974592681485098}
|
||||
- component: {fileID: 3950145718335139402}
|
||||
m_Layer: 0
|
||||
m_Name: 'TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]'
|
||||
m_Name: TMP SubMeshUI [Irkel Atlas Material + LiberationSans SDF Atlas]
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -2041,6 +2015,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6859780550973135875}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
@@ -2087,6 +2062,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4705461973370478516}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
@@ -2185,6 +2161,7 @@ RectTransform:
|
||||
- {fileID: 4523044671415516144}
|
||||
- {fileID: 6859780550973135875}
|
||||
m_Father: {fileID: 7084089121705905319}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
|
||||
+6
-22
@@ -345,34 +345,18 @@ namespace eagle {
|
||||
var details = quest.Details.DevelopProvincesQuest;
|
||||
var targetMonths = quest.ComponentCount;
|
||||
var currentMonths = quest.ComponentsFulfilled;
|
||||
var currentProvinces =
|
||||
model.PlayerId.HasValue
|
||||
? model.ProvincesForFaction(model.PlayerId.Value)
|
||||
.Count(pid => model.Provinces[pid]
|
||||
.FullInfo?.ProvinceOrders ==
|
||||
ProvinceOrderType.Develop)
|
||||
: 0;
|
||||
var statusText = currentProvinces >= details.TargetProvinceCount
|
||||
? "currently on target"
|
||||
: "not currently meeting";
|
||||
return $"Maintain {details.TargetProvinceCount} provinces with Develop orders for {targetMonths} months ({currentMonths}/{targetMonths} months, {statusText})";
|
||||
var progressText =
|
||||
currentMonths > 0 ? $" ({currentMonths}/{targetMonths} months)" : "";
|
||||
return $"Maintain {details.TargetProvinceCount} provinces with Develop orders for {targetMonths} months{progressText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.MobilizeProvincesQuest: {
|
||||
var details = quest.Details.MobilizeProvincesQuest;
|
||||
var targetMonths = quest.ComponentCount;
|
||||
var currentMonths = quest.ComponentsFulfilled;
|
||||
var currentProvinces =
|
||||
model.PlayerId.HasValue
|
||||
? model.ProvincesForFaction(model.PlayerId.Value)
|
||||
.Count(pid => model.Provinces[pid]
|
||||
.FullInfo?.ProvinceOrders ==
|
||||
ProvinceOrderType.Mobilize)
|
||||
: 0;
|
||||
var statusText = currentProvinces >= details.TargetProvinceCount
|
||||
? "currently on target"
|
||||
: "not currently meeting";
|
||||
return $"Maintain {details.TargetProvinceCount} provinces with Mobilize orders for {targetMonths} months ({currentMonths}/{targetMonths} months, {statusText})";
|
||||
var progressText =
|
||||
currentMonths > 0 ? $" ({currentMonths}/{targetMonths} months)" : "";
|
||||
return $"Maintain {details.TargetProvinceCount} provinces with Mobilize orders for {targetMonths} months{progressText}";
|
||||
}
|
||||
|
||||
case SealedValueOneofCase.BattalionDiversityQuest:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using HexCell = HexGrid.CellGeometry;
|
||||
@@ -39,19 +38,12 @@ public class HexMesh : MonoBehaviour {
|
||||
|
||||
hexMesh.RecalculateNormals();
|
||||
|
||||
if (needsSetSharedMesh && triangles.Count > 0) {
|
||||
// Defer mesh collider assignment to next frame to avoid PhysX cooking
|
||||
// errors during initial setup
|
||||
StartCoroutine(SetMeshColliderNextFrame());
|
||||
if (needsSetSharedMesh) {
|
||||
meshCollider.sharedMesh = hexMesh;
|
||||
needsSetSharedMesh = false;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SetMeshColliderNextFrame() {
|
||||
yield return null; // Wait one frame
|
||||
if (meshCollider != null && hexMesh != null) { meshCollider.sharedMesh = hexMesh; }
|
||||
}
|
||||
|
||||
private void Triangulate(HexCell cell) {
|
||||
Vector3 center = new Vector3(
|
||||
cell.AnchoredPosition.x,
|
||||
|
||||
+7
-71
@@ -74,21 +74,6 @@ namespace Shardok {
|
||||
public Button endTurnButton;
|
||||
public TMP_Text locationNameText;
|
||||
public TMP_Text roundInfoText;
|
||||
|
||||
[Header("Layout Variants")]
|
||||
public GameObject topRowContainer;
|
||||
public GameObject leftColumnContainer;
|
||||
public TMP_Text turnStatusLabelAlt;
|
||||
public TMP_Text locationNameTextAlt;
|
||||
public TMP_Text roundInfoTextAlt;
|
||||
public TMP_Text connectionStatusLabelAlt;
|
||||
|
||||
private double _aspectRatio;
|
||||
private const double WideScreenThreshold = 2.0;
|
||||
private bool IsWideScreen => _aspectRatio > WideScreenThreshold;
|
||||
private int _lastWidth;
|
||||
private int _lastHeight;
|
||||
|
||||
public TMP_Text gameOverText;
|
||||
public Canvas gameOverCanvas;
|
||||
public AudioSource audioClipSource;
|
||||
@@ -259,10 +244,7 @@ namespace Shardok {
|
||||
commandWarningPanel.SetActive(false);
|
||||
}
|
||||
|
||||
void Update() {
|
||||
HandleButton();
|
||||
UpdateLayoutForAspectRatio();
|
||||
}
|
||||
void Update() { HandleButton(); }
|
||||
|
||||
public void EndTurn() {
|
||||
endTurnButton.interactable = false;
|
||||
@@ -548,7 +530,7 @@ namespace Shardok {
|
||||
|
||||
if (Model.GameStatus != null &&
|
||||
(Model.GameStatus.State == GameStatus.Types.State.Victory)) {
|
||||
SetTurnStatusText("Game Over!");
|
||||
turnStatusLabel.text = "Game Over!";
|
||||
|
||||
gameOverText.text = Model.GameStatus.Description;
|
||||
gameOverCanvas.gameObject.SetActive(true);
|
||||
@@ -557,7 +539,7 @@ namespace Shardok {
|
||||
endTurnButton.interactable = true;
|
||||
} else if (Model.GameStatus != null && Model.MyTurn) {
|
||||
gameOverCanvas.gameObject.SetActive(false);
|
||||
SetTurnStatusText("Your Turn");
|
||||
turnStatusLabel.text = "Your Turn";
|
||||
|
||||
if (Model.InSetUp) {
|
||||
endTurnButton.GetComponentInChildren<TMP_Text>().text = "Commit";
|
||||
@@ -592,20 +574,19 @@ namespace Shardok {
|
||||
|
||||
SelectAppropriateDefaultCommand();
|
||||
} else {
|
||||
SetTurnStatusText($"{Model.CurrentPlayerName}'s Turn");
|
||||
turnStatusLabel.text = $"{Model.CurrentPlayerName}'s Turn";
|
||||
}
|
||||
|
||||
SetLocationNameText(Model.LocationName);
|
||||
locationNameText.text = Model.LocationName;
|
||||
|
||||
if (Model.History.Count > 0) {
|
||||
string monthString = new DateTime(777, Model.Month, 1)
|
||||
.ToString("MMMM", CultureInfo.InvariantCulture);
|
||||
string roundText = $"{monthString} {Model.CurrentRound}";
|
||||
roundInfoText.text = $"{monthString} {Model.CurrentRound}";
|
||||
Weather weather = Model.Weather;
|
||||
if (weather != null) {
|
||||
roundText += ", " + ProtoExtensions.WeatherToString(weather);
|
||||
roundInfoText.text += ", " + ProtoExtensions.WeatherToString(weather);
|
||||
}
|
||||
SetRoundInfoText(roundText);
|
||||
|
||||
// Update weather visual effects
|
||||
if (weatherEffectAnimator != null) { weatherEffectAnimator.SetWeather(weather); }
|
||||
@@ -2211,50 +2192,5 @@ namespace Shardok {
|
||||
int gridRow = Model.Map.RowCount - 1 - mapCoords.Row;
|
||||
return gridRow * Model.Map.ColumnCount + mapCoords.Column;
|
||||
}
|
||||
|
||||
#region Layout Variant Helpers
|
||||
|
||||
private void SetTurnStatusText(string text) {
|
||||
turnStatusLabel.text = text;
|
||||
if (turnStatusLabelAlt != null) turnStatusLabelAlt.text = text;
|
||||
}
|
||||
|
||||
private void SetLocationNameText(string text) {
|
||||
locationNameText.text = text;
|
||||
if (locationNameTextAlt != null) locationNameTextAlt.text = text;
|
||||
}
|
||||
|
||||
private void SetRoundInfoText(string text) {
|
||||
roundInfoText.text = text;
|
||||
if (roundInfoTextAlt != null) roundInfoTextAlt.text = text;
|
||||
}
|
||||
|
||||
private void SetConnectionStatusText(string text) {
|
||||
connectionStatusLabel.text = text;
|
||||
if (connectionStatusLabelAlt != null) connectionStatusLabelAlt.text = text;
|
||||
}
|
||||
|
||||
private void UpdateLayoutForAspectRatio() {
|
||||
var newWidth = Screen.width;
|
||||
var newHeight = Screen.height;
|
||||
|
||||
if (newWidth == _lastWidth && newHeight == _lastHeight) { return; }
|
||||
|
||||
_lastWidth = newWidth;
|
||||
_lastHeight = newHeight;
|
||||
_aspectRatio = (double)newWidth / newHeight;
|
||||
|
||||
if (IsWideScreen) {
|
||||
// Widescreen: use left column layout
|
||||
if (topRowContainer != null) topRowContainer.SetActive(false);
|
||||
if (leftColumnContainer != null) leftColumnContainer.SetActive(true);
|
||||
} else {
|
||||
// Normal/narrow: use top row layout
|
||||
if (topRowContainer != null) topRowContainer.SetActive(true);
|
||||
if (leftColumnContainer != null) leftColumnContainer.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
+9
-36
File diff suppressed because one or more lines are too long
+1
-58
@@ -34,9 +34,6 @@ namespace Eagle0.Tutorial {
|
||||
// Register Shardok (tactical battle) tutorials
|
||||
RegisterShardokTutorials(registry);
|
||||
|
||||
// Register tutorial battle tutorials (scripted first-time battle)
|
||||
RegisterTutorialBattleTutorials(registry);
|
||||
|
||||
// Register lobby tutorials (pre-game)
|
||||
RegisterLobbyTutorials(registry);
|
||||
}
|
||||
@@ -565,8 +562,7 @@ namespace Eagle0.Tutorial {
|
||||
"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.\n\n" +
|
||||
"You can also designate a <b>Focus Province</b> where your vassals will send excess supplies.");
|
||||
"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
|
||||
@@ -1105,59 +1101,6 @@ namespace Eagle0.Tutorial {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// ========== TUTORIAL BATTLE TUTORIALS ==========
|
||||
// These appear during the scripted tutorial battle for first-time players.
|
||||
|
||||
private static void RegisterTutorialBattleTutorials(TutorialTriggerRegistry registry) {
|
||||
// Tutorial battle started - special intro for the scripted battle
|
||||
var battleStarted = CreateSingleStepTutorial(
|
||||
"tutorial_battle_started",
|
||||
"Defend Your Home!",
|
||||
"The traitor Ikhaan Tarn attacks your province with his veterans. " +
|
||||
"Your small garrison is outnumbered, but you must hold the line.\n\n" +
|
||||
"Fight bravely! Position your units wisely and look for opportunities " +
|
||||
"to strike at the enemy's weak points.\n\n" +
|
||||
"<color=#FFCC00>Help is on the way...</color>",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(battleStarted, "tutorial_battle_started");
|
||||
|
||||
// Tutorial enemy fled - when Tarn flees the battle
|
||||
var enemyFled = CreateSingleStepTutorial(
|
||||
"tutorial_enemy_fled",
|
||||
"The Enemy Retreats!",
|
||||
"Ikhaan Tarn has fled the battlefield! Your brave defense has " +
|
||||
"bought precious time.\n\n" +
|
||||
"Perhaps he realized that you are more formidable than he expected, " +
|
||||
"or perhaps something else has scared him off...",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(enemyFled, "tutorial_enemy_fled");
|
||||
|
||||
// Tutorial reinforcements arrived - when heroes arrive to help
|
||||
var reinforcementsArrived = CreateSingleStepTutorial(
|
||||
"tutorial_reinforcements_arrived",
|
||||
"Allies Arrive!",
|
||||
"Reinforcements have arrived just in time! " +
|
||||
"<b>Elena Fyar</b>, <b>Hedrick</b>, and <b>The Boulder</b> have joined " +
|
||||
"your forces.\n\n" +
|
||||
"These heroes heard of your stand against the traitor and rode hard " +
|
||||
"to aid you. They will now fight under your banner.\n\n" +
|
||||
"<color=#00FF00>Check your reserves - new units are available!</color>",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(reinforcementsArrived, "tutorial_reinforcements_arrived");
|
||||
|
||||
// Tutorial battle victory - when the player wins the tutorial battle
|
||||
var battleVictory = CreateSingleStepTutorial(
|
||||
"tutorial_battle_victory",
|
||||
"Victory!",
|
||||
"You have defended your home against the traitor's attack!\n\n" +
|
||||
"The heroes who came to your aid have sworn to serve you. " +
|
||||
"With their help, you can now begin building your power and " +
|
||||
"eventually bring Ikhaan Tarn to justice.\n\n" +
|
||||
"<b>Your journey as a ruler begins now.</b>",
|
||||
TutorialDisplayMode.Modal);
|
||||
registry.RegisterTutorial(battleVictory, "tutorial_battle_victory");
|
||||
}
|
||||
|
||||
// ========== PRE-GAME TUTORIALS ==========
|
||||
// These appear before the user enters a game (sign-in, lobby).
|
||||
|
||||
|
||||
-8
@@ -558,14 +558,6 @@ namespace Eagle0.Tutorial {
|
||||
case ActionType.CrossWaterFailed:
|
||||
OnGameEvent("terrain_water_encountered", result);
|
||||
break;
|
||||
|
||||
// Tutorial battle events
|
||||
case ActionType.TutorialEnemyFled:
|
||||
OnGameEvent("tutorial_enemy_fled", result);
|
||||
break;
|
||||
case ActionType.TutorialReinforcementsArrived:
|
||||
OnGameEvent("tutorial_reinforcements_arrived", result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-238
@@ -1,238 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Eagle0.Tutorial {
|
||||
/// <summary>
|
||||
/// Controller for displaying narrative screens before tutorial battles.
|
||||
/// Shows story screens with title, body text, and optional hero portraits.
|
||||
/// Chains through multiple screens before proceeding to gameplay.
|
||||
/// </summary>
|
||||
public class NarrativeScreenController : MonoBehaviour {
|
||||
[Header("Content")]
|
||||
[Tooltip("Title text")]
|
||||
public TextMeshProUGUI TitleText;
|
||||
|
||||
[Tooltip("Body text")]
|
||||
public TextMeshProUGUI BodyText;
|
||||
|
||||
[Tooltip("Optional hero portrait or scene image")]
|
||||
public Image SceneImage;
|
||||
|
||||
[Tooltip("Container for the scene image")]
|
||||
public GameObject SceneImageContainer;
|
||||
|
||||
[Header("Buttons")]
|
||||
[Tooltip("Continue button")]
|
||||
public Button ContinueButton;
|
||||
|
||||
[Tooltip("Text on continue button")]
|
||||
public TextMeshProUGUI ContinueButtonText;
|
||||
|
||||
[Tooltip("Skip button to skip all narrative")]
|
||||
public Button SkipButton;
|
||||
|
||||
[Header("Progress")]
|
||||
[Tooltip("Progress indicator (e.g., dots or page numbers)")]
|
||||
public TextMeshProUGUI ProgressText;
|
||||
|
||||
[Header("Background")]
|
||||
[Tooltip("Background panel container")]
|
||||
public GameObject PanelContainer;
|
||||
|
||||
[Tooltip("Fade overlay for transitions")]
|
||||
public CanvasGroup FadeOverlay;
|
||||
|
||||
[Header("Animation")]
|
||||
[Tooltip("Animator for screen transitions")]
|
||||
public Animator ScreenAnimator;
|
||||
|
||||
[Tooltip("Fade duration in seconds")]
|
||||
public float FadeDuration = 0.3f;
|
||||
|
||||
// Internal state
|
||||
private List<TutorialNarrativeScreen> _screens;
|
||||
private int _currentScreenIndex;
|
||||
private Action _onComplete;
|
||||
private bool _isTransitioning;
|
||||
|
||||
private void Awake() {
|
||||
if (ContinueButton != null) { ContinueButton.onClick.AddListener(OnContinueClicked); }
|
||||
if (SkipButton != null) { SkipButton.onClick.AddListener(OnSkipClicked); }
|
||||
|
||||
Hide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the narrative screen sequence.
|
||||
/// </summary>
|
||||
/// <param name="screens">List of narrative screens to display</param>
|
||||
/// <param name="onComplete">Callback when all screens are dismissed</param>
|
||||
public void Show(IEnumerable<TutorialNarrativeScreen> screens, Action onComplete) {
|
||||
_screens = new List<TutorialNarrativeScreen>(screens);
|
||||
_onComplete = onComplete;
|
||||
_currentScreenIndex = 0;
|
||||
|
||||
if (_screens.Count == 0) {
|
||||
_onComplete?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
gameObject.SetActive(true);
|
||||
if (PanelContainer != null) { PanelContainer.SetActive(true); }
|
||||
|
||||
DisplayCurrentScreen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays the current screen in the sequence.
|
||||
/// </summary>
|
||||
private void DisplayCurrentScreen() {
|
||||
if (_currentScreenIndex >= _screens.Count) {
|
||||
CompleteNarrative();
|
||||
return;
|
||||
}
|
||||
|
||||
var screen = _screens[_currentScreenIndex];
|
||||
|
||||
// Set title
|
||||
if (TitleText != null) { TitleText.text = screen.Title ?? ""; }
|
||||
|
||||
// Set body text
|
||||
if (BodyText != null) {
|
||||
BodyText.text = screen.BodyText ?? "";
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(BodyText.GetComponent<RectTransform>());
|
||||
}
|
||||
|
||||
// Set scene image if provided
|
||||
if (SceneImageContainer != null) {
|
||||
bool hasImage = !string.IsNullOrEmpty(screen.ImagePath);
|
||||
SceneImageContainer.SetActive(hasImage);
|
||||
|
||||
if (hasImage && SceneImage != null) {
|
||||
// Load image from resources
|
||||
var sprite = Resources.Load<Sprite>(screen.ImagePath);
|
||||
if (sprite != null) {
|
||||
SceneImage.sprite = sprite;
|
||||
} else {
|
||||
Debug.LogWarning(
|
||||
$"NarrativeScreenController: Could not load image at '{screen.ImagePath}'");
|
||||
SceneImageContainer.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update button text based on whether this is the last screen
|
||||
if (ContinueButtonText != null) {
|
||||
bool isLastScreen = _currentScreenIndex >= _screens.Count - 1;
|
||||
ContinueButtonText.text = isLastScreen ? "Begin Battle" : "Continue";
|
||||
}
|
||||
|
||||
// Update progress indicator
|
||||
if (ProgressText != null) {
|
||||
if (_screens.Count > 1) {
|
||||
ProgressText.text = $"{_currentScreenIndex + 1} / {_screens.Count}";
|
||||
ProgressText.gameObject.SetActive(true);
|
||||
} else {
|
||||
ProgressText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger show animation if available
|
||||
if (ScreenAnimator != null) { ScreenAnimator.SetTrigger("Show"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when Continue button is clicked.
|
||||
/// </summary>
|
||||
private void OnContinueClicked() {
|
||||
if (_isTransitioning) return;
|
||||
|
||||
_currentScreenIndex++;
|
||||
|
||||
if (_currentScreenIndex >= _screens.Count) {
|
||||
CompleteNarrative();
|
||||
} else {
|
||||
// Animate transition to next screen
|
||||
StartTransition(() => DisplayCurrentScreen());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when Skip button is clicked.
|
||||
/// </summary>
|
||||
private void OnSkipClicked() {
|
||||
if (_isTransitioning) return;
|
||||
CompleteNarrative();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a transition animation between screens.
|
||||
/// </summary>
|
||||
private void StartTransition(Action onTransitionComplete) {
|
||||
if (FadeOverlay == null || FadeDuration <= 0) {
|
||||
onTransitionComplete?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_isTransitioning = true;
|
||||
StartCoroutine(TransitionCoroutine(onTransitionComplete));
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator TransitionCoroutine(Action onComplete) {
|
||||
// Fade out
|
||||
float elapsed = 0f;
|
||||
while (elapsed < FadeDuration) {
|
||||
elapsed += Time.deltaTime;
|
||||
FadeOverlay.alpha = Mathf.Lerp(0f, 1f, elapsed / FadeDuration);
|
||||
yield return null;
|
||||
}
|
||||
FadeOverlay.alpha = 1f;
|
||||
|
||||
// Call the transition action
|
||||
onComplete?.Invoke();
|
||||
|
||||
// Fade in
|
||||
elapsed = 0f;
|
||||
while (elapsed < FadeDuration) {
|
||||
elapsed += Time.deltaTime;
|
||||
FadeOverlay.alpha = Mathf.Lerp(1f, 0f, elapsed / FadeDuration);
|
||||
yield return null;
|
||||
}
|
||||
FadeOverlay.alpha = 0f;
|
||||
|
||||
_isTransitioning = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the narrative sequence and invokes the callback.
|
||||
/// </summary>
|
||||
private void CompleteNarrative() {
|
||||
var callback = _onComplete;
|
||||
Hide();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the narrative screen.
|
||||
/// </summary>
|
||||
public void Hide() {
|
||||
if (ScreenAnimator != null) { ScreenAnimator.SetTrigger("Hide"); }
|
||||
if (PanelContainer != null) { PanelContainer.SetActive(false); }
|
||||
gameObject.SetActive(false);
|
||||
|
||||
_screens = null;
|
||||
_onComplete = null;
|
||||
_currentScreenIndex = 0;
|
||||
_isTransitioning = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the controller is currently showing screens.
|
||||
/// </summary>
|
||||
public bool IsShowing => gameObject.activeSelf && _screens != null && _screens.Count > 0;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ PlayerSettings:
|
||||
AndroidFilterTouchesWhenObscured: 0
|
||||
AndroidEnableSustainedPerformanceMode: 0
|
||||
defaultScreenOrientation: 4
|
||||
targetDevice: 1
|
||||
targetDevice: 2
|
||||
useOnDemandResources: 0
|
||||
accelerometerFrequency: 60
|
||||
companyName: Shardok Games
|
||||
|
||||
@@ -349,7 +349,6 @@ message CreateGameRequest {
|
||||
string desired_leader_text_id = 1;
|
||||
int32 total_player_count = 2;
|
||||
int32 human_player_count = 3;
|
||||
bool is_tutorial_mode = 4;
|
||||
}
|
||||
|
||||
message CreateGameResponse {
|
||||
|
||||
@@ -61,7 +61,6 @@ message QuestDetails {
|
||||
ReconProvincesQuest recon_provinces_quest = 44;
|
||||
RepairDevastationQuest repair_devastation_quest = 45;
|
||||
ReconSpecificProvincesQuest recon_specific_provinces_quest = 46;
|
||||
StartDroughtQuest start_drought_quest = 47;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,10 +221,6 @@ message StartEpidemicQuest {
|
||||
int32 province_id = 1;
|
||||
}
|
||||
|
||||
message StartDroughtQuest {
|
||||
int32 province_id = 1;
|
||||
}
|
||||
|
||||
message SpendOnFeastsQuest {
|
||||
int32 total_gold = 1;
|
||||
}
|
||||
|
||||
@@ -52,22 +52,6 @@ message SetFaction {
|
||||
int32 earliest_round_for_invitation = 5;
|
||||
}
|
||||
|
||||
// Configuration for tutorial battle that triggers at game start
|
||||
message TutorialBattleConfig {
|
||||
string attacker_faction_head = 1; // Faction head who attacks
|
||||
int32 target_province_id = 2; // Province to attack
|
||||
int32 flee_after_defender_units_lost = 3; // Attacker flees after defender loses N units
|
||||
int32 flee_after_rounds = 4; // Attacker flees after N rounds
|
||||
repeated string reinforcements = 5; // Hero names for reinforcements when attacker flees
|
||||
}
|
||||
|
||||
// Narrative screen shown before tutorial battle
|
||||
message TutorialNarrativeScreen {
|
||||
string title = 1;
|
||||
string body_text = 2;
|
||||
string image_path = 3;
|
||||
}
|
||||
|
||||
message GameParameters {
|
||||
string map_file_path = 2;
|
||||
|
||||
@@ -75,8 +59,4 @@ message GameParameters {
|
||||
ProvinceOverrides occupied_province_overrides = 4;
|
||||
|
||||
repeated SetFaction set_factions = 5;
|
||||
|
||||
// Tutorial-specific configuration
|
||||
TutorialBattleConfig tutorial_battle = 6;
|
||||
repeated TutorialNarrativeScreen tutorial_narrative_screens = 7;
|
||||
}
|
||||
|
||||
@@ -22,13 +22,6 @@ option java_package = "net.eagle0.eagle.views";
|
||||
option java_outer_classname = "GameStateView";
|
||||
option objc_class_prefix = "E0G";
|
||||
|
||||
// Narrative screen for tutorial and story sequences
|
||||
message TutorialNarrativeScreen {
|
||||
string title = 1;
|
||||
string body_text = 2;
|
||||
string image_path = 3; // Optional hero portrait or scene image
|
||||
}
|
||||
|
||||
message GameStateView {
|
||||
int32 current_round_id = 1;
|
||||
.net.eagle0.eagle.common.RoundPhase current_phase = 2;
|
||||
@@ -50,9 +43,6 @@ message GameStateView {
|
||||
repeated .net.eagle0.eagle.common.BattalionType battalion_types = 12;
|
||||
|
||||
repeated .net.eagle0.eagle.common.ChronicleEntry chronicle_entries = 14;
|
||||
|
||||
// Tutorial narrative screens to display before gameplay
|
||||
repeated TutorialNarrativeScreen pending_narrative_screens = 15;
|
||||
}
|
||||
|
||||
message GameStateViewDiff {
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
{
|
||||
"mapFilePath": "/net/eagle0/eagle/province_map.tsv",
|
||||
"genericProvinceOverrides": {
|
||||
"economy": 20.0,
|
||||
"agriculture": 20.0,
|
||||
"infrastructure": 20.0,
|
||||
"specialBattalionTypes": []
|
||||
},
|
||||
"occupiedProvinceOverrides": {
|
||||
"randomRulingPlayerHeroCount": 0,
|
||||
"economy": 40.0,
|
||||
"agriculture": 40.0,
|
||||
"infrastructure": 45.0,
|
||||
"gold": 500,
|
||||
"food": 3000,
|
||||
"support": 40,
|
||||
"orders": "ENTRUST",
|
||||
"specialBattalionTypes": [],
|
||||
"battalions": [
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 300,
|
||||
"training": 60,
|
||||
"armament": 60,
|
||||
"name": "Ranil's Spearmen"
|
||||
},
|
||||
{
|
||||
"type": "LIGHT_INFANTRY",
|
||||
"size": 300,
|
||||
"training": 60,
|
||||
"armament": 60,
|
||||
"name": "Ranil's Guard"
|
||||
},
|
||||
{
|
||||
"type": "LONGBOWMEN",
|
||||
"size": 200,
|
||||
"training": 60,
|
||||
"armament": 60,
|
||||
"name": "Ranil's Archers"
|
||||
}
|
||||
]
|
||||
},
|
||||
"setFactions": [
|
||||
{
|
||||
"factionHeadName": "Ikhaan Tarn",
|
||||
"swornBrotherNames": [],
|
||||
"occupiedProvinces": [
|
||||
{
|
||||
"provinceId": 32,
|
||||
"economy": 85.0,
|
||||
"agriculture": 91.0,
|
||||
"infrastructure": 66.0,
|
||||
"gold": 4400,
|
||||
"food": 5500,
|
||||
"support": 35,
|
||||
"orders": "ENTRUST",
|
||||
"rulingPlayerHeroNames": ["Ikhaan Tarn"],
|
||||
"randomRulingPlayerHeroCount": 2,
|
||||
"specialBattalionTypes": [],
|
||||
"battalions": [
|
||||
{
|
||||
"type": "HEAVY_CAVALRY",
|
||||
"size": 400,
|
||||
"training": 80,
|
||||
"armament": 80,
|
||||
"name": "Tarn's Knights"
|
||||
},
|
||||
{
|
||||
"type": "HEAVY_INFANTRY",
|
||||
"size": 500,
|
||||
"training": 80,
|
||||
"armament": 80,
|
||||
"name": "Tarn's Stormtroopers"
|
||||
},
|
||||
{
|
||||
"type": "LONGBOWMEN",
|
||||
"size": 300,
|
||||
"training": 80,
|
||||
"armament": 80,
|
||||
"name": "Tarn's Sharpshooters"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"startingTrusts": [],
|
||||
"earliestRoundForInvitation": 999
|
||||
}
|
||||
],
|
||||
"tutorialBattle": {
|
||||
"attackerFactionHead": "Ikhaan Tarn",
|
||||
"targetProvinceId": 14,
|
||||
"fleeAfterDefenderUnitsLost": 1,
|
||||
"fleeAfterRounds": 5,
|
||||
"reinforcements": ["Elena Fyar", "Hedrick", "The Boulder"]
|
||||
},
|
||||
"tutorialNarrativeScreens": [
|
||||
{
|
||||
"title": "The Engineer of Onmaa",
|
||||
"bodyText": "You are John Ranil, an engineer who once served the crown. When civil war erupted, you retreated to Onmaa, your ancestral home, hoping to wait out the conflict in peace.",
|
||||
"imagePath": ""
|
||||
},
|
||||
{
|
||||
"title": "The Traitor Arrives",
|
||||
"bodyText": "The traitor Ikhaan Tarn has arrived at Onmaa with his army. He once served King Bregos Fyar but betrayed him to join The Eagle, a mysterious mage from distant lands. Now Tarn seeks to expand his new master's domain.",
|
||||
"imagePath": ""
|
||||
},
|
||||
{
|
||||
"title": "Defend Your Home",
|
||||
"bodyText": "Against impossible odds, you must defend your home. Your small garrison is no match for Tarn's veterans, but you must hold the line until help arrives. Fight bravely, engineer.",
|
||||
"imagePath": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -58,28 +58,4 @@ object ApiKeys {
|
||||
lazy val anthropic: String = getKey(anthropicKeyName, anthropicEnvVar)
|
||||
|
||||
lazy val gemini: String = getKey(geminiKeyName, geminiEnvVar)
|
||||
|
||||
/** Check if OpenAI API key is available without throwing */
|
||||
def hasOpenAI: Boolean =
|
||||
Option(System.getenv(openAIEnvVar)).exists(_.nonEmpty) ||
|
||||
dictionary.get(openAIKeyName).exists(v => v.nonEmpty && !v.startsWith("your_"))
|
||||
|
||||
/** Check if Anthropic API key is available without throwing */
|
||||
def hasAnthropic: Boolean =
|
||||
Option(System.getenv(anthropicEnvVar)).exists(_.nonEmpty) ||
|
||||
dictionary.get(anthropicKeyName).exists(v => v.nonEmpty && !v.startsWith("your_"))
|
||||
|
||||
/** Check if Gemini API key is available without throwing */
|
||||
def hasGemini: Boolean =
|
||||
Option(System.getenv(geminiEnvVar)).exists(_.nonEmpty) ||
|
||||
dictionary.get(geminiKeyName).exists(v => v.nonEmpty && !v.startsWith("your_"))
|
||||
|
||||
/** Get list of providers with valid API keys configured */
|
||||
def availableProviders: Vector[String] =
|
||||
Vector("openai", "claude", "gemini").filter {
|
||||
case "openai" => hasOpenAI
|
||||
case "claude" => hasAnthropic
|
||||
case "gemini" => hasGemini
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,6 @@ load("@rules_scala//scala:scala.bzl", "scala_binary", "scala_library")
|
||||
scala_library(
|
||||
name = "api_keys",
|
||||
srcs = ["ApiKeys.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "external_text_generation_error",
|
||||
srcs = ["ExternalTextGenerationError.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
@@ -68,11 +60,7 @@ scala_library(
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
exports = [
|
||||
":external_text_generation_error",
|
||||
],
|
||||
deps = [
|
||||
":external_text_generation_error",
|
||||
":external_text_generation_service_impl",
|
||||
":rate_limits",
|
||||
":streaming_text_results",
|
||||
|
||||
+14
-8
@@ -18,6 +18,16 @@ import net.eagle0.common.sse.OkHttpSseListener
|
||||
import okhttp3.{MediaType, OkHttpClient, Request, RequestBody}
|
||||
import okhttp3.sse.EventSources
|
||||
|
||||
enum ExternalTextGenerationError extends Error:
|
||||
case RateLimit(code: Int, msg: String)
|
||||
case Http(code: Int, msg: String)
|
||||
case Timeout(msg: String)
|
||||
|
||||
def message: String = this match
|
||||
case RateLimit(_, msg) => msg
|
||||
case Http(_, msg) => msg
|
||||
case Timeout(msg) => msg
|
||||
|
||||
object ExternalTextGenerationCaller {
|
||||
private val initialBackoffSeconds: Double = 2.0
|
||||
|
||||
@@ -197,20 +207,16 @@ final class ExternalTextGenerationCaller(
|
||||
case Failure(exception) => Failure(exception)
|
||||
}.recoverWith {
|
||||
// Don't retry 4xx client errors - they won't succeed
|
||||
case e: ExternalTextGenerationError.Http =>
|
||||
Future.failed(e)
|
||||
// Don't retry 5xx server errors at caller level - let LlmResolver handle failover
|
||||
case e: ExternalTextGenerationError.ServerError =>
|
||||
println(s"Server error ${e.code} - not retrying, will failover to another provider")
|
||||
case e: ExternalTextGenerationError.Http =>
|
||||
Future.failed(e)
|
||||
// Retry transient errors (timeouts, network issues)
|
||||
case e: ExternalTextGenerationError.Timeout =>
|
||||
case e: ExternalTextGenerationError.Timeout =>
|
||||
println(s"Timeout error - retrying: ${e.message}")
|
||||
tryAgain()
|
||||
case e: IOException =>
|
||||
case e: IOException =>
|
||||
println(s"IOException - retrying: ${e.getMessage}")
|
||||
tryAgain()
|
||||
case e: Throwable =>
|
||||
case e: Throwable =>
|
||||
println(s"error ${e.getMessage} - retrying")
|
||||
tryAgain()
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package net.eagle0.common.llm_integration
|
||||
|
||||
enum ExternalTextGenerationError extends Error:
|
||||
case RateLimit(code: Int, msg: String)
|
||||
case Http(code: Int, msg: String)
|
||||
case ServerError(code: Int, msg: String) // 5xx errors - should trigger provider failover
|
||||
case Timeout(msg: String)
|
||||
|
||||
def message: String = this match
|
||||
case RateLimit(_, msg) => msg
|
||||
case Http(_, msg) => msg
|
||||
case ServerError(_, msg) => msg
|
||||
case Timeout(msg) => msg
|
||||
@@ -24,7 +24,6 @@ scala_library(
|
||||
srcs = ["OkHttpSseListener.scala"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/common/llm_integration:external_text_generation_error",
|
||||
"@maven//:com_squareup_okhttp3_okhttp",
|
||||
"@maven//:com_squareup_okhttp3_okhttp_sse",
|
||||
],
|
||||
|
||||
@@ -3,7 +3,6 @@ package net.eagle0.common.sse
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.function.Consumer
|
||||
|
||||
import net.eagle0.common.llm_integration.ExternalTextGenerationError
|
||||
import okhttp3.sse.{EventSource, EventSourceListener}
|
||||
import okhttp3.Response
|
||||
|
||||
@@ -59,28 +58,13 @@ class OkHttpSseListener(messageDataConsumer: Consumer[String]) extends EventSour
|
||||
} else {
|
||||
println(s"SSE failure: $errorMessage")
|
||||
}
|
||||
|
||||
// Create appropriate exception based on response code
|
||||
val exception = if response != null && response.code() >= 500 then {
|
||||
// 5xx server errors - should trigger provider failover
|
||||
ExternalTextGenerationError.ServerError(
|
||||
response.code(),
|
||||
s"Server error ${response.code()}: $errorMessage"
|
||||
)
|
||||
} else if response != null && response.code() >= 400 then {
|
||||
// 4xx client errors
|
||||
ExternalTextGenerationError.Http(
|
||||
response.code(),
|
||||
s"HTTP error ${response.code()}: $errorMessage"
|
||||
)
|
||||
} else if t != null then {
|
||||
t
|
||||
} else {
|
||||
new RuntimeException(
|
||||
s"SSE failed with response code ${Option(response).map(_.code().toString).getOrElse("unknown")}"
|
||||
)
|
||||
}
|
||||
|
||||
val _ = future.completeExceptionally(exception)
|
||||
// OkHttp can pass null throwable for HTTP error responses
|
||||
val exception =
|
||||
if t != null then t
|
||||
else
|
||||
new RuntimeException(
|
||||
s"SSE failed with response code ${Option(response).map(_.code().toString).getOrElse("unknown")}"
|
||||
)
|
||||
val _ = future.completeExceptionally(exception)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -2,6 +2,7 @@ package net.eagle0.eagle.library.actions.availability
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.library.settings.FeastGoldCostPerHero
|
||||
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.FeastAvailable
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
@@ -9,7 +10,17 @@ import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
|
||||
object AvailableFeastCommandFactory extends ScalaAvailableCommandsFactory {
|
||||
|
||||
private def feastGoldCost(province: ProvinceT): Int =
|
||||
private def hasHeroThatBenefits(
|
||||
gameState: GameState,
|
||||
province: ProvinceT
|
||||
): Boolean = {
|
||||
val factions = gameState.factions.values.toVector
|
||||
province.rulingFactionHeroIds
|
||||
.flatMap(hid => gameState.heroes.get(hid))
|
||||
.exists(h => h.vigor < h.constitution || HeroUtils.effectiveLoyalty(h, factions) < 100.0)
|
||||
}
|
||||
|
||||
def feastGoldCost(province: ProvinceT): Int =
|
||||
(province.rulingFactionHeroIds.length.toDouble * province.priceIndex * FeastGoldCostPerHero.intValue).ceil.toInt
|
||||
|
||||
override def availableCommand(
|
||||
@@ -21,7 +32,7 @@ object AvailableFeastCommandFactory extends ScalaAvailableCommandsFactory {
|
||||
val goldCost = feastGoldCost(province)
|
||||
|
||||
Option.when(
|
||||
province.gold >= goldCost
|
||||
province.gold >= goldCost && hasHeroThatBenefits(gameState, province)
|
||||
) {
|
||||
FeastAvailable(
|
||||
goldCost = goldCost,
|
||||
|
||||
@@ -92,7 +92,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:min_support_for_taxes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:portion_of_capacity_for_upgrade_battalion_quest",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/battalion_type_finder",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/province",
|
||||
"//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",
|
||||
@@ -1226,7 +1225,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/random_state_sequencer",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:returning_heroes",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:quest_fulfillment_utils",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:province_view_filter",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
|
||||
# Transitive deps of action_result_concrete (needed because ActionResultC has fields of these types)
|
||||
@@ -1242,10 +1240,7 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:incoming_end_turn_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+5
-8
@@ -27,7 +27,6 @@ import net.eagle0.eagle.model.state.quest.{
|
||||
ReturnPrisonerQuest,
|
||||
SendSuppliesQuest,
|
||||
StartBlizzardQuest,
|
||||
StartDroughtQuest,
|
||||
StartEpidemicQuest,
|
||||
SwearBrotherhoodWithHeroQuest,
|
||||
TruceCountQuest,
|
||||
@@ -176,17 +175,15 @@ case class CheckForFailedQuestsAction(
|
||||
case _: StartBlizzardQuest =>
|
||||
// Never fails - if a blizzard already exists, quest creation should not offer this
|
||||
false
|
||||
case _: StartDroughtQuest =>
|
||||
// Never fails - if a drought already exists, quest creation should not offer this
|
||||
false
|
||||
case _: StartEpidemicQuest =>
|
||||
// Never fails - if an epidemic already exists, quest creation should not offer this
|
||||
false
|
||||
case SendSuppliesQuest(_, _, targetProvinceId, _) =>
|
||||
// Fails if we no longer own the target province
|
||||
!provinces.exists(p =>
|
||||
p.id == targetProvinceId &&
|
||||
p.rulingFactionId == province.rulingFactionId
|
||||
// Fails if target province is no longer owned by an enemy faction
|
||||
val targetProvince = provinces.find(_.id == targetProvinceId)
|
||||
targetProvince.forall(tp =>
|
||||
tp.rulingFactionId.isEmpty ||
|
||||
tp.rulingFactionId == province.rulingFactionId
|
||||
)
|
||||
case RestProvinceQuest(_, _, targetProvinceId) =>
|
||||
// Fails if we no longer own the target province
|
||||
|
||||
+2
-11
@@ -4,7 +4,6 @@ import net.eagle0.eagle.{BattalionId, FactionId, GameId, HeroId, ProvinceId, Rou
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSequentialResultsAction
|
||||
import net.eagle0.eagle.library.settings.{MinSupportForTaxes, PortionOfCapacityForUpgradeBattalionQuest}
|
||||
import net.eagle0.eagle.library.util.battalion_type_finder.BattalionTypeFinder
|
||||
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
|
||||
import net.eagle0.eagle.library.util.province.ProvinceUtils
|
||||
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentUtils
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
@@ -42,7 +41,6 @@ import net.eagle0.eagle.model.state.quest.{
|
||||
ReturnPrisonerQuest,
|
||||
SpecificExpansionQuest,
|
||||
StartBlizzardQuest,
|
||||
StartDroughtQuest,
|
||||
StartEpidemicQuest,
|
||||
SuppressRiotByForceQuest,
|
||||
SwearBrotherhoodWithHeroQuest,
|
||||
@@ -206,16 +204,11 @@ case class CheckForFulfilledQuestsAction(
|
||||
case _: BetrayAllyQuest =>
|
||||
false // action quest - fulfilled in BreakAllianceResolutionHelpers
|
||||
case BorderSecurityQuest(targetProvinceId) =>
|
||||
// Fulfilled when all neighbors of the target province are controlled by the faction or an ally
|
||||
// Fulfilled when all neighbors of the target province are controlled by the faction
|
||||
provinceWithId(targetProvinceId).exists { targetProvince =>
|
||||
targetProvince.neighbors.forall { neighbor =>
|
||||
provinceWithId(neighbor.provinceId).exists { neighborProvince =>
|
||||
neighborProvince.rulingFactionId == province.rulingFactionId ||
|
||||
(for {
|
||||
questFactionId <- province.rulingFactionId
|
||||
neighborFactionId <- neighborProvince.rulingFactionId
|
||||
} yield FactionUtils.hasAlliance(questFactionId, neighborFactionId, factions))
|
||||
.getOrElse(false)
|
||||
neighborProvince.rulingFactionId == province.rulingFactionId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,8 +216,6 @@ case class CheckForFulfilledQuestsAction(
|
||||
false // action quest - fulfilled in ResolveBattleAction
|
||||
case _: StartBlizzardQuest =>
|
||||
false // action quest - fulfilled in ControlWeatherCommand
|
||||
case _: StartDroughtQuest =>
|
||||
false // action quest - fulfilled in ControlWeatherCommand
|
||||
case _: StartEpidemicQuest =>
|
||||
false // action quest - fulfilled in ControlWeatherCommand
|
||||
// Note: WinBattlesQuest is handled by ComponentQuest case above
|
||||
|
||||
+2
-56
@@ -1,12 +1,9 @@
|
||||
package net.eagle0.eagle.library.actions.impl.action
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState}
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultApplier
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessRandomSequentialResultsAction
|
||||
import net.eagle0.eagle.library.actions.random_state_sequencer.RandomStateSequencer
|
||||
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentUtils
|
||||
import net.eagle0.eagle.library.util.view_filters.ProvinceViewFilter
|
||||
import net.eagle0.eagle.library.util.ReturningHeroes
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
@@ -14,9 +11,7 @@ import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedFact
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType.{EndReconResolutionPhase, ReconSucceeded}
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.{IncomingEndTurnAction, IncomingRecon, ProvinceT}
|
||||
import net.eagle0.eagle.model.state.quest.{ReconProvincesQuest, ReconSpecificProvincesQuest}
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroT
|
||||
import net.eagle0.eagle.model.state.province.{IncomingEndTurnAction, IncomingRecon}
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
import net.eagle0.eagle.ProvinceId
|
||||
|
||||
@@ -69,9 +64,6 @@ case class PerformReconResolutionAction(
|
||||
// Extract heroId from the recon action details
|
||||
val heroId = incomingEndTurnAction.details.asInstanceOf[IncomingRecon].heroId
|
||||
|
||||
val factionProvinces = gameState.provinces.values.toVector
|
||||
.filter(_.rulingFactionId.contains(fromFactionId))
|
||||
|
||||
ReturningHeroes
|
||||
.heroesReturningToFaction(
|
||||
hids = Vector(heroId),
|
||||
@@ -81,7 +73,7 @@ case class PerformReconResolutionAction(
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.map { returningHeroes =>
|
||||
val baseResult = ActionResultC(
|
||||
ActionResultC(
|
||||
actionResultType = ReconSucceeded,
|
||||
changedHeroes = returningHeroes.changedHeroes,
|
||||
changedProvinces = Vector(
|
||||
@@ -111,52 +103,6 @@ case class PerformReconResolutionAction(
|
||||
)
|
||||
}.toVector
|
||||
)
|
||||
|
||||
// Increment recon quest counters
|
||||
val withReconProvincesQuestIncrement =
|
||||
reconProvincesQuestIncrementer(factionProvinces)(baseResult)
|
||||
reconSpecificProvincesQuestIncrementer(factionProvinces, provinceId)(withReconProvincesQuestIncrement)
|
||||
}
|
||||
}
|
||||
|
||||
private def reconProvincesQuestCheck(
|
||||
uh: UnaffiliatedHeroT,
|
||||
@unused province: ProvinceT
|
||||
): Boolean =
|
||||
uh.quest.exists {
|
||||
case q: ReconProvincesQuest => q.componentsFulfilled < q.componentCount
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private def reconProvincesQuestIncrementer(
|
||||
factionProvinces: Vector[ProvinceT]
|
||||
): ActionResultT => ActionResultT =
|
||||
QuestFulfillmentUtils.withCountersIncremented(
|
||||
provinces = factionProvinces,
|
||||
incrementAmount = 1,
|
||||
check = reconProvincesQuestCheck
|
||||
)
|
||||
|
||||
private def reconSpecificProvincesQuestCheck(
|
||||
reconnedProvinceId: ProvinceId
|
||||
)(
|
||||
uh: UnaffiliatedHeroT,
|
||||
@unused province: ProvinceT
|
||||
): Boolean =
|
||||
uh.quest.exists {
|
||||
case q: ReconSpecificProvincesQuest =>
|
||||
q.componentsFulfilled < q.componentCount &&
|
||||
q.targetProvinceIds.contains(reconnedProvinceId)
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private def reconSpecificProvincesQuestIncrementer(
|
||||
factionProvinces: Vector[ProvinceT],
|
||||
reconnedProvinceId: ProvinceId
|
||||
): ActionResultT => ActionResultT =
|
||||
QuestFulfillmentUtils.withCountersIncremented(
|
||||
provinces = factionProvinces,
|
||||
incrementAmount = 1,
|
||||
check = reconSpecificProvincesQuestCheck(reconnedProvinceId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -421,7 +421,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:loyalty_gain_from_feast",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:vigor_gain_from_feast",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:price_index_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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
|
||||
@@ -435,8 +434,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -683,7 +680,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:improvement_support_gain",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:improvement_xp_per_stat",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
@@ -701,8 +697,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1141,7 +1135,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_simple_action",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:rest_vigor_gain",
|
||||
"//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",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
|
||||
@@ -1156,8 +1149,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero:gender",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -212,8 +212,7 @@ class CommandFactory extends TCommandFactory {
|
||||
currentRoundId = gameState.currentRoundId,
|
||||
allProvinces = allProvinces(gameState),
|
||||
factions = allFactions(gameState),
|
||||
battalions = allBattalions(gameState),
|
||||
heroes = gameState.heroes.values.toVector
|
||||
battalions = allBattalions(gameState)
|
||||
)
|
||||
case (ac: ExileVassalAvailable, sc: ExileVassalSelected) =>
|
||||
ExileVassalCommand.make(
|
||||
@@ -227,13 +226,12 @@ class CommandFactory extends TCommandFactory {
|
||||
)
|
||||
case (ac: FeastAvailable, _: FeastSelected.type) =>
|
||||
val province = gameState.provinces(ac.actingProvinceId)
|
||||
val faction = gameState.factions(actingFactionId)
|
||||
FeastCommand.make(
|
||||
actingFactionId = actingFactionId,
|
||||
province = province,
|
||||
rulingFactionHeroes = province.rulingFactionHeroIds
|
||||
.map(gameState.heroes),
|
||||
factionLeaderIds = faction.leaderIds,
|
||||
factionLeaderIds = gameState.factions(actingFactionId).leaderIds,
|
||||
goldCost = ac.goldCost
|
||||
)
|
||||
case (ac: FreeForAllDecisionAvailable, sc: FreeForAllDecisionSelected) =>
|
||||
@@ -325,9 +323,7 @@ class CommandFactory extends TCommandFactory {
|
||||
improvementType = improvementTypeMap(sc.improvementType),
|
||||
availableHeroIds = ac.availableHeroIds,
|
||||
availableImprovementTypes = ac.availableTypes.map(improvementTypeMap),
|
||||
lockImprovementType = sc.lockType,
|
||||
factionProvinces = gameState.provinces.values.toVector
|
||||
.filter(_.rulingFactionId.contains(actingFactionId))
|
||||
lockImprovementType = sc.lockType
|
||||
)
|
||||
case (ac: IssueOrdersAvailable, sc: IssueOrdersSelected) =>
|
||||
IssueOrdersCommand.make(
|
||||
@@ -649,9 +645,7 @@ class CommandFactory extends TCommandFactory {
|
||||
RestCommand.make(
|
||||
actingFactionId = actingFactionId,
|
||||
provinceId = ac.actingProvinceId,
|
||||
factionHeroesInProvince = factionHeroesInProvince(gameState, ac.actingProvinceId),
|
||||
factionProvinces = gameState.provinces.values.toVector
|
||||
.filter(_.rulingFactionId.contains(actingFactionId))
|
||||
factionHeroesInProvince = factionHeroesInProvince(gameState, ac.actingProvinceId)
|
||||
)
|
||||
case (ac: ReturnAvailable, _: ReturnSelected.type) =>
|
||||
ReturnCommand.make(
|
||||
|
||||
@@ -18,7 +18,6 @@ import net.eagle0.eagle.model.action_result.NotificationDetails.Divined
|
||||
import net.eagle0.eagle.model.action_result.NotificationT.Llm
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.quest.{
|
||||
AlmsToProvinceQuest,
|
||||
@@ -29,8 +28,6 @@ import net.eagle0.eagle.model.state.quest.{
|
||||
ImproveInfrastructureQuest,
|
||||
SpecificExpansionQuest,
|
||||
StartBlizzardQuest,
|
||||
StartDroughtQuest,
|
||||
StartEpidemicQuest,
|
||||
TruceWithFactionQuest
|
||||
}
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroT
|
||||
@@ -50,8 +47,7 @@ object DivineCommand {
|
||||
currentRoundId: RoundId,
|
||||
allProvinces: Vector[ProvinceT],
|
||||
factions: Vector[FactionT],
|
||||
battalions: Vector[BattalionT],
|
||||
heroes: Vector[HeroT]
|
||||
battalions: Vector[BattalionT]
|
||||
): ProtolessRandomSimpleAction = {
|
||||
commandRequire(
|
||||
selectedHeroIds.distinct.size == selectedHeroIds.size,
|
||||
@@ -83,8 +79,7 @@ object DivineCommand {
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = battalions,
|
||||
heroes = heroes
|
||||
battalions = battalions
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,8 +91,7 @@ object DivineCommand {
|
||||
currentRoundId: RoundId,
|
||||
allProvinces: Vector[ProvinceT],
|
||||
factions: Vector[FactionT],
|
||||
battalions: Vector[BattalionT],
|
||||
heroes: Vector[HeroT]
|
||||
battalions: Vector[BattalionT]
|
||||
) extends ProtolessRandomSimpleAction {
|
||||
override def immediateExecute(
|
||||
functionalRandom: FunctionalRandom
|
||||
@@ -121,7 +115,6 @@ object DivineCommand {
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = battalions,
|
||||
heroes = heroes,
|
||||
functionalRandom = fr
|
||||
)
|
||||
.map { newRI =>
|
||||
@@ -185,10 +178,6 @@ object DivineCommand {
|
||||
Vector(provinceId)
|
||||
case StartBlizzardQuest(provinceId) =>
|
||||
Vector(provinceId)
|
||||
case StartDroughtQuest(provinceId) =>
|
||||
Vector(provinceId)
|
||||
case StartEpidemicQuest(provinceId) =>
|
||||
Vector(provinceId)
|
||||
}
|
||||
.getOrElse(Vector())
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package net.eagle0.eagle.library.actions.impl.command
|
||||
import net.eagle0.eagle.{FactionId, HeroId}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
|
||||
import net.eagle0.eagle.library.settings.{LoyaltyGainFromFeast, VigorGainFromFeast}
|
||||
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentUtils
|
||||
import net.eagle0.eagle.library.util.PriceIndexUtils
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHeroC, StatDelta, StatNoChange}
|
||||
@@ -11,7 +10,6 @@ import net.eagle0.eagle.model.action_result.types.ActionResultType.Feast
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.quest.SpendOnFeastsQuest
|
||||
|
||||
object FeastCommand {
|
||||
def make(
|
||||
@@ -58,43 +56,18 @@ object FeastCommand {
|
||||
)
|
||||
)
|
||||
|
||||
private def changedProvinces: Vector[ChangedProvinceC] = {
|
||||
// Increment SpendOnFeastsQuest progress by goldCost for unaffiliated hero in this province
|
||||
val questProgressChange = QuestFulfillmentUtils.counterIncrementedCP(
|
||||
province = province,
|
||||
incrementAmount = goldCost,
|
||||
check = (uh, _) =>
|
||||
uh.quest.exists {
|
||||
case q: SpendOnFeastsQuest => q.componentsFulfilled < q.componentCount
|
||||
case _ => false
|
||||
}
|
||||
private def changedProvinces: Vector[ChangedProvinceC] =
|
||||
Vector(
|
||||
ChangedProvinceC(
|
||||
provinceId = province.id,
|
||||
goldDelta = Some(-goldCost),
|
||||
newPriceIndex = PriceIndexUtils.maybeNewPriceIndexFromGoldSpend(
|
||||
currentPriceIndex = province.priceIndex,
|
||||
goldSpent = goldCost
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
questProgressChange match {
|
||||
case Some(cp) =>
|
||||
Vector(
|
||||
cp.copy(
|
||||
goldDelta = Some(-goldCost),
|
||||
newPriceIndex = PriceIndexUtils.maybeNewPriceIndexFromGoldSpend(
|
||||
currentPriceIndex = province.priceIndex,
|
||||
goldSpent = goldCost
|
||||
)
|
||||
)
|
||||
)
|
||||
case None =>
|
||||
Vector(
|
||||
ChangedProvinceC(
|
||||
provinceId = province.id,
|
||||
goldDelta = Some(-goldCost),
|
||||
newPriceIndex = PriceIndexUtils.maybeNewPriceIndexFromGoldSpend(
|
||||
currentPriceIndex = province.priceIndex,
|
||||
goldSpent = goldCost
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def immediateExecute: ActionResultT =
|
||||
ActionResultC(
|
||||
actionResultType = Feast,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package net.eagle0.eagle.library.actions.impl.command
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
import net.eagle0.eagle.{FactionId, HeroId}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
|
||||
import net.eagle0.eagle.library.settings.{
|
||||
@@ -12,7 +10,6 @@ import net.eagle0.eagle.library.settings.{
|
||||
ImprovementSupportGain,
|
||||
ImprovementXpPerStat
|
||||
}
|
||||
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentUtils
|
||||
import net.eagle0.eagle.library.util.EagleRequire.commandRequire
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.{ChangedProvinceC, NewLockedImprovementType}
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHeroC, StatDelta}
|
||||
@@ -20,8 +17,6 @@ import net.eagle0.eagle.model.action_result.types.ActionResultType.Improve
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.hero.{HeroT, Profession}
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.quest.RepairDevastationQuest
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroT
|
||||
import net.eagle0.eagle.model.state.ImprovementType
|
||||
|
||||
object ImproveCommand {
|
||||
@@ -32,8 +27,7 @@ object ImproveCommand {
|
||||
improvementType: ImprovementType,
|
||||
lockImprovementType: Boolean,
|
||||
availableHeroIds: Vector[HeroId],
|
||||
availableImprovementTypes: Vector[ImprovementType],
|
||||
factionProvinces: Vector[ProvinceT]
|
||||
availableImprovementTypes: Vector[ImprovementType]
|
||||
): ProtolessSimpleAction = {
|
||||
commandRequire(
|
||||
availableHeroIds.contains(actingHero.id),
|
||||
@@ -50,8 +44,7 @@ object ImproveCommand {
|
||||
actingHero = actingHero,
|
||||
province = actingProvince,
|
||||
improvementType = improvementType,
|
||||
lockType = lockImprovementType,
|
||||
factionProvinces = factionProvinces
|
||||
lockType = lockImprovementType
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,8 +53,7 @@ object ImproveCommand {
|
||||
province: ProvinceT,
|
||||
actingHero: HeroT,
|
||||
improvementType: ImprovementType,
|
||||
lockType: Boolean,
|
||||
factionProvinces: Vector[ProvinceT]
|
||||
lockType: Boolean
|
||||
) extends ProtolessSimpleAction {
|
||||
private val maxImprovement = 100.0
|
||||
|
||||
@@ -138,7 +130,7 @@ object ImproveCommand {
|
||||
agilityXpDelta = Some(ImprovementXpPerStat.intValue)
|
||||
)
|
||||
|
||||
val baseResult = ActionResultC(
|
||||
ActionResultC(
|
||||
actionResultType = Improve,
|
||||
actingFactionId = Some(factionId),
|
||||
provinceId = Some(provinceId),
|
||||
@@ -146,33 +138,8 @@ object ImproveCommand {
|
||||
changedHeroes = Vector(leaderAfter),
|
||||
changedProvinces = Vector(changedProvince)
|
||||
)
|
||||
|
||||
// Increment RepairDevastationQuest progress when repairing devastation
|
||||
improvementType match {
|
||||
case ImprovementType.Devastation =>
|
||||
repairDevastationQuestIncrementer(withProfessionBonus.toInt)(baseResult)
|
||||
case _ => baseResult
|
||||
}
|
||||
}
|
||||
|
||||
private def repairDevastationQuestCheck(
|
||||
uh: UnaffiliatedHeroT,
|
||||
@unused province: ProvinceT
|
||||
): Boolean =
|
||||
uh.quest.exists {
|
||||
case q: RepairDevastationQuest => q.componentsFulfilled < q.componentCount
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private def repairDevastationQuestIncrementer(
|
||||
devastationRepaired: Int
|
||||
): ActionResultT => ActionResultT =
|
||||
QuestFulfillmentUtils.withCountersIncremented(
|
||||
provinces = factionProvinces,
|
||||
incrementAmount = devastationRepaired,
|
||||
check = repairDevastationQuestCheck
|
||||
)
|
||||
|
||||
private def ImprovementFromZero(actor: HeroT): Double =
|
||||
ImprovementPerStat.doubleValue * (actor.strength + actor.agility) / 2.0
|
||||
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
package net.eagle0.eagle.library.actions.impl.command
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
import net.eagle0.eagle.{FactionId, ProvinceId}
|
||||
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
|
||||
import net.eagle0.eagle.library.settings.RestVigorGain
|
||||
import net.eagle0.eagle.library.util.quest_fulfillment.QuestFulfillmentUtils
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHeroC, StatDelta}
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType.Rest
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.quest.RestProvinceQuest
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroT
|
||||
|
||||
object RestCommand {
|
||||
private case class RestCommand(
|
||||
factionId: FactionId,
|
||||
provinceId: ProvinceId,
|
||||
factionHeroesInProvince: Vector[HeroT],
|
||||
factionProvinces: Vector[ProvinceT]
|
||||
factionHeroesInProvince: Vector[HeroT]
|
||||
) extends ProtolessSimpleAction {
|
||||
override def immediateExecute: ActionResultT = {
|
||||
val restedHeroes = factionHeroesInProvince
|
||||
@@ -34,46 +27,24 @@ object RestCommand {
|
||||
)
|
||||
)
|
||||
|
||||
val baseResult = ActionResultC(
|
||||
ActionResultC(
|
||||
actionResultType = Rest,
|
||||
actingFactionId = Some(factionId),
|
||||
provinceId = Some(provinceId),
|
||||
provinceIdActed = Some(provinceId),
|
||||
changedHeroes = restedHeroes
|
||||
)
|
||||
|
||||
// Increment RestProvinceQuest progress when resting in the target province
|
||||
restProvinceQuestIncrementer(baseResult)
|
||||
}
|
||||
|
||||
private def restProvinceQuestCheck(
|
||||
uh: UnaffiliatedHeroT,
|
||||
@unused province: ProvinceT
|
||||
): Boolean =
|
||||
uh.quest.exists {
|
||||
case q: RestProvinceQuest =>
|
||||
q.targetProvinceId == provinceId && q.componentsFulfilled < q.componentCount
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private def restProvinceQuestIncrementer: ActionResultT => ActionResultT =
|
||||
QuestFulfillmentUtils.withCountersIncremented(
|
||||
provinces = factionProvinces,
|
||||
incrementAmount = 1,
|
||||
check = restProvinceQuestCheck
|
||||
)
|
||||
}
|
||||
|
||||
def make(
|
||||
actingFactionId: FactionId,
|
||||
provinceId: ProvinceId,
|
||||
factionHeroesInProvince: Vector[HeroT],
|
||||
factionProvinces: Vector[ProvinceT]
|
||||
factionHeroesInProvince: Vector[HeroT]
|
||||
): ProtolessSimpleAction =
|
||||
RestCommand(
|
||||
factionId = actingFactionId,
|
||||
provinceId = provinceId,
|
||||
factionHeroesInProvince = factionHeroesInProvince,
|
||||
factionProvinces = factionProvinces
|
||||
factionHeroesInProvince = factionHeroesInProvince
|
||||
)
|
||||
}
|
||||
|
||||
-6
@@ -318,12 +318,6 @@ case class DivineMessagePromptGenerator(
|
||||
s"$divinedHeroName wants to see a magical epidemic unleashed upon ${targetProvince.name}."
|
||||
)
|
||||
|
||||
case StartDroughtQuest(targetProvinceId) =>
|
||||
val targetProvince = gameState.provinces(targetProvinceId)
|
||||
TextGenerationSuccess(
|
||||
s"$divinedHeroName wants to see a magical drought unleashed upon ${targetProvince.name}."
|
||||
)
|
||||
|
||||
case SpendOnFeastsQuest(componentCount, _, totalGold) =>
|
||||
TextGenerationSuccess(
|
||||
s"$divinedHeroName wants $actingHeroName to host lavish feasts spending $totalGold gold over $componentCount months."
|
||||
|
||||
-6
@@ -326,12 +326,6 @@ object QuestEndedGeneratorUtilities {
|
||||
unaffiliatedHeroName <- unaffiliatedHeroNameResult
|
||||
} yield s"$unaffiliatedHeroName wanted to see a magical epidemic unleashed upon ${targetProvince.name}."
|
||||
|
||||
case StartDroughtQuest(targetProvinceId) =>
|
||||
val targetProvince = gameState.provinces(targetProvinceId)
|
||||
for {
|
||||
unaffiliatedHeroName <- unaffiliatedHeroNameResult
|
||||
} yield s"$unaffiliatedHeroName wanted to see a magical drought unleashed upon ${targetProvince.name}."
|
||||
|
||||
case SpendOnFeastsQuest(componentCount, _, totalGold) =>
|
||||
for {
|
||||
unaffiliatedHeroName <- unaffiliatedHeroNameResult
|
||||
|
||||
@@ -305,7 +305,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:give_to_heroes_in_province_quest_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:improve_quest_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:quest_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:total_development_quest_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:truce_count_quest_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:truce_with_faction_quest_command_chooser",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:unaffiliated_hero_with_quest",
|
||||
|
||||
+2
-4
@@ -10,7 +10,6 @@ import net.eagle0.eagle.library.util.command_choice_helpers.quest_command_select
|
||||
GiveToHeroesInProvinceQuestCommandChooser,
|
||||
ImproveQuestCommandChooser,
|
||||
QuestCommandChooser,
|
||||
TotalDevelopmentQuestCommandChooser,
|
||||
TruceCountQuestCommandChooser,
|
||||
TruceWithFactionQuestCommandChooser,
|
||||
UnaffiliatedHeroWithQuest
|
||||
@@ -23,16 +22,15 @@ import net.eagle0.eagle.FactionId
|
||||
|
||||
object FulfillQuestsCommandSelector {
|
||||
private val choosers: Vector[QuestCommandChooser] = Vector(
|
||||
AllianceQuestCommandChooser,
|
||||
TruceWithFactionQuestCommandChooser,
|
||||
ImproveQuestCommandChooser,
|
||||
TotalDevelopmentQuestCommandChooser,
|
||||
AlmsToProvinceQuestCommandChooser,
|
||||
GiveToHeroesInProvinceQuestCommandChooser,
|
||||
AlmsAcrossRealmQuestCommandChooser,
|
||||
GiveToHeroesAcrossRealmQuestCommandChooser,
|
||||
TruceCountQuestCommandChooser,
|
||||
DismissSpecificVassalCommandChooser,
|
||||
AllianceQuestCommandChooser
|
||||
DismissSpecificVassalCommandChooser
|
||||
)
|
||||
|
||||
private def choiceFrom(
|
||||
|
||||
-25
@@ -191,31 +191,6 @@ scala_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "total_development_quest_command_chooser",
|
||||
srcs = ["TotalDevelopmentQuestCommandChooser.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers/quest_command_selectors:__pkg__",
|
||||
],
|
||||
exports = [
|
||||
":quest_command_chooser",
|
||||
],
|
||||
deps = [
|
||||
":quest_command_chooser",
|
||||
":unaffiliated_hero_with_quest",
|
||||
"//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:improve_command_selector",
|
||||
"//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/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "truce_count_quest_command_chooser",
|
||||
srcs = ["TruceCountQuestCommandChooser.scala"],
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
package net.eagle0.eagle.library.util.command_choice_helpers.quest_command_selectors
|
||||
|
||||
import net.eagle0.eagle.library.util.command_choice_helpers.ImproveCommandSelector
|
||||
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.command.common.ImprovementType
|
||||
import net.eagle0.eagle.model.state.command.common.ImprovementType.{Agriculture, Devastation, Economy, Infrastructure}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.quest.TotalDevelopmentQuest
|
||||
import net.eagle0.eagle.FactionId
|
||||
|
||||
object TotalDevelopmentQuestCommandChooser extends DeterministicQuestCommandChooser {
|
||||
|
||||
private def currentTotalDevelopment(province: ProvinceT): Double =
|
||||
ProvinceUtils.effectiveAgriculture(province) +
|
||||
ProvinceUtils.effectiveEconomy(province) +
|
||||
ProvinceUtils.effectiveInfrastructure(province)
|
||||
|
||||
private def shouldRepairDevastation(province: ProvinceT): Boolean = {
|
||||
val totalDevastation =
|
||||
province.economyDevastation + province.agricultureDevastation + province.infrastructureDevastation
|
||||
totalDevastation >= 4
|
||||
}
|
||||
|
||||
private def lowestImprovementType(province: ProvinceT): ImprovementType = {
|
||||
val eco = ProvinceUtils.effectiveEconomy(province)
|
||||
val agri = ProvinceUtils.effectiveAgriculture(province)
|
||||
val infra = ProvinceUtils.effectiveInfrastructure(province)
|
||||
|
||||
if eco <= agri && eco <= infra then Economy
|
||||
else if agri <= infra then Agriculture
|
||||
else Infrastructure
|
||||
}
|
||||
|
||||
private def chosenImprovementType(province: ProvinceT): ImprovementType =
|
||||
if shouldRepairDevastation(province) then Devastation
|
||||
else lowestImprovementType(province)
|
||||
|
||||
override def chosenDeterministicCommand(
|
||||
actingFactionId: FactionId,
|
||||
gameState: GameState,
|
||||
availableCommands: Vector[AvailableCommand],
|
||||
uhsWithQuests: Vector[UnaffiliatedHeroWithQuest]
|
||||
): Option[CommandSelection] =
|
||||
uhsWithQuests
|
||||
.map(uhwq => (uhwq.pid, uhwq.quest))
|
||||
.collect { case (pid, q: TotalDevelopmentQuest) => (pid, q) }
|
||||
.filter {
|
||||
case (pid, quest) =>
|
||||
val province = gameState.provinces(pid)
|
||||
// Only try to improve if we haven't reached the target yet
|
||||
currentTotalDevelopment(province) < quest.desiredTotalDevelopment
|
||||
}
|
||||
.flatMap {
|
||||
case (pid, _) =>
|
||||
val province = gameState.provinces(pid)
|
||||
val improvementType = chosenImprovementType(province)
|
||||
ImproveCommandSelector.chosenSpecificImprovementCommandForProvince(
|
||||
actingFactionId = actingFactionId,
|
||||
actingProvinceId = pid,
|
||||
availableCommands = availableCommands,
|
||||
improvementType = improvementType
|
||||
)
|
||||
}
|
||||
.headOption
|
||||
.map(_.withReason("fulfill TotalDevelopment quest"))
|
||||
}
|
||||
+37
-195
@@ -27,8 +27,6 @@ import net.eagle0.eagle.library.util.ProvinceDistances
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel.Ally
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.hero.Profession
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.quest.{
|
||||
AllianceQuest,
|
||||
@@ -53,17 +51,12 @@ import net.eagle0.eagle.model.state.quest.{
|
||||
ImproveInfrastructureQuest,
|
||||
MobilizeProvincesQuest,
|
||||
Quest,
|
||||
ReconProvincesQuest,
|
||||
ReconSpecificProvincesQuest,
|
||||
ReleaseAllPrisonersQuest,
|
||||
ReleasePrisonerQuest,
|
||||
RepairDevastationQuest,
|
||||
RescueImprisonedLeaderQuest,
|
||||
RestProvinceQuest,
|
||||
ReturnPrisonerQuest,
|
||||
SendSuppliesQuest,
|
||||
SpecificExpansionQuest,
|
||||
SpendOnFeastsQuest,
|
||||
StartBlizzardQuest,
|
||||
StartEpidemicQuest,
|
||||
SuppressRiotByForceQuest,
|
||||
@@ -120,7 +113,6 @@ object QuestCreationUtils {
|
||||
allProvinces: Vector[ProvinceT],
|
||||
factions: Vector[FactionT],
|
||||
battalions: Vector[BattalionT],
|
||||
heroes: Vector[HeroT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[Quest]] =
|
||||
functionalRandom.nextFlatMap(
|
||||
@@ -158,14 +150,9 @@ object QuestCreationUtils {
|
||||
QuestCreatorFrom(winBattleOutnumberedQuests),
|
||||
winBattlesQuests,
|
||||
QuestCreatorFrom(apprehendOutlawQuests),
|
||||
startBlizzardQuests(heroes),
|
||||
startEpidemicQuests(heroes),
|
||||
sendSuppliesQuests,
|
||||
restProvinceQuests,
|
||||
reconProvincesQuests,
|
||||
repairDevastationQuests,
|
||||
spendOnFeastsQuests,
|
||||
reconSpecificProvincesQuests
|
||||
QuestCreatorFrom(startBlizzardQuests),
|
||||
QuestCreatorFrom(startEpidemicQuests),
|
||||
sendSuppliesQuests
|
||||
)
|
||||
) {
|
||||
case (questCreator, fr) =>
|
||||
@@ -922,71 +909,45 @@ object QuestCreationUtils {
|
||||
.filter(_.unaffiliatedHeroType == Outlaw)
|
||||
.map(outlaw => ApprehendOutlawQuest(outlawHeroId = outlaw.heroId))
|
||||
|
||||
private def startBlizzardQuests(heroes: Vector[HeroT]): QuestCreator =
|
||||
(
|
||||
province: ProvinceT,
|
||||
allProvinces: Vector[ProvinceT],
|
||||
_: Vector[FactionT],
|
||||
_: Vector[BattalionT],
|
||||
functionalRandom: FunctionalRandom
|
||||
) => {
|
||||
// Only available if faction has at least one Mage
|
||||
val hasMage = province.rulingFactionHeroIds.exists { heroId =>
|
||||
heroes.find(_.id == heroId).exists(_.profession == Profession.Mage)
|
||||
}
|
||||
private def startBlizzardQuests(
|
||||
province: ProvinceT,
|
||||
allProvinces: Vector[ProvinceT],
|
||||
@unused factions: Vector[FactionT],
|
||||
@unused battalions: Vector[BattalionT]
|
||||
): Vector[Quest] = {
|
||||
// Target provinces in the faction's territory or adjacent to it that don't have a blizzard
|
||||
val factionProvinces = FactionUtils.provinces(province.getRulingFactionId, allProvinces)
|
||||
|
||||
if !hasMage then RandomState(Vector(), functionalRandom)
|
||||
else {
|
||||
// Target provinces in the faction's territory or adjacent to it that don't have a blizzard
|
||||
val factionProvinces = FactionUtils.provinces(province.getRulingFactionId, allProvinces)
|
||||
// Get provinces reachable by Control Weather: faction provinces + neighbors of faction provinces
|
||||
val reachableProvinceIds = factionProvinces.flatMap { fp =>
|
||||
fp.id +: fp.neighbors.map(_.provinceId)
|
||||
}.distinct
|
||||
|
||||
// Get provinces reachable by Control Weather: faction provinces + neighbors of faction provinces
|
||||
val reachableProvinceIds = factionProvinces.flatMap { fp =>
|
||||
fp.id +: fp.neighbors.map(_.provinceId)
|
||||
}.distinct
|
||||
reachableProvinceIds
|
||||
.flatMap(pid => allProvinces.find(_.id == pid))
|
||||
.filterNot(ProvinceUtils.hasBlizzard)
|
||||
.map(targetProvince => StartBlizzardQuest(provinceId = targetProvince.id))
|
||||
}
|
||||
|
||||
val quests = reachableProvinceIds
|
||||
.flatMap(pid => allProvinces.find(_.id == pid))
|
||||
.filterNot(ProvinceUtils.hasBlizzard)
|
||||
.map(targetProvince => StartBlizzardQuest(provinceId = targetProvince.id))
|
||||
private def startEpidemicQuests(
|
||||
province: ProvinceT,
|
||||
allProvinces: Vector[ProvinceT],
|
||||
@unused factions: Vector[FactionT],
|
||||
@unused battalions: Vector[BattalionT]
|
||||
): Vector[Quest] = {
|
||||
// Target provinces in the faction's territory or adjacent to it that don't have an epidemic
|
||||
val factionProvinces = FactionUtils.provinces(province.getRulingFactionId, allProvinces)
|
||||
|
||||
RandomState(quests, functionalRandom)
|
||||
}
|
||||
end if
|
||||
}
|
||||
// Include faction provinces and their neighbors as potential targets
|
||||
val targetProvinceIds = factionProvinces.flatMap { fp =>
|
||||
fp.id +: fp.neighbors.map(_.provinceId)
|
||||
}.distinct
|
||||
|
||||
private def startEpidemicQuests(heroes: Vector[HeroT]): QuestCreator =
|
||||
(
|
||||
province: ProvinceT,
|
||||
allProvinces: Vector[ProvinceT],
|
||||
_: Vector[FactionT],
|
||||
_: Vector[BattalionT],
|
||||
functionalRandom: FunctionalRandom
|
||||
) => {
|
||||
// Only available if faction has at least one Necromancer
|
||||
val hasNecromancer = province.rulingFactionHeroIds.exists { heroId =>
|
||||
heroes.find(_.id == heroId).exists(_.profession == Profession.Necromancer)
|
||||
}
|
||||
|
||||
if !hasNecromancer then RandomState(Vector(), functionalRandom)
|
||||
else {
|
||||
// Target provinces in the faction's territory or adjacent to it that don't have an epidemic
|
||||
val factionProvinces = FactionUtils.provinces(province.getRulingFactionId, allProvinces)
|
||||
|
||||
// Include faction provinces and their neighbors as potential targets
|
||||
val targetProvinceIds = factionProvinces.flatMap { fp =>
|
||||
fp.id +: fp.neighbors.map(_.provinceId)
|
||||
}.distinct
|
||||
|
||||
val quests = targetProvinceIds
|
||||
.flatMap(pid => allProvinces.find(_.id == pid))
|
||||
.filterNot(ProvinceUtils.hasEpidemic)
|
||||
.map(targetProvince => StartEpidemicQuest(provinceId = targetProvince.id))
|
||||
|
||||
RandomState(quests, functionalRandom)
|
||||
}
|
||||
end if
|
||||
}
|
||||
targetProvinceIds
|
||||
.flatMap(pid => allProvinces.find(_.id == pid))
|
||||
.filterNot(ProvinceUtils.hasEpidemic)
|
||||
.map(targetProvince => StartEpidemicQuest(provinceId = targetProvince.id))
|
||||
}
|
||||
|
||||
private def sendSuppliesQuests(
|
||||
province: ProvinceT,
|
||||
@@ -1019,123 +980,4 @@ object QuestCreationUtils {
|
||||
}
|
||||
end if
|
||||
}
|
||||
|
||||
private def restProvinceQuests(
|
||||
province: ProvinceT,
|
||||
allProvinces: Vector[ProvinceT],
|
||||
@unused factions: Vector[FactionT],
|
||||
@unused battalions: Vector[BattalionT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[Quest]] = {
|
||||
// Get all faction provinces as potential targets
|
||||
val factionProvinces = FactionUtils.provinces(province.getRulingFactionId, allProvinces)
|
||||
|
||||
val minMonths = 2
|
||||
val maxMonths = 4
|
||||
|
||||
functionalRandom.nextIntInclusive(minMonths, maxMonths).map { targetMonths =>
|
||||
factionProvinces.map { targetProvince =>
|
||||
RestProvinceQuest(
|
||||
componentCount = targetMonths,
|
||||
componentsFulfilled = 0,
|
||||
targetProvinceId = targetProvince.id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def reconProvincesQuests(
|
||||
@unused province: ProvinceT,
|
||||
@unused allProvinces: Vector[ProvinceT],
|
||||
@unused factions: Vector[FactionT],
|
||||
@unused battalions: Vector[BattalionT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[Quest]] = {
|
||||
val minProvinces = 3
|
||||
val maxProvinces = 6
|
||||
|
||||
functionalRandom.nextIntInclusive(minProvinces, maxProvinces).map { targetCount =>
|
||||
Vector(
|
||||
ReconProvincesQuest(
|
||||
componentCount = targetCount,
|
||||
componentsFulfilled = 0,
|
||||
targetProvinceCount = targetCount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def repairDevastationQuests(
|
||||
@unused province: ProvinceT,
|
||||
@unused allProvinces: Vector[ProvinceT],
|
||||
@unused factions: Vector[FactionT],
|
||||
@unused battalions: Vector[BattalionT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[Quest]] = {
|
||||
val minDevastation = 30
|
||||
val maxDevastation = 100
|
||||
|
||||
functionalRandom.nextIntInclusive(minDevastation, maxDevastation).map { targetAmount =>
|
||||
Vector(
|
||||
RepairDevastationQuest(
|
||||
componentCount = targetAmount,
|
||||
componentsFulfilled = 0,
|
||||
targetDevastationRepaired = targetAmount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def spendOnFeastsQuests(
|
||||
@unused province: ProvinceT,
|
||||
@unused allProvinces: Vector[ProvinceT],
|
||||
@unused factions: Vector[FactionT],
|
||||
@unused battalions: Vector[BattalionT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[Quest]] =
|
||||
functionalRandom.nextIntInclusive(500, 1000).map { totalGold =>
|
||||
Vector(
|
||||
SpendOnFeastsQuest(
|
||||
componentCount = totalGold,
|
||||
componentsFulfilled = 0,
|
||||
totalGold = totalGold
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private def reconSpecificProvincesQuests(
|
||||
province: ProvinceT,
|
||||
allProvinces: Vector[ProvinceT],
|
||||
@unused factions: Vector[FactionT],
|
||||
@unused battalions: Vector[BattalionT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[Vector[Quest]] = {
|
||||
// Find provinces not controlled by this faction (good targets for recon)
|
||||
val nonFactionProvinces = allProvinces
|
||||
.filterNot(_.rulingFactionId.contains(province.getRulingFactionId))
|
||||
|
||||
val minProvinces = 2
|
||||
val maxProvinces = 4
|
||||
|
||||
// Only available if there are enough non-faction provinces
|
||||
if nonFactionProvinces.size < minProvinces then RandomState(Vector(), functionalRandom)
|
||||
else {
|
||||
val cappedMax = Math.min(maxProvinces, nonFactionProvinces.size)
|
||||
functionalRandom
|
||||
.nextIntInclusive(minProvinces, cappedMax)
|
||||
.continue {
|
||||
case (targetCount, fr) =>
|
||||
fr.nextRandomSubset(targetCount, nonFactionProvinces).map { selectedProvinces =>
|
||||
Vector(
|
||||
ReconSpecificProvincesQuest(
|
||||
componentCount = targetCount,
|
||||
componentsFulfilled = 0,
|
||||
targetProvinceIds = selectedProvinces.map(_.id)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
end if
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -32,7 +32,6 @@ object UnaffiliatedHeroUtils {
|
||||
allProvinces: Vector[ProvinceT],
|
||||
factions: Vector[FactionT],
|
||||
battalions: Vector[BattalionT],
|
||||
heroes: Vector[HeroT],
|
||||
functionalRandom: FunctionalRandom
|
||||
): RandomState[RecruitmentInfo] =
|
||||
QuestCreationUtils
|
||||
@@ -41,7 +40,6 @@ object UnaffiliatedHeroUtils {
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = battalions,
|
||||
heroes = heroes,
|
||||
functionalRandom
|
||||
)
|
||||
.continue { case (quests, fr) => fr.nextRandomElementOpt(quests) }
|
||||
|
||||
@@ -37,7 +37,6 @@ import net.eagle0.eagle.common.unaffiliated_hero_quest.{
|
||||
SpecificExpansionQuest as SpecificExpansionQuestProto,
|
||||
SpendOnFeastsQuest as SpendOnFeastsQuestProto,
|
||||
StartBlizzardQuest as StartBlizzardQuestProto,
|
||||
StartDroughtQuest as StartDroughtQuestProto,
|
||||
StartEpidemicQuest as StartEpidemicQuestProto,
|
||||
SuppressRiotByForceQuest as SuppressRiotByForceQuestProto,
|
||||
SwearBrotherhoodWithHeroQuest as SwearBrotherhoodWithHeroQuestProto,
|
||||
@@ -219,8 +218,6 @@ object QuestConverter {
|
||||
QuestProto(details = StartBlizzardQuestProto(q.provinceId))
|
||||
case q: StartEpidemicQuest =>
|
||||
QuestProto(details = StartEpidemicQuestProto(q.provinceId))
|
||||
case q: StartDroughtQuest =>
|
||||
QuestProto(details = StartDroughtQuestProto(q.provinceId))
|
||||
case q: SpendOnFeastsQuest =>
|
||||
QuestProto(
|
||||
details = SpendOnFeastsQuestProto(q.totalGold),
|
||||
@@ -453,8 +450,6 @@ object QuestConverter {
|
||||
StartBlizzardQuest(provinceId)
|
||||
case StartEpidemicQuestProto(provinceId, _ /* unknownFieldSet */ ) =>
|
||||
StartEpidemicQuest(provinceId)
|
||||
case StartDroughtQuestProto(provinceId, _ /* unknownFieldSet */ ) =>
|
||||
StartDroughtQuest(provinceId)
|
||||
case SpendOnFeastsQuestProto(totalGold, _ /* unknownFieldSet */ ) =>
|
||||
SpendOnFeastsQuest(
|
||||
componentCount = questProto.componentCount,
|
||||
|
||||
@@ -173,8 +173,6 @@ case class StartBlizzardQuest(provinceId: ProvinceId) extends Quest
|
||||
|
||||
case class StartEpidemicQuest(provinceId: ProvinceId) extends Quest
|
||||
|
||||
case class StartDroughtQuest(provinceId: ProvinceId) extends Quest
|
||||
|
||||
case class SpendOnFeastsQuest(
|
||||
override val componentCount: Int,
|
||||
override val componentsFulfilled: Int,
|
||||
|
||||
@@ -342,9 +342,7 @@ scala_library(
|
||||
deps = [
|
||||
":llm_update_queuing_proxy",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:functional_random",
|
||||
"//src/main/scala/net/eagle0/common:simple_timed_logger",
|
||||
"//src/main/scala/net/eagle0/common/llm_integration:api_keys",
|
||||
"//src/main/scala/net/eagle0/common/llm_integration:claude_service_impl",
|
||||
"//src/main/scala/net/eagle0/common/llm_integration:external_text_generation_caller",
|
||||
"//src/main/scala/net/eagle0/common/llm_integration:external_text_generation_service_impl",
|
||||
|
||||
@@ -65,12 +65,11 @@ class EagleServiceImpl(
|
||||
): Future[CreateGameResponse] =
|
||||
lockAndUpdateGamesWith(uid =>
|
||||
gamesManager.createGameForUser(
|
||||
expandedGameParameters = GameParametersUtils.parametersFor(request.isTutorialMode),
|
||||
expandedGameParameters = GameParametersUtils.defaultExpandedGameParameters,
|
||||
userId = uid,
|
||||
desiredLeaderTextId = request.desiredLeaderTextId,
|
||||
maxHumanPlayers = request.humanPlayerCount,
|
||||
totalPlayers = request.totalPlayerCount,
|
||||
isTutorialMode = request.isTutorialMode
|
||||
totalPlayers = request.totalPlayerCount
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -68,8 +68,7 @@ case class GameAwaitingPlayers(
|
||||
existingHumanPlayers: Vector[WaitingGamePlayerInfo],
|
||||
totalPlayerCount: Int,
|
||||
humanPlayerCount: Int,
|
||||
greatPeople: Vector[Hero],
|
||||
isTutorialMode: Boolean = false
|
||||
greatPeople: Vector[Hero]
|
||||
) {
|
||||
private def claimedNameTextIds: Vector[String] =
|
||||
existingHumanPlayers.map(_.leaderNameTextId)
|
||||
@@ -86,8 +85,7 @@ case class GameAwaitingPlayers(
|
||||
existingHumanPlayers = existingHumanPlayers :+ waitingGamePlayerInfo,
|
||||
totalPlayerCount = totalPlayerCount,
|
||||
humanPlayerCount = humanPlayerCount,
|
||||
greatPeople = greatPeople,
|
||||
isTutorialMode = isTutorialMode
|
||||
greatPeople = greatPeople
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1106,8 +1104,7 @@ class GamesManager(
|
||||
expandedGameParameters = gap.expandedGameParameters,
|
||||
gameId = newGap.gameId,
|
||||
humanPlayers = newGap.existingHumanPlayers,
|
||||
aiPlayerCount = newGap.totalPlayerCount - newGap.existingHumanPlayers.size,
|
||||
isTutorialMode = newGap.isTutorialMode
|
||||
aiPlayerCount = newGap.totalPlayerCount - newGap.existingHumanPlayers.size
|
||||
): Unit
|
||||
} else
|
||||
gamesAwaitingPlayers = gamesAwaitingPlayers
|
||||
@@ -1146,8 +1143,7 @@ class GamesManager(
|
||||
userId: UserId,
|
||||
desiredLeaderTextId: String,
|
||||
maxHumanPlayers: Int,
|
||||
totalPlayers: Int,
|
||||
isTutorialMode: Boolean = false
|
||||
totalPlayers: Int
|
||||
): CreateGameResponse =
|
||||
if maxHumanPlayers > totalPlayers || totalPlayers > MaxSupportedPlayers.intValue
|
||||
then CreateGameResponse(INVALID_OPTIONS_RESULT, 0)
|
||||
@@ -1160,8 +1156,7 @@ class GamesManager(
|
||||
existingHumanPlayers = Vector.empty,
|
||||
totalPlayerCount = totalPlayers,
|
||||
humanPlayerCount = maxHumanPlayers,
|
||||
greatPeople = expandedGameParameters.greatPeopleHeroProtos,
|
||||
isTutorialMode = isTutorialMode
|
||||
greatPeople = expandedGameParameters.greatPeopleHeroProtos
|
||||
)
|
||||
|
||||
val result = joinGame(
|
||||
@@ -1214,8 +1209,7 @@ class GamesManager(
|
||||
expandedGameParameters: ExpandedGameParameters,
|
||||
gameId: GameId,
|
||||
humanPlayers: Vector[WaitingGamePlayerInfo],
|
||||
aiPlayerCount: Int,
|
||||
isTutorialMode: Boolean = false
|
||||
aiPlayerCount: Int
|
||||
): GameControllerWithPostResults = {
|
||||
val humanPlayerCount = humanPlayers.size
|
||||
val initialEar = gameCreation
|
||||
@@ -1225,8 +1219,7 @@ class GamesManager(
|
||||
persister = gamePersisterCreation.persisterForGame(gameId),
|
||||
humanPlayerFactionLeaderNameTextIds = humanPlayers.map(_.leaderNameTextId),
|
||||
playerCount = humanPlayerCount + aiPlayerCount,
|
||||
allBattalionTypes = GamesManager.battalionTypes,
|
||||
isTutorialMode = isTutorialMode
|
||||
allBattalionTypes = GamesManager.battalionTypes
|
||||
)
|
||||
|
||||
val unrequestedTexts = initialEar.results
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
package net.eagle0.eagle.service
|
||||
|
||||
import java.io.FileWriter
|
||||
import java.time.{Duration, Instant}
|
||||
import java.util.function.Consumer
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
import io.sentry.Sentry
|
||||
import net.eagle0.common.{FunctionalRandom, RandomState, SimpleTimedLogger}
|
||||
import net.eagle0.common.llm_integration.{
|
||||
ApiKeys,
|
||||
ClaudeServiceImpl,
|
||||
ExternalTextGenerationCaller,
|
||||
ExternalTextGenerationError,
|
||||
GeminiServiceImpl,
|
||||
OpenAIChatCompletionsServiceImpl,
|
||||
StreamingTextResults
|
||||
OpenAIChatCompletionsServiceImpl
|
||||
}
|
||||
import net.eagle0.eagle.client_text.{
|
||||
ClientTextStore,
|
||||
@@ -93,39 +87,17 @@ object LlmResolver {
|
||||
gameState: GameState,
|
||||
partialCompletion: Option[String] = None
|
||||
)
|
||||
|
||||
/** Provider health tracking for circuit breaker pattern */
|
||||
case class ProviderHealth(
|
||||
consecutiveFailures: Int = 0,
|
||||
unhealthySince: Option[Instant] = None
|
||||
) {
|
||||
private val unhealthyDurationMinutes = 5
|
||||
private val failureThreshold = 3
|
||||
|
||||
def isHealthy: Boolean = unhealthySince.forall { since =>
|
||||
Duration.between(since, Instant.now()).toMinutes >= unhealthyDurationMinutes
|
||||
}
|
||||
|
||||
def recordFailure: ProviderHealth = {
|
||||
val newFailures = consecutiveFailures + 1
|
||||
if newFailures >= failureThreshold then ProviderHealth(newFailures, Some(Instant.now()))
|
||||
else ProviderHealth(newFailures, unhealthySince)
|
||||
}
|
||||
|
||||
def recordSuccess: ProviderHealth = ProviderHealth(0, None)
|
||||
}
|
||||
}
|
||||
|
||||
class LlmResolver(updateReceiver: LlmUpdateQueuingProxy, isGameValid: GameId => Boolean) {
|
||||
import LlmResolver.{LlmRequestWithGameState, ProviderHealth}
|
||||
import LlmResolver.LlmRequestWithGameState
|
||||
|
||||
implicit val ec: ExecutionContext = ExecutionContext.global
|
||||
|
||||
private val callerCount = 2
|
||||
|
||||
/** Create callers for a specific provider */
|
||||
private def createCallersForProvider(provider: String): Vector[ExternalTextGenerationCaller] =
|
||||
provider.toLowerCase match {
|
||||
private def createLlmCallers(): Vector[ExternalTextGenerationCaller] =
|
||||
LlmProvider.stringValue.toLowerCase match {
|
||||
case "openai" =>
|
||||
(1 to callerCount).map { _ =>
|
||||
new ExternalTextGenerationCaller(
|
||||
@@ -144,102 +116,17 @@ class LlmResolver(updateReceiver: LlmUpdateQueuingProxy, isGameValid: GameId =>
|
||||
serviceImpl = new GeminiServiceImpl(defaultModelName = GeminiModelName.stringValue)
|
||||
)
|
||||
}.toVector
|
||||
case other =>
|
||||
throw new IllegalArgumentException(s"Unknown LLM provider: $other")
|
||||
case provider =>
|
||||
throw new IllegalArgumentException(s"Unknown LLM provider: $provider")
|
||||
}
|
||||
|
||||
/** Create callers for all available providers (not just primary) */
|
||||
private def createAllProviderCallers(): Map[String, Vector[ExternalTextGenerationCaller]] = {
|
||||
val available = ApiKeys.availableProviders
|
||||
SimpleTimedLogger.printLogger.logLine(s"[LLM] Available providers: ${available.mkString(", ")}")
|
||||
available.flatMap { provider =>
|
||||
scala.util.Try(createCallersForProvider(provider)) match {
|
||||
case scala.util.Success(callers) =>
|
||||
SimpleTimedLogger.printLogger.logLine(s"[LLM] Created $callerCount callers for provider: $provider")
|
||||
Some(provider -> callers)
|
||||
case scala.util.Failure(ex) =>
|
||||
SimpleTimedLogger.printLogger.logLine(s"[LLM] Failed to create callers for $provider: ${ex.getMessage}")
|
||||
None
|
||||
}
|
||||
}.toMap
|
||||
}
|
||||
|
||||
// Multi-provider caller map
|
||||
@volatile private var _providerCallersInitialized: Boolean = false
|
||||
@volatile private var _providerCallers: Map[String, Vector[ExternalTextGenerationCaller]] = Map.empty
|
||||
|
||||
private def providerCallers: Map[String, Vector[ExternalTextGenerationCaller]] = {
|
||||
if !_providerCallersInitialized then {
|
||||
synchronized {
|
||||
if !_providerCallersInitialized then {
|
||||
_providerCallers = createAllProviderCallers()
|
||||
_providerCallersInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
_providerCallers
|
||||
}
|
||||
|
||||
// Provider health tracking
|
||||
@volatile private var _providerHealth: Map[String, ProviderHealth] = Map.empty
|
||||
|
||||
private def providerHealth: Map[String, ProviderHealth] = synchronized {
|
||||
if _providerHealth.isEmpty && providerCallers.nonEmpty then {
|
||||
_providerHealth = providerCallers.keys.map(_ -> ProviderHealth()).toMap
|
||||
}
|
||||
_providerHealth
|
||||
}
|
||||
|
||||
private def recordProviderFailure(provider: String): Unit = synchronized {
|
||||
val current = providerHealth.getOrElse(provider, ProviderHealth())
|
||||
val updated = current.recordFailure
|
||||
_providerHealth = providerHealth.updated(provider, updated)
|
||||
if !updated.isHealthy then {
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[LLM] Provider $provider marked unhealthy after ${updated.consecutiveFailures} consecutive failures"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def recordProviderSuccess(provider: String): Unit = synchronized {
|
||||
val current = providerHealth.getOrElse(provider, ProviderHealth())
|
||||
if current.consecutiveFailures > 0 || current.unhealthySince.isDefined then {
|
||||
SimpleTimedLogger.printLogger.logLine(s"[LLM] Provider $provider recovered")
|
||||
}
|
||||
_providerHealth = providerHealth.updated(provider, ProviderHealth())
|
||||
}
|
||||
|
||||
/** Get callers for healthy providers, primary first */
|
||||
private def getHealthyCallers: Vector[(String, ExternalTextGenerationCaller)] = {
|
||||
val primary = LlmProvider.stringValue.toLowerCase
|
||||
val orderedProviders = (primary +: ApiKeys.availableProviders.filterNot(_ == primary)).distinct
|
||||
|
||||
orderedProviders
|
||||
.filter(p => providerHealth.get(p).forall(_.isHealthy))
|
||||
.flatMap(p => providerCallers.get(p).toVector.flatten.map(c => (p, c)))
|
||||
}
|
||||
|
||||
/** Select a caller with capacity, preferring healthy providers */
|
||||
private def selectCaller: Option[(String, ExternalTextGenerationCaller)] =
|
||||
getHealthyCallers.filter { case (_, c) => c.getInProgressCount < c.maxConcurrentStreams }.minByOption {
|
||||
case (_, c) => c.getInProgressCount
|
||||
}
|
||||
|
||||
/** Select a caller from a provider other than the excluded one */
|
||||
private def selectCallerExcluding(excludeProvider: String): Option[(String, ExternalTextGenerationCaller)] =
|
||||
getHealthyCallers.filter {
|
||||
case (p, c) => p != excludeProvider && c.getInProgressCount < c.maxConcurrentStreams
|
||||
}.minByOption { case (_, c) => c.getInProgressCount }
|
||||
|
||||
// Legacy accessor for compatibility
|
||||
@volatile private var _llmCallersInitialized: Boolean = false
|
||||
@volatile private var _llmCallers: Vector[ExternalTextGenerationCaller] = Vector.empty
|
||||
def llmCallers: Vector[ExternalTextGenerationCaller] = {
|
||||
if !_llmCallersInitialized then {
|
||||
synchronized {
|
||||
if !_llmCallersInitialized then {
|
||||
// Use the primary provider's callers for backward compatibility
|
||||
_llmCallers = providerCallers.getOrElse(LlmProvider.stringValue.toLowerCase, Vector.empty)
|
||||
_llmCallers = createLlmCallers()
|
||||
_llmCallersInitialized = true
|
||||
}
|
||||
}
|
||||
@@ -248,12 +135,8 @@ class LlmResolver(updateReceiver: LlmUpdateQueuingProxy, isGameValid: GameId =>
|
||||
}
|
||||
|
||||
def recreateLlmCallers(): Unit = synchronized {
|
||||
_providerCallers = createAllProviderCallers()
|
||||
_providerCallersInitialized = true
|
||||
_providerHealth = _providerCallers.keys.map(_ -> ProviderHealth()).toMap
|
||||
_llmCallers = _providerCallers.getOrElse(LlmProvider.stringValue.toLowerCase, Vector.empty)
|
||||
_llmCallers = createLlmCallers()
|
||||
_llmCallersInitialized = true
|
||||
SimpleTimedLogger.printLogger.logLine("[LLM] Recreated callers for all providers")
|
||||
}
|
||||
|
||||
lazy private val fileWriter =
|
||||
@@ -350,73 +233,39 @@ class LlmResolver(updateReceiver: LlmUpdateQueuingProxy, isGameValid: GameId =>
|
||||
)
|
||||
}
|
||||
|
||||
val streamingConsumer: Consumer[StreamingTextResults] = streamingResults =>
|
||||
updateReceiver.receiveStreamingLlmResponse(
|
||||
streamId = streamingResults.streamId,
|
||||
llmRequest = llmRequest,
|
||||
responseText = streamingResults.value,
|
||||
completed = streamingResults.completed
|
||||
)
|
||||
|
||||
/** Try to complete the request with a specific provider, with optional failover */
|
||||
def tryWithProvider(
|
||||
providerName: String,
|
||||
caller: ExternalTextGenerationCaller,
|
||||
excludedProviders: Set[String]
|
||||
): Future[Unit] =
|
||||
caller
|
||||
.streamCompletion(
|
||||
inputText = prompt,
|
||||
partialCompletion = partialCompletion,
|
||||
streamingConsumer = streamingConsumer
|
||||
)
|
||||
.transform {
|
||||
case Success(result) =>
|
||||
recordProviderSuccess(providerName)
|
||||
Success(result)
|
||||
case Failure(e: ExternalTextGenerationError.ServerError) =>
|
||||
recordProviderFailure(providerName)
|
||||
Failure(e)
|
||||
case Failure(e) => Failure(e)
|
||||
}
|
||||
.recoverWith {
|
||||
case e: ExternalTextGenerationError.ServerError =>
|
||||
// Try failover to another provider
|
||||
val newExcluded = excludedProviders + providerName
|
||||
selectCallerExcluding(providerName) match {
|
||||
case Some((nextProvider, nextCaller)) if !newExcluded.contains(nextProvider) =>
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[LLM] Provider $providerName returned ${e.code}, failing over to $nextProvider"
|
||||
)
|
||||
tryWithProvider(nextProvider, nextCaller, newExcluded)
|
||||
case _ =>
|
||||
// No more providers to try, propagate the error
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[LLM] Provider $providerName returned ${e.code}, no fallback providers available"
|
||||
)
|
||||
Future.failed(e)
|
||||
}
|
||||
}
|
||||
|
||||
selectCaller.map {
|
||||
case (providerName, chosenCaller) =>
|
||||
llmCallers
|
||||
.filter(x => x.getInProgressCount < x.maxConcurrentStreams)
|
||||
.minByOption(_.getInProgressCount)
|
||||
.map { chosenCaller =>
|
||||
log(llmRequest, prompt)
|
||||
|
||||
LlmResolverRequested {
|
||||
tryWithProvider(providerName, chosenCaller, Set.empty).recover {
|
||||
case ex =>
|
||||
// Log the failure for debugging and send to Sentry
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"LLM processing failed for request ${llmRequest.requestId}: $ex"
|
||||
)
|
||||
ex.printStackTrace()
|
||||
Sentry.captureException(ex)
|
||||
// Handle failure by moving request back to unrequested state for retry
|
||||
updateReceiver.receiveStreamingLlmFailure(llmRequest)
|
||||
throw ex // Re-throw to maintain error semantics
|
||||
}
|
||||
chosenCaller
|
||||
.streamCompletion(
|
||||
inputText = prompt,
|
||||
partialCompletion = partialCompletion,
|
||||
streamingConsumer = streamingResults =>
|
||||
updateReceiver.receiveStreamingLlmResponse(
|
||||
streamId = streamingResults.streamId,
|
||||
llmRequest = llmRequest,
|
||||
responseText = streamingResults.value,
|
||||
completed = streamingResults.completed
|
||||
)
|
||||
)
|
||||
.recover {
|
||||
case ex =>
|
||||
// Log the failure for debugging and send to Sentry
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"LLM processing failed for request ${llmRequest.requestId}: $ex"
|
||||
)
|
||||
ex.printStackTrace()
|
||||
Sentry.captureException(ex)
|
||||
// Handle failure by moving request back to unrequested state for retry
|
||||
updateReceiver.receiveStreamingLlmFailure(llmRequest)
|
||||
throw ex // Re-throw to maintain error semantics
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.getOrElse(LlmResolverTooManyRequestsInFlight)
|
||||
case TextGenerationDependencyInProgress(
|
||||
notSatisfiedTextId,
|
||||
|
||||
@@ -665,8 +665,9 @@ final case class GameController(
|
||||
streamingTextStatuses: Vector[StreamingTextStatus],
|
||||
responseObserver: SyncResponseObserver
|
||||
): GameController =
|
||||
userIdToFactionId.get(userId) match {
|
||||
case Some(fid) =>
|
||||
userIdToFactionId
|
||||
.get(userId)
|
||||
.map { fid =>
|
||||
this.withHumanClient(
|
||||
clientTextStore.completeTexts.values
|
||||
.filter(ct =>
|
||||
@@ -706,17 +707,8 @@ final case class GameController(
|
||||
client.update(clientText = clientText)
|
||||
}
|
||||
)
|
||||
case None =>
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"streamUpdates: userId $userId not found in game $gameId - client should reconnect"
|
||||
)
|
||||
responseObserver.onError(
|
||||
Status.UNAUTHENTICATED
|
||||
.withDescription("User not found in game - please reconnect")
|
||||
.asRuntimeException()
|
||||
)
|
||||
this
|
||||
}
|
||||
}
|
||||
.get
|
||||
|
||||
def stopStreaming(userId: UserId): GameController =
|
||||
GameController(
|
||||
|
||||
@@ -33,9 +33,6 @@ object GameParametersUtils {
|
||||
private val defaultResourceLocation: String =
|
||||
"/net/eagle0/eagle/game_parameters.json"
|
||||
|
||||
private val tutorialResourceLocation: String =
|
||||
"/net/eagle0/eagle/tutorial_game_parameters.json"
|
||||
|
||||
private val fixedHeroFilePath: String = "/net/eagle0/eagle/heroes.tsv"
|
||||
private val generatedHeroFilePath: String =
|
||||
"/net/eagle0/eagle/generated_heroes.tsv"
|
||||
@@ -77,12 +74,6 @@ object GameParametersUtils {
|
||||
val defaultExpandedGameParameters: ExpandedGameParameters =
|
||||
expandedGameParametersFrom(defaultResourceLocation).get
|
||||
|
||||
lazy val tutorialExpandedGameParameters: ExpandedGameParameters =
|
||||
expandedGameParametersFrom(tutorialResourceLocation).get
|
||||
|
||||
def parametersFor(isTutorialMode: Boolean): ExpandedGameParameters =
|
||||
if isTutorialMode then tutorialExpandedGameParameters else defaultExpandedGameParameters
|
||||
|
||||
private def heroNamesMap(fixedHeroes: FixedHeroes): Map[String, String] = {
|
||||
val heroes = fixedHeroes.greatPeople.map(
|
||||
_.greatPerson.hero
|
||||
|
||||
@@ -21,13 +21,11 @@ import net.eagle0.eagle.model.action_result.generated_text_request.{
|
||||
}
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType
|
||||
import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
import net.eagle0.eagle.model.state.{Army, CombatUnit, HostileArmyGroup, HostileArmyGroupStatus, MovingArmy, Supplies}
|
||||
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
|
||||
import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship.RelationshipLevel.Hostile
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.quest.DefeatFactionQuest
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.concrete.UnaffiliatedHeroC
|
||||
import net.eagle0.eagle.model.state.BattalionType
|
||||
@@ -65,8 +63,7 @@ trait NewGameCreation {
|
||||
persister: Persister,
|
||||
humanPlayerFactionLeaderNameTextIds: Vector[String],
|
||||
playerCount: Int,
|
||||
allBattalionTypes: Vector[BattalionType],
|
||||
isTutorialMode: Boolean = false
|
||||
allBattalionTypes: Vector[BattalionType]
|
||||
): EngineAndResults
|
||||
}
|
||||
|
||||
@@ -267,23 +264,20 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
expandedGameParameters: ExpandedGameParameters,
|
||||
headName: String,
|
||||
provinceId: ProvinceId,
|
||||
gameId: GameId,
|
||||
isTutorialMode: Boolean = false
|
||||
gameId: GameId
|
||||
): RandomState[ActionResultC] =
|
||||
withFactionHead(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
headName = headName,
|
||||
provinceId = provinceId,
|
||||
gameId = gameId,
|
||||
isTutorialMode = isTutorialMode
|
||||
gameId = gameId
|
||||
)
|
||||
|
||||
def withHumanPlayerFactions(
|
||||
expandedGameParameters: ExpandedGameParameters,
|
||||
factionHeadNameTextIds: Vector[String],
|
||||
provinceIds: Vector[ProvinceId],
|
||||
gameId: GameId,
|
||||
isTutorialMode: Boolean = false
|
||||
gameId: GameId
|
||||
): RandomState[ActionResultC] =
|
||||
factionHeadNameTextIds
|
||||
.map(nameTextId =>
|
||||
@@ -301,8 +295,7 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
headName = name,
|
||||
provinceId = pid,
|
||||
gameId = gameId,
|
||||
isTutorialMode = isTutorialMode
|
||||
gameId = gameId
|
||||
)
|
||||
}
|
||||
|
||||
@@ -310,8 +303,7 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
expandedGameParameters: ExpandedGameParameters,
|
||||
headName: String,
|
||||
provinceId: ProvinceId,
|
||||
gameId: GameId,
|
||||
isTutorialMode: Boolean = false
|
||||
gameId: GameId
|
||||
): RandomState[ActionResultC] = {
|
||||
val fid = value.newValue.nextFactionId
|
||||
|
||||
@@ -323,10 +315,6 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
greatPerson = gp.greatPerson.copy(hero = gp.greatPerson.hero.withFactionId(fid))
|
||||
)
|
||||
|
||||
// Tutorial mode: 4 heroes (faction head + 3 sworn brothers)
|
||||
// Normal mode: uses randomRulingPlayerHeroCount from occupiedProvinceOverrides (default 2)
|
||||
val tutorialExtraHeroCount = if isTutorialMode then 3 else 0
|
||||
|
||||
value
|
||||
.map(_.withLoadedHero(gpWithFid.greatPerson.hero.withFactionId(fid)))
|
||||
.map {
|
||||
@@ -342,22 +330,14 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
)
|
||||
} match {
|
||||
case RandomState((ar, hid), nr) =>
|
||||
// In tutorial mode, add extra sworn brothers for a total of 4 heroes
|
||||
val withExtraTutorialHeroes =
|
||||
if isTutorialMode then RandomState(ar, nr).withRandomHeroes(fid, gameId, tutorialExtraHeroCount)
|
||||
else RandomState((ar, Vector[HeroId]()), nr)
|
||||
|
||||
withExtraTutorialHeroes.continue {
|
||||
case ((arWithHeroes, extraHids), nextRandom) =>
|
||||
RandomState(arWithHeroes, nextRandom).withProvinceOverrides(
|
||||
province = value.newValue.provinceMap(provinceId),
|
||||
provinceOverrides = expandedGameParameters.gameParameters.getOccupiedProvinceOverrides,
|
||||
extraHeroIds = Vector(hid) ++ extraHids,
|
||||
factionId = fid,
|
||||
battalionName = Some(s"$headName's Loyalists"),
|
||||
gameId = gameId
|
||||
)
|
||||
}
|
||||
RandomState(ar, nr).withProvinceOverrides(
|
||||
province = value.newValue.provinceMap(provinceId),
|
||||
provinceOverrides = expandedGameParameters.gameParameters.getOccupiedProvinceOverrides,
|
||||
extraHeroIds = Vector(hid),
|
||||
factionId = fid,
|
||||
battalionName = Some(s"$headName's Loyalists"),
|
||||
gameId = gameId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,115 +533,13 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
)
|
||||
}
|
||||
|
||||
// Tutorial mode constants
|
||||
private val tutorialPlayerStartProvinceId: ProvinceId = 14 // Onmaa
|
||||
private val tutorialRivalFactionId: FactionId = 2 // Norfolk's faction
|
||||
|
||||
/// Sets up a tutorial battle by adding a hostile army group with Attacking status.
|
||||
/// When the round advances to BattleRequest phase, the battle will be automatically created.
|
||||
private def setupTutorialBattle(
|
||||
ar: ActionResultC,
|
||||
attackerFactionHeadName: String,
|
||||
targetProvinceId: ProvinceId
|
||||
): ActionResultC = {
|
||||
// Find the attacker faction by their faction head's name (using nameTextId)
|
||||
val attackerHero = ar.newHeroes.find(_.nameTextId == attackerFactionHeadName)
|
||||
val attackerFactionId = attackerHero.flatMap(_.factionId)
|
||||
|
||||
// Find the defender (player) faction - should be the ruling faction of the target province
|
||||
val targetProvince = ar.newProvinces.find(_.id == targetProvinceId)
|
||||
val defenderFactionId = targetProvince.flatMap(_.rulingFactionId)
|
||||
|
||||
(attackerFactionId, defenderFactionId, targetProvince) match {
|
||||
case (Some(attackerFid), Some(defenderFid), Some(province)) =>
|
||||
// Get attacker heroes and their battalions
|
||||
val attackerHeroes = ar.newHeroes.filter(_.factionId.contains(attackerFid))
|
||||
val attackerUnits = attackerHeroes.map { hero =>
|
||||
// Find battalions for this hero (at their province)
|
||||
val heroBattalionIds = ar.newProvinces
|
||||
.filter(_.rulingFactionId.contains(attackerFid))
|
||||
.flatMap(_.battalionIds)
|
||||
CombatUnit(
|
||||
factionId = attackerFid,
|
||||
heroId = hero.id,
|
||||
battalionId = heroBattalionIds.headOption
|
||||
)
|
||||
}
|
||||
|
||||
// Get defender heroes and their battalions
|
||||
val defenderHeroes = ar.newHeroes.filter(_.factionId.contains(defenderFid))
|
||||
val defenderUnits = defenderHeroes.map { hero =>
|
||||
val heroBattalionIds = province.battalionIds
|
||||
CombatUnit(
|
||||
factionId = defenderFid,
|
||||
heroId = hero.id,
|
||||
battalionId = heroBattalionIds.headOption
|
||||
)
|
||||
}
|
||||
|
||||
// Find the origin province (attacker's province)
|
||||
val attackerProvince = ar.newProvinces
|
||||
.find(_.rulingFactionId.contains(attackerFid))
|
||||
.map(_.id)
|
||||
.getOrElse(32) // Default to province 32 if not found
|
||||
|
||||
// Get starting position index from neighbor info
|
||||
val startingPositionIndex = province.neighbors
|
||||
.find(_.provinceId == attackerProvince)
|
||||
.map(_.startingPositionIndex)
|
||||
.getOrElse(0)
|
||||
|
||||
// Create hostile army group with Attacking status (bypasses decision phase)
|
||||
val hostileArmyGroup = HostileArmyGroup(
|
||||
factionId = attackerFid,
|
||||
armies = Vector(
|
||||
MovingArmy(
|
||||
id = 1,
|
||||
army = Army(
|
||||
factionId = attackerFid,
|
||||
units = attackerUnits,
|
||||
fleeProvinceId = Some(attackerProvince)
|
||||
),
|
||||
arrivalRound = 1,
|
||||
destinationProvinceId = targetProvinceId,
|
||||
originProvinceId = attackerProvince,
|
||||
supplies = Supplies(gold = 500, food = 1000),
|
||||
suppliesLoss = 0.0,
|
||||
startingPositionIndex = Some(startingPositionIndex)
|
||||
)
|
||||
),
|
||||
status = HostileArmyGroupStatus.Attacking
|
||||
)
|
||||
|
||||
// Create defending army for the player
|
||||
val defendingArmy = Army(
|
||||
factionId = defenderFid,
|
||||
units = defenderUnits,
|
||||
fleeProvinceId = None
|
||||
)
|
||||
|
||||
// Add the hostile army and defending army to the province
|
||||
val updatedProvince = province.updateWith(
|
||||
hostileArmies = province.hostileArmies :+ hostileArmyGroup,
|
||||
defendingArmy = Some(defendingArmy)
|
||||
)
|
||||
|
||||
ar.withProvince(updatedProvince)
|
||||
|
||||
case _ =>
|
||||
// If we can't find the factions or province, return unchanged
|
||||
ar
|
||||
}
|
||||
}
|
||||
|
||||
override def createGame(
|
||||
expandedGameParameters: ExpandedGameParameters,
|
||||
gameId: GameId,
|
||||
persister: Persister,
|
||||
humanPlayerFactionLeaderNameTextIds: Vector[String],
|
||||
playerCount: Int,
|
||||
allBattalionTypes: Vector[BattalionType],
|
||||
isTutorialMode: Boolean = false
|
||||
allBattalionTypes: Vector[BattalionType]
|
||||
): EngineAndResults = {
|
||||
val gameParameters = expandedGameParameters.gameParameters
|
||||
|
||||
@@ -676,22 +554,13 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
gameId = gameId
|
||||
)
|
||||
|
||||
// In tutorial mode, player spawns at province 14 (Onmaa) instead of random
|
||||
val splitPids = setFactionsResult.continue {
|
||||
case (sfr, nextRandom) =>
|
||||
if isTutorialMode then {
|
||||
// Tutorial: force player to province 14, no AI provinces needed (they're all set factions)
|
||||
RandomState(
|
||||
(Vector(tutorialPlayerStartProvinceId), Vector[ProvinceId]()),
|
||||
nextRandom
|
||||
)
|
||||
} else {
|
||||
randomProvinceIdSet(
|
||||
sfr.newProvinces.toVector,
|
||||
playerCount,
|
||||
nextRandom
|
||||
).map(_.splitAt(humanPlayerFactionLeaderNameTextIds.size))
|
||||
}
|
||||
randomProvinceIdSet(
|
||||
sfr.newProvinces.toVector,
|
||||
playerCount,
|
||||
nextRandom
|
||||
).map(_.splitAt(humanPlayerFactionLeaderNameTextIds.size))
|
||||
}
|
||||
|
||||
val allFactionsResult = splitPids.continue {
|
||||
@@ -701,8 +570,7 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
factionHeadNameTextIds = humanPlayerFactionLeaderNameTextIds,
|
||||
provinceIds = humanProvinceIds,
|
||||
gameId = gameId,
|
||||
isTutorialMode = isTutorialMode
|
||||
gameId = gameId
|
||||
)
|
||||
.withOtherAiFactions(
|
||||
expandedGameParameters = expandedGameParameters,
|
||||
@@ -799,63 +667,8 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
)
|
||||
}
|
||||
|
||||
// In tutorial mode, add an unaffiliated hero with DefeatFactionQuest to player's starting province
|
||||
val withTutorialQuestHeroState =
|
||||
if isTutorialMode then {
|
||||
val currentAR = provincesWithExtraHeroesState.newValue
|
||||
val nextRandom = provincesWithExtraHeroesState.nextRandom
|
||||
val questHeroId = currentAR.nextHeroId
|
||||
|
||||
// Generate a random hero for the quest giver
|
||||
val (heroGenResult, newRandom) = RandomHeroGenerator
|
||||
.createHeroesWithNoProfession(
|
||||
hids = Vector(questHeroId),
|
||||
functionalRandom = nextRandom,
|
||||
remainingImagePathsByProfession = (p, g) => ImagePaths.imagePathsByProfession((p, g)),
|
||||
date = StartDate.startDate
|
||||
) match {
|
||||
case RandomState(result, nr) => (result, nr)
|
||||
}
|
||||
|
||||
val questHero = heroGenResult.head match {
|
||||
case HeroGenerationResponse(hero, nameInfo) =>
|
||||
(
|
||||
hero,
|
||||
NameGenerationRequestCreator.nameGenerationRequest(
|
||||
heroId = hero.id,
|
||||
gameId = gameId,
|
||||
nameInfo = nameInfo
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Create the unaffiliated hero with DefeatFactionQuest
|
||||
val tutorialQuestHero = UnaffiliatedHeroC(
|
||||
heroId = questHeroId,
|
||||
unaffiliatedHeroType = UnaffiliatedHeroType.Resident,
|
||||
recruitmentInfo = RecruitmentInfo.HasQuest(
|
||||
DefeatFactionQuest(targetFactionId = tutorialRivalFactionId)
|
||||
)
|
||||
)
|
||||
|
||||
// Add the hero to the player's starting province
|
||||
val playerProvince = currentAR.provinceMap(tutorialPlayerStartProvinceId)
|
||||
val updatedProvince = playerProvince.updateWith(
|
||||
unaffiliatedHeroes = playerProvince.unaffiliatedHeroes :+ tutorialQuestHero
|
||||
)
|
||||
|
||||
val updatedAR = currentAR
|
||||
.withHeroes(Vector(questHero._1))
|
||||
.withGeneratedTextRequests(questHero._2.map(convertNameRequest).toVector)
|
||||
.withProvince(updatedProvince)
|
||||
|
||||
RandomState(updatedAR, newRandom)
|
||||
} else {
|
||||
provincesWithExtraHeroesState
|
||||
}
|
||||
|
||||
val withBattalionTypesResult =
|
||||
withTutorialQuestHeroState.newValue.copy(
|
||||
provincesWithExtraHeroesState.newValue.copy(
|
||||
newBattalionTypes = allBattalionTypes
|
||||
)
|
||||
|
||||
@@ -903,20 +716,8 @@ object FixedNewGameCreation extends NewGameCreation {
|
||||
newHeroes = withBattalionTypesResult.newHeroes.sortBy(_.id)
|
||||
)
|
||||
|
||||
// Set up tutorial battle if configured
|
||||
val withTutorialBattle =
|
||||
if isTutorialMode then
|
||||
gameParameters.tutorialBattle.fold(withPersonalityRequests) { tutorialBattle =>
|
||||
setupTutorialBattle(
|
||||
ar = withPersonalityRequests,
|
||||
attackerFactionHeadName = tutorialBattle.attackerFactionHead,
|
||||
targetProvinceId = tutorialBattle.targetProvinceId
|
||||
)
|
||||
}
|
||||
else withPersonalityRequests
|
||||
|
||||
createEngine(
|
||||
startGameActionResult = withTutorialBattle,
|
||||
startGameActionResult = withPersonalityRequests,
|
||||
heroGenerator = fixedHeroesARwithGenerator.newValue._2,
|
||||
gameId = gameId,
|
||||
persister = persister
|
||||
|
||||
+10
-14
@@ -134,20 +134,16 @@ class ShardokInterfaceGrpcClient(
|
||||
val _ = Future {
|
||||
Thread.sleep(delayMs)
|
||||
pendingBattles.synchronized {
|
||||
pendingBattles.get(shardokGameId) match {
|
||||
case Some((_, storedRequest)) =>
|
||||
println(s"Reconnecting stream for $shardokGameId (delay was ${delayMs}ms)")
|
||||
try subscribeToGame(eagleGameId, shardokGameId, Some(storedRequest))
|
||||
catch {
|
||||
case e: Exception =>
|
||||
println(s"Reconnect failed for $shardokGameId: $e")
|
||||
// Exponential backoff with max 10 seconds
|
||||
val nextDelay = Math.min(delayMs * 2, 10000)
|
||||
scheduleReconnect(eagleGameId, shardokGameId, nextDelay)
|
||||
}
|
||||
case None =>
|
||||
// Battle was completed or cancelled while waiting to reconnect
|
||||
}
|
||||
if pendingBattles.contains(shardokGameId) then
|
||||
println(s"Reconnecting stream for $shardokGameId (delay was ${delayMs}ms)")
|
||||
try subscribeToGame(eagleGameId, shardokGameId, None)
|
||||
catch {
|
||||
case e: Exception =>
|
||||
println(s"Reconnect failed for $shardokGameId: $e")
|
||||
// Exponential backoff with max 10 seconds
|
||||
val nextDelay = Math.min(delayMs * 2, 10000)
|
||||
scheduleReconnect(eagleGameId, shardokGameId, nextDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,11 @@ cc_library(
|
||||
)
|
||||
|
||||
# Test for AbstractMCTSAI algorithm
|
||||
# Disabled: MCTS is not currently in use
|
||||
cc_test(
|
||||
name = "abstract_mcts_ai_test",
|
||||
srcs = ["AbstractMCTSAI_test.cpp"],
|
||||
copts = TEST_COPTS,
|
||||
linkstatic = True,
|
||||
tags = ["manual"],
|
||||
deps = [
|
||||
":mock_tictactoe",
|
||||
"//src/main/cpp/net/eagle0/common/mcts/abstract:abstract_mcts_ai",
|
||||
@@ -63,6 +61,7 @@ cc_test(
|
||||
test_suite(
|
||||
name = "all_abstract_tests",
|
||||
tests = [
|
||||
":abstract_mcts_ai_test",
|
||||
":mcts_integration_test",
|
||||
":mcts_node_test",
|
||||
],
|
||||
|
||||
+6
-7
@@ -60,8 +60,7 @@ class AvailableFeastCommandFactoryTest extends AnyFlatSpec with Matchers with Be
|
||||
) shouldBe empty
|
||||
}
|
||||
|
||||
"availableCommand" should "be available even if everybody has maxed loyalty and vigor" in {
|
||||
// Feast is available as long as there's enough gold, so players can complete SpendOnFeastsQuest
|
||||
"availableCommand" should "return nothing if everybody has maxed loyalty and vigor" in {
|
||||
val gameState = GameState(
|
||||
gameId = 0,
|
||||
currentRoundId = 0,
|
||||
@@ -97,11 +96,11 @@ class AvailableFeastCommandFactoryTest extends AnyFlatSpec with Matchers with Be
|
||||
chronicleEntries = Vector()
|
||||
)
|
||||
|
||||
inside(AvailableFeastCommandFactory.availableCommand(gameState, 9, 5).get) {
|
||||
case cmd: FeastAvailable =>
|
||||
cmd.actingProvinceId shouldBe 5
|
||||
cmd.goldCost shouldBe 60
|
||||
}
|
||||
AvailableFeastCommandFactory.availableCommand(
|
||||
gameState,
|
||||
9,
|
||||
5
|
||||
) shouldBe empty
|
||||
}
|
||||
|
||||
"availableCommand" should "set the basics" in {
|
||||
|
||||
@@ -562,10 +562,8 @@ scala_test(
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:incoming_end_turn_action",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/quest",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/run_status",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/view/province:province_view",
|
||||
],
|
||||
)
|
||||
|
||||
-166
@@ -12,10 +12,8 @@ import net.eagle0.eagle.model.state.faction.concrete.FactionC
|
||||
import net.eagle0.eagle.model.state.faction.FactionRelationship
|
||||
import net.eagle0.eagle.model.state.hero.concrete.HeroC
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.province.Neighbor
|
||||
import net.eagle0.eagle.model.state.quest.{
|
||||
AllianceQuest,
|
||||
BorderSecurityQuest,
|
||||
DismissSpecificVassalQuest,
|
||||
GiveToHeroesAcrossRealmQuest,
|
||||
GrandArmyQuest,
|
||||
@@ -908,168 +906,4 @@ class CheckForFulfilledQuestsActionTest extends AnyFlatSpec with Matchers with B
|
||||
province = province
|
||||
) shouldBe false
|
||||
}
|
||||
|
||||
behavior of "BorderSecurityQuest"
|
||||
|
||||
it should "be fulfilled when all neighbors are controlled by the faction" in {
|
||||
val targetProvince = ProvinceC(
|
||||
id = 100,
|
||||
rulingFactionId = Some(fid),
|
||||
neighbors = Vector(
|
||||
Neighbor(provinceId = 101, startingPositionIndex = 0),
|
||||
Neighbor(provinceId = 102, startingPositionIndex = 0)
|
||||
)
|
||||
)
|
||||
val neighbor1 = ProvinceC(id = 101, rulingFactionId = Some(fid))
|
||||
val neighbor2 = ProvinceC(id = 102, rulingFactionId = Some(fid))
|
||||
val questProvince = province.copy(rulingFactionId = Some(fid))
|
||||
|
||||
CheckForFulfilledQuestsAction(
|
||||
gameId = gameId,
|
||||
currentDate = date,
|
||||
currentRoundId = roundId,
|
||||
provinces = Vector(questProvince, targetProvince, neighbor1, neighbor2),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
getHero = _ => None,
|
||||
battalionTypes = battalionTypes,
|
||||
heroBackstoryTextIdLookup = hid => s"backstory_$hid"
|
||||
).didFulfillQuest(
|
||||
quest = BorderSecurityQuest(targetProvinceId = 100),
|
||||
province = questProvince
|
||||
) shouldBe true
|
||||
}
|
||||
|
||||
it should "not be fulfilled when a neighbor is controlled by another faction" in {
|
||||
val targetProvince = ProvinceC(
|
||||
id = 100,
|
||||
rulingFactionId = Some(fid),
|
||||
neighbors = Vector(
|
||||
Neighbor(provinceId = 101, startingPositionIndex = 0),
|
||||
Neighbor(provinceId = 102, startingPositionIndex = 0)
|
||||
)
|
||||
)
|
||||
val neighbor1 = ProvinceC(id = 101, rulingFactionId = Some(fid))
|
||||
val neighbor2 = ProvinceC(id = 102, rulingFactionId = Some(otherFid))
|
||||
val questProvince = province.copy(rulingFactionId = Some(fid))
|
||||
|
||||
CheckForFulfilledQuestsAction(
|
||||
gameId = gameId,
|
||||
currentDate = date,
|
||||
currentRoundId = roundId,
|
||||
provinces = Vector(questProvince, targetProvince, neighbor1, neighbor2),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
getHero = _ => None,
|
||||
battalionTypes = battalionTypes,
|
||||
heroBackstoryTextIdLookup = hid => s"backstory_$hid"
|
||||
).didFulfillQuest(
|
||||
quest = BorderSecurityQuest(targetProvinceId = 100),
|
||||
province = questProvince
|
||||
) shouldBe false
|
||||
}
|
||||
|
||||
it should "be fulfilled when a neighbor is controlled by an ally" in {
|
||||
val allyFid = 50
|
||||
val targetProvince = ProvinceC(
|
||||
id = 100,
|
||||
rulingFactionId = Some(fid),
|
||||
neighbors = Vector(
|
||||
Neighbor(provinceId = 101, startingPositionIndex = 0),
|
||||
Neighbor(provinceId = 102, startingPositionIndex = 0)
|
||||
)
|
||||
)
|
||||
val neighbor1 = ProvinceC(id = 101, rulingFactionId = Some(fid))
|
||||
val neighbor2 = ProvinceC(id = 102, rulingFactionId = Some(allyFid))
|
||||
val questProvince = province.copy(rulingFactionId = Some(fid))
|
||||
|
||||
val factionWithAlly = faction.copy(
|
||||
factionRelationships = Vector(
|
||||
FactionRelationship(
|
||||
targetFactionId = allyFid,
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.Ally
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val allyFaction = FactionC(
|
||||
id = allyFid,
|
||||
name = s"Faction $allyFid",
|
||||
factionHeadId = allyFid,
|
||||
leaderIds = Vector(allyFid),
|
||||
factionRelationships = Vector(
|
||||
FactionRelationship(
|
||||
targetFactionId = fid,
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.Ally
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
CheckForFulfilledQuestsAction(
|
||||
gameId = gameId,
|
||||
currentDate = date,
|
||||
currentRoundId = roundId,
|
||||
provinces = Vector(questProvince, targetProvince, neighbor1, neighbor2),
|
||||
factions = Vector(factionWithAlly, allyFaction),
|
||||
battalions = Vector(),
|
||||
getHero = _ => None,
|
||||
battalionTypes = battalionTypes,
|
||||
heroBackstoryTextIdLookup = hid => s"backstory_$hid"
|
||||
).didFulfillQuest(
|
||||
quest = BorderSecurityQuest(targetProvinceId = 100),
|
||||
province = questProvince
|
||||
) shouldBe true
|
||||
}
|
||||
|
||||
it should "not be fulfilled when a neighbor is controlled by a faction with only a truce" in {
|
||||
val truceFid = 50
|
||||
val targetProvince = ProvinceC(
|
||||
id = 100,
|
||||
rulingFactionId = Some(fid),
|
||||
neighbors = Vector(
|
||||
Neighbor(provinceId = 101, startingPositionIndex = 0),
|
||||
Neighbor(provinceId = 102, startingPositionIndex = 0)
|
||||
)
|
||||
)
|
||||
val neighbor1 = ProvinceC(id = 101, rulingFactionId = Some(fid))
|
||||
val neighbor2 = ProvinceC(id = 102, rulingFactionId = Some(truceFid))
|
||||
val questProvince = province.copy(rulingFactionId = Some(fid))
|
||||
|
||||
val factionWithTruce = faction.copy(
|
||||
factionRelationships = Vector(
|
||||
FactionRelationship(
|
||||
targetFactionId = truceFid,
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.Truce
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val truceFaction = FactionC(
|
||||
id = truceFid,
|
||||
name = s"Faction $truceFid",
|
||||
factionHeadId = truceFid,
|
||||
leaderIds = Vector(truceFid),
|
||||
factionRelationships = Vector(
|
||||
FactionRelationship(
|
||||
targetFactionId = fid,
|
||||
relationshipLevel = FactionRelationship.RelationshipLevel.Truce
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
CheckForFulfilledQuestsAction(
|
||||
gameId = gameId,
|
||||
currentDate = date,
|
||||
currentRoundId = roundId,
|
||||
provinces = Vector(questProvince, targetProvince, neighbor1, neighbor2),
|
||||
factions = Vector(factionWithTruce, truceFaction),
|
||||
battalions = Vector(),
|
||||
getHero = _ => None,
|
||||
battalionTypes = battalionTypes,
|
||||
heroBackstoryTextIdLookup = hid => s"backstory_$hid"
|
||||
).didFulfillQuest(
|
||||
quest = BorderSecurityQuest(targetProvinceId = 100),
|
||||
province = questProvince
|
||||
) shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
-121
@@ -15,15 +15,11 @@ import net.eagle0.eagle.model.state.hero.backstory_version.BackstoryVersion
|
||||
import net.eagle0.eagle.model.state.hero.concrete.HeroC
|
||||
import net.eagle0.eagle.model.state.province.{IncomingEndTurnAction, IncomingRecon, ProvinceOrderType}
|
||||
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
|
||||
import net.eagle0.eagle.model.state.quest.{ReconProvincesQuest, ReconSpecificProvincesQuest}
|
||||
import net.eagle0.eagle.model.state.run_status.RunStatus
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
|
||||
import net.eagle0.eagle.model.state.unaffiliated_hero.concrete.UnaffiliatedHeroC
|
||||
import net.eagle0.eagle.model.state.RoundPhase
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.BeforeAndAfterEach
|
||||
import org.scalatest.Inside.inside
|
||||
|
||||
class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEach with Matchers {
|
||||
|
||||
@@ -297,121 +293,4 @@ class PerformReconResolutionActionTest extends AnyFlatSpec with BeforeAndAfterEa
|
||||
allResults.count(_.actionResultType == ReconSucceeded) shouldBe 3
|
||||
allResults.count(_.actionResultType == EndReconResolutionPhase) shouldBe 1
|
||||
}
|
||||
|
||||
"recon quests" should "increment ReconProvincesQuest counter" in {
|
||||
val quest = ReconProvincesQuest(componentCount = 5, componentsFulfilled = 2, targetProvinceCount = 5)
|
||||
val questHero = UnaffiliatedHeroC(
|
||||
heroId = 200,
|
||||
unaffiliatedHeroType = UnaffiliatedHeroType.Traveler,
|
||||
recruitmentInfo = RecruitmentInfo.HasQuest(quest)
|
||||
)
|
||||
|
||||
val provincesWithQuest = baseProvinces +
|
||||
(9 -> baseProvinces(9).copy(unaffiliatedHeroes = Vector(questHero)))
|
||||
|
||||
val heroesWithQuest = baseHeroes + (200 -> HeroC(id = 200))
|
||||
|
||||
val gsWithQuest = makeGameState(provincesWithQuest, heroesWithQuest, baseFactions)
|
||||
|
||||
val oneResult = PerformReconResolutionAction(gsWithQuest, actionResultApplier)
|
||||
.oneResult(
|
||||
provinceId = 3,
|
||||
incomingEndTurnAction = incomingRecon,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.newValue
|
||||
|
||||
inside(oneResult) {
|
||||
case ar: ActionResultC =>
|
||||
// Find the changed unaffiliated hero across all province changes for province 9
|
||||
val updatedUH = ar.changedProvinces.collect { case cp: ChangedProvinceC if cp.provinceId == 9 => cp }
|
||||
.flatMap(_.changedUnaffiliatedHeroes)
|
||||
.find(_.heroId == 200)
|
||||
|
||||
inside(updatedUH) {
|
||||
case Some(uh) =>
|
||||
inside(uh.quest) {
|
||||
case Some(q: ReconProvincesQuest) =>
|
||||
q.componentsFulfilled shouldBe 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it should "increment ReconSpecificProvincesQuest counter when target province is reconned" in {
|
||||
val quest =
|
||||
ReconSpecificProvincesQuest(componentCount = 3, componentsFulfilled = 0, targetProvinceIds = Vector(3, 15))
|
||||
val questHero = UnaffiliatedHeroC(
|
||||
heroId = 200,
|
||||
unaffiliatedHeroType = UnaffiliatedHeroType.Traveler,
|
||||
recruitmentInfo = RecruitmentInfo.HasQuest(quest)
|
||||
)
|
||||
|
||||
val provincesWithQuest = baseProvinces +
|
||||
(9 -> baseProvinces(9).copy(unaffiliatedHeroes = Vector(questHero)))
|
||||
|
||||
val heroesWithQuest = baseHeroes + (200 -> HeroC(id = 200))
|
||||
|
||||
val gsWithQuest = makeGameState(provincesWithQuest, heroesWithQuest, baseFactions)
|
||||
|
||||
val oneResult = PerformReconResolutionAction(gsWithQuest, actionResultApplier)
|
||||
.oneResult(
|
||||
provinceId = 3,
|
||||
incomingEndTurnAction = incomingRecon,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.newValue
|
||||
|
||||
inside(oneResult) {
|
||||
case ar: ActionResultC =>
|
||||
// Find the changed unaffiliated hero across all province changes for province 9
|
||||
val updatedUH = ar.changedProvinces.collect { case cp: ChangedProvinceC if cp.provinceId == 9 => cp }
|
||||
.flatMap(_.changedUnaffiliatedHeroes)
|
||||
.find(_.heroId == 200)
|
||||
|
||||
inside(updatedUH) {
|
||||
case Some(uh) =>
|
||||
inside(uh.quest) {
|
||||
case Some(q: ReconSpecificProvincesQuest) =>
|
||||
q.componentsFulfilled shouldBe 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it should "not increment ReconSpecificProvincesQuest counter when non-target province is reconned" in {
|
||||
val quest =
|
||||
ReconSpecificProvincesQuest(componentCount = 3, componentsFulfilled = 0, targetProvinceIds = Vector(15, 20))
|
||||
val questHero = UnaffiliatedHeroC(
|
||||
heroId = 200,
|
||||
unaffiliatedHeroType = UnaffiliatedHeroType.Traveler,
|
||||
recruitmentInfo = RecruitmentInfo.HasQuest(quest)
|
||||
)
|
||||
|
||||
val provincesWithQuest = baseProvinces +
|
||||
(9 -> baseProvinces(9).copy(unaffiliatedHeroes = Vector(questHero)))
|
||||
|
||||
val heroesWithQuest = baseHeroes + (200 -> HeroC(id = 200))
|
||||
|
||||
val gsWithQuest = makeGameState(provincesWithQuest, heroesWithQuest, baseFactions)
|
||||
|
||||
val oneResult = PerformReconResolutionAction(gsWithQuest, actionResultApplier)
|
||||
.oneResult(
|
||||
provinceId = 3,
|
||||
incomingEndTurnAction = incomingRecon,
|
||||
functionalRandom = functionalRandom
|
||||
)
|
||||
.newValue
|
||||
|
||||
inside(oneResult) {
|
||||
case ar: ActionResultC =>
|
||||
val provinceCP = ar.changedProvinces.collectFirst {
|
||||
case cp: ChangedProvinceC if cp.provinceId == 9 => cp
|
||||
}
|
||||
// No quest update expected - either no CP for province 9 or no changed UHs
|
||||
provinceCP.foreach { cp =>
|
||||
cp.changedUnaffiliatedHeroes.find(_.heroId == 200) shouldBe None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-41
@@ -24,6 +24,7 @@ import net.eagle0.eagle.library.settings.{
|
||||
PriceIndexShiftPerGold
|
||||
}
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.concrete.NotificationC
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType
|
||||
import net.eagle0.eagle.model.action_result.NotificationDetails.Divined
|
||||
import net.eagle0.eagle.model.action_result.NotificationT.Llm
|
||||
@@ -111,8 +112,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
ex.getMessage shouldBe "requirement failed: Duplicate heroIds found in Vector(12, 13, 12)"
|
||||
}
|
||||
@@ -128,8 +128,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province.withGold(199)),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
ex.getMessage shouldBe "requirement failed: Insufficient gold to divine 2 heroes"
|
||||
}
|
||||
@@ -145,8 +144,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
ex.getMessage shouldBe "requirement failed: Selected heroes Vector(12, 14) was not a subset of Vector(12, 13, 15)"
|
||||
}
|
||||
@@ -162,8 +160,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -179,8 +176,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
.immediateExecute(functionalRandom = functionalRandom)
|
||||
.newValue
|
||||
@@ -203,8 +199,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
)
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
|
||||
val ex = the[EagleInternalException] thrownBy
|
||||
@@ -224,8 +219,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
.immediateExecute(functionalRandom = functionalRandom)
|
||||
.newValue
|
||||
@@ -260,8 +254,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
.immediateExecute(functionalRandom = functionalRandom)
|
||||
.newValue
|
||||
@@ -283,8 +276,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province.withPriceIndex(0.9)),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
.immediateExecute(functionalRandom = functionalRandom)
|
||||
.newValue
|
||||
@@ -306,8 +298,7 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
.immediateExecute(functionalRandom = functionalRandom)
|
||||
.newValue
|
||||
@@ -329,30 +320,28 @@ class DivineCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
currentRoundId = currentRoundId,
|
||||
allProvinces = Vector(province),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector()
|
||||
battalions = Vector()
|
||||
)
|
||||
.immediateExecute(functionalRandom = functionalRandom)
|
||||
.newValue
|
||||
|
||||
result.newNotifications should have size 2
|
||||
|
||||
// Verify each notification has the correct field associations
|
||||
// (ignoring affectedProvinceIds which varies based on random quest assignment)
|
||||
forExactly(1, result.newNotifications) { notif =>
|
||||
notif.affectedHeroIds shouldBe Vector(12)
|
||||
notif.details shouldBe Divined
|
||||
notif.targetFactionIds shouldBe Vector(actingFactionId)
|
||||
notif.llm shouldBe Llm.Id("387 divine province 93 uh 12")
|
||||
notif.deferred shouldBe false
|
||||
}
|
||||
|
||||
forExactly(1, result.newNotifications) { notif =>
|
||||
notif.affectedHeroIds shouldBe Vector(15)
|
||||
notif.details shouldBe Divined
|
||||
notif.targetFactionIds shouldBe Vector(actingFactionId)
|
||||
notif.llm shouldBe Llm.Id("387 divine province 93 uh 15")
|
||||
notif.deferred shouldBe false
|
||||
}
|
||||
result.newNotifications should contain theSameElementsAs Vector(
|
||||
NotificationC(
|
||||
details = Divined,
|
||||
targetFactionIds = Vector(actingFactionId),
|
||||
llm = Llm.Id("387 divine province 93 uh 12"),
|
||||
affectedProvinceIds = Vector(provinceId),
|
||||
affectedHeroIds = Vector(12),
|
||||
deferred = false
|
||||
),
|
||||
NotificationC(
|
||||
details = Divined,
|
||||
targetFactionIds = Vector(actingFactionId),
|
||||
llm = Llm.Id("387 divine province 93 uh 15"),
|
||||
affectedProvinceIds = Vector(),
|
||||
affectedHeroIds = Vector(15),
|
||||
deferred = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-40
@@ -87,8 +87,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = Vector(ImprovementType.Devastation),
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
cmd.immediateExecute
|
||||
@@ -106,8 +105,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -123,8 +121,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -140,8 +137,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -160,8 +156,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -181,8 +176,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -198,8 +192,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -215,8 +208,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -236,8 +228,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Devastation,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -260,8 +251,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Devastation,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val betterExpectedImprovement = 7.12 // improvementPerStat * (85 + 93) / 2.0
|
||||
@@ -284,8 +274,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Devastation,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val expectedImprovement =
|
||||
@@ -308,8 +297,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Devastation,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -330,8 +318,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -352,8 +339,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val reducedExpectedImprovement =
|
||||
@@ -377,8 +363,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val betterExpectedImprovement = 7.12 // improvementPerStat * (85 + 93) / 2.0
|
||||
@@ -402,8 +387,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val expectedImprovement =
|
||||
@@ -426,8 +410,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Economy,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -448,8 +431,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Agriculture,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -470,8 +452,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Infrastructure,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = false,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = false
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
@@ -492,8 +473,7 @@ class ImproveCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEa
|
||||
improvementType = ImprovementType.Infrastructure,
|
||||
availableHeroIds = heroes.map(_.id),
|
||||
availableImprovementTypes = availableTypes,
|
||||
lockImprovementType = true,
|
||||
factionProvinces = Vector(province)
|
||||
lockImprovementType = true
|
||||
)
|
||||
|
||||
val result = cmd.immediateExecute
|
||||
|
||||
@@ -43,8 +43,7 @@ class RestCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEach
|
||||
val command = RestCommand.make(
|
||||
actingFactionId = rulingFactionId,
|
||||
provinceId = provinceId,
|
||||
factionHeroesInProvince = factionHeroesInProvince,
|
||||
factionProvinces = Vector()
|
||||
factionHeroesInProvince = factionHeroesInProvince
|
||||
)
|
||||
|
||||
val result = command.immediateExecute
|
||||
@@ -62,8 +61,7 @@ class RestCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEach
|
||||
val command = RestCommand.make(
|
||||
actingFactionId = rulingFactionId,
|
||||
provinceId = provinceId,
|
||||
factionHeroesInProvince = factionHeroesInProvince,
|
||||
factionProvinces = Vector()
|
||||
factionHeroesInProvince = factionHeroesInProvince
|
||||
)
|
||||
|
||||
val result = command.immediateExecute
|
||||
@@ -83,8 +81,7 @@ class RestCommandTest extends AnyFlatSpec with Matchers with BeforeAndAfterEach
|
||||
val command = RestCommand.make(
|
||||
actingFactionId = rulingFactionId,
|
||||
provinceId = provinceId,
|
||||
factionHeroesInProvince = heroesWithAlmostRested,
|
||||
factionProvinces = Vector()
|
||||
factionHeroesInProvince = heroesWithAlmostRested
|
||||
)
|
||||
|
||||
val result = command.immediateExecute
|
||||
|
||||
+41
-82
@@ -147,7 +147,6 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
@@ -165,8 +164,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -184,8 +182,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -202,8 +199,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -221,8 +217,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -239,8 +234,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -256,8 +250,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue should contain.allOf(
|
||||
SpecificExpansionQuest(provinceId = immediate1.id),
|
||||
@@ -280,8 +273,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -298,8 +290,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -316,8 +307,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -335,8 +325,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue should contain(
|
||||
DismissSpecificVassalQuest(targetHeroId = 2)
|
||||
@@ -351,8 +340,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) {
|
||||
@@ -369,8 +357,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) {
|
||||
@@ -390,8 +377,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
) { q =>
|
||||
@@ -412,8 +398,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
.collect { case ubq: UpgradeBattalionQuest => ubq }
|
||||
@@ -452,8 +437,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = battalions,
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -493,8 +477,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = battalions,
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
val ubqs = quests.collect {
|
||||
@@ -521,15 +504,14 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = battalions,
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
forAtLeast(1, quests) { q =>
|
||||
inside(q) {
|
||||
case GrandArmyQuest(totalTroopCount) =>
|
||||
totalTroopCount should be >= 3200 + 2500
|
||||
totalTroopCount should be > 3200 + 2500
|
||||
totalTroopCount should be < 3200 + 5000
|
||||
}
|
||||
}
|
||||
@@ -554,8 +536,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
leaderIds = Vector(73)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -589,8 +570,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
leaderIds = Vector(73)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -623,8 +603,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
leaderIds = Vector(73)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -657,8 +636,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -682,8 +660,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -706,8 +683,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -738,8 +714,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -782,8 +757,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -836,8 +810,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -909,8 +882,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
)
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -930,8 +902,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -953,8 +924,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -981,8 +951,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1011,8 +980,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1036,8 +1004,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1066,8 +1033,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
allProvinces = allProvinces,
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1092,8 +1058,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1122,8 +1087,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1172,8 +1136,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1231,8 +1194,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1295,8 +1257,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
name = "Faction 71"
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1364,8 +1325,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
name = "Faction 73"
|
||||
),
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
@@ -1433,8 +1393,7 @@ class QuestCreationUtilsTest extends AnyFlatSpec with Matchers with BeforeAndAft
|
||||
),
|
||||
factions = factions,
|
||||
battalions = Vector(),
|
||||
heroes = Vector(),
|
||||
functionalRandom = new JankyRandom(new Random())
|
||||
new JankyRandom(new Random())
|
||||
)
|
||||
.newValue
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
val mockEngineAndResults = mock[EngineAndResults]
|
||||
|
||||
mockGameCreation.createGame
|
||||
.expects(*, *, *, *, *, *, *)
|
||||
.expects(*, *, *, *, *, *)
|
||||
.returns(mockEngineAndResults): Unit
|
||||
(() => mockEngine.updated)
|
||||
.expects()
|
||||
@@ -254,7 +254,7 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
allowGamesE0esReads()
|
||||
val mockEngineAndResults = mock[EngineAndResults]
|
||||
mockGameCreation.createGame
|
||||
.expects(*, *, *, *, *, *, *)
|
||||
.expects(*, *, *, *, *, *)
|
||||
.returns(mockEngineAndResults): Unit
|
||||
|
||||
(() => mockEngine.updated)
|
||||
|
||||
Reference in New Issue
Block a user