共享会话
🗺️ 島嶼點擊放大效果
分享于 2026年9月9日 17:10
QuanAI以下修改可讓島嶼選擇與棋子選擇互相獨立:
- 點擊島上的任意落子點或島嶼空白區域,都會選中該島。
- 無論是否已選中己方棋子,最後點擊的島都會放大至
1.5倍。 - 中間四島
1、2、3、5原地放大。 - 外圍三島向棋盤中心位移後放大:
- 上島向下。
- 左下島向右上。
- 右下島向左上。
- 島嶼、棋子、提示點、選擇環及點擊區域會同步變換。
1. 增加島嶼選中樣式
在 SVG 內部 <style> 中,放在 .island-copy 和 .hit-slot 相關樣式附近:
css
.island-copy.is-selected{
filter:url(#islandShadow) brightness(1.08);
}
.island-hit{
opacity:0;
pointer-events:all;
cursor:pointer;
}2. 增加縮放參數及選中狀態
在 ISLAND_CENTERS 後面加入:
js
const ISLAND_ZOOM=1.5;
/*
外圍三島的位移量。
位移值等於島嶼向外部分因 1.5 倍縮放增加的尺寸,
因而能保持原本與棋盤外框的邊距:
上島:向下
左下島:向右上
右下島:向左上
*/
const OUTER_ISLAND_SHIFT={
0:[0,24.75],
4:[24.032,-16.875],
6:[-24.032,-16.875]
};找到:
js
let selectedCell=-1;改成:
js
let selectedCell=-1;
let selectedIsland=-1;找到:
js
const islandUses=[];
const hitSlots=[];改成:
js
const islandUses=[];
const islandHitGroups=[];
const hitSlots=[];3. 增加島嶼變換函式
放在 cellCoordinates() 函式後面:
js
function getIslandView(island){
if(island!==selectedIsland){
return {
scale:1,
dx:0,
dy:0
};
}
const shift=OUTER_ISLAND_SHIFT[island]||[0,0];
return {
scale:ISLAND_ZOOM,
dx:shift[0],
dy:shift[1]
};
}
/*
先以島嶼中心為縮放中心,再套用外圍島的向內位移。
*/
function islandTransform(island){
const [cx,cy]=ISLAND_CENTERS[island];
const {scale,dx,dy}=getIslandView(island);
return [
`translate(${dx} ${dy})`,
`translate(${cx} ${cy})`,
`scale(${scale})`,
`translate(${-cx} ${-cy})`
].join(" ");
}
/*
取得經過島嶼縮放、位移後的畫面座標。
用於最後一步連線這類橫跨不同島嶼的圖形。
*/
function visualCellCoordinates(index){
const island=(index/7)|0;
const [cx,cy]=ISLAND_CENTERS[island];
const [x,y]=cellCoordinates(index);
const {scale,dx,dy}=getIslandView(island);
return [
cx+dx+(x-cx)*scale,
cy+dy+(y-cy)*scale
];
}4. 替換 buildBoard()
把原來的整個 buildBoard() 替換成:
js
function buildBoard(){
const islandLayer=$("#islandLayer");
const hitLayer=$("#hitLayer");
ISLAND_CENTERS.forEach(([x,y],island)=>{
/*
可見島嶼。
*/
const use=createSVGElement("use",{
href:"#island-symbol",
transform:`translate(${x} ${y})`,
class:"island-copy",
"data-island":island
});
use.style.color="#fff0c5";
islandLayer.appendChild(use);
islandUses.push(use);
/*
每座島擁有獨立的點擊群組。
群組和可見島嶼使用相同 transform,
因而放大後點擊位置仍然正確。
*/
const hitGroup=createSVGElement("g",{
"data-hit-island":island
});
const islandHit=createSVGElement("use",{
href:"#island-symbol",
transform:`translate(${x} ${y})`,
class:"island-hit",
"data-island":island,
"aria-hidden":"true"
});
hitGroup.appendChild(islandHit);
for(let point=0;point<7;point++){
const index=island*7+point;
const [cellX,cellY]=cellCoordinates(index);
const circle=createSVGElement("circle",{
cx:cellX,
cy:cellY,
r:11.5,
class:"hit-slot",
"data-cell":index,
"data-island":island,
"aria-hidden":"true"
});
hitGroup.appendChild(circle);
hitSlots[index]=circle;
}
hitLayer.appendChild(hitGroup);
islandHitGroups.push(hitGroup);
});
}新增的透明 .island-hit 會覆蓋整座島,所以不必精準點在七個落子點上也能選中島嶼。
5. 修改 renderBoard()
在 state.claims.forEach() 中加入島嶼變換。將原來的區段:
js
state.claims.forEach((owner,index)=>{
const use=islandUses[index];
use.style.color=
owner===1 ? "#ff9f82" :
owner===-1 ? "#d7f7ff" :
"#fff0c5";
use.dataset.owner=String(owner);
});替換成:
js
state.claims.forEach((owner,index)=>{
const use=islandUses[index];
const [x,y]=ISLAND_CENTERS[index];
use.style.color=
owner===1 ? "#ff9f82" :
owner===-1 ? "#d7f7ff" :
"#fff0c5";
use.dataset.owner=String(owner);
use.classList.toggle("is-selected",index===selectedIsland);
use.setAttribute(
"transform",
`${islandTransform(index)} translate(${x} ${y})`
);
});
/*
點擊群組必須和島嶼使用相同的縮放及位移。
*/
islandHitGroups.forEach((group,index)=>{
group.setAttribute("transform",islandTransform(index));
});
/*
將選中島及其點擊區移到所在圖層的最後,
避免被其他島的點擊區域遮蓋。
*/
if(selectedIsland>=0){
$("#islandLayer").appendChild(islandUses[selectedIsland]);
$("#hitLayer").appendChild(islandHitGroups[selectedIsland]);
}然後刪除 renderBoard() 最後原本的整段游標判斷:
js
hitSlots.forEach((slot,index)=>{
if(
gameStarted &&
!thinking &&
!state.winner &&
controllers[state.turn]==="human"
){
slot.style.cursor=
state.board[index]===state.turn || state.board[index]===0
? "pointer"
: "not-allowed";
}else{
slot.style.cursor="default";
}
});改成:
js
hitSlots.forEach(slot=>{
slot.style.cursor="pointer";
});即使該位置不能落子,點擊它仍然可以選中所屬島嶼。
6. 讓棋子、提示和最後一步同步變換
修改 renderPieces()
找到:
js
const [x,y]=cellCoordinates(index);
const use=createSVGElement("use",{
href:player===1 ? "#fire-piece" : "#ice-piece",
transform:`translate(${x} ${y})`,
class:"piece-use"
});替換成:
js
const island=(index/7)|0;
const [x,y]=cellCoordinates(index);
const use=createSVGElement("use",{
href:player===1 ? "#fire-piece" : "#ice-piece",
transform:
`${islandTransform(island)} translate(${x} ${y})`,
class:"piece-use"
});修改 renderLastMove()
找到:
js
const [x1,y1]=cellCoordinates(move.from);
const [x2,y2]=cellCoordinates(move.to);替換成:
js
const [x1,y1]=visualCellCoordinates(move.from);
const [x2,y2]=visualCellCoordinates(move.to);修改 renderTargets()
建立選中環的程式:
js
const [sx,sy]=cellCoordinates(selectedCell);
const ring=createSVGElement("circle",{
cx:sx,
cy:sy,
r:12.5,
class:"selection-ring",
stroke:state.turn===1 ? "#ffdc6b" : "#75dcff"
});替換成:
js
const selectedCellIsland=(selectedCell/7)|0;
const [sx,sy]=cellCoordinates(selectedCell);
const ring=createSVGElement("circle",{
cx:sx,
cy:sy,
r:12.5,
class:"selection-ring",
stroke:state.turn===1 ? "#ffdc6b" : "#75dcff",
transform:islandTransform(selectedCellIsland)
});同一函式中,找到建立目的地提示點的程式:
js
for(const move of getMovesFrom(state,selectedCell)){
const [x,y]=cellCoordinates(move.to);
const marker=createSVGElement("circle",{
cx:x,
cy:y,
r:4.2,
class:"target-dot"
});
targetLayer.appendChild(marker);
}替換成:
js
for(const move of getMovesFrom(state,selectedCell)){
const destinationIsland=(move.to/7)|0;
const [x,y]=cellCoordinates(move.to);
const marker=createSVGElement("circle",{
cx:x,
cy:y,
r:4.2,
class:"target-dot",
transform:islandTransform(destinationIsland)
});
targetLayer.appendChild(marker);
}7. 修改點擊邏輯
將整個 onCellClick() 替換成:
js
function onIslandClick(island){
if(!Number.isInteger(island)||island<0||island>=7) return;
selectedIsland=island;
renderBoard();
renderStatus();
}
function onCellClick(index){
/*
島嶼選擇優先於棋子操作。
因此即使:
1. 尚未選中己方棋子;
2. 點到敵方棋子;
3. 點到不能移動的位置;
4. AI 正在思考;
這座島仍會被選中。
*/
selectedIsland=(index/7)|0;
if(
!gameStarted ||
thinking ||
state.winner ||
controllers[state.turn]!=="human"
){
renderBoard();
renderStatus();
return;
}
const occupant=state.board[index];
if(occupant===state.turn){
lastAIStats=null;
selectedCell=selectedCell===index ? -1 : index;
renderBoard();
renderStatus();
return;
}
if(
occupant===0 &&
selectedCell>=0 &&
isLegalMove(state,{from:selectedCell,to:index})
){
/*
selectedIsland 已經是目的地所屬島嶼,
commitMove 後仍會保持放大。
*/
commitMove(
{from:selectedCell,to:index},
"human",
null
);
return;
}
/*
即使不是合法移動,島嶼選擇仍然生效。
*/
renderBoard();
renderStatus();
}再將 bindEvents() 中原本的:
js
$("#hitLayer").addEventListener("click",event=>{
const target=event.target.closest(".hit-slot");
if(target) onCellClick(Number(target.dataset.cell));
});替換成:
js
$("#hitLayer").addEventListener("click",event=>{
const target=event.target.closest?.(".hit-slot,.island-hit");
if(!target) return;
if(target.classList.contains("hit-slot")){
onCellClick(Number(target.dataset.cell));
}else{
onIslandClick(Number(target.dataset.island));
}
});8. 重置時清除島嶼選擇
在 resetGame() 中,找到:
js
selectedCell=-1;在其後加入:
js
selectedIsland=-1;在 restoreHistory() 中也同樣改成:
js
selectedCell=-1;
selectedIsland=-1;最後將 Escape 鍵相關區段:
js
}else if(selectedCell>=0){
selectedCell=-1;
renderBoard();
renderStatus();
}替換成:
js
}else if(selectedCell>=0||selectedIsland>=0){
selectedCell=-1;
selectedIsland=-1;
renderBoard();
renderStatus();
}這樣按下 Escape 會同時取消棋子及島嶼選擇。