|
|
|
/**
|
|
|
|
@file game.h
|
|
|
|
|
|
|
|
Main source file of Anarch the game that puts together all the pieces. main
|
|
|
|
game logic is implemented here.
|
|
|
|
|
|
|
|
physics notes (you can break this when messing around with game constants):
|
|
|
|
|
|
|
|
- Lowest ceiling under which player can fit is 4 height steps.
|
|
|
|
- Widest hole over which player can run without jumping is 1 square.
|
|
|
|
- Widest hole over which the player can jump is 3 squares.
|
|
|
|
- Highest step a player can walk onto without jumping is 2 height steps.
|
|
|
|
- Highest step a player can jump onto is 3 height steps.
|
|
|
|
|
|
|
|
by Miloslav Ciz (drummyfish), 2019
|
|
|
|
|
|
|
|
Released under CC0 1.0 (https://creativecommons.org/publicdomain/zero/1.0/)
|
|
|
|
plus a waiver of all other intellectual property. The goal of this work is
|
|
|
|
be and remain completely in the public domain forever, available for any use
|
|
|
|
whatsoever.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#ifndef _SFG_GAME_H
|
|
|
|
#define _SFG_GAME_H
|
|
|
|
|
|
|
|
#include <stdint.h> // Needed for fixed width types, can easily be replaced.
|
|
|
|
|
|
|
|
/*
|
|
|
|
The following keys are mandatory to be implemented on any platform in order
|
|
|
|
for the game to be playable. Enums are bloat.
|
|
|
|
*/
|
|
|
|
#define SFG_KEY_UP 0
|
|
|
|
#define SFG_KEY_RIGHT 1
|
|
|
|
#define SFG_KEY_DOWN 2
|
|
|
|
#define SFG_KEY_LEFT 3
|
|
|
|
#define SFG_KEY_A 4 ///< fire, confirm
|
|
|
|
#define SFG_KEY_B 5 ///< cancel, strafe, look up/down
|
|
|
|
#define SFG_KEY_C 6 ///< menu, jump, switch weapons
|
|
|
|
|
|
|
|
/*
|
|
|
|
The following keys are optional for a platform to implement. They just make
|
|
|
|
the controls more comfortable.
|
|
|
|
*/
|
|
|
|
#define SFG_KEY_JUMP 7
|
|
|
|
#define SFG_KEY_STRAFE_LEFT 8
|
|
|
|
#define SFG_KEY_STRAFE_RIGHT 9
|
|
|
|
#define SFG_KEY_MAP 10
|
|
|
|
#define SFG_KEY_TOGGLE_FREELOOK 11
|
|
|
|
#define SFG_KEY_NEXT_WEAPON 12
|
|
|
|
#define SFG_KEY_PREVIOUS_WEAPON 13
|
|
|
|
#define SFG_KEY_MENU 14
|
|
|
|
#define SFG_KEY_CYCLE_WEAPON 15
|
|
|
|
|
|
|
|
#define SFG_KEY_COUNT 16 ///< Number of keys.
|
|
|
|
|
|
|
|
/* ============================= PORTING =================================== */
|
|
|
|
|
|
|
|
/* When porting, do the following:
|
|
|
|
- Include this file (and possibly other optional files, like sounds.h) in
|
|
|
|
your main_*.c frontend source.
|
|
|
|
- Implement the following functions in your frontend source.
|
|
|
|
- Call SFG_init() from your frontend initialization code.
|
|
|
|
- Call SFG_mainLoopBody() from within your frontend main loop.
|
|
|
|
|
|
|
|
If your platform is an AVR CPU (e.g. some Arduinos) and so has Harvard
|
|
|
|
architecture, define #SFG_AVR 1 before including this file in your frontend
|
|
|
|
source. */
|
|
|
|
|
|
|
|
#ifndef SFG_LOG
|
|
|
|
#define SFG_LOG(str) {} ///< Can be redefined to log game messages.
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifndef SFG_CPU_LOAD
|
|
|
|
#define SFG_CPU_LOAD(percent) {} ///< Can be redefined to check CPU load in %.
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifndef SFG_GAME_STEP_COMMAND
|
|
|
|
#define SFG_GAME_STEP_COMMAND {} /**< Will be called each simlation step (good
|
|
|
|
for creating deterministic behavior such as
|
|
|
|
demos (SFG_mainLoopBody() calls potentially
|
|
|
|
multiple simulation steps). */
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/**
|
|
|
|
Returns 1 (0) if given key is pressed (not pressed). At least the mandatory
|
|
|
|
keys have to be implemented, the optional keys don't have to ever return 1.
|
|
|
|
See the key constant definitions to see which ones are mandatory.
|
|
|
|
*/
|
|
|
|
int8_t SFG_keyPressed(uint8_t key);
|
|
|
|
|
|
|
|
/**
|
|
|
|
Optional function for mouse/joystick/analog controls, gets mouse x and y
|
|
|
|
offset in pixels from the game screen center (to achieve classic FPS mouse
|
|
|
|
controls the platform should center the mouse after this call). If the
|
|
|
|
platform isn't using a mouse, this function can simply return [0,0] offset at
|
|
|
|
each call, or even do nothing at all (leave the variables as are).
|
|
|
|
*/
|
|
|
|
void SFG_getMouseOffset(int16_t *x, int16_t *y);
|
|
|
|
|
|
|
|
/**
|
|
|
|
Returns time in milliseconds sice program start.
|
|
|
|
*/
|
|
|
|
uint32_t SFG_getTimeMs();
|
|
|
|
|
|
|
|
/**
|
|
|
|
Sleep (yield CPU) for specified amount of ms. This is used to relieve CPU
|
|
|
|
usage. If your platform doesn't need this or handles it in other way, this
|
|
|
|
function can do nothing.
|
|
|
|
*/
|
|
|
|
void SFG_sleepMs(uint16_t timeMs);
|
|
|
|
|
|
|
|
/**
|
|
|
|
Set specified screen pixel. ColorIndex is the index of the game's palette.
|
|
|
|
The function doesn't have to (and shouldn't, for the sake of performance)
|
|
|
|
check whether the coordinates are within screen bounds.
|
|
|
|
*/
|
|
|
|
static inline void SFG_setPixel(uint16_t x, uint16_t y, uint8_t colorIndex);
|
|
|
|
|
|
|
|
/**
|
|
|
|
Play given sound effect (SFX). This function may or may not use the sound
|
|
|
|
samples provided in sounds.h, and it may or may not ignore the (logarithmic)
|
|
|
|
volume parameter (0 to 255). Depending on the platform, the function can play
|
|
|
|
completely different samples or even e.g. just beeps. If the platform can't
|
|
|
|
play sounds, this function implementation can simply be left empty. This
|
|
|
|
function doesn't have to implement safety measures, the back end takes cares
|
|
|
|
of them.
|
|
|
|
*/
|
|
|
|
void SFG_playSound(uint8_t soundIndex, uint8_t volume);
|
|
|
|
|
|
|
|
#define SFG_MUSIC_TURN_OFF 0
|
|
|
|
#define SFG_MUSIC_TURN_ON 1
|
|
|
|
#define SFG_MUSIC_NEXT 2
|
|
|
|
|
|
|
|
/**
|
|
|
|
Informs the frontend how music should play, e.g. turn on/off, change track,
|
|
|
|
... See SFG_MUSIC_* constants. Playing music is optional and the frontend may
|
|
|
|
ignore this. If a frontend wants to implement music, it can use the bytebeat
|
|
|
|
provided in sounds.h or use its own.
|
|
|
|
*/
|
|
|
|
void SFG_setMusic(uint8_t value);
|
|
|
|
|
|
|
|
#define SFG_EVENT_VIBRATE 0 ///< the controller should vibrate (or blink etc.)
|
|
|
|
#define SFG_EVENT_PLAYER_HURT 1
|
|
|
|
#define SFG_EVENT_PLAYER_DIES 2
|
|
|
|
#define SFG_EVENT_LEVEL_STARTS 3
|
|
|
|
#define SFG_EVENT_LEVEL_WON 4
|
|
|
|
#define SFG_EVENT_MONSTER_DIES 5
|
|
|
|
#define SFG_EVENT_PLAYER_TAKES_ITEM 6
|
|
|
|
#define SFG_EVENT_EXPLOSION 7
|
|
|
|
#define SFG_EVENT_PLAYER_TELEPORTS 8
|
|
|
|
#define SFG_EVENT_PLAYER_CHANGES_WEAPON 9
|
|
|
|
|
|
|
|
/**
|
|
|
|
This is an optional function that informs the frontend about special events
|
|
|
|
which may trigger something special on the platform, such as a controller
|
|
|
|
vibration, logging etc. The implementation of this function may be left empty.
|
|
|
|
*/
|
|
|
|
void SFG_processEvent(uint8_t event, uint8_t data);
|
|
|
|
|
|
|
|
#define SFG_SAVE_SIZE 12 ///< size of the save in bytes
|
|
|
|
|
|
|
|
/**
|
|
|
|
Optional function for permanently saving the game state. Platforms that don't
|
|
|
|
have permanent storage (HDD, EEPROM etc.) may let this function simply do
|
|
|
|
nothing. If implemented, the function should save the passed data into its
|
|
|
|
permanent storage, e.g. a file, a cookie etc.
|
|
|
|
*/
|
|
|
|
void SFG_save(uint8_t data[SFG_SAVE_SIZE]);
|
|
|
|
|
|
|
|
/**
|
|
|
|
Optional function for retrieving game data that were saved to permanent
|
|
|
|
storage. Platforms without permanent storage may let this function do nothing.
|
|
|
|
If implemented, the function should fill the passed array with data from
|
|
|
|
permanent storage, e.g. a file, a cookie etc.
|
|
|
|
|
|
|
|
If this function is called before SFG_save was ever called and no data is
|
|
|
|
present in permanent memory, this function should do nothing (leave the data
|
|
|
|
array as is).
|
|
|
|
|
|
|
|
This function should return 1 if saving/loading is possible or 0 if not (this
|
|
|
|
will be used by the game to detect saving/loading capability).
|
|
|
|
*/
|
|
|
|
uint8_t SFG_load(uint8_t data[SFG_SAVE_SIZE]);
|
|
|
|
|
|
|
|
/* ========================================================================= */
|
|
|
|
|
|
|
|
/**
|
|
|
|
Main game loop body, call this inside your platform's specific main loop.
|
|
|
|
Returns 1 if the game continues or 0 if the game was exited and program should
|
|
|
|
halt. This functions handles reaching the target FPS and sleeping for
|
|
|
|
relieving CPU, so don't do this.
|
|
|
|
*/
|
|
|
|
uint8_t SFG_mainLoopBody();
|
|
|
|
|
|
|
|
/**
|
|
|
|
Initializes the game, call this in the platform's initialization code.
|
|
|
|
*/
|
|
|
|
void SFG_init();
|
|
|
|
|
|
|
|
#include "settings.h"
|
|
|
|
|
|
|
|
#if SFG_AVR
|
|
|
|
#include <avr/pgmspace.h>
|
|
|
|
|
|
|
|
#define SFG_PROGRAM_MEMORY const PROGMEM
|
|
|
|
#define SFG_PROGRAM_MEMORY_U8(addr) pgm_read_byte(addr)
|
|
|
|
#else
|
|
|
|
#define SFG_PROGRAM_MEMORY static const
|
|
|
|
#define SFG_PROGRAM_MEMORY_U8(addr) ((uint8_t) (*(addr)))
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include "images.h" // don't change the order of these includes
|
|
|
|
#include "levels.h"
|
|
|
|
#include "texts.h"
|
|
|
|
#include "palette.h"
|
|
|
|
|
|
|
|
#if SFG_TEXTURE_DISTANCE == 0
|
|
|
|
#define RCL_COMPUTE_WALL_TEXCOORDS 0
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#define RCL_PIXEL_FUNCTION SFG_pixelFunc
|
|
|
|
#define RCL_TEXTURE_VERTICAL_STRETCH 0
|
|
|
|
|
|
|
|
#define RCL_CAMERA_COLL_HEIGHT_BELOW 800
|
|
|
|
#define RCL_CAMERA_COLL_HEIGHT_ABOVE 200
|
|
|
|
|
|
|
|
#define RCL_HORIZONTAL_FOV SFG_FOV_HORIZONTAL
|
|
|
|
#define RCL_VERTICAL_FOV SFG_FOV_VERTICAL
|
|
|
|
|
|
|
|
#include "raycastlib.h"
|
|
|
|
|
|
|
|
#include "constants.h"
|
|
|
|
|
|
|
|
typedef struct
|
|
|
|
{
|
|
|
|
uint8_t coords[2];
|
|
|
|
uint8_t state; /**< door state in format:
|
|
|
|
|
|
|
|
MSB ccbaaaaa LSB
|
|
|
|
|
|
|
|
aaaaa: current door height (how much they're open)
|
|
|
|
b: whether currently going up (0) or down (1)
|
|
|
|
cc: by which card (key) the door is unlocked, 00
|
|
|
|
means no card (unlocked), 1 means card 0 etc. */
|
|
|
|
} SFG_DoorRecord;
|
|
|
|
|
|
|
|
#define SFG_SPRITE_SIZE(size0to3) \
|
|
|
|
(((size0to3 + 3) * SFG_BASE_SPRITE_SIZE) / 4)
|
|
|
|
|
|
|
|
#define SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(size0to3) \
|
|
|
|
(SFG_SPRITE_SIZE(size0to3) / 2)
|
|
|
|
|
|
|
|
#define SFG_SPRITE_SIZE_PIXELS(size0to3) \
|
|
|
|
((SFG_SPRITE_SIZE(size0to3) * SFG_SPRITE_MAX_SIZE) / RCL_UNITS_PER_SQUARE)
|
|
|
|
|
|
|
|
/**
|
|
|
|
Holds information about one instance of a level item (a type of level element,
|
|
|
|
e.g. pickable items, decorations etc.). The format is following:
|
|
|
|
|
|
|
|
MSB abbbbbbb LSB
|
|
|
|
|
|
|
|
a: active flag, 1 means the item is nearby to player and is active
|
|
|
|
bbbbbbb: index to elements array of the current level, pointing to element
|
|
|
|
representing this item
|
|
|
|
*/
|
|
|
|
typedef uint8_t SFG_ItemRecord;
|
|
|
|
|
|
|
|
#define SFG_ITEM_RECORD_ACTIVE_MASK 0x80
|
|
|
|
|
|
|
|
#define SFG_ITEM_RECORD_LEVEL_ELEMENT(itemRecord) \
|
|
|
|
(SFG_currentLevel.levelPointer->elements[itemRecord & \
|
|
|
|
~SFG_ITEM_RECORD_ACTIVE_MASK])
|
|
|
|
|
|
|
|
typedef struct
|
|
|
|
{
|
|
|
|
uint8_t stateType; /**< Holds state (lower 4 bits) and type of monster (upper
|
|
|
|
4 bits). */
|
|
|
|
uint8_t coords[2]; /**< monster position, in 1/4s of a square */
|
|
|
|
uint8_t health;
|
|
|
|
} SFG_MonsterRecord;
|
|
|
|
|
|
|
|
#define SFG_MR_STATE(mr) ((mr).stateType & SFG_MONSTER_MASK_STATE)
|
|
|
|
#define SFG_MR_TYPE(mr) \
|
|
|
|
(SFG_MONSTER_INDEX_TO_TYPE(((mr).stateType & SFG_MONSTER_MASK_TYPE) >> 4))
|
|
|
|
|
|
|
|
#define SFG_MONSTER_COORD_TO_RCL_UNITS(c) ((RCL_UNITS_PER_SQUARE / 8) + c * 256)
|
|
|
|
#define SFG_MONSTER_COORD_TO_SQUARES(c) (c / 4)
|
|
|
|
|
|
|
|
#define SFG_ELEMENT_COORD_TO_RCL_UNITS(c) \
|
|
|
|
(c * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2)
|
|
|
|
|
|
|
|
#define SFG_MONSTER_MASK_STATE 0x0f
|
|
|
|
#define SFG_MONSTER_MASK_TYPE 0xf0
|
|
|
|
|
|
|
|
#define SFG_MONSTER_STATE_INACTIVE 0 ///< Not nearby, not actively updated.
|
|
|
|
#define SFG_MONSTER_STATE_IDLE 1
|
|
|
|
#define SFG_MONSTER_STATE_ATTACKING 2
|
|
|
|
#define SFG_MONSTER_STATE_HURTING 3
|
|
|
|
#define SFG_MONSTER_STATE_DYING 4
|
|
|
|
#define SFG_MONSTER_STATE_GOING_N 5
|
|
|
|
#define SFG_MONSTER_STATE_GOING_NE 6
|
|
|
|
#define SFG_MONSTER_STATE_GOING_E 7
|
|
|
|
#define SFG_MONSTER_STATE_GOING_SE 8
|
|
|
|
#define SFG_MONSTER_STATE_GOING_S 9
|
|
|
|
#define SFG_MONSTER_STATE_GOING_SW 10
|
|
|
|
#define SFG_MONSTER_STATE_GOING_W 11
|
|
|
|
#define SFG_MONSTER_STATE_GOING_NW 12
|
|
|
|
#define SFG_MONSTER_STATE_DEAD 13
|
|
|
|
|
|
|
|
typedef struct
|
|
|
|
{
|
|
|
|
uint8_t type;
|
|
|
|
uint8_t doubleFramesToLive; /**< This number times two (because 255 could be
|
|
|
|
too little at high FPS) says after how many
|
|
|
|
frames the projectile is destroyed. */
|
|
|
|
uint16_t position[3]; /**< Current position, stored as u16 to save space, as
|
|
|
|
that is exactly enough to store position on 64x64
|
|
|
|
map. */
|
|
|
|
int16_t direction[3]; /**< Added to position each game step. */
|
|
|
|
} SFG_ProjectileRecord;
|
|
|
|
|
|
|
|
#define SFG_GAME_STATE_INIT 0 ///< first state, waiting for key releases
|
|
|
|
#define SFG_GAME_STATE_PLAYING 1
|
|
|
|
#define SFG_GAME_STATE_WIN 2
|
|
|
|
#define SFG_GAME_STATE_LOSE 3
|
|
|
|
#define SFG_GAME_STATE_INTRO 4
|
|
|
|
#define SFG_GAME_STATE_OUTRO 5
|
|
|
|
#define SFG_GAME_STATE_MAP 6
|
|
|
|
#define SFG_GAME_STATE_LEVEL_START 7
|
|
|
|
#define SFG_GAME_STATE_MENU 8
|
|
|
|
|
|
|
|
#define SFG_MENU_ITEM_CONTINUE 0
|
|
|
|
#define SFG_MENU_ITEM_MAP 1
|
|
|
|
#define SFG_MENU_ITEM_PLAY 2
|
|
|
|
#define SFG_MENU_ITEM_LOAD 3
|
|
|
|
#define SFG_MENU_ITEM_SOUND 4
|
|
|
|
#define SFG_MENU_ITEM_SHEAR 5
|
|
|
|
#define SFG_MENU_ITEM_EXIT 6
|
|
|
|
|
|
|
|
#define SFG_MENU_ITEM_NONE 255
|
|
|
|
|
|
|
|
/*
|
|
|
|
GLOBAL VARIABLES
|
|
|
|
===============================================================================
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
Groups global variables related to the game as such in a single struct. There
|
|
|
|
are still other global structs for player, level etc.
|
|
|
|
*/
|
|
|
|
struct
|
|
|
|
{
|
|
|
|
uint8_t state; ///< Current game state.
|
|
|
|
uint32_t stateTime; ///< Time in ms from last state change.
|
|
|
|
uint8_t currentRandom; ///< for RNG
|
|
|
|
uint8_t spriteAnimationFrame;
|
|
|
|
uint8_t soundsPlayedThisFrame; /**< Each bit says whether given sound was
|
|
|
|
played this frame, prevents playing too many
|
|
|
|
sounds at once. */
|
|
|
|
RCL_RayConstraints rayConstraints; ///< Ray constraints for rendering.
|
|
|
|
RCL_RayConstraints visibilityRayConstraints; ///< Constraints for visibility.
|
|
|
|
uint8_t keyStates[SFG_KEY_COUNT]; /**< Pressed states of keys, each value
|
|
|
|
stores the number of frames for which the
|
|
|
|
key has been held. */
|
|
|
|
uint8_t zBuffer[SFG_Z_BUFFER_SIZE];
|
|
|
|
uint8_t textureAverageColors[SFG_WALL_TEXTURE_COUNT]; /**< Contains average
|
|
|
|
color for each wall texture. */
|
|
|
|
int8_t backgroundScaleMap[SFG_GAME_RESOLUTION_Y];
|
|
|
|
uint16_t backgroundScroll;
|
|
|
|
uint8_t spriteSamplingPoints[SFG_MAX_SPRITE_SIZE]; /**< Helper for
|
|
|
|
precomputing sprite
|
|
|
|
sampling positions for
|
|
|
|
drawing. */
|
|
|
|
uint32_t frameTime; ///< time (in ms) of the current frame start
|
|
|
|
uint32_t frame; ///< frame number
|
|
|
|
uint8_t selectedMenuItem;
|
|
|
|
uint8_t selectedLevel; ///< level to play selected in the main menu
|
|
|
|
uint8_t antiSpam; ///< Prevents log message spamming.
|
|
|
|
uint8_t settings; /**< dynamic game settings (can be changed at runtime),
|
|
|
|
bit meaning:
|
|
|
|
|
|
|
|
MSB -------- LSB
|
|
|
|
||||
|
|
|
|
|||\_ sound (SFX)
|
|
|
|
||\__ music
|
|
|
|
|\___ shearing
|
|
|
|
\____ freelook (shearing not sliding back) */
|
|
|
|
uint8_t blink; ///< Says whether blinkg is currently on or off.
|
|
|
|
uint8_t saved; /**< Helper variable to know if game was saved. Can be
|
|
|
|
0 (not saved), 1 (just saved) or 255 (can't save).*/
|
|
|
|
uint8_t cheatState; /**< Highest bit say whether cheat is enabled, other bits
|
|
|
|
represent the state of typing the cheat code. */
|
|
|
|
uint8_t save[SFG_SAVE_SIZE]; /**< Stores the game save state that's kept in
|
|
|
|
the persistent memory.
|
|
|
|
|
|
|
|
The save format is binary and platform independent.
|
|
|
|
The save contains game settings, game progress and a
|
|
|
|
saved position. The format is as follows:
|
|
|
|
|
|
|
|
0 4b (less signif.) highest level that has been reached
|
|
|
|
0 4b (more signif.) level number of the saved position (0: no save)
|
|
|
|
1 8b game settings (SFG_game.settings)
|
|
|
|
2 8b health at saved position
|
|
|
|
3 8b bullet ammo at saved position
|
|
|
|
4 8b rocket ammo at saved position
|
|
|
|
5 8b plasma ammo at saved position
|
|
|
|
6 32b little endian total play time, in 10ths of sec
|
|
|
|
10 16b little endian total enemies killed from start */
|
|
|
|
uint8_t continues; ///< Whether the game continues or was exited.
|
|
|
|
} SFG_game;
|
|
|
|
|
|
|
|
#define SFG_SAVE_TOTAL_TIME (SFG_game.save[6] + SFG_game.save[7] * 256 + \
|
|
|
|
SFG_game.save[8] * 65536 + SFG_game.save[9] * 4294967296)
|
|
|
|
|
|
|
|
/**
|
|
|
|
Stores player state.
|
|
|
|
*/
|
|
|
|
struct
|
|
|
|
{
|
|
|
|
RCL_Camera camera;
|
|
|
|
int8_t squarePosition[2];
|
|
|
|
RCL_Vector2D direction;
|
|
|
|
RCL_Unit verticalSpeed;
|
|
|
|
RCL_Unit previousVerticalSpeed; /**< Vertical speed in previous frame, needed
|
|
|
|
for determining whether player is in the
|
|
|
|
air. */
|
|
|
|
uint16_t headBobFrame;
|
|
|
|
uint8_t weapon; ///< currently selected weapon
|
|
|
|
uint8_t health;
|
|
|
|
uint32_t weaponCooldownFrames; ///< frames left for weapon cooldown
|
|
|
|
uint32_t lastHurtFrame;
|
|
|
|
uint32_t lastItemTakenFrame;
|
|
|
|
uint8_t ammo[SFG_AMMO_TOTAL];
|
|
|
|
uint8_t cards; /**< Lowest 3 bits say which access cards
|
|
|
|
have been taken, the next 3 bits say
|
|
|
|
which cards should be blinking in the HUD,
|
|
|
|
the last 2 bits are a blink reset counter. */
|
|
|
|
uint8_t justTeleported;
|
|
|
|
int8_t previousWeaponDirection; ///< Direction (+/0/-) of previous weapon.
|
|
|
|
} SFG_player;
|
|
|
|
|
|
|
|
/**
|
|
|
|
Stores the current level and helper precomputed values for better performance.
|
|
|
|
*/
|
|
|
|
struct
|
|
|
|
{
|
|
|
|
const SFG_Level *levelPointer;
|
|
|
|
uint8_t levelNumber;
|
|
|
|
const uint8_t* textures[7]; ///< textures the level is using
|
|
|
|
uint32_t timeStart;
|
|
|
|
uint32_t frameStart;
|
|
|
|
uint32_t completionTime10sOfS; ///< completion time in 10ths of second
|
|
|
|
uint8_t floorColor;
|
|
|
|
uint8_t ceilingColor;
|
|
|
|
|
|
|
|
SFG_DoorRecord doorRecords[SFG_MAX_DOORS];
|
|
|
|
uint8_t doorRecordCount;
|
|
|
|
uint8_t checkedDoorIndex; ///< Says which door are currently being checked.
|
|
|
|
|
|
|
|
SFG_ItemRecord itemRecords[SFG_MAX_ITEMS]; ///< Holds level items.
|
|
|
|
uint8_t itemRecordCount;
|
|
|
|
uint8_t checkedItemIndex; ///< Same as checkedDoorIndex, but for items.
|
|
|
|
|
|
|
|
SFG_MonsterRecord monsterRecords[SFG_MAX_MONSTERS];
|
|
|
|
uint8_t monsterRecordCount;
|
|
|
|
uint8_t checkedMonsterIndex;
|
|
|
|
|
|
|
|
SFG_ProjectileRecord projectileRecords[SFG_MAX_PROJECTILES];
|
|
|
|
uint8_t projectileRecordCount;
|
|
|
|
uint8_t bossCount;
|
|
|
|
uint8_t monstersDead;
|
|
|
|
uint8_t backgroundImage;
|
|
|
|
uint8_t teleporterCount;
|
|
|
|
uint16_t mapRevealMask; /**< Bits say which parts of the map have been
|
|
|
|
revealed. */
|
|
|
|
uint8_t itemCollisionMap[(SFG_MAP_SIZE * SFG_MAP_SIZE) / 8];
|
|
|
|
/**< Bit array, for each map square says whether there
|
|
|
|
is a colliding item or not. */
|
|
|
|
} SFG_currentLevel;
|
|
|
|
|
|
|
|
#if SFG_AVR
|
|
|
|
/**
|
|
|
|
Copy of the current level that is stored in RAM. This is only done on Arduino
|
|
|
|
because accessing it in program memory (PROGMEM) directly would be a pain.
|
|
|
|
Because of this Arduino needs more RAM.
|
|
|
|
*/
|
|
|
|
SFG_Level SFG_ramLevel;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/**
|
|
|
|
Helper function for accessing the itemCollisionMap bits.
|
|
|
|
*/
|
|
|
|
void SFG_getItemCollisionMapIndex(
|
|
|
|
uint8_t x, uint8_t y, uint16_t *byte, uint8_t *bit)
|
|
|
|
{
|
|
|
|
uint16_t index = y * SFG_MAP_SIZE + x;
|
|
|
|
|
|
|
|
*byte = index / 8;
|
|
|
|
*bit = index % 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SFG_setItemCollisionMapBit(uint8_t x, uint8_t y, uint8_t value)
|
|
|
|
{
|
|
|
|
uint16_t byte;
|
|
|
|
uint8_t bit;
|
|
|
|
|
|
|
|
SFG_getItemCollisionMapIndex(x,y,&byte,&bit);
|
|
|
|
|
|
|
|
SFG_currentLevel.itemCollisionMap[byte] &= ~(0x01 << bit);
|
|
|
|
SFG_currentLevel.itemCollisionMap[byte] |= (value & 0x01) << bit;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t SFG_getItemCollisionMapBit(uint8_t x, uint8_t y)
|
|
|
|
{
|
|
|
|
uint16_t byte;
|
|
|
|
uint8_t bit;
|
|
|
|
|
|
|
|
SFG_getItemCollisionMapIndex(x,y,&byte,&bit);
|
|
|
|
return (SFG_currentLevel.itemCollisionMap[byte] >> bit) & 0x01;
|
|
|
|
}
|
|
|
|
|
|
|
|
#if SFG_DITHERED_SHADOW
|
|
|
|
static const uint8_t SFG_ditheringPatterns[] =
|
|
|
|
{
|
|
|
|
0,0,0,0,
|
|
|
|
0,0,0,0,
|
|
|
|
|
|
|
|
0,0,0,0,
|
|
|
|
0,1,0,0,
|
|
|
|
|
|
|
|
0,0,0,0,
|
|
|
|
0,1,0,1,
|
|
|
|
|
|
|
|
1,0,1,0,
|
|
|
|
0,1,0,0,
|
|
|
|
|
|
|
|
1,0,1,0,
|
|
|
|
0,1,0,1,
|
|
|
|
|
|
|
|
1,0,1,0,
|
|
|
|
0,1,1,1,
|
|
|
|
|
|
|
|
1,1,1,1,
|
|
|
|
0,1,0,1,
|
|
|
|
|
|
|
|
1,1,1,1,
|
|
|
|
0,1,1,1,
|
|
|
|
|
|
|
|
1,1,1,1,
|
|
|
|
1,1,1,1
|
|
|
|
};
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/*
|
|
|
|
FUNCTIONS
|
|
|
|
===============================================================================
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
Returns a pseudorandom byte. This is a very simple congruent generator, its
|
|
|
|
parameters have been chosen so that each number (0-255) is included in the
|
|
|
|
output exactly once!
|
|
|
|
*/
|
|
|
|
uint8_t SFG_random()
|
|
|
|
{
|
|
|
|
SFG_game.currentRandom *= 13;
|
|
|
|
SFG_game.currentRandom += 7;
|
|
|
|
|
|
|
|
return SFG_game.currentRandom;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SFG_playGameSound(uint8_t soundIndex, uint8_t volume)
|
|
|
|
{
|
|
|
|
if (!(SFG_game.settings & 0x01))
|
|
|
|
return;
|
|
|
|
|
|
|
|
uint8_t mask = 0x01 << soundIndex;
|
|
|
|
|
|
|
|
if (!(SFG_game.soundsPlayedThisFrame & mask))
|
|
|
|
{
|
|
|
|
SFG_playSound(soundIndex,volume);
|
|
|
|
SFG_game.soundsPlayedThisFrame |= mask;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Returns a damage value for specific attack type (SFG_WEAPON_FIRE_TYPE_...),
|
|
|
|
with added randomness (so the values will differ). For explosion pass
|
|
|
|
SFG_WEAPON_FIRE_TYPE_FIREBALL.
|
|
|
|
*/
|
|
|
|
uint8_t SFG_getDamageValue(uint8_t attackType)
|
|
|
|
{
|
|
|
|
if (attackType >= SFG_WEAPON_FIRE_TYPES_TOTAL)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
int32_t value = SFG_attackDamageTable[attackType]; // has to be signed
|
|
|
|
int32_t maxAdd = (value * SFG_DAMAGE_RANDOMNESS) / 256;
|
|
|
|
|
|
|
|
value = value + (maxAdd / 2) - (SFG_random() * maxAdd / 256);
|
|
|
|
|
|
|
|
if (value < 0)
|
|
|
|
value = 0;
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Saves game data to persistent storage.
|
|
|
|
*/
|
|
|
|
void SFG_gameSave()
|
|
|
|
{
|
|
|
|
if (SFG_game.saved == SFG_CANT_SAVE)
|
|
|
|
return;
|
|
|
|
|
|
|
|
SFG_LOG("saving game data");
|
|
|
|
|
|
|
|
SFG_save(SFG_game.save);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Loads game data from persistent storage.
|
|
|
|
*/
|
|
|
|
void SFG_gameLoad()
|
|
|
|
{
|
|
|
|
if (SFG_game.saved == SFG_CANT_SAVE)
|
|
|
|
return;
|
|
|
|
|
|
|
|
SFG_LOG("loading game data");
|
|
|
|
|
|
|
|
uint8_t result = SFG_load(SFG_game.save);
|
|
|
|
|
|
|
|
if (result == 0)
|
|
|
|
SFG_game.saved = SFG_CANT_SAVE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Returns ammo type for given weapon.
|
|
|
|
*/
|
|
|
|
uint8_t SFG_weaponAmmo(uint8_t weapon)
|
|
|
|
{
|
|
|
|
if (weapon == SFG_WEAPON_KNIFE)
|
|
|
|
return SFG_AMMO_NONE;
|
|
|
|
if (weapon == SFG_WEAPON_MACHINE_GUN ||
|
|
|
|
weapon == SFG_WEAPON_SHOTGUN)
|
|
|
|
return SFG_AMMO_BULLETS;
|
|
|
|
else if (weapon == SFG_WEAPON_ROCKET_LAUNCHER)
|
|
|
|
return SFG_AMMO_ROCKETS;
|
|
|
|
else
|
|
|
|
return SFG_AMMO_PLASMA;
|
|
|
|
}
|
|
|
|
|
|
|
|
RCL_Unit SFG_taxicabDistance(
|
|
|
|
RCL_Unit x0, RCL_Unit y0, RCL_Unit z0, RCL_Unit x1, RCL_Unit y1, RCL_Unit z1)
|
|
|
|
{
|
|
|
|
return (RCL_abs(x0 - x1) + RCL_abs(y0 - y1) + RCL_abs(z0 - z1));
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t SFG_isInActiveDistanceFromPlayer(RCL_Unit x, RCL_Unit y, RCL_Unit z)
|
|
|
|
{
|
|
|
|
return SFG_taxicabDistance(
|
|
|
|
x,y,z,SFG_player.camera.position.x,SFG_player.camera.position.y,
|
|
|
|
SFG_player.camera.height) <= SFG_LEVEL_ELEMENT_ACTIVE_DISTANCE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Function called when a level end to compute the stats etc.
|
|
|
|
*/
|
|
|
|
void SFG_levelEnds()
|
|
|
|
{
|
|
|
|
SFG_currentLevel.completionTime10sOfS = (SFG_MS_PER_FRAME *
|
|
|
|
(SFG_game.frame - SFG_currentLevel.frameStart)) / 100;
|
|
|
|
|
|
|
|
if (
|
|
|
|
(SFG_player.health != 0) &&
|
|
|
|
(SFG_currentLevel.levelNumber >= (SFG_game.save[0] & 0x0f)) &&
|
|
|
|
((SFG_currentLevel.levelNumber + 1) < SFG_NUMBER_OF_LEVELS))
|
|
|
|
{
|
|
|
|
SFG_game.save[0] = // save progress
|
|
|
|
(SFG_game.save[0] & 0xf0) | (SFG_currentLevel.levelNumber + 1);
|
|
|
|
|
|
|
|
SFG_gameSave();
|
|
|
|
}
|
|
|
|
|
|
|
|
SFG_currentLevel.monstersDead = 0;
|
|
|
|
|
|
|
|
for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
|
|
|
|
if (SFG_currentLevel.monsterRecords[i].health == 0)
|
|
|
|
SFG_currentLevel.monstersDead++;
|
|
|
|
|
|
|
|
uint32_t totalTime = SFG_SAVE_TOTAL_TIME;
|
|
|
|
|
|
|
|
if ((SFG_currentLevel.levelNumber == 0) || (totalTime != 0))
|
|
|
|
{
|
|
|
|
SFG_LOG("Updating save totals.");
|
|
|
|
|
|
|
|
totalTime += SFG_currentLevel.completionTime10sOfS;
|
|
|
|
|
|
|
|
for (uint8_t i = 0; i < 4; ++i)
|
|
|
|
{
|
|
|
|
SFG_game.save[6 + i] = totalTime % 256;
|
|
|
|
totalTime /= 256;
|
|
|
|
}
|
|
|
|
|
|
|
|
SFG_game.save[10] += SFG_currentLevel.monstersDead % 256;
|
|
|
|
SFG_game.save[11] += SFG_currentLevel.monstersDead / 256;
|
|
|
|
}
|
|
|
|
|
|
|
|
SFG_game.save[2] = SFG_player.health;
|
|
|
|
SFG_game.save[3] = SFG_player.ammo[0];
|
|
|
|
SFG_game.save[4] = SFG_player.ammo[1];
|
|
|
|
SFG_game.save[5] = SFG_player.ammo[2];
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline uint8_t SFG_RCLUnitToZBuffer(RCL_Unit x)
|
|
|
|
{
|
|
|
|
x /= (RCL_UNITS_PER_SQUARE / 8);
|
|
|
|
|
|
|
|
uint8_t okay = x < 256;
|
|
|
|
|
|
|
|
return okay * (x + 1) - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
const uint8_t *SFG_getMonsterSprite(
|
|
|
|
uint8_t monsterType, uint8_t state, uint8_t frame)
|
|
|
|
{
|
|
|
|
uint8_t index =
|
|
|
|
state == SFG_MONSTER_STATE_DEAD ? 18 : 17;
|
|
|
|
// ^ makes the compiled binary smaller compared to returning pointers directly
|
|
|
|
|
|
|
|
if ((state != SFG_MONSTER_STATE_DYING) && (state != SFG_MONSTER_STATE_DEAD))
|
|
|
|
switch (monsterType)
|
|
|
|
{
|
|
|
|
case SFG_LEVEL_ELEMENT_MONSTER_SPIDER:
|
|
|
|
switch (state)
|
|
|
|
{
|
|
|
|
case SFG_MONSTER_STATE_ATTACKING: index = 1; break;
|
|
|
|
case SFG_MONSTER_STATE_IDLE: index = 0; break;
|
|
|
|
default: index = frame ? 0 : 2; break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SFG_LEVEL_ELEMENT_MONSTER_WARRIOR:
|
|
|
|
index = state != SFG_MONSTER_STATE_ATTACKING ? 6 : 7;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SFG_LEVEL_ELEMENT_MONSTER_DESTROYER:
|
|
|
|
switch (state)
|
|
|
|
{
|
|
|
|
case SFG_MONSTER_STATE_ATTACKING: index = 4; break;
|
|
|
|
case SFG_MONSTER_STATE_IDLE: index = 3; break;
|
|
|
|
default: index = frame ? 3 : 5; break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SFG_LEVEL_ELEMENT_MONSTER_PLASMABOT:
|
|
|
|
index = state != SFG_MONSTER_STATE_ATTACKING ? 8 : 9;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SFG_LEVEL_ELEMENT_MONSTER_ENDER:
|
|
|
|
switch (state)
|
|
|
|
{
|
|
|
|
case SFG_MONSTER_STATE_ATTACKING: index = 12; break;
|
|
|
|
case SFG_MONSTER_STATE_IDLE: index = 10; break;
|
|
|
|
default: index = frame ? 10 : 11; break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SFG_LEVEL_ELEMENT_MONSTER_TURRET:
|
|
|
|
switch (state)
|
|
|
|
{
|
|
|
|
case SFG_MONSTER_STATE_ATTACKING: index = 15; break;
|
|
|
|
case SFG_MONSTER_STATE_IDLE: index = 13; break;
|
|
|
|
default: index = frame ? 13 : 14; break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SFG_LEVEL_ELEMENT_MONSTER_EXPLODER:
|
|
|
|
default:
|
|
|
|
index = 16;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return SFG_monsterSprites + index * SFG_TEXTURE_STORE_SIZE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Says whether given key is currently pressed (down). This should be preferred
|
|
|
|
to SFG_keyPressed().
|
|
|
|
*/
|
|
|
|
uint8_t SFG_keyIsDown(uint8_t key)
|
|
|
|
{
|
|
|
|
return SFG_game.keyStates[key] != 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Says whether given key has been pressed in the current frame.
|
|
|
|
*/
|
|
|
|
uint8_t SFG_keyJustPressed(uint8_t key)
|
|
|
|
{
|
|
|
|
return (SFG_game.keyStates[key]) == 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Says whether a key is being repeated after being held for certain time.
|
|
|
|
*/
|
|
|
|
uint8_t SFG_keyRepeated(uint8_t key)
|
|
|
|
{
|
|
|
|
return
|
|
|
|
((SFG_game.keyStates[key] >= SFG_KEY_REPEAT_DELAY_FRAMES) ||
|
|
|
|
(SFG_game.keyStates[key] == 255)) &&
|
|
|
|
(SFG_game.frame % SFG_KEY_REPEAT_PERIOD_FRAMES == 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t SFG_keyRegisters(uint8_t key)
|
|
|
|
{
|
|
|
|
return SFG_keyJustPressed(key) || SFG_keyRepeated(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
#if SFG_RESOLUTION_SCALEDOWN == 1
|
|
|
|
#define SFG_setGamePixel SFG_setPixel
|
|
|
|
#else
|
|
|
|
|
|
|
|
/**
|
|
|
|
Sets the game pixel (a pixel that can potentially be bigger than the screen
|
|
|
|
pixel).
|
|
|
|
*/
|
|
|
|
static inline void SFG_setGamePixel(uint16_t x, uint16_t y, uint8_t colorIndex)
|
|
|
|
{
|
|
|
|
uint16_t screenY = y * SFG_RESOLUTION_SCALEDOWN;
|
|
|
|
uint16_t screenX = x * SFG_RESOLUTION_SCALEDOWN;
|
|
|
|
|
|
|
|
for (uint16_t j = screenY; j < screenY + SFG_RESOLUTION_SCALEDOWN; ++j)
|
|
|
|
for (uint16_t i = screenX; i < screenX + SFG_RESOLUTION_SCALEDOWN; ++i)
|
|
|
|
SFG_setPixel(i,j,colorIndex);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
void SFG_recomputePLayerDirection()
|
|
|
|
{
|
|
|
|
SFG_player.camera.direction =
|
|
|
|
RCL_wrap(SFG_player.camera.direction,RCL_UNITS_PER_SQUARE);
|
|
|
|
|
|
|
|
SFG_player.direction = RCL_angleToDirection(SFG_player.camera.direction);
|
|
|
|
|
|
|
|
SFG_player.direction.x =
|
|
|
|
(SFG_player.direction.x * SFG_PLAYER_MOVE_UNITS_PER_FRAME)
|
|
|
|
/ RCL_UNITS_PER_SQUARE;
|
|
|
|
|
|
|
|
SFG_player.direction.y =
|
|
|
|
(SFG_player.direction.y * SFG_PLAYER_MOVE_UNITS_PER_FRAME)
|
|
|
|
/ RCL_UNITS_PER_SQUARE;
|
|
|
|
|
|
|
|
SFG_game.backgroundScroll =
|
|
|
|
((SFG_player.camera.direction * 8) * SFG_GAME_RESOLUTION_Y)
|
|
|
|
/ RCL_UNITS_PER_SQUARE;
|
|
|
|
}
|
|
|
|
|
|
|
|
#if SFG_BACKGROUND_BLUR != 0
|
|
|
|
uint8_t SFG_backgroundBlurIndex = 0;
|
|
|
|
|
|
|
|
static const int8_t SFG_backgroundBlurOffsets[8] =
|
|
|
|
{
|
|
|
|
0 * SFG_BACKGROUND_BLUR,
|
|
|
|
16 * SFG_BACKGROUND_BLUR,
|
|
|
|
7 * SFG_BACKGROUND_BLUR,
|
|
|
|
17 * SFG_BACKGROUND_BLUR,
|
|
|
|
1 * SFG_BACKGROUND_BLUR,
|
|
|
|
4 * SFG_BACKGROUND_BLUR,
|
|
|
|
15 * SFG_BACKGROUND_BLUR,
|
|
|
|
9 * SFG_BACKGROUND_BLUR,
|
|
|
|
};
|
|
|
|
#endif
|
|
|
|
|
|
|
|
static inline uint8_t SFG_fogValueDiminish(RCL_Unit depth)
|
|
|
|
{
|
|
|
|
return depth / SFG_FOG_DIMINISH_STEP;
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline uint8_t
|
|
|
|
SFG_getTexelFull(uint8_t textureIndex,RCL_Unit u, RCL_Unit v)
|
|
|
|
{
|
|
|
|
return
|
|
|
|
SFG_getTexel(
|
|
|
|
textureIndex != 255 ?
|
|
|
|
SFG_currentLevel.textures[textureIndex] :
|
|
|
|
(SFG_wallTextures + SFG_currentLevel.levelPointer->doorTextureIndex
|
|
|
|
* SFG_TEXTURE_STORE_SIZE),
|
|
|
|
u / (RCL_UNITS_PER_SQUARE / SFG_TEXTURE_SIZE),
|
|
|
|
v / (RCL_UNITS_PER_SQUARE / SFG_TEXTURE_SIZE));
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline uint8_t SFG_getTexelAverage(uint8_t textureIndex)
|
|
|
|
{
|
|
|
|
return
|
|
|
|
textureIndex != 255 ?
|
|
|
|
SFG_game.textureAverageColors[
|
|
|
|
SFG_currentLevel.levelPointer->textureIndices[textureIndex]]
|
|
|
|
:
|
|
|
|
(
|
|
|
|
SFG_game.textureAverageColors[
|
|
|
|
SFG_currentLevel.levelPointer->doorTextureIndex]
|
|
|
|
+ 1 // to distinguish from normal walls
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SFG_pixelFunc(RCL_PixelInfo *pixel)
|
|
|
|
{
|
|
|
|
uint8_t color;
|
|
|
|
uint8_t shadow = 0;
|
|
|
|
|
|
|
|
if (pixel->isHorizon && pixel->depth > RCL_UNITS_PER_SQUARE * 16)
|
|
|
|
{
|
|
|
|
color = SFG_TRANSPARENT_COLOR;
|
|
|
|
}
|
|
|
|
else if (pixel->isWall)
|
|
|
|