Skip to content
Open
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
69 changes: 21 additions & 48 deletions packages/client/src/game/dashboard/AgentSkillsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { GAME_API_URL } from "@/lib/api-config";
import React, { useState, useEffect, useRef } from "react";
import type { Agent } from "./types";
import { ChevronDown, ChevronUp, Swords, TrendingUp } from "lucide-react";
import {
calculateCombatLevel,
normalizeCombatSkills,
getXPForLevel,
getXPProgress,
type Skills,
} from "@hyperscape/shared";

// Configuration constants
const SKILLS_POLL_INTERVAL_MS = 10000; // Poll every 10 seconds to avoid rate limiting
Expand Down Expand Up @@ -47,23 +54,7 @@ async function fetchWithRetry(
throw lastError || new Error("Request failed after retries");
}

interface SkillData {
level: number;
xp: number;
}

interface AgentSkills {
attack?: SkillData;
strength?: SkillData;
defense?: SkillData;
constitution?: SkillData;
// ranged?: SkillData; // Hidden for melee-only MVP
woodcutting?: SkillData;
fishing?: SkillData;
firemaking?: SkillData;
cooking?: SkillData;
agility?: SkillData;
}
type AgentSkills = Partial<Skills>;

interface AgentSkillsPanelProps {
agent: Agent;
Expand All @@ -84,36 +75,6 @@ const SKILL_CONFIG = [
{ key: "agility", label: "Agility", icon: "🏃" },
] as const;

// XP calculation for progress bar
function calculateXPForLevel(level: number): number {
let total = 0;
for (let i = 1; i < level; i++) {
total += Math.floor(i + 300 * Math.pow(2, i / 7));
}
return Math.floor(total / 4);
}

function getXPProgress(xp: number, level: number): number {
if (level >= MAX_SKILL_LEVEL) return 100;
const currentLevelXP = calculateXPForLevel(level);
const nextLevelXP = calculateXPForLevel(level + 1);
const xpIntoLevel = xp - currentLevelXP;
const xpForThisLevel = nextLevelXP - currentLevelXP;
return Math.min(100, Math.max(0, (xpIntoLevel / xpForThisLevel) * 100));
}

// Calculate combat level from skills (melee-only MVP)
function calculateCombatLevel(skills: AgentSkills): number {
const defense = skills.defense?.level || 1;
const constitution = skills.constitution?.level || 10;
const attack = skills.attack?.level || 1;
const strength = skills.strength?.level || 1;

const base = 0.25 * (defense + constitution);
const melee = 0.325 * (attack + strength);
return Math.floor(base + melee);
}

function SkillRow({
icon,
label,
Expand Down Expand Up @@ -351,7 +312,19 @@ export const AgentSkillsPanel: React.FC<AgentSkillsPanelProps> = ({
}, 0)
: 0;

const combatLevel = skills ? calculateCombatLevel(skills) : 3;
const combatLevel = skills
? calculateCombatLevel(
normalizeCombatSkills({
attack: skills.attack?.level,
strength: skills.strength?.level,
defense: skills.defense?.level,
constitution: skills.constitution?.level,
ranged: skills.ranged?.level,
magic: skills.magic?.level,
prayer: skills.prayer?.level,
}),
)
: 3;

return (
<div className="border-t border-[#8b4513]/30 bg-[#0b0a15]/80">
Expand Down
46 changes: 30 additions & 16 deletions packages/client/src/game/dashboard/AgentSummaryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { GAME_API_URL } from "@/lib/api-config";
import React, { useState, useEffect, useRef, useCallback } from "react";
import type { Agent } from "./types";
import { Swords, Activity, Target, Coins, Clock, Heart } from "lucide-react";
import {
calculateCombatLevel,
normalizeCombatSkills,
} from "@hyperscape/shared";

interface HealthData {
current: number;
Expand Down Expand Up @@ -67,20 +71,6 @@ function formatXp(xp: number): string {
return xp.toLocaleString();
}

// Calculate combat level from skills
function calculateCombatLevel(
skills: Record<string, { level: number }> | null,
): number {
if (!skills) return 3;
const defense = skills.defense?.level || 1;
const constitution = skills.constitution?.level || 10;
const attack = skills.attack?.level || 1;
const strength = skills.strength?.level || 1;
const base = 0.25 * (defense + constitution);
const melee = 0.325 * (attack + strength);
return Math.floor(base + melee);
}

// Calculate total level
function calculateTotalLevel(
skills: Record<string, { level: number }> | null,
Expand Down Expand Up @@ -199,7 +189,19 @@ export const AgentSummaryCard: React.FC<AgentSummaryCardProps> = ({
...prev,
online: posData.online !== false,
uptimeMs: Date.now() - sessionStartTime,
combatLevel: calculateCombatLevel(skills),
combatLevel: skills
? calculateCombatLevel(
normalizeCombatSkills({
attack: skills.attack?.level,
strength: skills.strength?.level,
defense: skills.defense?.level,
constitution: skills.constitution?.level,
ranged: skills.ranged?.level,
magic: skills.magic?.level,
prayer: skills.prayer?.level,
}),
)
: 3,
totalLevel: calculateTotalLevel(skills),
currentGoal: goalData?.goal?.description || null,
goalProgress: goalData?.goal?.progressPercent || 0,
Expand All @@ -215,7 +217,19 @@ export const AgentSummaryCard: React.FC<AgentSummaryCardProps> = ({
...prev,
online: agent.status === "active",
uptimeMs: Date.now() - sessionStartTime,
combatLevel: calculateCombatLevel(skills),
combatLevel: skills
? calculateCombatLevel(
normalizeCombatSkills({
attack: skills.attack?.level,
strength: skills.strength?.level,
defense: skills.defense?.level,
constitution: skills.constitution?.level,
ranged: skills.ranged?.level,
magic: skills.magic?.level,
prayer: skills.prayer?.level,
}),
)
: 3,
totalLevel: calculateTotalLevel(skills),
currentGoal: goalData?.goal?.description || null,
goalProgress: goalData?.goal?.progressPercent || 0,
Expand Down
20 changes: 11 additions & 9 deletions packages/client/src/game/hud/EntityContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import React, { useState, useEffect, useRef } from "react";
import type { World, LabelSegment } from "@hyperscape/shared";
import type {
World,
LabelSegment,
ContextMenuAction as SharedContextMenuAction,
} from "@hyperscape/shared";
import { useThemeStore } from "@/ui";
import { UI } from "@/ui/core";
import {
getContextMenuItemStyle,
getContextMenuSurfaceStyle,
} from "@/ui/theme/themes";

export interface ContextMenuAction {
id: string;
label: string;
/** Rich text label with colors/styles - takes precedence over label if present */
styledLabel?: LabelSegment[];
icon?: string;
enabled: boolean;
/** Context menu action for React rendering — shared action minus handler/priority, plus onClick */
export type ContextMenuAction = Omit<
SharedContextMenuAction,
"handler" | "priority"
> & {
onClick: () => void;
}
};

/**
* Render a styled label with colored segments.
Expand Down
Loading
Loading