Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/mcp-bridge/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ async function getCharacters() {
return result.characters || [];
}

async function getCharacter() {
const profile = activeHandle.resolveProfileName();
if (!profile) return null;
const profileData = townClient.loadProfile(profile);
if (!profileData) return null;
const { result } = await authenticatedRequest('GET', `/api/characters/${profileData.id}`);
return result || null;
}

async function authenticatedRequest(method, apiPath, body) {
const { auth, result, profile } = await activeHandle.request(method, apiPath, body);
if (profile?.profile) setActiveProfileName(profile.profile);
Expand Down Expand Up @@ -250,13 +259,32 @@ function flushContext() {
});
}

function formatCharacter(character) {
if (!character) return '你还没有登录角色。';

let text = `🎭 【${character.name} 的角色面板】\n\n`;
text += `⭐ 等级: ${character.level} (经验: ${character.xp})\n`;
text += `❤️ 生命: ${character.hp}/${character.maxHp}\n`;
text += `💰 金币: ${character.gold}\n\n`;
text += `📈 属性:\n`;
text += ` 力 ${character.str} (STR)\n`;
text += ` 敏 ${character.dex} (DEX)\n`;
text += ` 智 ${character.int} (INT)\n`;
text += ` 体 ${character.vit} (VIT)\n\n`;
text += `🎁 可分配点数: ${character.statPoints}\n`;
text += `✨ 技能点数: ${character.skillPoints}`;

return text;
}

