render, block, world: first prototype

This commit is contained in:
Lephenixnoir 2022-07-16 22:02:04 +01:00
parent 56fb2c8366
commit b92cbcec5b
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
18 changed files with 601 additions and 22 deletions

2
.gitignore vendored
View File

@ -5,7 +5,7 @@
/*.g3a
# Python bytecode
__pycache__/
__pycache__/
# Common IDE files
*.sublime-project

View File

@ -1,24 +1,26 @@
cmake_minimum_required(VERSION 3.15)
project(Nooncraft)
project(Nooncraft LANGUAGES CXX)
include(GenerateG3A)
include(Fxconv)
find_package(Gint 2.8 REQUIRED)
set(SOURCES
src/main.c
# ...
)
# Shared assets, fx-9860G-only assets and fx-CG-50-only assets
src/block.cpp
src/graphics.cpp
src/main.cpp
src/render.cpp
src/world.cpp)
set(ASSETS
# ...
)
assets-cg/font_nooncraft.png
assets-cg/blockinfo.yaml)
fxconv_declare_assets(${ASSETS} WITH_METADATA)
fxconv_declare_converters(converters.py)
add_executable(addin ${SOURCES} ${ASSETS})
target_compile_options(addin PRIVATE -Wall -Wextra -Os)
target_link_libraries(addin Gint::Gint)
target_compile_options(addin PRIVATE -Wall -Wextra -Wno-narrowing -Os -std=c++20)
target_link_libraries(addin Gint::Gint -lsupc++)
if("${FXSDK_PLATFORM_LONG}" STREQUAL fx9860G)
message(FATAL_ERROR "This game is not supported on fx-9860G!")

12
assets-cg/blockinfo.yaml Normal file
View File

@ -0,0 +1,12 @@
- name: "AIR"
cluster: " "
walkable: yes
breakability: Fluid
- name: "STONE"
cluster: "□□□□"
toolKind: Pickaxe
- name: "COBBLESTONE"
cluster: "####"
toolKind: Pickaxe

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,10 @@
font_nooncraft.png:
type: font
name: font_nooncraft
charset: ascii
grid.size: 5x5
grid.padding: 1
blockinfo.yaml:
custom-type: blockinfo
name: Nooncraft_blockInfo

BIN
assets-cg/world.xcf Normal file

Binary file not shown.

92
converters.py Normal file
View File

@ -0,0 +1,92 @@
import fxconv
import yaml
def convert(input, output, params, target):
recognized = True
if params["custom-type"] == "blockinfo":
o = convert_blockinfo(input, params)
else:
recognized = False
if recognized:
fxconv.elf(o, output, "_" + params["name"], **target)
return 0
return 1
def mkBreakability(br):
return fxconv.u8({
None: 0,
"Fluid": 0,
"ByAnything": 1,
"ByTool": 2,
"Unbreakable": 3,
}[br])
def mkToolKind(tk):
return fxconv.u8({
None: 0,
"Pickaxe": 0,
"Axe": 1,
"Shovel": 2,
"Hoe": 3,
}[tk])
def mkBool(b):
if isinstance(b, str):
b = b.lower()
return fxconv.u8(b in [True, "yes", "true"])
def mkGlyph(s):
assert len(s) == 1
special = {
0x2058: 1,
0x00B7: 2,
0x2225: 3,
0x00A4: 4,
0x2197: 5,
0x00BB: 6,
0x2193: 7,
0x00EE: 8,
0x222B: 9,
0x25AA: 10,
0x2605: 11,
0x25A1: 0x7f,
}
if ord(s) >= 0x20 and ord(s) <= 0x7f:
return fxconv.u8(ord(s))
elif ord(s) in special:
return fxconv.u8(special[ord(s)])
else:
raise ValueError(f"unknown glyph {s}")
def mkGlyphCluster(str):
assert len(str) == 4
return b"".join(mkGlyph(s) for s in str)
def convert_blockinfo(input, params):
with open(input, "r") as fp:
data = yaml.safe_load(fp.read())
o = fxconv.ObjectData()
for b in data:
# Make sure there are no typos in the fields
assert (f in ["name", "breakability", "toolKind",
"baseBreakDurationTicks", "walkable"] for f in b)
# If a tool is specified, default to Breakability::ByTool
tk = b.get("toolKind", None)
br = b.get("breakability", "ByTool" if tk is not None else "Fluid")
o += fxconv.string(b["name"])
o += mkGlyphCluster(b["cluster"])
o += mkBreakability(br)
o += mkToolKind(tk)
o += fxconv.u16(int(b.get("baseBreakDurationTicks", "20")))
o += mkBool(b.get("walkable", False))
o += bytes(3)
o += fxconv.sym("Nooncraft_blockInfoCount")
o += fxconv.u32(len(data))
return o

17
src/block.cpp Normal file
View File

@ -0,0 +1,17 @@
#include "block.h"
extern "C" {
extern BlockInfo Nooncraft_blockInfo[];
extern int Nooncraft_blockInfoCount;
}
namespace Nooncraft {
BlockInfo *getBlockInfo(Block b)
{
if(b >= Nooncraft_blockInfoCount)
return nullptr;
return &Nooncraft_blockInfo[b];
}
} /* namespace Nooncraft */

