refactor: Range

This commit is contained in:
unanmed 2024-09-30 18:07:20 +08:00
parent 39f38cd703
commit 42f611b357
13 changed files with 117 additions and 263 deletions

View File

@ -506,18 +506,14 @@ gameKey
.realize('special', () => { .realize('special', () => {
if (hovered) { if (hovered) {
const { x, y } = hovered; const { x, y } = hovered;
const enemy = core.status.thisMap.enemy.list.find(v => { const enemy = core.status.thisMap.enemy.get(x, y);
return v.x === x && v.y === y;
});
if (enemy) mainUi.open('fixedDetail', { panel: 'special' }); if (enemy) mainUi.open('fixedDetail', { panel: 'special' });
} }
}) })
.realize('critical', () => { .realize('critical', () => {
if (hovered) { if (hovered) {
const { x, y } = hovered; const { x, y } = hovered;
const enemy = core.status.thisMap.enemy.list.find(v => { const enemy = core.status.thisMap.enemy.get(x, y);
return v.x === x && v.y === y;
});
if (enemy) mainUi.open('fixedDetail', { panel: 'critical' }); if (enemy) mainUi.open('fixedDetail', { panel: 'critical' });
} }
}) })

View File

@ -13,12 +13,7 @@ export function getEnemy(
y: number, y: number,
floorId: FloorIds = core.status.floorId floorId: FloorIds = core.status.floorId
) { ) {
const enemy = core.status.maps[floorId].enemy.list.find(v => { const enemy = core.status.maps[floorId].enemy.get(x, y);
return v.x === x && v.y === y;
});
if (!enemy) {
return null;
}
return enemy; return enemy;
} }

View File

