Compare commits

..

62 Commits
master ... dev

Author SHA1 Message Date
ab3e4a177b Allow swiping for inputting high score name 2020-01-22 20:50:30 +04:00
fc912863ad Register high score if it fits anywhere in the top 10.
When adding it, render in appropriate position.
2020-01-22 20:18:14 +04:00
5645b4bcf5 Add visible return/save buttons to high scores pages 2020-01-22 19:42:49 +04:00
1989b4557b Move GHOST_SPAWN_DIRS and GHOST_SPAWN_POINTS 2020-01-22 19:12:06 +04:00
784bcac437 Move pellet eaten counters to level 2020-01-22 03:03:05 +04:00
f655fe2448 Refactors 2020-01-22 02:40:36 +04:00
b19daafc2b Refactor some constants to Constants class
Use TILE_SIZE everywhere a magic 8 appears (in the context of a tile size)
2020-01-22 02:35:05 +04:00
fb6f77b72c Alter positioning on high score screens
Fix removal of preNewGame() after GameOver
2020-01-16 17:01:07 +04:00
0a058e3d19 Adding of new high scores + displaying of actual high score 2020-01-16 16:40:09 +04:00
a9e99a363c Plumb in high scores 2020-01-16 16:23:30 +04:00
0d69521986 Make score grow to the left instead of right
Draw high score heading
2020-01-16 13:04:05 +04:00
5c227e53d1 Render fonts ourselves with a font spritesheet.
Ensures font looks original to classic pac-man
2020-01-16 12:32:32 +04:00
e4e0d0c1b0 Support dynamic icon 2020-01-15 23:19:19 +04:00
0e906479d1 Add basic touch controls 2020-01-15 22:22:09 +04:00
1585407bcb Update project for Android builds 2020-01-15 22:22:01 +04:00
82d85a926d Add filler textures to level sprites 2020-01-14 16:01:33 +04:00
b05cca6b3f Remove unused ShapeRenderer, add FPS counter in DEBUG mode
Move ghost reverse after path logic, which may have been the cause of
ghosts getting stuck inside the house...?

Moved where 'paused' is rendered
2020-01-07 15:47:41 +04:00
b8fcfb4de7 Sound pausing/resuming 2020-01-07 14:58:20 +04:00
c0c0a86a4c Menu music/sounds 2020-01-07 14:56:10 +04:00
982e4ea5ad More sounds! Closes #4 2020-01-07 14:11:46 +04:00
c7f352ea0c Add central Sound class
Stop sounds when last pellet is eaten.
2020-01-06 17:02:55 +04:00
02a4c7d5ed Refactor 2020-01-05 23:48:39 +04:00
13d1948d48 Let ghosts already returning continue when another ghost is caught
Only catch one ghost per frame
Fix new game shortcut
2020-01-05 23:28:23 +04:00
2bc78baa74 Simplify rendering logic, no longer calculating offset in many places 2020-01-05 14:47:23 +04:00
b770c2e40d Don't reverse ghosts immediately, wait until they've reached the next tile
Also fix index out of bounds in retrieving next scatterChaseTimer value,
and retrieving an offset-by-1 value (due to increasing
scatterChaseTransition after retrieving the next timer value).

Closes #9
2020-01-05 14:08:34 +04:00
fb3e26da35 Fix lives being removed on new round 2020-01-05 12:47:39 +04:00
31a2657c13 Implement round progression mechanics and Elroy speeds
Closes #3
Closes #6
2020-01-05 12:44:52 +04:00
37944fccd9 Cleaner update routine by extracting logic into new methods 2020-01-05 10:42:45 +04:00
ab94395eac A little more refactoring
Declare methods before calling for easier following
2020-01-05 10:31:24 +04:00
389533d394 Significant amount of refactoring:
- Removed several mode timer variables and implemented a GameMode enum w/
associated timer.
- Removed individual chase/scatter timer, replace with single timer
- Hopefully made level/round initializing methods more coherent
2020-01-05 02:35:54 +04:00
ecaf86a57a Cache assets 2020-01-02 20:51:50 +04:00
d21a4b8c1d Add round clear flashing, closes #5 2020-01-02 02:30:39 +04:00
6bfe740416 Allow WASD 2019-12-29 12:25:13 +04:00
bf0d5e041b Functional menu 2019-12-29 12:24:51 +04:00
30962673d5 Version 0.1.0 2019-12-29 00:33:16 +04:00
0501917756 Set speed based on current state before each update. Fixes #2
Ghosts now slow down in side tunnels
All speeds now ratios of PlayState.FULL_SPEED
2019-12-29 00:27:13 +04:00
20719621e2 Removed unused imports 2019-12-28 23:37:28 +04:00
9b06b294f4 Removed ghost == null checks that are no longer necessary 2019-12-28 23:35:29 +04:00
3fe14e6e5d Implement ghost house exiting logic (wtf, pacman designers?)
- Fixed bug in Clyde's chase behaviour (was using Inky's instead of
his own position)
- Create a subclass of Ghost for each ghost's unique house leaving logic
- Now spawn ghosts only once
2019-12-28 23:26:58 +04:00
dd9bdac33d Add chase/scatter alternation 2019-12-28 16:51:09 +04:00
0d42aac28d Fixed ghost in-house behaviour
Speed up ghost cape animation
2019-12-28 14:04:20 +04:00
906ef93a11 Initialize all state vars in intializeField before new game
Fixed ghosts remaining frightened at the start of a new round
Fixed ghosts staying slow after being frightened
Reduce wait time after death to 2 seconds
2019-12-28 13:32:23 +04:00
c32451a597 Add debug rendering of an entity's current tile 2019-12-28 12:55:58 +04:00
f6fbad0e6d Shorter turning code using vector math 2019-12-28 12:50:41 +04:00
8a0298b2db Shorter movement code making use of vector math 2019-12-28 12:21:28 +04:00
ac347dc6d8 Give + render points for capturing ghost
Slow frightened ghosts down
2019-12-28 01:19:36 +04:00
d33435bf42 Minor change
Always return when the gameOverTimer or newGameTmer is > 0
Move variables
2019-12-28 00:53:38 +04:00
a8e4d2526e Fix (again) and simplify ghost capture logic
Spawn ghosts inside their house
Start a new game by pressing 'n'
2019-12-28 00:47:18 +04:00
e38c7a2dea Implement PInky, Inky, and Clyde's chase behaviours 2019-12-27 17:58:12 +04:00
933f696fe8 Add fix for ghosts getting stuck, Fixes #1 2019-12-27 16:57:04 +04:00
8f13d9295c Remove redundant checks 2019-12-27 16:11:46 +04:00
5eef40f767 Do the flashy thing 2019-12-27 15:45:34 +04:00
cd2b968631 Ghosts can get inside their house now
Add Path object for defining  apath an entity should take at a given speed.

Fix some issues with ghost capturing logic
2019-12-27 14:35:47 +04:00
2a65084363 Rudimentary ghost capturing 2019-12-27 00:51:41 +04:00
0bd4d7f042 Implement BinkyScatterBehaviour 2019-12-26 16:26:37 +04:00
afad1017a5 Implement FrightBehaviour
This took way too long.
2019-12-26 16:13:29 +04:00
75d0c41bb5 Ghosts can chase!
Refactor position to be a Vector2 for convenience sake.
2019-12-26 03:38:14 +04:00
d10148ace1 Refactor N/E/S/W to U/D/L/R 2019-12-26 01:54:58 +04:00
0177090ea3 Paccy should start off to the left 2019-12-26 01:52:34 +04:00
1153205ddd There's a ghost! And death. And a game over sequence. 2019-12-26 01:50:26 +04:00
6073c6c84a Ready! 2019-12-25 20:27:20 +04:00
e2f601c8be Refactor MovableEntity.Direction outside of MovableEntity 2019-12-25 19:35:41 +04:00
97 changed files with 3117 additions and 327 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.me.pacman" >
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:isGame="true"
android:appCategory="game"
android:label="@string/app_name"
android:theme="@style/GdxTheme" >
<activity
android:name="com.me.pacman.AndroidLauncher"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -11,9 +11,9 @@
######*##### ## #####*######
######*## ##*######
######*## ^^^__^^^ ##*######
######*## ^^^^^^^^ ##*######
------* ^^^^^^^^ *------
######*## ^^^^^^^^ ##*######
######*## ^ ^ ##*######
------* ^ ^ *------
######*## ^ ^ ##*######
######*## ^^^^^^^^ ##*######
######*## ##*######
######*## ######## ##*######

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 943 B

After

Width:  |  Height:  |  Size: 943 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 655 B

90
android/build.gradle Normal file
View File

