mirror of https://gitlab.com/drummyfish/anarch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5024 lines
138 KiB
5024 lines
138 KiB
/** |
|
@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) |
|
{ |
|
uint8_t textureIndex = |
|
pixel->isFloor ? |
|
( |
|
((pixel->hit.type & SFG_TILE_PROPERTY_MASK) != SFG_TILE_PROPERTY_DOOR) ? |
|
(pixel->hit.type & 0x7) |
|
: |
|
( |
|
(pixel->texCoords.y > RCL_UNITS_PER_SQUARE) ? |
|
(pixel->hit.type & 0x7) : 255 |
|
) |
|
): |
|
((pixel->hit.type & 0x38) >> 3); |
|
|
|
#if SFG_TEXTURE_DISTANCE != 0 |
|
RCL_Unit textureV = pixel->texCoords.y; |
|
|
|
if ((pixel->hit.type & SFG_TILE_PROPERTY_MASK) == |
|
SFG_TILE_PROPERTY_SQUEEZER) |
|
textureV += pixel->wallHeight; |
|
#endif |
|
|
|
color = |
|
textureIndex != SFG_TILE_TEXTURE_TRANSPARENT ? |
|
( |
|
#if SFG_TEXTURE_DISTANCE >= 65535 |
|
SFG_getTexelFull(textureIndex,pixel->texCoords.x,textureV) |
|
#elif SFG_TEXTURE_DISTANCE == 0 |
|
SFG_getTexelAverage(textureIndex) |
|
#else |
|
pixel->depth <= SFG_TEXTURE_DISTANCE ? |
|
SFG_getTexelFull(textureIndex,pixel->texCoords.x,textureV) : |
|
SFG_getTexelAverage(textureIndex) |
|
#endif |
|
) |
|
: |
|
SFG_TRANSPARENT_COLOR; |
|
|
|
shadow = pixel->hit.direction >> 1; |
|
} |
|
else // floor/ceiling |
|
{ |
|
color = pixel->isFloor ? |
|
( |
|
#if SFG_DIFFERENT_FLOOR_CEILING_COLORS |
|
2 + (pixel->height / SFG_WALL_HEIGHT_STEP) % 4 |
|
#else |
|
SFG_currentLevel.floorColor |
|
#endif |
|
) : |
|
(pixel->height < SFG_CEILING_MAX_HEIGHT ? |
|
( |
|
#if SFG_DIFFERENT_FLOOR_CEILING_COLORS |
|
18 + (pixel->height / SFG_WALL_HEIGHT_STEP) % 4 |
|
#else |
|
SFG_currentLevel.ceilingColor |
|
#endif |
|
) |
|
: SFG_TRANSPARENT_COLOR); |
|
} |
|
|
|
if (color != SFG_TRANSPARENT_COLOR) |
|
{ |
|
#if SFG_DITHERED_SHADOW |
|
uint8_t fogShadow = (pixel->depth * 8) / SFG_FOG_DIMINISH_STEP; |
|
|
|
uint8_t fogShadowPart = fogShadow & 0x07; |
|
|
|
fogShadow /= 8; |
|
|
|
uint8_t xMod4 = pixel->position.x & 0x03; |
|
uint8_t yMod2 = pixel->position.y & 0x01; |
|
|
|
shadow += |
|
fogShadow + SFG_ditheringPatterns[fogShadowPart * 8 + yMod2 * 4 + xMod4]; |
|
#else |
|
shadow += SFG_fogValueDiminish(pixel->depth); |
|
#endif |
|
|
|
#if SFG_ENABLE_FOG |
|
color = palette_minusValue(color,shadow); |
|
#endif |
|
} |
|
else |
|
{ |
|
#if SFG_DRAW_LEVEL_BACKGROUND |
|
color = SFG_getTexel(SFG_backgroundImages + |
|
SFG_currentLevel.backgroundImage * SFG_TEXTURE_STORE_SIZE, |
|
SFG_game.backgroundScaleMap[((pixel->position.x |
|
#if SFG_BACKGROUND_BLUR != 0 |
|
+ SFG_backgroundBlurOffsets[SFG_backgroundBlurIndex] |
|
#endif |
|
) * SFG_RAYCASTING_SUBSAMPLE + SFG_game.backgroundScroll) % SFG_GAME_RESOLUTION_Y], |
|
(SFG_game.backgroundScaleMap[(pixel->position.y // ^ TODO: get rid of mod? |
|
#if SFG_BACKGROUND_BLUR != 0 |
|
+ SFG_backgroundBlurOffsets[SFG_backgroundBlurIndex + 1] |
|
#endif |
|
) % SFG_GAME_RESOLUTION_Y]) |
|
); |
|
|
|
#if SFG_BACKGROUND_BLUR != 0 |
|
SFG_backgroundBlurIndex = (SFG_backgroundBlurIndex + 1) % 8; |
|
#endif |
|
#else |
|
color = 1; |
|
#endif |
|
} |
|
|
|
#if SFG_BRIGHTNESS > 0 |
|
color = palette_plusValue(color,SFG_BRIGHTNESS); |
|
#elif SFG_BRIGHTNESS < 0 |
|
color = palette_minusValue(color,-1 * SFG_BRIGHTNESS); |
|
#endif |
|
|
|
#if SFG_RAYCASTING_SUBSAMPLE == 1 |
|
// the other version will probably get optimized to this, but just in case |
|
SFG_setGamePixel(pixel->position.x,pixel->position.y,color); |
|
#else |
|
RCL_Unit screenX = pixel->position.x * SFG_RAYCASTING_SUBSAMPLE; |
|
|
|
for (int_fast8_t i = 0; i < SFG_RAYCASTING_SUBSAMPLE; ++i) |
|
{ |
|
SFG_setGamePixel(screenX,pixel->position.y,color); |
|
screenX++; |
|
} |
|
#endif |
|
} |
|
|
|
/** |
|
Draws image on screen, with transparency. This is faster than sprite drawing. |
|
For performance sake drawing near screen edges is not pixel perfect. |
|
*/ |
|
void SFG_blitImage( |
|
const uint8_t *image, |
|
int16_t posX, |
|
int16_t posY, |
|
uint8_t scale) |
|
{ |
|
if (scale == 0) |
|
return; |
|
|
|
uint16_t x0 = posX, |
|
x1, |
|
y0 = posY, |
|
y1; |
|
|
|
uint8_t u0 = 0, v0 = 0; |
|
|
|
if (posX < 0) |
|
{ |
|
x0 = 0; |
|
u0 = (-1 * posX) / scale; |
|
} |
|
|
|
posX += scale * SFG_TEXTURE_SIZE; |
|
|
|
uint16_t limitX = SFG_GAME_RESOLUTION_X - scale; |
|
uint16_t limitY = SFG_GAME_RESOLUTION_Y - scale; |
|
|
|
x1 = posX >= 0 ? |
|
(posX <= limitX ? posX : limitX) |
|
: 0; |
|
|
|
if (x1 >= SFG_GAME_RESOLUTION_X) |
|
x1 = SFG_GAME_RESOLUTION_X - 1; |
|
|
|
if (posY < 0) |
|
{ |
|
y0 = 0; |
|
v0 = (-1 * posY) / scale; |
|
} |
|
|
|
posY += scale * SFG_TEXTURE_SIZE; |
|
|
|
y1 = posY >= 0 ? (posY <= limitY ? posY : limitY) : 0; |
|
|
|
if (y1 >= SFG_GAME_RESOLUTION_Y) |
|
y1 = SFG_GAME_RESOLUTION_Y - 1; |
|
|
|
uint8_t v = v0; |
|
|
|
for (uint16_t y = y0; y < y1; y += scale) |
|
{ |
|
uint8_t u = u0; |
|
|
|
for (uint16_t x = x0; x < x1; x += scale) |
|
{ |
|
uint8_t color = SFG_getTexel(image,u,v); |
|
|
|
if (color != SFG_TRANSPARENT_COLOR) |
|
{ |
|
uint16_t sY = y; |
|
|
|
for (uint8_t j = 0; j < scale; ++j) |
|
{ |
|
uint16_t sX = x; |
|
|
|
for (uint8_t i = 0; i < scale; ++i) |
|
{ |
|
SFG_setGamePixel(sX,sY,color); |
|
sX++; |
|
} |
|
|
|
sY++; |
|
} |
|
} |
|
u++; |
|
} |
|
v++; |
|
} |
|
} |
|
|
|
void SFG_drawScaledSprite( |
|
const uint8_t *image, |
|
int16_t centerX, |
|
int16_t centerY, |
|
int16_t size, |
|
uint8_t minusValue, |
|
RCL_Unit distance) |
|
{ |
|
if (size == 0) |
|
return; |
|
|
|
if (size > SFG_MAX_SPRITE_SIZE) |
|
size = SFG_MAX_SPRITE_SIZE; |
|
|
|
uint16_t halfSize = size / 2; |
|
|
|
int16_t topLeftX = centerX - halfSize; |
|
int16_t topLeftY = centerY - halfSize; |
|
|
|
int16_t x0, u0; |
|
|
|
if (topLeftX < 0) |
|
{ |
|
u0 = -1 * topLeftX; |
|
x0 = 0; |
|
} |
|
else |
|
{ |
|
u0 = 0; |
|
x0 = topLeftX; |
|
} |
|
|
|
int16_t x1 = topLeftX + size - 1; |
|
|
|
if (x1 >= SFG_GAME_RESOLUTION_X) |
|
x1 = SFG_GAME_RESOLUTION_X - 1; |
|
|
|
int16_t y0, v0; |
|
|
|
if (topLeftY < 0) |
|
{ |
|
v0 = -1 * topLeftY; |
|
y0 = 0; |
|
} |
|
else |
|
{ |
|
v0 = 0; |
|
y0 = topLeftY; |
|
} |
|
|
|
int16_t y1 = topLeftY + size - 1; |
|
|
|
if (y1 >= SFG_GAME_RESOLUTION_Y) |
|
y1 = SFG_GAME_RESOLUTION_Y - 1; |
|
|
|
if ((x0 > x1) || (y0 > y1) || (u0 >= size) || (v0 >= size)) // outside screen? |
|
return; |
|
|
|
int16_t u1 = u0 + (x1 - x0); |
|
int16_t v1 = v0 + (y1 - y0); |
|
|
|
// precompute sampling positions: |
|
|
|
int16_t uMin = RCL_min(u0,u1); |
|
int16_t vMin = RCL_min(v0,v1); |
|
int16_t uMax = RCL_max(u0,u1); |
|
int16_t vMax = RCL_max(v0,v1); |
|
|
|
int16_t precompFrom = RCL_min(uMin,vMin); |
|
int16_t precompTo = RCL_max(uMax,vMax); |
|
|
|
precompFrom = RCL_max(0,precompFrom); |
|
precompTo = RCL_min(SFG_MAX_SPRITE_SIZE - 1,precompTo); |
|
|
|
#define PRECOMP_SCALE 512 |
|
|
|
int16_t precompStepScaled = ((SFG_TEXTURE_SIZE) * PRECOMP_SCALE) / size; |
|
int16_t precompPosScaled = precompFrom * precompStepScaled; |
|
|
|
for (int16_t i = precompFrom; i <= precompTo; ++i) |
|
{ |
|
SFG_game.spriteSamplingPoints[i] = precompPosScaled / PRECOMP_SCALE; |
|
precompPosScaled += precompStepScaled; |
|
} |
|
|
|
#undef PRECOMP_SCALE |
|
|
|
uint8_t zDistance = SFG_RCLUnitToZBuffer(distance); |
|
|
|
for (int16_t x = x0, u = u0; x <= x1; ++x, ++u) |
|
{ |
|
if (SFG_game.zBuffer[x] >= zDistance) |
|
{ |
|
int8_t columnTransparent = 1; |
|
|
|
for (int16_t y = y0, v = v0; y <= y1; ++y, ++v) |
|
{ |
|
uint8_t color = |
|
SFG_getTexel(image,SFG_game.spriteSamplingPoints[u], |
|
SFG_game.spriteSamplingPoints[v]); |
|
|
|
if (color != SFG_TRANSPARENT_COLOR) |
|
{ |
|
#if SFG_DIMINISH_SPRITES |
|
color = palette_minusValue(color,minusValue); |
|
#endif |
|
columnTransparent = 0; |
|
|
|
SFG_setGamePixel(x,y,color); |
|
} |
|
} |
|
|
|
if (!columnTransparent) |
|
SFG_game.zBuffer[x] = zDistance; |
|
} |
|
} |
|
} |
|
|
|
RCL_Unit SFG_texturesAt(int16_t x, int16_t y) |
|
{ |
|
uint8_t p; |
|
|
|
SFG_TileDefinition tile = |
|
SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&p); |
|
|
|
return |
|
SFG_TILE_FLOOR_TEXTURE(tile) | (SFG_TILE_CEILING_TEXTURE(tile) << 3) | p; |
|
// ^ store both textures (floor and ceiling) and properties in one number |
|
} |
|
|
|
RCL_Unit SFG_movingWallHeight |
|
( |
|
RCL_Unit low, |
|
RCL_Unit high, |
|
uint32_t time |
|
) |
|
{ |
|
RCL_Unit height = RCL_nonZero(high - low); |
|
RCL_Unit halfHeight = height / 2; |
|
|
|
RCL_Unit sinArg = |
|
(time * ((SFG_MOVING_WALL_SPEED * RCL_UNITS_PER_SQUARE) / 1000)) / height; |
|
|
|
return |
|
low + halfHeight + (RCL_sin(sinArg) * halfHeight) / RCL_UNITS_PER_SQUARE; |
|
} |
|
|
|
RCL_Unit SFG_floorHeightAt(int16_t x, int16_t y) |
|
{ |
|
uint8_t properties; |
|
|
|
SFG_TileDefinition tile = |
|
SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&properties); |
|
|
|
RCL_Unit doorHeight = 0; |
|
|
|
if (properties == SFG_TILE_PROPERTY_DOOR) |
|
{ |
|
for (uint8_t i = 0; i < SFG_currentLevel.doorRecordCount; ++i) |
|
{ |
|
SFG_DoorRecord *door = &(SFG_currentLevel.doorRecords[i]); |
|
|
|
if ((door->coords[0] == x) && (door->coords[1] == y)) |
|
{ |
|
doorHeight = door->state & SFG_DOOR_VERTICAL_POSITION_MASK; |
|
|
|
doorHeight = doorHeight != (0xff & SFG_DOOR_VERTICAL_POSITION_MASK) ? |
|
doorHeight * SFG_DOOR_HEIGHT_STEP : RCL_UNITS_PER_SQUARE; |
|
|
|
break; |
|
} |
|
} |
|
} |
|
else if (properties == SFG_TILE_PROPERTY_ELEVATOR) |
|
{ |
|
RCL_Unit height = |
|
SFG_TILE_FLOOR_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP; |
|
|
|
return SFG_movingWallHeight( |
|
height, |
|
height + SFG_TILE_CEILING_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP, |
|
SFG_game.frameTime - SFG_currentLevel.timeStart); |
|
} |
|
|
|
return SFG_TILE_FLOOR_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP - doorHeight; |
|
} |
|
|
|
/** |
|
Like SFG_floorCollisionHeightAt, but takes into account colliding items on |
|
the map, so the squares that have these items are higher. The former function |
|
is for rendering, this one is for collision checking. |
|
*/ |
|
RCL_Unit SFG_floorCollisionHeightAt(int16_t x, int16_t y) |
|
{ |
|
return SFG_floorHeightAt(x,y) + |
|
SFG_getItemCollisionMapBit(x,y) * RCL_UNITS_PER_SQUARE; |
|
} |
|
|
|
void SFG_getPlayerWeaponInfo( |
|
uint8_t *ammoType, uint8_t *projectileCount, uint8_t *canShoot) |
|
{ |
|
*ammoType = SFG_weaponAmmo(SFG_player.weapon); |
|
|
|
*projectileCount = SFG_GET_WEAPON_PROJECTILE_COUNT(SFG_player.weapon); |
|
|
|
#if SFG_INFINITE_AMMO |
|
*canShoot = 1; |
|
#else |
|
*canShoot = |
|
((*ammoType == SFG_AMMO_NONE) || |
|
(SFG_player.ammo[*ammoType] >= *projectileCount) || |
|
(SFG_game.cheatState & 0x80)); |
|
#endif |
|
} |
|
|
|
void SFG_playerRotateWeapon(uint8_t next) |
|
{ |
|
uint8_t initialWeapon = SFG_player.weapon; |
|
int8_t increment = next ? 1 : -1; |
|
|
|
while (1) |
|
{ |
|
SFG_player.weapon = |
|
(SFG_WEAPONS_TOTAL + SFG_player.weapon + increment) % SFG_WEAPONS_TOTAL; |
|
|
|
if (SFG_player.weapon == initialWeapon) |
|
break; |
|
|
|
uint8_t ammo, projectileCount, canShoot; |
|
|
|
SFG_getPlayerWeaponInfo(&ammo,&projectileCount,&canShoot); |
|
|
|
if (canShoot) |
|
break; |
|
} |
|
} |
|
|
|
void SFG_initPlayer() |
|
{ |
|
RCL_initCamera(&SFG_player.camera); |
|
|
|
SFG_player.camera.resolution.x = |
|
SFG_GAME_RESOLUTION_X / SFG_RAYCASTING_SUBSAMPLE; |
|
|
|
SFG_player.camera.resolution.y = SFG_GAME_RESOLUTION_Y - SFG_HUD_BAR_HEIGHT; |
|
|
|
SFG_player.camera.position.x = RCL_UNITS_PER_SQUARE / 2 + |
|
SFG_currentLevel.levelPointer->playerStart[0] * RCL_UNITS_PER_SQUARE; |
|
|
|
SFG_player.camera.position.y = RCL_UNITS_PER_SQUARE / 2 + |
|
SFG_currentLevel.levelPointer->playerStart[1] * RCL_UNITS_PER_SQUARE; |
|
|
|
SFG_player.squarePosition[0] = |
|
SFG_player.camera.position.x / RCL_UNITS_PER_SQUARE; |
|
|
|
SFG_player.squarePosition[1] = |
|
SFG_player.camera.position.y / RCL_UNITS_PER_SQUARE; |
|
|
|
SFG_player.camera.height = SFG_floorHeightAt( |
|
SFG_currentLevel.levelPointer->playerStart[0], |
|
SFG_currentLevel.levelPointer->playerStart[1]) + |
|
RCL_CAMERA_COLL_HEIGHT_BELOW; |
|
|
|
SFG_player.camera.direction = SFG_currentLevel.levelPointer->playerStart[2] * |
|
(RCL_UNITS_PER_SQUARE / 256); |
|
|
|
SFG_recomputePLayerDirection(); |
|
|
|
SFG_player.previousVerticalSpeed = 0; |
|
|
|
SFG_player.headBobFrame = 0; |
|
|
|
SFG_player.weapon = SFG_WEAPON_KNIFE; |
|
|
|
SFG_player.weaponCooldownFrames = 0; |
|
SFG_player.lastHurtFrame = SFG_game.frame; |
|
SFG_player.lastItemTakenFrame = SFG_game.frame; |
|
|
|
SFG_player.health = SFG_PLAYER_START_HEALTH; |
|
|
|
SFG_player.previousWeaponDirection = 0; |
|
|
|
SFG_player.cards = |
|
#if SFG_UNLOCK_DOOR |
|
0x07; |
|
#else |
|
0; |
|
#endif |
|
|
|
SFG_player.justTeleported = 0; |
|
|
|
for (uint8_t i = 0; i < SFG_AMMO_TOTAL; ++i) |
|
SFG_player.ammo[i] = 0; |
|
} |
|
|
|
RCL_Unit SFG_ceilingHeightAt(int16_t x, int16_t y) |
|
{ |
|
uint8_t properties; |
|
SFG_TileDefinition tile = |
|
SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&properties); |
|
|
|
if (properties == SFG_TILE_PROPERTY_ELEVATOR) |
|
return SFG_CEILING_MAX_HEIGHT; |
|
|
|
uint8_t height = SFG_TILE_CEILING_HEIGHT(tile); |
|
|
|
return properties != SFG_TILE_PROPERTY_SQUEEZER ? |
|
( |
|
height != SFG_TILE_CEILING_MAX_HEIGHT ? |
|
((SFG_TILE_FLOOR_HEIGHT(tile) + height) * SFG_WALL_HEIGHT_STEP) : |
|
SFG_CEILING_MAX_HEIGHT |
|
) : |
|
SFG_movingWallHeight( |
|
SFG_TILE_FLOOR_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP, |
|
(SFG_TILE_CEILING_HEIGHT(tile) + SFG_TILE_FLOOR_HEIGHT(tile)) |
|
* SFG_WALL_HEIGHT_STEP, |
|
SFG_game.frameTime - SFG_currentLevel.timeStart); |
|
} |
|
|
|
/** |
|
Gets sprite (image and sprite size) for given item. |
|
*/ |
|
void SFG_getItemSprite( |
|
uint8_t elementType, const uint8_t **sprite, uint8_t *spriteSize) |
|
{ |
|
*spriteSize = 0; |
|
*sprite = SFG_itemSprites + (elementType - 1) * SFG_TEXTURE_STORE_SIZE; |
|
|
|
switch (elementType) |
|
{ |
|
case SFG_LEVEL_ELEMENT_TREE: |
|
case SFG_LEVEL_ELEMENT_RUIN: |
|
case SFG_LEVEL_ELEMENT_LAMP: |
|
case SFG_LEVEL_ELEMENT_TELEPORTER: |
|
*spriteSize = 2; |
|
break; |
|
|
|
case SFG_LEVEL_ELEMENT_TERMINAL: |
|
*spriteSize = 1; |
|
break; |
|
|
|
case SFG_LEVEL_ELEMENT_FINISH: |
|
case SFG_LEVEL_ELEMENT_COLUMN: |
|
*spriteSize = 3; |
|
break; |
|
|
|
case SFG_LEVEL_ELEMENT_CARD0: |
|
case SFG_LEVEL_ELEMENT_CARD1: |
|
case SFG_LEVEL_ELEMENT_CARD2: |
|
*sprite = SFG_itemSprites + |
|
(SFG_LEVEL_ELEMENT_CARD0 - 1) * SFG_TEXTURE_STORE_SIZE; |
|
break; |
|
|
|
case SFG_LEVEL_ELEMENT_BLOCKER: |
|
*sprite = 0; |
|
break; |
|
|
|
default: |
|
break; |
|
} |
|
} |
|
|
|
/** |
|
Says whether given item type collides, i.e. stops player from moving. |
|
*/ |
|
uint8_t SFG_itemCollides(uint8_t elementType) |
|
{ |
|
return |
|
elementType == SFG_LEVEL_ELEMENT_BARREL || |
|
elementType == SFG_LEVEL_ELEMENT_TREE || |
|
elementType == SFG_LEVEL_ELEMENT_TERMINAL || |
|
elementType == SFG_LEVEL_ELEMENT_COLUMN || |
|
elementType == SFG_LEVEL_ELEMENT_RUIN || |
|
elementType == SFG_LEVEL_ELEMENT_BLOCKER || |
|
elementType == SFG_LEVEL_ELEMENT_LAMP; |
|
} |
|
|
|
void SFG_setGameState(uint8_t state) |
|
{ |
|
SFG_LOG("changing game state"); |
|
SFG_game.state = state; |
|
SFG_game.stateTime = 0; |
|
} |
|
|
|
void SFG_setAndInitLevel(uint8_t levelNumber) |
|
{ |
|
SFG_LOG("setting and initializing level"); |
|
|
|
const SFG_Level *level; |
|
|
|
#if SFG_AVR |
|
memcpy_P(&SFG_ramLevel,SFG_levels[levelNumber],sizeof(SFG_Level)); |
|
level = &SFG_ramLevel; |
|
#else |
|
level = SFG_levels[levelNumber]; |
|
#endif |
|
|
|
SFG_game.currentRandom = 0; |
|
|
|
if (SFG_game.saved != SFG_CANT_SAVE) |
|
SFG_game.saved = 0; |
|
|
|
SFG_currentLevel.levelNumber = levelNumber; |
|
SFG_currentLevel.monstersDead = 0; |
|
SFG_currentLevel.backgroundImage = level->backgroundImage; |
|
SFG_currentLevel.levelPointer = level; |
|
SFG_currentLevel.bossCount = 0; |
|
SFG_currentLevel.floorColor = level->floorColor; |
|
SFG_currentLevel.ceilingColor = level->ceilingColor; |
|
SFG_currentLevel.completionTime10sOfS = 0; |
|
|
|
for (uint8_t i = 0; i < 7; ++i) |
|
SFG_currentLevel.textures[i] = |
|
SFG_wallTextures + level->textureIndices[i] * SFG_TEXTURE_STORE_SIZE; |
|
|
|
SFG_LOG("initializing doors"); |
|
|
|
SFG_currentLevel.checkedDoorIndex = 0; |
|
SFG_currentLevel.doorRecordCount = 0; |
|
SFG_currentLevel.projectileRecordCount = 0; |
|
SFG_currentLevel.teleporterCount = 0; |
|
SFG_currentLevel.mapRevealMask = |
|
#if SFG_REVEAL_MAP |
|
0xffff; |
|
#else |
|
0; |
|
#endif |
|
|
|
for (uint8_t j = 0; j < SFG_MAP_SIZE; ++j) |
|
{ |
|
for (uint8_t i = 0; i < SFG_MAP_SIZE; ++i) |
|
{ |
|
uint8_t properties; |
|
|
|
SFG_getMapTile(level,i,j,&properties); |
|
|
|
if ((properties & SFG_TILE_PROPERTY_MASK) == SFG_TILE_PROPERTY_DOOR) |
|
{ |
|
SFG_DoorRecord *d = |
|
&(SFG_currentLevel.doorRecords[SFG_currentLevel.doorRecordCount]); |
|
|
|
d->coords[0] = i; |
|
d->coords[1] = j; |
|
d->state = 0x00; |
|
|
|
SFG_currentLevel.doorRecordCount++; |
|
} |
|
|
|
if (SFG_currentLevel.doorRecordCount >= SFG_MAX_DOORS) |
|
{ |
|
SFG_LOG("warning: too many doors!"); |
|
break; |
|
} |
|
} |
|
|
|
if (SFG_currentLevel.doorRecordCount >= SFG_MAX_DOORS) |
|
break; |
|
} |
|
|
|
SFG_LOG("initializing level elements"); |
|
|
|
SFG_currentLevel.itemRecordCount = 0; |
|
SFG_currentLevel.checkedItemIndex = 0; |
|
|
|
SFG_currentLevel.monsterRecordCount = 0; |
|
SFG_currentLevel.checkedMonsterIndex = 0; |
|
|
|
SFG_MonsterRecord *monster; |
|
|
|
for (uint16_t i = 0; i < ((SFG_MAP_SIZE * SFG_MAP_SIZE) / 8); ++i) |
|
SFG_currentLevel.itemCollisionMap[i] = 0; |
|
|
|
for (uint8_t i = 0; i < SFG_MAX_LEVEL_ELEMENTS; ++i) |
|
{ |
|
const SFG_LevelElement *e = &(SFG_currentLevel.levelPointer->elements[i]); |
|
|
|
if (e->type != SFG_LEVEL_ELEMENT_NONE) |
|
{ |
|
if (SFG_LEVEL_ELEMENT_TYPE_IS_MOSTER(e->type)) |
|
{ |
|
monster = |
|
&(SFG_currentLevel.monsterRecords[SFG_currentLevel.monsterRecordCount]); |
|
|
|
monster->stateType = (SFG_MONSTER_TYPE_TO_INDEX(e->type) << 4) |
|
| SFG_MONSTER_STATE_INACTIVE; |
|
|
|
monster->health = |
|
SFG_GET_MONSTER_MAX_HEALTH(SFG_MONSTER_TYPE_TO_INDEX(e->type)); |
|
|
|
monster->coords[0] = e->coords[0] * 4 + 2; |
|
monster->coords[1] = e->coords[1] * 4 + 2; |
|
|
|
SFG_currentLevel.monsterRecordCount++; |
|
|
|
if (e->type == SFG_LEVEL_ELEMENT_MONSTER_ENDER) |
|
SFG_currentLevel.bossCount++; |
|
} |
|
else if ((e->type < SFG_LEVEL_ELEMENT_LOCK0) || |
|
(e->type > SFG_LEVEL_ELEMENT_LOCK2)) |
|
{ |
|
SFG_currentLevel.itemRecords[SFG_currentLevel.itemRecordCount] = i; |
|
SFG_currentLevel.itemRecordCount++; |
|
|
|
if (e->type == SFG_LEVEL_ELEMENT_TELEPORTER) |
|
SFG_currentLevel.teleporterCount++; |
|
|
|
if (SFG_itemCollides(e->type)) |
|
SFG_setItemCollisionMapBit(e->coords[0],e->coords[1],1); |
|
} |
|
else |
|
{ |
|
uint8_t properties; |
|
|
|
SFG_getMapTile(level,e->coords[0],e->coords[1],&properties); |
|
|
|
if ((properties & SFG_TILE_PROPERTY_MASK) == SFG_TILE_PROPERTY_DOOR) |
|
{ |
|
// find the door record and lock the door: |
|
for (uint16_t j = 0; j < SFG_currentLevel.doorRecordCount; ++j) |
|
{ |
|
SFG_DoorRecord *d = &(SFG_currentLevel.doorRecords[j]); |
|
|
|
if (d->coords[0] == e->coords[0] && d->coords[1] == e->coords[1]) |
|
{ |
|
d->state |= (e->type - SFG_LEVEL_ELEMENT_LOCK0 + 1) << 6; |
|
break; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
SFG_LOG("warning: lock not put on door tile!"); |
|
} |
|
} |
|
} |
|
} |
|
|
|
SFG_currentLevel.timeStart = SFG_game.frameTime; |
|
SFG_currentLevel.frameStart = SFG_game.frame; |
|
|
|
SFG_game.spriteAnimationFrame = 0; |
|
|
|
SFG_initPlayer(); |
|
SFG_setGameState(SFG_GAME_STATE_LEVEL_START); |
|
SFG_setMusic(SFG_MUSIC_NEXT); |
|
SFG_processEvent(SFG_EVENT_LEVEL_STARTS,levelNumber); |
|
} |
|
|
|
void SFG_createDefaultSaveData(uint8_t *memory) |
|
{ |
|
for (uint16_t i = 0; i < SFG_SAVE_SIZE; ++i) |
|
memory[i] = 0; |
|
|
|
memory[1] = SFG_DEFAULT_SETTINGS; |
|
} |
|
|
|
void SFG_init() |
|
{ |
|
SFG_LOG("initializing game") |
|
|
|
SFG_game.frame = 0; |
|
SFG_game.frameTime = 0; |
|
SFG_game.currentRandom = 0; |
|
SFG_game.cheatState = 0; |
|
SFG_game.continues = 1; |
|
|
|
RCL_initRayConstraints(&SFG_game.rayConstraints); |
|
SFG_game.rayConstraints.maxHits = SFG_RAYCASTING_MAX_HITS; |
|
SFG_game.rayConstraints.maxSteps = SFG_RAYCASTING_MAX_STEPS; |
|
|
|
RCL_initRayConstraints(&SFG_game.visibilityRayConstraints); |
|
SFG_game.visibilityRayConstraints.maxHits = |
|
SFG_RAYCASTING_VISIBILITY_MAX_HITS; |
|
SFG_game.visibilityRayConstraints.maxSteps = |
|
SFG_RAYCASTING_VISIBILITY_MAX_STEPS; |
|
|
|
SFG_game.antiSpam = 0; |
|
|
|
SFG_LOG("computing average texture colors") |
|
|
|
for (uint8_t i = 0; i < SFG_WALL_TEXTURE_COUNT; ++i) |
|
{ |
|
/** For simplicity, we round colors so that there is only 64 of them, and |
|
we count them up to 256. */ |
|
|
|
uint8_t colorHistogram[64]; |
|
|
|
for (uint8_t j = 0; j < 64; ++j) |
|
colorHistogram[j] = 0; |
|
|
|
for (uint8_t y = 0; y < SFG_TEXTURE_SIZE; ++y) |
|
for (uint8_t x = 0; x < SFG_TEXTURE_SIZE; ++x) |
|
{ |
|
uint8_t color = |
|
SFG_getTexel(SFG_wallTextures + i * SFG_TEXTURE_STORE_SIZE,x,y) / 4; |
|
|
|
colorHistogram[color] += 1; |
|
|
|
if (colorHistogram[color] == 255) |
|
break; |
|
} |
|
|
|
uint8_t maxIndex = 0; |
|
|
|
for (uint8_t j = 0; j < 64; ++j) |
|
{ |
|
if (colorHistogram[j] == 255) |
|
{ |
|
maxIndex = j; |
|
break; |
|
} |
|
|
|
if (colorHistogram[j] > colorHistogram[maxIndex]) |
|
maxIndex = j; |
|
} |
|
|
|
SFG_game.textureAverageColors[i] = maxIndex * 4; |
|
} |
|
|
|
for (uint16_t i = 0; i < SFG_GAME_RESOLUTION_Y; ++i) |
|
SFG_game.backgroundScaleMap[i] = |
|
(i * SFG_TEXTURE_SIZE) / SFG_GAME_RESOLUTION_Y; |
|
|
|
for (uint8_t i = 0; i < SFG_KEY_COUNT; ++i) |
|
SFG_game.keyStates[i] = 0; |
|
|
|
SFG_currentLevel.levelPointer = 0; |
|
SFG_game.backgroundScroll = 0; |
|
SFG_game.selectedMenuItem = 0; |
|
SFG_game.selectedLevel = 0; |
|
SFG_game.settings = SFG_DEFAULT_SETTINGS; |
|
SFG_game.saved = 0; |
|
|
|
SFG_createDefaultSaveData(SFG_game.save); |
|
|
|
SFG_gameLoad(); // attempt to load settings |
|
|
|
if (SFG_game.saved != SFG_CANT_SAVE) |
|
{ |
|
SFG_LOG("settings loaded"); |
|
SFG_game.settings = SFG_game.save[1]; |
|
} |
|
else |
|
{ |
|
SFG_LOG("saving/loading not possible"); |
|
SFG_game.save[0] = SFG_NUMBER_OF_LEVELS - 1; // revealed all levels |
|
} |
|
|
|
#if SFG_ALL_LEVELS |
|
SFG_game.save[0] = SFG_NUMBER_OF_LEVELS - 1; |
|
#endif |
|
|
|
SFG_setMusic((SFG_game.settings & 0x02) ? |
|
SFG_MUSIC_TURN_ON : SFG_MUSIC_TURN_OFF); |
|
|
|
#if SFG_START_LEVEL == 0 |
|
SFG_setGameState(SFG_GAME_STATE_INIT); |
|
#else |
|
SFG_setAndInitLevel(SFG_START_LEVEL - 1); |
|
#endif |
|
} |
|
|
|
/** |
|
Adds new projectile to the current level, returns 1 if added, 0 if not (max |
|
count reached). |
|
*/ |
|
uint8_t SFG_createProjectile(SFG_ProjectileRecord projectile) |
|
{ |
|
if (SFG_currentLevel.projectileRecordCount >= SFG_MAX_PROJECTILES) |
|
return 0; |
|
|
|
SFG_currentLevel.projectileRecords[SFG_currentLevel.projectileRecordCount] = |
|
projectile; |
|
|
|
SFG_currentLevel.projectileRecordCount++; |
|
|
|
return 1; |
|
} |
|
|
|
/** |
|
Launches projectile of given type from given position in given direction |
|
(has to be normalized), with given offset (so as to not collide with the |
|
shooting entity). Returns the same value as SFG_createProjectile. |
|
*/ |
|
uint8_t SFG_launchProjectile( |
|
uint8_t type, |
|
RCL_Vector2D shootFrom, |
|
RCL_Unit shootFromHeight, |
|
RCL_Vector2D direction, |
|
RCL_Unit verticalSpeed, |
|
RCL_Unit offsetDistance |
|
) |
|
{ |
|
if (type == SFG_PROJECTILE_NONE) |
|
return 0; |
|
|
|
SFG_ProjectileRecord p; |
|
|
|
p.type = type; |
|
p.doubleFramesToLive = |
|
RCL_nonZero(SFG_GET_PROJECTILE_FRAMES_TO_LIVE(type) / 2); |
|
|
|
p.position[0] = |
|
shootFrom.x + (direction.x * offsetDistance) / RCL_UNITS_PER_SQUARE; |
|
p.position[1] = |
|
shootFrom.y + (direction.y * offsetDistance) / RCL_UNITS_PER_SQUARE; |
|
p.position[2] = shootFromHeight; |
|
|
|
p.direction[0] = |
|
(direction.x * SFG_GET_PROJECTILE_SPEED_UPS(type)) / RCL_UNITS_PER_SQUARE; |
|
p.direction[1] = |
|
(direction.y * SFG_GET_PROJECTILE_SPEED_UPS(type)) / RCL_UNITS_PER_SQUARE; |
|
p.direction[2] = verticalSpeed; |
|
|
|
return SFG_createProjectile(p); |
|
} |
|
|
|
/** |
|
Pushes a given position away from a center by given distance, with collisions. |
|
Returns 1 if push away happened, otherwise 0. |
|
*/ |
|
uint8_t SFG_pushAway( |
|
RCL_Unit pos[3], |
|
RCL_Unit centerX, |
|
RCL_Unit centerY, |
|
RCL_Unit preferredDirection, |
|
RCL_Unit distance) |
|
{ |
|
RCL_Vector2D fromCenter; |
|
|
|
fromCenter.x = pos[0] - centerX; |
|
fromCenter.y = pos[1] - centerY; |
|
|
|
RCL_Unit l = RCL_len(fromCenter); |
|
|
|
if (l < 128) |
|
{ |
|
fromCenter = RCL_angleToDirection(preferredDirection); |
|
l = RCL_UNITS_PER_SQUARE; |
|
} |
|
|
|
RCL_Vector2D offset; |
|
|
|
offset.x = (fromCenter.x * distance) / l; |
|
offset.y = (fromCenter.y * distance) / l; |
|
|
|
RCL_Camera c; |
|
|
|
RCL_initCamera(&c); |
|
|
|
c.position.x = pos[0]; |
|
c.position.y = pos[1]; |
|
c.height = pos[2]; |
|
|
|
RCL_moveCameraWithCollision(&c,offset,0,SFG_floorCollisionHeightAt, |
|
SFG_ceilingHeightAt,1,1); |
|
|
|
pos[0] = c.position.x; |
|
pos[1] = c.position.y; |
|
pos[2] = c.height; |
|
|
|
return 1; |
|
} |
|
|
|
uint8_t SFG_pushPlayerAway( |
|
RCL_Unit centerX, RCL_Unit centerY, RCL_Unit distance) |
|
{ |
|
RCL_Unit p[3]; |
|
|
|
p[0] = SFG_player.camera.position.x; |
|
p[1] = SFG_player.camera.position.y; |
|
p[2] = SFG_player.camera.height; |
|
|
|
uint8_t result = SFG_pushAway(p,centerX,centerY, |
|
SFG_player.camera.direction - RCL_UNITS_PER_SQUARE / 2, |
|
distance); |
|
|
|
SFG_player.camera.position.x = p[0]; |
|
SFG_player.camera.position.y = p[1]; |
|
SFG_player.camera.height = p[2]; |
|
|
|
return result; |
|
} |
|
|
|
/** |
|
Helper function to resolve collision with level element. The function supposes |
|
the collision already does happen and only resolves it. Returns adjusted move |
|
offset. |
|
*/ |
|
RCL_Vector2D SFG_resolveCollisionWithElement( |
|
RCL_Vector2D position, RCL_Vector2D moveOffset, RCL_Vector2D elementPos) |
|
{ |
|
RCL_Unit dx = RCL_abs(elementPos.x - position.x); |
|
RCL_Unit dy = RCL_abs(elementPos.y - position.y); |
|
|
|
if (dx > dy) |
|
{ |
|
// colliding from left/right |
|
|
|
if ((moveOffset.x > 0) == (position.x < elementPos.x)) |
|
moveOffset.x = 0; |
|
// ^ only stop if heading towards element, to avoid getting stuck |
|
} |
|
else |
|
{ |
|
// colliding from up/down |
|
|
|
if ((moveOffset.y > 0) == (position.y < elementPos.y)) |
|
moveOffset.y = 0; |
|
} |
|
|
|
return moveOffset; |
|
} |
|
|
|
/** |
|
Adds or substracts player's health during the playing state due to taking |
|
damage (negative value) or getting healed. Negative value will be corrected by |
|
SFG_PLAYER_DAMAGE_MULTIPLIER in this function. |
|
*/ |
|
void SFG_playerChangeHealth(int8_t healthAdd) |
|
{ |
|
if (SFG_game.state != SFG_GAME_STATE_PLAYING) |
|
return; // don't hurt during level starting phase |
|
|
|
if (healthAdd < 0) |
|
{ |
|
if (SFG_game.cheatState & 0x80) // invincible? |
|
return; |
|
|
|
healthAdd = |
|
RCL_min(-1, |
|
(((RCL_Unit) healthAdd) * SFG_PLAYER_DAMAGE_MULTIPLIER) / |
|
RCL_UNITS_PER_SQUARE); |
|
|
|
SFG_player.lastHurtFrame = SFG_game.frame; |
|
SFG_processEvent(SFG_EVENT_VIBRATE,0); |
|
SFG_processEvent(SFG_EVENT_PLAYER_HURT,-1 * healthAdd); |
|
} |
|
|
|
int16_t health = SFG_player.health; |
|
health += healthAdd; |
|
health = RCL_clamp(health,0,SFG_PLAYER_MAX_HEALTH); |
|
|
|
SFG_player.health = health; |
|
} |
|
|
|
uint8_t SFG_distantSoundVolume(RCL_Unit x, RCL_Unit y, RCL_Unit z) |
|
{ |
|
RCL_Unit distance = SFG_taxicabDistance(x,y,z, |
|
SFG_player.camera.position.x, |
|
SFG_player.camera.position.y, |
|
SFG_player.camera.height); |
|
|
|
if (distance >= SFG_SFX_MAX_DISTANCE) |
|
return 0; |
|
|
|
uint32_t result = 255 - (distance * 255) / SFG_SFX_MAX_DISTANCE; |
|
|
|
return (result * result) / 256; |
|
} |
|
|
|
/** |
|
Same as SFG_playerChangeHealth but for monsters. |
|
*/ |
|
void SFG_monsterChangeHealth(SFG_MonsterRecord *monster, int8_t healthAdd) |
|
{ |
|
int16_t health = monster->health; |
|
|
|
health += healthAdd; |
|
health = RCL_clamp(health,0,255); |
|
monster->health = health; |
|
|
|
if (healthAdd < 0) |
|
{ |
|
// play hurt sound |
|
|
|
uint8_t volume = SFG_distantSoundVolume( |
|
SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]), |
|
SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]), |
|
SFG_floorHeightAt( |
|
SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]), |
|
SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1]))); |
|
|
|
SFG_playGameSound(5,volume); |
|
|
|
if (monster->health == 0) |
|
SFG_playGameSound(2,volume); |
|
} |
|
} |
|
|
|
void SFG_removeItem(uint8_t index) |
|
{ |
|
SFG_LOG("removing item"); |
|
|
|
for (uint16_t j = index; j < SFG_currentLevel.itemRecordCount - 1; ++j) |
|
SFG_currentLevel.itemRecords[j] = |
|
SFG_currentLevel.itemRecords[j + 1]; |
|
|
|
SFG_currentLevel.itemRecordCount--; |
|
} |
|
|
|
/** |
|
Checks a 3D point visibility from player's position (WITHOUT considering |
|
facing direction). |
|
*/ |
|
static inline uint8_t SFG_spriteIsVisible(RCL_Vector2D pos, RCL_Unit height) |
|
{ |
|
return |
|
RCL_castRay3D( |
|
SFG_player.camera.position, |
|
SFG_player.camera.height, |
|
pos, |
|
height, |
|
SFG_floorHeightAt, |
|
SFG_ceilingHeightAt, |
|
SFG_game.visibilityRayConstraints |
|
) == RCL_UNITS_PER_SQUARE; |
|
} |
|
|
|
RCL_Unit SFG_directionTangent(RCL_Unit dirX, RCL_Unit dirY, RCL_Unit dirZ) |
|
{ |
|
RCL_Vector2D v; |
|
|
|
v.x = dirX; |
|
v.y = dirY; |
|
|
|
return (dirZ * RCL_UNITS_PER_SQUARE) / RCL_len(v); |
|
} |
|
|
|
/** |
|
Returns a tangent in RCL_Unit of vertical autoaim, given current game state. |
|
*/ |
|
RCL_Unit SFG_autoaimVertically() |
|
{ |
|
for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i) |
|
{ |
|
SFG_MonsterRecord m = SFG_currentLevel.monsterRecords[i]; |
|
|
|
uint8_t state = SFG_MR_STATE(m); |
|
|
|
if (state == SFG_MONSTER_STATE_INACTIVE || |
|
state == SFG_MONSTER_STATE_DEAD) |
|
continue; |
|
|
|
RCL_Vector2D worldPosition, toMonster; |
|
|
|
worldPosition.x = SFG_MONSTER_COORD_TO_RCL_UNITS(m.coords[0]); |
|
worldPosition.y = SFG_MONSTER_COORD_TO_RCL_UNITS(m.coords[1]); |
|
|
|
toMonster.x = worldPosition.x - SFG_player.camera.position.x; |
|
toMonster.y = worldPosition.y - SFG_player.camera.position.y; |
|
|
|
if (RCL_abs( |
|
RCL_vectorsAngleCos(SFG_player.direction,toMonster) |
|
- RCL_UNITS_PER_SQUARE) < SFG_VERTICAL_AUTOAIM_ANGLE_THRESHOLD) |
|
{ |
|
uint8_t spriteSize = SFG_GET_MONSTER_SPRITE_SIZE( |
|
SFG_MONSTER_TYPE_TO_INDEX(SFG_MR_TYPE(m))); |
|
|
|
RCL_Unit worldHeight = |
|
SFG_floorHeightAt( |
|
SFG_MONSTER_COORD_TO_SQUARES(m.coords[0]), |
|
SFG_MONSTER_COORD_TO_SQUARES(m.coords[1])) |
|
+ |
|
SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(spriteSize); |
|
|
|
if (SFG_spriteIsVisible(worldPosition,worldHeight)) |
|
return SFG_directionTangent(toMonster.x,toMonster.y, |
|
worldHeight - (SFG_player.camera.height)); |
|
} |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
/** |
|
Helper function, returns a pointer to level element representing item with |
|
given index, but only if the item is active (otherwise 0 is returned). |
|
*/ |
|
static inline const SFG_LevelElement *SFG_getActiveItemElement(uint8_t index) |
|
{ |
|
SFG_ItemRecord item = SFG_currentLevel.itemRecords[index]; |
|
|
|
if ((item & SFG_ITEM_RECORD_ACTIVE_MASK) == 0) |
|
return 0; |
|
|
|
return &(SFG_currentLevel.levelPointer->elements[item & |
|
~SFG_ITEM_RECORD_ACTIVE_MASK]); |
|
} |
|
|
|
static inline const SFG_LevelElement *SFG_getLevelElement(uint8_t index) |
|
{ |
|
SFG_ItemRecord item = SFG_currentLevel.itemRecords[index]; |
|
|
|
return &(SFG_currentLevel.levelPointer->elements[item & |
|
~SFG_ITEM_RECORD_ACTIVE_MASK]); |
|
} |
|
|
|
void SFG_createExplosion(RCL_Unit, RCL_Unit, RCL_Unit); // forward decl |
|
|
|
void SFG_explodeBarrel(uint8_t itemIndex, RCL_Unit x, RCL_Unit y, RCL_Unit z) |
|
{ |
|
const SFG_LevelElement *e = SFG_getLevelElement(itemIndex); |
|
SFG_setItemCollisionMapBit(e->coords[0],e->coords[1],0); |
|
SFG_removeItem(itemIndex); |
|
SFG_createExplosion(x,y,z); |
|
} |
|
|
|
void SFG_createExplosion(RCL_Unit x, RCL_Unit y, RCL_Unit z) |
|
{ |
|
SFG_ProjectileRecord explosion; |
|
|
|
SFG_playGameSound(2,SFG_distantSoundVolume(x,y,z)); |
|
SFG_processEvent(SFG_EVENT_EXPLOSION,0); |
|
|
|
explosion.type = SFG_PROJECTILE_EXPLOSION; |
|
|
|
explosion.position[0] = x; |
|
explosion.position[1] = y; |
|
explosion.position[2] = z; |
|
|
|
explosion.direction[0] = 0; |
|
explosion.direction[1] = 0; |
|
explosion.direction[2] = 0; |
|
|
|
explosion.doubleFramesToLive = RCL_nonZero( |
|
SFG_GET_PROJECTILE_FRAMES_TO_LIVE(SFG_PROJECTILE_EXPLOSION) / 2); |
|
|
|
SFG_createProjectile(explosion); |
|
|
|
uint8_t damage = SFG_getDamageValue(SFG_WEAPON_FIRE_TYPE_FIREBALL); |
|
|
|
if (SFG_taxicabDistance(x,y,z,SFG_player.camera.position.x, |
|
SFG_player.camera.position.y,SFG_player.camera.height) |
|
<= SFG_EXPLOSION_RADIUS) |
|
{ |
|
SFG_playerChangeHealth(-1 * damage); |
|
SFG_pushPlayerAway(x,y,SFG_EXPLOSION_PUSH_AWAY_DISTANCE); |
|
} |
|
|
|
for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i) |
|
{ |
|
SFG_MonsterRecord *monster = &(SFG_currentLevel.monsterRecords[i]); |
|
|
|
uint16_t state = SFG_MR_STATE(*monster); |
|
|
|
if ((state == SFG_MONSTER_STATE_INACTIVE) || |
|
(state == SFG_MONSTER_STATE_DEAD)) |
|
continue; |
|
|
|
RCL_Unit monsterHeight = |
|
SFG_floorHeightAt( |
|
SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]), |
|
SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1])) |
|
+ RCL_UNITS_PER_SQUARE / 2; |
|
|
|
if (SFG_taxicabDistance( |
|
SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]), |
|
SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]),monsterHeight, |
|
x,y,z) <= SFG_EXPLOSION_RADIUS) |
|
{ |
|
SFG_monsterChangeHealth(monster, |
|
-1 * SFG_getDamageValue(SFG_WEAPON_FIRE_TYPE_FIREBALL)); |
|
} |
|
} |
|
|
|
// explode nearby barrels |
|
|
|
if (damage >= SFG_BARREL_EXPLOSION_DAMAGE_THRESHOLD) |
|
for (uint16_t i = 0; i < SFG_currentLevel.itemRecordCount; ++i) |
|
{ |
|
SFG_ItemRecord item = SFG_currentLevel.itemRecords[i]; |
|
|
|
/* We DON'T check just active barrels but all, otherwise it looks weird |
|
that out of sight barrels in a line didn't explode.*/ |
|
|
|
SFG_LevelElement element = SFG_ITEM_RECORD_LEVEL_ELEMENT(item); |
|
|
|
if (element.type != SFG_LEVEL_ELEMENT_BARREL) |
|
continue; |
|
|
|
RCL_Unit elementX = |
|
element.coords[0] * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2; |
|
|
|
RCL_Unit elementY = |
|
element.coords[1] * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2; |
|
|
|
RCL_Unit elementHeight = |
|
SFG_floorHeightAt(element.coords[0],element.coords[1]); |
|
|
|
if (SFG_taxicabDistance( |
|
x,y,z,elementX,elementY,elementHeight) <= SFG_EXPLOSION_RADIUS) |
|
{ |
|
SFG_explodeBarrel(i,elementX,elementY,elementHeight); |
|
i--; |
|
} |
|
} |
|
} |
|
|
|
void SFG_createDust(RCL_Unit x, RCL_Unit y, RCL_Unit z) |
|
{ |
|
SFG_ProjectileRecord dust; |
|
|
|
dust.type = SFG_PROJECTILE_DUST; |
|
|
|
dust.position[0] = x; |
|
dust.position[1] = y; |
|
dust.position[2] = z; |
|
|
|
dust.direction[0] = 0; |
|
dust.direction[1] = 0; |
|
dust.direction[2] = 0; |
|
|
|
dust.doubleFramesToLive = |
|
RCL_nonZero(SFG_GET_PROJECTILE_FRAMES_TO_LIVE(SFG_PROJECTILE_DUST) / 2); |
|
|
|
SFG_createProjectile(dust); |
|
} |
|
|
|
void SFG_getMonsterWorldPosition(SFG_MonsterRecord *monster, RCL_Unit *x, |
|
RCL_Unit *y, RCL_Unit *z) |
|
{ |
|
*x = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]); |
|
*y = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]); |
|
*z = SFG_floorHeightAt( |
|
SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]), |
|
SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1])) |
|
+ RCL_UNITS_PER_SQUARE / 2; |
|
} |
|
|
|
void SFG_monsterPerformAI(SFG_MonsterRecord *monster) |
|
{ |
|
uint8_t state = SFG_MR_STATE(*monster); |
|
uint8_t type = SFG_MR_TYPE(*monster); |
|
uint8_t monsterNumber = SFG_MONSTER_TYPE_TO_INDEX(type); |
|
uint8_t attackType = SFG_GET_MONSTER_ATTACK_TYPE(monsterNumber); |
|
|
|
int8_t coordAdd[2]; |
|
|
|
coordAdd[0] = 0; |
|
coordAdd[1] = 0; |
|
|
|
uint8_t notRanged = |
|
(attackType == SFG_MONSTER_ATTACK_MELEE) || |
|
(attackType == SFG_MONSTER_ATTACK_EXPLODE); |
|
|
|
uint8_t monsterSquare[2]; |
|
/* because of some insanely retarded C++ compilers that error on narrowing |
|
conversion between { } we init this way: */ |
|
monsterSquare[0] = SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]); |
|
monsterSquare[1] = SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1]); |
|
|
|
RCL_Unit currentHeight = |
|
SFG_floorCollisionHeightAt(monsterSquare[0],monsterSquare[1]); |
|
|
|
if ( // ranged monsters: sometimes randomly attack |
|
!notRanged && |
|
(SFG_random() < |
|
SFG_GET_MONSTER_AGGRESSIVITY(SFG_MONSTER_TYPE_TO_INDEX(type))) |
|
) |
|
{ |
|
RCL_Vector2D pos; |
|
pos.x = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]); |
|
pos.y = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]); |
|
|
|
if (SFG_random() % 4 != 0 && |
|
SFG_spriteIsVisible(pos,currentHeight + // only if player is visible |
|
SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND( |
|
SFG_GET_MONSTER_SPRITE_SIZE( |
|
SFG_MONSTER_TYPE_TO_INDEX(type))))) |
|
{ |
|
// ranged attack |
|
|
|
state = SFG_MONSTER_STATE_ATTACKING; |
|
|
|
RCL_Vector2D dir; |
|
|
|
dir.x = SFG_player.camera.position.x - pos.x |
|
- 128 * SFG_MONSTER_AIM_RANDOMNESS + |
|
SFG_random() * SFG_MONSTER_AIM_RANDOMNESS; |
|
|
|
dir.y = SFG_player.camera.position.y - pos.y |
|
- 128 * SFG_MONSTER_AIM_RANDOMNESS + |
|
SFG_random() * SFG_MONSTER_AIM_RANDOMNESS; |
|
|
|
uint8_t projectile; |
|
|
|
switch (SFG_GET_MONSTER_ATTACK_TYPE(monsterNumber)) |
|
{ |
|
case SFG_MONSTER_ATTACK_FIREBALL: |
|
projectile = SFG_PROJECTILE_FIREBALL; |
|
break; |
|
|
|
case SFG_MONSTER_ATTACK_BULLET: |
|
projectile = SFG_PROJECTILE_BULLET; |
|
break; |
|
|
|
case SFG_MONSTER_ATTACK_PLASMA: |
|
projectile = SFG_PROJECTILE_PLASMA; |
|
break; |
|
|
|
case SFG_MONSTER_ATTACK_FIREBALL_BULLET: |
|
projectile = (SFG_random() < 128) ? |
|
SFG_PROJECTILE_FIREBALL : |
|
SFG_PROJECTILE_BULLET; |
|
break; |
|
|
|
case SFG_MONSTER_ATTACK_FIREBALL_PLASMA: |
|
projectile = (SFG_random() < 128) ? |
|
SFG_PROJECTILE_FIREBALL : |
|
SFG_PROJECTILE_PLASMA; |
|
break; |
|
|
|
default: |
|
projectile = SFG_PROJECTILE_NONE; |
|
break; |
|
} |
|
|
|
if (projectile == SFG_PROJECTILE_BULLET) |
|
SFG_playGameSound(0, |
|
SFG_distantSoundVolume( |
|
SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]), |
|
SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]), |
|
currentHeight) |
|
); |
|
|
|
RCL_Unit middleHeight = currentHeight + |
|
SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(SFG_GET_MONSTER_SPRITE_SIZE( |
|
SFG_MONSTER_TYPE_TO_INDEX(SFG_MR_TYPE(*monster)))); |
|
|
|
RCL_Unit verticalSpeed = ( |
|
((projectile != SFG_PROJECTILE_NONE) ? |
|
SFG_GET_PROJECTILE_SPEED_UPS(projectile) : 0) * |
|
SFG_directionTangent(dir.x,dir.y,SFG_player.camera.height - |
|
middleHeight)) / RCL_UNITS_PER_SQUARE; |
|
|
|
dir = RCL_normalize(dir); |
|
|
|
SFG_launchProjectile( |
|
projectile, |
|
pos, |
|
middleHeight, |
|
dir, |
|
verticalSpeed, |
|
SFG_PROJECTILE_SPAWN_OFFSET |
|
); |
|
} // if visible |
|
else |
|
state = SFG_MONSTER_STATE_IDLE; |
|
} |
|
else if (state == SFG_MONSTER_STATE_IDLE) |
|
{ |
|
if (notRanged) |
|
{ |
|
// non-ranged monsters: walk towards player |
|
|
|
RCL_Unit pX, pY, pZ; |
|
SFG_getMonsterWorldPosition(monster,&pX,&pY,&pZ); |
|
|
|
uint8_t isClose = // close to player? |
|
SFG_taxicabDistance(pX,pY,pZ, |
|
SFG_player.camera.position.x, |
|
SFG_player.camera.position.y, |
|
SFG_player.camera.height) <= SFG_MELEE_RANGE; |
|
|
|
if (!isClose) |
|
{ |
|
// walk towards player |
|
|
|
if (monsterSquare[0] > SFG_player.squarePosition[0]) |
|
{ |
|
if (monsterSquare[1] > SFG_player.squarePosition[1]) |
|
state = SFG_MONSTER_STATE_GOING_NW; |
|
else if (monsterSquare[1] < SFG_player.squarePosition[1]) |
|
state = SFG_MONSTER_STATE_GOING_SW; |
|
else |
|
state = SFG_MONSTER_STATE_GOING_W; |
|
} |
|
else if (monsterSquare[0] < SFG_player.squarePosition[0]) |
|
{ |
|
if (monsterSquare[1] > SFG_player.squarePosition[1]) |
|
state = SFG_MONSTER_STATE_GOING_NE; |
|
else if (monsterSquare[1] < SFG_player.squarePosition[1]) |
|
state = SFG_MONSTER_STATE_GOING_SE; |
|
else |
|
state = SFG_MONSTER_STATE_GOING_E; |
|
} |
|
else |
|
{ |
|
if (monsterSquare[1] > SFG_player.squarePosition[1]) |
|
state = SFG_MONSTER_STATE_GOING_N; |
|
else if (monsterSquare[1] < SFG_player.squarePosition[1]) |
|
state = SFG_MONSTER_STATE_GOING_S; |
|
} |
|
} |
|
else // is close |
|
{ |
|
// melee, close-up attack |
|
|
|
if (attackType == SFG_MONSTER_ATTACK_MELEE) |
|
{ |
|
// melee attack |
|
|
|
state = SFG_MONSTER_STATE_ATTACKING; |
|
|
|
SFG_playerChangeHealth( |
|
-1 * SFG_getDamageValue(SFG_WEAPON_FIRE_TYPE_MELEE)); |
|
|
|
SFG_playGameSound(3,255); |
|
} |
|
else // SFG_MONSTER_ATTACK_EXPLODE |
|
{ |
|
// explode |
|
|
|
SFG_createExplosion(pX,pY,pZ); |
|
monster->health = 0; |
|
} |
|
} |
|
} |
|
else // ranged monsters |
|
{ |
|
// choose walk direction randomly |
|
|
|
switch (SFG_random() % 8) |
|
{ |
|
case 0: state = SFG_MONSTER_STATE_GOING_E; break; |
|
case 1: state = SFG_MONSTER_STATE_GOING_W; break; |
|
case 2: state = SFG_MONSTER_STATE_GOING_N; break; |
|
case 3: state = SFG_MONSTER_STATE_GOING_S; break; |
|
case 4: state = SFG_MONSTER_STATE_GOING_NE; break; |
|
case 5: state = SFG_MONSTER_STATE_GOING_NW; break; |
|
case 6: state = SFG_MONSTER_STATE_GOING_SE; break; |
|
case 7: state = SFG_MONSTER_STATE_GOING_SW; break; |
|
default: break; |
|
} |
|
} |
|
} |
|
else if (state == SFG_MONSTER_STATE_ATTACKING) |
|
{ |
|
state = SFG_MONSTER_STATE_IDLE; |
|
} |
|
else |
|
{ |
|
int8_t add = 1; |
|
|
|
if (attackType == SFG_MONSTER_ATTACK_MELEE) |
|
add = 2; |
|
else if (attackType == SFG_MONSTER_ATTACK_EXPLODE) |
|
add = 3; |
|
|
|
if (state == SFG_MONSTER_STATE_GOING_E || |
|
state == SFG_MONSTER_STATE_GOING_NE || |
|
state == SFG_MONSTER_STATE_GOING_SE) |
|
coordAdd[0] = add; |
|
else if (state == SFG_MONSTER_STATE_GOING_W || |
|
state == SFG_MONSTER_STATE_GOING_SW || |
|
state == SFG_MONSTER_STATE_GOING_NW) |
|
coordAdd[0] = -1 * add; |
|
|
|
if (state == SFG_MONSTER_STATE_GOING_N || |
|
state == SFG_MONSTER_STATE_GOING_NE || |
|
state == SFG_MONSTER_STATE_GOING_NW) |
|
coordAdd[1] = -1 * add; |
|
else if (state == SFG_MONSTER_STATE_GOING_S || |
|
state == SFG_MONSTER_STATE_GOING_SE || |
|
state == SFG_MONSTER_STATE_GOING_SW) |
|
coordAdd[1] = add; |
|
|
|
if ((coordAdd[0] != 0 || coordAdd[1] != 0) && SFG_random() < |
|
SFG_MONSTER_SOUND_PROBABILITY) |
|
SFG_playGameSound(5, |
|
SFG_distantSoundVolume( |
|
SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]), |
|
SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]), |
|
currentHeight) / 2); |
|
|
|
state = SFG_MONSTER_STATE_IDLE; |
|
} |
|
|
|
int16_t newPos[2]; |
|
|
|
newPos[0] = monster->coords[0] + coordAdd[0]; |
|
newPos[1] = monster->coords[1] + coordAdd[1]; |
|
|
|
int8_t collision = 0; |
|
|
|
if (newPos[0] < 0 || newPos[0] >= 256 || newPos[1] < 0 || newPos[1] >= 256) |
|
{ |
|
collision = 1; |
|
} |
|
else |
|
{ |
|
uint8_t movingDiagonally = (coordAdd[0] != 0) && (coordAdd[1] != 0); |
|
|
|
// when moving diagonally, we need to check extra tiles |
|
|
|
for (uint8_t i = 0; i < (1 + movingDiagonally); ++i) |
|
{ |
|
newPos[0] = monster->coords[0] + (i != 1) * coordAdd[0]; |
|
|
|
RCL_Unit newHeight = |
|
SFG_floorCollisionHeightAt( |
|
SFG_MONSTER_COORD_TO_SQUARES(newPos[0]), |
|
SFG_MONSTER_COORD_TO_SQUARES(newPos[1])); |
|
|
|
collision = |
|
RCL_abs(currentHeight - newHeight) > RCL_CAMERA_COLL_STEP_HEIGHT; |
|
|
|
if (!collision) |
|
collision = (SFG_ceilingHeightAt( |
|
SFG_MONSTER_COORD_TO_SQUARES(newPos[0]), |
|
SFG_MONSTER_COORD_TO_SQUARES(newPos[1])) - newHeight) < |
|
SFG_MONSTER_COLLISION_HEIGHT; |
|
|
|
if (collision) |
|
break; |
|
} |
|
|
|
newPos[0] = monster->coords[0] + coordAdd[0]; |
|
} |
|
|
|
if (collision) |
|
{ |
|
state = SFG_MONSTER_STATE_IDLE; |
|
// ^ will force the monster to choose random direction in the next update |
|
|
|
newPos[0] = monster->coords[0]; |
|
newPos[1] = monster->coords[1]; |
|
} |
|
|
|
monster->stateType = state | (monsterNumber << 4); |
|
monster->coords[0] = newPos[0]; |
|
monster->coords[1] = newPos[1];; |
|
} |
|
|
|
static inline uint8_t SFG_elementCollides( |
|
RCL_Unit pointX, |
|
RCL_Unit pointY, |
|
RCL_Unit pointZ, |
|
RCL_Unit elementX, |
|
RCL_Unit elementY, |
|
RCL_Unit elementHeight |
|
) |
|
{ |
|
return |
|
SFG_taxicabDistance(pointX,pointY,pointZ,elementX,elementY,elementHeight) |
|
<= SFG_ELEMENT_COLLISION_RADIUS; |
|
} |
|
|
|
/** |
|
Checks collision of a projectile with level element at given position. |
|
*/ |
|
uint8_t SFG_projectileCollides(SFG_ProjectileRecord *projectile, |
|
RCL_Unit x, RCL_Unit y, RCL_Unit z) |
|
{ |
|
if (!SFG_elementCollides(x,y,z, |
|
projectile->position[0],projectile->position[1],projectile->position[2])) |
|
return 0; |
|
|
|
if ((projectile->type == SFG_PROJECTILE_EXPLOSION) || |
|
(projectile->type == SFG_PROJECTILE_DUST)) |
|
return 0; |
|
|
|
/* For directional projectiles we only register a collision if its direction |
|
is "towards" the element so that the shooter doesn't get shot by his own |
|
projectile. */ |
|
|
|
RCL_Vector2D projDir, toElement; |
|
|
|
projDir.x = projectile->direction[0]; |
|
projDir.y = projectile->direction[1]; |
|
|
|
toElement.x = x - projectile->position[0]; |
|
toElement.y = y - projectile->position[1]; |
|
|
|
return RCL_vectorsAngleCos(projDir,toElement) >= 0; |
|
} |
|
|
|
/** |