Update
[anarch.git] / game.h
blob991058d7acb166e43039dfeae40244be7c68b5fe
1 /**
2 @file game.h
4 Main source file of Anarch the game that puts together all the pieces. main
5 game logic is implemented here.
7 physics notes (you can break this when messing around with game constants):
9 - Lowest ceiling under which player can fit is 4 height steps.
10 - Widest hole over which player can run without jumping is 1 square.
11 - Widest hole over which the player can jump is 3 squares.
12 - Highest step a player can walk onto without jumping is 2 height steps.
13 - Highest step a player can jump onto is 3 height steps.
15 by Miloslav Ciz (drummyfish), 2019
17 Released under CC0 1.0 (https://creativecommons.org/publicdomain/zero/1.0/)
18 plus a waiver of all other intellectual property. The goal of this work is
19 be and remain completely in the public domain forever, available for any use
20 whatsoever.
23 #ifndef _SFG_GAME_H
24 #define _SFG_GAME_H
26 #include <stdint.h> // Needed for fixed width types, can easily be replaced.
29 The following keys are mandatory to be implemented on any platform in order
30 for the game to be playable. Enums are bloat.
32 #define SFG_KEY_UP 0
33 #define SFG_KEY_RIGHT 1
34 #define SFG_KEY_DOWN 2
35 #define SFG_KEY_LEFT 3
36 #define SFG_KEY_A 4 ///< fire, confirm
37 #define SFG_KEY_B 5 ///< cancel, strafe, look up/down
38 #define SFG_KEY_C 6 ///< menu, jump, switch weapons
41 The following keys are optional for a platform to implement. They just make
42 the controls more comfortable.
44 #define SFG_KEY_JUMP 7
45 #define SFG_KEY_STRAFE_LEFT 8
46 #define SFG_KEY_STRAFE_RIGHT 9
47 #define SFG_KEY_MAP 10
48 #define SFG_KEY_TOGGLE_FREELOOK 11
49 #define SFG_KEY_NEXT_WEAPON 12
50 #define SFG_KEY_PREVIOUS_WEAPON 13
51 #define SFG_KEY_MENU 14
52 #define SFG_KEY_CYCLE_WEAPON 15
54 #define SFG_KEY_COUNT 16 ///< Number of keys.
56 /* ============================= PORTING =================================== */
58 /* When porting, do the following:
59 - Include this file (and possibly other optional files, like sounds.h) in
60 your main_*.c frontend source.
61 - Implement the following functions in your frontend source.
62 - Call SFG_init() from your frontend initialization code.
63 - Call SFG_mainLoopBody() from within your frontend main loop.
65 If your platform is an AVR CPU (e.g. some Arduinos) and so has Harvard
66 architecture, define #SFG_AVR 1 before including this file in your frontend
67 source. */
69 #ifndef SFG_LOG
70 #define SFG_LOG(str) {} ///< Can be redefined to log game messages.
71 #endif
73 #ifndef SFG_CPU_LOAD
74 #define SFG_CPU_LOAD(percent) {} ///< Can be redefined to check CPU load in %.
75 #endif
77 #ifndef SFG_GAME_STEP_COMMAND
78 #define SFG_GAME_STEP_COMMAND {} /**< Will be called each simulation step
79 (good for creating deterministic behavior
80 such as demos (SFG_mainLoopBody() calls
81 potentially multiple simulation steps). */
82 #endif
84 /**
85 Returns 1 (0) if given key is pressed (not pressed). At least the mandatory
86 keys have to be implemented, the optional keys don't have to ever return 1.
87 See the key constant definitions to see which ones are mandatory.
89 int8_t SFG_keyPressed(uint8_t key);
91 /**
92 Optional function for mouse/joystick/analog controls, gets mouse x and y
93 offset in pixels from the game screen center (to achieve classic FPS mouse
94 controls the platform should center the mouse after this call). If the
95 platform isn't using a mouse, this function can simply return [0,0] offset at
96 each call, or even do nothing at all (leave the variables as are).
98 void SFG_getMouseOffset(int16_t *x, int16_t *y);
101 Returns time in milliseconds sice program start.
103 uint32_t SFG_getTimeMs();
105 /**
106 Sleep (yield CPU) for specified amount of ms. This is used to relieve CPU
107 usage. If your platform doesn't need this or handles it in other way, this
108 function can do nothing.
110 void SFG_sleepMs(uint16_t timeMs);
113 Set specified screen pixel. ColorIndex is the index of the game's palette.
114 The function doesn't have to (and shouldn't, for the sake of performance)
115 check whether the coordinates are within screen bounds.
117 static inline void SFG_setPixel(uint16_t x, uint16_t y, uint8_t colorIndex);
120 Play given sound effect (SFX). This function may or may not use the sound
121 samples provided in sounds.h, and it may or may not ignore the (logarithmic)
122 volume parameter (0 to 255). Depending on the platform, the function can play
123 completely different samples or even e.g. just beeps. If the platform can't
124 play sounds, this function implementation can simply be left empty. This
125 function doesn't have to implement safety measures, the back end takes cares
126 of them.
128 void SFG_playSound(uint8_t soundIndex, uint8_t volume);
130 #define SFG_MUSIC_TURN_OFF 0
131 #define SFG_MUSIC_TURN_ON 1
132 #define SFG_MUSIC_NEXT 2
135 Informs the frontend how music should play, e.g. turn on/off, change track,
136 ... See SFG_MUSIC_* constants. Playing music is optional and the frontend may
137 ignore this. If a frontend wants to implement music, it can use the bytebeat
138 provided in sounds.h or use its own.
140 void SFG_setMusic(uint8_t value);
142 #define SFG_EVENT_VIBRATE 0 ///< the controller should vibrate (or blink etc.)
143 #define SFG_EVENT_PLAYER_HURT 1
144 #define SFG_EVENT_PLAYER_DIES 2
145 #define SFG_EVENT_LEVEL_STARTS 3
146 #define SFG_EVENT_LEVEL_WON 4
147 #define SFG_EVENT_MONSTER_DIES 5
148 #define SFG_EVENT_PLAYER_TAKES_ITEM 6
149 #define SFG_EVENT_EXPLOSION 7
150 #define SFG_EVENT_PLAYER_TELEPORTS 8
151 #define SFG_EVENT_PLAYER_CHANGES_WEAPON 9
154 This is an optional function that informs the frontend about special events
155 which may trigger something special on the platform, such as a controller
156 vibration, logging etc. The implementation of this function may be left empty.
158 void SFG_processEvent(uint8_t event, uint8_t data);
160 #define SFG_SAVE_SIZE 12 ///< size of the save in bytes
163 Optional function for permanently saving the game state. Platforms that don't
164 have permanent storage (HDD, EEPROM etc.) may let this function simply do
165 nothing. If implemented, the function should save the passed data into its
166 permanent storage, e.g. a file, a cookie etc.
168 void SFG_save(uint8_t data[SFG_SAVE_SIZE]);
171 Optional function for retrieving game data that were saved to permanent
172 storage. Platforms without permanent storage may let this function do nothing.
173 If implemented, the function should fill the passed array with data from
174 permanent storage, e.g. a file, a cookie etc.
176 If this function is called before SFG_save was ever called and no data is
177 present in permanent memory, this function should do nothing (leave the data
178 array as is).
180 This function should return 1 if saving/loading is possible or 0 if not (this
181 will be used by the game to detect saving/loading capability).
183 uint8_t SFG_load(uint8_t data[SFG_SAVE_SIZE]);
185 /* ========================================================================= */
188 Main game loop body, call this inside your platform's specific main loop.
189 Returns 1 if the game continues or 0 if the game was exited and program should
190 halt. This functions handles reaching the target FPS and sleeping for
191 relieving CPU, so don't do this.
193 uint8_t SFG_mainLoopBody();
196 Initializes the game, call this in the platform's initialization code.
198 void SFG_init();
200 #include "settings.h"
202 #if SFG_AVR
203 #include <avr/pgmspace.h>
205 #define SFG_PROGRAM_MEMORY const PROGMEM
206 #define SFG_PROGRAM_MEMORY_U8(addr) pgm_read_byte(addr)
207 #else
208 #define SFG_PROGRAM_MEMORY static const
209 #define SFG_PROGRAM_MEMORY_U8(addr) ((uint8_t) (*(addr)))
210 #endif
212 #include "images.h" // don't change the order of these includes
213 #include "levels.h"
214 #include "texts.h"
215 #include "palette.h"
217 #if SFG_TEXTURE_DISTANCE == 0
218 #define RCL_COMPUTE_WALL_TEXCOORDS 0
219 #endif
221 #define RCL_PIXEL_FUNCTION SFG_pixelFunc
222 #define RCL_TEXTURE_VERTICAL_STRETCH 0
224 #define RCL_CAMERA_COLL_HEIGHT_BELOW 800
225 #define RCL_CAMERA_COLL_HEIGHT_ABOVE 200
227 #define RCL_HORIZONTAL_FOV SFG_FOV_HORIZONTAL
228 #define RCL_VERTICAL_FOV SFG_FOV_VERTICAL
230 #include "raycastlib.h"
232 #include "constants.h"
234 typedef struct
236 uint8_t coords[2];
237 uint8_t state; /**< door state in format:
239 MSB ccbaaaaa LSB
241 aaaaa: current door height (how much they're open)
242 b: whether currently going up (0) or down (1)
243 cc: by which card (key) the door is unlocked, 00
244 means no card (unlocked), 1 means card 0 etc. */
245 } SFG_DoorRecord;
247 #define SFG_SPRITE_SIZE(size0to3) \
248 (((size0to3 + 3) * SFG_BASE_SPRITE_SIZE) / 4)
250 #define SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(size0to3) \
251 (SFG_SPRITE_SIZE(size0to3) / 2)
253 #define SFG_SPRITE_SIZE_PIXELS(size0to3) \
254 ((SFG_SPRITE_SIZE(size0to3) * SFG_SPRITE_MAX_SIZE) / RCL_UNITS_PER_SQUARE)
257 Holds information about one instance of a level item (a type of level element,
258 e.g. pickable items, decorations etc.). The format is following:
260 MSB abbbbbbb LSB
262 a: active flag, 1 means the item is nearby to player and is active
263 bbbbbbb: index to elements array of the current level, pointing to element
264 representing this item
266 typedef uint8_t SFG_ItemRecord;
268 #define SFG_ITEM_RECORD_ACTIVE_MASK 0x80
270 #define SFG_ITEM_RECORD_LEVEL_ELEMENT(itemRecord) \
271 (SFG_currentLevel.levelPointer->elements[itemRecord & \
272 ~SFG_ITEM_RECORD_ACTIVE_MASK])
274 typedef struct
276 uint8_t stateType; /**< Holds state (lower 4 bits) and type of monster (upper
277 4 bits). */
278 uint8_t coords[2]; /**< monster position, in 1/4s of a square */
279 uint8_t health;
280 } SFG_MonsterRecord;
282 #define SFG_MR_STATE(mr) ((mr).stateType & SFG_MONSTER_MASK_STATE)
283 #define SFG_MR_TYPE(mr) \
284 (SFG_MONSTER_INDEX_TO_TYPE(((mr).stateType & SFG_MONSTER_MASK_TYPE) >> 4))
286 #define SFG_MONSTER_COORD_TO_RCL_UNITS(c) ((RCL_UNITS_PER_SQUARE / 8) + c * 256)
287 #define SFG_MONSTER_COORD_TO_SQUARES(c) (c / 4)
289 #define SFG_ELEMENT_COORD_TO_RCL_UNITS(c) \
290 (c * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2)
292 #define SFG_MONSTER_MASK_STATE 0x0f
293 #define SFG_MONSTER_MASK_TYPE 0xf0
295 #define SFG_MONSTER_STATE_INACTIVE 0 ///< Not nearby, not actively updated.
296 #define SFG_MONSTER_STATE_IDLE 1
297 #define SFG_MONSTER_STATE_ATTACKING 2
298 #define SFG_MONSTER_STATE_HURTING 3
299 #define SFG_MONSTER_STATE_DYING 4
300 #define SFG_MONSTER_STATE_GOING_N 5
301 #define SFG_MONSTER_STATE_GOING_NE 6
302 #define SFG_MONSTER_STATE_GOING_E 7
303 #define SFG_MONSTER_STATE_GOING_SE 8
304 #define SFG_MONSTER_STATE_GOING_S 9
305 #define SFG_MONSTER_STATE_GOING_SW 10
306 #define SFG_MONSTER_STATE_GOING_W 11
307 #define SFG_MONSTER_STATE_GOING_NW 12
308 #define SFG_MONSTER_STATE_DEAD 13
310 typedef struct
312 uint8_t type;
313 uint8_t doubleFramesToLive; /**< This number times two (because 255 could be
314 too little at high FPS) says after how many
315 frames the projectile is destroyed. */
316 uint16_t position[3]; /**< Current position, stored as u16 to save space, as
317 that is exactly enough to store position on 64x64
318 map. */
319 int16_t direction[3]; /**< Added to position each game step. */
320 } SFG_ProjectileRecord;
322 #define SFG_GAME_STATE_INIT 0 ///< first state, waiting for key releases
323 #define SFG_GAME_STATE_PLAYING 1
324 #define SFG_GAME_STATE_WIN 2
325 #define SFG_GAME_STATE_LOSE 3
326 #define SFG_GAME_STATE_INTRO 4
327 #define SFG_GAME_STATE_OUTRO 5
328 #define SFG_GAME_STATE_MAP 6
329 #define SFG_GAME_STATE_LEVEL_START 7
330 #define SFG_GAME_STATE_MENU 8
332 #define SFG_MENU_ITEM_CONTINUE 0
333 #define SFG_MENU_ITEM_MAP 1
334 #define SFG_MENU_ITEM_PLAY 2
335 #define SFG_MENU_ITEM_LOAD 3
336 #define SFG_MENU_ITEM_SOUND 4
337 #define SFG_MENU_ITEM_SHEAR 5
338 #define SFG_MENU_ITEM_EXIT 6
340 #define SFG_MENU_ITEM_NONE 255
343 GLOBAL VARIABLES
344 ===============================================================================
348 Groups global variables related to the game as such in a single struct. There
349 are still other global structs for player, level etc.
351 struct
353 uint8_t state; ///< Current game state.
354 uint32_t stateTime; ///< Time in ms from last state change.
355 uint8_t currentRandom; ///< for RNG
356 uint8_t spriteAnimationFrame;
357 uint8_t soundsPlayedThisFrame; /**< Each bit says whether given sound was
358 played this frame, prevents playing too many
359 sounds at once. */
360 RCL_RayConstraints rayConstraints; ///< Ray constraints for rendering.
361 RCL_RayConstraints visibilityRayConstraints; ///< Constraints for visibility.
362 uint8_t keyStates[SFG_KEY_COUNT]; /**< Pressed states of keys, each value
363 stores the number of frames for which the
364 key has been held. */
365 uint8_t zBuffer[SFG_Z_BUFFER_SIZE];
366 uint8_t textureAverageColors[SFG_WALL_TEXTURE_COUNT]; /**< Contains average
367 color for each wall texture. */
368 int8_t backgroundScaleMap[SFG_GAME_RESOLUTION_Y];
369 uint16_t backgroundScroll;
370 uint8_t spriteSamplingPoints[SFG_MAX_SPRITE_SIZE]; /**< Helper for
371 precomputing sprite
372 sampling positions for
373 drawing. */
374 uint32_t frameTime; ///< time (in ms) of the current frame start
375 uint32_t frame; ///< frame number
376 uint8_t selectedMenuItem;
377 uint8_t selectedLevel; ///< level to play selected in the main menu
378 uint8_t antiSpam; ///< Prevents log message spamming.
379 uint8_t settings; /**< dynamic game settings (can be changed at runtime),
380 bit meaning:
382 MSB -------- LSB
383 ||||
384 |||\_ sound (SFX)
385 ||\__ music
386 |\___ shearing
387 \____ freelook (shearing not sliding back) */
388 uint8_t blink; ///< Says whether blinkg is currently on or off.
389 uint8_t saved; /**< Helper variable to know if game was saved. Can be
390 0 (not saved), 1 (just saved) or 255 (can't save).*/
391 uint8_t cheatState; /**< Highest bit say whether cheat is enabled, other bits
392 represent the state of typing the cheat code. */
393 uint8_t save[SFG_SAVE_SIZE]; /**< Stores the game save state that's kept in
394 the persistent memory.
396 The save format is binary and platform independent.
397 The save contains game settings, game progress and a
398 saved position. The format is as follows:
400 0 4b (less signif.) highest level that has been reached
401 0 4b (more signif.) level number of the saved position (0: no save)
402 1 8b game settings (SFG_game.settings)
403 2 8b health at saved position
404 3 8b bullet ammo at saved position
405 4 8b rocket ammo at saved position
406 5 8b plasma ammo at saved position
407 6 32b little endian total play time, in 10ths of sec
408 10 16b little endian total enemies killed from start */
409 uint8_t continues; ///< Whether the game continues or was exited.
410 } SFG_game;
412 #define SFG_SAVE_TOTAL_TIME (SFG_game.save[6] + SFG_game.save[7] * 256 + \
413 SFG_game.save[8] * 65536 + SFG_game.save[9] * 4294967296)
416 Stores player state.
418 struct
420 RCL_Camera camera;
421 int8_t squarePosition[2];
422 RCL_Vector2D direction;
423 RCL_Unit verticalSpeed;
424 RCL_Unit previousVerticalSpeed; /**< Vertical speed in previous frame, needed
425 for determining whether player is in the
426 air. */
427 uint16_t headBobFrame;
428 uint8_t weapon; ///< currently selected weapon
429 uint8_t health;
430 uint32_t weaponCooldownFrames; ///< frames left for weapon cooldown
431 uint32_t lastHurtFrame;
432 uint32_t lastItemTakenFrame;
433 uint8_t ammo[SFG_AMMO_TOTAL];
434 uint8_t cards; /**< Lowest 3 bits say which access cards
435 have been taken, the next 3 bits say
436 which cards should be blinking in the HUD,
437 the last 2 bits are a blink reset counter. */
438 uint8_t justTeleported;
439 int8_t previousWeaponDirection; ///< Direction (+/0/-) of previous weapon.
440 } SFG_player;
443 Stores the current level and helper precomputed values for better performance.
445 struct
447 const SFG_Level *levelPointer;
448 uint8_t levelNumber;
449 const uint8_t* textures[7]; ///< textures the level is using
450 uint32_t timeStart;
451 uint32_t frameStart;
452 uint32_t completionTime10sOfS; ///< completion time in 10ths of second
453 uint8_t floorColor;
454 uint8_t ceilingColor;
456 SFG_DoorRecord doorRecords[SFG_MAX_DOORS];
457 uint8_t doorRecordCount;
458 uint8_t checkedDoorIndex; ///< Says which door are currently being checked.
460 SFG_ItemRecord itemRecords[SFG_MAX_ITEMS]; ///< Holds level items.
461 uint8_t itemRecordCount;
462 uint8_t checkedItemIndex; ///< Same as checkedDoorIndex, but for items.
464 SFG_MonsterRecord monsterRecords[SFG_MAX_MONSTERS];
465 uint8_t monsterRecordCount;
466 uint8_t checkedMonsterIndex;
468 SFG_ProjectileRecord projectileRecords[SFG_MAX_PROJECTILES];
469 uint8_t projectileRecordCount;
470 uint8_t bossCount;
471 uint8_t monstersDead;
472 uint8_t backgroundImage;
473 uint8_t teleporterCount;
474 uint16_t mapRevealMask; /**< Bits say which parts of the map have been
475 revealed. */
476 uint8_t itemCollisionMap[(SFG_MAP_SIZE * SFG_MAP_SIZE) / 8];
477 /**< Bit array, for each map square says whether there
478 is a colliding item or not. */
479 } SFG_currentLevel;
481 #if SFG_AVR
483 Copy of the current level that is stored in RAM. This is only done on Arduino
484 because accessing it in program memory (PROGMEM) directly would be a pain.
485 Because of this Arduino needs more RAM.
487 SFG_Level SFG_ramLevel;
488 #endif
491 Helper function for accessing the itemCollisionMap bits.
493 void SFG_getItemCollisionMapIndex(
494 uint8_t x, uint8_t y, uint16_t *byte, uint8_t *bit)
496 uint16_t index = y * SFG_MAP_SIZE + x;
498 *byte = index / 8;
499 *bit = index % 8;
502 void SFG_setItemCollisionMapBit(uint8_t x, uint8_t y, uint8_t value)
504 uint16_t byte;
505 uint8_t bit;
507 SFG_getItemCollisionMapIndex(x,y,&byte,&bit);
509 SFG_currentLevel.itemCollisionMap[byte] &= ~(0x01 << bit);
510 SFG_currentLevel.itemCollisionMap[byte] |= (value & 0x01) << bit;
513 uint8_t SFG_getItemCollisionMapBit(uint8_t x, uint8_t y)
515 uint16_t byte;
516 uint8_t bit;
518 SFG_getItemCollisionMapIndex(x,y,&byte,&bit);
519 return (SFG_currentLevel.itemCollisionMap[byte] >> bit) & 0x01;
522 #if SFG_DITHERED_SHADOW
523 static const uint8_t SFG_ditheringPatterns[] =
525 0,0,0,0,
526 0,0,0,0,
528 0,0,0,0,
529 0,1,0,0,
531 0,0,0,0,
532 0,1,0,1,
534 1,0,1,0,
535 0,1,0,0,
537 1,0,1,0,
538 0,1,0,1,
540 1,0,1,0,
541 0,1,1,1,
543 1,1,1,1,
544 0,1,0,1,
546 1,1,1,1,
547 0,1,1,1,
549 1,1,1,1,
550 1,1,1,1
552 #endif
555 FUNCTIONS
556 ===============================================================================
560 Returns a pseudorandom byte. This is a very simple congruent generator, its
561 parameters have been chosen so that each number (0-255) is included in the
562 output exactly once!
564 uint8_t SFG_random()
566 SFG_game.currentRandom *= 13;
567 SFG_game.currentRandom += 7;
569 return SFG_game.currentRandom;
572 void SFG_playGameSound(uint8_t soundIndex, uint8_t volume)
574 if (!(SFG_game.settings & 0x01))
575 return;
577 uint8_t mask = 0x01 << soundIndex;
579 if (!(SFG_game.soundsPlayedThisFrame & mask))
581 SFG_playSound(soundIndex,volume);
582 SFG_game.soundsPlayedThisFrame |= mask;
587 Returns a damage value for specific attack type (SFG_WEAPON_FIRE_TYPE_...),
588 with added randomness (so the values will differ). For explosion pass
589 SFG_WEAPON_FIRE_TYPE_FIREBALL.
591 uint8_t SFG_getDamageValue(uint8_t attackType)
593 if (attackType >= SFG_WEAPON_FIRE_TYPES_TOTAL)
594 return 0;
596 int32_t value = SFG_attackDamageTable[attackType]; // has to be signed
597 int32_t maxAdd = (value * SFG_DAMAGE_RANDOMNESS) / 256;
599 value = value + (maxAdd / 2) - (SFG_random() * maxAdd / 256);
601 if (value < 0)
602 value = 0;
604 return value;
608 Saves game data to persistent storage.
610 void SFG_gameSave()
612 if (SFG_game.saved == SFG_CANT_SAVE)
613 return;
615 SFG_LOG("saving game data");
617 SFG_save(SFG_game.save);
621 Loads game data from persistent storage.
623 void SFG_gameLoad()
625 if (SFG_game.saved == SFG_CANT_SAVE)
626 return;
628 SFG_LOG("loading game data");
630 uint8_t result = SFG_load(SFG_game.save);
632 if (result == 0)
633 SFG_game.saved = SFG_CANT_SAVE;
637 Returns ammo type for given weapon.
639 uint8_t SFG_weaponAmmo(uint8_t weapon)
641 if (weapon == SFG_WEAPON_KNIFE)
642 return SFG_AMMO_NONE;
643 if (weapon == SFG_WEAPON_MACHINE_GUN ||
644 weapon == SFG_WEAPON_SHOTGUN)
645 return SFG_AMMO_BULLETS;
646 else if (weapon == SFG_WEAPON_ROCKET_LAUNCHER)
647 return SFG_AMMO_ROCKETS;
648 else
649 return SFG_AMMO_PLASMA;
652 RCL_Unit SFG_taxicabDistance(
653 RCL_Unit x0, RCL_Unit y0, RCL_Unit z0, RCL_Unit x1, RCL_Unit y1, RCL_Unit z1)
655 return (RCL_abs(x0 - x1) + RCL_abs(y0 - y1) + RCL_abs(z0 - z1));
658 uint8_t SFG_isInActiveDistanceFromPlayer(RCL_Unit x, RCL_Unit y, RCL_Unit z)
660 return SFG_taxicabDistance(
661 x,y,z,SFG_player.camera.position.x,SFG_player.camera.position.y,
662 SFG_player.camera.height) <= SFG_LEVEL_ELEMENT_ACTIVE_DISTANCE;
666 Function called when a level end to compute the stats etc.
668 void SFG_levelEnds()
670 SFG_currentLevel.completionTime10sOfS = (SFG_MS_PER_FRAME *
671 (SFG_game.frame - SFG_currentLevel.frameStart)) / 100;
673 if (
674 (SFG_player.health != 0) &&
675 (SFG_currentLevel.levelNumber >= (SFG_game.save[0] & 0x0f)) &&
676 ((SFG_currentLevel.levelNumber + 1) < SFG_NUMBER_OF_LEVELS))
678 SFG_game.save[0] = // save progress
679 (SFG_game.save[0] & 0xf0) | (SFG_currentLevel.levelNumber + 1);
681 SFG_gameSave();
684 SFG_currentLevel.monstersDead = 0;
686 for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
687 if (SFG_currentLevel.monsterRecords[i].health == 0)
688 SFG_currentLevel.monstersDead++;
690 uint32_t totalTime = SFG_SAVE_TOTAL_TIME;
692 if ((SFG_currentLevel.levelNumber == 0) || (totalTime != 0))
694 SFG_LOG("Updating save totals.");
696 totalTime += SFG_currentLevel.completionTime10sOfS;
698 for (uint8_t i = 0; i < 4; ++i)
700 SFG_game.save[6 + i] = totalTime % 256;
701 totalTime /= 256;
704 SFG_game.save[10] += SFG_currentLevel.monstersDead % 256;
705 SFG_game.save[11] += SFG_currentLevel.monstersDead / 256;
708 SFG_game.save[2] = SFG_player.health;
709 SFG_game.save[3] = SFG_player.ammo[0];
710 SFG_game.save[4] = SFG_player.ammo[1];
711 SFG_game.save[5] = SFG_player.ammo[2];
714 static inline uint8_t SFG_RCLUnitToZBuffer(RCL_Unit x)
716 x /= (RCL_UNITS_PER_SQUARE / 8);
718 uint8_t okay = x < 256;
720 return okay * (x + 1) - 1;
723 const uint8_t *SFG_getMonsterSprite(
724 uint8_t monsterType, uint8_t state, uint8_t frame)
726 uint8_t index =
727 state == SFG_MONSTER_STATE_DEAD ? 18 : 17;
728 // ^ makes the compiled binary smaller compared to returning pointers directly
730 if ((state != SFG_MONSTER_STATE_DYING) && (state != SFG_MONSTER_STATE_DEAD))
731 switch (monsterType)
733 case SFG_LEVEL_ELEMENT_MONSTER_SPIDER:
734 switch (state)
736 case SFG_MONSTER_STATE_ATTACKING: index = 1; break;
737 case SFG_MONSTER_STATE_IDLE: index = 0; break;
738 default: index = frame ? 0 : 2; break;
740 break;
742 case SFG_LEVEL_ELEMENT_MONSTER_WARRIOR:
743 index = state != SFG_MONSTER_STATE_ATTACKING ? 6 : 7;
744 break;
746 case SFG_LEVEL_ELEMENT_MONSTER_DESTROYER:
747 switch (state)
749 case SFG_MONSTER_STATE_ATTACKING: index = 4; break;
750 case SFG_MONSTER_STATE_IDLE: index = 3; break;
751 default: index = frame ? 3 : 5; break;
753 break;
755 case SFG_LEVEL_ELEMENT_MONSTER_PLASMABOT:
756 index = state != SFG_MONSTER_STATE_ATTACKING ? 8 : 9;
757 break;
759 case SFG_LEVEL_ELEMENT_MONSTER_ENDER:
760 switch (state)
762 case SFG_MONSTER_STATE_ATTACKING: index = 12; break;
763 case SFG_MONSTER_STATE_IDLE: index = 10; break;
764 default: index = frame ? 10 : 11; break;
766 break;
768 case SFG_LEVEL_ELEMENT_MONSTER_TURRET:
769 switch (state)
771 case SFG_MONSTER_STATE_ATTACKING: index = 15; break;
772 case SFG_MONSTER_STATE_IDLE: index = 13; break;
773 default: index = frame ? 13 : 14; break;
775 break;
777 case SFG_LEVEL_ELEMENT_MONSTER_EXPLODER:
778 default:
779 index = 16;
780 break;
783 return SFG_monsterSprites + index * SFG_TEXTURE_STORE_SIZE;
787 Says whether given key is currently pressed (down). This should be preferred
788 to SFG_keyPressed().
790 uint8_t SFG_keyIsDown(uint8_t key)
792 return SFG_game.keyStates[key] != 0;
796 Says whether given key has been pressed in the current frame.
798 uint8_t SFG_keyJustPressed(uint8_t key)
800 return (SFG_game.keyStates[key]) == 1;
804 Says whether a key is being repeated after being held for certain time.
806 uint8_t SFG_keyRepeated(uint8_t key)
808 return
809 ((SFG_game.keyStates[key] >= SFG_KEY_REPEAT_DELAY_FRAMES) ||
810 (SFG_game.keyStates[key] == 255)) &&
811 (SFG_game.frame % SFG_KEY_REPEAT_PERIOD_FRAMES == 0);
814 uint16_t SFG_keyRegisters(uint8_t key)
816 return SFG_keyJustPressed(key) || SFG_keyRepeated(key);
819 #if SFG_RESOLUTION_SCALEDOWN == 1
820 #define SFG_setGamePixel SFG_setPixel
821 #else
824 Sets the game pixel (a pixel that can potentially be bigger than the screen
825 pixel).
827 static inline void SFG_setGamePixel(uint16_t x, uint16_t y, uint8_t colorIndex)
829 uint16_t screenY = y * SFG_RESOLUTION_SCALEDOWN;
830 uint16_t screenX = x * SFG_RESOLUTION_SCALEDOWN;
832 for (uint16_t j = screenY; j < screenY + SFG_RESOLUTION_SCALEDOWN; ++j)
833 for (uint16_t i = screenX; i < screenX + SFG_RESOLUTION_SCALEDOWN; ++i)
834 SFG_setPixel(i,j,colorIndex);
836 #endif
838 void SFG_recomputePLayerDirection()
840 SFG_player.camera.direction =
841 RCL_wrap(SFG_player.camera.direction,RCL_UNITS_PER_SQUARE);
843 SFG_player.direction = RCL_angleToDirection(SFG_player.camera.direction);
845 SFG_player.direction.x =
846 (SFG_player.direction.x * SFG_PLAYER_MOVE_UNITS_PER_FRAME)
847 / RCL_UNITS_PER_SQUARE;
849 SFG_player.direction.y =
850 (SFG_player.direction.y * SFG_PLAYER_MOVE_UNITS_PER_FRAME)
851 / RCL_UNITS_PER_SQUARE;
853 SFG_game.backgroundScroll =
854 ((SFG_player.camera.direction * 8) * SFG_GAME_RESOLUTION_Y)
855 / RCL_UNITS_PER_SQUARE;
858 #if SFG_BACKGROUND_BLUR != 0
859 uint8_t SFG_backgroundBlurIndex = 0;
861 static const int8_t SFG_backgroundBlurOffsets[8] =
863 0 * SFG_BACKGROUND_BLUR,
864 16 * SFG_BACKGROUND_BLUR,
865 7 * SFG_BACKGROUND_BLUR,
866 17 * SFG_BACKGROUND_BLUR,
867 1 * SFG_BACKGROUND_BLUR,
868 4 * SFG_BACKGROUND_BLUR,
869 15 * SFG_BACKGROUND_BLUR,
870 9 * SFG_BACKGROUND_BLUR,
872 #endif
874 static inline uint8_t SFG_fogValueDiminish(RCL_Unit depth)
876 return depth / SFG_FOG_DIMINISH_STEP;
879 static inline uint8_t
880 SFG_getTexelFull(uint8_t textureIndex,RCL_Unit u, RCL_Unit v)
882 return
883 SFG_getTexel(
884 textureIndex != 255 ?
885 SFG_currentLevel.textures[textureIndex] :
886 (SFG_wallTextures + SFG_currentLevel.levelPointer->doorTextureIndex
887 * SFG_TEXTURE_STORE_SIZE),
888 u / (RCL_UNITS_PER_SQUARE / SFG_TEXTURE_SIZE),
889 v / (RCL_UNITS_PER_SQUARE / SFG_TEXTURE_SIZE));
892 static inline uint8_t SFG_getTexelAverage(uint8_t textureIndex)
894 return
895 textureIndex != 255 ?
896 SFG_game.textureAverageColors[
897 SFG_currentLevel.levelPointer->textureIndices[textureIndex]]
900 SFG_game.textureAverageColors[
901 SFG_currentLevel.levelPointer->doorTextureIndex]
902 + 1 // to distinguish from normal walls
906 void SFG_pixelFunc(RCL_PixelInfo *pixel)
908 uint8_t color;
909 uint8_t shadow = 0;
911 if (pixel->isHorizon && pixel->depth > RCL_UNITS_PER_SQUARE * 16)
913 color = SFG_TRANSPARENT_COLOR;
915 else if (pixel->isWall)
917 uint8_t textureIndex =
918 pixel->isFloor ?
920 ((pixel->hit.type & SFG_TILE_PROPERTY_MASK) != SFG_TILE_PROPERTY_DOOR) ?
921 (pixel->hit.type & 0x7)
924 (pixel->texCoords.y > RCL_UNITS_PER_SQUARE) ?
925 (pixel->hit.type & 0x7) : 255
928 ((pixel->hit.type & 0x38) >> 3);
930 #if SFG_TEXTURE_DISTANCE != 0
931 RCL_Unit textureV = pixel->texCoords.y;
933 if ((pixel->hit.type & SFG_TILE_PROPERTY_MASK) ==
934 SFG_TILE_PROPERTY_SQUEEZER)
935 textureV += pixel->wallHeight;
936 #endif
938 color =
939 textureIndex != SFG_TILE_TEXTURE_TRANSPARENT ?
941 #if SFG_TEXTURE_DISTANCE >= 65535
942 SFG_getTexelFull(textureIndex,pixel->texCoords.x,textureV)
943 #elif SFG_TEXTURE_DISTANCE == 0
944 SFG_getTexelAverage(textureIndex)
945 #else
946 pixel->depth <= SFG_TEXTURE_DISTANCE ?
947 SFG_getTexelFull(textureIndex,pixel->texCoords.x,textureV) :
948 SFG_getTexelAverage(textureIndex)
949 #endif
952 SFG_TRANSPARENT_COLOR;
954 shadow = pixel->hit.direction >> 1;
956 else // floor/ceiling
958 color = pixel->isFloor ?
960 #if SFG_DIFFERENT_FLOOR_CEILING_COLORS
961 2 + (pixel->height / SFG_WALL_HEIGHT_STEP) % 4
962 #else
963 SFG_currentLevel.floorColor
964 #endif
965 ) :
966 (pixel->height < SFG_CEILING_MAX_HEIGHT ?
968 #if SFG_DIFFERENT_FLOOR_CEILING_COLORS
969 18 + (pixel->height / SFG_WALL_HEIGHT_STEP) % 4
970 #else
971 SFG_currentLevel.ceilingColor
972 #endif
974 : SFG_TRANSPARENT_COLOR);
977 if (color != SFG_TRANSPARENT_COLOR)
979 #if SFG_DITHERED_SHADOW
980 uint8_t fogShadow = (pixel->depth * 8) / SFG_FOG_DIMINISH_STEP;
982 uint8_t fogShadowPart = fogShadow & 0x07;
984 fogShadow /= 8;
986 uint8_t xMod4 = pixel->position.x & 0x03;
987 uint8_t yMod2 = pixel->position.y & 0x01;
989 shadow +=
990 fogShadow + SFG_ditheringPatterns[fogShadowPart * 8 + yMod2 * 4 + xMod4];
991 #else
992 shadow += SFG_fogValueDiminish(pixel->depth);
993 #endif
995 #if SFG_ENABLE_FOG
996 color = palette_minusValue(color,shadow);
997 #endif
999 else
1001 #if SFG_DRAW_LEVEL_BACKGROUND
1002 color = SFG_getTexel(SFG_backgroundImages +
1003 SFG_currentLevel.backgroundImage * SFG_TEXTURE_STORE_SIZE,
1004 SFG_game.backgroundScaleMap[((pixel->position.x
1005 #if SFG_BACKGROUND_BLUR != 0
1006 + SFG_backgroundBlurOffsets[SFG_backgroundBlurIndex]
1007 #endif
1008 ) * SFG_RAYCASTING_SUBSAMPLE + SFG_game.backgroundScroll) % SFG_GAME_RESOLUTION_Y],
1009 (SFG_game.backgroundScaleMap[(pixel->position.y // ^ TODO: get rid of mod?
1010 #if SFG_BACKGROUND_BLUR != 0
1011 + SFG_backgroundBlurOffsets[SFG_backgroundBlurIndex + 1]
1012 #endif
1013 ) % SFG_GAME_RESOLUTION_Y])
1016 #if SFG_BACKGROUND_BLUR != 0
1017 SFG_backgroundBlurIndex = (SFG_backgroundBlurIndex + 1) % 8;
1018 #endif
1019 #else
1020 color = 1;
1021 #endif
1024 #if SFG_BRIGHTNESS > 0
1025 color = palette_plusValue(color,SFG_BRIGHTNESS);
1026 #elif SFG_BRIGHTNESS < 0
1027 color = palette_minusValue(color,-1 * SFG_BRIGHTNESS);
1028 #endif
1030 #if SFG_RAYCASTING_SUBSAMPLE == 1
1031 // the other version will probably get optimized to this, but just in case
1032 SFG_setGamePixel(pixel->position.x,pixel->position.y,color);
1033 #else
1034 RCL_Unit screenX = pixel->position.x * SFG_RAYCASTING_SUBSAMPLE;
1036 for (int_fast8_t i = 0; i < SFG_RAYCASTING_SUBSAMPLE; ++i)
1038 SFG_setGamePixel(screenX,pixel->position.y,color);
1039 screenX++;
1041 #endif
1045 Draws image on screen, with transparency. This is faster than sprite drawing.
1046 For performance sake drawing near screen edges is not pixel perfect.
1048 void SFG_blitImage(
1049 const uint8_t *image,
1050 int16_t posX,
1051 int16_t posY,
1052 uint8_t scale)
1054 if (scale == 0)
1055 return;
1057 uint16_t x0 = posX,
1059 y0 = posY,
1062 uint8_t u0 = 0, v0 = 0;
1064 if (posX < 0)
1066 x0 = 0;
1067 u0 = (-1 * posX) / scale;
1070 posX += scale * SFG_TEXTURE_SIZE;
1072 uint16_t limitX = SFG_GAME_RESOLUTION_X - scale;
1073 uint16_t limitY = SFG_GAME_RESOLUTION_Y - scale;
1075 x1 = posX >= 0 ?
1076 (posX <= limitX ? posX : limitX)
1077 : 0;
1079 if (x1 >= SFG_GAME_RESOLUTION_X)
1080 x1 = SFG_GAME_RESOLUTION_X - 1;
1082 if (posY < 0)
1084 y0 = 0;
1085 v0 = (-1 * posY) / scale;
1088 posY += scale * SFG_TEXTURE_SIZE;
1090 y1 = posY >= 0 ? (posY <= limitY ? posY : limitY) : 0;
1092 if (y1 >= SFG_GAME_RESOLUTION_Y)
1093 y1 = SFG_GAME_RESOLUTION_Y - 1;
1095 uint8_t v = v0;
1097 for (uint16_t y = y0; y < y1; y += scale)
1099 uint8_t u = u0;
1101 for (uint16_t x = x0; x < x1; x += scale)
1103 uint8_t color = SFG_getTexel(image,u,v);
1105 if (color != SFG_TRANSPARENT_COLOR)
1107 uint16_t sY = y;
1109 for (uint8_t j = 0; j < scale; ++j)
1111 uint16_t sX = x;
1113 for (uint8_t i = 0; i < scale; ++i)
1115 SFG_setGamePixel(sX,sY,color);
1116 sX++;
1119 sY++;
1122 u++;
1124 v++;
1128 void SFG_drawScaledSprite(
1129 const uint8_t *image,
1130 int16_t centerX,
1131 int16_t centerY,
1132 int16_t size,
1133 uint8_t minusValue,
1134 RCL_Unit distance)
1136 if (size == 0)
1137 return;
1139 if (size > SFG_MAX_SPRITE_SIZE)
1140 size = SFG_MAX_SPRITE_SIZE;
1142 uint16_t halfSize = size / 2;
1144 int16_t topLeftX = centerX - halfSize;
1145 int16_t topLeftY = centerY - halfSize;
1147 int16_t x0, u0;
1149 if (topLeftX < 0)
1151 u0 = -1 * topLeftX;
1152 x0 = 0;
1154 else
1156 u0 = 0;
1157 x0 = topLeftX;
1160 int16_t x1 = topLeftX + size - 1;
1162 if (x1 >= SFG_GAME_RESOLUTION_X)
1163 x1 = SFG_GAME_RESOLUTION_X - 1;
1165 int16_t y0, v0;
1167 if (topLeftY < 0)
1169 v0 = -1 * topLeftY;
1170 y0 = 0;
1172 else
1174 v0 = 0;
1175 y0 = topLeftY;
1178 int16_t y1 = topLeftY + size - 1;
1180 if (y1 >= SFG_GAME_RESOLUTION_Y)
1181 y1 = SFG_GAME_RESOLUTION_Y - 1;
1183 if ((x0 > x1) || (y0 > y1) || (u0 >= size) || (v0 >= size)) // outside screen?
1184 return;
1186 int16_t u1 = u0 + (x1 - x0);
1187 int16_t v1 = v0 + (y1 - y0);
1189 // precompute sampling positions:
1191 int16_t uMin = RCL_min(u0,u1);
1192 int16_t vMin = RCL_min(v0,v1);
1193 int16_t uMax = RCL_max(u0,u1);
1194 int16_t vMax = RCL_max(v0,v1);
1196 int16_t precompFrom = RCL_min(uMin,vMin);
1197 int16_t precompTo = RCL_max(uMax,vMax);
1199 precompFrom = RCL_max(0,precompFrom);
1200 precompTo = RCL_min(SFG_MAX_SPRITE_SIZE - 1,precompTo);
1202 #define PRECOMP_SCALE 512
1204 int16_t precompStepScaled = ((SFG_TEXTURE_SIZE) * PRECOMP_SCALE) / size;
1205 int16_t precompPosScaled = precompFrom * precompStepScaled;
1207 for (int16_t i = precompFrom; i <= precompTo; ++i)
1209 SFG_game.spriteSamplingPoints[i] = precompPosScaled / PRECOMP_SCALE;
1210 precompPosScaled += precompStepScaled;
1213 #undef PRECOMP_SCALE
1215 uint8_t zDistance = SFG_RCLUnitToZBuffer(distance);
1217 for (int16_t x = x0, u = u0; x <= x1; ++x, ++u)
1219 if (SFG_game.zBuffer[x] >= zDistance)
1221 int8_t columnTransparent = 1;
1223 for (int16_t y = y0, v = v0; y <= y1; ++y, ++v)
1225 uint8_t color =
1226 SFG_getTexel(image,SFG_game.spriteSamplingPoints[u],
1227 SFG_game.spriteSamplingPoints[v]);
1229 if (color != SFG_TRANSPARENT_COLOR)
1231 #if SFG_DIMINISH_SPRITES
1232 color = palette_minusValue(color,minusValue);
1233 #endif
1234 columnTransparent = 0;
1236 SFG_setGamePixel(x,y,color);
1240 if (!columnTransparent)
1241 SFG_game.zBuffer[x] = zDistance;
1246 RCL_Unit SFG_texturesAt(int16_t x, int16_t y)
1248 uint8_t p;
1250 SFG_TileDefinition tile =
1251 SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&p);
1253 return
1254 SFG_TILE_FLOOR_TEXTURE(tile) | (SFG_TILE_CEILING_TEXTURE(tile) << 3) | p;
1255 // ^ store both textures (floor and ceiling) and properties in one number
1258 RCL_Unit SFG_movingWallHeight
1260 RCL_Unit low,
1261 RCL_Unit high,
1262 uint32_t time
1265 RCL_Unit height = RCL_nonZero(high - low);
1266 RCL_Unit halfHeight = height / 2;
1268 RCL_Unit sinArg =
1269 (time * ((SFG_MOVING_WALL_SPEED * RCL_UNITS_PER_SQUARE) / 1000)) / height;
1271 return
1272 low + halfHeight + (RCL_sin(sinArg) * halfHeight) / RCL_UNITS_PER_SQUARE;
1275 RCL_Unit SFG_floorHeightAt(int16_t x, int16_t y)
1277 uint8_t properties;
1279 SFG_TileDefinition tile =
1280 SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&properties);
1282 RCL_Unit doorHeight = 0;
1284 if (properties == SFG_TILE_PROPERTY_DOOR)
1286 for (uint8_t i = 0; i < SFG_currentLevel.doorRecordCount; ++i)
1288 SFG_DoorRecord *door = &(SFG_currentLevel.doorRecords[i]);
1290 if ((door->coords[0] == x) && (door->coords[1] == y))
1292 doorHeight = door->state & SFG_DOOR_VERTICAL_POSITION_MASK;
1294 doorHeight = doorHeight != (0xff & SFG_DOOR_VERTICAL_POSITION_MASK) ?
1295 doorHeight * SFG_DOOR_HEIGHT_STEP : RCL_UNITS_PER_SQUARE;
1297 break;
1301 else if (properties == SFG_TILE_PROPERTY_ELEVATOR)
1303 RCL_Unit height =
1304 SFG_TILE_FLOOR_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP;
1306 return SFG_movingWallHeight(
1307 height,
1308 height + SFG_TILE_CEILING_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP,
1309 SFG_game.frameTime - SFG_currentLevel.timeStart);
1312 return SFG_TILE_FLOOR_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP - doorHeight;
1316 Like SFG_floorCollisionHeightAt, but takes into account colliding items on
1317 the map, so the squares that have these items are higher. The former function
1318 is for rendering, this one is for collision checking.
1320 RCL_Unit SFG_floorCollisionHeightAt(int16_t x, int16_t y)
1322 return SFG_floorHeightAt(x,y) +
1323 SFG_getItemCollisionMapBit(x,y) * RCL_UNITS_PER_SQUARE;
1326 void SFG_getPlayerWeaponInfo(
1327 uint8_t *ammoType, uint8_t *projectileCount, uint8_t *canShoot)
1329 *ammoType = SFG_weaponAmmo(SFG_player.weapon);
1331 *projectileCount = SFG_GET_WEAPON_PROJECTILE_COUNT(SFG_player.weapon);
1333 #if SFG_INFINITE_AMMO
1334 *canShoot = 1;
1335 #else
1336 *canShoot =
1337 ((*ammoType == SFG_AMMO_NONE) ||
1338 (SFG_player.ammo[*ammoType] >= *projectileCount) ||
1339 (SFG_game.cheatState & 0x80));
1340 #endif
1343 void SFG_playerRotateWeapon(uint8_t next)
1345 uint8_t initialWeapon = SFG_player.weapon;
1346 int8_t increment = next ? 1 : -1;
1348 while (1)
1350 SFG_player.weapon =
1351 (SFG_WEAPONS_TOTAL + SFG_player.weapon + increment) % SFG_WEAPONS_TOTAL;
1353 if (SFG_player.weapon == initialWeapon)
1354 break;
1356 uint8_t ammo, projectileCount, canShoot;
1358 SFG_getPlayerWeaponInfo(&ammo,&projectileCount,&canShoot);
1360 if (canShoot)
1361 break;
1365 void SFG_initPlayer()
1367 RCL_initCamera(&SFG_player.camera);
1369 SFG_player.camera.resolution.x =
1370 SFG_GAME_RESOLUTION_X / SFG_RAYCASTING_SUBSAMPLE;
1372 SFG_player.camera.resolution.y = SFG_GAME_RESOLUTION_Y - SFG_HUD_BAR_HEIGHT;
1374 SFG_player.camera.position.x = RCL_UNITS_PER_SQUARE / 2 +
1375 SFG_currentLevel.levelPointer->playerStart[0] * RCL_UNITS_PER_SQUARE;
1377 SFG_player.camera.position.y = RCL_UNITS_PER_SQUARE / 2 +
1378 SFG_currentLevel.levelPointer->playerStart[1] * RCL_UNITS_PER_SQUARE;
1380 SFG_player.squarePosition[0] =
1381 SFG_player.camera.position.x / RCL_UNITS_PER_SQUARE;
1383 SFG_player.squarePosition[1] =
1384 SFG_player.camera.position.y / RCL_UNITS_PER_SQUARE;
1386 SFG_player.camera.height = SFG_floorHeightAt(
1387 SFG_currentLevel.levelPointer->playerStart[0],
1388 SFG_currentLevel.levelPointer->playerStart[1]) +
1389 RCL_CAMERA_COLL_HEIGHT_BELOW;
1391 SFG_player.camera.direction = SFG_currentLevel.levelPointer->playerStart[2] *
1392 (RCL_UNITS_PER_SQUARE / 256);
1394 SFG_recomputePLayerDirection();
1396 SFG_player.previousVerticalSpeed = 0;
1398 SFG_player.headBobFrame = 0;
1400 SFG_player.weapon = SFG_WEAPON_KNIFE;
1402 SFG_player.weaponCooldownFrames = 0;
1403 SFG_player.lastHurtFrame = SFG_game.frame;
1404 SFG_player.lastItemTakenFrame = SFG_game.frame;
1406 SFG_player.health = SFG_PLAYER_START_HEALTH;
1408 SFG_player.previousWeaponDirection = 0;
1410 SFG_player.cards =
1411 #if SFG_UNLOCK_DOOR
1412 0x07;
1413 #else
1415 #endif
1417 SFG_player.justTeleported = 0;
1419 for (uint8_t i = 0; i < SFG_AMMO_TOTAL; ++i)
1420 SFG_player.ammo[i] = 0;
1423 RCL_Unit SFG_ceilingHeightAt(int16_t x, int16_t y)
1425 uint8_t properties;
1426 SFG_TileDefinition tile =
1427 SFG_getMapTile(SFG_currentLevel.levelPointer,x,y,&properties);
1429 if (properties == SFG_TILE_PROPERTY_ELEVATOR)
1430 return SFG_CEILING_MAX_HEIGHT;
1432 uint8_t height = SFG_TILE_CEILING_HEIGHT(tile);
1434 return properties != SFG_TILE_PROPERTY_SQUEEZER ?
1436 height != SFG_TILE_CEILING_MAX_HEIGHT ?
1437 ((SFG_TILE_FLOOR_HEIGHT(tile) + height) * SFG_WALL_HEIGHT_STEP) :
1438 SFG_CEILING_MAX_HEIGHT
1440 SFG_movingWallHeight(
1441 SFG_TILE_FLOOR_HEIGHT(tile) * SFG_WALL_HEIGHT_STEP,
1442 (SFG_TILE_CEILING_HEIGHT(tile) + SFG_TILE_FLOOR_HEIGHT(tile))
1443 * SFG_WALL_HEIGHT_STEP,
1444 SFG_game.frameTime - SFG_currentLevel.timeStart);
1448 Gets sprite (image and sprite size) for given item.
1450 void SFG_getItemSprite(
1451 uint8_t elementType, const uint8_t **sprite, uint8_t *spriteSize)
1453 *spriteSize = 0;
1454 *sprite = SFG_itemSprites + (elementType - 1) * SFG_TEXTURE_STORE_SIZE;
1456 switch (elementType)
1458 case SFG_LEVEL_ELEMENT_TREE:
1459 case SFG_LEVEL_ELEMENT_RUIN:
1460 case SFG_LEVEL_ELEMENT_LAMP:
1461 case SFG_LEVEL_ELEMENT_TELEPORTER:
1462 *spriteSize = 2;
1463 break;
1465 case SFG_LEVEL_ELEMENT_TERMINAL:
1466 *spriteSize = 1;
1467 break;
1469 case SFG_LEVEL_ELEMENT_FINISH:
1470 case SFG_LEVEL_ELEMENT_COLUMN:
1471 *spriteSize = 3;
1472 break;
1474 case SFG_LEVEL_ELEMENT_CARD0:
1475 case SFG_LEVEL_ELEMENT_CARD1:
1476 case SFG_LEVEL_ELEMENT_CARD2:
1477 *sprite = SFG_itemSprites +
1478 (SFG_LEVEL_ELEMENT_CARD0 - 1) * SFG_TEXTURE_STORE_SIZE;
1479 break;
1481 case SFG_LEVEL_ELEMENT_BLOCKER:
1482 *sprite = 0;
1483 break;
1485 default:
1486 break;
1491 Says whether given item type collides, i.e. stops player from moving.
1493 uint8_t SFG_itemCollides(uint8_t elementType)
1495 return
1496 elementType == SFG_LEVEL_ELEMENT_BARREL ||
1497 elementType == SFG_LEVEL_ELEMENT_TREE ||
1498 elementType == SFG_LEVEL_ELEMENT_TERMINAL ||
1499 elementType == SFG_LEVEL_ELEMENT_COLUMN ||
1500 elementType == SFG_LEVEL_ELEMENT_RUIN ||
1501 elementType == SFG_LEVEL_ELEMENT_BLOCKER ||
1502 elementType == SFG_LEVEL_ELEMENT_LAMP;
1505 void SFG_setGameState(uint8_t state)
1507 SFG_LOG("changing game state");
1508 SFG_game.state = state;
1509 SFG_game.stateTime = 0;
1512 void SFG_setAndInitLevel(uint8_t levelNumber)
1514 SFG_LOG("setting and initializing level");
1516 const SFG_Level *level;
1518 #if SFG_AVR
1519 memcpy_P(&SFG_ramLevel,SFG_levels[levelNumber],sizeof(SFG_Level));
1520 level = &SFG_ramLevel;
1521 #else
1522 level = SFG_levels[levelNumber];
1523 #endif
1525 SFG_game.currentRandom = 0;
1527 if (SFG_game.saved != SFG_CANT_SAVE)
1528 SFG_game.saved = 0;
1530 SFG_currentLevel.levelNumber = levelNumber;
1531 SFG_currentLevel.monstersDead = 0;
1532 SFG_currentLevel.backgroundImage = level->backgroundImage;
1533 SFG_currentLevel.levelPointer = level;
1534 SFG_currentLevel.bossCount = 0;
1535 SFG_currentLevel.floorColor = level->floorColor;
1536 SFG_currentLevel.ceilingColor = level->ceilingColor;
1537 SFG_currentLevel.completionTime10sOfS = 0;
1539 for (uint8_t i = 0; i < 7; ++i)
1540 SFG_currentLevel.textures[i] =
1541 SFG_wallTextures + level->textureIndices[i] * SFG_TEXTURE_STORE_SIZE;
1543 SFG_LOG("initializing doors");
1545 SFG_currentLevel.checkedDoorIndex = 0;
1546 SFG_currentLevel.doorRecordCount = 0;
1547 SFG_currentLevel.projectileRecordCount = 0;
1548 SFG_currentLevel.teleporterCount = 0;
1549 SFG_currentLevel.mapRevealMask =
1550 #if SFG_REVEAL_MAP
1551 0xffff;
1552 #else
1554 #endif
1556 for (uint8_t j = 0; j < SFG_MAP_SIZE; ++j)
1558 for (uint8_t i = 0; i < SFG_MAP_SIZE; ++i)
1560 uint8_t properties;
1562 SFG_getMapTile(level,i,j,&properties);
1564 if ((properties & SFG_TILE_PROPERTY_MASK) == SFG_TILE_PROPERTY_DOOR)
1566 SFG_DoorRecord *d =
1567 &(SFG_currentLevel.doorRecords[SFG_currentLevel.doorRecordCount]);
1569 d->coords[0] = i;
1570 d->coords[1] = j;
1571 d->state = 0x00;
1573 SFG_currentLevel.doorRecordCount++;
1576 if (SFG_currentLevel.doorRecordCount >= SFG_MAX_DOORS)
1578 SFG_LOG("warning: too many doors!");
1579 break;
1583 if (SFG_currentLevel.doorRecordCount >= SFG_MAX_DOORS)
1584 break;
1587 SFG_LOG("initializing level elements");
1589 SFG_currentLevel.itemRecordCount = 0;
1590 SFG_currentLevel.checkedItemIndex = 0;
1592 SFG_currentLevel.monsterRecordCount = 0;
1593 SFG_currentLevel.checkedMonsterIndex = 0;
1595 SFG_MonsterRecord *monster;
1597 for (uint16_t i = 0; i < ((SFG_MAP_SIZE * SFG_MAP_SIZE) / 8); ++i)
1598 SFG_currentLevel.itemCollisionMap[i] = 0;
1600 for (uint8_t i = 0; i < SFG_MAX_LEVEL_ELEMENTS; ++i)
1602 const SFG_LevelElement *e = &(SFG_currentLevel.levelPointer->elements[i]);
1604 if (e->type != SFG_LEVEL_ELEMENT_NONE)
1606 if (SFG_LEVEL_ELEMENT_TYPE_IS_MOSTER(e->type))
1608 monster =
1609 &(SFG_currentLevel.monsterRecords[SFG_currentLevel.monsterRecordCount]);
1611 monster->stateType = (SFG_MONSTER_TYPE_TO_INDEX(e->type) << 4)
1612 | SFG_MONSTER_STATE_INACTIVE;
1614 monster->health =
1615 SFG_GET_MONSTER_MAX_HEALTH(SFG_MONSTER_TYPE_TO_INDEX(e->type));
1617 monster->coords[0] = e->coords[0] * 4 + 2;
1618 monster->coords[1] = e->coords[1] * 4 + 2;
1620 SFG_currentLevel.monsterRecordCount++;
1622 if (e->type == SFG_LEVEL_ELEMENT_MONSTER_ENDER)
1623 SFG_currentLevel.bossCount++;
1625 else if ((e->type < SFG_LEVEL_ELEMENT_LOCK0) ||
1626 (e->type > SFG_LEVEL_ELEMENT_LOCK2))
1628 SFG_currentLevel.itemRecords[SFG_currentLevel.itemRecordCount] = i;
1629 SFG_currentLevel.itemRecordCount++;
1631 if (e->type == SFG_LEVEL_ELEMENT_TELEPORTER)
1632 SFG_currentLevel.teleporterCount++;
1634 if (SFG_itemCollides(e->type))
1635 SFG_setItemCollisionMapBit(e->coords[0],e->coords[1],1);
1637 else
1639 uint8_t properties;
1641 SFG_getMapTile(level,e->coords[0],e->coords[1],&properties);
1643 if ((properties & SFG_TILE_PROPERTY_MASK) == SFG_TILE_PROPERTY_DOOR)
1645 // find the door record and lock the door:
1646 for (uint16_t j = 0; j < SFG_currentLevel.doorRecordCount; ++j)
1648 SFG_DoorRecord *d = &(SFG_currentLevel.doorRecords[j]);
1650 if (d->coords[0] == e->coords[0] && d->coords[1] == e->coords[1])
1652 d->state |= (e->type - SFG_LEVEL_ELEMENT_LOCK0 + 1) << 6;
1653 break;
1657 else
1659 SFG_LOG("warning: lock not put on door tile!");
1665 SFG_currentLevel.timeStart = SFG_game.frameTime;
1666 SFG_currentLevel.frameStart = SFG_game.frame;
1668 SFG_game.spriteAnimationFrame = 0;
1670 SFG_initPlayer();
1671 SFG_setGameState(SFG_GAME_STATE_LEVEL_START);
1672 SFG_setMusic(SFG_MUSIC_NEXT);
1673 SFG_processEvent(SFG_EVENT_LEVEL_STARTS,levelNumber);
1676 void SFG_createDefaultSaveData(uint8_t *memory)
1678 for (uint16_t i = 0; i < SFG_SAVE_SIZE; ++i)
1679 memory[i] = 0;
1681 memory[1] = SFG_DEFAULT_SETTINGS;
1684 void SFG_init()
1686 SFG_LOG("initializing game")
1688 SFG_game.frame = 0;
1689 SFG_game.frameTime = 0;
1690 SFG_game.currentRandom = 0;
1691 SFG_game.cheatState = 0;
1692 SFG_game.continues = 1;
1694 RCL_initRayConstraints(&SFG_game.rayConstraints);
1695 SFG_game.rayConstraints.maxHits = SFG_RAYCASTING_MAX_HITS;
1696 SFG_game.rayConstraints.maxSteps = SFG_RAYCASTING_MAX_STEPS;
1698 RCL_initRayConstraints(&SFG_game.visibilityRayConstraints);
1699 SFG_game.visibilityRayConstraints.maxHits =
1700 SFG_RAYCASTING_VISIBILITY_MAX_HITS;
1701 SFG_game.visibilityRayConstraints.maxSteps =
1702 SFG_RAYCASTING_VISIBILITY_MAX_STEPS;
1704 SFG_game.antiSpam = 0;
1706 SFG_LOG("computing average texture colors")
1708 for (uint8_t i = 0; i < SFG_WALL_TEXTURE_COUNT; ++i)
1710 /** For simplicity, we round colors so that there is only 64 of them, and
1711 we count them up to 256. */
1713 uint8_t colorHistogram[64];
1715 for (uint8_t j = 0; j < 64; ++j)
1716 colorHistogram[j] = 0;
1718 for (uint8_t y = 0; y < SFG_TEXTURE_SIZE; ++y)
1719 for (uint8_t x = 0; x < SFG_TEXTURE_SIZE; ++x)
1721 uint8_t color =
1722 SFG_getTexel(SFG_wallTextures + i * SFG_TEXTURE_STORE_SIZE,x,y) / 4;
1724 colorHistogram[color] += 1;
1726 if (colorHistogram[color] == 255)
1727 break;
1730 uint8_t maxIndex = 0;
1732 for (uint8_t j = 0; j < 64; ++j)
1734 if (colorHistogram[j] == 255)
1736 maxIndex = j;
1737 break;
1740 if (colorHistogram[j] > colorHistogram[maxIndex])
1741 maxIndex = j;
1744 SFG_game.textureAverageColors[i] = maxIndex * 4;
1747 for (uint16_t i = 0; i < SFG_GAME_RESOLUTION_Y; ++i)
1748 SFG_game.backgroundScaleMap[i] =
1749 (i * SFG_TEXTURE_SIZE) / SFG_GAME_RESOLUTION_Y;
1751 for (uint8_t i = 0; i < SFG_KEY_COUNT; ++i)
1752 SFG_game.keyStates[i] = 0;
1754 SFG_currentLevel.levelPointer = 0;
1755 SFG_game.backgroundScroll = 0;
1756 SFG_game.selectedMenuItem = 0;
1757 SFG_game.selectedLevel = 0;
1758 SFG_game.settings = SFG_DEFAULT_SETTINGS;
1759 SFG_game.saved = 0;
1761 SFG_createDefaultSaveData(SFG_game.save);
1763 SFG_gameLoad(); // attempt to load settings
1765 if (SFG_game.saved != SFG_CANT_SAVE)
1767 SFG_LOG("settings loaded");
1768 SFG_game.settings = SFG_game.save[1];
1770 else
1772 SFG_LOG("saving/loading not possible");
1773 SFG_game.save[0] = SFG_NUMBER_OF_LEVELS - 1; // revealed all levels
1776 #if SFG_ALL_LEVELS
1777 SFG_game.save[0] = SFG_NUMBER_OF_LEVELS - 1;
1778 #endif
1780 SFG_setMusic((SFG_game.settings & 0x02) ?
1781 SFG_MUSIC_TURN_ON : SFG_MUSIC_TURN_OFF);
1783 #if SFG_START_LEVEL == 0
1784 SFG_setGameState(SFG_GAME_STATE_INIT);
1785 #else
1786 SFG_setAndInitLevel(SFG_START_LEVEL - 1);
1787 #endif
1791 Adds new projectile to the current level, returns 1 if added, 0 if not (max
1792 count reached).
1794 uint8_t SFG_createProjectile(SFG_ProjectileRecord projectile)
1796 if (SFG_currentLevel.projectileRecordCount >= SFG_MAX_PROJECTILES)
1797 return 0;
1799 SFG_currentLevel.projectileRecords[SFG_currentLevel.projectileRecordCount] =
1800 projectile;
1802 SFG_currentLevel.projectileRecordCount++;
1804 return 1;
1808 Launches projectile of given type from given position in given direction
1809 (has to be normalized), with given offset (so as to not collide with the
1810 shooting entity). Returns the same value as SFG_createProjectile.
1812 uint8_t SFG_launchProjectile(
1813 uint8_t type,
1814 RCL_Vector2D shootFrom,
1815 RCL_Unit shootFromHeight,
1816 RCL_Vector2D direction,
1817 RCL_Unit verticalSpeed,
1818 RCL_Unit offsetDistance
1821 if (type == SFG_PROJECTILE_NONE)
1822 return 0;
1824 SFG_ProjectileRecord p;
1826 p.type = type;
1827 p.doubleFramesToLive =
1828 RCL_nonZero(SFG_GET_PROJECTILE_FRAMES_TO_LIVE(type) / 2);
1830 p.position[0] =
1831 shootFrom.x + (direction.x * offsetDistance) / RCL_UNITS_PER_SQUARE;
1832 p.position[1] =
1833 shootFrom.y + (direction.y * offsetDistance) / RCL_UNITS_PER_SQUARE;
1834 p.position[2] = shootFromHeight;
1836 p.direction[0] =
1837 (direction.x * SFG_GET_PROJECTILE_SPEED_UPS(type)) / RCL_UNITS_PER_SQUARE;
1838 p.direction[1] =
1839 (direction.y * SFG_GET_PROJECTILE_SPEED_UPS(type)) / RCL_UNITS_PER_SQUARE;
1840 p.direction[2] = verticalSpeed;
1842 return SFG_createProjectile(p);
1846 Pushes a given position away from a center by given distance, with collisions.
1847 Returns 1 if push away happened, otherwise 0.
1849 uint8_t SFG_pushAway(
1850 RCL_Unit pos[3],
1851 RCL_Unit centerX,
1852 RCL_Unit centerY,
1853 RCL_Unit preferredDirection,
1854 RCL_Unit distance)
1856 RCL_Vector2D fromCenter;
1858 fromCenter.x = pos[0] - centerX;
1859 fromCenter.y = pos[1] - centerY;
1861 RCL_Unit l = RCL_len(fromCenter);
1863 if (l < 128)
1865 fromCenter = RCL_angleToDirection(preferredDirection);
1866 l = RCL_UNITS_PER_SQUARE;
1869 RCL_Vector2D offset;
1871 offset.x = (fromCenter.x * distance) / l;
1872 offset.y = (fromCenter.y * distance) / l;
1874 RCL_Camera c;
1876 RCL_initCamera(&c);
1878 c.position.x = pos[0];
1879 c.position.y = pos[1];
1880 c.height = pos[2];
1882 RCL_moveCameraWithCollision(&c,offset,0,SFG_floorCollisionHeightAt,
1883 SFG_ceilingHeightAt,1,1);
1885 pos[0] = c.position.x;
1886 pos[1] = c.position.y;
1887 pos[2] = c.height;
1889 return 1;
1892 uint8_t SFG_pushPlayerAway(
1893 RCL_Unit centerX, RCL_Unit centerY, RCL_Unit distance)
1895 RCL_Unit p[3];
1897 p[0] = SFG_player.camera.position.x;
1898 p[1] = SFG_player.camera.position.y;
1899 p[2] = SFG_player.camera.height;
1901 uint8_t result = SFG_pushAway(p,centerX,centerY,
1902 SFG_player.camera.direction - RCL_UNITS_PER_SQUARE / 2,
1903 distance);
1905 SFG_player.camera.position.x = p[0];
1906 SFG_player.camera.position.y = p[1];
1907 SFG_player.camera.height = p[2];
1909 return result;
1913 Helper function to resolve collision with level element. The function supposes
1914 the collision already does happen and only resolves it. Returns adjusted move
1915 offset.
1917 RCL_Vector2D SFG_resolveCollisionWithElement(
1918 RCL_Vector2D position, RCL_Vector2D moveOffset, RCL_Vector2D elementPos)
1920 RCL_Unit dx = RCL_abs(elementPos.x - position.x);
1921 RCL_Unit dy = RCL_abs(elementPos.y - position.y);
1923 if (dx > dy)
1925 // colliding from left/right
1927 if ((moveOffset.x > 0) == (position.x < elementPos.x))
1928 moveOffset.x = 0;
1929 // ^ only stop if heading towards element, to avoid getting stuck
1931 else
1933 // colliding from up/down
1935 if ((moveOffset.y > 0) == (position.y < elementPos.y))
1936 moveOffset.y = 0;
1939 return moveOffset;
1943 Adds or substracts player's health during the playing state due to taking
1944 damage (negative value) or getting healed. Negative value will be corrected by
1945 SFG_PLAYER_DAMAGE_MULTIPLIER in this function.
1947 void SFG_playerChangeHealth(int8_t healthAdd)
1949 if (SFG_game.state != SFG_GAME_STATE_PLAYING)
1950 return; // don't hurt during level starting phase
1952 if (healthAdd < 0)
1954 if (SFG_game.cheatState & 0x80) // invincible?
1955 return;
1957 healthAdd =
1958 RCL_min(-1,
1959 (((RCL_Unit) healthAdd) * SFG_PLAYER_DAMAGE_MULTIPLIER) /
1960 RCL_UNITS_PER_SQUARE);
1962 SFG_player.lastHurtFrame = SFG_game.frame;
1963 SFG_processEvent(SFG_EVENT_VIBRATE,0);
1964 SFG_processEvent(SFG_EVENT_PLAYER_HURT,-1 * healthAdd);
1967 int16_t health = SFG_player.health;
1968 health += healthAdd;
1969 health = RCL_clamp(health,0,SFG_PLAYER_MAX_HEALTH);
1971 SFG_player.health = health;
1974 uint8_t SFG_distantSoundVolume(RCL_Unit x, RCL_Unit y, RCL_Unit z)
1976 RCL_Unit distance = SFG_taxicabDistance(x,y,z,
1977 SFG_player.camera.position.x,
1978 SFG_player.camera.position.y,
1979 SFG_player.camera.height);
1981 if (distance >= SFG_SFX_MAX_DISTANCE)
1982 return 0;
1984 uint32_t result = 255 - (distance * 255) / SFG_SFX_MAX_DISTANCE;
1986 return (result * result) / 256;
1990 Same as SFG_playerChangeHealth but for monsters.
1992 void SFG_monsterChangeHealth(SFG_MonsterRecord *monster, int8_t healthAdd)
1994 int16_t health = monster->health;
1996 health += healthAdd;
1997 health = RCL_clamp(health,0,255);
1998 monster->health = health;
2000 if (healthAdd < 0)
2002 // play hurt sound
2004 uint8_t volume = SFG_distantSoundVolume(
2005 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]),
2006 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]),
2007 SFG_floorHeightAt(
2008 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]),
2009 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1])));
2011 SFG_playGameSound(5,volume);
2013 if (monster->health == 0)
2014 SFG_playGameSound(2,volume);
2018 void SFG_removeItem(uint8_t index)
2020 SFG_LOG("removing item");
2022 for (uint16_t j = index; j < SFG_currentLevel.itemRecordCount - 1; ++j)
2023 SFG_currentLevel.itemRecords[j] =
2024 SFG_currentLevel.itemRecords[j + 1];
2026 SFG_currentLevel.itemRecordCount--;
2030 Checks a 3D point visibility from player's position (WITHOUT considering
2031 facing direction).
2033 static inline uint8_t SFG_spriteIsVisible(RCL_Vector2D pos, RCL_Unit height)
2035 return
2036 RCL_castRay3D(
2037 SFG_player.camera.position,
2038 SFG_player.camera.height,
2039 pos,
2040 height,
2041 SFG_floorHeightAt,
2042 SFG_ceilingHeightAt,
2043 SFG_game.visibilityRayConstraints
2044 ) == RCL_UNITS_PER_SQUARE;
2047 RCL_Unit SFG_directionTangent(RCL_Unit dirX, RCL_Unit dirY, RCL_Unit dirZ)
2049 RCL_Vector2D v;
2051 v.x = dirX;
2052 v.y = dirY;
2054 return (dirZ * RCL_UNITS_PER_SQUARE) / RCL_len(v);
2058 Returns a tangent in RCL_Unit of vertical autoaim, given current game state.
2060 RCL_Unit SFG_autoaimVertically()
2062 for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
2064 SFG_MonsterRecord m = SFG_currentLevel.monsterRecords[i];
2066 uint8_t state = SFG_MR_STATE(m);
2068 if (state == SFG_MONSTER_STATE_INACTIVE ||
2069 state == SFG_MONSTER_STATE_DEAD)
2070 continue;
2072 RCL_Vector2D worldPosition, toMonster;
2074 worldPosition.x = SFG_MONSTER_COORD_TO_RCL_UNITS(m.coords[0]);
2075 worldPosition.y = SFG_MONSTER_COORD_TO_RCL_UNITS(m.coords[1]);
2077 toMonster.x = worldPosition.x - SFG_player.camera.position.x;
2078 toMonster.y = worldPosition.y - SFG_player.camera.position.y;
2080 if (RCL_abs(
2081 RCL_vectorsAngleCos(SFG_player.direction,toMonster)
2082 - RCL_UNITS_PER_SQUARE) < SFG_VERTICAL_AUTOAIM_ANGLE_THRESHOLD)
2084 uint8_t spriteSize = SFG_GET_MONSTER_SPRITE_SIZE(
2085 SFG_MONSTER_TYPE_TO_INDEX(SFG_MR_TYPE(m)));
2087 RCL_Unit worldHeight =
2088 SFG_floorHeightAt(
2089 SFG_MONSTER_COORD_TO_SQUARES(m.coords[0]),
2090 SFG_MONSTER_COORD_TO_SQUARES(m.coords[1]))
2092 SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(spriteSize);
2094 if (SFG_spriteIsVisible(worldPosition,worldHeight))
2095 return SFG_directionTangent(toMonster.x,toMonster.y,
2096 worldHeight - (SFG_player.camera.height));
2100 return 0;
2104 Helper function, returns a pointer to level element representing item with
2105 given index, but only if the item is active (otherwise 0 is returned).
2107 static inline const SFG_LevelElement *SFG_getActiveItemElement(uint8_t index)
2109 SFG_ItemRecord item = SFG_currentLevel.itemRecords[index];
2111 if ((item & SFG_ITEM_RECORD_ACTIVE_MASK) == 0)
2112 return 0;
2114 return &(SFG_currentLevel.levelPointer->elements[item &
2115 ~SFG_ITEM_RECORD_ACTIVE_MASK]);
2118 static inline const SFG_LevelElement *SFG_getLevelElement(uint8_t index)
2120 SFG_ItemRecord item = SFG_currentLevel.itemRecords[index];
2122 return &(SFG_currentLevel.levelPointer->elements[item &
2123 ~SFG_ITEM_RECORD_ACTIVE_MASK]);
2126 void SFG_createExplosion(RCL_Unit, RCL_Unit, RCL_Unit); // forward decl
2128 void SFG_explodeBarrel(uint8_t itemIndex, RCL_Unit x, RCL_Unit y, RCL_Unit z)
2130 const SFG_LevelElement *e = SFG_getLevelElement(itemIndex);
2131 SFG_setItemCollisionMapBit(e->coords[0],e->coords[1],0);
2132 SFG_removeItem(itemIndex);
2133 SFG_createExplosion(x,y,z);
2136 void SFG_createExplosion(RCL_Unit x, RCL_Unit y, RCL_Unit z)
2138 SFG_ProjectileRecord explosion;
2140 SFG_playGameSound(2,SFG_distantSoundVolume(x,y,z));
2141 SFG_processEvent(SFG_EVENT_EXPLOSION,0);
2143 explosion.type = SFG_PROJECTILE_EXPLOSION;
2145 explosion.position[0] = x;
2146 explosion.position[1] = y;
2147 explosion.position[2] = z;
2149 explosion.direction[0] = 0;
2150 explosion.direction[1] = 0;
2151 explosion.direction[2] = 0;
2153 explosion.doubleFramesToLive = RCL_nonZero(
2154 SFG_GET_PROJECTILE_FRAMES_TO_LIVE(SFG_PROJECTILE_EXPLOSION) / 2);
2156 SFG_createProjectile(explosion);
2158 uint8_t damage = SFG_getDamageValue(SFG_WEAPON_FIRE_TYPE_FIREBALL);
2160 if (SFG_taxicabDistance(x,y,z,SFG_player.camera.position.x,
2161 SFG_player.camera.position.y,SFG_player.camera.height)
2162 <= SFG_EXPLOSION_RADIUS)
2164 SFG_playerChangeHealth(-1 * damage);
2165 SFG_pushPlayerAway(x,y,SFG_EXPLOSION_PUSH_AWAY_DISTANCE);
2168 for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
2170 SFG_MonsterRecord *monster = &(SFG_currentLevel.monsterRecords[i]);
2172 uint16_t state = SFG_MR_STATE(*monster);
2174 if ((state == SFG_MONSTER_STATE_INACTIVE) ||
2175 (state == SFG_MONSTER_STATE_DEAD))
2176 continue;
2178 RCL_Unit monsterHeight =
2179 SFG_floorHeightAt(
2180 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]),
2181 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1]))
2182 + RCL_UNITS_PER_SQUARE / 2;
2184 if (SFG_taxicabDistance(
2185 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]),
2186 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]),monsterHeight,
2187 x,y,z) <= SFG_EXPLOSION_RADIUS)
2189 SFG_monsterChangeHealth(monster,
2190 -1 * SFG_getDamageValue(SFG_WEAPON_FIRE_TYPE_FIREBALL));
2194 // explode nearby barrels
2196 if (damage >= SFG_BARREL_EXPLOSION_DAMAGE_THRESHOLD)
2197 for (uint16_t i = 0; i < SFG_currentLevel.itemRecordCount; ++i)
2199 SFG_ItemRecord item = SFG_currentLevel.itemRecords[i];
2201 /* We DON'T check just active barrels but all, otherwise it looks weird
2202 that out of sight barrels in a line didn't explode.*/
2204 SFG_LevelElement element = SFG_ITEM_RECORD_LEVEL_ELEMENT(item);
2206 if (element.type != SFG_LEVEL_ELEMENT_BARREL)
2207 continue;
2209 RCL_Unit elementX =
2210 element.coords[0] * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2;
2212 RCL_Unit elementY =
2213 element.coords[1] * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2;
2215 RCL_Unit elementHeight =
2216 SFG_floorHeightAt(element.coords[0],element.coords[1]);
2218 if (SFG_taxicabDistance(
2219 x,y,z,elementX,elementY,elementHeight) <= SFG_EXPLOSION_RADIUS)
2221 SFG_explodeBarrel(i,elementX,elementY,elementHeight);
2222 i--;
2227 void SFG_createDust(RCL_Unit x, RCL_Unit y, RCL_Unit z)
2229 SFG_ProjectileRecord dust;
2231 dust.type = SFG_PROJECTILE_DUST;
2233 dust.position[0] = x;
2234 dust.position[1] = y;
2235 dust.position[2] = z;
2237 dust.direction[0] = 0;
2238 dust.direction[1] = 0;
2239 dust.direction[2] = 0;
2241 dust.doubleFramesToLive =
2242 RCL_nonZero(SFG_GET_PROJECTILE_FRAMES_TO_LIVE(SFG_PROJECTILE_DUST) / 2);
2244 SFG_createProjectile(dust);
2247 void SFG_getMonsterWorldPosition(SFG_MonsterRecord *monster, RCL_Unit *x,
2248 RCL_Unit *y, RCL_Unit *z)
2250 *x = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]);
2251 *y = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]);
2252 *z = SFG_floorHeightAt(
2253 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]),
2254 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1]))
2255 + RCL_UNITS_PER_SQUARE / 2;
2258 void SFG_monsterPerformAI(SFG_MonsterRecord *monster)
2260 uint8_t state = SFG_MR_STATE(*monster);
2261 uint8_t type = SFG_MR_TYPE(*monster);
2262 uint8_t monsterNumber = SFG_MONSTER_TYPE_TO_INDEX(type);
2263 uint8_t attackType = SFG_GET_MONSTER_ATTACK_TYPE(monsterNumber);
2265 int8_t coordAdd[2];
2267 coordAdd[0] = 0;
2268 coordAdd[1] = 0;
2270 uint8_t notRanged =
2271 (attackType == SFG_MONSTER_ATTACK_MELEE) ||
2272 (attackType == SFG_MONSTER_ATTACK_EXPLODE);
2274 uint8_t monsterSquare[2];
2275 /* because of some insanely retarded C++ compilers that error on narrowing
2276 conversion between { } we init this way: */
2277 monsterSquare[0] = SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]);
2278 monsterSquare[1] = SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1]);
2280 RCL_Unit currentHeight =
2281 SFG_floorCollisionHeightAt(monsterSquare[0],monsterSquare[1]);
2283 if ( // ranged monsters: sometimes randomly attack
2284 !notRanged &&
2285 (SFG_random() <
2286 SFG_GET_MONSTER_AGGRESSIVITY(SFG_MONSTER_TYPE_TO_INDEX(type)))
2289 RCL_Vector2D pos;
2290 pos.x = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]);
2291 pos.y = SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]);
2293 if (SFG_random() % 4 != 0 &&
2294 SFG_spriteIsVisible(pos,currentHeight + // only if player is visible
2295 SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(
2296 SFG_GET_MONSTER_SPRITE_SIZE(
2297 SFG_MONSTER_TYPE_TO_INDEX(type)))))
2299 // ranged attack
2301 state = SFG_MONSTER_STATE_ATTACKING;
2303 RCL_Vector2D dir;
2305 dir.x = SFG_player.camera.position.x - pos.x
2306 - 128 * SFG_MONSTER_AIM_RANDOMNESS +
2307 SFG_random() * SFG_MONSTER_AIM_RANDOMNESS;
2309 dir.y = SFG_player.camera.position.y - pos.y
2310 - 128 * SFG_MONSTER_AIM_RANDOMNESS +
2311 SFG_random() * SFG_MONSTER_AIM_RANDOMNESS;
2313 uint8_t projectile;
2315 switch (SFG_GET_MONSTER_ATTACK_TYPE(monsterNumber))
2317 case SFG_MONSTER_ATTACK_FIREBALL:
2318 projectile = SFG_PROJECTILE_FIREBALL;
2319 break;
2321 case SFG_MONSTER_ATTACK_BULLET:
2322 projectile = SFG_PROJECTILE_BULLET;
2323 break;
2325 case SFG_MONSTER_ATTACK_PLASMA:
2326 projectile = SFG_PROJECTILE_PLASMA;
2327 break;
2329 case SFG_MONSTER_ATTACK_FIREBALL_BULLET:
2330 projectile = (SFG_random() < 128) ?
2331 SFG_PROJECTILE_FIREBALL :
2332 SFG_PROJECTILE_BULLET;
2333 break;
2335 case SFG_MONSTER_ATTACK_FIREBALL_PLASMA:
2336 projectile = (SFG_random() < 128) ?
2337 SFG_PROJECTILE_FIREBALL :
2338 SFG_PROJECTILE_PLASMA;
2339 break;
2341 default:
2342 projectile = SFG_PROJECTILE_NONE;
2343 break;
2346 if (projectile == SFG_PROJECTILE_BULLET)
2347 SFG_playGameSound(0,
2348 SFG_distantSoundVolume(
2349 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]),
2350 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]),
2351 currentHeight)
2354 RCL_Unit middleHeight = currentHeight +
2355 SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(SFG_GET_MONSTER_SPRITE_SIZE(
2356 SFG_MONSTER_TYPE_TO_INDEX(SFG_MR_TYPE(*monster))));
2358 RCL_Unit verticalSpeed = (
2359 ((projectile != SFG_PROJECTILE_NONE) ?
2360 SFG_GET_PROJECTILE_SPEED_UPS(projectile) : 0) *
2361 SFG_directionTangent(dir.x,dir.y,SFG_player.camera.height -
2362 middleHeight)) / RCL_UNITS_PER_SQUARE;
2364 dir = RCL_normalize(dir);
2366 SFG_launchProjectile(
2367 projectile,
2368 pos,
2369 middleHeight,
2370 dir,
2371 verticalSpeed,
2372 SFG_PROJECTILE_SPAWN_OFFSET
2374 } // if visible
2375 else
2376 state = SFG_MONSTER_STATE_IDLE;
2378 else if (state == SFG_MONSTER_STATE_IDLE)
2380 if (notRanged)
2382 // non-ranged monsters: walk towards player
2384 RCL_Unit pX, pY, pZ;
2385 SFG_getMonsterWorldPosition(monster,&pX,&pY,&pZ);
2387 uint8_t isClose = // close to player?
2388 SFG_taxicabDistance(pX,pY,pZ,
2389 SFG_player.camera.position.x,
2390 SFG_player.camera.position.y,
2391 SFG_player.camera.height) <= SFG_MELEE_RANGE;
2393 if (!isClose)
2395 // walk towards player
2397 if (monsterSquare[0] > SFG_player.squarePosition[0])
2399 if (monsterSquare[1] > SFG_player.squarePosition[1])
2400 state = SFG_MONSTER_STATE_GOING_NW;
2401 else if (monsterSquare[1] < SFG_player.squarePosition[1])
2402 state = SFG_MONSTER_STATE_GOING_SW;
2403 else
2404 state = SFG_MONSTER_STATE_GOING_W;
2406 else if (monsterSquare[0] < SFG_player.squarePosition[0])
2408 if (monsterSquare[1] > SFG_player.squarePosition[1])
2409 state = SFG_MONSTER_STATE_GOING_NE;
2410 else if (monsterSquare[1] < SFG_player.squarePosition[1])
2411 state = SFG_MONSTER_STATE_GOING_SE;
2412 else
2413 state = SFG_MONSTER_STATE_GOING_E;
2415 else
2417 if (monsterSquare[1] > SFG_player.squarePosition[1])
2418 state = SFG_MONSTER_STATE_GOING_N;
2419 else if (monsterSquare[1] < SFG_player.squarePosition[1])
2420 state = SFG_MONSTER_STATE_GOING_S;
2423 else // is close
2425 // melee, close-up attack
2427 if (attackType == SFG_MONSTER_ATTACK_MELEE)
2429 // melee attack
2431 state = SFG_MONSTER_STATE_ATTACKING;
2433 SFG_playerChangeHealth(
2434 -1 * SFG_getDamageValue(SFG_WEAPON_FIRE_TYPE_MELEE));
2436 SFG_playGameSound(3,255);
2438 else // SFG_MONSTER_ATTACK_EXPLODE
2440 // explode
2442 SFG_createExplosion(pX,pY,pZ);
2443 monster->health = 0;
2447 else // ranged monsters
2449 // choose walk direction randomly
2451 switch (SFG_random() % 8)
2453 case 0: state = SFG_MONSTER_STATE_GOING_E; break;
2454 case 1: state = SFG_MONSTER_STATE_GOING_W; break;
2455 case 2: state = SFG_MONSTER_STATE_GOING_N; break;
2456 case 3: state = SFG_MONSTER_STATE_GOING_S; break;
2457 case 4: state = SFG_MONSTER_STATE_GOING_NE; break;
2458 case 5: state = SFG_MONSTER_STATE_GOING_NW; break;
2459 case 6: state = SFG_MONSTER_STATE_GOING_SE; break;
2460 case 7: state = SFG_MONSTER_STATE_GOING_SW; break;
2461 default: break;
2465 else if (state == SFG_MONSTER_STATE_ATTACKING)
2467 state = SFG_MONSTER_STATE_IDLE;
2469 else
2471 int8_t add = 1;
2473 if (attackType == SFG_MONSTER_ATTACK_MELEE)
2474 add = 2;
2475 else if (attackType == SFG_MONSTER_ATTACK_EXPLODE)
2476 add = 3;
2478 if (state == SFG_MONSTER_STATE_GOING_E ||
2479 state == SFG_MONSTER_STATE_GOING_NE ||
2480 state == SFG_MONSTER_STATE_GOING_SE)
2481 coordAdd[0] = add;
2482 else if (state == SFG_MONSTER_STATE_GOING_W ||
2483 state == SFG_MONSTER_STATE_GOING_SW ||
2484 state == SFG_MONSTER_STATE_GOING_NW)
2485 coordAdd[0] = -1 * add;
2487 if (state == SFG_MONSTER_STATE_GOING_N ||
2488 state == SFG_MONSTER_STATE_GOING_NE ||
2489 state == SFG_MONSTER_STATE_GOING_NW)
2490 coordAdd[1] = -1 * add;
2491 else if (state == SFG_MONSTER_STATE_GOING_S ||
2492 state == SFG_MONSTER_STATE_GOING_SE ||
2493 state == SFG_MONSTER_STATE_GOING_SW)
2494 coordAdd[1] = add;
2496 if ((coordAdd[0] != 0 || coordAdd[1] != 0) && SFG_random() <
2497 SFG_MONSTER_SOUND_PROBABILITY)
2498 SFG_playGameSound(5,
2499 SFG_distantSoundVolume(
2500 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]),
2501 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]),
2502 currentHeight) / 2);
2504 state = SFG_MONSTER_STATE_IDLE;
2507 int16_t newPos[2];
2509 newPos[0] = monster->coords[0] + coordAdd[0];
2510 newPos[1] = monster->coords[1] + coordAdd[1];
2512 int8_t collision = 0;
2514 if (newPos[0] < 0 || newPos[0] >= 256 || newPos[1] < 0 || newPos[1] >= 256)
2516 collision = 1;
2518 else
2520 uint8_t movingDiagonally = (coordAdd[0] != 0) && (coordAdd[1] != 0);
2522 // when moving diagonally, we need to check extra tiles
2524 for (uint8_t i = 0; i < (1 + movingDiagonally); ++i)
2526 newPos[0] = monster->coords[0] + (i != 1) * coordAdd[0];
2528 RCL_Unit newHeight =
2529 SFG_floorCollisionHeightAt(
2530 SFG_MONSTER_COORD_TO_SQUARES(newPos[0]),
2531 SFG_MONSTER_COORD_TO_SQUARES(newPos[1]));
2533 collision =
2534 RCL_abs(currentHeight - newHeight) > RCL_CAMERA_COLL_STEP_HEIGHT;
2536 if (!collision)
2537 collision = (SFG_ceilingHeightAt(
2538 SFG_MONSTER_COORD_TO_SQUARES(newPos[0]),
2539 SFG_MONSTER_COORD_TO_SQUARES(newPos[1])) - newHeight) <
2540 SFG_MONSTER_COLLISION_HEIGHT;
2542 if (collision)
2543 break;
2546 newPos[0] = monster->coords[0] + coordAdd[0];
2549 if (collision)
2551 state = SFG_MONSTER_STATE_IDLE;
2552 // ^ will force the monster to choose random direction in the next update
2554 newPos[0] = monster->coords[0];
2555 newPos[1] = monster->coords[1];
2558 monster->stateType = state | (monsterNumber << 4);
2559 monster->coords[0] = newPos[0];
2560 monster->coords[1] = newPos[1];;
2563 static inline uint8_t SFG_elementCollides(
2564 RCL_Unit pointX,
2565 RCL_Unit pointY,
2566 RCL_Unit pointZ,
2567 RCL_Unit elementX,
2568 RCL_Unit elementY,
2569 RCL_Unit elementHeight
2572 return
2573 SFG_taxicabDistance(pointX,pointY,pointZ,elementX,elementY,elementHeight)
2574 <= SFG_ELEMENT_COLLISION_RADIUS;
2578 Checks collision of a projectile with level element at given position.
2580 uint8_t SFG_projectileCollides(SFG_ProjectileRecord *projectile,
2581 RCL_Unit x, RCL_Unit y, RCL_Unit z)
2583 if (!SFG_elementCollides(x,y,z,
2584 projectile->position[0],projectile->position[1],projectile->position[2]))
2585 return 0;
2587 if ((projectile->type == SFG_PROJECTILE_EXPLOSION) ||
2588 (projectile->type == SFG_PROJECTILE_DUST))
2589 return 0;
2591 /* For directional projectiles we only register a collision if its direction
2592 is "towards" the element so that the shooter doesn't get shot by his own
2593 projectile. */
2595 RCL_Vector2D projDir, toElement;
2597 projDir.x = projectile->direction[0];
2598 projDir.y = projectile->direction[1];
2600 toElement.x = x - projectile->position[0];
2601 toElement.y = y - projectile->position[1];
2603 return RCL_vectorsAngleCos(projDir,toElement) >= 0;
2607 Updates a frame of the currently loaded level, i.e. enemies, projectiles,
2608 animations etc., with the exception of player.
2610 void SFG_updateLevel()
2612 // update projectiles:
2614 uint8_t substractFrames =
2615 ((SFG_game.frame - SFG_currentLevel.frameStart) & 0x01) ? 1 : 0;
2616 /* ^ only substract frames to live every other frame because a maximum of
2617 256 frames would be too few */
2619 for (int8_t i = 0; i < SFG_currentLevel.projectileRecordCount; ++i)
2620 { // ^ has to be signed
2621 SFG_ProjectileRecord *p = &(SFG_currentLevel.projectileRecords[i]);
2623 uint8_t attackType = 255;
2625 if (p->type == SFG_PROJECTILE_BULLET)
2626 attackType = SFG_WEAPON_FIRE_TYPE_BULLET;
2627 else if (p->type == SFG_PROJECTILE_PLASMA)
2628 attackType = SFG_WEAPON_FIRE_TYPE_PLASMA;
2630 RCL_Unit pos[3] = {0,0,0}; /* we have to convert from uint16_t because of
2631 under/overflows */
2632 uint8_t eliminate = 0;
2634 for (uint8_t j = 0; j < 3; ++j)
2636 pos[j] = p->position[j];
2637 pos[j] += p->direction[j];
2639 if ( // projectile outside map?
2640 (pos[j] < 0) ||
2641 (pos[j] >= (SFG_MAP_SIZE * RCL_UNITS_PER_SQUARE)))
2643 eliminate = 1;
2644 break;
2648 if (p->doubleFramesToLive == 0) // no more time to live?
2650 eliminate = 1;
2652 else if (
2653 (p->type != SFG_PROJECTILE_EXPLOSION) &&
2654 (p->type != SFG_PROJECTILE_DUST))
2656 if (SFG_projectileCollides( // collides with player?
2658 SFG_player.camera.position.x,
2659 SFG_player.camera.position.y,
2660 SFG_player.camera.height))
2662 eliminate = 1;
2664 SFG_playerChangeHealth(-1 * SFG_getDamageValue(attackType));
2667 /* Check collision with the map (we don't use SFG_floorCollisionHeightAt
2668 because collisions with items have to be done differently for
2669 projectiles). */
2671 if (!eliminate &&
2672 ((SFG_floorHeightAt(pos[0] / RCL_UNITS_PER_SQUARE,pos[1] /
2673 RCL_UNITS_PER_SQUARE) >= pos[2])
2675 (SFG_ceilingHeightAt(pos[0] / RCL_UNITS_PER_SQUARE,pos[1] /
2676 RCL_UNITS_PER_SQUARE) <= pos[2]))
2678 eliminate = 1;
2680 // check collision with active level elements
2682 if (!eliminate) // monsters
2683 for (uint16_t j = 0; j < SFG_currentLevel.monsterRecordCount; ++j)
2685 SFG_MonsterRecord *m = &(SFG_currentLevel.monsterRecords[j]);
2687 uint8_t state = SFG_MR_STATE(*m);
2689 if ((state != SFG_MONSTER_STATE_INACTIVE) &&
2690 (state != SFG_MONSTER_STATE_DEAD))
2692 if (SFG_projectileCollides(p,
2693 SFG_MONSTER_COORD_TO_RCL_UNITS(m->coords[0]),
2694 SFG_MONSTER_COORD_TO_RCL_UNITS(m->coords[1]),
2695 SFG_floorHeightAt(
2696 SFG_MONSTER_COORD_TO_SQUARES(m->coords[0]),
2697 SFG_MONSTER_COORD_TO_SQUARES(m->coords[1]))
2700 eliminate = 1;
2701 SFG_monsterChangeHealth(m,-1 * SFG_getDamageValue(attackType));
2702 break;
2707 if (!eliminate) // items (can't check itemCollisionMap because of barrels)
2708 for (uint16_t j = 0; j < SFG_currentLevel.itemRecordCount; ++j)
2710 const SFG_LevelElement *e = SFG_getActiveItemElement(j);
2712 if (e != 0 && SFG_itemCollides(e->type))
2714 RCL_Unit x = SFG_ELEMENT_COORD_TO_RCL_UNITS(e->coords[0]);
2715 RCL_Unit y = SFG_ELEMENT_COORD_TO_RCL_UNITS(e->coords[1]);
2716 RCL_Unit z = SFG_floorHeightAt(e->coords[0],e->coords[1]);
2718 if (SFG_projectileCollides(p,x,y,z))
2720 if (
2721 (e->type == SFG_LEVEL_ELEMENT_BARREL) &&
2722 (SFG_getDamageValue(attackType) >=
2723 SFG_BARREL_EXPLOSION_DAMAGE_THRESHOLD)
2726 SFG_explodeBarrel(j,x,y,z);
2729 eliminate = 1;
2730 break;
2736 if (eliminate)
2738 if (p->type == SFG_PROJECTILE_FIREBALL)
2739 SFG_createExplosion(p->position[0],p->position[1],p->position[2]);
2740 else if (p->type == SFG_PROJECTILE_BULLET)
2741 SFG_createDust(p->position[0],p->position[1],p->position[2]);
2742 else if (p->type == SFG_PROJECTILE_PLASMA)
2743 SFG_playGameSound(4,SFG_distantSoundVolume(pos[0],pos[1],pos[2]));
2745 // remove the projectile
2747 for (uint8_t j = i; j < SFG_currentLevel.projectileRecordCount - 1; ++j)
2748 SFG_currentLevel.projectileRecords[j] =
2749 SFG_currentLevel.projectileRecords[j + 1];
2751 SFG_currentLevel.projectileRecordCount--;
2753 i--;
2755 else
2757 p->position[0] = pos[0];
2758 p->position[1] = pos[1];
2759 p->position[2] = pos[2];
2762 p->doubleFramesToLive -= substractFrames;
2765 // handle door:
2766 if (SFG_currentLevel.doorRecordCount > 0) // has to be here
2768 /* Check door on whether a player is standing nearby. For performance
2769 reasons we only check a few doors and move to others in the next
2770 frame. */
2772 if (SFG_currentLevel.checkedDoorIndex == 0)
2774 uint8_t count = SFG_player.cards >> 6;
2776 SFG_player.cards = (count <= 1) ?
2777 (SFG_player.cards & 0x07) :
2778 ((SFG_player.cards & 0x7f) | ((count - 1) << 6));
2781 for (uint16_t i = 0;
2782 i < RCL_min(SFG_ELEMENT_DISTANCES_CHECKED_PER_FRAME,
2783 SFG_currentLevel.doorRecordCount);
2784 ++i)
2786 SFG_DoorRecord *door =
2787 &(SFG_currentLevel.doorRecords[SFG_currentLevel.checkedDoorIndex]);
2789 uint8_t upDownState = door->state & SFG_DOOR_UP_DOWN_MASK;
2791 uint8_t newUpDownState = 0;
2793 uint8_t lock = SFG_DOOR_LOCK(door->state);
2795 if ( // player near door?
2796 (door->coords[0] >= (SFG_player.squarePosition[0] - 1)) &&
2797 (door->coords[0] <= (SFG_player.squarePosition[0] + 1)) &&
2798 (door->coords[1] >= (SFG_player.squarePosition[1] - 1)) &&
2799 (door->coords[1] <= (SFG_player.squarePosition[1] + 1)))
2801 if (lock == 0)
2803 newUpDownState = SFG_DOOR_UP_DOWN_MASK;
2805 else
2807 lock = 1 << (lock - 1);
2809 if (SFG_player.cards & lock) // player has the card?
2810 newUpDownState = SFG_DOOR_UP_DOWN_MASK;
2811 else
2812 SFG_player.cards =
2813 (SFG_player.cards & 0x07) | (lock << 3) | (2 << 6);
2817 if (upDownState != newUpDownState)
2818 SFG_playGameSound(1,255);
2820 door->state = (door->state & ~SFG_DOOR_UP_DOWN_MASK) | newUpDownState;
2822 SFG_currentLevel.checkedDoorIndex++;
2824 if (SFG_currentLevel.checkedDoorIndex >= SFG_currentLevel.doorRecordCount)
2825 SFG_currentLevel.checkedDoorIndex = 0;
2828 // move door up/down:
2829 for (uint32_t i = 0; i < SFG_currentLevel.doorRecordCount; ++i)
2831 SFG_DoorRecord *door = &(SFG_currentLevel.doorRecords[i]);
2833 int8_t height = door->state & SFG_DOOR_VERTICAL_POSITION_MASK;
2835 height = (door->state & SFG_DOOR_UP_DOWN_MASK) ?
2836 RCL_min(0x1f,height + SFG_DOOR_INCREMENT_PER_FRAME) :
2837 RCL_max(0x00,height - SFG_DOOR_INCREMENT_PER_FRAME);
2839 door->state = (door->state & ~SFG_DOOR_VERTICAL_POSITION_MASK) | height;
2843 // handle items, in a similar manner to door:
2844 if (SFG_currentLevel.itemRecordCount > 0) // has to be here
2846 // check item distances:
2848 for (uint16_t i = 0;
2849 i < RCL_min(SFG_ELEMENT_DISTANCES_CHECKED_PER_FRAME,
2850 SFG_currentLevel.itemRecordCount);
2851 ++i)
2853 SFG_ItemRecord item =
2854 SFG_currentLevel.itemRecords[SFG_currentLevel.checkedItemIndex];
2856 item &= ~SFG_ITEM_RECORD_ACTIVE_MASK;
2858 SFG_LevelElement e =
2859 SFG_currentLevel.levelPointer->elements[item];
2861 if (
2862 SFG_isInActiveDistanceFromPlayer(
2863 e.coords[0] * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2,
2864 e.coords[1] * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2,
2865 SFG_floorHeightAt(e.coords[0],e.coords[1]) + RCL_UNITS_PER_SQUARE / 2)
2867 item |= SFG_ITEM_RECORD_ACTIVE_MASK;
2869 SFG_currentLevel.itemRecords[SFG_currentLevel.checkedItemIndex] = item;
2871 SFG_currentLevel.checkedItemIndex++;
2873 if (SFG_currentLevel.checkedItemIndex >= SFG_currentLevel.itemRecordCount)
2874 SFG_currentLevel.checkedItemIndex = 0;
2878 // similarly handle monsters:
2879 if (SFG_currentLevel.monsterRecordCount > 0) // has to be here
2881 // check monster distances:
2883 for (uint16_t i = 0;
2884 i < RCL_min(SFG_ELEMENT_DISTANCES_CHECKED_PER_FRAME,
2885 SFG_currentLevel.monsterRecordCount);
2886 ++i)
2888 SFG_MonsterRecord *monster =
2889 &(SFG_currentLevel.monsterRecords[SFG_currentLevel.checkedMonsterIndex]);
2891 if ( // far away from the player?
2892 !SFG_isInActiveDistanceFromPlayer(
2893 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]),
2894 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]),
2895 SFG_floorHeightAt(
2896 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]),
2897 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[1]))
2898 + RCL_UNITS_PER_SQUARE / 2
2902 monster->stateType =
2903 (monster->stateType & SFG_MONSTER_MASK_TYPE) |
2904 SFG_MONSTER_STATE_INACTIVE;
2906 else if (SFG_MR_STATE(*monster) == SFG_MONSTER_STATE_INACTIVE)
2908 monster->stateType =
2909 (monster->stateType & SFG_MONSTER_MASK_TYPE) |
2910 (monster->health != 0 ?
2911 SFG_MONSTER_STATE_IDLE : SFG_MONSTER_STATE_DEAD);
2914 SFG_currentLevel.checkedMonsterIndex++;
2916 if (SFG_currentLevel.checkedMonsterIndex >=
2917 SFG_currentLevel.monsterRecordCount)
2918 SFG_currentLevel.checkedMonsterIndex = 0;
2922 // update AI and handle dead monsters:
2923 if ((SFG_game.frame - SFG_currentLevel.frameStart) %
2924 SFG_AI_UPDATE_FRAME_INTERVAL == 0)
2926 for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
2928 SFG_MonsterRecord *monster = &(SFG_currentLevel.monsterRecords[i]);
2929 uint8_t state = SFG_MR_STATE(*monster);
2931 if ((state == SFG_MONSTER_STATE_INACTIVE) ||
2932 (state == SFG_MONSTER_STATE_DEAD))
2933 continue;
2935 if (state == SFG_MONSTER_STATE_DYING)
2937 monster->stateType =
2938 (monster->stateType & 0xf0) | SFG_MONSTER_STATE_DEAD;
2940 else if (monster->health == 0)
2942 monster->stateType = (monster->stateType & SFG_MONSTER_MASK_TYPE) |
2943 SFG_MONSTER_STATE_DYING;
2945 if (SFG_MR_TYPE(*monster) == SFG_LEVEL_ELEMENT_MONSTER_ENDER)
2947 SFG_currentLevel.bossCount--;
2949 // last boss killed gives player a key card
2951 if (SFG_currentLevel.bossCount == 0)
2953 SFG_LOG("boss killed, giving player a card");
2954 SFG_player.cards |= 0x04;
2958 SFG_processEvent(SFG_EVENT_MONSTER_DIES,SFG_MR_TYPE(*monster));
2960 if (SFG_MR_TYPE(*monster) == SFG_LEVEL_ELEMENT_MONSTER_EXPLODER)
2961 SFG_createExplosion(
2962 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[0]),
2963 SFG_MONSTER_COORD_TO_RCL_UNITS(monster->coords[1]),
2964 SFG_floorCollisionHeightAt(
2965 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0]),
2966 SFG_MONSTER_COORD_TO_SQUARES(monster->coords[0])) +
2967 RCL_UNITS_PER_SQUARE / 2);
2969 else
2971 #if SFG_PREVIEW_MODE == 0
2972 SFG_monsterPerformAI(monster);
2973 #endif
2980 Maps square position on the map to a bit in map reveal mask.
2982 static inline uint16_t SFG_getMapRevealBit(uint8_t squareX, uint8_t squareY)
2984 return 1 << ((squareY / 16) * 4 + squareX / 16);
2988 Draws text on screen using the bitmap font stored in assets.
2990 void SFG_drawText(
2991 const char *text,
2992 uint16_t x,
2993 uint16_t y,
2994 uint8_t size,
2995 uint8_t color,
2996 uint16_t maxLength,
2997 uint16_t limitX)
2999 if (size == 0)
3000 size = 1;
3002 if (limitX == 0)
3003 limitX = 65535;
3005 if (maxLength == 0)
3006 maxLength = 65535;
3008 uint16_t pos = 0;
3010 uint16_t currentX = x;
3011 uint16_t currentY = y;
3013 while (pos < maxLength && text[pos] != 0) // for each character
3015 uint16_t character =
3016 //SFG_PROGRAM_MEMORY_U8(SFG_font + SFG_charToFontIndex(text[pos]));
3017 SFG_font[SFG_charToFontIndex(text[pos])];
3019 for (uint8_t i = 0; i < SFG_FONT_CHARACTER_SIZE; ++i) // for each line
3021 currentY = y;
3023 for (uint8_t j = 0; j < SFG_FONT_CHARACTER_SIZE; ++j) // for each row
3025 if (character & 0x8000)
3026 for (uint8_t k = 0; k < size; ++k)
3027 for (uint8_t l = 0; l < size; ++l)
3029 uint16_t drawX = currentX + k;
3030 uint16_t drawY = currentY + l;
3032 if (drawX < SFG_GAME_RESOLUTION_X &&
3033 drawY < SFG_GAME_RESOLUTION_Y)
3034 SFG_setGamePixel(drawX,drawY,color);
3037 currentY += size;
3038 character = character << 1;
3041 currentX += size;
3044 currentX += size; // space
3046 if (currentX > limitX)
3048 currentX = x;
3049 y += (SFG_FONT_CHARACTER_SIZE + 1) * size;
3052 pos++;
3056 void SFG_drawLevelStartOverlay()
3058 uint8_t stage = (SFG_game.stateTime * 4) / SFG_LEVEL_START_DURATION;
3060 // fade in:
3062 for (uint16_t y = 0; y < SFG_GAME_RESOLUTION_Y; ++y)
3063 for (uint16_t x = 0; x < SFG_GAME_RESOLUTION_X; ++x)
3065 uint8_t draw = 0;
3067 switch (stage)
3069 case 0: draw = 1; break;
3070 case 1: draw = (x % 2) || (y % 2); break;
3071 case 2: draw = (x % 2) == (y % 2); break;
3072 case 3: draw = (x % 2) && (y % 2); break;
3073 default: break;
3076 if (draw)
3077 SFG_setGamePixel(x,y,0);
3080 if (SFG_game.saved == 1)
3081 SFG_drawText(SFG_TEXT_SAVED,SFG_HUD_MARGIN,SFG_HUD_MARGIN,
3082 SFG_FONT_SIZE_MEDIUM,7,255,0);
3086 Sets player's height to match the floor height below him.
3088 void SFG_updatePlayerHeight()
3090 SFG_player.camera.height =
3091 SFG_floorCollisionHeightAt(
3092 SFG_player.squarePosition[0],SFG_player.squarePosition[1]) +
3093 RCL_CAMERA_COLL_HEIGHT_BELOW;
3096 void SFG_winLevel()
3098 SFG_levelEnds();
3099 SFG_setGameState(SFG_GAME_STATE_WIN);
3100 SFG_playGameSound(2,255);
3101 SFG_processEvent(SFG_EVENT_VIBRATE,0);
3102 SFG_processEvent(SFG_EVENT_LEVEL_WON,SFG_currentLevel.levelNumber + 1);
3106 Part of SFG_gameStep() for SFG_GAME_STATE_PLAYING.
3108 void SFG_gameStepPlaying()
3110 #if SFG_QUICK_WIN
3111 if (SFG_game.stateTime > 500)
3112 SFG_winLevel();
3113 #endif
3115 if (
3116 (SFG_keyIsDown(SFG_KEY_C) && SFG_keyIsDown(SFG_KEY_DOWN)) ||
3117 SFG_keyIsDown(SFG_KEY_MENU))
3119 SFG_setGameState(SFG_GAME_STATE_MENU);
3120 SFG_playGameSound(3,SFG_MENU_CLICK_VOLUME);
3121 return;
3124 SFG_updateLevel();
3126 int8_t recomputeDirection = SFG_currentLevel.frameStart == SFG_game.frame;
3128 RCL_Vector2D moveOffset;
3130 moveOffset.x = 0;
3131 moveOffset.y = 0;
3133 int8_t strafe = 0;
3135 uint8_t currentWeapon = SFG_player.weapon;
3137 #if SFG_HEADBOB_ENABLED
3138 int8_t bobbing = 0;
3139 #endif
3141 int8_t shearing = 0;
3143 if (SFG_player.weaponCooldownFrames > 0)
3144 SFG_player.weaponCooldownFrames--;
3146 if (SFG_keyJustPressed(SFG_KEY_TOGGLE_FREELOOK))
3147 SFG_game.settings = (SFG_game.settings & 0x04) ?
3148 (SFG_game.settings & ~0x0c) : (SFG_game.settings | 0x0c);
3150 int8_t canSwitchWeapon = SFG_player.weaponCooldownFrames == 0;
3152 if (SFG_keyJustPressed(SFG_KEY_NEXT_WEAPON) && canSwitchWeapon)
3153 SFG_playerRotateWeapon(1);
3154 else if (SFG_keyJustPressed(SFG_KEY_PREVIOUS_WEAPON) && canSwitchWeapon)
3155 SFG_playerRotateWeapon(0);
3156 else if (SFG_keyJustPressed(SFG_KEY_CYCLE_WEAPON) &&
3157 SFG_player.previousWeaponDirection)
3158 SFG_playerRotateWeapon(SFG_player.previousWeaponDirection > 0);
3160 uint8_t shearingOn = SFG_game.settings & 0x04;
3162 if (SFG_keyIsDown(SFG_KEY_B))
3164 if (shearingOn) // B + U/D: shearing (if on)
3166 if (SFG_keyIsDown(SFG_KEY_UP))
3168 SFG_player.camera.shear =
3169 RCL_min(SFG_CAMERA_MAX_SHEAR_PIXELS,
3170 SFG_player.camera.shear + SFG_CAMERA_SHEAR_STEP_PER_FRAME);
3172 shearing = 1;
3174 else if (SFG_keyIsDown(SFG_KEY_DOWN))
3176 SFG_player.camera.shear =
3177 RCL_max(-1 * SFG_CAMERA_MAX_SHEAR_PIXELS,
3178 SFG_player.camera.shear - SFG_CAMERA_SHEAR_STEP_PER_FRAME);
3180 shearing = 1;
3184 if (!SFG_keyIsDown(SFG_KEY_C))
3185 { // B + L/R: strafing
3186 if (SFG_keyIsDown(SFG_KEY_LEFT))
3187 strafe = -1;
3188 else if (SFG_keyIsDown(SFG_KEY_RIGHT))
3189 strafe = 1;
3193 if (SFG_keyIsDown(SFG_KEY_C)) // C + A/B/L/R: weapon switching
3195 if ((SFG_keyJustPressed(SFG_KEY_LEFT) || SFG_keyJustPressed(SFG_KEY_A)) &&
3196 canSwitchWeapon)
3197 SFG_playerRotateWeapon(0);
3198 else if (
3199 (SFG_keyJustPressed(SFG_KEY_RIGHT) || SFG_keyJustPressed(SFG_KEY_B)) &&
3200 canSwitchWeapon)
3201 SFG_playerRotateWeapon(1);
3203 else if (!SFG_keyIsDown(SFG_KEY_B)) // L/R: turning
3205 if (SFG_keyIsDown(SFG_KEY_LEFT))
3207 SFG_player.camera.direction -= SFG_PLAYER_TURN_UNITS_PER_FRAME;
3208 recomputeDirection = 1;
3210 else if (SFG_keyIsDown(SFG_KEY_RIGHT))
3212 SFG_player.camera.direction += SFG_PLAYER_TURN_UNITS_PER_FRAME;
3213 recomputeDirection = 1;
3217 if (!SFG_keyIsDown(SFG_KEY_B) || !shearingOn) // U/D: movement
3219 if (SFG_keyIsDown(SFG_KEY_UP))
3221 moveOffset.x += SFG_player.direction.x;
3222 moveOffset.y += SFG_player.direction.y;
3223 #if SFG_HEADBOB_ENABLED
3224 bobbing = 1;
3225 #endif
3227 else if (SFG_keyIsDown(SFG_KEY_DOWN))
3229 moveOffset.x -= SFG_player.direction.x;
3230 moveOffset.y -= SFG_player.direction.y;
3231 #if SFG_HEADBOB_ENABLED
3232 bobbing = 1;
3233 #endif
3237 int16_t mouseX = 0, mouseY = 0;
3239 SFG_getMouseOffset(&mouseX,&mouseY);
3241 if (mouseX != 0) // mouse turning
3243 SFG_player.camera.direction +=
3244 (mouseX * SFG_MOUSE_SENSITIVITY_HORIZONTAL) / 128;
3246 recomputeDirection = 1;
3249 if ((mouseY != 0) && shearingOn) // mouse shearing
3250 SFG_player.camera.shear =
3251 RCL_max(RCL_min(
3252 SFG_player.camera.shear -
3253 (mouseY * SFG_MOUSE_SENSITIVITY_VERTICAL) / 128,
3254 SFG_CAMERA_MAX_SHEAR_PIXELS),
3255 -1 * SFG_CAMERA_MAX_SHEAR_PIXELS);
3257 if (recomputeDirection)
3258 SFG_recomputePLayerDirection();
3260 if (SFG_keyIsDown(SFG_KEY_STRAFE_LEFT))
3261 strafe = -1;
3262 else if (SFG_keyIsDown(SFG_KEY_STRAFE_RIGHT))
3263 strafe = 1;
3265 if (strafe != 0)
3267 uint8_t normalize = (moveOffset.x != 0) || (moveOffset.y != 0);
3269 moveOffset.x += strafe * SFG_player.direction.y;
3270 moveOffset.y -= strafe * SFG_player.direction.x;
3272 if (normalize)
3274 // This prevents reaching higher speed when moving diagonally.
3276 moveOffset = RCL_normalize(moveOffset);
3278 moveOffset.x = (moveOffset.x * SFG_PLAYER_MOVE_UNITS_PER_FRAME)
3279 / RCL_UNITS_PER_SQUARE;
3281 moveOffset.y = (moveOffset.y * SFG_PLAYER_MOVE_UNITS_PER_FRAME)
3282 / RCL_UNITS_PER_SQUARE;
3286 #if SFG_PREVIEW_MODE
3287 if (SFG_keyIsDown(SFG_KEY_B))
3288 SFG_player.verticalSpeed = SFG_PLAYER_MOVE_UNITS_PER_FRAME;
3289 else if (SFG_keyIsDown(SFG_KEY_C))
3290 SFG_player.verticalSpeed = -1 * SFG_PLAYER_MOVE_UNITS_PER_FRAME;
3291 else
3292 SFG_player.verticalSpeed = 0;
3293 #else
3294 RCL_Unit verticalOffset =
3297 SFG_keyIsDown(SFG_KEY_JUMP) ||
3298 (SFG_keyIsDown(SFG_KEY_UP) && SFG_keyIsDown(SFG_KEY_C))
3299 ) &&
3300 (SFG_player.verticalSpeed == 0) &&
3301 (SFG_player.previousVerticalSpeed == 0)) ?
3302 SFG_PLAYER_JUMP_OFFSET_PER_FRAME : // jump
3303 (SFG_player.verticalSpeed - SFG_GRAVITY_SPEED_INCREASE_PER_FRAME);
3304 #endif
3306 if (!shearing && SFG_player.camera.shear != 0 && !(SFG_game.settings & 0x08))
3308 // gradually shear back to zero
3310 SFG_player.camera.shear =
3311 (SFG_player.camera.shear > 0) ?
3312 RCL_max(0,SFG_player.camera.shear - SFG_CAMERA_SHEAR_STEP_PER_FRAME) :
3313 RCL_min(0,SFG_player.camera.shear + SFG_CAMERA_SHEAR_STEP_PER_FRAME);
3316 #if SFG_HEADBOB_ENABLED && !SFG_PREVIEW_MODE
3317 if (bobbing)
3319 SFG_player.headBobFrame += SFG_HEADBOB_FRAME_INCREASE_PER_FRAME;
3321 else if (SFG_player.headBobFrame != 0)
3323 // smoothly stop bobbing
3325 uint8_t quadrant = (SFG_player.headBobFrame % RCL_UNITS_PER_SQUARE) /
3326 (RCL_UNITS_PER_SQUARE / 4);
3328 /* When in quadrant in which sin is going away from zero, switch to the
3329 same value of the next quadrant, so that bobbing starts to go towards
3330 zero immediately. */
3332 if (quadrant % 2 == 0)
3333 SFG_player.headBobFrame =
3334 ((quadrant + 1) * RCL_UNITS_PER_SQUARE / 4) +
3335 (RCL_UNITS_PER_SQUARE / 4 - SFG_player.headBobFrame %
3336 (RCL_UNITS_PER_SQUARE / 4));
3338 RCL_Unit currentFrame = SFG_player.headBobFrame;
3339 RCL_Unit nextFrame = SFG_player.headBobFrame + 16;
3341 // only stop bobbing when we pass a frame at which sin crosses zero
3342 SFG_player.headBobFrame =
3343 (currentFrame / (RCL_UNITS_PER_SQUARE / 2) ==
3344 nextFrame / (RCL_UNITS_PER_SQUARE / 2)) ?
3345 nextFrame : 0;
3347 #endif
3349 RCL_Unit previousHeight = SFG_player.camera.height;
3351 // handle player collision with level elements:
3353 // monsters:
3354 for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
3356 SFG_MonsterRecord *m = &(SFG_currentLevel.monsterRecords[i]);
3358 uint8_t state = SFG_MR_STATE(*m);
3360 if (state == SFG_MONSTER_STATE_INACTIVE || state == SFG_MONSTER_STATE_DEAD)
3361 continue;
3363 RCL_Vector2D mPos;
3365 mPos.x = SFG_MONSTER_COORD_TO_RCL_UNITS(m->coords[0]);
3366 mPos.y = SFG_MONSTER_COORD_TO_RCL_UNITS(m->coords[1]);
3368 if (
3369 SFG_elementCollides(
3370 SFG_player.camera.position.x,
3371 SFG_player.camera.position.y,
3372 SFG_player.camera.height,
3373 mPos.x,
3374 mPos.y,
3375 SFG_floorHeightAt(
3376 SFG_MONSTER_COORD_TO_SQUARES(m->coords[0]),
3377 SFG_MONSTER_COORD_TO_SQUARES(m->coords[1]))
3381 moveOffset = SFG_resolveCollisionWithElement(
3382 SFG_player.camera.position,moveOffset,mPos);
3386 uint8_t collidesWithTeleporter = 0;
3388 /* item collisions with player (only those that don't stop player's movement,
3389 as those are handled differently, via itemCollisionMap): */
3390 for (int16_t i = 0; i < SFG_currentLevel.itemRecordCount; ++i)
3391 // ^ has to be int16_t (signed)
3393 if (!(SFG_currentLevel.itemRecords[i] & SFG_ITEM_RECORD_ACTIVE_MASK))
3394 continue;
3396 const SFG_LevelElement *e = SFG_getActiveItemElement(i);
3398 if (e != 0)
3400 RCL_Vector2D ePos;
3402 ePos.x = SFG_ELEMENT_COORD_TO_RCL_UNITS(e->coords[0]);
3403 ePos.y = SFG_ELEMENT_COORD_TO_RCL_UNITS(e->coords[1]);
3405 if (!SFG_itemCollides(e->type) &&
3406 SFG_elementCollides(
3407 SFG_player.camera.position.x,
3408 SFG_player.camera.position.y,
3409 SFG_player.camera.height,
3410 ePos.x,
3411 ePos.y,
3412 SFG_floorHeightAt(e->coords[0],e->coords[1]))
3415 uint8_t eliminate = 1;
3417 uint8_t onlyKnife = 1;
3419 for (uint8_t j = 0; j < SFG_AMMO_TOTAL; ++j)
3420 if (SFG_player.ammo[j] != 0)
3422 onlyKnife = 0;
3423 break;
3426 switch (e->type)
3428 case SFG_LEVEL_ELEMENT_HEALTH:
3429 if (SFG_player.health < SFG_PLAYER_MAX_HEALTH)
3430 SFG_playerChangeHealth(SFG_HEALTH_KIT_VALUE);
3431 else
3432 eliminate = 0;
3433 break;
3435 #define addAmmo(type) \
3436 if (SFG_player.ammo[SFG_AMMO_##type] < SFG_AMMO_MAX_##type) \
3438 SFG_player.ammo[SFG_AMMO_##type] = RCL_min(SFG_AMMO_MAX_##type,\
3439 SFG_player.ammo[SFG_AMMO_##type] + SFG_AMMO_INCREASE_##type);\
3440 if (onlyKnife) SFG_playerRotateWeapon(1); \
3442 else\
3443 eliminate = 0;
3445 case SFG_LEVEL_ELEMENT_BULLETS:
3446 addAmmo(BULLETS)
3447 break;
3449 case SFG_LEVEL_ELEMENT_ROCKETS:
3450 addAmmo(ROCKETS)
3451 break;
3453 case SFG_LEVEL_ELEMENT_PLASMA:
3454 addAmmo(PLASMA)
3455 break;
3457 #undef addAmmo
3459 case SFG_LEVEL_ELEMENT_CARD0:
3460 case SFG_LEVEL_ELEMENT_CARD1:
3461 case SFG_LEVEL_ELEMENT_CARD2:
3462 SFG_player.cards |= 1 << (e->type - SFG_LEVEL_ELEMENT_CARD0);
3463 break;
3465 case SFG_LEVEL_ELEMENT_TELEPORTER:
3466 collidesWithTeleporter = 1;
3467 eliminate = 0;
3468 break;
3470 case SFG_LEVEL_ELEMENT_FINISH:
3471 SFG_winLevel();
3472 eliminate = 0;
3473 break;
3475 default:
3476 eliminate = 0;
3477 break;
3480 if (eliminate) // take the item
3482 #if !SFG_PREVIEW_MODE
3483 SFG_removeItem(i);
3484 SFG_player.lastItemTakenFrame = SFG_game.frame;
3485 i--;
3486 SFG_playGameSound(3,255);
3487 SFG_processEvent(SFG_EVENT_PLAYER_TAKES_ITEM,e->type);
3488 #endif
3490 else if (
3491 e->type == SFG_LEVEL_ELEMENT_TELEPORTER &&
3492 SFG_currentLevel.teleporterCount > 1 &&
3493 !SFG_player.justTeleported)
3495 // teleport to random destination teleporter
3497 uint8_t teleporterNumber =
3498 SFG_random() % (SFG_currentLevel.teleporterCount - 1) + 1;
3500 for (uint16_t j = 0; j < SFG_currentLevel.itemRecordCount; ++j)
3502 SFG_LevelElement e2 =
3503 SFG_currentLevel.levelPointer->elements
3504 [SFG_currentLevel.itemRecords[j] &
3505 ~SFG_ITEM_RECORD_ACTIVE_MASK];
3507 if ((e2.type == SFG_LEVEL_ELEMENT_TELEPORTER) && (j != i))
3508 teleporterNumber--;
3510 if (teleporterNumber == 0)
3512 SFG_player.camera.position.x =
3513 SFG_ELEMENT_COORD_TO_RCL_UNITS(e2.coords[0]);
3515 SFG_player.camera.position.y =
3516 SFG_ELEMENT_COORD_TO_RCL_UNITS(e2.coords[1]);
3518 SFG_player.camera.height =
3519 SFG_floorHeightAt(e2.coords[0],e2.coords[1]) +
3520 RCL_CAMERA_COLL_HEIGHT_BELOW;
3522 SFG_currentLevel.itemRecords[j] |= SFG_ITEM_RECORD_ACTIVE_MASK;
3523 /* ^ we have to make the new teleporter immediately active so
3524 that it will immediately collide */
3526 SFG_player.justTeleported = 1;
3528 SFG_playGameSound(4,255);
3529 SFG_processEvent(SFG_EVENT_PLAYER_TELEPORTS,0);
3531 break;
3532 } // if teleporterNumber == 0
3533 } // for level items
3534 } // if eliminate
3535 } // if item collides
3536 } // if element != 0
3537 } // for, item collision check
3539 if (!collidesWithTeleporter)
3540 SFG_player.justTeleported = 0;
3542 #if SFG_PREVIEW_MODE
3543 SFG_player.camera.position.x +=
3544 SFG_PREVIEW_MODE_SPEED_MULTIPLIER * moveOffset.x;
3546 SFG_player.camera.position.y +=
3547 SFG_PREVIEW_MODE_SPEED_MULTIPLIER * moveOffset.y;
3549 SFG_player.camera.height +=
3550 SFG_PREVIEW_MODE_SPEED_MULTIPLIER * SFG_player.verticalSpeed;
3551 #else
3552 RCL_moveCameraWithCollision(&(SFG_player.camera),moveOffset,
3553 verticalOffset,SFG_floorCollisionHeightAt,SFG_ceilingHeightAt,1,1);
3555 SFG_player.previousVerticalSpeed = SFG_player.verticalSpeed;
3557 RCL_Unit limit = RCL_max(RCL_max(0,verticalOffset),SFG_player.verticalSpeed);
3559 SFG_player.verticalSpeed =
3560 RCL_min(limit,SFG_player.camera.height - previousHeight);
3561 /* ^ By "limit" we assure height increase caused by climbing a step doesn't
3562 add vertical velocity. */
3563 #endif
3565 #if SFG_PREVIEW_MODE == 0
3566 if (
3567 SFG_keyIsDown(SFG_KEY_A) &&
3568 !SFG_keyIsDown(SFG_KEY_C) &&
3569 (SFG_player.weaponCooldownFrames == 0) &&
3570 (SFG_game.stateTime > 400) // don't immediately shoot if returning from menu
3573 /* Player attack/shoot/fire, this has to be done AFTER the player is moved,
3574 otherwise he could shoot himself while running forward. */
3576 uint8_t ammo, projectileCount, canShoot;
3578 SFG_getPlayerWeaponInfo(&ammo,&projectileCount,&canShoot);
3580 if (canShoot)
3582 uint8_t sound;
3584 switch (SFG_player.weapon)
3586 case SFG_WEAPON_KNIFE: sound = 255; break;
3587 case SFG_WEAPON_ROCKET_LAUNCHER:
3588 case SFG_WEAPON_SHOTGUN: sound = 2; break;
3589 case SFG_WEAPON_PLASMAGUN:
3590 case SFG_WEAPON_SOLUTION: sound = 4; break;
3591 default: sound = 0; break;
3594 if (sound != 255)
3595 SFG_playGameSound(sound,255);
3597 if (ammo != SFG_AMMO_NONE)
3598 SFG_player.ammo[ammo] -= projectileCount;
3600 uint8_t projectile;
3602 switch (SFG_GET_WEAPON_FIRE_TYPE(SFG_player.weapon))
3604 case SFG_WEAPON_FIRE_TYPE_PLASMA:
3605 projectile = SFG_PROJECTILE_PLASMA;
3606 break;
3608 case SFG_WEAPON_FIRE_TYPE_FIREBALL:
3609 projectile = SFG_PROJECTILE_FIREBALL;
3610 break;
3612 case SFG_WEAPON_FIRE_TYPE_BULLET:
3613 projectile = SFG_PROJECTILE_BULLET;
3614 break;
3616 case SFG_WEAPON_FIRE_TYPE_MELEE:
3617 projectile = SFG_PROJECTILE_NONE;
3618 break;
3620 default:
3621 projectile = 255;
3622 break;
3625 if (projectile != SFG_PROJECTILE_NONE)
3627 uint16_t angleAdd = SFG_PROJECTILE_SPREAD_ANGLE / (projectileCount + 1);
3629 RCL_Unit direction =
3630 (SFG_player.camera.direction - SFG_PROJECTILE_SPREAD_ANGLE / 2)
3631 + angleAdd;
3633 RCL_Unit projectileSpeed = SFG_GET_PROJECTILE_SPEED_UPS(projectile);
3635 /* Vertical speed will be either determined by autoaim (if shearing is
3636 off) or the camera shear value. */
3637 RCL_Unit verticalSpeed = (SFG_game.settings & 0x04) ?
3638 (SFG_player.camera.shear * projectileSpeed * 2) / // only approximate
3639 SFG_CAMERA_MAX_SHEAR_PIXELS
3641 (projectileSpeed * SFG_autoaimVertically()) / RCL_UNITS_PER_SQUARE;
3643 for (uint8_t i = 0; i < projectileCount; ++i)
3645 SFG_launchProjectile(
3646 projectile,
3647 SFG_player.camera.position,
3648 SFG_player.camera.height,
3649 RCL_angleToDirection(direction),
3650 verticalSpeed,
3651 SFG_PROJECTILE_SPAWN_OFFSET
3654 direction += angleAdd;
3657 else
3659 // player's melee attack
3661 for (uint16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
3663 SFG_MonsterRecord *m = &(SFG_currentLevel.monsterRecords[i]);
3665 uint8_t state = SFG_MR_STATE(*m);
3667 if ((state == SFG_MONSTER_STATE_INACTIVE) ||
3668 (state == SFG_MONSTER_STATE_DEAD))
3669 continue;
3671 RCL_Unit pX, pY, pZ;
3672 SFG_getMonsterWorldPosition(m,&pX,&pY,&pZ);
3674 if (SFG_taxicabDistance(pX,pY,pZ,
3675 SFG_player.camera.position.x,
3676 SFG_player.camera.position.y,
3677 SFG_player.camera.height) > SFG_MELEE_RANGE)
3678 continue;
3680 RCL_Vector2D toMonster;
3682 toMonster.x = pX - SFG_player.camera.position.x;
3683 toMonster.y = pY - SFG_player.camera.position.y;
3685 if (RCL_vectorsAngleCos(SFG_player.direction,toMonster) >=
3686 (RCL_UNITS_PER_SQUARE - SFG_PLAYER_MELEE_ANGLE))
3688 SFG_monsterChangeHealth(m,
3689 -1 * SFG_getDamageValue(SFG_WEAPON_FIRE_TYPE_MELEE));
3691 SFG_createDust(pX,pY,pZ);
3693 break;
3698 SFG_player.weaponCooldownFrames =
3699 RCL_max(
3700 SFG_GET_WEAPON_FIRE_COOLDOWN_FRAMES(SFG_player.weapon),
3701 SFG_MIN_WEAPON_COOLDOWN_FRAMES);
3703 SFG_getPlayerWeaponInfo(&ammo,&projectileCount,&canShoot);
3705 if (!canShoot)
3707 // No more ammo, switch to the second strongest weapon.
3709 SFG_playerRotateWeapon(1);
3711 uint8_t previousWeapon = SFG_player.weapon;
3713 SFG_playerRotateWeapon(0);
3715 if (previousWeapon > SFG_player.weapon)
3716 SFG_playerRotateWeapon(1);
3718 } // endif: has enough ammo?
3719 } // attack
3720 #endif // SFG_PREVIEW_MODE == 0
3722 SFG_player.squarePosition[0] =
3723 SFG_player.camera.position.x / RCL_UNITS_PER_SQUARE;
3725 SFG_player.squarePosition[1] =
3726 SFG_player.camera.position.y / RCL_UNITS_PER_SQUARE;
3728 SFG_currentLevel.mapRevealMask |=
3729 SFG_getMapRevealBit(
3730 SFG_player.squarePosition[0],
3731 SFG_player.squarePosition[1]);
3733 uint8_t properties;
3735 SFG_getMapTile(SFG_currentLevel.levelPointer,SFG_player.squarePosition[0],
3736 SFG_player.squarePosition[1],&properties);
3738 if ( // squeezer check
3739 (properties == SFG_TILE_PROPERTY_SQUEEZER) &&
3740 ((SFG_ceilingHeightAt(
3741 SFG_player.squarePosition[0],SFG_player.squarePosition[1]) -
3742 SFG_floorHeightAt(
3743 SFG_player.squarePosition[0],SFG_player.squarePosition[1]))
3745 (RCL_CAMERA_COLL_HEIGHT_ABOVE + RCL_CAMERA_COLL_HEIGHT_BELOW)))
3747 SFG_LOG("player is squeezed");
3748 SFG_player.health = 0;
3751 if (SFG_player.weapon != currentWeapon) // if weapon switched, start cooldown
3753 if (SFG_player.weapon == (currentWeapon + 1) % SFG_WEAPONS_TOTAL)
3754 SFG_player.previousWeaponDirection = -1;
3755 else if (currentWeapon == (SFG_player.weapon + 1) % SFG_WEAPONS_TOTAL)
3756 SFG_player.previousWeaponDirection = 1;
3757 else
3758 SFG_player.previousWeaponDirection = 0;
3760 SFG_player.weaponCooldownFrames =
3761 SFG_GET_WEAPON_FIRE_COOLDOWN_FRAMES(SFG_player.weapon) / 2;
3764 #if SFG_IMMORTAL == 0
3765 if (SFG_player.health == 0)
3767 SFG_LOG("player dies");
3768 SFG_levelEnds();
3769 SFG_processEvent(SFG_EVENT_VIBRATE,0);
3770 SFG_processEvent(SFG_EVENT_PLAYER_DIES,0);
3771 SFG_setGameState(SFG_GAME_STATE_LOSE);
3773 #endif
3777 This function defines which items are displayed in the menu.
3779 uint8_t SFG_getMenuItem(uint8_t index)
3781 uint8_t current = 0;
3783 while (1) // find first legitimate item
3785 if ( // skip non-legitimate items
3786 ((current <= SFG_MENU_ITEM_MAP) && (SFG_currentLevel.levelPointer == 0))
3787 || ((current == SFG_MENU_ITEM_LOAD) && ((SFG_game.save[0] >> 4) == 0)))
3789 current++;
3790 continue;
3793 if (index == 0)
3794 return (current <= (SFG_MENU_ITEM_EXIT - (SFG_CAN_EXIT ? 0 : 1))
3795 ) ? current : SFG_MENU_ITEM_NONE;
3797 current++;
3798 index--;
3801 return SFG_MENU_ITEM_NONE;
3804 void SFG_handleCheats()
3806 // this is a state machine handling cheat typing
3808 uint8_t expectedKey;
3810 switch (SFG_game.cheatState & 0x0f)
3812 case 0: case 3: case 5: case 7: case 10:
3813 expectedKey = SFG_KEY_A; break;
3814 case 1: case 8:
3815 expectedKey = SFG_KEY_B; break;
3816 case 2: case 9:
3817 expectedKey = SFG_KEY_RIGHT; break;
3818 case 4:
3819 expectedKey = SFG_KEY_C; break;
3820 case 6: default:
3821 expectedKey = SFG_KEY_DOWN; break;
3824 for (uint8_t i = 0; i < SFG_KEY_COUNT; ++i) // no other keys must be pressed
3825 if ((i != expectedKey) && SFG_keyJustPressed(i))
3827 SFG_game.cheatState &= 0xf0; // back to start state
3828 return;
3831 if (!SFG_keyJustPressed(expectedKey))
3832 return;
3834 SFG_game.cheatState++; // go to next state
3836 if ((SFG_game.cheatState & 0x0f) > 10) // final state resolved?
3838 if (SFG_game.cheatState & 0x80)
3840 SFG_LOG("cheat disabled");
3841 SFG_game.cheatState = 0;
3843 else
3845 SFG_LOG("cheat activated");
3846 SFG_playGameSound(4,255);
3847 SFG_playerChangeHealth(SFG_PLAYER_MAX_HEALTH);
3848 SFG_player.ammo[SFG_AMMO_BULLETS] = SFG_AMMO_MAX_BULLETS;
3849 SFG_player.ammo[SFG_AMMO_ROCKETS] = SFG_AMMO_MAX_ROCKETS;
3850 SFG_player.ammo[SFG_AMMO_PLASMA] = SFG_AMMO_MAX_PLASMA;
3851 SFG_player.weapon = SFG_WEAPON_SOLUTION;
3852 SFG_player.cards |= 0x07;
3853 SFG_game.cheatState = 0x80;
3858 void SFG_gameStepMenu()
3860 uint8_t menuItems = 0;
3862 while (SFG_getMenuItem(menuItems) != SFG_MENU_ITEM_NONE)
3863 menuItems++;
3865 uint8_t item = SFG_getMenuItem(SFG_game.selectedMenuItem);
3867 if (SFG_keyRegisters(SFG_KEY_DOWN) &&
3868 (SFG_game.selectedMenuItem < menuItems - 1))
3870 SFG_game.selectedMenuItem++;
3871 SFG_playGameSound(3,SFG_MENU_CLICK_VOLUME);
3873 else if (SFG_keyRegisters(SFG_KEY_UP) && (SFG_game.selectedMenuItem > 0))
3875 SFG_game.selectedMenuItem--;
3876 SFG_playGameSound(3,SFG_MENU_CLICK_VOLUME);
3878 else if (SFG_keyJustPressed(SFG_KEY_A))
3880 switch (item)
3882 case SFG_MENU_ITEM_PLAY:
3883 for (uint8_t i = 6; i < SFG_SAVE_SIZE; ++i) // reset totals in save
3884 SFG_game.save[i] = 0;
3886 if (SFG_game.selectedLevel == 0)
3888 SFG_currentLevel.levelNumber = 0; // to draw intro, not outro
3889 SFG_setGameState(SFG_GAME_STATE_INTRO);
3891 else
3892 SFG_setAndInitLevel(SFG_game.selectedLevel);
3894 break;
3896 case SFG_MENU_ITEM_LOAD:
3898 SFG_gameLoad();
3900 uint8_t saveBackup[SFG_SAVE_SIZE];
3902 for (uint8_t i = 0; i < SFG_SAVE_SIZE; ++i)
3903 saveBackup[i] = SFG_game.save[i];
3905 SFG_setAndInitLevel(SFG_game.save[0] >> 4);
3907 for (uint8_t i = 0; i < SFG_SAVE_SIZE; ++i)
3908 SFG_game.save[i] = saveBackup[i];
3910 SFG_player.health = SFG_game.save[2];
3911 SFG_player.ammo[0] = SFG_game.save[3];
3912 SFG_player.ammo[1] = SFG_game.save[4];
3913 SFG_player.ammo[2] = SFG_game.save[5];
3915 SFG_playerRotateWeapon(1); // this chooses weapon with ammo available
3916 break;
3919 case SFG_MENU_ITEM_CONTINUE:
3920 SFG_setGameState(SFG_GAME_STATE_PLAYING);
3921 break;
3923 case SFG_MENU_ITEM_MAP:
3924 SFG_setGameState(SFG_GAME_STATE_MAP);
3925 break;
3927 case SFG_MENU_ITEM_SOUND:
3928 SFG_LOG("sound changed");
3930 SFG_game.settings =
3931 (SFG_game.settings & ~0x03) | ((SFG_game.settings + 1) & 0x03);
3933 SFG_playGameSound(3,SFG_MENU_CLICK_VOLUME);
3935 if ((SFG_game.settings & 0x02) !=
3936 ((SFG_game.settings - 1) & 0x02))
3937 SFG_setMusic((SFG_game.settings & 0x02) ?
3938 SFG_MUSIC_TURN_ON : SFG_MUSIC_TURN_OFF);
3940 SFG_game.save[1] = SFG_game.settings;
3941 SFG_gameSave();
3943 break;
3945 case SFG_MENU_ITEM_SHEAR:
3947 uint8_t current = (SFG_game.settings >> 2) & 0x03;
3949 current++;
3951 if (current == 2) // option that doesn't make sense, skip
3952 current++;
3954 SFG_game.settings =
3955 (SFG_game.settings & ~0x0c) | ((current & 0x03) << 2);
3957 SFG_game.save[1] = SFG_game.settings;
3958 SFG_gameSave();
3960 break;
3963 case SFG_MENU_ITEM_EXIT:
3964 SFG_game.continues = 0;
3965 break;
3967 default:
3968 break;
3971 else if (item == SFG_MENU_ITEM_PLAY)
3973 if (SFG_keyRegisters(SFG_KEY_RIGHT) &&
3974 (SFG_game.selectedLevel < (SFG_game.save[0] & 0x0f)))
3976 SFG_game.selectedLevel++;
3977 SFG_playGameSound(3,SFG_MENU_CLICK_VOLUME);
3979 else if (SFG_keyRegisters(SFG_KEY_LEFT) && SFG_game.selectedLevel > 0)
3981 SFG_game.selectedLevel--;
3982 SFG_playGameSound(3,SFG_MENU_CLICK_VOLUME);
3988 Performs one game step (logic, physics, menu, ...), happening SFG_MS_PER_FRAME
3989 after the previous step.
3991 void SFG_gameStep()
3993 SFG_GAME_STEP_COMMAND
3995 SFG_game.soundsPlayedThisFrame = 0;
3997 SFG_game.blink = (SFG_game.frame / SFG_BLINK_PERIOD_FRAMES) % 2;
3999 for (uint8_t i = 0; i < SFG_KEY_COUNT; ++i)
4000 if (!SFG_keyPressed(i))
4001 SFG_game.keyStates[i] = 0;
4002 else if (SFG_game.keyStates[i] < 255)
4003 SFG_game.keyStates[i]++;
4005 if ((SFG_currentLevel.frameStart - SFG_game.frame) %
4006 SFG_SPRITE_ANIMATION_FRAME_DURATION == 0)
4007 SFG_game.spriteAnimationFrame++;
4009 switch (SFG_game.state)
4011 case SFG_GAME_STATE_PLAYING:
4012 SFG_handleCheats();
4013 SFG_gameStepPlaying();
4014 break;
4016 case SFG_GAME_STATE_MENU:
4017 SFG_gameStepMenu();
4018 break;
4020 case SFG_GAME_STATE_LOSE:
4022 // player die animation (lose)
4024 SFG_updateLevel(); // let monsters and other things continue moving
4025 SFG_updatePlayerHeight(); // in case player is on elevator
4027 int32_t t = SFG_game.stateTime;
4029 RCL_Unit h = SFG_floorHeightAt(SFG_player.squarePosition[0],
4030 SFG_player.squarePosition[1]);
4032 SFG_player.camera.height =
4033 RCL_max(h,h + ((SFG_LOSE_ANIMATION_DURATION - t) *
4034 RCL_CAMERA_COLL_HEIGHT_BELOW) / SFG_LOSE_ANIMATION_DURATION);
4036 SFG_player.camera.shear =
4037 RCL_min(SFG_CAMERA_MAX_SHEAR_PIXELS / 4,
4038 (t * (SFG_CAMERA_MAX_SHEAR_PIXELS / 4)) / SFG_LOSE_ANIMATION_DURATION);
4040 if (t > SFG_LOSE_ANIMATION_DURATION &&
4041 (SFG_keyIsDown(SFG_KEY_A) || SFG_keyIsDown(SFG_KEY_B)))
4043 for (uint8_t i = 6; i < SFG_SAVE_SIZE; ++i)
4044 SFG_game.save[i] = 0;
4046 SFG_setAndInitLevel(SFG_currentLevel.levelNumber);
4049 break;
4052 case SFG_GAME_STATE_WIN:
4054 // win animation
4056 SFG_updateLevel();
4058 int32_t t = SFG_game.stateTime;
4060 if (t > SFG_WIN_ANIMATION_DURATION)
4062 if (SFG_currentLevel.levelNumber == (SFG_NUMBER_OF_LEVELS - 1))
4064 if (SFG_keyIsDown(SFG_KEY_A))
4066 SFG_setGameState(SFG_GAME_STATE_OUTRO);
4067 SFG_setMusic(SFG_MUSIC_TURN_OFF);
4070 else if (SFG_keyIsDown(SFG_KEY_RIGHT) ||
4071 SFG_keyIsDown(SFG_KEY_LEFT) ||
4072 SFG_keyIsDown(SFG_KEY_STRAFE_LEFT) ||
4073 SFG_keyIsDown(SFG_KEY_STRAFE_RIGHT))
4075 SFG_setAndInitLevel(SFG_currentLevel.levelNumber + 1);
4077 SFG_player.health = SFG_game.save[2];
4078 SFG_player.ammo[0] = SFG_game.save[3];
4079 SFG_player.ammo[1] = SFG_game.save[4];
4080 SFG_player.ammo[2] = SFG_game.save[5];
4082 SFG_playerRotateWeapon(1);
4084 if (SFG_keyIsDown(SFG_KEY_RIGHT) || SFG_keyIsDown(SFG_KEY_STRAFE_RIGHT))
4086 // save the current position
4087 SFG_game.save[0] =
4088 (SFG_game.save[0] & 0x0f) | (SFG_currentLevel.levelNumber << 4);
4090 SFG_gameSave();
4091 SFG_game.saved = 1;
4096 break;
4099 case SFG_GAME_STATE_MAP:
4100 if (SFG_keyIsDown(SFG_KEY_B))
4101 SFG_setGameState(SFG_GAME_STATE_MENU);
4103 break;
4105 case SFG_GAME_STATE_INTRO:
4106 if (SFG_keyJustPressed(SFG_KEY_A) || SFG_keyJustPressed(SFG_KEY_B))
4107 SFG_setAndInitLevel(0);
4109 break;
4111 case SFG_GAME_STATE_OUTRO:
4112 if ((SFG_game.stateTime > SFG_STORYTEXT_DURATION) &&
4113 (SFG_keyIsDown(SFG_KEY_A) ||
4114 SFG_keyIsDown(SFG_KEY_B)))
4116 SFG_currentLevel.levelPointer = 0;
4117 SFG_currentLevel.levelNumber = 0;
4118 SFG_setGameState(SFG_GAME_STATE_MENU);
4119 SFG_playGameSound(3,SFG_MENU_CLICK_VOLUME);
4120 SFG_setMusic(SFG_MUSIC_TURN_ON);
4123 break;
4125 case SFG_GAME_STATE_LEVEL_START:
4127 SFG_updateLevel();
4128 SFG_updatePlayerHeight(); // in case player is on elevator
4130 int16_t x = 0, y = 0;
4132 SFG_getMouseOffset(&x,&y); // this keeps centering the mouse
4134 if (SFG_game.stateTime >= SFG_LEVEL_START_DURATION)
4135 SFG_setGameState(SFG_GAME_STATE_PLAYING);
4137 break;
4140 default:
4141 break;
4144 SFG_game.stateTime += SFG_MS_PER_FRAME;
4147 void SFG_fillRectangle(
4148 uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t color)
4150 if ((x + width > SFG_GAME_RESOLUTION_X) ||
4151 (y + height > SFG_GAME_RESOLUTION_Y))
4152 return;
4154 for (uint16_t j = y; j < y + height; ++j)
4155 for (uint16_t i = x; i < x + width; ++i)
4156 SFG_setGamePixel(i,j,color);
4159 static inline void SFG_clearScreen(uint8_t color)
4161 SFG_fillRectangle(0,0,SFG_GAME_RESOLUTION_X,
4162 SFG_GAME_RESOLUTION_Y,color);
4166 Draws fullscreen map of the current level.
4168 void SFG_drawMap()
4170 SFG_clearScreen(0);
4172 uint16_t maxJ =
4173 (SFG_MAP_PIXEL_SIZE * SFG_MAP_SIZE) < SFG_GAME_RESOLUTION_Y ?
4174 (SFG_MAP_SIZE) : (SFG_GAME_RESOLUTION_Y / SFG_MAP_PIXEL_SIZE);
4176 uint16_t maxI =
4177 (SFG_MAP_PIXEL_SIZE * SFG_MAP_SIZE) < SFG_GAME_RESOLUTION_X ?
4178 (SFG_MAP_SIZE) : (SFG_GAME_RESOLUTION_X / SFG_MAP_PIXEL_SIZE);
4180 uint16_t topLeftX =
4181 (SFG_GAME_RESOLUTION_X - (maxI * SFG_MAP_PIXEL_SIZE)) / 2;
4183 uint16_t topLeftY =
4184 (SFG_GAME_RESOLUTION_Y - (maxJ * SFG_MAP_PIXEL_SIZE)) / 2;
4186 uint16_t x;
4187 uint16_t y = topLeftY;
4189 uint8_t playerColor =
4190 SFG_game.blink ? SFG_MAP_PLAYER_COLOR1 : SFG_MAP_PLAYER_COLOR2;
4192 for (int16_t j = 0; j < maxJ; ++j)
4194 x = topLeftX;
4196 for (int16_t i = maxI - 1; i >= 0; --i)
4198 uint8_t color = 0; // init with non-revealed color
4200 if (SFG_currentLevel.mapRevealMask & SFG_getMapRevealBit(i,j))
4202 uint8_t properties;
4204 SFG_TileDefinition tile =
4205 SFG_getMapTile(SFG_currentLevel.levelPointer,i,j,&properties);
4207 color = playerColor; // start with player color
4209 if (i != SFG_player.squarePosition[0] ||
4210 j != SFG_player.squarePosition[1])
4212 if (properties == SFG_TILE_PROPERTY_ELEVATOR)
4213 color = SFG_MAP_ELEVATOR_COLOR;
4214 else if (properties == SFG_TILE_PROPERTY_SQUEEZER)
4215 color = SFG_MAP_SQUEEZER_COLOR;
4216 else if (properties == SFG_TILE_PROPERTY_DOOR)
4217 color = SFG_MAP_DOOR_COLOR;
4218 else
4220 color = 0;
4222 uint8_t c = SFG_TILE_CEILING_HEIGHT(tile) / 4;
4224 if (c != 0)
4225 color = (SFG_TILE_FLOOR_HEIGHT(tile) % 8 + 3) * 8 + c - 1;
4230 for (int_fast16_t k = 0; k < SFG_MAP_PIXEL_SIZE; ++k)
4231 for (int_fast16_t l = 0; l < SFG_MAP_PIXEL_SIZE; ++l)
4232 SFG_setGamePixel(x + l, y + k,color);
4234 x += SFG_MAP_PIXEL_SIZE;
4237 y += SFG_MAP_PIXEL_SIZE;
4242 Draws fullscreen story text (intro/outro).
4244 void SFG_drawStoryText()
4246 const char *text = SFG_outroText;
4247 uint16_t textColor = 23;
4248 uint8_t clearColor = 9;
4249 uint8_t sprite = 18;
4251 if (SFG_currentLevel.levelNumber != (SFG_NUMBER_OF_LEVELS - 1)) // intro?
4253 text = SFG_introText;
4254 textColor = 7;
4255 clearColor = 0;
4256 sprite = SFG_game.blink * 2;
4259 SFG_clearScreen(clearColor);
4261 if (SFG_GAME_RESOLUTION_Y > 50)
4262 SFG_blitImage(SFG_monsterSprites + sprite * SFG_TEXTURE_STORE_SIZE,
4263 (SFG_GAME_RESOLUTION_X - SFG_TEXTURE_SIZE * SFG_FONT_SIZE_SMALL) / 2,
4264 SFG_GAME_RESOLUTION_Y - (SFG_TEXTURE_SIZE + 3) * SFG_FONT_SIZE_SMALL,
4265 SFG_FONT_SIZE_SMALL);
4267 uint16_t textLen = 0;
4269 while (text[textLen] != 0)
4270 textLen++;
4272 uint16_t drawLen = RCL_min(
4273 textLen,(SFG_game.stateTime * textLen) / SFG_STORYTEXT_DURATION + 1);
4275 #define CHAR_SIZE (SFG_FONT_SIZE_SMALL * (SFG_FONT_CHARACTER_SIZE + 1))
4276 #define LINE_LENGTH (SFG_GAME_RESOLUTION_X / CHAR_SIZE)
4277 #define MAX_LENGTH (((SFG_GAME_RESOLUTION_Y / CHAR_SIZE) / 2) * LINE_LENGTH )
4279 uint16_t drawShift = (drawLen < MAX_LENGTH) ? 0 :
4280 (((drawLen - MAX_LENGTH) / LINE_LENGTH) * LINE_LENGTH);
4282 #undef CHAR_SIZE
4283 #undef LINE_LENGTH
4284 #undef MAX_LENGTH
4286 text += drawShift;
4287 drawLen -= drawShift;
4289 SFG_drawText(text,SFG_HUD_MARGIN,SFG_HUD_MARGIN,SFG_FONT_SIZE_SMALL,textColor,
4290 drawLen,SFG_GAME_RESOLUTION_X - SFG_HUD_MARGIN);
4294 Draws a number as text on screen, returns the number of characters drawn.
4296 uint8_t SFG_drawNumber(
4297 int16_t number,
4298 uint16_t x,
4299 uint16_t y,
4300 uint8_t size,
4301 uint8_t color)
4303 char text[7];
4305 text[6] = 0; // terminate the string
4307 int8_t positive = 1;
4309 if (number < 0)
4311 positive = 0;
4312 number *= -1;
4315 int8_t position = 5;
4317 while (1)
4319 text[position] = '0' + number % 10;
4320 number /= 10;
4322 position--;
4324 if (number == 0 || position == 0)
4325 break;
4328 if (!positive)
4330 text[position] = '-';
4331 position--;
4334 SFG_drawText(text + position + 1,x,y,size,color,0,0);
4336 return 5 - position;
4340 Draws a screen border that indicates something is happening, e.g. being hurt
4341 or taking an item.
4343 void SFG_drawIndicationBorder(uint16_t width, uint8_t color)
4345 for (int_fast16_t j = 0; j < width; ++j)
4347 uint16_t j2 = SFG_GAME_RESOLUTION_Y - 1 - j;
4349 for (int_fast16_t i = 0; i < SFG_GAME_RESOLUTION_X; ++i)
4351 if ((i & 0x01) == (j & 0x01))
4353 SFG_setGamePixel(i,j,color);
4354 SFG_setGamePixel(i,j2,color);
4359 for (int_fast16_t i = 0; i < width; ++i)
4361 uint16_t i2 = SFG_GAME_RESOLUTION_X - 1 - i;
4363 for (int_fast16_t j = width; j < SFG_GAME_RESOLUTION_Y - width; ++j)
4365 if ((i & 0x01) == (j & 0x01))
4367 SFG_setGamePixel(i,j,color);
4368 SFG_setGamePixel(i2,j,color);
4375 Draws the player weapon, includes handling the shoot animation.
4377 void SFG_drawWeapon(int16_t bobOffset)
4379 uint32_t animationLength =
4380 RCL_max(SFG_MIN_WEAPON_COOLDOWN_FRAMES,
4381 SFG_GET_WEAPON_FIRE_COOLDOWN_FRAMES(SFG_player.weapon));
4383 uint32_t shotAnimationFrame =
4384 animationLength - SFG_player.weaponCooldownFrames;
4386 bobOffset -= SFG_HUD_BAR_HEIGHT;
4388 uint8_t fireType = SFG_GET_WEAPON_FIRE_TYPE(SFG_player.weapon);
4390 if (shotAnimationFrame < animationLength)
4392 if (fireType == SFG_WEAPON_FIRE_TYPE_MELEE)
4394 bobOffset = shotAnimationFrame < animationLength / 2 ? 0 :
4395 2 * SFG_WEAPONBOB_OFFSET_PIXELS;
4397 else
4399 bobOffset +=
4400 ((animationLength - shotAnimationFrame) * SFG_WEAPON_IMAGE_SCALE * 20)
4401 / animationLength;
4403 if (
4404 ((fireType == SFG_WEAPON_FIRE_TYPE_FIREBALL) ||
4405 (fireType == SFG_WEAPON_FIRE_TYPE_BULLET)) &&
4406 shotAnimationFrame < animationLength / 2)
4407 SFG_blitImage(SFG_effectSprites,
4408 SFG_WEAPON_IMAGE_POSITION_X,
4409 SFG_WEAPON_IMAGE_POSITION_Y -
4410 (SFG_TEXTURE_SIZE / 3) * SFG_WEAPON_IMAGE_SCALE + bobOffset,
4411 SFG_WEAPON_IMAGE_SCALE);
4415 SFG_blitImage(SFG_weaponImages + SFG_player.weapon * SFG_TEXTURE_STORE_SIZE,
4416 SFG_WEAPON_IMAGE_POSITION_X,
4417 SFG_WEAPON_IMAGE_POSITION_Y + bobOffset - 1,
4418 SFG_WEAPON_IMAGE_SCALE);
4421 uint16_t SFG_textLen(const char *text)
4423 uint16_t result = 0;
4425 while (text[result] != 0)
4426 result++;
4428 return result;
4431 static inline uint16_t SFG_characterSize(uint8_t textSize)
4433 return (SFG_FONT_CHARACTER_SIZE + 1) * textSize;
4436 static inline uint16_t
4437 SFG_textHorizontalSize(const char *text, uint8_t textSize)
4439 return (SFG_textLen(text) * SFG_characterSize(textSize));
4442 void SFG_drawMenu()
4444 #define BACKGROUND_SCALE (SFG_GAME_RESOLUTION_X / (4 * SFG_TEXTURE_SIZE))
4446 #if BACKGROUND_SCALE == 0
4447 #undef BACKGROUND_SCALE
4448 #define BACKGROUND_SCALE 1
4449 #endif
4451 #define SCROLL_PIXELS_PER_FRAME ((64 * SFG_GAME_RESOLUTION_X) / (8 * SFG_FPS))
4453 #if SCROLL_PIXELS_PER_FRAME == 0
4454 #undef SCROLL_PIXELS_PER_FRAME
4455 #define SCROLL_PIXELS_PER_FRAME 1
4456 #endif
4458 #define SELECTION_START_X ((SFG_GAME_RESOLUTION_X - 12 * SFG_FONT_SIZE_MEDIUM\
4459 * (SFG_FONT_CHARACTER_SIZE + 1)) / 2)
4461 uint16_t scroll = (SFG_game.frame * SCROLL_PIXELS_PER_FRAME) / 64;
4463 for (uint16_t y = 0; y < SFG_GAME_RESOLUTION_Y; ++y)
4464 for (uint16_t x = 0; x < SFG_GAME_RESOLUTION_X; ++x)
4465 SFG_setGamePixel(x,y,
4466 (y >= (SFG_TEXTURE_SIZE * BACKGROUND_SCALE)) ? 0 :
4467 SFG_getTexel(SFG_backgroundImages,((x + scroll) / BACKGROUND_SCALE)
4468 % SFG_TEXTURE_SIZE,y / BACKGROUND_SCALE));
4470 uint16_t y = SFG_characterSize(SFG_FONT_SIZE_MEDIUM);
4472 SFG_blitImage(SFG_logoImage,SFG_GAME_RESOLUTION_X / 2 -
4473 (SFG_TEXTURE_SIZE / 2) * SFG_FONT_SIZE_SMALL,y,SFG_FONT_SIZE_SMALL);
4475 #if SFG_GAME_RESOLUTION_Y > 50
4476 y += 32 * SFG_FONT_SIZE_MEDIUM + SFG_characterSize(SFG_FONT_SIZE_MEDIUM);
4477 #else
4478 y = 2;
4479 #endif
4481 uint8_t i = 0;
4483 while (1) // draw menu items
4485 uint8_t item = SFG_getMenuItem(i);
4487 if (item == SFG_MENU_ITEM_NONE)
4488 break;
4490 #if (SFG_GAME_RESOLUTION_Y < 70) || SFG_FORCE_SINGLE_ITEM_MENU
4491 // with low resolution only display the selected item
4493 if (i != SFG_game.selectedMenuItem)
4495 i++;
4496 continue;
4498 #endif
4500 const char *text = SFG_menuItemTexts[item];
4501 uint8_t textLen = SFG_textLen(text);
4503 uint16_t drawX = (SFG_GAME_RESOLUTION_X -
4504 SFG_textHorizontalSize(text,SFG_FONT_SIZE_MEDIUM)) / 2;
4506 uint8_t textColor = 7;
4508 if (i != SFG_game.selectedMenuItem)
4509 textColor = 23;
4510 else
4511 SFG_fillRectangle( // menu item highlight
4512 SELECTION_START_X,
4513 y - SFG_FONT_SIZE_MEDIUM,
4514 SFG_GAME_RESOLUTION_X - SELECTION_START_X * 2,
4515 SFG_characterSize(SFG_FONT_SIZE_MEDIUM),2);
4517 SFG_drawText(text,drawX,y,SFG_FONT_SIZE_MEDIUM,textColor,0,0);
4519 if ((item == SFG_MENU_ITEM_PLAY || item == SFG_MENU_ITEM_SOUND
4520 || item == SFG_MENU_ITEM_SHEAR) &&
4521 ((i != SFG_game.selectedMenuItem) || SFG_game.blink))
4523 uint32_t x =
4524 drawX + SFG_characterSize(SFG_FONT_SIZE_MEDIUM) * (textLen + 1);
4526 uint8_t c = 93;
4528 if (item == SFG_MENU_ITEM_PLAY)
4529 SFG_drawNumber(SFG_game.selectedLevel + 1,x,y,SFG_FONT_SIZE_MEDIUM,c);
4530 else if (item == SFG_MENU_ITEM_SHEAR)
4532 uint8_t n = (SFG_game.settings >> 2) & 0x03;
4534 SFG_drawNumber(n == 3 ? 2 : n,x,y,SFG_FONT_SIZE_MEDIUM,c);
4536 else
4538 char settingText[3] = " ";
4540 settingText[0] = (SFG_game.settings & 0x01) ? 'S' : ' ';
4541 settingText[1] = (SFG_game.settings & 0x02) ? 'M' : ' ';
4543 SFG_drawText(settingText,x,y,SFG_FONT_SIZE_MEDIUM,c,0,0);
4547 y += SFG_characterSize(SFG_FONT_SIZE_MEDIUM) + SFG_FONT_SIZE_MEDIUM;
4548 i++;
4551 SFG_drawText(SFG_VERSION_STRING " CC0",SFG_HUD_MARGIN,SFG_GAME_RESOLUTION_Y -
4552 SFG_HUD_MARGIN - SFG_FONT_SIZE_SMALL * SFG_FONT_CHARACTER_SIZE,
4553 SFG_FONT_SIZE_SMALL,4,0,0);
4555 #if SFG_OS_IS_MALWARE
4556 if (SFG_game.blink)
4557 SFG_drawText(SFG_MALWARE_WARNING,SFG_HUD_MARGIN,SFG_HUD_MARGIN,
4558 SFG_FONT_SIZE_MEDIUM,95,0,0);
4559 #endif
4561 #undef MAX_ITEMS
4562 #undef BACKGROUND_SCALE
4563 #undef SCROLL_PIXELS_PER_FRAME
4566 void SFG_drawWinOverlay()
4568 uint32_t t = RCL_min(SFG_WIN_ANIMATION_DURATION,SFG_game.stateTime);
4570 uint32_t t2 = RCL_min(t,SFG_WIN_ANIMATION_DURATION / 4);
4572 #define STRIP_HEIGHT (SFG_GAME_RESOLUTION_Y / 2)
4573 #define INNER_STRIP_HEIGHT ((STRIP_HEIGHT * 3) / 4)
4574 #define STRIP_START ((SFG_GAME_RESOLUTION_Y - STRIP_HEIGHT) / 2)
4576 RCL_Unit l = (t2 * STRIP_HEIGHT * 4) / SFG_WIN_ANIMATION_DURATION;
4578 for (uint16_t y = STRIP_START; y < STRIP_START + l; ++y)
4579 for (uint16_t x = 0; x < SFG_GAME_RESOLUTION_X; ++x)
4580 SFG_setGamePixel(x,y,
4581 RCL_abs(y - (SFG_GAME_RESOLUTION_Y / 2)) <= (INNER_STRIP_HEIGHT / 2) ?
4582 0 : 172);
4584 char textLine[] = SFG_TEXT_LEVEL_COMPLETE;
4586 uint16_t y = SFG_GAME_RESOLUTION_Y / 2 -
4587 ((STRIP_HEIGHT + INNER_STRIP_HEIGHT) / 2) / 2;
4589 uint16_t x = (SFG_GAME_RESOLUTION_X -
4590 SFG_textHorizontalSize(textLine,SFG_FONT_SIZE_BIG)) / 2;
4592 SFG_drawText(textLine,x,y,SFG_FONT_SIZE_BIG,7 + SFG_game.blink * 95,255,0);
4594 uint32_t timeTotal = SFG_SAVE_TOTAL_TIME;
4596 // don't show totals in level 1:
4597 uint8_t blink = (SFG_game.blink) && (SFG_currentLevel.levelNumber != 0)
4598 && (timeTotal != 0);
4600 if (t >= (SFG_WIN_ANIMATION_DURATION / 2))
4602 y += (SFG_FONT_SIZE_BIG + SFG_FONT_SIZE_MEDIUM) * SFG_FONT_CHARACTER_SIZE;
4603 x = SFG_HUD_MARGIN;
4605 #define CHAR_SIZE (SFG_FONT_SIZE_SMALL * SFG_FONT_CHARACTER_SIZE)
4607 uint32_t time = blink ? timeTotal : SFG_currentLevel.completionTime10sOfS;
4609 x += SFG_drawNumber(time / 10,x,y,SFG_FONT_SIZE_SMALL,7) *
4610 CHAR_SIZE + SFG_FONT_SIZE_SMALL;
4612 char timeRest[5] = ".X s";
4614 timeRest[1] = '0' + (time % 10);
4616 SFG_drawText(timeRest,x,y,SFG_FONT_SIZE_SMALL,7,4,0);
4618 x = SFG_HUD_MARGIN;
4619 y += (SFG_FONT_SIZE_BIG + SFG_FONT_SIZE_MEDIUM) * SFG_FONT_CHARACTER_SIZE;
4621 if (blink)
4623 x += (SFG_drawNumber(SFG_game.save[10] + SFG_game.save[11] * 256,x,y,
4624 SFG_FONT_SIZE_SMALL,7) + 1) * CHAR_SIZE;
4626 else
4628 x += SFG_drawNumber(SFG_currentLevel.monstersDead,x,y,
4629 SFG_FONT_SIZE_SMALL,7) * CHAR_SIZE;
4631 SFG_drawText("/",x,y,SFG_FONT_SIZE_SMALL,7,1,0);
4633 x += CHAR_SIZE;
4635 x += (SFG_drawNumber(SFG_currentLevel.monsterRecordCount,x,y,
4636 SFG_FONT_SIZE_SMALL,7) + 1) * CHAR_SIZE;
4639 SFG_drawText(SFG_TEXT_KILLS,x,y,SFG_FONT_SIZE_SMALL,7,255,0);
4641 if ((t >= (SFG_WIN_ANIMATION_DURATION - 1)) &&
4642 (SFG_currentLevel.levelNumber != (SFG_NUMBER_OF_LEVELS - 1)) &&
4643 SFG_game.blink)
4645 y += (SFG_FONT_SIZE_BIG + SFG_FONT_SIZE_MEDIUM) * SFG_FONT_CHARACTER_SIZE;
4647 SFG_drawText(SFG_TEXT_SAVE_PROMPT,
4648 (SFG_GAME_RESOLUTION_X - SFG_textHorizontalSize(SFG_TEXT_SAVE_PROMPT,
4649 SFG_FONT_SIZE_MEDIUM)) / 2,y,SFG_FONT_SIZE_MEDIUM,7,255,0);
4652 #undef CHAR_SIZE
4655 #undef STRIP_HEIGHT
4656 #undef STRIP_START
4657 #undef INNER_STRIP_HEIGHT
4660 void SFG_draw()
4662 #if SFG_BACKGROUND_BLUR != 0
4663 SFG_backgroundBlurIndex = 0;
4664 #endif
4666 if (SFG_game.state == SFG_GAME_STATE_MENU)
4668 SFG_drawMenu();
4669 return;
4672 if (SFG_game.state == SFG_GAME_STATE_INTRO ||
4673 SFG_game.state == SFG_GAME_STATE_OUTRO)
4675 SFG_drawStoryText();
4676 return;
4679 if (SFG_keyIsDown(SFG_KEY_MAP) || (SFG_game.state == SFG_GAME_STATE_MAP))
4681 SFG_drawMap();
4683 else
4685 for (int_fast16_t i = 0; i < SFG_Z_BUFFER_SIZE; ++i)
4686 SFG_game.zBuffer[i] = 255;
4688 int16_t weaponBobOffset = 0;
4690 #if SFG_HEADBOB_ENABLED
4691 RCL_Unit headBobOffset = 0;
4693 #if SFG_HEADBOB_SHEAR != 0
4694 int16_t headBobShearOffset = 0;
4695 #endif
4697 if (SFG_game.state != SFG_GAME_STATE_LOSE)
4699 RCL_Unit bobSin = RCL_sin(SFG_player.headBobFrame);
4701 headBobOffset = (bobSin * SFG_HEADBOB_OFFSET) / RCL_UNITS_PER_SQUARE;
4703 #if SFG_HEADBOB_SHEAR != 0
4704 headBobShearOffset = (bobSin * SFG_HEADBOB_SHEAR) / RCL_UNITS_PER_SQUARE;
4705 SFG_player.camera.shear += headBobShearOffset;
4706 #endif
4708 weaponBobOffset =
4709 (bobSin * SFG_WEAPONBOB_OFFSET_PIXELS) / (RCL_UNITS_PER_SQUARE) +
4710 SFG_WEAPONBOB_OFFSET_PIXELS;
4712 else
4714 // player die animation
4716 weaponBobOffset =
4717 (SFG_WEAPON_IMAGE_SCALE * SFG_TEXTURE_SIZE * SFG_game.stateTime) /
4718 SFG_LOSE_ANIMATION_DURATION;
4721 // add head bob just for the rendering (we'll will substract it back later)
4723 SFG_player.camera.height += headBobOffset;
4724 #endif // headbob enabled?
4726 RCL_renderComplex(
4727 SFG_player.camera,
4728 SFG_floorHeightAt,
4729 SFG_ceilingHeightAt,
4730 SFG_texturesAt,
4731 SFG_game.rayConstraints);
4733 // draw sprites:
4735 // monster sprites:
4736 for (int_fast16_t i = 0; i < SFG_currentLevel.monsterRecordCount; ++i)
4738 SFG_MonsterRecord m = SFG_currentLevel.monsterRecords[i];
4739 uint8_t state = SFG_MR_STATE(m);
4741 if (state != SFG_MONSTER_STATE_INACTIVE)
4743 RCL_Vector2D worldPosition;
4745 worldPosition.x = SFG_MONSTER_COORD_TO_RCL_UNITS(m.coords[0]);
4746 worldPosition.y = SFG_MONSTER_COORD_TO_RCL_UNITS(m.coords[1]);
4748 uint8_t spriteSize = SFG_GET_MONSTER_SPRITE_SIZE(
4749 SFG_MONSTER_TYPE_TO_INDEX(SFG_MR_TYPE(m)));
4751 RCL_Unit worldHeight =
4752 SFG_floorHeightAt(
4753 SFG_MONSTER_COORD_TO_SQUARES(m.coords[0]),
4754 SFG_MONSTER_COORD_TO_SQUARES(m.coords[1]))
4755 + SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(spriteSize);
4757 RCL_PixelInfo p =
4758 RCL_mapToScreen(worldPosition,worldHeight,SFG_player.camera);
4760 if (p.depth > 0 &&
4761 SFG_spriteIsVisible(worldPosition,worldHeight))
4763 const uint8_t *s =
4764 SFG_getMonsterSprite(
4765 SFG_MR_TYPE(m),
4766 state,
4767 SFG_game.spriteAnimationFrame & 0x01);
4769 SFG_drawScaledSprite(s,
4770 p.position.x * SFG_RAYCASTING_SUBSAMPLE,p.position.y,
4771 RCL_perspectiveScaleVertical(
4772 SFG_SPRITE_SIZE_PIXELS(spriteSize),
4773 p.depth),
4774 p.depth / (RCL_UNITS_PER_SQUARE * 2),p.depth);
4779 // item sprites:
4780 for (int_fast16_t i = 0; i < SFG_currentLevel.itemRecordCount; ++i)
4781 if (SFG_currentLevel.itemRecords[i] & SFG_ITEM_RECORD_ACTIVE_MASK)
4783 RCL_Vector2D worldPosition;
4785 SFG_LevelElement e =
4786 SFG_currentLevel.levelPointer->elements[
4787 SFG_currentLevel.itemRecords[i] & ~SFG_ITEM_RECORD_ACTIVE_MASK];
4789 worldPosition.x =
4790 SFG_ELEMENT_COORD_TO_RCL_UNITS(e.coords[0]);
4792 worldPosition.y =
4793 SFG_ELEMENT_COORD_TO_RCL_UNITS(e.coords[1]);
4795 const uint8_t *sprite;
4796 uint8_t spriteSize;
4798 SFG_getItemSprite(e.type,&sprite,&spriteSize);
4800 if (sprite != 0)
4802 RCL_Unit worldHeight = SFG_floorHeightAt(e.coords[0],e.coords[1])
4803 + SFG_SPRITE_SIZE_TO_HEIGHT_ABOVE_GROUND(spriteSize);
4805 RCL_PixelInfo p =
4806 RCL_mapToScreen(worldPosition,worldHeight,SFG_player.camera);
4808 if (p.depth > 0 &&
4809 SFG_spriteIsVisible(worldPosition,worldHeight))
4810 SFG_drawScaledSprite(sprite,p.position.x * SFG_RAYCASTING_SUBSAMPLE,
4811 p.position.y,
4812 RCL_perspectiveScaleVertical(SFG_SPRITE_SIZE_PIXELS(spriteSize),
4813 p.depth),p.depth / (RCL_UNITS_PER_SQUARE * 2),p.depth);
4817 // projectile sprites:
4818 for (uint8_t i = 0; i < SFG_currentLevel.projectileRecordCount; ++i)
4820 SFG_ProjectileRecord *proj = &(SFG_currentLevel.projectileRecords[i]);
4822 if (proj->type == SFG_PROJECTILE_BULLET)
4823 continue; // bullets aren't drawn
4825 RCL_Vector2D worldPosition;
4827 worldPosition.x = proj->position[0];
4828 worldPosition.y = proj->position[1];
4830 RCL_PixelInfo p =
4831 RCL_mapToScreen(worldPosition,proj->position[2],SFG_player.camera);
4833 const uint8_t *s =
4834 SFG_effectSprites + proj->type * SFG_TEXTURE_STORE_SIZE;
4836 int16_t spriteSize = SFG_SPRITE_SIZE_PIXELS(0);
4838 if (proj->type == SFG_PROJECTILE_EXPLOSION ||
4839 proj->type == SFG_PROJECTILE_DUST)
4841 int16_t doubleFramesToLive =
4842 RCL_nonZero(SFG_GET_PROJECTILE_FRAMES_TO_LIVE(proj->type) / 2);
4844 // grow the explosion/dust sprite as an animation
4845 spriteSize = (
4846 SFG_SPRITE_SIZE_PIXELS(2) *
4847 RCL_sin(
4848 ((doubleFramesToLive -
4849 proj->doubleFramesToLive) * RCL_UNITS_PER_SQUARE / 4)
4850 / doubleFramesToLive)
4851 ) / RCL_UNITS_PER_SQUARE;
4854 if (p.depth > 0 &&
4855 SFG_spriteIsVisible(worldPosition,proj->position[2]))
4856 SFG_drawScaledSprite(s,
4857 p.position.x * SFG_RAYCASTING_SUBSAMPLE,p.position.y,
4858 RCL_perspectiveScaleVertical(spriteSize,p.depth),
4859 SFG_fogValueDiminish(p.depth),
4860 p.depth);
4863 #if SFG_HEADBOB_ENABLED
4864 // after rendering sprites substract back the head bob offset
4865 SFG_player.camera.height -= headBobOffset;
4867 #if SFG_HEADBOB_SHEAR != 0
4868 SFG_player.camera.shear -= headBobShearOffset;
4869 #endif
4871 #endif // head bob enabled?
4873 #if SFG_PREVIEW_MODE == 0
4874 SFG_drawWeapon(weaponBobOffset);
4875 #endif
4877 // draw HUD:
4879 // bar
4881 uint8_t color = 61;
4882 uint8_t color2 = 48;
4884 if (SFG_game.cheatState & 0x80)
4886 color = 170;
4887 color2 = 0;
4890 for (uint16_t j = SFG_GAME_RESOLUTION_Y - SFG_HUD_BAR_HEIGHT;
4891 j < SFG_GAME_RESOLUTION_Y; ++j)
4893 for (uint16_t i = 0; i < SFG_GAME_RESOLUTION_X; ++i)
4894 SFG_setGamePixel(i,j,color);
4896 color = color2;
4899 #define TEXT_Y (SFG_GAME_RESOLUTION_Y - SFG_HUD_MARGIN - \
4900 SFG_FONT_CHARACTER_SIZE * SFG_FONT_SIZE_MEDIUM)
4902 SFG_drawNumber( // health
4903 SFG_player.health,
4904 SFG_HUD_MARGIN,
4905 TEXT_Y,
4906 SFG_FONT_SIZE_MEDIUM,
4907 SFG_player.health > SFG_PLAYER_HEALTH_WARNING_LEVEL ? 6 : 175);
4909 SFG_drawNumber( // ammo
4910 SFG_player.weapon != SFG_WEAPON_KNIFE ?
4911 SFG_player.ammo[SFG_weaponAmmo(SFG_player.weapon)] : 0,
4912 SFG_GAME_RESOLUTION_X - SFG_HUD_MARGIN -
4913 (SFG_FONT_CHARACTER_SIZE + 1) * SFG_FONT_SIZE_MEDIUM * 3,
4914 TEXT_Y,
4915 SFG_FONT_SIZE_MEDIUM,
4916 6);
4918 for (uint8_t i = 0; i < 3; ++i) // access cards
4919 if (
4920 ((SFG_player.cards >> i) | ((SFG_player.cards >> (i + 3))
4921 & SFG_game.blink)) & 0x01)
4922 SFG_fillRectangle(
4923 SFG_HUD_MARGIN + (SFG_FONT_CHARACTER_SIZE + 1) *
4924 SFG_FONT_SIZE_MEDIUM * (5 + i),
4925 TEXT_Y,
4926 SFG_FONT_SIZE_MEDIUM * SFG_FONT_CHARACTER_SIZE,
4927 SFG_FONT_SIZE_MEDIUM * SFG_FONT_CHARACTER_SIZE,
4928 i == 0 ? 93 : (i == 1 ? 124 : 60));
4930 #undef TEXT_Y
4932 // border indicator
4934 if ((SFG_game.frame - SFG_player.lastHurtFrame
4935 <= SFG_HUD_BORDER_INDICATOR_DURATION_FRAMES) ||
4936 (SFG_game.state == SFG_GAME_STATE_LOSE))
4937 SFG_drawIndicationBorder(SFG_HUD_BORDER_INDICATOR_WIDTH_PIXELS,
4938 SFG_HUD_HURT_INDICATION_COLOR);
4939 else if (SFG_game.frame - SFG_player.lastItemTakenFrame
4940 <= SFG_HUD_BORDER_INDICATOR_DURATION_FRAMES)
4941 SFG_drawIndicationBorder(SFG_HUD_BORDER_INDICATOR_WIDTH_PIXELS,
4942 SFG_HUD_ITEM_TAKEN_INDICATION_COLOR);
4944 if (SFG_game.state == SFG_GAME_STATE_WIN)
4945 SFG_drawWinOverlay();
4946 else if (SFG_game.state == SFG_GAME_STATE_LEVEL_START)
4947 SFG_drawLevelStartOverlay();
4951 uint8_t SFG_mainLoopBody()
4953 /* Standard deterministic game loop, independed of actual achieved FPS.
4954 Each game logic (physics) frame is performed with the SFG_MS_PER_FRAME
4955 delta time. */
4957 if (SFG_game.state != SFG_GAME_STATE_INIT)
4959 uint32_t timeNow = SFG_getTimeMs();
4961 #if SFG_TIME_MULTIPLIER != 1024
4962 timeNow = (timeNow * SFG_TIME_MULTIPLIER) / 1024;
4963 #endif
4965 int32_t timeSinceLastFrame = timeNow - SFG_game.frameTime;
4967 if (timeSinceLastFrame >= SFG_MS_PER_FRAME)
4969 uint8_t steps = 0;
4971 uint8_t wasFirstFrame = SFG_game.frame == 0;
4973 while (timeSinceLastFrame >= SFG_MS_PER_FRAME)
4975 uint8_t previousWeapon = SFG_player.weapon;
4977 SFG_game.frameTime += SFG_MS_PER_FRAME;
4979 SFG_gameStep();
4981 if (SFG_player.weapon != previousWeapon)
4982 SFG_processEvent(SFG_EVENT_PLAYER_CHANGES_WEAPON,SFG_player.weapon);
4984 timeSinceLastFrame -= SFG_MS_PER_FRAME;
4986 SFG_game.frame++;
4987 steps++;
4990 if ((steps > 1) && (SFG_game.antiSpam == 0) && (!wasFirstFrame))
4992 SFG_LOG("failed to reach target FPS! consider setting a lower value")
4993 SFG_game.antiSpam = 30;
4996 if (SFG_game.antiSpam > 0)
4997 SFG_game.antiSpam--;
4999 // render only once
5000 SFG_draw();
5002 if (SFG_game.frame % 16 == 0)
5003 SFG_CPU_LOAD(((SFG_getTimeMs() - timeNow) * 100) / SFG_MS_PER_FRAME);
5005 else
5007 // wait, relieve CPU
5008 SFG_sleepMs(RCL_max(1,
5009 (3 * (SFG_game.frameTime + SFG_MS_PER_FRAME - timeNow)) / 4));
5012 else if (!SFG_keyPressed(SFG_KEY_A) && !SFG_keyPressed(SFG_KEY_B))
5014 /* At the beginning we have to wait for the release of the keys in order not
5015 to immediatelly confirm a menu item. */
5016 SFG_setGameState(SFG_GAME_STATE_MENU);
5019 return SFG_game.continues;
5022 #undef SFG_SAVE_TOTAL_TIME
5024 #endif // guard