Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9aa92a5
New wave with introduction of enemies.
pg94au Jan 12, 2025
ad86087
Use scheduler in deploying zaggers.
pg94au Jan 12, 2025
4b4184f
Format code.
pg94au Jan 15, 2025
614726b
Basic periodic single enemy swoops are functional.
pg94au Jan 15, 2025
b276e49
Cleanup.
pg94au Jan 15, 2025
e38caa6
Fix lint issues.
Jan 15, 2025
06c8812
Don't tell a Zagger to swoop if that's what it's doing now.
pg94au Jan 18, 2025
dc60dfd
Add ability for Zaggers to swoop and return.
pg94au Jan 19, 2025
b52da4a
Zagger can change direction once on swoop, but speed not consistent.
pg94au Jan 19, 2025
0269ea8
Settings for debugging Mocha tests.
pg94au Jan 24, 2025
54790a4
Added LineSegmentPath.
pg94au Jan 26, 2025
d155458
Fix wrong keyword.
pg94au Jan 26, 2025
7fde125
Added support for building line paths based on a fixed speed.
pg94au May 4, 2025
48973db
Remove lint warning.
pg94au Sep 7, 2025
edfe58a
Updating zagger.
pg94au Sep 8, 2025
7ab7642
Add spider images and switch to using it.
pg94au Sep 11, 2025
f180429
Speed things up.
pg94au Sep 11, 2025
10bbe91
New explosion sprites for spider.
pg94au Sep 11, 2025
eb3d0e0
Adding spider explosion sound.
pg94au Sep 12, 2025
3c4734b
Clean up swoop scheduling code.
pg94au Sep 12, 2025
9de7ea8
Get two swoopers going at once.
pg94au Sep 12, 2025
9edf872
Improvements to zigzag wave. Added web shot.
pg94au Sep 16, 2025
1ab2bac
Unique spider explosion sound.
pg94au Sep 16, 2025
939b14c
Move spiders to new fourth level.
pg94au Sep 16, 2025
a479275
Cleanup.
pg94au Sep 20, 2025
f58b1ef
Add web shot tests.
pg94au Sep 20, 2025
81133fa
Fix collision boundaries for web.
pg94au Sep 28, 2025
15e4ca2
Make spider level a bit harder.
pg94au Sep 30, 2025
4ff1bc0
Rename ZigZag/Zagger to Spider.
pg94au Oct 28, 2025
3b89b56
Remove console.log statements.
pg94au Oct 28, 2025
246748a
Add some tests for Spider class.
pg94au Nov 16, 2025
4b50d78
Add missing tests for Point.
pg94au Nov 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"mochaExplorer.files": "test/**/*.spec.[jt]s",
"mochaExplorer.require": "ts-node/register"
}
Binary file added images/spider.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/spider_explosion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/web.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,15 @@
{ name: 'probe_explosion', url: 'images/probe_explosion.png' },
{ name: 'saucer', url: 'images/saucer.png' },
{ name: 'saucer_explosion', url: 'images/saucer_explosion.png' },
{ name: 'spider', url: 'images/spider.png' },
{ name: 'spider_explosion', url: 'images/spider_explosion.png' },
{ name: 'spinner', url: 'images/spinner.png' },
{ name: 'splitter', url: 'images/splitter.png' },
{ name: 'splitter_fragment', url: 'images/splitter_fragment.png' },
{ name: 'splitter_left_separation', url: 'images/splitter_left_separation.png' },
{ name: 'splitter_right_separation', url: 'images/splitter_right_separation.png' },
{ name: 'star', url: 'images/star.png' }
{ name: 'star', url: 'images/star.png' },
{ name: 'web', url: 'images/web.png' }
];
renderer.preLoadImages(images, onImagesLoaded);

