/// /// /// /// interface AIOptions { actor : Thing; wanderer? : boolean; wandersOn? : Region; picksShinies? : boolean; grudgeRate? : number; hostileThreshold? : number; coolOffRate? : number; retaliates? : boolean; } interface DialogueHook { text : Say; tree : DialogueTree; } interface TalkingHeads { greeter : Person; answerer : Person; options : Array; runFirst : Array; runAndStop : Array; } class AI { public actor : Person; public wanderer = true; public wandersOn : Region; public wanderChance = 50; public picksShinies = true; public hostilityTargets : Array = []; public hostilityLevels : Array = []; public hostileTargets : Array = []; // Cache hostilities over 100 to reduce processing usage. public grudgeRate : number = 50; // Multiplies aggression level to determine anger. public retaliates = false; public warnedTimes = 0; public hostileThreshold : number = 100; // Amount of anger at which it becomes entirely hostile public coolOffRate : number = 5; // Amount, per turn, that rage subdues public noticed : Array = []; public newNoticed : Array = []; public newArrivals : Array = []; public oldLeavers : Array = []; public static rules = new Rulebook("Default AI Rules"); public extraRules : Array> = []; public static combatRules = new Rulebook("Default AI Combat Rules"); public extraCombatRules : Array> = []; public static talktoRules = new Rulebook("Default Talk To Rules"); public extraTalktoRules : Array> = []; public static investigateRules = new Rulebook("Default Ask About Rules"); public extraInvestigateRules : Array> = []; public static reacttoRules = new Rulebook("Default React To Rules"); public extraReacttoRules : Array> = []; public reactingTo : Action; public storedReaction : Action; public constructor (options : AIOptions) { for (let key in options) { this[key] = options[key]; } } /** * Executing an AI returns an Action. DOESN'T execute the action, just finds it! * @returns {Promise} */ public async execute () : Promise { let promise : Promise; let inCombat = false; if (this.storedReaction != undefined) { let result = this.storedReaction; this.storedReaction = undefined; return result; } if (this.hostileTargets.length > 0) { for (let i = this.hostileTargets.length - 1; i >= 0; i--) { if (this.hostileTargets[i].isVisibleTo(this.actor)) { inCombat = true; break; } } } else if (this.actor.reputation < -9) { this.hostileTargets.push(WorldState.player); inCombat = WorldState.player.isVisibleTo(this.actor); } let result : Action; if (inCombat) { // TUNNEL VISION promise = AI.combatRules.execute({ noun : this.actor }, ...this.extraCombatRules); result = await promise; if (result != undefined) { return result; } // No action was found? } this.renoticeThings(); promise = AI.rules.execute({ noun : this.actor }, ...this.extraRules); result = await promise; this.coolOff(); return result; } public renoticeThings () { this.noticed = this.newNoticed; this.newNoticed = []; let stuff = this.actor.getRoom().getContainedAndVisibleTo(this.actor); for (let i = stuff.length - 1; i >= 0; i--) { this.newNoticed.push(stuff[i]); } this.newNoticed = this.newNoticed.filter( ( el ) => !this.noticed.includes( el ) ); this.oldLeavers = this.noticed.filter( ( el ) => !this.newNoticed.includes( el ) ); } public addRulesBook (...books : Array>) { this.extraRules.push(...books) arrayUnique(this.extraRules); } public addCombatRulesBook (...books : Array>) { this.extraCombatRules.push(...books) arrayUnique(this.extraCombatRules); } public addHostility (target : Thing, amount : number) { amount = amount * this.grudgeRate; let index = this.hostilityTargets.indexOf(target); if (index == -1) { index = this.hostilityTargets.push(target); this.hostilityLevels.push(amount); } else { this.hostilityLevels[index] += amount; } if (this.hostilityLevels[index] >= this.hostileThreshold && !this.hostileTargets.includes(target)) { this.hostileTargets.push(target); } else if (this.hostilityLevels[index] <= 0) { this.removeHostility(index, target); } } public removeHostility (index, target) { let hostile = this.hostileTargets.indexOf(target); if (hostile != -1) { this.hostileTargets.splice(hostile, 1); } this.hostilityTargets.splice(index, 1); this.hostilityLevels.splice(index, 1); } public getHostilityTo (target : Thing) { let index = this.hostilityTargets.indexOf(target); if (index != -1) { return this.hostilityLevels[index]; } else { return 0; } } // TODO: Make this a rulebook. public coolOff () { for (let i = this.hostilityTargets.length - 1; i >= 0; i--) { this.hostilityLevels[i] -= this.coolOffRate; if (this.hostilityLevels[i] <= 0) { this.removeHostility(i, this.hostilityTargets[i]); } if (this.hostilityLevels[i] < this.hostileThreshold) { let hostile = this.hostileTargets.indexOf(this.hostilityTargets[i]); if (hostile != -1) { this.hostileTargets.splice(hostile, 1); } } } } public getPoked (action : Action) { this.reactingTo = action; if (this.actor instanceof Person) { //AIRules.getPoked(this.actor, action); return AI.reacttoRules.execute({ noun : this.actor }, ...this.extraReacttoRules); } } public answerTo (noun : TalkingHeads) { return AI.talktoRules.execute({ noun : noun }, ...this.extraTalktoRules); } public interrogateTo (noun : TalkingHeads) { return AI.investigateRules.execute({ noun : noun }, ...this.extraInvestigateRules); } } module AIRules { /** * This is or behavioral rules regarding something that is happening RIGHT NOW. * i.e. Rule for what a monster does when the player has just insulted them, or for when the player triggers an alarm, etc. * @type {number} */ export var PRIORITY_ACTING_ON_SITUATION = 5; /** * This is for behavioral rules about what the NPC SEES. * i.e. Is there a shiny on the ground for me to take? Do I see the player and if so how do I feel about it? * @type {number} */ export var PRIORITY_ACTING_ON_PLACE = 3; /** * This is for rules for when the NPC has nothing better to do. * i.e. Standard guarding routes, etc. * @type {number} */ export var PRIORITY_ACTING_ON_IDLE = 1; } module AIDialogueRules { /** * This is for when the NPC must talk about something that is currently happening. * For "Talk To", this means that whatever is happening is much more important than whatever the player might want to talk about. * Like if the player is currently fighting the NPC, or the player has just killed the NPC's best friend and this is kind of more urgent. * @type {number} */ export var PRIORITY_TALKING_ABOUT_SITUATION = 5; /** * This is for rules for when the NPC has nothing better to do. * For "Talk To", this is the default rule for when the player talks to the enemy. We'll probably only have one rule here for each npc type, possibly more than one only if the NPC's demeanor changes, idk. * @type {number} */ export var PRIORITY_ACTING_ON_IDLE = 1; }