├── .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: "", 93 | to: "" 94 | }, { 95 | from: "", 96 | to: "" 97 | }, { 98 | from: "", 99 | to: "" 100 | }] 101 | }, 102 | js: { 103 | src: ['dist/out.min.js'], 104 | overwrite: true, 105 | replacements: [{ 106 | from: "'use strict';", 107 | to:"" 108 | }, { 109 | from: "\n", 110 | to: "" 111 | }, { 112 | from: "void 0", 113 | to: "null" 114 | }, { 115 | from: "const ", 116 | to: "let " 117 | }/*, { 118 | from: "var ", 119 | to: "let " 120 | }*//*, { 121 | from: /\[(\d+)\]\:/, 122 | to: "${1}:" 123 | }*/] 124 | }, 125 | }, 126 | copy: { 127 | html: { 128 | files: [ 129 | {expand: true, src: ['index.html'], dest: 'dist/'}, 130 | {expand: true, src: ['index.css'], dest: 'dist/'} 131 | ] 132 | } 133 | }, 134 | devUpdate: { 135 | main: { 136 | options: { 137 | //task options go here 138 | updateType: 'force', 139 | reportUpdated: true 140 | } 141 | } 142 | } 143 | }); 144 | 145 | // clean 146 | grunt.loadNpmTasks('grunt-contrib-clean'); 147 | // load the plugin that provides the closure compiler 148 | grunt.loadNpmTasks('grunt-closure-compiler'); 149 | // Load the plugin that provides the "TS" task. 150 | grunt.loadNpmTasks('grunt-ts'); 151 | // copy 152 | grunt.loadNpmTasks('grunt-contrib-copy'); 153 | // replace text in file 154 | grunt.loadNpmTasks('grunt-text-replace'); 155 | // update version 156 | grunt.loadNpmTasks('grunt-dev-update'); 157 | // inline js 158 | grunt.loadNpmTasks('grunt-inline'); 159 | // live reload 160 | grunt.loadNpmTasks('grunt-contrib-watch'); 161 | // server for live reload 162 | grunt.loadNpmTasks('grunt-contrib-connect'); 163 | // copying html 164 | grunt.loadNpmTasks('grunt-contrib-copy'); 165 | // minifying css 166 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 167 | // minifying html 168 | grunt.loadNpmTasks('grunt-contrib-htmlmin'); 169 | 170 | // Default task(s). 171 | grunt.registerTask('reset', ['clean:all']); 172 | grunt.registerTask('prod', ['ts']); 173 | grunt.registerTask('dist', ['prod', 'closure-compiler:es2015', 'copy','cssmin','replace:html', 'replace:js', 'inline', 'htmlmin']); 174 | grunt.registerTask('default', ['prod', 'connect', 'watch']); 175 | 176 | }; -------------------------------------------------------------------------------- /src/ts/common/synth_speech.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------------------------------------------- 2 | // 3 | // tss.js -- Tiny Speech Synthesizer in JavaScript 4 | // 5 | // Original code: stan_1901 (Andrey Stephanov) 6 | // http://pouet.net/prod.php?which=50530 7 | // 8 | // JavaScript port: losso/code red (Alexander Grupe) 9 | // http://heckmeck.de/demoscene/tiny-speech-synth-js/ 10 | // 11 | // --------------------------------------------------------------------------- 12 | 13 | const Sawtooth = ( x: number ) => { 14 | return ( .5 - ( x - Math.floor ( x / MATH_PI_2 ) * MATH_PI_2 ) / MATH_PI_2 ); 15 | } 16 | 17 | type Phenome = [number, number, number, number, number?, number?, number?, number?, number?, number?]; 18 | 19 | /*let g_phonemes = { 20 | 'o': { f:[12, 15, 0], w:[ 10, 10, 0], len:3, amp: 6, osc:0, plosive:0 }, 21 | 'i': { f:[ 5, 56, 0], w:[ 10, 10, 0], len:3, amp: 3, osc:0, plosive:0 }, 22 | 'j': { f:[ 5, 56, 0], w:[ 10, 10, 0], len:1, amp: 3, osc:0, plosive:0 }, 23 | 'u': { f:[ 5, 14, 0], w:[ 10, 10, 0], len:3, amp: 3, osc:0, plosive:0 }, 24 | 'a': { f:[18, 30, 0], w:[ 10, 10, 0], len:3, amp:15, osc:0, plosive:0 }, 25 | 'e': { f:[14, 50, 0], w:[ 10, 10, 0], len:3, amp:15, osc:0, plosive:0 }, 26 | 'E': { f:[20, 40, 0], w:[ 10, 10, 0], len:3, amp:12, osc:0, plosive:0 }, 27 | 'w': { f:[ 3, 14, 0], w:[ 10, 10, 0], len:3, amp: 1, osc:0, plosive:0 }, 28 | 'v': { f:[ 2, 20, 0], w:[ 20, 10, 0], len:3, amp: 3, osc:0, plosive:0 }, 29 | 'T': { f:[ 2, 20, 0], w:[ 40, 1, 0], len:3, amp: 5, osc:0, plosive:0 }, 30 | 'z': { f:[ 5, 28, 80], w:[ 10, 5, 10], len:3, amp: 3, osc:0, plosive:0 }, 31 | 'Z': { f:[ 4, 30, 60], w:[ 50, 1, 5], len:3, amp: 5, osc:0, plosive:0 }, 32 | 'b': { f:[ 4, 0, 0], w:[ 10, 0, 0], len:1, amp: 2, osc:0, plosive:0 }, 33 | 'd': { f:[ 4, 40, 80], w:[ 10, 10, 10], len:1, amp: 2, osc:0, plosive:0 }, 34 | 'm': { f:[ 4, 20, 0], w:[ 10, 10, 0], len:3, amp: 2, osc:0, plosive:0 }, 35 | 'n': { f:[ 4, 40, 0], w:[ 10, 10, 0], len:3, amp: 2, osc:0, plosive:0 }, 36 | 'r': { f:[ 3, 10, 20], w:[ 30, 8, 1], len:3, amp: 3, osc:0, plosive:0 }, 37 | 'l': { f:[ 8, 20, 0], w:[ 10, 10, 0], len:3, amp: 5, osc:0, plosive:0 }, 38 | 'g': { f:[ 2, 10, 26], w:[ 15, 5, 2], len:2, amp: 1, osc:0, plosive:0 }, 39 | 'f': { f:[ 8, 20, 34], w:[ 10, 10, 10], len:3, amp: 4, osc:1, plosive:0 }, 40 | 'h': { f:[22, 26, 32], w:[ 30, 10, 30], len:1, amp:10, osc:1, plosive:0 }, 41 | 's': { f:[80, 110, 0], w:[ 80, 40, 0], len:3, amp: 5, osc:1, plosive:0 }, 42 | 'S': { f:[20, 30, 0], w:[100, 100, 0], len:3, amp:10, osc:1, plosive:0 }, 43 | 'p': { f:[ 4, 10, 20], w:[ 5, 10, 10], len:1, amp: 2, osc:1, plosive:1 }, 44 | 't': { f:[ 4, 20, 40], w:[ 10, 20, 5], len:1, amp: 3, osc:1, plosive:1 }, 45 | 'k': { f:[20, 80, 0], w:[ 10, 10, 0], len:1, amp: 3, osc:1, plosive:1 } 46 | }; 47 | */ 48 | let g_phonemes: Phenome[]; 49 | if (FLAG_RANDOMIZE_PHENOMES) { 50 | g_phonemes = []; 51 | for (let i=0; i<26; i++) { 52 | const a = Math.random(); 53 | const b = Math.random(); 54 | const c = Math.random(); 55 | const d = Math.random(); 56 | const e = Math.random(); 57 | 58 | g_phonemes[i] = [a * a * 20, b, c, d, e].map(v => v | 0) as Phenome; 59 | } 60 | } else { 61 | g_phonemes = [ 62 | [10, 10, 0, 18, 30], // 'a': 63 | ,//[10, 0, 0, 4], // 'b': 64 | , // c 65 | [10, 10, 10, 4, 40, 80], // 'd': 66 | [10, 10, 0, 14, 50], // 'e': 67 | [10, 10, 10, 8, 20, 34], // 'f': 68 | [15, 5, 2, 2, 10, 26], // 'g': 69 | [30, 10, 30, 22, 26, 32], // 'h': 70 | [10, 10, 0, 5, 56], // 'i': 71 | [10, 10, 0, 5, 56], // 'j': 72 | ,//[10, 10, 0, 20, 80], //'k': 73 | ,//[10, 10, 0, 8, 20], // 'l': 74 | ,//[10, 10, 0, 4, 20], // 'm': 75 | [10, 10, 0, 4, 40], //'n': 76 | [10, 10, 0, 12, 15], // 'o': 77 | [ 5, 10, 10, 4, 10, 20], // 'p': 78 | ,// q 79 | [30, 8, 1, 3, 10, 20], // 'r': 80 | [80, 40, 0, 80, 110], // 's': 81 | [10, 20, 5, 4, 20, 40], // 't': 82 | [10, 10, 0, 5, 14], // 'u': 83 | //[20, 10, 0, 2, 20], // 'v': 84 | //[ 3, 14, 0, 10, 10, 0, 3, 1], // 'w': // unused? 85 | //, // x 86 | //, // y 87 | //[ 5, 28, 80, 10, 5, 10, 3, 3], // 'z': // unused? 88 | ]; 89 | } 90 | 91 | 92 | const OFFSET_F = 3; 93 | // const INDEX_LEN = 6; 94 | // const INDEX_AMP = 7; 95 | // const INDEX_OSC = 8; 96 | // const INDEX_PLOSIVE = 9; 97 | 98 | 99 | // Synthesizes speech and adds it to specified buffer 100 | const SynthSpeech = ( buf: Float32Array, text: string, sampleFrequency: number ) => { 101 | let bufPos = 0; 102 | // Loop through all phonemes 103 | text.split('').forEach((c, textPos) => { 104 | let l = text.charCodeAt(textPos) - 97; // (a is 97) 105 | // Find phoneme description 106 | let p = g_phonemes[l]; 107 | if (!p) { 108 | // space 109 | if (l == 32) { 110 | bufPos += (sampleFrequency * .2) | 0; 111 | } 112 | } else { 113 | //let v = p[INDEX_AMP]; 114 | let v = 3; 115 | // Generate sound 116 | //let sl = p[INDEX_LEN] * (sampleFrequency / 15); 117 | let sl = (2+l/9) * (sampleFrequency / 15); 118 | for ( let f = 0; f < 3; f++ ) { 119 | let ff = p[f + OFFSET_F]; 120 | if ( ff ) { 121 | let freq = ff*(30/sampleFrequency); 122 | let buf1Res = 0, buf2Res = 0; 123 | let q = 1 - p[f] * (MATH_PI_2 * 5 / sampleFrequency); 124 | //let b = buf; <-- store current bufPos? 125 | let thisBufPos = bufPos; 126 | let xp = 0; 127 | for ( let s = 0; s < sl; s++ ) { 128 | // let n = Math.random()-.5; 129 | // let x = n; 130 | // if ( !p[INDEX_OSC] ) { 131 | let x = Sawtooth ( s * (60 * MATH_PI_2 / sampleFrequency) ); 132 | // xp = 0; 133 | // } 134 | // Apply formant filter 135 | x += 2 * Math.cos ( MATH_PI_2 * freq ) * buf1Res * q - buf2Res * q * q; 136 | buf2Res = buf1Res; 137 | buf1Res = x; 138 | x = .75 * xp + x * v; 139 | xp = x; 140 | x = x/128 - 1; 141 | buf[thisBufPos++] = buf[thisBufPos]/2+x; 142 | buf[thisBufPos++] = buf[thisBufPos]/2+x; 143 | } 144 | } 145 | } 146 | // Overlap neighbour phonemes 147 | bufPos += ((3*sl/4)<<1); 148 | // if ( p[INDEX_PLOSIVE] ) 149 | // bufPos += (sl&0xfffffe); 150 | } 151 | }); 152 | } -------------------------------------------------------------------------------- /src/ts/common/graphics.ts: -------------------------------------------------------------------------------- 1 | const ANIMATION_ID_NONE = 0; 2 | const ANIMATION_ID_RESTING = 1; 3 | const ANIMATION_ID_WALKING = 2; 4 | const ANIMATION_ID_JUMPING = 3; 5 | const ANIMATION_ID_GRABBING = 4; 6 | const ANIMATION_ID_INSERTING = 5; 7 | const ANIMATION_ID_PICKING_UP = 6; 8 | const ANIMATION_ID_THROWING = 7; 9 | const ANIMATION_ID_DROPPING = 8; 10 | const ANIMATION_ID_PRESSING_BUTTON = 9; 11 | const ANIMATION_ID_DEATH = 10; 12 | const ANIMATION_ID_CARRYING = 11; 13 | const ANIMATION_ID_SHOOTING = 12; 14 | const ANIMATION_ID_SAVING = 13; 15 | 16 | type HSL = [number, number, number]; 17 | 18 | type RectangleWithPaletteIndex = [number, number, number, number, number, (number | [number, number?, number?, number?])?, number?]; 19 | type TriangleWithPaletteIndex = [number, number, number, number, number, number, number, number]; 20 | 21 | type Image = (RectangleWithPaletteIndex | TriangleWithPaletteIndex)[]; 22 | 23 | type Joint = { 24 | gid?: number, 25 | imageIndex?: number; 26 | transformations?: Transform[]; 27 | renderBefore?: Joint[]; 28 | renderAfter?: Joint[]; 29 | } 30 | 31 | type Pose = {[_:number]: Transform[]}; 32 | 33 | type AnimationSequence = { 34 | poseIds: number[]; 35 | poseDuration: number; 36 | repeatCount?: number; 37 | } 38 | 39 | type Graphic = { 40 | imageryWidth: number; 41 | imageryHeight: number; 42 | imagery: Image[]; 43 | joints: Joint[]; 44 | poses?: Pose[]; 45 | animations?: {[_:number]: AnimationSequence}; 46 | cachedStyles?: {[_: number]: string}; 47 | } 48 | 49 | const TRANSFORM_TYPE_SCALE = 0; 50 | const TRANSFORM_TYPE_TRANSLATE = 1; 51 | const TRANSFORM_TYPE_ROTATE = 2; 52 | const TRANSFORM_TYPE_OPACITY = 3; 53 | type Transform = { 54 | transformType: 0; 55 | scaleX: number; 56 | scaleY: number; 57 | } | { 58 | transformType: 1; 59 | dx: number; 60 | dy: number; 61 | } | { 62 | transformType: 2; 63 | rAngle: number; 64 | aroundX?: number; 65 | aroundY?: number; 66 | } | { 67 | transformType: 3; 68 | opacity: number; 69 | }; 70 | 71 | interface PostJointRenderCallback { 72 | (c: CanvasRenderingContext2D, joint: Joint): void; 73 | } 74 | 75 | type PoseAndProgress = { 76 | poseId: number; 77 | animationProgress: number; 78 | } 79 | 80 | let hslToStyle = (hsl: HSL) => { 81 | const [h, s, l] = hsl; 82 | return `hsl(${h},${s}%,${l}%)` 83 | }; 84 | 85 | let drawGraphic = (c: CanvasRenderingContext2D, g: Graphic, palette: HSL[], callback: PostJointRenderCallback, poses: PoseAndProgress[]) => { 86 | drawGraphicJoints(c, g, palette, g.joints, callback, poses) 87 | }; 88 | 89 | let drawGraphicJoints = (c: CanvasRenderingContext2D, g: Graphic, palette: HSL[], joints: Joint[], callback: PostJointRenderCallback, poses: PoseAndProgress[]) => { 90 | if (joints) { 91 | joints.forEach(j => { 92 | c.save(); 93 | applyGraphicTransformations(c, j.transformations, 1); 94 | poses.forEach(p => applyGraphicTransformations(c, g.poses[p.poseId][j.gid], p.animationProgress)); 95 | drawGraphicJoints(c, g, palette, j.renderBefore, callback, poses); 96 | if (j.imageIndex != null) { 97 | g.imagery[j.imageIndex].forEach(image => { 98 | c.beginPath(); 99 | let fillPaletteIndex: number; 100 | let strokePaletteIndex: number; 101 | if (image.length < 8) { 102 | // rect 103 | let [ix, iy, iw, ih, fpi, r, spi] = image; 104 | let r1: number, r2: number, r3: number, r4: number; 105 | if (r) { 106 | if ((r as number[]).length) { 107 | [r1, r2, r3, r4] = r as number[]; 108 | } else { 109 | r1 = r2 = r3 = r4 = r as number; 110 | } 111 | } 112 | r1 = r1 || 0; 113 | r2 = r2 || 0; 114 | r3 = r3 || 0; 115 | r4 = r4 || 0; 116 | c.moveTo(ix + r1, iy); 117 | c.arcTo(ix + iw, iy, ix + iw, iy + r2, r2); 118 | c.arcTo(ix + iw, iy + ih, ix + iw - r3, iy + ih, r3); 119 | c.arcTo(ix, iy + ih, ix, iy + r4, r4); 120 | c.arcTo(ix, iy, ix + r1, iy, r1); 121 | fillPaletteIndex = fpi; 122 | strokePaletteIndex = spi; 123 | } else { 124 | // tri 125 | let [x1, y1, x2, y2, x3, y3, fpi, spi] = image as number[]; 126 | c.moveTo(x1, y1); 127 | c.lineTo(x2, y2); 128 | c.lineTo(x3, y3); 129 | c.lineTo(x1, y1); 130 | fillPaletteIndex = fpi; 131 | strokePaletteIndex = spi; 132 | } 133 | c.fillStyle = hslToStyle(palette[fillPaletteIndex]); 134 | c.fill(); 135 | if (strokePaletteIndex != null) { 136 | c.strokeStyle = hslToStyle(palette[strokePaletteIndex]); 137 | c.lineWidth = 1; 138 | c.stroke(); 139 | } 140 | }); 141 | } 142 | drawGraphicJoints(c, g, palette, j.renderAfter, callback, poses); 143 | callback(c, j); 144 | c.restore(); 145 | }); 146 | } 147 | }; 148 | 149 | let applyGraphicTransformations = (c: CanvasRenderingContext2D, transforms: Transform[], progress: number) => { 150 | if (transforms) { 151 | transforms.forEach(t => { 152 | switch(t.transformType) { 153 | case 0: 154 | c.scale(1 - (1 - t.scaleX) * progress, 1 - (1 - t.scaleY) * progress); 155 | break; 156 | case 1: 157 | c.translate(t.dx * progress, t.dy * progress); 158 | break; 159 | case 2: 160 | c.translate(t.aroundX || 0, t.aroundY || 0); 161 | c.rotate(t.rAngle * progress); 162 | c.translate(-t.aroundX || 0, -t.aroundY || 0); 163 | break; 164 | case 3: 165 | c.globalAlpha = t.opacity * progress; 166 | break; 167 | } 168 | }); 169 | } 170 | } -------------------------------------------------------------------------------- /src/ts/graphics/mainframe.graphic.ts: -------------------------------------------------------------------------------- 1 | const MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_MEDIUM = 0; 2 | const MAINFRAME_GRAPHIC_PALETTE_INDEX_FACEPLATE = 1; 3 | const MAINFRAME_GRAPHIC_PALETTE_INDEX_TAPE = 2; 4 | 5 | const MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS = 5; 6 | const MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS_INNER = 1; 7 | 8 | const MAINFRAME_GRAPHIC_IMAGE_INDEX_BODY = 0; 9 | const MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER = 1; 10 | const MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER_HOLE = 2; 11 | 12 | const MAINFRAME_GRAPHIC_JOINT_ID_BODY = 0; 13 | const MAINFRAME_GRAPHIC_JOINT_ID_LEFT_SPINNER = 1; 14 | const MAINFRAME_GRAPHIC_JOINT_ID_RIGHT_SPINNER = 2; 15 | 16 | const MAINFRAME_GRAPHIC_POSE_ID_SHAKE_LEFT = 0; 17 | const MAINFRAME_GRAPHIC_POSE_ID_SHAKE_RIGHT = 1; 18 | 19 | const mainframeGraphic: Graphic = { 20 | imageryWidth: 32, 21 | imageryHeight: 48, 22 | imagery: [ 23 | // body 24 | [ 25 | [-16, 0, 32, 48, MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_MEDIUM], 26 | //[-16, 0, 32, 46, MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_LIGHT], 27 | //[-16, 2, 32, 46, MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_DARK], 28 | [-16, 2, 32, 44, MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_MEDIUM], 29 | //[-12, 5, 26, 36, MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_LIGHT], 30 | //[-14, 3, 26, 36, MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_DARK], 31 | [-13, 4, 26, 36, MAINFRAME_GRAPHIC_PALETTE_INDEX_FACEPLATE], 32 | [-5, 12, 10, 10, MAINFRAME_GRAPHIC_PALETTE_INDEX_TAPE], 33 | [-10, 30, 5, 10, MAINFRAME_GRAPHIC_PALETTE_INDEX_TAPE], 34 | [5, 30, 5, 10, MAINFRAME_GRAPHIC_PALETTE_INDEX_TAPE], 35 | [-5, 8, 10, 1, MAINFRAME_GRAPHIC_PALETTE_INDEX_TAPE], 36 | [-16, 22, 32, 8, MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_MEDIUM], 37 | ], 38 | // tape spinners 39 | [ 40 | [-MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS, -MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS, MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS * 2, MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS * 2, MAINFRAME_GRAPHIC_PALETTE_INDEX_BODY_MEDIUM, MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS], 41 | [-MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS_INNER, -MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS_INNER, MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS_INNER * 2, MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS_INNER * 2, MAINFRAME_GRAPHIC_PALETTE_INDEX_TAPE, MAINFRAME_GRAPHIC_TAPE_SPINNER_RADIUS_INNER], 42 | ], 43 | // tape spinner hole 44 | [ 45 | [2, -.5, 2, 1, MAINFRAME_GRAPHIC_PALETTE_INDEX_TAPE, .5], 46 | ], 47 | ], 48 | joints: [{ 49 | gid: MAINFRAME_GRAPHIC_JOINT_ID_BODY, 50 | imageIndex: BLOCK_GRAPHIC_IMAGE_INDEX_BODY, 51 | renderAfter: [{ 52 | gid: MAINFRAME_GRAPHIC_JOINT_ID_LEFT_SPINNER, 53 | imageIndex: MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER, 54 | transformations: [{ 55 | transformType: TRANSFORM_TYPE_TRANSLATE, 56 | dx: -6, 57 | dy: 13, 58 | }, { 59 | transformType: TRANSFORM_TYPE_ROTATE, 60 | rAngle: LOW_P_MATH_PI/6, 61 | }], 62 | renderAfter: [{ 63 | imageIndex: MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER_HOLE, 64 | }, { 65 | imageIndex: MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER_HOLE, 66 | transformations: [{ 67 | transformType: TRANSFORM_TYPE_ROTATE, 68 | rAngle: LOW_P_MATH_PI_2/3 69 | }] 70 | }, { 71 | imageIndex: MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER_HOLE, 72 | transformations: [{ 73 | transformType: TRANSFORM_TYPE_ROTATE, 74 | rAngle: LOW_P_MATH_PI_ON_3*4 75 | }] 76 | }] 77 | },{ 78 | gid: MAINFRAME_GRAPHIC_JOINT_ID_RIGHT_SPINNER, 79 | imageIndex: MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER, 80 | transformations: [{ 81 | transformType: TRANSFORM_TYPE_TRANSLATE, 82 | dx: 6, 83 | dy: 13, 84 | }, { 85 | transformType: TRANSFORM_TYPE_ROTATE, 86 | rAngle: LOW_P_MATH_PI/6, 87 | }], 88 | renderAfter: [{ 89 | imageIndex: MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER_HOLE, 90 | }, { 91 | imageIndex: MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER_HOLE, 92 | transformations: [{ 93 | transformType: TRANSFORM_TYPE_ROTATE, 94 | rAngle: LOW_P_MATH_PI_2/3 95 | }] 96 | }, { 97 | imageIndex: MAINFRAME_GRAPHIC_IMAGE_INDEX_TAPE_SPINNER_HOLE, 98 | transformations: [{ 99 | transformType: TRANSFORM_TYPE_ROTATE, 100 | rAngle: LOW_P_MATH_PI*4/3 101 | }] 102 | }] 103 | }] 104 | }], 105 | poses: [{ // MAINFRAME_GRAPHIC_POSE_ID_SHAKE_LEFT 106 | [MAINFRAME_GRAPHIC_JOINT_ID_BODY]: [{ 107 | transformType: TRANSFORM_TYPE_ROTATE, 108 | rAngle: -LOW_P_MATH_PI/30, 109 | aroundX: 16, 110 | aroundY: 48, 111 | }, { 112 | transformType: TRANSFORM_TYPE_TRANSLATE, 113 | dx: 0, 114 | dy: -3, 115 | }], 116 | [MAINFRAME_GRAPHIC_JOINT_ID_LEFT_SPINNER]: [{ 117 | transformType: TRANSFORM_TYPE_ROTATE, 118 | rAngle: LOW_P_MATH_PI_2, 119 | }], 120 | [MAINFRAME_GRAPHIC_JOINT_ID_RIGHT_SPINNER]: [{ 121 | transformType: TRANSFORM_TYPE_ROTATE, 122 | rAngle: LOW_P_MATH_PI_2, 123 | }], 124 | }, { // MAINFRAME_GRAPHIC_POSE_ID_SHAKE_RIGHT 125 | [MAINFRAME_GRAPHIC_JOINT_ID_BODY]: [{ 126 | transformType: TRANSFORM_TYPE_ROTATE, 127 | rAngle: LOW_P_MATH_PI/30, 128 | aroundX: 0, 129 | aroundY: 48, 130 | }], 131 | [MAINFRAME_GRAPHIC_JOINT_ID_LEFT_SPINNER]: [{ 132 | transformType: TRANSFORM_TYPE_ROTATE, 133 | rAngle: 9, 134 | }], 135 | [MAINFRAME_GRAPHIC_JOINT_ID_RIGHT_SPINNER]: [{ 136 | transformType: TRANSFORM_TYPE_ROTATE, 137 | rAngle: 9, 138 | }], 139 | }], 140 | animations: { 141 | [ANIMATION_ID_SAVING]: { 142 | poseDuration: 150, 143 | poseIds: [MAINFRAME_GRAPHIC_POSE_ID_SHAKE_LEFT, MAINFRAME_GRAPHIC_POSE_ID_SHAKE_RIGHT, MAINFRAME_GRAPHIC_POSE_ID_SHAKE_LEFT, MAINFRAME_GRAPHIC_POSE_ID_SHAKE_RIGHT], 144 | } 145 | }, 146 | }; 147 | -------------------------------------------------------------------------------- /src/ts/graphics/pressureplate.graphic.ts: -------------------------------------------------------------------------------- 1 | const PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_LIGHT = 0; 2 | const PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_MEDIUM = 1; 3 | const PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_DARK = 2; 4 | const PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_TAPE_DECK = 3; 5 | const PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_EYES = 4; 6 | 7 | const PRESSURE_PLATE_GRAPHIC_IMAGE_INDEX_BODY = 0; 8 | const PRESSURE_PLATE_GRAPHIC_IMAGE_INDEX_PLATE = 1; 9 | const PRESSURE_PLATE_GRAPHIC_IMAGE_INDEX_TAPE_DECK = 2; 10 | 11 | const PRESSURE_PLATE_GRAPHIC_JOINT_ID_BODY = 0; 12 | const PRESSURE_PLATE_GRAPHIC_JOINT_ID_PLATE = 1; 13 | const PRESSURE_PLATE_GRAPHIC_JOINT_ID_TAPE = 3; 14 | 15 | const PRESSURE_PLATE_ROUNDING = 4; 16 | const PRESSURE_PLATE_PLATE_HEIGHT = 6; 17 | const PRESSURE_PLATE_PLATE_GAP = 2; 18 | const PRESSURE_PLATE_INSET = 2; 19 | const PRESSURE_PLATE_STRUT_WIDTH = 4; 20 | 21 | const PRESSURE_PLATE_POSE_ID_TRIGGER = 0; 22 | 23 | const pressurePlateGraphicFactory = (width: number, height: number, edge: Edge) => { 24 | const rAngle = (edge - 1) * MED_P_MATH_PI_ON_2; 25 | const imageryWidth = width; 26 | const imageryHeight = height; 27 | if (!(edge % 2)) { 28 | width = height; 29 | height = imageryWidth; 30 | } 31 | const ly = (edge/2) | 0; 32 | const lx = (edge + ly + 1) % 2; 33 | // switch (edge) { 34 | // case EDGE_TOP: //1, 0, sin 0, cos 1 35 | // lx = 0; 36 | // ly = 0; 37 | // break; 38 | // case EDGE_BOTTOM: //3, 180, sin 0, cos -1 39 | // lx = 1; 40 | // ly = 1; 41 | // break; 42 | // case EDGE_RIGHT: // 2, 90, sin 1, cos 0 43 | // lx = 0; 44 | // ly = 1; 45 | // break; 46 | // case EDGE_LEFT: // 0, 270, sin -1, cos 0 47 | // lx = 1; 48 | // ly = 0; 49 | // break; 50 | // } 51 | const blockRounding: [number, number, number, number] = [0, 0, PRESSURE_PLATE_ROUNDING, PRESSURE_PLATE_ROUNDING]; 52 | const blockHeight = height - PRESSURE_PLATE_PLATE_HEIGHT; 53 | const plateRounding: [number, number] = [PRESSURE_PLATE_ROUNDING, PRESSURE_PLATE_ROUNDING]; 54 | const struts: RectangleWithPaletteIndex[] = []; 55 | let strutCount = width / 16; 56 | const strutInterval = (width - (PRESSURE_PLATE_STRUT_WIDTH * strutCount))/(strutCount+1); 57 | let x = 0; 58 | while (strutCount--) { 59 | x += strutInterval; 60 | struts.push([x, -PRESSURE_PLATE_PLATE_GAP, PRESSURE_PLATE_STRUT_WIDTH, PRESSURE_PLATE_PLATE_GAP, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_DARK]); 61 | x += PRESSURE_PLATE_STRUT_WIDTH; 62 | } 63 | 64 | 65 | const graphic: Graphic = { 66 | imageryWidth, 67 | imageryHeight, 68 | imagery: [ 69 | // block 70 | [ 71 | ...struts, 72 | [0, 0, width, blockHeight, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_MEDIUM, blockRounding], 73 | [PRESSURE_PLATE_INSET * lx, PRESSURE_PLATE_INSET * ly, width - PRESSURE_PLATE_INSET, blockHeight - PRESSURE_PLATE_INSET, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_LIGHT, blockRounding], 74 | [PRESSURE_PLATE_INSET * ((lx+1)%2), PRESSURE_PLATE_INSET * ((ly+1)%2), width - PRESSURE_PLATE_INSET, blockHeight - PRESSURE_PLATE_INSET, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_DARK, blockRounding], 75 | [PRESSURE_PLATE_INSET, 1, width - PRESSURE_PLATE_INSET*2, blockHeight - PRESSURE_PLATE_INSET - 1, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_MEDIUM, blockRounding], 76 | ], 77 | // plate 78 | [ 79 | [0, 0, width, PRESSURE_PLATE_PLATE_HEIGHT, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_MEDIUM, plateRounding], 80 | [PRESSURE_PLATE_INSET * lx, PRESSURE_PLATE_INSET * ly, width - PRESSURE_PLATE_INSET, PRESSURE_PLATE_PLATE_HEIGHT - PRESSURE_PLATE_INSET, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_LIGHT, plateRounding], 81 | [PRESSURE_PLATE_INSET * ((lx+1)%2), PRESSURE_PLATE_INSET * ((ly+1)%2), width - PRESSURE_PLATE_INSET, PRESSURE_PLATE_PLATE_HEIGHT - PRESSURE_PLATE_INSET, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_DARK, plateRounding], 82 | [PRESSURE_PLATE_INSET, PRESSURE_PLATE_INSET, width - PRESSURE_PLATE_INSET*2, PRESSURE_PLATE_PLATE_HEIGHT - PRESSURE_PLATE_INSET - 1, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_MEDIUM, plateRounding], 83 | ], 84 | // tape deck 85 | [ 86 | [-7, 0, 14, 9, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_TAPE_DECK], 87 | [-3, 4, 1, 1, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_EYES], 88 | [3, 4, 1, 1, PRESSURE_PLATE_GRAPHIC_PALETTE_INDEX_EYES], 89 | ] 90 | ], 91 | joints: [{ 92 | gid: PRESSURE_PLATE_GRAPHIC_JOINT_ID_BODY, 93 | imageIndex: PRESSURE_PLATE_GRAPHIC_IMAGE_INDEX_BODY, 94 | transformations: [{ 95 | transformType: TRANSFORM_TYPE_ROTATE, 96 | rAngle, 97 | aroundX: 0, 98 | aroundY: height/2, 99 | }, { 100 | transformType: TRANSFORM_TYPE_TRANSLATE, 101 | dx: -width/2, 102 | dy: PRESSURE_PLATE_PLATE_HEIGHT, 103 | }], 104 | }, { 105 | gid: PRESSURE_PLATE_GRAPHIC_JOINT_ID_PLATE, 106 | imageIndex: PRESSURE_PLATE_GRAPHIC_IMAGE_INDEX_PLATE, 107 | transformations: [{ 108 | transformType: TRANSFORM_TYPE_ROTATE, 109 | rAngle, 110 | aroundX: 0, 111 | aroundY: height/2, 112 | }, { 113 | transformType: TRANSFORM_TYPE_TRANSLATE, 114 | dx: -width/2, 115 | dy: -PRESSURE_PLATE_PLATE_GAP, 116 | }], 117 | }, { 118 | gid: PRESSURE_PLATE_GRAPHIC_JOINT_ID_TAPE, 119 | imageIndex: PRESSURE_PLATE_GRAPHIC_IMAGE_INDEX_TAPE_DECK, 120 | transformations: [{ 121 | transformType: TRANSFORM_TYPE_TRANSLATE, 122 | dx: 16 - width/2, 123 | dy: PRESSURE_PLATE_PLATE_HEIGHT + 4, 124 | }], 125 | }], 126 | poses: [ 127 | // trigger pressure plate 128 | { 129 | [PRESSURE_PLATE_GRAPHIC_JOINT_ID_PLATE]: [{// body 130 | transformType: TRANSFORM_TYPE_TRANSLATE, 131 | dx: 0, 132 | dy: PRESSURE_PLATE_PLATE_GAP, 133 | }], 134 | } 135 | ], 136 | animations: { 137 | [ANIMATION_ID_PRESSING_BUTTON]: { 138 | poseDuration: 99, 139 | poseIds: [PRESSURE_PLATE_POSE_ID_TRIGGER], 140 | repeatCount: 1, 141 | } 142 | } 143 | }; 144 | return graphic; 145 | } -------------------------------------------------------------------------------- /src/ts/graphics/robot.graphic.ts: -------------------------------------------------------------------------------- 1 | const ROBOT_GRAPHIC_PALETTE_INDEX_BODY = 0; 2 | const ROBOT_GRAPHIC_PALETTE_INDEX_HEAD = 1; 3 | const ROBOT_GRAPHIC_PALETTE_INDEX_TAPE_DECK = 2; 4 | const ROBOT_GRAPHIC_PALETTE_INDEX_EYE = 3; 5 | 6 | const ROBOT_GRAPHIC_IMAGE_INDEX_BODY = 0; 7 | const ROBOT_GRAPHIC_IMAGE_INDEX_HEAD = 1; 8 | const ROBOT_GRAPHIC_IMAGE_INDEX_EYE = 2; 9 | const ROBOT_GRAPHIC_IMAGE_INDEX_HAND = 3; 10 | const ROBOT_GRAPHIC_IMAGE_INDEX_LEG = 4; 11 | 12 | const ROBOT_GRAPHIC_JOINT_ID_EVERYTHING = 0; 13 | const ROBOT_GRAPHIC_JOINT_ID_BODY = 1; 14 | const ROBOT_GRAPHIC_JOINT_ID_HEAD = 2; 15 | const ROBOT_GRAPHIC_JOINT_ID_LEFT_LEG = 3; 16 | const ROBOT_GRAPHIC_JOINT_ID_RIGHT_LEG = 4; 17 | const ROBOT_GRAPHIC_JOINT_ID_LEFT_ARM = 5; 18 | const ROBOT_GRAPHIC_JOINT_ID_RIGHT_ARM = 6; 19 | const ROBOT_GRAPHIC_JOINT_ID_TAPE_DECK = 7; 20 | const ROBOT_GRAPHIC_JOINT_ID_RIGHT_HAND = 8; 21 | 22 | const ROBOT_GRAPHIC_POSE_ID_STEP_LEFT = 0; 23 | const ROBOT_GRAPHIC_POSE_ID_STEP_RIGHT = 1; 24 | const ROBOT_GRAPHIC_POSE_ID_SHOOT = 2; 25 | 26 | const robotGraphic: Graphic = { 27 | imageryWidth: 32, 28 | imageryHeight: 32, 29 | imagery: [ 30 | [ // body 31 | [-12, -28, 32, 16, ROBOT_GRAPHIC_PALETTE_INDEX_BODY], 32 | [-12, -28, 16, 28, ROBOT_GRAPHIC_PALETTE_INDEX_BODY] 33 | ], 34 | [ // head 35 | [-9, -10, 18, 12, ROBOT_GRAPHIC_PALETTE_INDEX_HEAD], // frame 36 | [-7, -8, 14, 8, ROBOT_GRAPHIC_PALETTE_INDEX_TAPE_DECK], // tape deck 37 | ], 38 | [ // eye 39 | [-1, -.5, 2, 1, ROBOT_GRAPHIC_PALETTE_INDEX_EYE], 40 | [-.5, -1, 1, 2, ROBOT_GRAPHIC_PALETTE_INDEX_EYE], 41 | ], 42 | [ // arm 43 | [-2, -3, 4, 6, ROBOT_GRAPHIC_PALETTE_INDEX_TAPE_DECK], 44 | ], 45 | [ // leg 46 | [-2, 0, 4, 6, ROBOT_GRAPHIC_PALETTE_INDEX_TAPE_DECK], 47 | ], 48 | ], 49 | joints: [{ 50 | //id: ROBOT_GRAPHIC_JOINT_ID_EVERYTHING, 51 | transformations: [{ 52 | transformType: TRANSFORM_TYPE_TRANSLATE, 53 | dx: -10, 54 | dy: 0, 55 | }], 56 | renderAfter: [{ 57 | gid: ROBOT_GRAPHIC_JOINT_ID_LEFT_LEG, 58 | imageIndex: ROBOT_GRAPHIC_IMAGE_INDEX_LEG, 59 | transformations: [{ 60 | transformType: TRANSFORM_TYPE_TRANSLATE, 61 | dx: -4, 62 | dy: 26, 63 | }] 64 | }, { 65 | gid: ROBOT_GRAPHIC_JOINT_ID_RIGHT_LEG, 66 | imageIndex: ROBOT_GRAPHIC_IMAGE_INDEX_LEG, 67 | transformations: [{ 68 | transformType: TRANSFORM_TYPE_TRANSLATE, 69 | dx: 4, 70 | dy: 26, 71 | }] 72 | }, { 73 | gid: ROBOT_GRAPHIC_JOINT_ID_BODY, 74 | imageIndex: ROBOT_GRAPHIC_IMAGE_INDEX_BODY, 75 | transformations: [{ 76 | transformType: TRANSFORM_TYPE_TRANSLATE, 77 | dx: 4, 78 | dy: 28, 79 | }], 80 | renderAfter: [{ 81 | gid: ROBOT_GRAPHIC_JOINT_ID_LEFT_ARM, 82 | imageIndex: ROBOT_GRAPHIC_IMAGE_INDEX_HAND, 83 | transformations: [{ 84 | transformType: TRANSFORM_TYPE_TRANSLATE, 85 | dx: 18, 86 | dy: -10, 87 | }] 88 | }, { 89 | //id: ROBOT_GRAPHIC_JOINT_ID_RIGHT_ARM, 90 | imageIndex: ROBOT_GRAPHIC_IMAGE_INDEX_HAND, 91 | transformations: [{ 92 | transformType: TRANSFORM_TYPE_TRANSLATE, 93 | dx: 8, 94 | dy: -10, 95 | }], 96 | /*renderAfter: [{ 97 | id: ROBOT_GRAPHIC_JOINT_ID_RIGHT_HAND, 98 | transformations: [{ 99 | transformType: TRANSFORM_TYPE_TRANSLATE, 100 | dx: 0, 101 | dy: 5, 102 | }] 103 | }]*/ 104 | }, { 105 | gid: ROBOT_GRAPHIC_JOINT_ID_HEAD, 106 | imageIndex: ROBOT_GRAPHIC_IMAGE_INDEX_HEAD, 107 | transformations: [{ 108 | transformType: TRANSFORM_TYPE_TRANSLATE, 109 | dx: 15, 110 | dy: -12, 111 | }], 112 | renderAfter: [{ 113 | imageIndex: ROBOT_GRAPHIC_IMAGE_INDEX_EYE, 114 | transformations: [{ 115 | transformType: TRANSFORM_TYPE_TRANSLATE, 116 | dx: 3, 117 | dy: -4, 118 | }] 119 | }, { 120 | imageIndex: ROBOT_GRAPHIC_IMAGE_INDEX_EYE, 121 | transformations: [{ 122 | transformType: TRANSFORM_TYPE_TRANSLATE, 123 | dx: -3, 124 | dy: -4, 125 | }], 126 | }/*, { unused 127 | id: ROBOT_GRAPHIC_JOINT_ID_TAPE_DECK, 128 | transformations: [{ 129 | transformType: TRANSFORM_TYPE_TRANSLATE, 130 | dx: 0, 131 | dy: -9, 132 | }] 133 | }*/], 134 | }], 135 | }], 136 | }], 137 | poses: [{ // ROBOT_GRAPHIC_POSE_ID_STEP_LEFT 138 | [ROBOT_GRAPHIC_JOINT_ID_BODY]: [{ 139 | transformType: TRANSFORM_TYPE_ROTATE, 140 | rAngle: -LOW_P_MATH_PI/20, 141 | }], 142 | [ROBOT_GRAPHIC_JOINT_ID_LEFT_LEG]: [{ 143 | transformType: TRANSFORM_TYPE_TRANSLATE, 144 | dx: 0, 145 | dy: -3, 146 | }], 147 | [ROBOT_GRAPHIC_JOINT_ID_HEAD]: [{ 148 | transformType: TRANSFORM_TYPE_ROTATE, 149 | rAngle: LOW_P_MATH_PI/20, 150 | }], 151 | }, { // ROBOT_GRAPHIC_POSE_ID_STEP_RIGHT 152 | [ROBOT_GRAPHIC_JOINT_ID_BODY]: [{ 153 | transformType: TRANSFORM_TYPE_ROTATE, 154 | rAngle: LOW_P_MATH_PI/20, 155 | }], 156 | [ROBOT_GRAPHIC_JOINT_ID_RIGHT_LEG]: [{ 157 | transformType: TRANSFORM_TYPE_TRANSLATE, 158 | dx: 0, 159 | dy: -3, 160 | }], 161 | [ROBOT_GRAPHIC_JOINT_ID_HEAD]: [{ 162 | transformType: TRANSFORM_TYPE_ROTATE, 163 | rAngle: -LOW_P_MATH_PI/20, 164 | }], 165 | }, { // ROBOT_GRAPHIC_POSE_ID_SHOOT 166 | [ROBOT_GRAPHIC_JOINT_ID_BODY]: [{ 167 | transformType: TRANSFORM_TYPE_ROTATE, 168 | rAngle: -LOW_P_MATH_PI/8, 169 | }], 170 | }], 171 | animations: { 172 | [ANIMATION_ID_WALKING]: { 173 | poseDuration: 299, 174 | poseIds: [ROBOT_GRAPHIC_POSE_ID_STEP_LEFT, ROBOT_GRAPHIC_POSE_ID_STEP_RIGHT], 175 | }, 176 | [ANIMATION_ID_SHOOTING]: { 177 | poseDuration: BULLET_INTERVAL, 178 | poseIds: [ROBOT_GRAPHIC_POSE_ID_SHOOT], 179 | }, 180 | }, 181 | }; -------------------------------------------------------------------------------- /src/ts/common/sounds.ts: -------------------------------------------------------------------------------- 1 | interface Sound { 2 | (): void; 3 | } 4 | 5 | const dtmfSoundFactory = ( 6 | audioContext: AudioContext, 7 | frequency1: number, 8 | frequency2: number, 9 | durationSeconds: number, 10 | ) => { 11 | return () => { 12 | const now = audioContext.currentTime; 13 | const end = now + durationSeconds; 14 | const osc1 = audioContext.createOscillator(); 15 | const osc2 = audioContext.createOscillator(); 16 | osc1.frequency.value = frequency1; 17 | osc2.frequency.value = frequency2; 18 | 19 | const gain = audioContext.createGain(); 20 | gain.gain.value = .1; 21 | 22 | const filter = audioContext.createBiquadFilter(); 23 | filter.type = 'lowpass'; 24 | filter.frequency.value = 8000; 25 | 26 | osc1.connect(gain); 27 | osc2.connect(gain); 28 | 29 | gain.connect(filter); 30 | filter.connect(audioContext.destination); 31 | osc1.start(); 32 | osc2.start(); 33 | osc1.stop(end); 34 | osc2.stop(end); 35 | osc1.onended = () => { 36 | 37 | if( !FLAG_MINIMAL_AUDIO_CLEANUP ) { 38 | [osc1, osc2, gain, filter].map(audioDisconnectSingleNode); 39 | } else { 40 | [gain].map(audioDisconnectSingleNode); 41 | } 42 | } 43 | }; 44 | } 45 | 46 | let vibratoSoundFactory = ( 47 | audioContext: AudioContext, 48 | durationSeconds: number, 49 | attackSeconds: number, 50 | attackVolume: number, 51 | sustainVolume: number, 52 | oscillatorType: OscillatorType, 53 | oscillatorStartFrequency: number, 54 | oscillatorEndFrequency: number, 55 | filterFrequency?: number, 56 | vibratoType?: OscillatorType, 57 | vibratoFrequency?: number 58 | ) => { 59 | return () => { 60 | let now = audioContext.currentTime; 61 | let oscillator = audioContext.createOscillator(); 62 | oscillator.frequency.setValueAtTime(oscillatorStartFrequency, now); 63 | oscillator.frequency.linearRampToValueAtTime(oscillatorEndFrequency, now + durationSeconds); 64 | oscillator.type = oscillatorType; 65 | 66 | let gain = audioContext.createGain(); 67 | let decay = durationSeconds * .2; 68 | linearRampGain(gain, now, attackVolume, sustainVolume, attackSeconds, decay, null, durationSeconds); 69 | 70 | let vibrato: OscillatorNode; 71 | let vibratoGain: GainNode; 72 | let filter: BiquadFilterNode; 73 | if( vibratoType ) { 74 | vibrato = audioContext.createOscillator(); 75 | vibrato.frequency.value = vibratoFrequency; 76 | vibrato.type = vibratoType; 77 | 78 | vibratoGain = audioContext.createGain(); 79 | vibratoGain.gain.value = -999; 80 | 81 | vibrato.connect(vibratoGain); 82 | vibratoGain.connect(oscillator.detune); 83 | 84 | vibrato.start(); 85 | vibrato.stop(now + durationSeconds); 86 | } 87 | 88 | if( filterFrequency ) { 89 | filter = audioContext.createBiquadFilter(); 90 | filter.type = filterFrequency < oscillatorStartFrequency 91 | ? 'highpass' 92 | : 'lowpass'; 93 | filter.Q.value = 0; 94 | filter.frequency.value = filterFrequency; 95 | oscillator.connect(filter); 96 | filter.connect(gain); 97 | } else { 98 | oscillator.connect(gain); 99 | } 100 | 101 | 102 | 103 | //gain.connect(vibratoGain); 104 | gain.connect(audioContext.destination); 105 | 106 | oscillator.start(); 107 | oscillator.stop(now + durationSeconds); 108 | oscillator.onended = () => { 109 | if( !FLAG_MINIMAL_AUDIO_CLEANUP ) { 110 | [oscillator, gain, vibrato, vibratoGain, filter].map(audioDisconnectSingleNode); 111 | } else { 112 | [gain].map(audioDisconnectSingleNode); 113 | } 114 | } 115 | } 116 | }; 117 | 118 | const audioDisconnectSingleNode = (node: AudioNode) => { 119 | if( node ) { 120 | node.disconnect(); 121 | } 122 | } 123 | 124 | const linearRampGain = (gain: GainNode, now: number, attackVolume: number, sustainVolume, attackSeconds: number, decaySeconds: number, sustainSeconds:number, durationSeconds: number) => { 125 | gain.gain.value = 0; 126 | gain.gain.setValueAtTime(0, now); 127 | gain.gain.linearRampToValueAtTime(attackVolume, now + attackSeconds); 128 | gain.gain.linearRampToValueAtTime(sustainVolume, now + decaySeconds); 129 | if (sustainSeconds) { 130 | gain.gain.linearRampToValueAtTime(sustainVolume, now + sustainSeconds); 131 | } 132 | gain.gain.linearRampToValueAtTime(0, now + durationSeconds); 133 | } 134 | 135 | const boomSoundFactory =( 136 | audioContext: AudioContext, 137 | durationSeconds: number, 138 | attackSeconds: number, 139 | filterFrequency: number, 140 | attackVolume: number, 141 | sustainVolume: number 142 | ): Sound => { 143 | let sampleRate = audioContext.sampleRate; 144 | let frameCount = durationSeconds * sampleRate | 0; 145 | let buffer = audioContext.createBuffer(1, frameCount, sampleRate); 146 | let data = buffer.getChannelData(0); 147 | 148 | while (frameCount--) { 149 | data[frameCount] = Math.random() * 2 - 1; 150 | } 151 | 152 | return () => { 153 | 154 | let staticNode = audioContext.createBufferSource(); 155 | staticNode.buffer = buffer; 156 | staticNode.loop = true; 157 | 158 | let filter = audioContext.createBiquadFilter(); 159 | filter.type = 'lowpass'; 160 | filter.Q.value = 0; 161 | filter.frequency.value = filterFrequency; 162 | 163 | //decay 164 | let gain = audioContext.createGain(); 165 | let decay = durationSeconds * .2; 166 | linearRampGain(gain, audioContext.currentTime, attackVolume, sustainVolume, attackSeconds, decay, null, durationSeconds); 167 | 168 | staticNode.connect(filter); 169 | filter.connect(gain); 170 | gain.connect(audioContext.destination); 171 | 172 | staticNode.start(); 173 | staticNode.stop(audioContext.currentTime + durationSeconds); 174 | staticNode.onended = () => { 175 | if( FLAG_MINIMAL_AUDIO_CLEANUP ) { 176 | audioDisconnectSingleNode(gain); 177 | } else { 178 | [gain, staticNode, filter].map(audioDisconnectSingleNode); 179 | } 180 | } 181 | } 182 | } 183 | 184 | const SECONDS_PER_LETTER = .2; 185 | const synthesizeSpeech = ( 186 | audioContext: AudioContext, 187 | word: string, 188 | baseVolume: number, 189 | ) => { 190 | // create the buffer 191 | let durationSeconds = word.length * SECONDS_PER_LETTER; 192 | let sampleRate = audioContext.sampleRate; 193 | let frameCount = durationSeconds * sampleRate | 0; 194 | let buffer = audioContext.createBuffer(1, frameCount, sampleRate * 2); 195 | let data = buffer.getChannelData(0); 196 | 197 | SynthSpeech(data, word, sampleRate); 198 | 199 | return () => { 200 | let staticNode = audioContext.createBufferSource(); 201 | staticNode.buffer = buffer; 202 | 203 | let gain = audioContext.createGain(); 204 | gain.gain.value = baseVolume; 205 | 206 | staticNode.connect(gain); 207 | gain.connect(audioContext.destination); 208 | 209 | staticNode.start(); 210 | staticNode.stop(audioContext.currentTime + durationSeconds); 211 | staticNode.onended = () => { 212 | if( FLAG_MINIMAL_AUDIO_CLEANUP ) { 213 | audioDisconnectSingleNode(gain); 214 | } else { 215 | [gain, staticNode].map(audioDisconnectSingleNode); 216 | } 217 | } 218 | }; 219 | } -------------------------------------------------------------------------------- /src/ts/common/inputs.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | const INSTRUCTION_ID_COUNT_0 = 0 5 | const INSTRUCTION_ID_COUNT_1 = 1; 6 | const INSTRUCTION_ID_COUNT_2 = 2; 7 | const INSTRUCTION_ID_COUNT_3 = 3; 8 | const INSTRUCTION_ID_COUNT_4 = 4; 9 | const INSTRUCTION_ID_COUNT_5 = 5; 10 | const INSTRUCTION_ID_COUNT_6 = 6; 11 | const INSTRUCTION_ID_COUNT_7 = 7; 12 | const INSTRUCTION_ID_COUNT_8 = 8; 13 | const INSTRUCTION_ID_COUNT_9 = 9; 14 | const INSTRUCTION_ID_DO_NOTHING = 10 15 | const INSTRUCTION_ID_UP = 11; 16 | const INSTRUCTION_ID_DOWN = 12; 17 | const INSTRUCTION_ID_LEFT = 13; 18 | const INSTRUCTION_ID_RIGHT = 14; 19 | const INSTRUCTION_ID_JUMP = 15; 20 | const INSTRUCTION_ID_REWIND = 16; 21 | const INSTRUCTION_ID_FAST_FORWARD = 17; 22 | const INSTRUCTION_ID_STOP = 18; 23 | const INSTRUCTION_ID_PICK_UP = 19; 24 | const INSTRUCTION_ID_DROP = 20; 25 | const INSTRUCTION_ID_THROW = 21; 26 | const INSTRUCTION_ID_INSERT = 22; 27 | const INSTRUCTION_ID_EJECT = 23; 28 | const INSTRUCTION_ID_PLAY = 24; 29 | const INSTRUCTION_ID_RECORD = 25; 30 | const INSTRUCTION_ID_SAVE = 26; 31 | const INSTRUCTION_ID_SHOOT = 27; 32 | const INSTRUCTION_ID_WAIT = 28; 33 | const INSTRUCTION_ID_ASSPULL = 29; 34 | const TOTAL_INSTRUCTION_COUNT = 29; 35 | 36 | type Instruction = { 37 | keyCodes?: number[], 38 | keyChar?: string, 39 | readableName?: string, 40 | phoneticName?: string, 41 | animationId?: number, 42 | hold?: number | boolean, 43 | remember?: number | boolean, 44 | spoken?: Sound, 45 | automatedDuration?: number, 46 | }; 47 | 48 | const INSTRUCTIONS: Instruction[] = [{ 49 | // count 0 50 | keyCodes: [48], // 0 51 | phoneticName: 'o', 52 | }, { 53 | // count 1 54 | keyCodes: [49], // 1 55 | phoneticName: 'un', 56 | }, { 57 | // count 2 58 | keyCodes: [50], // 2, 59 | phoneticName: 'to', 60 | }, { 61 | // count 3 62 | keyCodes: [51], // 3 63 | phoneticName: 'tre', 64 | }, { 65 | // count 4 66 | keyCodes: [52], // 4 67 | phoneticName: 'or', 68 | }, { 69 | // count 5 70 | keyCodes: [53], // 5 71 | phoneticName: 'fi', 72 | }, { 73 | // count 6 74 | keyCodes: [54], // 6 75 | phoneticName: 'si', 76 | }, { 77 | // count 7 78 | keyCodes: [55], // 7 79 | phoneticName: 'sen', 80 | }, { 81 | // count 8 82 | keyCodes: [56], // 8 83 | phoneticName: 'at', 84 | }, { 85 | // count 9 86 | keyCodes: [57], // 9 87 | phoneticName: 'ni', 88 | }, { 89 | // noop / do nothing 90 | }, { 91 | // up 92 | keyCodes: [87, 38], // w, up arrow 93 | readableName: FLAG_EMOJIS ? '⬆️' : 'up', 94 | hold: 1, 95 | }, { 96 | // down 97 | keyCodes: [83, 40], // s, down arrow 98 | readableName: FLAG_EMOJIS ? '⬇️' : 'down', 99 | hold: 1, 100 | }, { 101 | // left 102 | keyCodes: [65, 37], // a, left arrow 103 | readableName: FLAG_EMOJIS ? '⬅️' : 'left', 104 | animationId: ANIMATION_ID_WALKING, 105 | hold: 1, 106 | }, { 107 | // right 108 | keyCodes: [68, 39], // d, right arrow 109 | readableName: FLAG_EMOJIS ? '➡️' : 'right', 110 | animationId: ANIMATION_ID_WALKING, 111 | hold: 1, 112 | }, { 113 | // jump 114 | keyCodes: [74, 32], // j, space 115 | //readableName: FLAG_EMOJIS ? '🐇' : 'jump', 116 | }, { 117 | // rewind 118 | keyCodes: [219], // [ 119 | //keyChar: '[', 120 | //readableName: FLAG_EMOJIS ? '⏪' : 'rewind', 121 | animationId: ANIMATION_ID_PRESSING_BUTTON, 122 | hold: 1, 123 | automatedDuration: REWIND_INTERVAL, 124 | }, { 125 | // fast forward 126 | keyCodes: [221], // ] 127 | //keyChar: ']', 128 | //readableName: FLAG_EMOJIS ? '⏩' : 'fast forward', 129 | animationId: ANIMATION_ID_PRESSING_BUTTON, 130 | hold: 1, 131 | automatedDuration: REWIND_INTERVAL, 132 | }, { 133 | // stop 134 | //readableName: FLAG_EMOJIS ? '⏹' : 'stop', 135 | }, { 136 | // pick up / get 137 | keyCodes: [71], // g 138 | //readableName: FLAG_EMOJIS ? '⇡' : 'pick up', 139 | animationId: ANIMATION_ID_PICKING_UP, 140 | }, { 141 | // drop 142 | keyCodes: [66], // b 143 | readableName: FLAG_EMOJIS ? '⇣' : 'drop', 144 | animationId: ANIMATION_ID_DROPPING, 145 | }, { 146 | // throw 147 | keyCodes: [84], // t 148 | //readableName: FLAG_EMOJIS ? '🏹' : 'throw', 149 | animationId: ANIMATION_ID_THROWING, 150 | }, { 151 | // insert 152 | keyCodes: [73], // i 153 | readableName: FLAG_EMOJIS ? '📩' : 'insert', 154 | animationId: ANIMATION_ID_INSERTING, 155 | }, { 156 | // eject 157 | keyCodes: [75], // k 158 | readableName: FLAG_EMOJIS ? '⏏️' : 'eject', 159 | }, { 160 | // play 161 | keyCodes: [80], // p 162 | //readableName: FLAG_EMOJIS ? '▶' : 'play', 163 | animationId: ANIMATION_ID_PRESSING_BUTTON, 164 | hold: 1, 165 | remember: 1, 166 | automatedDuration: PLAYBACK_INTERVAL, 167 | }, { 168 | // record 169 | keyCodes: [82], // r 170 | //readableName: FLAG_EMOJIS ? '⏺️' : 'record', 171 | hold: 1, 172 | automatedDuration: PLAYBACK_INTERVAL, 173 | }, { 174 | // save world 175 | readableName: FLAG_EMOJIS ? '💾' : 'save', 176 | animationId: ANIMATION_ID_SAVING, 177 | }, { 178 | // shoot 179 | readableName: FLAG_EMOJIS ? '🔫' : 'fire', 180 | keyCodes: [13], // enter 181 | hold: 1, 182 | automatedDuration: BULLET_INTERVAL, 183 | }, { 184 | // wait 185 | //readableName: FLAG_EMOJIS ? '⌛' : 'wait', 186 | automatedDuration: 999, 187 | }, { 188 | // ass pull 189 | }/*, { 190 | // help 191 | keyCodes: [72], // h 192 | readableName: FLAG_EMOJIS ? '📖' : 'help', 193 | }*/]; 194 | 195 | const INPUT_KEY_CODE_MAPPINGS: {[_: number]: number } = {}; 196 | const INSTRUCTION_TO_ANIMATION_IDS: {[_: number]: number} = {}; 197 | const initInstructions = (audioContext: AudioContext, sounds: {[_:number]: Sound}) => { 198 | INSTRUCTIONS.forEach((instruction, id) => { 199 | instruction.keyCodes && instruction.keyCodes.forEach(keyCode => INPUT_KEY_CODE_MAPPINGS[keyCode] = id); 200 | INSTRUCTION_TO_ANIMATION_IDS[id] = instruction.animationId; 201 | if (FLAG_NATIVE_SPEECH_SYNTHESIS && window.speechSynthesis) { 202 | const utterance = new SpeechSynthesisUtterance(instructionToName(id)) 203 | utterance.volume = .2; 204 | instruction.spoken = () => { 205 | window.speechSynthesis.speak(utterance); 206 | }; 207 | } else if (FLAG_LOCAL_SPEECH_SYNTHESIS) { 208 | const name = INSTRUCTIONS[id].phoneticName || instructionToName(id); 209 | instruction.spoken = name && synthesizeSpeech(audioContext, name, .01) || sounds[id]; 210 | } else if (FLAG_TONAL_SPEECH_SYNTHESIS) { 211 | instruction.spoken = dtmfSoundFactory(audioContext, 212 | DTMF_FREQUENCIES_1[id % DTMF_FREQUENCIES_1.length], 213 | DTMF_FREQUENCIES_2[(id / DTMF_FREQUENCIES_1.length | 0) % DTMF_FREQUENCIES_2.length], 214 | INSTRUCTION_DURATION, 215 | ); 216 | } else { 217 | instruction.spoken = sounds[id] || (() => 0); 218 | } 219 | }); 220 | } 221 | 222 | const instructionToName = (instructionId: number) => { 223 | const i = INSTRUCTIONS[instructionId]; 224 | return i.readableName || instructionToKey(i); 225 | }; 226 | 227 | const instructionToUtterance = (instructionId: number) => { 228 | const instruction = INSTRUCTIONS[instructionId]; 229 | return instruction.spoken; 230 | }; 231 | 232 | const instructionToKey = (i: Instruction) => i.keyChar || i.keyCodes && String.fromCharCode(i.keyCodes[0]); 233 | 234 | 235 | type Inputs = { 236 | states?: {[_:number]: number}, 237 | reads?: {[_:number]: number}, 238 | }; 239 | 240 | let setInput = (entity: Entity, input: number, now: number) => { 241 | const everyEntity = entity as EveryEntity; 242 | const ir = everyEntity.instructionRepeat || 1; 243 | everyEntity.instructionRepeat = 0; 244 | everyEntity.activeInputs.states[input] = now; 245 | return ir; 246 | } 247 | 248 | let setRead = (entity: Entity, input: number, now: number) => { 249 | const everyEntity = entity as EveryEntity; 250 | const inputs = everyEntity.activeInputs; 251 | const value = inputs.states[input]; 252 | const lastRead = (inputs.reads[input] || 0); 253 | if (lastRead < value) { 254 | inputs.reads[input] = now; 255 | return 1; 256 | } 257 | } 258 | 259 | let readInput = (entity: Entity, input: number, now: number) => { 260 | const everyEntity = entity as EveryEntity; 261 | const inputs = everyEntity.activeInputs; 262 | const hold = INSTRUCTIONS[input].hold; 263 | const value = inputs.states[input]; 264 | const lastRead = (inputs.reads[input] || 0); 265 | if ((!everyEntity.capabilities || everyEntity.capabilities.indexOf(input) >= 0) && value && lastRead < value) { 266 | if (!hold) { 267 | inputs.reads[input] = now; 268 | } 269 | return 1; 270 | } else { 271 | return 0; 272 | } 273 | } 274 | 275 | let doRepeatingInput = (entity: Entity, input: number, now: number, delta: number, interval?: number) => { 276 | const everyEntity = entity as EveryEntity; 277 | const inputs = everyEntity.activeInputs; 278 | if (!everyEntity.capabilities || everyEntity.capabilities.indexOf(input) >= 0) { 279 | if (!interval) { 280 | const graphic = everyEntity.graphic; 281 | const animationId = INSTRUCTION_TO_ANIMATION_IDS[input]; 282 | const animation = graphic.animations && graphic.animations[animationId]; 283 | if (animation) { 284 | interval = animation.poseDuration; 285 | } 286 | } 287 | if (interval) { 288 | const value = inputs.states[input]; 289 | const diff = now - value; 290 | return ((diff/interval | 0) != ((diff + delta)/interval | 0)); 291 | } 292 | } 293 | } -------------------------------------------------------------------------------- /src/ts/game/entities.ts: -------------------------------------------------------------------------------- 1 | const ORIENTATION_LEFT = 0; 2 | const ORIENTATION_RIGHT = 1; 3 | 4 | const COLLISION_GROUP_SPEECH_BUBBLES = -1; 5 | const COLLISION_GROUP_TERRAIN = 0; 6 | const COLLISION_GROUP_ITEMS = 1; 7 | const COLLISION_GROUP_SPIKES = 2; 8 | const COLLISION_GROUP_PUSHABLES = 3; 9 | const COLLISION_GROUP_PLAYER = 4; 10 | const COLLISION_GROUP_ENEMIES = 5; 11 | const COLLISION_GROUP_BACKGROUNDED = 6; 12 | const COLLISION_GROUP_BULLETS = 7; 13 | 14 | const COLLISION_MASK_TERRAIN = ~0; 15 | // don't collide with bullets or items or backgrounded 16 | const COLLISION_MASK_SPIKES = 0x22000F; 17 | // don't collide with spikes 18 | const COLLISION_MASK_PUSHABLES = 0xFFFF0FF; 19 | // collides with terrain and pushables 20 | const COLLISION_MASK_ITEMS = 0xF00F; 21 | // collides with terrain and pushables and other players and the bottom of enemies 22 | const COLLISION_MASK_PLAYER = 0x8FF80F; 23 | // collides with terrain, spikes on the bottom, pushables, other enemies on the top and bottom and the player on the top 24 | const COLLISION_MASK_ENEMIES = 0xA2F80F; 25 | const COLLISION_MASK_BACKGROUNDED = 0; 26 | const COLLISION_MASK_BULLETS = 0xF; 27 | 28 | const EDGE_GRAB = 4; 29 | //const GRAB_MASK = 1 + 8 + 32; 30 | const GRAB_MASK = 41; 31 | // const BULLET_MASK = 16 + 32; 32 | const BULLET_MASK = 48; // enemies and player 33 | 34 | type Orientation = 0 | 1; 35 | 36 | type PassiveAnimation = { 37 | animationStartTime: number; 38 | notifyComplete: (worldAge: number) => void; 39 | }; 40 | 41 | type CommonEntity = { 42 | eid: number; 43 | entityType: number; 44 | persistentId?: number; 45 | } 46 | 47 | type GraphicalEntity = { 48 | graphic: Graphic; 49 | palette: HSL[]; 50 | previousAnimationPoseDuration?: number; 51 | previousPoseId?: number; 52 | currentPoseId?: number; 53 | currentAnimationId?: number; 54 | currentAnimationStartTime?: number; 55 | passiveAnimations?: {[_:number]: PassiveAnimation}; 56 | } & CommonEntity; 57 | 58 | type ActiveGraphicalEntity = { 59 | targetAnimationId?: number; 60 | } & GraphicalEntity; 61 | 62 | type SpatialEntity = { 63 | bounds: Rectangle, 64 | collisionGroup: number, 65 | collisionMask: number, 66 | } & CommonEntity; 67 | 68 | type MovableEntity = { 69 | velocity: Vector; 70 | mass?: number; 71 | ignoreMass?: number | boolean; 72 | gravityMultiplier: number; 73 | lastCollisions?: [number, number, number, number, number]; 74 | boundsWithVelocity?: Rectangle; 75 | remainingTime?: number; 76 | carrier?: MovableEntity; 77 | lastCarried?: number; 78 | carrying?: MovableEntity[]; 79 | carryingPreviously?: MovableEntity[]; 80 | restitution?: number; 81 | airTurn?: number | boolean; 82 | crushedEdges?: number; 83 | } & SpatialEntity; 84 | 85 | type ActiveMovableEntity = { 86 | baseVelocity: number; 87 | adjustedBaseVelocity?: number; 88 | activeInputs: Inputs; 89 | holding: {[_:number]: MovableEntity}; 90 | handJointId?: number; 91 | insertionJointId?: number; 92 | } & MovableEntity; 93 | 94 | type PlaybackEntity = { 95 | playing?: number | boolean; 96 | playbackStartTime?: number; 97 | nextScriptIndex?: number; 98 | autoRewind?: number | boolean; 99 | capabilities?: number[]; 100 | } & MovableEntity; 101 | 102 | type InstructableEntity = { 103 | nextInstructionTime?: number; 104 | instructionRepeat?: number; 105 | rememberedInstruction?: number; 106 | } & PlaybackEntity & ActiveMovableEntity & ListeningEntity; 107 | 108 | type OrientableEntity = { 109 | entityOrientation: Orientation; 110 | orientationStartTime: number; 111 | } & SpatialEntity; 112 | 113 | type GrabbingEntity = { 114 | grabbing?: SpatialEntity; 115 | grabMask: number; 116 | } & ActiveMovableEntity & OrientableEntity; 117 | 118 | type SpeakingEntity = { 119 | // we can only speak at certain intervals to make recording coherent 120 | toSpeak: number[]; 121 | }; 122 | 123 | type ListeningEntity = { 124 | instructionsHeard?: number[]; 125 | lastHeard?: number; 126 | hue?: number; 127 | }; 128 | 129 | type RecordingEntity = { 130 | recording?: boolean | number; 131 | } & ListeningEntity & PlaybackEntity; 132 | 133 | type MortalEntity = { 134 | deathAge?: number; 135 | }; 136 | 137 | type Asspull = (x: number, y: number, id: () => number) => [Entity]; 138 | 139 | type AsspullEntity = { 140 | asspull?: Asspull, 141 | } 142 | 143 | type EveryEntity = CommonEntity 144 | & GraphicalEntity 145 | & ActiveGraphicalEntity 146 | & SpatialEntity 147 | & MovableEntity 148 | & ActiveMovableEntity 149 | & PlaybackEntity 150 | & InstructableEntity 151 | & OrientableEntity 152 | & GrabbingEntity 153 | & SpeakingEntity 154 | & ListeningEntity 155 | & RecordingEntity 156 | & MortalEntity 157 | & AsspullEntity; 158 | 159 | const ENTITY_TYPE_PLAYER = 0; 160 | type Player = { 161 | entityType: 0, 162 | commandsVisible?: boolean | number, 163 | } & CommonEntity & GrabbingEntity & ActiveGraphicalEntity & PlaybackEntity & ListeningEntity & SpeakingEntity & MortalEntity; 164 | 165 | const ENTITY_TYPE_ROBOT = 1; 166 | type Robot = { 167 | entityType: 1, 168 | } & CommonEntity & ActiveMovableEntity & InstructableEntity & OrientableEntity & ActiveGraphicalEntity & ListeningEntity & MortalEntity & AsspullEntity; 169 | 170 | const ENTITY_TYPE_BLOCK = 2; 171 | type Block = { 172 | entityType: 2, 173 | } & CommonEntity & SpatialEntity & GraphicalEntity; 174 | 175 | const ENTITY_TYPE_CRATE = 3; 176 | type Crate = { 177 | entityType: 3, 178 | } & CommonEntity & MovableEntity & GraphicalEntity; 179 | 180 | const ENTITY_TYPE_BRICK = 4; 181 | type Brick = { 182 | entityType: 4, 183 | } & CommonEntity & MovableEntity & OrientableEntity; 184 | 185 | const ENTITY_TYPE_TAPE = 5; 186 | type Tape = { 187 | entityType: 5; 188 | script: number[]; 189 | hue: number; 190 | } & CommonEntity & MovableEntity & OrientableEntity & GraphicalEntity; 191 | 192 | const ENTITY_TYPE_REPEATER = 6; 193 | type Repeater = { 194 | entityType: 6, 195 | } & CommonEntity & ActiveMovableEntity & InstructableEntity & PlaybackEntity & GraphicalEntity & SpeakingEntity & MortalEntity; 196 | 197 | const ENTITY_TYPE_LETHAL = 7; 198 | type Lethal = { 199 | entityType: 7, 200 | } & CommonEntity & SpatialEntity & GraphicalEntity; 201 | 202 | const ENTITY_TYPE_PRESSURE_PLATE = 8; 203 | type PressurePlate = { 204 | entityType: 8, 205 | pressureEdge: Edge, 206 | } & CommonEntity & ActiveMovableEntity & PlaybackEntity & ActiveGraphicalEntity & SpeakingEntity; 207 | 208 | const ENTITY_TYPE_PLATFORM = 9; 209 | type Platform = { 210 | entityType: 9, 211 | direction: Edge, 212 | } & CommonEntity & ActiveMovableEntity & InstructableEntity & ActiveGraphicalEntity & ListeningEntity; 213 | 214 | const ENTITY_TYPE_SPEECH_BUBBLE = 10; 215 | type SpeechBubble = { 216 | entityType: 10, 217 | instruction: number, 218 | hue: number, 219 | spokenAge: number, 220 | collisionGroup: -1, // collision group speech bubbles 221 | } & CommonEntity & MovableEntity & MortalEntity; 222 | 223 | const ENTITY_TYPE_GUN = 11; 224 | type Gun = { 225 | entityType: 11, 226 | lastFired: number, 227 | fireRate: number, 228 | } & CommonEntity & MovableEntity & OrientableEntity & GraphicalEntity; 229 | 230 | const ENTITY_TYPE_BULLET = 12; 231 | type Bullet = { 232 | entityType: 12, 233 | } & CommonEntity & MovableEntity & GraphicalEntity & OrientableEntity; 234 | 235 | const ENTITY_TYPE_SCENERY = 13; 236 | type Scenery = { 237 | entityType: 13, 238 | text: string, 239 | } & CommonEntity & SpatialEntity; 240 | 241 | type Entity = Player | Robot | Block | Crate | Brick | Tape | Repeater | Lethal | PressurePlate | Platform | SpeechBubble | Gun | Bullet | Scenery; 242 | 243 | let entitySetVelocity = (room: Room, entity: MovableEntity, v: number, axis: number, timeRemaining: number) => { 244 | let dv = v - entity.velocity[axis]; 245 | entityAddVelocity(room, entity, dv, axis, timeRemaining); 246 | }; 247 | 248 | let entityAddVelocity = (room: Room, entity: MovableEntity, dv: number, axis: number, timeRemaining: number, reposition?: number | boolean) => { 249 | if (reposition) { 250 | roomRemoveEntityFromTiles(room, entity as Entity) 251 | let delta = entity.remainingTime - timeRemaining; 252 | entity.remainingTime = timeRemaining; 253 | axisMap(entity.bounds, entity.velocity, entityUpdatePosition(delta), entity.bounds); 254 | } 255 | entity.velocity[axis] += dv; 256 | [...entity.carryingPreviously, ...entity.carrying].forEach( 257 | e => !axis && entityAddVelocity(room, e, dv, axis, timeRemaining, 1) 258 | ); 259 | reposition && roomAddEntityToTiles(room, entity as Entity); 260 | }; 261 | 262 | let entityCalculateBoundsWithVelocity = (entity: Entity) => { 263 | const movableEntity = entity as MovableEntity; 264 | if (movableEntity.boundsWithVelocity) { 265 | const millis = movableEntity.remainingTime; 266 | if (millis) { 267 | axisMap(movableEntity.bounds, movableEntity.velocity, ([s], [v]) => s + Math.min(0, v * millis) - MAX_ROUNDING_ERROR_SIZE, movableEntity.boundsWithVelocity); 268 | axisMap(movableEntity.bounds, movableEntity.velocity, ([_, l], [v]) => l + Math.abs(v * millis) + MAX_ROUNDING_ERROR_SIZE * 2, movableEntity.boundsWithVelocity, 2); 269 | } else { 270 | movableEntity.boundsWithVelocity = [...movableEntity.bounds] as Rectangle; 271 | } 272 | } 273 | } 274 | 275 | let entitySetCarrying = (carrier?: SpatialEntity, carrying?: SpatialEntity, isCarryingAndWorldAge?: number) => { 276 | const movableCarrier = carrier as MovableEntity; 277 | const movableCarrying = carrying as MovableEntity; 278 | if (carrier && carrying && movableCarrier.velocity && movableCarrying.velocity) { 279 | //movableCarrying.velocity[0] = movableCarrier.velocity[0]; 280 | arrayRemoveElement(movableCarrier.carryingPreviously, movableCarrying); 281 | arrayRemoveElement(movableCarrier.carrying, movableCarrying); 282 | if (isCarryingAndWorldAge) { 283 | movableCarrier.carrying.push(movableCarrying); 284 | movableCarrying.carrier = movableCarrier; 285 | movableCarrying.lastCarried = isCarryingAndWorldAge; 286 | if (FLAG_CHECK_CIRCULAR_CARRYING) { 287 | let c = carrier; 288 | while (c) { 289 | if (c == carrying) { 290 | console.log("circular carrying"); 291 | } 292 | c = (c as MovableEntity).carrier; 293 | } 294 | } 295 | } else if (movableCarrying.carrier == movableCarrier) { 296 | movableCarrying.carrier = undefined; 297 | } 298 | } 299 | } 300 | 301 | let entityCalculateMass = (e: MovableEntity, includeZeroMass?: number | boolean): number => { 302 | return (e.mass || includeZeroMass) && [...e.carrying, ...e.carryingPreviously].reduce((totalMass, carried) => totalMass + entityCalculateMass(carried), e.mass) || 0; 303 | } 304 | 305 | let entityUpdatePosition = (delta: number) => ([s]: [number], [v]: [number]) => s + v * delta; 306 | 307 | let entityUpdatePositionToTimeRemaining = (room: Room, entity: Entity, remainingTime: number) => { 308 | const movableEntity = entity as MovableEntity; 309 | if (movableEntity.velocity && movableEntity.remainingTime > remainingTime) { 310 | roomRemoveEntityFromTiles(room, entity); 311 | const delta = movableEntity.remainingTime - remainingTime; 312 | movableEntity.remainingTime = remainingTime; 313 | axisMap(movableEntity.bounds, movableEntity.velocity, entityUpdatePosition(delta), movableEntity.bounds); 314 | roomAddEntityToTiles(room, entity); 315 | } 316 | }; 317 | 318 | let entityAddPassiveAnimation = ( 319 | entity: Entity, 320 | instructionId: number, 321 | sounds: {[_: number]: Sound}, 322 | worldAge: number, 323 | check: () => T, 324 | doit: (t: T, worldAge) => void 325 | ) => { 326 | const graphicalEntity = entity as GraphicalEntity; 327 | const animationId = INSTRUCTION_TO_ANIMATION_IDS[instructionId]; 328 | const t = check(); 329 | if (!graphicalEntity.passiveAnimations[animationId] && t) { 330 | if (graphicalEntity.graphic.animations[animationId] != null) { 331 | graphicalEntity.passiveAnimations[animationId] = { 332 | notifyComplete: (worldAge: number) => { 333 | const newT = check(); 334 | if (newT == t) { 335 | doit(t, worldAge) 336 | entityPlaySound(entity, instructionId, sounds); 337 | } 338 | }, 339 | animationStartTime: worldAge, 340 | }; 341 | } else { 342 | doit(t, worldAge); 343 | entityPlaySound(entity, instructionId, sounds); 344 | } 345 | } 346 | } 347 | 348 | let entityPlaySound = (entity: Entity, instruction: number, sounds: {[_: number]: Sound}, saying?: number | boolean) => { 349 | if (saying) { 350 | const speakingEntity = entity as SpeakingEntity; 351 | speakingEntity.toSpeak.unshift(instruction); 352 | } else { 353 | const sound = sounds[instruction]; 354 | if (sound) { 355 | sound(); 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/ts/graphics/player.graphic.ts: -------------------------------------------------------------------------------- 1 | const PLAYER_GRAPHIC_PALETTE_INDEX_FRONT_BODY = 0; 2 | const PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY = 1; 3 | const PLAYER_GRAPHIC_PALETTE_INDEX_FACE_PLATE = 2; 4 | const PLAYER_GRAPHIC_PALETTE_INDEX_LEGS = 3; 5 | const PLAYER_GRAPHIC_PALETTE_INDEX_RECORD_BUTTON = 4; 6 | 7 | const PLAYER_GRAPHIC_IMAGE_INDEX_FRONT_PANEL = 0; 8 | const PLAYER_GRAPHIC_IMAGE_INDEX_SIDE_PANEL = 1; 9 | const PLAYER_GRAPHIC_IMAGE_INDEX_LEG = 2; 10 | const PLAYER_GRAPHIC_IMAGE_INDEX_ARM = 3; 11 | const PLAYER_GRAPHIC_IMAGE_INDEX_EYE = 4; 12 | const PLAYER_GRAPHIC_IMAGE_INDEX_HANDLE = 5; 13 | 14 | const PLAYER_GRAPHIC_JOINT_ID_BODY = 0; 15 | const PLAYER_GRAPHIC_JOINT_ID_FRONT_PANEL = 1; 16 | const PLAYER_GRAPHIC_JOINT_ID_SIDE_PANEL = 2; 17 | const PLAYER_GRAPHIC_JOINT_ID_LEFT_LEG = 3; 18 | const PLAYER_GRAPHIC_JOINT_ID_RIGHT_LEG = 4; 19 | const PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM = 5; 20 | const PLAYER_GRAPHIC_JOINT_ID_LEFT_ARM = 6; 21 | const PLAYER_GRAPHIC_JOINT_ID_TORSO = 7; 22 | const PLAYER_GRAPHIC_JOINT_ID_LEFT_EYE = 8; 23 | const PLAYER_GRAPHIC_JOINT_ID_RIGHT_EYE = 9; 24 | const PLAYER_GRAPHIC_JOINT_ID_HANDLE = 10; 25 | const PLAYER_GRAPHIC_JOINT_ID_RIGHT_HAND = 11; 26 | const PLAYER_GRAPHIC_JOINT_ID_TAPE_DECK = 12; 27 | 28 | const PLAYER_GRAPHIC_POSE_ID_STEP_LEFT = 0; 29 | const PLAYER_GRAPHIC_POSE_ID_STEP_RIGHT = 1; 30 | const PLAYER_GRAPHIC_POSE_ID_JUMP = 2; 31 | const PLAYER_GRAPHIC_POSE_ID_REST_UP = 3; 32 | const PLAYER_GRAPHIC_POSE_ID_REST_DOWN = 4; 33 | const PLAYER_GRAPHIC_POSE_ID_GRAB = 5; 34 | const PLAYER_GRAPHIC_POSE_ID_INSERT = 6; 35 | const PLAYER_GRAPHIC_POSE_ID_PICK_UP = 7; 36 | const PLAYER_GRAPHIC_POSE_ID_THROW_WIND_UP = 8; 37 | const PLAYER_GRAPHIC_POSE_ID_THROW_PITCH = 9; 38 | const PLAYER_GRAPHIC_POSE_ID_DROP = 10; 39 | const PLAYER_GRAPHIC_POSE_ID_PUSH_BUTTON = 11; 40 | 41 | const playerPalette: HSL[] = [ 42 | [40, 5, 60], // font body colour 43 | [40, 5, 40], // side body colour 44 | [120, 40, 30], // face plate colour 45 | [40, 5, 10], // legs/arms/buttons 46 | [0, 99, 40], // red 47 | ]; 48 | 49 | const bossPalette: HSL[] = [ 50 | [0, 40, 30], // font body colour 51 | [0, 40, 20], // side body colour 52 | [0, 0, 50], // face plate colour 53 | [0, 0, 30], // legs/arms/buttons 54 | [0, 99, 40], // red 55 | ]; 56 | 57 | const playerGraphic: Graphic = { 58 | imageryWidth: 22, 59 | imageryHeight: 30, 60 | imagery: [ 61 | // front panel 62 | [ 63 | [0, 0, 20, 26, PLAYER_GRAPHIC_PALETTE_INDEX_FRONT_BODY], // panel 64 | [2, 6, 16, 8, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // tape area 65 | [3, 0, 14, 4, PLAYER_GRAPHIC_PALETTE_INDEX_LEGS], // button area 66 | [3, 0, 3, 4, PLAYER_GRAPHIC_PALETTE_INDEX_RECORD_BUTTON], // record button 67 | [3, 16, 2, 2, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // speaker hole 68 | [9, 16, 2, 2, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // speaker hole 69 | [15, 16, 2, 2, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // speaker hole 70 | [3, 21, 2, 2, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // speaker hole 71 | [9, 21, 2, 2, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // speaker hole 72 | [15, 21, 2, 2, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // speaker hole 73 | ], 74 | // side panel 75 | [ 76 | [-8, 0, 8, 26, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], 77 | ], 78 | // leg + foot 79 | [ 80 | [-2, 0, 4, 2, PLAYER_GRAPHIC_PALETTE_INDEX_LEGS], 81 | [-3, 2, 8, 2, PLAYER_GRAPHIC_PALETTE_INDEX_LEGS], 82 | ], 83 | // arm 84 | [ 85 | [-2, 0, 4, 8, PLAYER_GRAPHIC_PALETTE_INDEX_LEGS], 86 | ], 87 | // eye 88 | [ 89 | [-.5, -1, 1, 2, PLAYER_GRAPHIC_PALETTE_INDEX_LEGS], 90 | [-1, -.5, 2, 1, PLAYER_GRAPHIC_PALETTE_INDEX_LEGS], 91 | ], 92 | // handle 93 | [ 94 | [-5, -4, 2, 4, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // handle left 95 | [15, -4, 2, 4, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // handle right 96 | [-5, -4, 22, 2, PLAYER_GRAPHIC_PALETTE_INDEX_SIDE_BODY], // handle top 97 | ], 98 | ], 99 | joints: [{ 100 | gid: PLAYER_GRAPHIC_JOINT_ID_BODY, // body 101 | transformations: [{ 102 | transformType: TRANSFORM_TYPE_TRANSLATE, 103 | dx: -6, 104 | dy: 0, 105 | }], 106 | renderAfter: [{ 107 | gid: PLAYER_GRAPHIC_JOINT_ID_LEFT_LEG, // left leg 108 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_LEG, // leg 109 | transformations: [{ 110 | transformType: TRANSFORM_TYPE_TRANSLATE, 111 | dx: 0, 112 | dy: 26, 113 | }], 114 | }, { 115 | gid: PLAYER_GRAPHIC_JOINT_ID_RIGHT_LEG, // right leg 116 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_LEG, // leg 117 | transformations: [{ 118 | transformType: TRANSFORM_TYPE_TRANSLATE, 119 | dx: 14, 120 | dy: 26, 121 | }], 122 | }, { 123 | gid: PLAYER_GRAPHIC_JOINT_ID_TORSO, // torso 124 | renderAfter: [{ 125 | gid: PLAYER_GRAPHIC_JOINT_ID_FRONT_PANEL, // front panel 126 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_FRONT_PANEL, // font panel 127 | renderBefore: [{ 128 | gid: PLAYER_GRAPHIC_JOINT_ID_LEFT_ARM, // left arm 129 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_ARM, // arm 130 | transformations: [{ 131 | transformType: TRANSFORM_TYPE_TRANSLATE, 132 | dx: 18, 133 | dy: 8, 134 | }], 135 | }, { // handle 136 | gid: PLAYER_GRAPHIC_JOINT_ID_HANDLE, // handle 137 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_HANDLE, // handle 138 | }], 139 | renderAfter: [{ 140 | //id: PLAYER_GRAPHIC_JOINT_ID_LEFT_EYE, // left eye 141 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_EYE, // eye 142 | transformations: [{ 143 | transformType: TRANSFORM_TYPE_TRANSLATE, 144 | dx: 7, 145 | dy: 10, 146 | }], 147 | }, { 148 | //id: PLAYER_GRAPHIC_JOINT_ID_RIGHT_EYE, // right eye 149 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_EYE, // eye 150 | transformations: [{ 151 | transformType: TRANSFORM_TYPE_TRANSLATE, 152 | dx: 14, 153 | dy: 10, 154 | }], 155 | }, { 156 | gid: PLAYER_GRAPHIC_JOINT_ID_TAPE_DECK, 157 | transformations: [{ 158 | transformType: TRANSFORM_TYPE_TRANSLATE, 159 | dx: 10, 160 | dy: 5, 161 | }] 162 | }], 163 | }, { 164 | gid: PLAYER_GRAPHIC_JOINT_ID_SIDE_PANEL, // side panel 165 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_SIDE_PANEL, // side panel 166 | renderAfter: [{ 167 | gid: PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM, // right arm 168 | imageIndex: PLAYER_GRAPHIC_IMAGE_INDEX_ARM, // arm 169 | transformations: [{ 170 | transformType: TRANSFORM_TYPE_TRANSLATE, 171 | dx: -5, 172 | dy: 8, 173 | }], 174 | renderAfter: [{ 175 | gid: PLAYER_GRAPHIC_JOINT_ID_RIGHT_HAND, 176 | transformations: [{ 177 | transformType: TRANSFORM_TYPE_TRANSLATE, 178 | dx: 0, 179 | dy: 6, 180 | }] 181 | }] 182 | }], 183 | }], 184 | }], 185 | }], 186 | poses: [ 187 | // step left (0) 188 | { 189 | [PLAYER_GRAPHIC_JOINT_ID_BODY]: [{// body 190 | transformType: TRANSFORM_TYPE_TRANSLATE, 191 | dx: -6, 192 | dy: 0, 193 | }, { 194 | transformType: TRANSFORM_TYPE_ROTATE, 195 | rAngle: LOW_P_MATH_PI/20, 196 | }], 197 | [PLAYER_GRAPHIC_JOINT_ID_FRONT_PANEL]: [{ // front panel 198 | transformType: TRANSFORM_TYPE_SCALE, 199 | scaleX: 1.2, 200 | scaleY: 1, 201 | }], 202 | [PLAYER_GRAPHIC_JOINT_ID_SIDE_PANEL]: [{ // side panel 203 | transformType: TRANSFORM_TYPE_SCALE, 204 | scaleX: .6, 205 | scaleY: 1, 206 | }], 207 | [PLAYER_GRAPHIC_JOINT_ID_LEFT_LEG]: [{ // left leg 208 | transformType: TRANSFORM_TYPE_TRANSLATE, 209 | dx: 0, 210 | dy: -6, 211 | }, { 212 | transformType: TRANSFORM_TYPE_ROTATE, 213 | rAngle: LOW_P_MATH_PI/5, 214 | }], 215 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ // right arm 216 | transformType: TRANSFORM_TYPE_ROTATE, 217 | rAngle: -LOW_P_MATH_PI_ON_3, 218 | }], 219 | [PLAYER_GRAPHIC_JOINT_ID_LEFT_ARM]: [{ // left arm 220 | transformType: TRANSFORM_TYPE_ROTATE, 221 | rAngle: LOW_P_MATH_PI_ON_3, 222 | }], 223 | [PLAYER_GRAPHIC_JOINT_ID_HANDLE]: [{ // handle 224 | transformType: TRANSFORM_TYPE_SCALE, 225 | scaleX: .8, 226 | scaleY: 1, 227 | }, { 228 | transformType: TRANSFORM_TYPE_TRANSLATE, 229 | dx: 3, 230 | dy: 0, 231 | }], 232 | }, 233 | // step right (1) 234 | { 235 | [PLAYER_GRAPHIC_JOINT_ID_BODY]: [{ // body 236 | transformType: TRANSFORM_TYPE_TRANSLATE, 237 | dx: 6, 238 | dy: 0, 239 | }, { 240 | transformType: TRANSFORM_TYPE_ROTATE, 241 | rAngle: -LOW_P_MATH_PI/20, 242 | }], 243 | [PLAYER_GRAPHIC_JOINT_ID_FRONT_PANEL]: [{ // front panel 244 | transformType: TRANSFORM_TYPE_SCALE, 245 | scaleX: .6, 246 | scaleY: 1, 247 | }], 248 | [PLAYER_GRAPHIC_JOINT_ID_SIDE_PANEL]: [{ // side panel 249 | transformType: TRANSFORM_TYPE_SCALE, 250 | scaleX: 1.2, 251 | scaleY: 1, 252 | }], 253 | [PLAYER_GRAPHIC_JOINT_ID_LEFT_LEG]: [{ // left leg 254 | transformType: TRANSFORM_TYPE_TRANSLATE, 255 | dx: -4, 256 | dy: 0, 257 | }], 258 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_LEG]: [{ // right leg 259 | transformType: TRANSFORM_TYPE_TRANSLATE, 260 | dx: -4, 261 | dy: -6, 262 | }, { 263 | transformType: TRANSFORM_TYPE_ROTATE, 264 | rAngle: LOW_P_MATH_PI/5, 265 | }], 266 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ // right arm 267 | transformType: TRANSFORM_TYPE_ROTATE, 268 | rAngle: LOW_P_MATH_PI_ON_3, 269 | }], 270 | [PLAYER_GRAPHIC_JOINT_ID_LEFT_ARM]: [{ // left arm 271 | transformType: TRANSFORM_TYPE_ROTATE, 272 | rAngle: -LOW_P_MATH_PI_ON_3, 273 | }], 274 | [PLAYER_GRAPHIC_JOINT_ID_HANDLE]: [{ // handle 275 | transformType: TRANSFORM_TYPE_SCALE, 276 | scaleX: .7, 277 | scaleY: 1, 278 | }] 279 | }, 280 | // jump (2) 281 | { 282 | [PLAYER_GRAPHIC_JOINT_ID_LEFT_LEG]: [{ // left leg 283 | transformType: TRANSFORM_TYPE_ROTATE, 284 | rAngle: LOW_P_MATH_PI/8 285 | }], 286 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_LEG]: [{ // right leg 287 | transformType: TRANSFORM_TYPE_ROTATE, 288 | rAngle: -LOW_P_MATH_PI/8 289 | }], 290 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ // right arm 291 | transformType: TRANSFORM_TYPE_ROTATE, 292 | rAngle: LOW_P_MATH_PI_ON_3*2, 293 | }], 294 | [PLAYER_GRAPHIC_JOINT_ID_LEFT_ARM]: [{ // left arm 295 | transformType: TRANSFORM_TYPE_ROTATE, 296 | rAngle: -LOW_P_MATH_PI_ON_3*2, 297 | }], 298 | }, 299 | // rest up (3) 300 | { 301 | [PLAYER_GRAPHIC_JOINT_ID_TORSO]: [{ // torso 302 | transformType: TRANSFORM_TYPE_TRANSLATE, 303 | dx: 0, 304 | dy: -2, 305 | }, { 306 | transformType: TRANSFORM_TYPE_SCALE, 307 | scaleX: .95, 308 | scaleY: 1.1, 309 | }] 310 | }, 311 | // rest down (4) 312 | { 313 | // defaults 314 | }, 315 | // grab (5) 316 | { 317 | [PLAYER_GRAPHIC_JOINT_ID_BODY]: [{ // body 318 | transformType: TRANSFORM_TYPE_TRANSLATE, 319 | dx: -6, 320 | dy: 0, 321 | }, { 322 | transformType: TRANSFORM_TYPE_ROTATE, 323 | rAngle: -LOW_P_MATH_PI/10, 324 | }], 325 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ // right arm 326 | transformType: TRANSFORM_TYPE_ROTATE, 327 | rAngle: LOW_P_MATH_PI/10 328 | }], 329 | [PLAYER_GRAPHIC_JOINT_ID_LEFT_ARM]: [{ // left arm 330 | transformType: TRANSFORM_TYPE_ROTATE, 331 | rAngle: -LOW_P_MATH_PI_ON_3 332 | }] 333 | }, 334 | // insert 335 | { 336 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ 337 | transformType: TRANSFORM_TYPE_ROTATE, 338 | rAngle: -LOW_P_MATH_PI_ON_2 339 | }], 340 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_HAND]: [{ 341 | transformType: TRANSFORM_TYPE_ROTATE, 342 | rAngle: LOW_P_MATH_PI_ON_2 343 | }], 344 | }, 345 | // pick up 346 | { 347 | [PLAYER_GRAPHIC_JOINT_ID_TORSO]: [{ 348 | transformType: TRANSFORM_TYPE_SCALE, 349 | scaleX: 1.1, 350 | scaleY: .8, 351 | }, { 352 | transformType: TRANSFORM_TYPE_TRANSLATE, 353 | dx: 0, 354 | dy: 10, 355 | }], 356 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ 357 | transformType: TRANSFORM_TYPE_ROTATE, 358 | rAngle: -LOW_P_MATH_PI_ON_4, 359 | }], 360 | }, 361 | // throw wind up 362 | { 363 | [PLAYER_GRAPHIC_JOINT_ID_BODY]: [{ 364 | transformType: TRANSFORM_TYPE_ROTATE, 365 | rAngle: -LOW_P_MATH_PI_ON_4, 366 | aroundX: 3, 367 | aroundY: 26, 368 | }], 369 | [PLAYER_GRAPHIC_JOINT_ID_TORSO]: [{ 370 | transformType: TRANSFORM_TYPE_SCALE, 371 | scaleX: 1.2, 372 | scaleY: .8, 373 | }, { 374 | transformType: TRANSFORM_TYPE_TRANSLATE, 375 | dx: 0, 376 | dy: 10, 377 | }], 378 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ 379 | transformType: TRANSFORM_TYPE_ROTATE, 380 | rAngle: LOW_P_MATH_PI_ON_4*3, 381 | }], 382 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_LEG]: [{ 383 | transformType: TRANSFORM_TYPE_ROTATE, 384 | rAngle: LOW_P_MATH_PI_ON_4, 385 | }], 386 | }, 387 | // throw pitch 388 | { 389 | [PLAYER_GRAPHIC_JOINT_ID_BODY]: [{ 390 | transformType: TRANSFORM_TYPE_ROTATE, 391 | rAngle: LOW_P_MATH_PI_ON_4, 392 | aroundX: 10, 393 | aroundY: 26, 394 | }], 395 | [PLAYER_GRAPHIC_JOINT_ID_TORSO]: [{ 396 | transformType: TRANSFORM_TYPE_SCALE, 397 | scaleX: .9, 398 | scaleY: 1.2, 399 | }, { 400 | transformType: TRANSFORM_TYPE_TRANSLATE, 401 | dx: 0, 402 | dy: -4, 403 | }], 404 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ 405 | transformType: TRANSFORM_TYPE_ROTATE, 406 | rAngle: 3*LOW_P_MATH_PI_ON_2, 407 | }], 408 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_LEG]: [{ 409 | transformType: TRANSFORM_TYPE_ROTATE, 410 | rAngle: -LOW_P_MATH_PI_ON_4, 411 | }], 412 | }, 413 | // drop 414 | { 415 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ 416 | transformType: TRANSFORM_TYPE_ROTATE, 417 | rAngle: -LOW_P_MATH_PI_ON_3, 418 | }], 419 | }, 420 | // push a button 421 | { 422 | [PLAYER_GRAPHIC_JOINT_ID_RIGHT_ARM]: [{ 423 | transformType: TRANSFORM_TYPE_ROTATE, 424 | rAngle: -LOW_P_MATH_PI_ON_4*3, 425 | }, { 426 | transformType: TRANSFORM_TYPE_TRANSLATE, 427 | dx: 0, 428 | dy: 4, 429 | }], 430 | } 431 | ], 432 | animations: { 433 | [ANIMATION_ID_WALKING]: { // walk (0) 434 | poseDuration: 299, 435 | poseIds: [PLAYER_GRAPHIC_POSE_ID_STEP_LEFT, PLAYER_GRAPHIC_POSE_ID_STEP_RIGHT], // step left, step right 436 | }, 437 | [ANIMATION_ID_JUMPING]: { // jump (1) 438 | poseDuration: 399, 439 | poseIds: [PLAYER_GRAPHIC_POSE_ID_JUMP], // jump 440 | repeatCount: 1, 441 | }, 442 | [ANIMATION_ID_RESTING]: { 443 | poseDuration: 999, 444 | poseIds: [PLAYER_GRAPHIC_POSE_ID_REST_UP, PLAYER_GRAPHIC_POSE_ID_REST_DOWN], 445 | }, 446 | [ANIMATION_ID_GRABBING]: { 447 | poseDuration: 99, 448 | poseIds: [PLAYER_GRAPHIC_POSE_ID_GRAB], 449 | repeatCount: 1, 450 | }, 451 | [ANIMATION_ID_INSERTING]: { 452 | poseDuration: 299, 453 | poseIds: [PLAYER_GRAPHIC_POSE_ID_INSERT], 454 | }, 455 | [ANIMATION_ID_PICKING_UP]: { 456 | poseDuration: 199, 457 | poseIds: [PLAYER_GRAPHIC_POSE_ID_PICK_UP], 458 | }, 459 | [ANIMATION_ID_THROWING]: { 460 | poseDuration: 180, 461 | poseIds: [PLAYER_GRAPHIC_POSE_ID_THROW_WIND_UP, PLAYER_GRAPHIC_POSE_ID_THROW_PITCH], 462 | }, 463 | [ANIMATION_ID_DROPPING]: { 464 | poseDuration: 199, 465 | poseIds: [PLAYER_GRAPHIC_POSE_ID_DROP], 466 | }, 467 | [ANIMATION_ID_PRESSING_BUTTON]: { 468 | poseDuration: 299, 469 | poseIds: [PLAYER_GRAPHIC_POSE_ID_PUSH_BUTTON], 470 | repeatCount: 1, 471 | }, 472 | [ANIMATION_ID_DEATH]: { 473 | poseDuration: 99, 474 | poseIds: [PLAYER_GRAPHIC_POSE_ID_JUMP], 475 | } 476 | }, 477 | }; 478 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/ts/factories/room.factory.ts: -------------------------------------------------------------------------------- 1 | type EntityFactory = (x: number, y: number, id: IdFactory, pid?: number) => Entity[]; 2 | 3 | const PLAYER_PERSISTENT_ID = 1; 4 | const PERSISTENT_ID_TUTORIAL_END_1 = 2; 5 | const PERSISTENT_ID_TUTORIAL_END_2 = 3; 6 | const PERSISTENT_ID_TUTORIAL_END_3 = 4; 7 | const TUTORIAL_TAPE_PLATOFORM_PERSITENT_ID = 5; 8 | const PERSISTENT_ID_GUN_ROOM_1 = 6; 9 | const PERSISTENT_ID_GUN_ROOM_2 = 7; 10 | const PERSISTENT_ID_GUN_ROOM_3 = 8; 11 | const PERSISTENT_ID_LEFT_RIGHT_ROOM_1 = 9; 12 | const PERSISTENT_ID_LEFT_RIGHT_ROOM_2 = 10; 13 | 14 | const MAX_PERSISTENT_ID_PLUS_1 = 11; 15 | 16 | // do not use 0 as hue for red as we do a lazy null check 17 | const HUE_TAPE_SET_1 = 60; 18 | const HUE_TAPE_SET_2 = 145; 19 | const HUE_TAPE_SET_3 = 1; 20 | const HUE_TAPE_SET_4 = 217; 21 | const HUE_TAPE_SET_5 = 289; 22 | 23 | const HSL_AREA_0_BLOCKS: HSL = [15, 35, 40]; 24 | const BACKGROUND_AREA_0: HSL[] = [[240, 60, 80], [240, 50, 50], [120, 20, 20]]; 25 | 26 | const HSL_AREA_1_BLOCKS: HSL = [240, 40, 40]; 27 | const BACKGROUND_AREA_1: HSL[] = [[199, 5, 20], [120, 20, 20]]; 28 | 29 | const HSL_AREA_2_BLOCKS: HSL = [180, 40, 25]; 30 | const BACKGROUND_AREA_2: HSL[] = [[120, 20, 20], [0, 30, 20]]; 31 | 32 | const HSL_AREA_3_BLOCKS: HSL = [299, 20, 40]; 33 | const BACKGROUND_AREA_3: HSL[] = [[299, 20, 30], [199, 5, 20]]; 34 | 35 | const BACKGROUND_AREA_4: HSL[] = [[240, 20, 50], [299, 20, 30]]; 36 | const BACKGROUND_AREA_5: HSL[] = [[240, 100, 100],[240, 20, 50]] 37 | 38 | 39 | 40 | const compositeEntityFactoryFactory = (entityFactories: EntityFactory[]) => { 41 | return (x, y, id: IdFactory) => { 42 | return entityFactories.reduce((entities, entityFactory) => [...entities, ...entityFactory(x, y, id)], []); 43 | } 44 | } 45 | 46 | type Legend = {[_:string]: EntityFactory}; 47 | 48 | type RoomFactoryMetadata = { 49 | worldWidth: number, 50 | worldHeight: number, 51 | factory: RoomFactory, 52 | }; 53 | 54 | type RoomAndEntity = [number, number, Entity]; 55 | 56 | const roomFactoryFactory = () => { 57 | const spikeFactory = spikeFactoryFactory(); 58 | 59 | // set up all the entities 60 | const baseLegend: Legend = { 61 | 'o': blockFactoryFactory(HSL_AREA_0_BLOCKS, 6), 62 | 'y': blockFactoryFactory(HSL_AREA_1_BLOCKS), 63 | //'x': blockFactoryFactory(HSL_AREA_2_BLOCKS, 6), 64 | 'z': blockFactoryFactory(HSL_AREA_3_BLOCKS, 2), 65 | 'a': crateFactory, 66 | 'v': spikeFactory, 67 | 'T': sceneryFactoryFactory('🌳', 3), // 🌳 68 | //'A': sceneryFactoryFactory('🌲', 3), // 🌲 69 | 'O': sceneryFactoryFactory('☁️', 2), // ☁️ 70 | 'U': sceneryFactoryFactory('☁️', 3), 71 | //'F': sceneryFactoryFactory('🌻', 1), // 🌻 72 | //'M': sceneryFactoryFactory('🍄', .5), // 🍄 73 | //'I': sceneryFactoryFactory('🕯️', .5), // 🕯️ 74 | //'Z': sceneryFactoryFactory('🖼️', 1), // 🖼️ 75 | //'H': sceneryFactoryFactory('🏺', 1), // 🏺 76 | //'K': sceneryFactoryFactory('⚗️', 2), // ⚗️ 77 | //'P': persistentEntityFactoryFactory(playerFactory, PLAYER_PERSISTENT_ID), 78 | 'm': mainframeFactoryFactory(HUE_TAPE_SET_1), 79 | } 80 | 81 | const script1 = [INSTRUCTION_ID_COUNT_4, INSTRUCTION_ID_LEFT, 82 | INSTRUCTION_ID_ASSPULL, 83 | INSTRUCTION_ID_COUNT_4, INSTRUCTION_ID_RIGHT, 84 | INSTRUCTION_ID_THROW, INSTRUCTION_ID_WAIT, 85 | INSTRUCTION_ID_JUMP, 86 | INSTRUCTION_ID_COUNT_4, INSTRUCTION_ID_WAIT, 87 | INSTRUCTION_ID_ASSPULL, 88 | INSTRUCTION_ID_COUNT_4, INSTRUCTION_ID_LEFT, 89 | INSTRUCTION_ID_THROW, INSTRUCTION_ID_WAIT, 90 | INSTRUCTION_ID_COUNT_2, INSTRUCTION_ID_RIGHT, 91 | INSTRUCTION_ID_JUMP, INSTRUCTION_ID_WAIT, 92 | INSTRUCTION_ID_ASSPULL, 93 | INSTRUCTION_ID_COUNT_2, INSTRUCTION_ID_RIGHT, 94 | INSTRUCTION_ID_JUMP, INSTRUCTION_ID_WAIT, 95 | INSTRUCTION_ID_EJECT 96 | ]; 97 | const script2 = [ 98 | INSTRUCTION_ID_ASSPULL, 99 | INSTRUCTION_ID_COUNT_4, INSTRUCTION_ID_LEFT, 100 | INSTRUCTION_ID_JUMP, INSTRUCTION_ID_WAIT, 101 | INSTRUCTION_ID_COUNT_4, INSTRUCTION_ID_RIGHT, 102 | INSTRUCTION_ID_DROP, INSTRUCTION_ID_WAIT, 103 | INSTRUCTION_ID_ASSPULL, 104 | INSTRUCTION_ID_COUNT_4, INSTRUCTION_ID_LEFT, 105 | INSTRUCTION_ID_JUMP, INSTRUCTION_ID_WAIT, 106 | INSTRUCTION_ID_DROP, INSTRUCTION_ID_WAIT, 107 | INSTRUCTION_ID_ASSPULL, 108 | INSTRUCTION_ID_COUNT_2, INSTRUCTION_ID_RIGHT, 109 | INSTRUCTION_ID_JUMP, INSTRUCTION_ID_WAIT, 110 | INSTRUCTION_ID_COUNT_2, INSTRUCTION_ID_RIGHT, 111 | INSTRUCTION_ID_EJECT 112 | ]; 113 | const asspulls: Asspull[] = [ 114 | crateFactory as Asspull, 115 | crateFactory as Asspull, 116 | tapeFactoryFactory(script2, HUE_TAPE_SET_2, undefined, 2) as Asspull, 117 | gunFactoryFactory(robotFactoryFactory(ORIENTATION_LEFT, HUE_TAPE_SET_2)) as Asspull, 118 | gunFactoryFactory(robotFactoryFactory(ORIENTATION_RIGHT, HUE_TAPE_SET_4)) as Asspull, 119 | tapeFactoryFactory(script1, HUE_TAPE_SET_1, undefined, 2) as Asspull, 120 | ]; 121 | const bossAsspull = () => { 122 | let asspullIndex = 0; 123 | return (x, y, id) => asspulls[(asspullIndex++)%asspulls.length](x, y, id); 124 | }; 125 | 126 | const roomFactories: RoomFactory[][] = [ 127 | [ 128 | // 0, 0 129 | , 130 | // 1, 0 131 | , 132 | // 2, 0 133 | , 134 | // 3, 0 135 | legendRoomFactory( 136 | {...baseLegend, 137 | 'C': sceneryFactoryFactory('🏰', 3), 138 | 'T': sceneryFactoryFactory('Fin', 2), 139 | 'B': tapeFactoryFactory([ 140 | INSTRUCTION_ID_WAIT, INSTRUCTION_ID_COUNT_4, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_WAIT, INSTRUCTION_ID_COUNT_2, INSTRUCTION_ID_RIGHT, 141 | ...script1 142 | 143 | ], HUE_TAPE_SET_1, bossFactoryFactory(HUE_TAPE_SET_3, undefined, bossAsspull), 2), 144 | }, 145 | ' ' + 146 | ' ' + 147 | ' ' + 148 | ' T ' + 149 | ' ' + 150 | ' ' + 151 | ' ' + 152 | ' ' + 153 | ' ' + 154 | ' C B ' + 155 | ' O O O O OzzzO OO ' + 156 | 'UO U U U U U U U U' + 157 | 'zzzzzzz zzzzzzz', 158 | MAX_TILES_ACROSS, 159 | BACKGROUND_AREA_5, 160 | ), 161 | ], 162 | [ 163 | // 0, 0 164 | , 165 | // 1, 0 166 | , 167 | // 2, 0 168 | , 169 | // 3, 0 170 | legendRoomFactory( 171 | {...baseLegend, 172 | 'Z': blockFactoryFactory(HSL_AREA_3_BLOCKS, 3, 1, .5), 173 | 'F': platformFactoryFactory(6, 1, EDGE_RIGHT, HUE_TAPE_SET_1), 174 | '1': tapeFactoryFactory([INSTRUCTION_ID_COUNT_6, INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_3_BLOCKS, EDGE_BOTTOM)), 175 | '2': tapeFactoryFactory([INSTRUCTION_ID_COUNT_6, INSTRUCTION_ID_LEFT], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_3_BLOCKS, EDGE_BOTTOM)), 176 | '3': tapeFactoryFactory([,,,,INSTRUCTION_ID_INSERT], HUE_TAPE_SET_3, pressurePlateFactoryFactory(1, 1, HSL_AREA_3_BLOCKS, EDGE_BOTTOM)), 177 | '5': tapeFactoryFactory([INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_COUNT_3, INSTRUCTION_ID_SHOOT, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_COUNT_3, INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_2, pressurePlateFactoryFactory(1, 1, HSL_AREA_3_BLOCKS, EDGE_RIGHT)), 178 | '6': tapeFactoryFactory([INSTRUCTION_ID_LEFT, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_COUNT_3, INSTRUCTION_ID_SHOOT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_COUNT_3, INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_4, pressurePlateFactoryFactory(1, 1, HSL_AREA_3_BLOCKS, EDGE_LEFT)), 179 | '7': tapeFactoryFactory([INSTRUCTION_ID_COUNT_5, INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_3_BLOCKS, EDGE_BOTTOM)), 180 | '8': tapeFactoryFactory([INSTRUCTION_ID_COUNT_5, INSTRUCTION_ID_LEFT], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_3_BLOCKS, EDGE_BOTTOM)), 181 | //'P': persistentEntityFactoryFactory(playerFactory, PLAYER_PERSISTENT_ID), 182 | 183 | }, 184 | 'zzzzzzz zzzzzzz' + 185 | 'z3 7 2 1 8 3z' + 186 | 'z z' + 187 | 'z z' + 188 | 'Z Z' + 189 | '5 F 6' + 190 | 'z z' + 191 | 'z z' + 192 | 'zzz zzz' + 193 | 'z z' + 194 | 'z z' + 195 | 'zzvvvvzz zzvvvvzz' + 196 | 'zzzzzzz zzzzzzz', 197 | MAX_TILES_ACROSS, 198 | BACKGROUND_AREA_4, 199 | ), 200 | ], 201 | [ 202 | // 0, 0 203 | , 204 | // 1, 0 205 | , 206 | // 2, 0 207 | , 208 | // 3, 0 209 | legendRoomFactory( 210 | {...baseLegend, 211 | 'F': platformFactoryFactory(4, 1, EDGE_TOP, HUE_TAPE_SET_5), 212 | '1': tapeFactoryFactory([INSTRUCTION_ID_COUNT_9, INSTRUCTION_ID_UP], HUE_TAPE_SET_5, pressurePlateFactoryFactory(1, 1, HSL_AREA_3_BLOCKS, EDGE_RIGHT)), 213 | }, 214 | 'zzzzzzz zzzzzzz' + 215 | 'z yy z' + 216 | 'z y z' + 217 | 'z z' + 218 | '1 z' + 219 | 'z F z' + 220 | 'z yy z' + 221 | 'z yy z' + 222 | 'z yy z' + 223 | 'z yy z' + 224 | 'z z' + 225 | 'z yyz' + 226 | 'zzzzzzz yyzz', 227 | MAX_TILES_ACROSS, 228 | BACKGROUND_AREA_3, 229 | ), 230 | ], 231 | [ 232 | // 0, 1 233 | legendRoomFactory( 234 | {...baseLegend, 235 | 't': tapeFactoryFactory([INSTRUCTION_ID_UP, INSTRUCTION_ID_DOWN, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_4), 236 | 'B': tapeFactoryFactory( 237 | [INSTRUCTION_ID_WAIT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_WAIT, INSTRUCTION_ID_DROP, INSTRUCTION_ID_WAIT, INSTRUCTION_ID_COUNT_9, INSTRUCTION_ID_LEFT], 238 | HUE_TAPE_SET_1, 239 | bossFactoryFactory(HUE_TAPE_SET_3, persistentEntityFactoryFactory(playerFactory, PLAYER_PERSISTENT_ID)), 240 | 2 241 | ), 242 | }, 243 | ' ' + 244 | ' O ' + 245 | ' B O ' + 246 | 'yyy U ' + 247 | 'ooo aT T ' + 248 | 'ooo oooooo' + 249 | 'ooo oooooo' + 250 | 'ooo Foooooo' + 251 | 'oooo ooooooo' + 252 | 'ooooooooooooo o' + 253 | 'oooooooooooo ' + 254 | 'ooooooooooooo t ' + 255 | 'oooooooooooooooooo', 256 | MAX_TILES_ACROSS, 257 | BACKGROUND_AREA_0, 258 | ), 259 | // 1, 1 260 | legendRoomFactory( 261 | {...baseLegend, 262 | 'R': robotFactoryFactory(ORIENTATION_LEFT, HUE_TAPE_SET_1), 263 | '1': tapeFactoryFactory([INSTRUCTION_ID_COUNT_1, INSTRUCTION_ID_COUNT_3, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_COUNT_1, INSTRUCTION_ID_COUNT_5, INSTRUCTION_ID_RIGHT,,,,,,,], HUE_TAPE_SET_1, repeaterFactoryFactory(HUE_TAPE_SET_5)), 264 | 'p': platformFactoryFactory(2, 1, EDGE_RIGHT, HUE_TAPE_SET_4), 265 | 'M': tapeFactoryFactory([INSTRUCTION_ID_SAVE], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_LEFT)), 266 | }, 267 | ' ' + 268 | ' O ' + 269 | ' a ' + 270 | ' oo M ' + 271 | ' T T R ym T' + 272 | 'ooooooo yyooo' + 273 | 'ooooooo 1 yoooo' + 274 | 'oooooooooooooooooo' + 275 | 'oooooooooooooooooo' + 276 | 'ooo ooo' + 277 | ' p ' + 278 | ' yvvvvvvvv ' + 279 | 'o yyyyyyyyyyyyyyy', 280 | MAX_TILES_ACROSS, 281 | BACKGROUND_AREA_0, 282 | ), 283 | // 2, 1 284 | legendRoomFactory( 285 | {...baseLegend, 286 | 'a': tapeFactoryFactory([INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS)), 287 | 'A': platformFactoryFactory(2, 1, EDGE_RIGHT, HUE_TAPE_SET_1), 288 | '1': tapeFactoryFactory([INSTRUCTION_ID_COUNT_7, INSTRUCTION_ID_LEFT], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_LEFT)), 289 | 'b': tapeFactoryFactory([INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_2, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS)), 290 | 'B': platformFactoryFactory(2, 1, EDGE_RIGHT, HUE_TAPE_SET_2), 291 | '2': tapeFactoryFactory([INSTRUCTION_ID_COUNT_7, INSTRUCTION_ID_LEFT], HUE_TAPE_SET_2, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_LEFT)), 292 | 'c': tapeFactoryFactory([INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_3, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS)), 293 | 'C': platformFactoryFactory(2, 1, EDGE_RIGHT, HUE_TAPE_SET_3), 294 | '3': tapeFactoryFactory([INSTRUCTION_ID_COUNT_7, INSTRUCTION_ID_LEFT], HUE_TAPE_SET_3, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_BOTTOM)), 295 | }, 296 | ' yyy' + 297 | ' O yyy' + 298 | ' U 3yy' + 299 | ' ' + 300 | 'T ' + 301 | 'oo yyyyy' + 302 | 'ooc C yyyy' + 303 | 'ooo yyyy' + 304 | 'ooob B 2yyy' + 305 | 'oooo ' + 306 | ' oa A 1yy' + 307 | ' oovvvvvvvvvvyyy' + 308 | 'yy oooooooooooooyy', 309 | MAX_TILES_ACROSS, 310 | BACKGROUND_AREA_0, 311 | ), 312 | // 3, 1 313 | legendRoomFactory( 314 | {...baseLegend, 315 | 'd': platformFactoryFactory(1, 5, EDGE_TOP, HUE_TAPE_SET_1), 316 | 'U': tapeFactoryFactory([INSTRUCTION_ID_UP,INSTRUCTION_ID_UP,INSTRUCTION_ID_UP,INSTRUCTION_ID_UP,INSTRUCTION_ID_SAVE], HUE_TAPE_SET_1, pressurePlateFactoryFactory(2, 1, HSL_AREA_1_BLOCKS)), 317 | 'D': tapeFactoryFactory([INSTRUCTION_ID_DOWN,INSTRUCTION_ID_DOWN,INSTRUCTION_ID_DOWN,INSTRUCTION_ID_DOWN], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_LEFT)), 318 | 'P': persistentEntityFactoryFactory(platformFactoryFactory(1, 1, EDGE_TOP, HUE_TAPE_SET_3), PERSISTENT_ID_TUTORIAL_END_3), 319 | 'h': platformFactoryFactory(1, 1, EDGE_RIGHT, HUE_TAPE_SET_2), 320 | 'H': tapeFactoryFactory([INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_2, pressurePlateFactoryFactory(2, 1, HSL_AREA_1_BLOCKS)), 321 | 'S': persistentEntityFactoryFactory(platformFactoryFactory(2, 1, EDGE_TOP, HUE_TAPE_SET_4), PERSISTENT_ID_TUTORIAL_END_2), 322 | }, 323 | 'yyyyyyy yyyy' + 324 | 'yha ydyI yyy ' + 325 | 'y y y yy yy y ' + 326 | ' yPy' + 327 | ' Z y' + 328 | 'yyyH U yyyS y' + 329 | 'y yyy yy' + 330 | 'ym yyy y' + 331 | 'yyyyy y' + 332 | ' y yyyyyyyyyyyy' + 333 | 'y D y' + 334 | 'yyyyyy y' + 335 | 'yyyyyyyy yyyyyyyy', 336 | MAX_TILES_ACROSS, 337 | BACKGROUND_AREA_1, 338 | ), 339 | // 4, 1 340 | legendRoomFactory( 341 | {...baseLegend, 342 | '1': tapeFactoryFactory([INSTRUCTION_ID_COUNT_3, INSTRUCTION_ID_COUNT_0, INSTRUCTION_ID_DOWN], HUE_TAPE_SET_5, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_BOTTOM)), 343 | 't': tapeFactoryFactory([INSTRUCTION_ID_COUNT_1, INSTRUCTION_ID_COUNT_0, INSTRUCTION_ID_EJECT], HUE_TAPE_SET_5), 344 | 'T': tapeFactoryFactory([INSTRUCTION_ID_LEFT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_DOWN, INSTRUCTION_ID_DROP], HUE_TAPE_SET_3), 345 | }, 346 | 'yyyyyyyyyyyyyyyyyy' + 347 | ' y 1y' + 348 | ' t y y' + 349 | 'yyyy yyyyyyyyy y' + 350 | 'y yyy y' + 351 | 'y 1yy y y' + 352 | 'y yyy y' + 353 | 'y yyyyyy y' + 354 | 'y yyyyyy y' + 355 | 'y yyyyyy y' + 356 | 'y yyyyyyyy y' + 357 | 'yT yyyyyyyy y' + 358 | 'yyy yyyyyyyy y', 359 | MAX_TILES_ACROSS, 360 | BACKGROUND_AREA_1, 361 | ), 362 | 363 | ], 364 | [ 365 | // 0, 2 366 | , 367 | // 1, 2 368 | legendRoomFactory( 369 | {...baseLegend, 370 | 'Y': blockFactoryFactory(HSL_AREA_1_BLOCKS, 1, .8, .6), 371 | '0': tapeFactoryFactory([INSTRUCTION_ID_UP, INSTRUCTION_ID_UP, INSTRUCTION_ID_UP,,, INSTRUCTION_ID_DOWN, INSTRUCTION_ID_DOWN, INSTRUCTION_ID_DOWN], HUE_TAPE_SET_1, repeaterFactoryFactory(HUE_TAPE_SET_5)), 372 | '1': tapeFactoryFactory([INSTRUCTION_ID_DOWN, INSTRUCTION_ID_DOWN, INSTRUCTION_ID_DOWN,INSTRUCTION_ID_UP, INSTRUCTION_ID_UP, INSTRUCTION_ID_UP], HUE_TAPE_SET_2, repeaterFactoryFactory(HUE_TAPE_SET_5)), 373 | '2': platformFactoryFactory(2, .5, EDGE_LEFT, HUE_TAPE_SET_2), 374 | '3': platformFactoryFactory(2, .5, EDGE_RIGHT, HUE_TAPE_SET_1), 375 | '4': platformFactoryFactory(1, 4, EDGE_TOP, HUE_TAPE_SET_1), 376 | '5': platformFactoryFactory(1, 4, EDGE_BOTTOM, HUE_TAPE_SET_2), 377 | '6': compositeEntityFactoryFactory([tapeFactoryFactory([INSTRUCTION_ID_LEFT, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_LEFT,INSTRUCTION_ID_LEFT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_RIGHT,INSTRUCTION_ID_RIGHT,,], HUE_TAPE_SET_1, repeaterFactoryFactory(HUE_TAPE_SET_5)), spikeFactory]), 378 | '7': compositeEntityFactoryFactory([tapeFactoryFactory([INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_RIGHT, INSTRUCTION_ID_RIGHT,, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_LEFT, INSTRUCTION_ID_LEFT], HUE_TAPE_SET_2, repeaterFactoryFactory(HUE_TAPE_SET_5)), spikeFactory]), 379 | }, 380 | 'y yyyyyyyyyyyyyy' + 381 | 'yy y 5 5 5 5 y' + 382 | 'y yy 1 y' + 383 | 'y yy ' + 384 | 'y y y 4 4 4 yyyy' + 385 | 'y y y' + 386 | 'y4 yy y' + 387 | 'y y0 y' + 388 | 'y YYYYYYYYYYyy y' + 389 | 'y 2 y' + 390 | 'yy2 3 y' + 391 | 'y7vvvvvvvvvvvvvv6y' + 392 | 'yyyyyyyyyyyyyyyyyy', 393 | MAX_TILES_ACROSS, 394 | BACKGROUND_AREA_2, 395 | ), 396 | // 2, 2 397 | legendRoomFactory( 398 | {...baseLegend, 399 | 'M': tapeFactoryFactory([INSTRUCTION_ID_SAVE], HUE_TAPE_SET_1, pressurePlateFactoryFactory(2, 1, HSL_AREA_1_BLOCKS)), 400 | 'R': gunFactoryFactory(robotFactoryFactory(ORIENTATION_RIGHT, HUE_TAPE_SET_1)), 401 | 'd': platformFactoryFactory(1, 2, EDGE_TOP, HUE_TAPE_SET_1), 402 | 'D': tapeFactoryFactory([INSTRUCTION_ID_UP, INSTRUCTION_ID_COUNT_6, INSTRUCTION_ID_SHOOT, INSTRUCTION_ID_DOWN], HUE_TAPE_SET_1, pressurePlateFactoryFactory(2, 1, HSL_AREA_1_BLOCKS)), 403 | 'c': tapeFactoryFactory([INSTRUCTION_ID_COUNT_1], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_RIGHT)), 404 | }, 405 | 'yy yyyyyyyyyyyyyy' + 406 | 'yy ' + 407 | 'yyyyyyyyyyyyyyyyyy' + 408 | ' yy yy' + 409 | 'yy y y' + 410 | 'y my yyy y' + 411 | 'y yyM yy y y' + 412 | 'y yyyyyy y yy' + 413 | 'yy yy y ' + 414 | 'y yc yy ' + 415 | 'y yyyyyyyyyyyd yy' + 416 | 'y R yy' + 417 | 'yyyyyyyyyyyyyyD yy', 418 | MAX_TILES_ACROSS, 419 | BACKGROUND_AREA_2, 420 | ), 421 | // 3, 2 422 | legendRoomFactory( 423 | {...baseLegend, 424 | '1': tapeFactoryFactory([,INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_5, repeaterFactoryFactory(HUE_TAPE_SET_5)), 425 | '2': tapeFactoryFactory([INSTRUCTION_ID_LEFT,], HUE_TAPE_SET_5, repeaterFactoryFactory(HUE_TAPE_SET_5)), 426 | 'a': platformFactoryFactory(1, 2, EDGE_BOTTOM, HUE_TAPE_SET_1), 427 | 'A': tapeFactoryFactory([INSTRUCTION_ID_UP], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_BOTTOM)), 428 | '3': tapeFactoryFactory([INSTRUCTION_ID_DOWN], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS)), 429 | 'b': platformFactoryFactory(1, 2, EDGE_BOTTOM, HUE_TAPE_SET_2), 430 | 'B': tapeFactoryFactory([INSTRUCTION_ID_UP], HUE_TAPE_SET_2, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_BOTTOM)), 431 | '4': tapeFactoryFactory([INSTRUCTION_ID_DOWN], HUE_TAPE_SET_2, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS)), 432 | 'c': persistentEntityFactoryFactory(platformFactoryFactory(1, 2, EDGE_BOTTOM, HUE_TAPE_SET_4), PERSISTENT_ID_LEFT_RIGHT_ROOM_1), 433 | 'd': persistentEntityFactoryFactory(platformFactoryFactory(1, 2, EDGE_BOTTOM, HUE_TAPE_SET_3), PERSISTENT_ID_LEFT_RIGHT_ROOM_2), 434 | 'R': compositeEntityFactoryFactory([robotFactoryFactory(ORIENTATION_RIGHT, HUE_TAPE_SET_5), spikeFactory]), 435 | }, 436 | 'yyyyyyyy yyyyyyyy' + 437 | ' 2c d1 ' + 438 | 'y yby yy yay ' + 439 | 'y y y y y y' + 440 | 'y yyyy4 3yyyy y' + 441 | 'y y BA y y' + 442 | 'y y' + 443 | 'yByyy yyyy yyyAy' + 444 | ' yy yy ' + 445 | ' ' + 446 | 'yy yvvvvRvvvvvy yy' + 447 | 'y yyyyyyyyyyyy y' + 448 | 'y3yyyyyyyyyyyyyy4y', 449 | MAX_TILES_ACROSS, 450 | BACKGROUND_AREA_2, 451 | ), 452 | // 4, 2 453 | legendRoomFactory( 454 | {...baseLegend, 455 | 'a': gunFactoryFactory(robotFactoryFactory(ORIENTATION_LEFT, HUE_TAPE_SET_1)), 456 | 'A': tapeFactoryFactory([INSTRUCTION_ID_LEFT, INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS)), 457 | '1': tapeFactoryFactory([INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_1, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_RIGHT)), 458 | 'b': gunFactoryFactory(robotFactoryFactory(ORIENTATION_LEFT, HUE_TAPE_SET_2)), 459 | 'B': tapeFactoryFactory([INSTRUCTION_ID_COUNT_6, INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_2, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS)), 460 | '2': tapeFactoryFactory([INSTRUCTION_ID_LEFT], HUE_TAPE_SET_2, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_BOTTOM)), 461 | 'c': gunFactoryFactory(robotFactoryFactory(ORIENTATION_LEFT, HUE_TAPE_SET_3)), 462 | 'C': tapeFactoryFactory([INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_3, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS)), 463 | '3': tapeFactoryFactory([INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_3, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_RIGHT)), 464 | 'Q': tapeFactoryFactory([INSTRUCTION_ID_LEFT], HUE_TAPE_SET_3, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_BOTTOM)), 465 | 'd': gunFactoryFactory(robotFactoryFactory(ORIENTATION_LEFT, HUE_TAPE_SET_4)), 466 | 'D': tapeFactoryFactory([INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_4, pressurePlateFactoryFactory(2, 1, HSL_AREA_1_BLOCKS)), 467 | '4': tapeFactoryFactory([INSTRUCTION_ID_SHOOT], HUE_TAPE_SET_4, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_RIGHT)), 468 | '$': tapeFactoryFactory([INSTRUCTION_ID_LEFT], HUE_TAPE_SET_4, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_BOTTOM)), 469 | 'e': persistentEntityFactoryFactory(platformFactoryFactory(1, 6, EDGE_BOTTOM, HUE_TAPE_SET_5), PERSISTENT_ID_GUN_ROOM_1), 470 | 'f': platformFactoryFactory(1, 1, EDGE_BOTTOM, HUE_TAPE_SET_5), 471 | 'g': persistentEntityFactoryFactory(platformFactoryFactory(1, 7, EDGE_BOTTOM, HUE_TAPE_SET_5), PERSISTENT_ID_GUN_ROOM_2), 472 | 'E': tapeFactoryFactory([INSTRUCTION_ID_COUNT_2, INSTRUCTION_ID_COUNT_0, INSTRUCTION_ID_UP,,,], HUE_TAPE_SET_5, pressurePlateFactoryFactory(2, 1, HSL_AREA_1_BLOCKS)), 473 | 'l': persistentEntityFactoryFactory(platformFactoryFactory(4, 1, EDGE_RIGHT, HUE_TAPE_SET_5), PERSISTENT_ID_GUN_ROOM_3), 474 | 'k': platformFactoryFactory(1, 1, EDGE_RIGHT, HUE_TAPE_SET_5), 475 | 'L': tapeFactoryFactory([INSTRUCTION_ID_COUNT_6, INSTRUCTION_ID_RIGHT], HUE_TAPE_SET_5, pressurePlateFactoryFactory(1, 1, HSL_AREA_1_BLOCKS, EDGE_BOTTOM)), 476 | 'm': mainframeFactoryFactory(HUE_TAPE_SET_5), 477 | 'M': tapeFactoryFactory([INSTRUCTION_ID_SAVE], HUE_TAPE_SET_5, pressurePlateFactoryFactory(2, 1, HSL_AREA_1_BLOCKS)), 478 | }, 479 | 'yyy eyy$2Qyyyg y' + 480 | ' y y y' + 481 | 'l Ly ay' + 482 | 'y1 fy' + 483 | 'yyy A b y' + 484 | 'yyk B yyy' + 485 | 'yyy4 c y' + 486 | 'yyy yyy yyyyy' + 487 | ' y3 dy' + 488 | ' y yyyy' + 489 | 'y yyy4 yy y' + 490 | 'y E my' + 491 | 'yyM yyyyyyyyyyyyyy', 492 | MAX_TILES_ACROSS, 493 | BACKGROUND_AREA_2, 494 | ), 495 | ], 496 | ]; 497 | // NOTE: the x/y s are reversed 498 | const result: RoomFactoryMetadata = { 499 | factory: (rx: number, ry: number, id: IdFactory) => { 500 | const factory = roomFactories[ry][rx]; 501 | return factory && factory(rx, ry, id); 502 | }, 503 | worldWidth: 5, 504 | worldHeight: 5, 505 | } 506 | return result; 507 | } 508 | 509 | const legendRoomFactory = (legend: Legend, roomDefinition: string, width: number, background: HSL[] = [[0, 0, 30]]) => { 510 | const adjustedWidth = width + 1; 511 | const height = ((roomDefinition.length / width) | 0) + 1; 512 | const halfBlockFactory = blockFactoryFactory(HSL_AREA_0_BLOCKS); 513 | return (x: number, y: number, id: IdFactory) => { 514 | const room: Room = { 515 | allEntities: [], 516 | bounds: [x, y, adjustedWidth, height], 517 | gravity: DEFAULT_GRAVITY, 518 | tiles: array2DCreate(adjustedWidth, height, () => []), 519 | updatableEntities: [], 520 | soundWaves: [], 521 | bg: background, 522 | }; 523 | roomDefinition.split('').forEach((c, i) => { 524 | const tx = i % width + 1; 525 | const ty = ((i / width) | 0) + 1; 526 | const entityFactory = legend[c]; 527 | if (entityFactory) { 528 | const entities = entityFactory(tx, ty, id); 529 | entities.forEach(entity => { 530 | // adjust 531 | const everyEntity = entity as EveryEntity; 532 | if (everyEntity.gravityMultiplier && everyEntity.mass) { 533 | everyEntity.bounds[1] -= MAX_ROUNDING_ERROR_SIZE; 534 | } 535 | roomAddEntity(room, entity); 536 | if (ty == 1 && (entity.entityType == ENTITY_TYPE_BLOCK || entity.entityType == ENTITY_TYPE_PRESSURE_PLATE)) { 537 | // want to add in a half block at the top to stop the player from walking around on the roof 538 | const halfBlock = halfBlockFactory(tx, 0, id)[0] 539 | halfBlock.bounds[1] -= .5; 540 | roomAddEntity(room, halfBlock); 541 | } 542 | }); 543 | } 544 | }); 545 | // also extract any entities that are persistent and in this room 546 | for (let pid = 1; pid < MAX_PERSISTENT_ID_PLUS_1; pid++) { 547 | const json = localStorage.getItem(pid as any); 548 | if (json) { 549 | const [roomX, roomY, entity] = JSON.parse(json) as RoomAndEntity; 550 | if (roomX == x && roomY == y) { 551 | // TODO zero out some stuff on this entity 552 | roomAddEntity(room, entity); 553 | } 554 | } 555 | } 556 | 557 | return room; 558 | } 559 | }; --------------------------------------------------------------------------------