diff --git a/docs/builds.md b/docs/builds.md index 853c8862..931368b7 100644 --- a/docs/builds.md +++ b/docs/builds.md @@ -33,6 +33,7 @@ open -a "Hoppy’s Adventure Arcade.app" --args -- --booth --route=heroes_vs_vil ``` - **Fullscreen.** The game starts fullscreen without changing the saved settings. +- **Booth QR code.** A scannable, branded CodeRabbit link stays in the top-right corner on every route in booth mode. The provided code points to `https://coderabbit.link/ato26-ad?qr=1`, which identifies QR scans for attribution. To change the destination, regenerate `hoppys-adventure-arcade/Assets/booth/booth_qr.png` from the repository root with `swift hoppys-adventure-arcade/tools/booth/generate_qr.swift` and pass the output path and URL as optional arguments, then rebuild the game. - **Idle reset.** After 90 seconds with no keyboard, mouse, touch or controller input, any game returns to the attract route. That is the hub, whose rotating previews act as the attract screen, unless `--attract=` names `beatem_up` or `runner`, the other menus that start their own music. - **Game over.** A lost game doesn't offer unlimited retries. A player gets `--retries=` retries (default 0); after that, the game shows no retry screen. Instead a **GAME OVER** card thanks the player, shows their score and counts down six seconds, then the attract route opens for the next player. The count starts again whenever the attract route opens. Every game takes part: Heroes vs. Villains, Code City Brawl, the platformer levels and the Endless Runner. Without `--booth`, every game keeps its own retry screen. - **No quitting from the menu.** The menu's **Quit Game** does nothing in booth mode; staff close the window (Cmd+Q, Alt+F4). diff --git a/hoppys-adventure-arcade/Assets/booth/booth_qr.png b/hoppys-adventure-arcade/Assets/booth/booth_qr.png new file mode 100644 index 00000000..377fd627 Binary files /dev/null and b/hoppys-adventure-arcade/Assets/booth/booth_qr.png differ diff --git a/hoppys-adventure-arcade/Assets/booth/booth_qr.png.import b/hoppys-adventure-arcade/Assets/booth/booth_qr.png.import new file mode 100644 index 00000000..b55ec3a6 --- /dev/null +++ b/hoppys-adventure-arcade/Assets/booth/booth_qr.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b10olno1qij0m" +path="res://.godot/imported/booth_qr.png-5aaeaad2f8935f6369e746678fa82863.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/booth/booth_qr.png" +dest_files=["res://.godot/imported/booth_qr.png-5aaeaad2f8935f6369e746678fa82863.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/hoppys-adventure-arcade/Assets/booth/booth_qr_logo.png b/hoppys-adventure-arcade/Assets/booth/booth_qr_logo.png new file mode 100644 index 00000000..e8179d9b Binary files /dev/null and b/hoppys-adventure-arcade/Assets/booth/booth_qr_logo.png differ diff --git a/hoppys-adventure-arcade/Assets/booth/booth_qr_logo.png.import b/hoppys-adventure-arcade/Assets/booth/booth_qr_logo.png.import new file mode 100644 index 00000000..a46fda9a --- /dev/null +++ b/hoppys-adventure-arcade/Assets/booth/booth_qr_logo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dp1njvgw18v7c" +path="res://.godot/imported/booth_qr_logo.png-8be0cf10ff0693dea7aa858282c39071.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/booth/booth_qr_logo.png" +dest_files=["res://.godot/imported/booth_qr_logo.png-8be0cf10ff0693dea7aa858282c39071.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/hoppys-adventure-arcade/app/app_router.gd b/hoppys-adventure-arcade/app/app_router.gd index 0d08792a..226ed1e7 100644 --- a/hoppys-adventure-arcade/app/app_router.gd +++ b/hoppys-adventure-arcade/app/app_router.gd @@ -3,6 +3,7 @@ const BoothLog := preload("res://app/booth_log.gd") const BoothHeartbeat := preload("res://app/booth_heartbeat.gd") const NextPlayerCard := preload("res://app/next_player_card.gd") const BoothSoak := preload("res://app/booth_soak.gd") +const BoothQrOverlay := preload("res://app/booth_qr_overlay.gd") const ROUTES := { "heroes_vs_villains": "res://scenes/beatem_up/reinvent/heroes_vs_villains.tscn", "code_city_brawl": "res://scenes/beatem_up/code_city_brawl.tscn", @@ -65,6 +66,7 @@ var next_player: NextPlayerCard var next_player_left := 0.0 ## The soak test (app/booth_soak.gd), when the game was launched with `--soak=`. var soak: BoothSoak +var booth_qr: Control func _ready() -> void: # The veil tween must keep running while a microgame has the tree paused. process_mode = Node.PROCESS_MODE_ALWAYS @@ -75,6 +77,9 @@ func _ready() -> void: veil.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) veil.mouse_filter = Control.MOUSE_FILTER_IGNORE veil.modulate.a = 0 + booth_qr = BoothQrOverlay.new() + booth_qr.visible = false + add_child(booth_qr) add_child(veil) var args := OS.get_cmdline_user_args() if SMOKE_FLAG in args: @@ -239,6 +244,8 @@ func set_booth(enabled: bool) -> void: # A card counting down over a paused, finished game would strand it: hand off right away. var finishing := not enabled and next_player != null and next_player.visible booth = enabled + if booth_qr: + booth_qr.visible = enabled idle_seconds = 0.0 AudioService.set_attract(enabled) game_overs = 0 diff --git a/hoppys-adventure-arcade/app/booth_qr_overlay.gd b/hoppys-adventure-arcade/app/booth_qr_overlay.gd new file mode 100644 index 00000000..28cdcb2b --- /dev/null +++ b/hoppys-adventure-arcade/app/booth_qr_overlay.gd @@ -0,0 +1,56 @@ +extends Control + +const QR_TEXTURE: Texture2D = preload("res://Assets/booth/booth_qr.png") + +func _ready() -> void: + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + mouse_filter = Control.MOUSE_FILTER_IGNORE + var panel := PanelContainer.new() + panel.name = "BoothQrPanel" + panel.set_anchors_preset(Control.PRESET_TOP_RIGHT) + panel.offset_left = -208 + panel.offset_top = 24 + panel.offset_right = -24 + panel.offset_bottom = 230 + panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + var panel_style := StyleBoxFlat.new() + panel_style.bg_color = Color("fffaf2") + panel_style.border_color = Color("182432") + panel_style.set_border_width_all(2) + panel_style.set_corner_radius_all(8) + panel_style.content_margin_left = 10 + panel_style.content_margin_right = 10 + panel_style.content_margin_top = 8 + panel_style.content_margin_bottom = 8 + panel.add_theme_stylebox_override("panel", panel_style) + add_child(panel) + + var content := VBoxContainer.new() + content.alignment = BoxContainer.ALIGNMENT_CENTER + content.add_theme_constant_override("separation", 4) + content.mouse_filter = Control.MOUSE_FILTER_IGNORE + panel.add_child(content) + + var heading := Label.new() + heading.text = "SCAN TO LEARN MORE" + heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + heading.add_theme_font_size_override("font_size", 11) + heading.add_theme_color_override("font_color", Color("182432")) + heading.mouse_filter = Control.MOUSE_FILTER_IGNORE + content.add_child(heading) + + var code := TextureRect.new() + code.texture = QR_TEXTURE + code.custom_minimum_size = Vector2(148, 148) + code.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + code.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + code.mouse_filter = Control.MOUSE_FILTER_IGNORE + content.add_child(code) + + var attribution := Label.new() + attribution.text = "CODERABBIT.LINK" + attribution.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + attribution.add_theme_font_size_override("font_size", 10) + attribution.add_theme_color_override("font_color", Color("536271")) + attribution.mouse_filter = Control.MOUSE_FILTER_IGNORE + content.add_child(attribution) diff --git a/hoppys-adventure-arcade/app/booth_qr_overlay.gd.uid b/hoppys-adventure-arcade/app/booth_qr_overlay.gd.uid new file mode 100644 index 00000000..ad0e3809 --- /dev/null +++ b/hoppys-adventure-arcade/app/booth_qr_overlay.gd.uid @@ -0,0 +1 @@ +uid://bxotudq7alix7 diff --git a/hoppys-adventure-arcade/tools/booth/generate_qr.swift b/hoppys-adventure-arcade/tools/booth/generate_qr.swift new file mode 100644 index 00000000..c20a7e5c --- /dev/null +++ b/hoppys-adventure-arcade/tools/booth/generate_qr.swift @@ -0,0 +1,45 @@ +import AppKit +import CoreImage.CIFilterBuiltins +import Foundation + +let defaultURL = "https://coderabbit.link/ato26-ad?qr=1" +let scriptDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent() +let projectDirectory = scriptDirectory.deletingLastPathComponent().deletingLastPathComponent() +let defaultOutput = projectDirectory.appendingPathComponent("Assets/booth/booth_qr.png").path +let logoPath = projectDirectory.appendingPathComponent("Assets/booth/booth_qr_logo.png").path +let output = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : defaultOutput +let value = CommandLine.arguments.count > 2 ? CommandLine.arguments[2] : defaultURL + +let filter = CIFilter.qrCodeGenerator() +filter.message = Data(value.utf8) +filter.correctionLevel = "H" + +guard let image = filter.outputImage?.transformed(by: CGAffineTransform(scaleX: 12, y: 12)) else { + fatalError("Could not generate QR code") +} +let qr = NSBitmapImageRep(ciImage: image) +guard let logo = NSImage(contentsOfFile: logoPath), + let logoTiff = logo.tiffRepresentation, + let logoBitmap = NSBitmapImageRep(data: logoTiff) else { + fatalError("Could not load QR or CodeRabbit logo image") +} + +let size = NSSize(width: qr.pixelsWide, height: qr.pixelsHigh) +let composite = NSImage(size: size) +composite.lockFocus() +NSColor.white.setFill() +NSRect(origin: .zero, size: size).fill() +qr.draw(in: NSRect(origin: .zero, size: size)) +let logoSize = size.width * 0.223 +let logoRect = NSRect(x: (size.width - logoSize) / 2, y: (size.height - logoSize) / 2, + width: logoSize, height: logoSize) +logoBitmap.draw(in: logoRect) +composite.unlockFocus() + +guard let tiff = composite.tiffRepresentation, + let bitmap = NSBitmapImageRep(data: tiff), + let png = bitmap.representation(using: .png, properties: [:]) else { + fatalError("Could not encode QR code as PNG") +} +try png.write(to: URL(fileURLWithPath: output)) +print("Wrote \(output) for \(value)")