248 lines
7.4 KiB
C#
248 lines
7.4 KiB
C#
#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<int?>().FirstOrDefault(threshold => threshold > State.Points); }
|
|
}
|
|
|
|
public HeroRunState State { get; }
|
|
|
|
public HeroRuleConfig Config { get; }
|
|
|
|
public event Action<HeroRunState>? 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;
|
|
} |