Initial commit
@@ -0,0 +1,18 @@
|
|||||||
|
# https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{java,scala,groovy,kt,kts}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.gradle]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Gradle / build
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
*/build/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Android
|
||||||
|
local.properties
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
.cxx/
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ / Android Studio
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Tower
|
||||||
|
|
||||||
|
A [libGDX](https://libgdx.com/) project generated with [gdx-liftoff](https://github.com/libgdx/gdx-liftoff).
|
||||||
|
|
||||||
|
This project was generated with a template including simple application launchers and an `ApplicationAdapter` extension that draws libGDX logo.
|
||||||
|
|
||||||
|
## Platforms
|
||||||
|
|
||||||
|
- `core`: Main module with the application logic shared by all platforms.
|
||||||
|
- `lwjgl3`: Primary desktop platform using LWJGL3; was called 'desktop' in older docs.
|
||||||
|
- `android`: Android mobile platform. Needs Android SDK.
|
||||||
|
|
||||||
|
## Gradle
|
||||||
|
|
||||||
|
This project uses [Gradle](https://gradle.org/) to manage dependencies.
|
||||||
|
The Gradle wrapper was included, so you can run Gradle tasks using `gradlew.bat` or `./gradlew` commands.
|
||||||
|
Useful Gradle tasks and flags:
|
||||||
|
|
||||||
|
- `--continue`: when using this flag, errors will not stop the tasks from running.
|
||||||
|
- `--daemon`: thanks to this flag, Gradle daemon will be used to run chosen tasks.
|
||||||
|
- `--offline`: when using this flag, cached dependency archives will be used.
|
||||||
|
- `--refresh-dependencies`: this flag forces validation of all dependencies. Useful for snapshot versions.
|
||||||
|
- `android:lint`: performs Android project validation.
|
||||||
|
- `build`: builds sources and archives of every project.
|
||||||
|
- `cleanEclipse`: removes Eclipse project data.
|
||||||
|
- `cleanIdea`: removes IntelliJ project data.
|
||||||
|
- `clean`: removes `build` folders, which store compiled classes and built archives.
|
||||||
|
- `eclipse`: generates Eclipse project data.
|
||||||
|
- `idea`: generates IntelliJ project data.
|
||||||
|
- `lwjgl3:jar`: builds application's runnable jar, which can be found at `lwjgl3/build/libs`.
|
||||||
|
- `lwjgl3:run`: starts the application.
|
||||||
|
- `test`: runs unit tests (if any).
|
||||||
|
|
||||||
|
Note that most tasks that are not specific to a single project can be run with `name:` prefix, where the `name` should be replaced with the ID of a specific project.
|
||||||
|
For example, `core:clean` removes `build` folder only from the `core` project.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:fullBackupContent="true"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:isGame="true"
|
||||||
|
android:appCategory="game"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
android:theme="@style/GdxTheme">
|
||||||
|
<activity
|
||||||
|
android:name="ru.project.tower.android.AndroidLauncher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "ru.project.tower"
|
||||||
|
compileSdk 35
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
manifest.srcFile 'AndroidManifest.xml'
|
||||||
|
java.setSrcDirs(['src/main/java'])
|
||||||
|
aidl.setSrcDirs(['src/main/java'])
|
||||||
|
renderscript.setSrcDirs(['src/main/java'])
|
||||||
|
res.setSrcDirs(['res'])
|
||||||
|
assets.setSrcDirs(['../assets'])
|
||||||
|
jniLibs.setSrcDirs(['libs'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/robovm/ios/robovm.xml', 'META-INF/DEPENDENCIES.txt', 'META-INF/DEPENDENCIES',
|
||||||
|
'META-INF/dependencies.txt', '**/*.gwt.xml']
|
||||||
|
pickFirsts += ['META-INF/LICENSE.txt', 'META-INF/LICENSE', 'META-INF/license.txt', 'META-INF/LGPL2.1',
|
||||||
|
'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/notice.txt']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultConfig {
|
||||||
|
applicationId 'ru.project.tower'
|
||||||
|
minSdkVersion 19
|
||||||
|
targetSdkVersion 35
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
multiDexEnabled true
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility "8"
|
||||||
|
targetCompatibility "8"
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
// needed for AAPT2, may be needed for other tools
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations { natives }
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||||
|
implementation "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion"
|
||||||
|
implementation project(':core')
|
||||||
|
|
||||||
|
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-arm64-v8a"
|
||||||
|
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-armeabi-v7a"
|
||||||
|
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86"
|
||||||
|
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86_64"
|
||||||
|
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a"
|
||||||
|
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a"
|
||||||
|
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86"
|
||||||
|
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
tasks.register('copyAndroidNatives') {
|
||||||
|
doFirst {
|
||||||
|
file("libs/armeabi-v7a/").mkdirs()
|
||||||
|
file("libs/arm64-v8a/").mkdirs()
|
||||||
|
file("libs/x86_64/").mkdirs()
|
||||||
|
file("libs/x86/").mkdirs()
|
||||||
|
|
||||||
|
configurations.natives.copy().files.each { jar ->
|
||||||
|
def outputDir = null
|
||||||
|
if(jar.name.endsWith("natives-armeabi-v7a.jar")) outputDir = file("libs/armeabi-v7a")
|
||||||
|
if(jar.name.endsWith("natives-arm64-v8a.jar")) outputDir = file("libs/arm64-v8a")
|
||||||
|
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.matching { it.name.contains("merge") && it.name.contains("JniLibFolders") }.configureEach { packageTask ->
|
||||||
|
packageTask.dependsOn 'copyAndroidNatives'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('run', 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_SDK_ROOT"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path = "$System.env.ANDROID_SDK_ROOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
def adb = path + "/platform-tools/adb"
|
||||||
|
commandLine "$adb", 'shell', 'am', 'start', '-n', 'ru.project.tower/ru.project.tower.android.AndroidLauncher'
|
||||||
|
}
|
||||||
|
|
||||||
|
eclipse.project.name = appName + "-android"
|
||||||
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
# 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
|
||||||
|
# https://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
|
||||||
|
|
||||||
|
# Needed by the gdx-controllers official extension.
|
||||||
|
-keep class com.badlogic.gdx.controllers.android.AndroidControllers
|
||||||
|
|
||||||
|
# Needed by the Box2D official extension.
|
||||||
|
-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
# You will need the next three lines if you use scene2d for UI or gameplay.
|
||||||
|
# If you don't use scene2d at all, you can remove or comment out the next line:
|
||||||
|
-keep public class com.badlogic.gdx.scenes.scene2d.** { *; }
|
||||||
|
# You will need the next two lines if you use BitmapFont or any scene2d.ui text:
|
||||||
|
-keep public class com.badlogic.gdx.graphics.g2d.BitmapFont { *; }
|
||||||
|
# You will probably need this line in most cases:
|
||||||
|
-keep public class com.badlogic.gdx.graphics.Color { *; }
|
||||||
|
|
||||||
|
# These two lines are used with mapping files; see https://developer.android.com/build/shrink-code#retracing
|
||||||
|
-keepattributes LineNumberTable,SourceFile
|
||||||
|
-renamesourcefileattribute SourceFile
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# This file is automatically generated by Android Tools.
|
||||||
|
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||||
|
#
|
||||||
|
# This file must be checked in Version Control Systems.
|
||||||
|
#
|
||||||
|
# To customize properties used by the Ant build system edit
|
||||||
|
# "ant.properties", and override values to adapt the script to your
|
||||||
|
# project structure.
|
||||||
|
#
|
||||||
|
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
|
||||||
|
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-rules.pro
|
||||||
|
|
||||||
|
# Project target.
|
||||||
|
target=android-19
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="512dp"
|
||||||
|
android:height="512dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:pathData="M457.78,372.5l-201.78,116.5l-201.78,-116.5l0,-233l201.78,-116.5l201.78,116.5z"
|
||||||
|
android:strokeWidth="23"
|
||||||
|
android:fillColor="#4a4a4a"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M449.71,363.84l-193.71,111.84l-193.71,-111.84l0,-223.68l193.71,-111.84l193.71,111.84z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M256,67.33c-0.36,0.13 -1.07,0.4 -2,0.99 -1.67,1.07 -3.94,2.89 -6.4,5.26 -4.93,4.75 -10.72,11.66 -16.1,19.34a142.77,142.77 0,0 0,-2.95 4.41c9.43,-1.86 18.44,-2.8 27.45,-2.8 9.01,0 18.03,0.94 27.45,2.8a142.77,142.77 135,0 0,-2.95 -4.41c-5.38,-7.68 -11.17,-14.6 -16.1,-19.34 -2.46,-2.37 -4.73,-4.2 -6.4,-5.26a9.93,9.93 135,0 0,-2 -0.99zM256,108.66c-11.75,0 -23.5,1.9 -36.98,5.69C214.74,123.36 212,131.66 212,136.8l0,127.71c4.08,-13.81 7.7,-30.59 11.2,-51.52L223.2,200.8c0,-8.8 3.65,-18.74 9.03,-27.7 2.69,-4.48 5.85,-8.63 9.66,-11.94C245.69,157.86 250.4,155.2 256,155.2s10.31,2.66 14.12,5.96c3.81,3.3 6.97,7.45 9.66,11.94C285.15,182.06 288.8,192 288.8,200.8l0,12.19c3.5,20.94 7.12,37.71 11.2,51.52L300,136.8c0,-5.14 -2.74,-13.44 -7.02,-22.45 -13.47,-3.79 -25.23,-5.69 -36.98,-5.69zM185.6,150.71c-5.21,9.21 -12,23.48 -12,37.29l0,31.2l24,0l0,-31.2c0,-13.81 -6.79,-28.08 -12,-37.29zM326.4,150.71c-5.21,9.21 -12,23.48 -12,37.29l0,31.2l24,0l0,-31.2c0,-13.81 -6.79,-28.08 -12,-37.29zM256,169.6c-0.8,0 -2.49,0.54 -4.68,2.44 -2.19,1.9 -4.63,4.95 -6.74,8.46C240.35,187.54 237.6,196.8 237.6,200.8l0,131.02l-2.16,2.12c-8.49,8.33 -10.26,28.91 -10.52,42.22 5.34,2.83 14.26,5.13 23.88,5.94L248.8,341.6l14.4,0l0,40.49c9.62,-0.8 18.54,-3.11 23.88,-5.94 -0.25,-13.27 -1.97,-33.66 -10.57,-42.26L274.4,331.78L274.4,200.8c0,-4 -2.75,-13.26 -6.97,-20.3 -2.11,-3.52 -4.55,-6.57 -6.74,-8.46C258.49,170.14 256.8,169.6 256,169.6zM243.2,193.6l25.6,0l0,14.4l-25.6,0l0,-14.4zM173.6,233.6l0,93.05c9.94,-8.52 17.6,-17.06 24,-27.75L197.6,233.6l-24,0zM314.4,233.6l0,65.3c6.4,10.69 14.06,19.24 24,27.75L338.4,233.6l-24,0zM223.2,276.78c-2.81,8.32 -5.87,15.8 -9.36,22.54 -7.44,14.41 -16.73,25.6 -28.37,36.03l30.94,1.55c1.73,-3.88 3.94,-7.57 6.78,-10.87l0,-49.24zM288.8,276.78l0,49.28c2.81,3.3 5,6.98 6.72,10.84l31,-1.55c-11.64,-10.42 -20.92,-21.62 -28.37,-36.03 -3.48,-6.74 -6.55,-14.22 -9.36,-22.54zM446.4,289.61c-10.84,4.52 -15.22,15.65 -21.56,33.46 0,0 -36.65,0.34 -42.7,14.05 -6.46,14.65 19.67,43.83 19.67,43.83s-21.71,-6.98 -30.34,-1.69c-9.59,5.88 -13.61,30.87 -13.61,30.87 -2.83,-0.68 -5.52,-1.24 -8.11,-1.71L341.33,383.2l8.53,0l-4.79,-8.77 -42.81,2.14 -0.65,1.2c-0,0.73 -0.01,1.56 -0.01,2.22l0,3.2l5.6,0l-8.53,25.6 17.07,-12.8 4,12.01c-11.83,2.51 -20.44,8.55 -28.72,16.43L281.6,405.6l12.8,0l-10.24,-12.49C275.61,395.79 265.83,396.8 256,396.8c-9.85,0 -19.66,-1.02 -28.22,-3.7L217.6,405.6l12.8,0l-10.31,20.62c-9.71,-4.02 -20.02,-8.02 -30.08,-11.44L196.27,396l17.07,12.8L204.8,383.2l5.6,0l0,-3.2c0,-0.65 -0.01,-1.47 -0.01,-2.18l-0.68,-1.24 -42.82,-2.14 -4.76,8.77l8.53,0l-7.96,23.89c-3.19,-0.65 -6.24,-1.16 -9.1,-1.49 -25.25,-2.9 -31.78,3.71 -46.35,9.93 0,0 13.33,-77.26 -13.35,-94.7 -3.3,-2.16 -7.43,-3.03 -11.99,-2.98 -5.06,0.04 -10.67,1.22 -16.31,2.99L65.6,442.4l189.6,0l0.8,1.6 12.8,-25.6 18.79,9.4c-4.45,4.5 -8.93,9.46 -13.88,14.6L446.4,442.4L446.4,289.61zM168.82,348.93c-4.32,3.9 -5.52,7.7 -5.45,9.24 0.04,0.88 0.19,1 0.41,1.19 0.22,0.19 0.81,0.63 2.63,0.63l0.18,0l44.16,2.21c0.25,-3.58 0.68,-7.33 1.39,-11.11l-43.31,-2.17zM343.18,348.93l-43.35,2.17c0.72,3.78 1.15,7.53 1.41,11.11l44.18,-2.21L345.6,360c1.82,0 2.42,-0.44 2.63,-0.63 0.22,-0.19 0.37,-0.32 0.41,-1.19 0.06,-1.54 -1.14,-5.34 -5.45,-9.24zM179.2,396l5.7,17.09c-7.29,-2.35 -14.35,-4.32 -20.86,-5.72L179.2,396zM332.8,396l16.44,12.33c-7.58,-1.34 -14.17,-1.78 -20.05,-1.5L332.8,396zM243.2,418.4l11.64,23.27c-7.9,-3.74 -18.63,-8.66 -30.68,-13.75L243.2,418.4z"
|
||||||
|
android:fillColor="#ffda54"/>
|
||||||
|
</group>
|
||||||
|
<path
|
||||||
|
android:pathData="M135.76,190.05L112.46,190.05v-15.61q0,-6.81 -0.77,-8.47 -0.72,-1.72 -3.26,-1.72 -2.88,0 -3.65,2.05 -0.77,2.05 -0.77,8.85v41.61q0,6.53 0.77,8.52 0.77,1.99 3.49,1.99 2.6,0 3.38,-1.99 0.83,-1.99 0.83,-9.35v-11.23h23.3v3.49q0,13.89 -1.99,19.7 -1.94,5.81 -8.69,10.18 -6.7,4.37 -16.55,4.37 -10.24,0 -16.88,-3.71 -6.64,-3.71 -8.8,-10.24 -2.16,-6.59 -2.16,-19.76L80.7,182.58q0,-9.68 0.66,-14.5 0.66,-4.87 3.93,-9.35 3.32,-4.48 9.13,-7.03 5.87,-2.6 13.45,-2.6 10.29,0 16.99,3.98 6.7,3.98 8.8,9.96 2.1,5.92 2.1,18.48zM197.68,150.98v89.59h-23.3v-37.63h-6.97v37.63h-23.3v-89.59h23.3v32.04h6.97v-32.04zM247.27,150.98 L260.6,240.57L236.75,240.57l-1.16,-16.1h-8.36l-1.38,16.1h-24.13l11.84,-89.59zM234.93,208.59q-1.77,-15.22 -3.54,-37.57 -3.54,25.68 -4.43,37.57zM316.6,150.98v89.59h-20.42l-12.12,-40.73v40.73h-19.48v-89.59h19.48l13.06,40.34v-40.34zM379.41,183.96h-23.3v-8.13q0,-7.69 -0.66,-9.63 -0.66,-1.94 -3.15,-1.94 -2.16,0 -2.93,1.66 -0.77,1.66 -0.77,8.52v43q0,6.03 0.77,7.97 0.77,1.88 3.1,1.88 2.55,0 3.43,-2.16 0.94,-2.16 0.94,-8.41v-10.62h-4.7v-13.61h27.28v48.09h-14.66l-2.16,-6.42q-2.38,4.15 -6.03,6.25 -3.6,2.05 -8.52,2.05 -5.87,0 -11.01,-2.82 -5.09,-2.88 -7.75,-7.08 -2.66,-4.21 -3.32,-8.8 -0.66,-4.65 -0.66,-13.89v-26.62q0,-12.84 1.38,-18.65 1.38,-5.81 7.91,-10.62 6.59,-4.87 16.99,-4.87 10.24,0 16.99,4.21 6.75,4.21 8.8,10.02 2.05,5.76 2.05,16.77zM388.43,150.98h38.85v17.93h-15.55v16.99h14.55v17.04h-14.55v19.7h17.1v17.93L388.43,240.57Z"
|
||||||
|
android:strokeWidth="6.1"
|
||||||
|
android:fillColor="#f30d0d"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m202.17,286.98v17.93h-13.83v71.66h-23.3v-71.66h-13.78v-17.93zM261.05,286.98v89.59h-23.3v-37.63h-6.97v37.63L207.48,376.57v-89.59h23.3v32.04h6.97v-32.04zM293.69,286.98v89.59h-23.3v-89.59zM352.41,314.09L330.77,314.09v-6.64q0,-4.65 -0.83,-5.92 -0.83,-1.27 -2.77,-1.27 -2.1,0 -3.21,1.72 -1.05,1.72 -1.05,5.2 0,4.48 1.22,6.75 1.16,2.27 6.59,5.48 15.55,9.24 19.59,15.16 4.04,5.92 4.04,19.09 0,9.57 -2.27,14.11 -2.21,4.54 -8.63,7.64 -6.42,3.04 -14.94,3.04 -9.35,0 -15.99,-3.54 -6.59,-3.54 -8.63,-9.02 -2.05,-5.48 -2.05,-15.55v-5.87h21.64v10.9q0,5.04 0.89,6.47 0.94,1.44 3.26,1.44 2.32,0 3.43,-1.83 1.16,-1.83 1.16,-5.42 0,-7.91 -2.16,-10.35 -2.21,-2.43 -10.9,-8.13 -8.69,-5.76 -11.51,-8.36 -2.82,-2.6 -4.7,-7.19 -1.83,-4.59 -1.83,-11.73 0,-10.29 2.6,-15.05 2.66,-4.76 8.52,-7.42 5.87,-2.71 14.17,-2.71 9.08,0 15.44,2.93 6.42,2.93 8.47,7.42 2.1,4.43 2.1,15.11z"
|
||||||
|
android:strokeWidth="6.1"
|
||||||
|
android:fillColor="#f30d0d"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_background_color">#F5A623FF</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Tower</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ru.project.tower.android;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.backends.android.AndroidApplication;
|
||||||
|
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
|
||||||
|
import ru.project.tower.Main;
|
||||||
|
|
||||||
|
/** Launches the Android application. */
|
||||||
|
public class AndroidLauncher extends AndroidApplication {
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
AndroidApplicationConfiguration configuration = new AndroidApplicationConfiguration();
|
||||||
|
configuration.useImmersiveMode = true; // Recommended, but not required.
|
||||||
|
initialize(new Main(), configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
This file can be deleted if there are any other files present in this folder.
|
||||||
|
It is only here to ensure this folder is added to Git, instead of being
|
||||||
|
ignored because it is empty.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.gitkeep
|
||||||
|
hs_err_pid15412.log
|
||||||
|
libgdx.png
|
||||||
|
pop.mp3
|
||||||
|
pop.wav
|
||||||
|
roboto.ttf
|
||||||
|
shot.mp3
|
||||||
|
shot.wav
|
||||||
|
ui.ttf
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://s01.oss.sonatype.org' }
|
||||||
|
gradlePluginPortal()
|
||||||
|
mavenLocal()
|
||||||
|
google()
|
||||||
|
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||||
|
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "com.android.tools.build:gradle:8.6.1"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
apply plugin: 'eclipse'
|
||||||
|
apply plugin: 'idea'
|
||||||
|
|
||||||
|
// This allows you to "Build and run using IntelliJ IDEA", an option in IDEA's Settings.
|
||||||
|
idea {
|
||||||
|
module {
|
||||||
|
outputDir file('build/classes/java/main')
|
||||||
|
testOutputDir file('build/classes/java/test')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(subprojects - project(':android')) {
|
||||||
|
apply plugin: 'java-library'
|
||||||
|
sourceCompatibility = 8
|
||||||
|
|
||||||
|
// From https://lyze.dev/2021/04/29/libGDX-Internal-Assets-List/
|
||||||
|
// The article can be helpful when using assets.txt in your project.
|
||||||
|
tasks.register('generateAssetList') {
|
||||||
|
inputs.dir("${project.rootDir}/assets/")
|
||||||
|
// projectFolder/assets
|
||||||
|
File assetsFolder = new File("${project.rootDir}/assets/")
|
||||||
|
// projectFolder/assets/assets.txt
|
||||||
|
File assetsFile = new File(assetsFolder, "assets.txt")
|
||||||
|
// delete that file in case we've already created it
|
||||||
|
assetsFile.delete()
|
||||||
|
|
||||||
|
// iterate through all files inside that folder
|
||||||
|
// convert it to a relative path
|
||||||
|
// and append it to the file assets.txt
|
||||||
|
fileTree(assetsFolder).collect { assetsFolder.relativePath(it) }.sort().each {
|
||||||
|
assetsFile.append(it + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processResources.dependsOn 'generateAssetList'
|
||||||
|
|
||||||
|
compileJava {
|
||||||
|
options.incremental = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
version = "$projectVersion"
|
||||||
|
ext.appName = 'Tower'
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://s01.oss.sonatype.org' }
|
||||||
|
// You may want to remove the following line if you have errors downloading dependencies.
|
||||||
|
mavenLocal()
|
||||||
|
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||||
|
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eclipse.project.name = 'Tower' + '-parent'
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
|
||||||
|
eclipse.project.name = appName + '-core'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
|
||||||
|
api "com.badlogicgames.gdx:gdx:$gdxVersion"
|
||||||
|
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
|
||||||
|
testImplementation 'org.assertj:assertj-core:3.26.3'
|
||||||
|
|
||||||
|
if(enableGraalNative == 'true') {
|
||||||
|
implementation "io.github.berstanio:gdx-svmhelper-annotations:$graalHelperVersion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ru.project.tower;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx;
|
||||||
|
|
||||||
|
public final class DebugLog {
|
||||||
|
public static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
private DebugLog() {}
|
||||||
|
|
||||||
|
public static void log(String tag, String message) {
|
||||||
|
if (DEBUG) Gdx.app.log(tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void log(String tag, String message, Throwable t) {
|
||||||
|
if (DEBUG) Gdx.app.log(tag, message, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package ru.project.tower;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Preferences;
|
||||||
|
import com.badlogic.gdx.utils.Disposable;
|
||||||
|
import ru.project.tower.abilities.PlayerAbilities;
|
||||||
|
import ru.project.tower.player.PlayerStats;
|
||||||
|
import ru.project.tower.screens.gamescreen.layout.SafeAreaInsetsProvider;
|
||||||
|
import ru.project.tower.support.GameAssetService;
|
||||||
|
import ru.project.tower.support.LibGdxAssetService;
|
||||||
|
import ru.project.tower.talents.TalentTree;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class GameContext implements Disposable {
|
||||||
|
|
||||||
|
public final Preferences playerDataPrefs;
|
||||||
|
public final Preferences abilitiesPrefs;
|
||||||
|
public final GameAssetService assetService;
|
||||||
|
public final SafeAreaInsetsProvider safeAreaInsetsProvider;
|
||||||
|
public final PlayerStats playerStats;
|
||||||
|
public final PlayerAbilities playerAbilities;
|
||||||
|
public final GameState gameState;
|
||||||
|
public final TalentTree talentTree;
|
||||||
|
|
||||||
|
public GameContext(Preferences playerDataPrefs, Preferences abilitiesPrefs) {
|
||||||
|
this(
|
||||||
|
playerDataPrefs,
|
||||||
|
abilitiesPrefs,
|
||||||
|
new LibGdxAssetService(),
|
||||||
|
SafeAreaInsetsProvider.none());
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameContext(
|
||||||
|
Preferences playerDataPrefs,
|
||||||
|
Preferences abilitiesPrefs,
|
||||||
|
GameAssetService assetService) {
|
||||||
|
this(playerDataPrefs, abilitiesPrefs, assetService, SafeAreaInsetsProvider.none());
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameContext(
|
||||||
|
Preferences playerDataPrefs,
|
||||||
|
Preferences abilitiesPrefs,
|
||||||
|
GameAssetService assetService,
|
||||||
|
SafeAreaInsetsProvider safeAreaInsetsProvider) {
|
||||||
|
this.playerDataPrefs = playerDataPrefs;
|
||||||
|
this.abilitiesPrefs = abilitiesPrefs;
|
||||||
|
this.assetService = assetService;
|
||||||
|
this.safeAreaInsetsProvider = safeAreaInsetsProvider;
|
||||||
|
this.playerStats = new PlayerStats(playerDataPrefs);
|
||||||
|
this.playerAbilities = new PlayerAbilities(abilitiesPrefs);
|
||||||
|
this.gameState = new GameState();
|
||||||
|
this.talentTree = new TalentTree(this.playerStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
assetService.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
package ru.project.tower;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import ru.project.tower.abilities.AbilityKind;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class GameState {
|
||||||
|
private boolean returnedFromShop = false;
|
||||||
|
|
||||||
|
private Array<AbilityKind> selectedAbilities = new Array<>();
|
||||||
|
|
||||||
|
private RunSnapshot savedRunSnapshot;
|
||||||
|
private Array<String> spawnedBosses = new Array<>();
|
||||||
|
|
||||||
|
public GameState() {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void saveGameState(Snapshot snapshot) {
|
||||||
|
Snapshot nextSnapshot = Objects.requireNonNull(snapshot, "snapshot");
|
||||||
|
returnedFromShop = true;
|
||||||
|
savedRunSnapshot = new RunSnapshot(nextSnapshot, selectedAbilities, spawnedBosses);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public Snapshot restoreGameState() {
|
||||||
|
if (!returnedFromShop) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (savedRunSnapshot == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
selectedAbilities.clear();
|
||||||
|
selectedAbilities.addAll(savedRunSnapshot.selectedAbilities);
|
||||||
|
spawnedBosses.clear();
|
||||||
|
spawnedBosses.addAll(savedRunSnapshot.spawnedBosses);
|
||||||
|
return savedRunSnapshot.snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void resetGameState() {
|
||||||
|
DebugLog.log("GameState", "Resetting game state");
|
||||||
|
returnedFromShop = false;
|
||||||
|
|
||||||
|
selectedAbilities.clear();
|
||||||
|
spawnedBosses.clear();
|
||||||
|
savedRunSnapshot = null;
|
||||||
|
|
||||||
|
DebugLog.log("GameState", "Cleared selected abilities");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isReturnedFromShop() {
|
||||||
|
return returnedFromShop;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void setSelectedAbilities(Array<AbilityKind> abilities) {
|
||||||
|
DebugLog.log("GameState", "Setting selected abilities, count: " + abilities.size);
|
||||||
|
selectedAbilities.clear();
|
||||||
|
selectedAbilities.addAll(abilities);
|
||||||
|
for (AbilityKind ability : selectedAbilities) {
|
||||||
|
DebugLog.log("GameState", "Added ability: " + ability.persistedId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedAbilityIds(Array<String> abilityIds) {
|
||||||
|
DebugLog.log("GameState", "Setting selected ability ids, count: " + abilityIds.size);
|
||||||
|
selectedAbilities.clear();
|
||||||
|
for (String abilityId : abilityIds) {
|
||||||
|
Optional<AbilityKind> kind = AbilityKind.fromPersistedId(abilityId);
|
||||||
|
if (kind.isPresent()) {
|
||||||
|
selectedAbilities.add(kind.get());
|
||||||
|
DebugLog.log("GameState", "Added ability: " + abilityId);
|
||||||
|
} else {
|
||||||
|
DebugLog.log("GameState", "Ignoring unknown ability id: " + abilityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public Array<AbilityKind> getSelectedAbilities() {
|
||||||
|
DebugLog.log("GameState", "Getting selected abilities, count: " + selectedAbilities.size);
|
||||||
|
return copyOf(selectedAbilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<String> getSelectedAbilityIds() {
|
||||||
|
Array<String> abilityIds = new Array<>();
|
||||||
|
for (AbilityKind kind : selectedAbilities) {
|
||||||
|
abilityIds.add(kind.persistedId());
|
||||||
|
}
|
||||||
|
return abilityIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<String> getSpawnedBosses() {
|
||||||
|
return copyOf(spawnedBosses);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSpawnedBosses(Array<String> spawnedBosses) {
|
||||||
|
this.spawnedBosses.clear();
|
||||||
|
this.spawnedBosses.addAll(spawnedBosses);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Array<T> copyOf(Array<T> source) {
|
||||||
|
Array<T> copy = new Array<>();
|
||||||
|
copy.addAll(source);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RunSnapshot {
|
||||||
|
private final Snapshot snapshot;
|
||||||
|
private final Array<AbilityKind> selectedAbilities;
|
||||||
|
private final Array<String> spawnedBosses;
|
||||||
|
|
||||||
|
private RunSnapshot(
|
||||||
|
Snapshot snapshot,
|
||||||
|
Array<AbilityKind> selectedAbilities,
|
||||||
|
Array<String> spawnedBosses) {
|
||||||
|
this.snapshot = Objects.requireNonNull(snapshot, "snapshot");
|
||||||
|
this.selectedAbilities = copyOf(selectedAbilities);
|
||||||
|
this.spawnedBosses = copyOf(spawnedBosses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Snapshot {
|
||||||
|
private final int money;
|
||||||
|
private final int runCurrencyEarned;
|
||||||
|
private final HealthSnapshot health;
|
||||||
|
private final BulletSnapshot bulletStats;
|
||||||
|
private final UpgradeSnapshot upgrades;
|
||||||
|
private final WaveSnapshot wave;
|
||||||
|
private final ru.project.tower.run.RunRngStreams.Snapshot rng;
|
||||||
|
|
||||||
|
public Snapshot(
|
||||||
|
int money,
|
||||||
|
HealthSnapshot health,
|
||||||
|
BulletSnapshot bulletStats,
|
||||||
|
UpgradeSnapshot upgrades,
|
||||||
|
WaveSnapshot wave) {
|
||||||
|
this(money, health, bulletStats, upgrades, wave, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot(
|
||||||
|
int money,
|
||||||
|
int runCurrencyEarned,
|
||||||
|
HealthSnapshot health,
|
||||||
|
BulletSnapshot bulletStats,
|
||||||
|
UpgradeSnapshot upgrades,
|
||||||
|
WaveSnapshot wave) {
|
||||||
|
this(money, runCurrencyEarned, health, bulletStats, upgrades, wave, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot(
|
||||||
|
int money,
|
||||||
|
HealthSnapshot health,
|
||||||
|
BulletSnapshot bulletStats,
|
||||||
|
UpgradeSnapshot upgrades,
|
||||||
|
WaveSnapshot wave,
|
||||||
|
ru.project.tower.run.RunRngStreams.Snapshot rng) {
|
||||||
|
this(money, 0, health, bulletStats, upgrades, wave, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot(
|
||||||
|
int money,
|
||||||
|
int runCurrencyEarned,
|
||||||
|
HealthSnapshot health,
|
||||||
|
BulletSnapshot bulletStats,
|
||||||
|
UpgradeSnapshot upgrades,
|
||||||
|
WaveSnapshot wave,
|
||||||
|
ru.project.tower.run.RunRngStreams.Snapshot rng) {
|
||||||
|
this.money = money;
|
||||||
|
this.runCurrencyEarned = Math.max(0, runCurrencyEarned);
|
||||||
|
this.health = Objects.requireNonNull(health, "health");
|
||||||
|
this.bulletStats = Objects.requireNonNull(bulletStats, "bulletStats");
|
||||||
|
this.upgrades = Objects.requireNonNull(upgrades, "upgrades");
|
||||||
|
this.wave = Objects.requireNonNull(wave, "wave");
|
||||||
|
this.rng = rng;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMoney() {
|
||||||
|
return money;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRunCurrencyEarned() {
|
||||||
|
return runCurrencyEarned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HealthSnapshot getHealth() {
|
||||||
|
return health;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletSnapshot getBulletStats() {
|
||||||
|
return bulletStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpgradeSnapshot getUpgrades() {
|
||||||
|
return upgrades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaveSnapshot getWave() {
|
||||||
|
return wave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ru.project.tower.run.RunRngStreams.Snapshot getRng() {
|
||||||
|
return rng;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class HealthSnapshot {
|
||||||
|
private final int squareHealth;
|
||||||
|
private final int maxSquareHealth;
|
||||||
|
private final int healthRegenRate;
|
||||||
|
|
||||||
|
public HealthSnapshot(int squareHealth, int maxSquareHealth, int healthRegenRate) {
|
||||||
|
this.squareHealth = squareHealth;
|
||||||
|
this.maxSquareHealth = maxSquareHealth;
|
||||||
|
this.healthRegenRate = healthRegenRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSquareHealth() {
|
||||||
|
return squareHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxSquareHealth() {
|
||||||
|
return maxSquareHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHealthRegenRate() {
|
||||||
|
return healthRegenRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class BulletSnapshot {
|
||||||
|
private final float bulletSpeed;
|
||||||
|
private final int bulletDamage;
|
||||||
|
private final float shotCooldown;
|
||||||
|
|
||||||
|
public BulletSnapshot(float bulletSpeed, int bulletDamage, float shotCooldown) {
|
||||||
|
this.bulletSpeed = bulletSpeed;
|
||||||
|
this.bulletDamage = bulletDamage;
|
||||||
|
this.shotCooldown = shotCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getBulletSpeed() {
|
||||||
|
return bulletSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBulletDamage() {
|
||||||
|
return bulletDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getShotCooldown() {
|
||||||
|
return shotCooldown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class UpgradeSnapshot {
|
||||||
|
private final int healthUpgradeLevel;
|
||||||
|
private final int damageUpgradeLevel;
|
||||||
|
private final int speedUpgradeLevel;
|
||||||
|
private final int cooldownUpgradeLevel;
|
||||||
|
private final int regenUpgradeLevel;
|
||||||
|
private final int critChanceUpgradeLevel;
|
||||||
|
private final int critMultiplierUpgradeLevel;
|
||||||
|
|
||||||
|
public UpgradeSnapshot(
|
||||||
|
int healthUpgradeLevel,
|
||||||
|
int damageUpgradeLevel,
|
||||||
|
int speedUpgradeLevel,
|
||||||
|
int cooldownUpgradeLevel,
|
||||||
|
int regenUpgradeLevel,
|
||||||
|
int critChanceUpgradeLevel,
|
||||||
|
int critMultiplierUpgradeLevel) {
|
||||||
|
this.healthUpgradeLevel = healthUpgradeLevel;
|
||||||
|
this.damageUpgradeLevel = damageUpgradeLevel;
|
||||||
|
this.speedUpgradeLevel = speedUpgradeLevel;
|
||||||
|
this.cooldownUpgradeLevel = cooldownUpgradeLevel;
|
||||||
|
this.regenUpgradeLevel = regenUpgradeLevel;
|
||||||
|
this.critChanceUpgradeLevel = critChanceUpgradeLevel;
|
||||||
|
this.critMultiplierUpgradeLevel = critMultiplierUpgradeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHealthUpgradeLevel() {
|
||||||
|
return healthUpgradeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDamageUpgradeLevel() {
|
||||||
|
return damageUpgradeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSpeedUpgradeLevel() {
|
||||||
|
return speedUpgradeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCooldownUpgradeLevel() {
|
||||||
|
return cooldownUpgradeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRegenUpgradeLevel() {
|
||||||
|
return regenUpgradeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCritChanceUpgradeLevel() {
|
||||||
|
return critChanceUpgradeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCritMultiplierUpgradeLevel() {
|
||||||
|
return critMultiplierUpgradeLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class WaveSnapshot {
|
||||||
|
private final int currentWave;
|
||||||
|
private final boolean waveActive;
|
||||||
|
private final long waveStartTime;
|
||||||
|
private final long enemiesSpawnedThisWave;
|
||||||
|
private final long enemiesPerWave;
|
||||||
|
|
||||||
|
public WaveSnapshot(
|
||||||
|
int currentWave,
|
||||||
|
boolean waveActive,
|
||||||
|
long waveStartTime,
|
||||||
|
long enemiesSpawnedThisWave,
|
||||||
|
long enemiesPerWave) {
|
||||||
|
this.currentWave = currentWave;
|
||||||
|
this.waveActive = waveActive;
|
||||||
|
this.waveStartTime = waveStartTime;
|
||||||
|
this.enemiesSpawnedThisWave = enemiesSpawnedThisWave;
|
||||||
|
this.enemiesPerWave = enemiesPerWave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentWave() {
|
||||||
|
return currentWave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWaveActive() {
|
||||||
|
return waveActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getWaveStartTime() {
|
||||||
|
return waveStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getEnemiesSpawnedThisWave() {
|
||||||
|
return enemiesSpawnedThisWave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getEnemiesPerWave() {
|
||||||
|
return enemiesPerWave;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package ru.project.tower;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Game;
|
||||||
|
import com.badlogic.gdx.Gdx;
|
||||||
|
import java.util.Objects;
|
||||||
|
import ru.project.tower.navigation.ScreenNavigation;
|
||||||
|
import ru.project.tower.navigation.ScreenNavigator;
|
||||||
|
import ru.project.tower.screens.gamescreen.layout.SafeAreaInsetsProvider;
|
||||||
|
|
||||||
|
public class Main extends Game {
|
||||||
|
|
||||||
|
private static final String PLAYER_DATA_PREFS_NAME = "tower_attack_player_data";
|
||||||
|
private static final String ABILITIES_PREFS_NAME = "tower_attack_player_abilities";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private GameContext ctx;
|
||||||
|
|
||||||
|
private ScreenNavigator screenNavigator;
|
||||||
|
private final SafeAreaInsetsProvider safeAreaInsetsProvider;
|
||||||
|
|
||||||
|
public Main() {
|
||||||
|
this(SafeAreaInsetsProvider.none());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Main(SafeAreaInsetsProvider safeAreaInsetsProvider) {
|
||||||
|
this.safeAreaInsetsProvider =
|
||||||
|
Objects.requireNonNull(safeAreaInsetsProvider, "safeAreaInsetsProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final ScreenNavigator screenNavigator() {
|
||||||
|
return screenNavigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final GameContext gameContext() {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create() {
|
||||||
|
ctx =
|
||||||
|
new GameContext(
|
||||||
|
Gdx.app.getPreferences(PLAYER_DATA_PREFS_NAME),
|
||||||
|
Gdx.app.getPreferences(ABILITIES_PREFS_NAME),
|
||||||
|
new ru.project.tower.support.LibGdxAssetService(),
|
||||||
|
safeAreaInsetsProvider);
|
||||||
|
screenNavigator = ScreenNavigation.forGame(this, ctx);
|
||||||
|
screenNavigator.showMainMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
if (getScreen() != null) {
|
||||||
|
getScreen().dispose();
|
||||||
|
}
|
||||||
|
if (ctx != null) {
|
||||||
|
ctx.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package ru.project.tower.abilities;
|
||||||
|
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
|
||||||
|
public class AbilityEffect {
|
||||||
|
public float x, y;
|
||||||
|
public float radius;
|
||||||
|
public float duration;
|
||||||
|
public long startTime;
|
||||||
|
public String type;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public AbilityEffect(float x, float y, float radius, float duration, int typeId) {
|
||||||
|
this(x, y, radius, duration, typeId, GameClock.SYSTEM.nowMs(), GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbilityEffect(
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
float radius,
|
||||||
|
float duration,
|
||||||
|
int typeId,
|
||||||
|
long startTime,
|
||||||
|
GameClock clock) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.radius = radius;
|
||||||
|
this.duration = duration;
|
||||||
|
this.startTime = startTime;
|
||||||
|
this.clock = clock;
|
||||||
|
|
||||||
|
switch (typeId) {
|
||||||
|
case 0:
|
||||||
|
this.type = "freeze";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this.type = "explosion";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this.type = "shield";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
this.type = "impulse";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.type = "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return isExpired(clock.nowMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired(long now) {
|
||||||
|
return now - startTime > duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getAlpha() {
|
||||||
|
return getAlpha(clock.nowMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getAlpha(long now) {
|
||||||
|
float progress = (now - startTime) / duration;
|
||||||
|
return 1f - progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRemainingPercentage() {
|
||||||
|
return getRemainingPercentage(clock.nowMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRemainingPercentage(long now) {
|
||||||
|
float elapsed = now - startTime;
|
||||||
|
return Math.max(0f, 1f - (elapsed / duration));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package ru.project.tower.abilities;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public interface AbilityHost {
|
||||||
|
void addFloatingText(float x, float y, String text, Color color);
|
||||||
|
|
||||||
|
void createExplosion(float x, float y, Color color);
|
||||||
|
|
||||||
|
void triggerKillShake();
|
||||||
|
|
||||||
|
void triggerCritHitstop();
|
||||||
|
|
||||||
|
int effectiveBulletDamage();
|
||||||
|
|
||||||
|
float abilityScalingFactor();
|
||||||
|
|
||||||
|
float gameScale();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
long abilityCooldownReductionMs();
|
||||||
|
|
||||||
|
boolean isBoss(BaseCircleData circle);
|
||||||
|
|
||||||
|
void addMoney(int amount);
|
||||||
|
|
||||||
|
void killEnemy(BaseCircleData enemy);
|
||||||
|
|
||||||
|
|
||||||
|
void onTurretShot(Turret turret, BaseCircleData target, long now);
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package ru.project.tower.abilities;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public enum AbilityKind {
|
||||||
|
FREEZE("freeze", "has_freeze_ability", true, 8_000L, "Заморозка", "ЗАМОРОЗКА", 50, 0),
|
||||||
|
EXPLOSION("explosion", "has_explosion_ability", true, 12_000L, "Взрыв", "ВЗРЫВ", 75, 0),
|
||||||
|
SHIELD("shield", "has_shield_ability", false, 20_000L, "Щит", "ЩИТ", 60, 12),
|
||||||
|
IMPULSE("impulse", "has_impulse_ability", false, 15_000L, "Импульс", "ИМПУЛЬС", 65, 16),
|
||||||
|
DASH("dash", "has_dash_ability", false, 5_000L, "Рывок", "РЫВОК", 80, 20),
|
||||||
|
PULL("pull", "has_pull_ability", false, 10_000L, "Притяжение", "ТЯГА", 90, 24),
|
||||||
|
TURRET("turret", "has_turret_ability", false, 25_000L, "Турель", "ТУРЕЛЬ", 120, 28);
|
||||||
|
|
||||||
|
private static final AbilityKind[] ABILITY_KINDS = values();
|
||||||
|
|
||||||
|
private final String persistedId;
|
||||||
|
private final String preferenceKey;
|
||||||
|
private final boolean defaultUnlocked;
|
||||||
|
private final long baseCooldownMs;
|
||||||
|
private final String selectionName;
|
||||||
|
private final String hudLabel;
|
||||||
|
private final int shopPrice;
|
||||||
|
private final int unlockWaveRequirement;
|
||||||
|
|
||||||
|
AbilityKind(
|
||||||
|
String persistedId,
|
||||||
|
String preferenceKey,
|
||||||
|
boolean defaultUnlocked,
|
||||||
|
long baseCooldownMs,
|
||||||
|
String selectionName,
|
||||||
|
String hudLabel,
|
||||||
|
int shopPrice,
|
||||||
|
int unlockWaveRequirement) {
|
||||||
|
this.persistedId = persistedId;
|
||||||
|
this.preferenceKey = preferenceKey;
|
||||||
|
this.defaultUnlocked = defaultUnlocked;
|
||||||
|
this.baseCooldownMs = baseCooldownMs;
|
||||||
|
this.selectionName = selectionName;
|
||||||
|
this.hudLabel = hudLabel;
|
||||||
|
this.shopPrice = shopPrice;
|
||||||
|
this.unlockWaveRequirement = unlockWaveRequirement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String persistedId() {
|
||||||
|
return persistedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String preferenceKey() {
|
||||||
|
return preferenceKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean defaultUnlocked() {
|
||||||
|
return defaultUnlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long baseCooldownMs() {
|
||||||
|
return baseCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String selectionName() {
|
||||||
|
return selectionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String hudLabel() {
|
||||||
|
return hudLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int shopPrice() {
|
||||||
|
return shopPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int unlockWaveRequirement() {
|
||||||
|
return unlockWaveRequirement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<AbilityKind> fromPersistedId(String persistedId) {
|
||||||
|
if (persistedId == null) return Optional.empty();
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
if (kind.persistedId.equals(persistedId)) return Optional.of(kind);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
package ru.project.tower.abilities;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.math.MathUtils;
|
||||||
|
import com.badlogic.gdx.math.Rectangle;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Optional;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
import ru.project.tower.support.RandomSource;
|
||||||
|
import ru.project.tower.support.RangeChecks;
|
||||||
|
import ru.project.tower.systems.CircleSpatialIndex;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public final class AbilitySystem {
|
||||||
|
|
||||||
|
|
||||||
|
public static final long FREEZE_COOLDOWN = AbilityKind.FREEZE.baseCooldownMs();
|
||||||
|
public static final long EXPLOSION_COOLDOWN = AbilityKind.EXPLOSION.baseCooldownMs();
|
||||||
|
public static final long SHIELD_COOLDOWN = AbilityKind.SHIELD.baseCooldownMs();
|
||||||
|
public static final long IMPULSE_COOLDOWN = AbilityKind.IMPULSE.baseCooldownMs();
|
||||||
|
public static final long DASH_COOLDOWN = AbilityKind.DASH.baseCooldownMs();
|
||||||
|
public static final long PULL_COOLDOWN = AbilityKind.PULL.baseCooldownMs();
|
||||||
|
public static final long TURRET_COOLDOWN = AbilityKind.TURRET.baseCooldownMs();
|
||||||
|
public static final long ABILITY_COOLDOWN_MIN = 1_500L;
|
||||||
|
|
||||||
|
|
||||||
|
public static final float FREEZE_RADIUS = 150f;
|
||||||
|
public static final int FREEZE_DURATION = 3_000;
|
||||||
|
public static final int EXPLOSION_DAMAGE = 15;
|
||||||
|
public static final float EXPLOSION_RADIUS = 150f;
|
||||||
|
public static final int SHIELD_DURATION = 5_000;
|
||||||
|
public static final float IMPULSE_RADIUS = 200f;
|
||||||
|
public static final float IMPULSE_FORCE = 10_000f;
|
||||||
|
public static final int IMPULSE_DURATION = 500;
|
||||||
|
public static final int DASH_IFRAME_DURATION = 600;
|
||||||
|
public static final float DASH_REPULSE_RADIUS = 180f;
|
||||||
|
public static final float DASH_REPULSE_FORCE = 3_000f;
|
||||||
|
public static final float PULL_RADIUS = 260f;
|
||||||
|
public static final float PULL_FORCE = 6_000f;
|
||||||
|
public static final int PULL_DURATION = 700;
|
||||||
|
public static final int TURRET_LIFETIME = 12_000;
|
||||||
|
public static final long TURRET_SHOT_INTERVAL = 450L;
|
||||||
|
public static final float TURRET_RANGE = 450f;
|
||||||
|
private static final float FORCE_FALLOFF_MIN = 0.15f;
|
||||||
|
|
||||||
|
|
||||||
|
public static final float TURRET_DEPLOY_DISTANCE = 100f;
|
||||||
|
|
||||||
|
|
||||||
|
private static final Color NEON_CYAN = new Color(0.2f, 1f, 1f, 1f);
|
||||||
|
private static final Color NEON_GREEN = new Color(0.2f, 1f, 0.2f, 1f);
|
||||||
|
private static final AbilityKind[] ABILITY_KINDS = AbilityKind.values();
|
||||||
|
|
||||||
|
|
||||||
|
private final Rectangle playerSquare;
|
||||||
|
private final Array<BaseCircleData> circles;
|
||||||
|
private final PlayerAbilities playerAbilities;
|
||||||
|
private final Array<AbilityKind> selectedAbilities;
|
||||||
|
private final RandomSource random;
|
||||||
|
private final AbilityHost host;
|
||||||
|
private final GameClock clock;
|
||||||
|
private final CircleSpatialIndex turretTargetIndex = new CircleSpatialIndex();
|
||||||
|
private int lastTurretTargetCandidateChecks;
|
||||||
|
|
||||||
|
|
||||||
|
private final EnumMap<AbilityKind, Long> lastActivationTimes = new EnumMap<>(AbilityKind.class);
|
||||||
|
|
||||||
|
|
||||||
|
private boolean shieldActive = false;
|
||||||
|
private float activeShieldDuration = 0f;
|
||||||
|
private long dashIFramesUntil = 0L;
|
||||||
|
private long lastFreezeCastTime = 0L;
|
||||||
|
private float activeFreezeDuration = 0f;
|
||||||
|
|
||||||
|
private final Array<AbilityEffect> activeEffects = new Array<>();
|
||||||
|
private final Array<Turret> activeTurrets = new Array<>();
|
||||||
|
|
||||||
|
public AbilitySystem(
|
||||||
|
Rectangle playerSquare,
|
||||||
|
Array<BaseCircleData> circles,
|
||||||
|
PlayerAbilities playerAbilities,
|
||||||
|
Array<AbilityKind> selectedAbilities,
|
||||||
|
RandomSource random,
|
||||||
|
AbilityHost host) {
|
||||||
|
this(
|
||||||
|
playerSquare,
|
||||||
|
circles,
|
||||||
|
playerAbilities,
|
||||||
|
selectedAbilities,
|
||||||
|
random,
|
||||||
|
host,
|
||||||
|
GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbilitySystem(
|
||||||
|
Rectangle playerSquare,
|
||||||
|
Array<BaseCircleData> circles,
|
||||||
|
PlayerAbilities playerAbilities,
|
||||||
|
Array<AbilityKind> selectedAbilities,
|
||||||
|
RandomSource random,
|
||||||
|
AbilityHost host,
|
||||||
|
GameClock clock) {
|
||||||
|
this.playerSquare = playerSquare;
|
||||||
|
this.circles = circles;
|
||||||
|
this.playerAbilities = playerAbilities;
|
||||||
|
this.selectedAbilities = selectedAbilities;
|
||||||
|
this.random = random;
|
||||||
|
this.host = host;
|
||||||
|
this.clock = clock;
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
lastActivationTimes.put(kind, 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void activateFreeze(float x, float y, long now) {
|
||||||
|
if (!canActivate(AbilityKind.FREEZE, now)) return;
|
||||||
|
|
||||||
|
float centerX = playerSquare.x + playerSquare.width / 2f;
|
||||||
|
float centerY = playerSquare.y + playerSquare.height / 2f;
|
||||||
|
float scaling = host.abilityScalingFactor();
|
||||||
|
float radius = FREEZE_RADIUS * (1 + (scaling - 1) * 0.3f);
|
||||||
|
float duration = FREEZE_DURATION * scaling;
|
||||||
|
|
||||||
|
activeEffects.add(new AbilityEffect(centerX, centerY, radius, duration, 0, now, clock));
|
||||||
|
setLastActivationTime(AbilityKind.FREEZE, now);
|
||||||
|
|
||||||
|
float radius2 = RangeChecks.radiusSquared(radius);
|
||||||
|
for (BaseCircleData c : circles) {
|
||||||
|
if (RangeChecks.distanceSquared(centerX, centerY, c.circle.x, c.circle.y) <= radius2) {
|
||||||
|
c.velocity.set(0f, 0f);
|
||||||
|
c.setFrozen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastFreezeCastTime = now;
|
||||||
|
activeFreezeDuration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private long getCooldown(AbilityKind ability) {
|
||||||
|
if (ability == null) return 0L;
|
||||||
|
return Math.max(
|
||||||
|
ABILITY_COOLDOWN_MIN, ability.baseCooldownMs() - host.abilityCooldownReductionMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canActivate(AbilityKind ability, long now) {
|
||||||
|
return playerAbilities.hasAbility(ability)
|
||||||
|
&& selectedAbilities.contains(ability, true)
|
||||||
|
&& now - lastActivationTime(ability) >= getCooldown(ability);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long lastActivationTime(AbilityKind ability) {
|
||||||
|
Long last = lastActivationTimes.get(ability);
|
||||||
|
return last == null ? 0L : last;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLastActivationTime(AbilityKind ability, long now) {
|
||||||
|
lastActivationTimes.put(ability, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateExplosion(float x, float y, long now) {
|
||||||
|
if (!canActivate(AbilityKind.EXPLOSION, now)) return;
|
||||||
|
|
||||||
|
float centerX = playerSquare.x + playerSquare.width / 2f;
|
||||||
|
float centerY = playerSquare.y + playerSquare.height / 2f;
|
||||||
|
float scaling = host.abilityScalingFactor();
|
||||||
|
float radius = EXPLOSION_RADIUS * (1 + (scaling - 1) * 0.5f);
|
||||||
|
int damage = Math.round(EXPLOSION_DAMAGE * scaling);
|
||||||
|
|
||||||
|
activeEffects.add(new AbilityEffect(centerX, centerY, radius, 500, 1, now, clock));
|
||||||
|
setLastActivationTime(AbilityKind.EXPLOSION, now);
|
||||||
|
|
||||||
|
float radius2 = RangeChecks.radiusSquared(radius);
|
||||||
|
for (int i = circles.size - 1; i >= 0; i--) {
|
||||||
|
BaseCircleData c = circles.get(i);
|
||||||
|
if (RangeChecks.distanceSquared(centerX, centerY, c.circle.x, c.circle.y) <= radius2) {
|
||||||
|
int actual = c.isFrozen() ? damage * 2 : damage;
|
||||||
|
c.health -= actual;
|
||||||
|
if (c.isFrozen()) {
|
||||||
|
host.addFloatingText(c.getX(), c.getY(), "SHATTER!", NEON_CYAN);
|
||||||
|
}
|
||||||
|
if (c.health <= 0) {
|
||||||
|
host.killEnemy(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
host.triggerKillShake();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateShield(long now) {
|
||||||
|
if (!canActivate(AbilityKind.SHIELD, now)) return;
|
||||||
|
|
||||||
|
shieldActive = true;
|
||||||
|
float scaling = host.abilityScalingFactor();
|
||||||
|
float duration = SHIELD_DURATION * scaling;
|
||||||
|
activeShieldDuration = duration;
|
||||||
|
activeEffects.add(
|
||||||
|
new AbilityEffect(
|
||||||
|
playerSquare.x + playerSquare.width / 2f,
|
||||||
|
playerSquare.y + playerSquare.height / 2f,
|
||||||
|
playerSquare.width * 1.5f,
|
||||||
|
duration,
|
||||||
|
2,
|
||||||
|
now,
|
||||||
|
clock));
|
||||||
|
setLastActivationTime(AbilityKind.SHIELD, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void grantShieldPulse(long now, int durationMs) {
|
||||||
|
if (durationMs <= 0) return;
|
||||||
|
shieldActive = true;
|
||||||
|
activeShieldDuration = Math.max(activeShieldDuration, durationMs);
|
||||||
|
setLastActivationTime(AbilityKind.SHIELD, now);
|
||||||
|
activeEffects.add(
|
||||||
|
new AbilityEffect(
|
||||||
|
playerSquare.x + playerSquare.width / 2f,
|
||||||
|
playerSquare.y + playerSquare.height / 2f,
|
||||||
|
playerSquare.width,
|
||||||
|
durationMs,
|
||||||
|
2,
|
||||||
|
now,
|
||||||
|
clock));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reduceAllCooldowns(long amountMs) {
|
||||||
|
if (amountMs <= 0) return;
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
lastActivationTimes.put(kind, lastActivationTime(kind) - amountMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateImpulse(long now) {
|
||||||
|
if (!canActivate(AbilityKind.IMPULSE, now)) return;
|
||||||
|
|
||||||
|
float scaling = host.abilityScalingFactor();
|
||||||
|
float radius = IMPULSE_RADIUS * (1 + (scaling - 1) * 0.4f);
|
||||||
|
float force = IMPULSE_FORCE * scaling;
|
||||||
|
float centerX = playerSquare.x + playerSquare.width / 2f;
|
||||||
|
float centerY = playerSquare.y + playerSquare.height / 2f;
|
||||||
|
|
||||||
|
activeEffects.add(
|
||||||
|
new AbilityEffect(centerX, centerY, radius, IMPULSE_DURATION, 3, now, clock));
|
||||||
|
setLastActivationTime(AbilityKind.IMPULSE, now);
|
||||||
|
|
||||||
|
boolean shatteredAny = false;
|
||||||
|
float radius2 = RangeChecks.radiusSquared(radius);
|
||||||
|
for (int i = circles.size - 1; i >= 0; i--) {
|
||||||
|
BaseCircleData c = circles.get(i);
|
||||||
|
float dx = c.circle.x - centerX;
|
||||||
|
float dy = c.circle.y - centerY;
|
||||||
|
float d2 = RangeChecks.distanceSquared(centerX, centerY, c.circle.x, c.circle.y);
|
||||||
|
if (d2 <= radius2) {
|
||||||
|
if (c.isFrozen() && c.health <= 50 && !host.isBoss(c)) {
|
||||||
|
host.addFloatingText(c.getX(), c.getY(), "SHATTER!", NEON_CYAN);
|
||||||
|
host.killEnemy(c);
|
||||||
|
shatteredAny = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float d = (float) Math.sqrt(d2);
|
||||||
|
float dirX = dx / Math.max(0.1f, d);
|
||||||
|
float dirY = dy / Math.max(0.1f, d);
|
||||||
|
float forceFactor = radialForceFactor(d, radius);
|
||||||
|
float actualForce = force * forceFactor;
|
||||||
|
c.velocity.set(dirX * actualForce, dirY * actualForce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shatteredAny) host.triggerCritHitstop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateDash(long now) {
|
||||||
|
if (!canActivate(AbilityKind.DASH, now)) return;
|
||||||
|
|
||||||
|
setLastActivationTime(AbilityKind.DASH, now);
|
||||||
|
dashIFramesUntil = now + DASH_IFRAME_DURATION;
|
||||||
|
float centerX = playerSquare.x + playerSquare.width / 2f;
|
||||||
|
float centerY = playerSquare.y + playerSquare.height / 2f;
|
||||||
|
activeEffects.add(
|
||||||
|
new AbilityEffect(
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
DASH_REPULSE_RADIUS,
|
||||||
|
DASH_IFRAME_DURATION,
|
||||||
|
2,
|
||||||
|
now,
|
||||||
|
clock));
|
||||||
|
|
||||||
|
float radius2 = RangeChecks.radiusSquared(DASH_REPULSE_RADIUS);
|
||||||
|
for (BaseCircleData c : circles) {
|
||||||
|
float dx = c.circle.x - centerX;
|
||||||
|
float dy = c.circle.y - centerY;
|
||||||
|
float d2 = RangeChecks.distanceSquared(centerX, centerY, c.circle.x, c.circle.y);
|
||||||
|
if (d2 <= radius2) {
|
||||||
|
float d = (float) Math.sqrt(d2);
|
||||||
|
float dirX = dx / Math.max(0.1f, d);
|
||||||
|
float dirY = dy / Math.max(0.1f, d);
|
||||||
|
float f = DASH_REPULSE_FORCE * radialForceFactor(d, DASH_REPULSE_RADIUS);
|
||||||
|
c.velocity.set(dirX * f, dirY * f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
host.addFloatingText(centerX, centerY + 30, "DASH!", NEON_CYAN);
|
||||||
|
host.triggerKillShake();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activatePull(long now) {
|
||||||
|
if (!canActivate(AbilityKind.PULL, now)) return;
|
||||||
|
|
||||||
|
setLastActivationTime(AbilityKind.PULL, now);
|
||||||
|
float scaling = host.abilityScalingFactor();
|
||||||
|
float radius = PULL_RADIUS * (1f + (scaling - 1f) * 0.3f);
|
||||||
|
float force = PULL_FORCE * scaling;
|
||||||
|
float centerX = playerSquare.x + playerSquare.width / 2f;
|
||||||
|
float centerY = playerSquare.y + playerSquare.height / 2f;
|
||||||
|
activeEffects.add(
|
||||||
|
new AbilityEffect(centerX, centerY, radius, PULL_DURATION, 3, now, clock));
|
||||||
|
|
||||||
|
float radius2 = RangeChecks.radiusSquared(radius);
|
||||||
|
for (BaseCircleData c : circles) {
|
||||||
|
if (host.isBoss(c)) continue;
|
||||||
|
float d2 = RangeChecks.distanceSquared(centerX, centerY, c.circle.x, c.circle.y);
|
||||||
|
if (d2 <= radius2 && d2 > 1f) {
|
||||||
|
float d = (float) Math.sqrt(d2);
|
||||||
|
float dirX = (centerX - c.circle.x) / d;
|
||||||
|
float dirY = (centerY - c.circle.y) / d;
|
||||||
|
float forceFactor = radialForceFactor(d, radius);
|
||||||
|
c.velocity.set(dirX * force * forceFactor, dirY * force * forceFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float radialForceFactor(float distance, float radius) {
|
||||||
|
return Math.max(FORCE_FALLOFF_MIN, 1f - distance / radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateTurret(long now) {
|
||||||
|
if (!canActivate(AbilityKind.TURRET, now)) return;
|
||||||
|
|
||||||
|
setLastActivationTime(AbilityKind.TURRET, now);
|
||||||
|
float centerX = playerSquare.x + playerSquare.width / 2f;
|
||||||
|
float centerY = playerSquare.y + playerSquare.height / 2f;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
float angle = random.angle();
|
||||||
|
float dist = TURRET_DEPLOY_DISTANCE * host.gameScale();
|
||||||
|
float tx = centerX + MathUtils.cos(angle) * dist;
|
||||||
|
float ty = centerY + MathUtils.sin(angle) * dist;
|
||||||
|
|
||||||
|
int dmg = Math.max(1, host.effectiveBulletDamage());
|
||||||
|
activeTurrets.add(new Turret(tx, ty, now, dmg));
|
||||||
|
host.addFloatingText(tx, ty + 20, "TURRET!", NEON_GREEN);
|
||||||
|
host.triggerKillShake();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick(long now) {
|
||||||
|
lastTurretTargetCandidateChecks = 0;
|
||||||
|
if (shieldActive && now - lastActivationTime(AbilityKind.SHIELD) > activeShieldDuration)
|
||||||
|
shieldActive = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (activeFreezeDuration > 0f && now - lastFreezeCastTime > activeFreezeDuration) {
|
||||||
|
for (BaseCircleData c : circles) c.setFrozen(false);
|
||||||
|
activeFreezeDuration = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = activeEffects.size - 1; i >= 0; i--) {
|
||||||
|
if (activeEffects.get(i).isExpired(now)) activeEffects.removeIndex(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTurrets.size > 0) {
|
||||||
|
turretTargetIndex.build(circles);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = activeTurrets.size - 1; i >= 0; i--) {
|
||||||
|
Turret t = activeTurrets.get(i);
|
||||||
|
if (now - t.spawnedAt > TURRET_LIFETIME) {
|
||||||
|
activeTurrets.removeIndex(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (now - t.lastShot < TURRET_SHOT_INTERVAL) continue;
|
||||||
|
|
||||||
|
BaseCircleData target = null;
|
||||||
|
float best = Float.POSITIVE_INFINITY;
|
||||||
|
float range2 = RangeChecks.radiusSquared(TURRET_RANGE);
|
||||||
|
int candidateCount = turretTargetIndex.queryRadius(t.x, t.y, TURRET_RANGE);
|
||||||
|
for (int candidate = 0; candidate < candidateCount; candidate++) {
|
||||||
|
BaseCircleData c = circles.get(turretTargetIndex.candidateAt(candidate));
|
||||||
|
lastTurretTargetCandidateChecks++;
|
||||||
|
float d2 = RangeChecks.distanceSquared(t.x, t.y, c.circle.x, c.circle.y);
|
||||||
|
if (d2 < range2 && d2 < best) {
|
||||||
|
target = c;
|
||||||
|
best = d2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target == null) continue;
|
||||||
|
t.lastShot = now;
|
||||||
|
host.onTurretShot(t, target, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastTurretTargetCandidateChecksForTests() {
|
||||||
|
return lastTurretTargetCandidateChecks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetForNewRun() {
|
||||||
|
shieldActive = false;
|
||||||
|
activeShieldDuration = 0f;
|
||||||
|
dashIFramesUntil = 0L;
|
||||||
|
lastFreezeCastTime = 0L;
|
||||||
|
activeFreezeDuration = 0f;
|
||||||
|
activeEffects.clear();
|
||||||
|
activeTurrets.clear();
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
lastActivationTimes.put(kind, 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public long remainingCooldownMs(String ability, long now) {
|
||||||
|
Optional<AbilityKind> kind = AbilityKind.fromPersistedId(ability);
|
||||||
|
return kind.isPresent() ? remainingCooldownMs(kind.get(), now) : 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long remainingCooldownMs(AbilityKind ability, long now) {
|
||||||
|
if (ability == null) return 0L;
|
||||||
|
return Math.max(0L, getCooldown(ability) - (now - lastActivationTime(ability)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShieldActive(long now) {
|
||||||
|
return shieldActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDashInvulnerable(long now) {
|
||||||
|
return now < dashIFramesUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFreezeZoneActive(long now) {
|
||||||
|
return activeFreezeDuration > 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<AbilityEffect> getActiveEffects() {
|
||||||
|
return activeEffects;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<Turret> getActiveTurrets() {
|
||||||
|
return activeTurrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package ru.project.tower.abilities;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx;
|
||||||
|
import com.badlogic.gdx.Preferences;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Optional;
|
||||||
|
import ru.project.tower.DebugLog;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class PlayerAbilities {
|
||||||
|
private static final AbilityKind[] ABILITY_KINDS = AbilityKind.values();
|
||||||
|
|
||||||
|
private final EnumMap<AbilityKind, Boolean> ownedAbilities = new EnumMap<>(AbilityKind.class);
|
||||||
|
|
||||||
|
private final Preferences preferences;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public PlayerAbilities(Preferences preferences) {
|
||||||
|
try {
|
||||||
|
DebugLog.log("PlayerAbilities", "Initializing PlayerAbilities");
|
||||||
|
this.preferences = preferences;
|
||||||
|
DebugLog.log("PlayerAbilities", "Got preferences");
|
||||||
|
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
ownedAbilities.put(
|
||||||
|
kind, preferences.getBoolean(kind.preferenceKey(), kind.defaultUnlocked()));
|
||||||
|
}
|
||||||
|
|
||||||
|
logLoadedAbilities();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Gdx.app.error("PlayerAbilities", "Error in constructor: ", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logLoadedAbilities() {
|
||||||
|
if (DebugLog.DEBUG) {
|
||||||
|
DebugLog.log(
|
||||||
|
"PlayerAbilities",
|
||||||
|
"Loaded abilities - Freeze: "
|
||||||
|
+ hasAbility(AbilityKind.FREEZE)
|
||||||
|
+ ", Explosion: "
|
||||||
|
+ hasAbility(AbilityKind.EXPLOSION)
|
||||||
|
+ ", Shield: "
|
||||||
|
+ hasAbility(AbilityKind.SHIELD)
|
||||||
|
+ ", Impulse: "
|
||||||
|
+ hasAbility(AbilityKind.IMPULSE)
|
||||||
|
+ ", Dash: "
|
||||||
|
+ hasAbility(AbilityKind.DASH)
|
||||||
|
+ ", Pull: "
|
||||||
|
+ hasAbility(AbilityKind.PULL)
|
||||||
|
+ ", Turret: "
|
||||||
|
+ hasAbility(AbilityKind.TURRET));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public Array<AbilityKind> getPurchasedAbilities() {
|
||||||
|
try {
|
||||||
|
Array<AbilityKind> abilities = new Array<>();
|
||||||
|
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
if (hasAbility(kind)) abilities.add(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DebugLog.DEBUG) {
|
||||||
|
DebugLog.log(
|
||||||
|
"PlayerAbilities", "Getting purchased abilities, count: " + abilities.size);
|
||||||
|
for (AbilityKind ability : abilities) {
|
||||||
|
DebugLog.log("PlayerAbilities", "Available ability: " + ability.persistedId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return abilities;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Gdx.app.error("PlayerAbilities", "Error in getPurchasedAbilities: ", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int purchasedAbilityCount() {
|
||||||
|
int count = 0;
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
if (hasAbility(kind)) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<String> getPurchasedAbilityIds() {
|
||||||
|
Array<String> abilityIds = new Array<>();
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
if (hasAbility(kind)) abilityIds.add(kind.persistedId());
|
||||||
|
}
|
||||||
|
return abilityIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public boolean hasAbility(AbilityKind abilityType) {
|
||||||
|
return abilityType != null && Boolean.TRUE.equals(ownedAbilities.get(abilityType));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAbility(String abilityType) {
|
||||||
|
Optional<AbilityKind> kind = AbilityKind.fromPersistedId(abilityType);
|
||||||
|
return kind.isPresent() && hasAbility(kind.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void purchaseAbility(AbilityKind abilityType) {
|
||||||
|
if (abilityType == null) return;
|
||||||
|
ownedAbilities.put(abilityType, true);
|
||||||
|
saveData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canUnlock(AbilityKind abilityType, int maxWaveReached) {
|
||||||
|
if (abilityType == null) return false;
|
||||||
|
return maxWaveReached >= abilityType.unlockWaveRequirement();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean purchaseAbilityIfUnlocked(AbilityKind abilityType, int maxWaveReached) {
|
||||||
|
if (!canUnlock(abilityType, maxWaveReached)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
purchaseAbility(abilityType);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void purchaseAbility(String abilityType) {
|
||||||
|
Optional<AbilityKind> kind = AbilityKind.fromPersistedId(abilityType);
|
||||||
|
if (kind.isPresent()) {
|
||||||
|
purchaseAbility(kind.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void saveData() {
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
preferences.putBoolean(kind.preferenceKey(), hasAbility(kind));
|
||||||
|
}
|
||||||
|
preferences.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void clearAll() {
|
||||||
|
for (AbilityKind kind : ABILITY_KINDS) {
|
||||||
|
ownedAbilities.put(kind, false);
|
||||||
|
}
|
||||||
|
saveData();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package ru.project.tower.abilities;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public final class Turret {
|
||||||
|
public float x, y;
|
||||||
|
public long spawnedAt;
|
||||||
|
public long lastShot;
|
||||||
|
public int damage;
|
||||||
|
|
||||||
|
public Turret(float x, float y, long now, int damage) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.spawnedAt = now;
|
||||||
|
this.lastShot = now;
|
||||||
|
this.damage = damage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class BalanceEffectRegistry {
|
||||||
|
private static final float DEFAULT_RATING_FLOOR = 0f;
|
||||||
|
private static final BalanceEffectSpec[] EFFECTS = {
|
||||||
|
effect(
|
||||||
|
"attack.core_damage",
|
||||||
|
PowerAxis.DAMAGE,
|
||||||
|
BalanceEffectSpec.Owner.SHOP,
|
||||||
|
"Урон ядра",
|
||||||
|
"Базовая сила попадания.",
|
||||||
|
95f),
|
||||||
|
effect(
|
||||||
|
"attack.cadence",
|
||||||
|
PowerAxis.CADENCE,
|
||||||
|
BalanceEffectSpec.Owner.SHOP,
|
||||||
|
"Скорострельность",
|
||||||
|
"Темп стрельбы с общим floor по cooldown.",
|
||||||
|
90f),
|
||||||
|
effect(
|
||||||
|
"attack.crit",
|
||||||
|
PowerAxis.CRIT,
|
||||||
|
BalanceEffectSpec.Owner.SHOP,
|
||||||
|
"Критический контур",
|
||||||
|
"Шанс и сила критических попаданий.",
|
||||||
|
75f),
|
||||||
|
effect(
|
||||||
|
"attack.pierce",
|
||||||
|
PowerAxis.PIERCE,
|
||||||
|
BalanceEffectSpec.Owner.CARD,
|
||||||
|
"Пробитие",
|
||||||
|
"Зачистка плотных волн без прямого boss-DPS.",
|
||||||
|
70f),
|
||||||
|
effect(
|
||||||
|
"attack.aoe",
|
||||||
|
PowerAxis.AOE,
|
||||||
|
BalanceEffectSpec.Owner.CARD,
|
||||||
|
"Осколочный урон",
|
||||||
|
"Площадной урон по скоплениям.",
|
||||||
|
70f),
|
||||||
|
effect(
|
||||||
|
"attack.controlled",
|
||||||
|
PowerAxis.CONTROL,
|
||||||
|
BalanceEffectSpec.Owner.ABILITY,
|
||||||
|
"Эксплуатация контроля",
|
||||||
|
"Сила по замедленным, стянутым и замороженным целям.",
|
||||||
|
70f),
|
||||||
|
effect(
|
||||||
|
"attack.bossing",
|
||||||
|
PowerAxis.BOSSING,
|
||||||
|
BalanceEffectSpec.Owner.SHOP,
|
||||||
|
"Анти-босс модуль",
|
||||||
|
"Отдельная ось урона по элитам и боссам.",
|
||||||
|
65f),
|
||||||
|
effect(
|
||||||
|
"defense.health",
|
||||||
|
PowerAxis.HEALTH,
|
||||||
|
BalanceEffectSpec.Owner.SHOP,
|
||||||
|
"Запас корпуса",
|
||||||
|
"Максимальное здоровье и effective HP.",
|
||||||
|
95f),
|
||||||
|
effect(
|
||||||
|
"defense.regen",
|
||||||
|
PowerAxis.REGEN,
|
||||||
|
BalanceEffectSpec.Owner.SHOP,
|
||||||
|
"Ремонтный цикл",
|
||||||
|
"Восстановление в длинном забеге.",
|
||||||
|
80f),
|
||||||
|
effect(
|
||||||
|
"defense.shield",
|
||||||
|
PowerAxis.SHIELD,
|
||||||
|
BalanceEffectSpec.Owner.ABILITY,
|
||||||
|
"Щитовой контур",
|
||||||
|
"Временное здоровье и щитовые окна.",
|
||||||
|
80f),
|
||||||
|
effect(
|
||||||
|
"defense.armor",
|
||||||
|
PowerAxis.ARMOR,
|
||||||
|
BalanceEffectSpec.Owner.SHOP,
|
||||||
|
"Броня",
|
||||||
|
"Снижение контактного и снарядного урона.",
|
||||||
|
75f),
|
||||||
|
effect(
|
||||||
|
"bonus.economy",
|
||||||
|
PowerAxis.ECONOMY,
|
||||||
|
BalanceEffectSpec.Owner.SHOP,
|
||||||
|
"Экономика",
|
||||||
|
"Доход, скидки и окупаемость покупок.",
|
||||||
|
70f),
|
||||||
|
effect(
|
||||||
|
"bonus.utility",
|
||||||
|
PowerAxis.UTILITY,
|
||||||
|
BalanceEffectSpec.Owner.BONUS,
|
||||||
|
"Утилити",
|
||||||
|
"Радиус подбора, длительность и качество бонусов.",
|
||||||
|
70f),
|
||||||
|
effect(
|
||||||
|
"ability.scaling",
|
||||||
|
PowerAxis.ABILITY,
|
||||||
|
BalanceEffectSpec.Owner.TALENT,
|
||||||
|
"Мастерство способностей",
|
||||||
|
"Cooldown и сила активных способностей.",
|
||||||
|
70f)
|
||||||
|
};
|
||||||
|
private static final EnumMap<PowerAxis, BalanceEffectSpec> BY_AXIS =
|
||||||
|
new EnumMap<>(PowerAxis.class);
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (BalanceEffectSpec effect : EFFECTS) {
|
||||||
|
BY_AXIS.put(effect.axis(), effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BalanceEffectRegistry() {}
|
||||||
|
|
||||||
|
public static BalanceEffectSpec[] all() {
|
||||||
|
BalanceEffectSpec[] copy = new BalanceEffectSpec[EFFECTS.length];
|
||||||
|
System.arraycopy(EFFECTS, 0, copy, 0, EFFECTS.length);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BalanceEffectSpec forAxis(PowerAxis axis) {
|
||||||
|
BalanceEffectSpec effect = BY_AXIS.get(axis);
|
||||||
|
if (effect == null) {
|
||||||
|
throw new IllegalArgumentException("No balance effect registered for axis: " + axis);
|
||||||
|
}
|
||||||
|
return effect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<PowerAxis, BalanceEffectSpec> byAxis() {
|
||||||
|
return new EnumMap<>(BY_AXIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BalanceEffectSpec effect(
|
||||||
|
String id,
|
||||||
|
PowerAxis axis,
|
||||||
|
BalanceEffectSpec.Owner owner,
|
||||||
|
String gameName,
|
||||||
|
String description,
|
||||||
|
float ratingCap) {
|
||||||
|
return new BalanceEffectSpec(
|
||||||
|
id, axis, owner, gameName, description, DEFAULT_RATING_FLOOR, ratingCap);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class BalanceEffectSpec {
|
||||||
|
public enum Owner {
|
||||||
|
SHOP,
|
||||||
|
CARD,
|
||||||
|
TALENT,
|
||||||
|
BONUS,
|
||||||
|
ABILITY,
|
||||||
|
SIMULATION
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final PowerAxis axis;
|
||||||
|
private final Owner owner;
|
||||||
|
private final String gameName;
|
||||||
|
private final String description;
|
||||||
|
private final Float ratingFloor;
|
||||||
|
private final Float ratingCap;
|
||||||
|
|
||||||
|
BalanceEffectSpec(
|
||||||
|
String id,
|
||||||
|
PowerAxis axis,
|
||||||
|
Owner owner,
|
||||||
|
String gameName,
|
||||||
|
String description,
|
||||||
|
Float ratingFloor,
|
||||||
|
Float ratingCap) {
|
||||||
|
this.id = Objects.requireNonNull(id, "id");
|
||||||
|
this.axis = Objects.requireNonNull(axis, "axis");
|
||||||
|
this.owner = Objects.requireNonNull(owner, "owner");
|
||||||
|
this.gameName = Objects.requireNonNull(gameName, "gameName");
|
||||||
|
this.description = Objects.requireNonNull(description, "description");
|
||||||
|
this.ratingFloor = ratingFloor;
|
||||||
|
this.ratingCap = ratingCap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PowerAxis axis() {
|
||||||
|
return axis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Owner owner() {
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String gameName() {
|
||||||
|
return gameName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String description() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Float ratingFloor() {
|
||||||
|
return ratingFloor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Float ratingCap() {
|
||||||
|
return ratingCap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasLimit() {
|
||||||
|
return ratingFloor != null || ratingCap != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class BalanceReportExporter {
|
||||||
|
private BalanceReportExporter() {}
|
||||||
|
|
||||||
|
public static String export(String reportId, List<SimulationReport> reports) {
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
out.append("balance-report ").append(reportId == null ? "unnamed" : reportId).append('\n');
|
||||||
|
out.append(
|
||||||
|
"strategy,wave,firstBoss,shop,moneyEarned,moneySpent,abilities,synergies,bonusSpawned,bonusCollected,damage,cooldownMs\n");
|
||||||
|
if (reports == null) {
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
for (SimulationReport report : reports) {
|
||||||
|
out.append(report.strategyId())
|
||||||
|
.append(',')
|
||||||
|
.append(report.waveReached())
|
||||||
|
.append(',')
|
||||||
|
.append(format(report.firstBossKillSeconds()))
|
||||||
|
.append(',')
|
||||||
|
.append(report.shopPurchases())
|
||||||
|
.append(',')
|
||||||
|
.append(report.moneyEarned())
|
||||||
|
.append(',')
|
||||||
|
.append(report.moneySpent())
|
||||||
|
.append(',')
|
||||||
|
.append(report.abilityUses())
|
||||||
|
.append(',')
|
||||||
|
.append(report.triggeredSynergies())
|
||||||
|
.append(',')
|
||||||
|
.append(report.bonusSpawned())
|
||||||
|
.append(',')
|
||||||
|
.append(report.bonusCollected())
|
||||||
|
.append(',')
|
||||||
|
.append(format(report.effectiveDamage()))
|
||||||
|
.append(',')
|
||||||
|
.append(format(report.effectiveShotCooldownMs()))
|
||||||
|
.append('\n');
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(float value) {
|
||||||
|
return String.format(Locale.ROOT, "%.2f", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class BalanceRouteReport {
|
||||||
|
private final String strategyId;
|
||||||
|
private final int waveReached;
|
||||||
|
private final float firstBossKillSeconds;
|
||||||
|
private final int shopPurchases;
|
||||||
|
private final int abilityUses;
|
||||||
|
private final int selectedDamageCards;
|
||||||
|
private final int selectedFireRateCards;
|
||||||
|
private final int peakVisibleBonusBalls;
|
||||||
|
private final float effectiveDamage;
|
||||||
|
private final float effectiveShotCooldownMs;
|
||||||
|
private final EnumMap<PowerAxis, Float> effectivePowerByAxis;
|
||||||
|
private final int moneyEarned;
|
||||||
|
private final int moneySpent;
|
||||||
|
private final int economyPaybackWave;
|
||||||
|
|
||||||
|
BalanceRouteReport(
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float firstBossKillSeconds,
|
||||||
|
int shopPurchases,
|
||||||
|
int abilityUses,
|
||||||
|
int selectedDamageCards,
|
||||||
|
int selectedFireRateCards,
|
||||||
|
int peakVisibleBonusBalls,
|
||||||
|
float effectiveDamage,
|
||||||
|
float effectiveShotCooldownMs) {
|
||||||
|
this(
|
||||||
|
strategyId,
|
||||||
|
waveReached,
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopPurchases,
|
||||||
|
abilityUses,
|
||||||
|
selectedDamageCards,
|
||||||
|
selectedFireRateCards,
|
||||||
|
peakVisibleBonusBalls,
|
||||||
|
effectiveDamage,
|
||||||
|
effectiveShotCooldownMs,
|
||||||
|
emptyAxes(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
BalanceRouteReport(
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float firstBossKillSeconds,
|
||||||
|
int shopPurchases,
|
||||||
|
int abilityUses,
|
||||||
|
int selectedDamageCards,
|
||||||
|
int selectedFireRateCards,
|
||||||
|
int peakVisibleBonusBalls,
|
||||||
|
float effectiveDamage,
|
||||||
|
float effectiveShotCooldownMs,
|
||||||
|
Map<PowerAxis, Float> effectivePowerByAxis) {
|
||||||
|
this(
|
||||||
|
strategyId,
|
||||||
|
waveReached,
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopPurchases,
|
||||||
|
abilityUses,
|
||||||
|
selectedDamageCards,
|
||||||
|
selectedFireRateCards,
|
||||||
|
peakVisibleBonusBalls,
|
||||||
|
effectiveDamage,
|
||||||
|
effectiveShotCooldownMs,
|
||||||
|
effectivePowerByAxis,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
BalanceRouteReport(
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float firstBossKillSeconds,
|
||||||
|
int shopPurchases,
|
||||||
|
int abilityUses,
|
||||||
|
int selectedDamageCards,
|
||||||
|
int selectedFireRateCards,
|
||||||
|
int peakVisibleBonusBalls,
|
||||||
|
float effectiveDamage,
|
||||||
|
float effectiveShotCooldownMs,
|
||||||
|
Map<PowerAxis, Float> effectivePowerByAxis,
|
||||||
|
int moneyEarned,
|
||||||
|
int moneySpent,
|
||||||
|
int economyPaybackWave) {
|
||||||
|
this.strategyId = strategyId;
|
||||||
|
this.waveReached = waveReached;
|
||||||
|
this.firstBossKillSeconds = firstBossKillSeconds;
|
||||||
|
this.shopPurchases = shopPurchases;
|
||||||
|
this.abilityUses = abilityUses;
|
||||||
|
this.selectedDamageCards = selectedDamageCards;
|
||||||
|
this.selectedFireRateCards = selectedFireRateCards;
|
||||||
|
this.peakVisibleBonusBalls = peakVisibleBonusBalls;
|
||||||
|
this.effectiveDamage = effectiveDamage;
|
||||||
|
this.effectiveShotCooldownMs = effectiveShotCooldownMs;
|
||||||
|
this.effectivePowerByAxis = completeAxes(effectivePowerByAxis);
|
||||||
|
this.moneyEarned = moneyEarned;
|
||||||
|
this.moneySpent = moneySpent;
|
||||||
|
this.economyPaybackWave = economyPaybackWave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String strategyId() {
|
||||||
|
return strategyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int waveReached() {
|
||||||
|
return waveReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float firstBossKillSeconds() {
|
||||||
|
return firstBossKillSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int shopPurchases() {
|
||||||
|
return shopPurchases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int abilityUses() {
|
||||||
|
return abilityUses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int selectedDamageCards() {
|
||||||
|
return selectedDamageCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int selectedFireRateCards() {
|
||||||
|
return selectedFireRateCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int peakVisibleBonusBalls() {
|
||||||
|
return peakVisibleBonusBalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float effectiveDamage() {
|
||||||
|
return effectiveDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float effectiveShotCooldownMs() {
|
||||||
|
return effectiveShotCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<PowerAxis, Float> effectivePowerByAxis() {
|
||||||
|
return new EnumMap<>(effectivePowerByAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int moneyEarned() {
|
||||||
|
return moneyEarned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int moneySpent() {
|
||||||
|
return moneySpent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int economyPaybackWave() {
|
||||||
|
return economyPaybackWave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float defensePower() {
|
||||||
|
return value(PowerAxis.HEALTH)
|
||||||
|
+ value(PowerAxis.REGEN)
|
||||||
|
+ value(PowerAxis.SHIELD)
|
||||||
|
+ value(PowerAxis.ARMOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float utilityPower() {
|
||||||
|
return value(PowerAxis.ECONOMY) + value(PowerAxis.UTILITY) + value(PowerAxis.ABILITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float value(PowerAxis axis) {
|
||||||
|
Float value = effectivePowerByAxis.get(axis);
|
||||||
|
return value == null ? 0f : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EnumMap<PowerAxis, Float> emptyAxes() {
|
||||||
|
EnumMap<PowerAxis, Float> axes = new EnumMap<>(PowerAxis.class);
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
axes.put(axis, 0f);
|
||||||
|
}
|
||||||
|
return axes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EnumMap<PowerAxis, Float> completeAxes(Map<PowerAxis, Float> source) {
|
||||||
|
EnumMap<PowerAxis, Float> axes = emptyAxes();
|
||||||
|
if (source != null) {
|
||||||
|
axes.putAll(source);
|
||||||
|
}
|
||||||
|
return axes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import ru.project.tower.entities.circles.BonusBallData;
|
||||||
|
import ru.project.tower.player.PlayerCombatState;
|
||||||
|
import ru.project.tower.upgrades.UpgradeKind;
|
||||||
|
|
||||||
|
public final class BalanceRouteSimulator {
|
||||||
|
private static final int MAX_AUDIT_WAVE = 120;
|
||||||
|
private static final int FIRST_BOSS_WAVE = 20;
|
||||||
|
|
||||||
|
public BalanceRouteReport runDamageFireRateNoShopNoAbility(long seed) {
|
||||||
|
RouteState state = new RouteState("damage-fire-rate:no-shop:no-ability", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applyExploitCardIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOverwhelmed(state, wave, false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runFreshNoMenuTalentShopCardAbilityRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("fresh:no-menu-talent:shop-card-ability", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applyFreshNoMenuTalentCardIfDue(wave);
|
||||||
|
state.applyFreshNoMenuTalentShopIfDue(wave);
|
||||||
|
state.applyFreshNoMenuTalentAbilityIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
|
||||||
|
if (isFreshNoMenuTalentOverwhelmed(state, wave)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runBalancedShopAndAbilityRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("balanced:shop:ability", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applyBalancedCardIfDue(wave);
|
||||||
|
state.applyBalancedShopIfDue(wave);
|
||||||
|
state.applyAbilityIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOverwhelmed(state, wave, true)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runSustainControlShopAndAbilityRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("sustain-control:shop:ability", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applySustainControlCardIfDue(wave);
|
||||||
|
state.applySustainControlShopIfDue(wave);
|
||||||
|
state.applyAbilityIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOverwhelmed(state, wave, true)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runMixedNoShopNoAbilityRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("mixed:no-shop:no-ability", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applyBalancedCardIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOverwhelmed(state, wave, false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runAttackSingleTargetShopRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("attack:single-target-shop", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applySingleTargetShopIfDue(wave);
|
||||||
|
state.applyBalancedCardIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
if (isOverwhelmed(state, wave, true)) break;
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runAttackSwarmShopRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("attack:swarm-shop", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applySwarmShopIfDue(wave);
|
||||||
|
state.applyBalancedCardIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
if (isOverwhelmed(state, wave, true)) break;
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runAttackBossShopRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("attack:boss-shop", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applyBossShopIfDue(wave);
|
||||||
|
state.applyBalancedCardIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
if (isOverwhelmed(state, wave, true)) break;
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runDefenseShopRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("defense:shop", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applyDefenseShopIfDue(wave);
|
||||||
|
state.applySustainControlCardIfDue(wave);
|
||||||
|
state.applyAbilityIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
if (isOverwhelmed(state, wave, true)) break;
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceRouteReport runUtilityEconomyShopRoute(long seed) {
|
||||||
|
RouteState state = new RouteState("utility:economy-shop", 0, 0);
|
||||||
|
for (int wave = 1; wave <= MAX_AUDIT_WAVE; wave++) {
|
||||||
|
state.waveReached = wave;
|
||||||
|
state.applyUtilityShopIfDue(wave);
|
||||||
|
state.applyBalancedCardIfDue(wave);
|
||||||
|
state.observeBonusDensity(wave);
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
state.firstBossKillSeconds =
|
||||||
|
bossKillSeconds(EndlessBalanceSpec.bossHealthForWave(wave), state, wave);
|
||||||
|
}
|
||||||
|
if (isOverwhelmed(state, wave, true)) break;
|
||||||
|
}
|
||||||
|
return state.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOverwhelmed(RouteState state, int wave, boolean balancedRoute) {
|
||||||
|
float routeSupport =
|
||||||
|
balancedRoute ? state.shopPurchases * 2.15f + state.abilityUses * 0.35f : 0f;
|
||||||
|
float requiredDps =
|
||||||
|
4.5f + wave * 0.85f + (float) Math.pow(wave, 1.22f) * 0.08f - routeSupport;
|
||||||
|
return wave > FIRST_BOSS_WAVE && state.effectiveDps(wave) < requiredDps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isFreshNoMenuTalentOverwhelmed(RouteState state, int wave) {
|
||||||
|
float routeSupport =
|
||||||
|
Math.min(state.shopPurchases, 5) * 0.75f + Math.min(state.abilityUses, 2) * 0.35f;
|
||||||
|
float requiredDps =
|
||||||
|
1.75f
|
||||||
|
+ wave * 0.42f
|
||||||
|
+ (float) Math.pow(wave, 1.18f) * 0.10f
|
||||||
|
+ Math.max(0, wave - 11) * 2.00f
|
||||||
|
- routeSupport;
|
||||||
|
return wave >= 3 && state.effectiveDps(wave) < requiredDps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float bossKillSeconds(float bossHealth, RouteState state, int wave) {
|
||||||
|
return bossHealth / Math.max(0.01f, state.effectiveDps(wave));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RouteState {
|
||||||
|
private final String strategyId;
|
||||||
|
private int waveReached;
|
||||||
|
private float firstBossKillSeconds;
|
||||||
|
private int shopPurchases;
|
||||||
|
private int abilityUses;
|
||||||
|
private int selectedDamageCards;
|
||||||
|
private int selectedFireRateCards;
|
||||||
|
private int selectedMixedCards;
|
||||||
|
private int peakVisibleBonusBalls;
|
||||||
|
private float baseDamage = 1f;
|
||||||
|
private float runDamageBonus;
|
||||||
|
private float shotCooldownMs = PlayerCombatState.DEFAULT_SHOT_COOLDOWN_MS;
|
||||||
|
private float runCooldownMultiplier = 1f;
|
||||||
|
private float critChance = PlayerCombatState.DEFAULT_BASE_CRIT_CHANCE;
|
||||||
|
private float critMultiplier = PlayerCombatState.DEFAULT_BASE_CRIT_MULTIPLIER;
|
||||||
|
private float sustainRating;
|
||||||
|
private float pierceRating;
|
||||||
|
private float aoeRating;
|
||||||
|
private float bossingRating;
|
||||||
|
private float armorRating;
|
||||||
|
private float shieldRating;
|
||||||
|
private float economyRating;
|
||||||
|
private float utilityRating;
|
||||||
|
private int moneyEarned;
|
||||||
|
private int moneySpent;
|
||||||
|
private int economyPaybackWave;
|
||||||
|
|
||||||
|
private RouteState(String strategyId, int shopPurchases, int abilityUses) {
|
||||||
|
this.strategyId = strategyId;
|
||||||
|
this.shopPurchases = shopPurchases;
|
||||||
|
this.abilityUses = abilityUses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyExploitCardIfDue(int wave) {
|
||||||
|
if (wave % 3 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedDamageCards <= selectedFireRateCards) {
|
||||||
|
applyDamageCard();
|
||||||
|
} else {
|
||||||
|
applyFireRateCard(0.9f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBalancedCardIfDue(int wave) {
|
||||||
|
if (wave % 3 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lane = selectedMixedCards % 4;
|
||||||
|
if (lane == 0) {
|
||||||
|
applyDamageCard();
|
||||||
|
} else if (lane == 1) {
|
||||||
|
applyFireRateCard(0.92f);
|
||||||
|
} else if (lane == 2) {
|
||||||
|
critChance += 0.035f;
|
||||||
|
} else {
|
||||||
|
sustainRating += 1.6f;
|
||||||
|
}
|
||||||
|
selectedMixedCards++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySustainControlCardIfDue(int wave) {
|
||||||
|
if (wave % 3 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lane = selectedMixedCards % 4;
|
||||||
|
if (lane == 0) {
|
||||||
|
sustainRating += 2.8f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
critChance += 0.04f;
|
||||||
|
} else if (lane == 2) {
|
||||||
|
applyFireRateCard(0.94f);
|
||||||
|
} else {
|
||||||
|
applyDamageCard();
|
||||||
|
}
|
||||||
|
selectedMixedCards++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFreshNoMenuTalentCardIfDue(int wave) {
|
||||||
|
if (wave < 2 || wave % 3 != 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lane = selectedMixedCards % 3;
|
||||||
|
if (lane == 0) {
|
||||||
|
applyDamageCard();
|
||||||
|
} else if (lane == 1) {
|
||||||
|
applyFireRateCard(0.94f);
|
||||||
|
} else {
|
||||||
|
sustainRating += 0.9f;
|
||||||
|
}
|
||||||
|
selectedMixedCards++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyDamageCard() {
|
||||||
|
if (selectedDamageCards >= UpgradeKind.DAMAGE.maxStacks()) {
|
||||||
|
applyFireRateCard(0.9f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedDamageCards++;
|
||||||
|
runDamageBonus +=
|
||||||
|
Math.max(
|
||||||
|
1f,
|
||||||
|
baseDamage
|
||||||
|
* EndlessBalanceSpec.upgradeCardDamagePercent(
|
||||||
|
selectedDamageCards - 1)
|
||||||
|
/ 100f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFireRateCard(float ignoredMultiplier) {
|
||||||
|
if (selectedFireRateCards >= UpgradeKind.FIRE_RATE.maxStacks()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float multiplier =
|
||||||
|
EndlessBalanceSpec.upgradeCardFireRateMultiplier(selectedFireRateCards);
|
||||||
|
selectedFireRateCards++;
|
||||||
|
runCooldownMultiplier *= multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBalancedShopIfDue(int wave) {
|
||||||
|
if (wave < 4 || wave % 2 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lane = shopPurchases % 5;
|
||||||
|
if (lane == 0) {
|
||||||
|
baseDamage += 1.4f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
shotCooldownMs = Math.max(260f, shotCooldownMs - 38f);
|
||||||
|
} else if (lane == 2) {
|
||||||
|
critChance += 0.04f;
|
||||||
|
} else if (lane == 3) {
|
||||||
|
critMultiplier += 0.09f;
|
||||||
|
} else {
|
||||||
|
sustainRating += 3.2f;
|
||||||
|
}
|
||||||
|
shopPurchases++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySustainControlShopIfDue(int wave) {
|
||||||
|
if (wave < 4 || wave % 2 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lane = shopPurchases % 5;
|
||||||
|
if (lane == 0) {
|
||||||
|
sustainRating += 4.2f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
shotCooldownMs = Math.max(300f, shotCooldownMs - 30f);
|
||||||
|
} else if (lane == 2) {
|
||||||
|
critChance += 0.035f;
|
||||||
|
} else if (lane == 3) {
|
||||||
|
baseDamage += 1.0f;
|
||||||
|
} else {
|
||||||
|
critMultiplier += 0.07f;
|
||||||
|
}
|
||||||
|
shopPurchases++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFreshNoMenuTalentShopIfDue(int wave) {
|
||||||
|
if (wave < 2 || wave % 2 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lane = shopPurchases % 4;
|
||||||
|
if (lane == 0) {
|
||||||
|
baseDamage += 0.35f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
shotCooldownMs = Math.max(420f, shotCooldownMs - 18f);
|
||||||
|
} else if (lane == 2) {
|
||||||
|
sustainRating += 0.8f;
|
||||||
|
} else {
|
||||||
|
critChance += 0.012f;
|
||||||
|
}
|
||||||
|
shopPurchases++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySingleTargetShopIfDue(int wave) {
|
||||||
|
if (wave < 4 || wave % 2 != 0) return;
|
||||||
|
int lane = shopPurchases % 5;
|
||||||
|
if (lane == 0) {
|
||||||
|
baseDamage += 1.8f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
critChance += 0.055f;
|
||||||
|
} else if (lane == 2) {
|
||||||
|
critMultiplier += 0.12f;
|
||||||
|
} else if (lane == 3) {
|
||||||
|
bossingRating += 1.0f;
|
||||||
|
} else {
|
||||||
|
shotCooldownMs = Math.max(280f, shotCooldownMs - 24f);
|
||||||
|
}
|
||||||
|
shopPurchases++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySwarmShopIfDue(int wave) {
|
||||||
|
if (wave < 4 || wave % 2 != 0) return;
|
||||||
|
int lane = shopPurchases % 5;
|
||||||
|
if (lane == 0) {
|
||||||
|
pierceRating += 3.4f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
aoeRating += 4.2f;
|
||||||
|
} else if (lane == 2) {
|
||||||
|
baseDamage += 0.8f;
|
||||||
|
} else if (lane == 3) {
|
||||||
|
shotCooldownMs = Math.max(310f, shotCooldownMs - 28f);
|
||||||
|
} else {
|
||||||
|
utilityRating += 0.8f;
|
||||||
|
}
|
||||||
|
shopPurchases++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBossShopIfDue(int wave) {
|
||||||
|
if (wave < 4 || wave % 2 != 0) return;
|
||||||
|
int lane = shopPurchases % 5;
|
||||||
|
if (lane == 0) {
|
||||||
|
bossingRating += 4.5f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
critChance += 0.045f;
|
||||||
|
} else if (lane == 2) {
|
||||||
|
critMultiplier += 0.10f;
|
||||||
|
} else if (lane == 3) {
|
||||||
|
baseDamage += 1.1f;
|
||||||
|
} else {
|
||||||
|
shotCooldownMs = Math.max(300f, shotCooldownMs - 18f);
|
||||||
|
}
|
||||||
|
shopPurchases++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyDefenseShopIfDue(int wave) {
|
||||||
|
if (wave < 4 || wave % 2 != 0) return;
|
||||||
|
int lane = shopPurchases % 5;
|
||||||
|
if (lane == 0) {
|
||||||
|
sustainRating += 5.0f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
armorRating += 4.0f;
|
||||||
|
} else if (lane == 2) {
|
||||||
|
shieldRating += 3.5f;
|
||||||
|
} else if (lane == 3) {
|
||||||
|
aoeRating += 0.8f;
|
||||||
|
} else {
|
||||||
|
baseDamage += 0.7f;
|
||||||
|
}
|
||||||
|
shopPurchases++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyUtilityShopIfDue(int wave) {
|
||||||
|
moneyEarned += 8 + wave / 2;
|
||||||
|
if (wave < 4 || wave % 2 != 0) return;
|
||||||
|
int cost = Math.max(3, 7 - Math.min(3, shopPurchases / 2));
|
||||||
|
moneySpent += cost;
|
||||||
|
if (economyPaybackWave == 0 && moneyEarned > moneySpent) {
|
||||||
|
economyPaybackWave = wave;
|
||||||
|
}
|
||||||
|
int lane = shopPurchases % 5;
|
||||||
|
if (lane == 0) {
|
||||||
|
economyRating += 3.8f;
|
||||||
|
} else if (lane == 1) {
|
||||||
|
utilityRating += 4.0f;
|
||||||
|
} else if (lane == 2) {
|
||||||
|
peakVisibleBonusBalls = Math.max(peakVisibleBonusBalls, 2);
|
||||||
|
} else if (lane == 3) {
|
||||||
|
shotCooldownMs = Math.max(330f, shotCooldownMs - 18f);
|
||||||
|
} else {
|
||||||
|
baseDamage += 0.6f;
|
||||||
|
}
|
||||||
|
shopPurchases++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAbilityIfDue(int wave) {
|
||||||
|
if (wave >= 12 && wave % 5 == 0) {
|
||||||
|
abilityUses++;
|
||||||
|
sustainRating += 0.55f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFreshNoMenuTalentAbilityIfDue(int wave) {
|
||||||
|
if (wave >= 6 && wave % 4 == 2) {
|
||||||
|
abilityUses++;
|
||||||
|
sustainRating += 0.45f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void observeBonusDensity(int wave) {
|
||||||
|
int visibleBonusBalls = expectedVisibleBonusBalls(wave);
|
||||||
|
peakVisibleBonusBalls = Math.max(peakVisibleBonusBalls, visibleBonusBalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float effectiveDps(int wave) {
|
||||||
|
int bonusStacks = expectedVisibleBonusBalls(wave);
|
||||||
|
float damageMultiplier =
|
||||||
|
EndlessBalanceSpec.activeBonusMultiplier(
|
||||||
|
BonusBallData.BonusType.DAMAGE, bonusStacks / 3);
|
||||||
|
float cooldownMultiplier =
|
||||||
|
EndlessBalanceSpec.activeBonusMultiplier(
|
||||||
|
BonusBallData.BonusType.FIRE_RATE, bonusStacks / 3);
|
||||||
|
float averageCritMultiplier =
|
||||||
|
1f
|
||||||
|
+ Math.min(EndlessBalanceSpec.CRIT_CHANCE_CAP, critChance)
|
||||||
|
* (critMultiplier - 1f);
|
||||||
|
float sustainMultiplier = 1f + Math.min(0.45f, sustainRating * 0.018f);
|
||||||
|
float shapeMultiplier =
|
||||||
|
1f
|
||||||
|
+ Math.min(0.25f, pierceRating * 0.010f + aoeRating * 0.008f)
|
||||||
|
+ Math.min(0.18f, bossingRating * 0.010f);
|
||||||
|
float damage = (baseDamage + runDamageBonus) * damageMultiplier;
|
||||||
|
float cooldown =
|
||||||
|
Math.max(90f, shotCooldownMs * runCooldownMultiplier * cooldownMultiplier);
|
||||||
|
return damage
|
||||||
|
* averageCritMultiplier
|
||||||
|
* sustainMultiplier
|
||||||
|
* shapeMultiplier
|
||||||
|
* 1000f
|
||||||
|
/ cooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int expectedVisibleBonusBalls(int wave) {
|
||||||
|
float chance =
|
||||||
|
Math.min(
|
||||||
|
0.2f,
|
||||||
|
EndlessBalanceSpec.bonusDropBudgetThroughWave(wave)
|
||||||
|
/ (float) Math.max(8, wave + 20));
|
||||||
|
int expectedRolls =
|
||||||
|
Math.round(EndlessBalanceSpec.waveTuning(wave).enemiesPerWave() * chance);
|
||||||
|
int budgeted = Math.min(EndlessBalanceSpec.bonusDropBudgetForWave(wave), expectedRolls);
|
||||||
|
return Math.min(EndlessBalanceSpec.maxVisibleBonusBalls(), budgeted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BalanceRouteReport report() {
|
||||||
|
float finalDamage = baseDamage + runDamageBonus;
|
||||||
|
float finalCooldown = Math.max(90f, shotCooldownMs * runCooldownMultiplier);
|
||||||
|
EnumMap<PowerAxis, Float> axes = new EnumMap<>(PowerAxis.class);
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
axes.put(axis, 0f);
|
||||||
|
}
|
||||||
|
axes.put(PowerAxis.DAMAGE, finalDamage);
|
||||||
|
axes.put(PowerAxis.CADENCE, 1000f / Math.max(1f, finalCooldown));
|
||||||
|
axes.put(PowerAxis.CRIT, Math.max(0f, critChance * 100f + (critMultiplier - 1f) * 25f));
|
||||||
|
axes.put(PowerAxis.PIERCE, selectedMixedCards * 0.75f + pierceRating);
|
||||||
|
axes.put(
|
||||||
|
PowerAxis.AOE,
|
||||||
|
selectedMixedCards * 0.60f + peakVisibleBonusBalls * 0.35f + aoeRating);
|
||||||
|
axes.put(PowerAxis.CONTROL, abilityUses * 1.4f + sustainRating * 0.25f);
|
||||||
|
axes.put(
|
||||||
|
PowerAxis.BOSSING,
|
||||||
|
finalDamage * 0.70f + axes.get(PowerAxis.CRIT) * 0.20f + bossingRating);
|
||||||
|
axes.put(PowerAxis.HEALTH, sustainRating);
|
||||||
|
axes.put(PowerAxis.REGEN, sustainRating * 0.70f);
|
||||||
|
axes.put(PowerAxis.SHIELD, abilityUses * 1.8f + shopPurchases * 0.15f + shieldRating);
|
||||||
|
axes.put(PowerAxis.ARMOR, sustainRating * 0.45f + armorRating);
|
||||||
|
axes.put(PowerAxis.ECONOMY, shopPurchases * 0.70f + economyRating);
|
||||||
|
axes.put(
|
||||||
|
PowerAxis.UTILITY,
|
||||||
|
peakVisibleBonusBalls * 1.2f + selectedMixedCards * 0.20f + utilityRating);
|
||||||
|
axes.put(PowerAxis.ABILITY, abilityUses * 2.1f);
|
||||||
|
return new BalanceRouteReport(
|
||||||
|
strategyId,
|
||||||
|
waveReached,
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopPurchases,
|
||||||
|
abilityUses,
|
||||||
|
selectedDamageCards,
|
||||||
|
selectedFireRateCards,
|
||||||
|
peakVisibleBonusBalls,
|
||||||
|
finalDamage,
|
||||||
|
finalCooldown,
|
||||||
|
axes,
|
||||||
|
moneyEarned,
|
||||||
|
moneySpent,
|
||||||
|
economyPaybackWave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
public final class BalanceRouteTarget {
|
||||||
|
private final String strategyId;
|
||||||
|
private final int minWaveReached;
|
||||||
|
private final int maxWaveReached;
|
||||||
|
private final float minFirstBossKillSeconds;
|
||||||
|
private final float maxFirstBossKillSeconds;
|
||||||
|
private final boolean usesPermanentMenuTalents;
|
||||||
|
private final boolean usesInRunShop;
|
||||||
|
private final boolean usesRunCards;
|
||||||
|
private final boolean usesAbilities;
|
||||||
|
private final String designGoal;
|
||||||
|
|
||||||
|
public BalanceRouteTarget(
|
||||||
|
String strategyId,
|
||||||
|
int minWaveReached,
|
||||||
|
int maxWaveReached,
|
||||||
|
float minFirstBossKillSeconds,
|
||||||
|
float maxFirstBossKillSeconds,
|
||||||
|
boolean usesPermanentMenuTalents,
|
||||||
|
boolean usesInRunShop,
|
||||||
|
boolean usesRunCards,
|
||||||
|
boolean usesAbilities,
|
||||||
|
String designGoal) {
|
||||||
|
this.strategyId = strategyId;
|
||||||
|
this.minWaveReached = minWaveReached;
|
||||||
|
this.maxWaveReached = maxWaveReached;
|
||||||
|
this.minFirstBossKillSeconds = minFirstBossKillSeconds;
|
||||||
|
this.maxFirstBossKillSeconds = maxFirstBossKillSeconds;
|
||||||
|
this.usesPermanentMenuTalents = usesPermanentMenuTalents;
|
||||||
|
this.usesInRunShop = usesInRunShop;
|
||||||
|
this.usesRunCards = usesRunCards;
|
||||||
|
this.usesAbilities = usesAbilities;
|
||||||
|
this.designGoal = designGoal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String strategyId() {
|
||||||
|
return strategyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int minWaveReached() {
|
||||||
|
return minWaveReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int maxWaveReached() {
|
||||||
|
return maxWaveReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float minFirstBossKillSeconds() {
|
||||||
|
return minFirstBossKillSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float maxFirstBossKillSeconds() {
|
||||||
|
return maxFirstBossKillSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean usesPermanentMenuTalents() {
|
||||||
|
return usesPermanentMenuTalents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean usesInRunShop() {
|
||||||
|
return usesInRunShop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean usesRunCards() {
|
||||||
|
return usesRunCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean usesAbilities() {
|
||||||
|
return usesAbilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String designGoal() {
|
||||||
|
return designGoal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsWaveReached(int waveReached) {
|
||||||
|
return waveReached >= minWaveReached && waveReached <= maxWaveReached;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,909 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Preferences;
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Rectangle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import com.badlogic.gdx.utils.Pools;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import ru.project.tower.abilities.AbilityHost;
|
||||||
|
import ru.project.tower.abilities.AbilityKind;
|
||||||
|
import ru.project.tower.abilities.AbilitySystem;
|
||||||
|
import ru.project.tower.abilities.PlayerAbilities;
|
||||||
|
import ru.project.tower.abilities.Turret;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.entities.circles.BasicCircleData;
|
||||||
|
import ru.project.tower.entities.circles.BonusBallData;
|
||||||
|
import ru.project.tower.player.EffectiveCombatStats;
|
||||||
|
import ru.project.tower.player.PlayerCombatState;
|
||||||
|
import ru.project.tower.player.PlayerRunCombatCalculator;
|
||||||
|
import ru.project.tower.player.PlayerStats;
|
||||||
|
import ru.project.tower.support.SeededRandomSource;
|
||||||
|
import ru.project.tower.systems.BonusBallSystem;
|
||||||
|
import ru.project.tower.talents.TalentData;
|
||||||
|
import ru.project.tower.talents.TalentTree;
|
||||||
|
import ru.project.tower.upgrades.UpgradeHost;
|
||||||
|
import ru.project.tower.upgrades.UpgradeKind;
|
||||||
|
import ru.project.tower.upgrades.UpgradeSystem;
|
||||||
|
import ru.project.tower.upgrades.shop.ShopUpgradeHost;
|
||||||
|
import ru.project.tower.upgrades.shop.ShopUpgradeKind;
|
||||||
|
import ru.project.tower.upgrades.shop.ShopUpgradeSystem;
|
||||||
|
import ru.project.tower.waves.WaveTuning;
|
||||||
|
|
||||||
|
public final class BalanceRuntimeHarness {
|
||||||
|
private static final int MAX_RUNTIME_WAVE = 140;
|
||||||
|
private static final int FIRST_BOSS_WAVE = EndlessBalanceSpec.BOSS_WAVE_INTERVAL;
|
||||||
|
private static final Rectangle PLAYER_SQUARE = new Rectangle(100f, 100f, 50f, 50f);
|
||||||
|
private static final BonusBallData.BonusType[] RUNTIME_BONUS_SEQUENCE = {
|
||||||
|
BonusBallData.BonusType.DAMAGE,
|
||||||
|
BonusBallData.BonusType.FIRE_RATE,
|
||||||
|
BonusBallData.BonusType.BULLET_SPEED,
|
||||||
|
BonusBallData.BonusType.CRIT_WINDOW,
|
||||||
|
BonusBallData.BonusType.PICKUP_MAGNET,
|
||||||
|
BonusBallData.BonusType.HEAL,
|
||||||
|
BonusBallData.BonusType.SHIELD_PULSE,
|
||||||
|
BonusBallData.BonusType.ABILITY_CHARGE,
|
||||||
|
BonusBallData.BonusType.COIN_BURST
|
||||||
|
};
|
||||||
|
|
||||||
|
public List<SimulationReport> run(long seed) {
|
||||||
|
return List.of(
|
||||||
|
simulate(seed, RuntimeRoute.fresh()),
|
||||||
|
simulate(seed, RuntimeRoute.exploit()),
|
||||||
|
simulate(seed, RuntimeRoute.balanced()),
|
||||||
|
simulate(seed, RuntimeRoute.moderateMeta()),
|
||||||
|
simulate(seed, RuntimeRoute.highMeta()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimulationReport simulate(long seed, RuntimeRoute route) {
|
||||||
|
RuntimeState state = new RuntimeState(seed, route);
|
||||||
|
float firstBossKillSeconds = 0f;
|
||||||
|
int waveReached = 0;
|
||||||
|
|
||||||
|
for (int wave = 1; wave <= MAX_RUNTIME_WAVE; wave++) {
|
||||||
|
waveReached = wave;
|
||||||
|
state.prepareWave(wave);
|
||||||
|
state.applyRouteChoices(wave);
|
||||||
|
state.spawnBonusIfBudgetAllows(wave);
|
||||||
|
state.castAbilityIfDue(wave);
|
||||||
|
|
||||||
|
if (wave == FIRST_BOSS_WAVE) {
|
||||||
|
firstBossKillSeconds = state.bossKillSeconds(wave);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean overwhelmed = state.isOverwhelmed(wave);
|
||||||
|
state.finishWave();
|
||||||
|
if (overwhelmed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.report(waveReached, firstBossKillSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RuntimeState implements UpgradeHost, ShopUpgradeHost, AbilityHost {
|
||||||
|
private final RuntimeRoute route;
|
||||||
|
private final long seed;
|
||||||
|
private final SeededRandomSource random;
|
||||||
|
private final PlayerCombatState combatState = new PlayerCombatState();
|
||||||
|
private final Array<BaseCircleData> circles = new Array<>();
|
||||||
|
private final Array<BonusBallData> runtimeBonusBalls = new Array<>();
|
||||||
|
private final PlayerAbilities playerAbilities;
|
||||||
|
private final Array<AbilityKind> selectedAbilities = new Array<>();
|
||||||
|
private final UpgradeSystem upgradeSystem;
|
||||||
|
private final ShopUpgradeSystem shopUpgradeSystem;
|
||||||
|
private final BonusBallSystem bonusBallSystem = new BonusBallSystem(runtimeBonusBalls);
|
||||||
|
private final AbilitySystem abilitySystem;
|
||||||
|
private final TalentTree talentTree;
|
||||||
|
private final PlayerStats playerStats;
|
||||||
|
private final EnumMap<PowerAxis, Float> axes = new EnumMap<>(PowerAxis.class);
|
||||||
|
|
||||||
|
private float baseDamage = 1f;
|
||||||
|
private float runDamageBonus;
|
||||||
|
private int runPierceBonus;
|
||||||
|
private float runAoeBonus;
|
||||||
|
private int currentWave = 1;
|
||||||
|
private int selectedDamageCards;
|
||||||
|
private int selectedFireRateCards;
|
||||||
|
private int abilityUses;
|
||||||
|
private int bonusDrops;
|
||||||
|
private int bonusSpawned;
|
||||||
|
private int bonusCollected;
|
||||||
|
private int peakVisibleBonusBalls;
|
||||||
|
private int activeBonusUptimeWaves;
|
||||||
|
private int moneyEarned;
|
||||||
|
private int moneySpent;
|
||||||
|
private float damageTaken;
|
||||||
|
|
||||||
|
private RuntimeState(long seed, RuntimeRoute route) {
|
||||||
|
this.route = route;
|
||||||
|
this.seed = seed;
|
||||||
|
this.random =
|
||||||
|
new SeededRandomSource(SeededRandomSource.forkSeed(seed, route.strategyId));
|
||||||
|
this.playerAbilities = new PlayerAbilities(new RuntimeAbilityPreferences());
|
||||||
|
for (AbilityKind ability : AbilityKind.values()) {
|
||||||
|
selectedAbilities.add(ability);
|
||||||
|
}
|
||||||
|
this.upgradeSystem = new UpgradeSystem(random, this);
|
||||||
|
this.shopUpgradeSystem = new ShopUpgradeSystem(this);
|
||||||
|
this.playerStats = new PlayerStats(new RuntimeAbilityPreferences());
|
||||||
|
route.applyMeta(playerStats);
|
||||||
|
this.talentTree = new TalentTree(playerStats);
|
||||||
|
this.abilitySystem =
|
||||||
|
new AbilitySystem(
|
||||||
|
PLAYER_SQUARE,
|
||||||
|
circles,
|
||||||
|
playerAbilities,
|
||||||
|
selectedAbilities,
|
||||||
|
random,
|
||||||
|
this);
|
||||||
|
applyPermanentTalentStats();
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
axes.put(axis, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPermanentTalentStats() {
|
||||||
|
baseDamage += talentTree.getStatBonus(TalentData.TalentType.DAMAGE);
|
||||||
|
int maxHealth =
|
||||||
|
PlayerCombatState.DEFAULT_MAX_HEALTH
|
||||||
|
+ (int) talentTree.getStatBonus(TalentData.TalentType.HEALTH);
|
||||||
|
float shotCooldown =
|
||||||
|
Math.max(
|
||||||
|
PlayerCombatState.MIN_SHOT_COOLDOWN_MS,
|
||||||
|
PlayerCombatState.DEFAULT_SHOT_COOLDOWN_MS
|
||||||
|
- talentTree.getStatBonus(TalentData.TalentType.COOLDOWN));
|
||||||
|
int regen = (int) talentTree.getStatBonus(TalentData.TalentType.REGEN);
|
||||||
|
combatState.resetForNewRun(maxHealth, shotCooldown, regen);
|
||||||
|
combatState.setMoney(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareWave(int wave) {
|
||||||
|
currentWave = wave;
|
||||||
|
int income = Math.max(1, 3 + wave / 2);
|
||||||
|
combatState.addMoney(income);
|
||||||
|
moneyEarned += income;
|
||||||
|
circles.clear();
|
||||||
|
WaveTuning tuning = EndlessBalanceSpec.waveTuning(wave);
|
||||||
|
int health = tuning.basicCircleHealth();
|
||||||
|
circles.add(
|
||||||
|
new BasicCircleData(
|
||||||
|
new Circle(125f, 125f, 10f),
|
||||||
|
new Vector2(),
|
||||||
|
health,
|
||||||
|
tuning.circleDamage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyRouteChoices(int wave) {
|
||||||
|
if (route.cardsEnabled && wave % 3 == 0) {
|
||||||
|
applyCard();
|
||||||
|
}
|
||||||
|
if (route.shopEnabled && wave >= route.firstShopWave && wave % 2 == 0) {
|
||||||
|
buyShopUpgrade();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCard() {
|
||||||
|
upgradeSystem.triggerPick();
|
||||||
|
UpgradeKind[] options = upgradeSystem.getPickOptions();
|
||||||
|
if (options == null) return;
|
||||||
|
|
||||||
|
int choice;
|
||||||
|
if (route.rawDamageFireRateOnly) {
|
||||||
|
UpgradeKind kind =
|
||||||
|
selectedDamageCards <= selectedFireRateCards
|
||||||
|
? UpgradeKind.DAMAGE
|
||||||
|
: UpgradeKind.FIRE_RATE;
|
||||||
|
choice = indexOf(options, kind);
|
||||||
|
} else {
|
||||||
|
choice = chooseFreshStarterCardIfNeeded();
|
||||||
|
if (choice < 0) {
|
||||||
|
improveMixedOfferWithRerolls();
|
||||||
|
choice = chooseMixedCardOption();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (choice < 0) choice = 0;
|
||||||
|
|
||||||
|
UpgradeKind selected = options[choice];
|
||||||
|
upgradeSystem.applyPick(choice);
|
||||||
|
if (isDamageLike(selected)) selectedDamageCards++;
|
||||||
|
if (isCadenceLike(selected)) selectedFireRateCards++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDamageLike(UpgradeKind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case DAMAGE:
|
||||||
|
case BOSS_BREAKER:
|
||||||
|
case EXECUTION_LINE:
|
||||||
|
case BLAST_PROTOCOL:
|
||||||
|
case VOLATILE_BARGAIN:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCadenceLike(UpgradeKind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case FIRE_RATE:
|
||||||
|
case BONUS_MAGNET:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int indexOf(UpgradeKind[] options, UpgradeKind kind) {
|
||||||
|
for (int i = 0; i < options.length; i++) {
|
||||||
|
if (options[i] == kind) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int chooseFreshStarterCardIfNeeded() {
|
||||||
|
if (!route.strategyId.contains("fresh")
|
||||||
|
|| selectedDamageCards + selectedFireRateCards > 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int index = indexOfDamageOrCadenceLike(upgradeSystem.getPickOptions());
|
||||||
|
while (index < 0 && upgradeSystem.rerollsRemaining() > 0) {
|
||||||
|
if (!upgradeSystem.rerollPick(shopUpgradeSystem.cardRerollDiscount())) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
index = indexOfDamageOrCadenceLike(upgradeSystem.getPickOptions());
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int indexOfDamageOrCadenceLike(UpgradeKind[] options) {
|
||||||
|
if (options == null) return -1;
|
||||||
|
for (int i = 0; i < options.length; i++) {
|
||||||
|
if (isDamageLike(options[i]) || isCadenceLike(options[i])) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int chooseMixedCardOption() {
|
||||||
|
UpgradeSystem.OfferCardView[] views = upgradeSystem.activeOfferViews();
|
||||||
|
int bestIndex = 0;
|
||||||
|
int bestScore = Integer.MIN_VALUE;
|
||||||
|
for (int i = 0; i < views.length; i++) {
|
||||||
|
int score = mixedCardScore(views[i]);
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void improveMixedOfferWithRerolls() {
|
||||||
|
while (bestMixedCardScore() < 50 && upgradeSystem.rerollsRemaining() > 0) {
|
||||||
|
if (!upgradeSystem.rerollPick(shopUpgradeSystem.cardRerollDiscount())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int bestMixedCardScore() {
|
||||||
|
UpgradeSystem.OfferCardView[] views = upgradeSystem.activeOfferViews();
|
||||||
|
int bestScore = Integer.MIN_VALUE;
|
||||||
|
for (UpgradeSystem.OfferCardView view : views) {
|
||||||
|
bestScore = Math.max(bestScore, mixedCardScore(view));
|
||||||
|
}
|
||||||
|
return bestScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int mixedCardScore(UpgradeSystem.OfferCardView view) {
|
||||||
|
if (view.synergyReady()) return 100;
|
||||||
|
switch (view.kind()) {
|
||||||
|
case DAMAGE:
|
||||||
|
case CRIT:
|
||||||
|
return view.stacks() < 2 ? 90 - view.stacks() : 40;
|
||||||
|
case FIRE_RATE:
|
||||||
|
return view.stacks() < 2 ? 80 - view.stacks() : 35;
|
||||||
|
case PIERCE:
|
||||||
|
return view.stacks() < 2 ? 75 - view.stacks() : 50;
|
||||||
|
case AOE:
|
||||||
|
return view.stacks() < 2 ? 70 - view.stacks() : 45;
|
||||||
|
case HEAL:
|
||||||
|
case REGEN:
|
||||||
|
return view.stacks() < 2 ? 60 - view.stacks() : 30;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buyShopUpgrade() {
|
||||||
|
ShopUpgradeKind kind;
|
||||||
|
int lane =
|
||||||
|
shopUpgradeSystem.level(ShopUpgradeKind.DAMAGE)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.COOLDOWN)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.REGEN)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.CRIT_CHANCE);
|
||||||
|
if (route.rawDamageFireRateOnly) {
|
||||||
|
kind = lane % 2 == 0 ? ShopUpgradeKind.DAMAGE : ShopUpgradeKind.COOLDOWN;
|
||||||
|
} else {
|
||||||
|
int family = lane % 4;
|
||||||
|
if (family == 0) {
|
||||||
|
kind = ShopUpgradeKind.DAMAGE;
|
||||||
|
} else if (family == 1) {
|
||||||
|
kind = ShopUpgradeKind.COOLDOWN;
|
||||||
|
} else if (family == 2) {
|
||||||
|
kind = ShopUpgradeKind.REGEN;
|
||||||
|
} else {
|
||||||
|
kind = ShopUpgradeKind.CRIT_CHANCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shopUpgradeSystem.tryBuy(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnBonusIfBudgetAllows(int wave) {
|
||||||
|
int budget = EndlessBalanceSpec.bonusDropBudgetForWave(wave);
|
||||||
|
if (budget <= 0) return;
|
||||||
|
|
||||||
|
for (int i = 0;
|
||||||
|
i < budget
|
||||||
|
&& runtimeBonusBalls.size < EndlessBalanceSpec.maxVisibleBonusBalls();
|
||||||
|
i++) {
|
||||||
|
BonusBallData ball = Pools.obtain(BonusBallData.class);
|
||||||
|
BonusBallData.BonusType type =
|
||||||
|
RUNTIME_BONUS_SEQUENCE[bonusSpawned % RUNTIME_BONUS_SEQUENCE.length];
|
||||||
|
ball.configure(125f, 125f, 10f, 0f, 0f, type);
|
||||||
|
runtimeBonusBalls.add(ball);
|
||||||
|
bonusSpawned++;
|
||||||
|
bonusDrops = bonusSpawned;
|
||||||
|
peakVisibleBonusBalls = Math.max(peakVisibleBonusBalls, runtimeBonusBalls.size);
|
||||||
|
|
||||||
|
if (shouldCollectSpawnedBonus()) {
|
||||||
|
bonusBallSystem.collect(runtimeBonusBalls.size - 1);
|
||||||
|
bonusCollected++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldCollectSpawnedBonus() {
|
||||||
|
boolean pickupPity =
|
||||||
|
route.bonusPickupSkill >= 0.15f
|
||||||
|
&& runtimeBonusBalls.size >= EndlessBalanceSpec.maxVisibleBonusBalls();
|
||||||
|
return random.nextFloat() < route.bonusPickupSkill || pickupPity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishWave() {
|
||||||
|
if (bonusBallSystem.getActiveBonusCount() > 0) {
|
||||||
|
activeBonusUptimeWaves++;
|
||||||
|
}
|
||||||
|
bonusBallSystem.endWave();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void castAbilityIfDue(int wave) {
|
||||||
|
if (!route.abilitiesEnabled
|
||||||
|
|| wave < route.firstAbilityWave
|
||||||
|
|| wave % route.abilityEveryWaves != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long now = 200_000L + wave * 30_000L;
|
||||||
|
abilitySystem.activateExplosion(0f, 0f, now);
|
||||||
|
abilityUses++;
|
||||||
|
abilitySystem.tick(now + 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOverwhelmed(int wave) {
|
||||||
|
WaveTuning tuning = EndlessBalanceSpec.waveTuning(wave);
|
||||||
|
float pressure =
|
||||||
|
tuning.enemiesPerWave() * tuning.basicCircleHealth() * 0.034f
|
||||||
|
+ tuning.circleDamage() * 1.65f
|
||||||
|
+ Math.max(0, wave - 11) * route.postEarlyPressure;
|
||||||
|
float dps = effectiveDps();
|
||||||
|
float sustain =
|
||||||
|
combatState.getMaxHealth() * 0.020f
|
||||||
|
+ combatState.getHealthRegenRate() * 0.75f
|
||||||
|
+ runAoeBonus * 0.025f
|
||||||
|
+ runPierceBonus * 0.30f
|
||||||
|
+ abilityUses * route.abilityValue;
|
||||||
|
damageTaken += Math.max(0f, pressure - dps - sustain) * 0.18f;
|
||||||
|
return wave >= route.minimumFailCheckWave && dps + sustain < pressure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float effectiveDps() {
|
||||||
|
EffectiveCombatStats stats = currentStats();
|
||||||
|
float crit = 1f + stats.critChance() * (stats.critMultiplier() - 1f);
|
||||||
|
return stats.damage() * crit * 1000f / Math.max(90f, stats.shotCooldownMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private float bossKillSeconds(int wave) {
|
||||||
|
return EndlessBalanceSpec.bossHealthForWave(wave) / Math.max(0.01f, effectiveDps());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimulationReport report(int waveReached, float firstBossKillSeconds) {
|
||||||
|
EffectiveCombatStats stats = currentStats();
|
||||||
|
axes.put(PowerAxis.DAMAGE, stats.damage());
|
||||||
|
axes.put(PowerAxis.CADENCE, 1000f / Math.max(1f, stats.shotCooldownMs()));
|
||||||
|
axes.put(PowerAxis.HEALTH, (float) stats.healthRegenRate() * 2f);
|
||||||
|
axes.put(PowerAxis.AOE, stats.aoeRadiusBonus());
|
||||||
|
axes.put(PowerAxis.CONTROL, stats.controlRating());
|
||||||
|
axes.put(PowerAxis.REGEN, (float) stats.healthRegenRate());
|
||||||
|
axes.put(PowerAxis.CRIT, bonusBallSystem.getCritChanceBonus());
|
||||||
|
axes.put(PowerAxis.UTILITY, bonusBallSystem.getPickupRadiusMultiplier() - 1f);
|
||||||
|
axes.put(PowerAxis.ECONOMY, (float) moneyEarned / Math.max(1f, waveReached));
|
||||||
|
axes.put(PowerAxis.ABILITY, Math.max(0f, abilityUses * 1.5f));
|
||||||
|
for (Map.Entry<PowerAxis, Integer> rating :
|
||||||
|
talentTree.getPermanentAxisRatings().entrySet()) {
|
||||||
|
axes.put(rating.getKey(), axes.get(rating.getKey()) + rating.getValue());
|
||||||
|
}
|
||||||
|
return new SimulationReport(
|
||||||
|
seed,
|
||||||
|
route.strategyId,
|
||||||
|
waveReached,
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopUpgradeSystem.level(ShopUpgradeKind.DAMAGE)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.COOLDOWN)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.REGEN)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.CRIT_CHANCE),
|
||||||
|
0f,
|
||||||
|
waveReached >= 100 ? 0 : 1,
|
||||||
|
damageTaken,
|
||||||
|
bonusDrops,
|
||||||
|
activeBonusPower(),
|
||||||
|
Math.max(0.01f, waveReached / 100f),
|
||||||
|
axes,
|
||||||
|
"runtime-harness",
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopUpgradeSystem.level(ShopUpgradeKind.DAMAGE)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.COOLDOWN)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.REGEN)
|
||||||
|
+ shopUpgradeSystem.level(ShopUpgradeKind.CRIT_CHANCE),
|
||||||
|
abilityUses,
|
||||||
|
selectedDamageCards,
|
||||||
|
selectedFireRateCards,
|
||||||
|
peakVisibleBonusBalls,
|
||||||
|
stats.damage(),
|
||||||
|
stats.shotCooldownMs(),
|
||||||
|
bonusSpawned,
|
||||||
|
bonusCollected,
|
||||||
|
activeBonusUptimeWaves,
|
||||||
|
upgradeSystem.triggeredSynergyCount(),
|
||||||
|
moneyEarned,
|
||||||
|
moneySpent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float activeBonusPower() {
|
||||||
|
return Math.max(
|
||||||
|
Math.max(
|
||||||
|
bonusBallSystem.getDamageMultiplier(),
|
||||||
|
1f / Math.max(0.01f, bonusBallSystem.getCooldownMultiplier())),
|
||||||
|
1f
|
||||||
|
+ bonusBallSystem.getCritChanceBonus()
|
||||||
|
+ bonusBallSystem.getPickupRadiusMultiplier()
|
||||||
|
- 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EffectiveCombatStats currentStats() {
|
||||||
|
return EffectiveCombatStats.fromSources(
|
||||||
|
combatState,
|
||||||
|
shopUpgradeSystem,
|
||||||
|
upgradeSystem,
|
||||||
|
bonusBallSystem,
|
||||||
|
talentTree,
|
||||||
|
currentWave,
|
||||||
|
baseDamage,
|
||||||
|
runDamageBonus,
|
||||||
|
runPierceBonus,
|
||||||
|
runAoeBonus,
|
||||||
|
runPierceBonus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRunDamagePercentBonus(int percent) {
|
||||||
|
runDamageBonus += Math.max(1f, baseDamage * percent / 100f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRunPierceBonus(int bonus) {
|
||||||
|
runPierceBonus += bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRunAoeBonus(float radiusBonus) {
|
||||||
|
runAoeBonus += radiusBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMaxHealth(int amount) {
|
||||||
|
combatState.setMaxHealth(combatState.getMaxHealth() + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void healToFull() {
|
||||||
|
combatState.setHealth(combatState.getMaxHealth());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addHealthRegen(int perSecond) {
|
||||||
|
combatState.setHealthRegenRate(combatState.getHealthRegenRate() + perSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addFloatingText(float x, float y, String text, Color color) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getPlayerCenterX() {
|
||||||
|
return PLAYER_SQUARE.x + PLAYER_SQUARE.width / 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getPlayerCenterY() {
|
||||||
|
return PLAYER_SQUARE.y + PLAYER_SQUARE.height / 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean spendMoney(int amount) {
|
||||||
|
boolean spent = combatState.spendMoney(amount);
|
||||||
|
if (spent) {
|
||||||
|
moneySpent += amount;
|
||||||
|
}
|
||||||
|
return spent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addBaseDamage(int amount) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addBaseSpeed(float amount) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reduceShotCooldown(float amount, float minimum) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createExplosion(float x, float y, Color color) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void triggerKillShake() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void triggerCritHitstop() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int effectiveBulletDamage() {
|
||||||
|
return Math.round(currentStats().damage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float abilityScalingFactor() {
|
||||||
|
return PlayerRunCombatCalculator.abilityScalingFactor(currentWave);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float gameScale() {
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long abilityCooldownReductionMs() {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBoss(BaseCircleData circle) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMoney(int amount) {
|
||||||
|
combatState.addMoney(amount);
|
||||||
|
moneyEarned += Math.max(0, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void killEnemy(BaseCircleData enemy) {
|
||||||
|
addMoney(enemy.getReward());
|
||||||
|
circles.removeValue(enemy, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTurretShot(Turret turret, BaseCircleData target, long now) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RuntimeRoute {
|
||||||
|
private final String strategyId;
|
||||||
|
private final boolean shopEnabled;
|
||||||
|
private final boolean cardsEnabled;
|
||||||
|
private final boolean abilitiesEnabled;
|
||||||
|
private final boolean rawDamageFireRateOnly;
|
||||||
|
private final int firstShopWave;
|
||||||
|
private final int firstAbilityWave;
|
||||||
|
private final int abilityEveryWaves;
|
||||||
|
private final int minimumFailCheckWave;
|
||||||
|
private final float bonusPickupSkill;
|
||||||
|
private final float abilityValue;
|
||||||
|
private final float postEarlyPressure;
|
||||||
|
private final int metaProfile;
|
||||||
|
|
||||||
|
private RuntimeRoute(
|
||||||
|
String strategyId,
|
||||||
|
boolean shopEnabled,
|
||||||
|
boolean cardsEnabled,
|
||||||
|
boolean abilitiesEnabled,
|
||||||
|
boolean rawDamageFireRateOnly,
|
||||||
|
int firstShopWave,
|
||||||
|
int firstAbilityWave,
|
||||||
|
int abilityEveryWaves,
|
||||||
|
int minimumFailCheckWave,
|
||||||
|
float bonusPickupSkill,
|
||||||
|
float abilityValue,
|
||||||
|
float postEarlyPressure) {
|
||||||
|
this(
|
||||||
|
strategyId,
|
||||||
|
shopEnabled,
|
||||||
|
cardsEnabled,
|
||||||
|
abilitiesEnabled,
|
||||||
|
rawDamageFireRateOnly,
|
||||||
|
firstShopWave,
|
||||||
|
firstAbilityWave,
|
||||||
|
abilityEveryWaves,
|
||||||
|
minimumFailCheckWave,
|
||||||
|
bonusPickupSkill,
|
||||||
|
abilityValue,
|
||||||
|
postEarlyPressure,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeRoute(
|
||||||
|
String strategyId,
|
||||||
|
boolean shopEnabled,
|
||||||
|
boolean cardsEnabled,
|
||||||
|
boolean abilitiesEnabled,
|
||||||
|
boolean rawDamageFireRateOnly,
|
||||||
|
int firstShopWave,
|
||||||
|
int firstAbilityWave,
|
||||||
|
int abilityEveryWaves,
|
||||||
|
int minimumFailCheckWave,
|
||||||
|
float bonusPickupSkill,
|
||||||
|
float abilityValue,
|
||||||
|
float postEarlyPressure,
|
||||||
|
int metaProfile) {
|
||||||
|
this.strategyId = strategyId;
|
||||||
|
this.shopEnabled = shopEnabled;
|
||||||
|
this.cardsEnabled = cardsEnabled;
|
||||||
|
this.abilitiesEnabled = abilitiesEnabled;
|
||||||
|
this.rawDamageFireRateOnly = rawDamageFireRateOnly;
|
||||||
|
this.firstShopWave = firstShopWave;
|
||||||
|
this.firstAbilityWave = firstAbilityWave;
|
||||||
|
this.abilityEveryWaves = abilityEveryWaves;
|
||||||
|
this.minimumFailCheckWave = minimumFailCheckWave;
|
||||||
|
this.bonusPickupSkill = bonusPickupSkill;
|
||||||
|
this.abilityValue = abilityValue;
|
||||||
|
this.postEarlyPressure = postEarlyPressure;
|
||||||
|
this.metaProfile = metaProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeRoute fresh() {
|
||||||
|
return new RuntimeRoute(
|
||||||
|
"runtime:fresh:no-menu-talent:shop-card-ability",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
4,
|
||||||
|
10,
|
||||||
|
0.20f,
|
||||||
|
0.65f,
|
||||||
|
2.6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeRoute exploit() {
|
||||||
|
return new RuntimeRoute(
|
||||||
|
"runtime:damage-fire-rate:no-shop:no-ability",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
999,
|
||||||
|
999,
|
||||||
|
999,
|
||||||
|
3,
|
||||||
|
0.05f,
|
||||||
|
0f,
|
||||||
|
3.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeRoute balanced() {
|
||||||
|
return new RuntimeRoute(
|
||||||
|
"runtime:balanced:shop:ability",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
3,
|
||||||
|
24,
|
||||||
|
0.30f,
|
||||||
|
1.40f,
|
||||||
|
1.6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeRoute moderateMeta() {
|
||||||
|
return new RuntimeRoute(
|
||||||
|
"runtime:moderate-meta:shop-card-ability",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
3,
|
||||||
|
28,
|
||||||
|
0.35f,
|
||||||
|
1.55f,
|
||||||
|
1.45f,
|
||||||
|
1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeRoute highMeta() {
|
||||||
|
return new RuntimeRoute(
|
||||||
|
"runtime:high-meta:shop-card-ability",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
3,
|
||||||
|
34,
|
||||||
|
0.40f,
|
||||||
|
1.70f,
|
||||||
|
1.35f,
|
||||||
|
2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyMeta(PlayerStats stats) {
|
||||||
|
if (metaProfile <= 0) return;
|
||||||
|
invest(stats, "health_1", metaProfile == 1 ? 3 : 5);
|
||||||
|
invest(stats, "damage_1", metaProfile == 1 ? 2 : 3);
|
||||||
|
invest(stats, "cooldown_1", metaProfile == 1 ? 2 : 4);
|
||||||
|
invest(stats, "speed_1", metaProfile == 1 ? 1 : 3);
|
||||||
|
invest(stats, "regen_1", metaProfile == 1 ? 2 : 4);
|
||||||
|
invest(stats, "control_1", metaProfile == 1 ? 1 : 3);
|
||||||
|
invest(stats, "economy_1", metaProfile == 1 ? 1 : 3);
|
||||||
|
invest(stats, "ability_1", metaProfile == 1 ? 1 : 3);
|
||||||
|
invest(stats, "utility_1", metaProfile == 1 ? 1 : 3);
|
||||||
|
invest(stats, "economy_reroll", metaProfile == 1 ? 1 : 2);
|
||||||
|
invest(stats, "utility_bonus", metaProfile == 1 ? 1 : 2);
|
||||||
|
if (metaProfile >= 2) {
|
||||||
|
invest(stats, "health_2", 2);
|
||||||
|
invest(stats, "cooldown_2", 2);
|
||||||
|
invest(stats, "regen_2", 2);
|
||||||
|
invest(stats, "keystone_pierce", 1);
|
||||||
|
invest(stats, "keystone_aoe", 1);
|
||||||
|
invest(stats, "attack_precision", 2);
|
||||||
|
invest(stats, "control_execute", 2);
|
||||||
|
invest(stats, "engineering_turret", 2);
|
||||||
|
invest(stats, "survival_barrier", 2);
|
||||||
|
invest(stats, "ability_blast", 2);
|
||||||
|
invest(stats, "keystone_turret", 1);
|
||||||
|
invest(stats, "keystone_control_execute", 1);
|
||||||
|
invest(stats, "keystone_shield_regen", 1);
|
||||||
|
invest(stats, "keystone_economy_reroll", 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invest(PlayerStats stats, String talentId, int levels) {
|
||||||
|
for (int i = 0; i < levels; i++) {
|
||||||
|
stats.incrementTalentNode(talentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RuntimeAbilityPreferences implements Preferences {
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Preferences putBoolean(String key, boolean val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Preferences putInteger(String key, int val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Preferences putLong(String key, long val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Preferences putFloat(String key, float val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Preferences putString(String key, String val) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Preferences put(Map<String, ?> vals) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInteger(String key) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLong(String key) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getFloat(String key) {
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getString(String key) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInteger(String key, int defValue) {
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLong(String key, long defValue) {
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getFloat(String key, float defValue) {
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getString(String key, String defValue) {
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, ?> get() {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(String key) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(String key) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.List;
|
||||||
|
import ru.project.tower.entities.circles.BonusBallData;
|
||||||
|
import ru.project.tower.run.RunRngStreams;
|
||||||
|
|
||||||
|
public final class BalanceSimulationHarness {
|
||||||
|
private static final int TARGET_WAVES = 120;
|
||||||
|
|
||||||
|
public List<SimulationReport> runRepresentativeSuite(long[] seeds) {
|
||||||
|
BalanceRouteSimulator routeSimulator = new BalanceRouteSimulator();
|
||||||
|
List<SimulationReport> reports = new ArrayList<>();
|
||||||
|
for (long seed : seeds) {
|
||||||
|
reports.add(
|
||||||
|
SimulationReport.fromRoute(
|
||||||
|
seed, routeSimulator.runFreshNoMenuTalentShopCardAbilityRoute(seed)));
|
||||||
|
reports.add(
|
||||||
|
SimulationReport.fromRoute(
|
||||||
|
seed, routeSimulator.runDamageFireRateNoShopNoAbility(seed)));
|
||||||
|
reports.add(
|
||||||
|
SimulationReport.fromRoute(
|
||||||
|
seed, routeSimulator.runBalancedShopAndAbilityRoute(seed)));
|
||||||
|
reports.add(
|
||||||
|
SimulationReport.fromRoute(
|
||||||
|
seed, routeSimulator.runSustainControlShopAndAbilityRoute(seed)));
|
||||||
|
reports.add(
|
||||||
|
SimulationReport.fromRoute(
|
||||||
|
seed, routeSimulator.runMixedNoShopNoAbilityRoute(seed)));
|
||||||
|
}
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SimulationReport> runRuntimeBackedSuite(long[] seeds) {
|
||||||
|
BalanceRuntimeHarness runtimeHarness = new BalanceRuntimeHarness();
|
||||||
|
List<SimulationReport> reports = new ArrayList<>();
|
||||||
|
for (long seed : seeds) {
|
||||||
|
reports.addAll(runtimeHarness.run(seed));
|
||||||
|
}
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimulationReport run(long seed, StrategyProfile profile) {
|
||||||
|
RunRngStreams streams = RunRngStreams.fromSeed(seed);
|
||||||
|
EnumMap<PowerAxis, Float> ratings = new EnumMap<>(PowerAxis.class);
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
ratings.put(axis, profile.metaRating() * profile.weight(axis) * 0.35f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int purchases = 0;
|
||||||
|
int activeDamageStacks = 0;
|
||||||
|
int activeSpeedStacks = 0;
|
||||||
|
int activeFireRateStacks = 0;
|
||||||
|
float survivability = 1f;
|
||||||
|
float bossKillSeconds = 0f;
|
||||||
|
float clearSecondsTotal = 0f;
|
||||||
|
float damageTaken = 0f;
|
||||||
|
int bonusDrops = 0;
|
||||||
|
int waveReached = 0;
|
||||||
|
|
||||||
|
for (int wave = 1; wave <= TARGET_WAVES; wave++) {
|
||||||
|
waveReached = wave;
|
||||||
|
PowerAxis chosenAxis = chooseAxis(streams.upgradeDrafts().nextFloat(), profile);
|
||||||
|
ratings.put(chosenAxis, ratings.get(chosenAxis) + 2.2f * profile.weight(chosenAxis));
|
||||||
|
purchases++;
|
||||||
|
|
||||||
|
if (streams.bonusDrops().nextFloat() < budgetedDropChance(wave)) {
|
||||||
|
bonusDrops++;
|
||||||
|
int type = streams.bonusDrops().nextIntInclusive(2);
|
||||||
|
if (type == 0) activeDamageStacks++;
|
||||||
|
if (type == 1) activeSpeedStacks++;
|
||||||
|
if (type == 2) activeFireRateStacks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bossKillSeconds = EndlessBalanceSpec.expectedBossKillSeconds(wave, ratings);
|
||||||
|
float incoming =
|
||||||
|
EndlessBalanceSpec.waveTuning(wave).circleDamage()
|
||||||
|
* (1.1f - Math.min(0.55f, ratings.get(PowerAxis.CONTROL) * 0.006f));
|
||||||
|
float mitigation =
|
||||||
|
EndlessBalanceSpec.axisMultiplier(ratings.get(PowerAxis.SHIELD))
|
||||||
|
+ EndlessBalanceSpec.axisMultiplier(ratings.get(PowerAxis.REGEN)) * 0.7f
|
||||||
|
+ EndlessBalanceSpec.axisMultiplier(ratings.get(PowerAxis.HEALTH))
|
||||||
|
* 0.5f;
|
||||||
|
survivability += mitigation * 0.015f - incoming * 0.010f;
|
||||||
|
damageTaken += Math.max(0f, incoming - mitigation * 0.35f);
|
||||||
|
clearSecondsTotal +=
|
||||||
|
Math.max(3f, EndlessBalanceSpec.waveTuning(wave).enemiesPerWave() / 8f);
|
||||||
|
if (survivability <= 0.15f) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float activeBonusPower =
|
||||||
|
EndlessBalanceSpec.activeBonusMultiplier(
|
||||||
|
BonusBallData.BonusType.DAMAGE, activeDamageStacks)
|
||||||
|
+ EndlessBalanceSpec.activeBonusMultiplier(
|
||||||
|
BonusBallData.BonusType.BULLET_SPEED, activeSpeedStacks)
|
||||||
|
+ (1f
|
||||||
|
/ EndlessBalanceSpec.activeBonusMultiplier(
|
||||||
|
BonusBallData.BonusType.FIRE_RATE, activeFireRateStacks));
|
||||||
|
return new SimulationReport(
|
||||||
|
seed,
|
||||||
|
profile.id(),
|
||||||
|
waveReached,
|
||||||
|
bossKillSeconds,
|
||||||
|
purchases,
|
||||||
|
clearSecondsTotal / Math.max(1, waveReached),
|
||||||
|
survivability <= 0.15f ? 1 : 0,
|
||||||
|
damageTaken,
|
||||||
|
bonusDrops,
|
||||||
|
activeBonusPower,
|
||||||
|
Math.max(0.01f, survivability),
|
||||||
|
ratings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PowerAxis chooseAxis(float roll, StrategyProfile profile) {
|
||||||
|
float total = 0f;
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
total += profile.weight(axis);
|
||||||
|
}
|
||||||
|
float target = roll * total;
|
||||||
|
float cumulative = 0f;
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
cumulative += profile.weight(axis);
|
||||||
|
if (target <= cumulative) {
|
||||||
|
return axis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PowerAxis.DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float budgetedDropChance(int wave) {
|
||||||
|
return Math.min(
|
||||||
|
0.32f, EndlessBalanceSpec.bonusDropBudgetThroughWave(wave) / (float) (wave + 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
public final class BossBalanceSpec {
|
||||||
|
private static final BossBalanceSpec GENERIC =
|
||||||
|
new BossBalanceSpec("generic", 1.0f, 8f, 32f, 7f, 42f, 5f);
|
||||||
|
private static final BossBalanceSpec[] SPECS = {
|
||||||
|
new BossBalanceSpec("SwarmQueen", 1.05f, 9f, 34f, 8f, 44f, 6f),
|
||||||
|
new BossBalanceSpec("Juggernaut", 1.22f, 11f, 40f, 9f, 48f, 7f),
|
||||||
|
new BossBalanceSpec("ShadowWeaver", 0.96f, 8f, 32f, 7f, 42f, 5f),
|
||||||
|
new BossBalanceSpec("VolatileReactor", 1.08f, 9f, 35f, 8f, 45f, 5f),
|
||||||
|
new BossBalanceSpec("GlacialWarden", 1.14f, 10f, 38f, 8f, 46f, 7f),
|
||||||
|
new BossBalanceSpec("IllusionMaster", 1.00f, 8f, 33f, 7f, 43f, 6f)
|
||||||
|
};
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final float healthMultiplier;
|
||||||
|
private final float firstBossMinTtkSeconds;
|
||||||
|
private final float firstBossMaxTtkSeconds;
|
||||||
|
private final float lateBossMinTtkSeconds;
|
||||||
|
private final float lateBossMaxTtkSeconds;
|
||||||
|
private final float minimumMechanicExposureSeconds;
|
||||||
|
|
||||||
|
private BossBalanceSpec(
|
||||||
|
String id,
|
||||||
|
float healthMultiplier,
|
||||||
|
float firstBossMinTtkSeconds,
|
||||||
|
float firstBossMaxTtkSeconds,
|
||||||
|
float lateBossMinTtkSeconds,
|
||||||
|
float lateBossMaxTtkSeconds,
|
||||||
|
float minimumMechanicExposureSeconds) {
|
||||||
|
this.id = id;
|
||||||
|
this.healthMultiplier = healthMultiplier;
|
||||||
|
this.firstBossMinTtkSeconds = firstBossMinTtkSeconds;
|
||||||
|
this.firstBossMaxTtkSeconds = firstBossMaxTtkSeconds;
|
||||||
|
this.lateBossMinTtkSeconds = lateBossMinTtkSeconds;
|
||||||
|
this.lateBossMaxTtkSeconds = lateBossMaxTtkSeconds;
|
||||||
|
this.minimumMechanicExposureSeconds = minimumMechanicExposureSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BossBalanceSpec forBoss(String id) {
|
||||||
|
for (BossBalanceSpec spec : SPECS) {
|
||||||
|
if (spec.id.equals(id)) return spec;
|
||||||
|
}
|
||||||
|
return GENERIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int healthFloorForWave(int wave) {
|
||||||
|
return Math.round(EndlessBalanceSpec.bossHealthForWave(wave) * healthMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float expectedKillSecondsAtWave(int wave, float effectiveDps) {
|
||||||
|
return healthFloorForWave(wave) / Math.max(0.01f, effectiveDps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float firstBossMinTtkSeconds() {
|
||||||
|
return firstBossMinTtkSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float firstBossMaxTtkSeconds() {
|
||||||
|
return firstBossMaxTtkSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float lateBossMinTtkSeconds() {
|
||||||
|
return lateBossMinTtkSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float lateBossMaxTtkSeconds() {
|
||||||
|
return lateBossMaxTtkSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float minimumMechanicExposureSeconds() {
|
||||||
|
return minimumMechanicExposureSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import ru.project.tower.entities.circles.BonusBallData;
|
||||||
|
import ru.project.tower.upgrades.shop.ShopUpgradeKind;
|
||||||
|
import ru.project.tower.upgrades.shop.ShopUpgradeRegistry;
|
||||||
|
import ru.project.tower.waves.WaveTuning;
|
||||||
|
|
||||||
|
|
||||||
|
public final class EndlessBalanceSpec {
|
||||||
|
public static final int SHOP_MAX_LEVEL = 30;
|
||||||
|
public static final int TALENT_AXIS_MAX_RATING = 55;
|
||||||
|
public static final float CRIT_CHANCE_CAP = 0.65f;
|
||||||
|
public static final float ACTIVE_DAMAGE_BONUS_CAP = 2.35f;
|
||||||
|
public static final float ACTIVE_FIRE_RATE_COOLDOWN_FLOOR = 0.42f;
|
||||||
|
public static final float ACTIVE_BULLET_SPEED_CAP = 2.25f;
|
||||||
|
public static final int BONUS_DROP_BUDGET_PER_TEN_WAVES = 7;
|
||||||
|
public static final int MAX_VISIBLE_BONUS_BALLS = 3;
|
||||||
|
public static final int BOSS_WAVE_INTERVAL = 20;
|
||||||
|
public static final float MIN_EXPECTED_LATE_BOSS_KILL_SECONDS = 6.0f;
|
||||||
|
|
||||||
|
private static final String[] ARCHETYPES = {
|
||||||
|
"BASIC", "STRONG", "FAST", "SPLITTER", "HEALER", "DASHER", "SNIPER", "SPAWNER"
|
||||||
|
};
|
||||||
|
private static final WaveBandTarget[] WAVE_BAND_TARGETS = {
|
||||||
|
new WaveBandTarget("teach", 1, 20, 1.0f, "teach movement, pickups, first boss prep"),
|
||||||
|
new WaveBandTarget("build-check", 21, 60, 1.65f, "require mixed cards and early shop"),
|
||||||
|
new WaveBandTarget(
|
||||||
|
"synergy-check", 61, 100, 2.35f, "require shop, abilities, and synergies"),
|
||||||
|
new WaveBandTarget(
|
||||||
|
"endless-pressure", 101, Integer.MAX_VALUE, 3.10f, "stress capped builds")
|
||||||
|
};
|
||||||
|
private static final BalanceRouteTarget[] ROUTE_TARGETS = {
|
||||||
|
new BalanceRouteTarget(
|
||||||
|
"fresh:no-menu-talent:shop-card-ability",
|
||||||
|
10,
|
||||||
|
12,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"fresh profile learns in-run shop, cards, and abilities before meta carries"
|
||||||
|
+ " farther"),
|
||||||
|
new BalanceRouteTarget(
|
||||||
|
"challenge:no-menu-talent:no-shop:no-ability",
|
||||||
|
3,
|
||||||
|
7,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"challenge profile intentionally ignores core in-run tools"),
|
||||||
|
new BalanceRouteTarget(
|
||||||
|
"damage-fire-rate:no-shop:no-ability",
|
||||||
|
20,
|
||||||
|
99,
|
||||||
|
8f,
|
||||||
|
60f,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
"regression profile must not revive the old wave-100 exploit"),
|
||||||
|
new BalanceRouteTarget(
|
||||||
|
"mixed:no-shop:no-ability",
|
||||||
|
20,
|
||||||
|
99,
|
||||||
|
7f,
|
||||||
|
60f,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
"mixed cards beat raw exploit but still need shop and abilities"),
|
||||||
|
new BalanceRouteTarget(
|
||||||
|
"balanced:shop:ability",
|
||||||
|
25,
|
||||||
|
120,
|
||||||
|
MIN_EXPECTED_LATE_BOSS_KILL_SECONDS,
|
||||||
|
45f,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"balanced route demonstrates normal in-run progression"),
|
||||||
|
new BalanceRouteTarget(
|
||||||
|
"sustain-control:shop:ability",
|
||||||
|
25,
|
||||||
|
120,
|
||||||
|
MIN_EXPECTED_LATE_BOSS_KILL_SECONDS,
|
||||||
|
45f,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"defensive route remains viable without raw damage dominance"),
|
||||||
|
new BalanceRouteTarget(
|
||||||
|
"moderate-meta:shop-card-ability",
|
||||||
|
45,
|
||||||
|
90,
|
||||||
|
MIN_EXPECTED_LATE_BOSS_KILL_SECONDS,
|
||||||
|
40f,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"moderate meta progression pushes past early walls without endless dominance"),
|
||||||
|
new BalanceRouteTarget(
|
||||||
|
"high-meta:capped-shop-card-ability",
|
||||||
|
100,
|
||||||
|
140,
|
||||||
|
MIN_EXPECTED_LATE_BOSS_KILL_SECONDS,
|
||||||
|
35f,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"capped meta handles endless pressure but must still respect boss TTK")
|
||||||
|
};
|
||||||
|
|
||||||
|
private EndlessBalanceSpec() {}
|
||||||
|
|
||||||
|
public static WaveTuning waveTuning(int wave) {
|
||||||
|
int safeWave = Math.max(1, wave);
|
||||||
|
float band = band(safeWave);
|
||||||
|
int enemies = Math.min(240, Math.round(5f + (safeWave - 1) * 1.6f + band * 8f));
|
||||||
|
float strong = Math.min(0.75f, 0.10f * (1 + (safeWave - 1) * 0.1f));
|
||||||
|
float fast = safeWave >= 3 ? 0.15f * (1 + (safeWave - 3) * 0.05f) : 0f;
|
||||||
|
float splitter = 0f;
|
||||||
|
if (safeWave >= 7) {
|
||||||
|
float progressToMax = Math.min(1f, (safeWave - 7) / 10f);
|
||||||
|
splitter = 0.05f + (0.15f - 0.05f) * progressToMax;
|
||||||
|
}
|
||||||
|
int basicHealth =
|
||||||
|
Math.min(20_000, 1 + Math.round((float) Math.pow(safeWave - 1, 1.45f) * 2.2f));
|
||||||
|
int strongHealth = Math.max(3, basicHealth * 3);
|
||||||
|
int damage = Math.min(180, 1 + Math.round((float) Math.pow(safeWave - 1, 1.05f) * 0.45f));
|
||||||
|
float speed = Math.min(2.6f, 1f + (safeWave - 1) * 0.012f);
|
||||||
|
return new WaveTuning(
|
||||||
|
safeWave,
|
||||||
|
enemies,
|
||||||
|
strong,
|
||||||
|
fast,
|
||||||
|
splitter,
|
||||||
|
basicHealth,
|
||||||
|
strongHealth,
|
||||||
|
damage,
|
||||||
|
speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WaveBandTarget waveBandTarget(int wave) {
|
||||||
|
int safeWave = Math.max(1, wave);
|
||||||
|
for (WaveBandTarget target : WAVE_BAND_TARGETS) {
|
||||||
|
if (safeWave >= target.firstWave() && safeWave <= target.lastWave()) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return WAVE_BAND_TARGETS[WAVE_BAND_TARGETS.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BalanceRouteTarget routeTarget(String strategyId) {
|
||||||
|
for (BalanceRouteTarget target : ROUTE_TARGETS) {
|
||||||
|
if (target.strategyId().equals(strategyId)) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown balance route target: " + strategyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EnemyComposition compositionForWave(int wave) {
|
||||||
|
int safeWave = Math.max(1, wave);
|
||||||
|
float strong = safeWave >= 2 ? Math.min(0.22f, 0.08f + safeWave * 0.0015f) : 0f;
|
||||||
|
float fast = safeWave >= 3 ? Math.min(0.16f, 0.06f + safeWave * 0.001f) : 0f;
|
||||||
|
float splitter = safeWave >= 7 ? Math.min(0.12f, 0.04f + safeWave * 0.001f) : 0f;
|
||||||
|
float healer = safeWave >= 8 ? Math.min(0.08f, 0.025f + safeWave * 0.0007f) : 0f;
|
||||||
|
float dasher = safeWave >= 6 ? Math.min(0.11f, 0.04f + safeWave * 0.0008f) : 0f;
|
||||||
|
float sniper = safeWave >= 7 ? Math.min(0.09f, 0.03f + safeWave * 0.0007f) : 0f;
|
||||||
|
float spawner =
|
||||||
|
safeWave >= 15 && (safeWave - 15) % 11 == 0
|
||||||
|
? Math.min(0.10f, 0.05f + safeWave * 0.0005f)
|
||||||
|
: 0f;
|
||||||
|
float basic =
|
||||||
|
Math.max(0.20f, 1f - strong - fast - splitter - healer - dasher - sniper - spawner);
|
||||||
|
return new EnemyComposition(
|
||||||
|
ARCHETYPES,
|
||||||
|
new float[] {basic, strong, fast, splitter, healer, dasher, sniper, spawner});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int bonusDropBudgetThroughWave(int wave) {
|
||||||
|
return Math.max(1, ((Math.max(1, wave) + 9) / 10) * BONUS_DROP_BUDGET_PER_TEN_WAVES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int bonusDropBudgetForWave(int wave) {
|
||||||
|
int bandWave = (Math.max(1, wave) - 1) % 10;
|
||||||
|
return bandWave < BONUS_DROP_BUDGET_PER_TEN_WAVES ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int maxVisibleBonusBalls() {
|
||||||
|
return MAX_VISIBLE_BONUS_BALLS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float activeBonusMultiplier(BonusBallData.BonusType type, int stacks) {
|
||||||
|
int safeStacks = Math.max(0, stacks);
|
||||||
|
switch (type) {
|
||||||
|
case DAMAGE:
|
||||||
|
if (safeStacks == 0) return 1f;
|
||||||
|
return Math.min(
|
||||||
|
ACTIVE_DAMAGE_BONUS_CAP,
|
||||||
|
2f + diminishing(0f, 0.18f, safeStacks - 1, 0.55f));
|
||||||
|
case BULLET_SPEED:
|
||||||
|
if (safeStacks == 0) return 1f;
|
||||||
|
if (safeStacks == 1) return 1.5f;
|
||||||
|
return Math.min(
|
||||||
|
ACTIVE_BULLET_SPEED_CAP,
|
||||||
|
2.25f + diminishing(0f, 0.08f, safeStacks - 2, 0.50f));
|
||||||
|
case FIRE_RATE:
|
||||||
|
if (safeStacks == 0) return 1f;
|
||||||
|
return Math.max(
|
||||||
|
ACTIVE_FIRE_RATE_COOLDOWN_FLOOR,
|
||||||
|
(float) (0.6f * Math.pow(0.75f, safeStacks - 1)));
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown bonus type: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int upgradeCardDamagePercent(int existingStacks) {
|
||||||
|
int safeStacks = Math.max(0, existingStacks);
|
||||||
|
return Math.max(6, Math.round(15f * (float) Math.pow(0.86f, safeStacks)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float upgradeCardFireRateMultiplier(int existingStacks) {
|
||||||
|
int safeStacks = Math.max(0, existingStacks);
|
||||||
|
return Math.min(0.96f, 1f - 0.10f * (float) Math.pow(0.72f, safeStacks));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int shopCost(ShopUpgradeKind kind, int level) {
|
||||||
|
int safeLevel = Math.max(0, level);
|
||||||
|
double scale = ShopUpgradeRegistry.definition(kind).costScale();
|
||||||
|
return Math.max(5, (int) (5.0 * Math.pow(scale, Math.min(safeLevel, SHOP_MAX_LEVEL))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopRating(ShopUpgradeKind kind, int level) {
|
||||||
|
float familyScale =
|
||||||
|
kind == ShopUpgradeKind.CRIT_CHANCE || kind == ShopUpgradeKind.CRIT_MULTIPLIER
|
||||||
|
? 0.82f
|
||||||
|
: 1f;
|
||||||
|
return familyScale * diminishing(0f, 4f, Math.min(level, SHOP_MAX_LEVEL), 0.89f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int shopDamageBonusAtLevel(int level) {
|
||||||
|
return Math.round(shopRating(ShopUpgradeKind.DAMAGE, level) * 0.25f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopSpeedBonusAtLevel(int level) {
|
||||||
|
return shopRating(ShopUpgradeKind.SPEED, level) * 12.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopCooldownReductionAtLevel(int level) {
|
||||||
|
return shopRating(ShopUpgradeKind.COOLDOWN, level) * 12.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int shopRegenBonusAtLevel(int level) {
|
||||||
|
return Math.round(shopRating(ShopUpgradeKind.REGEN, level) * 0.25f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopCritChanceBonusAtLevel(int level) {
|
||||||
|
return Math.min(CRIT_CHANCE_CAP, shopRating(ShopUpgradeKind.CRIT_CHANCE, level) * 0.0065f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopCritMultiplierBonusAtLevel(int level) {
|
||||||
|
return shopRating(ShopUpgradeKind.CRIT_MULTIPLIER, level) * 0.0305f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int shopPierceBonusAtLevel(int level) {
|
||||||
|
return Math.min(6, Math.round(shopRating(ShopUpgradeKind.PIERCE, level) * 0.18f));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopAoeRadiusBonusAtLevel(int level) {
|
||||||
|
return Math.min(180f, shopRating(ShopUpgradeKind.SPLASH_DAMAGE, level) * 3.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopExecuteDamageBonusAtLevel(int level) {
|
||||||
|
return Math.min(0.35f, shopRating(ShopUpgradeKind.EXECUTE_DAMAGE, level) * 0.006f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopBossDamageBonusAtLevel(int level) {
|
||||||
|
return Math.min(0.40f, shopRating(ShopUpgradeKind.BOSS_DAMAGE, level) * 0.0065f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopControlledDamageBonusAtLevel(int level) {
|
||||||
|
return Math.min(0.35f, shopRating(ShopUpgradeKind.CONTROL_DAMAGE, level) * 0.006f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopContactArmorReductionAtLevel(int level) {
|
||||||
|
return Math.min(0.35f, shopRating(ShopUpgradeKind.CONTACT_ARMOR, level) * 0.006f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopProjectileArmorReductionAtLevel(int level) {
|
||||||
|
return Math.min(0.30f, shopRating(ShopUpgradeKind.PROJECTILE_ARMOR, level) * 0.005f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int shopShieldCapacityBonusAtLevel(int level) {
|
||||||
|
return Math.round(shopRating(ShopUpgradeKind.SHIELD_CAPACITY, level) * 1.8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopShieldRechargeReductionAtLevel(int level) {
|
||||||
|
return Math.min(0.35f, shopRating(ShopUpgradeKind.SHIELD_RECHARGE, level) * 0.006f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopEmergencyBarrierThresholdAtLevel(int level) {
|
||||||
|
return Math.min(0.30f, shopRating(ShopUpgradeKind.EMERGENCY_BARRIER, level) * 0.004f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int shopWaveRecoveryHealAtLevel(int level) {
|
||||||
|
return Math.round(shopRating(ShopUpgradeKind.WAVE_RECOVERY, level) * 0.65f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopSafeMobilityReductionAtLevel(int level) {
|
||||||
|
return Math.min(0.30f, shopRating(ShopUpgradeKind.SAFE_MOBILITY, level) * 0.005f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopSlowAuraStrengthAtLevel(int level) {
|
||||||
|
return Math.min(0.24f, shopRating(ShopUpgradeKind.SLOW_AURA, level) * 0.004f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopPickupRadiusBonusAtLevel(int level) {
|
||||||
|
return Math.min(180f, shopRating(ShopUpgradeKind.PICKUP_RADIUS, level) * 2.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int shopBonusDurationBonusAtLevel(int level) {
|
||||||
|
return Math.min(3, Math.round(shopRating(ShopUpgradeKind.BONUS_DURATION, level) * 0.08f));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopBonusPowerBonusAtLevel(int level) {
|
||||||
|
return Math.min(0.30f, shopRating(ShopUpgradeKind.BONUS_POWER, level) * 0.005f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopBonusStabilityBonusAtLevel(int level) {
|
||||||
|
return Math.min(0.35f, shopRating(ShopUpgradeKind.BONUS_STABILITY, level) * 0.006f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int shopBonusPityBonusAtLevel(int level) {
|
||||||
|
return Math.min(3, Math.round(shopRating(ShopUpgradeKind.BONUS_PITY, level) * 0.08f));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopBonusFilterChanceAtLevel(int level) {
|
||||||
|
return Math.min(0.35f, shopRating(ShopUpgradeKind.BONUS_FILTER, level) * 0.006f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopCoinIncomeBonusAtLevel(int level) {
|
||||||
|
return Math.min(0.45f, shopRating(ShopUpgradeKind.COIN_INCOME, level) * 0.007f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopDiscountAtLevel(int level) {
|
||||||
|
return Math.min(0.25f, shopRating(ShopUpgradeKind.SHOP_DISCOUNT, level) * 0.004f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopCardRerollDiscountAtLevel(int level) {
|
||||||
|
return Math.min(0.35f, shopRating(ShopUpgradeKind.CARD_REROLL, level) * 0.006f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shopSynergyCatalystBonusAtLevel(int level) {
|
||||||
|
return Math.min(0.30f, shopRating(ShopUpgradeKind.SYNERGY_CATALYST, level) * 0.005f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int permanentTalentRating(int investedLevels) {
|
||||||
|
int safeLevels = Math.max(0, investedLevels);
|
||||||
|
if (safeLevels >= 30) {
|
||||||
|
return TALENT_AXIS_MAX_RATING;
|
||||||
|
}
|
||||||
|
return Math.min(TALENT_AXIS_MAX_RATING, Math.round(diminishing(0f, 5f, safeLevels, 0.88f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float axisMultiplier(float rating) {
|
||||||
|
return 1f + (float) (Math.log1p(Math.max(0f, rating)) / Math.log(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float critChanceFromRating(float baseCritChance, float rating) {
|
||||||
|
return Math.min(CRIT_CHANCE_CAP, baseCritChance + rating * 0.0065f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int bossHealthForWave(int wave) {
|
||||||
|
int safeWave = Math.max(BOSS_WAVE_INTERVAL, wave);
|
||||||
|
return Math.round(180f + safeWave * 34f + band(safeWave) * 420f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int bossHealthForWave(String bossId, int wave) {
|
||||||
|
return BossBalanceSpec.forBoss(bossId).healthFloorForWave(wave);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float expectedBossKillSeconds(int wave, Map<PowerAxis, Float> ratings) {
|
||||||
|
float effectiveDps = 10f * offenseMultiplier(ratings);
|
||||||
|
return bossHealthForWave(wave) / effectiveDps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float offenseMultiplier(Map<PowerAxis, Float> ratings) {
|
||||||
|
return cappedAxisMultiplier(ratings, PowerAxis.DAMAGE, 1f)
|
||||||
|
* cappedAxisMultiplier(ratings, PowerAxis.CADENCE, 1f)
|
||||||
|
* cappedAxisMultiplier(ratings, PowerAxis.CRIT, 0.6f)
|
||||||
|
* cappedAxisMultiplier(ratings, PowerAxis.PIERCE, 0.18f)
|
||||||
|
* cappedAxisMultiplier(ratings, PowerAxis.AOE, 0.24f)
|
||||||
|
* cappedAxisMultiplier(ratings, PowerAxis.CONTROL, 0.16f)
|
||||||
|
* cappedAxisMultiplier(ratings, PowerAxis.BOSSING, 0.30f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float effectivePower(Map<PowerAxis, Float> ratings) {
|
||||||
|
float total = 0f;
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
total += cappedAxisMultiplier(ratings, axis, 1f);
|
||||||
|
}
|
||||||
|
float synergy =
|
||||||
|
Math.min(
|
||||||
|
cappedValue(ratings, PowerAxis.DAMAGE),
|
||||||
|
cappedValue(ratings, PowerAxis.CRIT))
|
||||||
|
* 0.025f
|
||||||
|
+ Math.min(
|
||||||
|
value(ratings, PowerAxis.SHIELD),
|
||||||
|
value(ratings, PowerAxis.REGEN))
|
||||||
|
* 0.020f
|
||||||
|
+ Math.min(value(ratings, PowerAxis.AOE), value(ratings, PowerAxis.CONTROL))
|
||||||
|
* 0.018f;
|
||||||
|
return total + synergy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float cappedAxisMultiplier(
|
||||||
|
Map<PowerAxis, Float> ratings, PowerAxis axis, float scale) {
|
||||||
|
return axisMultiplier(cappedValue(ratings, axis) * scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float cappedValue(Map<PowerAxis, Float> ratings, PowerAxis axis) {
|
||||||
|
float raw = value(ratings, axis);
|
||||||
|
BalanceEffectSpec spec = BalanceEffectRegistry.forAxis(axis);
|
||||||
|
float floored = spec.ratingFloor() == null ? raw : Math.max(spec.ratingFloor(), raw);
|
||||||
|
return spec.ratingCap() == null ? floored : Math.min(spec.ratingCap(), floored);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float value(Map<PowerAxis, Float> ratings, PowerAxis axis) {
|
||||||
|
if (ratings == null) return 0f;
|
||||||
|
Float value = ratings.get(axis);
|
||||||
|
return value == null ? 0f : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float diminishing(float base, float perStack, int stacks, float retention) {
|
||||||
|
float value = base;
|
||||||
|
float next = perStack;
|
||||||
|
for (int i = 0; i < stacks; i++) {
|
||||||
|
value += next;
|
||||||
|
next *= retention;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float band(int wave) {
|
||||||
|
return Math.max(0, (wave - 1) / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class WaveBandTarget {
|
||||||
|
private final String id;
|
||||||
|
private final int firstWave;
|
||||||
|
private final int lastWave;
|
||||||
|
private final float pressureMultiplier;
|
||||||
|
private final String designGoal;
|
||||||
|
|
||||||
|
private WaveBandTarget(
|
||||||
|
String id,
|
||||||
|
int firstWave,
|
||||||
|
int lastWave,
|
||||||
|
float pressureMultiplier,
|
||||||
|
String designGoal) {
|
||||||
|
this.id = id;
|
||||||
|
this.firstWave = firstWave;
|
||||||
|
this.lastWave = lastWave;
|
||||||
|
this.pressureMultiplier = pressureMultiplier;
|
||||||
|
this.designGoal = designGoal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int firstWave() {
|
||||||
|
return firstWave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int lastWave() {
|
||||||
|
return lastWave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float pressureMultiplier() {
|
||||||
|
return pressureMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String designGoal() {
|
||||||
|
return designGoal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class EnemyComposition {
|
||||||
|
private final String[] archetypes;
|
||||||
|
private final float[] weights;
|
||||||
|
|
||||||
|
public EnemyComposition(String[] archetypes, float[] weights) {
|
||||||
|
if (archetypes.length != weights.length || archetypes.length == 0) {
|
||||||
|
throw new IllegalArgumentException("composition requires matching non-empty arrays");
|
||||||
|
}
|
||||||
|
this.archetypes = Arrays.copyOf(archetypes, archetypes.length);
|
||||||
|
this.weights = Arrays.copyOf(weights, weights.length);
|
||||||
|
normalize(this.weights);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return archetypes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String archetypeAt(int index) {
|
||||||
|
return archetypes[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public float weightAt(int index) {
|
||||||
|
return weights[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public float weightOf(String archetype) {
|
||||||
|
Objects.requireNonNull(archetype, "archetype");
|
||||||
|
for (int i = 0; i < archetypes.length; i++) {
|
||||||
|
if (archetypes[i].equals(archetype)) {
|
||||||
|
return weights[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float totalWeight() {
|
||||||
|
float total = 0f;
|
||||||
|
for (float weight : weights) {
|
||||||
|
total += weight;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String select(float roll) {
|
||||||
|
float clampedRoll = Math.max(0f, Math.min(0.999999f, roll));
|
||||||
|
float cumulative = 0f;
|
||||||
|
for (int i = 0; i < archetypes.length; i++) {
|
||||||
|
cumulative += weights[i];
|
||||||
|
if (clampedRoll < cumulative) {
|
||||||
|
return archetypes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return archetypes[archetypes.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void normalize(float[] weights) {
|
||||||
|
float total = 0f;
|
||||||
|
for (float weight : weights) {
|
||||||
|
if (weight < 0f) {
|
||||||
|
throw new IllegalArgumentException("composition weights cannot be negative");
|
||||||
|
}
|
||||||
|
total += weight;
|
||||||
|
}
|
||||||
|
if (total <= 0f) {
|
||||||
|
throw new IllegalArgumentException("composition weight total must be > 0");
|
||||||
|
}
|
||||||
|
for (int i = 0; i < weights.length; i++) {
|
||||||
|
weights[i] /= total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
public final class MetaProgressionPacing {
|
||||||
|
public static final int FIRST_ABILITY_TARGET_WAVE = 12;
|
||||||
|
public static final int FIRST_BOSS_TARGET_WAVE = 20;
|
||||||
|
public static final int FIRST_KEYSTONE_TARGET_WAVE = 25;
|
||||||
|
public static final int FIRST_MAXED_AXIS_TARGET_RUNS = 8;
|
||||||
|
|
||||||
|
private MetaProgressionPacing() {}
|
||||||
|
|
||||||
|
public static int currencyEarnedForRun(int reachedWave) {
|
||||||
|
int safeWave = Math.max(0, reachedWave);
|
||||||
|
int waveClearCurrency = safeWave * 5 + (safeWave / 5) * 10;
|
||||||
|
int gameOverAward = safeWave;
|
||||||
|
return waveClearCurrency + gameOverAward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int projectedCurrencyAfterRuns(int... reachedWaves) {
|
||||||
|
int total = 0;
|
||||||
|
for (int wave : reachedWaves) {
|
||||||
|
total += currencyEarnedForRun(wave);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int firstKeystoneCostTarget() {
|
||||||
|
return 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int firstMaxedAxisCostTarget() {
|
||||||
|
return 100 + 150 + 225 + 337 + 506;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
public enum PowerAxis {
|
||||||
|
DAMAGE,
|
||||||
|
CADENCE,
|
||||||
|
CRIT,
|
||||||
|
PIERCE,
|
||||||
|
AOE,
|
||||||
|
CONTROL,
|
||||||
|
BOSSING,
|
||||||
|
HEALTH,
|
||||||
|
REGEN,
|
||||||
|
SHIELD,
|
||||||
|
ARMOR,
|
||||||
|
ECONOMY,
|
||||||
|
UTILITY,
|
||||||
|
ABILITY
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import ru.project.tower.support.RandomSource;
|
||||||
|
|
||||||
|
public final class SeededUpgradeDraftDeck {
|
||||||
|
private static final int OFFER_COUNT = 3;
|
||||||
|
private static final int MAX_REROLLS = 2;
|
||||||
|
private static final int BASE_REROLL_COST = 15;
|
||||||
|
private final UpgradeCard[] registry;
|
||||||
|
private final Map<String, Integer> pickedCopies = new HashMap<>();
|
||||||
|
private UpgradeCard[] activeOffer;
|
||||||
|
private int rerollsUsed;
|
||||||
|
|
||||||
|
public SeededUpgradeDraftDeck() {
|
||||||
|
this(UpgradeCardRegistry.all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SeededUpgradeDraftDeck(UpgradeCard[] registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpgradeCard[] draft(RandomSource random) {
|
||||||
|
UpgradeCard[] pool = eligiblePool();
|
||||||
|
UpgradeCard[] offer = new UpgradeCard[Math.min(OFFER_COUNT, pool.length)];
|
||||||
|
int poolSize = pool.length;
|
||||||
|
for (int i = 0; i < offer.length; i++) {
|
||||||
|
int index = random.nextIntInclusive(poolSize - 1);
|
||||||
|
offer[i] = pool[index];
|
||||||
|
int after = poolSize - index - 1;
|
||||||
|
if (after > 0) {
|
||||||
|
System.arraycopy(pool, index + 1, pool, index, after);
|
||||||
|
}
|
||||||
|
poolSize--;
|
||||||
|
}
|
||||||
|
activeOffer = offer;
|
||||||
|
return copyOffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pick(int index) {
|
||||||
|
if (activeOffer == null || index < 0 || index >= activeOffer.length) {
|
||||||
|
throw new IllegalArgumentException("No active card offer at index " + index);
|
||||||
|
}
|
||||||
|
UpgradeCard card = activeOffer[index];
|
||||||
|
pickedCopies.put(card.id(), pickedCopies.getOrDefault(card.id(), 0) + 1);
|
||||||
|
activeOffer = null;
|
||||||
|
rerollsUsed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpgradeCard[] reroll(RandomSource random) {
|
||||||
|
if (activeOffer == null) {
|
||||||
|
throw new IllegalStateException("Cannot reroll before drafting an offer");
|
||||||
|
}
|
||||||
|
if (!canReroll()) {
|
||||||
|
throw new IllegalStateException("No card rerolls remaining");
|
||||||
|
}
|
||||||
|
rerollsUsed++;
|
||||||
|
return draft(random);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canReroll() {
|
||||||
|
return rerollsRemaining() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int rerollsRemaining() {
|
||||||
|
return Math.max(0, MAX_REROLLS - rerollsUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int rerollCost(float discountFraction) {
|
||||||
|
float discount = Math.max(0f, Math.min(0.80f, discountFraction));
|
||||||
|
return Math.max(1, (int) Math.ceil(BASE_REROLL_COST * (1f - discount)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot snapshot() {
|
||||||
|
String[] ids = activeOffer == null ? new String[0] : new String[activeOffer.length];
|
||||||
|
for (int i = 0; i < ids.length; i++) {
|
||||||
|
ids[i] = activeOffer[i].id();
|
||||||
|
}
|
||||||
|
return new Snapshot(new HashMap<>(pickedCopies), ids, rerollsUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void apply(Snapshot snapshot) {
|
||||||
|
pickedCopies.clear();
|
||||||
|
pickedCopies.putAll(snapshot.pickedCopies());
|
||||||
|
rerollsUsed = snapshot.rerollsUsed();
|
||||||
|
activeOffer = new UpgradeCard[snapshot.activeOfferIds().length];
|
||||||
|
for (int i = 0; i < activeOffer.length; i++) {
|
||||||
|
activeOffer[i] = find(snapshot.activeOfferIds()[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UpgradeCard[] copyOffer() {
|
||||||
|
UpgradeCard[] copy = new UpgradeCard[activeOffer.length];
|
||||||
|
System.arraycopy(activeOffer, 0, copy, 0, activeOffer.length);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UpgradeCard[] eligiblePool() {
|
||||||
|
int count = 0;
|
||||||
|
for (UpgradeCard card : registry) {
|
||||||
|
if (pickedCopies.getOrDefault(card.id(), 0) < card.maxCopies()) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpgradeCard[] pool = new UpgradeCard[count];
|
||||||
|
int index = 0;
|
||||||
|
for (UpgradeCard card : registry) {
|
||||||
|
if (pickedCopies.getOrDefault(card.id(), 0) < card.maxCopies()) {
|
||||||
|
pool[index++] = card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UpgradeCard find(String id) {
|
||||||
|
for (UpgradeCard card : registry) {
|
||||||
|
if (card.id().equals(id)) {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown upgrade card id: " + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Snapshot {
|
||||||
|
private final Map<String, Integer> pickedCopies;
|
||||||
|
private final String[] activeOfferIds;
|
||||||
|
private final int rerollsUsed;
|
||||||
|
|
||||||
|
public Snapshot(Map<String, Integer> pickedCopies, String[] activeOfferIds) {
|
||||||
|
this(pickedCopies, activeOfferIds, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot(
|
||||||
|
Map<String, Integer> pickedCopies, String[] activeOfferIds, int rerollsUsed) {
|
||||||
|
this.pickedCopies = new HashMap<>(pickedCopies);
|
||||||
|
this.activeOfferIds = new String[activeOfferIds.length];
|
||||||
|
System.arraycopy(activeOfferIds, 0, this.activeOfferIds, 0, activeOfferIds.length);
|
||||||
|
this.rerollsUsed = Math.max(0, rerollsUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Integer> pickedCopies() {
|
||||||
|
return new HashMap<>(pickedCopies);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] activeOfferIds() {
|
||||||
|
String[] copy = new String[activeOfferIds.length];
|
||||||
|
System.arraycopy(activeOfferIds, 0, copy, 0, activeOfferIds.length);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int rerollsUsed() {
|
||||||
|
return rerollsUsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class SimulationReport {
|
||||||
|
private final long seed;
|
||||||
|
private final String sourceKind;
|
||||||
|
private final String strategyId;
|
||||||
|
private final int waveReached;
|
||||||
|
private final float latestBossKillSeconds;
|
||||||
|
private final int purchases;
|
||||||
|
private final int moneyEarned;
|
||||||
|
private final int moneySpent;
|
||||||
|
private final float averageClearSeconds;
|
||||||
|
private final int deaths;
|
||||||
|
private final float damageTaken;
|
||||||
|
private final int bonusDrops;
|
||||||
|
private final int bonusSpawned;
|
||||||
|
private final int bonusCollected;
|
||||||
|
private final float activeBonusPower;
|
||||||
|
private final int activeBonusUptimeWaves;
|
||||||
|
private final int triggeredSynergies;
|
||||||
|
private final float survivability;
|
||||||
|
private final EnumMap<PowerAxis, Float> effectivePowerByAxis;
|
||||||
|
private final float firstBossKillSeconds;
|
||||||
|
private final int shopPurchases;
|
||||||
|
private final int abilityUses;
|
||||||
|
private final int selectedDamageCards;
|
||||||
|
private final int selectedFireRateCards;
|
||||||
|
private final int peakVisibleBonusBalls;
|
||||||
|
private final float effectiveDamage;
|
||||||
|
private final float effectiveShotCooldownMs;
|
||||||
|
|
||||||
|
public SimulationReport(
|
||||||
|
long seed,
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float latestBossKillSeconds,
|
||||||
|
int purchases,
|
||||||
|
float averageClearSeconds,
|
||||||
|
int deaths,
|
||||||
|
float damageTaken,
|
||||||
|
int bonusDrops,
|
||||||
|
float activeBonusPower,
|
||||||
|
float survivability,
|
||||||
|
Map<PowerAxis, Float> effectivePowerByAxis) {
|
||||||
|
this(
|
||||||
|
seed,
|
||||||
|
strategyId,
|
||||||
|
waveReached,
|
||||||
|
latestBossKillSeconds,
|
||||||
|
purchases,
|
||||||
|
averageClearSeconds,
|
||||||
|
deaths,
|
||||||
|
damageTaken,
|
||||||
|
bonusDrops,
|
||||||
|
activeBonusPower,
|
||||||
|
survivability,
|
||||||
|
effectivePowerByAxis,
|
||||||
|
"axis-simulation",
|
||||||
|
latestBossKillSeconds,
|
||||||
|
purchases,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimulationReport(
|
||||||
|
long seed,
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float latestBossKillSeconds,
|
||||||
|
int purchases,
|
||||||
|
float averageClearSeconds,
|
||||||
|
int deaths,
|
||||||
|
float damageTaken,
|
||||||
|
int bonusDrops,
|
||||||
|
float activeBonusPower,
|
||||||
|
float survivability,
|
||||||
|
Map<PowerAxis, Float> effectivePowerByAxis,
|
||||||
|
float firstBossKillSeconds,
|
||||||
|
int shopPurchases,
|
||||||
|
int abilityUses,
|
||||||
|
int selectedDamageCards,
|
||||||
|
int selectedFireRateCards,
|
||||||
|
int peakVisibleBonusBalls,
|
||||||
|
float effectiveDamage,
|
||||||
|
float effectiveShotCooldownMs) {
|
||||||
|
this(
|
||||||
|
seed,
|
||||||
|
strategyId,
|
||||||
|
waveReached,
|
||||||
|
latestBossKillSeconds,
|
||||||
|
purchases,
|
||||||
|
averageClearSeconds,
|
||||||
|
deaths,
|
||||||
|
damageTaken,
|
||||||
|
bonusDrops,
|
||||||
|
activeBonusPower,
|
||||||
|
survivability,
|
||||||
|
effectivePowerByAxis,
|
||||||
|
"axis-simulation",
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopPurchases,
|
||||||
|
abilityUses,
|
||||||
|
selectedDamageCards,
|
||||||
|
selectedFireRateCards,
|
||||||
|
peakVisibleBonusBalls,
|
||||||
|
effectiveDamage,
|
||||||
|
effectiveShotCooldownMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimulationReport(
|
||||||
|
long seed,
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float latestBossKillSeconds,
|
||||||
|
int purchases,
|
||||||
|
float averageClearSeconds,
|
||||||
|
int deaths,
|
||||||
|
float damageTaken,
|
||||||
|
int bonusDrops,
|
||||||
|
float activeBonusPower,
|
||||||
|
float survivability,
|
||||||
|
Map<PowerAxis, Float> effectivePowerByAxis,
|
||||||
|
String sourceKind,
|
||||||
|
float firstBossKillSeconds,
|
||||||
|
int shopPurchases,
|
||||||
|
int abilityUses,
|
||||||
|
int selectedDamageCards,
|
||||||
|
int selectedFireRateCards,
|
||||||
|
int peakVisibleBonusBalls,
|
||||||
|
float effectiveDamage,
|
||||||
|
float effectiveShotCooldownMs) {
|
||||||
|
this(
|
||||||
|
seed,
|
||||||
|
strategyId,
|
||||||
|
waveReached,
|
||||||
|
latestBossKillSeconds,
|
||||||
|
purchases,
|
||||||
|
averageClearSeconds,
|
||||||
|
deaths,
|
||||||
|
damageTaken,
|
||||||
|
bonusDrops,
|
||||||
|
activeBonusPower,
|
||||||
|
survivability,
|
||||||
|
effectivePowerByAxis,
|
||||||
|
sourceKind,
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopPurchases,
|
||||||
|
abilityUses,
|
||||||
|
selectedDamageCards,
|
||||||
|
selectedFireRateCards,
|
||||||
|
peakVisibleBonusBalls,
|
||||||
|
effectiveDamage,
|
||||||
|
effectiveShotCooldownMs,
|
||||||
|
bonusDrops,
|
||||||
|
bonusDrops,
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimulationReport(
|
||||||
|
long seed,
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float latestBossKillSeconds,
|
||||||
|
int purchases,
|
||||||
|
float averageClearSeconds,
|
||||||
|
int deaths,
|
||||||
|
float damageTaken,
|
||||||
|
int bonusDrops,
|
||||||
|
float activeBonusPower,
|
||||||
|
float survivability,
|
||||||
|
Map<PowerAxis, Float> effectivePowerByAxis,
|
||||||
|
String sourceKind,
|
||||||
|
float firstBossKillSeconds,
|
||||||
|
int shopPurchases,
|
||||||
|
int abilityUses,
|
||||||
|
int selectedDamageCards,
|
||||||
|
int selectedFireRateCards,
|
||||||
|
int peakVisibleBonusBalls,
|
||||||
|
float effectiveDamage,
|
||||||
|
float effectiveShotCooldownMs,
|
||||||
|
int bonusSpawned,
|
||||||
|
int bonusCollected,
|
||||||
|
int activeBonusUptimeWaves) {
|
||||||
|
this(
|
||||||
|
seed,
|
||||||
|
strategyId,
|
||||||
|
waveReached,
|
||||||
|
latestBossKillSeconds,
|
||||||
|
purchases,
|
||||||
|
averageClearSeconds,
|
||||||
|
deaths,
|
||||||
|
damageTaken,
|
||||||
|
bonusDrops,
|
||||||
|
activeBonusPower,
|
||||||
|
survivability,
|
||||||
|
effectivePowerByAxis,
|
||||||
|
sourceKind,
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopPurchases,
|
||||||
|
abilityUses,
|
||||||
|
selectedDamageCards,
|
||||||
|
selectedFireRateCards,
|
||||||
|
peakVisibleBonusBalls,
|
||||||
|
effectiveDamage,
|
||||||
|
effectiveShotCooldownMs,
|
||||||
|
bonusSpawned,
|
||||||
|
bonusCollected,
|
||||||
|
activeBonusUptimeWaves,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimulationReport(
|
||||||
|
long seed,
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float latestBossKillSeconds,
|
||||||
|
int purchases,
|
||||||
|
float averageClearSeconds,
|
||||||
|
int deaths,
|
||||||
|
float damageTaken,
|
||||||
|
int bonusDrops,
|
||||||
|
float activeBonusPower,
|
||||||
|
float survivability,
|
||||||
|
Map<PowerAxis, Float> effectivePowerByAxis,
|
||||||
|
String sourceKind,
|
||||||
|
float firstBossKillSeconds,
|
||||||
|
int shopPurchases,
|
||||||
|
int abilityUses,
|
||||||
|
int selectedDamageCards,
|
||||||
|
int selectedFireRateCards,
|
||||||
|
int peakVisibleBonusBalls,
|
||||||
|
float effectiveDamage,
|
||||||
|
float effectiveShotCooldownMs,
|
||||||
|
int bonusSpawned,
|
||||||
|
int bonusCollected,
|
||||||
|
int activeBonusUptimeWaves,
|
||||||
|
int triggeredSynergies) {
|
||||||
|
this(
|
||||||
|
seed,
|
||||||
|
strategyId,
|
||||||
|
waveReached,
|
||||||
|
latestBossKillSeconds,
|
||||||
|
purchases,
|
||||||
|
averageClearSeconds,
|
||||||
|
deaths,
|
||||||
|
damageTaken,
|
||||||
|
bonusDrops,
|
||||||
|
activeBonusPower,
|
||||||
|
survivability,
|
||||||
|
effectivePowerByAxis,
|
||||||
|
sourceKind,
|
||||||
|
firstBossKillSeconds,
|
||||||
|
shopPurchases,
|
||||||
|
abilityUses,
|
||||||
|
selectedDamageCards,
|
||||||
|
selectedFireRateCards,
|
||||||
|
peakVisibleBonusBalls,
|
||||||
|
effectiveDamage,
|
||||||
|
effectiveShotCooldownMs,
|
||||||
|
bonusSpawned,
|
||||||
|
bonusCollected,
|
||||||
|
activeBonusUptimeWaves,
|
||||||
|
triggeredSynergies,
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimulationReport(
|
||||||
|
long seed,
|
||||||
|
String strategyId,
|
||||||
|
int waveReached,
|
||||||
|
float latestBossKillSeconds,
|
||||||
|
int purchases,
|
||||||
|
float averageClearSeconds,
|
||||||
|
int deaths,
|
||||||
|
float damageTaken,
|
||||||
|
int bonusDrops,
|
||||||
|
float activeBonusPower,
|
||||||
|
float survivability,
|
||||||
|
Map<PowerAxis, Float> effectivePowerByAxis,
|
||||||
|
String sourceKind,
|
||||||
|
float firstBossKillSeconds,
|
||||||
|
int shopPurchases,
|
||||||
|
int abilityUses,
|
||||||
|
int selectedDamageCards,
|
||||||
|
int selectedFireRateCards,
|
||||||
|
int peakVisibleBonusBalls,
|
||||||
|
float effectiveDamage,
|
||||||
|
float effectiveShotCooldownMs,
|
||||||
|
int bonusSpawned,
|
||||||
|
int bonusCollected,
|
||||||
|
int activeBonusUptimeWaves,
|
||||||
|
int triggeredSynergies,
|
||||||
|
int moneyEarned,
|
||||||
|
int moneySpent) {
|
||||||
|
this.seed = seed;
|
||||||
|
this.sourceKind = sourceKind;
|
||||||
|
this.strategyId = strategyId;
|
||||||
|
this.waveReached = waveReached;
|
||||||
|
this.latestBossKillSeconds = latestBossKillSeconds;
|
||||||
|
this.purchases = purchases;
|
||||||
|
this.moneyEarned = moneyEarned;
|
||||||
|
this.moneySpent = moneySpent;
|
||||||
|
this.averageClearSeconds = averageClearSeconds;
|
||||||
|
this.deaths = deaths;
|
||||||
|
this.damageTaken = damageTaken;
|
||||||
|
this.bonusDrops = bonusDrops;
|
||||||
|
this.bonusSpawned = bonusSpawned;
|
||||||
|
this.bonusCollected = bonusCollected;
|
||||||
|
this.activeBonusPower = activeBonusPower;
|
||||||
|
this.activeBonusUptimeWaves = activeBonusUptimeWaves;
|
||||||
|
this.triggeredSynergies = triggeredSynergies;
|
||||||
|
this.survivability = survivability;
|
||||||
|
this.effectivePowerByAxis = new EnumMap<>(effectivePowerByAxis);
|
||||||
|
this.firstBossKillSeconds = firstBossKillSeconds;
|
||||||
|
this.shopPurchases = shopPurchases;
|
||||||
|
this.abilityUses = abilityUses;
|
||||||
|
this.selectedDamageCards = selectedDamageCards;
|
||||||
|
this.selectedFireRateCards = selectedFireRateCards;
|
||||||
|
this.peakVisibleBonusBalls = peakVisibleBonusBalls;
|
||||||
|
this.effectiveDamage = effectiveDamage;
|
||||||
|
this.effectiveShotCooldownMs = effectiveShotCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SimulationReport fromRoute(long seed, BalanceRouteReport route) {
|
||||||
|
EnumMap<PowerAxis, Float> axes = new EnumMap<>(route.effectivePowerByAxis());
|
||||||
|
axes.put(PowerAxis.DAMAGE, route.effectiveDamage());
|
||||||
|
axes.put(PowerAxis.CADENCE, 1000f / Math.max(1f, route.effectiveShotCooldownMs()));
|
||||||
|
return new SimulationReport(
|
||||||
|
seed,
|
||||||
|
route.strategyId(),
|
||||||
|
route.waveReached(),
|
||||||
|
route.firstBossKillSeconds(),
|
||||||
|
route.shopPurchases(),
|
||||||
|
0f,
|
||||||
|
route.waveReached() >= 100 ? 0 : 1,
|
||||||
|
0f,
|
||||||
|
route.peakVisibleBonusBalls(),
|
||||||
|
route.peakVisibleBonusBalls(),
|
||||||
|
Math.max(0.01f, route.waveReached() / 100f),
|
||||||
|
axes,
|
||||||
|
"route-simulation",
|
||||||
|
route.firstBossKillSeconds(),
|
||||||
|
route.shopPurchases(),
|
||||||
|
route.abilityUses(),
|
||||||
|
route.selectedDamageCards(),
|
||||||
|
route.selectedFireRateCards(),
|
||||||
|
route.peakVisibleBonusBalls(),
|
||||||
|
route.effectiveDamage(),
|
||||||
|
route.effectiveShotCooldownMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
public long seed() {
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sourceKind() {
|
||||||
|
return sourceKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String strategyId() {
|
||||||
|
return strategyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int waveReached() {
|
||||||
|
return waveReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float latestBossKillSeconds() {
|
||||||
|
return latestBossKillSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int purchases() {
|
||||||
|
return purchases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int moneyEarned() {
|
||||||
|
return moneyEarned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int moneySpent() {
|
||||||
|
return moneySpent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float averageClearSeconds() {
|
||||||
|
return averageClearSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int deaths() {
|
||||||
|
return deaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float damageTaken() {
|
||||||
|
return damageTaken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int bonusDrops() {
|
||||||
|
return bonusDrops;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int bonusSpawned() {
|
||||||
|
return bonusSpawned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int bonusCollected() {
|
||||||
|
return bonusCollected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float activeBonusPower() {
|
||||||
|
return activeBonusPower;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int activeBonusUptimeWaves() {
|
||||||
|
return activeBonusUptimeWaves;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int triggeredSynergies() {
|
||||||
|
return triggeredSynergies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float survivability() {
|
||||||
|
return survivability;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<PowerAxis, Float> effectivePowerByAxis() {
|
||||||
|
return new EnumMap<>(effectivePowerByAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float firstBossKillSeconds() {
|
||||||
|
return firstBossKillSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int shopPurchases() {
|
||||||
|
return shopPurchases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int abilityUses() {
|
||||||
|
return abilityUses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int selectedDamageCards() {
|
||||||
|
return selectedDamageCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int selectedFireRateCards() {
|
||||||
|
return selectedFireRateCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int peakVisibleBonusBalls() {
|
||||||
|
return peakVisibleBonusBalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float effectiveDamage() {
|
||||||
|
return effectiveDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float effectiveShotCooldownMs() {
|
||||||
|
return effectiveShotCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String summaryLine() {
|
||||||
|
return "seed="
|
||||||
|
+ seed
|
||||||
|
+ " source="
|
||||||
|
+ sourceKind
|
||||||
|
+ " strategy="
|
||||||
|
+ strategyId
|
||||||
|
+ " wave="
|
||||||
|
+ waveReached
|
||||||
|
+ " firstBoss="
|
||||||
|
+ firstBossKillSeconds
|
||||||
|
+ "s shop="
|
||||||
|
+ shopPurchases
|
||||||
|
+ " moneyEarned="
|
||||||
|
+ moneyEarned
|
||||||
|
+ " moneySpent="
|
||||||
|
+ moneySpent
|
||||||
|
+ " abilities="
|
||||||
|
+ abilityUses
|
||||||
|
+ " cards="
|
||||||
|
+ selectedDamageCards
|
||||||
|
+ "/"
|
||||||
|
+ selectedFireRateCards
|
||||||
|
+ " visibleBonus="
|
||||||
|
+ peakVisibleBonusBalls
|
||||||
|
+ " bonusDrops="
|
||||||
|
+ bonusDrops
|
||||||
|
+ " bonusSpawned="
|
||||||
|
+ bonusSpawned
|
||||||
|
+ " bonusCollected="
|
||||||
|
+ bonusCollected
|
||||||
|
+ " activeBonus="
|
||||||
|
+ activeBonusPower
|
||||||
|
+ " activeBonusUptime="
|
||||||
|
+ activeBonusUptimeWaves
|
||||||
|
+ " synergies="
|
||||||
|
+ triggeredSynergies
|
||||||
|
+ " damageTaken="
|
||||||
|
+ damageTaken
|
||||||
|
+ " damage="
|
||||||
|
+ effectiveDamage
|
||||||
|
+ " cooldownMs="
|
||||||
|
+ effectiveShotCooldownMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class StrategyProfile {
|
||||||
|
private final String id;
|
||||||
|
private final Map<PowerAxis, Float> axisWeights;
|
||||||
|
private final float metaRating;
|
||||||
|
|
||||||
|
private StrategyProfile(String id, Map<PowerAxis, Float> axisWeights, float metaRating) {
|
||||||
|
this.id = id;
|
||||||
|
this.axisWeights = new EnumMap<>(axisWeights);
|
||||||
|
this.metaRating = metaRating;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StrategyProfile of(String id, PowerAxis primary, PowerAxis secondary) {
|
||||||
|
EnumMap<PowerAxis, Float> weights = new EnumMap<>(PowerAxis.class);
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
weights.put(axis, 0.35f);
|
||||||
|
}
|
||||||
|
weights.put(primary, 1f);
|
||||||
|
weights.put(secondary, 0.75f);
|
||||||
|
return new StrategyProfile(id, weights, 8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StrategyProfile mixed() {
|
||||||
|
EnumMap<PowerAxis, Float> weights = new EnumMap<>(PowerAxis.class);
|
||||||
|
for (PowerAxis axis : PowerAxis.values()) {
|
||||||
|
weights.put(axis, 0.70f);
|
||||||
|
}
|
||||||
|
return new StrategyProfile("mixed", weights, 8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StrategyProfile withMetaTier(String tierId, float metaRating) {
|
||||||
|
return new StrategyProfile(id + ":" + tierId, axisWeights, metaRating);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float weight(PowerAxis axis) {
|
||||||
|
return axisWeights.get(axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float metaRating() {
|
||||||
|
return metaRating;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class UpgradeCard {
|
||||||
|
private final String id;
|
||||||
|
private final String family;
|
||||||
|
private final PowerAxis primaryAxis;
|
||||||
|
private final PowerAxis synergyAxis;
|
||||||
|
private final float rating;
|
||||||
|
private final int maxCopies;
|
||||||
|
private final UpgradeCardRarity rarity;
|
||||||
|
private final float offerWeight;
|
||||||
|
private final String[] tags;
|
||||||
|
|
||||||
|
public UpgradeCard(
|
||||||
|
String id,
|
||||||
|
String family,
|
||||||
|
PowerAxis primaryAxis,
|
||||||
|
PowerAxis synergyAxis,
|
||||||
|
float rating,
|
||||||
|
int maxCopies) {
|
||||||
|
this(
|
||||||
|
id,
|
||||||
|
family,
|
||||||
|
primaryAxis,
|
||||||
|
synergyAxis,
|
||||||
|
rating,
|
||||||
|
maxCopies,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
1f,
|
||||||
|
family);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpgradeCard(
|
||||||
|
String id,
|
||||||
|
String family,
|
||||||
|
PowerAxis primaryAxis,
|
||||||
|
PowerAxis synergyAxis,
|
||||||
|
float rating,
|
||||||
|
int maxCopies,
|
||||||
|
UpgradeCardRarity rarity,
|
||||||
|
float offerWeight,
|
||||||
|
String... tags) {
|
||||||
|
this.id = Objects.requireNonNull(id, "id");
|
||||||
|
this.family = Objects.requireNonNull(family, "family");
|
||||||
|
this.primaryAxis = Objects.requireNonNull(primaryAxis, "primaryAxis");
|
||||||
|
this.synergyAxis = synergyAxis;
|
||||||
|
this.rating = rating;
|
||||||
|
this.maxCopies = maxCopies;
|
||||||
|
this.rarity = Objects.requireNonNull(rarity, "rarity");
|
||||||
|
this.offerWeight = offerWeight;
|
||||||
|
this.tags = tags == null ? new String[0] : tags.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String family() {
|
||||||
|
return family;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PowerAxis primaryAxis() {
|
||||||
|
return primaryAxis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PowerAxis synergyAxis() {
|
||||||
|
return synergyAxis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float rating() {
|
||||||
|
return rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int maxCopies() {
|
||||||
|
return maxCopies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpgradeCardRarity rarity() {
|
||||||
|
return rarity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float offerWeight() {
|
||||||
|
return offerWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] tags() {
|
||||||
|
return tags.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
public enum UpgradeCardRarity {
|
||||||
|
COMMON,
|
||||||
|
UNCOMMON,
|
||||||
|
RARE
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package ru.project.tower.balance;
|
||||||
|
|
||||||
|
public final class UpgradeCardRegistry {
|
||||||
|
private static final UpgradeCard[] CARDS = {
|
||||||
|
card(
|
||||||
|
"focused_damage",
|
||||||
|
"attack",
|
||||||
|
PowerAxis.DAMAGE,
|
||||||
|
PowerAxis.CRIT,
|
||||||
|
5f,
|
||||||
|
8,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
1.2f,
|
||||||
|
"pure",
|
||||||
|
"damage"),
|
||||||
|
card(
|
||||||
|
"rupture_crit",
|
||||||
|
"attack",
|
||||||
|
PowerAxis.CRIT,
|
||||||
|
PowerAxis.DAMAGE,
|
||||||
|
4f,
|
||||||
|
7,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
1.1f,
|
||||||
|
"crit",
|
||||||
|
"burst"),
|
||||||
|
card(
|
||||||
|
"boss_breaker",
|
||||||
|
"attack",
|
||||||
|
PowerAxis.BOSSING,
|
||||||
|
PowerAxis.CRIT,
|
||||||
|
3.8f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.UNCOMMON,
|
||||||
|
0.85f,
|
||||||
|
"boss",
|
||||||
|
"single-target"),
|
||||||
|
card(
|
||||||
|
"execution_line",
|
||||||
|
"attack",
|
||||||
|
PowerAxis.DAMAGE,
|
||||||
|
PowerAxis.BOSSING,
|
||||||
|
3.6f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.UNCOMMON,
|
||||||
|
0.85f,
|
||||||
|
"execute",
|
||||||
|
"single-target"),
|
||||||
|
card(
|
||||||
|
"arc_burst",
|
||||||
|
"control",
|
||||||
|
PowerAxis.AOE,
|
||||||
|
PowerAxis.CONTROL,
|
||||||
|
4.5f,
|
||||||
|
7,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
1.0f,
|
||||||
|
"aoe",
|
||||||
|
"control"),
|
||||||
|
card(
|
||||||
|
"static_field",
|
||||||
|
"control",
|
||||||
|
PowerAxis.CONTROL,
|
||||||
|
PowerAxis.AOE,
|
||||||
|
4.5f,
|
||||||
|
7,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
1.0f,
|
||||||
|
"slow",
|
||||||
|
"aoe"),
|
||||||
|
card(
|
||||||
|
"gravity_lens",
|
||||||
|
"control",
|
||||||
|
PowerAxis.CONTROL,
|
||||||
|
PowerAxis.BOSSING,
|
||||||
|
3.5f,
|
||||||
|
5,
|
||||||
|
UpgradeCardRarity.RARE,
|
||||||
|
0.55f,
|
||||||
|
"pull",
|
||||||
|
"boss"),
|
||||||
|
card(
|
||||||
|
"barrier_cycle",
|
||||||
|
"defense",
|
||||||
|
PowerAxis.SHIELD,
|
||||||
|
PowerAxis.REGEN,
|
||||||
|
4f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
1.0f,
|
||||||
|
"shield",
|
||||||
|
"regen"),
|
||||||
|
card(
|
||||||
|
"repair_loop",
|
||||||
|
"defense",
|
||||||
|
PowerAxis.REGEN,
|
||||||
|
PowerAxis.SHIELD,
|
||||||
|
4f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
1.0f,
|
||||||
|
"regen",
|
||||||
|
"shield"),
|
||||||
|
card(
|
||||||
|
"plated_core",
|
||||||
|
"defense",
|
||||||
|
PowerAxis.ARMOR,
|
||||||
|
PowerAxis.HEALTH,
|
||||||
|
3.6f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.UNCOMMON,
|
||||||
|
0.85f,
|
||||||
|
"armor",
|
||||||
|
"health"),
|
||||||
|
card(
|
||||||
|
"overdrive_sustain",
|
||||||
|
"defense",
|
||||||
|
PowerAxis.HEALTH,
|
||||||
|
PowerAxis.CADENCE,
|
||||||
|
3.5f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.UNCOMMON,
|
||||||
|
0.75f,
|
||||||
|
"health",
|
||||||
|
"tempo"),
|
||||||
|
card(
|
||||||
|
"coin_compounder",
|
||||||
|
"utility",
|
||||||
|
PowerAxis.ECONOMY,
|
||||||
|
PowerAxis.UTILITY,
|
||||||
|
3.2f,
|
||||||
|
5,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
0.9f,
|
||||||
|
"money",
|
||||||
|
"shop"),
|
||||||
|
card(
|
||||||
|
"reroll_signal",
|
||||||
|
"utility",
|
||||||
|
PowerAxis.UTILITY,
|
||||||
|
PowerAxis.ECONOMY,
|
||||||
|
3.2f,
|
||||||
|
5,
|
||||||
|
UpgradeCardRarity.UNCOMMON,
|
||||||
|
0.75f,
|
||||||
|
"reroll",
|
||||||
|
"choice"),
|
||||||
|
card(
|
||||||
|
"bonus_magnet",
|
||||||
|
"utility",
|
||||||
|
PowerAxis.UTILITY,
|
||||||
|
PowerAxis.CADENCE,
|
||||||
|
3.0f,
|
||||||
|
5,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
0.9f,
|
||||||
|
"bonus",
|
||||||
|
"pickup"),
|
||||||
|
card(
|
||||||
|
"coil_accelerator",
|
||||||
|
"attack",
|
||||||
|
PowerAxis.CADENCE,
|
||||||
|
PowerAxis.DAMAGE,
|
||||||
|
3.5f,
|
||||||
|
7,
|
||||||
|
UpgradeCardRarity.COMMON,
|
||||||
|
1.05f,
|
||||||
|
"cadence",
|
||||||
|
"tempo"),
|
||||||
|
card(
|
||||||
|
"turret_directive",
|
||||||
|
"ability",
|
||||||
|
PowerAxis.ABILITY,
|
||||||
|
PowerAxis.AOE,
|
||||||
|
4f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.UNCOMMON,
|
||||||
|
0.8f,
|
||||||
|
"turret",
|
||||||
|
"aoe"),
|
||||||
|
card(
|
||||||
|
"blast_protocol",
|
||||||
|
"ability",
|
||||||
|
PowerAxis.ABILITY,
|
||||||
|
PowerAxis.DAMAGE,
|
||||||
|
3.6f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.UNCOMMON,
|
||||||
|
0.8f,
|
||||||
|
"explosion",
|
||||||
|
"damage"),
|
||||||
|
card(
|
||||||
|
"shield_script",
|
||||||
|
"ability",
|
||||||
|
PowerAxis.ABILITY,
|
||||||
|
PowerAxis.SHIELD,
|
||||||
|
3.4f,
|
||||||
|
6,
|
||||||
|
UpgradeCardRarity.UNCOMMON,
|
||||||
|
0.8f,
|
||||||
|
"shield",
|
||||||
|
"cooldown"),
|
||||||
|
card(
|
||||||
|
"volatile_bargain",
|
||||||
|
"risk",
|
||||||
|
PowerAxis.DAMAGE,
|
||||||
|
PowerAxis.HEALTH,
|
||||||
|
5.5f,
|
||||||
|
4,
|
||||||
|
UpgradeCardRarity.RARE,
|
||||||
|
0.45f,
|
||||||
|
"risk",
|
||||||
|
"damage"),
|
||||||
|
card(
|
||||||
|
"glass_cannon",
|
||||||
|
"risk",
|
||||||
|
PowerAxis.CRIT,
|
||||||
|
PowerAxis.ARMOR,
|
||||||
|
5.0f,
|
||||||
|
4,
|
||||||
|
UpgradeCardRarity.RARE,
|
||||||
|
0.45f,
|
||||||
|
"risk",
|
||||||
|
"crit")
|
||||||
|
};
|
||||||
|
|
||||||
|
private UpgradeCardRegistry() {}
|
||||||
|
|
||||||
|
public static UpgradeCard[] all() {
|
||||||
|
UpgradeCard[] copy = new UpgradeCard[CARDS.length];
|
||||||
|
System.arraycopy(CARDS, 0, copy, 0, CARDS.length);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpgradeCard card(
|
||||||
|
String id,
|
||||||
|
String family,
|
||||||
|
PowerAxis primaryAxis,
|
||||||
|
PowerAxis synergyAxis,
|
||||||
|
float rating,
|
||||||
|
int maxCopies,
|
||||||
|
UpgradeCardRarity rarity,
|
||||||
|
float offerWeight,
|
||||||
|
String... tags) {
|
||||||
|
return new UpgradeCard(
|
||||||
|
id, family, primaryAxis, synergyAxis, rating, maxCopies, rarity, offerWeight, tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package ru.project.tower.content;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import java.util.Objects;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.entities.circles.bosses.GlacialWardenData;
|
||||||
|
import ru.project.tower.entities.circles.bosses.IllusionMasterData;
|
||||||
|
import ru.project.tower.entities.circles.bosses.JuggernautData;
|
||||||
|
import ru.project.tower.entities.circles.bosses.ShadowWeaverData;
|
||||||
|
import ru.project.tower.entities.circles.bosses.SwarmQueenData;
|
||||||
|
import ru.project.tower.entities.circles.bosses.VolatileReactorData;
|
||||||
|
import ru.project.tower.support.BossEffectsHost;
|
||||||
|
|
||||||
|
|
||||||
|
public final class BossContentRegistry {
|
||||||
|
private static final BossSpec[] BOSSES = {
|
||||||
|
boss(
|
||||||
|
"SwarmQueen",
|
||||||
|
SwarmQueenData.class,
|
||||||
|
SwarmQueenData.getStaticRadius(),
|
||||||
|
SwarmQueenData.getStaticSpeed(),
|
||||||
|
(circle, velocity, host) -> new SwarmQueenData(circle, velocity, host)),
|
||||||
|
boss(
|
||||||
|
"Juggernaut",
|
||||||
|
JuggernautData.class,
|
||||||
|
JuggernautData.getStaticRadius(),
|
||||||
|
JuggernautData.getStaticSpeed(),
|
||||||
|
(circle, velocity, host) -> new JuggernautData(circle, velocity)),
|
||||||
|
boss(
|
||||||
|
"ShadowWeaver",
|
||||||
|
ShadowWeaverData.class,
|
||||||
|
ShadowWeaverData.getStaticRadius(),
|
||||||
|
ShadowWeaverData.getStaticSpeed(),
|
||||||
|
(circle, velocity, host) -> new ShadowWeaverData(circle, velocity)),
|
||||||
|
boss(
|
||||||
|
"VolatileReactor",
|
||||||
|
VolatileReactorData.class,
|
||||||
|
VolatileReactorData.getStaticRadius(),
|
||||||
|
VolatileReactorData.getStaticSpeed(),
|
||||||
|
(circle, velocity, host) -> new VolatileReactorData(circle, velocity)),
|
||||||
|
boss(
|
||||||
|
"GlacialWarden",
|
||||||
|
GlacialWardenData.class,
|
||||||
|
GlacialWardenData.getStaticRadius(),
|
||||||
|
GlacialWardenData.getStaticSpeed(),
|
||||||
|
(circle, velocity, host) -> new GlacialWardenData(circle, velocity)),
|
||||||
|
boss(
|
||||||
|
"IllusionMaster",
|
||||||
|
IllusionMasterData.class,
|
||||||
|
IllusionMasterData.getStaticRadius(),
|
||||||
|
IllusionMasterData.getStaticSpeed(),
|
||||||
|
(circle, velocity, host) -> new IllusionMasterData(circle, velocity))
|
||||||
|
};
|
||||||
|
|
||||||
|
private BossContentRegistry() {}
|
||||||
|
|
||||||
|
public static BossSpec[] all() {
|
||||||
|
return BOSSES.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] ids() {
|
||||||
|
String[] ids = new String[BOSSES.length];
|
||||||
|
for (int i = 0; i < BOSSES.length; i++) {
|
||||||
|
ids[i] = BOSSES[i].id();
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BossSpec findById(String id) {
|
||||||
|
for (BossSpec boss : BOSSES) {
|
||||||
|
if (boss.id().equals(id)) {
|
||||||
|
return boss;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBoss(BaseCircleData enemy) {
|
||||||
|
if (enemy == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Class<?> enemyClass = enemy.getClass();
|
||||||
|
for (BossSpec boss : BOSSES) {
|
||||||
|
if (boss.type().isAssignableFrom(enemyClass)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BossSpec boss(
|
||||||
|
String id,
|
||||||
|
Class<? extends BaseCircleData> type,
|
||||||
|
float radius,
|
||||||
|
float speed,
|
||||||
|
BossFactory factory) {
|
||||||
|
return new BossSpec(id, type, radius, speed, factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface BossFactory {
|
||||||
|
BaseCircleData create(Circle circle, Vector2 velocity, BossEffectsHost host);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class BossSpec {
|
||||||
|
private final String id;
|
||||||
|
private final Class<? extends BaseCircleData> type;
|
||||||
|
private final float radius;
|
||||||
|
private final float speed;
|
||||||
|
private final BossFactory factory;
|
||||||
|
|
||||||
|
private BossSpec(
|
||||||
|
String id,
|
||||||
|
Class<? extends BaseCircleData> type,
|
||||||
|
float radius,
|
||||||
|
float speed,
|
||||||
|
BossFactory factory) {
|
||||||
|
this.id = Objects.requireNonNull(id, "id");
|
||||||
|
this.type = Objects.requireNonNull(type, "type");
|
||||||
|
this.radius = radius;
|
||||||
|
this.speed = speed;
|
||||||
|
this.factory = Objects.requireNonNull(factory, "factory");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<? extends BaseCircleData> type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float radius() {
|
||||||
|
return radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float speed() {
|
||||||
|
return speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseCircleData create(Circle circle, Vector2 velocity, BossEffectsHost host) {
|
||||||
|
return factory.create(circle, velocity, host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package ru.project.tower.entities.bullets;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Pool;
|
||||||
|
|
||||||
|
public class BulletData implements Pool.Poolable {
|
||||||
|
public Circle bullet;
|
||||||
|
public Vector2 velocity;
|
||||||
|
public int damage;
|
||||||
|
private boolean isEnemyBullet;
|
||||||
|
private boolean isCrit = false;
|
||||||
|
private Color color;
|
||||||
|
|
||||||
|
|
||||||
|
public int piercesLeft = 0;
|
||||||
|
public boolean homing = false;
|
||||||
|
public float aoeRadius = 0f;
|
||||||
|
public float trailParticleSpawnBudget = 0f;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public BulletData() {
|
||||||
|
this.bullet = new Circle(0f, 0f, 1f);
|
||||||
|
this.velocity = new Vector2(0f, 0f);
|
||||||
|
this.damage = 1;
|
||||||
|
this.isEnemyBullet = false;
|
||||||
|
this.color = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletData(Circle bullet, Vector2 velocity, int damage) {
|
||||||
|
this.bullet = bullet;
|
||||||
|
this.velocity = velocity;
|
||||||
|
this.damage = damage;
|
||||||
|
this.isEnemyBullet = false;
|
||||||
|
this.color = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletData(Circle bullet, Vector2 velocity, boolean isEnemyBullet) {
|
||||||
|
this.bullet = bullet;
|
||||||
|
this.velocity = velocity;
|
||||||
|
this.damage = isEnemyBullet ? 10 : 1;
|
||||||
|
this.isEnemyBullet = isEnemyBullet;
|
||||||
|
this.color = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void configureAsPlayerBullet(
|
||||||
|
float x, float y, float radius, float vx, float vy, int damage) {
|
||||||
|
bullet.x = x;
|
||||||
|
bullet.y = y;
|
||||||
|
bullet.radius = radius;
|
||||||
|
velocity.x = vx;
|
||||||
|
velocity.y = vy;
|
||||||
|
this.damage = damage;
|
||||||
|
this.isEnemyBullet = false;
|
||||||
|
this.isCrit = false;
|
||||||
|
this.color = null;
|
||||||
|
this.piercesLeft = 0;
|
||||||
|
this.homing = false;
|
||||||
|
this.aoeRadius = 0f;
|
||||||
|
this.trailParticleSpawnBudget = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void configureAsEnemyBullet(
|
||||||
|
float x, float y, float radius, float vx, float vy, int damage) {
|
||||||
|
bullet.x = x;
|
||||||
|
bullet.y = y;
|
||||||
|
bullet.radius = radius;
|
||||||
|
velocity.x = vx;
|
||||||
|
velocity.y = vy;
|
||||||
|
this.damage = damage;
|
||||||
|
this.isEnemyBullet = true;
|
||||||
|
this.isCrit = false;
|
||||||
|
this.color = null;
|
||||||
|
this.piercesLeft = 0;
|
||||||
|
this.homing = false;
|
||||||
|
this.aoeRadius = 0f;
|
||||||
|
this.trailParticleSpawnBudget = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
|
||||||
|
bullet.x = 0f;
|
||||||
|
bullet.y = 0f;
|
||||||
|
bullet.radius = 1f;
|
||||||
|
velocity.x = 0f;
|
||||||
|
velocity.y = 0f;
|
||||||
|
damage = 1;
|
||||||
|
isEnemyBullet = false;
|
||||||
|
isCrit = false;
|
||||||
|
color = null;
|
||||||
|
piercesLeft = 0;
|
||||||
|
homing = false;
|
||||||
|
aoeRadius = 0f;
|
||||||
|
trailParticleSpawnBudget = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getX() {
|
||||||
|
return bullet.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getY() {
|
||||||
|
return bullet.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRadius() {
|
||||||
|
return bullet.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void move(float delta) {
|
||||||
|
bullet.x += velocity.x * delta;
|
||||||
|
bullet.y += velocity.y * delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Circle getCircle() {
|
||||||
|
return bullet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnemyBullet() {
|
||||||
|
return isEnemyBullet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(Color color) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCrit(boolean isCrit) {
|
||||||
|
this.isCrit = isCrit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCrit() {
|
||||||
|
return isCrit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package ru.project.tower.entities.bullets;
|
||||||
|
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public interface BulletEffectsSink {
|
||||||
|
|
||||||
|
|
||||||
|
void onEnemyHit(BaseCircleData enemy, int damage, boolean isCrit);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void onEnemyKilled(BaseCircleData enemy);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void onEnemyBulletHitPlayer(BulletData bullet);
|
||||||
|
|
||||||
|
|
||||||
|
void onCrit(BaseCircleData enemy, int damage);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package ru.project.tower.entities.bullets;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public final class BulletMultipliers {
|
||||||
|
|
||||||
|
public final float speedMultiplier;
|
||||||
|
public final float damageMultiplier;
|
||||||
|
public final boolean isCrit;
|
||||||
|
public final float critDamageMultiplier;
|
||||||
|
|
||||||
|
public BulletMultipliers(
|
||||||
|
float speedMultiplier,
|
||||||
|
float damageMultiplier,
|
||||||
|
boolean isCrit,
|
||||||
|
float critDamageMultiplier) {
|
||||||
|
this.speedMultiplier = speedMultiplier;
|
||||||
|
this.damageMultiplier = damageMultiplier;
|
||||||
|
this.isCrit = isCrit;
|
||||||
|
this.critDamageMultiplier = critDamageMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BulletMultipliers defaults() {
|
||||||
|
return new BulletMultipliers(1f, 1f, false, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
package ru.project.tower.entities.bullets;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Rectangle;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import com.badlogic.gdx.utils.Pools;
|
||||||
|
import java.util.Objects;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.support.RangeChecks;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class BulletSystem {
|
||||||
|
|
||||||
|
private static final int DEFAULT_BASE_DAMAGE = 1;
|
||||||
|
private static final float DEFAULT_BASE_SPEED = 300f;
|
||||||
|
private static final int TICK_MISS = 0;
|
||||||
|
private static final int TICK_HIT = 1;
|
||||||
|
private static final int TICK_ENEMY_INDEX_DIRTY = 2;
|
||||||
|
|
||||||
|
private int bulletDamage = DEFAULT_BASE_DAMAGE;
|
||||||
|
private float bulletSpeed = DEFAULT_BASE_SPEED;
|
||||||
|
|
||||||
|
private int runDamageBonus = 0;
|
||||||
|
private int runPierceBonus = 0;
|
||||||
|
private float runAoeBonus = 0f;
|
||||||
|
|
||||||
|
private final Array<BulletData> bullets = new Array<>();
|
||||||
|
private final BulletTalentProvider talentProvider;
|
||||||
|
private final EnemySpatialIndex enemySpatialIndex = new EnemySpatialIndex();
|
||||||
|
private int lastPrimaryCandidateChecksForTests;
|
||||||
|
private float primaryHitTime;
|
||||||
|
|
||||||
|
public BulletSystem(BulletTalentProvider talentProvider) {
|
||||||
|
this.talentProvider = Objects.requireNonNull(talentProvider, "talentProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<BulletData> getBullets() {
|
||||||
|
return bullets;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getLastPrimaryCandidateChecksForTests() {
|
||||||
|
return lastPrimaryCandidateChecksForTests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBaseDamage() {
|
||||||
|
return bulletDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getBaseSpeed() {
|
||||||
|
return bulletSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRunDamageBonus() {
|
||||||
|
return runDamageBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRunPierceBonus() {
|
||||||
|
return runPierceBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRunAoeBonus() {
|
||||||
|
return runAoeBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void fireBullet(
|
||||||
|
float fromX,
|
||||||
|
float fromY,
|
||||||
|
float angleRad,
|
||||||
|
float bulletRadius,
|
||||||
|
float gameScale,
|
||||||
|
BulletMultipliers mult) {
|
||||||
|
fireBullet(
|
||||||
|
fromX,
|
||||||
|
fromY,
|
||||||
|
angleRad,
|
||||||
|
bulletRadius,
|
||||||
|
gameScale,
|
||||||
|
mult.speedMultiplier,
|
||||||
|
mult.damageMultiplier,
|
||||||
|
mult.isCrit,
|
||||||
|
mult.critDamageMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fireBullet(
|
||||||
|
float fromX,
|
||||||
|
float fromY,
|
||||||
|
float angleRad,
|
||||||
|
float bulletRadius,
|
||||||
|
float gameScale,
|
||||||
|
float speedMultiplier,
|
||||||
|
float damageMultiplier,
|
||||||
|
boolean isCrit,
|
||||||
|
float critDamageMultiplier) {
|
||||||
|
float effectiveSpeed = bulletSpeed * speedMultiplier * gameScale;
|
||||||
|
int baseDmg = bulletDamage + runDamageBonus;
|
||||||
|
int damage = (int) (baseDmg * damageMultiplier);
|
||||||
|
if (isCrit) {
|
||||||
|
damage = (int) (damage * critDamageMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
float vx = (float) Math.cos(angleRad) * effectiveSpeed;
|
||||||
|
float vy = (float) Math.sin(angleRad) * effectiveSpeed;
|
||||||
|
|
||||||
|
BulletData bullet = Pools.obtain(BulletData.class);
|
||||||
|
bullet.configureAsPlayerBullet(fromX, fromY, bulletRadius, vx, vy, damage);
|
||||||
|
bullet.setCrit(isCrit);
|
||||||
|
bullet.piercesLeft = talentProvider.pierceLevel() + runPierceBonus;
|
||||||
|
bullet.aoeRadius = talentProvider.aoeLevel() * 30f + runAoeBonus;
|
||||||
|
|
||||||
|
bullets.add(bullet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fireTurretShot(
|
||||||
|
float fromX,
|
||||||
|
float fromY,
|
||||||
|
float angleRad,
|
||||||
|
float bulletRadius,
|
||||||
|
float gameScale,
|
||||||
|
int damage) {
|
||||||
|
float speed = bulletSpeed * gameScale;
|
||||||
|
float vx = (float) Math.cos(angleRad) * speed;
|
||||||
|
float vy = (float) Math.sin(angleRad) * speed;
|
||||||
|
BulletData bullet = Pools.obtain(BulletData.class);
|
||||||
|
bullet.configureAsPlayerBullet(fromX, fromY, bulletRadius, vx, vy, damage);
|
||||||
|
bullets.add(bullet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addEnemyBullet(BulletData bullet) {
|
||||||
|
bullets.add(bullet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fireEnemyBullet(
|
||||||
|
float fromX, float fromY, float bulletRadius, float vx, float vy, int damage) {
|
||||||
|
BulletData bullet = Pools.obtain(BulletData.class);
|
||||||
|
bullet.configureAsEnemyBullet(fromX, fromY, bulletRadius, vx, vy, damage);
|
||||||
|
bullets.add(bullet);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void tick(
|
||||||
|
float delta,
|
||||||
|
Array<BaseCircleData> enemies,
|
||||||
|
Rectangle playerSquare,
|
||||||
|
BulletEffectsSink sink) {
|
||||||
|
lastPrimaryCandidateChecksForTests = 0;
|
||||||
|
boolean enemyIndexDirty = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < bullets.size; i++) {
|
||||||
|
BulletData bulletData = bullets.get(i);
|
||||||
|
float px = bulletData.bullet.x;
|
||||||
|
float py = bulletData.bullet.y;
|
||||||
|
float mx = bulletData.velocity.x * delta;
|
||||||
|
float my = bulletData.velocity.y * delta;
|
||||||
|
int tickResult;
|
||||||
|
|
||||||
|
if (bulletData.isEnemyBullet()) {
|
||||||
|
tickResult =
|
||||||
|
tickEnemyBullet(bulletData, mx, my, playerSquare, sink)
|
||||||
|
? TICK_HIT
|
||||||
|
: TICK_MISS;
|
||||||
|
} else {
|
||||||
|
if (enemyIndexDirty || enemySpatialIndex.enemyCount() != enemies.size) {
|
||||||
|
enemySpatialIndex.build(enemies);
|
||||||
|
enemyIndexDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tickResult = tickPlayerBullet(bulletData, px, py, mx, my, enemies, sink);
|
||||||
|
if ((tickResult & TICK_ENEMY_INDEX_DIRTY) != 0) {
|
||||||
|
enemyIndexDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((tickResult & TICK_HIT) != 0 && finishBulletHit(bulletData, i)) {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tickEnemyBullet(
|
||||||
|
BulletData bulletData,
|
||||||
|
float moveX,
|
||||||
|
float moveY,
|
||||||
|
Rectangle playerSquare,
|
||||||
|
BulletEffectsSink sink) {
|
||||||
|
bulletData.bullet.x += moveX;
|
||||||
|
bulletData.bullet.y += moveY;
|
||||||
|
|
||||||
|
float squareCenterX = playerSquare.x + playerSquare.width / 2f;
|
||||||
|
float squareCenterY = playerSquare.y + playerSquare.height / 2f;
|
||||||
|
float collideAt =
|
||||||
|
bulletData.bullet.radius + Math.min(playerSquare.width, playerSquare.height) / 2f;
|
||||||
|
|
||||||
|
if (RangeChecks.withinRadiusExclusive(
|
||||||
|
bulletData.bullet.x,
|
||||||
|
bulletData.bullet.y,
|
||||||
|
squareCenterX,
|
||||||
|
squareCenterY,
|
||||||
|
collideAt)) {
|
||||||
|
sink.onEnemyBulletHitPlayer(bulletData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int tickPlayerBullet(
|
||||||
|
BulletData bulletData,
|
||||||
|
float previousX,
|
||||||
|
float previousY,
|
||||||
|
float moveX,
|
||||||
|
float moveY,
|
||||||
|
Array<BaseCircleData> enemies,
|
||||||
|
BulletEffectsSink sink) {
|
||||||
|
int primaryIndex =
|
||||||
|
findPrimaryHitIndex(bulletData, previousX, previousY, moveX, moveY, enemies);
|
||||||
|
if (primaryIndex < 0) {
|
||||||
|
return finishPlayerBulletMiss(bulletData, previousX, previousY, moveX, moveY);
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseCircleData primary = enemies.get(primaryIndex);
|
||||||
|
movePlayerBulletToPrimaryHit(bulletData, previousX, previousY, moveX, moveY);
|
||||||
|
return applyPrimaryHit(bulletData, primary, enemies, sink);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findPrimaryHitIndex(
|
||||||
|
BulletData bulletData,
|
||||||
|
float previousX,
|
||||||
|
float previousY,
|
||||||
|
float moveX,
|
||||||
|
float moveY,
|
||||||
|
Array<BaseCircleData> enemies) {
|
||||||
|
int earliestIndex = -1;
|
||||||
|
float earliestTime = Float.POSITIVE_INFINITY;
|
||||||
|
float a = moveX * moveX + moveY * moveY;
|
||||||
|
int candidateCount =
|
||||||
|
queryPrimaryHitCandidates(bulletData, previousX, previousY, moveX, moveY);
|
||||||
|
|
||||||
|
for (int candidate = 0; candidate < candidateCount; candidate++) {
|
||||||
|
int j = enemySpatialIndex.candidateAt(candidate);
|
||||||
|
BaseCircleData enemy = enemies.get(j);
|
||||||
|
lastPrimaryCandidateChecksForTests++;
|
||||||
|
float t =
|
||||||
|
primaryCollisionTime(bulletData, enemy, previousX, previousY, moveX, moveY, a);
|
||||||
|
if (t == Float.POSITIVE_INFINITY) continue;
|
||||||
|
|
||||||
|
if (isEarlierPrimaryHit(t, earliestTime, j, earliestIndex)) {
|
||||||
|
earliestTime = t;
|
||||||
|
earliestIndex = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryHitTime = earliestTime;
|
||||||
|
return earliestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int queryPrimaryHitCandidates(
|
||||||
|
BulletData bulletData, float previousX, float previousY, float moveX, float moveY) {
|
||||||
|
float sweepPadding = bulletData.bullet.radius + enemySpatialIndex.maxEnemyRadius();
|
||||||
|
return enemySpatialIndex.queryRect(
|
||||||
|
Math.min(previousX, previousX + moveX) - sweepPadding,
|
||||||
|
Math.min(previousY, previousY + moveY) - sweepPadding,
|
||||||
|
Math.max(previousX, previousX + moveX) + sweepPadding,
|
||||||
|
Math.max(previousY, previousY + moveY) + sweepPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float primaryCollisionTime(
|
||||||
|
BulletData bulletData,
|
||||||
|
BaseCircleData enemy,
|
||||||
|
float previousX,
|
||||||
|
float previousY,
|
||||||
|
float moveX,
|
||||||
|
float moveY,
|
||||||
|
float a) {
|
||||||
|
float radiusSum = bulletData.bullet.radius + enemy.circle.radius;
|
||||||
|
float dx = previousX - enemy.circle.x;
|
||||||
|
float dy = previousY - enemy.circle.y;
|
||||||
|
float c = dx * dx + dy * dy - radiusSum * radiusSum;
|
||||||
|
|
||||||
|
if (c <= 0f) return 0f;
|
||||||
|
if (a < 1e-6f) return Float.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
float b = 2f * (dx * moveX + dy * moveY);
|
||||||
|
float disc = b * b - 4f * a * c;
|
||||||
|
if (disc < 0f) return Float.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
float t = (-b - (float) Math.sqrt(disc)) / (2f * a);
|
||||||
|
if (t < 0f || t > 1f) return Float.POSITIVE_INFINITY;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEarlierPrimaryHit(float t, float earliestTime, int j, int earliestIndex) {
|
||||||
|
return t < earliestTime
|
||||||
|
|| (Float.compare(t, earliestTime) == 0
|
||||||
|
&& (earliestIndex < 0 || j < earliestIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int finishPlayerBulletMiss(
|
||||||
|
BulletData bulletData, float previousX, float previousY, float moveX, float moveY) {
|
||||||
|
bulletData.bullet.x = previousX + moveX;
|
||||||
|
bulletData.bullet.y = previousY + moveY;
|
||||||
|
return TICK_MISS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void movePlayerBulletToPrimaryHit(
|
||||||
|
BulletData bulletData, float previousX, float previousY, float moveX, float moveY) {
|
||||||
|
bulletData.bullet.x = previousX + primaryHitTime * moveX;
|
||||||
|
bulletData.bullet.y = previousY + primaryHitTime * moveY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int applyPrimaryHit(
|
||||||
|
BulletData bulletData,
|
||||||
|
BaseCircleData primary,
|
||||||
|
Array<BaseCircleData> enemies,
|
||||||
|
BulletEffectsSink sink) {
|
||||||
|
|
||||||
|
applyPlayerDamage(primary, bulletData.damage, bulletData.isCrit(), sink);
|
||||||
|
|
||||||
|
if (bulletData.isCrit()) {
|
||||||
|
sink.onCrit(primary, bulletData.damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = TICK_HIT;
|
||||||
|
if (bulletData.aoeRadius > 0f) {
|
||||||
|
result |= applyAoeDamage(bulletData, primary, enemies, sink);
|
||||||
|
}
|
||||||
|
if (primary.health <= 0) {
|
||||||
|
sink.onEnemyKilled(primary);
|
||||||
|
result |= TICK_ENEMY_INDEX_DIRTY;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int applyAoeDamage(
|
||||||
|
BulletData bulletData,
|
||||||
|
BaseCircleData primary,
|
||||||
|
Array<BaseCircleData> enemies,
|
||||||
|
BulletEffectsSink sink) {
|
||||||
|
float bx = bulletData.bullet.x;
|
||||||
|
float by = bulletData.bullet.y;
|
||||||
|
float aoeRadius2 = RangeChecks.radiusSquared(bulletData.aoeRadius);
|
||||||
|
int aoeCandidateCount = enemySpatialIndex.queryRadius(bx, by, bulletData.aoeRadius);
|
||||||
|
enemySpatialIndex.sortCandidatesDescending(aoeCandidateCount);
|
||||||
|
int result = 0;
|
||||||
|
for (int candidate = 0; candidate < aoeCandidateCount; candidate++) {
|
||||||
|
int k = enemySpatialIndex.candidateAt(candidate);
|
||||||
|
BaseCircleData other = enemies.get(k);
|
||||||
|
if (other == primary) continue;
|
||||||
|
if (RangeChecks.distanceSquared(bx, by, other.circle.x, other.circle.y) <= aoeRadius2) {
|
||||||
|
applyPlayerDamage(other, bulletData.damage, false, sink);
|
||||||
|
if (other.health <= 0) {
|
||||||
|
sink.onEnemyKilled(other);
|
||||||
|
result |= TICK_ENEMY_INDEX_DIRTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPlayerDamage(
|
||||||
|
BaseCircleData enemy, int damage, boolean crit, BulletEffectsSink sink) {
|
||||||
|
enemy.health -= damage;
|
||||||
|
sink.onEnemyHit(enemy, damage, crit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean finishBulletHit(BulletData bulletData, int bulletIndex) {
|
||||||
|
if (bulletData.piercesLeft > 0 && !bulletData.isEnemyBullet()) {
|
||||||
|
bulletData.piercesLeft--;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pools.free(bullets.removeIndex(bulletIndex));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public int getEffectiveDamage(float externalDamageMultiplier) {
|
||||||
|
int base = bulletDamage + runDamageBonus;
|
||||||
|
return (int) (base * externalDamageMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getEffectiveSpeed(float externalSpeedMultiplier) {
|
||||||
|
return bulletSpeed * externalSpeedMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void increaseBaseDamage(int delta) {
|
||||||
|
bulletDamage += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void increaseBaseSpeed(float delta) {
|
||||||
|
bulletSpeed += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRunDamageBonus(int delta) {
|
||||||
|
runDamageBonus += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRunPierceBonus(int delta) {
|
||||||
|
runPierceBonus += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRunAoeBonus(float delta) {
|
||||||
|
runAoeBonus += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void updateRadius(float newRadius) {
|
||||||
|
for (int i = 0; i < bullets.size; i++) {
|
||||||
|
bullets.get(i).bullet.radius = newRadius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
for (int i = 0; i < bullets.size; i++) {
|
||||||
|
Pools.free(bullets.get(i));
|
||||||
|
}
|
||||||
|
bullets.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetToBaseline() {
|
||||||
|
bulletDamage = DEFAULT_BASE_DAMAGE;
|
||||||
|
bulletSpeed = DEFAULT_BASE_SPEED;
|
||||||
|
runDamageBonus = 0;
|
||||||
|
runPierceBonus = 0;
|
||||||
|
runAoeBonus = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyTalentBaseStats() {
|
||||||
|
bulletDamage += (int) talentProvider.damageBonus();
|
||||||
|
bulletSpeed += talentProvider.speedBonus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startNewRun() {
|
||||||
|
clear();
|
||||||
|
resetToBaseline();
|
||||||
|
applyTalentBaseStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package ru.project.tower.entities.bullets;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import ru.project.tower.talents.TalentData;
|
||||||
|
import ru.project.tower.talents.TalentTree;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public interface BulletTalentProvider {
|
||||||
|
int pierceLevel();
|
||||||
|
|
||||||
|
int aoeLevel();
|
||||||
|
|
||||||
|
float damageBonus();
|
||||||
|
|
||||||
|
float speedBonus();
|
||||||
|
|
||||||
|
static BulletTalentProvider none() {
|
||||||
|
return new BulletTalentProvider() {
|
||||||
|
@Override
|
||||||
|
public int pierceLevel() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int aoeLevel() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float damageBonus() {
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float speedBonus() {
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static BulletTalentProvider fromTalentTree(TalentTree talentTree) {
|
||||||
|
Objects.requireNonNull(talentTree, "talentTree");
|
||||||
|
return new BulletTalentProvider() {
|
||||||
|
@Override
|
||||||
|
public int pierceLevel() {
|
||||||
|
return talentTree.getCurrentLevel("keystone_pierce");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int aoeLevel() {
|
||||||
|
return talentTree.getCurrentLevel("keystone_aoe");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float damageBonus() {
|
||||||
|
return talentTree.getStatBonus(TalentData.TalentType.DAMAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float speedBonus() {
|
||||||
|
return talentTree.getStatBonus(TalentData.TalentType.SPEED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package ru.project.tower.entities.bullets;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.systems.CircleSpatialGrid;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
final class EnemySpatialIndex {
|
||||||
|
private final CircleSpatialGrid grid = new CircleSpatialGrid();
|
||||||
|
|
||||||
|
void build(Array<BaseCircleData> enemies) {
|
||||||
|
grid.build(enemies);
|
||||||
|
}
|
||||||
|
|
||||||
|
int enemyCount() {
|
||||||
|
return grid.itemCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
float maxEnemyRadius() {
|
||||||
|
return grid.maxRadius();
|
||||||
|
}
|
||||||
|
|
||||||
|
int queryRect(float minX, float minY, float maxX, float maxY) {
|
||||||
|
return grid.queryRect(minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
int queryRadius(float x, float y, float radius) {
|
||||||
|
return grid.queryRadius(x, y, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortCandidatesDescending(int count) {
|
||||||
|
grid.sortCandidatesDescending(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
int candidateAt(int index) {
|
||||||
|
return grid.candidateAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Pool;
|
||||||
|
import com.badlogic.gdx.utils.Pools;
|
||||||
|
import com.badlogic.gdx.utils.ReflectionPool;
|
||||||
|
|
||||||
|
public abstract class BaseCircleData implements Pool.Poolable {
|
||||||
|
private static final int SPAWN_POOL_INITIAL_CAPACITY = 16;
|
||||||
|
private static final int SPAWN_POOL_MAX_CAPACITY = 4096;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Pools.set(
|
||||||
|
BasicCircleData.class,
|
||||||
|
new ReflectionPool<>(
|
||||||
|
BasicCircleData.class,
|
||||||
|
SPAWN_POOL_INITIAL_CAPACITY,
|
||||||
|
SPAWN_POOL_MAX_CAPACITY));
|
||||||
|
Pools.set(
|
||||||
|
FastCircleData.class,
|
||||||
|
new ReflectionPool<>(
|
||||||
|
FastCircleData.class,
|
||||||
|
SPAWN_POOL_INITIAL_CAPACITY,
|
||||||
|
SPAWN_POOL_MAX_CAPACITY));
|
||||||
|
Pools.set(
|
||||||
|
StrongCircleData.class,
|
||||||
|
new ReflectionPool<>(
|
||||||
|
StrongCircleData.class,
|
||||||
|
SPAWN_POOL_INITIAL_CAPACITY,
|
||||||
|
SPAWN_POOL_MAX_CAPACITY));
|
||||||
|
Pools.set(
|
||||||
|
SplitterCircleData.class,
|
||||||
|
new ReflectionPool<>(
|
||||||
|
SplitterCircleData.class,
|
||||||
|
SPAWN_POOL_INITIAL_CAPACITY,
|
||||||
|
SPAWN_POOL_MAX_CAPACITY));
|
||||||
|
Pools.set(
|
||||||
|
SpawnerCircleData.class,
|
||||||
|
new ReflectionPool<>(
|
||||||
|
SpawnerCircleData.class,
|
||||||
|
SPAWN_POOL_INITIAL_CAPACITY,
|
||||||
|
SPAWN_POOL_MAX_CAPACITY));
|
||||||
|
Pools.set(
|
||||||
|
HealerCircleData.class,
|
||||||
|
new ReflectionPool<>(
|
||||||
|
HealerCircleData.class,
|
||||||
|
SPAWN_POOL_INITIAL_CAPACITY,
|
||||||
|
SPAWN_POOL_MAX_CAPACITY));
|
||||||
|
Pools.set(
|
||||||
|
DasherCircleData.class,
|
||||||
|
new ReflectionPool<>(
|
||||||
|
DasherCircleData.class,
|
||||||
|
SPAWN_POOL_INITIAL_CAPACITY,
|
||||||
|
SPAWN_POOL_MAX_CAPACITY));
|
||||||
|
Pools.set(
|
||||||
|
SniperCircleData.class,
|
||||||
|
new ReflectionPool<>(
|
||||||
|
SniperCircleData.class,
|
||||||
|
SPAWN_POOL_INITIAL_CAPACITY,
|
||||||
|
SPAWN_POOL_MAX_CAPACITY));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Circle circle;
|
||||||
|
public Vector2 velocity;
|
||||||
|
public int health;
|
||||||
|
public int damage;
|
||||||
|
private boolean frozen = false;
|
||||||
|
private boolean elite = false;
|
||||||
|
|
||||||
|
protected BaseCircleData() {
|
||||||
|
this(new Circle(0f, 0f, 0f), new Vector2(0f, 0f), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseCircleData(Circle circle, Vector2 velocity, int health, int damage) {
|
||||||
|
this.circle = circle;
|
||||||
|
this.velocity = velocity;
|
||||||
|
this.health = health;
|
||||||
|
this.damage = damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void configure(
|
||||||
|
float x, float y, float radius, float vx, float vy, int health, int damage) {
|
||||||
|
circle.x = x;
|
||||||
|
circle.y = y;
|
||||||
|
circle.radius = radius;
|
||||||
|
velocity.x = vx;
|
||||||
|
velocity.y = vy;
|
||||||
|
this.health = health;
|
||||||
|
this.damage = damage;
|
||||||
|
frozen = false;
|
||||||
|
elite = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
configure(0f, 0f, 0f, 0f, 0f, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void freeIfSpawnPooled(BaseCircleData circleData) {
|
||||||
|
Class<?> type = circleData.getClass();
|
||||||
|
if (type == BasicCircleData.class
|
||||||
|
|| type == FastCircleData.class
|
||||||
|
|| type == StrongCircleData.class
|
||||||
|
|| type == SplitterCircleData.class
|
||||||
|
|| type == SpawnerCircleData.class
|
||||||
|
|| type == HealerCircleData.class
|
||||||
|
|| type == DasherCircleData.class
|
||||||
|
|| type == SniperCircleData.class) {
|
||||||
|
Pools.free(circleData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isElite() {
|
||||||
|
return elite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void makeElite() {
|
||||||
|
if (elite) return;
|
||||||
|
elite = true;
|
||||||
|
this.health *= 2;
|
||||||
|
this.damage = Math.max(1, (int) (this.damage * 1.5f));
|
||||||
|
this.circle.radius *= 1.15f;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract float[] getColor();
|
||||||
|
|
||||||
|
public float getX() {
|
||||||
|
return circle.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getY() {
|
||||||
|
return circle.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRadius() {
|
||||||
|
return circle.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void move(float delta) {
|
||||||
|
if (!frozen) {
|
||||||
|
circle.x += velocity.x * delta;
|
||||||
|
circle.y += velocity.y * delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFrozen() {
|
||||||
|
return frozen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFrozen(boolean frozen) {
|
||||||
|
this.frozen = frozen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void takeDamage(int damage) {
|
||||||
|
health -= damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDead() {
|
||||||
|
return health <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final int getReward() {
|
||||||
|
return getBaseReward() * (elite ? 3 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
|
||||||
|
public class BasicCircleData extends BaseCircleData {
|
||||||
|
private static final float[] COLOR = {1f, 0.5f, 0.5f, 1f};
|
||||||
|
private int baseReward = 1;
|
||||||
|
|
||||||
|
public BasicCircleData() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BasicCircleData(Circle circle, Vector2 velocity, int health, int damage) {
|
||||||
|
super(circle, velocity, health, damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(
|
||||||
|
float x, float y, float radius, float vx, float vy, int health, int damage) {
|
||||||
|
super.configure(x, y, radius, vx, vy, health, damage);
|
||||||
|
baseReward = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markSpawnerChild() {
|
||||||
|
markNoRewardSpawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markNoRewardSpawn() {
|
||||||
|
baseReward = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return baseReward;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Pool;
|
||||||
|
|
||||||
|
public class BonusBallData extends BaseCircleData implements Pool.Poolable {
|
||||||
|
public enum BonusType {
|
||||||
|
BULLET_SPEED,
|
||||||
|
DAMAGE,
|
||||||
|
FIRE_RATE,
|
||||||
|
SHIELD_PULSE,
|
||||||
|
HEAL,
|
||||||
|
CRIT_WINDOW,
|
||||||
|
PICKUP_MAGNET,
|
||||||
|
ABILITY_CHARGE,
|
||||||
|
COIN_BURST
|
||||||
|
}
|
||||||
|
|
||||||
|
private BonusType bonusType;
|
||||||
|
private float pulseTime = 0;
|
||||||
|
|
||||||
|
|
||||||
|
public BonusBallData() {
|
||||||
|
super(new Circle(0f, 0f, 1f), new Vector2(0f, 0f), 1, 0);
|
||||||
|
this.bonusType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BonusBallData(Circle circle, Vector2 velocity, BonusType bonusType) {
|
||||||
|
super(circle, velocity, 1, 0);
|
||||||
|
this.bonusType = bonusType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void configure(float x, float y, float radius, float vx, float vy, BonusType type) {
|
||||||
|
circle.x = x;
|
||||||
|
circle.y = y;
|
||||||
|
circle.radius = radius;
|
||||||
|
velocity.x = vx;
|
||||||
|
velocity.y = vy;
|
||||||
|
health = 1;
|
||||||
|
damage = 0;
|
||||||
|
setFrozen(false);
|
||||||
|
this.bonusType = type;
|
||||||
|
this.pulseTime = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BonusType getBonusType() {
|
||||||
|
return bonusType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(float delta) {
|
||||||
|
pulseTime += delta;
|
||||||
|
move(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float[] BULLET_SPEED_COLOR = {0.30f, 0.85f, 1.00f, 1.0f};
|
||||||
|
private static final float[] DAMAGE_COLOR = {1.00f, 0.36f, 0.32f, 1.0f};
|
||||||
|
private static final float[] FIRE_RATE_COLOR = {1.00f, 0.86f, 0.25f, 1.0f};
|
||||||
|
private static final float[] SHIELD_COLOR = {0.28f, 0.55f, 1.00f, 1.0f};
|
||||||
|
private static final float[] HEAL_COLOR = {0.25f, 1.00f, 0.55f, 1.0f};
|
||||||
|
private static final float[] CRIT_COLOR = {1.00f, 0.35f, 0.95f, 1.0f};
|
||||||
|
private static final float[] MAGNET_COLOR = {0.55f, 1.00f, 0.95f, 1.0f};
|
||||||
|
private static final float[] ABILITY_COLOR = {0.72f, 0.50f, 1.00f, 1.0f};
|
||||||
|
private static final float[] COIN_COLOR = {1.00f, 0.72f, 0.18f, 1.0f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
if (bonusType == null) return COIN_COLOR;
|
||||||
|
switch (bonusType) {
|
||||||
|
case BULLET_SPEED:
|
||||||
|
return BULLET_SPEED_COLOR;
|
||||||
|
case DAMAGE:
|
||||||
|
return DAMAGE_COLOR;
|
||||||
|
case FIRE_RATE:
|
||||||
|
return FIRE_RATE_COLOR;
|
||||||
|
case SHIELD_PULSE:
|
||||||
|
return SHIELD_COLOR;
|
||||||
|
case HEAL:
|
||||||
|
return HEAL_COLOR;
|
||||||
|
case CRIT_WINDOW:
|
||||||
|
return CRIT_COLOR;
|
||||||
|
case PICKUP_MAGNET:
|
||||||
|
return MAGNET_COLOR;
|
||||||
|
case ABILITY_CHARGE:
|
||||||
|
return ABILITY_COLOR;
|
||||||
|
case COIN_BURST:
|
||||||
|
default:
|
||||||
|
return COIN_COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getGlowAmount() {
|
||||||
|
return 0.6f + 0.4f * (float) Math.sin(pulseTime * 4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
circle.x = 0f;
|
||||||
|
circle.y = 0f;
|
||||||
|
circle.radius = 1f;
|
||||||
|
velocity.x = 0f;
|
||||||
|
velocity.y = 0f;
|
||||||
|
health = 1;
|
||||||
|
damage = 0;
|
||||||
|
setFrozen(false);
|
||||||
|
bonusType = null;
|
||||||
|
pulseTime = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
import ru.project.tower.support.RangeChecks;
|
||||||
|
|
||||||
|
public class DasherCircleData extends BaseCircleData {
|
||||||
|
private static final long WINDUP_MS = 800L;
|
||||||
|
private static final long DASH_MS = 400L;
|
||||||
|
private static final float DASH_SPEED_MULT = 3f;
|
||||||
|
private static final float CHARGE_RANGE = 280f;
|
||||||
|
|
||||||
|
private enum State {
|
||||||
|
IDLE,
|
||||||
|
WINDUP,
|
||||||
|
DASHING
|
||||||
|
}
|
||||||
|
|
||||||
|
private State state = State.IDLE;
|
||||||
|
private long stateEnter = 0L;
|
||||||
|
private final Vector2 baseVelocity;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public DasherCircleData() {
|
||||||
|
super();
|
||||||
|
this.baseVelocity = new Vector2();
|
||||||
|
this.clock = GameClock.SYSTEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DasherCircleData(Circle circle, Vector2 velocity, int health, int damage) {
|
||||||
|
this(circle, velocity, health, damage, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DasherCircleData(
|
||||||
|
Circle circle, Vector2 velocity, int health, int damage, GameClock clock) {
|
||||||
|
super(circle, velocity, health, damage);
|
||||||
|
this.baseVelocity = new Vector2(velocity);
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(
|
||||||
|
float x, float y, float radius, float vx, float vy, int health, int damage) {
|
||||||
|
super.configure(x, y, radius, vx, vy, health, damage);
|
||||||
|
baseVelocity.set(vx, vy);
|
||||||
|
state = State.IDLE;
|
||||||
|
stateEnter = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
super.reset();
|
||||||
|
baseVelocity.setZero();
|
||||||
|
state = State.IDLE;
|
||||||
|
stateEnter = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWindup() {
|
||||||
|
return state == State.WINDUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDashing() {
|
||||||
|
return state == State.DASHING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick(float squareX, float squareY) {
|
||||||
|
long t = now();
|
||||||
|
switch (state) {
|
||||||
|
case IDLE:
|
||||||
|
if (RangeChecks.withinRadiusExclusive(
|
||||||
|
getX(), getY(), squareX, squareY, CHARGE_RANGE)) {
|
||||||
|
state = State.WINDUP;
|
||||||
|
stateEnter = t;
|
||||||
|
velocity.scl(0.3f);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WINDUP:
|
||||||
|
if (t - stateEnter > WINDUP_MS) {
|
||||||
|
state = State.DASHING;
|
||||||
|
stateEnter = t;
|
||||||
|
float dx = squareX - getX(), dy = squareY - getY();
|
||||||
|
float len = (float) Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (len > 0f) {
|
||||||
|
velocity.set(dx / len, dy / len).scl(baseVelocity.len() * DASH_SPEED_MULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DASHING:
|
||||||
|
if (t - stateEnter > DASH_MS) {
|
||||||
|
state = State.IDLE;
|
||||||
|
velocity.set(baseVelocity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float[] COLOR_WINDUP = {1f, 0.9f, 0.3f, 1f};
|
||||||
|
private static final float[] COLOR_DASHING = {1f, 0.4f, 0.1f, 1f};
|
||||||
|
private static final float[] COLOR_IDLE = {0.9f, 0.6f, 0.2f, 1f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
switch (state) {
|
||||||
|
case WINDUP:
|
||||||
|
return COLOR_WINDUP;
|
||||||
|
case DASHING:
|
||||||
|
return COLOR_DASHING;
|
||||||
|
default:
|
||||||
|
return COLOR_IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
|
||||||
|
public class FastCircleData extends BaseCircleData {
|
||||||
|
private static final float[] COLOR = {1f, 1f, 0f, 1f};
|
||||||
|
|
||||||
|
public FastCircleData() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FastCircleData(Circle circle, Vector2 velocity, int health, int damage) {
|
||||||
|
super(circle, velocity, health, damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
|
||||||
|
public class HealerCircleData extends BaseCircleData {
|
||||||
|
private static final long HEAL_INTERVAL_MS = 1500L;
|
||||||
|
private static final float HEAL_RADIUS = 120f;
|
||||||
|
private static final int HEAL_AMOUNT = 3;
|
||||||
|
|
||||||
|
private long lastHealTime;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public HealerCircleData() {
|
||||||
|
super();
|
||||||
|
this.clock = GameClock.SYSTEM;
|
||||||
|
this.lastHealTime = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HealerCircleData(Circle circle, Vector2 velocity, int health, int damage) {
|
||||||
|
this(circle, velocity, health, damage, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HealerCircleData(
|
||||||
|
Circle circle, Vector2 velocity, int health, int damage, GameClock clock) {
|
||||||
|
super(circle, velocity, health, damage);
|
||||||
|
this.clock = clock;
|
||||||
|
this.lastHealTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void resetHealTimer() {
|
||||||
|
this.lastHealTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldPulse() {
|
||||||
|
if (now() - lastHealTime > HEAL_INTERVAL_MS) {
|
||||||
|
lastHealTime = now();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(
|
||||||
|
float x, float y, float radius, float vx, float vy, int health, int damage) {
|
||||||
|
super.configure(x, y, radius, vx, vy, health, damage);
|
||||||
|
this.lastHealTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
super.reset();
|
||||||
|
this.lastHealTime = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getHealRadius() {
|
||||||
|
return HEAL_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getHealAmount() {
|
||||||
|
return HEAL_AMOUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float[] COLOR = {0.2f, 1.0f, 0.5f, 1f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
import ru.project.tower.support.RangeChecks;
|
||||||
|
|
||||||
|
public class SniperCircleData extends BaseCircleData {
|
||||||
|
private static final float TARGET_RANGE = 100f;
|
||||||
|
private static final long FIRE_INTERVAL_MS = 2000L;
|
||||||
|
private static final float PROJECTILE_SPEED = 200f;
|
||||||
|
private static final float PROJECTILE_RADIUS = 4f;
|
||||||
|
|
||||||
|
private long lastShotAt = 0L;
|
||||||
|
private float playerX = 0f;
|
||||||
|
private float playerY = 0f;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public SniperCircleData() {
|
||||||
|
super();
|
||||||
|
this.clock = GameClock.SYSTEM;
|
||||||
|
this.lastShotAt = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniperCircleData(Circle circle, Vector2 velocity, int health, int damage) {
|
||||||
|
this(circle, velocity, health, damage, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SniperCircleData(
|
||||||
|
Circle circle, Vector2 velocity, int health, int damage, GameClock clock) {
|
||||||
|
super(circle, velocity, health, damage);
|
||||||
|
this.clock = clock;
|
||||||
|
this.lastShotAt = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerPosition(float x, float y) {
|
||||||
|
this.playerX = x;
|
||||||
|
this.playerY = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void resetFireTimer() {
|
||||||
|
this.lastShotAt = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void move(float delta) {
|
||||||
|
if (!RangeChecks.withinRadiusInclusive(getX(), getY(), playerX, playerY, TARGET_RANGE)) {
|
||||||
|
super.move(delta);
|
||||||
|
} else {
|
||||||
|
velocity.set(0f, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldShoot() {
|
||||||
|
if (now() - lastShotAt > FIRE_INTERVAL_MS) {
|
||||||
|
lastShotAt = now();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(
|
||||||
|
float x, float y, float radius, float vx, float vy, int health, int damage) {
|
||||||
|
super.configure(x, y, radius, vx, vy, health, damage);
|
||||||
|
lastShotAt = now();
|
||||||
|
playerX = 0f;
|
||||||
|
playerY = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
super.reset();
|
||||||
|
lastShotAt = 0L;
|
||||||
|
playerX = 0f;
|
||||||
|
playerY = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHoldingPosition() {
|
||||||
|
return RangeChecks.withinRadiusInclusive(getX(), getY(), playerX, playerY, TARGET_RANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getTargetRange() {
|
||||||
|
return TARGET_RANGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getFireIntervalMs() {
|
||||||
|
return FIRE_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getProjectileSpeed() {
|
||||||
|
return PROJECTILE_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getProjectileRadius() {
|
||||||
|
return PROJECTILE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float[] COLOR = {1f, 0.2f, 0.2f, 1f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
|
||||||
|
public class SpawnerCircleData extends BaseCircleData {
|
||||||
|
private long lastSpawnTime = 0;
|
||||||
|
private static final long SPAWN_INTERVAL = 2000;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public SpawnerCircleData() {
|
||||||
|
this(GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SpawnerCircleData(GameClock clock) {
|
||||||
|
super();
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpawnerCircleData(Circle circle, Vector2 velocity, int health) {
|
||||||
|
this(circle, velocity, health, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpawnerCircleData(Circle circle, Vector2 velocity, int health, GameClock clock) {
|
||||||
|
super(circle, velocity, health, 0);
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void configure(float x, float y, float radius, float vx, float vy, int health) {
|
||||||
|
super.configure(x, y, radius, vx, vy, health, 0);
|
||||||
|
lastSpawnTime = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(
|
||||||
|
float x, float y, float radius, float vx, float vy, int health, int damage) {
|
||||||
|
configure(x, y, radius, vx, vy, health);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
super.reset();
|
||||||
|
lastSpawnTime = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float[] COLOR = {0.5f, 0f, 0.2f, 1f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldSpawnCircle() {
|
||||||
|
long currentTime = clock.nowMs();
|
||||||
|
if (currentTime - lastSpawnTime > SPAWN_INTERVAL) {
|
||||||
|
lastSpawnTime = currentTime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
|
||||||
|
public class SplitterCircleData extends BaseCircleData {
|
||||||
|
private static final float[] COLOR_SHARD = {0.8f, 0.6f, 1f, 1f};
|
||||||
|
private static final float[] COLOR_FULL = {0.6f, 0f, 0.8f, 1f};
|
||||||
|
|
||||||
|
public boolean isShard = false;
|
||||||
|
|
||||||
|
public SplitterCircleData() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SplitterCircleData(
|
||||||
|
Circle circle, Vector2 velocity, int health, int damage, boolean isShard) {
|
||||||
|
super(circle, velocity, health, damage);
|
||||||
|
this.isShard = isShard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(
|
||||||
|
float x, float y, float radius, float vx, float vy, int health, int damage) {
|
||||||
|
configure(x, y, radius, vx, vy, health, damage, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void configure(
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
float radius,
|
||||||
|
float vx,
|
||||||
|
float vy,
|
||||||
|
int health,
|
||||||
|
int damage,
|
||||||
|
boolean isShard) {
|
||||||
|
super.configure(x, y, radius, vx, vy, health, damage);
|
||||||
|
this.isShard = isShard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
super.reset();
|
||||||
|
isShard = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return isShard ? COLOR_SHARD : COLOR_FULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package ru.project.tower.entities.circles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
|
||||||
|
public class StrongCircleData extends BaseCircleData {
|
||||||
|
private int maxHealth;
|
||||||
|
private final float[] colorScratch = {0.3f, 0.3f, 0f, 1f};
|
||||||
|
|
||||||
|
public StrongCircleData() {
|
||||||
|
super();
|
||||||
|
this.maxHealth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StrongCircleData(Circle circle, Vector2 velocity, int health, int damage) {
|
||||||
|
super(circle, velocity, health, damage);
|
||||||
|
this.maxHealth = health;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(
|
||||||
|
float x, float y, float radius, float vx, float vy, int health, int damage) {
|
||||||
|
super.configure(x, y, radius, vx, vy, health, damage);
|
||||||
|
this.maxHealth = health;
|
||||||
|
colorScratch[0] = 0.3f;
|
||||||
|
colorScratch[1] = 0.3f;
|
||||||
|
colorScratch[2] = health == 0 ? 0f : 1f;
|
||||||
|
colorScratch[3] = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
super.reset();
|
||||||
|
this.maxHealth = 0;
|
||||||
|
colorScratch[0] = 0.3f;
|
||||||
|
colorScratch[1] = 0.3f;
|
||||||
|
colorScratch[2] = 0f;
|
||||||
|
colorScratch[3] = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
colorScratch[2] = (float) health / maxHealth;
|
||||||
|
return colorScratch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
package ru.project.tower.entities.circles.bosses;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
import ru.project.tower.support.MathUtilsRandomSource;
|
||||||
|
import ru.project.tower.support.RandomSource;
|
||||||
|
|
||||||
|
public class GlacialWardenData extends BaseCircleData {
|
||||||
|
|
||||||
|
private static final float WARDEN_SPEED = 60f;
|
||||||
|
private static final int WARDEN_HEALTH = 200;
|
||||||
|
private static final int WARDEN_DAMAGE = 15;
|
||||||
|
private static final float WARDEN_RADIUS = 45f;
|
||||||
|
|
||||||
|
private static final float AURA_RADIUS = 250f;
|
||||||
|
private static final float ATTACK_SPEED_SLOW = 0.5f;
|
||||||
|
private static final float PROJECTILE_SPEED_SLOW = 0.6f;
|
||||||
|
private static final float FREEZE_ATTACK_SPEED_MULTIPLIER = 0.25f;
|
||||||
|
private static final float FREEZE_PROJECTILE_SPEED_MULTIPLIER = 0.25f;
|
||||||
|
|
||||||
|
private static final long SHARD_COOLDOWN = 3000;
|
||||||
|
private static final int SHARDS_PER_VOLLEY = 7;
|
||||||
|
private static final float SHARD_SPEED = 220f;
|
||||||
|
private static final float SHARD_RADIUS = 7f;
|
||||||
|
private static final int SHARD_DAMAGE = 8;
|
||||||
|
private static final long SHARD_DAMAGE_COOLDOWN_MS = 1000L;
|
||||||
|
private static final float FREEZE_CHANCE = 0.15f;
|
||||||
|
private static final long FREEZE_DURATION = 500;
|
||||||
|
|
||||||
|
private static final long PRISON_COOLDOWN = 10000;
|
||||||
|
private static final long PRISON_CAST_TIME = 2000;
|
||||||
|
private static final long PRISON_DURATION = 4000;
|
||||||
|
private static final int PRISON_DAMAGE = 5;
|
||||||
|
private static final float PRISON_RADIUS = 80f;
|
||||||
|
|
||||||
|
private long lastShardTime;
|
||||||
|
private long lastPrisonTime;
|
||||||
|
private boolean isCastingPrison;
|
||||||
|
private long prisonCastStartTime;
|
||||||
|
private boolean isPrisonActive;
|
||||||
|
private long prisonStartTime;
|
||||||
|
private Vector2 prisonChargePosition;
|
||||||
|
private Array<IceShard> activeShards;
|
||||||
|
|
||||||
|
private boolean isShielded = true;
|
||||||
|
private long lastShieldBreakTime = 0;
|
||||||
|
private long shieldStartTime = 0;
|
||||||
|
private static final long SHIELD_DURATION = 10000;
|
||||||
|
private static final long SHIELD_COOLDOWN = 5000;
|
||||||
|
private static final float FREEZE_RADIUS = 100f;
|
||||||
|
private long lastFreezeTime = 0;
|
||||||
|
private static final long FREEZE_COOLDOWN = 8000;
|
||||||
|
private final GameClock clock;
|
||||||
|
private final RandomSource random;
|
||||||
|
private boolean freezeFieldActive;
|
||||||
|
private long freezeFieldEndTime;
|
||||||
|
private boolean playerInsideAura;
|
||||||
|
private float activeAttackSpeedMultiplier = 1f;
|
||||||
|
private float activeProjectileSpeedMultiplier = 1f;
|
||||||
|
|
||||||
|
public GlacialWardenData(Circle circle, Vector2 velocity) {
|
||||||
|
this(circle, velocity, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GlacialWardenData(Circle circle, Vector2 velocity, GameClock clock) {
|
||||||
|
this(circle, velocity, clock, new MathUtilsRandomSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
public GlacialWardenData(
|
||||||
|
Circle circle, Vector2 velocity, GameClock clock, RandomSource random) {
|
||||||
|
super(circle, velocity, WARDEN_HEALTH, WARDEN_DAMAGE);
|
||||||
|
this.clock = clock;
|
||||||
|
this.random = random;
|
||||||
|
this.isCastingPrison = false;
|
||||||
|
this.isPrisonActive = false;
|
||||||
|
this.prisonChargePosition = new Vector2();
|
||||||
|
this.activeShards = new Array<>();
|
||||||
|
shieldStartTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long timeSince(long pastMs) {
|
||||||
|
return now() - pastMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void move(float delta) {
|
||||||
|
if (!isCastingPrison) {
|
||||||
|
super.move(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrisonActive && timeSince(prisonStartTime) > PRISON_DURATION) {
|
||||||
|
isPrisonActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCastingPrison && timeSince(prisonCastStartTime) > PRISON_CAST_TIME) {
|
||||||
|
isCastingPrison = false;
|
||||||
|
isPrisonActive = true;
|
||||||
|
prisonStartTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = activeShards.size - 1; i >= 0; i--) {
|
||||||
|
IceShard shard = activeShards.get(i);
|
||||||
|
shard.update(delta);
|
||||||
|
if (!shard.isActive()) {
|
||||||
|
activeShards.removeIndex(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float[] COLOR_SHIELDED = {0.7f, 0.9f, 1.0f, 1.0f};
|
||||||
|
private static final float[] COLOR_DEFAULT = {0.4f, 0.6f, 0.8f, 1.0f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return isShielded ? COLOR_SHIELDED : COLOR_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldShootShards() {
|
||||||
|
return timeSince(lastShardTime) > SHARD_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markShardsShot() {
|
||||||
|
lastShardTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldStartPrison() {
|
||||||
|
return !isCastingPrison && !isPrisonActive && timeSince(lastPrisonTime) > PRISON_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startPrisonCast(float targetX, float targetY) {
|
||||||
|
isCastingPrison = true;
|
||||||
|
prisonCastStartTime = now();
|
||||||
|
lastPrisonTime = now();
|
||||||
|
prisonChargePosition.set(targetX, targetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addIceShard(float x, float y, Vector2 velocity) {
|
||||||
|
IceShard shard = new IceShard(x, y, velocity, clock, random);
|
||||||
|
activeShards.add(shard);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getAuraRadius() {
|
||||||
|
return AURA_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticAuraRadius() {
|
||||||
|
return AURA_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getAttackSpeedSlow() {
|
||||||
|
return ATTACK_SPEED_SLOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getProjectileSpeedSlow() {
|
||||||
|
return PROJECTILE_SPEED_SLOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getFreezeAttackSpeedMultiplier() {
|
||||||
|
return FREEZE_ATTACK_SPEED_MULTIPLIER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getFreezeProjectileSpeedMultiplier() {
|
||||||
|
return FREEZE_PROJECTILE_SPEED_MULTIPLIER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getShardRadius() {
|
||||||
|
return SHARD_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getShardSpeed() {
|
||||||
|
return SHARD_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getShardDamage() {
|
||||||
|
return SHARD_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getShardDamageCooldownMs() {
|
||||||
|
return SHARD_DAMAGE_COOLDOWN_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getShardsPerVolley() {
|
||||||
|
return SHARDS_PER_VOLLEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getFreezeChance() {
|
||||||
|
return FREEZE_CHANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getFreezeDuration() {
|
||||||
|
return FREEZE_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getPrisonRadius() {
|
||||||
|
return PRISON_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getPrisonDamage() {
|
||||||
|
return PRISON_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCastingPrison() {
|
||||||
|
return isCastingPrison;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPrisonActive() {
|
||||||
|
return isPrisonActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPrisonCastProgress() {
|
||||||
|
if (!isCastingPrison) return 0f;
|
||||||
|
return Math.min(timeSince(prisonCastStartTime) / (float) PRISON_CAST_TIME, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 getPrisonChargePosition() {
|
||||||
|
return prisonChargePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<IceShard> getActiveShards() {
|
||||||
|
return activeShards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IceShard {
|
||||||
|
private static final float LIFETIME = 2000f;
|
||||||
|
|
||||||
|
public float x, y;
|
||||||
|
public Vector2 velocity;
|
||||||
|
public long creationTime;
|
||||||
|
public float rotation;
|
||||||
|
public float rotationSpeed;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public IceShard(float x, float y, Vector2 velocity) {
|
||||||
|
this(x, y, velocity, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IceShard(float x, float y, Vector2 velocity, GameClock clock) {
|
||||||
|
this(x, y, velocity, clock, new MathUtilsRandomSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
public IceShard(float x, float y, Vector2 velocity, GameClock clock, RandomSource random) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.velocity = velocity;
|
||||||
|
this.clock = clock;
|
||||||
|
this.creationTime = clock.nowMs();
|
||||||
|
this.rotation = random.angle();
|
||||||
|
this.rotationSpeed = random.range(-5f, 5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(float delta) {
|
||||||
|
x += velocity.x * delta;
|
||||||
|
y += velocity.y * delta;
|
||||||
|
rotation += rotationSpeed * delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return clock.nowMs() - creationTime < LIFETIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getAlpha() {
|
||||||
|
float age = (clock.nowMs() - creationTime) / LIFETIME;
|
||||||
|
return 1f - age * age;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticRadius() {
|
||||||
|
return 35f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticSpeed() {
|
||||||
|
return 45f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update() {
|
||||||
|
long currentTime = now();
|
||||||
|
|
||||||
|
if (isShielded && currentTime - shieldStartTime > SHIELD_DURATION) {
|
||||||
|
isShielded = false;
|
||||||
|
lastShieldBreakTime = currentTime;
|
||||||
|
} else if (!isShielded && currentTime - lastShieldBreakTime > SHIELD_COOLDOWN) {
|
||||||
|
isShielded = true;
|
||||||
|
shieldStartTime = currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldFreeze() {
|
||||||
|
long currentTime = now();
|
||||||
|
if (currentTime - lastFreezeTime > FREEZE_COOLDOWN) {
|
||||||
|
lastFreezeTime = currentTime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getFreezeRadius() {
|
||||||
|
return FREEZE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateFreezeField() {
|
||||||
|
freezeFieldActive = true;
|
||||||
|
freezeFieldEndTime = now() + FREEZE_DURATION;
|
||||||
|
updateActiveCombatMultipliers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFreezeFieldActive() {
|
||||||
|
boolean active = updateFreezeFieldState();
|
||||||
|
updateActiveCombatMultipliers();
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean updateFreezeFieldState() {
|
||||||
|
if (freezeFieldActive && now() >= freezeFieldEndTime) {
|
||||||
|
freezeFieldActive = false;
|
||||||
|
}
|
||||||
|
return freezeFieldActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updatePlayerAuraState(boolean insideAura) {
|
||||||
|
playerInsideAura = insideAura;
|
||||||
|
updateActiveCombatMultipliers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateActiveCombatMultipliers() {
|
||||||
|
boolean frozen = updateFreezeFieldState();
|
||||||
|
if (frozen) {
|
||||||
|
activeAttackSpeedMultiplier = FREEZE_ATTACK_SPEED_MULTIPLIER;
|
||||||
|
activeProjectileSpeedMultiplier = FREEZE_PROJECTILE_SPEED_MULTIPLIER;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeAttackSpeedMultiplier = playerInsideAura ? ATTACK_SPEED_SLOW : 1f;
|
||||||
|
activeProjectileSpeedMultiplier = playerInsideAura ? PROJECTILE_SPEED_SLOW : 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPlayerInsideAura() {
|
||||||
|
return playerInsideAura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getActiveAttackSpeedMultiplier() {
|
||||||
|
updateActiveCombatMultipliers();
|
||||||
|
return activeAttackSpeedMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getActiveProjectileSpeedMultiplier() {
|
||||||
|
updateActiveCombatMultipliers();
|
||||||
|
return activeProjectileSpeedMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShielded() {
|
||||||
|
return isShielded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void takeDamage(int damage) {
|
||||||
|
if (!isShielded) {
|
||||||
|
super.takeDamage(damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package ru.project.tower.entities.circles.bosses;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.MathUtils;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
|
||||||
|
public class IllusionMasterData extends BaseCircleData {
|
||||||
|
|
||||||
|
private static final float ILLUSION_MASTER_RADIUS = 38f;
|
||||||
|
private static final float ILLUSION_MASTER_SPEED = 90f;
|
||||||
|
private static final int ILLUSION_MASTER_HEALTH = 180;
|
||||||
|
private static final int ILLUSION_MASTER_DAMAGE = 25;
|
||||||
|
|
||||||
|
private static final long FORM_CHANGE_INTERVAL = 8000;
|
||||||
|
private static final float FAST_FORM_SPEED = 150f;
|
||||||
|
private static final float ARMORED_FORM_SPEED = 30f;
|
||||||
|
private static final float DAMAGE_MULTIPLIER_FAST = 2.0f;
|
||||||
|
private static final float DAMAGE_MULTIPLIER_ARMORED = 0.5f;
|
||||||
|
|
||||||
|
private static final long ILLUSION_ATTACK_COOLDOWN = 5000;
|
||||||
|
private static final int FAKE_PROJECTILES_COUNT = 8;
|
||||||
|
private static final int REAL_PROJECTILES_COUNT = 3;
|
||||||
|
private static final float PROJECTILE_SPEED = 180f;
|
||||||
|
private static final float PROJECTILE_RADIUS = 8f;
|
||||||
|
private static final int PROJECTILE_DAMAGE = 12;
|
||||||
|
|
||||||
|
private static final float COLOR_CHANGE_SPEED = 2f;
|
||||||
|
private static final float DISTORTION_INTENSITY = 0.3f;
|
||||||
|
private static final long DISTORTION_DURATION = 3000;
|
||||||
|
|
||||||
|
private BossForm currentForm;
|
||||||
|
private long lastFormChangeTime;
|
||||||
|
private long lastIllusionAttackTime;
|
||||||
|
private float colorPhase;
|
||||||
|
private boolean isDistortionActive;
|
||||||
|
private long distortionStartTime;
|
||||||
|
private float baseSpeed;
|
||||||
|
|
||||||
|
private Array<IllusionData> illusions;
|
||||||
|
private long lastIllusionTime = 0;
|
||||||
|
private long lastProjectileTime = 0;
|
||||||
|
private static final long ILLUSION_COOLDOWN = 10000;
|
||||||
|
private static final long ILLUSION_DURATION_MS = 8000L;
|
||||||
|
private static final int ILLUSION_CONTACT_DAMAGE = 5;
|
||||||
|
private static final long ILLUSION_DAMAGE_COOLDOWN_MS = 1000L;
|
||||||
|
private static final long PROJECTILE_COOLDOWN = 3000;
|
||||||
|
private static final int MAX_ILLUSIONS = 3;
|
||||||
|
private static final int PROJECTILES_PER_VOLLEY = 5;
|
||||||
|
private static final float ILLUSION_HEALTH_PERCENT = 0.3f;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public enum BossForm {
|
||||||
|
FAST,
|
||||||
|
ARMORED
|
||||||
|
}
|
||||||
|
|
||||||
|
public IllusionMasterData(Circle circle, Vector2 velocity) {
|
||||||
|
this(circle, velocity, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IllusionMasterData(Circle circle, Vector2 velocity, GameClock clock) {
|
||||||
|
super(circle, velocity, ILLUSION_MASTER_HEALTH, ILLUSION_MASTER_DAMAGE);
|
||||||
|
this.clock = clock;
|
||||||
|
this.currentForm = BossForm.FAST;
|
||||||
|
this.lastFormChangeTime = now();
|
||||||
|
this.lastIllusionAttackTime = now();
|
||||||
|
this.colorPhase = 0f;
|
||||||
|
this.isDistortionActive = false;
|
||||||
|
this.baseSpeed = FAST_FORM_SPEED;
|
||||||
|
this.illusions = new Array<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long timeSince(long pastMs) {
|
||||||
|
return now() - pastMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void move(float delta) {
|
||||||
|
|
||||||
|
colorPhase += delta * COLOR_CHANGE_SPEED;
|
||||||
|
if (colorPhase > MathUtils.PI2) {
|
||||||
|
colorPhase -= MathUtils.PI2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSince(lastFormChangeTime) > FORM_CHANGE_INTERVAL) {
|
||||||
|
switchForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.move(delta);
|
||||||
|
|
||||||
|
for (int i = illusions.size - 1; i >= 0; i--) {
|
||||||
|
IllusionData illusion = illusions.get(i);
|
||||||
|
if (illusion.isDead() || illusion.isExpired(now())) {
|
||||||
|
illusions.removeIndex(i);
|
||||||
|
} else {
|
||||||
|
illusion.move(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void switchForm() {
|
||||||
|
currentForm = (currentForm == BossForm.FAST) ? BossForm.ARMORED : BossForm.FAST;
|
||||||
|
lastFormChangeTime = now();
|
||||||
|
|
||||||
|
baseSpeed = (currentForm == BossForm.FAST) ? FAST_FORM_SPEED : ARMORED_FORM_SPEED;
|
||||||
|
velocity.nor().scl(baseSpeed);
|
||||||
|
|
||||||
|
activateDistortion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateDistortion() {
|
||||||
|
isDistortionActive = true;
|
||||||
|
distortionStartTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final float[] colorScratch = {0f, 0f, 0f, 1f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
colorScratch[0] = 0.5f + 0.5f * MathUtils.sin(colorPhase);
|
||||||
|
colorScratch[1] = 0.5f + 0.5f * MathUtils.sin(colorPhase + MathUtils.PI2 / 3);
|
||||||
|
colorScratch[2] = 0.5f + 0.5f * MathUtils.sin(colorPhase + 2 * MathUtils.PI2 / 3);
|
||||||
|
return colorScratch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldAttack() {
|
||||||
|
return timeSince(lastIllusionAttackTime) > ILLUSION_ATTACK_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markAttackUsed() {
|
||||||
|
lastIllusionAttackTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void takeDamage(int damage) {
|
||||||
|
float multiplier =
|
||||||
|
(currentForm == BossForm.FAST) ? DAMAGE_MULTIPLIER_FAST : DAMAGE_MULTIPLIER_ARMORED;
|
||||||
|
super.takeDamage((int) (damage * multiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDistortionActive() {
|
||||||
|
if (isDistortionActive && timeSince(distortionStartTime) > DISTORTION_DURATION) {
|
||||||
|
isDistortionActive = false;
|
||||||
|
}
|
||||||
|
return isDistortionActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getDistortionIntensity() {
|
||||||
|
if (!isDistortionActive) return 0f;
|
||||||
|
float progress = timeSince(distortionStartTime) / (float) DISTORTION_DURATION;
|
||||||
|
return DISTORTION_INTENSITY * (1f - progress * progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BossForm getCurrentForm() {
|
||||||
|
return currentForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getProjectileRadius() {
|
||||||
|
return PROJECTILE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getProjectileSpeed() {
|
||||||
|
return PROJECTILE_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getProjectileDamage() {
|
||||||
|
return PROJECTILE_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getFakeProjectilesCount() {
|
||||||
|
return FAKE_PROJECTILES_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getRealProjectilesCount() {
|
||||||
|
return REAL_PROJECTILES_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 225;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticRadius() {
|
||||||
|
return 25f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticSpeed() {
|
||||||
|
return 65f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldCreateIllusions() {
|
||||||
|
long currentTime = now();
|
||||||
|
if (currentTime - lastIllusionTime > ILLUSION_COOLDOWN && illusions.size < MAX_ILLUSIONS) {
|
||||||
|
lastIllusionTime = currentTime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldShootProjectiles() {
|
||||||
|
long currentTime = now();
|
||||||
|
if (currentTime - lastProjectileTime > PROJECTILE_COOLDOWN) {
|
||||||
|
lastProjectileTime = currentTime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProjectilesPerVolley() {
|
||||||
|
return PROJECTILES_PER_VOLLEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getIllusionDurationMs() {
|
||||||
|
return ILLUSION_DURATION_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getIllusionContactDamage() {
|
||||||
|
return ILLUSION_CONTACT_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getIllusionDamageCooldownMs() {
|
||||||
|
return ILLUSION_DAMAGE_COOLDOWN_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IllusionData createIllusion(float x, float y) {
|
||||||
|
Circle illusionCircle = new Circle(x, y, circle.radius);
|
||||||
|
Vector2 illusionVelocity = velocity.cpy();
|
||||||
|
int illusionHealth = (int) (health * ILLUSION_HEALTH_PERCENT);
|
||||||
|
IllusionData illusion =
|
||||||
|
new IllusionData(
|
||||||
|
illusionCircle,
|
||||||
|
illusionVelocity,
|
||||||
|
illusionHealth,
|
||||||
|
ILLUSION_CONTACT_DAMAGE,
|
||||||
|
now());
|
||||||
|
illusions.add(illusion);
|
||||||
|
return illusion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeIllusion(IllusionData illusion) {
|
||||||
|
illusions.removeValue(illusion, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<IllusionData> getIllusions() {
|
||||||
|
return illusions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IllusionData extends BaseCircleData {
|
||||||
|
private final long creationTime;
|
||||||
|
|
||||||
|
public IllusionData(
|
||||||
|
Circle circle, Vector2 velocity, int health, int damage, long creationTime) {
|
||||||
|
super(circle, velocity, health, damage);
|
||||||
|
this.creationTime = creationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCreationTime() {
|
||||||
|
return creationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExpired(long nowMs) {
|
||||||
|
return nowMs - creationTime > ILLUSION_DURATION_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final float[] illusionColor = {0.8f, 0.3f, 0.8f, 0.7f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return illusionColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package ru.project.tower.entities.circles.bosses;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
|
||||||
|
public class JuggernautData extends BaseCircleData {
|
||||||
|
|
||||||
|
private static final float JUGGERNAUT_CHARGE_SPEED = 300f;
|
||||||
|
private static final int JUGGERNAUT_BASE_HEALTH = 300;
|
||||||
|
private static final int JUGGERNAUT_DAMAGE = 25;
|
||||||
|
private static final float JUGGERNAUT_RADIUS = 45f;
|
||||||
|
|
||||||
|
private static final int ARMOR_LAYERS = 3;
|
||||||
|
private static final long INVULNERABILITY_DURATION = 2000;
|
||||||
|
|
||||||
|
private static final long CHARGE_PREPARATION_TIME = 2000;
|
||||||
|
private static final long CHARGE_COOLDOWN = 5000;
|
||||||
|
private static final long CHARGE_DURATION = 2000;
|
||||||
|
private static final float CHARGE_SPEED_MULTIPLIER = 3.0f;
|
||||||
|
private static final long SHOCKWAVE_COOLDOWN = 10000;
|
||||||
|
private static final float SHOCKWAVE_RADIUS = 200f;
|
||||||
|
private static final float SHOCKWAVE_FORCE = 500f;
|
||||||
|
|
||||||
|
private int currentLayer;
|
||||||
|
private long lastLayerBreakTime;
|
||||||
|
private boolean isCharging;
|
||||||
|
private boolean isPreparingCharge;
|
||||||
|
private long chargeStartTime;
|
||||||
|
private long lastChargeTime;
|
||||||
|
private long lastShockwaveTime;
|
||||||
|
private Vector2 chargeDirection;
|
||||||
|
private Vector2 normalVelocity;
|
||||||
|
private Vector2 chargeVelocity;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public JuggernautData(Circle circle, Vector2 velocity) {
|
||||||
|
this(circle, velocity, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JuggernautData(Circle circle, Vector2 velocity, GameClock clock) {
|
||||||
|
super(circle, velocity, JUGGERNAUT_BASE_HEALTH, JUGGERNAUT_DAMAGE);
|
||||||
|
this.clock = clock;
|
||||||
|
this.currentLayer = ARMOR_LAYERS;
|
||||||
|
this.isCharging = false;
|
||||||
|
this.isPreparingCharge = false;
|
||||||
|
this.chargeDirection = new Vector2();
|
||||||
|
this.normalVelocity = velocity.cpy();
|
||||||
|
this.chargeVelocity = velocity.cpy().scl(CHARGE_SPEED_MULTIPLIER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long timeSince(long pastMs) {
|
||||||
|
return now() - pastMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void move(float delta) {
|
||||||
|
if (isCharging) {
|
||||||
|
|
||||||
|
circle.x += chargeDirection.x * JUGGERNAUT_CHARGE_SPEED * delta;
|
||||||
|
circle.y += chargeDirection.y * JUGGERNAUT_CHARGE_SPEED * delta;
|
||||||
|
} else {
|
||||||
|
super.move(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float[] COLOR = {0.8f, 0.4f, 0.0f, 1.0f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
return COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInvulnerable() {
|
||||||
|
return timeSince(lastLayerBreakTime) < INVULNERABILITY_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void takeDamage(int damage) {
|
||||||
|
if (isInvulnerable()) return;
|
||||||
|
|
||||||
|
super.takeDamage(damage);
|
||||||
|
|
||||||
|
if (health <= 0 && currentLayer > 1) {
|
||||||
|
currentLayer--;
|
||||||
|
health = JUGGERNAUT_BASE_HEALTH / ARMOR_LAYERS;
|
||||||
|
lastLayerBreakTime = now();
|
||||||
|
|
||||||
|
lastShockwaveTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldStartCharge() {
|
||||||
|
long currentTime = now();
|
||||||
|
if (!isCharging && !isPreparingCharge && currentTime - lastChargeTime > CHARGE_COOLDOWN) {
|
||||||
|
isPreparingCharge = true;
|
||||||
|
chargeStartTime = currentTime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldExecuteCharge() {
|
||||||
|
if (isPreparingCharge && timeSince(chargeStartTime) > CHARGE_PREPARATION_TIME) {
|
||||||
|
isPreparingCharge = false;
|
||||||
|
isCharging = true;
|
||||||
|
lastChargeTime = now();
|
||||||
|
velocity.set(chargeVelocity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChargeDirection(float x, float y) {
|
||||||
|
float length = (float) Math.sqrt(x * x + y * y);
|
||||||
|
if (length > 0) {
|
||||||
|
chargeDirection.set(x / length, y / length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldShockwave() {
|
||||||
|
return timeSince(lastShockwaveTime) > SHOCKWAVE_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void performShockwave() {
|
||||||
|
lastShockwaveTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endCharge() {
|
||||||
|
isCharging = false;
|
||||||
|
isPreparingCharge = false;
|
||||||
|
velocity.set(normalVelocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCharging() {
|
||||||
|
return isCharging;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPreparingCharge() {
|
||||||
|
return isPreparingCharge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getChargeProgress() {
|
||||||
|
if (!isPreparingCharge) return 0f;
|
||||||
|
return Math.min(timeSince(chargeStartTime) / (float) CHARGE_PREPARATION_TIME, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentLayer() {
|
||||||
|
return currentLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getShockwaveRadius() {
|
||||||
|
return SHOCKWAVE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getShockwaveForce() {
|
||||||
|
return SHOCKWAVE_FORCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticRadius() {
|
||||||
|
return 40f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticSpeed() {
|
||||||
|
return 40f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update() {
|
||||||
|
long currentTime = now();
|
||||||
|
if (isCharging && currentTime - lastChargeTime > CHARGE_DURATION) {
|
||||||
|
isCharging = false;
|
||||||
|
velocity.set(normalVelocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package ru.project.tower.entities.circles.bosses;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
|
||||||
|
public class ShadowWeaverData extends BaseCircleData {
|
||||||
|
|
||||||
|
private static final float SHADOW_WEAVER_SPEED = 100f;
|
||||||
|
private static final int SHADOW_WEAVER_BASE_HEALTH = 200;
|
||||||
|
private static final int SHADOW_WEAVER_DAMAGE = 15;
|
||||||
|
private static final float SHADOW_WEAVER_RADIUS = 40f;
|
||||||
|
|
||||||
|
private static final long TELEPORT_COOLDOWN = 5000;
|
||||||
|
private static final long TELEPORT_DURATION = 1000;
|
||||||
|
private static final float TELEPORT_MAX_DISTANCE = 300f;
|
||||||
|
|
||||||
|
private static final int MAX_CLONES = 3;
|
||||||
|
private static final long CLONE_DURATION = 8000;
|
||||||
|
private static final int CLONE_HEALTH = 30;
|
||||||
|
private static final int CLONE_DAMAGE = 5;
|
||||||
|
private static final long CLONE_DAMAGE_COOLDOWN_MS = 1000L;
|
||||||
|
|
||||||
|
private static final long PROJECTILE_COOLDOWN = 2000;
|
||||||
|
private static final float PROJECTILE_SPEED = 200f;
|
||||||
|
private static final float PROJECTILE_RADIUS = 8f;
|
||||||
|
private static final int PROJECTILE_DAMAGE = 10;
|
||||||
|
|
||||||
|
private boolean isTeleporting;
|
||||||
|
private long lastTeleportTime;
|
||||||
|
private long teleportStartTime;
|
||||||
|
private Vector2 teleportTarget;
|
||||||
|
private Vector2 originalPosition;
|
||||||
|
private long lastProjectileTime;
|
||||||
|
private float teleportProgress;
|
||||||
|
private Array<ShadowCloneData> clones;
|
||||||
|
|
||||||
|
private boolean isInvisible = false;
|
||||||
|
private long lastInvisibilityTime = 0;
|
||||||
|
private long invisibilityStartTime = 0;
|
||||||
|
private static final long INVISIBILITY_COOLDOWN = 7000;
|
||||||
|
private static final long INVISIBILITY_DURATION = 3000;
|
||||||
|
private static final float INVISIBLE_SPEED_MULTIPLIER = 1.5f;
|
||||||
|
private Vector2 normalVelocity;
|
||||||
|
private Vector2 invisibleVelocity;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public ShadowWeaverData(Circle circle, Vector2 velocity) {
|
||||||
|
this(circle, velocity, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShadowWeaverData(Circle circle, Vector2 velocity, GameClock clock) {
|
||||||
|
super(circle, velocity, SHADOW_WEAVER_BASE_HEALTH, SHADOW_WEAVER_DAMAGE);
|
||||||
|
this.clock = clock;
|
||||||
|
this.isTeleporting = false;
|
||||||
|
this.teleportTarget = new Vector2();
|
||||||
|
this.originalPosition = new Vector2();
|
||||||
|
this.clones = new Array<>();
|
||||||
|
this.normalVelocity = velocity.cpy();
|
||||||
|
this.invisibleVelocity = velocity.cpy().scl(INVISIBLE_SPEED_MULTIPLIER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long timeSince(long pastMs) {
|
||||||
|
return now() - pastMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void move(float delta) {
|
||||||
|
if (isTeleporting) {
|
||||||
|
|
||||||
|
teleportProgress =
|
||||||
|
Math.min(1f, timeSince(teleportStartTime) / (float) TELEPORT_DURATION);
|
||||||
|
|
||||||
|
circle.x =
|
||||||
|
originalPosition.x + (teleportTarget.x - originalPosition.x) * teleportProgress;
|
||||||
|
circle.y =
|
||||||
|
originalPosition.y + (teleportTarget.y - originalPosition.y) * teleportProgress;
|
||||||
|
|
||||||
|
if (teleportProgress >= 1f) {
|
||||||
|
isTeleporting = false;
|
||||||
|
lastTeleportTime = now();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.move(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = clones.size - 1; i >= 0; i--) {
|
||||||
|
ShadowCloneData clone = clones.get(i);
|
||||||
|
if (timeSince(clone.getCreationTime()) > CLONE_DURATION) {
|
||||||
|
clones.removeIndex(i);
|
||||||
|
} else {
|
||||||
|
clone.move(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final float[] colorScratch = {0.3f, 0.0f, 0.5f, 1.0f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
colorScratch[3] = isInvisible ? 0.3f : 1.0f;
|
||||||
|
return colorScratch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldTeleport() {
|
||||||
|
return !isTeleporting && timeSince(lastTeleportTime) > TELEPORT_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startTeleport(float targetX, float targetY) {
|
||||||
|
isTeleporting = true;
|
||||||
|
teleportStartTime = now();
|
||||||
|
teleportProgress = 0f;
|
||||||
|
originalPosition.set(circle.x, circle.y);
|
||||||
|
teleportTarget.set(targetX, targetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createClones() {
|
||||||
|
if (clones.size < MAX_CLONES) {
|
||||||
|
for (int i = 0; i < MAX_CLONES; i++) {
|
||||||
|
float angle = (float) (i * Math.PI * 2 / MAX_CLONES);
|
||||||
|
float distance = 100f;
|
||||||
|
float cloneX = circle.x + (float) Math.cos(angle) * distance;
|
||||||
|
float cloneY = circle.y + (float) Math.sin(angle) * distance;
|
||||||
|
|
||||||
|
Circle cloneCircle = new Circle(cloneX, cloneY, SHADOW_WEAVER_RADIUS);
|
||||||
|
Vector2 cloneVelocity = new Vector2(velocity).scl(0.8f);
|
||||||
|
|
||||||
|
ShadowCloneData clone = new ShadowCloneData(cloneCircle, cloneVelocity);
|
||||||
|
clones.add(clone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldShoot() {
|
||||||
|
return timeSince(lastProjectileTime) > PROJECTILE_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markProjectileShot() {
|
||||||
|
lastProjectileTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTeleporting() {
|
||||||
|
return isTeleporting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getTeleportProgress() {
|
||||||
|
return teleportProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<ShadowCloneData> getClones() {
|
||||||
|
return clones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getCloneDamage() {
|
||||||
|
return CLONE_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getCloneDamageCooldownMs() {
|
||||||
|
return CLONE_DAMAGE_COOLDOWN_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getProjectileRadius() {
|
||||||
|
return PROJECTILE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getProjectileSpeed() {
|
||||||
|
return PROJECTILE_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getProjectileDamage() {
|
||||||
|
return PROJECTILE_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticRadius() {
|
||||||
|
return 25f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticSpeed() {
|
||||||
|
return 70f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldBecomeInvisible() {
|
||||||
|
long currentTime = now();
|
||||||
|
if (!isInvisible && currentTime - lastInvisibilityTime > INVISIBILITY_COOLDOWN) {
|
||||||
|
isInvisible = true;
|
||||||
|
invisibilityStartTime = currentTime;
|
||||||
|
lastInvisibilityTime = currentTime;
|
||||||
|
velocity.set(invisibleVelocity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update() {
|
||||||
|
long currentTime = now();
|
||||||
|
if (isInvisible && currentTime - invisibilityStartTime > INVISIBILITY_DURATION) {
|
||||||
|
isInvisible = false;
|
||||||
|
velocity.set(normalVelocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInvisible() {
|
||||||
|
return isInvisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ShadowCloneData extends BaseCircleData {
|
||||||
|
private long creationTime;
|
||||||
|
|
||||||
|
public ShadowCloneData(Circle circle, Vector2 velocity) {
|
||||||
|
super(circle, velocity, CLONE_HEALTH, CLONE_DAMAGE);
|
||||||
|
this.creationTime = ShadowWeaverData.this.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCreationTime() {
|
||||||
|
return creationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final float[] cloneColorScratch = {0f, 0f, 0f, 0.5f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
float[] bossColor = ShadowWeaverData.this.getColor();
|
||||||
|
cloneColorScratch[0] = bossColor[0];
|
||||||
|
cloneColorScratch[1] = bossColor[1];
|
||||||
|
cloneColorScratch[2] = bossColor[2];
|
||||||
|
return cloneColorScratch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
package ru.project.tower.entities.circles.bosses;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.support.BossEffectsHost;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
|
||||||
|
public class SwarmQueenData extends BaseCircleData {
|
||||||
|
|
||||||
|
private static final float QUEEN_RADIUS = 40f;
|
||||||
|
private static final int QUEEN_HEALTH = 200;
|
||||||
|
private static final float QUEEN_SPEED = 40f;
|
||||||
|
private static final int QUEEN_DAMAGE = 15;
|
||||||
|
|
||||||
|
private static final int MINIONS_PER_SPAWN = 6;
|
||||||
|
private static final long MINION_SPAWN_COOLDOWN = 6000;
|
||||||
|
private static final long VULNERABILITY_DURATION = 2000;
|
||||||
|
|
||||||
|
private static final int BULLETS_PER_VOLLEY = 12;
|
||||||
|
private static final long BULLET_VOLLEY_COOLDOWN = 8000;
|
||||||
|
private static final float BULLET_SPEED = 200f;
|
||||||
|
private static final float BULLET_RADIUS = 5f;
|
||||||
|
private static final int BULLET_DAMAGE = 10;
|
||||||
|
|
||||||
|
private static final int ORBITAL_EYES_COUNT = 4;
|
||||||
|
private static final float ORBITAL_RADIUS = 60f;
|
||||||
|
private static final float ORBITAL_SPEED = 2f;
|
||||||
|
|
||||||
|
private static final int ORBITAL_PARTICLES_COUNT = 12;
|
||||||
|
private static final float ORBITAL_PARTICLE_RADIUS = 3f;
|
||||||
|
private static final float ORBITAL_PARTICLE_SPEED = 3f;
|
||||||
|
|
||||||
|
private long lastMinionSpawnTime;
|
||||||
|
private long lastBulletVolleyTime;
|
||||||
|
private boolean isVulnerable;
|
||||||
|
private long vulnerabilityStartTime;
|
||||||
|
private float orbitalAngle;
|
||||||
|
private float particleAngle = 0f;
|
||||||
|
private Array<Vector2> orbitalEyePositions;
|
||||||
|
private Array<Vector2> orbitalParticles;
|
||||||
|
private static final long CHARGING_DURATION = 1000;
|
||||||
|
private long chargingStartTime = 0;
|
||||||
|
private boolean isChargingComplete = false;
|
||||||
|
|
||||||
|
private static final Color BASE_COLOR = new Color(0.5f, 0.1f, 0.5f, 1f);
|
||||||
|
private static final Color CHARGING_COLOR = new Color(0.8f, 0.2f, 0.8f, 1f);
|
||||||
|
private static final Color VULNERABLE_COLOR = new Color(0.3f, 0.3f, 0.8f, 1f);
|
||||||
|
private static final Color MINION_COLOR = new Color(0.4f, 0.1f, 0.4f, 1f);
|
||||||
|
private static final Color PROJECTILE_COLOR = new Color(0.8f, 0.2f, 0.8f, 1f);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private final Color scratchBase = new Color();
|
||||||
|
private final Color scratchMinion = new Color();
|
||||||
|
private final Color scratchProjectile = new Color();
|
||||||
|
private final float[] scratchColorArr = new float[4];
|
||||||
|
|
||||||
|
private static final float EYE_RADIUS = 8f;
|
||||||
|
private static final float EYE_GLOW_MULTIPLIER = 1.5f;
|
||||||
|
private static final float PARTICLE_SPAWN_RATE = 0.1f;
|
||||||
|
private float particleTimer = 0f;
|
||||||
|
|
||||||
|
private final BossEffectsHost host;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public SwarmQueenData(Circle circle, Vector2 velocity, BossEffectsHost host) {
|
||||||
|
this(circle, velocity, host, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SwarmQueenData(Circle circle, Vector2 velocity, BossEffectsHost host, GameClock clock) {
|
||||||
|
super(circle, velocity, QUEEN_HEALTH, QUEEN_DAMAGE);
|
||||||
|
this.host = host;
|
||||||
|
this.clock = clock;
|
||||||
|
this.orbitalEyePositions = new Array<>(ORBITAL_EYES_COUNT);
|
||||||
|
this.orbitalParticles = new Array<>(ORBITAL_PARTICLES_COUNT);
|
||||||
|
|
||||||
|
for (int i = 0; i < ORBITAL_EYES_COUNT; i++) {
|
||||||
|
this.orbitalEyePositions.add(new Vector2());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < ORBITAL_PARTICLES_COUNT; i++) {
|
||||||
|
this.orbitalParticles.add(new Vector2());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrbitalEyes();
|
||||||
|
updateOrbitalParticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long timeSince(long pastMs) {
|
||||||
|
return now() - pastMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void move(float delta) {
|
||||||
|
super.move(delta);
|
||||||
|
|
||||||
|
orbitalAngle += ORBITAL_SPEED * delta;
|
||||||
|
updateOrbitalEyes();
|
||||||
|
|
||||||
|
particleAngle += ORBITAL_PARTICLE_SPEED * delta;
|
||||||
|
updateOrbitalParticles();
|
||||||
|
|
||||||
|
if (isVulnerable && timeSince(vulnerabilityStartTime) > VULNERABILITY_DURATION) {
|
||||||
|
isVulnerable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateOrbitalEyes() {
|
||||||
|
float angleStep = (float) (2 * Math.PI / ORBITAL_EYES_COUNT);
|
||||||
|
for (int i = 0; i < ORBITAL_EYES_COUNT; i++) {
|
||||||
|
float angle = orbitalAngle + i * angleStep;
|
||||||
|
float x = getX() + ORBITAL_RADIUS * (float) Math.cos(angle);
|
||||||
|
float y = getY() + ORBITAL_RADIUS * (float) Math.sin(angle);
|
||||||
|
orbitalEyePositions.get(i).set(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateOrbitalParticles() {
|
||||||
|
float baseRadius = circle.radius * 1.5f;
|
||||||
|
float angleStep = (float) (2 * Math.PI / ORBITAL_PARTICLES_COUNT);
|
||||||
|
|
||||||
|
for (int i = 0; i < ORBITAL_PARTICLES_COUNT; i++) {
|
||||||
|
float angle = particleAngle + i * angleStep;
|
||||||
|
|
||||||
|
float radiusOffset = 5f * (float) Math.sin(now() / 200.0 + i * 0.5f);
|
||||||
|
float radius = baseRadius + radiusOffset;
|
||||||
|
|
||||||
|
float x = getX() + radius * (float) Math.cos(angle);
|
||||||
|
float y = getY() + radius * (float) Math.sin(angle);
|
||||||
|
orbitalParticles.get(i).set(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color getBaseColor() {
|
||||||
|
scratchBase.set(BASE_COLOR);
|
||||||
|
if (isCharging()) {
|
||||||
|
scratchBase.lerp(CHARGING_COLOR, getChargeProgress());
|
||||||
|
} else if (isVulnerable()) {
|
||||||
|
scratchBase.lerp(VULNERABLE_COLOR, 0.7f);
|
||||||
|
}
|
||||||
|
scratchBase.a = 0.8f + 0.2f * (float) Math.sin(now() / 200.0);
|
||||||
|
return scratchBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
Color color = getBaseColor();
|
||||||
|
scratchColorArr[0] = color.r;
|
||||||
|
scratchColorArr[1] = color.g;
|
||||||
|
scratchColorArr[2] = color.b;
|
||||||
|
scratchColorArr[3] = color.a;
|
||||||
|
return scratchColorArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color getOrbitalParticleColor() {
|
||||||
|
return getBaseColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldSpawnMinions() {
|
||||||
|
return timeSince(lastMinionSpawnTime) > MINION_SPAWN_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onMinionSpawn() {
|
||||||
|
long currentTime = now();
|
||||||
|
lastMinionSpawnTime = currentTime;
|
||||||
|
isVulnerable = true;
|
||||||
|
vulnerabilityStartTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReadyToFireProjectiles() {
|
||||||
|
return timeSince(lastBulletVolleyTime) > BULLET_VOLLEY_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onProjectileAttackStart() {
|
||||||
|
lastBulletVolleyTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticRadius() {
|
||||||
|
return QUEEN_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticSpeed() {
|
||||||
|
return QUEEN_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getMinionsPerWave() {
|
||||||
|
return MINIONS_PER_SPAWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getBulletsPerVolley() {
|
||||||
|
return BULLETS_PER_VOLLEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getBulletSpeed() {
|
||||||
|
return BULLET_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getBulletRadius() {
|
||||||
|
return BULLET_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getBulletDamage() {
|
||||||
|
return BULLET_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<Vector2> getOrbitalEyePositions() {
|
||||||
|
return orbitalEyePositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<Vector2> getOrbitalParticles() {
|
||||||
|
return orbitalParticles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getOrbitalParticleRadius() {
|
||||||
|
return ORBITAL_PARTICLE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVulnerable() {
|
||||||
|
return isVulnerable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void takeDamage(int damage) {
|
||||||
|
if (isVulnerable) {
|
||||||
|
super.takeDamage(damage * 2);
|
||||||
|
} else {
|
||||||
|
super.takeDamage(damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCharging() {
|
||||||
|
if (isReadyToFireProjectiles() && !isChargingComplete) {
|
||||||
|
if (chargingStartTime == 0) {
|
||||||
|
chargingStartTime = now();
|
||||||
|
}
|
||||||
|
boolean charging = timeSince(chargingStartTime) < CHARGING_DURATION;
|
||||||
|
if (!charging) {
|
||||||
|
isChargingComplete = true;
|
||||||
|
}
|
||||||
|
return charging;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void finishCharging() {
|
||||||
|
chargingStartTime = 0;
|
||||||
|
isChargingComplete = false;
|
||||||
|
onProjectileAttackStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getChargeProgress() {
|
||||||
|
if (chargingStartTime == 0) {
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
float progress = (float) timeSince(chargingStartTime) / CHARGING_DURATION;
|
||||||
|
return Math.min(progress, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color getMinionColor() {
|
||||||
|
scratchMinion.set(MINION_COLOR);
|
||||||
|
if (isVulnerable()) {
|
||||||
|
scratchMinion.lerp(VULNERABLE_COLOR, 0.5f);
|
||||||
|
}
|
||||||
|
scratchMinion.a = 0.9f + 0.1f * (float) Math.sin(now() / 300.0);
|
||||||
|
return scratchMinion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color getProjectileColor() {
|
||||||
|
scratchProjectile.set(PROJECTILE_COLOR);
|
||||||
|
if (isVulnerable()) {
|
||||||
|
scratchProjectile.lerp(VULNERABLE_COLOR, 0.3f);
|
||||||
|
}
|
||||||
|
scratchProjectile.a = 0.7f + 0.3f * (float) Math.sin(now() / 150.0);
|
||||||
|
return scratchProjectile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getEyeRadius() {
|
||||||
|
float baseRadius = EYE_RADIUS;
|
||||||
|
if (isCharging()) {
|
||||||
|
|
||||||
|
baseRadius *= (1f + 0.5f * getChargeProgress());
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseRadius * (1f + 0.1f * (float) Math.sin(now() / 150.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getEyeGlow() {
|
||||||
|
float baseGlow = 1f;
|
||||||
|
if (isCharging()) {
|
||||||
|
|
||||||
|
baseGlow = 1f + getChargeProgress() * 2f;
|
||||||
|
} else if (isVulnerable()) {
|
||||||
|
|
||||||
|
baseGlow = 0.7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseGlow * (1f + 0.2f * (float) Math.sin(now() / 200.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateParticles(float delta) {
|
||||||
|
particleTimer += delta;
|
||||||
|
if (particleTimer >= PARTICLE_SPAWN_RATE) {
|
||||||
|
particleTimer = 0f;
|
||||||
|
if (isCharging()) {
|
||||||
|
|
||||||
|
spawnChargeParticles();
|
||||||
|
} else if (isVulnerable()) {
|
||||||
|
|
||||||
|
spawnVulnerabilityParticles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Color scratchParticleColor = new Color();
|
||||||
|
|
||||||
|
private void spawnChargeParticles() {
|
||||||
|
if (host != null) {
|
||||||
|
scratchParticleColor.set(CHARGING_COLOR);
|
||||||
|
scratchParticleColor.a = 0.7f + 0.3f * getChargeProgress();
|
||||||
|
host.createBossParticles(getX(), getY(), circle.radius, scratchParticleColor, 1.5f, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnVulnerabilityParticles() {
|
||||||
|
if (host != null) {
|
||||||
|
scratchParticleColor.set(VULNERABLE_COLOR);
|
||||||
|
scratchParticleColor.a = 0.6f;
|
||||||
|
host.createBossParticles(getX(), getY(), circle.radius, scratchParticleColor, 1.2f, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package ru.project.tower.entities.circles.bosses;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Circle;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Array;
|
||||||
|
import ru.project.tower.entities.circles.BaseCircleData;
|
||||||
|
import ru.project.tower.support.GameClock;
|
||||||
|
|
||||||
|
public class VolatileReactorData extends BaseCircleData {
|
||||||
|
|
||||||
|
private static final float REACTOR_SPEED = 80f;
|
||||||
|
private static final int REACTOR_BASE_HEALTH = 250;
|
||||||
|
private static final int REACTOR_DAMAGE = 20;
|
||||||
|
private static final float REACTOR_RADIUS = 40f;
|
||||||
|
|
||||||
|
private static final long HEAT_CYCLE_DURATION = 15000;
|
||||||
|
private static final int EXPLOSION_DAMAGE = 50;
|
||||||
|
private static final float EXPLOSION_RADIUS = 300f;
|
||||||
|
private static final long COOLDOWN_DURATION = 5000;
|
||||||
|
|
||||||
|
private static final long FIRE_ZONE_INTERVAL = 3000;
|
||||||
|
private static final long FIRE_ZONE_DURATION = 5000;
|
||||||
|
private static final float FIRE_ZONE_RADIUS = 60f;
|
||||||
|
private static final int FIRE_ZONE_DAMAGE = 5;
|
||||||
|
private static final long FIRE_ZONE_DAMAGE_COOLDOWN_MS = 1000L;
|
||||||
|
|
||||||
|
private static final long MISSILE_COOLDOWN = 7000;
|
||||||
|
private static final int MISSILES_PER_VOLLEY = 5;
|
||||||
|
private static final float MISSILE_SPEED = 250f;
|
||||||
|
private static final float MISSILE_RADIUS = 6f;
|
||||||
|
private static final int MISSILE_DAMAGE = 8;
|
||||||
|
|
||||||
|
private float heatLevel;
|
||||||
|
private long cycleStartTime;
|
||||||
|
private boolean isExploding;
|
||||||
|
private boolean isCoolingDown;
|
||||||
|
private long lastFireZoneTime;
|
||||||
|
private long lastMissileTime;
|
||||||
|
private Array<FireZone> fireZones;
|
||||||
|
|
||||||
|
private int energyLevel = 0;
|
||||||
|
private long lastEnergyGainTime = 0;
|
||||||
|
private static final long ENERGY_GAIN_INTERVAL = 1000;
|
||||||
|
private static final int MAX_ENERGY = 100;
|
||||||
|
private static final int ENERGY_GAIN_AMOUNT = 10;
|
||||||
|
private long explosionStartTime = 0;
|
||||||
|
private static final long EXPLOSION_DURATION = 500;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public VolatileReactorData(Circle circle, Vector2 velocity) {
|
||||||
|
this(circle, velocity, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VolatileReactorData(Circle circle, Vector2 velocity, GameClock clock) {
|
||||||
|
super(circle, velocity, REACTOR_BASE_HEALTH, REACTOR_DAMAGE);
|
||||||
|
this.clock = clock;
|
||||||
|
this.heatLevel = 0f;
|
||||||
|
this.cycleStartTime = now();
|
||||||
|
this.isExploding = false;
|
||||||
|
this.isCoolingDown = false;
|
||||||
|
this.fireZones = new Array<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long now() {
|
||||||
|
return clock.nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long timeSince(long pastMs) {
|
||||||
|
return now() - pastMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void move(float delta) {
|
||||||
|
super.move(delta);
|
||||||
|
|
||||||
|
if (!isCoolingDown) {
|
||||||
|
heatLevel = Math.min(1f, timeSince(cycleStartTime) / (float) HEAT_CYCLE_DURATION);
|
||||||
|
|
||||||
|
if (heatLevel >= 1f && !isExploding) {
|
||||||
|
isExploding = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
heatLevel = Math.max(0f, 1f - timeSince(cycleStartTime) / (float) COOLDOWN_DURATION);
|
||||||
|
|
||||||
|
if (heatLevel <= 0f) {
|
||||||
|
isCoolingDown = false;
|
||||||
|
cycleStartTime = now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = fireZones.size - 1; i >= 0; i--) {
|
||||||
|
FireZone zone = fireZones.get(i);
|
||||||
|
if (timeSince(zone.creationTime) > FIRE_ZONE_DURATION) {
|
||||||
|
fireZones.removeIndex(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final float[] colorScratch = {0f, 0f, 0.1f, 1f};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getColor() {
|
||||||
|
float pulseIntensity = 0.5f + 0.5f * (float) Math.sin(now() * 0.01f);
|
||||||
|
float heat = heatLevel + pulseIntensity * 0.2f;
|
||||||
|
colorScratch[0] = 0.8f + heat * 0.2f;
|
||||||
|
colorScratch[1] = 0.4f - heat * 0.3f;
|
||||||
|
return colorScratch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void explode() {
|
||||||
|
isExploding = false;
|
||||||
|
isCoolingDown = true;
|
||||||
|
cycleStartTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldCreateFireZone() {
|
||||||
|
return timeSince(lastFireZoneTime) > FIRE_ZONE_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createFireZone() {
|
||||||
|
FireZone zone = new FireZone(circle.x, circle.y, FIRE_ZONE_RADIUS, now(), clock);
|
||||||
|
fireZones.add(zone);
|
||||||
|
lastFireZoneTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldShootMissiles() {
|
||||||
|
return timeSince(lastMissileTime) > MISSILE_COOLDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markMissileShot() {
|
||||||
|
lastMissileTime = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getHeatLevel() {
|
||||||
|
return heatLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExploding() {
|
||||||
|
return isExploding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<FireZone> getFireZones() {
|
||||||
|
return fireZones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getFireZoneRadius() {
|
||||||
|
return FIRE_ZONE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getFireZoneDamage() {
|
||||||
|
return FIRE_ZONE_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getFireZoneDamageCooldownMs() {
|
||||||
|
return FIRE_ZONE_DAMAGE_COOLDOWN_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getMissileRadius() {
|
||||||
|
return MISSILE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getMissileSpeed() {
|
||||||
|
return MISSILE_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getMissileDamage() {
|
||||||
|
return MISSILE_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getMissilesPerVolley() {
|
||||||
|
return MISSILES_PER_VOLLEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getExplosionRadius() {
|
||||||
|
return EXPLOSION_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getExplosionDamage() {
|
||||||
|
return EXPLOSION_DAMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getBaseReward() {
|
||||||
|
return 225;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FireZone {
|
||||||
|
public float x, y;
|
||||||
|
public float radius;
|
||||||
|
public long creationTime;
|
||||||
|
private final GameClock clock;
|
||||||
|
|
||||||
|
public FireZone(float x, float y, float radius, long creationTime) {
|
||||||
|
this(x, y, radius, creationTime, GameClock.SYSTEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FireZone(float x, float y, float radius, long creationTime, GameClock clock) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.radius = radius;
|
||||||
|
this.creationTime = creationTime;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getAlpha() {
|
||||||
|
float age = (clock.nowMs() - creationTime) / (float) FIRE_ZONE_DURATION;
|
||||||
|
return 1f - age;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final float[] zoneColorScratch = {1f, 0.3f, 0.1f, 0f};
|
||||||
|
|
||||||
|
public float[] getColor() {
|
||||||
|
float intensity = 0.5f + 0.5f * (float) Math.sin(clock.nowMs() * 0.005f);
|
||||||
|
zoneColorScratch[3] = getAlpha() * (0.3f + intensity * 0.2f);
|
||||||
|
return zoneColorScratch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticRadius() {
|
||||||
|
return REACTOR_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getStaticSpeed() {
|
||||||
|
return REACTOR_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update() {
|
||||||
|
long currentTime = now();
|
||||||
|
|
||||||
|
if (!isExploding && currentTime - lastEnergyGainTime > ENERGY_GAIN_INTERVAL) {
|
||||||
|
energyLevel = Math.min(MAX_ENERGY, energyLevel + ENERGY_GAIN_AMOUNT);
|
||||||
|
lastEnergyGainTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExploding && currentTime - explosionStartTime > EXPLOSION_DURATION) {
|
||||||
|
isExploding = false;
|
||||||
|
energyLevel = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldExplode() {
|
||||||
|
return energyLevel >= MAX_ENERGY && !isExploding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startExplosion() {
|
||||||
|
if (!isExploding) {
|
||||||
|
isExploding = true;
|
||||||
|
explosionStartTime = now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEnergyLevel() {
|
||||||
|
return energyLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package ru.project.tower.entities.particles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Pool;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class FloatingText implements Pool.Poolable {
|
||||||
|
public final Vector2 position = new Vector2();
|
||||||
|
public final Vector2 velocity = new Vector2();
|
||||||
|
public String text;
|
||||||
|
public Color color;
|
||||||
|
public float lifetime;
|
||||||
|
public float maxLifetime;
|
||||||
|
public float scale;
|
||||||
|
public float initialScale;
|
||||||
|
|
||||||
|
|
||||||
|
public FloatingText() {}
|
||||||
|
|
||||||
|
public void configure(float x, float y, String text, Color srcColor) {
|
||||||
|
configure(x, y, text, srcColor, 1.0f, 1.0f, 50f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void configure(
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
String text,
|
||||||
|
Color srcColor,
|
||||||
|
float maxLifetime,
|
||||||
|
float initialScale,
|
||||||
|
float riseSpeed) {
|
||||||
|
position.set(x, y);
|
||||||
|
velocity.set(0f, riseSpeed);
|
||||||
|
this.text = text;
|
||||||
|
|
||||||
|
if (this.color == null) this.color = new Color();
|
||||||
|
this.color.set(srcColor);
|
||||||
|
this.maxLifetime = maxLifetime;
|
||||||
|
this.lifetime = maxLifetime;
|
||||||
|
this.initialScale = initialScale;
|
||||||
|
this.scale = initialScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(float delta) {
|
||||||
|
position.add(velocity.x * delta, velocity.y * delta);
|
||||||
|
lifetime -= delta;
|
||||||
|
velocity.y *= 0.95f;
|
||||||
|
float life = Math.max(0f, lifetime / maxLifetime);
|
||||||
|
scale = initialScale * (0.85f + 0.15f * life);
|
||||||
|
color.a = Math.min(1f, life / 0.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAlive() {
|
||||||
|
return lifetime > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
position.set(0f, 0f);
|
||||||
|
velocity.set(0f, 0f);
|
||||||
|
text = null;
|
||||||
|
|
||||||
|
if (color != null) color.set(0f, 0f, 0f, 0f);
|
||||||
|
lifetime = 0f;
|
||||||
|
maxLifetime = 0f;
|
||||||
|
scale = 0f;
|
||||||
|
initialScale = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package ru.project.tower.entities.particles;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.math.MathUtils;
|
||||||
|
import com.badlogic.gdx.math.Vector2;
|
||||||
|
import com.badlogic.gdx.utils.Pool;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class NeonParticle implements Pool.Poolable {
|
||||||
|
public final Vector2 position = new Vector2();
|
||||||
|
public final Vector2 velocity = new Vector2();
|
||||||
|
public final Color color = new Color();
|
||||||
|
public float size;
|
||||||
|
public float lifetime;
|
||||||
|
public float maxLifetime;
|
||||||
|
|
||||||
|
|
||||||
|
public NeonParticle() {}
|
||||||
|
|
||||||
|
public void configure(float x, float y, Color color) {
|
||||||
|
position.set(x, y);
|
||||||
|
velocity.set(MathUtils.random(-20f, 20f), MathUtils.random(-20f, 20f));
|
||||||
|
size = MathUtils.random(2f, 4f);
|
||||||
|
maxLifetime = MathUtils.random(0.5f, 2.0f);
|
||||||
|
lifetime = maxLifetime;
|
||||||
|
this.color.set(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(float delta) {
|
||||||
|
position.add(velocity.x * delta, velocity.y * delta);
|
||||||
|
lifetime -= delta;
|
||||||
|
size = Math.max(0, size - delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
position.set(0f, 0f);
|
||||||
|
velocity.set(0f, 0f);
|
||||||
|
size = 0f;
|
||||||
|
lifetime = 0f;
|
||||||
|
maxLifetime = 0f;
|
||||||
|
color.set(0f, 0f, 0f, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package ru.project.tower.navigation;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Screen;
|
||||||
|
import java.util.Objects;
|
||||||
|
import ru.project.tower.GameContext;
|
||||||
|
import ru.project.tower.screens.AbilitySelectionScreen;
|
||||||
|
import ru.project.tower.screens.AbilityShopScreen;
|
||||||
|
import ru.project.tower.screens.GameScreen;
|
||||||
|
import ru.project.tower.screens.MainScreen;
|
||||||
|
import ru.project.tower.screens.TalentTreeScreen;
|
||||||
|
import ru.project.tower.screens.abilityselection.AbilitySelectionDependencies;
|
||||||
|
import ru.project.tower.screens.abilityshop.AbilityShopDependencies;
|
||||||
|
import ru.project.tower.screens.gamescreen.GameScreenDependencies;
|
||||||
|
import ru.project.tower.screens.talenttree.TalentTreeScreenDependencies;
|
||||||
|
|
||||||
|
public final class DefaultScreenFactory implements ScreenFactory {
|
||||||
|
private final GameContext ctx;
|
||||||
|
|
||||||
|
public DefaultScreenFactory(GameContext ctx) {
|
||||||
|
this.ctx = Objects.requireNonNull(ctx, "ctx");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Screen createMainMenu(ScreenNavigator navigator) {
|
||||||
|
return new MainScreen(navigator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Screen createAbilitySelection(ScreenNavigator navigator) {
|
||||||
|
return new AbilitySelectionScreen(navigator, AbilitySelectionDependencies.from(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Screen createAbilityShop(ScreenNavigator navigator) {
|
||||||
|
return new AbilityShopScreen(navigator, AbilityShopDependencies.from(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Screen createTalentTree(ScreenNavigator navigator) {
|
||||||
|
return new TalentTreeScreen(navigator, TalentTreeScreenDependencies.from(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Screen createGameScreen(ScreenNavigator navigator) {
|
||||||
|
return new GameScreen(navigator, GameScreenDependencies.from(ctx));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package ru.project.tower.navigation;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Game;
|
||||||
|
import com.badlogic.gdx.Screen;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class GameScreenNavigator implements ScreenNavigator {
|
||||||
|
private final ScreenHost host;
|
||||||
|
private final ScreenFactory factory;
|
||||||
|
|
||||||
|
public GameScreenNavigator(ScreenHost host, ScreenFactory factory) {
|
||||||
|
this.host = Objects.requireNonNull(host, "host");
|
||||||
|
this.factory = Objects.requireNonNull(factory, "factory");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GameScreenNavigator forGame(Game game, ScreenFactory factory) {
|
||||||
|
return new GameScreenNavigator(new GdxGameScreenHost(game), factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showMainMenu() {
|
||||||
|
transitionTo(factory.createMainMenu(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showAbilitySelection() {
|
||||||
|
transitionTo(factory.createAbilitySelection(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showAbilityShop() {
|
||||||
|
transitionTo(factory.createAbilityShop(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showTalentTree() {
|
||||||
|
transitionTo(factory.createTalentTree(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showGameScreen() {
|
||||||
|
transitionTo(factory.createGameScreen(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transitionTo(Screen nextScreen) {
|
||||||
|
Screen previousScreen = host.currentScreen();
|
||||||
|
host.setScreen(Objects.requireNonNull(nextScreen, "nextScreen"));
|
||||||
|
if (previousScreen != null && previousScreen != nextScreen) {
|
||||||
|
previousScreen.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ScreenHost {
|
||||||
|
Screen currentScreen();
|
||||||
|
|
||||||
|
void setScreen(Screen screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class GdxGameScreenHost implements ScreenHost {
|
||||||
|
private final Game game;
|
||||||
|
|
||||||
|
private GdxGameScreenHost(Game game) {
|
||||||
|
this.game = Objects.requireNonNull(game, "game");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Screen currentScreen() {
|
||||||
|
return game.getScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setScreen(Screen screen) {
|
||||||
|
game.setScreen(screen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ru.project.tower.navigation;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Screen;
|
||||||
|
|
||||||
|
public interface ScreenFactory {
|
||||||
|
Screen createMainMenu(ScreenNavigator navigator);
|
||||||
|
|
||||||
|
Screen createAbilitySelection(ScreenNavigator navigator);
|
||||||
|
|
||||||
|
Screen createAbilityShop(ScreenNavigator navigator);
|
||||||
|
|
||||||
|
Screen createTalentTree(ScreenNavigator navigator);
|
||||||
|
|
||||||
|
Screen createGameScreen(ScreenNavigator navigator);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ru.project.tower.navigation;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Game;
|
||||||
|
import ru.project.tower.GameContext;
|
||||||
|
|
||||||
|
public final class ScreenNavigation {
|
||||||
|
private ScreenNavigation() {}
|
||||||
|
|
||||||
|
public static ScreenNavigator forGame(Game game, GameContext ctx) {
|
||||||
|
return GameScreenNavigator.forGame(game, new DefaultScreenFactory(ctx));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ru.project.tower.navigation;
|
||||||
|
|
||||||
|
public interface ScreenNavigator {
|
||||||
|
void showMainMenu();
|
||||||
|
|
||||||
|
void showAbilitySelection();
|
||||||
|
|
||||||
|
void showAbilityShop();
|
||||||
|
|
||||||
|
void showTalentTree();
|
||||||
|
|
||||||
|
void showGameScreen();
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package ru.project.tower.player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import ru.project.tower.balance.EndlessBalanceSpec;
|
||||||
|
import ru.project.tower.systems.BonusBallSystem;
|
||||||
|
import ru.project.tower.talents.TalentTree;
|
||||||
|
import ru.project.tower.upgrades.UpgradeSystem;
|
||||||
|
import ru.project.tower.upgrades.shop.ShopUpgradeSystem;
|
||||||
|
|
||||||
|
public final class EffectiveCombatStats {
|
||||||
|
private static final float CRIT_KEYSTONE_CHANCE_PER_LEVEL = 0.10f;
|
||||||
|
private static final String CRIT_KEYSTONE_ID = "keystone_crit";
|
||||||
|
|
||||||
|
private final float damage;
|
||||||
|
private final float shotCooldownMs;
|
||||||
|
private final float bulletSpeedMultiplier;
|
||||||
|
private final float critChance;
|
||||||
|
private final float critMultiplier;
|
||||||
|
private final int healthRegenRate;
|
||||||
|
private final int pierceBonus;
|
||||||
|
private final float aoeRadiusBonus;
|
||||||
|
private final float controlRating;
|
||||||
|
private final float executeDamageBonus;
|
||||||
|
private final float bossDamageBonus;
|
||||||
|
private final float controlledDamageBonus;
|
||||||
|
private final float abilityScalingFactor;
|
||||||
|
|
||||||
|
private EffectiveCombatStats(
|
||||||
|
float damage,
|
||||||
|
float shotCooldownMs,
|
||||||
|
float bulletSpeedMultiplier,
|
||||||
|
float critChance,
|
||||||
|
float critMultiplier,
|
||||||
|
int healthRegenRate,
|
||||||
|
int pierceBonus,
|
||||||
|
float aoeRadiusBonus,
|
||||||
|
float controlRating,
|
||||||
|
float executeDamageBonus,
|
||||||
|
float bossDamageBonus,
|
||||||
|
float controlledDamageBonus,
|
||||||
|
float abilityScalingFactor) {
|
||||||
|
this.damage = damage;
|
||||||
|
this.shotCooldownMs = shotCooldownMs;
|
||||||
|
this.bulletSpeedMultiplier = bulletSpeedMultiplier;
|
||||||
|
this.critChance = critChance;
|
||||||
|
this.critMultiplier = critMultiplier;
|
||||||
|
this.healthRegenRate = healthRegenRate;
|
||||||
|
this.pierceBonus = pierceBonus;
|
||||||
|
this.aoeRadiusBonus = aoeRadiusBonus;
|
||||||
|
this.controlRating = controlRating;
|
||||||
|
this.executeDamageBonus = executeDamageBonus;
|
||||||
|
this.bossDamageBonus = bossDamageBonus;
|
||||||
|
this.controlledDamageBonus = controlledDamageBonus;
|
||||||
|
this.abilityScalingFactor = abilityScalingFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EffectiveCombatStats fromSources(
|
||||||
|
PlayerCombatState combatState,
|
||||||
|
ShopUpgradeSystem shopUpgradeSystem,
|
||||||
|
UpgradeSystem upgradeSystem,
|
||||||
|
BonusBallSystem bonusBallSystem,
|
||||||
|
TalentTree talentTree,
|
||||||
|
int currentWave,
|
||||||
|
float baseDamage,
|
||||||
|
float runDamageBonus,
|
||||||
|
int runPierceBonus,
|
||||||
|
float runAoeRadiusBonus,
|
||||||
|
float controlRating) {
|
||||||
|
Objects.requireNonNull(combatState, "combatState");
|
||||||
|
Objects.requireNonNull(shopUpgradeSystem, "shopUpgradeSystem");
|
||||||
|
Objects.requireNonNull(upgradeSystem, "upgradeSystem");
|
||||||
|
Objects.requireNonNull(bonusBallSystem, "bonusBallSystem");
|
||||||
|
Objects.requireNonNull(talentTree, "talentTree");
|
||||||
|
|
||||||
|
float damage =
|
||||||
|
(baseDamage + shopUpgradeSystem.damageBonus() + runDamageBonus)
|
||||||
|
* bonusBallSystem.getDamageMultiplier();
|
||||||
|
return new EffectiveCombatStats(
|
||||||
|
damage,
|
||||||
|
shotCooldownMs(
|
||||||
|
combatState,
|
||||||
|
shopUpgradeSystem.cooldownReduction(),
|
||||||
|
upgradeSystem,
|
||||||
|
bonusBallSystem),
|
||||||
|
bonusBallSystem.getSpeedMultiplier(),
|
||||||
|
critChance(combatState, shopUpgradeSystem, upgradeSystem, talentTree),
|
||||||
|
critMultiplier(combatState, shopUpgradeSystem),
|
||||||
|
combatState.getHealthRegenRate() + shopUpgradeSystem.regenBonus(),
|
||||||
|
runPierceBonus + shopUpgradeSystem.pierceBonus(),
|
||||||
|
runAoeRadiusBonus + shopUpgradeSystem.aoeRadiusBonus(),
|
||||||
|
controlRating,
|
||||||
|
shopUpgradeSystem.executeDamageBonus(),
|
||||||
|
shopUpgradeSystem.bossDamageBonus(),
|
||||||
|
shopUpgradeSystem.controlledDamageBonus(),
|
||||||
|
PlayerRunCombatCalculator.abilityScalingFactor(currentWave));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float shotCooldownMs(
|
||||||
|
PlayerCombatState combatState,
|
||||||
|
float shopCooldownReduction,
|
||||||
|
UpgradeSystem upgradeSystem,
|
||||||
|
BonusBallSystem bonusBallSystem) {
|
||||||
|
Objects.requireNonNull(combatState, "combatState");
|
||||||
|
Objects.requireNonNull(upgradeSystem, "upgradeSystem");
|
||||||
|
Objects.requireNonNull(bonusBallSystem, "bonusBallSystem");
|
||||||
|
float cooldown =
|
||||||
|
(combatState.getShotCooldownMs() - shopCooldownReduction)
|
||||||
|
* upgradeSystem.getRunCooldownMult()
|
||||||
|
* bonusBallSystem.getCooldownMultiplier();
|
||||||
|
return Math.max(PlayerCombatState.MIN_SHOT_COOLDOWN_MS, cooldown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float critChance(
|
||||||
|
PlayerCombatState combatState,
|
||||||
|
ShopUpgradeSystem shopUpgradeSystem,
|
||||||
|
UpgradeSystem upgradeSystem,
|
||||||
|
TalentTree talentTree) {
|
||||||
|
Objects.requireNonNull(combatState, "combatState");
|
||||||
|
Objects.requireNonNull(shopUpgradeSystem, "shopUpgradeSystem");
|
||||||
|
Objects.requireNonNull(upgradeSystem, "upgradeSystem");
|
||||||
|
Objects.requireNonNull(talentTree, "talentTree");
|
||||||
|
int critKeystone = talentTree.getCurrentLevel(CRIT_KEYSTONE_ID);
|
||||||
|
return Math.min(
|
||||||
|
EndlessBalanceSpec.CRIT_CHANCE_CAP,
|
||||||
|
combatState.getBaseCritChance()
|
||||||
|
+ shopUpgradeSystem.critChanceBonus()
|
||||||
|
+ critKeystone * CRIT_KEYSTONE_CHANCE_PER_LEVEL
|
||||||
|
+ upgradeSystem.getRunCritBonus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float critMultiplier(
|
||||||
|
PlayerCombatState combatState, ShopUpgradeSystem shopUpgradeSystem) {
|
||||||
|
Objects.requireNonNull(combatState, "combatState");
|
||||||
|
Objects.requireNonNull(shopUpgradeSystem, "shopUpgradeSystem");
|
||||||
|
return combatState.getBaseCritMultiplier() + shopUpgradeSystem.critMultiplierBonus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float damage() {
|
||||||
|
return damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float shotCooldownMs() {
|
||||||
|
return shotCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float bulletSpeedMultiplier() {
|
||||||
|
return bulletSpeedMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float critChance() {
|
||||||
|
return critChance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float critMultiplier() {
|
||||||
|
return critMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int healthRegenRate() {
|
||||||
|
return healthRegenRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int pierceBonus() {
|
||||||
|
return pierceBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float aoeRadiusBonus() {
|
||||||
|
return aoeRadiusBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float controlRating() {
|
||||||
|
return controlRating;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float executeDamageBonus() {
|
||||||
|
return executeDamageBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float bossDamageBonus() {
|
||||||
|
return bossDamageBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float controlledDamageBonus() {
|
||||||
|
return controlledDamageBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float abilityScalingFactor() {
|
||||||
|
return abilityScalingFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
package ru.project.tower.player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class PlayerCombatState {
|
||||||
|
public static final int DEFAULT_MAX_HEALTH = 100;
|
||||||
|
public static final float DEFAULT_SHOT_COOLDOWN_MS = 800f;
|
||||||
|
public static final float MIN_SHOT_COOLDOWN_MS = 100f;
|
||||||
|
public static final long HEALTH_REGEN_INTERVAL_MS = 1_000L;
|
||||||
|
public static final float DEFAULT_BASE_CRIT_CHANCE = 0.05f;
|
||||||
|
public static final float DEFAULT_BASE_CRIT_MULTIPLIER = 1.5f;
|
||||||
|
|
||||||
|
private int health;
|
||||||
|
private int maxHealth;
|
||||||
|
private int healthRegenRate;
|
||||||
|
private int money;
|
||||||
|
private long lastDamageTimeMs;
|
||||||
|
private long lastHealthRegenTimeMs;
|
||||||
|
private float shotCooldownMs;
|
||||||
|
private float baseCritChance;
|
||||||
|
private float baseCritMultiplier;
|
||||||
|
private int gameOverCurrencyAward;
|
||||||
|
private int maxWaveReached;
|
||||||
|
private boolean gameOverCurrencyAwarded;
|
||||||
|
private int runCurrencyEarned;
|
||||||
|
|
||||||
|
public PlayerCombatState() {
|
||||||
|
resetForNewRun(DEFAULT_MAX_HEALTH, DEFAULT_SHOT_COOLDOWN_MS, 0);
|
||||||
|
baseCritChance = DEFAULT_BASE_CRIT_CHANCE;
|
||||||
|
baseCritMultiplier = DEFAULT_BASE_CRIT_MULTIPLIER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean damageIfVulnerable(int damage, long damageCooldownMs, long nowMs) {
|
||||||
|
return damageIfVulnerable(damage, damageCooldownMs, nowMs, DamageBlocker.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean damageIfVulnerable(
|
||||||
|
int damage, long damageCooldownMs, long nowMs, DamageBlocker blocker) {
|
||||||
|
if (damageCooldownMs > 0L && nowMs - lastDamageTimeMs <= damageCooldownMs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DamageBlocker activeBlocker = blocker == null ? DamageBlocker.NONE : blocker;
|
||||||
|
if (!activeBlocker.blocksDamage(nowMs)) {
|
||||||
|
health -= damage;
|
||||||
|
}
|
||||||
|
lastDamageTimeMs = nowMs;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean regenerateIfReady(long nowMs) {
|
||||||
|
if (nowMs - lastHealthRegenTimeMs <= HEALTH_REGEN_INTERVAL_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
health = Math.min(maxHealth, health + healthRegenRate);
|
||||||
|
lastHealthRegenTimeMs = nowMs;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetForNewRun(int maxHealth, float shotCooldownMs, int healthRegenRate) {
|
||||||
|
money = 0;
|
||||||
|
this.maxHealth = maxHealth;
|
||||||
|
health = maxHealth;
|
||||||
|
this.healthRegenRate = healthRegenRate;
|
||||||
|
this.shotCooldownMs = shotCooldownMs;
|
||||||
|
lastDamageTimeMs = 0L;
|
||||||
|
lastHealthRegenTimeMs = 0L;
|
||||||
|
gameOverCurrencyAward = 0;
|
||||||
|
gameOverCurrencyAwarded = false;
|
||||||
|
runCurrencyEarned = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot snapshot() {
|
||||||
|
return new Snapshot(
|
||||||
|
money,
|
||||||
|
health,
|
||||||
|
maxHealth,
|
||||||
|
healthRegenRate,
|
||||||
|
lastDamageTimeMs,
|
||||||
|
lastHealthRegenTimeMs,
|
||||||
|
shotCooldownMs,
|
||||||
|
baseCritChance,
|
||||||
|
baseCritMultiplier,
|
||||||
|
gameOverCurrencyAward,
|
||||||
|
maxWaveReached,
|
||||||
|
gameOverCurrencyAwarded,
|
||||||
|
runCurrencyEarned);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void apply(Snapshot snapshot) {
|
||||||
|
Snapshot source = Objects.requireNonNull(snapshot, "snapshot");
|
||||||
|
money = source.money();
|
||||||
|
health = source.health();
|
||||||
|
maxHealth = source.maxHealth();
|
||||||
|
healthRegenRate = source.healthRegenRate();
|
||||||
|
lastDamageTimeMs = source.lastDamageTimeMs();
|
||||||
|
lastHealthRegenTimeMs = source.lastHealthRegenTimeMs();
|
||||||
|
shotCooldownMs = source.shotCooldownMs();
|
||||||
|
baseCritChance = source.baseCritChance();
|
||||||
|
baseCritMultiplier = source.baseCritMultiplier();
|
||||||
|
gameOverCurrencyAward = source.gameOverCurrencyAward();
|
||||||
|
maxWaveReached = source.maxWaveReached();
|
||||||
|
gameOverCurrencyAwarded = source.gameOverCurrencyAwarded();
|
||||||
|
runCurrencyEarned = source.runCurrencyEarned();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int awardGameOverCurrencyOnce(int currentWave) {
|
||||||
|
if (gameOverCurrencyAwarded) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameOverCurrencyAward = currentWave;
|
||||||
|
maxWaveReached = Math.max(maxWaveReached, currentWave);
|
||||||
|
gameOverCurrencyAwarded = true;
|
||||||
|
return gameOverCurrencyAward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHealth() {
|
||||||
|
return health;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHealth(int health) {
|
||||||
|
this.health = health;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxHealth() {
|
||||||
|
return maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxHealth(int maxHealth) {
|
||||||
|
this.maxHealth = maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHealthRegenRate() {
|
||||||
|
return healthRegenRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHealthRegenRate(int healthRegenRate) {
|
||||||
|
this.healthRegenRate = healthRegenRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMoney() {
|
||||||
|
return money;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMoney(int money) {
|
||||||
|
this.money = money;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addMoney(int amount) {
|
||||||
|
if (amount > 0) {
|
||||||
|
money += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean spendMoney(int amount) {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (money < amount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
money -= amount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRunCurrencyEarned() {
|
||||||
|
return runCurrencyEarned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRunCurrencyEarned(int amount) {
|
||||||
|
if (amount > 0) {
|
||||||
|
runCurrencyEarned += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastDamageTimeMs() {
|
||||||
|
return lastDamageTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastDamageTimeMs(long lastDamageTimeMs) {
|
||||||
|
this.lastDamageTimeMs = lastDamageTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastHealthRegenTimeMs() {
|
||||||
|
return lastHealthRegenTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastHealthRegenTimeMs(long lastHealthRegenTimeMs) {
|
||||||
|
this.lastHealthRegenTimeMs = lastHealthRegenTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getShotCooldownMs() {
|
||||||
|
return shotCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShotCooldownMs(float shotCooldownMs) {
|
||||||
|
this.shotCooldownMs = shotCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reduceShotCooldown(float amountMs) {
|
||||||
|
shotCooldownMs = Math.max(MIN_SHOT_COOLDOWN_MS, shotCooldownMs - amountMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getBaseCritChance() {
|
||||||
|
return baseCritChance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaseCritChance(float baseCritChance) {
|
||||||
|
this.baseCritChance = baseCritChance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getBaseCritMultiplier() {
|
||||||
|
return baseCritMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaseCritMultiplier(float baseCritMultiplier) {
|
||||||
|
this.baseCritMultiplier = baseCritMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getGameOverCurrencyAward() {
|
||||||
|
return gameOverCurrencyAward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxWaveReached() {
|
||||||
|
return maxWaveReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGameOverCurrencyAwarded() {
|
||||||
|
return gameOverCurrencyAwarded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface DamageBlocker {
|
||||||
|
DamageBlocker NONE = nowMs -> false;
|
||||||
|
|
||||||
|
boolean blocksDamage(long nowMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Snapshot {
|
||||||
|
private final int money;
|
||||||
|
private final int health;
|
||||||
|
private final int maxHealth;
|
||||||
|
private final int healthRegenRate;
|
||||||
|
private final long lastDamageTimeMs;
|
||||||
|
private final long lastHealthRegenTimeMs;
|
||||||
|
private final float shotCooldownMs;
|
||||||
|
private final float baseCritChance;
|
||||||
|
private final float baseCritMultiplier;
|
||||||
|
private final int gameOverCurrencyAward;
|
||||||
|
private final int maxWaveReached;
|
||||||
|
private final boolean gameOverCurrencyAwarded;
|
||||||
|
private final int runCurrencyEarned;
|
||||||
|
|
||||||
|
public Snapshot(
|
||||||
|
int money,
|
||||||
|
int health,
|
||||||
|
int maxHealth,
|
||||||
|
int healthRegenRate,
|
||||||
|
long lastDamageTimeMs,
|
||||||
|
long lastHealthRegenTimeMs,
|
||||||
|
float shotCooldownMs,
|
||||||
|
float baseCritChance,
|
||||||
|
float baseCritMultiplier,
|
||||||
|
int gameOverCurrencyAward,
|
||||||
|
int maxWaveReached,
|
||||||
|
boolean gameOverCurrencyAwarded) {
|
||||||
|
this(
|
||||||
|
money,
|
||||||
|
health,
|
||||||
|
maxHealth,
|
||||||
|
healthRegenRate,
|
||||||
|
lastDamageTimeMs,
|
||||||
|
lastHealthRegenTimeMs,
|
||||||
|
shotCooldownMs,
|
||||||
|
baseCritChance,
|
||||||
|
baseCritMultiplier,
|
||||||
|
gameOverCurrencyAward,
|
||||||
|
maxWaveReached,
|
||||||
|
gameOverCurrencyAwarded,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot(
|
||||||
|
int money,
|
||||||
|
int health,
|
||||||
|
int maxHealth,
|
||||||
|
int healthRegenRate,
|
||||||
|
long lastDamageTimeMs,
|
||||||
|
long lastHealthRegenTimeMs,
|
||||||
|
float shotCooldownMs,
|
||||||
|
float baseCritChance,
|
||||||
|
float baseCritMultiplier,
|
||||||
|
int gameOverCurrencyAward,
|
||||||
|
int maxWaveReached,
|
||||||
|
boolean gameOverCurrencyAwarded,
|
||||||
|
int runCurrencyEarned) {
|
||||||
|
this.money = money;
|
||||||
|
this.health = health;
|
||||||
|
this.maxHealth = maxHealth;
|
||||||
|
this.healthRegenRate = healthRegenRate;
|
||||||
|
this.lastDamageTimeMs = lastDamageTimeMs;
|
||||||
|
this.lastHealthRegenTimeMs = lastHealthRegenTimeMs;
|
||||||
|
this.shotCooldownMs = shotCooldownMs;
|
||||||
|
this.baseCritChance = baseCritChance;
|
||||||
|
this.baseCritMultiplier = baseCritMultiplier;
|
||||||
|
this.gameOverCurrencyAward = gameOverCurrencyAward;
|
||||||
|
this.maxWaveReached = maxWaveReached;
|
||||||
|
this.gameOverCurrencyAwarded = gameOverCurrencyAwarded;
|
||||||
|
this.runCurrencyEarned = runCurrencyEarned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int money() {
|
||||||
|
return money;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int health() {
|
||||||
|
return health;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int maxHealth() {
|
||||||
|
return maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int healthRegenRate() {
|
||||||
|
return healthRegenRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long lastDamageTimeMs() {
|
||||||
|
return lastDamageTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long lastHealthRegenTimeMs() {
|
||||||
|
return lastHealthRegenTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float shotCooldownMs() {
|
||||||
|
return shotCooldownMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float baseCritChance() {
|
||||||
|
return baseCritChance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float baseCritMultiplier() {
|
||||||
|
return baseCritMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int gameOverCurrencyAward() {
|
||||||
|
return gameOverCurrencyAward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int maxWaveReached() {
|
||||||
|
return maxWaveReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean gameOverCurrencyAwarded() {
|
||||||
|
return gameOverCurrencyAwarded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int runCurrencyEarned() {
|
||||||
|
return runCurrencyEarned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||