Skip to content

Commit 3c8226e

Browse files
authored
Merge pull request #10 from Mnehmos/development
Sync development to main: ARM64 support + test infrastructure fixes
2 parents 96833cf + 0fd1e4a commit 3c8226e

File tree

117 files changed

+2316
-646
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+2316
-646
lines changed

.claude/settings.local.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(npm test:*)",
5+
"Bash(npm run build:*)",
6+
"Bash(git add:*)",
7+
"Bash(git commit:*)",
8+
"Bash(git push:*)",
9+
"Bash(npm run test:ci:*)",
10+
"Bash(git checkout:*)",
11+
"Bash(npx vitest:*)",
12+
"Bash(find:*)",
13+
"Bash(git stash:*)",
14+
"Bash(gh run list:*)",
15+
"Bash(gh run view:*)",
16+
"Bash(gh run watch:*)",
17+
"Bash(gh pr create:*)",
18+
"Bash(gh pr checks:*)",
19+
"Bash(gh pr view:*)",
20+
"Bash(git fetch:*)",
21+
"Bash(git merge:*)",
22+
"Bash(grep:*)"
23+
]
24+
}
25+
}

.roomodes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"customModes": []
3+
}

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,22 @@ Download the pre-built binary for your platform from the [Releases](https://gith
245245
.\rpg-mcp-win.exe
246246
```
247247

248-
**macOS:**
248+
**macOS (Intel):**
249249

250250
```bash
251251
# Download rpg-mcp-macos
252252
chmod +x rpg-mcp-macos
253253
./rpg-mcp-macos
254254
```
255255

256+
**macOS (Apple Silicon - M1/M2/M3/M4):**
257+
258+
```bash
259+
# Download rpg-mcp-macos-arm64
260+
chmod +x rpg-mcp-macos-arm64
261+
./rpg-mcp-macos-arm64
262+
```
263+
256264
**Linux:**
257265

258266
```bash
@@ -277,7 +285,7 @@ To build binaries yourself:
277285

278286
```bash
279287
npm run build:binaries
280-
# Output: dist-bundle/rpg-mcp-win.exe, rpg-mcp-macos, rpg-mcp-linux
288+
# Output: bin/rpg-mcp-win.exe, rpg-mcp-macos, rpg-mcp-macos-arm64, rpg-mcp-linux
281289
```
282290

283291
### MCP Client Configuration

esbuild.config.mjs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ async function build() {
412412
"pkg": {
413413
"scripts": [],
414414
"assets": [],
415-
"targets": ["node20-win-x64", "node20-macos-x64", "node20-linux-x64"],
415+
"targets": ["node20-win-x64", "node20-macos-x64", "node20-macos-arm64", "node20-linux-x64"],
416416
"outputPath": "../bin"
417417
}
418418
};
@@ -421,7 +421,7 @@ async function build() {
421421

422422
// Run pkg
423423
console.log('📦 Creating executables with pkg...');
424-
execSync('npx pkg dist-bundle/server.cjs --targets node20-win-x64,node20-macos-x64,node20-linux-x64 --output bin/rpg-mcp', {
424+
execSync('npx pkg dist-bundle/server.cjs --targets node20-win-x64,node20-macos-x64,node20-macos-arm64,node20-linux-x64 --output bin/rpg-mcp', {
425425
stdio: 'inherit',
426426
cwd: process.cwd()
427427
});
@@ -435,10 +435,11 @@ async function build() {
435435
console.log(' ⚠️ NOT using local node_modules (compiled for different Node version)');
436436
console.log('');
437437

438-
const platforms = ['win32-x64', 'darwin-x64', 'linux-x64'];
438+
const platforms = ['win32-x64', 'darwin-x64', 'darwin-arm64', 'linux-x64'];
439439
const platformSuffixes = {
440440
'win32-x64': 'win',
441-
'darwin-x64': 'macos',
441+
'darwin-x64': 'macos',
442+
'darwin-arm64': 'macos-arm64',
442443
'linux-x64': 'linux'
443444
};
444445

@@ -482,17 +483,25 @@ async function build() {
482483
console.log('');
483484
console.log('📦 Deployment instructions:');
484485
console.log('');
486+
console.log(' IMPORTANT: Create ../src-tauri/binaries/ directory if it doesn\'t exist:');
487+
console.log(' mkdir -p ../src-tauri/binaries (macOS/Linux)');
488+
console.log(' md ..\\src-tauri\\binaries (Windows, if not exists)');
489+
console.log('');
485490
console.log(' For Tauri (Windows):');
486-
console.log(' copy bin\\rpg-mcp-win.exe src-tauri\\binaries\\rpg-mcp-server-x86_64-pc-windows-msvc.exe');
487-
console.log(' copy bin\\better_sqlite3.node src-tauri\\binaries\\');
491+
console.log(' copy bin\\rpg-mcp-win.exe ..\\src-tauri\\binaries\\rpg-mcp-server-x86_64-pc-windows-msvc.exe');
492+
console.log(' copy bin\\better_sqlite3.node ..\\src-tauri\\binaries\\');
493+
console.log('');
494+
console.log(' For Tauri (macOS Intel):');
495+
console.log(' cp bin/rpg-mcp-macos ../src-tauri/binaries/rpg-mcp-server-x86_64-apple-darwin');
496+
console.log(' cp bin/better_sqlite3-macos.node ../src-tauri/binaries/better_sqlite3.node');
488497
console.log('');
489-
console.log(' For Tauri (macOS):');
490-
console.log(' cp bin/rpg-mcp-macos src-tauri/binaries/rpg-mcp-server-x86_64-apple-darwin');
491-
console.log(' cp bin/better_sqlite3-macos.node src-tauri/binaries/better_sqlite3.node');
498+
console.log(' For Tauri (macOS Apple Silicon):');
499+
console.log(' cp bin/rpg-mcp-macos-arm64 ../src-tauri/binaries/rpg-mcp-server-aarch64-apple-darwin');
500+
console.log(' cp bin/better_sqlite3-macos-arm64.node ../src-tauri/binaries/better_sqlite3.node');
492501
console.log('');
493502
console.log(' For Tauri (Linux):');
494-
console.log(' cp bin/rpg-mcp-linux src-tauri/binaries/rpg-mcp-server-x86_64-unknown-linux-gnu');
495-
console.log(' cp bin/better_sqlite3-linux.node src-tauri/binaries/better_sqlite3.node');
503+
console.log(' cp bin/rpg-mcp-linux ../src-tauri/binaries/rpg-mcp-server-x86_64-unknown-linux-gnu');
504+
console.log(' cp bin/better_sqlite3-linux.node ../src-tauri/binaries/better_sqlite3.node');
496505
console.log('');
497506

498507
} catch (error) {

src/engine/combat/engine.ts

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ export interface CombatParticipant {
6868
deathSaveFailures?: number; // 0-3, 3 = dead
6969
isStabilized?: boolean; // Unconscious but won't die
7070
isDead?: boolean; // Permanently defeated
71+
// COMBAT STATS (Auto-resolution)
72+
ac?: number; // Armor Class
73+
attackDamage?: string; // Default attack damage (e.g., "1d6+2")
74+
attackBonus?: number; // Default attack bonus used if none provided
7175
}
7276

7377
/**
@@ -193,6 +197,9 @@ export class CombatEngine {
193197
const rolledInitiative = this.rng.d20(p.initiativeBonus);
194198
return {
195199
...p,
200+
ac: p.ac,
201+
attackDamage: p.attackDamage,
202+
attackBonus: p.attackBonus,
196203
initiative: rolledInitiative,
197204
// Auto-detect isEnemy if not explicitly set
198205
isEnemy: p.isEnemy ?? this.detectIsEnemy(p.id, p.name),
@@ -258,30 +265,45 @@ export class CombatEngine {
258265

259266
/**
260267
* Auto-detect if a participant is an enemy based on ID/name patterns
268+
*
269+
* IMPORTANT: UUIDs (like "9e48fa16-0ee4-4b99-a1e0-a162528d1e24") are typically
270+
* player characters created via the UI. Pattern-based IDs (like "goblin-1",
271+
* "orc-archer-2") are typically spawned enemies. Default to false for UUIDs.
261272
*/
262273
private detectIsEnemy(id: string, name: string): boolean {
263274
const idLower = id.toLowerCase();
264275
const nameLower = name.toLowerCase();
265276

266-
// Common enemy patterns
277+
// Common enemy patterns - check NAME first (most reliable for determination)
267278
const enemyPatterns = [
268279
'goblin', 'orc', 'wolf', 'bandit', 'skeleton', 'zombie',
269280
'dragon', 'troll', 'ogre', 'kobold', 'gnoll', 'demon',
270281
'devil', 'undead', 'enemy', 'monster', 'creature', 'beast',
271282
'spider', 'rat', 'bat', 'slime', 'ghost', 'wraith',
272-
'dracolich', 'lich', 'vampire', 'golem', 'elemental'
283+
'dracolich', 'lich', 'vampire', 'golem', 'elemental',
284+
'cultist', 'thug', 'assassin', 'minion', 'guard', 'scout',
285+
'warrior', 'archer', 'mage', 'shaman', 'warlord', 'boss'
273286
];
274287

275-
// Check if ID or name contains enemy patterns
288+
// Check NAME for enemy patterns (more reliable since IDs can be UUIDs)
276289
for (const pattern of enemyPatterns) {
277-
if (idLower.includes(pattern) || nameLower.includes(pattern)) {
290+
if (nameLower.includes(pattern)) {
291+
return true;
292+
}
293+
}
294+
295+
// Check ID for enemy patterns (for pattern-based IDs like "goblin-1")
296+
for (const pattern of enemyPatterns) {
297+
if (idLower.includes(pattern)) {
278298
return true;
279299
}
280300
}
281301

282302
// Common player/ally patterns (not enemies)
283303
const allyPatterns = [
284-
'hero', 'player', 'pc', 'ally', 'companion', 'npc-friendly'
304+
'hero', 'player', 'pc', 'ally', 'companion', 'npc-friendly',
305+
'party', 'adventurer', 'cleric', 'paladin', 'ranger', 'rogue',
306+
'wizard', 'sorcerer', 'warlock', 'bard', 'druid', 'monk', 'fighter'
285307
];
286308

287309
for (const pattern of allyPatterns) {
@@ -290,8 +312,23 @@ export class CombatEngine {
290312
}
291313
}
292314

293-
// Default: assume it's an enemy if not clearly a player
294-
return !idLower.startsWith('player') && !idLower.startsWith('hero');
315+
// Check if ID looks like a UUID (player characters created via UI have UUIDs)
316+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
317+
if (uuidPattern.test(id)) {
318+
// UUIDs are typically player characters - default to NOT enemy
319+
return false;
320+
}
321+
322+
// Default: for non-UUID IDs that don't match patterns, check if ID starts with enemy pattern
323+
// This catches pattern-based IDs like "enemy-1" or "mob-3"
324+
if (idLower.startsWith('enemy') || idLower.startsWith('mob') || idLower.startsWith('hostile')) {
325+
return true;
326+
}
327+
328+
// Fallback default: unknown entities default to NOT enemy
329+
// Reasoning: It's safer to have an enemy show as friendly (player corrects it)
330+
// than to have a player character show as enemy (breaks immersion)
331+
return false;
295332
}
296333

297334
/**
@@ -511,7 +548,7 @@ export class CombatEngine {
511548
targetId: string,
512549
attackBonus: number,
513550
dc: number,
514-
damage: number,
551+
damage: number | string,
515552
damageType?: string // HIGH-002: Optional damage type for resistance calculation
516553
): CombatActionResult {
517554
if (!this.state) throw new Error('No active combat');
@@ -530,10 +567,26 @@ export class CombatEngine {
530567
let damageDealt = 0;
531568
let damageModifier: 'immune' | 'resistant' | 'vulnerable' | 'normal' = 'normal';
532569

570+
// Calculate base damage from number or string
571+
let baseDamageVal = 0;
572+
let damageBreakdownStr = '';
573+
574+
if (typeof damage === 'string') {
575+
const dmgResult = this.rng.rollDamageDetailed(damage);
576+
baseDamageVal = dmgResult.total;
577+
damageBreakdownStr = ` (${dmgResult.rolls.join('+')}${dmgResult.modifier >= 0 ? '+' + dmgResult.modifier : dmgResult.modifier})`;
578+
} else {
579+
baseDamageVal = damage;
580+
}
581+
533582
if (attackRoll.isHit) {
534-
const baseDamage = attackRoll.isCrit ? damage * 2 : damage;
583+
// Critical Hit: Double the dice (approx. double the value for now if passing number)
584+
// If string was passed, we ideally double the DICE, but for now double the total is consistent with current impl.
585+
// TODO: Implement proper crit rules (double dice) later using rollDamageDetailed
586+
const finalBaseDamage = attackRoll.isCrit ? baseDamageVal * 2 : baseDamageVal;
587+
535588
// HIGH-002: Apply resistance/vulnerability/immunity
536-
const modResult = this.calculateDamageWithModifiers(baseDamage, damageType, target);
589+
const modResult = this.calculateDamageWithModifiers(finalBaseDamage, damageType, target);
537590
damageDealt = modResult.finalDamage;
538591
damageModifier = modResult.modifier;
539592
target.hp = Math.max(0, target.hp - damageDealt);
@@ -566,7 +619,7 @@ export class CombatEngine {
566619
modStr = ' [Vulnerable - Doubled!]';
567620
}
568621

569-
breakdown += `\n\n💥 Damage: ${damageDealt}${typeStr}${attackRoll.isCrit ? ' (crit)' : ''}${modStr}\n`;
622+
breakdown += `\n\n💥 Damage: ${damageDealt}${typeStr}${damageBreakdownStr}${attackRoll.isCrit ? ' (crit)' : ''}${modStr}\n`;
570623
breakdown += ` ${target.name}: ${hpBefore}${target.hp}/${target.maxHp} HP`;
571624
if (defeated) {
572625
breakdown += ` [DEFEATED]`;
@@ -1033,27 +1086,43 @@ export class CombatEngine {
10331086

10341087
/**
10351088
* Enhanced nextTurn with condition processing and legendary action reset
1089+
* Now auto-skips dead participants (HP <= 0)
10361090
*/
10371091
nextTurnWithConditions(): CombatParticipant | null {
10381092
if (!this.state) return null;
10391093

10401094
// Process end-of-turn conditions for current participant (if not LAIR)
10411095
const currentParticipant = this.getCurrentParticipant();
1042-
if (currentParticipant) {
1096+
if (currentParticipant && currentParticipant.hp > 0) {
10431097
this.processEndOfTurnConditions(currentParticipant);
10441098
}
10451099

1046-
// Advance turn
1047-
this.state.currentTurnIndex++;
1100+
// Advance turn, automatically skipping dead participants
1101+
let iterations = 0;
1102+
const maxIterations = this.state.turnOrder.length + 1; // Safety limit
1103+
let newParticipant: CombatParticipant | null = null;
10481104

1049-
if (this.state.currentTurnIndex >= this.state.turnOrder.length) {
1050-
this.state.currentTurnIndex = 0;
1051-
this.state.round++;
1052-
}
1105+
do {
1106+
// Advance turn index
1107+
this.state.currentTurnIndex++;
1108+
1109+
if (this.state.currentTurnIndex >= this.state.turnOrder.length) {
1110+
this.state.currentTurnIndex = 0;
1111+
this.state.round++;
1112+
}
1113+
1114+
newParticipant = this.getCurrentParticipant();
1115+
iterations++;
1116+
1117+
// Exit if we found a living participant or exhausted all options
1118+
} while (
1119+
newParticipant &&
1120+
newParticipant.hp <= 0 &&
1121+
iterations < maxIterations
1122+
);
10531123

1054-
// Process start-of-turn conditions for new current participant (if not LAIR)
1055-
const newParticipant = this.getCurrentParticipant();
1056-
if (newParticipant) {
1124+
// Process start-of-turn conditions for new current participant (if alive)
1125+
if (newParticipant && newParticipant.hp > 0) {
10571126
this.processStartOfTurnConditions(newParticipant);
10581127
}
10591128

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Main entry point for the Unified MCP Simulation Server
2+
console.error('rpg-mcp-server v1.0.5 - FIXED SPELL VALIDATION');
23
export * from './schema/index.js';
34
export * from './storage/index.js';
45

src/schema/encounter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ export const TokenSchema = z.object({
103103
intelligence: z.number(),
104104
wisdom: z.number(),
105105
charisma: z.number()
106-
}).optional()
106+
}).optional(),
107+
// Combat Stats for Auto-Resolution
108+
ac: z.number().optional().describe('Armor Class for auto-resolution'),
109+
attackDamage: z.string().optional().describe('Default attack damage (e.g., "1d6+2")'),
110+
attackBonus: z.number().optional().describe('Default attack bonus')
107111
});
108112

109113
export type Token = z.infer<typeof TokenSchema>;

0 commit comments

Comments
 (0)