From 057d5ab813a3064e8a9f0fd91003a5c7ce2be04a Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 14 Apr 2026 18:10:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=80=AA=E7=89=A9=E5=8F=8A=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=E4=BC=A4=E5=AE=B3=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages-user/data-base/package.json | 4 +- packages-user/data-base/src/enemy/context.ts | 720 +++++++++++++++++ packages-user/data-base/src/enemy/enemy.ts | 134 ++++ packages-user/data-base/src/enemy/index.ts | 7 + packages-user/data-base/src/enemy/manager.ts | 146 ++++ .../data-base/src/enemy/mapDamage.ts | 341 ++++++++ packages-user/data-base/src/enemy/special.ts | 95 +++ packages-user/data-base/src/enemy/types.ts | 739 ++++++++++++++++++ packages-user/data-base/src/enemy/utils.ts | 25 + packages-user/data-base/src/index.ts | 1 + packages-user/data-state/src/core.ts | 5 +- packages-user/data-state/src/data.ts | 12 + packages-user/data-state/src/enemy/aura.ts | 121 +++ packages-user/data-state/src/enemy/index.ts | 2 + .../data-state/src/enemy/mapSpecials.ts | 358 +++++++++ packages-user/data-state/src/enemy/special.ts | 636 +++++++++------ packages-user/data-state/src/enemy/types.ts | 12 + packages-user/data-state/src/types.ts | 8 + packages-user/types/src/index.ts | 1 + packages-user/types/src/spatial.ts | 6 + packages/common/src/logger.json | 11 + packages/common/src/utils/dir.ts | 36 + packages/common/src/utils/index.ts | 2 + packages/common/src/utils/range.ts | 302 +++++++ packages/common/src/utils/types.ts | 125 +++ packages/types/src/index.ts | 1 + packages/types/src/utils.ts | 4 + src/types/declaration/enemy.d.ts | 48 +- 28 files changed, 3638 insertions(+), 264 deletions(-) create mode 100644 packages-user/data-base/src/enemy/context.ts create mode 100644 packages-user/data-base/src/enemy/enemy.ts create mode 100644 packages-user/data-base/src/enemy/index.ts create mode 100644 packages-user/data-base/src/enemy/manager.ts create mode 100644 packages-user/data-base/src/enemy/mapDamage.ts create mode 100644 packages-user/data-base/src/enemy/special.ts create mode 100644 packages-user/data-base/src/enemy/types.ts create mode 100644 packages-user/data-base/src/enemy/utils.ts create mode 100644 packages-user/data-state/src/data.ts create mode 100644 packages-user/data-state/src/enemy/aura.ts create mode 100644 packages-user/data-state/src/enemy/mapSpecials.ts create mode 100644 packages-user/data-state/src/enemy/types.ts create mode 100644 packages-user/types/src/spatial.ts create mode 100644 packages/common/src/utils/dir.ts create mode 100644 packages/common/src/utils/range.ts create mode 100644 packages/types/src/utils.ts diff --git a/packages-user/data-base/package.json b/packages-user/data-base/package.json index 0ed09ab..41e78e2 100644 --- a/packages-user/data-base/package.json +++ b/packages-user/data-base/package.json @@ -1,7 +1,9 @@ { "name": "@user/data-base", "dependencies": { + "@motajs/common": "workspace:*", "@motajs/types": "workspace:*", - "@motajs/loader": "workspace:*" + "@motajs/loader": "workspace:*", + "@user/types": "workspace:*" } } diff --git a/packages-user/data-base/src/enemy/context.ts b/packages-user/data-base/src/enemy/context.ts new file mode 100644 index 0000000..2b578d8 --- /dev/null +++ b/packages-user/data-base/src/enemy/context.ts @@ -0,0 +1,720 @@ +import { IRange, logger } from '@motajs/common'; +import { + IAuraConverter, + IAuraView, + IEnemy, + IEnemyAuraView, + IEnemyCommonQueryEffect, + IEnemyContext, + IEnemyFinalEffect, + IEnemySpecialModifier, + IEnemySpecialQueryEffect, + IEnemyView, + IMapDamage, + IReadonlyEnemy, + ISpecial +} from './types'; +import { EnemyView } from './enemy'; +import { MapLocIndexer } from './utils'; +import { ITileLocator } from '@user/types'; + +export class EnemyContext implements IEnemyContext { + private readonly enemyViewMap: Map = new Map(); + private readonly enemyMap: Map = new Map(); + private readonly locatorViewMap: Map = new Map(); + private readonly locatorEnemyMap: Map = new Map(); + private readonly computedToView: Map = new Map(); + + private readonly auraConverter: Set = new Set(); + private readonly converterStatus: Map = new Map(); + private readonly convertedAura: Map, IAuraView> = new Map(); + + private readonly commonQueryMap: Map = + 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); + } + + 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.specials) { + 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.specials) { + 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.specials) { + 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; + } +} diff --git a/packages-user/data-base/src/enemy/enemy.ts b/packages-user/data-base/src/enemy/enemy.ts new file mode 100644 index 0000000..1dc76c3 --- /dev/null +++ b/packages-user/data-base/src/enemy/enemy.ts @@ -0,0 +1,134 @@ +import { logger } from '@motajs/common'; +import { + IEnemy, + IEnemyAttributes, + IEnemyContext, + IReadonlyEnemy, + ISpecial, + IEnemyView +} from './types'; + +export class Enemy implements IEnemy { + readonly specials: Set> = new Set(); + /** code -> ISpecial 映射,用于快速查找 */ + private readonly specialMap: Map> = new Map(); + + constructor( + readonly id: string, + readonly code: number, + readonly attributes: IEnemyAttributes + ) {} + + getSpecial(code: number): ISpecial | null { + return (this.specialMap.get(code) as ISpecial) ?? null; + } + + hasSpecial(code: number): boolean { + return this.specialMap.has(code); + } + + addSpecial(special: ISpecial): void { + if (this.specialMap.has(special.code)) { + logger.warn(96, this.id, special.code.toString()); + return; + } + this.specials.add(special); + this.specialMap.set(special.code, special); + } + + deleteSpecial(special: number | ISpecial): void { + const code = typeof special === 'number' ? special : special.code; + const existing = this.specialMap.get(code); + if (!existing) return; + this.specials.delete(existing); + this.specialMap.delete(code); + } + + iterateSpecials(): Iterable> { + return this.specials; + } + + setAttribute( + key: K, + value: IEnemyAttributes[K] + ): void { + this.attributes[key] = value; + } + + getAttribute( + key: K + ): IEnemyAttributes[K] { + return this.attributes[key]; + } + + clone(): IEnemy { + const cloned = new Enemy( + this.id, + this.code, + structuredClone(this.attributes) + ); + for (const special of this.specials) { + cloned.addSpecial(special.clone()); + } + return cloned; + } + + copy(enemy: IReadonlyEnemy): void { + ATTRIBUTE_KEYS.forEach(key => { + this.setAttribute(key, structuredClone(enemy.getAttribute(key))); + }); + this.specials.clear(); + this.specialMap.clear(); + for (const special of enemy.iterateSpecials()) { + this.addSpecial(special.clone()); + } + } +} + +export class EnemyView implements IEnemyView { + private computedEnemy: IEnemy; + + constructor( + readonly baseEnemy: IEnemy, + readonly context: IEnemyContext + ) { + this.computedEnemy = baseEnemy.clone(); + } + + reset(): void { + this.computedEnemy.copy(this.baseEnemy); + } + + getBaseEnemy(): IReadonlyEnemy { + return this.baseEnemy; + } + + getComputedEnemy(): IReadonlyEnemy { + this.context.requestRefresh(this); + return this.computedEnemy; + } + + /** + * 获取计算中怪物对象,这个接口不对外暴露,仅在系统内部的 EnemyContext 中使用。 + */ + getComputingEnemy(): IEnemy { + return this.computedEnemy; + } + + getModifiableEnemy(): IEnemy { + return this.baseEnemy; + } + + markDirty(): void { + this.context.markDirty(this); + } +} + +export const ATTRIBUTE_KEYS: (keyof IEnemyAttributes)[] = [ + 'hp', + 'atk', + 'def', + 'money', + 'exp', + 'point' +]; diff --git a/packages-user/data-base/src/enemy/index.ts b/packages-user/data-base/src/enemy/index.ts new file mode 100644 index 0000000..875a8b3 --- /dev/null +++ b/packages-user/data-base/src/enemy/index.ts @@ -0,0 +1,7 @@ +export * from './enemy'; +export * from './context'; +export * from './mapDamage'; +export * from './manager'; +export * from './special'; +export * from './types'; +export * from './utils'; diff --git a/packages-user/data-base/src/enemy/manager.ts b/packages-user/data-base/src/enemy/manager.ts new file mode 100644 index 0000000..f3bd8da --- /dev/null +++ b/packages-user/data-base/src/enemy/manager.ts @@ -0,0 +1,146 @@ +import { logger } from '@motajs/common'; +import { Enemy as EnemyImpl } from './enemy'; +import { + IEnemy, + IEnemyAttributes, + IEnemyManager, + ISpecial, + SpecialCreation +} from './types'; + +export class EnemyManager implements IEnemyManager { + /** 特殊属性注册表,code -> 创建函数 */ + private readonly specialRegistry: Map> = + new Map(); + /** 自定义怪物属性注册表,name -> 默认值 */ + private readonly attributeRegistry: Map = new Map(); + /** 怪物模板表,code -> IEnemy */ + private readonly prefabByCode: Map = new Map(); + /** 怪物模板表,id -> IEnemy */ + private readonly prefabById: Map = new Map(); + /** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */ + private readonly legacyIdToCode: Map = new Map(); + + registerSpecial( + code: number, + cons: (enemy: IEnemy) => ISpecial + ): void { + this.specialRegistry.set(code, cons); + } + + registerAttribute(name: string, defaultValue: any): void { + if ( + typeof defaultValue === 'function' || + typeof defaultValue === 'symbol' || + typeof defaultValue === 'bigint' || + typeof defaultValue === 'undefined' + ) { + logger.error(53); + return; + } + this.attributeRegistry.set(name, defaultValue); + } + + fromLegacyEnemy(enemy: Enemy): IEnemy { + // 如果该旧样板怪物已经通过 addPrefabFromLegacy 注册为模板,直接克隆模板 + const existingCode = this.legacyIdToCode.get(enemy.id); + if (existingCode) { + const prefab = this.prefabByCode.get(existingCode); + if (prefab) { + return prefab.clone(); + } + } + + return this.convertLegacyEnemy(0, enemy); + } + + /** + * 真正执行旧样板怪物到新怪物对象的转换 + * @param code 怪物图块数字 + * @param enemy 旧样板怪物对象 + */ + private convertLegacyEnemy(code: number, enemy: Enemy): IEnemy { + const attrs: IEnemyAttributes = { + hp: enemy.hp, + atk: enemy.atk, + def: enemy.def, + money: enemy.money, + exp: enemy.exp, + point: enemy.point + }; + const result = new EnemyImpl(enemy.id, code, structuredClone(attrs)); + + // 转换特殊属性 + if (enemy.special) { + for (const specialCode of enemy.special) { + const creator = this.specialRegistry.get(specialCode); + if (!creator) continue; + const special = creator(result); + special.fromLegacyEnemy(enemy); + result.addSpecial(special); + } + } + + return result; + } + + createEnemy(code: number): IEnemy | null { + const prefab = this.prefabByCode.get(code); + if (!prefab) return null; + return prefab.clone(); + } + + createEnemyById(id: string): IEnemy | null { + const prefab = this.prefabById.get(id); + if (!prefab) return null; + return prefab.clone(); + } + + addPrefab(enemy: IEnemy): void { + if ( + this.prefabByCode.has(enemy.code) || + this.prefabById.has(enemy.id) + ) { + return; + } + const cloned = enemy.clone(); + this.prefabByCode.set(enemy.code, cloned); + this.prefabById.set(enemy.id, cloned); + } + + addPrefabFromLegacy(code: number, enemy: Enemy): void { + if (this.prefabByCode.has(code) || this.prefabById.has(enemy.id)) { + return; + } + const prefab = this.convertLegacyEnemy(code, enemy); + this.prefabByCode.set(code, prefab); + this.prefabById.set(prefab.id, prefab); + this.legacyIdToCode.set(enemy.id, code); + } + + getPrefab(code: number): IEnemy | null { + return this.prefabByCode.get(code) ?? null; + } + + getPrefabById(id: string): IEnemy | null { + return this.prefabById.get(id) ?? null; + } + + deletePrefab(code: number | string): void { + const prefab = + typeof code === 'number' + ? this.prefabByCode.get(code) + : this.prefabById.get(code); + if (!prefab) return; + this.prefabByCode.delete(prefab.code); + this.prefabById.delete(prefab.id); + } + + changePrefab(code: number | string, enemy: IEnemy): void { + // 先删除旧的模板(如果存在) + this.deletePrefab(code); + // 再添加新的模板 + this.prefabByCode.set(enemy.code, enemy); + this.prefabById.set(enemy.id, enemy); + } +} diff --git a/packages-user/data-base/src/enemy/mapDamage.ts b/packages-user/data-base/src/enemy/mapDamage.ts new file mode 100644 index 0000000..a38f588 --- /dev/null +++ b/packages-user/data-base/src/enemy/mapDamage.ts @@ -0,0 +1,341 @@ +import { logger } from '@motajs/common'; +import { ITileLocator } from '@user/types'; +import { + IEnemyContext, + IEnemyView, + IMapDamage, + IMapDamageConverter, + IMapDamageInfo, + IMapDamageReducer, + IMapDamageView, + IMapLocIndexer +} from './types'; + +interface IPointInfo { + /** 该点所有的地图伤害 */ + readonly damages: Set>; + /** 所有影响该点的地图伤害视图 */ + readonly affectedBy: Set>; +} + +interface IViewStore { + /** 该地图伤害视图所影响的伤害信息 */ + readonly damages: Map>; + /** 当前视图所属的怪物视图 */ + readonly enemy: IEnemyView; +} + +interface IDamageStore { + /** 该地图伤害信息的地图伤害视图来源 */ + readonly sourceView: IMapDamageView; + /** 地图伤害信息的来源怪物 */ + readonly sourceEnemy: IEnemyView; + /** 该地图伤害信息所处的索引 */ + readonly index: number; +} + +export class MapDamage implements IMapDamage { + /** 当前使用的地图伤害转换器 */ + private converter: IMapDamageConverter | null = null; + /** 当前使用的地图伤害合并器 */ + private reducer: IMapDamageReducer | null = null; + + /** 无来源地图伤害,坐标 -> 点伤害信息 */ + private readonly sourcelessDamage: Map = new Map(); + /** 有来源地图伤害,坐标 -> 点伤害信息 */ + private readonly sourcedDamage: Map = new Map(); + /** 地图伤害视图 -> 其信息对象 */ + private readonly viewStore: Map = new Map(); + /** 地图伤害信息 -> 其信息对象 */ + private readonly damageStore: Map = new Map(); + /** 怪物视图 -> 其影响对象 */ + private readonly enemyStore: Map> = + new Map(); + /** 需要延迟刷新的坐标索引 */ + private readonly dirtyIndexes: Set = new Set(); + /** 合并后伤害缓存,索引 -> 合并结果 */ + private readonly reducedCache: Map = new Map(); + + constructor( + readonly context: IEnemyContext, + readonly indexer: IMapLocIndexer + ) {} + + useConverter(converter: IMapDamageConverter): void { + this.converter = converter; + this.refreshAll(); + } + + useReducer(reducer: IMapDamageReducer): void { + this.reducer = reducer; + this.reducedCache.clear(); + } + + addMapDamage(locator: ITileLocator, info: IMapDamageInfo): void { + const index = this.indexer.locaterToIndex(locator); + const store = this.sourcelessDamage.getOrInsertComputed(index, () => ({ + affectedBy: new Set(), + damages: new Set() + })); + store.damages.add(info); + this.markDirtyIndex(index); + } + + deleteMapDamage(locator: ITileLocator, info: IMapDamageInfo): void { + const index = this.indexer.locaterToIndex(locator); + const current = this.sourcelessDamage.get(index); + if (!current) return; + current.damages.delete(info); + this.markDirtyIndex(index); + } + + /** + * 将指定索引标记为脏 + * @param index 坐标索引 + */ + private markDirtyIndex(index: number) { + this.dirtyIndexes.add(index); + this.reducedCache.delete(index); + } + + markDirty(locator: ITileLocator): void { + this.markDirtyIndex(this.indexer.locaterToIndex(locator)); + } + + markEnemyDirty(view: IEnemyView): void { + const store = this.enemyStore.get(view); + const locator = this.context.getEnemyLocatorByView(view); + if (!store) { + if (!locator) { + logger.warn(104); + } else { + this.refreshAll(); + } + return; + } + if (!locator) return; + this.refreshEnemyAndClearCache(view, locator); + } + + deleteEnemy(view: IEnemyView): void { + const store = this.enemyStore.get(view); + if (!store) return; + const collection = new Set(); + for (const viewItem of store) { + const affecting = this.viewStore.get(viewItem); + if (!affecting) continue; + affecting.damages.forEach((dam, index) => { + this.damageStore.delete(dam); + collection.add(index); + }); + this.viewStore.delete(viewItem); + } + this.enemyStore.delete(view); + collection.forEach(v => { + this.markDirtyIndex(v); + }); + } + + getReducedDamage(locator: ITileLocator): Readonly | null { + if (!this.reducer) { + logger.warn(103); + return null; + } + + const index = this.indexer.locaterToIndex(locator); + if (this.dirtyIndexes.has(index)) { + this.refreshIndex(index); + } + + const cache = this.reducedCache.get(index); + if (cache) return cache; + + const separated = this.getSeparatedDamageByIndex(index); + if (separated.size === 0) return null; + + const reduced = this.reducer.reduce(separated, locator); + this.reducedCache.set(index, reduced); + return reduced; + } + + /** + * 根据索引获取指定位置的合并前伤害 + * @param index 坐标索引 + */ + private getSeparatedDamageByIndex( + index: number + ): Set> { + const sourceless = this.sourcelessDamage.get(index); + const sourced = this.sourcedDamage.get(index); + if (sourceless) { + if (sourced) { + return sourced.damages.union(sourceless.damages); + } else { + return sourceless.damages; + } + } else if (sourced) { + return sourced.damages; + } else { + return new Set(); + } + } + + getSeparatedDamage( + locator: ITileLocator + ): Iterable> { + const index = this.indexer.locaterToIndex(locator); + if (this.dirtyIndexes.has(index)) { + this.refreshIndex(index); + } + return this.getSeparatedDamageByIndex(index); + } + + /** + * 清空所有有来源伤害的内部状态 + */ + private clearSourceState(): void { + this.sourcedDamage.clear(); + this.damageStore.clear(); + this.viewStore.clear(); + this.enemyStore.clear(); + this.dirtyIndexes.clear(); + this.reducedCache.clear(); + } + + /** + * 移除指定怪物所产生的地图伤害 + * @param view 怪物视图 + */ + private removeEnemyAffecting(view: IEnemyView) { + const views = this.enemyStore.get(view); + if (!views) return; + views.forEach(viewItem => { + const store = this.viewStore.get(viewItem); + if (!store) return; + store.damages.forEach((dam, index) => { + const point = this.sourcedDamage.get(index); + if (!point) return; + point.affectedBy.delete(viewItem); + point.damages.delete(dam); + this.damageStore.delete(dam); + }); + this.viewStore.delete(viewItem); + }); + this.enemyStore.delete(view); + } + + /** + * 刷新指定位置的怪物地图伤害,并执行刷新缓存的操作 + */ + private refreshEnemyAndClearCache(view: IEnemyView, locator: ITileLocator) { + this.removeEnemyAffecting(view); + const enemy = view.getComputedEnemy(); + const views = this.converter!.convert(enemy, locator, this.context); + const set = new Set(views); + if (set.size === 0) return; + this.enemyStore.set(view, set); + const collection = new Set(); + set.forEach(viewItem => { + const range = viewItem.getRange(); + const param = viewItem.getRangeParam(); + range.bindHost(this.context); + for (const index of range.iterateLoc(param)) { + const loc = this.indexer.indexToLocator(index); + const point = this.sourcedDamage.getOrInsertComputed( + index, + () => ({ + affectedBy: new Set(), + damages: new Set() + }) + ); + const damage = viewItem.getDamageWithoutCheck(loc); + if (damage) { + point.affectedBy.add(viewItem); + point.damages.add(damage); + collection.add(index); + } + } + }); + collection.forEach(v => { + this.dirtyIndexes.delete(v); + this.reducedCache.delete(v); + }); + } + + /** + * 刷新指定位置的怪物地图伤害 + */ + private refreshEnemy(view: IEnemyView, locator: ITileLocator) { + this.removeEnemyAffecting(view); + const enemy = view.getComputedEnemy(); + const views = this.converter!.convert(enemy, locator, this.context); + const set = new Set(views); + if (set.size === 0) return; + this.enemyStore.set(view, set); + set.forEach(viewItem => { + const range = viewItem.getRange(); + const param = viewItem.getRangeParam(); + range.bindHost(this.context); + for (const index of range.iterateLoc(param)) { + const loc = this.indexer.indexToLocator(index); + const point = this.sourcedDamage.getOrInsertComputed( + index, + () => ({ + affectedBy: new Set(), + damages: new Set() + }) + ); + const damage = viewItem.getDamageWithoutCheck(loc); + if (damage) { + point.affectedBy.add(viewItem); + point.damages.add(damage); + } + } + }); + } + + refreshAll(): void { + if (!this.converter) { + logger.warn(102); + return; + } + if (!this.reducer) { + logger.warn(103); + return; + } + + this.clearSourceState(); + this.reducedCache.clear(); + + for (const [locator, view] of this.context.iterateEnemy()) { + this.refreshEnemy(view, locator); + } + } + + /** + * 重新计算指定点位的有来源伤害缓存 + */ + private refreshIndex(index: number): void { + this.dirtyIndexes.delete(index); + this.reducedCache.delete(index); + + const locator = this.indexer.indexToLocator(index); + const point = this.sourcedDamage.get(index); + if (!point) return; + + for (const damage of point.damages) { + const store = this.damageStore.get(damage); + if (!store) continue; + const viewStore = this.viewStore.get(store.sourceView); + if (!viewStore) continue; + viewStore.damages.delete(index); + this.damageStore.delete(damage); + } + point.damages.clear(); + + point.affectedBy.forEach(view => { + const damage = view.getDamageWithoutCheck(locator); + if (damage) point.damages.add(damage); + }); + } +} diff --git a/packages-user/data-base/src/enemy/special.ts b/packages-user/data-base/src/enemy/special.ts new file mode 100644 index 0000000..87cd8ed --- /dev/null +++ b/packages-user/data-base/src/enemy/special.ts @@ -0,0 +1,95 @@ +import { ISpecial, SpecialCreation } from './types'; + +export interface ICommonSerializableSpecialConfig { + /** 获取特殊属性的名称 */ + getSpecialName: (special: ISpecial) => string; + /** 获取特殊属性的描述 */ + getDescription: (special: ISpecial) => string; + /** 从旧样板怪物对象获取此特殊属性对应的属性值 */ + fromLegacyEnemy: (enemy: Enemy) => T; +} + +export class CommonSerializableSpecial implements ISpecial { + constructor( + readonly code: number, + public value: T, + readonly config: ICommonSerializableSpecialConfig + ) {} + + setValue(value: T): void { + this.value = value; + } + + getValue(): T { + return this.value; + } + + getSpecialName(): string { + return this.config.getSpecialName(this); + } + + getDescription(): string { + return this.config.getDescription(this); + } + + fromLegacyEnemy(enemy: Enemy): void { + this.value = this.config.fromLegacyEnemy(enemy); + } + + clone(): ISpecial { + return new CommonSerializableSpecial( + this.code, + structuredClone(this.value), + this.config + ); + } +} + +export class NonePropertySpecial implements ISpecial { + value: void = undefined; + + constructor( + readonly code: number, + readonly config: ICommonSerializableSpecialConfig + ) {} + + setValue(_value: void): void { + // unneeded + } + + getValue(): void { + return void 0; + } + + getSpecialName(): string { + return this.config.getSpecialName(this); + } + + getDescription(): string { + return this.config.getDescription(this); + } + + fromLegacyEnemy(_enemy: Enemy): void { + // unneeded + } + + clone(): ISpecial { + return new NonePropertySpecial(this.code, this.config); + } +} + +export function defineCommonSerializableSpecial( + code: number, + value: T, + config: ICommonSerializableSpecialConfig +): SpecialCreation { + return () => + new CommonSerializableSpecial(code, structuredClone(value), config); +} + +export function defineNonePropertySpecial( + code: number, + config: ICommonSerializableSpecialConfig +): SpecialCreation { + return () => new NonePropertySpecial(code, config); +} diff --git a/packages-user/data-base/src/enemy/types.ts b/packages-user/data-base/src/enemy/types.ts new file mode 100644 index 0000000..aa48cda --- /dev/null +++ b/packages-user/data-base/src/enemy/types.ts @@ -0,0 +1,739 @@ +import { IRange } from '@motajs/common'; +import { ITileLocator } from '@user/types'; + +export interface IEnemyAttributes { + /** 怪物生命值 */ + hp: number; + /** 怪物攻击力 */ + atk: number; + /** 怪物防御力 */ + def: number; + /** 怪物金币 */ + money: number; + /** 怪物经验值 */ + exp: number; + /** 怪物加点量 */ + point: number; +} + +export interface ISpecial { + /** 特殊属性代码 */ + readonly code: number; + /** 特殊属性需要的数值 */ + readonly value: T; + + /** + * 设置特殊属性数值 + * @param value 特殊属性数值 + */ + setValue(value: T): void; + + /** + * 获取特殊属性数值 + */ + getValue(): T; + + /** + * 获取此特殊属性的名称 + */ + getSpecialName(): string; + + /** + * 获取此特殊属性的描述 + */ + getDescription(): string; + + /** + * 从旧样板的怪物对象中导入此特殊属性 + * @param enemy 旧样板怪物对象 + */ + fromLegacyEnemy(enemy: Enemy): void; + + /** + * 深拷贝此特殊属性 + */ + clone(): ISpecial; +} + +export interface IReadonlyEnemy { + /** 怪物标识符 */ + readonly id: string; + /** 怪物在地图上的标识数字 */ + readonly code: number; + + /** + * 根据特殊属性代码获取对应的对象 + * @param code 特殊属性代码 + */ + getSpecial(code: number): ISpecial | null; + + /** + * 判断怪物是否拥有指定属性 + * @param code 特殊属性代码 + */ + hasSpecial(code: number): boolean; + + /** + * 迭代此怪物所包含的所有特殊属性 + */ + iterateSpecials(): Iterable>; + + /** + * 获取怪物属性值 + * @param key 属性名称 + */ + getAttribute(key: K): IEnemyAttributes[K]; + + /** + * 深拷贝此怪物对象 + */ + clone(): IReadonlyEnemy; +} + +export interface IEnemy extends IReadonlyEnemy { + /** 怪物标识符 */ + readonly id: string; + /** 怪物在地图上的标识数字 */ + readonly code: number; + /** 怪物属性值 */ + readonly attributes: Readonly; + /** 怪物拥有的特殊属性列表 */ + readonly specials: Set>; + + /** + * 添加特殊属性 + * @param special 特殊属性对象 + */ + addSpecial(special: ISpecial): void; + + /** + * 删除指定的特殊属性 + * @param special 特殊属性代码或对象 + */ + deleteSpecial(special: number | ISpecial): void; + + /** + * 设置怪物属性值 + * @param key 属性名称 + * @param value 新的属性值 + */ + setAttribute( + key: K, + value: IEnemyAttributes[K] + ): void; + + /** + * 深拷贝此怪物对象 + */ + clone(): IEnemy; + + /** + * 从一个怪物对象中将属性复制到当前对象 + * @param enemy 怪物对象 + */ + copy(enemy: IReadonlyEnemy): void; +} + +export type SpecialCreation = (enemy: IEnemy) => ISpecial; + +export interface IEnemyManager { + /** + * 注册一个特殊属性 + * @param code 特殊属性代码 + * @param cons 特殊属性创建函数 + */ + registerSpecial(code: number, cons: SpecialCreation): void; + + /** + * 注册一个怪物属性 + * @param name 属性名称 + * @param defaultValue 属性默认值 + */ + registerAttribute(name: string, defaultValue: any): void; + + /** + * 根据旧样板怪物对象生成一个新的怪物对象 + * @param enemy 旧样板怪物对象 + */ + fromLegacyEnemy(enemy: Enemy): IEnemy; + + /** + * 创建怪物对象,如果对应数字的怪物不存在则会返回 `null` + * @param code 怪物图块数字 + */ + createEnemy(code: number): IEnemy | null; + + /** + * 根据怪物的 `id` 创建怪物对象,如果对应的怪物不存在则会返回 `null` + * @param id 怪物 `id` + */ + createEnemyById(id: string): IEnemy | null; + + /** + * 添加怪物模板,如果 `id` 或 `code` 与已有的冲突,则不会做任何操作, + * 如果需要修改怪物模板,请使用 {@link changePrefab} + * @param enemy 怪物对象 + */ + addPrefab(enemy: IEnemy): void; + + /** + * 从旧样板的怪物对象中添加怪物模板 + * @param code 怪物对象对应的图块数字 + * @param enemy 旧样板怪物对象 + */ + addPrefabFromLegacy(code: number, enemy: Enemy): void; + + /** + * 获取指定怪物的模板 + * @param code 怪物图块数字 + */ + getPrefab(code: number): IEnemy | null; + + /** + * 根据怪物的 `id` 获取对应的怪物模板 + * @param id 怪物 `id` + */ + getPrefabById(id: string): IEnemy | null; + + /** + * 删除指定的怪物模板 + * @param code 怪物的图块数字或 `id` + */ + deletePrefab(code: number | string): void; + + /** + * 修改一个已有的怪物模板,如果不存在则会新增 + * @param code 怪物的图块数字或 `id` + * @param enemy 新的怪物模板 + */ + changePrefab(code: number | string, enemy: IEnemy): void; +} + +//#region 辅助接口 + +export interface IMapLocHelper { + /** + * 坐标 -> 索引 + * @param x 横坐标 + * @param y 纵坐标 + */ + locToIndex(x: number, y: number): number; + + /** + * 定位符 -> 索引 + * @param locator 定位符 + */ + locaterToIndex(locator: ITileLocator): number; + + /** + * 索引 -> 定位符 + * @param index 索引 + */ + indexToLocator(index: number): ITileLocator; +} + +export interface IMapLocIndexer extends IMapLocHelper { + /** + * 设置地图宽度 + * @param width 地图宽度 + */ + setWidth(width: number): void; +} + +//#endregion + +//#region 怪物对象 + +export interface IEnemyView { + /** 怪物视图所属的上下文 */ + readonly context: IEnemyContext; + + /** + * 重置此怪物视图的状态,将计算后怪物对象恢复至初始状态 + */ + reset(): void; + + /** + * 获取基本怪物对象 + */ + getBaseEnemy(): IReadonlyEnemy; + + /** + * 获取计算后的怪物对象,返回的怪物对象同引用 + */ + getComputedEnemy(): IReadonlyEnemy; + + /** + * 获取可修改的怪物对象。如果修改此方法获取的怪物对象,那么怪物的真实信息是不会刷新的, + * 需要手动调用 markDirty 方法来刷新。 + */ + getModifiableEnemy(): IEnemy; + + /** + * 将此怪物标记为脏,需要更新 + */ + markDirty(): void; +} + +//#endregion + +//#region 光环与查询 + +export interface IEnemySpecialModifier { + /** + * 获取要添加到指定怪物身上的特殊属性 + * @param enemy 怪物对象 + * @param locator 怪物定位符 + */ + add(enemy: IReadonlyEnemy, locator: ITileLocator): ISpecial[]; + + /** + * 获取制定怪物身上要删除的特殊属性 + * @param enemy 怪物对象 + * @param locator 怪物定位符 + */ + delete(enemy: IReadonlyEnemy, locator: ITileLocator): ISpecial[]; + + /** + * 修改一个怪物的特殊属性,如果真正进行了修改则返回 true,否则返回 false + * @param enemy 怪物对象 + * @param special 要修改的怪物特殊属性 + * @param locator 怪物定位符 + */ + modify( + enemy: IReadonlyEnemy, + special: ISpecial, + locator: ITileLocator + ): boolean; +} + +export interface IAuraView { + /** 此光环视图的优先级 */ + readonly priority: number; + /** 此光环视图的影响范围 */ + readonly range: IRange; + + /** 这个光环视图是否有可能修改怪物的基本属性 */ + readonly couldApplyBase: boolean; + /** 这个光环视图是否有可能修改怪物的特殊属性 */ + readonly couldApplySpecial: boolean; + + /** + * 获取范围扫描参数 + */ + getRangeParam(): T; + + /** + * 对指定怪物对象施加修饰器 + * @param enemy 怪物对象 + * @param locator 怪物定位符 + */ + apply( + enemy: IEnemy, + baseEnemy: IReadonlyEnemy, + locator: ITileLocator + ): void; + + /** + * 对指定怪物对象添加特殊属性修饰器 + * @param enemy 怪物对象 + * @param locator 怪物定位符 + */ + applySpecial( + enemy: IReadonlyEnemy, + baseEnemy: IReadonlyEnemy, + locator: ITileLocator + ): IEnemySpecialModifier | null; +} + +export interface IEnemyAuraView extends IAuraView { + /** 此光环视图所属的怪物 */ + readonly enemy: IReadonlyEnemy; + /** 此光环视图所属的特殊属性 */ + readonly special: ISpecial; + /** 此光环视图所属怪物的定位符 */ + readonly locator: ITileLocator; +} + +export interface IAuraConverter { + /** + * 判断一个特殊属性是否应该被当前光环转换器执行转换 + */ + shouldConvert( + special: ISpecial, + enemy: IReadonlyEnemy, + locator: ITileLocator + ): boolean; + + /** + * 将一个特殊属性转换为光环视图 + */ + convert( + special: ISpecial, + enemy: IReadonlyEnemy, + locator: ITileLocator + ): IEnemyAuraView; +} + +export interface IEnemySpecialQueryModifier extends IEnemySpecialModifier { + /** + * 判断一个怪物是否应该查询外部状态 + */ + shouldQuery(enemy: IReadonlyEnemy, locator: ITileLocator): boolean; +} + +export interface IEnemySpecialQueryEffect { + /** 效果优先级,与光环属性共用 */ + readonly priority: number; + + /** + * 根据传入的怪物上下文,获取对应的怪物特殊属性修饰器 + */ + for(ctx: IEnemyContext): IEnemySpecialQueryModifier; +} + +export interface IEnemyCommonQueryEffect { + /** 优先级,越高的越先执行 */ + readonly priority: number; + + /** + * 对怪物的某个特殊属性施加常规查询效果 + */ + apply( + enemy: IEnemy, + special: ISpecial, + query: () => IEnemyContext, + locator: ITileLocator + ): void; +} + +export interface IEnemyFinalEffect { + /** 效果优先级,越高会越先被执行 */ + readonly priority: number; + + /** + * 向怪物施加最终修饰效果 + */ + apply(enemy: IEnemy, locator: ITileLocator): void; +} + +//#endregion + +//#region 地图伤害 + +export interface IMapDamageInfoExtra { + /** 捕捉怪物信息 */ + catch: Set; + /** 阻击怪物信息 */ + repulse: Set; +} + +export interface IMapDamageInfo { + /** 伤害值 */ + damage: number; + /** 伤害类型 */ + type: number; + /** 地图伤害额外信息 */ + extra: IMapDamageInfoExtra; +} + +export interface IMapDamageView { + /** 获取地图伤害影响范围 */ + getRange(): IRange; + + /** 获取范围参数 */ + getRangeParam(): T; + + /** + * 获取指定位置的地图伤害,会对坐标进行判断 + * @param locator 伤害位置 + */ + getDamageAt(locator: ITileLocator): Readonly | null; + + /** + * 获取指定位置的地图伤害,但是不会对坐标进行判断 + * @param locator 伤害位置 + */ + getDamageWithoutCheck( + locator: ITileLocator + ): Readonly | null; +} + +export interface IMapDamageConverter { + /** 转换地图伤害视图 */ + convert( + enemy: IReadonlyEnemy, + locator: ITileLocator, + context: IEnemyContext + ): IMapDamageView[]; +} + +export interface IMapDamageReducer { + /** 对伤害信息进行合并 */ + reduce( + info: Iterable>, + locator: ITileLocator + ): Readonly; +} + +export interface IMapDamage { + /** 当前绑定的怪物上下文 */ + readonly context: IEnemyContext; + + /** + * 设置地图伤害转换器,并基于当前上下文重建所有地图伤害视图 + * @param converter 地图伤害转换器 + */ + useConverter(converter: IMapDamageConverter): void; + + /** + * 设置地图伤害合并器 + * @param reducer 地图伤害合并器 + */ + useReducer(reducer: IMapDamageReducer): void; + + /** + * 在指定位置添加一条无来源地图伤害 + * @param locator 地图定位符 + * @param info 地图伤害信息 + */ + addMapDamage(locator: ITileLocator, info: IMapDamageInfo): void; + + /** + * 在指定位置删除一条无来源地图伤害 + * @param locator 地图定位符 + * @param info 地图伤害信息 + */ + deleteMapDamage(locator: ITileLocator, info: IMapDamageInfo): void; + + /** + * 将指定位置标记为脏,后续访问时会重新计算该点的有来源伤害 + * @param locator 地图定位符 + */ + markDirty(locator: ITileLocator): void; + + /** + * 将指定怪物对应的地图伤害标记为脏并刷新 + * @param view 怪物视图 + */ + markEnemyDirty(view: IEnemyView): void; + + /** + * 基于当前上下文重新刷新全部有来源地图伤害 + */ + refreshAll(): void; + + /** + * 删除指定怪物带来的全部地图伤害来源 + * @param view 怪物视图 + */ + deleteEnemy(view: IEnemyView): void; + + /** + * 获取指定位置合并后的地图伤害 + * @param locator 地图定位符 + */ + getReducedDamage(locator: ITileLocator): Readonly | null; + + /** + * 获取指定位置未合并的地图伤害列表 + * @param locator 地图定位符 + */ + getSeparatedDamage( + locator: ITileLocator + ): Iterable>; +} + +//#endregion + +//#region 上下文 + +export interface IEnemyContext { + /** 怪物上下文宽度 */ + readonly width: number; + /** 怪物上下文高度 */ + readonly height: number; + + /** + * 调整上下文尺寸,并清空当前上下文中的所有怪物与状态 + * @param width 地图宽度 + * @param height 地图高度 + */ + resize(width: number, height: number): void; + + /** + * 注册一个光环转换器 + * @param converter 光环转换器 + */ + registerAuraConverter(converter: IAuraConverter): void; + + /** + * 注销一个光环转换器 + * @param converter 光环转换器 + */ + unregisterAuraConverter(converter: IAuraConverter): void; + + /** + * 设置光环转换器的启用状态 + * @param converter 光环转换器 + * @param enabled 是否启用 + */ + setAuraConverterEnabled(converter: IAuraConverter, enabled: boolean): void; + + /** + * 注册一个特殊属性查询效果 + * @param effect 特殊属性查询效果 + */ + registerSpecialQueryEffect(effect: IEnemySpecialQueryEffect): void; + + /** + * 注销一个特殊属性查询效果 + * @param effect 特殊属性查询效果 + */ + unregisterSpecialQueryEffect(effect: IEnemySpecialQueryEffect): void; + + /** + * 为指定特殊属性代码注册常规查询效果 + * @param code 特殊属性代码 + * @param effect 常规查询效果 + */ + registerCommonQueryEffect( + code: number, + effect: IEnemyCommonQueryEffect + ): void; + + /** + * 注销指定特殊属性代码上的常规查询效果 + * @param code 特殊属性代码 + * @param effect 常规查询效果 + */ + unregisterCommonQueryEffect( + code: number, + effect: IEnemyCommonQueryEffect + ): void; + + /** + * 注册一个最终效果 + * @param effect 最终效果 + */ + registerFinalEffect(effect: IEnemyFinalEffect): void; + + /** + * 注销一个最终效果 + * @param effect 最终效果 + */ + unregisterFinalEffect(effect: IEnemyFinalEffect): void; + + /** + * 获取指定怪物对象当前所在位置 + * @param enemy 怪物对象 + */ + getEnemyLocator(enemy: IEnemy): Readonly | null; + + /** + * 获取指定怪物视图当前所在位置 + * @param view 怪物视图 + */ + getEnemyLocatorByView(view: IEnemyView): Readonly | null; + + /** + * 根据定位符获取怪物视图 + * @param locator 地图定位符 + */ + getEnemyByLocator(locator: ITileLocator): IEnemyView | null; + + /** + * 根据坐标获取怪物视图 + * @param x 横坐标 + * @param y 纵坐标 + */ + getEnemyByLoc(x: number, y: number): IEnemyView | null; + + /** + * 根据计算后怪物对象反查怪物视图 + * @param enemy 计算后怪物对象 + */ + getViewByComputed(enemy: IReadonlyEnemy): IEnemyView | null; + + /** + * 在指定位置放置一个怪物对象 + * @param locator 地图定位符 + * @param enemy 怪物对象 + */ + setEnemyAt(locator: ITileLocator, enemy: IEnemy): void; + + /** + * 删除指定位置的怪物 + * @param locator 地图定位符 + */ + deleteEnemy(locator: ITileLocator): void; + + /** + * 扫描指定范围内的怪物视图 + * @param range 范围对象 + * @param param 范围参数 + */ + scanRange(range: IRange, param: T): Iterable; + + /** + * 迭代上下文中的全部怪物 + */ + iterateEnemy(): Iterable<[ITileLocator, IEnemyView]>; + + /** + * 添加一个全局光环视图 + * @param aura 光环视图 + */ + addAura(aura: IAuraView): void; + + /** + * 删除一个全局光环视图 + * @param aura 光环视图 + */ + deleteAura(aura: IAuraView): void; + + /** + * 绑定地图伤害管理器 + * @param damage 地图伤害管理器 + */ + attachMapDamage(damage: IMapDamage | null): void; + + /** + * 获取当前绑定的地图伤害管理器 + */ + getMapDamage(): IMapDamage | null; + + /** + * 重建当前上下文中的全部怪物计算结果 + * + * 1. 对所有光环及特殊查询进行构建操作,这一步中会决定每个怪物所拥有的特殊属性,后续不会变动 + * 2. 执行所有的普通光环效果,修改怪物的基础属性 + * 3. 执行常规查询效果,允许查询上下文状态并修改怪物自身的基础属性 + * 4. 执行最终效果,不允许查询上下文状态,仅允许修改怪物自身的基础属性 + */ + buildup(): void; + + /** + * 将指定怪物视图标记为脏 + * @param view 怪物视图 + */ + markDirty(view: IEnemyView): void; + + /** + * 申请刷新指定怪物视图 + * @param view 怪物视图 + */ + requestRefresh(view: IEnemyView): void; + + /** + * 清空当前上下文中的所有对象与运行状态 + */ + clear(): void; + + /** + * 销毁当前上下文 + */ + destroy(): void; +} + +//#endregion diff --git a/packages-user/data-base/src/enemy/utils.ts b/packages-user/data-base/src/enemy/utils.ts new file mode 100644 index 0000000..fc8f5c8 --- /dev/null +++ b/packages-user/data-base/src/enemy/utils.ts @@ -0,0 +1,25 @@ +import { ITileLocator } from '@user/types'; +import { IMapLocIndexer } from './types'; + +export class MapLocIndexer implements IMapLocIndexer { + private width: number = 0; + + setWidth(width: number): void { + this.width = width; + } + + locToIndex(x: number, y: number): number { + return y * this.width + x; + } + + locaterToIndex(locator: ITileLocator): number { + return locator.y * this.width + locator.x; + } + + indexToLocator(index: number): ITileLocator { + return { + x: index % this.width, + y: Math.floor(index / this.width) + }; + } +} diff --git a/packages-user/data-base/src/index.ts b/packages-user/data-base/src/index.ts index 3db41fc..c23da5f 100644 --- a/packages-user/data-base/src/index.ts +++ b/packages-user/data-base/src/index.ts @@ -1,3 +1,4 @@ +export * from './enemy'; export * from './load'; export * from './game'; diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 34f1f8d..2a7bc51 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -1,12 +1,14 @@ -import { ICoreState, IStateSaveData } from './types'; +import { ICoreState, IGameDataState, IStateSaveData } from './types'; import { IHeroState, HeroState } from './hero'; import { ILayerState, LayerState } from './map'; import { IRoleFaceBinder, RoleFaceBinder } from './common'; +import { GameDataState } from './data'; export class CoreState implements ICoreState { readonly layer: ILayerState; readonly hero: IHeroState; readonly roleFace: IRoleFaceBinder; + readonly data: IGameDataState; readonly idNumberMap: Map; readonly numberIdMap: Map; @@ -16,6 +18,7 @@ export class CoreState implements ICoreState { this.roleFace = new RoleFaceBinder(); this.idNumberMap = new Map(); this.numberIdMap = new Map(); + this.data = new GameDataState(); } saveState(): IStateSaveData { diff --git a/packages-user/data-state/src/data.ts b/packages-user/data-state/src/data.ts new file mode 100644 index 0000000..64669b0 --- /dev/null +++ b/packages-user/data-state/src/data.ts @@ -0,0 +1,12 @@ +import { EnemyManager, IEnemyManager } from '@user/data-base'; +import { IGameDataState } from './types'; +import { registerSpecials } from './enemy'; + +export class GameDataState implements IGameDataState { + readonly enemyManager: IEnemyManager; + + constructor() { + this.enemyManager = new EnemyManager(); + registerSpecials(this.enemyManager); + } +} diff --git a/packages-user/data-state/src/enemy/aura.ts b/packages-user/data-state/src/enemy/aura.ts new file mode 100644 index 0000000..8835e29 --- /dev/null +++ b/packages-user/data-state/src/enemy/aura.ts @@ -0,0 +1,121 @@ +import { + FullRange, + IManhattanRangeParam, + IRange, + IRectRangeParam, + ManhattanRange, + RectRange +} from '@motajs/common'; +import { + IAuraConverter, + IEnemyAuraView, + IEnemySpecialModifier, + IReadonlyEnemy, + ISpecial, + IEnemy +} from '@user/data-base'; +import { IHaloValue } from './special'; +import { ITileLocator } from '@user/types'; + +const FULL_RANGE = new FullRange(); +const RECT_RANGE = new RectRange(); +const MANHATTAN_RANGE = new ManhattanRange(); + +//#region 25-光环 + +export class CommonAuraConverter implements IAuraConverter { + shouldConvert(special: ISpecial): boolean { + return special.code === 25; + } + + convert( + special: ISpecial, + enemy: IReadonlyEnemy, + locator: ITileLocator + ): IEnemyAuraView { + return new CommonAura(enemy, special as ISpecial, locator); + } +} + +export class CommonAura implements IEnemyAuraView< + IRectRangeParam | IManhattanRangeParam | void, + IHaloValue +> { + readonly priority: number = 25; + readonly couldApplyBase: boolean = true; + readonly couldApplySpecial: boolean = false; + + readonly range: IRange; + + constructor( + readonly enemy: IReadonlyEnemy, + readonly special: ISpecial, + readonly locator: ITileLocator + ) { + this.range = this.createRange(); + } + + private createRange(): IRange< + IRectRangeParam | IManhattanRangeParam | void + > { + const { haloRange, haloSquare } = this.special.value; + if (haloRange <= 0) { + return FULL_RANGE; + } + return haloSquare ? RECT_RANGE : MANHATTAN_RANGE; + } + + getRangeParam(): IRectRangeParam | IManhattanRangeParam | void { + const { haloRange, haloSquare } = this.special.value; + if (haloRange <= 0) { + return undefined; + } + if (haloSquare) { + return { + x: this.locator.x - haloRange, + y: this.locator.y - haloRange, + w: haloRange * 2 + 1, + h: haloRange * 2 + 1 + }; + } + return { + cx: this.locator.x, + cy: this.locator.y, + radius: haloRange + }; + } + + apply(enemy: IEnemy, baseEnemy: IReadonlyEnemy): void { + const { hpBuff, atkBuff, defBuff } = this.special.value; + + if (hpBuff !== 0) { + enemy.setAttribute( + 'hp', + enemy.getAttribute('hp') + + Math.floor((baseEnemy.getAttribute('hp') * hpBuff) / 100) + ); + } + + if (atkBuff !== 0) { + enemy.setAttribute( + 'atk', + enemy.getAttribute('atk') + + Math.floor((baseEnemy.getAttribute('atk') * atkBuff) / 100) + ); + } + + if (defBuff !== 0) { + enemy.setAttribute( + 'def', + enemy.getAttribute('def') + + Math.floor((baseEnemy.getAttribute('def') * defBuff) / 100) + ); + } + } + + applySpecial(): IEnemySpecialModifier | null { + return null; + } +} + +//#endregion diff --git a/packages-user/data-state/src/enemy/index.ts b/packages-user/data-state/src/enemy/index.ts index b34e38d..2e7ac2f 100644 --- a/packages-user/data-state/src/enemy/index.ts +++ b/packages-user/data-state/src/enemy/index.ts @@ -1,2 +1,4 @@ +export * from './aura'; export * from './damage'; +export * from './mapSpecials'; export * from './special'; diff --git a/packages-user/data-state/src/enemy/mapSpecials.ts b/packages-user/data-state/src/enemy/mapSpecials.ts new file mode 100644 index 0000000..100355c --- /dev/null +++ b/packages-user/data-state/src/enemy/mapSpecials.ts @@ -0,0 +1,358 @@ +import { + DirectionMapper, + IDirectionDescriptor, + InternalDirectionGroup, + IManhattanRangeParam, + IRange, + IRayRangeParam, + IRectRangeParam, + ManhattanRange, + RayRange, + RectRange +} from '@motajs/common'; +import { ITileLocator } from '@user/types'; +import { IReadonlyEnemy, ISpecial } from '@user/data-base'; +import { + IEnemyContext, + IMapDamageConverter, + IMapDamageInfo, + IMapDamageInfoExtra, + IMapDamageReducer, + IMapDamageView +} from '@user/data-base'; +import { IZoneValue } from './special'; +import { MapDamageType } from './types'; + +const RECT_RANGE = new RectRange(); +const MANHATTAN_RANGE = new ManhattanRange(); +const RAY_RANGE = new RayRange(); + +const DIRECTION_MAPPER = new DirectionMapper(); +const DIR4 = [...DIRECTION_MAPPER.map(InternalDirectionGroup.Dir4)]; + +//#region 地图伤害 + +abstract class BaseMapDamageView implements IMapDamageView { + constructor(protected readonly context: IEnemyContext) {} + + abstract getRange(): IRange; + + abstract getRangeParam(): T; + + getDamageAt(locator: ITileLocator): Readonly | null { + const range = this.getRange(); + const param = this.getRangeParam(); + range.bindHost(this.context); + if (!range.inRange(locator.x, locator.y, param)) { + return null; + } + + return this.getDamageWithoutCheck(locator); + } + + abstract getDamageWithoutCheck( + locator: ITileLocator + ): Readonly | null; + + /** + * 创建伤害信息 + * @param damage 伤害值 + * @param type 伤害类型 + * @param extra 额外信息 + */ + protected createInfo( + damage: number, + type: number, + extra?: Partial + ): IMapDamageInfo { + return { + damage, + type, + extra: { + catch: extra?.catch ?? new Set(), + repulse: extra?.repulse ?? new Set() + } + }; + } + + /** + * 判断一个点是否在上下文范围内 + * @param x 横坐标 + * @param y 纵坐标 + */ + protected isInBounds(x: number, y: number): boolean { + return ( + x >= 0 && + y >= 0 && + x < this.context.width && + y < this.context.height + ); + } +} + +export class ZoneDamageView extends BaseMapDamageView< + IRectRangeParam | IManhattanRangeParam +> { + constructor( + context: IEnemyContext, + private readonly locator: Readonly, + private readonly special: Readonly> + ) { + super(context); + } + + getRange(): IRange { + return this.special.value.zoneSquare ? RECT_RANGE : MANHATTAN_RANGE; + } + + getRangeParam(): IRectRangeParam | IManhattanRangeParam { + if (this.special.value.zoneSquare) { + return { + h: this.special.value.range * 2 + 1, + w: this.special.value.range * 2 + 1, + x: this.locator.x - this.special.value.range, + y: this.locator.y - this.special.value.range + }; + } + + return { + cx: this.locator.x, + cy: this.locator.y, + radius: this.special.value.range + }; + } + + getDamageWithoutCheck( + _locator: ITileLocator + ): Readonly | null { + return this.createInfo(this.special.value.zone, MapDamageType.Zone); + } +} + +export class RepulseDamageView extends BaseMapDamageView { + constructor( + context: IEnemyContext, + private readonly locator: Readonly, + private readonly special: Readonly> + ) { + super(context); + } + + getRange(): IRange { + return MANHATTAN_RANGE; + } + + getRangeParam(): IManhattanRangeParam { + return { + cx: this.locator.x, + cy: this.locator.y, + radius: 1 + }; + } + + getDamageWithoutCheck( + locator: ITileLocator + ): Readonly | null { + if (locator.x === this.locator.x && locator.y === this.locator.y) { + return null; + } + + return this.createInfo(this.special.value, MapDamageType.Repulse, { + repulse: new Set([this.locator]) + }); + } +} + +export class LaserDamageView extends BaseMapDamageView { + constructor( + context: IEnemyContext, + private readonly locator: Readonly, + private readonly special: Readonly>, + private readonly dir: IDirectionDescriptor[] = DIR4 + ) { + super(context); + } + + getRange(): IRange { + return RAY_RANGE; + } + + getRangeParam(): IRayRangeParam { + return { + cx: this.locator.x, + cy: this.locator.y, + dir: this.dir + }; + } + + getDamageWithoutCheck( + locator: ITileLocator + ): Readonly | null { + if (locator.x === this.locator.x && locator.y === this.locator.y) { + return null; + } + + return this.createInfo(this.special.value, MapDamageType.Layer); + } +} + +export class BetweenDamageView extends BaseMapDamageView { + private static readonly DAMAGE = 1; + + constructor( + context: IEnemyContext, + private readonly locator: Readonly + ) { + super(context); + } + + getRange(): IRange { + return MANHATTAN_RANGE; + } + + getRangeParam(): IManhattanRangeParam { + return { + cx: this.locator.x, + cy: this.locator.y, + radius: 1 + }; + } + + getDamageWithoutCheck( + locator: ITileLocator + ): Readonly | null { + const deltaX = locator.x - this.locator.x; + const deltaY = locator.y - this.locator.y; + if (Math.abs(deltaX) + Math.abs(deltaY) !== 1) { + return null; + } + if (deltaX <= 0 && deltaY <= 0) { + return null; + } + + const otherX = locator.x + deltaX; + const otherY = locator.y + deltaY; + if (!this.isInBounds(otherX, otherY)) { + return null; + } + + const other = this.context.getEnemyByLoc(otherX, otherY); + if (!other) { + return null; + } + if (!other.getComputedEnemy().hasSpecial(16)) { + return null; + } + + return this.createInfo(BetweenDamageView.DAMAGE, MapDamageType.Between); + } +} + +export class AmbushDamageView extends BaseMapDamageView { + constructor( + context: IEnemyContext, + private readonly locator: Readonly + ) { + super(context); + } + + getRange(): IRange { + return MANHATTAN_RANGE; + } + + getRangeParam(): IManhattanRangeParam { + return { + cx: this.locator.x, + cy: this.locator.y, + radius: 1 + }; + } + + getDamageWithoutCheck( + locator: ITileLocator + ): Readonly | null { + if (locator.x === this.locator.x && locator.y === this.locator.y) { + return null; + } + + return this.createInfo(0, MapDamageType.Unknown, { + catch: new Set([this.locator]) + }); + } +} + +//#endregion + +//#region 转换器 + +export class MainMapDamageConverter implements IMapDamageConverter { + convert( + enemy: IReadonlyEnemy, + locator: ITileLocator, + context: IEnemyContext + ): IMapDamageView[] { + const views: IMapDamageView[] = []; + + const zone = enemy.getSpecial(15); + if (zone) { + views.push(new ZoneDamageView(context, locator, zone)); + } + + if (enemy.hasSpecial(16)) { + views.push(new BetweenDamageView(context, locator)); + } + + const repulse = enemy.getSpecial(18); + if (repulse) { + views.push(new RepulseDamageView(context, locator, repulse)); + } + + const laser = enemy.getSpecial(24); + if (laser) { + views.push(new LaserDamageView(context, locator, laser)); + } + + if (enemy.hasSpecial(27)) { + views.push(new AmbushDamageView(context, locator)); + } + + return views; + } +} + +//#endregion + +//#region 合并器 + +export class MainMapDamageReducer implements IMapDamageReducer { + reduce( + info: Iterable>, + _locator: ITileLocator + ): Readonly { + let damage = 0; + let type = MapDamageType.Unknown; + let maxDamage = -Infinity; + const extra = { + catch: new Set(), + repulse: new Set() + }; + + for (const item of info) { + damage += item.damage; + if (item.damage > maxDamage) { + maxDamage = item.damage; + type = item.type; + } + item.extra.catch.forEach(v => extra.catch.add(v)); + item.extra.repulse.forEach(v => extra.repulse.add(v)); + } + + return { + damage, + extra, + type + }; + } +} + +//#endregion diff --git a/packages-user/data-state/src/enemy/special.ts b/packages-user/data-state/src/enemy/special.ts index 9ddf428..6690058 100644 --- a/packages-user/data-state/src/enemy/special.ts +++ b/packages-user/data-state/src/enemy/special.ts @@ -1,243 +1,425 @@ -import { EnemyInfo } from '@motajs/types'; +import { + defineCommonSerializableSpecial, + defineNonePropertySpecial, + IEnemyManager +} from '@user/data-base'; import { getHeroStatusOn } from '../legacy/hero'; -export interface SpecialDeclaration { - code: number; - name: string | ((enemy: EnemyInfo) => string); - desc: string | ((enemy: EnemyInfo) => string); - color: string; +//#region 复合属性值类型 + +export interface IVampireValue { + vampire: number; + add: boolean; } +export interface IZoneValue { + zone: number; + zoneSquare: boolean; + range: number; +} + +export interface IDegradationValue { + atkValue: number; + defValue: number; +} + +export interface IHaloValue { + haloRange: number; + haloSquare: boolean; + hpBuff: number; + atkBuff: number; + defBuff: number; +} + +//#endregion + /** - * 怪物特殊属性列表,当前版本中 code 最好与索引保持一致,不然可能会出现问题 + * 注册所有怪物特殊属性到 enemyManager + * * 属性实现位置一览('./'表示当前文件夹 '../'表示上一级文件夹): - * 1. 调参类属性 / 仅影响战斗过程的属性:./damage.ts calDamageWithTurn 函数 - * 2. 地图伤害:./damage.ts DamageEnemy.calMapDamage 方法,搜索 calMapDamage 即可搜到 - * 3. 光环属性:./damage.ts DamageEnemy.provideHalo 方法,搜索 provideHalo 即可搜到 - * 4. 仇恨 / 退化 等战后效果:packages-user/data-fallback/src/battle.ts 中的 afterBattle - * 5. 中毒的每步效果:../state/move.ts HeroMover.onStepEnd 方法,在约 590 行 + * 1. 调参类属性 | 仅影响战斗过程的属性:./damage.ts calDamageWithTurn 函数 + * 2. 地图伤害:./damage.ts DamageEnemy.calMapDamage 方法 + * 3. 光环属性:./damage.ts DamageEnemy.provideHalo 方法 + * 4. 仇恨 | 退化 等战后效果:packages-user/data-fallback/src/battle.ts 中的 afterBattle + * 5. 中毒的每步效果:../state/move.ts HeroMover.onStepEnd 方法 * 6. 中毒的瞬移效果:还在脚本编辑的 moveDirectly * 7. 衰弱效果:../state/hero.ts getHeroStatusOf 方法 * 8. 重生属性:还在脚本编辑的 changingFloor - * 9. 阻击 / 捕捉 的每步效果:packages-user/legacy-plugin-data/src/enemy/checkblock.ts + * 9. 阻击 | 捕捉 的每步效果:packages-user/legacy-plugin-data/src/enemy/checkblock.ts */ -export const specials: SpecialDeclaration[] = [ - { - code: 0, - name: '空', - desc: '空', - color: '#fff' - }, - { - code: 1, - name: '先攻', - desc: `怪物首先攻击。`, - color: '#fc3' - }, - { - code: 2, - name: '魔攻', - desc: '怪物攻击无视勇士的防御。', - color: '#bbb0ff' - }, - { - code: 3, - name: '坚固', - desc: '怪物防御不小于勇士攻击-1。', - color: '#c0b088' - }, - { - code: 4, - name: '2连击', - desc: '怪物每回合攻击2次。', - color: '#fe7' - }, - { - code: 5, - name: '3连击', - desc: '怪物每回合攻击3次。', - color: '#fe7' - }, - { - code: 6, - name: enemy => `${enemy.n ?? 4}连击`, - desc: enemy => `怪物每回合攻击${enemy.n ?? 4}次。`, - color: '#fe7' - }, - { - code: 7, - name: '破甲', - desc: enemy => - `战斗前,附加角色防御的${enemy.breakArmor ?? core.values.breakArmor}%作为伤害。`, - color: '#fe7' - }, - { - code: 8, - name: '反击', - desc: enemy => - `战斗时,怪物每回合附加角色攻击的${enemy.counterAttack ?? core.values.counterAttack}%作为伤害,无视角色防御。`, - color: '#fa4' - }, - { - code: 9, - name: '净化', - desc: enemy => - `战斗前,怪物附加角色护盾的${enemy.purify ?? core.values.purify}倍作为伤害。`, - color: '#80eed6' - }, - { - code: 10, - name: '模仿', - desc: `怪物的攻防与勇士相同。`, - color: '#b0c0dd' - }, - { - code: 11, - name: '吸血', - desc: enemy => { - const vampire = enemy.vampire ?? 0; - return ( - `战斗前,怪物首先吸取角色的${vampire}%生命` + - `(约${Math.floor((vampire / 100) * getHeroStatusOn('hp'))}点)作为伤害` + - (enemy.add ? `,并把伤害数值加到自身生命上。` : `。`) - ); - }, - color: '#ff00d2' - }, - { - code: 12, - name: '中毒', - desc: () => - `战斗后,角色陷入中毒状态,每一步损失生命${core.values.poisonDamage}点。`, - color: '#9e8' - }, - { - code: 13, - name: '衰弱', - desc: () => { - const weak = core.values.weakValue; - if (weak < 1) { - return `战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%`; - } else { - return `战斗后,角色陷入衰弱状态,攻防暂时下降${weak}点`; +export function registerSpecials(manager: IEnemyManager): void { + // 0 - 空 + manager.registerSpecial( + 0, + defineNonePropertySpecial(0, { + getSpecialName: () => '空', + getDescription: () => '空', + fromLegacyEnemy: () => {} + }) + ); + + // 1 - 先攻 + manager.registerSpecial( + 1, + defineNonePropertySpecial(1, { + getSpecialName: () => '先攻', + getDescription: () => '怪物首先攻击。', + fromLegacyEnemy: () => {} + }) + ); + + // 2 - 魔攻 + manager.registerSpecial( + 2, + defineNonePropertySpecial(2, { + getSpecialName: () => '魔攻', + getDescription: () => '怪物攻击无视勇士的防御。', + fromLegacyEnemy: () => {} + }) + ); + + // 3 - 坚固 + manager.registerSpecial( + 3, + defineNonePropertySpecial(3, { + getSpecialName: () => '坚固', + getDescription: () => '怪物防御不小于勇士攻击-1。', + fromLegacyEnemy: () => {} + }) + ); + + // 4 - 2连击 + manager.registerSpecial( + 4, + defineNonePropertySpecial(4, { + getSpecialName: () => '2连击', + getDescription: () => '怪物每回合攻击2次。', + fromLegacyEnemy: () => {} + }) + ); + + // 5 - 3连击 + manager.registerSpecial( + 5, + defineNonePropertySpecial(5, { + getSpecialName: () => '3连击', + getDescription: () => '怪物每回合攻击3次。', + fromLegacyEnemy: () => {} + }) + ); + + // 6 - n连击 + manager.registerSpecial( + 6, + defineCommonSerializableSpecial(6, 4, { + getSpecialName: special => `${special.value}连击`, + getDescription: special => `怪物每回合攻击${special.value}次。`, + fromLegacyEnemy: enemy => enemy.n ?? 4 + }) + ); + + // 7 - 破甲 + manager.registerSpecial( + 7, + defineCommonSerializableSpecial(7, 0, { + getSpecialName: () => '破甲', + getDescription: special => + `战斗前,附加角色防御的${special.value || core.values.breakArmor}%作为伤害。`, + fromLegacyEnemy: enemy => enemy.breakArmor ?? 0 + }) + ); + + // 8 - 反击 + manager.registerSpecial( + 8, + defineCommonSerializableSpecial(8, 0, { + getSpecialName: () => '反击', + getDescription: special => + `战斗时,怪物每回合附加角色攻击的${special.value || core.values.counterAttack}%作为伤害,无视角色防御。`, + fromLegacyEnemy: enemy => enemy.counterAttack ?? 0 + }) + ); + + // 9 - 净化 + manager.registerSpecial( + 9, + defineCommonSerializableSpecial(9, 0, { + getSpecialName: () => '净化', + getDescription: special => + `战斗前,怪物附加角色护盾的${special.value || core.values.purify}倍作为伤害。`, + fromLegacyEnemy: enemy => enemy.purify ?? 0 + }) + ); + + // 10 - 模仿 + manager.registerSpecial( + 10, + defineNonePropertySpecial(10, { + getSpecialName: () => '模仿', + getDescription: () => '怪物的攻防与勇士相同。', + fromLegacyEnemy: () => {} + }) + ); + + // 11 - 吸血 + manager.registerSpecial( + 11, + defineCommonSerializableSpecial( + 11, + { vampire: 0, add: false }, + { + getSpecialName: () => '吸血', + getDescription: special => { + const { vampire, add } = special.value; + return ( + `战斗前,怪物首先吸取角色的${vampire}%生命` + + `(约${Math.floor((vampire / 100) * getHeroStatusOn('hp'))}点)作为伤害` + + (add ? `,并把伤害数值加到自身生命上。` : `。`) + ); + }, + fromLegacyEnemy: enemy => ({ + vampire: enemy.vampire ?? 0, + add: enemy.add ?? false + }) } - }, - color: '#f0bbcc' - }, - { - code: 14, - name: '诅咒', - desc: '战斗后,角色陷入诅咒状态,战斗无法获得金币和经验。', - color: '#bbeef0' - }, - { - code: 15, - name: '领域', - desc: enemy => - `经过怪物周围${enemy.zoneSquare ? '九宫格' : '十字'}范围内${enemy.range ?? 1}格时自动减生命${enemy.zone ?? 0}点。`, - color: '#c677dd' - }, - { - code: 16, - name: '夹击', - desc: '经过两只相同的怪物中间,角色生命值变成一半。', - color: '#b9e' - }, - { - code: 17, - name: '仇恨', - desc: () => - `战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得${core.values.hatred}点仇恨值)。`, - color: '#b0b666' - }, - { - code: 18, - name: '阻击', - desc: enemy => - `经过怪物十字范围内时怪物后退一格,同时对勇士造成${enemy.repulse ?? 0}点伤害。`, - color: '#8888e6' - }, - { - code: 19, - name: '自爆', - desc: '战斗后角色的生命值变成1。', - color: '#ff6666' - }, - { - code: 20, - name: '无敌', - desc: `角色无法打败怪物,除非拥有十字架。`, - color: '#aaa' - }, - { - code: 21, - name: '退化', - desc: enemy => - `战斗后角色永久下降${enemy.atkValue ?? 0}点攻击和${enemy.defValue ?? 0}点防御。`, - color: 'cyan' - }, - { - code: 22, - name: '固伤', - desc: enemy => - `战斗前,怪物对角色造成${enemy.damage ?? 0}点固定伤害,未开启负伤时无视角色护盾。`, - color: '#f97' - }, - { - code: 23, - name: '重生', - desc: `怪物被击败后,角色转换楼层则怪物将再次出现。`, - color: '#dda0dd' - }, - { - code: 24, - name: '激光', - desc: enemy => `经过怪物同行或同列时自动减生命${enemy.laser ?? 0}点。`, - color: '#dda0dd' - }, - { - code: 25, - name: '光环', - desc: enemy => { - let str = ''; - if (enemy.haloRange) { - if (enemy.haloSquare) { - str += '对于该怪物九宫格'; + ) + ); + + // 12 - 中毒 + manager.registerSpecial( + 12, + defineNonePropertySpecial(12, { + getSpecialName: () => '中毒', + getDescription: () => + `战斗后,角色陷入中毒状态,每一步损失生命${core.values.poisonDamage}点。`, + fromLegacyEnemy: () => {} + }) + ); + + // 13 - 衰弱 + manager.registerSpecial( + 13, + defineNonePropertySpecial(13, { + getSpecialName: () => '衰弱', + getDescription: () => { + const weak = core.values.weakValue; + if (weak < 1) { + return `战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%`; } else { - str += '对于该怪物十字'; + return `战斗后,角色陷入衰弱状态,攻防暂时下降${weak}点`; } - str += `${enemy.haloRange ?? 1}格范围内所有怪物`; - } else { - str += `同楼层所有怪物`; + }, + fromLegacyEnemy: () => {} + }) + ); + + // 14 - 诅咒 + manager.registerSpecial( + 14, + defineNonePropertySpecial(14, { + getSpecialName: () => '诅咒', + getDescription: () => + '战斗后,角色陷入诅咒状态,战斗无法获得金币和经验。', + fromLegacyEnemy: () => {} + }) + ); + + // 15 - 领域 + manager.registerSpecial( + 15, + defineCommonSerializableSpecial( + 15, + { zone: 0, zoneSquare: false, range: 1 }, + { + getSpecialName: () => '领域', + getDescription: special => { + const { zone, zoneSquare, range } = special.value; + return `经过怪物周围${zoneSquare ? '九宫格' : '十字'}范围内${range}格时自动减生命${zone}点。`; + }, + fromLegacyEnemy: enemy => ({ + zone: enemy.zone ?? 0, + zoneSquare: enemy.zoneSquare ?? false, + range: enemy.range ?? 1 + }) } - if (enemy.hpBuff) { - str += `,生命提升${enemy.hpBuff}%`; + ) + ); + + // 16 - 夹击 + manager.registerSpecial( + 16, + defineNonePropertySpecial(16, { + getSpecialName: () => '夹击', + getDescription: () => + '经过两只相同的怪物中间,角色生命值变成一半。', + fromLegacyEnemy: () => {} + }) + ); + + // 17 - 仇恨 + manager.registerSpecial( + 17, + defineNonePropertySpecial(17, { + getSpecialName: () => '仇恨', + getDescription: () => + `战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得${core.values.hatred}点仇恨值)。`, + fromLegacyEnemy: () => {} + }) + ); + + // 18 - 阻击 + manager.registerSpecial( + 18, + defineCommonSerializableSpecial(18, 0, { + getSpecialName: () => '阻击', + getDescription: special => + `经过怪物十字范围内时怪物后退一格,同时对勇士造成${special.value}点伤害。`, + fromLegacyEnemy: enemy => enemy.repulse ?? 0 + }) + ); + + // 19 - 自爆 + manager.registerSpecial( + 19, + defineNonePropertySpecial(19, { + getSpecialName: () => '自爆', + getDescription: () => '战斗后角色的生命值变成1。', + fromLegacyEnemy: () => {} + }) + ); + + // 20 - 无敌 + manager.registerSpecial( + 20, + defineNonePropertySpecial(20, { + getSpecialName: () => '无敌', + getDescription: () => '角色无法打败怪物,除非拥有十字架。', + fromLegacyEnemy: () => {} + }) + ); + + // 21 - 退化 + manager.registerSpecial( + 21, + defineCommonSerializableSpecial( + 21, + { atkValue: 0, defValue: 0 }, + { + getSpecialName: () => '退化', + getDescription: special => { + const { atkValue, defValue } = special.value; + return `战斗后角色永久下降${atkValue}点攻击和${defValue}点防御。`; + }, + fromLegacyEnemy: enemy => ({ + atkValue: enemy.atkValue ?? 0, + defValue: enemy.defValue ?? 0 + }) } - if (enemy.atkBuff) { - str += `,攻击提升${enemy.atkBuff}%`; + ) + ); + + // 22 - 固伤 + manager.registerSpecial( + 22, + defineCommonSerializableSpecial(22, 0, { + getSpecialName: () => '固伤', + getDescription: special => + `战斗前,怪物对角色造成${special.value}点固定伤害,未开启负伤时无视角色护盾。`, + fromLegacyEnemy: enemy => enemy.damage ?? 0 + }) + ); + + // 23 - 重生 + manager.registerSpecial( + 23, + defineNonePropertySpecial(23, { + getSpecialName: () => '重生', + getDescription: () => + '怪物被击败后,角色转换楼层则怪物将再次出现。', + fromLegacyEnemy: () => {} + }) + ); + + // 24 - 激光 + manager.registerSpecial( + 24, + defineCommonSerializableSpecial(24, 0, { + getSpecialName: () => '激光', + getDescription: special => + `经过怪物同行或同列时自动减生命${special.value}点。`, + fromLegacyEnemy: enemy => enemy.laser ?? 0 + }) + ); + + // 25 - 光环 + manager.registerSpecial( + 25, + defineCommonSerializableSpecial( + 25, + { + haloRange: 0, + haloSquare: false, + hpBuff: 0, + atkBuff: 0, + defBuff: 0 + }, + { + getSpecialName: () => '光环', + getDescription: special => { + const { haloRange, haloSquare, hpBuff, atkBuff, defBuff } = + special.value; + let str = ''; + if (haloRange > 0) { + if (haloSquare) { + str += '对于该怪物九宫格'; + } else { + str += '对于该怪物十字'; + } + str += `${haloRange}格范围内所有怪物`; + } else { + str += '同楼层所有怪物'; + } + if (hpBuff) { + str += `,生命提升${hpBuff}%`; + } + if (atkBuff) { + str += `,攻击提升${atkBuff}%`; + } + if (defBuff) { + str += `,防御提升${defBuff}%`; + } + str += `,线性叠加。`; + return str; + }, + fromLegacyEnemy: enemy => ({ + haloRange: enemy.haloRange ?? 0, + haloSquare: enemy.haloSquare ?? false, + hpBuff: enemy.hpBuff ?? 0, + atkBuff: enemy.atkBuff ?? 0, + defBuff: enemy.defBuff ?? 0 + }) } - if (enemy.defBuff) { - str += `,防御提升${enemy.defBuff}%`; - } - if (enemy.haloAdd) { - str += `,线性叠加。`; - } else { - str += `,不可叠加。`; - } - return str; - }, - color: '#e6e099' - }, - { - code: 26, - name: '支援', - desc: `当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。`, - color: '#77c0b6' - }, - { - code: 27, - name: '捕捉', - desc: `当走到怪物十字范围内时会进行强制战斗。`, - color: '#ff6f0a' - } -]; + ) + ); + + // 26 - 支援 + manager.registerSpecial( + 26, + defineNonePropertySpecial(26, { + getSpecialName: () => '支援', + getDescription: () => + '当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。', + fromLegacyEnemy: () => {} + }) + ); + + // 27 - 捕捉 + manager.registerSpecial( + 27, + defineNonePropertySpecial(27, { + getSpecialName: () => '捕捉', + getDescription: () => '当走到怪物十字范围内时会进行强制战斗。', + fromLegacyEnemy: () => {} + }) + ); +} diff --git a/packages-user/data-state/src/enemy/types.ts b/packages-user/data-state/src/enemy/types.ts new file mode 100644 index 0000000..7a8c5d0 --- /dev/null +++ b/packages-user/data-state/src/enemy/types.ts @@ -0,0 +1,12 @@ +export const enum MapDamageType { + /** 未知伤害 */ + Unknown, + /** 领域伤害 */ + Zone, + /** 激光伤害 */ + Layer, + /** 阻击伤害 */ + Repulse, + /** 夹击伤害 */ + Between +} diff --git a/packages-user/data-state/src/types.ts b/packages-user/data-state/src/types.ts index a635bc3..ba4fc5b 100644 --- a/packages-user/data-state/src/types.ts +++ b/packages-user/data-state/src/types.ts @@ -1,6 +1,12 @@ import { ILayerState } from './map'; import { IHeroFollower, IHeroState } from './hero'; import { IRoleFaceBinder } from './common'; +import { IEnemyManager } from '@user/data-base'; + +export interface IGameDataState { + /** 怪物管理器 */ + readonly enemyManager: IEnemyManager; +} export interface IStateSaveData { /** 跟随者列表 */ @@ -14,6 +20,8 @@ export interface ICoreState { readonly hero: IHeroState; /** 朝向绑定 */ readonly roleFace: IRoleFaceBinder; + /** 游戏数据状态 */ + readonly data: IGameDataState; /** id 到图块数字的映射 */ readonly idNumberMap: Map; /** 图块数字到 id 的映射 */ diff --git a/packages-user/types/src/index.ts b/packages-user/types/src/index.ts index 493abad..17d326c 100644 --- a/packages-user/types/src/index.ts +++ b/packages-user/types/src/index.ts @@ -1 +1,2 @@ export * from './enemy'; +export * from './spatial'; diff --git a/packages-user/types/src/spatial.ts b/packages-user/types/src/spatial.ts new file mode 100644 index 0000000..4926ac7 --- /dev/null +++ b/packages-user/types/src/spatial.ts @@ -0,0 +1,6 @@ +export interface ITileLocator { + /** 图块所在横坐标 */ + x: number; + /** 图块所在纵坐标 */ + y: number; +} diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index fc0290f..7e019e0 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -52,6 +52,7 @@ "50": "Expected a planEnd call after animation plan calling.", "51": "Animatable object cannot be animated by plans with the same start time.", "52": "To get divider payload, an excitation binding is expected.", + "53": "Expected serializable value set as enemy's default attribute.", "1201": "Floor-damage extension needs 'floor-binder' extension as dependency." }, "warn": { @@ -150,6 +151,16 @@ "93": "Followers can only be removed when the last follower is not moving.", "94": "Expecting an excitation binding when using '$1'", "95": "Task adding is required before start loading.", + "96": "Enemy '$1': special with code $2 already exists", + "97": "Multiple aura converters matched for special code $1, skipping conversion.", + "98": "Special modifier conflicts with aura of priority $1 (current: $2), this may cause issues.", + "99": "Newly converted aura has priority $1 higher than current $2, skipping.", + "100": "Adding and deleting effect both on special aura is forbidden, considering split it into two auras.", + "101": "Local refresh affected aura converted by special $1, which means you may have modified specials outside special aura or special query effect.", + "102": "Map damage converter is missing, skipping map damage refresh.", + "103": "Map damage reducer is missing, reduced map damage is unavailable.", + "104": "Enemy dirty marking failed since specific enemy is not in current context.", + "105": "No specific map damage view stored, which seems like an internal bug of map damage system.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency." } } diff --git a/packages/common/src/utils/dir.ts b/packages/common/src/utils/dir.ts new file mode 100644 index 0000000..8fc979d --- /dev/null +++ b/packages/common/src/utils/dir.ts @@ -0,0 +1,36 @@ +import { + IDirectionDescriptor, + IDirectionMapper, + InternalDirectionGroup +} from './types'; + +export class DirectionMapper implements IDirectionMapper { + private readonly groups: Map = new Map(); + + constructor() { + this.registerGroup(InternalDirectionGroup.Dir4, [ + { x: 0, y: -1 }, + { x: 0, y: 1 }, + { x: -1, y: 0 }, + { x: 1, y: 0 } + ]); + this.registerGroup(InternalDirectionGroup.Dir8, [ + { x: 0, y: -1 }, + { x: 0, y: 1 }, + { x: -1, y: 0 }, + { x: 1, y: 0 }, + { x: -1, y: -1 }, + { x: 1, y: -1 }, + { x: -1, y: 1 }, + { x: 1, y: 1 } + ]); + } + + registerGroup(group: number, dir: Iterable): void { + this.groups.set(group, [...dir]); + } + + map(group: number): Iterable { + return this.groups.get(group) ?? []; + } +} diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 11d7c3a..69432d0 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -1,2 +1,4 @@ +export * from './dir'; export * from './func'; +export * from './range'; export * from './types'; diff --git a/packages/common/src/utils/range.ts b/packages/common/src/utils/range.ts new file mode 100644 index 0000000..25e5f4e --- /dev/null +++ b/packages/common/src/utils/range.ts @@ -0,0 +1,302 @@ +import { clamp } from 'lodash-es'; +import { + IDirectionDescriptor, + IManhattanRangeParam, + IRangeHost, + IRange, + IRayRangeParam, + IRectRangeParam +} from './types'; + +export abstract class BaseRange implements IRange { + protected host: IRangeHost = { width: 0, height: 0 }; + + bindHost(host: IRangeHost): void { + this.host = host; + } + + /** + * 判断一个点是否在宿主对象范围内 + * @param x 横坐标 + * @param y 纵坐标 + */ + protected isInBounds(x: number, y: number) { + const { width, height } = this.host; + return x >= 0 && y >= 0 && x < width && y < height; + } + + /** + * 判断一个坐标索引是否在宿主对象范围内 + * @param index 坐标索引 + */ + protected isValidIndex(index: number) { + const { width, height } = this.host; + return index >= 0 && index < width * height; + } + + /** + * 估算范围内总共有多少个点 + * @param param 传递的参数 + */ + protected abstract estimatePointCount(param: Readonly): number; + + abstract iterateLoc(param: Readonly): Iterable; + + abstract inRange(x: number, y: number, param: Readonly): boolean; + + inRangeIndex(index: number, param: Readonly): boolean { + const { width } = this.host; + return this.inRange(index % width, Math.floor(index / width), param); + } + + *iterate(list: Iterable, param: Readonly): Iterable { + for (const index of list) { + if (this.inRangeIndex(index, param)) yield index; + } + } + + *scan(list: Set, param: Readonly): Iterable { + for (const index of this.iterateLoc(param)) { + if (list.has(index)) yield index; + } + } + + autoDetect(list: Set, param: Readonly): Iterable { + if (this.estimatePointCount(param) < list.size) { + return this.scan(list, param); + } else { + return this.iterate(list, param); + } + } +} + +export class RectRange extends BaseRange { + protected estimatePointCount(param: Readonly): number { + return Math.abs(param.w) * Math.abs(param.h); + } + + *iterateLoc(param: Readonly): Iterable { + const { width, height } = this.host; + const sx = clamp(Math.min(param.x, param.x + param.w), 0, width); + const sy = clamp(Math.min(param.y, param.y + param.h), 0, height); + const ex = clamp(Math.max(param.x, param.x + param.w), 0, width); + const ey = clamp(Math.max(param.y, param.y + param.h), 0, height); + if (sx >= ex || sy >= ey) return; + + for (let y = sy; y < ey; y++) { + for (let x = sx; x < ex; x++) { + yield y * width + x; + } + } + } + + inRange(x: number, y: number, param: Readonly): boolean { + const sx = Math.min(param.x, param.x + param.w); + const sy = Math.min(param.y, param.y + param.h); + const ex = Math.max(param.x, param.x + param.w); + const ey = Math.max(param.y, param.y + param.h); + + return this.isInBounds(x, y) && x >= sx && y >= sy && x < ex && y < ey; + } +} + +export class ManhattanRange extends BaseRange { + protected estimatePointCount( + param: Readonly + ): number { + const radius = Math.max(0, param.radius); + return 1 + 2 * radius * (radius + 1); + } + + *iterateLoc(param: Readonly): Iterable { + const { width, height } = this.host; + for (let dy = -param.radius; dy <= param.radius; dy++) { + const y = param.cy + dy; + if (y < 0 || y >= height) { + continue; + } + + const span = param.radius - Math.abs(dy); + const startX = Math.max(0, param.cx - span); + const endX = Math.min(width - 1, param.cx + span); + for (let x = startX; x <= endX; x++) { + yield y * width + x; + } + } + } + + inRange( + x: number, + y: number, + param: Readonly + ): boolean { + return ( + this.isInBounds(x, y) && + Math.abs(x - param.cx) + Math.abs(y - param.cy) <= param.radius + ); + } +} + +export class RayRange extends BaseRange { + protected estimatePointCount(param: Readonly): number { + const { width, height } = this.host; + // 考虑到这种范围的 `inRange` 判断要更加耗时,因此将返回值略微降低,更倾向于使用 `scan` 方式 + return ((width + height) * param.dir.length) / 3; + } + + /** + * 判断一个点是否在某条射线上 + * @param x 横坐标 + * @param y 纵坐标 + * @param direction 方向对象 + * @param param 范围参数 + */ + private isPointOnRay( + x: number, + y: number, + direction: IDirectionDescriptor, + param: Readonly + ): boolean { + if (direction.x === 0 && direction.y === 0) { + return false; + } + + const deltaX = x - param.cx; + const deltaY = y - param.cy; + + if (direction.x === 0) { + return ( + deltaX === 0 && + deltaY % direction.y === 0 && + deltaY / direction.y >= 0 + ); + } + + if (direction.y === 0) { + return ( + deltaY === 0 && + deltaX % direction.x === 0 && + deltaX / direction.x >= 0 + ); + } + + if (deltaX % direction.x !== 0 || deltaY % direction.y !== 0) { + return false; + } + + const stepX = deltaX / direction.x; + const stepY = deltaY / direction.y; + return stepX >= 0 && stepX === stepY; + } + + *iterateLoc(param: Readonly): Iterable { + const { width } = this.host; + const yielded = new Set(); + + if (this.isInBounds(param.cx, param.cy)) { + const centerIndex = param.cy * width + param.cx; + yielded.add(centerIndex); + yield centerIndex; + } + + for (const direction of param.dir) { + if (direction.x === 0 && direction.y === 0) { + continue; + } + + let x = param.cx + direction.x; + let y = param.cy + direction.y; + while (this.isInBounds(x, y)) { + const index = y * width + x; + if (!yielded.has(index)) { + yielded.add(index); + yield index; + } + x += direction.x; + y += direction.y; + } + } + } + + inRange(x: number, y: number, param: Readonly): boolean { + if (!this.isInBounds(x, y)) { + return false; + } + + if (x === param.cx && y === param.cy) { + return true; + } + + for (const direction of param.dir) { + if (this.isPointOnRay(x, y, direction, param)) { + return true; + } + } + + return false; + } +} + +export class FullRange extends BaseRange { + protected estimatePointCount(): number { + const { width, height } = this.host; + return width * height; + } + + iterate(list: Iterable): Iterable { + return list; + } + + *iterateLoc(): Iterable { + const { width, height } = this.host; + for (let index = 0; index < width * height; index++) { + yield index; + } + } + + scan(list: Set): Iterable { + return list; + } + + autoDetect(list: Set): Iterable { + return list; + } + + inRange(x: number, y: number): boolean { + return this.isInBounds(x, y); + } + + inRangeIndex(index: number): boolean { + return this.isValidIndex(index); + } +} + +export class NoneRange extends BaseRange { + protected estimatePointCount(): number { + return 0; + } + + iterate(): Iterable { + return new Set(); + } + + iterateLoc(): Iterable { + return new Set(); + } + + scan(): Iterable { + return new Set(); + } + + autoDetect(): Iterable { + return new Set(); + } + + inRange(): boolean { + return false; + } + + inRangeIndex(): boolean { + return false; + } +} diff --git a/packages/common/src/utils/types.ts b/packages/common/src/utils/types.ts index fbff08c..3a7fd90 100644 --- a/packages/common/src/utils/types.ts +++ b/packages/common/src/utils/types.ts @@ -27,3 +27,128 @@ export interface ISearchable8Dir { /** 获取右下元素 */ rightDown(): ISearchable8Dir | null; } + +//#region 范围 + +export interface IRangeHost { + /** 区域整体宽度 */ + readonly width: number; + /** 区域整体高度 */ + readonly height: number; +} + +export interface IRange { + /** + * 绑定宿主对象,宽度和高度将会使用此宿主对象的宽高 + * @param host 宿主对象 + */ + bindHost(host: IRangeHost): void; + + /** + * 扫描一个可迭代对象,依次输出列表中在此范围内的对象。 + * 算法是依次迭代 `list` 的内容并判断其是否在范围内,计算次数为传入的列表长度。 + * @param list 要扫描的列表,每一项的值为 `y * width + x` + * @param param 传递给范围对象的参数 + */ + iterate(list: Iterable, param: Readonly): Iterable; + + /** + * 迭代范围内的所有坐标,输出值为 `y * width + x` + * @param param 传入范围对象的参数 + */ + iterateLoc(param: Readonly): Iterable; + + /** + * 指定一个列表,按照一定的顺序判定这一点是否在列表中。 + * 算法是按照范围的内置顺序依次变量,然后判断这一点是否在列表中,计算次数为范围内包含的坐标数。 + * @param list 要扫描的列表,每一项的值为 `y * width + x` + * @param param 传递给范围对象的参数 + */ + scan(list: Set, param: Readonly): Iterable; + + /** + * 自动决定是使用 {@link iterate} 来迭代还是使用 {@link scan} 来扫描。 + * @param list 要扫描的列表,每一项的值为 `y * width + x` + * @param param 传递给范围对象的参数 + */ + autoDetect(list: Set, param: Readonly): Iterable; + + /** + * 判断一个点是否在范围内 + * @param x 横坐标 + * @param y 纵坐标 + * @param param 传递给范围对象的参数 + */ + inRange(x: number, y: number, param: Readonly): boolean; + + /** + * 判断一个索引是否在范围内 + * @param index 索引,值表示 y * width + x + * @param param 传递给范围对象的参数 + */ + inRangeIndex(index: number, param: Readonly): boolean; +} + +export interface IRectRangeParam { + /** 左上角横坐标 */ + x: number; + /** 左上角纵坐标 */ + y: number; + /** 范围宽度 */ + w: number; + /** 范围高度 */ + h: number; +} + +export interface IManhattanRangeParam { + /** 中心横坐标 */ + cx: number; + /** 中心纵坐标 */ + cy: number; + /** 半径 */ + radius: number; +} + +export interface IRayRangeParam { + /** 中心点横坐标 */ + cx: number; + /** 中心点纵坐标 */ + cy: number; + /** 方向列表 */ + dir: IDirectionDescriptor[]; +} + +//#endregion + +//#region 实用接口 + +export const enum InternalDirectionGroup { + /** 上下左右四方向 */ + Dir4, + /** 上下左右+左上+右上+左下+右下八方向 */ + Dir8 +} + +export interface IDirectionDescriptor { + /** 横坐标增量 */ + readonly x: number; + /** 纵坐标增量 */ + readonly y: number; +} + +export interface IDirectionMapper { + /** + * 注册一个方向组别 + * @param group 方向组别 + * @param dir 方向锁包含的描述器 + */ + registerGroup(group: number, dir: Iterable): void; + + /** + * 根据指定方向组别进行遍历 + * @param group 方向组别 + */ + map(group: number): Iterable; +} + +//#endregion diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 493abad..ec6f224 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1 +1,2 @@ export * from './enemy'; +export * from './utils'; diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts new file mode 100644 index 0000000..a37f97b --- /dev/null +++ b/packages/types/src/utils.ts @@ -0,0 +1,4 @@ +export interface IPosition { + x: number; + y: number; +} diff --git a/src/types/declaration/enemy.d.ts b/src/types/declaration/enemy.d.ts index 942b499..f02d09f 100644 --- a/src/types/declaration/enemy.d.ts +++ b/src/types/declaration/enemy.d.ts @@ -15,42 +15,14 @@ type PartialNumbericEnemyProperty = | 'purify' | 'atkValue' | 'defValue' - | 'damage' - | 'iceDecline' - | 'iceCore' - | 'fireCore' - | 'together' - | 'hungry' - | 'ice' - | 'crit' - | 'courage' - | 'charge' - | 'paleShield' - | 'iceHalo' - | 'day' - | 'night' - | 'melt' - | 'hpHalo' - | 'assimilateRange'; + | 'damage'; type BooleanEnemyProperty = | 'zoneSquare' | 'haloSquare' | 'notBomb' | 'add' - | 'haloAdd' - | 'specialMultiply'; - -type DetailedEnemy = { - specialText: string[]; - toShowSpecial: string[]; - toShowColor: Color[]; - specialColor: Color[]; - damageColor: Color; - criticalDamage: number; - critical: number; - defDamage: number; -} & Enemy; + | 'haloAdd'; type Enemy = { /** @@ -89,11 +61,6 @@ type Enemy = { */ afterBattle: MotaEvent; - specialHalo?: number[]; - translation?: [number, number]; - /** 战争号角 */ - horn?: [number, number, number]; - /** 大怪物绑定贴图 */ bigImage?: ImageIds; } & { @@ -134,6 +101,17 @@ interface EnemyInfoBase extends EnemySpecialBase { point: number; } +type DetailedEnemy = { + specialText: string[]; + toShowSpecial: string[]; + toShowColor: Color[]; + specialColor: Color[]; + damageColor: Color; + criticalDamage: number; + critical: number; + defDamage: number; +} & Enemy; + /** * 怪物的特殊属性定义 */