import { Bounds } from "./Entity" ; import { clamp , isDev , rnd , shiftValue } from "./util" ; import { Fire } from "./entities/Fire" ; import { GameScene } from "./scenes/GameScene" ; import { RenderingLayer , RenderingType } from "./Renderer" ; import { ValueCurve , valueCurves } from "./Particles" ; import { Vector2Like } from "./graphics/Vector2" ; export interface CamFocus { x : number ; y : number ; duration : number ; startTime : number ; endTime : number ; zoom : number ; rotation : number ; progress : number ; dead : boolean ; force : number ; curve : ValueCurve ; resolve ? : Function ; } interface Rectangle { x : number ; y : number ; width : number ; height : number ; } type OverBoundData = { left : boolean ; right : boolean ; top : boolean ; bottom : boolean ; } ; export class Camera { public x = 0 ; public y = 0 ; public zoom = 1 ; public rotation = 0 ; private focuses : CamFocus [ ] = [ ] ; private time = 0 ; private interpolationTime ! : number ; private zoomingOut = false ; private currentBarTarget = 0 ; private currentBarHeight = 0 ; private bounds ? : Bounds ; constructor ( protected scene , private target , interpolationTime = 0.5 , private barHeight = 0.1 ) { if ( interpolationTime > 1 ) { throw new Error ( "Camera interpolation time may not exceed 1" ) ; } this . interpolationTime = interpolationTime / 2 ; if ( isDev ( ) ) { console . log ( "Dev mode, press “Tab” to zoom out & click somewhere to teleport there." ) ; document . addEventListener ( "keydown" , this . handleKeyDown . bind ( this ) ) ; document . addEventListener ( "keyup" , this . handleKeyUp . bind ( this ) ) ; this . scene . game . canvas . addEventListener ( "click" , this . handleClick . bind ( this ) ) ; } this . currentBarTarget = 0 ; this . currentBarHeight = 0 ; this . x = target . x ; this . y = target . y ; } public setBounds ( bounds ) { this . bounds = bounds ; } public getBounds ( ) { return this . bounds ; } private handleKeyDown ( e ) { if ( e . key === "Tab" ) { if ( ! e . repeat ) { this . zoomingOut = true ; } e . stopPropagation ( ) ; e . preventDefault ( ) ; } } private handleKeyUp ( e ) { if ( e . key === "Tab" ) { this . zoomingOut = false ; e . stopPropagation ( ) ; e . preventDefault ( ) ; } } private handleClick ( e ) { if ( this . zoomingOut ) { const rect = this . scene . game . canvas . getBoundingClientRect ( ) ; const cx = e . clientX - rect . x , cy = e . clientY - rect . y ; const px = cx / rect . width , py = cy / rect . height ; const worldRect = this . getVisibleRect ( ) ; const tx = worldRect . x + px * worldRect . width , ty = worldRect . y + ( 1 - py ) * worldRect . height ; this . scene . player . x = tx ; this . scene . player . y = ty ; this . scene . player . setVelocity ( 0 , 0 ) ; this . zoomingOut = false ; } } public getVisibleRect ( x = this . x , y = this . y ) { const cnv = this . scene . game . canvas ; const cw = cnv . width ; const ch = cnv . height ; const offx = cw / 2 / this . zoom ; const offy = ch / 2 / this . zoom ; return { x : x - offx , y : y - offy , width : offx * 2 , height : offy * 2 } ; } public isPointVisible ( x , y , radius = 0 ) { const visibleRect = this . getVisibleRect ( ) ; return ( x >= visibleRect . x - radius && y >= visibleRect . y - radius && x <= visibleRect . x + visibleRect . width + radius && y <= visibleRect . y + visibleRect . height + radius ) ; } public setCinematicBar ( target ) { this . currentBarTarget = target ; } private getBaseCameraTarget ( ) { let xTarget = this . target . x ; let yTarget = this . target . y + 30 ; if ( this . bounds ) { const targetVisibleRect = this . getVisibleRect ( xTarget , yTarget ) ; const overBounds = { left : ( targetVisibleRect . x < this . bounds . x ) , right : ( targetVisibleRect . x + targetVisibleRect . width ) > ( this . bounds . x + this . bounds . width ) , top : ( targetVisibleRect . y + targetVisibleRect . height ) > this . bounds . y , bottom : targetVisibleRect . y < ( this . bounds . y - this . bounds . height ) } ; if ( targetVisibleRect . width >= this . bounds . width ) { const visibleCenterX = targetVisibleRect . x + targetVisibleRect . width / 2 ; const boundCenterX = this . bounds . x + this . bounds . width / 2 ; const diff = boundCenterX - visibleCenterX ; xTarget += diff ; } else if ( overBounds . left ) { const diff = this . bounds . x - targetVisibleRect . x ; xTarget += diff ; } else if ( overBounds . right ) { const diff = ( this . bounds . x + this . bounds . width ) - ( targetVisibleRect . x + targetVisibleRect . width ) ; xTarget += diff ; } if ( targetVisibleRect . height >= this . bounds . height ) { const visibleCenterY = ( targetVisibleRect . y + targetVisibleRect . height ) - targetVisibleRect . height / 2 ; const boundCenterY = this . bounds . y - this . bounds . height / 2 ; const diff = boundCenterY - visibleCenterY ; yTarget += diff ; } else if ( overBounds . top ) { const diff = this . bounds . y - ( targetVisibleRect . y + targetVisibleRect . height ) ; yTarget += diff ; } else if ( overBounds . bottom ) { const diff = ( this . bounds . y - this . bounds . height ) - targetVisibleRect . y ; yTarget += diff ; } } return { x : xTarget , y : yTarget } ; } public update ( dt , time ) { this . time = time ; const baseCamTarget = this . getBaseCameraTarget ( ) ; this . x = baseCamTarget . x ; this . y = baseCamTarget . y ; if ( this . scene . fire . isAngry ( ) || this . scene . apocalypse ) { this . applyApocalypticShake ( this . scene . fire ) ; } this . zoom = this . zoomingOut ? 0.2 : 1 ; this . rotation = 0 ; for ( const focus of this . focuses ) { this . updateAndApplyFocus ( focus ) ; } this . focuses = this . focuses . filter ( f => ! f . dead ) ; this . currentBarHeight = shiftValue ( this . currentBarHeight , this . currentBarTarget , dt * 1.5 ) ; this . currentBarTarget = 0 ; } private applyApocalypticShake ( shakeSource ) { const dx = this . x - shakeSource . x , dy = this . y - shakeSource . y ; const dis = Math . sqrt ( dx * dx + dy * dy ) ; const maxDis = 200 ; if ( dis < maxDis ) { const intensity = ( shakeSource . intensity - 5 ) / 15 ; if ( intensity > 0 ) { const shake = 5 * intensity * ( 1 - dis / maxDis ) * ( this . scene . player . playerConversation ? 0.5 : 1 ) ; this . x += rnd ( - 1 , 1 ) * shake ; this . y += rnd ( - 1 , 1 ) * shake ; } } } /**
     * Returns true if cam target (player) currently has full attention, or is showing something else for the moment.
     */ public isOnTarget ( ) { return this . focuses . length === 0 ; } /**
     * Returns strength of camera focus on something other than the player. E.g. when camera focuses on some place to
     * show the player, the focus force will be between 0 and 1 during interpolation and exactly 1 while fully focusing
     * on that spot. This e.g. can be used for cinematic bars at top and bottom, scaling their height.
     */ public getFocusForce ( ) { return this . focuses . reduce ( ( a , b ) => Math . max ( a , b . force ) , 0 ) ; } public applyTransform ( ctx ) { ctx . scale ( this . zoom , this . zoom ) ; ctx . rotate ( this . rotation ) ; ctx . translate ( - this . x , this . y ) ; } public focusOn ( duration , x , y , zoom = 1 , rotation = 0 , curve = valueCurves . cos ( this . interpolationTime ) ) { const focus = { x , y , duration , zoom , rotation , startTime : this . time , endTime : this . time + duration , progress : 0 , dead : false , force : 0 , curve } ; this . focuses . push ( focus ) ; return new Promise ( ( resolve , reject ) => { focus . resolve = resolve ; this . updateAndApplyFocus ( focus ) ; } ) ; } public updateAndApplyFocus ( focus ) { focus . progress = clamp ( ( this . time - focus . startTime ) / focus . duration , 0 , 1 ) ; focus . dead = ( focus . progress >= 1 ) ; if ( ! focus . dead ) { const force = focus . force = focus . curve . get ( focus . progress ) ; const f1 = 1 - force ; this . x = f1 * this . x + force * focus . x ; this . y = f1 * this . y + force * focus . y ; const originalSize = 1 / this . zoom , targetSize = 1 / focus . zoom ; const currentSize = f1 * originalSize + force * targetSize ; this . zoom = 1 / currentSize ; this . rotation = f1 * this . rotation + force * focus . rotation ; } else { if ( focus . resolve ) { focus . resolve ( ) ; focus . resolve = undefined ; } } } public addCinematicBarsToRenderer ( force = this . getFocusForce ( ) ) { force = Math . max ( force , this . getFocusForce ( ) , this . currentBarHeight ) ; this . scene . renderer . add ( { type : RenderingType . BLACK_BARS , layer : RenderingLayer . BLACK_BARS , color : "black" , height : this . barHeight , force } ) ; } public drawBars ( ctx , force = this . getFocusForce ( ) ) { force = Math . max ( force , this . getFocusForce ( ) , this . currentBarHeight ) ; ctx . save ( ) ; ctx . fillStyle = "black" ; ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ; const f = 0.5 - 0.5 * Math . cos ( Math . PI * force ) ; const h = ctx . canvas . height * this . barHeight * f ; ctx . fillRect ( 0 , 0 , ctx . canvas . width , h ) ; ctx . fillRect ( 0 , ctx . canvas . height - h , ctx . canvas . width , h ) ; ctx . restore ( ) ; } }