Skip to content

Commit 4ced785

Browse files
Alex MartinezAlex Martinez
authored andcommitted
fix: quest system bug fixes and agent behavior improvements
- Fix mobType always undefined in getNearbyEntities (stored in entity.config.properties, not entity.data) — mobs now correctly identified for kill stage targeting - Fix attack cancel-requeue loop: add 15s cooldown per target so walk-to-attack completes before re-issuing requestServerAttack - Fix findMobForQuest to use exact mobType match (barbarian_warchief no longer matches barbarian kill stage) - Fix moveTowardMobType includes() matching navigating agent to wrong mob - Add barbarian/warchief spawning for intermediate quest camps - Fix QuestSystem missing RUNECRAFTING/CRAFTING/FLETCHING_COMPLETE subscribers - Fix gathering: stop player movement before starting gather session - Fix ore/fish resource variants (were falling back to tree_normal) - Fix ore_essence 100% mining success rate - Fix inventory management during quest gather stages (drop non-target ores) - Fix pickup filter to ignore non-target ores during iron ore quest stage - All 10 quests verified working end-to-end with DESTINY agent
1 parent d9a540b commit 4ced785

35 files changed

+1571
-252
lines changed

bun.lock

Lines changed: 438 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@
207207
"@livekit/rtc-node-linux-x64-gnu": "0.13.24",
208208
"@nomicfoundation/hardhat-ignition": "^3.0.7",
209209
"@rollup/rollup-darwin-x64": "^4.52.4",
210+
"@trpc/client": "^11.10.0",
211+
"@trpc/server": "^11.10.0",
210212
"@types/pg": "^8.15.5",
211213
"@vitejs/plugin-react": "^5.1.4",
212214
"better-sqlite3": "^12.2.0",

packages/client/src/game/CoreUI.tsx

Lines changed: 306 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { EscapeMenu } from "./hud/EscapeMenu";
3030
import { ConnectionIndicator } from "./hud/ConnectionIndicator";
3131
import { NotificationContainer } from "@/ui/components";
3232
import { Disconnected, KickedOverlay, DeathScreen } from "./hud/overlays";
33+
import { SpectatorOverlay } from "./SpectatorOverlay";
3334
import {
3435
COLORS,
3536
spacing,
@@ -57,6 +58,14 @@ export function CoreUI({ world }: { world: ClientWorld }) {
5758
return config?.mode === "spectator";
5859
})();
5960

