Browse Source

Begin NPC framework

- Added NPC type, updates, and storage/addition in heap
- Started interaction menus
- Added Guide
- Made a couple letters in the smalltext font a bit better
- Main menu now starts with the sun rising
master
KBD2 1 month ago
parent
commit
3224d7e0c4
11 changed files with 272 additions and 4 deletions
  1. BIN
      assets-fx/fonts/smalltext.png
  2. BIN
      assets-fx/img/npcs/guide.png
  3. BIN
      assets-fx/img/ui/npctalk.png
  4. +1
    -0
      include/entity.h
  5. +37
    -0
      include/npc.h
  6. +3
    -0
      include/world.h
  7. +12
    -0
      src/main.c
  8. +1
    -1
      src/menu.c
  9. +179
    -0
      src/npc.c
  10. +20
    -3
      src/render.c
  11. +19
    -0
      src/update.c

BIN
assets-fx/fonts/smalltext.png View File

Before After
Width: 112  |  Height: 48  |  Size: 704 B Width: 112  |  Height: 48  |  Size: 662 B

BIN
assets-fx/img/npcs/guide.png View File

Before After
Width: 32  |  Height: 400  |  Size: 1.8 KiB

BIN
assets-fx/img/ui/npctalk.png View File

Before After
Width: 52  |  Height: 7  |  Size: 230 B

+ 1
- 0
include/entity.h View File

