├── .gitignore
├── libbuild
└── closure-compiler-v20190729
│ └── build
│ └── compiler.jar
├── src
├── d.ts
│ └── externs.d.ts
├── externs
│ └── externs.js
└── ts
│ ├── factories
│ ├── scenery.factory.ts
│ ├── persistententity.factory.ts
│ ├── spikes.factory.ts
│ ├── crate.factory.ts
│ ├── player.factory.ts
│ ├── gun.factory.ts
│ ├── block.factory.ts
│ ├── mainframe.factory.ts
│ ├── tape.factory.ts
│ ├── boss.factory.ts
│ ├── repeater.factory.ts
│ ├── pressureplate.factory.ts
│ ├── platform.factory.ts
│ ├── robot.factory.ts
│ └── room.factory.ts
│ ├── graphics
│ ├── gun.graphic.ts
│ ├── bullet.graphic.ts
│ ├── block.graphic.ts
│ ├── spikes.graphic.ts
│ ├── tape.graphic.ts
│ ├── repeater.graphic.ts
│ ├── platform.graphic.ts
│ ├── crate.graphic.ts
│ ├── mainframe.graphic.ts
│ ├── pressureplate.graphic.ts
│ ├── robot.graphic.ts
│ └── player.graphic.ts
│ ├── flags.ts
│ ├── common
│ ├── arrays.ts
│ ├── shapes.ts
│ ├── synth_speech.ts
│ ├── graphics.ts
│ ├── sounds.ts
│ └── inputs.ts
│ ├── constants.ts
│ ├── game
│ ├── room.ts
│ ├── world.ts
│ └── entities.ts
│ └── index.ts
├── index.html
├── tsconfig.json
├── index.css
├── README.md
├── package.json
├── Gruntfile.js
└── dist
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/index.css
3 | dist/out.min.js
4 | build
5 | closure.txt
6 | package-lock.json
7 |
--------------------------------------------------------------------------------
/libbuild/closure-compiler-v20190729/build/compiler.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madmaw/playback/HEAD/libbuild/closure-compiler-v20190729/build/compiler.jar
--------------------------------------------------------------------------------
/src/d.ts/externs.d.ts:
--------------------------------------------------------------------------------
1 | declare const c: HTMLCanvasElement;
2 | declare const o: HTMLDivElement;
3 | declare const h: HTMLDivElement;
4 | declare const s: HTMLDivElement;
--------------------------------------------------------------------------------
/src/externs/externs.js:
--------------------------------------------------------------------------------
1 | var localStorage;
2 | var onload;
3 | var onresize
4 | var onkeyup;
5 | var onkeydown;
6 | var innerWidth;
7 | var innerHeight;
8 | var c;
9 | var o;
10 | var h;
11 | var s;
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "amd",
4 | "target": "ES2015",
5 | "outFile": "build/out.js",
6 | "rootDir": ".",
7 | "noImplicitAny": false,
8 | "removeComments": true,
9 | "preserveConstEnums": true,
10 | "sourceMap": true,
11 | "strictNullChecks": false
12 | },
13 | "include": [
14 | "src/ts/**/*",
15 | "src/d.ts/**/*"
16 | ],
17 | "exclude": [
18 | ]
19 | }
--------------------------------------------------------------------------------
/src/ts/factories/scenery.factory.ts:
--------------------------------------------------------------------------------
1 | let sceneryFactoryFactory = (text: string, scale: number) => {
2 | return (x: number, y: number, id: IdFactory) => {
3 | const scenery: Scenery = {
4 | eid: id(),
5 | collisionGroup: COLLISION_GROUP_BACKGROUNDED,
6 | collisionMask: 0,
7 | text,
8 | entityType: ENTITY_TYPE_SCENERY,
9 | bounds: [x, y - scale + 1, 1, scale],
10 | }
11 | return [scenery];
12 | };
13 | }
--------------------------------------------------------------------------------
/src/ts/factories/persistententity.factory.ts:
--------------------------------------------------------------------------------
1 | const persistentEntityFactoryFactory = (entityFactory: EntityFactory, persistentId: number) => {
2 | return (x: number, y: number, id: IdFactory) => {
3 | let entities = entityFactory(x, y, id, persistentId);
4 | if (!localStorage.getItem(persistentId as any)) {
5 | entities[0].persistentId = persistentId;
6 | entities[0].eid = persistentId;
7 | } else {
8 | entities = [];
9 | }
10 | return entities;
11 | }
12 | };
--------------------------------------------------------------------------------
/src/ts/factories/spikes.factory.ts:
--------------------------------------------------------------------------------
1 | const spikeFactoryFactory = () => {
2 | return (x: number, y: number, id: IdFactory) => {
3 | const spike: Lethal = {
4 | eid: id(),
5 | graphic: randomSpikeGraphic(),
6 | palette: spikesPalette,
7 | entityType: ENTITY_TYPE_LETHAL,
8 | collisionGroup: COLLISION_GROUP_SPIKES,
9 | collisionMask: COLLISION_MASK_SPIKES,
10 | bounds: rectangleCenterBounds(x, y, 1, .25),
11 | };
12 | return [spike];
13 | }
14 | };
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | background: #000;
5 | }
6 |
7 | #c, #o /*#h,*/ {
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | bottom: 0;
12 | right: 0;
13 | margin: auto;
14 | box-sizing: border-box;
15 | }
16 |
17 | #o /*#h,*/ {
18 | color: #fff;
19 | background: rgba(0, 0, 0, .5);
20 | opacity: 0;
21 | transition: opacity .5s;
22 | text-align: center;
23 | font: 9vh fantasy;
24 | padding-top: 40vh;
25 | }
26 | /*
27 | #h {
28 | padding: 9vh;
29 | font: 15px fantasy;
30 | }
31 | */
32 |
--------------------------------------------------------------------------------
/src/ts/factories/crate.factory.ts:
--------------------------------------------------------------------------------
1 | const crateFactory = (x: number, y: number, id: IdFactory) => {
2 | const crate: Crate = {
3 | eid: id(),
4 | entityType: ENTITY_TYPE_CRATE,
5 | collisionGroup: COLLISION_GROUP_PUSHABLES,
6 | collisionMask: COLLISION_MASK_PUSHABLES,
7 | bounds: rectangleCenterBounds(x, y, .95, .95),
8 | gravityMultiplier: 1,
9 | mass: 2,
10 | velocity: [0, 0],
11 | graphic: crateGraphic,
12 | palette: cratePalette,
13 | airTurn: 1,
14 | };
15 | return [crate];
16 | };
17 |
--------------------------------------------------------------------------------
/src/ts/graphics/gun.graphic.ts:
--------------------------------------------------------------------------------
1 | const GUN_GRAPHIC_PALETTE_INDEX_BODY = 0;
2 | const GUN_GRAPHIC_PALETTE_INDEX_GRIP = 1;
3 | const GUN_GRAPHIC_IMAGE_INDEX_BODY = 0;
4 |
5 | const gunGraphic: Graphic = {
6 | imageryWidth: 24,
7 | imageryHeight: 10,
8 | imagery: [
9 | // block
10 | [
11 | [-2, 0, 5, 8, GUN_GRAPHIC_PALETTE_INDEX_GRIP],
12 | [7, 2, 5, 8, GUN_GRAPHIC_PALETTE_INDEX_GRIP],
13 | [-12, 0, 4, 4, GUN_GRAPHIC_PALETTE_INDEX_BODY],
14 | [-12, 1, 22, 2, GUN_GRAPHIC_PALETTE_INDEX_BODY],
15 | [-5, 0, 15, 4, GUN_GRAPHIC_PALETTE_INDEX_BODY],
16 | ],
17 | ],
18 | joints: [{
19 | imageIndex: GUN_GRAPHIC_IMAGE_INDEX_BODY,
20 | }]
21 | }
22 |
--------------------------------------------------------------------------------
/src/ts/graphics/bullet.graphic.ts:
--------------------------------------------------------------------------------
1 | const BULLET_GRAPHIC_PALETTE_INDEX_START = 0;
2 | const BULLET_GRAPHIC_PALETTE_INDEX_MIDDLE = 1;
3 | const BULLET_GRAPHIC_PALETTE_INDEX_END = 2;
4 |
5 | const BULLET_GRAPHIC_IMAGE_INDEX_BODY = 0;
6 |
7 | const bulletPalette: HSL[] = [
8 | [60, 99, 80],
9 | [30, 99, 70],
10 | [0, 99, 60],
11 | ];
12 |
13 | const bulletGraphic: Graphic = {
14 | imageryWidth: 6,
15 | imageryHeight: 2,
16 | imagery: [
17 | // block
18 | [
19 | [0, 0, 2, 2, BULLET_GRAPHIC_PALETTE_INDEX_END],
20 | [2, 0, 2, 2, BULLET_GRAPHIC_PALETTE_INDEX_MIDDLE],
21 | [4, 0, 2, 2, BULLET_GRAPHIC_PALETTE_INDEX_START],
22 | ],
23 | ],
24 | joints: [{
25 | imageIndex: BULLET_GRAPHIC_IMAGE_INDEX_BODY,
26 | }]
27 | }
28 |
--------------------------------------------------------------------------------
/src/ts/flags.ts:
--------------------------------------------------------------------------------
1 | const FLAG_IMAGE_SMOOTHING_DISABLED = false;
2 | const FLAG_RECORD_PREVIOUS_FRAMES = false;
3 | const FLAG_DEBUG_PHYSICS = false;
4 | const FLAG_CHECK_TILES_VALID = false;
5 | const FLAG_CHECK_CIRCULAR_CARRYING = false;
6 | const FLAG_CARRIER_TURNS_CARRIED = false;
7 | const FLAG_MINIMAL_AUDIO_CLEANUP = true;
8 | const FLAG_AUDIO_CONTEXT_RESUME = true;
9 | const FLAG_EMOJIS = false;
10 | const FLAG_NATIVE_SPEECH_SYNTHESIS = false;
11 | const FLAG_LOCAL_SPEECH_SYNTHESIS = true;
12 | const FLAG_TONAL_SPEECH_SYNTHESIS = true;
13 | const FLAG_CHECK_OVERLAP_SELF = false;
14 | const FLAG_HELP = false;
15 | const FLAG_CHROME_FONT_HACK = false;
16 | const FLAG_RANDOMIZE_BLOCK_COLORS = true;
17 | // don't do this
18 | const FLAG_RANDOMIZE_PHENOMES = false;
19 | const FLAG_SHAKE = false;
20 | const FLAG_DEBUG_SKIPPED_FRAMES = false;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Oh no, you've been kicked out of the castle! Solve puzzles and make your way back to your rightful home.
2 |
3 | [Play it here](https://madmaw.github.io/playback/dist/)
4 |
5 | Puzzle-platformer game with an audio mechanic. Robots and platforms will only respond to sounds from tapes of the same color. Platforms with dark arrows on them will remember their position after saving, so you don't need to worry about losing too much progress if you die.
6 |
7 | Controls (when it says "hold" make sure you hold down the key)
8 |
9 | Hold Left/Right Arrows or A/D = Move Left/Right
10 | Space or J = Jump
11 | Hold Down Arrow or S = Stop Grabbing
12 | Up Arrow or W = Climb up (while grabbing)
13 | G = Get/Pick up
14 | B = Drop (Brop?)
15 | I = Insert tape
16 | K = Eje(k)t tape
17 | Hold P = Play
18 | Hold R = Record
19 | Hold [ = Rewind
20 | Hold ] = Fast Forward
21 | T = Throw
22 | Enter = Shoot gun (should you find one)
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "js13k2019",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/madmaw/js13k2019.git"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "bugs": {
13 | "url": "https://github.com/madmaw/js13k2019/issues"
14 | },
15 | "homepage": "https://github.com/madmaw/js13k2019#readme",
16 | "devDependencies": {
17 | "grunt": "^1.0.3",
18 | "grunt-closure-compiler": "0.0.21",
19 | "grunt-contrib-clean": "^2.0.0",
20 | "grunt-contrib-connect": "^2.0.0",
21 | "grunt-contrib-copy": "^1.0.0",
22 | "grunt-contrib-cssmin": "^3.0.0",
23 | "grunt-contrib-htmlmin": "^3.0.0",
24 | "grunt-contrib-watch": "^1.1.0",
25 | "grunt-dev-update": "^2.3.0",
26 | "grunt-inline": "^0.3.6",
27 | "grunt-text-replace": "^0.4.0",
28 | "grunt-ts": "^6.0.0-beta.19",
29 | "typescript": "^3.0.1"
30 | },
31 | "dependencies": {}
32 | }
33 |
--------------------------------------------------------------------------------
/src/ts/common/arrays.ts:
--------------------------------------------------------------------------------
1 | let arrayEquals = (a: T[], b: T[]) => {
2 | return a.length == b.length && arrayEqualsNoBoundsCheck(a, b);
3 | };
4 |
5 | let arrayEqualsNoBoundsCheck = (a: T[], b: T[]) => {
6 | return a.reduce((e, v, i) => e && v == b[i], true);
7 | };
8 |
9 | let arrayRemoveElement = (a: T[], e: T) => {
10 | const index = a.indexOf(e);
11 | if (index >= 0) {
12 | a.splice(index, 1);
13 | }
14 | }
15 |
16 | let array2DCreate = (w: number, h: number, factory: (x: number, y: number) => T) => {
17 | const r: Rectangle = [0, 0, w, h];
18 | const result: T[][] = [];
19 | rectangleIterateBounds(r, r, (x, y) => {
20 | if (!y) {
21 | result.push([]);
22 | }
23 | result[x].push(factory(x, y));
24 | });
25 | return result;
26 | };
27 |
28 | let objectIterate = (o: {[_: number]: T}, f: (t: T, k: number) => void) => {
29 | for(let k in o) {
30 | let v = o[k];
31 | f(v, k as any);
32 | }
33 | }
--------------------------------------------------------------------------------
/src/ts/factories/player.factory.ts:
--------------------------------------------------------------------------------
1 | const playerFactory = (x: number, y: number, id: IdFactory) => {
2 | const player: Player = {
3 | graphic: playerGraphic,
4 | palette: playerPalette,
5 | entityType: ENTITY_TYPE_PLAYER,
6 | bounds: rectangleCenterBounds(x, y, .6, .8),
7 | collisionGroup: COLLISION_GROUP_PLAYER,
8 | collisionMask: COLLISION_MASK_PLAYER,
9 | grabMask: GRAB_MASK,
10 | gravityMultiplier: 1,
11 | entityOrientation: ORIENTATION_RIGHT,
12 | orientationStartTime: 0,
13 | eid: id(),
14 | mass: 1,
15 | velocity: [0, 0],
16 | baseVelocity: BASE_VELOCITY,
17 | airTurn: 1,
18 | activeInputs: {},
19 | holding: {},
20 | toSpeak: [],
21 | handJointId: PLAYER_GRAPHIC_JOINT_ID_RIGHT_HAND,
22 | insertionJointId: PLAYER_GRAPHIC_JOINT_ID_TAPE_DECK,
23 | //capabilities: INSTRUCTIONS.map((instruction, i) => i).filter(i => INSTRUCTIONS[i].keyCodes),
24 | };
25 | return [player];
26 | }
--------------------------------------------------------------------------------
/src/ts/graphics/block.graphic.ts:
--------------------------------------------------------------------------------
1 | const BLOCK_GRAPHIC_PALETTE_INDEX_LIGHT = 0;
2 | const BLOCK_GRAPHIC_PALETTE_INDEX_MEDIUM = 1;
3 | const BLOCK_GRAPHIC_PALETTE_INDEX_DARK = 2;
4 |
5 | const BLOCK_GRAPHIC_IMAGE_INDEX_BODY = 0;
6 |
7 | const blockGraphicFactory = (width: number, height: number, rounding: number = 4) => {
8 | const blockGraphic: Graphic = {
9 | imageryWidth: width,
10 | imageryHeight: height,
11 | imagery: [
12 | // block
13 | [
14 | [-width/2, 0, width, height, BLOCK_GRAPHIC_PALETTE_INDEX_MEDIUM, rounding],
15 | [-width/2, 0, width - 2, height - 2, BLOCK_GRAPHIC_PALETTE_INDEX_LIGHT, rounding],
16 | [2 - width/2, 2, width - 2, height - 2, BLOCK_GRAPHIC_PALETTE_INDEX_DARK, rounding],
17 | [2 - width/2, 2, width - 4, height - 4, BLOCK_GRAPHIC_PALETTE_INDEX_MEDIUM, rounding],
18 | ],
19 | ],
20 | joints: [{
21 | imageIndex: BLOCK_GRAPHIC_IMAGE_INDEX_BODY,
22 | }]
23 | }
24 | return blockGraphic;
25 | };
26 |
--------------------------------------------------------------------------------
/src/ts/factories/gun.factory.ts:
--------------------------------------------------------------------------------
1 | const gunFactoryFactory = (holderFactory?: EntityFactory) => {
2 | return (x: number, y: number, id: IdFactory) => {
3 | const gun: Gun = {
4 | entityType: ENTITY_TYPE_GUN,
5 | graphic: gunGraphic,
6 | palette: [[0, 0, 80], [0, 0, 90]],
7 | bounds: rectangleCenterBounds(x, y, .8, .3),
8 | collisionGroup: COLLISION_GROUP_ITEMS,
9 | collisionMask: COLLISION_MASK_ITEMS,
10 | gravityMultiplier: 1,
11 | eid: id(),
12 | mass: .1,
13 | velocity: [0, 0],
14 | restitution: .4,
15 | entityOrientation: 0,
16 | orientationStartTime: 0,
17 | lastFired: 0,
18 | fireRate: BULLET_INTERVAL,
19 | };
20 | if (holderFactory) {
21 | const result = holderFactory(x, y, id);
22 | const e = result[0] as ActiveMovableEntity;
23 | e.holding[e.handJointId] = gun;
24 | return result;
25 | } else {
26 | return [gun];
27 | }
28 | }
29 | };
--------------------------------------------------------------------------------
/src/ts/factories/block.factory.ts:
--------------------------------------------------------------------------------
1 | const blockFactoryFactory = ([hue, baseSaturation, baseLighting]: HSL, rounding = 4, width = 1, height = 1) => {
2 | const blockGraphic = blockGraphicFactory(32 * width, 32 * height, rounding);
3 | return (x: number, y: number, id: IdFactory) => {
4 | const lighting = FLAG_RANDOMIZE_BLOCK_COLORS
5 | ? baseLighting + Math.random() * 5 - y
6 | : baseLighting;
7 | const saturation = FLAG_RANDOMIZE_BLOCK_COLORS
8 | ? baseSaturation - Math.random() * 5 + y
9 | : baseSaturation;
10 | const palette: HSL[] = [
11 | [hue, saturation, lighting + 9],
12 | [hue, saturation - 9, lighting],
13 | [hue, saturation, lighting - 9],
14 | ];
15 | const block: Block = {
16 | eid: id(),
17 | graphic: blockGraphic,
18 | palette,
19 | entityType: ENTITY_TYPE_BLOCK,
20 | collisionGroup: COLLISION_GROUP_TERRAIN,
21 | collisionMask: COLLISION_MASK_TERRAIN,
22 | bounds: [x + (1 - width)/2, y, width, height],
23 | }
24 | return [block];
25 | }
26 | };
27 |
28 |
--------------------------------------------------------------------------------
/src/ts/factories/mainframe.factory.ts:
--------------------------------------------------------------------------------
1 | const mainframeFactoryFactory = (hue: number) => {
2 | const palette: HSL[] = [
3 | [hue, 40, 60],
4 | [0, 0, 30],
5 | [0, 0, 9],
6 | ];
7 | return (x: number, y: number, id: IdFactory) => {
8 | const mainframe: Robot = {
9 | entityType: ENTITY_TYPE_ROBOT,
10 | graphic: mainframeGraphic,
11 | palette,
12 | bounds: rectangleCenterBounds(x, y, 1, 1.5),
13 | // only collide with robots on the bottom or top
14 | // don't collide with items at all
15 | collisionMask: COLLISION_MASK_BACKGROUNDED,
16 | collisionGroup: COLLISION_GROUP_BACKGROUNDED,
17 | gravityMultiplier: 0,
18 | eid: id(),
19 | nextInstructionTime: 0,
20 | nextScriptIndex: 0,
21 | velocity: [0, 0],
22 | baseVelocity: BASE_VELOCITY/3,
23 | entityOrientation: 1,
24 | orientationStartTime: 0,
25 | activeInputs: {
26 | },
27 | hue,
28 | holding: {},
29 | capabilities: [
30 | INSTRUCTION_ID_DO_NOTHING,
31 | INSTRUCTION_ID_SAVE,
32 | ],
33 | };
34 | return [mainframe];
35 | };
36 | }
--------------------------------------------------------------------------------
/src/ts/factories/tape.factory.ts:
--------------------------------------------------------------------------------
1 | const tapeFactoryFactory = (script: number[], hue: number, into?: EntityFactory, scale = 1) => {
2 | const palette: HSL[] = [
3 | [hue, 99, 70],
4 | [0, 0, 25],
5 | [0, 0, 99],
6 | ];
7 | return (x: number, y: number, id: IdFactory) => {
8 | const tape: Tape = {
9 | entityType: ENTITY_TYPE_TAPE,
10 | graphic: tapeGraphic,
11 | palette,
12 | bounds: rectangleCenterBounds(x, y, .5 * scale, .4 * scale),
13 | // don't collide with other items
14 | collisionGroup: scale > 1 ? COLLISION_GROUP_PUSHABLES : COLLISION_GROUP_ITEMS,
15 | collisionMask: scale > 1 ? COLLISION_MASK_PUSHABLES : COLLISION_MASK_ITEMS,
16 | gravityMultiplier: 1,
17 | eid: id(),
18 | mass: scale,
19 | velocity: [0, 0],
20 | restitution: .4,
21 | entityOrientation: 0,
22 | orientationStartTime: 0,
23 | script: [...script],
24 | hue,
25 | }
26 | if (into) {
27 | const entities = into(x, y, id);
28 | const ee = entities[0] as EveryEntity;
29 | ee.holding[ee.insertionJointId] = tape;
30 | return entities;
31 | } else {
32 | return [tape];
33 | }
34 | }
35 | };
--------------------------------------------------------------------------------
/src/ts/factories/boss.factory.ts:
--------------------------------------------------------------------------------
1 | const bossFactoryFactory = (hue: number, entityFactory?: EntityFactory, asspullFactory?: () => Asspull) => {
2 | return (x: number, y: number, id: IdFactory) => {
3 | const boss: Robot = {
4 | graphic: playerGraphic,
5 | palette: bossPalette,
6 | entityType: ENTITY_TYPE_ROBOT,
7 | bounds: rectangleCenterBounds(x, y, 1.5, 1.9),
8 | collisionGroup: COLLISION_GROUP_PLAYER,
9 | collisionMask: COLLISION_MASK_PLAYER,
10 | gravityMultiplier: 1,
11 | entityOrientation: ORIENTATION_RIGHT,
12 | orientationStartTime: 0,
13 | eid: id(),
14 | mass: 49,
15 | velocity: [0, 0],
16 | baseVelocity: BASE_VELOCITY,
17 | airTurn: 1,
18 | activeInputs: {},
19 | holding: {
20 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_HAND]: entityFactory && entityFactory(x, y, id)[0] as MovableEntity,
21 | },
22 | hue,
23 | handJointId: PLAYER_GRAPHIC_JOINT_ID_RIGHT_HAND,
24 | insertionJointId: PLAYER_GRAPHIC_JOINT_ID_TAPE_DECK,
25 | capabilities: INSTRUCTIONS.map((instruction, i) => i),
26 | nextScriptIndex: 0,
27 | asspull: asspullFactory && asspullFactory(),
28 | };
29 | return [boss];
30 | }
31 | };
--------------------------------------------------------------------------------
/src/ts/factories/repeater.factory.ts:
--------------------------------------------------------------------------------
1 | const repeaterFactoryFactory = (hue: number) => {
2 | const palette: HSL[] = [
3 | [hue, 30, 60],
4 | [hue, 40, 50],
5 | [0, 0, 30],
6 | [hue, 30, 30],
7 | [0, 0, 99],
8 | ];
9 | return (x: number, y: number, id: IdFactory) => {
10 | const repeater: Repeater = {
11 | graphic: repeaterGraphic,
12 | autoRewind: 1,
13 | baseVelocity: 0,
14 | bounds: rectangleCenterBounds(x, y, 1, .75),
15 | collisionGroup: COLLISION_GROUP_BACKGROUNDED,
16 | collisionMask: COLLISION_MASK_BACKGROUNDED,
17 | gravityMultiplier: 0,
18 | holding: {},
19 | eid: id(),
20 | activeInputs: {
21 | },
22 | entityType: ENTITY_TYPE_REPEATER,
23 | insertionJointId: REPEATER_GRAPHIC_JOINT_ID_TAPE_DECK,
24 | palette,
25 | velocity: [0, 0],
26 | playing: 1,
27 | playbackStartTime: 0,
28 | toSpeak: [],
29 | hue,
30 | // start playing immediately
31 | instructionsHeard: [INSTRUCTION_ID_PLAY],
32 | capabilities: [INSTRUCTION_ID_PLAY, INSTRUCTION_ID_REWIND, INSTRUCTION_ID_FAST_FORWARD, INSTRUCTION_ID_INSERT, INSTRUCTION_ID_EJECT],
33 | };
34 | return [repeater];
35 | }
36 | };
--------------------------------------------------------------------------------
/src/ts/factories/pressureplate.factory.ts:
--------------------------------------------------------------------------------
1 | const pressurePlateFactoryFactory = (
2 | width: number,
3 | height: number,
4 | [hue, baseSaturation, baseLighting]: HSL,
5 | edge: Edge = EDGE_TOP,
6 | ) => {
7 | const palette: HSL[] = [
8 | [hue, baseSaturation, baseLighting + 9],
9 | [hue, baseSaturation - 9, baseLighting],
10 | [hue, baseSaturation, baseLighting - 9],
11 | [hue, baseSaturation, baseLighting - 19],
12 | [0, 0, 99],
13 | ];
14 | const graphic = pressurePlateGraphicFactory(width * 32, height * 32, edge);
15 | return (x: number, y: number, id: IdFactory) => {
16 | const pressurePlate: PressurePlate = {
17 | eid: id(),
18 | graphic,
19 | palette,
20 | entityType: ENTITY_TYPE_PRESSURE_PLATE,
21 | collisionGroup: COLLISION_GROUP_TERRAIN,
22 | collisionMask: COLLISION_MASK_TERRAIN,
23 | bounds: [x, y, width, height],
24 | baseVelocity: 0,
25 | holding: {},
26 | handJointId: 0,
27 | insertionJointId: PRESSURE_PLATE_GRAPHIC_JOINT_ID_TAPE,
28 | activeInputs: {
29 | },
30 | gravityMultiplier: 0,
31 | toSpeak: [],
32 | velocity: [0, 0],
33 | autoRewind: 1,
34 | //capabilities: [INSTRUCTION_ID_PLAY, INSTRUCTION_ID_REWIND, INSTRUCTION_ID_FAST_FORWARD, INSTRUCTION_ID_EJECT],
35 | pressureEdge: edge,
36 | }
37 | return [pressurePlate];
38 | }
39 | };
40 |
41 |
--------------------------------------------------------------------------------
/src/ts/factories/platform.factory.ts:
--------------------------------------------------------------------------------
1 | const platformFactoryFactory = (w: number, h: number, direction: Edge, hue: number) => {
2 | const capabilities: number[] = direction % 2
3 | ? [INSTRUCTION_ID_UP, INSTRUCTION_ID_DOWN]
4 | : [INSTRUCTION_ID_LEFT, INSTRUCTION_ID_RIGHT];
5 | return (x: number, y: number, id: IdFactory, pid?: number) => {
6 | const lightingBoost = pid ? 30 : -30;
7 | const palette: HSL[] = [
8 | [hue, 60, 60],
9 | [hue, 40, 50],
10 | [hue, 60, 40],
11 | [hue, 40, 50 - lightingBoost],
12 | [hue, 40, 50 + lightingBoost],
13 | ];
14 | const graphic = platformGraphicFactory(w * 32, h * 32, direction);
15 | const platform: Platform = {
16 | entityType: ENTITY_TYPE_PLATFORM,
17 | graphic,
18 | palette,
19 | hue,
20 | bounds: [x, y, w, h],
21 | collisionMask: COLLISION_MASK_TERRAIN,
22 | collisionGroup: COLLISION_GROUP_TERRAIN,
23 | gravityMultiplier: 0,
24 | eid: id(),
25 | velocity: [0, 0],
26 | // can't be too fast or we outpace gravity and downward room transitions don't work while riding platforms
27 | baseVelocity: .0028,
28 | activeInputs: {
29 | },
30 | holding: {},
31 | capabilities: [...capabilities, INSTRUCTION_ID_DO_NOTHING],
32 | airTurn: 1,
33 | direction,
34 | };
35 | return [platform];
36 | };
37 | };
--------------------------------------------------------------------------------
/src/ts/graphics/spikes.graphic.ts:
--------------------------------------------------------------------------------
1 | const SPIKES_GRAPHIC_PALETTE_INDEX_LIGHT = 0;
2 | const SPIKES_GRAPHIC_PALETTE_INDEX_DARK = 1;
3 |
4 | const SPIKES_GRAPHIC_IMAGE_INDEX_SPIKE = 0;
5 |
6 | const spikesPalette: HSL[] = [
7 | [0, 0, 99],
8 | [0, 0, 40],
9 | ]
10 |
11 | const randomSpikeGraphic = () => {
12 | const height = 8;
13 | const width = 32;
14 | const joints: Joint[] = [];
15 | const jointCount = Math.random() * 4 | 0 + 8;
16 | for (let i=0; i<=jointCount; i++) {
17 | const p = i / jointCount - .5;
18 | joints.push({
19 | imageIndex: SPIKES_GRAPHIC_IMAGE_INDEX_SPIKE,
20 | transformations: [{
21 | transformType: TRANSFORM_TYPE_TRANSLATE,
22 | dx: (i + (Math.random() * 2 - 1)) * width/jointCount - width/2 - p * 3,
23 | dy: 14,
24 | }, {
25 | transformType: TRANSFORM_TYPE_SCALE,
26 | scaleX: (Math.random() + 2) / 3,
27 | scaleY: (Math.random() + 2.5 - Math.abs(p)) / 2,
28 | }, {
29 | transformType: TRANSFORM_TYPE_ROTATE,
30 | rAngle: LOW_P_MATH_PI * (Math.random() - .5)/9 + p * LOW_P_MATH_PI/9,
31 | }]
32 | });
33 | }
34 | const spikeGraphic: Graphic = {
35 | imageryWidth: width,
36 | imageryHeight: height,
37 | imagery: [
38 | // spike
39 | [
40 | [0, -12, 4, 0, -4, 0, SPIKES_GRAPHIC_PALETTE_INDEX_LIGHT, SPIKES_GRAPHIC_PALETTE_INDEX_DARK],
41 | ],
42 | ],
43 | joints,
44 | };
45 | return spikeGraphic;
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/src/ts/graphics/tape.graphic.ts:
--------------------------------------------------------------------------------
1 | const TAPE_GRAPHIC_PALETTE_INDEX_LABEL_COLOR = 0;
2 | const TAPE_GRAPHIC_PALETTE_INDEX_BODY_COLOR = 1;
3 | const TAPE_GRAPHIC_PALETTE_INDEX_EYE_COLOR = 2;
4 |
5 | const TAPE_GRAPHIC_IMAGE_INDEX_BODY = 0;
6 | const TAPE_GRAPHIC_IMAGE_INDEX_EYE = 1;
7 |
8 | const TAPE_GRAPHIC_JOINT_ID_BODY = 0;
9 |
10 | const tapeGraphic: Graphic = {
11 | imageryWidth: 18,
12 | imageryHeight: 12,
13 | imagery: [
14 | // tape body
15 | [
16 | [-9, -6, 18, 12, TAPE_GRAPHIC_PALETTE_INDEX_BODY_COLOR, [2, 2]],
17 | [-8, -5, 16, 8, TAPE_GRAPHIC_PALETTE_INDEX_LABEL_COLOR],
18 | [-6, -3, 12, 4, TAPE_GRAPHIC_PALETTE_INDEX_BODY_COLOR, 2],
19 | ],
20 | // eye
21 | [
22 | [-1, -1, 2, 2, TAPE_GRAPHIC_PALETTE_INDEX_EYE_COLOR, 1],
23 | ]
24 | ],
25 | joints: [{
26 | //id: TAPE_GRAPHIC_JOINT_ID_BODY,
27 | imageIndex: TAPE_GRAPHIC_IMAGE_INDEX_BODY,
28 | transformations: [{
29 | transformType: TRANSFORM_TYPE_TRANSLATE,
30 | dx: 0,
31 | dy: 6,
32 | }],
33 | renderAfter: [{
34 | imageIndex: TAPE_GRAPHIC_IMAGE_INDEX_EYE,
35 | transformations: [{
36 | transformType: TRANSFORM_TYPE_TRANSLATE,
37 | dx: -4,
38 | dy: -1,
39 | }]
40 | }, {
41 | imageIndex: TAPE_GRAPHIC_IMAGE_INDEX_EYE,
42 | transformations: [{
43 | transformType: TRANSFORM_TYPE_TRANSLATE,
44 | dx: 4,
45 | dy: -1,
46 | }]
47 | }],
48 | }]
49 | }
--------------------------------------------------------------------------------
/src/ts/factories/robot.factory.ts:
--------------------------------------------------------------------------------
1 | const robotFactoryFactory = (orientation: Orientation, hue: number) => {
2 | const palette: HSL[] = [
3 | [hue, 50, 50],
4 | [hue, 50, 40],
5 | [hue, 50, 20],
6 | [hue, 0, 99],
7 | ];
8 | return (x: number, y: number, id: IdFactory) => {
9 | const robot: Robot = {
10 | entityType: ENTITY_TYPE_ROBOT,
11 | graphic: robotGraphic,
12 | palette,
13 | bounds: rectangleCenterBounds(x, y, .9, .9),
14 | // only collide with robots on the bottom or top
15 | // don't collide with items at all
16 | collisionMask: COLLISION_MASK_ENEMIES,
17 | collisionGroup: COLLISION_GROUP_ENEMIES,
18 | gravityMultiplier: 1,
19 | eid: id(),
20 | mass: 2,
21 | velocity: [0, 0],
22 | baseVelocity: BASE_VELOCITY/3,
23 | entityOrientation: orientation,
24 | orientationStartTime: 0,
25 | activeInputs: {
26 | },
27 | holding: {},
28 | hue,
29 | handJointId: ROBOT_GRAPHIC_JOINT_ID_LEFT_ARM,
30 | // insertionJointId: ROBOT_GRAPHIC_JOINT_ID_TAPE_DECK,
31 | nextScriptIndex: 0,
32 | capabilities: [
33 | INSTRUCTION_ID_UP,
34 | INSTRUCTION_ID_DOWN,
35 | INSTRUCTION_ID_LEFT,
36 | INSTRUCTION_ID_RIGHT,
37 | // INSTRUCTION_ID_REWIND,
38 | // INSTRUCTION_ID_FAST_FORWARD,
39 | // INSTRUCTION_ID_PICK_UP,
40 | INSTRUCTION_ID_DROP,
41 | // INSTRUCTION_ID_THROW,
42 | // INSTRUCTION_ID_EJECT,
43 | // INSTRUCTION_ID_PLAY,
44 | INSTRUCTION_ID_SHOOT,
45 | ],
46 | };
47 | return [robot];
48 | }
49 | };
--------------------------------------------------------------------------------
/src/ts/graphics/repeater.graphic.ts:
--------------------------------------------------------------------------------
1 | const REPEATER_GRAPHIC_PALETTE_INDEX_MEDIUM = 0;
2 | const REPEATER_GRAPHIC_PALETTE_INDEX_DARK = 1;
3 | const REPEATER_GRAPHIC_PALETTE_INDEX_SPEAKER = 2;
4 | const REPEATER_GRAPHIC_PALETTE_INDEX_TAPE_DECK = 3;
5 | const REPEATER_GRAPHIC_PALETTE_INDEX_EYES = 4;
6 |
7 | const REPEATER_GRAPHIC_IMAGE_INDEX_BODY = 0;
8 |
9 | const REPEATER_GRAPHIC_JOINT_ID_BODY = 0;
10 | const REPEATER_GRAPHIC_JOINT_ID_TAPE_DECK = 1;
11 |
12 | const REPEATER_ROUNDING = 5;
13 |
14 | const repeaterPaletteCyan: HSL[] = [
15 | [210, 30, 70],
16 | [210, 20, 60],
17 | [210, 30, 50],
18 | [0, 0, 30],
19 | [210, 30, 30],
20 | [0, 0, 99],
21 | ];
22 |
23 | const repeaterGraphic: Graphic = {
24 | imageryWidth: 32,
25 | imageryHeight: 24,
26 | imagery: [
27 | // body
28 | [
29 | [-16, -12, 32, 24, REPEATER_GRAPHIC_PALETTE_INDEX_DARK, REPEATER_ROUNDING],
30 | [-15, -11, 30, 22, REPEATER_GRAPHIC_PALETTE_INDEX_MEDIUM, REPEATER_ROUNDING - 1],
31 | [1.5, -6.5, 12, 12, REPEATER_GRAPHIC_PALETTE_INDEX_DARK, 6],
32 | [2, -6, 12, 12, REPEATER_GRAPHIC_PALETTE_INDEX_SPEAKER, 6],
33 | [-13.5, -6, 13, 8, REPEATER_GRAPHIC_PALETTE_INDEX_TAPE_DECK],
34 | [-5, -3, 2, 2, REPEATER_GRAPHIC_PALETTE_INDEX_EYES, 1],
35 | [-11, -3, 2, 2, REPEATER_GRAPHIC_PALETTE_INDEX_EYES, 1],
36 | ],
37 | ],
38 | joints: [{
39 | //id: REPEATER_GRAPHIC_JOINT_ID_BODY,
40 | imageIndex: REPEATER_GRAPHIC_IMAGE_INDEX_BODY,
41 | transformations: [{
42 | transformType: TRANSFORM_TYPE_TRANSLATE,
43 | dx: 0,
44 | dy: 12,
45 | }],
46 | }, {
47 | gid: REPEATER_GRAPHIC_JOINT_ID_TAPE_DECK,
48 | transformations: [{
49 | transformType: TRANSFORM_TYPE_TRANSLATE,
50 | dx: -7.5,
51 | dy: 5.5,
52 | }]
53 | }]
54 | }
--------------------------------------------------------------------------------
/src/ts/constants.ts:
--------------------------------------------------------------------------------
1 | const MAX_TILES_ACROSS = 18;
2 | const MAX_TILES_DOWN = 13;
3 | const MAX_TILES_ACROSS_MINUS_1 = 17;
4 | const MAX_TILES_DOWN_MINUS_1 = 12;
5 | const DEFAULT_GRAVITY: Vector = [0, 7e-5]; //[0, .00007];
6 | const BASE_VELOCITY = .006;
7 | const JUMP_VELOCITY = .014;
8 | const CLIMB_VELOCITY = .011;
9 | const MAX_VELOCITY = .015;
10 | const MAX_JUMP_AGE = 99;
11 | const TURN_DURATION = 150;
12 | const SCALING_JUMP = 1;
13 | const GRAB_DIMENSION = .15;
14 | const GRAB_DIMENSION_X_2 = .3;
15 | const GRAB_VELOCITY_SCALE = .9;
16 | const MAX_DELTA = Math.floor(GRAB_DIMENSION_X_2/MAX_VELOCITY) - 1; // 19 ms
17 | const MIN_DELTA = 5;
18 | const MAX_ROUNDING_ERROR_SIZE = 1e-6;//.000001;
19 | const MAX_COLLISION_COUNT = 2;
20 | const AUTOMATIC_ANIMATION_DELAY = 40;
21 | const GRAB_OFFSET = .01;
22 | const THROW_POWER = .04;
23 | const EJECT_VELOCITY = .01;
24 | const INSTRUCTION_DURATION = .3;
25 | const DTMF_FREQUENCIES_1 = [1209, 1336, 1477];
26 | const DTMF_FREQUENCIES_2 = [697, 770, 852, 941, 1038, 1131];
27 | const PLAYBACK_INTERVAL = 999;
28 | const BULLET_INTERVAL = 199;
29 | const REWIND_INTERVAL = 199;
30 | const SPEECH_FADE_INTERVAL = PLAYBACK_INTERVAL * 2;
31 | const SPEECH_TEXT_HEIGHT = 1;
32 | const SPEECH_TEXT_SCALE = .5;
33 | const SPEECH_TEXT_PADDING = .2;
34 | const SPEECH_CALLOUT_HEIGHT = .2;
35 | const SPEECH_CALLOUT_WIDTH = .2;
36 | const MAX_VISIBLE_INSTRUCTIONS = 1;
37 | const MESSAGE_DISPLAY_TIME = 2999;
38 | const MAX_DEATH_AGE = 999;
39 | const CARRY_AGE_CHECK = 40;
40 | const BULLET_WIDTH = .3;
41 | const BULLET_HEIGHT = .1;
42 | const SOUND_WAVE_STEP_TIME = 40;
43 | const SOUND_WAVE_DISPLAY_TIME = 99;
44 | const MATH_PI = 3.14;
45 | const MATH_PI_2 = 6.28;
46 | const MATH_PI_ON_2 = 1.57;
47 | const MED_P_MATH_PI = 3.1;
48 | const MED_P_MATH_PI_2 = 6.3;
49 | const MED_P_MATH_PI_ON_2 = 1.6;
50 | const LOW_P_MATH_PI = 3;
51 | const LOW_P_MATH_PI_2 = 6;
52 | const LOW_P_MATH_PI_ON_2 = 1.6;
53 | const LOW_P_MATH_PI_ON_3 = 1;
54 | const LOW_P_MATH_PI_ON_4 = .8;
--------------------------------------------------------------------------------
/src/ts/graphics/platform.graphic.ts:
--------------------------------------------------------------------------------
1 | const PLATFORM_GRAPHIC_PALETTE_INDEX_LIGHT = 0;
2 | const PLATFORM_GRAPHIC_PALETTE_INDEX_MEDIUM = 1;
3 | const PLATFORM_GRAPHIC_PALETTE_INDEX_DARK = 2;
4 | const PLATFORM_GRAPHIC_PALETTE_INDEX_ARROW_FILL = 3;
5 | const PLATFORM_GRAPHIC_PALETTE_INDEX_ARROW_STROKE = 4;
6 |
7 | const PLATFORM_GRAPHIC_IMAGE_INDEX_BODY = 0;
8 | const PLATFORM_GRAPHIC_IMAGE_INDEX_ARROW = 1;
9 |
10 | const PLATFORM_GRAPHIC_ROUNDINGS: [number, number, number, number] = [0, 0, 10, 10];
11 |
12 | const platformGraphicFactory = (width: number, height: number, edge: Edge) => {
13 | const vertical = edge % 2;
14 | const roundings = height > width ? 0 : PLATFORM_GRAPHIC_ROUNDINGS;
15 | const blockGraphic: Graphic = {
16 | imageryWidth: width,
17 | imageryHeight: height,
18 | imagery: [
19 | // block
20 | [
21 | [-width/2, 0, width, height, BLOCK_GRAPHIC_PALETTE_INDEX_MEDIUM, roundings],
22 | [-width/2, 0, width - 2, height - 2, BLOCK_GRAPHIC_PALETTE_INDEX_LIGHT, roundings],
23 | [2 - width/2, 2, width - 2, height - 2, BLOCK_GRAPHIC_PALETTE_INDEX_DARK, roundings],
24 | [2 - width/2, 2, width - 4, height - 4, BLOCK_GRAPHIC_PALETTE_INDEX_MEDIUM, roundings],
25 | ],
26 | // arrow
27 | [
28 | [0, 0, -8, -4, -8, 4, PLATFORM_GRAPHIC_PALETTE_INDEX_ARROW_FILL, PLATFORM_GRAPHIC_PALETTE_INDEX_ARROW_STROKE],
29 | ],
30 | ],
31 | joints: [{
32 | imageIndex: PLATFORM_GRAPHIC_IMAGE_INDEX_BODY,
33 | renderAfter: [{
34 | imageIndex: PLATFORM_GRAPHIC_IMAGE_INDEX_ARROW,
35 | transformations: [{
36 | transformType: TRANSFORM_TYPE_TRANSLATE,
37 | dx: vertical ? 0 : width/2 - 4,
38 | dy: vertical ? height - 4 : height/2,
39 | }, {
40 | transformType: TRANSFORM_TYPE_ROTATE,
41 | rAngle: vertical ? MATH_PI_ON_2 : 0,
42 | }]
43 | }, {
44 | imageIndex: PLATFORM_GRAPHIC_IMAGE_INDEX_ARROW,
45 | transformations: [{
46 | transformType: TRANSFORM_TYPE_TRANSLATE,
47 | dx: vertical ? 0 : -width/2 + 4,
48 | dy: vertical ? 4 : height/2,
49 | }, {
50 | transformType: TRANSFORM_TYPE_ROTATE,
51 | rAngle: vertical ? -MED_P_MATH_PI_ON_2 : MED_P_MATH_PI,
52 | }]
53 | }]
54 | }]
55 | }
56 | return blockGraphic;
57 | };
58 |
--------------------------------------------------------------------------------
/src/ts/common/shapes.ts:
--------------------------------------------------------------------------------
1 | const EDGE_LEFT = 0;
2 | const EDGE_TOP = 1;
3 | const EDGE_RIGHT = 2;
4 | const EDGE_BOTTOM = 3;
5 |
6 | type Edge = 0 | 1 | 2 | 3;
7 |
8 | const EDGE_OFFSETS: Vector[] = [
9 | [-1, 0],
10 | [0, -1],
11 | [1, 0],
12 | [0, 1],
13 | ];
14 |
15 | type Rectangle = [number, number, number, number];
16 |
17 | type Vector = [number, number];
18 |
19 | const rectangleCenterBounds = (x: number, y: number, w: number, h: number) => [x + (1 - w)/2, y + 1 - h, w, h] as Rectangle;
20 |
21 | // let rectangleLineOverlaps = (r1: Rectangle, r2: Rectangle) =>
22 | // axisMap(r1, r2, ([scalar1, length1]: number[], [scalar2, length2]: number[]) => {
23 | // const min1 = scalar1;
24 | // const min2 = scalar2;
25 | // const max1 = scalar1 + length1;
26 | // const max2 = scalar2 + length2;
27 | // return min1 >= min2 && min1 < max2 || min2 >= min1 && min2 < max1;
28 | // });
29 |
30 | // let rectangleOverlaps = (r1: Rectangle, r2: Rectangle) => {
31 | // const overlap = rectangleLineOverlap(r1, r2);
32 | // return overlap[0] && overlap[1];
33 | // };
34 |
35 | let rectangleLineOverlap = (r1: Rectangle, r2: Rectangle) =>
36 | axisMap(r1, r2, ([scalar1, length1]: number[], [scalar2, length2]: number[]) => {
37 | const min1 = scalar1;
38 | const min2 = scalar2;
39 | const max1 = scalar1 + length1;
40 | const max2 = scalar2 + length2;
41 | return Math.max(0, Math.min(max1, max2) - Math.max(min1, min2));
42 | });
43 |
44 | let rectangleOverlap = (r1: Rectangle, r2: Rectangle) => {
45 | const overlap = rectangleLineOverlap(r1, r2);
46 | return overlap[0] * overlap[1];
47 | }
48 |
49 | let rectangleRoundInBounds = (r: Rectangle, roomBounds: Rectangle) => {
50 | return axisMap(r, roomBounds, ([s, l]: [number, number], [_, max]: [number, number]) => {
51 | let maxRounded = Math.min(Math.floor(s + l), max - 1);
52 | let minRounded = Math.max(0, Math.floor(s));
53 | return [minRounded, maxRounded];
54 | });
55 | }
56 |
57 | let rectangleIterateBounds = (bounds: Rectangle | undefined, roomBounds: Rectangle, i: (x: number, y: number) => void) => {
58 | if (bounds) {
59 | const [[minx, maxx], [miny, maxy]] = rectangleRoundInBounds(bounds, roomBounds);
60 | for (let tx=minx; tx<=maxx; tx++) {
61 | for (let ty=miny; ty<=maxy; ty++) {
62 | i(tx, ty);
63 | }
64 | }
65 | }
66 | }
67 |
68 | const axisFilter1 = (_: any, i: number) => i % 2 == 0;
69 | const axisFilter2 = (_: any, i: number) => i % 2 > 0;
70 |
71 | let axisMap = (r1: number[], r2: number[], t: (values: number[], values2: number[], i: number) => T, into: T[] = [], intoOffset = 0): T[] => {
72 | into[intoOffset] = t(r1.filter(axisFilter1), r2.filter(axisFilter1), 0);
73 | into[intoOffset+1] = t(r1.filter(axisFilter2), r2.filter(axisFilter2), 1);
74 | return into as [T, T];
75 | }
76 |
--------------------------------------------------------------------------------
/src/ts/graphics/crate.graphic.ts:
--------------------------------------------------------------------------------
1 | const CRATE_GRAPHIC_PALETTE_INDEX_LIGHT = 0;
2 | const CRATE_GRAPHIC_PALETTE_INDEX_MEDIUM = 1;
3 | const CRATE_GRAPHIC_PALETTE_INDEX_DARK = 2;
4 |
5 | const CRATE_GRAPHIC_IMAGE_INDEX_BOARD = 0;
6 |
7 | const CRATE_GRAPHIC_JOINT_ID_BODY = 0;
8 |
9 | const cratePalette: HSL[] = [
10 | [30, 40, 40],
11 | [30, 50, 30],
12 | [30, 50, 20],
13 | ];
14 |
15 | const crateGraphic: Graphic = {
16 | imageryWidth: 16,
17 | imageryHeight: 16,
18 | imagery: [
19 | // board
20 | [
21 | [-7.5, -1.5, 15, 3, CRATE_GRAPHIC_PALETTE_INDEX_MEDIUM],
22 | [-8, -1, 1, 2, CRATE_GRAPHIC_PALETTE_INDEX_LIGHT],
23 | [-7, -2, 14, 1, CRATE_GRAPHIC_PALETTE_INDEX_LIGHT],
24 | [7, -1, 1, 2, CRATE_GRAPHIC_PALETTE_INDEX_DARK],
25 | [-7, 1, 14, 1, CRATE_GRAPHIC_PALETTE_INDEX_DARK],
26 | ],
27 | ],
28 | joints: [{
29 | //id: CRATE_GRAPHIC_JOINT_ID_BODY,
30 | transformations: [{
31 | transformType: TRANSFORM_TYPE_TRANSLATE,
32 | dx: 0,
33 | dy: 2,
34 | }],
35 | renderAfter: [{
36 | imageIndex: CRATE_GRAPHIC_IMAGE_INDEX_BOARD,
37 | transformations: [{
38 | transformType: TRANSFORM_TYPE_TRANSLATE,
39 | dx: 0,
40 | dy: 4,
41 | }],
42 | }, {
43 | imageIndex: CRATE_GRAPHIC_IMAGE_INDEX_BOARD,
44 | transformations: [{
45 | transformType: TRANSFORM_TYPE_TRANSLATE,
46 | dx: 0,
47 | dy: 8,
48 | }],
49 | }, {
50 | imageIndex: CRATE_GRAPHIC_IMAGE_INDEX_BOARD,
51 | transformations: [{
52 | transformType: TRANSFORM_TYPE_TRANSLATE,
53 | dx: -6,
54 | dy: 6,
55 | }, {
56 | transformType: TRANSFORM_TYPE_SCALE,
57 | scaleX: 1,
58 | scaleY:-1,
59 | }, {
60 | transformType: TRANSFORM_TYPE_ROTATE,
61 | rAngle: -MED_P_MATH_PI_ON_2,
62 | }],
63 | }, {
64 | imageIndex: CRATE_GRAPHIC_IMAGE_INDEX_BOARD,
65 | transformations: [{
66 | transformType: TRANSFORM_TYPE_TRANSLATE,
67 | dx: 6,
68 | dy: 6,
69 | }, {
70 | transformType: TRANSFORM_TYPE_SCALE,
71 | scaleX: 1,
72 | scaleY:-1,
73 | }, {
74 | transformType: TRANSFORM_TYPE_ROTATE,
75 | rAngle: -MED_P_MATH_PI_ON_2,
76 | }],
77 | }, {
78 | imageIndex: CRATE_GRAPHIC_IMAGE_INDEX_BOARD,
79 | }, {
80 | imageIndex: CRATE_GRAPHIC_IMAGE_INDEX_BOARD,
81 | transformations: [{
82 | transformType: TRANSFORM_TYPE_TRANSLATE,
83 | dx: 0,
84 | dy: 12,
85 | }],
86 | }]
87 | }]
88 | }
--------------------------------------------------------------------------------
/src/ts/game/room.ts:
--------------------------------------------------------------------------------
1 | type SoundWave = {
2 | tileReachability: number[][];
3 | hue: number;
4 | timeSaid: number;
5 | }
6 |
7 | type Room = {
8 | allEntities: Entity[];
9 | updatableEntities: Entity[];
10 | tiles: Entity[][][];
11 | bounds: Rectangle;
12 | gravity: Vector;
13 | recorder?: RecordingEntity;
14 | soundWaves: SoundWave[];
15 | bg: HSL[],
16 | }
17 |
18 | type IdFactory = () => number;
19 |
20 | type RoomFactory = (x: number, y: number, id: IdFactory) => Room;
21 |
22 | let roomIterateEntities = (room: Room, bounds: Rectangle | undefined, i: (entity: Entity) => void, useBoundsWithVelocity?: number | boolean) => {
23 | const handled = new Set();
24 | roomIterateBounds(room, bounds, tile => tile.forEach(e => {
25 | if (!handled.has(e.eid) && rectangleOverlap(useBoundsWithVelocity && (e as MovableEntity).boundsWithVelocity || (e as MovableEntity).bounds, bounds)) {
26 | i(e);
27 | handled.add(e.eid);
28 | }
29 | }));
30 | }
31 |
32 | let roomIterateBounds = (room: Room, bounds: Rectangle | undefined, i: (tile: Entity[], tx: number, ty: number) => void) => {
33 | rectangleIterateBounds(bounds, room.bounds, (tx, ty) => {
34 | i(room.tiles[tx][ty], tx, ty);
35 | })
36 | }
37 |
38 | let roomAddEntity = (room: Room, entity: Entity, deltas?: Vector) => {
39 | const everyEntity = entity as EveryEntity;
40 | if (deltas) {
41 | axisMap(deltas, everyEntity.bounds, ([d], [v]) => v + d, everyEntity.bounds);
42 | }
43 | room.allEntities.push(entity);
44 | if(everyEntity.velocity) {
45 | room.updatableEntities.push(entity);
46 | }
47 | roomAddEntityToTiles(room, entity);
48 | if (deltas) {
49 | [...(entity as MovableEntity).carrying, ...(entity as MovableEntity).carryingPreviously].forEach(
50 | e => roomAddEntity(room, e as Entity, deltas)
51 | );
52 | }
53 | }
54 |
55 | let roomAddEntityToTiles = (room: Room, entity: Entity) => {
56 | if (!(entity as MortalEntity).deathAge) {
57 | const movableEntity = entity as MovableEntity;
58 | entityCalculateBoundsWithVelocity(entity);
59 | if (FLAG_CHECK_TILES_VALID) {
60 | const alreadyThere = room.tiles.find(tiles => tiles.find(entities => entities.find(e => e == entity)));
61 | if (alreadyThere) {
62 | console.log('added but already there');
63 | }
64 | }
65 |
66 | roomIterateBounds(room, movableEntity.boundsWithVelocity || movableEntity.bounds, tile => tile.push(entity));
67 | }
68 | }
69 |
70 | let roomRemoveEntity = (room: Room, entity: Entity, includeCarried?: number | boolean) => {
71 | arrayRemoveElement(room.allEntities, entity);
72 | if((entity as MovableEntity).velocity || (entity as GraphicalEntity).graphic) {
73 | arrayRemoveElement(room.updatableEntities, entity);
74 | }
75 | roomRemoveEntityFromTiles(room, entity);
76 | if (includeCarried) {
77 | [...(entity as MovableEntity).carrying, ...(entity as MovableEntity).carryingPreviously].forEach(
78 | e => roomRemoveEntity(room, e as Entity, 1)
79 | );
80 | }
81 | }
82 |
83 | let roomRemoveEntityFromTiles = (room: Room, entity: Entity) => {
84 | const movableEntity = entity as MovableEntity;
85 | roomIterateBounds(room, movableEntity.boundsWithVelocity || movableEntity.bounds, tile => {
86 | arrayRemoveElement(tile, entity);
87 | });
88 | if (FLAG_CHECK_TILES_VALID) {
89 | const stillThere = room.tiles.find(tiles => tiles.find(entities => entities.find(e => e == entity)));
90 | if (stillThere) {
91 | console.log('removed but still there');
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/ts/game/world.ts:
--------------------------------------------------------------------------------
1 | type World = {
2 | rooms: Room[][];
3 | currentRoom: Vector;
4 | size: Vector;
5 | idFactory: IdFactory;
6 | age: number;
7 | previousFrames?: string[];
8 | player: Player;
9 | instructionSounds: {[_:number]: Sound};
10 | lastSaved?: number;
11 | lastShaken?: number,
12 | shakeSound?: Sound,
13 | };
14 |
15 | const createWorld = (audioContext: AudioContext, w: number, h: number, roomFactory: RoomFactory) => {
16 | // leave some space for persistent ids, start at 99
17 | let nextId = MAX_PERSISTENT_ID_PLUS_1;
18 | const idFactory = () => nextId++;
19 | let player: Player;
20 | let startX: number;
21 | let startY: number;
22 | const rooms = array2DCreate(w, h, (x, y) => {
23 | const room = roomFactory && roomFactory(x, y, idFactory)
24 | let found: Entity | MovableEntity;
25 | const cb = (e: Entity | MovableEntity) => {
26 | if (e) {
27 | const ee = e as EveryEntity;
28 | if (e.entityType == ENTITY_TYPE_PLAYER) {
29 | found = e;
30 | }
31 | objectIterate(ee.holding, cb);
32 | }
33 | };
34 | room && room.updatableEntities.forEach(cb);
35 | if (found) {
36 | player = found as Player;
37 | startX = x;
38 | startY = y;
39 | }
40 | return room;
41 | });
42 |
43 | const instructionSounds: {[_:number]: Sound} = {
44 | //[SOUND_ID_JUMP]: vibratoSoundFactory(audioContext, .5, .1, 1, .2, 'square', 440, 220),
45 | //[SOUND_ID_JUMP]: vibratoSoundFactory(audioContext, .2, 0, .1, .05, 'triangle', 500, 2e3, 599),
46 | //[SOUND_ID_JUMP]: vibratoSoundFactory(audioContext, .3, 0, .1, .05, 'triangle', 400, 700, 900, 'sine', 60),
47 | //[SOUND_ID_THROW]: vibratoSoundFactory(audioContext, .3, 0, .3, .4, 'square', 440, 660, 500),
48 | //[SOUND_ID_THROW]: dtmfSoundFactory(audioContext, 697, 1209, .1),
49 | [INSTRUCTION_ID_JUMP]: vibratoSoundFactory(audioContext, .3, 0, .2, .05, 'triangle', 499, 699, 399, 'sine', 60),
50 | [INSTRUCTION_ID_THROW]: vibratoSoundFactory(audioContext, .2, 0, .2, .05, 'triangle', 499, 2e3, 599),
51 | //[INSTRUCTION_ID_DO_NOTHING]: dtmfSoundFactory(audioContext, 350, 440, INSTRUCTION_DURATION),
52 | [INSTRUCTION_ID_REWIND]: vibratoSoundFactory(audioContext, .2, 0, .1, .05, 'sine', 1440, 2999, 999, 'sawtooth', 199),
53 | [INSTRUCTION_ID_FAST_FORWARD]: vibratoSoundFactory(audioContext, .2, 0, .1, .05, 'sine', 2999, 1440, 2000, 'triangle', 199),
54 | [INSTRUCTION_ID_LEFT]: boomSoundFactory(audioContext, .05, .01, 2e3, .1, .05),
55 | [INSTRUCTION_ID_RIGHT]: boomSoundFactory(audioContext, .05, .01, 2e3, .1, .05),
56 | [INSTRUCTION_ID_EJECT]: vibratoSoundFactory(audioContext, .2, 0, .2, .05, 'triangle', 299, 2e3, 699),
57 | [INSTRUCTION_ID_DROP]: vibratoSoundFactory(audioContext, .2, 0, .2, .05, 'triangle', 199, 2e3, 599),
58 | [INSTRUCTION_ID_PICK_UP]: vibratoSoundFactory(audioContext, .2, 0, .2, .05, 'triangle', 699, 2e3, 599),
59 | [INSTRUCTION_ID_SHOOT]: boomSoundFactory(audioContext, .3, .01, 399, 1, .5),
60 | [INSTRUCTION_ID_STOP]: boomSoundFactory(audioContext, .1, 0, 1e3, .5, .4),
61 | [INSTRUCTION_ID_RECORD]: vibratoSoundFactory(audioContext, .3, 0, .2, .05, 'triangle', 799, 1e3, 499, 'sawtooth', 99),
62 | [INSTRUCTION_ID_ASSPULL]: vibratoSoundFactory(audioContext, .2, 0, .2, .05, 'triangle', 299, 2e3, 599),
63 | };
64 | // for (let instruction = 0; instruction < 10; instruction++) {
65 | // // numeric, use DTMF
66 | // instructionSounds[instruction] = dtmfSoundFactory(
67 | // audioContext,
68 | // DTMF_FREQUENCIES_1[instruction % DTMF_FREQUENCIES_1.length],
69 | // DTMF_FREQUENCIES_2[(instruction / DTMF_FREQUENCIES_1.length | 0) % DTMF_FREQUENCIES_2.length],
70 | // INSTRUCTION_DURATION,
71 | // );
72 | // }
73 | initInstructions(audioContext, instructionSounds);
74 |
75 | const age = parseInt(localStorage.getItem(0 as any) || 0 as any);
76 |
77 | const shakeSound: Sound = FLAG_SHAKE
78 | ? boomSoundFactory(audioContext, .4, .01, 0, 1, .3)
79 | : undefined;
80 |
81 |
82 | const world: World = {
83 | currentRoom: [startX, startY],
84 | size: [w, h],
85 | rooms,
86 | idFactory,
87 | age,
88 | player,
89 | instructionSounds,
90 | //lastShaken: 0,
91 | //shakeSound,
92 | };
93 | return world;
94 | }
95 |
--------------------------------------------------------------------------------
/src/ts/index.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 |
7 | onload = () => {
8 | const audioContext = new AudioContext();
9 | const {
10 | factory,
11 | worldHeight: height,
12 | worldWidth: width,
13 | } = roomFactoryFactory();
14 | let world: World;
15 | const recreateWorld = () => {
16 | world = createWorld(audioContext, width, height, factory);
17 | };
18 | recreateWorld();
19 |
20 | let context: CanvasRenderingContext2D;
21 | let scale: number;
22 | let clientWidth: number;
23 | let clientHeight: number;
24 | const elements = [c, o];
25 | const resize = () => {
26 | const aspectRatio = innerWidth/innerHeight;
27 | const targetWidth = MAX_TILES_ACROSS;
28 | const targetHeight = MAX_TILES_DOWN;
29 | scale = (((aspectRatio < targetWidth/targetHeight
30 | ? innerWidth/targetWidth
31 | : innerHeight/targetHeight)/SCALING_JUMP)|0) * SCALING_JUMP;
32 | // for some reason, fonts don't render when scale is a multiple of 5!?
33 | if (FLAG_CHROME_FONT_HACK && !(scale % 5)) {
34 | scale--;
35 | }
36 | clientWidth = targetWidth * scale;
37 | clientHeight = targetHeight * scale;
38 | c.width = clientWidth;
39 | c.height = clientHeight;
40 | context = c.getContext('2d');
41 | context.textAlign = 'center';
42 | context.font = `${SPEECH_TEXT_HEIGHT}px fantasy`;
43 | context.lineWidth = 1/scale;
44 | if (FLAG_IMAGE_SMOOTHING_DISABLED) {
45 | context.imageSmoothingEnabled = false;
46 | }
47 | elements.forEach(e => e.setAttribute('style', `width:${clientWidth}px;height:${clientHeight}px`));
48 | }
49 | onresize = resize;
50 | resize();
51 |
52 | const activeKeyCodes: {[_:number]: number} = {
53 | // 65: 1,
54 | };
55 |
56 | onkeydown = (e: KeyboardEvent) => {
57 | if (FLAG_AUDIO_CONTEXT_RESUME) {
58 | audioContext.resume();
59 | }
60 | activeKeyCodes[e.keyCode] = activeKeyCodes[e.keyCode]
61 | ? activeKeyCodes[e.keyCode]
62 | : world.age;
63 | };
64 |
65 | onkeyup = (e: KeyboardEvent) => {
66 | activeKeyCodes[e.keyCode] = 0;
67 | };
68 |
69 | let then: number | undefined;
70 | let remainder = 0;
71 | const update = (now?: number) => {
72 | let delta = Math.min((now||0) - (then||0), MAX_DELTA * 3) + remainder;
73 | const inputs = world.player.activeInputs;
74 | inputs.states = {};
75 | for (let keyCode in INPUT_KEY_CODE_MAPPINGS) {
76 | const input = INPUT_KEY_CODE_MAPPINGS[keyCode];
77 | inputs.states[input] = Math.max(inputs.states[input] || 0, activeKeyCodes[keyCode] || 0);
78 | }
79 | for(;;) {
80 | // const d = Math.max(Math.min(MAX_DELTA, delta), MIN_DELTA);
81 | let d = MAX_DELTA;
82 | if(delta < d) {
83 | break;
84 | };
85 | delta -= d;
86 | const render = delta < MAX_DELTA;
87 | context.save();
88 | if (render) {
89 | //context.clearRect(0, 0, clientWidth, clientHeight);
90 | if (FLAG_SHAKE) {
91 | const shake = Math.sqrt(Math.max(0, world.lastShaken - world.age));
92 | context.translate(shake * (Math.random() - .5), shake * (Math.random() - .5));
93 | }
94 | }
95 | context.scale(scale, scale);
96 | updateAndRenderWorld(context, world, d, render);
97 | context.restore();
98 | }
99 | remainder = delta;
100 | if (world.player.deathAge && readInput(world.player, INSTRUCTION_ID_JUMP, world.age)) {
101 | recreateWorld();
102 | }
103 | // game over, help, etc...
104 | renderPlayer(world.player, world);
105 | requestAnimationFrame(update);
106 | then = now;
107 | };
108 | update();
109 | };
110 |
111 | const renderPlayer = (player: Player, world: World) => {
112 | let message: string;
113 | if (player.deathAge) {
114 | message = 'Space to Retry';
115 | } else if (world.lastSaved > world.age - MESSAGE_DISPLAY_TIME){
116 | message = 'Saved';
117 | }
118 | if (message) {
119 | o.innerText = message;
120 | }
121 | // can also accept numeric values
122 | o.style.opacity = (message ? 1: 0) as any;
123 | if (FLAG_HELP) {
124 | if (player.commandsVisible) {
125 | h.style.opacity = '1';
126 | // render out all the commands
127 | h.innerHTML = INSTRUCTIONS.map((instruction, instructionId) => {
128 | if( player.capabilities.indexOf(instructionId) >= 0 && instruction.keyCodes ) {
129 | return `${instructionToKey(instruction)}${instruction.hold?'+hold':''}) ${instructionToName(instructionId)}
`
130 | }
131 | return '';
132 | }).join('');
133 | } else {
134 | h.style.opacity = '0';
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 |
3 | // Project configuration.
4 | grunt.initConfig({
5 | pkg: grunt.file.readJSON('package.json'),
6 | ts: {
7 | default: {
8 | tsconfig: './tsconfig.json'
9 | }
10 | },
11 | watch: {
12 | default: {
13 | files: ["src/ts/**/*", "index.html", "index.css"],
14 | tasks: ['ts:default'],
15 | options: {
16 | livereload: true
17 | }
18 | }
19 | },
20 | connect: {
21 | server: {
22 | options: {
23 | livereload: true
24 | }
25 | }
26 | },
27 | clean: {
28 | all: ["build", "dist", "dist.zip", "js13k.zip"]
29 | },
30 | 'closure-compiler': {
31 | es2015: {
32 | closurePath: 'libbuild/closure-compiler-v20190729',
33 | js: 'build/out.js',
34 | jsOutputFile: 'dist/out.min.js',
35 | maxBuffer: 500,
36 | reportFile: 'closure.txt',
37 | options: {
38 | compilation_level: 'ADVANCED_OPTIMIZATIONS',
39 | language_in: 'ECMASCRIPT_2015',
40 | language_out: 'ECMASCRIPT_2015',
41 | externs: 'src/externs/externs.js'
42 | }
43 | },
44 | es5: {
45 | closurePath: 'libbuild/closure-compiler-v20190729',
46 | js: 'build/out.js',
47 | jsOutputFile: 'dist/out.min.js',
48 | maxBuffer: 500,
49 | reportFile: 'closure.txt',
50 | options: {
51 | compilation_level: 'ADVANCED_OPTIMIZATIONS',
52 | language_in: 'ECMASCRIPT_2015',
53 | language_out: 'ECMASCRIPT5',
54 | externs: 'src/externs/externs.js'
55 | }
56 | }
57 | },
58 | cssmin: {
59 | options: {
60 | },
61 | target: {
62 | files: {
63 | 'dist/index.css': ['dist/index.css']
64 | }
65 | }
66 | },
67 | htmlmin: {
68 | dist: {
69 | options: {
70 | removeComments: true,
71 | collapseWhitespace: true
72 | },
73 | files: {
74 | 'dist/index.html': 'dist/index.html'
75 | }
76 | }
77 | },
78 | inline: {
79 | dist: {
80 | src: 'dist/index.html',
81 | dest: 'dist/index.html'
82 | }
83 | },
84 | replace: {
85 | html: {
86 | src: ['dist/index.html'],
87 | overwrite: true,
88 | replacements: [{
89 | from: /build\/out\.js/g,
90 | to:"out.min.js"
91 | }, { // gut the HTML entirely!
92 | from: "