diff --git a/.gitignore b/.gitignore index c92b40f..d5105fe 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ _*.* media/icons/* !media/icons/app.* !media/icons/default_icon.png -!media/icons/w3dhub.png \ No newline at end of file +!media/icons/w3dhub.png +media/banners/* +!media/banners/.gitkeep \ No newline at end of file diff --git a/lib/application_manager.rb b/lib/application_manager.rb index 7e5abb6..7416196 100644 --- a/lib/application_manager.rb +++ b/lib/application_manager.rb @@ -133,13 +133,16 @@ class W3DHub "open" end + # TODO: Change if this correct on Linux + user_data_path = "#{Dir.home}/Documents/W3D Hub/games/#{app_id}-#{channel}" + path = case type when :installation app_data[:install_directory] when :user_data - app_data[:install_directory] + user_data_path when :screenshots - app_data[:install_directory] + Dir.exist?("#{user_data_path}/Screenshots") ? "#{user_data_path}/Screenshots" : user_data_path else raise "Unknown folder type: #{type.inspect}" end diff --git a/lib/gui_state_ext.rb b/lib/gui_state_ext.rb new file mode 100644 index 0000000..2fa7c7d --- /dev/null +++ b/lib/gui_state_ext.rb @@ -0,0 +1,52 @@ +module CyberarmEngine + class GuiState < CyberarmEngine::GameState + def menu(host_element, items:, width: 200) + container = CyberarmEngine::Element::Stack.new( + parent: host_element.parent, + width: width, + theme: W3DHub::THEME, + border_color: 0xff_000000, + border_thickness: 1 + ) + + container.instance_variable_set(:"@__menu", host_element) + + container.define_singleton_method(:recalculate_menu) do + @x = @__menu.x + @y = @__menu.y + @__menu.height + + @y = @__menu.y - height if @y + height > window.height + end + + def container.recalculate + super + + recalculate_menu + end + + items.each do |item| + btn = CyberarmEngine::Element::Button.new( + item[:label], + { + parent: container, + width: 1.0, + text_align: :left, + theme: W3DHub::THEME, + border_thickness: 0, + margin: 0 + }, + proc do + item[:block]&.call + end + ) + container.add(btn) + end + + container.recalculate + container.recalculate + container.recalculate + + show_menu(container) + end + end +end diff --git a/lib/page.rb b/lib/page.rb index 13cd42d..0025336 100644 --- a/lib/page.rb +++ b/lib/page.rb @@ -47,5 +47,9 @@ class W3DHub def button_up(id) end + + def menu(host_element, items:) + @host.menu(host_element, items: items) + end end end \ No newline at end of file diff --git a/lib/pages/games_redesign.rb b/lib/pages/games_redesign.rb index b9565c1..3124091 100644 --- a/lib/pages/games_redesign.rb +++ b/lib/pages/games_redesign.rb @@ -18,7 +18,7 @@ class W3DHub end # Game Menu - @game_page_container = stack(width: 1.0, fill: true) do + @game_page_container = stack(width: 1.0, fill: true, background_image: "#{GAME_ROOT_PATH}/media/textures/noiseb.png", background_image_mode: :tiled) do # , background_image: "C:/Users/cyber/Downloads/vlcsnap-2022-04-24-22h24m15s854.png" end end @@ -46,9 +46,8 @@ class W3DHub image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{game.id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png" image_color = Store.application_manager.installed?(game.id, game.channels.first.id) ? 0xff_ffffff : 0x66_ffffff - flow(width: 1.0, height: 1.0, margin: 8, background_image: image_path, background_image_color: image_color, background_image_mode: :fill) do - image "#{GAME_ROOT_PATH}/media/ui_icons/return.png", width: 24, margin_left: -6, margin_top: -6, color: 0xdd_ff8844 if Store.application_manager.updateable?(game.id, game.channels.first.id) - image "#{GAME_ROOT_PATH}/media/ui_icons/import.png", width: 24, margin_left: -4, margin_top: -6, color: 0xdd_ffffff unless Store.application_manager.installed?(game.id, game.channels.first.id) + flow(width: 1.0, height: 1.0, margin: 8, background_image: image_path, background_image_color: image_color, background_image_mode: :fill_height) do + image "#{GAME_ROOT_PATH}/media/ui_icons/import.png", width: 24, margin_left: -4, margin_top: -6, color: 0xff_ff8800 if Store.application_manager.updateable?(game.id, game.channels.first.id) end # inscription game.name, width: 1.0, text_align: :center, text_size: 14 @@ -91,23 +90,21 @@ class W3DHub image_path = "#{GAME_ROOT_PATH}/media/banners/#{game.id}.png" if File.exist?(image_path) - stack(width: 360-8, height: 200, margin: 8, background_image: image_path, background_image_mode: :fill_width) + image image_path, width: 1.0 else - stack(width: 360-8, height: 200, padding: 8) do - banner game.name unless File.exist?(image_path) - end + banner game.name unless File.exist?(image_path) end - stack(width: 1.0, fill: true, scroll: true) do + stack(width: 1.0, fill: true, scroll: true, margin_top: 32) do if Store.application_manager.installed?(game.id, channel.id) Hash.new.tap { |hash| - hash[I18n.t(:"games.game_settings")] = { icon: "gear", block: proc { Store.application_manager.settings(game.id, channel.id) } } - hash[I18n.t(:"games.wine_configuration")] = { icon: "gear", block: proc { Store.application_manager.wine_configuration(game.id, channel.id) } } if W3DHub.unix? - hash[I18n.t(:"games.game_modifications")] = { icon: "gear", enabled: true, block: proc { populate_game_modifications(game, channel) } } - if game.id != "ren" - hash[I18n.t(:"games.repair_installation")] = { icon: "wrench", block: proc { Store.application_manager.repair(game.id, channel.id) } } - hash[I18n.t(:"games.uninstall_game")] = { icon: "trashCan", block: proc { Store.application_manager.uninstall(game.id, channel.id) } } - end + # hash[I18n.t(:"games.game_settings")] = { icon: "gear", block: proc { Store.application_manager.settings(game.id, channel.id) } } + # hash[I18n.t(:"games.wine_configuration")] = { icon: "gear", block: proc { Store.application_manager.wine_configuration(game.id, channel.id) } } if W3DHub.unix? + # hash[I18n.t(:"games.game_modifications")] = { icon: "gear", enabled: true, block: proc { populate_game_modifications(game, channel) } } + # if game.id != "ren" + # hash[I18n.t(:"games.repair_installation")] = { icon: "wrench", block: proc { Store.application_manager.repair(game.id, channel.id) } } + # hash[I18n.t(:"games.uninstall_game")] = { icon: "trashCan", block: proc { Store.application_manager.uninstall(game.id, channel.id) } } + # end hash[I18n.t(:"games.install_folder")] = { icon: nil, block: proc { Store.application_manager.show_folder(game.id, channel.id, :installation) } } hash[I18n.t(:"games.user_data_folder")] = { icon: nil, block: proc { Store.application_manager.show_folder(game.id, channel.id, :user_data) } } hash[I18n.t(:"games.view_screenshots")] = { icon: nil, block: proc { Store.application_manager.show_folder(game.id, channel.id, :screenshots) } } @@ -134,6 +131,9 @@ class W3DHub if game.channels.count > 1 # Release channel + + inscription I18n.t(:"games.game_version"), width: 1.0, text_align: :center + flow(width: 1.0, height: 48) do # background 0xff_444411 list_box(width: 1.0, items: game.channels.map(&:name), choose: channel.name, enabled: game.channels.count > 1) do |value| @@ -161,6 +161,22 @@ class W3DHub Store.application_manager.run(game.id, channel.id) end + button get_image("#{GAME_ROOT_PATH}/media/ui_icons/gear.png"), tip: I18n.t(:"games.game_options"), image_height: 32, margin_left: 0 do |btn| + items = [] + + items << { label: I18n.t(:"games.game_settings"), block: proc { Store.application_manager.settings(game.id, channel.id) } } + items << { label: I18n.t(:"games.wine_configuration"), block: proc { Store.application_manager.wine_configuration(game.id, channel.id) } } if W3DHub.unix? + items << { label: I18n.t(:"games.game_modifications"), block: proc { populate_game_modifications(game, channel) } } + if game.id != "ren" + items << { label: I18n.t(:"games.repair_installation"), block: proc { Store.application_manager.repair(game.id, channel.id) } } + items << { label: I18n.t(:"games.uninstall_game"), block: proc { Store.application_manager.uninstall(game.id, channel.id) } } + end + + # From gui_state_ext.rb + # TODO: Implement in engine proper + menu(btn, items: items) + end + else installing = Store.application_manager.task?(:installer, game.id, channel.id) @@ -277,7 +293,7 @@ class W3DHub # Detailed view news_blurb_container = stack(width: 1.0, height: 1.0, background: 0xaa_000000, padding: 4) do tagline "#{item.title}", width: 1.0 - inscription item.timestamp.strftime("%Y-%m-%d") + inscription "#{item.author} • #{item.timestamp.strftime("%Y-%m-%d")}" inscription item.blurb.gsub(/\n+/, "\n").strip[0..1024], fill: true button I18n.t(:"games.read_more"), width: 1.0, margin_top: 8, margin_bottom: 0, padding_top: 4, padding_bottom: 4 do diff --git a/lib/pages/server_browser.rb b/lib/pages/server_browser.rb index ee3173f..92e9c9e 100644 --- a/lib/pages/server_browser.rb +++ b/lib/pages/server_browser.rb @@ -62,11 +62,19 @@ class W3DHub # button get_image("#{GAME_ROOT_PATH}/media/ui_icons/return.png"), tip: I18n.t(:"server_browser.refresh"), image_height: 1.0, margin_left: 16, padding_left: 2, padding_right: 2, padding_top: 2, padding_bottom: 2 do # fetch_server_list # end + + flow(fill: true) + + button "Direct Connect", height: 1.0, padding_top: 4, padding_bottom: 4, enabled: false, tip: "Directly connect to a game server (under development)" do + push_state(W3DHub::States::DirectConnectDialog) + end end - flow(min_width: 372, width: 0.38, max_width: 512, height: 1.0) do - inscription "#{I18n.t(:"server_browser.nickname")}:", width: 0.32 - @nickname_label = inscription "#{Store.settings[:server_list_username]}", width: 0.6 + flow(min_width: 372, width: 0.38, max_width: 512, height: 1.0) do |container| + flow(fill: true) + + inscription "#{I18n.t(:"server_browser.nickname")}:" + @nickname_label = inscription "#{Store.settings[:server_list_username]}" image "#{GAME_ROOT_PATH}/media/ui_icons/wrench.png", height: 16, hover: { color: 0xaa_ffffff }, tip: I18n.t(:"server_browser.set_nickname") do # Prompt for player name prompt_for_nickname( @@ -74,6 +82,10 @@ class W3DHub @nickname_label.value = entry Store.settings[:server_list_username] = entry Store.settings.save_settings + + container.recalculate + container.recalculate + container.recalculate end ) end @@ -248,16 +260,21 @@ class W3DHub stack(width: 1.0, height: 1.0, padding: 8) do stack(width: 1.0, height: 220) do flow(width: 1.0, height: 0.2) do + flow(fill: true) + image game_icon(server), width: 0.05 - tagline server.status.name, width: 0.949, text_wrap: :none + tagline server.status.name, text_wrap: :none + + flow(fill: true) end - stack(width: 1.0, height: 0.2) do + flow(width: 1.0, height: 0.2) do game_installed = Store.application_manager.installed?(server.game, server.channel) game_updatable = Store.application_manager.updateable?(server.game, server.channel) style = server.channel != "release" ? TESTING_BUTTON : {} - button "#{I18n.t(:"server_browser.join_server")}", margin_left: 96, enabled: (game_installed && !game_updatable), **style do + flow(fill: true) + button "#{I18n.t(:"server_browser.join_server")}", enabled: (game_installed && !game_updatable), **style do # Check for nickname # prompt for nickname # !abort unless nickname set @@ -294,6 +311,13 @@ class W3DHub end end end + + if Store.developer_mode + list_box(items: (1..12).to_a.map(&:to_s), margin_left: 16, **TESTING_BUTTON) + button "Multijoin", tip: "Launch multiple clients with configured username_\#{number}", **TESTING_BUTTON, enabled: true + end + + flow(fill: true) end # Server Info @@ -329,17 +353,22 @@ class W3DHub # Game score and balance display flow(width: 1.0, height: 48, border_thickness_bottom: 2, border_color_bottom: 0x44_ffffff) do - stack(width: 0.4, height: 1.0) do + stack(fill: true, height: 1.0) do para "#{server.status.teams[0].name} (#{server.status.players.select { |pl| pl.team == 0 }.count})", width: 1.0, text_align: :center para formatted_score(game_balance[:team_0_score].to_i), width: 1.0, text_align: :center end stack(width: 0.2, height: 1.0) do - image game_balance[:icon], height: 0.5, margin_left: 20, tip: game_balance[:message], color: game_balance[:color] + flow(width: 1.0, height: 0.5) do + flow(fill: true) + image game_balance[:icon], height: 1.0, tip: game_balance[:message], color: game_balance[:color] + flow(fill: true) + end + para game_balance[:ratio].round(2).to_s, width: 1.0, text_align: :center end - stack(width: 0.4, height: 1.0) do + stack(fill: true, height: 1.0) do para "#{server.status.teams[1].name} (#{server.status.players.select { |pl| pl.team == 1 }.count})", width: 1.0, text_align: :center para formatted_score(game_balance[:team_1_score].to_i), width: 1.0, text_align: :center end @@ -347,7 +376,7 @@ class W3DHub # Team roster flow(width: 1.0, fill: true, scroll: true) do - stack(width: 0.499) do + stack(width: 0.5) do server.status.players.select { |ply| ply.team == 0 }.sort_by { |ply| ply.score }.reverse.each_with_index do |player, i| flow(width: 1.0, height: 18) do background 0xff_333333 if i.even? diff --git a/lib/states/direct_connect_dialog.rb b/lib/states/direct_connect_dialog.rb new file mode 100644 index 0000000..0494dc8 --- /dev/null +++ b/lib/states/direct_connect_dialog.rb @@ -0,0 +1,188 @@ +class W3DHub + class States + class DirectConnectDialog < CyberarmEngine::GuiState + def setup + window.show_cursor = true + + theme(W3DHub::THEME) + + background 0xee_444444 + + stack(width: 1.0, height: 1.0, margin: 128, background: 0xee_222222) do + # Title bar + flow(width: 1.0, height: 32, padding: 8) do + background 0x88_000000 + + image "#{GAME_ROOT_PATH}/media/ui_icons/export.png", width: 32, align: :center, color: 0xaa_ffffff + + tagline "#{I18n.t(:"server_browser.direct_connect")}", fill: true, text_align: :center + end + + stack(width: 1.0, fill: true, scroll: true) do + stack(width: 1.0, height: 60, margin_left: 8, margin_right: 8) do + para "Server profiles", text_align: :center, width: 1.0 + + flow(width: 1.0, fill: true) do + list = [""] # window.config.server_profiles.count.positive? ? window.config.server_profiles.map { |pf| pf.name }.insert(0, "") : [""] + + @server_profiles_list = list_box items: list, fill: true, height: 1.0 + @server_profiles_list.subscribe(:changed) do |list| + list.items.delete("") if list.value != "" + + profile = window.config.server_profiles.find { |pf| pf.name == list.value } + populate_from_server_profile(profile ? profile : window.config.settings) + + valid_for_multiplayer? + end + + button get_image("#{GAME_ROOT_PATH}/media/ui_icons/plus.png"), image_height: 1.0, tip: "Create new profile" do + push_state(ServerProfileForm, save_callback: method(:save_server_profile)) + end + + @server_delete_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/minus.png"), image_height: 1.0, tip: "Remove selected profile" do + push_state(ConfirmDialog, message: "Purge server profile") + end + + @server_edit_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/gear.png"), image_height: 1.0, tip: "Edit and save selected profile" do + push_state(ServerProfileForm, editing: window.config.server_profiles.find { |pf| pf.name == @server_profiles_list.value }, save_callback: method(:save_server_profile)) + end + end + end + + stack(width: 1.0, fill: true, margin_top: 8, padding: 8, border_color: 0xff_111111, border_thickness: 1) do + flow(width: 1.0, height: 60) do + stack(width: 0.5, height: 1.0) do + para "Nickname:" + @server_nickname = edit_line "", width: 1.0, fill: true + @server_nickname.subscribe(:changed) do |e| + @changes_made = true if @server_profiles_list.value.length.positive? + + valid_for_multiplayer? + end + end + + stack(width: 0.5, height: 1.0) do + para "Server Password:" + @server_password = edit_line "", width: 1.0, fill: true, margin_left: 4, type: :password + @server_password.subscribe(:changed) do |e| + @changes_made = true if @server_profiles_list.value.length.positive? + + valid_for_multiplayer? + end + end + end + + flow(width: 1.0, height: 60) do + stack(width: 0.5, height: 1.0) do + para "Server IP or Hostname:" + @server_hostname = edit_line "", width: 1.0, fill: true + @server_hostname.subscribe(:changed) do |e| + @changes_made = true if @server_profiles_list.value.length.positive? + + valid_for_multiplayer? + end + end + + stack(width: 0.5, height: 1.0) do + para "Server Port:" + @server_port = edit_line "", width: 1.0, fill: true, margin_left: 4 + @server_port.subscribe(:changed) do |e| + @changes_made = true if @server_profiles_list.value.length.positive? + + valid_for_multiplayer? + end + end + end + + stack(width: 1.0, height: 60) do + para "Game or Mod:" + + flow(width: 1.0, fill: true) do + list = [""] # window.config.games.count.positive? ? window.config.games.map { |g| g.title } : [""] + + @games_list = list_box items: list, fill: true, height: 1.0 + @games_list.subscribe(:changed) do |list| + list.items.delete("") if list.value != "" + + @changes_made = true if @server_profiles_list.value.length.positive? + + valid_for_multiplayer? + end + + button get_image("#{GAME_ROOT_PATH}/media/ui_icons/plus.png"), image_height: 1.0, tip: "Add game" do + push_state(GameForm, save_callback: method(:save_game)) + end + + @game_delete_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/minus.png"), image_height: 1.0, tip: "Remove selected game" do + push_state(ConfirmDialog, message: "Remove game?") + end + + @game_edit_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/gear.png"), image_height: 1.0, tip: "Edit selected game" do + push_state(GameForm, editing: window.config.games.find { |g| g.title == @games_list.value }, save_callback: method(:save_game)) + end + end + end + + stack(width: 1.0, height: 60) do + para "Launch arguments (Optional):" + @launch_arguments = edit_line "", width: 1.0, fill: true + @launch_arguments.subscribe(:changed) do |e| + @changes_made = true if @server_profiles_list.value.length.positive? + + valid_for_multiplayer? + end + end + + stack(width: 1.0, height: 60) do + para "IRC Profile:" + + flow(width: 1.0, fill: true) do + @irc_profiles_list = list_box items: ["None"], fill: true, height: 1.0 + @irc_profiles_list.subscribe(:changed) do |list| + @changes_made = true if @server_profiles_list.value.length.positive? + + valid_for_multiplayer? + end + + button get_image("#{GAME_ROOT_PATH}/media/ui_icons/plus.png"), image_height: 1.0, tip: "Add IRC profile" do + push_state(IRCProfileForm, save_callback: method(:save_irc_profile)) + end + + @irc_delete_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/minus.png"), image_height: 1.0, tip: "Remove selected IRC profile" do + push_state(ConfirmDialog, message: "") + end + + @irc_edit_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/gear.png"), image_height: 1.0, tip: "Edit selected IRC profile" do + push_state(IRCProfileForm, editing: window.config.irc_profiles.find { |pf| pf.name == @irc_profiles_list.value }, save_callback: method(:save_irc_profile)) + end + end + end + end + end + + flow(width: 1.0, height: 40, padding: 8) do + button "Cancel", width: 0.25 do + pop_state + @options[:cancel_callback]&.call + end + + stack(fill: true) + + button "Connect", width: 0.25 do + pop_state + @options[:accept_callback]&.call + end + end + end + end + + def draw + previous_state&.draw + + Gosu.flush + + super + end + end + end +end diff --git a/lib/states/prompt_dialog.rb b/lib/states/prompt_dialog.rb index 7602732..f7d191e 100644 --- a/lib/states/prompt_dialog.rb +++ b/lib/states/prompt_dialog.rb @@ -17,7 +17,7 @@ class W3DHub image "#{GAME_ROOT_PATH}/media/ui_icons/question.png", width: 32, align: :center, color: 0xff_ff8800 - tagline "#{@options[:title]}", width: 0.9, text_align: :center + tagline "#{@options[:title]}", fill: true, text_align: :center end stack(width: 1.0, fill: true, padding: 16) do diff --git a/lib/window.rb b/lib/window.rb index 96df093..2907b6f 100644 --- a/lib/window.rb +++ b/lib/window.rb @@ -20,6 +20,7 @@ class W3DHub # push_state(W3DHub::States::DemoInputDelay) # push_state(W3DHub::States::Welcome) push_state(W3DHub::States::Boot) + # push_state(W3DHub::States::DirectConnectDialog) end def update diff --git a/locales/en.yml b/locales/en.yml index 14a8383..e07fa9a 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -26,6 +26,8 @@ en: settings: Settings games: game_settings: Game Settings + game_options: Game Options + game_version: Game Version wine_configuration: Wine Configuration game_modifications: Game Modifications repair_installation: Repair Installation @@ -38,6 +40,7 @@ en: channel: Channel version: Version server_browser: + direct_connect: Direct Connect refresh: Refresh join_server: Join Server game: Game diff --git a/media/textures/noise.png b/media/textures/noise.png new file mode 100644 index 0000000..4ae484c Binary files /dev/null and b/media/textures/noise.png differ diff --git a/media/textures/noiseb.png b/media/textures/noiseb.png new file mode 100644 index 0000000..8d2d104 Binary files /dev/null and b/media/textures/noiseb.png differ diff --git a/media/textures/noisec.png b/media/textures/noisec.png new file mode 100644 index 0000000..ab662df Binary files /dev/null and b/media/textures/noisec.png differ diff --git a/media/textures/noised.png b/media/textures/noised.png new file mode 100644 index 0000000..65b6e8c Binary files /dev/null and b/media/textures/noised.png differ diff --git a/media/ui_icons/plus.png b/media/ui_icons/plus.png new file mode 100644 index 0000000..3f5cf37 Binary files /dev/null and b/media/ui_icons/plus.png differ diff --git a/w3d_hub_linux_launcher.rb b/w3d_hub_linux_launcher.rb index d1c8c30..faa896f 100644 --- a/w3d_hub_linux_launcher.rb +++ b/w3d_hub_linux_launcher.rb @@ -53,6 +53,9 @@ require "protocol/websocket/connection" I18n.load_path << Dir["#{W3DHub::GAME_ROOT_PATH}/locales/*.yml"] I18n.default_locale = :en +# GUI_DEBUG = true +require_relative "lib/gui_state_ext" + require_relative "lib/version" require_relative "lib/theme" require_relative "lib/common" @@ -82,6 +85,7 @@ require_relative "lib/states/welcome" require_relative "lib/states/message_dialog" require_relative "lib/states/prompt_dialog" require_relative "lib/states/confirm_dialog" +require_relative "lib/states/direct_connect_dialog" require_relative "lib/api" require_relative "lib/api/service_status"