feat: Hero state & Item state

This commit is contained in:
unanmed 2024-08-01 21:39:33 +08:00
parent 5d24906364
commit 1df2bc66f4
13 changed files with 741 additions and 122 deletions

View File

@ -17,9 +17,11 @@
"@ant-design/icons-vue": "^6.1.0",
"@emotion/css": "^11.11.2",
"@vueuse/core": "^10.4.1",
"anon-tokyo": "0.0.0-alpha.0",
"ant-design-vue": "^3.2.20",
"axios": "^1.5.0",
"chart.js": "^4.4.0",
"eventemitter3": "^5.0.1",
"gl-matrix": "^3.4.3",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",

View File

@ -14,6 +14,9 @@ dependencies:
'@vueuse/core':
specifier: ^10.4.1
version: 10.4.1(vue@3.3.4)
anon-tokyo:
specifier: 0.0.0-alpha.0
version: 0.0.0-alpha.0
ant-design-vue:
specifier: ^3.2.20
version: 3.2.20(vue@3.3.4)
@ -23,6 +26,9 @@ dependencies:
chart.js:
specifier: ^4.4.0
version: 4.4.0
eventemitter3:
specifier: ^5.0.1
version: 5.0.1
gl-matrix:
specifier: ^3.4.3
version: 3.4.3
@ -2696,6 +2702,12 @@ packages:
indent-string: 4.0.0
dev: true
/anon-tokyo@0.0.0-alpha.0:
resolution: {integrity: sha512-4kq9NOB56RUC6YqZAkkuA2mLhfzdLa39RSi+dUOk6geL4rldWspBZP2XbKv3hhG8nf+HDL2LSOTb7opSbqY/gg==}
dependencies:
lodash-es: 4.17.21
dev: false
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}

View File