@ -0,0 +1,90 @@
android {
buildToolsVersion "29.0.2"
compileSdkVersion 29
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
packagingOptions {
exclude 'META-INF/robovm/ios/robovm.xml'
}
defaultConfig {
applicationId "com.me.pacman"
minSdkVersion 14
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
// called every time gradle gets executed, takes the native dependencies of
// the natives configuration, and extracts them to the proper libs/ folders
// so they get packed with the APK.
task copyAndroidNatives {
doFirst {
file("libs/armeabi/").mkdirs()
file("libs/armeabi-v7a/").mkdirs()
file("libs/arm64-v8a/").mkdirs()
file("libs/x86_64/").mkdirs()
file("libs/x86/").mkdirs()
configurations.natives.files.each { jar ->
def outputDir = null
if (jar.name.endsWith("natives-arm64-v8a.jar")) outputDir = file("libs/arm64-v8a")
if (jar.name.endsWith("natives-armeabi-v7a.jar")) outputDir = file("libs/armeabi-v7a")
if(jar.name.endsWith("natives-armeabi.jar")) outputDir = file("libs/armeabi")
if(jar.name.endsWith("natives-x86_64.jar")) outputDir = file("libs/x86_64")
if(jar.name.endsWith("natives-x86.jar")) outputDir = file("libs/x86")
if(outputDir != null) {
copy {
from zipTree(jar)
into outputDir
include "*.so"
}
}
}
}
}
tasks.whenTaskAdded { packageTask ->
if (packageTask.name.contains("package")) {
packageTask.dependsOn 'copyAndroidNatives'
}
}
task run(type: Exec) {
def path
def localProperties = project.file("../local.properties")
if (localProperties.exists()) {
Properties properties = new Properties()
localProperties.withInputStream { instr ->
properties.load(instr)
}
def sdkDir = properties.getProperty('sdk.dir')
if (sdkDir) {
path = sdkDir
} else {
path = "$System.env.ANDROID_HOME"
}
} else {
path = "$System.env.ANDROID_HOME"
}
def adb = path + "/platform-tools/adb"
commandLine "$adb", 'shell', 'am', 'start', '-n', 'com.me.pacman/com.me.pacman.AndroidLauncher'
}

BIN
android/ic_launcher-web.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

45
android/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,45 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-verbose
-dontwarn android.support.**
-dontwarn com.badlogic.gdx.backends.android.AndroidFragmentApplication
-dontwarn com.badlogic.gdx.utils.GdxBuild
-dontwarn com.badlogic.gdx.physics.box2d.utils.Box2DBuild
-dontwarn com.badlogic.gdx.jnigen.BuildTarget*
-dontwarn com.badlogic.gdx.graphics.g2d.freetype.FreetypeBuild
-keep class com.badlogic.gdx.controllers.android.AndroidControllers
-keepclassmembers class com.badlogic.gdx.backends.android.AndroidInput* {
<init>(com.badlogic.gdx.Application, android.content.Context, java.lang.Object, com.badlogic.gdx.backends.android.AndroidApplicationConfiguration);
}
-keepclassmembers class com.badlogic.gdx.physics.box2d.World {
boolean contactFilter(long, long);
void beginContact(long);
void endContact(long);
void preSolve(long, long);
void postSolve(long, long);
boolean reportFixture(long);
float reportRayFixture(long, float, float, float, float, float);
}

View File

@ -0,0 +1,9 @@
# This file is used by the Eclipse ADT plugin. It is unnecessary for IDEA and Android Studio projects, which
# configure Proguard and the Android target via the build.gradle file.
# To enable ProGuard to work with Eclipse ADT, uncomment this (available properties: sdk.dir, user.home)
# and ensure proguard.jar in the Android SDK is up to date (or alternately reduce the android target to 23 or lower):
# proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-rules.pro
# Project target.
target=android-19

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_background_color"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M72.76,69.051A24,24 0,0 1,46.166 76.177,24 24,0 0,1 30.375,53.624 24,24 0,0 1,46.166 31.072,24 24,0 0,1 72.76,38.197L54.375,53.624Z"
android:strokeAlpha="1"
android:strokeWidth="0.74999523"
android:fillColor="#ffff00"
android:strokeColor="#ffff00"
android:fillAlpha="1"/>
<path
android:pathData="M51,35.75L60,35.75A1.5,3 0,0 1,61.5 38.75L61.5,39.75A1.5,3 0,0 1,60 42.75L51,42.75A1.5,3 0,0 1,49.5 39.75L49.5,38.75A1.5,3 0,0 1,51 35.75z"
android:strokeAlpha="1"
android:strokeWidth="0"
android:fillColor="#1a1a1a"
android:strokeColor="#1a1a1a"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_background_color">#000000FF</color>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Pac-Dude</string>
</resources>

View File

@ -0,0 +1,12 @@
<resources>
<style name="GdxTheme" parent="android:Theme">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowFullscreen">true</item>
</style>
</resources>

View File

@ -0,0 +1,17 @@
package com.me.pacman;
import android.os.Bundle;
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import com.me.pacman.PacDude;
public class AndroidLauncher extends AndroidApplication {
@Override
protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
initialize(new PacDude(), config);
}
}

View File

@ -10,8 +10,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
}
}
@ -49,6 +48,28 @@ project(":desktop") {
}
}
project(":android") {
apply plugin: "android"
configurations { natives }
dependencies {
implementation project(":core")
api "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64"
api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-armeabi"
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-armeabi-v7a"
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-arm64-v8a"
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86"
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86_64"
}
}
project(":core") {
apply plugin: "java-library"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

View File

@ -3,7 +3,6 @@ package com.me.pacman;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.FileHandleResolver;
import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
@ -16,6 +15,12 @@ public class Assets {
private AssetManager manager;
private Texture levelBackground;
private Texture levelWinBackground;
private Texture menuBackground;
private Texture highScoresBackground;
public TextureRegion[][] font;
public TextureRegion[][] level;
public TextureRegion[][] deathAnimation;
public TextureRegion[][] ghosts;
@ -24,7 +29,7 @@ public class Assets {
public TextureRegion[][] volume;
public Sound beginning, beginning_alt;
public Sound chaseSound;
public Sound fright;
public Sound chomp_1, chomp_2;
public Sound deathSound;
public Sound eat_fruit, eat_ghost;
@ -32,15 +37,18 @@ public class Assets {
public Sound return_base;
public Sound siren, siren_fast, siren_faster, siren_fastest;
public Assets() {
this.manager = new AssetManager();
}
public void loadAssets() {
manager.load("level_background.png", Texture.class);
manager.load("level_background_win.png", Texture.class);
manager.load("menu_background.png", Texture.class);
manager.load("high_scores_background.png", Texture.class);
manager.load("logo.png", Texture.class);
manager.load("sprites/font.png", Texture.class);
manager.load("sprites/level.png", Texture.class);
manager.load("sprites/death.png", Texture.class);
manager.load("sprites/ghosts.png", Texture.class);
@ -50,7 +58,7 @@ public class Assets {
manager.load("sounds/beginning.wav", Sound.class);
manager.load("sounds/beginning_alt.wav", Sound.class);
manager.load("sounds/chase.wav", Sound.class);
manager.load("sounds/fright.wav", Sound.class);
manager.load("sounds/chomp_1.wav", Sound.class);
manager.load("sounds/chomp_2.wav", Sound.class);
manager.load("sounds/death.wav", Sound.class);
@ -63,20 +71,16 @@ public class Assets {
manager.load("sounds/siren_faster.wav", Sound.class);
manager.load("sounds/siren_fastest.wav", Sound.class);
// Yayyy! all of this to load a font
FileHandleResolver resolver = new InternalFileHandleResolver();
manager.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver));
manager.setLoader(BitmapFont.class, ".ttf", new FreetypeFontLoader(resolver));
FreetypeFontLoader.FreeTypeFontLoaderParameter font = new FreetypeFontLoader.FreeTypeFontLoaderParameter();
font.fontFileName = "fonts/joystix.ttf";
font.fontParameters.size = 10;
font.fontParameters.mono = true;
manager.load("fonts/joystix.ttf", BitmapFont.class, font);
// finish loading assets from disk
manager.finishLoading();
levelBackground = manager.get("level_background.png", Texture.class);
levelWinBackground = manager.get("level_background_win.png", Texture.class);
menuBackground = manager.get("menu_background.png", Texture.class);
highScoresBackground = manager.get("high_scores_background.png", Texture.class);
// cache our texture regions
font = TextureRegion.split(manager.get("sprites/font.png", Texture.class), 8, 8);
level = TextureRegion.split(manager.get("sprites/level.png", Texture.class), 8, 8);
deathAnimation = TextureRegion.split(manager.get("sprites/death.png", Texture.class), 16, 16);
ghosts = TextureRegion.split(manager.get("sprites/ghosts.png", Texture.class), 16, 16);
@ -87,7 +91,7 @@ public class Assets {
// all our sounds
beginning = manager.get("sounds/beginning.wav", Sound.class);
beginning_alt = manager.get("sounds/beginning_alt.wav", Sound.class);
chaseSound = manager.get("sounds/chase.wav", Sound.class);
fright = manager.get("sounds/fright.wav", Sound.class);
chomp_1 = manager.get("sounds/chomp_1.wav", Sound.class);
chomp_2 = manager.get("sounds/chomp_2.wav", Sound.class);
deathSound = manager.get("sounds/death.wav", Sound.class);
@ -102,17 +106,23 @@ public class Assets {
}
public Texture getLevelBackground() {
return manager.get("level_background.png", Texture.class);
return levelBackground;
}
public Texture getLevelWinBackground() {
return levelWinBackground;
}
public Texture getMenuBackground() {
return menuBackground;
}
public Texture getHighScoresBackground() { return highScoresBackground; }
public Texture getLogo() {
return manager.get("logo.png", Texture.class);
}
public BitmapFont getFont() {
return manager.get("fonts/joystix.ttf", BitmapFont.class);
}
public void dispose() {
manager.dispose();
}

View File

@ -0,0 +1,44 @@
package com.me.pacman;
import com.me.pacman.entity.Direction;
public final class Constants {
public static final String TITLE = "Pac-Dude";
public static final String VERSION = "v0.1.0";
public static final boolean DEBUG = false;
// Pixels
public static final int TILE_SIZE = 8;
// The size of the original Pac-Man game in 8x8 pixel tiles
public static final int GAME_WIDTH_TILES = 28;
public static final int GAME_HEIGHT_TILES = 36;
// The game window width and height - arbitrary units (in this case, matches up with the pixel
// counts of the original Pacman.
public static final int GAME_WIDTH = GAME_WIDTH_TILES * TILE_SIZE;
public static final int GAME_HEIGHT = GAME_HEIGHT_TILES * TILE_SIZE;
// Pac-man level rendering position offset in game coordinates
public static final int LEVEL_OFFSET_X = 0;
public static final int LEVEL_OFFSET_Y = 2 * TILE_SIZE;
// 100% speed in tiles (8 pixel / tile) per second. roughly 75 pixels/second
// 75 pixels/second aligns with the per-frame pixel movement steps in:
// https://github.com/masonicGIT/pacman/blob/master/pacman.js
// and is close to the 75.75757625 pixels/second claimed in:
// https://pacman.holenet.info/#LvlSpecs
public static final float FULL_SPEED = 9.375f;
// Ghost spawn directions, should remain constant assuming they always exit the ghost house
// from the top
public static final Direction[] GHOST_SPAWN_DIRS = {
Direction.LEFT, // Blinky
Direction.UP, // Pinky
Direction.DOWN, // Inky
Direction.DOWN, // Clyde
};
}

View File

@ -0,0 +1,48 @@
package com.me.pacman;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
public class FontRenderer {
public static final char[] CHARS = "abcdefghijklmnopqrstuvwxyz0123456789!?()[]<>$&*:#^~-_/\\".toCharArray();
public static final int[] CHAR_TO_INDEX = new int[256];
private final TextureRegion[][] sprites;
private Color color = Color.WHITE;
public FontRenderer(TextureRegion[][] sprites) {
this.sprites = sprites;
}
public void setColor(Color color) {
this.color = color;
}
public void draw(SpriteBatch batch, String str, int x, int y) {
float oldColor = batch.getPackedColor();
batch.setColor(color);
for (char c : str.toCharArray()) {
if (Character.isWhitespace(c)) {
x += 8;
continue;
}
int i = CHAR_TO_INDEX[c];
batch.draw(sprites[i / 8][i % 8], x, y);
x += 8;
}
batch.setPackedColor(oldColor);
}
static {
for (int i = 0; i < CHARS.length; i++) {
CHAR_TO_INDEX[CHARS[i]] = i;
}
}
}

View File

@ -0,0 +1,67 @@
package com.me.pacman;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
/**
* A simple class for retrieving and saving high scores to persistent storage.
*
* Scores are saved in a simple text file
*/
public class HighScoreManager {
public static final String FILE = "scores.dat";
private FileHandle file;
private Array<Score> scores = new Array<>(10);
public HighScoreManager() {
file = Gdx.files.local(FILE);
loadScores();
}
private void loadScores() {
if (!file.exists()) {
return;
}
String data = file.readString();
String[] lines = data.split("\n");
for (String line : lines) {
String[] parts = line.split("\t");
scores.add(new Score(parts[0], Integer.parseInt(parts[1])));
}
}
private void saveScores() {
for (int i = 0; i < scores.size; i++) {
Score score = scores.get(i);
boolean append = i > 0;
file.writeString(String.format("%s\t%d\n", score.scorer, score.score), append);
}
}
private void sortScores() {
scores.sort(Score.SCORE_SORTER);
}
public Score getCurrentHighScore() {
return scores.size > 0 ? scores.get(0) : null;
}
public Score[] getHighScores(int count) {
Score[] ret = new Score[Math.min(count, scores.size)];
System.arraycopy(scores.items, 0, ret, 0, ret.length);
return ret;
}
public void addScore(Score score) {
scores.add(score);
sortScores();
saveScores();
}
}

View File

@ -2,50 +2,65 @@ package com.me.pacman;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import com.me.pacman.state.PlayState;
import com.me.pacman.state.MenuState;
import com.me.pacman.state.State;
public class PacDude extends Game {
public static final String TITLE = "Pac-Dude";
public static final String VERSION = "v0.0.1";
public static final int LEVEL_WIDTH = 224;
public static final int LEVEL_HEIGHT = 288;
public Camera cam;
public Viewport viewport;
public Assets assets;
public SpriteBatch batch;
public ShapeRenderer sr;
public SoundManager sound;
public OrthographicCamera cam;
public Viewport viewport;
public SpriteBatch batch;
public FontRenderer fontRenderer;
public HighScoreManager highScores;
private State nextState;
@Override
public void create () {
cam = new OrthographicCamera();
viewport = new FitViewport(LEVEL_WIDTH, LEVEL_HEIGHT, cam);
viewport = new FitViewport(Constants.GAME_WIDTH, Constants.GAME_HEIGHT, cam);
viewport.apply(true);
assets = new Assets();
assets.loadAssets();
sound = new SoundManager(this);
batch = new SpriteBatch();
sr = new ShapeRenderer();
fontRenderer = new FontRenderer(assets.font);
setScreen(new PlayState(this));
highScores = new HighScoreManager();
Gdx.gl.glClearColor(0, 0, 0, 1);
setNextState(new MenuState(this));
}
@Override
public void render () {
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
if (nextState != null) {
Screen currScreen = getScreen();
if (currScreen != null) currScreen.dispose();
nextState.setup();
super.setScreen(nextState);
nextState = null;
}
batch.begin();
if (Constants.DEBUG) {
fontRenderer.draw(batch, "fps:" + Gdx.graphics.getFramesPerSecond(), 19 * Constants.TILE_SIZE, 34 * Constants.TILE_SIZE);
}
super.render();
batch.end();
}
@ -55,7 +70,6 @@ public class PacDude extends Game {
viewport.update(width, height);
batch.setProjectionMatrix(cam.combined);
sr.setProjectionMatrix(cam.combined);
super.resize(width, height);
}
@ -65,4 +79,14 @@ public class PacDude extends Game {
batch.dispose();
assets.dispose();
}
@Override
public void setScreen(Screen screen) {
throw new IllegalStateException("Use setNextState instead.");
}
public void setNextState(State state) {
this.nextState = state;
}
}

View File

@ -0,0 +1,271 @@
package com.me.pacman;
import java.util.Arrays;
public class RoundModifiers {
private static final RoundModifiers[] ROUND_MODIFIERS;
private float frightTime;
private float pacmanSpeed;
private float pacmanFrightSpeed;
private float ghostSpeed;
private float ghostFrightSpeed;
private float ghostTunnelSpeed;
private int elroy1DotsLeft;
private float elroy1Speed;
private int elroy2DotsLeft;
private float elroy2Speed;
private int forceLeaveSeconds;
private float[] scatterChaseSwitchTimings;
private static int cap(int round) {
return Math.min(round, 20);
}
public static float getFrightTime(int round) {
return ROUND_MODIFIERS[cap(round)].frightTime;
}
public static float getPacmanSpeed(int round) {
return Constants.FULL_SPEED * ROUND_MODIFIERS[cap(round)].pacmanSpeed;
}
public static float getPacmanFrightSpeed(int round) {
return Constants.FULL_SPEED * ROUND_MODIFIERS[cap(round)].pacmanFrightSpeed;
}
public static float getGhostSpeed(int round) {
return Constants.FULL_SPEED * ROUND_MODIFIERS[cap(round)].ghostSpeed;
}
public static float getGhostFrightSpeed(int round) {
return Constants.FULL_SPEED * ROUND_MODIFIERS[cap(round)].ghostFrightSpeed;
}
public static float getGhostTunnelSpeed(int round) {
return Constants.FULL_SPEED * ROUND_MODIFIERS[cap(round)].ghostTunnelSpeed;
}
public static int getElroy1DotsLeft(int round) {
return ROUND_MODIFIERS[cap(round)].elroy1DotsLeft;
}
public static float getElroy1Speed(int round) {
return Constants.FULL_SPEED * ROUND_MODIFIERS[cap(round)].elroy1Speed;
}
public static int getElroy2DotsLeft(int round) {
return ROUND_MODIFIERS[cap(round)].elroy2DotsLeft;
}
public static float getElroy2Speed(int round) {
return Constants.FULL_SPEED * ROUND_MODIFIERS[cap(round)].elroy2Speed;
}
public static int getForceLeaveSeconds(int round) {
return ROUND_MODIFIERS[cap(round)].forceLeaveSeconds;
}
public static float getScatterChaseTimer(int round, int scatterChaseTransition) {
return ROUND_MODIFIERS[cap(round)].scatterChaseSwitchTimings[scatterChaseTransition];
}
public RoundModifiers clone() {
RoundModifiers mod = new RoundModifiers();
mod.frightTime = frightTime;
mod.pacmanSpeed = pacmanSpeed;
mod.pacmanFrightSpeed = pacmanFrightSpeed;
mod.ghostSpeed = ghostSpeed;
mod.ghostFrightSpeed = ghostFrightSpeed;
mod.ghostTunnelSpeed = ghostTunnelSpeed;
mod.elroy1DotsLeft = elroy1DotsLeft;
mod.elroy1Speed = elroy1Speed;
mod.elroy2DotsLeft = elroy2DotsLeft;
mod.elroy2Speed = elroy2Speed;
mod.forceLeaveSeconds = forceLeaveSeconds;
mod.scatterChaseSwitchTimings = Arrays.copyOf(scatterChaseSwitchTimings, scatterChaseSwitchTimings.length);
return mod;
}
static {
ROUND_MODIFIERS = new RoundModifiers[21];
int round = 0;
// Round 1
RoundModifiers mod = new RoundModifiers();
mod.frightTime = 6f;
mod.pacmanSpeed = 0.8f;
mod.pacmanFrightSpeed = 0.9f;
mod.ghostSpeed = 0.75f;
mod.ghostFrightSpeed = 0.5f;
mod.ghostTunnelSpeed = 0.4f;
mod.elroy1DotsLeft = 20;
mod.elroy1Speed = 0.8f;
mod.elroy2DotsLeft = 10;
mod.elroy2Speed = 0.85f;
mod.forceLeaveSeconds = 4;
mod.scatterChaseSwitchTimings = new float[] {7f, 20f, 7f, 20f, 5f, 20f, 5f};
ROUND_MODIFIERS[round++] = mod;
// Round 2
mod = mod.clone();
mod.frightTime = 5f;
mod.pacmanSpeed = 0.9f;
mod.pacmanFrightSpeed = 0.95f;
mod.ghostSpeed = 0.85f;
mod.ghostFrightSpeed = 0.55f;
mod.ghostTunnelSpeed = 0.45f;
mod.elroy1DotsLeft = 30;
mod.elroy1Speed = 0.9f;
mod.elroy2DotsLeft = 14;
mod.elroy2Speed = 0.95f;
mod.scatterChaseSwitchTimings = new float[] {7f, 20f, 7f, 20f, 5f, 1033f, 1/60f};
ROUND_MODIFIERS[round++] = mod;
// Round 3
mod = mod.clone();
mod.frightTime = 4f;
mod.elroy1DotsLeft = 40;
mod.elroy1Speed = 0.9f;
mod.elroy2DotsLeft = 20;
mod.elroy2Speed = 0.95f;
ROUND_MODIFIERS[round++] = mod;
// Round 4
mod = mod.clone();
mod.frightTime = 3f;
ROUND_MODIFIERS[round++] = mod;
// Round 5
mod = mod.clone();
mod.frightTime = 2f;
mod.pacmanSpeed = 1f;
mod.pacmanFrightSpeed = 1f;
mod.ghostSpeed = 0.95f;
mod.ghostFrightSpeed = 0.6f;
mod.ghostTunnelSpeed = 0.5f;
mod.elroy1Speed = 1f;
mod.elroy2Speed = 1.05f;
mod.forceLeaveSeconds = 3;
mod.scatterChaseSwitchTimings = new float[] {7f, 20f, 7f, 20f, 5f, 1037f, 1/60f};
ROUND_MODIFIERS[round++] = mod;
// Round 6
mod = mod.clone();
mod.frightTime = 5f;
mod.elroy1DotsLeft = 50;
mod.elroy2DotsLeft = 25;
ROUND_MODIFIERS[round++] = mod;
// Round 7 & 8
mod = mod.clone();
mod.frightTime = 2f;
ROUND_MODIFIERS[round++] = mod;
ROUND_MODIFIERS[round++] = mod;
// Round 9
mod = mod.clone();
mod.frightTime = 1f;
mod.elroy1DotsLeft = 60;
mod.elroy2DotsLeft = 30;
ROUND_MODIFIERS[round++] = mod;
// Round 10
mod = mod.clone();
mod.frightTime = 5f;
ROUND_MODIFIERS[round++] = mod;
// Round 11
mod = mod.clone();
mod.frightTime = 2f;
ROUND_MODIFIERS[round++] = mod;
// Round 12 & 13
mod = mod.clone();
mod.frightTime = 1f;
mod.elroy1DotsLeft = 80;
mod.elroy2DotsLeft = 40;
ROUND_MODIFIERS[round++] = mod;
ROUND_MODIFIERS[round++] = mod;
// Round 14
mod = mod.clone();
mod.frightTime = 3f;
ROUND_MODIFIERS[round++] = mod;
// Round 15 & 16
mod = mod.clone();
mod.frightTime = 1f;
mod.elroy1DotsLeft = 100;
mod.elroy2DotsLeft = 50;
ROUND_MODIFIERS[round++] = mod;
ROUND_MODIFIERS[round++] = mod;
// Round 17
mod = mod.clone();
mod.frightTime = 0f;
ROUND_MODIFIERS[round++] = mod;
// Round 18
mod = mod.clone();
mod.frightTime = 1f;
ROUND_MODIFIERS[round++] = mod;
// Round 19 & 20
mod = mod.clone();
mod.frightTime = 0f;
mod.elroy1DotsLeft = 120;
mod.elroy2DotsLeft = 60;
ROUND_MODIFIERS[round++] = mod;
ROUND_MODIFIERS[round++] = mod;
// Round 21+
mod = mod.clone();
mod.pacmanSpeed = 0.9f;
ROUND_MODIFIERS[round++] = mod;
}
}

View File

@ -0,0 +1,22 @@
package com.me.pacman;
import java.util.Comparator;
public final class Score {
public String scorer;
public int score;
public static final Comparator<Score> SCORE_SORTER = new Comparator<Score>() {
@Override
public int compare(Score a, Score b) {
return a.score < b.score? 1 : (a.score == b.score ? 0 : -1);
}
};
public Score(String scorer, int score) {
this.scorer = scorer;
this.score = score;
}
}

View File

@ -0,0 +1,167 @@
package com.me.pacman;
import java.util.Arrays;
public class SoundManager {
public enum Effect {
BEGINNING,
BEGINNING_ALT,
FRIGHT,
CHOMP_1,
CHOMP_2,
DEATH,
EAT_FRUIT,
EAT_GHOST,
EXTRA_LIFE,
RETURN_BASE,
SIREN,
SIREN_FAST,
SIREN_FASTER,
SIREN_FASTEST
}
private float volume = 1.0f;
private boolean muted;
private PacDude game;
private long[] playing;
private long[] looping;
public SoundManager(PacDude game) {
this.game = game;
this.playing = new long[Effect.values().length];
this.looping = new long[Effect.values().length];
Arrays.fill(this.playing, -1);
Arrays.fill(this.looping, -1);
}
private float volume() {
return muted? 0f : volume;
}
public void setMuted(boolean muted) {
this.muted = muted;
}
public long play(Effect effect) {
long id = getSound(effect).play(volume());
playing[effect.ordinal()] = id;
return id;
}
public long loop(Effect effect) {
long id = getSound(effect).loop(volume());
looping[effect.ordinal()] = id;
return id;
}
public boolean isPlaying(Effect effect) {
return playing[effect.ordinal()] > -1;
}
public boolean isLooping(Effect effect) {
return looping[effect.ordinal()] > -1;
}
public void stopPlaying(Effect effect) {
com.badlogic.gdx.audio.Sound sound = getSound(effect);
long id = playing[effect.ordinal()];
if (id > -1) {
sound.stop(id);
playing[effect.ordinal()] = -1;
}
}
public void stopLooping(Effect effect) {
com.badlogic.gdx.audio.Sound sound = getSound(effect);
long id = looping[effect.ordinal()];
if (id > -1) {
sound.stop(id);
looping[effect.ordinal()] = -1;
}
}
public void stopPlays() {
for (int i = 0; i < playing.length; i++) {
if (playing[i] > -1) {
getSound(Effect.values()[i]).stop(playing[i]);
playing[i] = -1;
}
}
}
public void stopLoops() {
for (int i = 0; i < looping.length; i++) {
if (looping[i] > -1) {
getSound(Effect.values()[i]).stop(looping[i]);
looping[i] = -1;
}
}
}
public void stopAll() {
stopPlays();
stopLoops();
}
public void pauseAll() {
for (int i = 0, n = Effect.values().length; i < n; i++) {
com.badlogic.gdx.audio.Sound sound = getSound(Effect.values()[i]);
if (playing[i] > -1) {
sound.pause(playing[i]);
}
if (looping[i] > -1) {
sound.pause(looping[i]);
}
}
}
public void resumeAll() {
for (int i = 0, n = Effect.values().length; i < n; i++) {
com.badlogic.gdx.audio.Sound sound = getSound(Effect.values()[i]);
if (playing[i] > -1) {
sound.resume(playing[i]);
}
if (looping[i] > -1) {
sound.resume(looping[i]);
}
}
}
private com.badlogic.gdx.audio.Sound getSound(Effect effect) {
Assets a = game.assets;
switch (effect) {
case BEGINNING:
return a.beginning;
case BEGINNING_ALT:
return a.beginning_alt;
case FRIGHT:
return a.fright;
case CHOMP_1:
return a.chomp_1;
case CHOMP_2:
return a.chomp_2;
case DEATH:
return a.deathSound;
case EAT_FRUIT:
return a.eat_fruit;
case EAT_GHOST:
return a.eat_ghost;
case EXTRA_LIFE:
return a.extra_life;
case RETURN_BASE:
return a.return_base;
case SIREN:
return a.siren;
case SIREN_FAST:
return a.siren_fast;
case SIREN_FASTER:
return a.siren_faster;
case SIREN_FASTEST:
return a.siren_fastest;
}
return null;
}
}

View File

@ -0,0 +1,42 @@
package com.me.pacman.entity;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.entity.ai.BlinkyChaseBehaviour;
import com.me.pacman.entity.ai.StaticTargetBehaviour;
import com.me.pacman.entity.ai.Target;
import com.me.pacman.RoundModifiers;
import com.me.pacman.state.PlayState;
public class Blinky extends Ghost {
public static final Target SCATTER_TARGET = new Target((Constants.GAME_WIDTH / 8) - 2, Constants.GAME_HEIGHT / 8);
public Blinky(PlayState state, Vector2 pos, Direction direction) {
super(state, pos, direction, 0, new BlinkyChaseBehaviour(state), new StaticTargetBehaviour(state, SCATTER_TARGET), false);
}
@Override
protected float getNormalSpeed() {
int round = state.round;
if (!state.hasDied || !state.ghosts[3].inHouse) {
if (state.level.getPelletsRemaining() <= RoundModifiers.getElroy2DotsLeft(round)) {
return RoundModifiers.getElroy2Speed(round);
} else if (state.level.getPelletsRemaining() <= RoundModifiers.getElroy1DotsLeft(round)) {
return RoundModifiers.getElroy1Speed(round);
}
}
return super.getNormalSpeed();
}
@Override
public void update(float dt) {
if (inHouse) {
// Immediately leave house
leaveHouse();
}
super.update(dt);
}
}

View File

@ -0,0 +1,34 @@
package com.me.pacman.entity;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.entity.ai.ClydeChaseBehaviour;
import com.me.pacman.entity.ai.StaticTargetBehaviour;
import com.me.pacman.entity.ai.Target;
import com.me.pacman.state.PlayState;
public class Clyde extends Ghost {
public static final Target SCATTER_TARGET = new Target(1, -1);
public static final int DOT_LIMIT = 60;
public Clyde(PlayState state, Vector2 pos, Direction direction) {
super(state, pos, direction, 3, new ClydeChaseBehaviour(state), new StaticTargetBehaviour(state, SCATTER_TARGET), true);
}
@Override
public void update(float dt) {
if (inHouse) {
if (state.pelletsEatenSinceDeathCounterEnabled) {
if (state.pelletsEatenSinceDeath >= 32) {
state.pelletsEatenSinceDeathCounterEnabled = false;
}
} else if (pelletCounter >= DOT_LIMIT) {
leaveHouse();
}
}
super.update(dt);
}
}

View File

@ -0,0 +1,54 @@
package com.me.pacman.entity;
import com.badlogic.gdx.math.Vector2;
public enum Direction {
UP(new Vector2(0f, 1f)),
DOWN(new Vector2(0f, -1f)),
LEFT(new Vector2(-1f, 0f)),
RIGHT(new Vector2(1f, 0f)),
;
private Vector2 vector;
Direction(Vector2 vector) {
this.vector = vector;
}
public Direction getOpposite() {
switch (this) {
case UP:
return DOWN;
case DOWN:
return UP;
case LEFT:
return RIGHT;
case RIGHT:
return LEFT;
}
return null;
}
public boolean isOpposite(Direction dir) {
return dir == getOpposite();
}
public Vector2 getVector(float scale) {
return new Vector2(this.vector).scl(scale);
}
public Vector2 getVector() {
return this.getVector(1f);
}
public static Direction fromVector(Vector2 vector) {
for (Direction dir : values()) {
if (dir.vector.hasSameDirection(vector)) {
return dir;
}
}
return null;
}
}

View File

@ -1,28 +1,54 @@
package com.me.pacman.entity;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.entity.ai.Target;
import com.me.pacman.level.LevelTile;
import com.me.pacman.state.LevelState;
public abstract class Entity {
public LevelState state;
public float x;
public float y;
public Vector2 pos;
public int age;
public Entity(LevelState state, float x, float y) {
this.state = state;
this.x = x;
this.y = y;
this.pos = new Vector2(x, y);
this.age = 0;
}
public void render(SpriteBatch batch, int offsetX, int offsetY) {
batch.draw(getSprite(), (int) (x * 8) + (offsetX - 8), (y * 8) + (offsetY - 8));
public abstract TextureRegion getSprite();
public void render() {
TextureRegion texture = getSprite();
if (texture == null) {
return;
}
state.drawSprite(texture, pos.x, pos.y);
if (Constants.DEBUG) {
state.level.renderTile(LevelTile.DEBUG, (int) pos.x, (int) pos.y, Constants.LEVEL_OFFSET_X, Constants.LEVEL_OFFSET_Y);
}
}
public abstract TextureRegion getSprite();
public boolean onSameTile(Entity other) {
return (int) pos.x == (int) other.pos.x && (int) pos.y == (int) other.pos.y;
}
public Vector2 getTileVector(float offset) {
return new Vector2((int) pos.x + offset, (int) pos.y + offset);
}
public Vector2 getTileVector() {
return new Vector2((int) pos.x, (int) pos.y);
}
public Target getAsTarget() {
return new Target(pos.x, pos.y);
}
public void update(float dt) {
this.age += 1;

View File

@ -0,0 +1,277 @@
package com.me.pacman.entity;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.entity.ai.Behaviour;
import com.me.pacman.entity.ai.ReturnToBase;
import com.me.pacman.entity.ai.Target;
import com.me.pacman.entity.path.EnterGhostHousePath;
import com.me.pacman.entity.path.ExitGhostHousePath;
import com.me.pacman.level.LevelTile;
import com.me.pacman.RoundModifiers;
import com.me.pacman.state.PlayState;
import java.util.ArrayList;
public class Ghost extends MovableEntity {
public static final float EYES_SPEED = 15f;
public static final float HOUSE_SPEED = Constants.FULL_SPEED * 0.5f;
public static final Direction[] GHOST_ORDER = { Direction.UP, Direction.LEFT, Direction.DOWN, Direction.RIGHT };
private TextureRegion[][] sprite;
private int spriteIndex;
private int counter = 0;
protected PlayState state;
public Behaviour currentBehaviour;
public Behaviour chaseBehaviour;
public Behaviour scatterBehaviour;
public Behaviour frightBehaviour;
public boolean inHouse;
public boolean caught;
public int pelletCounter;
public boolean reverse;
private int prevTileX;
private int prevTileY;
public Ghost(PlayState state, Vector2 pos, Direction direction, int spriteIndex,
Behaviour chaseBehaviour, Behaviour scatterBehaviour, boolean inHouse) {
super(state, pos.x, pos.y, RoundModifiers.getGhostSpeed(0), true, direction, 0.1f);
this.state = state;
this.spriteIndex = spriteIndex;
this.chaseBehaviour = chaseBehaviour;
this.scatterBehaviour = scatterBehaviour;
this.frightBehaviour = new FrightenedBehaviour(state);
this.inHouse = inHouse;
this.caught = false;
this.reverse = false;
sprite = state.getGame().assets.ghosts;
}
@Override
public TextureRegion getSprite() {
if (currentBehaviour instanceof ReturnToBase) {
return null;
} else if (state.frightTimer > 0 && !caught) {
if (state.frightTimer > 2) {
// Render the blue scared ghost
return sprite[0][counter % 2];
} else {
// Render the flashing white ghost
return sprite[0][(int) (state.frightTimer * 5) % 2 == 0? 2 : 0 + counter % 2];
}
} else {
return sprite[2 + (counter % 2)][spriteIndex];
}
}
@Override
public void render() {
super.render();
// draw eyes so the ghost can see
if (state.frightTimer <= 0 || caught) {
state.drawSprite(sprite[1][currDirection.ordinal()], pos.x, pos.y);
}
}
protected float getNormalSpeed() {
return RoundModifiers.getGhostSpeed(state.round);
}
@Override
public void update(float dt) {
if (age % 15 == 0) {
counter++;
}
LevelTile currentTile = state.level.getTile(pos);
if (currentTile == null || currentTile == LevelTile.TUNNEL) {
speed = RoundModifiers.getGhostTunnelSpeed(state.round);
} else if (currentBehaviour instanceof FrightenedBehaviour) {
speed = RoundModifiers.getGhostFrightSpeed(state.round);
} else if (currentBehaviour instanceof ReturnToBase) {
speed = EYES_SPEED;
} else if (inHouse) {
speed = HOUSE_SPEED;
} else {
speed = getNormalSpeed();
}
super.update(dt);
if (currentPath != null) {
if (!currentPath.finished()) {
return;
}
if (currentPath instanceof EnterGhostHousePath) {
inHouse = true;
state.updateBackgroundAudio();
}
currentPath = null;
}
int tileX = (int) pos.x;
int tileY = (int) pos.y;
if (reverse && (tileX != prevTileX || tileY != prevTileY)) {
setNextDirection(currDirection.getOpposite());
reverse = false;
}
prevTileX = tileX;
prevTileY = tileY;
if (inHouse) {
if (pos.y >= 17) {
currDirection = Direction.DOWN;
} else if (pos.y <= 16) {
currDirection = Direction.UP;
}
return;
}
if ((currentBehaviour == chaseBehaviour && state.scatter)
|| (currentBehaviour == scatterBehaviour && !state.scatter)) {
reverse = true;
updateBehaviour();
}
Direction nextDirection = getNextDirection();
if (!canMove) {
// we're stuck somewhere, let's change directions to a new valid direction
// check if we already have a nextDirection which will get us unstuck
if (nextDirection != null) {
Vector2 adjacent = nextDirection.getVector().add(pos);
LevelTile adjTile = state.level.getTile(adjacent);
if (adjTile != null && adjTile.isPassable()) {
return;
}
}
// we didn't so check other directions
for (Direction dir : GHOST_ORDER) {
if (dir.isOpposite(currDirection) || dir == currDirection) {
// don't just turn around or keep going our current direction (since we'll continue being stuck)
continue;
}
Vector2 adjacent = dir.getVector().add(pos);
LevelTile adjTile = state.level.getTile(adjacent);
if (adjTile != null && adjTile.isPassable()) {
setNextDirection(dir);
return;
}
}
}
if (nextDirection != null) {
return;
}
Target target = currentBehaviour.getTarget();
if (target == null) {
// no target, carry on current course
return;
}
if (currentBehaviour instanceof ReturnToBase && target.targetReached(pos)) {
currentPath = new EnterGhostHousePath(pos);
return;
}
// Rudimentary path finding
// Check all possible turns of the tile immediately in front of us and turn the direction that is closest
// to our target.
Vector2 ahead = new Vector2((int) pos.x, (int) pos.y).add(currDirection.getVector());
float shortest = Float.MAX_VALUE;
for (Direction dir : GHOST_ORDER) {
if (dir.isOpposite(currDirection)) {
continue;
}
Vector2 adjacent = dir.getVector().add(ahead);
LevelTile nextTile = state.level.getTile(adjacent);
if (nextTile != null && nextTile.isPassable()) {
// compute distance to target
float d = target.distance_sqr(adjacent);
if (d < shortest) {
shortest = d;
nextDirection = dir;
}
}
}
setNextDirection(nextDirection);
}
public void leaveHouse() {
currentPath = new ExitGhostHousePath(pos);
inHouse = false;
updateBehaviour();
}
public void updateBehaviour() {
if (state.frightTimer > 0 && !caught) {
currentBehaviour = frightBehaviour;
} else {
currentBehaviour = state.scatter ? scatterBehaviour : chaseBehaviour;
}
}
private class FrightenedBehaviour extends Behaviour {
private Target target;
public FrightenedBehaviour(PlayState state) {
super(state);
}
@Override
public Target getTarget() {
if (target != null && !target.targetReached(pos)) {
return target;
}
ArrayList<Vector2> adjacentTiles = new ArrayList<>(3);
ArrayList<Direction> possibleTurns = new ArrayList<>(3);
Vector2 ahead = new Vector2((int) pos.x, (int) pos.y).add(currDirection.getVector());
for (Direction dir : GHOST_ORDER) {
if (dir.isOpposite(currDirection)) {
// Don't consider going backwards
continue;
}
Vector2 adjacent = dir.getVector().add(ahead);
LevelTile nextTile = state.level.getTile(adjacent);
if (nextTile != null && nextTile.isPassable()) {
adjacentTiles.add(adjacent);
possibleTurns.add(dir);
}
}
if (possibleTurns.size() == 0) {
// No possible turns means no valid tiles ahead, which would be the tunnel.
target = null;
} else if (possibleTurns.size() == 1) {
if (possibleTurns.get(0) == currDirection) {
// can only go straight, so no target
target = null;
} else {
// turn the only possible direction
target = new Target(adjacentTiles.get(0));
}
} else {
// n-way intersection, pick random direction
int rand = state.random.nextInt(adjacentTiles.size());
target = new Target(adjacentTiles.get(rand));
}
return target;
}
}
}

View File

@ -0,0 +1,35 @@
package com.me.pacman.entity;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.entity.ai.InkyChaseBehaviour;
import com.me.pacman.entity.ai.StaticTargetBehaviour;
import com.me.pacman.entity.ai.Target;
import com.me.pacman.state.PlayState;
public class Inky extends Ghost {
public static final Target SCATTER_TARGET = new Target(Constants.GAME_WIDTH / 8, -1);
public static final int DOT_LIMIT = 30;
public Inky(PlayState state, Vector2 pos, Direction direction) {
super(state, pos, direction, 2, new InkyChaseBehaviour(state), new StaticTargetBehaviour(state, SCATTER_TARGET), true);
}
@Override
public void update(float dt) {
if (inHouse) {
if (state.pelletsEatenSinceDeathCounterEnabled) {
if (state.pelletsEatenSinceDeath >= 17) {
leaveHouse();
}
} else if (pelletCounter >= DOT_LIMIT) {
leaveHouse();
}
}
super.update(dt);
}
}

View File

@ -1,5 +1,7 @@
package com.me.pacman.entity;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.entity.path.Path;
import com.me.pacman.level.LevelTile;
import com.me.pacman.state.LevelState;
@ -14,6 +16,8 @@ public abstract class MovableEntity extends Entity {
private Direction nextDirection = null;
public boolean canMove = true;
public Path currentPath = null;
public MovableEntity(LevelState state, float x, float y, float speed, boolean moving, Direction currDirection, float turnTolerance) {
super(state, x, y);
this.speed = speed;
@ -29,55 +33,33 @@ public abstract class MovableEntity extends Entity {
return;
}
if (currentPath != null) {
currentPath.update(dt);
currentPath.updateEntity(this);
return;
}
LevelTile nextTile = null;
if (nextDirection != null) {
boolean turned = false;
switch (nextDirection) {
case NORTH:
nextTile = state.level.getTile(x, y + 1f);
if (nextTile == null) {
if (currDirection == Direction.SOUTH) {
turned = true;
}
} else if (nextTile.isPassable() && Math.abs(x - ((int) x + 0.5f)) <= tolerance) {
x = ((int) x) + 0.5f;
nextTile = state.level.getTile(nextDirection.getVector().add(pos));
if (nextTile == null) {
// turned around immediately after existing tunnel (or trying to turn 90 degrees after entering tunnel)
if (nextDirection.isOpposite(currDirection)) {
// only allow if turning 180 degrees
turned = true;
}
} else {
if (nextTile.isPassable()) {
// it's possible to turn - are we close enough to our tile's center to make the turn?
Vector2 tileCenter = getTileVector(0.5f);
if (pos.dst2(tileCenter) <= tolerance * tolerance) {
// yes, move us to the center of our tile
pos = tileCenter;
turned = true;
}
break;
case EAST:
nextTile = state.level.getTile(x + 1f, y);
if (nextTile == null) {
if (currDirection == Direction.WEST) {
turned = true;
}
} else if (nextTile.isPassable() && Math.abs(y - ((int) y + 0.5f)) <= tolerance) {
y = ((int) y) + 0.5f;
turned = true;
}
break;
case SOUTH:
nextTile = state.level.getTile(x, y - 1f);
if (nextTile == null) {
if (currDirection == Direction.NORTH) {
turned = true;
}
} else if (nextTile.isPassable() && Math.abs(x - ((int) x + 0.5f)) <= tolerance) {
x = ((int) x) + 0.5f;
turned = true;
}
break;
case WEST:
nextTile = state.level.getTile(x - 1f, y);
if (nextTile == null) {
if (currDirection == Direction.EAST) {
turned = true;
}
} else if (nextTile.isPassable() && Math.abs(y - ((int) y + 0.5f)) <= tolerance) {
y = ((int) y) + 0.5f;
turned = true;
}
break;
}
}
if (turned) {
currDirection = nextDirection;
@ -85,61 +67,43 @@ public abstract class MovableEntity extends Entity {
}
}
float dist = speed * dt;
LevelTile currentTile = state.level.getTile(pos);
Vector2 new_pos = currDirection.getVector(speed * dt).add(pos);
LevelTile currentTile = state.level.getTile(x, y);
nextTile = null;
float new_y = y;
float new_x = x;
switch (currDirection) {
case NORTH:
if (currentTile == null && y > state.level.height + 1) {
new_y = -1f;
canMove = true;
} else {
new_y += dist;
nextTile = state.level.getTile(new_x, new_y + 0.5f);
canMove = nextTile == null || nextTile.isPassable();
}
break;
case EAST:
if (currentTile == null && x > state.level.width + 1) {
new_x = -1f;
canMove = true;
} else {
new_x += dist;
nextTile = state.level.getTile(new_x + 0.5f, new_y);
canMove = nextTile == null || nextTile.isPassable();
}
break;
case SOUTH:
if (currentTile == null && y < 0) {
new_y = state.level.height + 1;
canMove = true;
} else {
new_y -= dist;
nextTile = state.level.getTile(new_x, new_y - 0.5f);
canMove = nextTile == null || nextTile.isPassable();
}
break;
case WEST:
if (currentTile == null && x < 0) {
new_x = state.level.width + 1;
canMove = true;
} else {
new_x -= dist;
nextTile = state.level.getTile(new_x - 0.5f, new_y);
canMove = nextTile == null || nextTile.isPassable();
}
break;
if (currentTile == null) {
// handle traveling back onto the screen
switch (currDirection) {
case UP:
if (pos.y >= state.level.height + 1f) {
new_pos.y = -1f;
}
break;
case DOWN:
if (pos.y <= 0) {
new_pos.y = state.level.height + 1f;
}
break;
case LEFT:
if (pos.x <= 0) {
new_pos.x = state.level.width + 1f;
}
break;
case RIGHT:
if (pos.x >= state.level.width + 1f) {
new_pos.x = -1f;
}
break;
}
canMove = true;
} else {
nextTile = state.level.getTile(currDirection.getVector(0.5f).add(new_pos));
canMove = nextTile == null || nextTile.isPassable();
}
// if move isn't going to collide with wall, move normally.
// otherwise, trim would-be decimal and center entity on tile
x = canMove? new_x : ((int) new_x) + 0.5f;
y = canMove? new_y : ((int) new_y) + 0.5f;
pos.x = canMove? new_pos.x : ((int) new_pos.x) + 0.5f;
pos.y = canMove? new_pos.y : ((int) new_pos.y) + 0.5f;
}
public void setNextDirection(Direction direction) {
@ -148,11 +112,8 @@ public abstract class MovableEntity extends Entity {
}
}
public enum Direction {
NORTH,
EAST,
SOUTH,
WEST
public Direction getNextDirection() {
return nextDirection;
}
}

View File

@ -1,46 +1,45 @@
package com.me.pacman.entity;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.level.LevelTile;
import com.me.pacman.RoundModifiers;
import com.me.pacman.state.PlayState;
public class Pacman extends MovableEntity {
public static final Vector2 HOME = new Vector2(14, 7.5f);
private TextureRegion[][] sprite;
private TextureRegion[][] death;
private PlayState state;
private int counter = 1;
private int freezeFrames = 0;
public boolean alive = true;
public int deathFrame = 0;
public Pacman(PlayState state, boolean moving) {
super(state, 14, 7.5f, 7.5f, moving, Direction.EAST, 0.3f);
super(state, HOME.x, HOME.y, Constants.FULL_SPEED * 0.8f, moving, Direction.LEFT, 0.3f);
this.state = state;
sprite = state.getGame().assets.pacman;
death = state.getGame().assets.deathAnimation;
}
@Override
public TextureRegion getSprite() {
if (!alive) {
return death[deathFrame / 4][deathFrame % 4];
}
if (!moving) {
return sprite[2][0];
}
int spriteDir;
switch(currDirection) {
case NORTH:
spriteDir = 0;
break;
case SOUTH:
spriteDir = 1;
break;
case WEST:
spriteDir = 2;
break;
default:
spriteDir = 3;
break;
}
return sprite[spriteDir][canMove ? counter % 3 : 1];
return sprite[currDirection.ordinal()][canMove ? counter % 3 : 1];
}
@Override
@ -50,24 +49,34 @@ public class Pacman extends MovableEntity {
return;
}
if (state.frightTimer > 0) {
speed = RoundModifiers.getPacmanFrightSpeed(state.round);
} else {
speed = RoundModifiers.getPacmanSpeed(state.round);
}
super.update(dt);
if (!state.paused && canMove && age % 4 == 0) {
if (canMove && age % 4 == 0) {
counter += 1;
}
LevelTile tile = state.level.getTile(x, y);
if (!alive && age % 8 == 0) {
deathFrame++;
}
LevelTile tile = state.level.getTile(pos);
if (tile == null) {
return;
}
switch (tile) {
case PELLET:
state.eatPellet(x, y);
state.eatPellet(pos.x, pos.y);
freezeFrames = 1;
break;
case POWER_PELLET:
state.eatPowerPellet(x, y);
state.eatPowerPellet(pos.x, pos.y);
freezeFrames = 3;
break;
}

View File

@ -0,0 +1,27 @@
package com.me.pacman.entity;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.entity.ai.PinkyChaseBehaviour;
import com.me.pacman.entity.ai.StaticTargetBehaviour;
import com.me.pacman.entity.ai.Target;
import com.me.pacman.state.PlayState;
public class Pinky extends Ghost {
public static final Target SCATTER_TARGET = new Target(2, Constants.GAME_HEIGHT / 8);
public Pinky(PlayState state, Vector2 pos, Direction direction) {
super(state, pos, direction, 1, new PinkyChaseBehaviour(state), new StaticTargetBehaviour(state, SCATTER_TARGET), true);
}
@Override
public void update(float dt) {
if (inHouse && (!state.pelletsEatenSinceDeathCounterEnabled || state.pelletsEatenSinceDeath >= 7)) {
leaveHouse();
}
super.update(dt);
}
}

View File

@ -0,0 +1,15 @@
package com.me.pacman.entity.ai;
import com.me.pacman.state.PlayState;
public abstract class Behaviour {
public PlayState state;
public Behaviour(PlayState state) {
this.state = state;
}
public abstract Target getTarget();
}

View File

@ -0,0 +1,16 @@
package com.me.pacman.entity.ai;
import com.me.pacman.state.PlayState;
public class BlinkyChaseBehaviour extends Behaviour {
public BlinkyChaseBehaviour(PlayState state) {
super(state);
}
@Override
public Target getTarget() {
return state.pacman.getAsTarget();
}
}

View File

@ -0,0 +1,20 @@
package com.me.pacman.entity.ai;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.entity.Clyde;
import com.me.pacman.state.PlayState;
public class ClydeChaseBehaviour extends Behaviour {
public ClydeChaseBehaviour(PlayState state) {
super(state);
}
@Override
public Target getTarget() {
Vector2 pacmanTile = state.pacman.getTileVector();
Vector2 clydeTile = state.ghosts[3].getTileVector();
// If clyde > 8 tiles away from pacman, target him directly - else, target Clyde's scatter tile
return pacmanTile.dst2(clydeTile) >= 8*8? new Target(pacmanTile) : Clyde.SCATTER_TARGET;
}
}

View File

@ -0,0 +1,20 @@
package com.me.pacman.entity.ai;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.state.PlayState;
public class InkyChaseBehaviour extends Behaviour {
public InkyChaseBehaviour(PlayState state) {
super(state);
}
@Override
public Target getTarget() {
Vector2 twoAheadPacman = state.pacman.currDirection.getVector(2).add(state.pacman.getTileVector());
Vector2 blinkyTile = state.ghosts[0].getTileVector();
float dst = twoAheadPacman.dst(blinkyTile); // distance between Blinky + pacman
Vector2 dir = twoAheadPacman.sub(blinkyTile); // direction from blink to pacman tile
return new Target(blinkyTile.add(dir.scl(dst * 2)));
}
}

View File

@ -0,0 +1,16 @@
package com.me.pacman.entity.ai;
import com.me.pacman.state.PlayState;
public class PinkyChaseBehaviour extends Behaviour {
public PinkyChaseBehaviour(PlayState state) {
super(state);
}
@Override
public Target getTarget() {
return new Target(state.pacman.currDirection.getVector(4).add(state.pacman.getTileVector()));
}
}

View File

@ -0,0 +1,18 @@
package com.me.pacman.entity.ai;
import com.me.pacman.state.PlayState;
public class ReturnToBase extends Behaviour {
final Target home = new Target(14, 19);
public ReturnToBase(PlayState state) {
super(state);
}
@Override
public Target getTarget() {
return home;
}
}

View File

@ -0,0 +1,18 @@
package com.me.pacman.entity.ai;
import com.me.pacman.state.PlayState;
public class StaticTargetBehaviour extends Behaviour {
private final Target target;
public StaticTargetBehaviour(PlayState state, Target target) {
super(state);
this.target = target;
}
@Override
public Target getTarget() {
return target;
}
}

View File

@ -0,0 +1,39 @@
package com.me.pacman.entity.ai;
import com.badlogic.gdx.math.Vector2;
public class Target {
public int x;
public int y;
public Target(int x, int y) {
this.x = x;
this.y = y;
}
public Target(float x, float y) {
this.x = (int) x;
this.y = (int) y;
}
public Target(Vector2 vector) {
this(vector.x, vector.y);
}
public float distance_sqr(Vector2 vec) {
float d_x = x - vec.x;
float d_y = y - vec.y;
// a^2 + b^2 = c^2. thanks pythagoras!
return d_x * d_x + d_y * d_y;
}
public boolean targetReached(float x, float y) {
return this.x == (int) x && this.y == (int) y;
}
public boolean targetReached(Vector2 vec) {
return targetReached(vec.x, vec.y);
}
}

View File

@ -0,0 +1,20 @@
package com.me.pacman.entity.path;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.entity.Ghost;
import java.util.ArrayList;
public class EnterGhostHousePath extends Path {
private static final ArrayList<Vector2> points = new ArrayList<Vector2>() {{
add(new Vector2(14, 19.5f));
add(new Vector2(14, 16.5f));
}};
public EnterGhostHousePath(Vector2 start) {
super(Ghost.EYES_SPEED, start);
addPoints(points);
}
}

View File

@ -0,0 +1,20 @@
package com.me.pacman.entity.path;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.entity.Ghost;
import java.util.ArrayList;
public class ExitGhostHousePath extends Path {
private static final ArrayList<Vector2> points = new ArrayList<Vector2>() {{
add(new Vector2(14, 16.5f));
add(new Vector2(14, 19.5f));
}};
public ExitGhostHousePath(Vector2 start) {
super(Ghost.HOUSE_SPEED, start);
addPoints(points);
}
}

View File

@ -0,0 +1,89 @@
package com.me.pacman.entity.path;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.entity.Direction;
import com.me.pacman.entity.MovableEntity;
import java.util.ArrayList;
import java.util.List;
public class Path {
private float speed; // speed in tiles/sec
private float elapsed; // seconds elapsed since beginning of path
private List<Vector2> points; // points in the path
private List<Float> segmentLengths;
private int pathSegments; // total path segments
private float pathLength; // total path length
private float progress; // distance along path
public Path(float speed, Vector2 start) {
this.speed = speed;
this.elapsed = 0;
this.points = new ArrayList<>();
this.segmentLengths = new ArrayList<>();
addPoint(start);
}
public void addPoints(List<Vector2> points) {
for (Vector2 point : points) {
addPoint(point);
}
}
protected void addPoint(Vector2 point) {
points.add(point);
if (points.size() == 1) {
return;
}
pathSegments++;
Vector2 previous = points.get(points.size()-2);
float length = previous.dst(point);
segmentLengths.add(length);
pathLength += length;
}
public void updateEntity(MovableEntity entity) {
if (finished()) {
return;
}
float tmp = progress;
for (int i = 0; i < pathSegments; i++) {
float segmentLength = segmentLengths.get(i);
if (tmp > segmentLength) {
tmp -= segmentLength;
continue;
}
Vector2 a = new Vector2(points.get(i));
Vector2 b = new Vector2(points.get(i+1));
Vector2 pathDir = b.sub(a).nor();
entity.currDirection = Direction.fromVector(pathDir);
entity.pos = a.add(pathDir.scl(tmp));
break;
}
}
public void reset() {
elapsed = 0;
progress = 0;
}
public boolean finished() {
return progress >= pathLength;
}
public void update(float dt) {
elapsed += dt;
progress = elapsed * speed;
}
}

View File

@ -1,33 +1,61 @@
package com.me.pacman.level;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.PacDude;
public class Level {
public static final Vector2[] GHOST_SPAWN_POINTS = {
new Vector2(14f, 19.5f),
new Vector2(14f, 16.5f),
new Vector2(12f, 16.5f),
new Vector2(16f, 16.5f),
};
private PacDude game;
private TextureRegion pellet;
private TextureRegion powerPellet;
private TextureRegion debug;
// Grid of tiles, [rows][columns]
public LevelTile[][] tiles;
// Level width and height in tiles
public int width;
public int height;
private int pellets;
private int pelletsRemaining;
public Level(PacDude game, String level) {
this.game = game;
pellet = game.assets.level[0][6];
powerPellet = game.assets.level[0][7];
debug = game.assets.level[1][7];
LevelLoader loader = new LevelLoader(level);
tiles = loader.loadLevel();
height = tiles.length;
width = tiles[0].length;
pellets = pelletsRemaining = getTileCount(LevelTile.PELLET) + getTileCount(LevelTile.POWER_PELLET);
}
public int getPelletCount() {
return pellets;
}
public int getPelletsRemaining() {
return pelletsRemaining;
}
public int getPelletsEaten() {
return pellets - pelletsRemaining;
}
public LevelTile getTile(int x, int y) {
@ -38,7 +66,15 @@ public class Level {
return getTile((int) x, (int) y);
}
public LevelTile getTile(Vector2 vec) {
return getTile(vec.x, vec.y);
}
public void setTile(int x, int y, LevelTile tile) {
LevelTile exist = tiles[y][x];
if (exist.isPellet() && !tile.isPellet()) {
pelletsRemaining--;
}
tiles[y][x] = tile;
}
@ -63,21 +99,27 @@ public class Level {
for (int i = 0; i < tiles.length; i++) {
LevelTile[] row = tiles[i];
for (int j = 0; j < row.length; j++) {
LevelTile component = row[j];
TextureRegion sprite;
switch (component) {
case PELLET:
sprite = pellet;
break;
case POWER_PELLET:
sprite = powerPellet;
break;
default:
continue;
}
game.batch.draw(sprite, (j * 8) + offsetX, (i * 8) + offsetY);
renderTile(row[j], j, i, offsetX, offsetY);
}
}
}
public void renderTile(LevelTile tile, int tileX, int tileY, int offsetX, int offsetY) {
TextureRegion sprite;
switch (tile) {
case PELLET:
sprite = pellet;
break;
case POWER_PELLET:
sprite = powerPellet;
break;
case DEBUG:
sprite = debug;
break;
default:
return;
}
game.batch.draw(sprite, (tileX * Constants.TILE_SIZE) + offsetX, (tileY * Constants.TILE_SIZE) + offsetY);
}
}

View File

@ -9,10 +9,26 @@ public enum LevelTile {
GHOST_CHAMBER,
GHOST_GATE,
EMPTY,
DEBUG,
;
public boolean isPassable() {
return this != WALL && this != GHOST_CHAMBER && this != GHOST_GATE;
switch (this) {
case WALL:
case GHOST_CHAMBER:
case GHOST_GATE:
return false;
}
return true;
}
public boolean isPellet() {
switch (this) {
case PELLET:
case POWER_PELLET:
return true;
}
return false;
}
}

View File

@ -0,0 +1,221 @@
package com.me.pacman.state;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.FontRenderer;
import com.me.pacman.Score;
import com.me.pacman.PacDude;
import com.me.pacman.entity.Direction;
import static com.me.pacman.state.HighScoresState.BUTTON;
public class HighScoreEntryState extends State {
private Texture background;
private Score score;
private final int position;
private int currLetter;
private char[] name;
private boolean flash;
private float elapsed;
private boolean buttonPressed;
private Direction nextDirection;
public HighScoreEntryState(PacDude game, Score score, int position) {
super(game);
this.position = position;
this.score = score;
this.currLetter = 0;
this.flash = false;
this.elapsed = 0f;
this.name = score.scorer.toCharArray();
this.nextDirection = null;
}
@Override
public void setup() {
this.background = game.assets.getHighScoresBackground();
Gdx.input.setInputProcessor(this.new Controller());
}
@Override
public void render() {
game.batch.draw(background, 0, 16);
game.fontRenderer.setColor(Color.YELLOW);
game.fontRenderer.draw(game.batch, "high scores", (8 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, 25 * Constants.TILE_SIZE);
game.fontRenderer.setColor(Color.CHARTREUSE);
game.fontRenderer.draw(game.batch, "score name", (8 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, 22 * Constants.TILE_SIZE);
Score[] scores = game.highScores.getHighScores(9);
for (int n = 0; n < scores.length + 1; n++) {
int y = (18 - n) * 9;
if (n == position) {
game.fontRenderer.setColor(Color.BLUE);
game.fontRenderer.draw(game.batch, "" + score.score, (8 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, y);
for (int i = 0; i < 3; i++) {
game.fontRenderer.draw(game.batch, String.valueOf((i == currLetter && flash)? '_' : name[i]), (16 + i) * Constants.TILE_SIZE + Constants.TILE_SIZE/2, y);
}
} else {
Score score = scores[n > position? n - 1 : n];
game.fontRenderer.setColor(Color.WHITE);
game.fontRenderer.draw(game.batch, String.format("%-8d%s", score.score, score.scorer), (8 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, y);
}
}
game.fontRenderer.setColor(buttonPressed ? Color.BLUE : Color.WHITE);
game.fontRenderer.draw(game.batch, "save", 12 * Constants.TILE_SIZE, 5 * Constants.TILE_SIZE - 1);
}
@Override
public void update(float dt) {
elapsed += dt;
if (elapsed >= 1/3f) {
elapsed = 0f;
flash = !flash;
}
if (nextDirection != null) {
switch (nextDirection) {
case UP:
int nextIndex = FontRenderer.CHAR_TO_INDEX[name[currLetter]] - 1;
if (nextIndex < 0) {
nextIndex = 25;
}
flash = false;
name[currLetter] = FontRenderer.CHARS[nextIndex];
break;
case DOWN:
nextIndex = FontRenderer.CHAR_TO_INDEX[name[currLetter]] + 1;
if (nextIndex > 25) {
nextIndex = 0;
}
flash = false;
name[currLetter] = FontRenderer.CHARS[nextIndex];
break;
case LEFT:
currLetter--;
if (currLetter < 0) {
currLetter = 2;
}
flash = true;
break;
case RIGHT:
currLetter++;
if (currLetter > 2) {
currLetter = 0;
}
flash = true;
break;
}
nextDirection = null;
}
}
@Override
public void dispose() {
Gdx.input.setInputProcessor(null);
}
private void saveScoreAndReturn() {
score.scorer = String.valueOf(name);
game.highScores.addScore(score);
game.setNextState(new HighScoresState(game));
}
private final class Controller extends InputAdapter {
int downX;
int downY;
@Override
public boolean keyDown(int keycode) {
switch(keycode) {
case Input.Keys.DOWN:
nextDirection = Direction.DOWN;
break;
case Input.Keys.UP:
nextDirection = Direction.UP;
break;
case Input.Keys.RIGHT:
nextDirection = Direction.RIGHT;
break;
case Input.Keys.LEFT:
nextDirection = Direction.LEFT;
break;
case Input.Keys.ENTER:
case Input.Keys.BACK:
case Input.Keys.ESCAPE:
saveScoreAndReturn();
break;
}
elapsed = 0f;
return super.keyDown(keycode);
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Vector2 coords = game.viewport.unproject(new Vector2(screenX, screenY));
if (BUTTON.contains(coords)) {
buttonPressed = true;
}
downX = screenX;
downY = screenY;
return true;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
if (buttonPressed) {
Vector2 coords = game.viewport.unproject(new Vector2(screenX, screenY));
if (BUTTON.contains(coords)) {
saveScoreAndReturn();
}
return true;
}
buttonPressed = false;
int relaX = screenX - downX;
int relaY = screenY - downY;
if (Math.abs(relaX) < 50 && Math.abs(relaY) < 50) {
// didn't move enough to consider a swipe
return true;
}
if (Math.abs(relaX) > Math.abs(relaY)) {
// x > y, so moving left-right
if (relaX > 0) {
nextDirection = Direction.RIGHT;
} else {
nextDirection = Direction.LEFT;
}
} else {
// else, moving up/down
int nextIndex;
if (relaY > 0) {
nextDirection = Direction.DOWN;
} else {
nextDirection = Direction.UP;
}
}
return true;
}
}
}

View File

@ -0,0 +1,106 @@
package com.me.pacman.state;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.PacDude;
import com.me.pacman.Score;
public class HighScoresState extends State {
public static final MenuState.BoundingBox BUTTON = new MenuState.BoundingBox(
11 * Constants.TILE_SIZE,
5 * Constants.TILE_SIZE,
17 * Constants.TILE_SIZE,
6 * Constants.TILE_SIZE);
private Texture background;
private boolean buttonPressed = false;
public HighScoresState(PacDude game) {
super(game);
}
@Override
public void setup() {
this.background = game.assets.getHighScoresBackground();
Gdx.input.setInputProcessor(this.new Controller());
}
@Override
public void render() {
game.batch.draw(background, 0, 16);
game.fontRenderer.setColor(Color.YELLOW);
game.fontRenderer.draw(game.batch, "high scores", (8 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, 25 * Constants.TILE_SIZE);
game.fontRenderer.setColor(Color.CHARTREUSE);
game.fontRenderer.draw(game.batch, "score name", (8 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, 22 * Constants.TILE_SIZE);
game.fontRenderer.setColor(Color.WHITE);
Score[] scores = game.highScores.getHighScores(10);
for (int i = 0; i < scores.length; i++) {
Score score = scores[i];
game.fontRenderer.draw(game.batch, String.format("%-8d%s", score.score, score.scorer), (8 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, (18 - i) * 9);
}
game.fontRenderer.setColor(buttonPressed ? Color.BLUE : Color.WHITE);
game.fontRenderer.draw(game.batch, "return", 11 * Constants.TILE_SIZE, 5 * Constants.TILE_SIZE - 1);
}
@Override
public void dispose() {
Gdx.input.setInputProcessor(null);
}
@Override
public void update(float dt) {}
private final class Controller extends InputAdapter {
@Override
public boolean keyDown(int keycode) {
switch(keycode) {
case Input.Keys.ENTER:
case Input.Keys.BACK:
case Input.Keys.ESCAPE:
game.setNextState(new MenuState(game));
break;
}
return super.keyDown(keycode);
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Vector2 coords = game.viewport.unproject(new Vector2(screenX, screenY));
if (BUTTON.contains(coords)) {
buttonPressed = true;
}
return true;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
if (!buttonPressed) {
return true;
}
buttonPressed = false;
Vector2 coords = game.viewport.unproject(new Vector2(screenX, screenY));
if (BUTTON.contains(coords)) {
game.setNextState(new MenuState(game));
}
return true;
}
}
}

View File

@ -1,5 +1,7 @@
package com.me.pacman.state;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.me.pacman.Constants;
import com.me.pacman.PacDude;
import com.me.pacman.level.Level;
@ -11,4 +13,12 @@ public abstract class LevelState extends State {
super(game);
}
public void drawSprite(TextureRegion sprite, float x, float y) {
game.batch.draw(
sprite,
(int) (x * Constants.TILE_SIZE) + (Constants.LEVEL_OFFSET_X - Constants.TILE_SIZE),
(int) (y * Constants.TILE_SIZE) + (Constants.LEVEL_OFFSET_Y - Constants.TILE_SIZE)
);
}
}

View File

@ -1,15 +1,28 @@
package com.me.pacman.state;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.PacDude;
import com.me.pacman.level.Level;
import com.me.pacman.SoundManager;
public class MenuState extends LevelState {
private Texture levelBackground;
private Texture background;
private Texture logo;
private BitmapFont font;
public static final int NEW_GAME = 0;
public static final int HIGH_SCORES = 1;
private static final BoundingBox NEW_GAME_BOX = new BoundingBox(88, 66, 136, 84);
private static final BoundingBox HIGH_SCORE_BOX = new BoundingBox(88, 42, 136, 60);
private int selectedOption;
public MenuState(PacDude game) {
super(game);
@ -17,23 +30,118 @@ public class MenuState extends LevelState {
@Override
public void setup() {
levelBackground = game.assets.getLevelBackground();
background = game.assets.getMenuBackground();
logo = game.assets.getLogo();
logo.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
font = game.assets.getFont();
level = new Level(game,"level");
Gdx.input.setInputProcessor(this.new Controller());
game.sound.play(SoundManager.Effect.BEGINNING);
switch(Gdx.app.getType()) {
case Android:
case iOS:
selectedOption = -1;
break;
default:
selectedOption = 0;
break;
}
}
@Override
public void render() {
game.batch.draw(levelBackground, 0, 16);
level.render(0, 16);
game.batch.draw(logo, 0, 124, 224, 120);
game.batch.draw(background, 0, 16);
game.batch.draw(logo, 0, 140, 224, 120);
game.fontRenderer.setColor(selectedOption == 0 ? Color.BLUE : Color.WHITE);
game.fontRenderer.draw(game.batch, "new", (12 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, (9 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2);
game.fontRenderer.draw(game.batch, "game", 12 * Constants.TILE_SIZE, (8 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2);
game.fontRenderer.setColor(selectedOption == 1 ? Color.BLUE : Color.WHITE);
game.fontRenderer.draw(game.batch, "high", (12 * Constants.TILE_SIZE), (6 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2);
game.fontRenderer.draw(game.batch, "scores", (11 * Constants.TILE_SIZE), (5 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2);
}
@Override
public void update(float dt) {
public void update(float dt) {}
private final class Controller extends InputAdapter {
@Override
public boolean keyDown(int keycode) {
switch (keycode) {
case Input.Keys.UP:
selectedOption = selectedOption > 0 ? --selectedOption : 1;
game.sound.play(SoundManager.Effect.CHOMP_1);
break;
case Input.Keys.DOWN:
selectedOption = selectedOption < 1 ? ++selectedOption : 0;
game.sound.play(SoundManager.Effect.CHOMP_2);
break;
case Input.Keys.ENTER:
switch (selectedOption) {
case NEW_GAME:
game.setNextState(new PlayState(game));
break;
case HIGH_SCORES:
game.setNextState(new HighScoresState(game));
break;
}
break;
}
return super.keyDown(keycode);
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Vector2 coords = game.viewport.unproject(new Vector2(screenX, screenY));
if (NEW_GAME_BOX.contains(coords)) {
selectedOption = 0;
} else if (HIGH_SCORE_BOX.contains(coords)) {
selectedOption = 1;
}
return true;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
Vector2 coords = game.viewport.unproject(new Vector2(screenX, screenY));
if (selectedOption == 0 && NEW_GAME_BOX.contains(coords)) {
game.setNextState(new PlayState(game));
} else if (selectedOption == 1 && HIGH_SCORE_BOX.contains(coords)) {
game.setNextState(new HighScoresState(game));
} else {
selectedOption = -1;
}
return true;
}
}
@Override
public void dispose() {
Gdx.input.setInputProcessor(null);
}
public static class BoundingBox {
private int minX, minY;
private int maxX, maxY;
public BoundingBox(int minX, int minY, int maxX, int maxY) {
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
public boolean contains(int x, int y) {
return minX < x && x < maxX && minY < y && y < maxY;
}
public boolean contains(Vector2 coords) {
return contains((int) coords.x, (int) coords.y);
}
}

View File

@ -5,133 +5,357 @@ import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.me.pacman.Constants;
import com.me.pacman.PacDude;
import com.me.pacman.entity.MovableEntity;
import com.me.pacman.entity.Pacman;
import com.me.pacman.Score;
import com.me.pacman.SoundManager;
import com.me.pacman.entity.*;
import com.me.pacman.entity.ai.ReturnToBase;
import com.me.pacman.entity.path.EnterGhostHousePath;
import com.me.pacman.level.Level;
import com.me.pacman.level.LevelTile;
import com.me.pacman.RoundModifiers;
import java.util.Random;
public class PlayState extends LevelState {
private Texture levelBackground;
private BitmapFont font;
private int pelletCount;
private int pelletEatenCount;
private int score;
private int lives;
private int round;
public boolean paused = false;
private long sirenId;
private float waitTimer;
private Texture levelBackground, winBackground;
private TextureRegion lifeSprite;
private Pacman pacman;
public Random random;
public int pelletsEatenSinceDeath;
public boolean pelletsEatenSinceDeathCounterEnabled;
public boolean hasDied;
private int score;
private int highScore;
private int lives;
public int round;
private boolean paused = false;
public float secondsSinceLastDot;
public boolean scatter;
private int scatterChaseTransition;
private float scatterChaseTimer;
public float frightTimer; // remaining fright time
private int ghostsCaught; // number of ghosts caught since last power pellet
private Ghost lastGhostCaptured;
public Pacman pacman;
public Ghost[] ghosts;
public enum GameState {
PRE_NEW_GAME(2.3f),
NEW_ROUND_WAIT(4.2f),
START_ROUND_WAIT(2f),
ROUND_WON_WAIT(1f),
ROUND_WON(2f),
PACMAN_CAUGHT,
PACMAN_CAUGHT_WAIT(1f),
GHOST_CAUGHT_POINTS_WAIT(1f),
GAME_OVER(2f),
PLAYING,
;
final float timer;
GameState(float timer) {
this.timer = timer;
}
GameState() {
this(0f);
}
}
private GameState state;
private float stateTimer;
public PlayState(PacDude game) {
super(game);
}
@Override
public void setup() {
levelBackground = game.assets.getLevelBackground();
font = game.assets.getFont();
lifeSprite = game.assets.pacman[2][1];
Gdx.input.setInputProcessor(new Controller());
pelletCount = 0;
pelletEatenCount = 0;
score = 0;
lives = 3;
round = 0;
newGame();
}
@Override
public void render() {
game.batch.draw(levelBackground, 0, 16);
level.render(0, 16);
game.assets.getFont().setColor(Color.WHITE);
game.assets.getFont().draw(game.batch, "" + score, 40, 279);
for (int i = 0; i < lives; i++) {
game.batch.draw(lifeSprite, i * 16, 0);
}
pacman.render(game.batch, 0, 16);
if (paused) {
game.assets.getFont().setColor(Color.YELLOW);
game.assets.getFont().draw(game.batch, "paused", 90, 151);
return;
}
}
@Override
public void update(float dt) {
if (paused) {
return;
}
if (waitTimer > 0) {
waitTimer -= dt;
if (waitTimer <= 0) {
startGame();
} else {
public void updateBackgroundAudio() {
for (Ghost ghost : ghosts) {
if ((ghost.currentBehaviour instanceof ReturnToBase
|| ghost.currentPath instanceof EnterGhostHousePath) && !ghost.inHouse) {
if (!game.sound.isLooping(SoundManager.Effect.RETURN_BASE)) {
game.sound.stopLoops();
game.sound.loop(SoundManager.Effect.RETURN_BASE);
}
return;
}
}
pacman.update(dt);
}
public void newGame() {
round++;
game.assets.siren.stop(sirenId);
level = new Level(game,"level");
pelletCount = level.getTileCount(LevelTile.PELLET);
pelletCount += level.getTileCount(LevelTile.POWER_PELLET);
if (round == 1) {
waitTimer = 4.3f;
game.assets.beginning.play(1.0f);
} else {
waitTimer = 4.2f;
game.assets.beginning_alt.play(1.0f);
if (frightTimer > 0) {
if (!game.sound.isLooping(SoundManager.Effect.FRIGHT)) {
game.sound.stopLoops();
game.sound.loop(SoundManager.Effect.FRIGHT);
}
return;
}
if (!game.sound.isLooping(SoundManager.Effect.SIREN)) {
game.sound.stopLoops();
game.sound.loop(SoundManager.Effect.SIREN);
}
}
private void setGameState(GameState state) {
if (state == null) return;
this.state = state;
this.stateTimer = state.timer;
}
private void spawnGhosts() {
ghosts = new Ghost[4];
ghosts[0] = new Blinky(this, new Vector2(Level.GHOST_SPAWN_POINTS[0]), Constants.GHOST_SPAWN_DIRS[0]);
ghosts[1] = new Pinky(this, new Vector2(Level.GHOST_SPAWN_POINTS[1]), Constants.GHOST_SPAWN_DIRS[1]);
ghosts[2] = new Inky(this, new Vector2(Level.GHOST_SPAWN_POINTS[2]), Constants.GHOST_SPAWN_DIRS[2]);
ghosts[3] = new Clyde(this, new Vector2(Level.GHOST_SPAWN_POINTS[3]), Constants.GHOST_SPAWN_DIRS[3]);
}
public void resetGhosts() {
for (int i = 0; i < 4; i++) {
if (ghosts[i] == null) continue;
ghosts[i].pos = new Vector2(Level.GHOST_SPAWN_POINTS[i]);
ghosts[i].currDirection = Constants.GHOST_SPAWN_DIRS[i];
ghosts[i].currentPath = null;
ghosts[i].caught = false;
ghosts[i].inHouse = i > 0;
ghosts[i].updateBehaviour();
}
}
private void initializeLevel() {
level = new Level(game,"level");
pacman = new Pacman(this, false);
}
public void startGame() {
private void initializeRound() {
scatter = true;
scatterChaseTransition = 0;
pelletsEatenSinceDeath = 0;
pelletsEatenSinceDeathCounterEnabled = false;
hasDied = false;
spawnGhosts();
}
private void resetField() {
frightTimer = 0f;
ghostsCaught = 0;
lastGhostCaptured = null;
secondsSinceLastDot = 0;
scatterChaseTimer = RoundModifiers.getScatterChaseTimer(round, scatterChaseTransition);
random = new Random(897198256012865L);
pacman = new Pacman(this, false);
resetGhosts();
}
private void preNewGame() {
score = 0;
Score hs = game.highScores.getCurrentHighScore();
highScore = hs == null ? 0 : hs.score;
lives = 3;
round = 0;
initializeLevel();
game.sound.stopAll();
game.sound.play(SoundManager.Effect.BEGINNING);
}
private void newGame() {
lives--;
initializeRound();
resetField();
}
private void newRound() {
initializeLevel();
initializeRound();
resetField();
round++;
game.sound.stopAll();
game.sound.play(SoundManager.Effect.BEGINNING_ALT);
}
private void startGame() {
game.sound.loop(SoundManager.Effect.SIREN);
pacman.moving = true;
game.assets.beginning.stop();
game.assets.beginning_alt.stop();
sirenId = game.assets.siren.loop(1.0f);
if (round == 1) {
lives--;
}
private GameState endGame() {
Score[] scores = game.highScores.getHighScores(10);
boolean addToScores = scores.length < 10;
int scorePosition = scores.length;
for (int i = 0; i < scores.length; i++) {
if (score > scores[i].score) {
addToScores = true;
scorePosition = i;
break;
}
}
if (addToScores) {
game.setNextState(new HighScoreEntryState(game, new Score("aaa", score), scorePosition));
return null;
}
return GameState.PRE_NEW_GAME;
}
@Override
public void setup() {
levelBackground = game.assets.getLevelBackground();
winBackground = game.assets.getLevelWinBackground();
lifeSprite = game.assets.pacman[2][1];
Gdx.input.setInputProcessor(new Controller());
preNewGame();
setGameState(GameState.PRE_NEW_GAME);
}
public GameState stateTransition() {
switch (state) {
// The state we're transitioning /from/
case PRE_NEW_GAME:
newGame();
return GameState.START_ROUND_WAIT;
case START_ROUND_WAIT:
case NEW_ROUND_WAIT:
startGame();
return GameState.PLAYING;
case ROUND_WON_WAIT:
return GameState.ROUND_WON;
case ROUND_WON:
newRound();
return GameState.NEW_ROUND_WAIT;
case GAME_OVER:
return endGame();
case GHOST_CAUGHT_POINTS_WAIT:
updateBackgroundAudio();
return GameState.PLAYING;
case PACMAN_CAUGHT_WAIT:
game.sound.play(SoundManager.Effect.DEATH);
pacman.alive = false;
return GameState.PACMAN_CAUGHT;
}
return null;
}
@Override
public void render() {
game.fontRenderer.setColor(Color.WHITE);
game.fontRenderer.draw(game.batch, "1up", 3 * Constants.TILE_SIZE, 35 * Constants.TILE_SIZE);
// Draw score
// Determine x position based on score size.
String s = score > 0 ? Integer.toString(score) : "00";
game.fontRenderer.draw(game.batch, s, (7 - s.length()) * Constants.TILE_SIZE, 34 * Constants.TILE_SIZE);
// Draw high score
game.fontRenderer.draw(game.batch, "high score", 9 * Constants.TILE_SIZE, 35 * Constants.TILE_SIZE);
game.fontRenderer.draw(game.batch, score >= highScore? s : Integer.toString(highScore), 12 * Constants.TILE_SIZE, 34 * Constants.TILE_SIZE);
// Draw remaining lives
for (int i = 0; i < lives; i++) {
game.batch.draw(lifeSprite, i * 16, 0);
}
// Draw the level tiles
level.render(Constants.LEVEL_OFFSET_X, Constants.LEVEL_OFFSET_Y);
if (state == GameState.ROUND_WON) {
// draw flashing level background
game.batch.draw((int) (stateTimer * 4) % 2 == 0? levelBackground : winBackground, Constants.LEVEL_OFFSET_X, Constants.LEVEL_OFFSET_Y);
return;
} else {
game.batch.draw(levelBackground, Constants.LEVEL_OFFSET_X, Constants.LEVEL_OFFSET_Y);
}
if (state != GameState.GHOST_CAUGHT_POINTS_WAIT) {
pacman.render();
}
switch (state) {
case PRE_NEW_GAME:
case PACMAN_CAUGHT:
case GAME_OVER:
break;
case GHOST_CAUGHT_POINTS_WAIT:
drawSprite(game.assets.points[0][ghostsCaught-1], lastGhostCaptured.pos.x, lastGhostCaptured.pos.y);
default:
for (Ghost ghost : ghosts) {
if (state == GameState.GHOST_CAUGHT_POINTS_WAIT && ghost == lastGhostCaptured) {
continue;
}
ghost.render();
}
}
if (paused) {
game.fontRenderer.setColor(Color.YELLOW);
game.fontRenderer.draw(game.batch, "paused", 11 * Constants.TILE_SIZE, 15 * Constants.TILE_SIZE);
} else {
switch (state) {
case PRE_NEW_GAME:
case NEW_ROUND_WAIT:
case START_ROUND_WAIT:
game.fontRenderer.setColor(Color.YELLOW);
game.fontRenderer.draw(game.batch, "ready!", 11 * Constants.TILE_SIZE, 15 * Constants.TILE_SIZE);
break;
case GAME_OVER:
game.fontRenderer.setColor(Color.RED);
game.fontRenderer.draw(game.batch, "game over", (9 * Constants.TILE_SIZE) + Constants.TILE_SIZE/2, 15 * Constants.TILE_SIZE);
break;
}
}
}
private void pelletEaten(float x, float y) {
level.setTile(x, y, LevelTile.EMPTY);
if (pelletEatenCount % 2 == 0) {
game.assets.chomp_1.play(1.0f);
} else {
game.assets.chomp_2.play(1.0f);
}
pelletEatenCount++;
pelletCount--;
if (pelletCount == 0) {
newGame();
game.sound.play(level.getPelletsEaten() % 2 == 0? SoundManager.Effect.CHOMP_1 : SoundManager.Effect.CHOMP_2);
if (pelletsEatenSinceDeathCounterEnabled) {
// Increase global dot counter when enabled
pelletsEatenSinceDeath++;
} else {
// else increment appropriate ghost's individual counter
for (Ghost ghost : ghosts) {
// Increment the dot counter of the first encountered ghost in the house
// Ghosts are ordered Blinky, Pinky, Inky, Clyde. Blinky's inHouse is always false,
// Increase the dotCounter of only one ghost: the first of the above list still in the house.
if (!ghost.inHouse) continue;
ghost.pelletCounter++;
break;
}
}
secondsSinceLastDot = 0;
if (level.getPelletsRemaining() == 0) {
game.sound.stopLoops();
setGameState(GameState.ROUND_WON_WAIT);
}
}
@ -142,8 +366,235 @@ public class PlayState extends LevelState {
public void eatPowerPellet(float x, float y) {
pelletEaten(x, y);
// TODO: Enable ghost chase mode
for (Ghost ghost : ghosts) {
if (ghost.currentBehaviour instanceof ReturnToBase) continue;
ghost.caught = false;
ghost.currentBehaviour = ghost.frightBehaviour;
ghost.reverse = true;
}
frightTimer = RoundModifiers.getFrightTime(round);
ghostsCaught = 0;
score += 50;
updateBackgroundAudio();
}
private void pacmanCaught() {
game.sound.stopLooping(SoundManager.Effect.SIREN);
pacman.moving = false;
pelletsEatenSinceDeath = 0;
pelletsEatenSinceDeathCounterEnabled = true;
hasDied = true;
setGameState(GameState.PACMAN_CAUGHT_WAIT);
}
private void ghostCaught(Ghost ghost) {
ghost.caught = true;
ghost.currentBehaviour = new ReturnToBase(this);
lastGhostCaptured = ghost;
ghostsCaught++;
score += ghostsCaught * 200;
game.sound.play(SoundManager.Effect.EAT_GHOST);
setGameState(GameState.GHOST_CAUGHT_POINTS_WAIT);
}
private void updateScatterTimer(float dt) {
if (scatterChaseTransition < 6) {
scatterChaseTimer -= dt;
if (scatterChaseTimer <= 0) {
scatter = !scatter;
scatterChaseTransition++;
scatterChaseTimer = RoundModifiers.getScatterChaseTimer(round, scatterChaseTransition);
}
}
}
private void updateFrightTimer(float dt) {
frightTimer -= dt;
if (frightTimer <= 0) {
for (Ghost ghost : ghosts) {
if (ghost.currentBehaviour instanceof ReturnToBase) continue;
ghost.caught = false;
ghost.currentBehaviour = ghost.chaseBehaviour;
}
updateBackgroundAudio();
}
}
private void updateSecondsSinceLastDot(float dt) {
secondsSinceLastDot += dt;
if (secondsSinceLastDot >= RoundModifiers.getForceLeaveSeconds(round)) {
// It's been 4 seconds since pacman last ate a dot, he's tryin' to avoid ghosts coming out!
// We'll get him...
for (int i = 1; i < 4; i++) {
Ghost ghost = ghosts[i];
if (ghost.inHouse) {
ghost.leaveHouse();
secondsSinceLastDot = 0;
break;
}
}
}
}
// Returns whether pacman has been caught
private boolean handleGhostCollision(Ghost ghost) {
if (ghost.onSameTile(pacman)) {
if (frightTimer > 0 && !ghost.caught) {
ghostCaught(ghost);
} else if (!(ghost.currentBehaviour instanceof ReturnToBase)) {
pacmanCaught();
}
return true;
}
return false;
}
@Override
public void update(float dt) {
if (paused) {
return;
}
if (Constants.DEBUG) {
// Fixed time step for debugger
dt = 1/60f;
}
switch (state) {
case PLAYING:
if (frightTimer <= 0) {
updateScatterTimer(dt);
} else {
updateFrightTimer(dt);
}
updateSecondsSinceLastDot(dt);
pacman.update(dt);
for (Ghost ghost : ghosts) {
ghost.update(dt);
if (handleGhostCollision(ghost)) {
return;
}
}
break;
case PACMAN_CAUGHT:
pacman.update(dt);
if (pacman.deathFrame == 13) {
if (lives > 0) {
lives--;
resetField();
setGameState(GameState.START_ROUND_WAIT);
} else {
setGameState(GameState.GAME_OVER);
}
}
break;
case GHOST_CAUGHT_POINTS_WAIT:
for (Ghost ghost : ghosts) {
if (ghost.currentBehaviour instanceof ReturnToBase && ghost != lastGhostCaptured) {
ghost.update(dt);
}
}
break;
}
if (state.timer > 0) {
stateTimer -= dt;
if (stateTimer <= 0) {
setGameState(stateTransition());
}
}
}
private final class Controller extends InputAdapter {
int downX;
int downY;
long downTime;
@Override
public boolean keyDown(int keycode) {
switch (keycode) {
case Input.Keys.UP:
case Input.Keys.W:
pacman.setNextDirection(Direction.UP);
break;
case Input.Keys.DOWN:
case Input.Keys.S:
pacman.setNextDirection(Direction.DOWN);
break;
case Input.Keys.LEFT:
case Input.Keys.A:
pacman.setNextDirection(Direction.LEFT);
break;
case Input.Keys.RIGHT:
case Input.Keys.D:
pacman.setNextDirection(Direction.RIGHT);
break;
case Input.Keys.ESCAPE:
case Input.Keys.P:
paused = !paused;
if (paused) {
game.sound.pauseAll();
} else {
game.sound.resumeAll();
}
break;
case Input.Keys.N:
preNewGame();
setGameState(GameState.PRE_NEW_GAME);
break;
}
return super.keyDown(keycode);
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
if (Math.abs(downX - screenX) < 50 && Math.abs(downY - screenY) < 50
&& System.currentTimeMillis() - downTime < 250) {
paused = !paused;
return true;
}
downX = screenX;
downY = screenY;
downTime = System.currentTimeMillis();
return true;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
int relaX = screenX - downX;
int relaY = screenY - downY;
if (Math.abs(relaX) < 50 && Math.abs(relaY) < 50) {
// didn't move enough to consider a swipe
return true;
}
if (Math.abs(relaX) > Math.abs(relaY)) {
// x > y, so moving left-right
if (relaX > 0) {
pacman.setNextDirection(Direction.RIGHT);
} else {
pacman.setNextDirection(Direction.LEFT);
}
} else {
// else, moving up/down
if (relaY > 0) {
pacman.setNextDirection(Direction.DOWN);
} else {
pacman.setNextDirection(Direction.UP);
}
}
return true;
}
}
@Override
@ -151,30 +602,4 @@ public class PlayState extends LevelState {
Gdx.input.setInputProcessor(null);
}
private final class Controller extends InputAdapter {
@Override
public boolean keyDown(int keycode) {
switch (keycode) {
case Input.Keys.UP:
pacman.setNextDirection(MovableEntity.Direction.NORTH);
break;
case Input.Keys.DOWN:
pacman.setNextDirection(MovableEntity.Direction.SOUTH);
break;
case Input.Keys.LEFT:
pacman.setNextDirection(MovableEntity.Direction.WEST);
break;
case Input.Keys.RIGHT:
pacman.setNextDirection(MovableEntity.Direction.EAST);
break;
case Input.Keys.P:
paused = !paused;
break;
}
return super.keyDown(keycode);
}
}
}

View File

@ -10,7 +10,6 @@ public abstract class State extends ScreenAdapter {
public State(PacDude game) {
this.game = game;
this.setup();
}
@Override

View File

@ -2,10 +2,10 @@ apply plugin: "java"
sourceCompatibility = 1.7
sourceSets.main.java.srcDirs = [ "src/" ]
sourceSets.main.resources.srcDirs = ["../core/assets"]
sourceSets.main.resources.srcDirs = ["../android/assets"]
project.ext.mainClassName = "com.me.pacman.desktop.DesktopLauncher"
project.ext.assetsDir = new File("../core/assets")
project.ext.assetsDir = new File("../android/assets")
task run(dependsOn: classes, type: JavaExec) {
main = project.mainClassName

View File

@ -2,14 +2,15 @@ package com.me.pacman.desktop;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.me.pacman.Constants;
import com.me.pacman.PacDude;
public class DesktopLauncher {
public static void main (String[] arg) {
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
config.title = PacDude.TITLE + " - " + PacDude.VERSION;
config.width = PacDude.LEVEL_WIDTH * 2;
config.height = PacDude.LEVEL_HEIGHT * 2;
config.title = Constants.TITLE + " - " + Constants.VERSION;
config.width = Constants.GAME_WIDTH * 2;
config.height = Constants.GAME_HEIGHT * 2;
config.resizable = true;
new LwjglApplication(new PacDude(), config);
}

View File

@ -1,6 +1,5 @@
#Tue Dec 24 14:05:31 GET 2019
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

112
icon.svg Normal file
View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48.026459cm"
height="48.026459cm"
viewBox="0 0 480.26459 480.26459"
version="1.1"
id="svg8"
inkscape:export-filename="/home/matt/Pacman/pacdude-512.png"
inkscape:export-xdpi="26.526085"
inkscape:export-ydpi="26.526085"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="icon.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.45254834"
inkscape:cx="1007.1956"
inkscape:cy="912.64337"
inkscape:document-units="mm"
inkscape:current-layer="layer2"
showgrid="true"
fit-margin-top="4"
fit-margin-left="4"
fit-margin-right="4"
fit-margin-bottom="4"
units="cm"
inkscape:window-width="1900"
inkscape:window-height="1039"
inkscape:window-x="10"
inkscape:window-y="31"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid4686"
snapvisiblegridlinesonly="false"
originx="-35.000003"
originy="-34.990077" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Background">
<rect
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#0000ff;stroke-width:10;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt"
id="rect4736"
width="480.26459"
height="480.26459"
x="0.14373186"
y="0.11090717"
rx="50"
ry="50" />
</g>
<g
inkscape:label="Base"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-108.45938,331.50482)">
<path
style="opacity:1;fill:#ffff00;fill-opacity:1;stroke:#ffff00;stroke-width:0.26458332;stroke-opacity:1"
id="path4538"
sodipodi:type="arc"
sodipodi:cx="348.59167"
sodipodi:cy="-91.372528"
sodipodi:rx="200"
sodipodi:ry="200"
sodipodi:start="0.6981317"
sodipodi:end="5.5850536"
d="M 501.80056,37.184994 A 200,200 0 0 1 280.18765,96.565996 200,200 0 0 1 148.59167,-91.372527 200,200 0 0 1 280.18765,-279.31105 a 200,200 0 0 1 221.61291,59.381 L 348.59167,-91.372528 Z" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Glasses"
transform="translate(-10.000003,-9.7354103)">
<rect
style="opacity:1;fill:#333333;fill-opacity:1;stroke:#ffff00;stroke-width:0.26458332;stroke-opacity:1"
id="rect4545"
width="95.61042"
height="58.758335"
x="228.37498"
y="95.285896"
rx="13.999999"
ry="7.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

87
icon_foreground.svg Normal file
View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="108pt"
height="108pt"
viewBox="0 0 108 108"
version="1.1"
id="svg4773"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="icon_foreground.svg">
<defs
id="defs4767" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.1365865"
inkscape:cx="-4.5326821"
inkscape:cy="69.711018"
inkscape:document-units="pt"
inkscape:current-layer="layer1"
showgrid="true"
units="pt"
objecttolerance="1"
gridtolerance="1"
inkscape:window-width="1900"
inkscape:window-height="1039"
inkscape:window-x="10"
inkscape:window-y="31"
inkscape:window-maximized="0"
scale-x="0.35278">
<inkscape:grid
type="xygrid"
id="grid4807"
dotted="false"
snapvisiblegridlinesonly="false" />
</sodipodi:namedview>
<metadata
id="metadata4770">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-258.9)">
<path
style="opacity:1;fill:#ffff00;fill-opacity:1;stroke:#ffff00;stroke-width:0.74999523;stroke-opacity:1"
id="path4538-6"
sodipodi:type="arc"
sodipodi:cx="54.374847"
sodipodi:cy="312.52429"
sodipodi:rx="23.999851"
sodipodi:ry="23.999851"
sodipodi:start="0.6981317"
sodipodi:end="5.5850536"
d="M 72.7598,327.9511 A 23.999851,23.999851 0 0 1 46.166415,335.07678 23.999851,23.999851 0 0 1 30.374996,312.52429 23.999851,23.999851 0 0 1 46.166415,289.97181 23.999851,23.999851 0 0 1 72.7598,297.09748 L 54.374847,312.52429 Z" />
<rect
style="opacity:1;fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4828"
width="11.999926"
height="6.9999561"
x="49.499874"
y="294.65024"
rx="1.4999907"
ry="2.9999814" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1 +1 @@
include 'desktop', 'core'
include 'desktop', 'android', 'core'