@ -1,5 +1,5 @@
import { getHeroStatusOf, getHeroStatusOn } from '@/game/state/hero'; import { getHeroStatusOf, getHeroStatusOn } from '@/game/state/hero';
import { Range, RangeCollection } from '@/plugin/game/range'; import { Range } from '../util/range';
import { ensureArray, has, manhattan } from '@/plugin/game/utils'; import { ensureArray, has, manhattan } from '@/plugin/game/utils';
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
@ -72,6 +72,7 @@ type HaloFn = (info: EnemyInfo, enemy: EnemyInfo) => void;
export const haloSpecials: Set<number> = new Set([ export const haloSpecials: Set<number> = new Set([
8, 21, 25, 26, 27, 29, 31, 32 8, 21, 25, 26, 27, 29, 31, 32
]); ]);
/** 不可被同化的属性 */
export const unassimilatable: Set<number> = new Set(haloSpecials); export const unassimilatable: Set<number> = new Set(haloSpecials);
unassimilatable.add(8).add(30); unassimilatable.add(8).add(30);
/** 特殊属性对应 */ /** 特殊属性对应 */
@ -101,18 +102,20 @@ interface EnemyCollectionEvent {
calculated: []; calculated: [];
} }
export class EnemyCollection export class EnemyCollection extends EventEmitter<EnemyCollectionEvent> {
extends EventEmitter<EnemyCollectionEvent>
implements RangeCollection<DamageEnemy>
{
floorId: FloorIds; floorId: FloorIds;
list: DamageEnemy[] = []; list: Map<number, DamageEnemy> = new Map();
range: Range<DamageEnemy> = new Range(this); range: Range = new Range();
// todo: 改成Map<number, MapDamage> /** 地图伤害 */
mapDamage: Record<string, MapDamage> = {}; mapDamage: Record<string, MapDamage> = {};
haloList: HaloData[] = []; haloList: HaloData[] = [];
/** 楼层宽度 */
width: number = 0;
/** 楼层高度 */
height: number = 0;
/** 乾坤挪移属性 */ /** 乾坤挪移属性 */
translation: [number, number] = [0, 0]; translation: [number, number] = [0, 0];
@ -123,20 +126,27 @@ export class EnemyCollection
} }
get(x: number, y: number) { get(x: number, y: number) {
return this.list.find(v => v.x === x && v.y === y); const index = x + y * this.width;
return this.list.get(index);
} }
/** /**
* *
*/ */
extract() { extract() {
this.list = []; this.list.clear();
core.extractBlocks(this.floorId); core.extractBlocks(this.floorId);
core.status.maps[this.floorId].blocks.forEach(v => { const floor = core.status.maps[this.floorId];
this.width = floor.width;
this.height = floor.height;
floor.blocks.forEach(v => {
if (v.disable) return; if (v.disable) return;
if (v.event.cls !== 'enemy48' && v.event.cls !== 'enemys') return; if (v.event.cls !== 'enemy48' && v.event.cls !== 'enemys') return;
const { x, y } = v;
const index = x + y * this.width;
const enemy = core.material.enemys[v.event.id as EnemyIds]; const enemy = core.material.enemys[v.event.id as EnemyIds];
this.list.push( this.list.set(
index,
new DamageEnemy(enemy, v.x, v.y, this.floorId, this) new DamageEnemy(enemy, v.x, v.y, this.floorId, this)
); );
}); });
@ -201,7 +211,7 @@ export class EnemyCollection
recursion: boolean = false recursion: boolean = false
) { ) {
const arr = ensureArray(halo); const arr = ensureArray(halo);
const enemys = this.range.scan(type, data); const enemys = this.range.type(type).scan(this.list.values(), data);
if (!recursion) { if (!recursion) {
arr.forEach(v => { arr.forEach(v => {
enemys.forEach(e => { enemys.forEach(e => {

View File

@ -2,13 +2,11 @@ import './system';
import '../plugin/game/index'; import '../plugin/game/index';
import * as damage from './enemy/damage'; import * as damage from './enemy/damage';
import { EventEmitter, IndexedEventEmitter } from '@/core/common/eventEmitter'; import { EventEmitter, IndexedEventEmitter } from '@/core/common/eventEmitter';
import { Range } from '@/plugin/game/range';
import { specials } from './enemy/special'; import { specials } from './enemy/special';
import { gameListener, hook, loading } from './game'; import { gameListener, hook, loading } from './game';
import * as battle from './enemy/battle'; import * as battle from './enemy/battle';
import * as hero from './state/hero'; import * as hero from './state/hero';
import * as miscMechanism from './mechanism/misc'; import * as miscMechanism from './mechanism/misc';
import * as study from './mechanism/study';
import { registerPresetState } from './state/preset'; import { registerPresetState } from './state/preset';
import { ItemState } from './state/item'; import { ItemState } from './state/item';
import { import {
@ -23,7 +21,6 @@ Mota.register('class', 'DamageEnemy', damage.DamageEnemy);
Mota.register('class', 'EnemyCollection', damage.EnemyCollection); Mota.register('class', 'EnemyCollection', damage.EnemyCollection);
Mota.register('class', 'EventEmitter', EventEmitter); Mota.register('class', 'EventEmitter', EventEmitter);
Mota.register('class', 'IndexedEventEmitter', IndexedEventEmitter); Mota.register('class', 'IndexedEventEmitter', IndexedEventEmitter);
Mota.register('class', 'Range', Range);
// ----- 函数注册 // ----- 函数注册
Mota.register('fn', 'getEnemy', battle.getEnemy); Mota.register('fn', 'getEnemy', battle.getEnemy);
Mota.register('fn', 'getHeroStatusOn', hero.getHeroStatusOn); Mota.register('fn', 'getHeroStatusOn', hero.getHeroStatusOn);
@ -36,8 +33,7 @@ Mota.register('var', 'gameListener', gameListener);
Mota.register('var', 'loading', loading); Mota.register('var', 'loading', loading);
// ----- 模块注册 // ----- 模块注册
Mota.register('module', 'Mechanism', { Mota.register('module', 'Mechanism', {
BluePalace: miscMechanism.BluePalace, BluePalace: miscMechanism.BluePalace
Study: study
}); });
Mota.register('module', 'State', { Mota.register('module', 'State', {
ItemState, ItemState,

View File

@ -1,81 +0,0 @@
import { has } from '@/plugin/game/utils';
import { EnemyInfo } from '../enemy/damage';
import { getSkillLevel } from '@/plugin/game/skillTree';
import { hook } from '../game';
// 负责勇士技能:学习
const canStudy: Set<number> = new Set([1, 4, 5, 6, 7, 10, 11, 20, 28, 30]);
const numberProp: Record<number, (keyof Enemy)[]> = {
1: ['crit'],
6: ['n'],
7: ['hungry'],
10: ['courage'],
11: ['charge'],
20: ['ice'],
28: ['paleShield']
};
hook.on('afterBattle', () => {
declineStudiedSkill();
});
/**
*
* @param number
*/
export function canStudySkill(number: number) {
const s = (core.status.hero.special ??= { num: [], last: [] });
if (Mota.Plugin.require('skillTree_g').getSkillLevel(11) === 0)
return false;
if (s.num.length >= 1) return false;
if (s.num.includes(number)) return false;
return canStudy.has(number);
}
/**
*
* @param enemy
* @param number
*/
export function studySkill(enemy: EnemyInfo, number: number) {
core.status.hero.special ??= { num: [], last: [] };
const s = core.status.hero.special;
if (!canStudySkill(number)) return false;
s.num.push(number);
s.last.push(getSkillLevel(11) * 3 + 2);
const value = numberProp[number] ?? [];
for (const key of value) {
s[key] = enemy[key];
}
return true;
}
/**
*
* @param num
* @param i
*/
export function forgetStudiedSkill(num: number, i: number) {
const s = core.status.hero.special;
const index = has(i) ? i : s.num.indexOf(num);
if (index === -1) return;
s.num.splice(index, 1);
s.last.splice(index, 1);
const value = numberProp[num] ?? [];
for (const key of value) {
delete s[key];
}
}
/**
*
*/
export function declineStudiedSkill() {
const s = (core.status.hero.special ??= { num: [], last: [] });
for (let i = 0; i < s.last.length; i++) {
s.last[i]--;
if (s.last[i] === 0) {
forgetStudiedSkill(s.num[i], i);
}
}
}

View File

@ -16,7 +16,6 @@ import type { MotaSetting, SettingDisplayer } from '@/core/main/setting';
import type { GameStorage } from '@/core/main/storage'; import type { GameStorage } from '@/core/main/storage';
import type { DamageEnemy, EnemyCollection } from './enemy/damage'; import type { DamageEnemy, EnemyCollection } from './enemy/damage';
import type { specials } from './enemy/special'; import type { specials } from './enemy/special';
import type { Range } from '@/plugin/game/range';
import type { KeyCode } from '@/plugin/keyCodes'; import type { KeyCode } from '@/plugin/keyCodes';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type * as battle from './enemy/battle'; import type * as battle from './enemy/battle';
@ -63,7 +62,6 @@ interface ClassInterface {
Danmaku: typeof Danmaku; Danmaku: typeof Danmaku;
// todo: 放到插件 ShaderEffect: typeof ShaderEffect; // todo: 放到插件 ShaderEffect: typeof ShaderEffect;
// 定义于游戏进程,渲染进程依然可用 // 定义于游戏进程,渲染进程依然可用
Range: typeof Range;
EnemyCollection: typeof EnemyCollection; EnemyCollection: typeof EnemyCollection;
DamageEnemy: typeof DamageEnemy; DamageEnemy: typeof DamageEnemy;
} }

81
src/game/util/range.ts Normal file
View File

@ -0,0 +1,81 @@
import { isNil } from 'lodash-es';
interface RangeTypeData {
square: { x: number; y: number; d: number };
rect: { x: number; y: number; w: number; h: number };
}
type InRangeFn<E extends Partial<Loc>, T> = (item: E, data: T) => boolean;
export class Range {
static rangeType: Record<string, RangeType> = {};
/**
*
* @param type
*/
type<T extends string>(
type: T
): T extends keyof RangeTypeData ? RangeType<RangeTypeData[T]> : RangeType {
return Range.rangeType[type] as T extends keyof RangeTypeData
? RangeType<RangeTypeData[T]>
: RangeType;
}
/**
*
* @param type
* @param fn
*/
static register<K extends keyof RangeTypeData>(
type: K,
fn: InRangeFn<Partial<Loc>, RangeTypeData[K]>
): void;
static register(type: string, fn: InRangeFn<Partial<Loc>, any>): void;
static register(type: string, fn: InRangeFn<Partial<Loc>, any>): void {
const range = new RangeType(type, fn);
this.rangeType[type] = range;
}
}
class RangeType<Type = any> {
readonly type: string;
/**
*
* @param item
* @param data
*/
readonly inRange: InRangeFn<Partial<Loc>, Type>;
constructor(type: string, fn: InRangeFn<Partial<Loc>, Type>) {
this.type = type;
this.inRange = fn;
}
/**
*
* @param items
* @param data
*/
scan<T extends Partial<Loc>>(items: Iterable<T>, data: Type): T[] {
const res: T[] = [];
for (const ele of items) {
if (this.inRange(ele, data)) {
res.push(ele);
}
}
return res;
}
}
Range.register('square', (item, { x, y, d }) => {
if (isNil(item.x) || isNil(item.y)) return false;
const r = Math.floor(d / 2);
return Math.abs(item.x - x) <= r && Math.abs(item.y - y) <= r;
});
Range.register('rect', (item, { x, y, w, h }) => {
if (isNil(item.x) || isNil(item.y)) return false;
const ex = x + w;
const ey = y + h;
return item.x >= x && item.y >= y && item.x < ex && item.y < ey;
});

View File

@ -1,126 +0,0 @@
import { has } from './utils';
type RangeScanFn<C extends Partial<Loc>> = (
collection: Range<C>,
data: any
) => C[];
type InRangeFn<C extends Partial<Loc>> = (
collection: Range<C>,
data: any,
item: Partial<Loc>
) => boolean;
interface RangeType<C extends Partial<Loc>> {
scan: RangeScanFn<C>;
inRange: InRangeFn<C>;
}
export interface RangeCollection<I extends Partial<Loc>> {
list: I[];
range: Range<I>;
}
export class Range<C extends Partial<Loc>> {
collection: RangeCollection<C>;
cache: Record<string, any> = {};
static rangeType: Record<string, RangeType<Partial<Loc>>> = {};
constructor(collection: RangeCollection<C>) {
this.collection = collection;
}
/**
* collection
* @param type
* @param data
* @returns
*/
scan(type: string, data: any): C[] {
const t = Range.rangeType[type];
if (!t) {
throw new Error(`Unknown range type: ${type}.`);
}
return t.scan(this, data) as C[];
}
inRange(type: string, data: any, item: Partial<Loc>) {
const t = Range.rangeType[type];
if (!t) {
throw new Error(`Unknown range type: ${type}.`);
}
return t.inRange(this, data, item);
}
clearCache() {
this.cache = {};
}
static registerRangeType(
type: string,
scan: RangeScanFn<Partial<Loc>>,
inRange: InRangeFn<Partial<Loc>>
) {
Range.rangeType[type] = {
scan,
inRange
};
}
}
// ----- 默认的范围类型
// 方形区域
Range.registerRangeType(
'square',
(col, { x, y, d }) => {
const list = col.collection.list;
const r = Math.floor(d / 2);
return list.filter(v => {
return (
has(v.x) &&
has(v.y) &&
Math.abs(v.x - x) <= r &&
Math.abs(v.y - y) <= r
);
});
},
(col, { x, y, d }, item) => {
const r = Math.floor(d / 2);
return (
has(item.x) &&
has(item.y) &&
Math.abs(item.x - x) <= r &&
Math.abs(item.y - y) <= r
);
}
);
Range.registerRangeType(
'rect',
(col, { x, y, w, h }) => {
const list = col.collection.list;
const ex = x + w;
const ey = y + h;
return list.filter(v => {
return (
has(v.x) &&
has(v.y) &&
v.x >= x &&
v.y >= y &&
v.x < ex &&
v.y < ey
);
});
},
(col, { x, y, w, h }, item) => {
const ex = x + w;
const ey = y + h;
const v = item;
return (
has(v.x) && has(v.y) && v.x >= x && v.y >= y && v.x < ex && v.y < ey
);
}
);

View File

@ -1,6 +1,4 @@
import { canStudySkill, studySkill } from '@/game/mechanism/study';
import { upgradeSkill } from './skillTree'; import { upgradeSkill } from './skillTree';
import { ensureFloorDamage } from '@/game/enemy/damage';
const replayableSettings = ['autoSkill']; const replayableSettings = ['autoSkill'];

View File

@ -524,7 +524,7 @@ export class MinimapDrawer {
ctx.fillRect(x - 6, y - 2, 12, 4); ctx.fillRect(x - 6, y - 2, 12, 4);
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
const enemy = core.status.maps[floorId].enemy.list; const enemy = core.status.maps[floorId].enemy.list;
if (enemy.length === 0) { if (enemy.size === 0) {
ctx.strokeStyle = 'lightgreen'; ctx.strokeStyle = 'lightgreen';
ctx.lineCap = 'round'; ctx.lineCap = 'round';
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
@ -533,8 +533,12 @@ export class MinimapDrawer {
ctx.lineTo(x - 0.5, y + 1); ctx.lineTo(x - 0.5, y + 1);
ctx.lineTo(x + 1.5, y - 1); ctx.lineTo(x + 1.5, y - 1);
ctx.stroke(); ctx.stroke();
} else if (enemy.length <= 2) { } else if (enemy.size <= 2) {
const ids = [...new Set(enemy.map(v => v.id))]; const idSet = new Set<EnemyIds>();
enemy.forEach(v => {
idSet.add(v.id);
});
const ids: EnemyIds[] = [...idSet];
if (ids.length === 1) { if (ids.length === 1) {
core.drawIcon(ctx, ids[0], x - 2, y - 2, 4, 4); core.drawIcon(ctx, ids[0], x - 2, y - 2, 4, 4);
} else if (ids.length === 2) { } else if (ids.length === 2) {
@ -552,7 +556,7 @@ export class MinimapDrawer {
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(`+${enemy.length}`, x, y); ctx.fillText(`+${enemy.size}`, x, y);
} }
ctx.restore(); ctx.restore();

View File

@ -28,9 +28,7 @@ detailInfo.pos = 0;
if (hovered) { if (hovered) {
const { x, y } = hovered; const { x, y } = hovered;
const enemy = core.status.thisMap.enemy.list.find(v => { const enemy = core.status.thisMap.enemy.get(x, y);
return v.x === x && v.y === y;
});
if (enemy) { if (enemy) {
const detail = getDetailedEnemy(enemy); const detail = getDetailedEnemy(enemy);
detailInfo.enemy = detail; detailInfo.enemy = detail;

View File

@ -16,7 +16,6 @@ export { default as Skill } from './skill.vue';
export { default as SkillTree } from './skillTree.vue'; export { default as SkillTree } from './skillTree.vue';
export { default as Start } from './start.vue'; export { default as Start } from './start.vue';
export { default as StatusBar } from './statusBar.vue'; export { default as StatusBar } from './statusBar.vue';
export { default as Study } from './study.vue';
export { default as Toolbox } from './toolbox.vue'; export { default as Toolbox } from './toolbox.vue';
export { default as Hotkey } from './hotkey.vue'; export { default as Hotkey } from './hotkey.vue';
export { default as Toolbar } from './toolbar.vue'; export { default as Toolbar } from './toolbar.vue';

View File

@ -1,14 +0,0 @@
<template>
<div id="study"></div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
</script>
<style lang="less" scoped>
#study {
width: 100%;
height: 100%;
}
</style>