Spaces:
Runtime error
Runtime error
Mobaile
Browse files
backend/game/commands.py
CHANGED
|
@@ -26,6 +26,7 @@ class ActionType(str, Enum):
|
|
| 26 |
QUERY = "query"
|
| 27 |
QUERY_UNITS = "query_units" # zone and/or unit_type → returns unit_ids
|
| 28 |
ASSIGN_TO_GROUP = "assign_to_group" # group_index (1-3), unit_ids (from query_units)
|
|
|
|
| 29 |
RESIGN = "resign" # abandon the game, opponent wins
|
| 30 |
|
| 31 |
|
|
|
|
| 26 |
QUERY = "query"
|
| 27 |
QUERY_UNITS = "query_units" # zone and/or unit_type → returns unit_ids
|
| 28 |
ASSIGN_TO_GROUP = "assign_to_group" # group_index (1-3), unit_ids (from query_units)
|
| 29 |
+
DEFEND = "defend" # patrol around a base/zone to defend it
|
| 30 |
RESIGN = "resign" # abandon the game, opponent wins
|
| 31 |
|
| 32 |
|
backend/game/engine.py
CHANGED
|
@@ -122,6 +122,7 @@ class GameEngine:
|
|
| 122 |
self._tick_mining(player)
|
| 123 |
|
| 124 |
self._tick_movement_and_combat()
|
|
|
|
| 125 |
self._tick_healing()
|
| 126 |
self._remove_dead()
|
| 127 |
|
|
@@ -719,6 +720,80 @@ class GameEngine:
|
|
| 719 |
ally.target_x = attacker.x
|
| 720 |
ally.target_y = attacker.y
|
| 721 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
def _tick_healing(self) -> None:
|
| 723 |
"""Medics heal the most-injured adjacent infantry unit."""
|
| 724 |
for player in self.state.players.values():
|
|
@@ -1191,6 +1266,8 @@ class GameEngine:
|
|
| 1191 |
return self._cmd_query_units(player, action)
|
| 1192 |
if t == ActionType.ASSIGN_TO_GROUP:
|
| 1193 |
return self._cmd_assign_to_group(player, action)
|
|
|
|
|
|
|
| 1194 |
if t == ActionType.RESIGN:
|
| 1195 |
return self._cmd_resign(player)
|
| 1196 |
return ActionResult(
|
|
@@ -1604,6 +1681,48 @@ class GameEngine:
|
|
| 1604 |
data={"gi": gi, "n": len(valid_ids)},
|
| 1605 |
)
|
| 1606 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1607 |
def _cmd_resign(self, player: PlayerState) -> ActionResult:
|
| 1608 |
"""Player forfeits — opponent is declared winner immediately."""
|
| 1609 |
opponent_id = next(
|
|
|
|
| 122 |
self._tick_mining(player)
|
| 123 |
|
| 124 |
self._tick_movement_and_combat()
|
| 125 |
+
self._apply_crowd_pressure()
|
| 126 |
self._tick_healing()
|
| 127 |
self._remove_dead()
|
| 128 |
|
|
|
|
| 720 |
ally.target_x = attacker.x
|
| 721 |
ally.target_y = attacker.y
|
| 722 |
|
| 723 |
+
def _apply_crowd_pressure(self) -> None:
|
| 724 |
+
"""Nudge ground units that are not firing when allied moving units press behind them.
|
| 725 |
+
|
| 726 |
+
For each non-firing idle/moving ground unit, we sum the pressure vectors of
|
| 727 |
+
nearby allies that are actively advancing toward it (within a ~3-unit cone).
|
| 728 |
+
A small fraction of the resulting vector is applied as a nudge, provided the
|
| 729 |
+
destination is collision-free.
|
| 730 |
+
"""
|
| 731 |
+
PUSH_RADIUS = UNIT_RADIUS * 5 # ~2.5 tile look-ahead for pushers
|
| 732 |
+
NUDGE = UNIT_RADIUS * 0.35 # ≈0.175 tiles per tick (soft shove)
|
| 733 |
+
MIN_DOT = 0.45 # cos ~63° — pusher must face the blockee
|
| 734 |
+
|
| 735 |
+
for player in self.state.players.values():
|
| 736 |
+
for unit in player.units.values():
|
| 737 |
+
defn = UNIT_DEFS[unit.unit_type]
|
| 738 |
+
# Only nudge ground units that are not shooting and not doing special work
|
| 739 |
+
if defn.is_flying:
|
| 740 |
+
continue
|
| 741 |
+
if unit.attack_target_id or unit.attack_target_building_id:
|
| 742 |
+
continue
|
| 743 |
+
if unit.is_sieged:
|
| 744 |
+
continue
|
| 745 |
+
if unit.status in (
|
| 746 |
+
UnitStatus.MINING_MINERALS,
|
| 747 |
+
UnitStatus.MINING_GAS,
|
| 748 |
+
UnitStatus.BUILDING,
|
| 749 |
+
UnitStatus.MOVING_TO_BUILD,
|
| 750 |
+
):
|
| 751 |
+
continue
|
| 752 |
+
|
| 753 |
+
fx, fy = 0.0, 0.0
|
| 754 |
+
for pusher in player.units.values():
|
| 755 |
+
if pusher.id == unit.id:
|
| 756 |
+
continue
|
| 757 |
+
if pusher.status not in (UnitStatus.MOVING, UnitStatus.ATTACKING, UnitStatus.PATROLLING):
|
| 758 |
+
continue
|
| 759 |
+
if pusher.target_x is None or pusher.target_y is None:
|
| 760 |
+
continue
|
| 761 |
+
|
| 762 |
+
dx_rel = unit.x - pusher.x
|
| 763 |
+
dy_rel = unit.y - pusher.y
|
| 764 |
+
d = math.hypot(dx_rel, dy_rel)
|
| 765 |
+
if d > PUSH_RADIUS or d < 1e-6:
|
| 766 |
+
continue
|
| 767 |
+
|
| 768 |
+
# Direction the pusher wants to go
|
| 769 |
+
dx_m = pusher.target_x - pusher.x
|
| 770 |
+
dy_m = pusher.target_y - pusher.y
|
| 771 |
+
dm = math.hypot(dx_m, dy_m)
|
| 772 |
+
if dm < 1e-6:
|
| 773 |
+
continue
|
| 774 |
+
nx_m, ny_m = dx_m / dm, dy_m / dm
|
| 775 |
+
|
| 776 |
+
# The blockee must be roughly in the pusher's forward cone
|
| 777 |
+
dot = (dx_rel / d) * nx_m + (dy_rel / d) * ny_m
|
| 778 |
+
if dot < MIN_DOT:
|
| 779 |
+
continue
|
| 780 |
+
|
| 781 |
+
# Pressure strength: stronger when closer and more aligned
|
| 782 |
+
strength = dot * (1.0 - d / PUSH_RADIUS)
|
| 783 |
+
fx += nx_m * strength
|
| 784 |
+
fy += ny_m * strength
|
| 785 |
+
|
| 786 |
+
fmag = math.hypot(fx, fy)
|
| 787 |
+
if fmag < 1e-6:
|
| 788 |
+
continue
|
| 789 |
+
|
| 790 |
+
new_x = unit.x + (fx / fmag) * NUDGE
|
| 791 |
+
new_y = unit.y + (fy / fmag) * NUDGE
|
| 792 |
+
|
| 793 |
+
if not self._would_overlap(unit, new_x, new_y):
|
| 794 |
+
unit.x = new_x
|
| 795 |
+
unit.y = new_y
|
| 796 |
+
|
| 797 |
def _tick_healing(self) -> None:
|
| 798 |
"""Medics heal the most-injured adjacent infantry unit."""
|
| 799 |
for player in self.state.players.values():
|
|
|
|
| 1266 |
return self._cmd_query_units(player, action)
|
| 1267 |
if t == ActionType.ASSIGN_TO_GROUP:
|
| 1268 |
return self._cmd_assign_to_group(player, action)
|
| 1269 |
+
if t == ActionType.DEFEND:
|
| 1270 |
+
return self._cmd_defend(player, action)
|
| 1271 |
if t == ActionType.RESIGN:
|
| 1272 |
return self._cmd_resign(player)
|
| 1273 |
return ActionResult(
|
|
|
|
| 1681 |
data={"gi": gi, "n": len(valid_ids)},
|
| 1682 |
)
|
| 1683 |
|
| 1684 |
+
def _cmd_defend(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1685 |
+
"""Send available military units to patrol around a base zone.
|
| 1686 |
+
|
| 1687 |
+
Units are spread evenly on a circle around the zone center and bounce
|
| 1688 |
+
between two diametrically-opposite perimeter points so they continuously
|
| 1689 |
+
sweep the area and auto-attack any enemy that enters their weapon range.
|
| 1690 |
+
"""
|
| 1691 |
+
units = self._resolve_selector(
|
| 1692 |
+
player, action.unit_selector or "all_military", max_count=action.count
|
| 1693 |
+
)
|
| 1694 |
+
units = [u for u in units if not u.is_sieged and u.unit_type != UnitType.SCV]
|
| 1695 |
+
if not units:
|
| 1696 |
+
return ActionResult(action_type="defend", success=False, data={"error": "no_units_selected"})
|
| 1697 |
+
|
| 1698 |
+
zone = action.target_zone or "my_base"
|
| 1699 |
+
cx, cy = self._resolve_zone(player.player_id, zone)
|
| 1700 |
+
|
| 1701 |
+
DEFEND_RADIUS = 6.0 # patrol orbit radius around the base center
|
| 1702 |
+
n = len(units)
|
| 1703 |
+
for i, unit in enumerate(units):
|
| 1704 |
+
angle = (2 * math.pi * i) / n
|
| 1705 |
+
# Two opposite points on the perimeter
|
| 1706 |
+
px1 = cx + DEFEND_RADIUS * math.cos(angle)
|
| 1707 |
+
py1 = cy + DEFEND_RADIUS * math.sin(angle)
|
| 1708 |
+
px2 = cx + DEFEND_RADIUS * math.cos(angle + math.pi)
|
| 1709 |
+
py2 = cy + DEFEND_RADIUS * math.sin(angle + math.pi)
|
| 1710 |
+
|
| 1711 |
+
is_flying = UNIT_DEFS[unit.unit_type].is_flying
|
| 1712 |
+
unit.attack_target_id = None
|
| 1713 |
+
unit.attack_target_building_id = None
|
| 1714 |
+
# Store the far patrol point as the return waypoint
|
| 1715 |
+
unit.patrol_x = px2
|
| 1716 |
+
unit.patrol_y = py2
|
| 1717 |
+
unit.status = UnitStatus.PATROLLING
|
| 1718 |
+
self._set_unit_destination(unit, px1, py1, is_flying=is_flying)
|
| 1719 |
+
|
| 1720 |
+
move_ack = [{"kind": "move_ack", "unit_type": units[0].unit_type.value}]
|
| 1721 |
+
return ActionResult(
|
| 1722 |
+
action_type="defend", success=True,
|
| 1723 |
+
data={"n": len(units), "zone": zone}, sound_events=move_ack,
|
| 1724 |
+
)
|
| 1725 |
+
|
| 1726 |
def _cmd_resign(self, player: PlayerState) -> ActionResult:
|
| 1727 |
"""Player forfeits — opponent is declared winner immediately."""
|
| 1728 |
opponent_id = next(
|
backend/voice/command_parser.py
CHANGED
|
@@ -42,6 +42,7 @@ supply_depot, barracks, engineering_bay, refinery, factory, armory, starport
|
|
| 42 |
- gather : collecter ressources → champs: unit_selector, resource_type ("minerals"|"gas"), count (optionnel)
|
| 43 |
- stop : arrêter des unités → champs: unit_selector, count (optionnel)
|
| 44 |
- patrol : patrouiller → champs: unit_selector, target_zone, count (optionnel)
|
|
|
|
| 45 |
- query : information sur l'état → aucun champ requis
|
| 46 |
- query_units : lister les IDs d'unités (pour affectation aux groupes) → champs optionnels: target_zone, unit_type
|
| 47 |
- assign_to_group : affecter des unités à un groupe (1, 2 ou 3) → champs: group_index (1-3). Utilise le résultat du query_units précédent si unit_ids vide. Pour "mets mes marines dans le groupe 1", génère d'abord query_units (unit_type: marine) puis assign_to_group (group_index: 1).
|
|
|
|
| 42 |
- gather : collecter ressources → champs: unit_selector, resource_type ("minerals"|"gas"), count (optionnel)
|
| 43 |
- stop : arrêter des unités → champs: unit_selector, count (optionnel)
|
| 44 |
- patrol : patrouiller → champs: unit_selector, target_zone, count (optionnel)
|
| 45 |
+
- defend : défendre une base (unités militaires patrouillent en cercle autour de la zone en attaquant automatiquement) → champs: unit_selector (optionnel), target_zone (défaut: my_base), count (optionnel). Utiliser quand le joueur dit "défends ma base", "protège la base", "garde la base", "defend my base", "hold the base", etc.
|
| 46 |
- query : information sur l'état → aucun champ requis
|
| 47 |
- query_units : lister les IDs d'unités (pour affectation aux groupes) → champs optionnels: target_zone, unit_type
|
| 48 |
- assign_to_group : affecter des unités à un groupe (1, 2 ou 3) → champs: group_index (1-3). Utilise le résultat du query_units précédent si unit_ids vide. Pour "mets mes marines dans le groupe 1", génère d'abord query_units (unit_type: marine) puis assign_to_group (group_index: 1).
|
frontend/src/lib/components/Minimap.svelte
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { backendUrl } from '$lib/socket';
|
| 3 |
-
import { gameState, myPlayerId, mapViewport, mapCenterRequest, mapVisibleCells, mapExploredCells, isTutorial } from '$lib/stores/game';
|
| 4 |
import { BUILDING_SIZES } from '$lib/types';
|
| 5 |
|
| 6 |
-
$: mapImageUrl = typeof window !== 'undefined' ? `${backendUrl()}/static/MAP.png` : '';
|
| 7 |
|
| 8 |
const MAP_W = 80;
|
| 9 |
const MAP_H = 80;
|
|
@@ -32,13 +32,20 @@
|
|
| 32 |
return visibleCells.has(key);
|
| 33 |
}
|
| 34 |
|
| 35 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
e.preventDefault();
|
| 37 |
e.stopPropagation();
|
| 38 |
const target = e.currentTarget as HTMLButtonElement;
|
| 39 |
const rect = target.getBoundingClientRect();
|
| 40 |
-
const
|
| 41 |
-
const
|
|
|
|
| 42 |
const cx = px * MAP_W;
|
| 43 |
const cy = py * MAP_H;
|
| 44 |
mapCenterRequest.set({ cx, cy });
|
|
@@ -50,6 +57,7 @@
|
|
| 50 |
class="minimap-btn"
|
| 51 |
aria-label="Minimap: click to move view"
|
| 52 |
on:click={onClick}
|
|
|
|
| 53 |
>
|
| 54 |
<svg
|
| 55 |
bind:this={minimapEl}
|
|
@@ -164,6 +172,9 @@
|
|
| 164 |
cursor: pointer;
|
| 165 |
overflow: hidden;
|
| 166 |
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
|
|
|
|
|
|
|
|
|
| 167 |
}
|
| 168 |
|
| 169 |
.minimap-btn:hover {
|
|
@@ -177,4 +188,11 @@
|
|
| 177 |
height: 160px;
|
| 178 |
pointer-events: none;
|
| 179 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
</style>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { backendUrl } from '$lib/socket';
|
| 3 |
+
import { gameState, myPlayerId, mapViewport, mapCenterRequest, mapVisibleCells, mapExploredCells, isTutorial, mapTextureUrl } from '$lib/stores/game';
|
| 4 |
import { BUILDING_SIZES } from '$lib/types';
|
| 5 |
|
| 6 |
+
$: mapImageUrl = $mapTextureUrl ?? (typeof window !== 'undefined' ? `${backendUrl()}/static/MAP.png` : '');
|
| 7 |
|
| 8 |
const MAP_W = 80;
|
| 9 |
const MAP_H = 80;
|
|
|
|
| 32 |
return visibleCells.has(key);
|
| 33 |
}
|
| 34 |
|
| 35 |
+
function getClickCoords(e: MouseEvent | TouchEvent): { clientX: number; clientY: number } {
|
| 36 |
+
if ('touches' in e && e.touches.length > 0) return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };
|
| 37 |
+
if ('changedTouches' in e && e.changedTouches.length > 0) return { clientX: e.changedTouches[0].clientX, clientY: e.changedTouches[0].clientY };
|
| 38 |
+
return { clientX: (e as MouseEvent).clientX, clientY: (e as MouseEvent).clientY };
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function onClick(e: MouseEvent | TouchEvent) {
|
| 42 |
e.preventDefault();
|
| 43 |
e.stopPropagation();
|
| 44 |
const target = e.currentTarget as HTMLButtonElement;
|
| 45 |
const rect = target.getBoundingClientRect();
|
| 46 |
+
const { clientX, clientY } = getClickCoords(e);
|
| 47 |
+
const px = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
| 48 |
+
const py = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
|
| 49 |
const cx = px * MAP_W;
|
| 50 |
const cy = py * MAP_H;
|
| 51 |
mapCenterRequest.set({ cx, cy });
|
|
|
|
| 57 |
class="minimap-btn"
|
| 58 |
aria-label="Minimap: click to move view"
|
| 59 |
on:click={onClick}
|
| 60 |
+
on:touchend|nonpassive={onClick}
|
| 61 |
>
|
| 62 |
<svg
|
| 63 |
bind:this={minimapEl}
|
|
|
|
| 172 |
cursor: pointer;
|
| 173 |
overflow: hidden;
|
| 174 |
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
| 175 |
+
touch-action: manipulation;
|
| 176 |
+
-webkit-touch-callout: none;
|
| 177 |
+
user-select: none;
|
| 178 |
}
|
| 179 |
|
| 180 |
.minimap-btn:hover {
|
|
|
|
| 188 |
height: 160px;
|
| 189 |
pointer-events: none;
|
| 190 |
}
|
| 191 |
+
|
| 192 |
+
@media (max-width: 600px) {
|
| 193 |
+
.minimap-svg {
|
| 194 |
+
width: 80px;
|
| 195 |
+
height: 80px;
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
</style>
|
frontend/src/lib/components/VoiceButton.svelte
CHANGED
|
@@ -113,6 +113,10 @@
|
|
| 113 |
transition: all 0.15s;
|
| 114 |
flex-shrink: 0;
|
| 115 |
overflow: visible;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
.voice-btn:not(:disabled):active,
|
|
|
|
| 113 |
transition: all 0.15s;
|
| 114 |
flex-shrink: 0;
|
| 115 |
overflow: visible;
|
| 116 |
+
touch-action: none;
|
| 117 |
+
-webkit-touch-callout: none;
|
| 118 |
+
user-select: none;
|
| 119 |
+
-webkit-user-select: none;
|
| 120 |
}
|
| 121 |
|
| 122 |
.voice-btn:not(:disabled):active,
|
frontend/src/routes/+page.svelte
CHANGED
|
@@ -1562,4 +1562,106 @@
|
|
| 1562 |
.activity-toast-link:hover {
|
| 1563 |
color: #c4b5fd;
|
| 1564 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1565 |
</style>
|
|
|
|
| 1562 |
.activity-toast-link:hover {
|
| 1563 |
color: #c4b5fd;
|
| 1564 |
}
|
| 1565 |
+
|
| 1566 |
+
@media (max-width: 480px) {
|
| 1567 |
+
.lobby {
|
| 1568 |
+
--banner-height: 38px;
|
| 1569 |
+
}
|
| 1570 |
+
|
| 1571 |
+
.powered-banner {
|
| 1572 |
+
gap: 2px;
|
| 1573 |
+
padding: 4px 8px;
|
| 1574 |
+
}
|
| 1575 |
+
|
| 1576 |
+
.powered-label {
|
| 1577 |
+
display: none;
|
| 1578 |
+
}
|
| 1579 |
+
|
| 1580 |
+
.powered-separator {
|
| 1581 |
+
margin: 0 2px;
|
| 1582 |
+
height: 14px;
|
| 1583 |
+
}
|
| 1584 |
+
|
| 1585 |
+
.powered-logo {
|
| 1586 |
+
padding: 2px 3px;
|
| 1587 |
+
}
|
| 1588 |
+
|
| 1589 |
+
.powered-logo img {
|
| 1590 |
+
height: 14px;
|
| 1591 |
+
max-width: 50px;
|
| 1592 |
+
}
|
| 1593 |
+
|
| 1594 |
+
.powered-author-name {
|
| 1595 |
+
display: none;
|
| 1596 |
+
}
|
| 1597 |
+
|
| 1598 |
+
.powered-author img {
|
| 1599 |
+
height: 14px;
|
| 1600 |
+
width: 14px;
|
| 1601 |
+
}
|
| 1602 |
+
|
| 1603 |
+
.lobby {
|
| 1604 |
+
padding: var(--banner-height) 12px 20px;
|
| 1605 |
+
gap: 12px;
|
| 1606 |
+
}
|
| 1607 |
+
|
| 1608 |
+
.lobby-content {
|
| 1609 |
+
gap: 12px;
|
| 1610 |
+
}
|
| 1611 |
+
|
| 1612 |
+
.btn {
|
| 1613 |
+
padding: 10px 14px;
|
| 1614 |
+
font-size: 0.85rem;
|
| 1615 |
+
gap: 6px;
|
| 1616 |
+
}
|
| 1617 |
+
|
| 1618 |
+
.btn-tutorial {
|
| 1619 |
+
margin-top: 6px;
|
| 1620 |
+
}
|
| 1621 |
+
|
| 1622 |
+
.btn-icon {
|
| 1623 |
+
font-size: 0.95rem;
|
| 1624 |
+
}
|
| 1625 |
+
|
| 1626 |
+
.name-input-row .btn {
|
| 1627 |
+
padding: 10px 12px;
|
| 1628 |
+
}
|
| 1629 |
+
|
| 1630 |
+
.text-input {
|
| 1631 |
+
padding: 10px 12px;
|
| 1632 |
+
font-size: 0.9rem;
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
.home-panel {
|
| 1636 |
+
padding: 14px;
|
| 1637 |
+
gap: 12px;
|
| 1638 |
+
}
|
| 1639 |
+
|
| 1640 |
+
.observe-row {
|
| 1641 |
+
gap: 8px;
|
| 1642 |
+
}
|
| 1643 |
+
|
| 1644 |
+
.btn-observe {
|
| 1645 |
+
padding: 8px 12px;
|
| 1646 |
+
font-size: 0.82rem;
|
| 1647 |
+
}
|
| 1648 |
+
|
| 1649 |
+
.leaderboard-fab,
|
| 1650 |
+
.help-btn {
|
| 1651 |
+
width: 36px;
|
| 1652 |
+
height: 36px;
|
| 1653 |
+
bottom: 12px;
|
| 1654 |
+
border-width: 2px;
|
| 1655 |
+
}
|
| 1656 |
+
|
| 1657 |
+
.leaderboard-fab {
|
| 1658 |
+
left: 12px;
|
| 1659 |
+
font-size: 1rem;
|
| 1660 |
+
}
|
| 1661 |
+
|
| 1662 |
+
.help-btn {
|
| 1663 |
+
right: 12px;
|
| 1664 |
+
font-size: 1.1rem;
|
| 1665 |
+
}
|
| 1666 |
+
}
|
| 1667 |
</style>
|