├── src ├── modules │ ├── GameObjects │ │ ├── index.ts │ │ ├── GameObjectsContainer.ts │ │ ├── Light.ts │ │ ├── Rectangle.ts │ │ ├── GameObject.ts │ │ ├── QuadTreeContainer.ts │ │ ├── Bullet.ts │ │ ├── Bomb.ts │ │ ├── mixins.ts │ │ ├── Humanoid.ts │ │ ├── Item.ts │ │ ├── PauseMenu.ts │ │ ├── TextModule.ts │ │ ├── Enemy.ts │ │ └── Player.ts │ ├── Color │ │ ├── Image.ts │ │ ├── Texture.ts │ │ ├── EmojiUtils.ts │ │ ├── Ground.ts │ │ └── Sprite.ts │ ├── Audio │ │ ├── helper.ts │ │ ├── Song.ts │ │ ├── AudioManager.ts │ │ ├── AudioEffect.ts │ │ └── AudioTrack.ts │ ├── Renderer │ │ ├── Renderer.ts │ │ └── Renderer2d.ts │ ├── constants │ │ └── tags.ts │ ├── Assets │ │ ├── EmojiAlternatives.ts │ │ └── Emojis.ts │ ├── Camera │ │ └── index.ts │ ├── Scene │ │ ├── Scene.ts │ │ ├── LabScene.ts │ │ ├── HellScene.ts │ │ └── CementeryScene.ts │ ├── Interruptor │ │ └── Interruptor.ts │ ├── Controller │ │ └── KeyboardController.ts │ ├── Primitives │ │ ├── index.spec.ts │ │ └── index.ts │ └── Game │ │ └── index.ts ├── utils │ ├── colors.ts │ ├── functional.ts │ ├── lightIntesity.ts │ ├── math.ts │ └── QuadTree.ts ├── sprite.png ├── game.ts └── index.html ├── .gitignore ├── game.zip ├── docs ├── mac.png ├── win.png └── ubuntu.png ├── jest.config.js ├── .gitlab-ci.yml ├── package.json ├── README.md ├── scripts └── rename-classes.ts └── tsconfig.json /src/modules/GameObjects/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | export const TRANSPARENT = '#0000' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache/ 2 | dist/ 3 | node_modules/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /game.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-sphere/gravepassing/HEAD/game.zip -------------------------------------------------------------------------------- /src/utils/functional.ts: -------------------------------------------------------------------------------- 1 | export const sum = (a: number, b: number) => a + b; -------------------------------------------------------------------------------- /docs/mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-sphere/gravepassing/HEAD/docs/mac.png -------------------------------------------------------------------------------- /docs/win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-sphere/gravepassing/HEAD/docs/win.png -------------------------------------------------------------------------------- /docs/ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-sphere/gravepassing/HEAD/docs/ubuntu.png -------------------------------------------------------------------------------- /src/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-sphere/gravepassing/HEAD/src/sprite.png -------------------------------------------------------------------------------- /src/modules/Color/Image.ts: -------------------------------------------------------------------------------- 1 | // FIXME: move to constants or sth. 2 | export const SIZE = 16; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /src/game.ts: -------------------------------------------------------------------------------- 1 | import { Game } from "./modules/Game"; 2 | const gameObject = document.querySelector('#game')!; 3 | const game = new Game(gameObject, 10, 10); 4 | game.start(); -------------------------------------------------------------------------------- /src/modules/Audio/helper.ts: -------------------------------------------------------------------------------- 1 | // Helper to have .setValueAtTime only once in the code. 2 | export const sV = (p: AudioParam, v: number, t: number) => p.setValueAtTime(v, t); 3 | export const sR = (p: AudioParam, v: number, t: number) => p.linearRampToValueAtTime(v, t); -------------------------------------------------------------------------------- /src/modules/Renderer/Renderer.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from "../Camera"; 2 | import { Game } from "../Game"; 3 | import { GameObjectsContainer } from "../GameObjects/GameObjectsContainer"; 4 | 5 | export interface Renderer { 6 | render(camera: Camera, gameObjects: GameObjectsContainer, dt: number, game: Game): void; 7 | } -------------------------------------------------------------------------------- /src/modules/Color/Texture.ts: -------------------------------------------------------------------------------- 1 | export abstract class NewTexture { 2 | abstract render(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): void; 3 | optimise(ctx: CanvasRenderingContext2D) { 4 | createImageBitmap(ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)) 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/lightIntesity.ts: -------------------------------------------------------------------------------- 1 | import { Light } from "../modules/GameObjects/Light"; 2 | import { Point } from "../modules/Primitives"; 3 | import { sum } from './functional'; 4 | 5 | export const lightIntensityAtPoint = (p: Point, lights: Light[]) => { 6 | return Math.min(1, lights.map(l => l.getIntensityAtPoint(p)).reduce(sum, 0)); 7 | } -------------------------------------------------------------------------------- /src/modules/constants/tags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Originally those tags were used. Now the are inlined so it compresses better. Still, leaving it here so you can check what each of them references. 3 | */ 4 | export const enum TAG { 5 | PLAYER = "p", 6 | ENEMY = "e", 7 | INTERACTIVE = "i", 8 | LIGHT = "l", 9 | GENERATED = "g" 10 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 5 | Gravepassing | by Kacper "kulak" Kula for js13kgames 2022 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/modules/GameObjects/GameObjectsContainer.ts: -------------------------------------------------------------------------------- 1 | import { Rectangle } from "../Primitives"; 2 | import { GameObject } from "./GameObject"; 3 | 4 | export interface GameObjectsContainer { 5 | getObjectsInArea(rect: Rectangle, t?: string): GameObject[]; 6 | add(obj: GameObject): void; 7 | 8 | remove(obj: GameObject): void; 9 | 10 | getAll(): GameObject[]; 11 | 12 | update(): void; 13 | } -------------------------------------------------------------------------------- /src/modules/Audio/Song.ts: -------------------------------------------------------------------------------- 1 | import { AudioTrack } from "./AudioTrack"; 2 | 3 | export class Song { 4 | ctx!: AudioContext; 5 | constructor(public tracks: AudioTrack[]) { 6 | } 7 | 8 | play() { 9 | this.ctx = new window.AudioContext(); 10 | this.tracks.forEach(t => t.start(this.ctx)); 11 | this.ctx.resume(); 12 | } 13 | 14 | stop() { 15 | this.tracks.forEach(t => t.stop()); 16 | } 17 | } -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:17 2 | 3 | stages: 4 | - build 5 | - pages 6 | 7 | project_build: 8 | stage: build 9 | script: 10 | - yarn install 11 | - yarn build 12 | - mv dist public 13 | artifacts: 14 | paths: 15 | - public 16 | cache: 17 | paths: 18 | - node_modules 19 | pages: 20 | stage: pages 21 | script: 22 | - ls public 23 | only: 24 | - main 25 | artifacts: 26 | paths: 27 | - public 28 | -------------------------------------------------------------------------------- /src/modules/Audio/AudioManager.ts: -------------------------------------------------------------------------------- 1 | import { BombAudioEffect, CollectedAudioEffect, EnemyKilledAudioEffect, IntroAudioEffect, ShotAudioEffect } from "./AudioEffect"; 2 | 3 | export class AudioManager { 4 | static instance?: AudioManager 5 | static get() { 6 | if (!this.instance) { 7 | this.instance = new AudioManager(); 8 | } 9 | return this.instance; 10 | } 11 | shot = new ShotAudioEffect(); 12 | bomb = new BombAudioEffect(); 13 | collect = new CollectedAudioEffect(); 14 | killed = new EnemyKilledAudioEffect(); 15 | intro = new IntroAudioEffect(); 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game2022", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "devDependencies": { 6 | "@types/jest": "^28.1.6", 7 | "parcel": "^2.6.2", 8 | "roadroller": "^2.1.0", 9 | "ts-jest": "^28.0.7", 10 | "ts-node": "^10.9.1", 11 | "typescript": "^4.7.4", 12 | "uglify-js": "^3.17.0" 13 | }, 14 | "scripts": { 15 | "start": "parcel src/index.html", 16 | "build": "parcel build src/index.html --no-source-maps --public-url ./", 17 | "uglify": "uglifyjs --compress --mangle --no-annotations --toplevel -o dist/ugly.js dist/index.*.js", 18 | "rename": "ts-node --compiler-options '{\"module\": \"commonjs\"}' scripts/rename-classes.ts", 19 | "test": "jest", 20 | "run-roadroller": "roadroller dist/ugly.js -o dist/s.js", 21 | "serve": "serve dist" 22 | }, 23 | "dependencies": { 24 | "jest": "^28.1.3", 25 | "ts-morph": "^15.1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/Assets/EmojiAlternatives.ts: -------------------------------------------------------------------------------- 1 | interface OptionalEmojiSet { 2 | // EMOJI 3 | e?: string; 4 | // POS 5 | pos?: [number, number]; 6 | // SIZE 7 | size?: number; 8 | } 9 | 10 | export const alt: Record = { 11 | "🪦": { e: "⚰️", pos: [0, 4], size: .9}, 12 | "⛓": { e: "👖" }, 13 | "🪨": { e: "💀" }, 14 | "🪵": { e: "🌳" }, 15 | "🦴": { e: "💀" } 16 | } 17 | 18 | 19 | export const win: Record = { 20 | "🔥": { pos: [1, -1], size: 1 }, 21 | "💣": { pos: [-1, -2]}, 22 | "👱": { pos: [-1, 0]}, 23 | "🕶": { size: 1.5, pos: [-1, -1]}, 24 | "⬛️": { pos: [-1, 0]}, 25 | "👖": { pos: [-0.5, 0]}, 26 | "🐷": { pos: [-1, 0]}, 27 | "🦋": { pos: [-1, 0]}, 28 | "🐮": { pos: [-1, 0]}, 29 | "👔": { pos: [-1, 0]}, 30 | "👩": { pos: [-1, 0]}, 31 | "🤖": { pos: [-1, 0]}, 32 | "👚": { pos: [-1, 0]}, 33 | "🐵": { pos: [-1, 0]}, 34 | "☢️": { pos: [5, 0]}, 35 | "👹": { pos: [-2, 1]} 36 | } 37 | 38 | export const tux: Record = { 39 | "🧧": { e: "👔" } 40 | } -------------------------------------------------------------------------------- /src/modules/Camera/index.ts: -------------------------------------------------------------------------------- 1 | import { WithCenter } from "../GameObjects/mixins"; 2 | import { Line, Point, Rectangle } from "../Primitives"; 3 | 4 | export class Camera { 5 | constructor(private ctx: CanvasRenderingContext2D) {} 6 | private following?: WithCenter; 7 | 8 | public prevCamera?: Point; 9 | 10 | get center() { 11 | if (!this.following) { 12 | return Point.ORIGIN; 13 | } 14 | let p = Point.ORIGIN; 15 | if (this.prevCamera) { 16 | p = this.prevCamera; 17 | } 18 | let c = this.following.center.mul(1/9, 1/7).round().mul(9, 7).add(0.5, 2); 19 | 20 | // Rounding camera to the grid 21 | const m = (new Line(p, c)).getMidpoint(0.1).snapToGrid(); 22 | this.prevCamera = m; 23 | return m; 24 | } 25 | 26 | get rawCenter() { 27 | if (!this.following) { 28 | return Point.ORIGIN; 29 | } 30 | return this.following.center.mul(1/9, 1/7).round().mul(9, 7).add(0.5, 2); 31 | } 32 | 33 | follow(go: WithCenter) { 34 | this.following = go; 35 | } 36 | } -------------------------------------------------------------------------------- /src/modules/Scene/Scene.ts: -------------------------------------------------------------------------------- 1 | import { Directional } from "../Assets/Emojis"; 2 | import { Ground } from "../Color/Ground"; 3 | import { Dither } from "../Color/Sprite"; 4 | import { Game } from "../Game"; 5 | import { GameObject } from "../GameObjects/GameObject"; 6 | import { GameObjectsContainer } from "../GameObjects/GameObjectsContainer"; 7 | import { Point } from "../Primitives"; 8 | 9 | interface Stage { 10 | lvl: number, 11 | res: (game: Game) => void; 12 | } 13 | 14 | // This can be removed probably? 15 | export interface SceneSettings { 16 | backgroundColor: string; 17 | ground: Ground; 18 | hudBackground: string; 19 | getDither: (n: number) => Dither; 20 | pCenter: Point; 21 | stages: Stage[]; 22 | enemies: Directional[]; 23 | } 24 | 25 | export abstract class Scene { 26 | 27 | stopMusic() { 28 | 29 | } 30 | protected gameObjects: Set = new Set(); 31 | constructor() { 32 | 33 | } 34 | 35 | addObjects(game: Game) { 36 | 37 | } 38 | 39 | register(container: GameObjectsContainer, game: Game): SceneSettings { 40 | this.addObjects(game); 41 | this.gameObjects.forEach(g => container.add(g)); 42 | return { 43 | backgroundColor: "hsla(173,39%,47%)", 44 | ground: new Ground([], 123), 45 | hudBackground: 'orange', 46 | getDither: Dither.generateDithers(32), 47 | pCenter: Point.ORIGIN, 48 | stages: [], 49 | enemies: [], 50 | } 51 | } 52 | 53 | unregister(container: GameObjectsContainer) { 54 | this.gameObjects.forEach(g => container.remove(g)); 55 | } 56 | } -------------------------------------------------------------------------------- /src/modules/Color/EmojiUtils.ts: -------------------------------------------------------------------------------- 1 | import { alt, tux, win } from "../Assets/EmojiAlternatives"; 2 | import { EmojiSet } from "./Sprite"; 3 | 4 | let emojiCanvas: HTMLCanvasElement; 5 | let emojiCtx: CanvasRenderingContext2D; 6 | 7 | const isKeyOf = (k: string, r: Record): k is T => { 8 | return k in r; 9 | } 10 | 11 | export const isEmojiRendering = (emoji: string) => { 12 | if (!emojiCanvas) { 13 | emojiCanvas = document.createElement('canvas'); 14 | emojiCanvas.width = emojiCanvas.height = 20; 15 | emojiCtx = emojiCanvas.getContext('2d')!; 16 | emojiCtx.font = '20px Arial'; 17 | } 18 | emojiCtx.clearRect(0, 0, 20, 20); 19 | emojiCtx.fillText(emoji, 0, 0); 20 | const data = emojiCtx.getImageData(0, 0, 20, 20); 21 | return data.data.findIndex(e => e > 0) > 0; 22 | } 23 | 24 | export const convertEmoji = (e: EmojiSet) => { 25 | if (isKeyOf(e.e, alt) && !isEmojiRendering(e.e)) { 26 | const pos = alt[e.e].pos || [0,0]; 27 | e = { 28 | ...e, 29 | e: alt[e.e].e || e.e, 30 | pos: [e.pos[0] + pos[0], e.pos[1]+ pos[1]], 31 | size: (alt[e.e].size || 1) * e.size 32 | } 33 | } 34 | if (navigator.platform.startsWith("Win") && isKeyOf(e.e,win)) { 35 | const pos = win[e.e].pos || [0,0]; 36 | e = { 37 | ...e, 38 | e: win[e.e].e || e.e, 39 | pos: [e.pos[0] + pos[0], e.pos[1]+ pos[1]], 40 | size: (win[e.e].size || 1) * e.size, 41 | } 42 | } 43 | if (navigator.platform.indexOf('Linux') >= 0 && isKeyOf(e.e, tux)) { 44 | e = { 45 | ...e, 46 | e: tux[e.e].e || e.e, 47 | pos: tux[e.e].pos || e.pos, 48 | size: (tux[e.e].size || 1) * e.size, 49 | } 50 | } 51 | return e; 52 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/Light.ts: -------------------------------------------------------------------------------- 1 | import { distance } from "../../utils/math"; 2 | import { Point, Rectangle } from "../Primitives"; 3 | import { GameObject, Renderable } from "./GameObject"; 4 | import { EmptyClass, WithCenter, withTags } from "./mixins"; 5 | 6 | export class Light extends withTags(EmptyClass) implements GameObject, WithCenter, Renderable { 7 | zIndex = 1; 8 | protected _center: Point; 9 | isHidden = false; 10 | 11 | get center() { 12 | return this._center; 13 | } 14 | 15 | set center(v: Point) { 16 | this._center = v; 17 | } 18 | constructor(center: Point, public intensity: number, public distance: number, public color: string = "#FFF") { 19 | super(); 20 | this._center = center; 21 | this._tags.push("l"); 22 | } 23 | render(ctx: CanvasRenderingContext2D, gameBB: Rectangle): void { 24 | // empty. 25 | } 26 | parentBBExclude: boolean = false; 27 | getBoundingBox(): Rectangle { 28 | return new Rectangle( 29 | this.center.add(-this.distance, -this.distance), 30 | this.center.add(this.distance, this.distance), 31 | ); 32 | } 33 | 34 | getIntensityAtPoint(p: Point): number { 35 | const d = distance(this.center, p); 36 | if (d > this.distance) { 37 | return 0; 38 | } 39 | return this.intensity * (1 - (d / this.distance)*(d / this.distance)); 40 | } 41 | 42 | update(t: number) {} 43 | 44 | isGlobal = false; 45 | } 46 | 47 | 48 | // FIXME: instead of this we can have minimal light set in dither. 49 | export class AmbientLight extends Light { 50 | constructor(intensity: number) { 51 | super(Point.ORIGIN, intensity, Infinity); 52 | } 53 | 54 | getIntensityAtPoint(p: Point): number { 55 | return this.intensity; 56 | } 57 | 58 | isGlobal = true; 59 | } -------------------------------------------------------------------------------- /src/modules/Interruptor/Interruptor.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardController } from "../Controller/KeyboardController"; 2 | import { Game } from "../Game"; 3 | import { GameObject, GetPosFn } from "../GameObjects/GameObject"; 4 | import { Rectangle } from "../Primitives"; 5 | 6 | export interface Interruptable { 7 | onResolution?(fn: () => void): void; 8 | start(controller: KeyboardController, game: Game): void; 9 | hasEnded: boolean; 10 | 11 | // should take same update like regular game objects. 12 | } 13 | 14 | export type InterGO = Interruptable & GameObject; 15 | 16 | export class Interruptor { 17 | isRunning = false; 18 | private _inters: InterGO[] = []; 19 | add(inter: InterGO) { 20 | // FIXME: priority problem? maybe you can't pause during "cutscenes" or "dialogues"? 21 | this._inters.push(inter); 22 | } 23 | 24 | isActive: boolean = false; 25 | 26 | update(controller: KeyboardController, game: Game) { 27 | if (!this.isRunning && this._inters.length) { 28 | // we should start the first one 29 | this.isRunning = true; 30 | const inter = this._inters[0]; 31 | inter.start(controller, game); 32 | } 33 | 34 | if (this.isRunning) { 35 | const inter = this._inters[0]; 36 | 37 | // should we be the only ones who send the timer updates? 38 | 39 | if (inter.hasEnded) { 40 | this._inters = this._inters.slice(1); 41 | this.isRunning = false; 42 | this.update(controller, game); 43 | } 44 | } 45 | } 46 | 47 | updateInter(dt: number) { 48 | this.isRunning && this._inters[0].update(dt, null!); 49 | } 50 | 51 | render(ctx: CanvasRenderingContext2D, bb: Rectangle, fn: GetPosFn) { 52 | this.isRunning && this._inters[0].render(ctx, bb, fn); 53 | } 54 | } -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | import { Line, Point } from "../modules/Primitives" 2 | 3 | export const ELIPSON = 0.0001; 4 | 5 | export const rnd = Math.random 6 | 7 | export const distance = (p1: Point, p2: Point): number => { 8 | return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); 9 | } 10 | 11 | export const getLinesIntersectionInternal = (l1: Line, l2: Line): Point => { 12 | const denominator = (l1.p1.x - l1.p2.x) * (l2.p1.y - l2.p2.y) - (l1.p1.y - l1.p2.y) * (l2.p1.x - l2.p2.x); 13 | const newX = 14 | ((l1.p1.x * l1.p2.y - l1.p1.y * l1.p2.x) * (l2.p1.x - l2.p2.x) - 15 | (l1.p1.x - l1.p2.x) * (l2.p1.x * l2.p2.y - l2.p1.y * l2.p2.x)) / denominator; 16 | 17 | const newY = 18 | ((l1.p1.x * l1.p2.y - l1.p1.y * l1.p2.x) * (l2.p1.y - l2.p2.y) - 19 | (l1.p1.y - l1.p2.y) * (l2.p1.x * l2.p2.y - l2.p1.y * l2.p2.x) 20 | ) / denominator; 21 | return new Point(newX, newY); 22 | } 23 | 24 | export const getLinesIntersection = (l1: Line, l2: Line): Point | null => { 25 | if (!l1 || !l2) { 26 | return null; 27 | } 28 | const p = getLinesIntersectionInternal(l1, l2); 29 | if (isPointOnLine(p, l1) && isPointOnLine(p, l2)) { 30 | return p; 31 | } 32 | return null; 33 | } 34 | 35 | export const isPointOnLine = (p: Point, l: Line): boolean => { 36 | return distance(l.p1, p) + distance(l.p2, p) - distance(l.p1, l.p2) < ELIPSON; 37 | } 38 | 39 | const normaliseAngle = (a: number) => { 40 | return (a + 2 * Math.PI) % (2 * Math.PI); 41 | } 42 | 43 | export const getAngularDistance = (target: number, compare: number) => { 44 | return ((target - compare) + 2 * Math.PI) % (2 * Math.PI); 45 | } 46 | 47 | export const isAngleInRange = (angle: number, minAngle: number, maxAngle: number) => { 48 | const a = normaliseAngle(angle); 49 | const a1 = normaliseAngle(minAngle); 50 | const a2 = normaliseAngle(maxAngle); 51 | return a >= Math.min(a1, a2) && a <= Math.max(a1, a2); 52 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/Rectangle.ts: -------------------------------------------------------------------------------- 1 | import { DirectionableTexture } from "../Color/Sprite"; 2 | import { NewTexture } from "../Color/Texture"; 3 | import { Point, Rectangle } from "../Primitives"; 4 | import { GameObject, GetPosFn, Renderable } from "./GameObject"; 5 | import { GameObjectsContainer } from "./GameObjectsContainer"; 6 | import { EmptyClass, withTags } from "./mixins"; 7 | 8 | export class RectangleObject extends withTags(EmptyClass) implements GameObject, Renderable { 9 | 10 | public rectangle: Rectangle; 11 | 12 | constructor(public p: Point, public texture: NewTexture, tag?: string|string[], scale = 1) { 13 | super(); 14 | this.rectangle = new Rectangle(p.add(scale, -scale), p); 15 | if (tag) { 16 | if(Array.isArray(tag)) { 17 | tag.forEach(t => this._tags.push(t)); 18 | } else { 19 | this._tags.push(tag); 20 | } 21 | } 22 | } 23 | render(ctx: CanvasRenderingContext2D, gameBB: Rectangle, getPosOnScreen: GetPosFn): void { 24 | if (!gameBB) { 25 | return; 26 | } 27 | if (this.isHidden) { 28 | return; 29 | } 30 | const r = this.getBoundingBox(); 31 | ctx.beginPath(); 32 | 33 | let p1 = getPosOnScreen(r.p1); 34 | let p2 = getPosOnScreen(r.p2); 35 | 36 | if (this.isGlobal) { 37 | // Displaying in screenc coordinates 38 | p1 = getPosOnScreen(gameBB.p1.add(r.p1.x, r.p1.y)); 39 | p2 = getPosOnScreen(gameBB.p1.add(r.p2.x, r.p2.y)); 40 | } 41 | 42 | this.texture.render(ctx, ...p1, p2[0]-p1[0], p2[1]-p1[1]); 43 | } 44 | isHidden: boolean = false; 45 | parentBBExclude: boolean = false; 46 | zIndex?: number | undefined; 47 | update(dt: number, container: GameObjectsContainer): void { 48 | 49 | } 50 | 51 | getBoundingBox(): Rectangle { 52 | return this.rectangle; 53 | } 54 | isGlobal: boolean = false; 55 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/GameObject.ts: -------------------------------------------------------------------------------- 1 | import { Point, Rectangle } from "../Primitives"; 2 | import { GameObjectsContainer } from "./GameObjectsContainer"; 3 | import { WithCenter } from "./mixins"; 4 | 5 | export type GetPosFn = (p:Point) => [number, number]; 6 | 7 | export interface GameObject { 8 | parentBBExclude: boolean; 9 | getTags(): string[]; 10 | hasTag(t: string): boolean; 11 | update(dt: number, container: GameObjectsContainer): void; 12 | 13 | getBoundingBox(): Rectangle; 14 | 15 | isGlobal: boolean; 16 | isHidden: boolean; 17 | zIndex?: number; 18 | render(ctx: CanvasRenderingContext2D, gameBB: Rectangle, getPosOnScreen: GetPosFn): void; 19 | } 20 | 21 | export class GameObjectGroup implements GameObject, WithCenter { 22 | parentBBExclude: boolean = false; 23 | isHidden = false; 24 | center!: Point; 25 | private _tags: string[] = []; 26 | update(dt: number, container: GameObjectsContainer): void {} 27 | getBoundingBox(): Rectangle { 28 | const boxes = this.objects 29 | .filter(o => !o.parentBBExclude).map(x => x.getBoundingBox()); 30 | if (!boxes.length) { 31 | return Rectangle.fromPoint(this.center); 32 | } 33 | return boxes.reduce((a, b) => Rectangle.boundingBox(a, b)); 34 | } 35 | zIndex?: number | undefined; 36 | private objects: GameObject[] = []; 37 | add(go: GameObject) { 38 | this.objects.push(go); 39 | return go; 40 | } 41 | 42 | getAll(): GameObject[] { 43 | return this.objects; 44 | } 45 | 46 | remove(go: GameObject) { 47 | this.objects = this.objects.filter(o => o !== go); 48 | } 49 | 50 | protected addTag(t: string) { 51 | this._tags.push(t); 52 | } 53 | 54 | getTags(): string[] { 55 | return [...this._tags]; 56 | } 57 | 58 | hasTag(t: string): boolean { 59 | return !!this._tags.find(tag => tag === t); 60 | } 61 | 62 | isGlobal: boolean = false; 63 | 64 | render(ctx: CanvasRenderingContext2D, gameBB: Rectangle) { 65 | // FIXME: RENDER GROUP PROPERLY? 66 | } 67 | } 68 | 69 | export interface Renderable { 70 | getBoundingBox(): Rectangle; 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gravepassing 🪦 2 | 3 | This is repository for Gravepassing - retro-style browser-based video game written in TypeScript for js13kgames 2022 competition. 4 | 5 | ## Build 🛠 6 | [Build available here](./game.zip). 7 | 8 | 11628 / 13312 B = 87.35% 9 | 10 | ## Story 📚 11 | You’re alone in a graveyard, hounded by armies of the undead. Can you escape death and rescue all of humanity? 12 | 13 | Set to 8-bit music, Gravepassing evokes Game Boy aesthetics with a 16x16 base tile grid, downscaled emojis and dithered ligthing for that overall retro vibe. 14 | 15 | You can play the game with a keyboard or controller. 16 | 17 | CONTROLS 18 | 19 | Keyboard: 20 | Move = [Arrow Keys] 21 | Fire = [Space] 22 | Drop bomb = [X] 23 | Menu = [ESC] 24 | 25 | Controller: 26 | Move = left joystick 27 | Fire = (A) 28 | Drop bomb = (B) 29 | Menu = (Options) / (Menu) 30 | 31 | The game should work on any operating system in Chrome / Firefox but it was primarily designed for MacOS emojis and looks the best in there. It adapts to other platforms too, you can say those are platform exclusive versions ;) 32 | 33 | Written in TypeScript. 34 | 35 | ## Screenshots 🖼 36 | ### MacOS 37 | ![Game Screenshot](./docs/mac.png) 38 | ### Windows 39 | ![Game Screenshot](./docs/win.png) 40 | ### Ubuntu 41 | ![Game Screenshot](./docs/ubuntu.png) 42 | 43 | ## Features ✨ 44 | - No assets - everything is built with Emojis! 🤪 45 | - 16x16 tile grid system 46 | - Evokes GameBoy Color / early handheld era vibe 47 | - 8-bit(ish) music 48 | - Dither effect for lighting 49 | - Written in TypeScript 50 | - 3 difficulty levels (changed in the menu accessible via ESC key) 51 | - Ability to turn off postprocessing effects 52 | - 3 stages with unique vibe and soundtrack 53 | - "Exclusive" platform versions - game renders in native system emojis (big chunky outline on Windows, beautiful MacOS emojis and in-between version on Ubuntu) 54 | 55 | Tested on MacOS (looks the best), Windows 7 and Ubuntu. 56 | 57 | ## Build process 📦 58 | - Custom script to rename classes / methods / properties 59 | - Parcel production mode 60 | - Uglify 61 | - Roadroller (❤️) 62 | - advzip 63 | 64 | ## Authors and attributions 🐙 65 | Made by [Kacper "kulak" Kula](https://twitter.com/kulak_at). 66 | 67 | Big thanks to Rae for ideas, brain storming, testing and improvements! -------------------------------------------------------------------------------- /src/modules/Audio/AudioEffect.ts: -------------------------------------------------------------------------------- 1 | import { sR, sV } from "./helper"; 2 | 3 | export class AudioEffect { 4 | ctx: AudioContext; 5 | osc: OscillatorNode; 6 | constructor() { 7 | this.ctx = new window.AudioContext(); 8 | this.osc = this.ctx.createOscillator(); 9 | const gain = this.ctx.createGain(); 10 | gain.gain.setValueAtTime(0.2, 0); 11 | this.osc.connect(gain); 12 | gain.connect(this.ctx.destination); 13 | this.setup(); 14 | } 15 | 16 | setup() { 17 | const {o,f} = this; 18 | o.type = 'triangle'; 19 | sV(f, 0, 0); 20 | o.start(); 21 | } 22 | 23 | play(...params: any[]) { 24 | } 25 | 26 | get f() { 27 | return this.o.frequency; 28 | } 29 | get o() { 30 | return this.osc; 31 | } 32 | 33 | get t() { 34 | return this.ctx.currentTime; 35 | } 36 | } 37 | 38 | export class ShotAudioEffect extends AudioEffect { 39 | play(): void { 40 | const {f, t} = this; 41 | sV(f, 200, t); 42 | sV(f, 400, t+.05); 43 | sR(f, 500, t+.06); 44 | sV(f, 0, t+.1); 45 | } 46 | } 47 | 48 | export class BombAudioEffect extends AudioEffect { 49 | play(explode: boolean) { 50 | const { f, o, t} = this; 51 | o.type = 'triangle' 52 | sV(f, 150,t) 53 | sR(f, 100,t+.13); 54 | if (explode) { 55 | sR(f, 20, t+.4); 56 | sV(f, 0,t+.5); 57 | } else { 58 | sR(f, 0,t+.131) 59 | } 60 | } 61 | } 62 | 63 | export class CollectedAudioEffect extends AudioEffect { 64 | play() { 65 | const {t,o,f} = this; 66 | o.type = 'square'; 67 | sV(f, 880, t); 68 | sV(f, 1760, t+.1); 69 | sV(f, 0, t+.2); 70 | } 71 | } 72 | 73 | export class EnemyKilledAudioEffect extends AudioEffect { 74 | play() { 75 | const {t,o,f} = this; 76 | o.type = 'square'; 77 | sV(f, 700, t); 78 | sV(f, 400, t+.05); 79 | sV(f, 700, t+.1); 80 | sV(f, 0, t+.2); 81 | } 82 | } 83 | 84 | export class IntroAudioEffect extends AudioEffect { 85 | play() { 86 | const {t,o,f} = this; 87 | o.type = 'sine'; 88 | sV(f,220,t); 89 | sV(f, 0,t+.15); 90 | sV(f, 440,t+.2); 91 | sV(f, 880, t+0.35); 92 | sV(f, 0, t+.5); 93 | } 94 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/QuadTreeContainer.ts: -------------------------------------------------------------------------------- 1 | import { QuadTree } from "../../utils/QuadTree"; 2 | import { Point, Rectangle } from "../Primitives"; 3 | import { GameObject, GameObjectGroup } from "./GameObject"; 4 | import { GameObjectsContainer } from "./GameObjectsContainer"; 5 | 6 | export class QuadTreeContainer implements GameObjectsContainer { 7 | 8 | private objects: Set = new Set(); 9 | private globalObjects: Set = new Set(); 10 | private quadTree!: QuadTree; 11 | private limit = 40; 12 | 13 | constructor() { 14 | this.update(); 15 | } 16 | 17 | getBoundaries() { 18 | const p1 = Point.ORIGIN; 19 | const p2 = Point.ORIGIN; 20 | this.objects.forEach(obj => { 21 | const bb = obj.getBoundingBox(); 22 | p1.x = Math.min(p1.x, bb.p1.x); 23 | p1.y = Math.min(p1.y, bb.p1.y); 24 | p2.x = Math.max(p2.x, bb.p2.x); 25 | p2.y = Math.max(p2.y, bb.p2.y); 26 | }); 27 | return new Rectangle(p1, p2); 28 | } 29 | 30 | update() { 31 | const bb = this.getBoundaries(); 32 | this.quadTree = new QuadTree(bb, this.limit); 33 | this.objects.forEach(o => this.quadTree.add(o)); 34 | } 35 | 36 | getObjectsInArea(rectangle: Rectangle, t: string | undefined = undefined): GameObject[] { 37 | const obj = [...this.globalObjects, ...this.quadTree.getInArea(rectangle)]; 38 | if (t) { 39 | return obj.filter(o => o.hasTag(t)); 40 | } 41 | return obj; 42 | } 43 | add(obj: GameObject) { 44 | if (obj instanceof GameObjectGroup) { 45 | obj.getAll().forEach(o => this.add(o)); 46 | // return; 47 | } 48 | if (obj.isGlobal) { 49 | this.globalObjects.add(obj); 50 | return; 51 | } 52 | this.objects.add(obj); 53 | this.quadTree && this.quadTree.add(obj); 54 | } 55 | 56 | remove(obj: GameObject): void { 57 | if (obj instanceof GameObjectGroup) { 58 | obj.getAll().forEach(o => this.remove(o)); 59 | } 60 | this.globalObjects.delete(obj); 61 | this.objects.delete(obj); 62 | this.update(); 63 | } 64 | 65 | getAll(): GameObject[] { 66 | return [...this.globalObjects, ...this.objects]; 67 | } 68 | 69 | get tree() { 70 | return this.quadTree; 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/modules/Controller/KeyboardController.ts: -------------------------------------------------------------------------------- 1 | 2 | // FIXME: REWORK IT. 3 | type ValueOf = T[keyof T]; 4 | const keys = { 5 | 'ArrowUp': "u", 6 | 'ArrowDown': "d", 7 | 'ArrowLeft': "l", 8 | 'ArrowRight': "r", 9 | ' ': 'a', 10 | 'x': 'b', 11 | 'Escape': 'e' 12 | } as const; 13 | 14 | const isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? 1 : 0; 15 | 16 | type T = Record, number>; 17 | 18 | export class KeyboardController { 19 | _v: T = Object.values(keys).reduce((a,b) => ({...a, [b]: 0}), {}) as unknown as T; 20 | 21 | get v() { 22 | const gp = navigator.getGamepads(); 23 | if (gp && gp.length > 0) { 24 | this.gamepad = gp[0]!; 25 | } else { 26 | this.gamepad = undefined; 27 | } 28 | if (this.gamepad && this.gamepad.connected) { 29 | const b = this.gamepad.buttons; 30 | const a = this.gamepad.axes; 31 | const v = { 32 | a: b[0+isFF].pressed, 33 | b: b[1+isFF].pressed, 34 | e: b[9+isFF].pressed, 35 | u: a[1] < -0.2 ? -1 : 0, 36 | d: a[1] > 0.2 ? 1 : 0, 37 | l: a[0] < -0.2 ? -1 : 0, 38 | r: a[0] > 0.2 ? 1 : 0, 39 | } 40 | return v; 41 | } 42 | return this._v; 43 | } 44 | 45 | gamepad?: Gamepad; 46 | 47 | constructor() { 48 | document.addEventListener('keydown', e => { 49 | if (keys[e.key as keyof typeof keys]) { 50 | this.v[keys[e.key as keyof typeof keys] as ValueOf] = 1; 51 | e.preventDefault(); 52 | } 53 | }); 54 | document.addEventListener('keyup', e => { 55 | if (keys[e.key as keyof typeof keys]) { 56 | this.v[keys[e.key as keyof typeof keys] as ValueOf] = 0; 57 | e.preventDefault(); 58 | } 59 | }); 60 | } 61 | 62 | vibrate(s = 0.5, w = 0.3, d = 100) { 63 | if (this.gamepad) { 64 | try { 65 | (this.gamepad as any).vibrationActuator.playEffect('dual-rumble', { 66 | duration: d, 67 | strongMagnitude: s, 68 | weakMagnitude: w 69 | }) 70 | } catch (e) { } 71 | } 72 | } 73 | 74 | get x() { 75 | return this.v.r ? 1 : this.v.l ? -1 : 0; 76 | } 77 | 78 | get y() { 79 | return this.v.u ? -1 : this.v.d ? 1 : 0; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/modules/GameObjects/Bullet.ts: -------------------------------------------------------------------------------- 1 | import { E } from "../Assets/Emojis"; 2 | import { AudioManager } from "../Audio/AudioManager"; 3 | import { Point, Rectangle } from "../Primitives"; 4 | import { GameObjectGroup } from "./GameObject"; 5 | import { GameObjectsContainer } from "./GameObjectsContainer"; 6 | import { SimpleHumanoid } from "./Humanoid"; 7 | import { withMovement } from "./mixins"; 8 | import { RectangleObject } from "./Rectangle"; 9 | 10 | const BULLET_SPEED = 0.02; 11 | 12 | type Callback = (t: SimpleHumanoid) => void; 13 | 14 | export class UsableItem extends withMovement(GameObjectGroup) { 15 | private _cb: Callback[] = []; 16 | onHit(cb: Callback) { 17 | this._cb.push(cb); 18 | } 19 | protected hit(t: SimpleHumanoid) { 20 | this._cb.forEach(c => c(t)); 21 | } 22 | 23 | } 24 | 25 | export class Bullet extends UsableItem { 26 | private o: RectangleObject; 27 | constructor(p: Point, private direction: Point, private lifeSpan = 100, private targetTag: string) { 28 | super(); 29 | this.center = p; 30 | this.o = new RectangleObject(p, E.bullet); 31 | this.add(this.o); 32 | 33 | // FIXME: remove duplication 34 | this.o.rectangle.moveTo(this.center); 35 | AudioManager.get().shot.play(); 36 | } 37 | 38 | getBoundingBox(): Rectangle { 39 | const bb = super.getBoundingBox().scale(1/3, 1/3); 40 | return bb.moveBy(new Point(bb.width, bb.height)); 41 | } 42 | 43 | update(dt: number, container: GameObjectsContainer): void { 44 | 45 | // Check collision 46 | const enemiesHit = container 47 | .getObjectsInArea(this.getBoundingBox(), this.targetTag) 48 | .filter(obj => obj.getBoundingBox().isIntersectingRectangle(this.o.getBoundingBox())); // FIXME: this can be optimised 49 | if (enemiesHit.length) { 50 | // hit only first 51 | const enemy = enemiesHit[0]; 52 | if (enemy instanceof SimpleHumanoid) { 53 | enemy.getHit(container); 54 | this.hit(enemy); 55 | if (enemy.life <= 0) { 56 | container.remove(enemiesHit[0]); 57 | } 58 | this.lifeSpan = 0; 59 | } 60 | } 61 | const m = this.move(dt, this.direction, BULLET_SPEED, container); 62 | this.lifeSpan -= dt; 63 | if (this.lifeSpan <= 0 || !m) { 64 | // DIE. 65 | container.remove(this); 66 | } 67 | this.o.rectangle.moveTo(this.center); 68 | } 69 | 70 | isHidden = false; 71 | } -------------------------------------------------------------------------------- /src/utils/QuadTree.ts: -------------------------------------------------------------------------------- 1 | import { GameObject } from "../modules/GameObjects/GameObject"; 2 | import { Rectangle } from "../modules/Primitives"; 3 | 4 | export class QuadTree { 5 | objects: Set = new Set(); 6 | subtrees: QuadTree[] = []; 7 | constructor(private _boundary: Rectangle, private limit: number = 10) { 8 | } 9 | 10 | get boundary() { 11 | return this._boundary; 12 | } 13 | 14 | get subTrees() { 15 | return this.subtrees; 16 | } 17 | 18 | private subdivide() { 19 | const p1 = this.boundary.p1; 20 | const p2 = this.boundary.p2; 21 | const mid = this.boundary.center; 22 | const w = this.boundary.width; 23 | const h = this.boundary.height; 24 | this.subtrees = [ 25 | new QuadTree(new Rectangle(p1, mid), this.limit), 26 | new QuadTree(new Rectangle(p1.add(w/2, 0), mid.add(w/2, 0)), this.limit), 27 | new QuadTree(new Rectangle(p1.add(0, h/2), mid.add(0, h/2)), this.limit), 28 | new QuadTree(new Rectangle(p1.add(w/2, h/2), mid.add(w/2, h/2)), this.limit), 29 | ]; 30 | 31 | // we need to add all exisitng points now 32 | this.objects.forEach(o => { 33 | this.subtrees.forEach(t => 34 | t.add(o) 35 | ) 36 | }) 37 | } 38 | 39 | add(obj: GameObject) { 40 | if (!this.boundary.isIntersectingRectangle(obj.getBoundingBox())) { 41 | return; 42 | } 43 | 44 | if (this.objects.size + 1 < this.limit || this.boundary.width < 10 || this.boundary.height < 10) { 45 | this.objects.add(obj); 46 | } else { 47 | if (!this.subtrees.length) { 48 | this.subdivide(); 49 | } 50 | this.subtrees.forEach(t => { 51 | t.add(obj); 52 | }); 53 | } 54 | } 55 | 56 | getInArea(boundary: Rectangle): Set { 57 | if (!this.boundary.isIntersectingRectangle(boundary)) { 58 | return new Set(); 59 | } 60 | if (this.subtrees.length) { 61 | const s = new Set(); 62 | for (const tree of this.subTrees) { 63 | tree.getInArea(boundary).forEach(obj => s.add(obj)); 64 | } 65 | return s; 66 | } 67 | const points = new Set(); 68 | this.objects.forEach(obj => { 69 | if (boundary.isIntersectingRectangle(obj.getBoundingBox())) { 70 | points.add(obj); 71 | } 72 | }); 73 | return points; 74 | } 75 | } -------------------------------------------------------------------------------- /src/modules/Primitives/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { Line, Point, Rectangle } from "."; 2 | 3 | describe("Primitives", () => { 4 | describe("Point", () => { 5 | it("should properly instantiate point", () => { 6 | const point = new Point(50, 30); 7 | expect(point.x).toEqual(50); 8 | expect(point.y).toEqual(30); 9 | }) 10 | }); 11 | 12 | describe("Line", () => { 13 | it("should properly instantiate line", () => { 14 | const line = new Line(Point.ORIGIN, new Point(40, 40)); 15 | expect(line.p1).toEqual(Point.ORIGIN); 16 | expect(line.p2).toEqual(Point.ORIGIN.add(40, 40)); 17 | expect(line.length).toBeCloseTo(56.5685); 18 | }); 19 | }); 20 | 21 | describe("Rectangle", () => { 22 | it("should properly instantiate rectangle", () => { 23 | const r = new Rectangle(Point.ORIGIN, Point.ORIGIN.add(50, 40)); 24 | expect(r.p1).toEqual(Point.ORIGIN); 25 | expect(r.p2.x).toEqual(50); 26 | expect(r.p2.y).toEqual(40); 27 | }); 28 | 29 | it("should properly change points if neccessary", () => { 30 | const r = new Rectangle(Point.ORIGIN, Point.ORIGIN.add(-50, 30)); 31 | expect(r.p1).toEqual(new Point(-50, 0)); 32 | expect(r.p2).toEqual(new Point(0, 30)); 33 | }); 34 | 35 | it("should intersect properly", () => { 36 | const r = new Rectangle(Point.ORIGIN, Point.ORIGIN.add(50, 50)); 37 | const r2 = new Rectangle(new Point(-1000, -1000), new Point(1000, 1000)); 38 | expect(r2.isIntersectingRectangle(r)).toBeTruthy(); 39 | }); 40 | 41 | it("should intersect properly 2", () => { 42 | const r1 = new Rectangle(new Point(-75, -43.1), new Point(75.46, 75.46)); 43 | const r2 = new Rectangle(new Point(-10, -50), new Point(10, 50)); 44 | expect(r1.isIntersectingRectangle(r2)).toBeTruthy(); 45 | expect(r2.isIntersectingRectangle(r1)).toBeTruthy(); 46 | }); 47 | 48 | it("should intersect properly 3", () => { 49 | const r1 = new Rectangle( 50 | Point.ORIGIN, 51 | Point.ORIGIN.add(-35, 125)); 52 | 53 | const r2 = new Rectangle( 54 | new Point(-3.999, 124.999), new Point(-35.01, 125.01) 55 | ); 56 | 57 | expect(r1.isIntersectingRectangle(r2)).toBeTruthy(); 58 | expect(r2.isIntersectingRectangle(r1)).toBeTruthy(); 59 | 60 | expect(r1.isIntersectingRectangle(r1)).toBeTruthy(); 61 | }); 62 | }) 63 | }) -------------------------------------------------------------------------------- /src/modules/GameObjects/Bomb.ts: -------------------------------------------------------------------------------- 1 | import { getLinesIntersection } from "../../utils/math"; 2 | import { AudioManager } from "../Audio/AudioManager"; 3 | import { Emoji } from "../Color/Sprite"; 4 | import { Line, Point, Rectangle } from "../Primitives"; 5 | import { UsableItem } from "./Bullet"; 6 | import { GameObjectsContainer } from "./GameObjectsContainer"; 7 | import { SimpleHumanoid } from "./Humanoid"; 8 | import { Light } from "./Light"; 9 | import { RectangleObject } from "./Rectangle"; 10 | 11 | export class Bomb extends UsableItem { 12 | isHidden = false; 13 | private o: RectangleObject; 14 | explosionTime = 900; 15 | exploded = false; 16 | constructor(p: Point, private lifeSpan = 1000, private targetTag: string) { 17 | super(); 18 | this.center = p; 19 | this.o = new RectangleObject(p, new Emoji("💣", 8, 1, 4, 4)); 20 | this.add(this.o); 21 | 22 | // FIXME: remove duplication 23 | this.o.rectangle.moveTo(this.center); 24 | AudioManager.get().bomb.play(false); 25 | } 26 | 27 | update(dt: number, container: GameObjectsContainer): void { 28 | 29 | // Check collision 30 | this.lifeSpan -= dt; 31 | if (this.lifeSpan <= 0 && !this.exploded) { 32 | this.exploded = true; 33 | // EXPLODE. 34 | AudioManager.get().bomb.play(true); 35 | 36 | const l = new Light( 37 | this.center, 38 | 0.3, 39 | 3, 40 | "#FF0") 41 | // Add light and remove bomb sprite. 42 | container.remove(this.o); 43 | this.add(l); 44 | container.add(l); 45 | 46 | // KILLING HERE. 47 | const bb = l.getBoundingBox(); 48 | const obst = container.getObjectsInArea(bb,"o").map(o => o.getBoundingBox().toLines()).flat(); 49 | container.getObjectsInArea(bb, this.targetTag).forEach(target => { 50 | if (target instanceof SimpleHumanoid) { 51 | // CHECK IF TARGET IS REACHABLE. 52 | const line = new Line(this.center, target.center); 53 | if (!obst.find(o => !!getLinesIntersection(o, line))) { 54 | target.getHit(container, 2); 55 | if (target.life <= 0) { 56 | container.remove(target); 57 | this.hit(target); 58 | } 59 | } 60 | } 61 | }) 62 | } 63 | if (this.lifeSpan + this.explosionTime <= 0) { 64 | container.remove(this); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/mixins.ts: -------------------------------------------------------------------------------- 1 | import { getLinesIntersection } from "../../utils/math"; 2 | import { Line, Point, Rectangle } from "../Primitives"; 3 | import { GameObject } from "./GameObject"; 4 | import { GameObjectsContainer } from "./GameObjectsContainer"; 5 | 6 | export class EmptyClass {} 7 | 8 | export type Constructable = new (...args: any[]) => T; 9 | 10 | type MethodsToOmit = 'update' | 'getBoundingBox' | 'isGlobal' | 'render'; 11 | 12 | export function withTags(constructor: T) { 13 | return class extends constructor implements Omit { 14 | parentBBExclude: boolean = false; 15 | isHidden = false; 16 | protected _tags: string[] = []; 17 | getTags(): string[] { 18 | return this._tags; 19 | } 20 | hasTag(t: string): boolean { 21 | return this._tags.findIndex(x => x === t) >= 0; 22 | } 23 | } 24 | } 25 | 26 | export interface WithCenter { 27 | center: Point; 28 | } 29 | 30 | export interface WithObstacles { 31 | get obstacles(): GameObject[]; 32 | } 33 | 34 | interface WithFeetBox { 35 | getFeetBox(): Rectangle; 36 | } 37 | 38 | const isWithFitBox = (t: any): t is WithFeetBox => { 39 | return !!t.getFeetBox; 40 | } 41 | 42 | interface Movable { 43 | move(dt: number, direction: Point, speed: number, container: GameObjectsContainer): void; 44 | } 45 | 46 | export function withMovement>(constructor: T) { 47 | return class extends constructor implements Movable { 48 | move(dt: number, direction: Point, speed: number, container: GameObjectsContainer): boolean { 49 | let distance = direction.mul(dt * speed); 50 | let line = new Line(this.center, this.center.addVec(distance)); 51 | let shortened = false; 52 | 53 | 54 | let bb = this.getBoundingBox(); 55 | if (isWithFitBox(this)) { 56 | // FIXME: expand one way or another. 57 | bb = this.getFeetBox().expand(0.01) 58 | } 59 | 60 | // SIMULATE MOVING BB BY LINE 61 | 62 | const combined = bb.moveBy(line.toPoint()); 63 | 64 | // FIXME: FIX STUCK ENEMIES HERE 65 | const stuck = !!container.getObjectsInArea(bb, "o").length; 66 | if (stuck) { 67 | // moving slightly left 68 | this.center = this.center.add(0.05, 0); 69 | } 70 | 71 | const obstacles = container.getObjectsInArea(combined, "o").map(o => o.getBoundingBox().toLines()).flat(); 72 | 73 | if(obstacles.length) { 74 | // push back just slightly so the user does not intersect anymore 75 | // this.center = this.center.copy().addVec(direction.copy().neg().mul(dt*speed*3)); 76 | return false; 77 | } 78 | 79 | for (let ob of obstacles) { 80 | const i = getLinesIntersection(line, ob); 81 | if (i) { 82 | line.p2 = i; 83 | shortened = true; 84 | } 85 | } 86 | this.center = this.center.addVec(line.getMidpoint(shortened ? 0 : 1).diffVec(line.p1)); //snapToGrid(); 87 | return true; 88 | } 89 | 90 | } 91 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/Humanoid.ts: -------------------------------------------------------------------------------- 1 | import { Directional } from "../Assets/Emojis"; 2 | import { DirectionableTexture } from "../Color/Sprite"; 3 | import { Point, Rectangle } from "../Primitives"; 4 | import { GameObject, GameObjectGroup } from "./GameObject"; 5 | import { GameObjectsContainer } from "./GameObjectsContainer"; 6 | import { Light } from "./Light"; 7 | import { withMovement } from "./mixins"; 8 | import { RectangleObject } from "./Rectangle"; 9 | 10 | class SimpleHumanoidPref extends GameObjectGroup { 11 | center: Point; 12 | protected sprite: RectangleObject; 13 | protected light: Light; 14 | 15 | // public xp: number = 0; 16 | protected fireCooldown: number = 0; 17 | constructor(d: Directional, distance: number = 4, intensity = 0.5) { 18 | super(); 19 | this.center = new Point(0, 0); 20 | 21 | this.sprite = new RectangleObject(this.center, new DirectionableTexture(d), [], d.l.scale); 22 | 23 | 24 | this.light = new Light( 25 | this.center, 26 | intensity, 27 | distance 28 | ) 29 | this.light.parentBBExclude = true; 30 | 31 | this.add(this.sprite); 32 | this.add(this.light); 33 | } 34 | zIndex?: number | undefined; 35 | 36 | private _obstacles: GameObject[] = []; 37 | get obstacles(): GameObject[] { 38 | return this._obstacles; 39 | } 40 | 41 | 42 | // getRenderInstructions(): Renderable[] { 43 | // return [ 44 | // ] 45 | // } 46 | 47 | update(dt: number, container: GameObjectsContainer) { 48 | // Move movement functions here. 49 | } 50 | 51 | getBoundingBox(): Rectangle { 52 | const bb = super.getBoundingBox(); 53 | const w = bb.width; 54 | return bb.moveBy(new Point(w / 3, 0)).scale(1/3, 1) 55 | } 56 | 57 | 58 | public lastX: number = 0; 59 | public lastY: number = 1; 60 | 61 | } 62 | 63 | export class SimpleHumanoid extends withMovement(SimpleHumanoidPref) { 64 | value = 0; 65 | move(dt: number, direction: Point, speed: number, container: GameObjectsContainer): boolean { 66 | const moved = super.move(dt, direction, speed, container); 67 | this.sprite.rectangle.moveTo(this.center); 68 | if (direction.x || direction.y) { 69 | this.lastX = direction.x; 70 | this.lastY = direction.y; 71 | } 72 | let d = 'down'; 73 | if (this.lastY !== 0) { 74 | d = this.lastY < 0 ? 'up' : 'down' 75 | } else { 76 | d = this.lastX < 0 ? 'left' : 'right'; 77 | // ((this.go.texture as DirectionableTexture).left as Sprite).flip = this.lastX > 0; 78 | 79 | } 80 | (this.sprite.texture as DirectionableTexture).setDirection( 81 | d, direction.distanceFromOrigin() 82 | ); 83 | this.light.center = this.sprite.rectangle.center; 84 | 85 | return moved; 86 | } 87 | 88 | die(container: GameObjectsContainer) { 89 | 90 | } 91 | 92 | life: number = 5; 93 | 94 | getHit(container: GameObjectsContainer, amount: number = 1) { 95 | this.life-=amount; 96 | if (this.life <= 0) { 97 | this.die(container); 98 | } 99 | } 100 | 101 | isGlobal = false; 102 | 103 | } -------------------------------------------------------------------------------- /src/modules/Scene/LabScene.ts: -------------------------------------------------------------------------------- 1 | import { E } from "../Assets/Emojis"; 2 | import { AudioTrack } from "../Audio/AudioTrack"; 3 | import { Song } from "../Audio/Song"; 4 | import { Ground } from "../Color/Ground"; 5 | import { Dither, Emoji } from "../Color/Sprite"; 6 | import { Game } from "../Game"; 7 | import { GameObjectsContainer } from "../GameObjects/GameObjectsContainer"; 8 | import { HellPortal } from "../GameObjects/Item"; 9 | import { AmbientLight } from "../GameObjects/Light"; 10 | import { TextModal } from "../GameObjects/TextModule"; 11 | import { Point } from "../Primitives"; 12 | import { Scene, SceneSettings } from "./Scene"; 13 | 14 | const bpm = 60; 15 | 16 | const T = { 17 | NOTE: () => new TextModal(["Looks like some scientist opened a portal to hell.","","Sound familiar?"]), 18 | NOTE_2: () => new TextModal(["The gate to hell is near."]), 19 | } 20 | 21 | export class LabScene extends Scene { 22 | song = new Song([ 23 | new AudioTrack(bpm*2, 0.5, "0 g.interruptorManager.add(T.NOTE())}, 77 | { lvl: 10, res: g => { 78 | g.interruptorManager.add(T.NOTE_2()); 79 | const f = new HellPortal(g.player.center); 80 | g.gameObjects.add(f); 81 | g.setObjective(f); 82 | }} 83 | ], 84 | enemies: [E.robotMan, E.zombie, E.zombieWoman], 85 | }; 86 | } 87 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/Item.ts: -------------------------------------------------------------------------------- 1 | import { rnd } from "../../utils/math"; 2 | import { E } from "../Assets/Emojis"; 3 | import { AudioManager } from "../Audio/AudioManager"; 4 | import { CombinedEmoji, Emoji } from "../Color/Sprite"; 5 | import { Point } from "../Primitives"; 6 | import { HellScene } from "../Scene/HellScene"; 7 | import { LabScene } from "../Scene/LabScene"; 8 | import { GameObjectGroup } from "./GameObject"; 9 | import { GameObjectsContainer } from "./GameObjectsContainer"; 10 | import { Player } from "./Player"; 11 | import { TextModal } from "./TextModule"; 12 | 13 | export class Item extends GameObjectGroup { 14 | isHidden = false; 15 | constructor(public p: Point, icon: CombinedEmoji, scale: number = 1) { 16 | super(); 17 | this.center = p; 18 | this.add(icon.toGameObject(p, scale)); 19 | } 20 | 21 | onAdd(player: Player) { 22 | } 23 | 24 | update(dt: number, container: GameObjectsContainer): void { 25 | // For now assuming only player can collect 26 | const players = container.getObjectsInArea(this.getBoundingBox(), "p"); 27 | if (!players.length) { 28 | return; // not collected 29 | } 30 | const player = players[0]; 31 | this.onAdd(player as unknown as Player); 32 | AudioManager.get().collect.play(); 33 | container.remove(this); 34 | } 35 | } 36 | 37 | 38 | export class LifeCollectableItem extends Item { 39 | constructor(p: Point) { 40 | super(p.add(0, 1), new Emoji("❤️", 4, 1, 4, 10)) 41 | } 42 | 43 | onAdd(player: Player): void { 44 | player.heal(); 45 | } 46 | } 47 | 48 | export class BombCollectableItem extends Item { 49 | constructor(p: Point) { 50 | super(p.add(0, 1), new CombinedEmoji([ 51 | { e: "💣", size: 6, pos: [4, 8] }, 52 | { e: "+", size: 6, pos: [9, 12] }, 53 | ])); 54 | } 55 | 56 | onAdd(player: Player): void { 57 | player.addItem('bomb'); 58 | } 59 | } 60 | 61 | export class Factory extends Item { 62 | constructor(p: Point) { 63 | const point = new Point(rnd(), rnd()).normalize().mul(60, 100); 64 | super(p.addVec(point), E.factory, 3); 65 | this.addTag("g"); // THIS MAKES IT NOT GENERATE ENEMIES AND OTHER THINGS AROUND IT. 66 | } 67 | 68 | onAdd(player: Player) { 69 | player.controller.vibrate(1, .5, 500); 70 | player.game.loadScene(new LabScene, false, true); 71 | } 72 | } 73 | 74 | export class HellPortal extends Item { 75 | constructor(p: Point) { 76 | const point = new Point(rnd(), rnd()).normalize().mul(60, 100); 77 | super(p.addVec(point), E.portal, 3); 78 | } 79 | 80 | onAdd(player: Player): void { 81 | player.controller.vibrate(1, .5, 500); 82 | player.game.loadScene(new HellScene(), false, true); 83 | } 84 | } 85 | 86 | export class SwitchItem extends Item { 87 | constructor(p: Point) { 88 | const point = new Point(rnd(), rnd()).normalize().mul(60, 100); 89 | super(p.addVec(point), E.switch, 3); 90 | this.addTag("g"); // THIS MAKES IT NOT GENERATE ENEMIES AND OTHER THINGS AROUND IT. 91 | } 92 | 93 | onAdd(player: Player): void { 94 | player.game.interruptorManager.add(new TextModal(["You've saved humanity.", "But no one will know since you're trapped here.", "", "Nothing left but to fight hell's hordes forever."])); 95 | player.game.objective = undefined; 96 | } 97 | } -------------------------------------------------------------------------------- /src/modules/Color/Ground.ts: -------------------------------------------------------------------------------- 1 | import { rnd } from "../../utils/math"; 2 | import { Game } from "../Game"; 3 | import { Enemy } from "../GameObjects/Enemy"; 4 | import { RectangleObject } from "../GameObjects/Rectangle"; 5 | import { Point, Rectangle } from "../Primitives"; 6 | import { SceneSettings } from "../Scene/Scene"; 7 | import { SIZE } from "./Image"; 8 | import { Emoji } from "./Sprite"; 9 | 10 | 11 | const FN = (x: number, y: number, S: number) => (Math.sin(432.432*S + x * y - 3*y+Math.cos(x-y))+1)/2; 12 | 13 | export interface EmojiList { 14 | e: Emoji, 15 | range: [number, number], 16 | asGameObject?: boolean, 17 | } 18 | 19 | 20 | export class Ground { 21 | private color = "#49A79C"; 22 | constructor(private emojis: EmojiList[] = [], private seed: number) { 23 | } 24 | render(ctx: CanvasRenderingContext2D, bb: Rectangle, rawBB: Rectangle, s: SceneSettings, game: Game): void { 25 | // Check if there are already generated obstacles in the area. 26 | const areGenerated = !!game.gameObjects.getObjectsInArea(rawBB, "g").length; // we want to generate elements in new area, not the animated one. 27 | 28 | let generatedAnything = false; 29 | const m = SIZE * game.MULTIPLIER; 30 | bb.forEachCell((x, y, oX, oY) => { 31 | const p = FN(x,y, this.seed || 231); 32 | ctx.fillStyle = s.backgroundColor || this.color; 33 | ctx.fillRect(oX*m, oY*m, m, m); 34 | 35 | this.emojis.filter(e => !e.asGameObject).forEach(e => { 36 | if (p > e.range[0] && p < e.range[1]) { 37 | e.e.render(ctx, oX *m, oY *m, m, m); 38 | } 39 | }); 40 | }); 41 | 42 | if (areGenerated) { return } 43 | rawBB.forEachCell((x,y,oX,oY) => { 44 | this.emojis.filter(e => e.asGameObject).forEach(e => { 45 | const p = FN(x,y, this.seed || 231); 46 | if (p > e.range[0] && p < e.range[1]) { 47 | const obj = new RectangleObject(new Point(x, y), e.e, ["g","o"]); // Tag: generated + obstacle 48 | game.gameObjects.add(obj); 49 | generatedAnything = true; 50 | } 51 | }); 52 | }); 53 | 54 | // GENERATE GAME OBJECTS IF NEEDED. 55 | if (generatedAnything) { // making sure we don't generate infinitly enemies on empty patches. 56 | const g = FN(rawBB.p1.x+0.424, rawBB.p1.y+0.2, this.seed+4324); 57 | const generatingNr = Math.round(g * 5); 58 | for(let i=0;i 0) { 70 | if(rnd() < s.difficulty * 0.1) { 71 | lifes++; 72 | } 73 | d-=10; 74 | } 75 | 76 | const p = rawBB.p1.add(rnd()*rawBB.width, rnd()*rawBB.height); 77 | const sprite = game.sceneSettings.enemies[Math.floor(rnd()*game.sceneSettings.enemies.length)]; 78 | game.gameObjects.add(new Enemy( 79 | sprite, 80 | value, p, lifes + (sprite.u.scale > 1 ? 2 : 0), 1500 - game.settings.difficulty * 550)); 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/modules/Audio/AudioTrack.ts: -------------------------------------------------------------------------------- 1 | import { sR, sV } from "./helper"; 2 | 3 | interface SynthConfig { 4 | type: OscillatorType; 5 | cutoff?: number; 6 | cutoffOpenRatio?: number; 7 | cutoffOpenDelay?: number; 8 | delay?: { 9 | time: number, 10 | gain: number, 11 | } 12 | } 13 | export class AudioTrack { 14 | private isStoped: boolean = true; 15 | constructor(public bpm: number, public duration: number, public d: string/* definition*/, public synth: SynthConfig) { 16 | } 17 | 18 | private osc!: OscillatorNode; 19 | private ctx!: AudioContext; 20 | 21 | private filter!: BiquadFilterNode; 22 | 23 | public start(ctx: AudioContext) { 24 | this.ctx = ctx; 25 | this.makeSynth(); 26 | } 27 | 28 | gains: GainNode[] = []; 29 | 30 | stop() { 31 | // FIXME: ramp music. 32 | this.isStoped = true; 33 | this.gains.forEach(g => sR(g.gain, 0, this.ctx.currentTime + 1.5)); 34 | } 35 | 36 | private makeSynth() { 37 | 38 | this.isStoped = false; 39 | const ctx = this.ctx; // new window.AudioContext(); 40 | const osc = ctx.createOscillator(); 41 | const fil = ctx.createBiquadFilter(); 42 | this.filter = fil; 43 | const gain = ctx.createGain(); 44 | this.gains.push(gain); 45 | 46 | const t = ctx.currentTime; 47 | 48 | gain.gain.setValueAtTime(0.1, t); 49 | 50 | fil.type = 'lowpass'; 51 | sV(fil.frequency,this.synth.cutoff || 40000, t); 52 | fil.connect(gain); 53 | gain.connect(ctx.destination); 54 | 55 | osc.type = this.synth.type; 56 | osc.connect(fil); 57 | 58 | if (this.synth.delay) { 59 | const del = ctx.createDelay() 60 | sV(del.delayTime,this.synth.delay.time, t); 61 | fil.connect(del); 62 | const delAten = ctx.createGain(); 63 | del.connect(delAten); 64 | delAten.connect(ctx.destination); 65 | sV(delAten.gain, this.synth.delay.gain, t); 66 | this.gains.push(delAten); 67 | } 68 | 69 | this.osc = osc; 70 | this.ctx = ctx; 71 | this.schedule(); 72 | osc.start(); 73 | 74 | } 75 | 76 | nextStartTime: number = -1; 77 | 78 | schedule() { 79 | if (this.isStoped) { 80 | return; 81 | } 82 | const l = 60 / this.bpm; 83 | let s = this.nextStartTime; 84 | if (s < 0) { 85 | s = this.ctx.currentTime; 86 | } 87 | if (this.synth.cutoff) { 88 | const c = this.synth.cutoff; 89 | const m = this.synth.cutoffOpenRatio || 1; 90 | const d = this.synth.cutoffOpenDelay || 0; 91 | const len = this.d.length*l; 92 | this.filter.frequency.cancelScheduledValues(s); 93 | sV(this.filter.frequency, this.synth.cutoff, s); 94 | this.filter.frequency.setTargetAtTime(c*m, s + d * len, len); 95 | } 96 | for(let i = 0; i < this.d.length; i++) { 97 | const m = this.d.charCodeAt(i); 98 | let hz = Math.pow(2, (m-69)/12)*440; 99 | if (hz > 5000) { 100 | hz = 0; 101 | } 102 | sV(this.osc.frequency, hz, s+l*i); 103 | this.duration < 1 && sV(this.osc.frequency, 0, s+l*i+l*this.duration); 104 | } 105 | this.nextStartTime = s + this.d.length * l; 106 | setTimeout(() => this.schedule(), l * this.d.length * 0.9 * 1000); 107 | } 108 | } -------------------------------------------------------------------------------- /src/modules/Scene/HellScene.ts: -------------------------------------------------------------------------------- 1 | import { E } from "../Assets/Emojis"; 2 | import { AudioTrack } from "../Audio/AudioTrack"; 3 | import { Song } from "../Audio/Song"; 4 | import { Ground } from "../Color/Ground"; 5 | import { Dither, Emoji } from "../Color/Sprite"; 6 | import { Game } from "../Game"; 7 | import { GameObjectsContainer } from "../GameObjects/GameObjectsContainer"; 8 | import { SwitchItem } from "../GameObjects/Item"; 9 | import { AmbientLight } from "../GameObjects/Light"; 10 | import { TextModal } from "../GameObjects/TextModule"; 11 | import { Point } from "../Primitives"; 12 | import { Scene, SceneSettings } from "./Scene"; 13 | 14 | 15 | const TXT = { 16 | NOTE: () => new TextModal(["The master switch will save us all.", "Where is it though?"]), 17 | NOTE_2: () => new TextModal(["A dying zombie directs you to the master switch."]), 18 | } 19 | 20 | const bpm = 60; 21 | const x = '0<<Ϝ0??Ϝ'; 22 | const end = '00::ϜϜϜϜ'; 23 | const track = x+x+end; 24 | const power = '5AAϡ5DDϡ5AAϡ5DDϡ55??ϡϡϡϡ'; 25 | const bass = '$$$￴$$￴￴'; 26 | const T = track + track + track + '0￴0￴￴￴￴￴$$￴" ￴$$'; 27 | const P = power + power + power + '<￴<￴<￴$￴￴￴￴.\x1B￴00'; 28 | const P2x = "AMMϭAPPϭAMMϭAPPϭAAKKϭϭϭϭAMMϭAPPϭAMMϭAPPϭAAKKϭϭϭϭAMMϭAPPϭAMMϭAPPϭAAKKϭϭϭϭH0H0H000000:'0<<" 29 | const P3x = '055ϕ088ϕ055ϕ088ϕ0033ϕϕϕϕ055ϕ088ϕ055ϕ088ϕ0033ϕϕϕϕ055ϕ088ϕ055ϕ088ϕ0033ϕϕϕϕ0│0│0│0││││00│00'; 30 | const P2 = P + P + P2x + P3x; 31 | 32 | export class HellScene extends Scene { 33 | constructor() { 34 | super(); 35 | this.gameObjects.add(new AmbientLight(0.3)); 36 | } 37 | song = new Song([ 38 | new AudioTrack( 39 | bpm*8, 1, 40 | T, { type: 'sawtooth', cutoff: 600, cutoffOpenRatio: 2}, 41 | ), 42 | new AudioTrack( 43 | bpm*8, 1, 44 | P2, { type: 'sawtooth', cutoff: 700, cutoffOpenRatio: 5} 45 | ), 46 | new AudioTrack( 47 | bpm*8, 0.5, bass, 48 | { type: "sawtooth", cutoff: 900, cutoffOpenRatio: 90, cutoffOpenDelay: 0.5 } 49 | ), 50 | new AudioTrack( 51 | bpm*8, 0.5, bass, 52 | { type: "square", cutoff: 500, cutoffOpenRatio: 5, cutoffOpenDelay: 0.5 } 53 | ) 54 | ]); 55 | 56 | stopMusic(): void { 57 | this.song.stop(); 58 | } 59 | 60 | register(container: GameObjectsContainer, game: Game): SceneSettings { 61 | super.register(container, game); 62 | this.song.play(); 63 | return { 64 | ground: new Ground([ 65 | { e: new Emoji("💀", 12, 1), range: [0.999, 1] }, 66 | { e: new Emoji("🍖", 8, 1, 0, 4), range: [0.5, 0.51] }, 67 | { e: new Emoji("🪨", 10, 1, 0, 5), range: [0.2, 0.35], asGameObject: true}, 68 | { e: new Emoji("🗿", 12, 1, 0, 2), range: [0.6, 0.61], asGameObject: true}, 69 | { e: new Emoji("🦴", 12, 1, 0, 2), range: [0.9, 0.92]}, 70 | ], 12.4334), 71 | hudBackground: '#400', 72 | backgroundColor: "rgba(100, 10, 10)", 73 | getDither: Dither.generateDithers(10, [200, 34, 24]), 74 | pCenter: Point.ORIGIN, 75 | stages: [ 76 | { lvl: 12, res: (g => g.interruptorManager.add(TXT.NOTE())), }, 77 | { lvl: 15, res: g => { 78 | g.interruptorManager.add(TXT.NOTE_2()); 79 | const f = new SwitchItem(g.player.center); 80 | g.gameObjects.add(f); 81 | g.setObjective(f); 82 | }}, 83 | ], 84 | enemies: [E.cowMan, E.frogMan, E.pigMan, E.robotMan, E.zombie, E.zombieWoman, E.devil], 85 | }; 86 | } 87 | } -------------------------------------------------------------------------------- /src/modules/Assets/Emojis.ts: -------------------------------------------------------------------------------- 1 | import { SIZE } from "../Color/Image"; 2 | import { AnimatedEmoji, CombinedEmoji, Emoji, EmojiSet } from "../Color/Sprite"; 3 | 4 | const S = 1; 5 | const glasses: EmojiSet = {e: "🕶", pos: [S * 4 + 1, S - 1], size: S * 4, color: "black"}; 6 | const singleGlass: EmojiSet = { e: "⬛️", pos: [S * 5, S * 1], size: S * 1}; 7 | const singleRightGlass: EmojiSet = { e: "⬛️", pos: [8, 1], size: S * 1}; 8 | 9 | export interface Directional { 10 | u: CombinedEmoji; 11 | d: CombinedEmoji; 12 | l: CombinedEmoji; 13 | r: CombinedEmoji; 14 | } 15 | 16 | const renderLegs = (s: number, steps: number, c: HTMLCanvasElement) => { 17 | const ctx = c.getContext('2d')!; 18 | const w = c.width/2; 19 | const h=c.height; 20 | const sc=h/SIZE; 21 | ctx.clearRect(w-sc, h, s/steps <= 0.5 ? -w : w, -h/4*(s/steps)); 22 | if (!s) { 23 | return; 24 | } 25 | // ctx.clearRect(w, h, s/steps <= 0.5 ? -w/3*2 : w, -h/4*(s/steps)) 26 | } 27 | 28 | const createDirectional = (head?: string, body?: string, pants?: string, shirtShift = 0, pantShift = 0, scale: number = 1): Directional => { 29 | const base: EmojiSet[] = [ 30 | { e: pants || "👖", pos: [scale*4, scale*10], size: scale*5, hueShift: pantShift}, 31 | { e: body || "🧧", pos: [scale*4, scale*5], size: scale*5, hueShift: shirtShift }, 32 | { e: head || "👱", pos: [scale*4, 0], size: scale*5 } 33 | ]; 34 | 35 | if (scale > 1) { 36 | const a = new AnimatedEmoji(base, scale, '#fff',10, renderLegs); 37 | return { 38 | u:a, 39 | d:a, 40 | l:a, 41 | r:a 42 | } 43 | } 44 | 45 | return { 46 | u: new AnimatedEmoji(base, scale, "#fff", 10, renderLegs), 47 | d: new AnimatedEmoji([...base, glasses], scale, '#fff', 10, renderLegs), 48 | l: new AnimatedEmoji([...base, singleGlass], scale, '#fff', 10, renderLegs), 49 | r: new AnimatedEmoji([...base, singleRightGlass], scale, '#fff', 10, renderLegs), 50 | } 51 | } 52 | 53 | export const E = { 54 | portal: new CombinedEmoji([ 55 | { e: "✨", size: 15, pos: [0, 5]}, 56 | { e: "✨", size: 15, pos: [15, 15]}, 57 | { e: "✨", size: 15, pos: [5, 35]}, 58 | { e: "🌀", size: 30, pos: [0, 15], hueShift: 50}, 59 | ], 3), 60 | switch: new CombinedEmoji([ 61 | { e: "🔌", size: 30, pos: [0, 15], hueShift: 0}, 62 | ], 3), 63 | factory: new CombinedEmoji([ 64 | { e: "🏢", size: 30, pos: [0, 15]}, 65 | { e: "☢️", size: 8, pos: [10, 24]}, 66 | { e: "🦴", size: 8, pos: [2, 40]}, 67 | { e: "💀", size: 8, pos: [20, 40]}, 68 | { e: "📡", size: 10, pos: [16, 6]} 69 | ], 3), 70 | playerDir: createDirectional(), 71 | 72 | pigMan: createDirectional("🐷", void 0, void 0, 180, 40), 73 | frogMan: createDirectional("🦋", void 0, void 0, 170, 200), 74 | cowMan: createDirectional("🐮", "👔", "👖", 180, 100), 75 | 76 | robotMan: createDirectional("🤖", "👔", "⛓"), 77 | zombieWoman: createDirectional("👩", "👚", "👖", 0, 30), 78 | zombie: createDirectional("🐵", "👔", "🦿"), 79 | 80 | devil: createDirectional("👹", "👔", "",200,0,2), 81 | 82 | health: new Emoji("❤️", 6, 1, 0, 5), 83 | healthOff: new Emoji("❤️", 6, 1, 0, 5, "", 0, 20), 84 | enemyH: new Emoji("❤️", 4, 1, 0, 0), 85 | enemyHOff: new Emoji("❤️", 4, 1, 0, 0, "", 0, 20), 86 | bullet: new Emoji("🔅", 4, 1, 6, 6), 87 | explamation: new Emoji("❗️", 6, 1), 88 | keyboard: new Emoji("⌨️", 10, 1), 89 | controller: new Emoji("🎮", 10, 1), 90 | bomb: new CombinedEmoji([ 91 | { e: "💣", size: 6, pos: [1, 3]}, 92 | ]), 93 | goal: { 94 | u: new Emoji("⬆️", 10, 1, 0, 6, '#FFF', 260, 120), 95 | d: new Emoji("⬇️", 10, 1, 0, 5, '#FFF', 260, 120), 96 | l: new Emoji("⬅️", 10, 1, 0, 6, '#FFF', 260, 120), 97 | r: new Emoji("➡️", 10, 1, 0, 6, '#FFF', 260, 120) 98 | 99 | }, 100 | } -------------------------------------------------------------------------------- /scripts/rename-classes.ts: -------------------------------------------------------------------------------- 1 | import { ClassDeclaration, DefinitionInfo, Project } from 'ts-morph'; 2 | 3 | let name = [65, 65]; 4 | let generateNextName = () => { 5 | if (name[1] === 90) { 6 | name[0]++; 7 | name[1] = 65; 8 | } 9 | name[1]++; 10 | } 11 | 12 | let nameString = () => 13 | name.map(n => String.fromCharCode(n)).join(''); 14 | 15 | let classNameIdx = 0; 16 | let mId = 0; 17 | let pId = 0; 18 | 19 | const renameAll = (entries: any[]) => { 20 | entries.forEach(info => { 21 | const mName = info.getName(); 22 | if (mName.length < 3) { 23 | console.log(`I THINK I ALREADY RENAMED THAT: ${mName}`); 24 | return; 25 | } 26 | const newName = nameString(); 27 | generateNextName(); 28 | console.log(` ${mName} -> ${newName}`); 29 | info.rename(newName, { 30 | renameInComments: false, 31 | renameInStrings: false, 32 | }); 33 | }) 34 | } 35 | 36 | // Initialize a project with our tsconfig file 37 | const project = new Project({ 38 | tsConfigFilePath: 'tsconfig.json' 39 | }); 40 | 41 | // Get all project files 42 | const sourceFiles = project.getSourceFiles(); 43 | 44 | sourceFiles.forEach(sourceFile => { 45 | console.log('👉', sourceFile.getBaseName()); 46 | 47 | 48 | // INTERFACES: let's change the props only 49 | // sourceFile.getInterfaces().forEach(i => { 50 | // console.log(`${i.getName()}`); 51 | // console.log('RENAME PROP'); 52 | // renameAll(i.getProperties()); 53 | // // console.log('RENAME METHODS'); 54 | // // renameAll(i.getMethods()); 55 | // }) 56 | 57 | // Get all interfaces in a file 58 | const classes = [...sourceFile.getClasses(), /*...sourceFile.getInterfaces()*/]; 59 | 60 | classes.forEach(i => { 61 | try { 62 | const name = i.getName(); 63 | if (!name) { 64 | console.warn('Undefined name') 65 | return; 66 | } 67 | const nextName = nameString(); 68 | generateNextName(); 69 | if (name === nextName) { 70 | return; 71 | } 72 | 73 | // Rename class 74 | console.log(name, '->', nextName); 75 | i.rename(nextName, { 76 | renameInComments: false, 77 | renameInStrings: false 78 | }); 79 | 80 | if (i instanceof ClassDeclaration) { 81 | 82 | console.log('-- Static methods'); 83 | renameAll(i.getStaticMethods()); 84 | 85 | console.log("--- Renaming method parameters too"); 86 | // rename method params 87 | i.getStaticMethods().forEach(m => { 88 | console.log(m.getName()); 89 | renameAll(m.getParameters()); 90 | }) 91 | 92 | console.log('-- Static Props'); 93 | renameAll(i.getStaticProperties()); 94 | 95 | 96 | console.log('-- Static members'); 97 | renameAll(i.getStaticMembers()); 98 | } 99 | 100 | 101 | console.log('--- Methods'); 102 | const methods = i.getMethods(); 103 | methods.forEach(m => { 104 | const mName = m.getName(); 105 | const newName = nameString(); 106 | generateNextName(); 107 | console.log(` ${name}::${mName} -> ${nextName}::${newName}`); 108 | m.rename(newName, { 109 | renameInComments: false, 110 | renameInStrings: false, 111 | }); 112 | }); 113 | 114 | console.log('--- Props'); 115 | const props = i.getProperties(); 116 | props.forEach(p => { 117 | const pName = p.getName(); 118 | const newName = nameString(); 119 | generateNextName(); 120 | console.log(` ${name}::${pName} -> ${nextName}::${newName}`); 121 | p.rename(newName, { 122 | renameInComments: false, 123 | renameInStrings: false, // This renames too much. 124 | }); 125 | }) 126 | 127 | } catch(e) { 128 | console.log(e, i); 129 | } 130 | }); 131 | }); 132 | 133 | // Save all changed files 134 | project.saveSync(); -------------------------------------------------------------------------------- /src/modules/GameObjects/PauseMenu.ts: -------------------------------------------------------------------------------- 1 | import { TRANSPARENT } from "../../utils/colors"; 2 | import { KeyboardController } from "../Controller/KeyboardController"; 3 | import { Game } from "../Game"; 4 | import { Interruptable } from "../Interruptor/Interruptor"; 5 | import { Point, Rectangle } from "../Primitives"; 6 | import { GameObject, GetPosFn } from "./GameObject"; 7 | import { GameObjectsContainer } from "./GameObjectsContainer"; 8 | import { EmptyClass, withTags } from "./mixins"; 9 | import { TextTexture } from "./TextModule"; 10 | 11 | const difficultyToString = (d: number): string => { 12 | if (d === 0) { 13 | return "EASY"; 14 | } else if (d === 1) { 15 | return "NORMAL" 16 | } else { 17 | return "HARD"; 18 | } 19 | } 20 | 21 | const diffText = (d: number) => "Change difficulty [" + difficultyToString(d) + "]"; 22 | 23 | const post = "Post-processing"; 24 | 25 | export class PauseMenu extends withTags(EmptyClass) implements GameObject, Interruptable { 26 | pauseText = new TextTexture(["PAUSED"], 3, 1, TRANSPARENT); 27 | options = [ 28 | new TextTexture(["Resume"], 10, 1, TRANSPARENT), 29 | new TextTexture([diffText(2)], 10, 1, TRANSPARENT), 30 | new TextTexture([], 10, 1, TRANSPARENT), 31 | new TextTexture(["Fullscreen"], 10, 1, TRANSPARENT), 32 | ] 33 | current = 0; 34 | private ctx?: CanvasRenderingContext2D; 35 | render(ctx: CanvasRenderingContext2D, bb: Rectangle, fn: GetPosFn) { 36 | this.ctx = ctx; 37 | ctx.fillStyle = "#000A"; 38 | const width = fn(bb.p2)[0]; 39 | const u = width / 10; 40 | ctx.fillRect(0, 0, width, width); 41 | this.pauseText.render(ctx, u, u, this.pauseText.w*u/2, u*this.pauseText.h/2); 42 | this.options.forEach((opt, i) => { 43 | if (this.current === i) { 44 | ctx.fillStyle = "rgba(30, 100, 40, 0.7)"; 45 | ctx.fillRect(u, (2+i/2)*u, u*opt.w/2, u*opt.h/2); 46 | } 47 | opt.render(ctx, u, (2+i/2)*u, u*opt.w/2, u*opt.h/2); 48 | }) 49 | } 50 | isHidden: boolean = false; 51 | parentBBExclude: boolean = false; 52 | center: Point = Point.ORIGIN; 53 | cooloff = 300; 54 | update(dt: number, container: GameObjectsContainer): void { 55 | this.cooloff -= dt; 56 | if (this.cooloff > 0) { 57 | return; 58 | } 59 | if (this.controller?.v.e || this.controller?.v.b) { 60 | this.hasEnded = true; 61 | } 62 | if (this.controller!.y > 0) { 63 | this.current = (this.current + 1) % this.options.length; 64 | this.cooloff = 250; 65 | return; 66 | } 67 | if (this.controller!.y < 0) { 68 | this.current = this.current === 0 ? this.options.length -1 : this.current - 1; 69 | this.cooloff = 250; 70 | return; 71 | } 72 | 73 | if (this.controller?.v.a) { 74 | this.cooloff = 250; 75 | const set = this.game.settings; 76 | switch (this.current) { 77 | case 0: 78 | this.hasEnded = true; 79 | break; 80 | case 1: 81 | set.difficulty = (set.difficulty + 1) % 3; 82 | break; 83 | case 2: 84 | set.post = !set.post; 85 | break; 86 | case 3: 87 | this.ctx?.canvas.requestFullscreen(); 88 | break; 89 | } 90 | this.saveSettings() 91 | this.setOptions(); 92 | } 93 | } 94 | saveSettings() { 95 | window.localStorage.setItem('hsph_set', JSON.stringify(this.game.settings)); 96 | } 97 | 98 | setOptions() { 99 | const set = this.game.settings; 100 | this.options[1].updateTexts([diffText(set.difficulty)]) 101 | this.options[2].updateTexts([post + "[" + (set.post ? 'ON' : 'OFF') +"]"]); 102 | } 103 | 104 | getBoundingBox(): Rectangle { 105 | return new Rectangle(Point.ORIGIN, new Point(10, 10)); 106 | } 107 | zIndex?: number | undefined = 1000; 108 | isGlobal: boolean = true; 109 | controller?: KeyboardController; 110 | game!: Game; 111 | start(controller: KeyboardController, game: Game): void { 112 | this.controller = controller; 113 | this.game = game; 114 | this.setOptions(); 115 | } 116 | hasEnded: boolean = false; 117 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/TextModule.ts: -------------------------------------------------------------------------------- 1 | import { SIZE } from "../Color/Image"; 2 | import { NewTexture } from "../Color/Texture"; 3 | import { KeyboardController } from "../Controller/KeyboardController"; 4 | import { Game } from "../Game"; 5 | import { Interruptable } from "../Interruptor/Interruptor"; 6 | import { Point, Rectangle } from "../Primitives"; 7 | import { GameObjectsContainer } from "./GameObjectsContainer"; 8 | import { RectangleObject } from "./Rectangle"; 9 | 10 | 11 | export class TextTexture extends NewTexture { 12 | canvas!: HTMLCanvasElement; 13 | constructor(protected text: string[], public w: number , public h: number, private bg?: string , private txtcol: string = '#FFF', private size = 7) { 14 | super(); 15 | this.generate(); 16 | } 17 | 18 | updateTexts(text: string[]) { 19 | this.text = text; 20 | this.generate(); 21 | } 22 | 23 | generate() { 24 | this.canvas = document.createElement('canvas'); 25 | this.canvas.style.letterSpacing = "44px"; 26 | const w = this.w*SIZE; 27 | const h = this.h*SIZE; 28 | this.canvas.width = w; 29 | this.canvas.height = h; 30 | const ctx = this.canvas.getContext('2d')!; 31 | ctx.fillStyle = this.bg || '#0000'; 32 | ctx.fillRect(0, 0, w, h); 33 | ctx.fillStyle = this.txtcol; 34 | ctx.textAlign = "start"; 35 | ctx.font = `${this.size}px 'Verdana'`; 36 | ctx.textBaseline = "top" 37 | ctx.imageSmoothingEnabled = false; 38 | this.text.forEach((text, i) => { 39 | ctx.fillText(text.toUpperCase().split('').join(String.fromCharCode(8202)), 8, 6+i*(2+this.size)); 40 | }); 41 | this.optimise(ctx); 42 | } 43 | render(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) { 44 | // FIXME: move bg drawing here so it's easier to change in menu items. 45 | ctx.globalAlpha = this.opacity; 46 | ctx.drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height, x, y, w , h); 47 | ctx.globalAlpha = 1; 48 | } 49 | 50 | opacity = 1; 51 | 52 | setOpacity(o: number) { 53 | this.opacity = o; 54 | } 55 | } 56 | 57 | export class TextGameObject extends RectangleObject implements Interruptable { 58 | isGlobal: boolean = true; 59 | 60 | autoHide = 2000; 61 | 62 | res?: () => void; 63 | 64 | constructor(text: string[], p: Point, private w: number, private h: number, private autoremove: boolean = false, bg: string = "black", textcolor: string = 'white', size = 7) { 65 | super(p, new TextTexture(text, 2*w, 2*h, bg, textcolor, size)); 66 | this.rectangle = new Rectangle(p, p.add(w, h)); 67 | } 68 | onResolution(fn: () => void): void { 69 | this.res = fn; 70 | } 71 | controller!: KeyboardController; 72 | game!: Game; 73 | isStartedAsInterrutable: boolean = false; 74 | start(controller: KeyboardController, game: Game): void { 75 | this.isStartedAsInterrutable = true; 76 | this.controller = controller; 77 | this.game = game; 78 | } 79 | hasEnded: boolean = false; 80 | 81 | cooloff = 500; 82 | update(dt: number, container: GameObjectsContainer): void { 83 | if (this.isStartedAsInterrutable) { 84 | this.cooloff -= dt; 85 | if (this.controller.v.a && this.cooloff < 0) { 86 | this.hasEnded = true; 87 | this.res && this.res(); 88 | } 89 | return; 90 | } 91 | if (!this.autoremove) { 92 | return; 93 | } 94 | this.autoHide -=dt 95 | if (this.autoHide <= 0) { 96 | const opacity = 1 + (this.autoHide / 500); 97 | if (opacity < 0) { 98 | container.remove(this); 99 | return; 100 | } 101 | (this.texture as TextTexture).setOpacity(Math.floor(opacity*5)/5); 102 | } 103 | } 104 | } 105 | 106 | export class InGameTextGO extends TextGameObject { 107 | constructor(text: string, p: Point, w: number, h: number, color: string, private animationMultiplier: number = 1) { 108 | super([text], p, w, h, true, "#0000", color); 109 | } 110 | isGlobal: boolean = false; 111 | autoHide = 1000; 112 | update(dt: number, container: GameObjectsContainer) { 113 | super.update(dt, container); 114 | this.rectangle = this.rectangle.moveBy(new Point(0, -this.animationMultiplier * dt*0.2/1000)); 115 | } 116 | } 117 | 118 | export class TextModal extends TextGameObject { 119 | constructor(t: string[]) { 120 | const textBlock = Point.ORIGIN.add(0.5, 5.5); 121 | super(t, textBlock, 9 ,2, false, "#0f0f26", "#cbcbd4", 8) 122 | } 123 | } -------------------------------------------------------------------------------- /src/modules/Scene/CementeryScene.ts: -------------------------------------------------------------------------------- 1 | import { E } from "../Assets/Emojis"; 2 | import { AudioTrack } from "../Audio/AudioTrack"; 3 | import { Song } from "../Audio/Song"; 4 | import { EmojiList, Ground } from "../Color/Ground"; 5 | import { CombinedEmoji, Dither, Emoji, EmojiSet } from "../Color/Sprite"; 6 | import { Game } from "../Game"; 7 | import { GameObjectsContainer } from "../GameObjects/GameObjectsContainer"; 8 | import { Factory } from "../GameObjects/Item"; 9 | import { AmbientLight } from "../GameObjects/Light"; 10 | import { TextModal } from "../GameObjects/TextModule"; 11 | import { Point } from "../Primitives"; 12 | import { Scene, SceneSettings } from "./Scene"; 13 | 14 | const T = { 15 | NOTE: () => new TextModal(["A note:", " 'Fight the zombies, find their source.'"]), 16 | NOTE_2: () => new TextModal(["Zombie factories – great."]), 17 | } 18 | 19 | 20 | 21 | const delay = { time: .1, gain: 0.1 }; 22 | 23 | const sil = "xxxxxxxx"; 24 | 25 | const X = "45241452" 26 | const V = X + X + X + "1451x5xx" + X + sil + X + "12115xxx"; 27 | 28 | const S = "xxxx"; 29 | 30 | const B = "$$$$" 31 | const C = "$$))" 32 | 33 | const var1 = B + S + B + S + B + B + S + S; 34 | const var2 = C + S + B + S + C + C + S + C; 35 | 36 | const V2 = var1+var1+var1+var2; 37 | 38 | 39 | 40 | 41 | 42 | 43 | export class CementeryScene extends Scene { 44 | song = new Song([ 45 | new AudioTrack(65 * 4, 1, V, { type: "square", cutoff: 400, delay, cutoffOpenRatio: 10, cutoffOpenDelay: 0.75 }), 46 | new AudioTrack(65 * 8, 0.5, V2, { type: "sawtooth", cutoff: 230, delay, cutoffOpenRatio: 5, cutoffOpenDelay: 0.50 }), 47 | // new AudioTrack(65, 1, "444x333x111x", { type: "square", cutoff: 100 }), 48 | // new AudioTrack(65 / 2, 1, ":::x:::x666x", { type: "triangle", cutoff: 300 }) 49 | 50 | ]);; 51 | addObjects(game: Game): void { 52 | game.gameObjects.add(new AmbientLight(0.23)); 53 | game.interruptorManager.add(new TextModal([ 54 | "You wake up in a cemetery.", 55 | "The noises bring to mind yesterday's horrors.", 56 | "Time to fight for your life.", 57 | "", 58 | "Press [Space]" 59 | ])); 60 | game.interruptorManager.add(new TextModal([ 61 | "Move = arrow keys", 62 | "Shoot = [space]", 63 | "Bomb = [X]", 64 | "Pause = [ESC]" 65 | ])); 66 | } 67 | 68 | stopMusic(): void { 69 | this.song.stop(); 70 | } 71 | 72 | register(container: GameObjectsContainer, game: Game): SceneSettings { 73 | super.register(container, game); 74 | this.song.play(); 75 | 76 | const cross = new CombinedEmoji([ 77 | { e: "✝", size: 16, pos: [0, 0] }, 78 | { e: "🌱 🌱", size: 4, pos: [0, 10], hueShift: -70 }, 79 | ], 1); 80 | 81 | const stone = new CombinedEmoji([ 82 | { e: "🪨", size: 10, pos: [0, 10] }, 83 | { e: "🌱 🌱", size: 4, pos: [0, 14], hueShift: -70 }, 84 | ], 1); 85 | 86 | const leaf = new CombinedEmoji([ 87 | { e: "🍃", size: 6, pos: [0, 0], hueShift: -30 }, 88 | { e: "🍃", size: 6, pos: [10, 0], hueShift: -50 }, 89 | { e: "🍃", size: 6, pos: [0, 10], hueShift: -70 }, 90 | { e: "🍃", size: 6, pos: [15, 0], hueShift: -30 }, 91 | { e: "🍃", size: 6, pos: [2, 10], hueShift: -20 }, 92 | ]); 93 | 94 | const wall = new CombinedEmoji([ 95 | { e: "🪵", size: 14, pos: [0, 0] } 96 | ]) 97 | 98 | const ground: EmojiList[] = [ 99 | // { e: grave, range: [0.999, 1] }, 100 | { e: new Emoji("🌱", 4, 1, 0, 0, '', -30), range: [0.5, 0.6] }, 101 | { e: cross, range: [0.2, 0.21] }, 102 | { e: stone, range: [0.6, 0.61] }, 103 | { e: leaf, range: [0.2, 0.3] }, 104 | { e: wall, range: [0.35, 0.37], asGameObject: true } 105 | ]; 106 | 107 | let range = 0.3; 108 | let progress = 0.005; 109 | for(let i=0;i<10;i++) { 110 | const em: EmojiSet[] = [{ e: "🪦", size: 12 + i % 3, pos: [0, 0] }]; 111 | if (i % 2 == 0) { 112 | em.push({e: "🌱", size: 6, pos: [0, 10], hueShift: -30 }) 113 | } 114 | if (i%4 == 0) { 115 | em.push({e: "🌱", size: 4, pos: [10, 10], hueShift: -50 }) 116 | } 117 | if (i% 3 == 0) { 118 | em.push({ e: "🌱🌱🌱", size: 4, pos: [5, 10], hueShift: -20 }); 119 | } 120 | ground.push({ e: new CombinedEmoji(em), range: [range, range+progress], asGameObject: true}); 121 | range+=progress; 122 | } 123 | 124 | return { 125 | backgroundColor: '#08422e', 126 | ground: new Ground(ground, 5234), 127 | hudBackground: 'rgb(30,30,50)', 128 | getDither: Dither.generateDithers(16, [5, 46, 32]), 129 | pCenter: Point.ORIGIN.add(50, 20), 130 | stages: [ 131 | { lvl: 2, res: (g => g.interruptorManager.add(T.NOTE()))}, 132 | { lvl: 5, res: (g => { 133 | g.interruptorManager.add(T.NOTE_2()); 134 | const f = new Factory(g.player.center); 135 | g.gameObjects.add(f); 136 | g.setObjective(f); 137 | }) 138 | } 139 | ], 140 | enemies: [E.cowMan, E.frogMan, E.pigMan] 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/modules/Primitives/index.ts: -------------------------------------------------------------------------------- 1 | import { distance } from "../../utils/math"; 2 | import { SIZE } from "../Color/Image"; 3 | 4 | export class Point { 5 | 6 | static get UNIT_UP() { 7 | return Point.ORIGIN.add(0, -1); 8 | } 9 | 10 | static get UNIT_DOWN() { 11 | return Point.ORIGIN.add(0, 1); 12 | } 13 | 14 | static get UNIT_LEFT() { 15 | return Point.ORIGIN.add(-1, 0); 16 | } 17 | 18 | static get UNIT_RIGHT() { 19 | return Point.ORIGIN.add(1, 0); 20 | } 21 | 22 | constructor(public x: number, public y: number) { } 23 | copy() { 24 | return new Point(this.x, this.y); 25 | } 26 | 27 | neg() { 28 | return new Point(-this.x, -this.y); 29 | } 30 | 31 | snapToGrid() { 32 | return new Point(Math.round(this.x*SIZE)/SIZE, Math.round(this.y*SIZE)/SIZE); 33 | } 34 | 35 | normalize(): Point { 36 | if (!this.distanceFromOrigin()) { 37 | return this; 38 | } 39 | return new Point( 40 | this.x / this.distanceFromOrigin(), 41 | this.y / this.distanceFromOrigin(), 42 | ); 43 | } 44 | 45 | add(x: number, y: number) { 46 | return new Point(this.x + x, this.y + y); 47 | } 48 | 49 | addVec(p: Point) { 50 | return new Point(this.x + p.x, this.y + p.y); 51 | } 52 | 53 | diffVec(p: Point) { 54 | return new Point(this.x - p.x, this.y - p.y); 55 | } 56 | 57 | mul(n: number, m: number | undefined = undefined) { 58 | return new Point(this.x * n, this.y * (m || n)); 59 | } 60 | 61 | round() { 62 | return new Point(Math.round(this.x), Math.round(this.y)); 63 | } 64 | 65 | static get ORIGIN() { 66 | return new Point(0, 0); 67 | } 68 | 69 | distanceFromOrigin() { 70 | return (new Line(Point.ORIGIN, this)).length; 71 | } 72 | } 73 | 74 | export class Line { 75 | constructor(public p1: Point, public p2: Point) { } 76 | 77 | getMidpoint(location: number = 0.5) { 78 | return new Point( 79 | (this.p1.x * (1-location) + this.p2.x * location), 80 | (this.p1.y * (1-location) + this.p2.y * location), 81 | ); 82 | } 83 | 84 | toPoint() { 85 | return new Point(this.p2.x-this.p1.x,this.p2.y-this.p1.y); 86 | } 87 | 88 | get length() { 89 | return distance(this.p1, this.p2); 90 | } 91 | } 92 | 93 | 94 | export class Rectangle extends Line { 95 | 96 | static get UNIT() { 97 | return new Rectangle(Point.ORIGIN, Point.ORIGIN); 98 | } 99 | 100 | static boundingBox(r1: Rectangle, r2: Rectangle): Rectangle { 101 | return new Rectangle( 102 | new Point( 103 | Math.min(r1.p1.x, r2.p1.x), 104 | Math.min(r1.p1.y, r2.p1.y) 105 | ), 106 | new Point( 107 | Math.max(r1.p2.x, r2.p2.x), 108 | Math.max(r1.p2.y, r2.p2.y) 109 | ) 110 | ); 111 | } 112 | 113 | expand(n: number) { 114 | return new Rectangle(this.p1.add(-n, -n), this.p2.add(n, n)); 115 | } 116 | 117 | toLines(): Line[] { 118 | return [ 119 | new Line(this.p1, this.p1.add(this.width, 0)), 120 | new Line(this.p1.add(this.width, 0), this.p2), 121 | new Line(this.p2, this.p1.add(0, this.height)), 122 | new Line(this.p1, this.p1.add(0, this.height)) 123 | ]; 124 | } 125 | 126 | constructor(p1: Point, p2: Point) { 127 | super( 128 | new Point(Math.min(p1.x, p2.x), Math.min(p1.y, p2.y)), 129 | new Point(Math.max(p1.x, p2.x), Math.max(p1.y, p2.y)), 130 | ); 131 | } 132 | 133 | moveTo(p: Point) { 134 | const w = this.width; 135 | const h = this.height; 136 | this.p1 = p.copy(); 137 | this.p2 = p.add(w, h); 138 | } 139 | 140 | moveBy(p: Point) { 141 | return new Rectangle( 142 | this.p1.add(p.x, p.y), 143 | this.p2.add(p.x, p.y), 144 | ); 145 | } 146 | 147 | forEachCell(fn: (x: number, y: number, oX: number, oY: number) => void): void { 148 | // Experimental 149 | 150 | for(let i=Math.floor(this.p1.x);i= p.x && 195 | this.p1.y <= p.y && this.p2.y >= p.y 196 | ); 197 | } 198 | 199 | isIntersectingRectangle(r: Rectangle): boolean { 200 | const noOverlap = ( 201 | this.p1.x > r.p2.x || 202 | this.p2.x < r.p1.x || 203 | this.p1.y > r.p2.y || 204 | this.p2.y < r.p1.y 205 | ); 206 | return !noOverlap; 207 | } 208 | } -------------------------------------------------------------------------------- /src/modules/Game/index.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from "../Camera"; 2 | import { GameObjectsContainer } from "../GameObjects/GameObjectsContainer"; 3 | import { Player } from "../GameObjects/Player"; 4 | import { Renderer2d } from "../Renderer/Renderer2d"; 5 | import { QuadTreeContainer } from "../GameObjects/QuadTreeContainer"; 6 | import { CementeryScene } from "../Scene/CementeryScene"; 7 | import { Scene, SceneSettings } from "../Scene/Scene"; 8 | import { SIZE } from "../Color/Image"; 9 | import { InterGO, Interruptor } from "../Interruptor/Interruptor"; 10 | import { Item } from "../GameObjects/Item"; 11 | import { HellScene } from "../Scene/HellScene"; 12 | import { LabScene } from "../Scene/LabScene"; 13 | import { KeyboardController } from "../Controller/KeyboardController"; 14 | 15 | const ZOOM_SPEED = 1; 16 | 17 | export class Game { 18 | private ctx!: CanvasRenderingContext2D; 19 | public camera!: Camera; 20 | public gameObject!: GameObjectsContainer; 21 | private renderer!: Renderer2d; 22 | private lastRenderTime: number = 0; 23 | public player!: Player; 24 | 25 | public sceneSettings!: SceneSettings; 26 | public MULTIPLIER = 1; 27 | public UNIT_SIZE = 1; 28 | 29 | public settings = { 30 | difficulty: 1, 31 | post: true, 32 | } 33 | 34 | public interruptorManager: Interruptor = new Interruptor(); 35 | 36 | currentScene?: Scene; 37 | 38 | width: number = 0; 39 | height: number = 0; 40 | 41 | controller: KeyboardController; 42 | 43 | 44 | isIntro = true; 45 | 46 | constructor(private canvas: HTMLCanvasElement, private w: number, h: number) { 47 | 48 | const fn = () => { 49 | const windowWidth = window.innerWidth; 50 | const windowHeight = window.innerHeight; 51 | this.MULTIPLIER = Math.floor(Math.min(windowWidth, windowHeight) / (w * SIZE)); 52 | this.UNIT_SIZE = SIZE * this.MULTIPLIER 53 | this.width = this.canvas.width = SIZE * w * this.MULTIPLIER; 54 | this.height = this.canvas.height = SIZE * h * this.MULTIPLIER; 55 | this.ctx = canvas.getContext('2d')!; 56 | this.ctx.imageSmoothingEnabled = false; 57 | }; 58 | fn(); 59 | window.onresize = fn; 60 | this.controller = new KeyboardController(); 61 | 62 | this.restart(); 63 | try { 64 | this.settings = JSON.parse(window.localStorage.getItem('hsph_set') || ''); 65 | } catch (e) { 66 | } 67 | } 68 | 69 | loadScene(scene: Scene, withRestart: boolean = false, withObstRemoval: boolean = false) { 70 | if (this.currentScene) { 71 | this.currentScene.stopMusic(); 72 | } 73 | 74 | if (withObstRemoval) { 75 | this.gameObjects = new QuadTreeContainer(); 76 | this.gameObjects.add(this.player); 77 | this.camera.follow(this.player); 78 | } 79 | 80 | if (withRestart) { 81 | this.restart(); 82 | } 83 | 84 | this.sceneSettings = scene.register(this.gameObjects, this); 85 | this.currentScene = scene; 86 | this.player.center = this.sceneSettings.pCenter; 87 | this.camera.prevCamera = this.player.center; 88 | this.objective = undefined; 89 | this.currentStage = 0; 90 | } 91 | 92 | objective?: Item; 93 | 94 | setObjective(c: Item) { 95 | this.objective = c; 96 | } 97 | 98 | currentStage = 0; 99 | gameObjects!: QuadTreeContainer; 100 | 101 | restart() { 102 | this.camera = new Camera(this.ctx); 103 | this.gameObjects = new QuadTreeContainer(); 104 | this.renderer = new Renderer2d(this.ctx, this); 105 | 106 | this.player = new Player(this.controller); 107 | this.player.setGame(this); // FIXME: do it properly maybe? 108 | this.gameObjects.add(this.player); 109 | this.camera.follow(this.player); 110 | this.renderer.setCamera(this.camera); 111 | this.currentStage = 0; 112 | } 113 | 114 | render() { 115 | 116 | const tDiff = Date.now() - this.lastRenderTime; 117 | this.lastRenderTime = Date.now(); 118 | 119 | if (this.isIntro) { 120 | this.isIntro = this.renderer.renderIntro(tDiff); 121 | if (!this.isIntro) { 122 | // LOAD SCENE 123 | this.loadScene(new CementeryScene(), true, true); 124 | } 125 | } else { 126 | const stgI = this.currentStage; 127 | if (stgI < this.sceneSettings.stages.length) { 128 | 129 | const stg = this.sceneSettings.stages[stgI]; 130 | if (stg.lvl <= this.player.lvl) { 131 | this.currentStage++; 132 | stg.res(this); 133 | } 134 | } 135 | this.interruptorManager.update(this.player.controller, this); 136 | if (this.interruptorManager.isRunning) { 137 | this.interruptorManager.updateInter(tDiff); 138 | this.gameObjects.update(); 139 | this.renderer.render(this.camera, this.gameObjects, tDiff, this, false); 140 | this.renderer.renderInterruptorManager(this.interruptorManager); 141 | this.renderer.renderPostEffects(); 142 | } else { 143 | this.gameObjects.getAll().forEach(g => { 144 | g.update(tDiff, this.gameObjects); 145 | }); 146 | 147 | this.gameObjects.update(); 148 | 149 | this.renderer.render(this.camera, this.gameObjects, tDiff, this); 150 | } 151 | } 152 | 153 | 154 | // this.qtRender.renderQuadtree((this.gameObjects as unknown as QuadTreeContainer).tree); 155 | // this.lastRenderTime = Date.now(); // time still marches on tho. 156 | requestAnimationFrame(() => this.render()); 157 | } 158 | 159 | registerInterruptor(i: InterGO) { 160 | this.interruptorManager.add(i); 161 | } 162 | 163 | start() { 164 | this.lastRenderTime = Date.now(); 165 | this.render(); 166 | 167 | } 168 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/Enemy.ts: -------------------------------------------------------------------------------- 1 | import { getLinesIntersection, rnd } from "../../utils/math"; 2 | import { Directional, E } from "../Assets/Emojis"; 3 | import { Line, Point } from "../Primitives"; 4 | import { GameObjectsContainer } from "./GameObjectsContainer"; 5 | import { SimpleHumanoid } from "./Humanoid"; 6 | import { BombCollectableItem, LifeCollectableItem } from "./Item"; 7 | import { BulletInventoryItem } from "./Player"; 8 | import { RectangleObject } from "./Rectangle"; 9 | 10 | const SPEED = 0.001; 11 | 12 | export class Enemy extends SimpleHumanoid { 13 | 14 | private p = Point.UNIT_DOWN; 15 | private changeTimedown = 0; 16 | constructor(d: Directional, public value: number = 100, p: Point = Point.ORIGIN, public life: number = 3, private initialCooldown = 1000) { 17 | super(d, 3, 0.5); 18 | this.center = p; 19 | this.addTag("e"); 20 | // FIXME: combine into single emo. 21 | for(let i=0;i 0.5) { 36 | this.directionMoveFirst = 'y'; 37 | } 38 | 39 | } 40 | 41 | exclamation: RectangleObject; 42 | 43 | lastFired = -1; 44 | inventory = new BulletInventoryItem() 45 | 46 | directionMoveFirst = 'x'; 47 | 48 | getHit(container: GameObjectsContainer, amount: number = 1) { 49 | super.getHit(container, amount); 50 | // update hit points above 51 | if (this.life === 0) { 52 | this.die(); 53 | } else { 54 | this.lives.forEach((l,i) => { 55 | i >= this.life ? l.texture = E.enemyHOff: void 0 56 | l.isHidden = false; 57 | }); 58 | } 59 | } 60 | 61 | die() { 62 | // Spawning items 63 | if (rnd() < 0.5) { 64 | this.container?.add(new LifeCollectableItem(this.center)); 65 | } else if (rnd() < 0.4) { 66 | this.container?.add(new BombCollectableItem(this.center)); 67 | } 68 | } 69 | 70 | 71 | // FIXME: probably do it in a nicer way. 72 | container!: GameObjectsContainer; 73 | 74 | private lives: RectangleObject[] = []; 75 | 76 | previouslySpotted = false; 77 | fireCooldown = 0; 78 | 79 | update(dt: number, container: GameObjectsContainer): void { 80 | this.container = container; 81 | this.changeTimedown -= dt; 82 | 83 | // TARGETTING PLAYER. 84 | const bb = this.getBoundingBox().expand(4); 85 | const player = container.getObjectsInArea(bb, "p"); 86 | let playerSpotted = false; 87 | if (player.length) { 88 | const line = new Line(player[0].getBoundingBox().center, this.center); 89 | const obst = container.getObjectsInArea(bb.expand(3), "o"); 90 | const bareer = obst.map(o => o.getBoundingBox().toLines()).flat().find(o => !!getLinesIntersection(o, line)); 91 | playerSpotted = !bareer; 92 | } 93 | this.exclamation.isHidden = !playerSpotted; 94 | 95 | let dir = this.p; 96 | 97 | let speed = SPEED; 98 | let xDiff = 0; 99 | let yDiff = 0; 100 | 101 | if (playerSpotted && !this.previouslySpotted) { 102 | this.lastFired = Date.now(); 103 | this.fireCooldown = this.initialCooldown; 104 | } 105 | this.previouslySpotted = playerSpotted; 106 | 107 | if (playerSpotted) { 108 | const line = new Line(player[0].getBoundingBox().center, this.center); 109 | if (line.length <= 1) { 110 | // stop 111 | dir = Point.ORIGIN; 112 | } else { 113 | xDiff = player[0].getBoundingBox().center.x - this.center.x - 0.5 114 | yDiff = player[0].getBoundingBox().center.y - this.center.y - 0.5 115 | if (Math.abs(xDiff) < 0.05) { 116 | xDiff = 0; 117 | } 118 | 119 | if (Math.abs(yDiff) < 0.05) { 120 | yDiff = 0; 121 | } 122 | // following 123 | dir = new Point( 124 | this.directionMoveFirst === 'x' ? xDiff : 0, 125 | this.directionMoveFirst === 'y' ? yDiff : 0).normalize() 126 | speed *= 2; 127 | } 128 | } 129 | 130 | 131 | let moved = this.move(dt, dir, speed, container); 132 | 133 | if (!moved && playerSpotted) { 134 | // trying moving on Y 135 | dir = new Point( 136 | this.directionMoveFirst === 'y' ? xDiff : 0, 137 | this.directionMoveFirst === 'x' ? yDiff : 0).normalize() 138 | moved = this.move(dt, dir, speed, container); 139 | } 140 | 141 | if (playerSpotted) { 142 | // Point towards player 143 | this.lastX = yDiff ? 0 : xDiff; 144 | this.lastY = yDiff; 145 | } 146 | 147 | this.exclamation.rectangle.moveTo(this.center.add(0.2, -0.6)); 148 | this.lives 149 | .forEach((l,i) => 150 | l.rectangle.moveTo(this.center.add(0.3*i, -0.3)) 151 | ); 152 | 153 | 154 | // FIXME: check where is the player and shot only then. 155 | // const player = container.getObjectsInArea() 156 | 157 | // FIRE? 158 | this.fireCooldown -= dt; 159 | if (this.fireCooldown <= 0 && playerSpotted) { 160 | // FIRE 161 | const go = this.inventory.use(this, container, "p"); 162 | go.forEach(g => { 163 | container.add(g); 164 | 165 | }); 166 | this.fireCooldown = 1000; 167 | } 168 | 169 | !moved && (this.changeTimedown = 0); 170 | 171 | 172 | if (this.changeTimedown <= 0) { 173 | const r = rnd(); 174 | if (r < 0.25) { 175 | this.p = Point.UNIT_UP; 176 | } else if (r < 0.5) { 177 | this.p = Point.UNIT_DOWN; 178 | } else if (r < 0.75) { 179 | this.p = Point.UNIT_LEFT; 180 | } else { 181 | this.p = Point.UNIT_RIGHT; 182 | } 183 | this.changeTimedown = 2000 + 1000 * rnd(); 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /src/modules/GameObjects/Player.ts: -------------------------------------------------------------------------------- 1 | import { E } from "../Assets/Emojis"; 2 | import { AudioManager } from "../Audio/AudioManager"; 3 | import { NewTexture } from "../Color/Texture"; 4 | import { KeyboardController } from "../Controller/KeyboardController"; 5 | import { Game } from "../Game"; 6 | import { Point } from "../Primitives"; 7 | import { CementeryScene } from "../Scene/CementeryScene"; 8 | import { Bomb } from "./Bomb"; 9 | import { Bullet, UsableItem } from "./Bullet"; 10 | import { GameObjectsContainer } from "./GameObjectsContainer"; 11 | import { SimpleHumanoid } from "./Humanoid"; 12 | import { PauseMenu } from "./PauseMenu"; 13 | import { InGameTextGO, TextGameObject, TextTexture } from "./TextModule"; 14 | 15 | const MAX_SPEED = 6.5; 16 | const BOMB_LIMIT = 9; 17 | 18 | class InventoryItem { 19 | public amount: number = 0; 20 | public cooldown = 300; 21 | use(user: Player, container: GameObjectsContainer, tag: string): UsableItem[] { 22 | return []; 23 | } 24 | } 25 | 26 | export class BulletInventoryItem extends InventoryItem { 27 | use(user: SimpleHumanoid, container: GameObjectsContainer, tag: string = "e") { 28 | return [ 29 | new Bullet(user.center, new Point(user.lastX, user.lastY), 300, tag) 30 | ]; 31 | } 32 | } 33 | 34 | class BombInventoryItem extends InventoryItem { 35 | amount = 1; 36 | use(user: Player) { 37 | if (this.amount <= 0) { 38 | return []; 39 | } 40 | this.amount--; 41 | return [ 42 | new Bomb(user.center, 1000, "e") 43 | ]; 44 | 45 | } 46 | } 47 | 48 | const lvlToXp = (lvl: number) => lvl <= 1 ? 0 : (lvl-1)*(lvl-1)*50; 49 | 50 | 51 | export class Player extends SimpleHumanoid { 52 | isHidden = false; 53 | private _xp: number = 0; 54 | public lvl: number = 1; 55 | public lvlProgress: number = 0; 56 | public xpTexture!: NewTexture ; 57 | public lvlTexture!: NewTexture; 58 | container!: GameObjectsContainer; 59 | speed = 3; 60 | maxBomb = 3; 61 | 62 | get xp() { 63 | return this._xp; 64 | } 65 | 66 | set xp(v: number) { 67 | this._xp = v; 68 | // FIXME: thresholds for LVLS 69 | this.xpTexture = new TextTexture([this.xp + "xp"],4, 1,"#0000"); 70 | 71 | let lowerT = lvlToXp(this.lvl); 72 | let upperT = lvlToXp(this.lvl+1); 73 | if (this._xp >= upperT) { 74 | // Advancing level! 75 | this.lvl++; 76 | lowerT = lvlToXp(this.lvl); 77 | upperT = lvlToXp(this.lvl + 1); 78 | const txt = new InGameTextGO("⬆ LVL UP", this.center, 4, 1, "#befabe"); 79 | this.container.add(txt); 80 | const MIN_COOLDOWN = 100; 81 | if (this.lvl % 2 === 0 && this.baseCooldown > MIN_COOLDOWN) { 82 | const txt2 = new InGameTextGO("⬆ SHOT RATE UP", this.center.add(0.5, -0.1), 4, 1, "#FA0", 1.5); 83 | this.container.add(txt2); 84 | this.baseCooldown = Math.max(MIN_COOLDOWN, 0.9*this.baseCooldown); 85 | } 86 | 87 | if (this.lvl % 3 === 0 && this.speed < MAX_SPEED) { 88 | this.speed = Math.min(MAX_SPEED, this.speed * 1.13); 89 | const txt2 = new InGameTextGO("⬆ SPEED UP", this.center.add(0.2, -0.15), 4, 1, "#FA0", 1.3); 90 | this.container.add(txt2); 91 | } 92 | 93 | if (this.lvl % 4 === 1) { 94 | this.maxBomb = Math.min(BOMB_LIMIT, this.maxBomb + 1); 95 | const txt2 = new InGameTextGO("⬆ MORE BOMBS", this.center.add(0.4, -0.1), 4, 1, "#0A0", 1.3); 96 | this.container.add(txt2); 97 | } 98 | } 99 | this.lvlProgress = (this._xp - lowerT) / (upperT - lowerT); 100 | this.lvlTexture = new TextTexture(["LVL "+this.lvl], 3, 1, "#0000"); 101 | } 102 | 103 | public items: InventoryItem[] = []; 104 | 105 | 106 | constructor(public controller: KeyboardController) { 107 | super(E.playerDir); 108 | this.xp = 0; 109 | this.center = new Point(0, -20); 110 | this.addTag("p"); 111 | this.items.push(new BulletInventoryItem()); 112 | this.items.push(new BombInventoryItem()); 113 | } 114 | 115 | game!: Game; 116 | setGame(game: Game) { 117 | this.game = game; 118 | } 119 | 120 | addItem(type: string) { 121 | if (this.items.length < 8) { 122 | if (type === 'bomb') { 123 | this.items[1].amount = Math.min(this.maxBomb, this.items[1].amount+1); 124 | } 125 | } 126 | } 127 | 128 | getFeetBox() { 129 | const bb = this.getBoundingBox(); 130 | return bb.scale(1, 1/5).moveBy(new Point(0, 4/5*bb.height)); 131 | } 132 | 133 | heal() { 134 | if (this.life < 5) { 135 | this.life++; 136 | } 137 | } 138 | 139 | getHit(container: GameObjectsContainer) { 140 | super.getHit(container); 141 | this.controller.vibrate(); 142 | } 143 | 144 | die(container: GameObjectsContainer) { 145 | 146 | const youDie = new TextGameObject(["You died"], new Point(3, 3), 4, 1, false, "#000","#AAA", 20); 147 | this.game.interruptorManager.add(youDie); 148 | youDie.onResolution(() => { 149 | this.game.loadScene(new CementeryScene, true); 150 | }) 151 | } 152 | 153 | baseCooldown = 500; 154 | 155 | 156 | inputCooloff = 0; 157 | update(dt: number, container: GameObjectsContainer) { 158 | this.inputCooloff -= dt; 159 | 160 | if (this.controller.v.e && this.inputCooloff < 0) { 161 | this.game.interruptorManager.add(new PauseMenu()); 162 | this.inputCooloff = 300; 163 | } 164 | 165 | this.container = container; 166 | const p = new Point( 167 | this.controller.x, 168 | this.controller.y, 169 | ).normalize(); 170 | 171 | this.fireCooldown -= dt; 172 | 173 | 174 | const {a,b} = this.controller.v; 175 | 176 | if ((a||b) && this.fireCooldown <= 0) { 177 | this.fireCooldown = this.baseCooldown; 178 | const inventory = this.items[a ? 0 : 1]; 179 | const go = inventory.use(this, container, "e"); 180 | 181 | go.forEach(g => { 182 | container.add(g); 183 | g.onHit(t => { 184 | if (t.life <= 0) { 185 | this.xp += t.value; 186 | AudioManager.get().killed.play(); 187 | } 188 | }); 189 | // FIXME: add proper on hit here. 190 | }) 191 | } 192 | 193 | this.move(dt, p, (this.speed+(1-this.game.settings.difficulty/2))/1000, container); 194 | 195 | // this.rotation += dt * ROTATION_VELOCITY * this.controller.rotation / 1000; 196 | 197 | } 198 | 199 | isGlobal = false; 200 | } -------------------------------------------------------------------------------- /src/modules/Color/Sprite.ts: -------------------------------------------------------------------------------- 1 | import { Directional } from "../Assets/Emojis"; 2 | import { RectangleObject } from "../GameObjects/Rectangle"; 3 | import { Point, Rectangle } from "../Primitives"; 4 | import { convertEmoji } from "./EmojiUtils"; 5 | import { SIZE } from "./Image"; 6 | import { NewTexture } from "./Texture"; 7 | 8 | export class DirectionableTexture extends NewTexture { 9 | private direction ='left'; 10 | 11 | constructor(public dir: Directional) { 12 | super(); 13 | } 14 | 15 | getEmoji() { 16 | switch (this.direction) { 17 | case 'right': return this.dir.r; 18 | case 'down': return this.dir.d; 19 | case 'up': return this.dir.u; 20 | default: return this.dir.l; 21 | } 22 | } 23 | 24 | render(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) { 25 | return this.getEmoji().render(ctx, x, y, w, h); 26 | } 27 | 28 | setDirection(d: string, len: number) { 29 | this.direction = d; 30 | const e = this.getEmoji(); 31 | if (e instanceof AnimatedEmoji) { 32 | if (len) { 33 | e.start(); 34 | } else { 35 | e.stop(); 36 | } 37 | } 38 | } 39 | 40 | } 41 | 42 | export interface EmojiSet { 43 | e: string; 44 | size: number; 45 | pos: number[]; 46 | hueShift?: number; 47 | brightness?: number; 48 | color?: string; 49 | } 50 | 51 | export class CombinedEmoji extends NewTexture { 52 | constructor(private emojis: EmojiSet[], public scale: number = 1, private color = 'white') { 53 | super(); 54 | this.generate() 55 | } 56 | canvas!: HTMLCanvasElement; 57 | _boundingBox!: Rectangle; 58 | toGameObject(p: Point, scale: number = 1): RectangleObject { 59 | return new RectangleObject(p, this, [], scale); 60 | } 61 | isGenerated = false; 62 | 63 | protected generate(..._: any[]): void { 64 | this.isGenerated = true; 65 | const c = document.createElement('canvas'); 66 | c.width = this.scale * SIZE; 67 | c.height = this.scale * SIZE; 68 | const ct = c.getContext('2d')!; 69 | // let p1: Point, p2: Point; 70 | this.emojis.forEach(e => { 71 | e = convertEmoji(e); 72 | ct.font = `${e.size}px Arial` 73 | ct.fillStyle = e.color || this.color; 74 | ct.textBaseline = "top"; 75 | ct.filter = `hue-rotate(${e.hueShift||0}deg) brightness(${e.brightness||100}%)`; 76 | ct.fillText(e.e, e.pos[0], e.pos[1]); 77 | ct.filter = ''; 78 | }); 79 | this.canvas = c; 80 | this.optimise(ct); 81 | } 82 | 83 | render(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) { 84 | !this.isGenerated && this.generate(); 85 | try { 86 | ctx.drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height, x, y, w, h); 87 | } catch (e) { 88 | } 89 | } 90 | } 91 | 92 | export class AnimatedEmoji extends CombinedEmoji { 93 | constructor(emojis: EmojiSet[], scale: number, color: string, private steps: number = 3, private stepFn: (step: number, steps: number, canvas: HTMLCanvasElement) => void) { 94 | super(emojis, scale, color); 95 | setTimeout(() => { 96 | // hack to make inherited classes have time to set constructor variables. 97 | this.generate(steps); 98 | }); 99 | } 100 | 101 | private canvases: HTMLCanvasElement[] = []; 102 | 103 | protected generate(steps: number): void { 104 | if (!this.steps) { 105 | return; 106 | } 107 | super.generate(); 108 | for(let i=0;i { 127 | this.setFrame(this._n + 1); 128 | }, 25); 129 | } 130 | 131 | stop() { 132 | if (this.h) { 133 | clearInterval(this.h); 134 | this.isStarted = false; 135 | this._n = 0; 136 | } 137 | } 138 | 139 | render(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): void { 140 | const c = this.canvases[this._n]; 141 | try { 142 | ctx.drawImage(c, 0, 0, c.width, c.height, x, y, w, h); 143 | } catch (e) { 144 | } 145 | } 146 | 147 | private _n: number = 0; 148 | setFrame(n: number) { 149 | this._n = n % this.steps; 150 | } 151 | } 152 | 153 | export class Emoji extends CombinedEmoji { 154 | constructor(e: string, size: number, scale: number, x = 0, y = 0, color: string = 'white', hueShift: number = 0, brightness: number = 100) { 155 | super([{e: e, size: size, pos: [x, y], hueShift, brightness}], scale, color); 156 | this.generate(); 157 | } 158 | } 159 | 160 | 161 | 162 | const D_STEP = 11; 163 | 164 | const c = (n: number, steps: number) => Math.round(n * (steps - 1)) 165 | export class Dither extends NewTexture { 166 | 167 | static generateDithers(steps: number = D_STEP, color: number[] = [44, 100, 94]) { 168 | const dithers: Dither[] = []; 169 | for(let i=0;i<=steps;i++) { 170 | const d = this.gD(steps, i, color); 171 | dithers.push(d); 172 | } 173 | return (n: number) => dithers[c(n, steps)]; 174 | } 175 | 176 | static gD(s: number, l: number, col: number[]) { 177 | return new Dither(l/s,s,col); 178 | } 179 | 180 | ctx: CanvasRenderingContext2D; 181 | canvas: HTMLCanvasElement; 182 | 183 | private constructor(private l: number, private s: number, private c: number[]) { 184 | super(); 185 | this.canvas = document.createElement('canvas'); 186 | this.canvas.width = SIZE; 187 | this.canvas.height = SIZE; 188 | this.ctx = this.canvas.getContext('2d')!; 189 | this.generate(); 190 | } 191 | render(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): void { 192 | try { 193 | ctx.drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height, x, y, w, h); 194 | } catch (e) { 195 | } 196 | } 197 | protected generate(): void { 198 | const ct = this.ctx; 199 | ct.clearRect(0, 0, SIZE, SIZE); 200 | ct.fillStyle = 'rgb('+this.c.join(',') + ', ' + (1-this.l/1.2-0.2) + ')'; 201 | if (this.l > 0.95) { 202 | return; 203 | } 204 | for(let i=0;i= c(this.l, this.s)) { 207 | ct.fillRect(i, j, 1, 1); 208 | } 209 | } 210 | } 211 | this.optimise(ct); 212 | } 213 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": false, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | "declarationMap": false, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/modules/Renderer/Renderer2d.ts: -------------------------------------------------------------------------------- 1 | import { lightIntensityAtPoint } from "../../utils/lightIntesity"; 2 | import { getLinesIntersection } from "../../utils/math"; 3 | import { E } from "../Assets/Emojis"; 4 | import { AudioManager } from "../Audio/AudioManager"; 5 | import { Camera } from "../Camera"; 6 | import { Dither } from "../Color/Sprite"; 7 | import { Game } from "../Game"; 8 | import { GameObjectsContainer } from "../GameObjects/GameObjectsContainer"; 9 | import { Light } from "../GameObjects/Light"; 10 | import { TextGameObject, TextTexture } from "../GameObjects/TextModule"; 11 | import { Interruptor } from "../Interruptor/Interruptor"; 12 | import { Line, Point, Rectangle } from "../Primitives"; 13 | import { SceneSettings } from "../Scene/Scene"; 14 | import { Renderer } from "./Renderer"; 15 | 16 | export class Renderer2d implements Renderer { 17 | private bb!: Rectangle; 18 | private center!: Point; 19 | constructor(private ctx: CanvasRenderingContext2D, private game: Game) {} 20 | 21 | get width() { 22 | return this.game.width; 23 | } 24 | 25 | get height() { 26 | return this.game.height; 27 | } 28 | 29 | getSizePerPixel() { 30 | return 1 / this.game.UNIT_SIZE; 31 | } 32 | 33 | getUnitSize() { 34 | return 1 / this.getSizePerPixel(); 35 | } 36 | 37 | getUnits() { 38 | return this.width / this.getUnitSize(); 39 | } 40 | 41 | private getBoundingBox(): Rectangle { 42 | const s = this.getSizePerPixel(); 43 | // FIXME: probably can use generator from Rectangle class? 44 | const c = this.center; 45 | 46 | const w = this.width; 47 | const h = this.height; 48 | return new Rectangle( 49 | new Point( 50 | c.x - (w / 2) * s, 51 | c.y - (h / 2) * s 52 | ), 53 | new Point( 54 | c.x + (w / 2) * s, 55 | c.y + (h / 2) * s, 56 | ) 57 | ); 58 | } 59 | 60 | private getRawBB() { 61 | const s = this.getSizePerPixel(); 62 | const c = this.game.camera.rawCenter; 63 | 64 | const w = this.width; 65 | const h = this.height; 66 | return new Rectangle( 67 | new Point( 68 | c.x - (w / 2) * s, 69 | c.y - (h / 2) * s 70 | ), 71 | new Point( 72 | c.x + (w / 2) * s, 73 | c.y + (h / 2) * s, 74 | ) 75 | ); 76 | } 77 | 78 | getPositionOnScreen(p: Point): [number, number] { 79 | const x = (this.center.x - p.x) 80 | const y = (this.center.y - p.y); 81 | const ys = this.getSizePerPixel(); 82 | const xs = this.getSizePerPixel(); 83 | 84 | return [ 85 | this.width / 2 - x / xs, 86 | this.height / 2 - y / ys, 87 | ]; 88 | } 89 | 90 | private renderBackground(settings: SceneSettings) { 91 | 92 | this.ctx.clearRect(0, 0, this.width, this.height); 93 | 94 | const point = this.getPositionOnScreen(new Point(Math.floor(this.bb.p1.x), Math.floor(this.bb.p1.y))); 95 | const unitSize = 1 / this.getSizePerPixel(); 96 | settings.ground.render(this.ctx, this.getBoundingBox(), this.getRawBB(), settings, this.game); 97 | } 98 | 99 | renderDitheredLight(lights: Light[], obstructions: Line[]) { 100 | const bb = this.getBoundingBox(); 101 | // FIXME: use proper bb function. 102 | for(let i=bb.p1.x;i<=bb.p2.x;i++) { 103 | for(let j=bb.p1.y;j<=bb.p2.y;j++) { 104 | const lightsFiltered = lights.filter(l => { 105 | const line = new Line(l.center, new Point(i + 0.5, j + 0.5)); 106 | if (l.isGlobal) { 107 | return true; 108 | } 109 | // Is light obstructed 110 | const find = obstructions.find(o => getLinesIntersection(o, line)); 111 | return !find; 112 | }); 113 | 114 | const p = new Point(i,j); 115 | 116 | const pos = this.getPositionOnScreen(p); 117 | const w = this.getUnitSize(); 118 | const h = this.getUnitSize(); 119 | 120 | // Extra colouring 121 | lightsFiltered.forEach(l => { 122 | if (l.color!== "#FFF") { 123 | this.ctx.globalCompositeOperation = "overlay" 124 | this.ctx.globalAlpha = lightIntensityAtPoint(new Point(i, j), [l]); 125 | this.ctx.fillStyle = l.color; 126 | this.ctx.fillRect(...pos, w, h); 127 | } 128 | }); 129 | this.ctx.globalCompositeOperation = "source-over"; 130 | this.ctx.globalAlpha = 1; 131 | const l = lightIntensityAtPoint(new Point(i,j), lightsFiltered); 132 | const d = this.game.sceneSettings.getDither(l); 133 | d.render(this.ctx, ...pos, w, h); 134 | } 135 | } 136 | } 137 | 138 | renderDebugLine(line: Line, color = '#FFF') { 139 | this.ctx.beginPath(); 140 | this.ctx.lineWidth = 2; 141 | this.ctx.strokeStyle = color; 142 | this.ctx.moveTo(...this.getPositionOnScreen(line.p1)); 143 | this.ctx.lineTo(...this.getPositionOnScreen(line.p2)); 144 | this.ctx.stroke(); 145 | } 146 | camera!: Camera; 147 | 148 | setCamera(camera: Camera) { 149 | this.camera = camera; 150 | this.center = camera.center; 151 | } 152 | 153 | prepareFrame() { 154 | this.center = this.camera.center; 155 | const boundingBox = this.getBoundingBox(); 156 | this.bb = boundingBox; 157 | } 158 | 159 | render(camera: Camera, gameObjects: GameObjectsContainer, dt: number, game: Game, post: boolean = true) { 160 | 161 | this.prepareFrame(); 162 | this.renderBackground(game.sceneSettings); 163 | const objects = gameObjects.getObjectsInArea(this.bb) 164 | .sort((a,b) => { 165 | if (a.isGlobal) { 166 | return 1; 167 | } 168 | if (b.isGlobal) { 169 | return -1; 170 | } 171 | return a.getBoundingBox().center.y-b.getBoundingBox().center.y 172 | }); 173 | const obstructions = gameObjects.getObjectsInArea(this.bb, "o").map(o => o.getBoundingBox().toLines()).flat(); 174 | const lights = objects.filter(o => o instanceof Light) as Light[]; 175 | this.renderDitheredLight(lights, obstructions); 176 | for (const obj of objects) { 177 | obj.render( 178 | this.ctx, 179 | this.bb, 180 | (p: Point) => this.getPositionOnScreen(p.snapToGrid()) 181 | ); 182 | } 183 | this.renderHUD(game); 184 | post && this.renderPostEffects(); 185 | } 186 | 187 | 188 | introTime = 0; 189 | playedIntroMusic = false; 190 | introText = new TextGameObject(["GRAVEPASSING"], new Point(2, 0), 8, 2, false, "","#FFF", 20); 191 | author = new TextGameObject(["by Kacper Kula", "", "with the help of Rae"], new Point(10, 0), 8, 3, false, "","#FFF", 10); 192 | pressAnyKey = new TextGameObject(["Press [Space]"], new Point(3, 8), 8, 3, false, "", "#FFF", 10); 193 | keyPressed = false; 194 | dit = Dither.gD(48, 20, [2,19,13]); 195 | renderIntro(dt: number) { 196 | this.dit = Dither.gD(120, Math.min(70, Math.floor(20 + this.introTime / 50)),[2,19,13]); 197 | this.ctx.fillStyle = '#053021'; 198 | this.ctx.fillRect(0, 0, this.width, this.height); 199 | for(let i=0;i<10;i++) { 200 | for(let j=0;j<10;j++) { 201 | this.dit.render(this.ctx, i*this.getUnitSize(), j*this.getUnitSize(), this.getUnitSize(), this.getUnitSize()); 202 | } 203 | } 204 | 205 | if (!this.keyPressed) { 206 | this.pressAnyKey.render(this.ctx, this.getBoundingBox(), (p) => this.getPositionOnScreen(p)); 207 | this.keyPressed = !!this.game.player.controller.v.a; 208 | this.renderPostEffects(); 209 | return true; 210 | } 211 | this.introTime += dt; 212 | 213 | this.introText.render(this.ctx, this.getBoundingBox(), (p) => this.getPositionOnScreen(p.snapToGrid())); 214 | const p = new Point(2, Math.min(3, this.introTime / 300)); 215 | this.introText.rectangle.moveTo(p); 216 | if (p.y >= 3) { 217 | if (!this.playedIntroMusic) { 218 | AudioManager.get().intro.play(); 219 | this.playedIntroMusic = true; 220 | } 221 | const r = new Point(2, Math.max(8, 10-(this.introTime-800)/200)); 222 | this.author.rectangle.moveTo(r); 223 | this.author.render(this.ctx, this.getBoundingBox(), (p) => this.getPositionOnScreen(p.snapToGrid())); 224 | } 225 | this.renderPostEffects(); 226 | if (this.introTime > 3500) { 227 | return false; 228 | } 229 | return true; 230 | } 231 | 232 | renderHUD(game: Game) { 233 | // FIXME: should it be separate GO? 234 | const u = this.getUnitSize(); 235 | const c = this.ctx; 236 | const x = u / 4; 237 | const y = (this.getUnits() - 2) * u - x; 238 | const q = u / 4; 239 | c.strokeStyle = game.sceneSettings.hudBackground || "#1a403b"; 240 | c.fillStyle = c.strokeStyle; 241 | c.lineWidth = 5; 242 | c.fillRect(x + q/2, y + q/2, this.getUnitSize() * (this.getUnits() - 0.75), this.getUnitSize() * 1.75); 243 | c.strokeRect(x, y, this.getUnitSize() * (this.getUnits() - 0.5), this.getUnitSize() * 2); 244 | 245 | 246 | let health = game.player.life; 247 | for(let i=0;i<5;i+=1) { 248 | let e = E.health; 249 | if (i >= health) { 250 | e = E.healthOff; 251 | } 252 | e.render(c, x + q + i*u/2, y + q/2, u, u); 253 | } 254 | 255 | // FIXME: we can remove bomb icon from item. 256 | const bomb = game.player.items[1]; 257 | (new TextTexture([bomb.amount + ' x'], 2, 1, "#0000")).render(c, x, y + q / 2 + u, u, u/2) 258 | E.bomb.render(c, x + q + u/2+4, y + q / 2 + u, u, u); 259 | 260 | this.ctx.font = "16px"; 261 | this.ctx.fillStyle = "white"; 262 | 263 | const text = game.player.xpTexture; 264 | const lvlText = game.player.lvlTexture; 265 | text.render(this.ctx, 3*u, y+q, 2*u, u/2); 266 | lvlText.render(this.ctx, 8.5*u-u/4, y+q, 1.5*u, u/2); 267 | 268 | this.ctx.fillStyle = "rgba(0,0,0,0.5)"; 269 | const wid = 3.5*u 270 | this.ctx.fillRect(4.5*u, y+2*q, wid, q/2); 271 | this.ctx.fillStyle = "rgb(30, 30, 200)" 272 | this.ctx.fillRect(4.5*u, y+2*q,wid*game.player.lvlProgress, q/2); 273 | 274 | 275 | if (this.game.controller.gamepad) { 276 | E.controller.render(this.ctx, 9*u, 9*u, u/2, u/2) 277 | } else { 278 | E.keyboard.render(this.ctx, 9*u, 9*u, u/2, u/2) 279 | } 280 | 281 | if(game.objective) { 282 | const xDiff = game.objective.center.x - game.player.center.x ; 283 | const yDiff = game.objective.center.y - game.player.center.y; 284 | if (Math.abs(xDiff) > 5) { 285 | if (xDiff < 0) { 286 | E.goal.l.render(this.ctx, u/4, 3*u, u, u); 287 | } else { 288 | E.goal.r.render(this.ctx, 8.5*u, 3*u, u, u); 289 | 290 | } 291 | } 292 | 293 | if (Math.abs(yDiff) > 5) { 294 | if (yDiff < 0) { 295 | E.goal.u.render(this.ctx, 4.5*u, u/4, u, u); 296 | } else { 297 | E.goal.d.render(this.ctx, 4.5*u, 6.25*u, u, u); 298 | 299 | } 300 | } 301 | } 302 | } 303 | 304 | postCanvas!: HTMLCanvasElement; 305 | pattern!: CanvasPattern; 306 | p2!: CanvasPattern; 307 | 308 | renderInterruptorManager(man: Interruptor) { 309 | this.prepareFrame(); 310 | man.render(this.ctx, this.bb, (p: Point) => this.getPositionOnScreen(p)); 311 | } 312 | 313 | renderPostEffects() { 314 | if (this.game.MULTIPLIER <3) { 315 | // NO SPACE FOR POST PROCESSING 316 | return; 317 | } 318 | 319 | if (!this.game.settings.post) { 320 | return; 321 | } 322 | 323 | if (!this.postCanvas) { 324 | this.postCanvas = document.createElement('canvas'); 325 | const m = this.game.MULTIPLIER; 326 | this.postCanvas.width = this.postCanvas.height = m; 327 | const ctx = this.postCanvas.getContext('2d')!; 328 | ctx.filter = 'blur(1px)'; 329 | ctx.fillStyle = "red"; 330 | ctx.fillRect(m/2, 0, m / 2, m/2); 331 | ctx.fillStyle = "green"; 332 | ctx.fillRect(0, 0, m/2, m); 333 | ctx.fillStyle = "blue"; 334 | ctx.fillRect(m/2, m/2, m/2, m/2); 335 | 336 | this.pattern = ctx.createPattern(this.postCanvas, "repeat")!; 337 | 338 | } 339 | 340 | 341 | 342 | this.ctx.globalAlpha = 0.6; 343 | this.ctx.globalCompositeOperation = "color-burn"; 344 | this.ctx.fillStyle = this.pattern; 345 | this.ctx.fillRect(0, 0, this.width, this.height); 346 | 347 | this.ctx.globalCompositeOperation = "source-over"; 348 | this.ctx.globalAlpha = 1; 349 | 350 | } 351 | } --------------------------------------------------------------------------------