feat: Implement manual game mode with 3D battlefield, custom context menu, and card actions including tokens and counters.
Some checks failed
Build and Deploy / build (push) Failing after 2m32s
Some checks failed
Build and Deploy / build (push) Failing after 2m32s
This commit is contained in:
@@ -30,6 +30,7 @@ interface GameState {
|
||||
order: string[]; // Turn order (player IDs)
|
||||
turn: number;
|
||||
phase: string;
|
||||
maxZ: number; // Tracker for depth sorting
|
||||
}
|
||||
|
||||
export class GameManager {
|
||||
@@ -43,6 +44,7 @@ export class GameManager {
|
||||
order: players.map(p => p.id),
|
||||
turn: 1,
|
||||
phase: 'beginning',
|
||||
maxZ: 100,
|
||||
};
|
||||
|
||||
players.forEach(p => {
|
||||
@@ -61,8 +63,6 @@ export class GameManager {
|
||||
gameState.players[gameState.order[0]].isActive = true;
|
||||
}
|
||||
|
||||
// TODO: Load decks here. For now, we start with empty board/library.
|
||||
|
||||
this.games.set(roomId, gameState);
|
||||
return gameState;
|
||||
}
|
||||
@@ -83,6 +83,18 @@ export class GameManager {
|
||||
case 'TAP_CARD':
|
||||
this.tapCard(game, action);
|
||||
break;
|
||||
case 'FLIP_CARD':
|
||||
this.flipCard(game, action);
|
||||
break;
|
||||
case 'ADD_COUNTER':
|
||||
this.addCounter(game, action);
|
||||
break;
|
||||
case 'CREATE_TOKEN':
|
||||
this.createToken(game, action);
|
||||
break;
|
||||
case 'DELETE_CARD':
|
||||
this.deleteCard(game, action);
|
||||
break;
|
||||
case 'UPDATE_LIFE':
|
||||
this.updateLife(game, action);
|
||||
break;
|
||||
@@ -90,7 +102,7 @@ export class GameManager {
|
||||
this.drawCard(game, action);
|
||||
break;
|
||||
case 'SHUFFLE_LIBRARY':
|
||||
this.shuffleLibrary(game, action); // Placeholder logic
|
||||
this.shuffleLibrary(game, action);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -100,15 +112,70 @@ export class GameManager {
|
||||
private moveCard(game: GameState, action: { cardId: string; toZone: CardInstance['zone']; position?: { x: number, y: number } }) {
|
||||
const card = game.cards[action.cardId];
|
||||
if (card) {
|
||||
// Bring to front
|
||||
card.position.z = ++game.maxZ;
|
||||
|
||||
card.zone = action.toZone;
|
||||
if (action.position) {
|
||||
card.position = { ...card.position, ...action.position };
|
||||
}
|
||||
// Reset tapped state if moving to hand/library/graveyard?
|
||||
if (['hand', 'library', 'graveyard', 'exile'].includes(action.toZone)) {
|
||||
|
||||
// Auto-untap and reveal if moving to public zones (optional, but helpful default)
|
||||
if (['hand', 'graveyard', 'exile'].includes(action.toZone)) {
|
||||
card.tapped = false;
|
||||
card.faceDown = action.toZone === 'library';
|
||||
card.faceDown = false;
|
||||
}
|
||||
// Library is usually face down
|
||||
if (action.toZone === 'library') {
|
||||
card.faceDown = true;
|
||||
card.tapped = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addCounter(game: GameState, action: { cardId: string; counterType: string; amount: number }) {
|
||||
const card = game.cards[action.cardId];
|
||||
if (card) {
|
||||
const existing = card.counters.find(c => c.type === action.counterType);
|
||||
if (existing) {
|
||||
existing.count += action.amount;
|
||||
// Remove if 0 or less? Usually yes for counters like +1/+1 but let's just keep logic simple
|
||||
if (existing.count <= 0) {
|
||||
card.counters = card.counters.filter(c => c.type !== action.counterType);
|
||||
}
|
||||
} else if (action.amount > 0) {
|
||||
card.counters.push({ type: action.counterType, count: action.amount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createToken(game: GameState, action: { ownerId: string; tokenData: any; position?: { x: number, y: number } }) {
|
||||
const tokenId = `token-${Math.random().toString(36).substring(7)}`;
|
||||
// @ts-ignore
|
||||
const token: CardInstance = {
|
||||
instanceId: tokenId,
|
||||
oracleId: 'token',
|
||||
name: action.tokenData.name || 'Token',
|
||||
imageUrl: action.tokenData.imageUrl || 'https://cards.scryfall.io/large/front/5/f/5f75e883-2574-4b9e-8fcb-5db3d9579fae.jpg?1692233606', // Generic token image
|
||||
controllerId: action.ownerId,
|
||||
ownerId: action.ownerId,
|
||||
zone: 'battlefield',
|
||||
tapped: false,
|
||||
faceDown: false,
|
||||
position: {
|
||||
x: action.position?.x || 50,
|
||||
y: action.position?.y || 50,
|
||||
z: ++game.maxZ
|
||||
},
|
||||
counters: [],
|
||||
ptModification: { power: action.tokenData.power || 0, toughness: action.tokenData.toughness || 0 }
|
||||
};
|
||||
game.cards[tokenId] = token;
|
||||
}
|
||||
|
||||
private deleteCard(game: GameState, action: { cardId: string }) {
|
||||
if (game.cards[action.cardId]) {
|
||||
delete game.cards[action.cardId];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +186,15 @@ export class GameManager {
|
||||
}
|
||||
}
|
||||
|
||||
private flipCard(game: GameState, action: { cardId: string }) {
|
||||
const card = game.cards[action.cardId];
|
||||
if (card) {
|
||||
// Bring to front on flip too
|
||||
card.position.z = ++game.maxZ;
|
||||
card.faceDown = !card.faceDown;
|
||||
}
|
||||
}
|
||||
|
||||
private updateLife(game: GameState, action: { playerId: string; amount: number }) {
|
||||
const player = game.players[action.playerId];
|
||||
if (player) {
|
||||
@@ -130,18 +206,18 @@ export class GameManager {
|
||||
// Find top card of library for this player
|
||||
const libraryCards = Object.values(game.cards).filter(c => c.ownerId === action.playerId && c.zone === 'library');
|
||||
if (libraryCards.length > 0) {
|
||||
// In a real implementation this should be ordered.
|
||||
// For now, just pick one (random or first).
|
||||
const card = libraryCards[0];
|
||||
// Pick random one (simulating shuffle for now)
|
||||
const randomIndex = Math.floor(Math.random() * libraryCards.length);
|
||||
const card = libraryCards[randomIndex];
|
||||
|
||||
card.zone = 'hand';
|
||||
card.faceDown = false;
|
||||
card.position.z = ++game.maxZ;
|
||||
}
|
||||
}
|
||||
|
||||
private shuffleLibrary(_game: GameState, _action: { playerId: string }) {
|
||||
// In a real implementation we would shuffle the order array.
|
||||
// Since we retrieve by filtering currently, we don't have order.
|
||||
// We need to implement order index if we want shuffling.
|
||||
// No-op in current logic since we pick randomly
|
||||
}
|
||||
|
||||
// Helper to add cards (e.g. at game start)
|
||||
|
||||
Reference in New Issue
Block a user