diff --git a/assets/img/environment.png b/assets/img/environment.png new file mode 100644 index 0000000..cb79a3d Binary files /dev/null and b/assets/img/environment.png differ diff --git a/assets/img/objects.png b/assets/img/objects.png new file mode 100644 index 0000000..e0814fb Binary files /dev/null and b/assets/img/objects.png differ diff --git a/assets/sounds/dungeon.mp3 b/assets/sounds/musics/dungeon.mp3 similarity index 100% rename from assets/sounds/dungeon.mp3 rename to assets/sounds/musics/dungeon.mp3 diff --git a/assets/sounds/sfx/msg_1.wav b/assets/sounds/sfx/msg_1.wav new file mode 100644 index 0000000..90bbfb5 Binary files /dev/null and b/assets/sounds/sfx/msg_1.wav differ diff --git a/assets/sounds/pew.mp3 b/assets/sounds/sfx/pew.mp3 similarity index 100% rename from assets/sounds/pew.mp3 rename to assets/sounds/sfx/pew.mp3 diff --git a/assets/sounds/sfx/pop.wav b/assets/sounds/sfx/pop.wav new file mode 100644 index 0000000..fb2ca76 Binary files /dev/null and b/assets/sounds/sfx/pop.wav differ diff --git a/css/style.css b/css/style.css index 9e30fd6..4bf220a 100644 --- a/css/style.css +++ b/css/style.css @@ -1,80 +1,97 @@ body { - background-color: black; + background-color: black; margin: 0; - padding: 0; - text-align: center; - overflow: hidden; + padding: 0; + text-align: center; + overflow: hidden; } #settings { - width: 15em; - text-align: left; - font-family: 'Courier New', Courier, monospace; - box-sizing: border-box; - position: fixed; - border-right: 1px solid #fff; - border-bottom: 1px solid #fff; - padding: .25em; - color: #fff; + width: 15em; + text-align: left; + font-family: 'Courier New', Courier, monospace; + box-sizing: border-box; + position: fixed; + border-right: 1px solid #fff; + border-bottom: 1px solid #fff; + padding: .25em; + color: #fff; } .settings-line { - height: 1.5em; + height: 1.5em; } .settings-label { - width: 15em; + width: 15em; } .settings-input-text { - width: 3em; - float: right; + width: 3em; + float: right; } canvas { - image-rendering: -moz-crisp-edges; /* Firefox */ - image-rendering: -webkit-crisp-edges; /* Webkit (Safari) */ - image-rendering: pixelated; /* Chrome */ - pointer-events: none; + image-rendering: -moz-crisp-edges; + /* Firefox */ + image-rendering: -webkit-crisp-edges; + /* Webkit (Safari) */ + image-rendering: pixelated; + /* Chrome */ + pointer-events: none; } #main-layers { - position: fixed; - left: 50%; - outline: 1px solid red; + position: fixed; + left: 50%; } -#main-layers canvas { - position: absolute; - left: 0; - top: 0; - transform: translate(-50%, 0); - -ms-transform: translate(-50%, 0); - background: transparent; + +#static-canvas, +#dynamic-canvas { + position: absolute; + left: 0; + top: 0; + transform: translate(-50%, 0); + -ms-transform: translate(-50%, 0); + background: transparent; } #static-canvas { - z-index: 0; + z-index: 0; - background-color: white; + background-color: white; } + #dynamic-canvas { - z-index: 1; + z-index: 1; } #minimap-canvas { - z-index: 2; + z-index: 2; - position: absolute; - top: 0; - right: 0; + position: absolute; + top: 0; + right: 0; } body { margin: 0; - padding: 0; - overflow: hidden; + padding: 0; + overflow: hidden; } .no-pointer-events { - pointer-events: none; -} \ No newline at end of file + pointer-events: none; +} + +#message-box { + z-index: 2; + border-radius: 6px; + padding-left: 1rem; + padding-right: 1rem; + + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); +} diff --git a/gulpfile.js b/gulpfile.js index 34dd42d..f7f8dc9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,6 +1,6 @@ /* Dependencies */ var gulp = require("gulp"), - runSequence = require("run-sequence"), + runSequence = require('gulp4-run-sequence'), del = require("del"), browserify = require("browserify"), source = require("vinyl-source-stream"), @@ -13,10 +13,10 @@ gulp.task("clean", () => del(["./dist"])); gulp.task("copy-html", () => gulp.src("src/*.html").pipe(gulp.dest("dist"))); gulp.task("copy-css", () => gulp.src("./css/**/*.css").pipe(gulp.dest("./dist/css"))); gulp.task("copy-assets", () => gulp.src("./assets/**/*.*").pipe(gulp.dest("./dist/assets"))); -gulp.task("copy-things", ["copy-html", "copy-css", "copy-assets"]); +gulp.task("copy-things", gulp.series("copy-html", "copy-css", "copy-assets")); /* TS */ -gulp.task("ts", function () { +gulp.task("ts", gulp.series(() => { return browserify({ basedir: ".", debug: true, @@ -28,13 +28,15 @@ gulp.task("ts", function () { .bundle() .pipe(source("bundle.js")) .pipe(gulp.dest("dist")); -}); +})); -/* Default */ -gulp.task("default", function () { - return runSequence( - "clean", - "copy-things", - "ts" - ); +gulp.task('default', function () { + return new Promise(function (resolve, reject) { + runSequence( + "clean", + "copy-things", + "ts" + ); + resolve(); + }); }); \ No newline at end of file diff --git a/package.json b/package.json index 30bfd60..ce2f6c9 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "devDependencies": { "browserify": "^16.2.2", "del": "^3.0.0", - "gulp": "^3.9.1", + "gulp": "^4.0.2", "gulp-typescript": "^5.0.0-alpha.3", - "run-sequence": "^2.2.1", + "gulp4-run-sequence": "^1.0.0", "tsify": "^4.0.0", "tslint": "^5.11.0", "typescript": "^3.0.1", diff --git a/src/actionable_entities.ts b/src/actionable_entities.ts new file mode 100644 index 0000000..5fdc34b --- /dev/null +++ b/src/actionable_entities.ts @@ -0,0 +1,21 @@ +import { ActionableEntity } from "./actionable_entity"; +import { Sign } from "./sign"; +import { Sprite } from "./sprite"; +import { Point } from "./point"; +import { angry_dialog, sample_dialog, glitchy_dialog } from "./messages/dialog_graph"; +/* +export function get_actionable_entities(canvas_w?: number, canvas_h?: number): ActionableEntity[] { + return [ + new Sign("angry_dialog", new Sprite(0, 0, 29, 31), new Point(canvas_w / 2 - 15, canvas_h / 2 - 100), 29, 31, true, 0, 1, null, 0.5, angry_dialog), + new Sign("sample_dialog", new Sprite(0, 0, 29, 31), new Point(canvas_w / 2 - 15, canvas_h / 2 - 100), 29, 31, true, 0, 1, null, 0.5, sample_dialog), + new Sign("glitchy_dialog", new Sprite(0, 0, 29, 31), new Point(canvas_w / 2 - 15, canvas_h / 2 - 100), 29, 31, true, 0, 1, null, 0.5, glitchy_dialog) + ] +}*/ + +export function get_actionable_entities(canvas_w?: number, canvas_h?: number): ActionableEntity[] { + return [ + new Sign("angry_dialog", new Sprite(0, 0, 29, 31), null, 29, 31, true, 0, 1, null, 0.5, angry_dialog), + new Sign("sample_dialog", new Sprite(0, 0, 29, 31), null, 29, 31, true, 0, 1, null, 0.5, sample_dialog), + new Sign("glitchy_dialog", new Sprite(0, 0, 29, 31), null, 29, 31, true, 0, 1, null, 0.5, glitchy_dialog) + ] +} \ No newline at end of file diff --git a/src/actionable_entity.ts b/src/actionable_entity.ts new file mode 100644 index 0000000..a53db9b --- /dev/null +++ b/src/actionable_entity.ts @@ -0,0 +1,52 @@ +import { Entity } from "./entity"; +import { Sprite } from "./sprite"; +import { Point } from "./point"; +import { IDrawable } from "./idrawable"; +import { IActionable } from "./iactionable"; +import { IMAGE_BANK } from "./main"; + +export abstract class ActionableEntity extends Entity implements IDrawable, IActionable { + public action_hitbox_ratio: number; + public action_hitbox: ActionableEntityHitbox; + public actionable: boolean; + public occuring: boolean; + + constructor(id: string, current_sprite: Sprite, position: Point, width: number, height: number, + has_collision_objects?: boolean, height_perspective?: number, + floor_level?: number, room_number?: number, action_hitbox_ratio?: number) { + + super(id, current_sprite, Point.copy(position), width, height, + has_collision_objects, height_perspective, floor_level, room_number); + this.action_hitbox_ratio = action_hitbox_ratio == null ? 0 : action_hitbox_ratio; + this.actionable = false; + this.occuring = false; + if (position != null) { + this.action_hitbox = new ActionableEntityHitbox(this.id + "-hitbox", null, + new Point(position.x - width * this.action_hitbox_ratio, position.y - height * this.action_hitbox_ratio), + width + width * this.action_hitbox_ratio * 2, height + height * this.action_hitbox_ratio * 2); + } + } + + public set_position(position: Point) { + this.position = new Point(position.x, position.y); + this.action_hitbox = new ActionableEntityHitbox(this.id + "-hitbox", null, + new Point(position.x - this.width * this.action_hitbox_ratio, position.y - this.height * this.action_hitbox_ratio), + this.width + this.width * this.action_hitbox_ratio * 2, this.height + this.height * this.action_hitbox_ratio * 2); + } + + public draw(ctx: CanvasRenderingContext2D): void { + ctx.drawImage(IMAGE_BANK.pictures[this.sprite_filename], + this.current_sprite.src_x, this.current_sprite.src_y, this.current_sprite.src_width, this.current_sprite.src_height, + this.position.x, this.position.y, this.width, this.height); + } + + public abstract action(): void; +} + +export class ActionableEntityHitbox extends Entity { + public action_hitbox: Entity; + + constructor(id: string, current_sprite: Sprite, position: Point, width: number, height: number) { + super(id, current_sprite, Point.copy(position), width, height, false); + } +} \ No newline at end of file diff --git a/src/character/jays.ts b/src/character/jays.ts index e02caba..027a86a 100644 --- a/src/character/jays.ts +++ b/src/character/jays.ts @@ -33,7 +33,7 @@ export class Jays extends Entity implements IDrawable { constructor() { // height is = body height + head height... super("jays", new Sprite(0, 20, 20, 20), new Point(canvas_W / 2 - 10, canvas_H / 2 - 20), - Jays.body_width, Jays.body_height + Jays.head_height); + Jays.body_width, Jays.body_height + Jays.head_height, true, Jays.head_height); this.sprite_filename = "assets/img/jays.png"; this.speed = 3; this._tear_delay = 480; @@ -102,7 +102,7 @@ export class Jays extends Entity implements IDrawable { class JaysHead extends Entity { constructor(id: string, current_sprite: Sprite, position: Point, width: number, height: number) { - super(id, current_sprite, Point.copy(position), width, height); + super(id, current_sprite, Point.copy(position), width, height, false); this.sprite_filename = "assets/img/jays.png"; } } \ No newline at end of file diff --git a/src/character/tear.ts b/src/character/tear.ts index c9eca2a..193ad8d 100644 --- a/src/character/tear.ts +++ b/src/character/tear.ts @@ -43,7 +43,7 @@ export class TearBasic extends Tear { public speed: number; public range: number; - private static _basic_tear_firing_sound = new AudioFile("assets/sounds/pew.mp3"); + private static _basic_tear_firing_sound = new AudioFile("assets/sounds/sfx/pew.mp3"); public get firing_sound(): AudioFile { return TearBasic._basic_tear_firing_sound; } constructor(pos: Point, direction: Direction, id?: string, current_sprite?: Sprite, width?: number, height?: number) { diff --git a/src/collision.ts b/src/collision.ts index 1577bc5..d8d8f0d 100644 --- a/src/collision.ts +++ b/src/collision.ts @@ -2,6 +2,7 @@ import { Entity } from "./entity"; import { IPositionable } from "./environment/positions_accessor"; import { Tile } from "./environment/tile"; import { Point } from "./point"; +import { Rect } from "./rect"; export class Rectangle implements IPositionable { @@ -21,7 +22,8 @@ export class Rectangle implements IPositionable { export class Collision { private constructor() { } - public static is_collision_rectangle(entity: Entity, object: Rectangle, next_position: Point = null): boolean { + public static is_collision_rectangle(entity: Entity, object: Rectangle, next_position: Point = null, height_perspective?: number): boolean { + height_perspective = height_perspective == null ? 0 : height_perspective; if (object == null) { return false; } @@ -29,9 +31,9 @@ export class Collision { next_position = entity.position; } - return Collision.is_collision( + return this.is_collision( next_position.x, next_position.y, next_position.x + entity.width, next_position.y + entity.height, - object.top_left.x, object.top_left.y, object.bottom_right.x, object.bottom_right.y + object.top_left.x, object.top_left.y, object.bottom_right.x, object.bottom_right.y - height_perspective ); } @@ -41,17 +43,35 @@ export class Collision { collisioner_y1 < collisionee_y2 && collisioner_y2 > collisionee_y1; } + public static is_collision_rect(rect1: Rect, rect2: Rect): boolean { + return this.is_collision(rect1.x, rect1.y, rect1.x + rect1.width, rect1.y + rect1.height, + rect2.x, rect2.y, rect2.x + rect2.width, rect2.y + rect2.height); + } + + public static is_collision_rects(rect1: Rect, rects: Rect[]): boolean { + for (let i = 0; i < rects.length; i++) { + if (this.is_collision(rect1.x, rect1.y, rect1.x + rect1.width, rect1.y + rect1.height, + rects[i].x, rects[i].y, rects[i].x + rects[i].width, rects[i].y + rects[i].height)) { + return true; + } + } + return false; + } + public static is_collision_entity_tile(entity: Entity, tile: Tile): boolean { - return Collision.is_collision(entity.position.x, entity.position.y, entity.position.x + entity.width, entity.position.y + entity.height, + return this.is_collision(entity.position.x, entity.position.y, entity.position.x + entity.width, entity.position.y + entity.height, tile.position.x, tile.position.y, tile.position.x + tile.width, tile.position.y + tile.height); } - public static is_collision_nextpos_entity_tile(next_position: Point, entity: Entity, tile: Tile): boolean { - return Collision.is_collision(next_position.x, next_position.y, next_position.x + entity.width, next_position.y + entity.height, - tile.position.x, tile.position.y, tile.position.x + tile.width, tile.position.y + tile.height); + public static is_collision_nextpos_entity_tile(next_position: Point, entity: Entity, tile: Tile, height_perspective?: number): boolean { + height_perspective = height_perspective == null ? 0 : height_perspective; + return this.is_collision(next_position.x, next_position.y, next_position.x + entity.width, next_position.y + entity.height, + tile.position.x, tile.position.y, tile.position.x + tile.width, tile.position.y + tile.height - height_perspective); } - /** - * TODO: I'd like to make Jays able to cross most of the warp before changing map without writing ugly code... - */ + public static is_collision_nextpos_entity(next_position: Point, entity1: Entity, entity2: Entity, height_perspective?: number): boolean { + height_perspective = height_perspective == null ? 0 : height_perspective; + return this.is_collision(next_position.x, next_position.y, next_position.x + entity1.width, next_position.y + entity1.height, + entity2.position.x, entity2.position.y, entity2.position.x + entity2.width, entity2.position.y + entity2.height - height_perspective); + } } \ No newline at end of file diff --git a/src/collision_delta.ts b/src/collision_delta.ts index b9c05a7..eb4f761 100644 --- a/src/collision_delta.ts +++ b/src/collision_delta.ts @@ -2,9 +2,11 @@ export class CollisionDelta { public is_collision: boolean; public delta_x: number; public delta_y: number; - constructor(is_collision?: boolean, delta_x?: number, delta_y?: number) { + public height_perspective: number; + constructor(is_collision?: boolean, delta_x?: number, delta_y?: number, height_perspective?: number) { + this.height_perspective = height_perspective == null ? 0 : height_perspective; this.is_collision = is_collision; this.delta_x = delta_x == null ? 0 : delta_x; - this.delta_y = delta_y == null ? 0 : delta_y; + this.delta_y = delta_y == null ? 0 : delta_y - this.height_perspective; } } \ No newline at end of file diff --git a/src/direction_event.ts b/src/direction_event.ts index 5aa8b08..fb5e083 100644 --- a/src/direction_event.ts +++ b/src/direction_event.ts @@ -34,4 +34,17 @@ export class DirectionEvent { break; } } + + public getCurrentDirectionToDraw(): Direction { + if(this.move_right) { + return Direction.RIGHT; + } + else if(this.move_left) { + return Direction.LEFT; + } + else if(this.move_up) { + return Direction.UP; + } + return Direction.DOWN; + } } \ No newline at end of file diff --git a/src/drawable_entity.ts b/src/drawable_entity.ts new file mode 100644 index 0000000..4598685 --- /dev/null +++ b/src/drawable_entity.ts @@ -0,0 +1,21 @@ +import { IPositionable } from "./environment/positions_accessor"; +import { Point } from "./point"; +import { Sprite } from "./sprite"; +import { IDrawable } from "./idrawable"; +import { Entity } from "./entity"; +import { IMAGE_BANK } from "./main"; + +export abstract class DrawableEntity extends Entity implements IPositionable, IDrawable { + + constructor(id: string, current_sprite: Sprite, pos: Point, width: number, height: number, + has_collision_objects?: boolean, height_perspective?: number, floor_level?: number, room_number?: number) { + + super(id, current_sprite, pos, width, height, has_collision_objects, height_perspective, floor_level, room_number); + } + + public draw(ctx: CanvasRenderingContext2D): void { + ctx.drawImage(IMAGE_BANK.pictures[this.sprite_filename], + this.current_sprite.src_x, this.current_sprite.src_y, this.current_sprite.src_width, this.current_sprite.src_height, + this.position.x, this.position.y, this.width, this.height); + } +} \ No newline at end of file diff --git a/src/entity.ts b/src/entity.ts index 353c06b..7e5d8d0 100644 --- a/src/entity.ts +++ b/src/entity.ts @@ -25,13 +25,33 @@ export abstract class Entity implements IPositionable { public position: Point; - constructor(id: string, current_sprite: Sprite, pos: Point, width: number, height: number) { + public has_collision_objects: boolean; + public height_perspective: number; + + public floor_level: number; + + public _room_number: number; + public get room_number(): number { return this._room_number; } + public set room_number(room_number: number) { + if (this._room_number != null) { + throw new Error("Cannot set a room number twice"); + } + this._room_number = room_number; + } + + constructor(id: string, current_sprite: Sprite, pos: Point, width: number, height: number, + has_collision_objects?: boolean, height_perspective?: number, floor_level?: number, room_number?: number) { + this._id = id; this.current_sprite = current_sprite; - this.position = new Point(pos.x, pos.y); + this.position = pos == null ? null : new Point(pos.x, pos.y); this._width = width; this._height = height; this.sprite_collecs = SpriteHelper.get_collecs(this.id); + this.has_collision_objects = has_collision_objects == null ? true : has_collision_objects; + this.height_perspective = height_perspective == null ? 0 : height_perspective; + this.floor_level = floor_level; + this.room_number = room_number; } public next_position(direction: Direction): Point { @@ -59,11 +79,10 @@ export abstract class Entity implements IPositionable { } public collision_map(direction: Direction, position: Point): CollisionDelta { - // Collision with walls const collision_rectangle = gameState.current_room.room_walls .get_walls_collisions_rectangles() - .find(rectangle => Collision.is_collision_rectangle(this, rectangle, position)); + .find(rectangle => Collision.is_collision_rectangle(this, rectangle, position, this.height_perspective)); if (collision_rectangle != null) { return this.get_collision_delta(direction, collision_rectangle); } @@ -72,19 +91,39 @@ export abstract class Entity implements IPositionable { for (let i = 0; i < gameState.current_room.height; i++) { for (let j = 0; j < gameState.current_room.width; j++) { const current_tile = gameState.current_room.tiles[i][j]; - if (!current_tile.has_collision || !Collision.is_collision_nextpos_entity_tile(position, this, current_tile)) { + if (!this.has_collision_objects || !current_tile.has_collision || !Collision.is_collision_nextpos_entity_tile(position, this, current_tile, this.height_perspective)) { continue; } return this.get_collision_delta(direction, current_tile); } } + // Collision with actionable entities + for (let i = 0; i < gameState.current_room.actionable_entities.length; i++) { + if (Collision.is_collision_nextpos_entity(position, this, gameState.current_room.actionable_entities[i].action_hitbox, this.height_perspective)) { + gameState.current_room.actionable_entities[i].actionable = true; + } else { + gameState.current_room.actionable_entities[i].actionable = false; + gameState.current_room.actionable_entities[i].occuring = false; // bof + } + if (Collision.is_collision_nextpos_entity(position, this, gameState.current_room.actionable_entities[i], this.height_perspective)) { + return this.get_collision_delta(direction, gameState.current_room.actionable_entities[i]); + } + } + + // Collision with drawable entities + for (let i = 0; i < gameState.current_room.drawable_entities.length; i++) { + if (Collision.is_collision_nextpos_entity(position, this, gameState.current_room.drawable_entities[i], this.height_perspective)) { + return this.get_collision_delta(direction, gameState.current_room.drawable_entities[i]); + } + } + return new CollisionDelta(false); } private get_collision_delta(direction: Direction, obstacle: IPositionable) { switch (direction) { - case Direction.UP: return new CollisionDelta(true, 0, PositionAccessor.bottom_y(obstacle) - PositionAccessor.top_y(this)); + case Direction.UP: return new CollisionDelta(true, 0, PositionAccessor.bottom_y(obstacle) - PositionAccessor.top_y(this), this.height_perspective); case Direction.DOWN: return new CollisionDelta(true, 0, PositionAccessor.top_y(obstacle) - PositionAccessor.bottom_y(this)); case Direction.LEFT: return new CollisionDelta(true, PositionAccessor.right_x(obstacle) - PositionAccessor.left_x(this), 0); case Direction.RIGHT: return new CollisionDelta(true, PositionAccessor.left_x(obstacle) - PositionAccessor.right_x(this), 0); diff --git a/src/environment/entities/rock.ts b/src/environment/entities/rock.ts new file mode 100644 index 0000000..7b7a319 --- /dev/null +++ b/src/environment/entities/rock.ts @@ -0,0 +1,24 @@ +import { DrawableEntity } from "../../drawable_entity"; +import { Sprite } from "../../sprite"; +import { Point } from "../../point"; + +export class Rock extends DrawableEntity { + constructor(type: number, position?: Point, floor_level?: number, room_number?: number) { + let id: string; + let sprite: Sprite; + let width: number; + let height: number; + switch (type) { + case 2: + id = "rock-2"; + sprite = new Sprite(0, 0, 60, 40); + width = 60; + height = 40; + break; + } + super(id, sprite, position, width, height, true, 0, floor_level, room_number); + this.sprite_filename = "assets/img/environment.png"; + } +} + +export const ROCK_2 = new Rock(2); \ No newline at end of file diff --git a/src/environment/floor_map.ts b/src/environment/floor_map.ts index 24865ba..2488207 100644 --- a/src/environment/floor_map.ts +++ b/src/environment/floor_map.ts @@ -86,7 +86,7 @@ export class FloorMap implements IDrawable { const grid_generation_result = map_generator.generate_grid(this.max_floor_map_width, this.max_floor_map_height); this.max_floor_map_height = grid_generation_result.grid.length; this.max_floor_map_width = grid_generation_result.grid[0].length; - this.maps_grid = map_generator.generate_rooms(grid_generation_result, this.floor); + this.maps_grid = map_generator.generate_rooms(grid_generation_result, this.floor); this.current_position = grid_generation_result.init_point; } @@ -171,6 +171,12 @@ export class FloorMap implements IDrawable { renderer.fill_round_rect(context, destination.x, destination.y, config.sizes.room_width, config.sizes.room_height, 3); renderer.stroke_round_rect(context, destination.x, destination.y, config.sizes.room_width, config.sizes.room_height, 3); + + context.fillStyle = "black"; + context.font = "30px"; + context.strokeText("" + room.id, destination.x + config.sizes.room_width / 2, destination.y + config.sizes.room_height / 2); + context.fillStyle = "white"; + context.fillText("" + room.id, destination.x + config.sizes.room_width / 2, destination.y + config.sizes.room_height / 2); } private draw_custom_room( diff --git a/src/environment/floors/floor.ts b/src/environment/floors/floor.ts index e53912b..954faf2 100644 --- a/src/environment/floors/floor.ts +++ b/src/environment/floors/floor.ts @@ -5,6 +5,12 @@ import { Point } from "../../point"; import { FloorMap } from "../floor_map"; import { get_room_map_definitions, RoomMapDefinition } from "../rooms/room_map_definition.decorator"; import { Door } from "../walls/door"; +import { RoomMap } from "../rooms/room_map"; +import { ActionableEntity } from "../../actionable_entity"; +import { ArrayUtil, MathUtil } from "../../util"; +import { get_actionable_entities } from "../../actionable_entities"; +import { Collision } from "../../collision"; +import { Rect } from "../../rect"; export abstract class Floor { public abstract get level(): number; @@ -13,7 +19,6 @@ export abstract class Floor { // NB: music can't be played if the user hasn't interacted with the page. Otherwise: // DOMException: play() failed because the user didn’t interact with the document first public abstract get base_music(): AudioFile; - // public abstract get available_rooms(): RoomMap[]; protected _floor_map: FloorMap; public get floor_map(): FloorMap { return this._floor_map; } @@ -21,25 +26,56 @@ export abstract class Floor { protected abstract _available_rooms: any[]; public get available_rooms(): RoomMapDefinition[] { return get_room_map_definitions(this._available_rooms); } - constructor() { } + public rooms: RoomMap[]; + public rooms_ids: number[]; + + public actionable_entities: ActionableEntity[]; + + constructor() { + this.rooms = new Array(); + } public initialize(): void { + this.rooms_ids = new Array(); this._floor_map = new FloorMap(this); + + this.spread_actionable_entities(); + // Draw minimap this.floor_map.next_room(); renderer.update_minimap(this.floor_map); } + public spread_actionable_entities(): void { + this.actionable_entities = get_actionable_entities(canvas_W, canvas_H); + this.spread_entities(["angry_dialog", "sample_dialog", "glitchy_dialog"], /*ArrayUtil.diff(*/this.rooms_ids/*, [this.first_room_id])*/); + this.rooms.forEach( + room => { + room.actionable_entities.forEach(entity => { + let good_location = false; + while (!good_location) { + entity.set_position(new Point( + MathUtil.get_random_int(60, canvas_W - entity.width - 60), + MathUtil.get_random_int(60, canvas_H - entity.height - 60) + )); + good_location = !Collision.is_collision_rects( + new Rect(entity.position.x, entity.position.y, entity.width, entity.height), + room.taken_spaces); + } + room.taken_spaces.push(new Rect(entity.position.x, entity.position.y, entity.width, entity.height)); + }); + } + ); + } + public on_collision_warp(door: Door) { // Remove tears gameState.tears.splice(0, gameState.tears.length); gameState.current_room = this.floor_map.next_room(door.direction); - console.log(gameState.current_room.requires_update); renderer.update_current_room(gameState.current_room); - console.log(gameState.current_room.requires_update); switch (door.direction) { case Direction.LEFT: gameState.jays.position = new Point(canvas_W - gameState.current_room.room_walls.wall_height - gameState.jays.width, (canvas_H / 2) - (gameState.jays.height / 2)); @@ -59,7 +95,18 @@ export abstract class Floor { renderer.update_minimap(this.floor_map); } - // public get_available_rooms(): RoomMapDefinition[] { - // const a = typeof(RoomMap); - // } + public spread_entities(entities_ids: string[], rooms_ids: number[]): void { + if (entities_ids.length > rooms_ids.length) { + throw new Error("Cannot spread entities when there are more entities than rooms"); + } + + let i = 0; + const shuffled_rooms_ids = ArrayUtil.shuffle(rooms_ids); + entities_ids.forEach(entity_id => { + console.log("Attribute entity " + entities_ids[i] + " to room " + shuffled_rooms_ids[i]); + this.rooms.find(room => room.id === shuffled_rooms_ids[i]).actionable_entities + .push(this.actionable_entities.find(entity => entity.id === entity_id)); + i++; + }); + } } \ No newline at end of file diff --git a/src/environment/floors/one/temple_floor.ts b/src/environment/floors/one/temple_floor.ts index 70a6f66..d5dd7b6 100644 --- a/src/environment/floors/one/temple_floor.ts +++ b/src/environment/floors/one/temple_floor.ts @@ -17,14 +17,14 @@ export class TempleFloor extends Floor { protected _available_rooms = [ FourFireRoom, EmptyGrassRoom, - RockyRoom, + RockyRoom/*, WaterLeftRightRoom, - DeadEndRightRoom + DeadEndRightRoom*/ ]; constructor() { super(); - this._base_music = new AudioFile("assets/sounds/dungeon.mp3"); + this._base_music = new AudioFile("assets/sounds/musics/dungeon.mp3"); this.base_music.loop = true; } } \ No newline at end of file diff --git a/src/environment/map_generator.ts b/src/environment/map_generator.ts index 234b0a8..4fdf2a4 100644 --- a/src/environment/map_generator.ts +++ b/src/environment/map_generator.ts @@ -94,7 +94,7 @@ export class MapGenerator { const grid = generation_result.grid; const result = new Array(grid.length); const available_rooms = floor.available_rooms; - + let i = 1; for (let y = 0; y < grid.length; ++y) { result[y] = new Array(grid[y].length); for (let x = 0; x < grid[y].length; ++x) { @@ -113,6 +113,10 @@ export class MapGenerator { const rand = MathUtil.get_random_int(possible_rooms.length); result[y][x] = possible_rooms[rand].get_room_map(doors_directions); + result[y][x].id = i; + floor.rooms.push(result[y][x]); + floor.rooms_ids.push(i); + i++; } } return result; diff --git a/src/environment/rooms/four_fire_room.ts b/src/environment/rooms/four_fire_room.ts index 16dac43..8d59f48 100644 --- a/src/environment/rooms/four_fire_room.ts +++ b/src/environment/rooms/four_fire_room.ts @@ -1,5 +1,5 @@ import { Direction } from "../../enum"; -import { canvas_H } from "../../main"; +import { canvas_H, canvas_W } from "../../main"; import { Point } from "../../point"; import { IRawMap } from "../irawmap"; import { RoomCornerWall } from "../walls/corner_wall"; @@ -70,6 +70,8 @@ export class FourFireRoom extends RoomMap { ]; super(raw_map, new RoomWalls(side_walls, corner_walls, doors, custom_elements)); + + this.generate_drawable_entities_random_location("rock-2", 2, 5); } } \ No newline at end of file diff --git a/src/environment/rooms/rocky_room.ts b/src/environment/rooms/rocky_room.ts index fd2757a..7a4ee48 100644 --- a/src/environment/rooms/rocky_room.ts +++ b/src/environment/rooms/rocky_room.ts @@ -72,6 +72,8 @@ export class RockyRoom extends RoomMap { ]; super(raw_map, new RoomWalls(side_walls, corner_walls, doors, custom_elements)); + + this.generate_drawable_entities_random_location("rock-2", 8, 15); } } \ No newline at end of file diff --git a/src/environment/rooms/room_map.ts b/src/environment/rooms/room_map.ts index 2ec9e4a..c2af37e 100644 --- a/src/environment/rooms/room_map.ts +++ b/src/environment/rooms/room_map.ts @@ -2,8 +2,24 @@ import { IUpdatableDrawable } from "../../idrawable"; import { IRawMap } from "../irawmap"; import { Tile, TILE_REF, TILE_TYPES } from "../tile"; import { RoomWalls } from "../walls/room_walls"; +import { DrawableEntity } from "../../drawable_entity"; +import { Rect } from "../../rect"; +import { MathUtil } from "../../util"; +import { Rock, ROCK_2 } from "../entities/rock"; +import { Point } from "../../point"; +import { canvas_W, canvas_H } from "../../main"; +import { Collision } from "../../collision"; +import { ActionableEntity } from "../../actionable_entity"; export abstract class RoomMap implements IUpdatableDrawable { + protected _id: number; + public get id(): number { return this._id; } + public set id(id: number) { + if (this._id != null) { + throw new Error("Cannot set a room id twice"); + } + this._id = id; + } protected _raw_map: IRawMap; public get raw_map(): IRawMap { return this._raw_map; } @@ -25,6 +41,10 @@ export abstract class RoomMap implements IUpdatableDrawable { public get requires_update(): boolean { return this.room_walls.requires_update; } + public drawable_entities: DrawableEntity[]; + public actionable_entities: ActionableEntity[]; + public taken_spaces: Rect[]; + public set has_been_visited(value: boolean) { if (value == null) { throw new Error("Property 'has_been_visited' can not be null"); @@ -47,17 +67,47 @@ export abstract class RoomMap implements IUpdatableDrawable { this._has_been_glimpsed = value; } - constructor(raw_map: IRawMap, wall: RoomWalls) { + constructor(raw_map: IRawMap, walls: RoomWalls, drawable_entities?: DrawableEntity[]) { if (raw_map == null) { throw new Error("parameter `raw_map` cannot be null"); } - if (wall == null) { + if (walls == null) { throw new Error("parameter `wall` cannot be null"); } this._raw_map = raw_map; - this._room_walls = wall; + this._room_walls = walls; this._tiles = this.get_tiles(this.raw_map, this._room_walls); + this.drawable_entities = drawable_entities == null ? new Array() : drawable_entities; + this.actionable_entities = new Array(); + this.taken_spaces = new Array(); + this.set_taken_spaces(); + } + + private set_taken_spaces(): void { + this.taken_spaces = new Array(); + this.tiles.forEach( + tiles_line => { + tiles_line.forEach( + tile => { + if (tile.has_collision) { + this.taken_spaces.push(new Rect(tile.position.x, tile.position.y, tile.width, tile.height)); + } + } + ); + } + ); + this.taken_spaces.push(new Rect(canvas_W / 2 - 10, canvas_H / 2 - 20, 20, 40)); // Jays spawn + this.room_walls.doors.forEach( + door => { + this.taken_spaces.push(new Rect( + door.position.x - door.width, + door.position.y - door.height, + door.width * 3, + door.height * 3 + )); + } + ); } protected get_tiles(raw_map: IRawMap, room_walls: RoomWalls): Tile[][] { @@ -89,6 +139,39 @@ export abstract class RoomMap implements IUpdatableDrawable { return TILE_TYPES[id] || TILE_REF; } + public generate_drawable_entity_random_location(type: string) { + let good_location = false; + let drawable_entity: DrawableEntity; + const nbTries = 20; + let i = 0; + while (!good_location && i < nbTries) { + switch (type) { + case "rock-2": + drawable_entity = new Rock(2, new Point( + MathUtil.get_random_int(60, canvas_W - ROCK_2.width - 60), + MathUtil.get_random_int(60, canvas_H - ROCK_2.height - 60))) + break; + default: + throw new Error("Drawable entity type '" + type + "' doesn't exist"); + } + good_location = !Collision.is_collision_rects( + new Rect(drawable_entity.position.x, drawable_entity.position.y, drawable_entity.width, drawable_entity.height), + this.taken_spaces); + i++; + } + if (i < nbTries) { + this.drawable_entities.push(drawable_entity); + this.taken_spaces.push(new Rect(drawable_entity.position.x, drawable_entity.position.y, drawable_entity.width, drawable_entity.height)); + } + } + + public generate_drawable_entities_random_location(type: string, nb: number, max?: number) { + nb = max == null ? nb : MathUtil.get_random_int(nb, max); + for (let i = 0; i < nb; i++) { + this.generate_drawable_entity_random_location(type); + } + } + public draw(ctx: CanvasRenderingContext2D): void { // Draw room walls this._room_walls.draw(ctx); diff --git a/src/gamestate.ts b/src/gamestate.ts index b705ba9..53540f3 100644 --- a/src/gamestate.ts +++ b/src/gamestate.ts @@ -2,7 +2,7 @@ import { AttackDirectionEvent } from "./attack_direction_event"; import { Jays } from "./character/jays"; import { Tear, TearBasic } from "./character/tear"; import { DirectionEvent } from "./direction_event"; -import { Arrow_Direction, Direction } from "./enum"; +import { Arrow_Direction, Direction, Direction_Int } from "./enum"; import { Floor } from "./environment/floors/floor"; import { TempleFloor } from "./environment/floors/one/temple_floor"; import { RoomMap } from "./environment/rooms/room_map"; @@ -14,6 +14,13 @@ import { Timer } from "./timer"; import { TIMERS } from "./timers"; import { TouchHelper } from "./touch_helper"; import { ArrayUtil, SetUtil } from "./util"; +import { MessageBox } from "./messages/message_box"; +import { ActionableEntity } from "./actionable_entity"; +import { Sign } from "./sign"; +import { Sprite } from "./sprite"; +import { first_sign } from "./messages/dialog_graph"; +import { get_actionable_entities } from "./actionable_entities"; +import { DrawableEntity } from "./drawable_entity"; export class GameState { public current_room: RoomMap; @@ -25,11 +32,16 @@ export class GameState { public tears: Tear[]; public joysticks: Joysticks; public touches: TouchList; + public current_message: MessageBox; + public paused: boolean; + // public actionable_entities: ActionableEntity[]; // move to floor + public first_room_id: number; constructor() { this.current_floor = new TempleFloor(); this.current_floor.initialize(); this.current_room = this.current_floor.floor_map.current_room; + this.first_room_id = this.current_room.id; renderer.update_current_room(this.current_room); this.direction_event = new DirectionEvent(); @@ -39,7 +51,15 @@ export class GameState { this.joysticks = new Joysticks(); + this.paused = false; + this.jays = new Jays(); + + // this.actionable_entities = get_actionable_entities(canvas_W, canvas_H); + // this.actionable_entities.push(new Sign("first_sign", new Sprite(0, 0, 29, 31), new Point(canvas_W / 2 - 15, canvas_H / 2 - 100), 29, 31, true, 0, 1, this.first_room_id, 0.5, first_sign)); + // this.spread_entities(["angry_dialog", "sample_dialog", "glitchy_dialog"], ArrayUtil.diff(this.current_floor.rooms_ids, [this.first_room_id])); + // this.current_room.actionable_entities.push(new Sign("first_sign", new Sprite(0, 0, 29, 31), new Point(canvas_W / 2 - 15, canvas_H / 2 - 100), 29, 31, true, 0, 1, this.first_room_id, 0.5, first_sign)); + document.onkeyup = event => this.key_up(event.key); document.onkeydown = event => this.key_down(event.key); @@ -171,7 +191,9 @@ export class GameState { public remove_direction_event(direction: Direction): void { if (direction != null && SetUtil.remove_from_array(this.directions_keyDown, direction)) { this.direction_event.setDirection(direction, false); - this.jays.direction_key_up(direction); + if (!this.paused) { + this.jays.direction_key_up(direction); + } } } @@ -205,6 +227,26 @@ export class GameState { case "f": renderer.scale(); break; + // Spacebar + case " ": + if (this.current_message != null) { + this.current_message.on_action_button(); + } + this.action(); + break; + case "p": + this.toggle_pause(); + break; + case "ArrowDown": + if (this.current_message != null) { + this.current_message.on_choice_button(Direction.DOWN); + } + break; + case "ArrowUp": + if (this.current_message != null) { + this.current_message.on_choice_button(Direction.UP); + } + break; } } @@ -226,13 +268,29 @@ export class GameState { tear.draw(dynamic_ctx); }); - this.jays.update(); - this.tears_update(); + if (!this.paused) { + this.jays.update(); + this.tears_update(); + } + + this.current_room.actionable_entities.forEach(actionable_entity => { + actionable_entity.draw(dynamic_ctx); + }); + + this.current_room.drawable_entities.forEach(drawable_entity => { + drawable_entity.draw(dynamic_ctx); + }); this.jays.draw(dynamic_ctx); + if (this.current_message != null) { + this.current_message.draw(); + } + this.touch_move(); + dynamic_ctx.restore(); + const self = this; window.requestAnimationFrame(() => self.update()); } @@ -267,4 +325,31 @@ export class GameState { public get_timer(id: string): Timer { return TIMERS.find(item => item.id === id); } + + public pause(): void { + this.paused = true; + this.jays.current_sprite = this.jays.sprite_collecs.get("MOTIONLESS")[Direction_Int.get(this.direction_event.getCurrentDirectionToDraw())]; + } + + public resume(): void { + this.paused = false; + } + + public toggle_pause(): void { + if (this.paused) { + this.resume(); + } + else { + this.pause(); + } + } + + public action(): void { + this.current_room.actionable_entities.forEach(actionable_entity => { + if (actionable_entity.actionable && !actionable_entity.occuring) { + actionable_entity.action(); + return; + } + }); + } } \ No newline at end of file diff --git a/src/iactionable.ts b/src/iactionable.ts new file mode 100644 index 0000000..853dd2e --- /dev/null +++ b/src/iactionable.ts @@ -0,0 +1,3 @@ +export interface IActionable { + action(): void; +} diff --git a/src/image_bank.ts b/src/image_bank.ts index 193e481..0d1ffca 100644 --- a/src/image_bank.ts +++ b/src/image_bank.ts @@ -12,7 +12,9 @@ export class ImageBank { "assets/img/jays.png", "assets/img/tear.png", "assets/img/walls/floor_one.png", - "assets/img/minimap_icons.png" + "assets/img/minimap_icons.png", + "assets/img/objects.png", + "assets/img/environment.png" ]; constructor() { diff --git a/src/index.html b/src/index.html index 5087ac4..ba39ab4 100644 --- a/src/index.html +++ b/src/index.html @@ -35,7 +35,7 @@
- +
diff --git a/src/main.ts b/src/main.ts index 45d2348..5198d97 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,8 @@ export const IMAGE_BANK = new ImageBank(); export const renderer = new Renderer(); export let gameState: GameState; -//TODO: add a localstorage service to retrieve the value the user wants to use +// TODO: add a localstorage service to retrieve the value the user wants to use +// --> PouchDB? export const key_mapper = new KeyMapper(); window.onload = () => { @@ -33,6 +34,7 @@ window.onload = () => { renderer.autoScale(); gameState = new GameState(); Settings.init(); + gameState.get_timer("tear").interval = gameState.jays.tear_delay; gameState.update(); }); diff --git a/src/messages/dialog_graph.ts b/src/messages/dialog_graph.ts new file mode 100644 index 0000000..ef74925 --- /dev/null +++ b/src/messages/dialog_graph.ts @@ -0,0 +1,128 @@ +export class DialogGraph { + private _character: string; + public get character(): string { return this._character; } + + private _first_node: IDialogNode; + public get first_node(): IDialogNode { return this._first_node; } + + constructor(character: string, first_node: IDialogNode) { + this._character = character; + this._first_node = first_node; + } +} + +export enum DialogAnimation { + None = 0, + Shaky = 1, + Glitchy_Spinny = 2, + Glitchy_Splitted = 3, + Glitchy_Uppercase = 4 +} + +export interface IDialogAnimation { + type: DialogAnimation; + factor?: number; +} + +export interface IDialogNode { + message: string; + animation?: IDialogAnimation; + next_node?: IDialogNode; + action?: () => void; +} + +export interface IQuestionNode extends IDialogNode { + answers: IDialogNode[]; +} + +export function get_animation(node: IDialogNode): DialogAnimation { + return node.animation != null ? node.animation.type : DialogAnimation.None; +} + +export function get_animation_factor(node: IDialogNode): number { + return node.animation != null && node.animation.factor != null ? node.animation.factor : 0; +} + +export const sample_dialog = { + message: "Shrubberies are my trade. I am a shrubber.", + next_node: { + message: "My name is Roger the Shrubber. I arrange, design, and sell shrubberies.", + animation: { type: DialogAnimation.Shaky, factor: 4 }, + next_node: { + message: "Are you saying Ni to that old woman?", + answers: [ + { + message: "Um, yes.", + animation: { type: DialogAnimation.None }, + next_node: { + message: "Oh, what sad times are these when passing ruffians can say Ni at will to old ladies." + } + }, + { + message: "Ni!", + animation: { type: DialogAnimation.Shaky, factor: 4 }, + next_node: { + message: "There is a pestilence upon this land, nothing is sacred. Even those who arrange and design shrubberies are under considerable economic stress in this period in history." + } + } + ] + } + } +}; + +export const first_sign = { + message: "after 2 years of abandonment, Jays development is back.", + next_node: { + message: "Brace yourself.", + animation: { type: DialogAnimation.Shaky, factor: 4 }, + } +}; + +export const angry_dialog = { + message: "Hello there! How are you? ^_^", + next_node: { + message: "Why don't you answer me...?", + animation: { type: DialogAnimation.Shaky, factor: 2 }, + next_node: { + message: "Please stop ignoring me...", + animation: { type: DialogAnimation.Shaky, factor: 4 }, + next_node: { + message: "Don't you think I've already suffered enough? Do you even know what I've been through until now?", + animation: { type: DialogAnimation.Shaky, factor: 6 }, + next_node: { + message: "Oh sure! Keep not answering! After all, you're smarter than everyone! You're above EVERYTHING", + animation: { type: DialogAnimation.Shaky, factor: 8 }, + next_node: { + message: "HEY, I'M TALKING TO YOU, YOU BRAT", + animation: { type: DialogAnimation.Shaky, factor: 10 }, + next_node: { + message: "I'm so angry that my text starts to shakea bit too much right now...", + animation: { type: DialogAnimation.Shaky, factor: 15 }, + next_node: { + message: "But after all, you're right... In this world. it's kill or to be killed.", + animation: { type: DialogAnimation.Shaky, factor: 20 }, + next_node: { + message: "Anyway, stop playing Cloud at Smash Bros, you fucking noob!", + animation: { type: DialogAnimation.Shaky, factor: 100 } + } + } + } + } + } + } + } + } +}; + +export const glitchy_dialog = { + message: "[GLITCHY_SPINNY] Hi There. I'm an Hey-Aye the AI. I'm not a bad guy. But I'm not nice either. To be honest, I don't care. Die.", + animation: { type: DialogAnimation.Glitchy_Spinny }, + next_node: { + message: "[GLITCHY_SPLITTED] Hi There. I'm an Hey-Aye the AI. I'm not a bad guy. But I'm not nice either. To be honest, I don't care. Die.", + animation: { type: DialogAnimation.Glitchy_Splitted, factor: 25 }, + next_node: { + message: "[GLITCHY_UPPERCASE] Hi There. I'm an Hey-Aye the AI. I'm not a bad guy. But I'm not nice either. To be honest, I don't care. Die.", + animation: { type: DialogAnimation.Glitchy_Uppercase }, + } + } +}; \ No newline at end of file diff --git a/src/messages/imessage_box_configuration.ts b/src/messages/imessage_box_configuration.ts new file mode 100644 index 0000000..80f9fc0 --- /dev/null +++ b/src/messages/imessage_box_configuration.ts @@ -0,0 +1,29 @@ +export interface IMessageBoxSettings { + fontFamily?: string; + fontColor?: string; + background?: string; + border?: string; + soundPath?: string; +} + +export const DEFAULT_MESSAGE_BOX_SETTINGS: IMessageBoxSettings = { + fontFamily: "Comic Sans MS", + fontColor: "white", + background: "black", + border: "1px solid white", + soundPath: "assets/sounds/sfx/msg_1.wav" +}; + +export function buildMessageBoxSettings(settings: IMessageBoxSettings): IMessageBoxSettings { + if (settings == null) { + settings = {}; + } + + return { + fontFamily: settings.fontFamily || DEFAULT_MESSAGE_BOX_SETTINGS.fontFamily, + fontColor: settings.fontColor || DEFAULT_MESSAGE_BOX_SETTINGS.fontColor, + background: settings.background || DEFAULT_MESSAGE_BOX_SETTINGS.background, + border: settings.border || DEFAULT_MESSAGE_BOX_SETTINGS.border, + soundPath: settings.soundPath || DEFAULT_MESSAGE_BOX_SETTINGS.soundPath + }; +} \ No newline at end of file diff --git a/src/messages/message_box.ts b/src/messages/message_box.ts new file mode 100644 index 0000000..8db32e7 --- /dev/null +++ b/src/messages/message_box.ts @@ -0,0 +1,369 @@ +import { AudioFile } from "../audio_file"; +import { gameState, renderer, static_canvas } from "../main"; +import { Timer } from "../timer"; +import { DialogGraph, IDialogNode, DialogAnimation, IQuestionNode as IQuestionNode, get_animation, get_animation_factor } from "./dialog_graph"; +import { buildMessageBoxSettings, IMessageBoxSettings } from "./imessage_box_configuration"; +import { MathUtil } from "../util"; +import { Direction } from "../enum"; + +/** + * TODO: Triggers, Next messages, Stop, Text scrolling, Storage + */ +export class MessageBox { + /** Bottom margin, so that the text is not stuck to the bottom of the message box */ + private static readonly _bottom_margin = 20; + private static readonly DOM_ID = "message-box"; + + /** Characters which shouldn't make any sound */ + private static readonly _silentCharacters = new Set([" ", ".", ",", ";"]); + + private _canvas: HTMLCanvasElement; + private _context: CanvasRenderingContext2D; + + private _audio: AudioFile; + + private _settings: IMessageBoxSettings; + + private _timer: Timer; + + private _width: number; + private _height: number; + + private _content: DialogGraph; + private _current_node: IDialogNode; + private _lines: string[]; + private _current_character_index: number; + private _current_line_index: number; + private _selected_choice_index: number; + + private _line_spacing: number; + private _character_spacing: number; + + private _choice_sound: AudioFile; + + // Helpers + private get _current_line(): string { return this._lines[this._current_line_index]; } + private get _current_char(): string { return this._current_line[this._current_character_index]; } + + /** Returns true if the current message has been displayed entirely, false otherwise */ + private get _message_has_ended(): boolean { return this._current_line_index === this._lines.length - 1 && this._current_character_index === this._current_line.length - 1; } + + /** Returns the current node if it is a choice node, null otherwise */ + private get _current_question_node(): IQuestionNode { + return (this._current_node).answers != null && (this._current_node).answers.length > 0 ? this._current_node : null; + } + + private get_character_x_offset = (line: string, character_index: number): number => this._context.measureText(line.substring(0, character_index)).width * (1 + this._character_spacing); + + private get_character_y_offset = (line_index: number): number => (1 + line_index) * parseInt(this._context.font) * (1 + this._line_spacing); + + constructor(content: DialogGraph, boxSettings?: IMessageBoxSettings) { + this._content = content; + + this._settings = buildMessageBoxSettings(boxSettings); + + this._width = static_canvas.width; + this._height = static_canvas.height / 4; + + this._line_spacing = 0.4; + this._character_spacing = 0; + + this._current_character_index = 0; + this._current_line_index = 0; + } + + public start() { + this._canvas = document.createElement("canvas"); + this._context = this._canvas.getContext("2d", { alpha: true }); + renderer.disableCanvasSmoothing(this._context); + this._canvas.id = MessageBox.DOM_ID; + + this._audio = new AudioFile(this._settings.soundPath); + this._choice_sound = new AudioFile("assets/sounds/sfx/pop.wav"); + + // Background + this._canvas.style.background = this._settings.background; + this._canvas.style.border = this._settings.border; + + // Position + this._canvas.width = this._width; + this._canvas.height = this._height + MessageBox._bottom_margin; + + // Font + this._context.fillStyle = this._settings.fontColor; + this._context.font = `${this._height / 6}px ${this._settings.fontFamily}`; + + // Timer (important: init timer before loading the node) + this._timer = gameState.get_timer("textbox"); + this._timer.enable(); + + this.load_node(this._content.first_node); + + document.getElementById("main-layers").appendChild(this._canvas); + gameState.current_message = this; + gameState.pause(); + } + + public draw() { + if (!this._timer.next_tick()) { + return; + } + + switch (get_animation(this._current_node)) { + case DialogAnimation.None: + // Draw current character + this._context.fillText(this._current_char, this.get_character_x_offset(this._current_line, this._current_character_index), this.get_character_y_offset(this._current_line_index)); + break; + case DialogAnimation.Shaky: + this._context.save(); + this._context.clearRect(0, 0, this._width, this._height + MessageBox._bottom_margin); + + for (let line_index = 0; line_index <= this._current_line_index; ++line_index) { + for ( + let character_index = 0; + character_index < (line_index === this._current_line_index ? this._current_character_index + 1 : this._lines[line_index].length); + ++character_index + ) { + const char = this._lines[line_index][character_index]; + this._context.fillText( + char, + this.get_character_x_offset(this._lines[line_index], character_index) + MathUtil.get_random_int(get_animation_factor(this._current_node)), + this.get_character_y_offset(line_index) + MathUtil.get_random_int(get_animation_factor(this._current_node)) + ); + } + } + this._context.restore(); + break; + case DialogAnimation.Glitchy_Spinny: + this._context.save(); + this._context.clearRect(0, 0, this._width, this._height + MessageBox._bottom_margin); + + for (let line_index = 0; line_index <= this._current_line_index; ++line_index) { + const rng_character_index = MathUtil.get_random_int(this._lines[line_index].length - 1); + for ( + let character_index = 0; + character_index < (line_index === this._current_line_index ? this._current_character_index + 1 : this._lines[line_index].length); + ++character_index + ) { + const char = this._lines[line_index][character_index]; + if (character_index === rng_character_index) { + const letter_width = this._context.measureText(char).width; + const letter_height = parseInt(this._context.font); + + this._context.translate(this.get_character_x_offset(this._lines[line_index], character_index) + letter_width / 2, + this.get_character_y_offset(line_index) - letter_height / 2); + this._context.rotate(MathUtil.get_random_int(Math.PI * 100) / 100); + this._context.fillText(char, -letter_width / 2, letter_height / 2); + this._context.setTransform(1, 0, 0, 1, 0, 0); + } + else { + this._context.fillText( + char, + this.get_character_x_offset(this._lines[line_index], character_index), + this.get_character_y_offset(line_index) + ); + } + } + } + this._context.restore(); + break; + case DialogAnimation.Glitchy_Splitted: + this._context.save(); + this._context.clearRect(0, 0, this._width, this._height + MessageBox._bottom_margin); + + for (let line_index = 0; line_index <= this._current_line_index; ++line_index) { + const rng_character_index = MathUtil.get_random_int(this._lines[line_index].length - 1); + for ( + let character_index = 0; + character_index < (line_index === this._current_line_index ? this._current_character_index + 1 : this._lines[line_index].length); + ++character_index + ) { + const char = this._lines[line_index][character_index]; + if (character_index === rng_character_index) { + for (let i = 0; i < MathUtil.get_random_int(2) + 1; i++) { + this._context.fillText( + char, + this.get_character_x_offset(this._lines[line_index], character_index) + MathUtil.get_random_int(get_animation_factor(this._current_node)), + this.get_character_y_offset(line_index) + MathUtil.get_random_int(get_animation_factor(this._current_node)) + ); + } + } + else { + this._context.fillText( + char, + this.get_character_x_offset(this._lines[line_index], character_index), + this.get_character_y_offset(line_index) + ); + } + } + } + this._context.restore(); + break; + case DialogAnimation.Glitchy_Uppercase: + this._context.save(); + this._context.clearRect(0, 0, this._width, this._height + MessageBox._bottom_margin); + + for (let line_index = 0; line_index <= this._current_line_index; ++line_index) { + const rng_character_index = MathUtil.get_random_int(this._lines[line_index].length - 1); + for ( + let character_index = 0; + character_index < (line_index === this._current_line_index ? this._current_character_index + 1 : this._lines[line_index].length); + ++character_index + ) { + let char = this._lines[line_index][character_index]; + if (character_index === rng_character_index) { + char = char.toUpperCase(); + } + this._context.fillText( + char, + this.get_character_x_offset(this._lines[line_index], character_index), + this.get_character_y_offset(line_index) + ); + } + } + this._context.restore(); + break; + } + + if (this._current_character_index + 1 < this._current_line.length) { + ++this._current_character_index; + // Only play sound if character is not silent, and if there are new characters to display + if (!MessageBox._silentCharacters.has(this._current_char)) { + this._audio.play(); + } + } else { + this.handle_choices(); + if (this._current_line_index < this._lines.length - 1) { + this._current_character_index = 0; + ++this._current_line_index; + } + } + } + + /** + * Handle the case where the current node is a question node (displays the choices). + */ + private handle_choices(): void { + // Not a question node, do nothing + if (this._current_question_node == null) { + return; + } + + this._context.save(); + this._context.clearRect(0, this.get_character_y_offset(this._current_line_index) * 1.2, this._width, this._height + MessageBox._bottom_margin); + this._context.restore(); + + if (this._selected_choice_index == null) { + this._selected_choice_index = 0; + } + + for (let i = 0; i < this._current_question_node.answers.length; ++i) { + const current_choice = this._current_question_node.answers[i]; + const text = i === this._selected_choice_index ? `> ${current_choice.message}` : current_choice.message; + + switch (get_animation(current_choice)) { + case DialogAnimation.None: + this._context.fillText(text, this.get_character_x_offset(text, 0), this.get_character_y_offset(i + 1 + this._current_line_index)); + break; + case DialogAnimation.Shaky: + for (let character_index = 0; character_index < text.length; ++character_index) { + this._context.fillText( + text[character_index], + this.get_character_x_offset(text, character_index) + MathUtil.get_random_int(get_animation_factor(current_choice)), + this.get_character_y_offset(i + 1 + this._current_line_index) + MathUtil.get_random_int(get_animation_factor(current_choice)) + ); + } + break; + } + } + } + + /** + * Prepares the MessageBox to display the given node + */ + private load_node(node: IDialogNode) { + this._current_node = node; + this._lines = this.split_text_canvas(this._current_node.message); + this._current_line_index = 0; + this._current_character_index = 0; + this._timer.interval = 50; + this._selected_choice_index = null; + + this._context.save(); + this._context.clearRect(0, 0, this._width, this._height + MessageBox._bottom_margin); + this._context.restore(); + } + + /** + * Called when the action button is pressed (SpaceBar). + */ + public on_action_button(): void { + + if (this._message_has_ended) { + + // Execute action linked to the node if any + if (this._current_node.action != null) { + this._current_node.action(); + } + + if (this._current_question_node != null) { + // Load choice + this.load_node(this._current_question_node.answers[this._selected_choice_index].next_node); + } else if (this._current_node.next_node == null) { + // Close message box + document.getElementById(MessageBox.DOM_ID).remove(); + gameState.current_message = null; + gameState.resume(); + } else { + // Load next node + this.load_node(this._current_node.next_node); + } + } else { + // Speed up the message + this._timer.interval = 5; + } + } + + /** + * Called when a choice button is pressed. + * @param direction the direction of the choice button + */ + public on_choice_button(direction: Direction): void { + if (this._current_question_node == null) { + return; + } + + switch (direction) { + case Direction.UP: + if (this._selected_choice_index > 0) { + --this._selected_choice_index; + this._choice_sound.play(); + } + break; + case Direction.DOWN: + if (this._selected_choice_index < this._current_question_node.answers.length - 1) { + ++this._selected_choice_index; + this._choice_sound.play(); + } + break; + } + } + + private split_text_canvas(text: string): Array { + const words = text.split(" "); + const lines = new Array(); + let line = words[0]; + for (let i = 1; i < words.length; i++) { + const space = line === "" ? "" : " "; + const next_length = this._context.measureText(line + space + words[i]).width * (1 + this._character_spacing); + if (next_length <= this._width) { + line += ` ${words[i]}`; + } else { + lines.push(line); + line = words[i]; + } + } + lines.push(line); + return lines; + } +} diff --git a/src/renderer.ts b/src/renderer.ts index aa794b1..8fc7629 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -12,12 +12,16 @@ export class Renderer { public disableSmoothing(): void { main_layers.forEach(layer => { - layer.ctx["webkitImageSmoothingEnabled"] = false; - layer.ctx["mozImageSmoothingEnabled"] = false; - layer.ctx.imageSmoothingEnabled = false; + this.disableCanvasSmoothing(layer.ctx); }); } + public disableCanvasSmoothing(context: CanvasRenderingContext2D): void { + context["webkitImageSmoothingEnabled"] = false; + context["mozImageSmoothingEnabled"] = false; + context.imageSmoothingEnabled = false; + } + public scale(zoomScale?: number): void { this.zoomScale = zoomScale == null ? this.zoomScaleNext.get(this.zoomScale) : zoomScale; main_layers.forEach(layer => { diff --git a/src/sign.ts b/src/sign.ts new file mode 100644 index 0000000..cd986b7 --- /dev/null +++ b/src/sign.ts @@ -0,0 +1,24 @@ +import { Entity } from "./entity"; +import { Sprite } from "./sprite"; +import { Point } from "./point"; +import { ActionableEntity } from "./actionable_entity"; +import { IDialogNode, DialogGraph } from "./messages/dialog_graph"; +import { MessageBox } from "./messages/message_box"; + +export class Sign extends ActionableEntity { + public message: IDialogNode; + + constructor(id: string, current_sprite: Sprite, position: Point, width: number, height: number, + has_collision_objects?: boolean, height_perspective?: number, + floor_level?: number, room_number?: number, action_hitbox_ratio?: number, message?: IDialogNode) { + super(id, current_sprite, position, width, height, has_collision_objects, height_perspective, floor_level, room_number, action_hitbox_ratio); + this.sprite_filename = "assets/img/objects.png"; + this.message = message; + } + + public action() { + const msg = new MessageBox(new DialogGraph("San", this.message)); + this.occuring = true; + msg.start(); + } +} \ No newline at end of file diff --git a/src/timers.ts b/src/timers.ts index c000791..4eb0a03 100644 --- a/src/timers.ts +++ b/src/timers.ts @@ -3,5 +3,6 @@ import { Timer } from "./timer"; export const TIMERS: Timer[] = [ new Timer("tear", 0), new Timer("jays_sprites", 80), - new Timer("joysticks", 34) + new Timer("joysticks", 34), + new Timer("textbox", 50) ]; diff --git a/src/util.ts b/src/util.ts index c8e9da3..88c6d14 100644 --- a/src/util.ts +++ b/src/util.ts @@ -40,6 +40,15 @@ export class ArrayUtil { return array1.filter(item => array2.indexOf(item) < 0); } + public static shuffle(array: Array) { + const shufled_array = [...array]; + for (let i = shufled_array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shufled_array[i], shufled_array[j]] = [shufled_array[j], shufled_array[i]]; + } + return shufled_array; + } + public static find_nb_connected(x: number, y: number, array: Array>): number { const canUp = (x - 1 >= 0); const canDown = (x + 1 < array.length); @@ -164,7 +173,10 @@ export class MathUtil { return Math.abs(v1 - v2) < epsilon; } - public static get_random_int(max) { - return Math.floor(Math.random() * Math.floor(max)); + public static get_random_int(max: number, max2?: number): number { + if (max2 == null) { + return Math.floor(Math.random() * Math.floor(max)); + } + return max + Math.floor(Math.random() * Math.floor(max2 - max)); } } \ No newline at end of file