Skip to content

Commit 52d3d45

Browse files
committed
added combat behaviors for all actors - added default melee behavior and auto attack implementation - added event event emitters for combat-related events
added combat related properties to base classes
1 parent f426256 commit 52d3d45

File tree

11 files changed

+592
-210
lines changed

11 files changed

+592
-210
lines changed

src/game-engine/world/action/magic-on-npc.action.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export type magic_on_npcActionHandler = (buttonAction: Magic_On_NPCAction) => vo
3131
*/
3232
export interface Magic_On_NPCAction {
3333
// The npc world id that was clicked on after choosing the spell
34-
npc: Npc,
34+
npc: Npc;
3535
// The player performing the action.
3636
player: Player;
3737
// The ID of the UI widget that the button is on.
@@ -48,11 +48,11 @@ export interface Magic_On_NPCAction {
4848
* @param widgetId
4949
* @param buttonId
5050
*/
51-
const buttonActionPipe = (npc:Npc, player: Player, widgetId: number, buttonId: number): RunnableHooks<Magic_on_NPCAction> => {
51+
const buttonActionPipe = (npc:Npc, player: Player, widgetId: number, buttonId: number): RunnableHooks<Magic_On_NPCAction> => {
5252
//console.info(`pew pew you use magic on ${npc.name}!`);
5353

5454
// Find all object action plugins that reference this location object
55-
let matchingHooks = getActionHooks<Magic_On_NPCActionHook>('magic_on_npc');
55+
const matchingHooks = getActionHooks<Magic_On_NPCActionHook>('magic_on_npc');
5656

5757

5858
return {

src/game-engine/world/actor/actor.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { WalkingQueue } from './walking-queue';
22
import { ItemContainer } from '../items/item-container';
33
import { Animation, DamageType, Graphic, UpdateFlags } from './update-flags';
44
import { Npc } from './npc/npc';
5-
import { Skills } from '@engine/world/actor/skills';
5+
import { Skill, Skills } from '@engine/world/actor/skills';
66
import { Item } from '@engine/world/items/item';
77
import { Position } from '@engine/world/position';
88
import { DirectionData, directionFromIndex } from '@engine/world/direction';
@@ -14,6 +14,11 @@ import { WorldInstance } from '@engine/world/instances';
1414
import { Player } from '@engine/world/actor/player/player';
1515
import { ActionCancelType, ActionPipeline } from '@engine/world/action';
1616
import { LandscapeObject } from '@runejs/filestore';
17+
import { Behavior } from './behaviors/behavior';
18+
import { soundIds } from '../config/sound-ids';
19+
import { animationIds } from '../config/animation-ids';
20+
import { findNpc } from '../../config';
21+
import { itemIds } from '../config/item-ids';
1722

1823

1924
/**
@@ -59,20 +64,34 @@ export abstract class Actor {
5964
this._busy = false;
6065
}
6166

62-
public damage(amount: number, damageType: DamageType = DamageType.DAMAGE): 'alive' | 'dead' {
63-
let remainingHitpoints: number = this.skills.hitpoints.level - amount;
64-
const maximumHitpoints: number = this.skills.hitpoints.levelForExp;
65-
if(remainingHitpoints < 0) {
66-
remainingHitpoints = 0;
67-
}
68-
69-
this.skills.setHitpoints(remainingHitpoints);
67+
public damage(amount: number, damageType: DamageType = DamageType.DAMAGE) {
68+
let armorReduction = 0;
69+
let spellDamageReduction = 0;
70+
let poisonReistance = 0;
71+
amount -= armorReduction;
72+
this.hitPoints -= amount;
73+
this.skills.setHitpoints(this.hitPoints);
7074
this.updateFlags.addDamage(amount, amount === 0 ? DamageType.NO_DAMAGE : damageType,
71-
remainingHitpoints, maximumHitpoints);
72-
73-
return remainingHitpoints === 0 ? 'dead' : 'alive';
75+
this.hitPoints, this.maxHitPoints);
76+
//this actor should respond when hit
77+
world.playLocationSound(this.position, soundIds.npc.human.noArmorHitPlayer,5)
78+
this.playAnimation(this.getBlockAnimation());
7479
}
7580

81+
//public damage(amount: number, damageType: DamageType = DamageType.DAMAGE): 'alive' | 'dead' {
82+
// let remainingHitpoints: number = this.skills.hitpoints.level - amount;
83+
// const maximumHitpoints: number = this.skills.hitpoints.levelForExp;
84+
// if(remainingHitpoints < 0) {
85+
// remainingHitpoints = 0;
86+
// }
87+
88+
// this.skills.setHitpoints(remainingHitpoints);
89+
// this.updateFlags.addDamage(amount, amount === 0 ? DamageType.NO_DAMAGE : damageType,
90+
// remainingHitpoints, maximumHitpoints);
91+
92+
// return remainingHitpoints === 0 ? 'dead' : 'alive';
93+
//}
94+
7695
/**
7796
* Waits for the actor to reach the specified position before resolving it's promise.
7897
* The promise will be rejected if the actor's walking queue changes or their movement is otherwise canceled.
@@ -548,4 +567,23 @@ export abstract class Actor {
548567
};
549568
}
550569

570+
// #region Behaviors and Combat flags/checks
571+
public inCombat: boolean = false;
572+
public meleeDistance: number = 1;
573+
public Behaviors: Behavior[] = new Array();
574+
public isDead: boolean = false;
575+
public combatTargets: Actor[] = new Array();
576+
public hitPoints = this.skills.hitpoints.level * 4;
577+
public maxHitPoints = this.skills.hitpoints.level * 4;
578+
public get highestCombatSkill(): Skill {
579+
let attack = this.skills.getLevel('attack');
580+
let magic = this.skills.getLevel('magic');
581+
let ranged = this.skills.getLevel('ranged');
582+
583+
if (ranged > magic && ranged > ranged) return ranged;
584+
else if (magic > attack && magic > ranged) return magic;
585+
else return attack;
586+
}
587+
588+
// #endregion
551589
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Actor } from '../actor';
2+
import { regionChangeActionFactory } from '@engine/world/action/region-change.action';
3+
import { Subject } from 'rxjs';
4+
import { logger } from '@runejs/core';
5+
import { Behavior, BehaviorType } from './behavior';
6+
import { Timestamp } from 'rxjs/dist/types/internal/types';
7+
import { timestamp } from 'rxjs/dist/types/operators';
8+
import { Npc } from '../npc/npc';
9+
import { Player } from '../player/player';
10+
11+
export class AutoAttackBehavior extends Behavior {
12+
private _combatPulse;
13+
Type = BehaviorType.Combat;
14+
Name = "auto-attack-combat";
15+
//seconds
16+
private _CoolDown: number = 3;
17+
private _lastAttack = new Date();
18+
private _player: Player;
19+
//this should be called when combat starts
20+
public async init(me: Actor, them: Actor): Promise<void> {
21+
this.Me = me;
22+
this.Them = them;
23+
this._player = (me as Player);
24+
console.log('all set to auto attack!');
25+
(this.Them as Npc).npcEvents.on('death', (npc) => this._player.onNpcKill);
26+
super.init(me, them);
27+
}
28+
29+
public async tick() {
30+
if (this.Me == null) return;
31+
32+
return new Promise<void>(resolve => {
33+
//only use boolean checks in behaviors never calculated values if you can help it (performance)
34+
if (this.Me.inCombat) {
35+
if (this.Them.isDead) {
36+
this.Me.combatTargets.pop();
37+
resolve();
38+
return;
39+
}
40+
if (this.Distance > 25) {
41+
this.Me.inCombat = false;
42+
console.log('target too far away - ending combat');
43+
resolve();
44+
}
45+
46+
//If we are not in range move there
47+
if (this.Distance > this.Me.meleeDistance) this.Me.moveTo(this.Them);
48+
//Are you in range to attack?
49+
if (this.Distance <= this.Me.meleeDistance && this.coolDownElapsed) {
50+
this.doAttack();
51+
this.resetCoolDown(this._CoolDown);
52+
}
53+
54+
if (!this.Me.isDead) super.tick();
55+
resolve();
56+
}
57+
58+
59+
});
60+
}
61+
public async doAttack(): Promise<void> {
62+
return new Promise<void>(resolve => {
63+
//do attack stuff
64+
let _damage = this.Me.skills.strength.level;
65+
console.log(`you attack ${(this.Them as Npc).name} for ${_damage} damage! (after the CD)`);
66+
this.Them.damage(_damage);
67+
if (this.Them.hitPoints <= 0) {
68+
(this.Them as Npc).npcEvents.emit('death', this.Me);
69+
}
70+
71+
});
72+
}
73+
public get coolDownElapsed(): boolean {
74+
if (new Date() > this._lastAttack) return true;
75+
return false;
76+
}
77+
public resetCoolDown(seconds: number): void {
78+
this._lastAttack.setSeconds(this._lastAttack.getSeconds() + seconds);
79+
}
80+
81+
82+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Actor } from '../actor';
2+
import { regionChangeActionFactory } from '@engine/world/action/region-change.action';
3+
import { Subject } from 'rxjs';
4+
import { Npc } from '../npc/npc';
5+
import { Player } from '../player/player';
6+
7+
export abstract class Behavior {
8+
//because not all interaction between npcs will be combat oriented me/them is on the base class
9+
public Me: Actor;
10+
public Them: Actor;
11+
public Distance: number;
12+
public Name: string;
13+
public Type: BehaviorType;
14+
//Be sure to call the tick super so you wont have to duplicate all the functionality
15+
public async tick() {
16+
if (this.Me.isDead) return;
17+
//If we have more than one combat target be sure to select the next one
18+
if (this.Them == null && this.Me != null && this.Me.combatTargets.length > 0) this.Them == this.Me.combatTargets[0];
19+
//update calculated variables
20+
if (this.Them != null) this.Distance = this.Me.position.distanceBetween(this.Them.position);
21+
22+
return new Promise<void>(resolve => { resolve(); });
23+
}
24+
public async init(me: Actor = null, them: Actor = null) {
25+
if (me != null && them != null) me.combatTargets.push(them);
26+
return new Promise<void>(resolve => {
27+
resolve();
28+
});
29+
}
30+
}
31+
32+
33+
export enum BehaviorType {
34+
//movement
35+
Roaming = 'roaming', //world.tickComplete
36+
Chase = 'chase', //position.distanceBetween
37+
//combat
38+
Combat = 'combat',
39+
Flee = 'flee',
40+
41+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Actor } from '../actor';
2+
import { regionChangeActionFactory } from '@engine/world/action/region-change.action';
3+
import { Subject } from 'rxjs';
4+
import { logger } from '@runejs/core';
5+
import { Behavior, BehaviorType } from './behavior';
6+
import { Timestamp } from 'rxjs/dist/types/internal/types';
7+
import { timestamp } from 'rxjs/dist/types/operators';
8+
import { Player } from '../player/player';
9+
import { Npc } from '../npc/npc';
10+
11+
export class MeleeCombatBehavior extends Behavior {
12+
private _combatPulse;
13+
Type = BehaviorType.Combat;
14+
Name = "basic-melee-combat";
15+
//seconds
16+
private _CoolDown: number = 4;
17+
private _lastAttack = new Date();
18+
19+
//this should be called when combat starts
20+
public async init(me: Actor, them: Actor): Promise<void> {
21+
this.Me = me;
22+
this.Them = them;
23+
super.init(me, them);
24+
}
25+
26+
public async tick() {
27+
if (this.Me == null) return;
28+
29+
return new Promise<void>(resolve => {
30+
if (this.Me.inCombat && this.Me.hitPoints <= 0) {
31+
32+
this.Me.inCombat = false;
33+
this.Me.isDead = true;
34+
(this.Me as Npc).kill(false);
35+
if (this.Them.isNpc) (this.Them as Player).playerEvents.emit('kill', this.Them);
36+
37+
}
38+
//only use boolean checks in behaviors never calculated values if you can help it (performance)
39+
if (this.Me.inCombat) {
40+
if (this.Distance > 5) {
41+
this.Me.inCombat = false;
42+
console.log('You or your target has fled from combat!');
43+
resolve();
44+
}
45+
46+
//If we are not in range move there
47+
if (this.Distance > this.Me.meleeDistance) this.Me.moveTo(this.Them);
48+
//Are you in range to attack?
49+
if (this.Distance <= this.Me.meleeDistance && this.coolDownElapsed) {
50+
this.doAttack();
51+
this.resetCoolDown(this._CoolDown);
52+
}
53+
54+
if (!this.Me.isDead) super.tick();
55+
resolve();
56+
}
57+
58+
59+
});
60+
}
61+
public async doAttack():Promise<void> {
62+
return new Promise<void>(resolve => {
63+
//do attack stuff
64+
this.Me.playAnimation(this.Me.getAttackAnimation());
65+
66+
let _damage = this.Me.skills.strength.level;
67+
console.log(`${(this.Me as Npc).name} attacks ${(this.Them as Player).username} for ${_damage} damage! (after the CD)`);
68+
(this.Me as Npc).npcEvents.emit('damage', _damage);
69+
this.Them.damage(_damage);
70+
});
71+
}
72+
public get coolDownElapsed(): boolean {
73+
if (new Date() > this._lastAttack) return true;
74+
return false;
75+
}
76+
public resetCoolDown(seconds:number):void {
77+
this._lastAttack.setSeconds(this._lastAttack.getSeconds() + seconds);
78+
}
79+
80+
81+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export interface Magic {
2+
Name: string;
3+
ButtonID: number;
4+
CoolDown: number;
5+
BaseDamage: number;
6+
7+
DamageCalculation(): number;
8+
9+
}
10+
export abstract class Magic {
11+
12+
13+
}
14+

0 commit comments

Comments
 (0)