Add textbox on start, and popcorn barrels in the basement display a textbox.

master
immibis 2025-02-19 01:55:56 +01:00
parent b7f31cdeaf
commit f5e5886827
10 changed files with 206 additions and 47 deletions

View File

@ -1,6 +1,6 @@
PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
OBJS = pinetab2_framework.o game.o
OBJS = pinetab2_framework.o game.o textbox.o
SPRITES = $(patsubst $(PROJECT_ROOT)sprites/%.xcf,%,$(wildcard $(PROJECT_ROOT)sprites/*.xcf))
NAVMESHES = $(patsubst $(PROJECT_ROOT)navmesh/%.tmx,%,$(wildcard $(PROJECT_ROOT)navmesh/*.tmx))

View File

@ -20,8 +20,8 @@ def parse_object_polygon_points(object):
basey = int(object.attrib["y"])
if polygon_data is None:
# It's a rectangle by default
width = float(object.attrib["width"])
height = float(object.attrib["height"])
width = int(object.attrib["width"])
height = int(object.attrib["height"])
points = [(basex, basey), (basex + width, basey), (basex + width, basey + height), (basex, basey + height)]
else:
points = []
@ -53,6 +53,15 @@ def get_edge_equation(a, b):
R *= norm
return P,Q,R
def get_bounding_box(points):
(x1,y1),(x2,y2) = points[0],points[0]
for x,y in points:
if x<x1: x1=x
if x>x2: x2=x
if y<y1: y1=y
if y>y2: y2=y
return x1,y1,x2,y2
for object in tree.findall("objectgroup[@name='navmesh']/object"):
points = parse_object_polygon_points(object)
@ -130,7 +139,9 @@ out.write("\t(const struct level_clickregion[]){\n") # clickregions start
for object in tree.findall("objectgroup[@name='clickable']/object"):
out.write("\t\t{\n");
points = parse_object_polygon_points(object)
x1,y1,x2,y2 = get_bounding_box(points)
out.write("\t\t\t"+object.attrib["name"]+",\n")
out.write(f"\t\t\t{x1},{y1},{x2-x1},{y2-y1},\n")
out.write("\t\t\t" + str(len(points)) + ",\n")
out.write("\t\t\t(const struct level_clickregion_edge[]){\n")
for a,b in points_to_edges(points):

View File

@ -30,6 +30,7 @@ struct level_clickregion_edge {
};
struct level_clickregion {
int id;
int x, y, width, height;
int num_edges;
const struct level_clickregion_edge *edges;
};

View File

@ -2,6 +2,8 @@
#define MAX_SCRIPTS_PER_SCENE 30
#define MAX_STACKED_SCENES 5
#include <stdint.h>
struct object {
int id; // 0 if slot unused
int x;
@ -24,12 +26,17 @@ struct script {
};
typedef void (*scene_render_fn)(struct scene *s);
typedef void (*scene_animtimer_fn)(struct scene *s);
typedef void (*scene_handle_tap_fn)(struct scene *s, int x, int y);
void standard_scene_render(struct scene *s);
void standard_handle_tap(struct scene *s, int x, int y);
extern struct scene {
int id;
struct object objects[MAX_OBJECTS_PER_SCENE];
struct script scripts[MAX_SCRIPTS_PER_SCENE];
scene_render_fn render_fn;
scene_render_fn render_fn; // default standard_scene_render
scene_animtimer_fn animtimer_fn; // default null
scene_handle_tap_fn handle_tap_fn; // default standard_handle_tap
struct navmesh *navmesh; // defaults to non-null pointer to empty navmesh
} scenes[MAX_STACKED_SCENES];
extern int scene_depth; // number of stacked scenes
@ -48,10 +55,12 @@ struct script_player_walk {
#define SCRIPT_WAKEUP_OTHER_SCRIPT 2
#define SCRIPT_WAKEUP_OTHER_SCRIPT_INTERRUPTED 3 // Delivered to script that registers for SCRIPT_WAKEUP_OTHER_SCRIPT if that script is interrupted instead of completing.
typedef void (*scene_setup_fn)(int scene, int fromscene);
// engine
// Scene changes - even push - may discard remaining events for the scene.
void push_scene(int scene);
void push_scene(int scene, scene_setup_fn setup_fn);
void replace_scene(int scene);
void pop_scene();
void scene_add_clickrect(int id, int x, int y, int width, int height);
@ -63,12 +72,23 @@ struct script *scene_get_script(int id);
bool deliver_script_wakeup(int wakeupMode, int wakeupArg1, int wakeupType, int arg1, int arg2, int arg3, int arg4); // deliver to scripts with given wakeupMode and wakeupArg1. Returns true if scene changed.
void start_player_walk_to_point(int x, int y); // x and y are not corrected to lie within navmesh
void push_scene_textbox(const char *text);
// used during rendering
// pixel is 0xRRGGBB
extern uint32_t *curfb;
extern bool need_rerender;
void fillrect(int x1, int y1, int width, int height, uint32_t pixel);
void blit(int x, int y, int width, int height, uint32_t *pixels);
// game-specific constants
#define SCENE_LOBBY 1
#define SCENE_MANAGERS_OFFICE 2
#define SCENE_MANAGERS_OFFICE_SAFE 3
#define SCENE_BASEMENT 4
#define SCENE_TEXTBOX 5
// game.cpp
void scene_setup(int scene, int fromscene);
void onclick(int curscene, int objid);
void onclick(int curscene, struct object *obj);

View File

@ -24,20 +24,20 @@ static void create_player(int x, int y) {
}
void scene_setup(int scene, int fromscene) {
top_scene.render_fn = standard_scene_render;
switch(scene) {
case SCENE_LOBBY:
scene_add_object(OBJID_BACKGROUND, 0, 0, BGWIDTH, BGHEIGHT, _binary_sprite_lobby_raw_start);
scene_load_predef(&predef_lobby);
switch(fromscene) {
case SCENE_MANAGERS_OFFICE:
create_player(308, 445);
create_player(256, 445);
break;
default:
create_player(424, 675);
break;
}
top_scene.navmesh = &navmesh_lobby;
push_scene_textbox("Welcome\n\nto\n\ntestgame");
break;
case SCENE_MANAGERS_OFFICE:
scene_add_object(OBJID_BACKGROUND, 0, 0, BGWIDTH, BGHEIGHT, _binary_sprite_managers_office_raw_start);
@ -99,17 +99,40 @@ static void start_player_walk_to_point_then_transition_scene(int x, int y, int s
scr->vars[0] = scene;
}
void onclick(int curscene, int objid) {
static void do_popcorn_on_walk_finish(struct script *scr, int wakeupMode, int arg1, int arg2, int arg3, int arg4) {
scr->id = 0;
if(wakeupMode == SCRIPT_WAKEUP_OTHER_SCRIPT) {
push_scene_textbox("You got popcorn.");
}
else
assert(wakeupMode == SCRIPT_WAKEUP_OTHER_SCRIPT_INTERRUPTED);
}
static void start_player_walk_to_point_then_do_popcorn(int x, int y) {
start_player_walk_to_point(x, y);
struct script *scr = scene_add_script(OBJID_PLAYER_WALK_TO_DOOR_SCRIPT, true);
scr->wakeupMode = SCRIPT_WAKEUP_OTHER_SCRIPT;
scr->wakeupArg1 = OBJID_PLAYER_WALK_SCRIPT;
scr->wakeupFn = do_popcorn_on_walk_finish;
}
void onclick(int curscene, struct object *obj) {
if(obj->id == OBJID_CLOSE_MODAL) {
pop_scene();
return;
}
switch(curscene) {
case SCENE_LOBBY:
switch(objid) {
switch(obj->id) {
case OBJID_DOOR_TO_MANAGERS_OFFICE_FROM_LOBBY:
start_player_walk_to_point_then_transition_scene(312, 441, SCENE_MANAGERS_OFFICE);
return;
}
break;
case SCENE_MANAGERS_OFFICE:
switch(objid) {
switch(obj->id) {
case OBJID_DOOR_TO_LOBBY_FROM_MANAGERS_OFFICE:
start_player_walk_to_point_then_transition_scene(815, 713, SCENE_LOBBY);
return;
@ -122,16 +145,12 @@ void onclick(int curscene, int objid) {
}
break;
case SCENE_BASEMENT:
switch(objid) {
switch(obj->id) {
case OBJID_BASEMENT_LADDER:
start_player_walk_to_point_then_transition_scene(438, 402, SCENE_MANAGERS_OFFICE);
return;
}
break;
case SCENE_MANAGERS_OFFICE_SAFE:
switch(objid) {
case OBJID_CLOSE_MODAL:
pop_scene();
case OBJID_POPCORN_BARREL:
start_player_walk_to_point_then_do_popcorn(obj->x + obj->width/2, obj->y + obj->height);
return;
}
break;

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.9" tiledversion="1.9.2" orientation="orthogonal" renderorder="right-down" width="40" height="25" tilewidth="32" tileheight="32" infinite="0" nextlayerid="5" nextobjectid="4">
<map version="1.9" tiledversion="1.9.2" orientation="orthogonal" renderorder="right-down" width="40" height="25" tilewidth="32" tileheight="32" infinite="0" nextlayerid="5" nextobjectid="8">
<imagelayer id="2" name="Image Layer 1">
<image source="../build/default/basement.png" width="1280" height="800"/>
</imagelayer>
@ -13,5 +13,9 @@
</objectgroup>
<objectgroup id="4" name="clickable">
<object id="3" name="OBJID_BASEMENT_LADDER" x="396" y="86" width="85" height="312"/>
<object id="4" name="OBJID_POPCORN_BARREL" x="529" y="291" width="84" height="150"/>
<object id="5" name="OBJID_POPCORN_BARREL" x="628" y="291" width="84" height="150"/>
<object id="6" name="OBJID_POPCORN_BARREL" x="733" y="293" width="83" height="148"/>
<object id="7" name="OBJID_POPCORN_BARREL" x="835" y="295" width="81" height="146"/>
</objectgroup>
</map>

View File

@ -10,3 +10,4 @@
#define OBJID_BASEMENT_ELECTRICAL_BOX 10
#define OBJID_BASEMENT_LADDER 11
#define OBJID_CLOSE_MODAL 12
#define OBJID_POPCORN_BARREL 13

View File

@ -40,7 +40,6 @@ static struct mtslot {
} mtslots[MAX_MT_SLOTS] = {0};
static struct mtslot *curslot = &mtslots[0]; // TODO: query the current value of ABS_MT_SLOT
static void handle_tap(int x, int y); // x and y in landscape orientation
static void poll_touchscreen() {
struct input_event buf[64];
int nread;
@ -97,7 +96,7 @@ retry_read:
}
}*/
if(!s->lasttouch) {
handle_tap(s->y, 799-s->x);
top_scene.handle_tap_fn(&top_scene, s->y, 799-s->x);
}
}
s->lasttouch = s->touch;
@ -131,21 +130,16 @@ retry_read:
uint32_t current_animframe;
struct sprite {
int width, height;
uint32_t *pixels;
};
static uint32_t *curfb; // used during render cycle
static void blit(int x, int y, const struct sprite *spr) {
uint32_t *inbase = spr->pixels;
uint32_t *curfb; // used during render cycle
void blit(int x, int y, int width, int height, uint32_t *pixels) {
uint32_t *inbase = pixels;
//printf("blit %dx%d at %d,%d\n", spr->width, spr->height, x, y);
y = PLAYFIELD_HEIGHT - spr->height - y;
if(y >= PLAYFIELD_HEIGHT || x >= PLAYFIELD_WIDTH || y <= -spr->height || x <= -spr->width) return;
if(spr->height > PLAYFIELD_HEIGHT) error(1, 0, "sprite bigger than playfield not supported"); // would need to clip top and bottom simultaneously - not implemented for now
y = PLAYFIELD_HEIGHT - height - y;
if(y >= PLAYFIELD_HEIGHT || x >= PLAYFIELD_WIDTH || y <= -height || x <= -width) return;
if(height > PLAYFIELD_HEIGHT) error(1, 0, "sprite bigger than playfield not supported"); // would need to clip top and bottom simultaneously - not implemented for now
uint32_t *outbase = curfb + x*PLAYFIELD_HEIGHT + y;
for(int dx = 0; dx < spr->width; dx++, inbase += spr->height, outbase += PLAYFIELD_HEIGHT, x++) {
for(int dx = 0; dx < width; dx++, inbase += height, outbase += PLAYFIELD_HEIGHT, x++) {
if(x < 0) continue;
if(x >= PLAYFIELD_WIDTH) break;
@ -153,34 +147,59 @@ static void blit(int x, int y, const struct sprite *spr) {
// TODO: loop inversion would make sense
int offset = -y;
// must have at least one pixel to copy
memcpy(outbase+offset, inbase+offset, (spr->height-offset)*4);
memcpy(outbase+offset, inbase+offset, (height-offset)*4);
} else {
int ncopy = PLAYFIELD_HEIGHT-y;
if(ncopy > spr->height) ncopy = spr->height;
if(ncopy > height) ncopy = height;
memcpy(outbase, inbase, ncopy*4);
}
}
}
static void scene_clear_and_setup(int scene, int fromscene) {
void fillrect(int x1, int y1, int width, int height, uint32_t pixel) {
int x2 = x1 + width;
int y2 = y1 + height;
if(x1 < 0) x1 = 0;
if(y1 < 0) y1 = 0;
if(x2 >= PLAYFIELD_WIDTH) x2 = PLAYFIELD_WIDTH;
if(y2 >= PLAYFIELD_HEIGHT) y2 = PLAYFIELD_HEIGHT;
y1 = PLAYFIELD_HEIGHT-1-y1;
y2 = PLAYFIELD_HEIGHT-1-y2;
for(int x = x1; x < x2; x++) {
uint32_t *outbase = curfb + x*PLAYFIELD_HEIGHT + y2;
for(int y = y2; y < y1; y++) {
*outbase++ = pixel;
}
}
}
static void scene_clear_and_setup(int scene, int fromscene, scene_setup_fn setup_fn) {
memset(&top_scene, 0, sizeof(top_scene));
top_scene.id = scene;
top_scene.navmesh = &null_navmesh;
top_scene.handle_tap_fn = standard_handle_tap;
top_scene.render_fn = standard_scene_render;
scene_setup(scene, fromscene);
setup_fn(scene, fromscene);
need_rerender = true;
}
void push_scene(int scene) {
void push_scene(int scene, scene_setup_fn setup_fn) {
scene_depth++;
if(scene_depth > MAX_STACKED_SCENES) error(1, 0, "maximum scene depth exceeded");
scene_clear_and_setup(scene, -1);
scene_clear_and_setup(scene, -1, setup_fn);
}
void push_scene(int scene) {
push_scene(scene, scene_setup);
}
void replace_scene(int scene) {
scene_clear_and_setup(scene, top_scene.id);
scene_clear_and_setup(scene, top_scene.id, scene_setup);
}
void pop_scene() {
if(scene_depth == 1) error(1, 0, "no scenes left");
scene_depth--;
// any cleanup?
need_rerender = true;
}
struct object *scene_add_object(int id, int x, int y, int width, int height, const char *pixels) {
@ -203,7 +222,7 @@ struct object *scene_add_object(int id, int x, int y, int width, int height, con
void scene_load_predef(const struct level_predef_data *predef) {
for(const struct level_clickregion *cr = predef->clickregions; cr->num_edges; cr++) {
struct object *obj = scene_add_object(cr->id, 0, 0, 0, 0, nullptr);
struct object *obj = scene_add_object(cr->id, cr->x, cr->y, cr->width, cr->height, nullptr);
obj->clickregion = cr;
}
}
@ -362,9 +381,9 @@ void start_player_walk_to_point(int targetX, int targetY) {
printf("set course from triangle %d to %d\n", sourceTri, destTri);
}
void handle_tap(int x, int y) {
void standard_handle_tap(struct scene *scene, int x, int y) {
uint32_t walkgen = player_walk_generation;
int sceneid = top_scene.id;
int sceneid = scene->id;
struct navmesh *cur_navmesh = top_scene.navmesh;
struct object *objects = top_scene.objects;
for(int i = 0; i < MAX_OBJECTS_PER_SCENE; i++) {
@ -381,7 +400,7 @@ void handle_tap(int x, int y) {
}
// clicked on the object
onclick(sceneid, objects[i].id);
onclick(sceneid, &objects[i]);
if(top_scene.id != sceneid) return; // early exit if scene changed, and don't walk the player either
click_not_on_object:;
@ -485,8 +504,7 @@ void standard_scene_render(scene *s) {
struct object *objects = s->objects;
for(int i = 0; i < MAX_OBJECTS_PER_SCENE; i++) {
if(objects[i].id != 0 && objects[i].pixels) {
struct sprite spr = {.width = objects[i].width, .height = objects[i].height, .pixels = (uint32_t*)objects[i].pixels};
blit(objects[i].x, objects[i].y, &spr);
blit(objects[i].x, objects[i].y, objects[i].width, objects[i].height, (uint32_t*)objects[i].pixels);
}
}
}
@ -558,7 +576,8 @@ int main(int argc, char **argv) {
int timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if(timerfd < 0) error(1, errno, "timerfd_create");
{
struct itimerspec tspec = {.it_interval = {.tv_nsec = 250000000}, .it_value = {.tv_nsec=250000000}};
#define ANIMTIMER_INTERVAL 75000000
struct itimerspec tspec = {.it_interval = {.tv_nsec = ANIMTIMER_INTERVAL}, .it_value = {.tv_nsec = ANIMTIMER_INTERVAL}};
if(timerfd_settime(timerfd, 0, &tspec, NULL) < 0) error(1, errno, "timerfd_settime");
}
@ -596,7 +615,10 @@ int main(int argc, char **argv) {
if(read(timerfd, &dummy, sizeof dummy) < 0) error(1, errno, "read (timerfd)");
current_animframe++;
//printf("animation frame %u\n", (unsigned int)current_animframe);
need_rerender = true;
if(top_scene.animtimer_fn) {
top_scene.animtimer_fn(&top_scene);
}
}
if(polls[2].revents & POLLIN) {
char buf[256];

BIN
sprites/font.xcf Normal file

Binary file not shown.

81
textbox.cpp Normal file
View File

@ -0,0 +1,81 @@
#include "engine.h"
#include "objids.h"
#include <string.h>
// TODO: should be scene variables, so multiple textboxes can be stacked
static const char *textbox_text;
static int tb_width_chars, tb_height_chars;
static int visible_chars = 0;
extern const char _binary_sprite_font_raw_start[];
#define CHARSIZE 24
#define HSPACE 2
#define LINESPACE 8
static void textbox_render_fn(struct scene *s) {
int boxheight = tb_height_chars*(CHARSIZE + LINESPACE) - LINESPACE;
int boxwidth = tb_width_chars*(CHARSIZE + HSPACE) - HSPACE;
int x = (1280 - tb_width_chars*CHARSIZE)/2;
int y = (800 - tb_height_chars*CHARSIZE)/2;
int xleft = x;
fillrect(x-CHARSIZE, y-CHARSIZE, boxwidth+2*CHARSIZE, boxheight+2*CHARSIZE, 0);
for(const char *str = textbox_text; *str; str++) {
if(str == textbox_text + visible_chars)
break;
if(*str == '\n') {
x = xleft;
y += CHARSIZE + LINESPACE;
} else {
blit(x, y, CHARSIZE, CHARSIZE, (*str - 32)*(CHARSIZE*CHARSIZE) + (uint32_t*)_binary_sprite_font_raw_start);
x += CHARSIZE + HSPACE;
}
}
}
static void textbox_animtimer_fn(struct scene *s) {
if(textbox_text[visible_chars]) {
visible_chars++;
need_rerender = true;
}
}
static void textbox_handle_tap(struct scene *s, int x, int y) {
if(textbox_text[visible_chars]) {
visible_chars = strlen(textbox_text);
need_rerender = true;
} else {
pop_scene();
}
}
static void textbox_setup_fn(int scene, int fromscene) {
top_scene.render_fn = textbox_render_fn;
top_scene.animtimer_fn = textbox_animtimer_fn;
top_scene.handle_tap_fn = textbox_handle_tap;
}
void push_scene_textbox(const char *text) {
textbox_text = text;
visible_chars = 0;
tb_width_chars = 0;
tb_height_chars = 1;
int cur_width = 0;
for(const char *s = text; *s; s++) {
if(*s == '\n') {
tb_height_chars++;
cur_width = 0;
} else {
cur_width++;
if(cur_width > tb_width_chars)
tb_width_chars = cur_width;
}
}
push_scene(SCENE_TEXTBOX, textbox_setup_fn);
}