commit b50d20aa6344cb845ad21b5fd0cdccc6fca23ecf
parent 47f7871a37c91a4b50f73b680432929a776330a5
Author: beep <beep@wimdupont.com>
Date: Tue, 27 Oct 2026 10:22:47 +0000
Extract reusable engine interfaces and fixed-step game loop
Separate game actions and presentation from SDL, encapsulate media resources, and share the headless engine with an independent courier example.
Advance simulation with bounded fixed ticks, preserve pause behavior, and add timing, game-flow, and media regression coverage. Document remaining engine work and its goals.
Diffstat:
25 files changed, 2204 insertions(+), 1421 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,5 +1,8 @@
BIN = bin/phantasia
SMOKE = bin/ph-smoke
+GAME_SMOKE = bin/ph-game-smoke
+ENGINE_LIB = bin/libph-engine.a
+EXAMPLE = bin/ph-courier
RENDER_SMOKE = bin/ph-render-smoke
MAPC = bin/ph-mapc
OBJDIR = obj
@@ -28,26 +31,21 @@ AUDIO = \
assets/vendor/audio/rpg-sound-pack/interact.wav \
assets/vendor/audio/rpg-sound-pack/trade.wav
-SRC = \
- src/engine/area.c \
- src/engine/audio.c \
- src/engine/battle.c \
- src/engine/render.c \
- src/engine/save.c \
- src/engine/world.c \
- src/game/content.c \
- src/game/presentation.c \
- src/game/main.c
-
+ENGINE_SRC = src/engine/world.c src/engine/area.c src/engine/battle.c src/engine/save.c \
+ src/engine/audio.c src/engine/render.c src/engine/clock.c
+GAME_SRC = src/game/content.c src/game/game.c
+SRC = $(GAME_SRC) src/game/main.c
MAPC_SRC = src/tools/mapc.c src/game/content.c
CONFIG = config.h
OBJ = $(SRC:src/%.c=$(OBJDIR)/%.o)
-SMOKE_OBJ = $(OBJDIR)/tests/smoke.o
-RENDER_SMOKE_OBJ = $(OBJDIR)/tests/render.o
-CORE_OBJ = $(filter-out $(OBJDIR)/engine/render.o $(OBJDIR)/game/main.o \
- $(OBJDIR)/game/presentation.o,$(OBJ))
-DEP = $(OBJ:.o=.d) $(SMOKE_OBJ:.o=.d) $(RENDER_SMOKE_OBJ:.o=.d)
+ENGINE_OBJ = $(ENGINE_SRC:src/%.c=$(OBJDIR)/headless/%.o)
+HEADLESS_GAME_OBJ = $(GAME_SRC:src/%.c=$(OBJDIR)/headless/%.o)
+RENDER_OBJ = $(filter-out $(OBJDIR)/game/main.o,$(OBJ)) $(OBJDIR)/tests/render.o
+DEP = $(ENGINE_OBJ:.o=.d) $(OBJ:.o=.d) $(HEADLESS_GAME_OBJ:.o=.d) \
+ $(OBJDIR)/tests/render.d $(OBJDIR)/headless/tests/smoke.d \
+ $(OBJDIR)/headless/tests/game.d $(OBJDIR)/headless/examples/courier.d \
+ $(OBJDIR)/headless/game/presentation.d
SDL3_CFLAGS = $(shell pkg-config --cflags sdl3 2>/dev/null)
SDL3_LDLIBS = $(shell pkg-config --libs sdl3 2>/dev/null)
@@ -57,6 +55,7 @@ CFLAGS = -std=c99 -Wall -Wextra -Wpedantic -O2 -MMD -MP
LDLIBS = -lm
ifneq ($(strip $(SDL3_LDLIBS)),)
+SRC += src/engine/render_sdl.c src/engine/audio_sdl.c src/game/presentation.c
CPPFLAGS += -DPH_USE_SDL=1
CFLAGS += $(SDL3_CFLAGS)
LDLIBS += $(SDL3_LDLIBS)
@@ -64,25 +63,38 @@ else
CPPFLAGS += -DPH_USE_SDL=0
endif
-.PHONY: all clean render-smoke smoke
+.PHONY: all clean render-smoke smoke game-smoke engine-example check
all: $(BIN) $(MAPBIN) $(TILESET_BMP) $(HERO_BMP) $(ITEMS_BMP) $(CRITTERS_BMP)
-$(BIN): $(OBJ) $(MAPBIN) $(TILESET_BMP) $(HERO_BMP) $(ITEMS_BMP) $(CRITTERS_BMP) $(AUDIO)
+$(BIN): $(OBJ) $(ENGINE_LIB) $(MAPBIN) $(TILESET_BMP) $(HERO_BMP) $(ITEMS_BMP) $(CRITTERS_BMP) $(AUDIO)
+ @mkdir -p $(dir $@)
+ $(CC) $(OBJ) $(ENGINE_LIB) -o $@ $(LDLIBS)
+
+$(SMOKE): $(HEADLESS_GAME_OBJ) $(OBJDIR)/headless/tests/smoke.o $(ENGINE_LIB)
@mkdir -p $(dir $@)
- $(CC) $(OBJ) -o $@ $(LDLIBS)
+ $(CC) $^ -o $@ -lm
+
+$(ENGINE_LIB): $(ENGINE_OBJ)
+ @mkdir -p $(dir $@)
+ $(AR) rcs $@ $^
+
+$(EXAMPLE): $(OBJDIR)/headless/examples/courier.o $(ENGINE_LIB)
+ $(CC) $^ -o $@ -lm
-$(SMOKE): $(CORE_OBJ) $(SMOKE_OBJ)
+$(GAME_SMOKE): $(HEADLESS_GAME_OBJ) $(OBJDIR)/headless/tests/game.o $(ENGINE_LIB)
@mkdir -p $(dir $@)
- $(CC) $^ -o $@ $(LDLIBS)
+ $(CC) $^ -o $@ -lm
-$(RENDER_SMOKE): $(CORE_OBJ) $(OBJDIR)/engine/render.o \
- $(OBJDIR)/game/presentation.o $(RENDER_SMOKE_OBJ) $(MAPBIN) \
+# Headless targets never receive SDL headers, compiler flags, or libraries.
+$(OBJDIR)/headless/%.o: src/%.c $(CONFIG)
+ @mkdir -p $(dir $@)
+ $(CC) -I. -Isrc -DPH_USE_SDL=0 $(filter-out $(SDL3_CFLAGS),$(CFLAGS)) -c $< -o $@
+
+$(RENDER_SMOKE): $(RENDER_OBJ) $(ENGINE_LIB) $(MAPBIN) \
$(TILESET_BMP) $(HERO_BMP) $(ITEMS_BMP) $(CRITTERS_BMP) $(AUDIO)
@mkdir -p $(dir $@)
- $(CC) $(CORE_OBJ) $(OBJDIR)/engine/render.o $(OBJDIR)/game/presentation.o \
- $(RENDER_SMOKE_OBJ) \
- -o $@ $(LDLIBS)
+ $(CC) $(RENDER_OBJ) $(ENGINE_LIB) -o $@ $(LDLIBS)
$(MAPC): $(MAPC_SRC) $(CONFIG)
@mkdir -p $(dir $@)
@@ -92,7 +104,7 @@ $(OBJDIR)/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
-$(OBJ) $(SMOKE_OBJ) $(RENDER_SMOKE_OBJ): $(CONFIG)
+$(OBJ) $(OBJDIR)/tests/render.o: $(CONFIG)
$(CONFIG):
cp config.def.h $@
@@ -120,6 +132,14 @@ $(CRITTERS_BMP): $(CRITTERS_SRC)
smoke: $(MAPBIN) $(SMOKE)
$(SMOKE)
+game-smoke: $(MAPBIN) $(GAME_SMOKE)
+ $(GAME_SMOKE)
+
+engine-example: $(EXAMPLE)
+ $(EXAMPLE) --smoke
+
+check: smoke game-smoke engine-example render-smoke $(OBJDIR)/headless/game/presentation.o
+
render-smoke: $(RENDER_SMOKE)
SDL_AUDIODRIVER=dummy SDL_VIDEODRIVER=dummy $(RENDER_SMOKE)
diff --git a/README.adoc b/README.adoc
@@ -28,20 +28,49 @@ Phantasia's details may change while these principles remain stable:
== Architecture
-Phantasia is a game, not a general-purpose engine product. Its systems are
-nevertheless engine-like: they operate on entities, items, interactions, area
-markers, and caller-supplied catalogs rather than named game content. Here,
-"engine" means the reusable internal layer, not a separately packaged framework
-or a promise of API compatibility.
+Phantasia uses a small, reusable 2D RPG engine. Its systems operate on entities,
+items, interactions, area markers, and caller-supplied catalogs rather than named
+game content. The headless engine builds as `bin/libph-engine.a`, shared by
+Phantasia and the independent Parcel Run example. It is not yet a standalone
+SDK with a stable public API.
`src/engine` contains reusable systems and must never include `game/*`.
`src/game` owns Phantasia's content, menus and overlays, and application startup;
it may depend on the engine. Generic code belongs in the engine whenever it can
operate entirely on engine types and supplied data.
-Core logic remains headless-testable. SDL rendering is an optional engine layer,
-while game-specific presentation and application orchestration stay outside the
-simulation.
+Core logic and game flow remain headless-testable. `game/game` owns menu state,
+action routing, and world/battle transitions; `game/presentation` draws it.
+`game/main` translates SDL input into game actions and owns application lifecycle
+and media playback. Escape and Backspace remain separate actions.
+
+Rendering and audio use opaque engine contexts, asset IDs, and engine-owned
+rectangles, colors, and flip modes. SDL textures, streams, and devices live in
+the SDL backend implementation; `engine/sdl.h` is the host's construction bridge.
+The renderer owns its loaded textures and borrows the host's SDL render target,
+so it must be destroyed before that target. Audio owns its clips and device.
+
+World drawing and game presentation compile without SDL headers. The host supplies
+configured key labels as text, and only backend code issues SDL drawing/audio
+calls. The current backend is SDL; this boundary permits another implementation
+without requiring a runtime plugin system. Headless executables do not link SDL,
+even when it is installed.
+The host advances simulation at a fixed tick rate, configured by
+`PH_SIMULATION_HZ`. `PH_MAX_CATCHUP_STEPS` bounds work after a slow frame; excess
+time is discarded rather than left as a growing backlog. Integer clock credit
+retains fractional ticks without floating-point scheduling drift. Rendering runs
+once per host frame and currently displays the latest simulation state without
+interpolation. Held input is sampled per host frame; brief interaction presses
+are retained until a simulation tick runs.
+
+Menus discard paused time. Input-driven menu/mode/battle-phase changes discard
+the current frame's elapsed time so a resumed game cannot catch up paused time.
+Interactions and presentation events are handled after each simulation tick.
+Identical input per tick and random seeds reproduce simulation within the same
+build; this does not promise identical results across platforms.
+
+Camera state and compile-time simulation configuration still belong to the
+existing engine; this is an incremental separation, not a general-purpose SDK.
For the current implementation, start with `src/engine/*.h` for the engine's
public data types and operations, `src/game/content.c` for a concrete game setup,
@@ -85,6 +114,26 @@ Exercise SDL rendering in a headless environment:
make render-smoke
----
+Run all checks, including headless menu/battle flow, the independent example,
+SDL-free presentation compilation, and media pixel/lifecycle checks:
+
+[source,sh]
+----
+make check
+----
+
+Run the SDL-free Parcel Run example, which supplies its own map, content, terminal
+UI, and delivery rule without linking any `game/*` code:
+
+[source,sh]
+----
+make engine-example # builds and checks a scripted delivery
+./bin/ph-courier # WASD then Enter; Q to quit
+----
+
+`make smoke game-smoke engine-example` requires no SDL or graphics conversion.
+`make render-smoke` additionally exercises the SDL backend using dummy drivers.
+
With SDL3 available:
[source,sh]
diff --git a/TODO.adoc b/TODO.adoc
@@ -0,0 +1,23 @@
+= TODO
+
+Keep Phantasia's engine small and reusable for other 2D RPGs. Game content and
+rules should sit above the engine, platform details should stay in backends, and
+simulation should be reproducible enough to test without a window. Prefer changes
+that improve the game or demonstrate reuse over speculative framework features.
+
+The shared engine library, independent Parcel Run example, SDL-independent game
+flow and media interfaces, and fixed simulation ticks provide the foundation.
+
+* Separate camera state from `PhWorld` and remove viewport parameters from world
+ startup and save loading. Simulation and persistence should not depend on how
+ the world is viewed.
+* Validate content catalogs and map references before play. Report broken IDs,
+ destinations, and definitions early, with enough context to fix the data.
+* Add a small debug overlay for collision bounds, AI destinations, frame timings,
+ and event overflow. Make gameplay and performance problems easier to diagnose.
+* Record and replay input per simulation tick with a known random seed. Turn
+ reproducible gameplay bugs into regression tests; do not assume cross-platform
+ floating-point determinism.
+
+Separate SDK packaging, API stability, editors, scripting, and additional graphics
+backends can wait until a concrete game needs them.
diff --git a/config.def.h b/config.def.h
@@ -1,14 +1,15 @@
#ifndef CONFIG_H
#define CONFIG_H
-#define PH_CONFIG_VERSION 15
+#define PH_CONFIG_VERSION 16
#define PH_VIEW_W 320
#define PH_VIEW_H 240
#define PH_SCALE 3
#define PH_WINDOW_TITLE "phantasia"
#define PH_SAVE_PATH "phantasia.sav"
-#define PH_MAX_FRAME_TIME 0.05f
+#define PH_SIMULATION_HZ 60
+#define PH_MAX_CATCHUP_STEPS 4
#define PH_FRAME_DELAY_MS 16
#define PH_MUSIC_GAIN 0.24f
#define PH_EFFECT_GAIN 0.55f
diff --git a/src/engine/assets.h b/src/engine/assets.h
@@ -0,0 +1,21 @@
+#ifndef PH_ENGINE_ASSETS_H
+#define PH_ENGINE_ASSETS_H
+
+/* Caller-owned asset descriptions; loading belongs to the backend. */
+typedef struct {
+ unsigned char r;
+ unsigned char g;
+ unsigned char b;
+} PhColorKey;
+
+typedef struct {
+ const char *path;
+ PhColorKey key;
+} PhAssetDef;
+
+typedef struct {
+ const char *path;
+ float gain;
+} PhAudioDef;
+
+#endif
diff --git a/src/engine/audio.c b/src/engine/audio.c
@@ -17,129 +17,3 @@ ph_audio_distance_gain(float listener_x, float listener_y,
if (distance >= max_distance) return 0.0f;
return 1.0f - distance / max_distance;
}
-
-#if PH_USE_SDL
-#include <string.h>
-
-static int
-ph_audio_stream_clip(SDL_AudioStream *stream, const PhAudioClip *clip,
- const SDL_AudioSpec *device_spec)
-{
- if (!stream || !clip || !clip->data || !clip->length) return -1;
- if (!SDL_ClearAudioStream(stream) ||
- !SDL_SetAudioStreamFormat(stream, &clip->spec, device_spec) ||
- !SDL_SetAudioStreamGain(stream, clip->gain) ||
- !SDL_PutAudioStreamData(stream, clip->data, (int)clip->length))
- return -1;
- return 0;
-}
-
-int
-ph_audio_init(PhAudio *audio, const PhAudioDef *defs, int count)
-{
- int i;
-
- if (!audio || !defs || count <= 1 || count > PH_AUDIO_CLIP_MAX) return -1;
- memset(audio, 0, sizeof(*audio));
- audio->clip_count = count;
- audio->music_enabled = 1;
- audio->effects_enabled = 1;
- for (i = 1; i < count; ++i) {
- audio->clips[i].gain = defs[i].gain;
- if (!defs[i].path || !SDL_LoadWAV(defs[i].path, &audio->clips[i].spec,
- &audio->clips[i].data, &audio->clips[i].length)) goto fail;
- }
- audio->device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
- if (!audio->device || !SDL_GetAudioDeviceFormat(audio->device,
- &audio->device_spec, NULL)) goto fail;
- audio->music_stream = SDL_CreateAudioStream(&audio->clips[1].spec,
- &audio->device_spec);
- if (!audio->music_stream ||
- !SDL_BindAudioStream(audio->device, audio->music_stream)) goto fail;
- for (i = 0; i < PH_AUDIO_EFFECT_VOICES; ++i) {
- audio->effect_streams[i] = SDL_CreateAudioStream(&audio->clips[1].spec,
- &audio->device_spec);
- if (!audio->effect_streams[i] || !SDL_BindAudioStream(audio->device,
- audio->effect_streams[i])) goto fail;
- }
- return 0;
-
-fail:
- ph_audio_destroy(audio);
- return -1;
-}
-
-void
-ph_audio_music(PhAudio *audio, int id)
-{
- if (!audio || id <= 0 || id >= audio->clip_count ||
- id == audio->music_id) return;
- if (ph_audio_stream_clip(audio->music_stream, &audio->clips[id],
- &audio->device_spec) < 0) return;
- audio->music_id = id;
- if (!audio->music_enabled) SDL_SetAudioStreamGain(audio->music_stream, 0.0f);
-}
-
-void
-ph_audio_effect(PhAudio *audio, int id, float gain)
-{
- SDL_AudioStream *stream;
-
- if (!audio || !audio->effects_enabled ||
- id <= 0 || id >= audio->clip_count || gain <= 0.0f) return;
- stream = audio->effect_streams[audio->next_effect_voice];
- audio->next_effect_voice =
- (audio->next_effect_voice + 1) % PH_AUDIO_EFFECT_VOICES;
- if (ph_audio_stream_clip(stream, &audio->clips[id],
- &audio->device_spec) == 0)
- SDL_SetAudioStreamGain(stream, audio->clips[id].gain * gain);
-}
-
-void
-ph_audio_set_enabled(PhAudio *audio, int music, int effects)
-{
- if (!audio) return;
- music = !!music;
- effects = !!effects;
- if (audio->music_enabled != music) {
- audio->music_enabled = music;
- SDL_SetAudioStreamGain(audio->music_stream,
- music && audio->music_id > 0 ? audio->clips[audio->music_id].gain : 0.0f);
- }
- if (audio->effects_enabled != effects) {
- int i;
-
- audio->effects_enabled = effects;
- if (!effects)
- for (i = 0; i < PH_AUDIO_EFFECT_VOICES; ++i)
- SDL_ClearAudioStream(audio->effect_streams[i]);
- }
-}
-
-void
-ph_audio_update(PhAudio *audio)
-{
- const PhAudioClip *clip;
-
- if (!audio || audio->music_id <= 0 ||
- audio->music_id >= audio->clip_count) return;
- clip = &audio->clips[audio->music_id];
- if (SDL_GetAudioStreamQueued(audio->music_stream) < (int)clip->length / 2)
- SDL_PutAudioStreamData(audio->music_stream, clip->data, (int)clip->length);
-}
-
-void
-ph_audio_destroy(PhAudio *audio)
-{
- int i;
-
- if (!audio) return;
- if (audio->music_stream) SDL_DestroyAudioStream(audio->music_stream);
- for (i = 0; i < PH_AUDIO_EFFECT_VOICES; ++i)
- if (audio->effect_streams[i])
- SDL_DestroyAudioStream(audio->effect_streams[i]);
- if (audio->device) SDL_CloseAudioDevice(audio->device);
- for (i = 0; i < audio->clip_count; ++i) SDL_free(audio->clips[i].data);
- memset(audio, 0, sizeof(*audio));
-}
-#endif
diff --git a/src/engine/audio.h b/src/engine/audio.h
@@ -1,45 +1,20 @@
#ifndef PH_ENGINE_AUDIO_H
#define PH_ENGINE_AUDIO_H
-typedef struct {
- const char *path;
- float gain;
-} PhAudioDef;
+#include "engine/assets.h"
+
+typedef struct PhAudio PhAudio;
float ph_audio_distance_gain(float listener_x, float listener_y,
float source_x, float source_y, float max_distance);
-#if PH_USE_SDL
-#include <SDL3/SDL.h>
-
-enum { PH_AUDIO_CLIP_MAX = 64, PH_AUDIO_EFFECT_VOICES = 8 };
-
-typedef struct {
- SDL_AudioSpec spec;
- Uint8 *data;
- Uint32 length;
- float gain;
-} PhAudioClip;
-
-typedef struct {
- SDL_AudioDeviceID device;
- SDL_AudioStream *music_stream;
- SDL_AudioStream *effect_streams[PH_AUDIO_EFFECT_VOICES];
- SDL_AudioSpec device_spec;
- PhAudioClip clips[PH_AUDIO_CLIP_MAX];
- int clip_count;
- int music_id;
- int music_enabled;
- int effects_enabled;
- int next_effect_voice;
-} PhAudio;
-
-int ph_audio_init(PhAudio *audio, const PhAudioDef *defs, int count);
+/* NULL audio is safe: playback is optional. IDs index the creation catalog;
+ * index zero means no clip. Destroy releases the context and all its resources. */
void ph_audio_music(PhAudio *audio, int id);
void ph_audio_effect(PhAudio *audio, int id, float gain);
void ph_audio_set_enabled(PhAudio *audio, int music, int effects);
+void ph_audio_get_enabled(const PhAudio *audio, int *music, int *effects);
void ph_audio_update(PhAudio *audio);
void ph_audio_destroy(PhAudio *audio);
-#endif
#endif
diff --git a/src/engine/audio_sdl.c b/src/engine/audio_sdl.c
@@ -0,0 +1,156 @@
+#include "engine/sdl.h"
+
+#if PH_USE_SDL
+enum { PH_AUDIO_CLIP_MAX = 64, PH_AUDIO_EFFECT_VOICES = 8 };
+
+typedef struct {
+ SDL_AudioSpec spec;
+ Uint8 *data;
+ Uint32 length;
+ float gain;
+} PhAudioClip;
+
+struct PhAudio {
+ SDL_AudioDeviceID device;
+ SDL_AudioStream *music_stream;
+ SDL_AudioStream *effect_streams[PH_AUDIO_EFFECT_VOICES];
+ SDL_AudioSpec device_spec;
+ PhAudioClip clips[PH_AUDIO_CLIP_MAX];
+ int clip_count;
+ int music_id;
+ int music_enabled;
+ int effects_enabled;
+ int next_effect_voice;
+};
+
+static int
+ph_audio_stream_clip(SDL_AudioStream *stream, const PhAudioClip *clip,
+ const SDL_AudioSpec *device_spec)
+{
+ if (!stream || !clip || !clip->data || !clip->length) return -1;
+ if (!SDL_ClearAudioStream(stream) ||
+ !SDL_SetAudioStreamFormat(stream, &clip->spec, device_spec) ||
+ !SDL_SetAudioStreamGain(stream, clip->gain) ||
+ !SDL_PutAudioStreamData(stream, clip->data, (int)clip->length))
+ return -1;
+ return 0;
+}
+
+PhAudio *
+ph_audio_sdl_create(const PhAudioDef *defs, int count)
+{
+ PhAudio *audio;
+ int i;
+
+ if (!defs || count <= 1 || count > PH_AUDIO_CLIP_MAX) return NULL;
+ audio = SDL_calloc(1, sizeof(*audio));
+ if (!audio) return NULL;
+ audio->clip_count = count;
+ audio->music_enabled = 1;
+ audio->effects_enabled = 1;
+ for (i = 1; i < count; ++i) {
+ audio->clips[i].gain = defs[i].gain;
+ if (!defs[i].path || !SDL_LoadWAV(defs[i].path, &audio->clips[i].spec,
+ &audio->clips[i].data, &audio->clips[i].length)) goto fail;
+ }
+ audio->device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
+ if (!audio->device || !SDL_GetAudioDeviceFormat(audio->device,
+ &audio->device_spec, NULL)) goto fail;
+ audio->music_stream = SDL_CreateAudioStream(&audio->clips[1].spec,
+ &audio->device_spec);
+ if (!audio->music_stream ||
+ !SDL_BindAudioStream(audio->device, audio->music_stream)) goto fail;
+ for (i = 0; i < PH_AUDIO_EFFECT_VOICES; ++i) {
+ audio->effect_streams[i] = SDL_CreateAudioStream(&audio->clips[1].spec,
+ &audio->device_spec);
+ if (!audio->effect_streams[i] || !SDL_BindAudioStream(audio->device,
+ audio->effect_streams[i])) goto fail;
+ }
+ return audio;
+
+fail:
+ ph_audio_destroy(audio);
+ return NULL;
+}
+
+void
+ph_audio_music(PhAudio *audio, int id)
+{
+ if (!audio || id <= 0 || id >= audio->clip_count ||
+ id == audio->music_id) return;
+ if (ph_audio_stream_clip(audio->music_stream, &audio->clips[id],
+ &audio->device_spec) < 0) return;
+ audio->music_id = id;
+ if (!audio->music_enabled) SDL_SetAudioStreamGain(audio->music_stream, 0.0f);
+}
+
+void
+ph_audio_effect(PhAudio *audio, int id, float gain)
+{
+ SDL_AudioStream *stream;
+
+ if (!audio || !audio->effects_enabled ||
+ id <= 0 || id >= audio->clip_count || gain <= 0.0f) return;
+ stream = audio->effect_streams[audio->next_effect_voice];
+ audio->next_effect_voice =
+ (audio->next_effect_voice + 1) % PH_AUDIO_EFFECT_VOICES;
+ if (ph_audio_stream_clip(stream, &audio->clips[id],
+ &audio->device_spec) == 0)
+ SDL_SetAudioStreamGain(stream, audio->clips[id].gain * gain);
+}
+
+void
+ph_audio_set_enabled(PhAudio *audio, int music, int effects)
+{
+ if (!audio) return;
+ music = !!music;
+ effects = !!effects;
+ if (audio->music_enabled != music) {
+ audio->music_enabled = music;
+ SDL_SetAudioStreamGain(audio->music_stream,
+ music && audio->music_id > 0 ? audio->clips[audio->music_id].gain : 0.0f);
+ }
+ if (audio->effects_enabled != effects) {
+ int i;
+
+ audio->effects_enabled = effects;
+ if (!effects)
+ for (i = 0; i < PH_AUDIO_EFFECT_VOICES; ++i)
+ SDL_ClearAudioStream(audio->effect_streams[i]);
+ }
+}
+
+void
+ph_audio_update(PhAudio *audio)
+{
+ const PhAudioClip *clip;
+
+ if (!audio || audio->music_id <= 0 ||
+ audio->music_id >= audio->clip_count) return;
+ clip = &audio->clips[audio->music_id];
+ if (SDL_GetAudioStreamQueued(audio->music_stream) < (int)clip->length / 2)
+ SDL_PutAudioStreamData(audio->music_stream, clip->data, (int)clip->length);
+}
+
+void
+ph_audio_destroy(PhAudio *audio)
+{
+ int i;
+
+ if (!audio) return;
+ if (audio->music_stream) SDL_DestroyAudioStream(audio->music_stream);
+ for (i = 0; i < PH_AUDIO_EFFECT_VOICES; ++i)
+ if (audio->effect_streams[i])
+ SDL_DestroyAudioStream(audio->effect_streams[i]);
+ if (audio->device) SDL_CloseAudioDevice(audio->device);
+ for (i = 0; i < audio->clip_count; ++i) SDL_free(audio->clips[i].data);
+ SDL_free(audio);
+}
+
+void
+ph_audio_get_enabled(const PhAudio *audio, int *music, int *effects)
+{
+ if (music) *music = audio && audio->music_enabled;
+ if (effects) *effects = audio && audio->effects_enabled;
+}
+#endif
diff --git a/src/engine/clock.c b/src/engine/clock.c
@@ -0,0 +1,21 @@
+#include "engine/clock.h"
+
+int
+ph_clock_steps(PhClock *clock, uint64_t elapsed_ns, int hz, int max_steps)
+{
+ const uint64_t second = 1000000000;
+ uint64_t budget;
+ int steps;
+
+ if (hz <= 0 || max_steps <= 0) {
+ clock->credit = 0;
+ return 0;
+ }
+ budget = (uint64_t)max_steps * second;
+ if (clock->credit >= budget || elapsed_ns > (budget - clock->credit) / (unsigned)hz)
+ clock->credit = budget;
+ else clock->credit += elapsed_ns * (unsigned)hz;
+ steps = (int)(clock->credit / second);
+ clock->credit %= second;
+ return steps;
+}
diff --git a/src/engine/clock.h b/src/engine/clock.h
@@ -0,0 +1,13 @@
+#ifndef PH_ENGINE_CLOCK_H
+#define PH_ENGINE_CLOCK_H
+
+#include <stdint.h>
+
+/* Credit is nanoseconds multiplied by the requested tick rate. */
+typedef struct { uint64_t credit; } PhClock;
+
+/* Returns fixed ticks due, retaining fractional credit. Excess elapsed time
+ * beyond max_steps is discarded. Keep hz constant until the clock is reset. */
+int ph_clock_steps(PhClock *clock, uint64_t elapsed_ns, int hz, int max_steps);
+
+#endif
diff --git a/src/engine/render.c b/src/engine/render.c
@@ -1,96 +1,32 @@
#include "engine/render.h"
#include <math.h>
-#include <stdio.h>
-#include <string.h>
#include "config.h"
-#if PH_USE_SDL
static float
ph_world_pixel(float position, float camera)
{
return floorf(position + 0.5f) - floorf(camera + 0.5f);
}
-static SDL_Texture *
-ph_load_sprite_sheet(SDL_Renderer *renderer, const PhAssetDef *def)
-{
- SDL_Surface *surface;
- SDL_Texture *texture;
-
- surface = SDL_LoadBMP(def->path);
- if (!surface) {
- fprintf(stderr, "SDL_LoadBMP failed for %s: %s\n",
- def->path, SDL_GetError());
- return NULL;
- }
-
- SDL_SetSurfaceColorKey(surface, true, SDL_MapSurfaceRGB(surface,
- def->key.r, def->key.g, def->key.b));
- texture = SDL_CreateTextureFromSurface(renderer, surface);
- SDL_DestroySurface(surface);
- if (!texture) {
- fprintf(stderr, "SDL_CreateTextureFromSurface failed: %s\n", SDL_GetError());
- }
- return texture;
-}
-
-int
-ph_render_assets_load(PhRenderAssets *assets, SDL_Renderer *renderer,
- const PhAssetDef *defs, int def_count)
-{
- int i;
-
- memset(assets, 0, sizeof(*assets));
- if (!renderer || def_count < 0 || def_count > PH_MAX_ASSETS ||
- (def_count > 0 && !defs)) return -1;
- assets->texture_count = def_count;
- for (i = 0; i < def_count; ++i) {
- if (!defs[i].path) continue;
- assets->textures[i] = ph_load_sprite_sheet(renderer, &defs[i]);
- if (!assets->textures[i]) {
- ph_render_assets_destroy(assets);
- return -1;
- }
- }
- return 0;
-}
-
-void
-ph_render_assets_destroy(PhRenderAssets *assets)
-{
- int i;
-
- for (i = 0; i < assets->texture_count; ++i)
- SDL_DestroyTexture(assets->textures[i]);
- memset(assets, 0, sizeof(*assets));
-}
-
-SDL_Texture *
-ph_render_asset(const PhRenderAssets *assets, int asset_id)
-{
- return asset_id >= 0 && asset_id < assets->texture_count ?
- assets->textures[asset_id] : NULL;
-}
-
void
-ph_render_sprite(SDL_Renderer *renderer, SDL_Texture *texture,
- int sprite_tile_x, int sprite_tile_y, SDL_FRect dst, SDL_FlipMode flip)
+ph_render_sprite(PhRenderer *renderer, int asset_id,
+ int sprite_tile_x, int sprite_tile_y, PhRect dst, PhFlip flip)
{
- SDL_FRect src = {
+ PhRect src = {
.x = (float)(sprite_tile_x * PH_TILE_SIZE),
.y = (float)(sprite_tile_y * PH_TILE_SIZE),
.w = (float)PH_TILE_SIZE,
.h = (float)PH_TILE_SIZE,
};
- SDL_RenderTextureRotated(renderer, texture, &src, &dst, 0.0, NULL, flip);
+ ph_render_texture(renderer, asset_id, src, dst, flip);
}
void
ph_render_entity_frame(const PhEntityDef *def, const PhEntity *entity,
- int *sprite_tile_x, int *sprite_tile_y, SDL_FlipMode *flip)
+ int *sprite_tile_x, int *sprite_tile_y, PhFlip *flip)
{
int direction = entity->motion.facing_y > 0 ? PH_SPRITE_DOWN :
entity->motion.facing_y < 0 ? PH_SPRITE_UP : PH_SPRITE_SIDE;
@@ -99,11 +35,11 @@ ph_render_entity_frame(const PhEntityDef *def, const PhEntity *entity,
*sprite_tile_x = def->sprite_tiles[direction][frame][0];
*sprite_tile_y = def->sprite_tiles[direction][frame][1];
*flip = direction == PH_SPRITE_SIDE && entity->motion.facing_x > 0 ?
- SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
+ PH_FLIP_HORIZONTAL : PH_FLIP_NONE;
}
static void
-ph_draw_area(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorld *world)
+ph_draw_area(PhRenderer *renderer, const PhWorld *world)
{
int tx;
int ty;
@@ -115,33 +51,32 @@ ph_draw_area(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorld
tx < world->area.width && (float)(tx * PH_TILE_SIZE) <
world->camera.pos.x + world->camera.viewport_w; ++tx) {
const PhTileDef *def = ph_area_tile_def(&world->area, tx, ty);
- SDL_Texture *texture;
- SDL_FRect rect = {
+ int asset_id;
+ PhRect rect = {
.x = ph_world_pixel((float)(tx * PH_TILE_SIZE), world->camera.pos.x),
.y = ph_world_pixel((float)(ty * PH_TILE_SIZE), world->camera.pos.y),
.w = (float)PH_TILE_SIZE,
.h = (float)PH_TILE_SIZE,
};
- SDL_FRect src;
+ PhRect src;
int sprite_tile_x;
int sprite_tile_y;
if (!def) continue;
if (def->background_asset_id) {
- SDL_Texture *background = ph_render_asset(assets,
- def->background_asset_id);
- SDL_FRect background_src = {
+ int background = def->background_asset_id;
+ PhRect background_src = {
.x = (float)(def->background_sprite_tile_x * PH_TILE_SIZE),
.y = (float)(def->background_sprite_tile_y * PH_TILE_SIZE),
.w = (float)PH_TILE_SIZE,
.h = (float)PH_TILE_SIZE,
};
- if (background)
- SDL_RenderTexture(renderer, background, &background_src, &rect);
+ if (ph_render_has_asset(renderer, background))
+ ph_render_texture(renderer, background, background_src, rect, PH_FLIP_NONE);
}
- texture = ph_render_asset(assets, def->asset_id);
- if (!texture) continue;
+ asset_id = def->asset_id;
+ if (!ph_render_has_asset(renderer, asset_id)) continue;
sprite_tile_x = def->sprite_tile_x;
sprite_tile_y = def->sprite_tile_y;
if (def->autotile == PH_TILE_AUTOTILE_3X3) {
@@ -183,28 +118,28 @@ ph_draw_area(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorld
sprite_tile_x += offset_x;
sprite_tile_y += offset_y;
}
- src = (SDL_FRect){
+ src = (PhRect){
.x = (float)(sprite_tile_x * PH_TILE_SIZE),
.y = (float)(sprite_tile_y * PH_TILE_SIZE),
.w = (float)PH_TILE_SIZE,
.h = (float)PH_TILE_SIZE,
};
- SDL_RenderTexture(renderer, texture, &src, &rect);
+ ph_render_texture(renderer, asset_id, src, rect, PH_FLIP_NONE);
}
}
}
static void
-ph_draw_items(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorld *world)
+ph_draw_items(PhRenderer *renderer, const PhWorld *world)
{
int i;
for (i = 0; i < world->ground_item_count; ++i) {
const PhGroundItem *drop = &world->ground_items[i];
const PhItemDef *def = ph_world_item_def(world, drop->item_id);
- SDL_Texture *texture = NULL;
- SDL_FRect rect;
+ int asset_id = -1;
+ PhRect rect;
if (!drop->active || drop->area_id != world->area_id) {
continue;
@@ -215,44 +150,46 @@ ph_draw_items(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorl
rect.w = 6.0f;
rect.h = 6.0f;
- if (def) texture = ph_render_asset(assets, def->asset_id);
- if (texture && def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0) {
+ if (def) asset_id = def->asset_id;
+ if (ph_render_has_asset(renderer, asset_id) &&
+ def->sprite_tile_x >= 0 && def->sprite_tile_y >= 0) {
rect.x = ph_world_pixel(drop->pos.x, world->camera.pos.x) -
PH_TILE_SIZE * 0.5f;
rect.y = ph_world_pixel(drop->pos.y, world->camera.pos.y) -
PH_TILE_SIZE * 0.5f;
rect.w = PH_TILE_SIZE;
rect.h = PH_TILE_SIZE;
- ph_render_sprite(renderer, texture,
- def->sprite_tile_x, def->sprite_tile_y, rect, SDL_FLIP_NONE);
+ ph_render_sprite(renderer, asset_id,
+ def->sprite_tile_x, def->sprite_tile_y, rect, PH_FLIP_NONE);
} else {
- SDL_SetRenderDrawColor(renderer, PH_ITEM_COLOR);
- SDL_RenderFillRect(renderer, &rect);
+ ph_render_color(renderer, (PhColor){ PH_ITEM_COLOR });
+ ph_render_fill(renderer, rect);
}
}
}
static void
-ph_draw_entities(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhWorld *world)
+ph_draw_entities(PhRenderer *renderer, const PhWorld *world)
{
int i;
for (i = 0; i < world->entity_count; ++i) {
const PhEntity *entity = &world->entities[i];
const PhEntityDef *def = ph_world_entity_def(world, entity->type_id);
- SDL_Texture *texture = NULL;
- SDL_FRect rect;
+ int asset_id = -1;
+ PhRect rect;
int sprite_tile_x;
int sprite_tile_y;
- SDL_FlipMode flip;
+ PhFlip flip;
if (!entity->active || entity->area_id != world->area_id || !def) {
continue;
}
- texture = ph_render_asset(assets, def->asset_id);
+ asset_id = def->asset_id;
ph_render_entity_frame(def, entity, &sprite_tile_x, &sprite_tile_y, &flip);
- if (texture && sprite_tile_x >= 0 && sprite_tile_y >= 0) {
+ if (ph_render_has_asset(renderer, asset_id) &&
+ sprite_tile_x >= 0 && sprite_tile_y >= 0) {
rect.x = ph_world_pixel(entity->pos.x, world->camera.pos.x) -
PH_TILE_SIZE * 0.5f;
@@ -261,7 +198,7 @@ ph_draw_entities(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhW
rect.w = PH_TILE_SIZE;
rect.h = PH_TILE_SIZE;
- ph_render_sprite(renderer, texture, sprite_tile_x, sprite_tile_y, rect, flip);
+ ph_render_sprite(renderer, asset_id, sprite_tile_x, sprite_tile_y, rect, flip);
} else {
rect.x = ph_world_pixel(entity->pos.x, world->camera.pos.x) - 7.0f;
rect.y = ph_world_pixel(entity->pos.y, world->camera.pos.y) - 12.0f;
@@ -269,20 +206,20 @@ ph_draw_entities(SDL_Renderer *renderer, const PhRenderAssets *assets, const PhW
rect.h = 18.0f;
if (def->kind == PH_ENTITY_MONSTER) {
- SDL_SetRenderDrawColor(renderer, PH_MONSTER_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MONSTER_COLOR });
} else {
- SDL_SetRenderDrawColor(renderer, PH_NPC_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_NPC_COLOR });
}
- SDL_RenderFillRect(renderer, &rect);
+ ph_render_fill(renderer, rect);
}
}
}
+
void
-ph_render_world(SDL_Renderer *renderer, const PhRenderAssets *assets,
+ph_render_world(PhRenderer *renderer,
const PhWorld *world)
{
- ph_draw_area(renderer, assets, world);
- ph_draw_items(renderer, assets, world);
- ph_draw_entities(renderer, assets, world);
+ ph_draw_area(renderer, world);
+ ph_draw_items(renderer, world);
+ ph_draw_entities(renderer, world);
}
-#endif
diff --git a/src/engine/render.h b/src/engine/render.h
@@ -2,39 +2,32 @@
#define PH_ENGINE_RENDER_H
#include "engine/world.h"
-
-enum { PH_MAX_ASSETS = 32 };
-
-typedef struct {
- unsigned char r;
- unsigned char g;
- unsigned char b;
-} PhColorKey;
-
-typedef struct {
- const char *path;
- PhColorKey key;
-} PhAssetDef;
-
-#if PH_USE_SDL
-#include <SDL3/SDL.h>
-
-typedef struct {
- SDL_Texture *textures[PH_MAX_ASSETS];
- int texture_count;
-} PhRenderAssets;
-
-int ph_render_assets_load(PhRenderAssets *assets, SDL_Renderer *renderer,
- const PhAssetDef *defs, int def_count);
-void ph_render_assets_destroy(PhRenderAssets *assets);
-SDL_Texture *ph_render_asset(const PhRenderAssets *assets, int asset_id);
-void ph_render_sprite(SDL_Renderer *renderer, SDL_Texture *texture,
- int sprite_tile_x, int sprite_tile_y, SDL_FRect dst, SDL_FlipMode flip);
+#include "engine/assets.h"
+
+enum { PH_MAX_ASSETS = 32, PH_RENDER_FONT_W = 8 };
+
+typedef struct PhRenderer PhRenderer;
+typedef struct { float x, y, w, h; } PhRect;
+typedef struct { unsigned char r, g, b, a; } PhColor;
+typedef enum { PH_FLIP_NONE, PH_FLIP_HORIZONTAL } PhFlip;
+
+/* Asset IDs are indices into the catalog passed at creation. Missing IDs are
+ * safe to draw (a no-op); use has_asset when a fallback graphic is needed. */
+int ph_render_has_asset(const PhRenderer *renderer, int asset_id);
+void ph_render_destroy(PhRenderer *renderer);
+void ph_render_color(PhRenderer *renderer, PhColor color);
+void ph_render_clear(PhRenderer *renderer);
+void ph_render_fill(PhRenderer *renderer, PhRect rect);
+void ph_render_outline(PhRenderer *renderer, PhRect rect);
+/* Built-in fixed-width text: PH_RENDER_FONT_W pixels per character. */
+void ph_render_text(PhRenderer *renderer, float x, float y, const char *text);
+void ph_render_present(PhRenderer *renderer);
+void ph_render_texture(PhRenderer *renderer, int asset_id,
+ PhRect src, PhRect dst, PhFlip flip);
+void ph_render_sprite(PhRenderer *renderer, int asset_id,
+ int sprite_tile_x, int sprite_tile_y, PhRect dst, PhFlip flip);
void ph_render_entity_frame(const PhEntityDef *def, const PhEntity *entity,
- int *sprite_tile_x, int *sprite_tile_y, SDL_FlipMode *flip);
-void ph_render_world(SDL_Renderer *renderer, const PhRenderAssets *assets,
- const PhWorld *world);
-
-#endif
+ int *sprite_tile_x, int *sprite_tile_y, PhFlip *flip);
+void ph_render_world(PhRenderer *renderer, const PhWorld *world);
#endif
diff --git a/src/engine/render_sdl.c b/src/engine/render_sdl.c
@@ -0,0 +1,112 @@
+#include "engine/sdl.h"
+
+#if PH_USE_SDL
+struct PhRenderer {
+ SDL_Renderer *target;
+ SDL_Texture *textures[PH_MAX_ASSETS];
+ int texture_count;
+};
+
+PhRenderer *
+ph_render_sdl_create(SDL_Renderer *target, const PhAssetDef *defs, int count)
+{
+ PhRenderer *renderer;
+ int i;
+
+ if (!target || count < 0 || count > PH_MAX_ASSETS || (count && !defs)) return NULL;
+ renderer = SDL_calloc(1, sizeof(*renderer));
+ if (!renderer) return NULL;
+ renderer->target = target;
+ renderer->texture_count = count;
+ for (i = 0; i < count; ++i) {
+ SDL_Surface *surface;
+
+ if (!defs[i].path) continue;
+ surface = SDL_LoadBMP(defs[i].path);
+ if (!surface) goto fail;
+ if (!SDL_SetSurfaceColorKey(surface, true, SDL_MapSurfaceRGB(surface,
+ defs[i].key.r, defs[i].key.g, defs[i].key.b))) {
+ SDL_DestroySurface(surface);
+ goto fail;
+ }
+ renderer->textures[i] = SDL_CreateTextureFromSurface(target, surface);
+ SDL_DestroySurface(surface);
+ if (!renderer->textures[i]) goto fail;
+ }
+ return renderer;
+fail:
+ ph_render_destroy(renderer);
+ return NULL;
+}
+
+void
+ph_render_destroy(PhRenderer *renderer)
+{
+ int i;
+
+ if (!renderer) return;
+ for (i = 0; i < renderer->texture_count; ++i)
+ SDL_DestroyTexture(renderer->textures[i]);
+ SDL_free(renderer);
+}
+
+int
+ph_render_has_asset(const PhRenderer *renderer, int asset_id)
+{
+ return renderer && asset_id >= 0 && asset_id < renderer->texture_count &&
+ renderer->textures[asset_id] != NULL;
+}
+
+void
+ph_render_color(PhRenderer *renderer, PhColor color)
+{
+ SDL_SetRenderDrawColor(renderer->target, color.r, color.g, color.b, color.a);
+}
+
+void
+ph_render_clear(PhRenderer *renderer)
+{
+ SDL_RenderClear(renderer->target);
+}
+
+void
+ph_render_fill(PhRenderer *renderer, PhRect rect)
+{
+ SDL_FRect native = { rect.x, rect.y, rect.w, rect.h };
+
+ SDL_RenderFillRect(renderer->target, &native);
+}
+
+void
+ph_render_outline(PhRenderer *renderer, PhRect rect)
+{
+ SDL_FRect native = { rect.x, rect.y, rect.w, rect.h };
+
+ SDL_RenderRect(renderer->target, &native);
+}
+
+void
+ph_render_text(PhRenderer *renderer, float x, float y, const char *text)
+{
+ SDL_RenderDebugText(renderer->target, x, y, text);
+}
+
+void
+ph_render_present(PhRenderer *renderer)
+{
+ SDL_RenderPresent(renderer->target);
+}
+
+void
+ph_render_texture(PhRenderer *renderer, int asset_id, PhRect src, PhRect dst,
+ PhFlip flip)
+{
+ SDL_FRect source = { src.x, src.y, src.w, src.h };
+ SDL_FRect destination = { dst.x, dst.y, dst.w, dst.h };
+
+ if (!ph_render_has_asset(renderer, asset_id)) return;
+ SDL_RenderTextureRotated(renderer->target, renderer->textures[asset_id],
+ &source, &destination, 0.0, NULL,
+ flip == PH_FLIP_HORIZONTAL ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
+}
+#endif
diff --git a/src/engine/sdl.h b/src/engine/sdl.h
@@ -0,0 +1,18 @@
+#ifndef PH_ENGINE_SDL_H
+#define PH_ENGINE_SDL_H
+
+#include "engine/render.h"
+#include "engine/audio.h"
+
+#if PH_USE_SDL
+#include <SDL3/SDL.h>
+
+/* The host initializes SDL. This context owns its textures but borrows target:
+ * destroy it before destroying target. Creation failure releases partial loads. */
+PhRenderer *ph_render_sdl_create(SDL_Renderer *target,
+ const PhAssetDef *defs, int count);
+/* Owns clips, streams and the device; destroy before SDL_Quit. */
+PhAudio *ph_audio_sdl_create(const PhAudioDef *defs, int count);
+#endif
+
+#endif
diff --git a/src/engine/world.h b/src/engine/world.h
@@ -3,7 +3,7 @@
#include <stddef.h>
-#define PH_CONFIG_API_VERSION 15
+#define PH_CONFIG_API_VERSION 16
enum {
PH_MAP_MAGIC = 0x50484d50,
diff --git a/src/examples/courier.c b/src/examples/courier.c
@@ -0,0 +1,127 @@
+/* A second game: collect a parcel and deliver it to the depot.
+ * Uses only the engine library, with its own content, rules and terminal UI. */
+#include "engine/world.h"
+
+#include <stdio.h>
+#include <string.h>
+
+enum { COURIER = 101, PARCEL = 501, WIDTH = 9, HEIGHT = 5 };
+
+static const PhTileDef tiles[] = {
+ { .symbol = '#', .blocks_movement = 1 },
+ { .symbol = '.' },
+ { .symbol = 'D' },
+};
+static const PhEntityDef actors[] = {
+ { .id = COURIER, .name = "Courier", .kind = PH_ENTITY_PLAYER,
+ .move_speed = 1, .stats = { .max_hp = 1 } },
+};
+static const PhItemDef items[] = {
+ { .id = PARCEL, .name = "Parcel" },
+};
+
+static void
+show(const PhWorld *world)
+{
+ const PhEntity *player = ph_world_player(world);
+ int x, y, i;
+
+ for (y = 0; y < HEIGHT; ++y) {
+ for (x = 0; x < WIDTH; ++x) {
+ int ch = ph_area_tile_def(&world->area, x, y)->symbol;
+
+ for (i = 0; i < world->ground_item_count; ++i) {
+ const PhGroundItem *item = &world->ground_items[i];
+
+ if (item->active && (int)(item->pos.x / PH_TILE_SIZE) == x &&
+ (int)(item->pos.y / PH_TILE_SIZE) == y) ch = '*';
+ }
+ if ((int)(player->pos.x / PH_TILE_SIZE) == x &&
+ (int)(player->pos.y / PH_TILE_SIZE) == y) ch = '@';
+ putchar(ch);
+ }
+ putchar('\n');
+ }
+ printf("Parcel: %s\n", ph_world_item_amount(world, PARCEL) ? "carrying" : "on the map");
+}
+
+static int
+step(PhWorld *world, int key)
+{
+ PhInput input = { .interact = 1 };
+ PhWorldEvent event;
+ const PhEntity *player;
+ int i;
+
+ if (key == 'w') input.move_y = -1;
+ else if (key == 's') input.move_y = 1;
+ else if (key == 'a') input.move_x = -1;
+ else if (key == 'd') input.move_x = 1;
+ else return 0;
+ ph_world_tick(world, input, 0.0f);
+ /* Advance one move at a fixed rate, without repeating the movement command. */
+ for (i = 0; i < 600 && ph_world_player(world)->motion.moving; ++i)
+ ph_world_tick(world, (PhInput){ .interact = 1 }, 1.0f / 60.0f);
+ while (ph_world_take_event(world, &event)) { }
+ player = ph_world_player(world);
+ return world->area.tiles[(int)(player->pos.y / PH_TILE_SIZE) * WIDTH +
+ (int)(player->pos.x / PH_TILE_SIZE)] == 2 &&
+ ph_world_item_amount(world, PARCEL) == 1;
+}
+
+int
+main(int argc, char **argv)
+{
+ unsigned char map[] =
+ "#########"
+ "#.......#"
+ "#.#####.#"
+ "#......D#"
+ "#########";
+ PhArea area = { .name = "Parcel Run", .width = WIDTH, .height = HEIGHT,
+ .tiles = map, .tile_defs = tiles, .tile_def_count = 3 };
+ PhWorldCatalog content = { .entities = actors, .items = items,
+ .entity_count = 1, .item_count = 1 };
+ PhWorld world;
+ int smoke = argc == 2 && strcmp(argv[1], "--smoke") == 0;
+ int won = 0;
+ int key, i;
+
+ /* Runtime maps store tile catalog indices, not source symbols. */
+ for (i = 0; i < WIDTH * HEIGHT; ++i)
+ map[i] = map[i] == '#' ? 0 : map[i] == 'D' ? 2 : 1;
+ if (ph_world_init(&world, area, 1, content, WIDTH * PH_TILE_SIZE,
+ HEIGHT * PH_TILE_SIZE) < 0) return 1;
+ key = ph_world_spawn_entity(&world, COURIER,
+ (PhVec2){ PH_TILE_CENTER(1), PH_TILE_CENTER(1) }, 0, 1);
+ if (key < 0) goto fail;
+ ph_world_set_player(&world, key);
+ if (ph_world_drop_item(&world, PARCEL,
+ (PhVec2){ PH_TILE_CENTER(4), PH_TILE_CENTER(1) }, 1, 1) < 0) goto fail;
+ if (smoke) {
+ const char *route = "wddddddss";
+ PhVec2 start = ph_world_player(&world)->pos;
+
+ /* First move hits a wall; then the route collects and delivers the parcel. */
+ step(&world, *route++);
+ if (ph_world_player(&world)->pos.x != start.x ||
+ ph_world_player(&world)->pos.y != start.y) goto fail;
+ while (*route) won = step(&world, *route++);
+ if (!won) goto fail;
+ } else {
+ puts("Parcel Run: WASD then Enter to move; Q to quit. Carry * to D.");
+ show(&world);
+ while (!won && (key = getchar()) != EOF && key != 'q') {
+ if (!strchr("wasd", key)) continue;
+ won = step(&world, key);
+ show(&world);
+ }
+ }
+ if (won) puts("Parcel delivered!");
+ ph_world_destroy(&world);
+ return 0;
+fail:
+ fputs("Parcel Run failed\n", stderr);
+ ph_world_destroy(&world);
+ return 1;
+}
diff --git a/src/game/content.h b/src/game/content.h
@@ -2,9 +2,8 @@
#define PH_GAME_CONTENT_H
#include "engine/area.h"
-#include "engine/audio.h"
+#include "engine/assets.h"
#include "engine/battle.h"
-#include "engine/render.h"
#include "engine/world.h"
enum { PH_SHOP_STOCK_MAX = 8, PH_SHOP_TALK_LINES = 2 };
diff --git a/src/game/game.c b/src/game/game.c
@@ -0,0 +1,609 @@
+#include "game/game.h"
+#include "engine/save.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include "config.h"
+
+#if PH_CONFIG_VERSION != PH_CONFIG_API_VERSION
+#error "config.h version does not match the engine; merge config.def.h"
+#endif
+#if PH_SIMULATION_HZ <= 0 || PH_MAX_CATCHUP_STEPS <= 0
+#error "simulation tick rate and catch-up limit must be positive"
+#endif
+
+static int
+ph_menu_direction(PhAction key)
+{
+ return key == PH_ACTION_UP ||
+ key == PH_ACTION_DOWN ||
+ key == PH_ACTION_LEFT ||
+ key == PH_ACTION_RIGHT;
+}
+
+static int
+ph_menu_back(PhAction key)
+{
+ return key == PH_ACTION_CANCEL || key == PH_ACTION_BACK;
+}
+
+void
+ph_options_open(PhOptionsUi *ui)
+{
+ if (!ui) return;
+ ui->selected = 0;
+ ui->mode = PH_OPTIONS_ROOT;
+}
+
+PhOptionsAction
+ph_options_input(PhOptionsUi *ui, PhAction key)
+{
+ int previous = key == PH_ACTION_UP ||
+ key == PH_ACTION_LEFT;
+ int next = key == PH_ACTION_DOWN ||
+ key == PH_ACTION_RIGHT;
+
+ if (!ui) return PH_OPTIONS_CLOSE;
+ if (ph_menu_back(key)) {
+ if (ui->mode == PH_OPTIONS_SOUND) {
+ ui->mode = PH_OPTIONS_ROOT;
+ ui->selected = 0;
+ return PH_OPTIONS_STAY;
+ }
+ return PH_OPTIONS_CLOSE;
+ }
+ if (previous || next) {
+ ui->selected = (ui->selected + 1) % 2;
+ return PH_OPTIONS_STAY;
+ }
+ if (key != PH_ACTION_ACCEPT) return PH_OPTIONS_STAY;
+ if (ui->mode == PH_OPTIONS_ROOT) {
+ if (ui->selected == 1) return PH_OPTIONS_EXIT;
+ ui->mode = PH_OPTIONS_SOUND;
+ ui->selected = 0;
+ } else if (ui->selected == 0) {
+ ui->music_enabled = !ui->music_enabled;
+ } else {
+ ui->effects_enabled = !ui->effects_enabled;
+ }
+ return PH_OPTIONS_STAY;
+}
+
+PhStartAction
+ph_start_input(PhStartUi *ui, PhAction key)
+{
+ int direction = key == PH_ACTION_UP ||
+ key == PH_ACTION_DOWN;
+
+ if (!ui) return PH_START_STAY;
+ if (direction) ui->selected = ui->can_continue ? !ui->selected : 1;
+ if (key != PH_ACTION_ACCEPT) return PH_START_STAY;
+ return ui->selected == 0 && ui->can_continue ?
+ PH_START_CONTINUE : PH_START_NEW;
+}
+
+int
+ph_inventory_step(const PhWorld *world, int selected, int direction)
+{
+ int i = selected < 0 ? (direction > 0 ? -1 : 0) : selected;
+ int step;
+
+ for (step = 0; step < world->content.item_count; ++step) {
+ i += direction;
+ if (i < 0) i = world->content.item_count - 1;
+ if (i >= world->content.item_count) i = 0;
+ if (world->inventory[i] > 0) return i;
+ }
+ return -1;
+}
+
+void
+ph_inventory_open(PhInventoryUi *ui, const PhWorld *world)
+{
+ memset(ui, 0, sizeof(*ui));
+ ui->selected = ph_inventory_step(world, -1, 1);
+}
+
+static void
+ph_inventory_reselect(PhInventoryUi *ui, const PhWorld *world)
+{
+ if (ui->selected >= 0 && world->inventory[ui->selected] > 0) return;
+ ui->selected = ph_inventory_step(world, ui->selected, 1);
+}
+
+static void
+ph_inventory_action(PhInventoryUi *ui, PhWorld *world, int action)
+{
+ const PhItemDef *item;
+ const char *name;
+ int result = -1;
+
+ if (ui->selected < 0) return;
+ item = &world->content.items[ui->selected];
+ name = item->name ? item->name : "Item";
+ if (action == PH_INVENTORY_EQUIP)
+ result = ph_world_equip_inventory_item(world, world->player_index, item->id);
+ else if (action == PH_INVENTORY_USE)
+ result = ph_world_use_inventory_item(world, world->player_index, item->id);
+ else if (action == PH_INVENTORY_DROP)
+ result = ph_world_drop_inventory_item(world, world->player_index, item->id);
+ else {
+ ui->mode = PH_INVENTORY_EXAMINE;
+ ui->status[0] = '\0';
+ return;
+ }
+
+ if (result < 0) {
+ snprintf(ui->status, sizeof(ui->status), "Cannot %s %s.",
+ action == PH_INVENTORY_EQUIP ? "equip" :
+ action == PH_INVENTORY_USE ? "use" : "drop", name);
+ return;
+ }
+ snprintf(ui->status, sizeof(ui->status), "%s %s.",
+ action == PH_INVENTORY_EQUIP ? "Equipped" :
+ action == PH_INVENTORY_USE ? "Used" : "Dropped", name);
+ ui->mode = PH_INVENTORY_BROWSE;
+ ph_inventory_reselect(ui, world);
+}
+
+int
+ph_inventory_action_enabled(const PhItemDef *item, int action)
+{
+ if (action == PH_INVENTORY_EQUIP) return item->equip_slot != PH_EQUIP_NONE;
+ if (action == PH_INVENTORY_USE) return item->use != PH_ITEM_USE_NONE;
+ return 1;
+}
+
+void
+ph_inventory_action_order(const PhItemDef *item, int actions[PH_INVENTORY_ACTION_COUNT])
+{
+ if (item->equip_slot != PH_EQUIP_NONE) {
+ actions[0] = PH_INVENTORY_EQUIP;
+ actions[1] = PH_INVENTORY_USE;
+ } else if (item->use != PH_ITEM_USE_NONE) {
+ actions[0] = PH_INVENTORY_USE;
+ actions[1] = PH_INVENTORY_EQUIP;
+ } else {
+ actions[0] = PH_INVENTORY_DROP;
+ actions[1] = PH_INVENTORY_EXAMINE_ACTION;
+ }
+ if (actions[0] < PH_INVENTORY_DROP) {
+ actions[2] = PH_INVENTORY_DROP;
+ actions[3] = PH_INVENTORY_EXAMINE_ACTION;
+ } else {
+ actions[2] = PH_INVENTORY_EQUIP;
+ actions[3] = PH_INVENTORY_USE;
+ }
+}
+
+static int
+ph_inventory_action_step(const PhItemDef *item, int selected, int direction)
+{
+ int actions[PH_INVENTORY_ACTION_COUNT];
+ int position = 0;
+ int step;
+
+ ph_inventory_action_order(item, actions);
+ while (position < PH_INVENTORY_ACTION_COUNT && actions[position] != selected) ++position;
+ for (step = 0; step < PH_INVENTORY_ACTION_COUNT; ++step) {
+ position = (position + (direction < 0 ?
+ PH_INVENTORY_ACTION_COUNT - 1 : 1)) % PH_INVENTORY_ACTION_COUNT;
+ if (ph_inventory_action_enabled(item, actions[position])) return actions[position];
+ }
+ return selected;
+}
+
+int
+ph_inventory_input(PhInventoryUi *ui, PhWorld *world, PhAction key)
+{
+ int previous = key == PH_ACTION_UP ||
+ key == PH_ACTION_LEFT;
+ int next = key == PH_ACTION_DOWN ||
+ key == PH_ACTION_RIGHT;
+
+ if (ph_menu_back(key)) {
+ if (ui->mode == PH_INVENTORY_EXAMINE)
+ ui->mode = PH_INVENTORY_ACTIONS;
+ else if (ui->mode == PH_INVENTORY_ACTIONS)
+ ui->mode = PH_INVENTORY_BROWSE;
+ else
+ return 1;
+ ui->status[0] = '\0';
+ return 0;
+ }
+ if (key == PH_ACTION_ACCEPT) {
+ if (ui->mode == PH_INVENTORY_BROWSE && ui->selected >= 0) {
+ int actions[PH_INVENTORY_ACTION_COUNT];
+
+ ph_inventory_action_order(&world->content.items[ui->selected], actions);
+ ui->mode = PH_INVENTORY_ACTIONS;
+ ui->option = actions[0];
+ ui->status[0] = '\0';
+ } else if (ui->mode == PH_INVENTORY_ACTIONS) {
+ ph_inventory_action(ui, world, ui->option);
+ } else if (ui->mode == PH_INVENTORY_EXAMINE) {
+ ui->mode = PH_INVENTORY_ACTIONS;
+ }
+ return 0;
+ }
+ if ((!previous && !next) || ui->mode == PH_INVENTORY_EXAMINE) return 0;
+ if (ui->mode == PH_INVENTORY_ACTIONS) {
+ ui->option = ph_inventory_action_step(&world->content.items[ui->selected],
+ ui->option, previous ? -1 : 1);
+ } else {
+ ui->selected = ph_inventory_step(world, ui->selected, previous ? -1 : 1);
+ ui->status[0] = '\0';
+ }
+ return 0;
+}
+
+void
+ph_spellbook_open(PhSpellbookUi *ui, const PhSpellBook *spells)
+{
+ memset(ui, 0, sizeof(*ui));
+ ui->selected = spells && spells->learned_count > 0 ? 0 : -1;
+}
+
+int
+ph_spellbook_action_enabled(const PhSpellDef *spell, int action)
+{
+ return action == PH_SPELLBOOK_EXAMINE_ACTION ||
+ (spell && spell->field_use != PH_SPELL_FIELD_NONE);
+}
+
+static int
+ph_spellbook_action_step(const PhSpellDef *spell, int selected, int direction)
+{
+ int action;
+ int step;
+
+ for (step = 1; step <= PH_SPELLBOOK_ACTION_COUNT; ++step) {
+ action = (selected + (direction < 0 ?
+ PH_SPELLBOOK_ACTION_COUNT - step : step)) % PH_SPELLBOOK_ACTION_COUNT;
+ if (ph_spellbook_action_enabled(spell, action)) return action;
+ }
+ return selected;
+}
+
+static void
+ph_spellbook_action(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells)
+{
+ const PhSpellDef *spell;
+ int amount;
+
+ if (ui->selected < 0 || ui->selected >= spells->learned_count) return;
+ spell = ph_spellbook_def(spells, spells->learned[ui->selected]);
+ if (!spell) return;
+ if (ui->option == PH_SPELLBOOK_EXAMINE_ACTION) {
+ ui->mode = PH_SPELLBOOK_EXAMINE;
+ ui->status[0] = '\0';
+ return;
+ }
+ amount = ph_spellbook_use(spells, world, world->player_index, spell->id);
+ if (amount < 0)
+ snprintf(ui->status, sizeof(ui->status), "Cannot use %s.", spell->name);
+ else {
+ snprintf(ui->status, sizeof(ui->status), "Restored %d HP.", amount);
+ ui->spell_used = 1;
+ }
+ ui->mode = PH_SPELLBOOK_BROWSE;
+}
+
+int
+ph_spellbook_input(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells,
+ PhAction key)
+{
+ int previous = key == PH_ACTION_UP ||
+ key == PH_ACTION_LEFT;
+ int next = key == PH_ACTION_DOWN ||
+ key == PH_ACTION_RIGHT;
+ const PhSpellDef *spell = ui->selected >= 0 &&
+ ui->selected < spells->learned_count ? ph_spellbook_def(spells,
+ spells->learned[ui->selected]) : NULL;
+
+ if (ph_menu_back(key)) {
+ if (ui->mode == PH_SPELLBOOK_EXAMINE)
+ ui->mode = PH_SPELLBOOK_ACTIONS;
+ else if (ui->mode == PH_SPELLBOOK_ACTIONS)
+ ui->mode = PH_SPELLBOOK_BROWSE;
+ else return 1;
+ ui->status[0] = '\0';
+ return 0;
+ }
+ if (key == PH_ACTION_ACCEPT) {
+ if (ui->mode == PH_SPELLBOOK_BROWSE && spell) {
+ ui->mode = PH_SPELLBOOK_ACTIONS;
+ ui->option = ph_spellbook_action_enabled(spell, PH_SPELLBOOK_USE) ?
+ PH_SPELLBOOK_USE : PH_SPELLBOOK_EXAMINE_ACTION;
+ ui->status[0] = '\0';
+ } else if (ui->mode == PH_SPELLBOOK_ACTIONS) {
+ ph_spellbook_action(ui, world, spells);
+ } else if (ui->mode == PH_SPELLBOOK_EXAMINE) {
+ ui->mode = PH_SPELLBOOK_ACTIONS;
+ }
+ return 0;
+ }
+ if ((!previous && !next) || ui->mode == PH_SPELLBOOK_EXAMINE) return 0;
+ if (ui->mode == PH_SPELLBOOK_ACTIONS) {
+ ui->option = ph_spellbook_action_step(spell, ui->option,
+ previous ? -1 : 1);
+ } else if (spells->learned_count > 0) {
+ ui->selected = (ui->selected + (previous ?
+ spells->learned_count - 1 : 1)) % spells->learned_count;
+ ui->status[0] = '\0';
+ }
+ return 0;
+}
+
+void
+ph_shop_open(PhShopUi *ui, int shop_id)
+{
+ memset(ui, 0, sizeof(*ui));
+ ui->shop_id = shop_id;
+}
+
+PhMenu
+ph_interaction_open(PhShopUi *shop_ui, int interaction_id)
+{
+ const PhInteractionDef *interaction = ph_game_interaction_def(interaction_id);
+
+ if (!interaction) return PH_MENU_NONE;
+ if (interaction->kind == PH_INTERACTION_SHOP &&
+ ph_game_shop_def(interaction->content_id)) {
+ ph_shop_open(shop_ui, interaction->content_id);
+ return PH_MENU_SHOP;
+ }
+ return PH_MENU_NONE;
+}
+
+int
+ph_shop_input(PhShopUi *ui, PhWorld *world, PhSpellBook *spells, PhAction key)
+{
+ const PhShopDef *shop = ph_game_shop_def(ui->shop_id);
+ int previous = key == PH_ACTION_UP ||
+ key == PH_ACTION_LEFT;
+ int next = key == PH_ACTION_DOWN ||
+ key == PH_ACTION_RIGHT;
+
+ if (!shop) return 1;
+ if (ph_menu_back(key)) {
+ if (ui->mode == PH_SHOP_ROOT) return 1;
+ ui->mode = PH_SHOP_ROOT;
+ ui->selected = 0;
+ ui->status[0] = '\0';
+ return 0;
+ }
+ if (ui->mode == PH_SHOP_TALK) {
+ if (key == PH_ACTION_ACCEPT) ui->mode = PH_SHOP_ROOT;
+ return 0;
+ }
+ if (previous || next) {
+ int count = ui->mode == PH_SHOP_ROOT ?
+ (shop->kind == PH_SHOP_ITEMS ? 3 : 2) :
+ ui->mode == PH_SHOP_BUY ? shop->stock_count : world->content.item_count;
+
+ if (ui->mode == PH_SHOP_SELL) {
+ ui->selected = ph_inventory_step(world, ui->selected,
+ previous ? -1 : 1);
+ } else if (count > 0) {
+ ui->selected = (ui->selected + (previous ? count - 1 : 1)) % count;
+ }
+ ui->status[0] = '\0';
+ return 0;
+ }
+ if (key != PH_ACTION_ACCEPT) return 0;
+ if (ui->mode == PH_SHOP_ROOT) {
+ ui->mode = shop->kind == PH_SHOP_ITEMS ?
+ (PhShopMode)(ui->selected + 1) :
+ ui->selected == 0 ? PH_SHOP_BUY : PH_SHOP_TALK;
+ ui->selected = ui->mode == PH_SHOP_SELL ?
+ ph_inventory_step(world, -1, 1) : 0;
+ return 0;
+ }
+ if (ui->mode == PH_SHOP_BUY && ui->selected < shop->stock_count) {
+ if (shop->kind == PH_SHOP_SPELLS) {
+ const PhSpellDef *spell = ph_spellbook_def(spells,
+ shop->stock[ui->selected]);
+
+ if (!spell) return 0;
+ if (ph_spellbook_has(spells, spell->id))
+ snprintf(ui->status, sizeof(ui->status), "Already learned.");
+ else if (ph_spellbook_buy(spells, world, world->player_index,
+ spell->id) < 0)
+ snprintf(ui->status, sizeof(ui->status), "Not enough gold.");
+ else
+ snprintf(ui->status, sizeof(ui->status), "Learned %s.", spell->name);
+ } else {
+ const PhItemDef *item = ph_world_item_def(world,
+ shop->stock[ui->selected]);
+
+ if (!item || ph_world_buy_item(world, world->player_index, item->id) < 0)
+ snprintf(ui->status, sizeof(ui->status), "Not enough gold.");
+ else
+ snprintf(ui->status, sizeof(ui->status), "Bought %s.", item->name);
+ }
+ } else if (ui->mode == PH_SHOP_SELL && ui->selected >= 0) {
+ const PhItemDef *item = &world->content.items[ui->selected];
+
+ if (ph_world_sell_item(world, world->player_index, item->id) < 0)
+ snprintf(ui->status, sizeof(ui->status), "Cannot sell that.");
+ else {
+ snprintf(ui->status, sizeof(ui->status), "Sold %s.", item->name);
+ if (world->inventory[ui->selected] <= 0)
+ ui->selected = ph_inventory_step(world, ui->selected, 1);
+ }
+ }
+ return 0;
+}
+
+int
+ph_game_input(PhGameContext *context, PhAction key)
+{
+ PhMenu menu = context->menu;
+ PhGameMode mode = *context->mode;
+ PhBattlePhase phase = context->battle->phase;
+ int result = 0;
+
+ if (context->menu == PH_MENU_START) {
+ PhStartAction action = ph_start_input(context->start, key);
+
+ if (action == PH_START_CONTINUE) context->menu = PH_MENU_NONE;
+ else if (action == PH_START_NEW) {
+ context->time_reset = 1;
+ return PH_GAME_REQUEST_NEW;
+ }
+ } else if (context->menu == PH_MENU_MAIN) {
+ PhOptionsAction action;
+
+ if (ph_menu_direction(key)) result |= PH_GAME_MENU_SOUND;
+ action = ph_options_input(context->options, key);
+ if (action == PH_OPTIONS_CLOSE) context->menu = PH_MENU_NONE;
+ else if (action == PH_OPTIONS_EXIT) result |= PH_GAME_REQUEST_EXIT;
+ } else if (context->menu == PH_MENU_NONE && key == PH_ACTION_CANCEL &&
+ (*context->mode == PH_GAME_WORLD || *context->mode == PH_GAME_VICTORY ||
+ *context->mode == PH_GAME_OVER || (*context->mode == PH_GAME_BATTLE &&
+ context->battle->phase != PH_BATTLE_MAGIC &&
+ context->battle->phase != PH_BATTLE_TARGET))) {
+ ph_options_open(context->options);
+ context->menu = PH_MENU_MAIN;
+ } else if (context->menu == PH_MENU_NONE && *context->mode == PH_GAME_BATTLE &&
+ (context->battle->phase == PH_BATTLE_COMMAND ||
+ context->battle->phase == PH_BATTLE_MAGIC ||
+ context->battle->phase == PH_BATTLE_TARGET)) {
+ int previous = key == PH_ACTION_UP ||
+ key == PH_ACTION_LEFT;
+ int next = key == PH_ACTION_DOWN ||
+ key == PH_ACTION_RIGHT;
+
+ if (context->battle->phase == PH_BATTLE_TARGET) {
+ if (previous || next) {
+ if (ph_battle_target_step(context->battle, context->world,
+ previous ? -1 : 1) >= 0) result |= PH_GAME_MENU_SOUND;
+ }
+ else if (key == PH_ACTION_ACCEPT)
+ ph_battle_confirm_target(context->battle, context->world);
+ else if (ph_menu_back(key))
+ ph_battle_cancel(context->battle);
+ } else if (context->battle->phase == PH_BATTLE_MAGIC) {
+ if ((previous || next) && context->battle->spells.learned_count > 0) {
+ if (ph_battle_spell_step(context->battle, previous ? -1 : 1) >= 0)
+ result |= PH_GAME_MENU_SOUND;
+ }
+ else if (key == PH_ACTION_ACCEPT)
+ ph_battle_cast(context->battle, context->world);
+ else if (ph_menu_back(key))
+ ph_battle_cancel(context->battle);
+ } else if (previous || next) {
+ context->battle->choice.command = (PhBattleCommand)((context->battle->choice.command + (previous ?
+ PH_BATTLE_COMMAND_COUNT - 1 : 1)) % PH_BATTLE_COMMAND_COUNT);
+ result |= PH_GAME_MENU_SOUND;
+ } else if (key == PH_ACTION_ACCEPT) {
+ ph_battle_select(context->battle, context->world);
+ }
+ } else if (context->menu == PH_MENU_NONE && *context->mode == PH_GAME_VICTORY &&
+ key == PH_ACTION_ACCEPT) {
+ *context->mode = PH_GAME_WORLD;
+ context->menu = PH_MENU_NONE;
+ } else if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_CHARACTER &&
+ ph_menu_back(key)) {
+ context->menu = PH_MENU_NONE;
+ } else if (*context->mode == PH_GAME_WORLD &&
+ (context->menu == PH_MENU_NONE || context->menu == PH_MENU_CHARACTER) &&
+ key == PH_ACTION_CHARACTER) {
+ context->menu = context->menu == PH_MENU_CHARACTER ? PH_MENU_NONE : PH_MENU_CHARACTER;
+ } else if (*context->mode == PH_GAME_WORLD &&
+ (context->menu == PH_MENU_NONE || context->menu == PH_MENU_INVENTORY) &&
+ key == PH_ACTION_INVENTORY) {
+ if (context->menu == PH_MENU_INVENTORY) context->menu = PH_MENU_NONE;
+ else {
+ context->menu = PH_MENU_INVENTORY;
+ ph_inventory_open(context->inventory, context->world);
+ }
+ } else if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_INVENTORY) {
+ if (ph_menu_direction(key) &&
+ context->inventory->mode != PH_INVENTORY_EXAMINE)
+ result |= PH_GAME_MENU_SOUND;
+ if (ph_inventory_input(context->inventory, context->world, key))
+ context->menu = PH_MENU_NONE;
+ } else if (*context->mode == PH_GAME_WORLD &&
+ (context->menu == PH_MENU_NONE || context->menu == PH_MENU_SPELLBOOK) &&
+ key == PH_ACTION_SPELLBOOK) {
+ if (context->menu == PH_MENU_SPELLBOOK) context->menu = PH_MENU_NONE;
+ else {
+ context->menu = PH_MENU_SPELLBOOK;
+ ph_spellbook_open(context->spellbook, context->spells);
+ }
+ } else if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_SPELLBOOK) {
+ if (ph_menu_direction(key) &&
+ context->spellbook->mode != PH_SPELLBOOK_EXAMINE)
+ result |= PH_GAME_MENU_SOUND;
+ if (ph_spellbook_input(context->spellbook, context->world, context->spells,
+ key)) context->menu = PH_MENU_NONE;
+ } else if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_SHOP) {
+ if (ph_menu_direction(key) &&
+ context->shop->mode != PH_SHOP_TALK) result |= PH_GAME_MENU_SOUND;
+ if (ph_shop_input(context->shop, context->world, context->spells, key))
+ context->menu = PH_MENU_NONE;
+ }
+ if (menu != context->menu || mode != *context->mode || phase != context->battle->phase)
+ context->time_reset = 1;
+ return result;
+}
+
+int
+ph_game_update(PhGameContext *context, PhInput input, float dt, const char *save_path)
+{
+ PhWorld *world = context->world;
+ PhGameMode *game = context->mode;
+ PhBattleResult result = PH_BATTLE_CONTINUE;
+ int old_area = world->area_id;
+ int save_requested = 0;
+
+ if (*game == PH_GAME_WORLD && context->menu == PH_MENU_NONE) {
+ ph_world_tick(world, input, dt);
+ if (ph_world_update_portal(world, ph_game_world.areas) < 0)
+ fprintf(stderr, "failed to enter area\n");
+ if (world->encounter_index >= 0) {
+ if (ph_save_write(save_path, world, context->spells) < 0)
+ fprintf(stderr, "failed to save pre-combat checkpoint\n");
+ ph_battle_begin(context->battle, world, world->encounter_index,
+ *context->spells);
+ *game = PH_GAME_TRANSITION;
+ }
+ }
+ if ((*game == PH_GAME_TRANSITION || *game == PH_GAME_BATTLE) &&
+ context->menu == PH_MENU_NONE)
+ result = ph_battle_update(context->battle, world, dt,
+ *game == PH_GAME_TRANSITION);
+ if (result == PH_BATTLE_READY) *game = PH_GAME_BATTLE;
+ else if (result == PH_BATTLE_VICTORY) {
+ *game = PH_GAME_VICTORY;
+ save_requested = 1;
+ }
+ else if (result == PH_BATTLE_DEFEAT) *game = PH_GAME_OVER;
+ return save_requested || world->area_id != old_area;
+}
+
+void
+ph_game_interaction(PhGameContext *context)
+{
+ if (*context->mode == PH_GAME_WORLD && context->menu == PH_MENU_NONE &&
+ context->world->interaction_id > 0) {
+ context->menu = ph_interaction_open(context->shop, context->world->interaction_id);
+ context->world->interaction_id = 0;
+ }
+}
+
+int
+ph_game_steps(PhGameContext *context, uint64_t elapsed_ns)
+{
+ if (context->time_reset || context->menu != PH_MENU_NONE ||
+ *context->mode == PH_GAME_VICTORY || *context->mode == PH_GAME_OVER) {
+ context->clock.credit = 0;
+ context->time_reset = 0;
+ return 0;
+ }
+ return ph_clock_steps(&context->clock, elapsed_ns,
+ PH_SIMULATION_HZ, PH_MAX_CATCHUP_STEPS);
+}
diff --git a/src/game/game.h b/src/game/game.h
@@ -0,0 +1,172 @@
+#ifndef PH_GAME_GAME_H
+#define PH_GAME_GAME_H
+
+#include "engine/battle.h"
+#include "engine/clock.h"
+#include "game/content.h"
+
+
+typedef enum {
+ PH_ACTION_NONE,
+ PH_ACTION_UP,
+ PH_ACTION_DOWN,
+ PH_ACTION_LEFT,
+ PH_ACTION_RIGHT,
+ PH_ACTION_ACCEPT,
+ PH_ACTION_CANCEL,
+ PH_ACTION_BACK,
+ PH_ACTION_CHARACTER,
+ PH_ACTION_INVENTORY,
+ PH_ACTION_SPELLBOOK,
+} PhAction;
+
+typedef enum {
+ PH_MENU_NONE,
+ PH_MENU_CHARACTER,
+ PH_MENU_INVENTORY,
+ PH_MENU_SPELLBOOK,
+ PH_MENU_SHOP,
+ PH_MENU_MAIN,
+ PH_MENU_START,
+} PhMenu;
+
+typedef enum {
+ PH_START_STAY,
+ PH_START_CONTINUE,
+ PH_START_NEW,
+} PhStartAction;
+
+typedef struct {
+ int selected;
+ int can_continue;
+} PhStartUi;
+
+typedef enum {
+ PH_GAME_WORLD,
+ PH_GAME_TRANSITION,
+ PH_GAME_BATTLE,
+ PH_GAME_VICTORY,
+ PH_GAME_OVER,
+} PhGameMode;
+
+typedef enum {
+ PH_INVENTORY_BROWSE,
+ PH_INVENTORY_ACTIONS,
+ PH_INVENTORY_EXAMINE,
+} PhInventoryMode;
+
+typedef enum {
+ PH_INVENTORY_EQUIP,
+ PH_INVENTORY_USE,
+ PH_INVENTORY_DROP,
+ PH_INVENTORY_EXAMINE_ACTION,
+ PH_INVENTORY_ACTION_COUNT,
+} PhInventoryAction;
+
+typedef struct {
+ int selected;
+ int option;
+ PhInventoryMode mode;
+ char status[64];
+} PhInventoryUi;
+
+typedef enum {
+ PH_SPELLBOOK_BROWSE,
+ PH_SPELLBOOK_ACTIONS,
+ PH_SPELLBOOK_EXAMINE,
+} PhSpellbookMode;
+
+typedef enum {
+ PH_SPELLBOOK_USE,
+ PH_SPELLBOOK_EXAMINE_ACTION,
+ PH_SPELLBOOK_ACTION_COUNT,
+} PhSpellbookAction;
+
+typedef struct {
+ int selected;
+ int option;
+ PhSpellbookMode mode;
+ int spell_used;
+ char status[64];
+} PhSpellbookUi;
+
+typedef enum {
+ PH_SHOP_ROOT,
+ PH_SHOP_BUY,
+ PH_SHOP_SELL,
+ PH_SHOP_TALK,
+} PhShopMode;
+
+typedef struct {
+ int shop_id;
+ int selected;
+ PhShopMode mode;
+ char status[64];
+} PhShopUi;
+
+typedef enum {
+ PH_OPTIONS_ROOT,
+ PH_OPTIONS_SOUND,
+} PhOptionsMode;
+
+typedef enum {
+ PH_OPTIONS_STAY,
+ PH_OPTIONS_CLOSE,
+ PH_OPTIONS_EXIT,
+} PhOptionsAction;
+
+typedef struct {
+ int selected;
+ int music_enabled;
+ int effects_enabled;
+ PhOptionsMode mode;
+} PhOptionsUi;
+
+typedef struct {
+ PhWorld *world;
+ PhInventoryUi *inventory;
+ PhSpellbookUi *spellbook;
+ PhShopUi *shop;
+ PhOptionsUi *options;
+ PhStartUi *start;
+ PhGameMode *mode;
+ PhBattle *battle;
+ PhSpellBook *spells;
+ PhMenu menu;
+ PhClock clock;
+ int time_reset;
+} PhGameContext;
+
+int ph_inventory_step(const PhWorld *world, int selected, int direction);
+void ph_inventory_open(PhInventoryUi *ui, const PhWorld *world);
+int ph_inventory_input(PhInventoryUi *ui, PhWorld *world, PhAction key);
+void ph_spellbook_open(PhSpellbookUi *ui, const PhSpellBook *spells);
+int ph_spellbook_input(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells,
+ PhAction key);
+void ph_shop_open(PhShopUi *ui, int shop_id);
+PhMenu ph_interaction_open(PhShopUi *shop_ui, int interaction_id);
+void ph_options_open(PhOptionsUi *ui);
+PhOptionsAction ph_options_input(PhOptionsUi *ui, PhAction key);
+PhStartAction ph_start_input(PhStartUi *ui, PhAction key);
+int ph_shop_input(PhShopUi *ui, PhWorld *world, PhSpellBook *spells,
+ PhAction key);
+/* Drawing and input share the same available menu actions. */
+int ph_inventory_action_enabled(const PhItemDef *item, int action);
+void ph_inventory_action_order(const PhItemDef *item, int actions[PH_INVENTORY_ACTION_COUNT]);
+int ph_spellbook_action_enabled(const PhSpellDef *spell, int action);
+
+enum {
+ PH_GAME_REQUEST_NEW = 1,
+ PH_GAME_REQUEST_EXIT = 2,
+ PH_GAME_MENU_SOUND = 4,
+};
+
+/* Returns request flags; the host performs lifecycle operations. */
+int ph_game_input(PhGameContext *context, PhAction key);
+/* Writes the pre-combat checkpoint; returns whether an autosave is needed. */
+int ph_game_update(PhGameContext *context, PhInput input, float dt, const char *save_path);
+void ph_game_interaction(PhGameContext *context);
+/* Discards paused time and the frame containing an input-driven state change. */
+int ph_game_steps(PhGameContext *context, uint64_t elapsed_ns);
+
+#endif
diff --git a/src/game/main.c b/src/game/main.c
@@ -1,6 +1,7 @@
#include "engine/battle.h"
#include "engine/area.h"
#include "engine/audio.h"
+#include "engine/sdl.h"
#include "engine/save.h"
#include "game/content.h"
#include "game/presentation.h"
@@ -28,63 +29,62 @@ ph_game_music(const PhGameContext *context)
ph_game_area_music(context->world->area_id) : PH_MUSIC_BATTLE;
}
-static int
-ph_update_game(PhGameContext *context, PhInput input, float dt)
+static float
+ph_world_sound_gain(const PhWorld *world, const PhWorldEvent *event)
{
- PhWorld *world = context->world;
- PhGameMode *game = context->mode;
- PhBattleResult result = PH_BATTLE_CONTINUE;
- int old_area = world->area_id;
- int save_requested = 0;
+ const PhEntity *player = ph_world_player(world);
- if (*game == PH_GAME_WORLD && context->menu == PH_MENU_NONE) {
- ph_world_tick(world, input, dt);
- if (ph_world_update_portal(world, ph_game_world.areas) < 0)
- fprintf(stderr, "failed to enter area\n");
- if (world->encounter_index >= 0) {
- if (ph_save_write(PH_SAVE_PATH, world, context->spells) < 0)
- fprintf(stderr, "failed to save pre-combat checkpoint\n");
- ph_battle_begin(context->battle, world, world->encounter_index,
- *context->spells);
- *game = PH_GAME_TRANSITION;
- }
- }
- if ((*game == PH_GAME_TRANSITION || *game == PH_GAME_BATTLE) &&
- context->menu == PH_MENU_NONE)
- result = ph_battle_update(context->battle, world, dt,
- *game == PH_GAME_TRANSITION);
- if (result == PH_BATTLE_READY) *game = PH_GAME_BATTLE;
- else if (result == PH_BATTLE_VICTORY) {
- *game = PH_GAME_VICTORY;
- save_requested = 1;
- }
- else if (result == PH_BATTLE_DEFEAT) *game = PH_GAME_OVER;
- return save_requested || world->area_id != old_area;
+ if (!player || !event || PH_SPATIAL_SOUND_DISTANCE <= 0.0f) return 0.0f;
+ return ph_audio_distance_gain(player->pos.x, player->pos.y,
+ event->pos.x, event->pos.y, PH_SPATIAL_SOUND_DISTANCE);
}
-static int
-ph_menu_direction(SDL_Scancode key)
+static void
+ph_game_audio(PhGameContext *context, PhAudio *audio, int effect)
{
- return key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
- key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
- key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2 ||
- key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
-}
+ {
+ int battle_effect = ph_game_battle_sound(ph_battle_take_event(context->battle));
-static int
-ph_menu_back(SDL_Scancode key)
-{
- return key == PH_KEY_MENU_CANCEL || key == PH_KEY_MENU_BACK;
+ if (battle_effect != PH_AUDIO_NONE) effect = battle_effect;
+ if (context->spellbook->spell_used) effect = PH_SOUND_SPELL;
+ context->spellbook->spell_used = 0;
+ }
+ if (audio) {
+ PhWorldEvent world_event;
+
+ ph_audio_music(audio, ph_game_music(context));
+ ph_audio_set_enabled(audio, context->options->music_enabled,
+ context->options->effects_enabled);
+ if (effect != PH_AUDIO_NONE) ph_audio_effect(audio, effect, 1.0f);
+ while (ph_world_take_event(context->world, &world_event)) {
+ int world_effect = ph_game_world_sound(context->world, &world_event);
+
+ if (world_effect != PH_AUDIO_NONE)
+ ph_audio_effect(audio, world_effect,
+ ph_world_sound_gain(context->world, &world_event));
+ }
+ ph_audio_update(audio);
+ } else {
+ PhWorldEvent world_event;
+
+ while (ph_world_take_event(context->world, &world_event)) { }
+ }
}
-static float
-ph_world_sound_gain(const PhWorld *world, const PhWorldEvent *event)
+static PhAction
+ph_sdl_action(SDL_Scancode key)
{
- const PhEntity *player = ph_world_player(world);
-
- if (!player || !event || PH_SPATIAL_SOUND_DISTANCE <= 0.0f) return 0.0f;
- return ph_audio_distance_gain(player->pos.x, player->pos.y,
- event->pos.x, event->pos.y, PH_SPATIAL_SOUND_DISTANCE);
+ if (key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2) return PH_ACTION_UP;
+ if (key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2) return PH_ACTION_DOWN;
+ if (key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2) return PH_ACTION_LEFT;
+ if (key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2) return PH_ACTION_RIGHT;
+ if (key == PH_KEY_MENU_ACCEPT) return PH_ACTION_ACCEPT;
+ if (key == PH_KEY_MENU_CANCEL) return PH_ACTION_CANCEL;
+ if (key == PH_KEY_MENU_BACK) return PH_ACTION_BACK;
+ if (key == PH_KEY_CHARACTER) return PH_ACTION_CHARACTER;
+ if (key == PH_KEY_INVENTORY) return PH_ACTION_INVENTORY;
+ if (key == PH_KEY_SPELLBOOK) return PH_ACTION_SPELLBOOK;
+ return PH_ACTION_NONE;
}
static PhInput
@@ -134,8 +134,16 @@ static int
ph_run_game(void)
{
SDL_Window *window;
- SDL_Renderer *renderer;
- PhRenderAssets assets;
+ SDL_Renderer *target;
+ PhRenderer *renderer;
+ PhControlLabels labels = {
+ .accept = SDL_GetScancodeName(PH_KEY_MENU_ACCEPT),
+ .back = SDL_GetScancodeName(PH_KEY_MENU_BACK),
+ .cancel = SDL_GetScancodeName(PH_KEY_MENU_CANCEL),
+ .character = SDL_GetScancodeName(PH_KEY_CHARACTER),
+ .inventory = SDL_GetScancodeName(PH_KEY_INVENTORY),
+ .spellbook = SDL_GetScancodeName(PH_KEY_SPELLBOOK),
+ };
PhWorld world;
PhInventoryUi inventory_ui = { 0 };
PhSpellbookUi spellbook_ui = { 0 };
@@ -145,7 +153,7 @@ ph_run_game(void)
PhGameMode game = PH_GAME_WORLD;
PhBattle battle = { 0 };
PhSpellBook spells = ph_player_spellbook;
- PhAudio audio = { 0 };
+ PhAudio *audio = NULL;
PhGameContext context = {
.world = &world,
.inventory = &inventory_ui,
@@ -156,11 +164,11 @@ ph_run_game(void)
.mode = &game,
.battle = &battle,
.spells = &spells,
+ .menu = PH_MENU_START,
};
Uint64 last_ticks;
int running = 1;
- int audio_ready = 0;
- PhMenu menu = PH_MENU_START;
+ int interact_pending = 0;
int save_status;
if (!SDL_Init(SDL_INIT_VIDEO)) {
@@ -180,14 +188,14 @@ ph_run_game(void)
}
start_ui.can_continue = save_status > 0;
start_ui.selected = start_ui.can_continue ? 0 : 1;
- if (ph_create_window_renderer(&window, &renderer) < 0) {
+ if (ph_create_window_renderer(&window, &target) < 0) {
ph_world_destroy(&world);
SDL_Quit();
return 1;
}
- if (ph_render_assets_load(&assets, renderer,
- ph_asset_defs, PH_ASSET_COUNT) < 0) {
- SDL_DestroyRenderer(renderer);
+ if (!(renderer = ph_render_sdl_create(target, ph_asset_defs, PH_ASSET_COUNT))) {
+ fprintf(stderr, "render initialization failed: %s\n", SDL_GetError());
+ SDL_DestroyRenderer(target);
SDL_DestroyWindow(window);
ph_world_destroy(&world);
SDL_Quit();
@@ -195,193 +203,77 @@ ph_run_game(void)
}
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
fprintf(stderr, "audio disabled: %s\n", SDL_GetError());
- } else if (ph_audio_init(&audio, ph_audio_defs, PH_AUDIO_COUNT) < 0) {
+ } else if (!(audio = ph_audio_sdl_create(ph_audio_defs, PH_AUDIO_COUNT))) {
fprintf(stderr, "audio disabled: %s\n", SDL_GetError());
- } else audio_ready = 1;
+ }
- SDL_SetRenderLogicalPresentation(renderer, PH_VIEW_W, PH_VIEW_H,
+ SDL_SetRenderLogicalPresentation(target, PH_VIEW_W, PH_VIEW_H,
SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
- last_ticks = SDL_GetTicks();
+ last_ticks = SDL_GetTicksNS();
while (running) {
SDL_Event event;
Uint64 now_ticks;
- float dt;
+ int steps, step;
PhInput input;
int effect = PH_AUDIO_NONE;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) {
running = 0;
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- menu == PH_MENU_START) {
- PhStartAction action = ph_start_key(&start_ui, event.key.scancode);
+ } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
+ int request = ph_game_input(&context, ph_sdl_action(event.key.scancode));
+
+ if (context.menu == PH_MENU_NONE && game == PH_GAME_WORLD &&
+ (event.key.scancode == PH_KEY_INTERACT_1 ||
+ event.key.scancode == PH_KEY_INTERACT_2)) interact_pending = 1;
- if (action == PH_START_CONTINUE) menu = PH_MENU_NONE;
- else if (action == PH_START_NEW) {
+ if (request & PH_GAME_MENU_SOUND) effect = PH_SOUND_MENU;
+ if (request & PH_GAME_REQUEST_EXIT) running = 0;
+ if (request & PH_GAME_REQUEST_NEW) {
ph_world_destroy(&world);
spells = ph_player_spellbook;
if (ph_world_start(&world, &ph_game_world,
(float)PH_VIEW_W, (float)PH_VIEW_H) < 0) running = 0;
else {
remove(PH_SAVE_PATH);
- menu = PH_MENU_NONE;
+ context.menu = PH_MENU_NONE;
}
}
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- menu == PH_MENU_MAIN) {
- PhOptionsAction action;
-
- if (ph_menu_direction(event.key.scancode)) effect = PH_SOUND_MENU;
- action = ph_options_key(&options_ui, event.key.scancode);
- if (action == PH_OPTIONS_CLOSE) menu = PH_MENU_NONE;
- else if (action == PH_OPTIONS_EXIT) running = 0;
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- menu == PH_MENU_NONE && event.key.scancode == PH_KEY_MENU_CANCEL &&
- (game == PH_GAME_WORLD || game == PH_GAME_VICTORY ||
- game == PH_GAME_OVER || (game == PH_GAME_BATTLE &&
- battle.phase != PH_BATTLE_MAGIC &&
- battle.phase != PH_BATTLE_TARGET))) {
- ph_options_open(&options_ui);
- menu = PH_MENU_MAIN;
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- menu == PH_MENU_NONE && game == PH_GAME_BATTLE &&
- (battle.phase == PH_BATTLE_COMMAND ||
- battle.phase == PH_BATTLE_MAGIC ||
- battle.phase == PH_BATTLE_TARGET)) {
- SDL_Scancode key = event.key.scancode;
- int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
- key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
- int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
- key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
-
- if (battle.phase == PH_BATTLE_TARGET) {
- if (previous || next) {
- if (ph_battle_target_step(&battle, &world,
- previous ? -1 : 1) >= 0) effect = PH_SOUND_MENU;
- }
- else if (key == PH_KEY_MENU_ACCEPT)
- ph_battle_confirm_target(&battle, &world);
- else if (ph_menu_back(key))
- ph_battle_cancel(&battle);
- } else if (battle.phase == PH_BATTLE_MAGIC) {
- if ((previous || next) && battle.spells.learned_count > 0) {
- if (ph_battle_spell_step(&battle, previous ? -1 : 1) >= 0)
- effect = PH_SOUND_MENU;
- }
- else if (key == PH_KEY_MENU_ACCEPT)
- ph_battle_cast(&battle, &world);
- else if (ph_menu_back(key))
- ph_battle_cancel(&battle);
- } else if (previous || next) {
- battle.choice.command = (PhBattleCommand)((battle.choice.command + (previous ?
- PH_BATTLE_COMMAND_COUNT - 1 : 1)) % PH_BATTLE_COMMAND_COUNT);
- effect = PH_SOUND_MENU;
- } else if (key == PH_KEY_MENU_ACCEPT) {
- ph_battle_select(&battle, &world);
- }
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- menu == PH_MENU_NONE && game == PH_GAME_VICTORY &&
- event.key.scancode == PH_KEY_MENU_ACCEPT) {
- game = PH_GAME_WORLD;
- menu = PH_MENU_NONE;
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- game == PH_GAME_WORLD && menu == PH_MENU_CHARACTER &&
- ph_menu_back(event.key.scancode)) {
- menu = PH_MENU_NONE;
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- game == PH_GAME_WORLD &&
- (menu == PH_MENU_NONE || menu == PH_MENU_CHARACTER) &&
- event.key.scancode == PH_KEY_CHARACTER) {
- menu = menu == PH_MENU_CHARACTER ? PH_MENU_NONE : PH_MENU_CHARACTER;
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- game == PH_GAME_WORLD &&
- (menu == PH_MENU_NONE || menu == PH_MENU_INVENTORY) &&
- event.key.scancode == PH_KEY_INVENTORY) {
- if (menu == PH_MENU_INVENTORY) menu = PH_MENU_NONE;
- else {
- menu = PH_MENU_INVENTORY;
- ph_inventory_open(&inventory_ui, &world);
- }
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- game == PH_GAME_WORLD && menu == PH_MENU_INVENTORY) {
- if (ph_menu_direction(event.key.scancode) &&
- inventory_ui.mode != PH_INVENTORY_EXAMINE)
- effect = PH_SOUND_MENU;
- if (ph_inventory_key(&inventory_ui, &world, event.key.scancode))
- menu = PH_MENU_NONE;
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- game == PH_GAME_WORLD &&
- (menu == PH_MENU_NONE || menu == PH_MENU_SPELLBOOK) &&
- event.key.scancode == PH_KEY_SPELLBOOK) {
- if (menu == PH_MENU_SPELLBOOK) menu = PH_MENU_NONE;
- else {
- menu = PH_MENU_SPELLBOOK;
- ph_spellbook_open(&spellbook_ui, &spells);
- }
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- game == PH_GAME_WORLD && menu == PH_MENU_SPELLBOOK) {
- if (ph_menu_direction(event.key.scancode) &&
- spellbook_ui.mode != PH_SPELLBOOK_EXAMINE)
- effect = PH_SOUND_MENU;
- if (ph_spellbook_key(&spellbook_ui, &world, &spells,
- event.key.scancode)) menu = PH_MENU_NONE;
- } else if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
- game == PH_GAME_WORLD && menu == PH_MENU_SHOP) {
- if (ph_menu_direction(event.key.scancode) &&
- shop_ui.mode != PH_SHOP_TALK) effect = PH_SOUND_MENU;
- if (ph_shop_key(&shop_ui, &world, &spells, event.key.scancode))
- menu = PH_MENU_NONE;
}
}
- now_ticks = SDL_GetTicks();
- dt = (float)(now_ticks - last_ticks) / 1000.0f;
- if (dt > PH_MAX_FRAME_TIME) dt = PH_MAX_FRAME_TIME;
+ if (!running) break;
+ now_ticks = SDL_GetTicksNS();
+ if (context.time_reset || context.menu != PH_MENU_NONE || game != PH_GAME_WORLD)
+ interact_pending = 0;
+ steps = ph_game_steps(&context, now_ticks - last_ticks);
last_ticks = now_ticks;
input = ph_read_input(SDL_GetKeyboardState(NULL));
- context.menu = menu;
- if (ph_update_game(&context, input, dt) &&
- ph_save_write(PH_SAVE_PATH, &world, &spells) < 0)
- fprintf(stderr, "failed to autosave\n");
- {
- int battle_effect = ph_game_battle_sound(ph_battle_take_event(&battle));
-
- if (battle_effect != PH_AUDIO_NONE) effect = battle_effect;
- if (spellbook_ui.spell_used) effect = PH_SOUND_SPELL;
- spellbook_ui.spell_used = 0;
- }
- if (audio_ready) {
- PhWorldEvent world_event;
-
- ph_audio_music(&audio, ph_game_music(&context));
- ph_audio_set_enabled(&audio, options_ui.music_enabled,
- options_ui.effects_enabled);
- if (effect != PH_AUDIO_NONE) ph_audio_effect(&audio, effect, 1.0f);
- while (ph_world_take_event(&world, &world_event)) {
- int world_effect = ph_game_world_sound(&world, &world_event);
+ ph_game_audio(&context, audio, effect);
+ for (step = 0; step < steps; ++step) {
+ PhInput tick_input = input;
- if (world_effect != PH_AUDIO_NONE)
- ph_audio_effect(&audio, world_effect,
- ph_world_sound_gain(&world, &world_event));
+ tick_input.interact |= interact_pending;
+ interact_pending = 0;
+ if (ph_game_update(&context, tick_input, 1.0f / PH_SIMULATION_HZ, PH_SAVE_PATH) &&
+ ph_save_write(PH_SAVE_PATH, &world, &spells) < 0)
+ fprintf(stderr, "failed to autosave\n");
+ ph_game_interaction(&context);
+ /* Drain each tick before the next can replace battle events or travel. */
+ ph_game_audio(&context, audio, PH_AUDIO_NONE);
+ if (context.menu != PH_MENU_NONE || game == PH_GAME_VICTORY || game == PH_GAME_OVER) {
+ context.clock.credit = 0;
+ break;
}
- ph_audio_update(&audio);
- } else {
- PhWorldEvent world_event;
-
- while (ph_world_take_event(&world, &world_event)) { }
- }
- ph_present_frame(renderer, &assets, &context);
- if (game == PH_GAME_WORLD && menu == PH_MENU_NONE &&
- world.interaction_id > 0) {
- menu = ph_interaction_open(&shop_ui, world.interaction_id);
- world.interaction_id = 0;
}
+
+ ph_present_frame(renderer, &context, &labels);
SDL_Delay(PH_FRAME_DELAY_MS);
}
- if (audio_ready) ph_audio_destroy(&audio);
- ph_render_assets_destroy(&assets);
- SDL_DestroyRenderer(renderer);
+ if (audio) ph_audio_destroy(audio);
+ ph_render_destroy(renderer);
+ SDL_DestroyRenderer(target);
SDL_DestroyWindow(window);
if ((game == PH_GAME_WORLD || game == PH_GAME_VICTORY) &&
ph_save_write(PH_SAVE_PATH, &world, &spells) < 0)
diff --git a/src/game/presentation.c b/src/game/presentation.c
@@ -7,15 +7,6 @@
#define LEN(a) (sizeof(a) / sizeof(0[a]))
-#if PH_USE_SDL
-enum { PH_UI_FONT_W = 8 };
-
-static int
-ph_menu_back(SDL_Scancode key)
-{
- return key == PH_KEY_MENU_CANCEL || key == PH_KEY_MENU_BACK;
-}
-
static float
ph_ui_right(float inset)
{
@@ -31,15 +22,15 @@ ph_ui_bottom(float inset)
static float
ph_ui_center_text(const char *text)
{
- return ((float)PH_VIEW_W - (float)strlen(text) * PH_UI_FONT_W) * 0.5f;
+ return ((float)PH_VIEW_W - (float)strlen(text) * PH_RENDER_FONT_W) * 0.5f;
}
static float
-ph_draw_wrapped_text(SDL_Renderer *renderer, float x, float y,
+ph_draw_wrapped_text(PhRenderer *renderer, float x, float y,
float width, float line_height, const char *text)
{
char line[128];
- size_t columns = width > PH_UI_FONT_W ? (size_t)(width / PH_UI_FONT_W) : 1;
+ size_t columns = width > PH_RENDER_FONT_W ? (size_t)(width / PH_RENDER_FONT_W) : 1;
while (text && *text) {
size_t remaining;
@@ -58,15 +49,13 @@ ph_draw_wrapped_text(SDL_Renderer *renderer, float x, float y,
if (take >= sizeof(line)) take = sizeof(line) - 1;
memcpy(line, text, take);
line[take] = '\0';
- SDL_RenderDebugText(renderer, x, y, line);
+ ph_render_text(renderer, x, y, line);
text += take;
y += line_height;
}
return y;
}
-#endif
-#if PH_USE_SDL
static void
ph_append_bonus(char *text, size_t size, size_t *used, int value, const char *label)
{
@@ -97,83 +86,41 @@ ph_format_bonuses(char *text, size_t size, PhStats bonuses)
}
static void
-ph_draw_menu(SDL_Renderer *renderer, const char *title, SDL_Scancode key)
+ph_draw_menu(PhRenderer *renderer, const char *title, const char *key, const char *back)
{
- SDL_FRect panel = { 12.0f, 12.0f, PH_VIEW_W - 24.0f, PH_VIEW_H - 24.0f };
+ PhRect panel = { 12.0f, 12.0f, PH_VIEW_W - 24.0f, PH_VIEW_H - 24.0f };
char text[32];
float x;
- SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
- SDL_RenderFillRect(renderer, &panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
- SDL_RenderRect(renderer, &panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, 24.0f, 22.0f, title);
- snprintf(text, sizeof(text), "CLOSE: %s / %s", SDL_GetScancodeName(key),
- SDL_GetScancodeName(PH_KEY_MENU_BACK));
- x = ph_ui_right(20.0f) - (float)strlen(text) * PH_UI_FONT_W;
+ ph_render_color(renderer, (PhColor){ PH_MENU_COLOR });
+ ph_render_fill(renderer, panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR });
+ ph_render_outline(renderer, panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, 24.0f, 22.0f, title);
+ snprintf(text, sizeof(text), "CLOSE: %s / %s", key,
+ back);
+ x = ph_ui_right(20.0f) - (float)strlen(text) * PH_RENDER_FONT_W;
if (x < 24.0f) x = 24.0f;
- SDL_RenderDebugText(renderer, x, (float)(PH_VIEW_H - 22), text);
-}
-
-void
-ph_options_open(PhOptionsUi *ui)
-{
- if (!ui) return;
- ui->selected = 0;
- ui->mode = PH_OPTIONS_ROOT;
-}
-
-PhOptionsAction
-ph_options_key(PhOptionsUi *ui, SDL_Scancode key)
-{
- int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
- key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
- int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
- key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
-
- if (!ui) return PH_OPTIONS_CLOSE;
- if (ph_menu_back(key)) {
- if (ui->mode == PH_OPTIONS_SOUND) {
- ui->mode = PH_OPTIONS_ROOT;
- ui->selected = 0;
- return PH_OPTIONS_STAY;
- }
- return PH_OPTIONS_CLOSE;
- }
- if (previous || next) {
- ui->selected = (ui->selected + 1) % 2;
- return PH_OPTIONS_STAY;
- }
- if (key != PH_KEY_MENU_ACCEPT) return PH_OPTIONS_STAY;
- if (ui->mode == PH_OPTIONS_ROOT) {
- if (ui->selected == 1) return PH_OPTIONS_EXIT;
- ui->mode = PH_OPTIONS_SOUND;
- ui->selected = 0;
- } else if (ui->selected == 0) {
- ui->music_enabled = !ui->music_enabled;
- } else {
- ui->effects_enabled = !ui->effects_enabled;
- }
- return PH_OPTIONS_STAY;
+ ph_render_text(renderer, x, (float)(PH_VIEW_H - 22), text);
}
static void
-ph_draw_options(SDL_Renderer *renderer, const PhOptionsUi *ui)
+ph_draw_options(PhRenderer *renderer, const PhOptionsUi *ui)
{
static const char *root_options[] = { "SOUND", "EXIT GAME" };
- SDL_FRect panel = { PH_VIEW_W * 0.5f - 84.0f,
+ PhRect panel = { PH_VIEW_W * 0.5f - 84.0f,
PH_VIEW_H * 0.5f - 58.0f, 168.0f, 116.0f };
char text[48];
int i;
if (!ui) return;
- SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
- SDL_RenderFillRect(renderer, &panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
- SDL_RenderRect(renderer, &panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, panel.x + 12.0f, panel.y + 10.0f,
+ ph_render_color(renderer, (PhColor){ PH_MENU_COLOR });
+ ph_render_fill(renderer, panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR });
+ ph_render_outline(renderer, panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, panel.x + 12.0f, panel.y + 10.0f,
ui->mode == PH_OPTIONS_SOUND ? "SOUND" : "OPTIONS");
for (i = 0; i < 2; ++i) {
if (ui->mode == PH_OPTIONS_SOUND)
@@ -182,55 +129,42 @@ ph_draw_options(SDL_Renderer *renderer, const PhOptionsUi *ui)
(i == 0 ? ui->music_enabled : ui->effects_enabled) ? "ON" : "OFF");
else snprintf(text, sizeof(text), "%c %s",
ui->selected == i ? '>' : ' ', root_options[i]);
- SDL_RenderDebugText(renderer, panel.x + 20.0f,
+ ph_render_text(renderer, panel.x + 20.0f,
panel.y + 38.0f + (float)i * 20.0f, text);
}
- SDL_RenderDebugText(renderer, panel.x + 12.0f, panel.y + 94.0f,
+ ph_render_text(renderer, panel.x + 12.0f, panel.y + 94.0f,
"ESC/BKSP BACK");
}
-PhStartAction
-ph_start_key(PhStartUi *ui, SDL_Scancode key)
-{
- int direction = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
- key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2;
-
- if (!ui) return PH_START_STAY;
- if (direction) ui->selected = ui->can_continue ? !ui->selected : 1;
- if (key != PH_KEY_MENU_ACCEPT) return PH_START_STAY;
- return ui->selected == 0 && ui->can_continue ?
- PH_START_CONTINUE : PH_START_NEW;
-}
-
static void
-ph_draw_start(SDL_Renderer *renderer, const PhStartUi *ui)
+ph_draw_start(PhRenderer *renderer, const PhStartUi *ui)
{
- SDL_FRect panel = { PH_VIEW_W * 0.5f - 82.0f,
+ PhRect panel = { PH_VIEW_W * 0.5f - 82.0f,
PH_VIEW_H * 0.5f - 52.0f, 164.0f, 104.0f };
char text[32];
int i;
if (!ui) return;
- SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
- SDL_RenderFillRect(renderer, &panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
- SDL_RenderRect(renderer, &panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, panel.x + 42.0f, panel.y + 12.0f, "PHANTASIA");
+ ph_render_color(renderer, (PhColor){ PH_MENU_COLOR });
+ ph_render_fill(renderer, panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR });
+ ph_render_outline(renderer, panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, panel.x + 42.0f, panel.y + 12.0f, "PHANTASIA");
for (i = 0; i < 2; ++i) {
snprintf(text, sizeof(text), "%c %s", ui->selected == i ? '>' : ' ',
i == 0 ? "CONTINUE" : "NEW GAME");
if (i == 0 && !ui->can_continue)
- SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR);
- else SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, panel.x + 36.0f,
+ ph_render_color(renderer, (PhColor){ PH_MENU_DISABLED_COLOR });
+ else ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, panel.x + 36.0f,
panel.y + 42.0f + (float)i * 18.0f, text);
}
}
static void
-ph_draw_character_sheet(SDL_Renderer *renderer, const PhRenderAssets *assets,
- const PhWorld *world)
+ph_draw_character_sheet(PhRenderer *renderer,
+ const PhWorld *world, const PhControlLabels *labels)
{
static const char *slot_names[PH_EQUIP_COUNT] = {
[PH_EQUIP_WEAPON] = "WEAPON",
@@ -240,233 +174,122 @@ ph_draw_character_sheet(SDL_Renderer *renderer, const PhRenderAssets *assets,
const PhEntity *player = ph_world_player(world);
const PhEntityDef *def;
PhStats stats;
- SDL_Texture *texture;
- SDL_FRect avatar = { 32.0f, 46.0f, PH_TILE_SIZE * 3.0f,
+ int asset_id;
+ PhRect avatar = { 32.0f, 46.0f, PH_TILE_SIZE * 3.0f,
PH_TILE_SIZE * 3.0f };
char text[128];
int slot;
if (!player || !(def = ph_world_entity_def(world, player->type_id))) return;
stats = ph_world_entity_stats(world, player);
- ph_draw_menu(renderer, "CHARACTER", PH_KEY_CHARACTER);
- SDL_RenderDebugText(renderer, 104.0f, 38.0f, def->name);
+ ph_draw_menu(renderer, "CHARACTER", labels->character, labels->back);
+ ph_render_text(renderer, 104.0f, 38.0f, def->name);
- texture = ph_render_asset(assets, def->asset_id);
- if (texture)
- ph_render_sprite(renderer, texture,
+ asset_id = def->asset_id;
+ if (ph_render_has_asset(renderer, asset_id))
+ ph_render_sprite(renderer, asset_id,
def->sprite_tiles[PH_SPRITE_DOWN][0][0],
def->sprite_tiles[PH_SPRITE_DOWN][0][1],
- avatar, SDL_FLIP_NONE);
+ avatar, PH_FLIP_NONE);
snprintf(text, sizeof(text), "HP %d / %d", player->vitals.hp, stats.max_hp);
- SDL_RenderDebugText(renderer, 104.0f, 54.0f, text);
+ ph_render_text(renderer, 104.0f, 54.0f, text);
snprintf(text, sizeof(text), "MP %d / %d", player->vitals.mp, stats.max_mp);
- SDL_RenderDebugText(renderer, 104.0f, 66.0f, text);
+ ph_render_text(renderer, 104.0f, 66.0f, text);
snprintf(text, sizeof(text), "STR %d", stats.strength);
- SDL_RenderDebugText(renderer, 104.0f, 78.0f, text);
+ ph_render_text(renderer, 104.0f, 78.0f, text);
snprintf(text, sizeof(text), "DEF %d", stats.defense);
- SDL_RenderDebugText(renderer, 104.0f, 90.0f, text);
+ ph_render_text(renderer, 104.0f, 90.0f, text);
snprintf(text, sizeof(text), "MAG %d", stats.magic);
- SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 54.0f, text);
+ ph_render_text(renderer, ph_ui_right(88.0f), 54.0f, text);
snprintf(text, sizeof(text), "MDEF %d", stats.magic_defense);
- SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 66.0f, text);
+ ph_render_text(renderer, ph_ui_right(88.0f), 66.0f, text);
snprintf(text, sizeof(text), "AGI %d", stats.agility);
- SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 78.0f, text);
+ ph_render_text(renderer, ph_ui_right(88.0f), 78.0f, text);
snprintf(text, sizeof(text), "LCK %d", stats.luck);
- SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 90.0f, text);
+ ph_render_text(renderer, ph_ui_right(88.0f), 90.0f, text);
snprintf(text, sizeof(text), "EVA %d", stats.evasion);
- SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 102.0f, text);
+ ph_render_text(renderer, ph_ui_right(88.0f), 102.0f, text);
snprintf(text, sizeof(text), "ACC %d", stats.accuracy);
- SDL_RenderDebugText(renderer, ph_ui_right(88.0f), 114.0f, text);
- SDL_RenderDebugText(renderer, 24.0f, 128.0f, "EQUIPPED");
+ ph_render_text(renderer, ph_ui_right(88.0f), 114.0f, text);
+ ph_render_text(renderer, 24.0f, 128.0f, "EQUIPPED");
for (slot = PH_EQUIP_WEAPON; slot < PH_EQUIP_COUNT; ++slot) {
const PhItemDef *item = ph_world_item_def(world, player->equipment[slot]);
- SDL_Texture *icon_texture;
+ int icon_id;
float y = 144.0f + (float)(slot - PH_EQUIP_WEAPON) * 25.0f;
snprintf(text, sizeof(text), "%s %s", slot_names[slot], item ? item->name : "-");
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, 48.0f, y, text);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, 48.0f, y, text);
if (!item) continue;
- icon_texture = ph_render_asset(assets, item->asset_id);
- if (icon_texture) {
- SDL_FRect icon = { 24.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE };
- ph_render_sprite(renderer, icon_texture,
- item->sprite_tile_x, item->sprite_tile_y, icon, SDL_FLIP_NONE);
+ icon_id = item->asset_id;
+ if (ph_render_has_asset(renderer, icon_id)) {
+ PhRect icon = { 24.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE };
+ ph_render_sprite(renderer, icon_id,
+ item->sprite_tile_x, item->sprite_tile_y, icon, PH_FLIP_NONE);
}
ph_format_bonuses(text, sizeof(text), item->bonuses);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR);
- SDL_RenderDebugText(renderer, 48.0f, y + 10.0f, text);
- }
-}
-
-int
-ph_inventory_step(const PhWorld *world, int selected, int direction)
-{
- int i = selected < 0 ? (direction > 0 ? -1 : 0) : selected;
- int step;
-
- for (step = 0; step < world->content.item_count; ++step) {
- i += direction;
- if (i < 0) i = world->content.item_count - 1;
- if (i >= world->content.item_count) i = 0;
- if (world->inventory[i] > 0) return i;
- }
- return -1;
-}
-
-void
-ph_inventory_open(PhInventoryUi *ui, const PhWorld *world)
-{
- memset(ui, 0, sizeof(*ui));
- ui->selected = ph_inventory_step(world, -1, 1);
-}
-
-static void
-ph_inventory_reselect(PhInventoryUi *ui, const PhWorld *world)
-{
- if (ui->selected >= 0 && world->inventory[ui->selected] > 0) return;
- ui->selected = ph_inventory_step(world, ui->selected, 1);
-}
-
-static void
-ph_inventory_action(PhInventoryUi *ui, PhWorld *world, int action)
-{
- const PhItemDef *item;
- const char *name;
- int result = -1;
-
- if (ui->selected < 0) return;
- item = &world->content.items[ui->selected];
- name = item->name ? item->name : "Item";
- if (action == PH_INVENTORY_EQUIP)
- result = ph_world_equip_inventory_item(world, world->player_index, item->id);
- else if (action == PH_INVENTORY_USE)
- result = ph_world_use_inventory_item(world, world->player_index, item->id);
- else if (action == PH_INVENTORY_DROP)
- result = ph_world_drop_inventory_item(world, world->player_index, item->id);
- else {
- ui->mode = PH_INVENTORY_EXAMINE;
- ui->status[0] = '\0';
- return;
- }
-
- if (result < 0) {
- snprintf(ui->status, sizeof(ui->status), "Cannot %s %s.",
- action == PH_INVENTORY_EQUIP ? "equip" :
- action == PH_INVENTORY_USE ? "use" : "drop", name);
- return;
- }
- snprintf(ui->status, sizeof(ui->status), "%s %s.",
- action == PH_INVENTORY_EQUIP ? "Equipped" :
- action == PH_INVENTORY_USE ? "Used" : "Dropped", name);
- ui->mode = PH_INVENTORY_BROWSE;
- ph_inventory_reselect(ui, world);
-}
-
-static int
-ph_inventory_action_enabled(const PhItemDef *item, int action)
-{
- if (action == PH_INVENTORY_EQUIP) return item->equip_slot != PH_EQUIP_NONE;
- if (action == PH_INVENTORY_USE) return item->use != PH_ITEM_USE_NONE;
- return 1;
-}
-
-static void
-ph_inventory_action_order(const PhItemDef *item, int actions[PH_INVENTORY_ACTION_COUNT])
-{
- if (item->equip_slot != PH_EQUIP_NONE) {
- actions[0] = PH_INVENTORY_EQUIP;
- actions[1] = PH_INVENTORY_USE;
- } else if (item->use != PH_ITEM_USE_NONE) {
- actions[0] = PH_INVENTORY_USE;
- actions[1] = PH_INVENTORY_EQUIP;
- } else {
- actions[0] = PH_INVENTORY_DROP;
- actions[1] = PH_INVENTORY_EXAMINE_ACTION;
- }
- if (actions[0] < PH_INVENTORY_DROP) {
- actions[2] = PH_INVENTORY_DROP;
- actions[3] = PH_INVENTORY_EXAMINE_ACTION;
- } else {
- actions[2] = PH_INVENTORY_EQUIP;
- actions[3] = PH_INVENTORY_USE;
+ ph_render_color(renderer, (PhColor){ PH_MENU_BONUS_COLOR });
+ ph_render_text(renderer, 48.0f, y + 10.0f, text);
}
}
-static int
-ph_inventory_action_step(const PhItemDef *item, int selected, int direction)
-{
- int actions[PH_INVENTORY_ACTION_COUNT];
- int position = 0;
- int step;
-
- ph_inventory_action_order(item, actions);
- while (position < PH_INVENTORY_ACTION_COUNT && actions[position] != selected) ++position;
- for (step = 0; step < PH_INVENTORY_ACTION_COUNT; ++step) {
- position = (position + (direction < 0 ?
- PH_INVENTORY_ACTION_COUNT - 1 : 1)) % PH_INVENTORY_ACTION_COUNT;
- if (ph_inventory_action_enabled(item, actions[position])) return actions[position];
- }
- return selected;
-}
-
static void
-ph_draw_inventory_popup(SDL_Renderer *renderer, const PhWorld *world,
+ph_draw_inventory_popup(PhRenderer *renderer, const PhWorld *world,
const PhInventoryUi *ui)
{
static const char *action_names[] = { "EQUIP", "USE", "DROP", "EXAMINE" };
const PhItemDef *item = &world->content.items[ui->selected];
int actions[PH_INVENTORY_ACTION_COUNT];
- SDL_FRect popup = ui->mode == PH_INVENTORY_EXAMINE ?
- (SDL_FRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } :
- (SDL_FRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 124.0f };
+ PhRect popup = ui->mode == PH_INVENTORY_EXAMINE ?
+ (PhRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } :
+ (PhRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 124.0f };
char text[128];
float x = ui->mode == PH_INVENTORY_EXAMINE ? 52.0f : ph_ui_right(152.0f);
int i;
- SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
- SDL_RenderFillRect(renderer, &popup);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
- SDL_RenderRect(renderer, &popup);
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_COLOR });
+ ph_render_fill(renderer, popup);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR });
+ ph_render_outline(renderer, popup);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
if (ui->mode == PH_INVENTORY_EXAMINE) {
- SDL_RenderDebugText(renderer, x, 58.0f,
+ ph_render_text(renderer, x, 58.0f,
item->name ? item->name : "ITEM");
- SDL_RenderDebugText(renderer, x, 76.0f,
+ ph_render_text(renderer, x, 76.0f,
item->description ? item->description : "No description.");
snprintf(text, sizeof(text), "SELL %d G", item->sell_value);
- SDL_RenderDebugText(renderer, x, 96.0f, text);
+ ph_render_text(renderer, x, 96.0f, text);
ph_format_bonuses(text, sizeof(text), item->bonuses);
if (item->use == PH_ITEM_USE_HEAL)
snprintf(text, sizeof(text), "HEALS %d HP", item->use_power);
else if (item->use == PH_ITEM_USE_RESTORE_MP)
snprintf(text, sizeof(text), "RESTORES %d MP", item->use_power);
- if (text[0]) SDL_RenderDebugText(renderer, x, 112.0f, text);
- SDL_RenderDebugText(renderer, x, 148.0f, "ENTER BACK");
+ if (text[0]) ph_render_text(renderer, x, 112.0f, text);
+ ph_render_text(renderer, x, 148.0f, "ENTER BACK");
return;
}
- SDL_RenderDebugText(renderer, ph_ui_right(152.0f), 58.0f, "ITEM ACTION");
+ ph_render_text(renderer, ph_ui_right(152.0f), 58.0f, "ITEM ACTION");
ph_inventory_action_order(item, actions);
for (i = 0; i < PH_INVENTORY_ACTION_COUNT; ++i) {
int action = actions[i];
if (ph_inventory_action_enabled(item, action))
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
else
- SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_DISABLED_COLOR });
snprintf(text, sizeof(text), "%c %s",
action == ui->option ? '>' : ' ', action_names[action]);
- SDL_RenderDebugText(renderer, ph_ui_right(152.0f),
+ ph_render_text(renderer, ph_ui_right(152.0f),
78.0f + (float)i * 18.0f, text);
}
}
static void
-ph_draw_inventory(SDL_Renderer *renderer, const PhRenderAssets *assets,
- const PhWorld *world, const PhInventoryUi *ui)
+ph_draw_inventory(PhRenderer *renderer,
+ const PhWorld *world, const PhInventoryUi *ui, const PhControlLabels *labels)
{
const PhEntity *player = ph_world_player(world);
char text[128];
@@ -482,14 +305,14 @@ ph_draw_inventory(SDL_Renderer *renderer, const PhRenderAssets *assets,
++total;
}
page = selected_rank / 5;
- ph_draw_menu(renderer, "INVENTORY", PH_KEY_INVENTORY);
+ ph_draw_menu(renderer, "INVENTORY", labels->inventory, labels->back);
if (player) {
snprintf(text, sizeof(text), "GOLD %d", player->gold);
- SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 22.0f, text);
+ ph_render_text(renderer, ph_ui_right(96.0f), 22.0f, text);
}
for (i = 0; i < world->content.item_count; ++i) {
const PhItemDef *item = &world->content.items[i];
- SDL_Texture *texture;
+ int asset_id;
float y;
if (world->inventory[i] <= 0) continue;
@@ -498,181 +321,85 @@ ph_draw_inventory(SDL_Renderer *renderer, const PhRenderAssets *assets,
continue;
}
y = 48.0f + (float)(rank++ - page * 5) * 27.0f;
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, 20.0f, y, i == ui->selected ? ">" : " ");
- texture = ph_render_asset(assets, item->asset_id);
- if (texture) {
- SDL_FRect icon = { 32.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE };
- ph_render_sprite(renderer, texture,
- item->sprite_tile_x, item->sprite_tile_y, icon, SDL_FLIP_NONE);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, 20.0f, y, i == ui->selected ? ">" : " ");
+ asset_id = item->asset_id;
+ if (ph_render_has_asset(renderer, asset_id)) {
+ PhRect icon = { 32.0f, y - 4.0f, PH_TILE_SIZE, PH_TILE_SIZE };
+ ph_render_sprite(renderer, asset_id,
+ item->sprite_tile_x, item->sprite_tile_y, icon, PH_FLIP_NONE);
}
snprintf(text, sizeof(text), "%s x%d",
item->name ? item->name : "Unknown", world->inventory[i]);
- SDL_RenderDebugText(renderer, 56.0f, y, text);
+ ph_render_text(renderer, 56.0f, y, text);
ph_format_bonuses(text, sizeof(text), item->bonuses);
if (!text[0] && item->use == PH_ITEM_USE_HEAL)
snprintf(text, sizeof(text), "HEALS %d HP", item->use_power);
if (!text[0] && item->use == PH_ITEM_USE_RESTORE_MP)
snprintf(text, sizeof(text), "RESTORES %d MP", item->use_power);
if (!text[0]) snprintf(text, sizeof(text), "SELL %d G", item->sell_value);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR);
- SDL_RenderDebugText(renderer, 56.0f, y + 10.0f, text);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BONUS_COLOR });
+ ph_render_text(renderer, 56.0f, y + 10.0f, text);
}
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- if (!total) SDL_RenderDebugText(renderer, 24.0f, 48.0f, "EMPTY");
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ if (!total) ph_render_text(renderer, 24.0f, 48.0f, "EMPTY");
if (total > 5) {
snprintf(text, sizeof(text), "PAGE %d/%d", page + 1, (total + 4) / 5);
- SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(56.0f), text);
+ ph_render_text(renderer, 24.0f, ph_ui_bottom(56.0f), text);
}
if (ui->status[0])
- SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status);
+ ph_render_text(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status);
snprintf(text, sizeof(text), "%s SELECT",
- SDL_GetScancodeName(PH_KEY_MENU_ACCEPT));
- SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(24.0f), text);
+ labels->accept);
+ ph_render_text(renderer, 24.0f, ph_ui_bottom(24.0f), text);
if (ui->selected >= 0 && ui->mode != PH_INVENTORY_BROWSE)
ph_draw_inventory_popup(renderer, world, ui);
}
-int
-ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key)
-{
- int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
- key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
- int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
- key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
-
- if (ph_menu_back(key)) {
- if (ui->mode == PH_INVENTORY_EXAMINE)
- ui->mode = PH_INVENTORY_ACTIONS;
- else if (ui->mode == PH_INVENTORY_ACTIONS)
- ui->mode = PH_INVENTORY_BROWSE;
- else
- return 1;
- ui->status[0] = '\0';
- return 0;
- }
- if (key == PH_KEY_MENU_ACCEPT) {
- if (ui->mode == PH_INVENTORY_BROWSE && ui->selected >= 0) {
- int actions[PH_INVENTORY_ACTION_COUNT];
-
- ph_inventory_action_order(&world->content.items[ui->selected], actions);
- ui->mode = PH_INVENTORY_ACTIONS;
- ui->option = actions[0];
- ui->status[0] = '\0';
- } else if (ui->mode == PH_INVENTORY_ACTIONS) {
- ph_inventory_action(ui, world, ui->option);
- } else if (ui->mode == PH_INVENTORY_EXAMINE) {
- ui->mode = PH_INVENTORY_ACTIONS;
- }
- return 0;
- }
- if ((!previous && !next) || ui->mode == PH_INVENTORY_EXAMINE) return 0;
- if (ui->mode == PH_INVENTORY_ACTIONS) {
- ui->option = ph_inventory_action_step(&world->content.items[ui->selected],
- ui->option, previous ? -1 : 1);
- } else {
- ui->selected = ph_inventory_step(world, ui->selected, previous ? -1 : 1);
- ui->status[0] = '\0';
- }
- return 0;
-}
-
-void
-ph_spellbook_open(PhSpellbookUi *ui, const PhSpellBook *spells)
-{
- memset(ui, 0, sizeof(*ui));
- ui->selected = spells && spells->learned_count > 0 ? 0 : -1;
-}
-
-static int
-ph_spellbook_action_enabled(const PhSpellDef *spell, int action)
-{
- return action == PH_SPELLBOOK_EXAMINE_ACTION ||
- (spell && spell->field_use != PH_SPELL_FIELD_NONE);
-}
-
-static int
-ph_spellbook_action_step(const PhSpellDef *spell, int selected, int direction)
-{
- int action;
- int step;
-
- for (step = 1; step <= PH_SPELLBOOK_ACTION_COUNT; ++step) {
- action = (selected + (direction < 0 ?
- PH_SPELLBOOK_ACTION_COUNT - step : step)) % PH_SPELLBOOK_ACTION_COUNT;
- if (ph_spellbook_action_enabled(spell, action)) return action;
- }
- return selected;
-}
-
static void
-ph_spellbook_action(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells)
-{
- const PhSpellDef *spell;
- int amount;
-
- if (ui->selected < 0 || ui->selected >= spells->learned_count) return;
- spell = ph_spellbook_def(spells, spells->learned[ui->selected]);
- if (!spell) return;
- if (ui->option == PH_SPELLBOOK_EXAMINE_ACTION) {
- ui->mode = PH_SPELLBOOK_EXAMINE;
- ui->status[0] = '\0';
- return;
- }
- amount = ph_spellbook_use(spells, world, world->player_index, spell->id);
- if (amount < 0)
- snprintf(ui->status, sizeof(ui->status), "Cannot use %s.", spell->name);
- else {
- snprintf(ui->status, sizeof(ui->status), "Restored %d HP.", amount);
- ui->spell_used = 1;
- }
- ui->mode = PH_SPELLBOOK_BROWSE;
-}
-
-static void
-ph_draw_spellbook_popup(SDL_Renderer *renderer, const PhSpellBook *spells,
+ph_draw_spellbook_popup(PhRenderer *renderer, const PhSpellBook *spells,
const PhSpellbookUi *ui)
{
const PhSpellDef *spell = ph_spellbook_def(spells,
spells->learned[ui->selected]);
- SDL_FRect popup = ui->mode == PH_SPELLBOOK_EXAMINE ?
- (SDL_FRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } :
- (SDL_FRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 82.0f };
+ PhRect popup = ui->mode == PH_SPELLBOOK_EXAMINE ?
+ (PhRect){ 40.0f, 48.0f, PH_VIEW_W - 64.0f, 124.0f } :
+ (PhRect){ PH_VIEW_W - 164.0f, 48.0f, 140.0f, 82.0f };
char text[96];
int action;
if (!spell) return;
- SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
- SDL_RenderFillRect(renderer, &popup);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
- SDL_RenderRect(renderer, &popup);
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_COLOR });
+ ph_render_fill(renderer, popup);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR });
+ ph_render_outline(renderer, popup);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
if (ui->mode == PH_SPELLBOOK_EXAMINE) {
- SDL_RenderDebugText(renderer, 52.0f, 58.0f, spell->name);
+ ph_render_text(renderer, 52.0f, 58.0f, spell->name);
ph_draw_wrapped_text(renderer, 52.0f, 76.0f, popup.w - 24.0f, 12.0f,
spell->description ? spell->description : "No description.");
snprintf(text, sizeof(text), "%d MP POWER %d DELAY %d%%",
spell->mp_cost, spell->power, spell->delay_percent);
- SDL_RenderDebugText(renderer, 52.0f, 126.0f, text);
- SDL_RenderDebugText(renderer, 52.0f, 148.0f, "ENTER BACK");
+ ph_render_text(renderer, 52.0f, 126.0f, text);
+ ph_render_text(renderer, 52.0f, 148.0f, "ENTER BACK");
return;
}
- SDL_RenderDebugText(renderer, ph_ui_right(152.0f), 58.0f, "SPELL ACTION");
+ ph_render_text(renderer, ph_ui_right(152.0f), 58.0f, "SPELL ACTION");
for (action = 0; action < PH_SPELLBOOK_ACTION_COUNT; ++action) {
if (ph_spellbook_action_enabled(spell, action))
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
else
- SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_DISABLED_COLOR });
snprintf(text, sizeof(text), "%c %s", ui->option == action ? '>' : ' ',
action == PH_SPELLBOOK_USE ? "USE" : "EXAMINE");
- SDL_RenderDebugText(renderer, ph_ui_right(152.0f),
+ ph_render_text(renderer, ph_ui_right(152.0f),
78.0f + (float)action * 18.0f, text);
}
}
static void
-ph_draw_spellbook(SDL_Renderer *renderer, const PhWorld *world,
- const PhSpellBook *spells, const PhSpellbookUi *ui)
+ph_draw_spellbook(PhRenderer *renderer, const PhWorld *world,
+ const PhSpellBook *spells, const PhSpellbookUi *ui, const PhControlLabels *labels)
{
const PhEntity *player = ph_world_player(world);
PhStats stats = ph_world_entity_stats(world, player);
@@ -680,191 +407,44 @@ ph_draw_spellbook(SDL_Renderer *renderer, const PhWorld *world,
int page = ui->selected < 0 ? 0 : ui->selected / 6;
int i;
- ph_draw_menu(renderer, "MAGICK BOOK", PH_KEY_SPELLBOOK);
+ ph_draw_menu(renderer, "MAGICK BOOK", labels->spellbook, labels->back);
if (player) {
snprintf(text, sizeof(text), "MP %d/%d", player->vitals.mp, stats.max_mp);
- SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 22.0f, text);
+ ph_render_text(renderer, ph_ui_right(96.0f), 22.0f, text);
}
for (i = page * 6; i < spells->learned_count && i < page * 6 + 6; ++i) {
const PhSpellDef *spell = ph_spellbook_def(spells, spells->learned[i]);
float y = 48.0f + (float)(i - page * 6) * 24.0f;
if (!spell) continue;
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
snprintf(text, sizeof(text), "%c %-14s %2d MP",
ui->selected == i ? '>' : ' ', spell->name, spell->mp_cost);
- SDL_RenderDebugText(renderer, 24.0f, y, text);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR);
+ ph_render_text(renderer, 24.0f, y, text);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BONUS_COLOR });
snprintf(text, sizeof(text), "%s POWER %d DELAY %d%%",
spell->field_use != PH_SPELL_FIELD_NONE ? "FIELD" : "BATTLE",
spell->power, spell->delay_percent);
- SDL_RenderDebugText(renderer, 40.0f, y + 10.0f, text);
+ ph_render_text(renderer, 40.0f, y + 10.0f, text);
}
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
if (spells->learned_count <= 0)
- SDL_RenderDebugText(renderer, 24.0f, 48.0f, "NO SPELLS LEARNED");
+ ph_render_text(renderer, 24.0f, 48.0f, "NO SPELLS LEARNED");
if (spells->learned_count > 6) {
snprintf(text, sizeof(text), "PAGE %d/%d", page + 1,
(spells->learned_count + 5) / 6);
- SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(56.0f), text);
+ ph_render_text(renderer, 24.0f, ph_ui_bottom(56.0f), text);
}
if (ui->status[0])
- SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status);
- SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(24.0f), "ENTER SELECT");
+ ph_render_text(renderer, 24.0f, ph_ui_bottom(40.0f), ui->status);
+ ph_render_text(renderer, 24.0f, ph_ui_bottom(24.0f), "ENTER SELECT");
if (ui->selected >= 0 && ui->mode != PH_SPELLBOOK_BROWSE)
ph_draw_spellbook_popup(renderer, spells, ui);
}
-int
-ph_spellbook_key(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells,
- SDL_Scancode key)
-{
- int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
- key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
- int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
- key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
- const PhSpellDef *spell = ui->selected >= 0 &&
- ui->selected < spells->learned_count ? ph_spellbook_def(spells,
- spells->learned[ui->selected]) : NULL;
-
- if (ph_menu_back(key)) {
- if (ui->mode == PH_SPELLBOOK_EXAMINE)
- ui->mode = PH_SPELLBOOK_ACTIONS;
- else if (ui->mode == PH_SPELLBOOK_ACTIONS)
- ui->mode = PH_SPELLBOOK_BROWSE;
- else return 1;
- ui->status[0] = '\0';
- return 0;
- }
- if (key == PH_KEY_MENU_ACCEPT) {
- if (ui->mode == PH_SPELLBOOK_BROWSE && spell) {
- ui->mode = PH_SPELLBOOK_ACTIONS;
- ui->option = ph_spellbook_action_enabled(spell, PH_SPELLBOOK_USE) ?
- PH_SPELLBOOK_USE : PH_SPELLBOOK_EXAMINE_ACTION;
- ui->status[0] = '\0';
- } else if (ui->mode == PH_SPELLBOOK_ACTIONS) {
- ph_spellbook_action(ui, world, spells);
- } else if (ui->mode == PH_SPELLBOOK_EXAMINE) {
- ui->mode = PH_SPELLBOOK_ACTIONS;
- }
- return 0;
- }
- if ((!previous && !next) || ui->mode == PH_SPELLBOOK_EXAMINE) return 0;
- if (ui->mode == PH_SPELLBOOK_ACTIONS) {
- ui->option = ph_spellbook_action_step(spell, ui->option,
- previous ? -1 : 1);
- } else if (spells->learned_count > 0) {
- ui->selected = (ui->selected + (previous ?
- spells->learned_count - 1 : 1)) % spells->learned_count;
- ui->status[0] = '\0';
- }
- return 0;
-}
-
-void
-ph_shop_open(PhShopUi *ui, int shop_id)
-{
- memset(ui, 0, sizeof(*ui));
- ui->shop_id = shop_id;
-}
-
-PhMenu
-ph_interaction_open(PhShopUi *shop_ui, int interaction_id)
-{
- const PhInteractionDef *interaction = ph_game_interaction_def(interaction_id);
-
- if (!interaction) return PH_MENU_NONE;
- if (interaction->kind == PH_INTERACTION_SHOP &&
- ph_game_shop_def(interaction->content_id)) {
- ph_shop_open(shop_ui, interaction->content_id);
- return PH_MENU_SHOP;
- }
- return PH_MENU_NONE;
-}
-
-int
-ph_shop_key(PhShopUi *ui, PhWorld *world, PhSpellBook *spells, SDL_Scancode key)
-{
- const PhShopDef *shop = ph_game_shop_def(ui->shop_id);
- int previous = key == PH_KEY_MENU_UP_1 || key == PH_KEY_MENU_UP_2 ||
- key == PH_KEY_MENU_LEFT_1 || key == PH_KEY_MENU_LEFT_2;
- int next = key == PH_KEY_MENU_DOWN_1 || key == PH_KEY_MENU_DOWN_2 ||
- key == PH_KEY_MENU_RIGHT_1 || key == PH_KEY_MENU_RIGHT_2;
-
- if (!shop) return 1;
- if (ph_menu_back(key)) {
- if (ui->mode == PH_SHOP_ROOT) return 1;
- ui->mode = PH_SHOP_ROOT;
- ui->selected = 0;
- ui->status[0] = '\0';
- return 0;
- }
- if (ui->mode == PH_SHOP_TALK) {
- if (key == PH_KEY_MENU_ACCEPT) ui->mode = PH_SHOP_ROOT;
- return 0;
- }
- if (previous || next) {
- int count = ui->mode == PH_SHOP_ROOT ?
- (shop->kind == PH_SHOP_ITEMS ? 3 : 2) :
- ui->mode == PH_SHOP_BUY ? shop->stock_count : world->content.item_count;
-
- if (ui->mode == PH_SHOP_SELL) {
- ui->selected = ph_inventory_step(world, ui->selected,
- previous ? -1 : 1);
- } else if (count > 0) {
- ui->selected = (ui->selected + (previous ? count - 1 : 1)) % count;
- }
- ui->status[0] = '\0';
- return 0;
- }
- if (key != PH_KEY_MENU_ACCEPT) return 0;
- if (ui->mode == PH_SHOP_ROOT) {
- ui->mode = shop->kind == PH_SHOP_ITEMS ?
- (PhShopMode)(ui->selected + 1) :
- ui->selected == 0 ? PH_SHOP_BUY : PH_SHOP_TALK;
- ui->selected = ui->mode == PH_SHOP_SELL ?
- ph_inventory_step(world, -1, 1) : 0;
- return 0;
- }
- if (ui->mode == PH_SHOP_BUY && ui->selected < shop->stock_count) {
- if (shop->kind == PH_SHOP_SPELLS) {
- const PhSpellDef *spell = ph_spellbook_def(spells,
- shop->stock[ui->selected]);
-
- if (!spell) return 0;
- if (ph_spellbook_has(spells, spell->id))
- snprintf(ui->status, sizeof(ui->status), "Already learned.");
- else if (ph_spellbook_buy(spells, world, world->player_index,
- spell->id) < 0)
- snprintf(ui->status, sizeof(ui->status), "Not enough gold.");
- else
- snprintf(ui->status, sizeof(ui->status), "Learned %s.", spell->name);
- } else {
- const PhItemDef *item = ph_world_item_def(world,
- shop->stock[ui->selected]);
-
- if (!item || ph_world_buy_item(world, world->player_index, item->id) < 0)
- snprintf(ui->status, sizeof(ui->status), "Not enough gold.");
- else
- snprintf(ui->status, sizeof(ui->status), "Bought %s.", item->name);
- }
- } else if (ui->mode == PH_SHOP_SELL && ui->selected >= 0) {
- const PhItemDef *item = &world->content.items[ui->selected];
-
- if (ph_world_sell_item(world, world->player_index, item->id) < 0)
- snprintf(ui->status, sizeof(ui->status), "Cannot sell that.");
- else {
- snprintf(ui->status, sizeof(ui->status), "Sold %s.", item->name);
- if (world->inventory[ui->selected] <= 0)
- ui->selected = ph_inventory_step(world, ui->selected, 1);
- }
- }
- return 0;
-}
-
static void
-ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world,
- const PhSpellBook *spells, const PhShopUi *ui)
+ph_draw_shop(PhRenderer *renderer, const PhWorld *world,
+ const PhSpellBook *spells, const PhShopUi *ui, const PhControlLabels *labels)
{
static const char *item_options[] = { "BUY", "SELL", "TALK" };
static const char *magick_options[] = { "LEARN", "TALK" };
@@ -874,9 +454,9 @@ ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world,
int i;
if (!shop || !player) return;
- ph_draw_menu(renderer, shop->name, PH_KEY_MENU_CANCEL);
+ ph_draw_menu(renderer, shop->name, labels->cancel, labels->back);
snprintf(text, sizeof(text), "GOLD %d", player->gold);
- SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 38.0f, text);
+ ph_render_text(renderer, ph_ui_right(96.0f), 38.0f, text);
if (ui->mode == PH_SHOP_ROOT) {
const char **options = shop->kind == PH_SHOP_ITEMS ?
item_options : magick_options;
@@ -886,13 +466,13 @@ ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world,
for (i = 0; i < option_count; ++i) {
snprintf(text, sizeof(text), "%c %s", ui->selected == i ? '>' : ' ',
options[i]);
- SDL_RenderDebugText(renderer, 32.0f, 56.0f + (float)i * 20.0f, text);
+ ph_render_text(renderer, 32.0f, 56.0f + (float)i * 20.0f, text);
}
- SDL_RenderDebugText(renderer, 32.0f, 132.0f,
+ ph_render_text(renderer, 32.0f, 132.0f,
shop->kind == PH_SHOP_ITEMS ? "Welcome, traveler." :
"Choose your lesson.");
} else if (ui->mode == PH_SHOP_BUY) {
- SDL_RenderDebugText(renderer, 24.0f, 42.0f,
+ ph_render_text(renderer, 24.0f, 42.0f,
shop->kind == PH_SHOP_ITEMS ? "BUY" : "MAGICK");
for (i = 0; i < shop->stock_count; ++i) {
if (shop->kind == PH_SHOP_SPELLS) {
@@ -913,12 +493,12 @@ ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world,
snprintf(text, sizeof(text), "%c %-18s %d G",
ui->selected == i ? '>' : ' ', item->name, item->buy_value);
}
- SDL_RenderDebugText(renderer, 24.0f, 62.0f + (float)i * 20.0f, text);
+ ph_render_text(renderer, 24.0f, 62.0f + (float)i * 20.0f, text);
}
} else if (ui->mode == PH_SHOP_SELL) {
int row = 0;
- SDL_RenderDebugText(renderer, 24.0f, 42.0f, "SELL");
+ ph_render_text(renderer, 24.0f, 42.0f, "SELL");
for (i = 0; i < world->content.item_count; ++i) {
const PhItemDef *item = &world->content.items[i];
@@ -926,9 +506,9 @@ ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world,
snprintf(text, sizeof(text), "%c %-16s x%d %d G",
ui->selected == i ? '>' : ' ', item->name, world->inventory[i],
item->sell_value);
- SDL_RenderDebugText(renderer, 24.0f, 62.0f + (float)row++ * 18.0f, text);
+ ph_render_text(renderer, 24.0f, 62.0f + (float)row++ * 18.0f, text);
}
- if (!row) SDL_RenderDebugText(renderer, 24.0f, 62.0f, "Nothing to sell.");
+ if (!row) ph_render_text(renderer, 24.0f, 62.0f, "Nothing to sell.");
} else {
float y = 48.0f;
float width = ph_ui_right(24.0f) - 24.0f;
@@ -938,44 +518,44 @@ ph_draw_shop(SDL_Renderer *renderer, const PhWorld *world,
shop->talk[i]);
y += 6.0f;
}
- SDL_RenderDebugText(renderer, 24.0f, y + 8.0f, "ENTER / ESC BACK");
+ ph_render_text(renderer, 24.0f, y + 8.0f, "ENTER / ESC BACK");
}
if (ui->status[0])
- SDL_RenderDebugText(renderer, 24.0f, ph_ui_bottom(36.0f), ui->status);
+ ph_render_text(renderer, 24.0f, ph_ui_bottom(36.0f), ui->status);
}
static void
-ph_draw_battle_entity(SDL_Renderer *renderer, const PhRenderAssets *assets,
- const PhWorld *world, int entity_index, SDL_FRect rect)
+ph_draw_battle_entity(PhRenderer *renderer,
+ const PhWorld *world, int entity_index, PhRect rect)
{
const PhEntity *entity = &world->entities[entity_index];
const PhEntityDef *def = ph_world_entity_def(world, entity->type_id);
- SDL_Texture *texture = NULL;
+ int asset_id = -1;
int x;
int y;
- SDL_FlipMode flip;
+ PhFlip flip;
if (!def) return;
- texture = ph_render_asset(assets, def->asset_id);
+ asset_id = def->asset_id;
if (def->kind == PH_ENTITY_PLAYER) {
x = def->sprite_tiles[PH_SPRITE_SIDE][0][0];
y = def->sprite_tiles[PH_SPRITE_SIDE][0][1];
- flip = SDL_FLIP_NONE;
+ flip = PH_FLIP_NONE;
} else {
ph_render_entity_frame(def, entity, &x, &y, &flip);
}
- if (texture) ph_render_sprite(renderer, texture, x, y, rect, flip);
+ if (ph_render_has_asset(renderer, asset_id)) ph_render_sprite(renderer, asset_id, x, y, rect, flip);
else {
if (def->kind == PH_ENTITY_MONSTER)
- SDL_SetRenderDrawColor(renderer, PH_MONSTER_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MONSTER_COLOR });
else
- SDL_SetRenderDrawColor(renderer, PH_NPC_COLOR);
- SDL_RenderFillRect(renderer, &rect);
+ ph_render_color(renderer, (PhColor){ PH_NPC_COLOR });
+ ph_render_fill(renderer, rect);
}
}
static void
-ph_draw_battle(SDL_Renderer *renderer, const PhRenderAssets *assets,
+ph_draw_battle(PhRenderer *renderer,
const PhWorld *world, const PhBattle *battle)
{
static const char *commands[PH_BATTLE_COMMAND_COUNT] = {
@@ -989,90 +569,90 @@ ph_draw_battle(SDL_Renderer *renderer, const PhRenderAssets *assets,
PhStats player_stats = ph_world_entity_stats(world, player);
PhStats enemy_stats = ph_world_entity_stats(world, enemy);
int order[PH_BATTLE_ORDER_COUNT];
- SDL_FRect ground = { 0.0f, PH_VIEW_H - 136.0f, PH_VIEW_W, 72.0f };
- SDL_FRect panel = { 8.0f, PH_VIEW_H - 64.0f, PH_VIEW_W - 16.0f, 56.0f };
- SDL_FRect spell_panel = { PH_VIEW_W * 0.5f - 88.0f,
+ PhRect ground = { 0.0f, PH_VIEW_H - 136.0f, PH_VIEW_W, 72.0f };
+ PhRect panel = { 8.0f, PH_VIEW_H - 64.0f, PH_VIEW_W - 16.0f, 56.0f };
+ PhRect spell_panel = { PH_VIEW_W * 0.5f - 88.0f,
PH_VIEW_H - 116.0f, 176.0f,
24.0f + (float)battle->spells.learned_count * 12.0f };
- SDL_FRect enemy_rect = { 56.0f + ph_battle_lunge(battle,
+ PhRect enemy_rect = { 56.0f + ph_battle_lunge(battle,
PH_BATTLE_ENEMY_ACTION), PH_VIEW_H - 172.0f,
PH_TILE_SIZE * 3.0f, PH_TILE_SIZE * 3.0f };
- SDL_FRect player_rect = { PH_VIEW_W - 88.0f - ph_battle_lunge(battle,
+ PhRect player_rect = { PH_VIEW_W - 88.0f - ph_battle_lunge(battle,
PH_BATTLE_PLAYER_ACTION), PH_VIEW_H - 120.0f,
PH_TILE_SIZE * 3.0f, PH_TILE_SIZE * 3.0f };
char text[96];
int i;
- SDL_SetRenderDrawColor(renderer, PH_BATTLE_BG_COLOR);
- SDL_RenderClear(renderer);
- SDL_SetRenderDrawColor(renderer, PH_BATTLE_GROUND_COLOR);
- SDL_RenderFillRect(renderer, &ground);
- ph_draw_battle_entity(renderer, assets, world, battle->enemy_index, enemy_rect);
- ph_draw_battle_entity(renderer, assets, world, world->player_index, player_rect);
+ ph_render_color(renderer, (PhColor){ PH_BATTLE_BG_COLOR });
+ ph_render_clear(renderer);
+ ph_render_color(renderer, (PhColor){ PH_BATTLE_GROUND_COLOR });
+ ph_render_fill(renderer, ground);
+ ph_draw_battle_entity(renderer, world, battle->enemy_index, enemy_rect);
+ ph_draw_battle_entity(renderer, world, world->player_index, player_rect);
if (battle->phase == PH_BATTLE_TARGET) {
- SDL_FRect target_rect = battle->choice.target_index == world->player_index ?
+ PhRect target_rect = battle->choice.target_index == world->player_index ?
player_rect : enemy_rect;
target_rect.x -= 3.0f;
target_rect.y -= 3.0f;
target_rect.w += 6.0f;
target_rect.h += 6.0f;
- SDL_SetRenderDrawColor(renderer, PH_MENU_BONUS_COLOR);
- SDL_RenderRect(renderer, &target_rect);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BONUS_COLOR });
+ ph_render_outline(renderer, target_rect);
}
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, 16.0f, 18.0f,
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, 16.0f, 18.0f,
enemy_def && enemy_def->name ? enemy_def->name : "ENEMY");
snprintf(text, sizeof(text), "HP %d/%d", enemy->vitals.hp, enemy_stats.max_hp);
- SDL_RenderDebugText(renderer, 16.0f, 30.0f, text);
+ ph_render_text(renderer, 16.0f, 30.0f, text);
if (enemy->accuracy.turns > 0) {
snprintf(text, sizeof(text), "ACC -%d%% (%d)", enemy->accuracy.value,
enemy->accuracy.turns);
- SDL_RenderDebugText(renderer, 16.0f, 42.0f, text);
+ ph_render_text(renderer, 16.0f, 42.0f, text);
}
ph_battle_order(battle, world, order);
- SDL_RenderDebugText(renderer, ph_ui_right(96.0f), 18.0f, "TURN ORDER");
+ ph_render_text(renderer, ph_ui_right(96.0f), 18.0f, "TURN ORDER");
for (i = 0; i < PH_BATTLE_ORDER_COUNT; ++i) {
snprintf(text, sizeof(text), "%d %s", i + 1,
order[i] == world->player_index ? "YOU" : "ENEMY");
- SDL_RenderDebugText(renderer, ph_ui_right(80.0f),
+ ph_render_text(renderer, ph_ui_right(80.0f),
30.0f + (float)i * 11.0f, text);
}
- SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
- SDL_RenderFillRect(renderer, &panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
- SDL_RenderRect(renderer, &panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_COLOR });
+ ph_render_fill(renderer, panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR });
+ ph_render_outline(renderer, panel);
for (i = 0; i < (int)LEN(commands); ++i) {
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
snprintf(text, sizeof(text), "%c %s",
(battle->phase == PH_BATTLE_COMMAND || battle->phase == PH_BATTLE_MAGIC ||
battle->phase == PH_BATTLE_TARGET) &&
battle->choice.command == (PhBattleCommand)i ? '>' : ' ',
commands[i]);
- SDL_RenderDebugText(renderer, 18.0f,
+ ph_render_text(renderer, 18.0f,
panel.y + 8.0f + (float)i * 14.0f, text);
}
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, 104.0f, panel.y + 8.0f, battle->message);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, 104.0f, panel.y + 8.0f, battle->message);
if (battle->phase == PH_BATTLE_TARGET) {
const PhEntityDef *target_def = ph_world_entity_def(world,
world->entities[battle->choice.target_index].type_id);
snprintf(text, sizeof(text), "TARGET: %s ENTER CONFIRM",
target_def && target_def->name ? target_def->name : "UNKNOWN");
- SDL_RenderDebugText(renderer, 104.0f, panel.y + 24.0f, text);
+ ph_render_text(renderer, 104.0f, panel.y + 24.0f, text);
}
snprintf(text, sizeof(text), "HP %d/%d", player->vitals.hp, player_stats.max_hp);
- SDL_RenderDebugText(renderer, ph_ui_right(80.0f), ph_ui_bottom(36.0f), text);
+ ph_render_text(renderer, ph_ui_right(80.0f), ph_ui_bottom(36.0f), text);
snprintf(text, sizeof(text), "MP %d/%d", player->vitals.mp, player_stats.max_mp);
- SDL_RenderDebugText(renderer, ph_ui_right(80.0f), ph_ui_bottom(24.0f), text);
+ ph_render_text(renderer, ph_ui_right(80.0f), ph_ui_bottom(24.0f), text);
if (battle->phase == PH_BATTLE_MAGIC) {
- SDL_SetRenderDrawColor(renderer, PH_MENU_COLOR);
- SDL_RenderFillRect(renderer, &spell_panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_BORDER_COLOR);
- SDL_RenderRect(renderer, &spell_panel);
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
- SDL_RenderDebugText(renderer, spell_panel.x + 8.0f,
+ ph_render_color(renderer, (PhColor){ PH_MENU_COLOR });
+ ph_render_fill(renderer, spell_panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_BORDER_COLOR });
+ ph_render_outline(renderer, spell_panel);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
+ ph_render_text(renderer, spell_panel.x + 8.0f,
spell_panel.y + 6.0f, "MAGICK");
for (i = 0; i < battle->spells.learned_count; ++i) {
const PhSpellDef *choice = ph_spell_def(battle,
@@ -1080,115 +660,113 @@ ph_draw_battle(SDL_Renderer *renderer, const PhRenderAssets *assets,
if (!choice) continue;
if (ph_spell_battle_usable(choice))
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
else
- SDL_SetRenderDrawColor(renderer, PH_MENU_DISABLED_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_MENU_DISABLED_COLOR });
snprintf(text, sizeof(text), "%c %s %dMP%s",
battle->choice.spell == i ? '>' : ' ', choice->name, choice->mp_cost,
choice->delay_percent < 100 ? " QUICK" : "");
- SDL_RenderDebugText(renderer, spell_panel.x + 8.0f,
+ ph_render_text(renderer, spell_panel.x + 8.0f,
spell_panel.y + 20.0f + (float)i * 12.0f, text);
}
}
}
static void
-ph_draw_battle_end(SDL_Renderer *renderer, const PhWorld *world,
+ph_draw_battle_end(PhRenderer *renderer, const PhWorld *world,
const PhBattle *battle, PhGameMode game)
{
const PhItemDef *loot = ph_world_item_def(world, battle->reward.item_id);
char text[96];
if (game == PH_GAME_VICTORY)
- SDL_SetRenderDrawColor(renderer, PH_BATTLE_BG_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_BATTLE_BG_COLOR });
else
- SDL_SetRenderDrawColor(renderer, PH_CLEAR_COLOR);
- SDL_RenderClear(renderer);
- SDL_SetRenderDrawColor(renderer, PH_MENU_TEXT_COLOR);
+ ph_render_color(renderer, (PhColor){ PH_CLEAR_COLOR });
+ ph_render_clear(renderer);
+ ph_render_color(renderer, (PhColor){ PH_MENU_TEXT_COLOR });
if (game == PH_GAME_OVER) {
- SDL_RenderDebugText(renderer, ph_ui_center_text("GAME OVER"),
+ ph_render_text(renderer, ph_ui_center_text("GAME OVER"),
PH_VIEW_H * 0.5f - 12.0f, "GAME OVER");
return;
}
- SDL_RenderDebugText(renderer, ph_ui_center_text("VICTORY!"),
+ ph_render_text(renderer, ph_ui_center_text("VICTORY!"),
PH_VIEW_H * 0.5f - 44.0f, "VICTORY!");
snprintf(text, sizeof(text), "XP GAINED %d", battle->reward.xp);
- SDL_RenderDebugText(renderer, ph_ui_center_text(text),
+ ph_render_text(renderer, ph_ui_center_text(text),
PH_VIEW_H * 0.5f - 14.0f, text);
if (loot && battle->reward.amount > 0)
snprintf(text, sizeof(text), "LOOT %s x%d", loot->name,
battle->reward.amount);
else
snprintf(text, sizeof(text), "LOOT NONE");
- SDL_RenderDebugText(renderer, ph_ui_center_text(text),
+ ph_render_text(renderer, ph_ui_center_text(text),
PH_VIEW_H * 0.5f + 4.0f, text);
- SDL_RenderDebugText(renderer, ph_ui_center_text("ENTER TO CONTINUE"),
+ ph_render_text(renderer, ph_ui_center_text("ENTER TO CONTINUE"),
PH_VIEW_H * 0.5f + 44.0f, "ENTER TO CONTINUE");
}
void
-ph_present_frame(SDL_Renderer *renderer, const PhRenderAssets *assets,
- const PhGameContext *context)
+ph_present_frame(PhRenderer *renderer,
+ const PhGameContext *context, const PhControlLabels *labels)
{
const PhWorld *world = context->world;
const PhBattle *battle = context->battle;
PhGameMode game = *context->mode;
if (game == PH_GAME_BATTLE) {
- ph_draw_battle(renderer, assets, world, battle);
+ ph_draw_battle(renderer, world, battle);
if (context->menu == PH_MENU_MAIN)
ph_draw_options(renderer, context->options);
- SDL_RenderPresent(renderer);
+ ph_render_present(renderer);
return;
}
if (game == PH_GAME_VICTORY || game == PH_GAME_OVER) {
ph_draw_battle_end(renderer, world, battle, game);
if (context->menu == PH_MENU_MAIN)
ph_draw_options(renderer, context->options);
- SDL_RenderPresent(renderer);
+ ph_render_present(renderer);
return;
}
- SDL_SetRenderDrawColor(renderer, PH_CLEAR_COLOR);
- SDL_RenderClear(renderer);
- ph_render_world(renderer, assets, world);
+ ph_render_color(renderer, (PhColor){ PH_CLEAR_COLOR });
+ ph_render_clear(renderer);
+ ph_render_world(renderer, world);
if (context->menu == PH_MENU_CHARACTER) {
- ph_draw_character_sheet(renderer, assets, world);
+ ph_draw_character_sheet(renderer, world, labels);
} else if (context->menu == PH_MENU_INVENTORY) {
- ph_draw_inventory(renderer, assets, world, context->inventory);
+ ph_draw_inventory(renderer, world, context->inventory, labels);
} else if (context->menu == PH_MENU_SPELLBOOK) {
- ph_draw_spellbook(renderer, world, context->spells, context->spellbook);
+ ph_draw_spellbook(renderer, world, context->spells, context->spellbook, labels);
} else if (context->menu == PH_MENU_SHOP) {
- ph_draw_shop(renderer, world, context->spells, context->shop);
+ ph_draw_shop(renderer, world, context->spells, context->shop, labels);
} else if (context->menu == PH_MENU_MAIN) {
ph_draw_options(renderer, context->options);
} else if (context->menu == PH_MENU_START) {
ph_draw_start(renderer, context->start);
} else if (world->notice.seconds > 0.0f && world->notice.text[0] != '\0') {
- SDL_FRect notice_bg = {
+ PhRect notice_bg = {
.x = 8.0f,
.y = (float)(PH_VIEW_H - 28),
.w = (float)(PH_VIEW_W - 16),
.h = 20.0f,
};
- SDL_SetRenderDrawColor(renderer, PH_NOTICE_COLOR);
- SDL_RenderFillRect(renderer, ¬ice_bg);
- SDL_SetRenderDrawColor(renderer, PH_NOTICE_TEXT_COLOR);
- SDL_RenderDebugText(renderer, 14.0f, (float)(PH_VIEW_H - 22),
+ ph_render_color(renderer, (PhColor){ PH_NOTICE_COLOR });
+ ph_render_fill(renderer, notice_bg);
+ ph_render_color(renderer, (PhColor){ PH_NOTICE_TEXT_COLOR });
+ ph_render_text(renderer, 14.0f, (float)(PH_VIEW_H - 22),
world->notice.text);
}
if (game == PH_GAME_TRANSITION) {
float progress = 1.0f - battle->timer / PH_BATTLE_TRANSITION_SECONDS;
float height = progress * PH_VIEW_H * 0.5f;
- SDL_FRect top = { 0.0f, 0.0f, PH_VIEW_W, height };
- SDL_FRect bottom = { 0.0f, PH_VIEW_H - height, PH_VIEW_W, height };
+ PhRect top = { 0.0f, 0.0f, PH_VIEW_W, height };
+ PhRect bottom = { 0.0f, PH_VIEW_H - height, PH_VIEW_W, height };
- SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
- SDL_RenderFillRect(renderer, &top);
- SDL_RenderFillRect(renderer, &bottom);
+ ph_render_color(renderer, (PhColor){ 0, 0, 0, 255 });
+ ph_render_fill(renderer, top);
+ ph_render_fill(renderer, bottom);
}
- SDL_RenderPresent(renderer);
+ ph_render_present(renderer);
}
-
-#endif
diff --git a/src/game/presentation.h b/src/game/presentation.h
@@ -1,144 +1,20 @@
#ifndef PH_GAME_PRESENTATION_H
#define PH_GAME_PRESENTATION_H
-#include "engine/battle.h"
+#include "game/game.h"
#include "engine/render.h"
-#include "game/content.h"
-
-#if PH_USE_SDL
-#include <SDL3/SDL.h>
-
-typedef enum {
- PH_MENU_NONE,
- PH_MENU_CHARACTER,
- PH_MENU_INVENTORY,
- PH_MENU_SPELLBOOK,
- PH_MENU_SHOP,
- PH_MENU_MAIN,
- PH_MENU_START,
-} PhMenu;
-
-typedef enum {
- PH_START_STAY,
- PH_START_CONTINUE,
- PH_START_NEW,
-} PhStartAction;
-
-typedef struct {
- int selected;
- int can_continue;
-} PhStartUi;
-
-typedef enum {
- PH_GAME_WORLD,
- PH_GAME_TRANSITION,
- PH_GAME_BATTLE,
- PH_GAME_VICTORY,
- PH_GAME_OVER,
-} PhGameMode;
-
-typedef enum {
- PH_INVENTORY_BROWSE,
- PH_INVENTORY_ACTIONS,
- PH_INVENTORY_EXAMINE,
-} PhInventoryMode;
-
-typedef enum {
- PH_INVENTORY_EQUIP,
- PH_INVENTORY_USE,
- PH_INVENTORY_DROP,
- PH_INVENTORY_EXAMINE_ACTION,
- PH_INVENTORY_ACTION_COUNT,
-} PhInventoryAction;
-
-typedef struct {
- int selected;
- int option;
- PhInventoryMode mode;
- char status[64];
-} PhInventoryUi;
-
-typedef enum {
- PH_SPELLBOOK_BROWSE,
- PH_SPELLBOOK_ACTIONS,
- PH_SPELLBOOK_EXAMINE,
-} PhSpellbookMode;
-
-typedef enum {
- PH_SPELLBOOK_USE,
- PH_SPELLBOOK_EXAMINE_ACTION,
- PH_SPELLBOOK_ACTION_COUNT,
-} PhSpellbookAction;
+/* Borrowed labels supplied by the input host for configured controls. */
typedef struct {
- int selected;
- int option;
- PhSpellbookMode mode;
- int spell_used;
- char status[64];
-} PhSpellbookUi;
-
-typedef enum {
- PH_SHOP_ROOT,
- PH_SHOP_BUY,
- PH_SHOP_SELL,
- PH_SHOP_TALK,
-} PhShopMode;
-
-typedef struct {
- int shop_id;
- int selected;
- PhShopMode mode;
- char status[64];
-} PhShopUi;
-
-typedef enum {
- PH_OPTIONS_ROOT,
- PH_OPTIONS_SOUND,
-} PhOptionsMode;
-
-typedef enum {
- PH_OPTIONS_STAY,
- PH_OPTIONS_CLOSE,
- PH_OPTIONS_EXIT,
-} PhOptionsAction;
-
-typedef struct {
- int selected;
- int music_enabled;
- int effects_enabled;
- PhOptionsMode mode;
-} PhOptionsUi;
-
-typedef struct {
- PhWorld *world;
- PhInventoryUi *inventory;
- PhSpellbookUi *spellbook;
- PhShopUi *shop;
- PhOptionsUi *options;
- PhStartUi *start;
- PhGameMode *mode;
- PhBattle *battle;
- PhSpellBook *spells;
- PhMenu menu;
-} PhGameContext;
-
-int ph_inventory_step(const PhWorld *world, int selected, int direction);
-void ph_inventory_open(PhInventoryUi *ui, const PhWorld *world);
-int ph_inventory_key(PhInventoryUi *ui, PhWorld *world, SDL_Scancode key);
-void ph_spellbook_open(PhSpellbookUi *ui, const PhSpellBook *spells);
-int ph_spellbook_key(PhSpellbookUi *ui, PhWorld *world, PhSpellBook *spells,
- SDL_Scancode key);
-void ph_shop_open(PhShopUi *ui, int shop_id);
-PhMenu ph_interaction_open(PhShopUi *shop_ui, int interaction_id);
-void ph_options_open(PhOptionsUi *ui);
-PhOptionsAction ph_options_key(PhOptionsUi *ui, SDL_Scancode key);
-PhStartAction ph_start_key(PhStartUi *ui, SDL_Scancode key);
-int ph_shop_key(PhShopUi *ui, PhWorld *world, PhSpellBook *spells,
- SDL_Scancode key);
-void ph_present_frame(SDL_Renderer *renderer, const PhRenderAssets *assets,
- const PhGameContext *context);
-
-#endif
+ const char *accept;
+ const char *back;
+ const char *cancel;
+ const char *character;
+ const char *inventory;
+ const char *spellbook;
+} PhControlLabels;
+
+void ph_present_frame(PhRenderer *renderer, const PhGameContext *context,
+ const PhControlLabels *labels);
#endif
diff --git a/src/tests/game.c b/src/tests/game.c
@@ -0,0 +1,221 @@
+#define _POSIX_C_SOURCE 200809L
+#include "game/game.h"
+#include "engine/save.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#define CHECK(test) do { if (!(test)) { \
+ fprintf(stderr, "game smoke failed at line %d: %s\n", __LINE__, #test); \
+ goto fail; } } while (0)
+
+static int
+ph_timing_smoke(void)
+{
+ static const PhTileDef tiles[] = { { .symbol = '.' } };
+ static const PhEntityDef actors[] = {
+ { .id = 1, .kind = PH_ENTITY_PLAYER, .move_speed = 1, .stats = { .max_hp = 1 } },
+ { .id = 2, .kind = PH_ENTITY_NPC, .move_speed = 1, .stats = { .max_hp = 1 } },
+ };
+ unsigned char map[20 * 8] = { 0 };
+ PhArea area = { .width = 20, .height = 8, .tiles = map,
+ .tile_defs = tiles, .tile_def_count = 1 };
+ PhWorldCatalog catalog = { .entities = actors, .entity_count = 2 };
+ PhWorld reference = { 0 }, world = { 0 };
+ PhClock clock = { 0 };
+ const int frames[] = { 60, 120, 288, 200 };
+ int run, frame, ticks, total = 0, moves, reference_moves = 0;
+
+ /* Fractional ticks survive short frames; huge stalls cannot overflow or
+ * leave a backlog to execute on subsequent frames. */
+ for (frame = 0; frame < 1000; ++frame)
+ total += ph_clock_steps(&clock, 1000000, 60, 4);
+ if (total != 60 || clock.credit != 0 ||
+ ph_clock_steps(&clock, UINT64_MAX, 60, 4) != 4 || clock.credit != 0 ||
+ ph_clock_steps(&clock, 0, 60, 4) != 0) return 1;
+
+ for (run = 0; run < 4; ++run) {
+ PhGameMode mode = PH_GAME_WORLD;
+ PhGameContext context = { .world = &world, .mode = &mode };
+ uint64_t previous = 0;
+
+ if (ph_world_init(&world, area, 1, catalog, 320, 128) < 0) return 1;
+ if (ph_world_spawn_entity(&world, 1, (PhVec2){ 24, 24 }, 0, 1) < 0 ||
+ ph_world_spawn_entity(&world, 2, (PhVec2){ 200, 88 }, 4, 1) < 0) return 1;
+ ph_world_set_player(&world, 0);
+ total = moves = 0;
+ for (frame = 1; frame <= frames[run]; ++frame) {
+ uint64_t now = (uint64_t)frame * 2000000000 / frames[run];
+
+ if (run == 3 && frame % 2) now -= 7000000;
+
+ ticks = ph_game_steps(&context, now - previous);
+ previous = now;
+ total += ticks;
+ while (ticks-- > 0) {
+ PhWorldEvent event;
+
+ ph_world_tick(&world, (PhInput){ .move_x = 1 }, 1.0f / PH_SIMULATION_HZ);
+ while (ph_world_take_event(&world, &event))
+ if (event.kind == PH_WORLD_EVENT_MOVE) ++moves;
+ }
+ }
+ if (total != 2 * PH_SIMULATION_HZ || context.clock.credit != 0 || moves == 0)
+ return 1;
+ if (!run) { reference = world; reference_moves = moves; }
+ else if (memcmp(&reference, &world, sizeof(world)) || moves != reference_moves)
+ return 1;
+ /* No owned area buffers or captured areas exist in this fixture. */
+ ph_world_destroy(&world);
+ }
+ return 0;
+}
+
+int
+main(void)
+{
+ PhWorld world = { 0 }, loaded = { 0 };
+ PhBattle battle = { 0 };
+ PhSpellBook spells = ph_player_spellbook, loaded_spells = ph_player_spellbook;
+ PhGameMode mode = PH_GAME_WORLD;
+ PhInventoryUi inventory = { 0 };
+ PhSpellbookUi spellbook = { 0 };
+ PhShopUi shop = { 0 };
+ PhOptionsUi options = { .music_enabled = 1, .effects_enabled = 1 };
+ PhStartUi start = { .can_continue = 1 };
+ PhGameContext game = { .world = &world, .battle = &battle, .mode = &mode,
+ .spells = &spells, .inventory = &inventory, .spellbook = &spellbook,
+ .shop = &shop, .options = &options, .start = &start, .menu = PH_MENU_START };
+ char directory[] = "/tmp/ph-game-XXXXXX";
+ char save_path[128], temp_path[132];
+ PhEntity *player;
+ PhVec2 position;
+ int i, enemy = -1, potion = -1, result = 1;
+ float timer;
+
+ if (ph_timing_smoke()) {
+ fputs("fixed-timestep smoke failed\n", stderr);
+ return 1;
+ }
+ if (!mkdtemp(directory)) return 1;
+ snprintf(save_path, sizeof(save_path), "%s/checkpoint.sav", directory);
+ snprintf(temp_path, sizeof(temp_path), "%s.tmp", save_path);
+ CHECK(ph_world_start(&world, &ph_game_world, PH_VIEW_W, PH_VIEW_H) == 0);
+ player = &world.entities[world.player_index];
+ position = player->pos;
+ ph_game_update(&game, (PhInput){ .move_x = 1 }, 0.05f, save_path);
+ CHECK(player->pos.x == position.x && player->pos.y == position.y);
+ CHECK(ph_game_input(&game, PH_ACTION_ACCEPT) == 0 && game.menu == PH_MENU_NONE);
+ CHECK(ph_game_steps(&game, UINT64_MAX) == 0);
+ CHECK(ph_game_steps(&game, 1000000) == 0 && game.clock.credit > 0);
+
+ /* Backspace stays at the root; Escape opens Options and pauses simulation. */
+ ph_game_input(&game, PH_ACTION_BACK);
+ CHECK(game.menu == PH_MENU_NONE);
+ ph_game_input(&game, PH_ACTION_CANCEL);
+ CHECK(game.menu == PH_MENU_MAIN);
+ CHECK(ph_game_steps(&game, UINT64_MAX) == 0 && game.clock.credit == 0);
+ ph_game_update(&game, (PhInput){ .move_x = 1 }, 0.05f, save_path);
+ CHECK(player->pos.x == position.x && player->pos.y == position.y);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(!options.music_enabled && options.effects_enabled);
+ CHECK(ph_game_input(&game, PH_ACTION_DOWN) & PH_GAME_MENU_SOUND);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(!options.music_enabled && !options.effects_enabled);
+ ph_game_input(&game, PH_ACTION_BACK);
+ CHECK(game.menu == PH_MENU_MAIN && options.mode == PH_OPTIONS_ROOT);
+ ph_game_input(&game, PH_ACTION_CANCEL);
+ CHECK(game.menu == PH_MENU_NONE);
+ CHECK(ph_game_steps(&game, UINT64_MAX) == 0 && game.clock.credit == 0);
+ CHECK(ph_game_steps(&game, 1000000000 / PH_SIMULATION_HZ + 1) == 1);
+
+ /* Use an item through the same actions as the SDL host. */
+ for (i = 0; i < world.content.item_count; ++i)
+ if (world.content.items[i].id == PH_ITEM_HEALING_POTION) potion = i;
+ CHECK(potion >= 0);
+ memset(world.inventory, 0, sizeof(world.inventory));
+ world.inventory[potion] = 2;
+ player->vitals.hp = 1;
+ ph_game_input(&game, PH_ACTION_INVENTORY);
+ CHECK(game.menu == PH_MENU_INVENTORY && inventory.selected == potion);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(inventory.mode == PH_INVENTORY_ACTIONS && inventory.option == PH_INVENTORY_USE);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(player->vitals.hp > 1 && world.inventory[potion] == 1);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ ph_game_input(&game, PH_ACTION_CANCEL);
+ CHECK(inventory.mode == PH_INVENTORY_BROWSE && game.menu == PH_MENU_INVENTORY);
+ ph_game_input(&game, PH_ACTION_BACK);
+ CHECK(game.menu == PH_MENU_NONE);
+
+ ph_game_input(&game, PH_ACTION_SPELLBOOK);
+ CHECK(game.menu == PH_MENU_SPELLBOOK);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(spellbook.mode == PH_SPELLBOOK_ACTIONS);
+ ph_game_input(&game, PH_ACTION_BACK);
+ ph_game_input(&game, PH_ACTION_BACK);
+ CHECK(game.menu == PH_MENU_NONE);
+ world.interaction_id = PH_INTERACTION_MIRA;
+ ph_game_interaction(&game);
+ CHECK(game.menu == PH_MENU_SHOP && world.interaction_id == 0);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(shop.mode == PH_SHOP_BUY);
+ ph_game_input(&game, PH_ACTION_BACK);
+ ph_game_input(&game, PH_ACTION_BACK);
+ CHECK(game.menu == PH_MENU_NONE);
+
+ /* Contact enters combat and saves a loadable pre-combat checkpoint. */
+ for (i = 0; i < world.entity_count; ++i)
+ if (world.entities[i].type_id == PH_DEF_SLIME) { enemy = i; break; }
+ CHECK(enemy >= 0);
+ world.encounter_index = enemy;
+ ph_game_update(&game, (PhInput){ 0 }, 0.0f, save_path);
+ CHECK(mode == PH_GAME_TRANSITION && world.combat_active);
+ CHECK(ph_save_load(save_path, &loaded, &ph_game_world, &loaded_spells,
+ PH_VIEW_W, PH_VIEW_H) == 1);
+ CHECK(!loaded.combat_active);
+ ph_game_update(&game, (PhInput){ 0 }, PH_BATTLE_TRANSITION_SECONDS, save_path);
+ CHECK(mode == PH_GAME_BATTLE);
+ ph_game_input(&game, PH_ACTION_BACK);
+ CHECK(game.menu == PH_MENU_NONE);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(battle.phase == PH_BATTLE_TARGET);
+ ph_game_input(&game, PH_ACTION_CANCEL);
+ CHECK(battle.phase == PH_BATTLE_COMMAND && game.menu == PH_MENU_NONE);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(battle.phase == PH_BATTLE_PLAYER_ACTION);
+ timer = battle.timer;
+ ph_game_input(&game, PH_ACTION_CANCEL);
+ ph_game_update(&game, (PhInput){ 0 }, 1.0f, save_path);
+ CHECK(game.menu == PH_MENU_MAIN && battle.timer == timer);
+ ph_game_input(&game, PH_ACTION_BACK);
+ /* Complete a resolved lethal action, avoiding random damage in this flow test. */
+ world.entities[enemy].vitals.hp = 0;
+ battle.resolved = 1;
+ CHECK(ph_game_update(&game, (PhInput){ 0 }, PH_BATTLE_ACTION_SECONDS, save_path));
+ CHECK(mode == PH_GAME_VICTORY && !world.combat_active);
+ ph_game_input(&game, PH_ACTION_ACCEPT);
+ CHECK(mode == PH_GAME_WORLD && game.menu == PH_MENU_NONE);
+
+ ph_game_input(&game, PH_ACTION_CANCEL);
+ ph_game_input(&game, PH_ACTION_DOWN);
+ CHECK(ph_game_input(&game, PH_ACTION_ACCEPT) & PH_GAME_REQUEST_EXIT);
+ game.menu = PH_MENU_START;
+ start.can_continue = 0;
+ CHECK(ph_game_input(&game, PH_ACTION_ACCEPT) & PH_GAME_REQUEST_NEW);
+ puts("game smoke: fixed timing, actions, menus, pause, checkpoint and battle flow passed");
+ result = 0;
+fail:
+ ph_world_destroy(&world);
+ ph_world_destroy(&loaded);
+ remove(save_path);
+ remove(temp_path);
+ rmdir(directory);
+ return result;
+}
diff --git a/src/tests/render.c b/src/tests/render.c
@@ -1,15 +1,100 @@
+#define _POSIX_C_SOURCE 200809L
#include "engine/battle.h"
#include "engine/area.h"
#include "engine/audio.h"
+#include "engine/sdl.h"
#include "game/content.h"
#include "game/presentation.h"
#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
#include "config.h"
#if PH_USE_SDL
static int
+ph_pixel_is(SDL_Surface *surface, int x, int y, PhColor color)
+{
+ Uint8 r, g, b, a;
+
+ return SDL_ReadSurfacePixel(surface, x, y, &r, &g, &b, &a) &&
+ r == color.r && g == color.g && b == color.b && a == color.a;
+}
+
+static int
+ph_media_smoke(SDL_Renderer *target, SDL_Surface *surface)
+{
+ char directory[] = "/tmp/ph-media-XXXXXX";
+ char path[128], missing[128];
+ SDL_Surface *source = NULL;
+ PhRenderer *renderer = NULL;
+ PhAudio *audio = NULL;
+ PhAssetDef defs[2];
+ PhAudioDef sounds[3] = { { 0 }, ph_audio_defs[1], { 0 } };
+ PhColor background = { 1, 2, 3, 255 };
+ PhColor red = { 255, 0, 0, 255 }, green = { 0, 255, 0, 255 };
+ PhRect src = { 0, 0, 3, 1 };
+ int music = 1, effects = 1, result = 1;
+
+ if (!mkdtemp(directory)) return 1;
+ snprintf(path, sizeof(path), "%s/sprite.bmp", directory);
+ snprintf(missing, sizeof(missing), "%s/missing", directory);
+ defs[0] = (PhAssetDef){ .path = path, .key = { 255, 0, 255 } };
+ defs[1] = (PhAssetDef){ .path = missing };
+ sounds[2].path = missing;
+ source = SDL_CreateSurface(3, 1, SDL_PIXELFORMAT_RGBA32);
+ if (!source || !SDL_WriteSurfacePixel(source, 0, 0, 255, 0, 0, 255) ||
+ !SDL_WriteSurfacePixel(source, 1, 0, 0, 255, 0, 255) ||
+ !SDL_WriteSurfacePixel(source, 2, 0, 255, 0, 255, 255) ||
+ !SDL_SaveBMP(source, path)) goto done;
+ /* Fail after loading one asset, then reuse the same borrowed SDL target. */
+ renderer = ph_render_sdl_create(target, defs, 2);
+ if (renderer) goto done;
+ audio = ph_audio_sdl_create(sounds, 3);
+ if (audio) goto done;
+ renderer = ph_render_sdl_create(target, defs, 1);
+ if (!renderer || !ph_render_has_asset(renderer, 0) ||
+ ph_render_has_asset(renderer, -1) || ph_render_has_asset(renderer, 1)) goto done;
+ ph_render_color(renderer, background);
+ ph_render_clear(renderer);
+ ph_render_texture(renderer, 0, src, (PhRect){ 2, 2, 3, 1 }, PH_FLIP_NONE);
+ ph_render_texture(renderer, 0, src, (PhRect){ 2, 3, 3, 1 }, PH_FLIP_HORIZONTAL);
+ ph_render_texture(renderer, -1, src, (PhRect){ 2, 2, 3, 1 }, PH_FLIP_NONE);
+ ph_render_texture(renderer, 1, src, (PhRect){ 2, 2, 3, 1 }, PH_FLIP_NONE);
+ ph_render_color(renderer, green);
+ ph_render_fill(renderer, (PhRect){ 10, 10, 4, 4 });
+ ph_render_color(renderer, red);
+ ph_render_outline(renderer, (PhRect){ 10, 10, 4, 4 });
+ ph_render_present(renderer);
+ if (!ph_pixel_is(surface, 2, 2, red) || !ph_pixel_is(surface, 3, 2, green) ||
+ !ph_pixel_is(surface, 4, 2, background) ||
+ !ph_pixel_is(surface, 2, 3, background) ||
+ !ph_pixel_is(surface, 3, 3, green) || !ph_pixel_is(surface, 4, 3, red) ||
+ !ph_pixel_is(surface, 10, 10, red) || !ph_pixel_is(surface, 11, 11, green) ||
+ !ph_pixel_is(surface, 14, 14, background)) goto done;
+ ph_render_destroy(renderer);
+ renderer = ph_render_sdl_create(target, NULL, 0);
+ if (!renderer || ph_render_has_asset(renderer, 0)) goto done;
+ /* Optional audio must be safe when creation fails or audio is disabled. */
+ ph_audio_music(NULL, 1);
+ ph_audio_effect(NULL, 1, 1.0f);
+ ph_audio_set_enabled(NULL, 1, 1);
+ ph_audio_update(NULL);
+ ph_audio_get_enabled(NULL, &music, &effects);
+ if (music || effects) goto done;
+ result = 0;
+done:
+ if (result) fprintf(stderr, "media smoke failed: pixels or resource lifecycle: %s\n", SDL_GetError());
+ ph_render_destroy(renderer);
+ ph_audio_destroy(audio);
+ SDL_DestroySurface(source);
+ remove(path);
+ rmdir(directory);
+ return result;
+}
+
+static int
ph_render_enter_portal(PhWorld *world, int destination_area_id)
{
int i;
@@ -32,9 +117,17 @@ int
main(void)
{
SDL_Surface *surface;
- SDL_Renderer *renderer;
- PhRenderAssets assets;
- PhAudio audio = { 0 };
+ SDL_Renderer *target;
+ PhRenderer *renderer;
+ PhAudio *audio = NULL;
+ PhControlLabels labels = {
+ .accept = SDL_GetScancodeName(PH_KEY_MENU_ACCEPT),
+ .back = SDL_GetScancodeName(PH_KEY_MENU_BACK),
+ .cancel = SDL_GetScancodeName(PH_KEY_MENU_CANCEL),
+ .character = SDL_GetScancodeName(PH_KEY_CHARACTER),
+ .inventory = SDL_GetScancodeName(PH_KEY_INVENTORY),
+ .spellbook = SDL_GetScancodeName(PH_KEY_SPELLBOOK),
+ };
PhWorld world;
PhInventoryUi inventory_ui;
PhSpellbookUi spellbook_ui;
@@ -55,6 +148,7 @@ main(void)
.battle = &battle,
.spells = &spells,
};
+ int music_enabled, effects_enabled;
int i;
int enemy_index = -1;
int result = 0;
@@ -75,17 +169,18 @@ main(void)
return 1;
}
- renderer = SDL_CreateSoftwareRenderer(surface);
- if (!renderer) {
+ target = SDL_CreateSoftwareRenderer(surface);
+ if (!target) {
fprintf(stderr, "SDL_CreateSoftwareRenderer failed: %s\n", SDL_GetError());
SDL_DestroySurface(surface);
SDL_Quit();
return 1;
}
+ result = ph_media_smoke(target, surface);
if (ph_world_start(&world, &ph_game_world,
(float)PH_VIEW_W, (float)PH_VIEW_H) < 0) {
- SDL_DestroyRenderer(renderer);
+ SDL_DestroyRenderer(target);
SDL_DestroySurface(surface);
SDL_Quit();
return 1;
@@ -93,7 +188,7 @@ main(void)
if (world.ground_item_count < 2) {
fprintf(stderr, "render smoke test failed: item setup\n");
ph_world_destroy(&world);
- SDL_DestroyRenderer(renderer);
+ SDL_DestroyRenderer(target);
SDL_DestroySurface(surface);
SDL_Quit();
return 1;
@@ -101,51 +196,51 @@ main(void)
first_item = world.ground_items[0].item_id;
second_item = world.ground_items[1].item_id;
- if (ph_render_assets_load(&assets, renderer,
- ph_asset_defs, PH_ASSET_COUNT) < 0) {
+ if (!(renderer = ph_render_sdl_create(target, ph_asset_defs, PH_ASSET_COUNT))) {
ph_world_destroy(&world);
- SDL_DestroyRenderer(renderer);
+ SDL_DestroyRenderer(target);
SDL_DestroySurface(surface);
SDL_Quit();
return 1;
}
- if (ph_start_key(&start_ui, PH_KEY_MENU_ACCEPT) != PH_START_NEW) {
+ if (ph_start_input(&start_ui, PH_ACTION_ACCEPT) != PH_START_NEW) {
fprintf(stderr, "render smoke test failed: new-game selection\n");
result = 1;
}
start_ui.can_continue = 1;
start_ui.selected = 0;
- if (ph_start_key(&start_ui, PH_KEY_MENU_ACCEPT) != PH_START_CONTINUE) {
+ if (ph_start_input(&start_ui, PH_ACTION_ACCEPT) != PH_START_CONTINUE) {
fprintf(stderr, "render smoke test failed: continue selection\n");
result = 1;
}
context.menu = PH_MENU_START;
- ph_present_frame(renderer, &assets, &context);
- if (ph_audio_init(&audio, ph_audio_defs, PH_AUDIO_COUNT) < 0) {
+ ph_present_frame(renderer, &context, &labels);
+ if (!(audio = ph_audio_sdl_create(ph_audio_defs, PH_AUDIO_COUNT))) {
fprintf(stderr, "audio smoke test failed: %s\n", SDL_GetError());
- ph_render_assets_destroy(&assets);
+ ph_render_destroy(renderer);
ph_world_destroy(&world);
- SDL_DestroyRenderer(renderer);
+ SDL_DestroyRenderer(target);
SDL_DestroySurface(surface);
SDL_Quit();
return 1;
}
- ph_audio_music(&audio, PH_MUSIC_MEADOW);
- ph_audio_update(&audio);
- ph_audio_effect(&audio, PH_SOUND_ATTACK, 1.0f);
- ph_audio_effect(&audio, PH_SOUND_HIT, 1.0f);
- ph_audio_effect(&audio, PH_SOUND_SPELL, 1.0f);
- ph_audio_effect(&audio, PH_SOUND_MENU, 0.5f);
- ph_audio_set_enabled(&audio, 0, 0);
- if (audio.music_enabled || audio.effects_enabled ||
+ ph_audio_music(audio, PH_MUSIC_MEADOW);
+ ph_audio_update(audio);
+ ph_audio_effect(audio, PH_SOUND_ATTACK, 1.0f);
+ ph_audio_effect(audio, PH_SOUND_HIT, 1.0f);
+ ph_audio_effect(audio, PH_SOUND_SPELL, 1.0f);
+ ph_audio_effect(audio, PH_SOUND_MENU, 0.5f);
+ ph_audio_set_enabled(audio, 0, 0);
+ ph_audio_get_enabled(audio, &music_enabled, &effects_enabled);
+ if (music_enabled || effects_enabled ||
ph_audio_distance_gain(0.0f, 0.0f, 160.0f, 0.0f, 160.0f) != 0.0f ||
ph_audio_distance_gain(0.0f, 0.0f, 80.0f, 0.0f, 160.0f) != 0.5f) {
fprintf(stderr, "audio smoke test failed: controls or distance\n");
result = 1;
}
- ph_audio_set_enabled(&audio, 1, 1);
- ph_audio_music(&audio, PH_MUSIC_TOWN);
- ph_audio_music(&audio, PH_MUSIC_BATTLE);
+ ph_audio_set_enabled(audio, 1, 1);
+ ph_audio_music(audio, PH_MUSIC_TOWN);
+ ph_audio_music(audio, PH_MUSIC_BATTLE);
ph_world_tick(&world, (PhInput){ .interact = 1 }, 0.0f);
if (ph_world_item_amount(&world, first_item) != 0) {
@@ -165,57 +260,57 @@ main(void)
}
ph_inventory_open(&inventory_ui, &world);
ph_options_open(&options_ui);
- if (ph_options_key(&options_ui, PH_KEY_MENU_DOWN_1) != PH_OPTIONS_STAY ||
+ if (ph_options_input(&options_ui, PH_ACTION_DOWN) != PH_OPTIONS_STAY ||
options_ui.selected != 1 ||
- ph_options_key(&options_ui, PH_KEY_MENU_UP_1) != PH_OPTIONS_STAY ||
+ ph_options_input(&options_ui, PH_ACTION_UP) != PH_OPTIONS_STAY ||
options_ui.selected != 0 ||
- ph_options_key(&options_ui, PH_KEY_MENU_ACCEPT) != PH_OPTIONS_STAY ||
+ ph_options_input(&options_ui, PH_ACTION_ACCEPT) != PH_OPTIONS_STAY ||
options_ui.mode != PH_OPTIONS_SOUND) {
fprintf(stderr, "render smoke test failed: options navigation\n");
result = 1;
}
- ph_options_key(&options_ui, PH_KEY_MENU_ACCEPT);
- ph_options_key(&options_ui, PH_KEY_MENU_DOWN_1);
- ph_options_key(&options_ui, PH_KEY_MENU_ACCEPT);
+ ph_options_input(&options_ui, PH_ACTION_ACCEPT);
+ ph_options_input(&options_ui, PH_ACTION_DOWN);
+ ph_options_input(&options_ui, PH_ACTION_ACCEPT);
if (options_ui.music_enabled || options_ui.effects_enabled) {
fprintf(stderr, "render smoke test failed: sound toggles\n");
result = 1;
}
context.menu = PH_MENU_MAIN;
- ph_present_frame(renderer, &assets, &context);
- ph_options_key(&options_ui, PH_KEY_MENU_BACK);
- if (ph_options_key(&options_ui, PH_KEY_MENU_BACK) != PH_OPTIONS_CLOSE) {
+ ph_present_frame(renderer, &context, &labels);
+ ph_options_input(&options_ui, PH_ACTION_BACK);
+ if (ph_options_input(&options_ui, PH_ACTION_BACK) != PH_OPTIONS_CLOSE) {
fprintf(stderr, "render smoke test failed: options cancel\n");
result = 1;
}
ph_options_open(&options_ui);
- ph_options_key(&options_ui, PH_KEY_MENU_DOWN_1);
- if (ph_options_key(&options_ui, PH_KEY_MENU_ACCEPT) != PH_OPTIONS_EXIT) {
+ ph_options_input(&options_ui, PH_ACTION_DOWN);
+ if (ph_options_input(&options_ui, PH_ACTION_ACCEPT) != PH_OPTIONS_EXIT) {
fprintf(stderr, "render smoke test failed: exit option\n");
result = 1;
}
options_ui.music_enabled = 1;
options_ui.effects_enabled = 1;
context.menu = PH_MENU_NONE;
- ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_ACCEPT);
+ ph_inventory_input(&inventory_ui, &world, PH_ACTION_ACCEPT);
if (inventory_ui.option != PH_INVENTORY_EQUIP) result = 1;
- ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1);
+ ph_inventory_input(&inventory_ui, &world, PH_ACTION_DOWN);
if (inventory_ui.option != PH_INVENTORY_DROP) result = 1;
inventory_ui.mode = PH_INVENTORY_BROWSE;
- ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1);
- ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_ACCEPT);
+ ph_inventory_input(&inventory_ui, &world, PH_ACTION_DOWN);
+ ph_inventory_input(&inventory_ui, &world, PH_ACTION_ACCEPT);
if (inventory_ui.option != PH_INVENTORY_USE) result = 1;
- ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_DOWN_1);
+ ph_inventory_input(&inventory_ui, &world, PH_ACTION_DOWN);
if (inventory_ui.option != PH_INVENTORY_DROP) {
fprintf(stderr, "render smoke test failed: inventory actions\n");
result = 1;
}
inventory_ui.mode = PH_INVENTORY_EXAMINE;
- if (ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_BACK) ||
+ if (ph_inventory_input(&inventory_ui, &world, PH_ACTION_BACK) ||
inventory_ui.mode != PH_INVENTORY_ACTIONS ||
- ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_BACK) ||
+ ph_inventory_input(&inventory_ui, &world, PH_ACTION_BACK) ||
inventory_ui.mode != PH_INVENTORY_BROWSE ||
- !ph_inventory_key(&inventory_ui, &world, PH_KEY_MENU_BACK)) {
+ !ph_inventory_input(&inventory_ui, &world, PH_ACTION_BACK)) {
fprintf(stderr, "render smoke test failed: inventory cancel\n");
result = 1;
}
@@ -225,20 +320,20 @@ main(void)
i == 9 ? PH_INVENTORY_ACTIONS : PH_INVENTORY_BROWSE;
context.menu = i == 7 ? PH_MENU_CHARACTER :
i >= 6 ? PH_MENU_INVENTORY : PH_MENU_NONE;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
}
ph_spellbook_open(&spellbook_ui, &spells);
- ph_spellbook_key(&spellbook_ui, &world, &spells, PH_KEY_MENU_ACCEPT);
+ ph_spellbook_input(&spellbook_ui, &world, &spells, PH_ACTION_ACCEPT);
if (spellbook_ui.option != PH_SPELLBOOK_EXAMINE_ACTION) {
fprintf(stderr, "render smoke test failed: disabled field spell\n");
result = 1;
}
spellbook_ui.mode = PH_SPELLBOOK_BROWSE;
spellbook_ui.selected = 1;
- ph_spellbook_key(&spellbook_ui, &world, &spells, PH_KEY_MENU_ACCEPT);
+ ph_spellbook_input(&spellbook_ui, &world, &spells, PH_ACTION_ACCEPT);
if (spellbook_ui.option != PH_SPELLBOOK_USE) result = 1;
world.entities[world.player_index].vitals.hp -= 10;
- ph_spellbook_key(&spellbook_ui, &world, &spells, PH_KEY_MENU_ACCEPT);
+ ph_spellbook_input(&spellbook_ui, &world, &spells, PH_ACTION_ACCEPT);
if (ph_world_player(&world)->vitals.hp !=
ph_world_entity_stats(&world, ph_world_player(&world)).max_hp ||
ph_world_player(&world)->vitals.mp != 40) {
@@ -248,34 +343,34 @@ main(void)
world.entities[world.player_index].vitals.mp =
ph_world_entity_stats(&world, ph_world_player(&world)).max_mp;
context.menu = PH_MENU_SPELLBOOK;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
spellbook_ui.mode = PH_SPELLBOOK_ACTIONS;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
spellbook_ui.mode = PH_SPELLBOOK_EXAMINE;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
ph_shop_open(&shop_ui, ph_shop_defs[0].id);
for (i = PH_SHOP_ROOT; i <= PH_SHOP_TALK; ++i) {
shop_ui.mode = (PhShopMode)i;
shop_ui.selected = i == PH_SHOP_SELL ?
ph_inventory_step(&world, -1, 1) : 0;
context.menu = PH_MENU_SHOP;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
}
ph_shop_open(&shop_ui, PH_SHOP_MAGICK);
shop_ui.mode = PH_SHOP_ROOT;
- ph_present_frame(renderer, &assets, &context);
- ph_shop_key(&shop_ui, &world, &spells, PH_KEY_MENU_ACCEPT);
+ ph_present_frame(renderer, &context, &labels);
+ ph_shop_input(&shop_ui, &world, &spells, PH_ACTION_ACCEPT);
if (shop_ui.mode != PH_SHOP_BUY) result = 1;
- ph_present_frame(renderer, &assets, &context);
- ph_shop_key(&shop_ui, &world, &spells, PH_KEY_MENU_ACCEPT);
+ ph_present_frame(renderer, &context, &labels);
+ ph_shop_input(&shop_ui, &world, &spells, PH_ACTION_ACCEPT);
if (!ph_spellbook_has(&spells, PH_SPELL_EMBER) ||
ph_world_player(&world)->gold != 16) {
fprintf(stderr, "render smoke test failed: magick purchase\n");
result = 1;
}
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
shop_ui.mode = PH_SHOP_TALK;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
for (i = 0; i < world.entity_count; ++i) {
const PhEntityDef *def = ph_world_entity_def(&world, world.entities[i].type_id);
if (def && def->kind == PH_ENTITY_MONSTER) enemy_index = i;
@@ -289,15 +384,15 @@ main(void)
context.menu = PH_MENU_NONE;
ph_battle_begin(&battle, &world, enemy_index, spells);
game = PH_GAME_TRANSITION;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
if (game != PH_GAME_TRANSITION) result = 1;
context.menu = PH_MENU_NONE;
ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS * 0.5f, 1);
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
game = PH_GAME_BATTLE;
ph_options_open(&options_ui);
context.menu = PH_MENU_MAIN;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
context.menu = PH_MENU_NONE;
battle.phase = PH_BATTLE_ENEMY_ACTION;
battle.timer = PH_BATTLE_ACTION_SECONDS;
@@ -312,7 +407,7 @@ main(void)
context.menu = PH_MENU_NONE;
battle_result = ph_battle_update(&battle, &world, 0.0f, 0);
if (battle_result == PH_BATTLE_DEFEAT) game = PH_GAME_OVER;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
if (game != PH_GAME_OVER) result = 1;
world.entities[world.player_index].vitals.hp =
ph_world_entity_stats(&world, ph_world_player(&world)).max_hp;
@@ -321,7 +416,7 @@ main(void)
context.menu = PH_MENU_NONE;
if (ph_battle_update(&battle, &world, PH_BATTLE_TRANSITION_SECONDS, 1) ==
PH_BATTLE_READY) game = PH_GAME_BATTLE;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
world.entities[enemy_index].vitals.hp =
ph_world_entity_stats(&world, &world.entities[enemy_index]).max_hp;
world.entities[enemy_index].vitals.hp = 1;
@@ -332,7 +427,7 @@ main(void)
if (battle.phase != PH_BATTLE_TARGET ||
battle.choice.target_index != world.player_index) result = 1;
if (ph_battle_target_step(&battle, &world, 1) != enemy_index) result = 1;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
ph_battle_confirm_target(&battle, &world);
ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0);
if (ph_battle_take_event(&battle) != PH_BATTLE_EVENT_SPELL) {
@@ -372,14 +467,14 @@ main(void)
result = 1;
}
context.menu = PH_MENU_NONE;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
ph_battle_cast(&battle, &world);
if (battle.phase != PH_BATTLE_TARGET ||
battle.choice.target_index != enemy_index) {
fprintf(stderr, "render smoke test failed: offensive target default\n");
result = 1;
}
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
ph_battle_confirm_target(&battle, &world);
context.menu = PH_MENU_NONE;
ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0);
@@ -387,10 +482,10 @@ main(void)
fprintf(stderr, "render smoke test failed: spell sound event\n");
result = 1;
}
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
context.menu = PH_MENU_NONE;
ph_battle_update(&battle, &world, PH_BATTLE_ACTION_SECONDS * 0.5f, 0);
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
if (game != PH_GAME_BATTLE || battle.phase != PH_BATTLE_COMMAND ||
battle.turn.current_actor != world.player_index ||
ph_world_player(&world)->vitals.mp != 35 ||
@@ -408,12 +503,12 @@ main(void)
fprintf(stderr, "render smoke test failed: attack sound event\n");
result = 1;
}
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
context.menu = PH_MENU_NONE;
battle_result = ph_battle_update(&battle, &world,
PH_BATTLE_ACTION_SECONDS * 0.5f, 0);
if (battle_result == PH_BATTLE_VICTORY) game = PH_GAME_VICTORY;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
if (game != PH_GAME_VICTORY ||
ph_world_player(&world)->xp != battle.reward.xp ||
ph_world_item_amount(&world, battle.reward.item_id) != 2) {
@@ -457,28 +552,28 @@ main(void)
ph_shop_open(&shop_ui, ph_shop_defs[0].id);
shop_ui.mode = PH_SHOP_TALK;
context.menu = PH_MENU_SHOP;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
if (ph_render_enter_portal(&world, PH_AREA_STONEHOLLOW) != 1) {
fprintf(stderr, "render smoke test failed: return to town\n");
result = 1;
}
context.menu = PH_MENU_NONE;
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
if (ph_render_enter_portal(&world, PH_AREA_MEADOW) != 1) {
fprintf(stderr, "render smoke test failed: return to meadow\n");
result = 1;
}
- ph_present_frame(renderer, &assets, &context);
+ ph_present_frame(renderer, &context, &labels);
if (!SDL_SaveBMP(surface, PH_RENDER_SMOKE_PATH)) {
fprintf(stderr, "SDL_SaveBMP failed: %s\n", SDL_GetError());
result = 1;
}
- ph_audio_destroy(&audio);
- ph_render_assets_destroy(&assets);
+ ph_audio_destroy(audio);
+ ph_render_destroy(renderer);
ph_world_destroy(&world);
- SDL_DestroyRenderer(renderer);
+ SDL_DestroyRenderer(target);
SDL_DestroySurface(surface);
SDL_Quit();
return result;
diff --git a/src/tests/smoke.c b/src/tests/smoke.c
@@ -110,6 +110,7 @@ ph_movement_test(int open, int blocked)
PhVec2 player_start = { PH_TILE_CENTER(PLAYER_COL), PH_TILE_CENTER(ROW) };
PhVec2 mob_start = { PH_TILE_CENTER(MOB_COL), PH_TILE_CENTER(ROW) };
float action_seconds = (float)PH_WORLD_ACTION_MILLISECONDS / 1000.0f;
+ const float frame_seconds = 0.05f;
PhWorld world;
PhWorldEvent event;
PhEntity *mob;
@@ -139,9 +140,9 @@ ph_movement_test(int open, int blocked)
!ph_world_take_event(&world, &event) ||
event.kind != PH_WORLD_EVENT_MOVE || event.entity_index != mob_index)
return 1;
- ph_world_tick(&world, (PhInput){ 0 }, PH_MAX_FRAME_TIME);
+ ph_world_tick(&world, (PhInput){ 0 }, frame_seconds);
if (mob->pos.x >= mob_start.x) return 1;
- ph_world_tick(&world, (PhInput){ .move_x = 1 }, PH_MAX_FRAME_TIME);
+ ph_world_tick(&world, (PhInput){ .move_x = 1 }, frame_seconds);
if (!world.entities[player_index].motion.moving ||
world.entities[player_index].motion.target.x !=
PH_TILE_CENTER(PLAYER_COL + 1) ||
@@ -149,7 +150,7 @@ ph_movement_test(int open, int blocked)
event.kind != PH_WORLD_EVENT_MOVE || event.entity_index != player_index ||
ph_entity_animation_frame(&world.content.entities[0],
&world.entities[player_index]) != 0) return 1;
- ph_world_tick(&world, (PhInput){ 0 }, PH_MAX_FRAME_TIME);
+ ph_world_tick(&world, (PhInput){ 0 }, frame_seconds);
if (ph_entity_animation_frame(&world.content.entities[0],
&world.entities[player_index]) <= 0) return 1;
world.entities[player_index].pos =