Add browser gamepad support with pad button pictograms

Co-Authored-By: deepseek-v4.1-flash (opencode-go) via CodeCraft
This commit is contained in:
Richard Zinkanddeepseek-v4.1-flash (opencode-go) via CodeCraft committed 2026-11-11 14:43:39 +01:00
1 parent 073999d592
commit 8d51d6ec6a
26 files changed
+1066 -22

No files matched your search

+19
View File
@@ -440,6 +440,25 @@ help.t_long = Dlouhý stisk: informace o místě; ruka: položí tam jednotky
help.t_double = Dvojité klepnutí rukou: pohlavek vlastním, pěst hrdinům na tvém území help.t_double = Dvojité klepnutí rukou: pohlavek vlastním, pěst hrdinům na tvém území
help.t_tools = Lišta nástrojů: kategorie otevřou řadu nástrojů; tlačítko menu otevře menu pauzy help.t_tools = Lišta nástrojů: kategorie otevřou řadu nástrojů; tlačítko menu otevře menu pauzy
help.start_touch = Klepni pro start help.start_touch = Klepni pro start
# gamepad (Gamepad API v prohlížeči)
set.gamepad = Tlačítka gamepadu
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Použít
pad.back = Zpět
pad.tool = Nástroj
pad.menu = Menu
pad.camera = Kamera
pad.zoom = Zvětšení
help.p_move = Levá páčka – posun mapy
help.p_aim = Pravá páčka – pohyb kurzoru
help.p_rot = D-pad – otáčení a náklon
help.p_zoom = LT / RT – přiblížení a oddálení
help.p_use = A / křížek – použít nástroj, potvrdit (držet: natáhnout obdélník)
help.p_back = B / kolečko – zpět, pauza Start Menu
help.p_tool = LB / RB – volba nástroje Select – tato nápověda
# rivers and bridges # rivers and bridges
tool.bridge.label = Most tool.bridge.label = Most
+19
View File
@@ -439,6 +439,25 @@ help.t_long = Langt tryk: oplysninger om stedet; hånd: sæt enhederne ned der
help.t_double = Dobbelttryk med hånden: bagkast til egne enheder, slå helte på din jord help.t_double = Dobbelttryk med hånden: bagkast til egne enheder, slå helte på din jord
help.t_tools = Værktøjslinje: kategorier åbner en række værktøjer; menuknappen åbner pausemenuen help.t_tools = Værktøjslinje: kategorier åbner en række værktøjer; menuknappen åbner pausemenuen
help.start_touch = Tryk for at starte help.start_touch = Tryk for at starte
# gamepad (browserens Gamepad API)
set.gamepad = Gamepad-knapper
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Brug
pad.back = Tilbage
pad.tool = Værktøj
pad.menu = Menu
pad.camera = Kamera
pad.zoom = Zoom
help.p_move = Venstre stick – flyt kortet
help.p_aim = Højre stick – flyt markøren
help.p_rot = D-pad – roter og vip
help.p_zoom = LT / RT – zoom ind og ud
help.p_use = A / kryds – brug værktøj, bekræft (hold: træk et rektangel)
help.p_back = B / cirkel – tilbage, pausemenu Start Menu
help.p_tool = LB / RB – vælg værktøj Select – denne hjælp
# rivers and bridges # rivers and bridges
tool.bridge.label = Bro tool.bridge.label = Bro
+19
View File
@@ -440,6 +440,25 @@ help.t_double = Doppelt tippen mit der Hand: Rückhand für eigene, Faust für H
help.t_tools = Werkzeugleiste: Kategorien öffnen eine Reihe Werkzeuge; die Menütaste öffnet das Pausemenü help.t_tools = Werkzeugleiste: Kategorien öffnen eine Reihe Werkzeuge; die Menütaste öffnet das Pausemenü
help.start_touch = Tippen zum Starten help.start_touch = Tippen zum Starten
# gamepad (Browser-Gamepad-API)
set.gamepad = Gamepad-Tasten
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Benutzen
pad.back = Zurück
pad.tool = Werkzeug
pad.menu = Menü
pad.camera = Kamera
pad.zoom = Zoom
help.p_move = Linker Stick Karte bewegen
help.p_aim = Rechter Stick Zeiger bewegen
help.p_rot = Steuerkreuz Drehen und neigen
help.p_zoom = LT / RT Hinein- und herauszoomen
help.p_use = A / Kreuz Werkzeug benutzen, bestätigen (halten: Rechteck ziehen)
help.p_back = B / Kreis Zurück, Pausenmenü Start Menü
help.p_tool = LB / RB Werkzeug wählen Select Diese Hilfe
# rivers and bridges # rivers and bridges
tool.bridge.label = Brücke tool.bridge.label = Brücke
tool.bridge.name = Brücke bauen tool.bridge.name = Brücke bauen
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Παρατεταμένο πάτημα: πληροφορίες γι
help.t_double = Διπλό πάτημα με το χέρι: χαστούκι στις δικές σου μονάδες, γροθιά σε ήρωες στο έδαφός σου help.t_double = Διπλό πάτημα με το χέρι: χαστούκι στις δικές σου μονάδες, γροθιά σε ήρωες στο έδαφός σου
help.t_tools = Γραμμή εργαλείων: οι κατηγορίες ανοίγουν μια σειρά εργαλείων, το κουμπί μενού ανοίγει το μενού παύσης help.t_tools = Γραμμή εργαλείων: οι κατηγορίες ανοίγουν μια σειρά εργαλείων, το κουμπί μενού ανοίγει το μενού παύσης
help.start_touch = Πάτησε για αρχή help.start_touch = Πάτησε για αρχή
# gamepad (Το Gamepad API του φυλλομετρητή)
set.gamepad = Κουμπιά χειριστηρίου
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Χρήση
pad.back = Πίσω
pad.tool = Εργαλείο
pad.menu = Μενού
pad.camera = Κάμερα
pad.zoom = Εστίαση
help.p_move = Αριστερό stick – μετακίνηση χάρτη
help.p_aim = Δεξί stick – μετακίνηση δείκτη
help.p_rot = D-pad – περιστροφή και κλίση
help.p_zoom = LT / RT – μεγέθυνση και σμίκρυνση
help.p_use = A / Σταυρός – χρήση εργαλείου, επιβεβαίωση (κράτημα: ορθογώνιο)
help.p_back = B / Κύκλος – πίσω, μενού παύσης Start Μενού
help.p_tool = LB / RB – επιλογή εργαλείου Select – αυτή η βοήθεια
# rivers and bridges # rivers and bridges
tool.bridge.label = Γέφυρα tool.bridge.label = Γέφυρα
+19
View File
@@ -441,6 +441,25 @@ help.t_double = Double tap with the hand: backhand own units, punch heroes on yo
help.t_tools = Toolbar: categories open a row of tools; the menu button opens the pause menu help.t_tools = Toolbar: categories open a row of tools; the menu button opens the pause menu
help.start_touch = Tap to start help.start_touch = Tap to start
# gamepad (browser Gamepad API)
set.gamepad = Gamepad buttons
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Use
pad.back = Back
pad.tool = Tool
pad.menu = Menu
pad.camera = Camera
pad.zoom = Zoom
help.p_move = Left stick Move the map
help.p_aim = Right stick Move the pointer
help.p_rot = D-pad Rotate and tilt
help.p_zoom = LT / RT Zoom in and out
help.p_use = A / cross Use the tool, confirm (hold: drag a rectangle)
help.p_back = B / circle Back, pause menu Start Menu
help.p_tool = LB / RB Select the tool Select This help
# rivers and bridges # rivers and bridges
tool.bridge.label = Bridge tool.bridge.label = Bridge
tool.bridge.name = Build Bridge tool.bridge.name = Build Bridge
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Pulsación larga: información del punto; mano: soltar ahí las un
help.t_double = Doble toque con la mano: abofetear unidades propias, golpear héroes en tu suelo help.t_double = Doble toque con la mano: abofetear unidades propias, golpear héroes en tu suelo
help.t_tools = Barra de herramientas: las categorías abren una fila de herramientas; el botón de menú abre el menú de pausa help.t_tools = Barra de herramientas: las categorías abren una fila de herramientas; el botón de menú abre el menú de pausa
help.start_touch = Toca para empezar help.start_touch = Toca para empezar
# gamepad (Gamepad API del navegador)
set.gamepad = Botones del mando
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Usar
pad.back = Atrás
pad.tool = Herramienta
pad.menu = Menú
pad.camera = Cámara
pad.zoom = Zoom
help.p_move = Stick izquierdo: mover el mapa
help.p_aim = Stick derecho: mover el puntero
help.p_rot = D-pad: rotar e inclinar
help.p_zoom = LT / RT: acercar y alejar
help.p_use = A / equis: usar herramienta, confirmar (mantener: arrastrar un rectángulo)
help.p_back = B / círculo: atrás, menú de pausa Start Menú
help.p_tool = LB / RB: elegir herramienta Select esta ayuda
# rivers and bridges # rivers and bridges
tool.bridge.label = Puente tool.bridge.label = Puente
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Pitkä painallus: tietoa kohdasta; käsi: pudota yksiköt siihen
help.t_double = Kaksoisnapautus kädellä: läimäytä omia, mukiloi sankareita omalla maalla help.t_double = Kaksoisnapautus kädellä: läimäytä omia, mukiloi sankareita omalla maalla
help.t_tools = Työkalupalkki: kategoriat avaavat työkalurivin; valikkonappi avaa taukovalikon help.t_tools = Työkalupalkki: kategoriat avaavat työkalurivin; valikkonappi avaa taukovalikon
help.start_touch = Napauta aloittaaksesi help.start_touch = Napauta aloittaaksesi
# gamepad (selaimen Gamepad API)
set.gamepad = Ohjaimen painikkeet
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Käytä
pad.back = Takaisin
pad.tool = Työkalu
pad.menu = Valikko
pad.camera = Kamera
pad.zoom = Zoom
help.p_move = Vasen sauva – siirrä karttaa
help.p_aim = Oikea sauva – siirrä osoitinta
help.p_rot = D-pad – pyöritä ja kallista
help.p_zoom = LT / RT – lähennä ja loitonna
help.p_use = A / risti – käytä työkalua, vahvista (pidä: vedä suorakulmio)
help.p_back = B / ympyrä – takaisin, taukovalikko Start Valikko
help.p_tool = LB / RB – valitse työkalu Select – tämä ohje
# rivers and bridges # rivers and bridges
tool.bridge.label = Silta tool.bridge.label = Silta
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Dugi pritisak: podaci o mjestu; ruka: spusti jedinice ondje
help.t_double = Dvostruki dodir rukom: pljusni svoje jedinice, udari junake na svom tlu help.t_double = Dvostruki dodir rukom: pljusni svoje jedinice, udari junake na svom tlu
help.t_tools = Alatna traka: kategorije otvaraju red alata; tipka izbornika otvara izbornik stanke help.t_tools = Alatna traka: kategorije otvaraju red alata; tipka izbornika otvara izbornik stanke
help.start_touch = Dodirni za početak help.start_touch = Dodirni za početak
# gamepad (Gamepad API preglednika)
set.gamepad = Gumbi gamepada
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Koristi
pad.back = Natrag
pad.tool = Alat
pad.menu = Izbornik
pad.camera = Kamera
pad.zoom = Zum
help.p_move = Lijevi stick – pomicanje karte
help.p_aim = Desni stick – pomicanje kursora
help.p_rot = D-pad – rotacija i nagib
help.p_zoom = LT / RT – zumiranje
help.p_use = A / križ – koristi alat, potvrdi (drži: povuci pravokutnik)
help.p_back = B / krug – natrag, izbornik pauze Start Izbornik
help.p_tool = LB / RB – odabir alata Select – ova pomoć
# rivers and bridges # rivers and bridges
tool.bridge.label = Most tool.bridge.label = Most
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Hosszú nyomás: információ a helyről; kéz: az egységek lerak
help.t_double = Dupla koppintás a kézzel: saját egységek felpofozása, hősök ütése a saját földeden help.t_double = Dupla koppintás a kézzel: saját egységek felpofozása, hősök ütése a saját földeden
help.t_tools = Eszköztár: a kategóriák egy eszközsort nyitnak meg; a menügomb a szünetmenüt nyitja meg help.t_tools = Eszköztár: a kategóriák egy eszközsort nyitnak meg; a menügomb a szünetmenüt nyitja meg
help.start_touch = Koppints az indításhoz help.start_touch = Koppints az indításhoz
# gamepad (a böngésző Gamepad API-ja)
set.gamepad = Gamepad gombok
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Használat
pad.back = Vissza
pad.tool = Eszköz
pad.menu = Menü
pad.camera = Kamera
pad.zoom = Nagyítás
help.p_move = Bal bot – térkép mozgatása
help.p_aim = Jobb bot – kurzor mozgatása
help.p_rot = D-pad – forgatás és döntés
help.p_zoom = LT / RT – közelebb és távolabb
help.p_use = A / kereszt – eszköz használata, megerősítés (tartsd: téglalap húzása)
help.p_back = B / kör – vissza, szünetmenü Start Menü
help.p_tool = LB / RB – eszköz választása Select ez a súgó
# rivers and bridges # rivers and bridges
tool.bridge.label = Híd tool.bridge.label = Híd
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Pressione lunga: informazioni sul punto; mano: posa lì le unità
help.t_double = Doppio tocco con la mano: schiaffo alle tue unità, pugno agli eroi sul tuo suolo help.t_double = Doppio tocco con la mano: schiaffo alle tue unità, pugno agli eroi sul tuo suolo
help.t_tools = Barra: le categorie aprono una fila di strumenti; il pulsante menu apre il menu di pausa help.t_tools = Barra: le categorie aprono una fila di strumenti; il pulsante menu apre il menu di pausa
help.start_touch = Tocca per iniziare help.start_touch = Tocca per iniziare
# gamepad (Gamepad API del browser)
set.gamepad = Pulsanti del gamepad
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Usa
pad.back = Indietro
pad.tool = Strumento
pad.menu = Menu
pad.camera = Camera
pad.zoom = Zoom
help.p_move = Stick sinistro: sposta la mappa
help.p_aim = Stick destro: sposta il puntatore
help.p_rot = D-pad: ruota e inclina
help.p_zoom = LT / RT: zoom avanti e indietro
help.p_use = A / croce: usa lo strumento, conferma (tieni premi: disegna un rettangolo)
help.p_back = B / cerchio: indietro, menu di pausa Start Menu
help.p_tool = LB / RB: scegli strumento Select questa guida
# rivers and bridges # rivers and bridges
tool.bridge.label = Ponte tool.bridge.label = Ponte
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Lang drukken: informatie over de plek; hand: units daar neerzetten
help.t_double = Dubbeltikken met de hand: eigen units een mep geven, helden op je grond slaan help.t_double = Dubbeltikken met de hand: eigen units een mep geven, helden op je grond slaan
help.t_tools = Werkbalk: categorieën openen een rij gereedschap; de menuknop opent het pauzemenu help.t_tools = Werkbalk: categorieën openen een rij gereedschap; de menuknop opent het pauzemenu
help.start_touch = Tik om te beginnen help.start_touch = Tik om te beginnen
# gamepad (Gamepad API van de browser)
set.gamepad = Gamepadknoppen
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Gebruiken
pad.back = Terug
pad.tool = Gereedschap
pad.menu = Menu
pad.camera = Camera
pad.zoom = Zoom
help.p_move = linker stick: kaart verplaatsen
help.p_aim = rechter stick: aanwijzer verplaatsen
help.p_rot = d-pad: draaien en kantelen
help.p_zoom = LT / RT: in- en uitzoomen
help.p_use = A / kruis: gereedschap gebruiken, bevestigen (indrukken: rechthoek slepen)
help.p_back = B / cirkel: terug, pauzemenu Start Menu
help.p_tool = LB / RB: gereedschap kiezen Select deze help
# rivers and bridges # rivers and bridges
tool.bridge.label = Brug tool.bridge.label = Brug
+19
View File
@@ -439,6 +439,25 @@ help.t_long = Langt trykk: informasjon om stedet; hånd: sett ned enhetene der
help.t_double = Dobbelttrykk med hånden: baksveiv egne enheter, slå helter på din grunn help.t_double = Dobbelttrykk med hånden: baksveiv egne enheter, slå helter på din grunn
help.t_tools = Verktøylinje: kategorier åpner en rad med verktøy; menyknappen åpner pausemenyen help.t_tools = Verktøylinje: kategorier åpner en rad med verktøy; menyknappen åpner pausemenyen
help.start_touch = Trykk for å starte help.start_touch = Trykk for å starte
# gamepad (nettleserens Gamepad API)
set.gamepad = Gamepad-knapper
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Bruk
pad.back = Tilbake
pad.tool = Verktøy
pad.menu = Meny
pad.camera = Kamera
pad.zoom = Zoom
help.p_move = Venstre stick – flytt kartet
help.p_aim = Høyre stick – flytt pekeren
help.p_rot = D-pad – roter og vip
help.p_zoom = LT / RT – zoom inn og ut
help.p_use = A / kryss – bruk verktøyet, bekreft (hold: dra et rektangel)
help.p_back = B / sirkel – tilbake, pausemeny Start Meny
help.p_tool = LB / RB – verktøyvalg Select – denne hjelpen
# rivers and bridges # rivers and bridges
tool.bridge.label = Bro tool.bridge.label = Bro
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Toque longo: informação sobre o ponto; mão: largar ali as unida
help.t_double = Toque duplo com a mão: esbofetear as tuas unidades, socar heróis no teu chão help.t_double = Toque duplo com a mão: esbofetear as tuas unidades, socar heróis no teu chão
help.t_tools = Barra de ferramentas: as categorias abrem uma fila de ferramentas; o botão de menu abre o menu de pausa help.t_tools = Barra de ferramentas: as categorias abrem uma fila de ferramentas; o botão de menu abre o menu de pausa
help.start_touch = Toca para começar help.start_touch = Toca para começar
# gamepad (Gamepad API do navegador)
set.gamepad = Botões do controle
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Usar
pad.back = Voltar
pad.tool = Ferramenta
pad.menu = Menu
pad.camera = Câmera
pad.zoom = Zoom
help.p_move = Analógico esquerdo: mover o mapa
help.p_aim = Analógico direito: mover o cursor
help.p_rot = D-pad: girar e inclinar
help.p_zoom = LT / RT: aproximar e afastar
help.p_use = A / cruz: usar a ferramenta, confirmar (segurar: arrastar um retângulo)
help.p_back = B / círculo: voltar, menu de pausa Start Menu
help.p_tool = LB / RB: escolher ferramenta Select esta ajuda
# rivers and bridges # rivers and bridges
tool.bridge.label = Ponte tool.bridge.label = Ponte
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Apăsare lungă: informații despre loc; mână: lasă jos unită
help.t_double = Atingere dublă cu mâna: palmă unităților tale, pumn eroilor de pe terenul tău help.t_double = Atingere dublă cu mâna: palmă unităților tale, pumn eroilor de pe terenul tău
help.t_tools = Bara de unelte: categoriile deschid un rând de unelte; butonul de meniu deschide meniul de pauză help.t_tools = Bara de unelte: categoriile deschid un rând de unelte; butonul de meniu deschide meniul de pauză
help.start_touch = Atinge pentru a începe help.start_touch = Atinge pentru a începe
# gamepad (Gamepad API-ul browserului)
set.gamepad = Butoane gamepad
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Folosește
pad.back = Înapoi
pad.tool = Unealtă
pad.menu = Meniu
pad.camera = Cameră
pad.zoom = Zoom
help.p_move = Stickul stâng – mută harta
help.p_aim = Stickul drept – mută pointerul
help.p_rot = D-pad – rotește și înclină
help.p_zoom = LT / RT – apropiere și depărtare
help.p_use = A / cruce – folosește unealta, confirmă (ține apăsat: trage un dreptunghi)
help.p_back = B / cerc – înapoi, meniul de pauză Start Meniu
help.p_tool = LB / RB – alege unealta Select – acest ajutor
# rivers and bridges # rivers and bridges
tool.bridge.label = Pod tool.bridge.label = Pod
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Долгое нажатие: сведения о месте; ру
help.t_double = Двойное касание рукой: шлепок своим, удар героям на своей земле help.t_double = Двойное касание рукой: шлепок своим, удар героям на своей земле
help.t_tools = Панель: категории открывают ряд инструментов; кнопка меню открывает паузу help.t_tools = Панель: категории открывают ряд инструментов; кнопка меню открывает паузу
help.start_touch = Коснись, чтобы начать help.start_touch = Коснись, чтобы начать
# gamepad (Gamepad API браузера)
set.gamepad = Кнопки геймпада
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Использовать
pad.back = Назад
pad.tool = Инструмент
pad.menu = Меню
pad.camera = Камера
pad.zoom = Масштаб
help.p_move = Левый стик — движение карты
help.p_aim = Правый стик — движение курсора
help.p_rot = Крестовина — поворот и наклон
help.p_zoom = LT / RT — приближение и отдаление
help.p_use = A / крестик — использовать инструмент, подтвердить (удерживать: рамка)
help.p_back = B / кружок — назад, меню паузы Start меню
help.p_tool = LB / RB — выбор инструмента Select эта справка
# rivers and bridges # rivers and bridges
tool.bridge.label = Мост tool.bridge.label = Мост
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Dlhé podržanie: informácie o mieste; ruka: položí tam jednotk
help.t_double = Dvojité ťuknutie rukou: zaucho vlastným, päsť hrdinom na tvojom území help.t_double = Dvojité ťuknutie rukou: zaucho vlastným, päsť hrdinom na tvojom území
help.t_tools = Lišta nástrojov: kategórie otvoria rad nástrojov; tlačidlo menu otvorí menu pauzy help.t_tools = Lišta nástrojov: kategórie otvoria rad nástrojov; tlačidlo menu otvorí menu pauzy
help.start_touch = Ťukni pre štart help.start_touch = Ťukni pre štart
# gamepad (Gamepad API prehliadača)
set.gamepad = Tlačidlá gamepadu
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Použiť
pad.back = Späť
pad.tool = Nástroj
pad.menu = Menu
pad.camera = Kamera
pad.zoom = Priblíženie
help.p_move = Ľavá páčka – posun mapy
help.p_aim = Pravá páčka – posun kurzora
help.p_rot = D-pad – otáčanie a náklon
help.p_zoom = LT / RT – priblíženie a oddialenie
help.p_use = A / krížik – použiť nástroj, potvrdiť (držať: natiahnuť obdĺžnik)
help.p_back = B / krúžok – späť, pauza Start Menu
help.p_tool = LB / RB – výber nástroja Select – táto nápoveda
# rivers and bridges # rivers and bridges
tool.bridge.label = Most tool.bridge.label = Most
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Дуги притисак: подаци о месту; рука:
help.t_double = Двоструки додир руком: ошамари своје јединице, удари хероје на свом тлу help.t_double = Двоструки додир руком: ошамари своје јединице, удари хероје на свом тлу
help.t_tools = Трака алатки: категорије отварају ред алатки; дугме менија отвара мени паузе help.t_tools = Трака алатки: категорије отварају ред алатки; дугме менија отвара мени паузе
help.start_touch = Додирни за почетак help.start_touch = Додирни за почетак
# gamepad (Gamepad API прегледника)
set.gamepad = Дугмад контролера
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Користи
pad.back = Назад
pad.tool = Алат
pad.menu = Мени
pad.camera = Камера
pad.zoom = Зум
help.p_move = Леви стик – померање карте
help.p_aim = Десни стик – померање показивача
help.p_rot = D-pad – ротација и нагиб
help.p_zoom = LT / RT – зумирање
help.p_use = A / крстић – користи алат, потврди (држати: повући правоугаоник)
help.p_back = B / круг – назад, пауза Start Мени
help.p_tool = LB / RB – избор алата Select – ова помоћ
# rivers and bridges # rivers and bridges
tool.bridge.label = Мост tool.bridge.label = Мост
+19
View File
@@ -440,6 +440,25 @@ help.t_long = Långt tryck: information om platsen; hand: släpp enheterna där
help.t_double = Dubbeltryck med handen: örfila egna enheter, slå hjältar på din mark help.t_double = Dubbeltryck med handen: örfila egna enheter, slå hjältar på din mark
help.t_tools = Verktygsfält: kategorier öppnar en rad verktyg; menyknappen öppnar pausmenyn help.t_tools = Verktygsfält: kategorier öppnar en rad verktyg; menyknappen öppnar pausmenyn
help.start_touch = Tryck för att börja help.start_touch = Tryck för att börja
# gamepad (webbläsarens Gamepad API)
set.gamepad = Gamepad-knappar
val.xbox = Xbox
val.playstation = PlayStation
val.nintendo = Nintendo
pad.use = Använd
pad.back = Tillbaka
pad.tool = Verktyg
pad.menu = Meny
pad.camera = Kamera
pad.zoom = Zoom
help.p_move = Vänster spak – flytta kartan
help.p_aim = Höger spak – flytta markören
help.p_rot = Pilknappar – rotera och luta
help.p_zoom = LT / RT – zooma in och ut
help.p_use = A / kors – använd verktyget, bekräfta (håll: dra en rektangel)
help.p_back = B / cirkel – tillbaka, pausmeny Start Meny
help.p_tool = LB / RB – välj verktyg Select – denna hjälp
# rivers and bridges # rivers and bridges
tool.bridge.label = Bro tool.bridge.label = Bro
+212 -1
View File
@@ -6,7 +6,7 @@ use macroquad::prelude::*;
pub const CELL: f32 = 16.0; pub const CELL: f32 = 16.0;
pub const ATLAS_COLS: u32 = 16; pub const ATLAS_COLS: u32 = 16;
pub const ATLAS_ROWS: u32 = 18; // rows 14-17: cells only the high-res atlases fill (see HD_SETS) pub const ATLAS_ROWS: u32 = 20; // rows 14-17: cells only the high-res atlases fill (see HD_SETS); rows 18-19: gamepad pictograms
// atlas indices (4 variants each) // atlas indices (4 variants each)
pub const T_ROCK: u32 = 0; pub const T_ROCK: u32 = 0;
@@ -98,6 +98,30 @@ pub const T_GIFT: u32 = 220; // a wrapped present (Christmas)
pub const T_FIR: u32 = 221; // fir needles with baubles (Christmas) pub const T_FIR: u32 = 221; // fir needles with baubles (Christmas)
pub const T_EGG: u32 = 222; // 2 variants: a painted egg (Easter) pub const T_EGG: u32 = 222; // 2 variants: a painted egg (Easter)
// gamepad pictograms (rows 18-19 of the atlas): face buttons of the three pad types, d-pad,
// sticks, shoulders and the select/start buttons
pub const I_PAD_XBOX_A: u32 = 288; // A, B, X, Y on coloured discs
pub const I_PAD_XBOX_B: u32 = 289;
pub const I_PAD_XBOX_X: u32 = 290;
pub const I_PAD_XBOX_Y: u32 = 291;
pub const I_PAD_PS_CROSS: u32 = 292; // cross, circle, square, triangle on dark discs
pub const I_PAD_PS_CIRCLE: u32 = 293;
pub const I_PAD_PS_SQUARE: u32 = 294;
pub const I_PAD_PS_TRIANGLE: u32 = 295;
pub const I_PAD_NIN_A: u32 = 296; // A, B, X, Y on light discs
pub const I_PAD_NIN_B: u32 = 297;
pub const I_PAD_NIN_X: u32 = 298;
pub const I_PAD_NIN_Y: u32 = 299;
pub const I_PAD_DPAD: u32 = 300;
pub const I_PAD_STICK_L: u32 = 301;
pub const I_PAD_STICK_R: u32 = 302;
pub const I_PAD_LB: u32 = 303;
pub const I_PAD_RB: u32 = 304;
pub const I_PAD_LT: u32 = 305;
pub const I_PAD_RT: u32 = 306;
pub const I_PAD_START: u32 = 307;
pub const I_PAD_SELECT: u32 = 308;
pub struct Gfx { pub struct Gfx {
pub atlas: Texture2D, pub atlas: Texture2D,
pub sprites: Texture2D, pub sprites: Texture2D,
@@ -335,6 +359,42 @@ fn earth_px(x: i32, y: i32, v: u32) -> Color {
shade(base, (big + fine) as i32) shade(base, (big + fine) as i32)
} }
// ---- gamepad pictograms (16x16 cells; see I_PAD_*)
// tiny 3x5 letters for the button pictograms
fn pad_glyph(ch: char, x: i32, y: i32) -> bool {
if !(0..3).contains(&x) || !(0..5).contains(&y) {
return false;
}
let rows: [u8; 5] = match ch {
'A' => [0b010, 0b101, 0b111, 0b101, 0b101],
'B' => [0b110, 0b101, 0b110, 0b101, 0b110],
'X' => [0b101, 0b101, 0b010, 0b101, 0b101],
'Y' => [0b101, 0b101, 0b010, 0b010, 0b010],
'L' => [0b100, 0b100, 0b100, 0b100, 0b111],
'R' => [0b110, 0b101, 0b110, 0b101, 0b101],
'T' => [0b111, 0b010, 0b010, 0b010, 0b010],
_ => [0; 5],
};
rows[y as usize] >> (2 - x) & 1 == 1
}
// a round button: rim, fill, a highlight top left
fn pad_disc(x: i32, y: i32, fill: (i32, i32, i32)) -> Option<Color> {
let (dx, dy) = (x - 8, y - 8);
let d2 = dx * dx + dy * dy;
if d2 > 30 {
return None;
}
Some(if d2 >= 22 {
shade((fill.0 / 2, fill.1 / 2, fill.2 / 2), (rnd(x, y, 5) * 4.0) as i32)
} else if dx + dy <= -7 {
shade(fill, 26)
} else {
c(fill.0, fill.1, fill.2)
})
}
thread_local! { thread_local! {
// the interface draws atlas cells from the high-res atlas (the high-res graphics style) // the interface draws atlas cells from the high-res atlas (the high-res graphics style)
static HIGH_RES_UI: std::cell::Cell<bool> = const { std::cell::Cell::new(false) }; static HIGH_RES_UI: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
@@ -1787,6 +1847,129 @@ fn gen_atlas() -> Image {
}); });
cv.cell(D_RUBY, |x, y| gems(x, y, 200, c(255, 150, 170), c(215, 30, 60), c(120, 12, 36))); cv.cell(D_RUBY, |x, y| gems(x, y, 200, c(255, 150, 170), c(215, 30, 60), c(120, 12, 36)));
cv.cell(D_DIAMOND, |x, y| gems(x, y, 210, c(255, 255, 255), c(170, 232, 255), c(80, 150, 200))); cv.cell(D_DIAMOND, |x, y| gems(x, y, 210, c(255, 255, 255), c(170, 232, 255), c(80, 150, 200)));
// ---- gamepad pictograms
// a face button with a letter (Xbox, Nintendo)
let face = |ch: char, fill: (i32, i32, i32), dark: bool| {
move |x: i32, y: i32| {
let disc = pad_disc(x, y, fill)?;
if pad_glyph(ch, x - 7, y - 6) {
return Some(if dark { c(42, 44, 50) } else { c(250, 250, 250) });
}
Some(disc)
}
};
cv.cell(I_PAD_XBOX_A, face('A', (74, 166, 86), false));
cv.cell(I_PAD_XBOX_B, face('B', (196, 66, 58), false));
cv.cell(I_PAD_XBOX_X, face('X', (64, 108, 204), false));
cv.cell(I_PAD_XBOX_Y, face('Y', (214, 176, 64), false));
cv.cell(I_PAD_NIN_A, face('A', (200, 202, 210), true));
cv.cell(I_PAD_NIN_B, face('B', (200, 202, 210), true));
cv.cell(I_PAD_NIN_X, face('X', (200, 202, 210), true));
cv.cell(I_PAD_NIN_Y, face('Y', (200, 202, 210), true));
// PlayStation: the shape symbols on dark discs
let symbol = |sym: u8, col: Color| {
move |x: i32, y: i32| {
let disc = pad_disc(x, y, (58, 60, 68))?;
let (dx, dy) = (x - 8, y - 8);
let on = match sym {
0 => dx.abs() <= 3 && dy.abs() <= 3 && (dx == dy || dx == -dy),
1 => {
let d = ((dx * dx + dy * dy) as f32).sqrt();
d >= 2.6 && d <= 4.1
}
2 => dx.abs() <= 3 && dy.abs() <= 3 && (dx.abs() == 3 || dy.abs() == 3),
_ => dy >= -3 && dy <= 3 && (dx.abs() == (dy + 3) / 2 || (dy == 3 && dx.abs() <= 3)),
};
Some(if on { col } else { disc })
}
};
cv.cell(I_PAD_PS_CROSS, symbol(0, c(96, 154, 255)));
cv.cell(I_PAD_PS_CIRCLE, symbol(1, c(240, 96, 96)));
cv.cell(I_PAD_PS_SQUARE, symbol(2, c(238, 130, 234)));
cv.cell(I_PAD_PS_TRIANGLE, symbol(3, c(100, 214, 152)));
// d-pad: a cross with a dark centre
cv.cell(I_PAD_DPAD, |x, y| {
let arm = (5..=10).contains(&x) || (5..=10).contains(&y);
if !arm || !(1..=14).contains(&x) || !(1..=14).contains(&y) {
return None;
}
let mid = (x - 8).abs() <= 1 && (y - 8).abs() <= 1;
let edge = x == 1 || y == 1 || x == 14 || y == 14;
Some(if mid { c(70, 72, 80) } else if edge { c(116, 118, 128) } else { c(172, 174, 186) })
});
// sticks: a ring with a knob, L/R in the corner
let stick = |side: char| {
move |x: i32, y: i32| {
let (dx, dy) = (x - 8, y - 8);
let d = ((dx * dx + dy * dy) as f32).sqrt();
if d <= 6.0 {
return Some(if d >= 4.6 {
c(148, 150, 162)
} else if d <= 2.8 {
c(208, 210, 220)
} else {
c(88, 90, 102)
});
}
if pad_glyph(side, x - 11, y - 10) {
return Some(c(190, 192, 202));
}
None
}
};
cv.cell(I_PAD_STICK_L, stick('L'));
cv.cell(I_PAD_STICK_R, stick('R'));
// shoulders (LB/RB) and triggers (LT/RT): a rounded body with its letters
let bumper = |a: char, b: char, tall: bool| {
move |x: i32, y: i32| {
let (y0, y1) = if tall { (2, 10) } else { (4, 11) };
let corner = (x == 3 || x == 12) && (y == y0 || y == y1);
if !(3..=12).contains(&x) || !(y0..=y1).contains(&y) || corner {
return None;
}
if pad_glyph(a, x - 4, y - (y0 + 1)) || pad_glyph(b, x - 8, y - (y0 + 1)) {
return Some(c(52, 54, 62));
}
Some(if x == 3 || x == 12 || y == y0 {
c(128, 130, 140)
} else if y == y1 {
c(94, 96, 106)
} else {
c(190, 192, 202)
})
}
};
cv.cell(I_PAD_LB, bumper('L', 'B', false));
cv.cell(I_PAD_RB, bumper('R', 'B', false));
cv.cell(I_PAD_LT, bumper('L', 'T', true));
cv.cell(I_PAD_RT, bumper('R', 'T', true));
// start: two bars (pause); select: two lines (list)
cv.cell(I_PAD_START, |x, y| {
if !(3..=12).contains(&x) || !(4..=11).contains(&y) {
return None;
}
let bars = (x == 5 || x == 6 || x == 9 || x == 10) && (6..=9).contains(&y);
Some(if bars {
c(232, 234, 242)
} else if x == 3 || y == 4 || x == 12 || y == 11 {
c(68, 70, 80)
} else {
c(118, 120, 132)
})
});
cv.cell(I_PAD_SELECT, |x, y| {
if !(2..=13).contains(&x) || !(4..=11).contains(&y) {
return None;
}
let lines = (5..=10).contains(&x) && (y == 6 || y == 9);
Some(if lines {
c(232, 234, 242)
} else if x == 2 || y == 4 || x == 13 || y == 11 {
c(68, 70, 80)
} else {
c(118, 120, 132)
})
});
cv.img cv.img
} }
@@ -3563,6 +3746,34 @@ mod atlas_dump {
} }
out.export_png("/tmp/atlases.png"); out.export_png("/tmp/atlases.png");
} }
// `cargo test --bins dump_pad_icons -- --ignored`: the gamepad pictograms, 6x scaled, in
// /tmp/pad-icons.png (Xbox, PlayStation, Nintendo, then d-pad, sticks and co.)
#[test]
#[ignore]
fn dump_pad_icons() {
let atlas = gen_atlas();
let ids: [u32; 21] = [
I_PAD_XBOX_A, I_PAD_XBOX_B, I_PAD_XBOX_X, I_PAD_XBOX_Y,
I_PAD_PS_CROSS, I_PAD_PS_CIRCLE, I_PAD_PS_SQUARE, I_PAD_PS_TRIANGLE,
I_PAD_NIN_A, I_PAD_NIN_B, I_PAD_NIN_X, I_PAD_NIN_Y,
I_PAD_DPAD, I_PAD_STICK_L, I_PAD_STICK_R, I_PAD_LB, I_PAD_RB, I_PAD_LT, I_PAD_RT,
I_PAD_START, I_PAD_SELECT,
];
let k = 6;
let cols = 7;
let rows = (ids.len() as u32).div_ceil(cols);
let mut out = Image::gen_image_color((cols * 16 * k) as u16, (rows * 16 * k) as u16, BLACK);
for (i, &id) in ids.iter().enumerate() {
let (ox, oy) = ((id % ATLAS_COLS) * 16, (id / ATLAS_COLS) * 16);
for y in 0..16 * k {
for x in 0..16 * k {
out.set_pixel((i as u32 % cols) * 16 * k + x, (i as u32 / cols) * 16 * k + y, atlas.get_pixel(ox + x / k, oy + y / k));
}
}
}
out.export_png("/tmp/pad-icons.png");
}
} }
#[cfg(test)] #[cfg(test)]
+70 -16
View File
@@ -30,6 +30,7 @@ mod personality;
mod platform; mod platform;
mod portraits; mod portraits;
mod orders; mod orders;
mod pad;
mod particles; mod particles;
mod props; mod props;
mod research; mod research;
@@ -933,6 +934,7 @@ async fn main() {
apply_game_settings(&mut app.game, &app.settings); apply_game_settings(&mut app.game, &app.settings);
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, mclick: -1.0, minimap_drag: false, tlast: None, touch_world: false, inspect: None }; let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, mclick: -1.0, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
let mut touch = TouchInput::new(); let mut touch = TouchInput::new();
let mut pad = pad::Pad::new();
let mut auto_touch = platform::coarse_pointer(); let mut auto_touch = platform::coarse_pointer();
let mut menu_swipe: Option<(f32, f32)> = None; // a swipe over a menu: last finger y, unused distance let mut menu_swipe: Option<(f32, f32)> = None; // a swipe over a menu: last finger y, unused distance
let mut menu_dragged = false; // the finger on a menu has swiped (no click when it lifts) let mut menu_dragged = false; // the finger on a menu has swiped (no click when it lifts)
@@ -1024,8 +1026,19 @@ async fn main() {
let p = touch.finger().or(gpos).unwrap_or(vec2(-1e5, -1e5)); let p = touch.finger().or(gpos).unwrap_or(vec2(-1e5, -1e5));
ui::set_pointer(p.x, p.y); ui::set_pointer(p.x, p.y);
} }
// ---------------- gamepad: the browser's Gamepad API (the desktop build has none)
pad.poll(app.settings.gamepad);
ui::set_gamepad(pad.connected());
// the pad's pointer starts where the mouse is and moves with the right stick; a mouse
// movement takes it over again, so both can be used side by side (a finger still wins)
if pad.connected() && !(touch_mode && touch.finger().is_some()) {
let m = mouse_position();
let moved = mouse_delta_position() != Vec2::ZERO;
let (px, py) = pad.aim(dt, ui::sw(), ui::sh(), (m.0 * ui::dpr(), m.1 * ui::dpr()), moved);
ui::set_pointer(px, py);
}
let (ux, uy) = ui::mouse(); // device pixels (interface) let (ux, uy) = ui::mouse(); // device pixels (interface)
let (mx, my) = if touch_mode { (ux / ui::dpr(), uy / ui::dpr()) } else { mouse_position() }; // CSS pixels (3D picking) let (mx, my) = if touch_mode || ui::gamepad_ui() { (ux / ui::dpr(), uy / ui::dpr()) } else { mouse_position() }; // CSS pixels (3D picking)
// ---------------- savegame files: file picker result or dropped onto the window // ---------------- savegame files: file picker result or dropped onto the window
let mut incoming: Option<Vec<u8>> = platform::take_file(); let mut incoming: Option<Vec<u8>> = platform::take_file();
@@ -1242,8 +1255,8 @@ async fn main() {
let keys = app.settings.clone(); let keys = app.settings.clone();
let key = |a: Action| keys.key(a); let key = |a: Action| keys.key(a);
// ---------------- Esc: chat, dialogs, menus, pause menu // ---------------- Esc (or the pad's B / Start): chat, dialogs, menus, pause menu
if is_key_pressed(KeyCode::Escape) { if is_key_pressed(KeyCode::Escape) || pad.pressed(pad::Btn::East) || pad.pressed(pad::Btn::Start) {
if ui.chat_open { if ui.chat_open {
ui.chat_open = false; ui.chat_open = false;
ui.chat_text.clear(); ui.chat_text.clear();
@@ -1293,9 +1306,28 @@ async fn main() {
inp.ldown = None; inp.ldown = None;
} }
} }
if is_key_pressed(key(Action::Help)) { if is_key_pressed(key(Action::Help)) || pad.pressed(pad::Btn::Select) {
ui.show_help = true; ui.show_help = true;
} }
// the pad cycles the tools: LB back, RB forward
let tool_step = if pad.pressed(pad::Btn::Rb) {
1
} else if pad.pressed(pad::Btn::Lb) {
-1
} else {
0
};
if tool_step != 0 {
let cur = TOOLS.iter().position(|&t| t == ui.tool).unwrap_or(0) as i32;
for k in 1..=TOOLS.len() as i32 {
let i = (cur + tool_step * k).rem_euclid(TOOLS.len() as i32) as usize;
if app.game.tool_allowed(i) && app.game.tool_visible(i) {
ui.tool = TOOLS[i];
inp.ldown = None;
break;
}
}
}
if is_key_pressed(key(Action::Pause)) && app.game.state == 0 { if is_key_pressed(key(Action::Pause)) && app.game.state == 0 {
app.game.paused = !app.game.paused; app.game.paused = !app.game.paused;
} }
@@ -1346,11 +1378,19 @@ async fn main() {
if is_key_down(key(Action::CamRight)) || is_key_down(KeyCode::Right) { if is_key_down(key(Action::CamRight)) || is_key_down(KeyCode::Right) {
move_cam(pan, 0.0, cam); move_cam(pan, 0.0, cam);
} }
// the pad's left stick moves the map, as W/A/S/D do
if pad.connected() {
let s = pad.stick(pad::Stick::Left);
if s != Vec2::ZERO {
let k = pan * 1.8;
move_cam(s.x * k, -s.y * k, cam);
}
}
let edge = 6.0; let edge = 6.0;
if !mouse_moved && mouse_delta_position() != Vec2::ZERO { if !mouse_moved && mouse_delta_position() != Vec2::ZERO {
mouse_moved = true; mouse_moved = true;
} }
if keys.edge_scroll && mouse_moved && !touch_mode && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() { if keys.edge_scroll && mouse_moved && !touch_mode && !ui::gamepad_ui() && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if mx < edge { if mx < edge {
move_cam(-pan, 0.0, cam); move_cam(-pan, 0.0, cam);
} else if mx > screen_width() - edge { } else if mx > screen_width() - edge {
@@ -1363,18 +1403,22 @@ async fn main() {
} }
} }
let rot = 1.8 * dt * keys.rotate_speed; let rot = 1.8 * dt * keys.rotate_speed;
if is_key_down(key(Action::RotateLeft)) { if is_key_down(key(Action::RotateLeft)) || pad.down(pad::Btn::Left) {
cam.yaw += rot; cam.yaw += rot;
} }
if is_key_down(key(Action::RotateRight)) { if is_key_down(key(Action::RotateRight)) || pad.down(pad::Btn::Right) {
cam.yaw -= rot; cam.yaw -= rot;
} }
if is_key_down(key(Action::TiltUp)) { if is_key_down(key(Action::TiltUp)) || pad.down(pad::Btn::Up) {
cam.pitch += rot * 0.6; cam.pitch += rot * 0.6;
} }
if is_key_down(key(Action::TiltDown)) { if is_key_down(key(Action::TiltDown)) || pad.down(pad::Btn::Down) {
cam.pitch -= rot * 0.6; cam.pitch -= rot * 0.6;
} }
// the pad's left stick click resets the view
if pad.pressed(pad::Btn::LStick) {
cam.reset_view();
}
// a double click with the middle button: the standard angle and turn (same place) // a double click with the middle button: the standard angle and turn (same place)
if is_mouse_button_pressed(MouseButton::Middle) && !touch_mode { if is_mouse_button_pressed(MouseButton::Middle) && !touch_mode {
let now = get_time(); let now = get_time();
@@ -1405,6 +1449,15 @@ async fn main() {
cam.tx += before.x - after.x; cam.tx += before.x - after.x;
cam.tz += before.y - after.y; cam.tz += before.y - after.y;
} }
// the pad's triggers zoom (RT in, LT out)
let zoom_in = if pad.down(pad::Btn::Rt) { 1.0 } else { 0.0 } - if pad.down(pad::Btn::Lt) { 1.0 } else { 0.0 };
if zoom_in != 0.0 && !over_ui {
let before = cam.ground_point(mx, my);
cam.zoom = (cam.zoom * (1.0 - zoom_in * 1.4 * dt * keys.zoom_speed)).clamp(6.0, 45.0);
let after = cam.ground_point(mx, my);
cam.tx += before.x - after.x;
cam.tz += before.y - after.y;
}
// touch: two fingers move, zoom and rotate the camera // touch: two fingers move, zoom and rotate the camera
for g in &gestures { for g in &gestures {
if let Gesture::Camera { center, pan, zoom, rotate } = *g { if let Gesture::Camera { center, pan, zoom, rotate } = *g {
@@ -1421,8 +1474,8 @@ async fn main() {
cam.tz += before.y - after.y; cam.tz += before.y - after.y;
} }
} }
// right button: drag = pan, click = drop // right button (or the pad's X / square): drag = pan, click = drop
if is_mouse_button_pressed(MouseButton::Right) { if is_mouse_button_pressed(MouseButton::Right) || pad.pressed(pad::Btn::West) {
inp.rdown = Some((mx, my)); inp.rdown = Some((mx, my));
inp.rlast = (mx, my); inp.rlast = (mx, my);
inp.rdrag = false; inp.rdrag = false;
@@ -1441,7 +1494,7 @@ async fn main() {
inp.rlast = (mx, my); inp.rlast = (mx, my);
} }
} }
if is_mouse_button_released(MouseButton::Right) { if is_mouse_button_released(MouseButton::Right) || pad.released(pad::Btn::West) {
if !inp.rdrag && !over_ui && app.game.state == 0 { if !inp.rdrag && !over_ui && app.game.state == 0 {
if inp.ldown.is_some() { if inp.ldown.is_some() {
inp.ldown = None; inp.ldown = None;
@@ -1459,13 +1512,13 @@ async fn main() {
// (in menus and dialogs a touch clicks when the finger lifts without a swipe, so that // (in menus and dialogs a touch clicks when the finger lifts without a swipe, so that
// swiping a list scrolls it instead of picking the entry under the finger) // swiping a list scrolls it instead of picking the entry under the finger)
let menu_touch = touch_mode && (in_menu || dialog); let menu_touch = touch_mode && (in_menu || dialog);
let pressed = if menu_touch { let pressed = (if menu_touch {
!menu_dragged && gestures.iter().any(|g| matches!(g, Gesture::Release(_))) !menu_dragged && gestures.iter().any(|g| matches!(g, Gesture::Release(_)))
} else if touch_mode { } else if touch_mode {
gestures.iter().any(|g| matches!(g, Gesture::Press(_))) gestures.iter().any(|g| matches!(g, Gesture::Press(_)))
} else { } else {
is_mouse_button_pressed(MouseButton::Left) is_mouse_button_pressed(MouseButton::Left)
}; }) || pad.pressed(pad::Btn::South);
if touch_mode && pressed { if touch_mode && pressed {
inp.touch_world = playing && !over_ui; inp.touch_world = playing && !over_ui;
inp.inspect = None; inp.inspect = None;
@@ -1916,7 +1969,7 @@ app.pending_briefing = briefing_of(&campaign, i);
} }
} }
} }
let held = if touch_mode { touch.finger().is_some() } else { is_mouse_button_down(MouseButton::Left) }; let held = (if touch_mode { touch.finger().is_some() } else { is_mouse_button_down(MouseButton::Left) }) || pad.down(pad::Btn::South);
if inp.minimap_drag { if inp.minimap_drag {
if held && ui.minimap_contains(ux, uy) { if held && ui.minimap_contains(ux, uy) {
let (x, y) = ui.minimap_world(ux, uy); let (x, y) = ui.minimap_world(ux, uy);
@@ -1976,7 +2029,8 @@ app.pending_briefing = briefing_of(&campaign, i);
inp.inspect = None; inp.inspect = None;
} }
} }
if !touch_mode && is_mouse_button_released(MouseButton::Left) { let released = (if touch_mode { false } else { is_mouse_button_released(MouseButton::Left) }) || pad.released(pad::Btn::South);
if released {
if let Some(start) = inp.ldown.take() { if let Some(start) = inp.ldown.take() {
if playing { if playing {
finish_rect(&mut app, ui.tool, start, mx, my); finish_rect(&mut app, ui.tool, start, mx, my);
+21 -1
View File
@@ -67,6 +67,7 @@ pub enum Item {
Hints, Hints,
Events, Events,
TouchUi, TouchUi,
Gamepad,
Voice, Voice,
Track, Track,
} }
@@ -77,7 +78,7 @@ fn tab_items(tab: usize) -> &'static [Item] {
0 => &[Language, Volume, MusicVolume, Track, Mute, Voice], 0 => &[Language, Volume, MusicVolume, Track, Mute, Voice],
1 => &[RotateSpeed, PanSpeed, ZoomSpeed, EdgeScroll, InvertMouse], 1 => &[RotateSpeed, PanSpeed, ZoomSpeed, EdgeScroll, InvertMouse],
2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale], 2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale],
3 => &[GameSpeed, Hints, Events, TouchUi], 3 => &[GameSpeed, Hints, Events, TouchUi, Gamepad],
4 => &[DebugFps, DebugFrameTime, DebugPing, DebugTriggerZones, DebugCommandMarkers, DebugPathLines, DebugHoliday], 4 => &[DebugFps, DebugFrameTime, DebugPing, DebugTriggerZones, DebugCommandMarkers, DebugPathLines, DebugHoliday],
_ => &[], _ => &[],
} }
@@ -110,6 +111,7 @@ fn item_label(it: Item) -> String {
Item::Hints => "set.hints", Item::Hints => "set.hints",
Item::Events => "set.events", Item::Events => "set.events",
Item::TouchUi => "set.touch_ui", Item::TouchUi => "set.touch_ui",
Item::Gamepad => "set.gamepad",
Item::Voice => "set.voice", Item::Voice => "set.voice",
Item::Track => "set.track", Item::Track => "set.track",
}) })
@@ -169,6 +171,12 @@ fn item_value(it: Item, s: &Settings) -> String {
2 => tr("val.off"), 2 => tr("val.off"),
_ => trf("val.auto_state", &[tr(if crate::ui::touch_ui() { "val.on" } else { "val.off" })]), _ => trf("val.auto_state", &[tr(if crate::ui::touch_ui() { "val.on" } else { "val.off" })]),
}, },
Item::Gamepad => tr(match s.gamepad {
1 => "val.xbox",
2 => "val.playstation",
3 => "val.nintendo",
_ => "val.auto_plain",
}),
} }
} }
@@ -221,6 +229,7 @@ fn item_change(it: Item, s: &mut Settings, d: i32) {
Item::Hints => s.hints = !s.hints, Item::Hints => s.hints = !s.hints,
Item::Events => s.events = !s.events, Item::Events => s.events = !s.events,
Item::TouchUi => s.touch_ui = cycle(&[0u8, 1, 2], s.touch_ui, d), Item::TouchUi => s.touch_ui = cycle(&[0u8, 1, 2], s.touch_ui, d),
Item::Gamepad => s.gamepad = cycle(&[0u8, 1, 2, 3], s.gamepad, d),
} }
} }
@@ -1514,6 +1523,17 @@ impl Menu {
// value with < > buttons // value with < > buttons
let vw = 18.0 * CW(); let vw = 18.0 * CW();
let vx = x + cols as f32 * CW() - vw - CW() * 3.0; let vx = x + cols as f32 * CW() - vw - CW() * 3.0;
if it == Item::Gamepad {
// the type's face buttons, right before the value: the pictograms the game uses
let kind = crate::pad::kind();
let isz = 2.0 * CH();
let gap = CW();
let mut ix = vx - CW() - 4.0 * isz - 3.0 * gap;
for b in [crate::pad::Btn::North, crate::pad::Btn::West, crate::pad::Btn::East, crate::pad::Btn::South] {
gfx.draw_cell(crate::ui::pad_icon(kind, b), ix, by, isz, isz, WHITE);
ix += isz + gap;
}
}
if it == Item::Language { if it == Item::Language {
self.lang_field(gfx, vx, by, vw); self.lang_field(gfx, vx, by, vw);
continue; continue;
+229
View File
@@ -0,0 +1,229 @@
// Gamepads via the browser's Gamepad API (see www/dc_plugin.js): polling, buttons, sticks and
// the virtual pointer. Native builds have no gamepad input (miniquad does not poll pads), so
// platform's stubs report "no gamepad" there.
//
// The standard Gamepad API mapping: buttons 0-3 = face buttons, 4/5 = shoulders, 6/7 = triggers,
// 8/9 = select/start, 10/11 = stick clicks, 12-15 = d-pad; axes 0/1 = left stick, 2/3 = right.
use macroquad::prelude::Vec2;
use std::cell::Cell;
// the standard Gamepad API button order; every variant keeps its index (RStick is 11), even the
// ones no action uses yet
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[allow(dead_code)]
pub enum Btn {
South, // A (Xbox, Nintendo), cross (PlayStation)
East, // B (Xbox), circle (PlayStation), A (Nintendo)
West, // X (Xbox), square (PlayStation), Y (Nintendo)
North, // Y (Xbox), triangle (PlayStation), X (Nintendo)
Lb,
Rb,
Lt,
Rt,
Select,
Start,
LStick,
RStick, // kept for the standard button index (11); no action uses it yet
Up,
Down,
Left,
Right,
}
impl Btn {
fn bit(self) -> u32 {
1 << self as u32
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Stick {
Left,
Right,
}
// The button pictograms the interface shows (Xbox, PlayStation, Nintendo).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Kind {
Xbox,
PlayStation,
Nintendo,
}
// Settings value: 0 automatic, 1 Xbox, 2 PlayStation, 3 Nintendo.
pub fn kind_for(setting: u8, detected: i32) -> Kind {
match setting {
1 => Kind::Xbox,
2 => Kind::PlayStation,
3 => Kind::Nintendo,
_ => match detected {
2 => Kind::PlayStation,
3 => Kind::Nintendo,
_ => Kind::Xbox,
},
}
}
thread_local! {
static KIND: Cell<Kind> = const { Cell::new(Kind::Xbox) };
}
// The kind whose pictograms the interface draws: the setting, or the automatic choice.
pub fn kind() -> Kind {
KIND.with(|c| c.get())
}
// Below this deflection a stick rests (controllers do not return to exactly 0).
const DEAD: f32 = 0.18;
pub struct Pad {
connected: bool,
buttons: u32,
prev: u32,
axes: [f32; 4],
pointer: (f32, f32), // device pixels of the interface
pointer_ready: bool,
}
impl Pad {
pub fn new() -> Pad {
Pad { connected: false, buttons: 0, prev: 0, axes: [0.0; 4], pointer: (0.0, 0.0), pointer_ready: false }
}
// Read the pad for this frame; kind_setting = Settings::gamepad.
pub fn poll(&mut self, kind_setting: u8) {
self.prev = if self.connected { self.buttons } else { 0 };
let slot = crate::platform::gamepad_slot();
self.connected = slot >= 0;
let detected = if self.connected { crate::platform::gamepad_kind(slot) } else { 0 };
KIND.with(|c| c.set(kind_for(kind_setting, detected)));
if !self.connected {
self.buttons = 0;
self.axes = [0.0; 4];
return;
}
self.buttons = crate::platform::gamepad_buttons(slot);
for (i, a) in self.axes.iter_mut().enumerate() {
*a = crate::platform::gamepad_axis(slot, i as i32);
}
}
pub fn connected(&self) -> bool {
self.connected
}
pub fn down(&self, b: Btn) -> bool {
self.connected && self.buttons & b.bit() != 0
}
pub fn pressed(&self, b: Btn) -> bool {
self.connected && self.buttons & b.bit() != 0 && self.prev & b.bit() == 0
}
pub fn released(&self, b: Btn) -> bool {
self.connected && self.buttons & b.bit() == 0 && self.prev & b.bit() != 0
}
// stick deflection, dead zone removed and scaled back to 0..1
pub fn stick(&self, side: Stick) -> Vec2 {
let (i, j) = match side {
Stick::Left => (0, 1),
Stick::Right => (2, 3),
};
let v = Vec2::new(self.axes[i], self.axes[j]);
let l = v.length();
if l <= DEAD {
Vec2::ZERO
} else {
v / l * ((l - DEAD) / (1.0 - DEAD))
}
}
// Move the interface pointer with the right stick. Mouse movements take it over at once, so
// mouse and pad can be used side by side; (w, h) is the interface size in device pixels.
// Returns the pointer in device pixels.
pub fn aim(&mut self, dt: f32, w: f32, h: f32, mouse: (f32, f32), mouse_moved: bool) -> (f32, f32) {
let w = w.max(1.0);
let h = h.max(1.0);
if !self.pointer_ready || mouse_moved {
self.pointer = (mouse.0.clamp(0.0, w), mouse.1.clamp(0.0, h));
self.pointer_ready = true;
}
let s = self.stick(Stick::Right);
let speed = w.max(640.0) * 1.15; // a full deflection crosses the screen in about a second
self.pointer.0 = (self.pointer.0 + s.x * speed * dt).clamp(0.0, w);
self.pointer.1 = (self.pointer.1 + s.y * speed * dt).clamp(0.0, h);
self.pointer
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_setting_picks_the_pictograms() {
assert_eq!(kind_for(0, 1), Kind::Xbox);
assert_eq!(kind_for(0, 2), Kind::PlayStation);
assert_eq!(kind_for(0, 3), Kind::Nintendo);
assert_eq!(kind_for(0, 0), Kind::Xbox);
assert_eq!(kind_for(2, 1), Kind::PlayStation);
assert_eq!(kind_for(3, 1), Kind::Nintendo);
assert_eq!(kind_for(1, 2), Kind::Xbox);
}
#[test]
fn sticks_have_a_dead_zone() {
let mut p = Pad::new();
p.connected = true;
p.axes = [0.1, -0.12, 0.0, 0.0];
assert_eq!(p.stick(Stick::Left), Vec2::ZERO);
p.axes = [1.0, 0.0, 0.0, 0.0];
let s = p.stick(Stick::Left);
assert!((s.x - 1.0).abs() < 0.001 && s.y.abs() < 0.001);
p.axes = [0.0, -0.59, 0.0, 0.0];
let s = p.stick(Stick::Left);
assert!((s.y + 0.5).abs() < 0.001);
}
#[test]
fn buttons_report_edges() {
let mut p = Pad::new();
p.connected = true;
p.buttons = 0;
p.prev = 0;
assert!(!p.down(Btn::South));
p.buttons = Btn::South.bit();
assert!(p.pressed(Btn::South) && p.down(Btn::South) && !p.released(Btn::South));
p.prev = p.buttons;
assert!(!p.pressed(Btn::South) && p.down(Btn::South));
p.buttons = 0;
p.prev = Btn::South.bit();
assert!(!p.down(Btn::South) && p.released(Btn::South));
}
#[test]
fn a_disconnected_pad_stays_silent() {
let mut p = Pad::new();
p.buttons = Btn::South.bit();
p.prev = 0;
assert!(!p.down(Btn::South));
assert!(!p.pressed(Btn::South));
}
#[test]
fn the_pointer_follows_the_mouse_and_the_stick() {
let mut p = Pad::new();
p.connected = true;
// the first frame takes the mouse position
assert_eq!(p.aim(0.016, 1000.0, 600.0, (100.0, 200.0), false), (100.0, 200.0));
// the right stick moves it
p.axes = [0.0, 0.0, 1.0, 0.0];
let (x, _) = p.aim(0.1, 1000.0, 600.0, (0.0, 0.0), false);
assert!(x > 200.0);
// a mouse movement takes it over again (a resting stick moves nothing)
p.axes = [0.0, 0.0, 0.0, 0.0];
assert_eq!(p.aim(0.1, 1000.0, 600.0, (5.0, 5.0), true), (5.0, 5.0));
}
}
+38
View File
@@ -36,6 +36,10 @@ mod imp {
fn dc_ws_take(id: i32) -> i32; fn dc_ws_take(id: i32) -> i32;
fn dc_ws_close(id: i32); fn dc_ws_close(id: i32);
fn dc_renderer() -> i32; fn dc_renderer() -> i32;
fn dc_gamepad_slot() -> i32;
fn dc_gamepad_buttons(slot: i32) -> u32;
fn dc_gamepad_axis(slot: i32, axis: i32) -> f32;
fn dc_gamepad_kind(slot: i32) -> i32;
} }
// starts a request; poll the returned id with http_poll() // starts a request; poll the returned id with http_poll()
@@ -189,6 +193,25 @@ mod imp {
String::from_utf8(buf).unwrap_or_default() String::from_utf8(buf).unwrap_or_default()
} }
} }
// the first connected gamepad (-1: none), see www/dc_plugin.js
pub fn gamepad_slot() -> i32 {
unsafe { dc_gamepad_slot() }
}
// bit i: standard button i is down
pub fn gamepad_buttons(slot: i32) -> u32 {
unsafe { dc_gamepad_buttons(slot) }
}
pub fn gamepad_axis(slot: i32, axis: i32) -> f32 {
unsafe { dc_gamepad_axis(slot, axis) }
}
// 0 unknown, 1 Xbox, 2 PlayStation, 3 Nintendo
pub fn gamepad_kind(slot: i32) -> i32 {
unsafe { dc_gamepad_kind(slot) }
}
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@@ -364,6 +387,21 @@ mod imp {
return "OpenGL".into(); return "OpenGL".into();
} }
} }
// The Gamepad API only exists in browsers. The desktop build has no gamepad input yet
// (miniquad does not poll gamepads; that would need a crate like gilrs).
pub fn gamepad_slot() -> i32 {
-1
}
pub fn gamepad_buttons(_slot: i32) -> u32 {
0
}
pub fn gamepad_axis(_slot: i32, _axis: i32) -> f32 {
0.0
}
pub fn gamepad_kind(_slot: i32) -> i32 {
0
}
} }
pub use imp::*; pub use imp::*;
+2
View File
@@ -153,6 +153,7 @@ pub struct Settings {
pub render_scale: u8, // old pixel-look divisor (no longer used, kept for old settings files) pub render_scale: u8, // old pixel-look divisor (no longer used, kept for old settings files)
pub pixel_rows: u16, // graphics style: rows of the world on every screen (270 pixel art, 1080 high-res) pub pixel_rows: u16, // graphics style: rows of the world on every screen (270 pixel art, 1080 high-res)
pub touch_ui: u8, // touch interface: 0 = automatic (after the first touch), 1 = on, 2 = off pub touch_ui: u8, // touch interface: 0 = automatic (after the first touch), 1 = on, 2 = off
pub gamepad: u8, // gamepad pictograms: 0 automatic (from the pad's id), 1 Xbox, 2 PlayStation, 3 Nintendo
pub debug_fps: bool, // show FPS and rendering backend pub debug_fps: bool, // show FPS and rendering backend
pub debug_frame_time: bool, // show the frame-time line and graph pub debug_frame_time: bool, // show the frame-time line and graph
pub debug_ping: bool, // show network ping pub debug_ping: bool, // show network ping
@@ -195,6 +196,7 @@ impl Default for Settings {
render_scale: 1, render_scale: 1,
pixel_rows: PIXEL_ART_ROWS, pixel_rows: PIXEL_ART_ROWS,
touch_ui: 0, touch_ui: 0,
gamepad: 0,
debug_fps: false, debug_fps: false,
debug_frame_time: false, debug_frame_time: false,
debug_ping: false, debug_ping: false,
+82 -4
View File
@@ -19,6 +19,7 @@ use macroquad::prelude::*;
// setting; automatic from the resolution in device pixels). // setting; automatic from the resolution in device pixels).
thread_local! { thread_local! {
static TOUCH: std::cell::Cell<bool> = std::cell::Cell::new(false); static TOUCH: std::cell::Cell<bool> = std::cell::Cell::new(false);
static GAMEPAD: std::cell::Cell<bool> = std::cell::Cell::new(false);
static POINTER: std::cell::Cell<(f32, f32)> = std::cell::Cell::new((-1e5, -1e5)); static POINTER: std::cell::Cell<(f32, f32)> = std::cell::Cell::new((-1e5, -1e5));
static VIRTUAL_ROWS: std::cell::Cell<u32> = std::cell::Cell::new(0); static VIRTUAL_ROWS: std::cell::Cell<u32> = std::cell::Cell::new(0);
static UI_TARGET: std::cell::RefCell<Option<(RenderTarget, u32, u32)>> = std::cell::RefCell::new(None); static UI_TARGET: std::cell::RefCell<Option<(RenderTarget, u32, u32)>> = std::cell::RefCell::new(None);
@@ -50,6 +51,13 @@ pub fn set_touch(on: bool) {
pub fn touch_ui() -> bool { pub fn touch_ui() -> bool {
TOUCH.with(|c| c.get()) TOUCH.with(|c| c.get())
} }
// a gamepad is connected: the interface shows its button pictograms (see pad.rs)
pub fn set_gamepad(on: bool) {
GAMEPAD.with(|c| c.set(on));
}
pub fn gamepad_ui() -> bool {
GAMEPAD.with(|c| c.get())
}
// in touch mode the pointer is the finger (device pixels), set by the input code each frame // in touch mode the pointer is the finger (device pixels), set by the input code each frame
pub fn set_pointer(x: f32, y: f32) { pub fn set_pointer(x: f32, y: f32) {
POINTER.with(|c| c.set((x, y))); POINTER.with(|c| c.set((x, y)));
@@ -70,7 +78,7 @@ pub fn sh() -> f32 {
(screen_height() * dpr()).round() (screen_height() * dpr()).round()
} }
pub fn mouse() -> (f32, f32) { pub fn mouse() -> (f32, f32) {
if touch_ui() { if touch_ui() || gamepad_ui() {
return POINTER.with(|c| c.get()); return POINTER.with(|c| c.get());
} }
let (x, y) = mouse_position(); let (x, y) = mouse_position();
@@ -443,6 +451,35 @@ pub const CATS: [(&str, &[usize]); 7] = [
("cat.orders", &[8, 45, 46, 7]), ("cat.orders", &[8, 45, 46, 7]),
]; ];
// The atlas cell of a gamepad button's pictogram, in the pad type's own symbols: Xbox A/B/X/Y,
// PlayStation cross/circle/square/triangle, Nintendo A/B/X/Y.
pub fn pad_icon(kind: crate::pad::Kind, b: crate::pad::Btn) -> u32 {
use crate::pad::{Btn, Kind};
match (kind, b) {
(Kind::Xbox, Btn::South) => I_PAD_XBOX_A,
(Kind::Xbox, Btn::East) => I_PAD_XBOX_B,
(Kind::Xbox, Btn::West) => I_PAD_XBOX_X,
(Kind::Xbox, Btn::North) => I_PAD_XBOX_Y,
(Kind::PlayStation, Btn::South) => I_PAD_PS_CROSS,
(Kind::PlayStation, Btn::East) => I_PAD_PS_CIRCLE,
(Kind::PlayStation, Btn::West) => I_PAD_PS_SQUARE,
(Kind::PlayStation, Btn::North) => I_PAD_PS_TRIANGLE,
(Kind::Nintendo, Btn::South) => I_PAD_NIN_A,
(Kind::Nintendo, Btn::East) => I_PAD_NIN_B,
(Kind::Nintendo, Btn::West) => I_PAD_NIN_X,
(Kind::Nintendo, Btn::North) => I_PAD_NIN_Y,
(_, Btn::Lb) => I_PAD_LB,
(_, Btn::Rb) => I_PAD_RB,
(_, Btn::Lt) => I_PAD_LT,
(_, Btn::Rt) => I_PAD_RT,
(_, Btn::Start) => I_PAD_START,
(_, Btn::Select) => I_PAD_SELECT,
(_, Btn::LStick) => I_PAD_STICK_L,
(_, Btn::RStick) => I_PAD_STICK_R,
_ => I_PAD_DPAD,
}
}
#[derive(Clone, Copy, PartialEq, Debug)] #[derive(Clone, Copy, PartialEq, Debug)]
enum TBtn { enum TBtn {
Cat(usize), Cat(usize),
@@ -897,7 +934,8 @@ impl Ui {
} }
// ---------------- toolbar: buttons of 6 x 3 characters, icons and labels on the grid // ---------------- toolbar: buttons of 6 x 3 characters, icons and labels on the grid
let tb_rows = 5; // (with a gamepad one row more: the pad's buttons as pictograms underneath)
let tb_rows = if gamepad_ui() { 6 } else { 5 };
let tb_y = sh - tb_rows as f32 * CH(); let tb_y = sh - tb_rows as f32 * CH();
let tb_y = (tb_y / CH()).floor() * CH(); let tb_y = (tb_y / CH()).floor() * CH();
let tb_h = sh - tb_y; let tb_h = sh - tb_y;
@@ -954,7 +992,7 @@ impl Ui {
let label = if tools.len() == 1 { ti.label.clone() } else { tr(key) }; let label = if tools.len() == 1 { ti.label.clone() } else { tr(key) };
let hovered = !touch_ui() && Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH()).contains(vec2(mx, my)); let hovered = !touch_ui() && Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH()).contains(vec2(mx, my));
// single tools show their key (desktop) and their description when hovered // single tools show their key (desktop) and their description when hovered
let key = if tools.len() == 1 && !touch_ui() { keys[tools[0]].as_str() } else { "" }; let key = if tools.len() == 1 && !touch_ui() && !gamepad_ui() { keys[tools[0]].as_str() } else { "" };
if hovered && tools.len() == 1 { if hovered && tools.len() == 1 {
hover_tool = Some(tools[0]); hover_tool = Some(tools[0]);
} }
@@ -1010,6 +1048,44 @@ impl Ui {
let ml: String = ml.chars().take(maxc).collect(); let ml: String = ml.chars().take(maxc).collect();
gfx.text(&ml, bx + (bw - gfx.measure(&ml, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT); gfx.text(&ml, bx + (bw - gfx.measure(&ml, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
self.touch_btns.push((r, TBtn::Menu)); self.touch_btns.push((r, TBtn::Menu));
// gamepad: its buttons as pictograms, on the extra row under the buttons
if gamepad_ui() {
let kind = crate::pad::kind();
let icon = 2.0 * CH(); // a 16 px atlas cell at 2*F
let gap = CW();
let south = [pad_icon(kind, crate::pad::Btn::South)];
let east = [pad_icon(kind, crate::pad::Btn::East)];
let shoulders = [I_PAD_LB, I_PAD_RB];
let start = [I_PAD_START];
let sticks = [I_PAD_STICK_L, I_PAD_STICK_R];
let triggers = [I_PAD_LT, I_PAD_RT];
let entries: [(&[u32], &str); 6] = [
(&south, "pad.use"),
(&east, "pad.back"),
(&shoulders, "pad.tool"),
(&start, "pad.menu"),
(&sticks, "pad.camera"),
(&triggers, "pad.zoom"),
];
let width = |icons: &[u32], label: &str| icon * icons.len() as f32 + gap * 0.5 * (icons.len() - 1) as f32 + gap + gfx.measure(label, SMALL());
let total = |n: usize| entries[..n].iter().map(|(i, k)| width(i, &tr(k))).sum::<f32>() + gap * 1.5 * (n - 1) as f32;
let avail = sw - 2.0 * CW();
let mut n = entries.len();
while n > 2 && total(n) > avail {
n -= 1;
}
let mut lx = grid_center(sw, total(n));
let ly = tb_y + 4.0 * CH();
for (icons, key) in entries.iter().take(n) {
for &id in icons.iter() {
gfx.draw_cell(id, lx, ly, icon, icon, WHITE);
lx += icon + gap * 0.5;
}
let label = tr(key);
gfx.text(&label, lx, ly + (icon - GH * SMALL()) * 0.5, SMALL(), TXT);
lx += gfx.measure(&label, SMALL()) + gap * 1.5;
}
}
// open category row: (category, x0, button width, columns) // open category row: (category, x0, button width, columns)
let popup = self.open_cat.map(|c| (c, x0, bw, bcols)); let popup = self.open_cat.map(|c| (c, x0, bw, bcols));
// touch: after choosing a tool its description is shown for a moment // touch: after choosing a tool its description is shown for a moment
@@ -1524,7 +1600,9 @@ impl Ui {
} }
fn draw_help(&mut self, gfx: &Gfx) { fn draw_help(&mut self, gfx: &Gfx) {
let controls: &[&str] = if touch_ui() { let controls: &[&str] = if gamepad_ui() {
&["help.p_move", "help.p_aim", "help.p_rot", "help.p_zoom", "help.p_use", "help.p_back", "help.p_tool"]
} else if touch_ui() {
&["help.t_tap", "help.t_drag", "help.t_two", "help.t_long", "help.t_double", "help.t_tools"] &["help.t_tap", "help.t_drag", "help.t_two", "help.t_long", "help.t_double", "help.t_tools"]
} else { } else {
&["help.k_left", "help.k_right", "help.k_slap", "help.k_pan", "help.k_rot", "help.k_reset", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"] &["help.k_left", "help.k_right", "help.k_slap", "help.k_pan", "help.k_rot", "help.k_reset", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"]
+70
View File
@@ -11,6 +11,16 @@
var fetched = { state: 0 }; // download of a file from the server: 0 idle, 1 running, 2 done, 3 failed var fetched = { state: 0 }; // download of a file from the server: 0 idle, 1 running, 2 done, 3 failed
var ws = {}; // socket id -> { state, queue, sock } var ws = {}; // socket id -> { state, queue, sock }
var wsNext = 1; var wsNext = 1;
var gpPad = null; // the gamepad handed out by dc_gamepad_slot (reused for its buttons/axes)
var gpSlot = -1;
// browsers offer the Gamepad API only in a secure context (https or localhost)
function gamepads() {
try {
return navigator.getGamepads ? navigator.getGamepads() : [];
} catch (e) {
return [];
}
}
function bytes(ptr, len) { function bytes(ptr, len) {
return new Uint8Array(wasm_memory.buffer, ptr, len); return new Uint8Array(wasm_memory.buffer, ptr, len);
@@ -193,6 +203,66 @@
env.dc_coarse_pointer = function () { env.dc_coarse_pointer = function () {
try { return window.matchMedia("(pointer: coarse)").matches ? 1 : 0; } catch (e) { return 0; } try { return window.matchMedia("(pointer: coarse)").matches ? 1 : 0; } catch (e) { return 0; }
}; };
// ---- gamepads: the game polls these functions once per frame (see src/pad.rs).
// The Gamepad API needs a secure context (https or localhost) and shows a pad only after
// its first button press. slot = the index in navigator.getGamepads(), -1 = no gamepad.
env.dc_gamepad_slot = function () {
gpPad = null;
gpSlot = -1;
var pads = gamepads();
for (var i = 0; i < pads.length; i++) {
if (pads[i] && pads[i].connected && pads[i].mapping === "standard") {
gpPad = pads[i];
gpSlot = i;
return i;
}
}
// a pad without the standard mapping (old DirectInput devices): use it anyway
for (var j = 0; j < pads.length; j++) {
if (pads[j] && pads[j].connected) {
gpPad = pads[j];
gpSlot = j;
return j;
}
}
return -1;
};
function gamepad(slot) {
if (gpPad && slot === gpSlot) return gpPad;
var pads = gamepads();
return pads[slot] && pads[slot].connected ? pads[slot] : null;
}
// bit i set: standard button i is down (triggers count from 40 % on)
env.dc_gamepad_buttons = function (slot) {
var p = gamepad(slot);
if (!p) return 0;
var m = 0;
for (var i = 0; i < p.buttons.length && i < 16; i++) {
var b = p.buttons[i];
if (b && (b.pressed || b.value > 0.4)) m |= 1 << i;
}
return m;
};
// axis 0/1 = left stick x/y, 2/3 = right stick x/y (all -1..1)
env.dc_gamepad_axis = function (slot, axis) {
var p = gamepad(slot);
if (!p || !p.axes || axis < 0 || axis >= p.axes.length) return 0;
return p.axes[axis];
};
// 0 unknown, 1 Xbox, 2 PlayStation, 3 Nintendo (from the pad's id)
// (the Xbox check comes first: its id contains "Wireless Controller" too)
env.dc_gamepad_kind = function (slot) {
var p = gamepad(slot);
if (!p) return 0;
var id = (p.id || "").toLowerCase();
if (id.indexOf("xbox") >= 0 || id.indexOf("xinput") >= 0 || id.indexOf("microsoft") >= 0) return 1;
if (id.indexOf("nintendo") >= 0 || id.indexOf("switch") >= 0 || id.indexOf("joy-con") >= 0 ||
id.indexOf("joycon") >= 0 || id.indexOf("pro controller") >= 0) return 3;
if (id.indexOf("playstation") >= 0 || id.indexOf("dualshock") >= 0 || id.indexOf("dualsense") >= 0 ||
id.indexOf("sony") >= 0 || id.indexOf("wireless controller") >= 0 || id.indexOf("054c") >= 0 ||
/(^|[^0-9])ps[345]([^0-9]|$)/.test(id)) return 2;
return 0;
};
env.dc_storage_set = function (kp, kl, vp, vl) { env.dc_storage_set = function (kp, kl, vp, vl) {
try { window.localStorage.setItem(str(kp, kl), str(vp, vl)); } catch (e) { } try { window.localStorage.setItem(str(kp, kl), str(vp, vl)); } catch (e) { }
}; };