#nullable enable using System; using System.Linq; using SideScrollerGame.Content; using SideScrollerGame.Content.Definitions; namespace SideScrollerGame.Hero.Rules; public sealed class HeroRuntimeService { public HeroRuntimeService(ContentRegistry registry, HeroRuleConfig config, string difficultyId) { m_Registry = registry; Config = config; m_ActiveDifficulty = ResolveDifficulty(difficultyId); State = new HeroRunState(m_ActiveDifficulty.Id, m_ActiveDifficulty.HeroStartingShieldCharges, m_ActiveDifficulty.HeroRetryCount, config); } public HeroRuleResult ApplyHit(bool invulnerable) { if (State.LifeState != HeroLifeState.Alive) { return Fail("Hero is not alive"); } if (invulnerable) { return Succeed("Hero ignored hit while invulnerable"); } if (State.ShieldCharges > 0) { State.ShieldCharges--; return Succeed($"Hero hit: shield {State.ShieldCharges}"); } return EnterDeathState("Hero killed"); } public HeroRuleResult Kill() { if (State.LifeState != HeroLifeState.Alive) { return Fail("Hero is not alive"); } State.ShieldCharges = 0; return EnterDeathState("Hero killed"); } public HeroRuleResult Rebirth() { if (State.LifeState != HeroLifeState.Dead) { return Fail("Hero is not waiting for rebirth"); } if (State.RetryCount <= 0) { State.LifeState = HeroLifeState.GameOver; return Succeed("Game over"); } State.RetryCount--; State.LifeState = HeroLifeState.Alive; State.ShieldCharges = m_ActiveDifficulty.HeroStartingShieldCharges; State.ResetInventory(Config); return Succeed("Hero reborn"); } public HeroRuleResult AddPoints(int points) { if (points <= 0) { return Fail($"Invalid point amount {points}"); } State.Points += points; int targetLevel = CalculateLevel(State.Points); int gainedLevels = Math.Max(0, targetLevel - State.Level); State.Level = targetLevel; State.ShieldCharges += gainedLevels; return Succeed(gainedLevels == 0 ? $"Points added: {points}" : $"Points added: {points}; level {State.Level}"); } public HeroRuleResult SetLevel(int level) { if (level < 1) { return Fail($"Invalid hero level {level}"); } State.Level = level; return Succeed($"Hero level set to {level}"); } public HeroRuleResult AddShieldCharge(int amount) { if (amount <= 0) { return Fail($"Invalid shield amount {amount}"); } State.ShieldCharges += amount; return Succeed($"Shield added: {State.ShieldCharges}"); } public HeroRuleResult RemoveShieldCharge(int amount) { if (amount <= 0) { return Fail($"Invalid shield amount {amount}"); } State.ShieldCharges = Math.Max(0, State.ShieldCharges - amount); return Succeed($"Shield removed: {State.ShieldCharges}"); } public HeroRuleResult SetRetryCount(int retryCount) { if (retryCount < 0) { return Fail($"Invalid retry count {retryCount}"); } State.RetryCount = retryCount; return Succeed($"Retries set to {retryCount}"); } public HeroRuleResult TogglePrimaryWeaponSlot() { if (State.PrimaryWeaponSlots.Count == 0) { return Fail("No primary weapon slots"); } State.SelectedPrimaryWeaponSlotIndex = (State.SelectedPrimaryWeaponSlotIndex + 1) % State.PrimaryWeaponSlots.Count; return Succeed($"Primary slot {State.SelectedPrimaryWeaponSlotIndex}"); } public HeroRuleResult ApplyPrimaryWeaponPickup(string weaponId) { if (!m_Registry.Weapons.ContainsKey(weaponId)) { return Fail($"Unknown primary weapon '{weaponId}'"); } int targetSlot = State.FindEmptyPrimarySlot(); if (targetSlot < 0) { targetSlot = State.SelectedPrimaryWeaponSlotIndex; } State.ReplacePrimaryWeaponSlot(targetSlot, weaponId); return Succeed($"Primary weapon {weaponId} in slot {targetSlot}"); } public HeroRuleResult ApplySecondaryWeaponPickup(string weaponId) { if (!m_Registry.Weapons.ContainsKey(weaponId)) { return Fail($"Unknown secondary weapon '{weaponId}'"); } State.CurrentSecondaryWeaponId = weaponId; return Succeed($"Secondary weapon {weaponId}"); } public HeroRuleResult AddSpecialAmmo(int amount) { State.SpecialAmmo = Math.Max(0, State.SpecialAmmo + amount); return Succeed($"Special ammo {State.SpecialAmmo}"); } public HeroRuleResult ApplySquadronMatePickup(string squadronMateTypeId) { if (!m_Registry.SquadronMateTypes.ContainsKey(squadronMateTypeId)) { return Fail($"Unknown squadron mate '{squadronMateTypeId}'"); } State.SquadronMateTypeId = squadronMateTypeId; State.SquadronMateCount = Math.Min(Config.MaxSquadronMateCount, State.SquadronMateCount + 1); return Succeed($"Squadron mates {State.SquadronMateCount} {squadronMateTypeId}"); } public HeroRuleResult ClearInventory() { State.ResetInventory(Config); return Succeed("Hero inventory cleared"); } public HeroMissionSnapshot CreateMissionSnapshot() { return new HeroMissionSnapshot(State.ActiveDifficultyId, State.LifeState, State.Level, State.Points, State.ShieldCharges, State.RetryCount, State.PrimaryWeaponSlots.ToList(), State.SelectedPrimaryWeaponSlotIndex, State.CurrentSecondaryWeaponId, State.CurrentSpecialWeaponId, State.SpecialAmmo, State.SquadronMateTypeId, State.SquadronMateCount); } public int? NextPointThreshold { get { return Config.PointThresholds.Cast().FirstOrDefault(threshold => threshold > State.Points); } } public HeroRunState State { get; } public HeroRuleConfig Config { get; } public event Action? StateChanged; private HeroRuleResult EnterDeathState(string message) { State.ShieldCharges = 0; State.ResetInventory(Config); State.LifeState = State.RetryCount > 0 ? HeroLifeState.Dead : HeroLifeState.GameOver; return Succeed(State.LifeState == HeroLifeState.GameOver ? "Game over" : message); } private HeroRuleResult Succeed(string message) { State.LastStateChange = message; StateChanged?.Invoke(State); return HeroRuleResult.Success(message, State); } private HeroRuleResult Fail(string message) { State.LastStateChange = message; return HeroRuleResult.Failure(message, State); } private int CalculateLevel(int points) { return 1 + Config.PointThresholds.Count(threshold => points >= threshold); } private DifficultyDefinition ResolveDifficulty(string difficultyId) { if (m_Registry.Difficulties.TryGetValue(difficultyId, out DifficultyDefinition? difficulty)) { return difficulty; } return m_Registry.DifficultyDefinitions.First(); } private readonly ContentRegistry m_Registry; private readonly DifficultyDefinition m_ActiveDifficulty; }