Compare commits
66 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3558f4a9c9 | |||
| 4d48ad2414 | |||
| e74f767cdb | |||
| 860c3d31a7 | |||
| 7a964d18d9 | |||
| 0cbbd15a60 | |||
| 47f46d230b | |||
| 8125d13e57 | |||
| 3c72b5e6d0 | |||
| 6035e8bb6c | |||
| 9c5b0e011b | |||
| c247fad1c0 | |||
| 4400819618 | |||
| 6c1b4c57ba | |||
| e593359803 | |||
| 978aca6c50 | |||
| 27f99cd5ac | |||
| 78098b1a69 | |||
| 6e5272e37d | |||
| 21688aa11b | |||
| 7df56f5855 | |||
| ef6cee7883 | |||
| 5d04deae9f | |||
| 3d86f9c42e | |||
| 63ab8605d0 | |||
| 4c1c309877 | |||
| 92c349e7f2 | |||
| c09ddcb1a2 | |||
| 8e23e34a66 | |||
| 3060273c1d | |||
| 11aa2c458f | |||
| 1cce2fe244 | |||
| 401d915a0b | |||
| a3311c79a7 | |||
| be9a84d8bd | |||
| dfcd0dcc63 | |||
| d778f08a95 | |||
| 0f74382fde | |||
| 8040d708e7 | |||
| b74b472573 | |||
| 104e607cc7 | |||
| b4db4afd5f | |||
| e1178d5f5c | |||
| 025a690b48 | |||
| c3b166112b | |||
| 3d5aa8ade7 | |||
| 425819641d | |||
| 62344f91ee | |||
| 94e920a80f | |||
| 238c2690c3 | |||
| 6797935834 | |||
| d22aa7c962 | |||
| ad2d67e91a | |||
| 498ddfbd12 | |||
| b0888668bf | |||
| e055b75a49 | |||
| a7b359f8be | |||
| dbb0b693b9 | |||
| 4aa9085c9c | |||
| 707d44862a | |||
| e90591c306 | |||
| 29bb6d7169 | |||
| a3789e6d74 | |||
| 1e42cf8ba2 | |||
| 221850654d | |||
| b3d58f325f |
181 changed files with 317326 additions and 1202 deletions
19
.gitignore
vendored
19
.gitignore
vendored
|
|
@ -1,5 +1,20 @@
|
|||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
stockfish
|
||||
Assets/ChessEngines
|
||||
# Node modules
|
||||
**/node_modules/
|
||||
# Assets/*
|
||||
# Chess engine binaries and their subdirectories
|
||||
Assets/ChessEngines/stockfish/*
|
||||
Assets/ChessEngines/Fairy-Stockfish/*
|
||||
Assets/ChessEngines/quack/*
|
||||
# Assets/ChessEngines/stockfish/**
|
||||
# Assets/ChessEngines/Fairy-Stockfish/**
|
||||
|
||||
|
||||
# # Keep only necessary files in fairy-chess-server
|
||||
Assets/ChessEngines/fairy-chess-server/*
|
||||
!Assets/ChessEngines/fairy-chess-server/*.js
|
||||
!Assets/ChessEngines/fairy-chess-server/*.json
|
||||
|
||||
build/*
|
||||
48
Assets/ChessEngines/fairy-chess-server/engine-path.js
Normal file
48
Assets/ChessEngines/fairy-chess-server/engine-path.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Assets/ChessEngines/fairy-chess-server/engine-path.js
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
// Get current file's directory in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
function getEnginePath() {
|
||||
console.log("getEnginePath")
|
||||
// First check if path is provided through environment variable
|
||||
if (process.env.FAIRY_STOCKFISH_PATH) {
|
||||
return process.env.FAIRY_STOCKFISH_PATH;
|
||||
}
|
||||
|
||||
// Get the current directory (fairy-chess-server)
|
||||
const currentDir = __dirname;
|
||||
console.log(currentDir )
|
||||
|
||||
// Navigate up to Assets/ChessEngines
|
||||
const engineBaseDir = path.join(currentDir, '..', '..');
|
||||
console.log(engineBaseDir )
|
||||
|
||||
// Determine OS and set appropriate path
|
||||
if (os.platform() === 'win32') {
|
||||
// Windows path
|
||||
console.log(engineBaseDir + '/ChessEngines' + '/stockfish' + '/stockfish.exe')
|
||||
return engineBaseDir + '/ChessEngines' + '/stockfish' + '/stockfish.exe'
|
||||
} else {
|
||||
//res://Assets/ChessEngines/Fairy-Stockfish/src/stockfish
|
||||
// Unix-like systems (Linux, macOS)
|
||||
console.log(engineBaseDir + '/ChessEngines'+ '/Fairy-Stockfish' + '/src' + '/stockfish')
|
||||
return engineBaseDir + '/ChessEngines'+ '/Fairy-Stockfish' + '/src' + '/stockfish';
|
||||
}
|
||||
}
|
||||
|
||||
// Verify engine path exists
|
||||
async function verifyEnginePath(enginePath) {
|
||||
const fs = await import('fs');
|
||||
if (!fs.existsSync(enginePath)) {
|
||||
throw new Error(`Chess engine not found at path: ${enginePath}`);
|
||||
}
|
||||
return enginePath;
|
||||
}
|
||||
|
||||
export { getEnginePath, verifyEnginePath };
|
||||
203
Assets/ChessEngines/fairy-chess-server/engine.js
Normal file
203
Assets/ChessEngines/fairy-chess-server/engine.js
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
// engine.js
|
||||
import { spawn } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
class ChessEngine extends EventEmitter {
|
||||
constructor(enginePath) {
|
||||
super();
|
||||
this.enginePath = enginePath;
|
||||
this.engine = null;
|
||||
this.isReady = false;
|
||||
this.currentFen = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log()
|
||||
this.engine = spawn(this.enginePath);
|
||||
|
||||
this.engine.stdout.on('data', (data) => {
|
||||
const messages = data.toString().trim().split('\n');
|
||||
messages.forEach(msg => this.handleEngineMessage(msg));
|
||||
});
|
||||
|
||||
this.engine.stderr.on('data', (data) => {
|
||||
console.error('Engine Error:', data.toString());
|
||||
});
|
||||
|
||||
this.engine.on('close', (code) => {
|
||||
console.log('Engine process closed with code:', code);
|
||||
this.isReady = false;
|
||||
// console.log('Attempting to restart engine...');
|
||||
// // Add a small delay before restarting to avoid rapid restart loops
|
||||
// setTimeout(() => {
|
||||
// this.start()
|
||||
// .then(() => {
|
||||
// // Restore previous state if needed
|
||||
// if (this.currentFen) {
|
||||
// this.setPosition(this.currentFen);
|
||||
// }
|
||||
// this.emit('restarted', { code });
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.error('Failed to restart engine:', err);
|
||||
// this.emit('restart-failed', { error: err });
|
||||
// });
|
||||
// }, 1000);
|
||||
});
|
||||
|
||||
// Initialize engine
|
||||
this.sendCommand('uci');
|
||||
this.waitforReady(resolve);
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
waitforReady(resolve){
|
||||
this.sendCommand('isready');
|
||||
|
||||
// Wait for readyok
|
||||
this.once('ready', () => {
|
||||
this.isReady = true;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
handleEngineMessage(message) {
|
||||
console.log('Engine:', message);
|
||||
|
||||
if (message.includes('readyok')) {
|
||||
this.emit('ready');
|
||||
}
|
||||
else if (message.startsWith('bestmove')) {
|
||||
const [, move] = message.split(' ');
|
||||
this.emit('bestmove', move);
|
||||
}
|
||||
else if (message.startsWith('info')) {
|
||||
this.emit('info', this.parseInfo(message));
|
||||
}
|
||||
}
|
||||
|
||||
parseInfo(info) {
|
||||
const parts = info.split(' ');
|
||||
const result = {};
|
||||
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
switch (parts[i]) {
|
||||
case 'depth':
|
||||
result.depth = parseInt(parts[++i]);
|
||||
break;
|
||||
case 'seldepth':
|
||||
result.seldepth = parseInt(parts[++i]);
|
||||
break;
|
||||
case 'score':
|
||||
result.score = {
|
||||
type: parts[++i],
|
||||
value: parseInt(parts[++i])
|
||||
};
|
||||
break;
|
||||
case 'nodes':
|
||||
result.nodes = parseInt(parts[++i]);
|
||||
break;
|
||||
case 'time':
|
||||
result.time = parseInt(parts[++i]);
|
||||
break;
|
||||
case 'pv':
|
||||
result.pv = [];
|
||||
while (++i < parts.length && !parts[i].includes('bmc')) {
|
||||
result.pv.push(parts[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
sendCommand(cmd) {
|
||||
if (this.engine && this.engine.stdin.writable) {
|
||||
console.log('Sending:', cmd);
|
||||
this.engine.stdin.write(cmd + '\n');
|
||||
}
|
||||
}
|
||||
async startPos() {
|
||||
if (!this.isReady) throw new Error('Engine not ready');
|
||||
|
||||
this.sendCommand(`position startpos`);
|
||||
|
||||
// Ensure engine is ready after position set
|
||||
this.sendCommand('isready');
|
||||
await new Promise(resolve => this.once('ready', resolve));
|
||||
}
|
||||
async setBoardPosition(fen) {
|
||||
if (!this.isReady) throw new Error('Engine not ready');
|
||||
|
||||
this.currentFen = fen;
|
||||
this.sendCommand(`position fen ${fen}`);
|
||||
|
||||
// Ensure engine is ready after position set
|
||||
this.sendCommand('isready');
|
||||
await new Promise(resolve => this.once('ready', resolve));
|
||||
}
|
||||
|
||||
async setNewGame() {
|
||||
// if (!this.isReady) throw new Error('Engine not ready');
|
||||
|
||||
this.sendCommand(`ucinewgame`);
|
||||
|
||||
// Ensure engine is ready after position set
|
||||
this.sendCommand('isready');
|
||||
await new Promise(resolve => this.once('ready', resolve));
|
||||
}
|
||||
|
||||
async getBestMove(options = {}) {
|
||||
if (!this.isReady) throw new Error('Engine not ready');
|
||||
if (!this.currentFen) throw new Error('Position not set');
|
||||
|
||||
const {
|
||||
depth = 15,
|
||||
movetime = 1000,
|
||||
nodes = null,
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Construct go command
|
||||
let goCmd = 'go';
|
||||
if (depth) goCmd += ` depth ${depth}`;
|
||||
if (movetime) goCmd += ` movetime ${movetime}`;
|
||||
if (nodes) goCmd += ` nodes ${nodes}`;
|
||||
|
||||
this.sendCommand(goCmd);
|
||||
|
||||
// Wait for bestmove response
|
||||
this.once('bestmove', (move) => {
|
||||
resolve(move);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getAnalysis(fen, options = {}) {
|
||||
await this.setBoardPosition(fen);
|
||||
const bestMove = await this.getBestMove(options);
|
||||
return { bestMove };
|
||||
}
|
||||
|
||||
async createNewGame(){
|
||||
await this.setNewGame();
|
||||
return true
|
||||
}
|
||||
|
||||
quit() {
|
||||
if (this.engine) {
|
||||
this.sendCommand('quit');
|
||||
this.engine = null;
|
||||
this.isReady = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChessEngine;
|
||||
408
Assets/ChessEngines/fairy-chess-server/index.js
Normal file
408
Assets/ChessEngines/fairy-chess-server/index.js
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
import express from 'express';
|
||||
import ffish from 'ffish';
|
||||
import ChessEngine from './engine.js';
|
||||
import { getEnginePath, verifyEnginePath } from './engine-path.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import os from 'os';
|
||||
|
||||
function getLogPath() {
|
||||
if (process.platform === 'linux') {
|
||||
// Use ~/.local/share/ChessBuilder/logs
|
||||
return path.join(os.homedir(), '.local', 'share', 'ChessBuilder', 'logs');
|
||||
} else if (process.platform === 'win32') {
|
||||
// Use %APPDATA%\ChessBuilder\logs
|
||||
return path.join(os.homedir(), 'AppData', 'Roaming', 'ChessBuilder', 'logs');
|
||||
} else {
|
||||
// macOS: ~/Library/Logs/ChessBuilder
|
||||
return path.join(os.homedir(), 'Library', 'ChessBuilder', 'logs');
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure log directory exists
|
||||
const logDir = getLogPath();
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
|
||||
const logFile = path.join(logDir, 'chess-server.log');
|
||||
console.log(`Logging to: ${logFile}`);
|
||||
|
||||
// Create a write stream for logging
|
||||
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
||||
|
||||
// Redirect console.log and console.error to both console and file
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
console.log = (...args) => {
|
||||
const message = args.map(arg =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg) : arg
|
||||
).join(' ') + '\n';
|
||||
|
||||
logStream.write(`[${new Date().toISOString()}] ${message}`);
|
||||
originalConsoleLog.apply(console, args);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
const message = args.map(arg =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg) : arg
|
||||
).join(' ') + '\n';
|
||||
|
||||
logStream.write(`[${new Date().toISOString()}] ERROR: ${message}`);
|
||||
originalConsoleError.apply(console, args);
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
fs.writeFileSync(logFile, `Server started at ${new Date().toISOString()}\n`, { flag: 'a' });
|
||||
fs.writeFileSync(logFile, `Process ID: ${process.pid}\n`, { flag: 'a' });
|
||||
fs.writeFileSync(logFile, `Working directory: ${process.cwd()}\n`, { flag: 'a' });
|
||||
fs.writeFileSync(logFile, `Node version: ${process.version}\n`, { flag: 'a' });
|
||||
} catch (error) {
|
||||
console.error('Failed to write startup log:', error);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const port = 27531;
|
||||
|
||||
let engine = null;
|
||||
let isReady = false;
|
||||
let lastResponse = null
|
||||
const SERVER_WAIT_THRESHOLD = 1 * 60 * 1000;
|
||||
const CHECK_INTERVAL = 5000;
|
||||
|
||||
// Initialize ffish and engine
|
||||
ffish.onRuntimeInitialized = async () => {
|
||||
try {
|
||||
isReady = true;
|
||||
console.log('Fairy-Chess library ready asd');
|
||||
|
||||
// Get and verify engine path
|
||||
const enginePath = getEnginePath();
|
||||
console.log('ENGINE PATH', enginePath);
|
||||
|
||||
// Initialize Stockfish
|
||||
fs.chmodSync(enginePath, '755');
|
||||
engine = new ChessEngine(enginePath);
|
||||
await engine.start();
|
||||
console.log('Chess engine initialized at:', enginePath, engine.isReady);
|
||||
|
||||
// Set initial engine options
|
||||
if (engine.isReady) {
|
||||
engine.sendCommand('setoption name Threads value 4');
|
||||
engine.sendCommand('setoption name Hash value 128');
|
||||
engine.sendCommand('setoption name MultiPV value 1');
|
||||
engine.sendCommand('setoption name UCI_LimitStrength value true');
|
||||
engine.sendCommand('uci');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Initialization error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
|
||||
console.log("@@@@@@@@@@@@@@@@@HEALTH CHECK@@@@@@@@@@@@@@@@@@@@@@")
|
||||
console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
|
||||
console.log(JSON.stringify(ffish.variants()))
|
||||
lastResponse = new Date().getTime()
|
||||
res.json({
|
||||
status: 'ok',
|
||||
engineReady: isReady && engine && engine.isReady,
|
||||
enginePath: getEnginePath(),
|
||||
platform: process.platform,
|
||||
variants: ffish.variants()
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Set Options
|
||||
app.post('/setoptions', (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
const { name, value, options } = req.body;
|
||||
if(name && value !== null){
|
||||
engine.sendCommand(`setoption name ${name} value ${value}`);
|
||||
res.json({
|
||||
status: 'ok',
|
||||
success: name,
|
||||
errs: null,
|
||||
});
|
||||
}else if(options && Array.isArray(options)){
|
||||
let success = [];
|
||||
let errs = [];
|
||||
for(let x = 0; x < options.length; x++){
|
||||
let option = options[x];
|
||||
const {name, value} = option;
|
||||
if(name && value !== null ){
|
||||
engine.sendCommand(`setoption name ${name} value ${value}`);
|
||||
success.push(name)
|
||||
}else {
|
||||
errs.push(name)
|
||||
}
|
||||
}
|
||||
res.json({
|
||||
status: 'ok',
|
||||
success: success,
|
||||
errs: errs,
|
||||
});
|
||||
}else {
|
||||
return res.status(400).json({ error: 'No Params Provided' });
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Validate FEN endpoint
|
||||
app.post('/validate', (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
const { fen, variant = 'chess' } = req.body;
|
||||
|
||||
if (!fen) {
|
||||
return res.status(400).json({ error: 'FEN string required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const tempBoard = new ffish.Board(variant);
|
||||
const isValid = tempBoard.setFen(fen);
|
||||
tempBoard.delete();
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
isValid,
|
||||
startingFen: ffish.startingFen(variant)
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: 'Invalid FEN or variant' });
|
||||
}
|
||||
});
|
||||
|
||||
// New game endpoint
|
||||
app.post('/new', async (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
const { variant = 'chess' } = req.body;
|
||||
|
||||
try {
|
||||
|
||||
const engineAnalysis = await engine.createNewGame();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Set position endpoint
|
||||
app.post('/position', async(req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
const { fen, variant = 'chess', start } = req.body;
|
||||
|
||||
|
||||
try {
|
||||
//we have a lot of funky rules lets not validate
|
||||
// const isValid = ffish.validateFen(fen) == 1
|
||||
|
||||
if(start){
|
||||
await engine.startPos()
|
||||
engine.sendCommand('d');
|
||||
}else if(fen){
|
||||
await engine.setBoardPosition(fen)
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
msg: "Set Position",
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message, poserr: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Make move endpoint
|
||||
app.post('/move', (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
const { move, notation = 'uci', variant = 'chess' } = req.body;
|
||||
|
||||
if (!move) {
|
||||
return res.status(400).json({ error: 'Move required' });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const response = {
|
||||
status: 'ok',
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// State endpoint
|
||||
app.get('/state', (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
try {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
createNewGame
|
||||
*/
|
||||
app.post('/newgame', async (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const response = {
|
||||
status: 'ok',
|
||||
};
|
||||
|
||||
// If engine is available, get engine analysis
|
||||
if (engine && engine.isReady) {
|
||||
const engineAnalysis = await engine.createNewGame();
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Analysis endpoint
|
||||
app.post('/analyze', async (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
|
||||
try {
|
||||
const { depth = 15, movetime = 1000 } = req.body;
|
||||
|
||||
// Get basic position analysis
|
||||
const positionAnalysis = {
|
||||
status: 'ok',
|
||||
};
|
||||
|
||||
// If engine is available, get engine analysis
|
||||
if (engine && engine.isReady) {
|
||||
const engineAnalysis = await engine.getAnalysis("", {
|
||||
depth,
|
||||
movetime
|
||||
});
|
||||
positionAnalysis.engineAnalysis = engineAnalysis;
|
||||
}
|
||||
|
||||
res.json(positionAnalysis);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Engine move endpoint
|
||||
app.post('/enginemove', async (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
|
||||
if (!engine || !engine.isReady) {
|
||||
return res.status(503).json({
|
||||
error: 'Engine not available',
|
||||
details: 'Chess engine is not initialized or not ready'
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
depth = 15,
|
||||
movetime = 1000,
|
||||
nodes = null,
|
||||
fen
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
const analysis = await engine.getAnalysis(fen, {
|
||||
depth,
|
||||
movetime,
|
||||
nodes
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
move: analysis.bestMove,
|
||||
analysis: analysis,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
error: 'Engine analysis failed',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.post('/shutdown', (req, res) => {
|
||||
lastResponse = new Date().getTime()
|
||||
res.json({ status: 'shutting_down' });
|
||||
|
||||
// Give time for response to be sent
|
||||
setTimeout(() => {
|
||||
closeServer()
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Cleanup handling
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Shutting down...');
|
||||
closeServer()
|
||||
logStream.end();
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Received SIGINT, shutting down...');
|
||||
closeServer()
|
||||
logStream.end();
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
lastResponse = new Date().getTime()
|
||||
startIdleMonitor();
|
||||
console.log(`Fairy-Chess server running on port ${port}`);
|
||||
});
|
||||
|
||||
function startIdleMonitor() {
|
||||
const checkIdle = () => {
|
||||
const currentTime = new Date().getTime();
|
||||
const timeSinceLastResponse = currentTime - lastResponse;
|
||||
|
||||
if (timeSinceLastResponse > SERVER_WAIT_THRESHOLD) {
|
||||
console.log(`Server idle for ${timeSinceLastResponse/1000} seconds. Shutting down...`);
|
||||
console.log(currentTime, lastResponse, timeSinceLastResponse, SERVER_WAIT_THRESHOLD)
|
||||
closeServer();
|
||||
}
|
||||
};
|
||||
|
||||
// Start the monitoring interval
|
||||
const monitorInterval = setInterval(checkIdle, CHECK_INTERVAL);
|
||||
|
||||
// Clean up interval on server close
|
||||
process.on('SIGTERM', () => {
|
||||
clearInterval(monitorInterval);
|
||||
closeServer();
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
clearInterval(monitorInterval);
|
||||
closeServer();
|
||||
});
|
||||
}
|
||||
|
||||
function closeServer(){
|
||||
if (engine) engine.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
607
Assets/ChessEngines/fairy-chess-server/package-lock.json
generated
Normal file
607
Assets/ChessEngines/fairy-chess-server/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,607 @@
|
|||
{
|
||||
"name": "fairy-chess-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"requires": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"requires": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
|
||||
},
|
||||
"call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"call-bound": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
|
||||
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"get-intrinsic": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"requires": {
|
||||
"safe-buffer": "5.2.1"
|
||||
}
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
|
||||
},
|
||||
"destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
|
||||
},
|
||||
"dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
|
||||
},
|
||||
"es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
|
||||
},
|
||||
"es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
|
||||
},
|
||||
"es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
},
|
||||
"etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"ffish": {
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/ffish/-/ffish-0.7.7.tgz",
|
||||
"integrity": "sha512-s356geaE1BzkWTGW9y8bKMI3iobyxjds0QfK7X1rhA5Gxco5UttpAypFpFad+ruFCvdR8t/OcgXlfQLp2qAPaA=="
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"unpipe": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
|
||||
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.0.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.0",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"requires": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
|
||||
},
|
||||
"has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"requires": {
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"requires": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||
},
|
||||
"math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"requires": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"requires": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"requires": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
}
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
}
|
||||
},
|
||||
"side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"requires": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
}
|
||||
},
|
||||
"side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"requires": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"requires": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
}
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Assets/ChessEngines/fairy-chess-server/package.json
Normal file
19
Assets/ChessEngines/fairy-chess-server/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "fairy-chess-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "node test.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"express": "^4.21.2",
|
||||
"ffish": "^0.7.7"
|
||||
}
|
||||
}
|
||||
157
Assets/ChessEngines/fairy-chess-server/test.js
Normal file
157
Assets/ChessEngines/fairy-chess-server/test.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const baseURL = 'http://localhost:27531';
|
||||
|
||||
async function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function testServer() {
|
||||
try {
|
||||
|
||||
await axios.post(`${baseURL}/shutdown`);
|
||||
return
|
||||
// Test 1: Health check
|
||||
console.log('\nTest 1: Health Check');
|
||||
const health = await axios.get(`${baseURL}/health`);
|
||||
console.log('Health status:', health.data);
|
||||
|
||||
// Wait for engine initialization
|
||||
await delay(2000);
|
||||
// Test 2: Validate FEN
|
||||
console.log('\nTest 1.5: Validate MOVE');
|
||||
const moveVal = await axios.post(`${baseURL}/move`, {
|
||||
move: '@a6'
|
||||
});
|
||||
console.log('MOVE validation:', moveVal);
|
||||
await delay(2000);
|
||||
|
||||
|
||||
// Test 2: Validate FEN
|
||||
console.log('\nTest 2: Validate FEN');
|
||||
const startPos = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
||||
const validation = await axios.post(`${baseURL}/validate`, {
|
||||
fen: startPos
|
||||
});
|
||||
console.log('FEN validation:', validation.data);
|
||||
|
||||
// Test 3: Create new game
|
||||
console.log('\nTest 3: Create New Game');
|
||||
const newGame = await axios.post(`${baseURL}/new`, {
|
||||
variant: 'chess'
|
||||
});
|
||||
console.log('New game created:', newGame.data);
|
||||
|
||||
// Test 4: Set position
|
||||
console.log('\nTest 4: Set Position');
|
||||
const position = await axios.post(`${baseURL}/position`, {
|
||||
fen: startPos,
|
||||
variant: 'chess'
|
||||
});
|
||||
console.log('Position set:', position.data);
|
||||
|
||||
// Test 5: Get engine move from starting position
|
||||
console.log('\nTest 5: Get Engine Move');
|
||||
const engineMove = await axios.post(`${baseURL}/enginemove`, {
|
||||
depth: 15,
|
||||
movetime: 1000
|
||||
});
|
||||
console.log('Engine move:', engineMove.data);
|
||||
|
||||
// Test 6: Make a specific move
|
||||
console.log('\nTest 6: Make Move (UCI)');
|
||||
const moveUCI = await axios.post(`${baseURL}/move`, {
|
||||
move: 'e2e4',
|
||||
notation: 'uci'
|
||||
});
|
||||
console.log('UCI move made:', moveUCI.data);
|
||||
|
||||
// Test 7: Make move using SAN notation
|
||||
console.log('\nTest 7: Make Move (SAN)');
|
||||
const moveSAN = await axios.post(`${baseURL}/move`, {
|
||||
move: 'Nf6',
|
||||
notation: 'san'
|
||||
});
|
||||
console.log('SAN move made:', moveSAN.data);
|
||||
|
||||
// Test 8: Get current state
|
||||
console.log('\nTest 8: Get Current State');
|
||||
const state = await axios.get(`${baseURL}/state`);
|
||||
console.log('Current state:', state.data);
|
||||
|
||||
// Test 9: Position Analysis
|
||||
console.log('\nTest 9: Position Analysis');
|
||||
const analysis = await axios.post(`${baseURL}/analyze`, {
|
||||
depth: 15,
|
||||
movetime: 1000
|
||||
});
|
||||
console.log('Position analysis:', analysis.data);
|
||||
|
||||
// Test 10: Test variant chess
|
||||
console.log('\nTest 10: Test Chess Variant');
|
||||
const variantGame = await axios.post(`${baseURL}/new`, {
|
||||
variant: 'crazyhouse'
|
||||
});
|
||||
console.log('Crazyhouse game created:', variantGame.data);
|
||||
|
||||
// Test 11: Test invalid FEN
|
||||
console.log('\nTest 11: Test Invalid FEN');
|
||||
try {
|
||||
await axios.post(`${baseURL}/position`, {
|
||||
fen: 'invalid fen string',
|
||||
variant: 'chess'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Invalid FEN correctly rejected:', error.response.data);
|
||||
}
|
||||
|
||||
// Test 12: Test invalid move
|
||||
console.log('\nTest 12: Test Invalid Move');
|
||||
try {
|
||||
await axios.post(`${baseURL}/move`, {
|
||||
move: 'e2e5', // Invalid move
|
||||
notation: 'uci'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Invalid move correctly rejected:', error.response.data);
|
||||
}
|
||||
|
||||
// Test 13: Engine Analysis with Different Settings
|
||||
console.log('\nTest 13: Engine Analysis with Custom Settings');
|
||||
const customAnalysis = await axios.post(`${baseURL}/analyze`, {
|
||||
depth: 20,
|
||||
movetime: 2000
|
||||
});
|
||||
console.log('Custom analysis:', customAnalysis.data);
|
||||
|
||||
// Test 14: Final State Check
|
||||
console.log('\nTest 14: State Check');
|
||||
const finalHealth = await axios.get(`${baseURL}/health`);
|
||||
console.log('Final server status:', {
|
||||
engineReady: finalHealth.data.engineReady,
|
||||
status: finalHealth.data.status
|
||||
});
|
||||
console.log('\nTest 15: SHUTDOWN');
|
||||
await axios.post(`${baseURL}/shutdown`);
|
||||
|
||||
|
||||
console.log('\nAll tests completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error.message);
|
||||
if (error.response) {
|
||||
console.error('Error details:', {
|
||||
status: error.response.status,
|
||||
data: error.response.data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
console.log('Starting Fairy-Chess server tests...');
|
||||
testServer().then(() => {
|
||||
console.log('Test suite completed.');
|
||||
}).catch(err => {
|
||||
console.error('Test suite failed:', err);
|
||||
});
|
||||
87
Assets/ChessEngines/fairy-chess-server/variants.js
Normal file
87
Assets/ChessEngines/fairy-chess-server/variants.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import fs from 'fs'
|
||||
|
||||
const pieceSetups = [
|
||||
"nk", // 2 cells
|
||||
"nkr", // 3 cells
|
||||
"nkbr", // 4 cells
|
||||
"rnkbr", // 5 cells
|
||||
"rbnkqr", // 6 cells
|
||||
"rnbkqnr", // 7 cells
|
||||
"rnbqkbnr", // 8 cells (standard chess)
|
||||
"rbnqknbnr", // 9 cells
|
||||
"rnbnqknbnr", // 10 cells
|
||||
"rnbnqknbnnr", // 11 cells
|
||||
"rnnbnqknbnnr" // 12 cells
|
||||
];
|
||||
|
||||
const files = "abcdefghijkl";
|
||||
|
||||
function generateVariant(width, height) {
|
||||
if (width < 2 || width > 12) {
|
||||
console.error("Width must be between 2 and 12");
|
||||
return "";
|
||||
}
|
||||
|
||||
if (height < 6 || height > 12) {
|
||||
console.error("Height must be between 6 and 12");
|
||||
return "";
|
||||
}
|
||||
|
||||
const pieceSetup = pieceSetups[width - 2];
|
||||
|
||||
|
||||
const emptyRanks = height - 4;
|
||||
|
||||
|
||||
const whitePieces = pieceSetup.toUpperCase();
|
||||
const blackPieces = pieceSetup.toLowerCase();
|
||||
|
||||
const whitePawns = "P".repeat(width);
|
||||
const blackPawns = "p".repeat(width);
|
||||
const emptyRank = width.toString();
|
||||
|
||||
let fenParts = [blackPieces, blackPawns];
|
||||
|
||||
for (let i = 0; i < emptyRanks; i++) {
|
||||
fenParts.push(emptyRank);
|
||||
}
|
||||
|
||||
fenParts.push(whitePawns, whitePieces);
|
||||
const fen = fenParts.join("/") + " w - - 0 1";
|
||||
|
||||
const maxFile = files[width - 1];
|
||||
|
||||
return `[chessbuilder${width}x${height}:chess]
|
||||
pieceToCharTable = PNBRQK..*@...........pnbrqk..*@...........
|
||||
maxRank = ${height}
|
||||
maxFile = ${maxFile}
|
||||
startFen = ${fen}
|
||||
|
||||
walltype = * # Duck wall
|
||||
walltype = @ # Stone wall
|
||||
|
||||
wallingRule = static
|
||||
wallOrMove = false
|
||||
mobilityRegion = *:@
|
||||
prohibitedMove = *@
|
||||
`;
|
||||
}
|
||||
function generateAllVariants() {
|
||||
let allVariants = "";
|
||||
let count = 0;
|
||||
|
||||
for (let height = 6; height <= 12; height++) {
|
||||
for (let width = 2; width <= 12; width++) {
|
||||
const variant = generateVariant(width, height);
|
||||
allVariants += variant + "\n";
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync('chessbuilder_variants.ini', allVariants);
|
||||
console.log(`Generated ${count} variants and saved to chessbuilder_variants.ini`);
|
||||
|
||||
return allVariants;
|
||||
}
|
||||
|
||||
generateAllVariants();
|
||||
4
Assets/Themes/SimpleMenuText.tres
Normal file
4
Assets/Themes/SimpleMenuText.tres
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[gd_resource type="Theme" format=3 uid="uid://btgbiqdc4kf15"]
|
||||
|
||||
[resource]
|
||||
default_font_size = 49
|
||||
4
Assets/Themes/Title.tres
Normal file
4
Assets/Themes/Title.tres
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[gd_resource type="Theme" format=3 uid="uid://cuq0xndnachqb"]
|
||||
|
||||
[resource]
|
||||
default_font_size = 80
|
||||
BIN
Assets/main_menu/characters.png
Normal file
BIN
Assets/main_menu/characters.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
34
Assets/main_menu/characters.png.import
Normal file
34
Assets/main_menu/characters.png.import
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bexpni52h8527"
|
||||
path="res://.godot/imported/characters.png-aa4b087f4ba916c3768cf8d9d020e5a0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/main_menu/characters.png"
|
||||
dest_files=["res://.godot/imported/characters.png-aa4b087f4ba916c3768cf8d9d020e5a0.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
Assets/main_menu/icon.png
Normal file
BIN
Assets/main_menu/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
34
Assets/main_menu/icon.png.import
Normal file
34
Assets/main_menu/icon.png.import
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bdb3grojgjtaq"
|
||||
path="res://.godot/imported/icon.png-a1f010172d24ada42d71428b39fa8e05.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/main_menu/icon.png"
|
||||
dest_files=["res://.godot/imported/icon.png-a1f010172d24ada42d71428b39fa8e05.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
Assets/main_menu/label_continue.png
Normal file
BIN
Assets/main_menu/label_continue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
34
Assets/main_menu/label_continue.png.import
Normal file
34
Assets/main_menu/label_continue.png.import
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bn0offg4w11w4"
|
||||
path="res://.godot/imported/label_continue.png-73ecf71c03ce72f5729846d86d279818.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/main_menu/label_continue.png"
|
||||
dest_files=["res://.godot/imported/label_continue.png-73ecf71c03ce72f5729846d86d279818.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
Assets/main_menu/label_options.png
Normal file
BIN
Assets/main_menu/label_options.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
34
Assets/main_menu/label_options.png.import
Normal file
34
Assets/main_menu/label_options.png.import
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b8khh5b1iwic1"
|
||||
path="res://.godot/imported/label_options.png-19b15ef319e033f5146c10542dd5f109.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/main_menu/label_options.png"
|
||||
dest_files=["res://.godot/imported/label_options.png-19b15ef319e033f5146c10542dd5f109.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
6
Game.json
Normal file
6
Game.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"version": "0.0.1",
|
||||
"title": "ChessBuilder",
|
||||
"developer": "Comet Studios",
|
||||
"release_date": "2025-01-15"
|
||||
}
|
||||
|
|
@ -21,11 +21,13 @@ var unitWhitelist: Array[String] = [] # List of piece types this card can be at
|
|||
var id: String = Utils.generate_guid()
|
||||
var stored_board_flow = null
|
||||
var stored_game_state = null
|
||||
var is_default: bool = false
|
||||
|
||||
func _init():
|
||||
remaining_turns = duration
|
||||
# print(id)
|
||||
|
||||
func reset():
|
||||
remaining_turns = duration
|
||||
func can_attach_to_piece(piece: Pawn) -> bool:
|
||||
# print(unitWhitelist, " | ", piece.name , " | ", unitWhitelist.has(piece.name))
|
||||
if unitWhitelist.is_empty():
|
||||
|
|
|
|||
1
Systems/Card.gd.uid
Normal file
1
Systems/Card.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://eqadab7yd0qq
|
||||
|
|
@ -1,65 +1,98 @@
|
|||
class_name CardDisplay extends Control
|
||||
|
||||
const CARD_WIDTH = 150
|
||||
const CARD_HEIGHT = 250
|
||||
# Base card dimensions
|
||||
const BASE_CARD_WIDTH = 150
|
||||
const BASE_CARD_HEIGHT = 250
|
||||
const CARD_MARGIN = 10
|
||||
const MAX_HAND_WIDTH_RATIO = 0.7 # Maximum portion of screen width to use for hand
|
||||
|
||||
var cardDisplays = []
|
||||
var cardDisplays = [] # Array of {panel: Node, card: Card}
|
||||
var selectedCard = null
|
||||
var container: HBoxContainer
|
||||
var cardPreviewScene = preload("res://card_preview_panel.tscn")
|
||||
var currently_hovered_card = null
|
||||
# Preview card panel instance for hovering
|
||||
var hoveredPreview: CardPreviewPanel = null
|
||||
var isPreviewVisible = false
|
||||
|
||||
func _ready():
|
||||
# Create the container first
|
||||
# Create the container for the hand
|
||||
container = HBoxContainer.new()
|
||||
container.name = "Hand"
|
||||
container.position = Vector2(10, 500)
|
||||
container.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
add_child(container)
|
||||
|
||||
# Create a hover preview panel that will follow the mouse
|
||||
hoveredPreview = cardPreviewScene.instantiate()
|
||||
hoveredPreview.visible = false
|
||||
add_child(hoveredPreview)
|
||||
|
||||
func update_hand(hand: Array):
|
||||
clear_cards()
|
||||
|
||||
# Calculate card sizes based on hand size and screen width
|
||||
var card_size = calculate_card_size(hand.size())
|
||||
|
||||
for card in hand:
|
||||
add_card_display(card)
|
||||
add_card_display(card, card_size)
|
||||
|
||||
func add_card_display(card):
|
||||
var cardPanel = PanelContainer.new()
|
||||
cardPanel.custom_minimum_size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||
func calculate_card_size(hand_size: int) -> Vector2:
|
||||
# Get screen dimensions
|
||||
var screen_width = get_viewport_rect().size.x
|
||||
var screen_height = get_viewport_rect().size.y
|
||||
|
||||
var vbox = VBoxContainer.new()
|
||||
vbox.set_meta("card_id", card.id)
|
||||
cardPanel.add_child(vbox)
|
||||
var max_hand_width = screen_width * MAX_HAND_WIDTH_RATIO
|
||||
var width_per_card = max_hand_width / max(hand_size, 1)
|
||||
|
||||
var nameLabel = Label.new()
|
||||
nameLabel.text = card.cardName
|
||||
nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(nameLabel)
|
||||
# Ensure minimum size and preserve aspect ratio
|
||||
var card_width = clamp(width_per_card - CARD_MARGIN, 80, BASE_CARD_WIDTH)
|
||||
var card_height = card_width * (BASE_CARD_HEIGHT / BASE_CARD_WIDTH)
|
||||
|
||||
var rankLabel = Label.new()
|
||||
rankLabel.text = "Rank " + str(card.rank)
|
||||
rankLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(rankLabel)
|
||||
# Ensure cards don't extend off the bottom of the screen
|
||||
var max_height = screen_height * 0.4 # Use at most 40% of screen height
|
||||
if card_height > max_height:
|
||||
card_height = max_height
|
||||
card_width = card_height * (BASE_CARD_WIDTH / BASE_CARD_HEIGHT)
|
||||
|
||||
var descLabel = Label.new()
|
||||
descLabel.text = card.description
|
||||
descLabel.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
descLabel.custom_minimum_size = Vector2(CARD_WIDTH - 10, 0)
|
||||
vbox.add_child(descLabel)
|
||||
|
||||
var unitWhitelistLabel = Label.new()
|
||||
unitWhitelistLabel.text = ", ".join(card.unitWhitelist)
|
||||
unitWhitelistLabel.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
unitWhitelistLabel.custom_minimum_size = Vector2(CARD_WIDTH - 10, 0)
|
||||
vbox.add_child(unitWhitelistLabel)
|
||||
return Vector2(card_width, card_height)
|
||||
|
||||
var idLabel = Label.new()
|
||||
idLabel.text = card.id.substr(card.id.length() - 8, -1)
|
||||
idLabel.autowrap_mode = true
|
||||
idLabel.custom_minimum_size = Vector2(CARD_WIDTH - 10, 0)
|
||||
vbox.add_child(idLabel)
|
||||
|
||||
cardPanel.gui_input.connect(func(event): _on_card_clicked(event, card))
|
||||
func add_card_display(card, card_size: Vector2 = Vector2(BASE_CARD_WIDTH, BASE_CARD_HEIGHT)):
|
||||
# Create a container for the card
|
||||
var cardContainer = PanelContainer.new()
|
||||
cardContainer.custom_minimum_size = card_size
|
||||
|
||||
cardDisplays.append(cardPanel)
|
||||
container.add_child(cardPanel)
|
||||
# Create a stylebox for selection highlighting
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Utils.DARK_CELL
|
||||
style.corner_radius_top_left = 5
|
||||
style.corner_radius_top_right = 5
|
||||
style.corner_radius_bottom_left = 5
|
||||
style.corner_radius_bottom_right = 5
|
||||
cardContainer.add_theme_stylebox_override("panel", style)
|
||||
|
||||
# Create compact card display
|
||||
var compact_card = CompactCardDisplay.new()
|
||||
compact_card.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
compact_card.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
|
||||
# Add to container
|
||||
cardContainer.add_child(compact_card)
|
||||
compact_card.set_card(card)
|
||||
|
||||
# Connect signals
|
||||
cardContainer.gui_input.connect(func(event): _on_card_clicked(event, card))
|
||||
cardContainer.mouse_entered.connect(func(): _on_card_mouse_entered(card))
|
||||
cardContainer.mouse_exited.connect(func(): _on_card_mouse_exited())
|
||||
|
||||
# Add to hand container
|
||||
container.add_child(cardContainer)
|
||||
|
||||
# Add to display list
|
||||
cardDisplays.append({
|
||||
"panel": cardContainer,
|
||||
"card": card
|
||||
})
|
||||
|
||||
func _on_card_clicked(event: InputEvent, card: Card):
|
||||
if event is InputEventMouseButton and event.pressed:
|
||||
|
|
@ -70,29 +103,70 @@ func _on_card_clicked(event: InputEvent, card: Card):
|
|||
selectedCard = null
|
||||
highlight_selectedCard(selectedCard)
|
||||
|
||||
func _on_card_mouse_entered( card: Card):
|
||||
# Show card preview when hovering
|
||||
# print("_on_card_mouse_entered")
|
||||
currently_hovered_card = card
|
||||
hoveredPreview.preview_card(card)
|
||||
hoveredPreview.visible = true
|
||||
|
||||
# Position it above the hand area
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
hoveredPreview.position = Vector2(mouse_pos.x - hoveredPreview.size.x/2, mouse_pos.y - hoveredPreview.size.y - 20)
|
||||
|
||||
# Make sure it's visible within screen bounds
|
||||
hoveredPreview.position.x = clamp(hoveredPreview.position.x, 10, get_viewport_rect().size.x - hoveredPreview.size.x - 10)
|
||||
hoveredPreview.position.y = clamp(hoveredPreview.position.y, 10, get_viewport_rect().size.y - hoveredPreview.size.y - 10)
|
||||
|
||||
isPreviewVisible = true
|
||||
|
||||
func _on_card_mouse_exited():
|
||||
# Only hide the preview after a delay and if we're not hovering over a new card
|
||||
var exited_card = currently_hovered_card
|
||||
currently_hovered_card = null
|
||||
|
||||
# Wait a bit before hiding
|
||||
await get_tree().create_timer(0.2).timeout
|
||||
|
||||
# Only hide if we're not hovering over a new card
|
||||
if exited_card == currently_hovered_card or currently_hovered_card == null:
|
||||
hoveredPreview.visible = false
|
||||
isPreviewVisible = false
|
||||
|
||||
func _process(delta):
|
||||
# If preview is visible, update its position to follow the mouse
|
||||
if isPreviewVisible:
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
hoveredPreview.position = Vector2(mouse_pos.x - hoveredPreview.size.x/2, mouse_pos.y - hoveredPreview.size.y - 20)
|
||||
hoveredPreview.position.x = clamp(hoveredPreview.position.x, 10, get_viewport_rect().size.x - hoveredPreview.size.x - 10)
|
||||
hoveredPreview.position.y = clamp(hoveredPreview.position.y, 10, get_viewport_rect().size.y - hoveredPreview.size.y - 10)
|
||||
|
||||
func highlight_selectedCard(selected: Card) -> void:
|
||||
for display in cardDisplays:
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Utils.DARK_CELL # Default color
|
||||
for card_data in cardDisplays:
|
||||
var panel = card_data.panel
|
||||
var compact_card = panel.get_child(0) as CompactCardDisplay
|
||||
|
||||
var vbox = display.get_child(0)
|
||||
if selected && vbox.get_meta("card_id") == selected.id:
|
||||
style.bg_color = Color(0.4, 0.6, 0.4, 1) # Selected color
|
||||
if selected && card_data.card.id == selected.id:
|
||||
|
||||
display.add_theme_stylebox_override("panel", style)
|
||||
# Apply highlighting to the CompactCardDisplay
|
||||
if compact_card:
|
||||
compact_card.set_selected(true)
|
||||
else:
|
||||
# Reset highlighting
|
||||
if compact_card:
|
||||
compact_card.set_selected(false)
|
||||
|
||||
func get_card_by_uuid(uuid: String) -> Card:
|
||||
for display in cardDisplays:
|
||||
var vbox = display.get_child(0)
|
||||
if vbox.get_meta("card_id") == uuid:
|
||||
return selectedCard
|
||||
for card_data in cardDisplays:
|
||||
if card_data.card.id == uuid:
|
||||
return card_data.card
|
||||
return null
|
||||
|
||||
func getSelectedCard() -> Card:
|
||||
return selectedCard
|
||||
|
||||
func clear_cards():
|
||||
for display in cardDisplays:
|
||||
display.queue_free()
|
||||
for card_data in cardDisplays:
|
||||
card_data.panel.queue_free()
|
||||
cardDisplays.clear()
|
||||
selectedCard = null
|
||||
selectedCard = null
|
||||
|
|
|
|||
1
Systems/CardDisplay.gd.uid
Normal file
1
Systems/CardDisplay.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dgsj76xih6r3y
|
||||
|
|
@ -6,88 +6,55 @@ const PREVIEW_HEIGHT = 200
|
|||
const SCREEN_MARGIN = 20
|
||||
|
||||
var currentCard: Card = null
|
||||
var previewPanel: PanelContainer
|
||||
var previewPanel: CardPreviewPanel
|
||||
|
||||
var movesRemainingLabel: Label
|
||||
func _init() -> void:
|
||||
|
||||
previewPanel = PanelContainer.new()
|
||||
previewPanel.custom_minimum_size = Vector2(PREVIEW_WIDTH, PREVIEW_HEIGHT)
|
||||
|
||||
previewPanel = preload("res://card_preview_panel.tscn").instantiate()
|
||||
|
||||
|
||||
position = Vector2(0, 0)
|
||||
|
||||
var vbox = VBoxContainer.new()
|
||||
previewPanel.add_child(vbox)
|
||||
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.2, 0.2, 0.2, 0.9)
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
style.corner_radius_bottom_left = 10
|
||||
style.corner_radius_bottom_right = 10
|
||||
previewPanel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
previewPanel.hide()
|
||||
|
||||
# Add the preview panel as a child
|
||||
add_child(previewPanel)
|
||||
|
||||
# Add a custom label for moves remaining that we'll show/hide as needed
|
||||
movesRemainingLabel = Label.new()
|
||||
movesRemainingLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
movesRemainingLabel.add_theme_font_size_override("font_size", 14)
|
||||
movesRemainingLabel.visible = false
|
||||
|
||||
# Add the label to the preview panel's content
|
||||
var content = previewPanel.get_node("CardContent")
|
||||
if content:
|
||||
content.add_child(movesRemainingLabel)
|
||||
|
||||
func show_card_preview(card: Card) -> void:
|
||||
if card == null:
|
||||
previewPanel.hide()
|
||||
currentCard = null
|
||||
hide_preview()
|
||||
return
|
||||
|
||||
currentCard = card
|
||||
var vbox = previewPanel.get_child(0)
|
||||
|
||||
for child in vbox.get_children():
|
||||
child.queue_free()
|
||||
# Use the preview_card method from CardPreviewPanel
|
||||
previewPanel.preview_card(card)
|
||||
|
||||
var nameLabel = Label.new()
|
||||
nameLabel.text = card.cardName
|
||||
nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
nameLabel.add_theme_font_size_override("font_size", 18)
|
||||
vbox.add_child(nameLabel)
|
||||
|
||||
var rankLabel = Label.new()
|
||||
rankLabel.text = "Rank " + str(card.rank)
|
||||
rankLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(rankLabel)
|
||||
|
||||
var idLabel = Label.new()
|
||||
idLabel.text = "ID: " + card.id.substr(card.id.length() - 8, -1)
|
||||
idLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(idLabel)
|
||||
|
||||
var durationLabel = Label.new()
|
||||
durationLabel.text = str(card.remaining_turns) + " turns remaining"
|
||||
durationLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(durationLabel)
|
||||
|
||||
var descLabel = Label.new()
|
||||
descLabel.text = card.description
|
||||
descLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
descLabel.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
descLabel.custom_minimum_size = Vector2(PREVIEW_WIDTH - 20, 0)
|
||||
vbox.add_child(descLabel)
|
||||
|
||||
previewPanel.show()
|
||||
# Since we're showing a new card, hide the moves remaining label
|
||||
movesRemainingLabel.visible = false
|
||||
|
||||
func hide_preview() -> void:
|
||||
previewPanel.hide()
|
||||
previewPanel.hide_preview()
|
||||
currentCard = null
|
||||
movesRemainingLabel.visible = false
|
||||
|
||||
func update_moves_remaining(moves: int) -> void:
|
||||
if currentCard == null:
|
||||
return
|
||||
|
||||
var vbox = previewPanel.get_child(0)
|
||||
var movesLabel = Label.new()
|
||||
movesLabel.text = "Moves remaining this turn: " + str(moves)
|
||||
movesLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
|
||||
# Remove old moves label if it exists
|
||||
for child in vbox.get_children():
|
||||
if child.text.begins_with("Moves remaining this turn:"):
|
||||
child.queue_free()
|
||||
|
||||
vbox.add_child(movesLabel)
|
||||
# Update and show the moves remaining label
|
||||
movesRemainingLabel.text = "Moves remaining this turn: " + str(moves)
|
||||
movesRemainingLabel.visible = true
|
||||
1
Systems/CardPreview.gd.uid
Normal file
1
Systems/CardPreview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://p4ahefeutov0
|
||||
84
Systems/CardPreviewPanel.gd
Normal file
84
Systems/CardPreviewPanel.gd
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
extends Panel
|
||||
class_name CardPreviewPanel
|
||||
|
||||
# Node references
|
||||
@onready var card_name_label = $CardContent/CardNameLabel
|
||||
@onready var rank_label = $CardContent/RankLabel
|
||||
@onready var description_label = $CardContent/DescriptionLabel
|
||||
@onready var unit_list_label = $CardContent/UnitListLabel
|
||||
@onready var effect_type_label = $CardContent/EffectTypeLabel
|
||||
@onready var duration_label = $CardContent/DurationLabel
|
||||
|
||||
# Card rank colors (same as other card components)
|
||||
var rank_colors = {
|
||||
Card.Rank.RANK_0: Color(0.9, 0.1, 0.1, 1.0), # Red
|
||||
Card.Rank.RANK_1: Color(0.9, 0.6, 0.1, 1.0), # Orange
|
||||
Card.Rank.RANK_2: Color(0.1, 0.7, 0.1, 1.0), # Green
|
||||
Card.Rank.RANK_3: Color(0.1, 0.7, 0.9, 1.0) # Blue
|
||||
}
|
||||
|
||||
# Effect type descriptions
|
||||
var effect_type_names = {
|
||||
Card.EffectType.MOVEMENT_MODIFIER: "Movement Modifier",
|
||||
Card.EffectType.BOARD_EFFECT: "Board Effect",
|
||||
Card.EffectType.PIECE_EFFECT: "Piece Effect",
|
||||
Card.EffectType.SPECIAL_ACTION: "Special Action"
|
||||
}
|
||||
|
||||
func _ready():
|
||||
# Hide the preview initially
|
||||
visible = false
|
||||
|
||||
func preview_card(card: Card):
|
||||
if !card:
|
||||
hide_preview()
|
||||
return
|
||||
# if !is_inside_tree():
|
||||
# await ready
|
||||
# Update all card information
|
||||
card_name_label.text = card.cardName
|
||||
|
||||
# Set rank information
|
||||
var rank_text = "Rank " + str(card.rank)
|
||||
match card.rank:
|
||||
Card.Rank.RANK_0:
|
||||
rank_text += " (One-time use, removed from deck)"
|
||||
Card.Rank.RANK_1:
|
||||
rank_text += " (Once per match)"
|
||||
Card.Rank.RANK_2:
|
||||
rank_text += " (Discarded after use)"
|
||||
Card.Rank.RANK_3:
|
||||
rank_text += " (Reused)"
|
||||
rank_label.text = rank_text
|
||||
|
||||
# Set rank color
|
||||
if card.rank in rank_colors:
|
||||
rank_label.add_theme_color_override("font_color", rank_colors[card.rank])
|
||||
card_name_label.add_theme_color_override("font_color", rank_colors[card.rank])
|
||||
|
||||
# Set description
|
||||
description_label.text = card.description
|
||||
|
||||
# Set unit whitelist
|
||||
if card.unitWhitelist.is_empty():
|
||||
unit_list_label.text = "Can be applied to: Any piece"
|
||||
else:
|
||||
unit_list_label.text = "Can be applied to: " + ", ".join(card.unitWhitelist)
|
||||
|
||||
# Set effect type
|
||||
if card.effectType in effect_type_names:
|
||||
effect_type_label.text = "Effect Type: " + effect_type_names[card.effectType]
|
||||
else:
|
||||
effect_type_label.text = "Effect Type: Unknown"
|
||||
|
||||
# Set duration
|
||||
if card.duration > 0:
|
||||
duration_label.text = "Duration: " + str(card.duration) + " turns"
|
||||
else:
|
||||
duration_label.text = "Duration: Instant"
|
||||
|
||||
# Show the preview
|
||||
visible = true
|
||||
|
||||
func hide_preview():
|
||||
visible = false
|
||||
1
Systems/CardPreviewPanel.gd.uid
Normal file
1
Systems/CardPreviewPanel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://2xcreiq6lhe2
|
||||
54
Systems/Cards/CardBankItem.gd
Normal file
54
Systems/Cards/CardBankItem.gd
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
extends Control
|
||||
class_name CardBankItem
|
||||
|
||||
signal card_selected(card_item, card)
|
||||
|
||||
var current_card = null
|
||||
|
||||
@onready var rank_label = $RankLabel
|
||||
@onready var card_name_label = $CardNameLabel
|
||||
@onready var card_frame = $CardFrame
|
||||
|
||||
# Card rank colors (matching CardSlot colors)
|
||||
var rank_colors = {
|
||||
Card.Rank.RANK_0: Color(0.9, 0.1, 0.1, 1.0), # Red
|
||||
Card.Rank.RANK_1: Color(0.9, 0.6, 0.1, 1.0), # Orange
|
||||
Card.Rank.RANK_2: Color(0.1, 0.7, 0.1, 1.0), # Green
|
||||
Card.Rank.RANK_3: Color(0.1, 0.7, 0.9, 1.0) # Blue
|
||||
}
|
||||
|
||||
func _ready():
|
||||
# Connect signals for interaction
|
||||
custom_minimum_size = Vector2(0, 40) # Ensure it has enough height
|
||||
size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
if card_frame:
|
||||
card_frame.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
if rank_label:
|
||||
rank_label.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
if card_name_label:
|
||||
card_name_label.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
|
||||
# Connect GUI input signal
|
||||
gui_input.connect(_on_gui_input)
|
||||
|
||||
func set_card(card):
|
||||
current_card = card
|
||||
update_appearance()
|
||||
|
||||
func update_appearance():
|
||||
if current_card:
|
||||
# Update card details
|
||||
card_name_label.text = current_card.cardName
|
||||
rank_label.text = str(current_card.rank)
|
||||
|
||||
# Set color based on rank
|
||||
if current_card.rank in rank_colors:
|
||||
rank_label.modulate = rank_colors[current_card.rank]
|
||||
card_frame.modulate = rank_colors[current_card.rank]
|
||||
|
||||
func _on_gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
if current_card:
|
||||
emit_signal("card_selected", self, current_card)
|
||||
1
Systems/Cards/CardBankItem.gd.uid
Normal file
1
Systems/Cards/CardBankItem.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b1d17jfxagdc7
|
||||
93
Systems/Cards/CardSlot.gd
Normal file
93
Systems/Cards/CardSlot.gd
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
extends Panel
|
||||
class_name CardSlot
|
||||
|
||||
signal card_selected(card_slot, card)
|
||||
|
||||
var current_card = null
|
||||
var dragging = false
|
||||
var drag_offset = Vector2()
|
||||
|
||||
@onready var card_name_label = $CardNameLabel
|
||||
@onready var card_rank_label = $RankLabel
|
||||
@onready var card_border = $CardBorder
|
||||
@onready var card_bg = $CardBackground
|
||||
|
||||
# Card rank colors
|
||||
var rank_colors = {
|
||||
Card.Rank.RANK_0: Color(0.9, 0.1, 0.1, 1.0), # Red
|
||||
Card.Rank.RANK_1: Color(0.9, 0.6, 0.1, 1.0), # Orange
|
||||
Card.Rank.RANK_2: Color(0.1, 0.7, 0.1, 1.0), # Green
|
||||
Card.Rank.RANK_3: Color(0.1, 0.7, 0.9, 1.0) # Blue
|
||||
}
|
||||
|
||||
func _ready():
|
||||
# Set up card appearance
|
||||
update_appearance()
|
||||
|
||||
# Connect signals for interaction
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
gui_input.connect(_on_gui_input)
|
||||
if card_border:
|
||||
card_border.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
for child in get_children():
|
||||
if child is Control:
|
||||
child.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
|
||||
|
||||
func set_card(card):
|
||||
current_card = card
|
||||
update_appearance()
|
||||
|
||||
func clear():
|
||||
current_card = null
|
||||
update_appearance()
|
||||
|
||||
func has_card():
|
||||
return current_card != null
|
||||
|
||||
func update_appearance():
|
||||
if current_card:
|
||||
# Show card details
|
||||
card_name_label.text = current_card.cardName
|
||||
card_name_label.visible = true
|
||||
|
||||
# Set rank display
|
||||
card_rank_label.text = str(current_card.rank)
|
||||
card_rank_label.visible = true
|
||||
|
||||
# Set color based on rank
|
||||
if current_card.rank in rank_colors:
|
||||
card_rank_label.add_theme_color_override("font_color", rank_colors[current_card.rank])
|
||||
card_border.modulate = rank_colors[current_card.rank]
|
||||
|
||||
# Show card background
|
||||
card_bg.modulate = Color(0.2, 0.2, 0.25, 1.0)
|
||||
else:
|
||||
# Empty slot
|
||||
card_name_label.visible = false
|
||||
card_rank_label.visible = false
|
||||
card_border.modulate = Color(0.4, 0.4, 0.4, 0.5)
|
||||
card_bg.modulate = Color(0.15, 0.15, 0.15, 0.8)
|
||||
|
||||
func _on_gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
# Start drag
|
||||
if current_card:
|
||||
emit_signal("card_selected", self, current_card)
|
||||
# dragging = true
|
||||
# drag_offset = get_global_mouse_position() - global_position
|
||||
# else:
|
||||
# # End drag
|
||||
# if dragging:
|
||||
# dragging = false
|
||||
# emit_signal("card_selected", self, current_card)
|
||||
# elif current_card:
|
||||
# # Just a click, still select the card
|
||||
# emit_signal("card_selected", self, current_card)
|
||||
|
||||
func _process(delta):
|
||||
if dragging:
|
||||
# Update position while dragging
|
||||
global_position = get_global_mouse_position() - drag_offset
|
||||
1
Systems/Cards/CardSlot.gd.uid
Normal file
1
Systems/Cards/CardSlot.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b5888fqblsco5
|
||||
|
|
@ -4,12 +4,12 @@ func _init():
|
|||
super._init()
|
||||
# id = Utils.generate_guid()
|
||||
cardName = "Double Time"
|
||||
rank = Rank.RANK_2
|
||||
rank = Rank.RANK_3
|
||||
effectType = EffectType.MOVEMENT_MODIFIER
|
||||
description = "Piece can move twice in one turn"
|
||||
duration = 2 # Lasts for 2 turns
|
||||
unitWhitelist = []
|
||||
|
||||
is_default = true
|
||||
func modify_moves() -> Dictionary:
|
||||
return {
|
||||
"extra_moves": 1,
|
||||
|
|
|
|||
1
Systems/Cards/Doubletime.gd.uid
Normal file
1
Systems/Cards/Doubletime.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://o16wgudelf8x
|
||||
|
|
@ -22,7 +22,7 @@ func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
|||
var current_y = int(current_pos[1])
|
||||
|
||||
# Calculate target position
|
||||
var target_y = 7 if target_piece.Item_Color == 1 else 0
|
||||
var target_y = game_state.boardYSize - 1 if target_piece.Item_Color == 1 else 0
|
||||
var y_direction = 1 if target_y > current_y else -1
|
||||
|
||||
# Generate tiles to check
|
||||
|
|
@ -32,6 +32,12 @@ func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
|||
tiles_to_check.append(str(current_x) + "-" + str(y))
|
||||
y += y_direction
|
||||
|
||||
if game_state.isPlayerTurn():
|
||||
print("")
|
||||
else:
|
||||
print("REVERSING TILES TO CHECK")
|
||||
tiles_to_check.reverse()
|
||||
|
||||
# Find final position (stop before any impassable tile)
|
||||
var final_y = target_y
|
||||
for tile_name in tiles_to_check:
|
||||
|
|
@ -41,10 +47,14 @@ func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
|||
var tile_y = int(tile_name.split("-")[1])
|
||||
final_y = tile_y - y_direction
|
||||
break
|
||||
print("CHECKING TILES ", tiles_to_check)
|
||||
# Process pieces in path
|
||||
for tile_name in tiles_to_check:
|
||||
var tile_y = int(tile_name.split("-")[1])
|
||||
var tile = game_state.tileManager.get_tile(tile_name)
|
||||
if tile && !tile.passable:
|
||||
# print("CHECKING TILES ", tile_name, " passable ", tile.passable)
|
||||
break
|
||||
# elif !tile:
|
||||
# print("CHECKING TILES ", tile_name, " no tile")
|
||||
# Only process tiles up to where we're stopping
|
||||
|
||||
var container = board_flow.get_node(tile_name) as PieceContainer
|
||||
|
|
@ -55,7 +65,7 @@ func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
|||
# Move piece to final position
|
||||
print("FINAL TILES ", str(current_x) + "-" + str(final_y))
|
||||
var final_container = board_flow.get_node(str(current_x) + "-" + str(final_y)) as PieceContainer
|
||||
print("FINAL TILES ", final_container.get_piece().Item_Color)
|
||||
# print("FINAL TILES ", final_container.get_piece().Item_Color)
|
||||
|
||||
# Important: Need to remove the piece from its current container first
|
||||
# AND keep a reference to it
|
||||
|
|
@ -66,6 +76,8 @@ func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
|||
burned = true
|
||||
|
||||
game_state.resolveMoveEffects()
|
||||
game_state.clearSelection()
|
||||
game_state.stateMachine.transitionToNextState(Constants.POST_MOVE)
|
||||
# Breaks Stockfish for some reason
|
||||
|
||||
return true
|
||||
return true
|
||||
|
|
|
|||
1
Systems/Cards/Drunkdriving.gd.uid
Normal file
1
Systems/Cards/Drunkdriving.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bayyvtfwpshsh
|
||||
|
|
@ -6,13 +6,19 @@ func _init():
|
|||
duration = 1
|
||||
super._init()
|
||||
cardName = "Explosive Boots"
|
||||
rank = Rank.RANK_2
|
||||
rank = Rank.RANK_3
|
||||
effectType = EffectType.PIECE_EFFECT
|
||||
description = "All enemy units within 1 radius of the moving peice at the end of the turn are captured"
|
||||
unitWhitelist = ["Pawn", "Knight", "King"]
|
||||
remaining_turns = duration
|
||||
is_default = true
|
||||
|
||||
func reset():
|
||||
remaining_turns = duration
|
||||
valid = false
|
||||
|
||||
func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
||||
print("EXPLOSIVE apply_effect", target_piece)
|
||||
attached_piece = target_piece
|
||||
stored_board_flow = board_flow
|
||||
stored_game_state = game_state
|
||||
|
|
@ -22,6 +28,7 @@ func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
|||
setup_persistent_effect(game_state)
|
||||
# dont apply on card attachment
|
||||
if !valid:
|
||||
print("EXPLOSIVE apply_effect INVALID", )
|
||||
return true
|
||||
var piece_pos = target_piece.get_parent().name.split("-")
|
||||
var piece_x = int(piece_pos[0])
|
||||
|
|
@ -104,8 +111,10 @@ func setup_persistent_effect(game_state):
|
|||
game_state.connect("turn_changed", on_turn_changed)
|
||||
|
||||
func on_turn_changed():
|
||||
# print("TURN CHANGED ==================", attached_piece, " | ", remaining_turns, )
|
||||
|
||||
print("TURN CHANGED ==================", stored_game_state.isPlayerTurn() )
|
||||
if stored_game_state.stateMachine.disabled:
|
||||
return
|
||||
|
||||
if stored_game_state.isPlayerTurn() and attached_piece and remaining_turns > 0:
|
||||
var piece = attached_piece
|
||||
var flow = stored_board_flow
|
||||
|
|
|
|||
1
Systems/Cards/Explosiveboots.gd.uid
Normal file
1
Systems/Cards/Explosiveboots.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://64bdg86r6t4o
|
||||
|
|
@ -76,4 +76,4 @@ func remove_effect():
|
|||
var game_state = stored_game_state
|
||||
if game_state.is_connected("turn_changed", on_turn_changed):
|
||||
game_state.disconnect("turn_changed", on_turn_changed)
|
||||
super.remove_effect()
|
||||
super.remove_effect()
|
||||
|
|
|
|||
1
Systems/Cards/FieryCape.gd.uid
Normal file
1
Systems/Cards/FieryCape.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cpgq5itsb2l8j
|
||||
1
Systems/Cards/Fierytrail.gd.uid
Normal file
1
Systems/Cards/Fierytrail.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ca4j0d6hrcm4x
|
||||
143
Systems/Cards/Halitosis.gd
Normal file
143
Systems/Cards/Halitosis.gd
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
class_name HalitosisCard extends Card
|
||||
|
||||
const EFFECT_RADIUS = 1
|
||||
var valid = false;
|
||||
func _init():
|
||||
duration = 3
|
||||
super._init()
|
||||
cardName = "Halitosis"
|
||||
rank = Rank.RANK_2
|
||||
effectType = EffectType.PIECE_EFFECT
|
||||
description = "All enemy units within 1 radius of the moving piece at the end of the turn are pushed back"
|
||||
unitWhitelist = ["Pawn", "Knight", "King"]
|
||||
remaining_turns = duration
|
||||
is_default = false
|
||||
|
||||
func reset():
|
||||
remaining_turns = duration
|
||||
valid = false
|
||||
|
||||
func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
||||
print("HALITOSIS apply_effect", target_piece)
|
||||
attached_piece = target_piece
|
||||
stored_board_flow = board_flow
|
||||
stored_game_state = game_state
|
||||
if !target_piece or !board_flow or !game_state:
|
||||
print(cardName, " missing input param ")
|
||||
return false
|
||||
setup_persistent_effect(game_state)
|
||||
# dont apply on card attachment
|
||||
if !valid:
|
||||
print("HALITOSIS apply_effect INVALID", )
|
||||
return true
|
||||
var piece_pos = target_piece.get_parent().name.split("-")
|
||||
var piece_x = int(piece_pos[0])
|
||||
var piece_y = int(piece_pos[1])
|
||||
|
||||
var tiles_to_check = []
|
||||
for dx in range(-EFFECT_RADIUS, EFFECT_RADIUS + 1):
|
||||
for dy in range(-EFFECT_RADIUS, EFFECT_RADIUS + 1):
|
||||
if max(abs(dx), abs(dy)) > EFFECT_RADIUS:
|
||||
continue
|
||||
|
||||
var target_x = piece_x + dx
|
||||
var target_y = piece_y + dy
|
||||
|
||||
if target_x < 0 or target_x >= game_state.boardXSize or target_y < 0 or target_y >= game_state.boardYSize:
|
||||
continue
|
||||
|
||||
if dx == 0 and dy == 0:
|
||||
continue
|
||||
|
||||
tiles_to_check.append(str(target_x) + "-" + str(target_y))
|
||||
|
||||
# Process tiles and add overlay
|
||||
for tile_name in tiles_to_check:
|
||||
var container = board_flow.get_node(tile_name) as PieceContainer
|
||||
|
||||
# Handle overlay through container's overlay management
|
||||
container.remove_overlay("HalitosisOverlay")
|
||||
|
||||
var overlay = ColorRect.new()
|
||||
overlay.name = "HalitosisOverlay"
|
||||
overlay.color = Color(0.5, 0.7, 1, 0.3) # Blue-ish
|
||||
overlay.size = container.size
|
||||
overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_overlay(overlay)
|
||||
overlay.show()
|
||||
|
||||
# Check for pieces to affect
|
||||
var piece = container.get_piece()
|
||||
if piece != null and piece.Item_Color != target_piece.Item_Color:
|
||||
# Calculate the direction to push
|
||||
var push_dir_x = piece_x - int(tile_name.split("-")[0])
|
||||
var push_dir_y = piece_y - int(tile_name.split("-")[1])
|
||||
|
||||
# Normalize the direction vector
|
||||
var length = sqrt(push_dir_x * push_dir_x + push_dir_y * push_dir_y)
|
||||
if length > 0:
|
||||
push_dir_x = round(push_dir_x / length)
|
||||
push_dir_y = round(push_dir_y / length)
|
||||
|
||||
# Calculate target cell to push to
|
||||
var push_target_x = int(int(tile_name.split("-")[0]) - push_dir_x)
|
||||
var push_target_y = int(int(tile_name.split("-")[1]) - push_dir_y)
|
||||
var push_target = str(push_target_x) + "-" + str(push_target_y)
|
||||
print("HALITSOSES ", tile_name, " ", push_target)
|
||||
# Check if target is valid and empty
|
||||
if push_target_x >= 0 and push_target_x < game_state.boardXSize and push_target_y >= 0 and push_target_y < game_state.boardYSize:
|
||||
var target_container = board_flow.get_node(push_target) as PieceContainer
|
||||
if target_container and !target_container.has_piece():
|
||||
# Animate the push
|
||||
target_container.animate_movement(container, piece)
|
||||
else:
|
||||
# If we can't push (blocked), capture the piece instead
|
||||
# game_state.updatePointsAndCapture(piece)
|
||||
print("Blocked should we capture?")
|
||||
else:
|
||||
print("Off Board should we capture?")
|
||||
# If push would be off the board, capture the piece
|
||||
# game_state.updatePointsAndCapture(piece)
|
||||
|
||||
# Setup timer to remove overlays
|
||||
var timer = Timer.new()
|
||||
board_flow.add_child(timer)
|
||||
timer.wait_time = 1
|
||||
timer.one_shot = true
|
||||
timer.timeout.connect(func():
|
||||
for tile_name in tiles_to_check:
|
||||
var tile = board_flow.get_node(tile_name)
|
||||
var overlay = tile.get_node_or_null("HalitosisOverlay")
|
||||
if overlay:
|
||||
overlay.queue_free()
|
||||
timer.queue_free()
|
||||
)
|
||||
timer.start()
|
||||
|
||||
return true
|
||||
|
||||
func setup_persistent_effect(game_state):
|
||||
# Connect to the turn change signal if not already connected
|
||||
if !game_state.is_connected("turn_changed", on_turn_changed):
|
||||
game_state.connect("turn_changed", on_turn_changed)
|
||||
|
||||
func on_turn_changed():
|
||||
print("TURN CHANGED ==================", stored_game_state.isPlayerTurn())
|
||||
if stored_game_state.stateMachine.disabled:
|
||||
return
|
||||
|
||||
if stored_game_state.isPlayerTurn() and attached_piece and remaining_turns > 0:
|
||||
var piece = attached_piece
|
||||
var flow = stored_board_flow
|
||||
var state = stored_game_state
|
||||
|
||||
# Now try apply_effect with these values
|
||||
valid = true
|
||||
apply_effect(piece, flow, state)
|
||||
|
||||
func remove_effect():
|
||||
if attached_piece:
|
||||
var game_state = stored_game_state
|
||||
if game_state.is_connected("turn_changed", on_turn_changed):
|
||||
game_state.disconnect("turn_changed", on_turn_changed)
|
||||
super.remove_effect()
|
||||
1
Systems/Cards/Halitosis.gd.uid
Normal file
1
Systems/Cards/Halitosis.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b8f17yg2854vs
|
||||
1
Systems/Cards/Hopscotch.gd.uid
Normal file
1
Systems/Cards/Hopscotch.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://5w8amumsdpto
|
||||
28
Systems/Cards/HorseCostume.gd
Normal file
28
Systems/Cards/HorseCostume.gd
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
class_name HorseCostumeCard extends Card
|
||||
|
||||
func _init():
|
||||
super._init()
|
||||
# id = Utils.generate_guid()
|
||||
cardName = "Horse Costume"
|
||||
rank = Rank.RANK_2
|
||||
effectType = EffectType.MOVEMENT_MODIFIER
|
||||
description = "Attached Unit can move and capture in any direction"
|
||||
duration = 3
|
||||
unitWhitelist = ["Pawn", "Bishop", "Queen", "Rook", "King"]
|
||||
is_default = false
|
||||
# current_movement_string = "mWmFcfF"
|
||||
|
||||
|
||||
func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
||||
if !super.apply_effect(target_piece, board_flow, game_state):
|
||||
return false
|
||||
|
||||
attached_piece = target_piece
|
||||
attached_piece.current_movement_string = "N"
|
||||
return true
|
||||
|
||||
|
||||
func reset():
|
||||
super.reset()
|
||||
remaining_turns = duration
|
||||
attached_piece.reset_current_movement_string()
|
||||
1
Systems/Cards/HorseCostume.gd.uid
Normal file
1
Systems/Cards/HorseCostume.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://nw1oesh7pr48
|
||||
28
Systems/Cards/KingsSquire.gd
Normal file
28
Systems/Cards/KingsSquire.gd
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
class_name KingsSquireCard extends Card
|
||||
|
||||
func _init():
|
||||
super._init()
|
||||
# id = Utils.generate_guid()
|
||||
cardName = "Kings' Squire"
|
||||
rank = Rank.RANK_2
|
||||
effectType = EffectType.MOVEMENT_MODIFIER
|
||||
description = "Attached Pawn can move and capture in any direction"
|
||||
duration = 3
|
||||
unitWhitelist = ["Pawn"]
|
||||
is_default = false
|
||||
# current_movement_string = "mWmFcfF"
|
||||
|
||||
|
||||
func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
||||
if !super.apply_effect(target_piece, board_flow, game_state):
|
||||
return false
|
||||
|
||||
attached_piece = target_piece
|
||||
attached_piece.current_movement_string = "WF"
|
||||
return true
|
||||
|
||||
|
||||
func reset():
|
||||
super.reset()
|
||||
remaining_turns = duration
|
||||
attached_piece.reset_current_movement_string()
|
||||
1
Systems/Cards/KingsSquire.gd.uid
Normal file
1
Systems/Cards/KingsSquire.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ms7wi6dgoqnr
|
||||
28
Systems/Cards/QueensSquire.gd
Normal file
28
Systems/Cards/QueensSquire.gd
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
class_name QueensSquireCard extends Card
|
||||
|
||||
func _init():
|
||||
super._init()
|
||||
# id = Utils.generate_guid()
|
||||
cardName = "Queens' Squire"
|
||||
rank = Rank.RANK_3
|
||||
effectType = EffectType.MOVEMENT_MODIFIER
|
||||
description = "Attached Pawn can move in any direction, can only capture diagonally forward"
|
||||
duration = 4
|
||||
unitWhitelist = ["Pawn"]
|
||||
is_default = true
|
||||
# current_movement_string = "mWmFcfF"
|
||||
|
||||
|
||||
func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
||||
if !super.apply_effect(target_piece, board_flow, game_state):
|
||||
return false
|
||||
|
||||
attached_piece = target_piece
|
||||
attached_piece.current_movement_string = "mWmFcfF"
|
||||
return true
|
||||
|
||||
|
||||
func reset():
|
||||
super.reset()
|
||||
remaining_turns = duration
|
||||
attached_piece.reset_current_movement_string()
|
||||
1
Systems/Cards/QueensSquire.gd.uid
Normal file
1
Systems/Cards/QueensSquire.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://8vpac2cx4e0u
|
||||
|
|
@ -8,11 +8,15 @@ func _init():
|
|||
cardName = "Supernova"
|
||||
rank = Rank.RANK_0
|
||||
effectType = EffectType.PIECE_EFFECT
|
||||
duration = 5
|
||||
duration = 1
|
||||
description = "All enemy units within 4 radius are captured"
|
||||
unitWhitelist = ["King"]
|
||||
remaining_turns = duration
|
||||
|
||||
func reset():
|
||||
remaining_turns = duration
|
||||
valid = false
|
||||
|
||||
func apply_effect(target_piece = null, board_flow = null, game_state = null):
|
||||
attached_piece = target_piece
|
||||
stored_board_flow = board_flow
|
||||
|
|
|
|||
1
Systems/Cards/Supernova.gd.uid
Normal file
1
Systems/Cards/Supernova.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cvldhayf5ket0
|
||||
109
Systems/CompactCardDisplay.gd
Normal file
109
Systems/CompactCardDisplay.gd
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
extends Panel
|
||||
class_name CompactCardDisplay
|
||||
|
||||
# Node references (will be created programmatically)
|
||||
var card_name_label: Label
|
||||
var rank_label: Label
|
||||
var description_label: Label
|
||||
var effect_type_label: Label
|
||||
|
||||
# Card rank colors (same as other card components)
|
||||
var rank_colors = {
|
||||
Card.Rank.RANK_0: Color(0.9, 0.1, 0.1, 1.0), # Red
|
||||
Card.Rank.RANK_1: Color(0.9, 0.6, 0.1, 1.0), # Orange
|
||||
Card.Rank.RANK_2: Color(0.1, 0.7, 0.1, 1.0), # Green
|
||||
Card.Rank.RANK_3: Color(0.1, 0.7, 0.9, 1.0) # Blue
|
||||
}
|
||||
|
||||
# Effect type descriptions
|
||||
var effect_type_names = {
|
||||
Card.EffectType.MOVEMENT_MODIFIER: "Movement Modifier",
|
||||
Card.EffectType.BOARD_EFFECT: "Board Effect",
|
||||
Card.EffectType.PIECE_EFFECT: "Piece Effect",
|
||||
Card.EffectType.SPECIAL_ACTION: "Special Action"
|
||||
}
|
||||
|
||||
var current_card = null
|
||||
|
||||
func _ready():
|
||||
# Create a stylish background
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.15, 0.15, 0.15, 1.0)
|
||||
style.corner_radius_top_left = 5
|
||||
style.corner_radius_top_right = 5
|
||||
style.corner_radius_bottom_left = 5
|
||||
style.corner_radius_bottom_right = 5
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.border_color = Color(0.3, 0.3, 0.3)
|
||||
add_theme_stylebox_override("panel", style)
|
||||
|
||||
# Create a content container
|
||||
var content = VBoxContainer.new()
|
||||
content.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
content.add_theme_constant_override("separation", 2) # Tight spacing
|
||||
add_child(content)
|
||||
|
||||
card_name_label = Label.new()
|
||||
card_name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
card_name_label.add_theme_font_size_override("font_size", 12)
|
||||
card_name_label.clip_text = true # Prevent overflow
|
||||
content.add_child(card_name_label)
|
||||
|
||||
rank_label = Label.new()
|
||||
rank_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
rank_label.add_theme_font_size_override("font_size", 10)
|
||||
content.add_child(rank_label)
|
||||
|
||||
# Create description label (limited to 2-3 lines)
|
||||
description_label = Label.new()
|
||||
description_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
description_label.add_theme_font_size_override("font_size", 9)
|
||||
description_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
description_label.max_lines_visible = 3
|
||||
description_label.size_flags_vertical = SIZE_EXPAND_FILL
|
||||
content.add_child(description_label)
|
||||
|
||||
effect_type_label = Label.new()
|
||||
effect_type_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
effect_type_label.add_theme_font_size_override("font_size", 8)
|
||||
effect_type_label.clip_text = true
|
||||
content.add_child(effect_type_label)
|
||||
|
||||
|
||||
func set_selected(is_selected: bool) -> void:
|
||||
var style = get_theme_stylebox("panel").duplicate()
|
||||
|
||||
if is_selected:
|
||||
style.bg_color = Utils.GREEN_CELL # Selected color
|
||||
else:
|
||||
style.bg_color = Utils.DARK_CELL
|
||||
|
||||
add_theme_stylebox_override("panel", style)
|
||||
|
||||
func set_card(card: Card):
|
||||
if !card:
|
||||
return
|
||||
|
||||
if !is_inside_tree():
|
||||
await ready
|
||||
|
||||
current_card = card
|
||||
|
||||
card_name_label.text = card.cardName
|
||||
|
||||
rank_label.text = "Rank " + str(card.rank)
|
||||
if card.rank in rank_colors:
|
||||
rank_label.add_theme_color_override("font_color", rank_colors[card.rank])
|
||||
var style = get_theme_stylebox("panel").duplicate()
|
||||
style.border_color = rank_colors[card.rank]
|
||||
add_theme_stylebox_override("panel", style)
|
||||
|
||||
description_label.text = card.description
|
||||
|
||||
if card.effectType in effect_type_names:
|
||||
effect_type_label.text = "Effect: " + effect_type_names[card.effectType]
|
||||
else:
|
||||
effect_type_label.text = "Effect: Unknown"
|
||||
1
Systems/CompactCardDisplay.gd.uid
Normal file
1
Systems/CompactCardDisplay.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dm6uv77n0roen
|
||||
|
|
@ -3,40 +3,70 @@ class_name DeckManager
|
|||
|
||||
var deck: Array = []
|
||||
var hand: Array = []
|
||||
var bank: Array = []
|
||||
var discard: Array = []
|
||||
var attached_cards: Dictionary = {} # piece_id: card
|
||||
var attached_effects: Dictionary = {} # piece_id: [card]
|
||||
var hand_size: int = 5
|
||||
|
||||
# Card costs for shop
|
||||
const CARD_BASE_COSTS = {
|
||||
Card.Rank.RANK_0: 100,
|
||||
Card.Rank.RANK_1: 75,
|
||||
Card.Rank.RANK_2: 50,
|
||||
Card.Rank.RANK_3: 25
|
||||
}
|
||||
|
||||
var preloaded_hand_cards: Array = [] # array of string ids
|
||||
func _init():
|
||||
initializeStartingDeck()
|
||||
print("************************DECK INIT*****************")
|
||||
|
||||
func initializeStartingDeck():
|
||||
|
||||
|
||||
|
||||
func preload_cards(card_array = []):
|
||||
var cnt = 0
|
||||
preloaded_hand_cards.clear()
|
||||
for card in card_array:
|
||||
preloaded_hand_cards.append(card.id)
|
||||
|
||||
|
||||
func set_hand_size(size: int):
|
||||
hand_size = size
|
||||
|
||||
func initializeStartingDeck(starting_cards: Array = []):
|
||||
deck.clear();
|
||||
# for i in range(2):
|
||||
# deck.append(DoubleTimeCard.new())
|
||||
deck.append(FieryCapeCard.new())
|
||||
deck.append(FieryTrailCard.new())
|
||||
deck.append(ExplosiveBootsCard.new())
|
||||
deck.append(DoubleTimeCard.new())
|
||||
deck.append(DrunkDrivingCard.new())
|
||||
deck.append(HopscotchCard.new())
|
||||
deck.append(SupernovaCard.new())
|
||||
shuffleDeck()
|
||||
drawStartingHand()
|
||||
# addCardToDeck(ExplosiveBootsCard.new())
|
||||
# addCardToDeck(DoubleTimeCard.new())
|
||||
# # addCardToDeck(DrunkDrivingCard.new())
|
||||
# addCardToDeck(SupernovaCard.new())
|
||||
print("Initializing deck with " + str(starting_cards.size()) + " cards")
|
||||
for template_card in starting_cards:
|
||||
# Create a new instance of the same card type
|
||||
var new_card = create_new_card_instance(template_card)
|
||||
if new_card:
|
||||
addCardToDeck(new_card)
|
||||
else:
|
||||
print("Failed to create card: " + template_card.cardName)
|
||||
|
||||
|
||||
func initializeStartingBank():
|
||||
bank.append(HopscotchCard.new())
|
||||
bank.append(FieryCapeCard.new())
|
||||
bank.append(FieryTrailCard.new())
|
||||
bank.append(ExplosiveBootsCard.new())
|
||||
bank.append(DoubleTimeCard.new())
|
||||
bank.append(DrunkDrivingCard.new())
|
||||
bank.append(SupernovaCard.new())
|
||||
|
||||
for i in range(3):
|
||||
bank.append(HopscotchCard.new())
|
||||
bank.append(FieryCapeCard.new())
|
||||
|
||||
|
||||
func shuffleDeck():
|
||||
deck.shuffle()
|
||||
|
||||
func drawStartingHand():
|
||||
func drawStartingHand(skip_preload: bool = false):
|
||||
if !skip_preload:
|
||||
for id in preloaded_hand_cards:
|
||||
var matchCard = func matchCardId(card):
|
||||
return "id" in card and card.id == id
|
||||
var cardIndex = deck.find_custom(matchCard.bind())
|
||||
if cardIndex != -1:
|
||||
hand.append(deck.pop_at(cardIndex))
|
||||
|
||||
for i in range(min(hand_size, deck.size())):
|
||||
drawCard()
|
||||
|
||||
|
|
@ -48,13 +78,15 @@ func drawCard():
|
|||
|
||||
if !deck.is_empty() && hand.size() < hand_size:
|
||||
hand.append(deck.pop_back())
|
||||
|
||||
emit_signal("hand_updated", hand)
|
||||
|
||||
|
||||
signal hand_updated
|
||||
|
||||
|
||||
func addCardToDeck(card: Card):
|
||||
deck.insert(0, card)
|
||||
|
||||
|
||||
func playCard(card: Card, target_piece: Pawn, board_flow = null, game_state = null, avoidHand = false):
|
||||
if !avoidHand and !hand.has(card):
|
||||
|
|
@ -68,7 +100,7 @@ func playCard(card: Card, target_piece: Pawn, board_flow = null, game_state = nu
|
|||
attached_cards[target_piece.get_instance_id()] = card
|
||||
hand.erase(card)
|
||||
emit_signal("hand_updated", hand)
|
||||
|
||||
|
||||
|
||||
return true
|
||||
# print("Failed Play Card 2")
|
||||
|
|
@ -95,6 +127,12 @@ func playEffect(card: Card, target_piece: Pawn, board_flow = null, game_state =
|
|||
# print("Failed Play Card 2")
|
||||
return false
|
||||
|
||||
func cleanup():
|
||||
if hand.size() > 0:
|
||||
for card in hand:
|
||||
deck.push_back(card)
|
||||
hand.clear()
|
||||
|
||||
func updateEffectDurations():
|
||||
var expired_entries = [] # Store [piece_id, card_id] pairs to remove
|
||||
|
||||
|
|
@ -134,7 +172,7 @@ func updateCardDurations():
|
|||
if card.remaining_turns <= 0:
|
||||
expired_cards.append(piece_id)
|
||||
|
||||
|
||||
card.reset()
|
||||
match card.rank:
|
||||
Card.Rank.RANK_0:
|
||||
print("Rank 3 Burned permanently")
|
||||
|
|
@ -147,7 +185,7 @@ func updateCardDurations():
|
|||
discard.append(card)
|
||||
pass
|
||||
Card.Rank.RANK_3:
|
||||
deck.append(card)
|
||||
addCardToDeck(card)
|
||||
print("Rank 3 add to Deck")
|
||||
pass
|
||||
else:
|
||||
|
|
@ -175,10 +213,33 @@ func getShopCards(num_cards: int = 3) -> Array:
|
|||
return shop_cards
|
||||
|
||||
func calculateCardCost(card: Card) -> int:
|
||||
var base_cost = CARD_BASE_COSTS[card.rank]
|
||||
var base_cost = Utils.CardPrices[card.rank]
|
||||
# Add modifiers based on card effects
|
||||
return base_cost
|
||||
|
||||
func upgradeCard(card: Card) -> bool:
|
||||
# Implement card upgrading logic
|
||||
return false
|
||||
|
||||
|
||||
func create_new_card_instance(template_card: Card) -> Card:
|
||||
var new_card = null
|
||||
|
||||
var script = template_card.get_script()
|
||||
|
||||
if script:
|
||||
new_card = script.new()
|
||||
else:
|
||||
print("Warning: Could not get script from card: " + template_card.cardName)
|
||||
|
||||
var class_list = ProjectSettings.get_global_class_list()
|
||||
var card_class_name = template_card.get_class()
|
||||
|
||||
for class_info in class_list:
|
||||
if class_info["class"] == card_class_name:
|
||||
var card_script = load(class_info["path"])
|
||||
if card_script:
|
||||
new_card = card_script.new()
|
||||
break
|
||||
|
||||
return new_card
|
||||
|
|
|
|||
1
Systems/DeckManager.gd.uid
Normal file
1
Systems/DeckManager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bdwsolgdmo51y
|
||||
286
Systems/FairyStockfish/FairyStockfishVariantGenerator.gd
Normal file
286
Systems/FairyStockfish/FairyStockfishVariantGenerator.gd
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
extends Node
|
||||
class_name FairyStockfishVariantGenerator
|
||||
|
||||
const STANDARD_PIECE_CHARS = "PNBRQK"
|
||||
const files = "abcdefghijkl"
|
||||
const VARIANT_CHARS = "ACDEFGHIJLMOSTVWXYZ"
|
||||
|
||||
func generate_variant_string(chess_game: ChessGame) -> String:
|
||||
var width = chess_game.boardXSize
|
||||
var height = chess_game.boardYSize
|
||||
var prefix = "chessbuilder" + str(width) + "x" + str(height)
|
||||
|
||||
var piece_data = gather_piece_data(chess_game)
|
||||
var piece_definitions = piece_data.definitions
|
||||
var piece_mappings = piece_data.mappings
|
||||
var used_variant_chars = piece_data.used_chars
|
||||
|
||||
var variant_string = "[" + prefix + ":chess]\n"
|
||||
|
||||
# Build the pieceToCharTable with all used variant characters
|
||||
var char_table = build_piece_char_table(used_variant_chars)
|
||||
variant_string += "pieceToCharTable = " + char_table + "\n"
|
||||
|
||||
variant_string += "maxRank = " + str(height) + "\n"
|
||||
variant_string += "maxFile = " + files[width - 1] + "\n"
|
||||
|
||||
var current_fen = chess_game.getCurrentFen()
|
||||
variant_string += "startFen = " + current_fen + "\n\n"
|
||||
|
||||
# Add wall types
|
||||
variant_string += "walltype = * # Duck wall\n"
|
||||
variant_string += "walltype = @ # Stone wall\n\n"
|
||||
|
||||
# Add walling rules
|
||||
variant_string += "wallingRule = static\n"
|
||||
variant_string += "wallOrMove = false\n"
|
||||
variant_string += "mobilityRegion = *:@\n"
|
||||
variant_string += "prohibitedMove = *@\n\n"
|
||||
|
||||
# Add the piece definitions
|
||||
variant_string += "# Custom piece movement definitions\n"
|
||||
for piece_key in piece_definitions:
|
||||
variant_string += "piece " + piece_key + " = " + piece_definitions[piece_key] + "\n"
|
||||
|
||||
# Add pieceDrops if needed for capturing
|
||||
variant_string += "\n# Allow capturing with custom pieces\n"
|
||||
variant_string += "pieceDrops = true\n"
|
||||
|
||||
return variant_string
|
||||
|
||||
# Build a complete pieceToCharTable that includes all variant characters
|
||||
func build_piece_char_table(used_variant_chars: Array) -> String:
|
||||
var char_table = STANDARD_PIECE_CHARS
|
||||
|
||||
for variant_char in used_variant_chars:
|
||||
char_table += variant_char.to_upper()
|
||||
|
||||
char_table += "..*@..........."
|
||||
|
||||
# Add lowercase versions
|
||||
char_table += STANDARD_PIECE_CHARS.to_lower()
|
||||
|
||||
for variant_char in used_variant_chars:
|
||||
char_table += variant_char.to_lower()
|
||||
|
||||
char_table += "..*@..........."
|
||||
|
||||
return char_table
|
||||
|
||||
# Gather all piece data from the current game state
|
||||
func gather_piece_data(chess_game: ChessGame) -> Dictionary:
|
||||
# Dictionary to track unique movement patterns
|
||||
# Key format: "piece_char:color:movement_string"
|
||||
var unique_movements = {}
|
||||
|
||||
var modified_piece_types = {}
|
||||
|
||||
# First pass: collect all unique movement patterns
|
||||
for y in range(chess_game.boardYSize):
|
||||
for x in range(chess_game.boardXSize):
|
||||
var container = chess_game.boardContainer.get_node(str(x) + "-" + str(y)) as PieceContainer
|
||||
if container && container.has_piece():
|
||||
var piece = container.get_piece() as Pawn
|
||||
|
||||
if piece.current_movement_string != piece.original_movement_string:
|
||||
var piece_char = get_piece_char(piece)
|
||||
var color_key = "white" if piece.Item_Color == 0 else "black"
|
||||
var piece_key = piece_char + ":" + color_key
|
||||
|
||||
if !modified_piece_types.has(piece_key):
|
||||
modified_piece_types[piece_key] = true
|
||||
|
||||
var movement_key = piece_key + ":" + piece.current_movement_string
|
||||
|
||||
if !unique_movements.has(movement_key):
|
||||
unique_movements[movement_key] = {
|
||||
"piece_char": piece_char,
|
||||
"color": color_key,
|
||||
"betza_notation": piece.current_movement_string,
|
||||
"fairy_notation": convert_betza_to_fairy_stockfish(piece.current_movement_string),
|
||||
"positions": []
|
||||
}
|
||||
|
||||
unique_movements[movement_key].positions.append(str(x) + "-" + str(y))
|
||||
|
||||
var variant_char_index = 0
|
||||
var piece_definitions = {}
|
||||
var piece_mappings = {}
|
||||
var used_variant_chars = []
|
||||
|
||||
for movement_key in unique_movements:
|
||||
var movement_data = unique_movements[movement_key]
|
||||
var piece_char = movement_data.piece_char
|
||||
var color = movement_data.color
|
||||
var fs_notation = movement_data.fairy_notation
|
||||
|
||||
var variant_char
|
||||
if variant_char_index < VARIANT_CHARS.length():
|
||||
variant_char = VARIANT_CHARS[variant_char_index]
|
||||
used_variant_chars.append(variant_char)
|
||||
variant_char_index += 1
|
||||
else:
|
||||
# If we run out of variant chars, we'll need to add more or implement another solution
|
||||
print("Warning: Ran out of variant characters for piece definitions")
|
||||
break
|
||||
|
||||
# Store the piece definition
|
||||
var definition_key = variant_char.to_upper() if color == "white" else variant_char.to_lower()
|
||||
piece_definitions[definition_key] = fs_notation
|
||||
|
||||
for pos in movement_data.positions:
|
||||
piece_mappings[pos] = definition_key
|
||||
|
||||
return {
|
||||
"definitions": piece_definitions,
|
||||
"mappings": piece_mappings,
|
||||
"used_chars": used_variant_chars
|
||||
}
|
||||
|
||||
func get_piece_char(piece: Pawn) -> String:
|
||||
match piece.name:
|
||||
"Pawn": return "P"
|
||||
"Knight": return "N"
|
||||
"Bishop": return "B"
|
||||
"Rook": return "R"
|
||||
"Queen": return "Q"
|
||||
"King": return "K"
|
||||
_: return "P" # Default to pawn
|
||||
|
||||
func convert_betza_to_fairy_stockfish(betza_notation: String) -> String:
|
||||
var atoms = parse_betza_notation(betza_notation)
|
||||
var fairy_stockfish_moves = []
|
||||
|
||||
for atom in atoms:
|
||||
var move_type = atom.type
|
||||
var modifiers = atom.modifiers
|
||||
var range_limit = atom.range
|
||||
|
||||
var move_only = "m" in modifiers
|
||||
var capture_only = "c" in modifiers
|
||||
|
||||
# Process direction modifiers
|
||||
var direction_modifiers = []
|
||||
for modifier in modifiers:
|
||||
if modifier.length() > 1 || ["f", "b", "l", "r"].has(modifier):
|
||||
for char in modifier:
|
||||
if ["f", "b", "l", "r"].has(char):
|
||||
direction_modifiers.append(char)
|
||||
|
||||
var fs_move = ""
|
||||
|
||||
match move_type:
|
||||
"W": fs_move += "W" # Wazir (orthogonal step)
|
||||
"F": fs_move += "F" # Ferz (diagonal step)
|
||||
"R": fs_move += "R" # Rook (orthogonal slider)
|
||||
"B": fs_move += "B" # Bishop (diagonal slider)
|
||||
"N": fs_move += "N" # Knight
|
||||
"K": fs_move += "K" # King (one step in any direction)
|
||||
"Q": fs_move += "Q" # Queen (combination of R and B)
|
||||
|
||||
if range_limit > 0:
|
||||
fs_move += str(range_limit)
|
||||
|
||||
if direction_modifiers.size() > 0:
|
||||
fs_move += "/"
|
||||
for dir in direction_modifiers:
|
||||
match dir:
|
||||
"f": fs_move += "f"
|
||||
"b": fs_move += "b"
|
||||
"l": fs_move += "l"
|
||||
"r": fs_move += "r"
|
||||
|
||||
if move_only:
|
||||
fs_move += "m"
|
||||
elif capture_only:
|
||||
fs_move += "c"
|
||||
|
||||
fairy_stockfish_moves.append(fs_move)
|
||||
var ret_str = ""
|
||||
for moves in fairy_stockfish_moves:
|
||||
ret_str += str(moves) + " "
|
||||
|
||||
return ret_str
|
||||
|
||||
# Parse Betza notation into atoms
|
||||
func parse_betza_notation(notation: String) -> Array:
|
||||
var atoms = []
|
||||
var i = 0
|
||||
|
||||
while i < notation.length():
|
||||
var atom = {
|
||||
"type": "",
|
||||
"modifiers": [],
|
||||
"range": -1 # -1 means unlimited
|
||||
}
|
||||
|
||||
while i < notation.length() && ["m", "c"].has(notation[i]):
|
||||
atom.modifiers.append(notation[i])
|
||||
i += 1
|
||||
|
||||
var directions = ""
|
||||
while i < notation.length() && ["f", "b", "l", "r"].has(notation[i]):
|
||||
directions += notation[i]
|
||||
i += 1
|
||||
|
||||
if directions:
|
||||
atom.modifiers.append(directions)
|
||||
|
||||
# Get the atom type
|
||||
if i < notation.length():
|
||||
atom.type = notation[i]
|
||||
i += 1
|
||||
|
||||
# Check for range specification
|
||||
var range_str = ""
|
||||
while i < notation.length() && notation[i].is_valid_int():
|
||||
range_str += notation[i]
|
||||
i += 1
|
||||
|
||||
if range_str:
|
||||
atom.range = int(range_str)
|
||||
|
||||
atoms.append(atom)
|
||||
|
||||
return atoms
|
||||
|
||||
# Generate a modified FEN string with custom piece characters
|
||||
func generate_modified_fen(chess_game: ChessGame, piece_mappings: Dictionary) -> String:
|
||||
var fen_parts = chess_game.getCurrentFen().split(" ")
|
||||
var board_part = fen_parts[0]
|
||||
var rows = board_part.split("/")
|
||||
var new_rows = []
|
||||
|
||||
for y in range(rows.size()):
|
||||
var new_row = ""
|
||||
var x = 0
|
||||
|
||||
for c in rows[y]:
|
||||
if c.is_valid_int():
|
||||
# Handle empty squares
|
||||
new_row += c
|
||||
x += int(c)
|
||||
else:
|
||||
var pos = str(x) + "-" + str(y)
|
||||
if piece_mappings.has(pos):
|
||||
new_row += piece_mappings[pos]
|
||||
else:
|
||||
new_row += c
|
||||
x += 1
|
||||
|
||||
new_rows.append(new_row)
|
||||
|
||||
fen_parts[0] = new_rows.join("/")
|
||||
return fen_parts.join(" ")
|
||||
|
||||
# Save the variant definition to a file
|
||||
func save_variant_to_file(variant_string: String) -> bool:
|
||||
print("VARIANT ", variant_string)
|
||||
ServerManager.write_variant(variant_string)
|
||||
return true
|
||||
|
||||
# Helper function to get a complete variant definition for the current game state
|
||||
func generate_and_save_variant(chess_game: ChessGame) -> String:
|
||||
var variant_string = generate_variant_string(chess_game)
|
||||
save_variant_to_file(variant_string)
|
||||
return variant_string
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://boryt6vnd0icx
|
||||
279
Systems/FairyStockfish/ServerManager.gd
Normal file
279
Systems/FairyStockfish/ServerManager.gd
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
# ServerManager.gd
|
||||
extends Node
|
||||
|
||||
var server_path: String = ""
|
||||
var log_dir: String = ""
|
||||
var variant_dir : String = ""
|
||||
var log_string: String = ""
|
||||
var running := false
|
||||
var server_process_id: int = -50
|
||||
var request_timer: Timer
|
||||
var server_pinger: Timer
|
||||
var ping_http: HTTPRequest
|
||||
|
||||
var server_url = "http://localhost:27531"
|
||||
|
||||
|
||||
func write_variant(variant_string: String):
|
||||
|
||||
var file = FileAccess.open(variant_dir, FileAccess.WRITE_READ)
|
||||
var error = FileAccess.get_open_error()
|
||||
|
||||
if error != OK:
|
||||
# print("Failed to open log file. Error code: ", error)
|
||||
return
|
||||
|
||||
if file == null:
|
||||
# print("File handle is null")
|
||||
return
|
||||
|
||||
file.store_string(variant_string)
|
||||
|
||||
var write_error = FileAccess.get_open_error()
|
||||
if write_error != OK:
|
||||
print("Failed to write to log file. Error code: ", write_error)
|
||||
|
||||
file.close()
|
||||
|
||||
|
||||
func write_log(message: String):
|
||||
# First check if path is valid
|
||||
# print("Attempting to write to: ", log_dir)
|
||||
|
||||
var file = FileAccess.open(log_dir, FileAccess.WRITE_READ)
|
||||
var error = FileAccess.get_open_error()
|
||||
|
||||
if error != OK:
|
||||
# print("Failed to open log file. Error code: ", error)
|
||||
return
|
||||
|
||||
if file == null:
|
||||
# print("File handle is null")
|
||||
return
|
||||
|
||||
var timestamp = Time.get_datetime_string_from_system()
|
||||
var log_message = "[" + timestamp + "] " + message + "\n"
|
||||
log_string += log_message
|
||||
print("Writing message: ", log_message)
|
||||
file.store_string(log_string)
|
||||
|
||||
var write_error = FileAccess.get_open_error()
|
||||
if write_error != OK:
|
||||
print("Failed to write to log file. Error code: ", write_error)
|
||||
|
||||
file.close()
|
||||
func _ready():
|
||||
server_path = extract_server_files() + "/ChessEngines/fairy-chess-server"
|
||||
setup_logging()
|
||||
setup_variant()
|
||||
check_server_files(server_path)
|
||||
start_server()
|
||||
get_tree().set_auto_accept_quit(false)
|
||||
setup_server_pinger()
|
||||
|
||||
func setup_server_pinger():
|
||||
server_pinger = Timer.new()
|
||||
add_child(server_pinger)
|
||||
server_pinger.wait_time = 60.0
|
||||
server_pinger.one_shot = false
|
||||
server_pinger.timeout.connect(self._on_ping_timer_timeout)
|
||||
ping_http = HTTPRequest.new()
|
||||
add_child(ping_http)
|
||||
ping_http.request_completed.connect(self._on_ping_completed)
|
||||
|
||||
server_pinger.start()
|
||||
|
||||
|
||||
func _on_ping_timer_timeout():
|
||||
write_log("Pinging server health endpoint...")
|
||||
var error = ping_http.request(server_url + "/health")
|
||||
if error != OK:
|
||||
write_log("Error sending ping request: " + str(error))
|
||||
|
||||
|
||||
func _on_ping_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
|
||||
if result == HTTPRequest.RESULT_SUCCESS:
|
||||
write_log("Server ping successful")
|
||||
else:
|
||||
write_log("Server ping failed - may need to restart server")
|
||||
|
||||
|
||||
func write_log_dir_contents(path: String):
|
||||
var dir = DirAccess.open(path)
|
||||
if dir:
|
||||
write_log("Directory contents of: " + path)
|
||||
dir.list_dir_begin()
|
||||
var file_name = dir.get_next()
|
||||
while file_name != "":
|
||||
write_log(" - " + file_name)
|
||||
file_name = dir.get_next()
|
||||
else:
|
||||
write_log("ERROR: Could not open directory: " + path)
|
||||
func check_server_files(path: String):
|
||||
write_log("Checking server files...")
|
||||
write_log(path)
|
||||
|
||||
var index_path = path.path_join("index.js")
|
||||
if FileAccess.file_exists(index_path):
|
||||
write_log("index.js exists")
|
||||
var file = FileAccess.open(index_path, FileAccess.READ)
|
||||
if file:
|
||||
write_log("index.js contents:")
|
||||
# write_log(file.get_as_text())
|
||||
else:
|
||||
write_log("ERROR: Could not read index.js")
|
||||
else:
|
||||
write_log("ERROR: index.js does not exist at path: " + index_path)
|
||||
|
||||
func get_globalDir() -> String:
|
||||
|
||||
if OS.get_name() == "Linux":
|
||||
return OS.get_environment("HOME").path_join(".local/share/ChessBuilder")
|
||||
elif OS.get_name() == "Windows":
|
||||
return OS.get_environment("APPDATA").path_join("Roaming/ChessBuilder")
|
||||
else: # macOS
|
||||
return OS.get_environment("HOME").path_join("Library/ChessBuilder")
|
||||
|
||||
func setup_logging():
|
||||
var l_dir = get_globalDir() + "/logs"
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
DirAccess.make_dir_recursive_absolute(l_dir)
|
||||
|
||||
log_dir = l_dir.path_join("godot-chess.log")
|
||||
write_log("ServerManager initialized")
|
||||
func setup_variant():
|
||||
var l_dir = get_globalDir() + "/variant"
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
DirAccess.make_dir_recursive_absolute(l_dir)
|
||||
|
||||
variant_dir = l_dir.path_join("custom_variant.ini")
|
||||
write_log("ServerManager Variants initialized")
|
||||
|
||||
func _exit_tree():
|
||||
stop_server()
|
||||
get_tree().quit()
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||
stop_server()
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func start_server() -> bool:
|
||||
if running:
|
||||
return true
|
||||
|
||||
write_log("Starting chess server... " + server_path)
|
||||
var http_request = HTTPRequest.new()
|
||||
add_child(http_request)
|
||||
http_request.request_completed.connect(self._on_init_request_completed)
|
||||
|
||||
request_timer = Timer.new()
|
||||
add_child(request_timer)
|
||||
request_timer.wait_time = 2.0 # 2 seconds
|
||||
request_timer.one_shot = true
|
||||
request_timer.timeout.connect(self._on_request_timeout)
|
||||
request_timer.start()
|
||||
http_request.request(server_url + "/health")
|
||||
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func _on_request_timeout():
|
||||
write_log("Request timed out, starting server manually")
|
||||
# Clean up timer
|
||||
request_timer.queue_free()
|
||||
request_timer = null
|
||||
write_log("HTTP Request failed, starting server process")
|
||||
write_log(server_path + "/index.js")
|
||||
server_process_id = OS.create_process("node", [server_path + "/index.js"])
|
||||
write_log("SERVER PATH " + server_path)
|
||||
if server_process_id <= 0:
|
||||
printerr("Failed to start server")
|
||||
write_log("ERROR: Failed to start server, process ID: " + str(server_process_id))
|
||||
|
||||
return false
|
||||
|
||||
running = true
|
||||
write_log("Chess server started with PID: " + str(server_process_id))
|
||||
return
|
||||
|
||||
func _on_init_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
|
||||
write_log("*****************_on_init_request_completed************")
|
||||
if request_timer:
|
||||
request_timer.stop()
|
||||
request_timer.queue_free()
|
||||
request_timer = null
|
||||
var json = JSON.new()
|
||||
json.parse(body.get_string_from_utf8())
|
||||
var response = json.get_data()
|
||||
write_log("Init request completed")
|
||||
write_log("Result: " + str(response))
|
||||
if response == null:
|
||||
write_log("HTTP Request failed, starting server process")
|
||||
write_log(server_path + "/index.js")
|
||||
server_process_id = OS.create_process("node", [server_path + "/index.js"])
|
||||
write_log("SERVER PATH " + server_path)
|
||||
if server_process_id <= 0:
|
||||
printerr("Failed to start server")
|
||||
write_log("ERROR: Failed to start server, process ID: " + str(server_process_id))
|
||||
|
||||
return false
|
||||
|
||||
running = true
|
||||
write_log("Chess server started with PID: " + str(server_process_id))
|
||||
return
|
||||
running = true
|
||||
if response and response.status != "ok":
|
||||
print("Server error : ", response_code, json.parse(body.get_string_from_utf8()),)
|
||||
return
|
||||
|
||||
static func copy_directory_recursively(p_from: String, p_to: String) -> void:
|
||||
# Create target directory if it doesn't exist
|
||||
if not DirAccess.dir_exists_absolute(p_to):
|
||||
DirAccess.make_dir_recursive_absolute(p_to)
|
||||
|
||||
# Open source directory
|
||||
var dir = DirAccess.open(p_from)
|
||||
if dir:
|
||||
# Begin listing directory contents
|
||||
dir.list_dir_begin()
|
||||
var file_name = dir.get_next()
|
||||
|
||||
# Iterate through directory contents
|
||||
while file_name != "":
|
||||
# write_log(file_name + " isDIr? " + str(dir.current_is_dir()))
|
||||
if dir.current_is_dir():
|
||||
# Recursively copy subdirectories
|
||||
copy_directory_recursively(p_from.path_join(file_name), p_to.path_join(file_name))
|
||||
else:
|
||||
# Copy files
|
||||
dir.copy(p_from.path_join(file_name), p_to.path_join(file_name))
|
||||
file_name = dir.get_next()
|
||||
|
||||
func extract_server_files() -> String:
|
||||
write_log("Extracting server files from PCK...")
|
||||
var dir = get_globalDir() + "/Assets";
|
||||
copy_directory_recursively("res://Assets/", dir)
|
||||
return dir
|
||||
|
||||
|
||||
func stop_server():
|
||||
if not running:
|
||||
return
|
||||
|
||||
write_log("Stopping chess server...")
|
||||
# Send a stop request to the server
|
||||
var headers = ["Content-Type: application/json"]
|
||||
var http = HTTPClient.new()
|
||||
var data = JSON.stringify({"command": "shutdown"})
|
||||
http.request(HTTPClient.METHOD_POST, "/shutdown", headers, data)
|
||||
|
||||
running = false
|
||||
write_log("Chess server stopped")
|
||||
|
||||
func is_server_running() -> bool:
|
||||
return running
|
||||
1
Systems/FairyStockfish/ServerManager.gd.uid
Normal file
1
Systems/FairyStockfish/ServerManager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dae6g5472sh26
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
# Stockfish.gd
|
||||
extends Node
|
||||
|
||||
# References to game objects
|
||||
var board: Array
|
||||
var game: ChessGame
|
||||
|
||||
# Engine state
|
||||
var engine_path: String = ""
|
||||
var mutex: Mutex
|
||||
var running := false
|
||||
|
||||
# Game state
|
||||
var moves: Array = []
|
||||
var move_time: int = 1000 # in ms
|
||||
var generated_move: Dictionary = {} # Stores the last generated move
|
||||
|
||||
# Piece type mapping
|
||||
var symbol_from_piece_type := {
|
||||
"PAWN": "p", "KNIGHT": "n", "BISHOP": "b",
|
||||
"ROOK": "r", "QUEEN": "q", "KING": "k"
|
||||
}
|
||||
|
||||
var piece_type_from_symbol := {
|
||||
"p": "PAWN", "n": "KNIGHT", "b": "BISHOP",
|
||||
"r": "ROOK", "q": "QUEEN", "k": "KING"
|
||||
}
|
||||
|
||||
func _init(boardRef: Array):
|
||||
board = boardRef
|
||||
mutex = Mutex.new()
|
||||
|
||||
func _ready():
|
||||
game = get_parent() as ChessGame
|
||||
|
||||
func _exit_tree():
|
||||
disconnect_engine()
|
||||
|
||||
func connect_to_engine(path: String) -> bool:
|
||||
if running:
|
||||
return false
|
||||
|
||||
engine_path = path
|
||||
|
||||
# Test if we can execute stockfish
|
||||
var output = []
|
||||
var exit_code = OS.execute(engine_path, ["uci"], output, true)
|
||||
print("Exit code: ", exit_code)
|
||||
print("Output: ", output)
|
||||
if exit_code != OK:
|
||||
printerr("Failed to start Stockfish engine: ", exit_code)
|
||||
return false
|
||||
|
||||
running = true
|
||||
print("Connected to engine: ", engine_path)
|
||||
|
||||
# Initialize with current game state
|
||||
load_fen(game.getCurrentFen())
|
||||
return true
|
||||
|
||||
func disconnect_engine():
|
||||
if running:
|
||||
mutex.lock()
|
||||
var output = []
|
||||
OS.execute(engine_path, ["quit"], output, true)
|
||||
mutex.unlock()
|
||||
running = false
|
||||
engine_path = ""
|
||||
print("Disconnected from engine")
|
||||
|
||||
func limit_strength_to(elo_value: int):
|
||||
mutex.lock()
|
||||
if elo_value != -1: # Using -1 instead of int.MaxValue
|
||||
_send_command("setoption name UCI_LimitStrength value true")
|
||||
_send_command("setoption name UCI_Elo value " + str(elo_value))
|
||||
else:
|
||||
_send_command("setoption name UCI_LimitStrength value false")
|
||||
mutex.unlock()
|
||||
|
||||
func stop_calculating():
|
||||
mutex.lock()
|
||||
_send_command("stop")
|
||||
mutex.unlock()
|
||||
|
||||
func load_fen(fen: String):
|
||||
moves.clear()
|
||||
update_position(fen)
|
||||
|
||||
func update_position(fen: String):
|
||||
mutex.lock()
|
||||
_send_command("position fen " + fen)
|
||||
mutex.unlock()
|
||||
|
||||
func generateMove(think_time_ms: int = 1000) -> void:
|
||||
if not running:
|
||||
return
|
||||
|
||||
move_time = think_time_ms
|
||||
|
||||
# Update position first
|
||||
mutex.lock()
|
||||
var command = "position fen " + game.getCurrentFen()
|
||||
if moves.size() > 0:
|
||||
command += " moves " + " ".join(moves)
|
||||
print(command)
|
||||
var output = _send_command(command)
|
||||
|
||||
# Then get move
|
||||
output = _send_command("go movetime " + str(move_time))
|
||||
if output.size() == 0:
|
||||
return
|
||||
print(type_string(typeof(output[0])))
|
||||
var lines = output[0].split("\n")
|
||||
mutex.unlock()
|
||||
# Parse the output
|
||||
for line in lines:
|
||||
# print("-")
|
||||
# print(line)
|
||||
# print("-")
|
||||
if line.begins_with("bestmove"):
|
||||
var parts = line.split(" ")
|
||||
print( parts)
|
||||
if parts.size() >= 2:
|
||||
generated_move = {
|
||||
"move": parts[1],
|
||||
"ponder": parts[3]
|
||||
}
|
||||
print("Generated move: ", generated_move)
|
||||
return
|
||||
|
||||
generated_move = {}
|
||||
|
||||
func getGeneratedMove() -> Dictionary:
|
||||
var move = generated_move.duplicate()
|
||||
generated_move.clear() # Clear after retrieving
|
||||
return move
|
||||
|
||||
func from_move_to_string(move_data: Dictionary) -> String:
|
||||
var board_size = len(board)
|
||||
|
||||
# Get source and target squares
|
||||
var source_i = move_data.source % board_size
|
||||
var source_j = move_data.source / board_size
|
||||
var target_i = move_data.target % board_size
|
||||
var target_j = move_data.target / board_size
|
||||
|
||||
# Convert to algebraic notation
|
||||
var letters = "abcdefghijklmnopqrstuvwxyz".substr(0, board_size)
|
||||
var str_move = "%s%d%s%d" % [
|
||||
letters[source_i],
|
||||
board_size - source_j,
|
||||
letters[target_i],
|
||||
board_size - target_j
|
||||
]
|
||||
|
||||
# Add promotion piece if needed
|
||||
if move_data.get("flags", "") == "PROMOTION":
|
||||
str_move += symbol_from_piece_type[move_data.promotion_piece]
|
||||
|
||||
return str_move
|
||||
|
||||
func send_move(move_data: Dictionary):
|
||||
var str_move = from_move_to_string(move_data)
|
||||
moves.append(str_move)
|
||||
print("move: ", str_move)
|
||||
|
||||
# Update engine with the new move
|
||||
mutex.lock()
|
||||
var command = "position fen " + game.getCurrentFen()
|
||||
if moves.size() > 0:
|
||||
command += " moves " + " ".join(moves)
|
||||
_send_command(command)
|
||||
mutex.unlock()
|
||||
|
||||
func _send_command(command: String) -> Array:
|
||||
if not running:
|
||||
return []
|
||||
|
||||
var output = []
|
||||
OS.execute(engine_path, [command], output, true)
|
||||
return output
|
||||
306
Systems/FairyStockfish/StockfishClient.gd
Normal file
306
Systems/FairyStockfish/StockfishClient.gd
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
# StockfishClient.gd
|
||||
extends Node
|
||||
|
||||
var server_url = "http://localhost:27531"
|
||||
var http_request: HTTPRequest
|
||||
var running := false
|
||||
var generated_move: Dictionary = {}
|
||||
var move_time: int = 1000
|
||||
var game: ChessGame
|
||||
|
||||
var symbol_from_piece_type := {
|
||||
"PAWN": "p", "KNIGHT": "n", "BISHOP": "b",
|
||||
"ROOK": "r", "QUEEN": "q", "KING": "k"
|
||||
}
|
||||
|
||||
var piece_type_from_symbol := {
|
||||
"p": "PAWN", "n": "KNIGHT", "b": "BISHOP",
|
||||
"r": "ROOK", "q": "QUEEN", "k": "KING"
|
||||
}
|
||||
|
||||
func _init():
|
||||
print("STARTING SERVER CLIENT")
|
||||
http_request = HTTPRequest.new()
|
||||
add_child(http_request)
|
||||
http_request.request_completed.connect(self._on_request_completed)
|
||||
|
||||
func connect_to_engine(_path: String, g: ChessGame) -> bool:
|
||||
game = g
|
||||
# Wait for server to be ready
|
||||
var retries = 5
|
||||
var delay = 1.0 # seconds
|
||||
|
||||
while retries > 0:
|
||||
if ServerManager.is_server_running():
|
||||
print("**************SERVER RUNNING ****************")
|
||||
running = true
|
||||
# start_game(2100)
|
||||
return true
|
||||
|
||||
await get_tree().create_timer(delay).timeout
|
||||
retries -= 1
|
||||
print("**************ATTEMPTING SERVER CONNECTION****************")
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func start_game(elo: int) -> void:
|
||||
start_board(elo)
|
||||
load_fen(game.getCurrentFen())
|
||||
|
||||
|
||||
|
||||
func disconnect_engine():
|
||||
running = false
|
||||
|
||||
func stop_calculating():
|
||||
if not running:
|
||||
return
|
||||
# Send stop command to server
|
||||
http_request.request(server_url + "/stop", [], HTTPClient.METHOD_POST)
|
||||
|
||||
func load_fen(fen: String):
|
||||
if not running:
|
||||
return
|
||||
# var http_request = HTTPRequest.new()
|
||||
# add_child(http_request)
|
||||
# http_request.request_completed.connect(self._on_request_completed)
|
||||
var headers = ["Content-Type: application/json"]
|
||||
var body = JSON.new().stringify({
|
||||
"fen": fen
|
||||
})
|
||||
print(server_url + "/position")
|
||||
print(body)
|
||||
|
||||
http_request.request(server_url + "/position", headers, HTTPClient.METHOD_POST, body)
|
||||
await http_request.request_completed
|
||||
func start_board(elo: int, variant: String = "8x8"):
|
||||
if not running:
|
||||
return
|
||||
var headers = ["Content-Type: application/json"]
|
||||
var body = JSON.new().stringify({
|
||||
"variant": 'chess'
|
||||
})
|
||||
print(server_url + "/new")
|
||||
print(body)
|
||||
http_request.request(server_url + "/new", headers, HTTPClient.METHOD_POST, body)
|
||||
await http_request.request_completed
|
||||
setElo(elo, variant)
|
||||
|
||||
func clear_game( ):
|
||||
if not running:
|
||||
return
|
||||
var headers = ["Content-Type: application/json"]
|
||||
var body = JSON.new().stringify({
|
||||
"variant": 'chess'
|
||||
})
|
||||
print(server_url + "/new")
|
||||
print(body)
|
||||
|
||||
var request: HTTPRequest = HTTPRequest.new()
|
||||
request.request(server_url + "/new", headers, HTTPClient.METHOD_POST, body)
|
||||
await request.request_completed
|
||||
func _exit_tree():
|
||||
ServerManager.stop_server()
|
||||
disconnect_engine();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func get_globalDir() -> String:
|
||||
|
||||
if OS.get_name() == "Linux":
|
||||
return OS.get_environment("HOME").path_join(".local/share/ChessBuilder")
|
||||
elif OS.get_name() == "Windows":
|
||||
return OS.get_environment("APPDATA").path_join("Roaming/ChessBuilder")
|
||||
else: # macOS
|
||||
return OS.get_environment("HOME").path_join("Library/ChessBuilder")
|
||||
|
||||
|
||||
func setVariant(variant: String = "8x8"):
|
||||
if not running:
|
||||
return
|
||||
print("####################################")
|
||||
print("####################################")
|
||||
print("chessbuilder" + variant)
|
||||
print("####################################")
|
||||
print("####################################")
|
||||
print("####################################")
|
||||
var headers = ["Content-Type: application/json"]
|
||||
var data = {
|
||||
"options": [
|
||||
{
|
||||
"name": "VariantPath",
|
||||
"value": get_globalDir() + "/variant" + "/custom_variant.ini"
|
||||
},
|
||||
{
|
||||
"name": "UCI_Variant",
|
||||
"value": "chessbuilder" + variant
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
var body = JSON.new().stringify(data)
|
||||
|
||||
# Request engine move
|
||||
http_request.request(
|
||||
server_url + "/setoptions",
|
||||
headers,
|
||||
HTTPClient.METHOD_POST,
|
||||
body
|
||||
)
|
||||
|
||||
func setElo(elo: int = 1350, variant: String = "8x8") -> void:
|
||||
if not running:
|
||||
return
|
||||
var headers = ["Content-Type: application/json"]
|
||||
var data = {
|
||||
"options": [
|
||||
{
|
||||
"name": "VariantPath",
|
||||
"value": get_globalDir() + "/variant" + "/custom_variant.ini"
|
||||
},
|
||||
{
|
||||
"name": "UCI_LimitStrength",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "UCI_Elo",
|
||||
"value": str(elo) # Convert int to string
|
||||
},
|
||||
{
|
||||
"name": "UCI_Variant",
|
||||
"value": "chessbuilder" + variant
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
var body = JSON.new().stringify(data)
|
||||
|
||||
# Request engine move
|
||||
var elo_req = HTTPRequest.new()
|
||||
add_child(elo_req)
|
||||
elo_req.request_completed.connect(self._on_request_completed)
|
||||
elo_req.request(
|
||||
server_url + "/setoptions",
|
||||
headers,
|
||||
HTTPClient.METHOD_POST,
|
||||
body
|
||||
)
|
||||
await elo_req.request_completed
|
||||
http_request.request(
|
||||
server_url + "/position",
|
||||
headers,
|
||||
HTTPClient.METHOD_POST,
|
||||
JSON.new().stringify({"start": true})
|
||||
)
|
||||
|
||||
func generateMove(think_time_ms: int = 1000) -> void:
|
||||
if not running:
|
||||
return
|
||||
print("&&&&&&&&&&&&&&&GENERATING MOVE&&&&&&&&&&&&&&&&&&&&&&", str(game.generate_variant_aware_fen()))
|
||||
|
||||
move_time = think_time_ms
|
||||
|
||||
var headers = ["Content-Type: application/json"]
|
||||
var body = JSON.stringify({
|
||||
"movetime": think_time_ms,
|
||||
"depth": 15,
|
||||
"fen": str(game.generate_variant_aware_fen())
|
||||
})
|
||||
|
||||
# Request engine move
|
||||
var move_request = HTTPRequest.new()
|
||||
add_child(move_request)
|
||||
move_request.request_completed.connect(self._on_bestmove_completed)
|
||||
var error = move_request.request(
|
||||
server_url + "/enginemove",
|
||||
headers,
|
||||
HTTPClient.METHOD_POST,
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func getGeneratedMove() -> Dictionary:
|
||||
var move = generated_move.duplicate()
|
||||
generated_move.clear()
|
||||
return move
|
||||
|
||||
func send_move(move_data: Dictionary):
|
||||
if not running:
|
||||
return
|
||||
|
||||
var headers = ["Content-Type: application/json"]
|
||||
var move_str = from_move_to_string(move_data)
|
||||
var body = JSON.stringify({
|
||||
"move": move_str
|
||||
})
|
||||
|
||||
http_request.request(server_url + "/move", headers, HTTPClient.METHOD_POST, body)
|
||||
|
||||
# Helper functions
|
||||
func from_move_to_string(move_data: Dictionary) -> String:
|
||||
# Same implementation as original
|
||||
var board_size = 8 # Standard chess board
|
||||
|
||||
var source_i = move_data.source % board_size
|
||||
var source_j = move_data.source / board_size
|
||||
var target_i = move_data.target % board_size
|
||||
var target_j = move_data.target / board_size
|
||||
|
||||
var letters = "abcdefgh"
|
||||
var str_move = "%s%d%s%d" % [
|
||||
letters[source_i],
|
||||
board_size - source_j,
|
||||
letters[target_i],
|
||||
board_size - target_j
|
||||
]
|
||||
|
||||
if move_data.get("flags", "") == "PROMOTION":
|
||||
str_move += symbol_from_piece_type[move_data.promotion_piece].to_lower()
|
||||
|
||||
return str_move
|
||||
|
||||
|
||||
|
||||
func _on_bestmove_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
|
||||
print("^^^^^^^^^^^^^^^_on_bestmove_completed^^^^^^^^^^^^^^^^^")
|
||||
var json = JSON.new()
|
||||
json.parse(body.get_string_from_utf8())
|
||||
var response = json.get_data()
|
||||
# Will print the user agent string used by the HTTPRequest node (as recognized by httpbin.org).
|
||||
print(response)
|
||||
if response == null or result != HTTPRequest.RESULT_SUCCESS:
|
||||
print("HTTP Request failed")
|
||||
return
|
||||
|
||||
if response.status != "ok":
|
||||
print("Server error:", response_code, response,)
|
||||
return
|
||||
|
||||
|
||||
generated_move = {
|
||||
"move": response.move,
|
||||
"ponder": ""
|
||||
}
|
||||
|
||||
|
||||
|
||||
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
|
||||
print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^")
|
||||
var json = JSON.new()
|
||||
json.parse(body.get_string_from_utf8())
|
||||
var response = json.get_data()
|
||||
# Will print the user agent string used by the HTTPRequest node (as recognized by httpbin.org).
|
||||
print(response)
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
print("HTTP Request failed")
|
||||
return
|
||||
|
||||
if response.status != "ok":
|
||||
print("Server error:", response_code, json.parse(body.get_string_from_utf8()),)
|
||||
return
|
||||
1
Systems/FairyStockfish/StockfishClient.gd.uid
Normal file
1
Systems/FairyStockfish/StockfishClient.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bnxxex87ax7hc
|
||||
84
Systems/Game/CameraController.gd
Normal file
84
Systems/Game/CameraController.gd
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
class_name CameraController
|
||||
extends Node2D
|
||||
|
||||
signal zoom_changed(zoom_level)
|
||||
|
||||
# Camera movement properties
|
||||
@export var min_zoom: float = 0.5
|
||||
@export var max_zoom: float = 2.0
|
||||
@export var zoom_step: float = 0.1
|
||||
@export var pan_speed: float = 1.0
|
||||
|
||||
var board_container: Node
|
||||
var initial_board_position: Vector2
|
||||
var initial_board_size: Vector2
|
||||
|
||||
var current_zoom: float = 1.0
|
||||
var dragging: bool = false
|
||||
var drag_start_position: Vector2
|
||||
|
||||
func _init(target_board: Node) -> void:
|
||||
board_container = target_board
|
||||
|
||||
func _ready() -> void:
|
||||
initial_board_position = board_container.position
|
||||
if board_container.has_method("get_combined_minimum_size"):
|
||||
initial_board_size = board_container.get_combined_minimum_size()
|
||||
else:
|
||||
initial_board_size = board_container.size
|
||||
|
||||
current_zoom = 1.0
|
||||
board_container.scale = Vector2(current_zoom, current_zoom)
|
||||
|
||||
adjust_pivot()
|
||||
|
||||
func adjust_pivot() -> void:
|
||||
var center = initial_board_size / 2
|
||||
board_container.pivot_offset = center
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
zoom_in(event.position)
|
||||
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
zoom_out(event.position)
|
||||
elif event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||
if event.pressed:
|
||||
start_drag(event.position)
|
||||
else:
|
||||
end_drag()
|
||||
|
||||
if event is InputEventMouseMotion and dragging:
|
||||
update_drag(event.position)
|
||||
|
||||
func zoom_in(mouse_pos: Vector2) -> void:
|
||||
if current_zoom < max_zoom:
|
||||
apply_zoom(current_zoom + zoom_step)
|
||||
|
||||
func zoom_out(mouse_pos: Vector2) -> void:
|
||||
if current_zoom > min_zoom:
|
||||
apply_zoom(current_zoom - zoom_step)
|
||||
|
||||
func apply_zoom(new_zoom: float) -> void:
|
||||
current_zoom = new_zoom
|
||||
board_container.scale = Vector2(current_zoom, current_zoom)
|
||||
|
||||
emit_signal("zoom_changed", current_zoom)
|
||||
|
||||
func start_drag(position: Vector2) -> void:
|
||||
dragging = true
|
||||
drag_start_position = position
|
||||
|
||||
func end_drag() -> void:
|
||||
dragging = false
|
||||
|
||||
func update_drag(position: Vector2) -> void:
|
||||
if dragging:
|
||||
var delta = position - drag_start_position
|
||||
board_container.position += delta * pan_speed
|
||||
drag_start_position = position
|
||||
|
||||
func reset_view() -> void:
|
||||
current_zoom = 1.0
|
||||
board_container.scale = Vector2(current_zoom, current_zoom)
|
||||
board_container.position = initial_board_position
|
||||
1
Systems/Game/CameraController.gd.uid
Normal file
1
Systems/Game/CameraController.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bdbap6f4c4d5w
|
||||
File diff suppressed because it is too large
Load diff
1
Systems/Game/ChessGame.gd.uid
Normal file
1
Systems/Game/ChessGame.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cbcu68o863pfp
|
||||
226
Systems/Game/DeckManagerScreen.gd
Normal file
226
Systems/Game/DeckManagerScreen.gd
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
extends Control
|
||||
class_name DeckManagerScreen
|
||||
|
||||
signal back_pressed
|
||||
signal deck_manager_visibility_changed(isvisible)
|
||||
|
||||
# Node references
|
||||
@onready var deckGrid = $MainContainer/GridScrollContainer/GridContainer
|
||||
@onready var bankContainer = $MainContainer/BankContainer/ScrollContainer/VBoxContainer
|
||||
@onready var backButton = $BackButton
|
||||
@onready var card_preview = $CardPreviewPanel
|
||||
|
||||
# Default deck size (can be overridden via options)
|
||||
var maxDeckSize = 40
|
||||
var deckManager = null
|
||||
var bankCards = [] # Cards available but not in deck
|
||||
|
||||
# The current deck
|
||||
var currentDeck = []
|
||||
var current_preview_card = null
|
||||
|
||||
func _ready():
|
||||
# Connect back button
|
||||
if backButton:
|
||||
backButton.connect("pressed", Callable(self, "_on_backButton_pressed"))
|
||||
|
||||
if card_preview:
|
||||
card_preview.visible = false
|
||||
|
||||
|
||||
func initialize(options = null):
|
||||
# Process options if provided
|
||||
if options:
|
||||
if options.has("max_deck_size") and options.max_deck_size is int:
|
||||
maxDeckSize = options.max_deck_size
|
||||
|
||||
|
||||
# Find the DeckManager instance
|
||||
var board = get_node_or_null("/root/Board") as ChessGame
|
||||
if board and "deckManager" in board:
|
||||
deckManager = board.deckManager
|
||||
print("Found deck manager:", deckManager)
|
||||
# if(!deckManager.deck):
|
||||
# deckManager.initializeStartingDeck()
|
||||
else:
|
||||
print("DeckManager not found on Board node")
|
||||
print("DECK MANAGER")
|
||||
|
||||
# Load cards from deck and bank
|
||||
loadCards()
|
||||
|
||||
# Set up the grid with empty card containers
|
||||
setupDeckGrid()
|
||||
|
||||
# Populate the bank with available cards
|
||||
populateBank()
|
||||
|
||||
func loadCards():
|
||||
var board = get_node_or_null("/root/Board") as ChessGame
|
||||
if board and "deckManager" in board:
|
||||
deckManager = board.deckManager
|
||||
print("Found deck manager:", deckManager)
|
||||
if deckManager:
|
||||
# Clone the deck to work with
|
||||
currentDeck = deckManager.deck.duplicate()
|
||||
bankCards = deckManager.bank.duplicate()
|
||||
else:
|
||||
# Fallback with empty collections if deck manager not found
|
||||
currentDeck = []
|
||||
bankCards = []
|
||||
print("Warning: DeckManager not found")
|
||||
|
||||
|
||||
func setupDeckGrid():
|
||||
# Clear existing children
|
||||
for child in deckGrid.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Calculate grid dimensions
|
||||
var cols = 4 #check screen to deteremine
|
||||
var rows = maxDeckSize / cols
|
||||
deckGrid.columns = cols
|
||||
|
||||
# Create card slots
|
||||
for i in range(maxDeckSize):
|
||||
var card_slot = preload("res://card_slot.tscn").instantiate()
|
||||
deckGrid.add_child(card_slot)
|
||||
|
||||
# Connect signals
|
||||
card_slot.connect("card_selected", Callable(self, "_on_deck_card_selected"))
|
||||
_connect_hover_signals(card_slot)
|
||||
|
||||
# Set card if available
|
||||
if i < currentDeck.size():
|
||||
card_slot.set_card(currentDeck[i])
|
||||
else:
|
||||
card_slot.clear()
|
||||
|
||||
func _connect_hover_signals(node):
|
||||
# Add hover signals for preview functionality
|
||||
if node.is_class("Control"):
|
||||
node.mouse_entered.connect(Callable(self, "_on_card_mouse_entered").bind(node))
|
||||
# node.mouse_exited.connect(Callable(self, "_on_card_mouse_exited").bind(node))
|
||||
|
||||
|
||||
func populateBank():
|
||||
# Clear existing children
|
||||
for child in bankContainer.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Add each bank card to the list
|
||||
for card in bankCards:
|
||||
var card_item = preload("res://card_bank_item.tscn").instantiate()
|
||||
bankContainer.add_child(card_item)
|
||||
card_item.set_card(card)
|
||||
card_item.connect("card_selected", Callable(self, "_on_bank_card_selected"))
|
||||
_connect_hover_signals(card_item)
|
||||
|
||||
func _on_deck_card_selected(card_slot, card):
|
||||
if card:
|
||||
# Remove card from deck
|
||||
var index = currentDeck.find(card)
|
||||
if index >= 0:
|
||||
currentDeck.remove_at(index)
|
||||
|
||||
# Add to bank
|
||||
bankCards.append(card)
|
||||
|
||||
# Update UI
|
||||
card_slot.clear()
|
||||
populateBank()
|
||||
if current_preview_card == card:
|
||||
hide_card_preview()
|
||||
|
||||
func _on_bank_card_selected(card_item, card):
|
||||
print("_on_bank_card_selected")
|
||||
# Find first empty slot in deck
|
||||
var empty_slot_index = -1
|
||||
for i in range(deckGrid.get_child_count()):
|
||||
var slot = deckGrid.get_child(i)
|
||||
if !slot.has_card():
|
||||
empty_slot_index = i
|
||||
break
|
||||
|
||||
if empty_slot_index >= 0 and currentDeck.size() < maxDeckSize:
|
||||
# Remove from bank
|
||||
var bank_index = bankCards.find(card)
|
||||
if bank_index >= 0:
|
||||
bankCards.remove_at(bank_index)
|
||||
|
||||
# Add to deck
|
||||
currentDeck.append(card)
|
||||
|
||||
# Update UI
|
||||
deckGrid.get_child(empty_slot_index).set_card(card)
|
||||
populateBank()
|
||||
|
||||
func _on_card_mouse_entered(node):
|
||||
var card = null
|
||||
|
||||
# Get the card from the node
|
||||
if node.has_method("has_card") and node.has_card():
|
||||
# This is a CardSlot
|
||||
card = node.current_card
|
||||
elif node.has_method("set_card") and node.current_card:
|
||||
# This is a CardBankItem
|
||||
card = node.current_card
|
||||
|
||||
if card:
|
||||
show_card_preview(card)
|
||||
|
||||
# func _on_card_mouse_exited(node):
|
||||
# # Add a short delay before hiding the preview
|
||||
# # This prevents flickering when moving between cards
|
||||
# await get_tree().create_timer(0.1).timeout
|
||||
|
||||
# # Only hide if we're not hovering over another card that shows the same preview
|
||||
# if current_preview_card:
|
||||
# hide_card_preview()
|
||||
|
||||
func show_card_preview(card):
|
||||
if card_preview and card:
|
||||
current_preview_card = card
|
||||
card_preview.preview_card(card)
|
||||
|
||||
func hide_card_preview():
|
||||
if card_preview:
|
||||
current_preview_card = null
|
||||
card_preview.hide_preview()
|
||||
|
||||
func saveDeck():
|
||||
if deckManager:
|
||||
# Save the current deck to the deck manager
|
||||
deckManager.deck = currentDeck.duplicate()
|
||||
deckManager.bank = bankCards.duplicate()
|
||||
|
||||
print("Deck saved with ", currentDeck.size(), " cards")
|
||||
|
||||
func _on_backButton_pressed():
|
||||
# Save changes before returning
|
||||
saveDeck()
|
||||
|
||||
# Emit signal to go back
|
||||
emit_signal("back_pressed")
|
||||
|
||||
# Hide this screen
|
||||
visible = false
|
||||
|
||||
|
||||
|
||||
|
||||
# Notification
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_VISIBILITY_CHANGED:
|
||||
_on_visibility_changed(visible)
|
||||
|
||||
func _on_visibility_changed(is_visible):
|
||||
|
||||
print("DeckManager visibility changed to: ", is_visible)
|
||||
if is_visible:
|
||||
loadCards()
|
||||
setupDeckGrid()
|
||||
else:
|
||||
print("DeckManager is now invisible")
|
||||
|
||||
emit_signal("deck_manager_visibility_changed", is_visible)
|
||||
1
Systems/Game/DeckManagerScreen.gd.uid
Normal file
1
Systems/Game/DeckManagerScreen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://vxufsih5pgeu
|
||||
45
Systems/Game/Game.gd
Normal file
45
Systems/Game/Game.gd
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
class_name Game extends Node
|
||||
|
||||
|
||||
@onready var menuContainer = $MenuContainer
|
||||
@onready var chessGame = $ChessGame
|
||||
@onready var stateMachine = $StateMachine
|
||||
|
||||
func _ready():
|
||||
if menuContainer:
|
||||
menuContainer.visible = true
|
||||
|
||||
if chessGame:
|
||||
chessGame.visible = false
|
||||
|
||||
if menuContainer and menuContainer.has_signal("new_game_requested"):
|
||||
menuContainer.connect("new_game_requested", Callable(self, "_on_new_game_started"))
|
||||
|
||||
func _on_new_game_started():
|
||||
print("Starting new game...")
|
||||
|
||||
if chessGame:
|
||||
chessGame.visible = true
|
||||
|
||||
|
||||
if !chessGame.is_inside_tree():
|
||||
await chessGame.ready
|
||||
|
||||
_start_game_logic()
|
||||
|
||||
func _start_game_logic():
|
||||
if stateMachine and stateMachine.has_method("transitionToNextState"):
|
||||
stateMachine.transitionToNextState(Constants.WHITE_TURN)
|
||||
|
||||
|
||||
func _unhandled_input(event):
|
||||
if event is InputEventKey:
|
||||
if event.pressed and event.keycode == KEY_ESCAPE:
|
||||
_toggle_menu()
|
||||
|
||||
func _toggle_menu():
|
||||
if menuContainer:
|
||||
menuContainer.visible = !menuContainer.visible
|
||||
|
||||
if menuContainer.visible and chessGame:
|
||||
pass
|
||||
1
Systems/Game/Game.gd.uid
Normal file
1
Systems/Game/Game.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cbaoxhgtk4td8
|
||||
66
Systems/Game/GameMenuButton.gd
Normal file
66
Systems/Game/GameMenuButton.gd
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
extends RichTextLabel
|
||||
class_name GameMenuButton
|
||||
|
||||
signal pressed
|
||||
|
||||
# Style properties
|
||||
var normal_color: Color = Color(1, 1, 1, 1) # White
|
||||
var hover_color: Color = Color(1, 1, 0, 1) # Yellow
|
||||
var font_size: int = 28
|
||||
|
||||
func _ready():
|
||||
# Make this label clickable
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
# Prevent text wrapping
|
||||
autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||
fit_content = true
|
||||
|
||||
# Set size flags to expand horizontally
|
||||
size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
|
||||
# Remove default padding/margin
|
||||
add_theme_constant_override("margin_top", 0)
|
||||
add_theme_constant_override("margin_bottom", 0)
|
||||
add_theme_constant_override("margin_left", 0)
|
||||
add_theme_constant_override("margin_right", 0)
|
||||
|
||||
# Remove line spacing
|
||||
add_theme_constant_override("line_separation", 0)
|
||||
|
||||
# Set up base styling
|
||||
add_theme_font_size_override("normal_font_size", font_size)
|
||||
add_theme_color_override("default_color", normal_color)
|
||||
|
||||
# Make text bold
|
||||
bbcode_enabled = true
|
||||
text = "[b]" + text + "[/b]"
|
||||
|
||||
# Connect the gui_input signal to our own handler
|
||||
connect("gui_input", Callable(self, "_on_gui_input"))
|
||||
|
||||
# Connect hover signals for better user experience
|
||||
connect("mouse_entered", Callable(self, "_on_mouse_entered"))
|
||||
connect("mouse_exited", Callable(self, "_on_mouse_exited"))
|
||||
|
||||
func _on_gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
emit_signal("pressed")
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_mouse_entered():
|
||||
# Change appearance when mouse hovers
|
||||
add_theme_color_override("default_color", hover_color)
|
||||
|
||||
# Scale up slightly on hover (optional)
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "scale", Vector2(1.05, 1.05), 0.1)
|
||||
|
||||
func _on_mouse_exited():
|
||||
# Restore original appearance
|
||||
add_theme_color_override("default_color", normal_color)
|
||||
|
||||
# Scale back to normal (optional)
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.1)
|
||||
1
Systems/Game/GameMenuButton.gd.uid
Normal file
1
Systems/Game/GameMenuButton.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bfjmon81nckns
|
||||
100
Systems/Game/GameMenuScreen.gd
Normal file
100
Systems/Game/GameMenuScreen.gd
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
extends Control
|
||||
class_name GameMenuScreen
|
||||
|
||||
# Signals with optional parameters
|
||||
signal shop_open_requested(options)
|
||||
signal deckmanager_open_requested(options)
|
||||
signal map_open_requested(options)
|
||||
signal new_game_requested(options)
|
||||
signal lobby_open_requested(options)
|
||||
|
||||
# @onready var newGameButton = $HBoxContainer/VBoxContainer/MenuOptions/NewGameText
|
||||
# @onready var continueButton = $HBoxContainer/VBoxContainer/MenuOptions/Continue
|
||||
# @onready var optionsButton = $HBoxContainer/VBoxContainer/MenuOptions/Options
|
||||
# @onready var versionText = $HBoxContainer/VBoxContainer/VersionText
|
||||
# @onready var titleText = $HBoxContainer/VBoxContainer/TitleText
|
||||
# @onready var developerText = $HBoxContainer/VBoxContainer/DeveloperText
|
||||
|
||||
# Node references
|
||||
@onready var shopButton = $HBoxContainer/VBoxContainer/GameOptions/ShopText
|
||||
@onready var lobbyButton = $HBoxContainer/VBoxContainer/GameOptions/LobbyText
|
||||
@onready var mapButton = $HBoxContainer/VBoxContainer/GameOptions/MapText
|
||||
@onready var startButton = $HBoxContainer/VBoxContainer/GameOptions/StartText
|
||||
@onready var backButton = $HBoxContainer/VBoxContainer/GameOptions/BackText
|
||||
|
||||
# Reference to main menu container
|
||||
var main_menu_container = null
|
||||
|
||||
func _ready():
|
||||
# Setup button signals
|
||||
if shopButton:
|
||||
shopButton.visible = false;
|
||||
shopButton.connect("pressed", Callable(self, "_on_shop_button_pressed"))
|
||||
|
||||
if lobbyButton:
|
||||
lobbyButton.connect("pressed", Callable(self, "_on_lobby_button_pressed"))
|
||||
|
||||
if mapButton:
|
||||
mapButton.visible = false;
|
||||
mapButton.connect("pressed", Callable(self, "_on_map_button_pressed"))
|
||||
|
||||
if startButton:
|
||||
startButton.connect("pressed", Callable(self, "_on_start_button_pressed"))
|
||||
startButton.visible = false;
|
||||
|
||||
if backButton:
|
||||
backButton.connect("pressed", Callable(self, "_on_back_button_pressed"))
|
||||
|
||||
connect("map_open_requested", Callable(self, "_on_map_button_pressed"))
|
||||
|
||||
|
||||
|
||||
func setup(menu_container):
|
||||
main_menu_container = menu_container
|
||||
|
||||
func _on_shop_button_pressed():
|
||||
print("Shop button pressed")
|
||||
# Emit signal with null options
|
||||
emit_signal("shop_open_requested", null)
|
||||
|
||||
func _on_deck_button_pressed():
|
||||
print("Manage Deck button pressed")
|
||||
# Emit signal with null options
|
||||
emit_signal("deckmanager_open_requested", null)
|
||||
|
||||
func _on_map_button_pressed():
|
||||
print("Map button pressed")
|
||||
# ignore error, keeping the null prevents infinit recursion
|
||||
emit_signal("map_open_requested", null)
|
||||
visible = false
|
||||
|
||||
func _on_start_button_pressed():
|
||||
print("Start button pressed")
|
||||
# Emit signal with empty options dictionary
|
||||
emit_signal("new_game_requested", {})
|
||||
|
||||
# emit_signal("new_game_requested")
|
||||
# Hide this menu
|
||||
self.visible = false
|
||||
|
||||
func _on_back_button_pressed():
|
||||
print("Back button pressed")
|
||||
# Return to main menu
|
||||
if main_menu_container:
|
||||
main_menu_container.visible = true
|
||||
# Hide this menu
|
||||
self.visible = false
|
||||
|
||||
func show_menu():
|
||||
self.visible = true
|
||||
|
||||
func _on_lobby_open_requested(options):
|
||||
print("Lobby button pressed")
|
||||
emit_signal("lobby_open_requested", options)
|
||||
visible = false
|
||||
|
||||
|
||||
func _on_lobby_button_pressed():
|
||||
print("Lobby button pressed")
|
||||
emit_signal("lobby_open_requested", null)
|
||||
visible = false
|
||||
1
Systems/Game/GameMenuScreen.gd.uid
Normal file
1
Systems/Game/GameMenuScreen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://j0m4rwr86oi6
|
||||
258
Systems/Game/HandPreloadScreen.gd
Normal file
258
Systems/Game/HandPreloadScreen.gd
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
extends Control
|
||||
class_name HandPreloadScreen
|
||||
|
||||
signal back_pressed
|
||||
signal preload_visibility_changed(isvisible)
|
||||
|
||||
# Node references
|
||||
@onready var deckGrid = $MainContainer/GridScrollContainer/GridContainer
|
||||
@onready var bankContainer = $MainContainer/BankContainer/ScrollContainer/VBoxContainer
|
||||
@onready var backButton = $BackButton
|
||||
@onready var card_preview = $CardPreviewPanel
|
||||
@onready var token_label = $TokenLabel
|
||||
@onready var cost_label = $CostLabel
|
||||
|
||||
var handSize = 2
|
||||
var deckManager = null
|
||||
var player = null
|
||||
var bankCards = [] # Cards available
|
||||
var tokens = 0
|
||||
|
||||
var usedSlots = []
|
||||
var currentHand = []
|
||||
var current_preview_card = null
|
||||
|
||||
func _ready():
|
||||
# Connect back button
|
||||
if backButton:
|
||||
backButton.connect("pressed", Callable(self, "_on_backButton_pressed"))
|
||||
|
||||
if card_preview:
|
||||
card_preview.visible = false
|
||||
if cost_label:
|
||||
cost_label.visible = false
|
||||
|
||||
|
||||
func initialize(options = null):
|
||||
# Find the DeckManager instance
|
||||
var board = get_node_or_null("/root/Board") as ChessGame
|
||||
if board and "deckManager" in board:
|
||||
deckManager = board.deckManager
|
||||
print("Found deck manager:", deckManager)
|
||||
else:
|
||||
print("DeckManager not found on Board node")
|
||||
if board and "player" in board:
|
||||
player = board.player
|
||||
handSize = player.hand_size
|
||||
tokens = player.tokens
|
||||
updateTokenLabel()
|
||||
else:
|
||||
print("Player not found on Board node")
|
||||
|
||||
# Load cards from deck and bank
|
||||
loadCards()
|
||||
|
||||
# Set up the grid with empty card containers
|
||||
setupDeckGrid()
|
||||
|
||||
# Populate the bank with available cards
|
||||
populateBank()
|
||||
|
||||
|
||||
func updateTokenLabel():
|
||||
if token_label:
|
||||
token_label.text = str(tokens) + " TOKENS"
|
||||
|
||||
func loadCards():
|
||||
if deckManager:
|
||||
currentHand.clear()
|
||||
usedSlots.clear()
|
||||
# Clone the deck to work with
|
||||
bankCards = deckManager.deck.duplicate()
|
||||
for id in deckManager.preloaded_hand_cards:
|
||||
var matchCard = func matchCardId(card):
|
||||
return "id" in card and card.id == id
|
||||
var cardIndex = bankCards.find_custom(matchCard.bind())
|
||||
if cardIndex != -1:
|
||||
currentHand.append(bankCards[cardIndex].id)
|
||||
usedSlots.append(true)
|
||||
|
||||
else:
|
||||
# Fallback with empty collections if deck manager not found
|
||||
currentHand = []
|
||||
bankCards = []
|
||||
print("Warning: DeckManager not found")
|
||||
|
||||
|
||||
func setupDeckGrid():
|
||||
# Clear existing children
|
||||
for child in deckGrid.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Calculate grid dimensions
|
||||
var cols = 4 #check screen to deteremine
|
||||
var rows = handSize / cols
|
||||
deckGrid.columns = cols
|
||||
|
||||
# Create card slots
|
||||
for i in range(handSize):
|
||||
var card_slot = preload("res://card_slot.tscn").instantiate()
|
||||
deckGrid.add_child(card_slot)
|
||||
|
||||
# Connect signals
|
||||
card_slot.connect("card_selected", Callable(self, "_on_hand_card_selected"))
|
||||
_connect_hover_signals(card_slot)
|
||||
|
||||
# Set card if available
|
||||
if i < currentHand.size():
|
||||
var matchCard = func matchCardId(card):
|
||||
return "id" in card and card.id == currentHand[i]
|
||||
var cardIndex = bankCards.find_custom(matchCard.bind())
|
||||
if cardIndex != -1:
|
||||
card_slot.set_card(bankCards[cardIndex])
|
||||
else:
|
||||
card_slot.clear()
|
||||
|
||||
func _connect_hover_signals(node):
|
||||
# Add hover signals for preview functionality
|
||||
if node.is_class("Control"):
|
||||
node.mouse_entered.connect(Callable(self, "_on_card_mouse_entered").bind(node))
|
||||
# node.mouse_exited.connect(Callable(self, "_on_card_mouse_exited").bind(node))
|
||||
|
||||
|
||||
func populateBank():
|
||||
for child in bankContainer.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Add each bank card to the list
|
||||
for card in bankCards:
|
||||
var card_item = preload("res://card_bank_item.tscn").instantiate()
|
||||
bankContainer.add_child(card_item)
|
||||
card_item.set_card(card)
|
||||
card_item.connect("card_selected", Callable(self, "_on_bank_card_selected"))
|
||||
_connect_hover_signals(card_item)
|
||||
|
||||
|
||||
static var TokenCosts = {
|
||||
Card.Rank.RANK_0: 15, # Most expensive (one-time use)
|
||||
Card.Rank.RANK_1: 10, # Expensive (once per match)
|
||||
Card.Rank.RANK_2: 5, # Medium (multiple uses)
|
||||
Card.Rank.RANK_3: 3 # Cheapest (basic cards)
|
||||
}
|
||||
|
||||
func _on_hand_card_selected(card_slot, card):
|
||||
if card:
|
||||
# Remove card from deck
|
||||
var index = currentHand.find(card.id)
|
||||
if index >= 0:
|
||||
currentHand.remove_at(index)
|
||||
if index < usedSlots.size() and usedSlots[index] != true:
|
||||
tokens += Utils.TokenCosts[card.rank]
|
||||
usedSlots.remove_at(index)
|
||||
updateTokenLabel()
|
||||
|
||||
|
||||
# Update UI
|
||||
card_slot.clear()
|
||||
populateBank()
|
||||
if current_preview_card == card:
|
||||
hide_card_preview()
|
||||
|
||||
func _on_bank_card_selected(card_item, card):
|
||||
print("_on_bank_card_selected ", card.id)
|
||||
# Find first empty slot in deck
|
||||
var empty_slot_index = -1
|
||||
if card and (Utils.HandRankWhiteList.has(card.rank)):
|
||||
for i in range(deckGrid.get_child_count()):
|
||||
var slot = deckGrid.get_child(i)
|
||||
if !slot.has_card():
|
||||
empty_slot_index = i
|
||||
break
|
||||
|
||||
# print("_on_bank_card_selected ", currentHand, " ", handSize, " ", empty_slot_index, )
|
||||
# print("_on_bank_card_selected ", empty_slot_index >= 0, " ", currentHand.size() < handSize, " ", currentHand.find(card.id))
|
||||
if empty_slot_index >= 0 and currentHand.size() < handSize and currentHand.find(card.id) == -1 and tokens - Utils.TokenCosts[card.rank] > 0:
|
||||
# print("currentHand append ", card.id)
|
||||
|
||||
tokens -= Utils.TokenCosts[card.rank]
|
||||
updateTokenLabel()
|
||||
currentHand.append(card.id)
|
||||
usedSlots.append(false)
|
||||
|
||||
deckGrid.get_child(empty_slot_index).set_card(card)
|
||||
populateBank()
|
||||
|
||||
func _on_card_mouse_entered(node):
|
||||
var card = null
|
||||
|
||||
# Get the card from the node
|
||||
if node.has_method("has_card") and node.has_card():
|
||||
# This is a CardSlot
|
||||
card = node.current_card
|
||||
elif node.has_method("set_card") and node.current_card:
|
||||
# This is a CardBankItem
|
||||
card = node.current_card
|
||||
|
||||
if card:
|
||||
show_card_preview(card)
|
||||
|
||||
# func _on_card_mouse_exited(node):
|
||||
# # Add a short delay before hiding the preview
|
||||
# # This prevents flickering when moving between cards
|
||||
# await get_tree().create_timer(0.1).timeout
|
||||
|
||||
# # Only hide if we're not hovering over another card that shows the same preview
|
||||
# if current_preview_card:
|
||||
# hide_card_preview()
|
||||
|
||||
func show_card_preview(card):
|
||||
if card_preview and card:
|
||||
current_preview_card = card
|
||||
card_preview.preview_card(card)
|
||||
if cost_label:
|
||||
cost_label.visible = true
|
||||
cost_label.text = "Cost: " + str(Utils.TokenCosts[card.rank]) + " tokens"
|
||||
|
||||
func hide_card_preview():
|
||||
if cost_label:
|
||||
cost_label.visible = false
|
||||
if card_preview:
|
||||
current_preview_card = null
|
||||
card_preview.hide_preview()
|
||||
|
||||
func savePreloadCards():
|
||||
if deckManager:
|
||||
# Save the current deck to the deck manager
|
||||
deckManager.preloaded_hand_cards = currentHand.duplicate()
|
||||
|
||||
print("Preloads saved with ", currentHand.size(), " cards")
|
||||
player.tokens = tokens
|
||||
|
||||
func _on_backButton_pressed():
|
||||
# Save changes before returning
|
||||
savePreloadCards()
|
||||
|
||||
# Emit signal to go back
|
||||
emit_signal("back_pressed")
|
||||
|
||||
# Hide this screen
|
||||
visible = false
|
||||
|
||||
|
||||
|
||||
|
||||
# Notification
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_VISIBILITY_CHANGED:
|
||||
_on_visibility_changed(visible)
|
||||
|
||||
func _on_visibility_changed(is_visible):
|
||||
|
||||
print("Preload Screen visibility changed to: ", is_visible)
|
||||
if is_visible:
|
||||
loadCards()
|
||||
setupDeckGrid()
|
||||
else:
|
||||
print("Preload Screen is now invisible")
|
||||
|
||||
emit_signal("preload_visibility_changed", is_visible)
|
||||
1
Systems/Game/HandPreloadScreen.gd.uid
Normal file
1
Systems/Game/HandPreloadScreen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b7b2xlfvhgipb
|
||||
101
Systems/Game/Lobby/LobbyScreen.gd
Normal file
101
Systems/Game/Lobby/LobbyScreen.gd
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
extends Control
|
||||
class_name LobbyScreen
|
||||
|
||||
signal vanilla_selected
|
||||
signal shop_selected(options)
|
||||
signal preload_selected(options)
|
||||
signal deeper_selected
|
||||
signal back_pressed
|
||||
signal lobby_screen_visibility_changed(isvisible)
|
||||
|
||||
# Node references
|
||||
@onready var vanilla_button = $BottomContainer/VanillaButton
|
||||
@onready var shop_button = $CenterContainer/ShopButton
|
||||
@onready var deeper_button = $BottomContainer/DeeperButton
|
||||
@onready var back_button = $BackButton
|
||||
@onready var preload_button = $PreloadButton
|
||||
@onready var run_count_label = $TopBar/RunCountLabel
|
||||
@onready var token_label = $TopBar/TokenContainer/TokenLabel
|
||||
|
||||
var game: ChessGame
|
||||
var player: Player
|
||||
|
||||
func _ready():
|
||||
# Connect button signals
|
||||
if vanilla_button:
|
||||
vanilla_button.connect("pressed", Callable(self, "_on_vanilla_button_pressed"))
|
||||
|
||||
if shop_button:
|
||||
shop_button.connect("pressed", Callable(self, "_on_shop_button_pressed"))
|
||||
|
||||
if deeper_button:
|
||||
deeper_button.connect("pressed", Callable(self, "_on_deeper_button_pressed"))
|
||||
|
||||
if back_button:
|
||||
back_button.connect("pressed", Callable(self, "_on_back_button_pressed"))
|
||||
|
||||
if preload_button:
|
||||
preload_button.connect("pressed", Callable(self, "_on_preload_button_pressed"))
|
||||
|
||||
func initialize(options = null):
|
||||
game = get_node_or_null("/root/Board") as ChessGame
|
||||
if game and "player" in game:
|
||||
player = game.player
|
||||
update_display()
|
||||
|
||||
if deeper_button:
|
||||
deeper_button.disabled = player.get_run_count() == 0
|
||||
|
||||
func update_display():
|
||||
if player:
|
||||
if run_count_label:
|
||||
run_count_label.text = "RUN #" + str(player.get_run_count() + 1)
|
||||
|
||||
|
||||
if deeper_button:
|
||||
deeper_button.disabled = player.run_count == 0
|
||||
|
||||
if token_label:
|
||||
token_label.text = str(player.tokens) + " TOKENS"
|
||||
|
||||
func _on_preload_button_pressed():
|
||||
emit_signal("preload_selected")
|
||||
self.visible = false
|
||||
|
||||
func _on_vanilla_button_pressed():
|
||||
emit_signal("vanilla_selected")
|
||||
self.visible = false
|
||||
|
||||
func _on_shop_button_pressed():
|
||||
emit_signal("shop_selected", {})
|
||||
self.visible = false
|
||||
|
||||
func _on_deeper_button_pressed():
|
||||
emit_signal("deeper_selected")
|
||||
self.visible = false
|
||||
|
||||
func _on_back_button_pressed():
|
||||
emit_signal("back_pressed")
|
||||
# self.visible = false
|
||||
|
||||
func show_screen():
|
||||
initialize()
|
||||
self.visible = true
|
||||
|
||||
|
||||
|
||||
# Notification
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_VISIBILITY_CHANGED:
|
||||
_on_visibility_changed(visible)
|
||||
|
||||
func _on_visibility_changed(is_visible):
|
||||
|
||||
print("Lobby Screen visibility changed to: ", is_visible)
|
||||
if is_visible:
|
||||
update_display()
|
||||
else:
|
||||
print("Lobby Screen is now invisible")
|
||||
|
||||
emit_signal("lobby_screen_visibility_changed", is_visible)
|
||||
|
||||
1
Systems/Game/Lobby/LobbyScreen.gd.uid
Normal file
1
Systems/Game/Lobby/LobbyScreen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dkk0eu4fj7q5j
|
||||
387
Systems/Game/Lobby/LobbyShopScreen.gd
Normal file
387
Systems/Game/Lobby/LobbyShopScreen.gd
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
extends Control
|
||||
class_name LobbyShopScreen
|
||||
|
||||
signal back_pressed
|
||||
signal card_unlocked(card, token_cost)
|
||||
signal lobby_shop_visibility_changed(isvisible)
|
||||
signal hand_size_increased
|
||||
|
||||
# Node references
|
||||
@onready var card_carousel = $MainContainer/CardCarouselContainer/CardCarousel
|
||||
@onready var token_label = $TopBar/TokenContainer/TokenLabel
|
||||
@onready var buy_button = $BuyButton
|
||||
@onready var back_button = $BackButton
|
||||
@onready var card_preview = $CardPreviewPanel
|
||||
@onready var left_button = $MainContainer/CardCarouselContainer/LeftButton
|
||||
@onready var right_button = $MainContainer/CardCarouselContainer/RightButton
|
||||
@onready var hand_size_label = $HandSizeContainer/HandSizeLabel
|
||||
@onready var hand_size_cost_label = $HandSizeContainer/CostLabel
|
||||
@onready var increase_hand_size_button = $HandSizeContainer/IncreaseButton
|
||||
|
||||
var available_cards = []
|
||||
var player_tokens = 0
|
||||
var selected_card = null
|
||||
var selected_index = 0
|
||||
var carousel_page = 0
|
||||
var cards_per_page = 3
|
||||
var card_instance_map = {}
|
||||
var hovering_card_index = -1
|
||||
var mouse_over_any_card = false
|
||||
var game: ChessGame
|
||||
var player: Player
|
||||
|
||||
var card_costs = Utils.TokenCosts
|
||||
|
||||
var fibonacci = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
|
||||
|
||||
func _ready():
|
||||
if back_button:
|
||||
back_button.connect("pressed", Callable(self, "_on_back_button_pressed"))
|
||||
|
||||
if buy_button:
|
||||
buy_button.connect("pressed", Callable(self, "_on_buy_button_pressed"))
|
||||
|
||||
if increase_hand_size_button:
|
||||
increase_hand_size_button.connect("pressed", Callable(self, "_on_increase_hand_size_pressed"))
|
||||
|
||||
if left_button:
|
||||
left_button.pressed.connect(_on_left_button_pressed)
|
||||
|
||||
if right_button:
|
||||
right_button.pressed.connect(_on_right_button_pressed)
|
||||
|
||||
# Initialize with empty shop
|
||||
if card_preview:
|
||||
card_preview.visible = false
|
||||
|
||||
update_token_display()
|
||||
update_buy_button()
|
||||
update_navigation_buttons()
|
||||
update_hand_size_display()
|
||||
|
||||
func initialize(options = null):
|
||||
carousel_page = 0
|
||||
selected_index = 0
|
||||
|
||||
game = get_node_or_null("/root/Board") as ChessGame
|
||||
if game and "player" in game:
|
||||
player = game.player
|
||||
player_tokens = player.tokens
|
||||
|
||||
# Generate shop cards if not provided
|
||||
if available_cards.is_empty() and game and "deckManager" in game:
|
||||
var deck_manager = game.deckManager
|
||||
available_cards = generate_shop_cards(deck_manager)
|
||||
|
||||
# Update display
|
||||
update_token_display()
|
||||
update_hand_size_display()
|
||||
populate_carousel()
|
||||
update_navigation_buttons()
|
||||
|
||||
if not available_cards.is_empty():
|
||||
select_card(0) # Select the first card by default
|
||||
|
||||
func generate_shop_cards(deck_manager):
|
||||
var shop_cards = []
|
||||
var all_available_cards = []
|
||||
|
||||
for rank in player.cards_by_rank:
|
||||
for card in player.cards_by_rank[rank]:
|
||||
var is_unlocked = false
|
||||
for unlocked_card in player.unlocked_cards:
|
||||
if unlocked_card.cardName == card.cardName:
|
||||
is_unlocked = true
|
||||
break
|
||||
|
||||
if not is_unlocked:
|
||||
var card_instance = card.duplicate()
|
||||
all_available_cards.append(card_instance)
|
||||
|
||||
if all_available_cards.is_empty():
|
||||
print("No new cards available for the shop")
|
||||
return []
|
||||
|
||||
all_available_cards.shuffle()
|
||||
|
||||
var num_shop_cards = min(randi_range(5, 7), all_available_cards.size())
|
||||
|
||||
for i in range(num_shop_cards):
|
||||
shop_cards.append(all_available_cards[i])
|
||||
|
||||
print("Generated shop with " + str(shop_cards.size()) + " cards")
|
||||
return shop_cards
|
||||
|
||||
func populate_carousel():
|
||||
if card_carousel:
|
||||
for child in card_carousel.get_children():
|
||||
child.queue_free()
|
||||
|
||||
card_instance_map.clear()
|
||||
|
||||
var start_index = carousel_page * cards_per_page
|
||||
var end_index = min(start_index + cards_per_page, available_cards.size())
|
||||
|
||||
card_carousel.add_theme_constant_override("separation", 20)
|
||||
|
||||
for i in range(start_index, end_index):
|
||||
var card = available_cards[i]
|
||||
var card_visual = preload("res://card_visual.tscn").instantiate()
|
||||
|
||||
card_carousel.add_child(card_visual)
|
||||
card_visual.set_card(card)
|
||||
|
||||
var token_cost = get_card_cost(card)
|
||||
card_visual.set_price(token_cost)
|
||||
card_visual.set_string("tokens")
|
||||
|
||||
var instance_id = card_visual.get_instance_id()
|
||||
|
||||
card_instance_map[instance_id] = {
|
||||
"card_index": i,
|
||||
"card": card
|
||||
}
|
||||
|
||||
update_navigation_buttons()
|
||||
|
||||
func update_navigation_buttons():
|
||||
if left_button:
|
||||
left_button.disabled = (carousel_page <= 0)
|
||||
|
||||
if right_button:
|
||||
var max_page = ceil(float(available_cards.size()) / cards_per_page) - 1
|
||||
right_button.disabled = (carousel_page >= max_page or available_cards.size() <= cards_per_page)
|
||||
|
||||
func select_card(index):
|
||||
if index < 0 or index >= available_cards.size():
|
||||
return
|
||||
|
||||
var page_for_index = int(index / cards_per_page)
|
||||
if page_for_index != carousel_page:
|
||||
carousel_page = page_for_index
|
||||
populate_carousel()
|
||||
|
||||
selected_index = index
|
||||
selected_card = available_cards[index]
|
||||
|
||||
var visual_index = index % cards_per_page
|
||||
|
||||
for i in range(card_carousel.get_child_count()):
|
||||
var card_visual = card_carousel.get_child(i)
|
||||
card_visual.set_selected(i == visual_index)
|
||||
|
||||
if card_preview and selected_card:
|
||||
card_preview.preview_card(selected_card)
|
||||
card_preview.visible = true
|
||||
|
||||
update_buy_button()
|
||||
|
||||
func update_buy_button():
|
||||
if not buy_button or not selected_card:
|
||||
return
|
||||
|
||||
var cost = get_card_cost(selected_card)
|
||||
buy_button.text = "BUY (" + str(cost) + " tokens)"
|
||||
buy_button.disabled = cost > player_tokens
|
||||
|
||||
func get_card_cost(card):
|
||||
if not card:
|
||||
return 0
|
||||
if card.rank in card_costs:
|
||||
return card_costs[card.rank]
|
||||
return 10
|
||||
|
||||
func update_token_display():
|
||||
if token_label:
|
||||
token_label.text = str(player_tokens) + " TOKENS"
|
||||
|
||||
func update_hand_size_display():
|
||||
if not player or not hand_size_label or not hand_size_cost_label:
|
||||
return
|
||||
|
||||
hand_size_label.text = "HAND SIZE: " + str(player.hand_size)
|
||||
|
||||
# Calculate cost using Fibonacci sequence
|
||||
var fib_index = max(player.hand_size - 2, 1) + 2 # Start at index 2 (which is 1)
|
||||
if fib_index >= fibonacci.size():
|
||||
fib_index = fibonacci.size() - 1
|
||||
|
||||
var cost = fibonacci[fib_index]
|
||||
hand_size_cost_label.text = "Cost: " + str(cost) + " tokens"
|
||||
|
||||
if increase_hand_size_button:
|
||||
increase_hand_size_button.disabled = cost > player_tokens
|
||||
|
||||
func purchase_selected_card():
|
||||
if not selected_card or not player:
|
||||
return false
|
||||
|
||||
var cost = get_card_cost(selected_card)
|
||||
|
||||
if player_tokens < cost:
|
||||
print("Not enough tokens")
|
||||
return false
|
||||
|
||||
player_tokens -= cost
|
||||
player.tokens = player_tokens
|
||||
|
||||
var purchased_card = selected_card.duplicate()
|
||||
available_cards.remove_at(selected_index)
|
||||
|
||||
emit_signal("card_unlocked", purchased_card, cost)
|
||||
|
||||
update_token_display()
|
||||
|
||||
var max_page = max(0, ceil(float(available_cards.size()) / cards_per_page) - 1)
|
||||
|
||||
if carousel_page > max_page:
|
||||
carousel_page = max_page
|
||||
|
||||
populate_carousel()
|
||||
|
||||
if not available_cards.is_empty():
|
||||
var new_index = min(selected_index, available_cards.size() - 1)
|
||||
select_card(new_index)
|
||||
else:
|
||||
selected_card = null
|
||||
selected_index = -1
|
||||
if card_preview:
|
||||
card_preview.hide_preview()
|
||||
|
||||
buy_button.disabled = true
|
||||
|
||||
update_buy_button()
|
||||
return true
|
||||
|
||||
func increase_hand_size():
|
||||
if not player:
|
||||
return false
|
||||
|
||||
# Calculate cost using Fibonacci sequence
|
||||
var fib_index = max(player.hand_size - 2, 1) + 2
|
||||
if fib_index >= fibonacci.size():
|
||||
fib_index = fibonacci.size() - 1
|
||||
|
||||
var cost = fibonacci[fib_index]
|
||||
|
||||
if player_tokens < cost:
|
||||
print("Not enough tokens for hand size increase")
|
||||
return false
|
||||
if player.hand_size >= player.MAX_HAND_SIZE:
|
||||
print("Hand reached max size")
|
||||
return false
|
||||
player_tokens -= cost
|
||||
player.tokens = player_tokens
|
||||
player.set_hand_size(player.hand_size + 1)
|
||||
|
||||
update_token_display()
|
||||
update_hand_size_display()
|
||||
update_buy_button()
|
||||
|
||||
emit_signal("hand_size_increased")
|
||||
return true
|
||||
|
||||
# Signal handlers
|
||||
func _on_back_button_pressed():
|
||||
emit_signal("back_pressed")
|
||||
visible = false
|
||||
|
||||
func _on_buy_button_pressed():
|
||||
purchase_selected_card()
|
||||
|
||||
func _on_increase_hand_size_pressed():
|
||||
increase_hand_size()
|
||||
|
||||
func _on_left_button_pressed():
|
||||
if carousel_page > 0:
|
||||
carousel_page -= 1
|
||||
var new_index = carousel_page * cards_per_page
|
||||
populate_carousel()
|
||||
select_card(new_index)
|
||||
|
||||
func _on_right_button_pressed():
|
||||
var max_page = ceil(float(available_cards.size()) / cards_per_page) - 1
|
||||
if carousel_page < max_page:
|
||||
carousel_page += 1
|
||||
var new_index = carousel_page * cards_per_page
|
||||
populate_carousel()
|
||||
select_card(new_index)
|
||||
|
||||
func _on_card_visual_pressed(index):
|
||||
select_card(index)
|
||||
|
||||
func _on_card_visual_hover(card, index):
|
||||
if card_preview and card:
|
||||
card_preview.preview_card(card)
|
||||
card_preview.visible = true
|
||||
select_card(index)
|
||||
|
||||
func _on_card_visual_exit():
|
||||
if card_preview and selected_card:
|
||||
card_preview.preview_card(selected_card)
|
||||
elif card_preview:
|
||||
card_preview.hide_preview()
|
||||
|
||||
func _process(delta):
|
||||
if not visible:
|
||||
return
|
||||
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
|
||||
# Reset tracking
|
||||
var old_hovering_index = hovering_card_index
|
||||
hovering_card_index = -1
|
||||
mouse_over_any_card = false
|
||||
var mouse_clicked = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
|
||||
|
||||
for i in range(card_carousel.get_child_count()):
|
||||
var card_visual = card_carousel.get_child(i)
|
||||
var card_rect = card_visual.get_global_rect()
|
||||
|
||||
if card_rect.has_point(mouse_pos) and mouse_clicked:
|
||||
# Get data from instance map
|
||||
var instance_id = card_visual.get_instance_id()
|
||||
if card_instance_map.has(instance_id):
|
||||
var data = card_instance_map[instance_id]
|
||||
hovering_card_index = data.card_index
|
||||
mouse_over_any_card = true
|
||||
|
||||
# If we just started hovering this card
|
||||
if old_hovering_index != hovering_card_index:
|
||||
_on_card_visual_hover(data.card, data.card_index)
|
||||
|
||||
# Also trigger the card's own hover effect
|
||||
card_visual._on_mouse_entered()
|
||||
|
||||
break
|
||||
|
||||
# If we were hovering a card before but not now, trigger exit
|
||||
if old_hovering_index != -1 and hovering_card_index == -1:
|
||||
_on_card_visual_exit()
|
||||
|
||||
# Find the previously hovered card by index
|
||||
for i in range(card_carousel.get_child_count()):
|
||||
var card_visual = card_carousel.get_child(i)
|
||||
var instance_id = card_visual.get_instance_id()
|
||||
if card_instance_map.has(instance_id) and card_instance_map[instance_id].card_index == old_hovering_index:
|
||||
card_visual._on_mouse_exited()
|
||||
break
|
||||
|
||||
|
||||
|
||||
# Notification
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_VISIBILITY_CHANGED:
|
||||
_on_visibility_changed(visible)
|
||||
|
||||
func _on_visibility_changed(is_visible):
|
||||
|
||||
print("LobbyShop visibility changed to: ", is_visible)
|
||||
if is_visible:
|
||||
update_token_display()
|
||||
update_hand_size_display()
|
||||
update_buy_button()
|
||||
else:
|
||||
print("LobbyShop is now invisible")
|
||||
|
||||
emit_signal("lobby_shop_visibility_changed", is_visible)
|
||||
1
Systems/Game/Lobby/LobbyShopScreen.gd.uid
Normal file
1
Systems/Game/Lobby/LobbyShopScreen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://nrd5mq0tfmur
|
||||
108
Systems/Game/Map/DotPatternGenerator.gd
Normal file
108
Systems/Game/Map/DotPatternGenerator.gd
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
extends Control
|
||||
class_name DotPatternGenerator
|
||||
# Dot pattern settings
|
||||
const DOT_SPACING = 30
|
||||
const DOT_SIZE = 2
|
||||
const DOT_COLOR = Color(0.3, 0.3, 0.3, 0.5)
|
||||
const PATH_DOT_COLOR = Color(0.4, 0.4, 0.6, 0.8)
|
||||
const PATH_DOT_SIZE = 3
|
||||
const HIGHLIGHT_RADIUS = 150
|
||||
|
||||
# For path highlights
|
||||
var map_screen = null
|
||||
var highlight_dots = false
|
||||
|
||||
func _init(highlight: bool = false):
|
||||
highlight_dots = highlight
|
||||
# Don't process input
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
func _ready():
|
||||
# Apply the pattern
|
||||
generate_dots()
|
||||
|
||||
# Find map screen if we're highlighting paths
|
||||
if highlight_dots:
|
||||
map_screen = find_map_screen()
|
||||
|
||||
func find_map_screen():
|
||||
var parent = get_parent()
|
||||
while parent != null:
|
||||
if parent is MapScreen:
|
||||
return parent
|
||||
parent = parent.get_parent()
|
||||
return null
|
||||
|
||||
func generate_dots():
|
||||
# Clear existing dots
|
||||
for child in get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Get the size of this control
|
||||
var control_size = size
|
||||
|
||||
# Calculate how many dots we need
|
||||
var cols = int(control_size.x / DOT_SPACING) + 1
|
||||
var rows = int(control_size.y / DOT_SPACING) + 1
|
||||
|
||||
# Create dots
|
||||
for i in range(rows):
|
||||
for j in range(cols):
|
||||
var dot_position = Vector2(j * DOT_SPACING, i * DOT_SPACING)
|
||||
var on_path = false
|
||||
|
||||
# If highlighting, check if dot is near a path
|
||||
if highlight_dots and map_screen != null:
|
||||
on_path = is_near_path(dot_position)
|
||||
|
||||
# Create the dot with appropriate style
|
||||
var dot = ColorRect.new()
|
||||
dot.size = Vector2(DOT_SIZE, DOT_SIZE) if not on_path else Vector2(PATH_DOT_SIZE, PATH_DOT_SIZE)
|
||||
dot.color = DOT_COLOR if not on_path else PATH_DOT_COLOR
|
||||
dot.position = dot_position - (dot.size / 2)
|
||||
add_child(dot)
|
||||
|
||||
func is_near_path(position: Vector2) -> bool:
|
||||
# If no map screen, can't check paths
|
||||
if map_screen == null:
|
||||
return false
|
||||
|
||||
# Get connections from map screen
|
||||
for line in map_screen.connection_lines:
|
||||
if line.get_point_count() < 2:
|
||||
continue
|
||||
|
||||
# Check distance to line segment
|
||||
var start = line.get_point_position(0)
|
||||
var end = line.get_point_position(1)
|
||||
|
||||
# Adjust for potential offset in the map container
|
||||
start += map_screen.map_container.position
|
||||
end += map_screen.map_container.position
|
||||
|
||||
# Check if point is near the line
|
||||
var distance = distance_to_line_segment(position, start, end)
|
||||
if distance < HIGHLIGHT_RADIUS:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
# Calculate distance from point to line segment
|
||||
func distance_to_line_segment(point: Vector2, line_start: Vector2, line_end: Vector2) -> float:
|
||||
var line_vec = line_end - line_start
|
||||
var point_vec = point - line_start
|
||||
|
||||
var line_length_squared = line_vec.length_squared()
|
||||
if line_length_squared == 0:
|
||||
return point_vec.length() # Line segment is a point
|
||||
|
||||
# Calculate projection of point onto line
|
||||
var t = max(0, min(1, point_vec.dot(line_vec) / line_length_squared))
|
||||
var projection = line_start + t * line_vec
|
||||
|
||||
# Return distance from point to projection
|
||||
return (point - projection).length()
|
||||
|
||||
func update_dots():
|
||||
if highlight_dots and map_screen != null:
|
||||
generate_dots()
|
||||
1
Systems/Game/Map/DotPatternGenerator.gd.uid
Normal file
1
Systems/Game/Map/DotPatternGenerator.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://6cmhvsug8nbv
|
||||
795
Systems/Game/Map/MapGenerator.gd
Normal file
795
Systems/Game/Map/MapGenerator.gd
Normal file
|
|
@ -0,0 +1,795 @@
|
|||
extends RefCounted
|
||||
class_name ChessMapGenerator
|
||||
|
||||
|
||||
|
||||
var min_levels = 5
|
||||
var max_levels = 6
|
||||
var max_connections_per_node = 4
|
||||
|
||||
var min_nodes_per_level = 1
|
||||
var max_nodes_per_level = 4
|
||||
var positions_per_level = 4
|
||||
var starting_elo = 1000
|
||||
var final_elo = 2100
|
||||
var current_max_level = 0;
|
||||
# var level_unit_distribution = ["", "", "", "nkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr", "rbnqknbnr", "rnbnqknbnr", "rnbnqknbnbr", "rbnbnqknbnbr"]
|
||||
# var level_unit_distribution = ["", "nk", "nkr", "rnkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr"]
|
||||
|
||||
var level_unit_distribution = ["", "nk", "nkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr"]
|
||||
# ["", "nk", "nkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr", "rbnqknbnr", "rnbnqknbnr", "rnbnqknbnnr", "rnnbnqknbnnr"]
|
||||
# ["", "", "nkr", "rnkr", "rnkr", "rnkbr", "rnqkbr", "rnqkbnr", "rnbqkbnr", "rbnqknbnr", "rnbnqknbnr", "rnbnqknbnbr", "rbnbnqknbnbr"]
|
||||
var _rng = RandomNumberGenerator.new()
|
||||
var _next_id = 0
|
||||
|
||||
func _init(seed_value = null):
|
||||
# Set seed for reproducible maps
|
||||
if seed_value != null:
|
||||
_rng.seed = seed_value
|
||||
else:
|
||||
_rng.randomize()
|
||||
|
||||
func change_preset_mode(mode, player):
|
||||
# var game = get_node_or_null("/root/Board") as ChessGame
|
||||
# var player = game.player
|
||||
var run_count = player.run_count
|
||||
|
||||
match mode:
|
||||
"vanilla":
|
||||
min_levels = 5
|
||||
max_levels = 6
|
||||
var elo_step = Utils.VANILLA_ELO_STEP
|
||||
var base_elo = Utils.MIN_ELO + run_count * elo_step
|
||||
starting_elo = clamp(base_elo, Utils.MIN_ELO, Utils.MAX_ELO)
|
||||
final_elo = starting_elo + elo_step
|
||||
"deeper":
|
||||
min_levels = 10
|
||||
max_levels = 16
|
||||
var elo_step = Utils.DEEPER_ELO_STEP
|
||||
var base_elo = Utils.MIN_ELO + run_count * elo_step
|
||||
starting_elo = clamp(base_elo, Utils.MIN_ELO, Utils.MAX_ELO)
|
||||
final_elo = starting_elo + elo_step
|
||||
_:
|
||||
min_levels = 5
|
||||
max_levels = 6
|
||||
var elo_step = Utils.VANILLA_ELO_STEP
|
||||
var base_elo = Utils.MIN_ELO + run_count * elo_step
|
||||
starting_elo = clamp(base_elo, Utils.MIN_ELO, Utils.MAX_ELO)
|
||||
final_elo = starting_elo + elo_step
|
||||
|
||||
|
||||
|
||||
func generate_map(mode, player):
|
||||
var isVanilla = mode == "vanilla"
|
||||
change_preset_mode(mode, player)
|
||||
var nodes = []
|
||||
var connections = []
|
||||
_next_id = 0
|
||||
|
||||
var num_levels = _rng.randi_range(min_levels, max_levels)
|
||||
current_max_level = num_levels;
|
||||
var elo_step = float(final_elo - starting_elo) / (num_levels - 1)
|
||||
|
||||
var start_node = {
|
||||
"id": _get_next_id(),
|
||||
"type": Utils.RoomType.STARTING,
|
||||
"level": 0,
|
||||
"position": Vector2(3, 0),
|
||||
"metadata": {
|
||||
"is_escape": false,
|
||||
},
|
||||
"elo": starting_elo
|
||||
}
|
||||
nodes.append(start_node)
|
||||
|
||||
# Create final boss node
|
||||
var final_node = {
|
||||
"id": _get_next_id(),
|
||||
"type": Utils.RoomType.FINAL,
|
||||
"level": num_levels - 1,
|
||||
"position": Vector2(3, num_levels - 1),
|
||||
"metadata": {},
|
||||
"elo": final_elo
|
||||
}
|
||||
# final_node.metadata = generate_final_data(final_node)
|
||||
final_node = generate_node_data(final_node, player)
|
||||
# print("final_node ====", final_node)
|
||||
nodes.append(final_node)
|
||||
|
||||
var levels_nodes = {0: [start_node], (num_levels - 1): [final_node]}
|
||||
|
||||
for level in range(1, num_levels - 1):
|
||||
var level_nodes = []
|
||||
var level_elo = starting_elo + (elo_step * level)
|
||||
# Change this so that its more weighted towards the lower end with rare occurances of max_nodes_per_level rather than an even split
|
||||
var num_nodes = get_weighted_node_count(min_nodes_per_level, max_nodes_per_level)
|
||||
var available_positions = []
|
||||
for pos in range(positions_per_level):
|
||||
available_positions.append(pos)
|
||||
available_positions.shuffle()
|
||||
|
||||
|
||||
for i in range(num_nodes):
|
||||
var node_type = _get_random_room_type(level, num_levels)
|
||||
var node = {
|
||||
"id": _get_next_id(),
|
||||
"type": node_type,
|
||||
"level": level,
|
||||
"position": Vector2(available_positions[i], level),
|
||||
"elo": level_elo,
|
||||
"metadata": {}
|
||||
}
|
||||
node = generate_node_data(node, player)
|
||||
nodes.append(node)
|
||||
level_nodes.append(node)
|
||||
|
||||
levels_nodes[level] = level_nodes
|
||||
|
||||
# Connect nodes between levels
|
||||
# First connect starting node to level 1
|
||||
if levels_nodes.has(1) and levels_nodes[1].size() > 0:
|
||||
var num_connections = min(_rng.randi_range(2, 3), levels_nodes[1].size())
|
||||
var targets = levels_nodes[1].duplicate()
|
||||
targets.shuffle()
|
||||
|
||||
for i in range(num_connections):
|
||||
connections.append({
|
||||
"from": start_node.id,
|
||||
"to": targets[i].id
|
||||
})
|
||||
|
||||
# Keep track of which nodes are connected
|
||||
var connected_nodes = [start_node.id]
|
||||
for level in range(1, num_levels - 1):
|
||||
if not levels_nodes.has(level) or not levels_nodes.has(level + 1):
|
||||
continue
|
||||
|
||||
var current_level_nodes = levels_nodes[level]
|
||||
var next_level_nodes = levels_nodes[level + 1].duplicate()
|
||||
next_level_nodes.shuffle()
|
||||
|
||||
# For each node in current level that is connected from previous level
|
||||
for node in current_level_nodes:
|
||||
if _is_node_connected_to(node.id, connections):
|
||||
# Add to connected nodes
|
||||
connected_nodes.append(node.id)
|
||||
|
||||
# Connect to 1-2 nodes in next level if not the final level
|
||||
if level < num_levels - 2:
|
||||
var num_next_connections = _rng.randi_range(1, max_connections_per_node)
|
||||
num_next_connections = min(num_next_connections, next_level_nodes.size())
|
||||
var shouldTrim = _rng.randi_range(1, 15) > 3 || num_next_connections > 3
|
||||
for i in range(num_next_connections):
|
||||
if i < next_level_nodes.size():
|
||||
connections.append({
|
||||
"from": node.id,
|
||||
"to": next_level_nodes[i].id
|
||||
})
|
||||
|
||||
# # Remove the selected nodes so they aren't picked again
|
||||
# if lots of ocnnection earlier and deeper than lvl 3
|
||||
# if num_next_connections >= 2 && level > 3:
|
||||
if shouldTrim:
|
||||
for i in range(num_next_connections):
|
||||
if next_level_nodes.size() > 0:
|
||||
next_level_nodes.pop_front()
|
||||
|
||||
# Connect to final boss if at the level before
|
||||
elif level == num_levels - 2:
|
||||
connections.append({
|
||||
"from": node.id,
|
||||
"to": final_node.id
|
||||
})
|
||||
|
||||
# Remove any nodes that aren't connected
|
||||
var valid_nodes = []
|
||||
for node in nodes:
|
||||
if connected_nodes.has(node.id) or node.id == final_node.id:
|
||||
valid_nodes.append(node)
|
||||
|
||||
# Clean up connections to removed nodes
|
||||
var valid_connections = []
|
||||
for conn in connections:
|
||||
var from_valid = false
|
||||
var to_valid = false
|
||||
|
||||
for node in valid_nodes:
|
||||
if node.id == conn.from:
|
||||
from_valid = true
|
||||
if node.id == conn.to:
|
||||
to_valid = true
|
||||
|
||||
if from_valid and to_valid:
|
||||
valid_connections.append(conn)
|
||||
var index = 0
|
||||
for valid_node in valid_nodes:
|
||||
var isLeaf = true;
|
||||
for connection in valid_connections:
|
||||
# if theres outgoing connection we arent at a dead end
|
||||
if connection.from == valid_node.id:
|
||||
isLeaf = false
|
||||
break;
|
||||
valid_node.metadata.is_escape = isLeaf
|
||||
valid_nodes[index] = valid_node;
|
||||
index += 1
|
||||
|
||||
return {
|
||||
"nodes": valid_nodes,
|
||||
"connections": valid_connections,
|
||||
"levels": num_levels,
|
||||
"seed": _rng.seed
|
||||
}
|
||||
|
||||
func _get_random_room_type(level, total_levels):
|
||||
# return Utils.RoomType.BOSS
|
||||
var boss_chance = 0.1 + (level / float(total_levels) * 0.1)
|
||||
var shop_chance = 0.1 + (level / float(total_levels) * 0.05)
|
||||
var event_chance = 0.05
|
||||
|
||||
if level == total_levels - 2:
|
||||
boss_chance += 0.3
|
||||
|
||||
var roll = _rng.randf()
|
||||
if roll < boss_chance:
|
||||
return Utils.RoomType.BOSS
|
||||
elif roll < boss_chance + shop_chance:
|
||||
return Utils.RoomType.SHOP
|
||||
elif roll < boss_chance + shop_chance + event_chance:
|
||||
return Utils.RoomType.EVENT
|
||||
else:
|
||||
return Utils.RoomType.NORMAL
|
||||
|
||||
func _is_node_connected_to(node_id, connections):
|
||||
for conn in connections:
|
||||
if conn.to == node_id:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _get_next_id():
|
||||
var id = _next_id
|
||||
_next_id += 1
|
||||
return id
|
||||
|
||||
|
||||
|
||||
func get_weighted_node_count(min_count, max_count):
|
||||
# Use exponential distribution to weight toward lower values
|
||||
var range_size = max_count - min_count + 1
|
||||
|
||||
var rand_val = _rng.randf()
|
||||
|
||||
# Apply exponential weighting (higher exponent = more weight toward minimum)
|
||||
var weight_exponent = 2 # Adjust this to control the curve
|
||||
var weighted_val = pow(rand_val, weight_exponent)
|
||||
|
||||
var node_count = min_count + floor(weighted_val * range_size)
|
||||
|
||||
if rand_val > 0.70:
|
||||
node_count = max_count
|
||||
|
||||
return node_count
|
||||
|
||||
func generate_node_data(node, player):
|
||||
var data = {
|
||||
"id": node.id,
|
||||
"type": node.type,
|
||||
"level": node.level,
|
||||
"position": node.position,
|
||||
"elo": node.elo,
|
||||
"metadata": node.metadata
|
||||
}
|
||||
|
||||
match data.type:
|
||||
Utils.RoomType.STARTING:
|
||||
data.metadata = generate_starting_data(data, player)
|
||||
Utils.RoomType.NORMAL:
|
||||
data.metadata = generate_chess_data(data, player)
|
||||
Utils.RoomType.BOSS:
|
||||
data.metadata = generate_boss_data(data, player)
|
||||
Utils.RoomType.FINAL:
|
||||
data.metadata = generate_final_data(data, player)
|
||||
Utils.RoomType.SHOP:
|
||||
data.metadata = generate_shop_data(data, player)
|
||||
Utils.RoomType.EVENT:
|
||||
data.metadata = generate_event_data(data, player)
|
||||
_:
|
||||
data.metadata = {}
|
||||
return data
|
||||
|
||||
|
||||
func generate_boss_data(node, player):
|
||||
# level_unit_distribution
|
||||
# current_max_level
|
||||
var rng = float(node.level) / int(current_max_level)
|
||||
var game_type = Utils.BossType.ZERG
|
||||
var height = 6;
|
||||
# (current_value, min_value, max_value, min_index, max_index)
|
||||
var index = map_to_array_index(node.level, 2, current_max_level - 2, 1, level_unit_distribution.size() - 1);
|
||||
var unit_string = level_unit_distribution[index]
|
||||
var special_unit = ""
|
||||
var pawn_string = ""
|
||||
var enemy_unit_depth = 3
|
||||
if game_type == Utils.BossType.ZERG:
|
||||
index = 7
|
||||
height = 7
|
||||
# if index > 7:
|
||||
unit_string = level_unit_distribution[index]
|
||||
elif game_type == Utils.BossType.DOUBLETROUBLE:
|
||||
index = 9
|
||||
height = 9
|
||||
elif game_type == Utils.BossType.WARLARD:
|
||||
index = 10
|
||||
height = 10
|
||||
unit_string = "1rnbqkbnr1"
|
||||
special_unit = "rnnnqkbbbr"
|
||||
# if game_type == "zerg":
|
||||
# index = map_to_array_index(node.level, 5, current_max_level - 2, 1, level_unit_distribution.size() - 1);
|
||||
|
||||
for x in unit_string.length():
|
||||
pawn_string += "p"
|
||||
|
||||
|
||||
var fen = "";
|
||||
# "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
||||
if node.level >= 7 and node.level <= 10:
|
||||
# height = node.level
|
||||
enemy_unit_depth = 3
|
||||
elif node.level > 10:
|
||||
# height = 10
|
||||
enemy_unit_depth = 4
|
||||
for x in height - enemy_unit_depth:
|
||||
if x == 0:
|
||||
fen += unit_string + "/"
|
||||
elif x == 1:
|
||||
fen += pawn_string + "/"
|
||||
# elif x == height - 3:
|
||||
# fen += "u"
|
||||
# # for y in unit_string.length() - 1:
|
||||
# # fen += "*"
|
||||
# fen += "u/"
|
||||
else:
|
||||
fen += str(unit_string.length()) + "/"
|
||||
|
||||
|
||||
if game_type == Utils.BossType.ZERG:
|
||||
for x in enemy_unit_depth - 1:
|
||||
fen += pawn_string.to_upper() + "/"
|
||||
fen += pawn_string.to_upper()
|
||||
elif game_type == Utils.BossType.WARLARD:
|
||||
fen += pawn_string.to_upper() + "/" + special_unit.to_upper()
|
||||
else:
|
||||
fen += pawn_string.to_upper() + "/" + unit_string.to_upper()
|
||||
var fen_ending = " w KQkq - 0 1"
|
||||
# print("generate_chess_data ", fen + fen_ending)
|
||||
# change condition
|
||||
|
||||
var win_condition = Utils.WinConditionType.TURN_NUMBER
|
||||
var win_target_turn = null
|
||||
var loss_target_unit = null
|
||||
var win_target_unit = null
|
||||
var boss_turn_additional = null
|
||||
var loss_condition = Utils.LossConditionType.UNIT_LOST
|
||||
|
||||
if game_type == Utils.BossType.ZERG:
|
||||
win_condition = Utils.WinConditionType.TURN_NUMBER
|
||||
# smallest board = 2 x 6 = 12
|
||||
# largest board = 10 x 12 = 120
|
||||
# smallest turn target 10
|
||||
# largest turn target 50
|
||||
var roomSize = height * unit_string.length()
|
||||
var clamped_value = clamp(roomSize, 12, 120)
|
||||
var percentage = (clamped_value - 12) / (120 - 12)
|
||||
|
||||
win_target_turn = 10 + percentage * (50 - 10)
|
||||
loss_target_unit = "King"
|
||||
loss_condition = Utils.LossConditionType.UNIT_LOST
|
||||
elif game_type == Utils.BossType.DOUBLETROUBLE:
|
||||
boss_turn_additional = 2
|
||||
win_condition = Utils.WinConditionType.CAPTURE_UNIT
|
||||
win_target_unit = "King"
|
||||
loss_target_unit = "King"
|
||||
loss_condition = Utils.LossConditionType.UNIT_LOST
|
||||
return {
|
||||
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
|
||||
"fen": fen + fen_ending,
|
||||
"game_type": "chess",
|
||||
"boss_type": game_type,
|
||||
"has_opponent": true,
|
||||
"boss_turn_additional": boss_turn_additional,
|
||||
"win_condition": win_condition,
|
||||
"win_target_turn": win_target_turn,
|
||||
"win_target_unit": win_target_unit,
|
||||
"loss_target_unit": loss_target_unit,
|
||||
"loss_condition": loss_condition,
|
||||
"reward": {
|
||||
"gold": 50 * node.level,
|
||||
"cards": [],
|
||||
"selection": generate_shop_cards(3, player),
|
||||
"selection_limit": 2
|
||||
},
|
||||
"elo": node.elo,
|
||||
}
|
||||
|
||||
|
||||
func generate_shop_data(node, player):
|
||||
var num_shop_cards = min(randi_range(5, 7), player.unlocked_cards.size())
|
||||
return {
|
||||
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
|
||||
"cards": generate_shop_cards(num_shop_cards, player),
|
||||
}
|
||||
|
||||
func generate_shop_cards(num_shop_cards, player):
|
||||
var shop_cards = []
|
||||
|
||||
var all_cards = []
|
||||
|
||||
for card_class in player.unlocked_cards:
|
||||
var card = create_new_card_instance(card_class)
|
||||
all_cards.append(card)
|
||||
|
||||
all_cards.shuffle()
|
||||
|
||||
|
||||
for i in range(num_shop_cards):
|
||||
shop_cards.append(all_cards[i % player.unlocked_cards.size()])
|
||||
|
||||
return shop_cards
|
||||
|
||||
|
||||
func generate_starting_data(node, player):
|
||||
return {
|
||||
"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
||||
"elo": node.elo,
|
||||
}
|
||||
|
||||
func generate_event_data(node, player):
|
||||
var index = map_to_array_index(node.level, 2, current_max_level - 2, 1, level_unit_distribution.size() - 1);
|
||||
var unit_string = level_unit_distribution[index]
|
||||
|
||||
var height = 6
|
||||
var width = unit_string.length()
|
||||
if node.level > 7 and node.level <= 10:
|
||||
height = node.level
|
||||
elif node.level > 10:
|
||||
height = 10
|
||||
# var mazedata = generate_maze(width, height)
|
||||
var mazedata = generate_maze(12, 12)
|
||||
# print("**************************")
|
||||
# print("**************************")
|
||||
# print("**************************")
|
||||
# print(mazedata)
|
||||
# print("**************************")
|
||||
# print("**************************")
|
||||
# print("**************************")
|
||||
return {
|
||||
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
|
||||
"fen": mazedata.fen + " w KQkq - 0 1",
|
||||
"game_type": "maze",
|
||||
"has_opponent": false,
|
||||
"win_condition": Utils.WinConditionType.TILE_REACHED,
|
||||
"win_target": [mazedata.end],
|
||||
"win_target_unit": "King",
|
||||
"elo": node.elo,
|
||||
"reward": {
|
||||
"gold": 150 * node.level,
|
||||
"cards": [],
|
||||
"selection": generate_shop_cards(5, player),
|
||||
"selection_limit": 1
|
||||
},
|
||||
}
|
||||
# "rnbqkbnr1/pppppppp1/9/9/9/9/9/PPPPPPPP1/RNBQKBNR1 w KQkq - 0 1"
|
||||
|
||||
func generate_final_data(node, player):
|
||||
# level_unit_distribution
|
||||
# current_max_level
|
||||
var rng = float(node.level) / int(current_max_level)
|
||||
# print(node.level, " ", 2, " ", current_max_level - 2, " ", 1, " ", level_unit_distribution.size() - 1)
|
||||
var index = map_to_array_index(node.level, 2, current_max_level - 2, 1, level_unit_distribution.size() - 1);
|
||||
# print("generate_chess_data ", index)
|
||||
var unit_string = level_unit_distribution[index]
|
||||
var pawn_string = ""
|
||||
|
||||
for x in unit_string.length():
|
||||
pawn_string += "p"
|
||||
|
||||
var height = 6;
|
||||
# "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
||||
var fen = "";
|
||||
if node.level > 8 and node.level <= 10:
|
||||
height = node.level
|
||||
elif node.level > 10:
|
||||
height = 10
|
||||
for x in height - 2:
|
||||
if x == 0:
|
||||
fen += unit_string + "/"
|
||||
elif x == 1:
|
||||
fen += pawn_string + "/"
|
||||
# elif x == height - 3:
|
||||
# fen += "u"
|
||||
# # for y in unit_string.length() - 1:
|
||||
# # fen += "*"
|
||||
# fen += "u/"
|
||||
else:
|
||||
fen += str(unit_string.length()) + "/"
|
||||
|
||||
fen += pawn_string.to_upper() + "/" + unit_string.to_upper()
|
||||
var fen_ending = " w KQkq - 0 1"
|
||||
# print("generate_chess_data ", fen + fen_ending)
|
||||
var win_condition = Utils.WinConditionType.CAPTURE_UNIT # Default
|
||||
var loss_condition = Utils.LossConditionType.UNIT_LOST # Default
|
||||
|
||||
# Randomly select a win condition with increasing variety at higher levels
|
||||
var rng_val = _rng.randf()
|
||||
var conditions_pool = [Utils.WinConditionType.CAPTURE_UNIT] # Default pool
|
||||
|
||||
var additional_metadata = {}
|
||||
var result = {
|
||||
"is_escape": true,
|
||||
"fen": fen + fen_ending,
|
||||
"game_type": "chess",
|
||||
"win_condition": win_condition,
|
||||
"loss_condition": loss_condition,
|
||||
"has_opponent": true,
|
||||
"elo": node.elo,
|
||||
"reward": {
|
||||
"gold": 50 * node.level
|
||||
}
|
||||
}
|
||||
# Merge additional metadata
|
||||
for key in additional_metadata:
|
||||
result[key] = additional_metadata[key]
|
||||
|
||||
# print("final_node ", result)
|
||||
return result
|
||||
|
||||
func generate_chess_data(node, player):
|
||||
# level_unit_distribution
|
||||
# current_max_level
|
||||
var rng = float(node.level) / int(current_max_level)
|
||||
# print(node.level, " ", 2, " ", current_max_level - 2, " ", 1, " ", level_unit_distribution.size() - 1)
|
||||
var index = map_to_array_index(node.level, 2, current_max_level - 2, 1, level_unit_distribution.size() - 1);
|
||||
# print("generate_chess_data ", index)
|
||||
var unit_string = level_unit_distribution[index]
|
||||
var pawn_string = ""
|
||||
var height = 6;
|
||||
# unit_string = level_unit_distribution[level_unit_distribution.size() - 1]
|
||||
# height = 8
|
||||
for x in unit_string.length():
|
||||
pawn_string += "p"
|
||||
|
||||
# "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
||||
var fen = "";
|
||||
if node.level > 8 and node.level <= 10:
|
||||
height = node.level
|
||||
elif node.level > 10:
|
||||
height = 10
|
||||
for x in height - 2:
|
||||
if x == 0:
|
||||
fen += unit_string + "/"
|
||||
elif x == 1:
|
||||
fen += pawn_string + "/"
|
||||
# elif x == height - 3:
|
||||
# fen += "u"
|
||||
# # for y in unit_string.length() - 1:
|
||||
# # fen += "*"
|
||||
# fen += "u/"
|
||||
else:
|
||||
fen += str(unit_string.length()) + "/"
|
||||
|
||||
fen += pawn_string.to_upper() + "/" + unit_string.to_upper()
|
||||
var fen_ending = " w KQkq - 0 1"
|
||||
# print("generate_chess_data ", fen + fen_ending)
|
||||
var win_condition = Utils.WinConditionType.CAPTURE_UNIT # Default
|
||||
var loss_condition = Utils.LossConditionType.UNIT_LOST # Default
|
||||
|
||||
# Randomly select a win condition with increasing variety at higher levels
|
||||
var rng_val = _rng.randf()
|
||||
var conditions_pool = [Utils.WinConditionType.CAPTURE_UNIT] # Default pool
|
||||
|
||||
# # Add more varied win conditions at higher levels
|
||||
# if node.level >= 3:
|
||||
# conditions_pool.append(Utils.WinConditionType.BOARD_CLEARED)
|
||||
# if node.level >= 5:
|
||||
# conditions_pool.append(Utils.WinConditionType.TILE_REACHED)
|
||||
# if node.level >= 7:
|
||||
# conditions_pool.append(Utils.WinConditionType.TURN_NUMBER)
|
||||
|
||||
# # For certain special nodes, always use specific conditions
|
||||
# if node.type == Utils.RoomType.EVENT:
|
||||
# win_condition = Utils.WinConditionType.TILE_REACHED
|
||||
# elif node.type == Utils.RoomType.BOSS and node.level > 5:
|
||||
# # Bosses at higher levels have more varied win conditions
|
||||
# win_condition = conditions_pool[_rng.randi() % conditions_pool.size()]
|
||||
# else:
|
||||
# # Regular nodes have weighted randomness
|
||||
# if rng_val < 0.7: # 70% chance of default capture king
|
||||
# win_condition = Utils.WinConditionType.CAPTURE_UNIT
|
||||
# else:
|
||||
# # Pick randomly from the conditions pool
|
||||
# win_condition = conditions_pool[_rng.randi() % conditions_pool.size()]
|
||||
|
||||
# Create additional metadata for specific win conditions
|
||||
var additional_metadata = {}
|
||||
|
||||
# match win_condition:
|
||||
# Utils.WinConditionType.TILE_REACHED:
|
||||
# # Generate target tiles for the Tile win condition
|
||||
# var target_tiles = []
|
||||
# var target_count = 1 # Default to 1 target tile
|
||||
|
||||
# # Determine target unit (default is any piece)
|
||||
# var target_unit = ""
|
||||
# if _rng.randf() < 0.5: # 50% chance of specific piece
|
||||
# var pieces = ["Pawn", "Knight", "Bishop", "Rook", "Queen"]
|
||||
# target_unit = pieces[_rng.randi() % pieces.size()]
|
||||
|
||||
# # Create target tiles at the enemy's back rank
|
||||
# var target_x = _rng.randi() % unit_string.length()
|
||||
# target_tiles.append(str(target_x) + "-0") # Top row
|
||||
|
||||
# additional_metadata["target_tiles"] = target_tiles
|
||||
# additional_metadata["target_unit"] = target_unit
|
||||
|
||||
# Utils.WinConditionType.TURN_NUMBER:
|
||||
# # Set a target turn number
|
||||
# additional_metadata["target_turn"] = (node.level * 2) + _rng.randi_range(5, 10)
|
||||
|
||||
# # Also add a turn limit for loss condition
|
||||
# loss_condition = Utils.LossConditionType.TURN_NUMBER
|
||||
# additional_metadata["turn_limit"] = additional_metadata["target_turn"] + _rng.randi_range(5, 15)
|
||||
|
||||
# Build the result metadata
|
||||
var result = {
|
||||
"is_escape": node.metadata.is_escape if node.metadata.has("is_escape") else false,
|
||||
"fen": fen + fen_ending,
|
||||
"game_type": "chess",
|
||||
"win_condition": win_condition,
|
||||
"loss_condition": loss_condition,
|
||||
"has_opponent": true,
|
||||
"elo": node.elo,
|
||||
"reward": {
|
||||
"gold": 50 * node.level
|
||||
}
|
||||
}
|
||||
|
||||
# Merge additional metadata
|
||||
for key in additional_metadata:
|
||||
result[key] = additional_metadata[key]
|
||||
|
||||
return result
|
||||
|
||||
func map_to_array_index(current_value, min_value, max_value, min_index, max_index):
|
||||
# Ensure the current value is within bounds
|
||||
var clamped_value = clamp(current_value, min_value, max_value)
|
||||
|
||||
# Calculate how far along the input range we are (0.0 to 1.0)
|
||||
var normalized_position = float(clamped_value - min_value) / float(max_value - min_value)
|
||||
|
||||
# Map this to our target index range
|
||||
var index_range = max_index - min_index
|
||||
var mapped_index = min_index + round(normalized_position * index_range)
|
||||
|
||||
# Ensure we're returning an integer within the valid array index range
|
||||
return int(clamp(mapped_index, min_index, max_index))
|
||||
|
||||
|
||||
|
||||
func generate_maze(width: int, height: int) -> Dictionary:
|
||||
# Ensure dimensions are odd to have proper walls
|
||||
if width % 2 == 0:
|
||||
width += 1
|
||||
if height % 2 == 0:
|
||||
height += 1
|
||||
|
||||
# Initialize the maze with all walls
|
||||
var maze = []
|
||||
for y in range(height):
|
||||
var row = []
|
||||
for x in range(width):
|
||||
row.append("*") # * represents wall
|
||||
maze.append(row)
|
||||
|
||||
# Use a recursive backtracking algorithm to generate the maze
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.randomize()
|
||||
|
||||
# Start at a random odd position
|
||||
var start_x = rng.randi_range(0, width/2-1) * 2 + 1
|
||||
var start_y = rng.randi_range(0, height/2-1) * 2 + 1
|
||||
|
||||
# Carve the maze recursively
|
||||
_carve_maze(maze, start_x, start_y, width, height, rng)
|
||||
|
||||
# Pick a random end point (far from start point)
|
||||
var end_x = 0
|
||||
var end_y = 0
|
||||
var max_distance = 0
|
||||
|
||||
for y in range(1, height, 2):
|
||||
for x in range(1, width, 2):
|
||||
if maze[y][x] == " ": # Only consider path cells
|
||||
var distance = abs(x - start_x) + abs(y - start_y)
|
||||
if distance > max_distance:
|
||||
max_distance = distance
|
||||
end_x = x
|
||||
end_y = y
|
||||
|
||||
maze[start_y][start_x] = "k"
|
||||
|
||||
# Mark the end position with a space (keep it as a path)
|
||||
# It's already a space, but we ensure it here
|
||||
maze[end_y][end_x] = " "
|
||||
|
||||
# Convert the maze to a FEN-like string
|
||||
var fen_string = ""
|
||||
for y in range(height):
|
||||
var empty_count = 0
|
||||
for x in range(width):
|
||||
if maze[y][x] == " ": # Empty space
|
||||
empty_count += 1
|
||||
else:
|
||||
if empty_count > 0:
|
||||
fen_string += str(empty_count)
|
||||
empty_count = 0
|
||||
fen_string += maze[y][x]
|
||||
|
||||
# Add any remaining empty count
|
||||
if empty_count > 0:
|
||||
fen_string += str(empty_count)
|
||||
|
||||
# Add row separator (except for the last row)
|
||||
if y < height - 1:
|
||||
fen_string += "/"
|
||||
return {
|
||||
"fen": fen_string,
|
||||
"start": str(start_x) + "-"+ str(start_y),
|
||||
"end": str(end_x) + "-"+ str(end_y)
|
||||
}
|
||||
|
||||
# Recursive function to carve out the maze
|
||||
func _carve_maze(maze, x, y, width, height, rng):
|
||||
# Mark the current cell as a path
|
||||
maze[y][x] = " "
|
||||
|
||||
# Define the four possible directions (up, right, down, left)
|
||||
var directions = [[0, -2], [2, 0], [0, 2], [-2, 0]]
|
||||
|
||||
# Shuffle directions for randomness
|
||||
directions.shuffle()
|
||||
|
||||
# Try each direction
|
||||
for dir in directions:
|
||||
var nx = x + dir[0]
|
||||
var ny = y + dir[1]
|
||||
|
||||
# Check if the new position is valid and unvisited
|
||||
if nx > 0 and nx < width-1 and ny > 0 and ny < height-1 and maze[ny][nx] == "*":
|
||||
# Carve a path between the current cell and the new cell
|
||||
maze[y + dir[1]/2][x + dir[0]/2] = " "
|
||||
|
||||
# Recursively carve from the new cell
|
||||
_carve_maze(maze, nx, ny, width, height, rng)
|
||||
|
||||
|
||||
|
||||
func create_new_card_instance(template_card: Card) -> Card:
|
||||
var new_card = null
|
||||
|
||||
var script = template_card.get_script()
|
||||
|
||||
if script:
|
||||
new_card = script.new()
|
||||
else:
|
||||
print("Warning: Could not get script from card: " + template_card.cardName)
|
||||
|
||||
var class_list = ProjectSettings.get_global_class_list()
|
||||
var card_class_name = template_card.get_class()
|
||||
|
||||
for class_info in class_list:
|
||||
if class_info["class"] == card_class_name:
|
||||
var card_script = load(class_info["path"])
|
||||
if card_script:
|
||||
new_card = card_script.new()
|
||||
break
|
||||
|
||||
return new_card
|
||||
1
Systems/Game/Map/MapGenerator.gd.uid
Normal file
1
Systems/Game/Map/MapGenerator.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bs0u5jjxm2c18
|
||||
556
Systems/Game/Map/MapScreen.gd
Normal file
556
Systems/Game/Map/MapScreen.gd
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
extends Control
|
||||
class_name MapScreen
|
||||
|
||||
signal back_pressed
|
||||
signal deckmanager_open_requested(options)
|
||||
signal node_selected(node_data)
|
||||
signal map_visibility_changed(is_visible)
|
||||
const MapGenerator = preload("res://Systems/Game/Map/MapGenerator.gd")
|
||||
# Room type constants
|
||||
|
||||
# Node config
|
||||
const NODE_SIZE = Vector2(60, 60)
|
||||
const NODE_SPACING_X = 150
|
||||
const NODE_SPACING_Y = 120
|
||||
const LINE_WIDTH = 3
|
||||
const LINE_COLOR = Color(0.2, 0.2, 0.2)
|
||||
const LINE_COLOR_SELECTED = Color(0.2, 0.6, 0.2)
|
||||
const LINE_COLOR_ACCESSIBLE = Color(0.6, 0.6, 0.2)
|
||||
|
||||
const NODE_COLOR = Color(0.1, 0.1, 0.1, 0.5)
|
||||
const NODE_LEAF = Color(0.2, 0.6, 0.2)
|
||||
# Node symbols and colors
|
||||
const NODE_SYMBOLS = {
|
||||
Utils.RoomType.STARTING: "O", # Circle
|
||||
Utils.RoomType.NORMAL: "■", # Square
|
||||
Utils.RoomType.BOSS: "★", # Star
|
||||
Utils.RoomType.FINAL: "✱", # Burst
|
||||
Utils.RoomType.SHOP: "₵", # Star
|
||||
Utils.RoomType.EVENT: "?" # Burst
|
||||
}
|
||||
|
||||
const NODE_COLORS = {
|
||||
Utils.RoomType.STARTING: Color(1, 1, 1), # White
|
||||
Utils.RoomType.NORMAL: Color(0.2, 0.2, 0.2), # Dark gray
|
||||
Utils.RoomType.BOSS: Color(0.9, 0.2, 0.2), # Red
|
||||
Utils.RoomType.FINAL: Color(0.9, 0.9, 0.2), # Yellow
|
||||
Utils.RoomType.SHOP: Color(0.2, 0.7, 0.9), # Yellow
|
||||
Utils.RoomType.EVENT: Color(0.8, 0.4, 0.9) # Yellow
|
||||
}
|
||||
|
||||
var map_nodes = []
|
||||
var map_connections = []
|
||||
var node_buttons = {}
|
||||
var connection_lines = []
|
||||
var traversed_map = []
|
||||
var current_node = null
|
||||
|
||||
const SCROLL_PADDING_TOP = 80
|
||||
const SCROLL_PADDING_BOTTOM = 80
|
||||
const SCROLL_PADDING_LEFT = 60
|
||||
const SCROLL_PADDING_RIGHT = 100
|
||||
|
||||
var node_popup_scene = preload("res://node_map_popup.tscn")
|
||||
var selected_node_panel: NodePopup = null
|
||||
|
||||
@onready var map_container = $MapScrollContainer/MapContainer
|
||||
@onready var back_button = $BackButton
|
||||
@onready var deck_manager_button = $DeckManagerButton
|
||||
@onready var title_label = $TitleLabel
|
||||
@onready var legend_container = $LegendContainer
|
||||
@onready var gold_label = $GoldLabel
|
||||
|
||||
func _ready():
|
||||
# Connect back button
|
||||
if back_button:
|
||||
back_button.visible = false;
|
||||
back_button.connect("pressed", Callable(self, "_on_back_button_pressed"))
|
||||
if deck_manager_button:
|
||||
deck_manager_button.connect("pressed", Callable(self, "_on_deck_button_pressed"))
|
||||
|
||||
# Create legend
|
||||
create_legend()
|
||||
# generate_map()
|
||||
# display_map()
|
||||
selected_node_panel = node_popup_scene.instantiate()
|
||||
selected_node_panel.visible = false
|
||||
add_child(selected_node_panel)
|
||||
|
||||
if selected_node_panel:
|
||||
selected_node_panel.pressed.connect(on_node_panel_pressed)
|
||||
|
||||
func create_legend():
|
||||
# Create legend for room types
|
||||
var legend_items = [
|
||||
{"type": Utils.RoomType.BOSS, "text": "Boss"},
|
||||
{"type": Utils.RoomType.STARTING, "text": "Starting Room"},
|
||||
{"type": Utils.RoomType.FINAL, "text": "Final Boss"},
|
||||
{"type": Utils.RoomType.NORMAL, "text": "Normal Room"},
|
||||
{"type": Utils.RoomType.SHOP, "text": "Shop"},
|
||||
{"type": Utils.RoomType.EVENT, "text": "Event"},
|
||||
{"type": Utils.RoomType.NORMAL, "text": "Escape Room"}
|
||||
]
|
||||
|
||||
for item in legend_items:
|
||||
var hbox = HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 10)
|
||||
|
||||
var symbol = Label.new()
|
||||
symbol.text = NODE_SYMBOLS[item.type]
|
||||
if item.text != "Escape Room":
|
||||
symbol.add_theme_color_override("font_color", NODE_COLORS[item.type])
|
||||
else:
|
||||
symbol.add_theme_color_override("font_color", NODE_LEAF)
|
||||
symbol.add_theme_font_size_override("font_size", 24)
|
||||
|
||||
var text = Label.new()
|
||||
text.text = item.text
|
||||
|
||||
hbox.add_child(symbol)
|
||||
hbox.add_child(text)
|
||||
legend_container.add_child(hbox)
|
||||
|
||||
func generate_map(options):
|
||||
# {"mode": options.mode}
|
||||
# Clear existing map
|
||||
|
||||
var game = get_node_or_null("/root/Board") as ChessGame
|
||||
var player = game.player
|
||||
map_nodes.clear()
|
||||
map_connections.clear()
|
||||
connection_lines.clear()
|
||||
traversed_map.clear()
|
||||
var mapGen = MapGenerator.new().generate_map(options.mode, player)
|
||||
# Create starting node
|
||||
# var start_node = {
|
||||
# "id": 0,
|
||||
# "type": Utils.RoomType.STARTING,
|
||||
# "level": 0,
|
||||
# "position": Vector2(3, 4) # Column, Row
|
||||
# }
|
||||
# map_nodes.append(start_node)
|
||||
# current_node = start_node
|
||||
|
||||
# # Create final boss node
|
||||
# var final_node = {
|
||||
# "id": 1,
|
||||
# "type": Utils.RoomType.FINAL,
|
||||
# "level": levels - 1,
|
||||
# "position": Vector2(3, 0) # Column, Row
|
||||
# }
|
||||
# map_nodes.append(final_node)
|
||||
for node_data in mapGen.nodes:
|
||||
add_node(
|
||||
node_data.id,
|
||||
node_data.type,
|
||||
node_data.level,
|
||||
node_data.position,
|
||||
node_data.elo,
|
||||
node_data.metadata
|
||||
)
|
||||
|
||||
# Store elo rating if needed
|
||||
map_nodes[map_nodes.size() - 1].elo = node_data.elo
|
||||
|
||||
# Set current node to starting node (id 0)
|
||||
if node_data.id == 0:
|
||||
current_node = map_nodes[map_nodes.size() - 1]
|
||||
|
||||
# Process connections
|
||||
for connection in mapGen.connections:
|
||||
add_connection(connection.from, connection.to)
|
||||
|
||||
|
||||
func add_node(id, type, level, position, elo, metadata):
|
||||
map_nodes.append({
|
||||
"id": id,
|
||||
"type": type,
|
||||
"level": level,
|
||||
"position": position,
|
||||
"elo": elo,
|
||||
"metadata": metadata,
|
||||
})
|
||||
|
||||
func add_connection(from_id, to_id):
|
||||
map_connections.append({
|
||||
"from": from_id,
|
||||
"to": to_id
|
||||
})
|
||||
|
||||
func display_map():
|
||||
# Clear previous display
|
||||
for child in map_container.get_children():
|
||||
child.queue_free()
|
||||
node_buttons.clear()
|
||||
connection_lines = []
|
||||
|
||||
# Calculate map dimensions for proper padding
|
||||
var min_y = 9999
|
||||
var max_y = 0
|
||||
var min_x = 9999
|
||||
var max_x = 0
|
||||
# Find boundaries of the map
|
||||
for node_data in map_nodes:
|
||||
var x_pos = node_data.position.x * NODE_SPACING_X
|
||||
var y_pos = node_data.position.y * NODE_SPACING_Y
|
||||
min_x = min(min_x, x_pos)
|
||||
max_x = max(max_x, x_pos)
|
||||
min_y = min(min_y, y_pos)
|
||||
max_y = max(max_y, y_pos)
|
||||
|
||||
var map_width = max_x - min_x + NODE_SIZE.x * 2 + SCROLL_PADDING_LEFT + SCROLL_PADDING_RIGHT
|
||||
var map_height = max_y - min_y + NODE_SIZE.y * 2 + SCROLL_PADDING_TOP + SCROLL_PADDING_BOTTOM
|
||||
map_container.custom_minimum_size = Vector2(map_width, map_height)
|
||||
|
||||
var top_padding = Control.new()
|
||||
top_padding.custom_minimum_size = Vector2(map_width, SCROLL_PADDING_TOP)
|
||||
top_padding.position = Vector2(0, 0)
|
||||
map_container.add_child(top_padding)
|
||||
|
||||
var bottom_padding = Control.new()
|
||||
bottom_padding.custom_minimum_size = Vector2(map_width, SCROLL_PADDING_BOTTOM)
|
||||
bottom_padding.position = Vector2(0, map_height - SCROLL_PADDING_BOTTOM)
|
||||
map_container.add_child(bottom_padding)
|
||||
|
||||
var left_padding = Control.new()
|
||||
left_padding.custom_minimum_size = Vector2(SCROLL_PADDING_LEFT, map_height)
|
||||
left_padding.position = Vector2(0, 0)
|
||||
map_container.add_child(left_padding)
|
||||
|
||||
var right_padding = Control.new()
|
||||
right_padding.custom_minimum_size = Vector2(SCROLL_PADDING_RIGHT, map_height)
|
||||
right_padding.position = Vector2(map_width - SCROLL_PADDING_RIGHT, 0)
|
||||
map_container.add_child(right_padding)
|
||||
|
||||
|
||||
# Draw connections first (so they're behind nodes)
|
||||
for connection in map_connections:
|
||||
var from_node = get_node_by_id(connection.from)
|
||||
var to_node = get_node_by_id(connection.to)
|
||||
|
||||
if from_node and to_node:
|
||||
draw_curved_connection(from_node, to_node)
|
||||
|
||||
# Draw nodes
|
||||
for node_data in map_nodes:
|
||||
draw_node(node_data)
|
||||
|
||||
|
||||
func get_node_by_id(id):
|
||||
for node in map_nodes:
|
||||
if node.id == id:
|
||||
return node
|
||||
return null
|
||||
|
||||
func draw_node(node_data):
|
||||
var isLeaf = true;
|
||||
for connection in map_connections:
|
||||
# if theres outgoing connection we arent at a dead end
|
||||
# dont change final node colour
|
||||
if connection.from == node_data.id || node_data.id == 1:
|
||||
isLeaf = false
|
||||
break;
|
||||
|
||||
var button = Button.new()
|
||||
button.text = NODE_SYMBOLS[node_data.type]
|
||||
button.custom_minimum_size = NODE_SIZE
|
||||
button.flat = true
|
||||
|
||||
# Add some styling
|
||||
var font = FontFile.new()
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = NODE_COLOR
|
||||
style.corner_radius_top_left = 30
|
||||
style.corner_radius_top_right = 30
|
||||
style.corner_radius_bottom_left = 30
|
||||
style.corner_radius_bottom_right = 30
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
if isLeaf:
|
||||
style.bg_color = NODE_LEAF
|
||||
|
||||
style.border_color = NODE_COLORS[node_data.type]
|
||||
|
||||
button.add_theme_font_size_override("font_size", 28)
|
||||
if isLeaf:
|
||||
button.add_theme_color_override("font_color", NODE_LEAF)
|
||||
# style.bg_color = NODE_LEAF
|
||||
else:
|
||||
button.add_theme_color_override("font_color", NODE_COLORS[node_data.type])
|
||||
button.add_theme_stylebox_override("normal", style)
|
||||
|
||||
# Position the node with top and left padding adjustments
|
||||
var x_pos = node_data.position.x * NODE_SPACING_X + SCROLL_PADDING_LEFT
|
||||
var y_pos = node_data.position.y * NODE_SPACING_Y + SCROLL_PADDING_TOP
|
||||
button.position = Vector2(x_pos, y_pos) - NODE_SIZE/2
|
||||
|
||||
# Store data
|
||||
button.set_meta("node_data", node_data)
|
||||
|
||||
# Connect signal
|
||||
button.pressed.connect(func(): _on_node_pressed(node_data))
|
||||
# Add to map
|
||||
map_container.add_child(button)
|
||||
node_buttons[node_data.id] = button
|
||||
|
||||
# Highlight current node
|
||||
if current_node and node_data.id == current_node.id:
|
||||
highlight_current_node(button)
|
||||
|
||||
func on_node_panel_pressed(id):
|
||||
# print("on_node_panel_pressed ", id )
|
||||
if id != null:
|
||||
|
||||
current_node = null
|
||||
for node in map_nodes:
|
||||
if node.id == id:
|
||||
current_node = node
|
||||
break
|
||||
|
||||
traversed_node(current_node)
|
||||
# Refresh display to update highlights
|
||||
display_map()
|
||||
|
||||
# Emit signal with selected node data
|
||||
emit_signal("node_selected", current_node)
|
||||
selected_node_panel.visible = false;
|
||||
func draw_connection(from_node, to_node):
|
||||
var line = Line2D.new()
|
||||
line.width = LINE_WIDTH
|
||||
line.default_color = LINE_COLOR
|
||||
if from_node.id == current_node.id:
|
||||
line.default_color = LINE_COLOR_ACCESSIBLE
|
||||
elif traversed_map.has(to_node.id) and (traversed_map.has(from_node.id) || from_node.id == 0):
|
||||
line.default_color = LINE_COLOR_SELECTED
|
||||
|
||||
# Calculate start and end positions with top and left padding adjustments
|
||||
var start_pos = from_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
|
||||
var end_pos = to_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
|
||||
|
||||
# Add points
|
||||
line.add_point(start_pos)
|
||||
line.add_point(end_pos)
|
||||
|
||||
# Add to map
|
||||
map_container.add_child(line)
|
||||
connection_lines.append(line)
|
||||
|
||||
func highlight_current_node(button):
|
||||
var style = button.get_theme_stylebox("normal").duplicate()
|
||||
style.bg_color = Color(0.3, 0.3, 0.3, 0.7)
|
||||
style.border_width_left = 4
|
||||
style.border_width_top = 4
|
||||
style.border_width_right = 4
|
||||
style.border_width_bottom = 4
|
||||
button.add_theme_stylebox_override("normal", style)
|
||||
|
||||
func _on_node_pressed(node_data):
|
||||
print("SELECTED NODE", node_data)
|
||||
if is_node_accessible(node_data) || is_node_path_accessible(node_data):
|
||||
# Get node type description
|
||||
var node_name = get_node_type_name(node_data.type)
|
||||
var node_desc = get_node_description(node_data)
|
||||
|
||||
# Position the popup near the clicked node
|
||||
var button_pos = node_buttons[node_data.id].global_position
|
||||
var popup_pos = button_pos + Vector2(NODE_SIZE.x/2, NODE_SIZE.y/2)
|
||||
selected_node_panel.global_position = popup_pos
|
||||
|
||||
# Set popup data and make it visible
|
||||
selected_node_panel.set_card(node_data.id, node_name, node_desc)
|
||||
if traversed_map.has(node_data.id):
|
||||
selected_node_panel._disable_enter()
|
||||
else:
|
||||
selected_node_panel._enable_enter()
|
||||
selected_node_panel.visible = true
|
||||
else:
|
||||
print("Node not accessible from current position", node_data)
|
||||
|
||||
func traversed_node(node_data):
|
||||
traversed_map.append(node_data.id)
|
||||
|
||||
func is_node_path_accessible(node_data):
|
||||
if current_node == null:
|
||||
return node_data.type == Utils.RoomType.STARTING
|
||||
|
||||
# Check if there's a direct connection from current node to the path travelled
|
||||
for id in traversed_map:
|
||||
var cur = get_node_by_id(id)
|
||||
for connection in map_connections:
|
||||
if connection.from == cur.id and connection.to == node_data.id:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func is_node_accessible(node_data):
|
||||
if current_node == null:
|
||||
return node_data.type == Utils.RoomType.STARTING
|
||||
|
||||
# Check if there's a direct connection from current node to target node
|
||||
for connection in map_connections:
|
||||
if connection.from == current_node.id and connection.to == node_data.id:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func _on_back_button_pressed():
|
||||
emit_signal("back_pressed")
|
||||
visible = false
|
||||
|
||||
|
||||
func _on_deck_button_pressed():
|
||||
emit_signal("deckmanager_open_requested", {})
|
||||
visible = false
|
||||
|
||||
func draw_curved_connection(from_node, to_node):
|
||||
var line = Line2D.new()
|
||||
line.width = LINE_WIDTH
|
||||
line.default_color = LINE_COLOR
|
||||
if from_node.id == current_node.id:
|
||||
line.default_color = LINE_COLOR_ACCESSIBLE
|
||||
elif traversed_map.has(to_node.id) and (traversed_map.has(from_node.id) || from_node.id == 0):
|
||||
line.default_color = LINE_COLOR_SELECTED
|
||||
|
||||
var start_pos = from_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
|
||||
var end_pos = to_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
|
||||
|
||||
# Create a bezier curve path
|
||||
var points = []
|
||||
|
||||
# If nodes are on different levels (y positions)
|
||||
if from_node.position.y != to_node.position.y:
|
||||
var mid_y = (start_pos.y + end_pos.y) / 2
|
||||
var control_offset = NODE_SPACING_Y * 0.5 * (1 + abs(from_node.position.x - to_node.position.x) * 0.2)
|
||||
|
||||
var control1 = Vector2(start_pos.x, start_pos.y + control_offset)
|
||||
var control2 = Vector2(end_pos.x, end_pos.y - control_offset)
|
||||
|
||||
# Add more points for a smoother curve
|
||||
for i in range(11): # 0.0, 0.1, 0.2, ..., 1.0
|
||||
var t = i / 10.0
|
||||
var point = cubic_bezier(start_pos, control1, control2, end_pos, t)
|
||||
points.append(point)
|
||||
else:
|
||||
# For nodes on the same level, use a simple curved path
|
||||
var mid_x = (start_pos.x + end_pos.x) / 2
|
||||
var mid_y = start_pos.y
|
||||
var curve_height = NODE_SPACING_Y * 0.3 * (1 + abs(from_node.position.x - to_node.position.x) * 0.1)
|
||||
|
||||
if to_node.position.x > from_node.position.x:
|
||||
mid_y -= curve_height
|
||||
else:
|
||||
mid_y += curve_height
|
||||
|
||||
var mid_point = Vector2(mid_x, mid_y)
|
||||
|
||||
# Create a quadratic bezier curve with just 3 points
|
||||
for i in range(11):
|
||||
var t = i / 10.0
|
||||
var point = quadratic_bezier(start_pos, mid_point, end_pos, t)
|
||||
points.append(point)
|
||||
|
||||
# Add the points to the line
|
||||
for point in points:
|
||||
line.add_point(point)
|
||||
|
||||
# Add to map
|
||||
map_container.add_child(line)
|
||||
connection_lines.append(line)
|
||||
|
||||
func cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float) -> Vector2:
|
||||
var q0 = p0.lerp(p1, t)
|
||||
var q1 = p1.lerp(p2, t)
|
||||
var q2 = p2.lerp(p3, t)
|
||||
|
||||
var r0 = q0.lerp(q1, t)
|
||||
var r1 = q1.lerp(q2, t)
|
||||
|
||||
return r0.lerp(r1, t)
|
||||
|
||||
func quadratic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, t: float) -> Vector2:
|
||||
var q0 = p0.lerp(p1, t)
|
||||
var q1 = p1.lerp(p2, t)
|
||||
|
||||
return q0.lerp(q1, t)
|
||||
|
||||
|
||||
|
||||
|
||||
func get_node_type_name(type):
|
||||
match type:
|
||||
Utils.RoomType.STARTING:
|
||||
return "Starting Room"
|
||||
Utils.RoomType.NORMAL:
|
||||
return "Chess Match"
|
||||
Utils.RoomType.BOSS:
|
||||
return "Boss Battle"
|
||||
Utils.RoomType.FINAL:
|
||||
return "Final Boss"
|
||||
Utils.RoomType.SHOP:
|
||||
return "Card Shop"
|
||||
Utils.RoomType.EVENT:
|
||||
return "Random Event"
|
||||
_:
|
||||
return "Unknown Room"
|
||||
|
||||
|
||||
func get_node_description(node_data):
|
||||
var desc = ""
|
||||
match node_data.type:
|
||||
Utils.RoomType.STARTING:
|
||||
desc = "Begin the dungeon"
|
||||
Utils.RoomType.NORMAL:
|
||||
desc = "A chess match \n\n ELO: " + str(node_data.elo)
|
||||
Utils.RoomType.BOSS:
|
||||
desc = "Maybe add some Boss lore? \n\n ELO: " + str(node_data.elo)
|
||||
Utils.RoomType.FINAL:
|
||||
desc = "BHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHA"
|
||||
Utils.RoomType.SHOP:
|
||||
desc = "Purchase new cards"
|
||||
Utils.RoomType.EVENT:
|
||||
desc = "A random event from pawn zerg to slot machine"
|
||||
_:
|
||||
desc = "An unknown room type."
|
||||
|
||||
# Add additional info if this is a leaf node (dead end)
|
||||
var isLeaf = true
|
||||
for connection in map_connections:
|
||||
if connection.from == node_data.id || node_data.id == 1:
|
||||
isLeaf = false
|
||||
break
|
||||
|
||||
if isLeaf && node_data.type != Utils.RoomType.FINAL:
|
||||
desc += "\n\nWarning: This is an escape node"
|
||||
|
||||
return desc
|
||||
|
||||
|
||||
|
||||
func update_gold_display():
|
||||
var game = get_node_or_null("/root/Board") as ChessGame
|
||||
if gold_label:
|
||||
gold_label.text = str(game.player.get_gold()) + " GOLD"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Notification
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_VISIBILITY_CHANGED:
|
||||
_on_visibility_changed(visible)
|
||||
|
||||
func _on_visibility_changed(is_visible):
|
||||
|
||||
print("MapScreen visibility changed to: ", is_visible)
|
||||
if is_visible:
|
||||
update_gold_display()
|
||||
else:
|
||||
print("MapScreen is now invisible")
|
||||
|
||||
emit_signal("map_visibility_changed", is_visible)
|
||||
1
Systems/Game/Map/MapScreen.gd.uid
Normal file
1
Systems/Game/Map/MapScreen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ckpv3snpg6g34
|
||||
147
Systems/Game/Map/NodeLayoutHelper.gd
Normal file
147
Systems/Game/Map/NodeLayoutHelper.gd
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
extends RefCounted
|
||||
class_name NodeLayoutHelper
|
||||
|
||||
# Configuration
|
||||
var map_width: int = 6 # Available horizontal positions (0-5)
|
||||
var level_height: float = 1.0 # Vertical spacing between levels
|
||||
var node_spread: float = 0.7 # How much to spread nodes horizontally (0-1)
|
||||
var path_smoothness: float = 0.3 # How much to align nodes with their connections (0-1)
|
||||
|
||||
# Internal
|
||||
var _rng = RandomNumberGenerator.new()
|
||||
|
||||
func _init(seed_value: int = 0):
|
||||
if seed_value != 0:
|
||||
_rng.seed = seed_value
|
||||
else:
|
||||
_rng.randomize()
|
||||
|
||||
# Organize node positions for better visual layout
|
||||
func organize_layout(nodes: Array, connections: Array) -> Array:
|
||||
# Clone the nodes so we don't modify the original array
|
||||
var updated_nodes = []
|
||||
for node in nodes:
|
||||
updated_nodes.append(node.duplicate())
|
||||
|
||||
# Group nodes by level
|
||||
var nodes_by_level = {}
|
||||
|
||||
for node in updated_nodes:
|
||||
var level = node.level
|
||||
if not nodes_by_level.has(level):
|
||||
nodes_by_level[level] = []
|
||||
nodes_by_level[level].append(node)
|
||||
|
||||
# Process each level
|
||||
for level in nodes_by_level:
|
||||
var level_nodes = nodes_by_level[level]
|
||||
|
||||
# Skip levels with fixed positions
|
||||
if level == 0 or level == nodes_by_level.size() - 1:
|
||||
continue
|
||||
|
||||
# Calculate optimal positions based on connections
|
||||
for node in level_nodes:
|
||||
_adjust_node_position(node, updated_nodes, connections, nodes_by_level)
|
||||
|
||||
# Resolve overlaps after initial placement
|
||||
for level in nodes_by_level:
|
||||
var level_nodes = nodes_by_level[level]
|
||||
if level_nodes.size() > 1:
|
||||
_resolve_horizontal_overlaps(level_nodes)
|
||||
|
||||
return updated_nodes
|
||||
|
||||
# Adjust node position based on its connections
|
||||
func _adjust_node_position(node, all_nodes, connections, nodes_by_level):
|
||||
# Find all connections to/from this node
|
||||
var connected_from = [] # Nodes in the previous level connecting to this node
|
||||
var connected_to = [] # Nodes in the next level this node connects to
|
||||
|
||||
for conn in connections:
|
||||
if conn.to == node.id:
|
||||
var from_node = _find_node_by_id(all_nodes, conn.from)
|
||||
if from_node and from_node.level == node.level - 1:
|
||||
connected_from.append(from_node)
|
||||
|
||||
if conn.from == node.id:
|
||||
var to_node = _find_node_by_id(all_nodes, conn.to)
|
||||
if to_node and to_node.level == node.level + 1:
|
||||
connected_to.append(to_node)
|
||||
|
||||
# Calculate optimal x position based on connected nodes
|
||||
var optimal_x = node.position.x # Start with current position
|
||||
|
||||
if connected_from.size() > 0 or connected_to.size() > 0:
|
||||
var avg_x = 0.0
|
||||
var total_connected = 0
|
||||
|
||||
# Consider positions of connected nodes
|
||||
for from_node in connected_from:
|
||||
avg_x += from_node.position.x
|
||||
total_connected += 1
|
||||
|
||||
for to_node in connected_to:
|
||||
avg_x += to_node.position.x
|
||||
total_connected += 1
|
||||
|
||||
if total_connected > 0:
|
||||
avg_x /= total_connected
|
||||
|
||||
# Blend between current position and optimal position
|
||||
optimal_x = lerp(node.position.x, avg_x, path_smoothness)
|
||||
|
||||
# Add some random variation to prevent everything aligning too perfectly
|
||||
optimal_x += (_rng.randf() - 0.5) * node_spread
|
||||
|
||||
# Keep within bounds
|
||||
optimal_x = clamp(optimal_x, 0, map_width)
|
||||
|
||||
# Update node position
|
||||
node.position.x = optimal_x
|
||||
|
||||
# Y position is determined by level
|
||||
node.position.y = node.level * level_height
|
||||
|
||||
# Resolve horizontal overlaps between nodes on the same level
|
||||
func _resolve_horizontal_overlaps(level_nodes):
|
||||
# Sort nodes by x position
|
||||
level_nodes.sort_custom(func(a, b): return a.position.x < b.position.x)
|
||||
|
||||
# Minimum distance between nodes
|
||||
var min_distance = 1.0
|
||||
|
||||
# Check for overlaps and adjust
|
||||
for i in range(1, level_nodes.size()):
|
||||
var prev_node = level_nodes[i-1]
|
||||
var curr_node = level_nodes[i]
|
||||
|
||||
if curr_node.position.x - prev_node.position.x < min_distance:
|
||||
# If too close, move them apart
|
||||
var midpoint = (prev_node.position.x + curr_node.position.x) / 2
|
||||
var half_distance = min_distance / 2
|
||||
|
||||
# Try to maintain relative positions
|
||||
prev_node.position.x = midpoint - half_distance
|
||||
curr_node.position.x = midpoint + half_distance
|
||||
|
||||
# Ensure we stay within bounds
|
||||
if prev_node.position.x < 0:
|
||||
var shift = -prev_node.position.x
|
||||
prev_node.position.x += shift
|
||||
curr_node.position.x += shift
|
||||
|
||||
if curr_node.position.x > map_width:
|
||||
var shift = curr_node.position.x - map_width
|
||||
prev_node.position.x -= shift
|
||||
curr_node.position.x -= shift
|
||||
|
||||
# Final boundary check for previous node
|
||||
prev_node.position.x = max(0, prev_node.position.x)
|
||||
|
||||
# Find a node by its ID
|
||||
func _find_node_by_id(nodes, id):
|
||||
for node in nodes:
|
||||
if node.id == id:
|
||||
return node
|
||||
return null
|
||||
1
Systems/Game/Map/NodeLayoutHelper.gd.uid
Normal file
1
Systems/Game/Map/NodeLayoutHelper.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bmi14xk8tn7xc
|
||||
132
Systems/Game/Map/NodePopup.gd
Normal file
132
Systems/Game/Map/NodePopup.gd
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Add or modify this in your CardVisual.gd script
|
||||
|
||||
extends Control
|
||||
class_name NodePopup
|
||||
|
||||
signal pressed(id)
|
||||
# Node references
|
||||
@onready var card_container = $CardContainer
|
||||
@onready var name_label = $CardContainer/VBoxContainer/NameLabel
|
||||
@onready var desc_label = $CardContainer/VBoxContainer/DescriptionLabel
|
||||
@onready var enter_btn = $CardContainer/VBoxContainer/HBoxContainer/EnterButton
|
||||
@onready var close_btn = $CardContainer/VBoxContainer/HBoxContainer/CloseButton
|
||||
var node_id = null;
|
||||
var animation_tween
|
||||
|
||||
|
||||
func _ready():
|
||||
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
gui_input.connect(_on_gui_input)
|
||||
if card_container:
|
||||
card_container.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
for child in get_children():
|
||||
if child is Control:
|
||||
child.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
|
||||
if enter_btn:
|
||||
enter_btn.connect("pressed", Callable(self, "_on_enter_button_pressed"))
|
||||
|
||||
if close_btn:
|
||||
close_btn.connect("pressed", Callable(self, "_on_close_button_pressed"))
|
||||
card_container.visible = false
|
||||
# Setup initial scale for animation
|
||||
card_container.scale = Vector2(0.8, 0.8)
|
||||
set_process_input(true)
|
||||
|
||||
|
||||
func set_card(id:int, name: String, desc: String):
|
||||
if id == null:
|
||||
card_container.visible = false
|
||||
return
|
||||
|
||||
node_id = id
|
||||
card_container.visible = true
|
||||
|
||||
name_label.text = name
|
||||
desc_label.text = desc
|
||||
|
||||
# Animate popup appearance
|
||||
if animation_tween:
|
||||
animation_tween.kill()
|
||||
|
||||
animation_tween = create_tween()
|
||||
animation_tween.tween_property(card_container, "scale", Vector2(1.0, 1.0), 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
|
||||
|
||||
# Make sure popup is fully visible on screen
|
||||
call_deferred("ensure_on_screen")
|
||||
|
||||
func ensure_on_screen():
|
||||
await get_tree().process_frame
|
||||
# Get viewport size
|
||||
var viewport_size = get_viewport_rect().size
|
||||
# Get popup global position and size
|
||||
var popup_rect = Rect2(global_position, card_container.size)
|
||||
|
||||
# Adjust if off-screen
|
||||
if popup_rect.position.x < 0:
|
||||
global_position.x = 10
|
||||
elif popup_rect.position.x + popup_rect.size.x > viewport_size.x:
|
||||
global_position.x = viewport_size.x - popup_rect.size.x - 10
|
||||
|
||||
if popup_rect.position.y < 0:
|
||||
global_position.y = 10
|
||||
elif popup_rect.position.y + popup_rect.size.y > viewport_size.y:
|
||||
global_position.y = viewport_size.y - popup_rect.size.y - 10
|
||||
|
||||
|
||||
func _on_gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
var tween = create_tween()
|
||||
tween.tween_property(card_container, "scale", Vector2(1.05, 1.05), 0.1)
|
||||
tween.tween_property(card_container, "scale", Vector2(1.0, 1.0), 0.1)
|
||||
|
||||
# Handle dragging the popup
|
||||
var dragging = false
|
||||
var drag_start_pos = Vector2()
|
||||
|
||||
func _input(event):
|
||||
if not visible or not card_container.visible:
|
||||
return
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
# Check if mouse is over the card container
|
||||
var local_mouse_pos = get_local_mouse_position()
|
||||
if card_container.get_rect().has_point(local_mouse_pos):
|
||||
dragging = true
|
||||
drag_start_pos = local_mouse_pos - card_container.position
|
||||
else:
|
||||
dragging = false
|
||||
|
||||
elif event is InputEventMouseMotion and dragging:
|
||||
card_container.position = get_local_mouse_position() - drag_start_pos
|
||||
func _on_enter_button_pressed():
|
||||
# Add a confirmation animation before closing
|
||||
var tween = create_tween()
|
||||
tween.tween_property(card_container, "scale", Vector2(1.1, 1.1), 0.1)
|
||||
tween.tween_property(card_container, "scale", Vector2(0.8, 0.8), 0.2)
|
||||
tween.tween_callback(func(): _confirm_selection())
|
||||
|
||||
func _disable_enter():
|
||||
enter_btn.disabled = true;
|
||||
func _enable_enter():
|
||||
enter_btn.disabled = false;
|
||||
|
||||
func _confirm_selection():
|
||||
emit_signal("pressed", node_id)
|
||||
card_container.visible = false
|
||||
visible = false
|
||||
|
||||
func _on_close_button_pressed():
|
||||
# Add a closing animation
|
||||
var tween = create_tween()
|
||||
tween.tween_property(card_container, "scale", Vector2(0.8, 0.8), 0.2)
|
||||
tween.tween_callback(func(): _cancel_selection())
|
||||
|
||||
func _cancel_selection():
|
||||
emit_signal("pressed", null)
|
||||
card_container.visible = false
|
||||
visible = false
|
||||
1
Systems/Game/Map/NodePopup.gd.uid
Normal file
1
Systems/Game/Map/NodePopup.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://tsgxcwb1u8uc
|
||||
491
Systems/Game/Menu/MenuContainer.gd
Normal file
491
Systems/Game/Menu/MenuContainer.gd
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
extends Control
|
||||
class_name MenuContainer
|
||||
|
||||
const VERSION_FILE_PATH = "res://Game.json"
|
||||
# Signal to notify parent when a new game is requested
|
||||
# signal new_game_requested
|
||||
signal reward_closed(node)
|
||||
signal new_game_requested(options)
|
||||
signal shop_open_requested(options)
|
||||
signal reward_open_requested(options)
|
||||
signal deckmanager_open_requested(options)
|
||||
signal map_open_requested(options)
|
||||
signal node_completed(options)
|
||||
signal shop_closed
|
||||
signal card_purchased(card, price)
|
||||
signal card_unlocked(card, price)
|
||||
signal lobby_open_requested(options)
|
||||
signal lobby_shop_open_requested(options)
|
||||
signal vanilla_mode_selected
|
||||
signal deeper_mode_selected
|
||||
@onready var newGameButton = $HBoxContainer/VBoxContainer/MenuOptions/NewGameText
|
||||
@onready var continueButton = $HBoxContainer/VBoxContainer/MenuOptions/Continue
|
||||
@onready var optionsButton = $HBoxContainer/VBoxContainer/MenuOptions/Options
|
||||
@onready var versionText = $HBoxContainer/VBoxContainer/VersionText
|
||||
@onready var titleText = $HBoxContainer/VBoxContainer/TitleText
|
||||
@onready var developerText = $HBoxContainer/VBoxContainer/DeveloperText
|
||||
@onready var gameMenuScreen = get_node("/root/Board/GameMenuScreen")
|
||||
@onready var deckManagerScreen = get_node("/root/Board/DeckManagerScreen")
|
||||
@onready var mapScreen = get_node("/root/Board/MapScreen")
|
||||
@onready var stateMachine = get_node("/root/Board/StateMachine")
|
||||
@onready var shopScreen = get_node("/root/Board/ShopScreen")
|
||||
@onready var progressionScreen = get_node("/root/Board/ProgressionScreen")
|
||||
@onready var rewardScreen = get_node("/root/Board/RewardScreen")
|
||||
@onready var game = get_node_or_null("/root/Board") as ChessGame
|
||||
@onready var lobbyScreen = get_node_or_null("/root/Board/LobbyScreen")
|
||||
@onready var lobbyShopScreen = get_node_or_null("/root/Board/LobbyShopScreen")
|
||||
@onready var preloadhandScreen = get_node_or_null("/root/Board/HandPreloadScreen")
|
||||
|
||||
|
||||
|
||||
var player_gold = 10
|
||||
var back_to_map = false
|
||||
var winConditionManager: WinConditionManager
|
||||
var currentWinCondition = Utils.WinConditionType.CAPTURE_UNIT
|
||||
var currentLossCondition = Utils.LossConditionType.UNIT_LOST
|
||||
|
||||
|
||||
func _ready():
|
||||
# Connect menu option signals
|
||||
_connect_button(newGameButton, "_on_new_game_pressed")
|
||||
_connect_button(continueButton, "_on_continue_pressed")
|
||||
_connect_button(optionsButton, "_on_options_pressed")
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.setup(self)
|
||||
gameMenuScreen.connect("shop_open_requested", Callable(self, "_on_shop_open_requested"))
|
||||
gameMenuScreen.connect("deckmanager_open_requested", Callable(self, "_on_deckmanager_open_requested"))
|
||||
gameMenuScreen.connect("map_open_requested", Callable(self, "_on_map_open_requested"))
|
||||
gameMenuScreen.connect("new_game_requested", Callable(self, "_on_start_game_pressed"))
|
||||
gameMenuScreen.connect("reward_open_requested", Callable(self, "_on_reward_open_requested"))
|
||||
gameMenuScreen.connect("lobby_open_requested", Callable(self, "_on_lobby_open_requested"))
|
||||
gameMenuScreen.visible = false
|
||||
else:
|
||||
connect("map_open_requested", Callable(self, "_on_map_open_requested"))
|
||||
connect("reward_closed", Callable(self, "_on_reward_completed"))
|
||||
|
||||
game.connect("node_completed", Callable(self, "_on_node_completed"))
|
||||
if deckManagerScreen:
|
||||
deckManagerScreen.connect("back_pressed", Callable(self, "_on_deck_manager_back_pressed"))
|
||||
deckManagerScreen.visible = false
|
||||
|
||||
if preloadhandScreen:
|
||||
preloadhandScreen.connect("back_pressed", Callable(self, "_on_preload_back_pressed"))
|
||||
preloadhandScreen.visible = false
|
||||
if progressionScreen:
|
||||
progressionScreen.connect("save_pressed", Callable(self, "_on_progression_save_pressed"))
|
||||
progressionScreen.visible = false
|
||||
if mapScreen:
|
||||
mapScreen.connect("back_pressed", Callable(self, "_on_map_back_pressed"))
|
||||
mapScreen.connect("deckmanager_open_requested", Callable(self, "_on_deckmanager_open_requested_from_map"))
|
||||
mapScreen.connect("node_selected", Callable(self, "_on_map_node_selected"))
|
||||
mapScreen.visible = false
|
||||
|
||||
if shopScreen:
|
||||
shopScreen.connect("back_pressed", Callable(self, "_on_shop_back_pressed"))
|
||||
shopScreen.connect("card_purchased", Callable(self, "_on_shop_card_purchased"))
|
||||
shopScreen.visible = false
|
||||
if rewardScreen:
|
||||
rewardScreen.connect("back_pressed", Callable(self, "_on_reward_back_pressed"))
|
||||
rewardScreen.connect("card_purchased", Callable(self, "_on_reward_card"))
|
||||
rewardScreen.visible = false
|
||||
if lobbyScreen:
|
||||
lobbyScreen.connect("vanilla_selected", Callable(self, "_on_vanilla_selected"))
|
||||
lobbyScreen.connect("preload_selected", Callable(self, "_on_preload_selected"))
|
||||
lobbyScreen.connect("shop_selected", Callable(self, "_on_lobby_shop_open_requested"))
|
||||
lobbyScreen.connect("deeper_selected", Callable(self, "_on_deeper_selected"))
|
||||
lobbyScreen.connect("back_pressed", Callable(self, "_on_lobby_back_pressed"))
|
||||
# lobbyScreen.connect("map_open_requested", Callable(self, "_on_map_open_requested"))
|
||||
lobbyScreen.visible = false
|
||||
|
||||
if lobbyShopScreen:
|
||||
lobbyShopScreen.connect("back_pressed", Callable(self, "_on_lobby_shop_back_pressed"))
|
||||
lobbyShopScreen.connect("card_unlocked", Callable(self, "_on_lobby_shop_card_unlocked"))
|
||||
lobbyShopScreen.connect("hand_size_increased", Callable(self, "_on_hand_size_increased"))
|
||||
lobbyShopScreen.visible = false
|
||||
load_version()
|
||||
|
||||
|
||||
func load_version():
|
||||
|
||||
var version = "v.0.0.0"
|
||||
var title = "ChessBuilder"
|
||||
var developer = ""
|
||||
var file = FileAccess.open(VERSION_FILE_PATH, FileAccess.READ)
|
||||
if file:
|
||||
var json_text = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
# Parse the JSON
|
||||
var json = JSON.new()
|
||||
var error = json.parse(json_text)
|
||||
|
||||
if error == OK:
|
||||
var data = json.get_data()
|
||||
if data and typeof(data) == TYPE_DICTIONARY and data.has("version"):
|
||||
version = "v " + str(data.version)
|
||||
if data and typeof(data) == TYPE_DICTIONARY and data.has("title"):
|
||||
title = str(data.title)
|
||||
if data and typeof(data) == TYPE_DICTIONARY and data.has("developer"):
|
||||
developer = str(data.developer)
|
||||
else:
|
||||
print("JSON Parse Error: ", json.get_error_message())
|
||||
|
||||
if versionText:
|
||||
versionText.text = version
|
||||
if titleText:
|
||||
titleText.text = title
|
||||
if developerText:
|
||||
developerText.text = developer
|
||||
# Connect a button signal to a method
|
||||
func _connect_button(button, method_name):
|
||||
if button and button.has_signal("pressed"):
|
||||
if !button.is_connected("pressed", Callable(self, method_name)):
|
||||
button.connect("pressed", Callable(self, method_name))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func _on_preload_back_pressed():
|
||||
if preloadhandScreen:
|
||||
preloadhandScreen.visible = false
|
||||
if lobbyScreen:
|
||||
lobbyScreen.visible = true
|
||||
|
||||
|
||||
# Handle New Game button press
|
||||
func _on_new_game_pressed():
|
||||
print("New Game pressed, showing game menu")
|
||||
self.visible = false
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = true
|
||||
|
||||
# _on_map_open_requested({})
|
||||
# emit_signal("new_game_requested")
|
||||
|
||||
|
||||
|
||||
func _on_lobby_open_requested(options):
|
||||
print("Lobby requested with options:", options)
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = false
|
||||
if lobbyScreen:
|
||||
lobbyScreen.visible = true
|
||||
lobbyScreen.initialize(options)
|
||||
lobbyScreen.show_screen()
|
||||
print("_on_lobby_open_requested" )
|
||||
|
||||
func _on_lobby_back_pressed():
|
||||
if lobbyScreen:
|
||||
lobbyScreen.visible = false
|
||||
self.visible = true
|
||||
|
||||
func _on_preload_selected():
|
||||
print("Preload mode selected")
|
||||
preloadhandScreen.initialize(null)
|
||||
preloadhandScreen.visible = true
|
||||
|
||||
func _on_vanilla_selected():
|
||||
print("Vanilla mode selected")
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = true
|
||||
game.mode = "vanilla"
|
||||
_on_map_open_requested({"mode": "vanilla", "hand_size": 2, })
|
||||
# emit_signal("vanilla_mode_selected")
|
||||
|
||||
func _on_deeper_selected():
|
||||
print("Deeper mode selected")
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = true
|
||||
game.mode = "deeper"
|
||||
_on_map_open_requested({"mode": "deeper", "hand_size": game.player.hand_size,})
|
||||
|
||||
func _on_lobby_shop_open_requested(options):
|
||||
print("Lobby Shop requested with options:", options)
|
||||
if lobbyScreen:
|
||||
lobbyScreen.visible = false
|
||||
if lobbyShopScreen:
|
||||
lobbyShopScreen.visible = true
|
||||
lobbyShopScreen.initialize(options)
|
||||
emit_signal("lobby_shop_open_requested", options)
|
||||
|
||||
func _on_lobby_shop_back_pressed():
|
||||
if lobbyShopScreen:
|
||||
lobbyShopScreen.visible = false
|
||||
if lobbyScreen:
|
||||
lobbyScreen.visible = true
|
||||
|
||||
|
||||
func _on_lobby_shop_card_unlocked(card, token_cost):
|
||||
# Add the card to the player's bank
|
||||
if game and "deckManager" in game:
|
||||
var deck_manager = game.deckManager
|
||||
game.player.unlock_card(card)
|
||||
print("Added unlocked card to whitelist: ", card.cardName)
|
||||
|
||||
func _on_hand_size_increased():
|
||||
print("Hand size increased")
|
||||
|
||||
|
||||
# Handle Continue button press
|
||||
func _on_continue_pressed():
|
||||
print("Continue pressed")
|
||||
# Implement continue game logic here
|
||||
# self.visible = false
|
||||
# You can add logic to load a saved game if needed
|
||||
|
||||
# Handle Options button press
|
||||
func _on_options_pressed():
|
||||
print("Options pressed")
|
||||
# Implement options menu logic here
|
||||
# You could show an options panel or switch to an options menu
|
||||
|
||||
# Game Menu Screen Signals
|
||||
func _on_shop_open_requested(options):
|
||||
print("Shop requested with options:", options)
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = false
|
||||
|
||||
if options == null:
|
||||
options = {}
|
||||
|
||||
if "gold" in options:
|
||||
options["gold"] = game.player.get_gold()
|
||||
if mapScreen:
|
||||
mapScreen.visible = false
|
||||
if shopScreen:
|
||||
shopScreen.visible = true
|
||||
shopScreen.initialize(options)
|
||||
emit_signal("shop_open_requested", options)
|
||||
|
||||
func _on_shop_back_pressed():
|
||||
if shopScreen:
|
||||
shopScreen.visible = false
|
||||
game.player.set_gold(shopScreen.player_gold)
|
||||
|
||||
# Show game menu again
|
||||
if not back_to_map and gameMenuScreen:
|
||||
gameMenuScreen.visible = true
|
||||
if back_to_map:
|
||||
mapScreen.visible = true
|
||||
# Emit signal that shop was closed
|
||||
emit_signal("shop_closed")
|
||||
|
||||
func _on_shop_card_purchased(card, price):
|
||||
game.player.remove_gold(price)
|
||||
# Forward the signal
|
||||
emit_signal("card_purchased", card, price)
|
||||
|
||||
# Add the card to the player's bank
|
||||
if game and "deckManager" in game:
|
||||
var deck_manager = game.deckManager
|
||||
deck_manager.bank.append(card)
|
||||
print("Added purchased card to bank:", card.cardName)
|
||||
|
||||
|
||||
|
||||
func _on_reward_open_requested(options):
|
||||
print("Reward requested with options:", options)
|
||||
if shopScreen:
|
||||
shopScreen.visible = false
|
||||
|
||||
if options == null:
|
||||
options = {}
|
||||
|
||||
|
||||
if "gold" in options.metadata:
|
||||
options.metadata["gold"] = game.player.get_gold()
|
||||
if rewardScreen:
|
||||
rewardScreen.visible = true
|
||||
rewardScreen.initialize(options)
|
||||
emit_signal("reward_open_requested", options)
|
||||
|
||||
|
||||
func _on_reward_back_pressed(completed_node):
|
||||
if rewardScreen:
|
||||
rewardScreen.visible = false
|
||||
|
||||
if not back_to_map and gameMenuScreen:
|
||||
gameMenuScreen.visible = true
|
||||
if back_to_map:
|
||||
mapScreen.visible = true
|
||||
print("DONE: ", completed_node)
|
||||
var is_escape = completed_node.metadata.is_escape
|
||||
emit_signal("reward_closed", completed_node)
|
||||
|
||||
func _on_reward_card(card, price):
|
||||
# Forward the signal
|
||||
emit_signal("card_purchased", card, price)
|
||||
|
||||
# Add the card to the player's bank
|
||||
if game and "deckManager" in game:
|
||||
var deck_manager = game.deckManager
|
||||
deck_manager.bank.append(card)
|
||||
print("Added reward card to bank:", card.cardName)
|
||||
|
||||
|
||||
func _on_reward_completed(completed_node):
|
||||
var is_escape = completed_node.metadata.is_escape
|
||||
if progressionScreen and is_escape:
|
||||
progressionScreen.initialize({"max_deck_size": 10, "node": completed_node})
|
||||
if completed_node.type == Utils.RoomType.FINAL:
|
||||
game.player.add_tokens(1)
|
||||
progressionScreen.visible = true
|
||||
|
||||
|
||||
|
||||
func _on_progression_save_pressed():
|
||||
print("_on_progression_save_pressed")
|
||||
progressionScreen.visible = false
|
||||
gameMenuScreen.visible = false
|
||||
_on_lobby_open_requested(null)
|
||||
|
||||
|
||||
|
||||
func _on_deckmanager_open_requested_from_map(options):
|
||||
print("Deck Manager requested with options:", options)
|
||||
|
||||
# Hide game menu
|
||||
gameMenuScreen.visible = false
|
||||
|
||||
# Show and initialize deck manager
|
||||
|
||||
if deckManagerScreen:
|
||||
deckManagerScreen.visible = true
|
||||
deckManagerScreen.initialize(options)
|
||||
back_to_map = true
|
||||
# Also emit signal for other systems that might need to know
|
||||
emit_signal("deckmanager_open_requested", options)
|
||||
|
||||
|
||||
|
||||
func _on_deckmanager_open_requested(options):
|
||||
print("Deck Manager requested with options:", options)
|
||||
|
||||
# Hide game menu
|
||||
gameMenuScreen.visible = false
|
||||
|
||||
# Show and initialize deck manager
|
||||
if deckManagerScreen:
|
||||
deckManagerScreen.visible = true
|
||||
deckManagerScreen.initialize(options)
|
||||
|
||||
# Also emit signal for other systems that might need to know
|
||||
emit_signal("deckmanager_open_requested", options)
|
||||
|
||||
func _on_deck_manager_back_pressed():
|
||||
# Return to game menu screen
|
||||
deckManagerScreen.visible = false
|
||||
if not back_to_map:
|
||||
gameMenuScreen.visible = true
|
||||
else:
|
||||
mapScreen.visible = true
|
||||
|
||||
func _on_map_open_requested(options):
|
||||
print("Map requested with options:", options)
|
||||
gameMenuScreen.visible = false
|
||||
if "hand_size" in options:
|
||||
game.player.update_deck_hand_size(options.hand_size)
|
||||
if mapScreen:
|
||||
mapScreen.generate_map({"mode": options.mode})
|
||||
mapScreen.display_map()
|
||||
mapScreen.visible = true
|
||||
emit_signal("map_open_requested", options)
|
||||
|
||||
func _on_start_game_pressed(options):
|
||||
print("Starting game with options:", options)
|
||||
|
||||
emit_signal("new_game_requested", options)
|
||||
|
||||
func _on_map_back_pressed():
|
||||
mapScreen.visible = false
|
||||
gameMenuScreen.visible = true
|
||||
|
||||
# Map node selection
|
||||
func _on_map_node_selected(node_data):
|
||||
print("Selected map node: ", node_data)
|
||||
if node_data.type == Utils.RoomType.SHOP:
|
||||
print("Utils.RoomType.SHOP")
|
||||
back_to_map = true
|
||||
_on_shop_open_requested(node_data.metadata)
|
||||
elif node_data.type == Utils.RoomType.FINAL:
|
||||
print("Utils.RoomType.FINAL")
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = false
|
||||
if deckManagerScreen:
|
||||
deckManagerScreen.visible = false
|
||||
if mapScreen:
|
||||
mapScreen.visible = false
|
||||
back_to_map = true
|
||||
_on_start_game_pressed(node_data)
|
||||
elif node_data.type == Utils.RoomType.NORMAL:
|
||||
print("Utils.RoomType.NORMAL")
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = false
|
||||
if deckManagerScreen:
|
||||
deckManagerScreen.visible = false
|
||||
if mapScreen:
|
||||
mapScreen.visible = false
|
||||
back_to_map = true
|
||||
_on_start_game_pressed(node_data)
|
||||
elif node_data.type == Utils.RoomType.BOSS:
|
||||
print("Utils.RoomType.BOSS")
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = false
|
||||
if deckManagerScreen:
|
||||
deckManagerScreen.visible = false
|
||||
if mapScreen:
|
||||
mapScreen.visible = false
|
||||
back_to_map = true
|
||||
_on_start_game_pressed(node_data)
|
||||
elif node_data.type == Utils.RoomType.EVENT:
|
||||
print("Utils.RoomType.EVENT")
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = false
|
||||
if deckManagerScreen:
|
||||
deckManagerScreen.visible = false
|
||||
if mapScreen:
|
||||
mapScreen.visible = false
|
||||
back_to_map = true
|
||||
_on_start_game_pressed(node_data)
|
||||
|
||||
func _on_node_completed(options):
|
||||
var node_data = options.node
|
||||
var completed = options.completed
|
||||
var success = options.success
|
||||
if success:
|
||||
_on_reward_open_requested(node_data)
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**************MATCH***************")
|
||||
print("**************OVER****************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
else:
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**************GAME****************")
|
||||
print("**************OVER****************")
|
||||
print("*************GITGUD***************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
print("**********************************")
|
||||
|
||||
|
||||
# Public method to show the menu
|
||||
func show_menu():
|
||||
self.visible = true
|
||||
if gameMenuScreen:
|
||||
gameMenuScreen.visible = false
|
||||
if deckManagerScreen:
|
||||
deckManagerScreen.visible = false
|
||||
if mapScreen:
|
||||
mapScreen.visible = false
|
||||
1
Systems/Game/Menu/MenuContainer.gd.uid
Normal file
1
Systems/Game/Menu/MenuContainer.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bf5ljae05pvla
|
||||
39
Systems/Game/Menu/MenuOption.gd
Normal file
39
Systems/Game/Menu/MenuOption.gd
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
extends TextureRect
|
||||
class_name MenuOption
|
||||
|
||||
signal pressed
|
||||
|
||||
# Original modulate to return to after hover
|
||||
var _original_modulate: Color
|
||||
|
||||
func _ready():
|
||||
# Make this texture rect clickable
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
# Save original modulate color
|
||||
_original_modulate = modulate
|
||||
|
||||
# Connect the gui_input signal to our own handler
|
||||
connect("gui_input", Callable(self, "_on_gui_input"))
|
||||
|
||||
# Connect hover signals for better user experience
|
||||
connect("mouse_entered", Callable(self, "_on_mouse_entered"))
|
||||
connect("mouse_exited", Callable(self, "_on_mouse_exited"))
|
||||
|
||||
func _on_gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
emit_signal("pressed")
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_mouse_entered():
|
||||
# Change appearance when mouse hovers
|
||||
modulate = Color(1.2, 1.2, 0.8) # Slight yellowish tint and brightening
|
||||
|
||||
# Optional: Add a hover sound effect
|
||||
# if has_node("/root/SoundManager"):
|
||||
# get_node("/root/SoundManager").play_hover_sound()
|
||||
|
||||
func _on_mouse_exited():
|
||||
# Restore original appearance
|
||||
modulate = _original_modulate
|
||||
1
Systems/Game/Menu/MenuOption.gd.uid
Normal file
1
Systems/Game/Menu/MenuOption.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dbm5dv81lbdod
|
||||
50
Systems/Game/Menu/MenuTextOption.gd
Normal file
50
Systems/Game/Menu/MenuTextOption.gd
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
extends RichTextLabel
|
||||
class_name MenuTextOption
|
||||
|
||||
signal pressed
|
||||
|
||||
# Style properties
|
||||
var normal_color: Color = Color(1, 1, 1, 1) # White
|
||||
var hover_color: Color = Color(1, 1, 0, 1) # Yellow
|
||||
var font_size: int = 24
|
||||
|
||||
func _ready():
|
||||
# Make this label clickable
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
# Set up base styling
|
||||
add_theme_font_size_override("normal_font_size", font_size)
|
||||
add_theme_color_override("default_color", normal_color)
|
||||
|
||||
# Make text bold
|
||||
bbcode_enabled = true
|
||||
text = "[b]" + text + "[/b]"
|
||||
|
||||
# Connect the gui_input signal to our own handler
|
||||
connect("gui_input", Callable(self, "_on_gui_input"))
|
||||
|
||||
# Connect hover signals for better user experience
|
||||
connect("mouse_entered", Callable(self, "_on_mouse_entered"))
|
||||
connect("mouse_exited", Callable(self, "_on_mouse_exited"))
|
||||
|
||||
func _on_gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
emit_signal("pressed")
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_mouse_entered():
|
||||
# Change appearance when mouse hovers
|
||||
add_theme_color_override("default_color", hover_color)
|
||||
|
||||
# Scale up slightly on hover (optional)
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "scale", Vector2(1.05, 1.05), 0.1)
|
||||
|
||||
func _on_mouse_exited():
|
||||
# Restore original appearance
|
||||
add_theme_color_override("default_color", normal_color)
|
||||
|
||||
# Scale back to normal (optional)
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.1)
|
||||
1
Systems/Game/Menu/MenuTextOption.gd.uid
Normal file
1
Systems/Game/Menu/MenuTextOption.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c47i2m0ll101x
|
||||
167
Systems/Game/Player/Player.gd
Normal file
167
Systems/Game/Player/Player.gd
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
extends Node
|
||||
class_name Player
|
||||
|
||||
var attached_cards: Dictionary = {}
|
||||
var attached_effects: Dictionary = {}
|
||||
var hand_size: int = 2
|
||||
var gold: int = 70
|
||||
var tokens: int = 1
|
||||
var game: ChessGame
|
||||
var run_count: int = 0
|
||||
const MAX_HAND_SIZE = 5
|
||||
var unlocked_cards = []
|
||||
|
||||
var cards_by_rank = {
|
||||
Card.Rank.RANK_0: [], # Most powerful (one-time use)
|
||||
Card.Rank.RANK_1: [], # Once per match
|
||||
Card.Rank.RANK_2: [], # Discard pile cards
|
||||
Card.Rank.RANK_3: [] # Basic cards
|
||||
}
|
||||
|
||||
var cards_by_name = {}
|
||||
func _init(g: int, size: int, tok: int, gm: ChessGame):
|
||||
print("INIT PLAYER CHARACTER")
|
||||
# var classList = ProjectSettings.get_global_class_list();
|
||||
# for classType in classList:
|
||||
# if classType.base == "Card":
|
||||
# print("ALL CARDS ", classType)
|
||||
gold = g
|
||||
tokens = tok
|
||||
game = gm
|
||||
hand_size = size
|
||||
update_deck_hand_size(size)
|
||||
load_all_cards()
|
||||
initialize_deck_from_defaults(unlocked_cards)
|
||||
# Debug output
|
||||
print_card_list()
|
||||
|
||||
|
||||
func initialize_deck_from_defaults(unlocked_cards: Array = []):
|
||||
if "deckManager" in game:
|
||||
game.deckManager.initializeStartingDeck(unlocked_cards)
|
||||
|
||||
|
||||
func update_deck_hand_size(size: int):
|
||||
if "deckManager" in game:
|
||||
game.deckManager.set_hand_size(hand_size)
|
||||
|
||||
func set_hand_size(size: int):
|
||||
hand_size = size
|
||||
|
||||
func get_tokens() -> int:
|
||||
return tokens
|
||||
|
||||
func set_tokens(amount: int) -> void:
|
||||
tokens = amount
|
||||
|
||||
func remove_tokens(amount: int) -> void:
|
||||
tokens = max(0, tokens - amount)
|
||||
|
||||
func add_tokens(t: int) -> void :
|
||||
tokens += t
|
||||
|
||||
func increment_runs() -> void :
|
||||
run_count += 1
|
||||
|
||||
func get_run_count() -> int :
|
||||
return run_count
|
||||
|
||||
func get_gold() -> int :
|
||||
return gold
|
||||
|
||||
func add_gold(g: int) -> void :
|
||||
gold += g
|
||||
|
||||
func remove_gold(g: int) -> void :
|
||||
gold -= g
|
||||
|
||||
func set_gold(g: int) -> void :
|
||||
gold = g
|
||||
|
||||
func unlock_card(card_instance):
|
||||
unlocked_cards.append(card_instance)
|
||||
|
||||
func load_all_cards():
|
||||
var class_list = ProjectSettings.get_global_class_list()
|
||||
|
||||
print("Loading all card classes...")
|
||||
|
||||
for class_info in class_list:
|
||||
if class_info.base == "Card":
|
||||
var cname = class_info["class"]
|
||||
print("Found card class: ", cname)
|
||||
|
||||
var script_path = class_info["path"]
|
||||
var card_script = load(script_path)
|
||||
|
||||
if card_script == null:
|
||||
print("Couldn't load script: ", script_path)
|
||||
continue
|
||||
|
||||
var card_instance = card_script.new()
|
||||
if card_instance.is_default:
|
||||
unlocked_cards.append(card_instance)
|
||||
cards_by_name[card_instance.cardName] = card_instance
|
||||
|
||||
if card_instance.rank in cards_by_rank:
|
||||
cards_by_rank[card_instance.rank].append(card_instance)
|
||||
else:
|
||||
print("Warning: Card has unknown rank: ", card_instance.cardName, " (", card_instance.rank, ")")
|
||||
|
||||
print("Successfully loaded card: ", card_instance.cardName, " (Rank ", card_instance.rank, ")")
|
||||
|
||||
print("Card loading complete. Loaded ", cards_by_name.size(), " cards.")
|
||||
|
||||
|
||||
|
||||
func print_card_list():
|
||||
print("\n--- CARD LIST BY RANK ---")
|
||||
|
||||
for rank in cards_by_rank:
|
||||
var rank_name = ""
|
||||
match rank:
|
||||
Card.Rank.RANK_0: rank_name = "RANK 0 (One-time use)"
|
||||
Card.Rank.RANK_1: rank_name = "RANK 1 (Once per match)"
|
||||
Card.Rank.RANK_2: rank_name = "RANK 2 (Discard pile)"
|
||||
Card.Rank.RANK_3: rank_name = "RANK 3 (Basic)"
|
||||
_: rank_name = "UNKNOWN RANK"
|
||||
|
||||
print("\n" + rank_name + " (" + str(cards_by_rank[rank].size()) + " cards):")
|
||||
|
||||
for card in cards_by_rank[rank]:
|
||||
print(" - " + card.cardName)
|
||||
|
||||
print("\n--- END CARD LIST ---\n")
|
||||
print("\n--- UNLOCKED CARD LIST ---")
|
||||
for card in unlocked_cards:
|
||||
print(" - " + card.cardName)
|
||||
print("\n--- END UNLOCKED CARD LIST ---\n")
|
||||
|
||||
|
||||
|
||||
|
||||
func get_card(card_name: String) -> Card:
|
||||
if card_name in cards_by_name:
|
||||
return cards_by_name[card_name].duplicate()
|
||||
return null
|
||||
|
||||
func get_random_card(rank: int) -> Card:
|
||||
if rank in cards_by_rank and not cards_by_rank[rank].is_empty():
|
||||
var random_index = randi() % cards_by_rank[rank].size()
|
||||
return cards_by_rank[rank][random_index].duplicate()
|
||||
return null
|
||||
|
||||
func get_cards_by_rank(rank: int) -> Array:
|
||||
if rank in cards_by_rank:
|
||||
var result = []
|
||||
for card in cards_by_rank[rank]:
|
||||
result.append(card.duplicate())
|
||||
return result
|
||||
return []
|
||||
|
||||
func get_all_cards() -> Array:
|
||||
var result = []
|
||||
for rank in cards_by_rank:
|
||||
for card in cards_by_rank[rank]:
|
||||
result.append(card.duplicate())
|
||||
return result
|
||||
1
Systems/Game/Player/Player.gd.uid
Normal file
1
Systems/Game/Player/Player.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bulvkisinp2ur
|
||||
250
Systems/Game/ProgressionScreen.gd
Normal file
250
Systems/Game/ProgressionScreen.gd
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
extends Control
|
||||
class_name ProgressionScreen
|
||||
|
||||
signal save_pressed
|
||||
signal progression_screen_visibility_changed(isvisible)
|
||||
|
||||
@onready var deckGrid = $MainContainer/GridScrollContainer/GridContainer
|
||||
@onready var bankContainer = $MainContainer/BankContainer/ScrollContainer/VBoxContainer
|
||||
@onready var saveButton = $SaveButton
|
||||
@onready var card_preview = $CardPreviewPanel
|
||||
|
||||
var maxDeckSize = 10
|
||||
var deckManager = null
|
||||
var game = null
|
||||
var bankCards = [] # All Users card
|
||||
|
||||
# The current deck
|
||||
var currentDeck = []
|
||||
var current_preview_card = null
|
||||
|
||||
func _ready():
|
||||
# Connect back button
|
||||
if saveButton:
|
||||
saveButton.connect("pressed", Callable(self, "_on_save_button_pressed"))
|
||||
|
||||
if card_preview:
|
||||
card_preview.visible = false
|
||||
|
||||
|
||||
func initialize(options = null):
|
||||
if options:
|
||||
if options.has("max_deck_size") and options.max_deck_size is int:
|
||||
maxDeckSize = options.max_deck_size
|
||||
|
||||
|
||||
# Find the DeckManager instance
|
||||
var board = get_node_or_null("/root/Board") as ChessGame
|
||||
game = board
|
||||
if board and "deckManager" in board:
|
||||
deckManager = board.deckManager
|
||||
print("Found deck manager:", deckManager)
|
||||
# if(!deckManager.deck):
|
||||
# deckManager.initializeStartingDeck()
|
||||
else:
|
||||
print("DeckManager not found on Board node")
|
||||
print("DECK MANAGER")
|
||||
|
||||
# Load cards from deck and bank
|
||||
loadCards()
|
||||
|
||||
# Set up the grid with empty card containers
|
||||
setupDeckGrid()
|
||||
|
||||
# Populate the bank with available cards
|
||||
populateBank()
|
||||
|
||||
func loadCards():
|
||||
var board = get_node_or_null("/root/Board") as ChessGame
|
||||
if board and "deckManager" in board:
|
||||
deckManager = board.deckManager
|
||||
print("Found deck manager:", deckManager)
|
||||
if deckManager:
|
||||
# Clone the deck to work with
|
||||
currentDeck = []
|
||||
bankCards = deckManager.bank.duplicate()
|
||||
bankCards.append_array(deckManager.deck.duplicate())
|
||||
else:
|
||||
# Fallback with empty collections if deck manager not found
|
||||
currentDeck = []
|
||||
bankCards = []
|
||||
print("Warning: DeckManager not found")
|
||||
|
||||
|
||||
func setupDeckGrid():
|
||||
# Clear existing children
|
||||
for child in deckGrid.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Calculate grid dimensions
|
||||
var cols = 4 #check screen to deteremine
|
||||
var rows = maxDeckSize / cols
|
||||
deckGrid.columns = cols
|
||||
|
||||
# Create card slots
|
||||
for i in range(maxDeckSize):
|
||||
var card_slot = preload("res://card_slot.tscn").instantiate()
|
||||
deckGrid.add_child(card_slot)
|
||||
|
||||
# Connect signals
|
||||
card_slot.connect("card_selected", Callable(self, "_on_deck_card_selected"))
|
||||
_connect_hover_signals(card_slot)
|
||||
|
||||
# Set card if available
|
||||
if i < currentDeck.size():
|
||||
card_slot.set_card(currentDeck[i])
|
||||
else:
|
||||
card_slot.clear()
|
||||
|
||||
func _connect_hover_signals(node):
|
||||
# Add hover signals for preview functionality
|
||||
if node.is_class("Control"):
|
||||
node.mouse_entered.connect(Callable(self, "_on_card_mouse_entered").bind(node))
|
||||
# node.mouse_exited.connect(Callable(self, "_on_card_mouse_exited").bind(node))
|
||||
|
||||
|
||||
func populateBank():
|
||||
# Clear existing children
|
||||
for child in bankContainer.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Add each bank card to the list
|
||||
for card in bankCards:
|
||||
var card_item = preload("res://card_bank_item.tscn").instantiate()
|
||||
bankContainer.add_child(card_item)
|
||||
card_item.set_card(card)
|
||||
card_item.connect("card_selected", Callable(self, "_on_bank_card_selected"))
|
||||
_connect_hover_signals(card_item)
|
||||
|
||||
func _on_deck_card_selected(card_slot, card):
|
||||
if card:
|
||||
# Remove card from deck
|
||||
var index = currentDeck.find(card)
|
||||
if index >= 0:
|
||||
currentDeck.remove_at(index)
|
||||
|
||||
# Add to bank
|
||||
bankCards.append(card)
|
||||
|
||||
# Update UI
|
||||
card_slot.clear()
|
||||
populateBank()
|
||||
if current_preview_card == card:
|
||||
hide_card_preview()
|
||||
|
||||
func _on_bank_card_selected(card_item, card):
|
||||
print("_on_bank_card_selected")
|
||||
# Find first empty slot in deck
|
||||
var empty_slot_index = -1
|
||||
for i in range(deckGrid.get_child_count()):
|
||||
var slot = deckGrid.get_child(i)
|
||||
if !slot.has_card():
|
||||
empty_slot_index = i
|
||||
break
|
||||
|
||||
if empty_slot_index >= 0 and currentDeck.size() < maxDeckSize:
|
||||
# Remove from bank
|
||||
var bank_index = bankCards.find(card)
|
||||
if bank_index >= 0:
|
||||
bankCards.remove_at(bank_index)
|
||||
|
||||
# Add to deck
|
||||
currentDeck.append(card)
|
||||
|
||||
# Update UI
|
||||
deckGrid.get_child(empty_slot_index).set_card(card)
|
||||
populateBank()
|
||||
|
||||
func _on_card_mouse_entered(node):
|
||||
var card = null
|
||||
|
||||
# Get the card from the node
|
||||
if node.has_method("has_card") and node.has_card():
|
||||
# This is a CardSlot
|
||||
card = node.current_card
|
||||
elif node.has_method("set_card") and node.current_card:
|
||||
# This is a CardBankItem
|
||||
card = node.current_card
|
||||
|
||||
if card:
|
||||
show_card_preview(card)
|
||||
|
||||
# func _on_card_mouse_exited(node):
|
||||
# # Add a short delay before hiding the preview
|
||||
# # This prevents flickering when moving between cards
|
||||
# await get_tree().create_timer(0.1).timeout
|
||||
|
||||
# # Only hide if we're not hovering over another card that shows the same preview
|
||||
# if current_preview_card:
|
||||
# hide_card_preview()
|
||||
|
||||
func show_card_preview(card):
|
||||
if card_preview and card:
|
||||
current_preview_card = card
|
||||
card_preview.preview_card(card)
|
||||
|
||||
func hide_card_preview():
|
||||
if card_preview:
|
||||
current_preview_card = null
|
||||
card_preview.hide_preview()
|
||||
|
||||
func save_deck():
|
||||
sell_remaining_cards()
|
||||
if deckManager:
|
||||
# Save the current deck to the deck manager
|
||||
deckManager.deck = currentDeck.duplicate()
|
||||
deckManager.bank = []
|
||||
|
||||
print("Deck saved with ", currentDeck.size(), " cards")
|
||||
if game.player:
|
||||
game.player.increment_runs()
|
||||
|
||||
func sell_remaining_cards():
|
||||
for card in bankCards:
|
||||
sell_selected_card(card)
|
||||
bankCards.clear()
|
||||
|
||||
func get_card_price(card):
|
||||
if not card:
|
||||
return 0
|
||||
if card.rank in Utils.CardPrices:
|
||||
return Utils.CardPrices[card.rank]
|
||||
return 50
|
||||
|
||||
|
||||
func sell_selected_card(selected_card):
|
||||
|
||||
var price = get_card_price(selected_card)
|
||||
|
||||
game.player.add_gold(price)
|
||||
|
||||
return true
|
||||
|
||||
func _on_save_button_pressed():
|
||||
# Save changes before returning
|
||||
save_deck()
|
||||
|
||||
# Emit signal to go back
|
||||
emit_signal("save_pressed")
|
||||
|
||||
# Hide this screen
|
||||
visible = false
|
||||
|
||||
|
||||
|
||||
|
||||
# Notification
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_VISIBILITY_CHANGED:
|
||||
_on_visibility_changed(visible)
|
||||
|
||||
func _on_visibility_changed(is_visible):
|
||||
|
||||
print("Progression Screen visibility changed to: ", is_visible)
|
||||
if is_visible:
|
||||
loadCards()
|
||||
setupDeckGrid()
|
||||
else:
|
||||
print("Progression Screen is now invisible")
|
||||
|
||||
emit_signal("progression_screen_visibility_changed", is_visible)
|
||||
1
Systems/Game/ProgressionScreen.gd.uid
Normal file
1
Systems/Game/ProgressionScreen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dbohdt174pual
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue