npc : Implémentation théorique de la machine d'état

This commit is contained in:
attilavs2 2024-08-27 12:41:01 +02:00
parent e37bf141c2
commit 5842aa905e
7 changed files with 234 additions and 167 deletions

View file

@ -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))

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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
View file

@ -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;
}

View file

@ -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 */