UNPKG

stats-modifiers

Version:
449 lines (335 loc) 15.2 kB
/* Stats Modifiers Copyright (c) 2021 Cédric Ronvel The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ "use strict" ; function ModifiersTable( id , statsModifiers = null , active = true , isTemplate = false , events = null ) { this.id = id ; this.statsModifiers = {} ; // per-stat modifier this.active = !! active ; this.destroyed = false ; this.stackedOn = null ; // can be stacked on only one stats table this.isTemplate = !! isTemplate ; // templates are cloned before been stacked this.templateInstanceCount = 0 ; this.events = {} ; this.proxy = null ; if ( statsModifiers ) { for ( let statName in statsModifiers ) { this.setStatModifiers( statName , statsModifiers[ statName ] ) ; } } if ( Array.isArray( events ) ) { for ( let event of events ) { if ( event && typeof event === 'object' ) { this.setEvent( event ) ; } } } } ModifiersTable.prototype.__prototypeUID__ = 'stats-modifiers/ModifiersTable' ; ModifiersTable.prototype.__prototypeVersion__ = require( '../package.json' ).version ; module.exports = ModifiersTable ; const Modifier = require( './Modifier.js' ) ; const common = require( './common.js' ) ; const operators = require( './operators.js' ) ; const eventActions = require( './eventActions.js' ) ; const arrayKit = require( 'array-kit' ) ; ModifiersTable.prototype.setStatModifiers = function( statName , modifiers ) { if ( this.destroyed ) { return ; } if ( common.isPlainObject( modifiers ) && modifiers.__prototypeUID__ !== 'kung-fig/Operator' ) { // Nested object syntax for ( let key in modifiers ) { this.setStatModifiers( statName + '.' + key , modifiers[ key ] ) ; } return ; } // Check for the mono-modifier syntax, enclose it inside an array if ( ! Array.isArray( modifiers ) ) { modifiers = [ modifiers ] ; } else if ( ! modifiers[ 0 ] || typeof modifiers[ 0 ] !== 'object' ) { modifiers = [ modifiers ] ; } if ( ! this.statsModifiers[ statName ] ) { this.statsModifiers[ statName ] = {} ; } for ( let modifier of modifiers ) { if ( modifier && typeof modifier === 'object' ) { // There is the “classic” array syntax, and the object syntax (later is used by the KFG operator syntax) let operator , operand , priorityGroup ; if ( Array.isArray( modifier ) ) { [ operator , operand , priorityGroup ] = modifier ; } else { ( { operator , operand , priorityGroup } = modifier ) ; } if ( ! operator ) { throw new Error( "Modifier without an operator" ) ; } // note that some operators don't need right-hand-side operand if ( ! operators[ operator ] ) { throw new Error( "Unknown operator '" + operator + "'" ) ; } // unknown operator if ( operand === undefined ) { throw new Error( "Missing operand" ) ; } // missing operand this.setStatModifier( statName , operator , operand , priorityGroup ) ; if ( operators[ operator ].operandSubtreeExpansion && common.isPlainObject( operand ) ) { // This is both a regular operator AND a nested object syntax. // This is useful for the existence operator '#' to define subtree and existence in only one key, // it's very useful for KFG. for ( let key in operand ) { this.setStatModifiers( statName + '.' + key , operand[ key ] ) ; } } } } } ; ModifiersTable.prototype.setStatModifier = function( statName , operator , operand , priorityGroup = null ) { if ( this.destroyed ) { return ; } var canonicalOperator , key , ops = this.statsModifiers[ statName ] ; if ( ! operators[ operator ] ) { throw new Error( "Unknown operator '" + operator + "'" ) ; } if ( operators[ operator ].convert ) { operand = operators[ operator ]( operand ) ; operator = operators[ operator ].convert ; } key = canonicalOperator = operators[ operator ].id ; if ( priorityGroup !== null && priorityGroup !== operators[ operator ].priorityGroup ) { key += '_' + ( priorityGroup < 0 ? 'm' + ( - priorityGroup ) : 'p' + priorityGroup ) ; } if ( ops[ key ] ) { ops[ key ].merge( operand ) ; } else { ops[ key ] = new Modifier( this.id , canonicalOperator , operand , priorityGroup , this.active ) ; if ( this.stackedOn ) { this.stackedOn.addOneStatModifier( statName , ops[ key ] ) ; } } } ; // 'changeId' falsy: don't change ID, true: create a new id from current, any other truthy: the new ID ModifiersTable.prototype.clone = function( changeId = true ) { return new ModifiersTable( ! changeId ? this.id : changeId === true ? common.createCloneId( this.id ) : changeId , null , this.active , this.isTemplate ).extend( this ) ; } ; ModifiersTable.prototype.cloneProxy = function( changeId ) { return this.clone( changeId ).getProxy() ; } ; // If fromCloneId is set, we want to clone rather than instanciate a template, but since this share a lot of code... ModifiersTable.prototype.instanciate = function() { if ( ! this.isTemplate ) { return this ; } // /!\ or error??? return new ModifiersTable( this.id + '_' + ( this.templateInstanceCount ++ ) , null , this.active , false ).extend( this ) ; } ; // Extend the current modifiers table with another ModifiersTable.prototype.extend = function( withModifiersTable ) { var statName , modifiers , eventName ; for ( statName in withModifiersTable.statsModifiers ) { modifiers = Object.values( withModifiersTable.statsModifiers[ statName ] ).map( e => [ e.operator , e.operand , e.priorityGroup ] ) ; this.setStatModifiers( statName , modifiers ) ; } for ( eventName in withModifiersTable.events ) { withModifiersTable.events[ eventName ].forEach( event => this.setEvent_( eventName , event.times , event.every , event.action , event.params ) ) ; } return this ; } ; ModifiersTable.prototype.destroy = function() { this.activate( false ) ; this.destroyed = true ; } ; ModifiersTable.prototype.deactivate = function() { return this.activate( false ) ; } ; ModifiersTable.prototype.activate = function( active = true ) { if ( this.destroyed ) { return ; } active = !! active ; if ( active === this.active ) { return ; } this.active = active ; for ( let statName in this.statsModifiers ) { for ( let operator in this.statsModifiers[ statName ] ) { this.statsModifiers[ statName ][ operator ].active = this.active ; } } } ; const EVENT_RESERVED_KEYS = new Set( [ 'name' , 'times' , 'every' , 'action' , 'params' ] ) ; ModifiersTable.prototype.setRecurringEvent = function( eventName , action , params ) { return this.setEvent_( eventName , Infinity , 1 , action , params ) ; } ; ModifiersTable.prototype.setOneTimeEvent = function( eventName , action , params ) { return this.setEvent_( eventName , 1 , 1 , action , params ) ; } ; ModifiersTable.prototype.setEveryEvent = function( eventName , every , action , params ) { return this.setEvent_( eventName , Infinity , every , action , params ) ; } ; ModifiersTable.prototype.setCountdownEvent = function( eventName , countdown , action , params ) { return this.setEvent_( eventName , 1 , countdown , action , params ) ; } ; // Mainly for KFG ModifiersTable.prototype.setEvent = function( event ) { var params = null ; if ( ! event || typeof event !== 'object' ) { return ; } if ( event.params && typeof event.params === 'object' ) { params = event.params ; } else { // The params could be embedded on the top-level object (KFG shorthand syntax) for ( let key in event ) { if ( ! EVENT_RESERVED_KEYS.has( key ) ) { if ( ! params ) { params = {} ; } params[ key ] = event[ key ] ; } } } return this.setEvent_( event.name || '' , event.times !== undefined ? + event.times || 0 : Infinity , event.every !== undefined ? + event.every || 0 : 1 , event.action || '' , params ) ; } ; /* action: the function ID times: how many times the event occurs every: triggered only every X eventName */ ModifiersTable.prototype.setEvent_ = function( eventName , times , every , action , params ) { if ( ! eventActions[ action ] ) { return ; } if ( ! this.events[ eventName ] ) { this.events[ eventName ] = [] ; } this.events[ eventName ].push( { action , times , every , params , count: 0 , done: false } ) ; } ; // Trigger an event ModifiersTable.prototype.trigger = function( eventName ) { if ( this.destroyed ) { return ; } var deleteNeeded = false , events = this.events[ eventName ] ; if ( ! events || ! events.length ) { return ; } events.forEach( eventData => { // If the action recursively trigger a .trigger(), we need to check for eventData.done now if ( eventData.done ) { deleteNeeded = true ; return ; } eventData.count ++ ; if ( eventData.count % eventData.every !== 0 ) { return ; } if ( eventData.count / eventData.every >= eventData.times ) { eventData.done = true ; } eventActions[ eventData.action ]( this , eventData , eventData.params ) ; // Should be added after the actionFn, because it can set it to done (e.g.: fade) if ( eventData.done ) { deleteNeeded = true ; } } ) ; if ( deleteNeeded ) { //arrayKit.inPlaceFilter( events , eventData => eventData.countdown === null || eventData.countdown > 0 ) ; arrayKit.inPlaceFilter( events , eventData => ! eventData.done ) ; } } ; ModifiersTable.prototype.forEachModifier = function( fn ) { var statName , op ; for ( statName in this.statsModifiers ) { for ( op in this.statsModifiers[ statName ] ) { fn( this.statsModifiers[ statName ][ op ] , statName ) ; } } } ; ModifiersTable.prototype.getProxy = function() { if ( this.proxy ) { return this.proxy ; } this.proxy = new Proxy( this , MODIFIERS_TABLE_HANDLER ) ; return this.proxy ; } ; const MODIFIERS_TABLE_PROXY_METHODS = new Set( [ 'setStatModifiers' , 'trigger' ] ) ; const MODIFIERS_TABLE_HANDLER = { get: ( target , property ) => { if ( property === common.SYMBOL_UNPROXY ) { return target ; } if ( property === '__prototypeUID__' ) { return target.__prototypeUID__ ; } if ( property === '__prototypeVersion__' ) { return target.__prototypeVersion__ ; } if ( property === 'constructor' ) { return ModifiersTable ; } if ( property === 'toString' ) { return Object.prototype.toString ; } if ( property === 'clone' ) { return target.cloneProxy.bind( target ) ; } if ( MODIFIERS_TABLE_PROXY_METHODS.has( property ) ) { //return Reflect.get( target , property , receiver ) ; // Don't work, not bounded return target[ property ].bind( target ) ; } if ( target.statsModifiers[ property ] ) { let modifiers = target.statsModifiers[ property ] ; if ( modifiers[ common.SYMBOL_PROXY ] ) { return modifiers[ common.SYMBOL_PROXY ] ; } modifiers[ common.SYMBOL_PARENT ] = target ; modifiers[ common.SYMBOL_STAT_NAME ] = property ; return modifiers[ common.SYMBOL_PROXY ] = new Proxy( modifiers , INTERMEDIATE_MODIFIERS_TABLE_HANDLER ) ; } return ; } , // Mostly a copy of .get() has: ( target , property ) => { //if ( property === common.SYMBOL_UNPROXY ) { return target ; } //if ( property === '__prototypeUID__' ) { return target.__prototypeUID__ ; } //if ( property === '__prototypeVersion__' ) { return target.__prototypeVersion__ ; } if ( property === 'constructor' ) { return true ; } if ( property === 'toString' ) { return true ; } if ( property === 'clone' ) { return true ; } if ( MODIFIERS_TABLE_PROXY_METHODS.has( property ) ) { return true ; } if ( target.statsModifiers[ property ] ) { return true ; } return false ; } , set: () => false , deleteProperty: () => false , ownKeys: ( target ) => [ ... Object.keys( target.statsModifiers ) ] , getOwnPropertyDescriptor: ( target , property ) => { // configurable:true is forced by Proxy Invariants if ( target.statsModifiers[ property ] ) { return { value: MODIFIERS_TABLE_HANDLER.get( target , property , target ) , configurable: true } ; } } , getPrototypeOf: ( target ) => Reflect.getPrototypeOf( target ) , setPrototypeOf: () => false } ; // Proxy for the .statsModifiers[ key ] objects const INTERMEDIATE_MODIFIERS_TABLE_HANDLER = { get: ( target , property , receiver ) => { if ( property === common.SYMBOL_UNPROXY ) { return target ; } if ( property === 'constructor' ) { return Object ; } if ( property === 'toString' ) { return Object.prototype.toString ; } var targetValue = target[ property ] ; if ( targetValue && typeof targetValue === 'object' ) { return targetValue.getProxy() ; } return Reflect.get( target , property , receiver ) ; } , // Mostly a copy of .get() has: ( target , property ) => { if ( typeof property !== 'string' ) { return false ; } if ( property === 'constructor' ) { return true ; } if ( property === 'toString' ) { return true ; } return property in target ; } , set: ( target , property , value ) => { if ( typeof property !== 'string' ) { return false ; } var targetValue = target[ property ] ; if ( targetValue && typeof targetValue === 'object' ) { if ( ! value || typeof value !== 'object' ) { targetValue.set( value ) ; return true ; } } if ( targetValue === undefined ) { let match = property.match( /^([a-zA-Z_]+)([0-9]*)$/ ) ; if ( ! match ) { return false ; } let parent = target[ common.SYMBOL_PARENT ] , statName = target[ common.SYMBOL_STAT_NAME ] , operator = match[ 1 ] , priorityGroup = + match[ 2 ] ; parent.setStatModifier( statName , operator , value , priorityGroup ) ; return true ; } return false ; } , deleteProperty: ( target , property ) => { if ( typeof property !== 'string' ) { return false ; } if ( Object.hasOwn( target , property ) ) { return delete target[ property ] ; } return false ; } , ownKeys: ( target ) => Object.keys( target ) , getOwnPropertyDescriptor: ( target , property ) => { if ( typeof property !== 'string' ) { return ; } if ( ! Object.hasOwn( target , property ) ) { return ; } return { value: target?.getProxy ? target.getProxy() : target , writable: true , // Mandatory, for some reasons .ownKeys() is always cross-checking each props using getOwnPropertyDescriptor().enumerable enumerable: true , configurable: true } ; } , getPrototypeOf: () => Object.prototype , setPrototypeOf: () => false } ;