diff --git a/data/ui/mp/lb_chkbx_d.png b/data/ui/mp/lb_chkbx_d.png new file mode 100644 index 00000000..51ed12d3 Binary files /dev/null and b/data/ui/mp/lb_chkbx_d.png differ diff --git a/data/ui/mp/lb_chkbx_di_h.png b/data/ui/mp/lb_chkbx_di_h.png new file mode 100644 index 00000000..3613cef9 Binary files /dev/null and b/data/ui/mp/lb_chkbx_di_h.png differ diff --git a/data/ui/mp/lb_chkbx_di_i.png b/data/ui/mp/lb_chkbx_di_i.png new file mode 100644 index 00000000..3613cef9 Binary files /dev/null and b/data/ui/mp/lb_chkbx_di_i.png differ diff --git a/data/ui/mp/lb_chkbx_di_n.png b/data/ui/mp/lb_chkbx_di_n.png new file mode 100644 index 00000000..3613cef9 Binary files /dev/null and b/data/ui/mp/lb_chkbx_di_n.png differ diff --git a/data/ui/mp/lb_chkbx_h.png b/data/ui/mp/lb_chkbx_h.png new file mode 100644 index 00000000..ac43e81a Binary files /dev/null and b/data/ui/mp/lb_chkbx_h.png differ diff --git a/data/ui/mp/lb_chkbx_i.png b/data/ui/mp/lb_chkbx_i.png new file mode 100644 index 00000000..9f8342ca Binary files /dev/null and b/data/ui/mp/lb_chkbx_i.png differ diff --git a/data/ui/mp/lb_chkbx_n.png b/data/ui/mp/lb_chkbx_n.png new file mode 100644 index 00000000..e4d78667 Binary files /dev/null and b/data/ui/mp/lb_chkbx_n.png differ diff --git a/data/ui/mp/play/nextcat_d.png b/data/ui/mp/play/nextcat_d.png new file mode 100644 index 00000000..05a0710a Binary files /dev/null and b/data/ui/mp/play/nextcat_d.png differ diff --git a/data/ui/mp/play/nextcat_h.png b/data/ui/mp/play/nextcat_h.png new file mode 100644 index 00000000..87883e7a Binary files /dev/null and b/data/ui/mp/play/nextcat_h.png differ diff --git a/data/ui/mp/play/nextcat_i.png b/data/ui/mp/play/nextcat_i.png new file mode 100644 index 00000000..c4d121ff Binary files /dev/null and b/data/ui/mp/play/nextcat_i.png differ diff --git a/data/ui/mp/play/nextcat_n.png b/data/ui/mp/play/nextcat_n.png new file mode 100644 index 00000000..0a41972f Binary files /dev/null and b/data/ui/mp/play/nextcat_n.png differ diff --git a/data/ui/mp/play/prev_n.png b/data/ui/mp/play/prev_n.png index c44adb23..3654e286 100644 Binary files a/data/ui/mp/play/prev_n.png and b/data/ui/mp/play/prev_n.png differ diff --git a/data/ui/mp/play/prevcat_d.png b/data/ui/mp/play/prevcat_d.png new file mode 100644 index 00000000..89df61cb Binary files /dev/null and b/data/ui/mp/play/prevcat_d.png differ diff --git a/data/ui/mp/play/prevcat_h.png b/data/ui/mp/play/prevcat_h.png new file mode 100644 index 00000000..daed9300 Binary files /dev/null and b/data/ui/mp/play/prevcat_h.png differ diff --git a/data/ui/mp/play/prevcat_i.png b/data/ui/mp/play/prevcat_i.png new file mode 100644 index 00000000..9158391b Binary files /dev/null and b/data/ui/mp/play/prevcat_i.png differ diff --git a/data/ui/mp/play/prevcat_n.png b/data/ui/mp/play/prevcat_n.png new file mode 100644 index 00000000..2e8583e6 Binary files /dev/null and b/data/ui/mp/play/prevcat_n.png differ diff --git a/data/ui/mp/settings/inputbg.png b/data/ui/mp/settings/inputbg.png new file mode 100644 index 00000000..56ca6f20 Binary files /dev/null and b/data/ui/mp/settings/inputbg.png differ diff --git a/src/MarbleGame.hx b/src/MarbleGame.hx index 9063b119..1d53760e 100644 --- a/src/MarbleGame.hx +++ b/src/MarbleGame.hx @@ -1,5 +1,7 @@ package src; +import gui.MPExitGameDlg; +import gui.GuiControl; import gui.MPPlayMissionGui; import gui.MainMenuGui; #if !js @@ -47,7 +49,7 @@ class MarbleGame { var toRecord:Bool = false; var recordingName:String; - var exitGameDlg:ExitGameDlg; + var exitGameDlg:GuiControl; var touchInput:TouchInput; @@ -234,31 +236,46 @@ class MarbleGame { if (paused && world._ready) { Console.log("Game paused"); world.setCursorLock(false); - exitGameDlg = new ExitGameDlg((sender) -> { - canvas.popDialog(exitGameDlg); - var w = getWorld(); - if (w.isRecording) { - MarbleGame.canvas.pushDialog(new ReplayNameDlg(() -> { - quitMission(); - })); - } else { + if (world.isMultiplayer) { + exitGameDlg = new MPExitGameDlg(() -> { + canvas.popDialog(exitGameDlg); + paused = !paused; + var w = getWorld(); + w.setCursorLock(true); + }, () -> { + canvas.popDialog(exitGameDlg); quitMission(Net.isClient); if (Net.isMP && Net.isClient) { Net.disconnect(); } - } - }, (sender) -> { - canvas.popDialog(exitGameDlg); - paused = !paused; - var w = getWorld(); - w.setCursorLock(true); - }, (sender) -> { - canvas.popDialog(exitGameDlg); - var w = getWorld(); - w.restart(w.marble, true); - // world.setCursorLock(true); - paused = !paused; - }); + }); + } else { + exitGameDlg = new ExitGameDlg((sender) -> { + canvas.popDialog(exitGameDlg); + var w = getWorld(); + if (w.isRecording) { + MarbleGame.canvas.pushDialog(new ReplayNameDlg(() -> { + quitMission(); + })); + } else { + quitMission(Net.isClient); + if (Net.isMP && Net.isClient) { + Net.disconnect(); + } + } + }, (sender) -> { + canvas.popDialog(exitGameDlg); + paused = !paused; + var w = getWorld(); + w.setCursorLock(true); + }, (sender) -> { + canvas.popDialog(exitGameDlg); + var w = getWorld(); + w.restart(w.marble, true); + // world.setCursorLock(true); + paused = !paused; + }); + } canvas.pushDialog(exitGameDlg); } else { if (world._ready) { diff --git a/src/gui/JoinServerGui.hx b/src/gui/JoinServerGui.hx index 407adb0c..1b6ecb48 100644 --- a/src/gui/JoinServerGui.hx +++ b/src/gui/JoinServerGui.hx @@ -150,6 +150,9 @@ class JoinServerGui extends GuiImage { var serverSettingsBtn = new GuiButton(loadButtonImages("data/ui/mp/play/settings")); serverSettingsBtn.position = new Vector(171, 379); serverSettingsBtn.extent = new Vector(45, 45); + serverSettingsBtn.pressedAction = (e) -> { + MarbleGame.canvas.pushDialog(new MPServerDlg()); + } window.addChild(serverSettingsBtn); var exitBtn = new GuiButton(loadButtonImages("data/ui/mp/join/leave")); diff --git a/src/gui/MPExitGameDlg.hx b/src/gui/MPExitGameDlg.hx new file mode 100644 index 00000000..9f6a1964 --- /dev/null +++ b/src/gui/MPExitGameDlg.hx @@ -0,0 +1,127 @@ +package gui; + +import h2d.filter.DropShadow; +import net.Net; +import src.MarbleGame; +import hxd.res.BitmapFont; +import h3d.Vector; +import src.ResourceLoader; +import src.Settings; + +class MPExitGameDlg extends GuiControl { + public function new(resumeFunc:() -> Void, exitFunc:() -> Void) { + super(); + + this.horizSizing = Width; + this.vertSizing = Height; + this.position = new Vector(); + this.extent = new Vector(640, 480); + + function loadButtonImages(path:String) { + var normal = ResourceLoader.getResource('${path}_n.png', ResourceLoader.getImage, this.imageResources).toTile(); + var hover = ResourceLoader.getResource('${path}_h.png', ResourceLoader.getImage, this.imageResources).toTile(); + var pressed = ResourceLoader.getResource('${path}_d.png', ResourceLoader.getImage, this.imageResources).toTile(); + var disabled = ResourceLoader.getResource('${path}_i.png', ResourceLoader.getImage, this.imageResources).toTile(); + return [normal, hover, pressed, disabled]; + } + + var dialogImg = new GuiImage(ResourceLoader.getResource("data/ui/mp/team/teamjoin.png", ResourceLoader.getImage, this.imageResources).toTile()); + dialogImg.horizSizing = Center; + dialogImg.vertSizing = Center; + dialogImg.position = new Vector(146, 115); + dialogImg.extent = new Vector(347, 250); + this.addChild(dialogImg); + + var partialRestart = new GuiButton(loadButtonImages("data/ui/mp/exit/partial")); + partialRestart.position = new Vector(133, 80); + partialRestart.extent = new Vector(94, 45); + partialRestart.vertSizing = Top; + dialogImg.addChild(partialRestart); + if (!Net.isHost) { + partialRestart.disabled = true; + } + + var disconnectBtn = new GuiButton(Net.isHost ? loadButtonImages("data/ui/mp/exit/levelselect") : loadButtonImages("data/ui/mp/exit/disconnect")); + disconnectBtn.position = new Vector(22, 132); + disconnectBtn.extent = new Vector(114, 45); + disconnectBtn.vertSizing = Top; + disconnectBtn.pressedAction = (e) -> exitFunc(); + dialogImg.addChild(disconnectBtn); + + var resumeBtn = new GuiButton(loadButtonImages("data/ui/mp/exit/resume")); + resumeBtn.position = new Vector(133, 132); + resumeBtn.extent = new Vector(94, 45); + resumeBtn.vertSizing = Top; + resumeBtn.pressedAction = (e) -> resumeFunc(); + dialogImg.addChild(resumeBtn); + + var serverSettingsBtn = new GuiButton(loadButtonImages("data/ui/mp/play/settings")); + serverSettingsBtn.position = new Vector(195, 184); + serverSettingsBtn.extent = new Vector(45, 45); + serverSettingsBtn.vertSizing = Top; + serverSettingsBtn.pressedAction = (e) -> { + MarbleGame.canvas.pushDialog(new MPServerDlg()); + }; + dialogImg.addChild(serverSettingsBtn); + if (!Net.isHost) { + serverSettingsBtn.disabled = true; + } + + var kickBtn = new GuiButton(loadButtonImages("data/ui/mp/play/kick")); + kickBtn.position = new Vector(108, 184); + kickBtn.extent = new Vector(45, 45); + kickBtn.vertSizing = Top; + dialogImg.addChild(kickBtn); + if (!Net.isHost) { + kickBtn.disabled = true; + } + + var quickspawnBtn = new GuiButton(loadButtonImages("data/ui/mp/exit/respawn")); + quickspawnBtn.position = new Vector(224, 132); + quickspawnBtn.extent = new Vector(104, 45); + quickspawnBtn.vertSizing = Top; + dialogImg.addChild(quickspawnBtn); + + var completeRestart = new GuiButton(loadButtonImages("data/ui/mp/exit/complete")); + completeRestart.position = new Vector(224, 80); + completeRestart.extent = new Vector(104, 45); + completeRestart.vertSizing = Top; + dialogImg.addChild(completeRestart); + if (!Net.isHost) { + completeRestart.disabled = true; + } + + var markerFelt32fontdata = ResourceLoader.getFileEntry("data/font/MarkerFelt.fnt"); + var markerFelt32b = new BitmapFont(markerFelt32fontdata.entry); + @:privateAccess markerFelt32b.loader = ResourceLoader.loader; + var markerFelt32 = markerFelt32b.toSdfFont(cast 26 * Settings.uiScale, MultiChannel); + var markerFelt38 = markerFelt32b.toSdfFont(cast 31 * Settings.uiScale, MultiChannel); + + var exitTitle = new GuiText(markerFelt38); + exitTitle.position = new Vector(8, 28); + exitTitle.extent = new Vector(331, 30); + exitTitle.justify = Center; + exitTitle.text.text = "Ingame Options"; + exitTitle.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + dialogImg.addChild(exitTitle); + + var restartTitle = new GuiText(markerFelt32); + restartTitle.position = new Vector(20, 88); + restartTitle.extent = new Vector(114, 14); + restartTitle.justify = Center; + restartTitle.text.text = "Restart:"; + restartTitle.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + dialogImg.addChild(restartTitle); + + var jukeboxButton = new GuiButton(loadButtonImages("data/ui/jukebox/jb_pausemenu")); + jukeboxButton.vertSizing = Top; + jukeboxButton.horizSizing = Left; + jukeboxButton.position = new Vector(439, 403); + jukeboxButton.extent = new Vector(187, 65); + jukeboxButton.pressedAction = (e) -> { + MarbleGame.canvas.pushDialog(new JukeboxDlg()); + } + + this.addChild(jukeboxButton); + } +} diff --git a/src/gui/MPMarbleSelectGui.hx b/src/gui/MPMarbleSelectGui.hx new file mode 100644 index 00000000..11bdd661 --- /dev/null +++ b/src/gui/MPMarbleSelectGui.hx @@ -0,0 +1,214 @@ +package gui; + +import h2d.filter.DropShadow; +import hxd.res.BitmapFont; +import h3d.prim.Polygon; +import h3d.scene.Mesh; +import h3d.shader.AlphaChannel; +import src.MarbleGame; +import h3d.Vector; +import src.ResourceLoader; +import src.DtsObject; +import src.Settings; +import src.ResourceLoaderWorker; + +class MPMarbleSelectGui extends GuiImage { + public function new() { + var img = ResourceLoader.getImage("data/ui/mp/team/teamcreate.png"); + super(img.resource.toTile()); + this.horizSizing = Center; + this.vertSizing = Center; + this.position = new Vector(73, -59); + this.extent = new Vector(493, 361); + + var categoryNames = ["Official Marbles", "MBUltra"]; + + var curSelection:Int = Settings.optionsSettings.marbleIndex; + var curCategorySelection:Int = Settings.optionsSettings.marbleCategoryIndex; + + function loadButtonImages(path:String) { + var normal = ResourceLoader.getResource('${path}_n.png', ResourceLoader.getImage, this.imageResources).toTile(); + var hover = ResourceLoader.getResource('${path}_h.png', ResourceLoader.getImage, this.imageResources).toTile(); + var pressed = ResourceLoader.getResource('${path}_d.png', ResourceLoader.getImage, this.imageResources).toTile(); + var disabled = ResourceLoader.getResource('${path}_i.png', ResourceLoader.getImage, this.imageResources).toTile(); + return [normal, hover, pressed, disabled]; + } + + var markerFelt32fontdata = ResourceLoader.getFileEntry("data/font/MarkerFelt.fnt"); + var markerFelt32b = new BitmapFont(markerFelt32fontdata.entry); + @:privateAccess markerFelt32b.loader = ResourceLoader.loader; + var markerFelt32 = markerFelt32b.toSdfFont(cast 26 * Settings.uiScale, MultiChannel); + var markerFelt24 = markerFelt32b.toSdfFont(cast 18 * Settings.uiScale, MultiChannel); + var markerFelt28 = markerFelt32b.toSdfFont(cast 26 * Settings.uiScale, MultiChannel); + + var selectBtn = new GuiButton(loadButtonImages("data/ui/mp/play/choose")); + selectBtn.horizSizing = Center; + selectBtn.vertSizing = Top; + selectBtn.position = new Vector(199, 270); + selectBtn.extent = new Vector(95, 45); + selectBtn.pressedAction = (e) -> { + Settings.optionsSettings.marbleIndex = curSelection; + Settings.optionsSettings.marbleCategoryIndex = curCategorySelection; + Settings.optionsSettings.marbleSkin = MarbleSelectGui.marbleData[curCategorySelection][curSelection].skin; + Settings.optionsSettings.marbleModel = MarbleSelectGui.marbleData[curCategorySelection][curSelection].dts; + Settings.optionsSettings.marbleShader = MarbleSelectGui.marbleData[curCategorySelection][curSelection].shader; + Settings.save(); + MarbleGame.canvas.popDialog(this); + } + this.addChild(selectBtn); + + var marbleShow = buildObjectShow(MarbleSelectGui.marbleData[curCategorySelection][curSelection].dts, new Vector(171, 97), new Vector(150, 150), 2.6, + 0, [ + "base.marble" => MarbleSelectGui.marbleData[curCategorySelection][curSelection].skin + ".marble" + ]); + marbleShow.horizSizing = Center; + marbleShow.vertSizing = Bottom; + marbleShow.visible = true; + this.addChild(marbleShow); + + var titleText = new GuiMLText(markerFelt28, null); + titleText.text.textColor = 0xFFFFFF; + titleText.text.filter = new DropShadow(1.414, 0.785, 0, 1, 0x0000007F, 0.4, 1, true); + titleText.horizSizing = Center; + titleText.vertSizing = Bottom; + titleText.position = new Vector(140, 67); + titleText.extent = new Vector(213, 27); + titleText.text.text = '

