Redesign guild treasury and territory toolbar; wallet API returns role and personal balance
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
12 files changed
+707
-166
No files matched your search
@@ -2,10 +2,10 @@
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
Shield, Lock, Plus, Trash2, Compass, ArrowUp, ArrowDown, ArrowLeft, ArrowRight,
|
||||
MapPin, Globe, LoaderCircle, CheckCircle2, AlertTriangle, RefreshCw
|
||||
MapPin, Globe, LoaderCircle, RefreshCw, Server
|
||||
} from '@lucide/svelte';
|
||||
import { invoke } from '../lib/tauri';
|
||||
import { toast } from '../lib/store.svelte';
|
||||
import { abs, toast } from '../lib/store.svelte';
|
||||
import type { GuildClaim } from '../lib/types';
|
||||
import VantageMap from './VantageMap.svelte';
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
onunclaim?: (claimId: number) => void;
|
||||
} = $props();
|
||||
|
||||
let servers = $state<Array<{ id: number; name: string; instance_id: string; map_url: string }>>([]);
|
||||
let servers = $state<Array<{ id: number; name: string; instance_id: string; map_url: string; live_map?: boolean }>>([]);
|
||||
let serverId = $state<number | null>(null);
|
||||
let dimension = $state<'minecraft:overworld' | 'minecraft:the_nether' | 'minecraft:the_end'>('minecraft:overworld');
|
||||
let centerX = $state(0);
|
||||
@@ -37,8 +37,26 @@
|
||||
let selectedChunk = $state<{ x: number; z: number } | null>(null);
|
||||
let mapDimensions = $state<string[]>([]);
|
||||
let mapFailed = $state(false);
|
||||
const mapUrl = $derived(servers.find((s) => s.id === serverId)?.map_url || '');
|
||||
$effect(() => { mapUrl; mapFailed = false; mapDimensions = []; });
|
||||
// The panel's central live map (if switched on for this server) wins over an external Vantage URL.
|
||||
type LiveInfo = { ready: boolean; base: string; dimensions: { id: string; available: boolean }[] };
|
||||
let liveInfo = $state<LiveInfo | null>(null);
|
||||
const instanceServers = $derived(servers.filter((s) => s.instance_id === instanceId));
|
||||
const selectedServer = $derived(servers.find((s) => s.id === serverId));
|
||||
$effect(() => {
|
||||
const id = selectedServer?.live_map ? selectedServer.id : null;
|
||||
liveInfo = null;
|
||||
if (!id) return;
|
||||
let alive = true;
|
||||
invoke<LiveInfo>('get_livemap_info', { serverId: id }).then((i) => { if (alive) liveInfo = i; }).catch(() => {});
|
||||
return () => { alive = false; };
|
||||
});
|
||||
const central = $derived(!!liveInfo?.ready);
|
||||
const dimSlug = $derived(dimension.replace('minecraft:', ''));
|
||||
const mapUrl = $derived(central && liveInfo ? abs(`${liveInfo.base}/${dimSlug}/v1/worlds/default/manifest.json`) ?? '' : selectedServer?.map_url || '');
|
||||
const dimDisabled = (id: string) => central
|
||||
? !liveInfo?.dimensions.find((d) => d.id === id)?.available
|
||||
: !!mapUrl && !mapFailed && mapDimensions.length > 0 && !mapDimensions.includes(id);
|
||||
$effect(() => { mapUrl; mapFailed = false; if (!central) mapDimensions = []; });
|
||||
|
||||
const dimensions = [
|
||||
{ id: 'minecraft:overworld', label: 'Overworld', icon: Globe },
|
||||
@@ -144,45 +162,53 @@
|
||||
</script>
|
||||
|
||||
<div class="chunk-map-container glass">
|
||||
<label>Game server
|
||||
<select bind:value={serverId}>
|
||||
{#each servers.filter(s => s.instance_id === instanceId) as server}<option value={server.id}>{server.name}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
{#if !serverId}<p>Link a game server to this instance in the panel before claiming territory.</p>{/if}
|
||||
<!-- Controls Bar -->
|
||||
<div class="map-controls">
|
||||
<div class="dim-selector">
|
||||
{#each dimensions as dim}
|
||||
{@const DimIcon = dim.icon}
|
||||
<button
|
||||
class="dim-btn"
|
||||
class:active={dimension === dim.id}
|
||||
disabled={!!mapUrl && !mapFailed && mapDimensions.length > 0 && !mapDimensions.includes(dim.id)}
|
||||
onclick={() => { dimension = dim.id; claims = []; selectedChunk = null; }}
|
||||
>
|
||||
<DimIcon size={14} />
|
||||
<span>{dim.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
<div class="toolbar">
|
||||
<div class="tb-group">
|
||||
{#if instanceServers.length > 1}
|
||||
<label class="server-pick" title="Game server"><Server size={14} />
|
||||
<select bind:value={serverId} aria-label="Game server">
|
||||
{#each instanceServers as server}<option value={server.id}>{server.name}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
{:else if instanceServers[0]}
|
||||
<span class="chip server-chip"><Server size={13} /> {instanceServers[0].name}</span>
|
||||
{/if}
|
||||
<div class="dim-selector" role="tablist" aria-label="Dimension">
|
||||
{#each dimensions as dim}
|
||||
{@const DimIcon = dim.icon}
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={dimension === dim.id}
|
||||
class="dim-btn"
|
||||
class:active={dimension === dim.id}
|
||||
disabled={dimDisabled(dim.id)}
|
||||
onclick={() => { dimension = dim.id; claims = []; selectedChunk = null; }}
|
||||
>
|
||||
<DimIcon size={14} />
|
||||
<span>{dim.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if central}<span class="chip live-chip"><span class="pulse"></span> Live map</span>{/if}
|
||||
</div>
|
||||
|
||||
<div class="nav-controls">
|
||||
<span class="coord-label">Center: [{centerX}, {centerZ}]</span>
|
||||
<div class="nav-buttons">
|
||||
<button class="ghost icon sm" onclick={() => pan(0, -2)} title="Pan North"><ArrowUp size={14} /></button>
|
||||
<div class="nav-row">
|
||||
<button class="ghost icon sm" onclick={() => pan(-2, 0)} title="Pan West"><ArrowLeft size={14} /></button>
|
||||
<button class="ghost icon sm" onclick={resetCenter} title="Center at (0,0)"><MapPin size={14} /></button>
|
||||
<button class="ghost icon sm" onclick={() => pan(2, 0)} title="Pan East"><ArrowRight size={14} /></button>
|
||||
</div>
|
||||
<button class="ghost icon sm" onclick={() => pan(0, 2)} title="Pan South"><ArrowDown size={14} /></button>
|
||||
<div class="tb-group">
|
||||
<span class="coord-label" title="Chunk at the centre of the view">Chunk {centerX}, {centerZ}</span>
|
||||
<div class="pad">
|
||||
<button class="ghost icon sm" onclick={() => pan(-2, 0)} title="Pan west" aria-label="Pan west"><ArrowLeft size={14} /></button>
|
||||
<button class="ghost icon sm" onclick={() => pan(0, -2)} title="Pan north" aria-label="Pan north"><ArrowUp size={14} /></button>
|
||||
<button class="ghost icon sm" onclick={resetCenter} title="Back to the world origin" aria-label="Centre on origin"><MapPin size={14} /></button>
|
||||
<button class="ghost icon sm" onclick={() => pan(0, 2)} title="Pan south" aria-label="Pan south"><ArrowDown size={14} /></button>
|
||||
<button class="ghost icon sm" onclick={() => pan(2, 0)} title="Pan east" aria-label="Pan east"><ArrowRight size={14} /></button>
|
||||
</div>
|
||||
<button class="ghost icon sm refresh-btn" onclick={loadClaims} title="Refresh Claims">
|
||||
<button class="ghost icon sm" onclick={loadClaims} title="Refresh claims" aria-label="Refresh claims">
|
||||
<RefreshCw size={14} class={loading ? 'spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if !serverId}
|
||||
<div class="notice"><Server size={16} /> Link a game server to this instance in the panel before claiming territory.</div>
|
||||
{/if}
|
||||
|
||||
<!-- Main Chunk Map Canvas / Grid -->
|
||||
{#if mapUrl && !mapFailed}
|
||||
@@ -190,6 +216,7 @@
|
||||
<VantageMap
|
||||
world={mapUrl}
|
||||
serverId={serverId!}
|
||||
{central}
|
||||
{claims}
|
||||
{dimension}
|
||||
{myGuildId}
|
||||
@@ -316,62 +343,22 @@
|
||||
border: 1px solid var(--line);
|
||||
user-select: none;
|
||||
}
|
||||
.map-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.dim-selector {
|
||||
display: flex;
|
||||
background: color-mix(in srgb, var(--bg) 60%, transparent);
|
||||
padding: 0.2rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--line);
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.dim-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 560;
|
||||
color: var(--muted);
|
||||
border-radius: calc(var(--radius-sm) - 2px);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.dim-btn.active {
|
||||
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
||||
color: var(--text);
|
||||
}
|
||||
.nav-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.coord-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
.nav-row {
|
||||
display: flex;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
.refresh-btn {
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
.toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.6rem 1rem; }
|
||||
.tb-group { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
|
||||
.server-pick { position: relative; display: inline-flex; align-items: center; }
|
||||
.server-pick :global(svg) { position: absolute; left: 0.7rem; color: var(--muted); pointer-events: none; }
|
||||
.server-pick select { width: auto; min-width: 10rem; padding: 0.4rem 2rem 0.4rem 2rem; font-size: 0.82rem; font-weight: 560; border-radius: 99rem; }
|
||||
.server-chip { padding: 0.4rem 0.8rem; border-radius: 99rem; font-size: 0.82rem; }
|
||||
.live-chip { border-radius: 99rem; color: var(--success); background: color-mix(in srgb, var(--success) 14%, transparent); }
|
||||
.pulse { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 0 color-mix(in srgb, var(--success) 60%, transparent); animation: pulse 2s infinite; }
|
||||
@keyframes pulse { 70% { box-shadow: 0 0 0 0.4rem transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
|
||||
.dim-selector { display: flex; background: color-mix(in srgb, var(--bg) 60%, transparent); padding: 0.2rem; border-radius: 99rem; border: 1px solid var(--line); gap: 0.15rem; }
|
||||
.dim-btn { display: flex; align-items: center; gap: 0.35rem; background: transparent; border: none; padding: 0.3rem 0.8rem; font-size: 0.8rem; font-weight: 560; color: var(--muted); border-radius: 99rem; transition: all 0.15s; }
|
||||
.dim-btn:hover:not(:disabled) { color: var(--text); }
|
||||
.dim-btn.active { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--text); }
|
||||
.coord-label { font-size: 0.78rem; font-weight: 600; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.pad { display: inline-flex; padding: 0.15rem; border-radius: var(--radius-sm); border: 1px solid var(--line); background: color-mix(in srgb, var(--bg) 60%, transparent); }
|
||||
.notice { display: flex; align-items: center; gap: 0.6rem; padding: 0.7rem 0.9rem; border-radius: var(--radius-sm); font-size: 0.85rem; color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent); }
|
||||
.grid-wrapper {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
|
||||
Reference in new issue
Block a user