mirror of
https://github.com/D4C1-Labs/Flipper-ARF.git
synced 2026-07-03 14:11:37 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 492cee4373 | |||
| a0c53e381c |
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 James Howard (original)
|
||||
Copyright (c) 2025 Apfxtech (Flipper Zero port)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,177 @@
|
||||
# Flipper DOOM
|
||||
|
||||
A DOOM-style first-person shooter demake for the Flipper Zero
|
||||
(128×64 monochrome LCD). Built on the raycaster engine from
|
||||
[FlipperCatacombs](https://github.com/apfxtech/FlipperCatacombs) (a port of
|
||||
*Catacombs of the Damned* / arduboy3d by jhhoward), re-skinned with graphics
|
||||
converted **at build time from your own DOOM shareware WAD**.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Copy `dist/flipdoom.fap` to your SD card under `SD/apps/Games/`
|
||||
(or run `ufbt launch` with the Flipper connected via USB).
|
||||
2. On the Flipper: **Apps → Games → Flipper DOOM**.
|
||||
3. Title screen → main menu → select **Play** with OK.
|
||||
4. Fight through procedurally generated levels, find the exit gate on each
|
||||
floor, survive as deep as you can. Your score and high score are saved
|
||||
automatically.
|
||||
|
||||
---
|
||||
|
||||
## Controls
|
||||
|
||||
### Title screen
|
||||
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| Any button | Skip the title screen |
|
||||
|
||||
### Main menu
|
||||
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| **Up / Down** | Move cursor (Play / Sound on-off / Score / High score) |
|
||||
| **OK** | Select item |
|
||||
| **Back (hold ~300 ms)** | Exit the app |
|
||||
|
||||
### In game
|
||||
|
||||
| Button | Action |
|
||||
|---|---|
|
||||
| **Up / Down** | Move forward / backward |
|
||||
| **Left / Right** | Turn left / right |
|
||||
| **OK** | Fire the shotgun (hold for continuous fire) |
|
||||
| **OK held + Left / Right** | **Strafe** (circle-strafe while firing) — essential for dodging fireballs |
|
||||
| **Back (hold ~300 ms)** | Leave the game and return to the menu |
|
||||
|
||||
Notes:
|
||||
|
||||
- Firing costs ammo (bottom HUD bar); ammo regenerates when not shooting.
|
||||
- Strafing only works while OK is held. Tap OK to fire while turning
|
||||
normally; hold OK to switch the side arrows into dodge mode.
|
||||
- Walking into a backpack opens it; walking over pickups collects them.
|
||||
|
||||
### HUD
|
||||
|
||||
- **Bottom bar with cross icon** — health.
|
||||
- **Bar above it with bullets icon** — ammo.
|
||||
- Screen border flashes when you take damage.
|
||||
- Pickup/event messages appear at the top of the screen.
|
||||
|
||||
---
|
||||
|
||||
## Gameplay
|
||||
|
||||
### Enemies
|
||||
|
||||
| Enemy | Behavior |
|
||||
|---|---|
|
||||
| **Zombieman** | Weak, shoots from range, appears from level 1 |
|
||||
| **Sergeant** | Fast, shoots hard, keeps distance |
|
||||
| **Imp** | Throws fireballs, backs away if you get close |
|
||||
| **Demon** | Melee tank — charges you and bites |
|
||||
|
||||
Difficulty scales with depth: early levels spawn mostly zombies; imps join
|
||||
from level 3, and demons dominate from level 6.
|
||||
|
||||
### Exploding barrels
|
||||
|
||||
Shooting a barrel triggers an area explosion that deals heavy damage to every
|
||||
enemy nearby (one-shots zombies and sergeants) and hurts you if you stand too
|
||||
close — though it will never kill you outright, it can leave you at 1 HP.
|
||||
Barrels sometimes leave a pickup behind.
|
||||
|
||||
### Pickups
|
||||
|
||||
| Item | Effect |
|
||||
|---|---|
|
||||
| Stimpack | Restores health |
|
||||
| Backpack (walk into it) | Bonus points |
|
||||
| Armor / helmet / bonus items | Points |
|
||||
|
||||
### Scoring
|
||||
|
||||
Points are awarded for floors cleared, kills per enemy type, and items
|
||||
collected. Escaping the base (final exit) grants a large bonus. Score and
|
||||
high score are stored on the SD card (`apps_data/flipdoom/`) and persist
|
||||
between sessions.
|
||||
|
||||
---
|
||||
|
||||
## Sound
|
||||
|
||||
Tone-based sound effects through the Flipper buzzer (shotgun blast, hits,
|
||||
kills, pickups, player damage), all tuned within the buzzer's physical
|
||||
100–2500 Hz range. Sound can be toggled in the main menu; the system
|
||||
Stealth Mode is respected.
|
||||
|
||||
---
|
||||
|
||||
## Building from source
|
||||
|
||||
Requires Python 3 and [ufbt](https://pypi.org/project/ufbt/):
|
||||
|
||||
```sh
|
||||
pip install ufbt
|
||||
|
||||
cd FlipperDoom
|
||||
|
||||
# 1) Generate the sprite header from YOUR shareware WAD (not included here):
|
||||
python3 tools/extract_doom_assets.py /path/to/Doom1.WAD
|
||||
|
||||
# 2) Build / install:
|
||||
ufbt # produces dist/flipdoom.fap
|
||||
ufbt launch # builds, installs and runs on a connected Flipper
|
||||
```
|
||||
|
||||
### About the assets
|
||||
|
||||
No id Software assets are stored in this repository. The generated header
|
||||
`game/Generated/DoomSprites.inc.h` is produced locally by
|
||||
`tools/extract_doom_assets.py`, which decodes sprites from the user's own
|
||||
shareware WAD (freely distributable as a whole), rescales them and quantizes
|
||||
to 1-bit (black / 50% checker / white) in the engine's sprite formats.
|
||||
Do not redistribute the generated header — always regenerate it from a WAD
|
||||
you own. The "DOOM" title lettering and the HUD icons are original pixel art
|
||||
made for this project. Preview images of the converted assets are written to
|
||||
`tools/preview/`.
|
||||
|
||||
Entity mapping (game mechanics are inherited from the Catacombs engine):
|
||||
|
||||
| Engine entity | DOOM sprite | Role |
|
||||
|---|---|---|
|
||||
| Skeleton | Demon (SARG) | melee tank |
|
||||
| Mage | Imp (TROO) | fireball thrower |
|
||||
| Bat | Sergeant (SPOS) | fast shooter |
|
||||
| Spider | Zombieman (POSS) | weak shooter |
|
||||
| Weapon | Shotgun (SHTG + SHTF flash) | first person, centered |
|
||||
| Urn | Barrel (BAR1) | explodes when shot |
|
||||
| Potion | Stimpack (STIM) | health |
|
||||
| Chest / opened | Backpack (BPAK) / Clip (CLIP) | treasure |
|
||||
| Crown / scroll / coins | Armor / helmet / potion bottle (ARM1, BON2, BON1) | points |
|
||||
| Sign | Skull pile (POL5) | decoration |
|
||||
| Projectiles | Fireball (BAL1) | player & enemies |
|
||||
|
||||
## Why not a real doomgeneric port?
|
||||
|
||||
The hardware makes it impossible — not a software choice:
|
||||
|
||||
| | Flipper Zero | Real DOOM (doomgeneric) |
|
||||
|---|---|---|
|
||||
| Total RAM | 256 KB | — |
|
||||
| Free heap for apps | ~140 KB | ~7 MB (zone memory + framebuffer) |
|
||||
| Engine binary | FAPs load fully into RAM | ~524 KB compiled for Cortex-M4 |
|
||||
|
||||
The Flipper cannot execute code from the SD card (no XIP), so it can't even
|
||||
load the DOOM engine binary. DOOM ports to 256 KB microcontrollers (GBA,
|
||||
nRF52840) rely on memory-mapped flash, which the Flipper does not have. This
|
||||
demake keeps the aesthetic with an engine that actually fits: ~30 KB loaded,
|
||||
30 FPS on the 64 MHz Cortex-M4.
|
||||
|
||||
## License
|
||||
|
||||
Same license as the base project (see `LICENSE`). WAD contents are property
|
||||
of id Software; the extraction tool only transforms them locally for
|
||||
personal use.
|
||||
@@ -0,0 +1,15 @@
|
||||
App(
|
||||
appid="flipdoom",
|
||||
name="Flipper DOOM",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="flipdoom_app",
|
||||
cdefines=["APP_FLIPDOOM"],
|
||||
requires=["gui"],
|
||||
stack_size=8 * 1024,
|
||||
fap_category="Games",
|
||||
fap_icon="flipdoom_icon.png",
|
||||
order=36,
|
||||
fap_author="user",
|
||||
fap_version="1.0",
|
||||
fap_description="Doom-style raycaster demake for Flipper Zero. Graphics converted at build time from your own Doom shareware WAD.",
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 96 B |
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
static inline const void* pgm_read_ptr_safe(const void* p) {
|
||||
const void* out;
|
||||
std::memcpy(&out, p, sizeof(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Platform detection
|
||||
#if defined(_WIN32)
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#define PROGMEM
|
||||
#define PSTR(s) (s)
|
||||
#define pgm_read_byte(x) (*((uint8_t*)(x)))
|
||||
#define pgm_read_word(x) (*((uint16_t*)(x)))
|
||||
#define pgm_read_ptr(x) (*((uintptr_t*)(x)))
|
||||
#define strlen_P(x) strlen(x)
|
||||
#define strcpy_P(dst, src) strcpy(dst, src)
|
||||
#define memcpy_P(dst, src, n) memcpy(dst, src, n)
|
||||
#elif defined(__AVR__)
|
||||
// Arduino/Arduboy platform
|
||||
#include <avr/pgmspace.h>
|
||||
#else
|
||||
// Flipper Zero and other ARM platforms
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#define PROGMEM
|
||||
#define PSTR(s) (s)
|
||||
#define pgm_read_byte(x) (*((const uint8_t*)(x)))
|
||||
#define pgm_read_word(x) (*((const uint16_t*)(x)))
|
||||
#define pgm_read_dword(x) (*((const uint32_t*)(x)))
|
||||
#define strlen_P(x) strlen(x)
|
||||
#define strcpy_P(dst, src) strcpy(dst, src)
|
||||
#define strcmp_P(s1, s2) strcmp(s1, s2)
|
||||
#define strncmp_P(s1, s2, n) strncmp(s1, s2, n)
|
||||
#define memcpy_P(dst, src, n) memcpy(dst, src, n)
|
||||
#define sprintf_P sprintf
|
||||
#define snprintf_P snprintf
|
||||
#endif
|
||||
|
||||
// Display configuration
|
||||
#define DISPLAY_WIDTH 128
|
||||
#define DISPLAY_HEIGHT 64
|
||||
|
||||
// Game settings
|
||||
#define DEV_MODE 0
|
||||
|
||||
// Input definitions
|
||||
#define INPUT_LEFT 1
|
||||
#define INPUT_RIGHT 2
|
||||
#define INPUT_UP 4
|
||||
#define INPUT_DOWN 8
|
||||
#define INPUT_A 16
|
||||
#define INPUT_B 32
|
||||
|
||||
#ifndef COLOUR_WHITE
|
||||
#define COLOUR_WHITE 1
|
||||
#endif
|
||||
|
||||
#ifndef COLOUR_BLACK
|
||||
#define COLOUR_BLACK 0
|
||||
#endif
|
||||
|
||||
// Angle system (256 = 360 degrees)
|
||||
#define FIXED_ANGLE_MAX 256
|
||||
|
||||
// 3D rendering settings
|
||||
#define CAMERA_SCALE 1
|
||||
#define CLIP_PLANE 32
|
||||
#define CLIP_ANGLE 32
|
||||
#define NEAR_PLANE_MULTIPLIER 130
|
||||
#define NEAR_PLANE (DISPLAY_WIDTH * NEAR_PLANE_MULTIPLIER / 256)
|
||||
#define HORIZON (DISPLAY_HEIGHT / 2)
|
||||
|
||||
// World settings
|
||||
#define CELL_SIZE 256
|
||||
#define PARTICLES_PER_SYSTEM 8
|
||||
#define BASE_SPRITE_SIZE 16
|
||||
#define MAX_SPRITE_SIZE (DISPLAY_HEIGHT / 2)
|
||||
#define MIN_TEXTURE_DISTANCE 4
|
||||
#define MAX_QUEUED_DRAWABLES 12
|
||||
|
||||
// Player settings
|
||||
#define TURN_SPEED 3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
#pragma once
|
||||
#include "game/Defines.h"
|
||||
|
||||
#define WITH_IMAGE_TEXTURES 0
|
||||
#define WITH_VECTOR_TEXTURES 1
|
||||
#define WITH_TEXTURES (WITH_IMAGE_TEXTURES || WITH_VECTOR_TEXTURES)
|
||||
#define WITH_SPRITE_OUTLINES 1
|
||||
|
||||
struct Camera {
|
||||
int16_t x, y;
|
||||
uint8_t angle;
|
||||
int16_t rotCos, rotSin;
|
||||
int16_t clipCos, clipSin;
|
||||
uint8_t cellX, cellY;
|
||||
int8_t tilt;
|
||||
int8_t bob;
|
||||
uint8_t shakeTime;
|
||||
};
|
||||
|
||||
enum class DrawableType : uint8_t {
|
||||
Sprite = 0,
|
||||
ParticleSystem = 1
|
||||
};
|
||||
|
||||
enum class AnchorType : uint8_t {
|
||||
Floor,
|
||||
Center,
|
||||
BelowCenter,
|
||||
Ceiling
|
||||
};
|
||||
|
||||
struct QueuedDrawable {
|
||||
union {
|
||||
const uint16_t* spriteData;
|
||||
struct ParticleSystem* particleSystem;
|
||||
};
|
||||
|
||||
DrawableType type : 1;
|
||||
bool invert : 1;
|
||||
int8_t x;
|
||||
int8_t y;
|
||||
uint8_t halfSize;
|
||||
uint8_t inverseCameraDistance;
|
||||
};
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
static Camera camera;
|
||||
static uint8_t wBuffer[DISPLAY_WIDTH];
|
||||
static uint8_t globalRenderFrame;
|
||||
|
||||
static void Render();
|
||||
|
||||
static void DrawObject(
|
||||
const uint16_t* spriteData,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t scale = 128,
|
||||
AnchorType anchor = AnchorType::Floor,
|
||||
bool invert = false);
|
||||
static QueuedDrawable* CreateQueuedDrawable(uint8_t inverseCameraDistance);
|
||||
static int8_t GetHorizon(int16_t x);
|
||||
|
||||
static bool
|
||||
TransformAndCull(int16_t worldX, int16_t worldY, int16_t& outScreenX, int16_t& outScreenW);
|
||||
|
||||
static void DrawScaled(
|
||||
const uint16_t* data,
|
||||
int8_t x,
|
||||
int8_t y,
|
||||
uint8_t halfSize,
|
||||
uint8_t inverseCameraDistance,
|
||||
bool invert = false,
|
||||
uint8_t color = COLOUR_BLACK);
|
||||
|
||||
static int8_t horizonBuffer[DISPLAY_WIDTH];
|
||||
static uint8_t numBufferSlicesFilled;
|
||||
static void DrawWallLine(
|
||||
int16_t x1,
|
||||
int16_t y1,
|
||||
int16_t x2,
|
||||
int16_t y2,
|
||||
uint8_t clipLeft,
|
||||
uint8_t clipRight,
|
||||
uint8_t col);
|
||||
static void DrawWallSegment(
|
||||
const uint8_t* texture,
|
||||
int16_t x1,
|
||||
int16_t w1,
|
||||
int16_t x2,
|
||||
int16_t w2,
|
||||
uint8_t u1clip,
|
||||
uint8_t u2clip,
|
||||
bool edgeLeft,
|
||||
bool edgeRight,
|
||||
bool shadeEdge);
|
||||
static void DrawWall(
|
||||
const uint8_t* texture,
|
||||
int16_t x1,
|
||||
int16_t y1,
|
||||
int16_t x2,
|
||||
int16_t y2,
|
||||
bool edgeLeft,
|
||||
bool edgeRight,
|
||||
bool shadeEdge);
|
||||
static void DrawBackground();
|
||||
static uint8_t numQueuedDrawables;
|
||||
static bool isFrustrumClipped(int16_t x, int16_t y);
|
||||
|
||||
private:
|
||||
static QueuedDrawable queuedDrawables[MAX_QUEUED_DRAWABLES];
|
||||
|
||||
// #if WITH_IMAGE_TEXTURES
|
||||
// static void DrawWallSegment(const uint16_t* texture, int16_t x1, int16_t w1, int16_t x2, int16_t w2, uint8_t u1clip, uint8_t u2clip, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// static void DrawWall(const uint16_t* texture, int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// #elif WITH_VECTOR_TEXTURES
|
||||
// static void DrawWallLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint8_t clipLeft, uint8_t clipRight, uint8_t col);
|
||||
// static void DrawWallSegment(const uint8_t* texture, int16_t x1, int16_t w1, int16_t x2, int16_t w2, uint8_t u1clip, uint8_t u2clip, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// static void DrawWall(const uint8_t* texture, int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// #else
|
||||
// static void DrawWallSegment(int16_t x1, int16_t w1, int16_t x2, int16_t w2, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// static void DrawWall(int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool edgeLeft, bool edgeRight, bool shadeEdge);
|
||||
// #endif
|
||||
static void DrawFloorLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2);
|
||||
static void DrawFloorLineInner(int16_t x1, int16_t y1, int16_t x2, int16_t y2);
|
||||
static void DrawFloorLines();
|
||||
|
||||
static void TransformToViewSpace(int16_t x, int16_t y, int16_t& outX, int16_t& outY);
|
||||
static void TransformToScreenSpace(int16_t viewX, int16_t viewZ, int16_t& outX, int16_t& outW);
|
||||
|
||||
static void DrawCell(uint8_t x, uint8_t y);
|
||||
static void DrawCells();
|
||||
static void DrawWeapon();
|
||||
static void DrawHUD();
|
||||
static void DrawBar(uint8_t* screenPtr, const uint8_t* iconData, uint8_t amount, uint8_t max);
|
||||
static void DrawDamageIndicator();
|
||||
|
||||
static void QueueSprite(
|
||||
const uint16_t* data,
|
||||
int8_t x,
|
||||
int8_t y,
|
||||
uint8_t halfSize,
|
||||
uint8_t inverseCameraDistance,
|
||||
bool invert = false);
|
||||
static void RenderQueuedDrawables();
|
||||
};
|
||||
@@ -0,0 +1,484 @@
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Defines.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/Projectile.h"
|
||||
#include "game/Generated/SpriteTypes.h"
|
||||
#include "game/Sounds.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Particle.h"
|
||||
|
||||
Enemy EnemyManager::enemies[maxEnemies];
|
||||
|
||||
const EnemyArchetype Enemy::archetypes[(int)EnemyType::NumEnemyTypes] PROGMEM =
|
||||
{
|
||||
{
|
||||
// Skeleton
|
||||
skeletonSpriteData,
|
||||
50, // hp
|
||||
4, // speed
|
||||
20, // attackStrength
|
||||
3, // attackDuration
|
||||
2, // stunDuration
|
||||
false, // isRanged
|
||||
96, // sprite scale
|
||||
AnchorType::Floor // sprite anchor
|
||||
},
|
||||
{
|
||||
// Mage
|
||||
mageSpriteData,
|
||||
30, // hp
|
||||
5, // speed
|
||||
20, // attackStrength
|
||||
3, // attackDuration
|
||||
2, // stunDuration
|
||||
true, // isRanged
|
||||
96, // sprite scale
|
||||
AnchorType::Floor // sprite anchor
|
||||
},
|
||||
{
|
||||
// Bat -> shotgun sergeant (ranged, hits hard)
|
||||
batSpriteData,
|
||||
20, // hp
|
||||
6, // speed
|
||||
10, // attackStrength
|
||||
2, // attackDuration
|
||||
0, // stunDuration
|
||||
true, // isRanged
|
||||
90, // sprite scale
|
||||
AnchorType::Floor // sprite anchor
|
||||
},
|
||||
{
|
||||
// Spider -> zombieman (ranged, weak)
|
||||
spiderSpriteData,
|
||||
10, // hp
|
||||
5, // speed
|
||||
4, // attackStrength
|
||||
1, // attackDuration
|
||||
0, // stunDuration
|
||||
true, // isRanged
|
||||
80, // sprite scale
|
||||
AnchorType::Floor // sprite anchor
|
||||
}
|
||||
};
|
||||
|
||||
void Enemy::Init(EnemyType initType, int16_t initX, int16_t initY)
|
||||
{
|
||||
state = EnemyState::Idle;
|
||||
type = initType;
|
||||
x = initX;
|
||||
y = initY;
|
||||
frameDelay = 0;
|
||||
targetCellX = x / CELL_SIZE;
|
||||
targetCellY = y / CELL_SIZE;
|
||||
hp = GetArchetype()->GetHP();
|
||||
}
|
||||
|
||||
void Enemy::Damage(uint8_t amount)
|
||||
{
|
||||
if (amount >= hp)
|
||||
{
|
||||
Game::stats.enemyKills[(int)type]++;
|
||||
type = EnemyType::None;
|
||||
Platform::PlaySound(Sounds::Kill);
|
||||
ParticleSystemManager::CreateExplosion(x, y, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
hp -= amount;
|
||||
Platform::PlaySound(Sounds::Hit);
|
||||
state = EnemyState::Stunned;
|
||||
frameDelay = GetArchetype()->GetStunDuration();
|
||||
}
|
||||
}
|
||||
|
||||
const EnemyArchetype* Enemy::GetArchetype() const
|
||||
{
|
||||
if (type == EnemyType::None)
|
||||
return nullptr;
|
||||
return &archetypes[(int)type];
|
||||
}
|
||||
|
||||
int16_t Clamp(int16_t x, int16_t min, int16_t max)
|
||||
{
|
||||
if(x < min)
|
||||
return min;
|
||||
if(x > max)
|
||||
return max;
|
||||
return x;
|
||||
}
|
||||
|
||||
bool Enemy::TryPickCell(int8_t newX, int8_t newY)
|
||||
{
|
||||
if(Map::IsBlocked(newX, newY))// && !engine.map.isDoor(newX, newZ))
|
||||
return false;
|
||||
if(Map::IsBlocked(targetCellX, newY)) // && !engine.map.isDoor(targetCellX, newZ))
|
||||
return false;
|
||||
if(Map::IsBlocked(newX, targetCellY)) // && !engine.map.isDoor(newX, targetCellZ))
|
||||
return false;
|
||||
|
||||
for (Enemy& other : EnemyManager::enemies)
|
||||
{
|
||||
if(this != &other && other.IsValid())
|
||||
{
|
||||
if(other.targetCellX == newX && other.targetCellY == newY)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
targetCellX = newX;
|
||||
targetCellY = newY;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Enemy::TryPickCells(int8_t deltaX, int8_t deltaY)
|
||||
{
|
||||
return TryPickCell(targetCellX + deltaX, targetCellY + deltaY)
|
||||
|| TryPickCell(targetCellX + deltaX, targetCellY)
|
||||
|| TryPickCell(targetCellX, targetCellY + deltaY)
|
||||
|| TryPickCell(targetCellX - deltaX, targetCellY + deltaY)
|
||||
|| TryPickCell(targetCellX + deltaX, targetCellY - deltaY);
|
||||
}
|
||||
|
||||
uint8_t Enemy::GetPlayerCellDistance() const
|
||||
{
|
||||
uint8_t dx = ABS(Game::player.x - x) / CELL_SIZE;
|
||||
uint8_t dy = ABS(Game::player.y - y) / CELL_SIZE;
|
||||
return dx > dy ? dx : dy;
|
||||
}
|
||||
|
||||
void Enemy::PickNewTargetCell()
|
||||
{
|
||||
int8_t deltaX = (int8_t) Clamp((Game::player.x / CELL_SIZE) - targetCellX, -1, 1);
|
||||
int8_t deltaY = (int8_t) Clamp((Game::player.y / CELL_SIZE) - targetCellY, -1, 1);
|
||||
uint8_t dodgeChance = (uint8_t) Random();
|
||||
|
||||
if (GetArchetype()->GetIsRanged() && GetPlayerCellDistance() < 3)
|
||||
{
|
||||
deltaX = -deltaX;
|
||||
deltaY = -deltaY;
|
||||
}
|
||||
|
||||
if(deltaX == 0)
|
||||
{
|
||||
if(dodgeChance < 64)
|
||||
{
|
||||
deltaX = -1;
|
||||
}
|
||||
else if(dodgeChance < 128)
|
||||
{
|
||||
deltaX = 1;
|
||||
}
|
||||
}
|
||||
else if(deltaY == 0)
|
||||
{
|
||||
if(dodgeChance < 64)
|
||||
{
|
||||
deltaY = -1;
|
||||
}
|
||||
else if(dodgeChance < 128)
|
||||
{
|
||||
deltaY = 1;
|
||||
}
|
||||
}
|
||||
|
||||
TryPickCells(deltaX, deltaY);
|
||||
}
|
||||
|
||||
void Enemy::StunMove()
|
||||
{
|
||||
//int16_t targetX = Game::player.x;
|
||||
//int16_t targetY = Game::player.y;
|
||||
//
|
||||
//int16_t maxDelta = 3;
|
||||
//
|
||||
//int16_t deltaX = Clamp(targetX - x, -maxDelta, maxDelta);
|
||||
//int16_t deltaY = Clamp(targetY - y, -maxDelta, maxDelta);
|
||||
//
|
||||
//x -= deltaX;
|
||||
//y -= deltaY;
|
||||
|
||||
// int16_t deltaX = (Random() % 16) - 8;
|
||||
// int16_t deltaY = (Random() % 16) - 8;
|
||||
// x += deltaX;
|
||||
// y += deltaY;
|
||||
}
|
||||
|
||||
bool Enemy::TryMove()
|
||||
{
|
||||
if(Map::IsSolid(targetCellX, targetCellY))
|
||||
{
|
||||
//engine.map.openDoorsAt(targetCellX, targetCellZ, Direction_None);
|
||||
return false;
|
||||
}
|
||||
|
||||
int16_t targetX = (targetCellX * CELL_SIZE) + CELL_SIZE / 2;
|
||||
int16_t targetY = (targetCellY * CELL_SIZE) + CELL_SIZE / 2;
|
||||
|
||||
int16_t maxDelta = GetArchetype()->GetMovementSpeed();
|
||||
|
||||
int16_t deltaX = Clamp(targetX - x, -maxDelta, maxDelta);
|
||||
int16_t deltaY = Clamp(targetY - y, -maxDelta, maxDelta);
|
||||
|
||||
x += deltaX;
|
||||
y += deltaY;
|
||||
|
||||
if(IsOverlappingEntity(Game::player))
|
||||
{
|
||||
if (!GetArchetype()->GetIsRanged())
|
||||
{
|
||||
Game::player.Damage(GetArchetype()->GetAttackStrength());
|
||||
if (Game::player.hp == 0)
|
||||
{
|
||||
Game::stats.killedBy = type;
|
||||
}
|
||||
|
||||
state = EnemyState::Attacking;
|
||||
frameDelay = GetArchetype()->GetAttackDuration();
|
||||
}
|
||||
|
||||
x -= deltaX;
|
||||
y -= deltaY;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(x == targetX && y == targetY)
|
||||
{
|
||||
PickNewTargetCell();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Enemy::FireProjectile(uint8_t angle)
|
||||
{
|
||||
return ProjectileManager::FireProjectile(this, x, y, angle) != nullptr;
|
||||
}
|
||||
|
||||
bool Enemy::TryFireProjectile()
|
||||
{
|
||||
int8_t deltaX = (Game::player.x - x) / CELL_SIZE;
|
||||
int8_t deltaY = (Game::player.y - y) / CELL_SIZE;
|
||||
|
||||
if (deltaX == 0)
|
||||
{
|
||||
if (deltaY < 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_270);
|
||||
}
|
||||
else if (deltaY > 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_90);
|
||||
}
|
||||
}
|
||||
else if (deltaY == 0)
|
||||
{
|
||||
if (deltaX < 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_180);
|
||||
}
|
||||
else if (deltaX > 0)
|
||||
{
|
||||
return FireProjectile(0);
|
||||
}
|
||||
}
|
||||
else if (deltaX == deltaY)
|
||||
{
|
||||
if (deltaX > 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_45);
|
||||
}
|
||||
else
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_180 + FIXED_ANGLE_45);
|
||||
}
|
||||
}
|
||||
else if (deltaX == -deltaY)
|
||||
{
|
||||
if (deltaX > 0)
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_270 + FIXED_ANGLE_45);
|
||||
}
|
||||
else
|
||||
{
|
||||
return FireProjectile(FIXED_ANGLE_90 + FIXED_ANGLE_45);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Enemy::ShouldFireProjectile() const
|
||||
{
|
||||
uint8_t distance = GetPlayerCellDistance();
|
||||
uint8_t chance = 16 / (distance > 0 ? distance : 1);
|
||||
|
||||
return GetArchetype()->GetIsRanged() && (Random() & 0xff) < chance && Map::IsClearLine(x, y, Game::player.x, Game::player.y);
|
||||
}
|
||||
|
||||
void Enemy::Tick()
|
||||
{
|
||||
if (state == EnemyState::Stunned)
|
||||
{
|
||||
StunMove();
|
||||
}
|
||||
|
||||
if (frameDelay > 0)
|
||||
{
|
||||
if ((Game::globalTickFrame & 0xf) == 0)
|
||||
{
|
||||
frameDelay--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case EnemyState::Idle:
|
||||
if (Map::IsClearLine(x, y, Game::player.x, Game::player.y))
|
||||
{
|
||||
Platform::PlaySound(Sounds::SpotPlayer);
|
||||
state = EnemyState::Moving;
|
||||
}
|
||||
break;
|
||||
case EnemyState::Moving:
|
||||
TryMove();
|
||||
|
||||
if (ShouldFireProjectile())
|
||||
{
|
||||
if (TryFireProjectile())
|
||||
{
|
||||
Platform::PlaySound(Sounds::Shoot);
|
||||
state = EnemyState::Attacking;
|
||||
frameDelay = GetArchetype()->GetAttackDuration();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EnemyState::Attacking:
|
||||
state = EnemyState::Moving;
|
||||
break;
|
||||
case EnemyState::Stunned:
|
||||
state = EnemyState::Moving;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyManager::Init()
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
enemy.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyManager::Update()
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
if(enemy.IsValid())
|
||||
{
|
||||
enemy.Tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyManager::Draw()
|
||||
{
|
||||
for(Enemy& enemy : enemies)
|
||||
{
|
||||
if(enemy.IsValid())
|
||||
{
|
||||
bool invert = enemy.GetState() == EnemyState::Stunned && (Renderer::globalRenderFrame & 1);
|
||||
int frameOffset = (enemy.GetType() == EnemyType::Bat || enemy.GetState() == EnemyState::Moving) && (Game::globalTickFrame & 8) == 0 ? 32 : 0;
|
||||
|
||||
const EnemyArchetype* archetype = enemy.GetArchetype();
|
||||
Renderer::DrawObject(archetype->GetSpriteData() + frameOffset, enemy.x, enemy.y, archetype->GetSpriteScale(), archetype->GetSpriteAnchor(), invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyManager::Spawn(EnemyType enemyType, int16_t x, int16_t y)
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
if(!enemy.IsValid())
|
||||
{
|
||||
enemy.Init(enemyType, x, y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Doom-like difficulty curve: early levels are mostly zombiemen,
|
||||
// deeper levels bring sergeants, imps and finally demons
|
||||
static EnemyType PickEnemyType()
|
||||
{
|
||||
uint8_t roll = (uint8_t)(Random() % 100);
|
||||
uint8_t f = Game::floor;
|
||||
|
||||
if (f < 3)
|
||||
{
|
||||
if (roll < 55) return EnemyType::Spider; // zombieman
|
||||
if (roll < 85) return EnemyType::Bat; // sergeant
|
||||
return EnemyType::Mage; // imp
|
||||
}
|
||||
if (f < 6)
|
||||
{
|
||||
if (roll < 30) return EnemyType::Spider;
|
||||
if (roll < 55) return EnemyType::Bat;
|
||||
if (roll < 85) return EnemyType::Mage;
|
||||
return EnemyType::Skeleton; // demon
|
||||
}
|
||||
if (roll < 15) return EnemyType::Spider;
|
||||
if (roll < 40) return EnemyType::Bat;
|
||||
if (roll < 70) return EnemyType::Mage;
|
||||
return EnemyType::Skeleton;
|
||||
}
|
||||
|
||||
void EnemyManager::SpawnEnemies()
|
||||
{
|
||||
for (uint8_t y = 0; y < Map::height; y++)
|
||||
{
|
||||
for (uint8_t x = 0; x < Map::width; x++)
|
||||
{
|
||||
if (Map::GetCellSafe(x, y) == CellType::Monster)
|
||||
{
|
||||
EnemyManager::Spawn(PickEnemyType(), x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2);
|
||||
Map::SetCell(x, y, CellType::Empty);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Enemy* EnemyManager::GetOverlappingEnemy(Entity& entity)
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
if (enemy.IsValid() && enemy.IsOverlappingEntity(entity))
|
||||
{
|
||||
return &enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Enemy* EnemyManager::GetOverlappingEnemy(int16_t x, int16_t y)
|
||||
{
|
||||
for (Enemy& enemy : enemies)
|
||||
{
|
||||
if (enemy.IsValid() && enemy.IsOverlappingPoint(x, y))
|
||||
{
|
||||
return &enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Entity.h"
|
||||
#include "game/Defines.h"
|
||||
#include "game/Draw.h"
|
||||
|
||||
enum class EnemyType : uint8_t
|
||||
{
|
||||
Skeleton,
|
||||
Mage,
|
||||
Bat,
|
||||
Spider,
|
||||
NumEnemyTypes,
|
||||
None = NumEnemyTypes,
|
||||
Exit,
|
||||
};
|
||||
|
||||
enum class EnemyState : uint8_t
|
||||
{
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Stunned,
|
||||
Dying,
|
||||
Dead
|
||||
};
|
||||
|
||||
struct EnemyArchetype
|
||||
{
|
||||
const uint16_t* spriteData;
|
||||
|
||||
uint8_t hp;
|
||||
uint8_t movementSpeed;
|
||||
uint8_t attackStrength;
|
||||
uint8_t attackDuration;
|
||||
uint8_t stunDuration;
|
||||
uint8_t isRanged;
|
||||
uint8_t spriteScale;
|
||||
AnchorType spriteAnchor;
|
||||
|
||||
const uint16_t* GetSpriteData() const {return static_cast<const uint16_t*>(pgm_read_ptr_safe(&spriteData));}
|
||||
uint8_t GetHP() const { return pgm_read_byte(&hp); }
|
||||
uint8_t GetMovementSpeed() const { return pgm_read_byte(&movementSpeed); }
|
||||
uint8_t GetAttackStrength() const { return pgm_read_byte(&attackStrength); }
|
||||
uint8_t GetAttackDuration() const { return pgm_read_byte(&attackDuration); }
|
||||
uint8_t GetStunDuration() const { return pgm_read_byte(&stunDuration); }
|
||||
bool GetIsRanged() const { return pgm_read_byte(&isRanged) != 0; }
|
||||
uint8_t GetSpriteScale() const { return pgm_read_byte(&spriteScale); }
|
||||
AnchorType GetSpriteAnchor() const { return (AnchorType) pgm_read_byte(&spriteAnchor); }
|
||||
};
|
||||
|
||||
class Enemy : public Entity
|
||||
{
|
||||
public:
|
||||
void Init(EnemyType type, int16_t x, int16_t y);
|
||||
void Tick();
|
||||
bool IsValid() const { return type != EnemyType::None; }
|
||||
void Damage(uint8_t amount);
|
||||
void Clear() { type = EnemyType::None; }
|
||||
const EnemyArchetype* GetArchetype() const;
|
||||
EnemyState GetState() const { return state; }
|
||||
EnemyType GetType() const { return type; }
|
||||
|
||||
private:
|
||||
static const EnemyArchetype archetypes[(int)EnemyType::NumEnemyTypes];
|
||||
|
||||
bool ShouldFireProjectile() const;
|
||||
bool FireProjectile(uint8_t angle);
|
||||
bool TryMove();
|
||||
void StunMove();
|
||||
bool TryFireProjectile();
|
||||
void PickNewTargetCell();
|
||||
bool TryPickCells(int8_t deltaX, int8_t deltaY);
|
||||
bool TryPickCell(int8_t newX, int8_t newY);
|
||||
uint8_t GetPlayerCellDistance() const;
|
||||
|
||||
EnemyType type : 3;
|
||||
EnemyState state : 3;
|
||||
uint8_t frameDelay : 2;
|
||||
uint8_t hp;
|
||||
uint8_t targetCellX, targetCellY;
|
||||
};
|
||||
|
||||
class EnemyManager
|
||||
{
|
||||
public:
|
||||
static constexpr int maxEnemies = 24; //24;
|
||||
static Enemy enemies[maxEnemies];
|
||||
|
||||
static void Spawn(EnemyType enemyType, int16_t x, int16_t y);
|
||||
static void SpawnEnemies();
|
||||
|
||||
static Enemy* GetOverlappingEnemy(Entity& entity);
|
||||
static Enemy* GetOverlappingEnemy(int16_t x, int16_t y);
|
||||
|
||||
static void Init();
|
||||
static void Draw();
|
||||
static void Update();
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "game/Entity.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/Map.h"
|
||||
|
||||
#define ENTITY_SIZE 192
|
||||
|
||||
bool Entity::IsOverlappingEntity(const Entity& other) const
|
||||
{
|
||||
return (x >= other.x - ENTITY_SIZE && x <= other.x + ENTITY_SIZE
|
||||
&& y >= other.y - ENTITY_SIZE && y <= other.y + ENTITY_SIZE);
|
||||
}
|
||||
|
||||
bool Entity::IsOverlappingPoint(int16_t pointX, int16_t pointY) const
|
||||
{
|
||||
return (pointX >= x - ENTITY_SIZE / 2 && pointX <= x + ENTITY_SIZE / 2
|
||||
&& pointY >= y - ENTITY_SIZE / 2 && pointY <= y + ENTITY_SIZE / 2);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Entity
|
||||
{
|
||||
public:
|
||||
bool IsOverlappingPoint(int16_t pointX, int16_t pointY) const;
|
||||
bool IsOverlappingEntity(const Entity& other) const;
|
||||
|
||||
int16_t x, y;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// #include "FixedMath.h"
|
||||
|
||||
// static uint16_t xs = 1;
|
||||
|
||||
// uint16_t Random() {
|
||||
// xs ^= xs << 7;
|
||||
// xs ^= xs >> 9;
|
||||
// xs ^= xs << 8;
|
||||
// return xs;
|
||||
// }
|
||||
|
||||
// void SeedRandom() {
|
||||
// uint32_t r = furi_hal_random_get();
|
||||
// xs = (uint16_t)(r ^ (r >> 16));
|
||||
// }
|
||||
|
||||
// void SeedRandom(uint16_t seed) {
|
||||
// xs = seed | 1;
|
||||
// }
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#if defined(_WIN32)
|
||||
#include <math.h>
|
||||
#endif
|
||||
#include "game/Defines.h"
|
||||
|
||||
#define FIXED_SHIFT 8
|
||||
#define FIXED_ONE (1 << FIXED_SHIFT)
|
||||
#define INT_TO_FIXED(x) ((x) * FIXED_ONE)
|
||||
#define FIXED_TO_INT(x) ((x) >> 8)
|
||||
#define FLOAT_TO_FIXED(x) ((int16_t)((x) * FIXED_ONE))
|
||||
|
||||
#ifndef ABS
|
||||
#define ABS(x) (((x) < 0) ? -(x) : (x))
|
||||
#endif
|
||||
|
||||
#define FIXED_ANGLE_MAX 256
|
||||
#define FIXED_ANGLE_WRAP(x) ((x) & 255)
|
||||
#define FIXED_ANGLE_45 (FIXED_ANGLE_90 / 2)
|
||||
#define FIXED_ANGLE_90 (FIXED_ANGLE_MAX / 4)
|
||||
#define FIXED_ANGLE_180 (FIXED_ANGLE_90 * 2)
|
||||
#define FIXED_ANGLE_270 (FIXED_ANGLE_90 * 3)
|
||||
#define FIXED_ANGLE_TO_RADIANS(x) ((x) * (2.0f * 3.141592654f / FIXED_ANGLE_MAX))
|
||||
|
||||
extern const int16_t sinTable[FIXED_ANGLE_MAX] PROGMEM;
|
||||
|
||||
inline int16_t FixedSin(uint8_t angle) {
|
||||
return pgm_read_word(&sinTable[angle]);
|
||||
}
|
||||
|
||||
inline int16_t FixedCos(uint8_t angle) {
|
||||
return pgm_read_word(&sinTable[FIXED_ANGLE_WRAP(FIXED_ANGLE_90 - angle)]);
|
||||
}
|
||||
|
||||
// uint16_t Random();
|
||||
// void SeedRandom();
|
||||
// void SeedRandom(uint16_t seed);
|
||||
|
||||
inline uint16_t Random() {
|
||||
uint32_t r = furi_hal_random_get();
|
||||
return (uint16_t)(r ^ (r >> 16));
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#include <stdint.h>
|
||||
#include "game/Defines.h"
|
||||
#include "game/Font.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Generated/SpriteTypes.h"
|
||||
|
||||
static inline uint8_t v3(uint8_t m)
|
||||
{
|
||||
return m | (m << 1) | (m >> 1);
|
||||
}
|
||||
|
||||
static inline void apply4(uint8_t* dst, uint8_t a, uint8_t b, uint8_t c, uint8_t d, uint8_t xorMask)
|
||||
{
|
||||
if (xorMask)
|
||||
{
|
||||
dst[0] |= a;
|
||||
dst[1] |= b;
|
||||
dst[2] |= c;
|
||||
dst[3] |= d;
|
||||
}
|
||||
else
|
||||
{
|
||||
dst[0] &= ~a;
|
||||
dst[1] &= ~b;
|
||||
dst[2] &= ~c;
|
||||
dst[3] &= ~d;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void apply1(uint8_t* dst, uint8_t m, uint8_t xorMask)
|
||||
{
|
||||
if (xorMask) *dst |= m;
|
||||
else *dst &= ~m;
|
||||
}
|
||||
|
||||
void Font::PrintString(const char* str, uint8_t line, uint8_t x, uint8_t colour)
|
||||
{
|
||||
uint8_t* p = Platform::GetScreenBuffer() + DISPLAY_WIDTH * line + x;
|
||||
uint8_t xorMask = (colour == COLOUR_BLACK) ? 0 : 0xff;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
char c = *str++;
|
||||
if (!c) break;
|
||||
DrawChar(p, c, xorMask);
|
||||
p += glyphWidth;
|
||||
}
|
||||
}
|
||||
|
||||
void Font::PrintInt(uint16_t val, uint8_t line, uint8_t x, uint8_t colour)
|
||||
{
|
||||
uint8_t* p = Platform::GetScreenBuffer() + DISPLAY_WIDTH * line + x;
|
||||
uint8_t xorMask = (colour == COLOUR_BLACK) ? 0 : 0xff;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
DrawChar(p, '0', xorMask);
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[5];
|
||||
int n = 0;
|
||||
|
||||
while (val && n < 5)
|
||||
{
|
||||
buf[n++] = (char)('0' + (val % 10));
|
||||
val /= 10;
|
||||
}
|
||||
|
||||
while (n--)
|
||||
{
|
||||
DrawChar(p, buf[n], xorMask);
|
||||
p += glyphWidth;
|
||||
}
|
||||
}
|
||||
|
||||
void Font::DrawChar(uint8_t* p, char c, uint8_t xorMask)
|
||||
{
|
||||
uint8_t uc = (uint8_t)c;
|
||||
if (uc < firstGlyphIndex) return;
|
||||
|
||||
const uint8_t* f = fontPageData + glyphWidth * (uc - firstGlyphIndex);
|
||||
|
||||
uint8_t i0 = ~f[0];
|
||||
uint8_t i1 = ~f[1];
|
||||
uint8_t i2 = ~f[2];
|
||||
uint8_t i3 = ~f[3];
|
||||
|
||||
uint8_t t0 = v3(i0 | i1);
|
||||
uint8_t t1 = v3(i0 | i1 | i2);
|
||||
uint8_t t2 = v3(i1 | i2 | i3);
|
||||
uint8_t t3 = v3(i2 | i3);
|
||||
|
||||
uint8_t r0 = t0 & ~i0;
|
||||
uint8_t r1 = t1 & ~i1;
|
||||
uint8_t r2 = t2 & ~i2;
|
||||
uint8_t r3 = t3 & ~i3;
|
||||
|
||||
uint8_t outlineMask = xorMask ^ 0xff;
|
||||
|
||||
// The outline spills one byte to each side of the glyph. Clamp it to the
|
||||
// screen buffer: with x == 0 on the first line `p - 1` lands on the heap
|
||||
// block header of FlipperState (back_buffer is its first member) and the
|
||||
// `|=`/`&=` write corrupts the allocator metadata -- the game keeps
|
||||
// running fine but the firmware crashes/hangs later, when the app frees
|
||||
// its state on exit.
|
||||
uint8_t* bufStart = Platform::GetScreenBuffer();
|
||||
uint8_t* bufEnd = bufStart + (DISPLAY_WIDTH * DISPLAY_HEIGHT / 8);
|
||||
|
||||
if (p - 1 >= bufStart) apply1(p - 1, v3(i0), outlineMask);
|
||||
apply4(p, r0, r1, r2, r3, outlineMask);
|
||||
if (p + 4 < bufEnd) apply1(p + 4, v3(i3), outlineMask);
|
||||
|
||||
apply4(p, i0, i1, i2, i3, xorMask);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Defines.h"
|
||||
|
||||
#define FONT_WIDTH 4
|
||||
#define FONT_HEIGHT 6
|
||||
|
||||
class Font
|
||||
{
|
||||
public:
|
||||
static constexpr int glyphWidth = 4;
|
||||
static constexpr int glyphHeight = 8;
|
||||
static constexpr int firstGlyphIndex = 32;
|
||||
|
||||
static void PrintString(const char* str, uint8_t line, uint8_t x, uint8_t colour = COLOUR_BLACK);
|
||||
static void PrintInt(uint16_t value, uint8_t line, uint8_t x, uint8_t xorMask = COLOUR_BLACK);
|
||||
|
||||
private:
|
||||
static void DrawChar(uint8_t* screenPtr, char c, uint8_t xorMask);
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
#include "game/Defines.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/Projectile.h"
|
||||
#include "game/Particle.h"
|
||||
#include "game/MapGenerator.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Entity.h"
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Menu.h"
|
||||
|
||||
Player Game::player;
|
||||
const char* Game::displayMessage = nullptr;
|
||||
uint8_t Game::displayMessageTime = 0;
|
||||
Game::State Game::state = Game::State::Menu;
|
||||
uint8_t Game::floor = 1;
|
||||
uint8_t Game::globalTickFrame = 0;
|
||||
Stats Game::stats;
|
||||
Menu Game::menu;
|
||||
|
||||
void Game::Init() {
|
||||
menu.Init();
|
||||
ParticleSystemManager::Init();
|
||||
ProjectileManager::Init();
|
||||
EnemyManager::Init();
|
||||
}
|
||||
|
||||
bool Game::InMenu() {
|
||||
return (state != State::InGame);
|
||||
}
|
||||
|
||||
void Game::GoToMenu() {
|
||||
Game::stats.killedBy = EnemyType::Exit;
|
||||
SwitchState(State::FadeOut);
|
||||
}
|
||||
|
||||
void Game::StartGame() {
|
||||
floor = 1;
|
||||
stats.Reset();
|
||||
player.Init();
|
||||
SwitchState(State::EnteringLevel);
|
||||
}
|
||||
|
||||
void Game::SwitchState(State newState) {
|
||||
if(state != newState) {
|
||||
state = newState;
|
||||
menu.ResetTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void Game::ShowMessage(const char* message) {
|
||||
constexpr uint8_t messageDisplayTime = 90;
|
||||
|
||||
displayMessage = message;
|
||||
displayMessageTime = messageDisplayTime;
|
||||
}
|
||||
|
||||
void Game::NextLevel() {
|
||||
if(floor == 10) {
|
||||
GameOver();
|
||||
} else {
|
||||
floor++;
|
||||
SwitchState(State::EnteringLevel);
|
||||
}
|
||||
}
|
||||
|
||||
void Game::StartLevel() {
|
||||
ParticleSystemManager::Init();
|
||||
ProjectileManager::Init();
|
||||
EnemyManager::Init();
|
||||
MapGenerator::Generate();
|
||||
EnemyManager::SpawnEnemies();
|
||||
|
||||
player.NextLevel();
|
||||
SwitchState(State::InGame);
|
||||
}
|
||||
|
||||
void Game::Draw() {
|
||||
switch(state) {
|
||||
case State::Menu:
|
||||
menu.Draw();
|
||||
break;
|
||||
case State::EnteringLevel:
|
||||
menu.DrawEnteringLevel();
|
||||
break;
|
||||
// case State::TransitionToLevel:
|
||||
// menu.TransitionToLevel();
|
||||
// break;
|
||||
case State::InGame: {
|
||||
Renderer::camera.x = player.x;
|
||||
Renderer::camera.y = player.y;
|
||||
Renderer::camera.angle = player.angle;
|
||||
Renderer::Render();
|
||||
} break;
|
||||
case State::GameOver:
|
||||
menu.DrawGameOver();
|
||||
break;
|
||||
case State::FadeOut:
|
||||
menu.FadeOut();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Game::TickInGame() {
|
||||
if(displayMessageTime > 0) {
|
||||
displayMessageTime--;
|
||||
if(displayMessageTime == 0) displayMessage = nullptr;
|
||||
}
|
||||
|
||||
player.Tick();
|
||||
|
||||
ProjectileManager::Update();
|
||||
ParticleSystemManager::Update();
|
||||
EnemyManager::Update();
|
||||
|
||||
if(Map::GetCellSafe(player.x / CELL_SIZE, player.y / CELL_SIZE) == CellType::Exit) {
|
||||
NextLevel();
|
||||
}
|
||||
|
||||
if(player.hp == 0) {
|
||||
GameOver();
|
||||
}
|
||||
}
|
||||
|
||||
void Game::Tick() {
|
||||
globalTickFrame++;
|
||||
|
||||
switch(state) {
|
||||
case State::InGame:
|
||||
TickInGame();
|
||||
return;
|
||||
case State::EnteringLevel:
|
||||
menu.TickEnteringLevel();
|
||||
return;
|
||||
case State::Menu:
|
||||
menu.Tick();
|
||||
return;
|
||||
case State::GameOver:
|
||||
menu.TickGameOver();
|
||||
return;
|
||||
// case State::TransitionToLevel:
|
||||
// return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Game::GameOver() {
|
||||
SwitchState(State::FadeOut);
|
||||
}
|
||||
|
||||
void Stats::Reset() {
|
||||
killedBy = EnemyType::None;
|
||||
chestsOpened = 0;
|
||||
coinsCollected = 0;
|
||||
crownsCollected = 0;
|
||||
scrollsCollected = 0;
|
||||
|
||||
for(uint8_t& killCounter : enemyKills) {
|
||||
killCounter = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Defines.h"
|
||||
#include "game/Player.h"
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Menu.h"
|
||||
|
||||
class Entity;
|
||||
|
||||
struct Stats {
|
||||
EnemyType killedBy;
|
||||
uint8_t enemyKills[(int)EnemyType::NumEnemyTypes];
|
||||
uint8_t chestsOpened;
|
||||
uint8_t crownsCollected;
|
||||
uint8_t scrollsCollected;
|
||||
uint8_t coinsCollected;
|
||||
|
||||
void Reset();
|
||||
};
|
||||
|
||||
class Game {
|
||||
public:
|
||||
static Menu menu;
|
||||
static uint8_t globalTickFrame;
|
||||
|
||||
enum class State : uint8_t {
|
||||
Menu,
|
||||
EnteringLevel,
|
||||
InGame,
|
||||
GameOver,
|
||||
FadeOut,
|
||||
// TransitionToLevel
|
||||
};
|
||||
|
||||
static void Init();
|
||||
static void Tick();
|
||||
static void Draw();
|
||||
|
||||
static bool InMenu();
|
||||
static void GoToMenu();
|
||||
|
||||
static void StartGame();
|
||||
static void StartLevel();
|
||||
static void NextLevel();
|
||||
static void GameOver();
|
||||
|
||||
static void SwitchState(State newState);
|
||||
|
||||
static void ShowMessage(const char* message);
|
||||
|
||||
static Player player;
|
||||
|
||||
static const char* displayMessage;
|
||||
static uint8_t displayMessageTime;
|
||||
static uint8_t floor;
|
||||
|
||||
static Stats stats;
|
||||
|
||||
private:
|
||||
static void TickInGame();
|
||||
static State state;
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
// Auto-generated by tools/extract_doom_assets.py
|
||||
// Derived at build time from the user's local Doom shareware WAD.
|
||||
// Do not commit WAD-derived data to public repositories.
|
||||
|
||||
constexpr uint8_t skeletonSpriteData_numFrames = 2;
|
||||
extern const uint16_t skeletonSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0x1fc,0x144,0xffc,0xfac,0x7ffe,0x416,0x3ffe,0x2e,0x7ff,0x455,0x7fe,0x3e,
|
||||
0x4ffe,0x4476,0xfffe,0x2022,0xfe3e,0x7016,0xc9fe,0x802,0x1e0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x3f0,0x150,0x3fc,0x200,0x103e,0x1016,0x7ffe,0x82e,0x3ffe,0x1476,0x17ff,0x23a,
|
||||
0x7ff,0x41f,0x4ffe,0x23e,0xfffe,0x7056,0xfe1e,0x2802,0x3fe,0x114,0x3f0,0x2a0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t mageSpriteData_numFrames = 2;
|
||||
extern const uint16_t mageSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x3e0,0x0,0x78,0x8,0x38,0x8,0xbffe,0x414,0xffff,0x800a,
|
||||
0x83ff,0x15,0x7ffe,0xa,0x7ff8,0x1010,0x238,0x8,0x3f0,0x10,0x1c0,0x80,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x1e0,0x0,0x1f8,0x8,0x38,0x8,0x1ffc,0x8c,0x7fff,0x1107,0x17ff,0x8f,
|
||||
0xffff,0x517,0xfff8,0xa618,0x1b8,0x8,0xf8,0xa8,0x60,0x40,0x0,0x0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t batSpriteData_numFrames = 2;
|
||||
extern const uint16_t batSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0xf8,0x20,0x1f8,0x70,0xffc,0x88,0x1fff,0x47,0xffff,0x8023,
|
||||
0xfffe,0x4406,0x3f8,0x0,0x38,0x10,0x10,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x60,0x20,0x1f8,0x50,0xff8,0x8,0x5fff,0x47,0x7fff,0x23,
|
||||
0x7fff,0x7,0xfff8,0x0,0x378,0x10,0x78,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t spiderSpriteData_numFrames = 2;
|
||||
extern const uint16_t spiderSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0x78,0x50,0xf8,0x78,0x7fc,0x4c,0xfff,0xa,0xffff,0x17,0xffff,0x2a2a,
|
||||
0x3ffc,0x1004,0x38,0x8,0x30,0x10,0x30,0x20,0x8,0x0,0x8,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x60,0x40,0x1f8,0x78,0x7f8,0x58,0x1fff,0x20a,0x7fff,0x17,0x3fff,0x2a,
|
||||
0xfff8,0x10,0x778,0x8,0x78,0x10,0x38,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t projectileSpriteData_numFrames = 1;
|
||||
extern const uint16_t projectileSpriteData[] PROGMEM =
|
||||
{
|
||||
0x7e0,0x100,0x1ff8,0x2a0,0x3ffc,0x550,0x7ffe,0xaa8,0x7ffe,0x554,0xffff,0x2bea,0xffff,0x57f4,0xffff,0x2ffa,
|
||||
0xffff,0x57f4,0xffff,0x2bea,0xffff,0x57d4,0x7ffe,0x2baa,0x7ffe,0x1554,0x3ffc,0xaa0,0x1ff8,0x540,0x7e0,0x280
|
||||
};
|
||||
constexpr uint8_t enemyProjectileSpriteData_numFrames = 1;
|
||||
extern const uint16_t enemyProjectileSpriteData[] PROGMEM =
|
||||
{
|
||||
0x7e0,0x0,0x1ff8,0x220,0x3ffc,0x554,0x7ffe,0xaaa,0x7ffe,0x1554,0xffff,0x2fea,0xffff,0x17f4,0xffff,0x2fea,
|
||||
0xffff,0x57f5,0xffff,0x2bea,0xffff,0x57f4,0x7ffe,0x2fea,0x7ffe,0x15d4,0x3ffc,0x2aa8,0x1ff8,0x550,0x7e0,0x280
|
||||
};
|
||||
constexpr uint8_t torchSpriteData1_numFrames = 1;
|
||||
extern const uint16_t torchSpriteData1[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x8000,0x0,0xc0f0,0xc020,
|
||||
0xc0f8,0x4070,0x8000,0x8000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t torchSpriteData2_numFrames = 1;
|
||||
extern const uint16_t torchSpriteData2[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x8000,0x8000,0xc070,0xc050,0xfff8,0xea38,
|
||||
0xc070,0xc050,0x8000,0x8000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t urnSpriteData_numFrames = 1;
|
||||
extern const uint16_t urnSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0x7ffe,0x7ffe,0xffff,0xffee,0xffff,0x5dfd,0xffff,0xaaaa,0xffff,0x5557,0xffff,0x8aa,
|
||||
0xffff,0x5547,0xffff,0xb,0xffff,0x1101,0xffff,0x3,0xffff,0x1,0xffff,0x0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t potionSpriteData_numFrames = 1;
|
||||
extern const uint16_t potionSpriteData[] PROGMEM =
|
||||
{
|
||||
0xfffc,0x5554,0xfffe,0xafbe,0xffff,0x575f,0xffff,0xaebf,0xffff,0x555f,0xffff,0xa2bf,0xffff,0x57f,0xffff,0x82ff,
|
||||
0xffff,0x55f,0xffff,0xa2bf,0xffff,0x555f,0xffff,0xaabf,0xffff,0x575f,0xfffe,0xaebe,0xfffc,0x1554,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t chestSpriteData_numFrames = 1;
|
||||
extern const uint16_t chestSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0xfffc,0x1554,0xffff,0xaebe,0xffff,0x477f,0xfffe,0xaaae,0xfffc,0x4574,0xfffc,0xa0bc,
|
||||
0xfffc,0x457c,0xfffc,0xaaac,0xfffe,0x5776,0xffff,0xaabe,0xffff,0x457e,0xfffc,0x22a0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t chestOpenSpriteData_numFrames = 1;
|
||||
extern const uint16_t chestOpenSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0xfffe,0xaaaa,0xffff,0x5557,0xffff,0x2aab,0xffff,0x5,0xffff,0x28ab,0xffff,0x1557,0xffff,0x2b,
|
||||
0xffff,0x1555,0xffff,0x28ab,0xffff,0x5,0xffff,0x2aab,0xffff,0x5557,0xfffe,0xaaaa,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t scrollSpriteData_numFrames = 1;
|
||||
extern const uint16_t scrollSpriteData[] PROGMEM =
|
||||
{
|
||||
0x3fc0,0x1540,0x7ff0,0x2aa0,0xfff8,0x5550,0xfffc,0xe2a8,0x3ffc,0x1d4,0x3ffe,0x3fa,0xfffe,0x45fc,0xfffe,0xabfe,
|
||||
0xfffe,0x55fc,0xfffe,0x2bfe,0x3ffe,0x35c,0x3ffc,0x2a8,0xfffc,0xf154,0xfff8,0xaaa8,0x7ff0,0x5550,0x3fc0,0x2a80
|
||||
};
|
||||
constexpr uint8_t coinsSpriteData_numFrames = 1;
|
||||
extern const uint16_t coinsSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0x0,0x0,0x1e00,0x1000,0x3f80,0x2000,0x7fc0,0x4500,0xffc0,0x8000,0xfffe,0x4000,0xffff,0x2,
|
||||
0xffff,0x15,0xfffe,0x82,0xffc0,0x540,0x7fc0,0xa80,0x3f80,0x1500,0x1e00,0x800,0x0,0x0,0x0,0x0
|
||||
};
|
||||
constexpr uint8_t crownSpriteData_numFrames = 1;
|
||||
extern const uint16_t crownSpriteData[] PROGMEM =
|
||||
{
|
||||
0x300,0x100,0x380,0x380,0x780,0x180,0x1f80,0x380,0x7f80,0x4780,0xff80,0xae80,0xff80,0x5600,0xff00,0xaa00,
|
||||
0xff00,0x5400,0xff80,0xaa00,0xff80,0x4700,0x7f80,0x380,0x1f80,0x580,0x780,0x380,0x380,0x180,0x300,0x200
|
||||
};
|
||||
constexpr uint8_t signSpriteData_numFrames = 1;
|
||||
extern const uint16_t signSpriteData[] PROGMEM =
|
||||
{
|
||||
0x0,0x0,0xc000,0x0,0xc000,0x4000,0xc000,0x8000,0xc000,0xc000,0xc000,0x8000,0xc000,0x4000,0xc000,0x8000,
|
||||
0xc000,0x4000,0xe000,0xe000,0xe000,0x4000,0xc000,0x8000,0xc000,0x4000,0xc000,0xc000,0x0,0x0,0x0,0x0
|
||||
};
|
||||
extern const uint8_t handSpriteData1[] PROGMEM =
|
||||
{
|
||||
0x2e,0x1c,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x0,0xe0,0x8,0xf8,0x4,0xfc,
|
||||
0x0,0xfc,0x0,0xff,0x8,0xfc,0x4,0xfc,0x8,0xf8,0x0,0xe0,0x80,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x80,0x80,0x40,0xc0,0x80,0xc0,0x40,0xc0,0xa0,0xe0,0x40,0xe0,
|
||||
0xa0,0xe0,0x10,0xf0,0x0,0xf0,0x0,0xf0,0x0,0xf8,0x10,0xfe,0x0,0xff,0x0,0xff,0x0,0xff,0x10,0xff,0x0,0xff,0x0,0xff,
|
||||
0x0,0xff,0x10,0xff,0x0,0xff,0x0,0xff,0x0,0xff,0x10,0xfe,0x0,0xf8,0x10,0xf0,0x0,0xe0,0x0,0xe0,0x80,0xc0,0x0,0x80,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0xe0,0x80,0xfe,0x40,0xff,0xaa,0xff,0x55,0xff,0xaa,0xff,0x57,0xff,0x2f,0xff,0x5,0xff,0x0,0xff,0x0,0xff,
|
||||
0x20,0xff,0x60,0xff,0x62,0xff,0x60,0xff,0x60,0xff,0x60,0xff,0x20,0xff,0x20,0xff,0x20,0xff,0x0,0xff,0x20,0xff,0x20,0xff,
|
||||
0x20,0xff,0x60,0xff,0x60,0xff,0x60,0xff,0x22,0xff,0x60,0xff,0x20,0xff,0x50,0xff,0x0,0xff,0x1,0xff,0x0,0xfe,0x40,0xe0,
|
||||
0x80,0xc0,0x0,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xa,0xe,0x5,0xf,
|
||||
0xa,0xf,0x5,0xf,0x2,0xf,0x1,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,
|
||||
0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,
|
||||
0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,0x0,0xf,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
extern const uint8_t handSpriteData2[] PROGMEM =
|
||||
{
|
||||
0x2e,0x26,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x2,0x6,0x45,0xe7,
|
||||
0xa8,0xff,0x4,0xff,0xa,0xff,0x5,0xff,0xa,0xff,0x45,0xff,0xae,0xff,0x3f,0x3f,0xf,0xf,0x3,0x3,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x3,0xf,0xf,0x3f,0x3f,0xae,0xff,0x45,0xff,0xa,0xff,0x4,0xff,
|
||||
0xa,0xff,0x4,0xff,0xaa,0xff,0x15,0x37,0x2,0x6,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,
|
||||
0x0,0x1,0x0,0x1,0x0,0x1,0x0,0x1,0x0,0x1,0x0,0x0,0x0,0x0,0x0,0x80,0xa0,0xe0,0x50,0xf0,0xa0,0xf0,0x44,0xfc,
|
||||
0xa0,0xf0,0x50,0xf0,0xa0,0xe0,0x0,0x80,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x1,0x0,0x3,0x0,0x3,0x2,0x3,0x1,0x1,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x80,0x80,0x80,0x80,0x80,0x40,0xc0,
|
||||
0x80,0xc0,0x40,0xc0,0xa0,0xe0,0x50,0xf8,0xaa,0xfe,0x55,0xff,0xaa,0xff,0x55,0xff,0xaa,0xff,0x55,0xff,0xaa,0xff,0x55,0xff,
|
||||
0xaa,0xff,0x55,0xff,0xaa,0xfe,0x50,0xf8,0xa0,0xe0,0x40,0xc0,0x80,0x80,0x0,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,
|
||||
0xa8,0xf8,0x54,0xfe,0xaa,0xfe,0x55,0xff,0xff,0xff,0xfd,0xff,0xff,0xff,0x1d,0xff,0x8a,0xff,0x55,0xff,0xaa,0xff,0xd5,0xff,
|
||||
0x88,0xff,0x88,0xff,0x88,0xff,0x80,0xff,0x80,0xff,0x80,0xff,0x80,0xff,0x0,0xff,0x80,0xff,0x80,0xff,0x80,0xff,0x80,0xff,
|
||||
0x88,0xff,0x88,0xff,0x88,0xff,0xd5,0xff,0xaa,0xff,0x51,0xff,0x82,0xff,0x14,0xfe,0xa8,0xf8,0x0,0x80,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x28,0x38,0x15,0x3f,0x2a,0x3f,0x1d,0x3f,
|
||||
0x2a,0x3f,0x17,0x3f,0xb,0x3f,0x1,0x3f,0x8,0x3f,0x5,0x3f,0x0,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x1,0x3f,0x1,0x3f,
|
||||
0x3,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x0,0x3f,0x1,0x3f,0x3,0x3f,0x1,0x3f,
|
||||
0x1,0x3f,0x1,0x3f,0x1,0x3f,0x1,0x3f,0x0,0x3f,0x5,0x3f,0x0,0x3f,0x5,0x3f,0xa,0x3f,0x14,0x3e,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0
|
||||
};
|
||||
extern const uint8_t titleBitmapData[] PROGMEM =
|
||||
{
|
||||
0x80,0x40,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xf,0xf,0xf,0xf,0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,
|
||||
0x0,0x0,0x0,0x0,0xf,0xf,0xf,0xf,0xff,0xff,0xff,0xff,0xf,0xf,0xf,0xf,0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,
|
||||
0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0xf0,0x0,0x0,0x0,0x0,0xf,0xf,0xf,0xf,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0xf,0xf,0xf,0xf,0xff,0xff,0xff,0xff,0xf,0xf,0xf,0xf,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0xf0,0xf0,0xf0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
0xff,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,0x0,0x0,0x0,0x0,0xf0,0xf0,0xf0,0xf0,
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xaf,0x5f,0xaf,0x5f,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,
|
||||
0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,
|
||||
0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xa0,0x50,0xff,0xff,0xff,0xff,0xaa,0x55,0xaa,0x55,
|
||||
0xaa,0x55,0xaa,0x55,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xfa,0xf5,0xfa,0xf5,0xaa,0x55,0xaa,0x55,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,
|
||||
0xaa,0x55,0xaa,0x55,0xfa,0xf5,0xfa,0xf5,0xff,0xff,0xff,0xff,0xfa,0xf5,0xfa,0xf5,0xaa,0x55,0xaa,0x55,0xaf,0x5f,0xaf,0x5f,
|
||||
0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaf,0x5f,0xaa,0x55,0xaa,0x55,0xfa,0xf5,0xfa,0xf5,0xff,0xff,0xff,0xff,0xaa,0x55,0xaa,0x55,
|
||||
0xaa,0x55,0xaa,0x55,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
|
||||
};
|
||||
extern const uint8_t heartSpriteData[] PROGMEM =
|
||||
{
|
||||
0x1c,0x14,0x77,0x41,0x77,0x14,0x1c,0x0
|
||||
};
|
||||
extern const uint8_t manaSpriteData[] PROGMEM =
|
||||
{
|
||||
0x7e,0x7f,0x7e,0x0,0x7f,0x7e,0x7e,0x0
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// Engine assets kept from FlipperCatacombs (non-Doom): gates, wall texture, font
|
||||
// Generated from Images/entrance.png
|
||||
constexpr uint8_t entranceSpriteData_numFrames = 1;
|
||||
extern const uint16_t entranceSpriteData[] PROGMEM =
|
||||
{
|
||||
0x4,0x0,0xe,0x0,0xe,0x0,0x1f,0x0,0x3ff,0x3fc,0x9f,0x90,0x9f,0x90,0x9f,0x90,0x9f,0x90,0x9f,0x90,0x9f,0x90,0x3ff,0x3fc,0x1f,0x0,0xe,0x0,0xe,0x0,0x4,0x0
|
||||
};
|
||||
// Generated from Images/exit.png
|
||||
constexpr uint8_t exitSpriteData_numFrames = 1;
|
||||
extern const uint16_t exitSpriteData[] PROGMEM =
|
||||
{
|
||||
0x2000,0x0,0x7000,0x0,0x7000,0x0,0xf800,0x0,0xffc0,0x3fc0,0xf900,0x900,0xf900,0x900,0xf900,0x900,0xf900,0x900,0xf900,0x900,0xf900,0x900,0xffc0,0x3fc0,0xf800,0x0,0x7000,0x0,0x7000,0x0,0x2000,0x0
|
||||
};
|
||||
// Generated from Images/textures.png
|
||||
constexpr uint8_t wallTextureData_numTextures = 1;
|
||||
extern const uint16_t wallTextureData[] PROGMEM =
|
||||
{
|
||||
0xf3cf,0xf3cf,0xf3cf,0xf3c0,0xf3c0,0xf3cf,0x3cf,0x3cf,0xf3cf,0xf3cf,0xf00f,0xf00f,0xf3cf,0xf3cf,0xf3cf,0xf3cf
|
||||
};
|
||||
// Generated from Images/font.png
|
||||
extern const uint8_t fontPageData[] PROGMEM =
|
||||
{
|
||||
0xff,0xff,0xff,0xff,0xff,0xd1,0xff,0xff,0xf9,0xff,0xf9,0xff,0xc1,0xeb,0xc1,0xff,0xd3,0xc1,0xe5,0xff,0xcd,0xf7,0xd9,0xff,0xcb,0xd5,0xeb,0xff,0xff,0xf9,0xff,0xff,0xff,0xe3,0xdd,0xff,0xff,0xdd,0xe3,0xff,0xeb,0xf7,0xeb,0xff,0xf7,0xe3,0xf7,0xff,0xdf,0xef,0xff,0xff,0xf7,0xf7,0xf7,0xff,0xff,0xdf,0xff,0xff,0xcf,0xf7,0xf9,0xff,0xc3,0xdd,0xe1,0xff,0xdb,0xc1,0xdf,0xff,0xcd,0xd5,0xd3,0xff,0xdd,0xd5,0xeb,0xff,0xe1,0xef,0xc7,0xff,0xd1,0xd5,0xe5,0xff,0xc3,0xd5,0xe5,0xff,0xcd,0xf5,0xf9,0xff,0xc3,0xd5,0xe1,0xff,0xd3,0xd5,0xe1,0xff,0xff,0xeb,0xff,0xff,0xdf,0xeb,0xff,0xff,0xf7,0xeb,0xdd,0xff,0xeb,0xeb,0xeb,0xff,0xdd,0xeb,0xf7,0xff,0xfd,0xd5,0xf9,0xff,0xe3,0xdd,0xd3,0xff,0xc3,0xf5,0xc1,0xff,0xc3,0xd5,0xe9,0xff,0xe3,0xdd,0xdd,0xff,0xc1,0xdd,0xe3,0xff,0xc3,0xd5,0xd5,0xff,0xc3,0xf5,0xf5,0xff,0xe3,0xdd,0xc5,0xff,0xc1,0xf7,0xc1,0xff,0xdd,0xc1,0xdd,0xff,0xef,0xdd,0xe1,0xff,0xc1,0xf7,0xc9,0xff,0xc1,0xdf,0xdf,0xff,0xc1,0xfb,0xc1,0xff,0xc1,0xfd,0xc3,0xff,0xe3,0xdd,0xe3,0xff,0xc1,0xf5,0xf3,0xff,0xe3,0xcd,0xc1,0xff,0xc3,0xf5,0xc9,0xff,0xdb,0xd5,0xed,0xff,0xfd,0xc1,0xfd,0xff,0xe1,0xdf,0xc1,0xff,0xe1,0xdf,0xe1,0xff,0xc1,0xef,0xc1,0xff,0xc9,0xf7,0xc9,0xff,0xf9,0xc7,0xf9,0xff,0xcd,0xd5,0xd9,0xff,0xff,0xc1,0xdd,0xff,0xf9,0xf7,0xcf,0xff,0xff,0xdd,0xc1,0xff,0xfb,0xfd,0xfb,0xff,0xdf,0xdf,0xdf,0xff,0xff,0xfd,0xfb,0xff,0xe7,0xdb,0xc3,0xff,0xc1,0xdb,0xe7,0xff,0xe7,0xdb,0xdb,0xff,0xe7,0xdb,0xc1,0xff,0xe7,0xcb,0xd3,0xff,0xc3,0xed,0xfb,0xff,0xb7,0xab,0xc7,0xff,0xc1,0xf7,0xcf,0xff,0xff,0xe5,0xdf,0xff,0xbf,0xcb,0xff,0xff,0xc1,0xf7,0xcb,0xff,0xff,0xe1,0xdf,0xff,0xc3,0xf7,0xc3,0xff,0xc3,0xfb,0xc7,0xff,0xe7,0xdb,0xe7,0xff,0x83,0xdb,0xe7,0xff,0xe7,0xdb,0x83,0xff,0xc3,0xf7,0xfb,0xff,0xd7,0xd3,0xeb,0xff,0xe1,0xdb,0xdf,0xff,0xe3,0xdf,0xc3,0xff,0xc3,0xdf,0xe3,0xff,0xc3,0xef,0xc3,0xff,0xcb,0xf7,0xcb,0xff,0xf3,0xaf,0xc3,0xff,0xdb,0xcb,0xd3,0xff,0xf7,0xc1,0xdd,0xff,0xff,0xc1,0xff,0xff,0xdd,0xc1,0xf7,0xff,0xfb,0xfd,0xfb,0xff,0xe3,0xed,0xe3,0xff
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
// Declarations for assets generated by tools/extract_doom_assets.py
|
||||
// (converted at build time from the user's local Doom shareware WAD)
|
||||
constexpr uint8_t skeletonSpriteData_numFrames = 2;
|
||||
extern const uint16_t skeletonSpriteData[]; // demon
|
||||
constexpr uint8_t mageSpriteData_numFrames = 2;
|
||||
extern const uint16_t mageSpriteData[]; // imp
|
||||
constexpr uint8_t torchSpriteData1_numFrames = 1;
|
||||
extern const uint16_t torchSpriteData1[];
|
||||
constexpr uint8_t torchSpriteData2_numFrames = 1;
|
||||
extern const uint16_t torchSpriteData2[];
|
||||
constexpr uint8_t projectileSpriteData_numFrames = 1;
|
||||
extern const uint16_t projectileSpriteData[];
|
||||
constexpr uint8_t enemyProjectileSpriteData_numFrames = 1;
|
||||
extern const uint16_t enemyProjectileSpriteData[];
|
||||
constexpr uint8_t entranceSpriteData_numFrames = 1;
|
||||
extern const uint16_t entranceSpriteData[];
|
||||
constexpr uint8_t exitSpriteData_numFrames = 1;
|
||||
extern const uint16_t exitSpriteData[];
|
||||
constexpr uint8_t urnSpriteData_numFrames = 1;
|
||||
extern const uint16_t urnSpriteData[]; // barrel
|
||||
constexpr uint8_t signSpriteData_numFrames = 1;
|
||||
extern const uint16_t signSpriteData[]; // skull pile
|
||||
constexpr uint8_t crownSpriteData_numFrames = 1;
|
||||
extern const uint16_t crownSpriteData[]; // armor
|
||||
constexpr uint8_t coinsSpriteData_numFrames = 1;
|
||||
extern const uint16_t coinsSpriteData[]; // health bonus
|
||||
constexpr uint8_t scrollSpriteData_numFrames = 1;
|
||||
extern const uint16_t scrollSpriteData[]; // armor bonus
|
||||
constexpr uint8_t chestSpriteData_numFrames = 1;
|
||||
extern const uint16_t chestSpriteData[]; // backpack
|
||||
constexpr uint8_t chestOpenSpriteData_numFrames = 1;
|
||||
extern const uint16_t chestOpenSpriteData[]; // clip
|
||||
constexpr uint8_t potionSpriteData_numFrames = 1;
|
||||
extern const uint16_t potionSpriteData[]; // stimpack
|
||||
constexpr uint8_t batSpriteData_numFrames = 2;
|
||||
extern const uint16_t batSpriteData[]; // sergeant
|
||||
constexpr uint8_t spiderSpriteData_numFrames = 2;
|
||||
extern const uint16_t spiderSpriteData[]; // zombieman
|
||||
// First-person weapon (shotgun idle / firing)
|
||||
extern const uint8_t handSpriteData1[];
|
||||
extern const uint8_t handSpriteData2[];
|
||||
// Title screen (128x64, Platform::DrawSolidBitmap format)
|
||||
extern const uint8_t titleBitmapData[];
|
||||
constexpr uint8_t wallTextureData_numTextures = 1;
|
||||
extern const uint16_t wallTextureData[];
|
||||
extern const uint8_t fontPageData[];
|
||||
extern const uint8_t heartSpriteData[];
|
||||
extern const uint8_t manaSpriteData[];
|
||||
@@ -0,0 +1,8 @@
|
||||
const uint8_t scaleLUT[] PROGMEM = {
|
||||
15,0,8,15,0,4,8,12,15,0,2,5,8,10,13,15,0,2,4,6,8,10,12,14,15,0,1,3,4,6,8,9,11,12,14,15,0,1,2,4,5,6,8,9,10,12,13,14,15,0,1,2,3,4,5,6,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,15,0,0,1,2,3,4,5,6,7,8,8,9,10,11,12,13,14,15,15,0,0,1,2,3,4,4,5,6,7,8,8,9,10,11,12,12,13,14,15,15,0,0,1,2,2,3,4,5,5,6,7,8,8,9,10,10,11,12,13,13,14,15,15,0,0,1,2,2,3,4,4,5,6,6,7,8,8,9,10,10,11,12,12,13,14,14,15,15,0,0,1,1,2,3,3,4,4,5,6,6,7,8,8,9,9,10,11,11,12,12,13,14,14,15,15,0,0,1,1,2,2,3,4,4,5,5,6,6,7,8,8,9,9,10,10,11,12,12,13,13,14,14,15,15,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,15
|
||||
};
|
||||
|
||||
const int16_t sinTable[] PROGMEM = {
|
||||
0,6,12,18,25,31,37,43,49,56,62,68,74,80,86,92,97,103,109,115,120,126,131,136,142,147,152,157,162,167,171,176,181,185,189,193,197,201,205,209,212,216,219,222,225,228,231,234,236,238,241,243,244,246,248,249,251,252,253,254,254,255,255,255,255,255,255,255,254,254,253,252,251,249,248,246,244,243,241,238,236,234,231,228,225,222,219,216,212,209,205,201,197,193,189,185,181,176,171,167,162,157,152,147,142,136,131,126,120,115,109,103,97,92,86,80,74,68,62,56,49,43,37,31,25,18,12,6,0,-6,-12,-18,-25,-31,-37,-43,-49,-56,-62,-68,-74,-80,-86,-92,-97,-103,-109,-115,-120,-126,-131,-136,-142,-147,-152,-157,-162,-167,-171,-176,-181,-185,-189,-193,-197,-201,-205,-209,-212,-216,-219,-222,-225,-228,-231,-234,-236,-238,-241,-243,-244,-246,-248,-249,-251,-252,-253,-254,-254,-255,-255,-255,-255,-255,-255,-255,-254,-254,-253,-252,-251,-249,-248,-246,-244,-243,-241,-238,-236,-234,-231,-228,-225,-222,-219,-216,-212,-209,-205,-201,-197,-193,-189,-185,-181,-176,-171,-167,-162,-157,-152,-147,-142,-136,-131,-126,-120,-115,-109,-103,-97,-92,-86,-80,-74,-68,-62,-56,-49,-43,-37,-31,-25,-18,-12,-6
|
||||
};
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#include "game/Defines.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Enemy.h"
|
||||
|
||||
uint8_t Map::level[Map::width * Map::height / 2];
|
||||
|
||||
bool Map::IsBlocked(uint8_t x, uint8_t y)
|
||||
{
|
||||
return GetCellSafe(x, y) >= CellType::FirstCollidableCell;
|
||||
}
|
||||
|
||||
bool Map::IsSolid(uint8_t x, uint8_t y)
|
||||
{
|
||||
return GetCellSafe(x, y) >= CellType::FirstSolidCell;
|
||||
}
|
||||
|
||||
CellType Map::GetCell(uint8_t x, uint8_t y)
|
||||
{
|
||||
int index = y * Map::width + x;
|
||||
uint8_t cellData = level[index / 2];
|
||||
|
||||
if(index & 1)
|
||||
{
|
||||
return (CellType)(cellData >> 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (CellType)(cellData & 0xf);
|
||||
}
|
||||
}
|
||||
|
||||
CellType Map::GetCellSafe(uint8_t x, uint8_t y)
|
||||
{
|
||||
if(x >= Map::width || y >= Map::height)
|
||||
return CellType::BrickWall;
|
||||
|
||||
int index = y * Map::width + x;
|
||||
uint8_t cellData = level[index / 2];
|
||||
|
||||
if(index & 1)
|
||||
{
|
||||
return (CellType)(cellData >> 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (CellType)(cellData & 0xf);
|
||||
}
|
||||
}
|
||||
|
||||
void Map::SetCell(uint8_t x, uint8_t y, CellType type)
|
||||
{
|
||||
if (x >= Map::width || y >= Map::height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int index = (y * Map::width + x) / 2;
|
||||
uint8_t cellType = (uint8_t)type;
|
||||
|
||||
if(x & 1)
|
||||
{
|
||||
level[index] = (level[index] & 0xf) | (cellType << 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
level[index] = (level[index] & 0xf0) | (cellType & 0xf);
|
||||
}
|
||||
}
|
||||
|
||||
void Map::DebugDraw()
|
||||
{
|
||||
for(int y = 0; y < Map::height; y++)
|
||||
{
|
||||
for(int x = 0; x < Map::width; x++)
|
||||
{
|
||||
Platform::PutPixel(x, y, GetCell(x, y) == CellType::BrickWall ? 1 : 0);
|
||||
|
||||
if (x == Renderer::camera.cellX && y == Renderer::camera.cellY && (Game::globalTickFrame & 8) != 0)
|
||||
{
|
||||
Platform::PutPixel(x, y, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((Game::globalTickFrame & 2) != 0)
|
||||
{
|
||||
for (uint8_t n = 0; n < EnemyManager::maxEnemies; n++)
|
||||
{
|
||||
Enemy& enemy = EnemyManager::enemies[n];
|
||||
|
||||
if (enemy.IsValid())
|
||||
{
|
||||
Platform::PutPixel(enemy.x / CELL_SIZE, enemy.y / CELL_SIZE, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Map::IsClearLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2)
|
||||
{
|
||||
int cellX1 = x1 / CELL_SIZE;
|
||||
int cellX2 = x2 / CELL_SIZE;
|
||||
int cellY1 = y1 / CELL_SIZE;
|
||||
int cellY2 = y2 / CELL_SIZE;
|
||||
|
||||
int xdist = ABS(cellX2 - cellX1);
|
||||
|
||||
int partial, delta;
|
||||
int deltafrac;
|
||||
int xfrac, yfrac;
|
||||
int xstep, ystep;
|
||||
int32_t ltemp;
|
||||
int x, y;
|
||||
|
||||
if (xdist > 0)
|
||||
{
|
||||
if (cellX2 > cellX1)
|
||||
{
|
||||
partial = (CELL_SIZE * (cellX1 + 1) - x1);
|
||||
xstep = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
partial = (x1 - CELL_SIZE * (cellX1));
|
||||
xstep = -1;
|
||||
}
|
||||
|
||||
deltafrac = ABS(x2 - x1);
|
||||
delta = y2 - y1;
|
||||
ltemp = ((int32_t)delta * CELL_SIZE) / deltafrac;
|
||||
if (ltemp > 0x7fffl)
|
||||
ystep = 0x7fff;
|
||||
else if (ltemp < -0x7fffl)
|
||||
ystep = -0x7fff;
|
||||
else
|
||||
ystep = ltemp;
|
||||
yfrac = y1 + (((int32_t)ystep*partial) / CELL_SIZE);
|
||||
|
||||
x = cellX1 + xstep;
|
||||
cellX2 += xstep;
|
||||
do
|
||||
{
|
||||
y = (yfrac) / CELL_SIZE;
|
||||
yfrac += ystep;
|
||||
|
||||
if (IsSolid(x, y))
|
||||
return false;
|
||||
|
||||
x += xstep;
|
||||
|
||||
//
|
||||
// see if the door is open enough
|
||||
//
|
||||
/*value &= ~0x80;
|
||||
intercept = yfrac-ystep/2;
|
||||
|
||||
if (intercept>doorposition[value])
|
||||
return false;*/
|
||||
|
||||
} while (x != cellX2);
|
||||
}
|
||||
|
||||
int ydist = ABS(cellY2 - cellY1);
|
||||
|
||||
if (ydist > 0)
|
||||
{
|
||||
if (cellY2 > cellY1)
|
||||
{
|
||||
partial = (CELL_SIZE * (cellY1 + 1) - y1);
|
||||
ystep = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
partial = (y1 - CELL_SIZE * (cellY1));
|
||||
ystep = -1;
|
||||
}
|
||||
|
||||
deltafrac = ABS(y2 - y1);
|
||||
delta = x2 - x1;
|
||||
ltemp = ((int32_t)delta * CELL_SIZE)/deltafrac;
|
||||
if (ltemp > 0x7fffl)
|
||||
xstep = 0x7fff;
|
||||
else if (ltemp < -0x7fffl)
|
||||
xstep = -0x7fff;
|
||||
else
|
||||
xstep = ltemp;
|
||||
xfrac = x1 + (((int32_t)xstep*partial) / CELL_SIZE);
|
||||
|
||||
y = cellY1 + ystep;
|
||||
cellY2 += ystep;
|
||||
do
|
||||
{
|
||||
x = (xfrac) / CELL_SIZE;
|
||||
xfrac += xstep;
|
||||
|
||||
if (IsSolid(x, y))
|
||||
return false;
|
||||
y += ystep;
|
||||
|
||||
//
|
||||
// see if the door is open enough
|
||||
//
|
||||
/*value &= ~0x80;
|
||||
intercept = xfrac-xstep/2;
|
||||
|
||||
if (intercept>doorposition[value])
|
||||
return false;*/
|
||||
} while (y != cellY2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Map::DrawMinimap()
|
||||
{
|
||||
constexpr uint8_t minimapWidth = 24;
|
||||
constexpr uint8_t minimapHeight = 18;
|
||||
constexpr uint8_t minimapX = 0; //DISPLAY_WIDTH / 2 - minimapWidth / 2;
|
||||
constexpr uint8_t minimapY = 0; //DISPLAY_HEIGHT - minimapHeight;
|
||||
uint8_t playerCellX = Game::player.x / CELL_SIZE;
|
||||
uint8_t playerCellY = Game::player.y / CELL_SIZE;
|
||||
uint8_t startCellX = playerCellX - minimapWidth / 2;
|
||||
uint8_t startCellY = playerCellY - minimapHeight / 2;
|
||||
|
||||
uint8_t outX = minimapX;
|
||||
uint8_t cellX = startCellX;
|
||||
|
||||
for (uint8_t x = 0; x < minimapWidth; x++)
|
||||
{
|
||||
uint8_t outY = minimapY;
|
||||
uint8_t cellY = startCellY;
|
||||
|
||||
for (uint8_t y = 0; y < minimapHeight; y++)
|
||||
{
|
||||
if (cellX == playerCellX && cellY == playerCellY)
|
||||
{
|
||||
Platform::PutPixel(outX, outY, (Game::globalTickFrame & 3) ? COLOUR_BLACK : COLOUR_WHITE);
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::PutPixel(outX, outY, cellX < width && cellY < height && IsSolid(cellX, cellY) ? COLOUR_BLACK : COLOUR_WHITE);
|
||||
}
|
||||
outY++;
|
||||
cellY++;
|
||||
}
|
||||
|
||||
outX++;
|
||||
cellX++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
enum class CellType : uint8_t
|
||||
{
|
||||
Empty = 0,
|
||||
|
||||
// Monster types
|
||||
Monster,
|
||||
|
||||
// Non collidable decorations
|
||||
Torch,
|
||||
Entrance,
|
||||
Exit,
|
||||
|
||||
// Items
|
||||
Potion,
|
||||
Coins,
|
||||
Crown,
|
||||
Scroll,
|
||||
|
||||
// Collidable decorations
|
||||
Urn,
|
||||
Chest,
|
||||
ChestOpened,
|
||||
Sign,
|
||||
|
||||
// Solid cells
|
||||
BrickWall,
|
||||
|
||||
FirstCollidableCell = Urn,
|
||||
FirstSolidCell = BrickWall
|
||||
};
|
||||
|
||||
class Map
|
||||
{
|
||||
public:
|
||||
static constexpr int width = 32;
|
||||
static constexpr int height = 24;
|
||||
|
||||
static bool IsSolid(uint8_t x, uint8_t y);
|
||||
static bool IsBlocked(uint8_t x, uint8_t y);
|
||||
static inline bool IsBlockedAtWorldPosition(int16_t x, int16_t y)
|
||||
{
|
||||
return IsBlocked((uint8_t)(x >> 8), (uint8_t)(y >> 8));
|
||||
}
|
||||
|
||||
static CellType GetCell(uint8_t x, uint8_t y);
|
||||
static CellType GetCellSafe(uint8_t x, uint8_t y);
|
||||
static void SetCell(uint8_t x, uint8_t y, CellType cellType);
|
||||
static void DebugDraw();
|
||||
static void DrawMinimap();
|
||||
|
||||
static bool IsClearLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2);
|
||||
|
||||
private:
|
||||
static uint8_t level[width * height / 2];
|
||||
|
||||
};
|
||||
@@ -0,0 +1,610 @@
|
||||
#include "game/MapGenerator.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Game.h"
|
||||
|
||||
uint8_t MapGenerator::GetDistanceToCellType(uint8_t x, uint8_t y, CellType cellType)
|
||||
{
|
||||
uint8_t ringWidth = 3;
|
||||
|
||||
for (uint8_t offset = 1; offset < Map::width; offset++)
|
||||
{
|
||||
for (uint8_t i = 0; i < ringWidth; i++)
|
||||
{
|
||||
if (Map::GetCellSafe(x - offset + i, y - offset) == cellType)
|
||||
{
|
||||
return offset;
|
||||
}
|
||||
if (Map::GetCellSafe(x - offset + i, y + offset) == cellType)
|
||||
{
|
||||
return offset;
|
||||
}
|
||||
if (Map::GetCellSafe(x - offset, y - offset + i) == cellType)
|
||||
{
|
||||
return offset;
|
||||
}
|
||||
if (Map::GetCellSafe(x + offset, y - offset + i) == cellType)
|
||||
{
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
|
||||
ringWidth += 2;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
uint8_t MapGenerator::CountNeighbours(uint8_t x, uint8_t y)
|
||||
{
|
||||
uint8_t result = 0;
|
||||
|
||||
if (Map::GetCellSafe(x + 1, y) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x, y + 1) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x - 1, y) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x, y - 1) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x + 1, y + 1) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x - 1, y + 1) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x - 1, y - 1) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x + 1, y - 1) == CellType::Empty)
|
||||
result++;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
MapGenerator::NeighbourInfo MapGenerator::GetCellNeighbourInfo(uint8_t x, uint8_t y)
|
||||
{
|
||||
NeighbourInfo result;
|
||||
|
||||
result.count = 0;
|
||||
result.mask = 0;
|
||||
|
||||
if (Map::IsSolid(x, y - 1))
|
||||
{
|
||||
result.hasNorth = true;
|
||||
result.count++;
|
||||
}
|
||||
if (Map::IsSolid(x + 1, y))
|
||||
{
|
||||
result.hasEast = true;
|
||||
result.count++;
|
||||
}
|
||||
if (Map::IsSolid(x, y + 1))
|
||||
{
|
||||
result.hasSouth = true;
|
||||
result.count++;
|
||||
}
|
||||
if (Map::IsSolid(x - 1, y))
|
||||
{
|
||||
result.hasWest = true;
|
||||
result.count++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t MapGenerator::CountImmediateNeighbours(uint8_t x, uint8_t y)
|
||||
{
|
||||
uint8_t result = 0;
|
||||
|
||||
if (Map::GetCellSafe(x + 1, y) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x, y + 1) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x - 1, y) == CellType::Empty)
|
||||
result++;
|
||||
if (Map::GetCellSafe(x, y - 1) == CellType::Empty)
|
||||
result++;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
MapGenerator::NeighbourInfo MapGenerator::GetRoomNeighbourMask(uint8_t x, uint8_t y, uint8_t w, uint8_t h)
|
||||
{
|
||||
NeighbourInfo result;
|
||||
result.mask = 0;
|
||||
result.count = 0;
|
||||
|
||||
result.canDemolishNorth = y > 1;
|
||||
result.canDemolishWest = x > 1;
|
||||
result.canDemolishEast = x + w + 1 < Map::width - 1;
|
||||
result.canDemolishSouth = y + h + 1 < Map::height - 1;
|
||||
|
||||
// Don't demolish walls if the neighbouring room has the same wall length
|
||||
if (Map::GetCell(x - 1, y - 2) != CellType::Empty && Map::GetCell(x + w, y - 2) != CellType::Empty)
|
||||
{
|
||||
result.canDemolishNorth = false;
|
||||
}
|
||||
if (Map::GetCell(x - 2, y - 1) != CellType::Empty && Map::GetCell(x - 2, y + h) != CellType::Empty)
|
||||
{
|
||||
result.canDemolishWest = false;
|
||||
}
|
||||
if (Map::GetCell(x + w + 1, y - 1) != CellType::Empty && Map::GetCell(x + w, y + h + 1) != CellType::Empty)
|
||||
{
|
||||
result.canDemolishEast = false;
|
||||
}
|
||||
if (Map::GetCell(x - 1, y + h + 1) != CellType::Empty && Map::GetCell(x + w, y + h + 1) != CellType::Empty)
|
||||
{
|
||||
result.canDemolishSouth = false;
|
||||
}
|
||||
|
||||
// Don't demolish wall if this will leave an unattached wall
|
||||
if (Map::GetCell(x - 1, y - 2) == CellType::Empty && Map::GetCell(x - 2, y - 1) == CellType::Empty)
|
||||
{
|
||||
result.canDemolishNorth = false;
|
||||
result.canDemolishWest = false;
|
||||
}
|
||||
if (Map::GetCell(x + w, y - 2) == CellType::Empty && Map::GetCell(x + w + 1, y - 1) == CellType::Empty)
|
||||
{
|
||||
result.canDemolishNorth = false;
|
||||
result.canDemolishEast = false;
|
||||
}
|
||||
if (Map::GetCell(x + w, y + h + 1) == CellType::Empty && Map::GetCell(x + w + 1, y + h) == CellType::Empty)
|
||||
{
|
||||
result.canDemolishSouth = false;
|
||||
result.canDemolishEast = false;
|
||||
}
|
||||
if (Map::GetCell(x - 1, y + h + 1) == CellType::Empty && Map::GetCell(x - 2, y + h) == CellType::Empty)
|
||||
{
|
||||
result.canDemolishSouth = false;
|
||||
result.canDemolishWest = false;
|
||||
}
|
||||
|
||||
bool hasNorthWall = Map::GetCell(x, y - 1) != CellType::Empty && Map::GetCell(x + w - 1, y - 1) != CellType::Empty;
|
||||
bool hasEastWall = Map::GetCell(x + w, y) != CellType::Empty && Map::GetCell(x + w, y + h - 1) != CellType::Empty;
|
||||
bool hasSouthWall = Map::GetCell(x, y + h) != CellType::Empty && Map::GetCell(x + w - 1, y + h) != CellType::Empty;
|
||||
bool hasWestWall = Map::GetCell(x - 1, y) != CellType::Empty && Map::GetCell(x - 1, y + h - 1) != CellType::Empty;
|
||||
|
||||
if (!hasNorthWall)
|
||||
{
|
||||
result.canDemolishNorth = false;
|
||||
result.canDemolishEast = false;
|
||||
result.canDemolishWest = false;
|
||||
}
|
||||
if (!hasEastWall)
|
||||
{
|
||||
result.canDemolishNorth = false;
|
||||
result.canDemolishEast = false;
|
||||
result.canDemolishSouth = false;
|
||||
}
|
||||
if (!hasSouthWall)
|
||||
{
|
||||
result.canDemolishEast = false;
|
||||
result.canDemolishSouth = false;
|
||||
result.canDemolishWest = false;
|
||||
}
|
||||
if (!hasWestWall)
|
||||
{
|
||||
result.canDemolishNorth = false;
|
||||
result.canDemolishSouth = false;
|
||||
result.canDemolishWest = false;
|
||||
}
|
||||
|
||||
for (int i = x; i < x + w; i++)
|
||||
{
|
||||
if (Map::GetCell(i, y - 1) == CellType::Empty)
|
||||
{
|
||||
result.hasNorth = true;
|
||||
result.count++;
|
||||
}
|
||||
if (Map::GetCell(i, y + h) == CellType::Empty)
|
||||
{
|
||||
result.hasSouth = true;
|
||||
result.count++;
|
||||
}
|
||||
|
||||
// Don't demolish wall if there is an intersecting wall attached
|
||||
if (y > 1 && Map::GetCell(i, y - 2) != CellType::Empty)
|
||||
{
|
||||
result.canDemolishNorth = false;
|
||||
}
|
||||
if (y + h + 1 < Map::height - 1 && Map::GetCell(i, y + h + 1) != CellType::Empty)
|
||||
{
|
||||
result.canDemolishSouth = false;
|
||||
}
|
||||
}
|
||||
for (int j = y; j < y + h; j++)
|
||||
{
|
||||
if (Map::GetCell(x - 1, j) == CellType::Empty)
|
||||
{
|
||||
result.hasWest = true;
|
||||
result.count++;
|
||||
}
|
||||
if (Map::GetCell(x + w, j) == CellType::Empty)
|
||||
{
|
||||
result.hasEast = true;
|
||||
result.count++;
|
||||
}
|
||||
|
||||
// Don't demolish wall if there is an intersecting wall attached
|
||||
if (x > 1 && Map::GetCell(x - 2, j) != CellType::Empty)
|
||||
{
|
||||
result.canDemolishWest = false;
|
||||
}
|
||||
if (x + w + 1 < Map::width - 1 && Map::GetCell(x + w + 1, j) != CellType::Empty)
|
||||
{
|
||||
result.canDemolishEast = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void MapGenerator::SplitMap(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t doorX, uint8_t doorY)
|
||||
{
|
||||
constexpr int minRoomSize = 3;
|
||||
constexpr int maxRoomSize = 8;
|
||||
//constexpr int maxFloorSpace = 80;
|
||||
constexpr int demolishWallChance = 20;
|
||||
|
||||
if (doorX != 0 && doorY != 0)
|
||||
{
|
||||
Map::SetCell(doorX, doorY, CellType::Empty);
|
||||
}
|
||||
|
||||
bool splitVertical = false;
|
||||
bool splitHorizontal = false;
|
||||
|
||||
if (w > maxRoomSize || h > maxRoomSize)//w * h > maxFloorSpace)
|
||||
{
|
||||
if (w < h)
|
||||
{
|
||||
splitVertical = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
splitHorizontal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (splitVertical)
|
||||
{
|
||||
uint8_t splitSize;
|
||||
uint8_t splitAttempts = 255;
|
||||
do
|
||||
{
|
||||
splitSize = (Random() % (h - 2 * minRoomSize)) + minRoomSize;
|
||||
splitAttempts--;
|
||||
} while (splitAttempts > 0 && (Map::GetCell(x - 1, y + splitSize) == CellType::Empty || Map::GetCell(x + w, y + splitSize) == CellType::Empty
|
||||
|| Map::GetCell(x - 1, y + splitSize - 1) == CellType::Empty || Map::GetCell(x + w, y + splitSize - 1) == CellType::Empty
|
||||
|| Map::GetCell(x - 1, y + splitSize + 1) == CellType::Empty || Map::GetCell(x + w, y + splitSize + 1) == CellType::Empty));
|
||||
|
||||
if (splitAttempts > 0)
|
||||
{
|
||||
uint8_t splitDoorX = x + (Random() % (w - 2)) + 1;
|
||||
uint8_t splitDoorY = y + splitSize;
|
||||
|
||||
for (uint8_t i = x; i < x + w; i++)
|
||||
{
|
||||
Map::SetCell(i, y + splitSize, CellType::BrickWall);
|
||||
}
|
||||
|
||||
SplitMap(x, y + splitSize + 1, w, h - splitSize - 1, splitDoorX, splitDoorY);
|
||||
SplitMap(x, y, w, splitSize, splitDoorX, splitDoorY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (splitHorizontal)
|
||||
{
|
||||
uint8_t splitSize;
|
||||
uint8_t splitAttempts = 255;
|
||||
do
|
||||
{
|
||||
splitSize = (Random() % (w - 2 * minRoomSize)) + minRoomSize;
|
||||
splitAttempts--;
|
||||
} while (splitAttempts > 0 && (Map::GetCell(x + splitSize, y - 1) == CellType::Empty || Map::GetCell(x + splitSize, y + h) == CellType::Empty
|
||||
|| Map::GetCell(x + splitSize - 1, y - 1) == CellType::Empty || Map::GetCell(x + splitSize - 1, y + h) == CellType::Empty
|
||||
|| Map::GetCell(x + splitSize + 1, y - 1) == CellType::Empty || Map::GetCell(x + splitSize + 1, y + h) == CellType::Empty));
|
||||
|
||||
if (splitAttempts > 0)
|
||||
{
|
||||
uint8_t splitDoorX = x + splitSize;
|
||||
uint8_t splitDoorY = y + (Random() % (h - 2)) + 1;
|
||||
|
||||
for (uint8_t j = y; j < y + h; j++)
|
||||
{
|
||||
Map::SetCell(x + splitSize, j, CellType::BrickWall);
|
||||
}
|
||||
|
||||
SplitMap(x + splitSize + 1, y, w - splitSize - 1, h, splitDoorX, splitDoorY);
|
||||
SplitMap(x, y, splitSize, h, splitDoorX, splitDoorY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
NeighbourInfo neighbours = GetRoomNeighbourMask(x, y, w, h);
|
||||
|
||||
if (neighbours.canDemolishNorth && (Random() % 100) < demolishWallChance)
|
||||
{
|
||||
for (int i = 0; i < w; i++)
|
||||
{
|
||||
Map::SetCell(x + i, y - 1, CellType::Empty);
|
||||
}
|
||||
}
|
||||
else if (neighbours.canDemolishWest && (Random() % 100) < demolishWallChance)
|
||||
{
|
||||
for (int j = 0; j < h; j++)
|
||||
{
|
||||
Map::SetCell(x - 1, y + j, CellType::Empty);
|
||||
}
|
||||
}
|
||||
else if (neighbours.canDemolishSouth && (Random() % 100) < demolishWallChance)
|
||||
{
|
||||
for (int i = 0; i < w; i++)
|
||||
{
|
||||
Map::SetCell(x + i, y + h, CellType::Empty);
|
||||
}
|
||||
}
|
||||
else if (neighbours.canDemolishEast && (Random() % 100) < demolishWallChance)
|
||||
{
|
||||
for (int j = 0; j < h; j++)
|
||||
{
|
||||
Map::SetCell(x + w, y + j, CellType::Empty);
|
||||
}
|
||||
}
|
||||
|
||||
// Add decorations
|
||||
{
|
||||
// Add four cornering columns
|
||||
if (w == h && w >= 7 && h >= 7)
|
||||
{
|
||||
Map::SetCell(x + 1, y + 1, CellType::BrickWall);
|
||||
Map::SetCell(x + w - 2, y + 1, CellType::BrickWall);
|
||||
Map::SetCell(x + w - 2, y + h - 2, CellType::BrickWall);
|
||||
Map::SetCell(x + 1, y + h - 2, CellType::BrickWall);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MapGenerator::Generate()
|
||||
{
|
||||
uint8_t playerStartX = 1;
|
||||
uint8_t playerStartY = 1;
|
||||
|
||||
for (int y = 0; y < Map::height; y++)
|
||||
{
|
||||
for (int x = 0; x < Map::width; x++)
|
||||
{
|
||||
bool isEdge = x == 0 || y == 0 || x == Map::width - 1 || y == Map::height - 1;
|
||||
Map::SetCell(x, y, isEdge ? CellType::BrickWall : CellType::Empty);
|
||||
}
|
||||
}
|
||||
|
||||
SplitMap(1, 1, Map::width - 2, Map::height - 2, 0, 0);
|
||||
|
||||
// Find any big open spaces
|
||||
{
|
||||
bool hasOpenSpaces = true;
|
||||
|
||||
while (hasOpenSpaces)
|
||||
{
|
||||
hasOpenSpaces = false;
|
||||
|
||||
uint8_t x = 0, y = 0, space = 0;
|
||||
|
||||
for (uint8_t i = 1; i < Map::width - 1; i++)
|
||||
{
|
||||
for (uint8_t j = 0; j < Map::height - 1; j++)
|
||||
{
|
||||
bool foundWall = false;
|
||||
|
||||
for (uint8_t k = 0; k < Map::height && !foundWall; k++)
|
||||
{
|
||||
for (uint8_t u = 0; u < k && !foundWall; u++)
|
||||
{
|
||||
for (uint8_t v = 0; v < k && !foundWall; v++)
|
||||
{
|
||||
if (Map::GetCellSafe(i + u, j + v) != CellType::Empty)
|
||||
{
|
||||
foundWall = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundWall && k > space)
|
||||
{
|
||||
space = k;
|
||||
x = i;
|
||||
y = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (space > 6)
|
||||
{
|
||||
hasOpenSpaces = true;
|
||||
|
||||
// Stick a donut in the middle
|
||||
for (uint8_t n = 2; n < space - 2; n++)
|
||||
{
|
||||
Map::SetCell(x + n, y + 2, CellType::BrickWall);
|
||||
Map::SetCell(x + 2, y + n, CellType::BrickWall);
|
||||
Map::SetCell(x + n, y + space - 3, CellType::BrickWall);
|
||||
Map::SetCell(x + space - 3, y + n, CellType::BrickWall);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add torches
|
||||
{
|
||||
uint8_t attempts = 255;
|
||||
uint8_t toSpawn = 64;
|
||||
uint8_t minSpacing = 3;
|
||||
|
||||
while (attempts > 0 && toSpawn > 0)
|
||||
{
|
||||
uint8_t x = Random() % Map::width;
|
||||
uint8_t y = Random() % Map::height;
|
||||
|
||||
if (Map::GetCellSafe(x, y) == CellType::Empty)
|
||||
{
|
||||
NeighbourInfo info = GetCellNeighbourInfo(x, y);
|
||||
|
||||
if(info.count == 1 && GetDistanceToCellType(x, y, CellType::Torch) > minSpacing)
|
||||
{
|
||||
Map::SetCell(x, y, CellType::Torch);
|
||||
toSpawn--;
|
||||
attempts = 255;
|
||||
}
|
||||
}
|
||||
|
||||
attempts--;
|
||||
}
|
||||
}
|
||||
|
||||
// Add monsters
|
||||
{
|
||||
uint8_t attempts = 255;
|
||||
uint8_t monstersToSpawn = EnemyManager::maxEnemies;
|
||||
CellType monsterType = CellType::Monster;
|
||||
uint8_t minSpacing = 3;
|
||||
|
||||
while (attempts > 0 && monstersToSpawn > 0)
|
||||
{
|
||||
uint8_t x = Random() % Map::width;
|
||||
uint8_t y = Random() % Map::height;
|
||||
|
||||
if (Map::GetCellSafe(x, y) == CellType::Empty && Map::IsClearLine(x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2, playerStartX * CELL_SIZE + CELL_SIZE / 2, playerStartY * CELL_SIZE + CELL_SIZE / 2) == false)
|
||||
{
|
||||
NeighbourInfo info = GetCellNeighbourInfo(x, y);
|
||||
if (info.count == 0 && GetDistanceToCellType(x, y, monsterType) > minSpacing)
|
||||
{
|
||||
Map::SetCell(x, y, monsterType);
|
||||
monstersToSpawn--;
|
||||
attempts = 255;
|
||||
}
|
||||
}
|
||||
|
||||
attempts--;
|
||||
}
|
||||
}
|
||||
|
||||
// Add blocking decorations
|
||||
{
|
||||
uint8_t attempts = 255;
|
||||
uint8_t toSpawn = 255;
|
||||
CellType cellType = CellType::Urn;
|
||||
uint8_t minSpacing = 3;
|
||||
|
||||
while (attempts > 0 && toSpawn > 0)
|
||||
{
|
||||
uint8_t x = Random() % Map::width;
|
||||
uint8_t y = Random() % Map::height;
|
||||
|
||||
if (Map::GetCellSafe(x, y) == CellType::Empty)
|
||||
{
|
||||
NeighbourInfo info = GetCellNeighbourInfo(x, y);
|
||||
|
||||
if(info.IsCorner() && GetDistanceToCellType(x, y, cellType) > minSpacing)
|
||||
{
|
||||
Map::SetCell(x, y, cellType);
|
||||
toSpawn--;
|
||||
attempts = 255;
|
||||
}
|
||||
}
|
||||
|
||||
attempts--;
|
||||
}
|
||||
}
|
||||
|
||||
// Add entrance and exit
|
||||
Map::SetCell(1, 1, CellType::Entrance);
|
||||
Map::SetCell(Map::width - 3, Map::height - 3, CellType::Exit);
|
||||
|
||||
// Add sign
|
||||
if(false)
|
||||
{
|
||||
uint16_t attempts = 65535;
|
||||
constexpr uint8_t closeness = 5;
|
||||
|
||||
while (attempts > 0)
|
||||
{
|
||||
uint8_t x = Random() % closeness;
|
||||
uint8_t y = Random() % closeness;
|
||||
|
||||
if (Map::GetCellSafe(x, y) == CellType::Empty
|
||||
&& Map::GetCellSafe(x - 1, y) == CellType::Empty
|
||||
&& Map::GetCellSafe(x, y - 1) == CellType::Empty
|
||||
&& Map::GetCellSafe(x + 1, y) == CellType::Empty
|
||||
&& Map::GetCellSafe(x, y + 1) == CellType::Empty
|
||||
&& Map::GetCellSafe(x - 1, y - 1) == CellType::Empty
|
||||
&& Map::GetCellSafe(x + 1, y - 1) == CellType::Empty
|
||||
&& Map::GetCellSafe(x - 1, y + 1) == CellType::Empty
|
||||
&& Map::GetCellSafe(x + 1, y + 1) == CellType::Empty
|
||||
&& Map::IsClearLine(x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2, playerStartX * CELL_SIZE + CELL_SIZE / 2, playerStartY * CELL_SIZE + CELL_SIZE / 2))
|
||||
{
|
||||
Map::SetCell(x, y, CellType::Sign);
|
||||
break;
|
||||
}
|
||||
|
||||
attempts--;
|
||||
}
|
||||
}
|
||||
else if(Game::floor == 1)
|
||||
{
|
||||
Map::SetCell(2, 2, CellType::Sign);
|
||||
}
|
||||
|
||||
// Add treasure / items
|
||||
{
|
||||
uint16_t attempts = 65535;
|
||||
uint8_t toSpawn = 8;
|
||||
CellType cellType = CellType::Chest;
|
||||
uint8_t minSpacing = 3;
|
||||
uint8_t minExitSpacing = 6;
|
||||
|
||||
while (attempts > 0 && toSpawn > 0)
|
||||
{
|
||||
uint8_t x = Random() % Map::width;
|
||||
uint8_t y = Random() % Map::height;
|
||||
|
||||
switch (Random() % 5)
|
||||
{
|
||||
case 0:
|
||||
cellType = CellType::Potion;
|
||||
break;
|
||||
case 1:
|
||||
cellType = CellType::Coins;
|
||||
break;
|
||||
case 2:
|
||||
cellType = CellType::Chest;
|
||||
break;
|
||||
case 3:
|
||||
cellType = CellType::Crown;
|
||||
break;
|
||||
case 4:
|
||||
cellType = CellType::Scroll;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Map::GetCellSafe(x, y) == CellType::Empty)
|
||||
{
|
||||
NeighbourInfo info = GetCellNeighbourInfo(x, y);
|
||||
|
||||
if(info.count == 1
|
||||
&& GetDistanceToCellType(x, y, cellType) > minSpacing
|
||||
&& GetDistanceToCellType(x, y, CellType::Entrance) > minExitSpacing
|
||||
&& GetDistanceToCellType(x, y, CellType::Exit) > minExitSpacing)
|
||||
{
|
||||
Map::SetCell(x, y, cellType);
|
||||
toSpawn--;
|
||||
attempts = 255;
|
||||
}
|
||||
}
|
||||
|
||||
attempts--;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Map.h"
|
||||
|
||||
class MapGenerator
|
||||
{
|
||||
public:
|
||||
static void Generate();
|
||||
|
||||
private:
|
||||
|
||||
struct NeighbourInfo
|
||||
{
|
||||
union
|
||||
{
|
||||
uint8_t mask;
|
||||
|
||||
struct
|
||||
{
|
||||
bool hasNorth : 1;
|
||||
bool hasEast : 1;
|
||||
bool hasSouth : 1;
|
||||
bool hasWest : 1;
|
||||
|
||||
bool canDemolishNorth : 1;
|
||||
bool canDemolishEast : 1;
|
||||
bool canDemolishSouth : 1;
|
||||
bool canDemolishWest : 1;
|
||||
};
|
||||
};
|
||||
|
||||
uint8_t count;
|
||||
|
||||
bool IsCorner() const
|
||||
{
|
||||
if (count != 2)
|
||||
return false;
|
||||
|
||||
if (hasNorth && hasEast)
|
||||
return true;
|
||||
if (hasEast && hasSouth)
|
||||
return true;
|
||||
if (hasSouth && hasWest)
|
||||
return true;
|
||||
if (hasWest && hasNorth)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
static uint8_t CountNeighbours(uint8_t x, uint8_t y);
|
||||
static uint8_t CountImmediateNeighbours(uint8_t x, uint8_t y);
|
||||
static NeighbourInfo GetRoomNeighbourMask(uint8_t x, uint8_t y, uint8_t w, uint8_t h);
|
||||
static void SplitMap(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t doorX, uint8_t doorY);
|
||||
|
||||
static NeighbourInfo GetCellNeighbourInfo(uint8_t x, uint8_t y);
|
||||
static uint8_t GetDistanceToCellType(uint8_t x, uint8_t y, CellType cellType);
|
||||
};
|
||||
@@ -0,0 +1,705 @@
|
||||
#include "game/Defines.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Menu.h"
|
||||
#include "game/Font.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Textures.h"
|
||||
#include "game/Generated/SpriteTypes.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "lib/EEPROM.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
constexpr uint8_t EEPROM_BASE_ADDR = 0;
|
||||
|
||||
struct ObjDesc {
|
||||
const uint16_t* sprite;
|
||||
bool animated;
|
||||
bool invert;
|
||||
uint8_t varsIndex;
|
||||
};
|
||||
|
||||
static const ObjDesc kObjects[] = {
|
||||
{ chestSpriteData, false, false, 0 },
|
||||
{ crownSpriteData, false, false, 1 },
|
||||
{ scrollSpriteData, false, false, 2 },
|
||||
{ coinsSpriteData, false, false, 3 },
|
||||
{ skeletonSpriteData, true, false, 4 },
|
||||
{ mageSpriteData, true, false, 5 },
|
||||
{ batSpriteData, true, false, 6 },
|
||||
{ spiderSpriteData, true, false, 7 },
|
||||
{ exitSpriteData, false, false, 8 }
|
||||
};
|
||||
|
||||
static constexpr uint8_t kObjectsCount = (uint8_t)(sizeof(kObjects) / sizeof(kObjects[0]));
|
||||
static constexpr uint8_t SHIFT_MASK = 63;
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t MENU_ITEMS_COUNT = 4;
|
||||
constexpr uint8_t VISIBLE_ROWS = 2;
|
||||
|
||||
constexpr uint8_t MENU_FIRST_ROW = 4;
|
||||
constexpr uint8_t TEXT_X = 18;
|
||||
constexpr uint8_t CURSOR_X = 10;
|
||||
|
||||
constexpr uint8_t SPLASH_TIME_TICKS = 90;
|
||||
static uint8_t splashTimer = 0;
|
||||
static bool splashActive = true;
|
||||
|
||||
static uint8_t Wrap(int v, int n) {
|
||||
v %= n;
|
||||
if(v < 0) v += n;
|
||||
return (uint8_t)v;
|
||||
}
|
||||
|
||||
static uint8_t MaxTop() {
|
||||
return (MENU_ITEMS_COUNT > VISIBLE_ROWS) ? (uint8_t)(MENU_ITEMS_COUNT - VISIBLE_ROWS) : 0;
|
||||
}
|
||||
|
||||
void DrawMenuRoom() {
|
||||
const int16_t leftWall = 1 * CELL_SIZE;
|
||||
const int16_t rightWall = 4 * CELL_SIZE;
|
||||
const int16_t topWall = 1 * CELL_SIZE;
|
||||
const int16_t bottomWall = 4 * CELL_SIZE;
|
||||
|
||||
Renderer::camera.x = (int16_t)((leftWall + rightWall) / 2);
|
||||
Renderer::camera.y = (int16_t)((topWall + bottomWall) / 2);
|
||||
|
||||
static uint16_t angleFP = 0;
|
||||
constexpr uint16_t SPEED_FP = 64;
|
||||
angleFP = (uint16_t)(angleFP + SPEED_FP);
|
||||
Renderer::camera.angle = (uint8_t)(angleFP >> 8);
|
||||
|
||||
Renderer::camera.tilt = 0;
|
||||
Renderer::camera.bob = 0;
|
||||
|
||||
Renderer::globalRenderFrame++;
|
||||
Renderer::DrawBackground();
|
||||
|
||||
Renderer::numBufferSlicesFilled = 0;
|
||||
Renderer::numQueuedDrawables = 0;
|
||||
|
||||
for(uint8_t n = 0; n < DISPLAY_WIDTH; n++) {
|
||||
Renderer::wBuffer[n] = 0;
|
||||
Renderer::horizonBuffer[n] = HORIZON +
|
||||
(((DISPLAY_WIDTH / 2 - n) * Renderer::camera.tilt) >> 8) +
|
||||
Renderer::camera.bob;
|
||||
}
|
||||
|
||||
Renderer::camera.cellX = Renderer::camera.x / CELL_SIZE;
|
||||
Renderer::camera.cellY = Renderer::camera.y / CELL_SIZE;
|
||||
|
||||
{
|
||||
uint16_t rotPhase = (uint16_t)(0 - angleFP);
|
||||
uint8_t a0 = (uint8_t)(rotPhase >> 8);
|
||||
uint8_t f = (uint8_t)(rotPhase & 0xff);
|
||||
uint8_t a1 = (uint8_t)(a0 + 1);
|
||||
|
||||
int16_t c0 = FixedCos(a0);
|
||||
int16_t c1 = FixedCos(a1);
|
||||
int16_t s0 = FixedSin(a0);
|
||||
int16_t s1 = FixedSin(a1);
|
||||
|
||||
Renderer::camera.rotCos = (int16_t)(c0 + (((int32_t)(c1 - c0) * f) >> 8));
|
||||
Renderer::camera.rotSin = (int16_t)(s0 + (((int32_t)(s1 - s0) * f) >> 8));
|
||||
}
|
||||
|
||||
{
|
||||
uint16_t clipPhase = (uint16_t)(((uint16_t)CLIP_ANGLE << 8) - angleFP);
|
||||
uint8_t a0 = (uint8_t)(clipPhase >> 8);
|
||||
uint8_t f = (uint8_t)(clipPhase & 0xff);
|
||||
uint8_t a1 = (uint8_t)(a0 + 1);
|
||||
|
||||
int16_t c0 = FixedCos(a0);
|
||||
int16_t c1 = FixedCos(a1);
|
||||
int16_t s0 = FixedSin(a0);
|
||||
int16_t s1 = FixedSin(a1);
|
||||
|
||||
Renderer::camera.clipCos = (int16_t)(c0 + (((int32_t)(c1 - c0) * f) >> 8));
|
||||
Renderer::camera.clipSin = (int16_t)(s0 + (((int32_t)(s1 - s0) * f) >> 8));
|
||||
}
|
||||
|
||||
#if WITH_IMAGE_TEXTURES
|
||||
const uint16_t* texture = wallTextureData;
|
||||
#elif WITH_VECTOR_TEXTURES
|
||||
const uint8_t* texture = vectorTexture0;
|
||||
#endif
|
||||
|
||||
constexpr int8_t MIN_CELL = 0;
|
||||
constexpr int8_t MAX_CELL = 4;
|
||||
|
||||
#define MENU_SOLID(cx, cy) (((cx) == 0) || ((cx) == MAX_CELL) || ((cy) == 0) || ((cy) == MAX_CELL))
|
||||
#define MENU_SOLID_SAFE(cx, cy) \
|
||||
(((cx) < MIN_CELL || (cx) > MAX_CELL || (cy) < MIN_CELL || (cy) > MAX_CELL) ? \
|
||||
true : \
|
||||
MENU_SOLID((cx), (cy)))
|
||||
|
||||
int8_t xd, yd;
|
||||
int8_t x1, y1, x2, y2;
|
||||
|
||||
if(Renderer::camera.rotCos > 0) {
|
||||
x1 = MIN_CELL;
|
||||
x2 = MAX_CELL + 1;
|
||||
xd = 1;
|
||||
} else {
|
||||
x2 = MIN_CELL - 1;
|
||||
x1 = MAX_CELL;
|
||||
xd = -1;
|
||||
}
|
||||
|
||||
if(Renderer::camera.rotSin < 0) {
|
||||
y1 = MIN_CELL;
|
||||
y2 = MAX_CELL + 1;
|
||||
yd = 1;
|
||||
} else {
|
||||
y2 = MIN_CELL - 1;
|
||||
y1 = MAX_CELL;
|
||||
yd = -1;
|
||||
}
|
||||
|
||||
auto drawMenuCell = [&](int8_t x, int8_t y) {
|
||||
if(!MENU_SOLID(x, y)) return;
|
||||
if(Renderer::isFrustrumClipped(x, y)) return;
|
||||
if(Renderer::numBufferSlicesFilled >= DISPLAY_WIDTH) return;
|
||||
|
||||
const bool blockedLeft = MENU_SOLID_SAFE(x - 1, y);
|
||||
const bool blockedRight = MENU_SOLID_SAFE(x + 1, y);
|
||||
const bool blockedUp = MENU_SOLID_SAFE(x, y - 1);
|
||||
const bool blockedDown = MENU_SOLID_SAFE(x, y + 1);
|
||||
|
||||
int16_t wx1 = (int16_t)(x * CELL_SIZE);
|
||||
int16_t wy1 = (int16_t)(y * CELL_SIZE);
|
||||
int16_t wx2 = (int16_t)(wx1 + CELL_SIZE);
|
||||
int16_t wy2 = (int16_t)(wy1 + CELL_SIZE);
|
||||
|
||||
if(!blockedLeft && Renderer::camera.x < wx1) {
|
||||
#if WITH_TEXTURES
|
||||
Renderer::DrawWall(
|
||||
texture,
|
||||
wx1,
|
||||
wy1,
|
||||
wx1,
|
||||
wy2,
|
||||
!blockedUp && Renderer::camera.y > wy1,
|
||||
!blockedDown && Renderer::camera.y < wy2,
|
||||
false);
|
||||
#else
|
||||
Renderer::DrawWall(
|
||||
wx1,
|
||||
wy1,
|
||||
wx1,
|
||||
wy2,
|
||||
!blockedUp && Renderer::camera.y > wy1,
|
||||
!blockedDown && Renderer::camera.y < wy2,
|
||||
false);
|
||||
#endif
|
||||
}
|
||||
|
||||
if(!blockedDown && Renderer::camera.y > wy2) {
|
||||
#if WITH_TEXTURES
|
||||
Renderer::DrawWall(
|
||||
texture,
|
||||
wx1,
|
||||
wy2,
|
||||
wx2,
|
||||
wy2,
|
||||
!blockedLeft && Renderer::camera.x > wx1,
|
||||
!blockedRight && Renderer::camera.x < wx2,
|
||||
false);
|
||||
#else
|
||||
Renderer::DrawWall(
|
||||
wx1,
|
||||
wy2,
|
||||
wx2,
|
||||
wy2,
|
||||
!blockedLeft && Renderer::camera.x > wx1,
|
||||
!blockedRight && Renderer::camera.x < wx2,
|
||||
false);
|
||||
#endif
|
||||
}
|
||||
|
||||
if(!blockedRight && Renderer::camera.x > wx2) {
|
||||
#if WITH_TEXTURES
|
||||
Renderer::DrawWall(
|
||||
texture,
|
||||
wx2,
|
||||
wy2,
|
||||
wx2,
|
||||
wy1,
|
||||
!blockedDown && Renderer::camera.y < wy2,
|
||||
!blockedUp && Renderer::camera.y > wy1,
|
||||
false);
|
||||
#else
|
||||
Renderer::DrawWall(
|
||||
wx2,
|
||||
wy2,
|
||||
wx2,
|
||||
wy1,
|
||||
!blockedDown && Renderer::camera.y < wy2,
|
||||
!blockedUp && Renderer::camera.y > wy1,
|
||||
false);
|
||||
#endif
|
||||
}
|
||||
|
||||
if(!blockedUp && Renderer::camera.y < wy1) {
|
||||
#if WITH_TEXTURES
|
||||
Renderer::DrawWall(
|
||||
texture,
|
||||
wx2,
|
||||
wy1,
|
||||
wx1,
|
||||
wy1,
|
||||
!blockedRight && Renderer::camera.x < wx2,
|
||||
!blockedLeft && Renderer::camera.x > wx1,
|
||||
false);
|
||||
#else
|
||||
Renderer::DrawWall(
|
||||
wx2,
|
||||
wy1,
|
||||
wx1,
|
||||
wy1,
|
||||
!blockedRight && Renderer::camera.x < wx2,
|
||||
!blockedLeft && Renderer::camera.x > wx1,
|
||||
false);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
if(ABS(Renderer::camera.rotCos) < ABS(Renderer::camera.rotSin)) {
|
||||
for(int8_t y = y1; y != y2; y += yd) {
|
||||
for(int8_t x = x1; x != x2; x += xd) {
|
||||
drawMenuCell(x, y);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for(int8_t x = x1; x != x2; x += xd) {
|
||||
for(int8_t y = y1; y != y2; y += yd) {
|
||||
drawMenuCell(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#undef MENU_SOLID_SAFE
|
||||
#undef MENU_SOLID
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Draw() {
|
||||
if (splashActive) {
|
||||
// Title screen converted from the user's own shareware WAD
|
||||
Platform::DrawSolidBitmap(0, 0, titleBitmapData);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawMenuRoom();
|
||||
|
||||
Font::PrintString(PSTR("FLIPPER DOOM"), 2, 40, COLOUR_WHITE);
|
||||
|
||||
for (uint8_t row = 0; row < VISIBLE_ROWS; ++row) {
|
||||
uint8_t idx = (uint8_t)(m_topIndex + row);
|
||||
if (idx >= MENU_ITEMS_COUNT) break;
|
||||
PrintItem(idx, (uint8_t)(MENU_FIRST_ROW + row));
|
||||
}
|
||||
|
||||
static uint8_t bubble = 0;
|
||||
static uint16_t lastFrameSeen = 0xFFFF;
|
||||
|
||||
const uint16_t frame = (uint16_t)Game::globalTickFrame;
|
||||
|
||||
if (frame != lastFrameSeen) {
|
||||
if ((frame & SHIFT_MASK) == 0) {
|
||||
bubble = (uint8_t)(bubble + 2);
|
||||
if (bubble >= kObjectsCount) bubble = (uint8_t)(bubble - kObjectsCount);
|
||||
if (bubble >= kObjectsCount) bubble = (uint8_t)(bubble - kObjectsCount);
|
||||
}
|
||||
lastFrameSeen = frame;
|
||||
}
|
||||
|
||||
const uint8_t num1 = bubble;
|
||||
uint8_t num2 = (uint8_t)(bubble + 1);
|
||||
if (num2 >= kObjectsCount) num2 = 0;
|
||||
|
||||
const ObjDesc& sprite1 = kObjects[num1];
|
||||
const ObjDesc& sprite2 = kObjects[num2];
|
||||
|
||||
const int animOffset = ((Game::globalTickFrame & 8) == 0) ? 32 : 0;
|
||||
const int off1 = sprite1.animated ? animOffset : 0;
|
||||
const int off2 = sprite2.animated ? animOffset : 0;
|
||||
|
||||
const uint16_t* torchSprite =
|
||||
(Game::globalTickFrame & 4) ? torchSpriteData1 : torchSpriteData2;
|
||||
|
||||
if (sprite1.invert) {
|
||||
Renderer::DrawScaled(sprite1.sprite + off1, 66, 29, 9, 255, true, COLOUR_BLACK);
|
||||
} else {
|
||||
Renderer::DrawScaled(sprite1.sprite + off1, 66, 29, 9, 255);
|
||||
}
|
||||
|
||||
if (sprite2.invert) {
|
||||
Renderer::DrawScaled(sprite2.sprite + off2, 96, 30, 9, 255, true, COLOUR_BLACK);
|
||||
} else {
|
||||
Renderer::DrawScaled(sprite2.sprite + off2, 96, 30, 9, 255);
|
||||
}
|
||||
|
||||
Renderer::DrawScaled(torchSprite, 0, 10, 9, 255);
|
||||
Renderer::DrawScaled(torchSprite, DISPLAY_WIDTH - 18, 10, 9, 255);
|
||||
|
||||
Font::PrintInt(m_save[sprite1.varsIndex], MENU_FIRST_ROW + 1, 86, COLOUR_WHITE);
|
||||
Font::PrintInt(m_save[sprite2.varsIndex], MENU_FIRST_ROW + 1, 116, COLOUR_WHITE);
|
||||
|
||||
Font::PrintString(PSTR(">"), (uint8_t)(MENU_FIRST_ROW + m_cursorPos), CURSOR_X, COLOUR_WHITE);
|
||||
}
|
||||
|
||||
void Menu::PrintItem(uint8_t idx, uint8_t row) {
|
||||
switch(idx) {
|
||||
case 0:
|
||||
Font::PrintString(PSTR("Play"), row, TEXT_X, COLOUR_WHITE);
|
||||
break;
|
||||
case 1:
|
||||
Font::PrintString(PSTR("Sound:"), row, TEXT_X, COLOUR_WHITE);
|
||||
Font::PrintString(
|
||||
Platform::IsAudioEnabled() ? PSTR("on") : PSTR("off"), row, TEXT_X + 28, COLOUR_WHITE);
|
||||
break;
|
||||
case 2:
|
||||
Font::PrintString(PSTR("Score:"), row, TEXT_X, COLOUR_WHITE);
|
||||
Font::PrintInt(m_score, row, TEXT_X + 28, COLOUR_WHITE);
|
||||
break;
|
||||
case 3:
|
||||
Font::PrintString(PSTR("High:"), row, TEXT_X, COLOUR_WHITE);
|
||||
Font::PrintInt(m_high, row, TEXT_X + 28, COLOUR_WHITE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Init() {
|
||||
m_selection = 0;
|
||||
m_topIndex = 0;
|
||||
m_cursorPos = 0;
|
||||
|
||||
splashTimer = 0;
|
||||
splashActive = true;
|
||||
}
|
||||
|
||||
void Menu::DrawEnteringLevel() {
|
||||
DrawMenuRoom();
|
||||
Font::PrintString(PSTR("Entering E1M"), 3, 34, COLOUR_BLACK);
|
||||
Font::PrintInt(Game::floor, 3, 82, COLOUR_BLACK);
|
||||
}
|
||||
|
||||
static int CountCharsInt(int v) {
|
||||
int n = 1;
|
||||
if(v < 0) {
|
||||
n++;
|
||||
v = -v;
|
||||
} // минус тоже символ
|
||||
while(v >= 10) {
|
||||
v /= 10;
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void PrintScoreCentered(int finalScore) {
|
||||
const int screenW = 128;
|
||||
|
||||
int n = CountCharsInt(finalScore);
|
||||
int textW = 4 * n - 1;
|
||||
|
||||
int x = (screenW - textW) / 2;
|
||||
|
||||
Font::PrintInt(finalScore, 2, x, COLOUR_BLACK);
|
||||
}
|
||||
|
||||
void Menu::DrawGameOver() {
|
||||
uint16_t finalScore = 0;
|
||||
constexpr int finishBonus = 500;
|
||||
constexpr int levelBonus = 20;
|
||||
constexpr int chestBonus = 15;
|
||||
constexpr int crownBonus = 10;
|
||||
constexpr int scrollBonus = 8;
|
||||
constexpr int coinsBonus = 4;
|
||||
constexpr int skeletonKillBonus = 10;
|
||||
constexpr int mageKillBonus = 10;
|
||||
constexpr int batKillBonus = 5;
|
||||
constexpr int spiderKillBonus = 4;
|
||||
|
||||
finalScore += (Game::floor - 1) * levelBonus;
|
||||
if(Game::stats.killedBy == EnemyType::None) finalScore += finishBonus;
|
||||
finalScore += Game::stats.chestsOpened * chestBonus;
|
||||
finalScore += Game::stats.crownsCollected * crownBonus;
|
||||
finalScore += Game::stats.scrollsCollected * scrollBonus;
|
||||
finalScore += Game::stats.coinsCollected * coinsBonus;
|
||||
finalScore += Game::stats.enemyKills[(int)EnemyType::Skeleton] * skeletonKillBonus;
|
||||
finalScore += Game::stats.enemyKills[(int)EnemyType::Mage] * mageKillBonus;
|
||||
finalScore += Game::stats.enemyKills[(int)EnemyType::Bat] * batKillBonus;
|
||||
finalScore += Game::stats.enemyKills[(int)EnemyType::Spider] * spiderKillBonus;
|
||||
|
||||
DrawMenuRoom();
|
||||
PrintScoreCentered(finalScore);
|
||||
|
||||
switch(Game::stats.killedBy) {
|
||||
case EnemyType::Exit:
|
||||
Font::PrintString(PSTR("You have left the game."), 1, 18, COLOUR_BLACK);
|
||||
break;
|
||||
case EnemyType::None:
|
||||
Font::PrintString(PSTR("You escaped the base!"), 1, 22, COLOUR_BLACK);
|
||||
break;
|
||||
case EnemyType::Mage:
|
||||
Font::PrintString(PSTR("Killed by an imp on level"), 1, 8, COLOUR_BLACK);
|
||||
Font::PrintInt(Game::floor, 1, 112, COLOUR_BLACK);
|
||||
break;
|
||||
case EnemyType::Skeleton:
|
||||
Font::PrintString(PSTR("Killed by a demon on level"), 1, 6, COLOUR_BLACK);
|
||||
Font::PrintInt(Game::floor, 1, 114, COLOUR_BLACK);
|
||||
break;
|
||||
case EnemyType::Bat:
|
||||
Font::PrintString(PSTR("Killed by a sergeant on level"), 1, 2, COLOUR_BLACK);
|
||||
Font::PrintInt(Game::floor, 1, 120, COLOUR_BLACK);
|
||||
break;
|
||||
case EnemyType::Spider:
|
||||
Font::PrintString(PSTR("Killed by a zombie on level"), 1, 4, COLOUR_BLACK);
|
||||
Font::PrintInt(Game::floor, 1, 116, COLOUR_BLACK);
|
||||
break;
|
||||
}
|
||||
|
||||
constexpr uint8_t firstRow = 21;
|
||||
constexpr uint8_t secondRow = 38;
|
||||
|
||||
int offset = (Game::globalTickFrame & 8) == 0 ? 32 : 0;
|
||||
|
||||
Renderer::DrawScaled(chestSpriteData, 6, firstRow, 9, 255, false, COLOUR_WHITE);
|
||||
Font::PrintInt(Game::stats.chestsOpened, 4, 24, COLOUR_BLACK);
|
||||
|
||||
Renderer::DrawScaled(crownSpriteData, 6, secondRow, 9, 255, false, COLOUR_WHITE);
|
||||
Font::PrintInt(Game::stats.crownsCollected, 6, 24, COLOUR_BLACK);
|
||||
|
||||
Renderer::DrawScaled(scrollSpriteData, 36, firstRow, 9, 255, false, COLOUR_WHITE);
|
||||
Font::PrintInt(Game::stats.scrollsCollected, 4, 54, COLOUR_BLACK);
|
||||
|
||||
Renderer::DrawScaled(coinsSpriteData, 36, secondRow, 9, 255, false, COLOUR_WHITE);
|
||||
Font::PrintInt(Game::stats.coinsCollected, 6, 54, COLOUR_BLACK);
|
||||
|
||||
Renderer::DrawScaled(skeletonSpriteData + offset, 72, firstRow, 9, 255, false, COLOUR_WHITE);
|
||||
Font::PrintInt(Game::stats.enemyKills[(int)EnemyType::Skeleton], 4, 90, COLOUR_BLACK);
|
||||
|
||||
Renderer::DrawScaled(mageSpriteData + offset, 72, secondRow, 9, 255, false, COLOUR_WHITE);
|
||||
Font::PrintInt(Game::stats.enemyKills[(int)EnemyType::Mage], 6, 90, COLOUR_BLACK);
|
||||
|
||||
Renderer::DrawScaled(batSpriteData + offset, 102, firstRow, 9, 255, false, COLOUR_WHITE);
|
||||
Font::PrintInt(Game::stats.enemyKills[(int)EnemyType::Bat], 4, 120, COLOUR_BLACK);
|
||||
|
||||
Renderer::DrawScaled(spiderSpriteData + offset, 102, secondRow, 9, 255, false, COLOUR_WHITE);
|
||||
Font::PrintInt(Game::stats.enemyKills[(int)EnemyType::Spider], 6, 120, COLOUR_BLACK);
|
||||
|
||||
m_save[0] = Game::stats.chestsOpened;
|
||||
m_save[1] = Game::stats.crownsCollected;
|
||||
m_save[2] = Game::stats.scrollsCollected;
|
||||
m_save[3] = Game::stats.coinsCollected;
|
||||
m_save[4] = Game::stats.enemyKills[(int)EnemyType::Skeleton];
|
||||
m_save[5] = Game::stats.enemyKills[(int)EnemyType::Mage];
|
||||
m_save[6] = Game::stats.enemyKills[(int)EnemyType::Bat];
|
||||
m_save[7] = Game::stats.enemyKills[(int)EnemyType::Spider];
|
||||
m_save[8] = 0;
|
||||
|
||||
if(Game::floor > 0) {
|
||||
m_save[8] = Game::floor - 1;
|
||||
}
|
||||
|
||||
SetScore(finalScore);
|
||||
}
|
||||
|
||||
void Menu::Tick() {
|
||||
static uint8_t lastInput = 0;
|
||||
uint8_t input = Platform::GetInput();
|
||||
|
||||
if(splashActive) {
|
||||
if(splashTimer < SPLASH_TIME_TICKS) splashTimer++;
|
||||
if(splashTimer >= SPLASH_TIME_TICKS) splashActive = false;
|
||||
// any key skips the title screen
|
||||
if(input && !lastInput && splashTimer > 8) splashActive = false;
|
||||
lastInput = input;
|
||||
return;
|
||||
}
|
||||
|
||||
auto syncWindow = [&]() {
|
||||
uint8_t maxTop = MaxTop();
|
||||
|
||||
if(m_selection < m_topIndex) m_topIndex = m_selection;
|
||||
|
||||
uint8_t end = (uint8_t)(m_topIndex + (VISIBLE_ROWS - 1));
|
||||
if(m_selection > end) {
|
||||
int t = (int)m_selection - (VISIBLE_ROWS - 1);
|
||||
if(t < 0) t = 0;
|
||||
if(t > maxTop) t = maxTop;
|
||||
m_topIndex = (uint8_t)t;
|
||||
}
|
||||
|
||||
m_cursorPos = (uint8_t)(m_selection - m_topIndex);
|
||||
if(m_cursorPos >= VISIBLE_ROWS) m_cursorPos = (VISIBLE_ROWS - 1);
|
||||
};
|
||||
|
||||
if((input & INPUT_DOWN) && !(lastInput & INPUT_DOWN)) {
|
||||
uint8_t next = Wrap((int)m_selection + 1, MENU_ITEMS_COUNT);
|
||||
|
||||
if(m_cursorPos < (VISIBLE_ROWS - 1)) {
|
||||
m_selection = next;
|
||||
m_cursorPos++;
|
||||
} else {
|
||||
m_selection = next;
|
||||
|
||||
if(m_topIndex < MaxTop()) {
|
||||
m_topIndex++;
|
||||
} else {
|
||||
m_topIndex = 0;
|
||||
m_cursorPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
syncWindow();
|
||||
}
|
||||
|
||||
if((input & INPUT_UP) && !(lastInput & INPUT_UP)) {
|
||||
uint8_t prev = Wrap((int)m_selection - 1, MENU_ITEMS_COUNT);
|
||||
|
||||
if(m_cursorPos > 0) {
|
||||
m_selection = prev;
|
||||
m_cursorPos--;
|
||||
} else {
|
||||
m_selection = prev;
|
||||
|
||||
if(m_topIndex > 0) {
|
||||
m_topIndex--;
|
||||
} else {
|
||||
m_topIndex = MaxTop();
|
||||
m_cursorPos = (VISIBLE_ROWS - 1);
|
||||
}
|
||||
}
|
||||
|
||||
syncWindow();
|
||||
}
|
||||
|
||||
if((input & (INPUT_A | INPUT_B)) && !(lastInput & (INPUT_A | INPUT_B))) {
|
||||
switch(m_selection) {
|
||||
case 0:
|
||||
Game::StartGame();
|
||||
break;
|
||||
case 1:
|
||||
Platform::SetAudioEnabled(!Platform::IsAudioEnabled());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lastInput = input;
|
||||
}
|
||||
|
||||
void Menu::TickEnteringLevel() {
|
||||
constexpr uint8_t showTime = 45;
|
||||
if(timer < showTime) timer++;
|
||||
if(timer == showTime && Platform::GetInput() == 0) {
|
||||
Game::StartLevel();
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::TickGameOver() {
|
||||
constexpr uint8_t minShowTime = 30;
|
||||
|
||||
if(timer < minShowTime) timer++;
|
||||
|
||||
if(timer == minShowTime && (Platform::GetInput() & (INPUT_A | INPUT_B))) {
|
||||
timer++;
|
||||
} else if(timer == minShowTime + 1 && Platform::GetInput() == 0) {
|
||||
Game::SwitchState(Game::State::Menu);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void DrawEraseTile8x8(int16_t x, int16_t y, const uint8_t* frame8bytes) {
|
||||
for(uint8_t row = 0; row < 8; row++) {
|
||||
uint8_t rowMask = pgm_read_byte(frame8bytes + row);
|
||||
while(rowMask) {
|
||||
uint8_t b = (uint8_t)__builtin_ctz((unsigned)rowMask);
|
||||
uint8_t col = 7 - b;
|
||||
Platform::PutPixel(x + col, y + row, COLOUR_WHITE);
|
||||
rowMask &= (uint8_t)(rowMask - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::DrawTransitionFrame(uint8_t frameIndex) {
|
||||
const uint8_t w = pgm_read_byte(transitionSet + 0);
|
||||
const uint8_t h = pgm_read_byte(transitionSet + 1);
|
||||
(void)w;
|
||||
(void)h;
|
||||
|
||||
const uint8_t* framePtr = transitionSet + 2 + (uint16_t)frameIndex * 8;
|
||||
int16_t tileX = 120;
|
||||
int16_t tileY = 56;
|
||||
while(true) {
|
||||
DrawEraseTile8x8(tileX, tileY, framePtr);
|
||||
|
||||
tileX -= 8;
|
||||
if(tileX < 0) {
|
||||
tileX = 120;
|
||||
tileY -= 8;
|
||||
if(tileY < 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::ResetTimer() {
|
||||
timer = 0;
|
||||
}
|
||||
|
||||
static constexpr uint8_t TOTAL_TIME = 40;
|
||||
static constexpr uint8_t TOTAL_FRAMES = 8;
|
||||
|
||||
void Menu::RunTransition(Menu* menu, uint8_t& t, TransitionNextFn next) {
|
||||
uint8_t frameIndex = (uint16_t)t * TOTAL_FRAMES / TOTAL_TIME;
|
||||
if(frameIndex >= TOTAL_FRAMES) frameIndex = TOTAL_FRAMES - 1;
|
||||
menu->DrawTransitionFrame(frameIndex);
|
||||
if(frameIndex >= TOTAL_FRAMES - 2) {
|
||||
t = 0;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
++t;
|
||||
}
|
||||
|
||||
void Menu::FadeOut() {
|
||||
static uint8_t t = 0;
|
||||
RunTransition(this, t, +[]() { Game::SwitchState(Game::State::GameOver); });
|
||||
}
|
||||
|
||||
void Menu::ReadSave() {
|
||||
uint8_t addr = EEPROM_BASE_ADDR;
|
||||
m_score = (uint16_t)EEPROM.read(addr) | ((uint16_t)EEPROM.read(addr + 1) << 8); addr += 2;
|
||||
m_high = (uint16_t)EEPROM.read(addr) | ((uint16_t)EEPROM.read(addr + 1) << 8); addr += 2;
|
||||
m_storedHigh = m_high;
|
||||
|
||||
for(int i = 0; i < 9; i++) {
|
||||
m_save[i] = EEPROM.read(addr++);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::SetScore(uint16_t score) {
|
||||
if(score == 0) return;
|
||||
m_high = (score > m_storedHigh) ? score : m_storedHigh;
|
||||
m_score = score;
|
||||
}
|
||||
|
||||
void Menu::WriteSave() {
|
||||
uint8_t addr = EEPROM_BASE_ADDR;
|
||||
EEPROM.update(addr++, (uint8_t)(m_score & 0xFF));
|
||||
EEPROM.update(addr++, (uint8_t)(m_score >> 8));
|
||||
EEPROM.update(addr++, (uint8_t)(m_high & 0xFF));
|
||||
EEPROM.update(addr++, (uint8_t)(m_high >> 8));
|
||||
|
||||
for(int i = 0; i < 9; i++) {
|
||||
EEPROM.update(addr++, m_save[i]);
|
||||
}
|
||||
EEPROM.commit();
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// 8x8, 8 кадров (0..7) — как в первом коде
|
||||
static const uint8_t PROGMEM transitionSet[] = {
|
||||
8,
|
||||
8,
|
||||
// FRAME 00
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
// FRAME 01
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
// FRAME 02
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
// FRAME 03
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
// FRAME 04
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
// FRAME 05
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
0xFF,
|
||||
0xAA,
|
||||
// FRAME 06
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x00,
|
||||
// FRAME 07
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
0x55,
|
||||
0x00,
|
||||
};
|
||||
|
||||
class Menu {
|
||||
public:
|
||||
void Init();
|
||||
void Draw();
|
||||
void Tick();
|
||||
|
||||
void ReadSave();
|
||||
void WriteSave();
|
||||
|
||||
void TickEnteringLevel();
|
||||
void DrawEnteringLevel();
|
||||
void TransitionToLevel();
|
||||
|
||||
void TickGameOver();
|
||||
void DrawGameOver();
|
||||
|
||||
void ResetTimer();
|
||||
void FadeOut();
|
||||
|
||||
private:
|
||||
using TransitionNextFn = void (*)();
|
||||
static void RunTransition(Menu* menu, uint8_t& t, TransitionNextFn next);
|
||||
void DrawTransitionFrame(uint8_t frameIndex);
|
||||
|
||||
void SetScore(uint16_t score);
|
||||
void PrintItem(uint8_t idx, uint8_t row);
|
||||
|
||||
uint8_t m_selection = 0;
|
||||
uint8_t m_topIndex = 0;
|
||||
uint8_t m_cursorPos = 0;
|
||||
|
||||
uint16_t m_score = 0;
|
||||
uint16_t m_high = 0;
|
||||
uint16_t m_storedHigh = 0;
|
||||
uint8_t m_save[9] = {0};
|
||||
|
||||
union {
|
||||
uint16_t timer;
|
||||
uint16_t fizzleFade;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "game/Particle.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Platform.h"
|
||||
|
||||
ParticleSystem ParticleSystemManager::systems[MAX_SYSTEMS];
|
||||
|
||||
void ParticleSystem::Init()
|
||||
{
|
||||
life = 0;
|
||||
}
|
||||
|
||||
void ParticleSystem::Step()
|
||||
{
|
||||
for (Particle& p : particles)
|
||||
{
|
||||
if (p.IsActive())
|
||||
{
|
||||
p.velY += gravity;
|
||||
|
||||
if (p.x + p.velX < -127 || p.x + p.velX > 127 || p.y + p.velY < -127)
|
||||
{
|
||||
p.x = -128;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (p.y + p.velY >= 128)
|
||||
{
|
||||
p.velY = p.velX = 0;
|
||||
p.y = 127;
|
||||
}
|
||||
|
||||
p.x += p.velX;
|
||||
p.y += p.velY;
|
||||
}
|
||||
}
|
||||
|
||||
life--;
|
||||
}
|
||||
|
||||
void ParticleSystem::Draw(int x, int halfScale)
|
||||
{
|
||||
int scale = 2 * halfScale;
|
||||
int8_t horizon = Renderer::GetHorizon(x);
|
||||
uint8_t colour = isWhite ? COLOUR_WHITE : COLOUR_BLACK;
|
||||
|
||||
for (Particle& p : particles)
|
||||
{
|
||||
if (p.IsActive())
|
||||
{
|
||||
int outX = x + ((p.x * scale) >> 8);
|
||||
int outY = horizon + ((p.y * scale) >> 8);
|
||||
|
||||
if (outX >= 0 && outY >= 0 && outX < DISPLAY_WIDTH - 1 && outY < DISPLAY_HEIGHT - 1 && halfScale >= Renderer::wBuffer[outX])
|
||||
{
|
||||
Platform::PutPixel(outX, outY, colour);
|
||||
Platform::PutPixel(outX + 1, outY, colour);
|
||||
Platform::PutPixel(outX + 1, outY + 1, colour);
|
||||
Platform::PutPixel(outX, outY + 1, colour);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystem::Explode()
|
||||
{
|
||||
for (Particle& p : particles)
|
||||
{
|
||||
p.x = (Random() & 31) - 16;
|
||||
p.y = (Random() & 31) - 16;
|
||||
|
||||
p.velX = (Random() & 31) - 16;
|
||||
p.velY = (Random() & 31) - 25;
|
||||
}
|
||||
|
||||
life = 22;
|
||||
}
|
||||
|
||||
void ParticleSystemManager::Draw()
|
||||
{
|
||||
for (ParticleSystem& system : systems)
|
||||
{
|
||||
if(system.IsActive())
|
||||
{
|
||||
int16_t screenX, screenW;
|
||||
|
||||
if(Renderer::TransformAndCull(system.worldX, system.worldY, screenX, screenW))
|
||||
{
|
||||
QueuedDrawable* drawable = Renderer::CreateQueuedDrawable((uint8_t)screenW);
|
||||
if(drawable)
|
||||
{
|
||||
drawable->type = DrawableType::ParticleSystem;
|
||||
drawable->x = (int8_t)screenX;
|
||||
drawable->inverseCameraDistance = (uint8_t)screenW;
|
||||
drawable->particleSystem = &system;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystemManager::Init()
|
||||
{
|
||||
for (ParticleSystem& system : systems)
|
||||
{
|
||||
system.Init();
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystemManager::Update()
|
||||
{
|
||||
for (ParticleSystem& system : systems)
|
||||
{
|
||||
if(system.IsActive())
|
||||
{
|
||||
system.Step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystemManager::CreateExplosion(int16_t worldX, int16_t worldY, bool isWhite)
|
||||
{
|
||||
ParticleSystem* newSystem = nullptr;
|
||||
for(ParticleSystem& system : systems)
|
||||
{
|
||||
if(!system.IsActive())
|
||||
{
|
||||
newSystem = &system;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!newSystem)
|
||||
{
|
||||
newSystem = &systems[0];
|
||||
|
||||
for (uint8_t n = 1; n < MAX_SYSTEMS; n++)
|
||||
{
|
||||
if (systems[n].life < newSystem->life)
|
||||
{
|
||||
newSystem = &systems[n];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newSystem->worldX = worldX;
|
||||
newSystem->worldY = worldY;
|
||||
newSystem->isWhite = isWhite;
|
||||
newSystem->Explode();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Defines.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Game.h"
|
||||
|
||||
struct Particle
|
||||
{
|
||||
int8_t x, y;
|
||||
int8_t velX, velY;
|
||||
|
||||
inline bool IsActive() { return x != -128; }
|
||||
};
|
||||
|
||||
struct ParticleSystem
|
||||
{
|
||||
static constexpr int8_t gravity = 3;
|
||||
int16_t worldX, worldY;
|
||||
bool isWhite : 1;
|
||||
uint8_t life : 7;
|
||||
Particle particles[PARTICLES_PER_SYSTEM];
|
||||
|
||||
bool IsActive() { return life > 0; }
|
||||
|
||||
void Init();
|
||||
void Step();
|
||||
void Draw(int x, int scale);
|
||||
void Explode();
|
||||
};
|
||||
|
||||
class ParticleSystemManager
|
||||
{
|
||||
public:
|
||||
static constexpr int MAX_SYSTEMS = 3;
|
||||
static ParticleSystem systems[MAX_SYSTEMS];
|
||||
|
||||
static void Init();
|
||||
static void Draw();
|
||||
static void Update();
|
||||
static void CreateExplosion(int16_t x, int16_t y, bool isWhite = false);
|
||||
};
|
||||
@@ -0,0 +1,365 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <notification/notification.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "lib/flipper.h"
|
||||
|
||||
#define COLOUR_WHITE 0
|
||||
#define COLOUR_BLACK 1
|
||||
|
||||
#include "game/Game.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Defines.h"
|
||||
#include "game/Sounds.h"
|
||||
#include "lib/EEPROM.h"
|
||||
|
||||
// ---------------- SOUND ----------------
|
||||
|
||||
typedef struct {
|
||||
const uint16_t* pattern;
|
||||
} SoundRequest;
|
||||
|
||||
static FuriMessageQueue* g_sound_queue = NULL;
|
||||
static FuriThread* g_sound_thread = NULL;
|
||||
static volatile bool g_sound_thread_running = false;
|
||||
static const float kSoundVolume = 1.0f;
|
||||
static const uint32_t kToneTickHz = 780;
|
||||
|
||||
static inline uint32_t arduboy_ticks_to_ms(uint16_t ticks) {
|
||||
return (uint32_t)((ticks * 1000u + (kToneTickHz / 2)) / kToneTickHz);
|
||||
}
|
||||
|
||||
static int32_t sound_thread_fn(void* ctx) {
|
||||
UNUSED(ctx);
|
||||
SoundRequest req;
|
||||
|
||||
while(g_sound_thread_running) {
|
||||
if(furi_message_queue_get(g_sound_queue, &req, 50) != FuriStatusOk) continue;
|
||||
if(!g_state || !g_state->audio_enabled || !req.pattern) continue;
|
||||
if(!furi_hal_speaker_acquire(50)) continue;
|
||||
|
||||
const uint16_t* p = req.pattern;
|
||||
while(g_sound_thread_running && g_state && g_state->audio_enabled) {
|
||||
SoundRequest new_req;
|
||||
if(furi_message_queue_get(g_sound_queue, &new_req, 0) == FuriStatusOk) {
|
||||
if(new_req.pattern) p = new_req.pattern;
|
||||
}
|
||||
|
||||
uint16_t freq = *p++;
|
||||
if(freq == TONES_END) break;
|
||||
|
||||
uint16_t dur_ticks = *p++;
|
||||
uint32_t dur_ms = arduboy_ticks_to_ms(dur_ticks);
|
||||
if(dur_ms == 0) dur_ms = 1;
|
||||
|
||||
if(freq == 0) {
|
||||
furi_hal_speaker_stop();
|
||||
furi_delay_ms(dur_ms);
|
||||
} else {
|
||||
furi_hal_speaker_start((float)freq, kSoundVolume);
|
||||
furi_delay_ms(dur_ms);
|
||||
furi_hal_speaker_stop();
|
||||
}
|
||||
}
|
||||
|
||||
furi_hal_speaker_stop();
|
||||
furi_hal_speaker_release();
|
||||
}
|
||||
|
||||
if(furi_hal_speaker_is_mine()) {
|
||||
furi_hal_speaker_stop();
|
||||
furi_hal_speaker_release();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sound_system_init() {
|
||||
if(g_sound_queue || g_sound_thread) return;
|
||||
|
||||
g_sound_queue = furi_message_queue_alloc(4, sizeof(SoundRequest));
|
||||
|
||||
g_sound_thread = furi_thread_alloc();
|
||||
furi_thread_set_name(g_sound_thread, "GameSound");
|
||||
furi_thread_set_stack_size(g_sound_thread, 1024);
|
||||
furi_thread_set_priority(g_sound_thread, FuriThreadPriorityNormal);
|
||||
furi_thread_set_callback(g_sound_thread, sound_thread_fn);
|
||||
|
||||
g_sound_thread_running = true;
|
||||
furi_thread_start(g_sound_thread);
|
||||
}
|
||||
|
||||
static void sound_system_deinit() {
|
||||
if(!g_sound_thread) return;
|
||||
|
||||
g_sound_thread_running = false;
|
||||
furi_thread_join(g_sound_thread);
|
||||
furi_thread_free(g_sound_thread);
|
||||
g_sound_thread = NULL;
|
||||
|
||||
if(g_sound_queue) {
|
||||
furi_message_queue_free(g_sound_queue);
|
||||
g_sound_queue = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::PlaySound(const uint16_t* audioPattern) {
|
||||
if(!g_state || !g_state->audio_enabled || !audioPattern || !g_sound_queue) return;
|
||||
|
||||
SoundRequest req = {.pattern = audioPattern};
|
||||
if(furi_message_queue_put(g_sound_queue, &req, 0) != FuriStatusOk) {
|
||||
SoundRequest dummy;
|
||||
(void)furi_message_queue_get(g_sound_queue, &dummy, 0);
|
||||
(void)furi_message_queue_put(g_sound_queue, &req, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool Platform::IsAudioEnabled() {
|
||||
return g_state && g_state->audio_enabled;
|
||||
}
|
||||
|
||||
void Platform::SetAudioEnabled(bool enabled) {
|
||||
if(!g_state) return;
|
||||
bool was_enabled = g_state->audio_enabled;
|
||||
g_state->audio_enabled = enabled;
|
||||
if(enabled && !was_enabled)
|
||||
sound_system_init();
|
||||
else if(!enabled && was_enabled)
|
||||
sound_system_deinit();
|
||||
}
|
||||
|
||||
// ---------------- INPUT ----------------
|
||||
|
||||
uint8_t Platform::GetInput() {
|
||||
return g_state ? g_state->input_state : 0;
|
||||
}
|
||||
|
||||
// ---------------- DRAW ----------------
|
||||
|
||||
static constexpr uint8_t kDisplayPages = DISPLAY_HEIGHT / 8;
|
||||
|
||||
static inline int16_t floor_div8(int16_t value) {
|
||||
return (value >= 0) ? (value >> 3) : (int16_t)(-(((-value) + 7) >> 3));
|
||||
}
|
||||
|
||||
static inline void set_pixel(int16_t x, int16_t y, bool color) {
|
||||
if(!g_state) return;
|
||||
if((uint16_t)x >= DISPLAY_WIDTH || (uint16_t)y >= DISPLAY_HEIGHT) return;
|
||||
|
||||
uint8_t* buf = g_state->back_buffer;
|
||||
uint16_t idx = (uint16_t)(x + (y >> 3) * DISPLAY_WIDTH);
|
||||
uint8_t mask = (uint8_t)(1u << (y & 7));
|
||||
|
||||
if(color)
|
||||
buf[idx] |= mask;
|
||||
else
|
||||
buf[idx] &= (uint8_t)~mask;
|
||||
}
|
||||
|
||||
void Platform::PutPixel(uint8_t x, uint8_t y, uint8_t color) {
|
||||
set_pixel(x, y, color);
|
||||
}
|
||||
|
||||
void Platform::FillScreen(uint8_t color) {
|
||||
if(!g_state) return;
|
||||
memset(g_state->back_buffer, color ? 0xFF : 0x00, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
uint8_t* Platform::GetScreenBuffer() {
|
||||
return g_state ? g_state->back_buffer : NULL;
|
||||
}
|
||||
|
||||
void Platform::DrawVLine(uint8_t x, int8_t y0, int8_t y1, uint8_t pattern) {
|
||||
if(!g_state || pattern == 0 || x >= DISPLAY_WIDTH) return;
|
||||
|
||||
int16_t top = y0;
|
||||
int16_t bottom = y1;
|
||||
|
||||
if(top > bottom) {
|
||||
int16_t t = top;
|
||||
top = bottom;
|
||||
bottom = t;
|
||||
}
|
||||
|
||||
if(bottom < 0 || top >= DISPLAY_HEIGHT) return;
|
||||
|
||||
if(top < 0) top = 0;
|
||||
if(bottom >= DISPLAY_HEIGHT) bottom = DISPLAY_HEIGHT - 1;
|
||||
|
||||
uint8_t start_page = (uint8_t)(top >> 3);
|
||||
uint8_t end_page = (uint8_t)(bottom >> 3);
|
||||
uint8_t start_bit = (uint8_t)(top & 7);
|
||||
uint8_t end_bit = (uint8_t)(bottom & 7);
|
||||
|
||||
uint8_t* buf = g_state->back_buffer;
|
||||
|
||||
if(start_page == end_page) {
|
||||
uint8_t clip_mask =
|
||||
(uint8_t)((uint8_t)(0xFFu << start_bit) & (uint8_t)(0xFFu >> (7u - end_bit)));
|
||||
buf[(uint16_t)x + (uint16_t)start_page * DISPLAY_WIDTH] |= (uint8_t)(pattern & clip_mask);
|
||||
return;
|
||||
}
|
||||
|
||||
buf[(uint16_t)x + (uint16_t)start_page * DISPLAY_WIDTH] |=
|
||||
(uint8_t)(pattern & (uint8_t)(0xFFu << start_bit));
|
||||
|
||||
for(uint8_t page = (uint8_t)(start_page + 1); page < end_page; page++) {
|
||||
buf[(uint16_t)x + (uint16_t)page * DISPLAY_WIDTH] |= pattern;
|
||||
}
|
||||
|
||||
buf[(uint16_t)x + (uint16_t)end_page * DISPLAY_WIDTH] |=
|
||||
(uint8_t)(pattern & (uint8_t)(0xFFu >> (7u - end_bit)));
|
||||
}
|
||||
|
||||
static inline uint8_t get_page_mask(uint8_t page, uint8_t total_pages, uint8_t height) {
|
||||
if((page + 1u) != total_pages) return 0xFFu;
|
||||
uint8_t tail_bits = (uint8_t)(height & 7u);
|
||||
return tail_bits ? (uint8_t)((1u << tail_bits) - 1u) : 0xFFu;
|
||||
}
|
||||
|
||||
void Platform::DrawBitmap(int16_t x, int16_t y, const uint8_t* bmp) {
|
||||
if(!g_state || !bmp) return;
|
||||
|
||||
uint8_t w = bmp[0];
|
||||
uint8_t h = bmp[1];
|
||||
if(!w || !h) return;
|
||||
|
||||
int16_t x0 = x < 0 ? 0 : x;
|
||||
int16_t x1 = x + w;
|
||||
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
|
||||
if(x0 >= x1) return;
|
||||
|
||||
const uint8_t* data = bmp + 2;
|
||||
uint8_t pages = (uint8_t)((h + 7u) >> 3);
|
||||
uint8_t* dst = g_state->back_buffer;
|
||||
|
||||
for(int16_t dx = x0; dx < x1; dx++) {
|
||||
uint8_t sx = (uint8_t)(dx - x);
|
||||
const uint8_t* src_col = data + sx;
|
||||
|
||||
for(uint8_t page = 0; page < pages; page++) {
|
||||
int16_t base_y = y + ((int16_t)page << 3);
|
||||
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
|
||||
|
||||
uint8_t src = src_col[(uint16_t)page * w] & get_page_mask(page, pages, h);
|
||||
if(src == 0) continue;
|
||||
|
||||
int16_t dst_page = floor_div8(base_y);
|
||||
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
|
||||
|
||||
uint8_t low = (uint8_t)(src << y_shift);
|
||||
if((uint16_t)dst_page < kDisplayPages) {
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
|
||||
dst[idx] &= (uint8_t)~low;
|
||||
}
|
||||
|
||||
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
|
||||
uint8_t high = (uint8_t)(src >> (8u - y_shift));
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
|
||||
dst[idx] &= (uint8_t)~high;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::DrawSolidBitmap(int16_t x, int16_t y, const uint8_t* bmp) {
|
||||
if(!g_state || !bmp) return;
|
||||
|
||||
uint8_t w = bmp[0];
|
||||
uint8_t h = bmp[1];
|
||||
if(!w || !h) return;
|
||||
|
||||
int16_t x0 = x < 0 ? 0 : x;
|
||||
int16_t x1 = x + w;
|
||||
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
|
||||
if(x0 >= x1) return;
|
||||
|
||||
const uint8_t* data = bmp + 2;
|
||||
uint8_t pages = (uint8_t)((h + 7u) >> 3);
|
||||
uint8_t* dst = g_state->back_buffer;
|
||||
|
||||
for(int16_t dx = x0; dx < x1; dx++) {
|
||||
uint8_t sx = (uint8_t)(dx - x);
|
||||
const uint8_t* src_col = data + sx;
|
||||
|
||||
for(uint8_t page = 0; page < pages; page++) {
|
||||
int16_t base_y = y + ((int16_t)page << 3);
|
||||
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
|
||||
|
||||
uint8_t page_mask = get_page_mask(page, pages, h);
|
||||
uint8_t src = src_col[(uint16_t)page * w] & page_mask;
|
||||
uint8_t fill = (uint8_t)(~src) & page_mask;
|
||||
|
||||
int16_t dst_page = floor_div8(base_y);
|
||||
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
|
||||
|
||||
uint8_t region_low = (uint8_t)(page_mask << y_shift);
|
||||
uint8_t fill_low = (uint8_t)(fill << y_shift);
|
||||
if((uint16_t)dst_page < kDisplayPages) {
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
|
||||
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_low) | fill_low);
|
||||
}
|
||||
|
||||
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
|
||||
uint8_t region_high = (uint8_t)(page_mask >> (8u - y_shift));
|
||||
uint8_t fill_high = (uint8_t)(fill >> (8u - y_shift));
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
|
||||
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_high) | fill_high);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::DrawSprite(int16_t x, int16_t y, const uint8_t* bmp, uint8_t frame) {
|
||||
if(!g_state || !bmp) return;
|
||||
|
||||
uint8_t w = bmp[0];
|
||||
uint8_t h = bmp[1];
|
||||
if(!w || !h) return;
|
||||
|
||||
int16_t x0 = x < 0 ? 0 : x;
|
||||
int16_t x1 = x + w;
|
||||
if(x1 > DISPLAY_WIDTH) x1 = DISPLAY_WIDTH;
|
||||
if(x0 >= x1) return;
|
||||
|
||||
uint8_t pages = (uint8_t)((h + 7u) >> 3);
|
||||
uint16_t frame_size = (uint16_t)(w * pages);
|
||||
const uint8_t* data = bmp + 2 + (uint32_t)frame * frame_size * 2u;
|
||||
uint8_t* dst = g_state->back_buffer;
|
||||
|
||||
for(int16_t dx = x0; dx < x1; dx++) {
|
||||
uint8_t sx = (uint8_t)(dx - x);
|
||||
|
||||
for(uint8_t page = 0; page < pages; page++) {
|
||||
int16_t base_y = y + ((int16_t)page << 3);
|
||||
if(base_y <= -8 || base_y >= DISPLAY_HEIGHT) continue;
|
||||
|
||||
uint16_t src_index = (uint16_t)((page * w + sx) * 2u);
|
||||
uint8_t src = data[src_index];
|
||||
uint8_t mask = data[src_index + 1] & get_page_mask(page, pages, h);
|
||||
if(mask == 0) continue;
|
||||
|
||||
uint8_t fill = (uint8_t)(src & mask);
|
||||
int16_t dst_page = floor_div8(base_y);
|
||||
uint8_t y_shift = (uint8_t)(base_y - (dst_page << 3));
|
||||
|
||||
uint8_t region_low = (uint8_t)(mask << y_shift);
|
||||
uint8_t fill_low = (uint8_t)(fill << y_shift);
|
||||
if((uint16_t)dst_page < kDisplayPages) {
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)dst_page * DISPLAY_WIDTH;
|
||||
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_low) | fill_low);
|
||||
}
|
||||
|
||||
if(y_shift && (uint16_t)(dst_page + 1) < kDisplayPages) {
|
||||
uint8_t region_high = (uint8_t)(mask >> (8u - y_shift));
|
||||
uint8_t fill_high = (uint8_t)(fill >> (8u - y_shift));
|
||||
uint16_t idx = (uint16_t)dx + (uint16_t)(dst_page + 1) * DISPLAY_WIDTH;
|
||||
dst[idx] = (uint8_t)((dst[idx] & (uint8_t)~region_high) | fill_high);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Platform
|
||||
{
|
||||
public:
|
||||
static uint8_t GetInput(void);
|
||||
static uint8_t* GetScreenBuffer();
|
||||
|
||||
static void PlaySound(const uint16_t* audioPattern);
|
||||
static bool IsAudioEnabled();
|
||||
static void SetAudioEnabled(bool isEnabled);
|
||||
|
||||
static void FillScreen(uint8_t col);
|
||||
static void PutPixel(uint8_t x, uint8_t y, uint8_t colour);
|
||||
static void DrawBitmap(int16_t x, int16_t y, const uint8_t *bitmap);
|
||||
static void DrawSolidBitmap(int16_t x, int16_t y, const uint8_t *bitmap);
|
||||
static void DrawSprite(int16_t x, int16_t y, const uint8_t *bitmap, const uint8_t *mask, uint8_t frame, uint8_t mask_frame);
|
||||
static void DrawSprite(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame);
|
||||
|
||||
static void DrawVLine(uint8_t x, int8_t y1, int8_t y2, uint8_t pattern);
|
||||
static void DrawBackground();
|
||||
};
|
||||
@@ -0,0 +1,328 @@
|
||||
#include "game/Player.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Projectile.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Draw.h"
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/Sounds.h"
|
||||
#include "game/Particle.h"
|
||||
|
||||
#define USE_ROTATE_BOB 0
|
||||
#define STRAFE_TILT 14
|
||||
#define ROTATE_TILT 3
|
||||
|
||||
const char SignMessage1[] PROGMEM = "Abandon all hope ye who enter!";
|
||||
|
||||
void Player::Init()
|
||||
{
|
||||
NextLevel();
|
||||
hp = maxHP;
|
||||
}
|
||||
|
||||
void Player::NextLevel()
|
||||
{
|
||||
x = CELL_SIZE * 1 + CELL_SIZE / 2;
|
||||
y = CELL_SIZE * 1 + CELL_SIZE / 2;
|
||||
angle = FIXED_ANGLE_45;
|
||||
mana = maxMana;
|
||||
damageTime = 0;
|
||||
shakeTime = 0;
|
||||
reloadTime = 0;
|
||||
velocityX = 0;
|
||||
velocityY = 0;
|
||||
angularVelocity = 0;
|
||||
}
|
||||
|
||||
void Player::Fire()
|
||||
{
|
||||
if (mana >= manaFireCost)
|
||||
{
|
||||
reloadTime = 8;
|
||||
shakeTime = 6;
|
||||
|
||||
int16_t projectileX = x + FixedCos(angle + FIXED_ANGLE_90 / 2) / 4;
|
||||
int16_t projectileY = y + FixedSin(angle + FIXED_ANGLE_90 / 2) / 4;
|
||||
|
||||
ProjectileManager::FireProjectile(this, projectileX, projectileY, angle);
|
||||
mana -= manaFireCost;
|
||||
Platform::PlaySound(Sounds::Attack);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::Tick()
|
||||
{
|
||||
uint8_t input = Platform::GetInput();
|
||||
int8_t turnDelta = 0;
|
||||
int8_t targetTilt = 0;
|
||||
int8_t moveDelta = 0;
|
||||
int8_t strafeDelta = 0;
|
||||
|
||||
// Doom-style circle strafe: holding OK (fire) makes left/right strafe
|
||||
if (input & (INPUT_A | INPUT_B))
|
||||
{
|
||||
if (input & INPUT_LEFT)
|
||||
{
|
||||
strafeDelta--;
|
||||
}
|
||||
if (input & INPUT_RIGHT)
|
||||
{
|
||||
strafeDelta++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input & INPUT_LEFT)
|
||||
{
|
||||
turnDelta -= TURN_SPEED * 2;
|
||||
}
|
||||
if (input & INPUT_RIGHT)
|
||||
{
|
||||
turnDelta += TURN_SPEED * 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Testing shooting / recoil mechanic
|
||||
|
||||
if (reloadTime > 0)
|
||||
{
|
||||
reloadTime--;
|
||||
}
|
||||
else if (input & INPUT_B)
|
||||
{
|
||||
Fire();
|
||||
}
|
||||
|
||||
|
||||
if (angularVelocity < turnDelta)
|
||||
{
|
||||
angularVelocity++;
|
||||
}
|
||||
else if (angularVelocity > turnDelta)
|
||||
{
|
||||
angularVelocity--;
|
||||
}
|
||||
|
||||
angle += angularVelocity >> 1;
|
||||
|
||||
if (input & INPUT_UP)
|
||||
{
|
||||
moveDelta++;
|
||||
}
|
||||
if (input & INPUT_DOWN)
|
||||
{
|
||||
moveDelta--;
|
||||
}
|
||||
|
||||
static int tiltTimer = 0;
|
||||
tiltTimer++;
|
||||
if (moveDelta && USE_ROTATE_BOB)
|
||||
{
|
||||
targetTilt = (int8_t)(FixedSin(tiltTimer * 10) / 32);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetTilt = 0;
|
||||
}
|
||||
|
||||
targetTilt += angularVelocity * ROTATE_TILT;
|
||||
targetTilt += strafeDelta * STRAFE_TILT;
|
||||
int8_t targetBob = moveDelta || strafeDelta ? FixedSin(tiltTimer * 10) / 128 : 0;
|
||||
|
||||
if (shakeTime > 0)
|
||||
{
|
||||
shakeTime--;
|
||||
targetBob += (Random() & 3) - 1;
|
||||
targetTilt += (Random() & 31) - 16;
|
||||
}
|
||||
|
||||
constexpr int tiltRate = 6;
|
||||
|
||||
if (Renderer::camera.tilt < targetTilt)
|
||||
{
|
||||
Renderer::camera.tilt += tiltRate;
|
||||
if (Renderer::camera.tilt > targetTilt)
|
||||
{
|
||||
Renderer::camera.tilt = targetTilt;
|
||||
}
|
||||
}
|
||||
else if (Renderer::camera.tilt > targetTilt)
|
||||
{
|
||||
Renderer::camera.tilt -= tiltRate;
|
||||
if (Renderer::camera.tilt < targetTilt)
|
||||
{
|
||||
Renderer::camera.tilt = targetTilt;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr int bobRate = 3;
|
||||
|
||||
if (Renderer::camera.bob < targetBob)
|
||||
{
|
||||
Renderer::camera.bob += bobRate;
|
||||
if (Renderer::camera.bob > targetBob)
|
||||
{
|
||||
Renderer::camera.bob = targetBob;
|
||||
}
|
||||
}
|
||||
else if (Renderer::camera.bob > targetBob)
|
||||
{
|
||||
Renderer::camera.bob -= bobRate;
|
||||
if (Renderer::camera.bob < targetBob)
|
||||
{
|
||||
Renderer::camera.bob = targetBob;
|
||||
}
|
||||
}
|
||||
|
||||
int16_t cosAngle = FixedCos(angle);
|
||||
int16_t sinAngle = FixedSin(angle);
|
||||
|
||||
int16_t cos90Angle = FixedCos(angle + FIXED_ANGLE_90);
|
||||
int16_t sin90Angle = FixedSin(angle + FIXED_ANGLE_90);
|
||||
//camera.x += (moveDelta * cosAngle) >> 4;
|
||||
//camera.y += (moveDelta * sinAngle) >> 4;
|
||||
velocityX += (moveDelta * cosAngle) / 24;
|
||||
velocityY += (moveDelta * sinAngle) / 24;
|
||||
|
||||
velocityX += (strafeDelta * cos90Angle) / 24;
|
||||
velocityY += (strafeDelta * sin90Angle) / 24;
|
||||
|
||||
Move(velocityX / 4, velocityY / 4);
|
||||
|
||||
velocityX = (velocityX * 7) / 8;
|
||||
velocityY = (velocityY * 7) / 8;
|
||||
|
||||
if (mana < maxMana && reloadTime == 0)
|
||||
{
|
||||
mana += manaRechargeRate;
|
||||
}
|
||||
|
||||
if (damageTime > 0)
|
||||
damageTime--;
|
||||
|
||||
uint8_t cellX = x / CELL_SIZE;
|
||||
uint8_t cellY = y / CELL_SIZE;
|
||||
|
||||
switch (Map::GetCellSafe(cellX, cellY))
|
||||
{
|
||||
case CellType::Potion:
|
||||
if (hp < maxHP)
|
||||
{
|
||||
if (hp + potionStrength > maxHP)
|
||||
hp = maxHP;
|
||||
else
|
||||
hp += potionStrength;
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Picked up a stimpack"));
|
||||
}
|
||||
break;
|
||||
case CellType::Coins:
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Picked up a health bonus"));
|
||||
Game::stats.coinsCollected++;
|
||||
break;
|
||||
case CellType::Crown:
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Picked up some armor"));
|
||||
Game::stats.crownsCollected++;
|
||||
break;
|
||||
case CellType::Scroll:
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Picked up an armor bonus"));
|
||||
Game::stats.scrollsCollected++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool Player::IsWorldColliding() const
|
||||
{
|
||||
return Map::IsBlockedAtWorldPosition(x - collisionSize, y - collisionSize)
|
||||
|| Map::IsBlockedAtWorldPosition(x + collisionSize, y - collisionSize)
|
||||
|| Map::IsBlockedAtWorldPosition(x + collisionSize, y + collisionSize)
|
||||
|| Map::IsBlockedAtWorldPosition(x - collisionSize, y + collisionSize);
|
||||
}
|
||||
|
||||
bool Player::CheckCollisions()
|
||||
{
|
||||
int16_t lookAheadX = (x + (FixedCos(angle) * lookAheadDistance) / FIXED_ONE);
|
||||
int16_t lookAheadY = (y + (FixedSin(angle) * lookAheadDistance) / FIXED_ONE);
|
||||
uint8_t lookAheadCellX = (uint8_t)(lookAheadX / CELL_SIZE);
|
||||
uint8_t lookAheadCellY = (uint8_t)(lookAheadY / CELL_SIZE);
|
||||
|
||||
CellType lookAheadCell = Map::GetCellSafe(lookAheadCellX, lookAheadCellY);
|
||||
switch (lookAheadCell)
|
||||
{
|
||||
case CellType::Chest:
|
||||
Map::SetCell(lookAheadCellX, lookAheadCellY, CellType::ChestOpened);
|
||||
ParticleSystemManager::CreateExplosion(lookAheadX, lookAheadY, true);
|
||||
Platform::PlaySound(Sounds::Pickup);
|
||||
Game::ShowMessage(PSTR("Found a backpack of ammo!"));
|
||||
Game::stats.chestsOpened++;
|
||||
break;
|
||||
case CellType::Sign:
|
||||
Game::ShowMessage(SignMessage1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if (IsWorldColliding())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (EnemyManager::GetOverlappingEnemy(*this))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Player::Move(int16_t deltaX, int16_t deltaY)
|
||||
{
|
||||
x += deltaX;
|
||||
y += deltaY;
|
||||
|
||||
if (CheckCollisions())
|
||||
{
|
||||
y -= deltaY;
|
||||
if (CheckCollisions())
|
||||
{
|
||||
x -= deltaX;
|
||||
y += deltaY;
|
||||
|
||||
if (CheckCollisions())
|
||||
{
|
||||
y -= deltaY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::Damage(uint8_t damageAmount)
|
||||
{
|
||||
if(shakeTime < 6)
|
||||
shakeTime = 6;
|
||||
|
||||
damageTime = 8;
|
||||
|
||||
if (hp <= damageAmount)
|
||||
{
|
||||
Platform::PlaySound(Sounds::PlayerDeath);
|
||||
hp = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::PlaySound(Sounds::Ouch);
|
||||
hp -= damageAmount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Entity.h"
|
||||
|
||||
class Player : public Entity
|
||||
{
|
||||
public:
|
||||
uint8_t angle;
|
||||
int16_t velocityX, velocityY;
|
||||
int8_t angularVelocity;
|
||||
|
||||
uint8_t shakeTime;
|
||||
uint8_t damageTime;
|
||||
uint8_t reloadTime;
|
||||
|
||||
static constexpr uint8_t maxHP = 100;
|
||||
static constexpr uint8_t maxMana = 100;
|
||||
static constexpr uint8_t manaFireCost = 20;
|
||||
static constexpr uint8_t manaRechargeRate = 1;
|
||||
static constexpr uint8_t attackStrength = 10;
|
||||
static constexpr uint8_t collisionSize = 48;
|
||||
static constexpr uint8_t lookAheadDistance = 60;
|
||||
static constexpr uint8_t potionStrength = 25;
|
||||
|
||||
uint8_t hp;
|
||||
uint8_t mana;
|
||||
|
||||
void Init();
|
||||
void NextLevel();
|
||||
void Tick();
|
||||
void Fire();
|
||||
void Move(int16_t deltaX, int16_t deltaY);
|
||||
bool CheckCollisions();
|
||||
void Damage(uint8_t amount);
|
||||
bool IsWorldColliding() const;
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
#include "game/Defines.h"
|
||||
#include "game/Projectile.h"
|
||||
#include "game/Map.h"
|
||||
#include "game/FixedMath.h"
|
||||
#include "game/Particle.h"
|
||||
#include "game/Enemy.h"
|
||||
#include "game/Generated/SpriteTypes.h"
|
||||
#include "game/Platform.h"
|
||||
#include "game/Sounds.h"
|
||||
|
||||
Projectile ProjectileManager::projectiles[ProjectileManager::MAX_PROJECTILES];
|
||||
|
||||
Projectile* ProjectileManager::FireProjectile(Entity* owner, int16_t x, int16_t y, uint8_t angle)
|
||||
{
|
||||
for (Projectile& p : projectiles)
|
||||
{
|
||||
if(p.life == 0)
|
||||
{
|
||||
if (owner == &Game::player)
|
||||
p.ownerId = Projectile::playerOwnerId;
|
||||
else
|
||||
{
|
||||
for (uint8_t n = 0; n < EnemyManager::maxEnemies; n++)
|
||||
{
|
||||
if (&EnemyManager::enemies[n] == owner)
|
||||
{
|
||||
p.ownerId = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.life = 255;
|
||||
p.x = x;
|
||||
p.y = y;
|
||||
p.angle = angle;
|
||||
return &p;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Entity* Projectile::GetOwner() const
|
||||
{
|
||||
if (ownerId == playerOwnerId)
|
||||
return &Game::player;
|
||||
return &EnemyManager::enemies[ownerId];
|
||||
}
|
||||
|
||||
void ProjectileManager::Update()
|
||||
{
|
||||
for (Projectile& p : projectiles)
|
||||
{
|
||||
if(p.life > 0)
|
||||
{
|
||||
p.life--;
|
||||
|
||||
int16_t deltaX = FixedCos(p.angle) / 4;
|
||||
int16_t deltaY = FixedSin(p.angle) / 4;
|
||||
|
||||
p.x += deltaX;
|
||||
p.y += deltaY;
|
||||
|
||||
bool hitAnything = false;
|
||||
|
||||
Entity* owner = p.GetOwner();
|
||||
|
||||
if (Map::IsBlockedAtWorldPosition(p.x, p.y))
|
||||
{
|
||||
uint8_t cellX = p.x / CELL_SIZE;
|
||||
uint8_t cellY = p.y / CELL_SIZE;
|
||||
|
||||
if (Map::GetCellSafe(cellX, cellY) == CellType::Urn)
|
||||
{
|
||||
// Exploding barrel: splash damage to everything nearby
|
||||
int16_t barrelX = cellX * CELL_SIZE + CELL_SIZE / 2;
|
||||
int16_t barrelY = cellY * CELL_SIZE + CELL_SIZE / 2;
|
||||
constexpr int16_t blastRadius = CELL_SIZE + CELL_SIZE / 2;
|
||||
constexpr uint8_t enemyBlastDamage = 40;
|
||||
constexpr uint8_t playerBlastDamage = 15;
|
||||
|
||||
Map::SetCell(cellX, cellY, CellType::Empty);
|
||||
ParticleSystemManager::CreateExplosion(barrelX, barrelY, true);
|
||||
|
||||
for (uint8_t n = 0; n < EnemyManager::maxEnemies; n++)
|
||||
{
|
||||
Enemy& enemy = EnemyManager::enemies[n];
|
||||
if (!enemy.IsValid())
|
||||
continue;
|
||||
int16_t dx = enemy.x - barrelX;
|
||||
int16_t dy = enemy.y - barrelY;
|
||||
if (ABS(dx) < blastRadius && ABS(dy) < blastRadius)
|
||||
{
|
||||
enemy.Damage(enemyBlastDamage);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
int16_t dx = Game::player.x - barrelX;
|
||||
int16_t dy = Game::player.y - barrelY;
|
||||
if (ABS(dx) < blastRadius && ABS(dy) < blastRadius)
|
||||
{
|
||||
// barrels hurt but never kill the player outright
|
||||
uint8_t dmg = playerBlastDamage;
|
||||
if (Game::player.hp <= dmg)
|
||||
dmg = Game::player.hp > 1 ? (uint8_t)(Game::player.hp - 1) : 0;
|
||||
if (dmg)
|
||||
Game::player.Damage(dmg);
|
||||
}
|
||||
}
|
||||
|
||||
// occasionally the barrel leaves a pickup behind
|
||||
switch ((Random() % 6))
|
||||
{
|
||||
case 0:
|
||||
Map::SetCell(cellX, cellY, CellType::Potion);
|
||||
break;
|
||||
case 1:
|
||||
Map::SetCell(cellX, cellY, CellType::Coins);
|
||||
break;
|
||||
}
|
||||
Platform::PlaySound(Sounds::Kill);
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::PlaySound(Sounds::Hit);
|
||||
}
|
||||
|
||||
hitAnything = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (owner == &Game::player)
|
||||
{
|
||||
Enemy* overlappingEnemy = EnemyManager::GetOverlappingEnemy(p.x, p.y);
|
||||
if (overlappingEnemy)
|
||||
{
|
||||
overlappingEnemy->Damage(Player::attackStrength);
|
||||
|
||||
hitAnything = true;
|
||||
}
|
||||
}
|
||||
else if(Game::player.IsOverlappingPoint(p.x, p.y))
|
||||
{
|
||||
const EnemyArchetype* enemyArchetype = ((Enemy*)owner)->GetArchetype();
|
||||
if (enemyArchetype)
|
||||
{
|
||||
Game::player.Damage(enemyArchetype->GetAttackStrength());
|
||||
if (Game::player.hp == 0)
|
||||
{
|
||||
Game::stats.killedBy = ((Enemy*)owner)->GetType();
|
||||
}
|
||||
}
|
||||
hitAnything = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hitAnything)
|
||||
{
|
||||
ParticleSystemManager::CreateExplosion(p.x - deltaX, p.y - deltaY);
|
||||
p.life = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectileManager::Init()
|
||||
{
|
||||
for (Projectile& p : projectiles)
|
||||
{
|
||||
p.life = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectileManager::Draw()
|
||||
{
|
||||
for(Projectile& p : projectiles)
|
||||
{
|
||||
if (p.life > 0)
|
||||
{
|
||||
Renderer::DrawObject(p.ownerId == Projectile::playerOwnerId ? projectileSpriteData : enemyProjectileSpriteData, p.x, p.y, 32, AnchorType::BelowCenter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "game/Entity.h"
|
||||
|
||||
class Projectile : public Entity
|
||||
{
|
||||
public:
|
||||
uint8_t angle;
|
||||
uint8_t life;
|
||||
uint8_t ownerId;
|
||||
|
||||
static constexpr uint8_t playerOwnerId = 0xff;
|
||||
|
||||
Entity* GetOwner() const;
|
||||
};
|
||||
|
||||
class ProjectileManager
|
||||
{
|
||||
public:
|
||||
static constexpr int MAX_PROJECTILES = 8;
|
||||
static Projectile projectiles[MAX_PROJECTILES];
|
||||
|
||||
static Projectile* FireProjectile(Entity* owner, int16_t x, int16_t y, uint8_t angle);
|
||||
static void Init();
|
||||
static void Draw();
|
||||
static void Update();
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "game/Sounds.h"
|
||||
|
||||
// Shotgun blast: sharp crack followed by a fast descending boom with a
|
||||
// short pump echo. All frequencies within the buzzer's 100-2500 Hz range.
|
||||
const uint16_t Sounds::Attack[] PROGMEM = {
|
||||
900, 2, 500, 3, 320, 4, 230, 5,
|
||||
180, 6, 150, 7, 128, 9, 112, 11, 104, 13, 100, 15,
|
||||
0, 8,
|
||||
170, 4, 135, 6, 112, 9, 100, 14,
|
||||
TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Kill[] PROGMEM = {
|
||||
0x0151,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x014b,0x0007,0x0136,0x0007,0x0146,0x0007,0x0169,0x0007,0x019e,0x0007,0x007f,0x0007,0x0185,
|
||||
0x0007,0x0228,0x0007,0x026d,0x0007,0x02fc,0x0007,0x02e0,0x0007,0x01fd,0x0007,0x0219,0x0007,0x033c,0x0007,0x00e7,0x0007,0x0281,0x0007,0x026d,
|
||||
0x000e,0x052d,0x000e,0x04da,0x000e,0x011c,0x0007,0x0387,0x0007,0x0360,0x0007,0x033c,0x0007,0x0387,0x0007,0x02ad,0x000e,0x02c6,0x0007,0x010c,
|
||||
0x000e,0x02e0,0x0007,0x02c6,0x0007,0x0296,0x000e,0x0281,0x0007,0x02ad,0x0007,0x02c6,0x0007,0x02e0,0x0007,0x00e1,0x0007,0x031b,0x0007,0x0248,
|
||||
0x0007,0x0219,0x0007,0x01f1,0x0007,0x01d9,0x000e,0x01f1,0x0007,0x020b,0x0007,0x02ad,0x0007,0x00d1,0x0007,0x0248,0x0007,0x0238,0x0007,0x0219,
|
||||
0x0007,0x0228,0x000e,0x020b,0x0007,0x01e5,0x0007,0x01d9,0x0007, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Hit[] PROGMEM = {
|
||||
0x0195,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x0416,0x0007,0x007f,0x0007,0x0000,0x000e,0x0177,0x0007,0x0000,0x001d,0x0146,0x0007,0x0140,
|
||||
0x0007,0x013b,0x0007,0x0000,0x0047,0x00e7,0x0007,0x00e4,0x0007,0x0000,0x0015,0x009f,0x0007,0x009d,0x0007,0x0000,0x0088,0x0088,0x0007, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::PlayerDeath[] PROGMEM = {
|
||||
0x01b0,0x0007,0x0000,0x0015,0x00c8,0x0007,0x0000,0x0007,0x03b2,0x0007,0x0360,0x0007,0x02e0,0x0007,0x0296,0x0007,0x0248,0x0007,0x0219,0x0007,0x01fd,
|
||||
0x0007,0x0450,0x0007,0x01ce,0x0007,0x01b9,0x0007,0x01a7,0x0007,0x0450,0x0007,0x017e,0x0007,0x0163,0x0007,0x0140,0x0007,0x052d,0x0007,0x0110,
|
||||
0x0007,0x0109,0x0007,0x0102,0x0007,0x00f8,0x0007,0x04da,0x0007,0x00ca,0x0007,0x00bb,0x0007,0x00b3,0x0007,0x0491,0x0007,0x0096,0x0007,0x00d8,
|
||||
0x0007,0x0000,0x001d,0x0296,0x0007,0x0000,0x002b,0x0228,0x0007,0x0000,0x0024,0x00fb,0x000e, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::SpotPlayer[] PROGMEM = {
|
||||
0x0110,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x01d9,0x0007,0x01fd,0x0007,0x020b,0x000e,0x0228,0x000e,0x0238,0x0007,0x0248,0x0007,0x026d,
|
||||
0x0007,0x0281,0x0032,0x026d,0x0015,0x0248,0x0007,0x0238,0x0007,0x01b0,0x0007,0x017e,0x0007,0x0169,0x0007,0x014b,0x0007,0x0136,0x0007,0x0131,
|
||||
0x0007,0x0102,0x0007,0x00f8,0x0007,0x00f2,0x0007,0x00e9,0x0007,0x00e4,0x0007,0x00d8,0x0007,0x00bd,0x0007,0x00ab,0x0007,0x009d,0x0007,0x0096,
|
||||
0x0007,0x0094,0x0007,0x0092,0x0007,0x008e,0x0007,0x008b,0x0007,0x008a,0x0007,0x0089,0x0007,0x0088,0x0007,0x0087,0x0007,0x0086,0x0007,0x0085,
|
||||
0x0007,0x0084,0x0007,0x0083,0x0007,0x0082,0x0007,0x0081,0x000e,0x0081,0x0007,0x0080,0x0007,0x007e,0x0007,0x007d,0x0007,0x007d,0x0007,0x0000,
|
||||
0x0040,0x0090,0x0032, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Shoot[] PROGMEM = {
|
||||
0x02e0,0x0007,0x0000,0x0015,0x03e2,0x0007,0x0000,0x0007,0x04da,0x0015,0x00b4,0x0007,0x01d9,0x0007,0x0000,0x0007,0x01f1,0x000e,0x0080,0x0007,0x01f1,
|
||||
0x0007,0x0000,0x000e,0x025a,0x0007,0x00e4,0x0007,0x0000,0x0015,0x00c1,0x0007,0x0000,0x000e,0x01e5,0x0007,0x0000,0x0007,0x00ac,0x0007,0x0000,
|
||||
0x0015,0x0091,0x0007, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Pickup[] PROGMEM = {
|
||||
0x0120,0x0007,0x0000,0x0015,0x00e9,0x0007,0x0000,0x0007,0x0156,0x0015,0x0000,0x0032,0x0185,0x001d,0x0000,0x0040,0x020b,0x0015,0x0000,0x0032,0x0387,
|
||||
0x0024,0x0000,0x0040,0x0387,0x002b,0x0000,0x0040,0x03b2,0x0032, TONES_END
|
||||
};
|
||||
|
||||
const uint16_t Sounds::Ouch[] PROGMEM = {
|
||||
0x01ce,0x0007,0x0000,0x0015,0x018d,0x0007,0x0000,0x0007,0x0416,0x0007,0x0491,0x0007,0x03b2,0x0007,0x04da,0x0007,0x052d,0x0015,0x04da,0x0007,0x0491,
|
||||
0x0007,0x02e0,0x0007,0x0296,0x0007,0x025a,0x0007,0x01f1,0x0007,0x0219,0x0007,0x0000,0x0007,0x01ce,0x0007,0x007c,0x000e,0x015c,0x0007,0x007d,
|
||||
0x000e,0x0120,0x0007,0x007d,0x000e,0x00ef,0x0007,0x00d5,0x0007,0x00ca,0x0007,0x0000,0x0007,0x00c1,0x0007,0x00b9,0x0007,0x00b3,0x0007,0x00a9,
|
||||
0x0007,0x007d,0x0007,0x007e,0x0007,0x0093,0x0007,0x007e,0x0007,0x0000,0x0007,0x0089,0x0007,0x0085,0x0007,0x0082,0x0007,0x0080,0x0007,0x007e,
|
||||
0x0007,0x007d,0x0007, TONES_END
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "game/Defines.h"
|
||||
|
||||
#define TONES_END 0x8000
|
||||
|
||||
class Sounds
|
||||
{
|
||||
public:
|
||||
static const uint16_t Attack[];
|
||||
static const uint16_t Kill[];
|
||||
static const uint16_t Hit[];
|
||||
static const uint16_t PlayerDeath[];
|
||||
static const uint16_t SpotPlayer[];
|
||||
static const uint16_t Shoot[];
|
||||
static const uint16_t Pickup[];
|
||||
static const uint16_t Ouch[];
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "game/Defines.h"
|
||||
|
||||
// Tech-base wall panel: top/bottom seams with vertical panel gaps
|
||||
const uint8_t vectorTexture0[] PROGMEM =
|
||||
{
|
||||
6,
|
||||
0, 18, 128, 18,
|
||||
0, 110, 128, 110,
|
||||
32, 18, 32, 110,
|
||||
64, 18, 64, 110,
|
||||
96, 18, 96, 110,
|
||||
48, 64, 80, 64,
|
||||
};
|
||||
|
||||
const uint8_t vectorTexture1[] PROGMEM =
|
||||
{
|
||||
6,
|
||||
0, 16, 128, 16 ,
|
||||
0, 112, 128, 112 ,
|
||||
0, 16, 0, 112,
|
||||
0, 16, 128, 112,
|
||||
0, 112, 128, 16,
|
||||
128, 16, 128, 112,
|
||||
|
||||
/* 16, 16, 112, 16 ,
|
||||
16, 16, 16, 128,
|
||||
48, 16, 48, 128,
|
||||
80, 16, 80, 128,
|
||||
112, 16, 112, 128,*/
|
||||
};
|
||||
|
||||
const uint8_t vectorTexture2[] PROGMEM =
|
||||
{
|
||||
12,
|
||||
38,13,90,13,
|
||||
38,13,64,38,
|
||||
64,38,90,13,
|
||||
13,38,38,64,
|
||||
13,38,13,90,
|
||||
13,90,38,64,
|
||||
38,115,90,115,
|
||||
38,115,64,90,
|
||||
64,90,90,115,
|
||||
90,64,115,38,
|
||||
90,64,115,90,
|
||||
115,38,115,90,
|
||||
};
|
||||
|
||||
const uint8_t* const textures[] PROGMEM =
|
||||
{
|
||||
vectorTexture0,
|
||||
vectorTexture1,
|
||||
vectorTexture2,
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <furi.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#define EEPROM_LIB_PATH APP_DATA_PATH("eeprom.bin")
|
||||
|
||||
class EEPROMClass {
|
||||
public:
|
||||
static constexpr int kSize = 16;
|
||||
static constexpr size_t kPathSize = 128;
|
||||
|
||||
EEPROMClass()
|
||||
: loaded_(false)
|
||||
, dirty_(false)
|
||||
, path_resolved_(false) {
|
||||
memset(mem_, 0x00, kSize);
|
||||
memset(file_path_, 0x00, kPathSize);
|
||||
strncpy(file_path_, EEPROM_LIB_PATH, kPathSize - 1);
|
||||
}
|
||||
|
||||
void begin() {
|
||||
ensureLoaded_();
|
||||
}
|
||||
|
||||
int length() const {
|
||||
return kSize;
|
||||
}
|
||||
|
||||
uint8_t read(int addr) const {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr >= kSize) return 0;
|
||||
return mem_[addr];
|
||||
}
|
||||
|
||||
void write(int addr, uint8_t value) {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr >= kSize) return;
|
||||
mem_[addr] = value;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void update(int addr, uint8_t value) {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr >= kSize) return;
|
||||
if(mem_[addr] != value) {
|
||||
mem_[addr] = value;
|
||||
dirty_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T& get(int addr, T& out) const {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr + (int)sizeof(T) > kSize) return out;
|
||||
memcpy(&out, mem_ + addr, sizeof(T));
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const T& put(int addr, const T& in) {
|
||||
ensureLoaded_();
|
||||
if(addr < 0 || addr + (int)sizeof(T) > kSize) return in;
|
||||
|
||||
bool changed = false;
|
||||
const uint8_t* src = reinterpret_cast<const uint8_t*>(&in);
|
||||
for(size_t i = 0; i < sizeof(T); i++) {
|
||||
int a = addr + (int)i;
|
||||
if(mem_[a] != src[i]) {
|
||||
mem_[a] = src[i];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if(changed) dirty_ = true;
|
||||
return in;
|
||||
}
|
||||
|
||||
void clear(uint8_t value = 0) {
|
||||
ensureLoaded_();
|
||||
memset(mem_, value, kSize);
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
bool commit() {
|
||||
ensureLoaded_();
|
||||
if(!dirty_) return true;
|
||||
|
||||
const bool ok = writeFile_();
|
||||
if(ok) dirty_ = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool isDirty() const {
|
||||
return dirty_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool resolvePathIfNeeded_(Storage* storage) const {
|
||||
if(path_resolved_) return true;
|
||||
if(!storage) return false;
|
||||
|
||||
FuriString* path = furi_string_alloc_set_str(file_path_);
|
||||
if(!path) return false;
|
||||
|
||||
storage_common_resolve_path_and_ensure_app_directory(storage, path);
|
||||
const char* resolved = furi_string_get_cstr(path);
|
||||
|
||||
bool ok = false;
|
||||
if(resolved && resolved[0]) {
|
||||
const size_t len = strlen(resolved);
|
||||
if(len < kPathSize) {
|
||||
memcpy(file_path_, resolved, len + 1);
|
||||
path_resolved_ = true;
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_free(path);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void ensureDefaultDir_(Storage* storage) {
|
||||
if(!storage) return;
|
||||
(void)storage_common_mkdir(storage, STORAGE_APP_DATA_PATH_PREFIX);
|
||||
}
|
||||
|
||||
void ensureLoaded_() const {
|
||||
if(loaded_) return;
|
||||
|
||||
Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
|
||||
if(!storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
(void)resolvePathIfNeeded_(storage);
|
||||
ensureDefaultDir_(storage);
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(!file) {
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(mem_, 0x00, kSize);
|
||||
bool ok = storage_file_open(file, file_path_, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS);
|
||||
if(ok) {
|
||||
const uint64_t file_size = storage_file_size(file);
|
||||
bool need_rewrite = false;
|
||||
|
||||
(void)storage_file_seek(file, 0, true);
|
||||
const size_t rd = storage_file_read(file, mem_, kSize);
|
||||
if(rd < (size_t)kSize) {
|
||||
need_rewrite = true;
|
||||
}
|
||||
|
||||
if(file_size != (uint64_t)kSize) {
|
||||
need_rewrite = true;
|
||||
}
|
||||
|
||||
if(need_rewrite) {
|
||||
(void)storage_file_seek(file, 0, true);
|
||||
const size_t wr = storage_file_write(file, mem_, kSize);
|
||||
if(wr == (size_t)kSize) {
|
||||
(void)storage_file_truncate(file);
|
||||
(void)storage_file_sync(file);
|
||||
}
|
||||
}
|
||||
(void)storage_file_close(file);
|
||||
} else {
|
||||
(void)storage_file_close(file);
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
loaded_ = true;
|
||||
dirty_ = false;
|
||||
}
|
||||
|
||||
bool writeFile_() const {
|
||||
Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
|
||||
if(!storage) return false;
|
||||
|
||||
if(!path_resolved_ && !resolvePathIfNeeded_(storage)) {
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
ensureDefaultDir_(storage);
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(!file) {
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = storage_file_open(file, file_path_, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS);
|
||||
bool success = false;
|
||||
|
||||
if(ok) {
|
||||
(void)storage_file_seek(file, 0, true);
|
||||
size_t wr = storage_file_write(file, mem_, kSize);
|
||||
(void)storage_file_truncate(file);
|
||||
(void)storage_file_sync(file);
|
||||
(void)storage_file_close(file);
|
||||
success = (wr == (size_t)kSize);
|
||||
} else {
|
||||
(void)storage_file_close(file);
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return success;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable uint8_t mem_[kSize];
|
||||
mutable bool loaded_;
|
||||
mutable bool dirty_;
|
||||
mutable bool path_resolved_;
|
||||
|
||||
mutable char file_path_[kPathSize];
|
||||
};
|
||||
|
||||
#if (__cplusplus >= 201703L)
|
||||
inline EEPROMClass EEPROM;
|
||||
#else
|
||||
extern EEPROMClass EEPROM;
|
||||
#ifdef EEPROM_DEFINE_INSTANCE
|
||||
EEPROMClass EEPROM;
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
//lib/flipper.h
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define DISPLAY_WIDTH 128
|
||||
#define DISPLAY_HEIGHT 64
|
||||
#define BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_HEIGHT / 8)
|
||||
|
||||
typedef struct {
|
||||
uint8_t back_buffer[BUFFER_SIZE];
|
||||
uint8_t front_buffer[BUFFER_SIZE];
|
||||
|
||||
Gui* gui;
|
||||
Canvas* canvas;
|
||||
FuriMutex* fb_mutex;
|
||||
|
||||
volatile uint8_t input_state;
|
||||
volatile bool exit_requested;
|
||||
volatile bool audio_enabled;
|
||||
|
||||
// back-hold логика
|
||||
bool back_hold_active;
|
||||
uint16_t back_hold_start;
|
||||
bool back_hold_handled;
|
||||
|
||||
// input pubsub
|
||||
FuriPubSub* input_events;
|
||||
FuriPubSubSubscription* input_sub;
|
||||
} FlipperState;
|
||||
|
||||
extern FlipperState* g_state;
|
||||
@@ -0,0 +1,261 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "lib/flipper.h"
|
||||
#include "lib/EEPROM.h"
|
||||
#include "game/Game.h"
|
||||
#include "game/Platform.h"
|
||||
|
||||
#define TARGET_FRAMERATE 30
|
||||
#define HOLD_TIME_MS 300
|
||||
|
||||
FlipperState* g_state = NULL;
|
||||
|
||||
static volatile uint32_t s_input_cb_inflight = 0;
|
||||
static volatile uint32_t s_fb_cb_inflight = 0;
|
||||
static volatile uint8_t s_back_pressed = 0;
|
||||
|
||||
static inline void wait_inflight_zero(volatile uint32_t* counter) {
|
||||
while(__atomic_load_n(counter, __ATOMIC_ACQUIRE) != 0) {
|
||||
furi_delay_ms(1);
|
||||
}
|
||||
}
|
||||
|
||||
inline bool audio_enable(){
|
||||
return !furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode);
|
||||
}
|
||||
|
||||
static void framebuffer_commit_callback(
|
||||
uint8_t* data,
|
||||
size_t size,
|
||||
CanvasOrientation orientation,
|
||||
void* context) {
|
||||
__atomic_fetch_add(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
|
||||
FlipperState* state = (FlipperState*)context;
|
||||
if(!state || !data || size < BUFFER_SIZE) {
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
return;
|
||||
}
|
||||
(void)orientation;
|
||||
|
||||
if(furi_mutex_acquire(state->fb_mutex, 0) != FuriStatusOk) {
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* src = state->front_buffer;
|
||||
for(size_t i = 0; i < BUFFER_SIZE; i++) {
|
||||
data[i] = (uint8_t)(src[i] ^ 0xFF);
|
||||
}
|
||||
|
||||
furi_mutex_release(state->fb_mutex);
|
||||
|
||||
__atomic_fetch_sub(&s_fb_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
static void input_events_callback(const void* value, void* ctx) {
|
||||
if(!value || !ctx) return;
|
||||
|
||||
__atomic_fetch_add(&s_input_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
|
||||
FlipperState* state = (FlipperState*)ctx;
|
||||
const InputEvent* event = (const InputEvent*)value;
|
||||
|
||||
uint8_t bit = 0;
|
||||
switch(event->key) {
|
||||
case InputKeyUp:
|
||||
bit = INPUT_UP;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
bit = INPUT_DOWN;
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
bit = INPUT_LEFT;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
bit = INPUT_RIGHT;
|
||||
break;
|
||||
case InputKeyOk:
|
||||
bit = INPUT_B;
|
||||
break;
|
||||
case InputKeyBack:
|
||||
if((event->type == InputTypePress) || (event->type == InputTypeRepeat)) {
|
||||
(void)__atomic_store_n(&s_back_pressed, 1, __ATOMIC_RELAXED);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
(void)__atomic_store_n(&s_back_pressed, 0, __ATOMIC_RELAXED);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(state && bit) {
|
||||
if((event->type == InputTypePress) || (event->type == InputTypeRepeat)) {
|
||||
(void)__atomic_fetch_or((uint8_t*)&state->input_state, bit, __ATOMIC_RELAXED);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
(void)__atomic_fetch_and(
|
||||
(uint8_t*)&state->input_state, (uint8_t)~bit, __ATOMIC_RELAXED);
|
||||
}
|
||||
}
|
||||
|
||||
__atomic_fetch_sub(&s_input_cb_inflight, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
extern "C" int32_t flipdoom_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
Gui* gui = NULL;
|
||||
Canvas* canvas = NULL;
|
||||
FuriPubSub* input_events = NULL;
|
||||
FuriPubSubSubscription* input_sub = NULL;
|
||||
|
||||
FlipperState* st = (FlipperState*)malloc(sizeof(FlipperState));
|
||||
if(!st) return -1;
|
||||
memset(st, 0, sizeof(FlipperState));
|
||||
g_state = st;
|
||||
|
||||
do {
|
||||
st->fb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
if(!st->fb_mutex) break;
|
||||
|
||||
memset(st->back_buffer, 0x00, BUFFER_SIZE);
|
||||
memset(st->front_buffer, 0x00, BUFFER_SIZE);
|
||||
|
||||
EEPROM.begin();
|
||||
furi_delay_ms(50);
|
||||
Platform::SetAudioEnabled(audio_enable());
|
||||
Game::menu.ReadSave();
|
||||
|
||||
gui = (Gui*)furi_record_open(RECORD_GUI);
|
||||
if(!gui) break;
|
||||
st->gui = gui;
|
||||
|
||||
gui_add_framebuffer_callback(gui, framebuffer_commit_callback, st);
|
||||
|
||||
canvas = gui_direct_draw_acquire(gui);
|
||||
if(!canvas) break;
|
||||
st->canvas = canvas;
|
||||
|
||||
input_events = (FuriPubSub*)furi_record_open(RECORD_INPUT_EVENTS);
|
||||
if(!input_events) break;
|
||||
st->input_events = input_events;
|
||||
|
||||
input_sub = furi_pubsub_subscribe(input_events, input_events_callback, st);
|
||||
if(!input_sub) break;
|
||||
st->input_sub = input_sub;
|
||||
|
||||
const uint32_t tick_hz = furi_kernel_get_tick_frequency();
|
||||
uint32_t period_ticks = (tick_hz + (TARGET_FRAMERATE / 2)) / TARGET_FRAMERATE;
|
||||
if(period_ticks == 0) period_ticks = 1;
|
||||
const uint32_t hold_ticks = (uint32_t)((HOLD_TIME_MS * tick_hz + 999u) / 1000u);
|
||||
|
||||
uint32_t next_tick = furi_get_tick();
|
||||
|
||||
bool back_was_pressed = false;
|
||||
bool back_hold_fired = false;
|
||||
uint32_t back_press_tick = 0;
|
||||
|
||||
while(!st->exit_requested) {
|
||||
uint32_t now = furi_get_tick();
|
||||
|
||||
// frame pacing
|
||||
if((int32_t)(now - next_tick) < 0) {
|
||||
uint32_t dt_ticks = next_tick - now;
|
||||
uint32_t dt_ms = (dt_ticks * 1000u) / tick_hz;
|
||||
furi_delay_ms(dt_ms ? dt_ms : 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if((int32_t)(now - next_tick) > (int32_t)(period_ticks * 2)) {
|
||||
next_tick = now;
|
||||
}
|
||||
next_tick += period_ticks;
|
||||
|
||||
const bool back_pressed = (__atomic_load_n(&s_back_pressed, __ATOMIC_RELAXED) != 0);
|
||||
|
||||
// BACK hold logic
|
||||
if(!back_pressed) {
|
||||
back_was_pressed = false;
|
||||
back_hold_fired = false;
|
||||
} else {
|
||||
if(!back_was_pressed) {
|
||||
back_was_pressed = true;
|
||||
back_press_tick = now;
|
||||
back_hold_fired = false;
|
||||
}
|
||||
|
||||
if(!back_hold_fired && ((uint32_t)(now - back_press_tick) >= hold_ticks)) {
|
||||
back_hold_fired = true;
|
||||
if(Game::InMenu())
|
||||
st->exit_requested = true;
|
||||
else
|
||||
Game::GoToMenu();
|
||||
}
|
||||
}
|
||||
|
||||
if(st->exit_requested) break;
|
||||
|
||||
Game::Tick();
|
||||
Game::Draw();
|
||||
|
||||
// swap for framebuffer callback
|
||||
furi_mutex_acquire(st->fb_mutex, FuriWaitForever);
|
||||
memcpy(st->front_buffer, st->back_buffer, BUFFER_SIZE);
|
||||
furi_mutex_release(st->fb_mutex);
|
||||
|
||||
canvas_commit(canvas);
|
||||
}
|
||||
} while(false);
|
||||
|
||||
Game::menu.WriteSave();
|
||||
|
||||
if(input_sub && input_events) {
|
||||
furi_pubsub_unsubscribe(input_events, input_sub);
|
||||
input_sub = NULL;
|
||||
}
|
||||
st->input_sub = NULL;
|
||||
|
||||
wait_inflight_zero(&s_input_cb_inflight);
|
||||
(void)__atomic_store_n(&s_back_pressed, 0, __ATOMIC_RELAXED);
|
||||
|
||||
if(input_events) {
|
||||
furi_record_close(RECORD_INPUT_EVENTS);
|
||||
input_events = NULL;
|
||||
}
|
||||
st->input_events = NULL;
|
||||
|
||||
if(gui) {
|
||||
gui_remove_framebuffer_callback(gui, framebuffer_commit_callback, st);
|
||||
}
|
||||
|
||||
wait_inflight_zero(&s_fb_cb_inflight);
|
||||
|
||||
if(gui) {
|
||||
if(canvas) {
|
||||
gui_direct_draw_release(gui);
|
||||
canvas = NULL;
|
||||
}
|
||||
furi_record_close(RECORD_GUI);
|
||||
gui = NULL;
|
||||
}
|
||||
st->gui = NULL;
|
||||
st->canvas = NULL;
|
||||
|
||||
if(st->fb_mutex) {
|
||||
furi_mutex_free(st->fb_mutex);
|
||||
st->fb_mutex = NULL;
|
||||
}
|
||||
|
||||
Platform::SetAudioEnabled(false);
|
||||
|
||||
free(st);
|
||||
g_state = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
extract_doom_assets.py
|
||||
|
||||
Reads the user's own Doom shareware IWAD (Doom1.WAAD is freely distributable
|
||||
as a whole) and converts a selection of its sprites into 1-bit assets in the
|
||||
FlipperCatacombs engine formats. Nothing from the WAD is stored in this
|
||||
repository: the header is generated locally at build time from the WAD the
|
||||
user already has.
|
||||
|
||||
Usage:
|
||||
python3 tools/extract_doom_assets.py <path/to/Doom1.WAD> [output_header]
|
||||
|
||||
Output (default): game/Generated/DoomSprites.inc.h
|
||||
|
||||
Engine formats
|
||||
--------------
|
||||
1) Scaled sprite (16x16), uint16_t array, per frame:
|
||||
16 columns x 2 words: [transparency mask, colour], bit v = row v (bit0=top)
|
||||
2) Page sprite (Platform::DrawSprite): uint8_t array:
|
||||
w, h, then per page (8 rows), per column: [colour byte, mask byte]
|
||||
(bit0 = top row of the page)
|
||||
3) Solid bitmap (Platform::DrawSolidBitmap): uint8_t array:
|
||||
w, h, then per page, per column: colour byte where bit=1 means BLACK
|
||||
(DrawSolidBitmap writes fill = ~src)
|
||||
4) HUD icon: 8 raw page bytes (bit=1 -> white pixel)
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
# ---------------------------------------------------------------- WAD parsing
|
||||
|
||||
|
||||
class Wad:
|
||||
def __init__(self, path):
|
||||
self.data = open(path, "rb").read()
|
||||
ident, numlumps, diroff = struct.unpack_from("<4sII", self.data, 0)
|
||||
if ident not in (b"IWAD", b"PWAD"):
|
||||
raise ValueError("Not a WAD file")
|
||||
self.lumps = {}
|
||||
for i in range(numlumps):
|
||||
off, size, name = struct.unpack_from("<II8s", self.data, diroff + 16 * i)
|
||||
name = name.rstrip(b"\0").decode("ascii", "replace")
|
||||
# keep first occurrence (IWAD order)
|
||||
if name not in self.lumps:
|
||||
self.lumps[name] = (off, size)
|
||||
|
||||
def lump(self, name):
|
||||
off, size = self.lumps[name]
|
||||
return self.data[off : off + size]
|
||||
|
||||
def has(self, name):
|
||||
return name in self.lumps
|
||||
|
||||
|
||||
def load_palette(wad):
|
||||
pal = wad.lump("PLAYPAL")[:768]
|
||||
grays = []
|
||||
for i in range(256):
|
||||
r, g, b = pal[i * 3], pal[i * 3 + 1], pal[i * 3 + 2]
|
||||
grays.append(0.299 * r + 0.587 * g + 0.114 * b)
|
||||
return grays
|
||||
|
||||
|
||||
def decode_picture(wad, name, grays):
|
||||
"""Decode Doom picture format -> (w, h, pixels) where pixels is a list of
|
||||
rows; each entry is None (transparent) or gray 0..255."""
|
||||
raw = wad.lump(name)
|
||||
w, h, _lo, _to = struct.unpack_from("<hhhh", raw, 0)
|
||||
colofs = struct.unpack_from("<%di" % w, raw, 8)
|
||||
pix = [[None] * w for _ in range(h)]
|
||||
for x in range(w):
|
||||
p = colofs[x]
|
||||
while raw[p] != 0xFF:
|
||||
topdelta = raw[p]
|
||||
length = raw[p + 1]
|
||||
p += 3 # topdelta, length, pad
|
||||
for i in range(length):
|
||||
y = topdelta + i
|
||||
if 0 <= y < h:
|
||||
pix[y][x] = grays[raw[p]]
|
||||
p += 1
|
||||
p += 1 # trailing pad
|
||||
return w, h, pix
|
||||
|
||||
|
||||
# ------------------------------------------------------------- image helpers
|
||||
|
||||
BAYER4 = [
|
||||
[0, 8, 2, 10],
|
||||
[12, 4, 14, 6],
|
||||
[3, 11, 1, 9],
|
||||
[15, 7, 13, 5],
|
||||
]
|
||||
|
||||
|
||||
def bbox(pix):
|
||||
xs, ys = [], []
|
||||
for y, row in enumerate(pix):
|
||||
for x, v in enumerate(row):
|
||||
if v is not None:
|
||||
xs.append(x)
|
||||
ys.append(y)
|
||||
return min(xs), min(ys), max(xs) + 1, max(ys) + 1
|
||||
|
||||
|
||||
def crop(pix, x0, y0, x1, y1):
|
||||
return [row[x0:x1] for row in pix[y0:y1]]
|
||||
|
||||
|
||||
def box_resize(pix, nw, nh):
|
||||
"""Box-filter resize of (gray|None) grid; alpha = coverage."""
|
||||
h = len(pix)
|
||||
w = len(pix[0])
|
||||
out_gray = [[0.0] * nw for _ in range(nh)]
|
||||
out_alpha = [[0.0] * nw for _ in range(nh)]
|
||||
for ny in range(nh):
|
||||
sy0 = ny * h / nh
|
||||
sy1 = (ny + 1) * h / nh
|
||||
for nx in range(nw):
|
||||
sx0 = nx * w / nw
|
||||
sx1 = (nx + 1) * w / nw
|
||||
acc_g = acc_a = acc_w = 0.0
|
||||
y = int(sy0)
|
||||
while y < sy1 and y < h:
|
||||
wy = min(sy1, y + 1) - max(sy0, y)
|
||||
x = int(sx0)
|
||||
while x < sx1 and x < w:
|
||||
wx = min(sx1, x + 1) - max(sx0, x)
|
||||
weight = wx * wy
|
||||
acc_w += weight
|
||||
v = pix[y][x]
|
||||
if v is not None:
|
||||
acc_a += weight
|
||||
acc_g += weight * v
|
||||
x += 1
|
||||
y += 1
|
||||
if acc_w > 0 and acc_a > 0:
|
||||
out_gray[ny][nx] = acc_g / acc_a
|
||||
out_alpha[ny][nx] = acc_a / acc_w
|
||||
return out_gray, out_alpha
|
||||
|
||||
|
||||
def normalize(gray, alpha, thresh=0.5):
|
||||
"""Per-sprite contrast stretch over opaque pixels."""
|
||||
vals = [
|
||||
gray[y][x]
|
||||
for y in range(len(gray))
|
||||
for x in range(len(gray[0]))
|
||||
if alpha[y][x] >= thresh
|
||||
]
|
||||
if not vals:
|
||||
return gray
|
||||
lo, hi = min(vals), max(vals)
|
||||
if hi - lo < 1e-6:
|
||||
hi = lo + 1.0
|
||||
return [
|
||||
[(v - lo) * 255.0 / (hi - lo) for v in row]
|
||||
for row in gray
|
||||
]
|
||||
|
||||
|
||||
def to_1bit(gray, alpha, bias=0.0):
|
||||
"""3-tone quantization (black / 50% checker / white) for clean tiny
|
||||
sprites. Returns (colour, mask) row-major bools."""
|
||||
h, w = len(gray), len(gray[0])
|
||||
colour = [[False] * w for _ in range(h)]
|
||||
mask = [[False] * w for _ in range(h)]
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if alpha[y][x] >= 0.5:
|
||||
mask[y][x] = True
|
||||
v = gray[y][x] + bias
|
||||
if v < 80:
|
||||
colour[y][x] = False
|
||||
elif v < 175:
|
||||
colour[y][x] = ((x + y) & 1) == 0
|
||||
else:
|
||||
colour[y][x] = True
|
||||
return colour, mask
|
||||
|
||||
|
||||
def fit_grid(pix, size, valign, hpad=0):
|
||||
"""Crop to bbox, keep aspect, fit into size x size grid.
|
||||
valign: 'bottom' or 'center'."""
|
||||
x0, y0, x1, y1 = bbox(pix)
|
||||
pix = crop(pix, x0, y0, x1, y1)
|
||||
w = x1 - x0
|
||||
h = y1 - y0
|
||||
avail = size - hpad * 2
|
||||
if w >= h:
|
||||
nw = avail
|
||||
nh = max(1, round(h * avail / w))
|
||||
else:
|
||||
nh = avail
|
||||
nw = max(1, round(w * avail / h))
|
||||
gray, alpha = box_resize(pix, nw, nh)
|
||||
# paste into size x size
|
||||
g = [[0.0] * size for _ in range(size)]
|
||||
a = [[0.0] * size for _ in range(size)]
|
||||
ox = (size - nw) // 2
|
||||
oy = (size - nh) if valign == "bottom" else (size - nh) // 2
|
||||
for y in range(nh):
|
||||
for x in range(nw):
|
||||
g[oy + y][ox + x] = gray[y][x]
|
||||
a[oy + y][ox + x] = alpha[y][x]
|
||||
return g, a
|
||||
|
||||
|
||||
# ------------------------------------------------------------ format emitters
|
||||
|
||||
|
||||
def emit_scaled16(frames):
|
||||
"""frames: list of (colour, mask) 16x16 row-major -> list of uint16 words."""
|
||||
words = []
|
||||
for colour, mask in frames:
|
||||
for x in range(16):
|
||||
t = c = 0
|
||||
for y in range(16):
|
||||
if mask[y][x]:
|
||||
t |= 1 << y
|
||||
if colour[y][x]:
|
||||
c |= 1 << y
|
||||
words.append(t)
|
||||
words.append(c)
|
||||
return words
|
||||
|
||||
|
||||
def emit_page_sprite(colour, mask):
|
||||
"""-> list of bytes: w, h, then per page per column [colour, mask]."""
|
||||
h, w = len(colour), len(colour[0])
|
||||
pages = (h + 7) // 8
|
||||
out = [w, h]
|
||||
for page in range(pages):
|
||||
for x in range(w):
|
||||
cb = mb = 0
|
||||
for bit in range(8):
|
||||
y = page * 8 + bit
|
||||
if y < h and mask[y][x]:
|
||||
mb |= 1 << bit
|
||||
if colour[y][x]:
|
||||
cb |= 1 << bit
|
||||
out.append(cb)
|
||||
out.append(mb)
|
||||
return out
|
||||
|
||||
|
||||
def emit_solid_bitmap(colour):
|
||||
"""DrawSolidBitmap: bit=1 -> black. colour True = white pixel."""
|
||||
h, w = len(colour), len(colour[0])
|
||||
pages = (h + 7) // 8
|
||||
out = [w, h]
|
||||
for page in range(pages):
|
||||
for x in range(w):
|
||||
b = 0
|
||||
for bit in range(8):
|
||||
y = page * 8 + bit
|
||||
if y < h and not colour[y][x]:
|
||||
b |= 1 << bit
|
||||
out.append(b)
|
||||
return out
|
||||
|
||||
|
||||
def fmt_words(words, per_line=16):
|
||||
lines = []
|
||||
for i in range(0, len(words), per_line):
|
||||
lines.append(",".join("0x%x" % v for v in words[i : i + per_line]))
|
||||
return ",\n\t".join(lines)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ pipeline
|
||||
|
||||
|
||||
def sprite16(wad, grays, names, valign, bias=0.0):
|
||||
frames = []
|
||||
for n in names:
|
||||
w, h, pix = decode_picture(wad, n, grays)
|
||||
g, a = fit_grid(pix, 16, valign)
|
||||
g = normalize(g, a)
|
||||
frames.append(to_1bit(g, a, bias))
|
||||
return emit_scaled16(frames)
|
||||
|
||||
|
||||
def first_present(wad, *names):
|
||||
for n in names:
|
||||
if wad.has(n):
|
||||
return n
|
||||
raise KeyError("none of %s in WAD" % (names,))
|
||||
|
||||
|
||||
def build_weapon(wad, grays, target_w=46):
|
||||
"""Idle shotgun + firing frame (shotgun with muzzle flash composited)."""
|
||||
w, h, gun = decode_picture(wad, "SHTGA0", grays)
|
||||
x0, y0, x1, y1 = bbox(gun)
|
||||
gun = crop(gun, x0, y0, x1, y1)
|
||||
gw, gh = x1 - x0, y1 - y0
|
||||
nw = target_w
|
||||
nh = max(1, round(gh * nw / gw))
|
||||
g, a = box_resize(gun, nw, nh)
|
||||
g = normalize(g, a)
|
||||
# limit height so it doesn't cover too much of the 64px screen: keep the
|
||||
# top rows (barrel); the grip sticks out of the screen bottom like in Doom
|
||||
max_h = 28
|
||||
if nh > max_h:
|
||||
g = g[:max_h]
|
||||
a = a[:max_h]
|
||||
nh = max_h
|
||||
idle = to_1bit(g, a)
|
||||
|
||||
# firing frame: muzzle flash above the barrel
|
||||
fname = first_present(wad, "SHTFB0", "SHTFA0")
|
||||
fw, fh, fl = decode_picture(wad, fname, grays)
|
||||
fx0, fy0, fx1, fy1 = bbox(fl)
|
||||
fl = crop(fl, fx0, fy0, fx1, fy1)
|
||||
fsw = max(1, round((fx1 - fx0) * nw / gw))
|
||||
fsh = max(1, round((fy1 - fy0) * nw / gw))
|
||||
fg, fa = box_resize(fl, fsw, fsh)
|
||||
fg = normalize(fg, fa)
|
||||
# keep total height reasonable: crop the top of the flash if needed
|
||||
max_total = 38
|
||||
if nh + fsh > max_total:
|
||||
cut = nh + fsh - max_total
|
||||
fg = fg[cut:]
|
||||
fa = fa[cut:]
|
||||
fsh -= cut
|
||||
|
||||
# find barrel top-center of scaled gun: centroid of top opaque row
|
||||
top_row = 0
|
||||
for y in range(nh):
|
||||
if any(a[y][x] >= 0.5 for x in range(nw)):
|
||||
top_row = y
|
||||
break
|
||||
cols = [x for x in range(nw) if a[top_row][x] >= 0.5]
|
||||
cx = sum(cols) // len(cols) if cols else nw // 2
|
||||
|
||||
fire_h = nh + fsh
|
||||
FG = [[0.0] * nw for _ in range(fire_h)]
|
||||
FA = [[0.0] * nw for _ in range(fire_h)]
|
||||
for y in range(nh):
|
||||
for x in range(nw):
|
||||
FG[fsh + y][x] = g[y][x]
|
||||
FA[fsh + y][x] = a[y][x]
|
||||
ox = cx - fsw // 2
|
||||
for y in range(fsh):
|
||||
for x in range(fsw):
|
||||
dx = ox + x
|
||||
if 0 <= dx < nw and fa[y][x] >= 0.5:
|
||||
FG[y][dx] = fg[y][x]
|
||||
FA[y][dx] = fa[y][x]
|
||||
fire = to_1bit(FG, FA, bias=40.0) # flash reads brighter
|
||||
|
||||
return emit_page_sprite(*idle), emit_page_sprite(*fire)
|
||||
|
||||
|
||||
TITLE_LETTERS = {
|
||||
"D": [
|
||||
"######.",
|
||||
"##..##.",
|
||||
"##..###",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##..###",
|
||||
"##..##.",
|
||||
"######.",
|
||||
],
|
||||
"O": [
|
||||
".#####.",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
".#####.",
|
||||
],
|
||||
"M": [
|
||||
"##...##",
|
||||
"###.###",
|
||||
"#######",
|
||||
"##.#.##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
"##...##",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_title(wad, grays):
|
||||
"""Original blocky 'DOOM' pixel title with dithered gradient, 128x64."""
|
||||
W, H = 128, 64
|
||||
colour = [[False] * W for _ in range(H)]
|
||||
scale = 4 # each letter 7x8 -> 28x32
|
||||
lw, lh = 7 * scale, 8 * scale
|
||||
gap = 4
|
||||
total = 4 * lw + 3 * gap
|
||||
x0 = (W - total) // 2
|
||||
y0 = (H - lh) // 2
|
||||
for i, ch in enumerate("DOOM"):
|
||||
gl = TITLE_LETTERS[ch]
|
||||
ox = x0 + i * (lw + gap)
|
||||
for gy in range(8):
|
||||
for gx in range(7):
|
||||
if gl[gy][gx] != "#":
|
||||
continue
|
||||
for sy in range(scale):
|
||||
for sx in range(scale):
|
||||
x = ox + gx * scale + sx
|
||||
y = y0 + gy * scale + sy
|
||||
# metallic gradient: solid on top, dithered below
|
||||
if y - y0 < lh * 5 // 8:
|
||||
colour[y][x] = True
|
||||
else:
|
||||
colour[y][x] = ((x + y) & 1) == 0
|
||||
return emit_solid_bitmap(colour)
|
||||
|
||||
|
||||
def icon_from_pixmap(rows):
|
||||
"""8x8 pixmap ('#'=white) -> 8 page bytes, bit0 = top row."""
|
||||
out = []
|
||||
for x in range(8):
|
||||
b = 0
|
||||
for y in range(8):
|
||||
if rows[y][x] == "#":
|
||||
b |= 1 << y
|
||||
out.append(b)
|
||||
return out
|
||||
|
||||
|
||||
HEALTH_ICON = [
|
||||
"..###...",
|
||||
"..#.#...",
|
||||
"###.###.",
|
||||
"#.....#.",
|
||||
"###.###.",
|
||||
"..#.#...",
|
||||
"..###...",
|
||||
"........",
|
||||
]
|
||||
|
||||
AMMO_ICON = [
|
||||
".#..#...",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"###.###.",
|
||||
"........",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
wad_path = sys.argv[1] if len(sys.argv) > 1 else "../doomgeneric/Doom1.WAD"
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
out_path = (
|
||||
sys.argv[2]
|
||||
if len(sys.argv) > 2
|
||||
else os.path.join(root, "game", "Generated", "DoomSprites.inc.h")
|
||||
)
|
||||
|
||||
wad = Wad(wad_path)
|
||||
grays = load_palette(wad)
|
||||
|
||||
scaled = [] # (symbol, numFrames, words)
|
||||
|
||||
def add16(symbol, names, valign, bias=0.0):
|
||||
scaled.append((symbol, len(names), sprite16(wad, grays, names, valign, bias)))
|
||||
|
||||
# enemies (2 walk frames each)
|
||||
add16("skeletonSpriteData", ["SARGA1", "SARGB1"], "bottom") # pinky demon
|
||||
add16("mageSpriteData", ["TROOA1", "TROOB1"], "bottom") # imp
|
||||
add16("batSpriteData", ["SPOSA1", "SPOSB1"], "bottom") # shotgun sergeant
|
||||
add16("spiderSpriteData", ["POSSA1", "POSSB1"], "bottom") # zombieman
|
||||
|
||||
# projectiles
|
||||
ball = first_present(wad, "BAL1A0")
|
||||
ball2 = first_present(wad, "BAL1B0", "BAL1A0")
|
||||
add16("projectileSpriteData", [ball], "center", bias=60.0)
|
||||
add16("enemyProjectileSpriteData", [ball2], "center", bias=60.0)
|
||||
|
||||
# decorations / pickups
|
||||
torch1 = first_present(wad, "TREDA0", "CANDA0")
|
||||
torch2 = first_present(wad, "TREDC0", "TREDB0", "CANDA0")
|
||||
add16("torchSpriteData1", [torch1], "center", bias=40.0)
|
||||
add16("torchSpriteData2", [torch2], "center", bias=40.0)
|
||||
add16("urnSpriteData", ["BAR1A0"], "bottom") # barrel
|
||||
add16("potionSpriteData", ["STIMA0"], "bottom", bias=30.0) # stimpack
|
||||
add16("chestSpriteData", ["BPAKA0"], "bottom", bias=30.0) # backpack
|
||||
add16("chestOpenSpriteData", ["CLIPA0"], "bottom", bias=30.0)
|
||||
add16("scrollSpriteData", ["BON2A0"], "bottom", bias=30.0) # armor helmet
|
||||
add16("coinsSpriteData", ["BON1A0"], "bottom", bias=30.0) # potion bottle
|
||||
add16("crownSpriteData", ["ARM1A0"], "bottom", bias=30.0) # green armor
|
||||
add16("signSpriteData", ["POL5A0"], "bottom", bias=20.0) # skull pile
|
||||
|
||||
weapon_idle, weapon_fire = build_weapon(wad, grays)
|
||||
title = build_title(wad, grays)
|
||||
health = icon_from_pixmap(HEALTH_ICON)
|
||||
ammo = icon_from_pixmap(AMMO_ICON)
|
||||
|
||||
with open(out_path, "w") as f:
|
||||
f.write("// Auto-generated by tools/extract_doom_assets.py\n")
|
||||
f.write("// Derived at build time from the user's local Doom shareware WAD.\n")
|
||||
f.write("// Do not commit WAD-derived data to public repositories.\n\n")
|
||||
for symbol, nframes, words in scaled:
|
||||
f.write("constexpr uint8_t %s_numFrames = %d;\n" % (symbol, nframes))
|
||||
f.write("extern const uint16_t %s[] PROGMEM =\n{\n\t%s\n};\n" % (symbol, fmt_words(words)))
|
||||
f.write("extern const uint8_t handSpriteData1[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(weapon_idle, 24))
|
||||
f.write("extern const uint8_t handSpriteData2[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(weapon_fire, 24))
|
||||
f.write("extern const uint8_t titleBitmapData[] PROGMEM =\n{\n\t%s\n};\n" % fmt_words(title, 24))
|
||||
f.write("extern const uint8_t heartSpriteData[] PROGMEM =\n{\n%s\n};\n" % fmt_words(health))
|
||||
f.write("extern const uint8_t manaSpriteData[] PROGMEM =\n{\n%s\n};\n" % fmt_words(ammo))
|
||||
|
||||
total = sum(len(w) * 2 for _, _, w in scaled) + len(weapon_idle) + len(weapon_fire) + len(title) + 16
|
||||
print("Wrote %s (%d bytes of asset data)" % (out_path, total))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 671 B |
Binary file not shown.
|
After Width: | Height: | Size: 1016 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 287 B |
Reference in New Issue
Block a user