13 Commits

Author SHA1 Message Date
b4053b96eb fix: update Noble submodule — restore correct draw order
The sprite.update() must run before scene:update() so that
manual UI draws (buttons, text, menus) render on top of sprites.
2026-02-24 13:03:00 +01:00
9c867405e9 switch Noble Engine to our fork with 5 community bugfixes
Fork: assada/NobleEngine
Merged PRs:
- #63: fix background not drawing on scene transitions
- #65: mark NobleSprite dirty before drawing (alwaysRedraw=false)
- #86: fix animation frame duration issue
- #90: fix broken filter utility method
- #92: transition optimization (68% faster on hardware)
2026-02-24 12:56:07 +01:00
e86e59cd69 add README with build instructions and MIT license 2026-02-24 12:51:12 +01:00
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
fae2abf94e bomber imp 2025-04-12 13:32:36 +02:00
648e4a3dc4 bomber 2025-04-11 15:15:19 +02:00
95b2c825db map select 2025-04-11 01:25:01 +02:00
19fe3e3d13 fix: ass code l 2024-06-14 21:06:39 +03:00
a4d2684724 fix: ass code l 2024-06-14 20:50:55 +03:00
1349e92491 feat: add dialogue 2024-06-14 20:33:09 +03:00
60002df6e1 fix: player can select unavailable drone 2024-06-12 19:07:08 +03:00
92 changed files with 2679 additions and 140 deletions

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
*~

5
.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
url = https://github.com/assada/NobleEngine.git

View File

@@ -28,7 +28,8 @@
],
"runtime.version": "Lua 5.4",
"workspace.library": [
"/home/ut3usw/src/playdate-luacats",
"/home/ut3usw/Projects/VSCode-PlaydateTemplate/source/libraries"
"/Users/oleksiiilienko/projects/playdate-luacats",
"/Users/oleksiiilienko/Documents/fpv2/source/libraries",
"/Users/oleksiiilienko/Documents/fpv2/source"
]
}

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "playdate",
"request": "launch",
"name": "Playdate: Debug",
"preLaunchTask": "${defaultBuildTask}"
}
]
}

View File

@@ -5,4 +5,8 @@
"Lua.runtime.nonstandardSymbol": ["+=", "-=", "*=", "/=", "//=", "%=", "<<=", ">>=", "&=", "|=", "^="],
"Lua.workspace.library": ["$PLAYDATE_SDK_PATH/CoreLibs"],
"Lua.workspace.preloadFileSize": 1000,
}
"playdate-debug.sdkPath": "/Users/oleksiiilienko/Developer/PlaydateSDK",
"playdate-debug.sourcePath": "/Users/oleksiiilienko/Documents/fpv2/source",
"playdate-debug.outputPath": "/Users/oleksiiilienko/Documents/fpv2/builds",
"playdate-debug.productName": "FPV Game"
}

32
.vscode/tasks.json vendored
View File