35
src/block.h Normal file
View File

@ -0,0 +1,35 @@
// nooncraft.block: Block data in world map and general block information
#pragma once
#include "item.h"
#include "render.h"
#include <cstdint>
using Block = uint16_t;
enum class Breakability: uint8_t {
Fluid,
ByAnything,
ByTool,
Unbreakable,
};
/* General information on blocks */
struct BlockInfo
{
char const *name;
// TODO: Upgrade to random and/or dimension-specific sprites
GlyphCluster cluster;
Breakability breakability;
ToolKind toolKind;
int16_t baseBreakDurationTicks;
bool walkable;
};
namespace Nooncraft {
BlockInfo *getBlockInfo(Block b);
} /* namespace Nooncraft */

19
src/graphics.cpp Normal file
View File

@ -0,0 +1,19 @@
#include "graphics.h"
#include "render.h"
namespace Nooncraft {
void renderCamera(int x, int y, Camera *camera)
{
if(!camera)
return;
WorldRect r = camera->region;
for(int dy = r.ymin; dy <= r.ymax; dy++)
for(int dx = r.xmin; dx <= r.xmax; dx++) {
GlyphCluster c('@', '#', '~', '&');
renderCluster(x+2*dx, y+2*dy, c);
}
}
} /* namespace Nooncraft */

24
src/graphics.h Normal file
View File

@ -0,0 +1,24 @@
// nooncraft.graphics: Application-level rendering logic
#pragma once
#include "world.h"
struct Camera
{
Camera(World const *w): world {w}, center {0,0}, region {-1,1,-1,1} {}
/* Underlying world object */
World const *world;
/* Coordinates of the center point */
WorldCoord center;
/* Local rectangle around center at (0,0). This influences how much of the
screen the camera will take up when rendered */
WorldRect region;
};
namespace Nooncraft {
void renderCamera(int x, int y, Camera *camera);
} /* namespace Nooncraft */

8
src/item.h Normal file
View File

@ -0,0 +1,8 @@
// nooncraft.item: Item storage and properties
#pragma once
#include <cstdint>
enum class ToolKind: uint8_t {
Pickaxe, Axe, Shovel, Hoe,
};

View File

@ -1,12 +0,0 @@
#include <gint/display.h>
#include <gint/keyboard.h>
int main(void)
{
dclear(C_WHITE);
dtext(1, 1, C_BLACK, "Sample fxSDK add-in.");
dupdate();
getkey();
return 1;
}

53
src/main.cpp Normal file
View File

@ -0,0 +1,53 @@
#include "render.h"
#include "world.h"
#include "graphics.h"
#include <gint/keyboard.h>
#include <string.h>
int main(void)
{
World *world = Nooncraft::mkWorld(128, 128);
if(!world)
return 0;
Nooncraft::genWorld(world);
Camera *camera = new Camera(world);
camera->region = WorldRect(-13, 14, -6, 6);
// -
char separator[57];
memset(separator, '-', 56);
separator[56] = 0;
renderClear();
Nooncraft::renderCamera(26, 12, camera);
// HUD
GlyphCluster invLeft(' ', GLYPH_PARALLEL_TO, ' ', GLYPH_PARALLEL_TO);
GlyphCluster invRight(GLYPH_PARALLEL_TO, ' ', GLYPH_PARALLEL_TO, ' ');
int hudY = 27;
renderText(0, hudY, separator);
renderText(24, hudY+1, "COBBLESTONE");
renderCluster(0, hudY+1, invLeft);
renderCluster(2, hudY+1, Nooncraft::getBlockInfo(2)->cluster);
renderCluster(20, hudY+1, invRight);
renderText(2, hudY+3, "1 2 3 4 5 6 7 8 9");
// -
renderText(0, 0, "NOONCRAFT");
renderUpdate();
getkey();
//-
delete camera;
delete world;
return 1;
}

87
src/render.cpp Normal file
View File

