Compare commits

..

3 Commits

Author SHA1 Message Date
348bd4fe64 cleanup: QOL improvements — gitignore, Tags constants, remove debug artifacts
- Add .DS_Store, unused/, .vscode/settings.json to .gitignore
- Add .editorconfig (tabs for Lua, LF, UTF-8)
- Fix duplicate submodule entry in .gitmodules
- Add Tags table (player, tank, ground, granade, ammoCrate) — replace magic numbers
- Add SCREEN_W/SCREEN_H constants
- Fix leaked global variable `c` in Game.lua
- Remove Noble.showFPS = true from BomberScene
- Remove debug print() calls from granade, enemy, BomberScene
- Fix clean_build_dir glob bug in both build scripts
- Make PLAYDATE_SDK_PATH configurable via env var
2026-02-24 12:48:00 +01:00
8a039adc05 rework + cool bomber 2026-02-24 00:46:50 +01:00
9eb426021e unknown 2026-02-23 20:52:54 +01:00
62 changed files with 1022 additions and 146 deletions

BIN
.DS_Store vendored

Binary file not shown.

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.sh]
indent_style = space
indent_size = 4

15
.gitignore vendored
View File

@@ -1 +1,14 @@
builds/* builds/
source/assets/unused/
# OS
.DS_Store
Thumbs.db
# IDE (machine-specific)
.vscode/settings.json
# Editors
*.swp
*.swo
*~

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "Noble Engine"]
path = source/libraries/noble
url = https://github.com/NobleRobot/NobleEngine.git
[submodule "source/libraries/noble"] [submodule "source/libraries/noble"]
path = source/libraries/noble path = source/libraries/noble
url = https://github.com/NobleRobot/NobleEngine.git url = https://github.com/NobleRobot/NobleEngine.git

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export PLAYDATE_SDK_PATH="/home/ut3usw/PlaydateSDK-2.5.0" export PLAYDATE_SDK_PATH="${PLAYDATE_SDK_PATH:-/home/ut3usw/PlaydateSDK-2.5.0}"
# Check for color by variable and tput command # Check for color by variable and tput command
if [[ -z $NOCOLOR && -n $(command -v tput) ]]; then if [[ -z $NOCOLOR && -n $(command -v tput) ]]; then
@@ -103,7 +103,7 @@ function make_build_dir() {
function clean_build_dir() { function clean_build_dir() {
if [[ -d "${BUILD_DIR}" ]]; then if [[ -d "${BUILD_DIR}" ]]; then
log "Cleaning build directory..." log "Cleaning build directory..."
rm -rfv "${BUILD_DIR}/*" rm -rfv "${BUILD_DIR}"/*
chk_err chk_err
fi fi
} }

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export PLAYDATE_SDK_PATH="/Users/oleksiiilienko/Developer/PlaydateSDK" export PLAYDATE_SDK_PATH="${PLAYDATE_SDK_PATH:-/Users/oleksiiilienko/Developer/PlaydateSDK}"
# Check for color by variable and tput command # Check for color by variable and tput command
if [[ -z $NOCOLOR && -n $(command -v tput) ]]; then if [[ -z $NOCOLOR && -n $(command -v tput) ]]; then
@@ -103,7 +103,7 @@ function make_build_dir() {
function clean_build_dir() { function clean_build_dir() {
if [[ -d "${BUILD_DIR}" ]]; then if [[ -d "${BUILD_DIR}" ]]; then
log "Cleaning build directory..." log "Cleaning build directory..."
rm -rfv "${BUILD_DIR}/*" rm -rfv "${BUILD_DIR}"/*
chk_err chk_err
fi fi
} }

BIN
source/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 297 B

After

Width:  |  Height:  |  Size: 297 B

View File

Before

Width:  |  Height:  |  Size: 535 B

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -24,28 +24,45 @@ ZIndex = {
ui = 10, ui = 10,
alert = 12, alert = 12,
ground = 100, ground = 100,
flash = 101 flash = 101,
foreground = 102
} }
CollideGroups = { CollideGroups = {
player = 1, player = 1,
enemy = 2, enemy = 2,
props = 3, props = 3,
items = 4, items = 4,
wall = 5 wall = 5,
granade = 6
} }
Tags = {
player = 1,
tank = 2,
ground = 3,
granade = 154,
ammoCrate = 155,
}
SCREEN_W = 400
SCREEN_H = 240
Maps = { Maps = {
{ {
id = 1, id = 1,
name = "Vovchansk", name = "Vovchansk",
description = "This is a map", description = "This is a map",
locked = false, locked = false,
unlockMissions = 0,
killTarget = 10,
}, },
{ {
id = 2, id = 2,
name = "Mariupol", name = "Mariupol",
description = "This is a map", description = "This is a map",
locked = false, locked = true,
unlockMissions = 3,
killTarget = 15,
} }
} }
@@ -69,9 +86,10 @@ Drones = {
{ {
id = 2, id = 2,
mode = Modes.bomber, mode = Modes.bomber,
name = "Drone 2", name = "Bomber",
description = "This is a drone", description = "This is a drone",
price = 200, price = 200,
stockPrice = 50,
locked = false, locked = false,
preview = nil, preview = nil,
full = nil full = nil
@@ -80,7 +98,7 @@ Drones = {
id = 3, id = 3,
name = "Drone 3", name = "Drone 3",
description = "This is a drone", description = "This is a drone",
price = 300, price = -1,
locked = true, locked = true,
preview = nil, preview = nil,
full = nil full = nil
@@ -89,7 +107,7 @@ Drones = {
id = 4, id = 4,
name = "Drone 4", name = "Drone 4",
description = "This is a drone", description = "This is a drone",
price = 400, price = -1,
locked = true, locked = true,
preview = nil, preview = nil,
full = nil full = nil
@@ -111,7 +129,12 @@ import "scripts/MapCard"
import "scripts/bomber/movableCrosshair" import "scripts/bomber/movableCrosshair"
import "scripts/bomber/granade" import "scripts/bomber/granade"
import "scripts/bomber/explosionMark" import "scripts/bomber/explosionMark"
import "scripts/bomber/enemy"
import "scripts/bomber/ammoCrate"
import "scripts/bomber/smokeCloud"
import "scripts/bomber/floatingText"
import "scripts/bomber/allyBullet"
import "scripts/bomber/noiseAnimation"
import "scenes/BaseScene" import "scenes/BaseScene"
import 'scenes/Assemble' import 'scenes/Assemble'
import 'scenes/DroneCardSelector' import 'scenes/DroneCardSelector'
@@ -147,16 +170,55 @@ Noble.Settings.setup({
debug = false debug = false
}) })
Targets = {
{
id = "tank",
name = "Tank",
sprite = "assets/sprites/targets/tank",
spriteD = "assets/sprites/targets/tank_dead",
briefing = [[The drone is assembled and operational. We are ready for the mission.
An enemy tank is confirmed in the field. It threatens our advance.
Your task: eliminate the target. Clear the path for our assault units.
This operation is crucial. Execute with precision. Command out.]],
},
{
id = "btr",
name = "BTR",
sprite = "assets/sprites/targets/btr",
spriteD = "assets/sprites/targets/btr_dead",
briefing = [[The drone is assembled and operational. We are ready for the mission.
An enemy BTR has been spotted moving through the area. It's transporting troops.
Your task: hit the BTR before it reaches the frontline. Stop the reinforcements.
Time is critical. Strike hard. Command out.]],
},
}
CurrentMission = {
mapId = 1,
droneId = 1,
targetIndex = 1,
}
Noble.GameData.setup({ Noble.GameData.setup({
drone1 = 0, drone1 = 0,
drone2 = 0, drone2 = 0,
drone3 = 0, drone3 = 0,
drone4 = 0, drone4 = 0,
money = 150 money = 500,
bomberStock = 3,
missionsCompleted = 0,
}) })
playdate.display.setRefreshRate(50) playdate.display.setRefreshRate(50)
Noble.showFPS = false Noble.showFPS = false
Noble.new(BomberScene)
--Noble.new(BomberScene)
Noble.new(Menu)

View File

@@ -2,7 +2,7 @@ name=FPV Game
author=ut3usw author=ut3usw
description=This is a FPV Game description=This is a FPV Game
bundleID=guru.dead.fpv bundleID=guru.dead.fpv
version=0.2.0 version=0.2.6
buildNumber=10 buildNumber=13
imagePath=assets/launcher/ imagePath=assets/launcher/
launchSoundPath=assets/launcher/sound.wav launchSoundPath=assets/launcher/sound.wav

View File

@@ -13,7 +13,7 @@ function scene:popCode(button)
end end
scene.menuConfirmSound:stop() scene.menuConfirmSound:stop()
if scene.tickTimer.paused then if scene.tickTimer.paused then
scene.droneParts = scene:loadDrone(1, #scene.code) scene.droneParts = scene:loadDrone(CurrentMission.droneId, #scene.code)
scene.tickTimer:start() scene.tickTimer:start()
scene.progressBar:setVisible(true) scene.progressBar:setVisible(true)
end end
@@ -146,24 +146,12 @@ function scene:enter()
scene.buttonTimeout = 100 scene.buttonTimeout = 100
Noble.Input.setHandler(scene.inputHandler) Noble.Input.setHandler(scene.inputHandler)
local text = CurrentMission.targetIndex = math.random(1, #Targets)
[[The drone is assembled and operational. We are ready for the mission. local target = Targets[CurrentMission.targetIndex]
self.dialogue = pdDialogueBox(target.briefing, 390, 46)
An enemy tank is confirmed in the field. It threatens our advance.
Your task: eliminate the target. Clear the path for our assault units.
This operation is crucial. Execute with precision. Command out.]]
self.dialogue = pdDialogueBox(text, 390, 46)
-- self.dialogue:setPadding(4) -- self.dialogue:setPadding(4)
end end
function round(number)
local formatted = string.format("%.2f", number)
return formatted
end
local elapsedTime = 0 local elapsedTime = 0
function scene:update() function scene:update()
scene.super.update(self) scene.super.update(self)

View File

@@ -6,22 +6,59 @@ local elapsedTime = 0
scene.inputHandler = { scene.inputHandler = {
AButtonDown = function() AButtonDown = function()
if Drones[scene.menuIndex].locked == true then local drone = Drones[scene.menuIndex]
-- If locked, try to buy
if drone.locked == true then
if drone.price <= 0 then return end
local money = Noble.GameData.get("money")
if money >= drone.price then
Noble.GameData.set("money", money - drone.price)
Noble.GameData.set("drone" .. drone.id, 1)
drone.locked = false
scene.menuConfirmSound:play(1)
scene.purchaseText = "-$" .. drone.price
scene.purchaseTimer = playdate.timer.new(1200, 0, 40, playdate.easingFunctions.outCubic)
screenShake(200, 3)
else
screenShake(300, 5)
scene.noMoneyTimer = playdate.timer.new(800, 0, 800, playdate.easingFunctions.linear)
end
return return
end end
CurrentMission.droneId = drone.id
scene.menuConfirmSound:play(1) scene.menuConfirmSound:play(1)
mode = Drones[scene.menuIndex].mode local mode = drone.mode
local soundTable = playdate.sound.playingSources() local soundTable = playdate.sound.playingSources()
for i=1, #soundTable do for i=1, #soundTable do
soundTable[i]:stop() soundTable[i]:stop()
end end
if mode == Modes.bomber then if mode == Modes.bomber then
local stock = Noble.GameData.get("bomberStock")
if stock <= 0 then
return
end
Noble.GameData.set("bomberStock", stock - 1)
Noble.transition(BomberScene) Noble.transition(BomberScene)
else else
Noble.transition(Assemble) Noble.transition(Assemble)
end end
end, end,
BButtonDown = function() BButtonDown = function()
-- B on bomber drone: buy stock
local drone = Drones[scene.menuIndex]
if not drone.locked and drone.mode == Modes.bomber then
local money = Noble.GameData.get("money")
local stockPrice = drone.stockPrice or 50
if money >= stockPrice then
Noble.GameData.set("money", money - stockPrice)
Noble.GameData.set("bomberStock", Noble.GameData.get("bomberStock") + 1)
scene.menuConfirmSound:play(1)
scene.purchaseText = "-$" .. stockPrice
scene.purchaseY = 0
scene.purchaseTimer = playdate.timer.new(1200, 0, 40, playdate.easingFunctions.outCubic)
return
end
end
scene.menuBackSound:play(1) scene.menuBackSound:play(1)
Noble.transition(MapSelector) Noble.transition(MapSelector)
end, end,
@@ -62,6 +99,10 @@ function scene:setValues()
scene.currentX = 0 scene.currentX = 0
scene.targetX = 0 scene.targetX = 0
scene.purchaseText = nil
scene.purchaseTimer = nil
scene.noMoneyTimer = nil
end end
function scene:init() function scene:init()
@@ -78,6 +119,16 @@ end
function scene:enter() function scene:enter()
scene.super.enter(self) scene.super.enter(self)
-- Update locked state from GameData
for i = 1, #Drones do
Drones[i].locked = (Noble.GameData.get("drone" .. i) == 0)
end
scene.menuIndex = 1
scene.currentX = 0
scene.targetX = 0
scene.cards = {} scene.cards = {}
for i = 1, #Drones do for i = 1, #Drones do
scene.cards[i] = DroneCard(0, 0, Drones[i]) scene.cards[i] = DroneCard(0, 0, Drones[i])
@@ -108,13 +159,54 @@ function scene:update()
scene.cards[i]:moveTo(x + scene.currentX, 25) scene.cards[i]:moveTo(x + scene.currentX, 25)
end end
if Drones[scene.menuIndex].locked == false then local drone = Drones[scene.menuIndex]
-- Money display
Noble.Text.draw("$" .. Noble.GameData.get("money"), 200, 5, Noble.Text.ALIGN_CENTER, false, fontMed)
if drone.locked and drone.price > 0 then
self.aKey:draw(315, 207 + dy)
Noble.Text.draw("Buy $" .. drone.price, 333, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
elseif drone.locked then
Noble.Text.draw("Coming soon", 340, 210, Noble.Text.ALIGN_CENTER, false, fontMed)
elseif drone.mode == Modes.bomber then
local stock = Noble.GameData.get("bomberStock")
self.aKey:draw(315, 207 + dy)
Noble.Text.draw("Go (" .. stock .. "x)", 333, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
else
self.aKey:draw(315, 207 + dy) self.aKey:draw(315, 207 + dy)
Noble.Text.draw("Assemble", 333, 210, Noble.Text.ALIGN_LEFT, false, fontMed) Noble.Text.draw("Assemble", 333, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
end end
self.bKey:draw(15, 207 + dy) -- B button: back or buy stock
Noble.Text.draw("Back", 33, 210, Noble.Text.ALIGN_LEFT, false, fontMed) if not drone.locked and drone.mode == Modes.bomber then
self.bKey:draw(15, 207 + dy)
Noble.Text.draw("Buy +1: $" .. (drone.stockPrice or 50), 33, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
else
self.bKey:draw(15, 207 + dy)
Noble.Text.draw("Back", 33, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
end
-- Not enough money warning
if scene.noMoneyTimer then
if scene.noMoneyTimer.value < 800 then
Noble.Text.draw("Not enough $!", 200, 190, Noble.Text.ALIGN_CENTER, false, fontMed)
else
scene.noMoneyTimer = nil
end
end
-- Purchase animation
if scene.purchaseText and scene.purchaseTimer then
local offset = scene.purchaseTimer.value
local alpha = 1.0 - (offset / 40)
if alpha > 0 then
Noble.Text.draw(scene.purchaseText, 200, 20 - offset, Noble.Text.ALIGN_CENTER, false, fontMed)
else
scene.purchaseText = nil
scene.purchaseTimer = nil
end
end
scene.paginator:moveTo(200, 207) scene.paginator:moveTo(200, 207)
end end

View File

@@ -4,19 +4,6 @@ local scene = Game
local font = Graphics.font.new('assets/fonts/Mini Sans 2X') local font = Graphics.font.new('assets/fonts/Mini Sans 2X')
local function screenShake(shakeTime, shakeMagnitude)
local shakeTimer = playdate.timer.new(shakeTime, shakeMagnitude, 0)
shakeTimer.updateCallback = function(timer)
local magnitude = math.floor(timer.value)
local shakeX = math.random(-magnitude, magnitude)
local shakeY = math.random(-magnitude, magnitude)
playdate.display.setOffset(shakeX, shakeY)
end
shakeTimer.timerEndedCallback = function()
playdate.display.setOffset(0, 0)
end
end
function scene:drawBackground() function scene:drawBackground()
local speed = 0.1 local speed = 0.1
if scene.ground ~= nil then if scene.ground ~= nil then
@@ -34,7 +21,8 @@ function scene:drawBackground()
end end
function scene:setValues() function scene:setValues()
self.bg = Graphics.image.new("assets/sprites/bg1") local bgPaths = { "assets/sprites/backgrounds/fpv", "assets/sprites/backgrounds/bomber" }
self.bg = Graphics.image.new(bgPaths[CurrentMission.mapId])
scene.bgX = 0 scene.bgX = 0
scene.telemLostSound = playdate.sound.fileplayer.new("assets/audio/telemko") scene.telemLostSound = playdate.sound.fileplayer.new("assets/audio/telemko")
scene.telemLostSoundPlayed = false scene.telemLostSoundPlayed = false
@@ -66,8 +54,6 @@ end
function scene:start() function scene:start()
scene.super.start(self) scene.super.start(self)
playdate.ui.crankIndicator:draw() -- not sure why this is not working
self.optionsMenu:addMenuItem("Main Menu", function() Noble.transition(Menu) end) self.optionsMenu:addMenuItem("Main Menu", function() Noble.transition(Menu) end)
Noble.showFPS = false Noble.showFPS = false
end end
@@ -84,17 +70,17 @@ end
function scene:enter() function scene:enter()
scene.super.enter(self) scene.super.enter(self)
scene:setValues()
scene.player = Player(150, 100) scene.player = Player(150, 100)
scene.ground = Ground(0, 225, scene.player) scene.ground = Ground(0, 225, scene.player)
scene.balebaSpawner.timerEndedCallback = function() scene.balebaSpawner.timerEndedCallback = function()
scene:spawnBaleba() -- scene:spawnBaleba()
end end
for i = 1, 3 do for i = 1, 3 do
scene:spawnBaleba() -- scene:spawnBaleba()
end end
scene.helloAudio:play(1) scene.helloAudio:play(1)
@@ -103,14 +89,13 @@ function scene:enter()
end end
end end
function round(number)
local formatted = string.format("%.2f", number)
return formatted
end
function scene:update() function scene:update()
scene.super.update(self) scene.super.update(self)
if playdate.isCrankDocked() then
playdate.ui.crankIndicator:draw()
end
if scene.player == nil then if scene.player == nil then
return return
end end
@@ -151,8 +136,14 @@ function scene:update()
if scene.player.targetDone then if scene.player.targetDone then
message = "You did it!" message = "You did it!"
end end
c = notify(message, function() if scene.player.targetDone then
Noble.transition(Menu) local reward = 100
Noble.GameData.set("missionsCompleted", Noble.GameData.get("missionsCompleted") + 1)
Noble.GameData.set("money", Noble.GameData.get("money") + reward)
message = "You did it! +$" .. reward
end
local c = notify(message, function()
Noble.transition(DroneCardSelector)
c:remove() c:remove()
end) end)
c:moveTo(200, 120) c:moveTo(200, 120)
@@ -169,6 +160,7 @@ function scene:exit()
if scene.tank ~= nil then if scene.tank ~= nil then
scene.tank:remove() scene.tank:remove()
end end
scene.helloAudio:stop()
scene.telemLostSound:stop() scene.telemLostSound:stop()
scene.levelAudio:stop() scene.levelAudio:stop()
scene.balebaSpawner:remove() scene.balebaSpawner:remove()

View File

@@ -34,6 +34,17 @@ end
function scene:enter() function scene:enter()
scene.super.enter(self) scene.super.enter(self)
-- Update locked state from missionsCompleted
local completed = Noble.GameData.get("missionsCompleted")
for i = 1, #Maps do
Maps[i].locked = (completed < Maps[i].unlockMissions)
end
scene.menuIndex = 1
scene.currentX = 0
scene.targetX = 0
scene.cards = {} scene.cards = {}
for i = 1, #Maps do for i = 1, #Maps do
scene.cards[i] = MapCard(0, 0, Maps[i]) scene.cards[i] = MapCard(0, 0, Maps[i])
@@ -42,6 +53,7 @@ end
function scene:update() function scene:update()
scene.super.update(self) scene.super.update(self)
if not scene.cards then return end
elapsedTime = elapsedTime + 1 / playdate.display.getRefreshRate() elapsedTime = elapsedTime + 1 / playdate.display.getRefreshRate()
local dy = 2 * math.sin(20 * elapsedTime) local dy = 2 * math.sin(20 * elapsedTime)
@@ -60,15 +72,18 @@ function scene:update()
end end
-- Bottom background -- Bottom background
if Maps[scene.menuIndex].locked == false then local map = Maps[scene.menuIndex]
if map.locked == false then
self.aKey:draw(315, 207 + dy) self.aKey:draw(315, 207 + dy)
Noble.Text.draw("Select", 333, 210, Noble.Text.ALIGN_LEFT, false, fontMed) Noble.Text.draw("Select", 333, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
else
Noble.Text.draw(map.unlockMissions .. " missions to unlock", 200, 195, Noble.Text.ALIGN_CENTER, false, fontMed)
end end
self.bKey:draw(15, 207 + dy) self.bKey:draw(15, 207 + dy)
Noble.Text.draw("Back", 33, 210, Noble.Text.ALIGN_LEFT, false, fontMed) Noble.Text.draw("Back", 33, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
Noble.Text.draw(string.upper(Maps[scene.menuIndex].name), 200, 210, Noble.Text.ALIGN_CENTER, false, fontBig) Noble.Text.draw(string.upper(map.name), 200, 210, Noble.Text.ALIGN_CENTER, false, fontBig)
end end
@@ -95,6 +110,7 @@ scene.inputHandler = {
if Maps[scene.menuIndex].locked then if Maps[scene.menuIndex].locked then
return return
end end
CurrentMission.mapId = Maps[scene.menuIndex].id
scene.menuConfirmSound:play(1) scene.menuConfirmSound:play(1)
Noble.transition(DroneCardSelector) Noble.transition(DroneCardSelector)
end, end,

View File

@@ -110,7 +110,7 @@ end
function scene:exit() function scene:exit()
scene.super.exit(self) scene.super.exit(self)
-- scene.levelAudio:stop() scene.levelAudio:stop()
self.sequence = Sequence.new():from(self.menuY):to(self.menuYTo, 0.5, Ease.inSine) self.sequence = Sequence.new():from(self.menuY):to(self.menuYTo, 0.5, Ease.inSine)
self.sequence:start() self.sequence:start()
end end
@@ -126,6 +126,9 @@ function scene:setupMenu(__menu)
end end
return return
end) end)
__menu:addItem("Credits", function() return end) __menu:addItem("Credits", function()
Noble.GameData.resetAll()
print("GameData reset!")
end)
__menu:select("Start") __menu:select("Start")
end end

View File

@@ -7,10 +7,15 @@ local font = Graphics.font.new('assets/fonts/Mini Sans 2X')
function scene:init() function scene:init()
scene.super.init(self) scene.super.init(self)
self.bg = Graphics.image.new("assets/sprites/bg2") self.bg = Graphics.image.new("assets/sprites/backgrounds/bomber")
self.bgY = 0 self.bgY = 0
self.scrollSpeed = 0.6 self.scrollSpeed = 0.6
scene.dropSound = playdate.sound.fileplayer.new("assets/audio/drop1")
scene.themeSound = playdate.sound.fileplayer.new("assets/audio/bomberTheme")
scene.themeSound:setVolume(0.5)
scene.themeSound:play()
scene.progressBar = ProgressBar(50, 210, 50, 5) scene.progressBar = ProgressBar(50, 210, 50, 5)
scene.progressBar:set(0) scene.progressBar:set(0)
scene.progressBar:setVisible(false) scene.progressBar:setVisible(false)
@@ -19,19 +24,63 @@ function scene:init()
scene.grenadeCooldownTimer = nil scene.grenadeCooldownTimer = nil
scene.grenadeCooldownDuration = 100 scene.grenadeCooldownDuration = 100
scene.progressBarMax = 100 scene.progressBarMax = 100
scene.autoReload = false
scene.reloadProgress = 0
scene.crankSensitivity = 0.2
scene.availableGrenades = 8 scene.availableGrenades = 8
scene.killCount = 0
scene.killTarget = Maps[CurrentMission.mapId].killTarget or 10
scene.missionEnded = false
scene.enemies = {}
scene.enemySpawnTimer = nil
scene.enemySpawnInterval = 1000
scene.maxEnemies = 5
scene.nextEnemyIndex = 1
scene.minSpawnDelay = 500
scene.maxSpawnDelay = 3500
scene.enemySpeedBonus = 0
scene.enemySpeedMax = 1.5
scene.enemySpeedRamp = 0.0005
scene.crateTimer = 0
scene.crateInterval = math.random(400, 800)
scene.allyBulletTimer = 0
scene.allyBulletInterval = math.random(200, 500)
-- Drone battery (in frames, ~60 seconds at 50fps)
scene.battery = 3000
scene.batteryMax = 3000
-- Falling state
scene.falling = false
-- Combo tracking
scene.comboCount = 0
scene.comboText = nil
scene.comboTextTimer = nil
BomberScene.instance = self BomberScene.instance = self
end end
function scene:drawBackground() function scene:drawBackground()
if scene.missionEnded and scene.falling then
Graphics.clear(Graphics.kColorBlack)
return
end
self.bgY = self.bgY + self.scrollSpeed self.bgY = self.bgY + self.scrollSpeed
if self.bgY >= 720 then if self.bgY >= 720 then
self.bgY = 0 self.bgY = 0
end end
self.bg:draw(0, self.bgY - 720) self.bg:draw(0, self.bgY - 720)
self.bg:draw(0, self.bgY) self.bg:draw(0, self.bgY)
end end
@@ -55,7 +104,6 @@ scene.inputHandler = {
return return
end end
print("AButtonDown")
if not scene.grenadeCooldown then if not scene.grenadeCooldown then
Granade(scene.crosshair.x, scene.crosshair.y) Granade(scene.crosshair.x, scene.crosshair.y)
scene.grenadeCooldown = true scene.grenadeCooldown = true
@@ -63,15 +111,21 @@ scene.inputHandler = {
scene.progressBar:set(0) scene.progressBar:set(0)
scene.progressBar:setVisible(true) scene.progressBar:setVisible(true)
scene.availableGrenades = scene.availableGrenades - 1 scene.availableGrenades = scene.availableGrenades - 1
scene.dropSound:play()
scene.grenadeCooldownTimer = playdate.timer.new(scene.grenadeCooldownDuration, function() if scene.autoReload then
scene.grenadeCooldown = false scene.grenadeCooldownTimer = playdate.timer.new(scene.grenadeCooldownDuration, function()
scene.progressBar:setVisible(false) scene.grenadeCooldown = false
end) scene.progressBar:setVisible(false)
end)
scene.grenadeCooldownTimer.updateCallback = function(timer)
local percentage = (scene.grenadeCooldownDuration - timer.timeLeft) / scene.grenadeCooldownDuration * scene.progressBarMax scene.grenadeCooldownTimer.updateCallback = function(timer)
scene.progressBar:set(percentage) local percentage = (scene.grenadeCooldownDuration - timer.timeLeft) / scene.grenadeCooldownDuration * scene.progressBarMax
scene.progressBar:set(percentage)
end
else
scene.reloadProgress = 0
end end
end end
end end
@@ -81,33 +135,269 @@ function scene:enter()
scene.super.enter(self) scene.super.enter(self)
Noble.Input.setHandler(scene.inputHandler) Noble.Input.setHandler(scene.inputHandler)
scene.crosshair = MovableCrosshair(100, 100) scene.crosshair = MovableCrosshair(100, 100)
scene:scheduleNextEnemySpawn()
NoiseAnimation(200, 120)
end end
function scene:start() function scene:start()
scene.super.start(self) scene.super.start(self)
self.optionsMenu:addMenuItem("Main Menu", function() Noble.transition(Menu) end) self.optionsMenu:addMenuItem("Main Menu", function() Noble.transition(Menu) end)
Noble.showFPS = true Noble.showFPS = false
end
function scene:hasActiveGrenades()
local sprites = playdate.graphics.sprite.getAllSprites()
for i = 1, #sprites do
if sprites[i]:getTag() == Tags.granade then
return true
end
end
return false
end end
function scene:update() function scene:update()
scene.super.update(self) scene.super.update(self)
Noble.Text.draw(scene.availableGrenades .. "x", 10, 210, Noble.Text.ALIGN_LEFT, false, font)
if scene.availableGrenades <= 0 then if scene.missionEnded then return end
Noble.Text.draw("No grenades left", 200, 110, Noble.Text.ALIGN_CENTER, false, font)
scene.crosshair:setVisible(false) local killsBefore = scene.killCount
-- Ramp up enemy speed over time
if scene.enemySpeedBonus < scene.enemySpeedMax then
scene.enemySpeedBonus = scene.enemySpeedBonus + scene.enemySpeedRamp
end end
if playdate.isCrankDocked() then -- Drone battery
scene.battery = scene.battery - 1
if scene.battery <= 0 and not scene.falling then
scene.falling = true
scene.fallTimer = 0
scene.fallDuration = 120
-- Stop spawning new enemies
if scene.enemySpawnTimer then
scene.enemySpawnTimer:remove()
end
end
if scene.falling then
scene.fallTimer = scene.fallTimer + 1
if scene.fallTimer == 1 then
scene.crosshair:setVisible(false)
scene.fallSnapshot = playdate.graphics.getDisplayImage():copy()
-- Remove all gameplay sprites during fall
local allSprites = playdate.graphics.sprite.getAllSprites()
for i = 1, #allSprites do
allSprites[i]:remove()
end
end
local t = scene.fallTimer / scene.fallDuration
local scale = 1 + t * t * 5
local w = math.floor(400 * scale)
local h = math.floor(240 * scale)
local x = math.floor((400 - w) / 2)
local y = math.floor((240 - h) / 2)
Graphics.clear(Graphics.kColorBlack)
scene.fallSnapshot:drawScaled(x, y, scale)
if scene.fallTimer >= scene.fallDuration and not scene.missionEnded then
scene.missionEnded = true
scene.fallSnapshot = nil
-- Remove all gameplay sprites
local allSprites = playdate.graphics.sprite.getAllSprites()
for i = 1, #allSprites do
allSprites[i]:remove()
end
-- Crash effects
BigBoom()
screenShake(1500, 8)
local random = math.random(1, 4)
local crashSound = playdate.sound.fileplayer.new("assets/audio/boom" .. random)
crashSound:setVolume(0.8)
crashSound:play(1)
playdate.timer.performAfterDelay(2000, function()
local c
c = notify("Battery dead!", function()
Noble.transition(DroneCardSelector)
c:remove()
end)
c:moveTo(200, 120)
c:add()
end)
end
return
end
-- Spawn ammo crates
scene.crateTimer = scene.crateTimer + 1
if scene.crateTimer >= scene.crateInterval then
scene.crateTimer = 0
scene.crateInterval = math.random(400, 800)
AmmoCrate(math.random(30, 370), -20)
end
-- Ally bullets (steal kills)
scene.allyBulletTimer = scene.allyBulletTimer + 1
if scene.allyBulletTimer >= scene.allyBulletInterval then
scene.allyBulletTimer = 0
scene.allyBulletInterval = math.random(200, 500)
-- Find a random alive enemy to target
local alive = {}
for i = 1, #scene.enemies do
if scene.enemies[i] and not scene.enemies[i].removed and not scene.enemies[i].isDying then
alive[#alive + 1] = scene.enemies[i]
end
end
if #alive > 0 then
local target = alive[math.random(1, #alive)]
AllyBullet(target)
end
end
-- Victory check
if scene.killCount >= scene.killTarget then
scene.missionEnded = true
scene.crosshair:setVisible(false)
local reward = 100
Noble.GameData.set("missionsCompleted", Noble.GameData.get("missionsCompleted") + 1)
Noble.GameData.set("money", Noble.GameData.get("money") + reward)
local c
c = notify("Mission Complete! +$" .. reward, function()
Noble.transition(DroneCardSelector)
c:remove()
end)
c:moveTo(200, 120)
c:add()
return
end
-- Defeat check: no grenades left and no active grenades on screen
if scene.availableGrenades <= 0 and not scene.grenadeCooldown and not scene:hasActiveGrenades() then
scene.missionEnded = true
scene.crosshair:setVisible(false)
local c
c = notify("Mission Failed!", function()
Noble.transition(DroneCardSelector)
c:remove()
end)
c:moveTo(200, 120)
c:add()
return
end
if scene.grenadeCooldown and not scene.autoReload and not playdate.isCrankDocked() then
local change = playdate.getCrankChange()
if change > 0 or change < 0 then
scene.reloadProgress = scene.reloadProgress + (change * scene.crankSensitivity)
if scene.reloadProgress > scene.progressBarMax then
scene.reloadProgress = scene.progressBarMax
scene.grenadeCooldown = false
scene.progressBar:setVisible(false)
end
scene.progressBar:set(scene.reloadProgress)
end
end
-- Combo detection
local frameKills = scene.killCount - killsBefore
if frameKills >= 2 then
scene.comboText = "x" .. frameKills .. " COMBO!"
scene.comboTextTimer = playdate.timer.new(1500, 0, 1500, playdate.easingFunctions.linear)
scene.availableGrenades = scene.availableGrenades + (frameKills - 1)
end
-- HUD: kill count
Noble.Text.draw(scene.killCount .. "/" .. scene.killTarget, 350, 10, Noble.Text.ALIGN_RIGHT, false, font)
-- HUD: battery bar
local batW = 40
local batH = 6
local batX = 180
local batY = 10
local batFill = (scene.battery / scene.batteryMax) * batW
Graphics.drawRect(batX, batY, batW, batH)
Graphics.fillRect(batX, batY, batFill, batH)
-- HUD: combo text
if scene.comboText and scene.comboTextTimer then
if scene.comboTextTimer.value < 1500 then
Noble.Text.draw(scene.comboText, 200, 100, Noble.Text.ALIGN_CENTER, false, font)
else
scene.comboText = nil
scene.comboTextTimer = nil
end
end
Noble.Text.draw(scene.availableGrenades .. "x", 10, 210, Noble.Text.ALIGN_LEFT, false, font)
if scene.availableGrenades <= 0 and not scene:hasActiveGrenades() then
Noble.Text.draw("No grenades left", 200, 110, Noble.Text.ALIGN_CENTER, false, font)
scene.crosshair:setVisible(false)
scene.progressBar:setVisible(false)
elseif scene.availableGrenades <= 0 then
-- grenades still flying, wait
elseif playdate.isCrankDocked() then
Noble.Text.draw("Crank it to reload!", 200, 110, Noble.Text.ALIGN_CENTER, false, font) Noble.Text.draw("Crank it to reload!", 200, 110, Noble.Text.ALIGN_CENTER, false, font)
playdate.ui.crankIndicator:draw() playdate.ui.crankIndicator:draw()
end end
end end
-- TODO: to reset grenades spin crank function scene:spawnEnemies()
-- TODO: random spawn of enemies local activeEnemies = 0
for i = 1, #scene.enemies do
if scene.enemies[i] and not scene.enemies[i].removed then
activeEnemies = activeEnemies + 1
end
end
if activeEnemies < self.maxEnemies then
local isScout = math.random() < 0.1
scene.enemies[scene.nextEnemyIndex] = Enemy(math.random(30, 370), -20, isScout)
scene.nextEnemyIndex = scene.nextEnemyIndex + 1
end
scene:scheduleNextEnemySpawn()
end
function scene:scheduleNextEnemySpawn()
local delay = math.random(scene.minSpawnDelay, scene.maxSpawnDelay)
scene.enemySpawnTimer = playdate.timer.new(delay, function()
scene:spawnEnemies()
end)
end
function scene:finish()
scene.themeSound:stop()
scene.enemySpawnTimer:remove()
-- Remove ALL sprites to prevent leaking into next scene
local allSprites = playdate.graphics.sprite.getAllSprites()
for i = 1, #allSprites do
allSprites[i]:remove()
end
scene.enemies = {}
scene.progressBar = nil
if scene.grenadeCooldownTimer then
scene.grenadeCooldownTimer:remove()
end
scene.grenadeCooldownTimer = nil
scene.crosshair = nil
BomberScene.instance = nil
NoiseAnimation.isJamming = false
end
-- TODO: random spawn some decorations -- TODO: random spawn some decorations
-- TODO: add some music
-- TODO: add some sound effects
-- TODO: add clouds or smoke -- TODO: add clouds or smoke
-- TODO: random disactivate granades -- TODO: random disactivate granades

View File

@@ -0,0 +1,68 @@
AllyBullet = {}
class('AllyBullet').extends(playdate.graphics.sprite)
local killPhrases = { "stolen!", "mine!", "gotcha", "sorry :)", "too slow", "ez" }
function AllyBullet:init(targetEnemy)
AllyBullet.super.init(self)
self.target = targetEnemy
self.speed = 3
self.removed = false
self:setSize(4, 8)
self:setCenter(0.5, 0.5)
self:setZIndex(ZIndex.fx)
self:moveTo(targetEnemy.x + math.random(-30, 30), 250)
self:add()
self:markDirty()
end
function AllyBullet:update()
if self.removed then return end
-- Fly upward toward target
local dx = 0
local dy = -self.speed
if self.target and not self.target.removed and not self.target.isDying then
dx = (self.target.x - self.x) * 0.05
end
self:moveBy(dx, dy)
-- Rotate to match flight vector
local angle = math.deg(math.atan(dy, dx)) + 90
self:setRotation(angle)
-- Check if reached target
if self.target and not self.target.removed and not self.target.isDying then
local dist = math.abs(self.y - self.target.y) + math.abs(self.x - self.target.x)
if dist < 20 then
-- Kill enemy without counting toward player score
self.target:setImage(self.target.deadImage)
self.target.isDying = true
self.target.vx = math.random(-2, 2)
self.target.vy = math.random(-1, 1)
self.target:setRotation(math.random() * 360)
-- Show "stolen" text
local phrase = killPhrases[math.random(1, #killPhrases)]
FloatingText.spawnCustom(self.target.x, self.target.y, phrase)
self.removed = true
self:remove()
return
end
end
-- Off screen
if self.y < -10 then
self.removed = true
self:remove()
end
self:markDirty()
end
function AllyBullet:draw()
playdate.graphics.fillRect(0, 0, 4, 8)
end

View File

@@ -0,0 +1,71 @@
AmmoCrate = {}
class('AmmoCrate').extends(playdate.graphics.sprite)
function AmmoCrate:init(x, y)
AmmoCrate.super.init(self)
self.crateSize = 20
self:setSize(self.crateSize, self.crateSize)
self:setCenter(0.5, 0.5)
self:setZIndex(ZIndex.props)
self:setGroups(CollideGroups.props)
self:setCollidesWithGroups({ CollideGroups.granade })
self:setCollideRect(0, 0, self.crateSize, self.crateSize)
self:setTag(Tags.ammoCrate)
self.removed = false
self.bonusGrenades = 3
self:moveTo(x, y)
self:add()
self:markDirty()
end
function AmmoCrate:update()
if self.removed then return end
if not BomberScene.instance then return end
self:moveBy(0, BomberScene.instance.scrollSpeed)
local _, _, collisions, count = self:checkCollisions(self.x, self.y)
if count > 0 then
for i, collision in ipairs(collisions) do
if collision.other:getTag() == Tags.granade and collision.other.currentRadius <= 0.05 then
self:pickup()
return
end
end
end
if self.y > 260 then
self.removed = true
self:remove()
end
end
function AmmoCrate:pickup()
self.removed = true
BomberScene.availableGrenades = BomberScene.availableGrenades + self.bonusGrenades
local particle = ParticlePoly(self.x, self.y)
particle:setThickness(1)
particle:setSize(1, 2)
particle:setSpeed(1, 5)
particle:setColour(Graphics.kColorXOR)
particle:add(8)
self:remove()
end
function AmmoCrate:draw()
local s = self.crateSize
-- Box outline
playdate.graphics.drawRect(0, 0, s, s)
-- Cross pattern
playdate.graphics.drawLine(0, 0, s, s)
playdate.graphics.drawLine(s, 0, 0, s)
-- Inner + symbol
local mid = s / 2
playdate.graphics.drawLine(mid - 3, mid, mid + 3, mid)
playdate.graphics.drawLine(mid, mid - 3, mid, mid + 3)
end

View File

@@ -0,0 +1,110 @@
Enemy = {}
class('Enemy').extends(NobleSprite)
function Enemy:init(x, y, isScout)
Enemy.super.init(self)
self:moveTo(x, y)
self:setZIndex(4)
self:add(x,y)
self.markImage = Graphics.image.new("assets/sprites/bomber/enemy_alive_"..math.random(1,2))
self.deadImage = Graphics.image.new("assets/sprites/bomber/enemy_dead")
self.hitSound = playdate.sound.fileplayer.new("assets/audio/hit1")
self:setImage(self.markImage)
self.removed = false
self:setGroups(CollideGroups.enemy)
self:setCollidesWithGroups({
CollideGroups.granade,
CollideGroups.enemy
})
self:setCollideRect(-6, -6, 46, 46)
self:setSize(32, 32)
self.vx = 0
self.vy = 0
self.isDying = false
self.friction = 0.95
self.isScout = isScout or false
if self.isScout then
self.baseSpeed = math.random(8, 14) / 10
self.zigzagTime = math.random() * 100
self.zigzagAmplitude = math.random(8, 15) / 10
self.zigzagFrequency = math.random(4, 8) / 100
else
self.baseSpeed = math.random(2, 8) / 10
end
end
function Enemy:update()
if not BomberScene.instance then return end
local speed = 0
if self.isDying then
self.vx = self.vx * self.friction
self.vy = self.vy * self.friction
self:moveBy(self.vx, self.vy + BomberScene.instance.scrollSpeed)
if math.abs(self.vx) < 0.1 and math.abs(self.vy) < 0.1 then
self.isDying = false
self.removed = true
end
elseif not self.removed then
speed = self.baseSpeed + (BomberScene.enemySpeedBonus or 0)
local dx = 0
if self.isScout then
self.zigzagTime = self.zigzagTime + self.zigzagFrequency
dx = math.sin(self.zigzagTime) * self.zigzagAmplitude
end
self:moveBy(dx, BomberScene.instance.scrollSpeed + speed)
else
self:moveBy(0, BomberScene.instance.scrollSpeed)
end
local actualX, actualY, collisions, numberOfCollisions = self:checkCollisions(self.x, self.y)
if numberOfCollisions > 0 then
for i, collision in ipairs(collisions) do
if collision.other:getTag() == Tags.granade and collision.other.currentRadius <= 0.05 and not self.isDying then
self:setImage(self.deadImage)
self.hitSound:play()
self:applyExplosionForce(collision.other.x, collision.other.y)
end
end
end
if self.y > SCREEN_H + 10 then
if not self.removed then
self:remove()
self:superRemove()
self.removed = true
end
end
end
function Enemy:applyExplosionForce(explosionX, explosionY)
local dx = self.x - explosionX
local dy = self.y - explosionY
local dist = math.sqrt(dx*dx + dy*dy)
if dist == 0 then dist = 0.001 end
dx = dx / dist
dy = dy / dist
local maxForce = 5
local maxRadius = 100
local force = maxForce * (1 - math.min(dist, maxRadius) / maxRadius)
force = math.max(force, 1)
self.vx = dx * force
self.vy = dy * force * 0.5
self.isDying = true
BomberScene.killCount = BomberScene.killCount + 1
FloatingText(self.x, self.y)
self:setRotation(math.random() * 360)
end

View File

@@ -4,7 +4,7 @@ class('ExplosionMark').extends(NobleSprite)
function ExplosionMark:init(x, y) function ExplosionMark:init(x, y)
ExplosionMark.super.init(self) ExplosionMark.super.init(self)
self.id = math.random(1, 2) self.id = math.random(1, 2)
self.markImage = Graphics.image.new("assets/sprites/boomSplash" .. self.id) -- TODO: make it random self.markImage = Graphics.image.new("assets/sprites/bomber/boom_splash_" .. self.id)
self:setImage(self.markImage) self:setImage(self.markImage)
self:moveTo(x, y) self:moveTo(x, y)
self:setZIndex(5) self:setZIndex(5)
@@ -12,6 +12,7 @@ function ExplosionMark:init(x, y)
end end
function ExplosionMark:update() function ExplosionMark:update()
if not BomberScene.instance then return end
self:moveBy(0, BomberScene.instance.scrollSpeed) self:moveBy(0, BomberScene.instance.scrollSpeed)
if self.y > 240 + 32 then if self.y > 240 + 32 then

View File

@@ -0,0 +1,59 @@
FloatingText = {}
class('FloatingText').extends(playdate.graphics.sprite)
local floatFont = Graphics.font.new('assets/fonts/Mini Sans 2X')
local phrases = { "-1", "nice", "200", "dead", "done", "nice shot", "boom", "rip", "lol", "ez" }
function FloatingText.spawnCustom(x, y, text)
local ft = FloatingText(x, y)
ft.text = text
local w = floatFont:getTextWidth(text) + 4
ft:setSize(w, 16)
ft:markDirty()
return ft
end
function FloatingText:init(x, y)
FloatingText.super.init(self)
self.text = phrases[math.random(1, #phrases)]
self.life = 0
self.maxLife = 60
self.driftX = math.random(-20, 20) / 10
self.driftY = -math.random(10, 20) / 10
local w = floatFont:getTextWidth(self.text) + 4
self:setSize(w, 16)
self:setCenter(0.5, 0.5)
self:setZIndex(ZIndex.ui + 1)
self:moveTo(x, y)
self:add()
self:markDirty()
end
function FloatingText:update()
self.life = self.life + 1
if self.life >= self.maxLife then
self:remove()
return
end
self:moveBy(self.driftX, self.driftY)
self.driftY = self.driftY + 0.03
self:markDirty()
end
function FloatingText:draw()
local t = 1 - (self.life / self.maxLife)
if t > 0.5 then
floatFont:drawText(self.text, 2, 0)
else
local dither = playdate.graphics.image.kDitherTypeBayer4x4
local img = Graphics.image.new(self.width, self.height)
Graphics.pushContext(img)
floatFont:drawText(self.text, 2, 0)
Graphics.popContext()
img:drawFaded(0, 0, t * 2, dither)
end
end

View File

@@ -1,19 +1,6 @@
Granade = {} Granade = {}
class('Granade').extends(NobleSprite) class('Granade').extends(NobleSprite)
local function screenShake(shakeTime, shakeMagnitude)
local shakeTimer = playdate.timer.new(shakeTime, shakeMagnitude, 0)
shakeTimer.updateCallback = function(timer)
local magnitude = math.floor(timer.value)
local shakeX = math.random(-magnitude, magnitude)
local shakeY = math.random(-magnitude, magnitude)
playdate.display.setOffset(shakeX, shakeY)
end
shakeTimer.timerEndedCallback = function()
playdate.display.setOffset(0, 0)
end
end
function Granade:init(x, y) function Granade:init(x, y)
Granade.super.init(self) Granade.super.init(self)
@@ -21,12 +8,12 @@ function Granade:init(x, y)
self.currentRadius = self.initialRadius self.currentRadius = self.initialRadius
self.shrinkRate = 0.2 self.shrinkRate = 0.2
random = math.random(1, 4) local random = math.random(1, 4)
self.boomSound = playdate.sound.fileplayer.new("assets/audio/boom" .. random) self.boomSound = playdate.sound.fileplayer.new("assets/audio/boom" .. random)
self.boomSound:setVolume(0.5) self.boomSound:setVolume(0.5)
self.isActive = true self.isActive = true
-- Variables for random movement
self.randomMovementTimer = 0 self.randomMovementTimer = 0
self.randomXVelocity = 0 self.randomXVelocity = 0
self.randomYVelocity = 0 self.randomYVelocity = 0
@@ -35,10 +22,15 @@ function Granade:init(x, y)
self.spriteSize = size self.spriteSize = size
self:setSize(size, size) self:setSize(size, size)
self:moveTo(x, y) self:moveTo(x, y)
self:setZIndex(10)
self:setTag(Tags.granade)
self:setCenter(0.5, 0.5) self:setCenter(0.5, 0.5)
self:setGroups(CollideGroups.granade)
print("Granade init") self:setCollidesWithGroups({
print(self.x, self.y) CollideGroups.enemy
})
self:setCollideRect(0, 0, self:getSize())
self:add(x, y) self:add(x, y)
self:markDirty() self:markDirty()
end end
@@ -62,7 +54,6 @@ function Granade:update()
self.currentRadius = self.currentRadius - self.shrinkRate self.currentRadius = self.currentRadius - self.shrinkRate
if self.currentRadius <= 0 then if self.currentRadius <= 0 then
print("Granade deactivated")
self.isActive = false self.isActive = false
local particleB = ParticlePoly(self.x, self.y) local particleB = ParticlePoly(self.x, self.y)
particleB:setThickness(1) particleB:setThickness(1)
@@ -77,7 +68,8 @@ function Granade:update()
screenShake(1000, 5) screenShake(1000, 5)
SmallBoom() SmallBoom()
ExplosionMark(self.x, self.y) ExplosionMark(self.x, self.y)
SmokeCloud(self.x, self.y)
self:remove() self:remove()
end end
end end

View File

@@ -4,37 +4,44 @@ class('MovableCrosshair').extends(playdate.graphics.sprite)
function MovableCrosshair:init() function MovableCrosshair:init()
MovableCrosshair.super.init(self) MovableCrosshair.super.init(self)
-- Parameters for crosshair
self.lineLength = 10 self.lineLength = 10
self.gapSize = 3 self.gapSize = 3
-- Parameters for movement
self.baseX = 200 self.baseX = 200
self.baseY = 150 self.baseY = 150
self.moveRadius = 2 self.moveRadius = 2
self.moveSpeed = 2 self.moveSpeed = 2.3
self.time = 0 self.time = 0
-- Calculate size based on crosshair dimensions
local totalSize = (self.lineLength + self.gapSize) * 2 + 10 local totalSize = (self.lineLength + self.gapSize) * 2 + 10
self:setSize(totalSize, totalSize) self:setSize(totalSize, totalSize)
-- Set the drawing offset to middle of sprite
self.drawOffsetX = totalSize / 2 self.drawOffsetX = totalSize / 2
self.drawOffsetY = totalSize / 2 self.drawOffsetY = totalSize / 2
self:add(self.baseX, self.baseY) self:add(self.baseX, self.baseY)
self:setCenter(0.5, 0.5) self:setCenter(0.5, 0.5)
self:markDirty() self:markDirty()
self:setZIndex(11)
end end
function MovableCrosshair:update() function MovableCrosshair:update()
MovableCrosshair.super.update(self) MovableCrosshair.super.update(self)
self.time = self.time + playdate.display.getRefreshRate() / 1000 self.time = self.time + playdate.display.getRefreshRate() / 1000
local offsetX = math.sin(self.time) * self.moveRadius local radius = self.moveRadius
local offsetY = math.cos(self.time * 1.3) * self.moveRadius if NoiseAnimation.isJamming then
radius = 8
end
local offsetX = math.sin(self.time) * radius
local offsetY = math.cos(self.time * 1.3) * radius
if NoiseAnimation.isJamming then
offsetX = offsetX + math.random(-3, 3)
offsetY = offsetY + math.random(-3, 3)
end
self:moveTo(self.baseX + offsetX, self.baseY + offsetY) self:moveTo(self.baseX + offsetX, self.baseY + offsetY)
self:markDirty() self:markDirty()
end end
@@ -66,24 +73,24 @@ end
function MovableCrosshair:moveUp() function MovableCrosshair:moveUp()
if self.baseY > 5 then if self.baseY > 5 then
self.baseY = self.baseY - 1 self.baseY = self.baseY - self.moveSpeed
end end
end end
function MovableCrosshair:moveDown() function MovableCrosshair:moveDown()
if self.baseY < 235 then if self.baseY < 235 then
self.baseY = self.baseY + 1 self.baseY = self.baseY + self.moveSpeed
end end
end end
function MovableCrosshair:moveLeft() function MovableCrosshair:moveLeft()
if self.baseX > 5 then if self.baseX > 5 then
self.baseX = self.baseX - 1 self.baseX = self.baseX - self.moveSpeed
end end
end end
function MovableCrosshair:moveRight() function MovableCrosshair:moveRight()
if self.baseX < 395 then if self.baseX < 395 then
self.baseX = self.baseX + 1 self.baseX = self.baseX + self.moveSpeed
end end
end end

View File

@@ -0,0 +1,55 @@
NoiseAnimation = {}
class('NoiseAnimation').extends(NobleSprite)
-- Global EW (РЕБ) state accessible by crosshair
NoiseAnimation.isJamming = false
function NoiseAnimation:init(x, y)
NoiseAnimation.super.init(self, "assets/sprites/noise", true)
self.animation:addState("run", 2, 11)
self.animation:addState("idle", 1, 1)
self.animation.run.frameDuration = 2.5
self.animation:setState("idle")
self:setZIndex(ZIndex.foreground)
self:setSize(400, 240)
self:add()
self:moveTo(x, y)
self.state = "idle"
self.timer = 0
-- РЕБ timing: long idle periods, short jam bursts
self.minIdleDuration = 300
self.maxIdleDuration = 600
self.minJamDuration = 40
self.maxJamDuration = 120
self.nextSwitch = math.random(self.minIdleDuration, self.maxIdleDuration)
end
function NoiseAnimation:update()
self.timer = self.timer + 1
if self.timer >= self.nextSwitch then
self.timer = 0
if self.state == "idle" then
self.state = "jamming"
self.animation:setState("run")
self.nextSwitch = math.random(self.minJamDuration, self.maxJamDuration)
NoiseAnimation.isJamming = true
else
self.state = "idle"
self.animation:setState("idle")
self.nextSwitch = math.random(self.minIdleDuration, self.maxIdleDuration)
NoiseAnimation.isJamming = false
playdate.display.setOffset(0, 0)
end
end
-- Micro screen shake during jamming
if self.state == "jamming" then
local sx = math.random(-1, 1)
local sy = math.random(-1, 1)
playdate.display.setOffset(sx, sy)
end
end

View File

@@ -0,0 +1,54 @@
SmokeCloud = {}
class('SmokeCloud').extends(playdate.graphics.sprite)
function SmokeCloud:init(x, y)
SmokeCloud.super.init(self)
self.radius = 25
self.maxLife = 150
self.life = self.maxLife
local size = self.radius * 2 + 4
self:setSize(size, size)
self:setCenter(0.5, 0.5)
self:setZIndex(ZIndex.fx - 1)
self:moveTo(x, y)
self:add()
self:markDirty()
end
function SmokeCloud:update()
if not BomberScene.instance then
self:remove()
return
end
self:moveBy(0, BomberScene.instance.scrollSpeed)
self.life = self.life - 1
if self.life <= 0 or self.y > 280 then
self:remove()
return
end
self:markDirty()
end
function SmokeCloud:draw()
local t = self.life / self.maxLife
local r = self.radius * t
local cx = self.width / 2
local cy = self.height / 2
local dither = playdate.graphics.image.kDitherTypeBayer4x4
if t < 0.3 then
dither = playdate.graphics.image.kDitherTypeBayer8x8
elseif t < 0.6 then
dither = playdate.graphics.image.kDitherTypeBayer4x4
end
playdate.graphics.setColor(playdate.graphics.kColorBlack)
playdate.graphics.setDitherPattern(1 - t * 0.6, dither)
playdate.graphics.fillCircleAtPoint(cx, cy, r)
playdate.graphics.setColor(playdate.graphics.kColorBlack)
end

View File

@@ -2,7 +2,7 @@ Ground = {}
class("Ground").extends(NobleSprite) class("Ground").extends(NobleSprite)
function Ground:init(x, y, player) function Ground:init(x, y, player)
Ground.super.init(self, "assets/sprites/groundFin") Ground.super.init(self, "assets/sprites/ground_2")
-- Collision properties -- Collision properties
self:setZIndex(ZIndex.ground) self:setZIndex(ZIndex.ground)

View File

@@ -2,7 +2,7 @@ PageSprite = {}
class('PageSprite').extends(NobleSprite) class('PageSprite').extends(NobleSprite)
function PageSprite:init(x, y) function PageSprite:init(x, y)
Baleba.super.init(self, "assets/sprites/pages", true) PageSprite.super.init(self, "assets/sprites/pages", true)
self.animation:addState("1", 1, 1) self.animation:addState("1", 1, 1)
self.animation:addState("2", 2, 2) self.animation:addState("2", 2, 2)
self.animation:addState("3", 3, 3) self.animation:addState("3", 3, 3)

View File

@@ -30,7 +30,7 @@ function Player:init(x, y)
CollideGroups.wall CollideGroups.wall
}) })
self:setCollideRect(3, 19, 60, 33) self:setCollideRect(3, 19, 60, 33)
self:setTag(1) self:setTag(Tags.player)
-- Physics properties -- Physics properties
self.fallSpeed = 0.05 self.fallSpeed = 0.05
@@ -188,16 +188,13 @@ function Player:handleMovementAndCollisions()
end end
end end
if collisionTag == 3 then -- Ground if collisionTag == Tags.ground then
self:boom() self:boom()
return return
elseif collisionTag == 154 then -- Baleba elseif collisionTag == Tags.granade then
-- if self.debug then TODO: why debug always true?
-- return
-- end
self:boom(collisionObject) self:boom(collisionObject)
return return
elseif collisionTag == 2 then -- Tank elseif collisionTag == Tags.tank then
self:boom() self:boom()
BigBoom() BigBoom()

View File

@@ -3,21 +3,14 @@ Tank = {}
class("Tank").extends(Graphics.sprite) class("Tank").extends(Graphics.sprite)
function Tank:init(x, y, ground) function Tank:init(x, y, ground)
self.tankImage = Graphics.image.new("assets/sprites/tank") local target = Targets[CurrentMission.targetIndex]
self.tankImageD = Graphics.image.new("assets/sprites/tankD") self.tankImage = Graphics.image.new(target.sprite)
self.tankImageD = Graphics.image.new(target.spriteD)
Tank.super.init(self) Tank.super.init(self)
local width, height = self.tankImage:getSize()
self.faded_image = Graphics.image.new(width, height, Graphics.kColorClear)
Graphics.pushContext(self.faded_image)
self.tankImageD:drawBlurred(0, 0, 2, 2, Graphics.image.kDitherTypeFloydSteinberg)
Graphics.popContext()
-- Collision properties -- Collision properties
self:setZIndex(ZIndex.enemy) self:setZIndex(ZIndex.enemy)
self:setTag(2) self:setTag(Tags.tank)
self:setCollideRect(4, 56, 147, 65) self:setCollideRect(4, 56, 147, 65)
self:setGroups(CollideGroups.enemy) self:setGroups(CollideGroups.enemy)
self:setCollidesWithGroups( self:setCollidesWithGroups(
@@ -38,7 +31,7 @@ function Tank:fadein()
end end
function Tank:fadeout() function Tank:fadeout()
self:setImage(self.faded_image) self:setImage(self.tankImageD)
end end
function Tank:update() function Tank:update()