982 lines
28 KiB
C
982 lines
28 KiB
C
/**
|
|
game: this file implements the backend of a complete, actually playable
|
|
game, and is meant to be included and used by specific frontends (which
|
|
will handle each platform's hardware details and I/O).
|
|
|
|
TODO: more documentation
|
|
|
|
UNITS: There are various kinds of units used to ensure independence of the
|
|
game 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).
|
|
|
|
RESOURCE FILE: The game uses so called resource 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 resource 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 resource file is following: it consists of resource strings
|
|
separated by '#' character (resource strings cannot contain this character).
|
|
Each resource string starts with a magic number: a single character
|
|
identifying the type of the resource (map, replay, ...), then the name of the
|
|
resource follows, then character ';' and then the resource string itself (up
|
|
until next '#' or end of file). The format of the string depends on the type
|
|
of resource, i.e. the format of map string has a special format (described in
|
|
the map module) etc.
|
|
*/
|
|
|
|
#ifndef _LCR_GAME_H
|
|
#define _LCR_GAME_H
|
|
|
|
#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:
|
|
- 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
|
|
resource 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 resource 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_getNextResourceFileChar(void);
|
|
|
|
/**
|
|
Implement this in your frontend. This serves to store data in the optional
|
|
resource 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 resource file AND then reset the resource file reading position back to
|
|
the start.
|
|
*/
|
|
void LCR_appendResourceStr(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_MAP 0x04
|
|
#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 "constants.h"
|
|
#include "racing.h"
|
|
#include "settings.h"
|
|
#include "audio.h"
|
|
#include "assets.h"
|
|
#include "renderer.h"
|
|
|
|
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 resources of the same type
|
|
unsigned int firstItemIndex;
|
|
unsigned int itemsTotal;
|
|
} resourceFile;
|
|
} 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
|
|
}
|
|
|
|
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_METERS_PER_BLOCK * LCR_racingGetCarSpeedUnsigned() *
|
|
LCR_RACING_FPS) / (8 * 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 resource reading head to the beginning.
|
|
*/
|
|
void LCR_gameRewindResourceFile(void)
|
|
{
|
|
LCR_appendResourceStr("");
|
|
LCR_game.resourceFile.state = 0;
|
|
}
|
|
|
|
/**
|
|
Reads the next global resource file character (merged internal resource 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_gameGetNextResourceFileChar(void)
|
|
{
|
|
#if LCR_SETTING_ENABLE_RESOURCE_FILE
|
|
char c;
|
|
|
|
if (LCR_game.resourceFile.state < 0) // external file?
|
|
{
|
|
c = LCR_getNextResourceFileChar();
|
|
|
|
if (c == 0)
|
|
LCR_game.resourceFile.state = 0; // move to internal file next
|
|
}
|
|
else // internal file
|
|
{
|
|
c = LCR_internalResourceFile[LCR_game.resourceFile.state];
|
|
LCR_game.resourceFile.state++;
|
|
|
|
if (c == 0)
|
|
{
|
|
c = LCR_getNextResourceFileChar();
|
|
LCR_game.resourceFile.state = c ? -1 : 0; // trust this
|
|
}
|
|
}
|
|
|
|
return c;
|
|
#else
|
|
if (LCR_internalResourceFile[LCR_game.resourceFile.state] == 0)
|
|
{
|
|
LCR_game.resourceFile.state = 0;
|
|
return 0;
|
|
}
|
|
|
|
return LCR_internalResourceFile[LCR_game.resourceFile.state++];
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
Similar to LCR_gameGetNextResourceFileChar, but returns 0 instead of the
|
|
resource 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_gameGetNextResourceStrChar(void)
|
|
{
|
|
char c = LCR_gameGetNextResourceFileChar();
|
|
return c != LCR_RESOURCE_FILE_SEPARATOR ? c : 0;
|
|
}
|
|
|
|
/**
|
|
Seeks to the Nth resource string in the global resource file, after the magic
|
|
number, so that the name is now available for reading.
|
|
*/
|
|
void LCR_seekResourceByIndex(unsigned int index, char magicNumber)
|
|
{
|
|
char c;
|
|
|
|
LCR_LOG0("seeking resource string");
|
|
|
|
LCR_gameRewindResourceFile();
|
|
|
|
do
|
|
{
|
|
c = LCR_gameGetNextResourceFileChar();
|
|
|
|
if (c == magicNumber)
|
|
{
|
|
if (index)
|
|
index--;
|
|
else
|
|
return;
|
|
}
|
|
|
|
while (c != 0 && c != LCR_RESOURCE_FILE_SEPARATOR)
|
|
c = LCR_gameGetNextResourceFileChar();
|
|
|
|
} while (c);
|
|
}
|
|
|
|
void LCR_gameStartRun(unsigned int mapIndex)
|
|
{
|
|
char mapName[LCR_MAP_NAME_MAX_LEN];
|
|
|
|
LCR_seekResourceByIndex(mapIndex,'M');
|
|
|
|
mapName[0] = 0;
|
|
|
|
for (int i = 0; i < LCR_MAP_NAME_MAX_LEN; ++i)
|
|
{
|
|
char c = LCR_gameGetNextResourceFileChar();
|
|
|
|
if (c == LCR_RESOURCE_FILE_SEPARATOR2 ||
|
|
c == LCR_RESOURCE_FILE_SEPARATOR || c == 0)
|
|
break;
|
|
|
|
mapName[i] = c;
|
|
mapName[i + 1] = 0;
|
|
}
|
|
|
|
LCR_mapLoadFromStr(LCR_gameGetNextResourceStrChar,mapName);
|
|
LCR_gameSetState(LCR_GAME_STATE_LOADING_MAP);
|
|
}
|
|
|
|
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.resourceFile.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_gameLoadResourceFileChunk(unsigned int startIndex, char magicNumber)
|
|
{
|
|
char c;
|
|
unsigned char state = 0;
|
|
|
|
LCR_gameEraseMenuItemNames();
|
|
|
|
LCR_game.resourceFile.firstItemIndex = startIndex;
|
|
LCR_game.resourceFile.itemsTotal = 0;
|
|
|
|
LCR_gameRewindResourceFile();
|
|
|
|
/* 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_gameGetNextResourceFileChar();
|
|
|
|
if (c == 0)
|
|
return;
|
|
|
|
if (state == 0) // second magic char
|
|
{
|
|
state = 255;
|
|
|
|
if (c == magicNumber)
|
|
{
|
|
LCR_game.resourceFile.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_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]);
|
|
#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);
|
|
|
|
val = LCR_game.runTimeMS;
|
|
|
|
str[9] = 0;
|
|
|
|
str[6] = '0' + val % 10; // milliseconds
|
|
val /= 10;
|
|
str[7] = '0' + val % 10;
|
|
val /= 10;
|
|
str[8] = '0' + val % 10;
|
|
val /= 10;
|
|
|
|
str[3] = '0' + (val % 60) / 10; // seconds
|
|
str[4] = '0' + val % 10;
|
|
str[5] = '\'';
|
|
|
|
val = (val / 60) % 100; // minutes
|
|
|
|
str[0] = '0' + val / 10;
|
|
str[1] = '0' + val % 10;
|
|
str[2] = '\'';
|
|
|
|
LCR_rendererDrawText(str,20,LCR_EFFECTIVE_RESOLUTION_Y -
|
|
LCR_rendererComputeTextHeight(2) - 20,0,2);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
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.resourceFile.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.resourceFile.firstItemIndex +
|
|
LCR_RESOURCE_ITEM_CHUNK < LCR_game.resourceFile.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_gameStartRun(
|
|
LCR_game.resourceFile.firstItemIndex + LCR_game.menu.selectedItem);
|
|
break;
|
|
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case LCR_GAME_STATE_RUN_FINISHED:
|
|
if (LCR_game.keyStates[LCR_KEY_A] == 1)
|
|
//LCR_gameResetRun(LCR_racing.playingReplay);
|
|
LCR_gameResetRun(1);
|
|
|
|
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_gameLoadResourceFileChunk(
|
|
(tabSwitchedTo > 0) ? 0 : (LCR_game.resourceFile.firstItemIndex +
|
|
scrolled * LCR_RESOURCE_ITEM_CHUNK),
|
|
LCR_game.menu.selectedTab == 1 ? 'M' : 'R');
|
|
}
|
|
|
|
void _LCR_gameResourceCharWrite(char c)
|
|
{
|
|
printf("%c",c);
|
|
}
|
|
|
|
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_game.state == LCR_GAME_STATE_LOADING_MAP)
|
|
{
|
|
LCR_rendererLoadMap();
|
|
LCR_gameResetRun(LCR_racing.playingReplay);
|
|
}
|
|
|
|
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_gameResourceCharWrite);
|
|
|
|
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_game.state == LCR_GAME_STATE_LOADING_MAP)
|
|
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_game.state == LCR_GAME_STATE_LOADING_MAP)
|
|
{
|
|
// 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
|