//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 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())); }
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() }); }
/// <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; }
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 }); }
// 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 }); }