Skip to content

Commit f140ebd

Browse files
committed
- dynamic patrolling system with obstacles and edges detection
1 parent 815e171 commit f140ebd

File tree

5 files changed

+148
-94
lines changed

5 files changed

+148
-94
lines changed

public/assets/tilemaps/forest-map.json

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -451,63 +451,39 @@
451451
},
452452
{
453453
"height":0,
454-
"id":18,
454+
"id":24,
455455
"name":"pixie_ennemy_tag",
456-
"polyline":[
457-
{
458-
"x":0,
459-
"y":0
460-
},
461-
{
462-
"x":-258.666666666667,
463-
"y":1.33333333333331
464-
}],
456+
"point":true,
465457
"rotation":0,
466458
"type":"ennemy_patrol",
467459
"visible":true,
468460
"width":0,
469-
"x":691,
470-
"y":391.833333333333
461+
"x":737,
462+
"y":375.5
471463
},
472464
{
473465
"height":0,
474-
"id":20,
466+
"id":25,
475467
"name":"pixie_ennemy_tag",
476-
"polyline":[
477-
{
478-
"x":0,
479-
"y":0
480-
},
481-
{
482-
"x":-131.333666666667,
483-
"y":1.33333
484-
}],
468+
"point":true,
485469
"rotation":0,
486470
"type":"ennemy_patrol",
487471
"visible":true,
488472
"width":0,
489-
"x":982.833333333333,
490-
"y":194.666666666667
473+
"x":912,
474+
"y":187
491475
},
492476
{
493477
"height":0,
494-
"id":23,
478+
"id":26,
495479
"name":"pixie_ennemy_tag",
496-
"polyline":[
497-
{
498-
"x":0,
499-
"y":0
500-
},
501-
{
502-
"x":-131.334,
503-
"y":1.33333
504-
}],
480+
"point":true,
505481
"rotation":0,
506482
"type":"ennemy_patrol",
507483
"visible":true,
508484
"width":0,
509-
"x":1253,
510-
"y":387.5
485+
"x":1186.5,
486+
"y":373.5
511487
}],
512488
"opacity":1,
513489
"type":"objectgroup",
@@ -516,7 +492,7 @@
516492
"y":0
517493
}],
518494
"nextlayerid":10,
519-
"nextobjectid":24,
495+
"nextobjectid":27,
520496
"orientation":"orthogonal",
521497
"renderorder":"right-down",
522498
"tiledversion":"1.11.0",

