mirror of
https://github.com/motajs/template.git
synced 2026-05-02 12:23:13 +08:00
feat: 怪物及地图伤害系统
This commit is contained in:
parent
fe67939a05
commit
057d5ab813
@ -1,7 +1,9 @@
|
||||
{
|
||||
"name": "@user/data-base",
|
||||
"dependencies": {
|
||||
"@motajs/common": "workspace:*",
|
||||
"@motajs/types": "workspace:*",
|
||||
"@motajs/loader": "workspace:*"
|
||||
"@motajs/loader": "workspace:*",
|
||||
"@user/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
720
packages-user/data-base/src/enemy/context.ts
Normal file
720
packages-user/data-base/src/enemy/context.ts
Normal file
@ -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<number, EnemyView> = new Map();
|
||||
private readonly enemyMap: Map<number, IEnemy> = new Map();
|
||||
private readonly locatorViewMap: Map<IEnemyView, number> = new Map();
|
||||
private readonly locatorEnemyMap: Map<IEnemy, number> = new Map();
|
||||
private readonly computedToView: Map<IReadonlyEnemy, EnemyView> = new Map();
|
||||
|
||||
private readonly auraConverter: Set<IAuraConverter> = new Set();
|
||||
private readonly converterStatus: Map<IAuraConverter, boolean> = new Map();
|
||||
private readonly convertedAura: Map<ISpecial<any>, IAuraView> = new Map();
|
||||
|
||||
private readonly commonQueryMap: Map<number, IEnemyCommonQueryEffect[]> =
|
||||
new Map();
|
||||
|
||||
private readonly specialQueryEffects: Map<
|
||||
number,
|
||||
IEnemySpecialQueryEffect[]
|
||||
> = new Map();
|
||||
|
||||
private readonly finalEffects: IEnemyFinalEffect[] = [];
|
||||
private readonly globalAuraList: Set<IAuraView> = new Set();
|
||||
private readonly sortedAura: Map<number, Set<IAuraView>> = new Map();
|
||||
|
||||
private readonly needTotallyRefresh: Set<IEnemyView> = new Set();
|
||||
private readonly requestedCommonContext: Set<IEnemyView> = new Set();
|
||||
private readonly dirtyEnemy: Set<IEnemyView> = 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<ITileLocator> | null {
|
||||
const index = this.locatorEnemyMap.get(enemy);
|
||||
if (index === undefined) return null;
|
||||
return this.indexer.indexToLocator(index);
|
||||
}
|
||||
|
||||
getEnemyLocatorByView(view: IEnemyView): Readonly<ITileLocator> | 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<T>(
|
||||
range: IRange<T>,
|
||||
param: T
|
||||
): Iterable<EnemyView> {
|
||||
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<T>(range: IRange<T>, param: T): Iterable<IEnemyView> {
|
||||
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<any>,
|
||||
enemy: IReadonlyEnemy,
|
||||
locator: ITileLocator
|
||||
): IEnemyAuraView<any, any> | 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<IAuraView> {
|
||||
const toAdd = modifier.add(enemy, locator);
|
||||
const toDelete = modifier.delete(enemy, locator);
|
||||
|
||||
const affectedAuras = new Set<IAuraView>();
|
||||
|
||||
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<number>();
|
||||
|
||||
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<number>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
134
packages-user/data-base/src/enemy/enemy.ts
Normal file
134
packages-user/data-base/src/enemy/enemy.ts
Normal file
@ -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<ISpecial<any>> = new Set();
|
||||
/** code -> ISpecial 映射,用于快速查找 */
|
||||
private readonly specialMap: Map<number, ISpecial<any>> = new Map();
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly code: number,
|
||||
readonly attributes: IEnemyAttributes
|
||||
) {}
|
||||
|
||||
getSpecial<T>(code: number): ISpecial<T> | null {
|
||||
return (this.specialMap.get(code) as ISpecial<T>) ?? null;
|
||||
}
|
||||
|
||||
hasSpecial(code: number): boolean {
|
||||
return this.specialMap.has(code);
|
||||
}
|
||||
|
||||
addSpecial(special: ISpecial<any>): 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<any>): 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<ISpecial<any>> {
|
||||
return this.specials;
|
||||
}
|
||||
|
||||
setAttribute<K extends keyof IEnemyAttributes>(
|
||||
key: K,
|
||||
value: IEnemyAttributes[K]
|
||||
): void {
|
||||
this.attributes[key] = value;
|
||||
}
|
||||
|
||||
getAttribute<K extends keyof IEnemyAttributes>(
|
||||
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'
|
||||
];
|
||||
7
packages-user/data-base/src/enemy/index.ts
Normal file
7
packages-user/data-base/src/enemy/index.ts
Normal file
@ -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';
|
||||
146
packages-user/data-base/src/enemy/manager.ts
Normal file
146
packages-user/data-base/src/enemy/manager.ts
Normal file
@ -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<number, SpecialCreation<any>> =
|
||||
new Map();
|
||||
/** 自定义怪物属性注册表,name -> 默认值 */
|
||||
private readonly attributeRegistry: Map<string, any> = new Map();
|
||||
/** 怪物模板表,code -> IEnemy */
|
||||
private readonly prefabByCode: Map<number, IEnemy> = new Map();
|
||||
/** 怪物模板表,id -> IEnemy */
|
||||
private readonly prefabById: Map<string, IEnemy> = new Map();
|
||||
/** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */
|
||||
private readonly legacyIdToCode: Map<string, number> = new Map();
|
||||
|
||||
registerSpecial(
|
||||
code: number,
|
||||
cons: (enemy: IEnemy) => ISpecial<any>
|
||||
): 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);
|
||||
}
|
||||
}
|
||||
341
packages-user/data-base/src/enemy/mapDamage.ts
Normal file
341
packages-user/data-base/src/enemy/mapDamage.ts
Normal file
@ -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<IMapDamageInfo>>;
|
||||
/** 所有影响该点的地图伤害视图 */
|
||||
readonly affectedBy: Set<IMapDamageView<any>>;
|
||||
}
|
||||
|
||||
interface IViewStore {
|
||||
/** 该地图伤害视图所影响的伤害信息 */
|
||||
readonly damages: Map<number, Readonly<IMapDamageInfo>>;
|
||||
/** 当前视图所属的怪物视图 */
|
||||
readonly enemy: IEnemyView;
|
||||
}
|
||||
|
||||
interface IDamageStore {
|
||||
/** 该地图伤害信息的地图伤害视图来源 */
|
||||
readonly sourceView: IMapDamageView<any>;
|
||||
/** 地图伤害信息的来源怪物 */
|
||||
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<number, IPointInfo> = new Map();
|
||||
/** 有来源地图伤害,坐标 -> 点伤害信息 */
|
||||
private readonly sourcedDamage: Map<number, IPointInfo> = new Map();
|
||||
/** 地图伤害视图 -> 其信息对象 */
|
||||
private readonly viewStore: Map<IMapDamageView, IViewStore> = new Map();
|
||||
/** 地图伤害信息 -> 其信息对象 */
|
||||
private readonly damageStore: Map<IMapDamageInfo, IDamageStore> = new Map();
|
||||
/** 怪物视图 -> 其影响对象 */
|
||||
private readonly enemyStore: Map<IEnemyView, Set<IMapDamageView>> =
|
||||
new Map();
|
||||
/** 需要延迟刷新的坐标索引 */
|
||||
private readonly dirtyIndexes: Set<number> = new Set();
|
||||
/** 合并后伤害缓存,索引 -> 合并结果 */
|
||||
private readonly reducedCache: Map<number, IMapDamageInfo> = 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<number>();
|
||||
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<IMapDamageInfo> | 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<Readonly<IMapDamageInfo>> {
|
||||
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<Readonly<IMapDamageInfo>> {
|
||||
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<number>();
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
95
packages-user/data-base/src/enemy/special.ts
Normal file
95
packages-user/data-base/src/enemy/special.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { ISpecial, SpecialCreation } from './types';
|
||||
|
||||
export interface ICommonSerializableSpecialConfig<T> {
|
||||
/** 获取特殊属性的名称 */
|
||||
getSpecialName: (special: ISpecial<T>) => string;
|
||||
/** 获取特殊属性的描述 */
|
||||
getDescription: (special: ISpecial<T>) => string;
|
||||
/** 从旧样板怪物对象获取此特殊属性对应的属性值 */
|
||||
fromLegacyEnemy: (enemy: Enemy) => T;
|
||||
}
|
||||
|
||||
export class CommonSerializableSpecial<T> implements ISpecial<T> {
|
||||
constructor(
|
||||
readonly code: number,
|
||||
public value: T,
|
||||
readonly config: ICommonSerializableSpecialConfig<T>
|
||||
) {}
|
||||
|
||||
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<T> {
|
||||
return new CommonSerializableSpecial(
|
||||
this.code,
|
||||
structuredClone(this.value),
|
||||
this.config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class NonePropertySpecial implements ISpecial<void> {
|
||||
value: void = undefined;
|
||||
|
||||
constructor(
|
||||
readonly code: number,
|
||||
readonly config: ICommonSerializableSpecialConfig<void>
|
||||
) {}
|
||||
|
||||
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<void> {
|
||||
return new NonePropertySpecial(this.code, this.config);
|
||||
}
|
||||
}
|
||||
|
||||
export function defineCommonSerializableSpecial<T>(
|
||||
code: number,
|
||||
value: T,
|
||||
config: ICommonSerializableSpecialConfig<T>
|
||||
): SpecialCreation<T> {
|
||||
return () =>
|
||||
new CommonSerializableSpecial(code, structuredClone(value), config);
|
||||
}
|
||||
|
||||
export function defineNonePropertySpecial(
|
||||
code: number,
|
||||
config: ICommonSerializableSpecialConfig<void>
|
||||
): SpecialCreation<void> {
|
||||
return () => new NonePropertySpecial(code, config);
|
||||
}
|
||||
739
packages-user/data-base/src/enemy/types.ts
Normal file
739
packages-user/data-base/src/enemy/types.ts
Normal file
@ -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<T = void> {
|
||||
/** 特殊属性代码 */
|
||||
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<T>;
|
||||
}
|
||||
|
||||
export interface IReadonlyEnemy {
|
||||
/** 怪物标识符 */
|
||||
readonly id: string;
|
||||
/** 怪物在地图上的标识数字 */
|
||||
readonly code: number;
|
||||
|
||||
/**
|
||||
* 根据特殊属性代码获取对应的对象
|
||||
* @param code 特殊属性代码
|
||||
*/
|
||||
getSpecial<T>(code: number): ISpecial<T> | null;
|
||||
|
||||
/**
|
||||
* 判断怪物是否拥有指定属性
|
||||
* @param code 特殊属性代码
|
||||
*/
|
||||
hasSpecial(code: number): boolean;
|
||||
|
||||
/**
|
||||
* 迭代此怪物所包含的所有特殊属性
|
||||
*/
|
||||
iterateSpecials(): Iterable<ISpecial<any>>;
|
||||
|
||||
/**
|
||||
* 获取怪物属性值
|
||||
* @param key 属性名称
|
||||
*/
|
||||
getAttribute<K extends keyof IEnemyAttributes>(key: K): IEnemyAttributes[K];
|
||||
|
||||
/**
|
||||
* 深拷贝此怪物对象
|
||||
*/
|
||||
clone(): IReadonlyEnemy;
|
||||
}
|
||||
|
||||
export interface IEnemy extends IReadonlyEnemy {
|
||||
/** 怪物标识符 */
|
||||
readonly id: string;
|
||||
/** 怪物在地图上的标识数字 */
|
||||
readonly code: number;
|
||||
/** 怪物属性值 */
|
||||
readonly attributes: Readonly<IEnemyAttributes>;
|
||||
/** 怪物拥有的特殊属性列表 */
|
||||
readonly specials: Set<ISpecial<any>>;
|
||||
|
||||
/**
|
||||
* 添加特殊属性
|
||||
* @param special 特殊属性对象
|
||||
*/
|
||||
addSpecial(special: ISpecial<any>): void;
|
||||
|
||||
/**
|
||||
* 删除指定的特殊属性
|
||||
* @param special 特殊属性代码或对象
|
||||
*/
|
||||
deleteSpecial(special: number | ISpecial<any>): void;
|
||||
|
||||
/**
|
||||
* 设置怪物属性值
|
||||
* @param key 属性名称
|
||||
* @param value 新的属性值
|
||||
*/
|
||||
setAttribute<K extends keyof IEnemyAttributes>(
|
||||
key: K,
|
||||
value: IEnemyAttributes[K]
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 深拷贝此怪物对象
|
||||
*/
|
||||
clone(): IEnemy;
|
||||
|
||||
/**
|
||||
* 从一个怪物对象中将属性复制到当前对象
|
||||
* @param enemy 怪物对象
|
||||
*/
|
||||
copy(enemy: IReadonlyEnemy): void;
|
||||
}
|
||||
|
||||
export type SpecialCreation<T> = (enemy: IEnemy) => ISpecial<T>;
|
||||
|
||||
export interface IEnemyManager {
|
||||
/**
|
||||
* 注册一个特殊属性
|
||||
* @param code 特殊属性代码
|
||||
* @param cons 特殊属性创建函数
|
||||
*/
|
||||
registerSpecial(code: number, cons: SpecialCreation<any>): 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<any>[];
|
||||
|
||||
/**
|
||||
* 获取制定怪物身上要删除的特殊属性
|
||||
* @param enemy 怪物对象
|
||||
* @param locator 怪物定位符
|
||||
*/
|
||||
delete(enemy: IReadonlyEnemy, locator: ITileLocator): ISpecial<any>[];
|
||||
|
||||
/**
|
||||
* 修改一个怪物的特殊属性,如果真正进行了修改则返回 true,否则返回 false
|
||||
* @param enemy 怪物对象
|
||||
* @param special 要修改的怪物特殊属性
|
||||
* @param locator 怪物定位符
|
||||
*/
|
||||
modify(
|
||||
enemy: IReadonlyEnemy,
|
||||
special: ISpecial<any>,
|
||||
locator: ITileLocator
|
||||
): boolean;
|
||||
}
|
||||
|
||||
export interface IAuraView<T = any> {
|
||||
/** 此光环视图的优先级 */
|
||||
readonly priority: number;
|
||||
/** 此光环视图的影响范围 */
|
||||
readonly range: IRange<T>;
|
||||
|
||||
/** 这个光环视图是否有可能修改怪物的基本属性 */
|
||||
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<T, S> extends IAuraView<T> {
|
||||
/** 此光环视图所属的怪物 */
|
||||
readonly enemy: IReadonlyEnemy;
|
||||
/** 此光环视图所属的特殊属性 */
|
||||
readonly special: ISpecial<S>;
|
||||
/** 此光环视图所属怪物的定位符 */
|
||||
readonly locator: ITileLocator;
|
||||
}
|
||||
|
||||
export interface IAuraConverter {
|
||||
/**
|
||||
* 判断一个特殊属性是否应该被当前光环转换器执行转换
|
||||
*/
|
||||
shouldConvert(
|
||||
special: ISpecial<any>,
|
||||
enemy: IReadonlyEnemy,
|
||||
locator: ITileLocator
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* 将一个特殊属性转换为光环视图
|
||||
*/
|
||||
convert(
|
||||
special: ISpecial<any>,
|
||||
enemy: IReadonlyEnemy,
|
||||
locator: ITileLocator
|
||||
): IEnemyAuraView<any, any>;
|
||||
}
|
||||
|
||||
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<any>,
|
||||
query: () => IEnemyContext,
|
||||
locator: ITileLocator
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface IEnemyFinalEffect {
|
||||
/** 效果优先级,越高会越先被执行 */
|
||||
readonly priority: number;
|
||||
|
||||
/**
|
||||
* 向怪物施加最终修饰效果
|
||||
*/
|
||||
apply(enemy: IEnemy, locator: ITileLocator): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 地图伤害
|
||||
|
||||
export interface IMapDamageInfoExtra {
|
||||
/** 捕捉怪物信息 */
|
||||
catch: Set<ITileLocator>;
|
||||
/** 阻击怪物信息 */
|
||||
repulse: Set<ITileLocator>;
|
||||
}
|
||||
|
||||
export interface IMapDamageInfo {
|
||||
/** 伤害值 */
|
||||
damage: number;
|
||||
/** 伤害类型 */
|
||||
type: number;
|
||||
/** 地图伤害额外信息 */
|
||||
extra: IMapDamageInfoExtra;
|
||||
}
|
||||
|
||||
export interface IMapDamageView<T = any> {
|
||||
/** 获取地图伤害影响范围 */
|
||||
getRange(): IRange<T>;
|
||||
|
||||
/** 获取范围参数 */
|
||||
getRangeParam(): T;
|
||||
|
||||
/**
|
||||
* 获取指定位置的地图伤害,会对坐标进行判断
|
||||
* @param locator 伤害位置
|
||||
*/
|
||||
getDamageAt(locator: ITileLocator): Readonly<IMapDamageInfo> | null;
|
||||
|
||||
/**
|
||||
* 获取指定位置的地图伤害,但是不会对坐标进行判断
|
||||
* @param locator 伤害位置
|
||||
*/
|
||||
getDamageWithoutCheck(
|
||||
locator: ITileLocator
|
||||
): Readonly<IMapDamageInfo> | null;
|
||||
}
|
||||
|
||||
export interface IMapDamageConverter {
|
||||
/** 转换地图伤害视图 */
|
||||
convert(
|
||||
enemy: IReadonlyEnemy,
|
||||
locator: ITileLocator,
|
||||
context: IEnemyContext
|
||||
): IMapDamageView<any>[];
|
||||
}
|
||||
|
||||
export interface IMapDamageReducer {
|
||||
/** 对伤害信息进行合并 */
|
||||
reduce(
|
||||
info: Iterable<Readonly<IMapDamageInfo>>,
|
||||
locator: ITileLocator
|
||||
): Readonly<IMapDamageInfo>;
|
||||
}
|
||||
|
||||
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<IMapDamageInfo> | null;
|
||||
|
||||
/**
|
||||
* 获取指定位置未合并的地图伤害列表
|
||||
* @param locator 地图定位符
|
||||
*/
|
||||
getSeparatedDamage(
|
||||
locator: ITileLocator
|
||||
): Iterable<Readonly<IMapDamageInfo>>;
|
||||
}
|
||||
|
||||
//#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<ITileLocator> | null;
|
||||
|
||||
/**
|
||||
* 获取指定怪物视图当前所在位置
|
||||
* @param view 怪物视图
|
||||
*/
|
||||
getEnemyLocatorByView(view: IEnemyView): Readonly<ITileLocator> | 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<T>(range: IRange<T>, param: T): Iterable<IEnemyView>;
|
||||
|
||||
/**
|
||||
* 迭代上下文中的全部怪物
|
||||
*/
|
||||
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
|
||||
25
packages-user/data-base/src/enemy/utils.ts
Normal file
25
packages-user/data-base/src/enemy/utils.ts
Normal file
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './enemy';
|
||||
export * from './load';
|
||||
|
||||
export * from './game';
|
||||
|
||||
@ -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<string, number>;
|
||||
readonly numberIdMap: Map<number, string>;
|
||||
|
||||
@ -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 {
|
||||
|
||||
12
packages-user/data-state/src/data.ts
Normal file
12
packages-user/data-state/src/data.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
121
packages-user/data-state/src/enemy/aura.ts
Normal file
121
packages-user/data-state/src/enemy/aura.ts
Normal file
@ -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<any>): boolean {
|
||||
return special.code === 25;
|
||||
}
|
||||
|
||||
convert(
|
||||
special: ISpecial<any>,
|
||||
enemy: IReadonlyEnemy,
|
||||
locator: ITileLocator
|
||||
): IEnemyAuraView<any, any> {
|
||||
return new CommonAura(enemy, special as ISpecial<IHaloValue>, 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<IRectRangeParam | IManhattanRangeParam | void>;
|
||||
|
||||
constructor(
|
||||
readonly enemy: IReadonlyEnemy,
|
||||
readonly special: ISpecial<IHaloValue>,
|
||||
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
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './aura';
|
||||
export * from './damage';
|
||||
export * from './mapSpecials';
|
||||
export * from './special';
|
||||
|
||||
358
packages-user/data-state/src/enemy/mapSpecials.ts
Normal file
358
packages-user/data-state/src/enemy/mapSpecials.ts
Normal file
@ -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<T> implements IMapDamageView<T> {
|
||||
constructor(protected readonly context: IEnemyContext) {}
|
||||
|
||||
abstract getRange(): IRange<T>;
|
||||
|
||||
abstract getRangeParam(): T;
|
||||
|
||||
getDamageAt(locator: ITileLocator): Readonly<IMapDamageInfo> | 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<IMapDamageInfo> | null;
|
||||
|
||||
/**
|
||||
* 创建伤害信息
|
||||
* @param damage 伤害值
|
||||
* @param type 伤害类型
|
||||
* @param extra 额外信息
|
||||
*/
|
||||
protected createInfo(
|
||||
damage: number,
|
||||
type: number,
|
||||
extra?: Partial<IMapDamageInfoExtra>
|
||||
): 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<ITileLocator>,
|
||||
private readonly special: Readonly<ISpecial<IZoneValue>>
|
||||
) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
getRange(): IRange<IRectRangeParam | IManhattanRangeParam> {
|
||||
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<IMapDamageInfo> | null {
|
||||
return this.createInfo(this.special.value.zone, MapDamageType.Zone);
|
||||
}
|
||||
}
|
||||
|
||||
export class RepulseDamageView extends BaseMapDamageView<IManhattanRangeParam> {
|
||||
constructor(
|
||||
context: IEnemyContext,
|
||||
private readonly locator: Readonly<ITileLocator>,
|
||||
private readonly special: Readonly<ISpecial<number>>
|
||||
) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
getRange(): IRange<IManhattanRangeParam> {
|
||||
return MANHATTAN_RANGE;
|
||||
}
|
||||
|
||||
getRangeParam(): IManhattanRangeParam {
|
||||
return {
|
||||
cx: this.locator.x,
|
||||
cy: this.locator.y,
|
||||
radius: 1
|
||||
};
|
||||
}
|
||||
|
||||
getDamageWithoutCheck(
|
||||
locator: ITileLocator
|
||||
): Readonly<IMapDamageInfo> | 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<IRayRangeParam> {
|
||||
constructor(
|
||||
context: IEnemyContext,
|
||||
private readonly locator: Readonly<ITileLocator>,
|
||||
private readonly special: Readonly<ISpecial<number>>,
|
||||
private readonly dir: IDirectionDescriptor[] = DIR4
|
||||
) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
getRange(): IRange<IRayRangeParam> {
|
||||
return RAY_RANGE;
|
||||
}
|
||||
|
||||
getRangeParam(): IRayRangeParam {
|
||||
return {
|
||||
cx: this.locator.x,
|
||||
cy: this.locator.y,
|
||||
dir: this.dir
|
||||
};
|
||||
}
|
||||
|
||||
getDamageWithoutCheck(
|
||||
locator: ITileLocator
|
||||
): Readonly<IMapDamageInfo> | 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<IManhattanRangeParam> {
|
||||
private static readonly DAMAGE = 1;
|
||||
|
||||
constructor(
|
||||
context: IEnemyContext,
|
||||
private readonly locator: Readonly<ITileLocator>
|
||||
) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
getRange(): IRange<IManhattanRangeParam> {
|
||||
return MANHATTAN_RANGE;
|
||||
}
|
||||
|
||||
getRangeParam(): IManhattanRangeParam {
|
||||
return {
|
||||
cx: this.locator.x,
|
||||
cy: this.locator.y,
|
||||
radius: 1
|
||||
};
|
||||
}
|
||||
|
||||
getDamageWithoutCheck(
|
||||
locator: ITileLocator
|
||||
): Readonly<IMapDamageInfo> | 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<IManhattanRangeParam> {
|
||||
constructor(
|
||||
context: IEnemyContext,
|
||||
private readonly locator: Readonly<ITileLocator>
|
||||
) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
getRange(): IRange<IManhattanRangeParam> {
|
||||
return MANHATTAN_RANGE;
|
||||
}
|
||||
|
||||
getRangeParam(): IManhattanRangeParam {
|
||||
return {
|
||||
cx: this.locator.x,
|
||||
cy: this.locator.y,
|
||||
radius: 1
|
||||
};
|
||||
}
|
||||
|
||||
getDamageWithoutCheck(
|
||||
locator: ITileLocator
|
||||
): Readonly<IMapDamageInfo> | 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<any>[] {
|
||||
const views: IMapDamageView<any>[] = [];
|
||||
|
||||
const zone = enemy.getSpecial<IZoneValue>(15);
|
||||
if (zone) {
|
||||
views.push(new ZoneDamageView(context, locator, zone));
|
||||
}
|
||||
|
||||
if (enemy.hasSpecial(16)) {
|
||||
views.push(new BetweenDamageView(context, locator));
|
||||
}
|
||||
|
||||
const repulse = enemy.getSpecial<number>(18);
|
||||
if (repulse) {
|
||||
views.push(new RepulseDamageView(context, locator, repulse));
|
||||
}
|
||||
|
||||
const laser = enemy.getSpecial<number>(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<Readonly<IMapDamageInfo>>,
|
||||
_locator: ITileLocator
|
||||
): Readonly<IMapDamageInfo> {
|
||||
let damage = 0;
|
||||
let type = MapDamageType.Unknown;
|
||||
let maxDamage = -Infinity;
|
||||
const extra = {
|
||||
catch: new Set<ITileLocator>(),
|
||||
repulse: new Set<ITileLocator>()
|
||||
};
|
||||
|
||||
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
|
||||
@ -1,120 +1,207 @@
|
||||
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[] = [
|
||||
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<IVampireValue>(
|
||||
11,
|
||||
{ vampire: 0, add: false },
|
||||
{
|
||||
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;
|
||||
getSpecialName: () => '吸血',
|
||||
getDescription: special => {
|
||||
const { vampire, add } = special.value;
|
||||
return (
|
||||
`战斗前,怪物首先吸取角色的${vampire}%生命` +
|
||||
`(约${Math.floor((vampire / 100) * getHeroStatusOn('hp'))}点)作为伤害` +
|
||||
(enemy.add ? `,并把伤害数值加到自身生命上。` : `。`)
|
||||
(add ? `,并把伤害数值加到自身生命上。` : `。`)
|
||||
);
|
||||
},
|
||||
color: '#ff00d2'
|
||||
},
|
||||
{
|
||||
code: 12,
|
||||
name: '中毒',
|
||||
desc: () =>
|
||||
fromLegacyEnemy: enemy => ({
|
||||
vampire: enemy.vampire ?? 0,
|
||||
add: enemy.add ?? false
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 12 - 中毒
|
||||
manager.registerSpecial(
|
||||
12,
|
||||
defineNonePropertySpecial(12, {
|
||||
getSpecialName: () => '中毒',
|
||||
getDescription: () =>
|
||||
`战斗后,角色陷入中毒状态,每一步损失生命${core.values.poisonDamage}点。`,
|
||||
color: '#9e8'
|
||||
},
|
||||
{
|
||||
code: 13,
|
||||
name: '衰弱',
|
||||
desc: () => {
|
||||
fromLegacyEnemy: () => {}
|
||||
})
|
||||
);
|
||||
|
||||
// 13 - 衰弱
|
||||
manager.registerSpecial(
|
||||
13,
|
||||
defineNonePropertySpecial(13, {
|
||||
getSpecialName: () => '衰弱',
|
||||
getDescription: () => {
|
||||
const weak = core.values.weakValue;
|
||||
if (weak < 1) {
|
||||
return `战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%`;
|
||||
@ -122,122 +209,217 @@ export const specials: SpecialDeclaration[] = [
|
||||
return `战斗后,角色陷入衰弱状态,攻防暂时下降${weak}点`;
|
||||
}
|
||||
},
|
||||
color: '#f0bbcc'
|
||||
},
|
||||
fromLegacyEnemy: () => {}
|
||||
})
|
||||
);
|
||||
|
||||
// 14 - 诅咒
|
||||
manager.registerSpecial(
|
||||
14,
|
||||
defineNonePropertySpecial(14, {
|
||||
getSpecialName: () => '诅咒',
|
||||
getDescription: () =>
|
||||
'战斗后,角色陷入诅咒状态,战斗无法获得金币和经验。',
|
||||
fromLegacyEnemy: () => {}
|
||||
})
|
||||
);
|
||||
|
||||
// 15 - 领域
|
||||
manager.registerSpecial(
|
||||
15,
|
||||
defineCommonSerializableSpecial<IZoneValue>(
|
||||
15,
|
||||
{ zone: 0, zoneSquare: false, range: 1 },
|
||||
{
|
||||
code: 14,
|
||||
name: '诅咒',
|
||||
desc: '战斗后,角色陷入诅咒状态,战斗无法获得金币和经验。',
|
||||
color: '#bbeef0'
|
||||
getSpecialName: () => '领域',
|
||||
getDescription: special => {
|
||||
const { zone, zoneSquare, range } = special.value;
|
||||
return `经过怪物周围${zoneSquare ? '九宫格' : '十字'}范围内${range}格时自动减生命${zone}点。`;
|
||||
},
|
||||
{
|
||||
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: () =>
|
||||
fromLegacyEnemy: enemy => ({
|
||||
zone: enemy.zone ?? 0,
|
||||
zoneSquare: enemy.zoneSquare ?? false,
|
||||
range: enemy.range ?? 1
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 16 - 夹击
|
||||
manager.registerSpecial(
|
||||
16,
|
||||
defineNonePropertySpecial(16, {
|
||||
getSpecialName: () => '夹击',
|
||||
getDescription: () =>
|
||||
'经过两只相同的怪物中间,角色生命值变成一半。',
|
||||
fromLegacyEnemy: () => {}
|
||||
})
|
||||
);
|
||||
|
||||
// 17 - 仇恨
|
||||
manager.registerSpecial(
|
||||
17,
|
||||
defineNonePropertySpecial(17, {
|
||||
getSpecialName: () => '仇恨',
|
||||
getDescription: () =>
|
||||
`战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得${core.values.hatred}点仇恨值)。`,
|
||||
color: '#b0b666'
|
||||
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<IDegradationValue>(
|
||||
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
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 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<IHaloValue>(
|
||||
25,
|
||||
{
|
||||
haloRange: 0,
|
||||
haloSquare: false,
|
||||
hpBuff: 0,
|
||||
atkBuff: 0,
|
||||
defBuff: 0
|
||||
},
|
||||
{
|
||||
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 => {
|
||||
getSpecialName: () => '光环',
|
||||
getDescription: special => {
|
||||
const { haloRange, haloSquare, hpBuff, atkBuff, defBuff } =
|
||||
special.value;
|
||||
let str = '';
|
||||
if (enemy.haloRange) {
|
||||
if (enemy.haloSquare) {
|
||||
if (haloRange > 0) {
|
||||
if (haloSquare) {
|
||||
str += '对于该怪物九宫格';
|
||||
} else {
|
||||
str += '对于该怪物十字';
|
||||
}
|
||||
str += `${enemy.haloRange ?? 1}格范围内所有怪物`;
|
||||
str += `${haloRange}格范围内所有怪物`;
|
||||
} else {
|
||||
str += `同楼层所有怪物`;
|
||||
str += '同楼层所有怪物';
|
||||
}
|
||||
if (enemy.hpBuff) {
|
||||
str += `,生命提升${enemy.hpBuff}%`;
|
||||
if (hpBuff) {
|
||||
str += `,生命提升${hpBuff}%`;
|
||||
}
|
||||
if (enemy.atkBuff) {
|
||||
str += `,攻击提升${enemy.atkBuff}%`;
|
||||
if (atkBuff) {
|
||||
str += `,攻击提升${atkBuff}%`;
|
||||
}
|
||||
if (enemy.defBuff) {
|
||||
str += `,防御提升${enemy.defBuff}%`;
|
||||
if (defBuff) {
|
||||
str += `,防御提升${defBuff}%`;
|
||||
}
|
||||
if (enemy.haloAdd) {
|
||||
str += `,线性叠加。`;
|
||||
} else {
|
||||
str += `,不可叠加。`;
|
||||
}
|
||||
return str;
|
||||
},
|
||||
color: '#e6e099'
|
||||
},
|
||||
{
|
||||
code: 26,
|
||||
name: '支援',
|
||||
desc: `当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。`,
|
||||
color: '#77c0b6'
|
||||
},
|
||||
{
|
||||
code: 27,
|
||||
name: '捕捉',
|
||||
desc: `当走到怪物十字范围内时会进行强制战斗。`,
|
||||
color: '#ff6f0a'
|
||||
fromLegacyEnemy: enemy => ({
|
||||
haloRange: enemy.haloRange ?? 0,
|
||||
haloSquare: enemy.haloSquare ?? false,
|
||||
hpBuff: enemy.hpBuff ?? 0,
|
||||
atkBuff: enemy.atkBuff ?? 0,
|
||||
defBuff: enemy.defBuff ?? 0
|
||||
})
|
||||
}
|
||||
];
|
||||
)
|
||||
);
|
||||
|
||||
// 26 - 支援
|
||||
manager.registerSpecial(
|
||||
26,
|
||||
defineNonePropertySpecial(26, {
|
||||
getSpecialName: () => '支援',
|
||||
getDescription: () =>
|
||||
'当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。',
|
||||
fromLegacyEnemy: () => {}
|
||||
})
|
||||
);
|
||||
|
||||
// 27 - 捕捉
|
||||
manager.registerSpecial(
|
||||
27,
|
||||
defineNonePropertySpecial(27, {
|
||||
getSpecialName: () => '捕捉',
|
||||
getDescription: () => '当走到怪物十字范围内时会进行强制战斗。',
|
||||
fromLegacyEnemy: () => {}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
12
packages-user/data-state/src/enemy/types.ts
Normal file
12
packages-user/data-state/src/enemy/types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const enum MapDamageType {
|
||||
/** 未知伤害 */
|
||||
Unknown,
|
||||
/** 领域伤害 */
|
||||
Zone,
|
||||
/** 激光伤害 */
|
||||
Layer,
|
||||
/** 阻击伤害 */
|
||||
Repulse,
|
||||
/** 夹击伤害 */
|
||||
Between
|
||||
}
|
||||
@ -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<string, number>;
|
||||
/** 图块数字到 id 的映射 */
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from './enemy';
|
||||
export * from './spatial';
|
||||
|
||||
6
packages-user/types/src/spatial.ts
Normal file
6
packages-user/types/src/spatial.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface ITileLocator {
|
||||
/** 图块所在横坐标 */
|
||||
x: number;
|
||||
/** 图块所在纵坐标 */
|
||||
y: number;
|
||||
}
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
36
packages/common/src/utils/dir.ts
Normal file
36
packages/common/src/utils/dir.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {
|
||||
IDirectionDescriptor,
|
||||
IDirectionMapper,
|
||||
InternalDirectionGroup
|
||||
} from './types';
|
||||
|
||||
export class DirectionMapper implements IDirectionMapper {
|
||||
private readonly groups: Map<number, IDirectionDescriptor[]> = 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<IDirectionDescriptor>): void {
|
||||
this.groups.set(group, [...dir]);
|
||||
}
|
||||
|
||||
map(group: number): Iterable<IDirectionDescriptor> {
|
||||
return this.groups.get(group) ?? [];
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './dir';
|
||||
export * from './func';
|
||||
export * from './range';
|
||||
export * from './types';
|
||||
|
||||
302
packages/common/src/utils/range.ts
Normal file
302
packages/common/src/utils/range.ts
Normal file
@ -0,0 +1,302 @@
|
||||
import { clamp } from 'lodash-es';
|
||||
import {
|
||||
IDirectionDescriptor,
|
||||
IManhattanRangeParam,
|
||||
IRangeHost,
|
||||
IRange,
|
||||
IRayRangeParam,
|
||||
IRectRangeParam
|
||||
} from './types';
|
||||
|
||||
export abstract class BaseRange<T> implements IRange<T> {
|
||||
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<T>): number;
|
||||
|
||||
abstract iterateLoc(param: Readonly<T>): Iterable<number>;
|
||||
|
||||
abstract inRange(x: number, y: number, param: Readonly<T>): boolean;
|
||||
|
||||
inRangeIndex(index: number, param: Readonly<T>): boolean {
|
||||
const { width } = this.host;
|
||||
return this.inRange(index % width, Math.floor(index / width), param);
|
||||
}
|
||||
|
||||
*iterate(list: Iterable<number>, param: Readonly<T>): Iterable<number> {
|
||||
for (const index of list) {
|
||||
if (this.inRangeIndex(index, param)) yield index;
|
||||
}
|
||||
}
|
||||
|
||||
*scan(list: Set<number>, param: Readonly<T>): Iterable<number> {
|
||||
for (const index of this.iterateLoc(param)) {
|
||||
if (list.has(index)) yield index;
|
||||
}
|
||||
}
|
||||
|
||||
autoDetect(list: Set<number>, param: Readonly<T>): Iterable<number> {
|
||||
if (this.estimatePointCount(param) < list.size) {
|
||||
return this.scan(list, param);
|
||||
} else {
|
||||
return this.iterate(list, param);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RectRange extends BaseRange<IRectRangeParam> {
|
||||
protected estimatePointCount(param: Readonly<IRectRangeParam>): number {
|
||||
return Math.abs(param.w) * Math.abs(param.h);
|
||||
}
|
||||
|
||||
*iterateLoc(param: Readonly<IRectRangeParam>): Iterable<number> {
|
||||
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<IRectRangeParam>): 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<IManhattanRangeParam> {
|
||||
protected estimatePointCount(
|
||||
param: Readonly<IManhattanRangeParam>
|
||||
): number {
|
||||
const radius = Math.max(0, param.radius);
|
||||
return 1 + 2 * radius * (radius + 1);
|
||||
}
|
||||
|
||||
*iterateLoc(param: Readonly<IManhattanRangeParam>): Iterable<number> {
|
||||
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<IManhattanRangeParam>
|
||||
): boolean {
|
||||
return (
|
||||
this.isInBounds(x, y) &&
|
||||
Math.abs(x - param.cx) + Math.abs(y - param.cy) <= param.radius
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class RayRange extends BaseRange<IRayRangeParam> {
|
||||
protected estimatePointCount(param: Readonly<IRayRangeParam>): 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<IRayRangeParam>
|
||||
): 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<IRayRangeParam>): Iterable<number> {
|
||||
const { width } = this.host;
|
||||
const yielded = new Set<number>();
|
||||
|
||||
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<IRayRangeParam>): 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<void> {
|
||||
protected estimatePointCount(): number {
|
||||
const { width, height } = this.host;
|
||||
return width * height;
|
||||
}
|
||||
|
||||
iterate(list: Iterable<number>): Iterable<number> {
|
||||
return list;
|
||||
}
|
||||
|
||||
*iterateLoc(): Iterable<number> {
|
||||
const { width, height } = this.host;
|
||||
for (let index = 0; index < width * height; index++) {
|
||||
yield index;
|
||||
}
|
||||
}
|
||||
|
||||
scan(list: Set<number>): Iterable<number> {
|
||||
return list;
|
||||
}
|
||||
|
||||
autoDetect(list: Set<number>): Iterable<number> {
|
||||
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<void> {
|
||||
protected estimatePointCount(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
iterate(): Iterable<number> {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
iterateLoc(): Iterable<number> {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
scan(): Iterable<number> {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
autoDetect(): Iterable<number> {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
inRange(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
inRangeIndex(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -27,3 +27,128 @@ export interface ISearchable8Dir {
|
||||
/** 获取右下元素 */
|
||||
rightDown(): ISearchable8Dir | null;
|
||||
}
|
||||
|
||||
//#region 范围
|
||||
|
||||
export interface IRangeHost {
|
||||
/** 区域整体宽度 */
|
||||
readonly width: number;
|
||||
/** 区域整体高度 */
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
export interface IRange<T> {
|
||||
/**
|
||||
* 绑定宿主对象,宽度和高度将会使用此宿主对象的宽高
|
||||
* @param host 宿主对象
|
||||
*/
|
||||
bindHost(host: IRangeHost): void;
|
||||
|
||||
/**
|
||||
* 扫描一个可迭代对象,依次输出列表中在此范围内的对象。
|
||||
* 算法是依次迭代 `list` 的内容并判断其是否在范围内,计算次数为传入的列表长度。
|
||||
* @param list 要扫描的列表,每一项的值为 `y * width + x`
|
||||
* @param param 传递给范围对象的参数
|
||||
*/
|
||||
iterate(list: Iterable<number>, param: Readonly<T>): Iterable<number>;
|
||||
|
||||
/**
|
||||
* 迭代范围内的所有坐标,输出值为 `y * width + x`
|
||||
* @param param 传入范围对象的参数
|
||||
*/
|
||||
iterateLoc(param: Readonly<T>): Iterable<number>;
|
||||
|
||||
/**
|
||||
* 指定一个列表,按照一定的顺序判定这一点是否在列表中。
|
||||
* 算法是按照范围的内置顺序依次变量,然后判断这一点是否在列表中,计算次数为范围内包含的坐标数。
|
||||
* @param list 要扫描的列表,每一项的值为 `y * width + x`
|
||||
* @param param 传递给范围对象的参数
|
||||
*/
|
||||
scan(list: Set<number>, param: Readonly<T>): Iterable<number>;
|
||||
|
||||
/**
|
||||
* 自动决定是使用 {@link iterate} 来迭代还是使用 {@link scan} 来扫描。
|
||||
* @param list 要扫描的列表,每一项的值为 `y * width + x`
|
||||
* @param param 传递给范围对象的参数
|
||||
*/
|
||||
autoDetect(list: Set<number>, param: Readonly<T>): Iterable<number>;
|
||||
|
||||
/**
|
||||
* 判断一个点是否在范围内
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
* @param param 传递给范围对象的参数
|
||||
*/
|
||||
inRange(x: number, y: number, param: Readonly<T>): boolean;
|
||||
|
||||
/**
|
||||
* 判断一个索引是否在范围内
|
||||
* @param index 索引,值表示 y * width + x
|
||||
* @param param 传递给范围对象的参数
|
||||
*/
|
||||
inRangeIndex(index: number, param: Readonly<T>): 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<IDirectionDescriptor>): void;
|
||||
|
||||
/**
|
||||
* 根据指定方向组别进行遍历
|
||||
* @param group 方向组别
|
||||
*/
|
||||
map(group: number): Iterable<IDirectionDescriptor>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from './enemy';
|
||||
export * from './utils';
|
||||
|
||||
4
packages/types/src/utils.ts
Normal file
4
packages/types/src/utils.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface IPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
48
src/types/declaration/enemy.d.ts
vendored
48
src/types/declaration/enemy.d.ts
vendored
@ -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<I extends EnemyIds = EnemyIds> = {
|
||||
specialText: string[];
|
||||
toShowSpecial: string[];
|
||||
toShowColor: Color[];
|
||||
specialColor: Color[];
|
||||
damageColor: Color;
|
||||
criticalDamage: number;
|
||||
critical: number;
|
||||
defDamage: number;
|
||||
} & Enemy<I>;
|
||||
| 'haloAdd';
|
||||
|
||||
type Enemy<I extends EnemyIds = EnemyIds> = {
|
||||
/**
|
||||
@ -89,11 +61,6 @@ type Enemy<I extends EnemyIds = EnemyIds> = {
|
||||
*/
|
||||
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<I extends EnemyIds = EnemyIds> = {
|
||||
specialText: string[];
|
||||
toShowSpecial: string[];
|
||||
toShowColor: Color[];
|
||||
specialColor: Color[];
|
||||
damageColor: Color;
|
||||
criticalDamage: number;
|
||||
critical: number;
|
||||
defDamage: number;
|
||||
} & Enemy<I>;
|
||||
|
||||
/**
|
||||
* 怪物的特殊属性定义
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user