@ -22,7 +22,7 @@ type EmitFn<F extends (...params: any) => any> = (
type Key = number | string | symbol;
export class EventEmitter<T extends Record<keyof T, Callable>> {
export class EventEmitter<T extends Record<keyof T, Callable> = {}> {
protected events: {
[x: Key]: Listener<Callable>[];
} = {};

View File

@ -1200,7 +1200,7 @@ export class Damage extends Sprite {
/** 描边样式 */
strokeColor: CanvasStyle = '#000';
/** 描边粗细 */
strokeWidth: number = 1.5;
strokeWidth: number = 2;
constructor(floor?: FloorIds) {
super();

View File

@ -1,113 +0,0 @@
/**
*
* @param name
* @param floorId
*/
export function getHeroStatusOn(name: 'all', floorId?: FloorIds): HeroStatus;
export function getHeroStatusOn(
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOn<K extends keyof HeroStatus>(
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOn(
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
// @ts-ignore
return getHeroStatusOf(core.status.hero, name, floorId);
}
/**
*
* @param status
* @param name
* @param floorId
*/
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: 'all',
floorId?: FloorIds
): HeroStatus;
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOf<K extends keyof HeroStatus>(
status: Partial<HeroStatus>,
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOf(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
return getRealStatus(status, name, floorId);
}
function getRealStatus(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId: FloorIds = core.status.floorId
): any {
const { getSkillLevel } = Mota.Plugin.require('skillTree_g');
if (name instanceof Array) {
return Object.fromEntries(
name.map(v => [v, getRealStatus(status, v, floorId)])
);
}
if (name === 'all') {
return Object.fromEntries(
Object.keys(core.status.hero).map(v => [
v,
v !== 'all' &&
getRealStatus(status, v as keyof HeroStatus, floorId)
])
);
}
let s = (status?.[name] ?? core.status.hero[name]) as number;
if (s === null || s === void 0) {
throw new ReferenceError(
`Wrong hero status property name is delivered: ${name}`
);
}
// 永夜、极昼
if (name === 'atk' || name === 'def') {
s += window.flags?.[`night_${floorId}`] ?? 0;
}
// 技能
if (flags.bladeOn && flags.blade) {
const level = getSkillLevel(2);
if (name === 'atk') {
s *= 1 + 0.1 * level;
}
if (name === 'def') {
s *= 1 - 0.1 * level;
}
}
if (flags.shield && flags.shieldOn) {
const level = getSkillLevel(10);
if (name === 'atk') {
s *= 1 - 0.1 * level;
}
if (name === 'def') {
s *= 1 + 0.1 * level;
}
}
// buff
if (typeof s === 'number')
s *= core.getBuff(name as keyof NumbericHeroStatus);
// 取整
if (typeof s === 'number') s = Math.floor(s);
return s;
}

View File

@ -6,7 +6,7 @@ import { Range } from '@/plugin/game/range';
import { specials } from './enemy/special';
import { gameListener, hook, loading } from './game';
import * as battle from './enemy/battle';
import * as hero from './hero';
import * as hero from './state/hero';
import * as miscMechanism from './mechanism/misc';
import * as study from './mechanism/study';

474
src/game/state/hero.ts Normal file
View File

@ -0,0 +1,474 @@
import { logger } from '@/core/common/logger';
import { EventEmitter } from 'eventemitter3';
import { cloneDeep, isNil } from 'lodash-es';
import { GameState, ISerializable } from './state';
import { ItemState } from './item';
/**
*
* @param name
* @param floorId
*/
export function getHeroStatusOn(name: 'all', floorId?: FloorIds): HeroStatus;
export function getHeroStatusOn(
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOn<K extends keyof HeroStatus>(
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOn(
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
// @ts-ignore
return getHeroStatusOf(core.status.hero, name, floorId);
}
/**
*
* @param status
* @param name
* @param floorId
*/
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: 'all',
floorId?: FloorIds
): HeroStatus;
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOf<K extends keyof HeroStatus>(
status: Partial<HeroStatus>,
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOf(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
return getRealStatus(status, name, floorId);
}
function getRealStatus(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId: FloorIds = core.status.floorId
): any {
const { getSkillLevel } = Mota.Plugin.require('skillTree_g');
if (name instanceof Array) {
return Object.fromEntries(
name.map(v => [v, getRealStatus(status, v, floorId)])
);
}
if (name === 'all') {
return Object.fromEntries(
Object.keys(core.status.hero).map(v => [
v,
v !== 'all' &&
getRealStatus(status, v as keyof HeroStatus, floorId)
])
);
}
let s = (status?.[name] ?? core.status.hero[name]) as number;
if (s === null || s === void 0) {
throw new ReferenceError(
`Wrong hero status property name is delivered: ${name}`
);
}
// 永夜、极昼
if (name === 'atk' || name === 'def') {
s += window.flags?.[`night_${floorId}`] ?? 0;
}
// 技能
if (flags.bladeOn && flags.blade) {
const level = getSkillLevel(2);
if (name === 'atk') {
s *= 1 + 0.1 * level;
}
if (name === 'def') {
s *= 1 - 0.1 * level;
}
}
if (flags.shield && flags.shieldOn) {
const level = getSkillLevel(10);
if (name === 'atk') {
s *= 1 - 0.1 * level;
}
if (name === 'def') {
s *= 1 + 0.1 * level;
}
}
// buff
if (typeof s === 'number')
s *= core.getBuff(name as keyof NumbericHeroStatus);
// 取整
if (typeof s === 'number') s = Math.floor(s);
return s;
}
export interface IHeroStatusDefault {
atk: number;
def: number;
hp: number;
}
interface HeroStateEvent {
set: [key: string | number | symbol, value: any];
}
type HeroStatusCalculate<T> = <K extends keyof T>(key: K, value: T[K]) => T[K];
export class HeroState<
T extends object = IHeroStatusDefault
> extends EventEmitter<HeroStateEvent> {
readonly status: T;
readonly computedStatus: T;
readonly buffable: Set<keyof T> = new Set();
readonly buffMap: Map<keyof T, number> = new Map();
private cal: HeroStatusCalculate<T> = (_, value) => value;
constructor(init: T) {
super();
this.status = init;
this.computedStatus = cloneDeep(init);
}
/**
*
* @param key
* @param value
* @returns
*/
setStatus<K extends keyof T>(key: K, value: T[K]): boolean {
this.status[key] = value;
this.emit('set', key, value);
return this.refreshStatus(key);
}
/**
*
* @param key
* @param value
* @returns
*/
addStatus<K extends SelectKey<T, number>>(key: K, value: number): boolean {
if (typeof this.status[key] !== 'number') {
logger.warn(
14,
`Cannot add status of non-number status. Key: ${String(key)}`
);
return false;
}
return this.setStatus<K>(key, (this.status[key] + value) as T[K]);
}
/**
*
* @param key
* @returns
*/
getStatus<K extends keyof T>(key: K): T[K] {
return this.status[key];
}
/**
* 2.x所说的勇士真实属性
* @param key
*/
getComputedStatus<K extends keyof T>(key: K): T[K] {
return this.computedStatus[key];
}
/**
* buff加成
*/
markBuffable(key: SelectKey<T, number>): void {
if (typeof this.status[key] !== 'number') {
logger.warn(
12,
`Cannot mark buffable with a non-number status. Key: ${String(
key
)}.`
);
return;
}
this.buffable.add(key);
this.buffMap.set(key, 1);
}
/**
* buff值
* @param key buff的属性
* @param value buff值
* @returns
*/
setBuff(key: SelectKey<T, number>, value: number): boolean {
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
logger.warn(
13,
`Cannot set buff non-number status. Key: ${String(key)}.`
);
return false;
}
this.buffMap.set(key, value);
return this.refreshStatus(key);
}
/**
* buff值
* @param key buff属性
* @param value buff增量
* @returns
*/
addBuff(key: SelectKey<T, number>, value: number): boolean {
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
logger.warn(
13,
`Cannot set buff non-number status. Key: ${String(key)}.`
);
return false;
}
return this.setBuff(key, this.buffMap.get(key)! + value);
}
/**
*
* @param key
* @returns
*/
refreshStatus(key?: keyof T): boolean {
if (key === void 0) {
for (const [key, value] of Object.entries(this.status)) {
// @ts-ignore
this.computedStatus[key] = this.cal(key, value);
}
return true;
}
this.computedStatus[key] = this.cal(key, this.status[key]);
return true;
}
/**
* buff加成的属性
* @returns
*/
refreshBuffable(): boolean {
for (const key of this.buffable) {
this.computedStatus[key] = this.cal(key, this.status[key]);
}
return true;
}
/**
*
* @param fn key表示属性名value表示属性值
*/
overrideCalculate(fn: HeroStatusCalculate<T>) {
this.cal = fn;
}
}
interface IHeroItem {
items: Map<AllIdsOf<'items'>, number>;
/**
*
* @param item id
* @param value
* @returns
*/
setItem(item: AllIdsOf<'items'>, value: number): boolean;
/**
*
* @param item id
* @param value
* @returns
*/
addItem(item: AllIdsOf<'items'>, value: number): boolean;
/**
* 使
* @param item id
* @returns 使
*/
useItem(item: AllIdsOf<'items'>, x?: number, y?: number): boolean;
/**
*
* @param item id
* @param num
*/
getItem(item: AllIdsOf<'items'>, num: number): void;
/**
*
* @param item id
* @param x x坐标
* @param y y坐标
* @param floorId
* @param num
*/
getItem(
item: AllIdsOf<'items'>,
x: number,
y: number,
floorId?: FloorIds,
num?: number
): void;
/**
*
* @param item id
*/
itemCount(item: AllIdsOf<'items'>): number;
/**
*
* @param item id
*/
hasItem(item: AllIdsOf<'items'>): boolean;
}
interface HeroEvent {
beforeMove: [dir: Dir2];
afterMove: [dir: Dir2];
beforeMoveDirectly: [x: number, y: number];
afterMoveDirectly: [x: number, y: number];
stateChange: [state: HeroState<any>];
}
export class Hero<T extends object = IHeroStatusDefault>
extends EventEmitter<HeroEvent>
implements IHeroItem, ISerializable
{
x: number;
y: number;
floorId: FloorIds;
id: string;
state: HeroState<T>;
items: Map<AllIdsOf<'items'>, number> = new Map();
constructor(
id: string,
x: number,
y: number,
floorId: FloorIds,
state: HeroState<T>,
gameState: GameState
) {
super();
this.id = id;
this.x = x;
this.y = y;
this.floorId = floorId;
this.state = state;
const list = gameState.state.hero.list;
if (list.has(id)) {
logger.warn(11, `Repeated hero: ${id}.`);
}
list.set(id, this);
}
/**
* 使使使
* @param state
*/
setState(state: HeroState<T>): void {
this.state = state;
this.emit('stateChange', state);
}
/**
*
*/
getState(): HeroState<T> {
return this.state;
}
/**
* {@link HeroState.refreshStatus}
*/
refreshState(key?: keyof T) {
return this.state.refreshStatus(key);
}
setItem(item: AllIdsOf<'items'>, value: number): boolean {
this.items.set(item, value < 0 ? 0 : value);
return true;
}
addItem(item: AllIdsOf<'items'>, value: number): boolean {
return this.setItem(item, (this.items.get(item) ?? 0) + value);
}
useItem(item: AllIdsOf<'items'>): boolean {
const state = ItemState.item(item);
return !!state?.use(this);
}
/**
*
* @param item id
* @param num
*/
getItem(item: AllIdsOf<'items'>, num: number): boolean;
/**
*
* @param item id
* @param x x坐标
* @param y y坐标
* @param floorId
* @param num
*/
getItem(x: number, y: number, floorId?: FloorIds, num?: number): boolean;
getItem(
item: AllIdsOf<'items'> | number,
y: number,
floorId: FloorIds = this.floorId,
num: number = 1
): boolean {
if (!isNil(floorId) && typeof item === 'number') {
// 如果指定了坐标
const block = core.getBlock(item as number, y, floorId);
const id = block.event.id as AllIdsOf<'items'>;
const cls = core.material.items[id]?.cls;
if (cls === void 0) {
logger.warn(
15,
`Cannot get item of a non-item block on loc: ${item},${y},${floorId}`
);
return false;
}
return this.addItem(id, num!);
}
return this.addItem(item as AllIdsOf<'items'>, num!);
}
itemCount(item: AllIdsOf<'items'>): number {
return this.items.get(item) ?? 0;
}
hasItem(item: AllIdsOf<'items'>): boolean {
return this.itemCount(item) > 0;
}
toJSON(): string {
return '';
}
}

145
src/game/state/item.ts Normal file
View File

@ -0,0 +1,145 @@
import EventEmitter from 'eventemitter3';
import type { Hero } from './hero';
import { GameState, gameStates, IGameState } from './state';
import { loading } from '../game';
type EffectFn = (state: IGameState, hero: Hero<any>) => void;
type CanUseEffectFn = (state: IGameState, hero: Hero<any>) => boolean;
interface ItemStateEvent {
use: [hero: Hero<any>];
}
export class ItemState<
I extends AllIdsOf<'items'> = AllIdsOf<'items'>
> extends EventEmitter<ItemStateEvent> {
static items: Map<AllIdsOf<'items'>, ItemState> = new Map();
id: I;
cls: ItemClsOf<I>;
name: string;
text?: string;
hideInToolBox: boolean;
hideInReplay: boolean;
/** 即捡即用效果 */
itemEffect?: string;
/** 即捡即用道具捡过之后的提示 */
itemEffectTip?: string;
/** 使用道具时执行的事件 */
useItemEvent?: MotaEvent;
/** 使用道具时执行的代码 */
useItemEffect?: string;
/** 能否使用道具 */
canUseItemEffect?: string | boolean;
private noRoute: boolean = false;
itemEffectFn?: EffectFn;
useItemEffectFn?: EffectFn;
canUseItemEffectFn?: CanUseEffectFn;
constructor(id: I) {
super();
const items = items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a;
this.id = id;
const item = items[id];
this.cls = item.cls;
this.name = item.name;
this.text = item.text;
this.hideInToolBox = item.hideInToolBox;
this.hideInReplay = item.hideInReplay;
this.itemEffect = item.itemEffect;
this.itemEffectTip = item.itemEffectTip;
this.useItemEvent = item.useItemEvent;
this.useItemEffect = item.useItemEffect;
this.canUseItemEffect = item.canUseItemEffect;
this.compileFunction();
this.compileEvent();
}
private compileFunction() {
if (this.itemEffect) {
this.itemEffectFn = new Function(
`state`,
this.itemEffect
) as EffectFn;
}
if (this.useItemEffect) {
this.useItemEffectFn = new Function(
`state`,
this.useItemEffect
) as EffectFn;
}
if (this.canUseItemEffect) {
if (typeof this.canUseItemEffect === 'boolean') {
this.canUseItemEffectFn = () =>
this.canUseItemEffect as boolean;
} else {
this.useItemEffectFn = new Function(
`state`,
this.canUseItemEffect
) as CanUseEffectFn;
}
}
}
private compileEvent() {
// todo
}
/**
* 使
* @param state
* @param num 使tools和items有效
*/
use(hero: Hero<any>): boolean {
if (!this.canUse(hero)) return false;
if (!gameStates.now) return false;
const state = gameStates.now.state;
this.useItemEffectFn?.(state, hero);
if (this.useItemEvent) core.insertAction(this.useItemEvent);
if (!this.noRoute) state.route.push(`item:${this.id}`);
hero.addItem(this.id, -1);
this.emit('use', hero);
return true;
}
/**
* 使
* @param hero 使
*/
canUse(hero: Hero<any>, num: number = 1): boolean {
if (num <= 0) return false;
if (hero.itemCount(this.id) < num) return false;
if (!gameStates.now) return false;
return !!this.canUseItemEffectFn?.(gameStates.now.state, hero);
}
/**
* 使
*/
markNoRoute() {
this.noRoute = true;
}
/**
*
* @param id id
*/
static item<I extends AllIdsOf<'items'>>(id: I): ItemState<I> | undefined {
return this.items.get(id) as ItemState<I>;
}
}
loading.once('coreInit', () => {
for (const key of Object.keys(items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a)) {
ItemState.items.set(
key as AllIdsOf<'items'>,
new ItemState(key as AllIdsOf<'items'>)
);
}
});

91
src/game/state/state.ts Normal file
View File

@ -0,0 +1,91 @@
import { Undoable } from '@/core/interface';
import { Hero, HeroState } from './hero';
import EventEmitter from 'eventemitter3';
export interface ISerializable {
toJSON(): string;
}
export interface IGameState {
hero: MonoStore<Hero<any>>;
route: string[];
}
export class GameState implements ISerializable {
state: IGameState;
constructor(state: IGameState) {
this.state = state;
}
toJSON() {
return '';
}
static loadState(state: GameState) {}
static fromJSON(json: string) {}
static loadStateFromJSON(json: string) {}
}
interface MonoStoreEvent<T> {
change: [before: T | undefined, after: T | undefined];
}
export class MonoStore<T extends ISerializable>
extends EventEmitter<MonoStoreEvent<T>>
implements ISerializable
{
list: Map<string, T> = new Map();
using?: T;
use(id: string) {
const before = this.using;
this.using = this.list.get(id);
this.emit('change', before, this.using);
}
toJSON() {
return '';
}
}
interface StateStoreEvent {
undo: [state: GameState];
redo: [state: GameState];
change: [before: GameState | undefined, now: GameState];
}
class StateStore
extends EventEmitter<StateStoreEvent>
implements Undoable<GameState>
{
now?: GameState;
stack: GameState[] = [];
redoStack: GameState[] = [];
undo(): GameState | undefined {
const state = this.stack.pop();
if (!state) return void 0;
this.redoStack.push(state);
this.emit('undo', state);
return state;
}
redo(): GameState | undefined {
const state = this.redoStack.pop();
if (!state) return void 0;
this.stack.push(state);
this.emit('redo', state);
return state;
}
use(state: GameState) {
const before = this.now;
this.now = state;
this.emit('change', before, state);
}
}
export const gameStates = new StateStore();

View File

@ -20,7 +20,7 @@ import type { Range } from '@/plugin/game/range';
import type { KeyCode } from '@/plugin/keyCodes';
import type { Ref } from 'vue';
import type * as battle from './enemy/battle';
import type * as hero from './hero';
import type * as hero from './state/hero';
import type * as damage from './enemy/damage';
import type { Logger } from '@/core/common/logger';
import type { Danmaku } from '@/core/main/custom/danmaku';

View File

@ -94,6 +94,7 @@ interface Actions extends VoidedActionFuncs {
};
/**
* @deprecated
*
* @param action
* @param name 使
@ -108,6 +109,7 @@ interface Actions extends VoidedActionFuncs {
): void;
/**
* @deprecated
*
* @param action
* @param name
@ -115,6 +117,7 @@ interface Actions extends VoidedActionFuncs {
unregisterAction(action: ActionKey, name: string): void;
/**
* @deprecated
*
*/
doRegisteredAction<K extends ActionKey>(
@ -123,6 +126,7 @@ interface Actions extends VoidedActionFuncs {
): void;
/**
* @deprecated
* (_HX_ - 2, _HX_ + 2)
* @param x
*/

View File

@ -407,11 +407,13 @@ interface Control {
addGameCanvasTranslate(x: number, y: number): void;
/**
* @deprecated
*
*/
updateViewport(): void;
/**
* @deprecated
*
* @param px
* @param py
@ -419,6 +421,7 @@ interface Control {
setViewport(px?: number, py?: number): void;
/**
* @deprecated
* 西... + setViewport就完事了
* @param x
* @param y
@ -481,6 +484,7 @@ interface Control {
updateDamage(floorId?: FloorIds, ctx?: CtxRefer): void;
/**
* @deprecated
*
* @param ctx
*/

10
src/types/core.d.ts vendored
View File

@ -1261,11 +1261,6 @@ interface Main extends MainData {
*/
readonly __VERSION_CODE__: number;
readonly RESOURCE_INDEX: Record<string, string>;
readonly RESOURCE_URL: string;
readonly RESOURCE_SYMBOL: string;
readonly RESOURCE_TYPE: 'dev' | 'dist' | 'gh' | 'local';
/**
*
* @param mode
@ -1316,6 +1311,7 @@ interface Main extends MainData {
): void;
/**
* @deprecated
*
*/
setMainTipsText(text: string): void;
@ -1334,23 +1330,27 @@ interface Main extends MainData {
createOnChoiceAnimation(): void;
/**
* @deprecated
*
* @param index
*/
selectButton(index: number): void;
/**
* @deprecated
*
* @param fonts
*/
importFonts(fonts: FontIds[]): void;
/**
* @deprecated
*
*/
listen(): void;
/**
* @deprecated
* ts的插件转发
*/
forward(): void;