import { asset } from "./Assets" ; import { Bounds , Entity } from "./Entity" ; import { boundsFromMapObject , rnd , rndInt } from "./util" ; import { GameObject , GameScene , isCollidableGameObject } from "./scenes/GameScene" ; import { GameObjectInfo } from "./MapInfo" ; import { getImageData } from "./graphics" ; import { ParticleEmitter , Particles , valueCurves } from "./Particles" ; import { RenderingLayer , RenderingType } from "./Renderer" ; import { PETTING_ENDING_CUTSCENE_DURATION , WINDOW_ENDING_CUTSCENE_DURATION } from "./constants" ; export enum Environment { AIR = 0 , SOLID = 0xff000000 , PLATFORM = 0xff7f7f7f , BOUNCE = 0xffff0000 , WATER = 0xff0000ff , SOIL = 0xffffff00 , RAINCLOUD = 0xff00ff00 } export const validEnvironments = Object . values ( Environment ) ; export class World implements GameObject { @ asset ( "maps/level.png" ) private static foreground : HTMLImageElement ; @ asset ( "maps/level_collision.png" , { map : ( image ) => new Uint32Array ( getImageData ( image ) . data . buffer ) } ) private static collisionMap : Uint32Array ; @ asset ( [ "maps/bg.png" , "maps/bg2.png" , "maps/bg3.png" ] ) private static backgrounds : HTMLImageElement [ ] ; private scene : GameScene ; @ asset ( "sprites/raindrop.png" ) private static raindrop : HTMLImageElement ; private rainEmitter : ParticleEmitter ; private raining = false ; public constructor ( scene ) { this . scene = scene ; const rainSpawnPosition = this . scene . pointsOfInterest . find ( o => o . name === "rain_spawn_position" ) ; if ( ! rainSpawnPosition ) { throw new Error ( "Missing 'rain_spawn_position' point in map data to place rain emitter" ) ; } this . rainEmitter = this . scene . particles . createEmitter ( { position : { x : rainSpawnPosition . x , y : rainSpawnPosition . y } , offset : ( ) => ( { x : rnd ( - 1 , 1 ) * 26 , y : rnd ( - 1 , 1 ) * 5 } ) , velocity : ( ) => ( { x : rnd ( - 1 , 1 ) * 5 , y : - rnd ( 50 , 80 ) } ) , color : ( ) => World . raindrop , size : 4 , gravity : { x : 0 , y : - 100 } , lifetime : ( ) => 3 , alpha : 0.6 , alphaCurve : valueCurves . linear . invert ( ) } ) ; } public getWidth ( ) { return World . foreground . width ; } public getHeight ( ) { return World . foreground . height ; } public update ( ) { if ( this . raining ) { this . rainEmitter . emit ( rndInt ( 1 , 4 ) ) ; } } public draw ( ctx , width , height ) { const camX = this . scene . camera . x ; const camY = this . scene . camera . y ; const posXMultiplier = 1 - ( camX / this . getWidth ( ) * 2 ) ; let alpha = 1 ; if ( this . scene . pettingCutscene ) { alpha = Math . max ( 0 , 1 - ( this . scene . pettingCutsceneTime / PETTING_ENDING_CUTSCENE_DURATION ) ) ; } if ( this . scene . windowCutscene ) { alpha = Math . max ( 0 , 1 - ( this . scene . windowCutsceneTime / ( WINDOW_ENDING_CUTSCENE_DURATION / 1.5 ) ) ) ; } this . scene . renderer . add ( { type : RenderingType . DRAW_IMAGE , layer : RenderingLayer . TILEMAP_MAP , translation : { x : camX , y : - camY } , position : { x : - camX , y : - this . getHeight ( ) + camY } , asset : World . foreground , alpha : alpha } ) ; for ( const background of World . backgrounds ) { const bgX = this . getWidth ( ) / background . width ; const bgY = this . getHeight ( ) / background . height ; this . scene . renderer . add ( { type : RenderingType . DRAW_IMAGE , layer : RenderingLayer . TILEMAP_BACKGROUND , translation : { x : camX , y : - camY } , position : { x : ( - camX / bgX ) + ( - posXMultiplier * ( width / 2 ) ) , y : ( - this . getHeight ( ) + camY ) / bgY } , asset : background , alpha : alpha } ) ; } } public getEnvironment ( x , y ) { const index = ( this . getHeight ( ) - 1 - Math . round ( y ) ) * this . getWidth ( ) + Math . round ( x ) ; if ( index < 0 || index >= World . collisionMap . length ) { return Environment . AIR ; } return World . collisionMap [ index ] ; } param x param y return public collidesWith ( x , y , ignoreObjects = [ ] , ignore = [ ] ) { for ( const gameObject of this . scene . gameObjects ) { if ( gameObject !== this && ! ignoreObjects . includes ( gameObject ) && isCollidableGameObject ( gameObject ) ) { const environment = gameObject . collidesWith ( x , y ) ; if ( environment !== Environment . AIR && ! ignore . includes ( environment ) ) { return environment ; } } } const index = ( this . getHeight ( ) - 1 - Math . round ( y ) ) * this . getWidth ( ) + Math . round ( x ) ; if ( index < 0 || index >= World . collisionMap . length ) { return 0 ; } const environment = this . getEnvironment ( x , y ) ; if ( ! validEnvironments . includes ( environment ) || ( ignore && ignore . includes ( environment ) ) ) { return Environment . AIR ; } return World . collisionMap [ index ] ; } param sourceEntity param margin param ignoreEntities return public getEntityCollisions ( sourceEntity , margin = 0 , ignoreEntities = [ ] ) { const collidesWith = [ ] ; for ( const gameObject of this . scene . gameObjects ) { if ( gameObject !== sourceEntity && ! ( gameObject instanceof Particles ) && gameObject instanceof Entity && gameObject . isTrigger && ! ignoreEntities . includes ( gameObject ) ) { const colliding = this . boundingBoxesCollide ( sourceEntity . getBounds ( margin ) , gameObject . getBounds ( margin ) ) ; if ( colliding ) { collidesWith . push ( gameObject ) ; } } } return collidesWith ; } param sourceEntity public getTriggerCollisions ( sourceEntity ) { const collidesWith = [ ] ; for ( const triggerObject of this . scene . triggerObjects ) { const colliding = this . boundingBoxesCollide ( sourceEntity . getBounds ( ) , boundsFromMapObject ( triggerObject ) ) ; if ( colliding ) { collidesWith . push ( triggerObject ) ; } } return collidesWith ; } public getGateCollisions ( sourceEntity ) { const collidesWith = [ ] ; for ( const gateObject of this . scene . gateObjects ) { const colliding = this . boundingBoxesCollide ( sourceEntity . getBounds ( ) , boundsFromMapObject ( gateObject , 0 ) ) ; if ( colliding && ! gateObject . properties . disabled ) { collidesWith . push ( gateObject ) ; } } return collidesWith ; } public getCameraBounds ( sourceEntity ) { const collidesWith = [ ] ; for ( const triggerObject of this . scene . boundObjects ) { const colliding = this . boundingBoxesCollide ( sourceEntity . getBounds ( ) , boundsFromMapObject ( triggerObject ) ) ; if ( colliding ) { collidesWith . push ( triggerObject ) ; } } return collidesWith ; } param box1 param box2 return public boundingBoxesCollide ( box1 , box2 ) { return ! ( ( ( box1 . y - box1 . height ) > ( box2 . y ) ) || ( box1 . y < ( box2 . y - box2 . height ) ) || ( ( box1 . x + box1 . width ) < box2 . x ) || ( box1 . x > ( box2 . x + box2 . width ) ) ) ; } public getObjectAt ( x , y , ignoreObjects = [ ] , ignore = [ ] ) { for ( const gameObject of this . scene . gameObjects ) { if ( gameObject !== this && ! ignoreObjects . includes ( gameObject ) && isCollidableGameObject ( gameObject ) ) { const environment = gameObject . collidesWith ( x , y ) ; if ( environment !== Environment . AIR && ! ignore . includes ( environment ) ) { return gameObject ; } } } return null ; } param x param y param height return public collidesWithVerticalLine ( x , y , height , ignoreObjects ? , ignore ? ) { for ( let i = 0 ; i < height ; i ++ ) { const collision = this . collidesWith ( x , y - i , ignoreObjects , ignore ) ; if ( collision ) { return collision ; } } return 0 ; } param x param y return public getGround ( x , y , ignoreObjects ? , ignore ? ) { while ( y > 0 && ! this . collidesWith ( x , y , ignoreObjects , ignore ) ) { y -- ; } return y ; } public startRain ( ) { this . raining = true ; } public stopRain ( ) { this . raining = false ; } public isRaining ( ) { return this . raining ; } }