///
///
///
///
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("Check Attack");
public static carry = new Rulebook("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;
public static dodgeCost = 1;
public static fistCost = 1;
public static gettingAttacked = new OneOf(OneOf.PURELY_AT_RANDOM,
"moves on to attack you!",
"attacks you!",
"starts to attack you!",
"decides to attack you!",
"initiates an attack!",
"is attacking!",
"approaches with malicious intent!",
"aims at you with malicious intent!",
"aims at you!",
"goes for you!",
"comes for you!",
"seems hostile!",
"seems to want to fight!",
"wants to fight!",
"is coming right for you!",
"is coming right at you!",
"is not friendly at all!"
);
// TODO: Add infinitely more of these...
public constructor (actor : Thing, ...nouns : Array) {
super(actor, ...nouns);
this.requiresNoun = true;
this.requiresVisibility = false;
}
public getTarget () : Thing {
return this.getNoun(0);
}
public getWeapons () : Array {
let weaponNoun = this.getNoun(1);
if (weaponNoun == undefined) {
let weapons = >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;
}
return [weaponNoun];
}
public getCommandText () {
let weaponName;
let weapons = this.getWeapons();
if (weapons.length == 0) {
weaponName = "fists";
} else {
weaponName = [];
weapons.forEach((weapon => {
weaponName.push(weapon.getPrintedName());
}));
weaponName = weaponName.join(" and ");
}
return "attack " + this.getNoun(0).getPrintedName() + " with " + weaponName;
}
public generateDescription (group : ContentGroup) {
this.say.add(...(CombatDescription.getDescription(group)));
}
public static checkAttackable = ActionAttack.check.createAndAddRule({
name : "Attack - Is it attackable?",
firstPriority : Rule.PRIORITY_HIGHEST,
priority : Rule.PRIORITY_MEDIUM,
code : async (runner : RulebookRunner) => {
let action = runner.noun;
let target = runner.noun.getTarget();
if (!(target instanceof Person) && !target.breakable) {
if (action.actor == WorldState.player) {
action.say.add("You can't possibly damage that.");
}
action.stop();
return false;
}
}
});
public static checkAccuracyRule = ActionAttack.check.createAndAddRule({
name : "Do Dex Rolls - Choose defenses",
firstPriority : ActionAttack.checkAttackable.firstPriority,
priority : ActionAttack.checkAttackable.priority - 1,
code : async (runner : RulebookRunner) => {
let actor = runner.noun.actor;
let target = runner.noun.getTarget();
let action = runner.noun;
action.targetReaction = ActionAttackReaction.DODGE;
action.actorDexRoll = ActionAttack.getEffectiveCombatDex(actor);
action.targetDexRoll = 0;
if (target instanceof Person) {
action.targetDexRoll = ActionAttack.getEffectiveCombatDex(actor);
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) => {
let actor = runner.noun.actor;
let target = runner.noun.getTarget();
let action = runner.noun;
let getDamage = (person : Person, ...weapons : Array) => {
let damage = 0;
weapons.forEach(weapon => {
damage += weapon.getAttributeDamageFactor() * person.getStat(weapon.getAttribute());
});
return damage;
};
let getForce = (person : Person, ...weapons : Array) => {
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( target, ...attack.getWeapons()); // most likely only players do this
action.targetForceRoll = getForce( target, ...attack.getWeapons()); // most likely only players do this
}
}
});
public static carryOutAttack = ActionAttack.carry.createAndAddRule({
name: "Attack - Actor Attacks Person",
firstPriority : Rule.PRIORITY_HIGH,
priority : Rule.PRIORITY_MEDIUM,
code: async (runner: RulebookRunner) => {
let action = runner.noun;
ActionAttack.attackOnPerson(action);
if (action.targetReaction == ActionAttackReaction.COUNTERATTACK && ( 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);
}
}
});
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;
if (!(target instanceof Person)) {
ActionAttack.breakThing(action);
return;
}
if (!isHit) {
action.generateDescription(
(new ContentGroup())
.addUnit(
(new CombatUnit())
.setActor(actor)
.setTarget(target)
.setWeapon(...action.getWeapons())
.addMarker(CombatHit.MISS)
)
);
return; // End of action
}
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.
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();
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) {
( 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(
{
name : "Hyperlink - Attack",
firstPriority : Rule.PRIORITY_HIGHEST,
priority : Rule.PRIORITY_MEDIUM,
code : (rulebook : RulebookRunner) => {
let thing = rulebook.noun;
if ((thing instanceof Person || thing.breakable) && thing.isVisibleTo(WorldState.player)) {
Elements.HyperlinkHandler.addAvailableAction("Attack", new ActionAttack(WorldState.player, thing));
}
}
}
));