import { IRange, logger } from '@motajs/common'; import { ITileLocator } from '@user/types'; import { IAuraConverter, IAuraView, IEnemy, IEnemyAuraView, IEnemyCommonQueryEffect, IEnemyContext, IEnemyFinalEffect, IEnemySpecialModifier, IEnemySpecialQueryEffect, IEnemyView, IMapDamage, IReadonlyEnemy, ISpecial } from './types'; import { EnemyView } from './enemy'; import { MapLocIndexer } from './utils'; export class EnemyContext implements IEnemyContext { private readonly enemyViewMap: Map> = new Map(); private readonly enemyMap: Map> = new Map(); private readonly locatorViewMap: Map, number> = new Map(); private readonly locatorEnemyMap: Map, number> = new Map(); private readonly computedToView: Map< IReadonlyEnemy, EnemyView > = new Map(); private readonly auraConverter: Set> = new Set(); private readonly converterStatus: Map, boolean> = new Map(); private readonly convertedAura: Map, IAuraView> = new Map(); private readonly commonQueryMap: Map< number, IEnemyCommonQueryEffect[] > = new Map(); private readonly specialQueryEffects: Map< number, IEnemySpecialQueryEffect[] > = new Map(); private readonly finalEffects: IEnemyFinalEffect[] = []; private readonly globalAuraList: Set> = new Set(); private readonly sortedAura: Map>> = new Map(); private readonly needTotallyRefresh: Set> = new Set(); private readonly requestedCommonContext: Set> = new Set(); private readonly dirtyEnemy: Set> = new Set(); private mapDamage: IMapDamage | null = null; readonly indexer: MapLocIndexer = new MapLocIndexer(); private needUpdate: boolean = true; built: boolean = false; width: number = 0; height: number = 0; resize(width: number, height: number): void { this.clear(); this.width = width; this.height = height; this.indexer.setWidth(width); } registerAuraConverter(converter: IAuraConverter): void { this.auraConverter.add(converter); this.converterStatus.set(converter, true); } unregisterAuraConverter(converter: IAuraConverter): void { this.auraConverter.delete(converter); this.converterStatus.delete(converter); } setAuraConverterEnabled( converter: IAuraConverter, enabled: boolean ): void { if (!this.auraConverter.has(converter)) return; this.converterStatus.set(converter, enabled); } registerCommonQueryEffect( code: number, effect: IEnemyCommonQueryEffect ): void { const array = this.commonQueryMap.getOrInsert(code, []); array.push(effect); array.sort((a, b) => b.priority - a.priority); } unregisterCommonQueryEffect( code: number, effect: IEnemyCommonQueryEffect ): void { const array = this.commonQueryMap.get(code); if (!array) return; const index = array.indexOf(effect); if (index === -1) return; array.splice(index, 1); } registerSpecialQueryEffect(effect: IEnemySpecialQueryEffect): void { const list = this.specialQueryEffects.getOrInsert(effect.priority, []); list.push(effect); } unregisterSpecialQueryEffect( effect: IEnemySpecialQueryEffect ): void { const list = this.specialQueryEffects.get(effect.priority); if (!list) return; const index = list.indexOf(effect); if (index !== -1) { list.splice(index, 1); } if (list.length === 0) { this.specialQueryEffects.delete(effect.priority); } } registerFinalEffect(effect: IEnemyFinalEffect): void { this.finalEffects.push(effect); this.finalEffects.sort((a, b) => b.priority - a.priority); } unregisterFinalEffect(effect: IEnemyFinalEffect): void { const index = this.finalEffects.indexOf(effect); if (index !== -1) { this.finalEffects.splice(index, 1); } } getEnemyLocator(enemy: IEnemy): Readonly | null { const index = this.locatorEnemyMap.get(enemy); if (index === undefined) return null; return this.indexer.indexToLocator(index); } getEnemyLocatorByView( view: IEnemyView ): Readonly | null { const index = this.locatorViewMap.get(view); if (index === undefined) return null; return this.indexer.indexToLocator(index); } getEnemyByLocator(locator: ITileLocator): IEnemyView | null { const index = this.indexer.locToIndex(locator.x, locator.y); return this.enemyViewMap.get(index) ?? null; } getEnemyByLoc(x: number, y: number): IEnemyView | null { const index = this.indexer.locToIndex(x, y); return this.enemyViewMap.get(index) ?? null; } getViewByComputed(enemy: IReadonlyEnemy): IEnemyView | null { return this.computedToView.get(enemy) ?? null; } private deleteEnemyAt(index: number) { const view = this.enemyViewMap.get(index); const enemy = this.enemyMap.get(index); if (!view || !enemy) return; this.needUpdate = true; if (this.mapDamage) { this.mapDamage.deleteEnemy(view); } this.needTotallyRefresh.delete(view); this.dirtyEnemy.delete(view); this.requestedCommonContext.delete(view); this.computedToView.delete(view.getComputingEnemy()); this.enemyViewMap.delete(index); this.enemyMap.delete(index); this.locatorViewMap.delete(view); this.locatorEnemyMap.delete(enemy); } setEnemyAt(locator: ITileLocator, enemy: IEnemy): void { const index = this.indexer.locToIndex(locator.x, locator.y); this.deleteEnemyAt(index); const view = new EnemyView(enemy, this); this.enemyMap.set(index, enemy); this.enemyViewMap.set(index, view); this.locatorEnemyMap.set(enemy, index); this.locatorViewMap.set(view, index); this.computedToView.set(view.getComputingEnemy(), view); this.needUpdate = true; } deleteEnemy(locator: ITileLocator): void { const index = this.indexer.locToIndex(locator.x, locator.y); this.deleteEnemyAt(index); } private *internalScanRange( range: IRange, param: T ): Iterable> { range.bindHost(this); const keys = new Set(this.enemyViewMap.keys()); const matched = range.autoDetect(keys, param); const viewMap = this.enemyViewMap; for (const index of matched) { const view = viewMap.get(index); if (view) { yield view; } } } scanRange(range: IRange, param: T): Iterable> { return this.internalScanRange(range, param); } *iterateEnemy(): Iterable<[ITileLocator, IEnemyView]> { for (const [index, view] of this.enemyViewMap) { const locator = this.indexer.indexToLocator(index); yield [locator, view]; } } addAura(aura: IAuraView): void { this.globalAuraList.add(aura); this.needUpdate = true; } deleteAura(aura: IAuraView): void { this.globalAuraList.delete(aura); this.needUpdate = true; } attachMapDamage(damage: IMapDamage | null): void { this.mapDamage = damage; if (damage) { damage.refreshAll(); } } getMapDamage(): IMapDamage | null { return this.mapDamage; } private convertSpecial( special: ISpecial, enemy: IReadonlyEnemy, locator: ITileLocator ): IEnemyAuraView | null { let matched: IAuraConverter | null = null; for (const converter of this.auraConverter) { if (!this.converterStatus.get(converter)) continue; if (converter.shouldConvert(special, enemy, locator)) { if (matched) { logger.warn(97, special.code.toString()); return null; } matched = converter; } } if (!matched) return null; return matched.convert(special, enemy, locator, this); } private insertIntoSortedAura(aura: IAuraView): void { const set = this.sortedAura.getOrInsertComputed( aura.priority, () => new Set() ); set.add(aura); } private removeFromSortedAura(aura: IAuraView): void { const set = this.sortedAura.get(aura.priority); if (set) { set.delete(aura); if (set.size === 0) { this.sortedAura.delete(aura.priority); } } } private processSpecialModifier( modifier: IEnemySpecialModifier, enemy: IEnemy, locator: ITileLocator, currentPriority: number ): Set> { const toAdd = modifier.add(enemy, locator); const toDelete = modifier.delete(enemy, locator); const affectedAuras = new Set>(); if (toAdd.length > 0 && toDelete.length > 0) { logger.warn(100); return affectedAuras; } for (const adding of toAdd) { const aura = this.convertSpecial(adding, enemy, locator); if (aura) { if (import.meta.env.DEV && aura.priority > currentPriority) { logger.warn( 99, aura.priority.toString(), currentPriority.toString() ); continue; } this.convertedAura.set(adding, aura); this.insertIntoSortedAura(aura); affectedAuras.add(aura); } enemy.addSpecial(adding); } for (const deleting of toDelete) { enemy.deleteSpecial(deleting); const aura = this.convertedAura.get(deleting); if (aura) { if (import.meta.env.DEV && aura.priority >= currentPriority) { logger.warn( 98, aura.priority.toString(), currentPriority.toString() ); continue; } this.removeFromSortedAura(aura); this.convertedAura.delete(deleting); affectedAuras.add(aura); } } for (const special of enemy.iterateSpecials()) { const success = modifier.modify(enemy, special, locator); if (!success) continue; const aura = this.convertedAura.get(special); if (!aura) continue; affectedAuras.add(aura); if (import.meta.env.DEV && aura.priority >= currentPriority) { logger.warn( 98, aura.priority.toString(), currentPriority.toString() ); } } return affectedAuras; } private processSpecialQuery( effect: IEnemySpecialQueryEffect, currentPriority: number ): void { const modifier = effect.for(this); for (const [index, view] of this.enemyViewMap) { const locator = this.indexer.indexToLocator(index); const enemy = view.getComputingEnemy(); if (!modifier.shouldQuery(enemy, locator)) continue; const affectedAuras = this.processSpecialModifier( modifier, enemy, locator, currentPriority ); if (affectedAuras.size > 0) { this.needTotallyRefresh.add(view); } else { this.requestedCommonContext.add(view); } } } private processAuraSpecial( aura: IAuraView, currentPriority: number ): void { const param = aura.getRangeParam(); for (const enemyView of this.internalScanRange(aura.range, param)) { const locator = this.getEnemyLocatorByView(enemyView); if (!locator) continue; const enemy = enemyView.getComputingEnemy(); const base = enemyView.getBaseEnemy(); const modifier = aura.applySpecial(enemy, base, locator); if (!modifier) continue; this.processSpecialModifier( modifier, enemy, locator, currentPriority ); this.needTotallyRefresh.add(enemyView); } } private buildupSpecials(): void { for (const aura of this.globalAuraList) { this.insertIntoSortedAura(aura); } for (const [index, view] of this.enemyViewMap) { const enemy = view.getComputingEnemy(); const locator = this.indexer.indexToLocator(index); for (const special of enemy.iterateSpecials()) { const aura = this.convertSpecial(special, enemy, locator); if (!aura) continue; this.convertedAura.set(special, aura); this.insertIntoSortedAura(aura); } } const processedPriorities = new Set(); while (true) { let maxPriority: number | null = null; for (const priority of this.sortedAura.keys()) { if (!processedPriorities.has(priority)) { if (maxPriority === null || priority > maxPriority) { maxPriority = priority; } } } for (const priority of this.specialQueryEffects.keys()) { if (!processedPriorities.has(priority)) { if (maxPriority === null || priority > maxPriority) { maxPriority = priority; } } } if (maxPriority === null) break; processedPriorities.add(maxPriority); const auras = this.sortedAura.get(maxPriority); if (auras) { for (const aura of auras) { if (aura.couldApplySpecial) { this.processAuraSpecial(aura, maxPriority); } } } const effects = this.specialQueryEffects.get(maxPriority); if (effects) { for (const effect of effects) { this.processSpecialQuery(effect, maxPriority); } } } } private buildupBase(): void { const priorities = [...this.sortedAura.keys()].sort((a, b) => b - a); for (const p of priorities) { const auras = this.sortedAura.get(p); if (!auras) continue; for (const aura of auras) { const param = aura.getRangeParam(); for (const view of this.internalScanRange(aura.range, param)) { const enemy = view.getComputingEnemy(); const base = view.getBaseEnemy(); const locator = this.getEnemyLocatorByView(view)!; aura.apply(enemy, base, locator); } } } } private buildupQuery(): void { for (const [index, view] of this.enemyViewMap) { const enemy = view.getComputingEnemy(); const locator = this.indexer.indexToLocator(index); let queried = false; const query = () => { queried = true; return this; }; for (const special of enemy.iterateSpecials()) { const effects = this.commonQueryMap.get(special.code); if (!effects) continue; for (const effect of effects) { effect.apply(enemy, special, query, locator); } } if (queried) { this.requestedCommonContext.add(view); } } } private buildupFinal(): void { for (const [index, view] of this.enemyViewMap) { const enemy = view.getComputingEnemy(); const locator = this.indexer.indexToLocator(index); for (const effect of this.finalEffects) { effect.apply(enemy, locator); } } } buildup(): void { if (!this.needUpdate) return; this.needUpdate = false; this.sortedAura.clear(); this.convertedAura.clear(); this.dirtyEnemy.clear(); this.needTotallyRefresh.clear(); this.requestedCommonContext.clear(); const hasAura = this.auraConverter.size > 0; const hasSpecialQuery = this.specialQueryEffects.size > 0; if (hasAura || hasSpecialQuery) { this.buildupSpecials(); this.buildupBase(); } if (this.commonQueryMap.size > 0) { this.buildupQuery(); } if (this.finalEffects.length > 0) { this.buildupFinal(); } if (this.mapDamage) { this.mapDamage.refreshAll(); } } markDirty(view: IEnemyView): void { if (!this.locatorViewMap.has(view)) return; this.dirtyEnemy.add(view); } private refreshSpecialModifier( modifier: IEnemySpecialModifier, enemy: IEnemy, locator: ITileLocator ): void { const toAdd = modifier.add(enemy, locator); const toDelete = modifier.delete(enemy, locator); if (toAdd.length > 0 && toDelete.length > 0) { logger.warn(100); return; } for (const adding of toAdd) { enemy.addSpecial(adding); if (import.meta.env.DEV) { const aura = this.convertSpecial(adding, enemy, locator); if (aura) { logger.warn(101, adding.code.toString()); } } } for (const deleting of toDelete) { enemy.deleteSpecial(deleting); if (import.meta.env.DEV) { const aura = this.convertSpecial(deleting, enemy, locator); if (aura) { logger.warn(101, deleting.code.toString()); } } } for (const special of enemy.iterateSpecials()) { const success = modifier.modify(enemy, special, locator); if (import.meta.env.DEV && success) { const aura = this.convertedAura.get(special); if (aura) { logger.warn(101, special.code.toString()); } } } } private refreshEnemy(view: EnemyView): void { const locator = this.getEnemyLocatorByView(view); if (!locator) return; view.reset(); const enemy = view.getComputingEnemy(); const base = view.getBaseEnemy(); const specialPriorities = new Set(); for (const priority of this.sortedAura.keys()) { specialPriorities.add(priority); } for (const priority of this.specialQueryEffects.keys()) { specialPriorities.add(priority); } const orderedSpecialPriorities = [...specialPriorities].sort( (a, b) => b - a ); for (const priority of orderedSpecialPriorities) { const auras = this.sortedAura.get(priority); if (auras) { for (const aura of auras) { if (!aura.couldApplySpecial) continue; const param = aura.getRangeParam(); aura.range.bindHost(this); const inRange = aura.range.inRange( locator.x, locator.y, param ); if (!inRange) continue; const modifier = aura.applySpecial(enemy, base, locator); if (!modifier) continue; this.refreshSpecialModifier(modifier, enemy, locator); } } const effects = this.specialQueryEffects.get(priority); if (effects) { for (const effect of effects) { const modifier = effect.for(this); if (!modifier.shouldQuery(enemy, locator)) continue; this.refreshSpecialModifier(modifier, enemy, locator); } } } const basePriorities = [...this.sortedAura.keys()].sort( (a, b) => b - a ); for (const priority of basePriorities) { const auras = this.sortedAura.get(priority); if (!auras) continue; for (const aura of auras) { const param = aura.getRangeParam(); aura.range.bindHost(this); if (!aura.range.inRange(locator.x, locator.y, param)) { continue; } aura.apply(enemy, base, locator); } } this.requestedCommonContext.delete(view); let queried = false; const query = () => { queried = true; return this; }; for (const special of enemy.iterateSpecials()) { const effects = this.commonQueryMap.get(special.code); if (!effects) continue; for (const effect of effects) { effect.apply(enemy, special, query, locator); } } if (queried) { this.requestedCommonContext.add(view); } for (const effect of this.finalEffects) { effect.apply(enemy, locator); } this.dirtyEnemy.delete(view); if (this.mapDamage) { this.mapDamage.markEnemyDirty(view); } } requestRefresh(view: IEnemyView): void { if (!this.dirtyEnemy.has(view)) return; if (this.needTotallyRefresh.has(view)) { this.needUpdate = true; } if (this.needUpdate) { this.buildup(); return; } this.refreshEnemy(view as EnemyView); for (const requestedView of this.requestedCommonContext) { if (requestedView === view) continue; this.refreshEnemy(requestedView as EnemyView); } } clear(): void { this.enemyViewMap.clear(); this.enemyMap.clear(); this.locatorViewMap.clear(); this.locatorEnemyMap.clear(); this.computedToView.clear(); this.globalAuraList.clear(); this.sortedAura.clear(); this.needTotallyRefresh.clear(); this.requestedCommonContext.clear(); this.dirtyEnemy.clear(); if (this.mapDamage) { this.mapDamage.refreshAll(); } } destroy(): void { this.clear(); this.attachMapDamage(null); this.auraConverter.clear(); this.commonQueryMap.clear(); this.specialQueryEffects.clear(); this.finalEffects.length = 0; } }