import { AutoWired , Singleton , Inject } from 'typescript-ioc' ; import { capitalize , get } from 'lodash' ; import { Player } from '../../../shared/models' ; import { World , Tile } from './world' ; import { Direction , MovementType , Channel , AdventureLogEventType , EventName } from '../../../shared/interfaces' ; import { RNGService } from './rng-service' ; import { EventManager } from './event-manager' ; import { Logger } from '../logger' ; import { HolidayHelper } from './holiday-helper' ; import { AssetManager } from './asset-manager' ; import { PartyHelper } from './party-helper' ; @ Singleton @ AutoWired export class MovementHelper { @ Inject private world : World ; @ Inject private logger : Logger ; @ Inject private rng : RNGService ; @ Inject private assets : AssetManager ; @ Inject private eventManager : EventManager ; @ Inject private partyHelper : PartyHelper ; @ Inject private holidayHelper : HolidayHelper ; private directions : Direction [ ] = [ Direction . Southwest , Direction . South , Direction . Southeast , Direction . East , Direction . Northeast , Direction . North , Direction . Northwest , Direction . West ] ; public locToTeleport ( name ) { return this . assets . allTeleports [ name ] ; } public isValidTile ( map , x , y ) { const $map = this . world . getMap ( map ) ; if ( ! $map ) return false ; const $tile = $map . getTile ( x , y ) ; if ( ! $tile ) return false ; return ! ( x <= 0 || y <= 0 || x >= $map . width || y >= $map . height ) ; } public moveToStart ( player ) { player . setPos ( 10 , 10 , 'Norkos' , 'Norkos Town' ) ; } private getTileAt ( map , x , y ) { return this . world . getMap ( map ) . getTile ( x , y ) ; } private canEnterTile ( player , tile ) { const properties = get ( tile , 'object.properties' ) ; if ( properties ) { let totalRequirements = 0 ; let metRequirements = 0 ; if ( properties . requireMap ) { totalRequirements ++ ; if ( player . $statistics . get ( `Map/${ properties . requireMap }/Steps` ) > 0 ) metRequirements ++ ; } if ( properties . requireBoss ) { totalRequirements ++ ; if ( player . $statistics . get ( `BossKill/Boss/${ properties . requireBoss }` ) > 0 ) metRequirements ++ ; } if ( properties . requireClass ) { totalRequirements ++ ; if ( player . profession === properties . requireClass ) metRequirements ++ ; } if ( properties . requireAchievement ) { totalRequirements ++ ; if ( player . hasAchievement ( properties . requireAchievement ) ) metRequirements ++ ; } if ( properties . requireCollectible ) { totalRequirements ++ ; if ( player . $collectibles . hasCurrently ( properties . requireCollectible ) ) metRequirements ++ ; } if ( properties . requireAscension ) { totalRequirements ++ ; if ( player . ascensionLevel >= + properties . requireAscension ) metRequirements ++ ; } if ( properties . requireHoliday ) { totalRequirements ++ ; if ( this . holidayHelper . isHoliday ( properties . requireHoliday ) ) metRequirements ++ ; } if ( totalRequirements !== metRequirements ) return false ; } return ! tile . blocked && tile . terrain !== 'Void' ; } private num2dir ( dir , x , y ) { switch ( dir ) { case Direction . Southwest : return { x : x - 1 , y : y - 1 } ; case Direction . South : return { x : x , y : y - 1 } ; case Direction . Southeast : return { x : x + 1 , y : y - 1 } ; case Direction . West : return { x : x - 1 , y : y } ; case Direction . East : return { x : x + 1 , y : y } ; case Direction . Northwest : return { x : x - 1 , y : y + 1 } ; case Direction . North : return { x : x , y : y + 1 } ; case Direction . Northeast : return { x : x + 1 , y : y + 1 } ; default : return { x : x , y : y } ; } } private xyDiff2dir ( x1 , y1 , x2 , y2 ) { if ( x1 > x2 && y1 > y2 ) return Direction . Southwest ; if ( x1 === x2 && y1 > y2 ) return Direction . South ; if ( x1 < x2 && y1 > y2 ) return Direction . Southeast ; if ( x1 > x2 && y1 === y2 ) return Direction . West ; if ( x1 < x2 && y1 === y2 ) return Direction . East ; if ( x1 > x2 && y1 < y2 ) return Direction . Northwest ; if ( x1 === x2 && y1 < y2 ) return Direction . North ; if ( x1 < x2 && y1 < y2 ) return Direction . Northeast ; return Direction . Nowhere ; } private getInitialWeight ( player ) { let weight = [ 300 , 40 , 7 , 3 , 1 , 3 , 7 , 40 ] ; if ( ! player . divineDirection && player . $personalities . isActive ( 'Drunk' ) ) { weight = [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] ; } let dirMod = null ; if ( player . lastDir ) { dirMod = player . lastDir ; } if ( player . divineDirection ) { player . increaseStatistic ( `Character/Movement/Steps/Divine` , 1 ) ; dirMod = this . xyDiff2dir ( player . x , player . y , player . divineDirection . x , player . divineDirection . y ) ; } if ( dirMod ) { const lastDirIndex = this . directions . indexOf ( dirMod ) ; if ( lastDirIndex !== - 1 ) { weight = weight . slice ( weight . length - lastDirIndex ) . concat ( weight . slice ( 0 , weight . length - lastDirIndex ) ) ; } } return weight ; } private pickRandomDirection ( player , weight ) { const dirIndexes = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ] ; const weights = weight ; const useIndexes = [ ] ; const useWeights = [ ] ; for ( let i = 0 ; i < weights . length ; i ++ ) { if ( weights [ i ] === 0 ) continue ; useWeights . push ( weights [ i ] ) ; useIndexes . push ( dirIndexes [ i ] ) ; } if ( weight . length === 0 || useWeights . length === 0 ) { this . logger . error ( 'PlayerMovement' , new Error ( `${ player . name } in ${ player . map } @ ${ player . x },${ player . y } is unable to move due to no weights.
        Initial weights: ${ weight }` ) ) ; return { index : - 1 , dir : - 1 } ; } const dirIndex = this . rng . weighted ( useIndexes , useWeights ) ; return { index : dirIndex , dir : this . directions [ dirIndex ] } ; } public doTeleport ( player , { map , x , y , toLoc } ) { const tile = { terrain : '' , region : '' , blocked : false , object : { properties : { destx : x , desty : y , movementType : 'teleport' , map , toLoc } } } ; this . handleTileTeleport ( player , tile , true ) ; const realTile = this . getTileAt ( player . map , player . x , player . y ) ; this . handleTile ( player , realTile ) ; } private handleTile ( player , tile , ignoreIf ? ) { const type = get ( tile , 'object.properties.realtype' ) ; const forceEvent = get ( tile , 'object.properties.forceEvent' , '' ) ; if ( forceEvent ) { const cdCheck = `${ player . x },${ player . y }|${ player . map }` ; if ( ! player . cooldowns [ cdCheck ] || player . cooldowns [ cdCheck ] < Date . now ( ) ) { delete player . cooldowns [ cdCheck ] ; const oldil3EventNames = { 'ItemBless' : 'BlessItem' , 'ItemForsake' : 'ForsakeItem' , 'GoldBless' : 'BlessGold' , 'GoldForsake' : 'ForsakeGold' , 'XPBless' : 'BlessXP' , 'XPForsake' : 'ForsakeXP' , 'Gambling' : 'Gamble' } ; this . eventManager . doEventFor ( player , oldil3EventNames [ forceEvent ] || forceEvent , tile . object . properties ) ; if ( forceEvent !== EventName . Providence ) { player . cooldowns [ cdCheck ] = Date . now ( ) + ( 1000 * 60 * 5 ) ; } } } if ( ! type || ! this [ `handleTile${ type }` ] || ignoreIf === type ) return ; this [ `handleTile${ type }` ] ( player , tile ) ; } private handleTileTeleport ( player , tile , force = false ) { if ( ! tile . object ) return ; const dest = tile . object . properties ; dest . x = + dest . destx ; dest . y = + dest . desty ; if ( dest . movementType === MovementType . Ascend && player . $personalities . isActive ( 'Delver' ) ) { player . $$game . subscriptionManager . emitToChannel ( Channel . PlayerAdventureLog , { playerNames : [ player . name ] , data : { when : Date . now ( ) , type : AdventureLogEventType . Explore , message : 'You attempted to go up the stairs but your delving personality only lets you go down!' } } ) ; return ; } if ( dest . movementType === MovementType . Descend && player . $personalities . isActive ( 'ScaredOfTheDark' ) ) { player . $$game . subscriptionManager . emitToChannel ( Channel . PlayerAdventureLog , { playerNames : [ player . name ] , data : { when : Date . now ( ) , type : AdventureLogEventType . Explore , message : 'You attempted to go down the stairs but your scared of the dark personality only lets you go up!' } } ) ; return ; } if ( ! force && ( dest . movementType === MovementType . Ascend || dest . movementType === MovementType . Descend ) ) { if ( player . stepCooldown > 0 ) return ; player . stepCooldown = 10 ; } if ( ! dest . map && ! dest . toLoc ) { this . logger . error ( 'PlayerMovement' , new Error ( `No dest.map at ${ player . x }, ${ player . y } in ${ player . map }` ) ) ; return ; } if ( ! dest . movementType ) { this . logger . error ( 'PlayerMovement' , new Error ( `No dest.movementType at ${ player . x }, ${ player . y } in ${ player . map }` ) ) ; return ; } if ( ! dest . fromName ) dest . fromName = player . map ; if ( ! dest . destName ) dest . destName = dest . map ; player . divineDirection = null ; if ( dest . toLoc ) { if ( dest . toLoc === 'guildbase' ) { return ; } else { try { const toLocData = this . locToTeleport ( dest . toLoc ) ; dest . x = toLocData . x ; dest . y = toLocData . y ; dest . map = toLocData . map ; dest . destName = toLocData . formalName ; } catch ( e ) { this . logger . error ( 'PlayerMovement' , new Error ( `Loc data is not correct for ${ dest . toLoc } (${ JSON . stringify ( this . locToTeleport ( dest . toLoc ) ) })` ) ) ; player . setPos ( 10 , 10 , 'Norkos' , 'Wilderness' ) ; return ; } } } player . setPos ( dest . x , dest . y , dest . map , tile . region ) ; const newTile = this . getTileAt ( player . map , player . x , player . y ) ; player . region = newTile . region ; this . handleTile ( player , newTile , 'Teleport' ) ; player . increaseStatistic ( `Character/Movement/${ capitalize ( dest . movementType ) }` , 1 ) ; } private handleTileCollectible ( player , tileData ) { const collectible = tileData . object ; const collectibleName = collectible . name ; const collectibleRarity = get ( collectible , 'properties.rarity' , 'basic' ) ; player . tryFindCollectible ( { name : collectibleName , rarity : collectibleRarity , description : collectible . properties . flavorText , storyline : collectible . properties . storyline } ) ; } private handleTileTrainer ( player , tileData ) { const professionName = tileData . object . name ; const trainerName = tileData . object . properties . realName ? `${ tileData . object . properties . realName }, the ${ professionName } trainer` : `the ${ professionName } trainer` ; this . eventManager . doEventFor ( player , EventName . FindTrainer , { professionName , trainerName } ) ; } private handleTileBoss ( player , tileData ) { this . eventManager . doEventFor ( player , EventName . BattleBoss , { bossName : tileData . object . name } ) ; } private handleTileBossParty ( player , tileData ) { this . eventManager . doEventFor ( player , EventName . BattleBoss , { bossParty : tileData . object . name } ) ; } private handleTileTreasure ( player , tileData ) { this . eventManager . doEventFor ( player , EventName . FindTreasure , { treasureName : tileData . object . name } ) ; } public takeStep ( player ) { if ( player . $party && ( player . $personalities . isActive ( 'Camping' ) || player . $personalities . isActive ( 'Solo' ) ) ) { player . $$game . partyHelper . playerLeave ( player ) ; } if ( player . $personalities . isActive ( 'Camping' ) ) { player . increaseStatistic ( 'Character/Movement/Steps/Camping' , 1 ) ; return ; } const weight = this . getInitialWeight ( player ) ; if ( ! player . stepCooldown ) player . stepCooldown = 0 ; let attempts = 1 ; let { index , dir } = this . pickRandomDirection ( player , weight ) ; if ( index === - 1 || dir === - 1 ) return ; if ( player . $party && player . $personalities . isActive ( 'Telesheep' ) ) { const leader = this . partyHelper . getPartyLeader ( player . $party ) ; if ( leader !== player && leader && player && leader . map === player . map ) { dir = this . xyDiff2dir ( player . x , player . y , leader . x , leader . y ) ; } } let newLoc = this . num2dir ( dir , player . x , player . y ) ; let tile = this . getTileAt ( player . map , newLoc . x , newLoc . y ) ; while ( ! this . canEnterTile ( player , tile ) ) { if ( attempts > 8 ) { this . logger . error ( 'PlayerMovement' , new Error ( `Player ${ player . name } is position locked at ${ player . x }, ${ player . y } in ${ player . map }` ) ) ; break ; } weight [ index ] = 0 ; const res = this . pickRandomDirection ( player , weight ) ; index = res . index ; dir = res . dir ; newLoc = this . num2dir ( dir , player . x , player . y ) ; tile = this . getTileAt ( player . map , newLoc . x , newLoc . y ) ; attempts ++ ; } if ( ! tile . terrain ) { this . logger . error ( 'PlayerMovement' , new Error ( `Invalid tile terrain undefined for ${ player . name } @ ${ player . map }: ${ player . x },${ player . y }` ) ) ; } const oldLoc = { x : player . x , y : player . y , map : player . map } ; if ( player . stepCooldown > 0 ) { player . stepCooldown -- ; } player . stepCooldown -- ; player . lastDir = dir === Direction . Nowhere ? null : dir ; player . lastLoc = oldLoc ; player . setPos ( newLoc . x , newLoc . y , player . map , tile . region ) ; const map = this . world . getMap ( player . map ) ; if ( ! map || player . x <= 0 || player . y <= 0 || player . x >= map . width || player . y >= map . height ) { this . logger . error ( 'PlayerMovement' , new Error ( `Out of bounds for ${ player . name } at ${ player . region } on ${ player . map }:
          ${ player . x }, ${ player . y }. Old ${ oldLoc . x }, ${ oldLoc . y }` ) ) ; this . moveToStart ( player ) ; } this . handleTile ( player , tile ) ; player . increaseStatistic ( `Profession/${ player . profession }/Steps` , 1 ) ; player . increaseStatistic ( `Character/Movement/Steps/Normal` , 1 ) ; player . increaseStatistic ( `Environment/Terrain/${ capitalize ( tile . terrain ) || 'Void' }` , 1 ) ; player . increaseStatistic ( `Map/${ player . map }/Region/${ player . region || 'Wilderness' }` , 1 ) ; player . increaseStatistic ( `Map/${ player . map }/Steps` , 1 ) ; if ( ! player . divineDirection && player . $personalities . isActive ( 'Drunk' ) ) { player . increaseStatistic ( `Character/Movement/Steps/Drunk` , 1 ) ; } if ( player . $personalities . isActive ( 'Solo' ) ) { player . increaseStatistic ( `Character/Movement/Steps/Solo` , 1 ) ; } if ( player . $party ) { player . increaseStatistic ( `Character/Movement/Steps/Party` , 1 ) ; } } }