Monster AI Editing Tutorial - Ezekial711/MonsterHunterWorldModding GitHub Wiki

Understanding Monster AI Flow

The Basics

Monsters are generally in a specific state or situation. This state or situation determines their immediate actions. There are exceptions. Monsters outside of map boundaries or in transition areas will ignore everything to get back to a legal area. Within most normal circumstances (not including speedrunners holding a monster at HBG point on a corridor until it ignomiously dies like the Wayne family) a monster AI is governed by the relevant THK.

The mapping of situation to THK is controlled by the THKLst. When a monster switches context (voluntarily or involuntarily), the THKLst loads the relevant THK file and starts execution from the first node in the file.

When monsters are interrupted due to status or topple, or context switches it will always return to the first node in the relevant landing THK. There is no return to previous execution point.

Monsters execute actions as dictated by the THK. It's important to remember that actions have intrinsic properties, some are tied to the animation (LMT) entries they comprise, some are part of the executable (EXE) some are related to the Collision (COL) and some to the Shell (SHLP). Keep in mind sometimes some properties of an attack are actually produced by multiple actions. For example a monster turning to face the player and being able to rotate mid attack are actually two actions that are called in succession, a facing/turn action and the actual attack.

Monster action flow is defined by segments, an "instruction line". This are atomic code execution units (caveats apply). Which perform a task such as executing an action, conditional branching, setting an entry for a random chance table or returning.

Segments are grouped in nodes. When nodes perform jumps they do so at node level. Nodes are units of collective serial execution. Node internal execution is serial but they are not collectively serial. A node MUST CALL ANOTHER NODE at some point or execution will terminate until further interrupts or context switches occurr.

A THK is comprised of a list of nodes. A THKLst is an ordered list of THKs.

A Gentle Introduction

At a glance most monsters come with hefty lists of THKS, and it might seem daunting and even invasive to go in and globally edit a monster behaviour. In that sense, we might only want to spice up a specific quest, or a number of specific expeditions.

THKLst come to the rescue in that regard. A Custom Quest (MIB) can call a custom SOBJ for each monster. A SOBJ can in turn assign a unique THKLst to each monster it spawns. We can thus build a custom THKLst and follow this chain to have our edits only affect a specific monster on a specific quest. [TODO - Tutorialize, add pictures, example files, link templates *]

The format of the following sections use a combination of Nack's THK Editor, and Asterisk's Leviathon to illustrate and guide the user through the main concepts in monster programming. We provide code excerpts and sample files that can be included directly or with minimal effort in Leviathon source files. We link to the relevant Leviathon Reference entries when convenient, the read is advised to cover the relevant entries to understand the specifics about their mechanisms and wider uses. We mark sections as [Extended] when they depend on non-core features of the game and [Experimental] when they depend on non-core features of the Amsterdam Bucharest Compiler. We also will refer to Nack's THK GUI Editor when convenient as a debugging tool to verify results of the compilation process. [TODO - Links to all of the tools].

The compiler and decompiler have numerous features, not all of which are covered in this guide. Furthermore we might allude to language and compiler features but then promptly omit their use. This is because a lot of the features exist for the purpose of language and editing completion but should be irrelevant in practice (SHOULD BE, THK is still an open research topic as anything in the game). We attempt to provide the cleanest presentation of the language, understand this as, producing the most readable and mantainable code. Binary editing purists favour this features and out of respect for their craft and contributions the language supports this type of operations which the guide will discourage.

Setting Up the Project

Our experimental subject throughout this tutorial will be Behemoth. Behemoth has relatively simple non-combat THKs, a somewhat small set of possible actions and is also the monster I needed to work on. The Behemoth code seen here is part of ICE's Alexander's Behemoth Quest. [TODO - Link to ICE Server and ICE Repo?]

Preparing the MIB

Linking a SOBJ

[TODO - Make the guide about mib-sobj-thkl editing here with a sprinkle of alnk *]

Getting Familiar with the Instruments

Preliminaries

Now we can begin customizing our THKL. The first thing we are going to want to do is familiarize ourselves with working with the Amsterdam Bucharest Compiler. As its name indicates it compiles, however it also decompiles (maybe ABCD would have been a more appropiate name). The ABC can decompile THK files into NACK files and THKL into FAND Files.