61+
// The entity being followed in spectator mode — prefer config, fall back to URL param
62+
const spectatorFollowId = (() => {
63+
const config = window.__HYPERSCAPE_CONFIG__;
64+
if (config?.followEntity) return config.followEntity;
65+
if (config?.characterId) return config.characterId;
66+
return new URLSearchParams(window.location.search).get("followEntity");
67+
})();
68+
6069
// Presentation gating flags
6170
const [playerReady, setPlayerReady] = useState(() => !!world.entities.player);
6271
const [physReady, setPhysReady] = useState(false);
@@ -67,6 +76,14 @@ export function CoreUI({ world }: { world: ClientWorld }) {
6776
const [disconnected, setDisconnected] = useState(false);
6877
const [kicked, setKicked] = useState<string | null>(null);
6978
const [characterFlowActive, setCharacterFlowActive] = useState(false);
79+
const [characterList, setCharacterList] = useState<
80+
Array<{ id: string; name: string; level?: number }>
81+
>(() => {
82+
const net = world.network as {
83+
lastCharacterList?: Array<{ id: string; name: string; level?: number }>;
84+
};
85+
return net.lastCharacterList || [];
86+
});
7087
const [deathScreen, setDeathScreen] = useState<{
7188
message: string;
7289
killedBy: string;
@@ -168,13 +185,24 @@ export function CoreUI({ world }: { world: ClientWorld }) {
168185
world.on(EventType.UI_DEATH_SCREEN_CLOSE, handleDeathScreenClose);
169186
// Character selection flow (server-flagged)
170187
// Define named handlers for proper cleanup (anonymous functions don't work with off())
171-
const handleCharacterList = (): void => setCharacterFlowActive(true);
188+
const handleCharacterList = (data: unknown): void => {
189+
const listData = data as {
190+
characters?: Array<{ id: string; name: string; level?: number }>;
191+
};
192+
setCharacterList(listData.characters || []);
193+
setCharacterFlowActive(true);
194+
};
172195
const handleCharacterSelected = (): void => setCharacterFlowActive(false);
173196
world.on("character:list", handleCharacterList);
174197
world.on("character:selected", handleCharacterSelected);
175198
// If the packet arrived before UI mounted, consult network cache
176-
const network = world.network as { lastCharacterList?: unknown[] };
177-
if (network.lastCharacterList) setCharacterFlowActive(true);
199+
const network = world.network as {
200+
lastCharacterList?: Array<{ id: string; name: string; level?: number }>;
201+
};
202+
if (network.lastCharacterList) {
203+
setCharacterList(network.lastCharacterList);
204+
setCharacterFlowActive(true);
205+
}
178206

179207
return () => {
180208
// Clean up the ready timeout if it exists
@@ -427,16 +455,17 @@ export function CoreUI({ world }: { world: ClientWorld }) {
427455
<div id="core-ui-portal" />
428456
</div>
429457
{/* Non-scaled overlays - full screen elements */}
430-
{!ready && (
431-
<LoadingScreen
432-
world={world}
433-
message={
434-
characterFlowActive ? "Entering world..." : "Loading world..."
435-
}
436-
/>
458+
{!ready && !characterFlowActive && (
459+
<LoadingScreen world={world} message="Loading world..." />
460+
)}
461+
{!ready && characterFlowActive && (
462+
<CharacterSelectOverlay world={world} characters={characterList} />
437463
)}
438464
{kicked && <KickedOverlay code={kicked} />}
439465
{deathScreen && <DeathScreen data={deathScreen} world={world} />}
466+
{ready && isSpectatorMode && spectatorFollowId && (
467+
<SpectatorOverlay characterId={spectatorFollowId} />
468+
)}
440469
</main>
441470
</ChatProvider>
442471
);
@@ -722,6 +751,273 @@ function ToastMsg({ text }: { text: string }) {
722751
);
723752
}
724753

754+
type CharEntry = { id: string; name: string; level?: number };
755+
756+
function CharacterSelectOverlay({
757+
world,
758+
characters,
759+
}: {
760+
world: ClientWorld;
761+
characters: CharEntry[];
762+
}) {
763+
const [showCreate, setShowCreate] = useState(false);
764+
const [newName, setNewName] = useState("");
765+
const [submitting, setSubmitting] = useState(false);
766+
const [error, setError] = useState("");
767+
768+
const network = world.network as {
769+
requestCharacterSelect: (id: string) => void;
770+
requestCharacterCreate: (name: string) => void;
771+
requestEnterWorld: () => void;
772+
};
773+
774+
// Auto-select newly created character
775+
useEffect(() => {
776+
const onCreated = (data: unknown) => {
777+
const created = data as { id: string; name: string };
778+
if (created.id) {
779+
handleSelect(created.id);
780+
}
781+
};
782+
world.on(EventType.CHARACTER_CREATED, onCreated);
783+
return () => world.off(EventType.CHARACTER_CREATED, onCreated);
784+
// eslint-disable-next-line react-hooks/exhaustive-deps
785+
}, [world]);
786+
787+
const handleSelect = (id: string) => {
788+
if (typeof sessionStorage !== "undefined") {
789+
sessionStorage.setItem("selectedCharacterId", id);
790+
}
791+
network.requestCharacterSelect(id);
792+
network.requestEnterWorld();
793+
};
794+
795+
const handleCreate = () => {
796+
const name = newName.trim();
797+
if (!name) {
798+
setError("Name cannot be empty.");
799+
return;
800+
}
801+
if (name.length < 2 || name.length > 20) {
802+
setError("Name must be 2–20 characters.");
803+
return;
804+
}
805+
setSubmitting(true);
806+
setError("");
807+
network.requestCharacterCreate(name);
808+
// Server responds with character:created → auto-selects via the effect above
809+
};
810+
811+
return (
812+
<div
813+
className="absolute inset-0 flex items-center justify-center pointer-events-auto"
814+
style={{ background: "rgba(0,0,0,0.85)", zIndex: 1000 }}
815+
>
816+
<div
817+
style={{
818+
background: COLORS.BG_SOLID,
819+
border: `1px solid ${COLORS.BORDER_SECONDARY}`,
820+
borderRadius: borderRadius.xl,
821+
boxShadow: shadows.panel,
822+
padding: spacing["2xl"],
823+
minWidth: "320px",
824+
maxWidth: "480px",
825+
width: "90%",
826+
}}
827+
>
828+
<h2
829+
style={{
830+
color: COLORS.TEXT_PRIMARY,
831+
fontFamily: typography.fontFamily.heading,
832+
fontSize: typography.fontSize["2xl"],
833+
fontWeight: typography.fontWeight.bold,
834+
marginBottom: spacing.lg,
835+
textAlign: "center",
836+
}}
837+
>
838+
{showCreate ? "Create Character" : "Select Character"}
839+
</h2>
840+
841+
{!showCreate && (
842+
<>
843+
<div
844+
style={{
845+
display: "flex",
846+
flexDirection: "column",
847+
gap: spacing.sm,
848+
marginBottom: spacing.lg,
849+
}}
850+
>
851+
{characters.length === 0 && (
852+
<p
853+
style={{
854+
color: COLORS.TEXT_SECONDARY,
855+
textAlign: "center",
856+
fontSize: typography.fontSize.sm,
857+
marginBottom: spacing.sm,
858+
}}
859+
>
860+
No characters yet. Create one to begin!
861+
</p>
862+
)}
863+
{characters.map((char) => (
864+
<button
865+
key={char.id}
866+
onClick={() => handleSelect(char.id)}
867+
style={{
868+
background: "rgba(255,255,255,0.05)",
869+
border: `1px solid ${COLORS.BORDER_SECONDARY}`,
870+
borderRadius: borderRadius.md,
871+
padding: `${spacing.md} ${spacing.lg}`,
872+
color: COLORS.TEXT_PRIMARY,
873+
fontFamily: typography.fontFamily.body,
874+
fontSize: typography.fontSize.base,
875+
cursor: "pointer",
876+
textAlign: "left",
877+
display: "flex",
878+
justifyContent: "space-between",
879+
alignItems: "center",
880+
transition: "background 0.15s",
881+
}}
882+
onMouseEnter={(e) => {
883+
(e.currentTarget as HTMLButtonElement).style.background =
884+
"rgba(255,255,255,0.12)";
885+
}}
886+
onMouseLeave={(e) => {
887+
(e.currentTarget as HTMLButtonElement).style.background =
888+
"rgba(255,255,255,0.05)";
889+
}}
890+
>
891+
<span style={{ fontWeight: typography.fontWeight.medium }}>
892+
{char.name}
893+
</span>
894+
{char.level !== undefined && (
895+
<span
896+
style={{
897+
color: COLORS.TEXT_SECONDARY,
898+
fontSize: typography.fontSize.sm,
899+
}}
900+
>
901+
Lv {char.level}
902+
</span>
903+
)}
904+
</button>
905+
))}
906+
</div>
907+
<button
908+
onClick={() => setShowCreate(true)}
909+
style={{
910+
width: "100%",
911+
background: "rgba(99,102,241,0.2)",
912+
border: `1px solid rgba(99,102,241,0.5)`,
913+
borderRadius: borderRadius.md,
914+
padding: `${spacing.sm} ${spacing.lg}`,
915+
color: COLORS.TEXT_PRIMARY,
916+
fontFamily: typography.fontFamily.body,
917+
fontSize: typography.fontSize.base,
918+
cursor: "pointer",
919+
fontWeight: typography.fontWeight.medium,
920+
}}
921+
>
922+
+ New Character
923+
</button>
924+
</>
925+
)}
926+
927+
{showCreate && (
928+
<>
929+
<input
930+
autoFocus
931+
type="text"
932+
value={newName}
933+
onChange={(e) => setNewName(e.target.value)}
934+
onKeyDown={(e) => {
935+
if (e.key === "Enter") handleCreate();
936+
}}
937+
placeholder="Enter character name"
938+
maxLength={20}
939+
disabled={submitting}
940+
style={{
941+
width: "100%",
942+
background: "rgba(255,255,255,0.07)",
943+
border: `1px solid ${error ? "rgba(239,68,68,0.7)" : COLORS.BORDER_SECONDARY}`,
944+
borderRadius: borderRadius.md,
945+
padding: `${spacing.md} ${spacing.lg}`,
946+
color: COLORS.TEXT_PRIMARY,
947+
fontFamily: typography.fontFamily.body,
948+
fontSize: typography.fontSize.base,
949+
outline: "none",
950+
marginBottom: spacing.sm,
951+
boxSizing: "border-box",
952+
}}
953+
/>
954+
{error && (
955+
<p
956+
style={{
957+
color: "rgba(239,68,68,0.9)",
958+
fontSize: typography.fontSize.sm,
959+
marginBottom: spacing.sm,
960+
}}
961+
>
962+
{error}
963+
</p>
964+
)}
965+
<div
966+
style={{
967+
display: "flex",
968+
gap: spacing.sm,
969+
marginTop: spacing.sm,
970+
}}
971+
>
972+
<button
973+
onClick={() => {
974+
setShowCreate(false);
975+
setNewName("");
976+
setError("");
977+
}}
978+
disabled={submitting}
979+
style={{
980+
flex: 1,
981+
background: "rgba(255,255,255,0.05)",
982+
border: `1px solid ${COLORS.BORDER_SECONDARY}`,
983+
borderRadius: borderRadius.md,
984+
padding: spacing.sm,
985+
color: COLORS.TEXT_SECONDARY,
986+
fontFamily: typography.fontFamily.body,
987+
fontSize: typography.fontSize.base,
988+
cursor: "pointer",
989+
}}
990+
>
991+
Back
992+
</button>
993+
<button
994+
onClick={handleCreate}
995+
disabled={submitting || !newName.trim()}
996+
style={{
997+
flex: 2,
998+
background: submitting
999+
? "rgba(99,102,241,0.1)"
1000+
: "rgba(99,102,241,0.35)",
1001+
border: `1px solid rgba(99,102,241,0.5)`,
1002+
borderRadius: borderRadius.md,
1003+
padding: spacing.sm,
1004+
color: COLORS.TEXT_PRIMARY,
1005+
fontFamily: typography.fontFamily.body,
1006+
fontSize: typography.fontSize.base,
1007+
cursor: submitting ? "default" : "pointer",
1008+
fontWeight: typography.fontWeight.medium,
1009+
}}
1010+
>
1011+
{submitting ? "Creating..." : "Create"}
1012+
</button>
1013+
</div>
1014+
</>
1015+
)}
1016+
</div>
1017+
</div>
1018+
);
1019+
}
1020+
7251021
function TouchBtns({ world }: { world: ClientWorld }) {
7261022
const theme = useThemeStore((s) => s.theme);
7271023
const [isAction, setIsAction] = useState(() => {

packages/client/src/game/EmbeddedGameClient.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,7 @@ export function EmbeddedGameClient() {
850850
setTerrainReady(true);
851851
clearTerrainPolling();
852852
}
853-
}, 100);
853+
}, 500);
854854

855855
terrainTimeoutRef.current = setTimeout(() => {
856856
// Failsafe: avoid infinite loading if readiness signal is unavailable.
@@ -895,7 +895,7 @@ export function EmbeddedGameClient() {
895895
checkAvatarReady();
896896
avatarPollRef.current = setInterval(() => {
897897
checkAvatarReady();
898-
}, 250);
898+
}, 500);
899899
avatarTimeoutRef.current = setTimeout(() => {
900900
// Failsafe: don't block forever if avatar event is missed.
901901
setTargetAvatarReady(true);

0 commit comments

Comments
 (0)