mirror of
https://github.com/FullScreenShenanigans/EightBittr.git
synced 2026-08-12 02:18:15 -07:00
Merge pull request #290 from FullScreenShenanigans/examples-fullscreensaver
Added FullScreenSaver as code and finished implementing dist outputs
This commit is contained in:
+1
-1
@@ -3,5 +3,5 @@
|
||||
**/*.json
|
||||
**/dist
|
||||
**/node_modules
|
||||
**/webpack.config.js
|
||||
**/webpack.config.*
|
||||
packages/shenanigans-manager/setup/**/*
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["./packages/*/tsconfig.json"]
|
||||
"project": ["./examples/*/tsconfig.json", "./packages/*/tsconfig.json"]
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "simple-import-sort"],
|
||||
"rules": {
|
||||
|
||||
@@ -55,7 +55,7 @@ a project may be further defined and clarified by project maintainers.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at me@joshuakgoldberg.com. All
|
||||
reported by contacting the project team at git@joshuakgoldberg.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
|
||||
@@ -11,3 +11,6 @@ runs:
|
||||
cache: "yarn"
|
||||
- run: yarn install --frozen-lockfile
|
||||
shell: bash
|
||||
# Occasionally, GitHub actions seems to persist old files from previous runs
|
||||
- run: yarn clean
|
||||
shell: bash
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Dist
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/prepare
|
||||
- run: yarn hydrate
|
||||
- run: yarn dist
|
||||
+3
-2
@@ -4,6 +4,7 @@
|
||||
*.map
|
||||
*.tsbuildinfo
|
||||
node_modules/
|
||||
packages/*/lib/
|
||||
packages/*/test/
|
||||
*/*/dist/
|
||||
*/*/lib/
|
||||
*/*/test/
|
||||
!packages/shenanigans-manager/setup/**/*
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dictionaries": ["typescript"],
|
||||
"ignorePaths": ["lib", "node_modules", "**/*.d.ts", "**/*.js", "**/*.tsbuildinfo"],
|
||||
"dictionaries": ["css", "html", "softwareTerms", "typescript"],
|
||||
"ignorePaths": ["dist", "lib", "node_modules", "**/*.d.ts", "**/*.js", "**/*.tsbuildinfo"],
|
||||
"words": [
|
||||
"actorhittr",
|
||||
"areaspawnr",
|
||||
@@ -28,11 +28,13 @@
|
||||
"flagswappr",
|
||||
"fpsanalyzr",
|
||||
"frametickr",
|
||||
"fullscreensaver",
|
||||
"fullscreenshenanigans",
|
||||
"Gameboy",
|
||||
"gameplay",
|
||||
"genrand",
|
||||
"Gitter",
|
||||
"Goombas",
|
||||
"Grapher",
|
||||
"Graphr",
|
||||
"groupholdr",
|
||||
@@ -64,6 +66,8 @@
|
||||
"pixelrendr",
|
||||
"Playr",
|
||||
"quadskeepr",
|
||||
"redeclared",
|
||||
"redeclaring",
|
||||
"Rendr",
|
||||
"sceneplayr",
|
||||
"Screenr",
|
||||
@@ -80,7 +84,9 @@
|
||||
"TTFI",
|
||||
"unspawn",
|
||||
"unspawned",
|
||||
"unspawns",
|
||||
"userwrappr",
|
||||
"walkthrough",
|
||||
"worldseedr",
|
||||
"Wrappr",
|
||||
"Writr",
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ Once built, there will be up to three directories in this package containing cod
|
||||
|
||||
1. `src/`: The "source of truth" containing TypeScript source files.
|
||||
2. `lib/`: JavaScript source files, declaration files, and source maps generated by TypeScript from `src/`.
|
||||
3. `dist/`: Bundled, minified versions of JavaScript files generated by WebPack from `lib/` -- if `package.json` contains a `"dist": true` under `"shenanigans"`.
|
||||
3. `dist/`: Bundled, minified versions of JavaScript files generated by [Webpack](https://webpack.js.org) from `lib/` -- if `package.json` contains a `"dist": true` under `"shenanigans"`.
|
||||
|
||||
### `shenanigans-manager`
|
||||
|
||||
|
||||
@@ -88,6 +88,18 @@ Now, after a player dies, you'll be able to check the best high score for the ga
|
||||
FSS.itemsHolder.getItem("highScore");
|
||||
```
|
||||
|
||||
Now that we have both the `"highScore"` and `"score"` data working, redeclare an `itemsHolder` property on the `FullScreenSaver` class with an explicit type parameter:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Cache-based wrapper around localStorage.
|
||||
*/
|
||||
public readonly itemsHolder: ItemsHoldr<{
|
||||
highScore: number;
|
||||
score: number;
|
||||
}>;
|
||||
```
|
||||
|
||||
Great.
|
||||
Those browser console look ups aren't good enough for our game though.
|
||||
Next up, we'll create a text visualization of current points and high scores.
|
||||
@@ -19,7 +19,20 @@ Go ahead and add that group to `src/sections/Groups.ts` last so text gets drawn
|
||||
public readonly groupNames = ["Players", "Squares", "Text"];
|
||||
```
|
||||
|
||||
Add a corresponding `Text` definition as a child of `Actor` in the `src/sections/Objects.ts` `inheritance`, along with character names such as `CharA` and `Menu` under it:
|
||||
Add the `Text` name to the `groupHolder` property redeclared on the `FullScreenSaver` class:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* General storage abstraction for keyed containers of items.
|
||||
*/
|
||||
public readonly groupHolder: GroupHoldr<{
|
||||
Squares: Actor;
|
||||
Players: Actor;
|
||||
Text: Actor;
|
||||
}>;
|
||||
```
|
||||
|
||||
Additionally, add a corresponding `Text` definition as a child of `Actor` in the `src/sections/Objects.ts` `inheritance`, along with character names such as `CharA` and `Menu` under it:
|
||||
|
||||
```ts
|
||||
public readonly inheritance: ClassInheritance = {
|
||||
@@ -242,4 +255,9 @@ public start(player: Actor) {
|
||||
```
|
||||
|
||||
Glorious: text appears as soon as the player is created, and updates for their new scores.
|
||||
We're done here!
|
||||
|
||||
The game seems pretty complete for a demo at this point.
|
||||
The last step for us to take will be to add tests.
|
||||
Fun!
|
||||
|
||||
> 👉 [12. Tests](./12.%20Tests.md)
|
||||
@@ -0,0 +1,165 @@
|
||||
# Tests
|
||||
|
||||
EightBittr projects managed by `shenanigans-manager` come with infrastructure for setting up tests.
|
||||
The `yarn run test` command starts with a `yarn run test:setup` that creates a `test/` directory.
|
||||
|
||||
Each package receives a `test/index.html` file to run tests:
|
||||
|
||||
- Open `test/index.html` in a browser to run tests in that browser
|
||||
- `yarn run test` includes a `yarn run test:run` command to run tests in "headless" mode
|
||||
|
||||
Tests are written in [Mocha](https://github.com/mochajs/mocha) and [Chai](https://github.com/chaijs/chai).
|
||||
Their files are written using alongside source files under `src/` and named `*.test.ts?`.
|
||||
|
||||
## Defining Tests
|
||||
|
||||
Most tests retrieve actors from the game in some way and run assertions on them.
|
||||
The `groupHolder` property of game instances can be used to retrieve actors and the defined groups of them.
|
||||
Let's add a test to verify that the game starts with exactly one square.
|
||||
|
||||
Create a new `FullScreenSaver.test.ts` file as a sibling to `FullScreenSaver.ts`.
|
||||
Add a `describe` block with an `it` test:
|
||||
|
||||
```ts
|
||||
import { expect } from "chai";
|
||||
|
||||
import { createFullScreenSaver } from "./fakes.test";
|
||||
import { FullScreenSaver } from "./FullScreenSaver";
|
||||
|
||||
describe("FullScreenSaver", () => {
|
||||
it("starts with one square", () => {
|
||||
// Arrange
|
||||
const game = new FullScreenSaver({
|
||||
height: 320,
|
||||
width: 480,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(game.groupHolder.getGroup("Squares")).to.have.length(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Then, run `yarn test` in your terminal.
|
||||
You should see output in the terminal including a test success:
|
||||
|
||||
```plaintext
|
||||
FullScreenSaver
|
||||
✅ starts with one square
|
||||
```
|
||||
|
||||
Open `test/index.html` in a browser.
|
||||
You should also see a Mocha UI indicating tests passing there too.
|
||||
|
||||
Perfect.
|
||||
Let's add some more tests!
|
||||
|
||||
## Simulating Inputs
|
||||
|
||||
Testing the result of user interactions is typically done by simulating `game.inputWriter` events.
|
||||
Let's add a test that pressing a key down spawns a player:
|
||||
|
||||
```ts
|
||||
it("adds a player when a directional input is pressed", () => {
|
||||
// Arrange
|
||||
const game = new FullScreenSaver({
|
||||
height: 320,
|
||||
width: 480,
|
||||
});
|
||||
|
||||
// Act
|
||||
game.inputWriter.callEvent("onkeydown", "left");
|
||||
|
||||
// Assert
|
||||
expect(game.groupHolder.getGroup("Players")).to.have.length(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Fakes
|
||||
|
||||
Most game logic that gets tested involves some actions that happen over time.
|
||||
We'll need our tests to be able to programmatically "tick" the game clock.
|
||||
EightBittr projects typically use [Sinon's fake timers](https://sinonjs.org/releases/latest/fake-timers) to do so.
|
||||
|
||||
Create a new `fakes.test.ts` file alongside the other test file.
|
||||
In this file, we're going to add a `createFullScreenSaver` function to create a new `FullScreenSaver` instance with test-specific `components.frameTicker` settings:
|
||||
|
||||
```ts
|
||||
import { useFakeTimers } from "sinon-timers-repeatable";
|
||||
|
||||
import { FullScreenSaver } from "./FullScreenSaver";
|
||||
|
||||
export const createFullScreenSaver = () => {
|
||||
const clock = useFakeTimers();
|
||||
|
||||
const game = new FullScreenSaver({
|
||||
components: {
|
||||
frameTicker: {
|
||||
timing: {
|
||||
cancelFrame: clock.clearTimeout,
|
||||
getTimestamp: () => clock.now,
|
||||
requestFrame: (callback) =>
|
||||
clock.setTimeout(() => {
|
||||
callback(clock.now);
|
||||
}, 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 320,
|
||||
width: 480,
|
||||
});
|
||||
|
||||
game.frameTicker.play();
|
||||
|
||||
return { clock, game };
|
||||
};
|
||||
```
|
||||
|
||||
You can replace existing `const game` with calls to the `createFullScreenSaver` function:
|
||||
|
||||
```ts
|
||||
const { game } = createFullScreenSaver();
|
||||
```
|
||||
|
||||
Now that our games under test use mocked timing, we can tick forward the clock manually in tests.
|
||||
We can tick the clock forward by a number of frames by using `game.frameTicker.getInterval()` to get how many clock ticks it take to move the game forward one maintenance cycle.
|
||||
|
||||
We can also call `game.physics` to move actors around.
|
||||
This is useful if we want to, say, move the player out of the way of solids to check what happens when they survive for long enough to create a square
|
||||
|
||||
Add a test for the case of the player surviving 150 ticks to the `FullScreenSaver.test.ts` file, and one test for the case of the player not surviving:
|
||||
|
||||
```ts
|
||||
it("increases to two squares 150 ticks after a player is added", () => {
|
||||
// Arrange
|
||||
const { clock, game } = createFullScreenSaver();
|
||||
game.inputWriter.callEvent("onkeydown", "left");
|
||||
|
||||
// (move the player out of the way)
|
||||
game.physics.shiftActors(game.groupHolder.getGroup("Players"), 9001, 9001);
|
||||
|
||||
// Act
|
||||
clock.tick(150 * game.frameTicker.getInterval());
|
||||
|
||||
// Assert
|
||||
expect(game.groupHolder.getGroup("Squares")).to.have.length(2);
|
||||
});
|
||||
|
||||
it("stops adding squares after the player is hit", () => {
|
||||
// Arrange
|
||||
const { clock, game } = createFullScreenSaver();
|
||||
game.inputWriter.callEvent("onkeydown", "left");
|
||||
|
||||
// Act
|
||||
clock.tick(300 * game.frameTicker.getInterval());
|
||||
|
||||
// Assert
|
||||
expect(game.groupHolder.getGroup("Players")).to.have.length(0);
|
||||
expect(game.groupHolder.getGroup("Squares")).to.have.length(1);
|
||||
});
|
||||
```
|
||||
|
||||
At this point, the game is not only pretty complete for a demo, it's also stable and well-tested.
|
||||
Awesome stuff!
|
||||
Give yourself a pat on the back.
|
||||
You've completed the EightBittr walkthrough! 🏆
|
||||
@@ -20,7 +20,10 @@ import { ClassInheritance, ClassProperties } from "objectmakr";
|
||||
|
||||
import { FullScreenSaver } from "../FullScreenSaver";
|
||||
|
||||
export class Objects<Game extends EightBittr> extends ObjectsBase<Game> {
|
||||
/**
|
||||
* Raw ObjectMakr factory settings.
|
||||
*/
|
||||
export class Objects<Game extends FullScreenSaver> extends ObjectsBase<Game> {
|
||||
public readonly inheritance: ClassInheritance = {
|
||||
Actor: {
|
||||
Square: {},
|
||||
@@ -30,14 +33,14 @@ export class Objects<Game extends EightBittr> extends ObjectsBase<Game> {
|
||||
public readonly onMake = "onMake";
|
||||
|
||||
public readonly properties: ClassProperties = {
|
||||
Actor: {
|
||||
onMake: this.game.actors.process.bind(this.game.actors),
|
||||
},
|
||||
Square: {
|
||||
height: 64,
|
||||
groupType: "Squares",
|
||||
width: 64,
|
||||
},
|
||||
Actor: {
|
||||
onMake: this.game.actors.process.bind(this.game.actors),
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -51,6 +54,9 @@ import { EightBittr, EightBittrConstructorSettings } from "eightbittr";
|
||||
import { Objects } from "./sections/Objects";
|
||||
|
||||
export class FullScreenSaver extends EightBittr {
|
||||
/**
|
||||
* Raw ObjectMakr factory settings.
|
||||
*/
|
||||
@member(Objects)
|
||||
public readonly objects: Objects<this>;
|
||||
|
||||
@@ -88,6 +94,18 @@ Add this new section as a member to your `FullScreenSaver` class too:
|
||||
public readonly groups: Groups<this>;
|
||||
```
|
||||
|
||||
Additionally, you'll want to tell the `FullScreenSaver` class that its `groupHolder` property stores an array of `Actor`s under the group name `"Squares"`.
|
||||
Do so by importing `GroupHoldr` from `groupholdr` modules and redeclaring the `groupHolder` property on `FullScreenSaver` with an explicit type argument:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* General storage abstraction for keyed containers of items.
|
||||
*/
|
||||
public readonly groupHolder: GroupHoldr<{
|
||||
Squares: Actor;
|
||||
}>
|
||||
```
|
||||
|
||||
Now that the `Square` _exists_, we'll want to give it some sprite info to be _rendered_.
|
||||
|
||||
## Graphics
|
||||
+1
-1
@@ -15,7 +15,7 @@ public constructor(settings: EightBittrConstructorSettings) {
|
||||
|
||||
const square = this.actors.add("Square");
|
||||
|
||||
this.physics.setMid(square, this.mapScreener.height / 2, this.mapScreener.width / 2);
|
||||
this.physics.setMid(square, this.mapScreener.width / 2, this.mapScreener.height / 2);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -40,6 +40,7 @@ export class Maintenance<Game extends FullScreenSaver> extends MaintenanceBase<G
|
||||
*/
|
||||
public readonly maintain = (actor: Actor) => {
|
||||
this.game.physics.shiftBoth(actor, actor.xVelocity, actor.yVelocity);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public readonly maintainers: [string, ActorMaintainer][] = [["Squares", this.maintain]];
|
||||
@@ -61,7 +62,7 @@ Now, when you refresh the game, the starting square should slowly fly up and to
|
||||
## Wall Bouncing
|
||||
|
||||
We can check on each square's maintenance tick whether it's passed any of the screen walls and reverse its velocity if so.
|
||||
Add the following code to the `maintain` function to do so:
|
||||
Add the following code to the `maintain` function just after the `shiftBoth` to do so:
|
||||
|
||||
```ts
|
||||
if (actor.top < 0) {
|
||||
@@ -71,7 +72,7 @@ if (actor.top < 0) {
|
||||
|
||||
if (actor.bottom > this.game.mapScreener.height) {
|
||||
actor.yVelocity *= -1;
|
||||
this.game.physics.shiftVert(actor, this.game.mapScreener.height - actor.bottom);
|
||||
this.game.physics.shiftVertical(actor, this.game.mapScreener.height - actor.bottom);
|
||||
}
|
||||
|
||||
if (actor.left < 0) {
|
||||
@@ -81,7 +82,7 @@ if (actor.left < 0) {
|
||||
|
||||
if (actor.right > this.game.mapScreener.width) {
|
||||
actor.xVelocity *= -1;
|
||||
this.game.physics.shiftHoriz(actor, this.game.mapScreener.width - actor.right);
|
||||
this.game.physics.shiftHorizontal(actor, this.game.mapScreener.width - actor.right);
|
||||
}
|
||||
```
|
||||
|
||||
+24
-3
@@ -7,24 +7,26 @@ We'll create that player when the left key is pressed for the first time.
|
||||
|
||||
Creating the player Actor definition follows similar steps to the squares:
|
||||
|
||||
1. Create a new object declaration in `Objects`:
|
||||
1. Create a new object declaration in `Objects` for a `Player` actor type:
|
||||
|
||||
```ts
|
||||
public readonly inheritance: ClassInheritance = {
|
||||
Actor: {
|
||||
Player: {},
|
||||
Square: {},
|
||||
// ...
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```ts
|
||||
public readonly properties: ClassProperties = {
|
||||
// ...
|
||||
Player: {
|
||||
height: 32,
|
||||
groupType: "Players",
|
||||
width: 32,
|
||||
},
|
||||
// ...
|
||||
```
|
||||
|
||||
2. Create a group for the players in `Groups`:
|
||||
@@ -33,7 +35,16 @@ Creating the player Actor definition follows similar steps to the squares:
|
||||
public readonly groupNames = ["Players", "Squares"];
|
||||
```
|
||||
|
||||
3. Define a new color (light green) and sprite data (a square) for the new type in `Graphics`:
|
||||
3. Add to the `groupHolder: GroupHoldr` type argument in the `FullScreenSaver` class:
|
||||
|
||||
```ts
|
||||
public readonly groupHolder: GroupHoldr<{
|
||||
Squares: Actor;
|
||||
Players: Actor;
|
||||
}>;
|
||||
```
|
||||
|
||||
4. Define a new color (light green) and sprite data (a square) for the new type in `Graphics`:
|
||||
|
||||
```ts
|
||||
public readonly library = {
|
||||
@@ -102,6 +113,16 @@ export class Players extends Section<FullScreenSaver> {
|
||||
That `id: "player1"` will allow the new player Actor to be retrieved by id later on.
|
||||
The above code creates a player Actor on top of the first square Actor in the game with that id and some horizontal velocity.
|
||||
|
||||
Add the new `Players` section as a property of `FullScreenSaver`:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Creates and updates player Actors.
|
||||
*/
|
||||
@member(Players)
|
||||
public readonly players: Players;
|
||||
```
|
||||
|
||||
Lastly, add an entry to the `Maintenance` section for players to be maintained as well:
|
||||
|
||||
```ts
|
||||
+3
-3
@@ -24,7 +24,7 @@ export class Quadrants<Game extends FullScreenSaver> extends QuadrantsBase<Game>
|
||||
/**
|
||||
* Groups that should have their quadrants updated.
|
||||
*/
|
||||
public readonly activeGroupNames: string[] = ["Players", "Squares"];
|
||||
public readonly activeGroupNames = ["Players", "Squares"];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -57,7 +57,7 @@ export class Collisions<Game extends FullScreenSaver> extends CollisionsBase<Gam
|
||||
/**
|
||||
* Names of groups that should be checked for collisions.
|
||||
*/
|
||||
public readonly collidingGroupNames: string[] = ["Players"];
|
||||
public readonly collidingGroupNames = ["Players"];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -93,7 +93,7 @@ The game will now know when a player is touching a solid, but it won't know what
|
||||
|
||||
## Collision Handling
|
||||
|
||||
Add an equivalent `hitCallbackGenerators` later in the class to kill the player:
|
||||
Add an equivalent `hitCallbackGenerators` later in the `Collisions` class to kill the player:
|
||||
|
||||
```ts
|
||||
/**
|
||||
@@ -10,6 +10,10 @@ We'll do this by adding a function called whenever a square is added to the game
|
||||
Create a new standalone `Squares` section at `src/sections/Squares.ts` with an `addSquare` member function that takes in a coordinates and velocity to create a new square:
|
||||
|
||||
```ts
|
||||
import { Section } from "eightbittr";
|
||||
|
||||
import { FullScreenSaver } from "../FullScreenSaver";
|
||||
|
||||
/**
|
||||
* Creates square Actors in the game.
|
||||
*/
|
||||
@@ -44,7 +48,6 @@ public constructor(settings: EightBittrConstructorSettings) {
|
||||
this.quadsKeeper.resetQuadrants();
|
||||
|
||||
this.squares.addSquare(
|
||||
"Square",
|
||||
this.mapScreener.width / 2,
|
||||
this.mapScreener.height / 2,
|
||||
2,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user