Expand Down Expand Up @@ -165,7 +168,8 @@
{ name: 'player_explosion', url: 'sounds/player_explosion.mp3' },
{ name: 'player_hit', url: 'sounds/player_hit.mp3' },
{ name: 'probe_explosion', url: 'sounds/probe_explosion.mp3' },
{ name: 'saucer_explosion', url: 'sounds/saucer_explosion.mp3' }
{ name: 'saucer_explosion', url: 'sounds/saucer_explosion.mp3' },
{ name: 'spider_explosion', url: 'sounds/spider_explosion.mp3' }
];
audioPlayer.preLoadSounds(sounds, onSoundsLoaded, function(errors) {
alert('Failed to load sounds: ' + JSON.stringify(errors));
Expand Down
Binary file added sounds/spider_explosion.mp3
Binary file not shown.
8 changes: 7 additions & 1 deletion src/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {Scheduler} from './timing/Scheduler';
import {ScoreCounter} from './ScoreCounter';
import {SecondWave} from './waves/SecondWave';
import {SimpleWave} from './waves/SimpleWave';
import {SpiderWave} from "./waves/SpiderWave";
import {SpinnerWave} from './waves/SpinnerWave';
import {SpinnerWave2} from './waves/SpinnerWave2';
import {SplitterWave} from './waves/SplitterWave';
Expand Down Expand Up @@ -92,7 +93,7 @@ export class Game {
if (this._isActive) {
setTimeout(() => {
global.requestAnimationFrame(() => { this.tick() });
}, 1000/30);
}, 1000/40);
}
else {
debug('ticker: Ticker stopping because game is over.');
Expand Down Expand Up @@ -188,6 +189,11 @@ export class Game {
new SimpleWave(this._audioPlayer, this._world, this._clock),
new SecondWave(this._audioPlayer, this._world, this._clock),
new SplitterWave(this._audioPlayer, this._world, this._clock)
]),
new Level([
new SpinnerWave(this._audioPlayer, this._world, this._clock),
new SpinnerWave2(this._audioPlayer, this._world, this._clock),
new SpiderWave(this._audioPlayer, this._world, this._clock)
])
]
);
Expand Down
4 changes: 4 additions & 0 deletions src/Point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export class Point {
return new Point(this._x + count, this._y);
}

equals(other: Point): boolean {
return this._x === other._x && this._y === other._y;
}

toString(): string {
return 'Point(x=' + this._x + ', y=' + this._y + ')';
}
Expand Down
228 changes: 228 additions & 0 deletions src/enemies/Spider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import Debug from "debug";
const debug = Debug("Blaster:Saucer");
import {random} from 'underscore';

import {Actor} from "../Actor";
import {AudioPlayer} from "../devices/AudioPlayer";
import {Bounds} from '../Bounds';
import {Clock} from "../timing/Clock";
import {Enemy} from './Enemy';
import {ExplosionProperties} from '../ExplosionProperties';
import {HitArbiter} from '../HitArbiter';
import {ImageDetails} from '../ImageDetails';
import {LinePath} from '../paths/LinePath';
import {LineSegmentPath} from "../paths/LineSegmentPath";
import {PathAction} from '../paths/PathAction';
import {PathEntry} from "../paths/PathEntry";
import {Point} from '../Point';
import {ScheduledAction} from '../paths/ScheduledAction';
import {Scheduler} from '../timing/Scheduler';
import {Web} from '../shots/Web';
import {World} from "../World";

