private static ActionChosen AICombatActionMeleeOnly(EcsRegistrar rgs, long globalId, long attackerId, List <long> battlefieldEntityIds) { var action = new ActionChosen { Action = Vals.CombatAction.DoNothing, AgentId = attackerId }; var naturalWeaponsMap = rgs.GetPartSingle <Parts.NaturalWeaponsMap>(globalId); var targetIdsToWeights = battlefieldEntityIds .Select(id => new { Id = id, Agent = rgs.GetPartSingleOrDefault <Parts.Agent>(id) }) .Where(a => a.Id != attackerId && a.Agent != null && !Vals.CombatStatusTag.CombatTerminalStatuses.Intersect(a.Agent.CombatStatusTags).Any()) .ToDictionary(a => a.Id, a => 0); var attackerFaction = rgs.GetPartSingle <Parts.Faction>(attackerId); var factionInteractions = rgs.GetPartSingle <Parts.FactionInteractionSet>(globalId); //eventually we have to figure out how to weight faction relative to everything else. var possibleTargetIds = targetIdsToWeights.Keys.ToArray(); foreach (var possibleTargetId in possibleTargetIds) { var targetFaction = rgs.GetPartSingle <Parts.Faction>(possibleTargetId); targetIdsToWeights[possibleTargetId] = factionInteractions.Interactions .SingleOrDefault(i => i.SourceFaction == attackerFaction.CurrentPublicFaction && i.TargetFaction == targetFaction.CurrentPublicFaction) ?.Disposition ?? 0; } var minWeight = targetIdsToWeights.Values.Any() ? targetIdsToWeights.Values.Min() : 0; if (minWeight < 0) { var targetId = targetIdsToWeights.First(kvp => kvp.Value == minWeight).Key; var attackerAnatomy = rgs.GetPartSingle <Parts.Anatomy>(attackerId); //MWCTODO++: yaaaah this is way too human centric. var weaponId = attackerAnatomy.SlotsEquipped.GetValueOrDefault(Vals.BodySlots.WieldHandRight, 0); if (weaponId == 0) { weaponId = attackerAnatomy.SlotsEquipped.GetValueOrDefault(Vals.BodySlots.WieldHandLeft, 0); } if (weaponId == 0) { var nwmap = naturalWeaponsMap.NaturalWeaponSets.GetValueOrDefault(attackerAnatomy.NaturalWeaponsCategory, null); if (nwmap != null && nwmap.Keys.Any()) { //MWCTODO: and this isn't great. weaponId = nwmap.First().Value; } } action.Action = Vals.CombatAction.AttackWeaponMelee; action.TargetEntityId = targetId; action.WeaponEntityId = weaponId; } return(action); }
internal static Output Unequip(EcsRegistrar rgs, long unequipperId, long gearId) { var output = new Output { Category = "Text" }; var unequipperEntityName = rgs.GetPartSingle <Parts.EntityName>(unequipperId); var unequipperAnatomy = rgs.GetPartSingle <Parts.Anatomy>(unequipperId); var gearEntityName = rgs.GetPartSingle <Parts.EntityName>(gearId); bool unequipped = false; foreach (var slot in unequipperAnatomy.SlotsEquipped) { if (slot.Value == gearId) { unequipperAnatomy.SlotsEquipped[slot.Key] = 0L; unequipped = true; } } output.Data = unequipped ? output.Data = $"{unequipperEntityName.ProperName} removes the {gearEntityName.GeneralName}." : output.Data = $"{unequipperEntityName.ProperName} doesn't have the {gearEntityName.GeneralName} equipped and can't remove it."; return(output); }
//internal static List<Messages.Combat> ProcessAgentAction(EcsRegistrar rgs, long agentId, long? targetId, string action) internal static List <Messages.Combat> ProcessAgentAction(EcsRegistrar rgs, long agentId, ActionChosen agentAction) { if (Stances.Contains(agentAction.Action)) { return(ApplyStance(rgs, agentId, agentAction.Action)); } else if (agentAction.Action == Vals.CombatAction.SwitchToAI) { return(SwitchToAI(rgs, agentId)); } else if (agentAction.Action == Vals.CombatAction.AttackWeaponMelee) { return(ResolveSingleTargetMelee(rgs, agentId, agentAction.WeaponEntityId.Value, agentAction.TargetEntityId.Value)); } else if (agentAction.Action == Vals.CombatAction.DoNothing) { var agentNames = rgs.GetPartSingle <Parts.EntityName>(agentId); return(new List <Messages.Combat> { new Messages.Combat { Tick = rgs.NewId(), ActorId = agentId, ActorAction = $"{agentNames.ProperName} stands still and looks around in confusion. Probably some programmer failed to give him a good action in this scenario." } }); } else { throw new ArgumentException($"Action '{agentAction.Action}' not supported."); } }
internal static long Agent(EcsRegistrar rgs, string agentBlueprintName, string armorBlueprintName, string weaponBlueprintName) { long agentId = Blueprinter.GetEntityFromBlueprint(rgs, agentBlueprintName); long?armorId = string.IsNullOrEmpty(armorBlueprintName) ? (long?)null : Blueprinter.GetEntityFromBlueprint(rgs, armorBlueprintName); long?weaponId = string.IsNullOrEmpty(weaponBlueprintName) ? (long?)null : Blueprinter.GetEntityFromBlueprint(rgs, weaponBlueprintName); var anatomy = rgs.GetPartSingle <Parts.Anatomy>(agentId); if (!anatomy.SlotsEquipped.Any()) { var bodySlotsAsEnum = Vals.BodyPlan.BodyPlanToSlots[anatomy.BodyPlan]; anatomy.SlotsEquipped = bodySlotsAsEnum.GetUniqueFlags().ToDictionary(s => (Vals.BodySlots)s, s => 0L); } if (armorId.HasValue) { var armorResults = Sys.Anatomy.Equip(rgs, agentId, armorId.Value); } if (weaponId.HasValue) { var wieldResults = Sys.Anatomy.Equip(rgs, agentId, weaponId.Value); } return(agentId); }
internal static Dictionary <string, int> GetAdjustedSkills(EcsRegistrar rgs, IEnumerable <long> entityIds) { return(entityIds .Select(id => GetAdjustedSkills(rgs, id)) .SelectMany(d => d).ToLookup(dicts => dicts.Key, dicts => dicts.Value) .ToDictionary(l => l.Key, l => l.Sum())); }
internal static List <Output> GetEndOfBattleOutputs(EcsRegistrar rgs, List <long> battlefieldEntityIds) { var outputs = new List <Output>(); var nonCombatTerminals = battlefieldEntityIds .Select(id => new { Id = id, Agent = rgs.GetPartSingleOrDefault <Parts.Agent>(id) }) .Where(a => a.Agent != null && !a.Agent.CombatStatusTags.Intersect(Vals.CombatStatusTag.CombatTerminalStatuses).Any()); var distinctNctFactions = nonCombatTerminals .Select(a => rgs.GetPartSingleOrDefault <Parts.Faction>(a.Id)?.CurrentPublicFaction ?? Vals.FactionName.Unaffiliated) .Distinct() .ToList(); //this needs to hook into the narrator. var output = new Output() { Category = OutputCategory.Text }; if (!distinctNctFactions.Any()) { output.Data = "Somehow everyone died. Oops!"; outputs.Add(output); } else if (distinctNctFactions.Count == 1) { output.Data = $"Combat is over. {distinctNctFactions.First()} win."; outputs.Add(output); } return(outputs); }
/// <summary> /// Write (or overwrite) a blueprint for an entity. Only for use during programming, not during play. /// </summary> internal static void WriteBlueprint(EcsRegistrar rgs, long entityId, string blueprintName) { string revisedPartsJson = GetBlueprintForEntity(rgs, entityId); //we do not want to write a blueprint to the executable folder, that's useless. File.WriteAllText(_sourceCodeFolder + blueprintName + _blueprintSuffix, revisedPartsJson); }
internal static ActionChosen GetAgentAICombatAction(EcsRegistrar rgs, long globalId, string attackerAI, long attackerId, List <long> battlefieldEntityIds) { if (attackerAI == Vals.AI.MeleeOnly) { return(AICombatActionMeleeOnly(rgs, globalId, attackerId, battlefieldEntityIds)); } throw new ArgumentException($"Attacker AI {attackerAI} not supported."); }
internal static WeaponBundle GetWeaponBundle(EcsRegistrar rgs, long weaponId) { return(new WeaponBundle { EntityName = rgs.GetPartSingle <Parts.EntityName>(weaponId), PhysicalObject = rgs.GetPartSingle <Parts.PhysicalObject>(weaponId), Damagers = rgs.GetParts <Parts.Damager>(weaponId).ToList() }); }
internal static AgentBundle GetAgentBundle(EcsRegistrar rgs, long agentId) { return(new AgentBundle { Agent = rgs.GetPartSingle <Parts.Agent>(agentId), Anatomy = rgs.GetPartSingle <Parts.Anatomy>(agentId), EntityName = rgs.GetPartSingle <Parts.EntityName>(agentId), PhysicalObject = rgs.GetPartSingle <Parts.PhysicalObject>(agentId) }); }
internal static Dictionary <string, int> GetAdjustedSkills(EcsRegistrar rgs, long entityId) { var skillDicts = rgs.GetParts <Parts.Skillset>(entityId).Select(p => p.Skills); var modifierDicts = rgs.GetParts <Parts.SkillsModifier>(entityId).Select(p => p.SkillDeltas); return(skillDicts.Union(modifierDicts) .SelectMany(d => d) .ToLookup(dicts => dicts.Key, dicts => dicts.Value) .ToDictionary(l => l.Key, l => l.Sum())); }
/// <summary> /// Overwrite an entity name with the values from a new EntityName part. Null values in the new part are ignored. /// The original part is kept, and the new part is discarded after overwriting. /// </summary> internal static void Overwrite(EcsRegistrar rgs, long entityId, Parts.EntityName newNames) { var currentNames = rgs.GetParts <Parts.EntityName>(entityId).Single(); currentNames.LongDescription = newNames.LongDescription ?? currentNames.LongDescription; currentNames.ObscuredName = newNames.ObscuredName ?? currentNames.ObscuredName; currentNames.Pronoun = newNames.Pronoun ?? currentNames.Pronoun; currentNames.ProperName = newNames.ProperName ?? currentNames.ProperName; currentNames.ShortDescription = newNames.ShortDescription ?? currentNames.ShortDescription; }
private static List <Messages.Combat> SwitchToAI(EcsRegistrar rgs, long agentId) { var ai = rgs.GetPartSingle <Parts.Agent>(agentId); ai.ActiveCombatAI = Vals.AI.MeleeOnly; return(new List <Messages.Combat> { new Messages.Combat { Tick = rgs.NewId(), ActorId = agentId, ActorAction = "Switched to AI." } }); }
internal static IEnumerable <List <Output> > RunBattlefield(EcsRegistrar rgs, long globalId, long battlefieldId, Func <CombatChoicesAndTargets, ActionChosen> receivePlayerInput) { while (true) { var battlefieldContainer = rgs.GetPartSingle <Parts.Container>(battlefieldId); var battlefieldEntityIds = battlefieldContainer.EntityIds.ToList(); foreach (var agentId in battlefieldEntityIds) { var battlefieldAgent = rgs.GetPartSingleOrDefault <Parts.Agent>(agentId); if (battlefieldAgent == null) { continue; } if (battlefieldAgent.CombatStatusTags.Intersect(Vals.CombatStatusTag.CombatTerminalStatuses).Any()) { continue; } ActionChosen agentActionChosen; if (battlefieldAgent.ActiveCombatAI == Vals.AI.PlayerChoice) { var possibleActions = Agent.GetPossibleActions(rgs, globalId, agentId, battlefieldEntityIds); agentActionChosen = receivePlayerInput(possibleActions); } else { agentActionChosen = Agent.GetAgentAICombatAction(rgs, globalId, battlefieldAgent.ActiveCombatAI, agentId, battlefieldEntityIds); } var results = Combat.ProcessAgentAction(rgs, agentId, agentActionChosen); var output = Narrator.OutputForCombatMessages(rgs, results); yield return(output); } var endOfBattleOutputs = GetEndOfBattleOutputs(rgs, battlefieldEntityIds); if (endOfBattleOutputs.Any()) { yield return(endOfBattleOutputs); yield break; } } }
internal static long CreateBattlefield(EcsRegistrar rgs, IEnumerable <long> battlefieldEntityIds) { var battlefieldId = rgs.CreateEntity(); var battlefieldContainer = new Parts.Container { Description = "battlefield", Tag = Vals.ContainerTag.Battlefield, ItemsAreVisible = true, Preposition = "on" }; battlefieldEntityIds = battlefieldEntityIds ?? new long[] { }; foreach (long entityId in battlefieldEntityIds) { battlefieldContainer.EntityIds.Add(entityId); } rgs.AddPart(battlefieldId, battlefieldContainer); return(battlefieldId); }
internal static List <Messages.Combat> ApplyStance(EcsRegistrar rgs, long agentId, string stance) { var result = new Messages.Combat { Tick = rgs.NewId(), ActorId = agentId }; var stances = rgs.GetParts <Parts.SkillsModifier>(agentId).Where(p => Stances.Contains(p.Tag)); rgs.RemoveParts(agentId, stances); result.ActorAction = stance; if (stance == Vals.CombatAction.StanceAggressive) { rgs.AddPart(agentId, new Parts.SkillsModifier { Tag = Vals.CombatAction.StanceAggressive, SkillDeltas = new Dictionary <string, int> { [Vals.EntitySkillPhysical.Melee] = 1, [Vals.EntitySkillPhysical.Dodge] = -1 } }); } else if (stance == Vals.CombatAction.StanceDefensive) { rgs.AddPart(agentId, new Parts.SkillsModifier { Tag = Vals.CombatAction.StanceDefensive, SkillDeltas = new Dictionary <string, int> { [Vals.EntitySkillPhysical.Melee] = -1, [Vals.EntitySkillPhysical.Dodge] = 1 } }); } else if (stance == Vals.CombatAction.StanceStandGround) { //default stance, so the removal of previous ones means we're done. } else { throw new ArgumentException($"Invalid stance {stance}."); } return(new List <Messages.Combat> { result }); }
//as Parts evolve, the blueprints won't keep up. different ways we could handle, one might be to cycle through // every blueprint in the folder and load/rewrite it, that will at least get new props in as nulls. /// <summary> /// Get a blueprint for an entity. Blueprint part Ids are all forced to 0 (to reduce possible /// confusion when reading by hand). This method has a limited exception to the no-references rule /// --an entity with Container or Anatomy parts will automatically have the entity related values emptied /// before writing (which makes them safe). /// </summary> internal static string GetBlueprintForEntity(EcsRegistrar rgs, long entityId) { //make a copy before changing anything, just in case the user wants to continue working with original. string originalPartsJson = rgs.SerializeEntityParts(entityId); var revisedPartsList = rgs.DeserializeParts(originalPartsJson); foreach (var part in revisedPartsList) { part.Id = 0; if (part is Parts.Container) { ((Parts.Container)part).EntityIds.Clear(); } if (part is Parts.Anatomy) { var anatomy = (Parts.Anatomy)part; //anatomy.SlotsEquipped = GetClearedAnatomySlotsEquipped(anatomy.SlotsEquipped); anatomy.SlotsEquipped = anatomy.SlotsEquipped.ToDictionary(k => k.Key, v => 0L); } } return(rgs.SerializeEntityParts(revisedPartsList)); }
//internal static HashSet<long> GetEntityIdsFromFirstTagged(EcsRegistrar rgs, long ownerId, string tag) //{ // var container = rgs.GetParts<Parts.Container>(ownerId).FirstOrDefault(c => c.Tag == tag); // return container?.EntityIds ?? new HashSet<long>(); //} internal static AlterContainerContentsResultsMessage AddOrRemoveInventory(Action action, EcsRegistrar rgs, long ownerId, long itemAddOrRemoveId) { var containerPart = rgs.GetParts <Parts.Container>(ownerId).Single(c => c.Tag == Vals.ContainerTag.Inventory); var ids = containerPart.EntityIds; if (action == Action.AddEntity && !ids.Contains(itemAddOrRemoveId)) { ids.Add(itemAddOrRemoveId); } else if (action == Action.RemoveEntity && ids.Contains(itemAddOrRemoveId)) { ids.Remove(itemAddOrRemoveId); } else { return(new AlterContainerContentsResultsMessage { Succeeded = false, Output = "Can't add what already exists, or remove what does not exist." }); } var ownerNamePart = rgs.GetParts <Parts.EntityName>(ownerId).Single(); var itemNamePart = rgs.GetParts <Parts.EntityName>(itemAddOrRemoveId).SingleOrDefault(); string verbPhrase = (action == Action.AddEntity) ? $"adds {itemNamePart.GeneralName} to" : $"removes {itemNamePart.GeneralName} from"; string actionPhrase = $"{ownerNamePart.ProperName} {verbPhrase} {ownerNamePart.Pronoun} {containerPart.Tag}."; return(new AlterContainerContentsResultsMessage { Succeeded = true, Output = actionPhrase }); }
internal static AlterContainerContentsResultsMessage RemoveFromInventory(EcsRegistrar rgs, long ownerId, long toRemoveId, long?containerId = null) { return(AddOrRemoveInventory(Action.AddEntity, rgs, ownerId, toRemoveId)); }
internal static long GetEntityFromBlueprint(EcsRegistrar rgs, string blueprintName) { var blueprintJson = File.ReadAllText(_blueprintFolder + blueprintName + _blueprintSuffix); return(rgs.CreateEntity(blueprintJson)); }
// do we want to pass in the random function for better testing? not sure, but let's try it. public static List <Messages.Combat> ResolveSingleTargetMelee(EcsRegistrar rgs, long attackerId, long weaponId, long targetId, Func <int> randoRange7) { var attacker = Bundle.GetAgentBundle(rgs, attackerId); if (weaponId == 0) { return(new List <Messages.Combat> { new Messages.Combat { Tick = rgs.NewId(), ActorId = attackerId, ActorAction = $"{attacker.EntityName.ProperName} starts to attack, but merely spins around in place, because its weaponId is 0. SHOULD NOT EVER HAPPEN." } }); } var weapon = Bundle.GetWeaponBundle(rgs, weaponId); //this is only temporary, later we'll have a clock/scheduler. var msg = new Messages.Combat { Tick = rgs.NewId(), ActorId = attackerId, TargetId = targetId, ActorAction = Vals.CombatAction.AttackWeaponMelee, TargetAction = Vals.CombatAction.Dodge, AttackVerb = weapon.PhysicalObject.MeleeVerb }; var attackerAdjustedSkills = Skills.GetAdjustedSkills(rgs, attackerId); var targetAdjustedSkills = Skills.GetAdjustedSkills(rgs, targetId); int attackRoll = randoRange7(); decimal attackCritMultiplier = GetDamageMultiplierFromRange7(attackRoll); int attackerMeleeSkill = attackerAdjustedSkills[Vals.EntitySkillPhysical.Melee]; int attackerAdjustedRoll = attackRoll + attackerMeleeSkill; msg.ActorCritical = attackCritMultiplier.ToString(); msg.ActorAdjustedSkill = attackerMeleeSkill; msg.ActorAdjustedRoll = attackerAdjustedRoll; int targetDodgeRoll = randoRange7(); int targetDodgeSkill = targetAdjustedSkills[Vals.EntitySkillPhysical.Dodge]; //dodge is a difficult skill and always reduced by 1. int targetAdjustedDodgeRoll = Math.Max(0, targetDodgeRoll + targetDodgeSkill - 1); msg.TargetAdjustedSkill = targetDodgeSkill; msg.TargetAdjustedRoll = targetAdjustedDodgeRoll; int netAttack = Math.Max(0, attackerAdjustedRoll - targetAdjustedDodgeRoll); msg.NetActorRoll = netAttack; //a good attack gets whatever the attack crit damage multiplier is, a barely-attack gets a .5, and less gets a 0. decimal finalAttackMultiplier = (netAttack > 1) ? attackCritMultiplier : (netAttack == 1) ? 0.5m : 0m; var target = Bundle.GetAgentBundle(rgs, targetId); //heh, that distinct is important, since the same armor can now occupy multiple slots. var targetEquipmentIds = target.Anatomy.SlotsEquipped.Where(s => s.Value != 0).Select(s => s.Value).Distinct().ToList(); // ReSharper disable once ConvertClosureToMethodGroup var targetDamagePreventers = targetEquipmentIds.SelectMany(eid => rgs.GetParts <Parts.DamagePreventer>(eid)).ToList(); var damageAttempted = weapon.Damagers.Sum(d => d.DamageAmount) * finalAttackMultiplier; var damagePrevented = target.PhysicalObject.DefaultDamageThreshold + targetDamagePreventers.Sum(p => p.DamageThreshold); msg.AttemptedDamage = damageAttempted; //so we apply crit/attack multipliers first, then we subtract damage prevention, then we apply default damage multiplier and armor multipliers. whew! var damageDealt = Math.Max(0, (damageAttempted - damagePrevented) * target.PhysicalObject.DefaultDamageMultiplier * (targetDamagePreventers.Select(p => p.DamageMultiplier).Aggregate(1m, (p1, p2) => p1 * p2))); msg.NetDamage = damageDealt; target.PhysicalObject.Condition = target.PhysicalObject.Condition - (int)damageDealt; msg.TargetCondition = target.PhysicalObject.Condition; if (target.PhysicalObject.Condition <= 0) { if (!target.Agent.CombatStatusTags.Contains(Vals.CombatStatusTag.Dead)) { target.Agent.CombatStatusTags.Add(Vals.CombatStatusTag.Dead); msg.NewTargetCombatTags.Add(Vals.CombatStatusTag.Dead); } } return(new List <Messages.Combat> { msg }); }
public static List <Messages.Combat> ResolveSingleTargetMelee(EcsRegistrar rgs, long attackerId, long weaponId, long targetId) { return(ResolveSingleTargetMelee(rgs, attackerId, weaponId, targetId, Rando.GetRange7)); }
public static CombatChoicesAndTargets GetPossibleActions(EcsRegistrar rgs, long globalId, long agentId, List <long> battlefieldEntityIds) { var possibleCombatActions = new CombatChoicesAndTargets(); var naturalWeaponsMap = rgs.GetPartSingle <Parts.NaturalWeaponsMap>(globalId); var agentAnatomy = rgs.GetPartSingle <Parts.Anatomy>(agentId); //var agentWieldingDisabled = agentAnatomyModifers // .Where(m => m.EquipmentSlotDisabled == Vals.BodyEquipmentSlots.WieldObjectAppendage) // .Select(m => m.EquipmentSlotDisabled) // .ToList(); var agentWieldedIds = agentAnatomy.SlotsEquipped .Where(s => s.Key == Vals.BodySlots.WieldHandLeft || s.Key == Vals.BodySlots.WieldHandRight || s.Key == Vals.BodySlots.WieldTwoHanded) .Select(s => s.Value) .ToList(); var agentWieldedIdToWeaponEntityName = agentWieldedIds .Where(id => id != 0) .Distinct() .Select(id => new { Id = id, Name = rgs.GetPartSingle <Parts.EntityName>(id) }) .ToDictionary(n => n.Id, n => n.Name); var agentNaturalWeaponSlots = naturalWeaponsMap.NaturalWeaponSets[agentAnatomy.NaturalWeaponsCategory]; //MWCTODO: some magic happens here and we find out a wristblade or a laser eye has been equipped, and add that to the slots. // also any quickslots that might have attack items in them. we want quickslots. maybe a wristblade or a laser eye automatically adds a new, fixed quickslot for itself? i like this idea upon first think! var recordedIds = new HashSet <long>(); foreach (var slot in agentNaturalWeaponSlots.Keys) { var weaponId = agentAnatomy.SlotsEquipped[slot]; if (weaponId == 0L) { weaponId = agentNaturalWeaponSlots[slot]; } if (recordedIds.Contains(weaponId)) { continue; } var weapon = Bundle.GetWeaponBundle(rgs, weaponId); var choice = new AgentActionChoice { AgentId = agentId, Category = Vals.CombatCategory.TopLevelAction, Description = $"Melee {weapon.EntityName.GeneralName}", Action = Vals.CombatAction.AttackWeaponMelee, WeaponBodySlot = slot, WeaponEntityId = weaponId, NextCategory = Vals.CombatCategory.MeleeTarget }; possibleCombatActions.Choices.Add(choice); recordedIds.Add(weaponId); } foreach (long id in battlefieldEntityIds) { if (id == agentId) { continue; } var entityName = rgs.GetPartSingleOrDefault <Parts.EntityName>(id); var entityAgent = rgs.GetPartSingleOrDefault <Parts.Agent>(id); if (entityName == null || entityAgent == null) { continue; } if (entityAgent.CombatStatusTags.Intersect(Vals.CombatStatusTag.CombatTerminalStatuses).Any()) { continue; } possibleCombatActions.MeleeTargets.Add(id, entityName.ProperName); } //add stances possibleCombatActions.Choices.Add(new AgentActionChoice { AgentId = agentId, Category = Vals.CombatCategory.AllStances, Description = "Stance (Defensive)", Action = Vals.CombatAction.StanceDefensive }); possibleCombatActions.Choices.Add(new AgentActionChoice { AgentId = agentId, Category = Vals.CombatCategory.AllStances, Description = "Stance (Stand Ground)", Action = Vals.CombatAction.StanceStandGround }); possibleCombatActions.Choices.Add(new AgentActionChoice { AgentId = agentId, Category = Vals.CombatCategory.AllStances, Description = "Stance (Aggressive)", Action = Vals.CombatAction.StanceAggressive }); //add top-level choices that we know are present: note that we already did NextCategory = primary-melee because the description there is context dependent. possibleCombatActions.Choices.Add(new AgentActionChoice { AgentId = agentId, Category = Vals.CombatCategory.TopLevelAction, Description = "All Melee Options", NextCategory = Vals.CombatCategory.AllMelee }); possibleCombatActions.Choices.Add(new AgentActionChoice { AgentId = agentId, Category = Vals.CombatCategory.TopLevelAction, Description = "Change Stance", NextCategory = Vals.CombatCategory.AllStances }); possibleCombatActions.Choices.Add(new AgentActionChoice { AgentId = agentId, Category = Vals.CombatCategory.TopLevelAction, Description = "Run Away", Action = Vals.CombatAction.RunAway }); possibleCombatActions.Choices.Add(new AgentActionChoice { AgentId = agentId, Category = Vals.CombatCategory.TopLevelAction, Description = "Do Nothing", Action = Vals.CombatAction.DoNothing }); return(possibleCombatActions); }
//for now, hardcoding this is fine. once we get a dozen, reconsider (or maybe not). internal static Dictionary <string, Dictionary <Vals.BodySlots, long> > InitializeNaturalWeapons(EcsRegistrar rgs) { var humanPunchId = Blueprinter.GetEntityFromBlueprint(rgs, Vals.NaturalWeaponNames.HumanPunch); var humanNatWeapons = new Dictionary <Vals.BodySlots, long> { [Vals.BodySlots.WieldHandLeft] = humanPunchId, [Vals.BodySlots.WieldHandRight] = humanPunchId }; var wolfBiteId = Blueprinter.GetEntityFromBlueprint(rgs, Vals.NaturalWeaponNames.WolfBite); var wolfNatWeapons = new Dictionary <Vals.BodySlots, long> { [Vals.BodySlots.VertebrateMouth] = wolfBiteId }; return(new Dictionary <string, Dictionary <Vals.BodySlots, long> > { [Vals.NaturalWeaponSet.Human] = humanNatWeapons, [Vals.NaturalWeaponSet.Wolf] = wolfNatWeapons }); }
internal static List <Output> OutputForCombatMessages(EcsRegistrar rgs, List <Messages.Combat> messages) { var results = new List <Output>(); foreach (var msg in messages) { //MWCTODO: add a debug switch for this info results.Add(new Output { Category = OutputCategory.Text, Data = $"(DEBUG: attempted damage {msg.AttemptedDamage} net damage {msg.NetDamage} critical {msg.ActorCritical} target condition {msg.TargetCondition})" }); var result = new Output { Category = OutputCategory.Text }; var sb = new StringBuilder(); var actorNames = rgs.GetPartSingle <Parts.EntityName>(msg.ActorId); if (Vals.CombatAction.AllStances.Contains(msg.ActorAction)) { result.Data = $"{actorNames.ProperName} assumes a {msg.ActorAction} stance."; results.Add(result); } else if (msg.ActorAction == Vals.CombatAction.DoNothing) { result.Data = $"{actorNames.ProperName} stands still and looks around dumbly. What do?"; results.Add(result); } else if (msg.ActorAction == Vals.CombatAction.RunAway) { result.Data = $"{actorNames.ProperName} tries to run away, and {actorNames.Pronoun} feet spin on the floor in a cartoon whirlwind, but nothing actually happens. Uh oh."; results.Add(result); } else if (msg.TargetId == 0) { result.Data = $"MWCTODO: we don't know what {msg.ActorAction} is."; results.Add(result); } //if the result has data, we're done with this iteration. if (!string.IsNullOrEmpty(result.Data)) { continue; } var targetNames = rgs.GetPartSingle <Parts.EntityName>(msg.TargetId); sb.Append($"{actorNames.ProperName} {PresentTenseVerb(msg.AttackVerb)} with a {msg.ActorAdjustedRoll} attack."); if (msg.ActorAdjustedRoll <= 0) { sb.Append($", presenting no real threat to {targetNames.ProperName}, who steps back from it casually."); result.Data = sb.ToString(); AppendNoDamageButNearlyDead(sb, targetNames.ProperName, targetNames.Pronoun, msg.TargetCondition); results.Add(result); continue; } if (msg.AttemptedDamage <= 0) { sb.Append($", but {targetNames.ProperName} sees it coming and ducks out of the way, avoiding it entirely."); result.Data = sb.ToString(); AppendNoDamageButNearlyDead(sb, targetNames.ProperName, targetNames.Pronoun, msg.TargetCondition); results.Add(result); continue; } if (msg.NetDamage <= 0) { sb.Append($" and connects! Unfortunately for {actorNames.ProperName}, the attack isn't solid, and {targetNames.ProperName}'s armor protects {targetNames.Pronoun} entirely."); result.Data = sb.ToString(); AppendNoDamageButNearlyDead(sb, targetNames.ProperName, targetNames.Pronoun, msg.TargetCondition); results.Add(result); continue; } var targetConditionString = GetLivingThingConditionDesc(msg.TargetCondition); //any fatality or any crit ought to get a special fatality/crit message. if (msg.NetDamage > 7000) { sb.Append($". The attack is nearly perfect, and it is devastating. {targetNames.ProperName} is {targetConditionString}."); } else if (msg.NetDamage > 4000) { sb.Append($". It's a fine strike, penetrating the armor and inflicting a nasty wound. {targetNames.ProperName} is {targetConditionString}. "); } else if (msg.NetDamage > 1900) { sb.Append($". A hit . . . hard enough to notice. {targetNames.ProperName} is {targetConditionString}. "); } else if (msg.NetDamage > 0) { if (msg.TargetCondition > 2000) { sb.Append($". It doesn't amount to much, though. Barely a scratch. It will take a lot of these to wear down {targetNames.ProperName}."); } else if (msg.TargetCondition > 1) { sb.Append($". Earlier, {targetNames.ProperName} would have laughed at a wound like this. It's not much more than a scratch. {targetNames.ProperName} isn't laughing now. When you're already gravely wounded, any attack that lands might be your last, and {targetNames.ProperName} is already looking death in the eyes."); } else { sb.Append($". It's not much of an attack, but it didn't need to be. {targetNames.ProperName} watches in horror as the point slides home. {targetNames.Pronoun} lets go of {targetNames.Pronoun} weapon and reaches up for a moment before falling to the ground, dead."); } } result.Data = sb.ToString(); results.Add(result); } return(results); }
internal static Output Equip(EcsRegistrar rgs, long equipperId, long gearId) { var output = new Output { Category = "Text" }; //MWCTODO: bundleize this var equipperAnatomy = rgs.GetPartSingle <Parts.Anatomy>(equipperId); var equipperPhysicalObject = rgs.GetPartSingle <Parts.PhysicalObject>(equipperId); var equipperEntityName = rgs.GetPartSingle <Parts.EntityName>(equipperId); var gearPhysicalObject = rgs.GetPartSingle <Parts.PhysicalObject>(gearId); var gearEntityName = rgs.GetPartSingle <Parts.EntityName>(gearId); //someday we'll support gear that occupies multiple slots or using complex slot logic. if (gearPhysicalObject.EquipmentSlots == Vals.BodySlots.NoSlot || gearPhysicalObject.EquipmentSlots == Vals.BodySlots.Special) { throw new NotImplementedException(); } //MWCTODO++: rework this to use new DisabledSlot entity concept. //var equipperDisabledSlots = equipperAnatomyModifications.Select(m => m.NewEquipmentSlotsDisabled).ToList(); var equipperSlots = equipperAnatomy .SlotsEquipped //.Where(kvp => !equipperDisabledSlots.Contains(kvp.Key)) .ToDictionary(d => d.Key, d => d.Value); var matchingEquipperSlots = equipperSlots.Where(slot => (slot.Key & gearPhysicalObject.EquipmentSlots) == slot.Key).ToList(); if (!matchingEquipperSlots.Any()) { output.Data = $"{equipperEntityName.ProperName} doesn't have the right body parts to equip the {gearEntityName.GeneralName}."; return(output); } if (matchingEquipperSlots.All(s => s.Value != 0)) { output.Data = $"There's no open place to equip the {gearEntityName.GeneralName}. Unequip something else first."; return(output); } //we will special case 2h weapons here, though. and body armor! bool gearIs2Handed = (gearPhysicalObject.EquipmentSlots & Vals.BodySlots.WieldTwoHanded) == Vals.BodySlots.WieldTwoHanded; bool gearIsHumanBodyArmor = (gearPhysicalObject.EquipmentSlots & Vals.BodySlots.HumanBodyArmor) == Vals.BodySlots.HumanBodyArmor; if (gearIs2Handed && Vals.BodyPlan.BodySlotsEncompassedByOtherSlot[Vals.BodySlots.WieldTwoHanded].Any(s => equipperAnatomy.SlotsEquipped[s] != 0)) { output.Data = $"Two free hands are needed for the {gearEntityName.GeneralName}. Unequip something else first."; return(output); } if (gearIsHumanBodyArmor && Vals.BodyPlan.BodySlotsEncompassedByOtherSlot[Vals.BodySlots.HumanBodyArmor].Any(s => equipperAnatomy.SlotsEquipped[s] != 0)) { output.Data = $"Other equipment is in the way of equipping the {gearEntityName.GeneralName}. Unequip other gear first."; return(output); } //by equipping 2h-weapons and body armor in ALL the related slots, it makes checking equipment/weapons easy elsewhere. it will complicate unequip, though. var firstOpenSlot = matchingEquipperSlots.First(s => s.Value == 0); equipperAnatomy.SlotsEquipped[firstOpenSlot.Key] = gearId; if (gearIs2Handed) { foreach (var slot in Vals.BodyPlan.BodySlotsEncompassedByOtherSlot[Vals.BodySlots.WieldTwoHanded]) { equipperAnatomy.SlotsEquipped[slot] = gearId; } } if (gearIsHumanBodyArmor) { foreach (var slot in Vals.BodyPlan.BodySlotsEncompassedByOtherSlot[Vals.BodySlots.HumanBodyArmor]) { equipperAnatomy.SlotsEquipped[slot] = gearId; } } output.Data = $"{equipperEntityName.ProperName} equips the {gearEntityName.GeneralName}."; return(output); }