Adjusted for the web port

This commit is contained in:
2025-04-14 17:05:14 +03:00
parent 44546b1d72
commit f6d85c5315
19 changed files with 598 additions and 35 deletions

View File

@@ -1,5 +1,12 @@
# Shattered Pixel Dungeon # Shattered Pixel Dungeon
This is a fork of Shattered Pixel Dungeon that can also compile for the web. Inspired by [this](https://github.com/gnojus/pixel-dungeon-gdx) web port for the original Pixel Dungeon.
## Libraries Used
This project uses the following libraries:
- [TeaVM](https://teavm.org), licensed under the Apache License, Version 2.0.
- [gdx-teavm](https://github.com/xpenatan/gdx-teavm), licensed under the Apache License, Version 2.0.
[Shattered Pixel Dungeon](https://shatteredpixel.com/shatteredpd/) is an open-source traditional roguelike dungeon crawler with randomized levels and enemies, and hundreds of items to collect and use. It's based on the [source code of Pixel Dungeon](https://github.com/00-Evan/pixel-dungeon-gradle), by [Watabou](https://www.watabou.ru). [Shattered Pixel Dungeon](https://shatteredpixel.com/shatteredpd/) is an open-source traditional roguelike dungeon crawler with randomized levels and enemies, and hundreds of items to collect and use. It's based on the [source code of Pixel Dungeon](https://github.com/00-Evan/pixel-dungeon-gradle), by [Watabou](https://www.watabou.ru).
Shattered Pixel Dungeon currently compiles for Android, iOS, and Desktop platforms. You can find official releases of the game on: Shattered Pixel Dungeon currently compiles for Android, iOS, and Desktop platforms. You can find official releases of the game on:

View File

@@ -24,6 +24,8 @@ package com.watabou.glwrap;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import com.badlogic.gdx.graphics.GL20;
import com.watabou.utils.DeviceCompat;
public class Attribute { public class Attribute {
@@ -37,19 +39,21 @@ public class Attribute {
return location; return location;
} }
private static final GL20 activeGL = (DeviceCompat.isWeb()) ? Gdx.gl30 : Gdx.gl;
public void enable() { public void enable() {
Gdx.gl.glEnableVertexAttribArray( location ); activeGL.glEnableVertexAttribArray( location );
} }
public void disable() { public void disable() {
Gdx.gl.glDisableVertexAttribArray( location ); activeGL.glDisableVertexAttribArray( location );
} }
public void vertexPointer( int size, int stride, FloatBuffer ptr ) { public void vertexPointer( int size, int stride, FloatBuffer ptr ) {
Gdx.gl.glVertexAttribPointer( location, size, Gdx.gl.GL_FLOAT, false, stride * 4, ptr ); activeGL.glVertexAttribPointer( location, size, GL20.GL_FLOAT, false, stride * 4, ptr );
} }
public void vertexBuffer( int size, int stride, int offset) { public void vertexBuffer( int size, int stride, int offset) {
Gdx.gl.glVertexAttribPointer(location, size, Gdx.gl.GL_FLOAT, false, stride * 4, offset * 4); activeGL.glVertexAttribPointer( location, size, GL20.GL_FLOAT, false, stride * 4, offset * 4 );
} }
} }

View File

@@ -22,6 +22,8 @@
package com.watabou.noosa; package com.watabou.noosa;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.watabou.utils.DeviceCompat;
import com.watabou.glscripts.Script; import com.watabou.glscripts.Script;
import com.watabou.glwrap.Attribute; import com.watabou.glwrap.Attribute;
import com.watabou.glwrap.Quad; import com.watabou.glwrap.Quad;
@@ -34,6 +36,8 @@ import java.nio.ShortBuffer;
public class NoosaScript extends Script { public class NoosaScript extends Script {
private static final GL20 activeGL = (DeviceCompat.isWeb()) ? Gdx.gl30 : Gdx.gl20;
public Uniform uCamera; public Uniform uCamera;
public Uniform uModel; public Uniform uModel;
public Uniform uTex; public Uniform uTex;
@@ -44,6 +48,7 @@ public class NoosaScript extends Script {
private Camera lastCamera; private Camera lastCamera;
private int vertexBufferId;
public NoosaScript() { public NoosaScript() {
super(); super();
@@ -60,6 +65,12 @@ public class NoosaScript extends Script {
Quad.setupIndices(); Quad.setupIndices();
Quad.bindIndices(); Quad.bindIndices();
if (DeviceCompat.isWeb()) {
vertexBufferId = Gdx.gl30.glGenBuffer();
Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, vertexBufferId);
Gdx.gl30.glBufferData(Gdx.gl30.GL_ARRAY_BUFFER, 16384, null, Gdx.gl30.GL_DYNAMIC_DRAW);
Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, 0);
}
} }
@Override @Override
@@ -73,27 +84,50 @@ public class NoosaScript extends Script {
} }
public void drawElements( FloatBuffer vertices, ShortBuffer indices, int size ) { public void drawElements( FloatBuffer vertices, ShortBuffer indices, int size ) {
if (DeviceCompat.isWeb()) {
Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, vertexBufferId);
((Buffer)vertices).position( 0 );
Gdx.gl30.glBufferSubData(Gdx.gl30.GL_ARRAY_BUFFER, 0, vertices.remaining() * 4, vertices);
((Buffer)vertices).position( 0 ); aXY.vertexBuffer(2, 4, 0);
aXY.vertexPointer( 2, 4, vertices ); aUV.vertexBuffer(2, 4, 2);
((Buffer)vertices).position( 2 ); Quad.releaseIndices();
aUV.vertexPointer( 2, 4, vertices ); Gdx.gl30.glDrawElements( Gdx.gl30.GL_TRIANGLES, size, Gdx.gl30.GL_UNSIGNED_SHORT, indices );
Quad.bindIndices();
Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, 0);
} else {
((Buffer)vertices).position( 0 );
aXY.vertexPointer( 2, 4, vertices );
Quad.releaseIndices(); ((Buffer)vertices).position( 2 );
Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, size, Gdx.gl20.GL_UNSIGNED_SHORT, indices ); aUV.vertexPointer( 2, 4, vertices );
Quad.bindIndices();
Quad.releaseIndices();
Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, size, Gdx.gl20.GL_UNSIGNED_SHORT, indices );
Quad.bindIndices();
}
} }
public void drawQuad( FloatBuffer vertices ) { public void drawQuad( FloatBuffer vertices ) {
if (DeviceCompat.isWeb()) {
Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, vertexBufferId);
((Buffer)vertices).position( 0 );
Gdx.gl30.glBufferSubData(Gdx.gl30.GL_ARRAY_BUFFER, 0, vertices.remaining() * 4, vertices);
((Buffer)vertices).position( 0 ); Gdx.gl30.glVertexAttribPointer(aXY.location(), 2, Gdx.gl30.GL_FLOAT, false, 4 * 4, 0);
aXY.vertexPointer( 2, 4, vertices ); Gdx.gl30.glVertexAttribPointer(aUV.location(), 2, Gdx.gl30.GL_FLOAT, false, 4 * 4, 2 * 4);
Gdx.gl30.glDrawElements( Gdx.gl30.GL_TRIANGLES, Quad.SIZE, Gdx.gl30.GL_UNSIGNED_SHORT, 0 );
Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, 0);
} else {
((Buffer)vertices).position( 0 );
aXY.vertexPointer( 2, 4, vertices );
((Buffer)vertices).position( 2 ); ((Buffer)vertices).position( 2 );
aUV.vertexPointer( 2, 4, vertices ); aUV.vertexPointer( 2, 4, vertices );
Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE, Gdx.gl20.GL_UNSIGNED_SHORT, 0 ); Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE, Gdx.gl20.GL_UNSIGNED_SHORT, 0 );
}
} }
public void drawQuad( Vertexbuffer buffer ) { public void drawQuad( Vertexbuffer buffer ) {
@@ -107,7 +141,7 @@ public class NoosaScript extends Script {
buffer.release(); buffer.release();
Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE, Gdx.gl20.GL_UNSIGNED_SHORT, 0 ); activeGL.glDrawElements( GL20.GL_TRIANGLES, Quad.SIZE, GL20.GL_UNSIGNED_SHORT, 0 );
} }
public void drawQuadSet( FloatBuffer vertices, int size ) { public void drawQuadSet( FloatBuffer vertices, int size ) {
@@ -116,13 +150,26 @@ public class NoosaScript extends Script {
return; return;
} }
((Buffer)vertices).position( 0 ); if (DeviceCompat.isWeb()) {
aXY.vertexPointer( 2, 4, vertices ); Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, vertexBufferId);
((Buffer)vertices).position( 0 );
Gdx.gl30.glBufferSubData(Gdx.gl30.GL_ARRAY_BUFFER, 0, vertices.remaining() * 4, vertices);
((Buffer)vertices).position( 2 ); Gdx.gl30.glVertexAttribPointer(aXY.location(), 2, Gdx.gl30.GL_FLOAT, false, 4 * 4, 0);
aUV.vertexPointer( 2, 4, vertices ); Gdx.gl30.glVertexAttribPointer(aUV.location(), 2, Gdx.gl30.GL_FLOAT, false, 4 * 4, 2 * 4);
Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE * size, Gdx.gl20.GL_UNSIGNED_SHORT, 0 ); Gdx.gl30.glDrawElements( Gdx.gl30.GL_TRIANGLES, Quad.SIZE * size, Gdx.gl30.GL_UNSIGNED_SHORT, 0 );
Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, 0);
} else {
((Buffer)vertices).position( 0 );
aXY.vertexPointer( 2, 4, vertices );
((Buffer)vertices).position( 2 );
aUV.vertexPointer( 2, 4, vertices );
Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE * size, Gdx.gl20.GL_UNSIGNED_SHORT, 0 );
}
} }
public void drawQuadSet( Vertexbuffer buffer, int length, int offset ){ public void drawQuadSet( Vertexbuffer buffer, int length, int offset ){
@@ -140,7 +187,7 @@ public class NoosaScript extends Script {
buffer.release(); buffer.release();
Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE * length, Gdx.gl20.GL_UNSIGNED_SHORT, Quad.SIZE * Short.SIZE/8 * offset ); activeGL.glDrawElements( GL20.GL_TRIANGLES, Quad.SIZE * length, GL20.GL_UNSIGNED_SHORT, Quad.SIZE * Short.SIZE/8 * offset );
} }
public void lighting( float rm, float gm, float bm, float am, float ra, float ga, float ba, float aa ) { public void lighting( float rm, float gm, float bm, float am, float ra, float ga, float ba, float aa ) {
@@ -161,7 +208,7 @@ public class NoosaScript extends Script {
uCamera.valueM4( camera.matrix ); uCamera.valueM4( camera.matrix );
if (!camera.fullScreen) { if (!camera.fullScreen) {
Gdx.gl20.glEnable( Gdx.gl20.GL_SCISSOR_TEST ); activeGL.glEnable( GL20.GL_SCISSOR_TEST );
//This fixes pixel scaling issues on some hidpi displays (mainly on macOS) //This fixes pixel scaling issues on some hidpi displays (mainly on macOS)
// because for some reason all other openGL operations work on virtual pixels // because for some reason all other openGL operations work on virtual pixels
@@ -169,13 +216,13 @@ public class NoosaScript extends Script {
float xScale = (Gdx.graphics.getBackBufferWidth() / (float)Game.width ); float xScale = (Gdx.graphics.getBackBufferWidth() / (float)Game.width );
float yScale = ((Gdx.graphics.getBackBufferHeight()-Game.bottomInset) / (float)Game.height ); float yScale = ((Gdx.graphics.getBackBufferHeight()-Game.bottomInset) / (float)Game.height );
Gdx.gl20.glScissor( activeGL.glScissor(
Math.round(camera.x * xScale), Math.round(camera.x * xScale),
Math.round((Game.height - camera.screenHeight - camera.y) * yScale) + Game.bottomInset, Math.round((Game.height - camera.screenHeight - camera.y) * yScale) + Game.bottomInset,
Math.round(camera.screenWidth * xScale), Math.round(camera.screenWidth * xScale),
Math.round(camera.screenHeight * yScale)); Math.round(camera.screenHeight * yScale));
} else { } else {
Gdx.gl20.glDisable( Gdx.gl20.GL_SCISSOR_TEST ); activeGL.glDisable( GL20.GL_SCISSOR_TEST );
} }
} }
} }
@@ -189,8 +236,41 @@ public class NoosaScript extends Script {
return SHADER; return SHADER;
} }
private static final String SHADER = private static String SHADER;
static {
if (DeviceCompat.isWeb()) {
SHADER =
//vertex shader
"#version 300 es\n" +
"uniform mat4 uCamera;\n" +
"uniform mat4 uModel;\n" +
"in vec4 aXYZW;\n" +
"in vec2 aUV;\n" +
"out vec2 vUV;\n" +
"void main() {\n" +
" gl_Position = uCamera * uModel * aXYZW;\n" +
" vUV = aUV;\n" +
"}\n" +
//this symbol separates the vertex and fragment shaders (see Script.compile)
"//\n" +
//fragment shader
//preprocessor directives let us define precision on GLES platforms, and ignore it elsewhere
"#version 300 es\n" +
"#ifdef GL_ES\n" +
" precision mediump float;\n" +
"#endif\n" +
"in vec2 vUV;\n" +
"uniform sampler2D uTex;\n" +
"uniform vec4 uColorM;\n" +
"uniform vec4 uColorA;\n" +
"out vec4 fragColor;\n" +
"void main() {\n" +
" fragColor = texture( uTex, vUV ) * uColorM + uColorA;\n" +
"}\n";
} else {
SHADER =
//vertex shader //vertex shader
"uniform mat4 uCamera;\n" + "uniform mat4 uCamera;\n" +
"uniform mat4 uModel;\n" + "uniform mat4 uModel;\n" +
@@ -217,4 +297,6 @@ public class NoosaScript extends Script {
"void main() {\n" + "void main() {\n" +
" gl_FragColor = texture2D( uTex, vUV ) * uColorM + uColorA;\n" + " gl_FragColor = texture2D( uTex, vUV ) * uColorM + uColorA;\n" +
"}\n"; "}\n";
}
}
} }

