Compare commits

..
Author SHA1 Message Date
adminandClaude Opus 4.5 ad974357f4 Update deproto plan: add DiplomacyOfferStatus and next candidates
- Document completed DiplomacyOfferStatus enum migration (PR #5093)
- Add Enum Type Migrations section tracking proto enum conversions
- Add Next Candidates section with priority items for future work

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 05:36:47 -08:00
adminandClaude Opus 4.5 d9dfb5cbb2 Deproto: Use Scala Status in EligibleDiplomacyStatuses
Convert EligibleDiplomacyStatuses to use Scala Status types internally,
with conversion to proto at the call sites where needed for building
AvailableCommand proto messages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 21:40:31 -08:00
149 changed files with 12013 additions and 16604 deletions
+8 -8
View File
@@ -53,26 +53,26 @@ jobs:
run: |
# Upload sysroot tarball to DO Spaces
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.tar.xz \
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz \
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz \
--endpoint-url https://sfo3.digitaloceanspaces.com \
--acl public-read
# Upload sha256 file
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256 \
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.sha256 \
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.sha256 \
--endpoint-url https://sfo3.digitaloceanspaces.com \
--acl public-read
echo ""
echo "=== AMD64 Sysroot uploaded ==="
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz"
echo "URL: https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz"
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)"
echo ""
echo "Update MODULE.bazel with:"
echo "sysroot("
echo " name = \"linux_sysroot\","
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)\","
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz\"],"
echo " urls = [\"https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz\"],"
echo ")"
build-sysroot-arm64:
@@ -114,24 +114,24 @@ jobs:
run: |
# Upload sysroot tarball to DO Spaces
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.tar.xz \
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz \
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz \
--endpoint-url https://sfo3.digitaloceanspaces.com \
--acl public-read
# Upload sha256 file
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256 \
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.sha256 \
s3://eagle0-windows/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.sha256 \
--endpoint-url https://sfo3.digitaloceanspaces.com \
--acl public-read
echo ""
echo "=== ARM64 Sysroot uploaded ==="
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz"
echo "URL: https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz"
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)"
echo ""
echo "Update MODULE.bazel with:"
echo "sysroot("
echo " name = \"linux_sysroot_arm64\","
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)\","
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz\"],"
echo " urls = [\"https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz\"],"
echo ")"
+4 -6
View File
@@ -40,15 +40,13 @@ jobs:
echo ""
# List of repositories to clean
# Use tail to skip header row in case --no-header doesn't work
REPOS=$(doctl registry repository list-v2 --format Name --no-header | grep -v '^Name$' | grep -v '^$')
REPOS=$(doctl registry repository list-v2 --format Name --no-header)
for REPO in $REPOS; do
echo "=== Processing repository: ${REPO} ==="
# Get all manifests with their tags and dates
# Filter out header row and empty lines
MANIFESTS=$(doctl registry repository list-manifests "${REPO}" --format Digest,UpdatedAt,Tags --no-header 2>/dev/null | grep -v '^Digest' | grep -v '^$' || echo "")
MANIFESTS=$(doctl registry repository list-manifests "${REPO}" --format Digest,UpdatedAt,Tags --no-header 2>/dev/null || echo "")
if [ -z "$MANIFESTS" ]; then
echo " No manifests found"
@@ -56,8 +54,8 @@ jobs:
fi
echo "$MANIFESTS" | while read -r DIGEST UPDATED_AT TAGS; do
# Skip if no digest or if it doesn't look like a valid digest (sha256:...)
if [ -z "$DIGEST" ] || ! echo "$DIGEST" | grep -q '^sha256:'; then
# Skip if no digest
if [ -z "$DIGEST" ]; then
continue
fi
+33 -45
View File
@@ -159,63 +159,51 @@ jobs:
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
deploy-hetzner:
runs-on: self-hosted
runs-on: ubuntu-latest
needs: [build-shardok-arm64]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
env:
SHARDOK_IMAGE: ${{ needs.build-shardok-arm64.outputs.image_tag }}
steps:
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.HETZNER_SSH_KEY }}" > ~/.ssh/hetzner_deploy
chmod 600 ~/.ssh/hetzner_deploy
# Add host key to known_hosts to avoid prompt
ssh-keyscan -H ${{ secrets.HETZNER_IP }} >> ~/.ssh/known_hosts 2>/dev/null || true
- name: Deploy to Hetzner
run: |
ssh -i ~/.ssh/hetzner_deploy -o StrictHostKeyChecking=accept-new deploy@${{ secrets.HETZNER_IP }} << 'ENDSSH'
set -ex
cd /opt/eagle0
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HETZNER_IP }}
username: deploy
key: ${{ secrets.HETZNER_SSH_KEY }}
protocol: tcp6
script_stop: true
envs: SHARDOK_IMAGE
script: |
set -ex
cd /opt/eagle0
# Login to registry
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
# Login to registry
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
echo "Deploying Shardok ARM64: ${{ needs.build-shardok-arm64.outputs.image_tag }}"
echo "Deploying Shardok ARM64: $SHARDOK_IMAGE"
# Pull the new image
docker pull "${{ needs.build-shardok-arm64.outputs.image_tag }}"
# Pull the new image
docker pull "$SHARDOK_IMAGE"
# Stop and remove any container using port 40042 or named shardok*
docker ps -q --filter "publish=40042" | xargs -r docker stop
docker ps -aq --filter "name=shardok" | xargs -r docker rm -f
docker ps -aq --filter "publish=40042" | xargs -r docker rm -f
# Stop and remove existing container
docker stop shardok-ai 2>/dev/null || true
docker rm shardok-ai 2>/dev/null || true
# Run new container
docker run -d \
--name shardok-ai \
--restart unless-stopped \
-p 40042:40042 \
-v /opt/eagle0/data:/data \
-v /etc/shardok:/etc/shardok:ro \
-v /etc/letsencrypt:/etc/letsencrypt:ro \
-v /usr/local/share/eagle0:/usr/local/share/eagle0:ro \
-e SHARDOK_RESOURCES_PATH=/app/resources \
-e SHARDOK_MAPS_PATH=/app/resources/maps \
"${{ needs.build-shardok-arm64.outputs.image_tag }}"
# Run new container
docker run -d \
--name shardok-ai \
--restart unless-stopped \
-p 40042:40042 \
-v /opt/eagle0/data:/data \
"$SHARDOK_IMAGE"
# Wait and verify
sleep 5
docker ps | grep shardok-ai
# Wait and verify
sleep 5
docker ps | grep shardok-ai
# Cleanup old images
docker image prune -f
# Cleanup old images
docker image prune -f
echo "=== Hetzner deployment complete ==="
ENDSSH
- name: Cleanup SSH key
if: always()
run: rm -f ~/.ssh/hetzner_deploy
echo "=== Hetzner deployment complete ==="
+2 -2
View File
@@ -88,14 +88,14 @@ sysroot = use_repo_rule("@toolchains_llvm//toolchain:sysroot.bzl", "sysroot")
sysroot(
name = "linux_sysroot",
sha256 = "a06475004fe8003ae7ccb4fe1d5511feb9b27cce4a8826eb1dfd686ed83f3dba",
urls = ["https://eagle0-sysroot.sfo3.digitaloceanspaces.com/v3/ubuntu_noble_amd64_sysroot.tar.xz"],
urls = ["https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/v3/ubuntu_noble_amd64_sysroot.tar.xz"],
)
# ARM64 sysroot
sysroot(
name = "linux_sysroot_arm64",
sha256 = "87469137737e09bc73855007dab835477eb10a7b3ce3f725f93f64e25747f3f9",
urls = ["https://eagle0-sysroot.sfo3.digitaloceanspaces.com/v4/ubuntu_noble_arm64_sysroot.tar.xz"],
urls = ["https://eagle0-windows.sfo3.digitaloceanspaces.com/sysroot/v4/ubuntu_noble_arm64_sysroot.tar.xz"],
)
#
@@ -170,16 +170,18 @@ namespace Auth {
/// <summary>
/// Logout - invalidates refresh token on server.
/// Does NOT clear local tokens (OAuthManager handles that decision).
/// Routed to Eagle.
/// </summary>
public async Task LogoutAsync() {
try {
await _eagleClient.LogoutAsync(new LogoutRequest());
Debug.Log("[AuthClient] Server logout successful");
} catch (Exception ex) {
Debug.LogWarning($"[AuthClient] Logout RPC failed (may be expected): {ex.Message}");
}
// Clear local tokens regardless of server response
TokenStorage.Clear();
Debug.Log("[AuthClient] Logged out, tokens cleared");
}
public void Dispose() {
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Net.Eagle0.Eagle.Api.Auth;
using UnityEngine;
@@ -10,7 +9,6 @@ namespace Auth {
/// - Opens system browser for OAuth consent
/// - Polls server for OAuth completion (no deep links needed)
/// - Token storage and refresh
/// - Multi-account support
/// </summary>
public class OAuthManager : MonoBehaviour {
public static OAuthManager Instance { get; private set; }
@@ -18,7 +16,6 @@ namespace Auth {
private AuthClient _authClient;
private string _currentAuthServiceUrl;
private string _currentEagleUrl;
private OAuthProvider _currentLoginProvider; // Track provider during login
// Events for UI updates
public event Action<UserInfo> OnLoginSuccess;
@@ -83,8 +80,6 @@ namespace Auth {
/// </summary>
public async Task<CheckOAuthStatusResponse> LoginAsync(OAuthProvider provider) {
EnsureConfigured();
_currentLoginProvider = provider; // Track for later storage
try {
// Get OAuth URL and state from server
var (authUrl, state) = await _authClient.GetOAuthUrlAsync(provider);
@@ -96,15 +91,13 @@ namespace Auth {
// Poll for OAuth completion (server handles the callback)
var response = await _authClient.PollForOAuthCompletionAsync(state);
// Store tokens with provider info
var providerName = provider.ToString().ToLowerInvariant();
// Store tokens
TokenStorage.StoreTokens(
response.AccessToken,
response.RefreshToken,
response.ExpiresAt,
response.User.UserId,
response.User.DisplayName ?? "",
providerName);
response.User.DisplayName ?? "");
// Notify listeners
if (response.IsNewUser) {
@@ -163,15 +156,6 @@ namespace Auth {
// Validate session with server
try {
var response = await _authClient.GetCurrentUserAsync();
// Check if user still needs to set display name (e.g., closed app before setting
// it)
if (string.IsNullOrEmpty(response.User.DisplayName)) {
Debug.Log("[OAuthManager] Session restored but user needs display name");
OnNewUserNeedsDisplayName?.Invoke(false); // false = not a brand new user
return true;
}
Debug.Log($"[OAuthManager] Session restored for {response.User.DisplayName}");
OnLoginSuccess?.Invoke(response.User);
return true;
@@ -194,89 +178,16 @@ namespace Auth {
}
/// <summary>
/// Logout - clears current session but preserves stored tokens for quick re-login.
/// Logout and clear stored tokens.
/// </summary>
public async Task LogoutAsync() {
// Notify server of logout if connected
// LogoutAsync can work even without server connection - just clear local tokens
if (_authClient != null) {
try {
await _authClient.LogoutAsync();
} catch (Exception ex) {
Debug.LogWarning($"[OAuthManager] Server logout failed: {ex.Message}");
}
await _authClient.LogoutAsync();
} else {
TokenStorage.Clear();
}
// Clear current account selection but keep tokens stored
TokenStorage.ClearCurrentAccount();
OnLogout?.Invoke();
}
/// <summary>
/// Get all stored accounts for display in UI.
/// </summary>
public IReadOnlyList<StoredAccount> GetStoredAccounts() {
return TokenStorage.GetAllAccounts();
}
/// <summary>
/// Select and connect with a stored account.
/// If token is expired or about to expire, refreshes it first.
/// </summary>
public async Task<bool> ConnectWithStoredAccountAsync(StoredAccount account) {
EnsureConfigured();
// Select this account as current
TokenStorage.SelectAccount(account.AccountKey);
// Check if token needs refresh
if (!account.HasValidToken || account.NeedsRefresh) {
if (account.HasRefreshToken) {
try {
await _authClient.RefreshTokenAsync();
Debug.Log($"[OAuthManager] Refreshed token for {account.DisplayName}");
} catch (Exception ex) {
Debug.LogWarning($"[OAuthManager] Token refresh failed: {ex.Message}");
// Token refresh failed - need to re-authenticate
// Parse provider from account key
var provider = account.Provider.ToLowerInvariant() == "google"
? OAuthProvider.Google
: OAuthProvider.Discord;
OnLoginFailed?.Invoke("Session expired. Please sign in again.");
return false;
}
} else {
OnLoginFailed?.Invoke("Session expired. Please sign in again.");
return false;
}
}
// Validate session with server
try {
var response = await _authClient.GetCurrentUserAsync();
// Check if user still needs to set display name
if (string.IsNullOrEmpty(response.User.DisplayName)) {
Debug.Log("[OAuthManager] Stored account needs display name");
OnNewUserNeedsDisplayName?.Invoke(false);
return true;
}
Debug.Log(
$"[OAuthManager] Connected with stored account: {response.User.DisplayName}");
OnLoginSuccess?.Invoke(response.User);
return true;
} catch (Exception ex) {
Debug.LogWarning($"[OAuthManager] Session validation failed: {ex.Message}");
OnLoginFailed?.Invoke("Session invalid. Please sign in again.");
return false;
}
}
/// <summary>
/// Remove a stored account permanently.
/// </summary>
public void RemoveStoredAccount(StoredAccount account) {
TokenStorage.RemoveAccount(account.AccountKey);
}
}
}
@@ -1,63 +1,27 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Auth {
/// <summary>
/// Represents a stored OAuth account with tokens.
/// </summary>
[Serializable]
public class StoredAccount {
public string Provider; // "discord" or "google"
public string UserId; // OAuth provider user ID
public string DisplayName; // User's display name
public string AccessToken;
public string RefreshToken;
public long ExpiresAt; // Unix timestamp
public string AccountKey => $"{Provider}:{UserId}";
public bool HasValidToken => !string.IsNullOrEmpty(AccessToken) &&
ExpiresAt > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
public bool HasRefreshToken => !string.IsNullOrEmpty(RefreshToken);
public bool NeedsRefresh {
get {
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return ExpiresAt > 0 && ExpiresAt - now < 300; // 5 minutes
}
}
}
/// <summary>
/// Container for all stored accounts, serialized to JSON.
/// </summary>
[Serializable]
public class StoredAccountsData {
public List<StoredAccount> Accounts = new();
public string CurrentAccountKey; // Provider:UserId of active account
}
/// <summary>
/// Secure storage for OAuth tokens using Unity's PlayerPrefs.
/// Supports multiple accounts - tokens are preserved on logout.
/// Values are cached in memory to allow access from background threads.
/// In production, consider using platform-specific secure storage
/// (Keychain on iOS, Keystore on Android).
/// </summary>
public static class TokenStorage {
private const string AccountsDataKey = "eagle0_accounts_data";
private const string AccessTokenKey = "eagle0_access_token";
private const string RefreshTokenKey = "eagle0_refresh_token";
private const string ExpiresAtKey = "eagle0_expires_at";
private const string UserIdKey = "eagle0_user_id";
private const string DisplayNameKey = "eagle0_display_name";
// Legacy keys for migration
private const string LegacyAccessTokenKey = "eagle0_access_token";
private const string LegacyRefreshTokenKey = "eagle0_refresh_token";
private const string LegacyExpiresAtKey = "eagle0_expires_at";
private const string LegacyUserIdKey = "eagle0_user_id";
private const string LegacyDisplayNameKey = "eagle0_display_name";
// In-memory cache
private static StoredAccountsData _accountsData;
// In-memory cache for thread-safe access from background threads
// PlayerPrefs can only be accessed from the main thread
private static string _cachedAccessToken;
private static string _cachedRefreshToken;
private static long _cachedExpiresAt;
private static string _cachedUserId;
private static string _cachedDisplayName;
private static bool _cacheInitialized = false;
/// <summary>
@@ -65,245 +29,147 @@ namespace Auth {
/// Call this early in app startup (e.g., in Awake or Start).
/// </summary>
public static void InitializeCache() {
var json = PlayerPrefs.GetString(AccountsDataKey, "");
if (!string.IsNullOrEmpty(json)) {
try {
_accountsData = JsonUtility.FromJson<StoredAccountsData>(json);
} catch (Exception ex) {
Debug.LogWarning($"[TokenStorage] Failed to parse accounts data: {ex.Message}");
_accountsData = new StoredAccountsData();
}
} else {
_accountsData = new StoredAccountsData();
// Try to migrate legacy single-account data
MigrateLegacyData();
}
_cachedAccessToken = PlayerPrefs.GetString(AccessTokenKey, null);
_cachedRefreshToken = PlayerPrefs.GetString(RefreshTokenKey, null);
_cachedExpiresAt =
long.TryParse(PlayerPrefs.GetString(ExpiresAtKey, "0"), out var val) ? val : 0;
_cachedUserId = PlayerPrefs.GetString(UserIdKey, null);
_cachedDisplayName = PlayerPrefs.GetString(DisplayNameKey, null);
_cacheInitialized = true;
Debug.Log(
$"[TokenStorage] Cache initialized, {_accountsData.Accounts.Count} accounts stored");
$"[TokenStorage] Cache initialized, hasToken={!string.IsNullOrEmpty(_cachedAccessToken)}");
}
private static void MigrateLegacyData() {
var legacyAccessToken = PlayerPrefs.GetString(LegacyAccessTokenKey, "");
if (string.IsNullOrEmpty(legacyAccessToken)) return;
var legacyRefreshToken = PlayerPrefs.GetString(LegacyRefreshTokenKey, "");
var legacyExpiresAt =
long.TryParse(PlayerPrefs.GetString(LegacyExpiresAtKey, "0"), out var val) ? val
: 0;
var legacyUserId = PlayerPrefs.GetString(LegacyUserIdKey, "");
var legacyDisplayName = PlayerPrefs.GetString(LegacyDisplayNameKey, "");
if (!string.IsNullOrEmpty(legacyUserId)) {
// Assume discord for legacy data (most common)
var account = new StoredAccount {
Provider = "discord",
UserId = legacyUserId,
DisplayName = legacyDisplayName,
AccessToken = legacyAccessToken,
RefreshToken = legacyRefreshToken,
ExpiresAt = legacyExpiresAt
};
_accountsData.Accounts.Add(account);
_accountsData.CurrentAccountKey = account.AccountKey;
SaveAccountsData();
// Clear legacy keys
PlayerPrefs.DeleteKey(LegacyAccessTokenKey);
PlayerPrefs.DeleteKey(LegacyRefreshTokenKey);
PlayerPrefs.DeleteKey(LegacyExpiresAtKey);
PlayerPrefs.DeleteKey(LegacyUserIdKey);
PlayerPrefs.DeleteKey(LegacyDisplayNameKey);
PlayerPrefs.Save();
Debug.Log($"[TokenStorage] Migrated legacy account: {account.DisplayName}");
}
}
private static void SaveAccountsData() {
var json = JsonUtility.ToJson(_accountsData);
PlayerPrefs.SetString(AccountsDataKey, json);
PlayerPrefs.Save();
}
/// <summary>
/// Get all stored accounts.
/// </summary>
public static IReadOnlyList<StoredAccount> GetAllAccounts() {
EnsureInitialized();
return _accountsData.Accounts.AsReadOnly();
}
/// <summary>
/// Get the currently active account, or null if none selected.
/// </summary>
public static StoredAccount CurrentAccount {
public static bool HasValidToken {
get {
EnsureInitialized();
if (string.IsNullOrEmpty(_accountsData.CurrentAccountKey)) return null;
return _accountsData.Accounts.Find(
a => a.AccountKey == _accountsData.CurrentAccountKey);
var token = AccessToken;
var expiresAt = ExpiresAt;
return !string.IsNullOrEmpty(token) &&
expiresAt > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
}
public static bool HasRefreshToken => !string.IsNullOrEmpty(RefreshToken);
public static string AccessToken {
get {
if (!_cacheInitialized) {
Debug.LogWarning(
"[TokenStorage] Cache not initialized, returning cached value anyway");
}
return _cachedAccessToken;
}
private
set {
_cachedAccessToken = value;
PlayerPrefs.SetString(AccessTokenKey, value ?? "");
}
}
public static string RefreshToken {
get => _cachedRefreshToken;
private
set {
_cachedRefreshToken = value;
PlayerPrefs.SetString(RefreshTokenKey, value ?? "");
}
}
public static long ExpiresAt {
get => _cachedExpiresAt;
private
set {
_cachedExpiresAt = value;
PlayerPrefs.SetString(ExpiresAtKey, value.ToString());
}
}
public static string UserId {
get => _cachedUserId;
private
set {
_cachedUserId = value;
PlayerPrefs.SetString(UserIdKey, value ?? "");
}
}
public static string DisplayName {
get => _cachedDisplayName;
private
set {
_cachedDisplayName = value;
PlayerPrefs.SetString(DisplayNameKey, value ?? "");
}
}
/// <summary>
/// Select an account as the current active account.
/// </summary>
public static void SelectAccount(string accountKey) {
EnsureInitialized();
var account = _accountsData.Accounts.Find(a => a.AccountKey == accountKey);
if (account != null) {
_accountsData.CurrentAccountKey = accountKey;
SaveAccountsData();
Debug.Log($"[TokenStorage] Selected account: {account.DisplayName}");
}
}
/// <summary>
/// Clear the current account selection (logout without deleting tokens).
/// </summary>
public static void ClearCurrentAccount() {
EnsureInitialized();
_accountsData.CurrentAccountKey = null;
SaveAccountsData();
Debug.Log("[TokenStorage] Cleared current account selection");
}
/// <summary>
/// Remove a specific account and its tokens.
/// </summary>
public static void RemoveAccount(string accountKey) {
EnsureInitialized();
var removed = _accountsData.Accounts.RemoveAll(a => a.AccountKey == accountKey);
if (_accountsData.CurrentAccountKey == accountKey) {
_accountsData.CurrentAccountKey = null;
}
if (removed > 0) {
SaveAccountsData();
Debug.Log($"[TokenStorage] Removed account: {accountKey}");
}
}
// Compatibility properties that reference the current account
public static bool HasValidToken => CurrentAccount?.HasValidToken ?? false;
public static bool HasRefreshToken => CurrentAccount?.HasRefreshToken ?? false;
public static string AccessToken => CurrentAccount?.AccessToken;
public static string RefreshToken => CurrentAccount?.RefreshToken;
public static long ExpiresAt => CurrentAccount?.ExpiresAt ?? 0;
public static string UserId => CurrentAccount?.UserId;
public static string DisplayName => CurrentAccount?.DisplayName;
public static bool NeedsRefresh => CurrentAccount?.NeedsRefresh ?? false;
/// <summary>
/// Store tokens for an account. Creates new or updates existing.
/// Store tokens received from OAuth exchange.
/// </summary>
public static void StoreTokens(
string accessToken,
string refreshToken,
long expiresAt,
string userId,
string displayName,
string provider = "discord") {
EnsureInitialized();
string displayName) {
AccessToken = accessToken;
RefreshToken = refreshToken;
ExpiresAt = expiresAt;
UserId = userId;
DisplayName = displayName;
PlayerPrefs.Save();
var accountKey = $"{provider}:{userId}";
var existingAccount = _accountsData.Accounts.Find(a => a.AccountKey == accountKey);
if (existingAccount != null) {
existingAccount.AccessToken = accessToken;
existingAccount.RefreshToken = refreshToken;
existingAccount.ExpiresAt = expiresAt;
existingAccount.DisplayName = displayName;
} else {
var newAccount = new StoredAccount {
Provider = provider,
UserId = userId,
DisplayName = displayName,
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresAt = expiresAt
};
_accountsData.Accounts.Add(newAccount);
}
// Set as current account
_accountsData.CurrentAccountKey = accountKey;
SaveAccountsData();
Debug.Log($"[TokenStorage] Stored tokens for {displayName} ({provider})");
Debug.Log(
$"[TokenStorage] Stored tokens for user {displayName} (expires at {expiresAt})");
}
/// <summary>
/// Update access token after refresh for the current account.
/// Update access token after refresh.
/// </summary>
public static void UpdateAccessToken(string accessToken, long expiresAt) {
EnsureInitialized();
var account = CurrentAccount;
if (account != null) {
account.AccessToken = accessToken;
account.ExpiresAt = expiresAt;
SaveAccountsData();
Debug.Log($"[TokenStorage] Updated access token (expires at {expiresAt})");
}
AccessToken = accessToken;
ExpiresAt = expiresAt;
PlayerPrefs.Save();
Debug.Log($"[TokenStorage] Updated access token (expires at {expiresAt})");
}
/// <summary>
/// Update access token for a specific account (by key).
/// </summary>
public static void
UpdateAccessTokenForAccount(string accountKey, string accessToken, long expiresAt) {
EnsureInitialized();
var account = _accountsData.Accounts.Find(a => a.AccountKey == accountKey);
if (account != null) {
account.AccessToken = accessToken;
account.ExpiresAt = expiresAt;
SaveAccountsData();
Debug.Log(
$"[TokenStorage] Updated access token for {account.DisplayName} (expires at {expiresAt})");
}
}
/// <summary>
/// Update display name for the current account.
/// Update display name after user sets it.
/// </summary>
public static void UpdateDisplayName(string displayName) {
EnsureInitialized();
var account = CurrentAccount;
if (account != null) {
account.DisplayName = displayName;
SaveAccountsData();
}
}
/// <summary>
/// Clear all stored accounts (full reset).
/// </summary>
public static void ClearAll() {
_accountsData = new StoredAccountsData();
PlayerPrefs.DeleteKey(AccountsDataKey);
DisplayName = displayName;
PlayerPrefs.Save();
Debug.Log("[TokenStorage] Cleared all accounts");
}
/// <summary>
/// Legacy Clear method - now just clears current selection, not tokens.
/// Clear all stored tokens (logout).
/// </summary>
public static void Clear() { ClearCurrentAccount(); }
public static void Clear() {
_cachedAccessToken = null;
_cachedRefreshToken = null;
_cachedExpiresAt = 0;
_cachedUserId = null;
_cachedDisplayName = null;
private static void EnsureInitialized() {
if (!_cacheInitialized) {
Debug.LogWarning("[TokenStorage] Cache not initialized, initializing now");
InitializeCache();
PlayerPrefs.DeleteKey(AccessTokenKey);
PlayerPrefs.DeleteKey(RefreshTokenKey);
PlayerPrefs.DeleteKey(ExpiresAtKey);
PlayerPrefs.DeleteKey(UserIdKey);
PlayerPrefs.DeleteKey(DisplayNameKey);
PlayerPrefs.Save();
Debug.Log("[TokenStorage] Cleared all tokens");
}
/// <summary>
/// Check if token needs refresh (expires within 5 minutes).
/// </summary>
public static bool NeedsRefresh {
get {
var expiresAt = ExpiresAt;
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return expiresAt > 0 && expiresAt - now < 300; // 5 minutes
}
}
}
@@ -80,6 +80,9 @@ public class AuthInterceptor : Interceptor {
};
public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public TMP_Dropdown environmentDropdown;
public TMP_InputField nameField;
public TMP_InputField passwordField;
public TMP_Dropdown resolutionDropdown;
[Header("OAuth UI")]
@@ -88,18 +91,17 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public Button googleLoginButton;
public TextMeshProUGUI oauthStatusText;
[Header("Stored Accounts")]
public GameObject storedAccountsContainer;
public GameObject storedAccountButtonPrefab;
public Sprite discordProviderIcon;
public Sprite googleProviderIcon;
[Header("Display Name Setup")]
public GameObject displayNamePanel;
public TMP_InputField displayNameField;
public Button setDisplayNameButton;
public TextMeshProUGUI displayNameErrorText;
[Header("Legacy Auth (for testing)")]
public GameObject legacyAuthPanel;
public Button useLegacyAuthButton;
public TextMeshProUGUI useLegacyAuthButtonText;
[Header("Status Display")]
public TextMeshProUGUI connectionStatusText;
@@ -112,7 +114,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public Button logoutButton;
public Button customBattleButton;
public Button cancelCustomBattleButton;
public TMP_Dropdown lobbyEnvironmentDropdown;
public TextMeshProUGUI lobbyEnvironmentText;
public TextMeshProUGUI lobbyUserText;
public GameObject runningGamesListArea;
@@ -140,7 +142,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
private CancellationTokenSource _cancellationTokenSource;
private readonly Object pendingReplyLock = new Object();
private int _connectedEnvironmentIndex = -1; // -1 means not connected
private List<GameObject> _storedAccountButtons = new List<GameObject>();
private bool _useOAuth = false; // Default to legacy until server-side OAuth is ready
public ClientPregeneratedText clientPregeneratedText;
@@ -164,7 +166,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
: null;
private string GetUrlFromEnvironment() {
int envIndex = PlayerPrefs.GetInt(EnvironmentKey, 0);
int envIndex = environmentDropdown.value;
string prefix = EnvironmentOptions[envIndex];
return prefix + BaseDomain;
}
@@ -197,6 +199,14 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
resolutionDropdown.AddOptions(resolutions.Select(r => $"{r.width} x {r.height}").ToList());
resolutionDropdown.value = resolutions.ToList().IndexOf(currentResolution);
// Set up environment dropdown
environmentDropdown.ClearOptions();
environmentDropdown.AddOptions(EnvironmentDisplayNames);
environmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
nameField.text = PlayerPrefs.GetString(NameKey, "sample_name");
passwordField.text = PlayerPrefs.GetString(PasswordKey, "sample_password");
connectionCanvas.gameObject.SetActive(true);
eagleCanvas.gameObject.SetActive(false);
shardokCanvas.gameObject.SetActive(false);
@@ -222,7 +232,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
private void SetupOAuthUI() {
// Set up OAuth button click handlers for new sign-ins
// Set up OAuth button click handlers
if (discordLoginButton != null) {
discordLoginButton.onClick.AddListener(
() => OnOAuthLoginClicked(OAuthProvider.Discord));
@@ -233,6 +243,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
if (setDisplayNameButton != null) {
setDisplayNameButton.onClick.AddListener(OnSetDisplayNameClicked);
}
if (useLegacyAuthButton != null) {
useLegacyAuthButton.onClick.AddListener(OnUseLegacyAuthClicked);
}
// Subscribe to OAuthManager events
if (OAuthManager.Instance != null) {
@@ -242,79 +255,24 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
OAuthManager.Instance.OnLogout += OnOAuthLogout;
}
// Show auth panel with stored accounts
// Show appropriate panel based on auth state
ShowAuthPanel();
}
private void ShowAuthPanel() {
// Hide display name panel
// Hide all auth panels first
if (oauthPanel != null) oauthPanel.SetActive(false);
if (displayNamePanel != null) displayNamePanel.SetActive(false);
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(false);
// Show OAuth panel
if (oauthPanel != null) oauthPanel.SetActive(true);
// Refresh stored account buttons
RefreshStoredAccountButtons();
}
private void RefreshStoredAccountButtons() {
// Clear existing buttons
foreach (var btn in _storedAccountButtons) {
if (btn != null) Destroy(btn);
}
_storedAccountButtons.Clear();
if (storedAccountsContainer == null || storedAccountButtonPrefab == null) return;
// Get stored accounts
var accounts = OAuthManager.Instance?.GetStoredAccounts();
if (accounts == null || accounts.Count == 0) return;
// Create button for each stored account
foreach (var account in accounts) {
var buttonObj =
Instantiate(storedAccountButtonPrefab, storedAccountsContainer.transform);
buttonObj.transform.localScale = Vector3.one;
// Set button text (just display name, icon shows provider)
var buttonText = buttonObj.GetComponentInChildren<TextMeshProUGUI>();
if (buttonText != null) { buttonText.text = account.DisplayName; }
// Set provider icon (look for child named "ProviderIcon")
var providerIconTransform = buttonObj.transform.Find("ProviderIcon");
if (providerIconTransform != null) {
var providerImage = providerIconTransform.GetComponent<Image>();
if (providerImage != null) {
var isGoogle = account.Provider.ToLowerInvariant() == "google";
providerImage.sprite = isGoogle ? googleProviderIcon : discordProviderIcon;
}
}
// Set click handler
var button = buttonObj.GetComponent<Button>();
if (button != null) {
var accountCopy = account; // Capture for lambda
button.onClick.AddListener(() => OnStoredAccountClicked(accountCopy));
}
_storedAccountButtons.Add(buttonObj);
}
}
private async void OnStoredAccountClicked(StoredAccount account) {
// Configure OAuthManager with URLs
OAuthManager.Instance.SetServerUrls(GetAuthServiceUrl(), GetEagleUrl());
if (oauthStatusText != null) {
oauthStatusText.text = $"Connecting as {account.DisplayName}...";
}
var success = await OAuthManager.Instance.ConnectWithStoredAccountAsync(account);
if (success) {
// OnLoginSuccess will handle connection to lobby
// Show appropriate panel based on _useOAuth
if (_useOAuth) {
if (oauthPanel != null) oauthPanel.SetActive(true);
if (useLegacyAuthButtonText != null)
useLegacyAuthButtonText.text = "Use Classic Sign-in";
} else {
// Failed - might need re-auth, refresh the buttons
RefreshStoredAccountButtons();
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(true);
if (useLegacyAuthButtonText != null) useLegacyAuthButtonText.text = "Use OAuth Sign-in";
}
}
@@ -324,56 +282,22 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
if (cancelCustomBattleButton != null) {
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
}
// Set up environment dropdown
if (lobbyEnvironmentDropdown != null) {
lobbyEnvironmentDropdown.ClearOptions();
lobbyEnvironmentDropdown.AddOptions(EnvironmentDisplayNames);
lobbyEnvironmentDropdown.value = PlayerPrefs.GetInt(EnvironmentKey, 0);
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
}
}
private void UpdateLobbyStatusDisplays() {
// Update environment dropdown selection
if (lobbyEnvironmentDropdown != null && _connectedEnvironmentIndex >= 0) {
// Temporarily remove listener to avoid triggering reconnect
lobbyEnvironmentDropdown.onValueChanged.RemoveListener(OnLobbyEnvironmentChanged);
lobbyEnvironmentDropdown.value = _connectedEnvironmentIndex;
lobbyEnvironmentDropdown.onValueChanged.AddListener(OnLobbyEnvironmentChanged);
// Show environment (prod/qa)
if (lobbyEnvironmentText != null) {
lobbyEnvironmentText.text = ConnectedEnvironmentName ?? "";
}
// Show current user from OAuth
if (lobbyUserText != null) { lobbyUserText.text = TokenStorage.DisplayName ?? ""; }
}
private void OnLobbyEnvironmentChanged(int newEnvironmentIndex) {
if (newEnvironmentIndex == _connectedEnvironmentIndex) return;
Debug.Log(
$"[ConnectionHandler] Switching environment from {ConnectedEnvironmentName} to {EnvironmentDisplayNames[newEnvironmentIndex]}");
// Disconnect from current environment
_persistentClientConnection?.Dispose();
eagleConnection?.Dispose();
_httpClient?.Dispose();
_persistentClientConnection = null;
eagleConnection = null;
_httpClient = null;
// Update environment index and reconnect
_connectedEnvironmentIndex = newEnvironmentIndex;
PlayerPrefs.SetInt(EnvironmentKey, newEnvironmentIndex);
// Clear current game lists while reconnecting
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }
// Reconnect to new environment
_createConnection();
RequestMaps();
StartListeningForLobbyUpdates();
// Show current user - prefer OAuth display name, fall back to classic login name
if (lobbyUserText != null) {
if (_useOAuth && !string.IsNullOrEmpty(TokenStorage.DisplayName)) {
lobbyUserText.text = TokenStorage.DisplayName;
} else {
lobbyUserText.text = nameField.text;
}
}
}
private async void OnLogoutClicked() {
@@ -486,7 +410,27 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
});
}
private void ConnectWithOAuth() { _internalConnectEagle(); }
private void OnUseLegacyAuthClicked() {
_useOAuth = !_useOAuth; // Toggle instead of just setting false
if (_useOAuth) {
// Show OAuth panel
if (oauthPanel != null) oauthPanel.SetActive(true);
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(false);
if (useLegacyAuthButtonText != null)
useLegacyAuthButtonText.text = "Use Classic Sign-in";
} else {
// Show legacy panel
if (oauthPanel != null) oauthPanel.SetActive(false);
if (legacyAuthPanel != null) legacyAuthPanel.SetActive(true);
if (useLegacyAuthButtonText != null) useLegacyAuthButtonText.text = "Use OAuth Sign-in";
}
}
private void ConnectWithOAuth() {
_useOAuth = true;
_internalConnectEagle();
}
private float _statusUpdateTimer = 0f;
private const float StatusUpdateInterval = 0.5f;
@@ -690,7 +634,10 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
private void _createConnection() {
_connectedEnvironmentIndex = PlayerPrefs.GetInt(EnvironmentKey, 0);
_connectedEnvironmentIndex = environmentDropdown.value;
PlayerPrefs.SetInt(EnvironmentKey, _connectedEnvironmentIndex);
PlayerPrefs.SetString(NameKey, nameField.text);
PlayerPrefs.SetString(PasswordKey, passwordField.text);
// Dispose existing connections before creating new ones
_httpClient?.Dispose();
@@ -703,18 +650,25 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
string url = GetUrlFromEnvironment();
// OAuth JWT-based connection
if (!TokenStorage.HasValidToken) {
Debug.LogError("[ConnectionHandler] No valid token available for connection");
return;
if (_useOAuth && TokenStorage.HasValidToken) {
// Use JWT-based connection
eagleConnection = EagleConnection.CreateWithJwt(url);
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", TokenStorage.AccessToken);
} else {
// Legacy Basic Auth connection
PlayerPrefs.SetString(NameKey, nameField.text);
PlayerPrefs.SetString(PasswordKey, passwordField.text);
eagleConnection = new EagleConnection(nameField.text, passwordField.text, url);
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", eagleConnection.authHeader);
}
eagleConnection = EagleConnection.CreateWithJwt(url);
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", TokenStorage.AccessToken);
_persistentClientConnection = new PersistentClientConnection(
eagleConnection.EagleGrpcClient,
eagleConnection.credentials,
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: af9f637b3a1f6433f819a0bcdb326453
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,427 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1262787222422545696
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6242087783640990751}
- component: {fileID: 5444015614922696687}
- component: {fileID: 8871991993173924918}
- component: {fileID: 6965305925514130928}
m_Layer: 5
m_Name: ProviderIcon
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6242087783640990751
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1262787222422545696}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 157686408557551303}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5444015614922696687
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1262787222422545696}
m_CullTransparentMesh: 1
--- !u!114 &8871991993173924918
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1262787222422545696}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 80
m_PreferredHeight: 50
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!114 &6965305925514130928
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1262787222422545696}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: b6b68c8c37d5cf1419267a80c426ac83, type: 3}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &5166012670666351313
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 157686408557551303}
- component: {fileID: 3634401029247236684}
- component: {fileID: 7790188191322585408}
- component: {fileID: 646268409010340088}
- component: {fileID: 3097176747511135857}
- component: {fileID: 7691398338434978342}
m_Layer: 5
m_Name: StoredAccountButton
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &157686408557551303
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5166012670666351313}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7974514965529143181}
- {fileID: 6242087783640990751}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3634401029247236684
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5166012670666351313}
m_CullTransparentMesh: 1
--- !u!114 &7790188191322585408
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5166012670666351313}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &646268409010340088
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5166012670666351313}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 7790188191322585408}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!114 &3097176747511135857
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5166012670666351313}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 400
m_PreferredHeight: 60
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!114 &7691398338434978342
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5166012670666351313}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 5
m_Spacing: 0
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
m_ChildControlHeight: 1
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!1 &5617793748733146591
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7974514965529143181}
- component: {fileID: 4513536456760004536}
- component: {fileID: 6210669843299902677}
- component: {fileID: 572895541873054588}
m_Layer: 5
m_Name: Label
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7974514965529143181
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5617793748733146591}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 157686408557551303}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4513536456760004536
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5617793748733146591}
m_CullTransparentMesh: 1
--- !u!114 &6210669843299902677
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5617793748733146591}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: nolen (dan@danielcrosby.net)
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 18
m_fontSizeBase: 18
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!114 &572895541873054588
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5617793748733146591}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: 60
m_FlexibleWidth: 1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 6440904d10a244010b3a91e0aa247749
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -6,7 +6,7 @@ TextureImporter:
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,13 +37,13 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 8
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -100,7 +100,7 @@ TextureImporter:
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
spriteID:
internalID: 0
vertices: []
indices:
@@ -6,7 +6,7 @@ TextureImporter:
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,13 +37,13 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 8
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -100,7 +100,7 @@ TextureImporter:
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
spriteID:
internalID: 0
vertices: []
indices:
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,6 @@ using System.IO;
using common;
using eagle;
using Net.Eagle0.Shardok.Api;
using Shardok;
using TMPro;
using UnityEditor;
using UnityEngine;
@@ -29,9 +28,6 @@ public class SettingsPanelController : MonoBehaviour {
public Toggle tooltipHugsCursorToggle;
public HoveringTooltip hoveringTooltip;
public Toggle showAnimationTestsToggle;
public AnimationTestController animationTestController;
public Slider autoScrollSpeedSlider;
public TMP_Text autoScrollSpeedLabel;
@@ -43,7 +39,6 @@ public class SettingsPanelController : MonoBehaviour {
private const string MusicVolumeKey = "musicVolume";
public const string UseGoDiceKey = "useGoDice";
private const string TooltipHugsCursorKey = "tooltipHugsCursor";
private const string AnimationToggleKey = "animationToggle";
public EagleGameController eagleGameController;
public GameObject ShardokCanvas;
@@ -64,9 +59,6 @@ public class SettingsPanelController : MonoBehaviour {
tooltipHugsCursorToggle.isOn = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
hoveringTooltip.HugsCursor = tooltipHugsCursorToggle.isOn;
showAnimationTestsToggle.isOn = PlayerPrefs.GetInt(AnimationToggleKey, 0) == 1;
animationTestController.gameObject.SetActive(showAnimationTestsToggle.isOn);
if (autoScrollSpeedSlider != null) {
autoScrollSpeedSlider.value = AutoScrollingText.GlobalScrollSpeedMultiplier;
UpdateAutoScrollSpeedLabel(autoScrollSpeedSlider.value);
@@ -124,11 +116,6 @@ public class SettingsPanelController : MonoBehaviour {
hoveringTooltip.HugsCursor = val;
}
public void OnAnimationTestToggleChange(bool val) {
PlayerPrefs.SetInt(AnimationToggleKey, val ? 1 : 0);
animationTestController.gameObject.SetActive(val);
}
public void OnAutoScrollSpeedSliderChange(float val) {
AutoScrollingText.GlobalScrollSpeedMultiplier = val;
UpdateAutoScrollSpeedLabel(val);
@@ -1,169 +0,0 @@
fileFormatVersion: 2
guid: 32f7f3d380ec1413590676a13c22324c
TextureImporter:
internalIDToNameTable:
- first:
213: 7482667652216324306
second: Square
externalObjects: {}
serializedVersion: 11
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMasterTextureLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 256
spriteBorder: {x: 4, y: 4, z: 4, w: 4}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: 0
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: Triangle
rect:
serializedVersion: 2
x: 0
y: 0
width: 256
height: 256
alignment: 9
pivot: {x: 0.5, y: 0.28866667}
border: {x: 0, y: 0, z: 0, w: 0}
outline:
- - {x: 0, y: 93.7025033688}
- {x: -128, y: -128}
- {x: 128, y: -128}
physicsShape:
- - {x: 0, y: 93.7025033688}
- {x: -128, y: -128}
- {x: 128, y: -128}
tessellationDetail: 0
bones: []
spriteID: 2d009a6b596c7d760800000000000000
internalID: 7482667652216324306
vertices: []
indices:
edges: []
weights: []
outline: []
physicsShape:
- - {x: -128, y: 128}
- {x: -128, y: -128}
- {x: 128, y: -128}
- {x: 128, y: 128}
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable:
Triangle: 7482667652216324306
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 7fee21f21c4f146f69dd2eae14a94b4b
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: bfa8cdc5e73d54ed1876c17a47ff7346
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: bd23b09c6f27b4754a769858a2020668
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 3fe839b58f5fd46d695ece8ccf2412a5
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,4 +1,3 @@
using System.Collections;
using UnityEngine;
namespace Shardok {
@@ -126,30 +125,13 @@ namespace Shardok {
Debug.Log("Melee sequence complete!");
}
public void TestMove() { StartCoroutine(TestMoveSequence()); }
private IEnumerator TestMoveSequence() {
Debug.Log($"Testing Move (Infantry): {sourceCell} -> {targetCell}");
public void TestMove() {
Debug.Log($"Testing Move: {sourceCell} -> {targetCell}");
if (_moveAnimator != null) {
_moveAnimator.AnimateMove(
sourceCell,
targetCell,
Net.Eagle0.Shardok.Api.BattalionView.Types.BattalionTypeId.LightInfantry);
_moveAnimator.AnimateMove(sourceCell, targetCell);
PlaySound(Net.Eagle0.Shardok.Common.ActionType.Move);
} else {
Debug.LogWarning("MoveAnimator not available");
yield break;
}
yield return new WaitForSeconds(1.5f);
Debug.Log($"Testing Move (Cavalry): {sourceCell} -> {targetCell}");
if (_moveAnimator != null) {
_moveAnimator.AnimateMove(
sourceCell,
targetCell,
Net.Eagle0.Shardok.Api.BattalionView.Types.BattalionTypeId.LightCavalry);
PlaySound(Net.Eagle0.Shardok.Common.ActionType.Move);
}
}
@@ -59,8 +59,8 @@ namespace Shardok {
return;
}
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
Vector2? sourcePos = __hexGrid.GetCellLocalPosition(sourceCellIndex);
Vector2? targetPos = __hexGrid.GetCellLocalPosition(targetCellIndex);
if (sourcePos == null || targetPos == null) {
Debug.LogWarning(
@@ -60,7 +60,7 @@ namespace Shardok {
}
Vector2? sourcePos = __hexGrid.GetCellLocalPosition(sourceCellIndex);
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
Vector2? targetPos = __hexGrid.GetCellLocalPosition(targetCellIndex);
if (sourcePos == null || targetPos == null) {
Debug.LogWarning(
@@ -7,8 +7,7 @@ using static Net.Eagle0.Shardok.Api.BattalionView.Types;
namespace Shardok {
/// <summary>
/// Animates charge attacks between two hexes.
/// Shows attacker's weapon thrusting toward defender with accelerating motion
/// and a violent flash on impact.
/// Similar to MeleeAnimator but with more dramatic motion for charging units.
/// </summary>
public class ChargeAnimator : MonoBehaviour {
[Header("Weapon Sprites")]
@@ -30,41 +29,28 @@ namespace Shardok {
[Tooltip("Bone/claw sprite for undead charge")]
public Sprite boneSprite;
[Header("Impact Flash Settings")]
[Tooltip("Sprite for impact flash")]
public Sprite flashSprite;
[Tooltip("Color of impact flash")]
public Color flashColor = new Color(1f, 1f, 0.8f, 0.9f);
[Tooltip("Scale of impact flash")]
public float flashScale = 35f;
[Tooltip("Duration of flash")]
public float flashDuration = 0.1f;
[Header("Animation Settings")]
[Tooltip("Base scale of weapon sprites")]
public float weaponScale = 25.0f;
[Tooltip("Number of thrusts")]
public int thrustCount = 2;
[Tooltip("Duration of the charge thrust in seconds")]
public float chargeDuration = 0.15f;
[Tooltip("Duration of each thrust (slow to fast)")]
public float thrustDuration = 0.25f;
[Tooltip("Number of impact cycles")]
public int impactCount = 2;
[Tooltip("Duration of pullback between thrusts")]
public float pullbackDuration = 0.15f;
[Tooltip("Distance weapons travel toward each other (in local units)")]
public float thrustDistance = 35f;
[Tooltip("Starting position offset from midpoint (in local units)")]
public float startOffset = 60f;
[Tooltip("End position offset from midpoint (in local units)")]
public float endOffset = 15f;
[Tooltip("Offset from center point along attack axis (in local units)")]
public float weaponOffset = 50f;
[Tooltip("Base rotation offset for diagonal sprites (degrees)")]
public float baseRotationOffset = -45f;
[Tooltip("Amount of random shake (in local units)")]
public float shakeIntensity = 12f;
private HexGrid __hexGrid;
private Coroutine _activeAnimation;
@@ -73,6 +59,10 @@ namespace Shardok {
/// <summary>
/// Starts a charge animation between two hex cells.
/// </summary>
/// <param name="attackerCellIndex">Grid index of the charging hex</param>
/// <param name="defenderCellIndex">Grid index of the defending hex</param>
/// <param name="attackerType">Battalion type of the attacker</param>
/// <param name="defenderType">Battalion type of the defender</param>
public void AnimateCharge(
int attackerCellIndex,
int defenderCellIndex,
@@ -89,8 +79,8 @@ namespace Shardok {
return;
}
Vector2? attackerPos = __hexGrid.GetCellCenterPosition(attackerCellIndex);
Vector2? defenderPos = __hexGrid.GetCellCenterPosition(defenderCellIndex);
Vector2? attackerPos = __hexGrid.GetCellLocalPosition(attackerCellIndex);
Vector2? defenderPos = __hexGrid.GetCellLocalPosition(defenderCellIndex);
if (attackerPos == null || defenderPos == null) {
Debug.LogWarning(
@@ -100,148 +90,238 @@ namespace Shardok {
if (_activeAnimation != null) { StopCoroutine(_activeAnimation); }
_activeAnimation = StartCoroutine(
AnimateChargeThrusts(attackerPos.Value, defenderPos.Value, attackerType));
Debug.Log(
$"ChargeAnimator: Starting charge from cell {attackerCellIndex} ({attackerType}) to {defenderCellIndex} ({defenderType})");
_activeAnimation = StartCoroutine(AnimateChargeClash(
attackerPos.Value,
defenderPos.Value,
attackerType,
defenderType));
}
private Sprite GetWeaponSprite(BattalionTypeId type) {
private struct WeaponConfig {
public Sprite sprite;
public float scaleMultiplier;
public float violenceMultiplier;
}
private WeaponConfig GetWeaponConfig(BattalionTypeId type) {
switch (type) {
case BattalionTypeId.LightInfantry: return swordSprite;
case BattalionTypeId.HeavyInfantry: return maceSprite ?? swordSprite;
case BattalionTypeId.LightInfantry:
return new WeaponConfig {
sprite = swordSprite,
scaleMultiplier = 1.0f,
violenceMultiplier = 1.2f
};
case BattalionTypeId.HeavyInfantry:
return new WeaponConfig {
sprite = maceSprite ?? swordSprite,
scaleMultiplier = 1.2f,
violenceMultiplier = 1.4f
};
case BattalionTypeId.LightCavalry:
return smallSpearSprite ?? largeLanceSprite ?? swordSprite;
return new WeaponConfig {
sprite = smallSpearSprite ?? largeLanceSprite ?? swordSprite,
scaleMultiplier = 1.1f,
violenceMultiplier = 1.6f
};
case BattalionTypeId.HeavyCavalry:
return largeLanceSprite ?? smallSpearSprite ?? swordSprite;
case BattalionTypeId.Longbowmen: return daggerSprite ?? swordSprite;
case BattalionTypeId.Undead: return boneSprite ?? swordSprite;
default: return swordSprite;
return new WeaponConfig {
sprite = largeLanceSprite ?? smallSpearSprite ?? swordSprite,
scaleMultiplier = 1.4f,
violenceMultiplier = 2.0f
};
case BattalionTypeId.Longbowmen:
return new WeaponConfig {
sprite = daggerSprite ?? swordSprite,
scaleMultiplier = 0.8f,
violenceMultiplier = 1.0f
};
case BattalionTypeId.Undead:
return new WeaponConfig {
sprite = boneSprite ?? swordSprite,
scaleMultiplier = 1.0f,
violenceMultiplier = 1.3f
};
default:
return new WeaponConfig {
sprite = swordSprite,
scaleMultiplier = 1.0f,
violenceMultiplier = 1.2f
};
}
}
private float GetScaleMultiplier(BattalionTypeId type) {
switch (type) {
case BattalionTypeId.HeavyInfantry: return 1.2f;
case BattalionTypeId.LightCavalry: return 1.1f;
case BattalionTypeId.HeavyCavalry: return 1.4f;
case BattalionTypeId.Longbowmen: return 0.8f;
default: return 1.0f;
}
}
private IEnumerator AnimateChargeThrusts(
private IEnumerator AnimateChargeClash(
Vector2 attackerPos,
Vector2 defenderPos,
BattalionTypeId attackerType) {
// Calculate direction toward defender
BattalionTypeId attackerType,
BattalionTypeId defenderType) {
// Calculate midpoint and direction
Vector2 midpoint = (attackerPos + defenderPos) / 2f;
Vector2 direction = (defenderPos - attackerPos).normalized;
float baseAngle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
// Create attacker weapon
Sprite weaponSprite = GetWeaponSprite(attackerType);
float scale = weaponScale * GetScaleMultiplier(attackerType);
// Get weapon configs
WeaponConfig attackerConfig = GetWeaponConfig(attackerType);
WeaponConfig defenderConfig = GetWeaponConfig(defenderType);
// Create weapons
GameObject attackerWeapon = CreateWeapon(attackerConfig, baseAngle);
GameObject defenderWeapon = CreateWeapon(defenderConfig, baseAngle + 180f);
float attackerViolence = shakeIntensity * attackerConfig.violenceMultiplier;
float defenderViolence = shakeIntensity * defenderConfig.violenceMultiplier;
// Starting positions offset from midpoint (further back for charge buildup)
Vector3 attackerStart = new Vector3(
midpoint.x - direction.x * weaponOffset,
midpoint.y - direction.y * weaponOffset,
10f);
Vector3 defenderStart = new Vector3(
midpoint.x + direction.x * weaponOffset,
midpoint.y + direction.y * weaponOffset,
10f);
// Clash point (closer to midpoint)
Vector3 attackerClash = new Vector3(
midpoint.x - direction.x * (weaponOffset - thrustDistance),
midpoint.y - direction.y * (weaponOffset - thrustDistance),
10f);
Vector3 defenderClash = new Vector3(
midpoint.x + direction.x * (weaponOffset - thrustDistance),
midpoint.y + direction.y * (weaponOffset - thrustDistance),
10f);
attackerWeapon.transform.localPosition = attackerStart;
defenderWeapon.transform.localPosition = defenderStart;
// Animate impact cycles
for (int i = 0; i < impactCount; i++) {
// Charge thrust toward clash point
yield return StartCoroutine(AnimateWeaponThrust(
attackerWeapon,
attackerStart,
attackerClash,
chargeDuration / 2f,
true,
attackerViolence));
yield return StartCoroutine(AnimateWeaponThrust(
defenderWeapon,
defenderStart,
defenderClash,
chargeDuration / 2f,
true,
defenderViolence));
// Violent impact shake
float impactViolence = Mathf.Max(attackerViolence, defenderViolence);
yield return StartCoroutine(ImpactShake(
attackerWeapon,
defenderWeapon,
attackerClash,
defenderClash,
0.08f,
impactViolence));
// Pull back
yield return StartCoroutine(AnimateWeaponThrust(
attackerWeapon,
attackerClash,
attackerStart,
chargeDuration / 2f,
false,
attackerViolence * 0.5f));
yield return StartCoroutine(AnimateWeaponThrust(
defenderWeapon,
defenderClash,
defenderStart,
chargeDuration / 2f,
false,
defenderViolence * 0.5f));
}
// Cleanup
if (attackerWeapon != null) { Destroy(attackerWeapon); }
if (defenderWeapon != null) { Destroy(defenderWeapon); }
_activeAnimation = null;
}
private GameObject CreateWeapon(WeaponConfig config, float angle) {
GameObject weapon = new GameObject("ChargeWeapon");
weapon.transform.SetParent(__hexGrid.gridCanvas.transform, false);
SpriteRenderer renderer = weapon.AddComponent<SpriteRenderer>();
renderer.sprite = weaponSprite;
renderer.sprite = config.sprite;
renderer.sortingLayerName = "Effects";
weapon.transform.localRotation = Quaternion.Euler(0, 0, baseAngle + baseRotationOffset);
weapon.transform.localRotation = Quaternion.Euler(0, 0, angle + baseRotationOffset);
float scale = weaponScale * config.scaleMultiplier;
weapon.transform.localScale = new Vector3(scale, scale, scale);
// Calculate positions
Vector3 startPos = new Vector3(
midpoint.x - direction.x * startOffset,
midpoint.y - direction.y * startOffset,
10f);
Vector3 endPos = new Vector3(
midpoint.x + direction.x * endOffset,
midpoint.y + direction.y * endOffset,
10f);
weapon.transform.localPosition = startPos;
// Animate thrusts
for (int i = 0; i < thrustCount; i++) {
// Thrust forward - starts slow, accelerates
float elapsed = 0f;
while (elapsed < thrustDuration) {
if (weapon == null) yield break;
float t = elapsed / thrustDuration;
// Cubic ease-in for accelerating thrust
float easeT = t * t * t;
weapon.transform.localPosition = Vector3.Lerp(startPos, endPos, easeT);
elapsed += Time.deltaTime;
yield return null;
}
weapon.transform.localPosition = endPos;
// Impact flash
if (flashSprite != null) {
yield return StartCoroutine(ShowImpactFlash(endPos, direction));
}
// Pullback - smooth ease out
elapsed = 0f;
while (elapsed < pullbackDuration) {
if (weapon == null) yield break;
float t = elapsed / pullbackDuration;
// Ease out for smooth pullback
float easeT = t * (2f - t);
weapon.transform.localPosition = Vector3.Lerp(endPos, startPos, easeT);
elapsed += Time.deltaTime;
yield return null;
}
weapon.transform.localPosition = startPos;
// Brief pause between thrusts
if (i < thrustCount - 1) { yield return new WaitForSeconds(0.05f); }
}
// Cleanup
if (weapon != null) { Destroy(weapon); }
_activeAnimation = null;
return weapon;
}
private IEnumerator ShowImpactFlash(Vector3 position, Vector2 direction) {
GameObject flash = new GameObject("ChargeFlash");
flash.transform.SetParent(__hexGrid.gridCanvas.transform, false);
flash.transform.localPosition = position;
SpriteRenderer renderer = flash.AddComponent<SpriteRenderer>();
renderer.sprite = flashSprite;
renderer.sortingLayerName = "Effects";
renderer.color = flashColor;
private IEnumerator AnimateWeaponThrust(
GameObject weapon,
Vector3 fromPos,
Vector3 toPos,
float duration,
bool isCharging,
float violence) {
float elapsed = 0f;
while (elapsed < flashDuration) {
float t = elapsed / flashDuration;
// Quick expand then fade
float expandT = t < 0.3f ? t / 0.3f : 1f;
float scale = flashScale * (0.5f + 0.5f * expandT);
flash.transform.localScale = Vector3.one * scale;
while (elapsed < duration) {
if (weapon == null) { yield break; }
// Fade out
Color c = flashColor;
c.a = flashColor.a * (1f - t);
renderer.color = c;
float t = elapsed / duration;
// More aggressive ease for charge, smoother for retreat
float smoothT = isCharging ? t * t * t : t * (2f - t);
// Position with shake
Vector3 shake = new Vector3(
Random.Range(-violence, violence),
Random.Range(-violence, violence),
0f);
weapon.transform.localPosition = Vector3.Lerp(fromPos, toPos, smoothT) + shake;
elapsed += Time.deltaTime;
yield return null;
}
Destroy(flash);
if (weapon != null) { weapon.transform.localPosition = toPos; }
}
private IEnumerator ImpactShake(
GameObject weapon1,
GameObject weapon2,
Vector3 pos1,
Vector3 pos2,
float duration,
float violence) {
float elapsed = 0f;
float intensity = violence * 2.5f; // Extra intense for charge impact
while (elapsed < duration) {
if (weapon1 == null || weapon2 == null) { yield break; }
Vector3 shake1 = new Vector3(
Random.Range(-intensity, intensity),
Random.Range(-intensity, intensity),
0f);
Vector3 shake2 = new Vector3(
Random.Range(-intensity, intensity),
Random.Range(-intensity, intensity),
0f);
weapon1.transform.localPosition = pos1 + shake1;
weapon2.transform.localPosition = pos2 + shake2;
elapsed += Time.deltaTime;
yield return null;
}
}
/// <summary>
@@ -12,13 +12,7 @@ namespace Shardok {
public Sprite dropletSprite;
[Tooltip("Number of droplets")]
public int dropletCount = 25;
[Tooltip("Minimum droplet scale")]
public float dropletScaleMin = 5f;
[Tooltip("Maximum droplet scale")]
public float dropletScaleMax = 15f;
public int dropletCount = 12;
[Tooltip("Color of water")]
public Color waterColor = new Color(0.3f, 0.6f, 0.9f, 0.8f);
@@ -28,29 +22,23 @@ namespace Shardok {
public Sprite steamSprite;
[Tooltip("Number of steam puffs")]
public int steamCount = 8;
public int steamCount = 5;
[Tooltip("Color of steam")]
public Color steamColor = new Color(0.9f, 0.9f, 0.9f, 0.6f);
[Header("Animation Settings")]
[Tooltip("Height water falls from")]
public float fallHeight = 100f;
public float fallHeight = 60f;
[Tooltip("Spread radius of the water")]
public float waterSpread = 40f;
public float waterSpread = 25f;
[Tooltip("Duration of the water fall")]
public float fallDuration = 0.5f;
[Tooltip("Time spread for staggered droplet launch")]
public float launchSpread = 0.25f;
[Tooltip("Horizontal chaos during fall")]
public float fallChaos = 15f;
public float fallDuration = 0.3f;
[Tooltip("Duration of the steam effect")]
public float steamDuration = 0.5f;
public float steamDuration = 0.4f;
private HexGrid __hexGrid;
@@ -84,7 +72,6 @@ namespace Shardok {
// Create droplets
GameObject[] droplets = new GameObject[dropletCount];
Vector2[] dropletOffsets = new Vector2[dropletCount];
Vector2[] dropletChaos = new Vector2[dropletCount];
float[] dropletDelays = new float[dropletCount];
for (int i = 0; i < dropletCount; i++) {
@@ -97,23 +84,17 @@ namespace Shardok {
renderer.sprite = dropletSprite;
renderer.sortingLayerName = "Effects";
renderer.color = waterColor;
droplet.transform.localScale =
Vector3.one * Random.Range(dropletScaleMin, dropletScaleMax);
droplet.transform.localScale = Vector3.one * Random.Range(4f, 8f);
float angle = Random.Range(0f, Mathf.PI * 2f);
float radius = waterSpread * Random.Range(0f, 1f);
dropletOffsets[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
dropletDelays[i] = Random.Range(0f, launchSpread);
dropletDelays[i] = Random.Range(0f, fallDuration * 0.3f);
// Random horizontal chaos direction for each droplet
float chaosAngle = Random.Range(0f, Mathf.PI * 2f);
dropletChaos[i] =
new Vector2(Mathf.Cos(chaosAngle), Mathf.Sin(chaosAngle)) * fallChaos;
// Start above with some horizontal spread
// Start above
droplet.transform.localPosition = new Vector3(
target.x + dropletOffsets[i].x * 0.3f,
target.y + fallHeight + Random.Range(-10f, 10f),
target.x + dropletOffsets[i].x,
target.y + fallHeight + dropletOffsets[i].y * 0.5f,
5f);
droplets[i] = droplet;
@@ -121,27 +102,23 @@ namespace Shardok {
// Animate falling water
float elapsed = 0f;
float totalDuration = fallDuration + launchSpread;
while (elapsed < totalDuration) {
while (elapsed < fallDuration) {
float t = elapsed / fallDuration;
for (int i = 0; i < droplets.Length; i++) {
if (droplets[i] == null) continue;
float dropletT = Mathf.Clamp01(
(elapsed - dropletDelays[i]) /
(fallDuration - dropletDelays[i] * 0.5f));
(elapsed - dropletDelays[i]) / (fallDuration - dropletDelays[i]));
if (dropletT <= 0) continue;
// Accelerating fall
float fallT = dropletT * dropletT;
// Add chaotic horizontal movement (sine wave pattern)
float chaosT = Mathf.Sin(dropletT * Mathf.PI * 2f) * (1f - dropletT);
Vector2 chaos = dropletChaos[i] * chaosT;
float x = target.x + dropletOffsets[i].x + chaos.x;
float y = target.y + fallHeight * (1f - fallT);
droplets[i].transform.localPosition = new Vector3(x, y, 5f);
droplets[i].transform.localPosition =
new Vector3(target.x + dropletOffsets[i].x, y, 5f);
}
elapsed += Time.deltaTime;
@@ -18,11 +18,11 @@ namespace Shardok {
[Tooltip("Mace/hammer sprite for heavy infantry")]
public Sprite maceSprite;
[Tooltip("Falchion sprite for light cavalry")]
public Sprite falchionSprite;
[Tooltip("Small spear sprite for light cavalry")]
public Sprite smallSpearSprite;
[Tooltip("Two-handed sword sprite for heavy cavalry")]
public Sprite twoHandedSwordSprite;
[Tooltip("Large spear/lance sprite for heavy cavalry")]
public Sprite largeSpearSprite;
[Tooltip("Dagger sprite for longbowmen")]
public Sprite daggerSprite;
@@ -30,57 +30,15 @@ namespace Shardok {
[Tooltip("Bone/fist sprite for undead")]
public Sprite boneSprite;
[Header("Weapon Orientation - Sword")]
[Tooltip("Base rotation offset for sword (degrees)")]
public float swordRotationOffset = -45f;
[Tooltip("Flip sword horizontally")]
public bool swordFlipHorizontal = false;
[Header("Weapon Orientation - Mace")]
[Tooltip("Base rotation offset for mace (degrees)")]
public float maceRotationOffset = -45f;
[Tooltip("Flip mace horizontally")]
public bool maceFlipHorizontal = false;
[Header("Weapon Orientation - Falchion")]
[Tooltip("Base rotation offset for falchion (degrees)")]
public float falchionRotationOffset = -45f;
[Tooltip("Flip falchion horizontally")]
public bool falchionFlipHorizontal = false;
[Header("Weapon Orientation - Two-Handed Sword")]
[Tooltip("Base rotation offset for two-handed sword (degrees)")]
public float twoHandedSwordRotationOffset = -45f;
[Tooltip("Flip two-handed sword horizontally")]
public bool twoHandedSwordFlipHorizontal = false;
[Header("Weapon Orientation - Dagger")]
[Tooltip("Base rotation offset for dagger (degrees)")]
public float daggerRotationOffset = -45f;
[Tooltip("Flip dagger horizontally")]
public bool daggerFlipHorizontal = false;
[Header("Weapon Orientation - Bone")]
[Tooltip("Base rotation offset for bone (degrees)")]
public float boneRotationOffset = -45f;
[Tooltip("Flip bone horizontally")]
public bool boneFlipHorizontal = false;
[Header("Animation Settings")]
[Tooltip("Base scale of weapon sprites")]
public float weaponScale = 20.0f;
[Tooltip("Duration of a single swing cycle in seconds")]
public float clashDuration = 0.3f;
[Tooltip("Duration of a single clash cycle in seconds")]
public float clashDuration = 0.12f;
[Tooltip("Number of swing cycles")]
public int clashCount = 2;
[Tooltip("Number of clash cycles")]
public int clashCount = 3;
[Tooltip("Distance weapons travel toward each other (in local units)")]
public float thrustDistance = 25f;
@@ -88,11 +46,14 @@ namespace Shardok {
[Tooltip("Offset from center point along attack axis (in local units)")]
public float weaponOffset = 40f;
[Tooltip("Swing arc angle for swinging weapons (degrees)")]
public float swingArc = 90f;
[Tooltip("Base rotation offset for diagonal sprites (degrees)")]
public float baseRotationOffset = -45f;
[Tooltip("Amount of shake at impact only (in local units)")]
public float shakeIntensity = 3f;
[Tooltip("Swing arc angle for swinging weapons (degrees)")]
public float swingArc = 60f;
[Tooltip("Amount of random shake (in local units)")]
public float shakeIntensity = 8f;
private HexGrid __hexGrid;
private Coroutine _activeAnimation;
@@ -111,8 +72,8 @@ namespace Shardok {
int defenderCellIndex,
BattalionTypeId attackerType,
BattalionTypeId defenderType) {
if (swordSprite == null && maceSprite == null && falchionSprite == null &&
twoHandedSwordSprite == null && daggerSprite == null && boneSprite == null) {
if (swordSprite == null && maceSprite == null && smallSpearSprite == null &&
largeSpearSprite == null && daggerSprite == null && boneSprite == null) {
Debug.LogWarning("MeleeAnimator: No weapon sprites assigned");
return;
}
@@ -143,8 +104,7 @@ namespace Shardok {
public Sprite sprite;
public bool swings; // true = swing arc, false = thrust
public float scaleMultiplier;
public float rotationOffset;
public bool flipHorizontal;
public float violenceMultiplier; // shake intensity multiplier
}
private WeaponConfig GetWeaponConfig(BattalionTypeId type) {
@@ -154,66 +114,49 @@ namespace Shardok {
sprite = swordSprite,
swings = true,
scaleMultiplier = 1.0f,
rotationOffset = swordRotationOffset,
flipHorizontal = swordFlipHorizontal
violenceMultiplier = 0.3f
};
case BattalionTypeId.HeavyInfantry:
return new WeaponConfig {
sprite = maceSprite ?? swordSprite,
swings = true,
scaleMultiplier = 1.2f,
rotationOffset =
maceSprite != null ? maceRotationOffset : swordRotationOffset,
flipHorizontal =
maceSprite != null ? maceFlipHorizontal : swordFlipHorizontal
violenceMultiplier = 0.4f
};
case BattalionTypeId.LightCavalry:
return new WeaponConfig {
sprite = falchionSprite ?? swordSprite,
swings = true,
scaleMultiplier = 1.0f,
rotationOffset = falchionSprite != null ? falchionRotationOffset
: swordRotationOffset,
flipHorizontal = falchionSprite != null ? falchionFlipHorizontal
: swordFlipHorizontal
sprite = smallSpearSprite ?? largeSpearSprite ?? swordSprite,
swings = false,
scaleMultiplier = 0.9f,
violenceMultiplier = 1.0f
};
case BattalionTypeId.HeavyCavalry:
return new WeaponConfig {
sprite = twoHandedSwordSprite ?? swordSprite,
swings = true,
sprite = largeSpearSprite ?? smallSpearSprite ?? swordSprite,
swings = false,
scaleMultiplier = 1.4f,
rotationOffset = twoHandedSwordSprite != null ? twoHandedSwordRotationOffset
: swordRotationOffset,
flipHorizontal = twoHandedSwordSprite != null ? twoHandedSwordFlipHorizontal
: swordFlipHorizontal
violenceMultiplier = 1.8f
};
case BattalionTypeId.Longbowmen:
return new WeaponConfig {
sprite = daggerSprite ?? swordSprite,
swings = false,
scaleMultiplier = 0.7f,
rotationOffset =
daggerSprite != null ? daggerRotationOffset : swordRotationOffset,
flipHorizontal =
daggerSprite != null ? daggerFlipHorizontal : swordFlipHorizontal
violenceMultiplier = 0.8f
};
case BattalionTypeId.Undead:
return new WeaponConfig {
sprite = boneSprite ?? swordSprite,
swings = true,
scaleMultiplier = 1.0f,
rotationOffset =
boneSprite != null ? boneRotationOffset : swordRotationOffset,
flipHorizontal =
boneSprite != null ? boneFlipHorizontal : swordFlipHorizontal
violenceMultiplier = 0.5f
};
default:
return new WeaponConfig {
sprite = swordSprite,
swings = true,
scaleMultiplier = 1.0f,
rotationOffset = swordRotationOffset,
flipHorizontal = swordFlipHorizontal
violenceMultiplier = 0.3f
};
}
}
@@ -238,6 +181,8 @@ namespace Shardok {
bool attackerSwings = attackerConfig.swings;
bool defenderSwings = defenderConfig.swings;
float attackerViolence = shakeIntensity * attackerConfig.violenceMultiplier;
float defenderViolence = shakeIntensity * defenderConfig.violenceMultiplier;
// Starting positions offset from midpoint
Vector3 attackerStart = new Vector3(
@@ -262,15 +207,15 @@ namespace Shardok {
attackerWeapon.transform.localPosition = attackerStart;
defenderWeapon.transform.localPosition = defenderStart;
float attackerBaseAngle = baseAngle + attackerConfig.rotationOffset;
float defenderBaseAngle = baseAngle + 180f + defenderConfig.rotationOffset;
float attackerBaseAngle = baseAngle + baseRotationOffset;
float defenderBaseAngle = baseAngle + 180f + baseRotationOffset;
// Animate swing cycles with alternating swing directions
// Animate clash cycles with alternating swing directions
for (int i = 0; i < clashCount; i++) {
// Alternate swing direction each cycle
float swingDirection = (i % 2 == 0) ? 1f : -1f;
// Swing toward clash point
// Swing/thrust toward clash point
yield return StartCoroutine(AnimateWeaponSwing(
attackerWeapon,
attackerStart,
@@ -279,7 +224,8 @@ namespace Shardok {
attackerSwings ? -swingArc * swingDirection : 0f,
attackerSwings ? 0f : 0f,
clashDuration / 2f,
true));
true,
attackerViolence));
yield return StartCoroutine(AnimateWeaponSwing(
defenderWeapon,
@@ -289,18 +235,20 @@ namespace Shardok {
defenderSwings ? swingArc * swingDirection : 0f,
defenderSwings ? 0f : 0f,
clashDuration / 2f,
true));
true,
defenderViolence));
// Brief shake at impact
// Brief violent shake at impact (use max violence of both)
float impactViolence = Mathf.Max(attackerViolence, defenderViolence);
yield return StartCoroutine(ImpactShake(
attackerWeapon,
defenderWeapon,
attackerClash,
defenderClash,
0.05f,
shakeIntensity));
impactViolence));
// Swing back
// Swing/pull back
yield return StartCoroutine(AnimateWeaponSwing(
attackerWeapon,
attackerClash,
@@ -309,7 +257,8 @@ namespace Shardok {
0f,
attackerSwings ? -swingArc * swingDirection : 0f,
clashDuration / 2f,
false));
false,
attackerViolence));
yield return StartCoroutine(AnimateWeaponSwing(
defenderWeapon,
@@ -319,7 +268,8 @@ namespace Shardok {
0f,
defenderSwings ? swingArc * swingDirection : 0f,
clashDuration / 2f,
false));
false,
defenderViolence));
}
// Cleanup
@@ -336,10 +286,9 @@ namespace Shardok {
renderer.sprite = config.sprite;
renderer.sortingLayerName = "Effects";
weapon.transform.localRotation = Quaternion.Euler(0, 0, angle + config.rotationOffset);
weapon.transform.localRotation = Quaternion.Euler(0, 0, angle + baseRotationOffset);
float scale = weaponScale * config.scaleMultiplier;
float xScale = config.flipHorizontal ? -scale : scale;
weapon.transform.localScale = new Vector3(xScale, scale, scale);
weapon.transform.localScale = new Vector3(scale, scale, scale);
return weapon;
}
@@ -352,20 +301,25 @@ namespace Shardok {
float fromAngleOffset,
float toAngleOffset,
float duration,
bool isAttacking) {
bool isAttacking,
float violence) {
float elapsed = 0f;
while (elapsed < duration) {
if (weapon == null) { yield break; }
float t = elapsed / duration;
// Smooth ease for deliberate swing motion
// Aggressive ease for attack, smoother for retreat
float smoothT = isAttacking ? t * t * (3f - 2f * t) : t * (2f - t);
// Clean position interpolation - no shake during swing
weapon.transform.localPosition = Vector3.Lerp(fromPos, toPos, smoothT);
// Position with shake (scaled by violence)
Vector3 shake = new Vector3(
Random.Range(-violence, violence),
Random.Range(-violence, violence),
0f);
weapon.transform.localPosition = Vector3.Lerp(fromPos, toPos, smoothT) + shake;
// Rotation arc for swinging weapons
// Rotation (for swinging weapons)
float currentAngle =
Mathf.Lerp(baseAngle + fromAngleOffset, baseAngle + toAngleOffset, smoothT);
weapon.transform.localRotation = Quaternion.Euler(0, 0, currentAngle);
@@ -28,102 +28,40 @@ namespace Shardok {
[Tooltip("Sprite for trail particles")]
public Sprite trailSprite;
[Tooltip("Number of trail particles in continuous trail")]
public int trailParticleCount = 20;
[Tooltip("Number of trail particles")]
public int trailParticleCount = 8;
[Tooltip("Scale of trail particles")]
public float trailScale = 50f;
public float trailScale = 8f;
[Tooltip("Color of trail at front (hottest)")]
public Color trailColorHot = new Color(1f, 0.9f, 0.6f, 0.9f);
[Tooltip("Color of trail at back (cooler)")]
public Color trailColorCool = new Color(1f, 0.4f, 0.1f, 0.6f);
[Tooltip("Trail spread width")]
public float trailSpread = 12f;
[Tooltip("Color of trail")]
public Color trailColor = new Color(1f, 0.7f, 0.3f, 0.8f);
[Header("Explosion Settings")]
[Tooltip("Sprite for explosion")]
public Sprite explosionSprite;
[Tooltip("Initial scale of explosion")]
public float explosionStartScale = 20f;
public float explosionStartScale = 15f;
[Tooltip("Final scale of explosion")]
public float explosionEndScale = 250f;
public float explosionEndScale = 50f;
[Tooltip("Color of explosion core")]
public Color explosionColor = new Color(1f, 0.6f, 0.2f, 1f);
[Tooltip("Color of explosion")]
public Color explosionColor = new Color(1f, 0.4f, 0.1f, 1f);
[Tooltip("Number of fire sparks")]
public int sparkCount = 8;
[Tooltip("Number of sparks")]
public int sparkCount = 6;
[Tooltip("Spark spread distance")]
public float sparkSpread = 50f;
[Header("Debris Settings")]
[Tooltip("Sprite for rock debris")]
public Sprite debrisSprite;
[Tooltip("Number of debris rocks")]
public int debrisCount = 6;
[Tooltip("Scale of debris rocks")]
public float debrisScale = 6f;
[Tooltip("Debris spread distance")]
public float debrisSpread = 60f;
[Tooltip("Color of debris")]
public Color debrisColor = new Color(0.4f, 0.3f, 0.2f, 1f);
[Header("Meteor Rotation")]
[Tooltip("Rotation speed in degrees per second")]
public float rotationSpeed = 360f;
[Header("Charging Settings (MeteorStart)")]
[Tooltip("Sprite for charging glow")]
public Sprite chargeSprite;
[Tooltip("Color of charging effect")]
public Color chargeColor = new Color(1f, 0.6f, 0.2f, 0.7f);
[Tooltip("Starting scale of charge glow")]
public float chargeStartScale = 5f;
[Tooltip("Ending scale of charge glow")]
public float chargeEndScale = 25f;
[Tooltip("Duration of charge animation")]
public float chargeDuration = 0.5f;
[Header("Targeting Settings (MeteorTarget)")]
[Tooltip("Sprite for target indicator")]
public Sprite targetSprite;
[Tooltip("Color of target indicator")]
public Color targetColor = new Color(1f, 0.3f, 0.1f, 0.6f);
[Tooltip("Scale of target indicator")]
public float targetScale = 30f;
[Tooltip("Duration of target animation")]
public float targetDuration = 0.4f;
[Header("Cancel Settings (MeteorCancel)")]
[Tooltip("Color of fizzle effect")]
public Color cancelColor = new Color(0.5f, 0.5f, 0.5f, 0.8f);
[Tooltip("Duration of cancel animation")]
public float cancelDuration = 0.3f;
public float sparkSpread = 40f;
[Header("Animation Settings")]
[Tooltip("Duration of meteor fall")]
public float fallDuration = 0.8f;
public float fallDuration = 0.4f;
[Tooltip("Duration of explosion")]
public float explosionDuration = 0.6f;
public float explosionDuration = 0.5f;
private HexGrid __hexGrid;
@@ -149,315 +87,6 @@ namespace Shardok {
StartCoroutine(AnimateMeteor(targetPos.Value));
}
/// <summary>
/// Animates meteor charging effect at the caster's position.
/// Shows a growing fiery glow gathering energy.
/// </summary>
public void AnimateMeteorStart(int sourceCellIndex) {
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
Debug.LogWarning("MeteorAnimator: No _hexGrid or gridCanvas available");
return;
}
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
if (sourcePos == null) {
Debug.LogWarning($"MeteorAnimator: Invalid cell index {sourceCellIndex}");
return;
}
StartCoroutine(AnimateCharge(sourcePos.Value));
}
/// <summary>
/// Animates meteor targeting indicator at destination.
/// Shows a pulsing circle/crosshairs marking the target.
/// </summary>
public void AnimateMeteorTarget(int targetCellIndex) {
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
Debug.LogWarning("MeteorAnimator: No _hexGrid or gridCanvas available");
return;
}
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
if (targetPos == null) {
Debug.LogWarning($"MeteorAnimator: Invalid cell index {targetCellIndex}");
return;
}
StartCoroutine(AnimateTarget(targetPos.Value));
}
/// <summary>
/// Animates meteor cancel/fizzle effect at the caster's position.
/// Shows the charging energy dissipating.
/// </summary>
public void AnimateMeteorCancel(int sourceCellIndex) {
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
Debug.LogWarning("MeteorAnimator: No _hexGrid or gridCanvas available");
return;
}
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
if (sourcePos == null) {
Debug.LogWarning($"MeteorAnimator: Invalid cell index {sourceCellIndex}");
return;
}
StartCoroutine(AnimateCancel(sourcePos.Value));
}
private IEnumerator AnimateCharge(Vector2 source) {
Sprite sprite = chargeSprite ?? explosionSprite;
if (sprite == null) yield break;
// Create charging glow
GameObject glow = new GameObject("MeteorCharge");
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
glow.transform.localPosition = new Vector3(source.x, source.y, 5f);
SpriteRenderer renderer = glow.AddComponent<SpriteRenderer>();
renderer.sprite = sprite;
renderer.sortingLayerName = "Effects";
renderer.color = chargeColor;
// Create rising particles
int particleCount = 5;
GameObject[] particles = new GameObject[particleCount];
Vector2[] particleOffsets = new Vector2[particleCount];
for (int i = 0; i < particleCount; i++) {
if (trailSprite == null) break;
GameObject particle = new GameObject($"ChargeParticle_{i}");
particle.transform.SetParent(__hexGrid.gridCanvas.transform, false);
SpriteRenderer pRenderer = particle.AddComponent<SpriteRenderer>();
pRenderer.sprite = trailSprite;
pRenderer.sortingLayerName = "Effects";
pRenderer.color = chargeColor;
particle.transform.localScale = Vector3.one * trailScale;
float angle = (float)i / particleCount * Mathf.PI * 2f;
particleOffsets[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * 20f;
particles[i] = particle;
}
// Animation
float elapsed = 0f;
while (elapsed < chargeDuration) {
float t = elapsed / chargeDuration;
float easeT = t * (2f - t); // Ease out
// Grow glow
float scale = Mathf.Lerp(chargeStartScale, chargeEndScale, easeT);
glow.transform.localScale = Vector3.one * scale;
// Pulse alpha
Color c = chargeColor;
c.a = chargeColor.a * (0.7f + 0.3f * Mathf.Sin(elapsed * 12f));
renderer.color = c;
// Particles spiral inward and upward
for (int i = 0; i < particles.Length; i++) {
if (particles[i] == null) continue;
float particleT = (t + (float)i / particleCount) % 1f;
float radius = 20f * (1f - particleT);
float angle =
particleT * Mathf.PI * 4f + (float)i / particleCount * Mathf.PI * 2f;
Vector2 offset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
particles[i].transform.localPosition = new Vector3(
source.x + offset.x,
source.y + offset.y + particleT * 15f,
4f);
var pRenderer = particles[i].GetComponent<SpriteRenderer>();
if (pRenderer != null) {
Color pc = chargeColor;
pc.a = chargeColor.a * (1f - particleT * 0.5f);
pRenderer.color = pc;
}
}
elapsed += Time.deltaTime;
yield return null;
}
// Brief flash at end
glow.transform.localScale = Vector3.one * chargeEndScale * 1.2f;
Color flashColor = chargeColor;
flashColor.a = 1f;
renderer.color = flashColor;
yield return new WaitForSeconds(0.05f);
// Cleanup
Destroy(glow);
foreach (var p in particles) {
if (p != null) Destroy(p);
}
}
private IEnumerator AnimateTarget(Vector2 target) {
Sprite sprite = targetSprite ?? explosionSprite;
if (sprite == null) yield break;
// Create target indicator
GameObject indicator = new GameObject("MeteorTarget");
indicator.transform.SetParent(__hexGrid.gridCanvas.transform, false);
indicator.transform.localPosition = new Vector3(target.x, target.y, 5f);
SpriteRenderer renderer = indicator.AddComponent<SpriteRenderer>();
renderer.sprite = sprite;
renderer.sortingLayerName = "Effects";
renderer.color = targetColor;
// Create outer ring that contracts
GameObject ring = null;
SpriteRenderer ringRenderer = null;
if (sprite != null) {
ring = new GameObject("TargetRing");
ring.transform.SetParent(__hexGrid.gridCanvas.transform, false);
ring.transform.localPosition = new Vector3(target.x, target.y, 6f);
ringRenderer = ring.AddComponent<SpriteRenderer>();
ringRenderer.sprite = sprite;
ringRenderer.sortingLayerName = "Effects";
Color ringColor = targetColor;
ringColor.a = targetColor.a * 0.5f;
ringRenderer.color = ringColor;
}
// Animation - indicator appears, ring contracts to it
float elapsed = 0f;
while (elapsed < targetDuration) {
float t = elapsed / targetDuration;
// Main indicator pulses
float pulse = 0.8f + 0.2f * Mathf.Sin(elapsed * 15f);
indicator.transform.localScale = Vector3.one * targetScale * pulse;
Color c = targetColor;
c.a = targetColor.a * pulse;
renderer.color = c;
// Ring contracts inward
if (ring != null) {
float ringScale = targetScale * (2f - t);
ring.transform.localScale = Vector3.one * ringScale;
Color rc = targetColor;
rc.a = targetColor.a * 0.5f * (1f - t);
ringRenderer.color = rc;
}
elapsed += Time.deltaTime;
yield return null;
}
// Final pulse and fade
float fadeDuration = 0.15f;
elapsed = 0f;
while (elapsed < fadeDuration) {
float t = elapsed / fadeDuration;
indicator.transform.localScale = Vector3.one * targetScale * (1f + t * 0.3f);
Color c = targetColor;
c.a = targetColor.a * (1f - t);
renderer.color = c;
elapsed += Time.deltaTime;
yield return null;
}
// Cleanup
Destroy(indicator);
if (ring != null) Destroy(ring);
}
private IEnumerator AnimateCancel(Vector2 source) {
Sprite sprite = chargeSprite ?? explosionSprite;
if (sprite == null) yield break;
// Create fizzling glow (starts at charge end size)
GameObject glow = new GameObject("MeteorCancel");
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
glow.transform.localPosition = new Vector3(source.x, source.y, 5f);
glow.transform.localScale = Vector3.one * chargeEndScale;
SpriteRenderer renderer = glow.AddComponent<SpriteRenderer>();
renderer.sprite = sprite;
renderer.sortingLayerName = "Effects";
renderer.color = cancelColor;
// Create dispersing particles
int particleCount = 6;
GameObject[] particles = new GameObject[particleCount];
Vector2[] directions = new Vector2[particleCount];
for (int i = 0; i < particleCount; i++) {
if (trailSprite == null) break;
GameObject particle = new GameObject($"CancelParticle_{i}");
particle.transform.SetParent(__hexGrid.gridCanvas.transform, false);
particle.transform.localPosition = new Vector3(source.x, source.y, 4f);
SpriteRenderer pRenderer = particle.AddComponent<SpriteRenderer>();
pRenderer.sprite = trailSprite;
pRenderer.sortingLayerName = "Effects";
pRenderer.color = cancelColor;
particle.transform.localScale = Vector3.one * trailScale;
float angle = (float)i / particleCount * Mathf.PI * 2f + Random.Range(-0.2f, 0.2f);
directions[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
particles[i] = particle;
}
// Animation - shrink and fade while particles disperse
float elapsed = 0f;
while (elapsed < cancelDuration) {
float t = elapsed / cancelDuration;
// Shrink and fade glow
float scale = chargeEndScale * (1f - t * 0.7f);
glow.transform.localScale = Vector3.one * scale;
Color c = cancelColor;
c.a = cancelColor.a * (1f - t);
renderer.color = c;
// Particles drift outward and fade
for (int i = 0; i < particles.Length; i++) {
if (particles[i] == null) continue;
Vector2 pos = source + directions[i] * 30f * t;
particles[i].transform.localPosition = new Vector3(pos.x, pos.y, 4f);
var pRenderer = particles[i].GetComponent<SpriteRenderer>();
if (pRenderer != null) {
Color pc = cancelColor;
pc.a = cancelColor.a * (1f - t);
pRenderer.color = pc;
particles[i].transform.localScale =
Vector3.one * trailScale * (1f - t * 0.5f);
}
}
elapsed += Time.deltaTime;
yield return null;
}
// Cleanup
Destroy(glow);
foreach (var p in particles) {
if (p != null) Destroy(p);
}
}
private IEnumerator AnimateMeteor(Vector2 target) {
List<GameObject> objects = new List<GameObject>();
@@ -466,24 +95,25 @@ namespace Shardok {
Vector2 fallDirection = new Vector2(Mathf.Sin(angleRad), Mathf.Cos(angleRad));
Vector2 startPos = target + fallDirection * fallDistance;
// Create meteor rock
// Create meteor
GameObject meteor = null;
SpriteRenderer meteorRenderer = null;
float currentRotation = 0f;
if (meteorSprite != null) {
meteor = new GameObject("Meteor");
meteor.transform.SetParent(__hexGrid.gridCanvas.transform, false);
meteorRenderer = meteor.AddComponent<SpriteRenderer>();
SpriteRenderer meteorRenderer = meteor.AddComponent<SpriteRenderer>();
meteorRenderer.sprite = meteorSprite;
meteorRenderer.sortingLayerName = "Effects";
meteorRenderer.color = meteorColor;
meteor.transform.localScale = Vector3.one * meteorScale;
meteor.transform.localPosition = new Vector3(startPos.x, startPos.y, 3f);
// Rotate meteor to face direction of travel
float rotAngle = Mathf.Atan2(-fallDirection.x, -fallDirection.y) * Mathf.Rad2Deg;
meteor.transform.localRotation = Quaternion.Euler(0, 0, rotAngle);
objects.Add(meteor);
}
// Pre-create trail particles (continuous trail behind meteor)
// Trail particles
List<TrailParticle> trailParticles = new List<TrailParticle>();
for (int i = 0; i < trailParticleCount; i++) {
if (trailSprite == null) break;
@@ -494,75 +124,58 @@ namespace Shardok {
SpriteRenderer renderer = particle.AddComponent<SpriteRenderer>();
renderer.sprite = trailSprite;
renderer.sortingLayerName = "Effects";
particle.transform.localPosition = new Vector3(startPos.x, startPos.y, 4f);
// Particles further back are smaller and cooler colored
float trailT = (float)i / trailParticleCount;
float particleScale = trailScale * (1f - trailT * 0.6f);
particle.transform.localScale = Vector3.one * particleScale;
Color particleColor = Color.Lerp(trailColorHot, trailColorCool, trailT);
renderer.color = particleColor;
// Random offset perpendicular to fall direction
float perpOffset = Random.Range(-trailSpread, trailSpread) * trailT;
renderer.color = trailColor;
particle.transform.localScale = Vector3.one * trailScale;
particle.SetActive(false);
trailParticles.Add(new TrailParticle {
gameObject = particle,
delay = trailT * 0.3f, // How far behind the meteor this particle trails
offset = perpOffset
delay = (float)i / trailParticleCount * 0.15f,
offset = Random.Range(-5f, 5f)
});
objects.Add(particle);
}
// Fall phase with rotation and continuous trail
// Fall phase
float elapsed = 0f;
int nextTrailIndex = 0;
while (elapsed < fallDuration) {
float t = elapsed / fallDuration;
// Ease-in for accelerating fall (starts slow, speeds up)
float easeT = t * t;
float easeT = t * t; // Accelerating fall
Vector2 currentPos = Vector2.Lerp(startPos, target, easeT);
// Update meteor position and rotation
if (meteor != null) {
meteor.transform.localPosition = new Vector3(currentPos.x, currentPos.y, 3f);
// Continuous rotation
currentRotation += rotationSpeed * Time.deltaTime;
meteor.transform.localRotation = Quaternion.Euler(0, 0, currentRotation);
// Slight glow intensification as it approaches
Color c = meteorColor;
c.r = Mathf.Min(1f, meteorColor.r + t * 0.2f);
c.g = Mathf.Min(1f, meteorColor.g + t * 0.1f);
meteorRenderer.color = c;
}
// Update trail particles to follow behind meteor
Vector2 perpDir = new Vector2(-fallDirection.y, fallDirection.x);
for (int i = 0; i < trailParticles.Count; i++) {
var tp = trailParticles[i];
if (tp.gameObject == null) continue;
// Spawn trail particles
while (nextTrailIndex < trailParticles.Count &&
elapsed >= trailParticles[nextTrailIndex].delay) {
var tp = trailParticles[nextTrailIndex];
tp.gameObject.SetActive(true);
tp.spawnPosition =
currentPos + new Vector2(tp.offset, tp.offset * 0.5f) * fallDirection.x;
tp.gameObject.transform.localPosition =
new Vector3(tp.spawnPosition.x, tp.spawnPosition.y, 4f);
nextTrailIndex++;
}
// Calculate position along the trail (behind the meteor)
float trailT = (float)i / trailParticleCount;
float trailDistance = trailT * 50f; // How far behind
// Position is behind meteor along fall direction
Vector2 trailPos = currentPos + fallDirection * trailDistance;
// Add perpendicular wobble
trailPos += perpDir * tp.offset * Mathf.Sin(elapsed * 8f + i);
tp.gameObject.transform.localPosition = new Vector3(trailPos.x, trailPos.y, 4f);
// Flicker effect
var renderer = tp.gameObject.GetComponent<SpriteRenderer>();
if (renderer != null) {
Color baseColor = Color.Lerp(trailColorHot, trailColorCool, trailT);
baseColor.a *= 0.8f + 0.2f * Mathf.Sin(elapsed * 15f + i * 0.5f);
renderer.color = baseColor;
// Fade trail particles
foreach (var tp in trailParticles) {
if (!tp.gameObject.activeSelf) continue;
float age = elapsed - tp.delay;
if (age > 0) {
float fadeT = Mathf.Clamp01(age / 0.2f);
var renderer = tp.gameObject.GetComponent<SpriteRenderer>();
if (renderer != null) {
Color c = trailColor;
c.a = trailColor.a * (1f - fadeT);
renderer.color = c;
tp.gameObject.transform.localScale =
Vector3.one * trailScale * (1f - fadeT * 0.5f);
}
}
}
@@ -577,7 +190,7 @@ namespace Shardok {
}
objects.Clear();
// Explosion phase - bigger and with debris
// Explosion phase
GameObject explosion = null;
SpriteRenderer explosionRenderer = null;
@@ -590,11 +203,10 @@ namespace Shardok {
explosionRenderer.sprite = explosionSprite;
explosionRenderer.sortingLayerName = "Effects";
explosionRenderer.color = explosionColor;
explosion.transform.localScale = Vector3.one * explosionStartScale;
objects.Add(explosion);
}
// Fire sparks (fast moving, fiery)
// Sparks
List<SparkParticle> sparks = new List<SparkParticle>();
for (int i = 0; i < sparkCount; i++) {
if (trailSprite == null) break;
@@ -613,61 +225,27 @@ namespace Shardok {
sparks.Add(new SparkParticle {
gameObject = spark,
direction = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)),
speed = sparkSpread* Random.Range(0.8f, 1.4f)
speed = sparkSpread* Random.Range(0.7f, 1.3f)
});
objects.Add(spark);
}
// Rock debris (slower, with gravity arc, rotating)
List<DebrisParticle> debris = new List<DebrisParticle>();
Sprite actualDebrisSprite = debrisSprite ?? meteorSprite;
if (actualDebrisSprite != null) {
for (int i = 0; i < debrisCount; i++) {
GameObject rock = new GameObject($"Debris_{i}");
rock.transform.SetParent(__hexGrid.gridCanvas.transform, false);
rock.transform.localPosition = new Vector3(target.x, target.y, 3f);
SpriteRenderer renderer = rock.AddComponent<SpriteRenderer>();
renderer.sprite = actualDebrisSprite;
renderer.sortingLayerName = "Effects";
renderer.color = debrisColor;
float randomScale = debrisScale * Random.Range(0.6f, 1.4f);
rock.transform.localScale = Vector3.one * randomScale;
// Debris flies outward in all directions
float angle =
(float)i / debrisCount * Mathf.PI * 2f + Random.Range(-0.4f, 0.4f);
Vector2 dir = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
debris.Add(new DebrisParticle {
gameObject = rock,
direction = dir,
speed = debrisSpread * Random.Range(0.6f, 1.2f),
rotationSpeed = Random.Range(-400f, 400f),
currentRotation = Random.Range(0f, 360f)
});
objects.Add(rock);
}
}
// Explosion animation
elapsed = 0f;
while (elapsed < explosionDuration) {
float t = elapsed / explosionDuration;
// Expand and fade explosion with initial flash
// Expand and fade explosion
if (explosion != null) {
float flashT = t < 0.1f ? 1f + (0.1f - t) * 5f : 1f;
float scale = Mathf.Lerp(explosionStartScale, explosionEndScale, t) * flashT;
float scale = Mathf.Lerp(explosionStartScale, explosionEndScale, t);
explosion.transform.localScale = Vector3.one * scale;
Color c = explosionColor;
c.a = (1f - t * t); // Slower fade at start
c.a = 1f - t;
explosionRenderer.color = c;
}
// Animate fire sparks (fast, straight lines)
// Animate sparks
foreach (var spark in sparks) {
if (spark.gameObject == null) continue;
@@ -680,31 +258,7 @@ namespace Shardok {
c.a = 1f - t;
renderer.color = c;
spark.gameObject.transform.localScale =
Vector3.one * trailScale * 1.5f * (1f - t * 0.6f);
}
}
// Animate debris (flies outward in all directions, rotating)
for (int di = 0; di < debris.Count; di++) {
var d = debris[di];
if (d.gameObject == null) continue;
// Debris flies outward, slowing down over time
float easeT = t * (2f - t); // Ease out - fast start, slow end
Vector2 pos = target + d.direction * d.speed * easeT;
d.gameObject.transform.localPosition = new Vector3(pos.x, pos.y, 3f);
// Spin the debris
d.currentRotation += d.rotationSpeed * Time.deltaTime;
d.gameObject.transform.localRotation =
Quaternion.Euler(0, 0, d.currentRotation);
debris[di] = d; // Write back modified struct
var renderer = d.gameObject.GetComponent<SpriteRenderer>();
if (renderer != null) {
Color c = debrisColor;
c.a = 1f - t * 0.8f; // Debris fades slower
renderer.color = c;
Vector3.one * trailScale * 1.5f * (1f - t * 0.5f);
}
}
@@ -730,13 +284,5 @@ namespace Shardok {
public Vector2 direction;
public float speed;
}
private struct DebrisParticle {
public GameObject gameObject;
public Vector2 direction;
public float speed;
public float rotationSpeed;
public float currentRotation;
}
}
}
@@ -1,72 +1,41 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Net.Eagle0.Shardok.Api.BattalionView.Types;
namespace Shardok {
/// <summary>
/// Animates unit movement between hex cells using a trail of footprints/hoofprints.
/// Boot prints for infantry, horseshoe prints for cavalry.
/// Prints appear sequentially then fade out FIFO.
/// Animates unit movement from source to target hex with smooth interpolation.
/// Creates a temporary indicator that slides across the grid.
/// </summary>
public class MoveAnimator : MonoBehaviour {
[Header("Print Sprites")]
[Tooltip("Boot print sprite (will be flipped for left/right)")]
public Sprite bootPrintSprite;
[Tooltip("Paired horseshoe prints sprite")]
public Sprite hoofPrintSprite;
[Header("Animation Settings")]
[Tooltip("Number of prints between hex centers")]
public int printsPerHex = 5;
[Tooltip("Duration of the movement in seconds")]
public float moveDuration = 0.3f;
[Tooltip("Time between each print appearing (seconds)")]
public float printInterval = 0.06f;
[Tooltip("Optional sprite to show during movement")]
public Sprite moveIndicatorSprite;
[Tooltip("How long each print stays fully visible before fading (seconds)")]
public float printLingerTime = 0.3f;
[Tooltip("Scale of the movement indicator")]
public float indicatorScale = 15f;
[Tooltip("Duration of each print's fade out (seconds)")]
public float fadeDuration = 0.4f;
[Tooltip("Height offset for arc movement (0 = straight line)")]
public float arcHeight = 0.05f;
[Tooltip("Scale of print sprites")]
public float printScale = 1.5f;
private HexGrid __hexGrid;
[Tooltip("Lateral offset for alternating prints")]
public float lateralOffset = 6f;
[Tooltip("Color tint for boot prints")]
public Color bootPrintColor = new Color(0.3f, 0.2f, 0.1f, 0.8f);
[Tooltip("Color tint for hoof prints")]
public Color hoofPrintColor = new Color(0.25f, 0.15f, 0.05f, 0.8f);
private HexGrid _hexGrid;
private readonly List<Coroutine> _activeCoroutines = new List<Coroutine>();
private void Start() { _hexGrid = FindObjectOfType<HexGrid>(); }
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
/// <summary>
/// Animate movement from source to target hex.
/// Starts a move animation between two hex cells.
/// Multiple animations can run simultaneously.
/// </summary>
/// <param name="sourceCellIndex">Starting hex cell index</param>
/// <param name="targetCellIndex">Destination hex cell index</param>
/// <param name="unitType">Type of battalion (determines boot vs hoof prints)</param>
public void
AnimateMove(int sourceCellIndex, int targetCellIndex, BattalionTypeId unitType) {
if (bootPrintSprite == null && hoofPrintSprite == null) {
Debug.LogWarning("MoveAnimator: No print sprites assigned");
public void AnimateMove(int sourceCellIndex, int targetCellIndex) {
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
Debug.LogWarning("MoveAnimator: No _hexGrid or gridCanvas available");
return;
}
if (_hexGrid == null || _hexGrid.gridCanvas == null) {
Debug.LogWarning("MoveAnimator: No HexGrid or gridCanvas available");
return;
}
Vector2? sourcePos = _hexGrid.GetCellLocalPosition(sourceCellIndex);
Vector2? targetPos = _hexGrid.GetCellLocalPosition(targetCellIndex);
Vector2? sourcePos = __hexGrid.GetCellLocalPosition(sourceCellIndex);
Vector2? targetPos = __hexGrid.GetCellLocalPosition(targetCellIndex);
if (sourcePos == null || targetPos == null) {
Debug.LogWarning(
@@ -74,122 +43,45 @@ namespace Shardok {
return;
}
bool isMounted = unitType == BattalionTypeId.LightCavalry ||
unitType == BattalionTypeId.HeavyCavalry;
StartCoroutine(SpawnPrintTrail(sourcePos.Value, targetPos.Value, isMounted));
StartCoroutine(AnimateMovement(sourcePos.Value, targetPos.Value));
}
/// <summary>
/// Overload for when unit type is not available - defaults to boot prints.
/// </summary>
public void AnimateMove(int sourceCellIndex, int targetCellIndex) {
AnimateMove(sourceCellIndex, targetCellIndex, BattalionTypeId.LightInfantry);
}
private IEnumerator AnimateMovement(Vector2 source, Vector2 target) {
// Create movement indicator if sprite assigned
GameObject indicator = null;
if (moveIndicatorSprite != null) {
indicator = new GameObject("MoveIndicator");
indicator.transform.SetParent(__hexGrid.gridCanvas.transform, false);
private IEnumerator SpawnPrintTrail(Vector2 source, Vector2 target, bool isMounted) {
var prints = new List<GameObject>();
Vector2 direction = (target - source).normalized;
float totalDistance = Vector2.Distance(source, target);
// Calculate rotation angle for prints to face direction of travel
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg - 90f;
Sprite printSprite = isMounted ? (hoofPrintSprite ?? bootPrintSprite)
: (bootPrintSprite ?? hoofPrintSprite);
Color printColor = isMounted ? hoofPrintColor : bootPrintColor;
// Spawn prints along the path
for (int i = 0; i < printsPerHex; i++) {
float t = (i + 1f) / (printsPerHex + 1f);
Vector2 position = Vector2.Lerp(source, target, t);
// Alternate left/right for boot prints (flip horizontally)
bool flipX = !isMounted && (i % 2 == 1);
// Lateral offset for alternating prints (feet or front/rear hooves)
Vector2 perpendicular = new Vector2(-direction.y, direction.x);
float offset = (i % 2 == 0) ? -lateralOffset : lateralOffset;
position += perpendicular * offset;
GameObject print = CreatePrint(position, angle, printSprite, printColor, flipX);
prints.Add(print);
// Start FIFO fade coroutine for this print
var fadeCoroutine =
StartCoroutine(FadePrintFIFO(print, printLingerTime + i * printInterval));
_activeCoroutines.Add(fadeCoroutine);
yield return new WaitForSeconds(printInterval);
var renderer = indicator.AddComponent<SpriteRenderer>();
renderer.sprite = moveIndicatorSprite;
renderer.sortingLayerName = "Effects";
indicator.transform.localScale = Vector3.one * indicatorScale;
}
// Wait for all fades to complete
float totalWaitTime = printLingerTime + fadeDuration + 0.1f;
yield return new WaitForSeconds(totalWaitTime);
// Cleanup any remaining prints
foreach (var print in prints) {
if (print != null) { Destroy(print); }
}
_activeCoroutines.Clear();
}
private GameObject
CreatePrint(Vector2 position, float angle, Sprite sprite, Color color, bool flipX) {
GameObject print = new GameObject("Footprint");
print.transform.SetParent(_hexGrid.gridCanvas.transform, false);
SpriteRenderer renderer = print.AddComponent<SpriteRenderer>();
renderer.sprite = sprite;
renderer.sortingLayerName = "Effects";
renderer.sortingOrder = -1; // Below other effects
renderer.color = color;
print.transform.localPosition = new Vector3(position.x, position.y, 0);
print.transform.localRotation = Quaternion.Euler(0, 0, angle);
Vector3 scale = new Vector3(flipX ? -printScale : printScale, printScale, printScale);
print.transform.localScale = scale;
return print;
}
private IEnumerator FadePrintFIFO(GameObject print, float delay) {
// Wait before starting fade (FIFO - earlier prints fade first)
yield return new WaitForSeconds(delay);
if (print == null) yield break;
SpriteRenderer renderer = print.GetComponent<SpriteRenderer>();
if (renderer == null) yield break;
Color startColor = renderer.color;
float elapsed = 0f;
float distance = Vector2.Distance(source, target);
while (elapsed < fadeDuration) {
if (print == null || renderer == null) yield break;
while (elapsed < moveDuration) {
float t = elapsed / moveDuration;
float smoothT = t * t * (3f - 2f * t); // Smooth step
float t = elapsed / fadeDuration;
Color newColor = startColor;
newColor.a = Mathf.Lerp(startColor.a, 0f, t);
renderer.color = newColor;
Vector3 pos = Vector3.Lerp(
new Vector3(source.x, source.y, 5f),
new Vector3(target.x, target.y, 5f),
smoothT);
// Add slight arc on Y axis for visible height in top-down view
float arc = 4f * arcHeight * distance * t * (1f - t);
pos.y += arc;
if (indicator != null) { indicator.transform.localPosition = pos; }
elapsed += Time.deltaTime;
yield return null;
}
if (print != null) { Destroy(print); }
}
/// <summary>
/// Cancels all active print animations.
/// </summary>
public void CancelAllAnimations() {
foreach (var coroutine in _activeCoroutines) {
if (coroutine != null) { StopCoroutine(coroutine); }
}
_activeCoroutines.Clear();
if (indicator != null) { Destroy(indicator); }
}
}
}
@@ -3,9 +3,8 @@ using UnityEngine;
namespace Shardok {
/// <summary>
/// Animates scouting action with an eye at source, a vision cone extending
/// toward target, and a glow that travels with the cone tip and grows to
/// cover the target hex and its neighbors.
/// Animates scouting action with an eye/binoculars effect and scanning beam.
/// Creates a sweeping vision cone from source toward target.
/// </summary>
public class ScoutAnimator : MonoBehaviour {
[Header("Eye Settings")]
@@ -19,40 +18,34 @@ namespace Shardok {
public float eyeScale = 15f;
[Header("Vision Cone Settings")]
[Tooltip("Sprite for vision cone/beam (triangle pointing right)")]
[Tooltip("Sprite for vision cone/beam")]
public Sprite coneSprite;
[Tooltip("Color of the vision cone")]
public Color coneColor = new Color(1f, 1f, 0.5f, 0.3f);
[Tooltip("Width of the vision cone at base")]
public float coneWidth = 30f;
[Tooltip("Length of the vision cone")]
public float coneLength = 60f;
[Tooltip("Rotation offset for cone sprite (degrees). +90 for upward-pointing triangle")]
public float coneRotationOffset = 90f;
[Tooltip("Width of the vision cone")]
public float coneWidth = 40f;
[Header("Glow Settings")]
[Tooltip("Sprite for the traveling glow (circle)")]
public Sprite glowSprite;
[Header("Scan Lines Settings")]
[Tooltip("Sprite for scan line particles")]
public Sprite scanSprite;
[Tooltip("Color of the glow")]
public Color glowColor = new Color(1f, 1f, 0.7f, 0.5f);
[Tooltip("Number of scan lines")]
public int scanLineCount = 3;
[Tooltip("Starting scale of glow (at source)")]
public float glowStartScale = 10f;
[Tooltip("Final scale of glow in hex radii (2.5 = covers 7-hex area)")]
public float glowEndRadii = 2.5f;
[Tooltip("Color of scan lines")]
public Color scanColor = new Color(1f, 1f, 0.8f, 0.6f);
[Header("Animation Settings")]
[Tooltip("Duration of cone extension")]
public float extendDuration = 0.5f;
[Tooltip("Duration of the scan sweep")]
public float scanDuration = 0.6f;
[Tooltip("Duration the glow holds at target")]
public float holdDuration = 0.4f;
[Tooltip("Duration of fade out")]
public float fadeDuration = 0.3f;
[Tooltip("Sweep angle in degrees")]
public float sweepAngle = 30f;
private HexGrid __hexGrid;
@@ -62,6 +55,11 @@ namespace Shardok {
/// Starts a scout animation from source toward target hex.
/// </summary>
public void AnimateScout(int sourceCellIndex, int targetCellIndex) {
if (eyeSprite == null && coneSprite == null) {
Debug.LogWarning("ScoutAnimator: No sprites assigned");
return;
}
if (__hexGrid == null || __hexGrid.gridCanvas == null) {
Debug.LogWarning("ScoutAnimator: No _hexGrid or gridCanvas available");
return;
@@ -76,62 +74,59 @@ namespace Shardok {
return;
}
float hexRadius = __hexGrid.GetHexInnerRadius();
float glowEndScale = hexRadius * glowEndRadii * 2f;
StartCoroutine(AnimateScoutEffect(sourcePos.Value, targetPos.Value, glowEndScale));
StartCoroutine(AnimateScoutEffect(sourcePos.Value, targetPos.Value));
}
private IEnumerator AnimateScoutEffect(Vector2 source, Vector2 target, float glowEndScale) {
private IEnumerator AnimateScoutEffect(Vector2 source, Vector2 target) {
Vector2 direction = (target - source).normalized;
float baseAngle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
float distance = Vector2.Distance(source, target);
// Create eye at source
GameObject eye = null;
SpriteRenderer eyeRenderer = null;
if (eyeSprite != null) {
eye = new GameObject("ScoutEye");
eye.transform.SetParent(__hexGrid.gridCanvas.transform, false);
eye.transform.localPosition = new Vector3(source.x, source.y, 5f);
eye.transform.localScale = Vector3.zero;
eyeRenderer = eye.AddComponent<SpriteRenderer>();
eyeRenderer.sprite = eyeSprite;
eyeRenderer.sortingLayerName = "Effects";
eyeRenderer.color = eyeColor;
SpriteRenderer renderer = eye.AddComponent<SpriteRenderer>();
renderer.sprite = eyeSprite;
renderer.sortingLayerName = "Effects";
renderer.color = eyeColor;
}
// Create vision cone
GameObject cone = null;
SpriteRenderer coneRenderer = null;
if (coneSprite != null) {
cone = new GameObject("VisionCone");
cone.transform.SetParent(__hexGrid.gridCanvas.transform, false);
cone.transform.localPosition = new Vector3(source.x, source.y, 6f);
cone.transform.localRotation =
Quaternion.Euler(0, 0, baseAngle + coneRotationOffset);
cone.transform.localScale = new Vector3(coneWidth, 0f, 1f);
cone.transform.localRotation = Quaternion.Euler(0, 0, baseAngle - sweepAngle);
cone.transform.localScale = new Vector3(0f, coneWidth, 1f);
coneRenderer = cone.AddComponent<SpriteRenderer>();
coneRenderer.sprite = coneSprite;
coneRenderer.sortingLayerName = "Effects";
coneRenderer.color = coneColor;
SpriteRenderer renderer = cone.AddComponent<SpriteRenderer>();
renderer.sprite = coneSprite;
renderer.sortingLayerName = "Effects";
renderer.color = coneColor;
}
// Create traveling glow
GameObject glow = null;
SpriteRenderer glowRenderer = null;
if (glowSprite != null) {
glow = new GameObject("ScoutGlow");
glow.transform.SetParent(__hexGrid.gridCanvas.transform, false);
glow.transform.localPosition = new Vector3(source.x, source.y, 7f);
glow.transform.localScale = Vector3.one * glowStartScale;
// Create scan lines
GameObject[] scanLines = new GameObject[scanLineCount];
glowRenderer = glow.AddComponent<SpriteRenderer>();
glowRenderer.sprite = glowSprite;
glowRenderer.sortingLayerName = "Effects";
glowRenderer.color = glowColor;
for (int i = 0; i < scanLineCount; i++) {
if (scanSprite == null) break;
GameObject scanLine = new GameObject($"ScanLine_{i}");
scanLine.transform.SetParent(__hexGrid.gridCanvas.transform, false);
scanLine.transform.localPosition = new Vector3(source.x, source.y, 4f);
scanLine.transform.localScale = Vector3.one * 5f;
SpriteRenderer renderer = scanLine.AddComponent<SpriteRenderer>();
renderer.sprite = scanSprite;
renderer.sortingLayerName = "Effects";
renderer.color = scanColor;
scanLines[i] = scanLine;
}
// Eye appearance phase
@@ -142,7 +137,7 @@ namespace Shardok {
float t = elapsed / appearDuration;
if (eye != null) {
float scale = eyeScale * t * (2f - t);
float scale = eyeScale * t * (2f - t); // Overshoot slightly
eye.transform.localScale = Vector3.one * scale;
}
@@ -152,90 +147,89 @@ namespace Shardok {
if (eye != null) { eye.transform.localScale = Vector3.one * eyeScale; }
// Cone extension phase - cone extends, glow travels and grows
// Scan sweep phase
elapsed = 0f;
while (elapsed < extendDuration) {
float t = elapsed / extendDuration;
float easeT = t * (2f - t); // Ease out
while (elapsed < scanDuration) {
float t = elapsed / scanDuration;
float sweepT = Mathf.Sin(t * Mathf.PI); // Smooth ease in-out
// Extend cone - Y axis is length after rotation, X is width
// Rotate cone through sweep
if (cone != null) {
float length = distance * easeT;
Vector2 conePos = source + direction * (length * 0.5f);
cone.transform.localPosition = new Vector3(conePos.x, conePos.y, 6f);
cone.transform.localScale =
new Vector3(coneWidth * (1f - easeT * 0.3f), length, 1f);
float currentAngle = baseAngle - sweepAngle + sweepAngle * 2f * t;
cone.transform.localRotation = Quaternion.Euler(0, 0, currentAngle);
Color c = coneColor;
c.a = coneColor.a * (0.8f + 0.2f * Mathf.Sin(elapsed * 15f));
coneRenderer.color = c;
// Extend cone
float length = coneLength * Mathf.Min(t * 3f, 1f);
cone.transform.localScale = new Vector3(length, coneWidth, 1f);
// Pulse alpha
var coneRenderer = cone.GetComponent<SpriteRenderer>();
if (coneRenderer != null) {
Color c = coneColor;
c.a = coneColor.a * (0.7f + sweepT * 0.3f);
coneRenderer.color = c;
}
}
// Move and grow glow at cone tip
if (glow != null) {
Vector2 glowPos = source + direction * distance * easeT;
glow.transform.localPosition = new Vector3(glowPos.x, glowPos.y, 7f);
// Animate scan lines along the cone
for (int i = 0; i < scanLines.Length; i++) {
if (scanLines[i] == null) continue;
float glowScale = Mathf.Lerp(glowStartScale, glowEndScale, easeT);
glow.transform.localScale = Vector3.one * glowScale;
float lineT = (t + (float)i / scanLineCount * 0.3f) % 1f;
float lineAngle =
(baseAngle - sweepAngle + sweepAngle * 2f * t) * Mathf.Deg2Rad;
Vector2 lineDir = new Vector2(Mathf.Cos(lineAngle), Mathf.Sin(lineAngle));
Color c = glowColor;
c.a = glowColor.a * (0.7f + 0.3f * Mathf.Sin(elapsed * 12f));
glowRenderer.color = c;
Vector2 pos = source + lineDir * coneLength * lineT;
scanLines[i].transform.localPosition = new Vector3(pos.x, pos.y, 4f);
var renderer = scanLines[i].GetComponent<SpriteRenderer>();
if (renderer != null) {
Color c = scanColor;
c.a = scanColor.a * (1f - lineT) * sweepT;
renderer.color = c;
}
}
elapsed += Time.deltaTime;
yield return null;
}
// Ensure final positions
if (cone != null) {
Vector2 conePos = source + direction * (distance * 0.5f);
cone.transform.localPosition = new Vector3(conePos.x, conePos.y, 6f);
cone.transform.localScale = new Vector3(coneWidth * 0.7f, distance, 1f);
}
if (glow != null) {
glow.transform.localPosition = new Vector3(target.x, target.y, 7f);
glow.transform.localScale = Vector3.one * glowEndScale;
}
// Hold phase - glow pulses at target
// Fade out
float fadeDuration = 0.2f;
elapsed = 0f;
while (elapsed < holdDuration) {
float t = elapsed / holdDuration;
if (glowRenderer != null) {
Color c = glowColor;
c.a = glowColor.a * (0.8f + 0.2f * Mathf.Sin(elapsed * 10f));
glowRenderer.color = c;
}
elapsed += Time.deltaTime;
yield return null;
}
// Fade out phase
elapsed = 0f;
while (elapsed < fadeDuration) {
float t = elapsed / fadeDuration;
if (eyeRenderer != null) {
Color c = eyeColor;
c.a = eyeColor.a * (1f - t);
eyeRenderer.color = c;
if (eye != null) {
var renderer = eye.GetComponent<SpriteRenderer>();
if (renderer != null) {
Color c = eyeColor;
c.a = eyeColor.a * (1f - t);
renderer.color = c;
}
eye.transform.localScale = Vector3.one * eyeScale * (1f - t * 0.3f);
}
if (coneRenderer != null) {
Color c = coneColor;
c.a = coneColor.a * (1f - t);
coneRenderer.color = c;
if (cone != null) {
var renderer = cone.GetComponent<SpriteRenderer>();
if (renderer != null) {
Color c = coneColor;
c.a = coneColor.a * (1f - t);
renderer.color = c;
}
}
if (glowRenderer != null) {
Color c = glowColor;
c.a = glowColor.a * (1f - t);
glowRenderer.color = c;
foreach (var scanLine in scanLines) {
if (scanLine == null) continue;
var renderer = scanLine.GetComponent<SpriteRenderer>();
if (renderer != null) {
Color c = scanColor;
c.a = scanColor.a * (1f - t);
renderer.color = c;
}
}
elapsed += Time.deltaTime;
@@ -245,7 +239,9 @@ namespace Shardok {
// Cleanup
if (eye != null) { Destroy(eye); }
if (cone != null) { Destroy(cone); }
if (glow != null) { Destroy(glow); }
foreach (var scanLine in scanLines) {
if (scanLine != null) { Destroy(scanLine); }
}
}
}
}
@@ -35,10 +35,7 @@ namespace Shardok {
Fear,
// Magic
LightningBolt,
MeteorStart,
MeteorTarget,
MeteorCast,
MeteorCancel,
HolyWave,
RaiseDead,
Control,
@@ -1346,8 +1343,8 @@ namespace Shardok {
case CommandType.MoveCommand: return AnimationType.Move;
// Magic
case CommandType.LightningBoltCommand: return AnimationType.LightningBolt;
case CommandType.MeteorStartCommand: return AnimationType.MeteorStart;
case CommandType.MeteorTargetCommand: return AnimationType.MeteorTarget;
case CommandType.MeteorStartCommand:
case CommandType.MeteorTargetCommand: return AnimationType.MeteorCast;
case CommandType.HolyWaveCommand: return AnimationType.HolyWave;
case CommandType.RaiseDeadCommand: return AnimationType.RaiseDead;
case CommandType.ControlCommand: return AnimationType.Control;
@@ -1386,10 +1383,9 @@ namespace Shardok {
case ActionType.FearAttack: return AnimationType.Fear;
// Magic
case ActionType.LightningBolt: return AnimationType.LightningBolt;
case ActionType.MeteorStart: return AnimationType.MeteorStart;
case ActionType.MeteorTarget: return AnimationType.MeteorTarget;
case ActionType.MeteorStart:
case ActionType.MeteorTarget:
case ActionType.MeteorCast: return AnimationType.MeteorCast;
case ActionType.MeteorCancel: return AnimationType.MeteorCancel;
case ActionType.HolyWave:
case ActionType.HolyWaveDamage: return AnimationType.HolyWave;
case ActionType.RaisedUndead: return AnimationType.RaiseDead;
@@ -1429,10 +1425,7 @@ namespace Shardok {
case AnimationType.Fear: return ActionType.FearAttack;
// Magic
case AnimationType.LightningBolt: return ActionType.LightningBolt;
case AnimationType.MeteorStart: return ActionType.MeteorStart;
case AnimationType.MeteorTarget: return ActionType.MeteorTarget;
case AnimationType.MeteorCast: return ActionType.MeteorCast;
case AnimationType.MeteorCancel: return ActionType.MeteorCancel;
case AnimationType.HolyWave: return ActionType.HolyWave;
case AnimationType.RaiseDead: return ActionType.RaisedUndead;
case AnimationType.Control: return ActionType.Controlled;
@@ -1556,11 +1549,8 @@ namespace Shardok {
}
break;
case AnimationType.Move:
if (moveAnimator != null && attackerUnit != null) {
moveAnimator.AnimateMove(
sourceGridIndex,
targetGridIndex,
attackerUnit.Battalion.Type);
if (moveAnimator != null) {
moveAnimator.AnimateMove(sourceGridIndex, targetGridIndex);
}
break;
case AnimationType.LightningBolt:
@@ -1578,26 +1568,11 @@ namespace Shardok {
raiseDeadAnimator.AnimateRaiseDead(targetGridIndex);
}
break;
case AnimationType.MeteorStart:
if (meteorAnimator != null) {
meteorAnimator.AnimateMeteorStart(sourceGridIndex);
}
break;
case AnimationType.MeteorTarget:
if (meteorAnimator != null) {
meteorAnimator.AnimateMeteorTarget(targetGridIndex);
}
break;
case AnimationType.MeteorCast:
if (meteorAnimator != null) {
meteorAnimator.AnimateMeteorCast(targetGridIndex);
}
break;
case AnimationType.MeteorCancel:
if (meteorAnimator != null) {
meteorAnimator.AnimateMeteorCancel(sourceGridIndex);
}
break;
case AnimationType.HolyWave:
if (holyWaveAnimator != null) {
var undeadCells = FindNearbyUndeadCells(sourceGridIndex, 2);
@@ -1719,11 +1694,8 @@ namespace Shardok {
}
break;
case AnimationType.Move:
if (moveAnimator != null && attackerUnit != null) {
moveAnimator.AnimateMove(
sourceGridIndex,
targetGridIndex,
attackerUnit.Battalion.Type);
if (moveAnimator != null) {
moveAnimator.AnimateMove(sourceGridIndex, targetGridIndex);
}
break;
case AnimationType.LightningBolt:
@@ -1741,26 +1713,11 @@ namespace Shardok {
raiseDeadAnimator.AnimateRaiseDead(targetGridIndex);
}
break;
case AnimationType.MeteorStart:
if (meteorAnimator != null) {
meteorAnimator.AnimateMeteorStart(sourceGridIndex);
}
break;
case AnimationType.MeteorTarget:
if (meteorAnimator != null) {
meteorAnimator.AnimateMeteorTarget(targetGridIndex);
}
break;
case AnimationType.MeteorCast:
if (meteorAnimator != null) {
meteorAnimator.AnimateMeteorCast(targetGridIndex);
}
break;
case AnimationType.MeteorCancel:
if (meteorAnimator != null) {
meteorAnimator.AnimateMeteorCancel(sourceGridIndex);
}
break;
case AnimationType.HolyWave:
if (holyWaveAnimator != null) {
var undeadCells = FindNearbyUndeadCells(sourceGridIndex, 2);
@@ -43,18 +43,6 @@ namespace Shardok {
[Tooltip("Rotation arc of the swing (degrees)")]
public float swingArc = 45f;
[Tooltip("Base rotation offset for hammer (degrees)")]
public float hammerBaseRotation = 180f;
[Tooltip("Flip hammer horizontally")]
public bool hammerFlipHorizontal = true;
[Tooltip("Base rotation offset for alternate tool (degrees)")]
public float alternateBaseRotation = 0f;
[Tooltip("Flip alternate tool horizontally")]
public bool alternateFlipHorizontal = false;
private HexGrid __hexGrid;
private void Start() { __hexGrid = FindObjectOfType<HexGrid>(); }
@@ -91,11 +79,9 @@ namespace Shardok {
}
private IEnumerator AnimateStrikes(Vector2 target, bool useBridgeStyle) {
bool useAlternate = useBridgeStyle && alternateToolSprite != null;
Sprite toolSprite =
useAlternate ? alternateToolSprite : hammerSprite ?? alternateToolSprite;
float baseRotation = useAlternate ? alternateBaseRotation : hammerBaseRotation;
bool flipHorizontal = useAlternate ? alternateFlipHorizontal : hammerFlipHorizontal;
Sprite toolSprite = useBridgeStyle && alternateToolSprite != null
? alternateToolSprite
: hammerSprite ?? alternateToolSprite;
// Create tool
GameObject tool = new GameObject("Tool");
@@ -104,8 +90,7 @@ namespace Shardok {
SpriteRenderer toolRenderer = tool.AddComponent<SpriteRenderer>();
toolRenderer.sprite = toolSprite;
toolRenderer.sortingLayerName = "Effects";
float xScale = flipHorizontal ? -toolScale : toolScale;
tool.transform.localScale = new Vector3(xScale, toolScale, toolScale);
tool.transform.localScale = Vector3.one * toolScale;
// Starting position (raised above target)
Vector3 strikePoint = new Vector3(target.x, target.y, 5f);
@@ -115,7 +100,7 @@ namespace Shardok {
for (int i = 0; i < strikeCount; i++) {
// Raise tool
tool.transform.localPosition = raisePoint;
tool.transform.localRotation = Quaternion.Euler(0, 0, baseRotation - swingArc);
tool.transform.localRotation = Quaternion.Euler(0, 0, -swingArc);
float elapsed = 0f;
float downDuration = strikeDuration * 0.4f;
@@ -127,14 +112,14 @@ namespace Shardok {
tool.transform.localPosition = Vector3.Lerp(raisePoint, strikePoint, easeT);
float angle = Mathf.Lerp(-swingArc, 0f, easeT);
tool.transform.localRotation = Quaternion.Euler(0, 0, baseRotation + angle);
tool.transform.localRotation = Quaternion.Euler(0, 0, angle);
elapsed += Time.deltaTime;
yield return null;
}
tool.transform.localPosition = strikePoint;
tool.transform.localRotation = Quaternion.Euler(0, 0, baseRotation);
tool.transform.localRotation = Quaternion.Euler(0, 0, 0);
// Spawn sparks on impact
yield return StartCoroutine(SpawnSparks(target));
@@ -152,7 +137,7 @@ namespace Shardok {
tool.transform.localPosition = Vector3.Lerp(strikePoint, raisePoint, easeT);
float angle = Mathf.Lerp(0f, -swingArc, easeT);
tool.transform.localRotation = Quaternion.Euler(0, 0, baseRotation + angle);
tool.transform.localRotation = Quaternion.Euler(0, 0, angle);
elapsed += Time.deltaTime;
yield return null;
@@ -1,5 +1,144 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Win32;
namespace EagleInstaller {
public class CredentialManager {
public const string ServerUrl = "https://assets.eagle0.net";
public class Credentials {
public string Username { get; set; }
public string Password { get; set; }
public string ServerUrl { get; set; }
}
}
public class CredentialManager {
private const string RegistryKeyPath = @"SOFTWARE\Eagle0\Launcher";
private const string UsernameValueName = "Username";
private const string ServerUrlValueName = "ServerUrl";
private const string CredentialFileName = "eagle0_credentials.dat";
private static readonly string CredentialFilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"eagle0",
CredentialFileName);
/// <summary>
/// Loads saved credentials. Returns null if no valid credentials are found.
/// </summary>
public static Credentials LoadCredentials() {
try {
// Try to load from registry (username and server URL)
string username = null;
string serverUrl = null;
using (var key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath, false)) {
if (key != null) {
username = key.GetValue(UsernameValueName) as string;
serverUrl = key.GetValue(ServerUrlValueName) as string;
}
}
if (string.IsNullOrEmpty(username)) { return null; }
// Try to load encrypted password from file
string password = LoadEncryptedPassword();
if (string.IsNullOrEmpty(password)) { return null; }
return new Credentials {
Username = username,
Password = password,
ServerUrl = serverUrl ?? "https://eagle0.net"
};
} catch (Exception) {
// If anything fails, return null to prompt for credentials
return null;
}
}
/// <summary>
/// Saves credentials to registry (username/server) and encrypted file (password).
/// </summary>
public static void SaveCredentials(Credentials credentials) {
try {
// Save username and server URL to registry
using (var key = Registry.CurrentUser.CreateSubKey(RegistryKeyPath)) {
key.SetValue(UsernameValueName, credentials.Username);
key.SetValue(ServerUrlValueName, credentials.ServerUrl);
}
// Save encrypted password to file
SaveEncryptedPassword(credentials.Password);
} catch (Exception ex) {
throw new Exception($"Failed to save credentials: {ex.Message}", ex);
}
}
/// <summary>
/// Clears all saved credentials.
/// </summary>
public static void ClearCredentials() {
try {
// Clear registry entries
using (var key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath, true)) {
if (key != null) {
key.DeleteValue(UsernameValueName, false);
key.DeleteValue(ServerUrlValueName, false);
}
}
// Delete password file
if (File.Exists(CredentialFilePath)) { File.Delete(CredentialFilePath); }
} catch (Exception) {
// Ignore errors when clearing
}
}
private static string LoadEncryptedPassword() {
try {
if (!File.Exists(CredentialFilePath)) { return null; }
byte[] encryptedData = File.ReadAllBytes(CredentialFilePath);
byte[] decryptedData = ProtectedData.Unprotect(
encryptedData,
null,
DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(decryptedData);
} catch (Exception) { return null; }
}
private static void SaveEncryptedPassword(string password) {
try {
Directory.CreateDirectory(Path.GetDirectoryName(CredentialFilePath));
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
byte[] encryptedData =
ProtectedData.Protect(passwordBytes, null, DataProtectionScope.CurrentUser);
File.WriteAllBytes(CredentialFilePath, encryptedData);
} catch (Exception ex) {
throw new Exception($"Failed to save encrypted password: {ex.Message}", ex);
}
}
/// <summary>
/// Saves credentials in the same format used by the Unity client for compatibility.
/// </summary>
public static void SaveCredentialsForUnityClient(Credentials credentials) {
try {
// Unity uses PlayerPrefs which on Windows maps to registry entries
// We'll save to the same location the Unity client expects
const string unityRegistryPath =
@"SOFTWARE\Unity\UnityEditor\CompanyName\ProductName";
using (var key = Registry.CurrentUser.CreateSubKey(unityRegistryPath)) {
key.SetValue("urlKey", credentials.ServerUrl);
key.SetValue("nameKey", credentials.Username);
key.SetValue("passwordKey", credentials.Password);
}
} catch (Exception) {
// If Unity credential sharing fails, continue anyway
// The main launcher credentials are still saved
}
}
}
}
@@ -1,8 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
using System.Threading;
@@ -10,6 +13,9 @@ namespace EagleInstaller {
internal class UpdaterFailureException
(string message) : Exception(message);
internal class AuthenticationFailedException
(string message) : Exception(message);
public class InstallerUpdateInfo {
public bool InstallerNeedsUpdate { get; set; }
public string NewInstallerVersion { get; set; }
@@ -31,6 +37,8 @@ namespace EagleInstaller {
private readonly HttpClient _httpClient;
private static readonly SemaphoreSlim Pool = new(DownloadSlots, DownloadSlots);
// Current credentials and service URL
private Credentials CurrentCredentials { get; set; }
private Uri ServiceUrl { get; set; }
private struct FetchAttempt {
@@ -74,12 +82,27 @@ namespace EagleInstaller {
return dict;
}
public EagleUpdater(string serverUrl) {
if (string.IsNullOrEmpty(serverUrl)) {
throw new ArgumentNullException(nameof(serverUrl));
}
ServiceUrl = new Uri(serverUrl);
public EagleUpdater(Credentials credentials) {
CurrentCredentials =
credentials ?? throw new ArgumentNullException(nameof(credentials));
ServiceUrl = new Uri(CurrentCredentials.ServerUrl);
_httpClient = new HttpClient();
SetAuthentication(CurrentCredentials.Username, CurrentCredentials.Password);
}
/// <summary>
/// Updates the authentication credentials for future requests.
/// </summary>
public void UpdateCredentials(Credentials newCredentials) {
CurrentCredentials = newCredentials;
ServiceUrl = new Uri(CurrentCredentials.ServerUrl);
SetAuthentication(CurrentCredentials.Username, CurrentCredentials.Password);
}
private void SetAuthentication(string username, string password) {
_httpClient.DefaultRequestHeaders.Authorization =
AuthenticationHeader(username, password);
}
private List<string> Discrepancies(
@@ -233,6 +256,15 @@ namespace EagleInstaller {
return existingFiles;
}
private static AuthenticationHeaderValue AuthenticationHeader(
string name,
string password) {
return new AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes($"{name}:{password}")));
}
static async Task<string> LocalManifestText(string existingManifestPath) {
if (!File.Exists(existingManifestPath)) return null;
@@ -243,6 +275,11 @@ namespace EagleInstaller {
using HttpResponseMessage response =
await _httpClient.GetAsync(new Uri(ServiceUrl, RemoteManifestName));
if (response.StatusCode == HttpStatusCode.Unauthorized) {
throw new AuthenticationFailedException(
"Authentication failed while fetching manifest. Invalid username or password.");
}
response.EnsureSuccessStatusCode();
await using Stream responseStream = await response.Content.ReadAsStreamAsync();
using StreamReader reader = new StreamReader(responseStream);
@@ -286,6 +323,11 @@ namespace EagleInstaller {
using HttpResponseMessage response =
await _httpClient.GetAsync(new Uri(ServiceUrl, RemotePrefix + remotePath));
if (response.StatusCode == HttpStatusCode.Unauthorized) {
throw new AuthenticationFailedException(
$"Authentication failed while downloading {remotePath}. Invalid username or password.");
}
response.EnsureSuccessStatusCode();
await using Stream responseStream = await response.Content.ReadAsStreamAsync();
await using FileStream fileStream = File.Create(writePath);
@@ -337,6 +379,11 @@ namespace EagleInstaller {
using HttpResponseMessage response = await _httpClient.GetAsync(
new Uri(ServiceUrl, "assets/" + installerUrl));
if (response.StatusCode == HttpStatusCode.Unauthorized) {
throw new AuthenticationFailedException(
"Authentication failed while downloading new installer. Invalid username or password.");
}
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
@@ -0,0 +1,212 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace EagleInstaller {
public partial class LoginDialog : Form {
public Credentials Credentials { get; private set; }
public bool SaveCredentials { get; private set; }
private TextBox usernameTextBox;
private TextBox passwordTextBox;
private TextBox serverUrlTextBox;
private CheckBox saveCredentialsCheckBox;
private Button okButton;
private Button cancelButton;
private Label titleLabel;
private Label usernameLabel;
private Label passwordLabel;
private Label serverLabel;
public LoginDialog(string title = "Eagle0 Login", Credentials existingCredentials = null) {
InitializeComponent();
this.Text = title;
titleLabel.Text = title;
if (existingCredentials != null) {
usernameTextBox.Text = existingCredentials.Username ?? "";
passwordTextBox.Text = existingCredentials.Password ?? "";
serverUrlTextBox.Text = existingCredentials.ServerUrl ?? "https://eagle0.net";
} else {
serverUrlTextBox.Text = "https://eagle0.net";
}
// Default to saving credentials
saveCredentialsCheckBox.Checked = true;
}
private void InitializeComponent() {
// Form properties
this.Size = new Size(400, 280);
this.StartPosition = FormStartPosition.CenterScreen;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.ShowInTaskbar = false;
// Title label
titleLabel = new Label {
Text = "Eagle0 Login",
Font = new Font("Microsoft Sans Serif", 12F, FontStyle.Bold),
Location = new Point(20, 20),
Size = new Size(350, 25),
TextAlign = ContentAlignment.MiddleCenter
};
// Username label and textbox
usernameLabel = new Label {
Text = "Username:",
Location = new Point(20, 60),
Size = new Size(80, 23),
TextAlign = ContentAlignment.MiddleLeft
};
usernameTextBox = new TextBox {
Location = new Point(110, 60),
Size = new Size(250, 23),
TabIndex = 0
};
// Password label and textbox
passwordLabel = new Label {
Text = "Password:",
Location = new Point(20, 90),
Size = new Size(80, 23),
TextAlign = ContentAlignment.MiddleLeft
};
passwordTextBox = new TextBox {
Location = new Point(110, 90),
Size = new Size(250, 23),
UseSystemPasswordChar = true,
TabIndex = 1
};
// Server URL label and textbox
serverLabel = new Label {
Text = "Server:",
Location = new Point(20, 120),
Size = new Size(80, 23),
TextAlign = ContentAlignment.MiddleLeft
};
serverUrlTextBox = new TextBox {
Location = new Point(110, 120),
Size = new Size(250, 23),
TabIndex = 2
};
// Save credentials checkbox
saveCredentialsCheckBox = new CheckBox {
Text = "Remember credentials",
Location = new Point(20, 160),
Size = new Size(200, 23),
TabIndex = 3
};
// OK button
okButton = new Button {
Text = "OK",
Location = new Point(200, 200),
Size = new Size(80, 30),
TabIndex = 4,
DialogResult = DialogResult.OK
};
okButton.Click += OkButton_Click;
// Cancel button
cancelButton = new Button {
Text = "Cancel",
Location = new Point(290, 200),
Size = new Size(80, 30),
TabIndex = 5,
DialogResult = DialogResult.Cancel
};
// Add controls to form
this.Controls.AddRange(new Control[] {
titleLabel,
usernameLabel,
usernameTextBox,
passwordLabel,
passwordTextBox,
serverLabel,
serverUrlTextBox,
saveCredentialsCheckBox,
okButton,
cancelButton
});
// Set default button and cancel button
this.AcceptButton = okButton;
this.CancelButton = cancelButton;
// Focus on username field
this.ActiveControl = usernameTextBox;
}
private void OkButton_Click(object sender, EventArgs e) {
// Validate input
if (string.IsNullOrWhiteSpace(usernameTextBox.Text)) {
MessageBox.Show(
"Username is required.",
"Validation Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
usernameTextBox.Focus();
return;
}
if (string.IsNullOrWhiteSpace(passwordTextBox.Text)) {
MessageBox.Show(
"Password is required.",
"Validation Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
passwordTextBox.Focus();
return;
}
if (string.IsNullOrWhiteSpace(serverUrlTextBox.Text)) {
MessageBox.Show(
"Server URL is required.",
"Validation Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
serverUrlTextBox.Focus();
return;
}
// Create credentials object
Credentials = new Credentials {
Username = usernameTextBox.Text.Trim(),
Password = passwordTextBox.Text,
ServerUrl = serverUrlTextBox.Text.Trim()
};
// Normalize server URL
if (!Credentials.ServerUrl.StartsWith("http://") &&
!Credentials.ServerUrl.StartsWith("https://")) {
Credentials.ServerUrl = "https://" + Credentials.ServerUrl;
}
SaveCredentials = saveCredentialsCheckBox.Checked;
this.DialogResult = DialogResult.OK;
}
/// <summary>
/// Shows the login dialog and returns the entered credentials, or null if cancelled.
/// </summary>
public static (Credentials credentials, bool saveCredentials) ShowLoginDialog(
string title = "Eagle0 Login",
Credentials existingCredentials = null) {
using (var dialog = new LoginDialog(title, existingCredentials)) {
if (dialog.ShowDialog() == DialogResult.OK) {
return (dialog.Credentials, dialog.SaveCredentials);
}
return (null, false);
}
}
}
}
@@ -4,14 +4,29 @@ using System.Windows.Forms;
namespace EagleInstaller {
public partial class MainForm : Form {
private Panel _loginPanel;
private Panel _progressPanel;
private TextBox _progressTextBox;
private Label _statusLabel;
private Button _changeCredentialsButton;
private Button _exitButton;
public string ServerUrl => CredentialManager.ServerUrl;
// Login controls
private TextBox _usernameTextBox;
private TextBox _passwordTextBox;
private TextBox _serverUrlTextBox;
private CheckBox _saveCredentialsCheckBox;
private Button _loginButton;
private Button _cancelButton;
public MainForm() { InitializeComponent(); }
public Credentials EnteredCredentials { get; private set; }
public bool SaveCredentials { get; private set; }
public bool LoginCancelled { get; private set; }
public MainForm() {
InitializeComponent();
ShowLoginView();
}
private void InitializeComponent() {
// Form properties
@@ -22,15 +37,114 @@ namespace EagleInstaller {
this.MaximizeBox = false;
this.MinimizeBox = true;
// Create login panel
CreateLoginPanel();
// Create progress panel
CreateProgressPanel();
// Add panel to form
// Add panels to form
this.Controls.Add(_loginPanel);
this.Controls.Add(_progressPanel);
}
private void CreateLoginPanel() {
_loginPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(20) };
// Title
var titleLabel = new Label {
Text = "Eagle0 Login",
Font = new Font("Microsoft Sans Serif", 14F, FontStyle.Bold),
Location = new Point(20, 20),
Size = new Size(540, 30),
TextAlign = ContentAlignment.MiddleCenter
};
// Username
var usernameLabel = new Label {
Text = "Username:",
Location = new Point(50, 80),
Size = new Size(100, 23)
};
_usernameTextBox = new TextBox {
Location = new Point(160, 80),
Size = new Size(300, 23),
TabIndex = 0
};
// Password
var passwordLabel = new Label {
Text = "Password:",
Location = new Point(50, 110),
Size = new Size(100, 23)
};
_passwordTextBox = new TextBox {
Location = new Point(160, 110),
Size = new Size(300, 23),
UseSystemPasswordChar = true,
TabIndex = 1
};
// Server
var serverLabel = new Label {
Text = "Server:",
Location = new Point(50, 140),
Size = new Size(100, 23)
};
_serverUrlTextBox = new TextBox {
Location = new Point(160, 140),
Size = new Size(300, 23),
TabIndex = 2
};
// Save credentials checkbox
_saveCredentialsCheckBox = new CheckBox {
Text = "Remember credentials",
Location = new Point(50, 180),
Size = new Size(200, 23),
Checked = true,
TabIndex = 3
};
// Buttons
_loginButton = new Button {
Text = "Login",
Location = new Point(280, 220),
Size = new Size(80, 30),
TabIndex = 4
};
_loginButton.Click += LoginButton_Click;
_cancelButton = new Button {
Text = "Cancel",
Location = new Point(380, 220),
Size = new Size(80, 30),
TabIndex = 5
};
_cancelButton.Click += CancelButton_Click;
// Add controls to login panel
_loginPanel.Controls.AddRange(new Control[] {
titleLabel,
usernameLabel,
_usernameTextBox,
passwordLabel,
_passwordTextBox,
serverLabel,
_serverUrlTextBox,
_saveCredentialsCheckBox,
_loginButton,
_cancelButton
});
// Set form defaults
this.AcceptButton = _loginButton;
this.CancelButton = _cancelButton;
}
private void CreateProgressPanel() {
_progressPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(10) };
_progressPanel =
new Panel { Dock = DockStyle.Fill, Padding = new Padding(10), Visible = false };
// Title and status
var titleLabel = new Label {
@@ -50,7 +164,7 @@ namespace EagleInstaller {
// Progress text box with scrollbar
_progressTextBox = new TextBox {
Location = new Point(10, 75),
Size = new Size(560, 310),
Size = new Size(560, 280),
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
@@ -60,17 +174,52 @@ namespace EagleInstaller {
TabIndex = 0
};
// Exit button
// Control buttons
_changeCredentialsButton = new Button {
Text = "Change Credentials",
Location = new Point(350, 370),
Size = new Size(130, 30),
Enabled = false
};
_changeCredentialsButton.Click += ChangeCredentialsButton_Click;
_exitButton = new Button {
Text = "Exit",
Location = new Point(490, 395),
Location = new Point(490, 370),
Size = new Size(80, 30)
};
_exitButton.Click += ExitButton_Click;
// Add controls to progress panel
_progressPanel.Controls.AddRange(
new Control[] { titleLabel, _statusLabel, _progressTextBox, _exitButton });
_progressPanel.Controls.AddRange(new Control[] {
titleLabel,
_statusLabel,
_progressTextBox,
_changeCredentialsButton,
_exitButton
});
}
public void ShowLoginView() {
_loginPanel.Visible = true;
_progressPanel.Visible = false;
this.ActiveControl = _usernameTextBox;
}
public void ShowProgressView() {
_loginPanel.Visible = false;
_progressPanel.Visible = true;
_changeCredentialsButton.Enabled = true;
}
public void SetLoginCredentials(Credentials credentials) {
if (credentials != null) {
_usernameTextBox.Text = credentials.Username ?? "";
_passwordTextBox.Text = credentials.Password ?? "";
_serverUrlTextBox.Text = credentials.ServerUrl ?? "https://eagle0.net";
} else {
_serverUrlTextBox.Text = "https://eagle0.net";
}
}
public void UpdateStatus(string status) {
@@ -92,6 +241,72 @@ namespace EagleInstaller {
_progressTextBox.ScrollToCaret();
}
private void LoginButton_Click(object sender, EventArgs e) {
// Validate input
if (string.IsNullOrWhiteSpace(_usernameTextBox.Text)) {
MessageBox.Show(
"Username is required.",
"Validation Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
_usernameTextBox.Focus();
return;
}
if (string.IsNullOrWhiteSpace(_passwordTextBox.Text)) {
MessageBox.Show(
"Password is required.",
"Validation Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
_passwordTextBox.Focus();
return;
}
if (string.IsNullOrWhiteSpace(_serverUrlTextBox.Text)) {
MessageBox.Show(
"Server URL is required.",
"Validation Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
_serverUrlTextBox.Focus();
return;
}
// Create credentials
var serverUrl = _serverUrlTextBox.Text.Trim();
if (!serverUrl.StartsWith("http://") && !serverUrl.StartsWith("https://")) {
serverUrl = "https://" + serverUrl;
}
EnteredCredentials = new Credentials {
Username = _usernameTextBox.Text.Trim(),
Password = _passwordTextBox.Text,
ServerUrl = serverUrl
};
SaveCredentials = _saveCredentialsCheckBox.Checked;
LoginCancelled = false;
ShowProgressView();
}
private void CancelButton_Click(object sender, EventArgs e) {
LoginCancelled = true;
this.Close();
}
private void ChangeCredentialsButton_Click(object sender, EventArgs e) { ShowLoginView(); }
private void ExitButton_Click(object sender, EventArgs e) { this.Close(); }
public static MainForm CreateMainForm(
string title = "Eagle0 Login",
Credentials existingCredentials = null) {
var form = new MainForm();
form.Text = title;
form.SetLoginCredentials(existingCredentials);
return form;
}
}
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
@@ -35,6 +35,14 @@ namespace EagleInstaller {
return;
}
// Check if Shift key is held during launch to force login
bool shiftKeyHeld = (Control.ModifierKeys & Keys.Shift) == Keys.Shift;
// Check for credential management flags
bool forceLogin =
shiftKeyHeld || (args.Length > 0 &&
(args[0] == "--login" || args[0] == "--change-credentials"));
bool clearCredentials = args.Length > 0 && args[0] == "--clear-credentials";
bool showHelp = args.Length > 0 && (args[0] == "--help" || args[0] == "-h");
// Show help if requested
@@ -43,8 +51,16 @@ namespace EagleInstaller {
return;
}
// Create main form with default server URL
var mainForm = new MainForm();
// Handle clear credentials flag
if (clearCredentials) {
Console.WriteLine("Clearing saved credentials...");
CredentialManager.ClearCredentials();
Console.WriteLine("Credentials cleared successfully.");
return;
}
// Create main form
var mainForm = MainForm.CreateMainForm();
Logger.Initialize(mainForm);
// Show the form and ensure handle is created
@@ -52,7 +68,7 @@ namespace EagleInstaller {
mainForm.CreateControl();
// Start the installer logic in the background
var installerTask = RunInstallerLogic(mainForm);
var installerTask = RunInstallerLogic(mainForm, forceLogin);
// Run the application with the main form
Application.Run(mainForm);
@@ -61,45 +77,84 @@ namespace EagleInstaller {
await installerTask;
}
private static async Task RunInstallerLogic(MainForm mainForm) {
private static async Task RunInstallerLogic(MainForm mainForm, bool forceLogin) {
try {
string serverUrl = mainForm.ServerUrl;
Logger.WriteLine($"Using server: {serverUrl}");
EagleUpdater updater = new EagleUpdater(serverUrl);
// Stage 1: Check if installer itself needs update
var installerUpdateInfo = await updater.CheckForInstallerUpdate();
if (installerUpdateInfo.InstallerNeedsUpdate) {
Logger.UpdateStatus("Installer update required...");
bool downloadSuccess = await updater.DownloadAndLaunchNewInstaller(
installerUpdateInfo.NewInstallerUrl);
if (downloadSuccess) {
Logger.UpdateStatus("New installer launched. Exiting current version.");
// Wait a moment to ensure new installer starts before we exit
await Task.Delay(1000);
// Exit the application directly
Application.Exit();
return; // Exit this version
}
Logger.WriteError(
"Failed to download new installer, continuing with current version...");
// Get credentials (load saved or prompt user)
Credentials credentials = await GetValidCredentials(mainForm, forceLogin);
if (credentials == null) {
Logger.WriteError("No valid credentials provided. Exiting.");
return;
}
// Stage 2: Proceed with normal game update
bool success = await updater.MaybePerformUpdateWithRetries(FileLocation);
// Switch to progress view
SafeInvoke(mainForm, () => mainForm.ShowProgressView());
if (success) {
Logger.UpdateStatus("Launching Eagle0...");
Process.Start(ExeLocation);
EagleUpdater updater = new EagleUpdater(credentials);
// Exit the installer after launching the game
Application.Exit();
} else {
Logger.WriteError("Update failed!");
bool credentialsAreValid = false;
while (!credentialsAreValid) {
try {
// Stage 1: Check if installer itself needs update
var installerUpdateInfo = await updater.CheckForInstallerUpdate();
if (installerUpdateInfo.InstallerNeedsUpdate) {
Logger.UpdateStatus("Installer update required...");
bool downloadSuccess = await updater.DownloadAndLaunchNewInstaller(
installerUpdateInfo.NewInstallerUrl);
if (downloadSuccess) {
Logger.UpdateStatus(
"New installer launched. Exiting current version.");
// Wait a moment to ensure new installer starts before we exit
await Task.Delay(1000);
// Exit the application directly
Application.Exit();
return; // Exit this version
}
Logger.WriteError(
"Failed to download new installer, continuing with current version...");
}
// Stage 2: Proceed with normal game update
bool success = await updater.MaybePerformUpdateWithRetries(FileLocation);
if (success) {
Logger.UpdateStatus("Launching Eagle0...");
Process.Start(ExeLocation);
// Exit the installer after launching the game
Application.Exit();
return;
} else {
Logger.WriteError("Update failed!");
credentialsAreValid = true; // Don't retry on non-auth failures
}
} catch (AuthenticationFailedException ex) {
Logger.WriteError($"Authentication failed: {ex.Message}");
// Clear saved credentials since they're invalid
CredentialManager.ClearCredentials();
// Show login view again
SafeInvoke(mainForm, () => mainForm.ShowLoginView());
// Wait for new credentials
credentials = await GetValidCredentials(
mainForm,
true,
"Authentication Failed - Please re-enter credentials");
if (credentials == null) {
Logger.WriteError("No valid credentials provided. Exiting.");
return;
}
// Switch back to progress view
SafeInvoke(mainForm, () => mainForm.ShowProgressView());
// Update updater with new credentials
updater.UpdateCredentials(credentials);
}
}
} catch (Exception ex) { Logger.WriteError($"Unexpected error: {ex.Message}"); }
}
@@ -236,6 +291,88 @@ namespace EagleInstaller {
} finally { progressForm?.Close(); }
}
private static async Task<Credentials> GetValidCredentials(
MainForm mainForm,
bool forceLogin = false,
string title = "Eagle0 Login") {
Credentials savedCredentials = null;
// Try to load saved credentials first (unless forced to login)
if (!forceLogin) {
savedCredentials = CredentialManager.LoadCredentials();
if (savedCredentials != null) {
Logger.WriteLine(
$"Found saved credentials for user: {savedCredentials.Username}");
// Return saved credentials immediately for automatic login
return savedCredentials;
}
}
// If no saved credentials or force login, show login form
Logger.WriteLine("No saved credentials found. Please enter your login information.");
// Set the form title and pre-fill credentials if forcing login
SafeInvoke(mainForm, () => {
mainForm.Text = title;
if (forceLogin && savedCredentials != null) {
mainForm.SetLoginCredentials(savedCredentials);
}
});
// Wait for user to enter credentials through the form
return await WaitForCredentials(mainForm);
}
private static async Task<Credentials> WaitForCredentials(MainForm mainForm) {
var tcs = new TaskCompletionSource<Credentials>();
// Set up event handler to capture when login is completed
EventHandler loginCompleted = null;
loginCompleted = (sender, e) => {
var form = sender as MainForm;
if (form is { EnteredCredentials : not null }) {
var credentials = form.EnteredCredentials;
var saveCredentials = form.SaveCredentials;
if (saveCredentials) {
try {
CredentialManager.SaveCredentials(credentials);
CredentialManager.SaveCredentialsForUnityClient(credentials);
Logger.WriteLine("Credentials saved successfully.");
} catch (Exception ex) {
Logger.WriteError($"Failed to save credentials: {ex.Message}");
}
}
tcs.SetResult(credentials);
} else if (form != null && form.LoginCancelled) {
tcs.SetResult(null);
}
};
// Monitor for form changes (this is a simplified approach)
var monitorTask = Task.Run(async () => {
while (!tcs.Task.IsCompleted) {
await Task.Delay(100);
if (mainForm.EnteredCredentials != null || mainForm.LoginCancelled) {
loginCompleted(mainForm, EventArgs.Empty);
break;
}
}
});
return await tcs.Task;
}
private static void SafeInvoke(Control control, Action action) {
if (control.IsHandleCreated && control.InvokeRequired) {
control.Invoke(action);
} else if (control.IsHandleCreated) {
action();
}
}
private static void ShowHelp() {
Console.WriteLine("Eagle0 Installer");
Console.WriteLine();
@@ -243,11 +380,19 @@ namespace EagleInstaller {
Console.WriteLine(" EagleInstaller.exe [OPTIONS]");
Console.WriteLine();
Console.WriteLine("OPTIONS:");
Console.WriteLine(" --help, -h Show this help message");
Console.WriteLine(" --help, -h Show this help message");
Console.WriteLine(" --login, --change-credentials Force credential input dialog");
Console.WriteLine(" --clear-credentials Clear saved credentials and exit");
Console.WriteLine();
Console.WriteLine("NORMAL OPERATION:");
Console.WriteLine(" Run without arguments to start the normal update process.");
Console.WriteLine(" Downloads game files from assets.eagle0.net CDN.");
Console.WriteLine(" If credentials are saved, automatic login will occur.");
Console.WriteLine(" Hold Shift during launch to force credential input dialog.");
Console.WriteLine();
Console.WriteLine("EXAMPLES:");
Console.WriteLine(" EagleInstaller.exe # Normal operation");
Console.WriteLine(" EagleInstaller.exe --login # Force new login");
Console.WriteLine(" EagleInstaller.exe --clear-credentials # Clear saved login");
}
}
}
@@ -1,3 +1,3 @@
manifest_name = eagle0_manifest.txt
remote_manifest_prefix = installer/
remote_asset_prefix = unity3d/win/
remote_manifest_prefix = assets/installer/
remote_asset_prefix = assets/unity3d/win/
@@ -55,8 +55,8 @@ func main() {
log.Fatalf("Failed to create AWS client: %v", err)
}
// Upload installer to S3 with public ACL
err = bucketBasics.UploadFilePublic(bucketName, installerKey, installerPath)
// Upload installer to S3
err = bucketBasics.UploadFile(bucketName, installerKey, installerPath)
if err != nil {
log.Fatalf("Failed to upload installer: %v", err)
}
@@ -73,8 +73,8 @@ func main() {
log.Fatalf("Failed to create version file: %v", err)
}
// Upload version file to S3 with public ACL
err = bucketBasics.UploadFilePublic(bucketName, versionKey, tempVersionFile)
// Upload version file to S3
err = bucketBasics.UploadFile(bucketName, versionKey, tempVersionFile)
if err != nil {
log.Fatalf("Failed to upload version file: %v", err)
}
@@ -112,8 +112,8 @@ func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) err
// Build new manifest
newManifest := mm.buildManifest(sections)
// Upload updated manifest with public ACL
err = mm.bucketBasics.UploadBytesPublic(bucketName, manifestPath, []byte(newManifest))
// Upload updated manifest
err = mm.bucketBasics.UploadBytes(bucketName, manifestPath, []byte(newManifest))
if err != nil {
return fmt.Errorf("failed to upload manifest: %v", err)
}
@@ -118,7 +118,7 @@ func syncToS3(bb aws.BucketBasics, localDir string, s3Root string, localShas map
}
if !objectMatches {
err = bb.UploadFilePublic(bucketName, destinationPath, filePath)
err = bb.UploadFile(bucketName, destinationPath, filePath)
if err != nil {
return err
}
@@ -156,7 +156,7 @@ func handleDir(tempDir string, outputManifestPath string) {
log.Fatal(err)
}
err = bb.UploadBytesPublic(bucketName, manifestPath, []byte(localManifest))
err = bb.UploadBytes(bucketName, manifestPath, []byte(localManifest))
if err != nil {
log.Fatal(err)
}
+4 -31
View File
@@ -130,30 +130,16 @@ func (basics BucketBasics) FetchString(bucketName string, objectKey string) (str
}
func (basics BucketBasics) UploadFile(bucketName string, objectKey string, fileName string) error {
return basics.UploadFileWithACL(bucketName, objectKey, fileName, "")
}
func (basics BucketBasics) UploadFilePublic(bucketName string, objectKey string, fileName string) error {
return basics.UploadFileWithACL(bucketName, objectKey, fileName, "public-read")
}
func (basics BucketBasics) UploadFileWithACL(bucketName string, objectKey string, fileName string, acl string) error {
file, err := os.Open(fileName)
if err != nil {
return err
}
defer file.Close()
input := &s3.PutObjectInput{
_, err = basics.S3Client.PutObject(basics.ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: file,
}
if acl != "" {
input.ACL = types.ObjectCannedACL(acl)
}
_, err = basics.S3Client.PutObject(basics.ctx, input)
})
if err != nil {
return err
}
@@ -167,24 +153,11 @@ func (basics BucketBasics) UploadFileWithACL(bucketName string, objectKey string
}
func (basics BucketBasics) UploadBytes(bucketName string, objectKey string, data []byte) error {
return basics.UploadBytesWithACL(bucketName, objectKey, data, "")
}
func (basics BucketBasics) UploadBytesPublic(bucketName string, objectKey string, data []byte) error {
return basics.UploadBytesWithACL(bucketName, objectKey, data, "public-read")
}
func (basics BucketBasics) UploadBytesWithACL(bucketName string, objectKey string, data []byte, acl string) error {
input := &s3.PutObjectInput{
_, err := basics.S3Client.PutObject(basics.ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(data),
}
if acl != "" {
input.ACL = types.ObjectCannedACL(acl)
}
_, err := basics.S3Client.PutObject(basics.ctx, input)
})
if err != nil {
return err
}
@@ -91,15 +91,6 @@ object Main {
SimpleTimedLogger.printLogger.logLine("Sentry error tracking enabled")
}
// Set up global uncaught exception handler to log and report to Sentry
Thread.setDefaultUncaughtExceptionHandler { (thread, throwable) =>
SimpleTimedLogger.printLogger.logLine(
s"Uncaught exception in thread ${thread.getName}: $throwable"
)
throwable.printStackTrace()
Sentry.captureException(throwable)
}
SimpleTimedLogger.printLogger.logLine("Starting!")
// Register shutdown hook to flush pending S3 saves on graceful shutdown
@@ -204,11 +204,11 @@ case class EngineImpl(
def getAvailablePlayerCommands(
fid: FactionId
): Option[AvailableCommands] = {
val currentStateProto = GameStateConverter.toProto(currentState)
val commandsByProvince =
availableCommandsFactory.availablePlayerCommands(currentState, fid)
availableCommandsFactory.availablePlayerCommands(currentStateProto, fid)
Option.when(commandsByProvince.nonEmpty) {
val currentStateProto = GameStateConverter.toProto(currentState)
AvailableCommands(
token = tokenForFaction(fid),
commandsByProvince = commandsByProvince,
@@ -1,34 +1,32 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.available_command.AlmsAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.{MaxAlmsFood, MinVigorForAlms}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.AlmsAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.Profession
object AvailableAlmsCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableAlmsCommandFactory extends AvailableCommandsFactoryForType {
def availableHeroIds(gs: GameState, pid: ProvinceId): Vector[HeroId] =
gs.provinces(pid)
.rulingFactionHeroIds
.toVector
.filter(hid => gs.heroes(hid).vigor >= MinVigorForAlms.doubleValue)
.sortBy(hid => (gs.heroes(hid).profession != Profession.Paladin, -gs.heroes(hid).vigor))
.sortBy(hid => (!gs.heroes(hid).profession.isPaladin, -gs.heroes(hid).vigor))
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[AlmsAvailableCommand] = {
val hids = availableHeroIds(gameState, provinceId)
val province = gameState.provinces(provinceId)
Option.when(hids.nonEmpty && province.food > 0) {
AlmsAvailable(
AlmsAvailableCommand(
availableHeroIds = hids,
actingProvinceId = provinceId,
foodAvailable = Math.min(MaxAlmsFood.intValue, province.food)
foodAvailable = Math.min(MaxAlmsFood.intValue, gameState.provinces(provinceId).food)
)
}
}
@@ -1,24 +1,26 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.{ApprehendOutlawAvailableCommand, ResidentOutlaw}
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.UNAFFILIATED_HERO_OUTLAW
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
import net.eagle0.eagle.library.settings.MinVigorForApprehendOutlaw
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{ApprehendOutlawAvailable, ResidentOutlaw}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.HeroT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.unaffiliated_hero.{UnaffiliatedHeroT, UnaffiliatedHeroType}
import net.eagle0.eagle.library.util.view_filters.HeroViewFilter
object AvailableApprehendOutlawCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableApprehendOutlawCommandFactory extends AvailableCommandsFactoryForType {
private def availableToCapture(province: ProvinceT): Vector[UnaffiliatedHeroT] =
private def availableToCapture(province: Province): Vector[UnaffiliatedHero] =
province.unaffiliatedHeroes
.filter(_.unaffiliatedHeroType == UnaffiliatedHeroType.Outlaw)
.filter(_.`type` == UNAFFILIATED_HERO_OUTLAW)
.toVector
private def availableActors(
gameState: GameState,
province: ProvinceT
): Vector[HeroT] =
province: Province
): Vector[Hero] =
province.rulingFactionHeroIds
.map(gameState.heroes)
.filter(_.vigor >= MinVigorForApprehendOutlaw.doubleValue)
@@ -28,19 +30,25 @@ object AvailableApprehendOutlawCommandFactory extends ScalaAvailableCommandsFact
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[ApprehendOutlawAvailableCommand] = {
val province = gameState.provinces(provinceId)
val outlaws = availableToCapture(province)
val actors = availableActors(gameState, province)
Option.when(outlaws.nonEmpty && actors.nonEmpty) {
ApprehendOutlawAvailable(
ApprehendOutlawAvailableCommand(
actingProvinceId = provinceId,
availableHeroIds = actors.map(_.id),
outlaws = outlaws.map(outlaw =>
ResidentOutlaw(
heroId = outlaw.heroId,
lastFactionId = outlaw.lastFactionId
hero = Some(
HeroViewFilter.filteredHeroView(
hero = gameState.heroes(outlaw.heroId),
gs = gameState,
factionId = Some(factionId)
)
),
lastFactionId = outlaw.lastFaction
)
),
availableBattalionIds = province.battalionIds
@@ -1,18 +1,17 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.battalion_type_finder.BattalionTypeFinder
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{ArmTroopsAvailable, ArmamentCost}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.api.available_command.{ArmTroopsAvailableCommand, ArmamentCost}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.LegacyBattalionUtils
object AvailableArmTroopsCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableArmTroopsCommandFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[ArmTroopsAvailableCommand] = {
val province = gameState.provinces(provinceId)
if !province.rulerIsTraveling then return None
@@ -22,26 +21,29 @@ object AvailableArmTroopsCommandFactory extends ScalaAvailableCommandsFactory {
val battalions = province.battalionIds
.map(gameState.battalions)
.filter(
_.armament < ProvinceUtils.effectiveInfrastructure(province).floor
_.armament < LegacyProvinceUtils.effectiveInfrastructure(province).floor
)
.filter(_.size > 0)
val armamentCosts =
battalions
.map(_.typeId)
.map(_.`type`)
.distinct
.map(bt => BattalionTypeFinder.battalionType(bt, gameState.battalionTypes))
.map(bt =>
LegacyBattalionUtils
.battalionType(bt, gameState.battalionTypes.toVector)
)
.map(tp =>
ArmamentCost(
cost = tp.baseArmamentCostPerTroop * province.priceIndex,
battalionTypeId = tp.typeId.value
`type` = tp.typeId
)
)
val affordableBattalions = battalions
.filter(b =>
b.size * armamentCosts
.find(_.battalionTypeId == b.typeId.value)
.find(_.`type` == b.`type`)
.get
.cost < province.gold
)
@@ -49,12 +51,12 @@ object AvailableArmTroopsCommandFactory extends ScalaAvailableCommandsFactory {
if affordableBattalions.isEmpty then return None
Some(
ArmTroopsAvailable(
ArmTroopsAvailableCommand(
actingProvinceId = provinceId,
availableBattalions = affordableBattalions.map(_.id),
armamentCosts = armamentCosts,
maxArmament = ProvinceUtils.effectiveInfrastructure(province).floor.toInt,
availableBattalionTypeIds = gameState.battalionTypes.map(_.typeId.value)
maxArmament = LegacyProvinceUtils.effectiveInfrastructure(province).floor.toInt,
availableBattalionTypeIds = gameState.battalionTypes.map(_.typeId)
)
)
}
@@ -2,21 +2,24 @@ package net.eagle0.eagle.library.actions.availability
import net.eagle0.common.MoreOption
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceDistances}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.state.{Army, HostileArmyGroup, HostileArmyGroupStatus, MovingArmy}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{
ArmyStats,
AttackDecisionAvailable,
ExpandedCombatUnit
import net.eagle0.eagle.api.available_command.AttackDecisionAvailableCommand
import net.eagle0.eagle.api.command.util.army_stats.ArmyStats
import net.eagle0.eagle.api.command.util.attack_decision_type.{
AdvanceDecision,
AttackDecisionType,
DemandTributeDecision,
SafePassageDecision,
WithdrawDecision
}
import net.eagle0.eagle.model.state.command.common.AttackDecisionType
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.common.tribute_amount.TributeAmount
import net.eagle0.eagle.internal.army.{HostileArmyGroup, MovingArmy}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceDistances}
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableAttackDecisionCommandFactory extends AvailableCommandsFactoryForType {
// We need to include the armies that have already withdrawn or advanced, in a consistent sort order,
// so that the player doesn't learn what others have done by the shifts.
@@ -27,7 +30,8 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
gs.provinces(pid)
.hostileArmies
.flatMap(_.armies)
.sortBy(ma => (ma.army.factionId, ma.army.units.head.heroId))
.sortBy(ma => (ma.getArmy.factionId, ma.getArmy.units.head.heroId))
.toVector
private def myDecidingHostileArmies(
pid: ProvinceId,
@@ -37,7 +41,7 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
gs.provinces(pid)
.hostileArmies
.find(_.factionId == fid)
.filter(_.status == HostileArmyGroupStatus.AwaitingDecision)
.filter(_.status.asMessage.sealedValue.isAwaitingDecision)
private def defendingArmyStats(
attackerFid: FactionId,
@@ -62,11 +66,13 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
.takeRight(heroCount)
.sum
province.rulingFactionId.map { _ =>
province.rulingFactionId.map { defenderFid =>
ArmyStats(
factionId = province.rulingFactionId.get,
factionId = defenderFid,
heroCount = heroCount,
troopCount = troopCount
troopCount = troopCount,
hostility = LegacyFactionUtils.hostilityStatus(defenderFid, attackerFid, gs),
originProvinceId = pid
)
}
}
@@ -76,26 +82,15 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
pid: ProvinceId,
gs: GameState
): Vector[ArmyStats] =
relevantArmies(pid, gs).map(ma => stats(ma.army, gs)) ++ defendingArmyStats(
relevantArmies(pid, gs).map(ia =>
IncomingArmyUtils.stats(toFid, ia.getArmy, gs, ia.originProvince)
) ++ defendingArmyStats(
toFid,
pid,
gs
)
private def stats(
army: Army,
gs: GameState
): ArmyStats = {
val battalions = army.units.flatMap(_.battalionId).map(gs.battalions)
val troopCount = battalions.map(_.size).sum
ArmyStats(
factionId = army.factionId,
heroCount = army.units.size,
troopCount = troopCount
)
}
private def hasResourcesToDemand(province: ProvinceT): Boolean =
private def hasResourcesToDemand(province: Province): Boolean =
province.gold > 0 || province.food > 0
private def allArmiesCanFlee(
@@ -106,7 +101,7 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
myDecidingHostileArmies(pid, fid, gs)
.map(_.armies)
.getOrElse(Vector())
.map(_.army)
.flatMap(_.army)
.forall(_.fleeProvinceId.isDefined)
private def maybeAdvanceDecision(
@@ -121,28 +116,30 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
fid,
gs
)
)(AttackDecisionType.Advance)
)(AdvanceDecision())
private def maybeDemandTributeDecision(
gs: GameState,
pid: ProvinceId,
fid: FactionId
): Option[AttackDecisionType] = {
val province = gs.provinces(pid)
): Option[AttackDecisionType] =
Option.when(
hasResourcesToDemand(province)
hasResourcesToDemand(gs.provinces(pid))
&& IncomingArmyUtils.isHostileProvince(
province,
gs.provinces(pid),
fid,
gs
) && allArmiesCanFlee(gs, pid, fid)
)(
AttackDecisionType.DemandTribute(
gold = province.gold,
food = province.food
DemandTributeDecision(
Some(
TributeAmount(
food = gs.provinces(pid).food,
gold = gs.provinces(pid).gold
)
)
)
)
}
private def maybeWithdrawDecision(
gs: GameState,
@@ -151,7 +148,7 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
): Option[AttackDecisionType] =
Option.when(
allArmiesCanFlee(gs, pid, fid)
)(AttackDecisionType.Withdraw)
)(WithdrawDecision())
private def maybeSafePassageDecision(
gs: GameState,
@@ -166,8 +163,8 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
gs
)
) {
FactionUtils
.provinces(fid, gs.provinces.values.toVector)
LegacyFactionUtils
.provinces(fid, gs)
.maxByOption(_.rulingFactionHeroIds.size)
.orElse {
gs.provinces.values
@@ -180,13 +177,13 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
.closestProvinceOption(
pid,
emptyProvinces,
p => gs.provinces(p).neighbors.map(_.provinceId)
p => gs.provinces(p).neighbors.map(_.provinceId).toVector
)
.map(pwd => gs.provinces(pwd.provinceId))
}
}
.map { returnProvince =>
AttackDecisionType.SafePassage(returnProvince.id)
SafePassageDecision(returnProvince.id)
}
}
@@ -207,27 +204,11 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
fid
).toVector ++ maybeSafePassageDecision(gs, pid, fid).toVector
private def expandedCombatUnits(
armies: Vector[MovingArmy],
gs: GameState
): Vector[ExpandedCombatUnit] =
armies
.flatMap(_.army.units)
.map { cu =>
val battalionOpt = cu.battalionId.map(gs.battalions)
ExpandedCombatUnit(
heroId = cu.heroId,
battalionId = cu.battalionId,
heroName = gs.heroes(cu.heroId).nameTextId,
troopCount = battalionOpt.map(_.size).getOrElse(0)
)
}
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[AttackDecisionAvailableCommand] = {
internalRequire(
IncomingArmyUtils.incomingArmiesAreMutuallyAllied(
gameState.provinces(provinceId),
@@ -241,10 +222,12 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
case items if items.isEmpty => None
case nonemptyDecisionOptions =>
Some(
AttackDecisionAvailable(
AttackDecisionAvailableCommand(
actingProvinceId = provinceId,
armies = armyStats(factionId, provinceId, gameState),
actingUnits = expandedCombatUnits(hostileArmyGroup.armies, gameState),
actingUnits = hostileArmyGroup.armies
.flatMap(_.getArmy.units)
.map(cu => ExpandedCombatUnitUtils.expandedCombatUnit(cu, gameState)),
availableDecisions = nonemptyDecisionOptions
)
)
@@ -18,15 +18,13 @@ import net.eagle0.eagle.api.selected_command.{
TradeSelectedCommand
}
import net.eagle0.eagle.common.round_phase.RoundPhase.*
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.internal.province.Province as ProvinceProto
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.actions.impl.command.AvailableCommandTypeMap
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.IncomingArmyUtils
import net.eagle0.eagle.model.proto_converters.command.available.AvailableCommandConverter
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.game_state.GameState
object AvailableCommandsFactory {
def shouldFollow(
@@ -47,7 +45,7 @@ object AvailableCommandsFactory {
def suggestedProvinceId(
fid: FactionId,
commandsByProvince: Map[ProvinceId, OneProvinceAvailableCommands],
gs: GameStateProto
gs: GameState
): ProvinceId = {
val lastActedProvinceId =
gs.factions(fid).lastActedProvinceIdThisRound
@@ -77,60 +75,63 @@ object AvailableCommandsFactory {
}
class AvailableCommandsFactory(
private val travelingFactories: Vector[UnifiedCommandFactory] = Vector(
ScalaFactoryAdapter(AvailableRecruitHeroesCommandFactory),
ScalaFactoryAdapter(AvailableDeclineQuestCommandsFactory),
ScalaFactoryAdapter(AvailableDivineCommandsFactory),
ScalaFactoryAdapter(AvailableArmTroopsCommandFactory),
ScalaFactoryAdapter(AvailableTradeCommandFactory),
ScalaFactoryAdapter(AvailableManagePrisonersCommandFactory),
ScalaFactoryAdapter(AvailableReturnCommandsFactory)
private val travelingFactories: Vector[AvailableCommandsFactoryForType] = Vector(
AvailableRecruitHeroesCommandFactory,
AvailableDeclineQuestCommandsFactory,
AvailableDivineCommandsFactory,
AvailableArmTroopsCommandFactory,
AvailableTradeCommandFactory,
AvailableManagePrisonersCommandFactory,
AvailableReturnCommandsFactory
),
private val handleRiotFactories: Vector[UnifiedCommandFactory] = Vector(
ScalaFactoryAdapter(AvailableHandleRiotCrackDownCommandFactory),
ScalaFactoryAdapter(AvailableHandleRiotDoNothingCommandFactory),
ScalaFactoryAdapter(AvailableHandleRiotGiveCommandFactory)
private val handleRiotFactories: Vector[AvailableCommandsFactoryForType] = Vector(
AvailableHandleRiotCrackDownCommandFactory,
AvailableHandleRiotDoNothingCommandFactory,
AvailableHandleRiotGiveCommandFactory
),
private val freeForAllDecisionPhaseFactories: Vector[UnifiedCommandFactory] = Vector(
ScalaFactoryAdapter(AvailableFreeForAllDecisionCommandFactory)
private val freeForAllDecisionPhaseFactories: Vector[
AvailableCommandsFactoryForType
] = Vector(
AvailableFreeForAllDecisionCommandFactory
),
private val attackDecisionPhaseFactories: Vector[UnifiedCommandFactory] = Vector(
ScalaFactoryAdapter(AvailableAttackDecisionCommandFactory)
private val attackDecisionPhaseFactories: Vector[
AvailableCommandsFactoryForType
] = Vector(
AvailableAttackDecisionCommandFactory
),
private val defensePhaseFactories: Vector[UnifiedCommandFactory] = Vector(
ScalaFactoryAdapter(AvailableResolveTributeCommandsFactory),
ScalaFactoryAdapter(AvailableDefendCommandsFactory)
private val defensePhaseFactories: Vector[AvailableCommandsFactoryForType] = Vector(
AvailableResolveTributeCommandsFactory,
AvailableDefendCommandsFactory
),
private val commandPhaseFactories: Vector[UnifiedCommandFactory] = Vector(
ScalaFactoryAdapter(AvailableApprehendOutlawCommandFactory),
ScalaFactoryAdapter(AvailableControlWeatherCommandsFactory),
ScalaFactoryAdapter(AvailableDiplomacyCommandsFactory),
ScalaFactoryAdapter(AvailableExileVassalCommandFactory),
ScalaFactoryAdapter(AvailableFeastCommandFactory),
ScalaFactoryAdapter(AvailableHeroGiftCommandFactory),
ScalaFactoryAdapter(AvailableImproveCommandsFactory),
ScalaFactoryAdapter(AvailableMarchCommandFactory),
ScalaFactoryAdapter(AvailableIssueOrdersCommandFactory),
ScalaFactoryAdapter(AvailableOrganizeTroopsCommandsFactory),
ScalaFactoryAdapter(AvailableAlmsCommandFactory),
ScalaFactoryAdapter(AvailableReconCommandFactory),
ScalaFactoryAdapter(AvailableRestCommandsFactory),
ScalaFactoryAdapter(AvailableSendSuppliesCommandFactory),
ScalaFactoryAdapter(AvailableStartEpidemicCommandFactory),
ScalaFactoryAdapter(AvailableSuppressBeastsCommandFactory),
ScalaFactoryAdapter(AvailableSwearBrotherhoodCommandFactory),
ScalaFactoryAdapter(AvailableTrainCommandsFactory),
ScalaFactoryAdapter(AvailableTravelCommandsFactory)
private val commandPhaseFactories: Vector[AvailableCommandsFactoryForType] = Vector(
AvailableApprehendOutlawCommandFactory,
AvailableControlWeatherCommandsFactory,
AvailableDiplomacyCommandsFactory,
AvailableExileVassalCommandFactory,
AvailableFeastCommandFactory,
AvailableHeroGiftCommandFactory,
AvailableImproveCommandsFactory,
AvailableMarchCommandFactory,
AvailableIssueOrdersCommandFactory,
AvailableOrganizeTroopsCommandsFactory,
AvailableAlmsCommandFactory,
AvailableReconCommandFactory,
AvailableRestCommandsFactory,
AvailableSendSuppliesCommandFactory,
AvailableStartEpidemicCommandFactory,
AvailableSuppressBeastsCommandFactory,
AvailableSwearBrotherhoodCommandFactory,
AvailableTrainCommandsFactory,
AvailableTravelCommandsFactory
)
) {
private def allCommands(
factories: Vector[UnifiedCommandFactory],
scalaState: GameState,
protoState: GameStateProto,
factories: Vector[AvailableCommandsFactoryForType],
gs: GameState,
fid: FactionId,
pid: ProvinceId
): Vector[AvailableCommand] =
factories.flatMap(_.availableCommand(scalaState, protoState, fid, pid)).sortBy {
factories.flatMap(_.availableCommand(gs, fid, pid)).sortBy {
// Travel commands
case _: RecruitHeroesAvailableCommand => 1
case _: DivineAvailableCommand => 2
@@ -169,11 +170,11 @@ class AvailableCommandsFactory(
case _ => 100
}
def fid(gs: GameStateProto, pid: ProvinceId): Option[FactionId] =
def fid(gs: GameState, pid: ProvinceId): Option[FactionId] =
gs.provinces(pid).rulingFactionId
private def suggestedCommandIndex(
gs: GameStateProto,
gs: GameState,
pid: ProvinceId,
commands: Vector[AvailableCommand]
): Int =
@@ -188,7 +189,7 @@ class AvailableCommandsFactory(
.getOrElse(0)
private def optionalOneProvinceAvailableCommands(
gs: GameStateProto,
gs: GameState,
pid: ProvinceId,
commands: Vector[AvailableCommand]
): Option[OneProvinceAvailableCommands] =
@@ -204,155 +205,115 @@ class AvailableCommandsFactory(
)
}
def hasAvailablePleaseRecruitMePhaseCommands(gs: GameStateProto): Boolean = {
val scalaState = GameStateConverter.fromProto(gs)
def hasAvailablePleaseRecruitMePhaseCommands(gs: GameState): Boolean =
gs.factions.keys
.exists(fid => pleaseRecruitMePhasePlayerCommands(scalaState, gs, fid).nonEmpty)
}
.exists(fid => pleaseRecruitMePhasePlayerCommands(gs, fid).nonEmpty)
def commandPhaseCommandsForProvince(
gs: GameStateProto,
pid: ProvinceId
): Option[OneProvinceAvailableCommands] = {
val scalaState = GameStateConverter.fromProto(gs)
commandPhaseCommandsForProvinceInternal(scalaState, gs, pid)
}
private def commandPhaseCommandsForProvinceInternal(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
pid: ProvinceId
): Option[OneProvinceAvailableCommands] =
optionalOneProvinceAvailableCommands(
gs = protoState,
gs = gs,
pid = pid,
commands =
if protoState.provinces(pid).rulerIsTraveling then
allCommands(travelingFactories, scalaState, protoState, fid(protoState, pid).get, pid)
if gs.provinces(pid).rulerIsTraveling then allCommands(travelingFactories, gs, fid(gs, pid).get, pid)
else
allCommands(
commandPhaseFactories,
scalaState,
protoState,
fid(protoState, pid).get,
gs,
fid(gs, pid).get,
pid
)
)
private def provinceHasCommandPhaseCommands(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
pid: ProvinceId
): Boolean =
if protoState.provinces(pid).rulerIsTraveling then
hasCommand(travelingFactories, scalaState, protoState, fid(protoState, pid).get, pid)
if gs.provinces(pid).rulerIsTraveling then hasCommand(travelingFactories, gs, fid(gs, pid).get, pid)
else
hasCommand(
commandPhaseFactories,
scalaState,
protoState,
fid(protoState, pid).get,
gs,
fid(gs, pid).get,
pid
)
def defensePhaseCommandsForProvince(
gs: GameStateProto,
pid: ProvinceId
): Option[OneProvinceAvailableCommands] = {
val scalaState = GameStateConverter.fromProto(gs)
defensePhaseCommandsForProvinceInternal(scalaState, gs, pid)
}
private def defensePhaseCommandsForProvinceInternal(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
pid: ProvinceId
): Option[OneProvinceAvailableCommands] =
optionalOneProvinceAvailableCommands(
gs = protoState,
gs = gs,
pid = pid,
commands = allCommands(
defensePhaseFactories,
scalaState,
protoState,
protoState.provinces(pid).rulingFactionId.get,
gs,
gs.provinces(pid).rulingFactionId.get,
pid
)
)
private def hasCommand(
factories: Vector[UnifiedCommandFactory],
scalaState: GameState,
protoState: GameStateProto,
factories: Vector[AvailableCommandsFactoryForType],
gs: GameState,
fid: FactionId,
pid: ProvinceId
): Boolean =
factories.exists(_.availableCommand(scalaState, protoState, fid, pid).nonEmpty)
factories.exists(_.availableCommand(gs, fid, pid).nonEmpty)
private def availableAftermathCommandsForProvince(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
pid: ProvinceId
): Option[OneProvinceAvailableCommands] =
optionalOneProvinceAvailableCommands(
gs = protoState,
gs = gs,
pid = pid,
commands = scalaState
commands = gs
.provinces(pid)
.rulingFactionId
.flatMap(fid =>
AvailableHandleCapturedHeroCommandFactory
.availableCommand(
gameState = scalaState,
gameState = gs,
factionId = fid,
provinceId = pid
)
.map(cmd => AvailableCommandConverter.toProto(cmd, protoState, fid))
)
.toVector
)
private def factionLeaderProvinces(
gs: GameStateProto,
gs: GameState,
fid: FactionId
): Iterable[ProvinceProto] =
): Iterable[Province] =
gs.provinces.values
.filter(_.rulingFactionId.contains(fid))
.filter(p => LegacyProvinceUtils.ruledByFactionLeader(p, gs))
private def defensePhasePlayerCommands(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
SortedMap(
factionLeaderProvinces(protoState, fid)
factionLeaderProvinces(gs, fid)
.map(_.id)
.flatMap(pid => defensePhaseCommandsForProvinceInternal(scalaState, protoState, pid).map(pid -> _))
.flatMap(pid => defensePhaseCommandsForProvince(gs, pid).map(pid -> _))
.toVector*
)
def handleRiotPhaseCommandsForOneProvince(
gs: GameStateProto,
p: ProvinceProto
): Option[OneProvinceAvailableCommands] = {
val scalaState = GameStateConverter.fromProto(gs)
handleRiotPhaseCommandsForOneProvinceInternal(scalaState, gs, p)
}
private def handleRiotPhaseCommandsForOneProvinceInternal(
scalaState: GameState,
protoState: GameStateProto,
p: ProvinceProto
gs: GameState,
p: Province
): Option[OneProvinceAvailableCommands] =
p.rulingHeroId.flatMap { fid =>
optionalOneProvinceAvailableCommands(
gs = protoState,
gs = gs,
pid = p.id,
commands = allCommands(
factories = handleRiotFactories,
scalaState = scalaState,
protoState = protoState,
gs = gs,
fid = fid,
pid = p.id
)
@@ -360,19 +321,17 @@ class AvailableCommandsFactory(
}
private def attackDecisionPhaseCommandsForOneProvince(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId,
p: ProvinceProto
p: Province
): Option[OneProvinceAvailableCommands] =
MoreOption.flatWhen(p.hostileArmies.exists(_.factionId == fid))(
optionalOneProvinceAvailableCommands(
gs = protoState,
gs = gs,
pid = p.id,
commands = allCommands(
factories = attackDecisionPhaseFactories,
scalaState = scalaState,
protoState = protoState,
gs = gs,
fid = fid,
pid = p.id
)
@@ -380,173 +339,146 @@ class AvailableCommandsFactory(
)
private def freeForAllDecisionPhaseCommandsForOneProvince(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId,
p: ProvinceProto
p: Province
): Option[OneProvinceAvailableCommands] =
optionalOneProvinceAvailableCommands(
gs = protoState,
gs = gs,
pid = p.id,
commands = allCommands(
factories = freeForAllDecisionPhaseFactories,
scalaState = scalaState,
protoState = protoState,
gs = gs,
fid = fid,
pid = p.id
)
)
def attackDecisionPhasePlayerCommands(
gs: GameStateProto,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] = {
val scalaState = GameStateConverter.fromProto(gs)
attackDecisionPhasePlayerCommandsInternal(scalaState, gs, fid)
}
private def attackDecisionPhasePlayerCommandsInternal(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
SortedMap(
protoState.provinces.values
gs.provinces.values
.filter(
_.hostileArmies.exists(ha => ha.factionId == fid)
)
.flatMap(p =>
attackDecisionPhaseCommandsForOneProvince(scalaState, protoState, fid, p)
attackDecisionPhaseCommandsForOneProvince(gs, fid, p)
.map(opac => p.id -> opac)
)
.toVector*
)
private def handleRiotPhaseCommandsForFaction(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
SortedMap(
protoState.provinces.values
gs.provinces.values
.filter(_.rulingFactionId.contains(fid))
.filter(LegacyProvinceUtils.hasImminentRiot)
.filterNot(_.hasActed)
.flatMap(p =>
handleRiotPhaseCommandsForOneProvinceInternal(scalaState, protoState, p)
handleRiotPhaseCommandsForOneProvince(gs, p)
.map(opac => p.id -> opac)
)
.toVector*
)
private def handleRiotPhaseCommandsForFactionLeader(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
handleRiotPhaseCommandsForFaction(scalaState, protoState, fid).filter {
handleRiotPhaseCommandsForFaction(gs, fid).filter {
case (pid, _: OneProvinceAvailableCommands) =>
LegacyProvinceUtils.ruledByFactionLeader(protoState.provinces(pid), protoState)
LegacyProvinceUtils.ruledByFactionLeader(gs.provinces(pid), gs)
}
private def freeForAllDecisionPhasePlayerCommands(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
SortedMap(
protoState.provinces.values
gs.provinces.values
.filter(
_.hostileArmies.exists(ha => ha.factionId == fid)
)
.flatMap(p =>
freeForAllDecisionPhaseCommandsForOneProvince(scalaState, protoState, fid, p)
freeForAllDecisionPhaseCommandsForOneProvince(gs, fid, p)
.map(opac => p.id -> opac)
)
.toVector*
)
private def commandsPhasePlayerCommands(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
SortedMap(
factionLeaderProvinces(protoState, fid)
factionLeaderProvinces(gs, fid)
.filterNot(_.hasActed)
.map(_.id)
.flatMap(pid => commandPhaseCommandsForProvinceInternal(scalaState, protoState, pid).map(pid -> _))
.flatMap(pid => commandPhaseCommandsForProvince(gs, pid).map(pid -> _))
.toVector*
)
private def hasCommandsPhaseCommandsForPlayer(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): Boolean =
factionLeaderProvinces(protoState, fid)
factionLeaderProvinces(gs, fid)
.filterNot(_.hasActed)
.exists(p => provinceHasCommandPhaseCommands(scalaState, protoState, p.id))
.exists(p => provinceHasCommandPhaseCommands(gs, p.id))
private def hasPlayerDefenseCommandsForPlayer(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): Boolean =
factionLeaderProvinces(protoState, fid).exists(p =>
defensePhaseCommandsForProvinceInternal(scalaState, protoState, p.id).isDefined
)
factionLeaderProvinces(gs, fid).exists(p => defensePhaseCommandsForProvince(gs, p.id).isDefined)
def hasAvailableHandleRiotPhaseCommands(gs: GameStateProto): Boolean = {
val scalaState = GameStateConverter.fromProto(gs)
gs.factions.keys.exists(fid => handleRiotPhaseCommandsForFactionLeader(scalaState, gs, fid).nonEmpty)
}
def hasAvailableHandleRiotPhaseCommands(gs: GameState): Boolean =
gs.factions.keys.exists(fid => handleRiotPhaseCommandsForFactionLeader(gs, fid).nonEmpty)
def hasAvailablePlayerCommandsPhaseCommands(gs: GameStateProto): Boolean = {
val scalaState = GameStateConverter.fromProto(gs)
gs.factions.keys.exists(fid => hasCommandsPhaseCommandsForPlayer(scalaState, gs, fid))
}
def hasAvailablePlayerCommandsPhaseCommands(gs: GameState): Boolean =
gs.factions.keys.exists(fid => hasCommandsPhaseCommandsForPlayer(gs, fid))
def hasAvailableFreeForAllDecisionPhaseCommands(gs: GameStateProto): Boolean = {
val scalaState = GameStateConverter.fromProto(gs)
gs.factions.keys.exists(fid => freeForAllDecisionPhasePlayerCommands(scalaState, gs, fid).nonEmpty)
}
def hasAvailableFreeForAllDecisionPhaseCommands(gs: GameState): Boolean =
gs.factions.keys.exists(fid => freeForAllDecisionPhasePlayerCommands(gs, fid).nonEmpty)
def hasAvailableAttackDecisionPhaseCommands(gs: GameStateProto): Boolean =
def hasAvailableAttackDecisionPhaseCommands(gs: GameState): Boolean =
gs.factions.keys.exists(fid => attackDecisionPhasePlayerCommands(gs, fid).nonEmpty)
def hasAvailablePlayerDefenseCommands(gs: GameStateProto): Boolean = {
val scalaState = GameStateConverter.fromProto(gs)
gs.factions.keys.exists(fid => hasPlayerDefenseCommandsForPlayer(scalaState, gs, fid))
}
def hasAvailablePlayerDefenseCommands(gs: GameState): Boolean =
gs.factions.keys.exists(fid => hasPlayerDefenseCommandsForPlayer(gs, fid))
private def diplomacyResolutionPhasePlayerCommands(
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
(
for {
faction <- protoState.factions.get(fid)
faction <- gs.factions.get(fid)
diplomacyCommand <- AvailableResolveInvitationCommandFactory
.availableCommand(protoState, faction.id)
.availableCommand(gs, faction.id)
.orElse(
AvailableResolveTruceOfferCommandFactory
.availableCommand(protoState, faction.id)
.availableCommand(gs, faction.id)
)
.orElse(
AvailableResolveRansomOfferCommandFactory
.availableCommand(protoState, faction.id)
.availableCommand(gs, faction.id)
)
.orElse(
AvailableResolveAllianceOfferCommandFactory
.availableCommand(protoState, faction.id)
.availableCommand(gs, faction.id)
)
.orElse(
AvailableResolveBreakAllianceCommandFactory
.availableCommand(protoState, faction.id)
.availableCommand(gs, faction.id)
)
oneProvinceAvailableCommand <- optionalOneProvinceAvailableCommands(
gs = protoState,
gs = gs,
0,
Vector(diplomacyCommand)
)
@@ -554,62 +486,56 @@ class AvailableCommandsFactory(
).getOrElse(SortedMap.empty)
private def pleaseRecruitMePhasePlayerCommands(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
(
for {
faction <- protoState.factions.get(fid)
faction <- gs.factions.get(fid)
pleaseRecruitMeCommand <- AvailablePleaseRecruitMeCommandFactory
.availableCommand(scalaState, faction.id)
.availableCommand(GameStateConverter.fromProto(gs), faction.id)
oneProvinceAvailableCommand <- optionalOneProvinceAvailableCommands(
gs = protoState,
gs = gs,
0,
Vector(pleaseRecruitMeCommand)
)
} yield SortedMap(0 -> oneProvinceAvailableCommand)
).getOrElse(SortedMap.empty)
def hasAvailableDiplomacyResolutionPhaseCommands(gs: GameStateProto): Boolean =
def hasAvailableDiplomacyResolutionPhaseCommands(gs: GameState): Boolean =
gs.factions.keys
.exists(fid => diplomacyResolutionPhasePlayerCommands(gs, fid).nonEmpty)
private def availableAftermathPhasePlayerCommands(
scalaState: GameState,
protoState: GameStateProto,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
SortedMap(
LegacyFactionUtils
.provinces(fid, protoState)
.provinces(fid, gs)
.map(_.id)
.flatMap(pid => availableAftermathCommandsForProvince(scalaState, protoState, pid).map(pid -> _))*
.flatMap(pid => availableAftermathCommandsForProvince(gs, pid).map(pid -> _))*
)
/** Main entry point - takes Scala GameState. Converts to proto once internally. */
def availablePlayerCommands(
scalaState: GameState,
gs: GameState,
fid: FactionId
): SortedMap[ProvinceId, OneProvinceAvailableCommands] = {
val protoState = GameStateConverter.toProto(scalaState)
protoState.currentPhase match {
case HANDLE_RIOT => handleRiotPhaseCommandsForFactionLeader(scalaState, protoState, fid)
case PLEASE_RECRUIT_ME => pleaseRecruitMePhasePlayerCommands(scalaState, protoState, fid)
case PLAYER_COMMANDS => commandsPhasePlayerCommands(scalaState, protoState, fid)
): SortedMap[ProvinceId, OneProvinceAvailableCommands] =
gs.currentPhase match {
case HANDLE_RIOT => handleRiotPhaseCommandsForFactionLeader(gs, fid)
case PLEASE_RECRUIT_ME => pleaseRecruitMePhasePlayerCommands(gs, fid)
case PLAYER_COMMANDS => commandsPhasePlayerCommands(gs, fid)
case FREE_FOR_ALL_DECISION =>
freeForAllDecisionPhasePlayerCommands(scalaState, protoState, fid)
case ATTACK_DECISION => attackDecisionPhasePlayerCommandsInternal(scalaState, protoState, fid)
case DEFENSE_DECISION => defensePhasePlayerCommands(scalaState, protoState, fid)
case BATTLE_AFTERMATH => availableAftermathPhasePlayerCommands(scalaState, protoState, fid)
freeForAllDecisionPhasePlayerCommands(gs, fid)
case ATTACK_DECISION => attackDecisionPhasePlayerCommands(gs, fid)
case DEFENSE_DECISION => defensePhasePlayerCommands(gs, fid)
case BATTLE_AFTERMATH => availableAftermathPhasePlayerCommands(gs, fid)
case DIPLOMACY_RESOLUTION =>
diplomacyResolutionPhasePlayerCommands(protoState, fid)
diplomacyResolutionPhasePlayerCommands(gs, fid)
case _ => SortedMap.empty[ProvinceId, OneProvinceAvailableCommands]
}
}
def hasAvailablePlayerCommands(gs: GameStateProto): Boolean = {
val scalaState = GameStateConverter.fromProto(gs)
def hasAvailablePlayerCommands(gs: GameState): Boolean =
gs.currentPhase match {
case DIPLOMACY_RESOLUTION =>
hasAvailableDiplomacyResolutionPhaseCommands(gs)
@@ -625,13 +551,12 @@ class AvailableCommandsFactory(
hasAvailableFreeForAllDecisionPhaseCommands(gs)
case PLAYER_COMMANDS =>
gs.factions.keys.exists { fid =>
hasCommandsPhaseCommandsForPlayer(scalaState, gs, fid)
hasCommandsPhaseCommandsForPlayer(gs, fid)
}
case BATTLE_AFTERMATH =>
gs.factions.keys.exists { fid =>
availableAftermathPhasePlayerCommands(scalaState, gs, fid).nonEmpty
availableAftermathPhasePlayerCommands(gs, fid).nonEmpty
}
case _ => false
}
}
}
@@ -1,44 +1,49 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.ControlWeatherAvailableCommand
import net.eagle0.eagle.api.available_command.ControlWeatherAvailableCommand.TargetProvinceOptions
import net.eagle0.eagle.api.command.util.control_weather_type.ControlWeatherType.{
CONTROL_WEATHER_END_BLIZZARD,
CONTROL_WEATHER_END_DROUGHT,
CONTROL_WEATHER_START_BLIZZARD,
CONTROL_WEATHER_START_DROUGHT
}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.MinVigorForControlWeather
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{ControlWeatherAvailable, TargetProvinceOptions}
import net.eagle0.eagle.model.state.command.common.ControlWeatherType
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.Profession
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableControlWeatherCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableControlWeatherCommandsFactory extends AvailableCommandsFactoryForType {
private def makeControlWeatherCommand(
gameState: GameState,
province: ProvinceT
): Option[AvailableCommand] = {
province: Province
): Option[ControlWeatherAvailableCommand] = {
val magesWithSufficientVigor = province.rulingFactionHeroIds
.map(gameState.heroes)
.filter(_.profession == Profession.Mage)
.filter(_.profession.isMage)
.filter(_.vigor >= MinVigorForControlWeather.doubleValue)
.toVector
val pidPlusNeighborPids = province.id +: province.neighbors.map(_.provinceId)
val pidPlusNeighborPids = province.id +: province.neighbors.map(
_.provinceId
)
val options = pidPlusNeighborPids.map { pid =>
TargetProvinceOptions(
provinceId = pid,
controlWeatherTypes = (
if ProvinceUtils.hasBlizzard(gameState.provinces(pid)) then Vector(ControlWeatherType.EndBlizzard)
else Vector(ControlWeatherType.StartBlizzard)
if LegacyProvinceUtils.hasBlizzard(gameState.provinces(pid)) then Vector(CONTROL_WEATHER_END_BLIZZARD)
else Vector(CONTROL_WEATHER_START_BLIZZARD)
) ++ (
if ProvinceUtils.hasDrought(gameState.provinces(pid)) then Vector(ControlWeatherType.EndDrought)
else Vector(ControlWeatherType.StartDrought)
if LegacyProvinceUtils.hasDrought(gameState.provinces(pid)) then Vector(CONTROL_WEATHER_END_DROUGHT)
else Vector(CONTROL_WEATHER_START_DROUGHT)
)
)
}
Option.when(magesWithSufficientVigor.nonEmpty) {
ControlWeatherAvailable(
ControlWeatherAvailableCommand(
availableHeroIds = magesWithSufficientVigor
.sortBy(-_.vigor)
.map(_.id),
@@ -52,7 +57,7 @@ object AvailableControlWeatherCommandsFactory extends ScalaAvailableCommandsFact
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[ControlWeatherAvailableCommand] =
makeControlWeatherCommand(
gameState,
gameState.provinces(provinceId)
@@ -2,40 +2,31 @@ package net.eagle0.eagle.library.actions.availability
import net.eagle0.common.MoreOption
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{DeclineQuestAvailable, ExpandedUnaffiliatedHero}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.unaffiliated_hero.RecruitmentInfo
import net.eagle0.eagle.api.available_command.DeclineQuestAvailableCommand
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.RECRUITMENT_STATUS_HAS_QUEST
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.availability.ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableDeclineQuestCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableDeclineQuestCommandsFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[DeclineQuestAvailableCommand] = {
val province = gameState.provinces(provinceId)
MoreOption.flatWhen(
province.rulerIsTraveling
&& ProvinceUtils.ruledByFactionLeader(province, gameState.factions.values.toVector)
&& LegacyProvinceUtils.ruledByFactionLeader(province, gameState)
) {
val withQuests = province.unaffiliatedHeroes
.filter(_.recruitmentInfo match {
case _: RecruitmentInfo.HasQuest => true
case _ => false
})
.filter(_.recruitmentInfo.get.status == RECRUITMENT_STATUS_HAS_QUEST)
Option.when(withQuests.nonEmpty) {
DeclineQuestAvailable(
DeclineQuestAvailableCommand(
actingProvinceId = provinceId,
declinableHeroes = withQuests.map(uh =>
ExpandedUnaffiliatedHero(
heroId = uh.heroId,
heroName = gameState.heroes(uh.heroId).nameTextId,
goldCost = 0
)
)
declinableHeroes = withQuests.map(uh => expandedUnaffiliatedHero(gs = gameState, uh = uh))
)
}
}
@@ -1,66 +1,57 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.DefendAvailableCommand
import net.eagle0.eagle.api.command.util.battalion_with_food_cost.BattalionWithFoodCost
import net.eagle0.eagle.internal.army.Attacking
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.{MaxCombatUnitCountPerSide, MinVigorForDefend}
import net.eagle0.eagle.library.util.battalion_suitability.BattalionSuitability
import net.eagle0.eagle.library.util.command_choice_helpers.CombatUnitSelector
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.BattalionUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{
BattalionWithFoodCost,
DefendAvailable,
RecommendedCombatUnit,
SuitableBattalions
}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.{HeroT, Profession}
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.HostileArmyGroupStatus.Attacking
object AvailableDefendCommandsFactory extends ScalaAvailableCommandsFactory {
private val scalaProfessionOrdering: Map[Profession, Int] = Map(
Profession.Paladin -> 1,
Profession.Engineer -> 2,
Profession.Ranger -> 3,
Profession.Mage -> 4,
Profession.Necromancer -> 5,
Profession.Champion -> 6,
Profession.NoProfession -> 7
)
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.LegacyBattalionUtils
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
import net.eagle0.eagle.model.proto_converters.SuitableBattalionsConverter
object AvailableDefendCommandsFactory extends AvailableCommandsFactoryForType {
private def sortedFleeProvinces(
from: ProvinceT,
from: Province,
gs: GameState
): Vector[ProvinceId] =
if FactionUtils
if LegacyFactionUtils
.provinces(
factionId = from.rulingFactionId.get,
provinces = gs.provinces.values.toVector
gameState = gs
)
.map(_.id)
.toVector
.forall(_ == from.id)
then Vector()
else
from.neighbors
.map(_.provinceId)
.map(gs.provinces)
// Prioritize provinces this player owns, then empty provinces
.sortBy(p =>
if p.rulingFactionId == from.rulingFactionId then 0
else if p.rulingFactionId.isEmpty then 1
else 2
)
.toVector
.map(_.id)
private def makeDefendCommand(
gameState: GameState,
province: ProvinceT
): Option[AvailableCommand] = {
province: Province
): Option[DefendAvailableCommand] = {
val hostileArmies = province.hostileArmies.filter { army =>
army.status match {
case Attacking => true
case _ => false
case Attacking(_) =>
true
case _ => false
}
}
@@ -76,54 +67,40 @@ object AvailableDefendCommandsFactory extends ScalaAvailableCommandsFactory {
val availableBattalionsWithCosts = availableBattalions.map(batt =>
BattalionWithFoodCost(
battalionId = batt.id,
foodCost = BattalionUtils
.monthlyConsumedFood(
batt,
gameState.battalionTypes
)
.ceil
.toInt,
troopCount = batt.size
LegacyBattalionUtils.monthlyConsumedFood(
batt,
gameState.battalionTypes.toVector
)
)
)
val recommendedUnits = CombatUnitSelector
.selectedCombatBattalions(
desiredCount = availableHeroes.size.min(MaxCombatUnitCountPerSide.intValue),
heroes = availableHeroes.toVector,
battalions = availableBattalions.toVector,
battalionTypes = gameState.battalionTypes
)
.map(cu => RecommendedCombatUnit(heroId = cu.heroId, battalionId = cu.battalionId))
val hostileUnits = hostileArmies
val recommendedUnits = CombatUnitSelector.selectedCombatBattalions(
desiredCount = availableHeroes.size.min(MaxCombatUnitCountPerSide.intValue),
heroes = availableHeroes.map(HeroConverter.fromProto).toVector,
battalions = availableBattalions.map(BattalionConverter.fromProto).toVector,
battalionTypes = gameState.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
)
val hostileUnits = hostileArmies
.flatMap(_.armies)
.map(_.army)
.flatMap(_.army)
.flatMap(_.units)
// Convert from util.SuitableBattalions to AvailableCommand.SuitableBattalions
val suitableBattalionsMap = BattalionSuitability
.suitableBattalionsForHeroes(
hs = availableHeroes.toVector,
bs = availableBattalions.toVector,
bts = gameState.battalionTypes
)
.map {
case (heroId, sb) =>
heroId -> SuitableBattalions(sb.battalionIdsWithSuitability.map(_.battalionId))
}
Some(
DefendAvailable(
suitableBattalionsForHeroes = suitableBattalionsMap,
availableFleeProvinceIds = sortedFleeProvinces(from = province, gs = gameState),
availableBattalions = availableBattalionsWithCosts,
DefendAvailableCommand(
availableHeroIds = availableHeroes
.sortBy(h => scalaProfessionOrdering.getOrElse(h.profession, 10))
.sortBy(h => professionOrdering(h.profession))
.toVector
.map(_.id),
recommendedUnits = recommendedUnits,
actingProvinceId = province.id,
suitableBattalionsForHeroes = SuitableBattalionsConverter.toProtoMap(
BattalionSuitability.suitableBattalionsForHeroes(
hs = availableHeroes.map(HeroConverter.fromProto).toVector,
bs = availableBattalions.map(BattalionConverter.fromProto).toVector,
bts = gameState.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
)
),
availableBattalions = availableBattalionsWithCosts,
availableFleeProvinceIds = sortedFleeProvinces(from = province, gs = gameState),
recommendedUnits = recommendedUnits,
hostileFactionIds = hostileArmies.toVector.map(_.factionId).distinct,
hostileHeroCount = hostileUnits.size,
hostileTroopCount = hostileUnits
@@ -141,7 +118,7 @@ object AvailableDefendCommandsFactory extends ScalaAvailableCommandsFactory {
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[DefendAvailableCommand] =
makeDefendCommand(
gameState,
gameState.provinces(provinceId)
@@ -1,6 +1,23 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.available_command.DiplomacyAvailableCommand
import net.eagle0.eagle.api.command.util.diplomacy_option.{
AllianceOption,
BreakAllianceOption,
DiplomacyOption,
InvitationOption,
RansomOfferOption,
TruceOption
}
import net.eagle0.eagle.common.diplomacy_offer.{
HostageOfferedInExchange,
PrisonerOfferedInExchange,
PrisonerToBeRansomed,
RansomOfferDetails
}
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.UNAFFILIATED_HERO_PRISONER
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.{
AllianceGoldCost,
BreakAllianceGoldCost,
@@ -9,39 +26,28 @@ import net.eagle0.eagle.library.settings.{
MinVigorForDiplomacy,
TruceGoldCost
}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.hero.HeroUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.ProvinceDistances
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.DiplomacyOption
import net.eagle0.eagle.model.state.command.common.{
DiplomacyOptionType,
HostageOfferedInExchange,
PrisonerOfferedInExchange,
PrisonerToBeRansomed,
RansomOfferDetails
}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroType.Prisoner
object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableDiplomacyCommandsFactory extends AvailableCommandsFactoryForType {
def availableHeroIds(gs: GameState, pid: ProvinceId): Vector[HeroId] =
gs.provinces(pid)
.rulingFactionHeroIds
.filter(hid => gs.heroes(hid).vigor >= MinVigorForDiplomacy.doubleValue)
.toVector
def recommendedHeroId(gs: GameState, availableHids: Seq[HeroId]): HeroId =
availableHids
.sortBy(hid => (HeroUtils.fatigue(gs.heroes(hid)), -gs.heroes(hid).vigor)) match {
.sortBy(hid => (LegacyHeroUtils.fatigue(gs.heroes(hid)), -gs.heroes(hid).vigor)) match {
case sortedHeroes =>
sortedHeroes
.find(hid => !FactionUtils.isFactionHead(hid, gs.factions.values))
.find(hid => !LegacyFactionUtils.isFactionHead(hid, gs))
.orElse(
sortedHeroes
.find(hid => !FactionUtils.isFactionLeader(hid, gs.factions.values.toVector))
.find(hid => !LegacyFactionUtils.isFactionLeader(hid, gs))
)
.getOrElse(sortedHeroes.head)
}
@@ -50,49 +56,64 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
gs: GameState,
pid: ProvinceId,
targetFid: FactionId
): Option[DiplomacyOptionType] =
): Option[DiplomacyOption] =
Option.when(
gs.provinces(pid).gold >= TruceGoldCost.intValue
&& ProvinceUtils.ruledByFactionLeader(
&& LegacyProvinceUtils.ruledByFactionLeader(
province = gs.provinces(pid),
factions = gs.factions.values.toVector
gameState = gs
)
&& !FactionUtils.hasAlliance(
&& !LegacyFactionUtils.hasAlliance(
fid1 = gs.provinces(pid).rulingFactionId.get,
fid2 = targetFid,
factions = gs.factions.values.toVector
gs = gs
)
)(DiplomacyOptionType.Truce)
)(
TruceOption(
targetFactionId = targetFid,
goldCost = TruceGoldCost.intValue
)
)
private def allianceOptionForTargetFaction(
gs: GameState,
pid: ProvinceId,
actingFid: FactionId,
targetFid: FactionId
): Option[DiplomacyOptionType] =
): Option[DiplomacyOption] =
Option.when(
gs.provinces(pid).gold >= AllianceGoldCost.intValue
&& ProvinceUtils.ruledByFactionLeader(
&& LegacyProvinceUtils.ruledByFactionLeader(
province = gs.provinces(pid),
factions = gs.factions.values.toVector
) && !FactionUtils
.hasAlliance(fid1 = actingFid, fid2 = targetFid, factions = gs.factions.values.toVector)
)(DiplomacyOptionType.Alliance)
gameState = gs
) && !LegacyFactionUtils
.hasAlliance(fid1 = actingFid, fid2 = targetFid, gs = gs)
)(
AllianceOption(
targetFactionId = targetFid,
goldCost = AllianceGoldCost.intValue
)
)
private def breakAllianceOptionForTargetFaction(
gs: GameState,
pid: ProvinceId,
actingFid: FactionId,
targetFid: FactionId
): Option[DiplomacyOptionType] =
): Option[DiplomacyOption] =
Option.when(
gs.provinces(pid).gold >= BreakAllianceGoldCost.intValue
&& ProvinceUtils.ruledByFactionLeader(
&& LegacyProvinceUtils.ruledByFactionLeader(
province = gs.provinces(pid),
factions = gs.factions.values.toVector
) && FactionUtils
.hasAlliance(fid1 = actingFid, fid2 = targetFid, factions = gs.factions.values.toVector)
)(DiplomacyOptionType.BreakAlliance)
gameState = gs
) && LegacyFactionUtils
.hasAlliance(fid1 = actingFid, fid2 = targetFid, gs = gs)
)(
BreakAllianceOption(
targetFactionId = targetFid,
goldCost = BreakAllianceGoldCost.intValue
)
)
private def hasClearPath(
gs: GameState,
@@ -100,7 +121,7 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
actingFid: FactionId,
targetFid: FactionId
): Boolean =
FactionUtils.provinces(targetFid, gs.provinces.values.toVector).headOption.exists { targetProvince =>
LegacyFactionUtils.provinces(targetFid, gs).headOption.exists { targetProvince =>
val eligibleNeighbors: ProvinceId => Vector[ProvinceId] =
p =>
gs.provinces(p)
@@ -115,14 +136,15 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
// inviting across the target's province
neighborProvince.rulingFactionId.contains(targetFid) ||
// inviting across your ally's province
FactionUtils
LegacyFactionUtils
.hasAlliance(
neighborProvince.rulingFactionId.get,
actingFid,
gs.factions.values.toVector
gs
)
)
.map(_.id)
.toVector
ProvinceDistances
.distance(pid, targetProvince.id, eligibleNeighbors)
@@ -134,30 +156,35 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
pid: ProvinceId,
actingFid: FactionId,
targetFid: FactionId
): Option[DiplomacyOptionType] =
): Option[DiplomacyOption] =
Option.when(
gs.currentRoundId >= gs.factions(actingFid).earliestRoundForInvitation &&
gs.provinces(pid).gold >= InviteGoldCost.intValue
&& FactionUtils
&& LegacyFactionUtils
.provinceCount(
targetFid,
gs.provinces.values
gs
) <= MaxProvincesOnTargetForInvite.intValue
&& FactionUtils.provinceCount(actingFid, gs.provinces.values) > FactionUtils
.provinceCount(targetFid, gs.provinces.values)
&& ProvinceUtils.ruledByFactionLeader(
&& LegacyFactionUtils.provinceCount(actingFid, gs) > LegacyFactionUtils
.provinceCount(targetFid, gs)
&& LegacyProvinceUtils.ruledByFactionLeader(
province = gs.provinces(pid),
factions = gs.factions.values.toVector
gameState = gs
)
&& hasClearPath(gs, pid, actingFid, targetFid)
)(DiplomacyOptionType.Invitation)
)(
InvitationOption(
targetFactionId = targetFid,
goldCost = InviteGoldCost.intValue
)
)
private def ransomOptionsForTargetFaction(
gs: GameState,
pid: ProvinceId,
actingFid: FactionId,
targetFid: FactionId
): Vector[DiplomacyOptionType] = {
): Vector[DiplomacyOption] = {
val prisonersAvailableToOffer = availablePrisoners(gs.provinces(pid))
val hostagesAvailableToOffer = availableHostages(gs, pid)
val goldAvailableToOffer = gs.provinces(pid).gold
@@ -169,12 +196,15 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
factionId = actingFid,
targetFactionId = targetFid
).map { prisonerToBeRansomed =>
DiplomacyOptionType.Ransom(
RansomOfferDetails(
prisonerToBeRansomed = prisonerToBeRansomed,
prisonersOffered = prisonersAvailableToOffer,
hostagesOffered = hostagesAvailableToOffer,
goldOffered = goldAvailableToOffer
RansomOfferOption(
targetFactionId = targetFid,
ransomOffer = Some(
RansomOfferDetails(
prisonerToBeRansomed = Some(prisonerToBeRansomed),
prisonersOffered = prisonersAvailableToOffer,
hostagesOffered = hostagesAvailableToOffer,
goldOffered = goldAvailableToOffer
)
)
)
}
@@ -187,7 +217,7 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
pid: ProvinceId,
actingFid: FactionId,
targetFid: FactionId
): Vector[DiplomacyOptionType] =
): Vector[DiplomacyOption] =
ransomOptionsForTargetFaction(
gs,
pid,
@@ -228,11 +258,11 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[DiplomacyAvailableCommand] =
if availableHeroIds(gameState, provinceId).isEmpty then None
else if gameState.provinces(provinceId).rulingFactionHeroIds.size < 2 then None
else {
val optionsByFaction: Map[FactionId, Vector[DiplomacyOptionType]] = gameState.factions.keys
gameState.factions.keys
.filterNot(_ == factionId)
.filterNot(fid =>
hasIncomingDiplomacyOffer(
@@ -241,53 +271,70 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
gameState = gameState
)
)
.map { targetFid =>
targetFid -> diplomacyOptionsForTargetFaction(
.flatMap(targetFid =>
diplomacyOptionsForTargetFaction(
gs = gameState,
pid = provinceId,
actingFid = factionId,
targetFid = targetFid
)
}
.filter(_._2.nonEmpty)
.toMap
if optionsByFaction.isEmpty then None
else {
val heroIds = availableHeroIds(gameState, provinceId)
val options = optionsByFaction.map {
case (targetFid, optionTypes) =>
DiplomacyOption(
targetFactionId = targetFid,
optionTypes = optionTypes
)
}.toVector
.sortBy(opt => FactionUtils.sortKey(gameState.factions(opt.targetFactionId)))
Some(
AvailableCommand.DiplomacyAvailable(
availableHeroIds = heroIds,
options = options,
actingProvinceId = provinceId,
recommendedHeroId = recommendedHeroId(gameState, heroIds)
)
)
.toVector
.sortBy(opt =>
LegacyFactionUtils.sortKey(gameState.factions(opt match {
case TruceOption(targetFactionId, _, _ /* unknownFieldSet */ ) =>
targetFactionId
case InvitationOption(
targetFactionId,
_,
_ /* unknownFieldSet */
) =>
targetFactionId
case RansomOfferOption(
targetFactionId,
_,
_ /* unknownFieldSet */
) =>
targetFactionId
case AllianceOption(targetFactionId, _, _ /* unknownFieldSet */ ) =>
targetFactionId
case BreakAllianceOption(
targetFactionId,
_,
_ /* unknownFieldSet */
) =>
targetFactionId
case DiplomacyOption.Empty => ???
}))
) match {
case items if items.isEmpty => None
case options =>
val heroIds = availableHeroIds(gameState, provinceId)
Some(
DiplomacyAvailableCommand(
availableHeroIds = heroIds,
options = options,
actingProvinceId = provinceId,
recommendedHeroId = recommendedHeroId(gameState, heroIds)
)
)
}
end if
}
private def availablePrisoners(
province: ProvinceT
province: Province
): Vector[PrisonerOfferedInExchange] =
province.unaffiliatedHeroes
.filter(_.unaffiliatedHeroType == Prisoner)
.filter(_.`type` == UNAFFILIATED_HERO_PRISONER)
.map { prisoner =>
PrisonerOfferedInExchange(
heroId = prisoner.heroId,
factionIdForPrisoner = prisoner.lastFactionId,
factionIdForPrisoner = prisoner.lastFaction.getOrElse(-1),
provinceIdWithHero = province.id
)
}
.toVector
private def availableHostages(
gameState: GameState,
@@ -304,6 +351,7 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
provinceIdWithHero = provinceId
)
)
.toVector
private def availablePrisonersToRansom(
gameState: GameState,
@@ -315,10 +363,10 @@ object AvailableDiplomacyCommandsFactory extends ScalaAvailableCommandsFactory {
.filter(_.rulingFactionId.contains(targetFactionId))
.toVector
prisoner <- province.unaffiliatedHeroes.filter(
_.unaffiliatedHeroType == Prisoner
_.`type` == UNAFFILIATED_HERO_PRISONER
)
if prisoner.lastFactionId.contains(factionId)
if FactionUtils.isFactionLeader(prisoner.heroId, gameState.factions.values.toVector)
if prisoner.lastFaction.contains(factionId)
if LegacyFactionUtils.isFactionLeader(prisoner.heroId, gameState)
} yield PrisonerToBeRansomed(
prisonerHeroId = prisoner.heroId,
provinceIdForPrisoner = province.id
@@ -1,48 +1,41 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.common.MoreOption
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.DivineAvailableCommand
import net.eagle0.eagle.common.recruitment_info.RecruitmentStatus.RECRUITMENT_STATUS_NOT_DIVINED
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.actions.availability.ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero
import net.eagle0.eagle.library.settings.GoldCostForDivine
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{DivineAvailable, ExpandedUnaffiliatedHero}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.unaffiliated_hero.RecruitmentInfo
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableDivineCommandsFactory extends ScalaAvailableCommandsFactory {
private def effectiveCost(province: ProvinceT): Int =
object AvailableDivineCommandsFactory extends AvailableCommandsFactoryForType {
private def effectiveCost(province: Province): Int =
Math.floor(GoldCostForDivine.intValue * province.priceIndex + 0.5).toInt
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[DivineAvailableCommand] = {
val province = gameState.provinces(provinceId)
MoreOption.flatWhen(
province.rulerIsTraveling
&& ProvinceUtils.ruledByFactionLeader(province, gameState.factions.values.toVector)
&& LegacyProvinceUtils.ruledByFactionLeader(province, gameState)
&& province.gold >= effectiveCost(province)
) {
val undivined = province.unaffiliatedHeroes
.filter(_.recruitmentInfo == RecruitmentInfo.NotDivined)
.filter(_.recruitmentInfo.get.status == RECRUITMENT_STATUS_NOT_DIVINED)
val expandedHeroes = undivined.map(uh =>
ExpandedUnaffiliatedHero(
heroId = uh.heroId,
heroName = gameState.heroes(uh.heroId).nameTextId,
goldCost = effectiveCost(province)
)
)
val expandedWithQuestAvailable = undivined
.map(uh => expandedUnaffiliatedHero(gs = gameState, uh = uh))
Option.when(expandedHeroes.nonEmpty) {
DivineAvailable(
Option.when(expandedWithQuestAvailable.nonEmpty) {
DivineAvailableCommand(
actingProvinceId = provinceId,
costPerHero = effectiveCost(province),
divinableHeroes = expandedHeroes,
unavailableHeroes = Vector()
divinableHeroes = expandedWithQuestAvailable
)
}
}
@@ -1,39 +1,39 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.ExileVassalAvailable
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.api.available_command.ExileVassalAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableExileVassalCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableExileVassalCommandFactory extends AvailableCommandsFactoryForType {
def exilableHeroIds(
province: ProvinceT,
factions: Vector[FactionT]
gameState: GameState,
province: Province
): Vector[HeroId] =
province.rulingFactionHeroIds
.filterNot(hid => FactionUtils.isFactionLeader(hid, factions))
.filterNot(hid => LegacyFactionUtils.isFactionLeader(hid, gameState))
.toVector
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[ExileVassalAvailableCommand] = {
val province = gameState.provinces(provinceId)
val factions = gameState.factions.values.toVector
val exilable = exilableHeroIds(province, factions)
val exilable = exilableHeroIds(gameState, province)
Option.when(
ProvinceUtils.ruledByFactionLeader(province, factions) && exilable.nonEmpty
LegacyProvinceUtils
.ruledByFactionLeader(province, gameState) && exilable.nonEmpty
) {
ExileVassalAvailable(
ExileVassalAvailableCommand(
exilableHeroIds = exilable,
actingProvinceId = provinceId
)
}
}
}
@@ -1,40 +1,40 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.FeastAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
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
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
object AvailableFeastCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableFeastCommandFactory extends AvailableCommandsFactoryForType {
private def hasHeroThatBenefits(
gameState: GameState,
province: ProvinceT
): Boolean = {
val factions = gameState.factions.values.toVector
province: Province
): Boolean =
province.rulingFactionHeroIds
.flatMap(hid => gameState.heroes.get(hid))
.exists(h => h.vigor < h.constitution || HeroUtils.effectiveLoyalty(h, factions) < 100.0)
}
.map(gameState.heroes)
.exists(h =>
h.vigor < h.constitution || LegacyHeroUtils
.effectiveLoyalty(h.id, gameState) < 100.0
)
def feastGoldCost(province: ProvinceT): Int =
def feastGoldCost(province: Province): Int =
(province.rulingFactionHeroIds.length.toDouble * province.priceIndex * FeastGoldCostPerHero.intValue).ceil.toInt
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[FeastAvailableCommand] = {
val province = gameState.provinces(provinceId)
val goldCost = feastGoldCost(province)
Option.when(
province.gold >= goldCost && hasHeroThatBenefits(gameState, province)
) {
FeastAvailable(
FeastAvailableCommand(
goldCost = goldCost,
actingProvinceId = provinceId
)
@@ -1,29 +1,25 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.FreeForAllDecisionAvailableCommand
import net.eagle0.eagle.api.command.util.army_stats.ArmyStats
import net.eagle0.eagle.api.command.util.attack_decision_type.{AdvanceDecision, AttackDecisionType, WithdrawDecision}
import net.eagle0.eagle.common.round_phase.RoundPhase
import net.eagle0.eagle.internal.army.{AwaitingDecision, HostileArmyGroup, MovingArmy}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.IncomingArmyUtils
import net.eagle0.eagle.model.state.{Army, HostileArmyGroupStatus, MovingArmy}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{
ArmyStats,
ExpandedCombatUnit,
FreeForAllDecisionAvailable
}
import net.eagle0.eagle.model.state.command.common.AttackDecisionType
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.RoundPhase
object AvailableFreeForAllDecisionCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableFreeForAllDecisionCommandFactory extends AvailableCommandsFactoryForType {
private def decisionOptions(
armies: Vector[MovingArmy]
armies: Seq[MovingArmy]
): Vector[AttackDecisionType] =
if armies.forall(_.army.fleeProvinceId.isDefined) then
if armies.forall(_.getArmy.fleeProvinceId.isDefined) then
Vector(
AttackDecisionType.Advance,
AttackDecisionType.Withdraw
AdvanceDecision(),
WithdrawDecision()
)
else Vector(AttackDecisionType.Advance)
else Vector(AdvanceDecision())
// We need to include the armies that have already withdrawn or advanced, in a consistent sort order,
// so that the player doesn't learn what others have done by the shifts.
@@ -33,76 +29,48 @@ object AvailableFreeForAllDecisionCommandFactory extends ScalaAvailableCommandsF
): Vector[MovingArmy] =
gs.provinces(pid)
.hostileArmies
.filter(_.status == HostileArmyGroupStatus.AwaitingDecision)
.filter(_.status == AwaitingDecision())
.flatMap(_.armies)
.sortBy(ma => (ma.army.factionId, ma.army.units.head.heroId))
.sortBy(ma => (ma.getArmy.factionId, ma.getArmy.units.head.heroId))
.toVector
private def myHostileArmies(
pid: ProvinceId,
fid: FactionId,
gs: GameState
): Option[Vector[MovingArmy]] =
): Option[HostileArmyGroup] =
gs.provinces(pid)
.hostileArmies
.filter(_.status == HostileArmyGroupStatus.AwaitingDecision)
.filter(_.status == AwaitingDecision())
.find(_.factionId == fid)
.map(_.armies)
private def armyStats(
toFid: FactionId,
pid: ProvinceId,
gs: GameState
): Vector[ArmyStats] =
relevantArmies(pid, gs).map(ma => stats(toFid, ma.army, gs))
private def stats(
toFid: FactionId,
army: Army,
gs: GameState
): ArmyStats = {
val battalions = army.units.flatMap(_.battalionId).map(gs.battalions)
val troopCount = battalions.map(_.size).sum
ArmyStats(
factionId = army.factionId,
heroCount = army.units.size,
troopCount = troopCount
)
}
private def expandedCombatUnits(
armies: Vector[MovingArmy],
gs: GameState
): Vector[ExpandedCombatUnit] =
armies
.flatMap(_.army.units)
.map { cu =>
val battalionOpt = cu.battalionId.map(gs.battalions)
ExpandedCombatUnit(
heroId = cu.heroId,
battalionId = cu.battalionId,
heroName = gs.heroes(cu.heroId).nameTextId,
troopCount = battalionOpt.map(_.size).getOrElse(0)
)
}
relevantArmies(pid, gs).map(ia => IncomingArmyUtils.stats(toFid, ia.getArmy, gs, ia.originProvince))
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
myHostileArmies(provinceId, factionId, gameState).flatMap { armies =>
): Option[FreeForAllDecisionAvailableCommand] =
myHostileArmies(provinceId, factionId, gameState).flatMap { hostileArmyGroup =>
Option.when(
gameState.currentPhase == RoundPhase.FreeForAllDecision
gameState.currentPhase == RoundPhase.FREE_FOR_ALL_DECISION
&& !IncomingArmyUtils.incomingArmiesAreMutuallyAllied(
gameState.provinces(provinceId),
gameState
)
)(
FreeForAllDecisionAvailable(
FreeForAllDecisionAvailableCommand(
provinceId = provinceId,
armies = armyStats(factionId, provinceId, gameState),
actingUnits = expandedCombatUnits(armies, gameState),
availableDecisions = decisionOptions(armies)
actingUnits = hostileArmyGroup.armies
.flatMap(_.getArmy.units)
.map(cu => ExpandedCombatUnitUtils.expandedCombatUnit(cu, gameState)),
availableDecisions = decisionOptions(hostileArmyGroup.armies)
)
)
}
@@ -1,42 +1,51 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{
ExpandedCapturedHero,
HandleCapturedHeroAvailable
import net.eagle0.eagle.api.available_command.{ExpandedCapturedHero, HandleCapturedHeroAvailableCommand}
import net.eagle0.eagle.api.command.util.captured_hero_option.CapturedHeroOption.{
EXECUTE_CAPTURED_HERO_OPTION,
EXILE_CAPTURED_HERO_OPTION,
IMPRISON_CAPTURED_HERO_OPTION,
RECRUIT_CAPTURED_HERO_OPTION,
RETURN_CAPTURED_HERO_OPTION
}
import net.eagle0.eagle.model.state.command.common.CapturedHeroOption
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.CapturedHero
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.unaffiliated_hero.CapturedHero
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.view_filters.HeroViewFilter
object AvailableHandleCapturedHeroCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableHandleCapturedHeroCommandFactory extends AvailableCommandsFactoryForType {
private val optionsWithoutRecruit =
Vector(
CapturedHeroOption.Imprison,
CapturedHeroOption.Exile,
CapturedHeroOption.Execute
IMPRISON_CAPTURED_HERO_OPTION,
EXILE_CAPTURED_HERO_OPTION,
EXECUTE_CAPTURED_HERO_OPTION
)
private val optionsWithRecruit =
CapturedHeroOption.Recruit +: optionsWithoutRecruit
RECRUIT_CAPTURED_HERO_OPTION +: optionsWithoutRecruit
private val optionsForFactionLeader = Vector(
CapturedHeroOption.Imprison,
CapturedHeroOption.Return,
CapturedHeroOption.Execute
IMPRISON_CAPTURED_HERO_OPTION,
RETURN_CAPTURED_HERO_OPTION,
EXECUTE_CAPTURED_HERO_OPTION
)
private def expandedCapturedHero(
gs: GameState,
ch: CapturedHero
): ExpandedCapturedHero = {
val isFactionLeader = FactionUtils
.isFactionLeader(ch.heroId, gs.factions.values.toVector)
val isFactionLeader = LegacyFactionUtils
.isFactionLeader(ch.heroId, gs)
ExpandedCapturedHero(
heroId = ch.heroId,
heroName = gs.heroes(ch.heroId).nameTextId,
hero = Some(
HeroViewFilter
.filteredHeroView(
hero = gs.heroes(ch.heroId),
gs = gs,
factionId = None // can see unaffiliated heroes
)
),
options =
if ch.recruitmentAttempted then optionsWithoutRecruit
else if isFactionLeader
@@ -56,11 +65,11 @@ object AvailableHandleCapturedHeroCommandFactory extends ScalaAvailableCommandsF
gameState: GameState,
capturedHeroes: Vector[CapturedHero],
provinceId: ProvinceId
): Option[HandleCapturedHeroAvailable] =
): Option[HandleCapturedHeroAvailableCommand] =
capturedHeroes
.find(ch => ch.recruitmentRefusedMessageId.nonEmpty || ch.messageId.nonEmpty)
.map { capturedHero =>
HandleCapturedHeroAvailable(
HandleCapturedHeroAvailableCommand(
actingProvinceId = provinceId,
availableHeroes = Vector(expandedCapturedHero(gameState, capturedHero))
)
@@ -70,7 +79,7 @@ object AvailableHandleCapturedHeroCommandFactory extends ScalaAvailableCommandsF
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[HandleCapturedHeroAvailableCommand] =
// If we've already attempted to recruit one of the heroes, only show available commands for that one
gameState
.provinces(provinceId)
@@ -81,13 +90,13 @@ object AvailableHandleCapturedHeroCommandFactory extends ScalaAvailableCommandsF
case (alreadyRecruited, notRecruitedNonEmpty) if alreadyRecruited.isEmpty =>
firstOption(
gameState = gameState,
capturedHeroes = notRecruitedNonEmpty,
capturedHeroes = notRecruitedNonEmpty.toVector,
provinceId = provinceId
)
case (alreadyRecruitedNonEmpty, _) =>
firstOption(
gameState = gameState,
capturedHeroes = alreadyRecruitedNonEmpty,
capturedHeroes = alreadyRecruitedNonEmpty.toVector,
provinceId = provinceId
)
}
@@ -1,17 +1,16 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.available_command.HandleRiotCrackDownAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.MinVigorForCrackDown
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.HandleRiotCrackDownAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableHandleRiotCrackDownCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableHandleRiotCrackDownCommandFactory extends AvailableCommandsFactoryForType {
def crackDownHeroIds(
province: ProvinceT,
province: Province,
gameState: GameState
): Vector[HeroId] =
province.rulingFactionHeroIds
@@ -22,14 +21,15 @@ object AvailableHandleRiotCrackDownCommandFactory extends ScalaAvailableCommands
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[HandleRiotCrackDownAvailableCommand] = {
val province = gameState.provinces(provinceId)
val heroIds = crackDownHeroIds(province, gameState)
Option.when(
ProvinceUtils.hasImminentRiot(province) && !province.hasActed && heroIds.nonEmpty
LegacyProvinceUtils
.hasImminentRiot(province) && !province.hasActed && heroIds.nonEmpty
) {
HandleRiotCrackDownAvailable(
HandleRiotCrackDownAvailableCommand(
actingProvinceId = provinceId,
availableHeroIds = heroIds,
battalionIdsAvailable = province.battalionIds
@@ -1,22 +1,22 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.HandleRiotDoNothingAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.api.available_command.HandleRiotDoNothingAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableHandleRiotDoNothingCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableHandleRiotDoNothingCommandFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
val province = gameState.provinces(provinceId)
): Option[HandleRiotDoNothingAvailableCommand] =
Option.when(
ProvinceUtils.hasImminentRiot(province) && !province.hasActed
LegacyProvinceUtils.hasImminentRiot(
gameState
.provinces(provinceId)
) && !gameState.provinces(provinceId).hasActed
) {
HandleRiotDoNothingAvailable(actingProvinceId = provinceId)
HandleRiotDoNothingAvailableCommand(actingProvinceId = provinceId)
}
}
}
@@ -1,28 +1,30 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.HandleRiotGiveAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.{RiotMaxFood, RiotMaxGold}
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.HandleRiotGiveAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableHandleRiotGiveCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableHandleRiotGiveCommandFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[HandleRiotGiveAvailableCommand] = {
val province = gameState.provinces(provinceId)
val food = Math.min(province.food, RiotMaxFood.intValue)
val gold = Math.min(province.gold, RiotMaxGold.intValue)
Option.when(
ProvinceUtils.hasImminentRiot(province) && !province.hasActed && (food > 0 || gold > 0)
LegacyProvinceUtils
.hasImminentRiot(
province
) && !province.hasActed && (food > 0 || gold > 0)
) {
HandleRiotGiveAvailable(
HandleRiotGiveAvailableCommand(
actingProvinceId = provinceId,
foodAvailable = food,
goldAvailable = gold
@@ -1,23 +1,21 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.{EligibleGift, HeroGiftAvailableCommand}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.MaxGiftGold
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.hero.HeroUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{EligibleGift, HeroGiftAvailable}
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
object AvailableHeroGiftCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableHeroGiftCommandFactory extends AvailableCommandsFactoryForType {
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils.*
private def eligibleRecipients(
province: ProvinceT,
factions: Vector[FactionT]
): Vector[EligibleGift] =
province: Province,
gameState: GameState
): Seq[EligibleGift] =
province.rulingFactionHeroIds
.filterNot(hid => FactionUtils.isFactionLeader(hid, factions))
.filterNot(hid => LegacyFactionUtils.isFactionLeader(hid, gameState))
.map(hid =>
EligibleGift(
recipientHeroId = hid,
@@ -30,20 +28,17 @@ object AvailableHeroGiftCommandFactory extends ScalaAvailableCommandsFactory {
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
val factions = gameState.factions.values.toVector
HeroUtils
.consideredProvinces(gameState, factionId, provinceId)
): Option[HeroGiftAvailableCommand] =
consideredProvinces(gameState, factionId, provinceId)
.filterNot(_.gold == 0)
.flatMap(p => eligibleRecipients(p, factions)) match {
.flatMap(p => eligibleRecipients(p, gameState)) match {
case items if items.isEmpty => None
case eg =>
Some(
HeroGiftAvailable(
HeroGiftAvailableCommand(
eligibleGifts = eg,
actingProvinceId = provinceId
)
)
}
}
}
@@ -1,49 +1,30 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.ImproveAvailableCommand
import net.eagle0.eagle.common.improvement_type.ImprovementType.{AGRICULTURE, DEVASTATION, ECONOMY, INFRASTRUCTURE}
import net.eagle0.eagle.common.profession.Profession.ENGINEER
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.library.settings.MinVigorForImprove
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.ImproveAvailable
import net.eagle0.eagle.model.state.command.common.ImprovementType
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.{HeroT, Profession}
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.ImprovementType as StateImprovementType
object AvailableImproveCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableImproveCommandsFactory extends AvailableCommandsFactoryForType {
val IMPROVEMENT_MAX = 100.0
private val heroSorting: HeroT => (Double, Boolean, Double) = { (hero: HeroT) =>
private val heroSorting: Hero => (Double, Boolean, Double) = { (hero: Hero) =>
(
hero.vigor / hero.constitution.toDouble,
hero.profession == Profession.Engineer,
hero.profession == ENGINEER,
hero.strength + hero.agility + hero.constitution
)
}
private def availableTypes(province: ProvinceT): Vector[ImprovementType] =
(Option.when(province.economy < IMPROVEMENT_MAX)(ImprovementType.Economy) ++
Option.when(province.agriculture < IMPROVEMENT_MAX)(ImprovementType.Agriculture) ++
Option.when(province.infrastructure < IMPROVEMENT_MAX)(ImprovementType.Infrastructure) ++
Option.when(
province.economyDevastation > 0 || province.agricultureDevastation > 0 || province.infrastructureDevastation > 0
)(ImprovementType.Devastation)).toVector
private def convertImprovementType(
stateType: StateImprovementType
): ImprovementType = stateType match {
case StateImprovementType.Agriculture => ImprovementType.Agriculture
case StateImprovementType.Economy => ImprovementType.Economy
case StateImprovementType.Infrastructure => ImprovementType.Infrastructure
case StateImprovementType.Devastation => ImprovementType.Devastation
}
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[ImproveAvailableCommand] = {
val province = gameState.provinces(provinceId)
internalRequire(
@@ -52,18 +33,26 @@ object AvailableImproveCommandsFactory extends ScalaAvailableCommandsFactory {
)
val availableHeroes = province.rulingFactionHeroIds
.flatMap(hid => gameState.heroes.get(hid))
.map(gameState.heroes(_))
.filterNot(_.vigor < MinVigorForImprove.doubleValue)
val types = availableTypes(province)
val availableTypes =
Option.when(province.economy < IMPROVEMENT_MAX)(ECONOMY) ++
Option.when(province.agriculture < IMPROVEMENT_MAX)(AGRICULTURE) ++
Option.when(province.infrastructure < IMPROVEMENT_MAX) {
INFRASTRUCTURE
} ++
Option.when(
province.economyDevastation > 0 || province.agricultureDevastation > 0 || province.infrastructureDevastation > 0
)(DEVASTATION)
Option.when(availableHeroes.nonEmpty && types.nonEmpty) {
ImproveAvailable(
Option.when(availableHeroes.nonEmpty && availableTypes.nonEmpty) {
ImproveAvailableCommand(
availableHeroIds = availableHeroes.map(_.id),
availableTypes = types,
availableTypes = availableTypes.toVector,
actingProvinceId = provinceId,
recommendedHeroId = availableHeroes.maxBy(heroSorting).id,
lockedType = province.lockedImprovementType.map(convertImprovementType)
lockedType = province.lockedImprovementType
)
}
}
@@ -1,47 +1,34 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{IssueOrdersAvailable, ProvinceOrders}
import net.eagle0.eagle.model.state.command.common.ProvinceOrderType.{Develop, Entrust, Expand, Mobilize}
import net.eagle0.eagle.model.state.command.common.ProvinceOrderType as CommandProvinceOrderType
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceOrderType as StateProvinceOrderType
import net.eagle0.eagle.api.available_command.IssueOrdersAvailableCommand
import net.eagle0.eagle.api.command.util.province_orders.ProvinceOrders
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType.*
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
object AvailableIssueOrdersCommandFactory extends ScalaAvailableCommandsFactory {
private def toCommandOrderType(stateOrderType: StateProvinceOrderType): CommandProvinceOrderType =
stateOrderType match {
case StateProvinceOrderType.Develop => Develop
case StateProvinceOrderType.Mobilize => Mobilize
case StateProvinceOrderType.Expand => Expand
case StateProvinceOrderType.Entrust => Entrust
case StateProvinceOrderType.UnknownOrderType => Develop // Default fallback
}
object AvailableIssueOrdersCommandFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
val province = gameState.provinces(provinceId)
val faction = gameState.factions(factionId)
val provinces = gameState.provinces.values.toVector
): Option[IssueOrdersAvailableCommand] =
Option.when(
province.rulingHeroId.exists(faction.leaderIds.contains) &&
FactionUtils.provinceCount(factionId, provinces) > 1
gameState
.provinces(provinceId)
.rulingHeroId
.exists(gameState.factions(factionId).leaders.contains) &&
LegacyFactionUtils.provinceCount(factionId, gameState) > 1
) {
IssueOrdersAvailable(
IssueOrdersAvailableCommand(
actingProvinceId = provinceId,
currentOrders = provinces
currentOrders = gameState.provinces.values
.filter(_.rulingFactionId.contains(factionId))
.toVector
.sortBy(_.name)
.map(p => ProvinceOrders(provinceId = p.id, orderType = toCommandOrderType(p.provinceOrders))),
availableOrders = Vector(Develop, Mobilize, Expand, Entrust),
availableFocusProvinces = Vector()
.map(p => ProvinceOrders(provinceId = p.id, orders = p.provinceOrders)),
availableOrders = Vector(DEVELOP, MOBILIZE, EXPAND, ENTRUST)
)
}
}
}
@@ -1,18 +1,20 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.common.MoreOption
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{
ExpandedUnaffiliatedHero,
ManagePrisonersAvailable,
PrisonerToManage
import net.eagle0.eagle.api.available_command.{ManagePrisonersAvailableCommand, PrisonerToManage}
import net.eagle0.eagle.api.command.util.prisoner_management_type.{
PrisonerManagementOption,
PrisonerManagementOptionExecute,
PrisonerManagementOptionExile,
PrisonerManagementOptionMove,
PrisonerManagementOptionRelease,
PrisonerManagementOptionReturn
}
import net.eagle0.eagle.model.state.command.common.PrisonerManagementOption
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.unaffiliated_hero.{UnaffiliatedHeroT, UnaffiliatedHeroType}
import net.eagle0.eagle.common.unaffiliated_hero_type.UnaffiliatedHeroType.UNAFFILIATED_HERO_PRISONER
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.unaffiliated_hero.UnaffiliatedHero
object AvailableManagePrisonersCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableManagePrisonersCommandFactory extends AvailableCommandsFactoryForType {
private def availableMoveToProvinceIds(
fid: FactionId,
@@ -27,34 +29,34 @@ object AvailableManagePrisonersCommandFactory extends ScalaAvailableCommandsFact
.toVector
private def optionsForPrisoner(
prisoner: UnaffiliatedHeroT,
prisoner: UnaffiliatedHero,
moveToPids: Vector[ProvinceId],
gameState: GameState
): Vector[PrisonerManagementOption] = {
val factionWithThisPrisonerAsLeader = gameState.factions.values
.find(_.leaderIds.contains(prisoner.heroId))
.find(_.leaders.contains(prisoner.heroId))
if factionWithThisPrisonerAsLeader.isDefined then {
Vector(
PrisonerManagementOption.Execute
PrisonerManagementOptionExecute()
) ++ moveToPids
.map(pid => PrisonerManagementOption.Move(toProvinceId = pid))
.map(pid => PrisonerManagementOptionMove(toProvinceId = pid))
.toVector ++ Option
.when(
gameState.provinces.values.exists(
_.rulingFactionId.contains(factionWithThisPrisonerAsLeader.get.id)
)
)(
PrisonerManagementOption.Return(toFactionId = factionWithThisPrisonerAsLeader.get.id)
PrisonerManagementOptionReturn(toFactionId = factionWithThisPrisonerAsLeader.get.id)
)
.toVector
} else {
Vector(
PrisonerManagementOption.Execute,
PrisonerManagementOption.Exile,
PrisonerManagementOption.Release
PrisonerManagementOptionExecute(),
PrisonerManagementOptionExile(),
PrisonerManagementOptionRelease()
) ++ moveToPids
.map(pid => PrisonerManagementOption.Move(toProvinceId = pid))
.map(pid => PrisonerManagementOptionMove(toProvinceId = pid))
.toVector
}
}
@@ -63,37 +65,40 @@ object AvailableManagePrisonersCommandFactory extends ScalaAvailableCommandsFact
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[ManagePrisonersAvailableCommand] =
MoreOption.flatWhen(
gameState.provinces(provinceId).rulerIsTraveling
) {
val prisoners = gameState
)(
gameState
.provinces(provinceId)
.unaffiliatedHeroes
.filter(_.unaffiliatedHeroType == UnaffiliatedHeroType.Prisoner)
Option.when(prisoners.nonEmpty) {
ManagePrisonersAvailable(
actingProvinceId = provinceId,
prisoners = prisoners.map { prisoner =>
PrisonerToManage(
prisoner = ExpandedUnaffiliatedHero(
heroId = prisoner.heroId,
heroName = gameState.heroes(prisoner.heroId).nameTextId,
goldCost = 0
),
availableOptions = optionsForPrisoner(
prisoner,
moveToPids = availableMoveToProvinceIds(
factionId,
provinceId,
gameState
),
gameState
)
.filter(_.`type` == UNAFFILIATED_HERO_PRISONER) match {
case items if items.isEmpty => None
case prisoners: Seq[UnaffiliatedHero] =>
Some(
ManagePrisonersAvailableCommand(
actingProvinceId = provinceId,
prisoners = prisoners.map { prisoner =>
PrisonerToManage(
prisoner = Some(
ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(
gs = gameState,
uh = prisoner
)
),
availableOptions = optionsForPrisoner(
prisoner,
moveToPids = availableMoveToProvinceIds(
factionId,
provinceId,
gameState
),
gameState
)
)
}.toVector
)
}
)
)
}
}
)
}
@@ -1,35 +1,45 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.{
AvailableDestinationProvince,
MarchAvailableCommand,
MarchCommandFromOneProvince,
RecommendedBattleConfig
}
import net.eagle0.eagle.api.command.util.battalion_with_food_cost.BattalionWithFoodCost
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.{MaxCombatUnitCountPerSide, MinVigorForMarch}
import net.eagle0.eagle.library.util.{BattalionUtils, ShardokMapInfo}
import net.eagle0.eagle.library.util.{LegacyBattalionUtils, ShardokMapInfo}
import net.eagle0.eagle.library.util.battalion_suitability.BattalionSuitability
import net.eagle0.eagle.library.util.command_choice_helpers.{CombatUnitSelector, MarchSuppliesHelpers}
import net.eagle0.eagle.library.util.hero.HeroUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.*
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.library.util.hero.LegacyHeroUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.model.proto_converters.hero.HeroConverter
import net.eagle0.eagle.model.proto_converters.BattalionConverter
import net.eagle0.eagle.model.proto_converters.BattalionTypeConverter
import net.eagle0.eagle.model.proto_converters.SuitableBattalionsConverter
object AvailableMarchCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableMarchCommandFactory extends AvailableCommandsFactoryForType {
private def oneOriginProvinceCommand(
gameState: GameState,
originProvince: ProvinceT
originProvince: Province
): Option[MarchCommandFromOneProvince] = {
val availableDestinations = originProvince.neighbors
.map(_.provinceId)
.flatMap(gameState.provinces.get)
.filterNot(ProvinceUtils.hasBlizzard)
.filterNot(LegacyProvinceUtils.hasBlizzard)
.map { destinationProvince =>
val startingPositionIndex = destinationProvince.neighbors
.find(n => n.provinceId == originProvince.id)
.get
.startingPositionIndex
val mapInfo = ShardokMapInfo.shardokMapInfos(destinationProvince.id)
val requiresRiverCrossing = mapInfo.waterCrossingRequirementsByPosition(
val mapInfo = ShardokMapInfo.shardokMapInfos(destinationProvince.id)
val destinationCastleCount = mapInfo.castleCount
val requiresRiverCrossing = mapInfo.waterCrossingRequirementsByPosition(
startingPositionIndex
) >= 10
@@ -38,19 +48,20 @@ object AvailableMarchCommandFactory extends ScalaAvailableCommandsFactory {
requiresRiverCrossing = requiresRiverCrossing
)
}
.toVector
val availableHeroes = originProvince.rulingFactionHeroIds
.map(heroId => gameState.heroes(heroId))
.filter(_.vigor >= MinVigorForMarch.doubleValue)
if ProvinceUtils.hasBlizzard(originProvince) then return None
if LegacyProvinceUtils.hasBlizzard(originProvince) then return None
if availableDestinations.isEmpty then return None
if availableHeroes.isEmpty then return None
val availableBattalions = originProvince.battalionIds
.map(battId => gameState.battalions(battId))
.sortBy(_.typeId.value)
.sortBy(_.`type`.index)
val recommendedSendCount =
availableHeroes.size
@@ -61,14 +72,14 @@ object AvailableMarchCommandFactory extends ScalaAvailableCommandsFactory {
Option.when(recommendedSendCount > 0) {
val units = CombatUnitSelector.selectedCombatBattalions(
desiredCount = recommendedSendCount,
heroes = availableHeroes,
battalions = availableBattalions,
battalionTypes = gameState.battalionTypes
heroes = availableHeroes.map(HeroConverter.fromProto).toVector,
battalions = availableBattalions.map(BattalionConverter.fromProto).toVector,
battalionTypes = gameState.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
)
RecommendedBattleConfig(
units = units.map(cu => RecommendedCombatUnit(heroId = cu.heroId, battalionId = cu.battalionId)),
units = units,
food = MarchSuppliesHelpers.foodToTakeOnAttack(
units.flatMap(_.battalionId).map(gameState.battalions),
units.flatMap(_.battalionId).map(gameState.battalions).map(BattalionConverter.fromProto),
originProvince.food
),
gold = MarchSuppliesHelpers.goldToTakeOnAttack(
@@ -78,33 +89,27 @@ object AvailableMarchCommandFactory extends ScalaAvailableCommandsFactory {
)
}
val suitableBattalionsMap = BattalionSuitability
.suitableBattalionsForHeroes(
hs = availableHeroes,
bs = availableBattalions,
bts = gameState.battalionTypes
)
.map {
case (heroId, suitableBattalions) =>
heroId -> SuitableBattalions(
battalionIds = suitableBattalions.battalionIdsWithSuitability.map(_.battalionId)
)
}
Some(
MarchCommandFromOneProvince(
availableHeroIds = availableHeroes.map(_.id),
availableHeroIds = availableHeroes.map(_.id).toVector,
availableBattalions = availableBattalions.map { batt =>
BattalionWithFoodCost(
battalionId = batt.id,
foodCost = BattalionUtils.monthlyConsumedFood(batt, gameState.battalionTypes).toInt,
troopCount = batt.size
monthlyFood = LegacyBattalionUtils.monthlyConsumedFood(
battalion = batt,
bts = gameState.battalionTypes.toVector
)
)
},
}.toVector,
availableDestinationProvinces = availableDestinations,
suitableBattalionsForHeroes = suitableBattalionsMap,
suitableBattalionsForHeroes = SuitableBattalionsConverter.toProtoMap(
BattalionSuitability.suitableBattalionsForHeroes(
hs = availableHeroes.map(HeroConverter.fromProto).toVector,
bs = availableBattalions.map(BattalionConverter.fromProto).toVector,
bts = gameState.battalionTypes.map(BattalionTypeConverter.fromProto).toVector
)
),
originProvinceId = originProvince.id,
goldCost = 0, // March doesn't cost gold
goldAvailable = originProvince.gold,
foodAvailable = originProvince.food,
recommendedBattleConfig = recommendedConfig
@@ -116,22 +121,17 @@ object AvailableMarchCommandFactory extends ScalaAvailableCommandsFactory {
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
HeroUtils
): Option[MarchAvailableCommand] =
LegacyHeroUtils
.consideredProvinces(gameState, factionId, provinceId)
.flatMap(p => oneOriginProvinceCommand(gameState = gameState, originProvince = p)) match {
case items if items.isEmpty => None
case opcs =>
Some(
AvailableCommand.MarchAvailable(
MarchAvailableCommand(
actingProvinceId = provinceId,
oneProvinceCommands = opcs,
availableTypes = gameState.battalionTypes.map { bt =>
BattalionType(
typeId = bt.typeId.value,
name = bt.name
)
}
availableTypes = gameState.battalionTypes
)
)
}
@@ -1,45 +1,39 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{
BattalionTypeStatus,
OrganizeTroopsAvailable,
TroopCost
}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.api.available_command.{OrganizeTroopsAvailableCommand, TroopCost}
import net.eagle0.eagle.api.available_command.OrganizeTroopsAvailableCommand.BattalionTypeStatus
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableOrganizeTroopsCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableOrganizeTroopsCommandsFactory extends AvailableCommandsFactoryForType {
val maxBattalions = 10 // FIXME
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[OrganizeTroopsAvailableCommand] = {
val province = gameState.provinces(provinceId)
val availableTypeIds = gameState.battalionTypes
.filter(bt =>
ProvinceUtils.effectiveAgriculture(province) >= bt.minimumAgriculture &&
ProvinceUtils.effectiveEconomy(province) >= bt.minimumEconomy
)
.map(_.typeId)
val availableTypeStatuses = gameState.battalionTypes.map { bt =>
val availableTypeStatuses = gameState.battalionTypes.toVector.map { bt =>
BattalionTypeStatus(
typeId = bt.typeId.value,
typeId = bt.typeId,
minimumAgriculture = bt.minimumAgriculture,
minimumEconomy = bt.minimumEconomy,
meetsRequirements = availableTypeIds.contains(bt.typeId)
meetsRequirements = LegacyProvinceUtils
.availableBattalionTypeIds(
province,
gameState.battalionTypes.toVector
)
.contains(bt.typeId)
)
}
.sortBy(_.typeId)
.sortBy(_.typeId.value)
val troopCosts = gameState.battalionTypes.map(t =>
val troopCosts = gameState.battalionTypes.toVector.map(t =>
TroopCost(
battalionTypeId = t.typeId.value,
`type` = t.typeId,
costPerTroop = t.baseCost * province.priceIndex
)
)
@@ -50,14 +44,17 @@ object AvailableOrganizeTroopsCommandsFactory extends ScalaAvailableCommandsFact
val existingEligibleBattalions = province.battalionIds
.map(bid => gameState.battalions(bid))
.map(_.id)
.toVector
Option.when(
existingEligibleBattalions.nonEmpty || province.battalionIds.length < maxBattalions
) {
OrganizeTroopsAvailable(
OrganizeTroopsAvailableCommand(
actingProvinceId = provinceId,
// FIXME: adjust for province price index
troopCosts = troopCosts,
existingBattalions = existingEligibleBattalions,
// FIXME: max battalions
maxBattalions = maxBattalions,
availableBattalionTypes = availableTypeStatuses
)
@@ -1,43 +1,39 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.ReconAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.MinVigorForRecon
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.ReconAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.Profession
import net.eagle0.eagle.model.state.province.IncomingRecon
object AvailableReconCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableReconCommandFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[ReconAvailableCommand] = {
val province = gameState.provinces(provinceId)
if province.rulingFactionHeroIds.size < 2 then None
else {
val availableHeroes = province.rulingFactionHeroIds
.map(heroId => gameState.heroes(heroId))
.filter(_.profession == Profession.Ranger)
.filter(_.profession.isRanger)
.filter(_.vigor >= MinVigorForRecon.doubleValue)
.map(_.id)
val availableTargets = gameState.provinces.values
.filterNot(_.rulingFactionId.contains(factionId))
.filterNot(p =>
p.incomingEndTurnActions.exists(eta =>
eta.fromFactionId == factionId && eta.details.isInstanceOf[IncomingRecon]
)
.filterNot(
_.incomingEndTurnActions
.exists(eta => eta.action.isRecon && eta.fromFactionId == factionId)
)
.toVector
.sortBy(_.name)
.map(_.id)
Option.when(availableHeroes.nonEmpty && availableTargets.nonEmpty) {
ReconAvailable(
availableHeroIds = availableHeroes.toVector,
ReconAvailableCommand(
availableHeroIds = availableHeroes,
availableTargetProvinces = availableTargets,
actingProvinceId = provinceId
)
@@ -1,52 +1,45 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.RecruitHeroesAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.MinOddsForRecruitment
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.recruitment_odds.RecruitmentOdds
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{
ExpandedUnaffiliatedHero,
RecruitHeroesAvailable
}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
import net.eagle0.eagle.library.util.recruitment_odds.LegacyRecruitmentOdds
object AvailableRecruitHeroesCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableRecruitHeroesCommandFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[RecruitHeroesAvailableCommand] = {
val province = gameState.provinces(provinceId)
if !province.rulerIsTraveling then return None
val faction = gameState.factions(factionId)
if !ProvinceUtils.ruledByFactionLeader(province, gameState.factions.values.toVector) then return None
if !LegacyProvinceUtils.ruledByFactionLeader(province, gameState) then return None
val expandedRecruitable = province.unaffiliatedHeroes
.filterNot(uh => FactionUtils.isFactionLeader(uh.heroId, gameState.factions.values.toVector))
.filterNot(uh => LegacyFactionUtils.isFactionLeader(uh.heroId, gameState))
.filterNot(h =>
RecruitmentOdds.scoreForUnaffiliatedHero(
LegacyRecruitmentOdds.scoreForUnaffiliatedHero(
faction = faction,
factionLeader = gameState.heroes.get(faction.factionHeadId),
prestige = FactionUtils.prestige(faction, gameState.provinces.values.toVector),
prestige = LegacyFactionUtils.prestige(factionId, gameState),
targetHero = gameState.heroes(h.heroId),
targetUH = h
) < MinOddsForRecruitment.doubleValue
)
.map(uh =>
ExpandedUnaffiliatedHero(
heroId = uh.heroId,
heroName = gameState.heroes(uh.heroId).nameTextId,
goldCost = 0 // Recruitment is free
)
ExpandedUnaffiliatedHeroUtils
.expandedUnaffiliatedHero(gs = gameState, uh = uh)
)
Option.when(expandedRecruitable.nonEmpty) {
RecruitHeroesAvailable(
RecruitHeroesAvailableCommand(
availableHeroes = expandedRecruitable,
actingProvinceId = provinceId
)
@@ -1,54 +1,39 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{BattalionId, FactionId, ProvinceId}
import net.eagle0.eagle.model.state.{HostileArmyGroup, MovingArmy}
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{ResolveTributeAvailable, TributeAndFaction}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.HostileArmyGroupStatus.TributeDemanded
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.{ResolveTributeAvailableCommand, TributeAndFaction}
import net.eagle0.eagle.internal.army.{HostileArmyGroup, HostileArmyGroupStatus}
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.util.ArmyUtils
object AvailableResolveTributeCommandsFactory extends ScalaAvailableCommandsFactory {
private def heroCount(ag: HostileArmyGroup): Int =
ag.armies.map(ma => ma.army.units.size).sum
private def troopCount(ag: HostileArmyGroup, battalions: Map[BattalionId, BattalionT]): Int =
ag.armies
.flatMap(_.army.units)
.flatMap(_.battalionId)
.flatMap(battalions.get)
.map(_.size)
.sum
object AvailableResolveTributeCommandsFactory extends AvailableCommandsFactoryForType {
private def makeResolveTributeCommand(
province: ProvinceT,
province: Province,
gs: GameState
): Option[AvailableCommand] = {
): Option[ResolveTributeAvailableCommand] = {
val incomingTributeDemands =
province.hostileArmies.filter(ag =>
ag.status match {
case _: TributeDemanded => true
case _ => false
}
province.hostileArmies.filter(
_.status.asMessage.sealedValue.isTributeDemanded
)
Option.when(incomingTributeDemands.nonEmpty) {
ResolveTributeAvailable(
ResolveTributeAvailableCommand(
actingProvinceId = province.id,
demands = incomingTributeDemands.map { ag =>
val tributeAmount = ag.status match {
case TributeDemanded(amount) => amount
case _ => throw new IllegalStateException("Expected TributeDemanded status")
}
TributeAndFaction(
demandingFactionId = ag.factionId,
goldDemanded = tributeAmount.gold,
foodDemanded = tributeAmount.food,
heroCount = heroCount(ag),
troopCount = troopCount(ag, gs.battalions)
)
demands = incomingTributeDemands.map {
case ag @ HostileArmyGroup(
fid: FactionId,
_,
status: HostileArmyGroupStatus,
_ /* unknownFieldSet */
) =>
TributeAndFaction(
demandingFactionId = fid,
tributeDemanded = status.asMessage.getTributeDemanded.tributeAmount,
heroCount = ArmyUtils.heroCount(ag),
troopCount = ArmyUtils.troopCount(ag, gs)
)
},
availableGold = province.gold,
availableFood = province.food
@@ -60,6 +45,6 @@ object AvailableResolveTributeCommandsFactory extends ScalaAvailableCommandsFact
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[ResolveTributeAvailableCommand] =
makeResolveTributeCommand(gameState.provinces(provinceId), gameState)
}
@@ -1,18 +1,17 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.RestAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.api.available_command.RestAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
object AvailableRestCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableRestCommandsFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[RestAvailableCommand] =
Some(
RestAvailable(
RestAvailableCommand(
actingProvinceId = provinceId
)
)
@@ -1,16 +1,15 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.ReturnAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.api.available_command.ReturnAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
object AvailableReturnCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableReturnCommandsFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[ReturnAvailableCommand] = {
val province = gameState.provinces(provinceId)
require(
@@ -19,7 +18,7 @@ object AvailableReturnCommandsFactory extends ScalaAvailableCommandsFactory {
)
Option.when(province.rulerIsTraveling)(
ReturnAvailable(actingProvinceId = provinceId)
ReturnAvailableCommand(actingProvinceId = provinceId)
)
}
}
@@ -1,41 +1,47 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.SendSuppliesAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.MinVigorForSendSupplies
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.SendSuppliesAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.library.util.faction_utils.LegacyFactionUtils
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableSendSuppliesCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableSendSuppliesCommandFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[SendSuppliesAvailableCommand] = {
val province = gameState.provinces(provinceId)
val availableHeroIds = province.rulingFactionHeroIds
.map(heroId => gameState.heroes(heroId))
.filter(_.vigor >= MinVigorForSendSupplies.doubleValue)
.filter(
_.vigor >= MinVigorForSendSupplies.doubleValue
)
.map(_.id)
val destinationProvinceIds = province.neighbors
.map(_.provinceId)
.filterNot(pid => ProvinceUtils.hasBlizzard(gameState.provinces(pid)))
.filterNot(pid =>
LegacyProvinceUtils.hasBlizzard(
gameState
.provinces(pid)
)
)
val hasSupplies = province.gold > 0 || province.food > 0
Option.when(
!ProvinceUtils.hasBlizzard(province) &&
hasSupplies &&
availableHeroIds.nonEmpty &&
destinationProvinceIds.nonEmpty &&
FactionUtils.provinceCount(factionId, gameState.provinces.values) > 1
!LegacyProvinceUtils
.hasBlizzard(
province
) && hasSupplies && availableHeroIds.nonEmpty && destinationProvinceIds.nonEmpty && LegacyFactionUtils
.provinceCount(factionId, gameState) > 1
) {
SendSuppliesAvailable(
availableHeroIds = availableHeroIds.toVector,
SendSuppliesAvailableCommand(
availableHeroIds = availableHeroIds,
actingProvinceId = provinceId,
foodAvailable = province.food,
goldAvailable = province.gold,
@@ -1,37 +1,39 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.StartEpidemicAvailableCommand
import net.eagle0.eagle.api.available_command.StartEpidemicAvailableCommand.StartEpidemicOptions
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.MinVigorForStartEpidemic
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{StartEpidemicAvailable, StartEpidemicOptions}
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.Profession
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableStartEpidemicCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableStartEpidemicCommandFactory extends AvailableCommandsFactoryForType {
private def makeStartEpidemicCommand(
gameState: GameState,
province: ProvinceT
): Option[StartEpidemicAvailable] = {
province: Province
): Option[StartEpidemicAvailableCommand] = {
val necromancersWithSufficientVigor = province.rulingFactionHeroIds
.flatMap(gameState.heroes.get)
.filter(_.profession == Profession.Necromancer)
.map(gameState.heroes)
.filter(_.profession.isNecromancer)
.filter(_.vigor >= MinVigorForStartEpidemic.doubleValue)
val pidPlusNeighborPids = province.id +: province.neighbors.map(_.provinceId)
val pidPlusNeighborPids = province.id +: province.neighbors.map(
_.provinceId
)
val targetProvinceIds = pidPlusNeighborPids.filterNot { pid =>
gameState.provinces.get(pid).exists { p =>
p.rulingFactionId == province.rulingFactionId && ProvinceUtils.hasEpidemic(p)
}
}
val targetProvinceIds = pidPlusNeighborPids.filterNot(pid =>
gameState
.provinces(pid)
.rulingFactionId == province.rulingFactionId && LegacyProvinceUtils
.hasEpidemic(gameState.provinces(pid))
)
Option.when(
necromancersWithSufficientVigor.nonEmpty && targetProvinceIds.nonEmpty
) {
StartEpidemicAvailable(
StartEpidemicAvailableCommand(
availableHeroIds = necromancersWithSufficientVigor
.sortBy(-_.vigor)
.map(_.id),
@@ -45,7 +47,7 @@ object AvailableStartEpidemicCommandFactory extends ScalaAvailableCommandsFactor
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[StartEpidemicAvailableCommand] =
makeStartEpidemicCommand(
gameState,
gameState.provinces(provinceId)
@@ -1,26 +1,25 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.SuppressBeastsAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.province.Province
import net.eagle0.eagle.library.settings.MinVigorForSuppressBeasts
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.SuppressBeastsAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableSuppressBeastsCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableSuppressBeastsCommandFactory extends AvailableCommandsFactoryForType {
private def makeSuppressBeastsCommand(
gameState: GameState,
province: ProvinceT
): Option[SuppressBeastsAvailable] = {
province: Province
): Option[SuppressBeastsAvailableCommand] = {
val availableHeroIds = province.rulingFactionHeroIds
.filter(hid => gameState.heroes(hid).vigor >= MinVigorForSuppressBeasts.doubleValue)
Option.when(
ProvinceUtils.hasBeasts(province) && availableHeroIds.nonEmpty
LegacyProvinceUtils.hasBeasts(province) && availableHeroIds.nonEmpty
) {
SuppressBeastsAvailable(
SuppressBeastsAvailableCommand(
availableHeroIds = availableHeroIds,
availableBattalionIds = province.battalionIds,
actingProvinceId = province.id
@@ -32,7 +31,7 @@ object AvailableSuppressBeastsCommandFactory extends ScalaAvailableCommandsFacto
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[SuppressBeastsAvailableCommand] =
makeSuppressBeastsCommand(
gameState,
gameState.provinces(provinceId)
@@ -1,17 +1,17 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.available_command.SwearBrotherhoodAvailableCommand
import net.eagle0.eagle.api.available_command.SwearBrotherhoodAvailableCommand.HeroAndBackstory
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.{MaximumFactionLeaders, MinimumLoyaltyForSwearBrotherhood}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.{HeroAndBackstory, SwearBrotherhoodAvailable}
import net.eagle0.eagle.model.state.game_state.GameState
object AvailableSwearBrotherhoodCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableSwearBrotherhoodCommandFactory extends AvailableCommandsFactoryForType {
def canHaveMoreLeaders(gameState: GameState, factionId: FactionId): Boolean =
gameState
.factions(factionId)
.leaderIds
.leaders
.length < MaximumFactionLeaders.intValue
def containsFactionLeader(
@@ -32,9 +32,9 @@ object AvailableSwearBrotherhoodCommandFactory extends ScalaAvailableCommandsFac
gameState
.provinces(provinceId)
.rulingFactionHeroIds
.flatMap(gameState.heroes.get)
.map(gameState.heroes)
.filter(_.loyalty >= MinimumLoyaltyForSwearBrotherhood.doubleValue)
.filterNot(h => gameState.factions(factionId).leaderIds.contains(h.id))
.filterNot(h => gameState.factions(factionId).leaders.contains(h.id))
.map(_.id)
.toVector
@@ -42,7 +42,7 @@ object AvailableSwearBrotherhoodCommandFactory extends ScalaAvailableCommandsFac
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] =
): Option[SwearBrotherhoodAvailableCommand] =
if canHaveMoreLeaders(gameState, factionId) && containsFactionLeader(
gameState,
factionId,
@@ -53,7 +53,7 @@ object AvailableSwearBrotherhoodCommandFactory extends ScalaAvailableCommandsFac
case items if items.isEmpty => None
case hs =>
Some(
SwearBrotherhoodAvailable(
SwearBrotherhoodAvailableCommand(
availableHeroes = hs.map { hid =>
HeroAndBackstory(
heroId = hid,
@@ -1,24 +1,23 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.TradeAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.{BaseFoodBuyPrice, BaseFoodSellPrice}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.TradeAvailable
import net.eagle0.eagle.model.state.game_state.GameState
object AvailableTradeCommandFactory extends ScalaAvailableCommandsFactory {
object AvailableTradeCommandFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[TradeAvailableCommand] = {
val province = gameState.provinces(provinceId)
Option.when(
province.rulerIsTraveling && (province.gold > 0 || province.food > 0)
) {
TradeAvailable(
TradeAvailableCommand(
foodAvailable = province.food,
goldAvailable = province.gold,
foodSellPrice = BaseFoodSellPrice.doubleValue * province.priceIndex,
@@ -1,19 +1,19 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.TrainAvailableCommand
import net.eagle0.eagle.common.profession.Profession.CHAMPION
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.internal.hero.Hero
import net.eagle0.eagle.library.settings.MinVigorForTrain
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.TrainAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.{HeroT, Profession}
object AvailableTrainCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableTrainCommandsFactory extends AvailableCommandsFactoryForType {
private val TRAINING_MAX = 100.0
private val heroSorting: HeroT => (Double, Boolean, Double) = { (hero: HeroT) =>
private val heroSorting: Hero => (Double, Boolean, Double) = { (hero: Hero) =>
(
hero.vigor / hero.constitution.toDouble,
hero.profession == Profession.Champion,
hero.profession == CHAMPION,
hero.strength + hero.charisma + hero.constitution
)
}
@@ -22,7 +22,7 @@ object AvailableTrainCommandsFactory extends ScalaAvailableCommandsFactory {
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[TrainAvailableCommand] = {
val province = gameState.provinces(provinceId)
val availableHeroes = province.rulingFactionHeroIds
.map(heroId => gameState.heroes(heroId))
@@ -33,8 +33,8 @@ object AvailableTrainCommandsFactory extends ScalaAvailableCommandsFactory {
.exists(_.training < TRAINING_MAX)
Option.when(availableHeroes.nonEmpty && hasTrainableBattalion) {
TrainAvailable(
availableHeroIds = availableHeroes.map(_.id).toVector,
TrainAvailableCommand(
availableHeroIds = availableHeroes.map(_.id),
actingProvinceId = provinceId,
recommendedHeroId = availableHeroes.maxBy(heroSorting).id
)
@@ -1,19 +1,18 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.TravelAvailableCommand
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.MinVigorForTravel
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.TravelAvailable
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.library.util.province.LegacyProvinceUtils
object AvailableTravelCommandsFactory extends ScalaAvailableCommandsFactory {
object AvailableTravelCommandsFactory extends AvailableCommandsFactoryForType {
override def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand] = {
): Option[TravelAvailableCommand] = {
val province = gameState.provinces(provinceId)
require(
@@ -22,12 +21,16 @@ object AvailableTravelCommandsFactory extends ScalaAvailableCommandsFactory {
)
Option.when(
!province.rulerIsTraveling && !ProvinceUtils.hasBlizzard(province) && province.rulingHeroId
.flatMap(gameState.heroes.get)
!province.rulerIsTraveling && !LegacyProvinceUtils
.hasBlizzard(province) && province.rulingHeroId
.map(gameState.heroes(_))
.map(_.vigor)
.exists(_ >= MinVigorForTravel.doubleValue)
) {
TravelAvailable(actingProvinceId = provinceId)
TravelAvailableCommand(
actingProvinceId = provinceId
)
}
}
}
@@ -1,57 +1,5 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "scala_available_commands_factory",
srcs = ["ScalaAvailableCommandsFactory.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
scala_library(
name = "unified_command_factory",
srcs = ["UnifiedCommandFactory.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
scala_library(
name = "factory_adapters",
srcs = ["FactoryAdapters.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
],
exports = [
":unified_command_factory",
],
deps = [
":available_commands_factory_for_type",
":scala_available_commands_factory",
":unified_command_factory",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:available_command_converter",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
scala_library(
name = "availability",
srcs = ["AvailableCommandsFactory.scala"],
@@ -105,22 +53,17 @@ scala_library(
":available_trade_command_factory",
":available_train_command_factory",
":available_travel_commands_factory",
":factory_adapters",
":pkg",
":unified_command_factory",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/common:more_option",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:available_command_type_map",
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:available_command_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -131,18 +74,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_apprehend_outlaw",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:hero_view_filter",
],
)
@@ -152,16 +91,13 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/battalion_type_finder",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -172,21 +108,20 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":expanded_combat_unit_utils",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:more_option",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//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/library/util/faction_utils:legacy_faction_utils",
],
)
@@ -209,17 +144,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [":scala_available_commands_factory"],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_control_weather",
"//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/hero",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -229,17 +161,15 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:more_option",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:expanded_unaffiliated_hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -249,22 +179,24 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [":scala_available_commands_factory"],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_defend",
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
"//src/main/scala/net/eagle0/eagle/library/util/battalion_suitability",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:combat_unit_selector",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//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/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:suitable_battalions_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
],
)
@@ -274,11 +206,11 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:alliance_gold_cost",
"//src/main/scala/net/eagle0/eagle/library/settings:break_alliance_gold_cost",
@@ -287,14 +219,9 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_diplomacy",
"//src/main/scala/net/eagle0/eagle/library/settings:truce_gold_cost",
"//src/main/scala/net/eagle0/eagle/library/util:province_distances",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
"//src/main/scala/net/eagle0/eagle/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/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -304,19 +231,17 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":expanded_unaffiliated_hero_utils",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:expanded_unaffiliated_hero_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:more_option",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:gold_cost_for_divine",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -326,18 +251,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//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/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -347,17 +268,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:feast_gold_cost_per_hero",
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
],
)
@@ -368,17 +286,17 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":expanded_combat_unit_utils",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:expanded_combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -388,17 +306,15 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/model/state:captured_hero",
"//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/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:hero_view_filter",
],
)
@@ -410,16 +326,17 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:available_commands_factory_for_type",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_crack_down",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -431,14 +348,16 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:available_commands_factory_for_type",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -450,16 +369,18 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:available_commands_factory_for_type",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:riot_max_food",
"//src/main/scala/net/eagle0/eagle/library/settings:riot_max_gold",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -469,19 +390,15 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:max_gift_gold",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//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/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
],
)
@@ -491,20 +408,15 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:prisoner_management_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_improve",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/state:improvement_type",
"//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/hero",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
)
@@ -514,17 +426,15 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":expanded_unaffiliated_hero_utils",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:prisoner_management_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:more_option",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//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/unaffiliated_hero",
],
)
@@ -534,24 +444,27 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_march",
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
"//src/main/scala/net/eagle0/eagle/library/util:legacy_battalion_utils",
"//src/main/scala/net/eagle0/eagle/library/util:shardok_map_info",
"//src/main/scala/net/eagle0/eagle/library/util/battalion_suitability",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:combat_unit_selector",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:march_supplies_helpers",
"//src/main/scala/net/eagle0/eagle/library/util/hero:hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:suitable_battalions_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
],
)
@@ -561,15 +474,13 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [":scala_available_commands_factory"],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//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/library/util/faction_utils:legacy_faction_utils",
],
)
@@ -579,13 +490,13 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [":scala_available_commands_factory"],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -616,17 +527,15 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:max_alms_food",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_alms",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
],
)
@@ -636,17 +545,13 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_recon",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
"//src/main/scala/net/eagle0/eagle/model/state/province:incoming_end_turn_action",
],
)
@@ -656,19 +561,19 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":expanded_unaffiliated_hero_utils",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:expanded_unaffiliated_hero_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_odds_for_recruitment",
"//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/recruitment_odds",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/hero:legacy_hero_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
"//src/main/scala/net/eagle0/eagle/library/util/recruitment_odds:legacy_recruitment_odds",
],
)
@@ -802,17 +707,13 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/library/util:army_utils",
],
)
@@ -822,14 +723,12 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -839,14 +738,12 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -856,17 +753,15 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_send_supplies",
"//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/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils:legacy_faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -876,18 +771,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_start_epidemic",
"//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/game_state",
"//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/library/util/province:legacy_province_utils",
],
)
@@ -897,17 +788,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_suppress_beasts",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -917,17 +805,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:maximum_faction_leaders",
"//src/main/scala/net/eagle0/eagle/library/settings:minimum_loyalty_for_swear_brotherhood",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero/backstory_version",
],
)
@@ -937,16 +822,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:base_food_buy_price",
"//src/main/scala/net/eagle0/eagle/library/settings:base_food_sell_price",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -956,17 +839,13 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_train",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/hero:profession",
],
)
@@ -976,16 +855,14 @@ scala_library(
visibility = [
"//src/test/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
],
exports = [
":scala_available_commands_factory",
],
deps = [
":scala_available_commands_factory",
":available_commands_factory_for_type",
":pkg",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:min_vigor_for_travel",
"//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/game_state",
"//src/main/scala/net/eagle0/eagle/library/util/province:legacy_province_utils",
],
)
@@ -1,31 +0,0 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.AvailableCommand as AvailableCommandProto
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.model.proto_converters.command.available.AvailableCommandConverter
import net.eagle0.eagle.model.state.game_state.GameState
/** Wraps a NEW Scala factory - converts Scala output to Proto. */
class ScalaFactoryAdapter(factory: ScalaAvailableCommandsFactory) extends UnifiedCommandFactory {
override def availableCommand(
scalaState: GameState,
protoState: GameStateProto,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommandProto] =
factory
.availableCommand(scalaState, factionId, provinceId)
.map(cmd => AvailableCommandConverter.toProto(cmd, protoState, factionId))
}
/** Wraps a LEGACY proto factory - passes proto GameState to it. */
class LegacyFactoryAdapter(factory: AvailableCommandsFactoryForType) extends UnifiedCommandFactory {
override def availableCommand(
scalaState: GameState,
protoState: GameStateProto,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommandProto] =
factory.availableCommand(protoState, factionId, provinceId)
}
@@ -1,19 +0,0 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.game_state.GameState
/**
* Trait for factories that produce Scala AvailableCommand from Scala GameState.
*
* New factories should implement this trait to avoid proto dependencies. The output is converted to proto in the
* orchestrator via ScalaFactoryAdapter.
*/
trait ScalaAvailableCommandsFactory {
def availableCommand(
gameState: GameState,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommand]
}
@@ -1,21 +0,0 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.AvailableCommand as AvailableCommandProto
import net.eagle0.eagle.internal.game_state.GameState as GameStateProto
import net.eagle0.eagle.model.state.game_state.GameState
/**
* Unified trait for command factories used by the orchestrator.
*
* Takes both Scala and Proto GameState to support both new Scala factories (which use Scala state) and legacy Proto
* factories (which use Proto state). Returns Proto AvailableCommand for backward compatibility.
*/
trait UnifiedCommandFactory {
def availableCommand(
scalaState: GameState,
protoState: GameStateProto,
factionId: FactionId,
provinceId: ProvinceId
): Option[AvailableCommandProto]
}
@@ -495,7 +495,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -2,7 +2,7 @@ package net.eagle0.eagle.library.actions.llm_prompt_generators
import net.eagle0.eagle.{FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.client_text.{ClientTextStore, TextGenerationResult}
import net.eagle0.eagle.common.battalion_type.BattalionTypeId as BattalionTypeIdProto
import net.eagle0.eagle.common.battalion_type.BattalionTypeId
import net.eagle0.eagle.common.unaffiliated_hero_quest.{
AllianceQuest,
AlmsAcrossRealmQuest,
@@ -32,7 +32,6 @@ import net.eagle0.eagle.library.actions.llm_prompt_generators.HeroDescriptionGen
descriptionWithoutFaction
}
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.proto_converters.BattalionTypeIdConverter
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.hero.HeroT
@@ -190,15 +189,14 @@ object QuestEndedGeneratorUtilities {
case UpgradeBattalionQuest(
provinceId: FactionId,
battalionTypeIdProto: BattalionTypeIdProto,
battalionTypeId: BattalionTypeId,
minimumArmament: Double,
minimumTraining: Double,
_ /* unknownFieldSet */
) =>
val battalionTypeId = BattalionTypeIdConverter.fromProto(battalionTypeIdProto)
val battType =
val battType =
gameState.battalionTypes.find(_.typeId == battalionTypeId).get
val targetProvince = gameState.provinces(provinceId)
val targetProvince = gameState.provinces(provinceId)
for {
unaffiliatedHeroName <- unaffiliatedHeroNameResult
} yield s"$unaffiliatedHeroName wanted to see a mighty ${battType.name}, with an armament of ${minimumArmament.ceil.toInt} and training of ${minimumTraining.ceil.toInt}, in ${targetProvince.name}."
@@ -157,7 +157,6 @@ scala_library(
visibility = [
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
],
exports = [
@@ -193,7 +192,6 @@ scala_library(
visibility = [
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
],
exports = [
@@ -1,864 +0,0 @@
package net.eagle0.eagle.model.proto_converters.command.available
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.available_command as proto
import net.eagle0.eagle.api.command.util.attack_decision_type.{
AdvanceDecision,
AttackDecisionType as AttackDecisionTypeProto,
DemandTributeDecision,
SafePassageDecision,
WithdrawDecision
}
import net.eagle0.eagle.api.command.util.captured_hero_option.CapturedHeroOption as CapturedHeroOptionProto
import net.eagle0.eagle.api.command.util.control_weather_type.ControlWeatherType as ControlWeatherTypeProto
import net.eagle0.eagle.api.command.util.diplomacy_option.{
AllianceOption,
BreakAllianceOption,
DiplomacyOption as DiplomacyOptionProto,
InvitationOption,
RansomOfferOption,
TruceOption
}
import net.eagle0.eagle.api.command.util.prisoner_management_type.{
PrisonerManagementOption as PrisonerManagementOptionProto,
PrisonerManagementOptionExecute,
PrisonerManagementOptionExile,
PrisonerManagementOptionMove,
PrisonerManagementOptionRelease,
PrisonerManagementOptionReturn
}
import net.eagle0.eagle.common.battalion_type.{
BattalionType as BattalionTypeProto,
BattalionTypeId as BattalionTypeIdProto
}
import net.eagle0.eagle.common.diplomacy_offer.{
HostageOfferedInExchange as HostageOfferedInExchangeProto,
PrisonerOfferedInExchange as PrisonerOfferedInExchangeProto,
PrisonerToBeRansomed as PrisonerToBeRansomedProto,
RansomOfferDetails as RansomOfferDetailsProto
}
import net.eagle0.eagle.common.diplomacy_offer.DiplomacyOffer as DiplomacyOfferProto
import net.eagle0.eagle.common.improvement_type.ImprovementType as ImprovementTypeProto
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType as ProvinceOrderTypeProto
import net.eagle0.eagle.common.tribute_amount.TributeAmount as TributeAmountProto
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.settings.{AllianceGoldCost, BreakAllianceGoldCost, InviteGoldCost, TruceGoldCost}
import net.eagle0.eagle.library.util.view_filters.{HeroViewFilter, LegacyBattalionViewFilter}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.*
import net.eagle0.eagle.model.state.command.common.*
/**
* Converts Scala AvailableCommand to proto AvailableCommand.
*
* Requires GameState and viewing FactionId as context to construct full view objects (like HeroView) from IDs stored in
* the Scala types.
*/
object AvailableCommandConverter {
def toProto(
command: AvailableCommand,
gameState: GameState,
viewingFactionId: FactionId
): proto.AvailableCommand = command match {
case AlmsAvailable(
foodAvailable: Int,
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
) =>
proto.AlmsAvailableCommand(
foodAvailable = foodAvailable,
actingProvinceId = actingProvinceId,
availableHeroIds = availableHeroIds
)
case ApprehendOutlawAvailable(
outlaws: Vector[ResidentOutlaw],
availableBattalionIds: Vector[BattalionId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
) =>
proto.ApprehendOutlawAvailableCommand(
outlaws = outlaws.map(o => residentOutlawToProto(o, gameState, viewingFactionId)),
availableBattalionIds = availableBattalionIds,
actingProvinceId = actingProvinceId,
availableHeroIds = availableHeroIds
)
case ArmTroopsAvailable(
armamentCosts: Vector[ArmamentCost],
availableBattalions: Vector[BattalionId],
maxArmament: Int,
actingProvinceId: ProvinceId,
availableBattalionTypeIds: Vector[Int]
) =>
proto.ArmTroopsAvailableCommand(
armamentCosts = armamentCosts.map(armamentCostToProto),
availableBattalions = availableBattalions,
maxArmament = maxArmament,
actingProvinceId = actingProvinceId,
availableBattalionTypeIds = availableBattalionTypeIds.map(intToBattalionTypeId)
)
case AttackDecisionAvailable(
armies: Vector[ArmyStats],
actingUnits: Vector[ExpandedCombatUnit],
availableDecisions: Vector[AttackDecisionType],
actingProvinceId: ProvinceId
) =>
proto.AttackDecisionAvailableCommand(
armies = armies.map(armyStatsToProto),
actingUnits = actingUnits.map(u => expandedCombatUnitToProto(u, gameState)),
availableDecisions = availableDecisions.map(attackDecisionTypeToProto),
actingProvinceId = actingProvinceId
)
case ControlWeatherAvailable(
options: Vector[TargetProvinceOptions],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
) =>
proto.ControlWeatherAvailableCommand(
options = options.map(targetProvinceOptionsToProto),
actingProvinceId = actingProvinceId,
availableHeroIds = availableHeroIds
)
case DeclineQuestAvailable(
actingProvinceId: ProvinceId,
declinableHeroes: Vector[ExpandedUnaffiliatedHero]
) =>
proto.DeclineQuestAvailableCommand(
actingProvinceId = actingProvinceId,
declinableHeroes = declinableHeroes.map(h => expandedUnaffiliatedHeroToProto(h, gameState, actingProvinceId))
)
case DefendAvailable(
suitableBattalionsForHeroes: Map[HeroId, SuitableBattalions],
availableFleeProvinceIds: Vector[ProvinceId],
availableBattalions: Vector[BattalionWithFoodCost],
availableHeroIds: Vector[HeroId],
recommendedUnits: Vector[RecommendedCombatUnit],
actingProvinceId: ProvinceId,
hostileFactionIds: Vector[FactionId],
hostileHeroCount: Int,
hostileTroopCount: Int
) =>
proto.DefendAvailableCommand(
suitableBattalionsForHeroes = suitableBattalionsForHeroesToProto(suitableBattalionsForHeroes),
availableFleeProvinceIds = availableFleeProvinceIds,
availableBattalions = availableBattalions.map(battalionWithFoodCostToProto),
availableHeroIds = availableHeroIds,
recommendedUnits = recommendedUnits.map(recommendedCombatUnitToProto),
actingProvinceId = actingProvinceId,
hostileFactionIds = hostileFactionIds,
hostileHeroCount = hostileHeroCount,
hostileTroopCount = hostileTroopCount
)
case DiplomacyAvailable(
options: Vector[DiplomacyOption],
availableHeroIds: Vector[HeroId],
actingProvinceId: ProvinceId,
recommendedHeroId: HeroId
) =>
proto.DiplomacyAvailableCommand(
options = options.flatMap(diplomacyOptionToProtos),
availableHeroIds = availableHeroIds,
actingProvinceId = actingProvinceId,
recommendedHeroId = recommendedHeroId
)
case DivineAvailable(
costPerHero: Int,
divinableHeroes: Vector[ExpandedUnaffiliatedHero],
unavailableHeroes: Vector[ExpandedUnaffiliatedHero],
actingProvinceId: ProvinceId
) =>
proto.DivineAvailableCommand(
costPerHero = costPerHero,
divinableHeroes = divinableHeroes.map(h => expandedUnaffiliatedHeroToProto(h, gameState, actingProvinceId)),
unavailableHeroes = unavailableHeroes.map(h => expandedUnaffiliatedHeroToProto(h, gameState, actingProvinceId)),
actingProvinceId = actingProvinceId
)
case ExileVassalAvailable(actingProvinceId: ProvinceId, exilableHeroIds: Vector[HeroId]) =>
proto.ExileVassalAvailableCommand(
actingProvinceId = actingProvinceId,
exilableHeroIds = exilableHeroIds
)
case FeastAvailable(goldCost: Int, actingProvinceId: ProvinceId) =>
proto.FeastAvailableCommand(
goldCost = goldCost,
actingProvinceId = actingProvinceId
)
case FreeForAllDecisionAvailable(
provinceId: ProvinceId,
armies: Vector[ArmyStats],
actingUnits: Vector[ExpandedCombatUnit],
availableDecisions: Vector[AttackDecisionType]
) =>
proto.FreeForAllDecisionAvailableCommand(
provinceId = provinceId,
armies = armies.map(armyStatsToProto),
actingUnits = actingUnits.map(u => expandedCombatUnitToProto(u, gameState)),
availableDecisions = availableDecisions.map(attackDecisionTypeToProto)
)
case HandleCapturedHeroAvailable(
availableHeroes: Vector[ExpandedCapturedHero],
actingProvinceId: ProvinceId
) =>
proto.HandleCapturedHeroAvailableCommand(
availableHeroes = availableHeroes.map(h => expandedCapturedHeroToProto(h, gameState, viewingFactionId)),
actingProvinceId = actingProvinceId
)
case HandleRiotCrackDownAvailable(
availableHeroIds: Vector[HeroId],
battalionIdsAvailable: Vector[BattalionId],
actingProvinceId: ProvinceId
) =>
proto.HandleRiotCrackDownAvailableCommand(
availableHeroIds = availableHeroIds,
battalionIdsAvailable = battalionIdsAvailable,
actingProvinceId = actingProvinceId
)
case HandleRiotDoNothingAvailable(actingProvinceId: ProvinceId) =>
proto.HandleRiotDoNothingAvailableCommand(
actingProvinceId = actingProvinceId
)
case HandleRiotGiveAvailable(
foodAvailable: Int,
goldAvailable: Int,
actingProvinceId: ProvinceId
) =>
proto.HandleRiotGiveAvailableCommand(
foodAvailable = foodAvailable,
goldAvailable = goldAvailable,
actingProvinceId = actingProvinceId
)
case HeroGiftAvailable(eligibleGifts: Vector[EligibleGift], actingProvinceId: ProvinceId) =>
proto.HeroGiftAvailableCommand(
eligibleGifts = eligibleGifts.map(eligibleGiftToProto),
actingProvinceId = actingProvinceId
)
case ImproveAvailable(
availableTypes: Vector[ImprovementType],
availableHeroIds: Vector[HeroId],
recommendedHeroId: HeroId,
actingProvinceId: ProvinceId,
lockedType: Option[ImprovementType]
) =>
proto.ImproveAvailableCommand(
availableTypes = availableTypes.map(improvementTypeToProto),
availableHeroIds = availableHeroIds,
recommendedHeroId = recommendedHeroId,
actingProvinceId = actingProvinceId,
lockedType = lockedType.map(improvementTypeToProto).getOrElse(ImprovementTypeProto.NONE)
)
case IssueOrdersAvailable(
currentOrders: Vector[ProvinceOrders],
availableOrders: Vector[ProvinceOrderType],
availableFocusProvinces: Vector[ProvinceId],
actingProvinceId: ProvinceId
) =>
proto.IssueOrdersAvailableCommand(
currentOrders = currentOrders.map(provinceOrdersToProto),
availableOrders = availableOrders.map(provinceOrderTypeToProto),
availableFocusProvinces = availableFocusProvinces,
actingProvinceId = actingProvinceId
)
case ManagePrisonersAvailable(
actingProvinceId: ProvinceId,
prisoners: Vector[PrisonerToManage]
) =>
proto.ManagePrisonersAvailableCommand(
actingProvinceId = actingProvinceId,
prisoners = prisoners.map(p => prisonerToManageToProto(p, gameState, actingProvinceId))
)
case MarchAvailable(
oneProvinceCommands: Vector[MarchCommandFromOneProvince],
actingProvinceId: ProvinceId,
availableTypes: Vector[BattalionType]
) =>
proto.MarchAvailableCommand(
oneProvinceCommands = oneProvinceCommands.map(cmd => marchCommandFromOneProvinceToProto(cmd, gameState)),
actingProvinceId = actingProvinceId,
availableTypes = availableTypes.map(battalionTypeToProto)
)
case OrganizeTroopsAvailable(
troopCosts: Vector[TroopCost],
existingBattalions: Vector[BattalionId],
maxBattalions: Int,
actingProvinceId: ProvinceId,
availableBattalionTypes: Vector[BattalionTypeStatus]
) =>
proto.OrganizeTroopsAvailableCommand(
troopCosts = troopCosts.map(troopCostToProto),
existingBattalions = existingBattalions,
maxBattalions = maxBattalions,
actingProvinceId = actingProvinceId,
availableBattalionTypes = availableBattalionTypes.map(battalionTypeStatusToProto)
)
case PleaseRecruitMeAvailable(
availableProvinces: Vector[OneProvincePleaseRecruitMe],
actingProvinceId: ProvinceId
) =>
proto.PleaseRecruitMeAvailableCommand(
availableProvinces = availableProvinces.map(p => oneProvincePleaseRecruitMeToProto(p, gameState)),
actingProvinceId = actingProvinceId
)
case ReconAvailable(
availableTargetProvinces: Vector[ProvinceId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
) =>
proto.ReconAvailableCommand(
availableTargetProvinces = availableTargetProvinces,
actingProvinceId = actingProvinceId,
availableHeroIds = availableHeroIds
)
case RecruitHeroesAvailable(
availableHeroes: Vector[ExpandedUnaffiliatedHero],
actingProvinceId: ProvinceId
) =>
proto.RecruitHeroesAvailableCommand(
availableHeroes = availableHeroes.map(h => expandedUnaffiliatedHeroToProto(h, gameState, actingProvinceId)),
actingProvinceId = actingProvinceId
)
case ResolveAllianceOfferAvailable(offers: Vector[DiplomacyOfferInfo]) =>
proto.ResolveAllianceOfferAvailableCommand(
offers = offers.map(o => diplomacyOfferInfoToProto(o, gameState, viewingFactionId))
)
case ResolveBreakAllianceAvailable(offers: Vector[DiplomacyOfferInfo]) =>
proto.ResolveBreakAllianceAvailableCommand(
offers = offers.map(o => diplomacyOfferInfoToProto(o, gameState, viewingFactionId))
)
case ResolveInvitationAvailable(invitations: Vector[DiplomacyOfferInfo]) =>
proto.ResolveInvitationAvailableCommand(
invitations = invitations.map(o => diplomacyOfferInfoToProto(o, gameState, viewingFactionId))
)
case ResolveRansomOfferAvailable(offers: Vector[DiplomacyOfferInfo]) =>
proto.ResolveRansomOfferAvailableCommand(
offers = offers.map(o => diplomacyOfferInfoToProto(o, gameState, viewingFactionId))
)
case ResolveTruceOfferAvailable(offers: Vector[DiplomacyOfferInfo]) =>
proto.ResolveTruceOfferAvailableCommand(
offers = offers.map(o => diplomacyOfferInfoToProto(o, gameState, viewingFactionId))
)
case ResolveTributeAvailable(
demands: Vector[TributeAndFaction],
availableGold: Int,
availableFood: Int,
actingProvinceId: ProvinceId
) =>
proto.ResolveTributeAvailableCommand(
demands = demands.map(tributeAndFactionToProto),
availableGold = availableGold,
availableFood = availableFood,
actingProvinceId = actingProvinceId
)
case RestAvailable(actingProvinceId: ProvinceId) =>
proto.RestAvailableCommand(
actingProvinceId = actingProvinceId
)
case ReturnAvailable(actingProvinceId: ProvinceId) =>
proto.ReturnAvailableCommand(
actingProvinceId = actingProvinceId
)
case SendSuppliesAvailable(
goldAvailable: Int,
foodAvailable: Int,
availableDestinationProvinceIds: Vector[ProvinceId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
) =>
proto.SendSuppliesAvailableCommand(
goldAvailable = goldAvailable,
foodAvailable = foodAvailable,
availableDestinationProvinceIds = availableDestinationProvinceIds,
actingProvinceId = actingProvinceId,
availableHeroIds = availableHeroIds
)
case StartEpidemicAvailable(
options: Vector[StartEpidemicOptions],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
) =>
proto.StartEpidemicAvailableCommand(
options = options.map(startEpidemicOptionsToProto),
actingProvinceId = actingProvinceId,
availableHeroIds = availableHeroIds
)
case SuppressBeastsAvailable(
availableHeroIds: Vector[HeroId],
availableBattalionIds: Vector[BattalionId],
actingProvinceId: ProvinceId
) =>
proto.SuppressBeastsAvailableCommand(
availableHeroIds = availableHeroIds,
availableBattalionIds = availableBattalionIds,
actingProvinceId = actingProvinceId
)
case SwearBrotherhoodAvailable(
actingProvinceId: ProvinceId,
availableHeroes: Vector[HeroAndBackstory]
) =>
proto.SwearBrotherhoodAvailableCommand(
actingProvinceId = actingProvinceId,
availableHeroes = availableHeroes.map(heroAndBackstoryToProto)
)
case TradeAvailable(
foodAvailable: Int,
goldAvailable: Int,
foodBuyPrice: Double,
foodSellPrice: Double,
actingProvinceId: ProvinceId
) =>
proto.TradeAvailableCommand(
foodAvailable = foodAvailable,
goldAvailable = goldAvailable,
foodBuyPrice = foodBuyPrice,
foodSellPrice = foodSellPrice,
actingProvinceId = actingProvinceId
)
case TrainAvailable(
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId],
recommendedHeroId: HeroId
) =>
proto.TrainAvailableCommand(
actingProvinceId = actingProvinceId,
availableHeroIds = availableHeroIds,
recommendedHeroId = recommendedHeroId
)
case TravelAvailable(actingProvinceId: ProvinceId) =>
proto.TravelAvailableCommand(
actingProvinceId = actingProvinceId
)
}
// Helper functions for nested types
private def residentOutlawToProto(
outlaw: ResidentOutlaw,
gameState: GameState,
viewingFactionId: FactionId
): proto.ResidentOutlaw =
proto.ResidentOutlaw(
hero = Some(
HeroViewFilter.filteredHeroView(
hero = gameState.heroes(outlaw.heroId),
gs = gameState,
factionId = Some(viewingFactionId)
)
),
lastFactionId = outlaw.lastFactionId
)
private def armamentCostToProto(cost: ArmamentCost): proto.ArmamentCost =
proto.ArmamentCost(
`type` = intToBattalionTypeId(cost.battalionTypeId),
cost = cost.cost
)
private def intToBattalionTypeId(id: Int): BattalionTypeIdProto =
BattalionTypeIdProto.fromValue(id)
private def armyStatsToProto(stats: ArmyStats): net.eagle0.eagle.api.command.util.army_stats.ArmyStats =
net.eagle0.eagle.api.command.util.army_stats.ArmyStats(
factionId = stats.factionId,
heroCount = stats.heroCount,
troopCount = stats.troopCount
)
private def expandedCombatUnitToProto(
unit: ExpandedCombatUnit,
gameState: GameState
): net.eagle0.eagle.api.command.util.expanded_combat_unit.ExpandedCombatUnit =
net.eagle0.eagle.api.command.util.expanded_combat_unit.ExpandedCombatUnit(
hero = Some(
HeroViewFilter.filteredHeroView(
hero = gameState.heroes(unit.heroId),
gs = gameState,
factionId = None
)
),
battalion = unit.battalionId
.map(gameState.battalions)
.map(LegacyBattalionViewFilter.filteredBattalionViewBelongingToPlayer)
)
private def targetProvinceOptionsToProto(
options: TargetProvinceOptions
): proto.ControlWeatherAvailableCommand.TargetProvinceOptions =
proto.ControlWeatherAvailableCommand.TargetProvinceOptions(
provinceId = options.provinceId,
controlWeatherTypes = options.controlWeatherTypes.map(controlWeatherTypeToProto)
)
private def expandedUnaffiliatedHeroToProto(
hero: ExpandedUnaffiliatedHero,
gameState: GameState,
provinceId: ProvinceId
): net.eagle0.eagle.api.command.util.expanded_unaffiliated_hero.ExpandedUnaffiliatedHero = {
// Look up the full unaffiliated hero data from the province
val unaffiliatedHero = gameState
.provinces(provinceId)
.unaffiliatedHeroes
.find(_.heroId == hero.heroId)
.getOrElse(
throw new IllegalStateException(s"UnaffiliatedHero ${hero.heroId} not found in province $provinceId")
)
net.eagle0.eagle.api.command.util.expanded_unaffiliated_hero.ExpandedUnaffiliatedHero(
hero = Some(
HeroViewFilter.filteredHeroView(
hero = gameState.heroes(hero.heroId),
gs = gameState,
factionId = None
)
),
`type` = unaffiliatedHero.`type`,
quest = unaffiliatedHero.recruitmentInfo.flatMap(_.quest),
pleaseRecruitMeTextId = unaffiliatedHero.pleaseRecruitMeTextId,
divinationTextId = unaffiliatedHero.divinedTextId
)
}
private def suitableBattalionsForHeroesToProto(
sbMap: Map[HeroId, SuitableBattalions]
): Map[HeroId, net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions] =
// The Scala SuitableBattalions only has battalion IDs, but proto expects suitability levels.
// Convert with default OPTIMAL suitability since we don't have suitability info.
sbMap.map {
case (heroId, sb) =>
heroId -> net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions(
battalionIdsWithSuitability = sb.battalionIds.map { battalionId =>
net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions.BattalionIdWithSuitability(
battalionId = battalionId,
level =
net.eagle0.eagle.api.command.util.appropriate_battalions.SuitableBattalions.SuitabilityLevel.OPTIMAL
)
}
)
}
private def battalionWithFoodCostToProto(
b: BattalionWithFoodCost
): net.eagle0.eagle.api.command.util.battalion_with_food_cost.BattalionWithFoodCost =
net.eagle0.eagle.api.command.util.battalion_with_food_cost.BattalionWithFoodCost(
battalionId = b.battalionId,
monthlyFood = b.foodCost.toDouble // Scala has Int foodCost, proto has Double monthlyFood
)
private def recommendedCombatUnitToProto(
unit: RecommendedCombatUnit
): net.eagle0.eagle.common.combat_unit.CombatUnit =
net.eagle0.eagle.common.combat_unit.CombatUnit(
heroId = unit.heroId,
battalionId = unit.battalionId
)
// Scala DiplomacyOption groups multiple option types per target faction.
// Proto DiplomacyOption is a oneof with individual option types.
// So one Scala option can produce multiple proto options.
private def diplomacyOptionToProtos(option: DiplomacyOption): Vector[DiplomacyOptionProto] =
option.optionTypes.map { optionType =>
optionType match {
case DiplomacyOptionType.Alliance =>
AllianceOption(
targetFactionId = option.targetFactionId,
goldCost = AllianceGoldCost.intValue
)
case DiplomacyOptionType.Truce =>
TruceOption(
targetFactionId = option.targetFactionId,
goldCost = TruceGoldCost.intValue
)
case DiplomacyOptionType.Invitation =>
InvitationOption(
targetFactionId = option.targetFactionId,
goldCost = InviteGoldCost.intValue
)
case DiplomacyOptionType.BreakAlliance =>
BreakAllianceOption(
targetFactionId = option.targetFactionId,
goldCost = BreakAllianceGoldCost.intValue
)
case DiplomacyOptionType.Ransom(details: RansomOfferDetails) =>
RansomOfferOption(
targetFactionId = option.targetFactionId,
ransomOffer = Some(ransomOfferDetailsToProto(details))
)
}
}
private def ransomOfferDetailsToProto(details: RansomOfferDetails): RansomOfferDetailsProto =
RansomOfferDetailsProto(
prisonerToBeRansomed = Some(prisonerToBeRansomedToProto(details.prisonerToBeRansomed)),
prisonersOffered = details.prisonersOffered.map(prisonerOfferedInExchangeToProto),
hostagesOffered = details.hostagesOffered.map(hostageOfferedInExchangeToProto),
goldOffered = details.goldOffered
)
private def prisonerToBeRansomedToProto(prisoner: PrisonerToBeRansomed): PrisonerToBeRansomedProto =
PrisonerToBeRansomedProto(
prisonerHeroId = prisoner.prisonerHeroId,
provinceIdForPrisoner = prisoner.provinceIdForPrisoner
)
private def prisonerOfferedInExchangeToProto(
prisoner: PrisonerOfferedInExchange
): PrisonerOfferedInExchangeProto =
PrisonerOfferedInExchangeProto(
heroId = prisoner.heroId,
factionIdForPrisoner = prisoner.factionIdForPrisoner.getOrElse(-1),
provinceIdWithHero = prisoner.provinceIdWithHero
)
private def hostageOfferedInExchangeToProto(hostage: HostageOfferedInExchange): HostageOfferedInExchangeProto =
HostageOfferedInExchangeProto(
heroId = hostage.heroId,
provinceIdWithHero = hostage.provinceIdWithHero
)
private def expandedCapturedHeroToProto(
hero: ExpandedCapturedHero,
gameState: GameState,
viewingFactionId: FactionId
): proto.ExpandedCapturedHero =
proto.ExpandedCapturedHero(
hero = Some(
HeroViewFilter.filteredHeroView(
hero = gameState.heroes(hero.heroId),
gs = gameState,
factionId = Some(viewingFactionId)
)
),
options = hero.options.map(capturedHeroOptionToProto),
messageId = hero.messageId,
previousFactionId = hero.previousFactionId
)
private def eligibleGiftToProto(gift: EligibleGift): proto.EligibleGift =
proto.EligibleGift(
recipientHeroId = gift.recipientHeroId,
goldAvailable = gift.goldAvailable,
recipientProvinceId = gift.recipientProvinceId
)
private def provinceOrdersToProto(
orders: ProvinceOrders
): net.eagle0.eagle.api.command.util.province_orders.ProvinceOrders =
net.eagle0.eagle.api.command.util.province_orders.ProvinceOrders(
provinceId = orders.provinceId,
orders = provinceOrderTypeToProto(orders.orderType)
)
private def prisonerToManageToProto(
prisoner: PrisonerToManage,
gameState: GameState,
provinceId: ProvinceId
): proto.PrisonerToManage =
proto.PrisonerToManage(
prisoner = Some(expandedUnaffiliatedHeroToProto(prisoner.prisoner, gameState, provinceId)),
availableOptions = prisoner.availableOptions.map(prisonerManagementOptionToProto)
)
private def marchCommandFromOneProvinceToProto(
cmd: MarchCommandFromOneProvince,
gameState: GameState
): proto.MarchCommandFromOneProvince =
proto.MarchCommandFromOneProvince(
suitableBattalionsForHeroes = suitableBattalionsForHeroesToProto(cmd.suitableBattalionsForHeroes),
originProvinceId = cmd.originProvinceId,
goldCost = cmd.goldCost,
goldAvailable = cmd.goldAvailable,
foodAvailable = cmd.foodAvailable,
availableDestinationProvinces = cmd.availableDestinationProvinces.map(availableDestinationProvinceToProto),
availableHeroIds = cmd.availableHeroIds,
availableBattalions = cmd.availableBattalions.map(battalionWithFoodCostToProto),
recommendedBattleConfig = cmd.recommendedBattleConfig.map(recommendedBattleConfigToProto)
)
private def availableDestinationProvinceToProto(
dest: AvailableDestinationProvince
): proto.AvailableDestinationProvince =
proto.AvailableDestinationProvince(
provinceId = dest.provinceId,
requiresRiverCrossing = dest.requiresRiverCrossing
)
private def recommendedBattleConfigToProto(config: RecommendedBattleConfig): proto.RecommendedBattleConfig =
proto.RecommendedBattleConfig(
units = config.units.map(recommendedCombatUnitToProto),
food = config.food,
gold = config.gold
)
private def battalionTypeToProto(bt: BattalionType): BattalionTypeProto =
BattalionTypeProto(
typeId = intToBattalionTypeId(bt.typeId),
name = bt.name
)
private def troopCostToProto(cost: TroopCost): proto.TroopCost =
proto.TroopCost(
`type` = intToBattalionTypeId(cost.battalionTypeId),
costPerTroop = cost.costPerTroop
)
private def battalionTypeStatusToProto(
status: BattalionTypeStatus
): proto.OrganizeTroopsAvailableCommand.BattalionTypeStatus =
proto.OrganizeTroopsAvailableCommand.BattalionTypeStatus(
typeId = intToBattalionTypeId(status.typeId),
meetsRequirements = status.meetsRequirements,
minimumAgriculture = status.minimumAgriculture,
minimumEconomy = status.minimumEconomy
)
private def oneProvincePleaseRecruitMeToProto(
prov: OneProvincePleaseRecruitMe,
gameState: GameState
): proto.OneProvincePleaseRecruitMe =
proto.OneProvincePleaseRecruitMe(
provinceId = prov.provinceId,
availableHeroes = prov.availableHeroes.map(h => expandedUnaffiliatedHeroToProto(h, gameState, prov.provinceId))
)
// DiplomacyOfferInfo is too simplified - look up the full offer from GameState
private def diplomacyOfferInfoToProto(
info: DiplomacyOfferInfo,
gameState: GameState,
viewingFactionId: FactionId
): DiplomacyOfferProto =
// The offers exist in the faction's incoming diplomacy offers
gameState
.factions(viewingFactionId)
.incomingDiplomacyOffers
.find(offer =>
offer.originatingFactionId == info.offeringFactionId &&
offer.targetFactionId == info.targetFactionId
)
.getOrElse(
throw new IllegalStateException(
s"DiplomacyOffer from ${info.offeringFactionId} to ${info.targetFactionId} not found"
)
)
private def tributeAndFactionToProto(taf: TributeAndFaction): proto.TributeAndFaction =
proto.TributeAndFaction(
demandingFactionId = taf.demandingFactionId,
tributeDemanded = Some(
TributeAmountProto(
gold = taf.goldDemanded,
food = taf.foodDemanded
)
),
heroCount = taf.heroCount,
troopCount = taf.troopCount
)
private def startEpidemicOptionsToProto(
options: StartEpidemicOptions
): proto.StartEpidemicAvailableCommand.StartEpidemicOptions =
proto.StartEpidemicAvailableCommand.StartEpidemicOptions(
targetProvinceId = options.targetProvinceId
)
private def heroAndBackstoryToProto(hab: HeroAndBackstory): proto.SwearBrotherhoodAvailableCommand.HeroAndBackstory =
proto.SwearBrotherhoodAvailableCommand.HeroAndBackstory(
heroId = hab.heroId,
backstoryTextId = hab.backstoryTextId
)
// Enum converters
// AttackDecisionType proto is a oneof - each type is a case class
private def attackDecisionTypeToProto(t: AttackDecisionType): AttackDecisionTypeProto =
t match {
case AttackDecisionType.Advance => AdvanceDecision()
case AttackDecisionType.Withdraw => WithdrawDecision()
case AttackDecisionType.DemandTribute(gold: Int, food: Int) =>
DemandTributeDecision(
tribute = Some(TributeAmountProto(gold = gold, food = food))
)
case AttackDecisionType.SafePassage(destinationProvinceId: ProvinceId) =>
SafePassageDecision(toProvinceId = destinationProvinceId)
}
private def controlWeatherTypeToProto(t: ControlWeatherType): ControlWeatherTypeProto =
t match {
case ControlWeatherType.StartBlizzard => ControlWeatherTypeProto.CONTROL_WEATHER_START_BLIZZARD
case ControlWeatherType.EndBlizzard => ControlWeatherTypeProto.CONTROL_WEATHER_END_BLIZZARD
case ControlWeatherType.StartDrought => ControlWeatherTypeProto.CONTROL_WEATHER_START_DROUGHT
case ControlWeatherType.EndDrought => ControlWeatherTypeProto.CONTROL_WEATHER_END_DROUGHT
}
private def capturedHeroOptionToProto(opt: CapturedHeroOption): CapturedHeroOptionProto =
opt match {
case CapturedHeroOption.Imprison => CapturedHeroOptionProto.IMPRISON_CAPTURED_HERO_OPTION
case CapturedHeroOption.Execute => CapturedHeroOptionProto.EXECUTE_CAPTURED_HERO_OPTION
case CapturedHeroOption.Exile => CapturedHeroOptionProto.EXILE_CAPTURED_HERO_OPTION
case CapturedHeroOption.Return => CapturedHeroOptionProto.RETURN_CAPTURED_HERO_OPTION
case CapturedHeroOption.Recruit => CapturedHeroOptionProto.RECRUIT_CAPTURED_HERO_OPTION
}
private def improvementTypeToProto(t: ImprovementType): ImprovementTypeProto =
t match {
case ImprovementType.Agriculture => ImprovementTypeProto.AGRICULTURE
case ImprovementType.Economy => ImprovementTypeProto.ECONOMY
case ImprovementType.Infrastructure => ImprovementTypeProto.INFRASTRUCTURE
case ImprovementType.Devastation => ImprovementTypeProto.DEVASTATION
}
// ScalaPB generates oneofs as sealed traits where each option is a case class
// that extends the trait. So PrisonerManagementOptionRelease() is a PrisonerManagementOption.
private def prisonerManagementOptionToProto(opt: PrisonerManagementOption): PrisonerManagementOptionProto =
opt match {
case PrisonerManagementOption.Release => PrisonerManagementOptionRelease()
case PrisonerManagementOption.Execute => PrisonerManagementOptionExecute()
case PrisonerManagementOption.Exile => PrisonerManagementOptionExile()
case PrisonerManagementOption.Move(toProvinceId) => PrisonerManagementOptionMove(toProvinceId = toProvinceId)
case PrisonerManagementOption.Return(toFactionId) => PrisonerManagementOptionReturn(toFactionId = toFactionId)
}
private def provinceOrderTypeToProto(t: ProvinceOrderType): ProvinceOrderTypeProto =
t match {
case ProvinceOrderType.Develop => ProvinceOrderTypeProto.DEVELOP
case ProvinceOrderType.Mobilize => ProvinceOrderTypeProto.MOBILIZE
case ProvinceOrderType.Expand => ProvinceOrderTypeProto.EXPAND
case ProvinceOrderType.Entrust => ProvinceOrderTypeProto.ENTRUST
}
}
@@ -1,42 +0,0 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "available_command_converter",
srcs = ["AvailableCommandConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:appropriate_battalions_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:army_stats_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:attack_decision_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:battalion_with_food_cost_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:captured_hero_option_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:control_weather_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:diplomacy_option_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:expanded_combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:expanded_unaffiliated_hero_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:prisoner_management_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:province_orders_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:alliance_gold_cost",
"//src/main/scala/net/eagle0/eagle/library/settings:break_alliance_gold_cost",
"//src/main/scala/net/eagle0/eagle/library/settings:invite_gold_cost",
"//src/main/scala/net/eagle0/eagle/library/settings:truce_gold_cost",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:hero_view_filter",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:legacy_battalion_view_filter",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
"@com_google_protobuf//:protobuf_java",
],
)
@@ -1,30 +0,0 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "selected_command_converter",
srcs = ["SelectedCommandConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:attack_decision_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:captured_hero_option_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:control_weather_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:diplomacy_option_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:prisoner_management_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:province_orders_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_status_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
],
)
@@ -1,465 +0,0 @@
package net.eagle0.eagle.model.proto_converters.command.selected
import net.eagle0.eagle.api.command.util.attack_decision_type.{
AdvanceDecision,
AttackDecisionType as AttackDecisionTypeProto,
DemandTributeDecision,
SafePassageDecision,
WithdrawDecision
}
import net.eagle0.eagle.api.command.util.captured_hero_option.CapturedHeroOption as CapturedHeroOptionProto
import net.eagle0.eagle.api.command.util.control_weather_type.ControlWeatherType as ControlWeatherTypeProto
import net.eagle0.eagle.api.command.util.diplomacy_option.{
AllianceOption,
BreakAllianceOption,
DiplomacyOption as DiplomacyOptionProto,
InvitationOption,
RansomOfferOption,
TruceOption
}
import net.eagle0.eagle.api.command.util.prisoner_management_type.{
PrisonerManagementOption as PrisonerManagementOptionProto,
PrisonerManagementOptionExecute,
PrisonerManagementOptionExile,
PrisonerManagementOptionMove,
PrisonerManagementOptionRelease,
PrisonerManagementOptionReturn
}
import net.eagle0.eagle.api.selected_command as proto
import net.eagle0.eagle.common.diplomacy_offer.{
HostageOfferedInExchange as HostageOfferedInExchangeProto,
PrisonerOfferedInExchange as PrisonerOfferedInExchangeProto,
PrisonerToBeRansomed as PrisonerToBeRansomedProto,
RansomOfferDetails as RansomOfferDetailsProto
}
import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus as DiplomacyOfferStatusProto
import net.eagle0.eagle.common.improvement_type.ImprovementType as ImprovementTypeProto
import net.eagle0.eagle.common.province_order_type.ProvinceOrderType as ProvinceOrderTypeProto
import net.eagle0.eagle.model.state.command.common.*
import net.eagle0.eagle.model.state.command.selected.*
import net.eagle0.eagle.model.state.command.selected.SelectedCommand.*
import net.eagle0.eagle.model.state.diplomacy_offer.status.{Accepted, Rejected, Status}
/**
* Converts proto SelectedCommand to Scala SelectedCommand.
*/
object SelectedCommandConverter {
def fromProto(command: proto.SelectedCommand): SelectedCommand =
command match {
case cmd: proto.AlmsSelectedCommand =>
AlmsSelected(
amount = cmd.amount,
actingHeroId = cmd.actingHeroId
)
case cmd: proto.ApprehendOutlawSelectedCommand =>
ApprehendOutlawSelected(
heroIdToApprehend = cmd.heroIdToApprehend,
battalionId = cmd.battalionId,
actingHeroId = cmd.actingHeroId
)
case cmd: proto.ArmTroopsSelectedCommand =>
ArmTroopsSelected(
armedBattalions = cmd.armedBattalions.map(armedBattalionFromProto).toVector
)
case cmd: proto.AttackDecisionSelectedCommand =>
AttackDecisionSelected(
decision = attackDecisionTypeFromProto(cmd.decision)
)
case cmd: proto.ControlWeatherSelectedCommand =>
ControlWeatherSelected(
selectedType = controlWeatherTypeFromProto(cmd.selectedType),
selectedProvinceId = cmd.selectedProvinceId,
actingHeroId = cmd.actingHeroId
)
case cmd: proto.DeclineQuestSelectedCommand =>
DeclineQuestSelected(heroId = cmd.heroId)
case cmd: proto.DefendSelectedCommand =>
DefendSelected(
fleeProvinceId = cmd.fleeProvinceId,
defendingUnits = cmd.defendingUnits.map(combatUnitFromProto).toVector
)
case cmd: proto.DiplomacySelectedCommand =>
DiplomacySelected(
selectedOption = diplomacyOptionFromProto(cmd.selectedOption),
targetFactionId = cmd.targetFactionId,
sentHeroId = cmd.sentHeroId
)
case cmd: proto.DivineSelectedCommand =>
DivineSelected(heroIds = cmd.heroIds.toVector)
case cmd: proto.ExileVassalSelectedCommand =>
ExileVassalSelected(exiledHeroId = cmd.exiledHeroId)
case _: proto.FeastSelectedCommand =>
FeastSelected
case cmd: proto.FreeForAllDecisionSelectedCommand =>
FreeForAllDecisionSelected(
provinceId = cmd.provinceId,
decision = attackDecisionTypeFromProto(cmd.decision)
)
case cmd: proto.HandleCapturedHeroSelectedCommand =>
HandleCapturedHeroSelected(
heroId = cmd.heroId,
selectedOption = capturedHeroOptionFromProto(cmd.selectedOption)
)
case cmd: proto.HandleRiotCrackDownSelectedCommand =>
HandleRiotCrackDownSelected(
heroId = cmd.heroId,
battalionId = cmd.battalionId,
actingHeroId = cmd.actingHeroId
)
case _: proto.HandleRiotDoNothingSelectedCommand =>
HandleRiotDoNothingSelected
case cmd: proto.HandleRiotGiveSelectedCommand =>
HandleRiotGiveSelected(
foodAmount = cmd.foodAmount,
goldAmount = cmd.goldAmount
)
case cmd: proto.HeroGiftSelectedCommand =>
HeroGiftSelected(
recipientHeroId = cmd.recipientHeroId,
amount = cmd.amount
)
case cmd: proto.ImproveSelectedCommand =>
ImproveSelected(
improvementType = improvementTypeFromProto(cmd.improvementType),
actingHeroId = cmd.actingHeroId,
lockType = cmd.lockType
)
case cmd: proto.IssueOrdersSelectedCommand =>
IssueOrdersSelected(
newOrders = cmd.newOrders.map(provinceOrderFromProto).toVector,
newFocusProvince = cmd.newFocusProvince
)
case cmd: proto.ManagePrisonersSelectedCommand =>
ManagePrisonersSelected(
prisonerHeroId = cmd.prisonerHeroId,
chosenOption = prisonerManagementOptionFromProto(cmd.chosenOption)
)
case cmd: proto.MarchSelectedCommand =>
MarchSelected(
gold = cmd.gold,
food = cmd.food,
originProvince = cmd.originProvince,
destinationProvinceId = cmd.destinationProvinceId,
marchingUnits = cmd.marchingUnits.map(combatUnitFromProto).toVector
)
case cmd: proto.OrganizeTroopsSelectedCommand =>
OrganizeTroopsSelected(
changedBattalions = cmd.changedBattalions.map(changedBattalionFromProto).toVector,
newBattalions = cmd.newBattalions.map(newBattalionFromProto).toVector
)
case cmd: proto.PleaseRecruitMeSelectedCommand =>
PleaseRecruitMeSelected(
provinceId = cmd.provinceId,
heroId = cmd.heroId,
accept = cmd.accept
)
case cmd: proto.ReconSelectedCommand =>
ReconSelected(
actingHeroId = cmd.actingHeroId,
targetProvinceId = cmd.targetProvinceId
)
case cmd: proto.RecruitHeroesSelectedCommand =>
RecruitHeroesSelected(heroIds = cmd.heroIds.toVector)
case cmd: proto.ResolveAllianceOfferSelectedCommand =>
ResolveAllianceOfferSelected(
originatingFactionId = cmd.originatingFactionId,
resolution = statusFromProto(cmd.resolution)
)
case cmd: proto.ResolveBreakAllianceSelectedCommand =>
ResolveBreakAllianceSelected(
originatingFactionId = cmd.originatingFactionId,
resolution = statusFromProto(cmd.resolution)
)
case cmd: proto.ResolveInvitationSelectedCommand =>
ResolveInvitationSelected(
originatingFactionId = cmd.originatingFactionId,
resolution = statusFromProto(cmd.resolution)
)
case cmd: proto.ResolveRansomOfferSelectedCommand =>
val prisonerHeroId = cmd.ransomOffer.flatMap { offer =>
offer.offerDetails match {
case net.eagle0.eagle.common.diplomacy_offer.RansomOfferDetails(
prisonerToBeRansomed,
_,
_,
_,
_
) =>
prisonerToBeRansomed.map(_.prisonerHeroId)
case _ => None
}
}
ResolveRansomOfferSelected(
offeringFactionId = cmd.offeringFactionId,
prisonerHeroId = prisonerHeroId.getOrElse(-1),
resolution = statusFromProto(cmd.resolution)
)
case cmd: proto.ResolveTruceOfferSelectedCommand =>
ResolveTruceOfferSelected(
originatingFactionId = cmd.originatingFactionId,
resolution = statusFromProto(cmd.resolution)
)
case cmd: proto.ResolveTributeSelectedCommand =>
ResolveTributeSelected(
demandingFactionId = cmd.demandingFactionId,
paid = cmd.paid
)
case _: proto.RestSelectedCommand =>
RestSelected
case _: proto.ReturnSelectedCommand =>
ReturnSelected
case cmd: proto.SendSuppliesSelectedCommand =>
SendSuppliesSelected(
gold = cmd.gold,
food = cmd.food,
actingHeroId = cmd.actingHeroId,
destinationProvinceId = cmd.destinationProvinceId
)
case cmd: proto.StartEpidemicSelectedCommand =>
StartEpidemicSelected(
selectedProvinceId = cmd.selectedProvinceId,
actingHeroId = cmd.actingHeroId
)
case cmd: proto.SuppressBeastsSelectedCommand =>
SuppressBeastsSelected(
heroId = cmd.heroId,
battalionId = cmd.battalionId
)
case cmd: proto.SwearBrotherhoodSelectedCommand =>
SwearBrotherhoodSelected(newBrotherHeroId = cmd.newBrotherHeroId)
case cmd: proto.TradeSelectedCommand =>
TradeSelected(
tradeType = tradeTypeFromProto(cmd.tradeType),
amount = cmd.amount
)
case cmd: proto.TrainSelectedCommand =>
TrainSelected(actingHeroId = cmd.actingHeroId)
case _: proto.TravelSelectedCommand =>
TravelSelected
case proto.SelectedCommand.Empty =>
throw new IllegalArgumentException("SelectedCommand is empty")
}
// Helper functions for nested types
private def armedBattalionFromProto(
ab: net.eagle0.eagle.api.command.util.armed_battalion.ArmedBattalion
): ArmedBattalion =
ArmedBattalion(
battalionId = ab.id,
battalionTypeId = ab.newArmament
)
private def combatUnitFromProto(cu: net.eagle0.eagle.common.combat_unit.CombatUnit): CombatUnit =
CombatUnit(
heroId = cu.heroId,
battalionId = cu.battalionId
)
private def provinceOrderFromProto(
po: net.eagle0.eagle.api.command.util.province_orders.ProvinceOrders
): ProvinceOrder =
ProvinceOrder(
provinceId = po.provinceId,
orderType = provinceOrderTypeFromProto(po.orders)
)
private def changedBattalionFromProto(
cb: proto.OrganizeTroopsSelectedCommand.ChangedBattalion
): ChangedBattalion =
ChangedBattalion(
id = cb.id,
troopsFromOtherBattalion = cb.troopsFromOtherBattalion.map(troopsFromOtherBattalionFromProto).toVector,
newTroops = cb.newTroops,
dismissedTroops = cb.dismissedTroops
)
private def newBattalionFromProto(nb: proto.OrganizeTroopsSelectedCommand.NewBattalion): NewBattalion =
NewBattalion(
typeId = nb.`type`.value,
troopsFromOtherBattalion = nb.troopsFromOtherBattalion.map(troopsFromOtherBattalionFromProto).toVector,
newTroops = nb.newTroops
)
private def troopsFromOtherBattalionFromProto(
t: proto.OrganizeTroopsSelectedCommand.TroopsFromOtherBattalion
): TroopsFromOtherBattalion =
TroopsFromOtherBattalion(
fromBattalionId = t.fromBattalionId,
countMoved = t.countMoved
)
// Enum converters
private def attackDecisionTypeFromProto(decision: AttackDecisionTypeProto): AttackDecisionType =
decision match {
case _: AdvanceDecision => AttackDecisionType.Advance
case _: WithdrawDecision => AttackDecisionType.Withdraw
case d: DemandTributeDecision =>
val tribute = d.tribute.getOrElse(
throw new IllegalArgumentException("DemandTributeDecision missing tribute")
)
AttackDecisionType.DemandTribute(gold = tribute.gold, food = tribute.food)
case d: SafePassageDecision =>
AttackDecisionType.SafePassage(destinationProvinceId = d.toProvinceId)
case AttackDecisionTypeProto.Empty =>
throw new IllegalArgumentException("AttackDecisionType has no value set")
}
private def controlWeatherTypeFromProto(t: ControlWeatherTypeProto): ControlWeatherType =
t match {
case ControlWeatherTypeProto.CONTROL_WEATHER_START_BLIZZARD => ControlWeatherType.StartBlizzard
case ControlWeatherTypeProto.CONTROL_WEATHER_END_BLIZZARD => ControlWeatherType.EndBlizzard
case ControlWeatherTypeProto.CONTROL_WEATHER_START_DROUGHT => ControlWeatherType.StartDrought
case ControlWeatherTypeProto.CONTROL_WEATHER_END_DROUGHT => ControlWeatherType.EndDrought
case ControlWeatherTypeProto.CONTROL_WEATHER_UNKNOWN | ControlWeatherTypeProto.Unrecognized(_) =>
throw new IllegalArgumentException(s"Unknown ControlWeatherType: $t")
}
private def capturedHeroOptionFromProto(opt: CapturedHeroOptionProto): CapturedHeroOption =
opt match {
case CapturedHeroOptionProto.IMPRISON_CAPTURED_HERO_OPTION => CapturedHeroOption.Imprison
case CapturedHeroOptionProto.EXECUTE_CAPTURED_HERO_OPTION => CapturedHeroOption.Execute
case CapturedHeroOptionProto.EXILE_CAPTURED_HERO_OPTION => CapturedHeroOption.Exile
case CapturedHeroOptionProto.RETURN_CAPTURED_HERO_OPTION => CapturedHeroOption.Return
case CapturedHeroOptionProto.RECRUIT_CAPTURED_HERO_OPTION => CapturedHeroOption.Recruit
case CapturedHeroOptionProto.UNKNOWN_CAPTURED_HERO_OPTION | CapturedHeroOptionProto.Unrecognized(_) =>
throw new IllegalArgumentException(s"Unknown CapturedHeroOption: $opt")
}
private def diplomacyOptionFromProto(option: DiplomacyOptionProto): DiplomacyOptionType =
option match {
case _: AllianceOption => DiplomacyOptionType.Alliance
case _: TruceOption => DiplomacyOptionType.Truce
case _: InvitationOption => DiplomacyOptionType.Invitation
case _: BreakAllianceOption => DiplomacyOptionType.BreakAlliance
case r: RansomOfferOption =>
val details = r.ransomOffer.getOrElse(
throw new IllegalArgumentException("RansomOfferOption missing ransomOffer")
)
DiplomacyOptionType.Ransom(ransomOfferDetailsFromProto(details))
case DiplomacyOptionProto.Empty =>
throw new IllegalArgumentException("DiplomacyOption has no value set")
}
private def ransomOfferDetailsFromProto(details: RansomOfferDetailsProto): RansomOfferDetails =
RansomOfferDetails(
prisonerToBeRansomed = prisonerToBeRansomedFromProto(
details.prisonerToBeRansomed.getOrElse(
throw new IllegalArgumentException("RansomOfferDetails missing prisonerToBeRansomed")
)
),
prisonersOffered = details.prisonersOffered.map(prisonerOfferedInExchangeFromProto).toVector,
hostagesOffered = details.hostagesOffered.map(hostageOfferedInExchangeFromProto).toVector,
goldOffered = details.goldOffered
)
private def prisonerToBeRansomedFromProto(p: PrisonerToBeRansomedProto): PrisonerToBeRansomed =
PrisonerToBeRansomed(
prisonerHeroId = p.prisonerHeroId,
provinceIdForPrisoner = p.provinceIdForPrisoner
)
private def prisonerOfferedInExchangeFromProto(p: PrisonerOfferedInExchangeProto): PrisonerOfferedInExchange =
PrisonerOfferedInExchange(
heroId = p.heroId,
factionIdForPrisoner = if p.factionIdForPrisoner >= 0 then Some(p.factionIdForPrisoner) else None,
provinceIdWithHero = p.provinceIdWithHero
)
private def hostageOfferedInExchangeFromProto(h: HostageOfferedInExchangeProto): HostageOfferedInExchange =
HostageOfferedInExchange(
heroId = h.heroId,
provinceIdWithHero = h.provinceIdWithHero
)
private def improvementTypeFromProto(t: ImprovementTypeProto): ImprovementType =
t match {
case ImprovementTypeProto.AGRICULTURE => ImprovementType.Agriculture
case ImprovementTypeProto.ECONOMY => ImprovementType.Economy
case ImprovementTypeProto.INFRASTRUCTURE => ImprovementType.Infrastructure
case ImprovementTypeProto.NONE | ImprovementTypeProto.Unrecognized(_) =>
throw new IllegalArgumentException(s"Unknown ImprovementType: $t")
}
private def prisonerManagementOptionFromProto(opt: PrisonerManagementOptionProto): PrisonerManagementOption =
opt match {
case _: PrisonerManagementOptionRelease => PrisonerManagementOption.Release
case _: PrisonerManagementOptionExecute => PrisonerManagementOption.Execute
case _: PrisonerManagementOptionExile => PrisonerManagementOption.Exile
case move: PrisonerManagementOptionMove => PrisonerManagementOption.Move(toProvinceId = move.toProvinceId)
case ret: PrisonerManagementOptionReturn => PrisonerManagementOption.Return(toFactionId = ret.toFactionId)
case PrisonerManagementOptionProto.Empty =>
throw new IllegalArgumentException("PrisonerManagementOption has no value set")
}
private def provinceOrderTypeFromProto(t: ProvinceOrderTypeProto): ProvinceOrderType =
t match {
case ProvinceOrderTypeProto.DEVELOP => ProvinceOrderType.Develop
case ProvinceOrderTypeProto.MOBILIZE => ProvinceOrderType.Mobilize
case ProvinceOrderTypeProto.EXPAND => ProvinceOrderType.Expand
case ProvinceOrderTypeProto.ENTRUST => ProvinceOrderType.Entrust
case ProvinceOrderTypeProto.UNKNOWN_ORDERS | ProvinceOrderTypeProto.Unrecognized(_) =>
throw new IllegalArgumentException(s"Unknown ProvinceOrderType: $t")
}
private def tradeTypeFromProto(t: proto.TradeSelectedCommand.TradeType): TradeType =
t match {
case proto.TradeSelectedCommand.TradeType.BUY_FOOD => TradeType.BuyFood
case proto.TradeSelectedCommand.TradeType.SELL_FOOD => TradeType.SellFood
case proto.TradeSelectedCommand.TradeType.UNKNOWN | proto.TradeSelectedCommand.TradeType.Unrecognized(_) =>
throw new IllegalArgumentException(s"Unknown TradeType: $t")
}
private def statusFromProto(status: DiplomacyOfferStatusProto): Status =
status match {
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_ACCEPTED => Accepted
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_REJECTED => Rejected
case DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_UNKNOWN |
DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_UNRESOLVED |
DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_IMPRISONED |
DiplomacyOfferStatusProto.DIPLOMACY_OFFER_STATUS_INVALIDATED | DiplomacyOfferStatusProto.Unrecognized(_) =>
throw new IllegalArgumentException(s"Invalid resolution status: $status")
}
}
@@ -6,7 +6,6 @@ scala_library(
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:__pkg__",
@@ -136,7 +136,7 @@ scala_library(
name = "improvement_type",
srcs = ["ImprovementType.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
@@ -1,16 +1,24 @@
package net.eagle0.eagle.model.state
enum BattalionTypeId(val value: Int):
case LightInfantry extends BattalionTypeId(0)
case HeavyInfantry extends BattalionTypeId(1)
case LightCavalry extends BattalionTypeId(2)
case HeavyCavalry extends BattalionTypeId(3)
case Longbowmen extends BattalionTypeId(4)
case Undead extends BattalionTypeId(5)
sealed abstract class BattalionTypeId(val value: Int)
object BattalionTypeId {
case object LightInfantry extends BattalionTypeId(0)
case object HeavyInfantry extends BattalionTypeId(1)
case object LightCavalry extends BattalionTypeId(2)
case object HeavyCavalry extends BattalionTypeId(3)
case object Longbowmen extends BattalionTypeId(4)
case object Undead extends BattalionTypeId(5)
object BattalionTypeId:
def fromInt(value: Int): BattalionTypeId =
BattalionTypeId.fromOrdinal(value)
// Enable type-safe equality for this type
given CanEqual[BattalionTypeId, BattalionTypeId] = CanEqual.derived
def fromInt(value: Int): BattalionTypeId = value match {
case 0 => LightInfantry
case 1 => HeavyInfantry
case 2 => LightCavalry
case 3 => HeavyCavalry
case 4 => Longbowmen
case 5 => Undead
case _ =>
throw new IllegalArgumentException(
s"Invalid BattalionTypeId value: $value"
)
}
}
@@ -1,333 +0,0 @@
package net.eagle0.eagle.model.state.command.available
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.model.state.command.common.*
import AvailableCommand.*
/** Scala 3 enum representing available command options for a player. */
enum AvailableCommand:
case AlmsAvailable(
foodAvailable: Int,
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
case ApprehendOutlawAvailable(
outlaws: Vector[ResidentOutlaw],
availableBattalionIds: Vector[BattalionId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
case ArmTroopsAvailable(
armamentCosts: Vector[ArmamentCost],
availableBattalions: Vector[BattalionId],
maxArmament: Int,
actingProvinceId: ProvinceId,
availableBattalionTypeIds: Vector[Int]
)
case AttackDecisionAvailable(
armies: Vector[ArmyStats],
actingUnits: Vector[ExpandedCombatUnit],
availableDecisions: Vector[AttackDecisionType],
actingProvinceId: ProvinceId
)
case ControlWeatherAvailable(
options: Vector[TargetProvinceOptions],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
case DeclineQuestAvailable(
actingProvinceId: ProvinceId,
declinableHeroes: Vector[ExpandedUnaffiliatedHero]
)
case DefendAvailable(
suitableBattalionsForHeroes: Map[HeroId, SuitableBattalions],
availableFleeProvinceIds: Vector[ProvinceId],
availableBattalions: Vector[BattalionWithFoodCost],
availableHeroIds: Vector[HeroId],
recommendedUnits: Vector[RecommendedCombatUnit],
actingProvinceId: ProvinceId,
hostileFactionIds: Vector[FactionId],
hostileHeroCount: Int,
hostileTroopCount: Int
)
case DiplomacyAvailable(
options: Vector[DiplomacyOption],
availableHeroIds: Vector[HeroId],
actingProvinceId: ProvinceId,
recommendedHeroId: HeroId
)
case DivineAvailable(
costPerHero: Int,
divinableHeroes: Vector[ExpandedUnaffiliatedHero],
unavailableHeroes: Vector[ExpandedUnaffiliatedHero],
actingProvinceId: ProvinceId
)
case ExileVassalAvailable(
actingProvinceId: ProvinceId,
exilableHeroIds: Vector[HeroId]
)
case FeastAvailable(goldCost: Int, actingProvinceId: ProvinceId)
case FreeForAllDecisionAvailable(
provinceId: ProvinceId,
armies: Vector[ArmyStats],
actingUnits: Vector[ExpandedCombatUnit],
availableDecisions: Vector[AttackDecisionType]
)
case HandleCapturedHeroAvailable(
availableHeroes: Vector[ExpandedCapturedHero],
actingProvinceId: ProvinceId
)
case HandleRiotCrackDownAvailable(
availableHeroIds: Vector[HeroId],
battalionIdsAvailable: Vector[BattalionId],
actingProvinceId: ProvinceId
)
case HandleRiotDoNothingAvailable(actingProvinceId: ProvinceId)
case HandleRiotGiveAvailable(
foodAvailable: Int,
goldAvailable: Int,
actingProvinceId: ProvinceId
)
case HeroGiftAvailable(
eligibleGifts: Vector[EligibleGift],
actingProvinceId: ProvinceId
)
case ImproveAvailable(
availableTypes: Vector[ImprovementType],
availableHeroIds: Vector[HeroId],
recommendedHeroId: HeroId,
actingProvinceId: ProvinceId,
lockedType: Option[ImprovementType]
)
case IssueOrdersAvailable(
currentOrders: Vector[ProvinceOrders],
availableOrders: Vector[ProvinceOrderType],
availableFocusProvinces: Vector[ProvinceId],
actingProvinceId: ProvinceId
)
case ManagePrisonersAvailable(
actingProvinceId: ProvinceId,
prisoners: Vector[PrisonerToManage]
)
case MarchAvailable(
oneProvinceCommands: Vector[MarchCommandFromOneProvince],
actingProvinceId: ProvinceId,
availableTypes: Vector[BattalionType]
)
case OrganizeTroopsAvailable(
troopCosts: Vector[TroopCost],
existingBattalions: Vector[BattalionId],
maxBattalions: Int,
actingProvinceId: ProvinceId,
availableBattalionTypes: Vector[BattalionTypeStatus]
)
case PleaseRecruitMeAvailable(
availableProvinces: Vector[OneProvincePleaseRecruitMe],
actingProvinceId: ProvinceId
)
case ReconAvailable(
availableTargetProvinces: Vector[ProvinceId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
case RecruitHeroesAvailable(
availableHeroes: Vector[ExpandedUnaffiliatedHero],
actingProvinceId: ProvinceId
)
case ResolveAllianceOfferAvailable(offers: Vector[DiplomacyOfferInfo])
case ResolveBreakAllianceAvailable(offers: Vector[DiplomacyOfferInfo])
case ResolveInvitationAvailable(invitations: Vector[DiplomacyOfferInfo])
case ResolveRansomOfferAvailable(offers: Vector[DiplomacyOfferInfo])
case ResolveTruceOfferAvailable(offers: Vector[DiplomacyOfferInfo])
case ResolveTributeAvailable(
demands: Vector[TributeAndFaction],
availableGold: Int,
availableFood: Int,
actingProvinceId: ProvinceId
)
case RestAvailable(actingProvinceId: ProvinceId)
case ReturnAvailable(actingProvinceId: ProvinceId)
case SendSuppliesAvailable(
goldAvailable: Int,
foodAvailable: Int,
availableDestinationProvinceIds: Vector[ProvinceId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
case StartEpidemicAvailable(
options: Vector[StartEpidemicOptions],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
case SuppressBeastsAvailable(
availableHeroIds: Vector[HeroId],
availableBattalionIds: Vector[BattalionId],
actingProvinceId: ProvinceId
)
case SwearBrotherhoodAvailable(
actingProvinceId: ProvinceId,
availableHeroes: Vector[HeroAndBackstory]
)
case TradeAvailable(
foodAvailable: Int,
goldAvailable: Int,
foodBuyPrice: Double,
foodSellPrice: Double,
actingProvinceId: ProvinceId
)
case TrainAvailable(
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId],
recommendedHeroId: HeroId
)
case TravelAvailable(actingProvinceId: ProvinceId)
end AvailableCommand
/** Supporting types for AvailableCommand. */
object AvailableCommand:
case class ResidentOutlaw(
heroId: HeroId,
lastFactionId: Option[FactionId]
)
case class ArmamentCost(
battalionTypeId: Int,
cost: Double
)
case class ArmyStats(
factionId: FactionId,
heroCount: Int,
troopCount: Int
)
case class ExpandedCombatUnit(
heroId: HeroId,
battalionId: Option[BattalionId],
heroName: String,
troopCount: Int
)
case class TargetProvinceOptions(
provinceId: ProvinceId,
controlWeatherTypes: Vector[ControlWeatherType]
)
case class ExpandedUnaffiliatedHero(
heroId: HeroId,
heroName: String,
goldCost: Int
)
case class SuitableBattalions(
battalionIds: Vector[BattalionId]
)
case class BattalionWithFoodCost(
battalionId: BattalionId,
foodCost: Int,
troopCount: Int
)
case class RecommendedCombatUnit(
heroId: HeroId,
battalionId: Option[BattalionId]
)
case class DiplomacyOption(
targetFactionId: FactionId,
optionTypes: Vector[DiplomacyOptionType]
)
case class ExpandedCapturedHero(
heroId: HeroId,
heroName: String,
options: Vector[CapturedHeroOption],
messageId: String,
previousFactionId: FactionId
)
case class EligibleGift(
recipientHeroId: HeroId,
goldAvailable: Int,
recipientProvinceId: ProvinceId
)
case class ProvinceOrders(
provinceId: ProvinceId,
orderType: ProvinceOrderType
)
case class PrisonerToManage(
prisoner: ExpandedUnaffiliatedHero,
availableOptions: Vector[PrisonerManagementOption]
)
case class MarchCommandFromOneProvince(
suitableBattalionsForHeroes: Map[HeroId, SuitableBattalions],
originProvinceId: ProvinceId,
goldCost: Int,
goldAvailable: Int,
foodAvailable: Int,
availableDestinationProvinces: Vector[AvailableDestinationProvince],
availableHeroIds: Vector[HeroId],
availableBattalions: Vector[BattalionWithFoodCost],
recommendedBattleConfig: Option[RecommendedBattleConfig]
)
case class AvailableDestinationProvince(
provinceId: ProvinceId,
requiresRiverCrossing: Boolean
)
case class RecommendedBattleConfig(
units: Vector[RecommendedCombatUnit],
food: Int,
gold: Int
)
case class BattalionType(
typeId: Int,
name: String
)
case class TroopCost(
battalionTypeId: Int,
costPerTroop: Double
)
case class BattalionTypeStatus(
typeId: Int,
meetsRequirements: Boolean,
minimumAgriculture: Int,
minimumEconomy: Int
)
case class OneProvincePleaseRecruitMe(
provinceId: ProvinceId,
availableHeroes: Vector[ExpandedUnaffiliatedHero]
)
case class DiplomacyOfferInfo(
offeringFactionId: FactionId,
targetFactionId: FactionId,
heroId: Option[HeroId]
)
case class TributeAndFaction(
demandingFactionId: FactionId,
goldDemanded: Int,
foodDemanded: Int,
heroCount: Int,
troopCount: Int
)
case class StartEpidemicOptions(
targetProvinceId: ProvinceId
)
case class HeroAndBackstory(
heroId: HeroId,
backstoryTextId: String
)
end AvailableCommand
@@ -1,17 +0,0 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "available",
srcs = ["AvailableCommand.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/state/command:__subpackages__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
],
)

Some files were not shown because too many files have changed in this diff Show More