@ -123,6 +123,7 @@ struct Player {
Coords cursor;
Coords cursorTile;
Coords cursorWorld;
struct GhostObject {
short width;
short height;


+ 37
- 0
include/npc.h View File

@ -0,0 +1,37 @@
#pragma once
#include <gint/gray.h>
#include "entity.h"
enum NPCs {
NPC_GUIDE
};
enum MenuTypes {
MENU_SHOP,
MENU_GUIDE,
MENU_HEAL
};
typedef struct {
enum NPCs id;
bopti_image_t *sprite;
char *name;
struct EntityPhysicsProps props;
struct AnimationData anim;
int numInteractDialogue;
char **interactDialogue;
void (*menu)();
enum MenuTypes menuType;
} NPC;
bool isNPCAlive(enum NPCs id);
void addNPC(enum NPCs id);
bool removeNPC(enum NPCs id);
void npcUpdate(int frames);
bool npcTalk(int numDialogue, char **dialogue, enum MenuTypes type);

+ 3
- 0
include/world.h View File

@ -14,6 +14,7 @@ World API functions.
#include "entity.h"
#include "chest.h"
#include "save.h"
#include "npc.h"
#define MAX_FRIENDS 4
@ -83,6 +84,8 @@ enum Tiles {
struct World {
Tile *tiles;
Entity *entities;
int numNPCs;
NPC *npcs;
struct ParticleExplosion explosion;
int timeTicks;
struct Chests chests;


+ 12
- 0
src/main.c View File

@ -19,10 +19,14 @@
#include "update.h"
#include "generate.h"
#include "optimise.h"
#include "npc.h"
// Fixes linker problem for newlib
int __errno = 0;
// Fixes implicit declaration problem
void spu_zero();
// Syscalls
const unsigned int sc003B[] = { SCA, SCB, SCE, 0x003B };
const unsigned int sc019F[] = { SCA, SCB, SCE, 0x019F };
@ -94,6 +98,7 @@ bool gameLoop(volatile int *flag)
doEntityCycle(frames);
doSpawningCycle();
npcUpdate(frames);
if(player.combat.health <= 0)
{
if(respawnCounter == 1)
@ -235,10 +240,13 @@ int main(void)
world = (struct World)
{
.tiles = (Tile*)save.tileData,
.entities = (Entity*)malloc(MAX_ENTITIES * sizeof(Entity)),
.explosion = {
.particles = malloc(50 * sizeof(Particle))
},
.chests = {
.chests = malloc(MAX_CHESTS * sizeof(struct Chest)),
.addChest = &addChest,
@ -308,6 +316,8 @@ int main(void)
registerEquipment();
registerHeld();
addNPC(NPC_GUIDE);
// Do the game
doSave = gameLoop(&flag);
@ -328,6 +338,8 @@ int main(void)
if(doSave) gint_switch(&saveGame);
free(world.chests.chests);
// Nothing is allocated if there are no NPCs
if(world.npcs != NULL) free(world.npcs);
if(save.error != -99) saveFailMenu();
if(doSave)


+ 1
- 1
src/menu.c View File

@ -39,7 +39,7 @@ int mainMenu()
key_event_t key;
volatile int flag = 0;
int timer;
unsigned int frames = 0;
unsigned int frames = 300;
int bunnyBlink = 0;
int orbX, orbY;


+ 179
- 0
src/npc.c View File

@ -0,0 +1,179 @@
#include <gint/std/stdlib.h>
#include <gint/gray.h>
#include <gint/keyboard.h>
#include <gint/std/string.h>
#include "npc.h"
#include "world.h"
#include "render.h"
char *guideDialogue[] = {
"Greetings, player. Is there something I can help you with?",
"I am here to give you advice on what to do next. It is recommended that you talk with me anytime you get stuck.",
"They say there is a person who will tell you how to survive in this land... oh wait. That's me."
};
char *guideHelpDialogue[] = {
"Underground exploration can yield valuable treasures!",
"A house can be a useful refuge from the beasts that roam at night.",
"Ropes can be used to traverse pits!"
};
void guideMenu()
{
while(npcTalk(sizeof(guideHelpDialogue) / sizeof(char*), guideHelpDialogue, true));
}
bool isNPCAlive(enum NPCs id)
{
for(int i = 0; i < world.numNPCs; i++)
{
if(world.npcs[i].id == id) return true;
}
return false;
}
void addNPC(enum NPCs id)
{
extern bopti_image_t img_npcs_guide;
NPC *npc;
int tileX, tileY = 0;
world.numNPCs++;
world.npcs = realloc(world.npcs, world.numNPCs * sizeof(NPC));
allocCheck(world.npcs);
npc = &world.npcs[world.numNPCs - 1];
switch(id)
{
case NPC_GUIDE:
*npc = (NPC) {
.sprite = &img_npcs_guide,
.props = {
.width = 16,
.height = 23
},
.numInteractDialogue = sizeof(guideDialogue) / sizeof(char*),
.interactDialogue = guideDialogue,
.menu = &guideMenu,
.menuType = MENU_GUIDE
};
break;
}
tileX = game.WORLD_WIDTH >> 1;
while(getTile(tileX, tileY).id == TILE_NOTHING) tileY++;
tileY -= 4;
npc->props.x = tileX << 3;
npc->props.y = tileY << 3;
}
void npcUpdate(int frames)
{
NPC *npc;
for(int idx = 0; idx < world.numNPCs; idx++)
{
npc = &world.npcs[idx];
// Physics and movement
handlePhysics(&npc->props, frames, false, WATER_FRICTION);
if(npc->props.movingSelf)
{
if(rand() % 50 == 0) npc->props.movingSelf = false;
}
else if(rand() % 300 == 0)
{
npc->props.movingSelf = true;
npc->props.xVel = (rand() % 2) ? -0.3 : 0.3;
npc->anim.direction = npc->props.xVel < 0;
}
// Animation
if(npc->props.yVel != 0)
{
npc->anim.animation = 1;
npc->anim.animationFrame = 1;
}
else if(npc->props.xVel != 0)
{
if(npc->anim.animation != 2)
{
npc->anim.animation = 2;
npc->anim.animationFrame = 2;
}
}
else
{
npc->anim.animation = 0;
npc->anim.animationFrame = 0;
}
// Walking animation is the only one with multiple frames
if(frames & 1 && npc->anim.animation == 2)
{
npc->anim.animationFrame++;
if(npc->anim.animationFrame > 15) npc->anim.animationFrame = 2;
}
}
}
bool npcTalk(int numDialogue, char **dialogue, enum MenuTypes type)
{
char buffer[33];
extern bopti_image_t img_ui_npctalk;
key_event_t key;
int selectedLine = rand() % numDialogue;
int lines = strlen(dialogue[selectedLine]) / 32 + 1;
buffer[32] = '\0';
render(false);
drect_border(0, 0, 127, 7 * lines + 1, C_WHITE, 1, C_BLACK);
for(int line = 0; line < lines; line++)
{
strncpy(buffer, (char *)(dialogue[selectedLine] + 32 * line), 32);
dprint(2, line * 7 + 2, C_BLACK, buffer);
}
switch(type)
{
case MENU_GUIDE:
dsubimage(0, 7 * lines + 2, &img_ui_npctalk, 17, 0, 17, 7, DIMAGE_NONE);
break;
case MENU_SHOP:
dsubimage(0, 7 * lines + 2, &img_ui_npctalk, 0, 0, 17, 7, DIMAGE_NONE);
break;
case MENU_HEAL:
break;
}
dsubimage(18, 7 * lines + 2, &img_ui_npctalk, 34, 0, 17, 7, DIMAGE_NONE);
dupdate();
while(true)
{
key = getkey_opt(GETKEY_NONE, NULL);
switch(key.key)
{
case KEY_F1:
return true;
case KEY_F2:
return false;
default:
break;
}
}
}

+ 20
- 3
src/render.c View File

@ -74,6 +74,7 @@ void render(bool renderHUD)
int entX, entY;
int entSubrectX, entSubrectY;
Entity *ent;
NPC *npc;
int x, y;
@ -91,8 +92,11 @@ void render(bool renderHUD)
Particle particle;
player.cursorTile.x = (camX + player.cursor.x - (SCREEN_WIDTH >> 1)) >> 3;
player.cursorTile.y = (camY + player.cursor.y - (SCREEN_HEIGHT >> 1)) >> 3;
player.cursorWorld.x = camX + player.cursor.x - (SCREEN_WIDTH >> 1);
player.cursorWorld.y = camY + player.cursor.y - (SCREEN_HEIGHT >> 1);
player.cursorTile.x = player.cursorWorld.x >> 3;
player.cursorTile.y = player.cursorWorld.y >> 3;
updateVarBuffer(tileLeftX, tileTopY);
@ -236,6 +240,19 @@ void render(bool renderHUD)
}
}
for(int idx = 0; idx < world.numNPCs; idx++)
{
npc = &world.npcs[idx];
entX = npc->props.x - (camX - (SCREEN_WIDTH >> 1));
entY = npc->props.y - (camY - (SCREEN_HEIGHT >> 1));
entSubrectX = npc->anim.direction ? npc->props.width: 0;
// NPCs and player have a sticky out bit at the bottom that isn't included in their height,
// so add 2 instead of 1 when finding the subrectangle Y
entSubrectY = npc->anim.animationFrame * (npc->props.height + 2) + 1;
dsubimage(entX, entY, npc->sprite, entSubrectX, entSubrectY, npc->props.width, npc->props.height, DIMAGE_NONE);
}
// Only render player if the player isn't flashing or dead
if(!(player.combat.currImmuneFrames & 2) && player.combat.health > 0)
{
@ -283,7 +300,7 @@ void render(bool renderHUD)
}
// Render the hotbar if the player has recently interacted with their inventory
if(player.inventory.ticksSinceInteracted < 120)
if(renderHUD && player.inventory.ticksSinceInteracted < 120)
{
if(player.cursor.x < 82 && player.cursor.y < 19) hotbarY = 47;
else hotbarY = 0;


+ 19
- 0
src/update.c View File

@ -120,6 +120,7 @@ enum UpdateReturnCodes keyboardUpdate()
bool playerDead = player.combat.health <= 0;
struct Chest* chest;
int ret;
NPC *npc;
player.inventory.ticksSinceInteracted++;
@ -196,6 +197,24 @@ enum UpdateReturnCodes keyboardUpdate()
case KEY_9:
if(key.type != KEYEV_DOWN) break;
// Check NPCs
x = player.cursorWorld.x;
y = player.cursorWorld.y;
for(int idx = 0; idx < world.numNPCs; idx++)
{
npc = &world.npcs[idx];
if(x >= npc->props.x
&& x < npc->props.x + npc->props.width
&& y >= npc->props.y
&& y < npc->props.y + npc->props.height)
{
if(npcTalk(npc->numInteractDialogue, npc->interactDialogue, npc->menuType)) npc->menu();
break;
}
}
// Check interactable tiles
x = player.cursorTile.x;
y = player.cursorTile.y;
tile = getTile(x, y).id;


Loading…
Cancel
Save