${categoryNames[curCategorySelection]}

'; + this.addChild(titleText); + + var marbleText = new GuiMLText(markerFelt24, null); + marbleText.text.textColor = 0xFFFFFF; + marbleText.text.filter = new DropShadow(1.414, 0.785, 0, 1, 0x0000007F, 0.4, 1, true); + marbleText.horizSizing = Center; + marbleText.vertSizing = Bottom; + marbleText.position = new Vector(86, 243); + marbleText.extent = new Vector(320, 22); + marbleText.text.text = '

${MarbleSelectGui.marbleData[curCategorySelection][curSelection].name}

'; + this.addChild(marbleText); + + var changeMarbleText = new GuiImage(ResourceLoader.getResource("data/ui/play/change_marble_text.png", ResourceLoader.getImage, this.imageResources) + .toTile()); + changeMarbleText.horizSizing = Center; + changeMarbleText.position = new Vector(96, 26); + changeMarbleText.extent = new Vector(300, 39); + this.addChild(changeMarbleText); + + function setMarbleSelection(idx:Int, categoryIdx:Int) { + if (categoryIdx < 0) + categoryIdx = MarbleSelectGui.marbleData.length + categoryIdx; + if (categoryIdx >= MarbleSelectGui.marbleData.length) + categoryIdx -= MarbleSelectGui.marbleData.length; + + if (idx < 0) + idx = MarbleSelectGui.marbleData[categoryIdx].length + idx; + if (idx >= MarbleSelectGui.marbleData[categoryIdx].length) + idx -= MarbleSelectGui.marbleData[categoryIdx].length; + curSelection = idx; + curCategorySelection = categoryIdx; + var marble = MarbleSelectGui.marbleData[categoryIdx][idx]; + + titleText.text.text = '

