Skip to content

Instantly share code, notes, and snippets.

@Lyoko-Jeremie
Last active June 22, 2024 03:53
Show Gist options
  • Save Lyoko-Jeremie/158991db8d640b2de37690704cdd0d25 to your computer and use it in GitHub Desktop.
Save Lyoko-Jeremie/158991db8d640b2de37690704cdd0d25 to your computer and use it in GitHub Desktop.
为DoL Phone Mod编写的基于行为状态机的简易树算法(伪•伪代码)
import {uniq} from 'lodash';
export type PathConditionFunctionType = (si: StateInfo, ssi: StateSelectItem, ni: NpcInfo) => boolean;
export interface PhoneInnerState {
}
export interface StateSelectItem {
// == StateInfo::stateKey
parentsStateKey: string;
// a test function to test if need into this state (Active mode)
pathCondition:
string | // origin code string
// a function compile from the string with `new Function(code)`
// cache this for performance
PathConditionFunctionType;
nextStateKey: string;
// TODO other data .............
// player can select and send this message to npc ( this will into the Branch (Passive mode))
canSayString?: string;
// phone action [action string, action do function]
canDoSomething?: [string, string | (() => void)];
}
export function compilePathCondition(code: string): PathConditionFunctionType {
return new Function('si', 'ssi', 'pis', code) as PathConditionFunctionType;
}
export interface StateInfo {
// now state key
stateKey: string;
nextStateTable: Map<
// nextStateKey
string,
StateSelectItem
>;
// TODO other data .............
// npc will send message to player
sayString: string;
// npc will do something [action string, action do function]
doSomething: [string | undefined, string | (() => void)];
}
export interface ActiveEventStarter {
npcName: string;
// a test function to test if need start this state
pathCondition:
string |
((ni: NpcInfo) => boolean);
nextStateKey: string;
}
export interface NpcInfo {
npcName: string;
npcStateTable: Map<string, StateInfo>;
// when every new passage come / or other event
npcActiveEventStartKeyList: ActiveEventStarter[];
// this is for game save and load (can impl as other way)
userSelectedNextAnswer: string;
// now running state tree
nowStateKey: string;
// for multi state switch
// when switch to other state, save the old state key
// when switch back, restore the old state key
// when new passage come, check the nowStateKey , then check the backupStateKey
backupStateKey: string[];
}
;(async () => {
// test function
// < npcName-NpcStateTable< stateName-StateInfo > >
const allNpcTable: Map<
// npc name
string,
// npc state table
NpcInfo
> = new Map([
// all npc list
['Baily', {
npcName: 'Baily',
npcStateTable: new Map([
// every state of this npc
['startState', {
stateKey: 'startState',
nextStateTable: new Map([
['state1', {
parentsStateKey: 'startState',
pathCondition: 'return si.stateKey === "startState" && ssi.parentsStateKey === "startState" && ssi.nextStateKey === "state1"',
nextStateKey: 'state1',
} as StateSelectItem],
]),
} as StateInfo],
['state2', {
stateKey: 'state2',
nextStateTable: new Map([
['state3', {
parentsStateKey: 'state2',
pathCondition: 'return si.stateKey === "state2" && ssi.parentsStateKey === "state2" && xxx',
nextStateKey: 'state3',
} as StateSelectItem],
]),
} as StateInfo],
['state3', {
stateKey: 'state3',
nextStateTable: new Map(), // bla bla bla .......
} as StateInfo],
['rent-Start', {
stateKey: 'state3',
nextStateTable: new Map(), // bla bla bla .......
} as StateInfo],
]),
npcActiveEventStartKeyList: [
{
npcName: 'Baily',
pathCondition: 'return /* TODO new day come */',
nextStateKey: 'rent-Start',
} as ActiveEventStarter,
],
} as NpcInfo],
]);
const npcName = 'Baily';
const npcInfo = allNpcTable.get(npcName);
const npcTable = npcInfo?.npcStateTable;
const nowStateKey = npcInfo?.nowStateKey ?? '';
const nowState = npcTable?.get(nowStateKey);
let nextStateKey: string | undefined = undefined;
const nextStateTable = nowState?.nextStateTable;
if (nextStateTable) {
{ // active mode
for (const [k, ssi] of nextStateTable) {
// compile pathCondition and cache it
if (typeof ssi.pathCondition === 'string') {
ssi.pathCondition = compilePathCondition(ssi.pathCondition);
} else if (ssi.pathCondition(nowState, ssi, npcInfo!)) {
console.log('nextStateKey', ssi.nextStateKey);
nextStateKey = ssi.nextStateKey;
break;
}
}
}
{ // Passive mode
{ // mode 1, user only say
const canSayStringList = Array.from(nextStateTable.values()).map(ssi => ssi.canSayString).filter(s => !!s);
// TODO user to select in canSayStringList
// example
npcInfo!.userSelectedNextAnswer = canSayStringList[0] || '';
for (const [k, ssi] of nextStateTable) {
if (ssi.canSayString === npcInfo!.userSelectedNextAnswer) {
nextStateKey = ssi.nextStateKey;
break;
}
}
}
{ // mode 2, user do something
const canDoSomethingList = Array.from(nextStateTable.values()).map(ssi => ssi.canDoSomething).filter(s => !!s);
// TODO user to select in canDoSomethingList
// example
npcInfo!.userSelectedNextAnswer = canDoSomethingList[0]![0] || '';
for (const [k, ssi] of nextStateTable) {
if (ssi.canDoSomething && ssi.canDoSomething[0] === npcInfo!.userSelectedNextAnswer) {
nextStateKey = ssi.nextStateKey;
break;
}
}
}
}
}
if (!nextStateKey) {
// cannot find next state
// goto a default state OR show a ERROR message
return;
}
npcInfo!.nowStateKey = nextStateKey;
const nextState = npcTable?.get(nextStateKey)!;
// npc will say something
console.log(nextState.sayString);
// npc will do something
if (nextState.doSomething) {
// compile doSomething[1] and cache it
if (typeof nextState.doSomething[1] === 'string') {
nextState.doSomething[1] = new Function(nextState.doSomething[1]) as () => void;
}
// do and say
console.log(nextState.doSomething[0]);
nextState.doSomething[1]();
}
// ============== if new passage come ===================
{
const nInfo = npcInfo!;
for (const es of nInfo.npcActiveEventStartKeyList ?? []) {
// compile pathCondition and cache it
if (typeof es.pathCondition === 'string') {
es.pathCondition = new Function('pis', es.pathCondition) as (pis: PhoneInnerState) => boolean;
} else if (es.pathCondition(nInfo)) {
nInfo.backupStateKey.push(nInfo.nowStateKey);
nInfo.nowStateKey = es.nextStateKey;
}
}
// remove duplicate backupStateKey
nInfo.backupStateKey = uniq(nInfo.backupStateKey);
// first , run the all backupStateKey key (as above)
for (const bk of nInfo.backupStateKey) {
const bkState = nInfo.npcStateTable.get(bk);
if (bkState) {
// do something
}
}
// last , run the nowStateKey key
const nowState = nInfo.npcStateTable.get(nInfo.nowStateKey);
// do something
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment