Arrows for planning
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
--black: #1d3557;
|
||||
--white: #ffffff;
|
||||
--green: #10b981;
|
||||
--arrow-primary: #f59e0b;
|
||||
--arrow-secondary: #10b981;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -132,6 +134,8 @@
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.edge {
|
||||
@@ -175,7 +179,29 @@
|
||||
stroke-dasharray: 4;
|
||||
}
|
||||
|
||||
/* Victory Banner */
|
||||
.arrow {
|
||||
pointer-events: none;
|
||||
stroke-width: 4;
|
||||
stroke-linecap: round;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.arrow-pri {
|
||||
stroke: var(--arrow-primary);
|
||||
}
|
||||
|
||||
.arrow-sec {
|
||||
stroke: var(--arrow-secondary);
|
||||
}
|
||||
|
||||
#head-pri {
|
||||
fill: var(--arrow-primary);
|
||||
}
|
||||
|
||||
#head-sec {
|
||||
fill: var(--arrow-secondary);
|
||||
}
|
||||
|
||||
#victory-banner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -329,7 +355,7 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body oncontextmenu="return false;">
|
||||
|
||||
<div id="lobby" class="main-card">
|
||||
<h1 style="text-align: center; margin: 0; border-bottom: 4px solid var(--black); padding-bottom: 10px;">WOLF &
|
||||
@@ -364,7 +390,16 @@
|
||||
<p style="margin: 5px 0 0 0; font-size: 0.8rem;">SPIEL BEENDET</p>
|
||||
</div>
|
||||
|
||||
<svg viewBox="0 0 600 420"></svg>
|
||||
<svg viewBox="0 0 600 420">
|
||||
<defs>
|
||||
<marker id="arrowhead-pri" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto">
|
||||
<polygon points="0 0, 6 2, 0 4" id="head-pri" />
|
||||
</marker>
|
||||
<marker id="arrowhead-sec" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto">
|
||||
<polygon points="0 0, 6 2, 0 4" id="head-sec" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
@@ -384,11 +419,15 @@
|
||||
let roomId = params.get("room");
|
||||
let isLocal = params.get("mode") === "local";
|
||||
|
||||
// --- STATE & HISTORY ---
|
||||
let ws, myRole, selected = null;
|
||||
let state = null;
|
||||
let history = [];
|
||||
let historyIndex = -1;
|
||||
let plannedArrows = [];
|
||||
|
||||
let dragStartNode = null;
|
||||
let dragStartTime = 0;
|
||||
let rightClickStart = null;
|
||||
|
||||
const pos = { 0: [80, 210], 1: [180, 110], 2: [180, 210], 3: [180, 310], 4: [280, 110], 5: [280, 210], 6: [280, 310], 7: [380, 110], 8: [380, 210], 9: [380, 310], 10: [480, 210] };
|
||||
const edges = [[0, 1], [0, 2], [0, 3], [1, 2], [2, 3], [1, 4], [2, 5], [3, 6], [4, 5], [5, 6], [4, 7], [5, 8], [6, 9], [7, 8], [8, 9], [5, 1], [5, 3], [5, 7], [5, 9], [7, 10], [8, 10], [9, 10]];
|
||||
@@ -410,6 +449,7 @@
|
||||
const isAtEnd = historyIndex === history.length - 1;
|
||||
if (newState.moveCount === 0) {
|
||||
history = [JSON.parse(JSON.stringify(newState))];
|
||||
plannedArrows = [];
|
||||
} else if (!state || newState.moveCount > state.moveCount) {
|
||||
history.push(JSON.parse(JSON.stringify(newState)));
|
||||
}
|
||||
@@ -466,35 +506,39 @@
|
||||
nextState.moveCount++;
|
||||
nextState.winner = checkWinConditions(nextState);
|
||||
history = history.slice(0, historyIndex + 1);
|
||||
plannedArrows = [];
|
||||
updateHistory(nextState, true);
|
||||
}
|
||||
|
||||
function checkWinConditions(g) {
|
||||
// 1. Wolf reaches sheep starting line (Row 0)
|
||||
if (g.wolf === 0) return "wolf";
|
||||
|
||||
// 2. Max moves reached
|
||||
if (g.moveCount >= 40) return "wolf";
|
||||
|
||||
// 3. Wolf is trapped
|
||||
const canWolfMove = bWolf[g.wolf].some(to => !g.sheep.includes(to));
|
||||
if (!canWolfMove) return "sheep";
|
||||
|
||||
// 4. Sheep are trapped (Wolf wins)
|
||||
const canAnySheepMove = g.sheep.some(sPos => {
|
||||
return bSheep[sPos].some(to => to !== g.wolf && !g.sheep.includes(to));
|
||||
});
|
||||
if (!canAnySheepMove) return "wolf";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function toggleArrow(from, to, type) {
|
||||
if (from === to || !bWolf[from].includes(to)) return;
|
||||
const idx = plannedArrows.findIndex(a => a.from === from && a.to === to && a.type === type);
|
||||
if (idx > -1) plannedArrows.splice(idx, 1);
|
||||
else plannedArrows.push({ from, to, type });
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
const viewState = history[historyIndex];
|
||||
if (!viewState) return;
|
||||
const isViewingHistory = historyIndex < history.length - 1;
|
||||
const svg = document.querySelector("svg");
|
||||
|
||||
const defs = svg.querySelector("defs");
|
||||
svg.innerHTML = "";
|
||||
svg.appendChild(defs);
|
||||
|
||||
edges.forEach(([a, b]) => {
|
||||
const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
||||
@@ -537,17 +581,42 @@
|
||||
const isPossible = canInteract && selected !== null && (viewState.turn === "wolf" ? bWolf[selected] : bSheep[selected]).includes(i) && !isWolf && !isSheep;
|
||||
|
||||
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
g.onclick = () => {
|
||||
|
||||
g.onmousedown = (e) => {
|
||||
if (e.button === 0) {
|
||||
dragStartNode = i;
|
||||
dragStartTime = Date.now();
|
||||
}
|
||||
if (e.button === 2) rightClickStart = i;
|
||||
};
|
||||
|
||||
g.onmouseup = (e) => {
|
||||
if (e.button === 0 && dragStartNode !== null) {
|
||||
const duration = Date.now() - dragStartTime;
|
||||
if (dragStartNode === i && duration < 250) {
|
||||
if (viewState.winner || (!isLocal && isViewingHistory)) return;
|
||||
if (!isLocal && state.turn !== myRole) return;
|
||||
const isMine = (viewState.turn === "wolf" && isWolf) || (viewState.turn === "sheep" && isSheep);
|
||||
if (isMine) selected = i;
|
||||
else if (isPossible) {
|
||||
plannedArrows = [];
|
||||
if (isLocal) handleLocalMove(selected, i);
|
||||
else ws.send(JSON.stringify({ from: selected, to: i }));
|
||||
selected = null;
|
||||
} else selected = null;
|
||||
render();
|
||||
} else if (dragStartNode !== i) {
|
||||
toggleArrow(dragStartNode, i, 'sec');
|
||||
}
|
||||
dragStartNode = null;
|
||||
}
|
||||
|
||||
if (e.button === 2 && rightClickStart !== null) {
|
||||
if (rightClickStart !== i) toggleArrow(rightClickStart, i, 'pri');
|
||||
else plannedArrows = [];
|
||||
rightClickStart = null;
|
||||
render();
|
||||
}
|
||||
};
|
||||
|
||||
const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||
@@ -563,6 +632,20 @@
|
||||
t.textContent = i; g.appendChild(t);
|
||||
svg.appendChild(g);
|
||||
}
|
||||
|
||||
plannedArrows.forEach(arrow => {
|
||||
const s = pos[arrow.from]; const e = pos[arrow.to];
|
||||
const dx = e[0] - s[0]; const dy = e[1] - s[1];
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const x1 = s[0] + Math.cos(angle) * 28; const y1 = s[1] + Math.sin(angle) * 28;
|
||||
const x2 = e[0] - Math.cos(angle) * 32; const y2 = e[1] - Math.sin(angle) * 32;
|
||||
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
||||
line.setAttribute("x1", x1); line.setAttribute("y1", y1);
|
||||
line.setAttribute("x2", x2); line.setAttribute("y2", y2);
|
||||
line.setAttribute("class", `arrow ${arrow.type === 'pri' ? 'arrow-pri' : 'arrow-sec'}`);
|
||||
line.setAttribute("marker-end", `url(#arrowhead-${arrow.type})`);
|
||||
svg.appendChild(line);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRoomList(rooms) {
|
||||
@@ -582,6 +665,7 @@
|
||||
function startLocal() { window.location.search = `?mode=local`; }
|
||||
|
||||
document.getElementById("restartBtn").onclick = () => {
|
||||
plannedArrows = [];
|
||||
if (isLocal) initLocalState();
|
||||
else ws.send(JSON.stringify({ type: 'restart' }));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user