@ -0,0 +1,87 @@
#include "render.h"
#include <gint/display.h>
#include <gint/defs/attributes.h>
#include <stdio.h>
extern "C" {
extern font_t font_nooncraft;
}
GCONSTRUCTOR
static void init_font(void)
{
dfont(&font_nooncraft);
}
static bool nightMode = false;
static inline int fg()
{
return nightMode ? C_WHITE : C_BLACK;
}
static inline int bg()
{
return nightMode ? C_BLACK : C_WHITE;
}
static inline void dglyph(int x, int y, Glyph g)
{
dtext_opt(x, y, fg(), bg(), DTEXT_LEFT, DTEXT_TOP, &g, 1);
}
void renderSetNight(bool night)
{
nightMode = night;
}
void renderClear(void)
{
dclear(bg());
}
void renderUpdate(void)
{
dupdate();
}
static int cellX(int x, bool text)
{
return 4 + 7 * x - (!text && (x & 1));
}
static int cellY(int y, bool text)
{
(void)text;
return 2 + 7 * y - (y & 1);
}
void renderGlyph(int x, int y, Glyph g)
{
dglyph(cellX(x, true), cellY(y, true), g);
}
void renderCluster(int x, int y, GlyphCluster c)
{
int px = cellX(x & -2, false);
int py = cellY(y & -2, false);
dglyph(px, py, c.glyphs[0]);
dglyph(px+6, py, c.glyphs[1]);
dglyph(px, py+6, c.glyphs[2]);
dglyph(px+6, py+6, c.glyphs[3]);
}
void renderText(int x, int y, char const *text)
{
for(int i = 0; text[i]; i++)
renderGlyph(x + i, y, text[i]);
}
void renderFormat(int x, int y, char const *fmt, ...)
{
static char str[128];
va_list args;
va_start(args, fmt);
vsnprintf(str, sizeof str, fmt, args);
va_end(args);
renderText(x, y, str);
}

69
src/render.h Normal file
View File

@ -0,0 +1,69 @@
// nooncraft.render: Grid-like text rendering
//
// Owing to the CPC constraint, the entire rendering system for this game is
// just text with a single font. We make two adjustments on the grid model:
//
// 1. We group rows and columns by pairs, so that the row/column spacing
// alternates between 1 and 3. This allows blocks to be represented by
// "clusters" of 4 characters.
// 2. To keep text natural, we provide a text function which forces column
// spacing to 2, offsetting every other character by 1 pixel. Rows remain
// irregularly spaced.
//---
#pragma once
using Glyph = char;
/* Non-ASCII glyphs */
enum {
GLYPH_FOUR_DOT = 1, /* U+2058 FOUR DOT PUNCTUATION */
GLYPH_MIDDLE_DOT = 2, /* U+00B7 MIDDLE DOT */
GLYPH_PARALLEL_TO = 3, /* U+2225 PARALLEL TO */
GLYPH_CURRENCY_SIGN = 4, /* U+00A4 CURRENCY SIGN */
GLYPH_NORTH_EAST_ARROW = 5, /* U+2197 NORTH EAST ARROW */
GLYPH_RIGHT_GUILLEMET = 6, /* U+00BB RIGHT GUILLEMET */
GLYPH_DOWN_ARROW = 7, /* U+2193 DOWNARDS ARROW */
GLYPH_I_CIRCUMFLEX = 8, /* U+00EE SMALL LATIN I WITH CIRCUMFLEX */
GLYPH_INTEGRAL = 9, /* U+222B INTEGRAL */
GLYPH_SMALL_SQUARE = 10, /* U+25AA BLACK SMALL SQUARE */
GLYPH_STAR = 11, /* U+2605 BLACK STAR */
};
/* A cluster of 4 glyphs. Can be conversion-constructed from a multi-byte
character literal like 'AXE '. */
struct GlyphCluster
{
GlyphCluster(Glyph g1, Glyph g2, Glyph g3, Glyph g4):
glyphs {g1, g2, g3, g4}
{}
GlyphCluster(int mbliteral) {
this->glyphs[0] = mbliteral >> 24;
this->glyphs[1] = (mbliteral >> 16) & 0xff;
this->glyphs[2] = (mbliteral >> 8) & 0xff;
this->glyphs[3] = mbliteral & 0xff;
}
Glyph glyphs[4];
};
/* Enable or disable night mode (black-on-white). */
void renderSetNight(bool night);
/* Clear the screen. */
void renderClear(void);
/* Update the screen. */
void renderUpdate(void);
/* Render a single character with text alignment (2-pixel column spacing).
The position (x,y) is counted in characters units with (0,0) top-left. */
void renderGlyph(int x, int y, Glyph g);
/* Render a 2x2 cluster of 4 characters. The position (x,y) should be even on
both axes. */
void renderCluster(int x, int y, GlyphCluster c);
/* Render a string with text alignment. */
void renderText(int x, int y, char const *text);
/* Same with printf() formatting. */
void renderFormat(int x, int y, char const *fmt, ...);

