package modes; import gui.MPEndGameGui; import net.NetCommands; import net.BitStream.OutputBitStream; import net.NetPacket.GemPickupPacket; import net.NetPacket.GemSpawnPacket; import octree.IOctreeObject; import octree.IOctreeObject.RayIntersectionData; import h3d.col.Bounds; import octree.IOctreeElement; import shapes.GemBeam; import h3d.Quat; import h3d.Vector; import shapes.Gem; import mis.MisParser; import mis.MissionElement.MissionElementSimGroup; import octree.Octree; import mis.MissionElement.MissionElementTrigger; import mis.MissionElement.MissionElementType; import src.Mission; import src.Marble; import src.AudioManager; import src.ResourceLoader; import net.Net; import src.MarbleGame; import src.Util; @:structInit @:publicFields class GemSpawnPoint implements IOctreeObject { var gem:Gem; var gemBeam:GemBeam; var boundingBox:Bounds; var netIndex:Int; var priority:Int; public function new(vec:Vector, spawn:Gem, netIndex:Int) { boundingBox = new Bounds(); boundingBox.addPoint(vec.add(new Vector(-0.5, -0.5, -0.5)).toPoint()); boundingBox.addPoint(vec.add(new Vector(0.5, 0.5, 0.5)).toPoint()); this.gem = spawn; this.netIndex = netIndex; this.gem.netIndex = netIndex; } public function getElementType() { return 2; } public function setPriority(priority:Int) { this.priority = priority; } public function rayCast(rayOrigin:Vector, rayDirection:Vector, resultSet:Array, bestT:Float):Float { throw new haxe.exceptions.NotImplementedException(); // Not applicable } } class HuntMode extends NullMode { var playerSpawnPoints:Array = []; var spawnPointTaken = []; var gemOctree:Octree; var gemGroupRadius:Float; var maxGemsPerGroup:Int; var rng:RandomLCG = new RandomLCG(100); var rng2:RandomLCG = new RandomLCG(100); var gemSpawnPoints:Array; var lastSpawn:GemSpawnPoint; var activeGemSpawnGroup:Array; var gemBeams:Array = []; var gemToBeamMap:Map = []; var gemToBlackBeamMap:Map = []; var activeGems:Array = []; var points:Int = 0; var gemsCentroid:Vector; var idealSpawnIndex:Int; var expiredGems:Map = []; var competitiveTimerStartTicks:Int; override function missionScan(mission:Mission) { function scanMission(simGroup:MissionElementSimGroup) { var elToRemove = []; for (element in simGroup.elements) { if ([MissionElementType.Trigger].contains(element._type)) { var spawnSphere:MissionElementTrigger = cast element; var dbname = spawnSphere.datablock.toLowerCase(); if (dbname == "spawntrigger") { playerSpawnPoints.push(spawnSphere); spawnPointTaken.push(false); } } else if (element._type == MissionElementType.SimGroup) { var scanPls = true; if (Net.connectedServerInfo.oldSpawns) { if (element._name.toLowerCase() == "newversion") { // Remove this elToRemove.push(element); scanPls = false; } } else { if (element._name.toLowerCase() == "oldversion") { // Remove this elToRemove.push(element); scanPls = false; } } if (scanPls) scanMission(cast element); } } while (elToRemove.length > 0) { simGroup.elements.remove(elToRemove.pop()); } } scanMission(mission.root); }; override function getSpawnTransform() { var idx = Net.connectedServerInfo.competitiveMode ? idealSpawnIndex : Math.floor(rng2.randRange(0, playerSpawnPoints.length - 1)); if (!Net.connectedServerInfo.competitiveMode) { var allTaken = true; for (spw in spawnPointTaken) { if (!spw) { allTaken = false; break; } } if (!allTaken) { while (spawnPointTaken[idx]) { idx = Math.floor(rng2.randRange(0, playerSpawnPoints.length - 1)); } spawnPointTaken[idx] = true; } } var randomSpawn = playerSpawnPoints[idx]; var spawnPos = MisParser.parseVector3(randomSpawn.position); spawnPos.x *= -1; var spawnRot = MisParser.parseRotation(randomSpawn.rotation); spawnRot.x *= -1; spawnRot.w *= -1; var spawnMat = spawnRot.toMatrix(); var up = spawnMat.up(); if (MisParser.parseBoolean(randomSpawn.g)) up.load(up.multiply(-1)); spawnPos = spawnPos.add(up); // 1.5 -> 0.5 return { position: spawnPos, orientation: spawnRot, up: up } } public function freeSpawns() { for (i in 0...playerSpawnPoints.length) { spawnPointTaken[i] = false; } } override function getRespawnTransform(marble:Marble) { var lastContactPos = marble.lastContactPosition; if (lastContactPos == null) { var idx = Math.floor(rng2.randRange(0, playerSpawnPoints.length - 1)); var randomSpawn = playerSpawnPoints[idx]; var spawnPos = MisParser.parseVector3(randomSpawn.position); spawnPos.x *= -1; var spawnRot = MisParser.parseRotation(randomSpawn.rotation); spawnRot.x *= -1; spawnRot.w *= -1; var spawnMat = spawnRot.toMatrix(); var up = spawnMat.up(); if (MisParser.parseBoolean(randomSpawn.g)) up.load(up.multiply(-1)); spawnPos = spawnPos.add(up); // 1.5 -> 0.5 return { position: spawnPos, orientation: spawnRot, up: up } } // Pick closest spawn point var closestSpawn:MissionElementTrigger = null; var closestDistance = 1e10; for (spawn in playerSpawnPoints) { var pos = MisParser.parseVector3(spawn.position); pos.x *= -1; var dist = pos.distance(lastContactPos); if (dist < closestDistance) { closestDistance = dist; closestSpawn = spawn; } } if (closestSpawn != null) { var spawnPos = MisParser.parseVector3(closestSpawn.position); spawnPos.x *= -1; var spawnRot = MisParser.parseRotation(closestSpawn.rotation); spawnRot.x *= -1; spawnRot.w *= -1; var spawnMat = spawnRot.toMatrix(); var up = spawnMat.up(); if (MisParser.parseBoolean(closestSpawn.g)) up.load(up.multiply(-1)); spawnPos = spawnPos.add(up); // 1.5 -> 0.5 return { position: spawnPos, orientation: spawnRot, up: up } } return null; } function prepareGems() { if (this.gemSpawnPoints == null) { this.gemOctree = new Octree(); this.gemSpawnPoints = []; this.gemsCentroid = new Vector(); for (gem in this.level.gems) { var spawn:GemSpawnPoint = new GemSpawnPoint(gem.getAbsPos().getPosition(), gem, gemSpawnPoints.length); gem.setHide(true); gem.pickedUp = true; this.gemSpawnPoints.push(spawn); this.gemOctree.insert(spawn); gem.setHide(true); this.level.collisionWorld.removeEntity(gem.boundingCollider); // remove from octree to make it easy if (level.isMultiplayer) { @:privateAccess level.gemPredictions.alloc(); } gemsCentroid.load(gemsCentroid.add(gem.getAbsPos().getPosition())); } if (gemSpawnPoints.length > 0) gemsCentroid.load(gemsCentroid.multiply(1.0 / gemSpawnPoints.length)); var closestSpawnIndex = 0; var closestSpawnDistance = 1e8; for (i in 0...playerSpawnPoints.length) { var spawn = playerSpawnPoints[i]; var spawnPos = MisParser.parseVector3(spawn.position); spawnPos.x *= -1; if (spawnPos.distance(gemsCentroid) < closestSpawnDistance) { closestSpawnDistance = spawnPos.distance(gemsCentroid); closestSpawnIndex = i; } } idealSpawnIndex = closestSpawnIndex; } for (i in 0...spawnPointTaken.length) { spawnPointTaken[i] = false; } } override function getPreloadFiles() { return [ 'data/sound/opponentdiamond.wav', 'data/sound/firewrks.wav', 'data/shapes/items/blue.gem.png', 'data/shapes/items/red.gem.png', 'data/shapes/items/yellow.gem.png', 'data/shapes/items/platinum.gem.png' ]; } function setupGems() { hideExisting(); this.activeGems = []; this.activeGemSpawnGroup = []; this.rng.setSeed(cast Math.random() * 10000); this.rng2.setSeed(cast Math.random() * 10000); prepareGems(); spawnHuntGems(); } function spawnHuntGems(force:Bool = false) { if (activeGems.length != 0 && !force) return; var gemGroupRadius = 15.0; var maxGemsPerSpawn = 7; if (level.mission.missionInfo.maxgemsperspawn != null && level.mission.missionInfo.maxgemsperspawn != "") maxGemsPerSpawn = Std.parseInt(level.mission.missionInfo.maxgemsperspawn); if (level.mission.missionInfo.radiusfromgem != null && level.mission.missionInfo.radiusfromgem != "") gemGroupRadius = Std.parseFloat(level.mission.missionInfo.radiusfromgem); var spawnBlock = gemGroupRadius * 2; if (level.mission.missionInfo.spawnblock != null && level.mission.missionInfo.spawnblock != "") spawnBlock = Std.parseFloat(level.mission.missionInfo.spawnblock); var lastPos = null; if (lastSpawn != null) lastPos = lastSpawn.gem.getAbsPos().getPosition(); var furthestDist = 0.0; var furthest = null; for (i in 0...6) { var gem = gemSpawnPoints[Std.int(rng.randRange(0, gemSpawnPoints.length - 1))]; if (lastPos != null) { var dist = gem.gem.getAbsPos().getPosition().distance(lastPos); if (dist < spawnBlock) { if (dist > furthestDist) { furthestDist = dist; furthest = gem; } continue; } else { break; } } else { furthest = gem; break; } } if (furthest == null) { furthest = gemSpawnPoints[Std.int(rng.randRange(0, gemSpawnPoints.length - 1))]; } var pos = furthest.gem.getAbsPos().getPosition(); var results = []; while (results.length == 0) { var search = gemOctree.radiusSearch(pos, gemGroupRadius); for (elem in search) { var gemElem:GemSpawnPoint = cast elem; var gemPos = gemElem.gem.getAbsPos().getPosition(); if (level.mission.missionInfo.game == "PlatinumQuest") { if (Net.connectedServerInfo.oldSpawns) { // Spawn chances! var chance = switch (gemElem.gem.gemColor.toLowerCase()) { case "red.gem": level.mission.missionInfo.spawnchancered != null ? Std.parseFloat(level.mission.missionInfo.spawnchancered) : 0.9; case "yellow.gem": level.mission.missionInfo.spawnchanceyellow != null ? Std.parseFloat(level.mission.missionInfo.spawnchanceyellow) : 0.65; case "blue.gem": level.mission.missionInfo.spawnchanceblue != null ? Std.parseFloat(level.mission.missionInfo.spawnchanceblue) : 0.35; case "platinum.gem": level.mission.missionInfo.spawnchanceplatinum != null ? Std.parseFloat(level.mission.missionInfo.spawnchanceplatinum) : 0.18; default: 1.0; }; var choice = Math.random(); if (choice > chance) continue; // Don't spawn! } else { // Spawn chances! var chance = switch (gemElem.gem.gemColor.toLowerCase()) { case "red.gem": level.mission.missionInfo.redspawnchance != null ? Std.parseFloat(level.mission.missionInfo.redspawnchance) : 0.9; case "yellow.gem": level.mission.missionInfo.yellowspawnchance != null ? Std.parseFloat(level.mission.missionInfo.yellowspawnchance) : 0.65; case "blue.gem": level.mission.missionInfo.bluespawnchance != null ? Std.parseFloat(level.mission.missionInfo.bluespawnchance) : 0.35; case "platinum.gem": level.mission.missionInfo.platinumspawnchance != null ? Std.parseFloat(level.mission.missionInfo.platinumspawnchance) : 0.18; default: 1.0; }; var choice = Math.random(); if (choice > chance) continue; // Don't spawn! } } results.push({ gem: gemElem.netIndex, weight: this.gemGroupRadius - gemPos.distance(pos) + rng.randRange(0, getGemWeight(gemElem.gem) + 3) }); } } results.sort((a, b) -> { if (a.weight > b.weight) return -1; if (a.weight < b.weight) return 1; return 0; }); var spawnSet = results.slice(0, maxGemsPerSpawn).map(x -> x.gem); if (force) { for (activeGem in activeGemSpawnGroup) spawnSet.remove(activeGem); } for (gem in spawnSet) { spawnGem(gem); } if (!force) activeGemSpawnGroup = spawnSet; else { var uncollectedGems = []; for (g in activeGemSpawnGroup) { if (!gemSpawnPoints[g].gem.pickedUp) uncollectedGems.push(g); } activeGemSpawnGroup = uncollectedGems.concat(spawnSet); } if (level.isMultiplayer && Net.isHost) { var bs = new OutputBitStream(); bs.writeByte(GemSpawn); var packet = new GemSpawnPacket(); packet.gemIds = activeGemSpawnGroup; packet.expireds = []; for (i in 0...packet.gemIds.length) { if (expiredGems.exists(gemSpawnPoints[packet.gemIds[i]].gem)) { packet.expireds.push(true); } else { packet.expireds.push(false); } } packet.serialize(bs); Net.sendPacketToIngame(bs); } lastSpawn = furthest; } function spawnGem(spawn:Int, expired:Bool = false) { var gem = gemSpawnPoints[spawn]; gem.gem.setHide(false); gem.gem.pickedUp = false; this.level.collisionWorld.addEntity(gem.gem.boundingCollider); activeGems.push(gem.gem); if (!expired) { if (gem.gemBeam == null) { gem.gemBeam = new GemBeam(StringTools.replace(gem.gem.gemColor, '.gem', '')); var gemPos = gem.gem.getAbsPos().getPosition(); gem.gemBeam.setPosition(gemPos.x, gemPos.y, gemPos.z); gem.gemBeam.setRotationQuat(gem.gem.getRotationQuat().clone()); // gem.gemBeam.setOpacity(0.99); this.gemBeams.push(gem.gemBeam); this.gemToBeamMap.set(gem.gem, gem.gemBeam); level.addDtsObject(gem.gemBeam, () -> { // Please be fast lol }); } else { gem.gemBeam.setHide(false); } } else { if (gemToBlackBeamMap.exists(gem.gem)) { gemToBlackBeamMap.get(gem.gem).setHide(false); } else { var blackBeam = new GemBeam("black"); var pos = gem.gem.getAbsPos().getPosition(); blackBeam.setPosition(gem.gem.x, gem.gem.y, gem.gem.z); blackBeam.setRotationQuat(gem.gem.getRotationQuat().clone()); blackBeam.setHide(false); level.addDtsObject(blackBeam, () -> {}); gemToBlackBeamMap.set(gem.gem, blackBeam); } } } public inline function setGemHiddenStatus(gemId:Int, status:Bool) { var gemSpawn = gemSpawnPoints[gemId]; if (gemSpawn.gem != null) { gemSpawn.gem.pickedUp = status; gemSpawn.gem.setHide(status); if (expiredGems.exists(gemSpawn.gem)) { var blackBeam = gemToBlackBeamMap.get(gemSpawn.gem); blackBeam.setHide(status); gemSpawn.gemBeam.setHide(true); } else { gemSpawn.gemBeam.setHide(status); } if (status) this.activeGems.push(gemSpawn.gem); else this.activeGems.remove(gemSpawn.gem); } else { throw new haxe.Exception("Setting gem status for non existent gem!"); } } public function setActiveSpawnSphere(gems:Array, expireds:Array) { hideExisting(); expiredGems = []; for (i in 0...gems.length) { var gem = gems[i]; spawnGem(gem, expireds[i]); if (expireds[i]) { expiredGems.set(gemSpawnPoints[gem].gem, true); } } activeGemSpawnGroup = gems; } function getGemWeight(gem:Gem) { var col = gem.gemColor.toLowerCase(); if (col == "red.gem") return 0; if (col == "yellow.gem") return 1; if (col == "blue.gem") return 4; if (col == "platinum.gem") return 9; return 0; } function hideExisting() { lastSpawn = null; if (gemSpawnPoints != null) { for (gs in gemSpawnPoints) { gs.gem.setHide(true); gs.gem.pickedUp = true; if (gs.gemBeam != null) { gs.gemBeam.setHide(true); } if (gemToBlackBeamMap.exists(gs.gem)) { gemToBlackBeamMap.get(gs.gem).setHide(true); } } } } override public function getStartTime() { return level.mission.qualifyTime; } override function onRestart() { setupGems(); points = 0; competitiveTimerStartTicks = 0; @:privateAccess level.playGui.formatGemHuntCounter(points); } override function onMissionLoad() { prepareGems(); competitiveTimerStartTicks = 0; } override function onClientRestart() { prepareGems(); competitiveTimerStartTicks = 0; } override function onTimeExpire() { if (level.finishTime != null) return; AudioManager.playSound(ResourceLoader.getResource("data/sound/firewrks.wav", ResourceLoader.getAudio, @:privateAccess level.soundResources)); // AudioManager.playSound(ResourceLoader.getResource('data/sound/finish.wav', ResourceLoader.getAudio, @:privateAccess level.soundResources)); level.finishTime = level.timeState.clone(); level.marble.setMode(Finish); level.marble.camera.finish = true; level.finishYaw = level.marble.camera.CameraYaw; level.finishPitch = level.marble.camera.CameraPitch; // if (level.isMultiplayer) { // @:privateAccess level.playGui.doMPEndGameMessage(); // } else { // level.displayAlert("Congratulations! You've finished!"); // } level.cancel(@:privateAccess level.oobSchedule); level.cancel(@:privateAccess level.marble.oobSchedule); for (marble in level.marbles) { marble.setMode(Finish); level.cancel(@:privateAccess marble.oobSchedule); } if (Net.isHost) NetCommands.timerRanOut(); // Stop the ongoing sounds if (@:privateAccess level.timeTravelSound != null) { @:privateAccess level.timeTravelSound.stop(); @:privateAccess level.timeTravelSound = null; } if (@:privateAccess level.alarmSound != null) { @:privateAccess level.alarmSound.stop(); @:privateAccess level.alarmSound = null; } level.schedule(level.timeState.currentAttemptTime + 2, () -> { if (Util.isTouchDevice()) { MarbleGame.instance.touchInput.setControlsEnabled(false); } #if js var pointercontainer = js.Browser.document.querySelector("#pointercontainer"); pointercontainer.hidden = false; #end MarbleGame.canvas.pushDialog(new MPEndGameGui()); level.setCursorLock(false); return 0; }); } override function onGemPickup(marble:Marble, gem:Gem) { if ((@:privateAccess !marble.isNetUpdate && Net.isHost) || !Net.isMP) { if (marble == level.marble) AudioManager.playSound(ResourceLoader.getResource('data/sound/gotgem.wav', ResourceLoader.getAudio, @:privateAccess this.level.soundResources)); else AudioManager.playSound(ResourceLoader.getResource('data/sound/opponentdiamond.wav', ResourceLoader.getAudio, @:privateAccess this.level.soundResources)); } activeGems.remove(gem); var wasExpiredGem = false; if (expiredGems.exists(gem)) { wasExpiredGem = true; } if (gemToBlackBeamMap.exists(gem)) { gemToBlackBeamMap.get(gem).setHide(true); } var beam = gemToBeamMap.get(gem); beam.setHide(true); var incr = 0; switch (gem.gemColor.toLowerCase()) { case "red.gem": incr = 1; case "yellow.gem": incr = 2; case "blue.gem": incr = 5; case "platinum.gem": incr = 10; } if (@:privateAccess !marble.isNetUpdate) { if (marble == level.marble) { switch (gem.gemColor.toLowerCase()) { case "red.gem": points += 1; @:privateAccess level.playGui.addMiddleMessage('+1', 0xFF6666); case "yellow.gem": points += 2; @:privateAccess level.playGui.addMiddleMessage('+2', 0xFFFF66); case "blue.gem": points += 5; @:privateAccess level.playGui.addMiddleMessage('+5', 0x6666FF); case "platinum.gem": points += 10; @:privateAccess level.playGui.addMiddleMessage('+10', 0xdddddd); } if (Net.isHost) @:privateAccess level.playGui.formatGemHuntCounter(points); } } if (this.level.isMultiplayer && Net.isHost) { if (Net.connectedServerInfo.competitiveMode && !wasExpiredGem) { if (competitiveTimerStartTicks == 0) { NetCommands.setCompetitiveTimerStartTicks(this.level.timeState.ticks); } var remaining = 0; for (g in activeGems) if (!expiredGems.exists(g)) remaining++; if (remaining == 3) { var currentTime = level.timeState.ticks; var endTime = competitiveTimerStartTicks + (20000 >> 5); var remainingTicks = (endTime - currentTime); if (remainingTicks > (15000 >> 5)) { NetCommands.setCompetitiveTimerStartTicks(currentTime - (5000 >> 5)); } } if (remaining == 2) { var currentTime = level.timeState.ticks; var endTime = competitiveTimerStartTicks + (20000 >> 5); var remainingTicks = (endTime - currentTime); if (remainingTicks > (10000 >> 5)) { NetCommands.setCompetitiveTimerStartTicks(currentTime - (10000 >> 5)); } } if (remaining == 1) { var currentTime = level.timeState.ticks; var endTime = competitiveTimerStartTicks + (20000 >> 5); var remainingTicks = (endTime - currentTime); if (remainingTicks > (5000 >> 5)) { NetCommands.setCompetitiveTimerStartTicks(currentTime - (15000 >> 5)); } } if (remaining == 0) { NetCommands.setCompetitiveTimerStartTicks(0); spawnNextGemCluster(); } } var packet = new GemPickupPacket(); packet.clientId = @:privateAccess marble.connection == null ? 0 : @:privateAccess marble.connection.id; packet.gemId = gem.netIndex; packet.serverTicks = level.timeState.ticks; packet.scoreIncr = incr; var os = new OutputBitStream(); os.writeByte(GemPickup); packet.serialize(os); Net.sendPacketToIngame(os); // Settings.playStatistics.totalMPScore += incr; @:privateAccess level.playGui.incrementPlayerScore(packet.clientId, packet.scoreIncr); } if (wasExpiredGem) expiredGems.remove(gem); if (this.level.isMultiplayer && Net.isClient) { gem.pickUpClient = @:privateAccess marble.connection == null ? Net.clientId : @:privateAccess marble.connection.id; } if (!this.level.isMultiplayer || Net.isHost) { spawnHuntGems(); } } public function setCompetitiveTimerStartTicks(ticks:Int) { competitiveTimerStartTicks = ticks; } function spawnNextGemCluster() { // Expire all existing for (g in activeGems) { expiredGems.set(g, true); var gemBeam = gemToBeamMap.get(g); gemBeam.setHide(true); if (gemToBlackBeamMap.exists(g)) { gemToBlackBeamMap.get(g).setHide(false); } else { var blackBeam = new GemBeam("black"); var pos = g.getAbsPos().getPosition(); blackBeam.setPosition(g.x, g.y, g.z); blackBeam.setRotationQuat(g.getRotationQuat().clone()); blackBeam.setHide(false); level.addDtsObject(blackBeam, () -> {}); gemToBlackBeamMap.set(g, blackBeam); } } spawnHuntGems(true); } override function update(t:src.TimeState) { if (Net.connectedServerInfo.competitiveMode) { if (competitiveTimerStartTicks != 0) { var currentTime = Net.isHost ? t.ticks : @:privateAccess level.marble.serverTicks; var endTime = competitiveTimerStartTicks + (20000 >> 5); @:privateAccess level.playGui.formatCountdownTimer(Math.max(0, (endTime - currentTime) * 0.032), 0); if (Net.isHost && endTime < currentTime) { spawnNextGemCluster(); NetCommands.setCompetitiveTimerStartTicks(0); } } else { @:privateAccess level.playGui.formatCountdownTimer(0, 0); } } } override public function timeMultiplier() { return -1; } }