mirror of
https://git.planet-casio.com/Slyvtt/Collab_RPG.git
synced 2024-12-28 04:23:42 +01:00
npc : Implémentation théorique de la machine d'état
This commit is contained in:
parent
e37bf141c2
commit
5842aa905e
7 changed files with 234 additions and 167 deletions
|
@ -334,8 +334,8 @@ def convert_map(input: str, output: str, params: dict, target):
|
|||
npc_struct += fxconv.u8(0) # TODO: Group
|
||||
npc_struct += fxconv.u8(0) # Hostile to - leave as
|
||||
npc_struct += fxconv.u8(0) # state - leave as
|
||||
npc_struct += fxconv.u8(0) # Padding
|
||||
npc_struct += fxconv.u16(0) # Padding
|
||||
npc_struct += fxconv.u16(0) # target - leave as
|
||||
npc_struct += fxconv.u8(0) # padding
|
||||
map_struct += fxconv.ptr(npc_struct)
|
||||
# Load signs
|
||||
map_struct += fxconv.u32(len(signs))
|
||||
|
|
|
@ -282,13 +282,13 @@ void game_get_inputs(Game *game) {
|
|||
mynpc->xpath = NULL;
|
||||
mynpc->ypath = NULL;
|
||||
}
|
||||
while(keydown(KEY_F1)){
|
||||
while(keydown(KEY_F1)) {
|
||||
clearevents();
|
||||
}
|
||||
}
|
||||
if(keydown(KEY_F2)) {
|
||||
npc_remove_pos(0);
|
||||
while(keydown(KEY_F2)){
|
||||
while(keydown(KEY_F2)) {
|
||||
clearevents();
|
||||
}
|
||||
}
|
||||
|
|
46
src/game.h
46
src/game.h
|
@ -3,10 +3,34 @@
|
|||
|
||||
#include "animation.h"
|
||||
#include "events.h"
|
||||
// #include "npc.h"
|
||||
|
||||
#include <gint/display.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum {
|
||||
NPC_Static = 0, /*~= none, disqualifies from all AI*/
|
||||
NPC_Guard = 1,
|
||||
NPC_Bandit = 2,
|
||||
NPC_Monster = 3,
|
||||
NPC_Type_Count
|
||||
};
|
||||
|
||||
enum {
|
||||
NPC_T_NONE = 0,
|
||||
NPC_T_FRIENDLY = 1, /* The player's team */
|
||||
NPC_T_HOSTILE = 2, /* to the player */
|
||||
NPC_T_ALL = 3,
|
||||
NPC_T_Count
|
||||
};
|
||||
|
||||
enum {
|
||||
NPC_S_IDLE = 0,
|
||||
NPC_S_ATTACK = 1,
|
||||
NPC_S_FLEE = 2,
|
||||
NPC_S_WANDER = 3
|
||||
};
|
||||
|
||||
/* The direction where the player is going to. */
|
||||
typedef enum {
|
||||
D_UP,
|
||||
|
@ -53,9 +77,9 @@ typedef struct {
|
|||
weapon. */
|
||||
Slot equipped[3];
|
||||
/* 1 if the inventory is open. */
|
||||
char open : 1;
|
||||
char selected;
|
||||
char selection;
|
||||
int8_t open : 1;
|
||||
int8_t selected;
|
||||
int8_t selection;
|
||||
} Inventory;
|
||||
|
||||
typedef struct {
|
||||
|
@ -157,12 +181,20 @@ typedef struct {
|
|||
uint8_t hostile_to_group;
|
||||
/*state should be one of NPC_S*/
|
||||
uint8_t state;
|
||||
uint8_t __temp;
|
||||
/*Should be one of NPC_ - 0 if none*/
|
||||
uint16_t target;
|
||||
|
||||
/* uint16_t to keep the struct aligned */
|
||||
uint16_t __padding;
|
||||
/* to keep the struct aligned */
|
||||
uint8_t __padding;
|
||||
} NPC;
|
||||
|
||||
/*Map wide NPC info*/
|
||||
typedef struct {
|
||||
|
||||
uint16_t team_strength[NPC_T_Count];
|
||||
|
||||
} NPC_AI_Dat;
|
||||
|
||||
typedef struct {
|
||||
Collider collider;
|
||||
/* The destination portal */
|
||||
|
@ -231,6 +263,8 @@ typedef struct {
|
|||
Animation npc_animation;
|
||||
Inventory inventory;
|
||||
|
||||
NPC_AI_Dat npc_ai_dat;
|
||||
|
||||
int mana; /* Only for testing events TODO: Remove this! */
|
||||
} Game;
|
||||
|
||||
|
|
112
src/inventory.c
112
src/inventory.c
|
@ -22,7 +22,7 @@ void inventory_init(Inventory *inventory) {
|
|||
}
|
||||
|
||||
void inventory_draw(Inventory *inventory, Player *player) {
|
||||
size_t i;
|
||||
int i;
|
||||
if(inventory->open) {
|
||||
dimage(0, 0, &inventory_img);
|
||||
for(i = 0; i < SLOT_NUM; i++) {
|
||||
|
@ -76,23 +76,23 @@ void inventory_move_from_selected(Inventory *inventory) {
|
|||
inventory->slots[inventory->selected] = current;
|
||||
}
|
||||
|
||||
void inventory_use(Inventory *inventory, Player *player) {
|
||||
void inventory_use(Inventory *inventory, GUNUSED Player *player) {
|
||||
Item item = inventory->slots[inventory->selection].i;
|
||||
switch(item_types[item]) {
|
||||
case IT_TALISMAN:
|
||||
inventory->equipped[0] = inventory->slots[inventory->selection];
|
||||
break;
|
||||
case IT_ARMOR:
|
||||
inventory->equipped[1] = inventory->slots[inventory->selection];
|
||||
break;
|
||||
case IT_WEAPON:
|
||||
inventory->equipped[2] = inventory->slots[inventory->selection];
|
||||
break;
|
||||
case IT_FOOD:
|
||||
/* TODO */
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case IT_TALISMAN:
|
||||
inventory->equipped[0] = inventory->slots[inventory->selection];
|
||||
break;
|
||||
case IT_ARMOR:
|
||||
inventory->equipped[1] = inventory->slots[inventory->selection];
|
||||
break;
|
||||
case IT_WEAPON:
|
||||
inventory->equipped[2] = inventory->slots[inventory->selection];
|
||||
break;
|
||||
case IT_FOOD:
|
||||
/* TODO */
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
inventory->slots[inventory->selection].i = I_NONE;
|
||||
inventory->slots[inventory->selection].durability = 255;
|
||||
|
@ -102,50 +102,50 @@ void inventory_unequip(Inventory *inventory, ItemType type) {
|
|||
if(inventory->slots[inventory->selection].i)
|
||||
return;
|
||||
switch(type) {
|
||||
case IT_TALISMAN:
|
||||
inventory->slots[inventory->selection] = inventory->equipped[0];
|
||||
inventory->equipped[0].i = I_NONE;
|
||||
inventory->equipped[0].durability = 255;
|
||||
break;
|
||||
case IT_ARMOR:
|
||||
inventory->slots[inventory->selection] = inventory->equipped[1];
|
||||
inventory->equipped[1].i = I_NONE;
|
||||
inventory->equipped[1].durability = 255;
|
||||
break;
|
||||
case IT_WEAPON:
|
||||
inventory->slots[inventory->selection] = inventory->equipped[2];
|
||||
inventory->equipped[2].i = I_NONE;
|
||||
inventory->equipped[2].durability = 255;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case IT_TALISMAN:
|
||||
inventory->slots[inventory->selection] = inventory->equipped[0];
|
||||
inventory->equipped[0].i = I_NONE;
|
||||
inventory->equipped[0].durability = 255;
|
||||
break;
|
||||
case IT_ARMOR:
|
||||
inventory->slots[inventory->selection] = inventory->equipped[1];
|
||||
inventory->equipped[1].i = I_NONE;
|
||||
inventory->equipped[1].durability = 255;
|
||||
break;
|
||||
case IT_WEAPON:
|
||||
inventory->slots[inventory->selection] = inventory->equipped[2];
|
||||
inventory->equipped[2].i = I_NONE;
|
||||
inventory->equipped[2].durability = 255;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void inventory_move_selection(Inventory *inventory, Direction direction) {
|
||||
switch(direction) {
|
||||
case D_UP:
|
||||
inventory->selection -= SLOT_COLUMNS;
|
||||
if(inventory->selection < 0)
|
||||
inventory->selection = 0;
|
||||
break;
|
||||
case D_DOWN:
|
||||
inventory->selection += SLOT_COLUMNS;
|
||||
if(inventory->selection >= SLOT_NUM) {
|
||||
inventory->selection = SLOT_NUM - 1;
|
||||
}
|
||||
break;
|
||||
case D_LEFT:
|
||||
if(inventory->selection > 0) {
|
||||
inventory->selection--;
|
||||
}
|
||||
break;
|
||||
case D_RIGHT:
|
||||
if(inventory->selection < SLOT_NUM - 1) {
|
||||
inventory->selection++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case D_UP:
|
||||
inventory->selection -= SLOT_COLUMNS;
|
||||
if(inventory->selection < 0)
|
||||
inventory->selection = 0;
|
||||
break;
|
||||
case D_DOWN:
|
||||
inventory->selection += SLOT_COLUMNS;
|
||||
if(inventory->selection >= SLOT_NUM) {
|
||||
inventory->selection = SLOT_NUM - 1;
|
||||
}
|
||||
break;
|
||||
case D_LEFT:
|
||||
if(inventory->selection > 0) {
|
||||
inventory->selection--;
|
||||
}
|
||||
break;
|
||||
case D_RIGHT:
|
||||
if(inventory->selection < SLOT_NUM - 1) {
|
||||
inventory->selection++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
49
src/main.c
49
src/main.c
|
@ -33,32 +33,33 @@ extern Map *worldRPG[];
|
|||
|
||||
/* Game data (defined in "game.h")*/
|
||||
Game game = {
|
||||
NULL,
|
||||
{12 * PXSIZE,
|
||||
36 * PXSIZE,
|
||||
0,
|
||||
0,
|
||||
100,
|
||||
SPEED,
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
{}},
|
||||
{{}, {}, 0},
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
.map_level = NULL,
|
||||
.player = {.x = 12 * PXSIZE,
|
||||
.y = 36 * PXSIZE,
|
||||
.px = 0,
|
||||
.py = 0,
|
||||
.life = 100,
|
||||
.speed = SPEED,
|
||||
.canDoSomething = false,
|
||||
.whichAction = 0,
|
||||
.isDoingAction = false,
|
||||
.isInteractingWithNPC = false,
|
||||
.is_male = true,
|
||||
.animation = {}},
|
||||
.handler = {{}, {}, 0},
|
||||
.exittoOS = false,
|
||||
.screenshot = false,
|
||||
.record = false,
|
||||
.frame_duration = 0,
|
||||
|
||||
/* debug variables*/
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
{},
|
||||
{},
|
||||
100,
|
||||
.debug_map = false,
|
||||
.debug_player = false,
|
||||
.debug_extra = false,
|
||||
.npc_animation = {},
|
||||
.inventory = {},
|
||||
.npc_ai_dat = {.team_strength = {0, 0, 0, 0}},
|
||||
.mana = 100,
|
||||
};
|
||||
|
||||
/* screen capture management code. TODO: Clean this up! */
|
||||
|
|
154
src/npc.c
154
src/npc.c
|
@ -20,29 +20,25 @@ extern bopti_image_t tiny_npc_police;
|
|||
|
||||
NPC_TypeData npc_typedat[NPC_Type_Count] = {
|
||||
/*NPC_Static*/
|
||||
{0, 0, 0, 0},
|
||||
{0, 0, 0, 0, 0},
|
||||
/*NPC_Guard*/
|
||||
{
|
||||
.agressivity = 75,
|
||||
.cowardice = 85,
|
||||
.lazyness = 50,
|
||||
.wanderlust = 150
|
||||
},
|
||||
{.presence = 8,
|
||||
.agressivity = 75,
|
||||
.cowardice = 85,
|
||||
.lazyness = 50,
|
||||
.wanderlust = 150},
|
||||
/*NPC_Bandit*/
|
||||
{
|
||||
.agressivity = 110,
|
||||
.cowardice = 85,
|
||||
.lazyness = 40,
|
||||
.wanderlust = 60
|
||||
},
|
||||
{.presence = 6,
|
||||
.agressivity = 110,
|
||||
.cowardice = 85,
|
||||
.lazyness = 40,
|
||||
.wanderlust = 60},
|
||||
/*NPC_Monster*/
|
||||
{
|
||||
.agressivity = 170,
|
||||
.cowardice = 50,
|
||||
.lazyness = 40,
|
||||
.wanderlust = 80
|
||||
}
|
||||
};
|
||||
{.presence = 10,
|
||||
.agressivity = 170,
|
||||
.cowardice = 50,
|
||||
.lazyness = 40,
|
||||
.wanderlust = 80}};
|
||||
|
||||
NPC npc_stack[NPC_STACK_SIZE];
|
||||
uint32_t npc_count;
|
||||
|
@ -84,10 +80,10 @@ void npc_remove_pos(uint32_t pos) { npc_remove(&npc_stack[pos]); }
|
|||
|
||||
/*Takes input in curx/cury*/
|
||||
/*Incredibely jank*/
|
||||
bool npc_collision(Game *game, NPC *npc, int32_t dx, int32_t dy) {
|
||||
bool npc_collision(Game *game, int32_t dx, int32_t dy) {
|
||||
|
||||
int on_walkable = map_get_walkable(game, (dx>>PRECISION) / T_WIDTH,
|
||||
(dy>>PRECISION) / T_HEIGHT);
|
||||
int on_walkable = map_get_walkable(game, (dx >> PRECISION) / T_WIDTH,
|
||||
(dy >> PRECISION) / T_HEIGHT);
|
||||
|
||||
int speed = (on_walkable >= 0 && on_walkable < WALKABLE_TILE_MAX)
|
||||
? walkable_speed[on_walkable]
|
||||
|
@ -140,11 +136,11 @@ void as_clean(uint8_t *visited, uint8_t *gscore, uint8_t *fscore) {
|
|||
|
||||
/*Takes input as pixel position*/
|
||||
int npc_pathfind(int32_t dest_x, int32_t dest_y, Map *full_map, NPC *npc) {
|
||||
uint8_t *map = full_map->walkable;
|
||||
// uint8_t *map = full_map->walkable;
|
||||
uint32_t w = full_map->w;
|
||||
uint32_t h = full_map->h;
|
||||
uint32_t sx = npc_from_curxy(npc->curx);
|
||||
uint32_t sy = npc_from_curxy(npc->cury);
|
||||
// uint32_t sx = npc_from_curxy(npc->curx);
|
||||
// uint32_t sy = npc_from_curxy(npc->cury);
|
||||
dest_x /= PXSIZE;
|
||||
dest_y /= PXSIZE;
|
||||
|
||||
|
@ -167,8 +163,8 @@ int npc_pathfind(int32_t dest_x, int32_t dest_y, Map *full_map, NPC *npc) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int as_reconstruct_path(int16_t *came_from, int w, int h, int16_t start,
|
||||
int16_t dest, bool is_alloc, NPC *npc) {
|
||||
int as_reconstruct_path(int16_t *came_from, int w, int16_t start, int16_t dest,
|
||||
bool is_alloc, NPC *npc) {
|
||||
if(npc_clear_path(npc))
|
||||
goto as_recons_fail;
|
||||
|
||||
|
@ -208,8 +204,8 @@ int as_reconstruct_path(int16_t *came_from, int w, int h, int16_t start,
|
|||
npc->ypath[npc->path_length - i - 1] = ty;
|
||||
}*/
|
||||
|
||||
/*if(is_alloc)
|
||||
free(came_from);*/
|
||||
if(is_alloc)
|
||||
free(came_from);
|
||||
|
||||
npc->hasPath = true;
|
||||
|
||||
|
@ -320,7 +316,7 @@ int __npc_pathfind(int32_t dest_x, int32_t dest_y, Map *full_map, NPC *npc) {
|
|||
if(bx == dest_x && by == dest_y) {
|
||||
if(is_alloc)
|
||||
as_clean(visited, gscore, fscore);
|
||||
return 0; /*as_reconstruct_path(came_from, w, h, spos,
|
||||
return 0; /*as_reconstruct_path(came_from, w, spos,
|
||||
dest_y * w + dest_x, is_alloc npc)*/
|
||||
}
|
||||
|
||||
|
@ -361,6 +357,13 @@ int __npc_pathfind(int32_t dest_x, int32_t dest_y, Map *full_map, NPC *npc) {
|
|||
// Refactoring to make adding complexity cleaner
|
||||
void update_npcs(Game *game) {
|
||||
uint32_t i;
|
||||
for(i = 0; i < NPC_T_Count; i++)
|
||||
game->npc_ai_dat.team_strength[i] = 0;
|
||||
for(i = 0; i < npc_count; i++) {
|
||||
uint16_t type = npc_stack[i].type;
|
||||
game->npc_ai_dat.team_strength[type] += npc_typedat[type].presence;
|
||||
}
|
||||
|
||||
for(i = 0; i < game->map_level->nbNPC; i++) {
|
||||
update_npc(&game->map_level->npcs[i], game);
|
||||
}
|
||||
|
@ -369,35 +372,83 @@ void update_npcs(Game *game) {
|
|||
}
|
||||
}
|
||||
|
||||
void npc_ai(NPC *npc, Game *game){
|
||||
/*Returns NULL if none are spotted*/
|
||||
NPC *enemy_spotted(NPC *npc, Game *game) { return NULL; }
|
||||
|
||||
#define AI_CHANCE_MAX (0xFFFF * 0xFF)
|
||||
#define AI_RAND() (rand() & 0xFFFF)
|
||||
|
||||
void npc_ai(NPC *npc, Game *game) {
|
||||
NPC_TypeData *tdat = &npc_typedat[npc->type];
|
||||
NPC_AI_Dat *aidat = &game->npc_ai_dat;
|
||||
|
||||
/*Graph of the logic availiable in ticket #39*/
|
||||
|
||||
uint32_t idle_chance = 0;
|
||||
uint32_t wander_chance = 0;
|
||||
uint32_t flee_chance = 0;
|
||||
uint32_t attack_chance = 0;
|
||||
uint64_t temp;
|
||||
|
||||
switch(npc->state){
|
||||
case NPC_S_IDLE : {
|
||||
/*TODO : Expand conditions to switch states*/
|
||||
/* Fixed point ratio*/
|
||||
uint32_t relative_strength =
|
||||
(aidat->team_strength[npc->current_group] << PRECISION) /
|
||||
aidat->team_strength[npc->hostile_to_group];
|
||||
|
||||
break;
|
||||
}
|
||||
case NPC_S_WANDER : {
|
||||
/*TODO : Choose a random close point to pathfind to */
|
||||
break;
|
||||
}
|
||||
case NPC_S_FLEE : {
|
||||
/*TODO : Pathfind to a point away from the player*/
|
||||
break;
|
||||
}
|
||||
case NPC_S_ATTACK : {
|
||||
/*TODO : Attack !*/
|
||||
break;
|
||||
}
|
||||
default : {
|
||||
NPC *spotted = enemy_spotted(npc, game);
|
||||
|
||||
switch(npc->state) {
|
||||
default: {
|
||||
/*Get real*/
|
||||
npc->state = NPC_S_IDLE;
|
||||
__attribute__((fallthrough));
|
||||
}
|
||||
case NPC_S_IDLE: {
|
||||
if(spotted) {
|
||||
npc->target = spotted->type;
|
||||
npc->state = NPC_S_ATTACK;
|
||||
break;
|
||||
}
|
||||
wander_chance = AI_RAND() * tdat->wanderlust;
|
||||
idle_chance = AI_CHANCE_MAX - wander_chance;
|
||||
break;
|
||||
}
|
||||
case NPC_S_WANDER: {
|
||||
/*TODO : Choose a random close point to pathfind to */
|
||||
|
||||
if(spotted) {
|
||||
npc->target = spotted->type;
|
||||
npc->state = NPC_S_ATTACK;
|
||||
break;
|
||||
}
|
||||
idle_chance = AI_RAND() * tdat->lazyness;
|
||||
wander_chance = AI_CHANCE_MAX - idle_chance;
|
||||
|
||||
break;
|
||||
}
|
||||
case NPC_S_FLEE: {
|
||||
/*TODO : Pathfind to a point away from the player*/
|
||||
|
||||
/*Avoid overflow*/
|
||||
temp = (AI_RAND() * tdat->agressivity) * relative_strength;
|
||||
attack_chance = temp >> PRECISION;
|
||||
if(!spotted)
|
||||
wander_chance = AI_RAND() * tdat->cowardice;
|
||||
flee_chance = AI_CHANCE_MAX - attack_chance - wander_chance;
|
||||
|
||||
break;
|
||||
}
|
||||
case NPC_S_ATTACK: {
|
||||
/*TODO : Attack !*/
|
||||
|
||||
/*Decide what to do next*/
|
||||
temp = (AI_RAND() * tdat->cowardice) *
|
||||
((1 << PRECISION) / relative_strength);
|
||||
flee_chance = temp >> PRECISION;
|
||||
if(!spotted)
|
||||
flee_chance += (tdat->cowardice / 4) * 0xFFFF;
|
||||
attack_chance = AI_CHANCE_MAX - flee_chance;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -406,6 +457,7 @@ void npc_ai(NPC *npc, Game *game){
|
|||
extern const short int walkable_speed[WALKABLE_TILE_MAX];
|
||||
|
||||
void update_npc(NPC *npc, Game *game) {
|
||||
/*If he has a brain, poke it with a stick*/
|
||||
if(npc->type != NPC_Static)
|
||||
npc_ai(npc, game);
|
||||
|
||||
|
@ -446,7 +498,7 @@ void update_npc(NPC *npc, Game *game) {
|
|||
if(vecY > 0)
|
||||
mpos_y += 1 << PRECISION;
|
||||
|
||||
if(!npc_collision(game, npc, mpos_x, mpos_x)) {
|
||||
if(!npc_collision(game, mpos_x, mpos_x)) {
|
||||
npc->curx = new_x;
|
||||
npc->cury = new_y;
|
||||
}
|
||||
|
|
32
src/npc.h
32
src/npc.h
|
@ -18,38 +18,18 @@
|
|||
|
||||
typedef struct {
|
||||
|
||||
/*TODO : Stats*/
|
||||
/*TODO : Moar stats*/
|
||||
|
||||
uint8_t presence; /*How much the NPC adds to team strength*/
|
||||
|
||||
/*AI weights - Values on 255*/
|
||||
uint8_t agressivity; /*Attack*/
|
||||
uint8_t cowardice; /*Flee*/
|
||||
uint8_t lazyness; /*Idle*/
|
||||
uint8_t wanderlust; /*Wandering*/
|
||||
uint8_t cowardice; /*Flee*/
|
||||
uint8_t lazyness; /*Idle*/
|
||||
uint8_t wanderlust; /*Wandering*/
|
||||
|
||||
} NPC_TypeData;
|
||||
|
||||
enum {
|
||||
NPC_Static = 0, /*~= none, disqualifies from all AI*/
|
||||
NPC_Guard = 1,
|
||||
NPC_Bandit = 2,
|
||||
NPC_Monster = 3,
|
||||
NPC_Type_Count
|
||||
};
|
||||
|
||||
enum {
|
||||
NPC_T_NONE = 0,
|
||||
NPC_T_FRIENDLY = 1, /* The player's team */
|
||||
NPC_T_HOSTILE = 2, /* to the player */
|
||||
NPC_T_ALL = 3
|
||||
};
|
||||
|
||||
enum {
|
||||
NPC_S_IDLE = 0,
|
||||
NPC_S_ATTACK = 1,
|
||||
NPC_S_FLEE = 2,
|
||||
NPC_S_WANDER = 3
|
||||
};
|
||||
|
||||
/* /!\ Warning /!\
|
||||
* Do not keep hard references to non-static NPCs, as they will likely move
|
||||
* in the stack */
|
||||
|
|
Loading…
Reference in a new issue