-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathEnemy.java
More file actions
311 lines (273 loc) · 10.8 KB
/
Copy pathEnemy.java
File metadata and controls
311 lines (273 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/*
* Authors: Jerry Li & Victor Jiang
* Date: June 13, 2025
* Description: This class creates the frameworks for a basic autonomous enemy with pathfinding toward players,
* avoiding of obstacles, separation from other enemies, and wandering behaviour
*/
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
public class Enemy extends Entity {
public final float ENEMY_MAX_SPEED;
public static final float ENEMY_ACCELERATION = 0.6f;
public static final float ENEMY_DECELERATION = 0.8f;
public static final float SEPARATION_RADIUS = 120f;
public static final float SEPARATION_RADIUS_SQ = SEPARATION_RADIUS * SEPARATION_RADIUS;
public static final float WANDER_STRENGTH_BASE = 0.45f;
public static final float AVOIDANCE_DIST = 50f;
public static final float SPRITE_UPDATE_INTERVAL = 0.300f; // seconds
public static Random random = new Random();
public GamePanel gamePanel;
public BufferedImage[] sprites;
public CopyOnWriteArrayList<DamageNumber> damageNumbers = new CopyOnWriteArrayList<>();
public int currentSpriteIndex = 0;
public float spriteTimer = 0f;
public float stunTimer = 0f;
public int damage = 2;
public float wanderAngle = random.nextFloat() * (float) (Math.PI * 2);
public float targetOffsetX = (random.nextFloat() - 0.5f) * 60f;
public float targetOffsetY = (random.nextFloat() - 0.5f) * 60f;
public float offsetTimer = 0f;
public Enemy(int x, int y, int width, int height, int max_hp, float max_speed,
BufferedImage[] resizedSprites, GamePanel gamePanel) {
super(x, y, width, height, max_hp, (int) max_speed, resizedSprites);
this.ENEMY_MAX_SPEED = max_speed;
this.gamePanel = gamePanel;
this.sprites = resizedSprites; // assume already resized before passing in
}
// Contains some logic based off of
// https://ijssst.info/Vol-05/No-1&2/TOMLINSON.pdf
public void move(float dt, Player player) {
float dt60 = dt * 60f;
float px, py, cx, cy, dx, dy;
float distSq, dist, toX, toY;
float desiredSpeed, t = 0f;
float sepX = 0f, sepY = 0f;
int neighbors = 0, otherIndex = 0;
float thisCenterX, thisCenterY;
List<Enemy> allEnemies;
Enemy other = null, hit = null;
float odx = 0f, ody = 0f, dSq = 0f, strength = 0f, invD = 0f;
float sepMagSq = 0f, maxSpeedSq = 0f, invMag = 0f;
float wanderStrength = 0f, wx = 0f, wy = 0f;
float chaseX = 0f, chaseY = 0f, steerChaseX = 0f, steerChaseY = 0f;
float steerSepX = 0f, steerSepY = 0f, steerWanderX = 0f, steerWanderY = 0f;
final float wChase = 1.0f;
final float wSep = 0.5f;
final float wWander = 0.3f;
float steerX = 0f, steerY = 0f;
float currentSpeed = 0f, currentAngle = 0f, desiredAngle = 0f;
float angleDiff = 0f, maxTurn = 0f, newAngle = 0f;
float speedDiff = 0f, accel = 0f;
float fx = 0f, fy = 0f, avoidance = 0f, avoidAngle = 0f;
if (stunTimer > 0f) {
stunTimer -= dt;
if (stunTimer < 0f) stunTimer = 0f;
return;
}
// 1) Offset update once every 2 seconds
offsetTimer += dt;
if (offsetTimer > 2.0f) {
offsetTimer = 0f;
targetOffsetX = (random.nextFloat() - 0.5f) * 60f;
targetOffsetY = (random.nextFloat() - 0.5f) * 60f;
}
// 2) Direction to player (with offset)
px = (float) player.getCenterX() + targetOffsetX;
py = (float) player.getCenterY() + targetOffsetY;
cx = (float) getCenterX();
cy = (float) getCenterY();
dx = px - cx;
dy = py - cy;
distSq = dx * dx + dy * dy;
if (distSq < 1e-6f)
distSq = 1e-6f;
dist = (float) Math.sqrt(distSq);
toX = dx / dist;
toY = dy / dist;
// 3) Desired speed (smooth interpolation when close)
if (dist < 110f) {
t = dist / 110f;
desiredSpeed = ENEMY_MAX_SPEED * (t * t * (3 - 2 * t));
} else {
desiredSpeed = ENEMY_MAX_SPEED;
}
// 4) Separation from neighbors (avoid O(N^2) hypot calls when possible)
thisCenterX = cx;
thisCenterY = cy;
allEnemies = gamePanel.enemies;
other = null;
otherIndex = 0;
for (otherIndex = 0; otherIndex < allEnemies.size(); otherIndex++) {
other = allEnemies.get(otherIndex);
if (other == this)
continue;
odx = thisCenterX - (float) other.getCenterX();
ody = thisCenterY - (float) other.getCenterY();
dSq = odx * odx + ody * ody;
if (dSq > 0 && dSq < SEPARATION_RADIUS_SQ) {
// strength = (2.0f * SEPARATION_RADIUS) / dSq
strength = (2.0f * SEPARATION_RADIUS) / dSq;
invD = 1f / (float) Math.sqrt(dSq);
sepX += (odx * invD) * strength;
sepY += (ody * invD) * strength;
neighbors++;
}
}
if (neighbors > 0) {
sepMagSq = sepX * sepX + sepY * sepY;
maxSpeedSq = ENEMY_MAX_SPEED * ENEMY_MAX_SPEED;
if (sepMagSq > maxSpeedSq) {
invMag = 1f / (float) Math.sqrt(sepMagSq);
sepX = sepX * invMag * ENEMY_MAX_SPEED;
sepY = sepY * invMag * ENEMY_MAX_SPEED;
}
}
// 5) Wandering component
wanderAngle += (random.nextFloat() - 0.5f) * 0.2f;
wanderAngle *= 0.98f;
wanderStrength = dist < 200f ? dist / 200f : 1f;
wx = (float) Math.cos(wanderAngle) * WANDER_STRENGTH_BASE * wanderStrength;
wy = (float) Math.sin(wanderAngle) * WANDER_STRENGTH_BASE * wanderStrength;
// 6) Steering vectors
chaseX = toX * desiredSpeed;
chaseY = toY * desiredSpeed;
steerChaseX = chaseX - x_velocity;
steerChaseY = chaseY - y_velocity;
steerSepX = sepX - x_velocity;
steerSepY = sepY - y_velocity;
steerWanderX = wx - x_velocity;
steerWanderY = wy - y_velocity;
steerX = steerChaseX * wChase + steerSepX * wSep + steerWanderX * wWander;
steerY = steerChaseY * wChase + steerSepY * wSep + steerWanderY * wWander;
// 7) Smooth rotation & speed update
currentSpeed = (float) Math.sqrt(x_velocity * x_velocity + y_velocity * y_velocity);
currentAngle = (float) Math.atan2(y_velocity, x_velocity);
desiredAngle = (float) Math.atan2(steerY + y_velocity, steerX + x_velocity);
angleDiff = desiredAngle - currentAngle;
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
} else if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
maxTurn = 0.12f * dt60;
if (angleDiff > maxTurn) {
angleDiff = maxTurn;
} else if (angleDiff < -maxTurn) {
angleDiff = -maxTurn;
}
newAngle = currentAngle + angleDiff;
speedDiff = desiredSpeed - currentSpeed;
accel = ((speedDiff > 0f) ? ENEMY_ACCELERATION : ENEMY_DECELERATION) * dt60;
currentSpeed += Math.signum(speedDiff) * Math.min(Math.abs(speedDiff), accel);
x_velocity = (float) Math.cos(newAngle) * currentSpeed;
y_velocity = (float) Math.sin(newAngle) * currentSpeed;
// 8) Obstacle avoidance raycast (no Vector object)
fx = (float) Math.cos(newAngle);
fy = (float) Math.sin(newAngle);
hit = gamePanel.raycastEnemy(this, cx, cy, fx, fy, AVOIDANCE_DIST);
if (hit != null) {
avoidance = (random.nextBoolean() ? 1f : -1f) * 0.44f;
avoidAngle = newAngle + avoidance;
x_velocity = (float) Math.cos(avoidAngle) * currentSpeed * 0.8f;
y_velocity = (float) Math.sin(avoidAngle) * currentSpeed * 0.8f;
}
// 9) Apply movement & wrap
x += x_velocity * dt60;
y += y_velocity * dt60;
wrap();
spriteTimer += dt;
if (spriteTimer >= SPRITE_UPDATE_INTERVAL) {
currentSpriteIndex = (currentSpriteIndex + 1) % sprites.length;
spriteTimer -= SPRITE_UPDATE_INTERVAL;
}
}
// Wraps the enemy around the screen edges
public void wrap() {
if (x < -WIDTH) {
x = GamePanel.GAME_WIDTH;
} else if (x > GamePanel.GAME_WIDTH) {
x = -WIDTH;
}
if (y < -HEIGHT) {
y = GamePanel.GAME_HEIGHT;
} else if (y > GamePanel.GAME_HEIGHT) {
y = -HEIGHT;
}
}
/** Apply instant knockback velocity */
public void applyKnockback(float vx, float vy) {
x_velocity = vx;
y_velocity = vy;
}
public boolean isKnockedBack() {
return x_velocity != 0f || y_velocity != 0f;
}
public void applyStun(float duration) {
// Reset the stun timer so stuns do not stack
stunTimer = duration;
}
@Override
public void takeDamage(int dmg) {
DamageNumber damageNum;
damageNum = new DamageNumber(
(int) getCenterX(),
(int) getCenterY() - 20,
dmg);
damageNumbers.add(damageNum);
super.takeDamage(dmg);
}
/** Update and remove dead damage numbers without extra allocations */
public void updateDamageNumbers(float dt) {
int i = 0;
DamageNumber dn = null;
for (i = damageNumbers.size() - 1; i >= 0; i--) {
dn = damageNumbers.get(i);
dn.update(dt);
if (dn.isDead()) {
damageNumbers.remove(i);
}
}
}
public int getDamage() {
return damage;
}
/**
* Whether this enemy ignores collisions. Subclasses can override.
*/
public boolean isIntangible() {
return false;
}
@Override
public void draw(Graphics g) {
Graphics2D g2d = null;
DamageNumber dnIter = null;
int dnIndex = 0;
if (isDead()) {
return;
}
// Draw the current sprite frame
if (sprites != null && sprites[currentSpriteIndex] != null) {
g.drawImage(sprites[currentSpriteIndex], x, y, width, height, null);
} else {
g.setColor(Color.RED);
g.fillRect(x, y, width, height);
g.setColor(Color.WHITE);
g.drawRect(x, y, width, height);
}
// Draw all floating damage numbers
if (!damageNumbers.isEmpty()) {
g2d = (Graphics2D) g.create();
for (dnIndex = 0; dnIndex < damageNumbers.size(); dnIndex++) {
dnIter = damageNumbers.get(dnIndex);
dnIter.draw(g2d);
}
g2d.dispose();
}
}
public boolean isBoss() {
return false; // Override in subclasses if needed
}
}