Compare commits
87 Commits
f1d0ccd732
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a523951207 | |||
| 429857e7cb | |||
| 07fe9ee150 | |||
| d5becf0fdc | |||
| e4ed56a9d3 | |||
| e4241fc2d2 | |||
| 72ac79bd81 | |||
| ec3f7a91ae | |||
| c150711514 | |||
| 24c56ad23d | |||
| 601a9aa1ad | |||
| b48e3b88a4 | |||
| 94deee7154 | |||
| ddabdaaa29 | |||
| 30e160a94b | |||
| 89519cb7a3 | |||
| 408c56a421 | |||
| c43156e95f | |||
| 2a7d6fccfc | |||
| f564bb4616 | |||
| a855c37128 | |||
| a3b614400b | |||
| 570e57371f | |||
| e8c3e41988 | |||
| 1e1fa3f538 | |||
| 182623a846 | |||
| 727d011601 | |||
| c89f5bd7dc | |||
| 5e78f275c1 | |||
| 698b274d05 | |||
| 98ad8458fa | |||
| efc6b020e6 | |||
| e34375f05e | |||
| ae7a8921a0 | |||
| 161390fb65 | |||
| 256b594076 | |||
| 9844557a0a | |||
| 420dd71423 | |||
| 985193b89f | |||
| 0459b2e6e5 | |||
| 12d00c80a3 | |||
| 9a561552b5 | |||
| eacbdadf67 | |||
| 4189d2835f | |||
| 7d5928940c | |||
| f32f33faa3 | |||
| fa77eb92cc | |||
| 0183b02491 | |||
| 0c8fb9cfa1 | |||
| df23351665 | |||
| 97e9494fb2 | |||
| e58938c9d5 | |||
| c8c900dbd8 | |||
| 64e15c3f18 | |||
| e417be4c1f | |||
| 888b1da1bd | |||
| 21520e9922 | |||
| ab44ddd1b4 | |||
| 3877165783 | |||
| 055e1855e0 | |||
| 9bf60b6f6b | |||
| c79f9ee4d5 | |||
| 93ae09bd94 | |||
| aed67091a3 | |||
| c3b26f9dcf | |||
| 978055c02a | |||
| 4048bd39e8 | |||
| 328350b6c6 | |||
| 15af10d313 | |||
| 00755a383f | |||
| 95eccf0fbc | |||
| a26d1ddfd4 | |||
| f05e89785a | |||
| 63292900ac | |||
| 8a41066410 | |||
| e21d35a47d | |||
| 86b109f18a | |||
| b579692d42 | |||
| e9e0ef9da2 | |||
| e9c53a5717 | |||
| 05564c3136 | |||
|
|
04405066c4 | ||
|
|
94a412d168 | ||
|
|
70f18d4f3b | ||
|
|
bc1944b163 | ||
|
|
f47bd6b2e0 | ||
|
|
b56eb8cdb4 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
.local
|
.local
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
bomberman.zip
|
bombexpert.lua
|
||||||
bomberman/*
|
bombexpert/*
|
||||||
|
|||||||
36
.woodpecker.yml
Normal file
36
.woodpecker.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
steps:
|
||||||
|
- name: version
|
||||||
|
image: alpine
|
||||||
|
commands:
|
||||||
|
- 'apk add --no-cache make'
|
||||||
|
- 'make ci-version'
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
image: git.teletype.hu/internal/tic80pro:latest
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: /tmp
|
||||||
|
commands:
|
||||||
|
- 'make ci-export'
|
||||||
|
|
||||||
|
- name: artifact
|
||||||
|
image: alpine
|
||||||
|
environment:
|
||||||
|
DROPAREA_HOST: vps.teletype.hu
|
||||||
|
DROPAREA_PORT: 2223
|
||||||
|
DROPAREA_TARGET_PATH: /home/drop
|
||||||
|
DROPAREA_USER: drop
|
||||||
|
DROPAREA_SSH_PASSWORD:
|
||||||
|
from_secret: droparea_ssh_password
|
||||||
|
commands:
|
||||||
|
- 'apk add --no-cache make openssh-client sshpass'
|
||||||
|
- 'make ci-upload'
|
||||||
|
|
||||||
|
- name: update
|
||||||
|
image: alpine
|
||||||
|
environment:
|
||||||
|
UPDATE_SERVER: https://games.vps.teletype.hu
|
||||||
|
UPDATE_SECRET:
|
||||||
|
from_secret: update_secret_key
|
||||||
|
commands:
|
||||||
|
- 'apk add --no-cache make curl'
|
||||||
|
- 'make ci-update'
|
||||||
115
Makefile
Normal file
115
Makefile
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# -----------------------------------------
|
||||||
|
# Makefile – TIC-80 project builder
|
||||||
|
# -----------------------------------------
|
||||||
|
|
||||||
|
PROJECT = bombexpert
|
||||||
|
|
||||||
|
ORDER = $(PROJECT).inc
|
||||||
|
OUTPUT = $(PROJECT).lua
|
||||||
|
OUTPUT_ZIP = $(PROJECT).html.zip
|
||||||
|
OUTPUT_TIC = $(PROJECT).tic
|
||||||
|
|
||||||
|
SRC_DIR = inc
|
||||||
|
SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER))
|
||||||
|
|
||||||
|
ASSETS_LUA = inc/meta/meta.assets.lua
|
||||||
|
|
||||||
|
# CI/CD variables
|
||||||
|
VERSION_FILE = .version
|
||||||
|
GAME_LANG ?= lua
|
||||||
|
DROPAREA_HOST ?= vps.teletype.hu
|
||||||
|
DROPAREA_PORT ?= 2223
|
||||||
|
DROPAREA_TARGET_PATH ?= /home/drop
|
||||||
|
DROPAREA_USER ?= drop
|
||||||
|
UPDATE_SERVER ?= https://games.vps.teletype.hu
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build: $(OUTPUT)
|
||||||
|
|
||||||
|
$(OUTPUT): $(SRC) $(ORDER)
|
||||||
|
@rm -f $(OUTPUT)
|
||||||
|
@while read f; do \
|
||||||
|
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
|
||||||
|
echo "" >> $(OUTPUT); \
|
||||||
|
done < $(ORDER)
|
||||||
|
|
||||||
|
export: build
|
||||||
|
@if [ -z "$(VERSION)" ]; then \
|
||||||
|
echo "ERROR: VERSION not set!"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "==> Exporting HTML for version $(VERSION)"
|
||||||
|
@tic80 --cli --skip --fs=. \
|
||||||
|
--cmd="load $(OUTPUT) & save $(PROJECT)-$(VERSION) & export html $(PROJECT)-$(VERSION).html & exit"
|
||||||
|
@echo "==> Creating versioned files"
|
||||||
|
@if [ -f "$(PROJECT)-$(VERSION).tic" ]; then \
|
||||||
|
cp $(PROJECT)-$(VERSION).tic $(PROJECT).tic; \
|
||||||
|
fi
|
||||||
|
@if [ -f "$(PROJECT)-$(VERSION).html.zip" ]; then \
|
||||||
|
cp $(PROJECT)-$(VERSION).html.zip $(PROJECT).html.zip; \
|
||||||
|
fi
|
||||||
|
@echo "==> Generated files:"
|
||||||
|
@ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true
|
||||||
|
|
||||||
|
watch:
|
||||||
|
make build
|
||||||
|
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do make build; done
|
||||||
|
|
||||||
|
import_assets:
|
||||||
|
@for t in $(ASSET_TYPES); do \
|
||||||
|
for f in $(ASSETS_DIR)/$$t/*.png; do \
|
||||||
|
[ -e "$$f" ] || continue; \
|
||||||
|
echo "==> Importing $$f as $$t..."; \
|
||||||
|
tic80 --cli --skip --fs=. --cmd="import $$t $$f & exit"; \
|
||||||
|
done; \
|
||||||
|
done
|
||||||
|
|
||||||
|
export_assets: build
|
||||||
|
@echo "==> Exporting TIC-80 asset sections"
|
||||||
|
@mkdir -p inc/meta
|
||||||
|
@sed -n '/^-- <PALETTE>/,/^-- <\/PALETTE>/p;\
|
||||||
|
/^-- <TILES>/,/^-- <\/TILES>/p;\
|
||||||
|
/^-- <SPRITES>/,/^-- <\/SPRITES>/p;\
|
||||||
|
/^-- <MAP>/,/^-- <\/MAP>/p;\
|
||||||
|
/^-- <SFX>/,/^-- <\/SFX>/p;\
|
||||||
|
/^-- <MUSIC>/,/^-- <\/MUSIC>/p' \
|
||||||
|
$(OUTPUT) > $(ASSETS_LUA)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(OUTPUT)
|
||||||
|
@echo "==> Cleaned build artifacts"
|
||||||
|
|
||||||
|
# CI/CD Targets
|
||||||
|
ci-version:
|
||||||
|
@VERSION=$$(sed -n "s/^-- version: //p" inc/meta/meta.header.lua | head -n 1 | tr -d "[:space:]"); \
|
||||||
|
BRANCH=$${CI_COMMIT_BRANCH:-$${WOODPECKER_BRANCH}}; \
|
||||||
|
if [ "$$BRANCH" != "main" ] && [ "$$BRANCH" != "master" ] && [ -n "$$BRANCH" ]; then \
|
||||||
|
VERSION=dev-$$VERSION-$$BRANCH; \
|
||||||
|
fi; \
|
||||||
|
echo "VERSION is: $$VERSION"; \
|
||||||
|
echo $$VERSION > $(VERSION_FILE)
|
||||||
|
|
||||||
|
ci-export:
|
||||||
|
@VERSION=$$(cat $(VERSION_FILE)); \
|
||||||
|
echo "==> Building and exporting version $$VERSION"; \
|
||||||
|
$(MAKE) export VERSION=$$VERSION
|
||||||
|
|
||||||
|
ci-upload:
|
||||||
|
@VERSION=$$(cat $(VERSION_FILE)); \
|
||||||
|
echo "==> Uploading artifacts for version $$VERSION"; \
|
||||||
|
ls -lh $(PROJECT)-$$VERSION.* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true; \
|
||||||
|
cp $(PROJECT).lua $(PROJECT)-$$VERSION.lua; \
|
||||||
|
FILE_LUA=$(PROJECT)-$$VERSION.lua; \
|
||||||
|
FILE_TIC=$(PROJECT)-$$VERSION.tic; \
|
||||||
|
FILE_HTML_ZIP=$(PROJECT)-$$VERSION.html.zip; \
|
||||||
|
SCP_TARGET="$(DROPAREA_USER)@$(DROPAREA_HOST):$(DROPAREA_TARGET_PATH)/"; \
|
||||||
|
sshpass -p "$(DROPAREA_SSH_PASSWORD)" scp -o StrictHostKeyChecking=no -P $(DROPAREA_PORT) $$FILE_LUA $$FILE_TIC $$FILE_HTML_ZIP $$SCP_TARGET
|
||||||
|
|
||||||
|
ci-update:
|
||||||
|
@VERSION=$$(cat $(VERSION_FILE)); \
|
||||||
|
echo "==> Triggering update for version $$VERSION"; \
|
||||||
|
curl "$(UPDATE_SERVER)/update?secret=$(UPDATE_SECRET)&name=$(PROJECT)&platform=tic80&version=$$VERSION"
|
||||||
|
|
||||||
|
.PHONY: all build export watch import_assets export_assets clean ci-version ci-export ci-upload ci-update
|
||||||
|
|
||||||
139
README.md
139
README.md
@@ -1,93 +1,80 @@
|
|||||||
# Bomberman
|
|
||||||
|
|
||||||
|
# BombExpert
|
||||||
|
|
||||||
|
A classic Bomberman clone for [TIC-80](https://tic80.com/) fantasy console.
|
||||||
|
|
||||||
## Getting started
|
## Features
|
||||||
|
|
||||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
- 1 Player mode (vs AI)
|
||||||
|
- 2 Player local multiplayer
|
||||||
|
- Grid-based movement with smooth animation
|
||||||
|
- Destructible walls
|
||||||
|
- Power-ups:
|
||||||
|
- **B** (yellow): +1 bomb capacity
|
||||||
|
- **P** (orange): +1 blast range
|
||||||
|
- Smart AI opponent that seeks power-ups and avoids explosions
|
||||||
|
- Score tracking across rounds
|
||||||
|
|
||||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
## Controls
|
||||||
|
|
||||||
## Add your files
|
### Player 1 (Blue)
|
||||||
|
| Action | Key |
|
||||||
|
|--------|-----|
|
||||||
|
| Move | Arrow Keys |
|
||||||
|
| Place Bomb | Space |
|
||||||
|
|
||||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
### Player 2 (Red)
|
||||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
| Action | Key |
|
||||||
|
|--------|-----|
|
||||||
|
| Move | W, A, S, D |
|
||||||
|
| Place Bomb | G |
|
||||||
|
|
||||||
```
|
### Menu Navigation
|
||||||
cd existing_repo
|
| Action | Key |
|
||||||
git remote add origin https://tea.zenheads.hu/zsolt.tasnadi/bomberman.git
|
|--------|-----|
|
||||||
git branch -M main
|
| Navigate | Up / Down |
|
||||||
git push -uf origin main
|
| Select | Space |
|
||||||
|
| Back / Exit | Backspace |
|
||||||
|
|
||||||
|
## How to Play
|
||||||
|
|
||||||
|
1. Run the game in TIC-80
|
||||||
|
2. Select "1 Player Game" or "2 Player Game" from the menu
|
||||||
|
3. Navigate through the maze and place bombs to destroy breakable walls
|
||||||
|
4. Collect power-ups to increase your bomb capacity and blast range
|
||||||
|
5. Eliminate your opponent by catching them in an explosion
|
||||||
|
6. First player to win the round scores a point
|
||||||
|
|
||||||
|
## Running the Game
|
||||||
|
|
||||||
|
### In TIC-80
|
||||||
|
```bash
|
||||||
|
load bombexpert.lua
|
||||||
|
run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Integrate with your tools
|
### In Browser
|
||||||
|
Use the HTML export in the `bombexpert/` folder with the included server:
|
||||||
|
```bash
|
||||||
|
python serve.py
|
||||||
|
```
|
||||||
|
Then open http://localhost:3333 in your browser.
|
||||||
|
|
||||||
- [ ] [Set up project integrations](https://tea.zenheads.hu/zsolt.tasnadi/bomberman/-/settings/integrations)
|
## Requirements
|
||||||
|
|
||||||
## Collaborate with your team
|
- [TIC-80](https://tic80.com/) fantasy console (free version works)
|
||||||
|
- Or any modern web browser (for HTML export)
|
||||||
|
|
||||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
## Credits
|
||||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
|
||||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
|
||||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
|
||||||
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
|
||||||
|
|
||||||
## Test and Deploy
|
- **Author**: Zsolt Tasnadi
|
||||||
|
- **Powered by**: Claude
|
||||||
Use the built-in continuous integration in GitLab.
|
- **Sponsored by**: Zen Heads
|
||||||
|
|
||||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
|
||||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
|
||||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
|
||||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
|
||||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
# Editing this README
|
|
||||||
|
|
||||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
|
||||||
|
|
||||||
## Suggestions for a good README
|
|
||||||
|
|
||||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
|
||||||
|
|
||||||
## Name
|
|
||||||
Choose a self-explaining name for your project.
|
|
||||||
|
|
||||||
## Description
|
|
||||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
|
||||||
|
|
||||||
## Badges
|
|
||||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
|
||||||
|
|
||||||
## Visuals
|
|
||||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
State if you are open to contributions and what your requirements are for accepting them.
|
|
||||||
|
|
||||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
|
||||||
|
|
||||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
|
||||||
|
|
||||||
## Authors and acknowledgment
|
|
||||||
Show your appreciation to those who have contributed to the project.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
For open source projects, say how it is licensed.
|
|
||||||
|
|
||||||
## Project status
|
MIT License
|
||||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Happy X-MAS!
|
||||||
|
|||||||
2
bombexpert.inc
Normal file
2
bombexpert.inc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
meta/meta.header.lua
|
||||||
|
system/system.allin.lua
|
||||||
11
inc/meta/meta.header.lua
Normal file
11
inc/meta/meta.header.lua
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- title: BombExpert
|
||||||
|
-- name: bombexpert
|
||||||
|
-- author: Zsolt Tasnadi
|
||||||
|
-- desc: Simple BombExpert for TIC-80
|
||||||
|
-- site: http://teletype.hu
|
||||||
|
-- license: MIT License
|
||||||
|
-- version: 0.2
|
||||||
|
-- script: lua
|
||||||
|
|
||||||
|
-- luacheck: globals TIC btn btnp cls rect spr print exit sfx keyp key
|
||||||
|
-- luacheck: max line length 150
|
||||||
@@ -1,14 +1,3 @@
|
|||||||
-- title: Bomberman Clone
|
|
||||||
-- author: Zsolt Tasnadi
|
|
||||||
-- desc: Simple Bomberman clone for TIC-80
|
|
||||||
-- site: http://teletype.hu
|
|
||||||
-- license: MIT License
|
|
||||||
-- version: 0.2
|
|
||||||
-- script: lua
|
|
||||||
|
|
||||||
-- luacheck: globals TIC btn btnp cls rect spr print exit sfx keyp key
|
|
||||||
-- luacheck: max line length 150
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Constants
|
-- Constants
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -25,18 +14,6 @@ local EMPTY = 0
|
|||||||
local SOLID_WALL = 1
|
local SOLID_WALL = 1
|
||||||
local BREAKABLE_WALL = 2
|
local BREAKABLE_WALL = 2
|
||||||
|
|
||||||
-- Timing constants
|
|
||||||
local BOMB_TIMER = 90
|
|
||||||
local EXPLOSION_TIMER = 30
|
|
||||||
local SPREAD_DELAY = 6 -- ticks per cell spread
|
|
||||||
local SPLASH_DURATION = 90 -- 1.5 seconds at 60fps
|
|
||||||
local WIN_SCREEN_DURATION = 60
|
|
||||||
local AI_MOVE_DELAY = 20
|
|
||||||
local AI_BOMB_COOLDOWN = 90
|
|
||||||
|
|
||||||
-- Movement
|
|
||||||
local MOVE_SPEED = 2
|
|
||||||
|
|
||||||
-- Directions (up, down, left, right)
|
-- Directions (up, down, left, right)
|
||||||
local DIRECTIONS = {
|
local DIRECTIONS = {
|
||||||
{0, -1},
|
{0, -1},
|
||||||
@@ -44,6 +21,7 @@ local DIRECTIONS = {
|
|||||||
{-1, 0},
|
{-1, 0},
|
||||||
{1, 0}
|
{1, 0}
|
||||||
}
|
}
|
||||||
|
local SPREAD_DIRS = {-1, 1} -- negative and positive spread directions
|
||||||
|
|
||||||
-- Sprite indices (SPRITES section loads at 256+)
|
-- Sprite indices (SPRITES section loads at 256+)
|
||||||
local PLAYER_BLUE = 256
|
local PLAYER_BLUE = 256
|
||||||
@@ -71,9 +49,76 @@ local GAME_STATE_MENU = 1
|
|||||||
local GAME_STATE_PLAYING = 2
|
local GAME_STATE_PLAYING = 2
|
||||||
local GAME_STATE_HELP = 3
|
local GAME_STATE_HELP = 3
|
||||||
local GAME_STATE_CREDITS = 4
|
local GAME_STATE_CREDITS = 4
|
||||||
|
local GAME_STATE_SETTINGS = 5
|
||||||
|
|
||||||
-- Powerup spawn chance
|
--------------------------------------------------------------------------------
|
||||||
local POWERUP_SPAWN_CHANCE = 0.3
|
-- Game Configuration (easy to tweak game parameters)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local Config = {
|
||||||
|
-- Player settings
|
||||||
|
player = {
|
||||||
|
move_speed = 2,
|
||||||
|
start_bombs = 1,
|
||||||
|
start_power = 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Bomb settings
|
||||||
|
bomb = {
|
||||||
|
timer = 90,
|
||||||
|
explosion_duration = 30,
|
||||||
|
spread_delay = 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- AI settings
|
||||||
|
ai = {
|
||||||
|
move_delay = 20,
|
||||||
|
bomb_cooldown = 90,
|
||||||
|
danger_threshold = 30,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Map settings
|
||||||
|
map = {
|
||||||
|
breakable_wall_chance = 0.7,
|
||||||
|
powerup_spawn_chance = 0.3,
|
||||||
|
generator = "classic",
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Timing
|
||||||
|
timing = {
|
||||||
|
splash_duration = 90,
|
||||||
|
win_screen_duration = 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Sound System (centralized audio management)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local Sound = {
|
||||||
|
effects = {
|
||||||
|
explosion = {
|
||||||
|
id = 0,
|
||||||
|
note = nil,
|
||||||
|
duration = 30
|
||||||
|
},
|
||||||
|
pickup = {
|
||||||
|
id = 1,
|
||||||
|
note = nil,
|
||||||
|
duration = 8
|
||||||
|
},
|
||||||
|
-- Add new sounds here:
|
||||||
|
-- menu_select = {id = 2, note = nil, duration = 10},
|
||||||
|
-- player_death = {id = 3, note = nil, duration = 20},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sound.play(effect_name)
|
||||||
|
local effect = Sound.effects[effect_name]
|
||||||
|
if effect then
|
||||||
|
sfx(effect.id, effect.note, effect.duration)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Modules
|
-- Modules
|
||||||
@@ -88,6 +133,7 @@ local Splash = {}
|
|||||||
local Menu = {}
|
local Menu = {}
|
||||||
local Help = {}
|
local Help = {}
|
||||||
local Credits = {}
|
local Credits = {}
|
||||||
|
local Settings = {}
|
||||||
local WinScreen = {}
|
local WinScreen = {}
|
||||||
local GameBoard = {}
|
local GameBoard = {}
|
||||||
local Bomb = {}
|
local Bomb = {}
|
||||||
@@ -101,8 +147,10 @@ local Game = {}
|
|||||||
|
|
||||||
local State = {
|
local State = {
|
||||||
game_state = GAME_STATE_SPLASH,
|
game_state = GAME_STATE_SPLASH,
|
||||||
splash_timer = SPLASH_DURATION,
|
splash_timer = 0, -- Will be set from Config on first frame
|
||||||
|
initialized = false, -- Config loaded flag
|
||||||
menu_selection = 1,
|
menu_selection = 1,
|
||||||
|
settings_selection = 1,
|
||||||
two_player_mode = false,
|
two_player_mode = false,
|
||||||
players = {},
|
players = {},
|
||||||
powerups = {},
|
powerups = {},
|
||||||
@@ -132,14 +180,18 @@ local POWERUP_TYPES = {
|
|||||||
weight = 50,
|
weight = 50,
|
||||||
color = COLOR_YELLOW,
|
color = COLOR_YELLOW,
|
||||||
label = "B",
|
label = "B",
|
||||||
apply = function(player) player.maxBombs = player.maxBombs + 1 end
|
apply = function(player)
|
||||||
|
player.maxBombs = player.maxBombs + 1
|
||||||
|
end
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type = "power",
|
type = "power",
|
||||||
weight = 50,
|
weight = 50,
|
||||||
color = COLOR_ORANGE,
|
color = COLOR_ORANGE,
|
||||||
label = "P",
|
label = "P",
|
||||||
apply = function(player) player.bombPower = player.bombPower + 1 end
|
apply = function(player)
|
||||||
|
player.bombPower = player.bombPower + 1
|
||||||
|
end
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +228,7 @@ function Powerup.init()
|
|||||||
State.powerups = {}
|
State.powerups = {}
|
||||||
for row = 1, MAP_HEIGHT do
|
for row = 1, MAP_HEIGHT do
|
||||||
for col = 1, MAP_WIDTH do
|
for col = 1, MAP_WIDTH do
|
||||||
if State.map[row][col] == BREAKABLE_WALL and math.random() < POWERUP_SPAWN_CHANCE then
|
if State.map[row][col] == BREAKABLE_WALL and math.random() < Config.map.powerup_spawn_chance then
|
||||||
table.insert(State.powerups, {
|
table.insert(State.powerups, {
|
||||||
gridX = col,
|
gridX = col,
|
||||||
gridY = row,
|
gridY = row,
|
||||||
@@ -209,7 +261,7 @@ function Powerup.check_pickup()
|
|||||||
local config = Powerup.get_config(pw.type)
|
local config = Powerup.get_config(pw.type)
|
||||||
config.apply(player)
|
config.apply(player)
|
||||||
table.remove(State.powerups, i)
|
table.remove(State.powerups, i)
|
||||||
sfx(1, nil, 8)
|
Sound.play("pickup")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -224,7 +276,7 @@ function Input.action_pressed()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Input.back_pressed()
|
function Input.back_pressed()
|
||||||
return keyp(51) -- Backspace key
|
return keyp(51) or btnp(5) -- Backspace key or B button
|
||||||
end
|
end
|
||||||
|
|
||||||
function Input.up()
|
function Input.up()
|
||||||
@@ -251,6 +303,14 @@ function Input.down_pressed()
|
|||||||
return btnp(1)
|
return btnp(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Input.left_pressed()
|
||||||
|
return btnp(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Input.right_pressed()
|
||||||
|
return btnp(3)
|
||||||
|
end
|
||||||
|
|
||||||
-- Player 2 inputs (WASD + G for bomb)
|
-- Player 2 inputs (WASD + G for bomb)
|
||||||
function Input.p2_up()
|
function Input.p2_up()
|
||||||
return key(23) or btn(8) -- W key or gamepad 2 up
|
return key(23) or btn(8) -- W key or gamepad 2 up
|
||||||
@@ -320,28 +380,95 @@ function Map.is_spawn_area(row, col)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function Map.generate()
|
--------------------------------------------------------------------------------
|
||||||
|
-- Map Generators (extensible map generation system)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local MapGenerators = {}
|
||||||
|
|
||||||
|
-- Classic Bomberman grid pattern
|
||||||
|
function MapGenerators.classic(row, col)
|
||||||
|
if row % 2 == 1 and col % 2 == 1 and row > 1 and col > 1 then
|
||||||
|
return SOLID_WALL
|
||||||
|
elseif math.random() < Config.map.breakable_wall_chance then
|
||||||
|
return BREAKABLE_WALL
|
||||||
|
end
|
||||||
|
return EMPTY
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Open arena with fewer pillars
|
||||||
|
function MapGenerators.arena(row, col)
|
||||||
|
-- Only pillars at every 4th position
|
||||||
|
if row % 4 == 1 and col % 4 == 1 and row > 1 and col > 1 then
|
||||||
|
return SOLID_WALL
|
||||||
|
elseif math.random() < Config.map.breakable_wall_chance * 0.5 then
|
||||||
|
return BREAKABLE_WALL
|
||||||
|
end
|
||||||
|
return EMPTY
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Dense maze with more walls
|
||||||
|
function MapGenerators.maze(row, col)
|
||||||
|
if row % 2 == 1 and col % 2 == 1 and row > 1 and col > 1 then
|
||||||
|
return SOLID_WALL
|
||||||
|
elseif math.random() < 0.85 then
|
||||||
|
return BREAKABLE_WALL
|
||||||
|
end
|
||||||
|
return EMPTY
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Corridors pattern
|
||||||
|
function MapGenerators.corridors(row, col)
|
||||||
|
-- Horizontal corridors at rows 4, 8, 12
|
||||||
|
if (row == 4 or row == 8 or row == 12) and col > 1 and col < MAP_WIDTH then
|
||||||
|
if math.random() < 0.3 then
|
||||||
|
return BREAKABLE_WALL
|
||||||
|
end
|
||||||
|
return EMPTY
|
||||||
|
end
|
||||||
|
-- Vertical corridors at cols 7, 14, 21
|
||||||
|
if (col == 7 or col == 14 or col == 21) and row > 1 and row < MAP_HEIGHT then
|
||||||
|
if math.random() < 0.3 then
|
||||||
|
return BREAKABLE_WALL
|
||||||
|
end
|
||||||
|
return EMPTY
|
||||||
|
end
|
||||||
|
-- Rest is classic pattern
|
||||||
|
if row % 2 == 1 and col % 2 == 1 and row > 1 and col > 1 then
|
||||||
|
return SOLID_WALL
|
||||||
|
elseif math.random() < Config.map.breakable_wall_chance then
|
||||||
|
return BREAKABLE_WALL
|
||||||
|
end
|
||||||
|
return EMPTY
|
||||||
|
end
|
||||||
|
|
||||||
|
function Map.generate(generator_name)
|
||||||
|
generator_name = generator_name or Config.map.generator
|
||||||
|
local generator = MapGenerators[generator_name] or MapGenerators.classic
|
||||||
|
|
||||||
for row = 1, MAP_HEIGHT do
|
for row = 1, MAP_HEIGHT do
|
||||||
for col = 1, MAP_WIDTH do
|
for col = 1, MAP_WIDTH do
|
||||||
-- Border walls
|
-- Border walls (always)
|
||||||
if row == 1 or row == MAP_HEIGHT or col == 1 or col == MAP_WIDTH then
|
if row == 1 or row == MAP_HEIGHT or col == 1 or col == MAP_WIDTH then
|
||||||
State.map[row][col] = SOLID_WALL
|
State.map[row][col] = SOLID_WALL
|
||||||
-- Spawn areas MUST be empty
|
-- Spawn areas MUST be empty (always)
|
||||||
elseif Map.is_spawn_area(row, col) then
|
elseif Map.is_spawn_area(row, col) then
|
||||||
State.map[row][col] = EMPTY
|
State.map[row][col] = EMPTY
|
||||||
-- Grid pattern solid walls (odd row AND odd col, but not border)
|
-- Use selected generator for the rest
|
||||||
elseif row % 2 == 1 and col % 2 == 1 and row > 1 and col > 1 then
|
|
||||||
State.map[row][col] = SOLID_WALL
|
|
||||||
-- Random: breakable wall or empty
|
|
||||||
else
|
else
|
||||||
if math.random() < 0.7 then
|
State.map[row][col] = generator(row, col)
|
||||||
State.map[row][col] = BREAKABLE_WALL
|
|
||||||
else
|
|
||||||
State.map[row][col] = EMPTY
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Helper to get available generators
|
||||||
|
function Map.get_generators()
|
||||||
|
local names = {}
|
||||||
|
for name, _ in pairs(MapGenerators) do
|
||||||
|
table.insert(names, name)
|
||||||
|
end
|
||||||
|
return names
|
||||||
end
|
end
|
||||||
|
|
||||||
function Map.draw_shadows()
|
function Map.draw_shadows()
|
||||||
@@ -418,10 +545,17 @@ end
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
function Splash.update()
|
function Splash.update()
|
||||||
|
-- Initialize on first frame
|
||||||
|
if not State.initialized then
|
||||||
|
Settings.load()
|
||||||
|
State.splash_timer = Config.timing.splash_duration
|
||||||
|
State.initialized = true
|
||||||
|
end
|
||||||
|
|
||||||
cls(COLOR_BLACK)
|
cls(COLOR_BLACK)
|
||||||
|
|
||||||
UI.print_shadow("Bomberman", 85, 50, COLOR_BLUE, false, 2)
|
UI.print_shadow("BombExpert", 85, 50, COLOR_BLUE, false, 2)
|
||||||
UI.print_shadow("Clone", 100, 70, COLOR_BLUE, false, 2)
|
|
||||||
|
|
||||||
State.splash_timer = State.splash_timer - 1
|
State.splash_timer = State.splash_timer - 1
|
||||||
if State.splash_timer <= 0 then
|
if State.splash_timer <= 0 then
|
||||||
@@ -433,52 +567,74 @@ end
|
|||||||
-- Menu module
|
-- Menu module
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local MENU_ITEMS = {
|
||||||
|
{
|
||||||
|
label = "1 Player Game",
|
||||||
|
action = function()
|
||||||
|
State.two_player_mode = false
|
||||||
|
State.game_state = GAME_STATE_PLAYING
|
||||||
|
Game.init()
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "2 Player Game",
|
||||||
|
action = function()
|
||||||
|
State.two_player_mode = true
|
||||||
|
State.game_state = GAME_STATE_PLAYING
|
||||||
|
Game.init()
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Settings",
|
||||||
|
action = function()
|
||||||
|
State.game_state = GAME_STATE_SETTINGS
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Help",
|
||||||
|
action = function()
|
||||||
|
State.game_state = GAME_STATE_HELP
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Credits",
|
||||||
|
action = function()
|
||||||
|
State.game_state = GAME_STATE_CREDITS
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Exit",
|
||||||
|
action = exit
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function get_menu_color(index)
|
||||||
|
return (State.menu_selection == index) and COLOR_CYAN or COLOR_GRAY_LIGHT
|
||||||
|
end
|
||||||
|
|
||||||
function Menu.update()
|
function Menu.update()
|
||||||
cls(COLOR_BLACK)
|
cls(COLOR_BLACK)
|
||||||
|
|
||||||
UI.print_shadow("Bomberman", 85, 20, COLOR_BLUE, false, 2)
|
UI.print_shadow("BombExpert", 85, 20, COLOR_BLUE, false, 2)
|
||||||
UI.print_shadow("Clone", 100, 40, COLOR_BLUE, false, 2)
|
|
||||||
|
|
||||||
local unselected = COLOR_GRAY_LIGHT
|
|
||||||
local p1_color = (State.menu_selection == 1) and COLOR_CYAN or unselected
|
|
||||||
local p2_color = (State.menu_selection == 2) and COLOR_CYAN or unselected
|
|
||||||
local help_color = (State.menu_selection == 3) and COLOR_CYAN or unselected
|
|
||||||
local credits_color = (State.menu_selection == 4) and COLOR_CYAN or unselected
|
|
||||||
local exit_color = (State.menu_selection == 5) and COLOR_CYAN or unselected
|
|
||||||
|
|
||||||
local cursor_y = 60 + (State.menu_selection - 1) * 14
|
local cursor_y = 60 + (State.menu_selection - 1) * 14
|
||||||
UI.print_shadow(">", 60, cursor_y, COLOR_CYAN)
|
UI.print_shadow(">", 60, cursor_y, COLOR_CYAN)
|
||||||
|
|
||||||
UI.print_shadow("1 Player Game", 70, 60, p1_color)
|
for i, item in ipairs(MENU_ITEMS) do
|
||||||
UI.print_shadow("2 Player Game", 70, 74, p2_color)
|
UI.print_shadow(item.label, 70, 60 + (i - 1) * 14, get_menu_color(i))
|
||||||
UI.print_shadow("Help", 70, 88, help_color)
|
end
|
||||||
UI.print_shadow("Credits", 70, 102, credits_color)
|
|
||||||
UI.print_shadow("Exit", 70, 116, exit_color)
|
|
||||||
|
|
||||||
if Input.back_pressed() then
|
if Input.back_pressed() then
|
||||||
exit()
|
exit()
|
||||||
elseif Input.up_pressed() then
|
elseif Input.up_pressed() then
|
||||||
State.menu_selection = State.menu_selection - 1
|
State.menu_selection = State.menu_selection - 1
|
||||||
if State.menu_selection < 1 then State.menu_selection = 5 end
|
if State.menu_selection < 1 then State.menu_selection = #MENU_ITEMS end
|
||||||
elseif Input.down_pressed() then
|
elseif Input.down_pressed() then
|
||||||
State.menu_selection = State.menu_selection + 1
|
State.menu_selection = State.menu_selection + 1
|
||||||
if State.menu_selection > 5 then State.menu_selection = 1 end
|
if State.menu_selection > #MENU_ITEMS then State.menu_selection = 1 end
|
||||||
elseif Input.action_pressed() then
|
elseif Input.action_pressed() then
|
||||||
if State.menu_selection == 1 then
|
MENU_ITEMS[State.menu_selection].action()
|
||||||
State.two_player_mode = false
|
|
||||||
State.game_state = GAME_STATE_PLAYING
|
|
||||||
Game.init()
|
|
||||||
elseif State.menu_selection == 2 then
|
|
||||||
State.two_player_mode = true
|
|
||||||
State.game_state = GAME_STATE_PLAYING
|
|
||||||
Game.init()
|
|
||||||
elseif State.menu_selection == 3 then
|
|
||||||
State.game_state = GAME_STATE_HELP
|
|
||||||
elseif State.menu_selection == 4 then
|
|
||||||
State.game_state = GAME_STATE_CREDITS
|
|
||||||
else
|
|
||||||
exit()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -552,6 +708,224 @@ function Credits.update()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Settings module (persistent settings menu)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- luacheck: globals pmem
|
||||||
|
|
||||||
|
-- Settings definition: each setting maps to a pmem slot
|
||||||
|
-- pmem stores integers, so we use multipliers for decimals
|
||||||
|
local SETTINGS_ITEMS = {
|
||||||
|
{
|
||||||
|
label = "Start Bombs",
|
||||||
|
path = {"player", "start_bombs"},
|
||||||
|
min = 1,
|
||||||
|
max = 5,
|
||||||
|
step = 1,
|
||||||
|
pmem_slot = 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Start Power",
|
||||||
|
path = {"player", "start_power"},
|
||||||
|
min = 1,
|
||||||
|
max = 5,
|
||||||
|
step = 1,
|
||||||
|
pmem_slot = 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Move Speed",
|
||||||
|
path = {"player", "move_speed"},
|
||||||
|
min = 1,
|
||||||
|
max = 4,
|
||||||
|
step = 1,
|
||||||
|
pmem_slot = 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Bomb Timer",
|
||||||
|
path = {"bomb", "timer"},
|
||||||
|
min = 60,
|
||||||
|
max = 180,
|
||||||
|
step = 15,
|
||||||
|
pmem_slot = 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "AI Speed",
|
||||||
|
path = {"ai", "move_delay"},
|
||||||
|
min = 10,
|
||||||
|
max = 40,
|
||||||
|
step = 5,
|
||||||
|
pmem_slot = 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Wall Density",
|
||||||
|
path = {"map", "breakable_wall_chance"},
|
||||||
|
min = 30,
|
||||||
|
max = 90,
|
||||||
|
step = 10,
|
||||||
|
pmem_slot = 5,
|
||||||
|
multiplier = 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Powerup Chance",
|
||||||
|
path = {"map", "powerup_spawn_chance"},
|
||||||
|
min = 10,
|
||||||
|
max = 50,
|
||||||
|
step = 5,
|
||||||
|
pmem_slot = 6,
|
||||||
|
multiplier = 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Map Style",
|
||||||
|
path = {"map", "generator"},
|
||||||
|
min = 1,
|
||||||
|
max = 4,
|
||||||
|
step = 1,
|
||||||
|
pmem_slot = 7,
|
||||||
|
is_enum = true,
|
||||||
|
enum_values = {"classic", "arena", "maze", "corridors"}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Magic number to detect if pmem has been initialized
|
||||||
|
local PMEM_INIT_SLOT = 255
|
||||||
|
local PMEM_INIT_VALUE = 12345
|
||||||
|
|
||||||
|
local function get_config_value(item)
|
||||||
|
local value = Config
|
||||||
|
for _, key in ipairs(item.path) do
|
||||||
|
value = value[key]
|
||||||
|
end
|
||||||
|
if item.multiplier then
|
||||||
|
return math.floor(value * item.multiplier + 0.5)
|
||||||
|
end
|
||||||
|
if item.is_enum then
|
||||||
|
for i, v in ipairs(item.enum_values) do
|
||||||
|
if v == value then return i end
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set_config_value(item, value)
|
||||||
|
local target = Config
|
||||||
|
for i = 1, #item.path - 1 do
|
||||||
|
target = target[item.path[i]]
|
||||||
|
end
|
||||||
|
local final_key = item.path[#item.path]
|
||||||
|
if item.multiplier then
|
||||||
|
target[final_key] = value / item.multiplier
|
||||||
|
elseif item.is_enum then
|
||||||
|
target[final_key] = item.enum_values[value]
|
||||||
|
else
|
||||||
|
target[final_key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_display_value(item, value)
|
||||||
|
if item.is_enum then
|
||||||
|
return item.enum_values[value] or "?"
|
||||||
|
end
|
||||||
|
if item.multiplier then
|
||||||
|
return value .. "%"
|
||||||
|
end
|
||||||
|
return tostring(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings.load()
|
||||||
|
-- Check if pmem has been initialized
|
||||||
|
if pmem(PMEM_INIT_SLOT) ~= PMEM_INIT_VALUE then
|
||||||
|
-- First run - save defaults
|
||||||
|
Settings.save()
|
||||||
|
pmem(PMEM_INIT_SLOT, PMEM_INIT_VALUE)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Load values from pmem
|
||||||
|
for _, item in ipairs(SETTINGS_ITEMS) do
|
||||||
|
local stored = pmem(item.pmem_slot)
|
||||||
|
if stored >= item.min and stored <= item.max then
|
||||||
|
set_config_value(item, stored)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings.save()
|
||||||
|
for _, item in ipairs(SETTINGS_ITEMS) do
|
||||||
|
local value = get_config_value(item)
|
||||||
|
pmem(item.pmem_slot, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings.update()
|
||||||
|
cls(COLOR_BLACK)
|
||||||
|
|
||||||
|
UI.print_shadow("Settings", 85, 4, COLOR_BLUE, false, 2)
|
||||||
|
|
||||||
|
local start_y = 22
|
||||||
|
local item_height = 11
|
||||||
|
|
||||||
|
for i, item in ipairs(SETTINGS_ITEMS) do
|
||||||
|
local y = start_y + (i - 1) * item_height
|
||||||
|
local is_selected = (State.settings_selection == i)
|
||||||
|
local color = is_selected and COLOR_CYAN or COLOR_GRAY_LIGHT
|
||||||
|
|
||||||
|
-- Cursor
|
||||||
|
if is_selected then
|
||||||
|
print("<", 16, y, COLOR_CYAN)
|
||||||
|
print(">", 221, y, COLOR_CYAN)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Label
|
||||||
|
print(item.label, 26, y, color)
|
||||||
|
|
||||||
|
-- Value
|
||||||
|
local value = get_config_value(item)
|
||||||
|
local display = get_display_value(item, value)
|
||||||
|
print(display, 161, y, COLOR_YELLOW)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Back option
|
||||||
|
local back_y = start_y + #SETTINGS_ITEMS * item_height + 4
|
||||||
|
local back_selected = (State.settings_selection == #SETTINGS_ITEMS + 1)
|
||||||
|
if back_selected then
|
||||||
|
print(">", 71, back_y, COLOR_CYAN)
|
||||||
|
end
|
||||||
|
print("Save & Back", 81, back_y, back_selected and COLOR_CYAN or COLOR_GRAY_LIGHT)
|
||||||
|
|
||||||
|
-- Instructions at bottom
|
||||||
|
print("UP/DOWN:select LEFT/RIGHT:change", 28, 128, COLOR_GRAY_LIGHT)
|
||||||
|
|
||||||
|
-- Input handling
|
||||||
|
local max_selection = #SETTINGS_ITEMS + 1
|
||||||
|
|
||||||
|
if Input.up_pressed() then
|
||||||
|
State.settings_selection = State.settings_selection - 1
|
||||||
|
if State.settings_selection < 1 then State.settings_selection = max_selection end
|
||||||
|
elseif Input.down_pressed() then
|
||||||
|
State.settings_selection = State.settings_selection + 1
|
||||||
|
if State.settings_selection > max_selection then State.settings_selection = 1 end
|
||||||
|
elseif Input.left_pressed() and State.settings_selection <= #SETTINGS_ITEMS then
|
||||||
|
local item = SETTINGS_ITEMS[State.settings_selection]
|
||||||
|
local value = get_config_value(item)
|
||||||
|
value = value - item.step
|
||||||
|
if value < item.min then value = item.max end
|
||||||
|
set_config_value(item, value)
|
||||||
|
elseif Input.right_pressed() and State.settings_selection <= #SETTINGS_ITEMS then
|
||||||
|
local item = SETTINGS_ITEMS[State.settings_selection]
|
||||||
|
local value = get_config_value(item)
|
||||||
|
value = value + item.step
|
||||||
|
if value > item.max then value = item.min end
|
||||||
|
set_config_value(item, value)
|
||||||
|
elseif Input.action_pressed() or Input.back_pressed() then
|
||||||
|
Settings.save()
|
||||||
|
State.settings_selection = 1
|
||||||
|
State.game_state = GAME_STATE_MENU
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- WinScreen module
|
-- WinScreen module
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -606,7 +980,7 @@ function Bomb.draw_explosions()
|
|||||||
if expl.spread <= 0 then
|
if expl.spread <= 0 then
|
||||||
rect(drawX, drawY, TILE_SIZE, TILE_SIZE, COLOR_RED)
|
rect(drawX, drawY, TILE_SIZE, TILE_SIZE, COLOR_RED)
|
||||||
else
|
else
|
||||||
local progress = 1 - (expl.spread / (expl.dist * SPREAD_DELAY))
|
local progress = 1 - (expl.spread / (expl.dist * Config.bomb.spread_delay))
|
||||||
if progress > 0 then
|
if progress > 0 then
|
||||||
local size = math.floor(TILE_SIZE * progress)
|
local size = math.floor(TILE_SIZE * progress)
|
||||||
local off = math.floor((TILE_SIZE - size) / 2)
|
local off = math.floor((TILE_SIZE - size) / 2)
|
||||||
@@ -631,20 +1005,59 @@ function Bomb.place(player)
|
|||||||
table.insert(State.bombs, {
|
table.insert(State.bombs, {
|
||||||
x = bombX,
|
x = bombX,
|
||||||
y = bombY,
|
y = bombY,
|
||||||
timer = BOMB_TIMER,
|
timer = Config.bomb.timer,
|
||||||
owner = player,
|
owner = player,
|
||||||
power = player.bombPower
|
power = player.bombPower
|
||||||
})
|
})
|
||||||
player.activeBombs = player.activeBombs + 1
|
player.activeBombs = player.activeBombs + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function spread_explosion(bombX, bombY, gridX, gridY, power, is_horizontal)
|
||||||
|
for _, dir in ipairs(SPREAD_DIRS) do
|
||||||
|
for dist = 1, power do
|
||||||
|
local explX, explY, eGridX, eGridY
|
||||||
|
if is_horizontal then
|
||||||
|
explX = bombX + dir * dist * TILE_SIZE
|
||||||
|
explY = bombY
|
||||||
|
eGridX = gridX + dir * dist
|
||||||
|
eGridY = gridY
|
||||||
|
if eGridX < 1 or eGridX > MAP_WIDTH then break end
|
||||||
|
else
|
||||||
|
explX = bombX
|
||||||
|
explY = bombY + dir * dist * TILE_SIZE
|
||||||
|
eGridX = gridX
|
||||||
|
eGridY = gridY + dir * dist
|
||||||
|
if eGridY < 1 or eGridY > MAP_HEIGHT then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
local tile = State.map[eGridY][eGridX]
|
||||||
|
if tile == SOLID_WALL then break end
|
||||||
|
|
||||||
|
local is_breakable = tile == BREAKABLE_WALL
|
||||||
|
if is_breakable then
|
||||||
|
State.map[eGridY][eGridX] = EMPTY
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(State.explosions, {
|
||||||
|
x = explX,
|
||||||
|
y = explY,
|
||||||
|
timer = Config.bomb.explosion_duration,
|
||||||
|
dist = dist,
|
||||||
|
spread = dist * Config.bomb.spread_delay
|
||||||
|
})
|
||||||
|
|
||||||
|
if is_breakable then break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Bomb.explode(bombX, bombY, power)
|
function Bomb.explode(bombX, bombY, power)
|
||||||
power = power or 1
|
power = power or 1
|
||||||
sfx(0, nil, 30)
|
Sound.play("explosion")
|
||||||
table.insert(State.explosions, {
|
table.insert(State.explosions, {
|
||||||
x = bombX,
|
x = bombX,
|
||||||
y = bombY,
|
y = bombY,
|
||||||
timer = EXPLOSION_TIMER,
|
timer = Config.bomb.explosion_duration,
|
||||||
dist = 0,
|
dist = 0,
|
||||||
spread = 0
|
spread = 0
|
||||||
})
|
})
|
||||||
@@ -652,63 +1065,8 @@ function Bomb.explode(bombX, bombY, power)
|
|||||||
local gridX = math.floor(bombX / TILE_SIZE) + 1
|
local gridX = math.floor(bombX / TILE_SIZE) + 1
|
||||||
local gridY = math.floor(bombY / TILE_SIZE) + 1
|
local gridY = math.floor(bombY / TILE_SIZE) + 1
|
||||||
|
|
||||||
-- horizontal explosion
|
spread_explosion(bombX, bombY, gridX, gridY, power, true) -- horizontal
|
||||||
for _, dir in ipairs({-1, 1}) do
|
spread_explosion(bombX, bombY, gridX, gridY, power, false) -- vertical
|
||||||
for dist = 1, power do
|
|
||||||
local explX = bombX + dir * dist * TILE_SIZE
|
|
||||||
local eGridX = gridX + dir * dist
|
|
||||||
if eGridX < 1 or eGridX > MAP_WIDTH then break end
|
|
||||||
local tile = State.map[gridY][eGridX]
|
|
||||||
if tile == SOLID_WALL then break end
|
|
||||||
if tile == BREAKABLE_WALL then
|
|
||||||
State.map[gridY][eGridX] = EMPTY
|
|
||||||
table.insert(State.explosions, {
|
|
||||||
x = explX,
|
|
||||||
y = bombY,
|
|
||||||
timer = EXPLOSION_TIMER,
|
|
||||||
dist = dist,
|
|
||||||
spread = dist * SPREAD_DELAY
|
|
||||||
})
|
|
||||||
break
|
|
||||||
end
|
|
||||||
table.insert(State.explosions, {
|
|
||||||
x = explX,
|
|
||||||
y = bombY,
|
|
||||||
timer = EXPLOSION_TIMER,
|
|
||||||
dist = dist,
|
|
||||||
spread = dist * SPREAD_DELAY
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- vertical explosion
|
|
||||||
for _, dir in ipairs({-1, 1}) do
|
|
||||||
for dist = 1, power do
|
|
||||||
local explY = bombY + dir * dist * TILE_SIZE
|
|
||||||
local eGridY = gridY + dir * dist
|
|
||||||
if eGridY < 1 or eGridY > MAP_HEIGHT then break end
|
|
||||||
local tile = State.map[eGridY][gridX]
|
|
||||||
if tile == SOLID_WALL then break end
|
|
||||||
if tile == BREAKABLE_WALL then
|
|
||||||
State.map[eGridY][gridX] = EMPTY
|
|
||||||
table.insert(State.explosions, {
|
|
||||||
x = bombX,
|
|
||||||
y = explY,
|
|
||||||
timer = EXPLOSION_TIMER,
|
|
||||||
dist = dist,
|
|
||||||
spread = dist * SPREAD_DELAY
|
|
||||||
})
|
|
||||||
break
|
|
||||||
end
|
|
||||||
table.insert(State.explosions, {
|
|
||||||
x = bombX,
|
|
||||||
y = explY,
|
|
||||||
timer = EXPLOSION_TIMER,
|
|
||||||
dist = dist,
|
|
||||||
spread = dist * SPREAD_DELAY
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Bomb.update_all()
|
function Bomb.update_all()
|
||||||
@@ -748,6 +1106,18 @@ end
|
|||||||
-- AI module
|
-- AI module
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function is_blast_line_blocked(pos1, pos2, fixedCoord, is_horizontal)
|
||||||
|
local minPos = math.min(pos1, pos2)
|
||||||
|
local maxPos = math.max(pos1, pos2)
|
||||||
|
for i = minPos + 1, maxPos - 1 do
|
||||||
|
local tile = is_horizontal and State.map[fixedCoord][i] or State.map[i][fixedCoord]
|
||||||
|
if tile == SOLID_WALL then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
function AI.is_dangerous(gridX, gridY)
|
function AI.is_dangerous(gridX, gridY)
|
||||||
-- Check active explosions
|
-- Check active explosions
|
||||||
for _, expl in ipairs(State.explosions) do
|
for _, expl in ipairs(State.explosions) do
|
||||||
@@ -758,44 +1128,31 @@ function AI.is_dangerous(gridX, gridY)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check bombs about to explode (timer < 30) - need to escape!
|
-- Check bombs about to explode - need to escape!
|
||||||
for _, bomb in ipairs(State.bombs) do
|
for _, bomb in ipairs(State.bombs) do
|
||||||
local bombGridX = math.floor(bomb.x / TILE_SIZE) + 1
|
local bombGridX = math.floor(bomb.x / TILE_SIZE) + 1
|
||||||
local bombGridY = math.floor(bomb.y / TILE_SIZE) + 1
|
local bombGridY = math.floor(bomb.y / TILE_SIZE) + 1
|
||||||
local power = bomb.power or 1
|
local power = bomb.power or 1
|
||||||
|
|
||||||
-- Only urgent if bomb is about to explode
|
-- Only urgent if bomb is about to explode
|
||||||
if bomb.timer < 30 then
|
if bomb.timer < Config.ai.danger_threshold then
|
||||||
if gridX == bombGridX and gridY == bombGridY then
|
if gridX == bombGridX and gridY == bombGridY then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check blast radius only for soon-to-explode bombs
|
-- Check horizontal blast radius
|
||||||
if gridY == bombGridY and math.abs(gridX - bombGridX) <= power then
|
if gridY == bombGridY and math.abs(gridX - bombGridX) <= power then
|
||||||
local blocked = false
|
if not is_blast_line_blocked(gridX, bombGridX, gridY, true) then
|
||||||
local minX = math.min(gridX, bombGridX)
|
return true
|
||||||
local maxX = math.max(gridX, bombGridX)
|
|
||||||
for x = minX + 1, maxX - 1 do
|
|
||||||
if State.map[gridY][x] == SOLID_WALL then
|
|
||||||
blocked = true
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not blocked then return true end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
-- Check vertical blast radius
|
||||||
if gridX == bombGridX and math.abs(gridY - bombGridY) <= power then
|
if gridX == bombGridX and math.abs(gridY - bombGridY) <= power then
|
||||||
local blocked = false
|
if not is_blast_line_blocked(gridY, bombGridY, gridX, false) then
|
||||||
local minY = math.min(gridY, bombGridY)
|
return true
|
||||||
local maxY = math.max(gridY, bombGridY)
|
|
||||||
for y = minY + 1, maxY - 1 do
|
|
||||||
if State.map[y][gridX] == SOLID_WALL then
|
|
||||||
blocked = true
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not blocked then return true end
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
-- For bombs with more time, just avoid the bomb cell itself
|
-- For bombs with more time, just avoid the bomb cell itself
|
||||||
if gridX == bombGridX and gridY == bombGridY then
|
if gridX == bombGridX and gridY == bombGridY then
|
||||||
@@ -898,7 +1255,10 @@ function AI.move_and_bomb(player, target)
|
|||||||
local pwDist = math.abs(powerup.gridX - player.gridX) + math.abs(powerup.gridY - player.gridY)
|
local pwDist = math.abs(powerup.gridX - player.gridX) + math.abs(powerup.gridY - player.gridY)
|
||||||
local targetDist = math.abs(target.gridX - player.gridX) + math.abs(target.gridY - player.gridY)
|
local targetDist = math.abs(target.gridX - player.gridX) + math.abs(target.gridY - player.gridY)
|
||||||
if pwDist < targetDist or pwDist <= 5 then
|
if pwDist < targetDist or pwDist <= 5 then
|
||||||
actualTarget = {gridX = powerup.gridX, gridY = powerup.gridY}
|
actualTarget = {
|
||||||
|
gridX = powerup.gridX,
|
||||||
|
gridY = powerup.gridY
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -917,7 +1277,7 @@ function AI.move_and_bomb(player, target)
|
|||||||
player.lastGridX = player.gridX
|
player.lastGridX = player.gridX
|
||||||
player.lastGridY = player.gridY
|
player.lastGridY = player.gridY
|
||||||
Bomb.place(player)
|
Bomb.place(player)
|
||||||
player.bombCooldown = AI_BOMB_COOLDOWN
|
player.bombCooldown = Config.ai.bomb_cooldown
|
||||||
AI.escape_from_bomb(player)
|
AI.escape_from_bomb(player)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -1009,7 +1369,7 @@ function AI.update(player, target)
|
|||||||
end
|
end
|
||||||
|
|
||||||
player.moveTimer = player.moveTimer + 1
|
player.moveTimer = player.moveTimer + 1
|
||||||
if player.moveTimer < AI_MOVE_DELAY then return end
|
if player.moveTimer < Config.ai.move_delay then return end
|
||||||
|
|
||||||
player.moveTimer = 0
|
player.moveTimer = 0
|
||||||
AI.move_and_bomb(player, target)
|
AI.move_and_bomb(player, target)
|
||||||
@@ -1033,9 +1393,9 @@ function Player.create(gridX, gridY, color, is_ai)
|
|||||||
pixelX = (gridX - 1) * TILE_SIZE,
|
pixelX = (gridX - 1) * TILE_SIZE,
|
||||||
pixelY = (gridY - 1) * TILE_SIZE,
|
pixelY = (gridY - 1) * TILE_SIZE,
|
||||||
moving = false,
|
moving = false,
|
||||||
maxBombs = 1,
|
maxBombs = Config.player.start_bombs,
|
||||||
activeBombs = 0,
|
activeBombs = 0,
|
||||||
bombPower = 1,
|
bombPower = Config.player.start_power,
|
||||||
color = color,
|
color = color,
|
||||||
is_ai = is_ai,
|
is_ai = is_ai,
|
||||||
moveTimer = 0,
|
moveTimer = 0,
|
||||||
@@ -1050,16 +1410,16 @@ function Player.update_movement(player)
|
|||||||
local targetY = (player.gridY - 1) * TILE_SIZE
|
local targetY = (player.gridY - 1) * TILE_SIZE
|
||||||
|
|
||||||
if player.pixelX < targetX then
|
if player.pixelX < targetX then
|
||||||
player.pixelX = math.min(player.pixelX + MOVE_SPEED, targetX)
|
player.pixelX = math.min(player.pixelX + Config.player.move_speed, targetX)
|
||||||
player.moving = true
|
player.moving = true
|
||||||
elseif player.pixelX > targetX then
|
elseif player.pixelX > targetX then
|
||||||
player.pixelX = math.max(player.pixelX - MOVE_SPEED, targetX)
|
player.pixelX = math.max(player.pixelX - Config.player.move_speed, targetX)
|
||||||
player.moving = true
|
player.moving = true
|
||||||
elseif player.pixelY < targetY then
|
elseif player.pixelY < targetY then
|
||||||
player.pixelY = math.min(player.pixelY + MOVE_SPEED, targetY)
|
player.pixelY = math.min(player.pixelY + Config.player.move_speed, targetY)
|
||||||
player.moving = true
|
player.moving = true
|
||||||
elseif player.pixelY > targetY then
|
elseif player.pixelY > targetY then
|
||||||
player.pixelY = math.max(player.pixelY - MOVE_SPEED, targetY)
|
player.pixelY = math.max(player.pixelY - Config.player.move_speed, targetY)
|
||||||
player.moving = true
|
player.moving = true
|
||||||
else
|
else
|
||||||
player.moving = false
|
player.moving = false
|
||||||
@@ -1119,9 +1479,9 @@ function Player.reset(player)
|
|||||||
player.pixelX = (player.spawnX - 1) * TILE_SIZE
|
player.pixelX = (player.spawnX - 1) * TILE_SIZE
|
||||||
player.pixelY = (player.spawnY - 1) * TILE_SIZE
|
player.pixelY = (player.spawnY - 1) * TILE_SIZE
|
||||||
player.moving = false
|
player.moving = false
|
||||||
player.maxBombs = 1
|
player.maxBombs = Config.player.start_bombs
|
||||||
player.activeBombs = 0
|
player.activeBombs = 0
|
||||||
player.bombPower = 1
|
player.bombPower = Config.player.start_power
|
||||||
player.bombCooldown = 0
|
player.bombCooldown = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1158,7 +1518,7 @@ end
|
|||||||
|
|
||||||
function Game.set_winner(player_num)
|
function Game.set_winner(player_num)
|
||||||
State.winner = player_num
|
State.winner = player_num
|
||||||
State.win_timer = WIN_SCREEN_DURATION
|
State.win_timer = Config.timing.win_screen_duration
|
||||||
State.score[player_num] = State.score[player_num] + 1
|
State.score[player_num] = State.score[player_num] + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1205,22 +1565,7 @@ end
|
|||||||
-- Main game loop
|
-- Main game loop
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
function TIC()
|
local function update_playing()
|
||||||
if State.game_state == GAME_STATE_SPLASH then
|
|
||||||
Splash.update()
|
|
||||||
return
|
|
||||||
elseif State.game_state == GAME_STATE_MENU then
|
|
||||||
Menu.update()
|
|
||||||
return
|
|
||||||
elseif State.game_state == GAME_STATE_HELP then
|
|
||||||
Help.update()
|
|
||||||
return
|
|
||||||
elseif State.game_state == GAME_STATE_CREDITS then
|
|
||||||
Credits.update()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- GAME_STATE_PLAYING
|
|
||||||
cls(COLOR_GREEN)
|
cls(COLOR_GREEN)
|
||||||
|
|
||||||
-- ESC to return to menu
|
-- ESC to return to menu
|
||||||
@@ -1243,6 +1588,22 @@ function TIC()
|
|||||||
GameBoard.draw()
|
GameBoard.draw()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local STATE_HANDLERS = {
|
||||||
|
[GAME_STATE_SPLASH] = Splash.update,
|
||||||
|
[GAME_STATE_MENU] = Menu.update,
|
||||||
|
[GAME_STATE_HELP] = Help.update,
|
||||||
|
[GAME_STATE_CREDITS] = Credits.update,
|
||||||
|
[GAME_STATE_SETTINGS] = Settings.update,
|
||||||
|
[GAME_STATE_PLAYING] = update_playing,
|
||||||
|
}
|
||||||
|
|
||||||
|
function TIC()
|
||||||
|
local handler = STATE_HANDLERS[State.game_state]
|
||||||
|
if handler then
|
||||||
|
handler()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- <TILES>
|
-- <TILES>
|
||||||
-- 001:eccccccccc888888caaaaaaaca888888cacccccccacc0ccccacc0ccccacc0ccc
|
-- 001:eccccccccc888888caaaaaaaca888888cacccccccacc0ccccacc0ccccacc0ccc
|
||||||
-- 002:ccccceee8888cceeaaaa0cee888a0ceeccca0ccc0cca0c0c0cca0c0c0cca0c0c
|
-- 002:ccccceee8888cceeaaaa0cee888a0ceeccca0ccc0cca0c0c0cca0c0c0cca0c0c
|
||||||
20
serve.py
20
serve.py
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Simple static file server for Bomberman HTML export."""
|
|
||||||
|
|
||||||
import http.server
|
|
||||||
import socketserver
|
|
||||||
import os
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
PORT = 3333
|
|
||||||
DIRECTORY = "bomberman"
|
|
||||||
|
|
||||||
os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), DIRECTORY))
|
|
||||||
|
|
||||||
Handler = http.server.SimpleHTTPRequestHandler
|
|
||||||
|
|
||||||
with socketserver.TCPServer(("", PORT), Handler) as httpd:
|
|
||||||
url = f"http://localhost:{PORT}"
|
|
||||||
print(f"Serving at {url}")
|
|
||||||
webbrowser.open(url)
|
|
||||||
httpd.serve_forever()
|
|
||||||
Reference in New Issue
Block a user