|
@@ -1,17 +1,32 @@
|
|
|
/// <reference path="../Action.ts" />
|
|
|
/// <reference path="../Rule.ts" />
|
|
|
/// <reference path="../Rulebook.ts" />
|
|
|
-/// <reference path="../Things/Weapon/Weapon.ts" />
|
|
|
+/// <reference path="../Tests/DiceDangerous.ts" />
|
|
|
enum ActionAttackReaction {
|
|
|
DODGE, RESIST, COUNTERATTACK
|
|
|
}
|
|
|
|
|
|
+interface Weaponized {
|
|
|
+ getAttribute : () => Attribute; // What is the attribute used for the damage
|
|
|
+ getAttributeDamageFactor : () => number; // How much of the attribute is added for the attack.
|
|
|
+ getAttributeForceFactor : () => number; // How much of the attribute is added for the attack's pushing down capabilities
|
|
|
+ getAccuracy : () => number; // How much to add/remove from the Dex check.
|
|
|
+ getCost : () => number; // The cost of performing this attack.
|
|
|
+}
|
|
|
+
|
|
|
class ActionAttack extends Action {
|
|
|
public static check = new Rulebook<ActionAttack>("Check Attack");
|
|
|
public static carry = new Rulebook<ActionAttack>("Carry out Attack");
|
|
|
public actingAgressively = true;
|
|
|
public aggressivenessRating = 1;
|
|
|
+
|
|
|
private targetReaction : ActionAttackReaction;
|
|
|
+ private actorDexRoll : number;
|
|
|
+ private targetDexRoll : number;
|
|
|
+ private actorDamageRoll : number;
|
|
|
+ private targetDamageRoll : number;
|
|
|
+ private actorForceRoll : number;
|
|
|
+ private targetForceRoll : number;
|
|
|
public allowedStances = [PersonStance.STANDING];
|
|
|
|
|
|
public static randomness = 2;
|
|
@@ -21,7 +36,7 @@ class ActionAttack extends Action {
|
|
|
public static gettingAttacked = new OneOf(OneOf.PURELY_AT_RANDOM,
|
|
|
"moves on to attack you!",
|
|
|
"attacks you!",
|
|
|
- "tries to attack you!",
|
|
|
+ "starts to attack you!",
|
|
|
"decides to attack you!",
|
|
|
"initiates an attack!",
|
|
|
"is attacking!",
|
|
@@ -53,7 +68,14 @@ class ActionAttack extends Action {
|
|
|
let weaponNoun = this.getNoun(1);
|
|
|
|
|
|
if (weaponNoun == undefined) {
|
|
|
- let weapons = <Array<Weapon>> this.actor.getWorns(Weapon);
|
|
|
+ let weapons = <Array<Weapon>>this.actor.getWorns(Weapon);
|
|
|
+ if (weapons.length == 0) {
|
|
|
+ // If we ever have tentaclemonsters or stuff...
|
|
|
+ // TODO: Add parts for other creature types here.
|
|
|
+ if (this.actor instanceof Humanoid) {
|
|
|
+ return [this.actor.getPart(HumanoidHands)];
|
|
|
+ }
|
|
|
+ }
|
|
|
return weapons;
|
|
|
}
|
|
|
|
|
@@ -82,6 +104,7 @@ class ActionAttack extends Action {
|
|
|
public static checkAttackable = ActionAttack.check.createAndAddRule({
|
|
|
name : "Attack - Is it attackable?",
|
|
|
firstPriority : Rule.PRIORITY_HIGHEST,
|
|
|
+ priority : Rule.PRIORITY_MEDIUM,
|
|
|
code : async (runner : RulebookRunner<ActionAttack>) => {
|
|
|
let action = runner.noun;
|
|
|
let target = runner.noun.getTarget();
|
|
@@ -95,152 +118,221 @@ class ActionAttack extends Action {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
- // TODO: Reduce these god functions into smaller functions that are easier to modify by future changes.
|
|
|
public static checkAccuracyRule = ActionAttack.check.createAndAddRule({
|
|
|
- name : "Attack - Is it a hit?",
|
|
|
+ name : "Do Dex Rolls - Choose defenses",
|
|
|
+ firstPriority : ActionAttack.checkAttackable.firstPriority,
|
|
|
+ priority : ActionAttack.checkAttackable.priority - 1,
|
|
|
code : async (runner : RulebookRunner<ActionAttack>) => {
|
|
|
+ let actor = <Person> runner.noun.actor;
|
|
|
+ let target = runner.noun.getTarget();
|
|
|
let action = runner.noun;
|
|
|
- let actorDex = (<Person> runner.noun.actor).getStat(Attributes.Agility);
|
|
|
- let actor = (<Person> runner.noun.actor);
|
|
|
- if (actor.stance == PersonStance.ALLFOURS) {
|
|
|
- actorDex = Math.floor(actorDex/2);
|
|
|
- }
|
|
|
+ action.targetReaction = ActionAttackReaction.DODGE;
|
|
|
+ action.actorDexRoll = ActionAttack.getEffectiveCombatDex(actor);
|
|
|
+ action.targetDexRoll = 0;
|
|
|
|
|
|
- let targetDex = 0;
|
|
|
- let target = runner.noun.getTarget();
|
|
|
if (target instanceof Person) {
|
|
|
- targetDex = target.getStat(Attributes.Agility);
|
|
|
- if (target.stance == PersonStance.ALLFOURS) {
|
|
|
- targetDex = Math.floor(targetDex/2);
|
|
|
- } else if (target.stance == PersonStance.KNOCKEDOUT) {
|
|
|
- targetDex = 0;
|
|
|
- }
|
|
|
-
|
|
|
+ action.targetDexRoll = ActionAttack.getEffectiveCombatDex(actor);
|
|
|
|
|
|
- let reaction = ActionAttackReaction.DODGE;
|
|
|
- if (target.stance == PersonStance.KNOCKEDOUT) {
|
|
|
- reaction = ActionAttackReaction.RESIST;
|
|
|
- } else {
|
|
|
- if (target == WorldState.player) {
|
|
|
- Elements.CurrentTurnHandler.printAsContent(new Say(action.actor, " ", ActionAttack.gettingAttacked.getOne()));
|
|
|
- let choices = ["Counter-Attack", "Dodge", "Resist"];
|
|
|
- let choice = await Controls.giveChoices(false, ...choices);
|
|
|
- if (choice[1] == 0) {
|
|
|
- reaction = ActionAttackReaction.COUNTERATTACK;
|
|
|
- } else if (choice[1] == 2) {
|
|
|
- reaction = ActionAttackReaction.DODGE;
|
|
|
- } else {
|
|
|
- reaction = ActionAttackReaction.RESIST;
|
|
|
- }
|
|
|
+ if (WorldState.isPlayer(target)) {
|
|
|
+ // Let player choose reaction
|
|
|
+ let sayWarning = new Say(Say.Mention(actor), " ", ActionAttack.gettingAttacked.getOne());
|
|
|
+ let elements = await Elements.CurrentTurnHandler.getSayElementsAsContent(sayWarning);
|
|
|
+ Elements.CurrentTurnHandler.print(...elements);
|
|
|
+ let choices = ["Counter Attack", "Dodge", "Resist"];
|
|
|
+ let choice = await Controls.giveChoices(false, ...choices);
|
|
|
+ // if choice != dodge
|
|
|
+ if (choice[1] != 1) {
|
|
|
+ action.targetReaction = choice[1] == 0 ? ActionAttackReaction.COUNTERATTACK : ActionAttackReaction.RESIST;
|
|
|
+ action.targetDexRoll = action.targetDexRoll / 2;
|
|
|
}
|
|
|
+ Elements.CurrentTurnHandler.unprint(...elements);
|
|
|
}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ public static checkDamageRule = ActionAttack.check.createAndAddRule({
|
|
|
+ name : "Choose damages",
|
|
|
+ firstPriority : ActionAttack.checkAttackable.firstPriority,
|
|
|
+ priority : ActionAttack.checkAttackable.priority - 1,
|
|
|
+ code : async (runner : RulebookRunner<ActionAttack>) => {
|
|
|
+ let actor = <Person> runner.noun.actor;
|
|
|
+ let target = runner.noun.getTarget();
|
|
|
+ let action = runner.noun;
|
|
|
|
|
|
- let weapons = action.getWeapons();
|
|
|
- for (let i = 0; i < weapons.length; i++) {
|
|
|
- (<Person> action.actor).changeStamina(- weapons[i].attackCost);
|
|
|
- }
|
|
|
- if (weapons.length == 0) {
|
|
|
- (<Person> action.actor).changeStamina(- ActionAttack.fistCost)
|
|
|
- }
|
|
|
+ let getDamage = (person : Person, ...weapons : Array<Weaponized>) => {
|
|
|
+ let damage = 0;
|
|
|
+ weapons.forEach(weapon => {
|
|
|
+ damage += weapon.getAttributeDamageFactor() * person.getStat(weapon.getAttribute());
|
|
|
+ });
|
|
|
+ return damage;
|
|
|
+ };
|
|
|
|
|
|
- action.targetReaction = reaction;
|
|
|
-
|
|
|
- if (reaction == ActionAttackReaction.DODGE) {
|
|
|
- (<Person> target).changeStamina(- ActionAttack.dodgeCost);
|
|
|
- let attack = Math.floor(Math.random() * (ActionAttack.randomness + ActionAttack.randomness + 1)) - ActionAttack.randomness;
|
|
|
- let defense = Math.floor(Math.random() * (ActionAttack.randomness + ActionAttack.randomness + 1)) - ActionAttack.randomness;
|
|
|
- attack += actorDex;
|
|
|
- defense += targetDex;
|
|
|
- if (attack < defense) {
|
|
|
- action.generateDescription(
|
|
|
- (new ContentGroup())
|
|
|
- .addUnit(
|
|
|
- (new CombatUnit())
|
|
|
- .setActor(action.actor)
|
|
|
- .setTarget(target)
|
|
|
- .setWeapon(...weapons)
|
|
|
- .addMarker(CombatHit.MISS)
|
|
|
- )
|
|
|
- );
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
+ let getForce = (person : Person, ...weapons : Array<Weaponized>) => {
|
|
|
+ let force = 0;
|
|
|
+ weapons.forEach(weapon => {
|
|
|
+ force += weapon.getAttributeForceFactor() * person.getStat(weapon.getAttribute());
|
|
|
+ });
|
|
|
+ return force;
|
|
|
+ };
|
|
|
+
|
|
|
+ action.actorDamageRoll = getDamage(actor, ...action.getWeapons());
|
|
|
+ action.actorForceRoll = getForce(actor, ...action.getWeapons());
|
|
|
+
|
|
|
+ if (action.targetReaction == ActionAttackReaction.COUNTERATTACK) {
|
|
|
+ let attack = new ActionAttack(target);
|
|
|
+ action.targetDamageRoll = getDamage(<Person> target, ...attack.getWeapons()); // most likely only players do this
|
|
|
+ action.targetForceRoll = getForce(<Person> target, ...attack.getWeapons()); // most likely only players do this
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
public static carryOutAttack = ActionAttack.carry.createAndAddRule({
|
|
|
- name: "Attack - Go for the throat!",
|
|
|
+ name: "Attack - Actor Attacks Person",
|
|
|
+ firstPriority : Rule.PRIORITY_HIGH,
|
|
|
+ priority : Rule.PRIORITY_MEDIUM,
|
|
|
code: async (runner: RulebookRunner<ActionAttack>) => {
|
|
|
let action = runner.noun;
|
|
|
- let target = runner.noun.getTarget();
|
|
|
- let damage = 0;
|
|
|
- let weapons = action.getWeapons();
|
|
|
- for (let i = 0; i < weapons.length; i++) {
|
|
|
- damage += weapons[i].getDamage();
|
|
|
- }
|
|
|
- if (weapons.length == 0) {
|
|
|
- damage += (<Person> action.actor).getStat(Attributes.Strength);
|
|
|
+ ActionAttack.attackOnPerson(action);
|
|
|
+
|
|
|
+ if (action.targetReaction == ActionAttackReaction.COUNTERATTACK && (<Person> action.getTarget()).stance != PersonStance.KNOCKEDOUT) { // Safeguard against targets that were killed
|
|
|
+ let counter = new ActionAttack(action.getTarget(), action.actor);
|
|
|
+ counter.actorDamageRoll = action.targetDamageRoll;
|
|
|
+ counter.actorForceRoll = action.targetForceRoll;
|
|
|
+ counter.actorDexRoll = 100; // Counter Attacks always hit
|
|
|
+ counter.targetDexRoll = 0;
|
|
|
+ counter.targetReaction = ActionAttackReaction.DODGE;
|
|
|
+ ActionAttack.attackOnPerson(counter);
|
|
|
+ action.say.add(Say.PARAGRAPH_BREAK, counter.say);
|
|
|
}
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- if (!(target instanceof Person)) {
|
|
|
- if (target.breakable && target.breakableOn <= damage) {
|
|
|
- target.break();
|
|
|
- action.generateDescription(
|
|
|
- (new ContentGroup())
|
|
|
- .addUnit(
|
|
|
- (new CombatUnit())
|
|
|
- .setActor(action.actor)
|
|
|
- .setTarget(target)
|
|
|
- .setWeapon(...weapons)
|
|
|
- .addMarker(CombatHit.HIT, CombatResult.KILLED)
|
|
|
- )
|
|
|
- );
|
|
|
- }
|
|
|
- } else {
|
|
|
- let damageReduction = target.getStat(Attributes.Strength);
|
|
|
- if (action.targetReaction == ActionAttackReaction.RESIST) {
|
|
|
- damageReduction = Math.floor(damageReduction * 1.5)
|
|
|
- }
|
|
|
- let finalDamage = damage - damageReduction;
|
|
|
- if (finalDamage < 0) {
|
|
|
- finalDamage = 0;
|
|
|
- }
|
|
|
- action.aggressivenessRating = 1 + finalDamage;
|
|
|
- let torso = <HumanoidTorso> (<Person> target).getPart(HumanoidTorso);
|
|
|
- if (torso != undefined) {
|
|
|
- torso.changeSoreness(finalDamage);
|
|
|
- }
|
|
|
+ public static attackOnPerson (action : ActionAttack) {
|
|
|
+ let actor = action.actor;
|
|
|
+ let target = action.getTarget();
|
|
|
+ let isHit = Dice.testAgainstRoll(
|
|
|
+ {name : "Attacker's Hit", value : action.actorDexRoll},
|
|
|
+ {name : "Target's Dodge", value : action.targetDexRoll}
|
|
|
+ ) > 0;
|
|
|
|
|
|
- let hitType = (finalDamage * torso.getWeightedSoreness()) > (target.getMaxHealth() / 3) ? CombatHit.CRITICAL : CombatHit.HIT;
|
|
|
- let targetHealth = target.getHealthOnScale();
|
|
|
- let knockedOff = hitType == CombatHit.CRITICAL;
|
|
|
+ if (!(target instanceof Person)) {
|
|
|
+ ActionAttack.breakThing(action);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (knockedOff) {
|
|
|
- target.stance = PersonStance.ALLFOURS;
|
|
|
- }
|
|
|
+ if (!isHit) {
|
|
|
+ action.generateDescription(
|
|
|
+ (new ContentGroup())
|
|
|
+ .addUnit(
|
|
|
+ (new CombatUnit())
|
|
|
+ .setActor(actor)
|
|
|
+ .setTarget(target)
|
|
|
+ .setWeapon(...action.getWeapons())
|
|
|
+ .addMarker(CombatHit.MISS)
|
|
|
+ )
|
|
|
+ );
|
|
|
+ return; // End of action
|
|
|
+ }
|
|
|
|
|
|
- let result = targetHealth < -9 ? CombatResult.KILLED : targetHealth <= 0 ? CombatResult.KNOCKED_OFF :
|
|
|
- knockedOff ? CombatResult.KNOCKED : undefined;
|
|
|
+ let dice = new Dice("Combat Roll");
|
|
|
+ let actorDamage = dice.rollAndSum(action.actorDamageRoll);
|
|
|
+ // TODO: Add torso armor.
|
|
|
+ let targetRes = target.getStat(Attributes.Strength) * 0.25;
|
|
|
+ let previousHealth = target.getHealthOnScale();
|
|
|
+ let criticalHitThreshold = 4; // Chosen arbitrarily.
|
|
|
|
|
|
- if (targetHealth < -9) {
|
|
|
- target.die();
|
|
|
- }
|
|
|
+ let isKnockedDown = false;
|
|
|
+ if (target.stance == PersonStance.STANDING) {
|
|
|
+ isKnockedDown = Dice.testAgainstRoll(
|
|
|
+ {name : "Attacker's Force", value : action.actorForceRoll},
|
|
|
+ {name : "Target's Resistance", value : target.getStat(Attributes.Strength)}
|
|
|
+ ) > 0;
|
|
|
+ }
|
|
|
+ let finalHealth = target.getHealthOnScale();
|
|
|
|
|
|
|
|
|
- action.generateDescription(
|
|
|
- (new ContentGroup())
|
|
|
- .addUnit(
|
|
|
- (new CombatUnit())
|
|
|
- .setActor(action.actor)
|
|
|
- .setTarget(target)
|
|
|
- .setWeapon(...weapons)
|
|
|
- .addMarker(hitType, result)
|
|
|
- )
|
|
|
- );
|
|
|
- }
|
|
|
+ let finalDamage = Math.floor(actorDamage - targetRes) * 10; // Multiplies by 10 to make it work with Torsos Soreness
|
|
|
+ action.aggressivenessRating = Math.max(finalDamage / 10, 1); // no need to multiply by 10 here.
|
|
|
+ if (action.targetReaction == ActionAttackReaction.RESIST) {
|
|
|
+ finalDamage = finalDamage * 2 / 3; // TODO: Figure out if two thirds is enough.
|
|
|
}
|
|
|
- });
|
|
|
+ if (target instanceof Humanoid) {
|
|
|
+ (<HumanoidTorso> target.getPart(HumanoidTorso)).changeSoreness(finalDamage);
|
|
|
+ } else{
|
|
|
+ console.warn("CANT DAMAGE");
|
|
|
+ }
|
|
|
+
|
|
|
+ let markers = [];
|
|
|
+ if (finalDamage >= criticalHitThreshold) {
|
|
|
+ markers.push(CombatHit.CRITICAL);
|
|
|
+ } else {
|
|
|
+ markers.push(CombatHit.HIT);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (previousHealth > 0 && finalHealth <= 0) {
|
|
|
+ markers.push(CombatResult.KNOCKED_OFF);
|
|
|
+ target.stance = PersonStance.KNOCKEDOUT;
|
|
|
+ } else if (finalHealth <= -10) {
|
|
|
+ markers.push(CombatResult.KILLED);
|
|
|
+ target.stance = PersonStance.KNOCKEDOUT;
|
|
|
+ target.die();
|
|
|
+ } else if (isKnockedDown) {
|
|
|
+ markers.push(CombatResult.KNOCKED);
|
|
|
+ target.stance = PersonStance.ALLFOURS;
|
|
|
+ }
|
|
|
+
|
|
|
+ action.generateDescription(
|
|
|
+ (new ContentGroup())
|
|
|
+ .addUnit(
|
|
|
+ (new CombatUnit())
|
|
|
+ .setActor(actor)
|
|
|
+ .setTarget(target)
|
|
|
+ .setWeapon(...action.getWeapons())
|
|
|
+ .addMarker(...markers)
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ public static breakThing (action : ActionAttack) {
|
|
|
+ let actor = action.actor;
|
|
|
+ let target = action.getTarget();
|
|
|
+
|
|
|
+ let dice = new Dice("Combat Roll");
|
|
|
+ let actorDamage = dice.rollAndSum(action.actorDamageRoll);
|
|
|
+
|
|
|
+ if (actorDamage >= target.breakableOn) {
|
|
|
+ action.generateDescription(
|
|
|
+ (new ContentGroup())
|
|
|
+ .addUnit(
|
|
|
+ (new CombatUnit())
|
|
|
+ .setActor(actor)
|
|
|
+ .setTarget(target)
|
|
|
+ .setWeapon(...action.getWeapons())
|
|
|
+ .addMarker(CombatHit.CRITICAL)
|
|
|
+ )
|
|
|
+ );
|
|
|
+ target.break();
|
|
|
+ } else {
|
|
|
+ action.generateDescription(
|
|
|
+ (new ContentGroup())
|
|
|
+ .addUnit(
|
|
|
+ (new CombatUnit())
|
|
|
+ .setActor(actor)
|
|
|
+ .setTarget(target)
|
|
|
+ .setWeapon(...action.getWeapons())
|
|
|
+ .addMarker(CombatHit.HIT)
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static getEffectiveCombatDex (person : Person) {
|
|
|
+ let dex = person.getStat(Attributes.Agility);
|
|
|
+ if (person.stance != PersonStance.STANDING) {
|
|
|
+ return dex / 2;
|
|
|
+ }
|
|
|
+ return dex;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
Elements.HyperlinkHandler.HyperlinkingRulebook.addRule(new Rule(
|