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"]
path = source/libraries/noble
url = https://github.com/NobleRobot/NobleEngine.git

View File

@@ -1,6 +1,6 @@
#!/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
if [[ -z $NOCOLOR && -n $(command -v tput) ]]; then
@@ -103,7 +103,7 @@ function make_build_dir() {
function clean_build_dir() {
if [[ -d "${BUILD_DIR}" ]]; then
log "Cleaning build directory..."
rm -rfv "${BUILD_DIR}/*"
rm -rfv "${BUILD_DIR}"/*
chk_err
fi
}

View File

@@ -1,6 +1,6 @@
#!/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
if [[ -z $NOCOLOR && -n $(command -v tput) ]]; then
@@ -103,7 +103,7 @@ function make_build_dir() {
function clean_build_dir() {
if [[ -d "${BUILD_DIR}" ]]; then
log "Cleaning build directory..."
rm -rfv "${BUILD_DIR}/*"
rm -rfv "${BUILD_DIR}"/*
chk_err
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,
alert = 12,
ground = 100,
flash = 101
flash = 101,
foreground = 102
}
CollideGroups = {
player = 1,
enemy = 2,
props = 3,
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 = {
{
id = 1,
name = "Vovchansk",
description = "This is a map",
locked = false,
unlockMissions = 0,
killTarget = 10,
},
{
id = 2,
name = "Mariupol",
description = "This is a map",
locked = false,
locked = true,
unlockMissions = 3,
killTarget = 15,
}
}
@@ -69,9 +86,10 @@ Drones = {
{
id = 2,
mode = Modes.bomber,
name = "Drone 2",
name = "Bomber",
description = "This is a drone",
price = 200,
stockPrice = 50,
locked = false,
preview = nil,
full = nil
@@ -80,7 +98,7 @@ Drones = {
id = 3,
name = "Drone 3",
description = "This is a drone",
price = 300,
price = -1,
locked = true,
preview = nil,
full = nil
@@ -89,7 +107,7 @@ Drones = {
id = 4,
name = "Drone 4",
description = "This is a drone",
price = 400,
price = -1,
locked = true,
preview = nil,
full = nil
@@ -111,7 +129,12 @@ import "scripts/MapCard"
import "scripts/bomber/movableCrosshair"
import "scripts/bomber/granade"
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/Assemble'
import 'scenes/DroneCardSelector'
@@ -147,16 +170,55 @@ Noble.Settings.setup({
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({
drone1 = 0,
drone2 = 0,
drone3 = 0,
drone4 = 0,
money = 150
money = 500,
bomberStock = 3,
missionsCompleted = 0,
})
playdate.display.setRefreshRate(50)
Noble.showFPS = false
Noble.new(BomberScene)
--Noble.new(BomberScene)
Noble.new(Menu)

View File

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

View File

@@ -13,7 +13,7 @@ function scene:popCode(button)
end
scene.menuConfirmSound:stop()
if scene.tickTimer.paused then
scene.droneParts = scene:loadDrone(1, #scene.code)
scene.droneParts = scene:loadDrone(CurrentMission.droneId, #scene.code)
scene.tickTimer:start()
scene.progressBar:setVisible(true)
end
@@ -146,24 +146,12 @@ function scene:enter()
scene.buttonTimeout = 100
Noble.Input.setHandler(scene.inputHandler)
local text =
[[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.]]
self.dialogue = pdDialogueBox(text, 390, 46)
CurrentMission.targetIndex = math.random(1, #Targets)
local target = Targets[CurrentMission.targetIndex]
self.dialogue = pdDialogueBox(target.briefing, 390, 46)
-- self.dialogue:setPadding(4)
end
function round(number)
local formatted = string.format("%.2f", number)
return formatted
end
local elapsedTime = 0
function scene:update()
scene.super.update(self)

View File

@@ -6,22 +6,59 @@ local elapsedTime = 0
scene.inputHandler = {
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
end
CurrentMission.droneId = drone.id
scene.menuConfirmSound:play(1)
mode = Drones[scene.menuIndex].mode
local mode = drone.mode
local soundTable = playdate.sound.playingSources()
for i=1, #soundTable do
soundTable[i]:stop()
end
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)
else
Noble.transition(Assemble)
end
end,
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)
Noble.transition(MapSelector)
end,
@@ -62,6 +99,10 @@ function scene:setValues()
scene.currentX = 0
scene.targetX = 0
scene.purchaseText = nil
scene.purchaseTimer = nil
scene.noMoneyTimer = nil
end
function scene:init()
@@ -78,6 +119,16 @@ end
function scene:enter()
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 = {}
for i = 1, #Drones do
scene.cards[i] = DroneCard(0, 0, Drones[i])
@@ -108,13 +159,54 @@ function scene:update()
scene.cards[i]:moveTo(x + scene.currentX, 25)
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)
Noble.Text.draw("Assemble", 333, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
end
self.bKey:draw(15, 207 + dy)
Noble.Text.draw("Back", 33, 210, Noble.Text.ALIGN_LEFT, false, fontMed)
-- B button: back or buy stock
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)
end

View File

@@ -4,19 +4,6 @@ local scene = Game
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()
local speed = 0.1
if scene.ground ~= nil then
@@ -34,7 +21,8 @@ function scene:drawBackground()
end
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.telemLostSound = playdate.sound.fileplayer.new("assets/audio/telemko")
scene.telemLostSoundPlayed = false
@@ -66,8 +54,6 @@ end
function scene:start()
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)
Noble.showFPS = false
end
@@ -84,17 +70,17 @@ end
function scene:enter()
scene.super.enter(self)
scene:setValues()
scene.player = Player(150, 100)
scene.ground = Ground(0, 225, scene.player)
scene.balebaSpawner.timerEndedCallback = function()
scene:spawnBaleba()
-- scene:spawnBaleba()
end
for i = 1, 3 do
scene:spawnBaleba()
-- scene:spawnBaleba()
end
scene.helloAudio:play(1)
@@ -103,14 +89,13 @@ function scene:enter()
end
end
function round(number)
local formatted = string.format("%.2f", number)
return formatted
end
function scene:update()
scene.super.update(self)
if playdate.isCrankDocked() then
playdate.ui.crankIndicator:draw()
end
if scene.player == nil then
return
end
@@ -151,8 +136,14 @@ function scene:update()
if scene.player.targetDone then
message = "You did it!"
end
c = notify(message, function()
Noble.transition(Menu)
if scene.player.targetDone then
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()
end)
c:moveTo(200, 120)
@@ -169,6 +160,7 @@ function scene:exit()
if scene.tank ~= nil then
scene.tank:remove()
end
scene.helloAudio:stop()
scene.telemLostSound:stop()
scene.levelAudio:stop()
scene.balebaSpawner:remove()

View File

@@ -34,6 +34,17 @@ end
function scene:enter()
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 = {}
for i = 1, #Maps do
scene.cards[i] = MapCard(0, 0, Maps[i])
@@ -42,6 +53,7 @@ end
function scene:update()
scene.super.update(self)
if not scene.cards then return end
elapsedTime = elapsedTime + 1 / playdate.display.getRefreshRate()
local dy = 2 * math.sin(20 * elapsedTime)
@@ -60,15 +72,18 @@ function scene:update()
end
-- 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)
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
self.bKey:draw(15, 207 + dy)
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
@@ -95,6 +110,7 @@ scene.inputHandler = {
if Maps[scene.menuIndex].locked then
return
end
CurrentMission.mapId = Maps[scene.menuIndex].id
scene.menuConfirmSound:play(1)
Noble.transition(DroneCardSelector)
end,

View File

@@ -110,7 +110,7 @@ end
function scene:exit()
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:start()
end
@@ -126,6 +126,9 @@ function scene:setupMenu(__menu)
end
return
end)
__menu:addItem("Credits", function() return end)
__menu:addItem("Credits", function()
Noble.GameData.resetAll()
print("GameData reset!")
end)
__menu:select("Start")
end

View File

@@ -7,10 +7,15 @@ local font = Graphics.font.new('assets/fonts/Mini Sans 2X')
function scene:init()
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.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:set(0)
scene.progressBar:setVisible(false)
@@ -20,12 +25,56 @@ function scene:init()
scene.grenadeCooldownDuration = 100
scene.progressBarMax = 100
scene.autoReload = false
scene.reloadProgress = 0
scene.crankSensitivity = 0.2
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
end
function scene:drawBackground()
if scene.missionEnded and scene.falling then
Graphics.clear(Graphics.kColorBlack)
return
end
self.bgY = self.bgY + self.scrollSpeed
if self.bgY >= 720 then
@@ -55,7 +104,6 @@ scene.inputHandler = {
return
end
print("AButtonDown")
if not scene.grenadeCooldown then
Granade(scene.crosshair.x, scene.crosshair.y)
scene.grenadeCooldown = true
@@ -64,14 +112,20 @@ scene.inputHandler = {
scene.progressBar:setVisible(true)
scene.availableGrenades = scene.availableGrenades - 1
scene.grenadeCooldownTimer = playdate.timer.new(scene.grenadeCooldownDuration, function()
scene.grenadeCooldown = false
scene.progressBar:setVisible(false)
end)
scene.dropSound:play()
scene.grenadeCooldownTimer.updateCallback = function(timer)
local percentage = (scene.grenadeCooldownDuration - timer.timeLeft) / scene.grenadeCooldownDuration * scene.progressBarMax
scene.progressBar:set(percentage)
if scene.autoReload then
scene.grenadeCooldownTimer = playdate.timer.new(scene.grenadeCooldownDuration, function()
scene.grenadeCooldown = false
scene.progressBar:setVisible(false)
end)
scene.grenadeCooldownTimer.updateCallback = function(timer)
local percentage = (scene.grenadeCooldownDuration - timer.timeLeft) / scene.grenadeCooldownDuration * scene.progressBarMax
scene.progressBar:set(percentage)
end
else
scene.reloadProgress = 0
end
end
end
@@ -81,33 +135,269 @@ function scene:enter()
scene.super.enter(self)
Noble.Input.setHandler(scene.inputHandler)
scene.crosshair = MovableCrosshair(100, 100)
scene:scheduleNextEnemySpawn()
NoiseAnimation(200, 120)
end
function scene:start()
scene.super.start(self)
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
function scene:update()
scene.super.update(self)
Noble.Text.draw(scene.availableGrenades .. "x", 10, 210, Noble.Text.ALIGN_LEFT, false, font)
if scene.availableGrenades <= 0 then
Noble.Text.draw("No grenades left", 200, 110, Noble.Text.ALIGN_CENTER, false, font)
scene.crosshair:setVisible(false)
if scene.missionEnded then return end
local killsBefore = scene.killCount
-- Ramp up enemy speed over time
if scene.enemySpeedBonus < scene.enemySpeedMax then
scene.enemySpeedBonus = scene.enemySpeedBonus + scene.enemySpeedRamp
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)
playdate.ui.crankIndicator:draw()
end
end
-- TODO: to reset grenades spin crank
-- TODO: random spawn of enemies
function scene:spawnEnemies()
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: add some music
-- TODO: add some sound effects
-- TODO: add clouds or smoke
-- 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)
ExplosionMark.super.init(self)
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:moveTo(x, y)
self:setZIndex(5)
@@ -12,6 +12,7 @@ function ExplosionMark:init(x, y)
end
function ExplosionMark:update()
if not BomberScene.instance then return end
self:moveBy(0, BomberScene.instance.scrollSpeed)
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 = {}
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)
Granade.super.init(self)
@@ -21,12 +8,12 @@ function Granade:init(x, y)
self.currentRadius = self.initialRadius
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:setVolume(0.5)
self.isActive = true
-- Variables for random movement
self.randomMovementTimer = 0
self.randomXVelocity = 0
self.randomYVelocity = 0
@@ -35,10 +22,15 @@ function Granade:init(x, y)
self.spriteSize = size
self:setSize(size, size)
self:moveTo(x, y)
self:setZIndex(10)
self:setTag(Tags.granade)
self:setCenter(0.5, 0.5)
self:setGroups(CollideGroups.granade)
self:setCollidesWithGroups({
CollideGroups.enemy
})
self:setCollideRect(0, 0, self:getSize())
print("Granade init")
print(self.x, self.y)
self:add(x, y)
self:markDirty()
end
@@ -62,7 +54,6 @@ function Granade:update()
self.currentRadius = self.currentRadius - self.shrinkRate
if self.currentRadius <= 0 then
print("Granade deactivated")
self.isActive = false
local particleB = ParticlePoly(self.x, self.y)
particleB:setThickness(1)
@@ -77,6 +68,7 @@ function Granade:update()
screenShake(1000, 5)
SmallBoom()
ExplosionMark(self.x, self.y)
SmokeCloud(self.x, self.y)
self:remove()
end

View File

@@ -4,36 +4,43 @@ class('MovableCrosshair').extends(playdate.graphics.sprite)
function MovableCrosshair:init()
MovableCrosshair.super.init(self)
-- Parameters for crosshair
self.lineLength = 10
self.gapSize = 3
-- Parameters for movement
self.baseX = 200
self.baseY = 150
self.moveRadius = 2
self.moveSpeed = 2
self.moveSpeed = 2.3
self.time = 0
-- Calculate size based on crosshair dimensions
local totalSize = (self.lineLength + self.gapSize) * 2 + 10
self:setSize(totalSize, totalSize)
-- Set the drawing offset to middle of sprite
self.drawOffsetX = totalSize / 2
self.drawOffsetY = totalSize / 2
self:add(self.baseX, self.baseY)
self:setCenter(0.5, 0.5)
self:markDirty()
self:setZIndex(11)
end
function MovableCrosshair:update()
MovableCrosshair.super.update(self)
self.time = self.time + playdate.display.getRefreshRate() / 1000
local offsetX = math.sin(self.time) * self.moveRadius
local offsetY = math.cos(self.time * 1.3) * self.moveRadius
local radius = 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:markDirty()
@@ -66,24 +73,24 @@ end
function MovableCrosshair:moveUp()
if self.baseY > 5 then
self.baseY = self.baseY - 1
self.baseY = self.baseY - self.moveSpeed
end
end
function MovableCrosshair:moveDown()
if self.baseY < 235 then
self.baseY = self.baseY + 1
self.baseY = self.baseY + self.moveSpeed
end
end
function MovableCrosshair:moveLeft()
if self.baseX > 5 then
self.baseX = self.baseX - 1
self.baseX = self.baseX - self.moveSpeed
end
end
function MovableCrosshair:moveRight()
if self.baseX < 395 then
self.baseX = self.baseX + 1
self.baseX = self.baseX + self.moveSpeed
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)
function Ground:init(x, y, player)
Ground.super.init(self, "assets/sprites/groundFin")
Ground.super.init(self, "assets/sprites/ground_2")
-- Collision properties
self:setZIndex(ZIndex.ground)

View File

@@ -2,7 +2,7 @@ PageSprite = {}
class('PageSprite').extends(NobleSprite)
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("2", 2, 2)
self.animation:addState("3", 3, 3)

View File

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

View File

@@ -3,21 +3,14 @@ Tank = {}
class("Tank").extends(Graphics.sprite)
function Tank:init(x, y, ground)
self.tankImage = Graphics.image.new("assets/sprites/tank")
self.tankImageD = Graphics.image.new("assets/sprites/tankD")
local target = Targets[CurrentMission.targetIndex]
self.tankImage = Graphics.image.new(target.sprite)
self.tankImageD = Graphics.image.new(target.spriteD)
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
self:setZIndex(ZIndex.enemy)
self:setTag(2)
self:setTag(Tags.tank)
self:setCollideRect(4, 56, 147, 65)
self:setGroups(CollideGroups.enemy)
self:setCollidesWithGroups(
@@ -38,7 +31,7 @@ function Tank:fadein()
end
function Tank:fadeout()
self:setImage(self.faded_image)
self:setImage(self.tankImageD)
end
function Tank:update()