export class Spider extends Enemy {
public static readonly InitialHealth: number = 1;

private readonly _clock: Clock;
private readonly _scheduler: Scheduler;
private readonly _hitArbiter: HitArbiter;
private readonly _homePosition: Point;
private _currentFrame: number = 0;
private _currentPath!: PathEntry[];
private _pathPosition!: number;
private _state: Spider.State;

constructor(audioPlayer: AudioPlayer, world: World, clock: Clock, startingPoint: Point, homePosition: Point) {
super(audioPlayer, world, startingPoint, Spider.InitialHealth);
debug('Spider constructor');

this._clock = clock;
this._scheduler = new Scheduler(clock);
this._hitArbiter = new HitArbiter(this);
this._homePosition = homePosition;

this.prepareEntryPath(homePosition);
this._state = Spider.State.Entering;
this.advanceCurrentFrame();
}

get state(): Spider.State {
return this._state;
}

get explosionProperties(): ExplosionProperties {
return new ExplosionProperties(
'spider_explosion',
5,
52,
0.8,
'spider_explosion'
);
}

get scoreTotal(): number {
return 10;
}

getCollisionMask(actor: Actor): Bounds[] {
return [new Bounds(-40, 40, -25, 25)];
}

getDamageAgainst(target: Actor): number {
return 5;
}

get imageDetails(): ImageDetails {
return new ImageDetails('spider', 3, 95, this._currentFrame);
}

hitBy(actor: Actor, damage: number): boolean {
this._health = Math.max(0, this._health - damage);
return true;
}

tick(): void {
debug('Spider.tick');
super.tick();

if (!this._isActive) {
return;
}

this._scheduler.executeDueOperations();

this.step();

// Check if this enemy has hit the player.
const player = this._world.player;
if (player) {
this._hitArbiter.attemptToHit(player);
}
}

private advanceCurrentFrame(): void {
this._currentFrame = (this._currentFrame + 1) % 3;

this._scheduler.scheduleOperation(
'advanceCurrentFrame',
200,
() => { this.advanceCurrentFrame() }
);
}

private dropWeb(): void {
const web = new Web(this._audioPlayer, this._clock, this._world, this._location);
this._world.addActor(web);
}

public swoop(): void {
if (random(0, 1) === 0) {
// Swoop down and off the screen.
const lowestPoint = new Point(Math.floor(random(10, 430)), 670);
const turns = random(0, 2);
switch (turns) {
case 0:
const linePath = new LinePath(this._location, lowestPoint, [new ScheduledAction(0.50, PathAction.Fire)]);
this._currentPath = linePath.getPathForSpeed(10);
this._pathPosition = 0;
this._state = Spider.State.Swooping;
break;
case 1:
const midPoint = new Point(Math.floor(random(10, 430)), random(300, 500));
this._currentPath = new LineSegmentPath(
[this._location, midPoint, lowestPoint],
[new ScheduledAction(0.25, PathAction.Fire), new ScheduledAction(0.55, PathAction.Fire)]
).getPathForSpeed(10);
this._pathPosition = 0;
this._state = Spider.State.Swooping;
break;
case 2:
const midPoint1 = new Point(Math.floor(random(10, 430)), random(300, 400));
const midPoint2 = new Point(Math.floor(random(10, 430)), random(400, 500));
this._currentPath = new LineSegmentPath(
[this._location, midPoint1, midPoint2, lowestPoint],
[new ScheduledAction(0.30, PathAction.Fire), new ScheduledAction(0.60, PathAction.Fire)]
).getPathForSpeed(10);
this._pathPosition = 0;
this._state = Spider.State.Swooping;
break;
}
}
else {
// Swoop and return to home.
const lowestPoint = new Point(Math.floor(random(10, 430)), 500);
const swoopDownPath = new LinePath(this._location, lowestPoint, []);
const swoopReturnPath = new LinePath(lowestPoint, this._homePosition, [new ScheduledAction(0.50, PathAction.Fire)]);
this._currentPath = swoopDownPath.getPathForSpeed(8).concat(swoopReturnPath.getPathForSpeed(10));
this._pathPosition = 0;
this._state = Spider.State.SwoopAndReturn;
}

if (random(0, 1) === 0) {
this.dropWeb();
}
}

private step(): void {
// Choose the next path to follow once we've reach the end of the current path.
if (this._pathPosition >= this._currentPath.length) {
switch (this._state) {
case Spider.State.Entering:
this._state = Spider.State.Waiting;
break;
case Spider.State.SwoopAndReturn:
this._state = Spider.State.Waiting;
break;
case Spider.State.Swooping:
this._scheduler.scheduleOperation(
'enter',
3000,
() => {
// Determine our path back to our home position after.
const worldDimensions = this._world.dimensions;
const spiderStartingPoint = new Point(
Math.floor(random(0, worldDimensions.width-50)),
-20
);
// TODO: We might be able to call prepareEntryPath here instead.
const linePath = new LinePath(spiderStartingPoint, this._homePosition, []);
this._currentPath = linePath.getPathForSteps(10);
this._pathPosition = 0;
this._state = Spider.State.Entering;
}
);
this._state = Spider.State.SwoopComplete;
break;
}

return;
}

// Follow the current path.
switch(this._currentPath[this._pathPosition].action) {
case PathAction.Move:
this._location = this._currentPath[this._pathPosition].location!;
break;
case PathAction.Fire:
this.dropWeb();
break;
}
this._pathPosition++;
}

private prepareEntryPath(homePosition: Point): void {
const linePath = new LinePath(this._location, homePosition, []);
this._currentPath = linePath.getPathForSteps(10);
this._pathPosition = 0;
}
}

export namespace Spider {
export enum State {
Entering,
SwoopAndReturn,
SwoopComplete,
Swooping,
Waiting
}
}
2 changes: 1 addition & 1 deletion src/enemies/Spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export class Spinner extends Enemy {
}
}

export module Spinner {
export namespace Spinner {
export enum Bias {
Left,
Right
Expand Down
12 changes: 11 additions & 1 deletion src/paths/LinePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@ export class LinePath {
this._scheduledActions = scheduledActions;
}

getPath(numberOfSteps: number): PathEntry[] {
getPathForSpeed(speed: number): PathEntry[] {
const lineLength = Math.sqrt(
Math.pow(this._end.x - this._start.x, 2) +
Math.pow(this._end.y - this._start.y, 2)
);
const numberOfSteps = Math.max(Math.floor(lineLength / speed), 1);

return this.getPathForSteps(numberOfSteps);
}

getPathForSteps(numberOfSteps: number): PathEntry[] {
const path: PathEntry[] = [];

const stepSize = 1.0 / numberOfSteps;
Expand Down
Loading