When working with the Decompiler we'll want to deal with the files in proper chunk structure. That means we want to either use the files in the chunk as source, or close to chunk structure (for monsters, for example, with the "em/emYYY/0Z/data/emYYY.thklst" structure). The decompiler does not modify files, it creates new ones. We'll quickly cover how to run the decompiler here, a full explanation of all options can be found in the Leviathon Reference, though we recommend first finishing this guide before fully doing so. Alternatively you can call the compiler with the argument -h to display the help section. [TODO - Deecompiler Flags Reference, Screenshot of the help screen for the decompiler]

To find our monster ID we can either use the enumeration provided on the wiki on the sidebar [TODO - Link to Sidebar with monster file names] or we can use poedb list [TODO - Link to Poedb].

Using the Decompiler

To run the decompiler start by pressing Win+R, typing cmd and pressing Enter. A black window called the command line should have popped up. Drag the ABCompiler.exe to this window and it should automatically add it's full path to the console. Type one space and drag the THKLST to the console. It should also add its path. At this point we can start typing the options we want the decompiler to run with. Adding -outputPath followed by the path of a folder lets us specify where we want the decompiled files to end up. A very useful option is -analyze (and the accompanying -analysisOutputPath), it will provide a quick high level summary of the full project, we will be using some of the information produced by it. Because we are using the analysis tool we are going to also use -keepRegisters and -indeShow. This preserves some information which is not strictly necessary but allows us to use information from the analysis more effectively. You can run the decompiler by pressing enter at this point. [TODO - Picture of dragging the appropiate folder]

