-
-
Save Lyoko-Jeremie/158991db8d640b2de37690704cdd0d25 to your computer and use it in GitHub Desktop.
为DoL Phone Mod编写的基于行为状态机的简易树算法(伪•伪代码)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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