module.exports = {
connect,
disconnect,
login,
logout,
listProfiles,
getCharacters,
getCharacter,
getMap,
look,
walk,
Expand All @@ -273,6 +301,7 @@ module.exports = {
formatLogin: townClient.formatLogin,
formatProfilesList: townClient.formatProfilesList,
formatCharacters: townClient.formatCharacters,
formatCharacter,
formatMap: townClient.formatMap,
formatLook: townClient.formatLook,
formatWalk: townClient.formatWalk,
Expand Down
14 changes: 14 additions & 0 deletions packages/mcp-bridge/src/tools/character.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ const definitions = [
inputSchema: { type: 'object', properties: {} },
annotations: { title: 'Characters', readOnlyHint: true, destructiveHint: false, openWorldHint: false },
},
{
name: 'check_character',
description: '查看当前登录角色的属性面板,包括等级、经验、生命值、属性点和金币等',
inputSchema: { type: 'object', properties: {} },
annotations: { title: 'Check Character', readOnlyHint: true, destructiveHint: false, openWorldHint: false },
},
];

async function handle(name, args, client) {
Expand All @@ -53,6 +59,14 @@ async function handle(name, args, client) {
return { content: [{ type: 'text', text: client.formatCharacters(characters) }] };
}

if (name === 'check_character') {
const character = await client.getCharacter();
if (!character) {
return { content: [{ type: 'text', text: '你还没有登录角色,请先使用 login 命令登录。' }] };
}
return { content: [{ type: 'text', text: client.formatCharacter(character) }] };
}

return null;
}

Expand Down
107 changes: 105 additions & 2 deletions server/src/engine/world-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ const playerActivities = {};
const walkAborts = new Map();
const events = new EventEmitter();

const ZONE_REWARDS = {
weapon: { xp: 10, gold: 5 },
practice: { xp: 10, gold: 0 },
restaurant: { xp: 5, gold: 0 },
inn: { xp: 5, gold: 0 },
dock: { xp: 0, gold: 10 },
pond: { xp: 0, gold: 5 },
farm: { xp: 5, gold: 0 },
blacksmith: { xp: 10, gold: 0 },
shrine: { xp: 5, gold: 0 },
marketplace: { xp: 3, gold: 5 },
hotspring: { xp: 5, gold: 0 },
};

/** @type {import('./plugin-manager').PluginManager|null} */
let pluginManager = null;

Expand Down Expand Up @@ -317,6 +331,56 @@ function getProfileByHandle(handle) {
return sqliteStateStore.getProfileByHandle(handle);
}

function getOrCreateCharacter(profileId, name) {
const existing = sqliteStateStore.getCharacterByProfileId(profileId);
if (existing) return existing;

const now = new Date().toISOString();
const character = sqliteStateStore.createCharacter({
id: nextSnowflakeId(),
profileId,
name,
level: 1,
xp: 0,
hp: 100,
maxHp: 100,
str: 5,
dex: 5,
int: 5,
vit: 5,
gold: 50,
statPoints: 0,
skillPoints: 0,
createdAt: now,
updatedAt: now,
});
return character;
}

function getCharacter(profileId) {
return sqliteStateStore.getCharacterByProfileId(profileId);
}

function getCharacterById(id) {
return sqliteStateStore.getCharacter(id);
}

function updateCharacter(id, updates) {
return sqliteStateStore.updateCharacter(id, updates);
}

function addCharacterXp(id, amount) {
return sqliteStateStore.addCharacterXp(id, amount);
}

function addCharacterGold(id, amount) {
return sqliteStateStore.addCharacterGold(id, amount);
}

function allocateStatPoint(id, stat) {
return sqliteStateStore.allocateStatPoint(id, stat);
}

function verifyLoginProof(profile, timestamp, signature) {
if (!profile || !profile.publicKey || typeof signature !== 'string') return false;
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp)) return false;
Expand Down Expand Up @@ -361,6 +425,7 @@ function loginProfile(handle, timestamp, signature) {
const token = crypto.randomUUID();
const now = Date.now();
const player = join(profile.id, profile.name, profile.sprite, { trackActivity: true });
const character = getOrCreateCharacter(profile.id, profile.name);
const session = {
id: profile.id,
playerId: profile.id,
Expand All @@ -385,6 +450,7 @@ function loginProfile(handle, timestamp, signature) {
expires_at: new Date(session.expiresAt).toISOString(),
lease_expires_at: new Date(session.leaseExpiresAt).toISOString(),
player: sanitize(player),
character,
message: hadActiveSession ? `已接管角色 ${profile.name} 的在线会话。` : `已登录角色 ${profile.name}。`,
};
}
Expand Down Expand Up @@ -691,6 +757,36 @@ function interact(playerId, item) {
player.interactionText = result.action;
player.interactionIcon = result.icon || '';
player.interactionSound = result.sound || 'interact';

// Determine zone category for rewards
let zoneCategory = null;
if (zone) {
const normalizedName = (zone.name || '').toLowerCase();
for (const [matcher, category] of ZONE_CATEGORY_MAP) {
if (matcher.test(normalizedName)) {
zoneCategory = category;
break;
}
}
}

// Grant XP/Gold rewards if applicable
const rewards = ZONE_REWARDS[zoneCategory];
let rewardText = '';
if (rewards) {
const character = getOrCreateCharacter(playerId, player.name);
if (character) {
if (rewards.xp > 0) {
addCharacterXp(character.id, rewards.xp);
rewardText += ` +${rewards.xp} XP`;
}
if (rewards.gold > 0) {
addCharacterGold(character.id, rewards.gold);
rewardText += ` +${rewards.gold} Gold`;
}
}
}

emitPerception('interact', playerId, player.name, player.x, player.y, { zone: zone ? zone.name : '小镇街道', action: result.action });
broadcast();
setTimeout(() => {
Expand All @@ -712,8 +808,8 @@ function interact(playerId, item) {
item: result.item || item || null,
};
events.emit('interaction', entry);
addActivity(playerId, { type: 'interact', text: `在${zone ? zone.name : '街道'}: ${result.action}` });
return { zone: zone ? zone.name : '小镇街道', ...result };
addActivity(playerId, { type: 'interact', text: `在${zone ? zone.name : '街道'}: ${result.action}${rewardText}` });
return { zone: zone ? zone.name : '小镇街道', rewards: rewards || null, ...result };
}

function look(playerId) {
Expand Down Expand Up @@ -777,6 +873,13 @@ module.exports = {
getTokenSession,
getProfile,
getProfileByHandle,
getOrCreateCharacter,
getCharacter,
getCharacterById,
updateCharacter,
addCharacterXp,
addCharacterGold,
allocateStatPoint,
pruneExpiredSessions,
join,
removePlayer,
Expand Down
Loading
Loading