src/game-objects/ennemy.game-object.ts

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export interface EnnemyConfig {
99
scene: Scene;
1010
x: number;
1111
y: number;
12-
patrolDistance: number;
1312
chaseDistance: number;
1413
speed: number;
1514
patrolSpeed: number;
@@ -25,10 +24,11 @@ export interface EnnemyConfig {
2524
export class Ennemy extends ArcadeSprite {
2625
public declare body: ArcadeBody;
2726

27+
public get speed(): number {
28+
return this._config.speed;
29+
}
30+
2831
private _player: Hero;
29-
private _startingX: number;
30-
private _patrolDirection = 1;
31-
private _patrolTween: Phaser.Tweens.Tween | null = null;
3232
private _config: EnnemyConfig;
3333
private _isAttacking = false;
3434
private _canAttack = true;
@@ -37,16 +37,14 @@ export class Ennemy extends ArcadeSprite {
3737
body: ArcadeBody;
3838
};
3939

40-
private get _isPatrolling(): boolean {
41-
return this._patrolTween !== null;
42-
}
40+
private _direction = 1;
41+
private _isPatrolling = false;
4342
private get _physics(): Phaser.Physics.Arcade.ArcadePhysics {
4443
return this.scene.physics;
4544
}
4645

4746
constructor(config: EnnemyConfig, player: Hero) {
4847
super(config.scene, config.x, config.y, config.sprite);
49-
this._startingX = config.x;
5048
this._config = config;
5149

5250
this._player = player;
@@ -82,10 +80,12 @@ export class Ennemy extends ArcadeSprite {
8280
this
8381
);
8482

83+
this._direction = Phaser.Math.Distance.Between(this.x, this.y, this._player.x, this._player.y) > 0 ? 1 : -1;
84+
8585
this.startPatrol();
8686
}
8787

88-
public update(): void {
88+
public update(_: number, __: number): void {
8989
if (this._config.hp <= 0 || this._isAttacking) {
9090
return;
9191
}
@@ -102,7 +102,11 @@ export class Ennemy extends ArcadeSprite {
102102
this.chasePlayer();
103103
} //
104104
else if (distanceToPlayer > this._config.chaseDistance && !this._isPatrolling) {
105-
this.returnToStart();
105+
this.startPatrol();
106+
}
107+
108+
if (this._isPatrolling) {
109+
this.updatePatrol();
106110
}
107111

108112
GameHelper.animate(this, AnimationTag.ENNEMY_MOVING, {
@@ -126,23 +130,36 @@ export class Ennemy extends ArcadeSprite {
126130
}
127131
}
128132

133+
public knockback(strength: number, direction: number): void {
134+
this.body.setVelocityX((strength / this.body.mass) * direction);
135+
136+
this.scene.time.delayedCall(400, () => {
137+
// ennemy is dead
138+
if (!this.body) {
139+
return;
140+
}
141+
const speed = this._isPatrolling ? this._config.patrolSpeed : this._config.speed;
142+
this.body.setVelocityX(speed * this._direction);
143+
});
144+
}
145+
129146
private startPatrol(): void {
130-
if (this._patrolTween !== null) {
131-
return;
132-
}
147+
this._isPatrolling = true;
133148

134149
GameHelper.animate(this, AnimationTag.ENNEMY_MOVING);
135-
this._patrolDirection = Math.sign(this._config.patrolDistance);
136-
137-
this._patrolTween = this.scene.tweens.add({
138-
targets: this,
139-
x: this._startingX + this._config.patrolDistance,
140-
duration: (Math.abs(this._config.patrolDistance) / this._config.patrolSpeed) * 1000,
141-
yoyo: true,
142-
repeat: -1,
143-
onYoyo: () => (this._patrolDirection *= -1),
144-
onRepeat: () => (this._patrolDirection *= -1),
145-
});
150+
151+
this.body.setVelocityX(this._config.patrolSpeed * this._direction);
152+
}
153+
154+
private stopPatrol(): void {
155+
this._isPatrolling = false;
156+
}
157+
158+
private updatePatrol(): void {
159+
if (GameHelper.isObstacleAhead(this, this.scene) || GameHelper.isLedgeAhead(this, this.scene)) {
160+
this._direction *= -1;
161+
this.body.setVelocityX(this._config.patrolSpeed * this._direction);
162+
}
146163
}
147164

148165
private attack(): void {
@@ -151,9 +168,9 @@ export class Ennemy extends ArcadeSprite {
151168
}
152169

153170
this.scene.sound.play(SfxTag.PIXIE_ATTACK);
154-
const direction = this.flipX ? -1 : 1;
171+
155172
const hitboxPosition = {
156-
x: this.x + direction * this._attackHitbox.width,
173+
x: this.x + this._direction * this._attackHitbox.width,
157174
y: this.y,
158175
};
159176

@@ -183,39 +200,22 @@ export class Ennemy extends ArcadeSprite {
183200
});
184201
}
185202

186-
private stopPatrol(): void {
187-
if (this._patrolTween !== null) {
188-
this._patrolTween.stop();
189-
this._patrolTween = null;
190-
}
191-
}
192-
193203
private chasePlayer(): void {
194-
const direction = Math.sign(this._player.x - this.x);
195-
this.body.setVelocityX(direction * this._config.speed);
196-
}
197-
198-
private returnToStart(): void {
199-
if (GameHelper.isCloseEnough(this.x, this._startingX)) {
200-
this.body.setVelocityX(0);
201-
this.startPatrol();
202-
} else {
203-
const direction = Math.sign(this._startingX - this.x);
204-
this.body.setVelocityX(direction * this._config.patrolSpeed);
205-
}
204+
this._direction = Math.sign(this._player.x - this.x);
205+
this.body.setVelocityX(this._direction * this._config.speed);
206206
}
207207

208208
private updateFlipX(): void {
209209
// Patrol is using a tween, which updates X position instead of velocity
210210
if (this._isPatrolling) {
211-
this.setFlipX(this._patrolDirection < 0);
211+
this.setFlipX(this._direction < 0);
212212
} //
213213
else {
214214
const distanceToPlayer = Phaser.Math.Distance.Between(this.x, this.y, this._player.x, this._player.y);
215215

216216
if (distanceToPlayer <= this._config.chaseDistance) {
217-
const playerDirection = Math.sign(this._player.x - this.x);
218-
this.setFlipX(playerDirection < 0);
217+
this._direction = Math.sign(this._player.x - this.x);
218+
this.setFlipX(this._direction < 0);
219219
} //
220220
else {
221221
this.setFlipX(this.body.velocity.x < 0);

src/helpers/game.helper.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Sprite, Tween } from "@phaser-aliases";
1+
import { Sprite, SpriteWithDynamicBody, Tween } from "@phaser-aliases";
22
import { AnimationTag } from "@tags";
33
import { Scene } from "phaser";
44

@@ -66,4 +66,71 @@ export class GameHelper {
6666
yoyo: true,
6767
});
6868
}
69+
70+
public static isObstacleAhead(
71+
object: SpriteWithDynamicBody,
72+
scene: Scene,
73+
params?: {
74+
debug: boolean;
75+
debugColor?: number;
76+
}
77+
): boolean {
78+
// body origin is top left. we check the center of the sprite
79+
const posY = object.body.y + object.body.height / 2;
80+
return GameHelper.isColliderAtY(object, scene, posY, params);
81+
}
82+
83+
public static isLedgeAhead(
84+
object: SpriteWithDynamicBody,
85+
scene: Scene,
86+
params?: {
87+
debug: boolean;
88+
debugColor?: number;
89+
}
90+
): boolean {
91+
// body origin is top left. we check the bottom of the sprite + arbitrary offset of 10px
92+
const posY = object.body.y + object.body.height + 10;
93+
return !GameHelper.isColliderAtY(object, scene, posY, params);
94+
}
95+
96+
private static isColliderAtY(
97+
object: SpriteWithDynamicBody,
98+
scene: Scene,
99+
yPos: number,
100+
params?: {
101+
debug: boolean;
102+
debugColor?: number;
103+
}
104+
): boolean {
105+
const direction = object.flipX ? "LEFT" : "RIGHT";
106+
const baseOffset = object.body.width / 2;
107+
// body origin is top left, so we add the width of the sprite if looking right.
108+
const offsetX = direction === "RIGHT" ? baseOffset + object.body.width : -baseOffset;
109+
const checkX = object.body.x + offsetX;
110+
const checkY = yPos;
111+
112+
// Create a test groundCheckRect at the given position
113+
const groundCheckRect = new Phaser.Geom.Rectangle(checkX, checkY, 2, 2);
114+
const { x, y, width, height } = groundCheckRect;
115+
// Check if any physical object is overlapping with our test groundCheckRect
116+
const overlappingObjects = scene.physics.overlapRect(x, y, width, height, true, true);
117+
118+
if (params?.debug) {
119+
const debugGraphics = scene.add.graphics();
120+
debugGraphics.clear();
121+
debugGraphics.setDepth(1000000);
122+
debugGraphics.lineStyle(2, params.debugColor ?? 0xff0000);
123+
debugGraphics.strokeRect(checkX, checkY, 2, 2);
124+
125+
scene.time.addEvent({
126+
delay: 1000,
127+
callback: () => debugGraphics.destroy(),
128+
callbackScope: this,
129+
loop: false,
130+
});
131+
}
132+
133+
// TODO: check for static objects only
134+
return overlappingObjects.filter((obj) => obj instanceof Phaser.Physics.Arcade.StaticBody).length > 0;
135+
}
69136
}

src/levels/level-1/1-forest.level.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ export class ForestLevel {
110110
this.setCollisions();
111111
}
112112

113-
public update(): void {
113+
public update(time: number, delta: number): void {
114114
this._ennemies.getChildren().forEach((ennemy) => {
115-
ennemy.update();
115+
ennemy.update(time, delta);
116116
});
117117
}
118118

@@ -163,6 +163,8 @@ export class ForestLevel {
163163
.forEach((collider) => {
164164
collider.alpha = 0;
165165
});
166+
167+
this._scene.physics.world.enable(this._colliderGroup);
166168
}
167169

168170
private addHero(): void {
@@ -196,11 +198,11 @@ export class ForestLevel {
196198
const patrols = positions.filter((position) => position.type === "ennemy_patrol");
197199

198200
patrols.forEach((patrol, index) => {
199-
if (!patrol.x || !patrol.y || !patrol.polyline || patrol.polyline.length < 2) {
201+
if (!patrol.x || !patrol.y) {
200202
throw Error("Invalid patrol object");
201203
}
202204

203-
const { x, y, polyline } = patrol;
205+
const { x, y } = patrol;
204206
const ennemyTag = patrol.name;
205207

206208
if (!isEnumValue(EnnemyTag, ennemyTag)) {
@@ -211,7 +213,6 @@ export class ForestLevel {
211213
x,
212214
y,
213215
chaseDistance: 200,
214-
patrolDistance: polyline[1].x,
215216
speed: this.hero.speed - 80,
216217
patrolSpeed: 40,
217218
sprite: ennemyTag,
@@ -251,6 +252,16 @@ export class ForestLevel {
251252
throw Error("ennemy is not a sprite");
252253
}
253254

255+
if (!("body" in projectile)) {
256+
throw Error("projectile has no body");
257+
}
258+
259+
// TODO: compute strength based on ennemy mass
260+
const knockbackStrength = 50;
261+
const direction = projectile.body.velocity.x > 0 ? 1 : -1;
262+
263+
ennemy.body.setVelocityX(knockbackStrength * direction);
264+
ennemy.knockback(knockbackStrength, direction);
254265
ennemy.hurt(this.hero.damage);
255266
projectile.destroy();
256267
});

0 commit comments

Comments
 (0)