diff --git a/NEXT_RELEASE_DRAFT.md b/NEXT_RELEASE_DRAFT.md deleted file mode 100755 index e34f7c6..0000000 --- a/NEXT_RELEASE_DRAFT.md +++ /dev/null @@ -1,50 +0,0 @@ -## Features targeted for 22.3.0.0-alpha.0 -* `[PlutoRuntime,PlutoCore]` **Initial implementation of the layer system (formerly known as "stage")** - * A "layer", in this context, is a set of assets bound together - by programming logic. - Layer switching and asset management are handled by the engine. - * Layers can be stacked on top of each other and run sequentially - from bottom to top - * Upon layer switch - 1. Unload unused assets - 2. Load new assets - * Provide multiple means of layer switching - * Two modes with the initial release, asynchronous switching will come at a later date - 1. Instant switch - * Assets will be loaded and unloaded synchronously - * The layer switch will happen in one frame - 2. Deferred switch - * The layer will continue running until all assets load - * Assets will load synchronously, but at a slower pace - to avoid frame stutter - * Automated asset loading - * All asset management will eventually be handled by `PlutoCore` - * This includes audio clips, textures, sprites - * Add a common interface for all assets - * Let the stage system handle audio playback - * This API should be as neutral as possible and avoid steering towards - game-only use - * The stage manager should be relatively low-overhead and allow multiple - instances - * Allow stages to be inherited from, creating a stack-like structure -* `[PlutoAudio]` Integrate the Audio API with the Stage API - -## Features targeted for an unspecified future release -* `[PlutoSpritesheet]` Expanded capabilities - * Support for 9-slice rendering - * Support for animated sprite rendering - * Support for multidirectional sprite rendering - * A spritesheet skeleton editor - -* `[PlutoRuntime]` - * Asynchronous switch - * Assets will be loaded in asynchronously, where applicable - * Falls back to deferred switching for synchronous loading, - such as OpenGL texture upload - -* `[PlutoGUI]` A fully-fledged GUI engine - * Improve font-rendering capabilities - * Subpixel rendering support [?] - * Reimplement support for bitmap fonts -* Improve upon the support of thread-local Pluto instances - * The long term goal is to allow an unlimited amount of Pluto applications at any given time diff --git a/README.md b/README.md index 5ae0f9a..4a07794 100755 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ repositories { dependencies { - implementation group: "org.plutoengine", name: "plutocore", version: "22.2.0.0-alpha.0" + implementation group: "org.plutoengine", name: "plutocore", version: "22.2.0.0-alpha.2" } ``` @@ -41,10 +41,17 @@ repositories { } dependencies { - implementation("org.plutoengine", "plutocore", "22.2.0.0-alpha.0") + implementation("org.plutoengine", "plutocore", "22.2.0.0-alpha.2") } ``` +### Licensing + +While all code of PlutoEngine is licensed under MIT, some media in this repository +is licensed under different terms: + +* Music in the **jsr-clone** demo is made by Selulance, licensed under **CC - BY 3.0** + ### Versioning All submodules share a version number for simplicity reasons. @@ -64,6 +71,9 @@ version numbers.* ## Usability status of submodules +Keep in mind PlutoEngine is in alpha and all features are tentative. +The following list simply provides an overview of how likely breaking changes are to occur. + ### Safe submodules * **PlutoCore** - Stable * **PlutoFramebuffer** - Stable @@ -74,12 +84,12 @@ version numbers.* * **PlutoDisplay** - Stable, collision API nowhere near completion * **PlutoUSS2** - Stable * **PlutoLib** - Mostly stable - + * **PlutoRuntime** - Mostly stable + ### Unstable submodules - * **PlutoGUI** - Recently rewritten, the API is highly unstable - * **PlutoRuntime** - Somewhat tentative, the module API has been rewritten and might contain bugs - * **PlutoAudio** - Somewhat usable, unfinished - + * **PlutoAudio** - Very tentative, work in progress + * **PlutoGUI** - Recently rewritten, the API is highly unstable, work in progress + ## Current priorities @@ -92,12 +102,11 @@ See `NEXT_RELEASE_DRAFT.md` for details. ### Very high priority [ *Implemented in the current release.* ] + * Implement the layer system and integrate all existing systems with it * Improve image loading capabilities, possibly rewrite PlutoLib#TPL ### High priority [ *Implemented in the next release.* ] - * Finish PlutoAudio - * Depends on the stage system * Expand upon the Color API * Color mixing and blending * Color transformation diff --git a/UPDATE_NOTES.md b/UPDATE_NOTES.md index 06aec1c..baf53fe 100755 --- a/UPDATE_NOTES.md +++ b/UPDATE_NOTES.md @@ -1,4 +1,8 @@ ## 22.2.0.0-alpha.2 +* `[SDK]` The libraries now always reference natives for all architectures +* `[SDK]` Replaced `NEXT_RELEASE_DRAFT.md` with [an issue tracker](https://github.com/493msi/plutoengine/issues) +* `[PlutoAudio]` **Partial rewrite and support for managed sound effects** +* `plutoengine-demos/` **Added the `jsr-clone` demo** * `[PlutoSpritesheet]` Renamed `TemporalSprite#getSideCount` to `getFrameCount` ## 22.2.0.0-alpha.1 diff --git a/buildSrc/src/main/kotlin/org/plutoengine/Versions.kt b/buildSrc/src/main/kotlin/org/plutoengine/Versions.kt index 086ef3a..1ee2711 100755 --- a/buildSrc/src/main/kotlin/org/plutoengine/Versions.kt +++ b/buildSrc/src/main/kotlin/org/plutoengine/Versions.kt @@ -5,11 +5,16 @@ import org.gradle.api.JavaVersion object Versions { const val lwjglVersion = "3.3.1" - val lwjglNatives = when (OperatingSystem.current()) { - OperatingSystem.LINUX -> "natives-linux" - OperatingSystem.WINDOWS -> "natives-windows" - else -> throw Error("Unsupported operating system!") - } + val lwjglNatives = listOf( + "natives-linux-arm64", + "natives-linux-arm32", + "natives-linux", + "natives-macos-arm64", + "natives-macos", + "natives-windows-arm64", + "natives-windows", + "natives-windows-x86" + ) const val jomlVersion = "1.10.2" const val jomlPrimitivesVersion = "1.10.0" @@ -23,7 +28,7 @@ object Versions { const val isPrerelease = true const val prereleaseName = "alpha" - const val prerealeaseUpdate = 1 + const val prerealeaseUpdate = 2 val versionFull = if (isPrerelease) diff --git a/engine-core/plutoaudio/build.gradle.kts b/engine-core/plutoaudio/build.gradle.kts index 62b2948..6226e63 100755 --- a/engine-core/plutoaudio/build.gradle.kts +++ b/engine-core/plutoaudio/build.gradle.kts @@ -12,5 +12,7 @@ dependencies { api("org.lwjgl:lwjgl-openal") - runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = Versions.lwjglNatives) + org.plutoengine.Versions.lwjglNatives.forEach { + runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = it) + } } \ No newline at end of file diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/AudioLoader.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/AudioLoader.java index 9b74b96..799d40a 100644 --- a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/AudioLoader.java +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/AudioLoader.java @@ -1,8 +1,5 @@ package org.plutoengine.audio; -import org.lwjgl.stb.STBVorbis; -import org.lwjgl.stb.STBVorbisInfo; -import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; import org.plutoengine.buffer.BufferHelper; import org.plutoengine.logger.Logger; @@ -10,8 +7,6 @@ import org.plutoengine.logger.SmartSeverity; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.IntBuffer; -import java.nio.ShortBuffer; import java.nio.file.Files; import java.nio.file.Path; @@ -23,7 +18,7 @@ public class AudioLoader * medium-sized audio files, however it is discouraged to use such a track * in multiple audio sources at once due to the cost of seeking. */ - public static ISeekableAudioTrack loadMemoryDecoded(Path path) + public static SeekableTrack loadMemoryDecoded(Path path) { Logger.logf(SmartSeverity.AUDIO_PLUS, "Loading audio file: %s\n", path); @@ -45,13 +40,13 @@ public class AudioLoader * for from-memory PCM streaming. Good for frequently used small audio * files. */ - public static ISeekableAudioTrack loadMemoryPCM(Path path) + public static RandomAccessClip loadMemoryPCM(Path path) { Logger.logf(SmartSeverity.AUDIO_PLUS, "Loading audio file: %s\n", path); try { - return new MemoryPCMTrack(path); + return new MemoryPCMClip(path); } catch (IOException e) { @@ -61,7 +56,7 @@ public class AudioLoader } } - private static ByteBuffer loadIntoMemory(Path path) throws IOException + static ByteBuffer loadIntoMemory(Path path) throws IOException { var size = Files.size(path); @@ -73,202 +68,4 @@ public class AudioLoader return BufferHelper.readToByteBuffer(path, readData); } - private abstract static class StreamableTrack implements IAudioStream - { - protected int channels; - protected int sampleRate; - - @Override - public int getChannels() - { - return this.channels; - } - - @Override - public int getSampleRate() - { - return this.sampleRate; - } - } - - private abstract static class SeekableTrack extends StreamableTrack implements ISeekableAudioTrack - { - protected int samplesLength; - - @Override - public int getLengthInSamples() - { - return this.samplesLength; - } - } - - public static class MemoryPCMTrack extends SeekableTrack - { - private final ShortBuffer pcmAudio; - - private int sampleOffset = 0; - - private MemoryPCMTrack(Path path) throws IOException - { - long handle = MemoryUtil.NULL; - ByteBuffer audioBytes = null; - - try (MemoryStack stack = MemoryStack.stackPush()) - { - audioBytes = loadIntoMemory(path); - - IntBuffer error = stack.mallocInt(1); - handle = STBVorbis.stb_vorbis_open_memory(audioBytes, error, null); - - if (handle == MemoryUtil.NULL) - { - this.close(); - throw new IOException(String.format("Failed to load '%s', error code %d.\n", path, error.get(0))); - } - - STBVorbisInfo info = STBVorbisInfo.malloc(stack); - STBVorbis.stb_vorbis_get_info(handle, info); - - this.channels = info.channels(); - this.sampleRate = info.sample_rate(); - - // Downmix to mono, SOUNDS HORRIBLE - // - // this.channels = 1; - // this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(handle) * this.channels; - // this.pcmAudio = MemoryUtil.memAllocShort(this.samplesLength); - // var ptr = stack.pointers(this.pcmAudio); - // STBVorbis.stb_vorbis_get_samples_short(handle, ptr, this.samplesLength); - - this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(handle) * this.channels; - this.pcmAudio = MemoryUtil.memAllocShort(this.samplesLength); - STBVorbis.stb_vorbis_get_samples_short_interleaved(handle, this.channels, this.pcmAudio); - } - catch (IOException e) - { - this.close(); - throw e; - } - finally - { - MemoryUtil.memFree(audioBytes); - - if (handle != MemoryUtil.NULL) - { - STBVorbis.stb_vorbis_close(handle); - } - } - } - - @Override - public void seek(int sampleIndex) - { - this.sampleOffset = sampleIndex * this.getChannels(); - } - - @Override - public synchronized int getSamples(ShortBuffer pcm) - { - this.pcmAudio.limit(Math.min(this.sampleOffset + pcm.remaining(), this.pcmAudio.capacity())); - int read = this.pcmAudio.remaining(); - pcm.put(this.pcmAudio); - this.sampleOffset += read; - pcm.clear(); - - return read / this.getChannels(); - } - - @Override - public int getSampleOffset() - { - return this.sampleOffset / this.getChannels(); - } - - @Override - public void close() - { - MemoryUtil.memFree(this.pcmAudio); - } - } - - public static class MemoryDecodedVorbisTrack extends SeekableTrack - { - protected long handle; - - private final ByteBuffer encodedAudio; - - private MemoryDecodedVorbisTrack(Path path) throws IOException - { - try - { - this.encodedAudio = loadIntoMemory(path); - } - catch (IOException e) - { - this.close(); - throw e; - } - - try (MemoryStack stack = MemoryStack.stackPush()) - { - IntBuffer error = stack.mallocInt(1); - this.handle = STBVorbis.stb_vorbis_open_memory(this.encodedAudio, error, null); - - if (this.handle == MemoryUtil.NULL) - { - this.close(); - throw new IOException(String.format("Failed to load '%s', error code %d.\n", path, error.get(0))); - } - - STBVorbisInfo info = STBVorbisInfo.malloc(stack); - STBVorbis.stb_vorbis_get_info(this.handle, info); - - this.channels = info.channels(); - this.sampleRate = info.sample_rate(); - } - - this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(this.handle); - - Logger.logf(SmartSeverity.AUDIO, """ - \tSample rate:\t%d - \t\tChannels:\t%d - \t\tSamples:\t%d - %n""", this.sampleRate, this.channels, this.samplesLength); - } - - @Override - public synchronized int getSamples(ShortBuffer pcm) - { - if (this.handle == MemoryUtil.NULL) - { - return -1; - } - - return STBVorbis.stb_vorbis_get_samples_short_interleaved(this.handle, this.getChannels(), pcm); - } - - @Override - public void close() - { - MemoryUtil.memFree(this.encodedAudio); - - if (this.handle != MemoryUtil.NULL) - { - STBVorbis.stb_vorbis_close(this.handle); - this.handle = MemoryUtil.NULL; - } - } - - @Override - public synchronized void seek(int sampleIndex) - { - STBVorbis.stb_vorbis_seek(this.handle, sampleIndex); - } - - @Override - public int getSampleOffset() - { - return STBVorbis.stb_vorbis_get_sample_offset(this.handle); - } - } } diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ClipTrack.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ClipTrack.java new file mode 100644 index 0000000..eb2e55f --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ClipTrack.java @@ -0,0 +1,12 @@ +package org.plutoengine.audio; + +public abstract class ClipTrack extends SeekableTrack implements ISeekableClip +{ + protected int samplesLength; + + @Override + public int getLengthInSamples() + { + return this.samplesLength; + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IAudio.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IAudio.java new file mode 100644 index 0000000..2ebdbd5 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IAudio.java @@ -0,0 +1,10 @@ +package org.plutoengine.audio; + +public interface IAudio extends AutoCloseable +{ + int getSampleRate(); + + int getChannels(); + + void close(); +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IAudioStream.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IAudioStream.java deleted file mode 100644 index fb61fab..0000000 --- a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IAudioStream.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.plutoengine.audio; - -import java.nio.ShortBuffer; - -public interface IAudioStream extends AutoCloseable -{ - int getSamples(ShortBuffer pcm); - - int getSampleRate(); - - int getChannels(); - - void close(); -} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IClip.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IClip.java new file mode 100644 index 0000000..3adcbee --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IClip.java @@ -0,0 +1,6 @@ +package org.plutoengine.audio; + +public interface IClip extends IAudio +{ + int getLengthInSamples(); +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IRandomAccessAudio.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IRandomAccessAudio.java new file mode 100644 index 0000000..a6d3b1f --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IRandomAccessAudio.java @@ -0,0 +1,8 @@ +package org.plutoengine.audio; + +import java.nio.ShortBuffer; + +public interface IRandomAccessAudio extends IClip +{ + int getSamples(ShortBuffer pcm, int offset, boolean loopRead); +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableAudioTrack.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableClip.java similarity index 60% rename from engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableAudioTrack.java rename to engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableClip.java index b154067..86c5bd4 100644 --- a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableAudioTrack.java +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableClip.java @@ -1,11 +1,7 @@ package org.plutoengine.audio; -public interface ISeekableAudioTrack extends IAudioStream +public interface ISeekableClip extends ISeekableTrack, IClip { - int getSampleOffset(); - - int getLengthInSamples(); - default void skip(int sampleCount) { this.seek(Math.min(Math.max(0, this.getSampleOffset() + sampleCount), this.getLengthInSamples())); @@ -15,11 +11,4 @@ public interface ISeekableAudioTrack extends IAudioStream { this.seek(Math.round(this.getLengthInSamples() * offset0to1)); } - - default void rewind() - { - this.seek(0); - } - - void seek(int sampleIndex); } diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableTrack.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableTrack.java new file mode 100644 index 0000000..499d3c3 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/ISeekableTrack.java @@ -0,0 +1,11 @@ +package org.plutoengine.audio; + +public interface ISeekableTrack extends IStreamingAudio +{ + void seek(int sampleIndex); + + default void rewind() + { + this.seek(0); + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IStreamingAudio.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IStreamingAudio.java new file mode 100644 index 0000000..80188e4 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/IStreamingAudio.java @@ -0,0 +1,10 @@ +package org.plutoengine.audio; + +import java.nio.ShortBuffer; + +public interface IStreamingAudio extends IAudio +{ + int getSamples(ShortBuffer pcm); + + int getSampleOffset(); +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/MemoryDecodedVorbisTrack.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/MemoryDecodedVorbisTrack.java new file mode 100644 index 0000000..2f7b1ae --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/MemoryDecodedVorbisTrack.java @@ -0,0 +1,95 @@ +package org.plutoengine.audio; + +import org.lwjgl.stb.STBVorbis; +import org.lwjgl.stb.STBVorbisInfo; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.plutoengine.logger.Logger; +import org.plutoengine.logger.SmartSeverity; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.nio.file.Path; + +public class MemoryDecodedVorbisTrack extends ClipTrack +{ + protected long handle; + + private final ByteBuffer encodedAudio; + + MemoryDecodedVorbisTrack(Path path) throws IOException + { + try + { + this.encodedAudio = AudioLoader.loadIntoMemory(path); + } + catch (IOException e) + { + this.close(); + throw e; + } + + try (MemoryStack stack = MemoryStack.stackPush()) + { + IntBuffer error = stack.mallocInt(1); + this.handle = STBVorbis.stb_vorbis_open_memory(this.encodedAudio, error, null); + + if (this.handle == MemoryUtil.NULL) + { + this.close(); + throw new IOException(String.format("Failed to load '%s', error code %d.\n", path, error.get(0))); + } + + STBVorbisInfo info = STBVorbisInfo.malloc(stack); + STBVorbis.stb_vorbis_get_info(this.handle, info); + + this.channels = info.channels(); + this.sampleRate = info.sample_rate(); + } + + this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(this.handle); + + Logger.logf(SmartSeverity.AUDIO, """ + \tSample rate:\t%d + \t\tChannels:\t%d + \t\tSamples:\t%d + %n""", this.sampleRate, this.channels, this.samplesLength); + } + + @Override + public synchronized int getSamples(ShortBuffer pcm) + { + if (this.handle == MemoryUtil.NULL) + { + return -1; + } + + return STBVorbis.stb_vorbis_get_samples_short_interleaved(this.handle, this.getChannels(), pcm); + } + + @Override + public void close() + { + MemoryUtil.memFree(this.encodedAudio); + + if (this.handle != MemoryUtil.NULL) + { + STBVorbis.stb_vorbis_close(this.handle); + this.handle = MemoryUtil.NULL; + } + } + + @Override + public synchronized void seek(int sampleIndex) + { + STBVorbis.stb_vorbis_seek(this.handle, sampleIndex); + } + + @Override + public int getSampleOffset() + { + return STBVorbis.stb_vorbis_get_sample_offset(this.handle); + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/MemoryPCMClip.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/MemoryPCMClip.java new file mode 100644 index 0000000..20423b7 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/MemoryPCMClip.java @@ -0,0 +1,101 @@ +package org.plutoengine.audio; + +import org.lwjgl.stb.STBVorbis; +import org.lwjgl.stb.STBVorbisInfo; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.nio.file.Path; + +public class MemoryPCMClip extends RandomAccessClip +{ + private final ShortBuffer pcmAudio; + + MemoryPCMClip(Path path) throws IOException + { + long handle = MemoryUtil.NULL; + ByteBuffer audioBytes = null; + + try (MemoryStack stack = MemoryStack.stackPush()) + { + audioBytes = AudioLoader.loadIntoMemory(path); + + IntBuffer error = stack.mallocInt(1); + handle = STBVorbis.stb_vorbis_open_memory(audioBytes, error, null); + + if (handle == MemoryUtil.NULL) + { + this.close(); + throw new IOException(String.format("Failed to load '%s', error code %d.\n", path, error.get(0))); + } + + STBVorbisInfo info = STBVorbisInfo.malloc(stack); + STBVorbis.stb_vorbis_get_info(handle, info); + + this.channels = info.channels(); + this.sampleRate = info.sample_rate(); + + // Downmix to mono, SOUNDS HORRIBLE + // + // this.channels = 1; + // this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(handle) * this.channels; + // this.pcmAudio = MemoryUtil.memAllocShort(this.samplesLength); + // var ptr = stack.pointers(this.pcmAudio); + // STBVorbis.stb_vorbis_get_samples_short(handle, ptr, this.samplesLength); + + this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(handle) * this.channels; + this.pcmAudio = MemoryUtil.memAllocShort(this.samplesLength); + STBVorbis.stb_vorbis_get_samples_short_interleaved(handle, this.channels, this.pcmAudio); + } + catch (IOException e) + { + this.close(); + throw e; + } + finally + { + MemoryUtil.memFree(audioBytes); + + if (handle != MemoryUtil.NULL) + { + STBVorbis.stb_vorbis_close(handle); + } + } + } + + @Override + public int getLengthInSamples() + { + return this.samplesLength / this.getChannels(); + } + + @Override + public int getSamples(ShortBuffer pcm, int offset, boolean loopRead) + { + int readTotal = 0; + int read; + + do + { + int thisRemaining = this.pcmAudio.limit() - offset; + read = Math.min(pcm.remaining() - readTotal, thisRemaining); + pcm.put(pcm.position() + readTotal, this.pcmAudio, offset, read); + readTotal += read; + + offset = (offset + read) % (this.getLengthInSamples() * this.channels); + } + while (loopRead && pcm.limit() - readTotal > 0); + + return readTotal / this.getChannels(); + } + + @Override + public void close() + { + MemoryUtil.memFree(this.pcmAudio); + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/RandomAccessClip.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/RandomAccessClip.java new file mode 100644 index 0000000..52f8f7b --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/RandomAccessClip.java @@ -0,0 +1,12 @@ +package org.plutoengine.audio; + +public abstract class RandomAccessClip extends Track implements IRandomAccessAudio, IClip +{ + protected int samplesLength; + + @Override + public int getLengthInSamples() + { + return this.samplesLength; + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/SeekableTrack.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/SeekableTrack.java new file mode 100644 index 0000000..84016a6 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/SeekableTrack.java @@ -0,0 +1,5 @@ +package org.plutoengine.audio; + +public abstract class SeekableTrack extends Track implements ISeekableTrack +{ +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/Track.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/Track.java new file mode 100644 index 0000000..8a224d2 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/Track.java @@ -0,0 +1,19 @@ +package org.plutoengine.audio; + +abstract class Track implements IAudio +{ + protected int channels; + protected int sampleRate; + + @Override + public int getChannels() + { + return this.channels; + } + + @Override + public int getSampleRate() + { + return this.sampleRate; + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioBuffer.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioBuffer.java new file mode 100644 index 0000000..ac1e960 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioBuffer.java @@ -0,0 +1,35 @@ +package org.plutoengine.audio.al; + +import org.lwjgl.openal.AL10; + +import java.nio.ShortBuffer; + +class AudioBuffer implements AutoCloseable +{ + private final int id; + private final int format; + private final int sampleRate; + + AudioBuffer(int id, int format, int sampleRate) + { + this.id = id; + this.format = format; + this.sampleRate = sampleRate; + } + + public int getID() + { + return this.id; + } + + public void writeData(ShortBuffer data) + { + AL10.alBufferData(this.id, this.format, data, this.sampleRate); + } + + @Override + public void close() + { + AL10.alDeleteBuffers(this.id); + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioClipSource.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioClipSource.java new file mode 100644 index 0000000..0ce2c20 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioClipSource.java @@ -0,0 +1,44 @@ +package org.plutoengine.audio.al; + +import org.plutoengine.audio.RandomAccessClip; + +import java.nio.ShortBuffer; + +public class AudioClipSource extends AudioDoubleBufferedSource +{ + private final RandomAccessClip clip; + + private final boolean looping; + + private int readHead = 0; + + public AudioClipSource(RandomAccessClip clip, boolean looping) + { + super(clip); + + this.clip = clip; + this.looping = looping; + } + + public AudioClipSource(RandomAccessClip clip) + { + this(clip, false); + } + + @Override + protected int getSamples(ShortBuffer pcmTransferBuf) + { + var read = this.clip.getSamples(pcmTransferBuf, this.readHead * this.channels, this.looping); + this.readHead += read / this.channels; + + if (this.looping) + this.readHead %= this.clip.getLengthInSamples(); + + return read; + } + + public boolean isLooping() + { + return this.looping; + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioContext.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioContext.java new file mode 100644 index 0000000..7b105e3 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioContext.java @@ -0,0 +1,152 @@ +package org.plutoengine.audio.al; + +import org.joml.Matrix4x3f; +import org.joml.Matrix4x3fc; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.lwjgl.openal.*; +import org.lwjgl.system.MemoryUtil; +import org.plutoengine.Pluto; +import org.plutoengine.component.PlutoLocalComponent; +import org.plutoengine.logger.Logger; +import org.plutoengine.logger.SmartSeverity; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +/** + * @author 493msi + * + */ +public class AudioContext extends PlutoLocalComponent +{ + private long device = MemoryUtil.NULL; + private long context = MemoryUtil.NULL; + private ALCapabilities capabilities; + + private Matrix4x3fc transformation; + + AudioContext() + { + this.transformation = new Matrix4x3f(); + } + + @Override + protected void onMount(ComponentDependencyManager manager) + { + var devicePtr = ALC10.alcOpenDevice((ByteBuffer) null); + if (devicePtr == MemoryUtil.NULL) + { + Logger.log(SmartSeverity.AUDIO_ERROR, "Failed to open the default audio device."); + + // No audio device found, but the game should not crash just because we have no audio + return; + } + + this.device = devicePtr; + + var contextPtr = ALC10.alcCreateContext(devicePtr, (IntBuffer) null); + if (contextPtr == MemoryUtil.NULL) + { + ALC10.alcCloseDevice(devicePtr); + + Logger.log(SmartSeverity.AUDIO_ERROR, "Failed to create an OpenAL context."); + + // The game should not crash just because we have no audio + return; + } + + this.context = contextPtr; + + EXTThreadLocalContext.alcSetThreadContext(contextPtr); + + ALCCapabilities deviceCaps = ALC.createCapabilities(devicePtr); + var alCapabilities = AL.createCapabilities(deviceCaps); + + if (Pluto.DEBUG_MODE) + { + Logger.logf(SmartSeverity.AUDIO, "OpenAL10: %b\n", alCapabilities.OpenAL10); + Logger.logf(SmartSeverity.AUDIO, "OpenAL11: %b\n", alCapabilities.OpenAL11); + Logger.logf(SmartSeverity.AUDIO, "Distance model: %s\n", EnumDistanceModel.getByID(AL11.alGetInteger(AL11.AL_DISTANCE_MODEL))); + } + + this.capabilities = alCapabilities; + + Logger.log(SmartSeverity.AUDIO_PLUS, "Audio engine started."); + } + + public void setTransformation(Matrix4x3fc transformation) + { + this.transformation = transformation; + } + + public Vector3fc transform(Vector3fc vector) + { + return this.transformation.transformPosition(vector, new Vector3f()); + } + + public void setDistanceModel(EnumDistanceModel model) + { + AL10.alDistanceModel(model.getALID()); + } + + public void setSpeedOfSound(float speedOfSound) + { + AL11.alSpeedOfSound(speedOfSound); + } + + public void setSpeed(Vector3fc speed) + { + var tSpeed = this.transformation.transformPosition(speed, new Vector3f()); + AL10.alListener3f(AL10.AL_VELOCITY, tSpeed.x(), tSpeed.y(), tSpeed.z()); + } + + public void setPosition(Vector3f position) + { + var tPosition = this.transformation.transformPosition(position, new Vector3f()); + AL10.alListener3f(AL10.AL_POSITION, tPosition.x(), tPosition.y(), tPosition.z()); + } + + public void setVolume(float volume) + { + AL10.alListenerf(AL10.AL_GAIN, volume); + } + + public void setOrientation(Vector3f at, Vector3f up) + { + float[] data = new float[6]; + data[0] = at.x; + data[1] = at.y; + data[2] = at.z; + data[3] = up.x; + data[4] = up.y; + data[5] = up.z; + AL10.alListenerfv(AL10.AL_ORIENTATION, data); + } + + public boolean isReady() + { + return this.capabilities != null; + } + + @Override + protected void onUnmount() + { + Logger.log(SmartSeverity.AUDIO_MINUS, "Shutting down the audio engine."); + + EXTThreadLocalContext.alcSetThreadContext(MemoryUtil.NULL); + + ALC10.alcDestroyContext(this.context); + ALC10.alcCloseDevice(this.device); + + this.context = MemoryUtil.NULL; + this.device = MemoryUtil.NULL; + this.capabilities = null; + } + + @Override + public boolean isUnique() + { + return true; + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioDoubleBufferedSource.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioDoubleBufferedSource.java new file mode 100644 index 0000000..cc6595f --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioDoubleBufferedSource.java @@ -0,0 +1,185 @@ +package org.plutoengine.audio.al; + +import org.lwjgl.openal.AL10; +import org.lwjgl.openal.SOFTDirectChannels; +import org.lwjgl.system.MemoryUtil; +import org.plutoengine.audio.IAudio; + +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +abstract class AudioDoubleBufferedSource extends AudioSource +{ + private static final int BUFFER_SIZE_PER_CHANNEL = 8192; + private static final int DOUBLE_BUFFER = 2; + + protected final int format; + + protected final int channels; + + protected final int sampleRate; + + private final IntBuffer bufferIDs; + protected final Map buffers; + + protected final ShortBuffer pcmTransferBuf; + + protected boolean audioBufferDepleted; + + protected boolean closed; + + protected AudioDoubleBufferedSource(IAudio audio) + { + this.format = switch (audio.getChannels()) { + case 1 -> AL10.AL_FORMAT_MONO16; + case 2 -> AL10.AL_FORMAT_STEREO16; + default -> throw new UnsupportedOperationException("Unsupported number of channels: " + audio.getChannels()); + }; + + this.channels = audio.getChannels(); + this.sampleRate = audio.getSampleRate(); + + int bufferSize = audio.getChannels() * BUFFER_SIZE_PER_CHANNEL; + + this.pcmTransferBuf = MemoryUtil.memAllocShort(bufferSize); + + this.bufferIDs = MemoryUtil.memCallocInt(DOUBLE_BUFFER); + AL10.alGenBuffers(this.bufferIDs); + this.buffers = IntStream.range(0, this.bufferIDs.limit()) + .mapToObj(i -> new AudioBuffer(this.bufferIDs.get(i), this.format, this.sampleRate)) + .collect(Collectors.toMap(AudioBuffer::getID, Function.identity(), (l, r) -> l)); + + AL10.alSourcei(this.id, SOFTDirectChannels.AL_DIRECT_CHANNELS_SOFT, AL10.AL_TRUE); + } + + public boolean play() + { + if (this.closed) + return false; + + var state = AL10.alGetSourcei(this.id, AL10.AL_SOURCE_STATE); + + return switch (state) + { + case AL10.AL_PLAYING -> true; + case AL10.AL_PAUSED, AL10.AL_STOPPED -> super.play(); + case AL10.AL_INITIAL -> { + this.buffers.values() + .forEach(this::stream); + + yield super.play(); + } + default -> false; // Unexpected value, just say it didn't play + }; + } + + private void stream(AudioBuffer buffer) + { + if (this.audioBufferDepleted) + return; + + this.fillTransferBuffer(); + + if (this.audioBufferDepleted) + return; + + buffer.writeData(this.pcmTransferBuf); + AL10.alSourceQueueBuffers(this.id, buffer.getID()); + } + + private void fillTransferBuffer() + { + this.pcmTransferBuf.clear(); + int samplesPerChannel = this.getSamples(this.pcmTransferBuf); + + if (samplesPerChannel < BUFFER_SIZE_PER_CHANNEL) + { + this.audioBufferDepleted = true; + return; + } + + var samples = samplesPerChannel * this.channels; + this.pcmTransferBuf.limit(samples); + } + + protected abstract int getSamples(ShortBuffer pcmTransferBuf); + + protected List unqueueBuffers() + { + int processed = AL10.alGetSourcei(this.id, AL10.AL_BUFFERS_PROCESSED); + var unqueued = new ArrayList(DOUBLE_BUFFER); + + for (int i = 0; i < processed; i++) + { + int bufID = AL10.alSourceUnqueueBuffers(this.id); + var buffer = this.buffers.get(bufID); + unqueued.add(buffer); + } + + return unqueued; + + } + + public boolean update() + { + if (this.isClosed()) + return false; + + var unqueued = this.unqueueBuffers(); + unqueued.forEach(this::stream); + + if (AL10.alGetSourcei(this.id, AL10.AL_SOURCE_STATE) == AL10.AL_STOPPED) + { + if (this.audioBufferDepleted) + return false; + + if (unqueued.size() == DOUBLE_BUFFER) + return super.play(); + + return false; + } + + return true; + } + + public boolean updateOrClose() + { + boolean shouldClose = !this.update(); + + if (shouldClose) + this.close(); + + return shouldClose; + } + + public boolean isClosed() + { + return this.closed; + } + + @Override + public void close() + { + if (this.isClosed()) + return; + + this.closed = true; + + this.stop(); + + this.unqueueBuffers(); + + super.close(); + + this.buffers.values() + .forEach(AudioBuffer::close); + + MemoryUtil.memFree(this.pcmTransferBuf); + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioEngine.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioEngine.java index bf6502d..e80953f 100644 --- a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioEngine.java +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioEngine.java @@ -1,122 +1,78 @@ package org.plutoengine.audio.al; +import org.apache.commons.lang3.tuple.Pair; import org.joml.Vector3f; -import org.lwjgl.openal.*; -import org.lwjgl.system.MemoryUtil; -import org.plutoengine.Pluto; +import org.plutoengine.component.AbstractComponent; import org.plutoengine.component.ComponentToken; import org.plutoengine.component.PlutoLocalComponent; -import org.plutoengine.logger.Logger; -import org.plutoengine.logger.SmartSeverity; -import java.nio.ByteBuffer; -import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; -/** - * @author 493msi - * - */ public class AudioEngine extends PlutoLocalComponent { - private long device = MemoryUtil.NULL; - private long context = MemoryUtil.NULL; - private ALCapabilities capabilities; - public static final ComponentToken TOKEN = ComponentToken.create(AudioEngine::new); + private AudioContext context; + + private final List> sfx; + + private AudioEngine() { - + this.sfx = new ArrayList<>(); } @Override - protected void onMount(ComponentDependencyManager manager) + protected void onMount(AbstractComponent.ComponentDependencyManager manager) { - var devicePtr = ALC10.alcOpenDevice((ByteBuffer) null); - if (devicePtr == MemoryUtil.NULL) + this.context = manager.declareDependency(ComponentToken.create(AudioContext::new)); + } + + public void update() + { + for (var iterator = this.sfx.listIterator(); iterator.hasNext(); ) { - Logger.log(SmartSeverity.AUDIO_ERROR, "Failed to open the default audio device."); + var data = iterator.next(); + var source = data.getKey(); + var info = data.getValue(); - // No audio device found, but the game should not crash just because we have no audio - return; + var kaFunc = info.keepAliveFunction(); + if (kaFunc != null && !kaFunc.getAsBoolean()) + source.close(); + + var moveFunc = info.moveFunction(); + if (!source.isClosed() && moveFunc != null) + { + var prevPos = source.getPosition(); + var newPos = moveFunc.apply(prevPos); + var velocity = newPos.sub(prevPos, new Vector3f()); + + source.position(this.context, newPos); + source.velocity(this.context, velocity); + } + + if (source.updateOrClose()) + iterator.remove(); } - - this.device = devicePtr; - - var contextPtr = ALC10.alcCreateContext(devicePtr, (IntBuffer) null); - if (contextPtr == MemoryUtil.NULL) - { - ALC10.alcCloseDevice(devicePtr); - - Logger.log(SmartSeverity.AUDIO_ERROR, "Failed to create an OpenAL context."); - - // The game should not crash just because we have no audio - return; - } - - this.context = contextPtr; - - EXTThreadLocalContext.alcSetThreadContext(contextPtr); - - ALCCapabilities deviceCaps = ALC.createCapabilities(devicePtr); - var alCapabilities = AL.createCapabilities(deviceCaps); - - if (Pluto.DEBUG_MODE) - { - Logger.logf(SmartSeverity.AUDIO, "OpenAL10: %b\n", alCapabilities.OpenAL10); - Logger.logf(SmartSeverity.AUDIO, "OpenAL11: %b\n", alCapabilities.OpenAL11); - } - - this.capabilities = alCapabilities; - - Logger.log(SmartSeverity.AUDIO_PLUS, "Audio engine started."); } - public void setSpeed(Vector3f speed) + public void playSound(SoundEffect sfx) { - AL10.alListener3f(AL10.AL_VELOCITY, speed.x, speed.y, speed.z); - } - - public void setPosition(Vector3f position) - { - AL10.alListener3f(AL10.AL_POSITION, position.x, position.y, position.z); - } - - public void setVolume(float volume) - { - AL10.alListenerf(AL10.AL_GAIN, volume); - } - - public void setOrientation(Vector3f at, Vector3f up) - { - float[] data = new float[6]; - data[0] = at.x; - data[1] = at.y; - data[2] = at.z; - data[3] = up.x; - data[4] = up.y; - data[5] = up.z; - AL10.alListenerfv(AL10.AL_ORIENTATION, data); - } - - public boolean isReady() - { - return this.capabilities != null; + var soundEffect = new AudioClipSource(sfx.getClip()); + soundEffect.volume(sfx.getVolume()); + soundEffect.pitch(sfx.getPitch()); + soundEffect.position(this.context, sfx.getPosition()); + soundEffect.play(); + var info = new AudioSourceInfo(sfx.getMovementMapper(), sfx.getKeepAliveFunction()); + var data = Pair.of(soundEffect, info); + this.sfx.add(data); } @Override protected void onUnmount() { - Logger.log(SmartSeverity.AUDIO_MINUS, "Shutting down the audio engine."); - EXTThreadLocalContext.alcSetThreadContext(MemoryUtil.NULL); - - ALC10.alcDestroyContext(this.context); - ALC10.alcCloseDevice(this.device); - - this.context = MemoryUtil.NULL; - this.device = MemoryUtil.NULL; - this.capabilities = null; } @Override @@ -124,4 +80,9 @@ public class AudioEngine extends PlutoLocalComponent { return true; } + + public AudioContext getContext() + { + return this.context; + } } diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioSource.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioSource.java index 7ee3ce9..ed025da 100644 --- a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioSource.java +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioSource.java @@ -1,45 +1,73 @@ package org.plutoengine.audio.al; +import org.jetbrains.annotations.MustBeInvokedByOverriders; import org.joml.Vector3fc; import org.lwjgl.openal.AL10; public abstract class AudioSource implements AutoCloseable { - protected final int source; + protected final int id; + protected Vector3fc position; protected AudioSource() { - this.source = AL10.alGenSources(); + this.id = AL10.alGenSources(); } + @MustBeInvokedByOverriders + public boolean play() + { + AL10.alSourcePlay(this.id); + return true; + } + + @MustBeInvokedByOverriders + public void pause() + { + AL10.alSourcePause(this.id); + } + + @MustBeInvokedByOverriders public void stop() { - AL10.alSourceStop(this.source); + AL10.alSourceStop(this.id); } + @MustBeInvokedByOverriders public void close() { - this.stop(); - AL10.alDeleteSources(this.source); + AL10.alDeleteSources(this.id); } - public void position(Vector3fc pos) + @MustBeInvokedByOverriders + public void position(AudioContext context, Vector3fc pos) { - AL10.alSource3f(this.source, AL10.AL_POSITION, pos.x(), pos.y(), pos.z()); + this.position = pos; + var tPos = context.transform(pos); + AL10.alSource3f(this.id, AL10.AL_POSITION, tPos.x(), tPos.y(), tPos.z()); } - public void velocity(Vector3fc velocity) + public Vector3fc getPosition() { - AL10.alSource3f(this.source, AL10.AL_VELOCITY, velocity.x(), velocity.y(), velocity.z()); + return this.position; } + @MustBeInvokedByOverriders + public void velocity(AudioContext context, Vector3fc velocity) + { + var tVelocity = context.transform(velocity); + AL10.alSource3f(this.id, AL10.AL_VELOCITY, tVelocity.x(), tVelocity.y(), tVelocity.z()); + } + + @MustBeInvokedByOverriders public void pitch(float f) { - AL10.alSourcef(this.source, AL10.AL_PITCH, f); + AL10.alSourcef(this.id, AL10.AL_PITCH, f); } + @MustBeInvokedByOverriders public void volume(float f) { - AL10.alSourcef(this.source, AL10.AL_GAIN, f); + AL10.alSourcef(this.id, AL10.AL_GAIN, f); } } diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioSourceInfo.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioSourceInfo.java new file mode 100644 index 0000000..4aa5e75 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioSourceInfo.java @@ -0,0 +1,13 @@ +package org.plutoengine.audio.al; + +import org.joml.Vector3fc; + +import java.util.function.BooleanSupplier; +import java.util.function.UnaryOperator; + +public record AudioSourceInfo( + UnaryOperator moveFunction, + BooleanSupplier keepAliveFunction +) +{ +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioTrack.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioTrack.java deleted file mode 100644 index 926c435..0000000 --- a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioTrack.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.plutoengine.audio.al; - -import org.lwjgl.openal.AL10; -import org.lwjgl.openal.SOFTDirectChannels; -import org.lwjgl.system.MemoryUtil; -import org.plutoengine.audio.IAudioStream; -import org.plutoengine.audio.ISeekableAudioTrack; - -import java.nio.ShortBuffer; - -public class AudioTrack extends AudioSource -{ - private static final int BUFFER_SIZE_PER_CHANNEL = 16384; - - private final IAudioStream track; - - private final int format; - - private static final int DOUBLE_BUFFER = 2; - - private final int[] buffers; - - private boolean closeOnFinish; - - private final ShortBuffer pcm; - - public AudioTrack(IAudioStream track) - { - this.track = track; - - this.format = switch (track.getChannels()) { - case 1 -> AL10.AL_FORMAT_MONO16; - case 2 -> AL10.AL_FORMAT_STEREO16; - default -> throw new UnsupportedOperationException("Unsupported number of channels: " + track.getChannels()); - }; - - int bufferSize = track.getChannels() * BUFFER_SIZE_PER_CHANNEL; - - this.pcm = MemoryUtil.memAllocShort(bufferSize); - - this.buffers = new int[DOUBLE_BUFFER]; - AL10.alGenBuffers(this.buffers); - - AL10.alSourcei(this.source, SOFTDirectChannels.AL_DIRECT_CHANNELS_SOFT, AL10.AL_TRUE); - } - - @Override - public void close() - { - AL10.alDeleteBuffers(this.buffers); - AL10.alDeleteSources(this.source); - - MemoryUtil.memFree(this.pcm); - } - - public boolean play() - { - if (this.track instanceof ISeekableAudioTrack seekableAudioTrack) - seekableAudioTrack.rewind(); - - for (int buf : this.buffers) - { - this.stream(buf); - } - - AL10.alSourcePlay(this.source); - - return true; - } - - public void setCloseOnFinish() - { - this.closeOnFinish = true; - } - - private void stream(int buffer) - { - this.pcm.clear(); - int samplesPerChannel = this.track.getSamples(this.pcm); - - if (samplesPerChannel == 0) - return; - - var samples = samplesPerChannel * this.track.getChannels(); - this.pcm.limit(samples); - AL10.alBufferData(buffer, this.format, this.pcm, this.track.getSampleRate()); - AL10.alSourceQueueBuffers(this.source, buffer); - } - - public boolean update() - { - int processed = AL10.alGetSourcei(this.source, AL10.AL_BUFFERS_PROCESSED); - - for (int i = 0; i < processed; i++) - { - int buffer = AL10.alSourceUnqueueBuffers(this.source); - this.stream(buffer); - } - - if (AL10.alGetSourcei(this.source, AL10.AL_SOURCE_STATE) == AL10.AL_STOPPED) - { - if (this.closeOnFinish) - { - this.close(); - } - - return false; - } - - return true; - } - -} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioTrackSource.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioTrackSource.java new file mode 100644 index 0000000..ecbb04f --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/AudioTrackSource.java @@ -0,0 +1,30 @@ +package org.plutoengine.audio.al; + +import org.plutoengine.audio.ISeekableTrack; + +import java.nio.ShortBuffer; + +public class AudioTrackSource extends AudioDoubleBufferedSource +{ + private final ISeekableTrack track; + + public AudioTrackSource(ISeekableTrack track) + { + super(track); + + this.track = track; + } + + public boolean play() + { + this.track.rewind(); + + return super.play(); + } + + @Override + protected int getSamples(ShortBuffer pcmTransferBuf) + { + return this.track.getSamples(pcmTransferBuf); + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/EnumDistanceModel.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/EnumDistanceModel.java new file mode 100644 index 0000000..b7b0ae8 --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/EnumDistanceModel.java @@ -0,0 +1,34 @@ +package org.plutoengine.audio.al; + +import org.lwjgl.openal.AL11; + +import java.util.Arrays; + +public enum EnumDistanceModel implements IOpenALEnum +{ + NONE(AL11.AL_NONE), + INVERSE_DISTANCE(AL11.AL_INVERSE_DISTANCE), + INVERSE_DISTANCE_CLAMPED(AL11.AL_INVERSE_DISTANCE_CLAMPED), + LINEAR_DISTANCE(AL11.AL_LINEAR_DISTANCE), + LINEAR_DISTANCE_CLAMPED(AL11.AL_LINEAR_DISTANCE_CLAMPED), + EXPONENT_DISTANCE(AL11.AL_EXPONENT_DISTANCE), + EXPONENT_DISTANCE_CLAMPED(AL11.AL_EXPONENT_DISTANCE_CLAMPED); + + private final int alID; + + EnumDistanceModel(int alID) + { + this.alID = alID; + } + + public static EnumDistanceModel getByID(int id) + { + return Arrays.stream(EnumDistanceModel.values()).filter(model -> model.getALID() == id).findAny().orElse(null); + } + + @Override + public int getALID() + { + return this.alID; + } +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/IOpenALEnum.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/IOpenALEnum.java new file mode 100644 index 0000000..017a11e --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/IOpenALEnum.java @@ -0,0 +1,6 @@ +package org.plutoengine.audio.al; + +public interface IOpenALEnum +{ + int getALID(); +} diff --git a/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/SoundEffect.java b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/SoundEffect.java new file mode 100644 index 0000000..d65a1aa --- /dev/null +++ b/engine-core/plutoaudio/src/main/java/org/plutoengine/audio/al/SoundEffect.java @@ -0,0 +1,86 @@ +package org.plutoengine.audio.al; + +import org.jetbrains.annotations.NotNull; +import org.joml.Vector3fc; +import org.plutoengine.audio.RandomAccessClip; + +import java.util.function.BooleanSupplier; +import java.util.function.UnaryOperator; + +public class SoundEffect +{ + private final @NotNull RandomAccessClip clip; + private @NotNull Vector3fc position; + private float volume; + private float pitch; + private UnaryOperator movementMapper; + private BooleanSupplier keepAliveFunction; + + public SoundEffect(@NotNull RandomAccessClip soundEffect, @NotNull Vector3fc position, float volume) + { + this.clip = soundEffect; + this.position = position; + this.volume = volume; + this.pitch = 1.0f; + } + + public SoundEffect position(Vector3fc position) + { + this.position = position; + return this; + } + + public SoundEffect volume(float volume) + { + this.volume = volume; + return this; + } + + public SoundEffect pitch(float pitch) + { + this.pitch = pitch; + return this; + } + + public SoundEffect movementMapper(UnaryOperator movementMapper) + { + this.movementMapper = movementMapper; + return this; + } + + public SoundEffect keepAliveFunction(BooleanSupplier keepAliveFunction) + { + this.keepAliveFunction = keepAliveFunction; + return this; + } + + @NotNull RandomAccessClip getClip() + { + return this.clip; + } + + @NotNull Vector3fc getPosition() + { + return this.position; + } + + float getVolume() + { + return this.volume; + } + + float getPitch() + { + return this.pitch; + } + + UnaryOperator getMovementMapper() + { + return this.movementMapper; + } + + BooleanSupplier getKeepAliveFunction() + { + return this.keepAliveFunction; + } +} diff --git a/engine-core/plutocore/src/main/java/org/plutoengine/PlutoApplication.java b/engine-core/plutocore/src/main/java/org/plutoengine/PlutoApplication.java index e82b7f3..6a7e345 100644 --- a/engine-core/plutocore/src/main/java/org/plutoengine/PlutoApplication.java +++ b/engine-core/plutocore/src/main/java/org/plutoengine/PlutoApplication.java @@ -281,6 +281,7 @@ public abstract class PlutoApplication this.display.swapBuffers(); + audioEngine.update(); inputBus.resetStates(); this.display.pollEvents(); diff --git a/engine-core/plutodisplay/build.gradle.kts b/engine-core/plutodisplay/build.gradle.kts index 24f9b21..5195c41 100755 --- a/engine-core/plutodisplay/build.gradle.kts +++ b/engine-core/plutodisplay/build.gradle.kts @@ -14,10 +14,13 @@ dependencies { api("org.lwjgl", "lwjgl-glfw") api("org.lwjgl", "lwjgl-opengl") api("org.lwjgl", "lwjgl-stb") - runtimeOnly("org.lwjgl", "lwjgl", classifier = Versions.lwjglNatives) - runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = Versions.lwjglNatives) - runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = Versions.lwjglNatives) - runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = Versions.lwjglNatives) + + org.plutoengine.Versions.lwjglNatives.forEach { + runtimeOnly("org.lwjgl", "lwjgl", classifier = it) + runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = it) + runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = it) + runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = it) + } api("com.code-disaster.steamworks4j", "steamworks4j", Versions.steamworks4jVersion) api("com.code-disaster.steamworks4j", "steamworks4j-server", Versions.steamworks4jServerVersion) diff --git a/engine-core/plutodisplay/src/main/java/org/plutoengine/display/Framerate.java b/engine-core/plutodisplay/src/main/java/org/plutoengine/display/Framerate.java index c98193a..a8b5f5d 100644 --- a/engine-core/plutodisplay/src/main/java/org/plutoengine/display/Framerate.java +++ b/engine-core/plutodisplay/src/main/java/org/plutoengine/display/Framerate.java @@ -16,7 +16,7 @@ public class Framerate private static double animationTimer = 0; - private static double FPS = Double.NaN; + private static double fps = Double.NaN; private static int interpolatedFPS; @@ -31,7 +31,7 @@ public class Framerate public static double getFPS() { - return FPS; + return fps; } public static int getInterpolatedFPS() @@ -55,7 +55,7 @@ public class Framerate animationTimer += frameTimeNs / (double) TimeUnit.SECONDS.toNanos(1); // Maintain precision in case the engine runs for many hours animationTimer %= TimeUnit.DAYS.toMinutes(1); - FPS = TimeUnit.SECONDS.toMillis(1) / frameTime; + fps = TimeUnit.SECONDS.toMillis(1) / frameTime; } var nowMs = System.currentTimeMillis(); @@ -77,7 +77,7 @@ public class Framerate } else { - interpolatedFPS = (int) Math.round(FPS); + interpolatedFPS = (int) Math.round(fps); } lastDraw = now; diff --git a/engine-core/plutogui/build.gradle.kts b/engine-core/plutogui/build.gradle.kts index 08d8435..d469332 100755 --- a/engine-core/plutogui/build.gradle.kts +++ b/engine-core/plutogui/build.gradle.kts @@ -14,7 +14,9 @@ dependencies { implementation("com.fasterxml.jackson.dataformat", "jackson-dataformat-yaml", "2.12.3") implementation("org.lwjgl", "lwjgl-yoga") - runtimeOnly("org.lwjgl", "lwjgl-yoga", classifier = org.plutoengine.Versions.lwjglNatives) + org.plutoengine.Versions.lwjglNatives.forEach { + runtimeOnly("org.lwjgl", "lwjgl-yoga", classifier = it) + } implementation("org.commonmark", "commonmark", "0.18.1") } \ No newline at end of file diff --git a/engine-core/plutogui/src/main/java/org/plutoengine/graphics/PlutoGUIMod.java b/engine-core/plutogui/src/main/java/org/plutoengine/graphics/PlutoGUIMod.java index 0d35d18..9d23487 100644 --- a/engine-core/plutogui/src/main/java/org/plutoengine/graphics/PlutoGUIMod.java +++ b/engine-core/plutogui/src/main/java/org/plutoengine/graphics/PlutoGUIMod.java @@ -1,6 +1,7 @@ package org.plutoengine.graphics; import org.plutoengine.Pluto; +import org.plutoengine.graphics.gui.BitmapFontShader; import org.plutoengine.graphics.gui.FontShader; import org.plutoengine.graphics.texture.MagFilter; import org.plutoengine.graphics.texture.MinFilter; @@ -28,7 +29,7 @@ public class PlutoGUIMod implements IModEntryPoint public static FontShader fontShader; - public static FontShader bitmapFontShader; + public static BitmapFontShader bitmapFontShader; public void onLoad(Mod mod) { @@ -36,7 +37,7 @@ public class PlutoGUIMod implements IModEntryPoint fontShader = new RenderShaderBuilder(mod.getResource("shaders.VertexFontShader#glsl"), mod.getResource("shaders.FragmentFontShader#glsl")).build(FontShader.class, false); - bitmapFontShader = new RenderShaderBuilder(mod.getResource("shaders.VertexBitmapFontShader#glsl"), mod.getResource("shaders.FragmentBitmapFontShader#glsl")).build(FontShader.class, false); + bitmapFontShader = new RenderShaderBuilder(mod.getResource("shaders.VertexBitmapFontShader#glsl"), mod.getResource("shaders.FragmentBitmapFontShader#glsl")).build(BitmapFontShader.class, false); uiElementsAtlas = new RectangleTexture(); uiElementsAtlas.load(mod.getResource("gui.elements#png"), MagFilter.NEAREST, MinFilter.NEAREST, WrapMode.CLAMP_TO_EDGE, WrapMode.CLAMP_TO_EDGE); diff --git a/engine-core/plutogui/src/main/java/org/plutoengine/graphics/gui/BitmapTextShader.java b/engine-core/plutogui/src/main/java/org/plutoengine/graphics/gui/BitmapFontShader.java similarity index 97% rename from engine-core/plutogui/src/main/java/org/plutoengine/graphics/gui/BitmapTextShader.java rename to engine-core/plutogui/src/main/java/org/plutoengine/graphics/gui/BitmapFontShader.java index 01ff081..09f3e2f 100644 --- a/engine-core/plutogui/src/main/java/org/plutoengine/graphics/gui/BitmapTextShader.java +++ b/engine-core/plutogui/src/main/java/org/plutoengine/graphics/gui/BitmapFontShader.java @@ -13,7 +13,7 @@ import org.plutoengine.shader.uniform.auto.AutoViewportProjection; import org.plutoengine.util.color.IRGBA; @ShaderProgram -public final class BitmapTextShader extends ShaderBase implements IGUIShader +public final class BitmapFontShader extends ShaderBase implements IGUIShader { @AutoViewportProjection @Uniform(name = "projection") diff --git a/engine-core/plutoruntime/build.gradle.kts b/engine-core/plutoruntime/build.gradle.kts index 4265f44..64a9464 100755 --- a/engine-core/plutoruntime/build.gradle.kts +++ b/engine-core/plutoruntime/build.gradle.kts @@ -38,7 +38,10 @@ dependencies { implementation("org.lwjgl:lwjgl") implementation("org.lwjgl:lwjgl-xxhash") implementation("org.lwjgl:lwjgl-zstd") - runtimeOnly("org.lwjgl", "lwjgl", classifier = Versions.lwjglNatives) - runtimeOnly("org.lwjgl", "lwjgl-xxhash", classifier = Versions.lwjglNatives) - runtimeOnly("org.lwjgl", "lwjgl-zstd", classifier = Versions.lwjglNatives) + + Versions.lwjglNatives.forEach { + runtimeOnly("org.lwjgl", "lwjgl", classifier = it) + runtimeOnly("org.lwjgl", "lwjgl-xxhash", classifier = it) + runtimeOnly("org.lwjgl", "lwjgl-zstd", classifier = it) + } } \ No newline at end of file diff --git a/engine-demo/jsr-clone/build.gradle.kts b/engine-demo/jsr-clone/build.gradle.kts new file mode 100755 index 0000000..0a0665d --- /dev/null +++ b/engine-demo/jsr-clone/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("edu.sc.seis.launch4j") version "2.5.3" +} + +application { + mainClass.set("cz.tefek.srclone.Main") +} + +launch4j { + mainClassName = "cz.tefek.srclone.Main" + bundledJrePath = "jre" +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType { + jvmArgs = listOf( + "-Dcz.tefek.pluto.debug=true", + "-Dorg.lwjgl.util.Debug=true" + ) +} + +distributions { + main { + contents { + from("mods") { + into("mods") + } + } + } +} + +dependencies { + implementation(project(":plutoengine:plutocore")) +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/glfw/info.json b/engine-demo/jsr-clone/mods/glfw/info.json new file mode 100755 index 0000000..3bb8187 --- /dev/null +++ b/engine-demo/jsr-clone/mods/glfw/info.json @@ -0,0 +1,8 @@ +{ + "displayName": "GLFW", + "author": "The GLFW team", + "description": "The GLFW library, used for native window creation.", + "resourceRoots": { + + } +} diff --git a/engine-demo/jsr-clone/mods/lwjgl/info.json b/engine-demo/jsr-clone/mods/lwjgl/info.json new file mode 100755 index 0000000..e4a2c52 --- /dev/null +++ b/engine-demo/jsr-clone/mods/lwjgl/info.json @@ -0,0 +1,8 @@ +{ + "displayName": "LWJGL", + "description": "Lightweight Java Game Library", + "author": "The LWJGL team", + "resourceRoots": { + + } +} diff --git a/engine-demo/jsr-clone/mods/tefek.plutogui/default/gui/elements.png b/engine-demo/jsr-clone/mods/tefek.plutogui/default/gui/elements.png new file mode 100644 index 0000000..17a3698 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.plutogui/default/gui/elements.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/FragmentBitmapFontShader.glsl b/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/FragmentBitmapFontShader.glsl new file mode 100644 index 0000000..4b5806d --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/FragmentBitmapFontShader.glsl @@ -0,0 +1,75 @@ +#version 330 core + +in vec2 uvCoordinates; +in vec2 paintUVCoordinates; + +uniform sampler2DRect textureSampler; + +uniform int paintType; +uniform vec4 paintColor; + +uniform int paintGradientStopCount; +uniform vec4[16] paintGradientColors; +uniform float[16] paintGradientPositions; +uniform vec2[2] paintGradientEnds; + +out vec4 out_Color; + +vec4 solidColor(void) +{ + return paintColor; +} + +vec4 gammaMix(vec4 lCol, vec4 rCol, float ratio) +{ + float gamma = 2.2; + float one_over_gamma = 1 / gamma; + + vec4 ilCol = vec4(pow(lCol.r, gamma), pow(lCol.g, gamma), pow(lCol.b, gamma), lCol.a); + vec4 irCol = vec4(pow(rCol.r, gamma), pow(rCol.g, gamma), pow(rCol.b, gamma), rCol.a); + + vec4 fCol = mix(ilCol, irCol, ratio); + + return vec4(pow(fCol.r, one_over_gamma), pow(fCol.g, one_over_gamma), pow(fCol.b, one_over_gamma), fCol.a); +} + +vec4 gradientColor(void) +{ + float angle = atan(-paintGradientEnds[1].y + paintGradientEnds[0].y, paintGradientEnds[1].x - paintGradientEnds[0].x); + float rotatedStartX = paintGradientEnds[0].x * cos(angle) - paintGradientEnds[0].y * sin(angle); + float rotatedEndX = paintGradientEnds[1].x * cos(angle) - paintGradientEnds[1].y * sin(angle); + float d = rotatedEndX - rotatedStartX; + + float pX = paintUVCoordinates.x * cos(angle) - paintUVCoordinates.y * sin(angle); + + float mr = smoothstep(rotatedStartX + paintGradientPositions[0] * d, rotatedStartX + paintGradientPositions[1] * d, pX); + vec4 col = gammaMix(paintGradientColors[0], paintGradientColors[1], mr); + + for (int i = 1; i < paintGradientStopCount - 1; i++) + { + mr = smoothstep(rotatedStartX + paintGradientPositions[i] * d, rotatedStartX + paintGradientPositions[i + 1] * d, pX); + col = gammaMix(col, paintGradientColors[i + 1], mr); + } + + return col; +} + +void main(void) +{ + vec4 col; + + switch (paintType) + { + case 0: + col = solidColor(); + break; + + case 1: + col = gradientColor(); + break; + } + + col.rgba *= texture(textureSampler, uvCoordinates); + + out_Color = col; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/FragmentFontShader.glsl b/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/FragmentFontShader.glsl new file mode 100644 index 0000000..b71866c --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/FragmentFontShader.glsl @@ -0,0 +1,84 @@ +#version 330 core + +in vec2 uvCoordinates; +flat in int atlasPage; +in vec2 paintUVCoordinates; + +uniform sampler2DArray textureSampler; + +uniform int paintType; +uniform vec4 paintColor; + +uniform int paintGradientStopCount; +uniform vec4[16] paintGradientColors; +uniform float[16] paintGradientPositions; +uniform vec2[2] paintGradientEnds; + +uniform float pxScale; + +out vec4 out_Color; + +vec4 solidColor(void) +{ + return paintColor; +} + +vec4 gammaMix(vec4 lCol, vec4 rCol, float ratio) +{ + float gamma = 2.2; + float one_over_gamma = 1 / gamma; + + vec4 ilCol = vec4(pow(lCol.r, gamma), pow(lCol.g, gamma), pow(lCol.b, gamma), lCol.a); + vec4 irCol = vec4(pow(rCol.r, gamma), pow(rCol.g, gamma), pow(rCol.b, gamma), rCol.a); + + vec4 fCol = mix(ilCol, irCol, ratio); + + return vec4(pow(fCol.r, one_over_gamma), pow(fCol.g, one_over_gamma), pow(fCol.b, one_over_gamma), fCol.a); +} + +vec4 gradientColor(void) +{ + float angle = atan(-paintGradientEnds[1].y + paintGradientEnds[0].y, paintGradientEnds[1].x - paintGradientEnds[0].x); + float rotatedStartX = paintGradientEnds[0].x * cos(angle) - paintGradientEnds[0].y * sin(angle); + float rotatedEndX = paintGradientEnds[1].x * cos(angle) - paintGradientEnds[1].y * sin(angle); + float d = rotatedEndX - rotatedStartX; + + float pX = paintUVCoordinates.x * cos(angle) - paintUVCoordinates.y * sin(angle); + + float mr = smoothstep(rotatedStartX + paintGradientPositions[0] * d, rotatedStartX + paintGradientPositions[1] * d, pX); + vec4 col = gammaMix(paintGradientColors[0], paintGradientColors[1], mr); + + for (int i = 1; i < paintGradientStopCount - 1; i++) + { + mr = smoothstep(rotatedStartX + paintGradientPositions[i] * d, rotatedStartX + paintGradientPositions[i + 1] * d, pX); + col = gammaMix(col, paintGradientColors[i + 1], mr); + } + + return col; +} + +void main(void) +{ + vec3 texCoords = vec3(uvCoordinates, atlasPage); + + float threshold = 180.0 / 255.0 - 5.0 / pow(pxScale, 1.6); // Also help small text be readable + + float signedDist = texture(textureSampler, texCoords).r - threshold; + + vec4 col; + + switch (paintType) + { + case 0: + col = solidColor(); + break; + + case 1: + col = gradientColor(); + break; + } + + col.a *= smoothstep(0, 2.4 / pxScale, signedDist); + + out_Color = col; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/VertexBitmapFontShader.glsl b/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/VertexBitmapFontShader.glsl new file mode 100644 index 0000000..d38454d --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/VertexBitmapFontShader.glsl @@ -0,0 +1,19 @@ +#version 330 core + +in vec2 position; +in vec2 uvCoords; +in vec2 paintUVCoords; + +out vec2 uvCoordinates; +out vec2 paintUVCoordinates; + +uniform mat4 projection; +uniform mat3 transformation; + +void main(void) +{ + uvCoordinates = uvCoords; + paintUVCoordinates = paintUVCoords; + vec3 transformed = vec3((transformation * vec3(position, 1.0)).xy, 0.0); + gl_Position = projection * vec4(transformed, 1.0); +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/VertexFontShader.glsl b/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/VertexFontShader.glsl new file mode 100644 index 0000000..4a66efc --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutogui/default/shaders/VertexFontShader.glsl @@ -0,0 +1,22 @@ +#version 330 core + +in vec2 position; +in vec2 uvCoords; +in int page; +in vec2 paintUVCoords; + +out vec2 uvCoordinates; +out vec2 paintUVCoordinates; +flat out int atlasPage; + +uniform mat4 projection; +uniform mat3 transformation; + +void main(void) +{ + atlasPage = page; + uvCoordinates = uvCoords; + paintUVCoordinates = paintUVCoords; + vec3 transformed = vec3((transformation * vec3(position, 1.0)).xy, 0.0); + gl_Position = projection * vec4(transformed, 1.0); +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutogui/info.json b/engine-demo/jsr-clone/mods/tefek.plutogui/info.json new file mode 100755 index 0000000..ec2d942 --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutogui/info.json @@ -0,0 +1,11 @@ +{ + "displayName": "Pluto Engine GUI Renderer", + "description": "", + "author": "Tefek", + "resourceRoots": { + "default": { + "path": "default", + "type": "open" + } + } +} diff --git a/engine-demo/jsr-clone/mods/tefek.plutoshader/info.json b/engine-demo/jsr-clone/mods/tefek.plutoshader/info.json new file mode 100755 index 0000000..81c253e --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutoshader/info.json @@ -0,0 +1,7 @@ +{ + "displayName": "Pluto Shader", + "description": "PlutoEngine's shader manager.", + "author": "Tefek", + "resourceRoots": { + } +} diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentColor2D.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentColor2D.glsl new file mode 100755 index 0000000..1294253 --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentColor2D.glsl @@ -0,0 +1,10 @@ +#version 330 core + +uniform vec4 color; + +out vec4 out_Color; + +void main(void) +{ + out_Color = color; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentRectangle2D.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentRectangle2D.glsl new file mode 100755 index 0000000..bc16e2a --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentRectangle2D.glsl @@ -0,0 +1,16 @@ +#version 330 core + +in vec2 uvCoordinates; + +uniform sampler2DRect textureSampler; + +uniform vec4 recolor; + +out vec4 out_Color; + +void main(void) +{ + vec4 color = texture(textureSampler, uvCoordinates) * recolor; + + out_Color = color; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentRectangleWorld2D.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentRectangleWorld2D.glsl new file mode 100755 index 0000000..bc16e2a --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentRectangleWorld2D.glsl @@ -0,0 +1,16 @@ +#version 330 core + +in vec2 uvCoordinates; + +uniform sampler2DRect textureSampler; + +uniform vec4 recolor; + +out vec4 out_Color; + +void main(void) +{ + vec4 color = texture(textureSampler, uvCoordinates) * recolor; + + out_Color = color; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentSpriteSheet.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentSpriteSheet.glsl new file mode 100755 index 0000000..bc16e2a --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/FragmentSpriteSheet.glsl @@ -0,0 +1,16 @@ +#version 330 core + +in vec2 uvCoordinates; + +uniform sampler2DRect textureSampler; + +uniform vec4 recolor; + +out vec4 out_Color; + +void main(void) +{ + vec4 color = texture(textureSampler, uvCoordinates) * recolor; + + out_Color = color; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexColor2D.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexColor2D.glsl new file mode 100755 index 0000000..e1f61ea --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexColor2D.glsl @@ -0,0 +1,11 @@ +#version 330 core + +in vec2 position; + +uniform mat4 projection; +uniform mat3x2 transformation; + +void main(void) +{ + gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0); +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexRectangle2D.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexRectangle2D.glsl new file mode 100755 index 0000000..2a4fe41 --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexRectangle2D.glsl @@ -0,0 +1,19 @@ +#version 330 core + +in vec2 position; +in vec2 uvCoords; + +out vec2 uvCoordinates; + +uniform mat4 projection; +uniform mat3x2 transformation; + +uniform vec2 uvBase; +uniform vec2 uvDelta; + +void main(void) +{ + gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0); + + uvCoordinates = uvBase + uvCoords * uvDelta; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexRectangleWorld2D.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexRectangleWorld2D.glsl new file mode 100755 index 0000000..2a4fe41 --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexRectangleWorld2D.glsl @@ -0,0 +1,19 @@ +#version 330 core + +in vec2 position; +in vec2 uvCoords; + +out vec2 uvCoordinates; + +uniform mat4 projection; +uniform mat3x2 transformation; + +uniform vec2 uvBase; +uniform vec2 uvDelta; + +void main(void) +{ + gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0); + + uvCoordinates = uvBase + uvCoords * uvDelta; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexSpriteSheet.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexSpriteSheet.glsl new file mode 100755 index 0000000..2a4fe41 --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/VertexSpriteSheet.glsl @@ -0,0 +1,19 @@ +#version 330 core + +in vec2 position; +in vec2 uvCoords; + +out vec2 uvCoordinates; + +uniform mat4 projection; +uniform mat3x2 transformation; + +uniform vec2 uvBase; +uniform vec2 uvDelta; + +void main(void) +{ + gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0); + + uvCoordinates = uvBase + uvCoords * uvDelta; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/f2D.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/f2D.glsl new file mode 100755 index 0000000..69af927 --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/f2D.glsl @@ -0,0 +1,16 @@ +#version 330 core + +in vec2 uvCoordinates; + +uniform sampler2D textureSampler; + +uniform vec4 recolor; + +out vec4 out_Color; + +void main(void) +{ + vec4 color = texture(textureSampler, uvCoordinates) * recolor; + + out_Color = color; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/v2D.glsl b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/v2D.glsl new file mode 100755 index 0000000..59fe8d1 --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/default/shaders/v2D.glsl @@ -0,0 +1,19 @@ +#version 330 core + +in vec2 position; +in vec2 uvCoords; + +out vec2 uvCoordinates; + +uniform mat4 projection; +uniform mat3x2 transformation; + +uniform vec2 uvBase; +uniform vec2 uvDelta; + +void main(void) +{ + gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0); + + uvCoordinates = uvBase + uvCoords * uvDelta; +} \ No newline at end of file diff --git a/engine-demo/jsr-clone/mods/tefek.plutospritesheet/info.json b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/info.json new file mode 100755 index 0000000..f330c93 --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.plutospritesheet/info.json @@ -0,0 +1,11 @@ +{ + "displayName": "Pluto SpriteSheet", + "description": "A library to manage, store and draw sprites.", + "author": "Tefek", + "resourceRoots": { + "default": { + "path": "default", + "type": "open" + } + } +} diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/engine.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/engine.ogg new file mode 100755 index 0000000..3f9681d Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/engine.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion0.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion0.ogg new file mode 100755 index 0000000..0b1ba8d Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion0.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion1.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion1.ogg new file mode 100755 index 0000000..dc689e1 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion1.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion2.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion2.ogg new file mode 100755 index 0000000..0f59202 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion2.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion3.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion3.ogg new file mode 100755 index 0000000..4a30ed9 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion3.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion4.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion4.ogg new file mode 100755 index 0000000..7bb4b9e Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/explosion4.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/game_st.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/game_st.ogg new file mode 100755 index 0000000..4ba5b77 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/game_st.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/hit.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/hit.ogg new file mode 100755 index 0000000..9c4217f Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/hit.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/electronFlare.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/electronFlare.ogg new file mode 100644 index 0000000..927bf97 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/electronFlare.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/heatStar.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/heatStar.ogg new file mode 100644 index 0000000..fef7a51 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/heatStar.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/laser.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/laser.ogg new file mode 100644 index 0000000..078f7f5 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/laser.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/plasma.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/plasma.ogg new file mode 100644 index 0000000..c7c9482 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/plasma.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/polySwarm.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/polySwarm.ogg new file mode 100644 index 0000000..d19a4a4 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/polySwarm.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/tachyon.ogg b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/tachyon.ogg new file mode 100644 index 0000000..4873349 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/sound/shoot/tachyon.ogg differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/box.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/box.png new file mode 100644 index 0000000..f1d6936 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/box.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/e_scout.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/e_scout.png new file mode 100644 index 0000000..c090931 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/e_scout.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/e_small_bomber.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/e_small_bomber.png new file mode 100644 index 0000000..def51ee Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/e_small_bomber.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/ship.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/ship.png new file mode 100644 index 0000000..07bbb16 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/entities/ship.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/explosion1.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/explosion1.png new file mode 100644 index 0000000..43a8e36 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/explosion1.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/impact1.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/impact1.png new file mode 100644 index 0000000..d02c30c Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/impact1.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/projectilesBase.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/projectilesBase.png new file mode 100644 index 0000000..87b9f42 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/projectilesBase.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/rocketNozzle.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/rocketNozzle.png new file mode 100755 index 0000000..87247bb Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/particles/rocketNozzle.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/blue.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/blue.png new file mode 100755 index 0000000..1e5bdf1 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/blue.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/red.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/red.png new file mode 100755 index 0000000..bfc8ea2 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/red.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/redDwarf.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/redDwarf.png new file mode 100755 index 0000000..de4cd53 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/redDwarf.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/whiteDwarf.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/whiteDwarf.png new file mode 100755 index 0000000..c7ffdc3 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/whiteDwarf.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/yellow.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/yellow.png new file mode 100755 index 0000000..515d60f Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/stars/yellow.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/ui/youDied.png b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/ui/youDied.png new file mode 100755 index 0000000..cf91f58 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/default/textures/ui/youDied.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/icons/splash.png b/engine-demo/jsr-clone/mods/tefek.srclone/icons/splash.png new file mode 100755 index 0000000..fca5ad9 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/icons/splash.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/info.json b/engine-demo/jsr-clone/mods/tefek.srclone/info.json new file mode 100755 index 0000000..b717b1a --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.srclone/info.json @@ -0,0 +1,19 @@ +{ + "displayName": "SRClone", + "description": "Java reimplementation of SRClone.", + "author": "Tefek", + "resourceRoots": { + "default": { + "path": "default", + "type": "open" + }, + "icons": { + "path": "icons", + "type": "open" + }, + "plutofonts": { + "path": "plutofonts", + "type": "open" + } + } +} diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/plutostardust.ttf b/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/plutostardust.ttf new file mode 100755 index 0000000..543960a Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/plutostardust.ttf differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/srclone/font.png b/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/srclone/font.png new file mode 100755 index 0000000..dc44b21 Binary files /dev/null and b/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/srclone/font.png differ diff --git a/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/srclone/info.yaml b/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/srclone/info.yaml new file mode 100644 index 0000000..83b0f4b --- /dev/null +++ b/engine-demo/jsr-clone/mods/tefek.srclone/plutofonts/srclone/info.yaml @@ -0,0 +1,249 @@ +name: SRClone +meta: + scale: 8 + ascent: 8 + descent: 0 + lineGap: 1 +atlas: "pluto+asl://!font#png" +filtering: NEAREST +kern: + - + left: 0 + right: 0 + offset: 0 + +glyphs: + - + cp: 48 # 0 + sprite: + x: 0 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 49 # 1 + sprite: + x: 8 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 50 # 2 + sprite: + x: 16 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 51 # 3 + sprite: + x: 24 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 52 # 4 + sprite: + x: 32 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 53 # 5 + sprite: + x: 40 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 54 # 6 + sprite: + x: 48 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 55 # 7 + sprite: + x: 56 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 56 # 8 + sprite: + x: 64 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 57 # 9 + sprite: + x: 72 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 57 # 9 + sprite: + x: 72 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 43 # + + sprite: + x: 80 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 45 # + + sprite: + x: 88 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 46 # . + sprite: + x: 96 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 120 # x + sprite: + x: 104 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 128150 # 💖 - sparkling heart because the default red one is two codepoints :( + sprite: + x: 112 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 105 # i + sprite: + x: 120 + y: 0 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 83 # S + sprite: + x: 0 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 127776 # 🌠 - shooting star + sprite: + x: 8 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 128994 # 🟢 - large green circle + sprite: + x: 16 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 9728 # ☀ - sun + sprite: + x: 24 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 128314 # 🔺 - red up triangle + sprite: + x: 32 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 128160 # 💠 - diamond + sprite: + x: 40 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 121171 # 𝥓 - spiral arrow thingy + sprite: + x: 48 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 8734 # ∞ - infinity + sprite: + x: 112 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 + - + cp: 32 # space + sprite: + x: 120 + y: 8 + w: 8 + h: 8 + leftBearing: 0 + advance: 8 \ No newline at end of file diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/EnumTeam.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/EnumTeam.java new file mode 100644 index 0000000..d891afd --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/EnumTeam.java @@ -0,0 +1,7 @@ +package cz.tefek.srclone; + +public enum EnumTeam +{ + PLAYER, + ENEMY +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/Game.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/Game.java new file mode 100755 index 0000000..548b2a7 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/Game.java @@ -0,0 +1,252 @@ +package cz.tefek.srclone; + +import org.lwjgl.glfw.GLFW; +import org.plutoengine.Pluto; +import org.plutoengine.PlutoLocal; +import org.plutoengine.display.Display; +import org.plutoengine.graphics.ImmediateFontRenderer; +import org.plutoengine.libra.paint.LiPaint; +import org.plutoengine.libra.text.shaping.TextStyleOptions; +import org.plutoengine.logger.Logger; +import org.plutoengine.util.color.Color; + +import java.util.*; + +import cz.tefek.srclone.ammo.EnumAmmo; +import cz.tefek.srclone.audio.SRAudioEngine; +import cz.tefek.srclone.entity.Entity; +import cz.tefek.srclone.entity.EntityPlayer; +import cz.tefek.srclone.entity.projectile.EntityProjectile; +import cz.tefek.srclone.particle.Particle; + +public class Game +{ + private float camX; + private float camY; + + private float viewX; + private float viewY; + + private float deathScreenAnimation; + + private final List entities; + private final List projectiles; + private final List particles; + + private final EntityPlayer entityPlayer; + + private final List newEntities; + private final List newProjectiles; + private final List newParticles; + + private final Random random; + + private final SRAudioEngine audioEngine; + + private final GameDirector director; + + public Game() + { + Logger.log("==== NEW GAME ===="); + + this.camX = 0; + this.camY = 0; + + this.entities = new ArrayList<>(); + this.newEntities = new ArrayList<>(); + + this.projectiles = new ArrayList<>(); + this.newProjectiles = new ArrayList<>(); + + this.entityPlayer = new EntityPlayer(); + this.entityPlayer.init(this, 0.0f, 0.0f); + this.entityPlayer.addAmmo(EnumAmmo.P_LASER_BEAM, EnumAmmo.AMMO_INFINITE); + + // DEBUG + + if (Pluto.DEBUG_MODE) + { + Arrays.stream(EnumAmmo.values()).forEach(a -> this.entityPlayer.addAmmo(a, EnumAmmo.AMMO_INFINITE)); + } + + this.particles = new ArrayList<>(); + this.newParticles = new ArrayList<>(); + + this.random = new Random(); + + this.audioEngine = new SRAudioEngine(); + + this.director = new GameDirector(this); + } + + public void tick(double frameTime) + { + if (frameTime != 0 && !Double.isNaN(frameTime)) + { + var ft = (float) frameTime; + + if (!this.isOver()) + { + this.entities.forEach(e -> e.tick(ft)); + this.projectiles.forEach(p -> p.tick(ft)); + this.entityPlayer.tick(ft); + + this.director.tick(frameTime); + + this.entities.removeIf(Entity::isDead); + this.entities.addAll(this.newEntities); + this.newEntities.clear(); + + this.projectiles.removeIf(EntityProjectile::isDead); + this.projectiles.addAll(this.newProjectiles); + this.newProjectiles.clear(); + } + + this.particles.forEach(p -> p.tick(ft)); + this.particles.removeIf(Particle::isDead); + this.particles.addAll(this.newParticles); + this.newParticles.clear(); + } + + if (this.isOver()) + { + this.deathScreenAnimation += frameTime; + this.audioEngine.stopMusic(this); + } + + this.audioEngine.tick(this); + + this.render(); + } + + private void render() + { + this.camX = -this.entityPlayer.getX(); + this.camY = -this.entityPlayer.getY(); + + var display = PlutoLocal.components().getComponent(Display.class); + this.viewX = this.camX + display.getWidth() / 2.0f; + this.viewY = this.camY + display.getHeight() / 2.0f; + + SRCloneMod.starField.render(this.viewX, this.viewY); + + this.entities.forEach(Entity::render); + + if (!this.isOver()) + this.entityPlayer.render(); + + this.projectiles.forEach(Entity::render); + + this.particles.forEach(Particle::render); + + var selectedAmmo = this.entityPlayer.getSelectedAmmo(); + long ammoCnt = this.entityPlayer.getAmmo(selectedAmmo); + + String ammoStr; + + if (ammoCnt == EnumAmmo.AMMO_INFINITE) + ammoStr = "%sx∞"; + else + ammoStr = "%%sx%06d".formatted(ammoCnt); + + var ammoFont = new TextStyleOptions(24) + .setVerticalAlign(TextStyleOptions.TextAlign.START) + .setPaint(LiPaint.solidColor(Color.WHITE)); + + ImmediateFontRenderer.drawString(5.0f, display.getHeight() - 58.0f, ammoStr.formatted(selectedAmmo.getTextIcon()), SRCloneMod.srCloneFont, ammoFont); + + var healthFont = new TextStyleOptions(48) + .setVerticalAlign(TextStyleOptions.TextAlign.START) + .setPaint(LiPaint.solidColor(Color.WHITE)); + ImmediateFontRenderer.drawString(5.0f, display.getHeight() - 5.0f, "\uD83D\uDC96%03.0f".formatted(this.entityPlayer.getHealth()), SRCloneMod.srCloneFont, healthFont); + + var scoreFont = new TextStyleOptions(24) + .setPaint(LiPaint.solidColor(Color.WHITE)); + ImmediateFontRenderer.drawString(5.0f, 5.0f, ("S %010.0f").formatted(this.entityPlayer.getScore()), SRCloneMod.srCloneFont, scoreFont); + + if (this.deathScreenAnimation > 1.0f) + { + var youDiedStyle = new TextStyleOptions(64) + .setHorizontalAlign(TextStyleOptions.TextAlign.CENTER) + .setVerticalAlign(TextStyleOptions.TextAlign.START) + .setPaint(LiPaint.solidColor(Color.CRIMSON)); + ImmediateFontRenderer.drawString(display.getWidth() / 2.0f, display.getHeight() / 2.0f - 6.0f, "You Died!", SRCloneMod.font, youDiedStyle); + youDiedStyle.setPaint(LiPaint.solidColor(Color.WHITE)); + ImmediateFontRenderer.drawString(display.getWidth() / 2.0f, display.getHeight() / 2.0f - 8.0f, "You Died!", SRCloneMod.font, youDiedStyle); + + var playAgainKey = String.valueOf(GLFW.glfwGetKeyName(GLFW.GLFW_KEY_R, GLFW.glfwGetKeyScancode(GLFW.GLFW_KEY_R))); + var playAgainStr = "Press [%s] to play again...".formatted(playAgainKey.toUpperCase(Locale.ROOT)); + var playAgainStyle = new TextStyleOptions(40) + .setHorizontalAlign(TextStyleOptions.TextAlign.CENTER) + .setVerticalAlign(TextStyleOptions.TextAlign.END) + .setPaint(LiPaint.solidColor(Color.CRIMSON)); + ImmediateFontRenderer.drawString(display.getWidth() / 2.0f, display.getHeight() / 2.0f + 20.0f, playAgainStr, SRCloneMod.font, playAgainStyle); + playAgainStyle.setPaint(LiPaint.solidColor(Color.WHITE)); + ImmediateFontRenderer.drawString(display.getWidth() / 2.0f, display.getHeight() / 2.0f + 18.0f, playAgainStr, SRCloneMod.font, playAgainStyle); + } + } + + public SRAudioEngine getAudioEngine() + { + return this.audioEngine; + } + + public void addEntity(Entity entity, float x, float y) + { + if (entity instanceof EntityProjectile projectile) + { + this.addProjectile(projectile, x, y); + return; + } + + this.newEntities.add(entity); + entity.init(this, x, y); + } + + public void addParticle(Particle particle, float x, float y) + { + this.newParticles.add(particle); + particle.init(this, x, y); + } + + private void addProjectile(EntityProjectile projectile, float x, float y) + { + this.newProjectiles.add(projectile); + projectile.init(this, x, y); + } + + public boolean isOver() + { + return this.entityPlayer.isDead(); + } + + public float getDeathScreenAnimation() + { + return this.deathScreenAnimation; + } + + public List getEntities() + { + return this.entities; + } + + public EntityPlayer getEntityPlayer() + { + return this.entityPlayer; + } + + public Random getRandom() + { + return this.random; + } + + public float getViewX() + { + return this.viewX; + } + + public float getViewY() + { + return this.viewY; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/GameDirector.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/GameDirector.java new file mode 100644 index 0000000..07e708b --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/GameDirector.java @@ -0,0 +1,105 @@ +package cz.tefek.srclone; + +import org.plutoengine.PlutoLocal; +import org.plutoengine.display.Display; +import org.plutoengine.logger.Logger; + +import java.util.Random; + +import cz.tefek.srclone.entity.EntityPlayer; +import cz.tefek.srclone.entity.enemy.EntityEnemy; +import cz.tefek.srclone.entity.enemy.EntityEnemyScout; +import cz.tefek.srclone.entity.enemy.EntityEnemySmallBomber; + +public class GameDirector +{ + private double gameTime; + + private double spawnTimer; + + private final Game game; + + private double difficulty; + + GameDirector(Game game) + { + this.game = game; + } + + public void tick(double delta) + { + var rand = this.game.getRandom(); + + this.spawnTimer -= delta; + + if (this.spawnTimer <= 0) + { + this.difficulty = 10 + Math.pow(this.gameTime, 1.5f); + + double spawnTimerNext = 160 / (4 + Math.log10(difficulty)); + + double variation = 0.75f + rand.nextDouble() / 2.0; + double rest = (1.0f + Math.sin(difficulty / 120.0) / 4.0); + double difficultyModifier = Math.sqrt(difficulty / 25.0f); + int waveSize = (int) (1 + difficultyModifier * variation * rest); + + Logger.logf("Wave size: %d%n", waveSize); + Logger.logf("Next wave in: %.0f seconds%n", spawnTimerNext); + Logger.log("---------------"); + + var player = this.game.getEntityPlayer(); + + this.spawn(player, rand, waveSize); + + this.spawnTimer += spawnTimerNext; + } + + this.gameTime += delta; + } + + public double getDifficulty() + { + return this.difficulty; + } + + private void spawn(EntityPlayer player, Random rand, int budget) + { + var display = PlutoLocal.components().getComponent(Display.class); + + double minSpawnRadius = 200 + Math.hypot(display.getWidth(), display.getHeight()); // HACK: Spawn enemies just outside the visible radius + + float px = player.getX(); + float py = player.getY(); + + double tdir = rand.nextDouble() * 2 * Math.PI; + + for (int i = 0; i < budget; i++) + { + EntityEnemy enemy; + + int enemyTypes = 2; + + switch (rand.nextInt(enemyTypes)) + { + case 1: + if (budget >= 5) + { + enemy = new EntityEnemySmallBomber(); + i += 2; + break; + } + case 0: + default: + enemy = new EntityEnemyScout(); + } + + double varDir = (rand.nextDouble() - 0.5) * (Math.PI / 8); + + double t = tdir + varDir; + double x = px + Math.cos(t) * minSpawnRadius; + double y = py + Math.sin(t) * minSpawnRadius; + + this.game.addEntity(enemy, (float) x, (float) y); + } + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/IGameObject.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/IGameObject.java new file mode 100644 index 0000000..0499910 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/IGameObject.java @@ -0,0 +1,6 @@ +package cz.tefek.srclone; + +public interface IGameObject +{ + void init(Game game, float x, float y); +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/Main.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/Main.java new file mode 100755 index 0000000..8f2a5b7 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/Main.java @@ -0,0 +1,68 @@ +package cz.tefek.srclone; + +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL33; +import org.plutoengine.PlutoApplication; +import org.plutoengine.PlutoLocal; +import org.plutoengine.display.Framerate; +import org.plutoengine.graphics.ImmediateFontRenderer; +import org.plutoengine.input.Keyboard; +import org.plutoengine.libra.paint.LiPaint; +import org.plutoengine.libra.text.shaping.TextStyleOptions; +import org.plutoengine.math.ProjectionMatrix; +import org.plutoengine.shader.uniform.auto.AutomaticUniforms; +import org.plutoengine.util.color.Color; + +import java.util.concurrent.TimeUnit; + +public class Main extends PlutoApplication +{ + public static void main(String[] args) + { + var app = new Main(); + var cfg = new PlutoApplication.StartupConfig(); + cfg.windowInitialDimensions(1280, 720); + cfg.windowName("jsr-clone"); + // cfg.vsync(1); + app.run(args, cfg); + } + + private Game game; + + @Override + protected Class getMainModule() + { + return SRCloneMod.class; + } + + @Override + protected void init() + { + this.game = new Game(); + } + + @Override + protected void loop() + { + GL33.glEnable(GL33.GL_BLEND); + GL33.glBlendFunc(GL33.GL_SRC_ALPHA, GL33.GL_ONE_MINUS_SRC_ALPHA); + + GL33.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + GL33.glClear(GL33.GL_COLOR_BUFFER_BIT); + + var projection = ProjectionMatrix.createOrtho2D(this.display.getWidth(), this.display.getHeight()); + AutomaticUniforms.VIEWPORT_PROJECTION.fire(projection); + + var keyboard = PlutoLocal.components().getComponent(Keyboard.class); + if (this.game.isOver() && keyboard.pressed(GLFW.GLFW_KEY_R)) + { + this.game = new Game(); + } + + var delta = Framerate.getFrameTime() / TimeUnit.SECONDS.toMillis(1); + + this.game.tick(delta); + + ImmediateFontRenderer.drawString(this.display.getWidth(), 5, Framerate.getInterpolatedFPS() + " FPS", SRCloneMod.font, new TextStyleOptions(32).setPaint(LiPaint.solidColor(Color.WHITE)).setHorizontalAlign(TextStyleOptions.TextAlign.END)); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/SRCloneMod.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/SRCloneMod.java new file mode 100755 index 0000000..29dec5b --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/SRCloneMod.java @@ -0,0 +1,148 @@ +package cz.tefek.srclone; + +import org.plutoengine.audio.AudioLoader; +import org.plutoengine.audio.RandomAccessClip; +import org.plutoengine.audio.SeekableTrack; +import org.plutoengine.graphics.PlutoGUIMod; +import org.plutoengine.graphics.gl.vao.QuadPresets; +import org.plutoengine.graphics.gl.vao.VertexArray; +import org.plutoengine.graphics.gui.font.bitmap.BitmapFont; +import org.plutoengine.graphics.gui.font.stbttf.STBTTFont; +import org.plutoengine.graphics.texture.texture2d.RectangleTexture; +import org.plutoengine.libra.text.font.LiFontFamily; +import org.plutoengine.libra.text.shaping.TextStyleOptions; +import org.plutoengine.mod.IModEntryPoint; +import org.plutoengine.mod.Mod; +import org.plutoengine.mod.ModEntry; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import cz.tefek.srclone.graphics.DirectionalSprite; +import cz.tefek.srclone.graphics.StarField; + +@ModEntry(modID = "tefek.srclone", version = "0.1", dependencies = PlutoGUIMod.class) +public class SRCloneMod implements IModEntryPoint +{ + public static LiFontFamily font; + + public static LiFontFamily srCloneFont; + + public static SeekableTrack playingMusic; + + public static RandomAccessClip[] explosionSound; + public static RandomAccessClip impactSound; + public static Map shootSounds; + + public static VertexArray centeredQuad; + + public static RectangleTexture[] stars; + public static StarField starField; + + public static RectangleTexture projectilesBase; + + public static DirectionalSprite player; + public static RectangleTexture rocketNozzle; + + public static DirectionalSprite enemyScout; + public static DirectionalSprite enemySmallBomber; + + public static DirectionalSprite pickupBox; + + public static DirectionalSprite parExplosion; + + public static DirectionalSprite parImpact; + + @Override + public void onLoad(Mod mod) + { + font = new LiFontFamily<>(); + font.add(TextStyleOptions.STYLE_REGULAR, STBTTFont.load(mod.getResource("plutofonts$plutostardust#ttf"))); + + srCloneFont = new LiFontFamily<>(); + srCloneFont.add(TextStyleOptions.STYLE_REGULAR, BitmapFont.load(mod.getResource("plutofonts$srclone.info#yaml"))); + + playingMusic = AudioLoader.loadMemoryDecoded(mod.getResource("sound.game_st#ogg")); + + explosionSound = IntStream.range(0, 5) + .mapToObj("sound.explosion%d#ogg"::formatted) + .map(mod::getResource) + .map(AudioLoader::loadMemoryPCM) + .toArray(RandomAccessClip[]::new); + impactSound = AudioLoader.loadMemoryPCM(mod.getResource("sound.hit#ogg")); + + shootSounds = Stream.of("laser", "heatStar", "polySwarm", "electronFlare", "plasma", "tachyon") + .collect(Collectors.toMap(Function.identity(), name -> { + var path = "sound.shoot.%s#ogg".formatted(name); + var resource = mod.getResource(path); + assert resource != null; + var audio = AudioLoader.loadMemoryPCM(resource); + assert audio != null; + return audio; + })); + + centeredQuad = QuadPresets.halvedSize(); + + stars = Stream.of("blue", "red", "redDwarf", "whiteDwarf", "blue").map(starName -> { + var tex = new RectangleTexture(); + tex.load(mod.getResource("textures.stars.%s#png".formatted(starName))); + return tex; + }).toList().toArray(RectangleTexture[]::new); + + starField = new StarField(0, 0, 8192, 8192, stars); + + projectilesBase = new RectangleTexture(); + projectilesBase.load(mod.getResource("textures.particles.projectilesBase#png")); + + player = DirectionalSprite.create(mod.getResource("textures.entities.ship#png"), 32, 64, 64, 32); + rocketNozzle = new RectangleTexture(); + rocketNozzle.load(mod.getResource("textures.particles.rocketNozzle#png")); + + enemyScout = DirectionalSprite.create(mod.getResource("textures.entities.e_scout#png"), 16, 128, 128, 16); + enemySmallBomber = DirectionalSprite.create(mod.getResource("textures.entities.e_small_bomber#png"), 16, 128, 128, 16); + + parExplosion = DirectionalSprite.create(mod.getResource("textures.particles.explosion1#png"), 16, 128, 128, 16); + parImpact = DirectionalSprite.create(mod.getResource("textures.particles.impact1#png"), 10, 64, 64, 10); + + pickupBox = DirectionalSprite.create(mod.getResource("textures.entities.box#png"), 16, 64, 64, 16); + } + + @Override + public void onUnload() + { + pickupBox.close(); + + parImpact.close(); + parExplosion.close(); + + enemySmallBomber.close(); + enemyScout.close(); + + rocketNozzle.close(); + player.close(); + + projectilesBase.close(); + + for (var star : stars) + star.close(); + + centeredQuad.close(); + + shootSounds.values() + .forEach(RandomAccessClip::close); + + impactSound.close(); + + for (var track : explosionSound) + track.close(); + + playingMusic.close(); + + srCloneFont.close(); + + font.close(); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/ammo/EnumAmmo.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/ammo/EnumAmmo.java new file mode 100644 index 0000000..40fa426 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/ammo/EnumAmmo.java @@ -0,0 +1,89 @@ +package cz.tefek.srclone.ammo; + +import org.plutoengine.audio.RandomAccessClip; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.entity.projectile.*; + +public enum EnumAmmo +{ + P_LASER_BEAM("☀", "laser", 0.2f, EnumTeam.PLAYER, EntityProjectilePlayerLaserBeam::new), + P_HEAT_STAR("\uD83C\uDF20", "heatStar", 0.4f, EnumTeam.PLAYER, EntityProjectilePlayerHeatStar::new), + P_ELECTRON_FLARE("\uD83D\uDCA0", "electronFlare", 2.5f, EnumTeam.PLAYER, EntityProjectilePlayerElectronFlare::new), + P_POLY_SWARM("\uD83D\uDD3A", "polySwarm", 0.1f, EnumTeam.PLAYER, EntityProjectilePlayerPolySwarm::new), + P_PLASMA_DISC("\uD83D\uDFE2", "plasma", 2.5f, EnumTeam.PLAYER, EntityProjectilePlayerPlasmaDisc::new), + P_TACHYON_DISC("\uD836\uDD53", "tachyon", 2.0f, EnumTeam.PLAYER, EntityProjectilePlayerTachyonDisc::new), + + + E_LASER_BEAM("☀", "laser", 2.0f, EnumTeam.ENEMY, EntityProjectileEnemyLaserBeam::new), + + E_HEAT_STAR("\uD83C\uDF20", "heatStar", 3.0f, EnumTeam.ENEMY, EntityProjectileEnemyHeatStar::new); + + private final EnumTeam team; + + private final String shootSound; + + private final float cooldown; + + private final String textIcon; + + private final Supplier projectileSupplier; + + EnumAmmo(String textIcon, String shootSound, float cooldown, EnumTeam team, Supplier projectileCreateFunc) + { + this.textIcon = textIcon; + this.shootSound = shootSound; + this.cooldown = cooldown; + this.team = team; + this.projectileSupplier = projectileCreateFunc; + } + + private static final List PLAYER_SELECTABLE = Arrays.stream(EnumAmmo.values()) + .filter(EnumAmmo::isPlayerSelectable) + .toList(); + + public static final long AMMO_INFINITE = Long.MAX_VALUE; + + public static EnumAmmo next(EnumAmmo ammo) + { + int idx = PLAYER_SELECTABLE.indexOf(ammo); + return PLAYER_SELECTABLE.get((idx + 1) % PLAYER_SELECTABLE.size()); + } + + public static EnumAmmo previous(EnumAmmo ammo) + { + int idx = PLAYER_SELECTABLE.indexOf(ammo); + int size = PLAYER_SELECTABLE.size(); + return PLAYER_SELECTABLE.get((idx - 1 + size) % size); + } + + public float getCooldown() + { + return this.cooldown; + } + + public RandomAccessClip getShootSound() + { + return SRCloneMod.shootSounds.get(this.shootSound); + } + + public EntityProjectileAmmo createProjectile() + { + return this.projectileSupplier.get(); + } + + public boolean isPlayerSelectable() + { + return this.team == EnumTeam.PLAYER; + } + + public String getTextIcon() + { + return this.textIcon; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/audio/SRAudioEngine.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/audio/SRAudioEngine.java new file mode 100644 index 0000000..43a5382 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/audio/SRAudioEngine.java @@ -0,0 +1,79 @@ +package cz.tefek.srclone.audio; + +import org.joml.Matrix4x3f; +import org.joml.Vector3f; +import org.plutoengine.PlutoLocal; +import org.plutoengine.audio.RandomAccessClip; +import org.plutoengine.audio.al.AudioContext; +import org.plutoengine.audio.al.AudioEngine; +import org.plutoengine.audio.al.AudioTrackSource; +import org.plutoengine.audio.al.SoundEffect; + +import cz.tefek.srclone.Game; +import cz.tefek.srclone.SRCloneMod; + +public class SRAudioEngine +{ + private AudioTrackSource music; + + private final AudioEngine audioEngine; + private final AudioContext context; + + public SRAudioEngine() + { + this.audioEngine = PlutoLocal.components().getComponent(AudioEngine.class); + this.context = this.audioEngine.getContext(); + this.context.setTransformation(new Matrix4x3f().scale(1 / 1000.0f, 1 / 1000.0f, 1.0f)); + } + + public void tick(Game game) + { + var player = game.getEntityPlayer(); + + this.context.setPosition(new Vector3f(player.getX(), player.getY(), -1)); + + if (this.music == null && !game.isOver()) + { + this.music = new AudioTrackSource(SRCloneMod.playingMusic); + this.music.volume(0.1f); + this.music.play(); + } + else if (this.music != null && !this.music.update()) + this.music.play(); + } + + public void stopMusic(Game game) + { + if (this.music != null) + { + var mute = 1 - game.getDeathScreenAnimation(); + + if (mute < 0) + { + this.music.stop(); + this.music = null; + return; + } + + this.music.volume(0.2f * mute); + } + } + + public AudioEngine getAudioEngine() + { + return this.audioEngine; + } + + public void playSoundEffect(RandomAccessClip seekableAudioTrack, float x, float y, float volume) + { + var soundEffect = new SoundEffect(seekableAudioTrack, new Vector3f(x, y, 0), volume); + this.audioEngine.playSound(soundEffect); + } + + public void playSoundEffect(RandomAccessClip seekableAudioTrack, float x, float y, float volume, float pitch) + { + var soundEffect = new SoundEffect(seekableAudioTrack, new Vector3f(x, y, 0), volume); + soundEffect.pitch(pitch); + this.audioEngine.playSound(soundEffect); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/Entity.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/Entity.java new file mode 100755 index 0000000..b0ae1b3 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/Entity.java @@ -0,0 +1,124 @@ +package cz.tefek.srclone.entity; + +import org.joml.Vector3f; +import org.plutoengine.audio.RandomAccessClip; +import org.plutoengine.audio.al.SoundEffect; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.Game; +import cz.tefek.srclone.IGameObject; + +public abstract class Entity implements IGameObject +{ + protected float x; + protected float y; + + protected boolean collision; + + protected float size; + + protected float rotation; + + protected float lifetime; + + protected boolean deadFlag; + + protected EnumTeam team; + + protected Game game; + + protected Entity() + { + this.collision = true; + } + + @Override + public void init(Game game, float x, float y) + { + this.game = game; + this.x = x; + this.y = y; + } + + public void render() + { + + } + + public void tick(float delta) + { + this.lifetime += delta; + } + + public boolean isDead() + { + return this.deadFlag; + } + + protected final float getRenderX() + { + return this.x + this.game.getViewX(); + } + + protected final float getRenderY() + { + return this.y + this.game.getViewY(); + } + + public void setX(float x) + { + this.x = x; + } + + public void setY(float y) + { + this.y = y; + } + + public float getX() + { + return this.x; + } + + public float getY() + { + return this.y; + } + + public EnumTeam getTeam() + { + return this.team; + } + + public float getDistance(Entity entity) + { + return (float) (Math.hypot(entity.x - this.x, entity.y - this.y) - this.size - entity.size); + } + + public boolean collides(Entity entity) + { + float dx = entity.x - this.x; + float dy = entity.y - this.y; + + return dx * dx + dy * dy < this.size * this.size + entity.size * entity.size; + } + + protected void emitSoundEffect(RandomAccessClip clip, float volume, float pitch, boolean closeOnDeath) + { + var srae = this.game.getAudioEngine(); + var audioEngine = srae.getAudioEngine(); + var sfx = new SoundEffect(clip, new Vector3f(this.x, this.y, 0.0f), volume) + .pitch(pitch) + .movementMapper(oldPos -> this.deadFlag ? oldPos : new Vector3f(this.x, this.y, 0.0f)); + + if (closeOnDeath) + sfx.keepAliveFunction(() -> !this.deadFlag); + + audioEngine.playSound(sfx); + } + + public boolean hasCollision() + { + return this.collision; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/EntityLiving.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/EntityLiving.java new file mode 100644 index 0000000..36785f9 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/EntityLiving.java @@ -0,0 +1,77 @@ +package cz.tefek.srclone.entity; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.Game; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.particle.ParticleExplosion; + +public class EntityLiving extends Entity +{ + protected float health; + + protected float maxHealth; + + protected EntityLiving() + { + + } + + @Override + public void init(Game game, float x, float y) + { + super.init(game, x, y); + this.health = this.maxHealth; + } + + public void tick(float frameTime) + { + if (this.health <= 0) + { + this.deadFlag = true; + this.onDie(); + } + } + + public void heal(Entity source, float health) + { + this.health = Math.min(this.maxHealth, this.health + health); + this.onHeal(source, health); + } + + public void damage(Entity source, float health) + { + this.health -= health; + this.onDamage(source, health); + } + + public float getHealth() + { + return this.health; + } + + public float getMaxHealth() + { + return this.maxHealth; + } + + public void onHeal(Entity source, float amount) + { + + } + + public void onDamage(Entity source, float amount) + { + + } + + public void onDie() + { + var explosion = new ParticleExplosion(); + explosion.setSize(this.size * (this.team == EnumTeam.PLAYER ? 16.0f : 8.0f)); + this.game.addParticle(explosion, this.x, this.y); + + var rand = this.game.getRandom(); + var audioEngine = this.game.getAudioEngine(); + audioEngine.playSoundEffect(SRCloneMod.explosionSound[rand.nextInt(SRCloneMod.explosionSound.length)], this.x, this.y, 0.15f); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/EntityPlayer.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/EntityPlayer.java new file mode 100755 index 0000000..afa5a43 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/EntityPlayer.java @@ -0,0 +1,256 @@ +package cz.tefek.srclone.entity; + +import org.joml.Vector2f; +import org.lwjgl.glfw.GLFW; +import org.plutoengine.PlutoLocal; +import org.plutoengine.graphics.RectangleRenderer2D; +import org.plutoengine.input.Keyboard; +import org.plutoengine.math.BasicInterpolation; + +import java.util.EnumMap; +import java.util.Map; + +import cz.tefek.srclone.ammo.EnumAmmo; +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; + +public class EntityPlayer extends EntityLiving +{ + private float xSpeed; + private float ySpeed; + + private float enginePower; + + private float weaponCooldown; + + private double score; + + private EnumAmmo selectedAmmo; + + private final Map ammo; + + public EntityPlayer() + { + this.size = 16; + this.team = EnumTeam.PLAYER; + this.ammo = new EnumMap<>(EnumAmmo.class); + this.maxHealth = 100.0f; + this.selectedAmmo = EnumAmmo.P_LASER_BEAM; + } + + @Override + public void render() + { + var sides = SRCloneMod.player.getSideCount(); + var side = BasicInterpolation.roundedLerpWrap(this.rotation / (Math.PI * 2), 0, sides); + + float w = 64, h = 64; + + float engineOffset = 15; + + float engineX = this.getRenderX() - (float) (Math.sin(this.rotation) * engineOffset); + float engineY = this.getRenderY() - (float) (Math.cos(this.rotation) * engineOffset); + + float nozzleRot = (float) Math.toRadians(-side * 360.0f / sides + 135.0f); + + if (side >= sides / 4 * 3 || side <= sides / 4) + { + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(engineX, engineY, w, h) + .rotate(nozzleRot) + .recolor(1.0f, 1.0f, 1.0f, this.enginePower) + .texture(SRCloneMod.rocketNozzle) + .flush(); + + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(this.getRenderX(), this.getRenderY(), w, h) + .sprite(SRCloneMod.player.getSide(side)) + .flush(); + } + else + { + + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(this.getRenderX(), this.getRenderY(), w, h) + .sprite(SRCloneMod.player.getSide(side)) + .flush(); + + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(engineX, engineY, w, h) + .rotate(nozzleRot) + .recolor(1.0f, 1.0f, 1.0f, this.enginePower) + .texture(SRCloneMod.rocketNozzle) + .flush(); + } + } + + public void addScore(double score) + { + this.score += score; + } + + public double getScore() + { + return this.score; + } + + public void addAmmo(EnumAmmo ammo, long amount) + { + this.ammo.merge(ammo, amount, (oldCnt, add) -> oldCnt == EnumAmmo.AMMO_INFINITE ? EnumAmmo.AMMO_INFINITE : oldCnt + add); + } + + public long getAmmo(EnumAmmo ammo) + { + return this.ammo.getOrDefault(ammo, 0L); + } + + public EnumAmmo getSelectedAmmo() + { + return this.selectedAmmo; + } + + @Override + public void tick(float frameTime) + { + var keyboard = PlutoLocal.components().getComponent(Keyboard.class); + + this.tickShoot(keyboard, frameTime); + this.tickWeaponSelect(keyboard); + this.tickMovement(keyboard, frameTime); + + super.tick(frameTime); + } + + private void tickShoot(Keyboard keyboard, float frameTime) + { + this.weaponCooldown -= frameTime; + + if (this.weaponCooldown <= 0 && keyboard.isKeyDown(GLFW.GLFW_KEY_SPACE)) + { + var ammoCount = this.getAmmo(this.selectedAmmo); + + if (ammoCount > 0) + { + var projectile = this.selectedAmmo.createProjectile(); + float rot = (float) (-this.rotation + Math.PI / 2.0f); + projectile.setRotation(rot); + + var xPos = this.x + this.size * (float) Math.sin(this.rotation); + var yPos = this.y + this.size * (float) Math.cos(this.rotation); + this.game.addEntity(projectile, xPos, yPos); + + this.weaponCooldown += this.selectedAmmo.getCooldown(); + + this.addAmmo(this.selectedAmmo, -1); + } + + var newCount = this.getAmmo(this.selectedAmmo); + + if (newCount == 0) + { + var first = this.selectedAmmo; + + do + this.selectedAmmo = EnumAmmo.next(this.selectedAmmo); + while (this.selectedAmmo != first && this.getAmmo(this.selectedAmmo) == 0); + } + } + + this.weaponCooldown = Math.max(this.weaponCooldown, 0.0f); + } + + private void tickWeaponSelect(Keyboard keyboard) + { + if (keyboard.pressed(GLFW.GLFW_KEY_LEFT_CONTROL) || keyboard.pressed(GLFW.GLFW_KEY_Q)) + { + var first = this.selectedAmmo; + + do + this.selectedAmmo = EnumAmmo.previous(this.selectedAmmo); + while (this.selectedAmmo != first && this.getAmmo(this.selectedAmmo) == 0); + } + else if (keyboard.pressed(GLFW.GLFW_KEY_LEFT_SHIFT) || keyboard.pressed(GLFW.GLFW_KEY_E)) + { + var first = this.selectedAmmo; + + do + this.selectedAmmo = EnumAmmo.next(this.selectedAmmo); + while (this.selectedAmmo != first && this.getAmmo(this.selectedAmmo) == 0); + } + + if (keyboard.pressed(GLFW.GLFW_KEY_L)) + this.damage(this, 100); + } + + private void tickMovement(Keyboard keyboard, float frameTime) + { + float maxSpeed = 170; + float accel = 400 * frameTime; + + var joystick = GLFW.glfwGetJoystickAxes(GLFW.GLFW_JOYSTICK_1); + var joystickX = 0.0f; + var joystickY = 0.0f; + + var deadZone = 0.2f; + + if (joystick != null) + { + if (joystick.limit() >= 2) + { + joystickX = joystick.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_X); + joystickY = joystick.get(GLFW.GLFW_GAMEPAD_AXIS_LEFT_Y); + + if (Math.abs(joystickX) < deadZone) + joystickX = 0; + + if (Math.abs(joystickY) < deadZone) + joystickY = 0; + } + } + + float friction = (float) Math.pow(0.05f, frameTime); + + var movementAxes = new Vector2f(); + + if (keyboard.isKeyDown(GLFW.GLFW_KEY_UP) || keyboard.isKeyDown(GLFW.GLFW_KEY_W)) + movementAxes.y = -1; + else if (keyboard.isKeyDown(GLFW.GLFW_KEY_DOWN) || keyboard.isKeyDown(GLFW.GLFW_KEY_S)) + movementAxes.y = 1; + else + movementAxes.y = joystickY; + + if (keyboard.isKeyDown(GLFW.GLFW_KEY_LEFT) || keyboard.isKeyDown(GLFW.GLFW_KEY_A)) + movementAxes.x = -1; + else if (keyboard.isKeyDown(GLFW.GLFW_KEY_RIGHT) || keyboard.isKeyDown(GLFW.GLFW_KEY_D)) + movementAxes.x = 1; + else + movementAxes.x = joystickX; + + if (movementAxes.lengthSquared() > 1.0f) + movementAxes.normalize(); + + var xAccel = movementAxes.x * accel; + var yAccel = movementAxes.y * accel; + + this.enginePower = movementAxes.lengthSquared(); + + if (Math.signum(xAccel) != Math.signum(this.xSpeed)) + this.xSpeed *= friction; + + if (Math.signum(yAccel) != Math.signum(this.ySpeed)) + this.ySpeed *= friction; + + this.xSpeed += xAccel; + this.ySpeed += yAccel; + + this.xSpeed = Math.max(Math.min(maxSpeed, this.xSpeed), -maxSpeed); + this.ySpeed = Math.max(Math.min(maxSpeed, this.ySpeed), -maxSpeed); + + this.x += this.xSpeed * frameTime; + this.y += this.ySpeed * frameTime; + + if (this.xSpeed != 0 || this.ySpeed != 0) + this.rotation = (float) (Math.PI - Math.atan2(this.xSpeed, -this.ySpeed)); + + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemy.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemy.java new file mode 100644 index 0000000..2fdedc6 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemy.java @@ -0,0 +1,21 @@ +package cz.tefek.srclone.entity.enemy; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.entity.EntityLiving; + +public abstract class EntityEnemy extends EntityLiving +{ + protected EntityEnemy() + { + this.team = EnumTeam.ENEMY; + } + + @Override + public void onDie() + { + var player = this.game.getEntityPlayer(); + player.addScore(this.maxHealth); + + super.onDie(); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemyScout.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemyScout.java new file mode 100644 index 0000000..6e2c0f5 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemyScout.java @@ -0,0 +1,120 @@ +package cz.tefek.srclone.entity.enemy; + +import org.plutoengine.graphics.RectangleRenderer2D; + +import cz.tefek.srclone.Game; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; +import cz.tefek.srclone.entity.pickup.EntityBox; + +public class EntityEnemyScout extends EntityEnemy +{ + protected float movementChangeTimer; + protected float movementChangeInterval; + + protected float speed; + + protected float weaponCooldown; + + protected float targetAngle; + + protected float precision; + + protected float fleeRange; + + protected float turningRate; + + protected EnumAmmo ammo; + + public EntityEnemyScout() + { + this.maxHealth = 50.0f; + this.size = 32.0f; + this.speed = 90.0f; + this.ammo = EnumAmmo.E_LASER_BEAM; + this.movementChangeInterval = 0.5f; + this.precision = (float) (Math.PI / 4.0f); + this.fleeRange = 200.0f; + this.turningRate = (float) (Math.PI / 2.0f); + } + + @Override + public void init(Game game, float x, float y) + { + super.init(game, x, y); + this.movementChangeTimer = this.movementChangeInterval; + this.weaponCooldown = this.ammo.getCooldown() * 10.0f; + } + + @Override + public void tick(float delta) + { + this.movementChangeTimer -= delta; + + var player = this.game.getEntityPlayer(); + + float dx = player.getX() - this.x; + float dy = player.getY() - this.y; + + var rand = this.game.getRandom(); + + if (this.movementChangeTimer <= 0.0f) + { + float generalAngle = (float) Math.atan2(dx, dy); + this.targetAngle = generalAngle + this.precision * (rand.nextFloat() - 0.5f); + + this.movementChangeTimer += this.movementChangeInterval * (1.0f + rand.nextFloat() * 2.0f); + } + + if (player.getDistance(this) < this.fleeRange) + this.targetAngle = (float) (Math.atan2(dx, dy) - Math.PI); + + float errorCorrection = (float) ((this.targetAngle - this.rotation) % (2 * Math.PI)); + + if (errorCorrection > Math.PI) + errorCorrection -= 2 * Math.PI; + + if (errorCorrection < -Math.PI) + errorCorrection += 2 * Math.PI; + + float clampedErrorCorrection = Math.max(-this.turningRate * delta, Math.min(this.turningRate * delta, errorCorrection)); + this.rotation += clampedErrorCorrection; + + this.x += Math.cos(-this.rotation + Math.PI / 2.0) * this.speed * delta; + this.y += Math.sin(-this.rotation + Math.PI / 2.0) * this.speed * delta; + + this.weaponCooldown -= delta; + + if (this.weaponCooldown <= 0) + { + var projectile = this.ammo.createProjectile(); + float rot = (float) (-this.rotation + Math.PI / 2.0f); + projectile.setRotation(rot); + this.game.addEntity(projectile, this.x + this.size * (float) Math.sin(this.rotation), this.y + this.size * (float) Math.cos(this.rotation)); + + this.weaponCooldown = this.ammo.getCooldown(); + } + + super.tick(delta); + } + + @Override + public void render() + { + float w = 128, h = 128; + + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(this.getRenderX(), this.getRenderY(), w, h) + .sprite(SRCloneMod.enemyScout.getSideFromAngle(this.rotation)) + .flush(); + } + + @Override + public void onDie() + { + if (this.game.getRandom().nextFloat() < 0.05f) + this.game.addEntity(new EntityBox(), this.x, this.y); + + super.onDie(); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemySmallBomber.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemySmallBomber.java new file mode 100644 index 0000000..9985a3b --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/enemy/EntityEnemySmallBomber.java @@ -0,0 +1,117 @@ +package cz.tefek.srclone.entity.enemy; + +import org.plutoengine.graphics.RectangleRenderer2D; + +import cz.tefek.srclone.Game; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; +import cz.tefek.srclone.entity.pickup.EntityBox; +import cz.tefek.srclone.util.AngleUtil; + +public class EntityEnemySmallBomber extends EntityEnemy +{ + + protected float movementChangeTimer; + protected float movementChangeInterval; + + protected float speed; + + protected float weaponCooldown; + + protected float targetAngle; + + protected float precision; + + protected float fleeRange; + + protected float turningRate; + + protected EnumAmmo ammo; + + public EntityEnemySmallBomber() + { + this.maxHealth = 150.0f; + this.size = 32.0f; + this.speed = 180.0f; + this.ammo = EnumAmmo.E_HEAT_STAR; + this.movementChangeInterval = 0.3f; + this.precision = (float) (Math.PI / 80.0f); + this.fleeRange = 350.0f; + this.turningRate = (float) Math.PI; + } + + @Override + public void init(Game game, float x, float y) + { + super.init(game, x, y); + this.movementChangeTimer = this.movementChangeInterval; + this.weaponCooldown = this.ammo.getCooldown() * 10.0f; + } + + @Override + public void tick(float delta) + { + this.movementChangeTimer -= delta; + + var player = this.game.getEntityPlayer(); + + float dx = player.getX() - this.x; + float dy = player.getY() - this.y; + + var rand = this.game.getRandom(); + + float generalAngle = (float) Math.atan2(dx, dy); + + if (this.movementChangeTimer <= 0.0f) + { + this.targetAngle = generalAngle + this.precision * (rand.nextFloat() - 0.5f); + + this.movementChangeTimer += this.movementChangeInterval * (1.0f + rand.nextFloat() * 2.0f); + } + + if (player.getDistance(this) < this.fleeRange) + this.targetAngle = (float) (Math.atan2(dx, dy) - Math.PI); + + float errorCorrection = AngleUtil.within180(this.targetAngle - this.rotation); + + float clampedErrorCorrection = Math.max(-this.turningRate * delta, Math.min(this.turningRate * delta, errorCorrection)); + this.rotation += clampedErrorCorrection; + + this.x += Math.cos(-this.rotation + Math.PI / 2.0) * this.speed * delta; + this.y += Math.sin(-this.rotation + Math.PI / 2.0) * this.speed * delta; + + this.weaponCooldown -= delta; + + if (this.weaponCooldown <= 0 && Math.abs(AngleUtil.within180(generalAngle - this.rotation)) < Math.PI / 25.0f) + { + var projectile = this.ammo.createProjectile(); + float rot = (float) (-this.rotation + Math.PI / 2.0f); + projectile.setRotation(rot); + this.game.addEntity(projectile, this.x + this.size * (float) Math.sin(this.rotation), this.y + this.size * (float) Math.cos(this.rotation)); + + this.weaponCooldown = this.ammo.getCooldown(); + } + + super.tick(delta); + } + + @Override + public void render() + { + float w = 128, h = 128; + + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(this.getRenderX(), this.getRenderY(), w, h) + .sprite(SRCloneMod.enemySmallBomber.getSideFromAngle(this.rotation)) + .flush(); + } + + @Override + public void onDie() + { + if (this.game.getRandom().nextFloat() < 0.2f) + this.game.addEntity(new EntityBox(), this.x, this.y); + + super.onDie(); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/pickup/EntityBox.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/pickup/EntityBox.java new file mode 100644 index 0000000..6d2b867 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/pickup/EntityBox.java @@ -0,0 +1,99 @@ +package cz.tefek.srclone.entity.pickup; + +import org.plutoengine.display.Framerate; +import org.plutoengine.graphics.RectangleRenderer2D; + +import cz.tefek.srclone.ammo.EnumAmmo; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.entity.EntityPlayer; +import cz.tefek.srclone.particle.ParticleText; + +public class EntityBox extends EntityPickup +{ + protected float maxLifetime; + + public EntityBox() + { + this.size = 32.0f; + this.maxLifetime = 22.5f; + } + + @Override + public void tick(float delta) + { + if (this.lifetime >= this.maxLifetime) + this.deadFlag = true; + + super.tick(delta); + } + + @Override + public void render() + { + var opacity = (1.0f - this.lifetime / this.maxLifetime) * 5.0f; + opacity = Math.min(1.0f, Math.max(0.0f, opacity)); + + float w = 64, h = 64; + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(this.getRenderX(), this.getRenderY(), w, h) + .sprite(SRCloneMod.pickupBox.getSideFromAngle((float) Math.PI * Framerate.getAnimationTimer())) + .recolor(1.0f, 1.0f, 1.0f, opacity) + .flush(); + } + + @Override + protected void onPickup(EntityPlayer player) + { + var rand = this.game.getRandom(); + + double healChance = rand.nextDouble(); + double r = rand.nextDouble(); + double mul = rand.nextDouble(); + + String lootText; + + if (healChance < 0.2 && player.getHealth() < player.getMaxHealth()) + { + float healAmount = 15.0f; + lootText = "+%.0f\uD83D\uDC96".formatted(healAmount); + + player.heal(this, healAmount); + } + else + { + EnumAmmo ammo; + long count; + + if (r > 0.95) + { + ammo = EnumAmmo.P_TACHYON_DISC; + count = Math.round(5 + 3 * mul); + } + else if (r > 0.85) + { + ammo = EnumAmmo.P_ELECTRON_FLARE; + count = Math.round(4 + 2 * mul); + } + else if (r > 0.70) + { + ammo = EnumAmmo.P_PLASMA_DISC; + count = Math.round(3 + 3 * mul); + } + else if (r > 0.40) + { + ammo = EnumAmmo.P_HEAT_STAR; + count = Math.round(20 + 20 * mul); + } + else + { + ammo = EnumAmmo.P_POLY_SWARM; + count = Math.round(100 + 50 * mul); + } + + player.addAmmo(ammo, count); + lootText = "+%d%s".formatted(count, ammo.getTextIcon()); + } + + this.game.addParticle(new ParticleText(lootText), this.x, this.y); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/pickup/EntityPickup.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/pickup/EntityPickup.java new file mode 100644 index 0000000..c7e9312 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/pickup/EntityPickup.java @@ -0,0 +1,33 @@ +package cz.tefek.srclone.entity.pickup; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.entity.Entity; +import cz.tefek.srclone.entity.EntityPlayer; + +public class EntityPickup extends Entity +{ + protected EntityPickup() + { + this.team = EnumTeam.PLAYER; + this.collision = false; + } + + @Override + public void tick(float delta) + { + var player = this.game.getEntityPlayer(); + + if (this.collides(player)) + { + this.onPickup(player); + this.deadFlag = true; + } + + super.tick(delta); + } + + protected void onPickup(EntityPlayer player) + { + + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectile.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectile.java new file mode 100644 index 0000000..0b26e74 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectile.java @@ -0,0 +1,47 @@ +package cz.tefek.srclone.entity.projectile; + +import cz.tefek.srclone.entity.Entity; + +public abstract class EntityProjectile extends Entity +{ + protected EntityProjectile() + { + + } + + public void tick(float delta) + { + switch (this.team) + { + case ENEMY -> { + var player = this.game.getEntityPlayer(); + + if (this.collides(player)) + this.onHit(player); + } + + case PLAYER -> { + var collision = this.game.getEntities() + .parallelStream() + .filter(Entity::hasCollision) + .filter(this::collides) + .iterator(); + + while (collision.hasNext() && !this.deadFlag) + this.onHit(collision.next()); + } + } + + super.tick(delta); + } + + public void setRotation(float rotation) + { + this.rotation = rotation; + } + + protected void onHit(Entity entity) + { + + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileAmmo.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileAmmo.java new file mode 100644 index 0000000..d22de13 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileAmmo.java @@ -0,0 +1,111 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.RectangleRenderer2D; +import org.plutoengine.graphics.sprite.Sprite; +import org.plutoengine.graphics.texture.texture2d.RectangleTexture; + +import cz.tefek.srclone.Game; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; +import cz.tefek.srclone.entity.Entity; +import cz.tefek.srclone.entity.EntityLiving; +import cz.tefek.srclone.particle.ParticleImpact; + +public abstract class EntityProjectileAmmo extends EntityProjectile +{ + public static final float HIT_AUDIO_TICK_LIMIT = 0.075f; + + protected final EnumAmmo ammo; + + protected float ageOnHit; + + protected float damage; + + protected float maxLifetime; + protected float velocity; + + protected Sprite sprite; + + protected float visualSize; + + protected float hitSoundTimer = 0.0f; + + protected float velocityFalloff; + + protected EntityProjectileAmmo(EnumAmmo ammo) + { + this.ammo = ammo; + this.visualSize = 64.0f; + this.ageOnHit = 4.0f; + this.velocityFalloff = 0.95f; + } + + @Override + public void init(Game game, float x, float y) + { + super.init(game, x, y); + + var rand = this.game.getRandom(); + this.emitSoundEffect(this.ammo.getShootSound(), 0.30f, 0.9f + rand.nextFloat() * 0.2f, false); + } + + @Override + public void tick(float delta) + { + this.velocity *= Math.pow(this.velocityFalloff, delta); + + float vx = (float) (this.velocity * Math.cos(this.rotation)); + float vy = (float) (this.velocity * Math.sin(this.rotation)); + + this.x += vx * delta; + this.y += vy * delta; + + if (this.lifetime >= this.maxLifetime) + { + this.deadFlag = true; + return; + } + + this.hitSoundTimer -= delta; + + super.tick(delta); + } + + @Override + public void render() + { + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(this.getRenderX(), this.getRenderY(), this.visualSize, this.visualSize) + .rotate(this.rotation + (float) Math.PI / 4.0f) + .sprite(this.sprite) + .flush(); + } + + @Override + protected void onHit(Entity entity) + { + this.lifetime += this.ageOnHit; + + if (this.lifetime >= this.maxLifetime) + this.deadFlag = true; + + if (entity instanceof EntityLiving entityLiving) + { + entityLiving.damage(this, this.damage); + } + + var hitParticle = new ParticleImpact(); + hitParticle.setRotation(this.rotation - 1.75f * (float) Math.PI); + this.game.addParticle(hitParticle, this.x, this.y); + + if (this.hitSoundTimer <= 0) + { + var audioEngine = this.game.getAudioEngine(); + audioEngine.playSoundEffect(SRCloneMod.impactSound, this.x, this.y, 0.15f); + + this.hitSoundTimer = HIT_AUDIO_TICK_LIMIT; + } + + super.onHit(entity); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileAmmoSeeking.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileAmmoSeeking.java new file mode 100644 index 0000000..4d3f7e2 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileAmmoSeeking.java @@ -0,0 +1,51 @@ +package cz.tefek.srclone.entity.projectile; + +import java.util.Comparator; + +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectileAmmoSeeking extends EntityProjectileAmmo +{ + protected float steeringRate; + + protected EntityProjectileAmmoSeeking(EnumAmmo ammo) + { + super(ammo); + } + + @Override + public void tick(float dt) + { + var target = this.game.getEntities() + .parallelStream() + .filter(entity -> entity.getTeam() != this.team) + .min(Comparator.comparing(this::getDistance)); + + if (target.isPresent()) + { + var entity = target.get(); + + float dx = entity.getX() - this.x; + float dy = entity.getY() - this.y; + + float targetAngle = (float) Math.atan2(dy, dx); + + float errorCorrection = targetAngle - this.rotation; + + if (errorCorrection > Math.PI) + errorCorrection -= 2 * Math.PI; + + if (errorCorrection < -Math.PI) + errorCorrection += 2 * Math.PI; + + float clampedErrorCorrection = Math.max(-this.steeringRate * dt, Math.min(this.steeringRate * dt, errorCorrection)); + this.rotation += clampedErrorCorrection; + } + else + { + this.rotation += this.steeringRate / 2.0f * dt; + } + + super.tick(dt); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileEnemyHeatStar.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileEnemyHeatStar.java new file mode 100644 index 0000000..41a3dfb --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileEnemyHeatStar.java @@ -0,0 +1,21 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectileEnemyHeatStar extends EntityProjectileHeatStar +{ + public EntityProjectileEnemyHeatStar() + { + super(EnumAmmo.E_HEAT_STAR); + + this.team = EnumTeam.ENEMY; + this.sprite = new PartialTextureSprite(SRCloneMod.projectilesBase, 256, 0, 256, 256); + this.maxLifetime = 4.0f; + this.velocity = 200.0f; + this.damage = 20.0f; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileEnemyLaserBeam.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileEnemyLaserBeam.java new file mode 100644 index 0000000..2124f60 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileEnemyLaserBeam.java @@ -0,0 +1,21 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectileEnemyLaserBeam extends EntityProjectileLaserBeam +{ + public EntityProjectileEnemyLaserBeam() + { + super(EnumAmmo.E_LASER_BEAM); + + this.team = EnumTeam.ENEMY; + this.sprite = new PartialTextureSprite(SRCloneMod.projectilesBase, 512, 0, 256, 256); + this.maxLifetime = 4.0f; + this.velocity = 400.0f; + this.damage = 5.0f; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileHeatStar.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileHeatStar.java new file mode 100644 index 0000000..eca1595 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileHeatStar.java @@ -0,0 +1,12 @@ +package cz.tefek.srclone.entity.projectile; + +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectileHeatStar extends EntityProjectileAmmo +{ + + protected EntityProjectileHeatStar(EnumAmmo ammo) + { + super(ammo); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileLaserBeam.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileLaserBeam.java new file mode 100644 index 0000000..11137df --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectileLaserBeam.java @@ -0,0 +1,11 @@ +package cz.tefek.srclone.entity.projectile; + +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectileLaserBeam extends EntityProjectileAmmo +{ + protected EntityProjectileLaserBeam(EnumAmmo ammo) + { + super(ammo); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerElectronFlare.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerElectronFlare.java new file mode 100644 index 0000000..ecf44a1 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerElectronFlare.java @@ -0,0 +1,24 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectilePlayerElectronFlare extends EntityProjectileAmmoSeeking +{ + public EntityProjectilePlayerElectronFlare() + { + super(EnumAmmo.P_ELECTRON_FLARE); + + this.team = EnumTeam.PLAYER; + this.sprite = new PartialTextureSprite(SRCloneMod.projectilesBase, 0, 0, 256, 256); + this.size = 24.0f; + this.ageOnHit = 0.5f; + this.damage = 50.0f; + this.velocity = 350.0f; + this.maxLifetime = 5.0f; + this.steeringRate = (float) Math.PI; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerHeatStar.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerHeatStar.java new file mode 100644 index 0000000..e467925 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerHeatStar.java @@ -0,0 +1,21 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectilePlayerHeatStar extends EntityProjectileAmmo +{ + public EntityProjectilePlayerHeatStar() + { + super(EnumAmmo.P_HEAT_STAR); + + this.team = EnumTeam.PLAYER; + this.sprite = new PartialTextureSprite(SRCloneMod.projectilesBase, 768, 0, 256, 256); + this.maxLifetime = 8.0f; + this.velocity = 250.0f; + this.damage = 200.0f; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerLaserBeam.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerLaserBeam.java new file mode 100644 index 0000000..8737e14 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerLaserBeam.java @@ -0,0 +1,21 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectilePlayerLaserBeam extends EntityProjectileLaserBeam +{ + public EntityProjectilePlayerLaserBeam() + { + super(EnumAmmo.P_LASER_BEAM); + + this.team = EnumTeam.PLAYER; + this.sprite = new PartialTextureSprite(SRCloneMod.projectilesBase, 1024, 0, 256, 256); + this.maxLifetime = 4.0f; + this.velocity = 1000.0f; + this.damage = 20.0f; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerPlasmaDisc.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerPlasmaDisc.java new file mode 100644 index 0000000..6fe5fd0 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerPlasmaDisc.java @@ -0,0 +1,36 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectilePlayerPlasmaDisc extends EntityProjectileAmmo +{ + protected float damageBase; + + public EntityProjectilePlayerPlasmaDisc() + { + super(EnumAmmo.P_PLASMA_DISC); + + this.team = EnumTeam.PLAYER; + this.sprite = new PartialTextureSprite(SRCloneMod.projectilesBase, 1280, 0, 256, 256); + this.size = 24.0f; + this.ageOnHit = 0.0f; + this.damageBase = 150.0f; + this.maxLifetime = 20.0f; + this.velocity = 500.0f; + this.velocityFalloff = 0.9f; + } + + @Override + public void tick(float delta) + { + float steeringRate = (float) Math.PI; + this.rotation += steeringRate * delta; + this.damage = this.damageBase * delta * this.velocity / 100.0f; + + super.tick(delta); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerPolySwarm.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerPolySwarm.java new file mode 100644 index 0000000..5f4fa71 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerPolySwarm.java @@ -0,0 +1,24 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectilePlayerPolySwarm extends EntityProjectileAmmoSeeking +{ + public EntityProjectilePlayerPolySwarm() + { + super(EnumAmmo.P_POLY_SWARM); + + this.team = EnumTeam.PLAYER; + this.sprite = new PartialTextureSprite(SRCloneMod.projectilesBase, 1536, 0, 256, 256); + this.size = 24.0f; + this.ageOnHit = 10.0f; + this.damage = 7.5f; + this.velocity = 300.0f; + this.maxLifetime = 10.0f; + this.steeringRate = (float) Math.PI; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerTachyonDisc.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerTachyonDisc.java new file mode 100644 index 0000000..e5aad57 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/entity/projectile/EntityProjectilePlayerTachyonDisc.java @@ -0,0 +1,34 @@ +package cz.tefek.srclone.entity.projectile; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; + +import cz.tefek.srclone.EnumTeam; +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.ammo.EnumAmmo; + +public class EntityProjectilePlayerTachyonDisc extends EntityProjectileAmmo +{ + protected float damageBase; + + public EntityProjectilePlayerTachyonDisc() + { + super(EnumAmmo.P_TACHYON_DISC); + + this.team = EnumTeam.PLAYER; + this.sprite = new PartialTextureSprite(SRCloneMod.projectilesBase, 1792, 0, 256, 256); + this.size = 24.0f; + this.ageOnHit = 0.0f; + this.damageBase = 400.0f; + this.maxLifetime = 10.0f; + this.velocity = 100.0f; + this.velocityFalloff = 4.0f; + } + + @Override + public void tick(float delta) + { + this.damage = this.damageBase * delta * this.velocity / 100.0f; + + super.tick(delta); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/AnimationSprite.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/AnimationSprite.java new file mode 100755 index 0000000..79b7fd1 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/AnimationSprite.java @@ -0,0 +1,37 @@ +package cz.tefek.srclone.graphics; + +import org.plutoengine.graphics.sprite.PartialTextureSprite; +import org.plutoengine.graphics.sprite.TemporalSprite; +import org.plutoengine.graphics.texture.texture2d.RectangleTexture; + +import java.nio.file.Path; + +public final class AnimationSprite extends TemporalSprite implements AutoCloseable +{ + private final RectangleTexture backingTexture; + + private AnimationSprite(RectangleTexture backingTexture, PartialTextureSprite[] sprite) + { + super(sprite); + this.backingTexture = backingTexture; + } + + public static AnimationSprite create(Path path, int dimensions, int width, int height, int stride) + { + var backingTexture = new RectangleTexture(); + backingTexture.load(path); + + var sprites = new PartialTextureSprite[dimensions]; + + for (int i = 0; i < sprites.length; i++) + sprites[i] = new PartialTextureSprite(backingTexture, i % stride * width, i / stride * height, width, height); + + return new AnimationSprite(backingTexture, sprites); + } + + @Override + public void close() + { + this.backingTexture.close(); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/DirectionalSprite.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/DirectionalSprite.java new file mode 100755 index 0000000..cfe1f75 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/DirectionalSprite.java @@ -0,0 +1,40 @@ +package cz.tefek.srclone.graphics; + +import org.plutoengine.graphics.sprite.OrientedSprite; +import org.plutoengine.graphics.sprite.PartialTextureSprite; +import org.plutoengine.graphics.texture.MagFilter; +import org.plutoengine.graphics.texture.MinFilter; +import org.plutoengine.graphics.texture.WrapMode; +import org.plutoengine.graphics.texture.texture2d.RectangleTexture; + +import java.nio.file.Path; + +public final class DirectionalSprite extends OrientedSprite implements AutoCloseable +{ + private final RectangleTexture backingTexture; + + private DirectionalSprite(RectangleTexture backingTexture, PartialTextureSprite[] sprite) + { + super(sprite); + this.backingTexture = backingTexture; + } + + public static DirectionalSprite create(Path path, int dimensions, int width, int height, int stride) + { + var backingTexture = new RectangleTexture(); + backingTexture.load(path, MagFilter.NEAREST, MinFilter.LINEAR, WrapMode.CLAMP_TO_EDGE, WrapMode.CLAMP_TO_EDGE); + + var sprites = new PartialTextureSprite[dimensions]; + + for (int i = 0; i < sprites.length; i++) + sprites[i] = new PartialTextureSprite(backingTexture, i % stride * width, i / stride * height, width, height); + + return new DirectionalSprite(backingTexture, sprites); + } + + @Override + public void close() + { + this.backingTexture.close(); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/StarField.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/StarField.java new file mode 100755 index 0000000..4573790 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/graphics/StarField.java @@ -0,0 +1,63 @@ +package cz.tefek.srclone.graphics; + +import org.plutoengine.PlutoLocal; +import org.plutoengine.display.Display; +import org.plutoengine.graphics.RectangleRenderer2D; +import org.plutoengine.graphics.texture.texture2d.RectangleTexture; + +import java.util.Random; + +import cz.tefek.srclone.SRCloneMod; + +public class StarField +{ + private static final int STAR_COUNT = 512; + + private final Star[] stars; + + public StarField(float minX, float minY, float maxX, float maxY, RectangleTexture[] variations) + { + this.stars = new Star[STAR_COUNT]; + + var random = new Random(); + + for (int i = 0; i < STAR_COUNT; i++) + { + var x = minX + maxX * random.nextFloat(); + var y = minY + maxY * random.nextFloat(); + var z = random.nextFloat() * 0.9f + 0.1f; + var texture = variations[random.nextInt(variations.length)]; + var size = random.nextFloat() * 20.0f + 10.0f; + + stars[i] = new Star(x, y, z, texture, size); + } + } + + public void render(float camX, float camY) + { + var renderer = RectangleRenderer2D.draw(SRCloneMod.centeredQuad); + var display = PlutoLocal.components().getComponent(Display.class); + + var displayWidth = display.getWidth(); + var displayHeight = display.getHeight(); + + for (var star : this.stars) + { + var size = star.size() * star.z(); + var x = star.x() + star.z() * camX; + var y = star.y() + star.z() * camY; + + x = (x % displayWidth + displayWidth) % displayWidth; + y = (y % displayHeight + displayHeight) % displayHeight; + + renderer.at(x, y, size, size); + renderer.texture(star.texture()); + renderer.flush(); + } + } + + private record Star(float x, float y, float z, RectangleTexture texture, float size) + { + + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/Particle.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/Particle.java new file mode 100644 index 0000000..9678732 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/Particle.java @@ -0,0 +1,98 @@ +package cz.tefek.srclone.particle; + +import cz.tefek.srclone.Game; + +public abstract class Particle +{ + protected float x; + protected float y; + + protected float size; + + protected float rotation; + protected float rotationalVelocity; + protected float maxRotationalVelocity; + protected float rotationVariation; + + protected float lifetime; + protected float initialLifetime; + protected float lifetimeVariation; + + protected float initialScale; + protected float finalScale; + protected float scale; + + protected float animationTimer; + + protected boolean deadFlag; + + protected Game game; + + public Particle() + { + this.initialScale = 1.0f; + this.finalScale = 1.0f; + } + + public void init(Game game, float x, float y) + { + this.game = game; + + var rand = this.game.getRandom(); + + this.x = x; + this.y = y; + this.scale = this.initialScale; + this.lifetime = this.initialLifetime = this.initialLifetime + (rand.nextFloat() * 2.0f - 1.0f) * this.lifetimeVariation; + this.rotation = this.rotation + (rand.nextFloat() * 2.0f - 1.0f) * this.rotationVariation; + this.rotationalVelocity = this.rotationalVelocity + (rand.nextFloat() * 2.0f - 1.0f) * this.maxRotationalVelocity; + } + + public void setRotation(float rotation) + { + this.rotation = rotation; + } + + public void setSize(float size) + { + this.size = size; + } + + protected final float getRenderX() + { + return this.x + this.game.getViewX(); + } + + protected final float getRenderY() + { + return this.y + this.game.getViewY(); + } + + public void tick(float frameTime) + { + this.rotation += this.rotationalVelocity * frameTime; + this.lifetime -= frameTime; + + if (this.lifetime <= 0) + { + this.deadFlag = true; + return; + } + + float particleProgress = 1 - this.lifetime / this.initialLifetime; + + if (particleProgress >= 0 && particleProgress <= 1) + { + this.animationTimer = particleProgress; + } + + this.scale = this.initialScale * (1 - this.animationTimer) + this.finalScale * this.animationTimer; + } + + public abstract void render(); + + public boolean isDead() + { + return this.deadFlag; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleAnimatedSprite.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleAnimatedSprite.java new file mode 100644 index 0000000..5a274cf --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleAnimatedSprite.java @@ -0,0 +1,29 @@ +package cz.tefek.srclone.particle; + +import org.plutoengine.graphics.RectangleRenderer2D; +import org.plutoengine.math.BasicInterpolation; + +import cz.tefek.srclone.SRCloneMod; +import cz.tefek.srclone.graphics.DirectionalSprite; + +public class ParticleAnimatedSprite extends Particle +{ + protected DirectionalSprite sprite; + + public ParticleAnimatedSprite(DirectionalSprite sprite) + { + this.sprite = sprite; + } + + @Override + public void render() + { + var size = this.size * this.scale; + + RectangleRenderer2D.draw(SRCloneMod.centeredQuad) + .at(this.getRenderX(), this.getRenderY(), size, size) + .rotate(this.rotation) + .sprite(this.sprite.getSide(BasicInterpolation.floorLerp(this.animationTimer, 0, this.sprite.getSideCount() - 1))) + .flush(); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleExplosion.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleExplosion.java new file mode 100644 index 0000000..575a0c2 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleExplosion.java @@ -0,0 +1,16 @@ +package cz.tefek.srclone.particle; + +import cz.tefek.srclone.SRCloneMod; + +public class ParticleExplosion extends ParticleAnimatedSprite +{ + public ParticleExplosion() + { + super(SRCloneMod.parExplosion); + this.finalScale = 1.1f; + this.size = 256.0f; + this.initialLifetime = 0.7f; + this.maxRotationalVelocity = (float) Math.PI / 2.0f; + this.lifetimeVariation = 0.2f; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleImpact.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleImpact.java new file mode 100644 index 0000000..1c29a20 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleImpact.java @@ -0,0 +1,14 @@ +package cz.tefek.srclone.particle; + +import cz.tefek.srclone.SRCloneMod; + +public class ParticleImpact extends ParticleAnimatedSprite +{ + public ParticleImpact() + { + super(SRCloneMod.parImpact); + this.size = 64.0f; + this.initialLifetime = 0.10f; + this.maxRotationalVelocity = (float) Math.PI / 2.0f; + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleText.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleText.java new file mode 100644 index 0000000..81f8145 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/particle/ParticleText.java @@ -0,0 +1,35 @@ +package cz.tefek.srclone.particle; + +import org.plutoengine.graphics.ImmediateFontRenderer; +import org.plutoengine.libra.paint.LiPaint; +import org.plutoengine.libra.text.shaping.TextStyleOptions; +import org.plutoengine.util.color.Color; +import org.plutoengine.util.color.RGBA; + +import cz.tefek.srclone.SRCloneMod; + +public class ParticleText extends Particle +{ + protected String text; + + public ParticleText(String text) + { + this.text = text; + this.finalScale = 2.0f; + this.size = 16.0f; + this.initialLifetime = 2.0f; + } + + @Override + public void render() + { + var col = new RGBA(1.0f, 1.0f, 1.0f, Math.min(1.0f, 3.0f - 4.0f * this.animationTimer)); + + var style = new TextStyleOptions(this.scale * this.size) + .setPaint(LiPaint.solidColor(Color.from(col))) + .setVerticalAlign(TextStyleOptions.TextAlign.CENTER) + .setHorizontalAlign(TextStyleOptions.TextAlign.CENTER); + + ImmediateFontRenderer.drawString(this.getRenderX(), this.getRenderY(), this.text, SRCloneMod.srCloneFont, style); + } +} diff --git a/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/util/AngleUtil.java b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/util/AngleUtil.java new file mode 100644 index 0000000..4c33349 --- /dev/null +++ b/engine-demo/jsr-clone/src/main/java/cz/tefek/srclone/util/AngleUtil.java @@ -0,0 +1,17 @@ +package cz.tefek.srclone.util; + +public class AngleUtil +{ + public static float within180(float angle) + { + angle %= 2.0 * Math.PI; + + if (angle > Math.PI) + angle -= 2 * Math.PI; + + if (angle < -Math.PI) + angle += 2 * Math.PI; + + return angle; + } +}