View File

@@ -22,6 +22,7 @@
package com.watabou.noosa; package com.watabou.noosa;
import com.watabou.glscripts.Script; import com.watabou.glscripts.Script;
import com.watabou.utils.DeviceCompat;
//This class should be used on heavy pixel-fill based loads when lighting is not needed. //This class should be used on heavy pixel-fill based loads when lighting is not needed.
// It skips the lighting component of the fragment shader, giving a significant performance boost // It skips the lighting component of the fragment shader, giving a significant performance boost
@@ -44,8 +45,40 @@ public class NoosaScriptNoLighting extends NoosaScript {
return SHADER; return SHADER;
} }
private static final String SHADER = private static String SHADER;
static {
if (DeviceCompat.isWeb()) {
SHADER =
//vertex shader
"#version 300 es\n" +
"uniform mat4 uCamera;\n" +
"uniform mat4 uModel;\n" +
"in vec4 aXYZW;\n" +
"in vec2 aUV;\n" +
"out vec2 vUV;\n" +
"void main() {\n" +
" gl_Position = uCamera * uModel * aXYZW;\n" +
" vUV = aUV;\n" +
"}\n" +
//this symbol separates the vertex and fragment shaders (see Script.compile)
"//\n" +
//fragment shader
//preprocessor directives let us define precision on GLES platforms, and ignore it elsewhere
"#version 300 es\n" +
"#ifdef GL_ES\n" +
" precision mediump float;\n" +
"#endif\n" +
"in vec2 vUV;\n" +
"uniform sampler2D uTex;\n" +
"out vec4 fragColor;\n" +
"void main() {\n" +
" fragColor = texture( uTex, vUV );\n" +
"}\n";
} else {
SHADER =
//vertex shader //vertex shader
"uniform mat4 uCamera;\n" + "uniform mat4 uCamera;\n" +
"uniform mat4 uModel;\n" + "uniform mat4 uModel;\n" +
@@ -70,4 +103,6 @@ public class NoosaScriptNoLighting extends NoosaScript {
"void main() {\n" + "void main() {\n" +
" gl_FragColor = texture2D( uTex, vUV );\n" + " gl_FragColor = texture2D( uTex, vUV );\n" +
"}\n"; "}\n";
}
}
} }

