Licar/game.h
2025-01-20 21:33:05 +01:00

1139 lines
31 KiB
C

#ifndef _LCR_GAME_H
#define _LCR_GAME_H
/*
Licar: game module
This file implements the backend of a complete, actually playable game with
graphics, sound etc., and is meant to be included and used by specific
frontends (which will handle each platform's hardware details and I/O). See
the frontend info below for help with porting the game to a new platform.
The code uses LCR_ (or _LCR) prefix as a kind of namespace preventing
collision with 3rd party identifiers.
UNITS: There are various kinds of units used to ensure independence of the
modules. Here is a summary:
- LCR_GameUnit: data type, abstract unit of the game (racing module). One map
square is LCR_GAME_UNITs long, a full angle is also LCR_GAME_UNITs.
- LCR_GAME_UNIT: Size of one game square and full angle in LCR_GameUnits.
Square height is only half of this.
- S3L_Unit: data type, small3dlib's unit. May change with renderer change.
- S3L_F: small3dlib's value representing 1.0.
- LCR_RENDERER_UNIT: for the renderer one map square is this many S3L_Units.
- TPE_Unit: tinyphysicsengine's unit. May change with phys. engine change.
- TPE_F: tinyphysicsengine's value representing value 1.0.
- LCR_PHYSICS_UNIT: for the phys. eng. one map square is this many TPE_Units.
COORDINATE SYSTEM AND ROTATIONS: The game itself (racing module) is
independent of rendering and physics libraries, but out of convenient adopts
their coordinate system (X right, Y up, Z forward) and rotations (Euler
angles, by Z, then by X, then Y).
DATA FILE: The game uses so called data file to store various resources,
mainly maps and replays. There is considered to be one abstract global file
which is just a long text string. Internally the global file is composed of
a hardcoded internal data file string (stored in assets) with very basic maps,
and an optional user file (appended to the internal file) that the frontend
may provide, allowing adding more resources without recompiling the game. The
user file may be disabled on platforms that e.g. don't have file systems, but
the internal file will be always present. The format of the data file is
following: it consists of data strings separated by '#' character (data
strings cannot contain this character). Each data string starts with a magic
number: a single character identifying the type of the resource (map, replay,
...), then the name of the data follows, then character ';' and then the data
string itself (up until next '#' or end of file). The format of the string
depends on the type of the data, i.e. the format of map string has a special
format (described in the map module) etc.
*/
#define LCR_KEY_UP 0x00
#define LCR_KEY_RIGHT 0x01
#define LCR_KEY_DOWN 0x02
#define LCR_KEY_LEFT 0x03
#define LCR_KEY_A 0x04 ///< confirm, restart race
#define LCR_KEY_B 0x05 ///< cancel, open menu
#define LCR_KEYS_TOTAL 6
/*
FOR FRONTENDS (porting to other platforms):
- Implement the below described functions according to their description.
- Implement the main program and game loop.
- Call the below described functions as described.
- If you want to support music, make your frontend play music from the "music"
file in assets. It is in raw format, storing 8bit unsigned samples at 8000
Hz. Use the LCR_gameMusicOn to check what the music volume is. If you
don't support music, set LCR_SETTING_MUSIC to 0 in your frontend code so
that the game knows music is disabled.
*/
/**
Implement this in your frontend. Returns 1 if given key is pressed or 0
otherwise.
*/
uint8_t LCR_keyPressed(uint8_t key);
/**
Implement this in your frontend. This function pauses program execution for
given amount of milliseconds and relieves the CPU usage. On platforms that
don't support this the function may simply do nothing.
*/
void LCR_sleep(uint16_t timeMs);
/**
Implement this in your frontend. This function draws a pixel of given color
to the screen back buffer (i.e. NOT directly to screen, back buffer shall
only be copied to front buffer once the LCR_gameStep function finishes all
rendering). This function should NOT check for out-of-screen coordinates, this
is handled by the game internals and out-of-screen pixels will never be drawn.
The color value depends on game settings but is normally an RGB565 value.
*/
void LCR_drawPixel(unsigned long index, uint16_t color);
/**
Implement this in your frontend. This function will be called to log what the
program is doing. If you want to ignore logging, simply make the function do
nothing.
*/
void LCR_log(const char *str);
/**
Implement this in your frontend. This function serves for loading optional
data file that allows to add more maps, replays etc. If your frontend
won't support this, just make the function return 0. Otherwise it must return
characters from the data file one by one; after reaching the end of file
0 must be returned and the reading position will be reset to start again.
*/
char LCR_getNextDataFileChar(void);
/**
Implement this in your frontend. This serves to store data in the optional
data file, e.g. replays. If your frontend doesn't support this (e.g.
because the file is read only), the function may ignore the append, but if
the file is otherwise supported, a rewind of the read position must still be
done. If appending is supported, the function must append the provided string
to the data file AND then reset the data file reading position back to
the start.
*/
void LCR_appendDataStr(const char *str);
/**
Call this function in your frontend at the start of the program.
*/
void LCR_gameInit(void);
/**
Call this function in your frontend right before program end.
*/
void LCR_gameEnd(void);
/**
Call this function in your frontend repeatedly inside the main loop, pass the
current time as the number of milliseconds since program start. This function
will perform the game step AND other things such as checking the input states,
rendering or sleeping (all using the above functions you should implement).
Returns 0 if program should end, otherwise 1.
*/
uint8_t LCR_gameStep(uint32_t timeMs);
/**
Gets the current music volume;
*/
uint8_t LCR_gameMusicOn(void);
/**
Gets next audio sample (unsigned 8bit samples, 8 KHz).
*/
uint8_t LCR_gameGetNextAudioSample(void);
//------------------------------------------------------------------------------
#define LCR_LOG0(s) ;
#define LCR_LOG1(s) ;
#define LCR_LOG2(s) ;
#if LCR_SETTING_LOG_LEVEL > 0
#undef LCR_LOG0
#define LCR_LOG0(s) LCR_log(s);
#if LCR_SETTING_LOG_LEVEL > 1
#undef LCR_LOG1
#define LCR_LOG1(s) LCR_log(s);
#if LCR_SETTING_LOG_LEVEL > 2
#undef LCR_LOG2
#define LCR_LOG2(s) LCR_log(s);
#endif
#endif
#endif
#define LCR_CAMERA_MODE_DRIVE 0x00
#define LCR_CAMERA_MODE_DRIVE2 0x01
#define LCR_CAMERA_MODE_INSIDE 0x02
#define LCR_CAMERA_MODE_FREE 0x03
#define LCR_GAME_STATE_MENU 0x00
#define LCR_GAME_STATE_RUN_STARTING 0x01
#define LCR_GAME_STATE_RUN 0x02
#define LCR_GAME_STATE_RUN_FINISHED 0x03
#define LCR_GAME_STATE_LOADING_RUN 0x04
#define LCR_GAME_STATE_LOADING_REP1 0x05
#define LCR_GAME_STATE_LOADING_REP2 0x06
#define LCR_GAME_STATE_END 0xff
// forward decls of pixel drawing functions for the renderer
/**
Internal pixel drawing function that draws pixel at specified screen coords
without checking for safety (it's faster but can only be done if we know for
sure we're not drawing outside the screen).
*/
void LCR_drawPixelXYUnsafe(unsigned int x, unsigned int y, uint16_t color);
/**
Internal pixel drawing function that checks for out-of-screen coordinates. Use
this if the pixel can potentially lie of screen (however if you know it won't,
use the normal unsafe function in sake of performance).
*/
static inline void LCR_drawPixelXYSafe(unsigned int x, unsigned int y,
uint_fast16_t color);
#include "general.h"
#include "racing.h"
#include "audio.h"
#include "assets.h"
#include "renderer.h"
#define LCR_MENU_MAX_ITEMS 9 // don't change
#define LCR_RESOURCE_ITEM_CHUNK (LCR_MENU_MAX_ITEMS - 1)
#define LCR_MENU_TABS 4
#define LCR_MENU_STRING_SIZE 16
#define LCR_RESOURCE_FILE_SEPARATOR '#'
#define LCR_RESOURCE_FILE_SEPARATOR2 ';'
#define LCR_FREE_CAMERA_STEP \
((LCR_SETTING_FREE_CAMERA_SPEED * LCR_GAME_UNIT / 8) \
/ LCR_SETTING_FPS)
#if LCR_FREE_CAMERA_STEP == 0
#define LCR_FREE_CAMERA_STEP 1
#endif
#define LCR_FREE_CAMERA_TURN_STEP \
((LCR_SETTING_FREE_CAMERA_TURN_SPEED * LCR_GAME_UNIT) \
/ (360 * LCR_SETTING_FPS))
#if LCR_FREE_CAMERA_TURN_STEP == 0
#define LCR_FREE_CAMERA_TURN_STEP 1
#endif
struct
{
uint8_t state;
uint32_t stateStartTime;
uint32_t time;
uint32_t frame;
uint32_t nextRenderFrameTime;
uint32_t nextRacingTickTime;
uint8_t cameraMode;
uint8_t debugDraw;
uint8_t musicOn;
uint8_t keyStates[LCR_KEYS_TOTAL]; /**< Assures unchanging key states
during a single frame, hold number of
frames for which the key has been
continuously held. */
uint32_t runTimeMS; /**< Current time of the run */
struct
{
uint8_t selectedTab;
uint8_t selectedItem;
uint8_t itemCount;
char itemNames[LCR_MENU_MAX_ITEMS][LCR_MENU_STRING_SIZE];
const char *itemNamePtrs[LCR_MENU_MAX_ITEMS]; ///< helper array
} menu;
struct
{
int state; ///< -1 if reading external res. f., else pos.
// indices and counts are among the data of the same type
unsigned int firstItemIndex;
unsigned int itemsTotal;
} dataFile;
} LCR_game;
uint8_t LCR_gameMusicOn(void)
{
#if LCR_SETTING_MUSIC
return LCR_game.musicOn;
#else
return 0;
#endif
}
void LCR_drawPixelXYUnsafe(unsigned int x, unsigned int y,
uint16_t color)
{
#if LCR_SETTING_RESOLUTION_SUBDIVIDE == 1
LCR_drawPixel(y * LCR_SETTING_RESOLUTION_X + x,color);
#else
// TODO
#endif
}
void _LCR_physicdDebugDrawPixel(uint16_t x, uint16_t y, uint8_t color)
{
if (x > 1 && x < LCR_EFFECTIVE_RESOLUTION_X - 2 &&
y > 1 && y < LCR_EFFECTIVE_RESOLUTION_Y - 2)
{
uint16_t c = 0x8101 | (0x8f1f << (2 * color));
for (int j = -1; j <= 2; ++j)
for (int i = -1; i <= 2; ++i)
LCR_drawPixelXYUnsafe(x + i,y + j,c);
}
}
static inline void LCR_drawPixelXYSafe(unsigned int x, unsigned int y,
uint_fast16_t color)
{
if (x < LCR_EFFECTIVE_RESOLUTION_X && y < LCR_EFFECTIVE_RESOLUTION_Y)
LCR_drawPixelXYUnsafe(x,y,color);
}
void LCR_gameSetState(uint8_t state)
{
LCR_LOG1("changing state");
LCR_game.state = state;
LCR_game.stateStartTime = LCR_game.time;
}
LCR_GameUnit LCR_carSpeedKMH(void)
{
return // we use 28/8 as an approximation of 3.6 to convers MPS to KMH
(28 * LCR_SETTING_CMS_PER_BLOCK * LCR_racingGetCarSpeedUnsigned() *
LCR_RACING_FPS) / (800 * LCR_GAME_UNIT);
}
void LCR_gameResetRun(uint8_t replay)
{
LCR_GameUnit carTransform[6];
LCR_LOG0("resetting run");
LCR_mapReset();
LCR_racingRestart(replay);
LCR_rendererUnmarkCPs();
LCR_racingGetCarTransform(carTransform,carTransform + 3,0);
LCR_rendererSetCarTransform(carTransform,carTransform + 3);
LCR_rendererCameraReset();
LCR_gameSetState(LCR_GAME_STATE_RUN_STARTING);
LCR_game.runTimeMS = 0;
}
/**
Rewinds the global data file reading head to the beginning.
*/
void LCR_gameRewindDataFile(void)
{
LCR_appendDataStr("");
LCR_game.dataFile.state = 0;
}
/**
Reads the next global data file character (merged internal data file
with the optional user file). First the internal file will be read,
immediately followed by the user file, then zero char will be returned and
then reading starts over.
*/
char LCR_gameGetNextDataFileChar(void)
{
#if LCR_SETTING_ENABLE_RESOURCE_FILE
char c;
if (LCR_game.dataFile.state < 0) // external file?
{
c = LCR_getNextDataFileChar();
if (c == 0)
LCR_game.dataFile.state = 0; // move to internal file next
}
else // internal file
{
c = LCR_internalDataFile[LCR_game.dataFile.state];
LCR_game.dataFile.state++;
if (c == 0)
{
c = LCR_getNextDataFileChar();
LCR_game.dataFile.state = c ? -1 : 0; // trust this
}
}
return c;
#else
if (LCR_internalDataFile[LCR_game.dataFile.state] == 0)
{
LCR_game.dataFile.state = 0;
return 0;
}
return LCR_internalDataFile[LCR_game.dataFile.state++];
#endif
}
/**
Similar to LCR_gameGetNextDataFileChar, but returns 0 instead of the data
string separator character. This function is meant to be used by functions
that load something from a string while expecting a zero terminated string.
*/
char LCR_gameGetNextDataStrChar(void)
{
char c = LCR_gameGetNextDataFileChar();
return c != LCR_RESOURCE_FILE_SEPARATOR ? c : 0;
}
/**
Seeks to the Nth data string in the global data file, after the magic number,
so that the name is now available for reading.
*/
void LCR_seekDataByIndex(unsigned int index, char magicNumber)
{
char c;
LCR_LOG0("seeking data string");
LCR_gameRewindDataFile();
do
{
c = LCR_gameGetNextDataFileChar();
if (c == magicNumber)
{
if (index)
index--;
else
return;
}
while (c != 0 && c != LCR_RESOURCE_FILE_SEPARATOR)
c = LCR_gameGetNextDataFileChar();
} while (c);
}
void LCR_gameLoadMap(unsigned int mapIndex)
{
char mapName[LCR_MAP_NAME_MAX_LEN];
LCR_seekDataByIndex(mapIndex,'M');
mapName[0] = 0;
for (int i = 0; i < LCR_MAP_NAME_MAX_LEN; ++i)
{
char c = LCR_gameGetNextDataFileChar();
if (c == LCR_RESOURCE_FILE_SEPARATOR2 ||
c == LCR_RESOURCE_FILE_SEPARATOR || c == 0)
break;
mapName[i] = c;
mapName[i + 1] = 0;
}
LCR_mapLoadFromStr(LCR_gameGetNextDataStrChar,mapName);
}
/**
Loads replay by its index, returns index of a map for the replay (and the map
will be loaded as with LCR_mapLoadFromStr) or -1 if the map wasn't found or -2
if the replay couldn't be loaded. This function potentially reloads current
map!
*/
unsigned int LCR_gameLoadReplay(unsigned int replayIndex)
{
uint32_t mapHash;
uint16_t nameHash;
char c;
LCR_LOG1("loading replay and map");
LCR_seekDataByIndex(replayIndex,'R');
do // skip name
{
c = LCR_gameGetNextDataFileChar();
}
while (c && c != LCR_RESOURCE_FILE_SEPARATOR2 &&
c != LCR_RESOURCE_FILE_SEPARATOR);
if (!LCR_replayLoadFromStr(LCR_gameGetNextDataStrChar,
&mapHash,&nameHash))
return -2;
// now try to find the map with given nameHash
LCR_gameRewindDataFile();
unsigned int skipTo = 0;
while (1)
{
unsigned int mapIndex = 0;
while (1) // find first skipToth map
{
c = LCR_gameGetNextDataFileChar();
if (c == 0)
return -1;
else if (c == 'M')
{
if (mapIndex >= skipTo &&
nameHash == _LCR_simpleStrHash(LCR_gameGetNextDataStrChar,';'))
{
LCR_LOG2("map name hash matches");
LCR_gameLoadMap(mapIndex);
if (mapHash == LCR_currentMap.hash)
return mapIndex;
else
{
LCR_LOG2("map hash doesn't match");
// map hash doesn't match
skipTo = mapIndex + 1;
break;
}
}
mapIndex++;
}
while (c != LCR_RESOURCE_FILE_SEPARATOR)
{
if (c == 0)
return -1;
c = LCR_gameGetNextDataFileChar();
}
}
}
return 0;
}
void LCR_gameEraseMenuItemNames(void)
{
for (int i = 0; i < LCR_MENU_MAX_ITEMS; ++i)
for (int j = 0; j < LCR_MENU_STRING_SIZE; ++j)
LCR_game.menu.itemNames[i][j] = 0;
LCR_game.menu.itemCount = 0;
}
void LCR_gameSetMenuItemStr(uint8_t item, const char *str, char replaceChar)
{
for (int i = 0; i < LCR_MENU_STRING_SIZE - 1; ++i)
{
LCR_game.menu.itemNames[item][i] = str[i] == '$' ? replaceChar : str[i];
LCR_game.menu.itemNames[item][i + 1] = 0;
}
}
void LCR_gameLoadMainMenuItems(void)
{
for (int i = 0; i < 5; ++i)
{
char replaceChar = i == 0 ? '0' + LCR_game.cameraMode :
(i == 1 ? '0' + LCR_game.musicOn : ('0' + LCR_audio.on));
LCR_gameSetMenuItemStr(i,LCR_texts[LCR_TEXTS_MAIN_MENU + i],replaceChar);
}
LCR_game.menu.itemCount = 4;
}
void LCR_gameInit(void)
{
LCR_LOG0("initializing");
for (int i = 0; i < LCR_KEYS_TOTAL; ++i)
LCR_game.keyStates[i] = 0;
LCR_rendererInit();
LCR_racingInit();
LCR_audioInit();
LCR_game.dataFile.state = 0;
for (int i = 0; i < LCR_MENU_MAX_ITEMS; ++i)
LCR_game.menu.itemNamePtrs[i] = LCR_game.menu.itemNames[i];
LCR_game.menu.selectedTab = 0;
LCR_game.menu.selectedItem = 0;
LCR_game.frame = 0;
LCR_game.musicOn = 1;
LCR_game.nextRenderFrameTime = 0;
LCR_game.nextRacingTickTime = 0;
LCR_game.cameraMode = LCR_CAMERA_MODE_DRIVE;
LCR_gameLoadMainMenuItems();
LCR_gameSetState(LCR_GAME_STATE_MENU);
LCR_currentMap.blockCount = 0; // means no map loaded
}
/**
Loads up to LCR_RESOURCE_ITEM_CHUNK items of given type, starting at given
index (among items of the same type). This will also load the menu item names.
*/
void LCR_gameLoadDataFileChunk(unsigned int startIndex, char magicNumber)
{
char c;
unsigned char state = 0;
LCR_gameEraseMenuItemNames();
LCR_game.dataFile.firstItemIndex = startIndex;
LCR_game.dataFile.itemsTotal = 0;
LCR_gameRewindDataFile();
/* 3 iterations: in first we seek to the start index, in second we load the
names, in third we just read the rest to get the total count. */
for (int i = 0; i < 3; ++i)
{
while (1)
{
if (i == 0 && !startIndex)
break;
c = LCR_gameGetNextDataFileChar();
if (c == 0)
return;
if (state == 0) // second magic char
{
state = 255;
if (c == magicNumber)
{
LCR_game.dataFile.itemsTotal++;
if (i == 0)
startIndex--;
else if (i == 1)
state = 1;
}
}
else if (i == 1 && state != 255)
{
if (c == LCR_RESOURCE_FILE_SEPARATOR ||
c == LCR_RESOURCE_FILE_SEPARATOR2 ||
state >= 1 + LCR_MENU_STRING_SIZE - 1)
{
state = 255;
LCR_game.menu.itemCount++;
if (LCR_game.menu.itemCount >= LCR_RESOURCE_ITEM_CHUNK)
break;
}
else
LCR_game.menu.itemNames[LCR_game.menu.itemCount][state - 1] = c;
state++;
}
if (c == LCR_RESOURCE_FILE_SEPARATOR)
state = 0;
}
}
}
void LCR_gameEnd(void)
{
LCR_LOG0("ending");
}
void LCR_gameTimeToStr(uint32_t timeMS, char *str)
{
str[9] = 0;
str[6] = '0' + timeMS % 10; // milliseconds
timeMS /= 10;
str[7] = '0' + timeMS % 10;
timeMS /= 10;
str[8] = '0' + timeMS % 10;
timeMS /= 10;
str[3] = '0' + (timeMS % 60) / 10; // seconds
str[4] = '0' + timeMS % 10;
str[5] = '\'';
timeMS = (timeMS / 60) % 100; // minutes
str[0] = '0' + timeMS / 10;
str[1] = '0' + timeMS % 10;
str[2] = '\'';
}
int _LCR_gameIsLoading(void)
{
return
(LCR_game.state == LCR_GAME_STATE_LOADING_RUN) ||
(LCR_game.state == LCR_GAME_STATE_LOADING_REP1) ||
(LCR_game.state == LCR_GAME_STATE_LOADING_REP2);
}
void LCR_gameDraw3DView(void)
{
LCR_GameUnit carTransform[6];
LCR_GameUnit physicsInterpolationParam = LCR_GAME_UNIT -
((LCR_game.nextRacingTickTime - LCR_game.time) * LCR_GAME_UNIT)
/ LCR_RACING_TICK_MS;
LCR_racingGetCarTransform(carTransform,carTransform + 3,
physicsInterpolationParam);
LCR_rendererSetCarTransform(carTransform,carTransform + 3);
if (LCR_game.cameraMode != LCR_CAMERA_MODE_FREE &&
LCR_game.state != LCR_GAME_STATE_RUN_FINISHED)
LCR_rendererCameraFollow(
(LCR_game.cameraMode != LCR_CAMERA_MODE_INSIDE) +
(LCR_game.cameraMode == LCR_CAMERA_MODE_DRIVE2));
#if LCR_ANIMATE_CAR
LCR_rendererSetWheelState(LCR_racingGetWheelRotation(),
LCR_racingGetWheelSteer() * 2);
#endif
LCR_rendererDraw3D();
#if LCR_SETTING_DEBUG_PHYSICS_DRAW
LCR_GameUnit camTr[7];
LCR_rendererGetCameraTransform(camTr,camTr + 3,camTr + 6);
LCR_physicsDebugDraw(camTr,camTr + 3,camTr[6],_LCR_physicdDebugDrawPixel);
#endif
// GUI/HUD:
char str[10];
switch (LCR_game.state)
{
case LCR_GAME_STATE_RUN_STARTING:
str[0] = '0' + LCR_SETTING_COUNTDOWN_SECONDS -
(LCR_game.time - LCR_game.stateStartTime) / 1000;
str[1] = 0;
LCR_rendererDrawText(str,
(LCR_EFFECTIVE_RESOLUTION_X - LCR_rendererComputeTextWidth(str,8)) / 2,
LCR_EFFECTIVE_RESOLUTION_Y / 2,0x0707,8);
break;
default:
{
int val = LCR_carSpeedKMH();
if (val < 5) // don't show tiny oscillations when still
val = 0;
str[0] = val >= 100 ? '0' + (val / 100) % 10 : ' ';
str[1] = val >= 10 ? '0' + (val / 10) % 10 : ' ';
str[2] = '0' + val % 10;
str[3] = 0;
LCR_rendererDrawText(str,
LCR_EFFECTIVE_RESOLUTION_X -
LCR_rendererComputeTextWidth(str,2) - 20,
LCR_EFFECTIVE_RESOLUTION_Y -
LCR_rendererComputeTextHeight(2) - 20,0,2);
LCR_gameTimeToStr(LCR_game.runTimeMS,str);
LCR_rendererDrawText(str,20,LCR_EFFECTIVE_RESOLUTION_Y -
LCR_rendererComputeTextHeight(2) - 45,0,2);
LCR_gameTimeToStr(LCR_currentMap.targetTime,str);
LCR_rendererDrawText(str,20,LCR_EFFECTIVE_RESOLUTION_Y -
LCR_rendererComputeTextHeight(2) - 20,0x4208,2);
break;
}
}
}
void _LCR_gameDataCharWrite(char c)
{ // TODO
printf("%c",c);
}
/**
Helper subroutine, handles user input during main loop frame, EXCEPT for the
driving input (that is handled in the loop itself).
*/
void LCR_gameHandleInput(void)
{
int tabSwitchedTo = -1;
int scrolled = 0;
switch (LCR_game.state)
{
case LCR_GAME_STATE_MENU:
if (LCR_game.keyStates[LCR_KEY_RIGHT] == 1)
{
LCR_LOG1("menu tab right");
LCR_game.menu.selectedTab =
(LCR_game.menu.selectedTab + 1) % LCR_MENU_TABS;
tabSwitchedTo = LCR_game.menu.selectedTab;
LCR_game.menu.selectedItem = 0;
LCR_audioPlaySound(LCR_SOUND_CLICK);
}
else if (LCR_game.keyStates[LCR_KEY_LEFT] == 1)
{
LCR_LOG1("menu tab left");
LCR_game.menu.selectedTab =
(LCR_game.menu.selectedTab + LCR_MENU_TABS - 1) % LCR_MENU_TABS;
tabSwitchedTo = LCR_game.menu.selectedTab;
LCR_game.menu.selectedItem = 0;
LCR_audioPlaySound(LCR_SOUND_CLICK);
}
else if (LCR_game.keyStates[LCR_KEY_UP] == 1)
{
LCR_LOG1("menu item up");
if (LCR_game.menu.selectedItem != 0)
{
LCR_game.menu.selectedItem--;
LCR_audioPlaySound(LCR_SOUND_CLICK);
}
else if (LCR_game.menu.selectedTab != 0 &&
LCR_game.dataFile.firstItemIndex != 0)
{
LCR_game.menu.selectedItem = LCR_RESOURCE_ITEM_CHUNK - 1;
LCR_audioPlaySound(LCR_SOUND_CLICK);
scrolled = -1;
}
}
else if (LCR_game.keyStates[LCR_KEY_DOWN] == 1)
{
LCR_LOG1("menu item down");
if (LCR_game.menu.selectedTab == 0)
{
if (LCR_game.menu.selectedItem < 4)
{
LCR_game.menu.selectedItem++;
LCR_audioPlaySound(LCR_SOUND_CLICK);
}
}
else if (LCR_game.menu.selectedItem < LCR_game.menu.itemCount - 1)
{
LCR_game.menu.selectedItem++;
LCR_audioPlaySound(LCR_SOUND_CLICK);
}
else if (LCR_game.dataFile.firstItemIndex +
LCR_RESOURCE_ITEM_CHUNK < LCR_game.dataFile.itemsTotal)
{
LCR_game.menu.selectedItem = 0;
LCR_audioPlaySound(LCR_SOUND_CLICK);
scrolled = 1;
}
}
else if (LCR_game.keyStates[LCR_KEY_B] == 1 && LCR_currentMap.blockCount)
{
LCR_LOG1("menu quit");
LCR_gameSetState(LCR_GAME_STATE_RUN);
}
else if (LCR_game.keyStates[LCR_KEY_A] == 1)
{
LCR_LOG1("menu confirm");
LCR_audioPlaySound(LCR_SOUND_CLICK);
switch (LCR_game.menu.selectedTab)
{
case 0:
switch (LCR_game.menu.selectedItem)
{
case 0:
LCR_game.cameraMode = (LCR_game.cameraMode + 1) % 4;
LCR_rendererSetCarVisibility(
LCR_game.cameraMode != LCR_CAMERA_MODE_INSIDE);
LCR_rendererCameraReset();
break;
case 1:
LCR_game.musicOn = !LCR_game.musicOn;
break;
case 2:
LCR_audio.on = !LCR_audio.on;
break;
case 4:
LCR_gameSetState(LCR_GAME_STATE_END);
break;
default: break;
}
LCR_gameLoadMainMenuItems();
break;
case 1:
LCR_gameLoadMap(LCR_game.dataFile.firstItemIndex +
LCR_game.menu.selectedItem);
LCR_gameSetState(LCR_GAME_STATE_LOADING_RUN);
break;
case 2:
case 3:
{
int mapIndex = LCR_gameLoadReplay(LCR_game.dataFile.firstItemIndex +
LCR_game.menu.selectedItem);
if (mapIndex < -1)
{
LCR_LOG1("couldn't load replay");
}
else if (mapIndex == -1)
{
LCR_LOG1("couldn't load replay map");
}
else
LCR_gameSetState(LCR_game.menu.selectedTab == 2 ?
LCR_GAME_STATE_LOADING_REP1 : LCR_GAME_STATE_LOADING_REP2);
break;
}
default: break;
}
}
break;
case LCR_GAME_STATE_RUN_FINISHED:
if (LCR_game.keyStates[LCR_KEY_A] == 1)
LCR_gameResetRun(LCR_racing.playingReplay);
break;
case LCR_GAME_STATE_RUN_STARTING:
if (LCR_game.time - LCR_game.stateStartTime
>= 1000 * LCR_SETTING_COUNTDOWN_SECONDS)
LCR_gameSetState(LCR_GAME_STATE_RUN);
// fall through
default:
if (LCR_game.keyStates[LCR_KEY_B] == 1)
{
LCR_LOG1("menu open");
LCR_gameSetState(LCR_GAME_STATE_MENU);
LCR_game.menu.selectedItem = 0;
}
else if (LCR_game.cameraMode == LCR_CAMERA_MODE_FREE)
{
LCR_GameUnit offsets[5];
for (int i = 0; i < 5; ++i)
offsets[i] = 0;
if (LCR_game.keyStates[LCR_KEY_A])
{
if (LCR_game.keyStates[LCR_KEY_UP])
offsets[4] = LCR_FREE_CAMERA_TURN_STEP;
else if (LCR_game.keyStates[LCR_KEY_DOWN])
offsets[4] -= LCR_FREE_CAMERA_TURN_STEP;
if (LCR_game.keyStates[LCR_KEY_RIGHT])
offsets[3] -= LCR_FREE_CAMERA_TURN_STEP;
else if (LCR_game.keyStates[LCR_KEY_LEFT])
offsets[3] = LCR_FREE_CAMERA_TURN_STEP;
}
else
{
if (LCR_game.keyStates[LCR_KEY_UP])
offsets[0] = LCR_FREE_CAMERA_STEP;
else if (LCR_game.keyStates[LCR_KEY_DOWN])
offsets[0] -= LCR_FREE_CAMERA_STEP;
if (LCR_game.keyStates[LCR_KEY_RIGHT])
offsets[1] = LCR_FREE_CAMERA_STEP;
else if (LCR_game.keyStates[LCR_KEY_LEFT])
offsets[1] -= LCR_FREE_CAMERA_STEP;
}
LCR_rendererMoveCamera(offsets,offsets + 3);
}
else if (LCR_game.keyStates[LCR_KEY_A] == 1)
LCR_gameResetRun(LCR_racing.playingReplay);
break;
}
if (tabSwitchedTo == 0)
LCR_gameLoadMainMenuItems();
else if (tabSwitchedTo > 0 || scrolled != 0)
LCR_gameLoadDataFileChunk(
(tabSwitchedTo > 0) ? 0 : (LCR_game.dataFile.firstItemIndex +
scrolled * LCR_RESOURCE_ITEM_CHUNK),
LCR_game.menu.selectedTab == 1 ? 'M' : 'R');
}
uint8_t LCR_gameStep(uint32_t time)
{
uint32_t sleep = 0;
int paused = LCR_game.state == LCR_GAME_STATE_MENU ||
LCR_game.state == LCR_GAME_STATE_RUN_STARTING;
LCR_LOG2("game step (start)");
LCR_game.time = time;
for (int i = 0; i < LCR_KEYS_TOTAL; ++i)
LCR_game.keyStates[i] = LCR_keyPressed(i) ?
(LCR_game.keyStates[i] < 255 ? LCR_game.keyStates[i] + 1 : 255) : 0;
if (_LCR_gameIsLoading())
{
LCR_rendererLoadMap();
LCR_gameResetRun(
LCR_game.state == LCR_GAME_STATE_LOADING_REP1);
}
LCR_gameHandleInput();
// handle simulation:
while (time >= LCR_game.nextRacingTickTime)
{
LCR_LOG2("gonna step racing engine");
unsigned int input =
(LCR_game.cameraMode == LCR_CAMERA_MODE_FREE ||
LCR_game.state == LCR_GAME_STATE_RUN_FINISHED) ? 0 :
((LCR_game.keyStates[LCR_KEY_UP] ? LCR_RACING_INPUT_FORW : 0) |
(LCR_game.keyStates[LCR_KEY_RIGHT] ? LCR_RACING_INPUT_RIGHT : 0) |
(LCR_game.keyStates[LCR_KEY_DOWN] ? LCR_RACING_INPUT_BACK : 0) |
(LCR_game.keyStates[LCR_KEY_LEFT] ? LCR_RACING_INPUT_LEFT : 0));
uint32_t events = paused ? 0 : LCR_racingStep(input);
if (events & LCR_RACING_EVENT_CP_TAKEN)
{
int carBlock[3];
LCR_LOG1("CP taken");
LCR_racingGetCarBlockCoords(carBlock);
LCR_rendererMarkTakenCP(carBlock[0],carBlock[1],carBlock[2]);
LCR_audioPlaySound(LCR_SOUND_CLICK);
}
else if (events & LCR_RACING_EVENT_FINISHED &&
LCR_game.state != LCR_GAME_STATE_RUN_FINISHED)
{
LCR_LOG1("finished");
LCR_replayOutputStr(_LCR_gameDataCharWrite);
LCR_audioPlaySound(LCR_SOUND_CLICK);
LCR_gameSetState(LCR_GAME_STATE_RUN_FINISHED);
}
if (events & LCR_RACING_EVENT_CRASH_SMALL)
{
LCR_audioPlaySound(LCR_SOUND_CRASH_SMALL);
LCR_LOG1("crash (small)");
}
else if (events & LCR_RACING_EVENT_CRASH_BIG)
{
LCR_audioPlaySound(LCR_SOUND_CRASH_BIG);
LCR_LOG1("crash (big)");
}
int engineIntensity = LCR_carSpeedKMH() * 2;
LCR_audioSetEngineIntensity(paused ? 0 :
(engineIntensity < 256 ? engineIntensity : 255));
if (LCR_game.state != LCR_GAME_STATE_RUN_FINISHED)
LCR_game.runTimeMS = LCR_racingGetRunTimeMS();
LCR_game.nextRacingTickTime += LCR_RACING_TICK_MS;
}
sleep = (3 * (LCR_game.nextRacingTickTime - time)) / 4;
// handle rendering:
if (time >= LCR_game.nextRenderFrameTime)
{
LCR_LOG2("rendering next frame");
while (time >= LCR_game.nextRenderFrameTime)
LCR_game.nextRenderFrameTime += 1000 / LCR_SETTING_FPS;
if ((LCR_game.state == LCR_GAME_STATE_MENU) || _LCR_gameIsLoading())
LCR_rendererDrawMenu(LCR_texts[LCR_TEXTS_TABS
+ LCR_game.menu.selectedTab],LCR_game.menu.itemNamePtrs,
LCR_game.menu.itemCount + 1,LCR_game.menu.selectedItem);
else
LCR_gameDraw3DView();
}
else
{
uint32_t tmp = (3 * (LCR_game.nextRenderFrameTime - time)) / 4;
sleep = tmp < sleep ? tmp : sleep;
}
if (_LCR_gameIsLoading())
{
// show the "loading" screen
LCR_rendererDrawRect(
LCR_EFFECTIVE_RESOLUTION_X / 8,
LCR_EFFECTIVE_RESOLUTION_Y / 3,
LCR_EFFECTIVE_RESOLUTION_X - LCR_EFFECTIVE_RESOLUTION_X / 4,
LCR_EFFECTIVE_RESOLUTION_Y - 2 * LCR_EFFECTIVE_RESOLUTION_Y / 3,
0xffff,0);
LCR_rendererDrawText(LCR_texts[LCR_TEXTS_LOADING],
(LCR_EFFECTIVE_RESOLUTION_X -
LCR_rendererComputeTextWidth(LCR_texts[LCR_TEXTS_LOADING],4)) / 2,
(LCR_EFFECTIVE_RESOLUTION_Y -
LCR_rendererComputeTextHeight(4)) / 2,0x0000,4);
}
if (sleep)
LCR_sleep(sleep);
else
{
LCR_LOG2("can't sleep");
}
LCR_game.frame++;
LCR_LOG2("game step (end)");
return LCR_game.state != LCR_GAME_STATE_END;
}
uint8_t LCR_gameGetNextAudioSample(void)
{
return LCR_audioGetNextSample();
}
#endif // guard