mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-08 20:07:44 -06:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e87d458121 | ||
|
|
44f0b5d373 | ||
|
|
c828034d18 | ||
|
|
823f480916 | ||
|
|
53306de155 | ||
|
|
6ddc6d1181 | ||
|
|
fff44faa05 | ||
|
|
ca17984d98 |
@@ -3,3 +3,10 @@ fail-fast = false
|
|||||||
|
|
||||||
[profile.coverage]
|
[profile.coverage]
|
||||||
status-level = "none"
|
status-level = "none"
|
||||||
|
|
||||||
|
[[profile.default.overrides]]
|
||||||
|
filter = 'test(pacman::game::)'
|
||||||
|
test-group = 'serial'
|
||||||
|
|
||||||
|
[test-groups]
|
||||||
|
serial = { max-threads = 1 }
|
||||||
|
|||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
|||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
scripts/* linguist-detectable=false
|
||||||
|
|||||||
15
.github/workflows/coverage.yaml
vendored
15
.github/workflows/coverage.yaml
vendored
@@ -4,7 +4,7 @@ on: ["push", "pull_request"]
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_TOOLCHAIN: 1.86.0
|
RUST_TOOLCHAIN: nightly
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
coverage:
|
coverage:
|
||||||
@@ -46,12 +46,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate coverage report
|
- name: Generate coverage report
|
||||||
run: |
|
run: |
|
||||||
just coverage-codecov
|
just coverage
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Coveralls upload
|
||||||
uses: codecov/codecov-action@v4
|
uses: coverallsapp/github-action@v2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
github-token: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||||
files: ./codecov.json
|
path-to-lcov: lcov.info
|
||||||
disable_search: true
|
debug: true
|
||||||
verbose: true
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -20,3 +20,7 @@ coverage.html
|
|||||||
# Profiling output
|
# Profiling output
|
||||||
flamegraph.svg
|
flamegraph.svg
|
||||||
/profile.*
|
/profile.*
|
||||||
|
|
||||||
|
# temporary
|
||||||
|
assets/game/sound/*.wav
|
||||||
|
/*.py
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ repos:
|
|||||||
- id: forbid-submodules
|
- id: forbid-submodules
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
|
|
||||||
|
- repo: https://github.com/compilerla/conventional-pre-commit
|
||||||
|
rev: v4.2.0
|
||||||
|
hooks:
|
||||||
|
- id: conventional-pre-commit
|
||||||
|
stages: [commit-msg]
|
||||||
|
args: []
|
||||||
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: cargo-fmt
|
- id: cargo-fmt
|
||||||
@@ -20,15 +27,31 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
types: [rust]
|
types: [rust]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
- id: cargo-check
|
- id: cargo-check
|
||||||
name: cargo check
|
name: cargo check
|
||||||
entry: cargo check --all-targets
|
entry: cargo check --all-targets
|
||||||
language: system
|
language: system
|
||||||
types_or: [rust, cargo, cargo-lock]
|
types_or: [rust, cargo, cargo-lock]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
- id: cargo-check-wasm
|
- id: cargo-check-wasm
|
||||||
name: cargo check for wasm32-unknown-emscripten
|
name: cargo check for wasm32-unknown-emscripten
|
||||||
entry: cargo check --all-targets --target=wasm32-unknown-emscripten
|
entry: cargo check --all-targets --target=wasm32-unknown-emscripten
|
||||||
language: system
|
language: system
|
||||||
types_or: [rust, cargo, cargo-lock]
|
types_or: [rust, cargo, cargo-lock]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
|
- id: bump-version
|
||||||
|
name: bump version based on commit message
|
||||||
|
entry: python scripts/bump-version.py
|
||||||
|
language: system
|
||||||
|
stages: [commit-msg]
|
||||||
|
always_run: true
|
||||||
|
|
||||||
|
- id: tag-version
|
||||||
|
name: tag version based on commit message
|
||||||
|
entry: python scripts/tag-version.py
|
||||||
|
language: system
|
||||||
|
stages: [post-commit]
|
||||||
|
always_run: true
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -663,7 +663,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pacman"
|
name = "pacman"
|
||||||
version = "0.2.0"
|
version = "0.77.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pacman"
|
name = "pacman"
|
||||||
version = "0.2.0"
|
version = "0.77.1"
|
||||||
authors = ["Xevion"]
|
authors = ["Xevion"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86.0"
|
rust-version = "1.86.0"
|
||||||
@@ -98,3 +98,6 @@ x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" }
|
|||||||
x86_64-unknown-linux-gnu = { triplet = "x64-linux" }
|
x86_64-unknown-linux-gnu = { triplet = "x64-linux" }
|
||||||
x86_64-apple-darwin = { triplet = "x64-osx" }
|
x86_64-apple-darwin = { triplet = "x64-osx" }
|
||||||
aarch64-apple-darwin = { triplet = "arm64-osx" }
|
aarch64-apple-darwin = { triplet = "arm64-osx" }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] }
|
||||||
|
|||||||
28
Justfile
28
Justfile
@@ -1,9 +1,6 @@
|
|||||||
set shell := ["bash", "-c"]
|
set shell := ["bash", "-c"]
|
||||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||||
|
|
||||||
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
|
|
||||||
# You can use src/..., but the filename alone is acceptable too
|
|
||||||
coverage_exclude_pattern := "src/app\\.rs|audio\\.rs|src/error\\.rs|platform/emscripten\\.rs|bin/.+\\.rs|main\\.rs|platform/desktop\\.rs|platform/tracing_buffer\\.rs|platform/buffered_writer\\.rs|systems/debug\\.rs|systems/profiling\\.rs"
|
|
||||||
|
|
||||||
binary_extension := if os() == "windows" { ".exe" } else { "" }
|
binary_extension := if os() == "windows" { ".exe" } else { "" }
|
||||||
|
|
||||||
@@ -11,39 +8,26 @@ binary_extension := if os() == "windows" { ".exe" } else { "" }
|
|||||||
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
|
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
|
||||||
|
|
||||||
# Generate HTML report (for humans, source line inspection)
|
# Generate HTML report (for humans, source line inspection)
|
||||||
html: coverage-lcov
|
html: coverage
|
||||||
cargo llvm-cov report \
|
cargo llvm-cov report \
|
||||||
--remap-path-prefix \
|
--remap-path-prefix \
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
|
||||||
--html \
|
--html \
|
||||||
--open
|
--open
|
||||||
|
|
||||||
# Display report (for humans)
|
# Display report (for humans)
|
||||||
report-coverage: coverage-lcov
|
report-coverage: coverage
|
||||||
cargo llvm-cov report \
|
cargo llvm-cov report --remap-path-prefix
|
||||||
--remap-path-prefix \
|
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
|
|
||||||
|
|
||||||
# Run & generate LCOV report (as base report)
|
# Run & generate LCOV report (as base report)
|
||||||
coverage-lcov:
|
coverage:
|
||||||
cargo llvm-cov \
|
cargo +nightly llvm-cov \
|
||||||
--lcov \
|
--lcov \
|
||||||
--remap-path-prefix \
|
--remap-path-prefix \
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
--workspace \
|
||||||
--output-path lcov.info \
|
--output-path lcov.info \
|
||||||
--profile coverage \
|
--profile coverage \
|
||||||
--no-fail-fast nextest
|
--no-fail-fast nextest
|
||||||
|
|
||||||
# Run & generate Codecov report (for CI)
|
|
||||||
coverage-codecov:
|
|
||||||
cargo llvm-cov \
|
|
||||||
--codecov \
|
|
||||||
--remap-path-prefix \
|
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
|
||||||
--output-path codecov.json \
|
|
||||||
--profile coverage \
|
|
||||||
--no-fail-fast nextest
|
|
||||||
|
|
||||||
# Profile the project using 'samply'
|
# Profile the project using 'samply'
|
||||||
samply:
|
samply:
|
||||||
cargo build --profile profile
|
cargo build --profile profile
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# Pac-Man
|
# Pac-Man
|
||||||
|
|
||||||
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
||||||
|
|
||||||
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
||||||
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
||||||
[badge-coverage]: https://codecov.io/github/Xevion/Pac-Man/branch/master/graph/badge.svg?token=R2RBYUQK3I
|
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
|
||||||
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
||||||
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
||||||
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
||||||
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
||||||
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
|
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
|
||||||
[coverage]: https://codecov.io/github/Xevion/Pac-Man
|
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
|
||||||
[demo]: https://xevion.github.io/Pac-Man/
|
[demo]: https://xevion.github.io/Pac-Man/
|
||||||
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
||||||
|
|
||||||
|
|||||||
27
bacon.toml
27
bacon.toml
@@ -28,16 +28,18 @@ need_stdout = false
|
|||||||
|
|
||||||
[jobs.test]
|
[jobs.test]
|
||||||
command = [
|
command = [
|
||||||
"cargo", "nextest", "run",
|
"cargo",
|
||||||
"--hide-progress-bar", "--failure-output", "final"
|
"nextest",
|
||||||
|
"run",
|
||||||
|
"--hide-progress-bar",
|
||||||
|
"--failure-output",
|
||||||
|
"final",
|
||||||
]
|
]
|
||||||
need_stdout = true
|
need_stdout = true
|
||||||
analyzer = "nextest"
|
analyzer = "nextest"
|
||||||
|
|
||||||
[jobs.coverage]
|
[jobs.coverage]
|
||||||
command = [
|
command = ["just", "report-coverage"]
|
||||||
"just", "report-coverage"
|
|
||||||
]
|
|
||||||
need_stdout = true
|
need_stdout = true
|
||||||
ignored_lines = [
|
ignored_lines = [
|
||||||
"info:",
|
"info:",
|
||||||
@@ -54,7 +56,7 @@ ignored_lines = [
|
|||||||
"\\s*Finished.+in \\d+",
|
"\\s*Finished.+in \\d+",
|
||||||
"\\s*Summary\\s+\\[",
|
"\\s*Summary\\s+\\[",
|
||||||
"\\s*Blocking",
|
"\\s*Blocking",
|
||||||
"Finished report saved to"
|
"Finished report saved to",
|
||||||
]
|
]
|
||||||
on_change_strategy = "wait_then_restart"
|
on_change_strategy = "wait_then_restart"
|
||||||
|
|
||||||
@@ -66,21 +68,26 @@ need_stdout = false
|
|||||||
[jobs.doc-open]
|
[jobs.doc-open]
|
||||||
command = ["cargo", "doc", "--no-deps", "--open"]
|
command = ["cargo", "doc", "--no-deps", "--open"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
on_success = "back" # so that we don't open the browser at each change
|
on_success = "back" # so that we don't open the browser at each change
|
||||||
|
|
||||||
[jobs.run]
|
[jobs.run]
|
||||||
command = [
|
command = ["cargo", "run"]
|
||||||
"cargo", "run",
|
|
||||||
]
|
|
||||||
need_stdout = true
|
need_stdout = true
|
||||||
allow_warnings = true
|
allow_warnings = true
|
||||||
background = false
|
background = false
|
||||||
on_change_strategy = "kill_then_restart"
|
on_change_strategy = "kill_then_restart"
|
||||||
# kill = ["pkill", "-TERM", "-P"]'
|
# kill = ["pkill", "-TERM", "-P"]'
|
||||||
|
|
||||||
|
[jobs.precommit]
|
||||||
|
command = ["pre-commit", "run", "--all-files"]
|
||||||
|
need_stdout = true
|
||||||
|
background = false
|
||||||
|
on_change_strategy = "kill_then_restart"
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
c = "job:clippy"
|
c = "job:clippy"
|
||||||
alt-c = "job:check"
|
alt-c = "job:check"
|
||||||
ctrl-alt-c = "job:check-all"
|
ctrl-alt-c = "job:check-all"
|
||||||
shift-c = "job:clippy-all"
|
shift-c = "job:clippy-all"
|
||||||
f = "job:coverage"
|
f = "job:coverage"
|
||||||
|
p = "job:precommit"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
ignore:
|
|
||||||
- "src/(?:bin|platform))/.+\\.rs"
|
|
||||||
- "src/(?:app|events|formatter)\\.rs"
|
|
||||||
143
scripts/bump-version.py
Normal file
143
scripts/bump-version.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Pre-commit hook script to automatically bump Cargo.toml version based on commit message.
|
||||||
|
|
||||||
|
This script parses the commit message for version bump keywords and uses cargo set-version
|
||||||
|
to update the version in Cargo.toml accordingly.
|
||||||
|
|
||||||
|
Supported keywords:
|
||||||
|
- "major" or "breaking": Bump major version (1.0.0 -> 2.0.0)
|
||||||
|
- "minor" or "feature": Bump minor version (1.0.0 -> 1.1.0)
|
||||||
|
- "patch" or "fix" or "bugfix": Bump patch version (1.0.0 -> 1.0.1)
|
||||||
|
|
||||||
|
Usage: python scripts/bump-version.py <commit_message_file>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_version():
|
||||||
|
"""Get the current version from Cargo.toml."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["cargo", "metadata", "--format-version", "1", "--no-deps"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse the JSON output to get version
|
||||||
|
import json
|
||||||
|
metadata = json.loads(result.stdout)
|
||||||
|
return metadata["packages"][0]["version"]
|
||||||
|
except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError) as e:
|
||||||
|
print(f"Error getting current version: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def bump_version(current_version, bump_type):
|
||||||
|
"""Calculate the new version based on bump type."""
|
||||||
|
try:
|
||||||
|
major, minor, patch = map(int, current_version.split('.'))
|
||||||
|
|
||||||
|
if bump_type == "major":
|
||||||
|
return f"{major + 1}.0.0"
|
||||||
|
elif bump_type == "minor":
|
||||||
|
return f"{major}.{minor + 1}.0"
|
||||||
|
elif bump_type == "patch":
|
||||||
|
return f"{major}.{minor}.{patch + 1}"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except ValueError:
|
||||||
|
print(f"Invalid version format: {current_version}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_version(new_version):
|
||||||
|
"""Set the new version using cargo set-version."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["cargo", "set-version", new_version],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
print(f"Successfully bumped version to {new_version}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error setting version: {e}", file=sys.stderr)
|
||||||
|
print(f"stdout: {e.stdout}", file=sys.stderr)
|
||||||
|
print(f"stderr: {e.stderr}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def parse_commit_message(commit_message_file):
|
||||||
|
"""Parse the commit message file for version bump keywords."""
|
||||||
|
try:
|
||||||
|
with open(commit_message_file, 'r', encoding='utf-8') as f:
|
||||||
|
message = f.read().lower()
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Commit message file not found: {commit_message_file}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading commit message: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check for version bump keywords
|
||||||
|
if re.search(r'\b(major|breaking)\b', message):
|
||||||
|
return "major"
|
||||||
|
elif re.search(r'\b(minor|feature)\b', message):
|
||||||
|
return "minor"
|
||||||
|
elif re.search(r'\b(patch|fix|bugfix)\b', message):
|
||||||
|
return "patch"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: python scripts/bump-version.py <commit_message_file>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
commit_message_file = sys.argv[1]
|
||||||
|
|
||||||
|
# Parse commit message for version bump type
|
||||||
|
bump_type = parse_commit_message(commit_message_file)
|
||||||
|
|
||||||
|
if not bump_type:
|
||||||
|
print("No version bump keywords found in commit message")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
print(f"Found version bump type: {bump_type}")
|
||||||
|
|
||||||
|
# Get current version
|
||||||
|
current_version = get_current_version()
|
||||||
|
if not current_version:
|
||||||
|
print("Failed to get current version", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Current version: {current_version}")
|
||||||
|
|
||||||
|
# Calculate new version
|
||||||
|
new_version = bump_version(current_version, bump_type)
|
||||||
|
if not new_version:
|
||||||
|
print("Failed to calculate new version", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"New version: {new_version}")
|
||||||
|
|
||||||
|
# Set the new version
|
||||||
|
if set_version(new_version):
|
||||||
|
print("Version bump completed successfully")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("Version bump failed", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
125
scripts/tag-version.py
Normal file
125
scripts/tag-version.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Post-commit hook script to automatically create git tags based on the version in Cargo.toml.
|
||||||
|
|
||||||
|
This script reads the current version from Cargo.toml and creates a git tag with that version.
|
||||||
|
It's designed to run after the version has been bumped by the bump-version.py script.
|
||||||
|
|
||||||
|
Usage: python scripts/tag-version.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def get_version_from_cargo_toml():
|
||||||
|
"""Get the current version from Cargo.toml."""
|
||||||
|
cargo_toml_path = Path("Cargo.toml")
|
||||||
|
|
||||||
|
if not cargo_toml_path.exists():
|
||||||
|
print("Cargo.toml not found", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(cargo_toml_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Look for version = "x.y.z" pattern
|
||||||
|
version_match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
||||||
|
|
||||||
|
if version_match:
|
||||||
|
return version_match.group(1)
|
||||||
|
else:
|
||||||
|
print("Could not find version in Cargo.toml", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading Cargo.toml: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_tags():
|
||||||
|
"""Get list of existing git tags."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "tag", "--list"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
return result.stdout.strip().split('\n') if result.stdout.strip() else []
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error getting git tags: {e}", file=sys.stderr)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def create_git_tag(version):
|
||||||
|
"""Create a git tag with the specified version."""
|
||||||
|
tag_name = f"v{version}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if tag already exists
|
||||||
|
existing_tags = get_existing_tags()
|
||||||
|
if tag_name in existing_tags:
|
||||||
|
print(f"Tag {tag_name} already exists, skipping")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Create the tag
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "tag", tag_name],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Successfully created tag: {tag_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error creating git tag: {e}", file=sys.stderr)
|
||||||
|
print(f"stdout: {e.stdout}", file=sys.stderr)
|
||||||
|
print(f"stderr: {e.stderr}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_git_repository():
|
||||||
|
"""Check if we're in a git repository."""
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["git", "rev-parse", "--git-dir"],
|
||||||
|
capture_output=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Check if we're in a git repository
|
||||||
|
if not is_git_repository():
|
||||||
|
print("Not in a git repository, skipping tag creation")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Get the current version from Cargo.toml
|
||||||
|
version = get_version_from_cargo_toml()
|
||||||
|
|
||||||
|
if not version:
|
||||||
|
print("Could not determine version, skipping tag creation")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
print(f"Current version: {version}")
|
||||||
|
|
||||||
|
# Create the git tag
|
||||||
|
if create_git_tag(version):
|
||||||
|
print("Tag creation completed successfully")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("Tag creation failed", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||||
|
#![cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use sdl2::event::Event;
|
use sdl2::event::Event;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||||
|
#![cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
|
||||||
use circular_buffer::CircularBuffer;
|
use circular_buffer::CircularBuffer;
|
||||||
use pacman::constants::CANVAS_SIZE;
|
use pacman::constants::CANVAS_SIZE;
|
||||||
use sdl2::event::Event;
|
use sdl2::event::Event;
|
||||||
|
|||||||
@@ -132,8 +132,6 @@ pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [
|
|||||||
pub mod startup {
|
pub mod startup {
|
||||||
/// Number of frames for the startup sequence (3 seconds at 60 FPS)
|
/// Number of frames for the startup sequence (3 seconds at 60 FPS)
|
||||||
pub const STARTUP_FRAMES: u32 = 60 * 3;
|
pub const STARTUP_FRAMES: u32 = 60 * 3;
|
||||||
/// Number of ticks per frame during startup
|
|
||||||
pub const STARTUP_TICKS_PER_FRAME: u32 = 60;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Game mechanics constants
|
/// Game mechanics constants
|
||||||
|
|||||||
195
src/game.rs
195
src/game.rs
@@ -9,32 +9,26 @@ use crate::error::{GameError, GameResult};
|
|||||||
use crate::events::GameEvent;
|
use crate::events::GameEvent;
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::map::direction::Direction;
|
use crate::map::direction::Direction;
|
||||||
use crate::systems::blinking::Blinking;
|
|
||||||
use crate::systems::components::{GhostAnimation, GhostState, LastAnimationState};
|
|
||||||
use crate::systems::movement::{BufferedDirection, Position, Velocity};
|
|
||||||
use crate::systems::profiling::{SystemId, Timing};
|
|
||||||
use crate::systems::render::touch_ui_render_system;
|
|
||||||
use crate::systems::render::RenderDirty;
|
|
||||||
use crate::systems::{
|
use crate::systems::{
|
||||||
self, combined_render_system, ghost_collision_system, present_system, Hidden, LinearAnimation, MovementModifiers, NodeId,
|
self, audio_system, blinking_system, collision_system, combined_render_system, directional_render_system,
|
||||||
TouchState,
|
dirty_render_system, eaten_ghost_system, ghost_collision_system, ghost_movement_system, ghost_state_system,
|
||||||
};
|
hud_render_system, item_system, linear_render_system, present_system, profile, touch_ui_render_system, AudioEvent,
|
||||||
use crate::systems::{
|
AudioResource, AudioState, BackbufferResource, Blinking, BufferedDirection, Collider, DebugState, DebugTextureResource,
|
||||||
audio_system, blinking_system, collision_system, directional_render_system, dirty_render_system, eaten_ghost_system,
|
DeltaTime, DirectionalAnimation, EntityType, Frozen, GameStage, Ghost, GhostAnimation, GhostAnimations, GhostBundle,
|
||||||
ghost_movement_system, ghost_state_system, hud_render_system, item_system, linear_render_system, profile, AudioEvent,
|
GhostCollider, GhostState, GlobalState, Hidden, ItemBundle, ItemCollider, LastAnimationState, LinearAnimation,
|
||||||
AudioResource, AudioState, BackbufferResource, Collider, DebugState, DebugTextureResource, DeltaTime, DirectionalAnimation,
|
MapTextureResource, MovementModifiers, NodeId, PacmanCollider, PlayerAnimation, PlayerBundle, PlayerControlled,
|
||||||
EntityType, Frozen, Ghost, GhostAnimations, GhostBundle, GhostCollider, GlobalState, ItemBundle, ItemCollider,
|
PlayerDeathAnimation, PlayerLives, Position, RenderDirty, Renderable, ScoreResource, StartupSequence, SystemId,
|
||||||
MapTextureResource, PacmanCollider, PlayerBundle, PlayerControlled, Renderable, ScoreResource, StartupSequence,
|
SystemTimings, Timing, TouchState, Velocity,
|
||||||
SystemTimings,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::texture::animated::{DirectionalTiles, TileSequence};
|
use crate::texture::animated::{DirectionalTiles, TileSequence};
|
||||||
use crate::texture::sprite::AtlasTile;
|
use crate::texture::sprite::AtlasTile;
|
||||||
use crate::texture::sprites::{FrightenedColor, GameSprite, GhostSprite, MazeSprite, PacmanSprite};
|
use crate::texture::sprites::{FrightenedColor, GameSprite, GhostSprite, MazeSprite, PacmanSprite};
|
||||||
|
use bevy_ecs::change_detection::DetectChanges;
|
||||||
use bevy_ecs::event::EventRegistry;
|
use bevy_ecs::event::EventRegistry;
|
||||||
use bevy_ecs::observer::Trigger;
|
use bevy_ecs::observer::Trigger;
|
||||||
use bevy_ecs::schedule::common_conditions::resource_changed;
|
use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule, SystemSet};
|
||||||
use bevy_ecs::schedule::{Condition, IntoScheduleConfigs, Schedule, SystemSet};
|
use bevy_ecs::system::{Local, Res, ResMut};
|
||||||
use bevy_ecs::system::{Local, ResMut};
|
|
||||||
use bevy_ecs::world::World;
|
use bevy_ecs::world::World;
|
||||||
use sdl2::event::EventType;
|
use sdl2::event::EventType;
|
||||||
use sdl2::image::LoadTexture;
|
use sdl2::image::LoadTexture;
|
||||||
@@ -54,7 +48,9 @@ use crate::{
|
|||||||
|
|
||||||
/// System set for all rendering systems to ensure they run after gameplay logic
|
/// System set for all rendering systems to ensure they run after gameplay logic
|
||||||
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
||||||
pub struct RenderSet;
|
enum RenderSet {
|
||||||
|
Animation,
|
||||||
|
}
|
||||||
|
|
||||||
/// Core game state manager built on the Bevy ECS architecture.
|
/// Core game state manager built on the Bevy ECS architecture.
|
||||||
///
|
///
|
||||||
@@ -112,6 +108,8 @@ impl Game {
|
|||||||
let (player_animation, player_start_sprite) = Self::create_player_animations(&atlas)?;
|
let (player_animation, player_start_sprite) = Self::create_player_animations(&atlas)?;
|
||||||
let player_bundle = Self::create_player_bundle(&map, player_animation, player_start_sprite);
|
let player_bundle = Self::create_player_bundle(&map, player_animation, player_start_sprite);
|
||||||
|
|
||||||
|
let death_animation = Self::create_death_animation(&atlas)?;
|
||||||
|
|
||||||
let mut world = World::default();
|
let mut world = World::default();
|
||||||
let mut schedule = Schedule::default();
|
let mut schedule = Schedule::default();
|
||||||
|
|
||||||
@@ -127,6 +125,7 @@ impl Game {
|
|||||||
map_texture,
|
map_texture,
|
||||||
debug_texture,
|
debug_texture,
|
||||||
ttf_atlas,
|
ttf_atlas,
|
||||||
|
death_animation,
|
||||||
)?;
|
)?;
|
||||||
Self::configure_schedule(&mut schedule);
|
Self::configure_schedule(&mut schedule);
|
||||||
|
|
||||||
@@ -310,6 +309,18 @@ impl Game {
|
|||||||
Ok((player_animation, player_start_sprite))
|
Ok((player_animation, player_start_sprite))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_death_animation(atlas: &SpriteAtlas) -> GameResult<LinearAnimation> {
|
||||||
|
let mut death_tiles = Vec::new();
|
||||||
|
for i in 0..=10 {
|
||||||
|
// Assuming death animation has 11 frames named pacman/die_0, pacman/die_1, etc.
|
||||||
|
let tile = atlas.get_tile(&GameSprite::Pacman(PacmanSprite::Dying(i)).to_path())?;
|
||||||
|
death_tiles.push(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tile_sequence = TileSequence::new(&death_tiles);
|
||||||
|
Ok(LinearAnimation::new(tile_sequence, 8)) // 8 ticks per frame, non-looping
|
||||||
|
}
|
||||||
|
|
||||||
fn create_player_bundle(map: &Map, player_animation: DirectionalAnimation, player_start_sprite: AtlasTile) -> PlayerBundle {
|
fn create_player_bundle(map: &Map, player_animation: DirectionalAnimation, player_start_sprite: AtlasTile) -> PlayerBundle {
|
||||||
PlayerBundle {
|
PlayerBundle {
|
||||||
player: PlayerControlled,
|
player: PlayerControlled,
|
||||||
@@ -361,13 +372,19 @@ impl Game {
|
|||||||
map_texture: sdl2::render::Texture,
|
map_texture: sdl2::render::Texture,
|
||||||
debug_texture: sdl2::render::Texture,
|
debug_texture: sdl2::render::Texture,
|
||||||
ttf_atlas: crate::texture::ttf::TtfAtlas,
|
ttf_atlas: crate::texture::ttf::TtfAtlas,
|
||||||
|
death_animation: LinearAnimation,
|
||||||
) -> GameResult<()> {
|
) -> GameResult<()> {
|
||||||
world.insert_non_send_resource(atlas);
|
world.insert_non_send_resource(atlas);
|
||||||
world.insert_resource(Self::create_ghost_animations(world.non_send_resource::<SpriteAtlas>())?);
|
world.insert_resource(Self::create_ghost_animations(world.non_send_resource::<SpriteAtlas>())?);
|
||||||
|
let player_animation = Self::create_player_animations(world.non_send_resource::<SpriteAtlas>())?.0;
|
||||||
|
world.insert_resource(PlayerAnimation(player_animation));
|
||||||
|
world.insert_resource(PlayerDeathAnimation(death_animation));
|
||||||
|
|
||||||
world.insert_resource(BatchedLinesResource::new(&map, constants::LARGE_SCALE));
|
world.insert_resource(BatchedLinesResource::new(&map, constants::LARGE_SCALE));
|
||||||
world.insert_resource(map);
|
world.insert_resource(map);
|
||||||
world.insert_resource(GlobalState { exit: false });
|
world.insert_resource(GlobalState { exit: false });
|
||||||
|
world.insert_resource(GameStage::default());
|
||||||
|
world.insert_resource(PlayerLives::default());
|
||||||
world.insert_resource(ScoreResource(0));
|
world.insert_resource(ScoreResource(0));
|
||||||
world.insert_resource(SystemTimings::default());
|
world.insert_resource(SystemTimings::default());
|
||||||
world.insert_resource(Timing::default());
|
world.insert_resource(Timing::default());
|
||||||
@@ -378,10 +395,9 @@ impl Game {
|
|||||||
world.insert_resource(AudioState::default());
|
world.insert_resource(AudioState::default());
|
||||||
world.insert_resource(CursorPosition::default());
|
world.insert_resource(CursorPosition::default());
|
||||||
world.insert_resource(TouchState::default());
|
world.insert_resource(TouchState::default());
|
||||||
world.insert_resource(StartupSequence::new(
|
world.insert_resource(GameStage::Starting(StartupSequence::TextOnly {
|
||||||
constants::startup::STARTUP_FRAMES,
|
remaining_ticks: constants::startup::STARTUP_FRAMES,
|
||||||
constants::startup::STARTUP_TICKS_PER_FRAME,
|
}));
|
||||||
));
|
|
||||||
|
|
||||||
world.insert_non_send_resource(event_pump);
|
world.insert_non_send_resource(event_pump);
|
||||||
world.insert_non_send_resource::<&mut Canvas<Window>>(Box::leak(Box::new(canvas)));
|
world.insert_non_send_resource::<&mut Canvas<Window>>(Box::leak(Box::new(canvas)));
|
||||||
@@ -394,15 +410,14 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn configure_schedule(schedule: &mut Schedule) {
|
fn configure_schedule(schedule: &mut Schedule) {
|
||||||
|
let stage_system = profile(SystemId::Stage, systems::stage_system);
|
||||||
let input_system = profile(SystemId::Input, systems::input::input_system);
|
let input_system = profile(SystemId::Input, systems::input::input_system);
|
||||||
let player_control_system = profile(SystemId::PlayerControls, systems::player_control_system);
|
let player_control_system = profile(SystemId::PlayerControls, systems::player_control_system);
|
||||||
let player_movement_system = profile(SystemId::PlayerMovement, systems::player_movement_system);
|
let player_movement_system = profile(SystemId::PlayerMovement, systems::player_movement_system);
|
||||||
let startup_stage_system = profile(SystemId::Stage, systems::startup_stage_system);
|
|
||||||
let player_tunnel_slowdown_system = profile(SystemId::PlayerMovement, systems::player::player_tunnel_slowdown_system);
|
let player_tunnel_slowdown_system = profile(SystemId::PlayerMovement, systems::player::player_tunnel_slowdown_system);
|
||||||
let ghost_movement_system = profile(SystemId::Ghost, ghost_movement_system);
|
let ghost_movement_system = profile(SystemId::Ghost, ghost_movement_system);
|
||||||
let collision_system = profile(SystemId::Collision, collision_system);
|
let collision_system = profile(SystemId::Collision, collision_system);
|
||||||
let ghost_collision_system = profile(SystemId::GhostCollision, ghost_collision_system);
|
let ghost_collision_system = profile(SystemId::GhostCollision, ghost_collision_system);
|
||||||
|
|
||||||
let item_system = profile(SystemId::Item, item_system);
|
let item_system = profile(SystemId::Item, item_system);
|
||||||
let audio_system = profile(SystemId::Audio, audio_system);
|
let audio_system = profile(SystemId::Audio, audio_system);
|
||||||
let blinking_system = profile(SystemId::Blinking, blinking_system);
|
let blinking_system = profile(SystemId::Blinking, blinking_system);
|
||||||
@@ -412,41 +427,55 @@ impl Game {
|
|||||||
let hud_render_system = profile(SystemId::HudRender, hud_render_system);
|
let hud_render_system = profile(SystemId::HudRender, hud_render_system);
|
||||||
let present_system = profile(SystemId::Present, present_system);
|
let present_system = profile(SystemId::Present, present_system);
|
||||||
let unified_ghost_state_system = profile(SystemId::GhostStateAnimation, ghost_state_system);
|
let unified_ghost_state_system = profile(SystemId::GhostStateAnimation, ghost_state_system);
|
||||||
|
// let death_sequence_system = profile(SystemId::DeathSequence, death_sequence_system);
|
||||||
|
// let game_over_system = profile(SystemId::GameOver, systems::game_over_system);
|
||||||
|
let eaten_ghost_system = profile(SystemId::EatenGhost, eaten_ghost_system);
|
||||||
|
|
||||||
let forced_dirty_system = |mut dirty: ResMut<RenderDirty>| {
|
let forced_dirty_system = |mut dirty: ResMut<RenderDirty>| {
|
||||||
dirty.0 = true;
|
dirty.0 = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
schedule.add_systems((
|
schedule.add_systems(
|
||||||
forced_dirty_system.run_if(resource_changed::<ScoreResource>.or(resource_changed::<StartupSequence>)),
|
forced_dirty_system
|
||||||
(
|
.run_if(|score: Res<ScoreResource>, stage: Res<GameStage>| score.is_changed() || stage.is_changed()),
|
||||||
input_system.run_if(|mut local: Local<u8>| {
|
);
|
||||||
*local = local.wrapping_add(1u8);
|
|
||||||
// run every nth frame
|
// Input system should always run to prevent SDL event pump from blocking
|
||||||
*local % 2 == 0
|
let input_systems = (
|
||||||
}),
|
input_system.run_if(|mut local: Local<u8>| {
|
||||||
player_control_system,
|
*local = local.wrapping_add(1u8);
|
||||||
player_movement_system,
|
// run every nth frame
|
||||||
startup_stage_system,
|
*local % 2 == 0
|
||||||
)
|
}),
|
||||||
.chain(),
|
player_control_system,
|
||||||
player_tunnel_slowdown_system,
|
)
|
||||||
ghost_movement_system,
|
.chain();
|
||||||
profile(SystemId::EatenGhost, eaten_ghost_system),
|
|
||||||
unified_ghost_state_system,
|
let gameplay_systems = (
|
||||||
|
(player_movement_system, player_tunnel_slowdown_system, ghost_movement_system).chain(),
|
||||||
|
eaten_ghost_system,
|
||||||
(collision_system, ghost_collision_system, item_system).chain(),
|
(collision_system, ghost_collision_system, item_system).chain(),
|
||||||
audio_system,
|
unified_ghost_state_system,
|
||||||
blinking_system,
|
)
|
||||||
|
.chain()
|
||||||
|
.run_if(|game_state: Res<GameStage>| matches!(*game_state, GameStage::Playing));
|
||||||
|
|
||||||
|
schedule.add_systems((blinking_system, directional_render_system, linear_render_system).in_set(RenderSet::Animation));
|
||||||
|
|
||||||
|
schedule.add_systems((
|
||||||
|
stage_system,
|
||||||
|
input_systems,
|
||||||
|
gameplay_systems,
|
||||||
(
|
(
|
||||||
directional_render_system,
|
|
||||||
linear_render_system,
|
|
||||||
dirty_render_system,
|
dirty_render_system,
|
||||||
combined_render_system,
|
combined_render_system,
|
||||||
hud_render_system,
|
hud_render_system,
|
||||||
touch_ui_render_system,
|
touch_ui_render_system,
|
||||||
present_system,
|
present_system,
|
||||||
)
|
)
|
||||||
.chain(),
|
.chain()
|
||||||
|
.after(RenderSet::Animation),
|
||||||
|
audio_system,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +541,7 @@ impl Game {
|
|||||||
for (ghost_type, start_node) in ghost_start_positions {
|
for (ghost_type, start_node) in ghost_start_positions {
|
||||||
// Create the ghost bundle in a separate scope to manage borrows
|
// Create the ghost bundle in a separate scope to manage borrows
|
||||||
let ghost = {
|
let ghost = {
|
||||||
let animations = *world.resource::<GhostAnimations>().get_normal(&ghost_type).unwrap();
|
let animations = world.resource::<GhostAnimations>().get_normal(&ghost_type).unwrap().clone();
|
||||||
let atlas = world.non_send_resource::<SpriteAtlas>();
|
let atlas = world.non_send_resource::<SpriteAtlas>();
|
||||||
let sprite_path = GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Left, 0)).to_path();
|
let sprite_path = GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Left, 0)).to_path();
|
||||||
|
|
||||||
@@ -557,7 +586,7 @@ impl Game {
|
|||||||
TileSequence::new(&[left_eye]),
|
TileSequence::new(&[left_eye]),
|
||||||
TileSequence::new(&[right_eye]),
|
TileSequence::new(&[right_eye]),
|
||||||
);
|
);
|
||||||
let eyes = DirectionalAnimation::new(eyes_tiles, eyes_tiles, animation::GHOST_EATEN_SPEED);
|
let eyes = DirectionalAnimation::new(eyes_tiles.clone(), eyes_tiles, animation::GHOST_EATEN_SPEED);
|
||||||
|
|
||||||
let mut animations = HashMap::new();
|
let mut animations = HashMap::new();
|
||||||
|
|
||||||
@@ -586,7 +615,7 @@ impl Game {
|
|||||||
TileSequence::new(&left_tiles),
|
TileSequence::new(&left_tiles),
|
||||||
TileSequence::new(&right_tiles),
|
TileSequence::new(&right_tiles),
|
||||||
);
|
);
|
||||||
let normal = DirectionalAnimation::new(normal_moving, normal_moving, animation::GHOST_NORMAL_SPEED);
|
let normal = DirectionalAnimation::new(normal_moving.clone(), normal_moving, animation::GHOST_NORMAL_SPEED);
|
||||||
|
|
||||||
animations.insert(ghost_type, normal);
|
animations.insert(ghost_type, normal);
|
||||||
}
|
}
|
||||||
@@ -658,68 +687,4 @@ impl Game {
|
|||||||
|
|
||||||
state.exit
|
state.exit
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// Renders pathfinding debug lines from each ghost to Pac-Man.
|
|
||||||
// ///
|
|
||||||
// /// Each ghost's path is drawn in its respective color with a small offset
|
|
||||||
// /// to prevent overlapping lines.
|
|
||||||
// fn render_pathfinding_debug<T: sdl2::render::RenderTarget>(&self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
|
||||||
// let pacman_node = self.state.pacman.current_node_id();
|
|
||||||
|
|
||||||
// for ghost in self.state.ghosts.iter() {
|
|
||||||
// if let Ok(path) = ghost.calculate_path_to_target(&self.state.map.graph, pacman_node) {
|
|
||||||
// if path.len() < 2 {
|
|
||||||
// continue; // Skip if path is too short
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Set the ghost's color
|
|
||||||
// canvas.set_draw_color(ghost.debug_color());
|
|
||||||
|
|
||||||
// // Calculate offset based on ghost index to prevent overlapping lines
|
|
||||||
// // let offset = (i as f32) * 2.0 - 3.0; // Offset range: -3.0 to 3.0
|
|
||||||
|
|
||||||
// // Calculate a consistent offset direction for the entire path
|
|
||||||
// // let first_node = self.map.graph.get_node(path[0]).unwrap();
|
|
||||||
// // let last_node = self.map.graph.get_node(path[path.len() - 1]).unwrap();
|
|
||||||
|
|
||||||
// // Use the overall direction from start to end to determine the perpendicular offset
|
|
||||||
// let offset = match ghost.ghost_type {
|
|
||||||
// GhostType::Blinky => glam::Vec2::new(0.25, 0.5),
|
|
||||||
// GhostType::Pinky => glam::Vec2::new(-0.25, -0.25),
|
|
||||||
// GhostType::Inky => glam::Vec2::new(0.5, -0.5),
|
|
||||||
// GhostType::Clyde => glam::Vec2::new(-0.5, 0.25),
|
|
||||||
// } * 5.0;
|
|
||||||
|
|
||||||
// // Calculate offset positions for all nodes using the same perpendicular direction
|
|
||||||
// let mut offset_positions = Vec::new();
|
|
||||||
// for &node_id in &path {
|
|
||||||
// let node = self
|
|
||||||
// .state
|
|
||||||
// .map
|
|
||||||
// .graph
|
|
||||||
// .get_node(node_id)
|
|
||||||
// .ok_or(crate::error::EntityError::NodeNotFound(node_id))?;
|
|
||||||
// let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
|
||||||
// offset_positions.push(pos + offset);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Draw lines between the offset positions
|
|
||||||
// for window in offset_positions.windows(2) {
|
|
||||||
// if let (Some(from), Some(to)) = (window.first(), window.get(1)) {
|
|
||||||
// // Skip if the distance is too far (used for preventing lines between tunnel portals)
|
|
||||||
// if from.distance_squared(*to) > (crate::constants::CELL_SIZE * 16).pow(2) as f32 {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Draw the line
|
|
||||||
// canvas
|
|
||||||
// .draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32))
|
|
||||||
// .map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/lib.rs
14
src/lib.rs
@@ -1,14 +1,22 @@
|
|||||||
//! Pac-Man game library crate.
|
//! Pac-Man game library crate.
|
||||||
|
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||||
|
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod asset;
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod constants;
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod formatter;
|
pub mod formatter;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
pub mod platform;
|
||||||
|
|
||||||
|
pub mod asset;
|
||||||
|
pub mod constants;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod platform;
|
|
||||||
pub mod systems;
|
pub mod systems;
|
||||||
pub mod texture;
|
pub mod texture;
|
||||||
|
|||||||
16
src/main.rs
16
src/main.rs
@@ -1,20 +1,27 @@
|
|||||||
// Note: This disables the console window on Windows. We manually re-attach to the parent terminal or process later on.
|
// Note: This disables the console window on Windows. We manually re-attach to the parent terminal or process later on.
|
||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||||
|
|
||||||
use crate::{app::App, constants::LOOP_TIME};
|
use crate::{app::App, constants::LOOP_TIME};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
mod app;
|
mod app;
|
||||||
mod asset;
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
mod audio;
|
mod audio;
|
||||||
mod constants;
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
mod events;
|
mod events;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
mod formatter;
|
mod formatter;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
mod platform;
|
||||||
|
|
||||||
|
mod asset;
|
||||||
|
mod constants;
|
||||||
mod game;
|
mod game;
|
||||||
mod map;
|
mod map;
|
||||||
mod platform;
|
|
||||||
mod systems;
|
mod systems;
|
||||||
mod texture;
|
mod texture;
|
||||||
|
|
||||||
@@ -22,6 +29,7 @@ mod texture;
|
|||||||
///
|
///
|
||||||
/// This function initializes SDL, the window, the game state, and then enters
|
/// This function initializes SDL, the window, the game state, and then enters
|
||||||
/// the main game loop.
|
/// the main game loop.
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
// On Windows, this connects output streams to the console dynamically
|
// On Windows, this connects output streams to the console dynamically
|
||||||
// On Emscripten, this connects the subscriber to the browser console
|
// On Emscripten, this connects the subscriber to the browser console
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
use bevy_ecs::component::Component;
|
use bevy_ecs::{
|
||||||
use bevy_ecs::entity::Entity;
|
component::Component,
|
||||||
use bevy_ecs::event::{EventReader, EventWriter};
|
entity::Entity,
|
||||||
use bevy_ecs::query::With;
|
event::{EventReader, EventWriter},
|
||||||
use bevy_ecs::system::{Query, Res, ResMut};
|
query::With,
|
||||||
|
system::{Commands, Query, Res, ResMut},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::error::GameError;
|
use crate::error::GameError;
|
||||||
use crate::events::GameEvent;
|
use crate::events::GameEvent;
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::movement::Position;
|
use crate::systems::{
|
||||||
use crate::systems::{AudioEvent, Ghost, GhostState, PlayerControlled, ScoreResource};
|
components::GhostState, movement::Position, AudioEvent, DyingSequence, Frozen, GameStage, Ghost, PlayerControlled,
|
||||||
|
ScoreResource,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A component for defining the collision area of an entity.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Collider {
|
pub struct Collider {
|
||||||
pub size: f32,
|
pub size: f32,
|
||||||
@@ -62,6 +67,7 @@ pub fn check_collision(
|
|||||||
///
|
///
|
||||||
/// Also detects collisions between Pac-Man and ghosts for gameplay mechanics like
|
/// Also detects collisions between Pac-Man and ghosts for gameplay mechanics like
|
||||||
/// power pellet effects, ghost eating, and player death.
|
/// power pellet effects, ghost eating, and player death.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn collision_system(
|
pub fn collision_system(
|
||||||
map: Res<Map>,
|
map: Res<Map>,
|
||||||
pacman_query: Query<(Entity, &Position, &Collider), With<PacmanCollider>>,
|
pacman_query: Query<(Entity, &Position, &Collider), With<PacmanCollider>>,
|
||||||
@@ -107,10 +113,13 @@ pub fn collision_system(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn ghost_collision_system(
|
pub fn ghost_collision_system(
|
||||||
|
mut commands: Commands,
|
||||||
mut collision_events: EventReader<GameEvent>,
|
mut collision_events: EventReader<GameEvent>,
|
||||||
mut score: ResMut<ScoreResource>,
|
mut score: ResMut<ScoreResource>,
|
||||||
pacman_query: Query<(), With<PlayerControlled>>,
|
mut game_state: ResMut<GameStage>,
|
||||||
|
pacman_query: Query<Entity, With<PlayerControlled>>,
|
||||||
ghost_query: Query<(Entity, &Ghost), With<GhostCollider>>,
|
ghost_query: Query<(Entity, &Ghost), With<GhostCollider>>,
|
||||||
mut ghost_state_query: Query<&mut GhostState>,
|
mut ghost_state_query: Query<&mut GhostState>,
|
||||||
mut events: EventWriter<AudioEvent>,
|
mut events: EventWriter<AudioEvent>,
|
||||||
@@ -118,7 +127,7 @@ pub fn ghost_collision_system(
|
|||||||
for event in collision_events.read() {
|
for event in collision_events.read() {
|
||||||
if let GameEvent::Collision(entity1, entity2) = event {
|
if let GameEvent::Collision(entity1, entity2) = event {
|
||||||
// Check if one is Pacman and the other is a ghost
|
// Check if one is Pacman and the other is a ghost
|
||||||
let (_pacman_entity, ghost_entity) = if pacman_query.get(*entity1).is_ok() && ghost_query.get(*entity2).is_ok() {
|
let (pacman_entity, ghost_entity) = if pacman_query.get(*entity1).is_ok() && ghost_query.get(*entity2).is_ok() {
|
||||||
(*entity1, *entity2)
|
(*entity1, *entity2)
|
||||||
} else if pacman_query.get(*entity2).is_ok() && ghost_query.get(*entity1).is_ok() {
|
} else if pacman_query.get(*entity2).is_ok() && ghost_query.get(*entity1).is_ok() {
|
||||||
(*entity2, *entity1)
|
(*entity2, *entity1)
|
||||||
@@ -140,8 +149,12 @@ pub fn ghost_collision_system(
|
|||||||
|
|
||||||
// Play eat sound
|
// Play eat sound
|
||||||
events.write(AudioEvent::PlayEat);
|
events.write(AudioEvent::PlayEat);
|
||||||
} else {
|
} else if matches!(*ghost_state, GhostState::Normal) {
|
||||||
// Pac-Man dies (this would need a death system)
|
// Pac-Man dies
|
||||||
|
*game_state = GameStage::PlayerDying(DyingSequence::Frozen { remaining_ticks: 60 });
|
||||||
|
commands.entity(pacman_entity).insert(Frozen);
|
||||||
|
commands.entity(ghost_entity).insert(Frozen);
|
||||||
|
events.write(AudioEvent::StopAll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ pub struct Renderable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Directional animation component with shared timing across all directions
|
/// Directional animation component with shared timing across all directions
|
||||||
#[derive(Component, Clone, Copy)]
|
#[derive(Component, Clone)]
|
||||||
pub struct DirectionalAnimation {
|
pub struct DirectionalAnimation {
|
||||||
pub moving_tiles: DirectionalTiles,
|
pub moving_tiles: DirectionalTiles,
|
||||||
pub stopped_tiles: DirectionalTiles,
|
pub stopped_tiles: DirectionalTiles,
|
||||||
@@ -123,13 +123,18 @@ impl DirectionalAnimation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tag component to mark animations that should loop when they reach the end
|
||||||
|
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Looping;
|
||||||
|
|
||||||
/// Linear animation component for non-directional animations (frightened ghosts)
|
/// Linear animation component for non-directional animations (frightened ghosts)
|
||||||
#[derive(Component, Clone, Copy)]
|
#[derive(Component, Resource, Clone)]
|
||||||
pub struct LinearAnimation {
|
pub struct LinearAnimation {
|
||||||
pub tiles: TileSequence,
|
pub tiles: TileSequence,
|
||||||
pub current_frame: usize,
|
pub current_frame: usize,
|
||||||
pub time_bank: u16,
|
pub time_bank: u16,
|
||||||
pub frame_duration: u16,
|
pub frame_duration: u16,
|
||||||
|
pub finished: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LinearAnimation {
|
impl LinearAnimation {
|
||||||
@@ -140,6 +145,7 @@ impl LinearAnimation {
|
|||||||
current_frame: 0,
|
current_frame: 0,
|
||||||
time_bank: 0,
|
time_bank: 0,
|
||||||
frame_duration,
|
frame_duration,
|
||||||
|
finished: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +224,11 @@ pub struct Frozen;
|
|||||||
#[derive(Component, Debug, Clone, Copy)]
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
pub struct Eaten;
|
pub struct Eaten;
|
||||||
|
|
||||||
|
/// Tag component for Pac-Man during his death animation.
|
||||||
|
/// This is mainly because the Frozen tag would stop both movement and animation, while the Dying tag can signal that the animation should continue despite being frozen.
|
||||||
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
|
pub struct Dying;
|
||||||
|
|
||||||
#[derive(Component, Debug, Clone, Copy)]
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
pub enum GhostState {
|
pub enum GhostState {
|
||||||
/// Normal ghost behavior - chasing Pac-Man
|
/// Normal ghost behavior - chasing Pac-Man
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//! Debug rendering system
|
//! Debug rendering system
|
||||||
use std::cmp::Ordering;
|
#[cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||||
|
|
||||||
use crate::constants::{self, BOARD_PIXEL_OFFSET};
|
use crate::constants::{self, BOARD_PIXEL_OFFSET};
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings};
|
use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings};
|
||||||
@@ -13,6 +12,7 @@ use sdl2::rect::{Point, Rect};
|
|||||||
use sdl2::render::{Canvas, Texture};
|
use sdl2::render::{Canvas, Texture};
|
||||||
use sdl2::video::Window;
|
use sdl2::video::Window;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
@@ -149,6 +149,7 @@ fn transform_position_with_offset(pos: Vec2, scale: f32) -> IVec2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Renders timing information in the top-left corner of the screen using the debug text atlas
|
/// Renders timing information in the top-left corner of the screen using the debug text atlas
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
fn render_timing_display(
|
fn render_timing_display(
|
||||||
canvas: &mut Canvas<Window>,
|
canvas: &mut Canvas<Window>,
|
||||||
timings: &SystemTimings,
|
timings: &SystemTimings,
|
||||||
@@ -203,6 +204,7 @@ fn render_timing_display(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub fn debug_render_system(
|
pub fn debug_render_system(
|
||||||
canvas: &mut Canvas<Window>,
|
canvas: &mut Canvas<Window>,
|
||||||
ttf_atlas: &mut TtfAtlasResource,
|
ttf_atlas: &mut TtfAtlasResource,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::platform;
|
use crate::platform;
|
||||||
use crate::systems::components::{DirectionalAnimation, Frozen, GhostAnimation, GhostState, LastAnimationState, LinearAnimation};
|
use crate::systems::components::{
|
||||||
|
DirectionalAnimation, Frozen, GhostAnimation, GhostState, LastAnimationState, LinearAnimation, Looping,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
map::{
|
map::{
|
||||||
builder::Map,
|
builder::Map,
|
||||||
@@ -194,22 +196,26 @@ pub fn ghost_state_system(
|
|||||||
if last_animation_state.0 != current_animation_state {
|
if last_animation_state.0 != current_animation_state {
|
||||||
match current_animation_state {
|
match current_animation_state {
|
||||||
GhostAnimation::Frightened { flash } => {
|
GhostAnimation::Frightened { flash } => {
|
||||||
// Remove DirectionalAnimation, add LinearAnimation
|
// Remove DirectionalAnimation, add LinearAnimation with Looping component
|
||||||
commands
|
commands
|
||||||
.entity(entity)
|
.entity(entity)
|
||||||
.remove::<DirectionalAnimation>()
|
.remove::<DirectionalAnimation>()
|
||||||
.insert(*animations.frightened(flash));
|
.insert(animations.frightened(flash).clone())
|
||||||
|
.insert(Looping);
|
||||||
}
|
}
|
||||||
GhostAnimation::Normal => {
|
GhostAnimation::Normal => {
|
||||||
// Remove LinearAnimation, add DirectionalAnimation
|
// Remove LinearAnimation and Looping, add DirectionalAnimation
|
||||||
commands
|
commands
|
||||||
.entity(entity)
|
.entity(entity)
|
||||||
.remove::<LinearAnimation>()
|
.remove::<(LinearAnimation, Looping)>()
|
||||||
.insert(*animations.get_normal(ghost_type).unwrap());
|
.insert(animations.get_normal(ghost_type).unwrap().clone());
|
||||||
}
|
}
|
||||||
GhostAnimation::Eyes => {
|
GhostAnimation::Eyes => {
|
||||||
// Remove LinearAnimation, add DirectionalAnimation (eyes animation)
|
// Remove LinearAnimation and Looping, add DirectionalAnimation (eyes animation)
|
||||||
commands.entity(entity).remove::<LinearAnimation>().insert(*animations.eyes());
|
commands
|
||||||
|
.entity(entity)
|
||||||
|
.remove::<(LinearAnimation, Looping)>()
|
||||||
|
.insert(animations.eyes().clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
last_animation_state.0 = current_animation_state;
|
last_animation_state.0 = current_animation_state;
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
//! The Entity-Component-System (ECS) module.
|
//! This module contains all the systems in the game.
|
||||||
//!
|
|
||||||
//! This module contains all the ECS-related logic, including components, systems,
|
|
||||||
//! and resources.
|
|
||||||
|
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
pub mod debug;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
pub mod profiling;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
pub mod render;
|
||||||
|
|
||||||
pub mod blinking;
|
pub mod blinking;
|
||||||
pub mod collision;
|
pub mod collision;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod debug;
|
|
||||||
pub mod ghost;
|
pub mod ghost;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod profiling;
|
pub mod state;
|
||||||
pub mod render;
|
|
||||||
pub mod stage;
|
// Re-export all the modules. Do not fine-tune the exports.
|
||||||
|
|
||||||
pub use self::audio::*;
|
pub use self::audio::*;
|
||||||
pub use self::blinking::*;
|
pub use self::blinking::*;
|
||||||
@@ -29,4 +33,4 @@ pub use self::movement::*;
|
|||||||
pub use self::player::*;
|
pub use self::player::*;
|
||||||
pub use self::profiling::*;
|
pub use self::profiling::*;
|
||||||
pub use self::render::*;
|
pub use self::render::*;
|
||||||
pub use self::stage::*;
|
pub use self::state::*;
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
use crate::constants::CANVAS_SIZE;
|
|
||||||
use crate::error::{GameError, TextureError};
|
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::input::TouchState;
|
use crate::systems::input::TouchState;
|
||||||
use crate::systems::{
|
use crate::systems::{
|
||||||
debug_render_system, BatchedLinesResource, Collider, CursorPosition, DebugState, DebugTextureResource, DeltaTime,
|
debug_render_system, BatchedLinesResource, Collider, CursorPosition, DebugState, DebugTextureResource, DeltaTime,
|
||||||
DirectionalAnimation, LinearAnimation, Position, Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings,
|
DirectionalAnimation, Dying, Frozen, GameStage, LinearAnimation, Looping, PlayerLives, Position, Renderable, ScoreResource,
|
||||||
TtfAtlasResource, Velocity,
|
StartupSequence, SystemId, SystemTimings, TtfAtlasResource, Velocity,
|
||||||
};
|
};
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
use crate::texture::text::TextTexture;
|
use crate::texture::text::TextTexture;
|
||||||
|
use crate::{
|
||||||
|
constants::CANVAS_SIZE,
|
||||||
|
error::{GameError, TextureError},
|
||||||
|
};
|
||||||
use bevy_ecs::component::Component;
|
use bevy_ecs::component::Component;
|
||||||
use bevy_ecs::entity::Entity;
|
use bevy_ecs::entity::Entity;
|
||||||
use bevy_ecs::event::EventWriter;
|
use bevy_ecs::event::EventWriter;
|
||||||
use bevy_ecs::query::{Changed, Or, Without};
|
use bevy_ecs::query::{Changed, Has, Or, With, Without};
|
||||||
use bevy_ecs::removal_detection::RemovedComponents;
|
use bevy_ecs::removal_detection::RemovedComponents;
|
||||||
use bevy_ecs::resource::Resource;
|
use bevy_ecs::resource::Resource;
|
||||||
use bevy_ecs::system::{NonSendMut, Query, Res, ResMut};
|
use bevy_ecs::system::{NonSendMut, Query, Res, ResMut};
|
||||||
@@ -53,7 +55,7 @@ pub fn dirty_render_system(
|
|||||||
/// All directions share the same frame timing to ensure perfect synchronization.
|
/// All directions share the same frame timing to ensure perfect synchronization.
|
||||||
pub fn directional_render_system(
|
pub fn directional_render_system(
|
||||||
dt: Res<DeltaTime>,
|
dt: Res<DeltaTime>,
|
||||||
mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable)>,
|
mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable), Without<Frozen>>,
|
||||||
) {
|
) {
|
||||||
let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
|
let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
|
||||||
|
|
||||||
@@ -86,26 +88,35 @@ pub fn directional_render_system(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates linear animated entities (used for non-directional animations like frightened ghosts).
|
/// System that updates `Renderable` sprites for entities with `LinearAnimation`.
|
||||||
///
|
#[allow(clippy::type_complexity)]
|
||||||
/// This system handles entities that use LinearAnimation component for simple frame cycling.
|
pub fn linear_render_system(
|
||||||
pub fn linear_render_system(dt: Res<DeltaTime>, mut query: Query<(&mut LinearAnimation, &mut Renderable)>) {
|
dt: Res<DeltaTime>,
|
||||||
let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
|
mut query: Query<(&mut LinearAnimation, &mut Renderable, Has<Looping>), Or<(Without<Frozen>, With<Dying>)>>,
|
||||||
|
) {
|
||||||
for (mut anim, mut renderable) in query.iter_mut() {
|
for (mut anim, mut renderable, looping) in query.iter_mut() {
|
||||||
// Tick animation
|
if anim.finished {
|
||||||
anim.time_bank += ticks;
|
continue;
|
||||||
while anim.time_bank >= anim.frame_duration {
|
|
||||||
anim.time_bank -= anim.frame_duration;
|
|
||||||
anim.current_frame += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !anim.tiles.is_empty() {
|
anim.time_bank += dt.ticks as u16;
|
||||||
let new_tile = anim.tiles.get_tile(anim.current_frame);
|
let frames_to_advance = (anim.time_bank / anim.frame_duration) as usize;
|
||||||
if renderable.sprite != new_tile {
|
|
||||||
renderable.sprite = new_tile;
|
if frames_to_advance == 0 {
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total_frames = anim.tiles.len();
|
||||||
|
|
||||||
|
if !looping && anim.current_frame + frames_to_advance >= total_frames {
|
||||||
|
anim.finished = true;
|
||||||
|
anim.current_frame = total_frames - 1;
|
||||||
|
} else {
|
||||||
|
anim.current_frame += frames_to_advance;
|
||||||
|
}
|
||||||
|
|
||||||
|
anim.time_bank %= anim.frame_duration;
|
||||||
|
renderable.sprite = anim.tiles.get_tile(anim.current_frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,15 +204,16 @@ pub fn hud_render_system(
|
|||||||
mut backbuffer: NonSendMut<BackbufferResource>,
|
mut backbuffer: NonSendMut<BackbufferResource>,
|
||||||
mut canvas: NonSendMut<&mut Canvas<Window>>,
|
mut canvas: NonSendMut<&mut Canvas<Window>>,
|
||||||
mut atlas: NonSendMut<SpriteAtlas>,
|
mut atlas: NonSendMut<SpriteAtlas>,
|
||||||
|
player_lives: Res<PlayerLives>,
|
||||||
score: Res<ScoreResource>,
|
score: Res<ScoreResource>,
|
||||||
startup: Res<StartupSequence>,
|
stage: Res<GameStage>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
let _ = canvas.with_texture_canvas(&mut backbuffer.0, |canvas| {
|
let _ = canvas.with_texture_canvas(&mut backbuffer.0, |canvas| {
|
||||||
let mut text_renderer = TextTexture::new(1.0);
|
let mut text_renderer = TextTexture::new(1.0);
|
||||||
|
|
||||||
// Render lives and high score text in white
|
// Render lives and high score text in white
|
||||||
let lives = 3; // TODO: Get from actual lives resource
|
let lives = player_lives.0;
|
||||||
let lives_text = format!("{lives}UP HIGH SCORE ");
|
let lives_text = format!("{lives}UP HIGH SCORE ");
|
||||||
let lives_position = glam::UVec2::new(4 + 8 * 3, 2); // x_offset + lives_offset * 8, y_offset
|
let lives_position = glam::UVec2::new(4 + 8 * 3, 2); // x_offset + lives_offset * 8, y_offset
|
||||||
|
|
||||||
@@ -226,10 +238,21 @@ pub fn hud_render_system(
|
|||||||
errors.write(TextureError::RenderFailed(format!("Failed to render high score text: {}", e)).into());
|
errors.write(TextureError::RenderFailed(format!("Failed to render high score text: {}", e)).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render GAME OVER text
|
||||||
|
if matches!(*stage, GameStage::GameOver) {
|
||||||
|
let game_over_text = "GAME OVER";
|
||||||
|
let game_over_width = text_renderer.text_width(game_over_text);
|
||||||
|
let game_over_position = glam::UVec2::new((CANVAS_SIZE.x - game_over_width) / 2, 160);
|
||||||
|
if let Err(e) = text_renderer.render_with_color(canvas, &mut atlas, game_over_text, game_over_position, Color::RED) {
|
||||||
|
errors.write(TextureError::RenderFailed(format!("Failed to render GAME OVER text: {}", e)).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render text based on StartupSequence stage
|
// Render text based on StartupSequence stage
|
||||||
if matches!(
|
if matches!(
|
||||||
*startup,
|
*stage,
|
||||||
StartupSequence::TextOnly { .. } | StartupSequence::CharactersVisible { .. }
|
GameStage::Starting(StartupSequence::TextOnly { .. })
|
||||||
|
| GameStage::Starting(StartupSequence::CharactersVisible { .. })
|
||||||
) {
|
) {
|
||||||
let ready_text = "READY!";
|
let ready_text = "READY!";
|
||||||
let ready_width = text_renderer.text_width(ready_text);
|
let ready_width = text_renderer.text_width(ready_text);
|
||||||
@@ -238,7 +261,7 @@ pub fn hud_render_system(
|
|||||||
errors.write(TextureError::RenderFailed(format!("Failed to render READY text: {}", e)).into());
|
errors.write(TextureError::RenderFailed(format!("Failed to render READY text: {}", e)).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(*startup, StartupSequence::TextOnly { .. }) {
|
if matches!(*stage, GameStage::Starting(StartupSequence::TextOnly { .. })) {
|
||||||
let player_one_text = "PLAYER ONE";
|
let player_one_text = "PLAYER ONE";
|
||||||
let player_one_width = text_renderer.text_width(player_one_text);
|
let player_one_width = text_renderer.text_width(player_one_text);
|
||||||
let player_one_position = glam::UVec2::new((CANVAS_SIZE.x - player_one_width) / 2, 113);
|
let player_one_position = glam::UVec2::new((CANVAS_SIZE.x - player_one_width) / 2, 113);
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
use bevy_ecs::{
|
|
||||||
entity::Entity,
|
|
||||||
query::With,
|
|
||||||
resource::Resource,
|
|
||||||
system::{Commands, Query, ResMut},
|
|
||||||
};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::systems::{Blinking, Frozen, GhostCollider, Hidden, PlayerControlled};
|
|
||||||
|
|
||||||
#[derive(Resource, Debug, Clone, Copy)]
|
|
||||||
pub enum StartupSequence {
|
|
||||||
/// Stage 1: Text-only stage
|
|
||||||
/// - Player & ghosts are hidden
|
|
||||||
/// - READY! and PLAYER ONE text are shown
|
|
||||||
/// - Energizers do not blink
|
|
||||||
TextOnly {
|
|
||||||
/// Remaining ticks in this stage
|
|
||||||
remaining_ticks: u32,
|
|
||||||
},
|
|
||||||
/// Stage 2: Characters visible stage
|
|
||||||
/// - PLAYER ONE text is hidden, READY! text remains
|
|
||||||
/// - Ghosts and Pac-Man are now shown
|
|
||||||
CharactersVisible {
|
|
||||||
/// Remaining ticks in this stage
|
|
||||||
remaining_ticks: u32,
|
|
||||||
},
|
|
||||||
/// Stage 3: Game begins
|
|
||||||
/// - Final state, game is fully active
|
|
||||||
GameActive,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StartupSequence {
|
|
||||||
/// Creates a new StartupSequence with the specified duration in ticks
|
|
||||||
pub fn new(text_only_ticks: u32, _characters_visible_ticks: u32) -> Self {
|
|
||||||
Self::TextOnly {
|
|
||||||
remaining_ticks: text_only_ticks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ticks the timer by one frame, returning transition information if state changes
|
|
||||||
pub fn tick(&mut self) -> Option<(StartupSequence, StartupSequence)> {
|
|
||||||
match self {
|
|
||||||
StartupSequence::TextOnly { remaining_ticks } => {
|
|
||||||
if *remaining_ticks > 0 {
|
|
||||||
*remaining_ticks -= 1;
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let from = *self;
|
|
||||||
*self = StartupSequence::CharactersVisible {
|
|
||||||
remaining_ticks: 60, // 1 second at 60 FPS
|
|
||||||
};
|
|
||||||
Some((from, *self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StartupSequence::CharactersVisible { remaining_ticks } => {
|
|
||||||
if *remaining_ticks > 0 {
|
|
||||||
*remaining_ticks -= 1;
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let from = *self;
|
|
||||||
*self = StartupSequence::GameActive;
|
|
||||||
Some((from, *self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StartupSequence::GameActive => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles startup sequence transitions and component management
|
|
||||||
pub fn startup_stage_system(
|
|
||||||
mut startup: ResMut<StartupSequence>,
|
|
||||||
mut commands: Commands,
|
|
||||||
mut blinking_query: Query<Entity, With<Blinking>>,
|
|
||||||
mut player_query: Query<Entity, With<PlayerControlled>>,
|
|
||||||
mut ghost_query: Query<Entity, With<GhostCollider>>,
|
|
||||||
) {
|
|
||||||
if let Some((from, to)) = startup.tick() {
|
|
||||||
debug!("StartupSequence transition from {from:?} to {to:?}");
|
|
||||||
match (from, to) {
|
|
||||||
(StartupSequence::TextOnly { .. }, StartupSequence::CharactersVisible { .. }) => {
|
|
||||||
// Unhide the player & ghosts
|
|
||||||
for entity in player_query.iter_mut().chain(ghost_query.iter_mut()) {
|
|
||||||
commands.entity(entity).remove::<Hidden>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(StartupSequence::CharactersVisible { .. }, StartupSequence::GameActive) => {
|
|
||||||
// Unfreeze the player & ghosts & pellet blinking
|
|
||||||
for entity in player_query
|
|
||||||
.iter_mut()
|
|
||||||
.chain(ghost_query.iter_mut())
|
|
||||||
.chain(blinking_query.iter_mut())
|
|
||||||
{
|
|
||||||
commands.entity(entity).remove::<Frozen>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
315
src/systems/state.rs
Normal file
315
src/systems/state.rs
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
use std::mem::discriminant;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
map::builder::Map,
|
||||||
|
systems::{
|
||||||
|
AudioEvent, Blinking, DirectionalAnimation, Dying, Eaten, Frozen, Ghost, GhostCollider, GhostState, Hidden,
|
||||||
|
LinearAnimation, Looping, PlayerControlled, Position,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use bevy_ecs::{
|
||||||
|
entity::Entity,
|
||||||
|
event::EventWriter,
|
||||||
|
query::{With, Without},
|
||||||
|
resource::Resource,
|
||||||
|
system::{Commands, Query, Res, ResMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Resource, Clone)]
|
||||||
|
pub struct PlayerAnimation(pub DirectionalAnimation);
|
||||||
|
|
||||||
|
#[derive(Resource, Clone)]
|
||||||
|
pub struct PlayerDeathAnimation(pub LinearAnimation);
|
||||||
|
|
||||||
|
/// A resource to track the overall stage of the game from a high-level perspective.
|
||||||
|
#[derive(Resource, Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum GameStage {
|
||||||
|
Starting(StartupSequence),
|
||||||
|
/// The main gameplay loop is active.
|
||||||
|
Playing,
|
||||||
|
/// The player has died and the death sequence is in progress.
|
||||||
|
PlayerDying(DyingSequence),
|
||||||
|
/// The level is restarting after a death.
|
||||||
|
LevelRestarting,
|
||||||
|
/// The game has ended.
|
||||||
|
GameOver,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A resource that manages the multi-stage startup sequence of the game.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum StartupSequence {
|
||||||
|
/// Stage 1: Text-only stage
|
||||||
|
/// - Player & ghosts are hidden
|
||||||
|
/// - READY! and PLAYER ONE text are shown
|
||||||
|
/// - Energizers do not blink
|
||||||
|
TextOnly {
|
||||||
|
/// Remaining ticks in this stage
|
||||||
|
remaining_ticks: u32,
|
||||||
|
},
|
||||||
|
/// Stage 2: Characters visible stage
|
||||||
|
/// - PLAYER ONE text is hidden, READY! text remains
|
||||||
|
/// - Ghosts and Pac-Man are now shown
|
||||||
|
CharactersVisible {
|
||||||
|
/// Remaining ticks in this stage
|
||||||
|
remaining_ticks: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameStage {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Playing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state machine for the multi-stage death sequence.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum DyingSequence {
|
||||||
|
/// Initial stage: entities are frozen, waiting for a delay.
|
||||||
|
Frozen { remaining_ticks: u32 },
|
||||||
|
/// Second stage: Pac-Man's death animation is playing.
|
||||||
|
Animating { remaining_ticks: u32 },
|
||||||
|
/// Third stage: Pac-Man is now gone, waiting a moment before the level restarts.
|
||||||
|
Hidden { remaining_ticks: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A resource to store the number of player lives.
|
||||||
|
#[derive(Resource, Debug)]
|
||||||
|
pub struct PlayerLives(pub u8);
|
||||||
|
|
||||||
|
impl Default for PlayerLives {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles startup sequence transitions and component management
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn stage_system(
|
||||||
|
mut game_state: ResMut<GameStage>,
|
||||||
|
player_death_animation: Res<PlayerDeathAnimation>,
|
||||||
|
player_animation: Res<PlayerAnimation>,
|
||||||
|
mut player_lives: ResMut<PlayerLives>,
|
||||||
|
map: Res<Map>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut audio_events: EventWriter<AudioEvent>,
|
||||||
|
mut blinking_query: Query<Entity, With<Blinking>>,
|
||||||
|
mut player_query: Query<(Entity, &mut Position), With<PlayerControlled>>,
|
||||||
|
mut ghost_query: Query<(Entity, &Ghost, &mut Position), (With<GhostCollider>, Without<PlayerControlled>)>,
|
||||||
|
) {
|
||||||
|
let old_state = *game_state;
|
||||||
|
let new_state: GameStage = match &mut *game_state {
|
||||||
|
GameStage::Starting(startup) => match startup {
|
||||||
|
StartupSequence::TextOnly { remaining_ticks } => {
|
||||||
|
if *remaining_ticks > 0 {
|
||||||
|
GameStage::Starting(StartupSequence::TextOnly {
|
||||||
|
remaining_ticks: *remaining_ticks - 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
GameStage::Starting(StartupSequence::CharactersVisible { remaining_ticks: 60 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StartupSequence::CharactersVisible { remaining_ticks } => {
|
||||||
|
if *remaining_ticks > 0 {
|
||||||
|
GameStage::Starting(StartupSequence::CharactersVisible {
|
||||||
|
remaining_ticks: *remaining_ticks - 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
GameStage::Playing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GameStage::Playing => GameStage::Playing,
|
||||||
|
GameStage::PlayerDying(dying) => match dying {
|
||||||
|
DyingSequence::Frozen { remaining_ticks } => {
|
||||||
|
if *remaining_ticks > 0 {
|
||||||
|
GameStage::PlayerDying(DyingSequence::Frozen {
|
||||||
|
remaining_ticks: *remaining_ticks - 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let death_animation = &player_death_animation.0;
|
||||||
|
let remaining_ticks = (death_animation.tiles.len() * death_animation.frame_duration as usize) as u32;
|
||||||
|
GameStage::PlayerDying(DyingSequence::Animating { remaining_ticks })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DyingSequence::Animating { remaining_ticks } => {
|
||||||
|
if *remaining_ticks > 0 {
|
||||||
|
GameStage::PlayerDying(DyingSequence::Animating {
|
||||||
|
remaining_ticks: *remaining_ticks - 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
GameStage::PlayerDying(DyingSequence::Hidden { remaining_ticks: 60 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DyingSequence::Hidden { remaining_ticks } => {
|
||||||
|
if *remaining_ticks > 0 {
|
||||||
|
GameStage::PlayerDying(DyingSequence::Hidden {
|
||||||
|
remaining_ticks: *remaining_ticks - 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
player_lives.0 = player_lives.0.saturating_sub(1);
|
||||||
|
|
||||||
|
if player_lives.0 > 0 {
|
||||||
|
GameStage::LevelRestarting
|
||||||
|
} else {
|
||||||
|
GameStage::GameOver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GameStage::LevelRestarting => GameStage::Starting(StartupSequence::CharactersVisible { remaining_ticks: 60 }),
|
||||||
|
GameStage::GameOver => GameStage::GameOver,
|
||||||
|
};
|
||||||
|
|
||||||
|
if old_state == new_state {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match (old_state, new_state) {
|
||||||
|
(GameStage::Playing, GameStage::PlayerDying(DyingSequence::Frozen { .. })) => {
|
||||||
|
// Freeze the player & ghosts
|
||||||
|
for entity in player_query
|
||||||
|
.iter_mut()
|
||||||
|
.map(|(e, _)| e)
|
||||||
|
.chain(ghost_query.iter_mut().map(|(e, _, _)| e))
|
||||||
|
{
|
||||||
|
commands.entity(entity).insert(Frozen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(GameStage::PlayerDying(DyingSequence::Frozen { .. }), GameStage::PlayerDying(DyingSequence::Animating { .. })) => {
|
||||||
|
// Hide the ghosts
|
||||||
|
for (entity, _, _) in ghost_query.iter_mut() {
|
||||||
|
commands.entity(entity).insert(Hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Pac-Man's death animation
|
||||||
|
if let Ok((player_entity, _)) = player_query.single_mut() {
|
||||||
|
commands
|
||||||
|
.entity(player_entity)
|
||||||
|
.insert((Dying, player_death_animation.0.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play the death sound
|
||||||
|
audio_events.write(AudioEvent::PlayDeath);
|
||||||
|
}
|
||||||
|
(GameStage::PlayerDying(DyingSequence::Animating { .. }), GameStage::PlayerDying(DyingSequence::Hidden { .. })) => {
|
||||||
|
// Hide the player
|
||||||
|
if let Ok((player_entity, _)) = player_query.single_mut() {
|
||||||
|
commands.entity(player_entity).insert(Hidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(_, GameStage::LevelRestarting) => {
|
||||||
|
if let Ok((player_entity, mut pos)) = player_query.single_mut() {
|
||||||
|
*pos = Position::Stopped {
|
||||||
|
node: map.start_positions.pacman,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Freeze the blinking, force them to be visible (if they were hidden by blinking)
|
||||||
|
for entity in blinking_query.iter_mut() {
|
||||||
|
commands.entity(entity).insert(Frozen).remove::<Hidden>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the player animation
|
||||||
|
commands
|
||||||
|
.entity(player_entity)
|
||||||
|
.remove::<(Frozen, Dying, Hidden, LinearAnimation, Looping)>()
|
||||||
|
.insert(player_animation.0.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset ghost positions and state
|
||||||
|
for (ghost_entity, ghost, mut ghost_pos) in ghost_query.iter_mut() {
|
||||||
|
*ghost_pos = Position::Stopped {
|
||||||
|
node: match ghost {
|
||||||
|
Ghost::Blinky => map.start_positions.blinky,
|
||||||
|
Ghost::Pinky => map.start_positions.pinky,
|
||||||
|
Ghost::Inky => map.start_positions.inky,
|
||||||
|
Ghost::Clyde => map.start_positions.clyde,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
commands
|
||||||
|
.entity(ghost_entity)
|
||||||
|
.remove::<(Frozen, Hidden, Eaten)>()
|
||||||
|
.insert(GhostState::Normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(
|
||||||
|
GameStage::Starting(StartupSequence::TextOnly { .. }),
|
||||||
|
GameStage::Starting(StartupSequence::CharactersVisible { .. }),
|
||||||
|
) => {
|
||||||
|
// Unhide the player & ghosts
|
||||||
|
for entity in player_query
|
||||||
|
.iter_mut()
|
||||||
|
.map(|(e, _)| e)
|
||||||
|
.chain(ghost_query.iter_mut().map(|(e, _, _)| e))
|
||||||
|
{
|
||||||
|
commands.entity(entity).remove::<Hidden>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(GameStage::Starting(StartupSequence::CharactersVisible { .. }), GameStage::Playing) => {
|
||||||
|
// Unfreeze the player & ghosts & blinking
|
||||||
|
for entity in player_query
|
||||||
|
.iter_mut()
|
||||||
|
.map(|(e, _)| e)
|
||||||
|
.chain(ghost_query.iter_mut().map(|(e, _, _)| e))
|
||||||
|
.chain(blinking_query.iter_mut())
|
||||||
|
{
|
||||||
|
commands.entity(entity).remove::<Frozen>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(GameStage::PlayerDying(..), GameStage::GameOver) => {
|
||||||
|
// Freeze blinking
|
||||||
|
for entity in blinking_query.iter_mut() {
|
||||||
|
commands.entity(entity).insert(Frozen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let different = discriminant(&old_state) != discriminant(&new_state);
|
||||||
|
if different {
|
||||||
|
tracing::warn!(
|
||||||
|
new_state = ?new_state,
|
||||||
|
old_state = ?old_state,
|
||||||
|
"Unhandled game stage transition");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*game_state = new_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if let GameState::LevelRestarting = &*game_state {
|
||||||
|
// // When restarting, jump straight to the CharactersVisible stage
|
||||||
|
// // and unhide the entities.
|
||||||
|
// *startup = StartupSequence::new(0, 60 * 2); // 2 seconds for READY! text
|
||||||
|
// if let StartupSequence::TextOnly { .. } = *startup {
|
||||||
|
// // This will immediately transition to CharactersVisible on the next line
|
||||||
|
// } else {
|
||||||
|
// // Should be unreachable as we just set it
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Freeze Pac-Man and ghosts
|
||||||
|
// for entity in player_query.iter().chain(ghost_query.iter()) {
|
||||||
|
// commands.entity(entity).insert(Frozen);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// *game_state = GameState::Playing;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if let Some((old_state, new_state)) = startup.tick() {
|
||||||
|
// debug!("StartupSequence transition from {old_state:?} to {new_state:?}");
|
||||||
|
// match (old_state, new_state) {
|
||||||
|
// (StartupSequence::TextOnly { .. }, StartupSequence::CharactersVisible { .. }) => {
|
||||||
|
// // Unhide the player & ghosts
|
||||||
|
// for entity in player_query.iter().chain(ghost_query.iter()) {
|
||||||
|
// commands.entity(entity).remove::<Hidden>();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// (StartupSequence::CharactersVisible { .. }, StartupSequence::GameActive) => {
|
||||||
|
// // Unfreeze Pac-Man, ghosts and energizers
|
||||||
|
// for entity in player_query.iter().chain(ghost_query.iter()).chain(blinking_query.iter()) {
|
||||||
|
// commands.entity(entity).remove::<Frozen>();
|
||||||
|
// }
|
||||||
|
// *game_state = GameState::Playing;
|
||||||
|
// }
|
||||||
|
// _ => {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -1,53 +1,45 @@
|
|||||||
use crate::map::direction::Direction;
|
use glam::U16Vec2;
|
||||||
use crate::texture::sprite::AtlasTile;
|
|
||||||
|
|
||||||
/// Fixed-size tile sequence that avoids heap allocation
|
use crate::{map::direction::Direction, texture::sprite::AtlasTile};
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
|
/// A sequence of tiles for animation, backed by a vector.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct TileSequence {
|
pub struct TileSequence {
|
||||||
tiles: [AtlasTile; 4], // Fixed array, max 4 frames
|
tiles: Vec<AtlasTile>,
|
||||||
count: usize, // Actual number of frames used
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileSequence {
|
impl TileSequence {
|
||||||
/// Creates a new tile sequence from a slice of tiles
|
/// Creates a new tile sequence from a slice of tiles.
|
||||||
pub fn new(tiles: &[AtlasTile]) -> Self {
|
pub fn new(tiles: &[AtlasTile]) -> Self {
|
||||||
let mut tile_array = [AtlasTile {
|
Self { tiles: tiles.to_vec() }
|
||||||
pos: glam::U16Vec2::ZERO,
|
|
||||||
size: glam::U16Vec2::ZERO,
|
|
||||||
color: None,
|
|
||||||
}; 4];
|
|
||||||
|
|
||||||
let count = tiles.len().min(4);
|
|
||||||
tile_array[..count].copy_from_slice(&tiles[..count]);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
tiles: tile_array,
|
|
||||||
count,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the tile at the given frame index, wrapping if necessary
|
/// Returns the tile at the given frame index, wrapping if necessary
|
||||||
pub fn get_tile(&self, frame: usize) -> AtlasTile {
|
pub fn get_tile(&self, frame: usize) -> AtlasTile {
|
||||||
if self.count == 0 {
|
if self.tiles.is_empty() {
|
||||||
// Return a default empty tile if no tiles
|
// Return a default or handle the error appropriately
|
||||||
AtlasTile {
|
// For now, let's return a default tile, assuming it's a sensible default
|
||||||
pos: glam::U16Vec2::ZERO,
|
return AtlasTile {
|
||||||
size: glam::U16Vec2::ZERO,
|
pos: U16Vec2::ZERO,
|
||||||
|
size: U16Vec2::ZERO,
|
||||||
color: None,
|
color: None,
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
self.tiles[frame % self.count]
|
|
||||||
}
|
}
|
||||||
|
self.tiles[frame % self.tiles.len()]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this sequence has no tiles
|
pub fn len(&self) -> usize {
|
||||||
|
self.tiles.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the sequence contains any tiles.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.count == 0
|
self.tiles.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type-safe directional tile storage with named fields
|
/// A collection of tile sequences for each cardinal direction.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DirectionalTiles {
|
pub struct DirectionalTiles {
|
||||||
pub up: TileSequence,
|
pub up: TileSequence,
|
||||||
pub down: TileSequence,
|
pub down: TileSequence,
|
||||||
|
|||||||
Reference in New Issue
Block a user