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