Add hero runtime
This commit is contained in:
10
godot/scripts/hero/rules/HeroLifeState.cs
Normal file
10
godot/scripts/hero/rules/HeroLifeState.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
|
||||
namespace SideScrollerGame.Hero.Rules;
|
||||
|
||||
public enum HeroLifeState
|
||||
{
|
||||
Alive,
|
||||
Dead,
|
||||
GameOver
|
||||
}
|
||||
1
godot/scripts/hero/rules/HeroLifeState.cs.uid
Normal file
1
godot/scripts/hero/rules/HeroLifeState.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cr5kwlw3ovauj
|
||||
7
godot/scripts/hero/rules/HeroMissionSnapshot.cs
Normal file
7
godot/scripts/hero/rules/HeroMissionSnapshot.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SideScrollerGame.Hero.Rules;
|
||||
|
||||
public sealed record HeroMissionSnapshot(string ActiveDifficultyId, HeroLifeState LifeState, int Level, int Points, int ShieldCharges, int RetryCount, IReadOnlyList<string?> PrimaryWeaponSlots, int SelectedPrimaryWeaponSlotIndex, string CurrentSecondaryWeaponId, string CurrentSpecialWeaponId, int SpecialAmmo, string SquadronMateTypeId, int SquadronMateCount);
|
||||
1
godot/scripts/hero/rules/HeroMissionSnapshot.cs.uid
Normal file
1
godot/scripts/hero/rules/HeroMissionSnapshot.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://chkspscrqv1mi
|
||||
38
godot/scripts/hero/rules/HeroRuleConfig.cs
Normal file
38
godot/scripts/hero/rules/HeroRuleConfig.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SideScrollerGame.Hero.Rules;
|
||||
|
||||
public sealed class HeroRuleConfig
|
||||
{
|
||||
public HeroRuleConfig(int primaryWeaponSlotCount, string basePrimaryWeaponId, string baseSecondaryWeaponId, string defaultSpecialWeaponId, int defaultSpecialAmmo, int maxSquadronMateCount, IReadOnlyList<int> pointThresholds)
|
||||
{
|
||||
PrimaryWeaponSlotCount = primaryWeaponSlotCount;
|
||||
BasePrimaryWeaponId = basePrimaryWeaponId;
|
||||
BaseSecondaryWeaponId = baseSecondaryWeaponId;
|
||||
DefaultSpecialWeaponId = defaultSpecialWeaponId;
|
||||
DefaultSpecialAmmo = defaultSpecialAmmo;
|
||||
MaxSquadronMateCount = maxSquadronMateCount;
|
||||
PointThresholds = pointThresholds;
|
||||
}
|
||||
|
||||
public static HeroRuleConfig CreateDefault()
|
||||
{
|
||||
return new HeroRuleConfig(2, "weapon.primary.basic", "weapon.secondary.vertical", "weapon.special.bomb", 12, 4, [500, 1500, 3000]);
|
||||
}
|
||||
|
||||
public int PrimaryWeaponSlotCount { get; }
|
||||
|
||||
public string BasePrimaryWeaponId { get; }
|
||||
|
||||
public string BaseSecondaryWeaponId { get; }
|
||||
|
||||
public string DefaultSpecialWeaponId { get; }
|
||||
|
||||
public int DefaultSpecialAmmo { get; }
|
||||
|
||||
public int MaxSquadronMateCount { get; }
|
||||
|
||||
public IReadOnlyList<int> PointThresholds { get; }
|
||||
}
|
||||
1
godot/scripts/hero/rules/HeroRuleConfig.cs.uid
Normal file
1
godot/scripts/hero/rules/HeroRuleConfig.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dnk3adcvb0xma
|
||||
16
godot/scripts/hero/rules/HeroRuleResult.cs
Normal file
16
godot/scripts/hero/rules/HeroRuleResult.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
|
||||
namespace SideScrollerGame.Hero.Rules;
|
||||
|
||||
public sealed record HeroRuleResult(bool Succeeded, string Message, HeroRunState State)
|
||||
{
|
||||
public static HeroRuleResult Success(string message, HeroRunState state)
|
||||
{
|
||||
return new HeroRuleResult(true, message, state);
|
||||
}
|
||||
|
||||
public static HeroRuleResult Failure(string message, HeroRunState state)
|
||||
{
|
||||
return new HeroRuleResult(false, message, state);
|
||||
}
|
||||
}
|
||||
1
godot/scripts/hero/rules/HeroRuleResult.cs.uid
Normal file
1
godot/scripts/hero/rules/HeroRuleResult.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bvkra6itukg8p
|
||||
79
godot/scripts/hero/rules/HeroRunState.cs
Normal file
79
godot/scripts/hero/rules/HeroRunState.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SideScrollerGame.Hero.Rules;
|
||||
|
||||
public sealed class HeroRunState
|
||||
{
|
||||
public HeroRunState(string activeDifficultyId, int shieldCharges, int retryCount, HeroRuleConfig config)
|
||||
{
|
||||
ActiveDifficultyId = activeDifficultyId;
|
||||
ShieldCharges = shieldCharges;
|
||||
RetryCount = retryCount;
|
||||
m_PrimaryWeaponSlots = Enumerable.Repeat<string?>(null, config.PrimaryWeaponSlotCount).ToList();
|
||||
ResetInventory(config);
|
||||
LastStateChange = "Hero ready";
|
||||
}
|
||||
|
||||
public int FindEmptyPrimarySlot()
|
||||
{
|
||||
return m_PrimaryWeaponSlots.FindIndex(string.IsNullOrWhiteSpace);
|
||||
}
|
||||
|
||||
public void ReplacePrimaryWeaponSlot(int index, string? weaponId)
|
||||
{
|
||||
m_PrimaryWeaponSlots[index] = weaponId;
|
||||
}
|
||||
|
||||
public void ResetInventory(HeroRuleConfig config)
|
||||
{
|
||||
for (int i = 0; i < m_PrimaryWeaponSlots.Count; i++)
|
||||
{
|
||||
m_PrimaryWeaponSlots[i] = null;
|
||||
}
|
||||
|
||||
if (m_PrimaryWeaponSlots.Count > 0)
|
||||
{
|
||||
m_PrimaryWeaponSlots[0] = config.BasePrimaryWeaponId;
|
||||
}
|
||||
|
||||
SelectedPrimaryWeaponSlotIndex = 0;
|
||||
CurrentSecondaryWeaponId = config.BaseSecondaryWeaponId;
|
||||
CurrentSpecialWeaponId = config.DefaultSpecialWeaponId;
|
||||
SpecialAmmo = config.DefaultSpecialAmmo;
|
||||
SquadronMateTypeId = string.Empty;
|
||||
SquadronMateCount = 0;
|
||||
}
|
||||
|
||||
public string ActiveDifficultyId { get; set; }
|
||||
|
||||
public HeroLifeState LifeState { get; set; } = HeroLifeState.Alive;
|
||||
|
||||
public int Level { get; set; } = 1;
|
||||
|
||||
public int Points { get; set; }
|
||||
|
||||
public int ShieldCharges { get; set; }
|
||||
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
public IReadOnlyList<string?> PrimaryWeaponSlots => m_PrimaryWeaponSlots;
|
||||
|
||||
public int SelectedPrimaryWeaponSlotIndex { get; set; }
|
||||
|
||||
public string CurrentSecondaryWeaponId { get; set; } = string.Empty;
|
||||
|
||||
public string CurrentSpecialWeaponId { get; set; } = string.Empty;
|
||||
|
||||
public int SpecialAmmo { get; set; }
|
||||
|
||||
public string SquadronMateTypeId { get; set; } = string.Empty;
|
||||
|
||||
public int SquadronMateCount { get; set; }
|
||||
|
||||
public string LastStateChange { get; set; } = string.Empty;
|
||||
|
||||
private readonly List<string?> m_PrimaryWeaponSlots;
|
||||
}
|
||||
1
godot/scripts/hero/rules/HeroRunState.cs.uid
Normal file
1
godot/scripts/hero/rules/HeroRunState.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bet8wlbxg4xst
|
||||
248
godot/scripts/hero/rules/HeroRuntimeService.cs
Normal file
248
godot/scripts/hero/rules/HeroRuntimeService.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
#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;
|
||||
}
|
||||
1
godot/scripts/hero/rules/HeroRuntimeService.cs.uid
Normal file
1
godot/scripts/hero/rules/HeroRuntimeService.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://hfyvh0qvd7uv
|
||||
Reference in New Issue
Block a user