View File

@@ -32,6 +32,7 @@ import com.badlogic.gdx.math.Affine2;
import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Matrix4;
import com.watabou.glwrap.Matrix; import com.watabou.glwrap.Matrix;
import com.watabou.glwrap.Quad; import com.watabou.glwrap.Quad;
import com.watabou.utils.DeviceCompat;
import java.nio.Buffer; import java.nio.Buffer;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
@@ -79,9 +80,11 @@ public class RenderedText extends Image {
private synchronized void measure(){ private synchronized void measure(){
if (!DeviceCompat.isWeb()) {
if (Thread.currentThread().getName().equals("SHPD Actor Thread")){ if (Thread.currentThread().getName().equals("SHPD Actor Thread")){
throw new RuntimeException("Text measured from the actor thread!"); throw new RuntimeException("Text measured from the actor thread!");
} }
}
if ( text == null || text.equals("") ) { if ( text == null || text.equals("") ) {
text = ""; text = "";

View File

@@ -27,6 +27,7 @@ import com.watabou.glwrap.Quad;
import com.watabou.glwrap.Vertexbuffer; import com.watabou.glwrap.Vertexbuffer;
import com.watabou.utils.Rect; import com.watabou.utils.Rect;
import com.watabou.utils.RectF; import com.watabou.utils.RectF;
import com.watabou.utils.DeviceCompat;;
import java.nio.Buffer; import java.nio.Buffer;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
@@ -197,7 +198,9 @@ public class Tilemap extends Visual {
public void draw() { public void draw() {
super.draw(); super.draw();
if (DeviceCompat.isWeb()) {
fullUpdate = true;
}
if (!updated.isEmpty()) { if (!updated.isEmpty()) {
updateVertices(); updateVertices();
if (buffer == null) if (buffer == null)

View File

@@ -25,6 +25,7 @@ import com.badlogic.gdx.utils.JsonReader;
import com.badlogic.gdx.utils.JsonValue; import com.badlogic.gdx.utils.JsonValue;
import com.badlogic.gdx.utils.JsonWriter; import com.badlogic.gdx.utils.JsonWriter;
import com.watabou.noosa.Game; import com.watabou.noosa.Game;
import com.watabou.utils.DeviceCompat;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@@ -484,7 +485,15 @@ public class Bundle {
} }
//useful to turn this off for save data debugging. //useful to turn this off for save data debugging.
private static final boolean compressByDefault = true; private static final boolean compressByDefault;
static {
if (DeviceCompat.isWeb()) {
compressByDefault = false;
} else {
compressByDefault = true;
}
}
private static final int GZIP_BUFFER = 1024*4; //4 kb private static final int GZIP_BUFFER = 1024*4; //4 kb

View File

@@ -25,6 +25,7 @@ import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input; import com.badlogic.gdx.Input;
import com.badlogic.gdx.utils.SharedLibraryLoader; import com.badlogic.gdx.utils.SharedLibraryLoader;
import com.watabou.noosa.Game; import com.watabou.noosa.Game;
import com.badlogic.gdx.Application;
//TODO migrate to platformSupport class //TODO migrate to platformSupport class
public class DeviceCompat { public class DeviceCompat {
@@ -49,15 +50,22 @@ public class DeviceCompat {
} }
public static boolean isAndroid(){ public static boolean isAndroid(){
return SharedLibraryLoader.isAndroid; return Gdx.app.getType() == Application.ApplicationType.Android;
} }
public static boolean isiOS(){ public static boolean isiOS(){
return SharedLibraryLoader.isIos; return Gdx.app.getType() == Application.ApplicationType.iOS;
} }
public static boolean isDesktop(){ public static boolean isDesktop(){
return SharedLibraryLoader.isWindows || SharedLibraryLoader.isMac || SharedLibraryLoader.isLinux; return System.getProperty("os.name").toLowerCase().contains("win") ||
System.getProperty("os.name").toLowerCase().contains("mac") ||
System.getProperty("os.name").toLowerCase().contains("nux") ||
Gdx.app.getType() == Application.ApplicationType.WebGL;
}
public static boolean isWeb(){
return Gdx.app.getType() == Application.ApplicationType.WebGL;
} }
public static boolean hasHardKeyboard(){ public static boolean hasHardKeyboard(){

View File

@@ -29,6 +29,9 @@ allprojects {
gdxVersion = '1.12.1' gdxVersion = '1.12.1'
gdxControllersVersion = '2.2.4-SNAPSHOT' gdxControllersVersion = '2.2.4-SNAPSHOT'
robovmVersion = '2.3.21' robovmVersion = '2.3.21'
gdxTeaVMVersion = '1.0.5'
teaVMVersion = '0.11.0'
} }
version = appVersionName version = appVersionName

View File

@@ -43,6 +43,7 @@ import com.watabou.noosa.Game;
import com.watabou.utils.Bundlable; import com.watabou.utils.Bundlable;
import com.watabou.utils.Bundle; import com.watabou.utils.Bundle;
import com.watabou.utils.FileUtils; import com.watabou.utils.FileUtils;
import com.watabou.utils.DeviceCompat;;
import java.io.IOException; import java.io.IOException;
import java.text.DateFormat; import java.text.DateFormat;
@@ -56,6 +57,7 @@ import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.Random;
public enum Rankings { public enum Rankings {
@@ -65,6 +67,11 @@ public enum Rankings {
public static final String RANKINGS_FILE = "rankings.dat"; public static final String RANKINGS_FILE = "rankings.dat";
private static String generateUUID() {
Random random = new Random();
return Long.toHexString(random.nextLong()) + "-" + Long.toHexString(random.nextLong());
}
public ArrayList<Record> records; public ArrayList<Record> records;
public int lastRecord; public int lastRecord;
public int totalNumber; public int totalNumber;
@@ -116,7 +123,11 @@ public enum Rankings {
INSTANCE.saveGameData(rec); INSTANCE.saveGameData(rec);
if (DeviceCompat.isWeb()) {
rec.gameID = generateUUID();
} else {
rec.gameID = UUID.randomUUID().toString(); rec.gameID = UUID.randomUUID().toString();
}
if (rec.daily){ if (rec.daily){
if (Dungeon.dailyReplay){ if (Dungeon.dailyReplay){
@@ -533,7 +544,11 @@ public enum Rankings {
if (bundle.contains(DATA)) gameData = bundle.getBundle(DATA); if (bundle.contains(DATA)) gameData = bundle.getBundle(DATA);
if (bundle.contains(ID)) gameID = bundle.getString(ID); if (bundle.contains(ID)) gameID = bundle.getString(ID);
if (DeviceCompat.isWeb()) {
if (gameID == null) gameID = generateUUID();
} else {
if (gameID == null) gameID = UUID.randomUUID().toString(); if (gameID == null) gameID = UUID.randomUUID().toString();
}
} }

View File

@@ -643,11 +643,13 @@ public class GameScene extends PixelScene {
public void destroy() { public void destroy() {
//tell the actor thread to finish, then wait for it to complete any actions it may be doing. //tell the actor thread to finish, then wait for it to complete any actions it may be doing.
if (!DeviceCompat.isWeb()) {
if (!waitForActorThread( 4500, true )){ if (!waitForActorThread( 4500, true )){
Throwable t = new Throwable(); Throwable t = new Throwable();
t.setStackTrace(actorThread.getStackTrace()); t.setStackTrace(actorThread.getStackTrace());
throw new RuntimeException("timeout waiting for actor thread! ", t); throw new RuntimeException("timeout waiting for actor thread! ", t);
} }
}
Emitter.freezeEmitters = false; Emitter.freezeEmitters = false;

View File

@@ -89,7 +89,7 @@ public class v3_X_Changes {
changes.hardlight(Window.TITLE_COLOR); changes.hardlight(Window.TITLE_COLOR);
changeInfos.add(changes); changeInfos.add(changes);
if (DeviceCompat.isDesktop() && SharedLibraryLoader.isLinux) { if (DeviceCompat.isDesktop() && System.getProperty("os.name").toLowerCase().contains("nux")) {
changes.addButton(new ChangeButton(Icons.DISPLAY.get(), "A Note for Steam Deck users", changes.addButton(new ChangeButton(Icons.DISPLAY.get(), "A Note for Steam Deck users",
"A bug was fixed in this patch which affected display scaling on Steam Deck. Due to a quirk in how the Steam Deck reported display dimensions, the game incorrectly thought Steam Deck's screen was about 4\", instead of 7\".\n" + "A bug was fixed in this patch which affected display scaling on Steam Deck. Due to a quirk in how the Steam Deck reported display dimensions, the game incorrectly thought Steam Deck's screen was about 4\", instead of 7\".\n" +
"\n" + "\n" +

154
html/build.gradle Normal file
View File

@@ -0,0 +1,154 @@
apply plugin: 'java-library'
dependencies {
implementation "com.badlogicgames.gdx:gdx:$gdxVersion"
implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion"
implementation "com.github.xpenatan.gdx-teavm:backend-teavm:$gdxTeaVMVersion"
implementation "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
implementation "com.github.xpenatan.gdx-teavm:gdx-freetype-teavm:$gdxTeaVMVersion"
implementation project(':core')
implementation "org.teavm:teavm-jso:$teaVMVersion"
implementation "org.teavm:teavm-core:$teaVMVersion"
}
repositories {
google()
mavenCentral()
maven { url 'https://teavm.org/maven/repository' }
}
tasks.register('compileClient', JavaExec) {
classpath(sourceSets.main.runtimeClasspath)
mainClass.set('com.shatteredpixel.shatteredpixeldungeon.html.Compile')
jvmArgs('-Xms8g', '-Xmx14g', '-Xss16m', '-XX:MaxMetaspaceSize=12g', '-Dfile.encoding=UTF-8')
outputs.dir '../release/webapp'
doFirst {
println "Running compileClient..."
}
doLast {
println "compileClient completed!"
}
}
task modifyHtml {
doLast {
def htmlFile = file("../release/webapp/index.html")
def destinationDir = file("../release/webapp/assets")
def sourceFavicon = file("../desktop/src/main/assets/icons/windows.ico")
def sourceGif = file("../html/src/main/assets/logo.gif")
def destinationStartupLogo = file("../release/webapp/startup-logo.png")
// copy windows.ico
if (sourceFavicon.exists()) {
def destinationFile = new File(destinationDir, "windows.ico")
sourceFavicon.withInputStream { input ->
destinationFile.withOutputStream { output ->
output << input
}
}
println "Copied windows.ico to assets folder."
} else {
println "windows.ico does not exist. Skipping copy step."
}
if (sourceGif.exists()) {
destinationStartupLogo.parentFile.mkdirs()
sourceGif.withInputStream { input ->
destinationStartupLogo.withOutputStream { output ->
output << input
}
}
println "Replaced startup-logo.png with a gif."
} else {
println "Gif does not exist. Skipping replacement step."
}
if (htmlFile.exists()) {
def faviconLink = '<link rel="icon" href="assets/windows.ico" type="image/x-icon">'
def content = htmlFile.text
// add a link for the favicon
if (content.contains("</head>")) {
content = content.replace("</head>", faviconLink + "\n</head>")
println "Favicon link added to index.html."
}
// Change page title
if (content.contains("<title>")) {
content = content.replaceFirst("<title>.*?</title>", "<title>Shattered Pixel Dungeon</title>")
println "Title updated to 'Shattered Pixel Dungeon'."
} else {
println "No <title> tag found in index.html. Skipping title update."
}
if (content.contains("<style>") && content.contains("</style>")) {
content = content.replaceAll(
"(?s)<style>.*?</style>",
"""<style>
body {
display: flex;
justify-content: center;
align-items: center;
background: #000;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
#progress {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
#progress-img {
display: flex;
width: 850px;
height: auto;
margin-bottom: -20px;
}
#progress-box {
background: rgba(255,255,255,0.1);
border: 2px solid #fff;
display: flex;
margin-top: -20px;
justify-content: center;
align-items: center;
position: relative;
height: 30px;
width: 600px;
}
#progress-bar {
display: block;
background: #44FF82;
height: 20px;
width: 0%;
}
</style>"""
)
println "Successfully replaced the <style> block in index.html."
} else {
println "No <style> block found in index.html. Skipping style replacement."
}
htmlFile.text = content
} else {
println "index.html does not exist. Skipping favicon injection."
}
}
}
tasks.named('modifyHtml') {
mustRunAfter('compileClient')
}
tasks.named('build') {
dependsOn 'compileClient'
finalizedBy 'modifyHtml'
}

7
html/buildTestWeb.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
../gradlew :html:build
wait
if [[ -d ../release/webapp/ ]]; then
python3 -m http.server 8100 -d ../release/webapp/
fi

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

View File

@@ -0,0 +1,62 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* This file is derived from micronaut-libgdx-teavm (https://github.com/hollingsworthd/micronaut-libgdx-teavm),
* originally licensed under the Apache License, Version 2.0.
*
* Modifications made by Konsthol on 13/4/25:
* - Adjusted to compile Shattered Pixel Dungeon
*
* Copyright 2022 Daniel Hollingsworth
*/
package com.shatteredpixel.shatteredpixeldungeon.html;
import com.github.xpenatan.gdx.backends.teavm.config.AssetFileHandle;
import com.github.xpenatan.gdx.backends.teavm.config.TeaBuildConfiguration;
import com.github.xpenatan.gdx.backends.teavm.config.TeaBuilder;
import com.github.xpenatan.gdx.backends.teavm.gen.SkipClass;
import java.io.File;
import java.io.IOException;
import org.teavm.tooling.TeaVMTool;
import org.teavm.vm.TeaVMOptimizationLevel;
@SkipClass
public class Compile {
public static void main(String[] args) throws IOException {
deleteDir(new File("../release/webapp"));
TeaBuildConfiguration conf = new TeaBuildConfiguration();
conf.webappPath = new File("../release").getAbsolutePath();
conf.assetsPath.add(new AssetFileHandle("../core/src/main/assets"));
TeaVMTool tool = TeaBuilder.config(conf);
tool.setMainClass(TeaVMLauncher.class.getName());
tool.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED);
tool.setObfuscated(true);
tool.setShortFileNames(true);
tool.setSourceFilesCopied(false);
tool.setStrict(false);
tool.setSourceMapsFileGenerated(false);
tool.setDebugInformationGenerated(false);
tool.setIncremental(false);
TeaBuilder.build(tool);
}
private static void deleteDir(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
deleteDir(file);
}
}
dir.delete();
}
}

View File

@@ -0,0 +1,122 @@
package com.shatteredpixel.shatteredpixeldungeon.html;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Graphics;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.g2d.PixmapPacker;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.shatteredpixel.shatteredpixeldungeon.SPDSettings;
import com.watabou.input.ControllerHandler;
import com.watabou.noosa.Game;
import com.watabou.utils.PlatformSupport;
import org.teavm.jso.JSBody;
import java.util.HashMap;
import java.util.regex.Pattern;
public class HtmlPlatformSupport extends PlatformSupport {
@Override
public void updateDisplaySize() {
if (Gdx.app == null || Gdx.graphics == null) {
System.err.println("Gdx is not initialized yet. Skipping display size update.");
return;
}
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
}
@Override
public void updateSystemUI() {
Gdx.app.postRunnable( new Runnable() {
@Override
public void run() {
Object canvas = getCanvasElement("canvas");
if (SPDSettings.fullscreen()) {
System.out.println("Requesting fullscreen.");
requestFullscreen("canvas");
} else {
System.out.println("Exiting fullscreen.");
exitFullscreen();
setCanvasSize("canvas", SPDSettings.windowResolution().x, SPDSettings.windowResolution().y);
}
}
});
}
@JSBody(script = ""
+ "document.body.addEventListener('click', function() {"
+ " console.log('User clicked. Triggering fullscreen and audio...');"
+ " document.getElementById('canvas').requestFullscreen();"
+ " if (!window.audioContext) {"
+ " window.audioContext = new (window.AudioContext || window.webkitAudioContext)();"
+ " }"
+ " window.audioContext.resume().then(() => {"
+ " console.log('AudioContext resumed successfully.');"
+ " }).catch((err) => {"
+ " console.error('Failed to resume AudioContext:', err);"
+ " });"
+ "}, { once: true });")
public static native void setupClickListener();
@JSBody(params = {"id"}, script = "document.getElementById(id).requestFullscreen();")
private static native void requestFullscreen(String id);
@JSBody(params = {"id"}, script = "return document.getElementById(id);")
private static native Object getCanvasElement(String id);
@JSBody(params = {}, script = "document.exitFullscreen();")
private static native void exitFullscreen();
@JSBody(params = {"id", "width", "height"}, script = "var canvas = document.getElementById(id); canvas.width = width; canvas.height = height;")
private static native void setCanvasSize(String id, int width, int height);
@Override
public boolean connectedToUnmeteredNetwork() {
return true;
}
@Override
public boolean supportsVibration() {
return ControllerHandler.vibrationSupported();
}
private static FreeTypeFontGenerator basicFontGenerator;
@Override
public void setupFontGenerators(int pageSize, boolean systemfont) {
//don't bother doing anything if nothing has changed
if (fonts != null && this.pageSize == pageSize && this.systemfont == systemfont) {
return;
}
this.pageSize = pageSize;
this.systemfont = systemfont;
resetGenerators(false);
fonts = new HashMap<>();
basicFontGenerator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/pixel_font.ttf"));
fonts.put(basicFontGenerator, new HashMap<>());
packer = new PixmapPacker(pageSize, pageSize, Pixmap.Format.RGBA8888, 1, false);
}
@Override
protected FreeTypeFontGenerator getGeneratorForString( String input ) {
return basicFontGenerator;
}
private final Pattern regularsplitter = Pattern.compile("(?<=\n)|(?=\n)|(?<=_)|(?=_)|(?<=\\*\\*)|(?=\\*\\*)");
private final Pattern regularsplitterMultiline = Pattern.compile("(?<= )|(?= )|(?<=\n)|(?=\n)|(?<=_)|(?=_)|(?<=\\*\\*)|(?=\\*\\*)");
@Override
public String[] splitforTextBlock(String text, boolean multiline) {
if (multiline) {
return regularsplitterMultiline.split(text);
} else {
return regularsplitter.split(text);
}
}
}

View File

@@ -0,0 +1,46 @@
package com.shatteredpixel.shatteredpixeldungeon.html;
import com.badlogic.gdx.Files;
import com.watabou.noosa.Game;
import com.shatteredpixel.shatteredpixeldungeon.ShatteredPixelDungeon;
import com.github.xpenatan.gdx.backends.teavm.TeaApplicationConfiguration;
import com.github.xpenatan.gdx.backends.teavm.TeaApplication;
import com.watabou.utils.FileUtils;
public class TeaVMLauncher {
public static void main(String[] args) {
TeaApplicationConfiguration config = new TeaApplicationConfiguration("canvas");
config.width = 0;
config.height = 0;
config.useGL30 = true;
config.preloadListener = assetLoader -> {
try {
assetLoader.loadScript("freetype.js");
} catch (Exception e) {
System.err.println("Error loading freetype.js: " + e.getMessage());
}
};
initializeServices();
FileUtils.setDefaultFileProperties(Files.FileType.Local, "");
try {
HtmlPlatformSupport platformSupport = new HtmlPlatformSupport();
platformSupport.setupClickListener();
new TeaApplication(new ShatteredPixelDungeon(platformSupport), config);
} catch (Exception e) {
System.err.println("Error launching TeaApplication: " + e.getMessage());
e.printStackTrace();
}
}
private static void initializeServices() {
Game.version = "3.0.2";
Game.versionCode = 833;
}
}

View File

@@ -6,6 +6,7 @@ include ':core'
include ':android' include ':android'
include ':ios' include ':ios'
include ':desktop' include ':desktop'
include ':html'
//service modules //service modules
include ':services' include ':services'