@@ -3,6 +3,26 @@
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "pdc",
"problemMatcher": ["$pdc-lua", "$pdc-external"],
"label": "Playdate: Build"
},
{
"type": "playdate-simulator",
"problemMatcher": ["$pdc-external"],
"label": "Playdate: Run"
},
{
"label": "Playdate: Build and Run",
"dependsOn": ["Playdate: Build", "Playdate: Run"],
"dependsOrder": "sequence",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Invoke Build and Run script",
"type": "shell",
@@ -29,6 +49,12 @@
"build"
]
},
"osx": {
"command": "${workspaceFolder}/build_and_run_mac.sh",
"args": [
"build"
]
},
"presentation": {
"showReuseMessage": false,
"reveal": "always",
@@ -62,6 +88,12 @@
"run"
]
},
"osx": {
"command": "${workspaceFolder}/build_and_run_mac.sh",
"args": [
"run"
]
},
"presentation": {
"showReuseMessage": false,
"reveal": "always",

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 ut3usw
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,8 +1,76 @@
# FPV Run
# FPV Game
## TODO:
A Playdate game where you pilot FPV drones in combat missions. Two game modes: precision FPV strikes and top-down bomber runs.
- [ ] Menu audio
- [x] Tags, zOffset from constants
- [ ] Add global game state (?)
- [x] Add inertia to the player
## Game Modes
**FPV Mode** — Fly a first-person-view drone through the battlefield. Navigate obstacles, manage battery, and hit the target before telemetry is lost.
**Bomber Mode** — Command a bomber drone from above. Drop grenades on enemy soldiers, collect ammo crates, and complete kill targets to win.
## Features
- Two distinct drone types with unique gameplay
- Multiple maps with increasing difficulty
- Drone assembly mini-game
- Crank-based grenade reloading in Bomber mode
- Battery management and progressive difficulty
## Building
### Requirements
- [Playdate SDK](https://play.date/dev/)
- macOS or Linux
### Build & Run
```bash
# macOS
export PLAYDATE_SDK_PATH="/path/to/PlaydateSDK" # optional, has default
./build_and_run_mac.sh build
# Linux
export PLAYDATE_SDK_PATH="/path/to/PlaydateSDK"
./build_and_run.sh build
```
To run without rebuilding:
```bash
./build_and_run_mac.sh run
```
## Project Structure
```
source/
├── assets/
│ ├── audio/ # Sound effects and music
│ ├── fonts/ # Bitmap fonts
│ ├── images/ # UI images, cards, maps
│ ├── launcher/ # Playdate launcher assets
│ └── sprites/
│ ├── backgrounds/ # Level backgrounds
│ ├── bomber/ # Bomber mode sprites
│ ├── targets/ # Target vehicles
│ └── ... # Player, effects, UI sprites
├── libraries/ # Noble Engine, AnimatedSprite, etc.
├── scenes/ # Game scenes (Menu, Game, Bomber, etc.)
├── scripts/ # Game objects (Player, Enemy, Tank, etc.)
└── utilities/ # Helpers and UI utils
```
## Built With
- [Playdate SDK](https://play.date/dev/)
- [Noble Engine](https://github.com/NobleRobot/NobleEngine) — Scene management framework
- [AnimatedSprite](https://github.com/Whitebrim/AnimatedSprite) — Sprite animation library
## License
This project is licensed under the MIT License — see [LICENSE](LICENSE) for details.
## Author
**ut3usw** — [dead.guru](https://dead.guru)

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
}

145
build_and_run_mac.sh Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env bash
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
RED=$(tput setaf 1)
CYN=$(tput setaf 6)
YEL=$(tput setaf 3)
RST=$(tput sgr0)
fi
function display_help() {
printf "%s\n\n" "${0} build|run|-h|--help|"
printf "%-16s\n" "build: Builds the project and runs the Simulator"
printf "%-16s\n" "run : Skips building the project and runs the Simulator"
printf "\n"
printf "%s\n\n" "Set NOCOLOR=1 to disable terminal coloring"
exit 0
}
# We don't need fancy flags/operators for two commands
case $1 in
"build")
BUILD=1
;;
"run")
BUILD=0
;;
*)
display_help
exit 1
;;
esac
# Set some paths
BUILD_DIR="./builds"
SOURCE_DIR="./source"
PDX_PATH="${BUILD_DIR}/$(basename $(pwd)).pdx"
# Logging functions
function log() {
printf "%s\n" "${CYN}>> $1${RST}"
}
function log_warn() {
printf "%s\n" "${YEL}>! $1${RST}"
}
function log_err() {
printf "%s\n >> %s\n" "${RED}!! ERROR !!" "$1${RST}"
}
function check_pdxinfo() {
if [[ -f ./source/pdxinfo ]]; then
if grep "com.organization.package" ./source/pdxinfo 2>&1 >/dev/null; then
log_warn "PDXINFO NOTICE:"
log_warn "Don't forget to change your unique project info in 'source/pdxinfo': 'bundleID', 'name', 'author', 'description'."
log_warn "It's critical to change your game bundleID, so there will be no collisions with other games, installed via sideload."
log_warn "Read more about pdxinfo here: https://sdk.play.date/Inside%20Playdate.html#pdxinfo"
fi
fi
}
function chk_err() {
# Check for errors in last process and bail if needed
if [[ $? > 0 ]]; then
log_err "There was an issue with the previous command; exiting!"
exit 1
fi
}
function check_close_sim() {
# Check if we have 'pidof'
PIDOF=$(command -v pidof2)
# Prefer 'pidof'; use ps if not
if [[ -n $PIDOF ]]; then
SIMPID=$($PIDOF "PlaydateSimulator")
if [[ -n $SIMPID ]]; then
log "Found existing Simulator, closing..."
kill -9 $SIMPID
chk_err
fi
else
SIMPID=$(ps aux | grep PlaydateSimulator | grep -v grep | awk '{print $2}')
if [[ -n $SIMPID ]]; then
log "Found existing Simulator, closing..."
kill -9 $SIMPID
chk_err
fi
fi
}
# Create build dir
function make_build_dir() {
if [[ ! -d "${BUILD_DIR}" ]]; then
log "Creating build directory..."
mkdir -p "${BUILD_DIR}"
chk_err
fi
}
# Clean build dir
function clean_build_dir() {
if [[ -d "${BUILD_DIR}" ]]; then
log "Cleaning build directory..."
rm -rfv "${BUILD_DIR}"/*
chk_err
fi
}
# Compile the PDX
function build_pdx() {
if [[ $BUILD == 1 ]]; then
log "Building PDX with 'pdc'..."
$PLAYDATE_SDK_PATH/bin/pdc -sdkpath "${PLAYDATE_SDK_PATH}" "${SOURCE_DIR}" "${PDX_PATH}"
chk_err
fi
}
# Run the PDX with Simulator
function run_pdx() {
if [[ -d "${PDX_PATH}" ]]; then
log "Running PDX with Simulator..."
open -a "$PLAYDATE_SDK_PATH/bin/Playdate Simulator.app" "${PDX_PATH}"
else
log_err "PDX doesn't exist! Please 'build' the project first!"
fi
}
#### MAIN SCRIPT ####
if [[ $BUILD == 1 ]]; then
log "Attempting a build and run of PDX..."
make_build_dir
clean_build_dir
check_pdxinfo
build_pdx
check_close_sim
run_pdx
else
log "Attempting to run PDX: ${PDX_PATH}..."
check_close_sim
run_pdx
fi

BIN
source/.DS_Store vendored Normal file

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.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

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.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

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: 4.6 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

@@ -0,0 +1,47 @@
--metrics={"baseline":7,"xHeight":0,"capHeight":0,"pairs":{},"left":[],"right":[]}
datalen=1204
data=iVBORw0KGgoAAAANSUhEUgAAADgAAAAwCAYAAABE1blzAAAAAXNSR0IArs4c6QAAA0BJREFUaEPdWdtyqzAQa/7/o9uBqVMhpNUaCE17njr22uxVknMeHz//PuHv8eeD1tAG99TZ5eiwSfvjM2w3c176zxcop5XNchnbumQs62y/BDQccj7gGXc++f9Ag+EIZ55thnOpghiY647hOAaLa84/7jC+57mvLmDHuhXExLgKuQ5xgY47UwfYfVcddTFnWVW6qhSPuEsk2rnKsH/Kbr0/9bBrV25TlUG06VQAA+Nx6Z7fdUhnjhzCdVASMy1AegNU6juXoaj6+L9YSxlKVZrpgG7CqnZXc4yAtttXIFMNeYWCCUU7ASYhkVCYfd8oDRzsNkp9E75FMRIEVZCVeEj3t1EUCbSiBeY5ViUIGKxwVJCO6CsebKG0kldKWqUMqjlwEo0DTNzpFJHiUW7xJw9yxlW7pBnrnHEtunMMDF0gSSisd6oKYpvuhpY8vBJFXSW5OxxWSJ7tzEcH/a6yUUL71N3dCqjsJo5UgCWz/L1YfSO1sMKA1b+uFu1oQdUNqSIMXgn0+D70S9IME73KlBK+HZ7kCvLsnN1nlJa0pgJUQ8yc4wJkUKqUkso+82m3AywL/HYFZ75fKS2cwU3H3T2DSUi/bAYtCjUQzp11KOqo6aUoeopr3vlwJ5sO/RwPOrqoKnqY51KHVQFWYlZBNK8pRK1oqAIR9VpRL5CdtDwSIPMiU0hF+GjrzrmnU0qA3GcUrQg8IW63gq4zrli3FUzvPSWReAbVrB7lua4AcX495z1VRVWU17jtWFWoVnSasiMV8f6oqHBeuu8xbAPV4mnfzVLiza5/GwxwIPPO1Dbl21UVrNRMxaPYYmd40vLo7AzuUAp+NkTQ4TmsNOgV6Gln+ijKVXTCLdSduW6gXGk+t9mv4Fh9cKaCTn0ovqxQlYWF+hVguoLpUkcfjKDyZwTxX9qJhyuqcepoXXcz2MnoTIXTTCatiSDGyefgZYvOoGCaKcWDTM4OkNT8qrHg8xFFp7jlLxlXYvsoL6Uq3Jqfq2mCnXf8d1uQCmQcDDvkdK8IBRy3BYbtxwCgUCqhWGf/9uAQidwrWqGVI2qscIWktwbancEOj6mXydvNoKqCe3Gkl4gag1urp8j2dgde/cEvnycmPQiDA1YAAAAASUVORK5CYII=
width=8
height=8
tracking=1
0 8
1 8
2 8
3 8
4 8
5 8
6 8
7 8
8 8
9 8
space 8
<EFBFBD> 8
A 8
B 8
C 8
D 8
E 8
F 8
G 8
H 8
I 8
J 8
K 8
L 8
M 8
N 8
O 8
P 8
Q 8
R 8
S 8
T 8
U 8
V 8
W 8
X 8
Y 8
Z 8

Binary file not shown.

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

@@ -0,0 +1,763 @@
------------------------------------------------
--- Dialogue classes intended to ease the ---
--- implementation of dialogue into playdate ---
--- games. Developed by: ---
--- GammaGames, PizzaFuelDev and NickSr ---
------------------------------------------------
-- You can find examples and docs at https://github.com/PizzaFuel/pdDialogue
local pd = playdate
local gfx = pd.graphics
----------------------------------------------------------------------------
-- #Section: pdDialogue
----------------------------------------------------------------------------
pdDialogue = {}
function pdDialogue.wrap(lines, width, font)
--[[
lines: an array of strings
width: the maximum width of each line (in pixels)
font: the font to use (optional, uses default font if not provided)
]]--
font = font or gfx.getFont()
local result = {}
for _, line in ipairs(lines) do
local currentWidth, currentLine = 0, ""
if line == "" or font:getTextWidth(line) <= width then
table.insert(result, line)
goto continue
end
for word in line:gmatch("%S+") do
local wordWidth = font:getTextWidth(word)
local newLine = currentLine .. (currentLine ~= "" and " " or "") .. word
local newWidth = font:getTextWidth(newLine)
if newWidth >= width then
table.insert(result, currentLine)
currentWidth, currentLine = wordWidth, word
else
currentWidth, currentLine = newWidth, newLine
end
end
if currentWidth ~= 0 then
table.insert(result, currentLine)
end
::continue::
end
return result
end
function pdDialogue.window(text, startIndex, height, font)
--[[
text: an array of strings (pre-wrapped)
startIndex: the row index to start the window
height: the height (in pixels) of the window
font: the font to use (optional, uses default font if not provided)
]]--
font = font or gfx.getFont()
local result = {text[start_index]}
local rows = pdDialogue.getRows(height, font) - 1
for index = 1, rows do
-- Check if index is out of range of the text
if start_index + index > #text then
break
end
table.insert(result, text[i])
end
return table.concat(result, "\n")
end
function pdDialogue.paginate(lines, height, font)
--[[
lines: array of strings (pre-wrapped)
height: height to limit text (in pixels)
font: optional, will get current font if not provided
]]--
local result = {}
local currentLine = {}
font = font or gfx.getFont()
local rows = pdDialogue.getRows(height, font)
for _, line in ipairs(lines) do
if line == "" then
-- If line is empty and currentLine has text...
if #currentLine > 0 then
-- Merge currentLine and add to result
table.insert(result, table.concat(currentLine, "\n"))
currentLine = {}
end
else
-- If over row count...
if #currentLine >= rows then
-- Concat currentLine, add to result, and start new line
table.insert(result, table.concat(currentLine, "\n"))
currentLine = { line }
else
table.insert(currentLine, line)
end
end
end
-- If all lines are complete and currentLine is not empty, add to result
if #currentLine > 0 then
table.insert(result, table.concat(currentLine, "\n"))
currentLine = {}
end
return result
end
function pdDialogue.process(text, width, height, font)
--[[
text: string containing the text to be processed
width: width to limit text (in pixels)
height: height to limit text (in pixels)
font: optional, will get current font if not provided
]]--
local lines = {}
font = font or gfx.getFont()
-- Split newlines in text
for line in text:gmatch("([^\n]*)\n?") do
table.insert(lines, line)
end
-- Wrap the text
local wrapped = pdDialogue.wrap(lines, width, font)
-- Paginate the wrapped text
local paginated = pdDialogue.paginate(wrapped, height, font)
return paginated
end
function pdDialogue.getRows(height, font)
font = font or gfx.getFont()
local lineHeight = font:getHeight() + font:getLeading()
return math.floor(height / lineHeight)
end
function pdDialogue.getRowsf(height, font)
font = font or gfx.getFont()
local lineHeight = font:getHeight() + font:getLeading()
return height / lineHeight
end
----------------------------------------------------------------------------
-- #Section: pdDialogueSprite
----------------------------------------------------------------------------
pdDialogueSprite = {}
class("pdDialogueSprite").extends(gfx.sprite)
function pdDialogueSprite:init(dialogue)
--[[
dialogue: an instance of pdDialogueBox
]]--
pdDialogueSprite.super.init(self)
self.image = gfx.image.new(dialogue.width, dialogue.height)
self:setImage(self.image)
self.dialogue = dialogue
-- Remove sprite when dialogue is closed
local onClose = self.dialogue.onClose
function self.dialogue.onClose()
onClose()
self:remove()
end
end
function pdDialogueSprite:add()
pdDialogueSprite.super.add(self)
if not self.dialogue.enabled then
self.dialogue:enable()
end
end
function pdDialogueSprite:update()
pdDialogueSprite.super.update(self)
-- Redraw dialogue if it has changed (update returns true)
if self.dialogue:update() then
self.image:clear(gfx.kColorClear)
gfx.pushContext(self.image)
self.dialogue:draw(0, 0)
gfx.popContext()
self:markDirty()
end
end
----------------------------------------------------------------------------
-- #Section: pdDialogueBox
----------------------------------------------------------------------------
pdDialogueBox = {}
class("pdDialogueBox").extends()
function pdDialogueBox.buttonPrompt(x, y)
gfx.setImageDrawMode(gfx.kDrawModeCopy)
gfx.getSystemFont():drawText("", x, y)
end
function pdDialogueBox.arrowPrompt(x, y, color)
gfx.setColor(color or gfx.kColorBlack)
gfx.fillTriangle(
x, y,
x + 5, y + 5,
x + 10, y
)
end
function pdDialogueBox:init(text, width, height, font)
--[[
text: optional string of text to process
width: width of dialogue box (in pixels)
height: height of dialogue box (in pixels)
font: font to use for drawing text
]]--
pdDialogueBox.super.init(self)
self.speed = 0.5 -- char per frame
self.padding = 2
self.width = width
self.height = height
self.font = font
self.enabled = false
self.line_complete = false
self.dialogue_complete = false
if text ~= nil then
self:setText(text)
end
end
function pdDialogueBox:asSprite()
return pdDialogueSprite(self)
end
function pdDialogueBox:getInputHandlers()
local _speed = self:getSpeed()
return {
AButtonDown = function()
if self.dialogue_complete then
self:disable()
elseif self.line_complete then
self:nextPage()
else
self:setSpeed(_speed * 2)
end
end,
AButtonUp = function()
self:setSpeed(_speed)
end,
BButtonDown = function()
if self.line_complete then
if self.dialogue_complete then
self:disable()
else
self:nextPage()
self:finishLine()
end
else
self:finishLine()
end
end
}
end
function pdDialogueBox:enable()
self.enabled = true
self:onOpen()
end
function pdDialogueBox:disable()
self.enabled = false
self:onClose()
end
function pdDialogueBox:setText(text)
local font = self.font or gfx.getFont()
if type(font) == "table" then
font = font[gfx.font.kVariantNormal]
end
self.text = text
if text ~= nil then
self.pages = pdDialogue.process(text, self.width - self.padding * 2, self.height - self.padding * 2, font)
end
self:restartDialogue()
end
function pdDialogueBox:getText()
return self.text
end
function pdDialogueBox:setPages(pages)
self.pages = pages
self:restartDialogue()
end
function pdDialogueBox:getPages()
return self.pages
end
function pdDialogueBox:setWidth(width)
self.width = width
if self.text ~= nil then
self:setText(self.text)
end
end
function pdDialogueBox:getWidth()
return self.width
end
function pdDialogueBox:setHeight(height)
self.height = height
if self.text ~= nil then
self:setText(self.text)
end
end
function pdDialogueBox:getHeight()
return self.height
end
function pdDialogueBox:setPadding(padding)
self.padding = padding
-- Set text again because padding affects text wrapping
self:setText(self.text)
end
function pdDialogueBox:getPadding()
return self.padding
end
function pdDialogueBox:setFont(font)
self.font = font
end
function pdDialogueBox:getFont()
return self.font
end
function pdDialogueBox:setNineSlice(nineSlice)
self.nineSlice = nineSlice
end
function pdDialogueBox:getNineSlice()
return self.nineSlice
end
function pdDialogueBox:setSpeed(speed)
self.speed = speed
end
function pdDialogueBox:getSpeed()
return self.speed
end
function pdDialogueBox:restartDialogue()
self.currentPage = 1
self.currentChar = 1
self.line_complete = false
self.dialogue_complete = false
end
function pdDialogueBox:finishDialogue()
self.currentPage = #self.pages
self:finishLine()
end
function pdDialogueBox:restartLine()
self.currentChar = 1
self.line_complete = false
self.dialogue_complete = false
self.dirty = true
end
function pdDialogueBox:finishLine()
self.currentChar = #self.pages[self.currentPage]
self.line_complete = true
self.dialogue_complete = self.currentPage == #self.pages
self.dirty = true
end
function pdDialogueBox:previousPage()
if self.currentPage - 1 >= 1 then
self.currentPage -= 1
self:restartLine()
end
end
function pdDialogueBox:nextPage()
if self.currentPage + 1 <= #self.pages then
self.currentPage += 1
self:restartLine()
self:onNextPage()
end
end
function pdDialogueBox:drawBackground(x, y)
if self.nineSlice ~= nil then
gfx.setImageDrawMode(gfx.kDrawModeCopy)
self.nineSlice:drawInRect(x, y, self.width, self.height)
else
gfx.setColor(gfx.kColorWhite)
gfx.fillRect(x, y, self.width, self.height)
gfx.setColor(gfx.kColorBlack)
gfx.drawRect(x, y, self.width, self.height)
end
end
function pdDialogueBox:drawText(x, y, text)
gfx.setImageDrawMode(gfx.kDrawModeCopy)
if self.font ~= nil then
-- variable will be table if a font family
if type(self.font) == "table" then
-- Draw with font family
gfx.drawText(text, x, y, self.font)
else
-- Draw using font
self.font:drawText(text, x, y)
end
else
gfx.drawText(text, x, y)
end
end
function pdDialogueBox:drawPrompt(x, y)
pdDialogueBox.buttonPrompt(x + self.width - 20, y + self.height - 20)
end
function pdDialogueBox:draw(x, y)
local currentText = self.pages[self.currentPage]
if not self.line_complete then
currentText = currentText:sub(1, math.floor(self.currentChar))
end
self:drawBackground(x, y)
self:drawText(x + self.padding, y + self.padding, currentText)
if self.line_complete then
self:drawPrompt(x, y)
end
end
function pdDialogueBox:onOpen()
-- Overrideable by user
end
function pdDialogueBox:onPageComplete()
-- Overrideable by user
end
function pdDialogueBox:onNextPage()
-- Overrideable by user
end
function pdDialogueBox:onDialogueComplete()
-- Overrideable by user
end
function pdDialogueBox:onClose()
-- Overrideable by user
end
function pdDialogueBox:update()
local dirty = self.dirty
self.dirty = false
if not self.enabled then
return dirty
end
local pageLength = #self.pages[self.currentPage]
if self.currentChar < pageLength then
dirty = true
self.currentChar += self.speed
if self.currentChar > pageLength then
self.currentChar = pageLength
end
end
local previous_line_complete = self.line_complete
local previous_dialogue_complete = self.dialogue_complete
self.line_complete = self.currentChar == pageLength
self.dialogue_complete = self.line_complete and self.currentPage == #self.pages
if previous_line_complete ~= self.line_complete then
self:onPageComplete()
dirty = true
end
if previous_dialogue_complete ~= self.dialogue_complete then
self:onDialogueComplete()
dirty = true
end
return dirty
end
----------------------------------------------------------------------------
-- #Section: pdPortraitDialogueBox
----------------------------------------------------------------------------
pdPortraitDialogueBox = {}
class("pdPortraitDialogueBox").extends(pdDialogueBox)
function pdPortraitDialogueBox:init(name, drawable, text, width, height, font)
self.name = name
self.portrait = drawable
if self.portrait.getSize ~= nil then
self.portrait_width, self.portrait_height = self.portrait:getSize()
elseif self.portrait.getImage ~= nil then
self.portrait_width, self.portrait_height = self.portrait:getImage(1):getSize()
elseif self.portrait.image ~= nil then
if type(self.portrait.image) ~= "function" then
self.portrait_width, self.portrait_height = self.portrait.image:getSize()
else
self.portrait_width, self.portrait_height = self.portrait:image():getSize()
end
end
pdDialogueBox.init(self, text, width - self.portrait_width, height, font)
self:setAlignment(kTextAlignment.left)
end
function pdPortraitDialogueBox:setAlignment(alignment)
self.alignment = alignment
if self.alignment == kTextAlignment.left then
self.portrait_x_position = 0
else
self.portrait_x_position = self.width
end
end
function pdPortraitDialogueBox:getAlignment()
return self.alignment
end
function pdPortraitDialogueBox:draw(x, y)
local offset = self.alignment == kTextAlignment.left and self.portrait_width or 0
pdPortraitDialogueBox.super.draw(self, x + offset, y)
end
function pdPortraitDialogueBox:drawBackground(x, y)
pdPortraitDialogueBox.super.drawBackground(self, x, y)
self:drawPortrait(x + self.portrait_x_position - self.portrait_width, y)
end
function pdPortraitDialogueBox:drawPortrait(x, y)
if self.nineSlice ~= nil then
self.nineSlice:drawInRect(x, y, self.portrait_width, self.portrait_height)
else
gfx.setColor(gfx.kColorWhite)
gfx.fillRect(x, y, self.portrait_width, self.portrait_height)
gfx.setColor(gfx.kColorBlack)
gfx.drawRect(x, y, self.portrait_width, self.height)
end
local font = self.font or gfx.getFont()
self.portrait:draw(x, y)
font:drawTextAligned(
self.name,
x + self.portrait_width / 2,
y + self.height - font:getHeight() - self.padding,
kTextAlignment.center
)
end
----------------------------------------------------------------------------
-- #Section: dialogue box used in pdDialogue
----------------------------------------------------------------------------
pdDialogue.DialogueBox_x, pdDialogue.DialogueBox_y = 5, 186
pdDialogue.DialogueBox = pdDialogueBox(nil, 390, 48)
pdDialogue.DialogueBox_Callbacks = {}
pdDialogue.DialogueBox_Say_Default = nil
pdDialogue.DialogueBox_Say_Nils = nil
pdDialogue.DialogueBox_KeyValueMap = {
width={
set=function(value) pdDialogue.DialogueBox:setWidth(value) end,
get=function() return pdDialogue.DialogueBox:getWidth() end
},
height={
set=function(value) pdDialogue.DialogueBox:setHeight(value) end,
get=function() return pdDialogue.DialogueBox:getHeight() end
},
x={
set=function(value) pdDialogue.DialogueBox_x = value end,
get=function() return pdDialogue.DialogueBox_x end
},
y={
set=function(value) pdDialogue.DialogueBox_y = value end,
get=function() return pdDialogue.DialogueBox_y end
},
padding={
set=function(value) pdDialogue.DialogueBox:setPadding(value) end,
get=function() return pdDialogue.DialogueBox:getPadding() end
},
font={
set=function(value) pdDialogue.DialogueBox:setFont(value) end,
get=function() return pdDialogue.DialogueBox:getFont() end
},
fontFamily={
set=function(value) pdDialogue.DialogueBox.fontFamily = value end,
get=function() return pdDialogue.DialogueBox.fontFamily end
},
nineSlice={
set=function(value) pdDialogue.DialogueBox:setNineSlice(value) end,
get=function() return pdDialogue.DialogueBox:getNineSlice() end
},
speed={
set=function(value) pdDialogue.DialogueBox:setSpeed(value) end,
get=function() return pdDialogue.DialogueBox:getSpeed() end
},
drawBackground={
set=function(func) pdDialogue.DialogueBox_Callbacks["drawBackground"] = func end,
get=function() return pdDialogue.DialogueBox_Callbacks["drawBackground"] end
},
drawText={
set=function(func) pdDialogue.DialogueBox_Callbacks["drawText"] = func end,
get=function() return pdDialogue.DialogueBox_Callbacks["drawText"] end
},
drawPrompt={
set=function(func) pdDialogue.DialogueBox_Callbacks["drawPrompt"] = func end,
get=function() return pdDialogue.DialogueBox_Callbacks["drawPrompt"] end
},
onOpen={
set=function(func) pdDialogue.DialogueBox_Callbacks["onOpen"] = func end,
get=function() return pdDialogue.DialogueBox_Callbacks["onOpen"] end
},
onPageComplete={
set=function(func) pdDialogue.DialogueBox_Callbacks["onPageComplete"] = func end,
get=function() return pdDialogue.DialogueBox_Callbacks["onPageComplete"] end
},
onNextPage={
set=function(func) pdDialogue.DialogueBox_Callbacks["onNextPage"] = func end,
get=function() return pdDialogue.DialogueBox_Callbacks["onNextPage"] end
},
onDialogueComplete={
set=function(func) pdDialogue.DialogueBox_Callbacks["onDialogueComplete"] = func end,
get=function() return pdDialogue.DialogueBox_Callbacks["onDialogueComplete"] end
},
onClose={
set=function(func) pdDialogue.DialogueBox_Callbacks["onClose"] = func end,
get=function() return pdDialogue.DialogueBox_Callbacks["onClose"] end
}
}
function pdDialogue.DialogueBox:drawBackground(x, y)
if pdDialogue.DialogueBox_Callbacks["drawBackground"] ~= nil then
pdDialogue.DialogueBox_Callbacks["drawBackground"](self, x, y)
else
pdDialogue.DialogueBox.super.drawBackground(self, x, y)
end
end
function pdDialogue.DialogueBox:drawText(x, y ,text)
if pdDialogue.DialogueBox_Callbacks["drawText"] ~= nil then
pdDialogue.DialogueBox_Callbacks["drawText"](self, x, y, text)
else
pdDialogue.DialogueBox.super.drawText(self, x, y, text)
end
end
function pdDialogue.DialogueBox:drawPrompt(x, y)
if pdDialogue.DialogueBox_Callbacks["drawPrompt"] ~= nil then
pdDialogue.DialogueBox_Callbacks["drawPrompt"](self, x, y)
else
pdDialogue.DialogueBox.super.drawPrompt(self, x, y)
end
end
function pdDialogue.DialogueBox:onOpen()
pd.inputHandlers.push(self:getInputHandlers(), true)
if pdDialogue.DialogueBox_Callbacks["onOpen"] ~= nil then
pdDialogue.DialogueBox_Callbacks["onOpen"]()
end
end
function pdDialogue.DialogueBox:onPageComplete()
if pdDialogue.DialogueBox_Callbacks["onPageComplete"] ~= nil then
pdDialogue.DialogueBox_Callbacks["onPageComplete"]()
end
end
function pdDialogue.DialogueBox:onNextPage()
if pdDialogue.DialogueBox_Callbacks["onNextPage"] ~= nil then
pdDialogue.DialogueBox_Callbacks["onNextPage"]()
end
end
function pdDialogue.DialogueBox:onDialogueComplete()
if pdDialogue.DialogueBox_Callbacks["onDialogueComplete"] ~= nil then
pdDialogue.DialogueBox_Callbacks["onDialogueComplete"]()
end
end
function pdDialogue.DialogueBox:onClose()
-- Make a backup of the current onClose callback
local current = pdDialogue.DialogueBox_Callbacks["onClose"]
-- This will reset all (including the callbacks)
if pdDialogue.DialogueBox_Say_Default ~= nil then
pdDialogue.setup(pdDialogue.DialogueBox_Say_Default)
pdDialogue.DialogueBox_Say_Default = nil
end
if pdDialogue.DialogueBox_Say_Nils ~= nil then
for _, key in ipairs(pdDialogue.DialogueBox_Say_Nils) do
pdDialogue.set(key, nil)
end
pdDialogue.DialogueBox_Say_Nils = nil
end
pd.inputHandlers.pop()
-- If the current wasn't nil, call it
if current ~= nil then
current()
end
end
----------------------------------------------------------------------------
-- #Section: pdDialogue main user functions
----------------------------------------------------------------------------
function pdDialogue.set(key, value)
if pdDialogue.DialogueBox_KeyValueMap[key] ~= nil then
local backup = pdDialogue.DialogueBox_KeyValueMap[key].get()
pdDialogue.DialogueBox_KeyValueMap[key].set(value)
return backup
end
return nil
end
function pdDialogue.setup(config)
-- config: table of key value pairs. Supported keys are in pdDialogue.DialogueBox_KeyValueMap
local backup = {}
local nils = {}
for key, value in pairs(config) do
local backup_value = pdDialogue.set(key, value)
if backup_value ~= nil then
backup[key] = backup_value
else
table.insert(nils, key)
end
end
return backup, nils
end
function pdDialogue.say(text, config)
--[[
text: string (can be multiline) to say
config: optional table, will provide temporary overrides for this one dialogue box
]]--
if config ~= nil then
pdDialogue.DialogueBox_Say_Default, pdDialogue.DialogueBox_Say_Nils = pdDialogue.setup(config)
end
pdDialogue.DialogueBox:setText(text)
pdDialogue.DialogueBox:enable()
return pdDialogue.DialogueBox
end
function pdDialogue.update()
if pdDialogue.DialogueBox.enabled then
pdDialogue.DialogueBox:update()
pdDialogue.DialogueBox:draw(pdDialogue.DialogueBox_x, pdDialogue.DialogueBox_y)
end
end

View File

@@ -569,7 +569,7 @@ function ParticlePixel:update()
for part = 1, #self.particles, 1 do
local pix = self.particles[part]
playdate.graphics.drawPixel(pix.x,pix.y,pix.size)
playdate.graphics.drawPixel(pix.x,pix.y)
pix.x += math.sin(math.rad(pix.dir)) * pix.speed
pix.y -= math.cos(math.rad(pix.dir)) * pix.speed

View File

@@ -10,6 +10,7 @@ import 'libraries/noble/Noble'
import "libraries/AnimatedSprite"
import "libraries/pdParticles"
import "libraries/playout"
import "libraries/pdDialogue"
import 'utilities/enum'
import 'utilities/ui'
@@ -23,19 +24,57 @@ 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 = true,
unlockMissions = 3,
killTarget = 15,
}
}
Modes = {
fpv = "FPV",
bomber = "Bomber"
}
Drones = {
{
id = 1,
mode = Modes.fpv,
name = "Quad FPV",
description =
"This is a quadrocopter with a camera on it. It's a good drone for beginners. It's easy to control and has a good battery life.",
@@ -46,10 +85,12 @@ Drones = {
},
{
id = 2,
name = "Drone 2",
mode = Modes.bomber,
name = "Bomber",
description = "This is a drone",
price = 200,
locked = true,
stockPrice = 50,
locked = false,
preview = nil,
full = nil
},
@@ -57,7 +98,7 @@ Drones = {
id = 3,
name = "Drone 3",
description = "This is a drone",
price = 300,
price = -1,
locked = true,
preview = nil,
full = nil
@@ -66,7 +107,7 @@ Drones = {
id = 4,
name = "Drone 4",
description = "This is a drone",
price = 400,
price = -1,
locked = true,
preview = nil,
full = nil
@@ -75,6 +116,7 @@ Drones = {
import "scripts/player"
import "scripts/bigBoomSprite"
import "scripts/bomber/boom"
import "scripts/groundSprite"
import "scripts/balebaSprite"
import "scripts/dangerSprite"
@@ -83,12 +125,23 @@ import "scripts/progressBar"
import "scripts/selectionSprite"
import "scripts/DroneCard"
import "scripts/pageSprite"
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'
import 'scenes/Menu'
import 'scenes/Game'
import 'scenes/MapSelector'
import 'scenes/bomber/BomberScene'
Difficulty = {
Easy = "Easy",
@@ -114,19 +167,58 @@ DifficultySettings = {
Noble.Settings.setup({
difficulty = Difficulty.Medium,
music = true,
debug = true
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(Menu)

View File

@@ -2,7 +2,7 @@ name=FPV Game
author=ut3usw
description=This is a FPV Game
bundleID=guru.dead.fpv
version=0.1.1
buildNumber=2
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
@@ -104,7 +104,6 @@ function scene:setValues()
scene.timeToClick = DifficultySettings[scene.difficulty].assebleTime
scene.menuConfirmSound = playdate.sound.fileplayer.new("assets/audio/confirm")
self.aKey = Graphics.image.new("assets/sprites/buttons/A")
scene.musicEnabled = Noble.Settings.get("music")
@@ -133,70 +132,94 @@ end
function scene:start()
scene.super.start(self)
self.optionsMenu:addMenuItem("Main Menu", function() Noble.transition(Menu) end)
Noble.showFPS = false
end
function scene:enter()
scene.super.enter(self)
scene.buttonTimeout = 100
Noble.Input.setHandler(scene.inputHandler)
if scene.musicEnabled then
scene.levelAudio:play(0)
end
end
function round(number)
local formatted = string.format("%.2f", number)
return formatted
function scene:enter()
scene.super.enter(self)
scene.buttonTimeout = 100
Noble.Input.setHandler(scene.inputHandler)
CurrentMission.targetIndex = math.random(1, #Targets)
local target = Targets[CurrentMission.targetIndex]
self.dialogue = pdDialogueBox(target.briefing, 390, 46)
-- self.dialogue:setPadding(4)
end
local elapsedTime = 0
function scene:update()
scene.super.update(self)
elapsedTime = elapsedTime + 1 / playdate.display.getRefreshRate()
local sddy = 4 * math.sin(10 * elapsedTime)
local sdy = 4 * math.sin(7 * elapsedTime)
local sddx = 2 * math.cos(5 * elapsedTime)
elapsedTime = elapsedTime + 1 / playdate.display.getRefreshRate()
local sddy = 0
local sdy = 0
local sddx = 0
if #scene.code == 0 then
if self.dialogue.enabled ~= true then
Noble.Input.setHandler(self.dialogue:getInputHandlers())
function self.dialogue:onClose()
Noble.Transition.setDefaultProperties(Noble.Transition.SpotlightMask, {
x = 325,
y = 95,
xEnd = 96,
yEnd = 100,
invert = false
})
scene.menuConfirmSound:play(1)
Noble.transition(Game, nil, Noble.Transition.SpotlightMask)
end
self.dialogue:enable()
return
else
scene.droneParts[#scene.droneParts]:draw(100, 20)
local dy = 1 * math.sin(10 * elapsedTime)
self.dialogue:update()
self.dialogue:draw(5, 186 + dy)
end
self.progressBar:remove()
self.tickTimer:remove()
end
if #scene.code ~= 0 then -- TODO: this is a hack. no, it's not. it's a SHIT. Why i am do this? What i do with my life?
sddy = 4 * math.sin(10 * elapsedTime)
sdy = 4 * math.sin(7 * elapsedTime)
sddx = 2 * math.cos(5 * elapsedTime)
end
screwDriver:draw(300 + sddx, 100 + sddy)
solder:draw(0, 100 + sdy)
if #scene.code == 0 then
return
end
if scene.tickTimer.paused then
Noble.Text.draw("Assemble the drone!", 200, 110, Noble.Text.ALIGN_CENTER, false, font)
end
if #scene.code == 0 then
local dy = 1 * math.sin(10 * elapsedTime)
Noble.Transition.setDefaultProperties(Noble.Transition.SpotlightMask, {
x = 325,
y = 95,
xEnd = 96,
yEnd = 100,
invert = false
})
self.aKey:draw(200, 170 + dy)
Noble.Text.draw("Start Mission", 218, 175, Noble.Text.ALIGN_LEFT, false, fontMed)
self.progressBar:remove()
self.tickTimer:remove()
return
end
if scene.buttonTimeout <= 0 then
Noble.Text.draw("LOSE!", 200, 110, Noble.Text.ALIGN_CENTER, false, font)
Noble.Text.draw("Fuck!", 200, 110, Noble.Text.ALIGN_CENTER, false, font)
self.progressBar:remove()
self.tickTimer:remove()
screenShake(100, 5)
Noble.Input.setEnabled(false)
playdate.timer.performAfterDelay(2500, function() -- Return to the start after failure
Noble.Input.setEnabled(true)
Noble.transition(DroneCardSelector, nil, Noble.Transition.SpotlightMask);
end)
return
end
screwDriver:draw(300 + sddx, 100 + sddy)
solder:draw(0, 100 + sdy)
if scene.droneParts ~= nil and scene.dronePartIndex ~= nil and scene.dronePartIndex > 0 and #scene.droneParts > 0 then
scene.droneParts[scene.dronePartIndex]:draw(100, 20)
end

View File

@@ -7,26 +7,25 @@ class("BaseScene").extends(NobleScene)
function BaseScene:init()
BaseScene.super.init(self)
pd.resetElapsedTime() -- Reset time so each scene starts at 0
self.optionsMenu = pd.getSystemMenu() -- Store this so we have access to it in all scenes
pd.resetElapsedTime() -- Reset time so each scene starts at 0
self.optionsMenu = pd.getSystemMenu() -- Store this so we have access to it in all scenes
end
function BaseScene:update()
BaseScene.super.update(self)
Particles:update() -- Update our particle library
Particles:update()
end
function BaseScene:exit()
BaseScene.super.exit(self)
self.optionsMenu:removeAllMenuItems() -- Remove all custom menu items and reset menu image
self.optionsMenu:removeAllMenuItems() -- Remove all custom menu items and reset menu image
pd.setMenuImage(nil)
end
function BaseScene:drawBackground()
BaseScene.super.drawBackground(self)
BaseScene.super.drawBackground(self)
if self.background ~= nil then -- Helper so you can set a scene's background to an image
if self.background ~= nil then -- Helper so you can set a scene's background to an image
self.background:draw(0, 0)
end
end
end

View File

@@ -6,10 +6,62 @@ local elapsedTime = 0
scene.inputHandler = {
AButtonDown = function()
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)
Noble.transition(Assemble)
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,
BButtonDown = function() end,
downButtonDown = function()
end,
leftButtonDown = function()
@@ -38,13 +90,19 @@ function scene:setValues()
self.menuIndex = 1
self.aKey = Graphics.image.new("assets/sprites/buttons/A")
self.bKey = Graphics.image.new("assets/sprites/buttons/B")
scene.menuConfirmSound = playdate.sound.fileplayer.new("assets/audio/confirm")
scene.menuBackSound = playdate.sound.fileplayer.new("assets/audio/back")
scene.menuSelSound = playdate.sound.fileplayer.new("assets/audio/menu_select")
scene.menuSelSound:setVolume(0.5)
scene.currentX = 0
scene.targetX = 0
scene.purchaseText = nil
scene.purchaseTimer = nil
scene.noMoneyTimer = nil
end
function scene:init()
@@ -61,12 +119,20 @@ end
function scene:enter()
scene.super.enter(self)
scene.cards = {
DroneCard(0, 0, Drones[1]),
DroneCard(0, 0, Drones[2]),
DroneCard(0, 0, Drones[3]),
DroneCard(0, 0, Drones[4]),
}
-- 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])
end
scene.paginator = PageSprite(200, 207)
end
@@ -88,22 +154,66 @@ function scene:update()
end
local x = 0
for i = 1, 4 do
for i = 1, #scene.cards do
x = 29 + (339 + 16) * (i - 1)
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
-- 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
function scene:exit()
scene.super.exit(self)
for i = 1, 4 do
for i = 1, #scene.cards do
scene.cards[i]:remove()
end
Noble.showFPS = false

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
@@ -129,6 +114,7 @@ function scene:update()
end
if scene.player.isDead() then
scene.ground:setMoveSpeed(0)
if scene.resultShowed ~= true then
Noble.Text.draw("Telemetry Lost", 200, 110, Noble.Text.ALIGN_CENTER, false, font)
end
@@ -150,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)
@@ -168,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

@@ -0,0 +1,139 @@
MapSelector = {}
class("MapSelector").extends(BaseScene)
local scene = MapSelector
local fontMed = Graphics.font.new('assets/fonts/onyx_9')
local fontBig = Graphics.font.new('assets/fonts/opal_12')
local elapsedTime = 0
function scene:init()
playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeXOR)
scene.super.init(self)
scene.menuIndex = 1
self.aKey = Graphics.image.new("assets/sprites/buttons/A")
self.bKey = Graphics.image.new("assets/sprites/buttons/B")
scene.menuConfirmSound = playdate.sound.fileplayer.new("assets/audio/confirm")
scene.menuBackSound = playdate.sound.fileplayer.new("assets/audio/back")
scene.menuSelSound = playdate.sound.fileplayer.new("assets/audio/menu_select")
scene.menuSelSound:setVolume(0.5)
scene.currentX = 0
scene.targetX = 0
end
function scene:start()
scene.super.start(self)
Noble.showFPS = false
self.optionsMenu:addMenuItem("Main Menu", function() Noble.transition(Menu) end)
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])
end
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)
local speed = 40
if math.abs(scene.targetX - scene.currentX) < speed then
scene.currentX = scene.targetX
else
scene.currentX = scene.currentX + speed * ((scene.targetX > scene.currentX) and 1 or -1)
end
local x = 0
for i = 1, #scene.cards do
x = 400 * (i - 1)
scene.cards[i]:moveTo(x + scene.currentX, 0)
end
-- Bottom background
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(map.name), 200, 210, Noble.Text.ALIGN_CENTER, false, fontBig)
end
function scene:exit()
scene.super.exit(self)
for i = 1, #scene.cards do
scene.cards[i]:remove()
end
Noble.showFPS = false
playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeXOR)
end
function scene:finish()
scene.super.finish(self)
for i = 1, #scene.cards do
scene.cards[i]:remove()
end
playdate.display.setScale(1)
end
scene.inputHandler = {
AButtonDown = function()
if Maps[scene.menuIndex].locked then
return
end
CurrentMission.mapId = Maps[scene.menuIndex].id
scene.menuConfirmSound:play(1)
Noble.transition(DroneCardSelector)
end,
BButtonDown = function()
scene.menuBackSound:play(1)
Noble.transition(Menu)
end,
leftButtonDown = function()
if scene.menuIndex <= 1 then
return
end
scene.menuSelSound:play(1)
scene.targetX = scene.targetX + 400
scene.menuIndex = scene.menuIndex - 1
end,
rightButtonDown = function()
if scene.menuIndex >= #Maps then
return
end
scene.menuSelSound:play(1)
scene.targetX = scene.targetX - 400
scene.menuIndex = scene.menuIndex + 1
end,
upButtonDown = function()
end,
}

View File

@@ -24,6 +24,10 @@ function scene:setValues()
end
function scene:init()
local soundTable = playdate.sound.playingSources()
for i=1, #soundTable do
soundTable[i]:stop()
end
scene.super.init(self)
local menuSelSound = playdate.sound.fileplayer.new("assets/audio/menu_select")
@@ -112,7 +116,7 @@ function scene:exit()
end
function scene:setupMenu(__menu)
__menu:addItem("Start", function() Noble.transition(DroneCardSelector, nil, Noble.Transition.DipToWhite) end)
__menu:addItem("Start", function() Noble.transition(MapSelector, nil, Noble.Transition.DipToWhite) end)
__menu:addItem("Tutorial", function()
local debug = Noble.Settings.get("debug")
if debug then
@@ -122,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

@@ -0,0 +1,403 @@
BomberScene = {}
class("BomberScene").extends(BaseScene)
local scene = BomberScene
local font = Graphics.font.new('assets/fonts/Mini Sans 2X')
function scene:init()
scene.super.init(self)
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)
scene.grenadeCooldown = false
scene.grenadeCooldownTimer = nil
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
self.bgY = 0
end
self.bg:draw(0, self.bgY - 720)
self.bg:draw(0, self.bgY)
end
scene.inputHandler = {
upButtonHold = function()
scene.crosshair:moveUp()
end,
downButtonHold = function()
scene.crosshair:moveDown()
end,
leftButtonHold = function()
scene.crosshair:moveLeft()
end,
rightButtonHold = function()
scene.crosshair:moveRight()
end,
AButtonDown = function()
if scene.availableGrenades <= 0 then
return
end
if not scene.grenadeCooldown then
Granade(scene.crosshair.x, scene.crosshair.y)
scene.grenadeCooldown = true
scene.progressBar:set(0)
scene.progressBar:setVisible(true)
scene.availableGrenades = scene.availableGrenades - 1
scene.dropSound:play()
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
}
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 = 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)
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
-- 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
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 clouds or smoke
-- TODO: random disactivate granades

View File

@@ -0,0 +1,11 @@
local pd <const> = playdate
local gfx <const> = Graphics
class('MapCard').extends(gfx.sprite)
function MapCard:init(x, y, map)
self:setImage(gfx.image.new('assets/images/maps/map_' .. map.id .. '.png'))
self:setCenter(0, 0)
self:moveTo(x, y)
self:add()
end

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,28 @@
SmallBoom = {}
class("SmallBoom").extends(AnimatedSprite)
local smallBoomImageTable = Graphics.imagetable.new("assets/sprites/smallboom")
function SmallBoom:init()
SmallBoom.super.init(self, smallBoomImageTable)
-- Animation properties
self:addState("play", 1, 3, { tickStep = 1, loop = 2 })
self:setDefaultState("play")
self:playAnimation()
self:setCenter(0, 0)
self:setSize(playdate.display.getSize())
self:setZIndex(ZIndex.flash)
self:moveTo(0, 0)
self:add()
end
function SmallBoom:update()
self:updateAnimation()
end
function SmallBoom:stopAnimation()
self:remove()
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

@@ -0,0 +1,21 @@
ExplosionMark = {}
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/bomber/boom_splash_" .. self.id)
self:setImage(self.markImage)
self:moveTo(x, y)
self:setZIndex(5)
self:add(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
self:remove()
end
end

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

@@ -0,0 +1,83 @@
Granade = {}
class('Granade').extends(NobleSprite)
function Granade:init(x, y)
Granade.super.init(self)
self.initialRadius = 10
self.currentRadius = self.initialRadius
self.shrinkRate = 0.2
local random = math.random(1, 4)
self.boomSound = playdate.sound.fileplayer.new("assets/audio/boom" .. random)
self.boomSound:setVolume(0.5)
self.isActive = true
self.randomMovementTimer = 0
self.randomXVelocity = 0
self.randomYVelocity = 0
local size = self.initialRadius * 2
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())
self:add(x, y)
self:markDirty()
end
function Granade:update()
if self.isActive then
if BomberScene.instance then
self:moveBy(0, BomberScene.instance.scrollSpeed - 0.2)
self.randomMovementTimer = self.randomMovementTimer + 1
if self.randomMovementTimer >= 10 then
self.randomMovementTimer = 0
self.randomXVelocity = math.random(-50, 50) / 100
self.randomYVelocity = math.random(-5, 10) / 100
end
self:moveBy(self.randomXVelocity, self.randomYVelocity)
end
self.currentRadius = self.currentRadius - self.shrinkRate
if self.currentRadius <= 0 then
self.isActive = false
local particleB = ParticlePoly(self.x, self.y)
particleB:setThickness(1)
particleB:setAngular(-5, 5122)
particleB:setSize(1, 2)
particleB:setSpeed(1, 20)
particleB:setMode(Particles.modes.STAY)
particleB:setBounds(0, 0, 400, 240)
particleB:setColour(Graphics.kColorXOR)
particleB:add(20)
self.boomSound:play(1)
screenShake(1000, 5)
SmallBoom()
ExplosionMark(self.x, self.y)
SmokeCloud(self.x, self.y)
self:remove()
end
end
self:markDirty()
end
function Granade:draw()
local centerX = self.spriteSize / 2
local centerY = self.spriteSize / 2
playdate.graphics.fillCircleAtPoint(centerX, centerY, self.currentRadius)
end

View File

@@ -0,0 +1,96 @@
MovableCrosshair = {}
class('MovableCrosshair').extends(playdate.graphics.sprite)
function MovableCrosshair:init()
MovableCrosshair.super.init(self)
self.lineLength = 10
self.gapSize = 3
self.baseX = 200
self.baseY = 150
self.moveRadius = 2
self.moveSpeed = 2.3
self.time = 0
local totalSize = (self.lineLength + self.gapSize) * 2 + 10
self:setSize(totalSize, totalSize)
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 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()
end
function MovableCrosshair:draw()
local centerX = self.drawOffsetX
local centerY = self.drawOffsetY
playdate.graphics.drawLine(
centerX - self.lineLength - self.gapSize, centerY,
centerX - self.gapSize, centerY
)
playdate.graphics.drawLine(
centerX + self.gapSize, centerY,
centerX + self.lineLength + self.gapSize, centerY
)
playdate.graphics.drawLine(
centerX, centerY - self.lineLength - self.gapSize,
centerX, centerY - self.gapSize
)
playdate.graphics.drawLine(
centerX, centerY + self.gapSize,
centerX, centerY + self.lineLength + self.gapSize
)
end
function MovableCrosshair:moveUp()
if self.baseY > 5 then
self.baseY = self.baseY - self.moveSpeed
end
end
function MovableCrosshair:moveDown()
if self.baseY < 235 then
self.baseY = self.baseY + self.moveSpeed
end
end
function MovableCrosshair:moveLeft()
if self.baseX > 5 then
self.baseX = self.baseX - self.moveSpeed
end
end
function MovableCrosshair:moveRight()
if self.baseX < 395 then
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
@@ -63,6 +63,7 @@ end
function Player:handleInput()
if Player.bat <= 0 or Player.dead then
self.yInertia = self.yInertia * self.friction
return
end
@@ -104,13 +105,17 @@ function Player:handleInput()
-- Y velocity
if crankChange ~= 0 then
if Player.moveRight == false and Player.moveLeft == false then
if crankChange > 0 then
self.animation:setState("up")
if crankChange < 0 then -- TODO: animation depending on inertia
if self.animation.current == "down" then -- TODO: ABSOLUTE BULLSHIT
self.animation:setState("up")
end
else
self.animation:setState("down")
if self.animation.current == "up" then
self.animation:setState("down")
end
end
end
self.yInertia = self.yInertia - (acceleratedChange * 0.007)
self.yInertia = self.yInertia - (acceleratedChange * 0.02)
else
self.yInertia = self.yInertia * self.friction
end
@@ -156,6 +161,10 @@ function Player:handleMovementAndCollisions()
xVel = 0
end
if self.y < -20 and yVel < 0 and Player.bat > 0 then
yVel = 0
end
local _, _, collisions, length = self:checkCollisions(self.x + xVel, self.y + yVel)
self.touchingGround = false
@@ -179,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
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()