diff --git a/.classpath b/.classpath
index ccd4c4c..07661e7 100644
--- a/.classpath
+++ b/.classpath
@@ -2,6 +2,5 @@
-
diff --git a/.gitignore b/.gitignore
index 9bf81e5..52007bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/main.class
+/dist/
diff --git a/README.md b/README.md
index 3c706f6..0425201 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,11 @@ For non-windows users, you will need to have Java installed. Download the same g
other units in safe distance (hard difficulty).
+### Online leaderboard: ###
+
+Finish a match and submit your time to a global online leaderboard, ranked separately per difficulty. Scores are stored and served through Firebase.
+
+
### Demo playthrough video: ###
https://www.youtube.com/watch?v=hUJWMpyWdVo
diff --git a/audio/.gitignore b/audio/.gitignore
new file mode 100644
index 0000000..165993f
--- /dev/null
+++ b/audio/.gitignore
@@ -0,0 +1,4 @@
+/SoundEngine.class
+/SoundEngine$Sfx.class
+/SoundSynth.class
+/SoundSynth$Waveform.class
diff --git a/audio/SoundEngine.java b/audio/SoundEngine.java
new file mode 100644
index 0000000..9c41cfc
--- /dev/null
+++ b/audio/SoundEngine.java
@@ -0,0 +1,139 @@
+package audio;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.Clip;
+import javax.sound.sampled.FloatControl;
+import javax.sound.sampled.LineUnavailableException;
+
+import core.camera;
+
+//Small non-blocking sound engine on top of javax.sound.sampled. Every effect keeps a
+//pool of Clips so overlapping triggers (e.g. several units firing the same frame)
+//don't cut each other off. play() never blocks the caller - Clip playback runs on the
+//Java Sound mixer thread.
+public class SoundEngine {
+
+ public enum Sfx {
+ CLICK, SELECT, MOVE, ATTACK, HARVEST,
+ SHOOT_CANNON, SHOOT_CANNON_HEAVY, SHOOT_AUTOCANNON,
+ SHOOT_ROCKET, SHOOT_MISSILE, SHOOT_RAILGUN,
+ EXPLOSION_SMALL, EXPLOSION_LARGE,
+ DEPLOY_BUILDING, DEPLOY_VEHICLE,
+ VICTORY, DEFEAT,
+ CREDIT_EARN, CREDIT_SPEND
+ }
+
+ public static volatile boolean enabled = true;
+
+ private static final int POOL_SIZE = 4;
+ private static final AudioFormat FORMAT = new AudioFormat(SoundSynth.SAMPLE_RATE, 16, 1, true, false);
+
+ private static final Clip[][] pools = new Clip[Sfx.values().length][];
+ private static final int[] nextSlot = new int[Sfx.values().length];
+ private static boolean initialized;
+
+ public static synchronized void init() {
+ if (initialized)
+ return;
+ initialized = true;
+
+ load(Sfx.CLICK, SoundSynth.click());
+ load(Sfx.SELECT, SoundSynth.select());
+ load(Sfx.MOVE, SoundSynth.move());
+ load(Sfx.ATTACK, SoundSynth.attack());
+ load(Sfx.HARVEST, SoundSynth.harvest());
+ load(Sfx.SHOOT_CANNON, SoundSynth.shootCannon());
+ load(Sfx.SHOOT_CANNON_HEAVY, SoundSynth.shootCannonHeavy());
+ load(Sfx.SHOOT_AUTOCANNON, SoundSynth.shootAutocannon());
+ load(Sfx.SHOOT_ROCKET, SoundSynth.shootRocket());
+ load(Sfx.SHOOT_MISSILE, SoundSynth.shootMissile());
+ load(Sfx.SHOOT_RAILGUN, SoundSynth.shootRailgun());
+ load(Sfx.EXPLOSION_SMALL, SoundSynth.explosionSmall());
+ load(Sfx.EXPLOSION_LARGE, SoundSynth.explosionLarge());
+ load(Sfx.DEPLOY_BUILDING, SoundSynth.deployBuilding());
+ load(Sfx.DEPLOY_VEHICLE, SoundSynth.deployVehicle());
+ load(Sfx.VICTORY, SoundSynth.victory());
+ load(Sfx.DEFEAT, SoundSynth.defeat());
+ load(Sfx.CREDIT_EARN, SoundSynth.creditEarn());
+ load(Sfx.CREDIT_SPEND, SoundSynth.creditSpend());
+ }
+
+ private static void load(Sfx sfx, byte[] pcm) {
+ Clip[] clips = new Clip[POOL_SIZE];
+ for (int i = 0; i < POOL_SIZE; i++) {
+ try {
+ Clip clip = AudioSystem.getClip();
+ clip.open(FORMAT, pcm, 0, pcm.length);
+ clips[i] = clip;
+ } catch (LineUnavailableException e) {
+ clips[i] = null;
+ }
+ }
+ pools[sfx.ordinal()] = clips;
+ }
+
+ public static void play(Sfx sfx) {
+ play(sfx, 1f);
+ }
+
+ //world-space distance under which a battlefield sound (gunfire, explosions) plays at full volume
+ private static final float NEAR_DISTANCE = 3f;
+ //distance beyond which it's faded all the way down to MIN_VOLUME rather than going silent -
+ //still audible so the player gets a sense something is happening elsewhere on the map
+ private static final float FAR_DISTANCE = 14f;
+ private static final float MIN_VOLUME = 0.15f;
+
+ //volume multiplier for a sound emitted at the given world position, based on distance from the camera
+ public static float volumeForDistance(float x, float y, float z) {
+ float dx = x - camera.position.x;
+ float dy = y - camera.position.y;
+ float dz = z - camera.position.z;
+ float distance = (float) Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ if (distance <= NEAR_DISTANCE)
+ return 1f;
+ if (distance >= FAR_DISTANCE)
+ return MIN_VOLUME;
+
+ float t = (distance - NEAR_DISTANCE) / (FAR_DISTANCE - NEAR_DISTANCE);
+ return 1f - t * (1f - MIN_VOLUME);
+ }
+
+ public static void play(Sfx sfx, float volume) {
+ if (!enabled || !initialized)
+ return;
+
+ Clip[] clips = pools[sfx.ordinal()];
+ if (clips == null)
+ return;
+
+ int slot;
+ synchronized (nextSlot) {
+ slot = nextSlot[sfx.ordinal()];
+ nextSlot[sfx.ordinal()] = (slot + 1) % clips.length;
+ }
+
+ Clip clip = clips[slot];
+ if (clip == null)
+ return;
+
+ synchronized (clip) {
+ if (clip.isRunning())
+ clip.stop();
+ clip.setFramePosition(0);
+ applyVolume(clip, volume);
+ clip.start();
+ }
+ }
+
+ private static void applyVolume(Clip clip, float volume) {
+ if (!clip.isControlSupported(FloatControl.Type.MASTER_GAIN))
+ return;
+ FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
+ float clamped = Math.max(0.0001f, Math.min(1f, volume));
+ float dB = (float) (Math.log10(clamped) * 20.0);
+ dB = Math.max(gainControl.getMinimum(), Math.min(gainControl.getMaximum(), dB));
+ gainControl.setValue(dB);
+ }
+}
diff --git a/audio/SoundSynth.java b/audio/SoundSynth.java
new file mode 100644
index 0000000..45f66ca
--- /dev/null
+++ b/audio/SoundSynth.java
@@ -0,0 +1,390 @@
+package audio;
+
+import java.util.Random;
+
+//Generates every sound effect in code as 16-bit mono PCM, so the game ships with
+//zero audio assets and zero licensing to worry about.
+public class SoundSynth {
+
+ public static final float SAMPLE_RATE = 22050f;
+
+ private static final Random RNG = new Random();
+
+ private interface Waveform {
+ double sample(double phase);
+ }
+
+ private static final Waveform SINE = Math::sin;
+ private static final Waveform TRIANGLE = phase -> {
+ double x = ((phase / (2 * Math.PI)) % 1 + 1) % 1;
+ return 4 * Math.abs(x - 0.5) - 1;
+ };
+ private static final Waveform SQUARE = phase -> Math.sin(phase) >= 0 ? 1 : -1;
+ private static final Waveform SAWTOOTH = phase -> {
+ double x = ((phase / (2 * Math.PI)) % 1 + 1) % 1;
+ return 2 * x - 1;
+ };
+
+ private static int ms(int milliseconds) {
+ return (int) (SAMPLE_RATE * milliseconds / 1000.0);
+ }
+
+ //linear frequency sweep from freqStart to freqEnd over n samples
+ private static double[] tone(double freqStart, double freqEnd, int n, Waveform wf) {
+ double[] buf = new double[n];
+ double phase = 0;
+ for (int i = 0; i < n; i++) {
+ double frac = n <= 1 ? 0 : i / (double) (n - 1);
+ double freq = freqStart + (freqEnd - freqStart) * frac;
+ phase += 2 * Math.PI * freq / SAMPLE_RATE;
+ buf[i] = wf.sample(phase);
+ }
+ return buf;
+ }
+
+ //one-pole lowpass filtered white noise; smaller alpha = duller/bassier
+ private static double[] filteredNoise(int n, double alpha) {
+ double[] buf = new double[n];
+ double y = 0;
+ for (int i = 0; i < n; i++) {
+ double x = RNG.nextDouble() * 2 - 1;
+ y += alpha * (x - y);
+ buf[i] = y;
+ }
+ return buf;
+ }
+
+ //filtered noise whose cutoff opens up over time, for a rocket "whoosh"
+ private static double[] sweepingNoise(int n, double alphaStart, double alphaEnd) {
+ double[] buf = new double[n];
+ double y = 0;
+ for (int i = 0; i < n; i++) {
+ double frac = i / (double) n;
+ double alpha = alphaStart + (alphaEnd - alphaStart) * frac;
+ double x = RNG.nextDouble() * 2 - 1;
+ y += alpha * (x - y);
+ buf[i] = y;
+ }
+ return buf;
+ }
+
+ private static double expDecay(int i, int n, double rate) {
+ return Math.exp(-rate * i / n);
+ }
+
+ //envelope that rises then falls, peaking at peakFrac through the buffer
+ private static double bell(int i, int n, double peakFrac) {
+ double frac = i / (double) n;
+ if (frac < peakFrac)
+ return frac / peakFrac;
+ return Math.max(0, 1 - (frac - peakFrac) / (1 - peakFrac));
+ }
+
+ private static double[] envelope(double[] buf, java.util.function.IntToDoubleFunction env) {
+ double[] out = new double[buf.length];
+ for (int i = 0; i < buf.length; i++)
+ out[i] = buf[i] * env.applyAsDouble(i);
+ return out;
+ }
+
+ private static double[] gain(double[] buf, double g) {
+ double[] out = new double[buf.length];
+ for (int i = 0; i < buf.length; i++)
+ out[i] = buf[i] * g;
+ return out;
+ }
+
+ //soft-clip saturation for punch/grit without harsh digital clipping; drive > 1 pushes harder
+ private static double[] saturate(double[] buf, double drive) {
+ double[] out = new double[buf.length];
+ for (int i = 0; i < buf.length; i++)
+ out[i] = Math.tanh(buf[i] * drive);
+ return out;
+ }
+
+ private static double[] padStart(double[] buf, int totalLength) {
+ double[] out = new double[totalLength];
+ System.arraycopy(buf, 0, out, 0, Math.min(buf.length, totalLength));
+ return out;
+ }
+
+ private static double[] mix(double[]... parts) {
+ int n = 0;
+ for (double[] p : parts)
+ n = Math.max(n, p.length);
+ double[] out = new double[n];
+ for (double[] p : parts)
+ for (int i = 0; i < p.length; i++)
+ out[i] += p[i];
+ return out;
+ }
+
+ private static double[] concat(double[]... parts) {
+ int total = 0;
+ for (double[] p : parts)
+ total += p.length;
+ double[] out = new double[total];
+ int pos = 0;
+ for (double[] p : parts) {
+ System.arraycopy(p, 0, out, pos, p.length);
+ pos += p.length;
+ }
+ return out;
+ }
+
+ private static byte[] toPcm(double[] buf, double masterGain) {
+ byte[] out = new byte[buf.length * 2];
+ for (int i = 0; i < buf.length; i++) {
+ double v = buf[i] * masterGain;
+ if (v > 1)
+ v = 1;
+ if (v < -1)
+ v = -1;
+ short s = (short) Math.round(v * Short.MAX_VALUE);
+ out[i * 2] = (byte) (s & 0xff);
+ out[i * 2 + 1] = (byte) ((s >> 8) & 0xff);
+ }
+ return out;
+ }
+
+ //a crisp UI button tap: bright, very short, no low-end thump so it reads as a press, not a thud
+ public static byte[] click() {
+ int n = ms(18);
+ double[] tick = envelope(filteredNoise(n, 0.9), i -> expDecay(i, n, 17));
+ double[] snap = envelope(tone(1900, 900, n, SINE), i -> expDecay(i, n, 19));
+ return toPcm(saturate(mix(gain(tick, 0.55), gain(snap, 0.6)), 1.15), 0.3);
+ }
+
+ //a single "ding": the exact same tone+noise click structure as creditSpend() below, just pitched up
+ private static double[] ding(int n, double startPitch, double endPitch) {
+ return envelope(mix(tone(startPitch, endPitch, n, SINE), gain(filteredNoise(n, 0.6), 0.4)), i -> expDecay(i, n, 14));
+ }
+
+ //a longer "ding-ding-ding-ding..." for a credit increase (harvester delivering ore, a cancelled
+ //building refunding): the same simple click creditSpend() uses, just pitched higher and repeated
+ //several times, rather than trying to synthesize a more elaborate/realistic coin sound
+ public static byte[] creditEarn() {
+ int dingLen = ms(50);
+ int gapLen = ms(20);
+ int dingCount = 12;
+
+ double[] silence = new double[gapLen];
+ double[] result = new double[0];
+ for (int i = 0; i < dingCount; i++) {
+ double pitch = 3200 + RNG.nextDouble() * 400;
+ double[] d = ding(dingLen, pitch, pitch * 0.65);
+ result = i == 0 ? d : concat(result, silence, d);
+ }
+ return toPcm(result, 0.22);
+ }
+
+ //a quiet metallic coin-click for a credit decrease (construction, repair, research draining the
+ //treasury) - kept short and quiet since it can fire repeatedly during a fast trickle
+ public static byte[] creditSpend() {
+ int n = ms(35);
+ double[] click = envelope(mix(tone(950, 550, n, SINE), gain(filteredNoise(n, 0.6), 0.4)), i -> expDecay(i, n, 14));
+ return toPcm(click, 0.12);
+ }
+
+ //unit selection blip: kept soft/melodic, matches the softer command-confirmation style
+ public static byte[] select() {
+ int n1 = ms(70), gap = ms(15), n2 = ms(70);
+ double[] blip1 = envelope(tone(880, 880, n1, SINE), i -> expDecay(i, n1, 5));
+ double[] silence = new double[gap];
+ double[] blip2 = envelope(tone(1320, 1320, n2, SINE), i -> expDecay(i, n2, 5));
+ return toPcm(concat(blip1, silence, blip2), 0.32);
+ }
+
+ //command-confirmation chirp: kept soft/melodic rather than industrial, reads better as a quick acknowledgement
+ public static byte[] move() {
+ int n = ms(110);
+ double[] buf = envelope(tone(700, 450, n, TRIANGLE), i -> expDecay(i, n, 4));
+ return toPcm(buf, 0.32);
+ }
+
+ public static byte[] attack() {
+ int n = ms(140);
+ double[] toneBuf = envelope(tone(320, 220, n, SQUARE), i -> expDecay(i, n, 3.5));
+ double[] grit = envelope(filteredNoise(n, 0.6), i -> expDecay(i, n, 6));
+ return toPcm(mix(toneBuf, gain(grit, 0.25)), 0.3);
+ }
+
+ //a mechanical valve-clunk, not a soft chirp
+ public static byte[] harvest() {
+ int n = ms(85);
+ double[] clunk = envelope(tone(170, 105, n, SQUARE), i -> expDecay(i, n, 7));
+ double[] grit = envelope(filteredNoise(n, 0.45), i -> expDecay(i, n, 8));
+ return toPcm(saturate(mix(clunk, gain(grit, 0.4)), 1.25), 0.3);
+ }
+
+ public static byte[] shootCannon() {
+ int n = ms(140);
+ double[] thump = envelope(tone(95, 60, n, SINE), i -> expDecay(i, n, 7));
+ double[] crack = envelope(filteredNoise(n, 0.5), i -> expDecay(i, n, 9));
+ return toPcm(saturate(mix(thump, gain(crack, 0.65)), 1.3), 0.58);
+ }
+
+ public static byte[] shootRocket() {
+ int n = ms(230);
+ double[] whoosh = envelope(sweepingNoise(n, 0.08, 0.45), i -> bell(i, n, 0.35));
+ double[] sweep = envelope(tone(150, 550, n, SINE), i -> bell(i, n, 0.35));
+ return toPcm(saturate(mix(gain(whoosh, 0.8), gain(sweep, 0.35)), 1.15), 0.52);
+ }
+
+ //heavy tank's main gun: a big, saturated boom with a sub-bass layer and a breech clank tail
+ public static byte[] shootCannonHeavy() {
+ int n = ms(260);
+ double[] sub = envelope(tone(38, 20, n, SINE), i -> expDecay(i, n, 3.6));
+ double[] thump = envelope(tone(70, 38, n, SINE), i -> expDecay(i, n, 4.8));
+ double[] crack = envelope(filteredNoise(n, 0.32), i -> expDecay(i, n, 6));
+
+ int clankLen = ms(50);
+ double[] clank = envelope(tone(190, 130, clankLen, TRIANGLE), i -> expDecay(i, clankLen, 6));
+ double[] clankPadded = padStart(shiftRight(clank, ms(95)), n);
+
+ double[] mixed = mix(gain(sub, 0.8), thump, gain(crack, 0.85), gain(clankPadded, 0.35));
+ return toPcm(saturate(mixed, 1.5), 0.68);
+ }
+
+ //gun turret's rapid-fire burst: three metallic clacks, not three cute beeps
+ public static byte[] shootAutocannon() {
+ int tapLen = ms(28), gap = ms(24);
+ double[] tap = envelope(mix(gain(filteredNoise(tapLen, 0.6), 0.9), tone(240, 150, tapLen, SQUARE)),
+ i -> expDecay(i, tapLen, 11));
+ double[] tapSat = saturate(tap, 1.4);
+ double[] silence = new double[gap];
+ return toPcm(concat(tapSat, silence, tapSat, silence, tapSat), 0.42);
+ }
+
+ //missile turret's warhead: bigger and bassier than the tank rocket
+ public static byte[] shootMissile() {
+ int n = ms(320);
+ double[] whoosh = envelope(sweepingNoise(n, 0.05, 0.3), i -> bell(i, n, 0.3));
+ double[] sweep = envelope(tone(90, 320, n, SINE), i -> bell(i, n, 0.3));
+ double[] thump = envelope(tone(60, 40, ms(120), SINE), i -> expDecay(i, ms(120), 5));
+ return toPcm(saturate(mix(gain(whoosh, 0.85), gain(sweep, 0.3), gain(thump, 0.5)), 1.2), 0.62);
+ }
+
+ //stealth tank's railgun: a sci-fi energy zap, unlike anything mechanical
+ public static byte[] shootRailgun() {
+ int n = ms(160);
+ double[] zap = envelope(tone(2400, 300, n, SAWTOOTH), i -> expDecay(i, n, 5));
+ int ringLen = ms(60);
+ double[] ring = new double[n];
+ double[] ringTone = envelope(tone(900, 900, ringLen, SINE), i -> expDecay(i, ringLen, 6));
+ System.arraycopy(ringTone, 0, ring, ms(20), Math.min(ringLen, n - ms(20)));
+ return toPcm(mix(zap, gain(ring, 0.4)), 0.4);
+ }
+
+ //a building locking into place on the deployment grid: two heavy metal thunks with a bit of clang
+ public static byte[] deployBuilding() {
+ int thunkLen = ms(75), gap = ms(35);
+ double[] thunk1 = envelope(mix(tone(130, 70, thunkLen, SINE), gain(filteredNoise(thunkLen, 0.3), 0.55)),
+ i -> expDecay(i, thunkLen, 5.5));
+ double[] thunk2 = envelope(mix(tone(100, 55, thunkLen, SINE), gain(filteredNoise(thunkLen, 0.3), 0.55)),
+ i -> expDecay(i, thunkLen, 5.5));
+ double[] clank = envelope(tone(1400, 900, ms(15), SINE), i -> expDecay(i, ms(15), 6));
+ double[] silence = new double[gap];
+ double[] mixed = mix(concat(thunk1, silence, thunk2), gain(shiftRight(clank, 2 * thunkLen + gap + ms(15)), 0.45));
+ return toPcm(saturate(mixed, 1.3), 0.55);
+ }
+
+ //the MCV unfolding into a construction yard: unlock click, dull mechanical grind, panel clunks, final thuds
+ public static byte[] deployVehicle() {
+ int n = ms(650);
+
+ //every tonal whine tried here (sawtooth, square, pulsed) has read as electronic/balloon-like -
+ //drop the tone entirely and use only a dull, heavily-filtered noise swell for the extension
+ //movement, which reads as mechanical grinding rather than a synth whistle
+ int unlockLen = ms(25);
+ double[] unlock = envelope(mix(filteredNoise(unlockLen, 0.8), gain(tone(600, 300, unlockLen, SINE), 0.5)),
+ i -> expDecay(i, unlockLen, 10));
+
+ int grindLen = ms(420);
+ double[] grind = envelope(filteredNoise(grindLen, 0.18), i -> bell(i, grindLen, 0.5));
+
+ //three heavy metallic clunks as panels unfold, spread across the sequence
+ int clunkLen = ms(60);
+ double[] clunk1 = envelope(mix(tone(150, 90, clunkLen, SINE), gain(filteredNoise(clunkLen, 0.35), 0.6)),
+ i -> expDecay(i, clunkLen, 6));
+ double[] clunk2 = envelope(mix(tone(140, 85, clunkLen, SINE), gain(filteredNoise(clunkLen, 0.35), 0.6)),
+ i -> expDecay(i, clunkLen, 6));
+ double[] clunk3 = envelope(mix(tone(130, 80, clunkLen, SINE), gain(filteredNoise(clunkLen, 0.35), 0.6)),
+ i -> expDecay(i, clunkLen, 6));
+
+ //final heavy double-thud as the whole assembly locks into place
+ int thudLen = ms(90);
+ double[] thud1 = envelope(mix(tone(65, 32, thudLen, SINE), gain(filteredNoise(thudLen, 0.3), 0.6)),
+ i -> expDecay(i, thudLen, 5));
+ double[] thud2 = envelope(mix(tone(58, 28, thudLen, SINE), gain(filteredNoise(thudLen, 0.3), 0.6)),
+ i -> expDecay(i, thudLen, 5));
+
+ double[] mixed = mix(
+ padStart(unlock, n),
+ gain(padStart(shiftRight(grind, ms(40)), n), 0.55),
+ padStart(shiftRight(clunk1, ms(150)), n),
+ padStart(shiftRight(clunk2, ms(320)), n),
+ padStart(shiftRight(clunk3, ms(470)), n),
+ padStart(shiftRight(thud1, n - thudLen * 2), n),
+ padStart(shiftRight(thud2, n - thudLen), n));
+ return toPcm(saturate(mixed, 1.15), 0.55);
+ }
+
+ private static double[] shiftRight(double[] buf, int offset) {
+ double[] out = new double[offset + buf.length];
+ System.arraycopy(buf, 0, out, offset, buf.length);
+ return out;
+ }
+
+ public static byte[] explosionSmall() {
+ int n = ms(320);
+ int crackLen = ms(12);
+ double[] crack = padStart(envelope(filteredNoise(crackLen, 0.85), i -> expDecay(i, crackLen, 7)), n);
+ double[] boom = envelope(filteredNoise(n, 0.14), i -> expDecay(i, n, 3.6));
+ double[] rumble = envelope(tone(60, 32, n, SINE), i -> expDecay(i, n, 4.2));
+ double[] mixed = mix(gain(crack, 0.9), boom, gain(rumble, 0.65));
+ return toPcm(saturate(mixed, 1.35), 0.65);
+ }
+
+ public static byte[] explosionLarge() {
+ int n = ms(650);
+ int crackLen = ms(20);
+ double[] crack = padStart(envelope(filteredNoise(crackLen, 0.9), i -> expDecay(i, crackLen, 6)), n);
+ double[] boom = envelope(filteredNoise(n, 0.075), i -> expDecay(i, n, 2.1));
+ double[] rumble = envelope(tone(35, 18, n, SINE), i -> expDecay(i, n, 2.5));
+
+ int crackleStart = ms(110);
+ int crackleLen = n - crackleStart;
+ double[] crackle = new double[n];
+ double[] crackleNoise = envelope(filteredNoise(crackleLen, 0.35), i -> expDecay(i, crackleLen, 5));
+ System.arraycopy(crackleNoise, 0, crackle, crackleStart, crackleLen);
+
+ double[] mixed = mix(gain(crack, 0.8), boom, gain(rumble, 0.75), gain(crackle, 0.45));
+ return toPcm(saturate(mixed, 1.4), 0.85);
+ }
+
+ //a distorted sawtooth "horn" note, for an industrial fanfare instead of a chiptune melody
+ private static double[] hornNote(double freq, int n, double rate) {
+ double[] toneBuf = envelope(tone(freq, freq, n, SAWTOOTH), i -> expDecay(i, n, rate));
+ double[] grit = envelope(filteredNoise(n, 0.4), i -> expDecay(i, n, rate + 2));
+ return saturate(mix(toneBuf, gain(grit, 0.2)), 1.8);
+ }
+
+ public static byte[] victory() {
+ int noteLen = ms(150);
+ double[] c4 = hornNote(262, noteLen, 2.5);
+ double[] e4 = hornNote(330, noteLen, 2.5);
+ double[] g4 = hornNote(392, noteLen, 2.5);
+ double[] c5 = hornNote(523, noteLen * 2, 1.8);
+ return toPcm(concat(c4, e4, g4, c5), 0.42);
+ }
+
+ public static byte[] defeat() {
+ int noteLen = ms(190);
+ double[] a3 = hornNote(220, noteLen, 2.5);
+ double[] f3 = hornNote(175, noteLen, 2.5);
+ double[] d3 = hornNote(147, noteLen, 2.5);
+ double[] a2 = hornNote(110, noteLen * 2, 1.8);
+ return toPcm(concat(a3, f3, d3, a2), 0.42);
+ }
+}
diff --git a/core/AssetManager.java b/core/AssetManager.java
index 01bb244..21c9f81 100644
--- a/core/AssetManager.java
+++ b/core/AssetManager.java
@@ -4,6 +4,8 @@
import entity.*;
import gui.*;
import particles.*;
+import audio.SoundEngine;
+import audio.SoundEngine.Sfx;
//This class stores and maintains all the entities created in the game
public class AssetManager {
@@ -13,8 +15,10 @@ public class AssetManager {
public int visibleUnitCount;
public solidObject[] visibleUnit;
- public int[][] selectedUnitsInfo;
- public int[][] selectedUnitsInfo2;
+ //per-frame health bar/group/veterancy display info for every currently-visible unit (not just
+ //selected ones), sourced from visibleUnit[] so health bars show by default regardless of selection
+ public int[][] allUnitsInfo;
+ public int[][] allUnitsInfo2;
public float[][] visionPolygonInfo;
public float[][] visionPolygonInfo2;
@@ -34,6 +38,9 @@ public class AssetManager {
public float[][] explosionInfo;
public float[][] explosionInfo2;
public int explosionCount;;
+
+ //dynamic point lights spawned by explosions, so nearby models light up instead of just the ground
+ public float[][] blastLights;
public float[][] helixInfo;
public float[][] helixInfo2;
@@ -139,7 +146,10 @@ public void init(){
}
visibleUnit = new solidObject[400];
-
+
+ //x, y, z, remainingFrames, peakIntensity - a slot with remainingFrames<=0 is unused
+ blastLights = new float[24][5];
+
mainThread.pc = new playerCommander();
mainThread.ec = new enemyCommander();
@@ -154,8 +164,8 @@ public void prepareAssetForNewGame(){
camera.XZ_angle = 0;
- selectedUnitsInfo = new int[100][6];
- selectedUnitsInfo2 = new int[100][6];
+ allUnitsInfo = new int[400][6];
+ allUnitsInfo2 = new int[400][6];
visionPolygonInfo = new float[400][5];
visionPolygonInfo2 = new float[400][5];
@@ -226,13 +236,13 @@ public void prepareAssetForNewGame(){
communicationCenter.resetResearchStatus();
- addConstructionVehicle(new constructionVehicle(new vector(3.125f,-0.3f, 2.125f), 90, 0));
- addConstructionVehicle(new constructionVehicle(new vector(29.625f,-0.3f, 28.875f), 90, 1));
+ addConstructionVehicle(new constructionVehicle(new vector(3.125f,-0.3f, 2.125f), 90, 0));
+ addConstructionVehicle(new constructionVehicle(new vector(29.625f,-0.3f, 28.875f), 90, 1));
constructionVehicles[1].expand();
-
+
numberOfPlayerBuildings = 1;
numberOfAIBuildings = 1;
-
+
//testing only
for(int i = 0; i < 6; i ++){
@@ -279,8 +289,8 @@ public void destoryAsset() {
camera.frameIndex = 0;
camera.XZ_angle = 0;
- selectedUnitsInfo = null;
- selectedUnitsInfo2 = null;
+ allUnitsInfo = null;
+ allUnitsInfo2 = null;
visionPolygonInfo = null;
visionPolygonInfo2 = null;
@@ -298,6 +308,10 @@ public void destoryAsset() {
explosionInfo = null;
explosionInfo2 = null;
+ explosionCount = 0; //the blast-light harvesting loop at the top of updateAndDraw() reads
+ //explosionInfo unconditionally every frame, before explosionCount gets reset for the frame -
+ //without this, a stale nonzero count left over from the instant the game ended indexes into
+ //the now-null array on the very next frame
helixInfo = null;
helixInfo2 = null;
@@ -523,10 +537,12 @@ public void updateAndDraw(){
if(numberOfAIBuildings == 0) {
mainThread.playerVictory = true;
mainThread.gamePaused = true;
+ SoundEngine.play(Sfx.VICTORY);
destoryAllUnit(1);
}else if(numberOfPlayerBuildings == 0) {
mainThread.AIVictory = true;
mainThread.gamePaused = true;
+ SoundEngine.play(Sfx.DEFEAT);
destoryAllUnit(0);
}
@@ -538,6 +554,12 @@ public void updateAndDraw(){
}
+ //turn last frame's freshly spawned explosions into blast lights that nearby models can react to
+ for(int i = 0; i < explosionCount; i++){
+ registerBlastLight(explosionInfo[i][0], explosionInfo[i][1], explosionInfo[i][2], explosionInfo[i][3]);
+ }
+ decayBlastLights();
+
polygonCount = 0;
visibleUnitCount = 0;
visionPolygonCount = 0;
@@ -810,25 +832,28 @@ public void updateAndDraw(){
}
- //prepare selected unit list
- for(int i = 0; i < 99; i++){
- if(mainThread.pc.selectedUnits[i] != null && mainThread.pc.selectedUnits[i].isSelectable){
- selectedUnitsInfo[i][0] = mainThread.pc.selectedUnits[i].level << 16 | mainThread.pc.selectedUnits[i].groupNo << 8 | mainThread.pc.selectedUnits[i].type;
- selectedUnitsInfo[i][1] = (int)mainThread.pc.selectedUnits[i].tempCentre.screenX;
- selectedUnitsInfo[i][2] = (int)mainThread.pc.selectedUnits[i].tempCentre.screenY;
- if(mainThread.pc.selectedUnits[i].type == 199){
- selectedUnitsInfo[i][1] = (int)mainThread.pc.selectedUnits[i].screenX_gui;
- selectedUnitsInfo[i][2] = (int)mainThread.pc.selectedUnits[i].screenY_gui;
+ //prepare per-unit display info for every visible unit, not just selected ones - the
+ //veterancy star reads directly off this for every unit, while the health bar/group number/
+ //progress bar are only drawn for selected units (the isSelected flag packed into bit 24
+ //of [0] is what the draw loop gates on)
+ for(int i = 0; i < allUnitsInfo.length; i++){
+ if(i < visibleUnitCount && visibleUnit[i] != null && visibleUnit[i].isSelectable){
+ allUnitsInfo[i][0] = (visibleUnit[i].isSelected ? 1<<24 : 0) | visibleUnit[i].level << 16 | visibleUnit[i].groupNo << 8 | visibleUnit[i].type;
+ allUnitsInfo[i][1] = (int)visibleUnit[i].tempCentre.screenX;
+ allUnitsInfo[i][2] = (int)visibleUnit[i].tempCentre.screenY;
+ if(visibleUnit[i].type == 199){
+ allUnitsInfo[i][1] = (int)visibleUnit[i].screenX_gui;
+ allUnitsInfo[i][2] = (int)visibleUnit[i].screenY_gui;
}
-
- selectedUnitsInfo[i][3] = (int)mainThread.pc.selectedUnits[i].type;
- selectedUnitsInfo[i][4] = mainThread.pc.selectedUnits[i].currentHP;
- selectedUnitsInfo[i][5] = mainThread.pc.selectedUnits[i].progressStatus;
+
+ allUnitsInfo[i][3] = (int)visibleUnit[i].type;
+ allUnitsInfo[i][4] = visibleUnit[i].currentHP;
+ allUnitsInfo[i][5] = visibleUnit[i].progressStatus;
}else{
- selectedUnitsInfo[i][0] = -1;
+ allUnitsInfo[i][0] = -1;
}
}
-
+
}
}
@@ -837,9 +862,9 @@ public void updateAndDraw(){
//swap the resources that are held by the main thread and the post processing thread
public void swapResources(){
int[][] list;
- list = selectedUnitsInfo;
- selectedUnitsInfo = selectedUnitsInfo2;
- selectedUnitsInfo2 = list;
+ list = allUnitsInfo;
+ allUnitsInfo = allUnitsInfo2;
+ allUnitsInfo2 = list;
float[][] floatList;
floatList = visionPolygonInfo;
@@ -872,20 +897,64 @@ public void swapResources(){
confirmationIconInfo2 = iconInfo;
}
+ //register a dynamic light at an explosion's location; reuses the weakest/expired slot once full
+ public void registerBlastLight(float x, float y, float z, float size){
+ int weakestIndex = 0;
+ float weakestScore = Float.MAX_VALUE;
+
+ for(int i = 0; i < blastLights.length; i++){
+ if(blastLights[i][3] <= 0){
+ weakestIndex = i;
+ break;
+ }
+ if(blastLights[i][3] < weakestScore){
+ weakestScore = blastLights[i][3];
+ weakestIndex = i;
+ }
+ }
+
+ float[] light = blastLights[weakestIndex];
+ light[0] = x;
+ light[1] = y;
+ light[2] = z;
+ light[3] = 11; //shorter lifetime so the light fades out quickly instead of lingering
+ light[4] = size;
+ }
+
+ //ages every active blast light by one frame; called once per frame
+ public void decayBlastLights(){
+ for(int i = 0; i < blastLights.length; i++){
+ if(blastLights[i][3] > 0)
+ blastLights[i][3]--;
+ }
+ }
+
//spawn a bullet
public void spawnBullet(int angle, int damage, solidObject target, vector centre, solidObject attacker){
for(int i = 0; i < 200; i ++)
if(!bullets[i].isInAction){
bullets[i].setActive(angle, damage, target, centre, attacker);
+ float bulletVolume = SoundEngine.volumeForDistance(centre.x, centre.y, centre.z);
+ if(attacker.type == 7)
+ SoundEngine.play(Sfx.SHOOT_CANNON_HEAVY, bulletVolume);
+ else if(attacker.type == 200)
+ SoundEngine.play(Sfx.SHOOT_AUTOCANNON, bulletVolume);
+ else
+ SoundEngine.play(Sfx.SHOOT_CANNON, bulletVolume);
break;
}
}
-
+
//spawn a rocket
public void spawnRocket(int angle, int damage, solidObject target, vector centre, solidObject attacker){
for(int i = 0; i < 200; i ++)
if(!rockets[i].isInAction){
rockets[i].setActive(angle, damage, target, centre, attacker);
+ float rocketVolume = SoundEngine.volumeForDistance(centre.x, centre.y, centre.z);
+ if(attacker.type == 199)
+ SoundEngine.play(Sfx.SHOOT_MISSILE, rocketVolume);
+ else
+ SoundEngine.play(Sfx.SHOOT_ROCKET, rocketVolume);
break;
}
}
diff --git a/core/camera.java b/core/camera.java
index 9ad7a70..7f592b8 100644
--- a/core/camera.java
+++ b/core/camera.java
@@ -131,21 +131,56 @@ public void update(){
cosXZ_angle = gameData.cos[XZ_angle];
sinYZ_angle = gameData.sin[YZ_angle];
cosYZ_angle = gameData.cos[YZ_angle];
-
-
-
+
+
+
view_Direction.set(viewDirection);
view_Direction.rotate_YZ(YZ_angle);
view_Direction.rotate_XZ(XZ_angle);
view_Direction.y*=-1;
view_Direction.x*=-1;
view_Direction.unit();
-
+
position.add(-view_Direction.x*3, 0 , -view_Direction.z*3);
-
-
-
-
+
+
+
+
+ }
+
+ //pan the camera every frame while the right button is held, towards wherever the cursor has
+ //been dragged relative to the fixed anchor point where the drag started; the actual world-bounds
+ //clamp happens right after in update()
+ public static void panByHeldOffset(int offsetXPixels, int offsetYPixels){
+ float dx = holdSpeed(offsetXPixels);
+ float dy = holdSpeed(offsetYPixels);
+
+ vector leftVec = new vector(0,0,0);
+ leftVec.cross(view_Direction, left_);
+ leftVec.unit();
+ position.add(leftVec, dx);
+
+ vector forwardVec = new vector(view_Direction.x, 0, view_Direction.z);
+ forwardVec.unit();
+ position.add(forwardVec, -dy);
+ }
+
+ //converts how far (in pixels) the cursor has been dragged away from the anchor into a per-frame
+ //scroll speed: a small deadzone near the anchor to absorb hand tremor, then an immediate speed
+ //floor right past that (so even a barely-past-deadzone drag scrolls at a usable pace instead of
+ //crawling), ramping the rest of the way up to max speed as the drag gets larger
+ private static float holdSpeed(int pixels){
+ float deadZone = 8f;
+ float maxPixels = 220f;
+ float maxSpeed = 0.4f;
+ float minFraction = 0.18f; //speed as soon as the drag clears the deadzone, as a fraction of maxSpeed
+
+ float mag = Math.abs(pixels);
+ if(mag <= deadZone)
+ return 0;
+ float t = Math.min((mag - deadZone) / (maxPixels - deadZone), 1f);
+ float speed = (minFraction + (1f - minFraction) * t) * maxSpeed;
+ return pixels < 0 ? -speed : speed;
}
}
\ No newline at end of file
diff --git a/core/gameData.java b/core/gameData.java
index 3820d33..eb0e8a5 100644
--- a/core/gameData.java
+++ b/core/gameData.java
@@ -17,7 +17,7 @@ public class gameData {
public static int[][] size;
public static byte[][] cloakTextures;
- public static String imageFolder = "../images/";
+ public static String imageFolder = "/images/";
diff --git a/core/highscoreManager.java b/core/highscoreManager.java
index 9db6049..15ed01c 100644
--- a/core/highscoreManager.java
+++ b/core/highscoreManager.java
@@ -1,27 +1,57 @@
package core;
-import java.sql.*;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class highscoreManager implements Runnable{
- public Connection connect;
+
+ //Firebase project details (Project settings > General page has the Project ID and Web API Key -
+ //no service account or Auth needed). The API key is safe to ship inside the game jar: Firestore
+ //Security Rules on the "highscores" collection (see the setup notes) restrict it to creating one
+ //valid score document and reading scores - nothing else, unlike the old raw JDBC connection
+ //string which gave anyone full read/write/drop access.
+ private static final String FIREBASE_PROJECT_ID = "battle-tank-3";
+ private static final String FIREBASE_API_KEY = "AIzaSyCbKB324xgpJkY2Ei4bRoy_6dutUZ-bDMc";
+
+ private static final String BASE_URL = "https://firestore.googleapis.com/v1/projects/"
+ + FIREBASE_PROJECT_ID + "/databases/(default)/documents";
+
+ private static final HttpClient httpClient = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+
+ //tolerant of the whitespace Firestore's pretty-printed REST responses put around colons/braces
+ //(e.g. "player_name": { "stringValue": ... } rather than the compact form) - see fetchSkillLevel
+ //for how these two are paired up per-document
+ private static final Pattern NAME_PATTERN = Pattern.compile("\"player_name\"\\s*:\\s*\\{\\s*\"stringValue\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
+ private static final Pattern TIME_PATTERN = Pattern.compile("\"finishing_time\"\\s*:\\s*\\{\\s*\"integerValue\"\\s*:\\s*\"(-?\\d+)\"");
+ private static final Pattern ID_TOKEN_PATTERN = Pattern.compile("\"idToken\":\\s*\"([^\"]+)\"");
+ private static final Pattern LOCAL_ID_PATTERN = Pattern.compile("\"localId\":\\s*\"([^\"]+)\"");
+
public int counter;
-
+
public int status;
public static final int idle = 0;
public static final int processing = 1;
public static final int error = 2;
-
+
public int task;
public static final int none = 0;
public static final int loadHighscores = 1;
public static final int uploadScore = 2;
-
+
public boolean isSleeping;
-
+
public String playerName;
-
+
public String[][] result;
-
+
public highscoreManager(){
status = processing;
playerName = "";
@@ -30,180 +60,207 @@ public highscoreManager(){
@Override
public void run() {
- // TODO Auto-generated method stub
while(true) {
if(counter == 0) {
status = idle;
}
-
+
if(status == idle) {
-
- if(task != none) {
+
+ if(task != none) {
status = processing;
- Statement stmt = null;
- ResultSet rs = null;
-
- try {
-
- connect = DriverManager.getConnection("jdbc:mysql://db4free.net:3306/javarts", "javarts", "kgiFO3nGzT");
-
-
- } catch (Exception e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- status = error;
- }
-
-
-
+
if(task == loadHighscores) {
- //get high scores from remote database
-
+ //get high scores from Firestore, one skill level at a time
try {
String[][] myResult = new String[30][2];
- int numOfRows = 0;
-
- stmt = connect.createStatement();
- rs=stmt.executeQuery("select * from highscore where skillLevel = 0 order by finishingTime");
- while(rs.next()) {
- playerName = rs.getString(1);
- if(!hasDuplicateName(0, numOfRows, myResult, playerName)) {
-
- myResult[numOfRows][0] = playerName;
- myResult[numOfRows][1] = secondsToString(rs.getInt(2));
-
- numOfRows++;
- if(numOfRows == 10)
- break;
- }
- }
- rs.close();
-
- numOfRows = 10;
- rs=stmt.executeQuery("select * from highscore where skillLevel = 1 order by finishingTime");
- while(rs.next()) {
- playerName = rs.getString(1);
- if(!hasDuplicateName(10, numOfRows, myResult, playerName)) {
- myResult[numOfRows][0] = rs.getString(1);
- myResult[numOfRows][1] = secondsToString(rs.getInt(2));
-
- numOfRows++;
- if(numOfRows == 20)
- break;
- }
- }
- rs.close();
-
- numOfRows = 20;
- rs=stmt.executeQuery("select * from highscore where skillLevel = 2 order by finishingTime");
- while(rs.next()) {
- playerName = rs.getString(1);
- if(!hasDuplicateName(20, numOfRows, myResult, playerName)) {
- myResult[numOfRows][0] = rs.getString(1);
- myResult[numOfRows][1] = secondsToString(rs.getInt(2));
-
- numOfRows++;
- if(numOfRows == 30)
- break;
- }
- }
-
+
+ fetchSkillLevel(0, myResult, 0);
+ fetchSkillLevel(1, myResult, 10);
+ fetchSkillLevel(2, myResult, 20);
+
result = myResult;
- playerName ="";
-
+ playerName = "";
+
} catch (Exception e) {
- // TODO Auto-generated catch block
e.printStackTrace();
status = error;
result = null;
playerName = "";
- }finally {
- if (rs != null) {
- try {
- rs.close();
- } catch (SQLException e) { /* ignored */}
- }
- if (stmt != null) {
- try {
- stmt.close();
- } catch (SQLException e) { /* ignored */}
- }
- if (connect != null) {
- try {
- connect.close();
- } catch (SQLException e) { /* ignored */}
- }
}
-
-
+
}else if(task == uploadScore) {
- PreparedStatement preparedStmt = null;
try {
-
- // the mysql insert statement
- String query = " insert into highscore" + " values (?, ?, ?)";
- preparedStmt = connect.prepareStatement(query);
-
- preparedStmt.setString (1, playerName);
- preparedStmt.setInt (2, (int)(mainThread.gameFrame*0.025));
- preparedStmt.setInt (3, mainThread.ec.difficulty);
- preparedStmt.execute();
-
-
+ submitScoreWithRateLimit(playerName, (int)(mainThread.matchElapsedMs/1000), mainThread.ec.difficulty);
+ //invalidate the cached leaderboard so the next time it's viewed, it re-fetches
+ //and shows the score that was just submitted, instead of stale cached data
+ //from whenever the board was first opened this session
+ result = null;
+
}catch (Exception e) {
- // TODO Auto-generated catch block
e.printStackTrace();
status = error;
playerName = "";
- }finally {
- if (rs != null) {
- try {
- rs.close();
- } catch (SQLException e) { /* ignored */}
- }
- if (preparedStmt != null) {
- try {
- preparedStmt.close();
- } catch (SQLException e) { /* ignored */}
- }
- if (connect != null) {
- try {
- connect.close();
- } catch (SQLException e) { /* ignored */}
- }
- }
+ }
}
-
+
if(status != error)
status = idle;
task = none;
}
}
-
+
isSleeping = true;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
- // TODO Auto-generated catch block
e.printStackTrace();
}
isSleeping = false;
-
+
counter++;
}
-
+
+ }
+
+ //fetches up to 10 unique-named top scores for one skill level, writing them into myResult
+ //starting at index "start" - mirrors the original JDBC version's per-level dedup/limit behaviour
+ private void fetchSkillLevel(int skillLevel, String[][] myResult, int start) throws Exception {
+ //ask for a generous buffer since duplicate names get filtered out client-side below
+ String query = "{"
+ + "\"structuredQuery\":{"
+ + "\"from\":[{\"collectionId\":\"highscores\"}],"
+ + "\"where\":{\"fieldFilter\":{\"field\":{\"fieldPath\":\"skill_level\"},\"op\":\"EQUAL\",\"value\":{\"integerValue\":\"" + skillLevel + "\"}}},"
+ + "\"orderBy\":[{\"field\":{\"fieldPath\":\"finishing_time\"},\"direction\":\"ASCENDING\"}],"
+ + "\"limit\":50"
+ + "}}";
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + ":runQuery?key=" + FIREBASE_API_KEY))
+ .header("Content-Type", "application/json")
+ .timeout(Duration.ofSeconds(10))
+ .POST(HttpRequest.BodyPublishers.ofString(query))
+ .build();
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ if(response.statusCode() != 200)
+ throw new RuntimeException("Firestore query failed: " + response.statusCode() + " " + response.body());
+
+ //each result document contributes exactly one player_name and one finishing_time, appearing
+ //in the same relative order (name before time) with no interleaving between documents - so
+ //advancing both matchers in lockstep pairs them correctly without needing to first isolate
+ //each document's own "fields" block (which isn't actually flat - every value is wrapped in
+ //its own type-descriptor object, e.g. "player_name": {"stringValue": ...}, so a naive
+ //first-brace-to-first-brace scan was cutting off after only the first nested field)
+ int numOfRows = start;
+ Matcher nameM = NAME_PATTERN.matcher(response.body());
+ Matcher timeM = TIME_PATTERN.matcher(response.body());
+ while(nameM.find() && timeM.find() && numOfRows < start + 10){
+ String name = unescapeJson(nameM.group(1));
+ int seconds = Integer.parseInt(timeM.group(1));
+
+ if(!hasDuplicateName(start, numOfRows, myResult, name)){
+ myResult[numOfRows][0] = name;
+ myResult[numOfRows][1] = secondsToString(seconds);
+ numOfRows++;
+ }
+ }
}
-
+
+ //signs in anonymously (giving this submission a verifiable identity the security rules can key a
+ //cooldown off of), submits the score, then marks the identity's last-submission time so the next
+ //attempt gets throttled. A fresh anonymous identity is created per submission rather than reused -
+ //it doesn't need to persist across game sessions, since the cooldown only needs to catch someone
+ //hammering the same script/loop, not fingerprint a specific player across launches.
+ private void submitScoreWithRateLimit(String name, int finishingTimeSeconds, int skillLevel) throws Exception {
+ String[] auth = signInAnonymously();
+ String idToken = auth[0];
+ String uid = auth[1];
+
+ //submit first, against whatever the rate-limit doc currently says (or its absence) - touching
+ //the rate limit only AFTER a successful submission, so this attempt isn't blocked by its own update
+ uploadScore(idToken, name, finishingTimeSeconds, skillLevel);
+ touchRateLimit(idToken, uid);
+ }
+
+ private String[] signInAnonymously() throws Exception {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=" + FIREBASE_API_KEY))
+ .header("Content-Type", "application/json")
+ .timeout(Duration.ofSeconds(10))
+ .POST(HttpRequest.BodyPublishers.ofString("{\"returnSecureToken\":true}"))
+ .build();
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ if(response.statusCode() != 200)
+ throw new RuntimeException("Anonymous sign-in failed: " + response.statusCode() + " " + response.body());
+
+ Matcher tokenM = ID_TOKEN_PATTERN.matcher(response.body());
+ Matcher uidM = LOCAL_ID_PATTERN.matcher(response.body());
+ if(!tokenM.find() || !uidM.find())
+ throw new RuntimeException("Anonymous sign-in response missing idToken/localId: " + response.body());
+
+ return new String[]{tokenM.group(1), uidM.group(1)};
+ }
+
+ private void uploadScore(String idToken, String name, int finishingTimeSeconds, int skillLevel) throws Exception {
+ String body = "{\"fields\":{"
+ + "\"player_name\":{\"stringValue\":\"" + escapeJson(name) + "\"},"
+ + "\"finishing_time\":{\"integerValue\":\"" + finishingTimeSeconds + "\"},"
+ + "\"skill_level\":{\"integerValue\":\"" + skillLevel + "\"}"
+ + "}}";
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/highscores?key=" + FIREBASE_API_KEY))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + idToken)
+ .timeout(Duration.ofSeconds(10))
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ if(response.statusCode() / 100 != 2)
+ throw new RuntimeException("Firestore insert failed: " + response.statusCode() + " " + response.body());
+ }
+
+ //records "now" (by this client's clock) against this anonymous identity so the next submission can
+ //be throttled by the security rules; those rules cross-check this value against Firestore's own
+ //server-perceived request time within a tolerance, so a client can't just backdate it to permanently
+ //bypass its own cooldown
+ private void touchRateLimit(String idToken, String uid) throws Exception {
+ String body = "{\"fields\":{\"lastSubmission\":{\"timestampValue\":\"" + Instant.now().toString() + "\"}}}";
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/rateLimits/" + uid + "?key=" + FIREBASE_API_KEY + "&updateMask.fieldPaths=lastSubmission"))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + idToken)
+ .timeout(Duration.ofSeconds(10))
+ .method("PATCH", HttpRequest.BodyPublishers.ofString(body))
+ .build();
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ if(response.statusCode() / 100 != 2)
+ throw new RuntimeException("Rate-limit update failed: " + response.statusCode() + " " + response.body());
+ }
+
+ private static String escapeJson(String s) {
+ return s.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ private static String unescapeJson(String s) {
+ return s.replace("\\\"", "\"").replace("\\\\", "\\");
+ }
+
public boolean hasDuplicateName(int start, int current, String[][] myResult, String name) {
for(int i = start; i < current; i++) {
if(myResult[i][0].toLowerCase().equals(name.toLowerCase())) {
return true;
}
}
-
+
return false;
}
-
+
public String secondsToString(int pTime) {
int min = pTime/60;
int sec = pTime-(min*60);
diff --git a/core/mainThread.java b/core/mainThread.java
index 0858d1e..d95f163 100644
--- a/core/mainThread.java
+++ b/core/mainThread.java
@@ -12,6 +12,7 @@
import enemyAI.*;
import gui.*;
+import audio.SoundEngine;
public class mainThread extends JFrame implements KeyListener, ActionListener, MouseMotionListener, MouseListener, FocusListener{
@@ -31,6 +32,12 @@ public class mainThread extends JFrame implements KeyListener, ActionListener, M
public static long sleepTime;
public static int framePerSecond, cpuUsage;
public static double thisTime, lastTime;
+ public static long fpsWindowStart;
+ public static int frameCountThisSecond;
+ public static boolean hasWaterDistortion;
+ public static long matchElapsedMs;
+ public static long lastTickRealTime;
+ public static boolean wasPausedLastFrame = true;
public static boolean JavaRTSLoaded;
public static boolean gamePaused, gameStarted, playerVictory, AIVictory, afterMatch;
public static texture[] textures;
@@ -62,8 +69,8 @@ public class mainThread extends JFrame implements KeyListener, ActionListener, M
public static final int optionMenu = 4;
public static final int highscoreMenu = 5;
- public static final int screen_width = 1024;
- public static final int screen_height = 682;
+ public static final int screen_width = (int)(1024);
+ public static final int screen_height = (int)(682);
public static final int screen_size= screen_width*screen_height;
public static final int shadowmap_width = 2048;
@@ -102,7 +109,7 @@ public mainThread(){
screen = ((DataBufferInt)dest).getData();
bufferScreen = screen;
- doubleBuffer2 = new BufferedImage(screen_width, screen_width, BufferedImage.TYPE_INT_RGB);
+ doubleBuffer2 = new BufferedImage(screen_width, screen_height, BufferedImage.TYPE_INT_RGB);
DataBuffer dest2 = doubleBuffer2.getRaster().getDataBuffer();
screen2 = ((DataBufferInt)dest2).getData();
buffer2Screen = screen2;
@@ -129,8 +136,9 @@ public mainThread(){
}
frameIndex = 0;
- frameInterval = 25;
+ frameInterval = 22; //~45fps target (1000/45 = 22.2ms, rounded down to the nearest whole ms)
lastDraw = 0;
+ fpsWindowStart = System.currentTimeMillis();
try {
myRobot = new Robot();
} catch (AWTException e) {
@@ -179,7 +187,7 @@ public void actionPerformed(ActionEvent e){
panel.addMouseMotionListener(this);
panel.addMouseListener(this);
panel.addFocusListener(this);
- panel.requestFocus();
+ panel.requestFocusInWindow();
//create camera
Camera = new camera(new vector(3,2f,-1.25f), 0, 300);
@@ -207,6 +215,8 @@ public void actionPerformed(ActionEvent e){
theGameCursor = new gameCursor();
theGameCursor.init();
+
+ SoundEngine.init();
currentMouseX = getLocationOnScreen().x + screen_width/2;
currentMouseY = getLocationOnScreen().y + screen_height/2;
@@ -231,7 +241,13 @@ public void actionPerformed(ActionEvent e){
}
- frameIndex++;
+ frameIndex++;
+
+ //the very first requestFocusInWindow() call can silently fail if the window hasn't actually
+ //been realized on screen yet, which would leave keyboard hotkeys dead until the player clicks
+ //into the window - keep retrying for the first couple of seconds until focus actually lands
+ if(frameIndex < 90 && !panel.isFocusOwner())
+ panel.requestFocusInWindow();
if(capturedMouse && !mouseLeftScreen && !focusLost) {
currentMouseX = MouseInfo.getPointerInfo().getLocation().x;
@@ -265,9 +281,25 @@ public void actionPerformed(ActionEvent e){
if(!gamePaused) {
if(gameStarted)
gameFrame++;
-
- timeString = secondsToString((int)(gameFrame*0.025));
-
+
+ //the displayed/saved match time is measured from real elapsed wall-clock time (excluding
+ //time spent paused), not derived from gameFrame count, so it stays accurate regardless of
+ //the actual frame rate achieved
+ long now = System.currentTimeMillis();
+ if(gameStarted){
+ if(gameFrame == 1){
+ matchElapsedMs = 0;
+ lastTickRealTime = now;
+ }else if(wasPausedLastFrame){
+ lastTickRealTime = now;
+ }
+ matchElapsedMs += now - lastTickRealTime;
+ lastTickRealTime = now;
+ }
+ wasPausedLastFrame = false;
+
+ timeString = secondsToString((int)(matchElapsedMs/1000));
+
//handle user's interaction with game GUI
if(gameFrame == 1 && gameStarted){
theAssetManager.prepareAssetForNewGame();
@@ -277,7 +309,10 @@ public void actionPerformed(ActionEvent e){
//Clears the z-buffer. All depth values are set to 0.
clearDepthBuffer();
-
+
+ //reset once per frame; the rasterizer sets this back to true if any water polygon is actually drawn
+ hasWaterDistortion = false;
+
//update camera
Camera.update();
@@ -292,11 +327,10 @@ public void actionPerformed(ActionEvent e){
ec.update();
}
}else {
-
-
+ wasPausedLastFrame = true;
}
-
- //show unpassable obstacle
+
+ //show unpassable obstacle
//gridMap.draw();
if(this.getGraphics() != null && PPT!= null){
@@ -443,32 +477,34 @@ public void mouseExited(MouseEvent arg0) {
public void mousePressed(MouseEvent e) {
if(e.getButton() == 1){
inputHandler.leftMouseButtonPressed = true;
-
+
}
-
+
if(e.getButton() == 3){
inputHandler.rightMouseButtonPressed = true;
+ inputHandler.rightMouseButtonHeld = true;
}
-
+
}
@Override
public void mouseReleased(MouseEvent e) {
if(e.getButton() == 1){
-
+
inputHandler.leftMouseButtonReleased = true;
}
-
+
if(e.getButton() == 3){
inputHandler.rightMouseButtonReleased = true;
+ inputHandler.rightMouseButtonHeld = false;
}
-
+
}
public void loadTexture(){
textures = new texture[73];
- String imageFolder = "../images/";
+ String imageFolder = "/images/";
try{
textures[0] = new texture("basic", ImageIO.read(getClass().getResource(imageFolder + "1.jpg")), 9, 9);
textures[1] = new texture("explosion aura", ImageIO.read(getClass().getResource(imageFolder + "2.jpg")), 7, 7);
@@ -664,10 +700,20 @@ public void regulateFramerate(){
// TODO Auto-generated catch block
e1.printStackTrace();
}
-
+
lastDraw=System.currentTimeMillis();
+
+ frameCountThisSecond++;
+ long elapsedSinceWindowStart = lastDraw - fpsWindowStart;
+ if(elapsedSinceWindowStart >= 1000){
+ //use the actual elapsed wall-clock time rather than assuming exactly 1000ms passed,
+ //since the window can close a few ms late depending on when frames land
+ framePerSecond = (int)Math.round(frameCountThisSecond * 1000.0 / elapsedSinceWindowStart);
+ frameCountThisSecond = 0;
+ fpsWindowStart = lastDraw;
+ }
}
-
+
public static String secondsToString(int pTime) {
int min = pTime/60;
int sec = pTime-(min*60);
diff --git a/core/playerCommander.java b/core/playerCommander.java
index 3be1ce0..f6d7e6e 100644
--- a/core/playerCommander.java
+++ b/core/playerCommander.java
@@ -2,10 +2,13 @@
import java.awt.Rectangle;
+import entity.constructionVehicle;
import entity.constructionYard;
import entity.factory;
import entity.solidObject;
import gui.inputHandler;
+import audio.SoundEngine;
+import audio.SoundEngine.Sfx;
//this class interprets player's inputs and turns them into commands that can be issued to game units
public class playerCommander {
@@ -14,7 +17,7 @@ public class playerCommander {
public solidObject[][] groups;
- public boolean leftMouseButtonPressed, rightMouseButtonPressed, leftMouseButtonReleased, rightMouseButtonReleased, attackKeyPressed, toggleConyard, toggleFactory, holdKeyPressed, controlKeyPressed;
+ public boolean leftMouseButtonPressed, rightMouseButtonPressed, leftMouseButtonReleased, rightMouseButtonReleased, attackKeyPressed, toggleConyard, toggleFactory, holdKeyPressed, controlKeyPressed, deployKeyPressed;
public int numberTyped;
@@ -46,11 +49,24 @@ public class playerCommander {
public constructionYard selectedConstructionYard;
public baseInfo theBaseInfo;
+
+ //tracks the previous frame's credit total so a change (from any of the many scattered
+ //currentCredit++/-- sites across buildings/units) can be detected in one place and turned into a
+ //classic RTS money-counter tick, throttled so a fast trickle (e.g. construction cost draining
+ //every frame) doesn't spam a sound 45 times a second. earn and spend use separate cooldowns
+ //(rather than one shared one) so an earn tick is never suppressed just because a spend tick
+ //happened to fire a moment earlier - earning always wins: spend is muted for the whole duration
+ //of an earn sound instead of just being throttled like it throttles itself
+ private int previousCredit = -1;
+ private int creditEarnCooldown;
+ private int creditSpendCooldown;
+ private int earnSoundBusyFrames;
public boolean mouseOverSelectableUnit;
public int mouseOverUnitType;
public int mouseOverUnitTeam;
public boolean mouseOverUnitIsSelected;
+ public boolean mouseOverUnitCanDeploy;
public boolean hasConVehicleSelected;
public boolean hasHarvesterSelected;
public boolean hasTroopsSelected;
@@ -82,7 +98,31 @@ public void init(){
public void update(){
theBaseInfo.update();
-
+
+ if(creditEarnCooldown > 0)
+ creditEarnCooldown--;
+ if(creditSpendCooldown > 0)
+ creditSpendCooldown--;
+ if(earnSoundBusyFrames > 0)
+ earnSoundBusyFrames--;
+
+ if(previousCredit == -1){
+ previousCredit = theBaseInfo.currentCredit;
+ }else if(theBaseInfo.currentCredit != previousCredit){
+ if(theBaseInfo.currentCredit > previousCredit){
+ if(creditEarnCooldown == 0){
+ SoundEngine.play(Sfx.CREDIT_EARN);
+ creditEarnCooldown = 3;
+ earnSoundBusyFrames = 38; //~820ms at 45fps, matches the earn clip's length so spend stays muted while it rings out
+ }
+ }else if(creditSpendCooldown == 0 && earnSoundBusyFrames == 0){
+ SoundEngine.play(Sfx.CREDIT_SPEND);
+ creditSpendCooldown = 3;
+ }
+ previousCredit = theBaseInfo.currentCredit;
+ }
+
+
if(isDeployingBuilding){
if(leftMouseButtonPressed && !cursorIsInMiniMap() && !cursorIsInSideBar() && selectedConstructionYard.dg.canBeDeployed){
@@ -95,15 +135,16 @@ public void update(){
leftMouseButtonPressed = false;
}
- if(rightMouseButtonPressed){
+ if(rightMouseButtonReleased && !inputHandler.isDraggingCamera){
isDeployingBuilding = false;
selectedConstructionYard.needToDrawDeploymentGrid = false;
selectedConstructionYard = null;
- rightMouseButtonPressed = false;
+ rightMouseButtonReleased = false;
}else{
if(!cursorIsInMiniMap()){
theSideBarManager.update();
leftMouseButtonPressed = false;
+ rightMouseButtonReleased = false;
isMovingViewWindow = false;
return;
}
@@ -377,9 +418,9 @@ public void update(){
}
- if(rightMouseButtonPressed){
+ if(rightMouseButtonReleased && !inputHandler.isDraggingCamera){
attackKeyPressed = false;
-
+
if(cursorIsInMiniMap()){
clickPoint.set(0.25f*(inputHandler.mouse_x-3), 0, 0.25f*(127-(inputHandler.mouse_y-(screen_height-131))));
}else{
@@ -475,7 +516,7 @@ public void update(){
if(selectedConyardID != -1) {
for(int i = 0; i < factories.length; i++) {
- if(factories[i].ID == selectedConyardID) {
+ if(factories[i] != null && factories[i].ID == selectedConyardID) {
factoryIndex = i;
break;
}
@@ -502,7 +543,41 @@ public void update(){
toggleFactory = false;
}
-
+
+ if(deployKeyPressed) {
+ int numOfSelectedConstructionYard = 0;
+ constructionYard selectedConyard = null;
+ for(int i = 0; i < selectedUnits.length; i++) {
+ if(selectedUnits[i] != null && selectedUnits[i].teamNo == 0 && selectedUnits[i].currentHP > 0 && selectedUnits[i].type == 104) {
+ numOfSelectedConstructionYard++;
+ selectedConyard = (constructionYard)selectedUnits[i];
+ }
+ }
+
+ if(numOfSelectedConstructionYard == 1) {
+ constructionYard cy = selectedConyard;
+ if(cy.powerPlantProgress == 240 || cy.refineryProgress == 240 || cy.factoryProgress == 240
+ || cy.communicationCenterProgress == 240 || cy.gunTurretProgress == 240
+ || cy.missileTurretProgress == 240 || cy.techCenterProgress == 240) {
+ cy.needToDrawDeploymentGrid = true;
+ isDeployingBuilding = true;
+ selectedConstructionYard = cy;
+ }
+ }
+
+ //deploy any selected construction vehicle(s) that are ready to expand into a construction yard
+ for(int i = 0; i < selectedUnits.length; i++) {
+ if(selectedUnits[i] != null && selectedUnits[i].teamNo == 0 && selectedUnits[i].currentHP > 0 && selectedUnits[i].type == 3) {
+ constructionVehicle cv = (constructionVehicle)selectedUnits[i];
+ if(cv.canBeDeployed()) {
+ cv.expand();
+ }
+ }
+ }
+
+ deployKeyPressed = false;
+ }
+
//display health bar when mouse cursor hover over a unit
if(!isSelectingUnit){
@@ -535,7 +610,12 @@ public void update(){
}else if(selectedUnits[i].type == 2) {
hasHarvesterSelected = true;
}else if(selectedUnits[i].type == 3) {
- hasConVehicleSelected = true;
+ //don't show the move cursor for a construction vehicle that's already mid-deploy -
+ //it's no longer something you'd issue a move order to
+ constructionVehicle cv = (constructionVehicle)selectedUnits[i];
+ if(cv.jobStatus != cv.deploying){
+ hasConVehicleSelected = true;
+ }
}else if(selectedUnits[i].type == 200 || selectedUnits[i].type == 199) {
hasTowerSelected = true;
}
@@ -654,6 +734,7 @@ public void moveSelectedUnit(float x, float y){
theAssetManager.confirmationIconInfo[1] = x;
theAssetManager.confirmationIconInfo[2] = y;
theAssetManager.confirmationIconInfo[3] = 0xbb22;
+ SoundEngine.play(Sfx.MOVE);
}
}
@@ -716,53 +797,52 @@ public void attackMoveSelectUnit(float x, float y){
theAssetManager.confirmationIconInfo[1] = x;
theAssetManager.confirmationIconInfo[2] = y;
theAssetManager.confirmationIconInfo[3] = 0xcc2222;
+ SoundEngine.play(Sfx.ATTACK);
}
}
public void addMouseHoverUnitToDisplayInfo(Rectangle unitArea, Rectangle unitAreaSmall){
solidObject theSelected = null;
+ int theSelectedIndex = -1;
mouseOverSelectableUnit = false;
mouseOverUnitIsSelected = false;
+ mouseOverUnitCanDeploy = false;
for(int i = 0; i < theAssetManager.visibleUnitCount; i++){
if(unitArea.contains(theAssetManager.visibleUnit[i].tempCentre.screenX, theAssetManager.visibleUnit[i].tempCentre.screenY)){
if((theAssetManager.visibleUnit[i].type < 100 || theAssetManager.visibleUnit[i].type >= 199) && !unitAreaSmall.contains(theAssetManager.visibleUnit[i].tempCentre.screenX, theAssetManager.visibleUnit[i].tempCentre.screenY))
continue;
-
+
if(theAssetManager.visibleUnit[i].type < 100 || theAssetManager.visibleUnit[i].type >= 199){
theSelected = theAssetManager.visibleUnit[i];
+ theSelectedIndex = i;
break;
}
-
- theSelected = theAssetManager.visibleUnit[i];
+
+ theSelected = theAssetManager.visibleUnit[i];
+ theSelectedIndex = i;
}
}
-
+
if(theSelected != null && theSelected.isSelectable && !cursorIsInMiniMap() && !cursorIsInSideBar()) {
mouseOverSelectableUnit = true;
mouseOverUnitType = theSelected.type;
mouseOverUnitTeam = theSelected.teamNo;
if(theSelected.isSelected) {
mouseOverUnitIsSelected = true;
+ if(theSelected.type == 3 && ((constructionVehicle)theSelected).canBeDeployed()){
+ mouseOverUnitCanDeploy = true;
+ }
}
-
- }
-
- if(theSelected != null && !theSelected.isSelected && theSelected.isSelectable && !cursorIsInMiniMap() && !cursorIsInSideBar()){
- mainThread.theAssetManager.selectedUnitsInfo[99][0] = theSelected.level << 16 | theSelected.groupNo << 8 | theSelected.type;
- mainThread.theAssetManager.selectedUnitsInfo[99][1] = (int)theSelected.tempCentre.screenX;
- mainThread.theAssetManager.selectedUnitsInfo[99][2] = (int)theSelected.tempCentre.screenY;
- if(theSelected.type == 199){
- mainThread.theAssetManager.selectedUnitsInfo[99][1] = (int)theSelected.screenX_gui;
- mainThread.theAssetManager.selectedUnitsInfo[99][2] = (int)theSelected.screenY_gui;
+
+ //show the health bar/group/progress block for the hovered unit too, even if it isn't
+ //selected - reuses the same "treat as selected for display" bit the draw loop checks,
+ //restoring the original hover-preview behaviour within the new all-visible-units data flow
+ if(!theSelected.isSelected && theSelectedIndex >= 0 && theSelectedIndex < theAssetManager.allUnitsInfo.length){
+ theAssetManager.allUnitsInfo[theSelectedIndex][0] |= (1<<24);
}
-
- mainThread.theAssetManager.selectedUnitsInfo[99][3] = (int)theSelected.type;
- mainThread.theAssetManager.selectedUnitsInfo[99][4] = theSelected.currentHP;
- mainThread.theAssetManager.selectedUnitsInfo[99][5] = theSelected.progressStatus;
- }else{
- mainThread.theAssetManager.selectedUnitsInfo[99][0] = -1;
}
+
}
public void selectUnit(Rectangle unitArea, Rectangle unitAreaSmall){
@@ -785,7 +865,14 @@ public void selectUnit(Rectangle unitArea, Rectangle unitAreaSmall){
}
if(theSelected != null){
-
+
+ //clicking your own already-selected, ready-to-deploy construction vehicle deploys it
+ //into a construction yard instead of just re-selecting it
+ if(theSelected.type == 3 && theSelected.isSelected && ((constructionVehicle)theSelected).canBeDeployed()){
+ ((constructionVehicle)theSelected).expand();
+ return;
+ }
+
if(!controlKeyPressed)
deSelectAll();
@@ -796,7 +883,8 @@ public void selectUnit(Rectangle unitArea, Rectangle unitAreaSmall){
addToSelection(theSelected);
theSelected.isSelected = true;
-
+ SoundEngine.play(Sfx.SELECT);
+
if(doubleClicked){
int type = theSelected.type;
for(int j = 0; j < theAssetManager.visibleUnitCount; j++){
@@ -831,8 +919,10 @@ public void selectMultipleUnits(Rectangle area){
theAssetManager.visibleUnit[i].isSelected = true;
}
}
+ if(unitIsSelected)
+ SoundEngine.play(Sfx.SELECT);
}
-
+
public void addToSelection(solidObject o){
//dont add gold mine to select units
//if(o.type == 103)
@@ -959,6 +1049,7 @@ public void attackUnit(solidObject o){
theAssetManager.confirmationIconInfo[1] = o.centre.x;
theAssetManager.confirmationIconInfo[2] = o.centre.z;
theAssetManager.confirmationIconInfo[3] = 0xcc2222;
+ SoundEngine.play(Sfx.ATTACK);
}
}
@@ -972,7 +1063,7 @@ public void harvestMine(solidObject o){
theAssetManager.confirmationIconInfo[1] = o.centre.x;
theAssetManager.confirmationIconInfo[2] = o.centre.z;
theAssetManager.confirmationIconInfo[3] = 0xbbbb00;
-
+ SoundEngine.play(Sfx.HARVEST);
}
}
}
@@ -987,6 +1078,7 @@ public void returnToRefinery(solidObject o){
theAssetManager.confirmationIconInfo[1] = o.centre.x;
theAssetManager.confirmationIconInfo[2] = o.centre.z;
theAssetManager.confirmationIconInfo[3] = 0xbbbb00;
+ SoundEngine.play(Sfx.HARVEST);
}
}
}
diff --git a/core/polygon3D.java b/core/polygon3D.java
index e60c44a..89fa680 100644
--- a/core/polygon3D.java
+++ b/core/polygon3D.java
@@ -49,13 +49,14 @@ public class polygon3D {
public solidObject parentObject;
//A pool of vectors which will be used for vector arithmetic
- public static vector
+ public static vector
tempVector1 = new vector(0,0,0),
tempVector2 = new vector(0,0,0),
tempVector3 = new vector(0,0,0),
tempVector4 = new vector(0,0,0),
tempVector5 = new vector(0,0,0),
- tempVector6 = new vector(0,0,0);
+ tempVector6 = new vector(0,0,0),
+ tempVector7 = new vector(0,0,0);
//whether the polygon is visible
@@ -77,7 +78,11 @@ public class polygon3D {
public int diffuse_I;
public int Ambient_I = 16; //the default ambient intensity is 16
public int reflectance = 96;
-
+
+ //additional brightness from a nearby explosion this frame, added on top of diffuse_I at render time;
+ //recomputed fresh every frame in update(), never accumulates
+ public int blastLightBoost;
+
//diffuse value at vertex (only for polygons with 3 vertex)
public byte[] diffuse = new byte[3];
@@ -225,10 +230,30 @@ public void update(){
tempVector1.subtract(vertex3D[0]);
if(tempVector1.dot(normal) <= 0){
visible = false;
-
+
return;
}
-
+
+ //add this frame's explosion light contribution, if this face actually points toward it
+ blastLightBoost = 0;
+ if(parentObject != null && parentObject.activeBlastLightIntensity > 0){
+ tempVector7.set(parentObject.activeBlastLightPos);
+ tempVector7.subtract(vertex3D[0]);
+ tempVector7.unit();
+ double facing = normal.dot(tempVector7);
+ //each face is lit purely by its own flat normal, with nothing shared across edges. on
+ //thin/angled geometry (e.g. a crane arm) two adjacent faces can sit near opposite
+ //extremes of facing - one nearly toward the blast, one nearly away - so a purely
+ //directional boost makes one face jump to full brightness right next to a neighbor at
+ //zero, reading as a hard dark seam along their shared edge. flooring the boost at a
+ //fraction of peak (instead of letting it hit zero) keeps that face-to-face gap bounded
+ //while still favoring the side that actually faces the explosion
+ double normalizedFacing = (facing + 1) / 2; //-1..1 -> 0..1
+ double factor = 0.6 + 0.4 * normalizedFacing;
+ blastLightBoost = (int)(factor * parentObject.activeBlastLightIntensity * 90);
+ }
+
+
//translate vertex from world space to camera space
float x = 0,y = 0, z = 0,
camX = camera.position.x, camY = camera.position.y, camZ = camera.position.z,
diff --git a/core/postProcessingThread.java b/core/postProcessingThread.java
index f7820a6..ae565c0 100644
--- a/core/postProcessingThread.java
+++ b/core/postProcessingThread.java
@@ -10,13 +10,15 @@
import particles.explosion;
import particles.helix;
import particles.smokeParticle;
+import audio.SoundEngine;
+import audio.SoundEngine.Sfx;
//this class handles all the post processing effect
public class postProcessingThread implements Runnable{
public static int[] currentScreen;
public static int[] currentZbuffer;
- public static int[][] currentSelectedUnitsInfo;
+ public static int[][] currentAllUnitsInfo;
public static int[][] unitInfoTable;
public static float[][] visionPolygonInfo;
public static float[][] explosionInfo;
@@ -47,6 +49,7 @@ public class postProcessingThread implements Runnable{
public static byte[] shadowBitmap;
public static byte[] smoothedShadowBitmap;
public static short[] displacementBuffer;
+ public static boolean hasWaterDistortion;
public static int[] offScreen;
@@ -268,7 +271,9 @@ public void doPostProcesssing(){
if(!explosions[i].isInAction){
if(j < explosionCount){
tempFloat = explosionInfo[j];
- explosions[i].setActive(tempFloat[0], tempFloat[1], tempFloat[2], tempFloat[3], (int)tempFloat[4], (int)tempFloat[5], (int)tempFloat[6], tempFloat[7]);
+ explosions[i].setActive(tempFloat[0], tempFloat[1], tempFloat[2], tempFloat[3], (int)tempFloat[4], (int)tempFloat[5], (int)tempFloat[6], tempFloat[7]);
+ SoundEngine.play(tempFloat[3] >= 2.5f ? Sfx.EXPLOSION_LARGE : Sfx.EXPLOSION_SMALL,
+ SoundEngine.volumeForDistance(tempFloat[0], tempFloat[1], tempFloat[2]));
j++;
}else {
break;
@@ -303,21 +308,23 @@ public void doPostProcesssing(){
blendShadow();
- //apply distortion map
+ //apply distortion map (only water polygons write non-31 values here, so skip both passes entirely when none are on screen)
int xMap;
int yMap;
int distortionIndex;
-
- for(int i = 0; i < screen_size; i++){
- if(displacementBuffer[i] != 31){
- xMap = ((displacementBuffer[i]&992) >> 5) - 16;
- yMap = (displacementBuffer[i]&31) - 16;
- distortionIndex = i + xMap + yMap*screen_width;
- if(distortionIndex > 0 && distortionIndex < screen_size ){
- if(currentZbuffer[i] - currentZbuffer[distortionIndex] > -1000000)
- offScreen[i] = currentScreen[distortionIndex];
- else
- offScreen[i] = currentScreen[i];
+
+ if(hasWaterDistortion){
+ for(int i = 0; i < screen_size; i++){
+ if(displacementBuffer[i] != 31){
+ xMap = ((displacementBuffer[i]&992) >> 5) - 16;
+ yMap = (displacementBuffer[i]&31) - 16;
+ distortionIndex = i + xMap + yMap*screen_width;
+ if(distortionIndex > 0 && distortionIndex < screen_size ){
+ if(currentZbuffer[i] - currentZbuffer[distortionIndex] > -1000000)
+ offScreen[i] = currentScreen[distortionIndex];
+ else
+ offScreen[i] = currentScreen[i];
+ }
}
}
}
@@ -349,60 +356,61 @@ public void doPostProcesssing(){
int z = vector.Z_length;
- for(int i = screen_width; i < screen_size-screen_width; i++){
- if(displacementBuffer[i] != 31){
-
- r1 = (offScreen[i + 1]&0xff0000) >> 16;
- r2 = (offScreen[i - w_]&0xff0000) >> 16;
- r3 = (offScreen[i - screen_width]&0xff0000) >> 16;
- r4 = (offScreen[i - 1]&0xff0000) >> 16;
-
-
- g1 = (offScreen[i + 1]&0xff00) >> 8;
- g2 = (offScreen[i - w_]&0xff00) >> 8;
- g3 = (offScreen[i - screen_width]&0xff00) >> 8;
- g4 = (offScreen[i]&0xff00) >> 8;
-
-
- b1 = (offScreen[i + 1]&0xff);
- b2 = (offScreen[i - w_]&0xff);
- b3 = (offScreen[i - screen_width]&0xff);
- b4 = (offScreen[i]&0xff);
-
-
-
-
- currentScreen[i] = (((r1 + r2 + r3 + r4)>>3) << 16 | ((g1 + g2 + g3 + g4)>>3) << 8 | ((b1 + b2 + b3 + b4)>>3 )) + c;
-
- yMap = (displacementBuffer[i]&64512) >> 10;
-
-
- if(yMap > 0){
- eyeDirection.set(-i%screen_width+w_half, -h_half + i/screen_width, -z);
- eyeDirection.unit();
- float I = eyeDirection.dot(lightReflect);
-
- if(I > 0.985){
- int I_ = (int)((I-0.985) *24000);
-
- yMap = yMap * I_ /90;
-
- SpriteValue = 0x010101*yMap;
-
- pixel=(SpriteValue&MASK7Bit)+(currentScreen[i]&MASK7Bit);
- overflow=pixel&0x1010100;
- overflow=overflow-(overflow>>8);
- currentScreen[i] = overflow|pixel;
+ if(hasWaterDistortion){
+ for(int i = screen_width; i < screen_size-screen_width; i++){
+ if(displacementBuffer[i] != 31){
+
+ r1 = (offScreen[i + 1]&0xff0000) >> 16;
+ r2 = (offScreen[i - w_]&0xff0000) >> 16;
+ r3 = (offScreen[i - screen_width]&0xff0000) >> 16;
+ r4 = (offScreen[i - 1]&0xff0000) >> 16;
+
+
+ g1 = (offScreen[i + 1]&0xff00) >> 8;
+ g2 = (offScreen[i - w_]&0xff00) >> 8;
+ g3 = (offScreen[i - screen_width]&0xff00) >> 8;
+ g4 = (offScreen[i]&0xff00) >> 8;
+
+
+ b1 = (offScreen[i + 1]&0xff);
+ b2 = (offScreen[i - w_]&0xff);
+ b3 = (offScreen[i - screen_width]&0xff);
+ b4 = (offScreen[i]&0xff);
+
+
+
+ currentScreen[i] = (((r1 + r2 + r3 + r4)>>3) << 16 | ((g1 + g2 + g3 + g4)>>3) << 8 | ((b1 + b2 + b3 + b4)>>3 )) + c;
+
+ yMap = (displacementBuffer[i]&64512) >> 10;
+
+
+ if(yMap > 0){
+ eyeDirection.set(-i%screen_width+w_half, -h_half + i/screen_width, -z);
+ eyeDirection.unit();
+ float I = eyeDirection.dot(lightReflect);
+
+ if(I > 0.985){
+ int I_ = (int)((I-0.985) *24000);
+
+ yMap = yMap * I_ /90;
+
+ SpriteValue = 0x010101*yMap;
+
+ pixel=(SpriteValue&MASK7Bit)+(currentScreen[i]&MASK7Bit);
+ overflow=pixel&0x1010100;
+ overflow=overflow-(overflow>>8);
+ currentScreen[i] = overflow|pixel;
+ }
+
}
-
+
+ displacementBuffer[i] = 31;
+ }else{
+
+
}
-
- displacementBuffer[i] = 31;
- }else{
-
-
+
}
-
}
if(gameStarted) {
@@ -445,27 +453,36 @@ public void doPostProcesssing(){
- //draw health bar/Group info/unit level for every selected unit
- for(int i = 0; i < 100; i++){
+ //draw health bar/Group info/unit level for every visible unit (shown by default now,
+ //not just when selected)
+ for(int i = 0; i < currentAllUnitsInfo.length; i++){
- if(currentSelectedUnitsInfo[i][0] != -1){
- ObjectType = (currentSelectedUnitsInfo[i][0] & 0xff);
- groupNo = ((currentSelectedUnitsInfo[i][0] & 0xff00) >> 8);
- level = ((currentSelectedUnitsInfo[i][0] & 0xff0000) >> 16);
+ if(currentAllUnitsInfo[i][0] != -1){
+ ObjectType = (currentAllUnitsInfo[i][0] & 0xff);
+ groupNo = ((currentAllUnitsInfo[i][0] & 0xff00) >> 8);
+ level = ((currentAllUnitsInfo[i][0] & 0xff0000) >> 16);
+ boolean isSelected = (currentAllUnitsInfo[i][0] & (1<<24)) != 0;
maxHealth = unitInfoTable[ObjectType][0];
healthBarLength = unitInfoTable[ObjectType][1];
- xPos = currentSelectedUnitsInfo[i][1] + unitInfoTable[ObjectType][2];
- yPos = currentSelectedUnitsInfo[i][2] + unitInfoTable[ObjectType][3];
- remainingHealth = healthBarLength * currentSelectedUnitsInfo[i][4] / maxHealth;
-
+ xPos = currentAllUnitsInfo[i][1] + unitInfoTable[ObjectType][2];
+ yPos = currentAllUnitsInfo[i][2] + unitInfoTable[ObjectType][3];
+
+ //veterancy star shows on every visible unit regardless of selection, so ranked
+ //units are easy to spot at a glance - everything else below stays selection-only
+ if(level != 0){
+ theTextRenderer.drawStarCharacter(xPos + healthBarLength - 13, yPos + 5, level, currentScreen, 0xffff33, 0);
+ }
+
+ if(!isSelected)
+ continue;
+
+ remainingHealth = healthBarLength * currentAllUnitsInfo[i][4] / maxHealth;
+
//draw group info
if(groupNo != 255){
theTextRenderer.drawText_outline(xPos, yPos + 3, String.valueOf(groupNo+1), currentScreen, 0xffffff, 0);
}
- if(level != 0){
- theTextRenderer.drawStarCharacter(xPos + healthBarLength - 13, yPos + 5, level, currentScreen, 0xffff33, 0);
- }
-
+
if(remainingHealth <= 2 && remainingHealth != 0)
remainingHealth = 2;
@@ -526,8 +543,8 @@ else if((float)remainingHealth / healthBarLength > 0.25)
}
//draw progress bar if appliable
- if(currentSelectedUnitsInfo[i][5] >=0){
- int progress = healthBarLength * currentSelectedUnitsInfo[i][5] / 100;
+ if(currentAllUnitsInfo[i][5] >=0){
+ int progress = healthBarLength * currentAllUnitsInfo[i][5] / 100;
if(yPos > 0 && yPos < screen_height){
if(xPos >= 0 && xPos < screen_width)
@@ -886,12 +903,18 @@ public static void blurShadow(){
smoothedShadowBitmap[index] = 32;
else
smoothedShadowBitmap[index]= (byte)(shadowBitmap[index] + 127);
-
+
+ }else if(shadowBitmap[index] >= 32){
+ //this pixel is already fully lit - don't drag it down just because a
+ //shadowed neighbor (e.g. a building's own shadow right at its silhouette)
+ //happens to sit next to it in screen space; only soften pixels that are
+ //themselves at least partly shadowed, which is the blur's actual purpose
+ smoothedShadowBitmap[index] = shadowBitmap[index];
}else{
smoothedShadowBitmap[index] = (byte)((shadowBitmap[index] + shadowBitmap[index - 1] + shadowBitmap[index + screen_width] + shadowBitmap[index + w_]) >> 2);
//smoothedShadowBitmap[index] = (byte)((shadowBitmap[index+769] + shadowBitmap[index+767] + shadowBitmap[index-769] + shadowBitmap[index-767] + shadowBitmap[index-768] + shadowBitmap[index] + shadowBitmap[index - 1] + shadowBitmap[index + 768] + shadowBitmap[index + 1])>>3);
-
+
}
}
}
@@ -911,10 +934,12 @@ public static void blurShadow(){
smoothedShadowBitmap[index] = 32;
else
smoothedShadowBitmap[index]= (byte)(shadowBitmap[index] + 127);
+ }else if(shadowBitmap[index] >= 32){
+ smoothedShadowBitmap[index] = shadowBitmap[index];
}else{
smoothedShadowBitmap[index] = (byte)((shadowBitmap[index] + shadowBitmap[index - 1] + shadowBitmap[index - screen_width] + shadowBitmap[index - screen_width - 1]) >> 2);
-
-
+
+
}
}
}
@@ -935,10 +960,12 @@ public static void blurShadow(){
smoothedShadowBitmap[index] = 32;
else
smoothedShadowBitmap[index]= (byte)(shadowBitmap[index] + 127);
+ }else if(shadowBitmap[index] >= 32){
+ smoothedShadowBitmap[index] = shadowBitmap[index];
}else{
smoothedShadowBitmap[index] = (byte)((shadowBitmap[index] + shadowBitmap[index + 1] + shadowBitmap[index - screen_width] + shadowBitmap[index - w_]) >> 2);
-
-
+
+
}
}
}
@@ -957,9 +984,11 @@ public static void blurShadow(){
smoothedShadowBitmap[index] = 32;
else
smoothedShadowBitmap[index]= (byte)(shadowBitmap[index] + 127);
+ }else if(shadowBitmap[index] >= 32){
+ smoothedShadowBitmap[index] = shadowBitmap[index];
}else{
smoothedShadowBitmap[index] = (byte)((shadowBitmap[index] + shadowBitmap[index + 1] + shadowBitmap[index + screen_width] + shadowBitmap[index + screen_width + 1]) >> 2);
-
+
}
}
}
@@ -1011,8 +1040,9 @@ public static void prepareResources(){
currentScreen = mainThread.screen;
currentZbuffer = mainThread.zBuffer;
displacementBuffer = mainThread.displacementBuffer;
+ hasWaterDistortion = mainThread.hasWaterDistortion;
shadowBitmap = mainThread.shadowBitmap;
- currentSelectedUnitsInfo = mainThread.theAssetManager.selectedUnitsInfo;
+ currentAllUnitsInfo = mainThread.theAssetManager.allUnitsInfo;
visionPolygonInfo = mainThread.theAssetManager.visionPolygonInfo;
visionPolygonCount = mainThread.theAssetManager.visionPolygonCount;
unitsForMiniMap = mainThread.theAssetManager.unitsForMiniMap;
diff --git a/core/rasterizer.java b/core/rasterizer.java
index 8da4462..51f6377 100644
--- a/core/rasterizer.java
+++ b/core/rasterizer.java
@@ -41,7 +41,7 @@ public class rasterizer {
public static int[] xLeft = new int[screen_height], xRight = new int[screen_height];
//2 arrays that define the z depth across the polygon
- public static int[] zLeft = new int[screen_height], zRight = new int[screen_height];
+ //public static int[] zLeft = new int[screen_height], zRight = new int[screen_height];
//2 arrays that define the reflections across the polygon
public static vector[] RLeft = new vector[screen_height], RRight = new vector[screen_height];
@@ -184,6 +184,7 @@ public static void rasterize(polygon3D polygon){
}else if(polygon.type == 6){
scanPolygon();
findVectorOUV();
+ mainThread.hasWaterDistortion = true;
renderWaterPolygon();
}else if(polygon.type == 7){
scanPolygon();
@@ -687,7 +688,7 @@ public static void renderCloakedShadow(polygon3D polygon){
//render basic polygon that can't be shadowed (e.g polygon which back facing the light source)
public static void renderBasicPolygon(){
short[] texture = poly.myTexture.pixelData;
- diffuse_I = poly.diffuse_I&127;
+ diffuse_I = Math.min(poly.diffuse_I + poly.blastLightBoost, 127);
int[]colorTable = gameData.colorTable[diffuse_I];
int index;
@@ -766,26 +767,26 @@ public static void renderBasicPolygon(){
cDotWInverse = 1/cDotW;
X1 = (int)(aDotW*cDotWInverse);
Y1 = (int)(bDotW*cDotWInverse);
- dx = X1 - X;
- dy = Y1 - Y;
-
+ dx = ((X1 - X) << 8)/offset;
+ dy = ((Y1 - Y) << 8)/offset;
+
for( k = offset, d_x = 0, d_y = 0; k >0; k--, d_x+=dx, d_y+=dy, index++, z_left+=dz){
-
+
if(zBuffer[index] < z_left){
zBuffer[index] = z_left;
- textureIndex = (((d_x/offset) + X)&widthMask) + ((((d_y/offset) + Y)&heightMask)<0; k--, d_x+=dx, d_y+=dy, index++, z_left+=dz){
- if(zBuffer[index] < z_left){
+ if(zBuffer[index] < depth){
zBuffer[index] = depth + 20;
xPos = (d_x>>4) + X;
yPos = (d_y>>4) + Y;
@@ -955,15 +956,15 @@ public static void renderUnderGroundPolygon(){
cDotWInverse = 1/cDotW;
X1 = (int)(aDotW*cDotWInverse);
Y1 = (int)(bDotW*cDotWInverse);
- dx = X1 - X;
- dy = Y1 - Y;
-
+ dx = ((X1 - X) << 8)/offset;
+ dy = ((Y1 - Y) << 8)/offset;
+
for( k = offset, d_x = 0, d_y = 0; k >0; k--, d_x+=dx, d_y+=dy, index++, z_left+=dz){
-
- if(zBuffer[index] < z_left){
+
+ if(zBuffer[index] < depth){
zBuffer[index] = depth + 20;
- xPos = (d_x/offset) + X;
- yPos = (d_y/offset) + Y;
+ xPos = (d_x/256) + X;
+ yPos = (d_y/256) + Y;
textureIndex = (xPos&widthMask) + ((yPos&heightMask)<> 16;
@@ -992,7 +993,7 @@ public static void renderUnderGroundPolygon(){
//redner basic texture mapped polygon
public static void renderShadowedPolygon(){
short[] texture = poly.myTexture.pixelData;
- diffuse_I = poly.diffuse_I&127;
+ diffuse_I = Math.min(poly.diffuse_I + poly.blastLightBoost, 127);
int[] colorTable = gameData.colorTable[diffuse_I];
int index, z_lightspace, screenX_lightspace, screenY_lightspace, xPos, yPos;
@@ -1153,15 +1154,15 @@ public static void renderShadowedPolygon(){
cDotWInverse = 1/cDotW;
X1 = (int)(aDotW*cDotWInverse);
Y1 = (int)(bDotW*cDotWInverse);
- dx = X1 - X;
- dy = Y1 - Y;
-
+ dx = ((X1 - X) << 8)/offset;
+ dy = ((Y1 - Y) << 8)/offset;
+
for( k = offset, d_x = 0, d_y = 0; k >0; k--, d_x+=dx, d_y+=dy, index++, z_left+=dz){
-
+
if(zBuffer[index] < z_left){
zBuffer[index] = z_left;
- xPos = (d_x/offset) + X;
- yPos = (d_y/offset) + Y;
+ xPos = (d_x/256) + X;
+ yPos = (d_y/256) + Y;
textureIndex = (xPos&widthMask) + ((yPos&heightMask)<> 16;
@@ -1186,7 +1187,7 @@ public static void renderShadowedPolygon(){
public static void renderShadowedPolygon_Gouraud(){
short[] texture = poly.myTexture.pixelData;
- diffuse_I = poly.diffuse_I&127;
+ diffuse_I = Math.min(poly.diffuse_I + poly.blastLightBoost, 127);
int[] colorTable = gameData.colorTable[diffuse_I];
int index, z_lightspace, screenX_lightspace, screenY_lightspace, xPos, yPos;
@@ -1332,7 +1333,7 @@ public static void renderShadowedPolygon_Gouraud(){
if(z_lightspace - shadowBuffer[size] < shadowBias){
shadowBitmap[index] = 32;
- screen[index] = gameData.colorTable[diffuseStart >> 11][texture[textureIndex]];
+ screen[index] = gameData.colorTable[Math.min((diffuseStart >> 11) + poly.blastLightBoost, 127)][texture[textureIndex]];
}else{
shadowBitmap[index] = shadowLevel;
screen[index] = colorTable[texture[textureIndex]];
@@ -1355,15 +1356,15 @@ public static void renderShadowedPolygon_Gouraud(){
cDotWInverse = 1/cDotW;
X1 = (int)(aDotW*cDotWInverse);
Y1 = (int)(bDotW*cDotWInverse);
- dx = X1 - X;
- dy = Y1 - Y;
-
+ dx = ((X1 - X) << 8)/offset;
+ dy = ((Y1 - Y) << 8)/offset;
+
for( k = offset, d_x = 0, d_y = 0; k >0; k--, d_x+=dx, d_y+=dy, index++, z_left+=dz, diffuseStart+=diffuseGradient){
-
+
if(zBuffer[index] < z_left){
zBuffer[index] = z_left;
- xPos = (d_x/offset) + X;
- yPos = (d_y/offset) + Y;
+ xPos = (d_x/256) + X;
+ yPos = (d_y/256) + Y;
textureIndex = (xPos&widthMask) + ((yPos&heightMask)<> 16;
@@ -1373,17 +1374,17 @@ public static void renderShadowedPolygon_Gouraud(){
if(z_lightspace - shadowBuffer[size] < shadowBias){
shadowBitmap[index] = 32;
- screen[index] = gameData.colorTable[diffuseStart >> 11][texture[textureIndex]];
-
+ screen[index] = gameData.colorTable[Math.min((diffuseStart >> 11) + poly.blastLightBoost, 127)][texture[textureIndex]];
+
}else{
shadowBitmap[index] = shadowLevel;
screen[index] = colorTable[texture[textureIndex]];
}
-
-
+
+
}
}
-
+
break;
}
}
@@ -1391,7 +1392,7 @@ public static void renderShadowedPolygon_Gouraud(){
public static void renderShadowedPolygon_smooth(){
short[] texture = poly.myTexture.pixelData;
- diffuse_I = poly.diffuse_I&127;
+ diffuse_I = Math.min(poly.diffuse_I + poly.blastLightBoost, 127);
int[] colorTable = gameData.colorTable[diffuse_I];
int index, z_lightspace, screenX_lightspace, screenY_lightspace, xPos, yPos;
@@ -1536,6 +1537,7 @@ public static void renderShadowedPolygon_smooth(){
int lit = (int)(I_left + I_difference * (xPos%textureScaledWidth));
if(lit < 0)
lit = 0;
+ lit = Math.min(lit + poly.blastLightBoost, 127);
screen[index] = gameData.colorTable[lit][texture[textureIndex]];
}else{
shadowBitmap[index] = shadowLevel;
@@ -1559,15 +1561,15 @@ public static void renderShadowedPolygon_smooth(){
cDotWInverse = 1/cDotW;
X1 = (int)(aDotW*cDotWInverse);
Y1 = (int)(bDotW*cDotWInverse);
- dx = X1 - X;
- dy = Y1 - Y;
-
+ dx = ((X1 - X) << 8)/offset;
+ dy = ((Y1 - Y) << 8)/offset;
+
for( k = offset, d_x = 0, d_y = 0; k >0; k--, d_x+=dx, d_y+=dy, index++, z_left+=dz){
-
+
if(zBuffer[index] < z_left){
zBuffer[index] = z_left;
- xPos = (d_x/offset) + X;
- yPos = (d_y/offset) + Y;
+ xPos = (d_x/256) + X;
+ yPos = (d_y/256) + Y;
textureIndex = (xPos&widthMask) + ((yPos&heightMask)<> 16;
@@ -1581,6 +1583,7 @@ public static void renderShadowedPolygon_smooth(){
int lit = (int)(I_left + I_difference * (xPos%textureScaledWidth));
if(lit < 0)
lit = 0;
+ lit = Math.min(lit + poly.blastLightBoost, 127);
screen[index] = gameData.colorTable[lit][texture[textureIndex]];
}else{
shadowBitmap[index] = shadowLevel;
@@ -2093,7 +2096,7 @@ public static void renderZbufferRemoverPolygon(){
public static void renderCloakedPolygon(){
short[] texture = poly.myTexture.pixelData;
- diffuse_I = poly.diffuse_I&127;
+ diffuse_I = Math.min(poly.diffuse_I + poly.blastLightBoost, 127);
int[] colorTable = gameData.colorTable[diffuse_I];
int index, z_lightspace, screenX_lightspace, screenY_lightspace, xPos, yPos;
@@ -2265,9 +2268,9 @@ public static void renderCloakedPolygon(){
cDotWInverse = 1/cDotW;
X1 = (int)(aDotW*cDotWInverse);
Y1 = (int)(bDotW*cDotWInverse);
- dx = X1 - X;
- dy = Y1 - Y;
-
+ dx = ((X1 - X) << 8)/offset;
+ dy = ((Y1 - Y) << 8)/offset;
+
for( k = 0, d_x = 0, d_y = 0; k < offset; k++, d_x+=dx, d_y+=dy, index++, z_left+=dz){
if(zBuffer[index] < z_left){
@@ -2284,30 +2287,30 @@ public static void renderCloakedPolygon(){
}
zBuffer[index] = z_left;
- xPos = (d_x/offset) + X;
- yPos = (d_y/offset) + Y;
+ xPos = (d_x/256) + X;
+ yPos = (d_y/256) + Y;
textureIndex = (xPos&widthMask) + ((yPos&heightMask)<> 16;
screenY_lightspace = (XY_origin_y + dXY_xdirection_y * xPos + dXY_ydirection_y * yPos) >> 16;
-
+
int size = (screenX_lightspace + (screenY_lightspace << shadowmap_width_bit)) & shadowmap_size_;
-
+
if(z_lightspace - shadowBuffer[size] < shadowBias){
shadowBitmap[index] = 32;
}else{
shadowBitmap[index] = shadowLevel;
}
screen[index] = colorTable[texture[textureIndex]];
-
+
}
}
-
+
break;
}
}
}
-
+
public static void calculateDepthRangeAtGround() {
vector v = mainThread.my2Dto3DFactory.get3DLocation(poly, screen_width/2, 0);
v.subtract(camera.position);
diff --git a/core/sideBarManager.java b/core/sideBarManager.java
index 099d290..11925f4 100644
--- a/core/sideBarManager.java
+++ b/core/sideBarManager.java
@@ -2,6 +2,8 @@
import entity.*;
import gui.inputHandler;
+import audio.SoundEngine;
+import audio.SoundEngine.Sfx;
//this class handles player's interaction with the sidebar
public class sideBarManager {
@@ -43,7 +45,10 @@ public sideBarManager(playerCommander pc){
}
- public void update(){
+ public void update(){
+ if(leftMouseButtonClicked)
+ SoundEngine.play(Sfx.CLICK);
+
//reset sideBarInfo
for(int i = 0; i < 9; i++)
sideBarInfo[i] = -1;
diff --git a/entity/constructionVehicle.java b/entity/constructionVehicle.java
index 097c3c1..b535a0b 100644
--- a/entity/constructionVehicle.java
+++ b/entity/constructionVehicle.java
@@ -4,6 +4,8 @@
import core.*;
import enemyAI.enemyCommander;
+import audio.SoundEngine;
+import audio.SoundEngine.Sfx;
public class constructionVehicle extends solidObject {
@@ -1972,6 +1974,8 @@ public boolean canBeDeployed() {
public void expand() {
jobStatus = deploying;
+ if(teamNo == 0)
+ SoundEngine.play(Sfx.DEPLOY_VEHICLE);
float theXPos = ((boundary2D.x1 + 8) / 16 * 0.25f) + 0.125f;
float theYPos = ((boundary2D.y1 - 8 - 1) / 16 * 0.25f) + 0.125f;
diff --git a/entity/constructionYard.java b/entity/constructionYard.java
index 9276c34..651dcd3 100644
--- a/entity/constructionYard.java
+++ b/entity/constructionYard.java
@@ -5,6 +5,8 @@
import core.*;
import enemyAI.enemyCommander;
import gui.deployGrid;
+import audio.SoundEngine;
+import audio.SoundEngine.Sfx;
//the construction yard model
@@ -1065,7 +1067,8 @@ public int addPolygon(polygon3D[] polys, polygon3D poly){
//update the model
public void update(){
-
+ updateActiveBlastLight();
+
//update tech tree info
canBuildPowerPlant = theBaseInfo.canBuildPowerPlant;
canBuildRefinery = theBaseInfo.canBuildRefinery;
@@ -1104,17 +1107,17 @@ public void update(){
//process emerging from ground animation
if(centre.y < -0.79f){
centre.y+=0.01;
- for(int i = 0; i < polygons.length; i++){
+ for(int i = 0; i < polygons.length; i++){
polygons[i].origin.y+=0.01;
polygons[i].rightEnd.y+=0.01;
polygons[i].bottomEnd.y+=0.01;
-
+
for(int j = 0; j < polygons[i].vertex3D.length; j++){
polygons[i].vertex3D[j].y+=0.01;
}
}
-
-
+
+
shadowvertex0.y+=0.01;
shadowvertex1.y+=0.01;
shadowvertex2.y+=0.01;
@@ -1671,6 +1674,7 @@ public boolean isIdle(){
//create building
public void createBuilding(){
+ SoundEngine.play(Sfx.DEPLOY_BUILDING);
if(powerPlantProgress == 240){
int y = 127 - dg.gridOneIndex/128;
int x = dg.gridOneIndex%128 + 1;
diff --git a/entity/factory.java b/entity/factory.java
index 3b12c3b..5197764 100644
--- a/entity/factory.java
+++ b/entity/factory.java
@@ -815,6 +815,7 @@ public int addPolygon(polygon3D[] polys, polygon3D poly){
//update the model
public void update(){
+ updateActiveBlastLight();
//update tech tree info
canBuildLightTank = theBaseInfo.canBuildLightTank;
diff --git a/entity/goldMine.java b/entity/goldMine.java
index f183530..27307a4 100644
--- a/entity/goldMine.java
+++ b/entity/goldMine.java
@@ -263,6 +263,7 @@ private void makePolygons(){
//update the model
public void update(){
+ updateActiveBlastLight();
if(!mainThread.gameStarted) {
isRevealed = true;
}else {
diff --git a/entity/gunTurret.java b/entity/gunTurret.java
index e13a74a..e0c724e 100644
--- a/entity/gunTurret.java
+++ b/entity/gunTurret.java
@@ -292,7 +292,9 @@ public void makePolygons(){
//update the model
- public void update(){
+ public void update(){
+ updateActiveBlastLight();
+
//process emerging from ground animation
if(centre.y < -0.5f){
centre.y+=0.01f;
diff --git a/entity/harvester.java b/entity/harvester.java
index 4a4e5d7..faf2af0 100644
--- a/entity/harvester.java
+++ b/entity/harvester.java
@@ -578,9 +578,9 @@ public void makeTriangle(polygon3D[] triangles, int startIndex, int angle, float
//update the model
- public void update(){
-
-
+ public void update(){
+ updateActiveBlastLight();
+
//handle unloading gold ore event
if(unloadingCount > 0){
if(unloadingCount > 69 && cargoAngle > 300)
diff --git a/entity/heavyTank.java b/entity/heavyTank.java
index 2037410..8e59312 100644
--- a/entity/heavyTank.java
+++ b/entity/heavyTank.java
@@ -356,6 +356,8 @@ public void makePolygons(){
//update and draw model
public void update(){
+ updateActiveBlastLight();
+
//check if tank has been destroyed
if(currentHP <= 0){
//spawn an explosion when the tank is destroyed
diff --git a/entity/lightPole.java b/entity/lightPole.java
index 2b952d9..7a05b0b 100644
--- a/entity/lightPole.java
+++ b/entity/lightPole.java
@@ -195,16 +195,16 @@ private void makePolygons(){
polygons[i].parentObject = this;
}
-
-
-
+
+ theAssetManager = mainThread.theAssetManager;
}
-
- //update the model
+
+ //update the model
public void update(){
if(vanished)
- return;
-
+ return;
+
+ updateActiveBlastLight();
mainThread.gridMap.currentObstacleMap[tileIndex] = false;
//update center in camera coordinate
diff --git a/entity/lightTank.java b/entity/lightTank.java
index e9e8709..e7ebfac 100644
--- a/entity/lightTank.java
+++ b/entity/lightTank.java
@@ -251,7 +251,8 @@ public void makePolygons(){
//update and draw model
public void update(){
-
+ updateActiveBlastLight();
+
//check if tank has been destroyed
if(currentHP <= 0){
//spawn an explosion when the tank is destroyed
diff --git a/entity/palmTree.java b/entity/palmTree.java
index 7d04525..11b5d63 100644
--- a/entity/palmTree.java
+++ b/entity/palmTree.java
@@ -241,11 +241,13 @@ private void makePolygons(){
polygons[i].findDiffuse();
polygons[i].parentObject = this;
}
-
+
+ theAssetManager = mainThread.theAssetManager;
}
-
- //update the model
+
+ //update the model
public void update(){
+ updateActiveBlastLight();
mainThread.gridMap.currentObstacleMap[tileIndex] = false;
//update center in camera coordinate
diff --git a/entity/powerPlant.java b/entity/powerPlant.java
index 2cdf223..986e345 100644
--- a/entity/powerPlant.java
+++ b/entity/powerPlant.java
@@ -543,6 +543,7 @@ public void makePolygons(){
//update the model
public void update(){
+ updateActiveBlastLight();
//process emerging from ground animation
if(centre.y < -0.5f){
diff --git a/entity/refinery.java b/entity/refinery.java
index 4180cfd..31d6b7f 100644
--- a/entity/refinery.java
+++ b/entity/refinery.java
@@ -13,6 +13,7 @@ public class refinery extends solidObject{
public polygon3D storageCoverLeft;
public polygon3D storageCoverRight;
+ public polygon3D depositFloor;
public int unloadOreCountDown;
public final int unloadOreTime = 190;
@@ -367,7 +368,8 @@ public void makePolygons(){
addPolygon(polygons, storageCoverRight);
v = new vector[]{put(-0.34, 0.27, -0.01), put(-0.13, 0.27, -0.01), put(-0.13, 0.27, -0.24), put(-0.34, 0.27, -0.24)};
- addPolygon(polygons, new polygon3D(v, v[0].myClone(), v[1].myClone(), v[3].myClone(), mainThread.textures[34], 1,1f,3));
+ depositFloor = new polygon3D(v, v[0].myClone(), v[1].myClone(), v[3].myClone(), mainThread.textures[34], 1,1f,3);
+ addPolygon(polygons, depositFloor);
v = new vector[]{put(-0.34,0.3,0.08), put(-0.34,0.3,0.2), put(-0.2,0.4,0.2), put(-0.2,0.4,0.08)};
addPolygon(polygons, new polygon3D(v, v[0].myClone(), v[1].myClone(), v[3].myClone(), mainThread.textures[35], 0.5f,0.3f,1));
@@ -698,6 +700,8 @@ public int addPolygon(polygon3D[] polys, polygon3D poly){
//update the model
public void update(){
+ updateActiveBlastLight();
+
//process emerging from ground animation
if(centre.y < -0.79f){
centre.y+=0.02f;
@@ -1100,10 +1104,14 @@ public void draw(){
if(!visible)
return;
for(int i = 0; i < polygons.length; i++){
-
+
polygons[i].update();
}
-
+
+ //don't show the ore deposit while the refinery is still rising out of the ground
+ if(centre.y < -0.79f)
+ depositFloor.visible = false;
+
for(int i = 0; i < polygons.length; i++){
polygons[i].draw();
}
diff --git a/entity/solidObject.java b/entity/solidObject.java
index c17c3f8..8f84fe7 100644
--- a/entity/solidObject.java
+++ b/entity/solidObject.java
@@ -147,9 +147,60 @@ public abstract class solidObject{
public int ID;
public AssetManager theAssetManager;
-
+
public float height;
-
+
+ //the single strongest nearby explosion light this model is currently reacting to, if any
+ public vector activeBlastLightPos = new vector(0,0,0);
+ public float activeBlastLightIntensity;
+
+ //TEMPORARY DIAGNOSTIC: highest activeBlastLightIntensity ever observed on any model, shown in the
+ //title bar. score is supposed to be capped at 1f in updateActiveBlastLight() below - if this ever
+ //reads above 1.00 the cap itself is being bypassed somewhere; if it never exceeds 1.00 even during
+ //an "over bright" moment, the bug is in rendering, not in this scoring math
+ public static float debugPeakBlastIntensity = 0;
+
+ //finds the single strongest nearby blast light (if any) for this model to react to;
+ //deliberately picks only one light per model instead of summing every explosion in range,
+ //so each polygon only ever has to test against one light source
+ public void updateActiveBlastLight(){
+ activeBlastLightIntensity = 0;
+ if(theAssetManager == null)
+ return;
+
+ float[][] lights = theAssetManager.blastLights;
+ float range = 2.4f;
+
+ for(int i = 0; i < lights.length; i++){
+ float[] light = lights[i];
+ if(light[3] <= 0)
+ continue;
+
+ float dx = centre.x - light[0];
+ float dy = centre.y - light[1];
+ float dz = centre.z - light[2];
+ float distSq = dx*dx + dy*dy + dz*dz;
+ if(distSq >= range*range)
+ continue;
+
+ float dist = (float)Math.sqrt(distSq);
+ float distanceFalloff = 1f - dist/range;
+ distanceFalloff *= distanceFalloff; //steeper falloff so a closer light reliably wins over a bigger, farther one
+ float lifeFalloff = light[3]/11f;
+ float sizeFactor = Math.min(light[4]/2f, 1.5f); //bounds how much a bigger blast can outweigh distance
+ float score = Math.min(sizeFactor * distanceFalloff * lifeFalloff, 1f); //cap so a big, close, fresh blast can't inflate the boost beyond the range polygon3D's factor curve is tuned for
+
+ if(score > activeBlastLightIntensity){
+ activeBlastLightIntensity = score;
+ activeBlastLightPos.set(light[0], light[1], light[2]);
+ }
+ }
+
+ if(activeBlastLightIntensity > debugPeakBlastIntensity)
+ debugPeakBlastIntensity = activeBlastLightIntensity;
+ }
+
+
public static Rect fullSizedProbe = new Rect(0,0, 16, 16);
public int progressStatus = -1;
diff --git a/entity/stealthTank.java b/entity/stealthTank.java
index b63e5d7..26be65f 100644
--- a/entity/stealthTank.java
+++ b/entity/stealthTank.java
@@ -4,6 +4,8 @@
import core.*;
import enemyAI.enemyCommander;
+import audio.SoundEngine;
+import audio.SoundEngine.Sfx;
public class stealthTank extends solidObject{
@@ -1149,7 +1151,8 @@ public void fireRailgunShot(int attackAngle){
tempVector.rotate_XZ(360 - attackAngle);
attackCoolDown = myAttackCooldown;
- cloakCooldownCount = 120;
+ cloakCooldownCount = 120;
+ SoundEngine.play(Sfx.SHOOT_RAILGUN, SoundEngine.volumeForDistance(firingPosition.x, firingPosition.y, firingPosition.z));
for(float i = 0.1f; i < distanceToDesination; i+=0.1f){
if(theAssetManager.helixCount >= theAssetManager.helixInfo.length)
diff --git a/gui/SideBar.java b/gui/SideBar.java
index e9190db..7c4c57f 100644
--- a/gui/SideBar.java
+++ b/gui/SideBar.java
@@ -54,7 +54,7 @@ public void init(){
iconImages = new int[25][44 * 44];
iconImages_dark = new int[25][44 * 44];
- String folder = "../images/";
+ String folder = "/images/";
loadTexture(folder + "44.jpg", iconImages[0], 44, 44, iconImages_dark[0]);
loadTexture(folder + "47.jpg", iconImages[1], 44, 44, iconImages_dark[1]);
loadTexture(folder + "48.jpg", iconImages[2], 44, 44, iconImages_dark[2]);
diff --git a/gui/gameCursor.java b/gui/gameCursor.java
index 9c65dfc..5dbb201 100644
--- a/gui/gameCursor.java
+++ b/gui/gameCursor.java
@@ -13,6 +13,7 @@ public class gameCursor {
public int[][] arrowIcons;
public int[][] smallArrowIcons;
public int[] smallArrowIcons4;
+ public int[][] smallArrowCardinal; //0=right, 1=down, 2=left, 3=up - same thin style as smallArrowIcons, for the deploy cursor
public int[] cursorIcon;
public int[] screen;
public int[][] iconOverWriteBuffer;
@@ -23,7 +24,7 @@ public class gameCursor {
public void init() {
- String folder = "../images/";
+ String folder = "/images/";
arrowIcons = new int[8][24*24];
for(int i = 0; i < 8; i++) {
@@ -40,6 +41,12 @@ public void init() {
smallArrowIcons4 = new int[20*20];
loadTexture(folder + "smallArrow4.png", smallArrowIcons4, 20,20);
+
+ smallArrowCardinal = new int[4][20*20];
+ String[] cardinalNames = {"Right", "Down", "Left", "Up"};
+ for(int i = 0; i < 4; i++) {
+ loadTexture(folder + "smallArrow" + cardinalNames[i] + ".png", smallArrowCardinal[i], 20,20);
+ }
iconOverWriteBuffer = new int[1024][2];
for(int i = 0; i < 1024; i++) {
@@ -57,6 +64,7 @@ public void updateAndDraw(int[] screen) {
int mouseOverUnitType = mainThread.pc.mouseOverUnitType;
int mouseOverUnitTeam = mainThread.pc.mouseOverUnitTeam;
boolean mouseOverUnitIsSelected = mainThread.pc.mouseOverUnitIsSelected;
+ boolean mouseOverUnitCanDeploy = mainThread.pc.mouseOverUnitCanDeploy;
boolean hasConVehicleSelected = mainThread.pc.hasConVehicleSelected;
boolean hasHarvesterSelected = mainThread.pc.hasHarvesterSelected;
boolean hasTroopsSelected = mainThread.pc.hasTroopsSelected;
@@ -80,7 +88,16 @@ public void updateAndDraw(int[] screen) {
//draw arrow icons if the player is scrolling the screen with the mouse
int cursorX = 0;
int cursorY = 0;
- if(camera.MOVE_DOWN && !camera.MOVE_LEFT && ! camera.MOVE_RIGHT) {
+ if(inputHandler.isDraggingCamera && inputHandler.rightMouseButtonHeld) {
+ //isDraggingCamera alone isn't reset until the next press starts (playerCommander needs
+ //it to stay true through the release frame to suppress the move/attack click), so also
+ //require the button to still be physically held before showing the drag cursor icon
+ //show which of the 8 scroll directions the current drag offset corresponds to,
+ //reusing the same edge-scroll arrow icons but centred on the cursor itself
+ int dragIconIndex = dragDirectionIcon(mouseX - inputHandler.rightDragAnchorX, mouseY - inputHandler.rightDragAnchorY);
+ if(dragIconIndex >= 0)
+ drawIcon(arrowIcons[dragIconIndex], mouseX-12, mouseY-12);
+ }else if(camera.MOVE_DOWN && !camera.MOVE_LEFT && ! camera.MOVE_RIGHT) {
drawIcon(arrowIcons[1], mouseX-12,screen_height - 23);
}else if(camera.MOVE_UP && !camera.MOVE_LEFT && ! camera.MOVE_RIGHT) {
drawIcon(arrowIcons[3], mouseX-12,0);
@@ -143,7 +160,9 @@ public void updateAndDraw(int[] screen) {
drawIcon(arrowIcons[5], cursorX, cursorY);
}else if(mouseOverSelectableUnit && !cursorIsInMiniMap && !cursorIsInSideBar){
- if((hasTroopsSelected || hasTowerSelected) && mouseOverUnitTeam == 1) {
+ if(mouseOverUnitIsSelected && mouseOverUnitCanDeploy) {
+ drawDeployIcon(mouseX, mouseY);
+ }else if((hasTroopsSelected || hasTowerSelected) && mouseOverUnitTeam == 1) {
drawActionIcon(mouseX, mouseY, 1);
}else if(!hasHarvesterSelected && !hasTroopsSelected && !hasTowerSelected) {
if(!mouseOverUnitIsSelected)
@@ -400,7 +419,79 @@ public void drawActionIcon(int xPos, int yPos, int type) {
}
}
-
+
+ //construction-vehicle deploy cursor: like the move icon's 4 converging diagonal arrows, but
+ //mirrored - cardinal (up/down/left/right) arrows spreading outward from the cursor instead of
+ //diagonal arrows converging inward. Uses new thin cardinal arrow images (smallArrowCardinal, same
+ //20x20 slender style as the diagonal smallArrowIcons) rather than the bulkier edge-scroll icons.
+ public void drawDeployIcon(int xPos, int yPos) {
+ xPos -= 10;
+ yPos -= 10;
+
+ //radius grows outward over the cycle (the inverse of drawActionIcon's shrinking r), then snaps
+ //back, giving a "spreading away from the cursor" pulse instead of a "converging on it" one
+ int r = 9 + (mainThread.gameFrame%21)/2;
+
+ int arrowColor = 34 << 16 | 200 << 8 | 76;
+
+ drawSmallCardinalArrow(smallArrowCardinal[3], xPos, yPos - r, arrowColor); //up
+ drawSmallCardinalArrow(smallArrowCardinal[1], xPos, yPos + r, arrowColor); //down
+ drawSmallCardinalArrow(smallArrowCardinal[2], xPos - r, yPos, arrowColor); //left
+ drawSmallCardinalArrow(smallArrowCardinal[0], xPos + r, yPos, arrowColor); //right
+ }
+
+ private void drawSmallCardinalArrow(int[] icon, int xPos, int yPos, int arrowColor) {
+ int index = 0;
+ int color = 0;
+ int blue = 0;
+ int red = 0;
+ for(int i = 0; i < 20; i++) {
+ for(int j = 0; j < 20; j++) {
+ if(xPos + j < 0 || xPos + j >= screen_width || yPos + i < 0 || yPos + i >= screen_height)
+ continue;
+
+ index = xPos + j + (yPos + i)*screen_width;
+ color = icon[j+i*20];
+
+ blue = color&0xff;
+ red = (color&0xff0000) >> 16;
+ if(red < 100 && blue > 100)
+ continue;
+
+ if(pixelInsideSideArea(index))
+ continue;
+
+ if(red > 200)
+ color = arrowColor;
+
+ iconOverWriteBuffer[iconOverWriteBufferIndex][0] = index;
+ iconOverWriteBuffer[iconOverWriteBufferIndex][1] = screen[index];
+ iconOverWriteBufferIndex++;
+ screen[index] = color;
+ }
+ }
+ }
+
+ //maps a right-click-drag offset to the matching 8-direction arrow icon index (arrowIcons[0..7]:
+ //right, down, left, up, up-right, down-right, down-left, up-left), same set used for edge scrolling
+ public int dragDirectionIcon(int offsetX, int offsetY){
+ int deadZone = 8;
+ boolean right = offsetX > deadZone;
+ boolean left = offsetX < -deadZone;
+ boolean down = offsetY > deadZone;
+ boolean up = offsetY < -deadZone;
+
+ if(right && up) return 4;
+ if(right && down) return 5;
+ if(left && down) return 6;
+ if(left && up) return 7;
+ if(right) return 0;
+ if(down) return 1;
+ if(left) return 2;
+ if(up) return 3;
+ return -1;
+ }
+
public boolean pixelInsideSideArea(int index){
int x = index%screen_width;
int y = index/screen_width;
diff --git a/gui/gameMenu.java b/gui/gameMenu.java
index 109eeab..521ac2f 100644
--- a/gui/gameMenu.java
+++ b/gui/gameMenu.java
@@ -6,6 +6,8 @@
import java.awt.image.PixelGrabber;
import javax.imageio.ImageIO;
import core.*;
+import audio.SoundEngine;
+import audio.SoundEngine.Sfx;
public class gameMenu {
@@ -26,7 +28,7 @@ public class gameMenu {
public int[] titleImage, lightTankImage, rocketTankImage, stealthTankImage, heavyTankImage;
public button newGame, unpauseGame, showHelp, showOptions, showHighscores, quitGame, abortGame, easyGame, normalGame, hardGame, quitDifficulty, quitHelpMenu, quitOptionMenu, quitHighscoreMenu, nextPage, previousPage,
- enableMouseCapture, disableMouseCapture, enableFogOfWar, disableFogOfWar, confirmErrorLoadingHighscore, normalToHardButton, normalToEasyButton, hardToNormalButton, easyToNormalButton,
+ enableMouseCapture, disableMouseCapture, enableFogOfWar, disableFogOfWar, enableSound, disableSound, confirmErrorLoadingHighscore, normalToHardButton, normalToEasyButton, hardToNormalButton, easyToNormalButton,
backToMapDefeat, leaveGameDefeat, backToMapVictory, leaveGameVictory, uploadScore;
public char[] easyDescription, normalDescription, hardDescription, helpPage1, helpPage2, helpPage3, helpPage4, mouseMode;
@@ -65,7 +67,7 @@ public void init() {
for(int i = 0; i< 32; i++)
name[i] = 255;
- String folder = "../images/";
+ String folder = "/images/";
loadTexture(folder + "title.png", titleImage, 216, 35);
loadTexture(folder + "58.jpg", lightTankImage, 44, 44);
loadTexture(folder + "59.jpg", rocketTankImage, 44, 44);
@@ -114,17 +116,19 @@ public void init() {
+ "\"Esc\" -- Pause/Unpause the game.\n\n"
+ "\"Left Click\" -- Select a unit. Left click + mouse drag can be used to select up to \n100 units at a time. Double left click on a unit will automatically select surrounding \nunits of the same type.\n\n"
+ "\"Right Click\" -- Issue a move or attack command to the selected unit(s). You can \nalso use right click to set rally point or cancel build progress.\n\n"
+ + "\"Right Click + Drag\" -- Hold the right button and move the mouse to scroll the map, \nlike a modern RTS \"grab and drag\" camera.\n\n"
+ "\"a\" -- Force attack a unit. If no unit is under the cursor, then the selected units will \nbe set to attack move to the cursor location.\n\n"
+ "\"s\" -- stop current action for the selected unit(s).\n\n"
+ "\"Ctrl + number\" -- Create a control group and assigned the number to the group.\n\n"
- + "\"Ctrl + Left Click\" -- Add/Remove a unit to/from the selected units.\n\n"
- + "\"Ctrl + Mouse Drag\" -- Add units in the dragging box to the selected units.\n\n\n"
+ + "\"Ctrl + Left Click\" -- Add/Remove a unit to/from the selected units.\n\n\n\n"
+ " 1/4 ").toCharArray();
helpPage2 = (" Controls (Cont.) \n\n"
+ "\"Left and Right arrow keys\" -- Change camera view angle.\n\n"
+ "\"c\" -- Toggle between different construction yards under your control.\n\n"
- + "\"f\" -- Toggle between different factories under your control.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
+ + "\"f\" -- Toggle between different factories under your control.\n\n"
+ + "\"d\" -- Activate the deployment grid for a selected construction yard's building \nthat is ready to be placed, or deploy a selected construction vehicle, without having \nto click a sidebar icon first.\n\n"
+ + "\"Ctrl + Mouse Drag\" -- Add units in the dragging box to the selected units.\n\n\n\n\n\n\n\n\n\n\n"
+ " 2/4 ").toCharArray();
helpPage3 = (" Units \n\n"
@@ -152,7 +156,8 @@ public void init() {
+ " 4/4").toCharArray();
mouseMode = (" Options \n\n\nMouse capture. When enabled the game will prevent \nthe mouse cursor from leaving the current window.\n\n\n"
- + "Fog of war. When enabled, enemy units that are not \nin vision will be hidden. Note that your score will NOT \nbe saved when this option is disabled.").toCharArray();
+ + "Fog of war. When enabled, enemy units that are not \nin vision will be hidden. Note that your score will NOT \nbe saved when this option is disabled.\n\n\n"
+ + "Sound. Toggles all sound effects on or off.").toCharArray();
quitHelpMenu = new button("quitHelpMenu", "x", 670, 80, 18,16);
buttons.add(quitHelpMenu);
@@ -180,7 +185,13 @@ public void init() {
disableFogOfWar = new button("disableFogOfWar", "Enabled", 545, 215, 80, 25);
buttons.add(disableFogOfWar);
-
+
+ enableSound = new button("enableSound", "Disabled", 545, 285, 80, 25);
+ buttons.add(enableSound);
+
+ disableSound = new button("disableSound", "Enabled", 545, 285, 80, 25);
+ buttons.add(disableSound);
+
confirmErrorLoadingHighscore = new button("confirmErrorLoadingHighscore", "Ok", 350, 280, 80, 25);
buttons.add(confirmErrorLoadingHighscore);
@@ -477,7 +488,15 @@ public void updateAndDraw(int[] screen, boolean gameStarted, boolean gamePaused,
disableFogOfWar.display = true;
enableFogOfWar.display = false;
}
-
+
+ if(SoundEngine.enabled) {
+ disableSound.display = false;
+ enableSound.display = true;
+ }else {
+ disableSound.display = true;
+ enableSound.display = false;
+ }
+
quitOptionMenu.display = true;
if(postProcessingThread.gameStarted) {
@@ -648,6 +667,7 @@ public void updateButtons() {
uploadingScore = true;
}
+ SoundEngine.play(Sfx.CLICK);
postProcessingThread.buttonAction = buttons.get(i).name;
}
}
diff --git a/gui/inputHandler.java b/gui/inputHandler.java
index 2b2d3b7..c8b57c5 100644
--- a/gui/inputHandler.java
+++ b/gui/inputHandler.java
@@ -7,12 +7,13 @@
import core.camera;
import core.geometry;
import core.mainThread;
+import audio.SoundEngine;
public class inputHandler {
public static int mouse_x, mouse_y,mouse_x0, mouse_x1, mouse_y0, mouse_y1, cameraMovementAngle;
- public static boolean mouseIsInsideScreen, userIsHoldingA, userIsHoldingC, userIsHoldingF;
+ public static boolean mouseIsInsideScreen, userIsHoldingA, userIsHoldingC, userIsHoldingF, userIsHoldingD;
public static boolean leftKeyPressed,
rightKeyPressed,
@@ -25,8 +26,14 @@ public class inputHandler {
A_pressed,
C_pressed,
F_pressed,
+ D_pressed,
escapeKeyPressed,
- escapeKeyReleased;
+ escapeKeyReleased,
+ rightMouseButtonHeld,
+ isDraggingCamera,
+ wasRightMouseButtonHeldLastFrame;
+
+ public static int rightDragAnchorX, rightDragAnchorY;
public static int numberTyped;
@@ -80,8 +87,12 @@ public static void processInput(){
if(c == 'f' || c == 'F'){
F_pressed = true;
}
-
-
+
+ if(c == 'd' || c == 'D'){
+ D_pressed = true;
+ }
+
+
if(c >=49 && c <=53){
numberTyped = c - 48;
}
@@ -114,7 +125,11 @@ public static void processInput(){
if(c == 'f' || c == 'F'){
F_pressed = true;
}
-
+
+ if(c == 'd' || c == 'D'){
+ D_pressed = true;
+ }
+
if(c >=49 && c <=53){
numberTyped = c - 48;
}
@@ -148,6 +163,10 @@ public static void processInput(){
F_pressed = false;
userIsHoldingF = false;
}
+ if(c == 'd' || c == 'D'){
+ D_pressed = false;
+ userIsHoldingD = false;
+ }
keyReleaseBufferIndex++;
}
keyReleaseBufferIndex = 0;
@@ -172,7 +191,11 @@ public static void processInput(){
F_pressed = false;
userIsHoldingF = false;
}
-
+ if(c == 'd' || c == 'D'){
+ D_pressed = false;
+ userIsHoldingD = false;
+ }
+
keyReleaseBufferIndex++;
}
@@ -186,7 +209,34 @@ public static void processInput(){
camera.MOVE_DOWN = false;
camera.TURN_LEFT = false;
camera.TURN_RIGHT = false;
-
+
+ //right-click-drag map scrolling: press to anchor a point, then the screen keeps scrolling
+ //towards wherever the cursor is dragged - relative to that fixed anchor, not frame-to-frame
+ //movement - for as long as the button stays held, at a speed based on how far the cursor
+ //has been pulled away from the anchor. Releasing the button stops the scroll immediately.
+ if(rightMouseButtonHeld){
+ if(!wasRightMouseButtonHeldLastFrame){
+ rightDragAnchorX = mouse_x;
+ rightDragAnchorY = mouse_y;
+ isDraggingCamera = false;
+ }
+
+ int offsetX = mouse_x - rightDragAnchorX;
+ int offsetY = mouse_y - rightDragAnchorY;
+ if(!isDraggingCamera && Math.abs(offsetX) + Math.abs(offsetY) > 6)
+ isDraggingCamera = true;
+ if(isDraggingCamera)
+ camera.panByHeldOffset(offsetX, offsetY);
+
+ wasRightMouseButtonHeldLastFrame = true;
+ }else{
+ //isDraggingCamera itself is deliberately NOT reset here - playerCommander checks it on
+ //the same frame the release event fires to decide whether to suppress the move/attack
+ //command that a right-click would otherwise issue, and resetting it this early would
+ //defeat that check. gameCursor instead also requires rightMouseButtonHeld for its icon.
+ wasRightMouseButtonHeldLastFrame = false;
+ }
+
if(!mainThread.pc.isSelectingUnit){
mouse_x0 = mouse_x;
mouse_y0 = mouse_y;
@@ -314,6 +364,12 @@ public static void processInput(){
userIsHoldingF = true;
}
}
+ if(D_pressed) {
+ if(!userIsHoldingD) {
+ mainThread.pc.deployKeyPressed = true;
+ userIsHoldingD = true;
+ }
+ }
//handle escape key
@@ -363,12 +419,21 @@ public static void processInput(){
if(mainThread.buttonAction == "enableFogOfWar") {
mainThread.fogOfWarDisabled = false;
-
+
}else if(mainThread.buttonAction == "disableFogOfWar") {
mainThread.fogOfWarDisabled = true;
-
+
}
}
+
+ //toggle sound, allowed whether or not a game is in progress
+ if(mainThread.buttonAction == "enableSound") {
+ SoundEngine.enabled = true;
+
+ }else if(mainThread.buttonAction == "disableSound") {
+ SoundEngine.enabled = false;
+
+ }
//abort current game when the abort button is pressed
if(mainThread.gameStarted && mainThread.buttonAction == "abortGame") {
@@ -425,6 +490,7 @@ public static void processInput(){
S_pressed = false;
C_pressed = false;
F_pressed = false;
+ D_pressed = false;
numberTyped = 0;
}
diff --git a/gui/textRenderer.java b/gui/textRenderer.java
index 95dd448..6ee161f 100644
--- a/gui/textRenderer.java
+++ b/gui/textRenderer.java
@@ -27,7 +27,7 @@ public void init(){
//load font image
Image img = null;
try{
- img = ImageIO.read(getClass().getResource("../images/" + "font.jpg"));
+ img = ImageIO.read(getClass().getResource("/images/" +"font.jpg"));
}catch(Exception e){
e.printStackTrace();
}
@@ -40,7 +40,7 @@ public void init(){
}
try{
- img = ImageIO.read(getClass().getResource("../images/" + "menuFont.png"));
+ img = ImageIO.read(getClass().getResource("/images/" +"menuFont.png"));
}catch(Exception e){
e.printStackTrace();
}
@@ -86,7 +86,7 @@ public void init(){
//load half star images
try{
- img = ImageIO.read(getClass().getResource("../images/" + "84.jpg"));
+ img = ImageIO.read(getClass().getResource("/images/" +"84.jpg"));
}catch(Exception e){
e.printStackTrace();
}
@@ -101,7 +101,7 @@ public void init(){
//load star images
try{
- img = ImageIO.read(getClass().getResource("../images/" + "85.jpg"));
+ img = ImageIO.read(getClass().getResource("/images/" +"85.jpg"));
}catch(Exception e){
e.printStackTrace();
}
diff --git a/images/smallArrowDown.png b/images/smallArrowDown.png
new file mode 100644
index 0000000..d163477
Binary files /dev/null and b/images/smallArrowDown.png differ
diff --git a/images/smallArrowLeft.png b/images/smallArrowLeft.png
new file mode 100644
index 0000000..5631115
Binary files /dev/null and b/images/smallArrowLeft.png differ
diff --git a/images/smallArrowRight.png b/images/smallArrowRight.png
new file mode 100644
index 0000000..0a0b897
Binary files /dev/null and b/images/smallArrowRight.png differ
diff --git a/images/smallArrowUp.png b/images/smallArrowUp.png
new file mode 100644
index 0000000..93a7b93
Binary files /dev/null and b/images/smallArrowUp.png differ
diff --git a/main.java b/main.java
index 7e6fcf9..1109c35 100644
--- a/main.java
+++ b/main.java
@@ -3,6 +3,7 @@
public class main {
public static void main(String[] args){
+ System.setProperty("sun.java2d.uiScale", "1.0");
new mainThread();
}
diff --git a/mysql-connector-java-5.1.47.jar b/mysql-connector-java-5.1.47.jar
deleted file mode 100644
index f3398ed..0000000
Binary files a/mysql-connector-java-5.1.47.jar and /dev/null differ
diff --git a/particles/explosion.java b/particles/explosion.java
index 7fc0b24..e099dab 100644
--- a/particles/explosion.java
+++ b/particles/explosion.java
@@ -105,6 +105,12 @@ public void updateAndDrawExplosionAura(){
short[] sprite = mainThread.textures[1].explosionAura[frameIndex];
float ratioX = size*4f/tempCentre.z;
float ratioY = size*3.6f/tempCentre.z;
+ //without a cap, the aura grows without bound as the camera gets close (tempCentre.z shrinks),
+ //which is what made it balloon across the screen instead of staying local to the blast
+ if(ratioX > 2.5f)
+ ratioX = 2.5f;
+ if(ratioY > 2.5f)
+ ratioY = 2.5f;
int xPos = (int)tempCentre.screenX;
int yPos = (int)tempCentre.screenY;
int originalWidth = 128;
@@ -113,9 +119,6 @@ public void updateAndDrawExplosionAura(){
xStart +=5;
yStart +=5;
- int depth;
-
- int[] zbuffer = postProcessingThread.currentZbuffer;
byte[] smoothedShadowBitmap = postProcessingThread.smoothedShadowBitmap;
//find the size ratio between a sprite pixel and screen pixel
@@ -140,8 +143,7 @@ public void updateAndDrawExplosionAura(){
for(int i = yTop, y = yStart; i < yBot; i++, y++){
if(i < 0 || i >=screen_height)
continue;
-
- depth = zTop + i*zDelta;
+
int ratioInverseY_Times_Y_Times_originalWidth = (int)(ratioInverseY*y)*originalWidth;
for(int j = xTop, x = xStart; j < xBot; j++, x++){
@@ -150,11 +152,7 @@ public void updateAndDrawExplosionAura(){
if(j < 0 || j >= screen_width)
continue;
screenIndex = j + i*screen_width;
-
-
- if(zbuffer[screenIndex] - depth > 30000)
- continue;
-
+
SpriteValue = sprite[((int)(ratioInverseX*x) + ratioInverseY_Times_Y_Times_originalWidth)& 0x3fff] ;
if(SpriteValue > smoothedShadowBitmap[screenIndex])
@@ -191,6 +189,10 @@ public void drawExplosionSprite(){
if(lifeTime <=16){
int[] sprite = mainThread.textures[spriteIndex].explosions[frameIndex];
float ratio = size*2/tempCentre.z;
+ //same unbounded-growth issue as the aura above - cap it so the fireball itself
+ //can't balloon across the screen when the camera is close to the blast
+ if(ratio > 2.5f)
+ ratio = 2.5f;
int xPos = (int)tempCentre.screenX;
int yPos = (int)tempCentre.screenY;
int width = 64;
diff --git a/run.bat b/run.bat
new file mode 100644
index 0000000..7577de5
--- /dev/null
+++ b/run.bat
@@ -0,0 +1,20 @@
+@echo off
+setlocal
+cd /d "%~dp0"
+
+echo Compiling...
+del /q sources.txt 2>nul
+(for /r %%f in (*.java) do @echo %%f) > sources.txt
+javac -d . @sources.txt
+if errorlevel 1 (
+ echo.
+ echo Compilation failed.
+ del /q sources.txt
+ pause
+ exit /b 1
+)
+del /q sources.txt
+
+echo Launching...
+start "" javaw -cp "." main
+exit