Spaces:
Runtime error
Runtime error
Cancel Culture
Browse files
backend/game/buildings.py
CHANGED
|
@@ -34,12 +34,12 @@ class BuildingDef(BaseModel):
|
|
| 34 |
height: int = 2
|
| 35 |
supply_provided: int = 0
|
| 36 |
def col_hw(self) -> float:
|
| 37 |
-
"""Half collision width
|
| 38 |
-
return max(0.
|
| 39 |
|
| 40 |
def col_hh(self) -> float:
|
| 41 |
-
"""Half collision height
|
| 42 |
-
return max(0.
|
| 43 |
|
| 44 |
|
| 45 |
BUILDING_DEFS: dict[BuildingType, BuildingDef] = {
|
|
|
|
| 34 |
height: int = 2
|
| 35 |
supply_provided: int = 0
|
| 36 |
def col_hw(self) -> float:
|
| 37 |
+
"""Half collision width (~1/3 of visual size) so units can pass between adjacent buildings."""
|
| 38 |
+
return max(0.2, self.width / 6)
|
| 39 |
|
| 40 |
def col_hh(self) -> float:
|
| 41 |
+
"""Half collision height (~1/3 of visual size) so units can pass between adjacent buildings."""
|
| 42 |
+
return max(0.2, self.height / 6)
|
| 43 |
|
| 44 |
|
| 45 |
BUILDING_DEFS: dict[BuildingType, BuildingDef] = {
|
backend/game/engine.py
CHANGED
|
@@ -1504,6 +1504,49 @@ class GameEngine:
|
|
| 1504 |
self.state.winner = opponent_id
|
| 1505 |
return ActionResult(action_type="resign", success=True, data={})
|
| 1506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1507 |
# ------------------------------------------------------------------
|
| 1508 |
# Broadcast
|
| 1509 |
# ------------------------------------------------------------------
|
|
|
|
| 1504 |
self.state.winner = opponent_id
|
| 1505 |
return ActionResult(action_type="resign", success=True, data={})
|
| 1506 |
|
| 1507 |
+
def cancel_building_construction(self, player_id: str, building_id: str) -> ActionResult:
|
| 1508 |
+
"""Cancel an in-progress construction: remove the building and refund 75% of costs."""
|
| 1509 |
+
player = self.state.players.get(player_id)
|
| 1510 |
+
if not player:
|
| 1511 |
+
return ActionResult(action_type="cancel_construction", success=False, data={"error": "player_not_found"})
|
| 1512 |
+
|
| 1513 |
+
building = player.buildings.get(building_id)
|
| 1514 |
+
if not building:
|
| 1515 |
+
return ActionResult(action_type="cancel_construction", success=False, data={"error": "building_not_found"})
|
| 1516 |
+
if building.status != BuildingStatus.CONSTRUCTING:
|
| 1517 |
+
return ActionResult(action_type="cancel_construction", success=False, data={"error": "not_under_construction"})
|
| 1518 |
+
|
| 1519 |
+
defn = BUILDING_DEFS[building.building_type]
|
| 1520 |
+
|
| 1521 |
+
# Free the SCV assigned to this building
|
| 1522 |
+
for unit in player.units.values():
|
| 1523 |
+
if unit.building_target_id == building_id:
|
| 1524 |
+
unit.building_target_id = None
|
| 1525 |
+
unit.target_x = unit.target_y = None
|
| 1526 |
+
unit.path_waypoints = []
|
| 1527 |
+
unit.status = UnitStatus.IDLE
|
| 1528 |
+
|
| 1529 |
+
# If it's a refinery, mark the underlying geyser as free again
|
| 1530 |
+
if building.building_type == BuildingType.REFINERY:
|
| 1531 |
+
geyser_x = building.x - 1.0
|
| 1532 |
+
geyser_y = building.y - 1.0
|
| 1533 |
+
for resource in self.state.game_map.resources:
|
| 1534 |
+
if resource.resource_type == ResourceType.GEYSER and abs(resource.x - geyser_x) < 0.5 and abs(resource.y - geyser_y) < 0.5:
|
| 1535 |
+
resource.has_refinery = False
|
| 1536 |
+
break
|
| 1537 |
+
|
| 1538 |
+
del player.buildings[building_id]
|
| 1539 |
+
invalidate_path_cache()
|
| 1540 |
+
|
| 1541 |
+
# Refund 75% of the original cost (rounded down)
|
| 1542 |
+
player.minerals += int(defn.mineral_cost * 0.75)
|
| 1543 |
+
player.gas += int(defn.gas_cost * 0.75)
|
| 1544 |
+
|
| 1545 |
+
return ActionResult(
|
| 1546 |
+
action_type="cancel_construction", success=True,
|
| 1547 |
+
data={"building": building.building_type.value, "refund_minerals": int(defn.mineral_cost * 0.75), "refund_gas": int(defn.gas_cost * 0.75)},
|
| 1548 |
+
)
|
| 1549 |
+
|
| 1550 |
# ------------------------------------------------------------------
|
| 1551 |
# Broadcast
|
| 1552 |
# ------------------------------------------------------------------
|
backend/main.py
CHANGED
|
@@ -953,6 +953,26 @@ async def accept_bot(sid: str, data: dict) -> None:
|
|
| 953 |
await _start_game(room_obj.room_id)
|
| 954 |
|
| 955 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 956 |
@sio.event
|
| 957 |
async def start_tutorial(sid: str, data: dict) -> None:
|
| 958 |
"""Start a solo tutorial game for the player."""
|
|
|
|
| 953 |
await _start_game(room_obj.room_id)
|
| 954 |
|
| 955 |
|
| 956 |
+
@sio.event
|
| 957 |
+
async def cancel_construction(sid: str, data: dict) -> None:
|
| 958 |
+
"""Cancel an in-progress building construction and refund 75% of resources."""
|
| 959 |
+
room = lobby.get_room_for_sid(sid)
|
| 960 |
+
if not room:
|
| 961 |
+
await _emit_error(sid, "Tu n'es dans aucune room.")
|
| 962 |
+
return
|
| 963 |
+
engine = engines.get(room.room_id)
|
| 964 |
+
if not engine:
|
| 965 |
+
await _emit_error(sid, "La partie n'a pas encore commencé.")
|
| 966 |
+
return
|
| 967 |
+
building_id: str = data.get("building_id", "")
|
| 968 |
+
if not building_id:
|
| 969 |
+
await _emit_error(sid, "ID de bâtiment manquant.")
|
| 970 |
+
return
|
| 971 |
+
result = engine.cancel_building_construction(sid, building_id)
|
| 972 |
+
if not result.success:
|
| 973 |
+
await _emit_error(sid, f"Impossible d'annuler: {result.data.get('error', '?')}")
|
| 974 |
+
|
| 975 |
+
|
| 976 |
@sio.event
|
| 977 |
async def start_tutorial(sid: str, data: dict) -> None:
|
| 978 |
"""Start a solo tutorial game for the player."""
|
frontend/src/lib/components/HelpModal.svelte
CHANGED
|
@@ -1,5 +1,8 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
export let open = false;
|
|
|
|
|
|
|
|
|
|
| 3 |
</script>
|
| 4 |
|
| 5 |
{#if open}
|
|
@@ -127,14 +130,16 @@
|
|
| 127 |
<h3 class="help-section-title">🏗️ Tech Tree</h3>
|
| 128 |
<div class="tech-tree">
|
| 129 |
<div class="tech-row">
|
| 130 |
-
<div class="tech-node tech-start">
|
|
|
|
| 131 |
<img class="tech-sprite" src="/sprites/buildings/command_center.png" alt="" />
|
| 132 |
Command Center<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />400</span>
|
| 133 |
</div>
|
| 134 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/scv.png" alt="SCV" /> SCV</div>
|
| 135 |
</div>
|
| 136 |
<div class="tech-row tech-indent">
|
| 137 |
-
<div class="tech-node">
|
|
|
|
| 138 |
<img class="tech-sprite" src="/sprites/buildings/supply_depot.png" alt="" />
|
| 139 |
Supply Depot<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />100</span>
|
| 140 |
</div>
|
|
@@ -143,42 +148,48 @@
|
|
| 143 |
</div>
|
| 144 |
</div>
|
| 145 |
<div class="tech-row tech-indent2">
|
| 146 |
-
<div class="tech-node">
|
|
|
|
| 147 |
<img class="tech-sprite" src="/sprites/buildings/barracks.png" alt="" />
|
| 148 |
Barracks<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />150</span>
|
| 149 |
</div>
|
| 150 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/marine.png" alt="Marine" /> Marine</div>
|
| 151 |
</div>
|
| 152 |
<div class="tech-row tech-indent3">
|
| 153 |
-
<div class="tech-node">
|
|
|
|
| 154 |
<img class="tech-sprite" src="/sprites/buildings/engineering_bay.png" alt="" />
|
| 155 |
Eng. Bay<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />125</span>
|
| 156 |
</div>
|
| 157 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/medic.png" alt="Medic" /> Medic</div>
|
| 158 |
</div>
|
| 159 |
<div class="tech-row tech-indent3">
|
| 160 |
-
<div class="tech-node">
|
|
|
|
| 161 |
<img class="tech-sprite" src="/sprites/buildings/factory.png" alt="" />
|
| 162 |
Factory<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />200 <img class="cost-icon" src="/sprites/icons/gas.png" alt="gas" />100</span>
|
| 163 |
</div>
|
| 164 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/goliath.png" alt="Goliath" /> Goliath</div>
|
| 165 |
</div>
|
| 166 |
<div class="tech-row tech-indent4">
|
| 167 |
-
<div class="tech-node">
|
|
|
|
| 168 |
<img class="tech-sprite" src="/sprites/buildings/armory.png" alt="" />
|
| 169 |
Armory<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />100 <img class="cost-icon" src="/sprites/icons/gas.png" alt="gas" />50</span>
|
| 170 |
</div>
|
| 171 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/tank.png" alt="Tank" /> Tank</div>
|
| 172 |
</div>
|
| 173 |
<div class="tech-row tech-indent4">
|
| 174 |
-
<div class="tech-node">
|
|
|
|
| 175 |
<img class="tech-sprite" src="/sprites/buildings/starport.png" alt="" />
|
| 176 |
Starport<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />150 <img class="cost-icon" src="/sprites/icons/gas.png" alt="gas" />100</span>
|
| 177 |
</div>
|
| 178 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/wraith.png" alt="Wraith" /> Wraith</div>
|
| 179 |
</div>
|
| 180 |
<div class="tech-row tech-indent">
|
| 181 |
-
<div class="tech-node">
|
|
|
|
| 182 |
<img class="tech-sprite" src="/sprites/buildings/refinery.png" alt="" />
|
| 183 |
Refinery<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />100</span>
|
| 184 |
</div>
|
|
@@ -425,6 +436,19 @@
|
|
| 425 |
|
| 426 |
.tech-start { border-color: var(--accent); color: var(--accent); }
|
| 427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
.tech-cost {
|
| 429 |
font-size: 0.7rem;
|
| 430 |
color: var(--text-muted);
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
export let open = false;
|
| 3 |
+
export let builtBuildings: string[] = [];
|
| 4 |
+
|
| 5 |
+
$: built = new Set(builtBuildings);
|
| 6 |
</script>
|
| 7 |
|
| 8 |
{#if open}
|
|
|
|
| 130 |
<h3 class="help-section-title">🏗️ Tech Tree</h3>
|
| 131 |
<div class="tech-tree">
|
| 132 |
<div class="tech-row">
|
| 133 |
+
<div class="tech-node tech-start" class:tech-built={built.has('command_center')}>
|
| 134 |
+
{#if built.has('command_center')}<span class="tech-check">✓</span>{/if}
|
| 135 |
<img class="tech-sprite" src="/sprites/buildings/command_center.png" alt="" />
|
| 136 |
Command Center<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />400</span>
|
| 137 |
</div>
|
| 138 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/scv.png" alt="SCV" /> SCV</div>
|
| 139 |
</div>
|
| 140 |
<div class="tech-row tech-indent">
|
| 141 |
+
<div class="tech-node" class:tech-built={built.has('supply_depot')}>
|
| 142 |
+
{#if built.has('supply_depot')}<span class="tech-check">✓</span>{/if}
|
| 143 |
<img class="tech-sprite" src="/sprites/buildings/supply_depot.png" alt="" />
|
| 144 |
Supply Depot<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />100</span>
|
| 145 |
</div>
|
|
|
|
| 148 |
</div>
|
| 149 |
</div>
|
| 150 |
<div class="tech-row tech-indent2">
|
| 151 |
+
<div class="tech-node" class:tech-built={built.has('barracks')}>
|
| 152 |
+
{#if built.has('barracks')}<span class="tech-check">✓</span>{/if}
|
| 153 |
<img class="tech-sprite" src="/sprites/buildings/barracks.png" alt="" />
|
| 154 |
Barracks<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />150</span>
|
| 155 |
</div>
|
| 156 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/marine.png" alt="Marine" /> Marine</div>
|
| 157 |
</div>
|
| 158 |
<div class="tech-row tech-indent3">
|
| 159 |
+
<div class="tech-node" class:tech-built={built.has('engineering_bay')}>
|
| 160 |
+
{#if built.has('engineering_bay')}<span class="tech-check">✓</span>{/if}
|
| 161 |
<img class="tech-sprite" src="/sprites/buildings/engineering_bay.png" alt="" />
|
| 162 |
Eng. Bay<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />125</span>
|
| 163 |
</div>
|
| 164 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/medic.png" alt="Medic" /> Medic</div>
|
| 165 |
</div>
|
| 166 |
<div class="tech-row tech-indent3">
|
| 167 |
+
<div class="tech-node" class:tech-built={built.has('factory')}>
|
| 168 |
+
{#if built.has('factory')}<span class="tech-check">✓</span>{/if}
|
| 169 |
<img class="tech-sprite" src="/sprites/buildings/factory.png" alt="" />
|
| 170 |
Factory<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />200 <img class="cost-icon" src="/sprites/icons/gas.png" alt="gas" />100</span>
|
| 171 |
</div>
|
| 172 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/goliath.png" alt="Goliath" /> Goliath</div>
|
| 173 |
</div>
|
| 174 |
<div class="tech-row tech-indent4">
|
| 175 |
+
<div class="tech-node" class:tech-built={built.has('armory')}>
|
| 176 |
+
{#if built.has('armory')}<span class="tech-check">✓</span>{/if}
|
| 177 |
<img class="tech-sprite" src="/sprites/buildings/armory.png" alt="" />
|
| 178 |
Armory<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />100 <img class="cost-icon" src="/sprites/icons/gas.png" alt="gas" />50</span>
|
| 179 |
</div>
|
| 180 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/tank.png" alt="Tank" /> Tank</div>
|
| 181 |
</div>
|
| 182 |
<div class="tech-row tech-indent4">
|
| 183 |
+
<div class="tech-node" class:tech-built={built.has('starport')}>
|
| 184 |
+
{#if built.has('starport')}<span class="tech-check">✓</span>{/if}
|
| 185 |
<img class="tech-sprite" src="/sprites/buildings/starport.png" alt="" />
|
| 186 |
Starport<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />150 <img class="cost-icon" src="/sprites/icons/gas.png" alt="gas" />100</span>
|
| 187 |
</div>
|
| 188 |
<div class="tech-produces">→ <img class="tech-unit-sprite" src="/sprites/units/wraith.png" alt="Wraith" /> Wraith</div>
|
| 189 |
</div>
|
| 190 |
<div class="tech-row tech-indent">
|
| 191 |
+
<div class="tech-node" class:tech-built={built.has('refinery')}>
|
| 192 |
+
{#if built.has('refinery')}<span class="tech-check">✓</span>{/if}
|
| 193 |
<img class="tech-sprite" src="/sprites/buildings/refinery.png" alt="" />
|
| 194 |
Refinery<span class="tech-cost"><img class="cost-icon" src="/sprites/icons/mineral.png" alt="min" />100</span>
|
| 195 |
</div>
|
|
|
|
| 436 |
|
| 437 |
.tech-start { border-color: var(--accent); color: var(--accent); }
|
| 438 |
|
| 439 |
+
.tech-built {
|
| 440 |
+
border-color: rgba(80, 220, 130, 0.6) !important;
|
| 441 |
+
background: rgba(80, 220, 130, 0.08) !important;
|
| 442 |
+
color: #50dc82 !important;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.tech-check {
|
| 446 |
+
font-size: 0.75rem;
|
| 447 |
+
color: #50dc82;
|
| 448 |
+
font-weight: 700;
|
| 449 |
+
flex-shrink: 0;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
.tech-cost {
|
| 453 |
font-size: 0.7rem;
|
| 454 |
color: var(--text-muted);
|
frontend/src/lib/components/Map.svelte
CHANGED
|
@@ -792,6 +792,9 @@
|
|
| 792 |
{@const ry = rs?.renderY ?? u.y}
|
| 793 |
{@const angle = rs?.angle ?? 0}
|
| 794 |
{@const isSelected = u.id === $selectedUnit?.id}
|
|
|
|
|
|
|
|
|
|
| 795 |
|
| 796 |
<g transform="translate({rx},{ry})">
|
| 797 |
<!-- Selection ring -->
|
|
@@ -812,8 +815,8 @@
|
|
| 812 |
<!-- Unit sprite, rotated toward movement direction -->
|
| 813 |
<image
|
| 814 |
href="{spriteBase}/units/{UNIT_SPRITE[u.unit_type]}"
|
| 815 |
-
x=
|
| 816 |
-
width=
|
| 817 |
opacity={u.is_cloaked ? 0.35 : 1}
|
| 818 |
preserveAspectRatio="xMidYMid meet"
|
| 819 |
transform="rotate({angle})"
|
|
@@ -858,7 +861,7 @@
|
|
| 858 |
per-cell rects, works in all environments (no SVG mask url(#id) needed).
|
| 859 |
Computed reactively via $: fogRects so viewport panning is always in sync. -->
|
| 860 |
{#each fogRects as r}
|
| 861 |
-
<rect x={r.x} y={r.y} width={r.w} height=
|
| 862 |
{/each}
|
| 863 |
|
| 864 |
<!-- Tutorial target beacon (rendered above fog so it's always visible) -->
|
|
|
|
| 792 |
{@const ry = rs?.renderY ?? u.y}
|
| 793 |
{@const angle = rs?.angle ?? 0}
|
| 794 |
{@const isSelected = u.id === $selectedUnit?.id}
|
| 795 |
+
{@const isLargeUnit = u.unit_type === 'goliath' || u.unit_type === 'tank' || u.unit_type === 'wraith'}
|
| 796 |
+
{@const spriteHalf = isLargeUnit ? 0.76 : 0.38}
|
| 797 |
+
{@const spriteSize = isLargeUnit ? 1.52 : 0.76}
|
| 798 |
|
| 799 |
<g transform="translate({rx},{ry})">
|
| 800 |
<!-- Selection ring -->
|
|
|
|
| 815 |
<!-- Unit sprite, rotated toward movement direction -->
|
| 816 |
<image
|
| 817 |
href="{spriteBase}/units/{UNIT_SPRITE[u.unit_type]}"
|
| 818 |
+
x={-spriteHalf} y={-spriteHalf}
|
| 819 |
+
width={spriteSize} height={spriteSize}
|
| 820 |
opacity={u.is_cloaked ? 0.35 : 1}
|
| 821 |
preserveAspectRatio="xMidYMid meet"
|
| 822 |
transform="rotate({angle})"
|
|
|
|
| 861 |
per-cell rects, works in all environments (no SVG mask url(#id) needed).
|
| 862 |
Computed reactively via $: fogRects so viewport panning is always in sync. -->
|
| 863 |
{#each fogRects as r}
|
| 864 |
+
<rect x={r.x} y={r.y} width={r.w + 0.02} height={1.02} fill="rgba(0,0,0,{r.opacity})" pointer-events="none" shape-rendering="crispEdges" />
|
| 865 |
{/each}
|
| 866 |
|
| 867 |
<!-- Tutorial target beacon (rendered above fog so it's always visible) -->
|
frontend/src/lib/components/UnitPanel.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { selectedUnit, selectedBuilding, myPlayerId } from '$lib/stores/game';
|
| 3 |
import { UNIT_DESCRIPTIONS, BUILDING_LABELS } from '$lib/types';
|
|
|
|
| 4 |
|
| 5 |
$: unit = $selectedUnit;
|
| 6 |
$: building = $selectedBuilding;
|
|
@@ -42,6 +43,14 @@
|
|
| 42 |
function secFromTicks(t: number) {
|
| 43 |
return (t / 4).toFixed(0) + 's';
|
| 44 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
</script>
|
| 46 |
|
| 47 |
{#if isVisible}
|
|
@@ -107,6 +116,11 @@
|
|
| 107 |
<span class="stat-label">Building</span>
|
| 108 |
<span class="stat-val">{secFromTicks(building.construction_ticks_remaining)} remaining</span>
|
| 109 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
{/if}
|
| 111 |
|
| 112 |
{#if building.production_queue.length > 0}
|
|
@@ -277,4 +291,21 @@
|
|
| 277 |
.queue-icon { font-size: 0.65rem; color: var(--player); }
|
| 278 |
.queue-name { flex: 1; font-weight: 600; font-family: var(--font-mono); }
|
| 279 |
.queue-time { font-family: var(--font-mono); font-size: 0.75rem; color: var(--supply); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
</style>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { selectedUnit, selectedBuilding, myPlayerId } from '$lib/stores/game';
|
| 3 |
import { UNIT_DESCRIPTIONS, BUILDING_LABELS } from '$lib/types';
|
| 4 |
+
import { getSocket } from '$lib/socket';
|
| 5 |
|
| 6 |
$: unit = $selectedUnit;
|
| 7 |
$: building = $selectedBuilding;
|
|
|
|
| 43 |
function secFromTicks(t: number) {
|
| 44 |
return (t / 4).toFixed(0) + 's';
|
| 45 |
}
|
| 46 |
+
|
| 47 |
+
const socket = getSocket();
|
| 48 |
+
|
| 49 |
+
function cancelConstruction() {
|
| 50 |
+
if (!building) return;
|
| 51 |
+
socket.emit('cancel_construction', { building_id: building.id });
|
| 52 |
+
selectedBuilding.set(null);
|
| 53 |
+
}
|
| 54 |
</script>
|
| 55 |
|
| 56 |
{#if isVisible}
|
|
|
|
| 116 |
<span class="stat-label">Building</span>
|
| 117 |
<span class="stat-val">{secFromTicks(building.construction_ticks_remaining)} remaining</span>
|
| 118 |
</div>
|
| 119 |
+
{#if isOwn}
|
| 120 |
+
<button class="cancel-btn" on:click={cancelConstruction}>
|
| 121 |
+
✕ Annuler (remb. 75%)
|
| 122 |
+
</button>
|
| 123 |
+
{/if}
|
| 124 |
{/if}
|
| 125 |
|
| 126 |
{#if building.production_queue.length > 0}
|
|
|
|
| 291 |
.queue-icon { font-size: 0.65rem; color: var(--player); }
|
| 292 |
.queue-name { flex: 1; font-weight: 600; font-family: var(--font-mono); }
|
| 293 |
.queue-time { font-family: var(--font-mono); font-size: 0.75rem; color: var(--supply); }
|
| 294 |
+
|
| 295 |
+
.cancel-btn {
|
| 296 |
+
margin-top: 10px;
|
| 297 |
+
width: 100%;
|
| 298 |
+
padding: 8px 12px;
|
| 299 |
+
background: rgba(248, 81, 73, 0.12);
|
| 300 |
+
border: 1px solid rgba(248, 81, 73, 0.35);
|
| 301 |
+
border-radius: var(--radius);
|
| 302 |
+
color: var(--danger);
|
| 303 |
+
font-size: 0.8rem;
|
| 304 |
+
font-weight: 700;
|
| 305 |
+
cursor: pointer;
|
| 306 |
+
transition: background 0.15s;
|
| 307 |
+
}
|
| 308 |
+
.cancel-btn:hover {
|
| 309 |
+
background: rgba(248, 81, 73, 0.22);
|
| 310 |
+
}
|
| 311 |
</style>
|
frontend/src/routes/game/+page.svelte
CHANGED
|
@@ -147,6 +147,12 @@
|
|
| 147 |
$: myId = $myPlayerId;
|
| 148 |
$: isWinner = $winnerId === myId;
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
let helpOpen = false;
|
| 151 |
</script>
|
| 152 |
|
|
@@ -203,7 +209,7 @@
|
|
| 203 |
</div>
|
| 204 |
</div>
|
| 205 |
|
| 206 |
-
<HelpModal bind:open={helpOpen} />
|
| 207 |
|
| 208 |
<!-- Game over modal -->
|
| 209 |
{#if showGameOver}
|
|
|
|
| 147 |
$: myId = $myPlayerId;
|
| 148 |
$: isWinner = $winnerId === myId;
|
| 149 |
|
| 150 |
+
$: myBuiltBuildings = myId && $gameState
|
| 151 |
+
? Object.values($gameState.players[myId]?.buildings ?? {})
|
| 152 |
+
.filter((b: any) => b.status !== 'destroyed')
|
| 153 |
+
.map((b: any) => b.building_type as string)
|
| 154 |
+
: [];
|
| 155 |
+
|
| 156 |
let helpOpen = false;
|
| 157 |
</script>
|
| 158 |
|
|
|
|
| 209 |
</div>
|
| 210 |
</div>
|
| 211 |
|
| 212 |
+
<HelpModal bind:open={helpOpen} builtBuildings={myBuiltBuildings} />
|
| 213 |
|
| 214 |
<!-- Game over modal -->
|
| 215 |
{#if showGameOver}
|