${categoryNames[curCategorySelection]}

'; + marbleText.text.text = '

${marble.name}

'; + + var dtsObj = new DtsObject(); + dtsObj.dtsPath = marble.dts; + dtsObj.ambientRotate = true; + dtsObj.ambientSpinFactor /= -2; + dtsObj.showSequences = false; + dtsObj.useInstancing = false; + dtsObj.matNameOverride.set("base.marble", marble.skin + ".marble"); + + ResourceLoader.load(dtsObj.dtsPath).entry.load(() -> { + var dtsFile = ResourceLoader.loadDts(dtsObj.dtsPath); + var directoryPath = haxe.io.Path.directory(dtsObj.dtsPath); + var texToLoad = []; + for (i in 0...dtsFile.resource.matNames.length) { + var matName = dtsObj.matNameOverride.exists(dtsFile.resource.matNames[i]) ? dtsObj.matNameOverride.get(dtsFile.resource.matNames[i]) : dtsFile.resource.matNames[i]; + var fullNames = ResourceLoader.getFullNamesOf(directoryPath + '/' + matName).filter(x -> haxe.io.Path.extension(x) != "dts"); + var fullName = fullNames.length > 0 ? fullNames[0] : null; + if (fullName != null) { + texToLoad.push(fullName); + } + } + + var worker = new ResourceLoaderWorker(() -> { + dtsObj.init(null, () -> {}); // The lambda is not gonna run async anyway + for (mat in dtsObj.materials) { + mat.mainPass.enableLights = false; + mat.mainPass.culling = None; + if (mat.blendMode != Alpha && mat.blendMode != Add) + mat.mainPass.addShader(new AlphaChannel()); + } + marbleShow.changeObject(dtsObj); + }); + + for (texPath in texToLoad) { + worker.loadFile(texPath); + } + worker.run(); + }); + } + + var nextBtn = new GuiButton(loadButtonImages("data/ui/mp/play/next")); + nextBtn.position = new Vector(296, 270); + nextBtn.extent = new Vector(75, 45); + nextBtn.pressedAction = (e) -> { + setMarbleSelection(curSelection + 1, curCategorySelection); + } + this.addChild(nextBtn); + + var prevBtn = new GuiButton(loadButtonImages("data/ui/mp/play/prev")); + prevBtn.position = new Vector(123, 270); + prevBtn.extent = new Vector(75, 45); + prevBtn.pressedAction = (e) -> { + setMarbleSelection(curSelection - 1, curCategorySelection); + } + + var nextCategoryBtn = new GuiButton(loadButtonImages("data/ui/mp/play/nextcat")); + nextCategoryBtn.position = new Vector(371, 270); + nextCategoryBtn.extent = new Vector(85, 45); + nextCategoryBtn.pressedAction = (e) -> { + setMarbleSelection(0, curCategorySelection + 1); + } + this.addChild(nextCategoryBtn); + + var prevCategoryBtn = new GuiButton(loadButtonImages("data/ui/mp/play/prevcat")); + prevCategoryBtn.position = new Vector(37, 270); + prevCategoryBtn.extent = new Vector(85, 45); + prevCategoryBtn.pressedAction = (e) -> { + setMarbleSelection(0, curCategorySelection - 1); + } + this.addChild(prevCategoryBtn); + + setMarbleSelection(curSelection, curCategorySelection); + this.addChild(prevBtn); + } + + function buildObjectShow(dtsPath:String, position:Vector, extent:Vector, dist:Float = 5, pitch:Float = 0, matnameOverride:Map = null) { + var oShow = new GuiObjectShow(); + var dtsObj = new DtsObject(); + dtsObj.dtsPath = dtsPath; + dtsObj.ambientRotate = true; + dtsObj.ambientSpinFactor /= -2; + dtsObj.showSequences = false; + dtsObj.useInstancing = false; + if (matnameOverride != null) { + for (key => value in matnameOverride) { + dtsObj.matNameOverride.set(key, value); + } + } + dtsObj.init(null, () -> {}); // The lambda is not gonna run async anyway + for (mat in dtsObj.materials) { + mat.mainPass.enableLights = false; + mat.mainPass.culling = None; + if (mat.blendMode != Alpha && mat.blendMode != Add) + mat.mainPass.addShader(new AlphaChannel()); + } + oShow.sceneObject = dtsObj; + oShow.position = position; + oShow.extent = extent; + oShow.renderDistance = dist; + oShow.renderPitch = pitch; + return oShow; + } +} diff --git a/src/gui/MPPlayMissionGui.hx b/src/gui/MPPlayMissionGui.hx index be992e0a..e28fd217 100644 --- a/src/gui/MPPlayMissionGui.hx +++ b/src/gui/MPPlayMissionGui.hx @@ -155,6 +155,9 @@ class MPPlayMissionGui extends GuiImage { var searchBtn = new GuiButton(loadButtonImages("data/ui/mp/play/search")); searchBtn.position = new Vector(255, 514); searchBtn.extent = new Vector(44, 44); + searchBtn.pressedAction = (e) -> { + MarbleGame.canvas.pushDialog(new MPSearchGui(false)); + } window.addChild(searchBtn); var kickBtn = new GuiButton(loadButtonImages("data/ui/mp/play/kick")); @@ -165,11 +168,17 @@ class MPPlayMissionGui extends GuiImage { var serverSettingsBtn = new GuiButton(loadButtonImages("data/ui/mp/play/settings")); serverSettingsBtn.position = new Vector(157, 514); serverSettingsBtn.extent = new Vector(44, 44); + serverSettingsBtn.pressedAction = (e) -> { + MarbleGame.canvas.pushDialog(new MPServerDlg()); + } window.addChild(serverSettingsBtn); var marbleSelectBtn = new GuiButton(loadButtonImages("data/ui/mp/play/marble")); marbleSelectBtn.position = new Vector(206, 514); marbleSelectBtn.extent = new Vector(44, 44); + marbleSelectBtn.pressedAction = (e) -> { + MarbleGame.canvas.pushDialog(new MPMarbleSelectGui()); + } window.addChild(marbleSelectBtn); var temprev = new BitmapData(1, 1); @@ -200,9 +209,10 @@ class MPPlayMissionGui extends GuiImage { var difficultySelector = new GuiButton(loadButtonImages("data/ui/mp/play/difficulty_beginner")); difficultySelector.position = new Vector(161, 47); difficultySelector.extent = new Vector(204, 44); - difficultySelector.pressedAction = (e) -> { - MarbleGame.canvas.pushDialog(difficultyPopover); - }; + if (isHost) + difficultySelector.pressedAction = (e) -> { + MarbleGame.canvas.pushDialog(difficultyPopover); + }; window.addChild(difficultySelector); var difficultyCloseButton = new GuiButton(loadButtonImages("data/ui/mp/play/difficultymenu")); @@ -262,18 +272,21 @@ class MPPlayMissionGui extends GuiImage { prevBtn.extent = new Vector(73, 44); prevBtn.gamepadAccelerator = ["dpadLeft"]; prevBtn.pressedAction = (sender) -> { - setSelectedFunc(currentSelection - 1); + NetCommands.setLobbyLevelIndex(currentCategory, currentSelection - 1); } - window.addChild(prevBtn); + if (isHost) + window.addChild(prevBtn); var nextBtn = new GuiButton(loadButtonImagesExt("data/ui/mp/play/next")); nextBtn.position = new Vector(659, 514); nextBtn.extent = new Vector(73, 44); nextBtn.gamepadAccelerator = ["dpadRight"]; + nextBtn.pressedAction = (sender) -> { - setSelectedFunc(currentSelection + 1); + NetCommands.setLobbyLevelIndex(currentCategory, currentSelection + 1); } - window.addChild(nextBtn); + if (isHost) + window.addChild(nextBtn); var playBtn = new GuiButton(loadButtonImages("data/ui/mp/play/play")); playBtn.position = new Vector(565, 514); @@ -422,7 +435,7 @@ class MPPlayMissionGui extends GuiImage { currentCategoryStatic = currentCategory; - setSelectedFunc(currentList.length - 1); + NetCommands.setLobbyLevelIndex(category, currentList.length - 1); // if (doRender) // this.render(cast(this.parent, Canvas).scene2d); } @@ -519,6 +532,14 @@ class MPPlayMissionGui extends GuiImage { // } } + setLevelFn = (cat:String, index:Int) -> { + if (currentCategory != cat) { + currentCategory = cat; + setCategoryFunc(cat); + } + setSelectedFunc(index); + } + currentList = MissionList.missionList["multiplayer"]["beginner"]; setCategoryFunc(currentCategoryStatic, null, false); diff --git a/src/gui/MPSearchGui.hx b/src/gui/MPSearchGui.hx new file mode 100644 index 00000000..dfa3674f --- /dev/null +++ b/src/gui/MPSearchGui.hx @@ -0,0 +1,264 @@ +package gui; + +import src.Marbleland; +import h2d.Tile; +import hxd.BitmapData; +import src.MarbleGame; +import hxd.res.BitmapFont; +import h3d.Vector; +import src.ResourceLoader; +import src.Settings; +import src.MissionList; + +class MPSearchGui extends GuiImage { + public function new(isCustom:Bool) { + var img = ResourceLoader.getImage("data/ui/mp/search/window.png"); + super(img.resource.toTile()); + + this.horizSizing = Center; + this.vertSizing = Center; + this.position = new Vector(76, 8); + this.extent = new Vector(487, 463); + + var missionList = []; + if (!isCustom) { + for (diff in MissionList.missionList["multiplayer"]) { + for (mis in diff) { + missionList.push({ + mis: mis, + name: mis.title, + artist: mis.artist, + path: mis.path + }); + } + } + } else { + var customsList = MissionList.customMissions; + for (mis in customsList) { + missionList.push({ + mis: mis, + name: mis.title, + artist: mis.artist, + path: mis.path + }); + } + } + + var displayList = missionList.map(x -> x.name); + displayList.sort((x, y) -> (x > y) ? 1 : (x == y ? 0 : -1)); + missionList.sort((x, y) -> x.name > y.name ? 1 : (x.name == y.name ? 0 : -1)); + var retrieveMissionList = missionList; + + var searchMissionList:GuiTextListCtrl = null; + var scrollCtrl:GuiScrollCtrl = null; + + var currentSortBy = "title"; + + function sortBy(type:String, txt:String = "") { + if (type == "title") { + retrieveMissionList = missionList.filter(x -> StringTools.contains(x.name.toLowerCase(), txt.toLowerCase())); + displayList = retrieveMissionList.map(x -> x.name); + displayList.sort((x, y) -> (x > y) ? 1 : (x == y ? 0 : -1)); + retrieveMissionList.sort((x, y) -> x.name > y.name ? 1 : (x.name == y.name ? 0 : -1)); + } + if (type == "artist") { + retrieveMissionList = missionList.filter(x -> StringTools.contains(x.artist.toLowerCase(), txt.toLowerCase())); + retrieveMissionList.sort((x, y) -> x.artist > y.artist ? 1 : (x.artist == y.artist ? 0 : -1)); + displayList = retrieveMissionList.map(x -> '${x.name} By ${x.artist}'); + } + if (type == "file") { + retrieveMissionList = missionList.filter(x -> StringTools.contains(x.path.toLowerCase(), txt.toLowerCase())); + retrieveMissionList.sort((x, y) -> x.path > y.path ? 1 : (x.path == y.path ? 0 : -1)); + var idxofslash = 0; + displayList = retrieveMissionList.map(x -> { + var idxofslash = 0; + var slashcount = 0; + for (i in 0...x.path.length) { + if (x.path.charCodeAt(x.path.length - i - 1) == '/'.code) { + slashcount++; + if (slashcount == 2) { + idxofslash = x.path.length - i - 1; + break; + } + } + } + return '${x.path.substr(idxofslash + 1)}'; + }); + } + searchMissionList.setTexts(displayList); + scrollCtrl.setScrollMax(searchMissionList.calculateFullHeight()); + } + + function loadButtonImages(path:String) { + var normal = ResourceLoader.getResource('${path}_n.png', ResourceLoader.getImage, this.imageResources).toTile(); + var hover = ResourceLoader.getResource('${path}_h.png', ResourceLoader.getImage, this.imageResources).toTile(); + var pressed = ResourceLoader.getResource('${path}_d.png', ResourceLoader.getImage, this.imageResources).toTile(); + var disabledObj = ResourceLoader.getResource('${path}_i.png', ResourceLoader.getImage, this.imageResources); + var disabled = disabledObj != null ? disabledObj.toTile() : null; + return [normal, hover, pressed, disabled]; + } + + var markerFelt32fontdata = ResourceLoader.getFileEntry("data/font/MarkerFelt.fnt"); + var markerFelt32b = new BitmapFont(markerFelt32fontdata.entry); + @:privateAccess markerFelt32b.loader = ResourceLoader.loader; + var markerFelt32 = markerFelt32b.toSdfFont(cast 26 * Settings.uiScale, MultiChannel); + var markerFelt24 = markerFelt32b.toSdfFont(cast 18 * Settings.uiScale, MultiChannel); + var markerFelt18 = markerFelt32b.toSdfFont(cast 14 * Settings.uiScale, MultiChannel); + + var domcasual32fontdata = ResourceLoader.getFileEntry("data/font/DomCasualD.fnt"); + var domcasual32b = new BitmapFont(domcasual32fontdata.entry); + @:privateAccess domcasual32b.loader = ResourceLoader.loader; + var domcasual32 = domcasual32b.toSdfFont(cast 26 * Settings.uiScale, MultiChannel); + var domcasual64 = domcasual32b.toSdfFont(cast 58 * Settings.uiScale, MultiChannel); + var domcasual24 = domcasual32b.toSdfFont(cast 20 * Settings.uiScale, MultiChannel); + + var searchCancel = new GuiButton(loadButtonImages("data/ui/mp/search/cancel")); + searchCancel.vertSizing = Top; + searchCancel.position = new Vector(21, 395); + searchCancel.extent = new Vector(94, 45); + searchCancel.pressedAction = (e) -> { + MarbleGame.canvas.popDialog(this); + } + this.addChild(searchCancel); + + var selectedIdx:Int = -1; + + var searchPlay = new GuiButton(loadButtonImages("data/ui/mp/search/play")); + searchPlay.position = new Vector(370, 395); + searchPlay.extent = new Vector(94, 45); + searchPlay.disabled = true; + searchPlay.pressedAction = (e) -> { + if (selectedIdx != -1) { + var mis = retrieveMissionList[selectedIdx]; + cast(this.parent, Canvas).marbleGame.playMission(mis.mis); + } + } + this.addChild(searchPlay); + + var searchTitle = new GuiText(domcasual24); + searchTitle.position = new Vector(52, 23); + searchTitle.extent = new Vector(64, 25); + searchTitle.text.textColor = 0x696969; + searchTitle.text.text = "Title:"; + this.addChild(searchTitle); + + var searchEdit = new GuiTextInput(domcasual24); + searchEdit.text.textColor = 0; + searchEdit.text.selectionColor.setColor(0xFFFFFFFF); + searchEdit.text.selectionTile = h2d.Tile.fromColor(0x808080, 0, hxd.Math.ceil(searchEdit.text.font.lineHeight)); + searchEdit.position = new Vector(91, 23); + searchEdit.extent = new Vector(373, 29); + searchEdit.onTextChange = (txt) -> { + sortBy(currentSortBy, txt); + }; + this.addChild(searchEdit); + + scrollCtrl = new GuiScrollCtrl(ResourceLoader.getResource("data/ui/common/philscroll.png", ResourceLoader.getImage, this.imageResources).toTile()); + scrollCtrl.position = new Vector(18, 68); + scrollCtrl.extent = new Vector(447, 317); + scrollCtrl.childrenHandleScroll = true; + this.addChild(scrollCtrl); + + searchMissionList = new GuiTextListCtrl(markerFelt24, displayList, 0xFFFFFF); + searchMissionList.selectedColor = 0xFFFFFF; + searchMissionList.selectedFillColor = 0x7585E3; + searchMissionList.horizSizing = Width; + searchMissionList.position = new Vector(4, -1); + searchMissionList.extent = new Vector(432, 2880); + searchMissionList.textYOffset = -6; + searchMissionList.scrollable = true; + searchMissionList.onSelectedFunc = (sel) -> { + selectedIdx = sel; + if (retrieveMissionList.length <= selectedIdx || selectedIdx < 0) { + searchPlay.disabled = true; + } else { + searchPlay.disabled = false; + } + } + scrollCtrl.addChild(searchMissionList); + scrollCtrl.setScrollMax(searchMissionList.calculateFullHeight()); + + var optionsPopup:GuiButton = null; + + var searchOptions = new GuiButton(loadButtonImages("data/ui/mp/search/options")); + searchOptions.vertSizing = Top; + searchOptions.horizSizing = Right; + searchOptions.position = new Vector(121, 398); + searchOptions.extent = new Vector(94, 45); + searchOptions.pressedAction = (e) -> { + MarbleGame.canvas.pushDialog(optionsPopup); + } + this.addChild(searchOptions); + + var temprev = new BitmapData(1, 1); + temprev.setPixel(0, 0, 0); + var tmpprevtile = Tile.fromBitmap(temprev); + + optionsPopup = new GuiButton([tmpprevtile, tmpprevtile, tmpprevtile]); + optionsPopup.horizSizing = Width; + optionsPopup.vertSizing = Height; + optionsPopup.position = new Vector(0, 0); + optionsPopup.extent = new Vector(640, 480); + optionsPopup.pressedAction = (e) -> { + MarbleGame.canvas.popDialog(optionsPopup, false); + } + + var optionsPopupInner = new GuiControl(); + optionsPopupInner.horizSizing = Center; + optionsPopupInner.vertSizing = Center; + optionsPopupInner.position = new Vector(80, 7); + optionsPopupInner.extent = new Vector(480, 465); + optionsPopup.addChild(optionsPopupInner); + + var optionsBgR = ResourceLoader.getResource('data/ui/mp/play/moremenu.png', ResourceLoader.getImage, this.imageResources).toTile(); + + var optionsBg = new GuiImage(optionsBgR); + optionsBg.position = new Vector(0, 281); + optionsBg.extent = new Vector(348, 148); + optionsPopupInner.addChild(optionsBg); + + var searchByFile = new GuiButton(loadButtonImages("data/ui/mp/search/file")); + searchByFile.buttonType = Radio; + searchByFile.position = new Vector(229, 45); + searchByFile.extent = new Vector(67, 45); + searchByFile.pressedAction = (e) -> { + searchTitle.text.text = "File:"; + currentSortBy = "file"; + sortBy("file"); + }; + optionsBg.addChild(searchByFile); + + var searchByartist = new GuiButton(loadButtonImages("data/ui/mp/search/artist")); + searchByartist.buttonType = Radio; + searchByartist.position = new Vector(159, 45); + searchByartist.extent = new Vector(71, 45); + searchByartist.pressedAction = (e) -> { + searchTitle.text.text = "Artist:"; + currentSortBy = "artist"; + sortBy("artist"); + }; + optionsBg.addChild(searchByartist); + + var searchByTitle = new GuiButton(loadButtonImages("data/ui/mp/search/name")); + searchByTitle.buttonType = Radio; + searchByTitle.position = new Vector(92, 45); + searchByTitle.extent = new Vector(68, 45); + searchByTitle.pressed = true; + searchByTitle.pressedAction = (e) -> { + searchTitle.text.text = "Title:"; + currentSortBy = "title"; + sortBy("title"); + }; + optionsBg.addChild(searchByTitle); + + var searchRandom = new GuiButton(loadButtonImages("data/ui/mp/search/random")); + searchRandom.vertSizing = Top; + searchRandom.position = new Vector(44, 45); + searchRandom.extent = new Vector(44, 44); + searchRandom.pressedAction = (e) -> { + var mis = missionList[Math.floor(Math.random() * missionList.length)]; + cast(this.parent, Canvas).marbleGame.playMission(mis.mis); + } + optionsBg.addChild(searchRandom); + } +} diff --git a/src/gui/MPServerDlg.hx b/src/gui/MPServerDlg.hx new file mode 100644 index 00000000..0c34e10b --- /dev/null +++ b/src/gui/MPServerDlg.hx @@ -0,0 +1,203 @@ +package gui; + +import h2d.filter.DropShadow; +import src.Marbleland; +import h2d.Tile; +import hxd.BitmapData; +import src.MarbleGame; +import hxd.res.BitmapFont; +import h3d.Vector; +import src.ResourceLoader; +import src.Settings; +import src.MissionList; + +class MPServerDlg extends GuiImage { + public function new() { + var img = ResourceLoader.getImage("data/ui/mp/settings/serversettings.png"); + super(img.resource.toTile()); + + this.horizSizing = Center; + this.vertSizing = Center; + this.position = new Vector(100, 17); + this.extent = new Vector(440, 446); + + function loadButtonImages(path:String) { + var normal = ResourceLoader.getResource('${path}_n.png', ResourceLoader.getImage, this.imageResources).toTile(); + var hover = ResourceLoader.getResource('${path}_h.png', ResourceLoader.getImage, this.imageResources).toTile(); + var pressed = ResourceLoader.getResource('${path}_d.png', ResourceLoader.getImage, this.imageResources).toTile(); + return [normal, hover, pressed]; + } + + var markerFelt32fontdata = ResourceLoader.getFileEntry("data/font/MarkerFelt.fnt"); + var markerFelt32b = new BitmapFont(markerFelt32fontdata.entry); + @:privateAccess markerFelt32b.loader = ResourceLoader.loader; + var markerFelt32 = markerFelt32b.toSdfFont(cast 26 * Settings.uiScale, MultiChannel); + var markerFelt24 = markerFelt32b.toSdfFont(cast 20 * Settings.uiScale, MultiChannel); + var markerFelt20 = markerFelt32b.toSdfFont(cast 18.5 * Settings.uiScale, MultiChannel); + var markerFelt18 = markerFelt32b.toSdfFont(cast 17 * Settings.uiScale, MultiChannel); + var markerFelt26 = markerFelt32b.toSdfFont(cast 22 * Settings.uiScale, MultiChannel); + + var cancelBtn = new GuiButton(loadButtonImages("data/ui/mp/join/cancel")); + cancelBtn.vertSizing = Top; + cancelBtn.horizSizing = Left; + cancelBtn.position = new Vector(123, 384); + cancelBtn.extent = new Vector(94, 45); + cancelBtn.pressedAction = (e) -> { + MarbleGame.canvas.popDialog(this); + } + this.addChild(cancelBtn); + + var saveBtn = new GuiButton(loadButtonImages("data/ui/mp/join/save")); + saveBtn.horizSizing = Left; + saveBtn.vertSizing = Top; + saveBtn.position = new Vector(223, 384); + saveBtn.extent = new Vector(94, 45); + this.addChild(saveBtn); + + var title = new GuiText(markerFelt32); + title.text.text = "Server Settings"; + title.text.textColor = 0xFFFFFF; + title.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + title.justify = Center; + title.position = new Vector(11, 21); + title.extent = new Vector(418, 14); + title.horizSizing = Width; + this.addChild(title); + + // var showPwdTitle = new GuiText(markerFelt18); + // showPwdTitle.text.text = "Show Password"; + // showPwdTitle.text.textColor = 0xFFFFFF; + // showPwdTitle.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + // showPwdTitle.position = new Vector(259, 94); + // showPwdTitle.extent = new Vector(129, 14); + // showPwdTitle.horizSizing = Left; + // this.addChild(showPwdTitle); + + // var showPasswords = new GuiButton(loadButtonImages("data/ui/mp/lb_chkbx")); + // showPasswords.buttonType = Toggle; + // showPasswords.horizSizing = Left; + // showPasswords.position = new Vector(389, 86); + // showPasswords.extent = new Vector(31, 31); + // this.addChild(showPasswords); + + var serverSettingsContainer = new GuiControl(); + serverSettingsContainer.vertSizing = Height; + serverSettingsContainer.horizSizing = Left; + serverSettingsContainer.position = new Vector(16, 65); + serverSettingsContainer.extent = new Vector(390, 276); + this.addChild(serverSettingsContainer); + + var serverName = new GuiText(markerFelt18); + serverName.text.text = "Server Name:"; + serverName.text.textColor = 0xFFFFFF; + serverName.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + serverName.position = new Vector(0, 0); + serverName.extent = new Vector(206, 14); + serverSettingsContainer.addChild(serverName); + + var serverNameEditBg = new GuiImage(ResourceLoader.getResource("data/ui/mp/settings/inputbg.png", ResourceLoader.getImage, this.imageResources) + .toTile()); + serverNameEditBg.position = new Vector(93, 0); + serverNameEditBg.extent = new Vector(297, 35); + serverSettingsContainer.addChild(serverNameEditBg); + + var serverNameEdit = new GuiTextInput(markerFelt18); + serverNameEdit.position = new Vector(3, 3); + serverNameEdit.extent = new Vector(291, 29); + serverNameEdit.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + serverNameEdit.horizSizing = Left; + serverNameEditBg.addChild(serverNameEdit); + + var password = new GuiText(markerFelt18); + password.text.text = "Password:"; + password.text.textColor = 0xFFFFFF; + password.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + password.position = new Vector(0, 39); + password.extent = new Vector(206, 14); + serverSettingsContainer.addChild(password); + + var passwordEditBg = new GuiImage(ResourceLoader.getResource("data/ui/mp/settings/inputbg.png", ResourceLoader.getImage, this.imageResources) + .toTile()); + passwordEditBg.position = new Vector(93, 6 + 29); + passwordEditBg.extent = new Vector(297, 35); + serverSettingsContainer.addChild(passwordEditBg); + + var passwordEdit = new GuiTextInput(markerFelt18); + passwordEdit.position = new Vector(3, 3); + passwordEdit.extent = new Vector(291, 29); + passwordEdit.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + passwordEdit.horizSizing = Left; + passwordEditBg.addChild(passwordEdit); + + var serverDescTitle = new GuiText(markerFelt18); + serverDescTitle.text.text = "Server Info:"; + serverDescTitle.text.textColor = 0xFFFFFF; + serverDescTitle.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + serverDescTitle.position = new Vector(0, 39 * 2); + serverDescTitle.extent = new Vector(206, 14); + serverSettingsContainer.addChild(serverDescTitle); + + var serverDescEditBg = new GuiImage(ResourceLoader.getResource("data/ui/mp/settings/inputbg.png", ResourceLoader.getImage, this.imageResources) + .toTile()); + serverDescEditBg.position = new Vector(0, 39 * 3); + serverDescEditBg.extent = new Vector(297 + 93, 35); + serverSettingsContainer.addChild(serverDescEditBg); + + var serverDescEdit = new GuiTextInput(markerFelt18); + serverDescEdit.position = new Vector(3, 3); + serverDescEdit.extent = new Vector(291 + 93, 29); + serverDescEdit.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + serverDescEdit.horizSizing = Left; + serverDescEditBg.addChild(serverDescEdit); + + var maxPlayers = new GuiText(markerFelt18); + maxPlayers.text.text = "Max Players: 8"; + maxPlayers.text.textColor = 0xFFFFFF; + maxPlayers.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + maxPlayers.position = new Vector(0, 39 * 4); + maxPlayers.extent = new Vector(206, 14); + serverSettingsContainer.addChild(maxPlayers); + + var playerMinus = new GuiButton(loadButtonImages("data/ui/mp/settings/minus")); + playerMinus.position = new Vector(331, 9 + 29 * 5); + playerMinus.extent = new Vector(31, 31); + playerMinus.pressedAction = (sender) -> {}; + serverSettingsContainer.addChild(playerMinus); + + var playerPlus = new GuiButton(loadButtonImages("data/ui/mp/settings/plus")); + playerPlus.position = new Vector(359, 9 + 29 * 5); + playerPlus.extent = new Vector(31, 31); + playerPlus.pressedAction = (sender) -> {}; + serverSettingsContainer.addChild(playerPlus); + + var forceSpectators = new GuiText(markerFelt18); + forceSpectators.text.text = "Force Spectators:"; + forceSpectators.text.textColor = 0xFFFFFF; + forceSpectators.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + forceSpectators.position = new Vector(0, 39 * 5); + forceSpectators.extent = new Vector(206, 14); + serverSettingsContainer.addChild(forceSpectators); + + var forceSpectatorsChk = new GuiButton(loadButtonImages("data/ui/mp/lb_chkbx")); + forceSpectatorsChk.position = new Vector(359, 9 * 2 + 29 * 6 + 2); + forceSpectatorsChk.extent = new Vector(31, 31); + forceSpectatorsChk.buttonType = Toggle; + forceSpectatorsChk.pressedAction = (sender) -> {}; + serverSettingsContainer.addChild(forceSpectatorsChk); + + var quickRespawn = new GuiText(markerFelt18); + quickRespawn.text.text = "Allow Quick Respawn:"; + quickRespawn.text.textColor = 0xFFFFFF; + quickRespawn.text.filter = new DropShadow(1.414, 0.785, 0x0000000F, 1, 0, 0.4, 1, true); + quickRespawn.position = new Vector(0, 39 * 6); + quickRespawn.extent = new Vector(206, 14); + serverSettingsContainer.addChild(quickRespawn); + + var quickRespawnChk = new GuiButton(loadButtonImages("data/ui/mp/lb_chkbx")); + quickRespawnChk.position = new Vector(359, 9 * 3 + 29 * 7 + 4); + quickRespawnChk.extent = new Vector(31, 31); + quickRespawnChk.buttonType = Toggle; + quickRespawnChk.pressedAction = (sender) -> {}; + serverSettingsContainer.addChild(quickRespawnChk); + } +}