anarch/main.c

2635 lines
67 KiB
C
Raw Normal View History

2019-10-03 18:04:14 -04:00
/**
@file main.c
Main source file of the game that puts together all the pieces. main game
logic is implemented here.
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.
*/
2019-09-25 09:51:19 -04:00
#include <stdint.h>
2019-10-02 08:42:30 -04:00
/*
The following keys are mandatory to be implemented on any platform in order
for the game to be playable.
*/
2019-09-25 09:51:19 -04:00
#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
#define SFG_KEY_B 5
#define SFG_KEY_C 6
2019-10-14 09:56:26 -04:00
2019-10-02 08:42:30 -04:00
/*
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
2019-10-08 07:46:12 -04:00
#define SFG_KEY_MAP 10
2019-09-25 09:51:19 -04:00
2019-10-14 09:56:26 -04:00
#define SFG_KEY_COUNT 10 ///< Number of keys.
2019-09-25 09:51:19 -04:00
/* ============================= PORTING =================================== */
2019-09-27 06:46:44 -04:00
/* When porting, do the following:
- implement the following functions in your platform_*.h.
- Call SFG_init() from your platform initialization code.
- Call SFG_mainLoopBody() from within your platform's main loop.
- include "settings.h" in your platform_*.h and optionally hard-override
(redefine) some settings in platform_*.h, according to the platform's
needs.
*/
2019-09-25 09:51:19 -04:00
2019-09-29 07:50:40 -04:00
#define SFG_LOG(str) ; ///< Can be redefined to log messages for better debug.
2019-10-02 14:31:27 -04:00
#define SFG_PROGRAM_MEMORY static const /**< Can be redefined to platform's
specifier of program meory. */
2019-10-02 08:42:30 -04:00
/** Return 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 contant definitions to see which ones are mandatory. */
2019-09-25 09:51:19 -04:00
int8_t SFG_keyPressed(uint8_t key);
/** Return time in ms 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. The function doesn't have to check whether
the coordinates are within screen. */
static inline void SFG_setPixel(uint16_t x, uint16_t y, uint8_t colorIndex);
/* ========================================================================= */
/**
Game main loop body, call this inside the platform's specific main loop.
*/
void SFG_mainLoopBody();
/**
Initializes the whole program, call this in the platform initialization.
*/
void SFG_init();
2019-09-27 10:38:55 -04:00
#ifdef SFG_PLATFORM_POKITTO
#include "platform_pokitto.h"
#else
#include "platform_sdl.h"
#endif
2019-09-25 09:51:19 -04:00
2019-10-02 14:31:27 -04:00
#include "constants.h"
#include "levels.h"
#include "assets.h"
#include "palette.h"
2019-10-04 15:09:10 -04:00
#include "settings.h" // will include if not included by platform
#define RCL_PIXEL_FUNCTION SFG_pixelFunc
#define RCL_TEXTURE_VERTICAL_STRETCH 0
#define RCL_CAMERA_COLL_HEIGHT_BELOW 800
2019-10-14 07:31:13 -04:00
#define RCL_CAMERA_COLL_HEIGHT_ABOVE 150
2019-10-04 15:09:10 -04:00
#include "raycastlib.h"
2019-10-02 14:31:27 -04:00
2019-10-14 14:21:18 -04:00
/*
CONSTANTS
===============================================================================
*/
2019-10-23 18:32:04 -04:00
#define SFG_WEAPON_KNIFE 0
#define SFG_WEAPON_SHOTGUN 1
2019-10-23 20:03:57 -04:00
#define SFG_WEAPON_MACHINE_GUN 2
2019-11-09 15:45:32 -05:00
#define SFG_WEAPON_ROCKET_LAUNCHER 3
2019-11-08 15:12:46 -05:00
#define SFG_WEAPON_PLASMAGUN 4
2019-10-23 20:03:57 -04:00
2019-11-08 15:12:46 -05:00
#define SFG_WEAPONS_TOTAL 5
2019-10-23 18:32:04 -04:00
2019-10-04 10:32:24 -04:00
#define SFG_GAME_RESOLUTION_X \
(SFG_SCREEN_RESOLUTION_X / SFG_RESOLUTION_SCALEDOWN)
#define SFG_GAME_RESOLUTION_Y \
(SFG_SCREEN_RESOLUTION_Y / SFG_RESOLUTION_SCALEDOWN)
2019-09-25 09:51:19 -04:00
#define SFG_MS_PER_FRAME (1000 / SFG_FPS) // ms per frame with target FPS
2019-10-04 15:09:10 -04:00
#if SFG_MS_PER_FRAME == 0
#define SFG_MS_PER_FRAME 1
#endif
2019-10-13 19:47:41 -04:00
#define SFG_WEAPON_IMAGE_SCALE \
(SFG_GAME_RESOLUTION_X / (SFG_TEXTURE_SIZE * 5))
#if SFG_WEAPON_IMAGE_SCALE == 0
#define SFG_WEAPON_IMAGE_SCALE 1
#endif
2019-10-13 20:15:13 -04:00
#define SFG_WEAPONBOB_OFFSET_PIXELS \
(SFG_WEAPONBOB_OFFSET * SFG_WEAPON_IMAGE_SCALE)
2019-10-13 19:47:41 -04:00
#define SFG_WEAPON_IMAGE_POSITION_X \
(SFG_GAME_RESOLUTION_X / 2 - (SFG_WEAPON_IMAGE_SCALE * SFG_TEXTURE_SIZE) / 2)
#define SFG_WEAPON_IMAGE_POSITION_Y \
(SFG_GAME_RESOLUTION_Y - (SFG_WEAPON_IMAGE_SCALE * SFG_TEXTURE_SIZE))
2019-10-04 15:09:10 -04:00
#define SFG_PLAYER_TURN_UNITS_PER_FRAME \
2019-09-27 10:38:55 -04:00
((SFG_PLAYER_TURN_SPEED * RCL_UNITS_PER_SQUARE) / (360 * SFG_FPS))
2019-10-04 15:09:10 -04:00
#if SFG_PLAYER_TURN_UNITS_PER_FRAME == 0
#define SFG_PLAYER_TURN_UNITS_PER_FRAME 1
#endif
2019-09-27 10:38:55 -04:00
2019-10-04 15:09:10 -04:00
#define SFG_PLAYER_MOVE_UNITS_PER_FRAME \
((SFG_PLAYER_MOVE_SPEED * RCL_UNITS_PER_SQUARE) / SFG_FPS)
2019-10-01 14:25:21 -04:00
2019-10-04 15:09:10 -04:00
#if SFG_PLAYER_MOVE_UNITS_PER_FRAME == 0
#define SFG_PLAYER_MOVE_UNITS_PER_FRAME 1
#endif
2019-09-25 09:51:19 -04:00
2019-11-09 15:45:32 -05:00
#define SFG_FIREBALL_MOVE_UNITS_PER_FRAME \
((SFG_FIREBALL_SPEED * RCL_UNITS_PER_SQUARE) / SFG_FPS)
2019-10-21 09:21:22 -04:00
2019-11-09 15:45:32 -05:00
#if SFG_FIREBALL_MOVE_UNITS_PER_FRAME == 0
#define SFG_FIREBALL_MOVE_UNITS_PER_FRAME 1
2019-10-21 09:21:22 -04:00
#endif
2019-10-04 15:09:10 -04:00
#define SFG_GRAVITY_SPEED_INCREASE_PER_FRAME \
((SFG_GRAVITY_ACCELERATION * RCL_UNITS_PER_SQUARE) / (SFG_FPS * SFG_FPS))
2019-10-01 14:25:21 -04:00
2019-10-04 15:09:10 -04:00
#if SFG_GRAVITY_SPEED_INCREASE_PER_FRAME == 0
#define SFG_GRAVITY_SPEED_INCREASE_PER_FRAME 1
#endif
2019-09-25 09:51:19 -04:00
2019-10-09 11:44:31 -04:00
#define SFG_HEADBOB_FRAME_INCREASE_PER_FRAME \
(SFG_HEADBOB_SPEED / SFG_FPS)
#if SFG_HEADBOB_FRAME_INCREASE_PER_FRAME == 0
#define SFG_HEADBOB_FRAME_INCREASE_PER_FRAME 1
#endif
#define SFG_HEADBOB_ENABLED (SFG_HEADBOB_SPEED > 0 && SFG_HEADBOB_OFFSET > 0)
2019-10-11 09:01:36 -04:00
#define SFG_CAMERA_SHEAR_STEP_PER_FRAME \
((SFG_GAME_RESOLUTION_Y * SFG_CAMERA_SHEAR_SPEED) / SFG_FPS)
#if SFG_CAMERA_SHEAR_STEP_PER_FRAME == 0
#define SFG_CAMERA_SHEAR_STEP_PER_FRAME 1
#endif
#define SFG_CAMERA_MAX_SHEAR_PIXELS \
(SFG_CAMERA_MAX_SHEAR * SFG_GAME_RESOLUTION_Y / 1024)
2019-10-10 19:03:56 -04:00
#define SFG_FONT_SIZE_SMALL \
(SFG_GAME_RESOLUTION_X / (SFG_FONT_CHARACTER_SIZE * 50))
#if SFG_FONT_SIZE_SMALL == 0
#define SFG_FONT_SIZE_SMALL 1
#endif
#define SFG_FONT_SIZE_MEDIUM \
(SFG_GAME_RESOLUTION_X / (SFG_FONT_CHARACTER_SIZE * 30))
#if SFG_FONT_SIZE_MEDIUM == 0
#define SFG_FONT_SIZE_MEDIUM 1
#endif
#define SFG_FONT_SIZE_BIG \
(SFG_GAME_RESOLUTION_X / (SFG_FONT_CHARACTER_SIZE * 18))
#if SFG_FONT_SIZE_BIG == 0
#define SFG_FONT_SIZE_BIG 1
#endif
2019-10-21 18:15:07 -04:00
#define SFG_Z_BUFFER_SIZE SFG_GAME_RESOLUTION_X
2019-10-13 20:29:13 -04:00
2019-09-26 21:34:49 -04:00
/**
Step in which walls get higher, in raycastlib units.
*/
#define SFG_WALL_HEIGHT_STEP (RCL_UNITS_PER_SQUARE / 4)
2019-09-30 13:39:21 -04:00
#define SFG_CEILING_MAX_HEIGHT\
(16 * RCL_UNITS_PER_SQUARE - RCL_UNITS_PER_SQUARE / 2 )
2019-10-14 14:21:18 -04:00
#define SFG_DOOR_DEFAULT_STATE 0x1f
#define SFG_DOOR_UP_DOWN_MASK 0x20
#define SFG_DOOR_VERTICAL_POSITION_MASK 0x1f
#define SFG_DOOR_HEIGHT_STEP (RCL_UNITS_PER_SQUARE / 0x1f)
#define SFG_DOOR_INCREMENT_PER_FRAME \
(SFG_DOOR_OPEN_SPEED / (SFG_DOOR_HEIGHT_STEP * SFG_FPS))
2019-10-04 10:32:24 -04:00
2019-10-14 14:21:18 -04:00
#if SFG_DOOR_INCREMENT_PER_FRAME == 0
#define SFG_DOOR_INCREMENT_PER_FRAME 1
2019-10-04 10:32:24 -04:00
#endif
2019-10-14 14:21:18 -04:00
#define SFG_MAX_DOORS 32
2019-10-11 09:01:36 -04:00
2019-10-14 14:21:18 -04:00
#define SFG_ITEM_RECORD_ACTIVE_MASK 0x80
2019-10-01 14:25:21 -04:00
2019-10-17 15:19:32 -04:00
#define SFG_MAX_ITEMS SFG_MAX_LEVEL_ELEMENTS
2019-10-09 11:44:31 -04:00
2019-10-14 14:21:18 -04:00
#define SFG_MAX_SPRITE_SIZE SFG_GAME_RESOLUTION_X
2019-10-14 07:31:13 -04:00
2019-10-14 14:21:18 -04:00
#define SFG_MAP_PIXEL_SIZE (SFG_GAME_RESOLUTION_Y / SFG_MAP_SIZE)
2019-10-01 14:25:21 -04:00
2019-10-14 14:21:18 -04:00
#if SFG_MAP_PIXEL_SIZE == 0
#define SFG_MAP_SIZE 1
#endif
2019-10-01 14:25:21 -04:00
2019-10-02 14:31:27 -04:00
typedef struct
{
uint8_t coords[2];
uint8_t state; /**< door state in format:
2019-10-04 11:18:46 -04:00
MSB ccbaaaaa LSB
2019-10-02 14:31:27 -04:00
aaaaa: current door height (how much they're open)
b: whether currently going up (0) or down (1)
cc: by which keys the door is unlocked
*/
2019-10-04 11:18:46 -04:00
} SFG_DoorRecord;
2019-10-02 14:31:27 -04:00
2019-10-06 10:16:59 -04:00
/**
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;
2019-10-17 15:19:32 -04:00
typedef struct
{
uint8_t stateType; /**< Holds state (lower 4 bits) and type of monster
(upper 4 bits). */
2019-10-21 17:50:19 -04:00
uint8_t coords[2]; /**< Monster position, in 1/4s of a square */
2019-10-17 15:19:32 -04:00
uint8_t health;
} SFG_MonsterRecord;
2019-12-26 09:28:23 -05:00
#define SFG_MR_STATE(mr) ((mr).stateType & SFG_MONSTER_MASK_STATE)
#define SFG_MR_TYPE(mr) ((mr).stateType & SFG_MONSTER_MASK_TYPE)
2019-10-21 17:50:19 -04:00
#define SFG_MONSTER_COORD_TO_RCL_UNITS(c) (c * 256)
2019-10-21 19:34:40 -04:00
#define SFG_MONSTER_COORD_TO_SQUARES(c) (c / 4)
2019-10-21 17:50:19 -04:00
2019-10-22 06:13:10 -04:00
#define SFG_ELEMENT_COORD_TO_RCL_UNITS(c) \
(c * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2)
2019-10-18 09:25:07 -04:00
#define SFG_MONSTER_MASK_STATE 0x0f
#define SFG_MONSTER_MASK_TYPE 0xf0
2019-10-17 15:19:32 -04:00
#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
2019-10-18 09:25:07 -04:00
#define SFG_MONSTER_STATE_GOING_E 7
2019-10-18 10:34:51 -04:00
#define SFG_MONSTER_STATE_GOING_SE 8
2019-10-18 09:25:07 -04:00
#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
2019-10-17 15:19:32 -04:00
#define SFG_MAX_MONSTERS 64
2019-10-21 08:54:32 -04:00
// TODO: ^ move these MAX constants to constants.h?
2019-10-17 15:19:32 -04:00
2019-10-17 17:58:19 -04:00
#define SFG_AI_UPDATE_FRAME_INTERVAL \
2019-10-18 10:34:51 -04:00
(SFG_FPS / SFG_AI_FPS)
2019-10-17 17:58:19 -04:00
#if SFG_AI_UPDATE_FRAME_INTERVAL == 0
#define SFG_AI_UPDATE_FRAME_INTERVAL 1
#endif
2019-10-20 17:04:28 -04:00
typedef struct
{
uint8_t type;
2019-10-21 13:58:11 -04:00
uint8_t doubleFramesToLive; /**< This number times two (because 256 could be
too little at high FPS) says after how many
frames the projectile is destroyed. */
2019-10-20 17:04:28 -04:00
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. */
2019-10-21 08:54:32 -04:00
} SFG_ProjectileRecord;
#define SFG_MAX_PROJECTILES 12
2019-10-20 17:04:28 -04:00
2019-10-21 13:58:11 -04:00
#define SFG_PROJECTILE_EXPLOSION 0
#define SFG_PROJECTILE_FIREBALL 1
2019-11-09 05:59:52 -05:00
#define SFG_PROJECTILE_PLASMA 2
2019-10-21 13:58:11 -04:00
#define SFG_EXPLOSION_DURATION_DOUBLE_FRAMES \
(SFG_EXPLOSION_DURATION / SFG_MS_PER_FRAME)
#if SFG_EXPLOSION_DURATION_DOUBLE_FRAMES == 0
#define SFG_EXPLOSION_DURATION_FRAMES 1
#endif
2019-10-22 10:48:27 -04:00
#define SFG_SPRITE_ANIMATION_FRAME_DURATION \
(SFG_FPS / SFG_SPRITE_ANIMATION_SPEED)
#if SFG_SPRITE_ANIMATION_FRAME_DURATION == 0
#define SFG_SPRITE_ANIMATION_FRAME_DURATION 1
#endif
2019-10-22 15:02:39 -04:00
#define SFG_WEAPON_SHOTGUN_COOLDOWN_FRAMES \
(SFG_WEAPON_SHOTGUN_COOLDOWN / SFG_MS_PER_FRAME)
2019-11-09 16:01:19 -05:00
#define SFG_HUD_MARGIN (SFG_GAME_RESOLUTION_X / 40)
2019-10-23 18:09:46 -04:00
2019-10-25 17:17:59 -04:00
#define SFG_HUD_HEALTH_INDICATOR_WIDTH_PIXELS \
(SFG_GAME_RESOLUTION_Y / SFG_HUD_HEALTH_INDICATOR_WIDTH)
2019-10-25 16:41:52 -04:00
2019-10-25 17:17:59 -04:00
#define SFG_HUD_HEALTH_INDICATOR_DURATION_FRAMES \
(SFG_HUD_HEALTH_INDICATOR_DURATION / SFG_MS_PER_FRAME)
2019-10-25 16:41:52 -04:00
2019-10-25 17:17:59 -04:00
#if SFG_HUD_HEALTH_INDICATOR_DURATION_FRAMES == 0
#define SFG_HUD_HEALTH_INDICATOR_DURATION_FRAMES 1
2019-10-25 16:41:52 -04:00
#endif
2019-11-09 16:01:19 -05:00
#define SFG_HUD_BAR_HEIGHT \
(SFG_FONT_CHARACTER_SIZE * SFG_FONT_SIZE_MEDIUM + SFG_HUD_MARGIN * 2 + 1)
2019-10-14 14:21:18 -04:00
/*
GLOBAL VARIABLES
===============================================================================
*/
2019-10-06 10:47:47 -04:00
2019-10-18 10:34:51 -04:00
uint8_t SFG_currentRandom;
2019-10-22 10:48:27 -04:00
uint8_t SFG_spriteAnimationFrame;
2019-10-14 14:21:18 -04:00
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;
2019-10-22 15:02:39 -04:00
uint8_t weapon; ///< currently selected weapon
2019-10-23 18:09:46 -04:00
uint8_t health;
2019-10-25 16:41:52 -04:00
uint32_t lastShotFrame; ///< frame at which last shot was fired
uint32_t lastHurtFrame;
2019-10-14 14:21:18 -04:00
} SFG_player;
RCL_RayConstraints SFG_rayConstraints;
/**
Pressed states of keys, LSB of each value means current pessed states, other
bits store states in previous frames.
*/
uint8_t SFG_keyStates[SFG_KEY_COUNT];
uint8_t SFG_zBuffer[SFG_Z_BUFFER_SIZE];
int8_t SFG_backgroundScaleMap[SFG_GAME_RESOLUTION_Y];
uint16_t SFG_backgroundScroll;
2019-10-17 15:19:32 -04:00
/** Helper for precomputing sprite sampling positions for drawing. */
2019-10-14 14:21:18 -04:00
uint8_t SFG_spriteSamplingPoints[SFG_MAX_SPRITE_SIZE];
2019-10-17 15:19:32 -04:00
2019-10-14 14:21:18 -04:00
uint32_t SFG_frameTime; ///< Keeps a constant time (in ms) during a frame
uint32_t SFG_gameFrame;
uint32_t SFG_lastFrameTimeMs;
2019-10-06 10:16:59 -04:00
2019-09-26 15:15:07 -04:00
/**
Stores the current level and helper precomputed vaues for better performance.
*/
struct
{
const SFG_Level *levelPointer;
const uint8_t* textures[7];
2019-10-06 10:16:59 -04:00
2019-09-26 21:34:49 -04:00
uint32_t timeStart;
2019-10-04 11:42:54 -04:00
uint32_t frameStart;
2019-10-06 10:16:59 -04:00
2019-09-29 14:26:53 -04:00
uint8_t floorColor;
uint8_t ceilingColor;
2019-10-06 10:16:59 -04:00
SFG_DoorRecord doorRecords[SFG_MAX_DOORS];
2019-10-02 14:31:27 -04:00
uint8_t doorRecordCount;
2019-10-04 11:42:54 -04:00
uint8_t checkedDoorIndex; ///< Says which door are currently being checked.
2019-10-06 10:16:59 -04:00
2019-10-17 15:19:32 -04:00
SFG_ItemRecord itemRecords[SFG_MAX_ITEMS]; ///< Holds level items
2019-10-06 10:16:59 -04:00
uint8_t itemRecordCount;
2019-10-06 10:47:47 -04:00
uint8_t checkedItemIndex; ///< Same as checkedDoorIndex, but for items.
2019-10-17 15:19:32 -04:00
SFG_MonsterRecord monsterRecords[SFG_MAX_MONSTERS];
uint8_t monsterRecordCount;
uint8_t checkedMonsterIndex;
2019-10-21 08:54:32 -04:00
SFG_ProjectileRecord projectileRecords[SFG_MAX_PROJECTILES];
uint8_t projectileRecordCount;
2019-09-26 15:15:07 -04:00
} SFG_currentLevel;
2019-10-03 17:37:52 -04:00
#if SFG_DITHERED_SHADOW
SFG_PROGRAM_MEMORY 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
2019-10-14 14:21:18 -04:00
/*
FUNCTIONS
===============================================================================
*/
2019-10-18 10:34:51 -04:00
/**
Returns a pseudorandom byte. This is a congrent generator, its parameters
have been chosen so that each number (0-255) is included in the output
exactly once!
*/
uint8_t SFG_random()
{
SFG_currentRandom *= 13;
SFG_currentRandom += 7;
return SFG_currentRandom;
}
2019-10-21 19:34:40 -04:00
static inline RCL_Unit
SFG_taxicabDistance(RCL_Unit x0, RCL_Unit y0, RCL_Unit x1, RCL_Unit y1)
{
return RCL_absVal(x0 - x1) + RCL_absVal(y0 - y1);
}
2019-10-22 17:59:10 -04:00
static inline uint8_t SFG_RCLUnitToZBuffer(RCL_Unit x)
2019-10-18 11:06:38 -04:00
{
x /= RCL_UNITS_PER_SQUARE;
uint8_t okay = x < 255;
return okay * (x + 1) - 1;
2019-10-18 11:41:55 -04:00
}
2019-10-18 11:06:38 -04:00
2019-10-22 18:11:26 -04:00
const uint8_t *SFG_getMonsterSprite(
uint8_t monsterType, uint8_t state, uint8_t frame)
{
2019-12-27 11:17:14 -05:00
uint8_t index = 0; // makes the code smaller compared to returning pointers
2019-10-22 18:11:26 -04:00
switch (monsterType)
{
case SFG_LEVEL_ELEMENT_MONSTER_SPIDER:
switch (state)
{
2019-12-27 11:17:14 -05:00
case SFG_MONSTER_STATE_ATTACKING: index = 1; break;
case SFG_MONSTER_STATE_IDLE: index = 0; break;
default: index = frame ? 0 : 2; break;
2019-10-22 18:11:26 -04:00
}
break;
2019-10-24 17:51:24 -04:00
case SFG_LEVEL_ELEMENT_MONSTER_WARRIOR:
2019-12-27 11:17:14 -05:00
index = state != SFG_MONSTER_STATE_ATTACKING ? 6 : 7;
2019-10-24 17:51:24 -04:00
break;
2019-10-24 15:43:13 -04:00
case SFG_LEVEL_ELEMENT_MONSTER_DESTROYER:
switch (state)
{
2019-12-27 11:17:14 -05:00
case SFG_MONSTER_STATE_ATTACKING: index = 4; break;
case SFG_MONSTER_STATE_IDLE: index = 3; break;
default: index = frame ? 3 : 5; break;
2019-10-24 15:43:13 -04:00
}
break;
2019-11-09 10:32:13 -05:00
case SFG_LEVEL_ELEMENT_MONSTER_PLASMABOT:
2019-12-27 11:17:14 -05:00
index = state != SFG_MONSTER_STATE_ATTACKING ? 8 : 9;
2019-11-09 10:32:13 -05:00
break;
2019-11-11 18:56:31 -05:00
case SFG_LEVEL_ELEMENT_MONSTER_ENDER:
switch (state)
{
2019-12-27 11:17:14 -05:00
case SFG_MONSTER_STATE_ATTACKING: index = 12; break;
case SFG_MONSTER_STATE_IDLE: index = 10; break;
default: index = frame ? 10 : 11; break;
2019-11-11 18:56:31 -05:00
}
break;
2019-12-27 07:39:16 -05:00
case SFG_LEVEL_ELEMENT_MONSTER_TURRET:
switch (state)
{
2019-12-27 11:17:14 -05:00
case SFG_MONSTER_STATE_ATTACKING: index = 15; break;
case SFG_MONSTER_STATE_IDLE: index = 13; break;
default: index = frame ? 13 : 14; break;
2019-12-27 07:39:16 -05:00
}
break;
2019-12-27 18:06:11 -05:00
case SFG_LEVEL_ELEMENT_MONSTER_EXPLODER:
2019-10-22 18:11:26 -04:00
default:
2019-12-27 18:06:11 -05:00
index = 16;
2019-10-22 18:11:26 -04:00
break;
}
2019-12-27 11:17:14 -05:00
return SFG_monsterSprites[index];
2019-10-22 18:11:26 -04:00
}
2019-10-14 14:21:18 -04:00
/**
Says whether given key is currently pressed (down). This should be preferred
to SFG_keyPressed().
*/
uint8_t SFG_keyIsDown(uint8_t key)
{
return SFG_keyStates[key] & 0x01;
}
/**
Says whether given key has been pressed in the current frame.
*/
uint8_t SFG_keyJustPressed(uint8_t key)
{
return (SFG_keyStates[key] & 0x03) == 1;
}
#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_recompurePLayerDirection()
{
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_backgroundScroll =
((SFG_player.camera.direction * 8) * SFG_GAME_RESOLUTION_Y)
/ RCL_UNITS_PER_SQUARE;
}
void SFG_initPlayer()
{
RCL_initCamera(&SFG_player.camera);
SFG_player.camera.resolution.x =
SFG_GAME_RESOLUTION_X / SFG_RAYCASTING_SUBSAMPLE;
2019-11-09 16:01:19 -05:00
SFG_player.camera.resolution.y = SFG_GAME_RESOLUTION_Y - SFG_HUD_BAR_HEIGHT;
2019-10-14 14:21:18 -04:00
SFG_player.camera.height = RCL_UNITS_PER_SQUARE * 12;
SFG_player.camera.position.x = RCL_UNITS_PER_SQUARE * 15;
SFG_player.camera.position.y = RCL_UNITS_PER_SQUARE * 8;
SFG_recompurePLayerDirection();
2019-10-23 18:09:46 -04:00
2019-10-14 14:21:18 -04:00
SFG_player.previousVerticalSpeed = 0;
SFG_player.headBobFrame = 0;
2019-10-22 15:21:56 -04:00
SFG_player.weapon = 2;
2019-10-22 15:02:39 -04:00
SFG_player.lastShotFrame = SFG_gameFrame;
2019-10-25 16:41:52 -04:00
SFG_player.lastHurtFrame = SFG_gameFrame;
2019-10-23 18:09:46 -04:00
SFG_player.health = SFG_PLAYER_MAX_HEALTH;
2019-10-14 14:21:18 -04:00
}
2019-09-25 09:51:19 -04:00
void SFG_pixelFunc(RCL_PixelInfo *pixel)
{
uint8_t color;
uint8_t shadow = 0;
2019-10-06 19:18:42 -04:00
if (pixel->position.y == SFG_GAME_RESOLUTION_Y / 2)
2019-10-21 18:15:07 -04:00
for (uint8_t i = 0; i < SFG_RAYCASTING_SUBSAMPLE; ++i)
SFG_zBuffer[pixel->position.x * SFG_RAYCASTING_SUBSAMPLE + i] =
2019-10-22 17:59:10 -04:00
SFG_RCLUnitToZBuffer(pixel->depth);
2019-10-06 19:18:42 -04:00
2019-09-29 10:08:38 -04:00
if (pixel->isHorizon && pixel->depth > RCL_UNITS_PER_SQUARE * 16)
{
color = SFG_TRANSPARENT_COLOR;
}
else if (pixel->isWall)
2019-09-25 09:51:19 -04:00
{
2019-09-26 15:15:07 -04:00
uint8_t textureIndex =
pixel->isFloor ?
2019-09-27 13:04:49 -04:00
(
((pixel->hit.type & SFG_TILE_PROPERTY_MASK) != SFG_TILE_PROPERTY_DOOR) ?
(pixel->hit.type & 0x7)
:
(
2019-09-28 15:12:16 -04:00
(pixel->texCoords.y > RCL_UNITS_PER_SQUARE) ?
2019-09-29 10:08:38 -04:00
(pixel->hit.type & 0x7) : 255
2019-09-27 13:04:49 -04:00
)
):
2019-09-26 15:15:07 -04:00
((pixel->hit.type & 0x38) >> 3);
2019-09-29 14:17:31 -04:00
2019-09-27 20:08:28 -04:00
RCL_Unit textureV = pixel->texCoords.y;
2019-09-26 22:41:45 -04:00
2019-09-27 13:04:49 -04:00
if ((pixel->hit.type & SFG_TILE_PROPERTY_MASK) ==
SFG_TILE_PROPERTY_SQUEEZER)
2019-09-27 09:24:36 -04:00
textureV += pixel->wallHeight;
2019-09-26 22:41:45 -04:00
2019-09-25 19:34:57 -04:00
color =
2019-09-26 15:15:07 -04:00
textureIndex != SFG_TILE_TEXTURE_TRANSPARENT ?
2019-09-26 22:12:12 -04:00
(SFG_getTexel(
2019-09-28 15:12:16 -04:00
textureIndex != 255 ?
SFG_currentLevel.textures[textureIndex]:
2019-10-23 14:06:07 -04:00
SFG_wallTextures[SFG_currentLevel.levelPointer->doorTextureIndex],
2019-09-26 22:12:12 -04:00
pixel->texCoords.x / 32,
2019-09-26 22:41:45 -04:00
textureV / 32)
2019-09-26 22:12:12 -04:00
) :
2019-09-25 19:34:57 -04:00
SFG_TRANSPARENT_COLOR;
2019-09-25 09:51:19 -04:00
shadow = pixel->hit.direction >> 1;
}
else
{
2019-09-29 14:26:53 -04:00
color = pixel->isFloor ?
2019-09-30 13:39:21 -04:00
(SFG_currentLevel.floorColor) :
(pixel->height < SFG_CEILING_MAX_HEIGHT ?
SFG_currentLevel.ceilingColor : SFG_TRANSPARENT_COLOR);
2019-09-25 09:51:19 -04:00
}
2019-09-25 19:34:57 -04:00
if (color != SFG_TRANSPARENT_COLOR)
{
2019-09-26 19:25:22 -04:00
#if SFG_DITHERED_SHADOW
2019-10-03 17:37:52 -04:00
uint8_t fogShadow = (pixel->depth * 4) / (RCL_UNITS_PER_SQUARE);
2019-10-11 09:01:36 -04:00
2019-10-03 17:37:52 -04:00
uint8_t fogShadowPart = fogShadow & 0x07;
2019-09-26 19:25:22 -04:00
2019-10-03 17:37:52 -04:00
fogShadow /= 8;
2019-09-26 19:25:22 -04:00
2019-10-03 17:37:52 -04:00
uint8_t xMod4 = pixel->position.x & 0x03;
uint8_t yMod2 = pixel->position.y & 0x01;
2019-09-26 19:25:22 -04:00
2019-10-03 17:37:52 -04:00
shadow +=
fogShadow + SFG_ditheringPatterns[fogShadowPart * 8 + yMod2 * 4 + xMod4];
2019-09-26 19:25:22 -04:00
#else
2019-09-25 19:34:57 -04:00
shadow += pixel->depth / (RCL_UNITS_PER_SQUARE * 2);
2019-09-26 19:25:22 -04:00
#endif
2019-10-04 16:40:41 -04:00
#if SFG_ENABLE_FOG
2019-09-25 19:34:57 -04:00
color = palette_minusValue(color,shadow);
2019-10-04 16:40:41 -04:00
#endif
2019-09-25 19:34:57 -04:00
}
else
{
2019-10-23 14:06:07 -04:00
color = SFG_getTexel(SFG_backgroundImages[0],
2019-10-04 10:32:24 -04:00
SFG_backgroundScaleMap[(pixel->position.x * SFG_RAYCASTING_SUBSAMPLE + SFG_backgroundScroll) % SFG_GAME_RESOLUTION_Y],
2019-09-28 15:42:02 -04:00
// ^ TODO: get rid of mod?
2019-09-25 19:34:57 -04:00
SFG_backgroundScaleMap[pixel->position.y]);
}
2019-09-25 09:51:19 -04:00
2019-09-28 15:42:02 -04:00
RCL_Unit screenX = pixel->position.x * SFG_RAYCASTING_SUBSAMPLE;
for (uint8_t i = 0; i < SFG_RAYCASTING_SUBSAMPLE; ++i)
{
2019-10-04 10:32:24 -04:00
SFG_setGamePixel(screenX,pixel->position.y,color);
2019-09-28 15:42:02 -04:00
screenX++;
}
2019-09-25 09:51:19 -04:00
}
2019-10-13 19:47:41 -04:00
/**
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 u,v;
v = v0;
for (uint16_t y = y0; y < y1; y += scale)
{
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++;
}
}
2019-10-06 19:18:42 -04:00
void SFG_drawScaledSprite(
2019-10-01 19:31:59 -04:00
const uint8_t *image,
int16_t centerX,
int16_t centerY,
2019-10-06 11:12:12 -04:00
int16_t size,
2019-10-06 19:18:42 -04:00
uint8_t minusValue,
RCL_Unit distance)
2019-10-01 19:31:59 -04:00
{
if ((size > SFG_MAX_SPRITE_SIZE) || (size == 0))
return;
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;
2019-10-04 10:32:24 -04:00
if (x1 >= SFG_GAME_RESOLUTION_X)
x1 = SFG_GAME_RESOLUTION_X - 1;
2019-10-01 19:31:59 -04:00
int16_t y0, v0;
if (topLeftY < 0)
{
v0 = -1 * topLeftY;
y0 = 0;
}
else
{
v0 = 0;
y0 = topLeftY;
}
int16_t y1 = topLeftY + size - 1;
2019-10-04 10:32:24 -04:00
if (y1 >= SFG_GAME_RESOLUTION_Y)
y1 = SFG_GAME_RESOLUTION_Y - 1;
2019-10-01 19:31:59 -04:00
2019-10-06 16:04:08 -04:00
if ((x0 > x1) || (y0 > y1) || (u0 >= size) || (v0 >= size))
return; // outside screen?
2019-10-01 19:31:59 -04:00
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);
2019-10-06 16:04:08 -04:00
precompFrom = RCL_max(0,precompFrom);
precompTo = RCL_min(SFG_MAX_SPRITE_SIZE - 1,precompTo);
2019-10-01 19:31:59 -04:00
#define PRECOMP_SCALE 2048
2019-10-17 15:39:13 -04:00
int16_t precompStepScaled = ((SFG_TEXTURE_SIZE - 1) * PRECOMP_SCALE) / size;
2019-10-01 19:31:59 -04:00
int16_t precompPosScaled = precompFrom * precompStepScaled;
for (int16_t i = precompFrom; i <= precompTo; ++i)
{
SFG_spriteSamplingPoints[i] = precompPosScaled / PRECOMP_SCALE;
precompPosScaled += precompStepScaled;
}
#undef PRECOMP_SCALE
2019-10-22 17:59:10 -04:00
uint8_t zDistance = SFG_RCLUnitToZBuffer(distance);
2019-10-07 16:19:45 -04:00
2019-10-01 19:31:59 -04:00
for (int16_t x = x0, u = u0; x <= x1; ++x, ++u)
2019-10-06 19:18:42 -04:00
{
2019-10-22 15:21:56 -04:00
if (SFG_zBuffer[x] >= zDistance)
2019-10-06 11:05:26 -04:00
{
2019-10-06 19:18:42 -04:00
int8_t columnTransparent = 1;
2019-10-06 11:05:26 -04:00
2019-10-06 19:18:42 -04:00
for (int16_t y = y0, v = v0; y <= y1; ++y, ++v)
2019-10-06 11:12:12 -04:00
{
2019-10-06 19:18:42 -04:00
uint8_t color =
SFG_getTexel(image,SFG_spriteSamplingPoints[u],
SFG_spriteSamplingPoints[v]);
if (color != SFG_TRANSPARENT_COLOR)
{
2019-10-07 16:19:45 -04:00
#if SFG_DIMINISH_SPRITES
2019-10-06 19:18:42 -04:00
color = palette_minusValue(color,minusValue);
2019-10-22 17:59:10 -04:00
#endif
2019-10-06 19:18:42 -04:00
columnTransparent = 0;
2019-10-06 11:12:12 -04:00
2019-10-06 19:18:42 -04:00
SFG_setGamePixel(x,y,color);
}
2019-10-06 11:12:12 -04:00
}
2019-10-06 19:18:42 -04:00
if (!columnTransparent)
2019-10-22 15:21:56 -04:00
SFG_zBuffer[x] = zDistance;
2019-10-06 11:05:26 -04:00
}
2019-10-06 19:18:42 -04:00
}
2019-10-01 19:31:59 -04:00
}
2019-09-26 15:15:07 -04:00
RCL_Unit SFG_texturesAt(int16_t x, int16_t y)
2019-09-25 09:51:19 -04:00
{
2019-09-26 22:41:45 -04:00
uint8_t p;
2019-09-26 21:34:49 -04:00
2019-09-30 11:15:15 -04:00
SFG_TileDefinition tile = SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&p);
2019-09-26 22:41:45 -04:00
return
SFG_TILE_FLOOR_TEXTURE(tile) | (SFG_TILE_CEILING_TEXTURE(tile) << 3) | p;
// ^ store both textures (floor and ceiling) and properties in one number
2019-09-25 09:51:19 -04:00
}
2019-09-26 21:34:49 -04:00
RCL_Unit SFG_movingWallHeight
(
RCL_Unit low,
RCL_Unit high,
uint32_t time
)
{
RCL_Unit height = 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_sinInt(sinArg) * halfHeight) / RCL_UNITS_PER_SQUARE;
}
2019-09-25 09:51:19 -04:00
RCL_Unit SFG_floorHeightAt(int16_t x, int16_t y)
{
2019-09-26 21:34:49 -04:00
uint8_t properties;
2019-10-04 15:09:10 -04:00
SFG_TileDefinition tile =
SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&properties);
2019-09-25 09:51:19 -04:00
2019-10-04 15:09:10 -04:00
uint8_t doorHeight = 0;
if (properties == SFG_TILE_PROPERTY_DOOR)
{
for (uint8_t i = 0; i < SFG_currentLevel.doorRecordCount; ++i)
{
2019-10-06 10:16:59 -04:00
SFG_DoorRecord *door = &(SFG_currentLevel.doorRecords[i]);
2019-10-04 15:09:10 -04:00
if ((door->coords[0] == x) && (door->coords[1] == y))
{
doorHeight = door->state & SFG_DOOR_VERTICAL_POSITION_MASK;
break;
}
}
}
else if (properties == SFG_TILE_PROPERTY_ELEVATOR)
{
return SFG_movingWallHeight(
2019-09-26 21:34:49 -04:00
SFG_TILE_FLOOR_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP,
SFG_TILE_CEILING_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP,
2019-10-06 20:07:10 -04:00
SFG_frameTime - SFG_currentLevel.timeStart);
2019-10-04 15:09:10 -04:00
}
return SFG_TILE_FLOOR_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP -
doorHeight * SFG_DOOR_HEIGHT_STEP;
2019-09-25 09:51:19 -04:00
}
RCL_Unit SFG_ceilingHeightAt(int16_t x, int16_t y)
{
2019-09-26 21:34:49 -04:00
uint8_t properties;
2019-10-08 07:46:12 -04:00
SFG_TileDefinition tile =
SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&properties);
2019-09-26 21:34:49 -04:00
if (properties == SFG_TILE_PROPERTY_ELEVATOR)
return SFG_CEILING_MAX_HEIGHT;
2019-09-25 20:40:35 -04:00
uint8_t height = SFG_TILE_CEILING_HEIGHT(tile);
2019-09-27 09:24:36 -04:00
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,
2019-09-30 15:35:49 -04:00
(SFG_TILE_CEILING_HEIGHT(tile) + SFG_TILE_FLOOR_HEIGHT(tile))
* SFG_WALL_HEIGHT_STEP,
2019-10-06 20:07:10 -04:00
SFG_frameTime - SFG_currentLevel.timeStart);
2019-09-25 09:51:19 -04:00
}
2019-10-02 08:42:30 -04:00
void SFG_setAndInitLevel(const SFG_Level *level)
2019-09-26 21:34:49 -04:00
{
2019-09-29 07:50:40 -04:00
SFG_LOG("setting and initializing level");
2019-10-18 10:34:51 -04:00
SFG_currentRandom = 0;
2019-09-26 21:34:49 -04:00
SFG_currentLevel.levelPointer = level;
2019-09-29 14:26:53 -04:00
SFG_currentLevel.floorColor = level->floorColor;
SFG_currentLevel.ceilingColor = level->ceilingColor;
2019-09-26 21:34:49 -04:00
for (uint8_t i = 0; i < 7; ++i)
SFG_currentLevel.textures[i] =
2019-10-23 14:06:07 -04:00
SFG_wallTextures[level->textureIndices[i]];
2019-09-26 21:34:49 -04:00
2019-10-04 11:18:46 -04:00
SFG_LOG("initializing doors");
2019-10-06 10:47:47 -04:00
SFG_currentLevel.checkedDoorIndex = 0;
2019-10-04 11:18:46 -04:00
SFG_currentLevel.doorRecordCount = 0;
2019-10-21 08:54:32 -04:00
SFG_currentLevel.projectileRecordCount = 0;
2019-10-04 11:18:46 -04:00
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 =
2019-10-06 10:16:59 -04:00
&(SFG_currentLevel.doorRecords[SFG_currentLevel.doorRecordCount]);
2019-10-04 11:18:46 -04:00
d->coords[0] = i;
d->coords[1] = j;
2019-10-04 11:42:54 -04:00
d->state = SFG_DOOR_DEFAULT_STATE;
2019-10-04 11:18:46 -04:00
SFG_currentLevel.doorRecordCount++;
}
if (SFG_currentLevel.doorRecordCount >= SFG_MAX_DOORS)
break;
}
if (SFG_currentLevel.doorRecordCount >= SFG_MAX_DOORS)
break;
}
2019-10-06 10:16:59 -04:00
SFG_LOG("initializing level elements");
2019-10-17 15:19:32 -04:00
SFG_currentLevel.itemRecordCount = 0;
2019-10-06 10:47:47 -04:00
SFG_currentLevel.checkedItemIndex = 0;
2019-10-17 15:19:32 -04:00
SFG_currentLevel.monsterRecordCount = 0;
SFG_currentLevel.checkedMonsterIndex = 0;
SFG_MonsterRecord *monster;
2019-10-06 10:16:59 -04:00
for (uint8_t i = 0; i < SFG_MAX_LEVEL_ELEMENTS; ++i)
{
2019-10-06 14:17:09 -04:00
const SFG_LevelElement *e = &(SFG_currentLevel.levelPointer->elements[i]);
2019-10-06 10:16:59 -04:00
2019-12-27 11:17:14 -05:00
if (e->type != SFG_LEVEL_ELEMENT_NONE)
2019-10-06 10:16:59 -04:00
{
2019-12-27 11:17:14 -05:00
if (SFG_LEVEL_ELEMENT_TYPE_IS_MOSTER(e->type))
{
2019-10-24 15:43:13 -04:00
SFG_LOG("adding monster");
2019-10-17 15:19:32 -04:00
monster =
&(SFG_currentLevel.monsterRecords[SFG_currentLevel.monsterRecordCount]);
2019-10-25 18:50:22 -04:00
monster->stateType = e->type | 0;
2019-10-17 15:19:32 -04:00
monster->health = 255;
monster->coords[0] = e->coords[0] * 4;
monster->coords[1] = e->coords[1] * 4;
SFG_currentLevel.monsterRecordCount++;
2019-12-27 11:17:14 -05:00
}
else
{
SFG_LOG("adding item");
SFG_currentLevel.itemRecords[SFG_currentLevel.itemRecordCount] = i;
SFG_currentLevel.itemRecordCount++;
}
2019-10-06 10:16:59 -04:00
}
}
2019-09-27 10:38:55 -04:00
SFG_currentLevel.timeStart = SFG_getTimeMs();
2019-10-04 11:42:54 -04:00
SFG_currentLevel.frameStart = SFG_gameFrame;
2019-10-22 10:48:27 -04:00
SFG_spriteAnimationFrame = 0;
2019-09-26 21:34:49 -04:00
2019-10-01 14:25:21 -04:00
SFG_initPlayer();
2019-09-26 21:34:49 -04:00
}
2019-09-25 09:51:19 -04:00
2019-09-27 10:38:55 -04:00
void SFG_init()
{
2019-09-29 07:50:40 -04:00
SFG_LOG("initializing game")
2019-10-03 13:31:25 -04:00
SFG_gameFrame = 0;
2019-09-27 10:38:55 -04:00
2019-10-18 10:34:51 -04:00
SFG_currentRandom = 0;
2019-09-27 10:38:55 -04:00
RCL_initRayConstraints(&SFG_rayConstraints);
SFG_rayConstraints.maxHits = SFG_RAYCASTING_MAX_HITS;
SFG_rayConstraints.maxSteps = SFG_RAYCASTING_MAX_STEPS;
2019-10-04 10:32:24 -04:00
for (uint16_t i = 0; i < SFG_GAME_RESOLUTION_Y; ++i)
SFG_backgroundScaleMap[i] =
(i * SFG_TEXTURE_SIZE) / SFG_GAME_RESOLUTION_Y;
2019-09-27 10:38:55 -04:00
2019-10-14 09:56:26 -04:00
for (uint8_t i = 0; i < SFG_KEY_DOWN; ++i)
SFG_keyStates[i] = 0;
2019-09-27 10:38:55 -04:00
SFG_backgroundScroll = 0;
2019-10-02 08:42:30 -04:00
SFG_setAndInitLevel(&SFG_level0);
2019-10-06 10:16:59 -04:00
SFG_lastFrameTimeMs = SFG_getTimeMs();
2019-09-27 10:38:55 -04:00
}
2019-10-14 07:31:13 -04:00
void SFG_playerRotateWeapon(uint8_t next)
{
2019-10-23 20:03:57 -04:00
SFG_player.weapon += next ? 1 : -1;
SFG_player.weapon %= SFG_WEAPONS_TOTAL;
2019-10-14 07:31:13 -04:00
}
2019-10-22 10:58:21 -04:00
/**
Adds new projectile to the current level, return 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;
}
2019-10-22 11:29:21 -04:00
/**
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
)
{
SFG_ProjectileRecord p;
p.type = type;
p.doubleFramesToLive = 255;
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] =
2019-11-09 15:45:32 -05:00
(direction.x * SFG_FIREBALL_MOVE_UNITS_PER_FRAME) / RCL_UNITS_PER_SQUARE;
2019-10-22 11:29:21 -04:00
p.direction[1] =
2019-11-09 15:45:32 -05:00
(direction.y * SFG_FIREBALL_MOVE_UNITS_PER_FRAME) / RCL_UNITS_PER_SQUARE;
2019-10-22 11:29:21 -04:00
p.direction[2] = verticalSpeed;
return SFG_createProjectile(p);
}
2019-10-18 07:48:24 -04:00
void SFG_monsterPerformAI(SFG_MonsterRecord *monster)
2019-10-17 17:58:19 -04:00
{
2019-12-26 09:28:23 -05:00
uint8_t state = SFG_MR_STATE(*monster);
uint8_t type = SFG_MR_TYPE(*monster);
2019-10-18 09:25:07 -04:00
int8_t coordAdd[2];
coordAdd[0] = 0;
coordAdd[1] = 0;
2019-12-27 18:20:00 -05:00
uint8_t melee = (type == SFG_LEVEL_ELEMENT_MONSTER_WARRIOR) ||
(type == SFG_LEVEL_ELEMENT_MONSTER_EXPLODER);
2019-10-24 17:51:24 -04:00
2019-12-27 18:20:00 -05:00
if ( // sometimes randomly change state
(SFG_random() < SFG_AI_RANDOM_CHANGE_PROBABILITY) &&
(type != SFG_LEVEL_ELEMENT_MONSTER_EXPLODER))
2019-10-18 11:26:13 -04:00
{
2019-10-24 17:51:24 -04:00
if (!melee && (SFG_random() % 4 != 0))
2019-10-22 10:58:21 -04:00
{
// attack
state = SFG_MONSTER_STATE_ATTACKING;
2019-10-24 17:51:24 -04:00
if (type != SFG_LEVEL_ELEMENT_MONSTER_WARRIOR)
{
RCL_Vector2D pos;
RCL_Vector2D dir;
pos.x = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]);
pos.y = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]);
dir.x = SFG_player.camera.position.x - pos.x;
dir.y = SFG_player.camera.position.y - pos.y;
dir = RCL_normalize(dir);
SFG_launchProjectile(
2019-11-09 15:45:32 -05:00
type != SFG_LEVEL_ELEMENT_MONSTER_PLASMABOT ?
SFG_PROJECTILE_FIREBALL : SFG_PROJECTILE_PLASMA,
2019-10-24 17:51:24 -04:00
pos,
SFG_floorHeightAt(
SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]),
SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1])
) + RCL_UNITS_PER_SQUARE / 2,
dir,
0,
SFG_ELEMENT_COLLISION_DISTANCE
);
}
2019-10-22 10:58:21 -04:00
}
else
state = SFG_MONSTER_STATE_IDLE;
2019-10-18 11:26:13 -04:00
}
2019-10-22 10:48:27 -04:00
else if (state == SFG_MONSTER_STATE_IDLE)
2019-10-18 09:25:07 -04:00
{
2019-10-24 17:51:24 -04:00
if (melee)
2019-10-18 10:48:19 -04:00
{
2019-10-24 17:51:24 -04:00
// melee monsters walk towards player
uint8_t mX = SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]);
uint8_t mY = SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1]);
if (mX > SFG_player.squarePosition[0])
{
if (mY > SFG_player.squarePosition[1])
state = SFG_MONSTER_STATE_GOING_NW;
else if (mY < SFG_player.squarePosition[1])
state = SFG_MONSTER_STATE_GOING_SW;
else
state = SFG_MONSTER_STATE_GOING_W;
}
else if (mX < SFG_player.squarePosition[0])
{
if (mY > SFG_player.squarePosition[1])
state = SFG_MONSTER_STATE_GOING_NE;
else if (mY < SFG_player.squarePosition[1])
state = SFG_MONSTER_STATE_GOING_SE;
else
state = SFG_MONSTER_STATE_GOING_E;
}
else
{
if (mY > SFG_player.squarePosition[1])
state = SFG_MONSTER_STATE_GOING_N;
else if (mY < SFG_player.squarePosition[1])
state = SFG_MONSTER_STATE_GOING_S;
}
}
else
{
// ranged monsters choose 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;
}
2019-10-18 10:48:19 -04:00
}
2019-10-18 09:25:07 -04:00
}
2019-10-22 10:48:27 -04:00
else if (state == SFG_MONSTER_STATE_ATTACKING)
{
state = SFG_MONSTER_STATE_IDLE;
}
2019-10-18 09:25:07 -04:00
else
{
2019-12-27 18:20:00 -05:00
int8_t add = type != SFG_LEVEL_ELEMENT_MONSTER_EXPLODER ? 1 : 3;
2019-10-18 10:56:38 -04:00
if (state == SFG_MONSTER_STATE_GOING_E ||
state == SFG_MONSTER_STATE_GOING_NE ||
state == SFG_MONSTER_STATE_GOING_SE)
2019-12-27 18:20:00 -05:00
coordAdd[0] = add;
2019-10-18 10:56:38 -04:00
else if (state == SFG_MONSTER_STATE_GOING_W ||
state == SFG_MONSTER_STATE_GOING_SW ||
state == SFG_MONSTER_STATE_GOING_NW)
2019-12-27 18:20:00 -05:00
coordAdd[0] = -1 * add;
2019-10-18 10:56:38 -04:00
if (state == SFG_MONSTER_STATE_GOING_N ||
state == SFG_MONSTER_STATE_GOING_NE ||
state == SFG_MONSTER_STATE_GOING_NW)
2019-12-27 18:20:00 -05:00
coordAdd[1] = -1 * add;
2019-10-18 10:56:38 -04:00
else if (state == SFG_MONSTER_STATE_GOING_S ||
state == SFG_MONSTER_STATE_GOING_SE ||
state == SFG_MONSTER_STATE_GOING_SW)
2019-12-27 18:20:00 -05:00
coordAdd[1] = add;
if (add)
state = SFG_MONSTER_STATE_IDLE;
2019-10-18 09:25:07 -04:00
}
2019-10-18 10:48:19 -04:00
int16_t newPos[2];
2019-10-18 10:34:51 -04:00
2019-10-18 10:48:19 -04:00
newPos[0] = monster->coords[0] + coordAdd[0];
newPos[1] = monster->coords[1] + coordAdd[1];
2019-10-18 09:25:07 -04:00
2019-10-18 10:48:19 -04:00
int8_t collision = 0;
if (newPos[0] < 0 || newPos[0] >= 256 || newPos[1] < 0 || newPos[1] >= 256)
{
collision = 1;
}
else
{
RCL_Unit currentHeight =
SFG_floorHeightAt(monster->coords[0] / 4,monster->coords[1] / 4);
2019-10-18 09:25:07 -04:00
2019-10-18 10:48:19 -04:00
RCL_Unit newHeight =
SFG_floorHeightAt(newPos[0] / 4,newPos[1] / 4);
2019-10-18 09:25:07 -04:00
2019-10-18 10:48:19 -04:00
collision =
RCL_absVal(currentHeight - newHeight) > RCL_CAMERA_COLL_STEP_HEIGHT;
}
2019-10-18 10:34:51 -04:00
2019-10-18 10:48:19 -04:00
if (collision)
{
state = SFG_MONSTER_STATE_IDLE;
// ^ will force the monster to choose random direction in next update
newPos[0] = monster->coords[0];
newPos[1] = monster->coords[1];
}
2019-10-18 10:34:51 -04:00
2019-10-18 10:48:19 -04:00
monster->stateType = state | type;
monster->coords[0] = newPos[0];
monster->coords[1] = newPos[1];;
2019-10-17 17:58:19 -04:00
}
2019-10-21 14:49:28 -04:00
/**
Pushes a given position away from a center by given distance, with collisions.
2019-10-25 16:41:52 -04:00
Returns 1 if push away happened, otherwise 0.
2019-10-21 14:49:28 -04:00
*/
2019-10-25 16:41:52 -04:00
uint8_t SFG_pushAway(
2019-10-21 14:49:28 -04:00
RCL_Unit pos[3],
2019-10-21 19:34:40 -04:00
RCL_Unit centerX,
RCL_Unit centerY,
2019-10-21 14:49:28 -04:00
RCL_Unit preferredDirection,
RCL_Unit distance)
{
RCL_Vector2D fromCenter;
2019-10-21 19:34:40 -04:00
fromCenter.x = pos[0] - centerX;
fromCenter.y = pos[1] - centerY;
2019-10-21 14:49:28 -04:00
RCL_Unit l = RCL_len(fromCenter);
2019-10-21 17:09:08 -04:00
if (l < 128)
{
fromCenter = RCL_angleToDirection(preferredDirection);
l = RCL_UNITS_PER_SQUARE;
}
2019-10-21 17:36:30 -04:00
else if (l >= distance)
{
2019-10-25 16:41:52 -04:00
return 0;
2019-10-21 17:36:30 -04:00
}
2019-10-21 14:49:28 -04:00
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_floorHeightAt,
SFG_ceilingHeightAt,1,1);
pos[0] = c.position.x;
pos[1] = c.position.y;
pos[2] = c.height;
2019-10-25 16:41:52 -04:00
return 1;
2019-10-21 14:49:28 -04:00
}
2019-10-25 16:41:52 -04:00
uint8_t SFG_pushPlayerAway(RCL_Unit centerX, RCL_Unit centerY, RCL_Unit distance)
2019-10-21 19:34:40 -04:00
{
RCL_Unit p[3];
p[0] = SFG_player.camera.position.x;
p[1] = SFG_player.camera.position.y;
p[2] = SFG_player.camera.height;
2019-10-25 16:41:52 -04:00
uint8_t result = SFG_pushAway(p,centerX,centerY,
2019-10-21 19:34:40 -04:00
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];
2019-10-25 16:41:52 -04:00
return result;
2019-10-21 19:34:40 -04:00
}
2019-10-22 06:13:10 -04:00
/**
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_absVal(elementPos.x - position.x);
RCL_Unit dy = RCL_absVal(elementPos.y - position.y);
if (dx > dy)
{
// colliding from left/right
if ((moveOffset.x > 0) == (position.x < elementPos.x))
moveOffset.x = 0;
2019-10-22 06:14:45 -04:00
// ^ only stop if heading towards element, to avoid getting stuck
2019-10-22 06:13:10 -04:00
}
else
{
// colliding from up/down
if ((moveOffset.y > 0) == (position.y < elementPos.y))
2019-10-22 06:14:45 -04:00
moveOffset.y = 0;
2019-10-22 06:13:10 -04:00
}
return moveOffset;
}
2019-10-24 17:20:35 -04:00
/**
Adds or substracts player's health, which either hurts him (negative value)
or heals him (positive value).
*/
void SFG_playerChangeHealth(int8_t healthAdd)
{
int16_t health = SFG_player.health;
health += healthAdd;
health = RCL_clamp(health,0,SFG_PLAYER_MAX_HEALTH);
SFG_player.health = health;
2019-10-25 16:41:52 -04:00
if (healthAdd < 0)
SFG_player.lastHurtFrame = SFG_gameFrame;
2019-10-24 17:20:35 -04:00
}
2019-10-21 13:58:11 -04:00
void SFG_createExplosion(RCL_Unit x, RCL_Unit y, RCL_Unit z)
{
SFG_ProjectileRecord explostion;
explostion.type = SFG_PROJECTILE_EXPLOSION;
explostion.position[0] = x;
explostion.position[1] = y;
explostion.position[2] = z;
explostion.direction[0] = 0;
explostion.direction[1] = 0;
explostion.direction[2] = 0;
explostion.doubleFramesToLive = SFG_EXPLOSION_DURATION_DOUBLE_FRAMES;
SFG_createProjectile(explostion);
2019-10-21 14:49:28 -04:00
2019-10-25 16:41:52 -04:00
if (SFG_pushPlayerAway(x,y,SFG_EXPLOSION_DISTANCE))
SFG_playerChangeHealth(-1 * SFG_EXPLOSION_DAMAGE);
2019-10-21 13:58:11 -04:00
}
2019-10-21 20:03:44 -04:00
/**
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]);
}
2019-10-22 08:27:56 -04:00
static inline uint8_t SFG_elementCollides(
RCL_Unit pointX,
RCL_Unit pointY,
RCL_Unit pointZ,
RCL_Unit elementX,
RCL_Unit elementY,
RCL_Unit elementHeight,
RCL_Unit heightMargin,
RCL_Unit widthMargin
)
{
return
(
SFG_taxicabDistance(pointX,pointY,elementX,elementY)
<= (SFG_ELEMENT_COLLISION_DISTANCE + widthMargin)
)
&&
(
(pointZ - heightMargin - elementHeight)
<= SFG_ELEMENT_COLLISION_HEIGHT
);
}
2019-09-25 09:51:19 -04:00
/**
Performs one game step (logic, physics), happening SFG_MS_PER_FRAME after
previous frame.
*/
void SFG_gameStep()
{
2019-10-14 09:56:26 -04:00
for (uint8_t i = 0; i < SFG_KEY_COUNT; ++i)
SFG_keyStates[i] = (SFG_keyStates[i] << 1) | SFG_keyPressed(i);
2019-10-22 10:48:27 -04:00
if ((SFG_currentLevel.frameStart - SFG_gameFrame) %
SFG_SPRITE_ANIMATION_FRAME_DURATION == 0)
SFG_spriteAnimationFrame++;
2019-09-25 09:51:19 -04:00
int8_t recomputeDirection = 0;
2019-10-01 14:25:21 -04:00
RCL_Vector2D moveOffset;
moveOffset.x = 0;
moveOffset.y = 0;
2019-10-02 08:42:30 -04:00
int8_t strafe = 0;
2019-10-11 09:01:36 -04:00
#if SFG_HEADBOB_ENABLED
int8_t bobbing = 0;
#endif
int8_t shearing = 0;
2019-10-23 18:32:04 -04:00
if (
SFG_keyIsDown(SFG_KEY_B) &&
(SFG_gameFrame - SFG_player.lastShotFrame >
SFG_WEAPON_SHOTGUN_COOLDOWN_FRAMES))
2019-10-21 08:54:32 -04:00
{
// fire
2019-10-23 18:32:04 -04:00
2019-11-09 15:45:32 -05:00
if (SFG_player.weapon == SFG_WEAPON_ROCKET_LAUNCHER ||
2019-11-09 05:59:52 -05:00
SFG_player.weapon == SFG_WEAPON_PLASMAGUN)
2019-10-23 18:32:04 -04:00
SFG_launchProjectile(
2019-11-09 15:45:32 -05:00
SFG_player.weapon == SFG_WEAPON_ROCKET_LAUNCHER ?
2019-11-09 05:59:52 -05:00
SFG_PROJECTILE_FIREBALL : SFG_PROJECTILE_PLASMA,
2019-10-23 18:32:04 -04:00
SFG_player.camera.position,
SFG_player.camera.height,
RCL_angleToDirection(SFG_player.camera.direction),
2019-11-09 15:45:32 -05:00
(SFG_player.camera.shear * SFG_FIREBALL_MOVE_UNITS_PER_FRAME) /
2019-10-23 18:32:04 -04:00
SFG_CAMERA_MAX_SHEAR_PIXELS,
SFG_ELEMENT_COLLISION_DISTANCE + RCL_CAMERA_COLL_RADIUS
);
2019-10-22 15:02:39 -04:00
SFG_player.lastShotFrame = SFG_gameFrame;
2019-10-21 08:54:32 -04:00
}
2019-10-14 09:56:26 -04:00
if (SFG_keyIsDown(SFG_KEY_A))
2019-09-25 09:51:19 -04:00
{
2019-10-14 09:56:26 -04:00
if (SFG_keyIsDown(SFG_KEY_UP))
2019-10-11 09:01:36 -04:00
{
SFG_player.camera.shear =
RCL_min(SFG_CAMERA_MAX_SHEAR_PIXELS,
SFG_player.camera.shear + SFG_CAMERA_SHEAR_STEP_PER_FRAME);
shearing = 1;
}
2019-10-14 09:56:26 -04:00
else if (SFG_keyIsDown(SFG_KEY_DOWN))
2019-10-11 09:01:36 -04:00
{
SFG_player.camera.shear =
RCL_max(-1 * SFG_CAMERA_MAX_SHEAR_PIXELS,
SFG_player.camera.shear - SFG_CAMERA_SHEAR_STEP_PER_FRAME);
shearing = 1;
}
2019-10-14 09:56:26 -04:00
if (!SFG_keyIsDown(SFG_KEY_C))
2019-10-14 07:31:13 -04:00
{
2019-10-14 09:56:26 -04:00
if (SFG_keyIsDown(SFG_KEY_LEFT))
2019-10-14 07:31:13 -04:00
strafe = -1;
2019-10-14 09:56:26 -04:00
else if (SFG_keyIsDown(SFG_KEY_RIGHT))
2019-10-14 07:31:13 -04:00
strafe = 1;
}
else
{
2019-10-18 11:51:12 -04:00
if (SFG_keyJustPressed(SFG_KEY_LEFT) | SFG_keyJustPressed(SFG_KEY_A))
2019-10-14 07:31:13 -04:00
SFG_playerRotateWeapon(0);
2019-10-18 11:51:12 -04:00
else if (SFG_keyJustPressed(SFG_KEY_RIGHT) | SFG_keyJustPressed(SFG_KEY_B))
2019-10-14 07:31:13 -04:00
SFG_playerRotateWeapon(1);
}
2019-09-25 09:51:19 -04:00
}
2019-10-01 14:25:21 -04:00
else
2019-09-25 09:51:19 -04:00
{
2019-10-14 09:56:26 -04:00
if (!SFG_keyIsDown(SFG_KEY_C))
2019-10-01 14:25:21 -04:00
{
2019-10-14 09:56:26 -04:00
if (SFG_keyIsDown(SFG_KEY_LEFT))
2019-10-14 07:31:13 -04:00
{
SFG_player.camera.direction -= SFG_PLAYER_TURN_UNITS_PER_FRAME;
recomputeDirection = 1;
}
2019-10-14 09:56:26 -04:00
else if (SFG_keyIsDown(SFG_KEY_RIGHT))
2019-10-14 07:31:13 -04:00
{
SFG_player.camera.direction += SFG_PLAYER_TURN_UNITS_PER_FRAME;
recomputeDirection = 1;
}
2019-10-01 14:25:21 -04:00
}
2019-10-14 07:31:13 -04:00
else
2019-10-01 14:25:21 -04:00
{
2019-10-18 11:51:12 -04:00
if (SFG_keyJustPressed(SFG_KEY_LEFT) | SFG_keyJustPressed(SFG_KEY_A))
2019-10-14 07:31:13 -04:00
SFG_playerRotateWeapon(0);
2019-10-18 11:51:12 -04:00
else if (SFG_keyJustPressed(SFG_KEY_RIGHT) | SFG_keyJustPressed(SFG_KEY_B))
2019-10-14 07:31:13 -04:00
SFG_playerRotateWeapon(1);
2019-10-01 14:25:21 -04:00
}
if (recomputeDirection)
SFG_recompurePLayerDirection();
2019-10-11 09:01:36 -04:00
2019-10-14 09:56:26 -04:00
if (SFG_keyIsDown(SFG_KEY_UP))
2019-10-11 09:01:36 -04:00
{
moveOffset.x += SFG_player.direction.x;
moveOffset.y += SFG_player.direction.y;
#if SFG_HEADBOB_ENABLED
bobbing = 1;
#endif
}
2019-10-14 09:56:26 -04:00
else if (SFG_keyIsDown(SFG_KEY_DOWN))
2019-10-11 09:01:36 -04:00
{
moveOffset.x -= SFG_player.direction.x;
moveOffset.y -= SFG_player.direction.y;
#if SFG_HEADBOB_ENABLED
bobbing = 1;
#endif
}
2019-09-25 09:51:19 -04:00
}
2019-10-14 09:56:26 -04:00
if (SFG_keyIsDown(SFG_KEY_STRAFE_LEFT))
2019-10-02 08:42:30 -04:00
strafe = -1;
2019-10-14 09:56:26 -04:00
else if (SFG_keyIsDown(SFG_KEY_STRAFE_RIGHT))
2019-10-02 08:42:30 -04:00
strafe = 1;
if (strafe != 0)
{
2019-10-14 09:56:26 -04:00
moveOffset.x += strafe * SFG_player.direction.y;
moveOffset.y -= strafe * SFG_player.direction.x;
2019-10-02 08:42:30 -04:00
}
2019-10-02 06:02:52 -04:00
#if SFG_PREVIEW_MODE
2019-10-14 09:56:26 -04:00
if (SFG_keyIsDown(SFG_KEY_B))
2019-10-02 06:02:52 -04:00
SFG_player.verticalSpeed = SFG_PLAYER_MOVE_UNITS_PER_FRAME;
2019-10-14 09:56:26 -04:00
else if (SFG_keyIsDown(SFG_KEY_C))
2019-10-02 06:02:52 -04:00
SFG_player.verticalSpeed = -1 * SFG_PLAYER_MOVE_UNITS_PER_FRAME;
else
SFG_player.verticalSpeed = 0;
#else
2019-10-03 14:09:00 -04:00
RCL_Unit verticalOffset =
2019-10-03 14:24:34 -04:00
(
(
2019-10-14 09:56:26 -04:00
SFG_keyIsDown(SFG_KEY_JUMP) ||
(SFG_keyIsDown(SFG_KEY_UP) && SFG_keyIsDown(SFG_KEY_C))
2019-10-03 14:24:34 -04:00
) &&
(SFG_player.verticalSpeed == 0) &&
2019-10-03 14:09:00 -04:00
(SFG_player.previousVerticalSpeed == 0)) ?
SFG_PLAYER_JUMP_SPEED :
(SFG_player.verticalSpeed - SFG_GRAVITY_SPEED_INCREASE_PER_FRAME);
2019-10-02 06:02:52 -04:00
#endif
2019-09-25 09:51:19 -04:00
2019-10-11 09:01:36 -04:00
if (!shearing && SFG_player.camera.shear != 0)
2019-09-25 09:51:19 -04:00
{
2019-10-11 09:01:36 -04:00
// gradually shear back to zero
SFG_player.camera.shear =
(SFG_player.camera.shear > 0) ?
RCL_max(0,SFG_player.camera.shear - SFG_CAMERA_SHEAR_STEP_PER_FRAME) :
RCL_min(0,SFG_player.camera.shear + SFG_CAMERA_SHEAR_STEP_PER_FRAME);
2019-09-25 09:51:19 -04:00
}
2019-10-09 11:44:31 -04:00
#if SFG_HEADBOB_ENABLED
if (bobbing)
{
SFG_player.headBobFrame += SFG_HEADBOB_FRAME_INCREASE_PER_FRAME;
}
else if (SFG_player.headBobFrame != 0)
{
// smoothly stop bobbing
uint8_t quadrant = (SFG_player.headBobFrame % RCL_UNITS_PER_SQUARE) /
(RCL_UNITS_PER_SQUARE / 4);
/* when in quadrant in which sin is going away from zero, switch to the
same value of the next quadrant, so that bobbing starts to go towards
zero immediately */
if (quadrant % 2 == 0)
SFG_player.headBobFrame =
((quadrant + 1) * RCL_UNITS_PER_SQUARE / 4) +
(RCL_UNITS_PER_SQUARE / 4 - SFG_player.headBobFrame %
(RCL_UNITS_PER_SQUARE / 4));
RCL_Unit currentFrame = SFG_player.headBobFrame;
RCL_Unit nextFrame = SFG_player.headBobFrame + 16;
// only stop bobbing when we pass frame at which sin crosses ero
SFG_player.headBobFrame =
(currentFrame / (RCL_UNITS_PER_SQUARE / 2) ==
nextFrame / (RCL_UNITS_PER_SQUARE / 2)) ?
nextFrame : 0;
}
#endif
2019-10-01 14:25:21 -04:00
RCL_Unit previousHeight = SFG_player.camera.height;
2019-10-21 19:34:40 -04:00
// handle player collision with level elements:
2019-10-25 18:50:22 -04:00
// monsters:
2019-10-21 19:34:40 -04:00
for (uint8_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
{
SFG_MonsterRecord *m = &(SFG_currentLevel.monsterRecords[i]);
2019-12-26 09:28:23 -05:00
if (SFG_MR_STATE(*m) == SFG_MONSTER_STATE_INACTIVE)
2019-10-28 14:09:11 -04:00
continue;
2019-10-22 06:13:10 -04:00
RCL_Vector2D mPos;
mPos.x = SFG_MONSTER_COORD_TO_RCL_UNITS(m->coords[0]);
mPos.y = SFG_MONSTER_COORD_TO_RCL_UNITS(m->coords[1]);
2019-10-21 19:34:40 -04:00
if (
2019-10-22 08:27:56 -04:00
SFG_elementCollides(
SFG_player.camera.position.x,
SFG_player.camera.position.y,
SFG_player.camera.height,
mPos.x,
mPos.y,
SFG_floorHeightAt(
SFG_MONSTER_COORD_TO_SQUARES(m->coords[0]),
SFG_MONSTER_COORD_TO_SQUARES(m->coords[1])),
RCL_CAMERA_COLL_RADIUS / 2,
RCL_CAMERA_COLL_HEIGHT_BELOW
)
)
2019-10-21 19:34:40 -04:00
{
2019-10-22 06:13:10 -04:00
moveOffset = SFG_resolveCollisionWithElement(
SFG_player.camera.position,moveOffset,mPos);
2019-10-21 19:34:40 -04:00
}
}
2019-10-25 18:50:22 -04:00
// items:
for (int16_t i = 0; i < SFG_currentLevel.itemRecordCount; ++i)
// ^ has to be int16_t (signed)
2019-10-21 20:03:44 -04:00
{
2019-10-28 14:09:11 -04:00
if (!(SFG_currentLevel.itemRecords[i] & SFG_ITEM_RECORD_ACTIVE_MASK))
continue;
2019-10-21 20:03:44 -04:00
const SFG_LevelElement *e = SFG_getActiveItemElement(i);
if (e != 0)
{
2019-10-22 06:13:10 -04:00
RCL_Vector2D ePos;
ePos.x = SFG_ELEMENT_COORD_TO_RCL_UNITS(e->coords[0]);
ePos.y = SFG_ELEMENT_COORD_TO_RCL_UNITS(e->coords[1]);
2019-10-22 08:27:56 -04:00
if (
SFG_elementCollides(
SFG_player.camera.position.x,
SFG_player.camera.position.y,
SFG_player.camera.height,
ePos.x,
ePos.y,
SFG_floorHeightAt(e->coords[0],e->coords[1]),
RCL_CAMERA_COLL_RADIUS / 2,
RCL_CAMERA_COLL_HEIGHT_BELOW
)
)
2019-10-21 20:03:44 -04:00
{
2019-10-25 18:50:22 -04:00
if (e->type == SFG_LEVEL_ELEMENT_HEALTH)
{
SFG_playerChangeHealth(SFG_HEALTH_KIT_VALUE);
// take, eliminate the item
for (uint8_t j = i; j < SFG_currentLevel.itemRecordCount - 1; ++j)
SFG_currentLevel.itemRecords[j] =
SFG_currentLevel.itemRecords[j + 1];
SFG_currentLevel.itemRecordCount--;
i--;
}
else // collide
moveOffset = SFG_resolveCollisionWithElement(
SFG_player.camera.position,moveOffset,ePos);
2019-10-21 20:03:44 -04:00
}
2019-10-22 06:13:10 -04:00
}
2019-10-21 20:03:44 -04:00
}
2019-10-22 06:13:10 -04:00
#if SFG_PREVIEW_MODE
SFG_player.camera.position.x +=
SFG_PREVIEW_MODE_SPEED_MULTIPLIER * moveOffset.x;
SFG_player.camera.position.y +=
SFG_PREVIEW_MODE_SPEED_MULTIPLIER * moveOffset.y;
SFG_player.camera.height +=
SFG_PREVIEW_MODE_SPEED_MULTIPLIER * SFG_player.verticalSpeed;
#else
RCL_moveCameraWithCollision(&(SFG_player.camera),moveOffset,
verticalOffset,SFG_floorHeightAt,SFG_ceilingHeightAt,1,1);
SFG_player.previousVerticalSpeed = SFG_player.verticalSpeed;
RCL_Unit limit = RCL_max(RCL_max(0,verticalOffset),SFG_player.verticalSpeed);
SFG_player.verticalSpeed =
RCL_min(limit,SFG_player.camera.height - previousHeight);
/* ^ By "limit" we assure height increase caused by climbing a step doesn't
add vertical velocity. */
#endif
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;
2019-10-21 09:21:22 -04:00
// update projectiles:
2019-10-21 13:58:11 -04:00
uint8_t substractFrames =
(SFG_gameFrame - SFG_currentLevel.frameStart) & 0x01 ? 1 : 0;
// ^ only substract frames to live every other frame
2019-10-21 09:21:22 -04:00
for (int8_t i = 0; i < SFG_currentLevel.projectileRecordCount; ++i)
2019-10-25 17:17:59 -04:00
{ // ^ has to be signed
2019-10-21 09:21:22 -04:00
SFG_ProjectileRecord *p = &(SFG_currentLevel.projectileRecords[i]);
RCL_Unit pos[3]; // we have to convert from uint16_t because under/overflows
2019-10-21 17:36:30 -04:00
uint8_t eliminate = 0;
2019-10-21 09:21:22 -04:00
2019-10-25 17:17:59 -04:00
for (uint8_t j = 0; j < 3; ++j)
2019-10-21 09:21:22 -04:00
{
2019-10-25 17:17:59 -04:00
pos[j] = p->position[j];
pos[j] += p->direction[j];
2019-10-21 09:21:22 -04:00
if (
2019-10-25 17:17:59 -04:00
(pos[j] < 0) ||
(pos[j] >= (SFG_MAP_SIZE * RCL_UNITS_PER_SQUARE)))
2019-10-21 09:21:22 -04:00
{
2019-10-21 17:36:30 -04:00
eliminate = 1;
2019-10-21 09:21:22 -04:00
break;
}
}
2019-10-22 12:20:03 -04:00
if (
(p->doubleFramesToLive == 0) ||
SFG_elementCollides(
SFG_player.camera.position.x,
SFG_player.camera.position.y,
SFG_player.camera.height,
p->position[0],
p->position[1],
p->position[2],
0,
0
)
)
2019-10-21 17:50:19 -04:00
{
2019-10-21 17:36:30 -04:00
eliminate = 1;
2019-10-21 17:50:19 -04:00
}
else if (p->type != SFG_PROJECTILE_EXPLOSION)
{
2019-10-21 18:03:43 -04:00
// check collision with the map
2019-10-21 17:50:19 -04:00
if (SFG_floorHeightAt(pos[0] / RCL_UNITS_PER_SQUARE,pos[1] /
RCL_UNITS_PER_SQUARE) >= pos[2])
eliminate = 1;
2019-10-21 18:03:43 -04:00
// check collision with active level elements
2019-10-21 17:50:19 -04:00
2019-10-25 17:17:59 -04:00
for (uint8_t j = 0; j < SFG_currentLevel.monsterRecordCount; ++j)
2019-10-21 17:50:19 -04:00
{
2019-10-25 17:17:59 -04:00
SFG_MonsterRecord *m = &(SFG_currentLevel.monsterRecords[j]);
2019-12-26 09:28:23 -05:00
if (SFG_MR_TYPE(*m) != SFG_MONSTER_STATE_INACTIVE)
2019-10-21 17:50:19 -04:00
{
2019-10-22 08:27:56 -04:00
if (
SFG_elementCollides(
SFG_MONSTER_COORD_TO_RCL_UNITS(m->coords[0]),
SFG_MONSTER_COORD_TO_RCL_UNITS(m->coords[1]),
SFG_floorHeightAt(
SFG_MONSTER_COORD_TO_SQUARES(m->coords[0]),
SFG_MONSTER_COORD_TO_SQUARES(m->coords[1])),
2019-10-22 12:20:03 -04:00
p->position[0],
p->position[1],
p->position[2],
2019-10-22 08:27:56 -04:00
0,
0)
)
2019-10-21 17:50:19 -04:00
{
2019-10-22 11:29:21 -04:00
eliminate = 1;
2019-10-21 17:50:19 -04:00
break;
}
}
}
2019-10-21 18:03:43 -04:00
if (!eliminate)
2019-10-25 17:17:59 -04:00
for (uint8_t j = 0; j < SFG_currentLevel.itemRecordCount; ++j)
2019-10-21 18:03:43 -04:00
{
2019-10-25 17:17:59 -04:00
const SFG_LevelElement *e = SFG_getActiveItemElement(j);
2019-10-21 18:03:43 -04:00
2019-10-21 20:03:44 -04:00
if (e != 0)
2019-10-21 18:03:43 -04:00
{
2019-10-22 08:27:56 -04:00
if (
SFG_elementCollides(
p->position[0],
p->position[1],
p->position[2],
SFG_ELEMENT_COORD_TO_RCL_UNITS(e->coords[0]),
SFG_ELEMENT_COORD_TO_RCL_UNITS(e->coords[1]),
SFG_floorHeightAt(e->coords[0],e->coords[1]),
0,
0)
)
2019-10-21 18:03:43 -04:00
{
2019-10-25 18:50:22 -04:00
2019-10-21 18:03:43 -04:00
eliminate = 1;
break;
}
}
}
2019-10-21 17:50:19 -04:00
}
2019-10-21 13:58:11 -04:00
2019-10-21 17:36:30 -04:00
if (eliminate)
2019-10-21 09:21:22 -04:00
{
2019-10-21 14:07:28 -04:00
if (p->type == SFG_PROJECTILE_FIREBALL)
SFG_createExplosion(p->position[0],p->position[1],p->position[2]);
2019-10-21 09:21:22 -04:00
// remove the projectile
for (uint8_t j = i; j < SFG_currentLevel.projectileRecordCount - 1; ++j)
SFG_currentLevel.projectileRecords[j] =
SFG_currentLevel.projectileRecords[j + 1];
SFG_currentLevel.projectileRecordCount--;
i--;
}
else
{
p->position[0] = pos[0];
p->position[1] = pos[1];
p->position[2] = pos[2];
}
2019-10-21 13:58:11 -04:00
p->doubleFramesToLive -= substractFrames;
2019-10-21 09:21:22 -04:00
}
2019-10-04 11:42:54 -04:00
// handle door:
2019-10-06 10:47:47 -04:00
if (SFG_currentLevel.doorRecordCount > 0) // has to be here
{
/* Check one door on whether a player is standing nearby. For performance
2019-12-26 09:10:38 -05:00
reasons we only check a few doors and move to others in the next
frame. */
2019-10-06 10:47:47 -04:00
2019-12-26 09:10:38 -05:00
for (uint16_t i = 0;
i < RCL_min(SFG_ELEMENT_DISTANCES_CHECKED_PER_FRAME,
SFG_currentLevel.doorRecordCount);
++i)
{
SFG_DoorRecord *door =
&(SFG_currentLevel.doorRecords[SFG_currentLevel.checkedDoorIndex]);
2019-10-04 11:42:54 -04:00
2019-12-26 09:10:38 -05:00
door->state = (door->state & ~SFG_DOOR_UP_DOWN_MASK) |
(
((door->coords[0] >= (SFG_player.squarePosition[0] - 1)) &&
(door->coords[0] <= (SFG_player.squarePosition[0] + 1)) &&
(door->coords[1] >= (SFG_player.squarePosition[1] - 1)) &&
(door->coords[1] <= (SFG_player.squarePosition[1] + 1))) ?
SFG_DOOR_UP_DOWN_MASK : 0x00
);
2019-10-04 11:42:54 -04:00
2019-12-26 09:10:38 -05:00
SFG_currentLevel.checkedDoorIndex++;
2019-10-04 11:42:54 -04:00
2019-12-26 09:10:38 -05:00
if (SFG_currentLevel.checkedDoorIndex >= SFG_currentLevel.doorRecordCount)
SFG_currentLevel.checkedDoorIndex = 0;
}
2019-10-04 11:42:54 -04:00
2019-10-06 10:47:47 -04:00
for (uint32_t i = 0; i < SFG_currentLevel.doorRecordCount; ++i)
{
SFG_DoorRecord *door = &(SFG_currentLevel.doorRecords[i]);
2019-10-04 15:09:10 -04:00
2019-10-06 10:47:47 -04:00
int8_t height = door->state & SFG_DOOR_VERTICAL_POSITION_MASK;
height = (door->state & SFG_DOOR_UP_DOWN_MASK) ?
RCL_min(0x1f,height + SFG_DOOR_INCREMENT_PER_FRAME) :
RCL_max(0x00,height - SFG_DOOR_INCREMENT_PER_FRAME);
door->state = (door->state & ~SFG_DOOR_VERTICAL_POSITION_MASK) | height;
}
}
// handle items, in a similar manner to door:
if (SFG_currentLevel.itemRecordCount > 0) // has to be here
2019-10-04 15:09:10 -04:00
{
2019-12-26 09:10:38 -05:00
// check item distances:
2019-10-06 10:47:47 -04:00
2019-12-26 09:10:38 -05:00
for (uint16_t i = 0;
i < RCL_min(SFG_ELEMENT_DISTANCES_CHECKED_PER_FRAME,
SFG_currentLevel.itemRecordCount);
++i)
{
SFG_ItemRecord item =
SFG_currentLevel.itemRecords[SFG_currentLevel.checkedItemIndex];
2019-10-06 10:47:47 -04:00
2019-12-26 09:10:38 -05:00
item &= ~SFG_ITEM_RECORD_ACTIVE_MASK;
2019-10-06 10:47:47 -04:00
2019-12-26 09:10:38 -05:00
SFG_LevelElement e =
SFG_currentLevel.levelPointer->elements[item];
if (
RCL_absVal(SFG_player.squarePosition[0] - e.coords[0])
<= SFG_LEVEL_ELEMENT_ACTIVE_DISTANCE
&&
RCL_absVal(SFG_player.squarePosition[1] - e.coords[1])
<= SFG_LEVEL_ELEMENT_ACTIVE_DISTANCE
)
item |= SFG_ITEM_RECORD_ACTIVE_MASK;
2019-10-04 15:09:10 -04:00
2019-12-26 09:10:38 -05:00
SFG_currentLevel.itemRecords[SFG_currentLevel.checkedItemIndex] = item;
2019-10-04 15:09:10 -04:00
2019-12-26 09:10:38 -05:00
SFG_currentLevel.checkedItemIndex++;
2019-10-04 15:09:10 -04:00
2019-12-26 09:10:38 -05:00
if (SFG_currentLevel.checkedItemIndex >= SFG_currentLevel.itemRecordCount)
SFG_currentLevel.checkedItemIndex = 0;
}
2019-10-04 15:09:10 -04:00
}
2019-10-17 15:19:32 -04:00
// similarly handle monsters:
if (SFG_currentLevel.monsterRecordCount > 0) // has to be here
{
2019-12-26 09:10:38 -05:00
// check monster distances:
2019-10-17 15:19:32 -04:00
2019-12-26 09:10:38 -05:00
for (uint16_t i = 0;
i < RCL_min(SFG_ELEMENT_DISTANCES_CHECKED_PER_FRAME,
SFG_currentLevel.monsterRecordCount);
++i)
2019-10-18 09:25:07 -04:00
{
2019-12-26 09:10:38 -05:00
SFG_MonsterRecord *monster =
&(SFG_currentLevel.monsterRecords[SFG_currentLevel.checkedMonsterIndex]);
2019-10-17 15:19:32 -04:00
2019-12-26 09:10:38 -05:00
if ( // far away from the player?
RCL_absVal(SFG_player.squarePosition[0] - monster->coords[0] / 4)
> SFG_LEVEL_ELEMENT_ACTIVE_DISTANCE
||
RCL_absVal(SFG_player.squarePosition[1] - monster->coords[1] / 4)
> SFG_LEVEL_ELEMENT_ACTIVE_DISTANCE
)
{
monster->stateType =
(monster->stateType & SFG_MONSTER_MASK_TYPE) |
SFG_MONSTER_STATE_INACTIVE;
}
2019-12-26 09:28:23 -05:00
else if (SFG_MR_STATE(*monster) == SFG_MONSTER_STATE_INACTIVE)
2019-12-26 09:10:38 -05:00
{
monster->stateType =
(monster->stateType & SFG_MONSTER_MASK_TYPE) |
SFG_MONSTER_STATE_IDLE;
}
2019-10-17 15:19:32 -04:00
2019-12-26 09:10:38 -05:00
SFG_currentLevel.checkedMonsterIndex++;
if (SFG_currentLevel.checkedMonsterIndex >=
SFG_currentLevel.monsterRecordCount)
SFG_currentLevel.checkedMonsterIndex = 0;
}
2019-10-17 15:19:32 -04:00
}
2019-10-17 17:58:19 -04:00
2019-10-21 08:54:32 -04:00
// update AI
2019-10-18 10:34:51 -04:00
if ((SFG_gameFrame - SFG_currentLevel.frameStart) %
SFG_AI_UPDATE_FRAME_INTERVAL == 0)
2019-10-18 07:48:24 -04:00
for (uint8_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
2019-12-26 09:28:23 -05:00
if (SFG_MR_STATE(SFG_currentLevel.monsterRecords[i]) !=
SFG_MONSTER_STATE_INACTIVE)
SFG_monsterPerformAI(&(SFG_currentLevel.monsterRecords[i]));
2019-09-25 09:51:19 -04:00
}
2019-10-08 07:46:12 -04:00
void SFG_clearScreen(uint8_t color)
2019-10-06 11:03:15 -04:00
{
2019-10-08 07:46:12 -04:00
for (uint16_t j = 0; j < SFG_GAME_RESOLUTION_Y; ++j)
for (uint16_t i = 0; i < SFG_GAME_RESOLUTION_X; ++i)
SFG_setGamePixel(i,j,color);
}
/**
Draws fullscreen map of the current level.
*/
void SFG_drawMap()
{
SFG_clearScreen(0);
uint16_t maxJ =
(SFG_MAP_PIXEL_SIZE * SFG_MAP_SIZE) < SFG_GAME_RESOLUTION_Y ?
(SFG_MAP_SIZE) : (SFG_GAME_RESOLUTION_Y / SFG_MAP_PIXEL_SIZE);
uint16_t maxI =
(SFG_MAP_PIXEL_SIZE * SFG_MAP_SIZE) < SFG_GAME_RESOLUTION_X ?
(SFG_MAP_SIZE) : (SFG_GAME_RESOLUTION_X / SFG_MAP_PIXEL_SIZE);
uint16_t topLeftX =
(SFG_GAME_RESOLUTION_X - (maxI * SFG_MAP_PIXEL_SIZE)) / 2;
uint16_t topLeftY =
(SFG_GAME_RESOLUTION_Y - (maxJ * SFG_MAP_PIXEL_SIZE)) / 2;
uint16_t x;
uint16_t y = topLeftY;
2019-10-06 11:03:15 -04:00
2019-10-08 07:46:12 -04:00
for (int16_t j = maxJ - 1; j >= 0; --j)
{
x = topLeftX;
for (uint16_t i = 0; i < maxI; ++i)
2019-10-06 11:03:15 -04:00
{
2019-10-08 07:46:12 -04:00
uint8_t properties;
2019-10-06 11:03:15 -04:00
2019-10-08 07:46:12 -04:00
SFG_TileDefinition tile =
SFG_getMapTile(SFG_currentLevel.levelPointer,i,j,&properties);
2019-10-06 11:03:15 -04:00
2019-10-08 07:46:12 -04:00
uint8_t color = 94; // init with player color
if (i != SFG_player.squarePosition[0] ||
j != SFG_player.squarePosition[1])
{
if (properties == SFG_TILE_PROPERTY_ELEVATOR)
color = 46;
else if (properties == SFG_TILE_PROPERTY_SQUEEZER)
color = 63;
else
{
color = SFG_TILE_FLOOR_HEIGHT(tile) / 8 + 2;
2019-10-06 11:03:15 -04:00
2019-10-08 07:46:12 -04:00
if (properties == SFG_TILE_PROPERTY_DOOR)
color += 8;
}
}
2019-10-06 11:03:15 -04:00
2019-10-08 07:46:12 -04:00
for (uint16_t k = 0; k < SFG_MAP_PIXEL_SIZE; ++k)
for (uint16_t l = 0; l < SFG_MAP_PIXEL_SIZE; ++l)
SFG_setGamePixel(x + l, y + k,color);
2019-10-06 11:03:15 -04:00
2019-10-08 07:46:12 -04:00
x += SFG_MAP_PIXEL_SIZE;
2019-10-06 11:03:15 -04:00
}
2019-10-08 07:46:12 -04:00
y += SFG_MAP_PIXEL_SIZE;
2019-10-10 15:58:46 -04:00
}
}
/**
Draws text on screen using the bitmap font stored in assets.
*/
void SFG_drawText(
const char *text,
uint16_t x,
uint16_t y,
uint8_t size,
uint8_t color)
{
2019-10-10 16:31:50 -04:00
if (size == 0)
size = 1;
2019-10-10 15:58:46 -04:00
uint16_t pos = 0;
uint16_t currentX = x;
uint16_t currentY = y;
while (text[pos] != 0)
{
uint16_t character = SFG_font[SFG_charToFontIndex(text[pos])];
for (uint8_t i = 0; i < 4; ++i)
{
currentY = y;
for (uint8_t j = 0; j < 4; ++j)
{
if (character & 0x8000)
for (uint8_t k = 0; k < size; ++k)
for (uint8_t l = 0; l < size; ++l)
{
uint16_t drawX = currentX + k;
uint16_t drawY = currentY + l;
2019-10-10 16:31:50 -04:00
if (drawX < SFG_GAME_RESOLUTION_X &&
drawY < SFG_GAME_RESOLUTION_Y)
2019-10-10 15:58:46 -04:00
SFG_setGamePixel(drawX,drawY,color);
}
currentY += size;
character = character << 1;
}
currentX += size;
if (currentX >= SFG_GAME_RESOLUTION_X)
break;
}
currentX += size;
if (currentX >= SFG_GAME_RESOLUTION_X)
break;
pos++;
2019-10-08 07:46:12 -04:00
}
}
2019-10-10 19:03:56 -04:00
/**
Draws a number as text on screen, returns the number of characters drawn.
*/
uint8_t SFG_drawNumber(
int16_t number,
uint16_t x,
uint16_t y,
uint8_t size,
uint8_t color
)
{
char text[7];
text[6] = 0; // terminate the string
int8_t positive = 1;
if (number < 0)
{
positive = 0;
number *= -1;
}
int8_t position = 5;
while (1)
{
text[position] = '0' + number % 10;
number /= 10;
position--;
if (number == 0 || position == 0)
break;
}
if (!positive)
{
text[position] = '-';
position--;
}
SFG_drawText(text + position + 1,x,y,size,color);
return 5 - position;
}
2019-10-25 17:17:59 -04:00
void SFG_drawHealthChangeBorder(uint16_t width, uint8_t color)
2019-10-25 16:41:52 -04:00
{
for (uint16_t j = 0; j < width; ++j)
{
uint16_t j2 = SFG_GAME_RESOLUTION_Y - 1 - j;
for (uint16_t i = 0; i < SFG_GAME_RESOLUTION_X; ++i)
{
2019-10-25 17:06:47 -04:00
if ((i & 0x01) == (j & 0x01))
{
2019-10-25 17:17:59 -04:00
SFG_setGamePixel(i,j,color);
SFG_setGamePixel(i,j2,color);
2019-10-25 17:06:47 -04:00
}
2019-10-25 16:41:52 -04:00
}
}
for (uint16_t i = 0; i < width; ++i)
{
uint16_t i2 = SFG_GAME_RESOLUTION_X - 1 - i;
for (uint16_t j = width; j < SFG_GAME_RESOLUTION_Y - width; ++j)
{
2019-10-25 17:06:47 -04:00
if ((i & 0x01) == (j & 0x01))
{
2019-10-25 17:17:59 -04:00
SFG_setGamePixel(i,j,color);
SFG_setGamePixel(i2,j,color);
2019-10-25 17:06:47 -04:00
}
2019-10-25 16:41:52 -04:00
}
}
}
2019-10-23 18:32:04 -04:00
/**
Draws the player weapon, handling the shooting animation.
*/
2019-10-22 15:02:39 -04:00
void SFG_drawWeapon(int16_t bobOffset)
{
uint32_t shotAnimationFrame = SFG_gameFrame - SFG_player.lastShotFrame;
uint32_t animationLength = SFG_WEAPON_SHOTGUN_COOLDOWN_FRAMES;
2019-11-09 16:01:19 -05:00
bobOffset -= SFG_HUD_BAR_HEIGHT;
2019-10-22 15:02:39 -04:00
if (shotAnimationFrame < animationLength)
{
2019-10-23 18:32:04 -04:00
if (SFG_player.weapon == SFG_WEAPON_KNIFE)
{
bobOffset = shotAnimationFrame < animationLength / 2 ? 0 :
2 * SFG_WEAPONBOB_OFFSET_PIXELS ;
}
else
{
bobOffset +=
((animationLength - shotAnimationFrame) * SFG_WEAPON_IMAGE_SCALE * 20)
/ animationLength;
2019-11-09 05:59:52 -05:00
if (
SFG_player.weapon != SFG_WEAPON_KNIFE &&
SFG_player.weapon != SFG_WEAPON_PLASMAGUN &&
shotAnimationFrame < animationLength / 2)
2019-10-23 18:32:04 -04:00
SFG_blitImage(SFG_effectSprites[0],
SFG_WEAPON_IMAGE_POSITION_X,
SFG_WEAPON_IMAGE_POSITION_Y - (SFG_TEXTURE_SIZE / 3) * SFG_WEAPON_IMAGE_SCALE + bobOffset,
SFG_WEAPON_IMAGE_SCALE);
}
2019-10-22 15:02:39 -04:00
}
SFG_blitImage(SFG_weaponImages[SFG_player.weapon],
SFG_WEAPON_IMAGE_POSITION_X,
2019-11-08 15:12:46 -05:00
SFG_WEAPON_IMAGE_POSITION_Y + bobOffset - 1,
2019-10-22 15:02:39 -04:00
SFG_WEAPON_IMAGE_SCALE);
}
2019-10-08 07:46:12 -04:00
void SFG_draw()
{
if (SFG_keyPressed(SFG_KEY_MAP))
{
SFG_drawMap();
}
else
{
2019-10-13 20:29:13 -04:00
for (uint16_t i = 0; i < SFG_Z_BUFFER_SIZE; ++i)
2019-10-08 07:46:12 -04:00
SFG_zBuffer[i] = 255;
2019-10-14 14:21:18 -04:00
int16_t weaponBobOffset;
2019-10-13 20:15:13 -04:00
2019-10-09 11:44:31 -04:00
#if SFG_HEADBOB_ENABLED
2019-10-13 20:15:13 -04:00
RCL_Unit bobSin = RCL_sinInt(SFG_player.headBobFrame);
2019-10-09 11:44:31 -04:00
RCL_Unit headBobOffset =
2019-10-13 20:15:13 -04:00
(bobSin * SFG_HEADBOB_OFFSET) / RCL_UNITS_PER_SQUARE;
weaponBobOffset =
(bobSin * SFG_WEAPONBOB_OFFSET_PIXELS) / (RCL_UNITS_PER_SQUARE) +
SFG_WEAPONBOB_OFFSET_PIXELS;
2019-10-09 11:44:31 -04:00
// add head bob just for the rendering
SFG_player.camera.height += headBobOffset;
#endif
2019-10-08 07:46:12 -04:00
RCL_renderComplex(
SFG_player.camera,
SFG_floorHeightAt,
SFG_ceilingHeightAt,
SFG_texturesAt,
SFG_rayConstraints);
2019-10-09 11:44:31 -04:00
2019-10-08 07:46:12 -04:00
// draw sprites:
2019-10-18 11:06:38 -04:00
// monster sprites:
for (uint8_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
{
SFG_MonsterRecord m = SFG_currentLevel.monsterRecords[i];
2019-12-26 09:28:23 -05:00
uint8_t state = SFG_MR_STATE(m);
2019-10-22 10:48:27 -04:00
if (state != SFG_MONSTER_STATE_INACTIVE)
2019-10-18 11:06:38 -04:00
{
RCL_Vector2D worldPosition;
2019-10-22 06:13:10 -04:00
worldPosition.x = SFG_MONSTER_COORD_TO_RCL_UNITS(m.coords[0]);
worldPosition.y = SFG_MONSTER_COORD_TO_RCL_UNITS(m.coords[1]);
2019-10-18 11:06:38 -04:00
RCL_PixelInfo p =
RCL_mapToScreen(
worldPosition,
2019-10-22 06:13:10 -04:00
SFG_floorHeightAt(
SFG_MONSTER_COORD_TO_SQUARES(m.coords[0]),
SFG_MONSTER_COORD_TO_SQUARES(m.coords[1]))
+ RCL_UNITS_PER_SQUARE / 2,
2019-10-18 11:06:38 -04:00
SFG_player.camera);
if (p.depth > 0)
2019-10-22 10:48:27 -04:00
{
2019-10-22 18:11:26 -04:00
const uint8_t *s =
SFG_getMonsterSprite(
2019-12-26 09:28:23 -05:00
SFG_MR_TYPE(m),
2019-10-22 18:11:26 -04:00
state,
SFG_spriteAnimationFrame & 0x01);
2019-10-22 10:48:27 -04:00
SFG_drawScaledSprite(s,
2019-10-18 11:06:38 -04:00
p.position.x * SFG_RAYCASTING_SUBSAMPLE,p.position.y,
RCL_perspectiveScale(SFG_GAME_RESOLUTION_Y,p.depth),
p.depth / (RCL_UNITS_PER_SQUARE * 2),p.depth);
2019-10-22 10:48:27 -04:00
}
2019-10-18 11:06:38 -04:00
}
}
2019-10-17 15:19:32 -04:00
// item sprites:
2019-10-08 07:46:12 -04:00
for (uint8_t i = 0; i < SFG_currentLevel.itemRecordCount; ++i)
if (SFG_currentLevel.itemRecords[i] & SFG_ITEM_RECORD_ACTIVE_MASK)
{
RCL_Vector2D worldPosition;
SFG_LevelElement e =
SFG_currentLevel.levelPointer->elements[
SFG_currentLevel.itemRecords[i] & ~SFG_ITEM_RECORD_ACTIVE_MASK];
worldPosition.x =
2019-10-22 06:13:10 -04:00
SFG_ELEMENT_COORD_TO_RCL_UNITS(e.coords[0]);
2019-10-08 07:46:12 -04:00
worldPosition.y =
2019-10-22 06:13:10 -04:00
SFG_ELEMENT_COORD_TO_RCL_UNITS(e.coords[1]);
2019-10-08 07:46:12 -04:00
RCL_PixelInfo p =
RCL_mapToScreen(
worldPosition,
2019-10-23 14:17:12 -04:00
SFG_floorHeightAt(e.coords[0],e.coords[1])
+ RCL_UNITS_PER_SQUARE / 2,
2019-10-08 07:46:12 -04:00
SFG_player.camera);
if (p.depth > 0)
2019-10-25 18:50:22 -04:00
{
SFG_drawScaledSprite(SFG_itemSprites[e.type - 1],
2019-10-13 20:29:13 -04:00
p.position.x * SFG_RAYCASTING_SUBSAMPLE,p.position.y,
2019-10-08 07:46:12 -04:00
RCL_perspectiveScale(SFG_GAME_RESOLUTION_Y / 2,p.depth),
p.depth / (RCL_UNITS_PER_SQUARE * 2),p.depth);
2019-10-25 18:50:22 -04:00
}
2019-10-08 07:46:12 -04:00
}
2019-10-09 11:44:31 -04:00
2019-10-21 13:58:11 -04:00
// projecile sprites:
for (uint8_t i = 0; i < SFG_currentLevel.projectileRecordCount; ++i)
{
SFG_ProjectileRecord *proj = &(SFG_currentLevel.projectileRecords[i]);
RCL_Vector2D worldPosition;
worldPosition.x = proj->position[0];
worldPosition.y = proj->position[1];
RCL_PixelInfo p =
RCL_mapToScreen(worldPosition,proj->position[2],SFG_player.camera);
const uint8_t *s =
2019-11-09 05:59:52 -05:00
SFG_effectSprites[proj->type];
2019-10-21 14:07:28 -04:00
2019-10-22 15:02:39 -04:00
int16_t spriteSize = SFG_GAME_RESOLUTION_Y / 3;
2019-10-21 13:58:11 -04:00
if (proj->type == SFG_PROJECTILE_EXPLOSION)
{
2019-10-22 15:02:39 -04:00
// grow the explosion sprite as an animation
2019-10-21 13:58:11 -04:00
spriteSize =
2019-10-22 16:58:41 -04:00
(
SFG_GAME_RESOLUTION_Y *
RCL_sinInt(
((SFG_EXPLOSION_DURATION_DOUBLE_FRAMES -
proj->doubleFramesToLive) * RCL_UNITS_PER_SQUARE / 4)
/ SFG_EXPLOSION_DURATION_DOUBLE_FRAMES)
) / RCL_UNITS_PER_SQUARE;
2019-10-21 13:58:11 -04:00
}
if (p.depth > 0)
SFG_drawScaledSprite(s,
p.position.x * SFG_RAYCASTING_SUBSAMPLE,p.position.y,
2019-10-22 17:59:10 -04:00
RCL_perspectiveScale(spriteSize,p.depth),0,p.depth);
2019-10-21 13:58:11 -04:00
}
2019-10-09 11:44:31 -04:00
#if SFG_HEADBOB_ENABLED
// substract head bob after rendering
SFG_player.camera.height -= headBobOffset;
#endif
2019-10-10 15:58:46 -04:00
2019-11-09 16:01:19 -05:00
SFG_drawWeapon(weaponBobOffset);
2019-10-23 18:09:46 -04:00
// draw the HUD:
2019-11-09 16:01:19 -05:00
// bar
uint8_t color = 5;
for (uint16_t j = SFG_GAME_RESOLUTION_Y - SFG_HUD_BAR_HEIGHT;
j < SFG_GAME_RESOLUTION_Y; ++j)
{
for (uint16_t i = 0; i < SFG_GAME_RESOLUTION_X; ++i)
SFG_setGamePixel(i,j,color);
color = 2;
}
2019-10-24 17:20:35 -04:00
SFG_drawNumber( // health
2019-10-23 18:09:46 -04:00
SFG_player.health,
SFG_HUD_MARGIN,
SFG_GAME_RESOLUTION_Y - SFG_HUD_MARGIN -
SFG_FONT_CHARACTER_SIZE * SFG_FONT_SIZE_MEDIUM,
SFG_FONT_SIZE_MEDIUM,
2019-11-09 16:01:19 -05:00
SFG_player.health > SFG_PLAYER_HEALTH_WARNING_LEVEL ? 4 : 175);
2019-10-23 18:09:46 -04:00
2019-10-24 17:20:35 -04:00
SFG_drawNumber( // ammo
2019-10-23 18:09:46 -04:00
20,
SFG_GAME_RESOLUTION_X - SFG_HUD_MARGIN -
SFG_FONT_CHARACTER_SIZE * SFG_FONT_SIZE_MEDIUM * 3,
SFG_GAME_RESOLUTION_Y - SFG_HUD_MARGIN -
SFG_FONT_CHARACTER_SIZE * SFG_FONT_SIZE_MEDIUM,
SFG_FONT_SIZE_MEDIUM,
2019-11-09 16:01:19 -05:00
4);
2019-10-25 16:41:52 -04:00
if (SFG_gameFrame - SFG_player.lastHurtFrame
2019-10-25 17:17:59 -04:00
<= SFG_HUD_HEALTH_INDICATOR_DURATION_FRAMES)
SFG_drawHealthChangeBorder(SFG_HUD_HEALTH_INDICATOR_WIDTH_PIXELS,175);
2019-10-08 07:46:12 -04:00
}
2019-10-06 11:03:15 -04:00
}
2019-09-25 09:51:19 -04:00
void SFG_mainLoopBody()
{
/* standard deterministic game loop, independed on actuall achieved FPS,
each game logic (physics) frame is performed with the SFG_MS_PER_FRAME
delta time. */
uint32_t timeNow = SFG_getTimeMs();
2019-10-03 13:31:25 -04:00
uint32_t timeNextFrame = SFG_lastFrameTimeMs + SFG_MS_PER_FRAME;
2019-09-25 09:51:19 -04:00
2019-10-06 20:07:10 -04:00
SFG_frameTime = timeNow;
2019-10-03 13:31:25 -04:00
if (timeNow >= timeNextFrame)
2019-09-25 09:51:19 -04:00
{
2019-10-03 13:31:25 -04:00
uint32_t timeSinceLastFrame = timeNow - SFG_lastFrameTimeMs;
uint8_t steps = 0;
2019-09-25 09:51:19 -04:00
// perform game logic (physics), for each frame
while (timeSinceLastFrame >= SFG_MS_PER_FRAME)
{
SFG_gameStep();
timeSinceLastFrame -= SFG_MS_PER_FRAME;
2019-10-03 13:31:25 -04:00
SFG_gameFrame++;
steps++;
2019-09-25 09:51:19 -04:00
}
2019-10-03 13:31:25 -04:00
if (steps > 1)
SFG_LOG("Failed to reach target FPS! Consider setting a lower value.")
2019-09-25 09:51:19 -04:00
// render noly once
2019-10-06 11:03:15 -04:00
SFG_draw();
2019-09-25 09:51:19 -04:00
SFG_lastFrameTimeMs = timeNow;
}
2019-09-29 07:50:40 -04:00
else
2019-10-03 13:31:25 -04:00
{
SFG_sleepMs((timeNextFrame - timeNow) / 2); // wait, relieve CPU
}
2019-09-25 09:51:19 -04:00
}