Add Dino Game plugin
This commit is contained in:
+68
-8
@@ -22,6 +22,12 @@ ENVIRONMENTS = {
|
|||||||
"slim": "No serial logging, smallest binary size",
|
"slim": "No serial logging, smallest binary size",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BAUD_RATES = {
|
||||||
|
"1": ("115200", "Recommended - most reliable, avoids 'chip stopped responding'"),
|
||||||
|
"2": ("921600", "Faster - for those feeling froggy"),
|
||||||
|
}
|
||||||
|
DEFAULT_BAUD = "115200"
|
||||||
|
|
||||||
|
|
||||||
def run(cmd, cwd=None, check=True):
|
def run(cmd, cwd=None, check=True):
|
||||||
print(f" $ {cmd}")
|
print(f" $ {cmd}")
|
||||||
@@ -70,7 +76,7 @@ def find_backup_files():
|
|||||||
return sorted(glob.glob("backup.*.bin"), reverse=True)
|
return sorted(glob.glob("backup.*.bin"), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
def restore_from_backup():
|
def restore_from_backup(yes_all: bool = False):
|
||||||
"""If any backup.*.bin files are sitting in the current directory, offer
|
"""If any backup.*.bin files are sitting in the current directory, offer
|
||||||
to write one back to the device instead of proceeding with a fresh
|
to write one back to the device instead of proceeding with a fresh
|
||||||
install. This always runs before backup_device() and, if the user goes
|
install. This always runs before backup_device() and, if the user goes
|
||||||
@@ -80,6 +86,10 @@ def restore_from_backup():
|
|||||||
if not backups:
|
if not backups:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if yes_all:
|
||||||
|
print(f" Found {len(backups)} existing backup(s), skipping restore (--yes).")
|
||||||
|
return
|
||||||
|
|
||||||
print(f" Found {len(backups)} existing backup{'s' if len(backups) != 1 else ''}:")
|
print(f" Found {len(backups)} existing backup{'s' if len(backups) != 1 else ''}:")
|
||||||
for i, b in enumerate(backups, 1):
|
for i, b in enumerate(backups, 1):
|
||||||
tag = " ← most recent" if i == 1 else ""
|
tag = " ← most recent" if i == 1 else ""
|
||||||
@@ -152,7 +162,11 @@ def restore_from_backup():
|
|||||||
sys.exit("Aborted.")
|
sys.exit("Aborted.")
|
||||||
|
|
||||||
|
|
||||||
def backup_device():
|
def backup_device(yes_all: bool = False):
|
||||||
|
if yes_all:
|
||||||
|
print(" Skipping device backup (--yes).")
|
||||||
|
return
|
||||||
|
|
||||||
print(" Backup your current firmware before flashing? [Y/n]: ", end="", flush=True)
|
print(" Backup your current firmware before flashing? [Y/n]: ", end="", flush=True)
|
||||||
answer = input().strip().lower()
|
answer = input().strip().lower()
|
||||||
if answer not in ("", "y", "yes"):
|
if answer not in ("", "y", "yes"):
|
||||||
@@ -423,13 +437,47 @@ def prompt_for_upload_port():
|
|||||||
print(" Please enter a valid port name.")
|
print(" Please enter a valid port name.")
|
||||||
|
|
||||||
|
|
||||||
def build_and_flash(environment: str):
|
def prompt_for_baud_rate(override: str | None, yes_all: bool = False):
|
||||||
|
if override:
|
||||||
|
print(f"\n Upload baud rate: {override} (--baud)")
|
||||||
|
return override
|
||||||
|
|
||||||
|
if yes_all:
|
||||||
|
print(f"\n Upload baud rate: {DEFAULT_BAUD} (--yes)")
|
||||||
|
return DEFAULT_BAUD
|
||||||
|
|
||||||
|
print("\n Select upload baud rate:")
|
||||||
|
for key, (rate, desc) in BAUD_RATES.items():
|
||||||
|
tag = " ← default" if rate == DEFAULT_BAUD else ""
|
||||||
|
print(f" {key}. {rate} — {desc}{tag}")
|
||||||
|
|
||||||
|
choice = input(
|
||||||
|
f"\n Press ENTER for default ({DEFAULT_BAUD}), enter 1-{len(BAUD_RATES)}, "
|
||||||
|
"or type a custom rate: "
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
if not choice:
|
||||||
|
return DEFAULT_BAUD
|
||||||
|
if choice in BAUD_RATES:
|
||||||
|
return BAUD_RATES[choice][0]
|
||||||
|
if choice.isdigit():
|
||||||
|
return choice
|
||||||
|
|
||||||
|
print(f" Invalid input, using default ({DEFAULT_BAUD}).")
|
||||||
|
return DEFAULT_BAUD
|
||||||
|
|
||||||
|
|
||||||
|
def build_and_flash(environment: str, baud_rate: str):
|
||||||
print(f"\nBuilding firmware with PlatformIO (environment: {environment})...")
|
print(f"\nBuilding firmware with PlatformIO (environment: {environment})...")
|
||||||
run(f"pio run --environment {environment}", cwd=REPO_DIR)
|
run(f"pio run --environment {environment}", cwd=REPO_DIR)
|
||||||
|
|
||||||
port = prompt_for_upload_port()
|
port = prompt_for_upload_port()
|
||||||
|
|
||||||
print(f"\n Flashing to {port}...")
|
local_ini = os.path.join(REPO_DIR, "platformio.local.ini")
|
||||||
|
with open(local_ini, "w") as f:
|
||||||
|
f.write(f"[env:{environment}]\nupload_speed = {baud_rate}\n")
|
||||||
|
|
||||||
|
print(f"\n Flashing to {port} at {baud_rate} baud...")
|
||||||
run(
|
run(
|
||||||
f"pio run --target upload --environment {environment} --upload-port {port}",
|
f"pio run --target upload --environment {environment} --upload-port {port}",
|
||||||
cwd=REPO_DIR,
|
cwd=REPO_DIR,
|
||||||
@@ -452,7 +500,18 @@ def parse_args():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-y", "--yes",
|
"-y", "--yes",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Auto-answer Y to all plugin install prompts",
|
help="Auto-answer Y to all plugin install prompts, and skip the "
|
||||||
|
"restore/backup prompts entirely (fast install, no backup)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-b", "--baud",
|
||||||
|
default=None,
|
||||||
|
metavar="RATE",
|
||||||
|
help=(
|
||||||
|
"Upload baud rate override, skips the interactive prompt "
|
||||||
|
f"(default: {DEFAULT_BAUD}). Lower it (e.g. 115200) if flashing "
|
||||||
|
"fails with 'chip stopped responding'."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-reclone",
|
"--no-reclone",
|
||||||
@@ -483,13 +542,14 @@ def main():
|
|||||||
print(" Aborted.")
|
print(" Aborted.")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
print()
|
print()
|
||||||
restore_from_backup()
|
restore_from_backup(yes_all=args.yes)
|
||||||
backup_device()
|
backup_device(yes_all=args.yes)
|
||||||
clone_repo(force=not args.no_reclone)
|
clone_repo(force=not args.no_reclone)
|
||||||
clear_plugin_caches()
|
clear_plugin_caches()
|
||||||
chosen_plugins = select_plugins(yes_all=args.yes)
|
chosen_plugins = select_plugins(yes_all=args.yes)
|
||||||
apply_plugins(chosen_plugins, yes_all=args.yes)
|
apply_plugins(chosen_plugins, yes_all=args.yes)
|
||||||
build_and_flash(args.environment)
|
baud_rate = prompt_for_baud_rate(args.baud, yes_all=args.yes)
|
||||||
|
build_and_flash(args.environment, baud_rate)
|
||||||
print("\nDone.")
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
#include "DinoActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct SpriteRow {
|
||||||
|
uint8_t row, c0, c1;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int DINO_GRID_W = 22;
|
||||||
|
constexpr int DINO_GRID_H = 20;
|
||||||
|
constexpr int DINO_COLLIDE_X_OFFSET = 6;
|
||||||
|
constexpr int CACTUS_SMALL_W = 15;
|
||||||
|
constexpr int CACTUS_SMALL_H = 20;
|
||||||
|
constexpr int CACTUS_BIG_W = 15;
|
||||||
|
constexpr int CACTUS_BIG_H = 26;
|
||||||
|
constexpr int BIRD_W = 18;
|
||||||
|
constexpr int BIRD_H = 7;
|
||||||
|
|
||||||
|
constexpr SpriteRow DINO_HEAD_BODY[] = {
|
||||||
|
{0,14,19},{1,13,20},{2,13,20},{3,13,20},{4,13,21},{5,13,21},{6,10,19},
|
||||||
|
{7,11,17},{8,9,18},{9,6,18},{10,0,15},{11,0,15},{12,0,15},
|
||||||
|
{13,0,0},{13,4,15},{14,8,15},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SpriteRow DINO_RUN1_LEGS[] = {
|
||||||
|
{15,6,9},{16,6,9},{17,6,9},{18,6,9},{19,5,9},
|
||||||
|
{15,11,14},{16,11,14},{17,11,14},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SpriteRow DINO_RUN2_LEGS[] = {
|
||||||
|
{15,6,9},{16,6,9},{17,6,9},
|
||||||
|
{15,11,14},{16,11,14},{17,11,14},{18,11,14},{19,11,15},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SpriteRow DINO_JUMP_LEGS[] = {
|
||||||
|
{15,7,14},{16,7,14},{17,7,14},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SpriteRow CACTUS_SMALL[] = {
|
||||||
|
{0,7,7},
|
||||||
|
{1,6,8},
|
||||||
|
{2,3,3},{2,6,8},{2,11,11},
|
||||||
|
{3,2,4},{3,6,8},{3,10,12},
|
||||||
|
{4,2,4},{4,6,8},{4,10,12},
|
||||||
|
{5,2,4},{5,6,8},{5,10,12},
|
||||||
|
{6,2,4},{6,6,8},{6,10,12},
|
||||||
|
{7,2,4},{7,6,8},{7,10,12},
|
||||||
|
{8,2,4},{8,6,8},{8,10,12},
|
||||||
|
{9,2,12},
|
||||||
|
{10,2,12},
|
||||||
|
{11,6,8},{12,6,8},{13,6,8},{14,6,8},{15,6,8},
|
||||||
|
{16,6,8},{17,6,8},{18,6,8},{19,6,8},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SpriteRow CACTUS_BIG[] = {
|
||||||
|
{0,7,7},
|
||||||
|
{1,6,8},
|
||||||
|
{2,3,3},{2,6,8},{2,11,11},
|
||||||
|
{3,2,4},{3,6,8},{3,10,12},
|
||||||
|
{4,2,4},{4,6,8},{4,10,12},
|
||||||
|
{5,2,4},{5,6,8},{5,10,12},
|
||||||
|
{6,2,4},{6,6,8},{6,10,12},
|
||||||
|
{7,2,4},{7,6,8},{7,10,12},
|
||||||
|
{8,2,4},{8,6,8},{8,10,12},
|
||||||
|
{9,2,12},
|
||||||
|
{10,2,12},
|
||||||
|
{11,6,8},{12,6,8},{13,6,8},{14,6,8},{15,6,8},
|
||||||
|
{16,6,8},{17,6,8},{18,6,8},{19,6,8},{20,6,8},{21,6,8},
|
||||||
|
{22,6,8},{23,6,8},{24,6,8},{25,6,8},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SpriteRow BIRD_UP[] = {
|
||||||
|
{0,2,3},{0,14,15},
|
||||||
|
{1,4,5},{1,12,13},
|
||||||
|
{2,6,7},{2,10,11},
|
||||||
|
{3,6,11},
|
||||||
|
{4,7,10},
|
||||||
|
{5,8,9},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SpriteRow BIRD_DOWN[] = {
|
||||||
|
{0,7,10},
|
||||||
|
{1,6,11},
|
||||||
|
{2,7,10},
|
||||||
|
{3,8,9},
|
||||||
|
{4,6,7},{4,10,11},
|
||||||
|
{5,4,5},{5,12,13},
|
||||||
|
{6,2,3},{6,14,15},
|
||||||
|
};
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
constexpr size_t lengthOf(const SpriteRow (&)[N]) { return N; }
|
||||||
|
|
||||||
|
void drawSprite(GfxRenderer& r, const SpriteRow* rows, size_t count, int x, int y, int block) {
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
int w = (rows[i].c1 - rows[i].c0 + 1) * block;
|
||||||
|
r.fillRect(x + rows[i].c0 * block, y + rows[i].row * block, w, block, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void DinoActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
|
||||||
|
screenW_ = renderer.getScreenWidth();
|
||||||
|
screenH_ = renderer.getScreenHeight();
|
||||||
|
groundY_ = screenH_ - GROUND_MARGIN;
|
||||||
|
|
||||||
|
randomSeed(millis());
|
||||||
|
resetGame();
|
||||||
|
|
||||||
|
renderPending_ = true;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::onExit() {
|
||||||
|
Activity::onExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::resetGame() {
|
||||||
|
dinoY_ = 0;
|
||||||
|
velocityY_ = 0;
|
||||||
|
jumping_ = false;
|
||||||
|
|
||||||
|
for (auto& ob : obstacles_) {
|
||||||
|
ob.active = false;
|
||||||
|
}
|
||||||
|
distanceSinceSpawn_ = 0;
|
||||||
|
nextSpawnDistance_ = START_SPEED * (float)random(MIN_GAP_TICKS, MAX_GAP_TICKS);
|
||||||
|
|
||||||
|
birdX_ = screenW_ + random(0, 200);
|
||||||
|
birdY_ = random(BIRD_TOP_MIN, BIRD_TOP_MAX);
|
||||||
|
|
||||||
|
gameSpeed_ = START_SPEED;
|
||||||
|
score_ = 0;
|
||||||
|
gameOver_ = false;
|
||||||
|
animFrame_ = false;
|
||||||
|
|
||||||
|
lastUpdate_ = millis();
|
||||||
|
lastAnim_ = lastUpdate_;
|
||||||
|
lastScoreTick_ = lastUpdate_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DinoActivity::obstacleWidthPx(ObstacleType type) const {
|
||||||
|
switch (type) {
|
||||||
|
case ObstacleType::CactusSmall: return CACTUS_SMALL_W * BLOCK;
|
||||||
|
case ObstacleType::CactusBig: return CACTUS_BIG_W * BLOCK;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DinoActivity::obstacleHeightPx(ObstacleType type) const {
|
||||||
|
switch (type) {
|
||||||
|
case ObstacleType::CactusSmall: return CACTUS_SMALL_H * BLOCK;
|
||||||
|
case ObstacleType::CactusBig: return CACTUS_BIG_H * BLOCK;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::spawnObstacle() {
|
||||||
|
int slot = -1;
|
||||||
|
for (int i = 0; i < MAX_OBSTACLES; i++) {
|
||||||
|
if (!obstacles_[i].active) {
|
||||||
|
slot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slot < 0) return;
|
||||||
|
|
||||||
|
ObstacleType type = (random(0, 100) < 55) ? ObstacleType::CactusSmall : ObstacleType::CactusBig;
|
||||||
|
|
||||||
|
obstacles_[slot].active = true;
|
||||||
|
obstacles_[slot].type = type;
|
||||||
|
obstacles_[slot].x = screenW_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DinoActivity::anyNonPowerPressed() const {
|
||||||
|
return mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
||||||
|
mappedInput.wasPressed(MappedInputManager::Button::Confirm) ||
|
||||||
|
mappedInput.wasPressed(MappedInputManager::Button::Left) ||
|
||||||
|
mappedInput.wasPressed(MappedInputManager::Button::Right) ||
|
||||||
|
mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||||
|
mappedInput.wasPressed(MappedInputManager::Button::Down);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DinoActivity::checkCollision() const {
|
||||||
|
constexpr int INSET = 4;
|
||||||
|
|
||||||
|
int dw = (DINO_GRID_W - DINO_COLLIDE_X_OFFSET) * BLOCK;
|
||||||
|
int dh = DINO_GRID_H * BLOCK;
|
||||||
|
int dx0 = DINO_X + DINO_COLLIDE_X_OFFSET * BLOCK + INSET;
|
||||||
|
int dx1 = DINO_X + DINO_COLLIDE_X_OFFSET * BLOCK + dw - INSET;
|
||||||
|
int dy0 = groundY_ - dh + (int)dinoY_ + INSET;
|
||||||
|
int dy1 = groundY_ + (int)dinoY_ - INSET;
|
||||||
|
|
||||||
|
for (const auto& ob : obstacles_) {
|
||||||
|
if (!ob.active) continue;
|
||||||
|
|
||||||
|
int ow = obstacleWidthPx(ob.type);
|
||||||
|
int oh = obstacleHeightPx(ob.type);
|
||||||
|
int oy0 = groundY_ - oh;
|
||||||
|
int oy1 = oy0 + oh;
|
||||||
|
int ox0 = (int)ob.x;
|
||||||
|
int ox1 = ox0 + ow;
|
||||||
|
|
||||||
|
if (dx0 < ox1 && dx1 > ox0 && dy0 < oy1 && dy1 > oy0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::updateGame() {
|
||||||
|
for (auto& ob : obstacles_) {
|
||||||
|
if (!ob.active) continue;
|
||||||
|
ob.x -= gameSpeed_;
|
||||||
|
if (ob.x + obstacleWidthPx(ob.type) < 0) {
|
||||||
|
ob.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
distanceSinceSpawn_ += gameSpeed_;
|
||||||
|
if (distanceSinceSpawn_ >= nextSpawnDistance_) {
|
||||||
|
spawnObstacle();
|
||||||
|
distanceSinceSpawn_ = 0;
|
||||||
|
nextSpawnDistance_ = gameSpeed_ * (float)random(MIN_GAP_TICKS, MAX_GAP_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
birdX_ -= BIRD_SPEED;
|
||||||
|
if (birdX_ + BIRD_W * BLOCK < 0) {
|
||||||
|
birdX_ = screenW_ + random(0, 200);
|
||||||
|
birdY_ = random(BIRD_TOP_MIN, BIRD_TOP_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jumping_) {
|
||||||
|
velocityY_ += GRAVITY;
|
||||||
|
dinoY_ += velocityY_;
|
||||||
|
if (dinoY_ >= 0) {
|
||||||
|
dinoY_ = 0;
|
||||||
|
velocityY_ = 0;
|
||||||
|
jumping_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gameSpeed_ = START_SPEED + SPEED_STEP * (float)(score_ / SCORE_PER_SPEEDUP);
|
||||||
|
if (gameSpeed_ > MAX_SPEED) gameSpeed_ = MAX_SPEED;
|
||||||
|
|
||||||
|
if (checkCollision()) {
|
||||||
|
gameOver_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::loop() {
|
||||||
|
if (gameOver_) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Power)) {
|
||||||
|
while (mappedInput.isPressed(MappedInputManager::Button::Power)) {
|
||||||
|
mappedInput.update();
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (anyNonPowerPressed()) {
|
||||||
|
resetGame();
|
||||||
|
renderPending_ = true;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Power)) {
|
||||||
|
while (mappedInput.isPressed(MappedInputManager::Button::Power)) {
|
||||||
|
mappedInput.update();
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyNonPowerPressed() && !jumping_) {
|
||||||
|
jumping_ = true;
|
||||||
|
velocityY_ = JUMP_VELOCITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long now = millis();
|
||||||
|
|
||||||
|
if (now - lastUpdate_ >= PHYSICS_INTERVAL_MS) {
|
||||||
|
lastUpdate_ = now;
|
||||||
|
updateGame();
|
||||||
|
renderPending_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - lastAnim_ >= ANIM_INTERVAL_MS) {
|
||||||
|
lastAnim_ = now;
|
||||||
|
animFrame_ = !animFrame_;
|
||||||
|
renderPending_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - lastScoreTick_ >= SCORE_INTERVAL_MS) {
|
||||||
|
lastScoreTick_ = now;
|
||||||
|
score_++;
|
||||||
|
renderPending_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderPending_) {
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::drawDino(int x, int topY) const {
|
||||||
|
drawSprite(renderer, DINO_HEAD_BODY, lengthOf(DINO_HEAD_BODY), x, topY, BLOCK);
|
||||||
|
|
||||||
|
const SpriteRow* legSpans;
|
||||||
|
size_t legCount;
|
||||||
|
if (jumping_) {
|
||||||
|
legSpans = DINO_JUMP_LEGS;
|
||||||
|
legCount = lengthOf(DINO_JUMP_LEGS);
|
||||||
|
} else {
|
||||||
|
legSpans = animFrame_ ? DINO_RUN2_LEGS : DINO_RUN1_LEGS;
|
||||||
|
legCount = animFrame_ ? lengthOf(DINO_RUN2_LEGS) : lengthOf(DINO_RUN1_LEGS);
|
||||||
|
}
|
||||||
|
drawSprite(renderer, legSpans, legCount, x, topY, BLOCK);
|
||||||
|
|
||||||
|
constexpr int EYE_ROW = 2, EYE_C0 = 15, EYE_C1 = 16, EYE_ROWS = 2;
|
||||||
|
renderer.fillRect(x + EYE_C0 * BLOCK, topY + EYE_ROW * BLOCK,
|
||||||
|
(EYE_C1 - EYE_C0 + 1) * BLOCK, EYE_ROWS * BLOCK, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::drawObstacle(const Obstacle& ob) const {
|
||||||
|
int h = obstacleHeightPx(ob.type);
|
||||||
|
int x = (int)ob.x;
|
||||||
|
int y = groundY_ - h;
|
||||||
|
|
||||||
|
switch (ob.type) {
|
||||||
|
case ObstacleType::CactusSmall:
|
||||||
|
drawSprite(renderer, CACTUS_SMALL, lengthOf(CACTUS_SMALL), x, y, BLOCK);
|
||||||
|
break;
|
||||||
|
case ObstacleType::CactusBig:
|
||||||
|
drawSprite(renderer, CACTUS_BIG, lengthOf(CACTUS_BIG), x, y, BLOCK);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::drawBird() const {
|
||||||
|
if (animFrame_) {
|
||||||
|
drawSprite(renderer, BIRD_UP, lengthOf(BIRD_UP), (int)birdX_, birdY_, BLOCK);
|
||||||
|
} else {
|
||||||
|
drawSprite(renderer, BIRD_DOWN, lengthOf(BIRD_DOWN), (int)birdX_, birdY_, BLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DinoActivity::render(RenderLock&&) {
|
||||||
|
renderPending_ = false;
|
||||||
|
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
renderer.drawLine(0, groundY_, screenW_, groundY_);
|
||||||
|
|
||||||
|
drawBird();
|
||||||
|
|
||||||
|
char scoreStr[16];
|
||||||
|
snprintf(scoreStr, sizeof(scoreStr), "%05lu", score_);
|
||||||
|
int scoreW = renderer.getTextWidth(UI_10_FONT_ID, scoreStr);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, screenW_ - scoreW - 24, 24, scoreStr, true);
|
||||||
|
|
||||||
|
int dinoTopY = groundY_ - DINO_GRID_H * BLOCK + (int)dinoY_;
|
||||||
|
drawDino(DINO_X, dinoTopY);
|
||||||
|
|
||||||
|
for (const auto& ob : obstacles_) {
|
||||||
|
if (ob.active) drawObstacle(ob);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameOver_) {
|
||||||
|
const char* line1 = "GAME OVER";
|
||||||
|
const char* line2 = "Any button to Restart";
|
||||||
|
const char* line3 = "Power button to Exit";
|
||||||
|
|
||||||
|
int w1 = renderer.getTextWidth(UI_12_FONT_ID, line1, EpdFontFamily::BOLD);
|
||||||
|
int w2 = renderer.getTextWidth(UI_10_FONT_ID, line2);
|
||||||
|
int w3 = renderer.getTextWidth(UI_10_FONT_ID, line3);
|
||||||
|
|
||||||
|
int baseY = groundY_ / 2 - 20;
|
||||||
|
renderer.drawText(UI_12_FONT_ID, screenW_ / 2 - w1 / 2, baseY,
|
||||||
|
line1, true, EpdFontFamily::BOLD);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, screenW_ / 2 - w2 / 2, baseY + 36, line2, true);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, screenW_ / 2 - w3 / 2, baseY + 62, line3, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "activities/Activity.h"
|
||||||
|
|
||||||
|
class DinoActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
explicit DinoActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
|
: Activity("Dino", renderer, mappedInput) {}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
bool skipLoopDelay() override { return true; }
|
||||||
|
void render(RenderLock&&) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class ObstacleType { CactusSmall, CactusBig };
|
||||||
|
|
||||||
|
struct Obstacle {
|
||||||
|
bool active = false;
|
||||||
|
ObstacleType type = ObstacleType::CactusSmall;
|
||||||
|
float x = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int BLOCK = 5;
|
||||||
|
static constexpr int GROUND_MARGIN = 60;
|
||||||
|
static constexpr int DINO_X = 36;
|
||||||
|
static constexpr float GRAVITY = 0.5f;
|
||||||
|
static constexpr float JUMP_VELOCITY = -16.0f;
|
||||||
|
static constexpr int MAX_OBSTACLES = 3;
|
||||||
|
static constexpr float START_SPEED = 4.2f;
|
||||||
|
static constexpr float MAX_SPEED = 11.0f;
|
||||||
|
static constexpr float SPEED_STEP = 0.35f;
|
||||||
|
static constexpr int SCORE_PER_SPEEDUP = 150;
|
||||||
|
static constexpr int MIN_GAP_TICKS = 100;
|
||||||
|
static constexpr int MAX_GAP_TICKS = 160;
|
||||||
|
static constexpr float BIRD_SPEED = 2.0f;
|
||||||
|
static constexpr int BIRD_TOP_MIN = 40;
|
||||||
|
static constexpr int BIRD_TOP_MAX = 140;
|
||||||
|
static constexpr unsigned long PHYSICS_INTERVAL_MS = 33;
|
||||||
|
static constexpr unsigned long ANIM_INTERVAL_MS = 130;
|
||||||
|
static constexpr unsigned long SCORE_INTERVAL_MS = 90;
|
||||||
|
|
||||||
|
int screenW_ = 0;
|
||||||
|
int screenH_ = 0;
|
||||||
|
int groundY_ = 0;
|
||||||
|
|
||||||
|
float dinoY_ = 0;
|
||||||
|
float velocityY_ = 0;
|
||||||
|
bool jumping_ = false;
|
||||||
|
|
||||||
|
Obstacle obstacles_[MAX_OBSTACLES];
|
||||||
|
float distanceSinceSpawn_ = 0;
|
||||||
|
float nextSpawnDistance_ = 0;
|
||||||
|
|
||||||
|
float birdX_ = 0;
|
||||||
|
int birdY_ = 0;
|
||||||
|
|
||||||
|
float gameSpeed_ = START_SPEED;
|
||||||
|
unsigned long score_ = 0;
|
||||||
|
|
||||||
|
bool gameOver_ = false;
|
||||||
|
bool renderPending_ = false;
|
||||||
|
bool animFrame_ = false;
|
||||||
|
|
||||||
|
unsigned long lastUpdate_ = 0;
|
||||||
|
unsigned long lastAnim_ = 0;
|
||||||
|
unsigned long lastScoreTick_ = 0;
|
||||||
|
|
||||||
|
void resetGame();
|
||||||
|
void updateGame();
|
||||||
|
void spawnObstacle();
|
||||||
|
bool checkCollision() const;
|
||||||
|
bool anyNonPowerPressed() const;
|
||||||
|
void drawDino(int x, int topY) const;
|
||||||
|
void drawObstacle(const Obstacle& ob) const;
|
||||||
|
void drawBird() const;
|
||||||
|
int obstacleWidthPx(ObstacleType type) const;
|
||||||
|
int obstacleHeightPx(ObstacleType type) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
from framework.manifest import (
|
||||||
|
PluginManifest, SourceFile, Include, SettingActionEnumValue, PluginsTabEntry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_manifest(ctx):
|
||||||
|
return PluginManifest(
|
||||||
|
name="dinogame",
|
||||||
|
pretty_name="Dino Game",
|
||||||
|
source_files=[
|
||||||
|
SourceFile("DinoActivity.h", "src/activities"),
|
||||||
|
SourceFile("DinoActivity.cpp", "src/activities"),
|
||||||
|
],
|
||||||
|
includes=[
|
||||||
|
Include("activities/DinoActivity.h", "settings_activity_cpp"),
|
||||||
|
],
|
||||||
|
setting_actions=[
|
||||||
|
SettingActionEnumValue("DinoGame"),
|
||||||
|
],
|
||||||
|
plugins_tab_entries=[
|
||||||
|
PluginsTabEntry(
|
||||||
|
label="Dino Game",
|
||||||
|
kind="action",
|
||||||
|
action_name="DinoGame",
|
||||||
|
action_value_text="Launch",
|
||||||
|
activity_launch_expr="std::make_unique<DinoActivity>(renderer, mappedInput)",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user