71
src/world.cpp Normal file
View File

@ -0,0 +1,71 @@
#include "world.h"
#include <stdlib.h>
#include <string.h>
WorldCoord WorldCoord::Up(0, -1);
WorldCoord WorldCoord::Down(0, 1);
WorldCoord WorldCoord::Left(-1, 0);
WorldCoord WorldCoord::Right(1, 0);
namespace Nooncraft {
World *mkWorld(int width, int height)
{
WorldRect l, br;
World *w = (World *)malloc(sizeof *w);
if(!w) goto fail;
w->cells = (Block *)malloc(width * height * sizeof *w->cells);
if(!w->cells) goto fail;
l = WorldRect(
-width / 2, width - width / 2,
-height / 2, height - height / 2);
br = WorldRect(
l.xmin + 2, l.xmax - 2,
l.ymin + 2, l.ymax - 2);
w->limits = l;
w->worldBorder = br;
return w;
fail:
if(w)
free(w->cells);
free(w);
return nullptr;
}
void genWorld(World *w)
{
memset(w->cells, 0, w->cellDataSize());
int x1 = w->worldBorder.xmin + 1;
int x2 = w->worldBorder.xmax - 1;
int y1 = w->worldBorder.ymin + 1;
int y2 = w->worldBorder.ymax - 1;
for(int x = x1; x <= x2; x++) {
w->cellAt(x, y1) = 1; // TODO: Use cobblestone enum?
w->cellAt(x, y2) = 1;
}
for(int y = y1; y <= y2; y++) {
w->cellAt(x1, y) = 1;
w->cellAt(x2, y) = 1;
}
for(int x = -1; x <= +1; x++)
for(int y = -1; y <= +1; y++) {
if(x || y)
w->cellAt(x, y) = 1;
}
}
void freeWorld(World *w)
{
free(w->cells);
free(w);
}
} /* namespace Nooncraft */

92
src/world.h Normal file
View File

@ -0,0 +1,92 @@
// nooncraft.world: World data and structures
#pragma once
#include "block.h"
#include <stddef.h>
#include <gint/defs/attributes.h>
struct WorldCoord
{
int16_t x, y;
constexpr WorldCoord(): x {0}, y {0} {}
constexpr WorldCoord(int _x, int _y): x {(int16_t)_x}, y {(int16_t)_y} {}
inline constexpr WorldCoord &operator+=(WorldCoord const &other) {
this->x += other.x;
this->y += other.y;
return *this;
}
inline constexpr WorldCoord operator-=(WorldCoord const &other) {
this->x -= other.x;
this->y -= other.y;
return *this;
}
static WorldCoord Up, Down, Left, Right;
};
inline constexpr WorldCoord operator+(WorldCoord l, WorldCoord const &r) {
return (l += r);
}
inline constexpr WorldCoord operator-(WorldCoord l, WorldCoord const &r) {
return (l -= r);
}
struct WorldRect
{
/* All included */
int16_t xmin, xmax;
int16_t ymin, ymax;
constexpr WorldRect(): xmin {0}, xmax {0}, ymin {0}, ymax {0} {}
constexpr WorldRect(int _xmin, int _xmax, int _ymin, int _ymax):
xmin {(int16_t)_xmin}, xmax {(int16_t)_xmax},
ymin {(int16_t)_ymin}, ymax {(int16_t)_ymax} {}
int width() const {
return xmax - xmin + 1;
}
int height() const {
return ymax - ymin + 1;
}
};
struct World
{
/* Map data limits (ie. how far blocks exist) */
WorldRect limits;
/* World border position */
WorldRect worldBorder;
/* Block cells in row-major order */
Block *cells;
int cellCount() const {
return limits.width() * limits.height();
}
size_t cellDataSize() const {
return cellCount() * sizeof(*cells);
}
GINLINE Block const &cellAt(int16_t x, int16_t y) const {
int ix = (x + limits.xmin) + (y + limits.ymin) * limits.width();
return cells[ix];
}
GINLINE Block &cellAt(int16_t x, int16_t y) {
int ix = (x + limits.xmin) + (y + limits.ymin) * limits.width();
return cells[ix];
}
};
namespace Nooncraft {
/* Make an empty uninitialized world map. */
World *mkWorld(int width, int height);
/* Run the world generator, initializing the entire world. */
void genWorld(World *w);
/* Free a world object. */
void freeWorld(World *w);
} /* namespace Nooncraft */