gabraken commited on
Commit
d530c6f
·
1 Parent(s): 98d62d6

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 = half the visual half-width (box is 1/2 visual size)."""
38
- return max(0.35, self.width / 4)
39
 
40
  def col_hh(self) -> float:
41
- """Half collision height = half the visual half-height (box is 1/2 visual size)."""
42
- return max(0.35, self.height / 4)
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="-0.38" y="-0.38"
816
- width="0.76" height="0.76"
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="1" fill="rgba(0,0,0,{r.opacity})" pointer-events="none" />
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}