After it takes a brief moment to decompile your files should have arrived at the location you gave it (or the same folder if you didn't specify an output path). You will have a FAND file and one NACK file for every THK. If you used the analyze option you'll have an analysis file as well. Decompiling a THKL will also decompile all of the THK files linked to it in tandem and cross reference information across them during the procedure. Similarly compiling a FAND file will also compile all of the NACK files linked to it.

An Important Warning

DO NOT DECOMPILE A THK FILE IF YOUR GOAL IS TO EDIT MORE THAN ONE FILE IN THE PROJECT. DO NOT COMPILE A NACK FILE IF YOU ARE EDITING MORE THAN ONE FILE IN THE PROJECT. NEVER EDIT A GLOBAL THK/NACK OUTSIDE PROJECT MODE.

Dependencies between files are not preserved when editing more than one at a time, similarly do not decompile in project mode and compile back as individual files. In almost every case the compiler just won't let you because it will be missing information it expects from the rest of the project.

We can now begin editing the file.

Reading the Results

The first thing we are going to do is perform a full frontal lobotomy via FAND edit.

Our FAND file will look something like this, I've taken the liberty of omitting most of the entries we won't touch yet.

em121.fand

through em\em121\00\data
is Behemoth

Register RegisterVar0 as $B
Register RegisterVar1 as $C
Register RegisterVar2 as $J
Register RegisterVar3 as $U
Register RegisterVar4 as $V

Combat_Main = em121_00.nack @ 1131A51E
Combat_Enter = em121_01.nack @ B27CA6A0
...
Global = em121_55.nack @ 54F7F8EC

has 72 entries

A Quick Primer to Analysis Results.

Now is a good time to check on the results of the analysis. The analysis tool gives us quick a lot of information about the project. Orphan Nodes tells us which nodes in which files are not being called. This is useful if we wanted to clean up existing THKs as we can safely delete those nodes as they are not actually callable by the game in regular pathways (paths that follow the rules of THK execution outlined at the start).

The next section register usage tells us what registers are actually being used by each individual THK, this is why we kept registers (registers will be in alphabetical order regardless but this way it gives us confidence we are identifying the correct ones).

We get this report:

analysis.txt

====================
Register Usage
====================
Combat_Main:
	B: (Get,Set)
	C: (Get,Set)
	J: (Get,Set)
----------------
THK_04:
	B: (Get,Set)
	C: (Get,Set)
	J: (Get,Set)
----------------
Mount:
	C: (Set)
	U: (Get,Set)
----------------
Combat_Area_Change:
	B: (Get,Set)
	C: (Get)
	V: (Get,Set)
----------------
Combat_Blinded:
	C: (Get)
----------------
Global:
	B: (Get,Set)
	C: (Get,Set)
	J: (Get,Set)
	U: (Get,Set)
	V: (Get,Set)
----------------

Introducing Registers

Registers are how THKs can communicate with each other as well as preserve state. They arae the equivalent of assemby registers or global variables depending on your view. There are a finite amount of this, 21 in fact, 19 regular ones and 2 extended ones. Thanks to Leviathon the 21 have transparent functionality and the nomenclature reflect implementation details that won't be covered here. Registers support a limited number of operations (this functionality is significantly augmented by the ETL). For now we just care about what THK are setting data on them and which are reading.

We notice that register U is only used by Mounting and the Global THK. The Global THK because of its mode of operation will always have the union of all other THK operations (each THK report includes the register operations inside itself but also the operations inside nodes of Global which is calls). Thus the U register is fundamentally linked to mounting behaviour. We can read on our FAND file that THK 04 is pulling data from the same file as Combat Main, ergo we can also infer that register B is used for Combat Area Change communication with the main Combat process. Similarly register V is exclussive ot the Combat Area Change THK and J is internal to the CombatMain.

We can incorporate this facts on our source and strip them of the hardcoded letter identifier now. Leaving the letter identifier ensures that they map to the intended letter during compilation, but this is unnecessary as registers are freely swappable in terms of name.

em121.fand

through em\em121\00\data
is Behemoth

Register AreaChangeCommunication
Register GlobalVar0
Register CombatVar
Register MountVar
Register AreaChangeVar

Combat_Main = em121_00.nack @ 1131A51E
Combat_Enter = em121_01.nack @ B27CA6A0
...
Global = em121_55.nack @ 54F7F8EC

has 72 entries

We will need to go to our other source files and refactor the variable names appropiately. In Visual Studio, Fexty has provided an excellent language server plugin for Leviathon (the Leviathon Language Server, LLS from now on)[TODO - Tutorial on how to install it] [TODO - Tutorial on the refactor operation]. Alternatively if you are using a generic IDE you can do global replace at project scale or alternatively manually do such replacements.

Reading Actions

The next section of the analysis is Actions. Actions are executable classes which encapsulate a discrete monster action.

For those with experience working with the exe Actions have distinct VFT entries and DTI classes which can be string searched for (delete underscores to get the class name, append the monster's folder id as scope). Actions have a number of member functions including On_Execute (ran on action start), On_Update (ran every frame of the action), On_End (ran when the action finishes). All of this can be individually hooked. On_Execute additionally is in charge of playing the animation sequence involved with the action as well as manage all action start processes. On_Update handles hitboxes and triggering efx and monster status during the action (un-ko-able, easily toppled, etc). On_End performs cleanup and disables efx that should only run during the action.

For those without executable editing experience, actions are, well, monster actions. An action can contain multiple animation entries and describes a concrete event. For the most part we just use actions rather than go in and edit them so we are limited to the vocabular the monster already has. The analysis does not provide the full list of actions available to the monster (we can notice a few holes on the action list indices). Monsters can have actions not intended to be called directily and which only come as a consequence of a previous action (chains). They also have actions that you should NEVER call as they simply crash the game. This are normally intended to be interactions in specific contexts or prompted as a reaction to conditions on the environment (Kulu actions for when a Deviljho grabs it as a chew toy).

It DOES provide which actions are used by each THK, this gives us a pretty good guess on what each action does and what some of the unlabeled THKs might be about. Again Global provides the full list of actions that occur throughout the THKs. In this case we just make a mental note of which actions we have available at our disposal. Note that the LLS provides action suggestions from the complete list, so even if you are using that, it's recommendable to keep the analysis list handy to avoid calling unintended actions that might sound like what one is looking for but might be not intended for direct use.

Performing the First Edits

Customizing the THKL

At this point we have exhausted most of what we can do at first glance with the results of our analysis. Now we can focus on what we set out to do originally, make a unique combat AI entry.

As THK 04 is linked with Combat Main, we will also be editing it in simultaneous so to speak. We could make the conscious choice of giving it its own file to separate edits but we don't consider it necessary for the time being. For organization purposes we are going to add a new folder where we will keep our new thks. We create a folder (named "tu0Event") and drag the em121_00.nack file there. And we modify our FAND file appropriately. This will also be reflected at export time, producing the necessary folder on the output directory and correctly listing the path on the THKLST to include the added folder depth. [TODO - Indepth description of file path resolution and writing]

em121.fand

through em\em121\00\data
is Behemoth

Register AreaChangeCommunication
Register GlobalVar
Register CombatVar
Register MountVar
Register AreaChangeVar

Combat_Main = tu0Event/em121_00.nack @ 1131A51E
Combat_Enter = em121_01.nack @ B27CA6A0
...
THK_04 = tu0Event/em121_00.nack @ 1131A51E
...
Global = em121_55.nack @ 54F7F8EC

has 72 entries

If we wanted to split them (we don't), we would do:

Combat_Main = tu0Event/em121_00.nack @ 1131A51E
...
THK_04 = tu0Event/em121_04.nack @ 1131A51E

At this point we could pre-emptively declare a few more registers for future use. However notice that this isn't strictly necessary. It's a good practice of having a global view of registers, ideally with descriptive names. However the compiler can still determine the list of register namespaces during the analysis phase. If we need more registers we can always simply use them directly. However, when inter-THK communication is required, it's recommended to also note them on the FAND file. Additionally register variable hardcoding (setting a specific register variable to a specific letter) HAS to be done at FAND level. [TODO - Register Parsing Reference]

NACK File Imports

At first glance we are met by a relatively "short" file with some weird text:

em121_00.nack

importactions Behemoth as behemoth
importlibrary Global as Global

def node_000 @ 0
	self.targetEnemy(66) 
	>> Global.node_051 
	>> Global.node_049 
	>> Global.node_048 
	>> Global.node_053 
	>> Global.node_047 
	>> Global.node_052 
	>> Global.node_050 
	>> node_001 
	if self.target(4) 
		>> node_005 => reset 
	elif self.target(3) 
		>> node_005 => reset 
	elif self.target(55) 
		>> node_004 => reset 
	elif self.target(12) 
		>> node_006 => reset 
	elif self.target(14) 
		>> node_007 => reset 
	elif self.target(11) 
		>> node_008 => reset 
	elif self.target(0) 
		>> node_009 => reset 
	elif self.target(52) 
		>> node_012 => reset 
	elif self.vertical_distance_to_target().gt(450) 
		>> node_011 => reset 
	else 
		>> node_004 => reset 
	endif 
	reset 
endf 

def node_001 @ 2
	if function#5D() 
		function#5E() 
		self.targetEnemy(target_em.random_player_or_cat) 
		>> node_010 
		self.targetArea(4) 
	else 
		if function#103() 
			function#104() 
		else 
			self.targetEnemy(target_em.random_player_or_cat) 
		endif 
	endif 
	return 
endf 
...

At the top are the import declarations. This tell the file what namespaces/contexts it has access to. It imports calls from Global (whatever NACK file is declared as Global on the FAND file) and it imports actions from Behemoth. [TODO - Import Action Reference]

There's some additional namespaces that are always active, this are:

  • monster: Monster operates similar to the Behemoth import in that it determines what monster action list to use to convert text into action ids. However monster refers to the monster defined by the FAND file (you might have noticed the FAND file also had a monster import. This is useful for writing NACK files intended to be used in multiple projects with different monsters that share some actions (for example a NACK file just with behaviour for handling target changes while flying).
  • self: Specifies the start of a named function. We will cover more of this ahead but you'll notice node_000 has a few of those already. [TODO - Functions Reference]
  • Function Enums: Inside of function arguments (parenthesis) you might see different and varied scopes, this only exist for specific functions and are part of the FEXTY file specification [TODO - FEXTY File reference]
  • Caller: Tells a library file to look for a function or variable assignment on the body of the file that imported it (in programming terms it allows the notion of frameworks to be implemented in Leviathon). [TODO - Caller Reference]
  • Terminal: Tells a library file to look for a function or variable assignment on the body of the final file to import it. This is useful for import chains, where the desired behaviour should be that the one that started the chain is the one to provide some function or variable assignment to the library function. This is equivalent to Caller if the import is done by a top level module. [TODO - Terminal Reference]

While the language will allow you to import multiple monsters (nothing stops you from adding importactions Silver_Rathalos as slos) and it will correctly map their scopes to ids simultaneously. Ingame Behemoth will just use his own action ids, and as a result the actions you see won't be what you expect. Monsters can only use their own actions,

Usually, Global THKs have monster common behaviour that is used by multiple other THKs. While monster AI coding standards within Capcom are highly irregular, with numerous abnormalities product of subcontracting monster AI design, and wildly erratic function distribution patterns. It holds that in general, most of the monster action contexts lies within the Global THK while the individual THKs hold the flow of their application. In that sense one can think of the Global THK as an API to monster actions while the individual THKs only perform code flow and call the Global for corresponding actions.

In this tutorial however we will stray significantly from this. We will implement our own external library and draw from it instead of the Global THK. The reason we do this is that:

  1. Decompilation can't magically create function names, so we'd need to study the Global THK nodes to determine what each of them is intended to do or cover.
  2. Leviathon provides a better way of organizing code. The Global THK is colossal and has a lot of behaviour in one place, we can better spread our code around and document it more cleanly by managing several source files.
  3. We'd prefer not to have to change the Global THK if we needed to tweak behaviour as this would affect all other THKs besides the one we are working on.

If you are performing small edits or only wish to make smaller behaviour changes, then anytime we implement functions in external libraries, you can add them to the Global THK instead and invoke them from there. Or simply implement them and call them locally.

Architectural Design

We take this opportunity to make an aside on THK architectural design. By their very nature THK are simply procedural instructions, meaning they are a sequential execution with branching and some loops. This naturally pushes us to build AI trees. However trees will result in significant repetition as monsters must check distances and target validity constantly as well. Furthermore interrupts cause significant issues on this design as they forcefully reset the tree to the root. Trees in turn must eventually end and loop back (otherwise the monster will stand in one place looking at infinity).

If one is setting out to build a monster AI from scratch or even simply editing existing AI it's important to understand the existing archiectural approaches. In this section we present Capcom's AI Design, an alternative (which we don't claim is superior, simply different) and then explain the concept of frameworks and how they can be incorporated through Leviathon.

Element Catalogue

In the following diagram we will use a loose graphical language to graph AI patterns. Light orange nodes indicate entry points, either to THKs or to decision trees. Black nodes correspond to actions or internal deterministic processing. Blue nodes correspond to choice processing and decision taking. Red nodes represent return points where the tree connects back to some earlier state to continue from the line after some jump.

Nodes in the diagram do not represent a single THK node, they are groupings of them and are meant to be abstract collections of node which when seen together can be tied with an abstract behaviour that the diagram wishes to convey. THK Boundaries are given by the background areas. Overlap between boxes is used to convey that in some cases this functions might be in one THK or another.

Capcom AI Design

Capcom's AI template is not followed strictly. Several monsters have very different AI function distributions, and some might are absolute unorganized messes. The following design is what can be extracted as their most coherent pattern and can be found in a respectable amount of monsters, specifically those whose AI has moderately competent structure. Such an example is Pukei-Pukei. In constrast some monsters are unorganized crimes against humanity that hedge everything into a single file with very little logic distribution or organization, such as Safi'Jiiva.

At the core of it there's a main execution loop which deals with monster incidentals, such as the need to change areas, limp back to nest, etc. Mostly concerned with monster specific general transition behaviour. all of this nodes call a target controller node before advancing. The target controller identifies any immediately relevant actions, such as cheap shot when the player is paralyzed or stunned. Additionally it updates targets such as deciding which target to focus (monster, player or cat) as well as area leashes (check if the monster is inside the area it's navigation path sent it).

If the monster has unique actions (auras, novas, etc) or flight capabilities it goes into a segment of the code that handles taking-off and landing as well as nova and aura states. Regardless both continue into a choice of action ranges, which in turn is followed by a state check.

The state check determines if it should follow enraged/fatigued/normal behaviour. Following this it picks an action group. An Action Group or Combo is a tree of actions. Individually this nodes MIGHT have branching, but it's mostly relative to choices of variations of a move. For example checking player position relative to monster to determine if it should use a right or left side jab. Big decisions such as if to use a series of ranged move or perform an approach combo are done at the action range section. Once a combo starts, very rarely will a monster switch to a completely different paradigm (from melee jabs to long range lunges responding to player position change). When this trees finish they go back to the main loop which advances to the next node in the sequence.

This logic also holds for non-combat THKs, the combat actions are simply replaced with environmental interaction actions.

Pros:

  • The design is ammenable to combat interruptions. The main loop is mostly agnostic to which point one is inside of it, there is no need to resume.
  • The behaviour "feels organic". For the most part there are very few long term identifiable patterns outside the possible action groups.
  • States that need resuming are limited and thus register usage is low. Additionally resuming only requires tracking register values against a pre-set cap (such as flight ending after a specific number of actions).
  • The design is friendly to the low-level implementation of the THK system. Cons:
  • Monster behaviour is fundamentally purely random. There are no patterns to learn that the player can read. Learning the monster is learning the atomic moves and stops there.
  • Combat flow interruptions are meaningless in general, there is no point in planning topples or CC since the monster state is mostly irrelevant.
  • Resumable behaviour is simply resuming a state, not a behaviour , as a result, it prolongs undesirable states without actually allowing the stop of undesirable actions.

In summary, it makes the monsters more random, and reduces player agency. It is however very architecturally stable and direct to program. The pattern additionally is amenable to more than just combat and in general is how most monsters in the game are set up.

Stateful Design

The following is the design approach that this tutorial will be following. We select a stateful approach where the monster has modes. Modes can transition to (logically) neighboring modes during execution based on conditions. Actions still have the sub-selection paradigm from Capcom design where small variations (such as right and left pawswipe dependent on player position), but the general distance and state selection is instead used to define the mode changes. Additionally modes are stateful (within some consideration). We have an opening and ending mode, which are designed as fight opener and closer. We could abstract this as simply other modes but we keep them separate as they have different transition dynamics.

To handle interrupts we need a way to resume and to keep track of modes. This is possible within the THK language. However using Leviathon and the ETL we can more efficiently (in terms of execution time) and cleanly (in terms of code) include this capabilities.

This design pattern is much better suited for combat than for non-combat situation, in our case this isn't a major consideration however it's something to keep in mind if you intend to use it outside this context.

Pros:

  • Monster behaviour is structured. It's possible to learn and exploit patterns through repeated play.
  • It enables player agency. It's possible to nudge behaviour in specific directions with guaranteed payout for the risk and choices taken.
  • It's possible to limit degenerate behaviour and implement more complex sequential logic.
  • Combat flow interruptions are meaningful. It's possible to use information from interruptions and incorporate them into the operation. Cons:
  • The design is register heavy. There's a need to track and keep states. Register operations on the base language are limited.
  • Low-level implementation is extremely cumbersome and quickly becomes extremely complex. It's not amenable to hand edits.
  • The behaviour might feel inorganic, and scripted. Within some bounds there is more determinism in how monsters operate. Notes:
  • The limitations in terms of implementation are irrelevant when working on a higher level language like Leviathon.

The Framework Pattern

This is not strictly speaking an AI pattern but mostly a software design pattern. While it's not significant to our choice of Stateful Design, it's a significant paradigm that Leviathon design enables.

A framework pattern is a library design pattern where, instead of accessing a library for concrete functions, the library produces the code flow structure and we simply provide the concrete elements that the library needs. A framework is an inversion of the traditional library pattern. The value of frameworks comes from them being able to abstract code flow notions. In the case of monster AI, it allows us to reuse libraries that only deal with code flow. In fact we can combine implementation libraries with code flow libraries (with the help of the wrapper pattern).

For example we can convert the traditional Capcom Monster AI Design to an application of the Framework Pattern.

The implementation of Frameworks on Leviathon is done through the Monster, Caller and Terminal Scopes. Whenever we need behaviour to be provided by the file that imports our framework we can use the Caller scope and document that, so anyone that uses our framework knows to implement targets for those calls. Additionally we can use the Terminal scope when we expect our library or framework to be within nested imports by other libraries or frameworks but we only care about the start of the import chain.

The monster scope is simillar to Caller or Terminal. It enables us to specify generic actions and the start of the import chain to provide the actual monster which will use the action. This is useful for generic code flow that still needs monster interaction, such as Fly and Landing actions.

In traditional THK Design our code might look like this:

THK_00.nack

def main
	>> target_update
endf

def target_update
	... // Target Update Code
	>> action_range_choice
endf

def action_range_choice
	chance(20)
		>> action_range1
	elsec (40)
		>> action_range2
	elsec (40)
		>> action_range3
	endc
	return
endf

def action_range1
	chance(20)
		>> check_state1
	elsec (40)
		>> check_state2
	elsec (40)
		>> check_state3
	endc
	return
endf

def check_state1
	if self.state1
		>> Global.action1
	elif self.state2
		>> Global.action2
	else
		>> Global.action3
	endif
endf

...

In terms of framework code it'd look like this

THK_00.nack

import "actionFramework.nack" as Fwk
import Global as Global

LONGRANGE = 1500
MIDRANGE = 500

HIGH = 60
MID = 40
LOW = 20

def main
	>> Fwk.start
endf

def action1
	Global.action1
endf 

def action2
	Global.action2
endf 

def action3
	Global.action3
endf 

...

actionFramework.nack

def start
	>> target_update
endf

def target_update
	if self.distance2d()leq(Caller.MIDRANGE)
		-> monster.move
	elif self.distance2d()leq(Caller.LONGRNAGE)
		self.shuffle_target()
	>> action_range_choice
endf

def action_range_choice
	chance(Caller.LOW)
		>> action_range1
	elsec (Caller.MID)
		>> action_range2
	elsec (Caller.MID)
		>> action_range3
	endc
	return
endf

def action_range1
	chance(Caller.LOW)
		>> check_state1
	elsec (Caller.MID)
		>> check_state2
	elsec (Caller.MID)
		>> check_state3
	endc
	return
endf

def check_state1
	if self.state1
		>> Caller.action1
	elif self.state2
		>> Global.action2
	else
		>> Global.action3
	endif
endf

It might not look like a big improvement, but moving the code to a framework allows us to reuse this code on other monsters. The more flexible we make the framework and the more complex options we include the more we can power future edits using existing code.

Library Imports

My design approach in this case (and for pedagogical effect) is going to make 9 "modes" for the monster. Depending on the mode it will react differently to different player events. It will transition between modes under certain conditions. Of this 9 modes, 1 will be reserved for the battle opening and one for the end of the battle. So from the start I already know I want to be importing 9 library files. In turn I will implement an additional layer below this one to handle common functionality between modes. In particular I'll have 5 broad categories of actions which will be consumed by the "Mode" libraries.

We allow modes to transition between each other as per the upper links on the diagram. With the addition that under the right conditions any mode can transition to BLU, OP or CL (Blue Mage, Opening and Close). And similarly they depend on the underlying libraries bellow them.

While at it, I'll also delete almost everything in the Combat Main NACK. If I need to sample code I'll do so deliberately from a copy of the file. But in general I don't want any of the original Capcom for this project outside maybe some calls to the Global THK.

We can import external libraries (libraries not defined on the FAND file) by specifying their path (absolute or relative to the fand file). External libraries can in turn similarly import other libraries themselves (recursively up to any depth, as long as there are no cyclical dependencies). As we develop the monster's AI we might notice that some libraries depend on common behaviour and we might either want to add an additional deeper layer where multiple of this libraries will pull, or alternatively we might want to invert dependencies and have the libraries instead pull that behaviour from the NACK file that calls them. [TODO - Import Library Reference]

em121_00.nack

importactions Behemoth as behemoth
importlibrary Global as Global
importlibrary "./tu0Event/CombatOpening.nack" as opening
importlibrary "./tu0Event/CombatClosing.nack" as closing
importlibrary "./tu0Event/CombatWhiteMage.nack" as whm
importlibrary "./tu0Event/CombatBlackMage.nack" as blm
importlibrary "./tu0Event/CombatRedMage.nack" as rdm
importlibrary "./tu0Event/CombatMonk.nack" as mnk
importlibrary "./tu0Event/CombatPaladin.nack" as pld
importlibrary "./tu0Event/CombatDragoon.nack" as drg
importlibrary "./tu0Event/CombatBlueMage.nack" as blu

def node_000
	repeat
endf