diff --git a/TODO b/TODO index 0106a4c..089ee2f 100644 --- a/TODO +++ b/TODO @@ -31,6 +31,7 @@ * Find incentives to move from starting position * Outcome: disable the loot button if there is no loot * Ensure that tweens and particle emitters get destroyed once animation is done (or view changes) +* UI: add standard confirm dialog * Controls: do not focus on ship while targetting for area effects (dissociate hover and target) * Controls: fix hover being stuck when the cursor exits the window, or the item moves or is hidden * Drones: add hull points and take area damage diff --git a/graphics/ui/battle.svg b/graphics/ui/battle.svg index 37badfd..4c89536 100644 --- a/graphics/ui/battle.svg +++ b/graphics/ui/battle.svg @@ -18,10 +18,11 @@ version="1.1" inkscape:version="0.92.1 r15371" sodipodi:docname="battle.svg" - inkscape:export-filename="/tmp/whole.png" + inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/battle/actionbar/background.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" - style="enable-background:new"> + style="enable-background:new" + enable-background="new"> + + + @@ -2060,7 +2071,6 @@ height="131.76981" x="0" y="-27.637838" - transform="translate(0,27.637839)" inkscape:export-filename="/home/michael/workspace/perso/spacetac/out/assets/images/battle/actionbar.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" @@ -2069,7 +2079,8 @@ inkscape:tile-w="1920" inkscape:tile-h="131.76981" inkscape:tile-x0="0" - inkscape:tile-y0="6.3635254e-07" /> + inkscape:tile-y0="6.3635254e-07" + transform="translate(0,27.637839)" /> - - - - - - + + + + + + + + + + + + + + + 2 + style="fill:#aaaaaa;fill-opacity:1;stroke-width:0.9375px">2 image/svg+xml - + @@ -1346,6 +1346,118 @@ id="layer2" inkscape:label="Buttons" style="display:inline"> + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + SpaceTac + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cancel + + + + + + Give this invite code to your friend: + XT324 + Waiting for a connection ... + + + + + X + + + + diff --git a/graphics/ui/template.svg b/graphics/ui/template.svg new file mode 100644 index 0000000..2743328 --- /dev/null +++ b/graphics/ui/template.svg @@ -0,0 +1,54 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/out/assets/images/battle/action-active.png b/out/assets/images/battle/action-active.png deleted file mode 100644 index 3cdcc2d..0000000 Binary files a/out/assets/images/battle/action-active.png and /dev/null differ diff --git a/out/assets/images/battle/action-cooldown.png b/out/assets/images/battle/action-cooldown.png deleted file mode 100644 index 7c72147..0000000 Binary files a/out/assets/images/battle/action-cooldown.png and /dev/null differ diff --git a/out/assets/images/battle/action-inactive.png b/out/assets/images/battle/action-inactive.png deleted file mode 100644 index 7d69743..0000000 Binary files a/out/assets/images/battle/action-inactive.png and /dev/null differ diff --git a/out/assets/images/battle/action-selected.png b/out/assets/images/battle/action-selected.png deleted file mode 100644 index 1cc233a..0000000 Binary files a/out/assets/images/battle/action-selected.png and /dev/null differ diff --git a/out/assets/images/battle/actionbar.png b/out/assets/images/battle/actionbar.png deleted file mode 100644 index dd9139e..0000000 Binary files a/out/assets/images/battle/actionbar.png and /dev/null differ diff --git a/out/assets/images/battle/actions/endturn.png b/out/assets/images/battle/actionbar/action-endturn.png similarity index 100% rename from out/assets/images/battle/actions/endturn.png rename to out/assets/images/battle/actionbar/action-endturn.png diff --git a/out/assets/images/battle/actions/move.png b/out/assets/images/battle/actionbar/action-move.png similarity index 100% rename from out/assets/images/battle/actions/move.png rename to out/assets/images/battle/actionbar/action-move.png diff --git a/out/assets/images/battle/actionbar/background.png b/out/assets/images/battle/actionbar/background.png new file mode 100644 index 0000000..3212f3a Binary files /dev/null and b/out/assets/images/battle/actionbar/background.png differ diff --git a/out/assets/images/battle/actionbar/button-menu.png b/out/assets/images/battle/actionbar/button-menu.png new file mode 100644 index 0000000..7c74771 Binary files /dev/null and b/out/assets/images/battle/actionbar/button-menu.png differ diff --git a/out/assets/images/battle/actionbar/icon.png b/out/assets/images/battle/actionbar/icon.png new file mode 100644 index 0000000..03c9655 Binary files /dev/null and b/out/assets/images/battle/actionbar/icon.png differ diff --git a/out/assets/images/battle/actionbar/power.png b/out/assets/images/battle/actionbar/power.png new file mode 100644 index 0000000..25a1b05 Binary files /dev/null and b/out/assets/images/battle/actionbar/power.png differ diff --git a/out/assets/images/battle/power-available.png b/out/assets/images/battle/power-available.png deleted file mode 100644 index c42665d..0000000 Binary files a/out/assets/images/battle/power-available.png and /dev/null differ diff --git a/out/assets/images/battle/power-used.png b/out/assets/images/battle/power-used.png deleted file mode 100644 index 37f0d07..0000000 Binary files a/out/assets/images/battle/power-used.png and /dev/null differ diff --git a/out/assets/images/battle/power-using.png b/out/assets/images/battle/power-using.png deleted file mode 100644 index a4695ba..0000000 Binary files a/out/assets/images/battle/power-using.png and /dev/null differ diff --git a/out/assets/images/common/dialog-close.png b/out/assets/images/common/dialog-close.png new file mode 100644 index 0000000..65da8da Binary files /dev/null and b/out/assets/images/common/dialog-close.png differ diff --git a/out/assets/images/map/button-zoom.png b/out/assets/images/map/button-zoom.png deleted file mode 100644 index 7006dc1..0000000 Binary files a/out/assets/images/map/button-zoom.png and /dev/null differ diff --git a/out/assets/images/map/buttons.png b/out/assets/images/map/buttons.png new file mode 100644 index 0000000..5a2a9a7 Binary files /dev/null and b/out/assets/images/map/buttons.png differ diff --git a/out/assets/images/menu/button-fullscreen.png b/out/assets/images/menu/button-fullscreen.png deleted file mode 100644 index f34d054..0000000 Binary files a/out/assets/images/menu/button-fullscreen.png and /dev/null differ diff --git a/out/assets/images/options/background.png b/out/assets/images/options/background.png new file mode 100644 index 0000000..96b721d Binary files /dev/null and b/out/assets/images/options/background.png differ diff --git a/out/assets/images/options/button.png b/out/assets/images/options/button.png new file mode 100644 index 0000000..4d0c902 Binary files /dev/null and b/out/assets/images/options/button.png differ diff --git a/out/assets/images/options/options.png b/out/assets/images/options/options.png new file mode 100644 index 0000000..f21d954 Binary files /dev/null and b/out/assets/images/options/options.png differ diff --git a/out/assets/images/options/toggle.png b/out/assets/images/options/toggle.png new file mode 100644 index 0000000..48a7d00 Binary files /dev/null and b/out/assets/images/options/toggle.png differ diff --git a/src/MainUI.ts b/src/MainUI.ts index a4eb7c6..493513e 100644 --- a/src/MainUI.ts +++ b/src/MainUI.ts @@ -13,6 +13,9 @@ module TS.SpaceTac { // Audio manager audio: UI.Audio; + // Game options + options: UI.GameOptions; + // Storage used storage: Storage; @@ -25,7 +28,6 @@ module TS.SpaceTac { this.headless = headless; this.audio = new UI.Audio(this); - this.storage = localStorage; this.session = new GameSession(); @@ -53,6 +55,9 @@ module TS.SpaceTac { if (!this.headless) { this.plugins.add((Phaser.Plugin).SceneGraph); } + + this.audio = new UI.Audio(this); + this.options = new UI.GameOptions(this); } /** @@ -133,5 +138,27 @@ module TS.SpaceTac { return null; } } + + /** + * Check if the game is currently fullscreen + */ + isFullscreen(): boolean { + return this.scale.isFullScreen; + } + + /** + * Toggle fullscreen mode. + * + * Returns true if the result is fullscreen + */ + toggleFullscreen(active: boolean | null = null): boolean { + if (active === false || (active !== true && this.isFullscreen())) { + this.scale.stopFullScreen(); + return false; + } else { + this.scale.startFullScreen(true); + return true; + } + } } } diff --git a/src/ui/BaseView.ts b/src/ui/BaseView.ts index 4faf280..d795a09 100644 --- a/src/ui/BaseView.ts +++ b/src/ui/BaseView.ts @@ -4,26 +4,29 @@ module TS.SpaceTac.UI { */ export class BaseView extends Phaser.State { // Link to the root UI - gameui: MainUI; + gameui: MainUI // Message notifications - messages: Messages; + messages: Messages // Input and key bindings - inputs: InputManager; + inputs: InputManager // Animations - animations: Animations; + animations: Animations // Timing - timer: Timer; + timer: Timer // Tooltip - tooltip_layer: Phaser.Group; - tooltip: Tooltip; + tooltip_layer: Phaser.Group + tooltip: Tooltip // Layers - layers: Phaser.Group; + layers: Phaser.Group + + // Modal dialogs + dialogs_layer: Phaser.Group // Get the size of display getWidth(): number { @@ -56,6 +59,7 @@ module TS.SpaceTac.UI { // Layers this.layers = this.add.group(undefined, "View layers"); + this.dialogs_layer = this.add.group(undefined, "Dialogs layer"); this.tooltip_layer = this.add.group(undefined, "Tooltip layer"); this.tooltip = new Tooltip(this); this.messages = new Messages(this); @@ -80,6 +84,14 @@ module TS.SpaceTac.UI { this.timer.cancelAll(true); } + get audio() { + return this.gameui.audio; + } + + get options() { + return this.gameui.options; + } + /** * Go back to the router state */ @@ -123,20 +135,10 @@ module TS.SpaceTac.UI { } /** - * Toggle fullscreen mode. - * - * Returns true if the result is fullscreen + * Open options dialog */ - toggleFullscreen(active: boolean | null = null): boolean { - if (active === false || (active !== true && this.game.scale.isFullScreen)) { - this.scale.stopFullScreen(); - this.setStorage("fullscreen", "false"); - return false; - } else { - this.scale.startFullScreen(true); - this.setStorage("fullscreen", "true"); - return true; - } + showOptions(): void { + let dialog = new OptionsDialog(this); } /** diff --git a/src/ui/Preload.ts b/src/ui/Preload.ts index 2a8434d..2848564 100644 --- a/src/ui/Preload.ts +++ b/src/ui/Preload.ts @@ -11,11 +11,6 @@ module TS.SpaceTac.UI { this.load.setPreloadSprite(this.preloadBar); // Load images - this.loadImage("menu/title.png"); - this.loadImage("menu/button.png"); - this.loadImage("menu/button-hover.png"); - this.loadImage("menu/button-fullscreen.png"); - this.loadImage("menu/load-bg.png"); this.loadSheet("common/particles.png", 32); this.loadImage("common/transparent.png"); this.loadImage("common/debug.png"); @@ -23,20 +18,22 @@ module TS.SpaceTac.UI { this.loadImage("common/arrow.png"); this.loadImage("common/button-ok.png"); this.loadImage("common/button-cancel.png"); - this.loadImage("battle/shiplist/background.png"); - this.loadImage("battle/shiplist/item-background.png"); - this.loadImage("battle/shiplist/damage.png"); - this.loadImage("battle/shiplist/hover.png"); - this.loadImage("battle/shiplist/info-button.png"); + this.loadSheet("common/dialog-close.png", 92, 82); + this.loadImage("menu/title.png"); + this.loadImage("menu/button.png"); + this.loadImage("menu/button-hover.png"); + this.loadImage("menu/load-bg.png"); + this.loadImage("options/background.png"); + this.loadSheet("options/button.png", 497, 134); + this.loadSheet("options/options.png", 128, 128); + this.loadSheet("options/toggle.png", 149, 149); this.loadImage("battle/background.jpg"); - this.loadImage("battle/actionbar.png"); - this.loadImage("battle/action-inactive.png"); - this.loadImage("battle/action-active.png"); - this.loadImage("battle/action-selected.png"); - this.loadImage("battle/action-cooldown.png"); - this.loadImage("battle/power-available.png"); - this.loadImage("battle/power-using.png"); - this.loadImage("battle/power-used.png"); + this.loadImage("battle/actionbar/background.png"); + this.loadSheet("battle/actionbar/icon.png", 88, 88); + this.loadSheet("battle/actionbar/power.png", 58, 21); + this.loadImage("battle/actionbar/action-move.png"); + this.loadImage("battle/actionbar/action-endturn.png"); + this.loadSheet("battle/actionbar/button-menu.png", 79, 132); this.loadImage("battle/arena/background.png"); this.loadImage("battle/arena/ap-indicator.png"); this.loadImage("battle/arena/ship-normal-enemy.png"); @@ -53,8 +50,11 @@ module TS.SpaceTac.UI { this.loadImage("battle/arena/stasis.png"); this.loadImage("battle/arena/target.png"); this.loadImage("battle/arena/blast.png"); - this.loadImage("battle/actions/move.png"); - this.loadImage("battle/actions/endturn.png"); + this.loadImage("battle/shiplist/background.png"); + this.loadImage("battle/shiplist/item-background.png"); + this.loadImage("battle/shiplist/damage.png"); + this.loadImage("battle/shiplist/hover.png"); + this.loadImage("battle/shiplist/info-button.png"); this.loadImage("battle/weapon/default.png"); this.loadImage("battle/weapon/bullets.png"); this.loadImage("battle/weapon/hot.png"); @@ -74,7 +74,7 @@ module TS.SpaceTac.UI { this.loadSheet("map/action.png", 323, 192); this.loadImage("map/orbit.png"); this.loadImage("map/boundaries.png"); - this.loadSheet("map/button-zoom.png", 115, 191); + this.loadSheet("map/buttons.png", 115, 191); this.loadImage("map/location-star.png"); this.loadImage("map/location-planet.png"); this.loadImage("map/location-warp.png"); diff --git a/src/ui/TestGame.ts b/src/ui/TestGame.ts index ba83da2..79a6970 100644 --- a/src/ui/TestGame.ts +++ b/src/ui/TestGame.ts @@ -114,4 +114,18 @@ module TS.SpaceTac.UI.Specs { let tnode = node; expect(tnode.text).toEqual(content); } + + /** + * Check that a layer contains the given component at a given index + */ + export function checkComponentInLayer(layer: Phaser.Group, index: number, component: UIComponent) { + if (index >= layer.children.length) { + fail(`Not enough children in group ${layer.name} for ${component} at index ${index}`); + } else { + let child = layer.children[index]; + if (child !== (component).container) { + fail(`${component} is not at index ${index} in ${layer.name}`); + } + } + } } diff --git a/src/ui/battle/ActionBar.spec.ts b/src/ui/battle/ActionBar.spec.ts index a36234f..c44a707 100644 --- a/src/ui/battle/ActionBar.spec.ts +++ b/src/ui/battle/ActionBar.spec.ts @@ -105,11 +105,11 @@ module TS.SpaceTac.UI.Specs { bar.power.children.forEach((child, idx) => { let img = child; if (idx < available) { - expect(img.name).toEqual("battle-power-available"); + expect(img.data.frame).toEqual(0); } else if (idx < available + using) { - expect(img.name).toEqual("battle-power-using"); + expect(img.data.frame).toEqual(2); } else { - expect(img.name).toEqual("battle-power-used"); + expect(img.data.frame).toEqual(1); } }); } diff --git a/src/ui/battle/ActionBar.ts b/src/ui/battle/ActionBar.ts index 1887e51..a3a4951 100644 --- a/src/ui/battle/ActionBar.ts +++ b/src/ui/battle/ActionBar.ts @@ -33,21 +33,25 @@ module TS.SpaceTac.UI { battleview.layer_borders.add(this); // Background - this.addChild(new Phaser.Image(this.game, 0, 0, "battle-actionbar", 0)); + this.add(new Phaser.Image(this.game, 0, 0, "battle-actionbar-background", 0)); // Power bar this.power = this.game.add.group(); - this.addChild(this.power); + this.add(this.power); // Group for actions this.actions = new Phaser.Group(this.game); - this.addChild(this.actions); + this.add(this.actions); // Waiting icon this.icon_waiting = new Phaser.Image(this.game, this.width / 2, 50, "common-waiting", 0); this.icon_waiting.anchor.set(0.5, 0.5); this.icon_waiting.scale.set(0.5, 0.5); - this.addChild(this.icon_waiting); + this.add(this.icon_waiting); + + // Options button + let button = battleview.add.button(1841, 0, "battle-actionbar-button-menu", () => battleview.showOptions(), null, 1, 0, 0, 1, this); + battleview.tooltip.bindStaticText(button, "Game options"); // Key bindings battleview.inputs.bind("Escape", "Cancel action", () => this.actionEnded()); @@ -133,7 +137,7 @@ module TS.SpaceTac.UI { // Add an action icon addAction(ship: Ship, action: BaseAction): ActionIcon { - var icon = new ActionIcon(this, 192 + this.action_icons.length * 88, 8, ship, action, this.action_icons.length); + var icon = new ActionIcon(this, 170 + this.action_icons.length * 88, 8, ship, action, this.action_icons.length); this.action_icons.push(icon); return icon; @@ -150,23 +154,23 @@ module TS.SpaceTac.UI { range(current_power - power_capacity).forEach(i => this.power.removeChildAt(current_power - 1 - i)); //this.power.removeChildren(ship_power, current_power); // TODO bugged in phaser 2.6 } else if (power_capacity > current_power) { - range(power_capacity - current_power).forEach(i => this.game.add.image(190 + (current_power + i) * 56, 104, "battle-power-used", 0, this.power)); + range(power_capacity - current_power).forEach(i => this.game.add.image(192 + (current_power + i) * 56, 104, "battle-actionbar-power", 0, this.power)); } let power_value = this.ship_power_value; let remaining_power = power_value - selected_action; this.power.children.forEach((obj, idx) => { let img = obj; - let key: string; + let frame: number; if (idx < remaining_power) { - key = "battle-power-available"; + frame = 0; } else if (idx < power_value) { - key = "battle-power-using"; + frame = 2; } else { - key = "battle-power-used" + frame = 1; } - img.name = key; - img.loadTexture(key); + img.data.frame = frame; + img.frame = frame; }); } diff --git a/src/ui/battle/ActionIcon.ts b/src/ui/battle/ActionIcon.ts index eb442a1..a00b1cf 100644 --- a/src/ui/battle/ActionIcon.ts +++ b/src/ui/battle/ActionIcon.ts @@ -40,7 +40,7 @@ module TS.SpaceTac.UI { // Create an icon for a single ship action constructor(bar: ActionBar, x: number, y: number, ship: Ship, action: BaseAction, position: number) { - super(bar.game, x, y, "battle-action-inactive"); + super(bar.game, x, y, "battle-actionbar-icon"); this.bar = bar; this.battleview = bar.battleview; @@ -51,27 +51,27 @@ module TS.SpaceTac.UI { // Active layer this.active = false; - this.layer_active = new Phaser.Image(this.game, this.width / 2, this.height / 2, "battle-action-active", 0); + this.layer_active = new Phaser.Image(this.game, this.width / 2, this.height / 2, "battle-actionbar-icon", 1); this.layer_active.anchor.set(0.5, 0.5); this.layer_active.visible = false; this.addChild(this.layer_active); // Selected layer this.selected = false; - this.layer_selected = new Phaser.Image(this.game, this.width / 2, this.height / 2, "battle-action-selected", 0); + this.layer_selected = new Phaser.Image(this.game, this.width / 2, this.height / 2, "battle-actionbar-icon", 2); this.layer_selected.anchor.set(0.5, 0.5); this.layer_selected.visible = false; this.addChild(this.layer_selected); // Icon layer - let icon = this.battleview.getImage(`battle-actions-${action.code}`, `equipment-${action.equipment ? action.equipment.code : "---"}`); + let icon = this.battleview.getImage(`battle-actionbar-action-${action.code}`, `equipment-${action.equipment ? action.equipment.code : "---"}`); this.layer_icon = new Phaser.Image(this.game, this.width / 2, this.height / 2, icon, 0); this.layer_icon.anchor.set(0.5, 0.5); this.layer_icon.scale.set(0.25, 0.25); this.addChild(this.layer_icon); // Cooldown layer - this.cooldown = new Phaser.Image(this.game, this.width / 2, this.height / 2, "battle-action-cooldown"); + this.cooldown = new Phaser.Image(this.game, this.width / 2, this.height / 2, "battle-actionbar-icon", 3); this.cooldown.anchor.set(0.5, 0.5); this.cooldown_count = new Phaser.Text(this.game, 0, 0, "", { align: "center", font: "36pt Arial", fill: "#aaaaaa" }); this.cooldown_count.anchor.set(0.5, 0.5); diff --git a/src/ui/battle/ActionTooltip.ts b/src/ui/battle/ActionTooltip.ts index 778deb3..b0b3cd7 100644 --- a/src/ui/battle/ActionTooltip.ts +++ b/src/ui/battle/ActionTooltip.ts @@ -7,7 +7,7 @@ module TS.SpaceTac.UI { * Fill the tooltip */ static fill(filler: TooltipFiller, ship: Ship, action: BaseAction, position: number) { - let icon = filler.view.getImage(`equipment-${action.equipment ? action.equipment.code : "---"}`, `battle-actions-${action.code}`); + let icon = filler.view.getImage(`equipment-${action.equipment ? action.equipment.code : "---"}`, `battle-actionbar-action-${action.code}`); filler.addImage(0, 0, icon, 0.5); filler.addText(150, 0, action.equipment ? action.equipment.name : action.name, "#ffffff", 24); diff --git a/src/ui/common/Audio.ts b/src/ui/common/Audio.ts index c2813b2..573ba39 100644 --- a/src/ui/common/Audio.ts +++ b/src/ui/common/Audio.ts @@ -1,14 +1,13 @@ module TS.SpaceTac.UI { // Utility functions for sounds export class Audio { - - private game: MainUI; - - private music: Phaser.Sound | null; + private game: MainUI + private music: Phaser.Sound | null = null + private music_volume = 1 + private music_playing_volume = 1 constructor(game: MainUI) { this.game = game; - this.music = null; } // Check if the sound system is up and running @@ -31,7 +30,8 @@ module TS.SpaceTac.UI { this.stopMusic(); } if (!this.music) { - this.music = this.game.sound.play(key, volume, true); + this.music_playing_volume = volume; + this.music = this.game.sound.play(key, volume * this.music_volume, true); } } } @@ -46,13 +46,41 @@ module TS.SpaceTac.UI { } } - // Toggle the mute status of the sound system - toggleMute(): void { + /** + * Get the main volume (0-1) + */ + getMainVolume(): number { if (this.isActive()) { - if (this.game.sound.volume > 0) { - this.game.sound.volume = 0; - } else { - this.game.sound.volume = 1; + return this.game.sound.volume; + } else { + return 0; + } + } + + /** + * Set the main volume (0-1) + */ + setMainVolume(value: number) { + if (this.isActive()) { + this.game.sound.volume = clamp(value, 0, 1); + } + } + + /** + * Get the music volume (0-1) + */ + getMusicVolume(): number { + return this.music_volume; + } + + /** + * Set the music volume (0-1) + */ + setMusicVolume(value: number) { + this.music_volume = value; + if (this.isActive()) { + if (this.music) { + this.music.volume = value * this.music_playing_volume; } } } diff --git a/src/ui/common/InputManager.ts b/src/ui/common/InputManager.ts index 1ce69f8..1bfd5c6 100644 --- a/src/ui/common/InputManager.ts +++ b/src/ui/common/InputManager.ts @@ -36,10 +36,10 @@ module TS.SpaceTac.UI { this.game.state.start("router"); }); this.bind("m", "Toggle sound", () => { - this.game.audio.toggleMute(); + this.game.options.setNumberValue("mainvolume", this.game.options.getNumberValue("mainvolume") > 0 ? 0 : 1); }); this.bind("f", "Toggle fullscreen", () => { - view.toggleFullscreen(); + this.game.options.setBooleanValue("fullscreen", !this.game.options.getBooleanValue("fullscreen")); }); this.bind("+", "", () => { if (this.cheats_allowed) { diff --git a/src/ui/common/UIComponent.ts b/src/ui/common/UIComponent.ts index e75a8c6..28d5068 100644 --- a/src/ui/common/UIComponent.ts +++ b/src/ui/common/UIComponent.ts @@ -1,6 +1,32 @@ module TS.SpaceTac.UI { export type UIInternalComponent = Phaser.Group | Phaser.Image | Phaser.Button | Phaser.Sprite; + export type UIImageInfo = string | { key: string, frame?: number, frame1?: number, frame2?: number }; + export type UITextInfo = { content: string, color: string, size: number, bold?: boolean }; + + function imageFromInfo(game: Phaser.Game, info: UIImageInfo): Phaser.Image { + if (typeof info === "string") { + info = { key: info }; + } + let image = new Phaser.Image(game, 0, 0, info.key, info.frame); + image.anchor.set(0.5, 0.5); + return image; + } + + function textFromInfo(game: Phaser.Game, info: UITextInfo): Phaser.Text { + let style = { font: `${info.bold ? "bold " : ""}${info.size}pt Arial`, fill: info.color }; + let text = new Phaser.Text(game, 0, 0, info.content, style); + return text; + } + + function autoFromInfo(game: Phaser.Game, info: UIImageInfo | UITextInfo): Phaser.Text | Phaser.Image { + if (info.hasOwnProperty("content")) { + return textFromInfo(game, info); + } else { + return imageFromInfo(game, info); + } + } + /** * Base class for UI components */ @@ -39,6 +65,14 @@ module TS.SpaceTac.UI { return this.view.gameui; } + jasmineToString(): string { + return this.toString(); + } + + toString(): string { + return `<${classname(this)}>`; + } + /** * Move the a parent's layer */ @@ -46,6 +80,13 @@ module TS.SpaceTac.UI { layer.add(this.container); } + /** + * Destroy the component + */ + destroy(children = true) { + this.container.destroy(children); + } + /** * Create the internal phaser node */ @@ -143,7 +184,7 @@ module TS.SpaceTac.UI { /** * Add a button in the component, positioning its center. */ - addButton(x: number, y: number, on_click: Function, background: string, frame_normal = 0, frame_hover = frame_normal, tooltip = "", angle = 0) { + addButton(x: number, y: number, on_click: Function, background: string, frame_normal = 0, frame_hover = 1, tooltip = "", angle = 0) { let button = new Phaser.Button(this.view.game, x, y, background, on_click, undefined, frame_hover, frame_normal); button.anchor.set(0.5, 0.5); button.angle = angle; @@ -179,6 +220,40 @@ module TS.SpaceTac.UI { this.addInternalChild(image); } + /** + * Add a 2-states toggle button. + * + * *background* should have three frames (toggled, untoggled and hovered). + * + * Returns a function to force the state of the button. + */ + addToggleButton(x: number, y: number, background: UIImageInfo, content: UIImageInfo | UITextInfo, on_change: (toggled: boolean) => void): (toggled: boolean) => void { + let toggled = false; + let toggle = (state: boolean, broadcast = false) => { + toggled = state; + if (typeof background !== "string") { + image.frame = (toggled ? background.frame : background.frame1) || background.frame || 0; + } + contentobj.alpha = toggled ? 1 : 0.5; + if (broadcast) { + on_change(toggled); + } + }; + + let button = new Phaser.Button(this.container.game, x, y, "common-transparent", () => toggle(!toggled, true)); + + let image = imageFromInfo(this.game, background); + let contentobj = autoFromInfo(this.game, content); + + button.addChild(image); + button.addChild(contentobj); + this.addInternalChild(button); + + toggle(toggled); + + return toggle; + } + /** * Set the keyboard focus on this component. */ diff --git a/src/ui/common/UIDialog.spec.ts b/src/ui/common/UIDialog.spec.ts new file mode 100644 index 0000000..ee483a8 --- /dev/null +++ b/src/ui/common/UIDialog.spec.ts @@ -0,0 +1,31 @@ +module TS.SpaceTac.UI.Specs { + describe("UIDialog", () => { + let testgame = setupEmptyView(); + + it("sets up an overlay", function () { + let view = testgame.baseview; + expect(view.dialogs_layer.children.length).toBe(0); + + let dialog1 = new UIDialog(view, 10, 10, "fake"); + expect(view.dialogs_layer.children.length).toBe(2); + expect(view.dialogs_layer.children[0] instanceof Phaser.Button).toBe(true); + checkComponentInLayer(view.dialogs_layer, 1, dialog1); + + let dialog2 = new UIDialog(view, 10, 10, "fake"); + expect(view.dialogs_layer.children.length).toBe(3); + expect(view.dialogs_layer.children[0] instanceof Phaser.Button).toBe(true); + checkComponentInLayer(view.dialogs_layer, 1, dialog1); + checkComponentInLayer(view.dialogs_layer, 2, dialog2); + + dialog1.close(); + + expect(view.dialogs_layer.children.length).toBe(2); + expect(view.dialogs_layer.children[0] instanceof Phaser.Button).toBe(true); + checkComponentInLayer(view.dialogs_layer, 1, dialog2); + + dialog2.close(); + + expect(view.dialogs_layer.children.length).toBe(0); + }); + }); +} diff --git a/src/ui/common/UIDialog.ts b/src/ui/common/UIDialog.ts new file mode 100644 index 0000000..b0dfc0e --- /dev/null +++ b/src/ui/common/UIDialog.ts @@ -0,0 +1,48 @@ +/// + +module TS.SpaceTac.UI { + /** + * Base class for modal dialogs + * + * When a modal dialog opens, an overlay is displayed behind it to prevent clicking through it + */ + export class UIDialog extends UIComponent { + constructor(parent: BaseView, width: number, height: number, background: string) { + super(parent, width, height, background); + + if (parent.dialogs_layer.children.length == 0) { + this.addOverlay(parent.dialogs_layer); + } + + this.moveToLayer(parent.dialogs_layer); + this.setPositionInsideParent(0.5, 0.5); + } + + /** + * Add a control-capturing overlay + */ + addOverlay(layer: Phaser.Group): void { + let overlay = layer.game.add.button(0, 0, "common-transparent", () => null); + overlay.scale.set(this.view.getWidth() / overlay.width, this.view.getHeight() / overlay.height); + layer.add(overlay); + } + + /** + * Add a close button + */ + addCloseButton(key: string, x: number, y: number, frame = 0, frame_hover = 1): void { + this.addButton(x, y, () => this.close(), key, frame, frame_hover, "Close this dialog"); + } + + /** + * Close the dialog, removing the overlay if needed + */ + close() { + this.destroy(); + + if (this.view.dialogs_layer.children.length == 1) { + this.view.dialogs_layer.removeAll(true); + } + } + } +} diff --git a/src/ui/map/UniverseMapView.ts b/src/ui/map/UniverseMapView.ts index 970165d..a67caa2 100644 --- a/src/ui/map/UniverseMapView.ts +++ b/src/ui/map/UniverseMapView.ts @@ -88,14 +88,19 @@ module TS.SpaceTac.UI { this.actions.setPosition(30, 30); this.actions.moveToLayer(this.layer_overlay); - this.zoom_in = new Phaser.Button(this.game, 1520, 100, "map-button-zoom", () => this.setZoom(this.zoom + 1), undefined, 1, 0); + this.zoom_in = new Phaser.Button(this.game, 1540, 172, "map-buttons", () => this.setZoom(this.zoom + 1), undefined, 3, 0); this.zoom_in.anchor.set(0.5, 0.5); this.layer_overlay.add(this.zoom_in); this.tooltip.bindStaticText(this.zoom_in, "Zoom in"); - this.zoom_out = new Phaser.Button(this.game, 1520, 980, "map-button-zoom", () => this.setZoom(this.zoom - 1), undefined, 3, 2); + this.zoom_out = new Phaser.Button(this.game, 1540, 958, "map-buttons", () => this.setZoom(this.zoom - 1), undefined, 4, 1); this.zoom_out.anchor.set(0.5, 0.5); this.layer_overlay.add(this.zoom_out); this.tooltip.bindStaticText(this.zoom_out, "Zoom out"); + let options = new Phaser.Button(this.game, 1436, 69, "map-buttons", () => this.showOptions(), undefined, 5, 2); + options.angle = -90; + options.anchor.set(0.5, 0.5); + this.layer_overlay.add(options); + this.tooltip.bindStaticText(options, "Game options"); this.character_sheet = new CharacterSheet(this, this.getWidth() - 307); this.character_sheet.show(this.player.fleet.ships[0], false); diff --git a/src/ui/menu/MainMenu.ts b/src/ui/menu/MainMenu.ts index f48313e..5bbcdf7 100644 --- a/src/ui/menu/MainMenu.ts +++ b/src/ui/menu/MainMenu.ts @@ -36,7 +36,8 @@ module TS.SpaceTac.UI { this.button_quick_battle = this.addButton(1606, 674, "Quick Battle", "Play a single generated battle", () => this.onQuickBattle()); // Fullscreen button - let button = new Phaser.Button(this.game, 1815, 15, "menu-button-fullscreen", () => this.toggleFullscreen()); + let button = new Phaser.Button(this.game, this.getWidth(), 0, "options-options", () => this.options.toggleBoolean("fullscreen"), null, 2, 2); + button.anchor.set(1, 0); this.tooltip.bindStaticText(button, "Toggle full-screen"); this.layer_title.add(button); @@ -65,13 +66,7 @@ module TS.SpaceTac.UI { } addButton(x: number, y: number, caption: string, tooltip: string, callback: Function): Phaser.Button { - var button = this.add.button(x - 20, y + 20, "menu-button", () => { - let fullscreen = this.getStorage("fullscreen"); - if (fullscreen == "true") { - this.toggleFullscreen(true); - } - callback(); - }); + var button = this.add.button(x - 20, y + 20, "menu-button", callback); button.anchor.set(0.5, 0); button.input.useHandCursor = true; diff --git a/src/ui/options/GameOptions.ts b/src/ui/options/GameOptions.ts new file mode 100644 index 0000000..eaea14c --- /dev/null +++ b/src/ui/options/GameOptions.ts @@ -0,0 +1,98 @@ +module TS.SpaceTac.UI { + class GameOption { + code: string + current: T + getter: () => T + setter: (value: T) => any + + constructor(code: string, getter: () => T, setter: (value: T) => any) { + this.code = code; + this.getter = getter; + this.setter = setter; + this.current = getter(); + } + + set(value: T) { + this.setter(value); + this.current = this.getter(); + } + } + + /** + * Object to store and maintain game-wide options + * + * Options are kept on the browser storage when possible + */ + export class GameOptions { + booleans: { [code: string]: GameOption } + numbers: { [code: string]: GameOption } + + constructor(parent: MainUI) { + this.booleans = { + fullscreen: new GameOption("fullscreen", () => parent.isFullscreen(), value => parent.toggleFullscreen(value)), + } + this.numbers = { + mainvolume: new GameOption("mainvolume", () => parent.audio.getMainVolume(), value => parent.audio.setMainVolume(value)), + musicvolume: new GameOption("musicvolume", () => parent.audio.getMusicVolume(), value => parent.audio.setMusicVolume(value)), + } + } + + /** + * Get the current value of a boolean option + */ + getBooleanValue(code: string, default_value = false): boolean { + let option = this.booleans[code]; + if (option) { + return option.current; + } else { + return default_value; + } + } + + /** + * Set the current value of a boolean option + */ + setBooleanValue(code: string, value: boolean): boolean { + let option = this.booleans[code]; + if (option) { + option.set(value); + return true; + } else { + return false; + } + } + + /** + * Toggle a boolean value between true and false + */ + toggleBoolean(code: string): boolean { + this.setBooleanValue(code, !this.getBooleanValue(code)); + return this.getBooleanValue(code); + } + + /** + * Get the current value of a number option + */ + getNumberValue(code: string, default_value = 0): number { + let option = this.numbers[code]; + if (option) { + return option.current; + } else { + return default_value; + } + } + + /** + * Set the current value of a number option + */ + setNumberValue(code: string, value: number): boolean { + let option = this.numbers[code]; + if (option) { + option.set(value); + return true; + } else { + return false; + } + } + } +} diff --git a/src/ui/options/OptionsDialog.ts b/src/ui/options/OptionsDialog.ts new file mode 100644 index 0000000..f02d53e --- /dev/null +++ b/src/ui/options/OptionsDialog.ts @@ -0,0 +1,38 @@ +/// + +module TS.SpaceTac.UI { + /** + * Dialog to display game options + */ + export class OptionsDialog extends UIDialog { + constructor(parent: BaseView) { + super(parent, 1453, 1080, "options-background"); + + this.addCloseButton("common-dialog-close", 1304, 131, 0, 1); + + let toggle = this.addToggleButton(415, 381, + { key: "options-toggle", frame: 0, frame1: 1, frame2: 2 }, + { key: "options-options", frame: 0 }, + toggled => parent.options.setNumberValue("mainvolume", toggled ? 1 : 0)); + toggle(parent.options.getNumberValue("mainvolume") > 0); + + toggle = this.addToggleButton(this.width / 2, 381, + { key: "options-toggle", frame: 0, frame1: 1, frame2: 2 }, + { key: "options-options", frame: 1 }, + toggled => parent.options.setNumberValue("musicvolume", toggled ? 1 : 0)); + toggle(parent.options.getNumberValue("musicvolume") > 0); + + toggle = this.addToggleButton(this.width - 415, 381, + { key: "options-toggle", frame: 0, frame1: 1, frame2: 2 }, + { key: "options-options", frame: 2 }, + toggled => parent.options.setBooleanValue("fullscreen", toggled)); + toggle(parent.options.getBooleanValue("fullscreen")); + + this.addButton(this.width / 2, 600, () => null, "options-button"); + this.addText(this.width / 2, 600, "Invite a friend", "#5398e9", 36, true, true); + + this.addButton(this.width / 2, 800, () => parent.gameui.quitGame(), "options-button"); + this.addText(this.width / 2, 800, "Quit to menu", "#5398e9", 36, true, true); + } + } +}