Android build v1.0.44
This commit is contained in:
+227
-13
@@ -2664,10 +2664,15 @@ h2 {
|
||||
|
||||
.crafting-layout {
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(134px, 0.42fr) minmax(248px, 1fr) minmax(190px, 0.72fr);
|
||||
grid-template-columns: minmax(160px, 1fr) minmax(0, 2fr);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.crafting-available-panel {
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(170px, 0.75fr);
|
||||
}
|
||||
|
||||
.crafting-filters {
|
||||
gap: 7px;
|
||||
}
|
||||
@@ -4154,13 +4159,14 @@ h2 {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
grid-template-columns: minmax(210px, 0.55fr) minmax(360px, 1fr) minmax(320px, 0.85fr);
|
||||
grid-template-columns: minmax(230px, 1fr) minmax(0, 2fr);
|
||||
margin-top: 13px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.crafting-filters,
|
||||
.crafting-available-panel,
|
||||
.crafting-list-panel,
|
||||
.crafting-detail-panel {
|
||||
background: var(--panel-light);
|
||||
@@ -4177,10 +4183,20 @@ h2 {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.crafting-available-panel {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: minmax(360px, 1.05fr) minmax(280px, 0.95fr);
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.crafting-filter-grid {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.crafting-filter-grid button,
|
||||
@@ -6573,7 +6589,8 @@ h2 {
|
||||
|
||||
.gear-summary,
|
||||
.equipment-layout,
|
||||
.crafting-layout {
|
||||
.crafting-layout,
|
||||
.crafting-available-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -7244,9 +7261,203 @@ h2 {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.admin-class-layout {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: minmax(220px, 0.35fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.admin-class-list {
|
||||
background: #1c1e25;
|
||||
border: 2px solid #090a0d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
outline: 2px solid #494754;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.admin-class-list button {
|
||||
align-items: center;
|
||||
background: var(--panel-light);
|
||||
border: 2px solid #090a0d;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
grid-template-columns: 38px 1fr;
|
||||
min-height: 54px;
|
||||
outline: 2px solid #41404a;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.admin-class-list button.active,
|
||||
.admin-class-list button:hover {
|
||||
outline-color: var(--class-color, var(--gold));
|
||||
}
|
||||
|
||||
.admin-class-list button > span,
|
||||
.admin-class-hero > span {
|
||||
align-items: center;
|
||||
background: var(--class-color, var(--gold));
|
||||
color: #111217;
|
||||
display: flex;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 13px;
|
||||
height: 38px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.admin-class-list strong,
|
||||
.admin-class-list small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-class-list small {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.admin-class-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-class-hero {
|
||||
align-items: center;
|
||||
background: var(--panel-light);
|
||||
border: 2px solid #090a0d;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 54px minmax(180px, auto) minmax(0, 1fr);
|
||||
outline: 2px solid #494754;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.admin-class-hero > span {
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.admin-class-hero h2 {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-class-hero small,
|
||||
.admin-class-hero p {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.admin-class-table {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-class-table-head,
|
||||
.admin-class-row {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: minmax(230px, 1.5fr) minmax(90px, 0.7fr) minmax(110px, 0.6fr) minmax(65px, 0.45fr) minmax(80px, 0.5fr) minmax(70px, 0.45fr);
|
||||
}
|
||||
|
||||
.admin-class-table-head {
|
||||
color: var(--gold);
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7px;
|
||||
padding: 0 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-class-row {
|
||||
background: var(--panel-light);
|
||||
border: 2px solid #090a0d;
|
||||
outline: 2px solid #494754;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.admin-class-row > span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-class-row > span:first-child {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.admin-class-row i {
|
||||
color: var(--gold);
|
||||
font-style: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-class-row strong,
|
||||
.admin-class-row small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-class-row small {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.admin-class-talent-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
}
|
||||
|
||||
.admin-class-talent {
|
||||
background: var(--panel-light);
|
||||
border: 2px solid #090a0d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
outline: 2px solid #494754;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.admin-class-talent > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-class-talent > div > span {
|
||||
color: var(--gold);
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.admin-class-talent small,
|
||||
.admin-class-talent p {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-class-talent em {
|
||||
color: var(--green);
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.admin-upgrade-toolbar,
|
||||
.admin-upgrade-step {
|
||||
.admin-upgrade-step,
|
||||
.admin-class-layout,
|
||||
.admin-class-hero,
|
||||
.admin-class-table-head,
|
||||
.admin-class-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -7678,15 +7889,18 @@ h2 {
|
||||
|
||||
.workshop-shell .crafting-layout {
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
grid-template-columns: minmax(150px, 1fr) minmax(0, 2fr);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filters {
|
||||
display: grid;
|
||||
.workshop-shell .crafting-available-panel {
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filters {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filter-grid,
|
||||
@@ -7695,7 +7909,7 @@ h2 {
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filter-grid {
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filter-grid button {
|
||||
@@ -7985,7 +8199,7 @@ h2 {
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-layout {
|
||||
grid-template-columns: 110px minmax(0, 1fr) 174px;
|
||||
grid-template-columns: 110px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filter-grid {
|
||||
@@ -8189,7 +8403,7 @@ h2 {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filters {
|
||||
.workshop-shell .crafting-available-panel {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import type { CSSProperties, Dispatch, SetStateAction } from 'react'
|
||||
|
||||
type AdminItem = {
|
||||
id: number
|
||||
@@ -76,6 +76,49 @@ type AdminUpgradePath = {
|
||||
toItemId: number
|
||||
}
|
||||
|
||||
type AdminAbility = {
|
||||
id: number
|
||||
classId: number
|
||||
slug: string
|
||||
name: string
|
||||
spellType: string
|
||||
cost: number
|
||||
cooldown: number
|
||||
power: number
|
||||
unlockLevel: number
|
||||
glyph: string
|
||||
description: string
|
||||
}
|
||||
|
||||
type AdminTalent = {
|
||||
id: number
|
||||
classId: number
|
||||
slug: string
|
||||
name: string
|
||||
maxRank: number
|
||||
tier: number
|
||||
branch: number
|
||||
prerequisiteTalentId: number | null
|
||||
prerequisiteRank: number
|
||||
prerequisiteName: string | null
|
||||
effectType: string
|
||||
effectValuePerRank: number
|
||||
glyph: string
|
||||
description: string
|
||||
}
|
||||
|
||||
type AdminClass = {
|
||||
id: number
|
||||
slug: string
|
||||
name: string
|
||||
resourceName: string
|
||||
maxResource: number
|
||||
themeColor: string
|
||||
description: string
|
||||
abilities: AdminAbility[]
|
||||
talents: AdminTalent[]
|
||||
}
|
||||
|
||||
type AdminData = {
|
||||
items: AdminItem[]
|
||||
encounters: AdminEncounter[]
|
||||
@@ -84,9 +127,10 @@ type AdminData = {
|
||||
craftingRecipes: AdminRecipe[]
|
||||
dungeons: AdminDungeon[]
|
||||
gearUpgradePaths: AdminUpgradePath[]
|
||||
classes: AdminClass[]
|
||||
}
|
||||
|
||||
type AdminTab = 'items' | 'dungeons' | 'encounters' | 'loot' | 'crafting' | 'upgrades'
|
||||
type AdminTab = 'items' | 'dungeons' | 'encounters' | 'loot' | 'crafting' | 'upgrades' | 'classes'
|
||||
type SavingState = Record<string, boolean>
|
||||
type SetData = Dispatch<SetStateAction<AdminData | null>>
|
||||
type SetSaving = Dispatch<SetStateAction<SavingState>>
|
||||
@@ -99,6 +143,7 @@ const tabs: { id: AdminTab; label: string }[] = [
|
||||
{ id: 'loot', label: 'Loot' },
|
||||
{ id: 'crafting', label: 'Crafting' },
|
||||
{ id: 'upgrades', label: 'Upgrades' },
|
||||
{ id: 'classes', label: 'Classes' },
|
||||
]
|
||||
|
||||
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
@@ -143,6 +188,7 @@ export function AdminScreen({ onBack }: { onBack: () => void }) {
|
||||
{tab === 'loot' && <LootTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
||||
{tab === 'crafting' && <CraftingTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
||||
{tab === 'upgrades' && <UpgradesTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
||||
{tab === 'classes' && <ClassesTab data={data} />}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -830,7 +876,9 @@ function CraftingTab({ data, setData, setSaving, saving }: {
|
||||
)}
|
||||
|
||||
<h3 className="admin-loot-title">Required Components</h3>
|
||||
{(!recipe || recipe.components.length === 0) && <p className="admin-empty">No component requirements.</p>}
|
||||
{(!recipe || recipe.components.length === 0) && (
|
||||
<p className="admin-empty">No component requirements. Crafting and upgrades are blocked until materials are added.</p>
|
||||
)}
|
||||
<div className="admin-loot-list">
|
||||
{recipe?.components.map((comp) => (
|
||||
<div key={comp.itemId} className="admin-loot-row">
|
||||
@@ -981,7 +1029,7 @@ function UpgradesTab({ data, setData, setSaving, saving }: {
|
||||
{target
|
||||
? `Target requirements: ${targetRecipe && targetRecipe.components.length > 0
|
||||
? targetRecipe.components.map((component) => `${component.quantity}x ${itemName(data, component.itemId)}`).join(', ')
|
||||
: 'none'}`
|
||||
: 'none configured - upgrade blocked until materials are added'}`
|
||||
: 'No next upgrade selected.'}
|
||||
</p>
|
||||
<div className="admin-edit-actions">
|
||||
@@ -1015,6 +1063,98 @@ function UpgradesTab({ data, setData, setSaving, saving }: {
|
||||
)
|
||||
}
|
||||
|
||||
function ClassesTab({ data }: { data: AdminData }) {
|
||||
const [classId, setClassId] = useState(data.classes[0]?.id ?? 0)
|
||||
const selectedClass = data.classes.find((candidate) => candidate.id === classId)
|
||||
?? data.classes[0]
|
||||
?? null
|
||||
|
||||
return (
|
||||
<div className="admin-panel">
|
||||
<div className="admin-class-layout">
|
||||
<aside className="admin-class-list">
|
||||
<p className="eyebrow">Classes</p>
|
||||
{data.classes.map((gameClass) => (
|
||||
<button
|
||||
className={selectedClass?.id === gameClass.id ? 'active' : ''}
|
||||
key={gameClass.id}
|
||||
onClick={() => setClassId(gameClass.id)}
|
||||
style={{ '--class-color': gameClass.themeColor } as CSSProperties}
|
||||
type="button"
|
||||
>
|
||||
<span>{gameClass.name[0]}</span>
|
||||
<div>
|
||||
<strong>{gameClass.name}</strong>
|
||||
<small>{gameClass.resourceName} {gameClass.maxResource}</small>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
{selectedClass ? (
|
||||
<section className="admin-class-detail">
|
||||
<div className="admin-class-hero" style={{ '--class-color': selectedClass.themeColor } as CSSProperties}>
|
||||
<span>{selectedClass.name[0]}</span>
|
||||
<div>
|
||||
<p className="eyebrow">{selectedClass.slug}</p>
|
||||
<h2>{selectedClass.name}</h2>
|
||||
<small>{selectedClass.resourceName} pool: {selectedClass.maxResource}</small>
|
||||
</div>
|
||||
<p>{selectedClass.description}</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h3 className="admin-loot-title">Abilities ({selectedClass.abilities.length})</h3>
|
||||
<div className="admin-class-table">
|
||||
<div className="admin-class-table-head">
|
||||
<span>Ability</span>
|
||||
<span>Type</span>
|
||||
<span>Default Strength</span>
|
||||
<span>Cost</span>
|
||||
<span>Cooldown</span>
|
||||
<span>Unlock</span>
|
||||
</div>
|
||||
{selectedClass.abilities.map((ability) => (
|
||||
<div key={ability.id} className="admin-class-row">
|
||||
<span><i>{ability.glyph}</i><strong>{ability.name}</strong><small>{ability.description}</small></span>
|
||||
<span>{ability.spellType}</span>
|
||||
<span>{ability.power}</span>
|
||||
<span>{ability.cost}</span>
|
||||
<span>{ability.cooldown}s</span>
|
||||
<span>Lvl {ability.unlockLevel}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="admin-loot-title">Talents ({selectedClass.talents.length})</h3>
|
||||
<div className="admin-class-talent-grid">
|
||||
{selectedClass.talents.map((talent) => (
|
||||
<article key={talent.id} className="admin-class-talent">
|
||||
<div>
|
||||
<span>{talent.glyph}</span>
|
||||
<strong>{talent.name}</strong>
|
||||
</div>
|
||||
<small>Tier {talent.tier} · Branch {talent.branch} · Max {talent.maxRank}</small>
|
||||
<p>{talent.description}</p>
|
||||
<em>{talent.effectType}: {talent.effectValuePerRank}/rank</em>
|
||||
{talent.prerequisiteName && (
|
||||
<small>Requires {talent.prerequisiteName} rank {talent.prerequisiteRank}</small>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
) : (
|
||||
<p className="admin-empty">No classes found.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function jsonRequest(method: 'POST' | 'PUT', body: unknown): RequestInit {
|
||||
return {
|
||||
method,
|
||||
|
||||
@@ -596,8 +596,12 @@ export function EquipmentScreen({
|
||||
/>
|
||||
<div className="crafting-layout">
|
||||
<aside className="crafting-filters">
|
||||
<EquipmentHeading
|
||||
eyebrow="Slots"
|
||||
title="Gear Slots"
|
||||
detail={slotFilter === 'all' ? 'All' : SLOT_LABELS[slotFilter]}
|
||||
/>
|
||||
<div>
|
||||
<p className="eyebrow">Slot</p>
|
||||
<div className="crafting-filter-grid">
|
||||
<button
|
||||
className={slotFilter === 'all' ? 'active' : ''}
|
||||
@@ -657,38 +661,39 @@ export function EquipmentScreen({
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="crafting-list-panel">
|
||||
<EquipmentHeading
|
||||
eyebrow="Recipes"
|
||||
title={slotFilter === 'all' ? 'Available' : SLOT_LABELS[slotFilter]}
|
||||
detail={`Page ${recipePage + 1}/${recipePageCount}`}
|
||||
/>
|
||||
{filteredRecipes.length === 0 ? (
|
||||
<p className="inventory-empty">No recipes match filters.</p>
|
||||
) : (
|
||||
<div className="crafting-list">
|
||||
{recipePageItems.map((recipe) => (
|
||||
<button
|
||||
className={`${selectedRecipeId === recipe.id ? 'selected' : ''} rarity-${recipe.item.rarity}`}
|
||||
key={recipe.id}
|
||||
onClick={() => setSelectedRecipeId(recipe.id)}
|
||||
type="button"
|
||||
>
|
||||
<span>{recipe.item.glyph}</span>
|
||||
<div>
|
||||
<strong>{recipe.item.name}</strong>
|
||||
<small>
|
||||
{SLOT_LABELS[recipe.item.slot]} - Item Level {recipe.item.itemLevel}
|
||||
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
|
||||
</small>
|
||||
</div>
|
||||
<i className={recipe.canCraft ? 'ready' : 'missing'}>
|
||||
{recipe.canCraft ? 'Ready' : 'Needs materials'}
|
||||
</i>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<section className="crafting-available-panel">
|
||||
<section className="crafting-list-panel">
|
||||
<EquipmentHeading
|
||||
eyebrow="Available Gear"
|
||||
title={slotFilter === 'all' ? 'Craftable Gear' : SLOT_LABELS[slotFilter]}
|
||||
detail={`Page ${recipePage + 1}/${recipePageCount}`}
|
||||
/>
|
||||
{filteredRecipes.length === 0 ? (
|
||||
<p className="inventory-empty">No recipes match filters.</p>
|
||||
) : (
|
||||
<div className="crafting-list">
|
||||
{recipePageItems.map((recipe) => (
|
||||
<button
|
||||
className={`${selectedRecipeId === recipe.id ? 'selected' : ''} rarity-${recipe.item.rarity}`}
|
||||
key={recipe.id}
|
||||
onClick={() => setSelectedRecipeId(recipe.id)}
|
||||
type="button"
|
||||
>
|
||||
<span>{recipe.item.glyph}</span>
|
||||
<div>
|
||||
<strong>{recipe.item.name}</strong>
|
||||
<small>
|
||||
{SLOT_LABELS[recipe.item.slot]} - Item Level {recipe.item.itemLevel}
|
||||
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
|
||||
</small>
|
||||
</div>
|
||||
<i className={recipe.canCraft ? 'ready' : 'missing'}>
|
||||
{recipe.canCraft ? 'Ready' : 'Needs materials'}
|
||||
</i>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && (
|
||||
<ListPager
|
||||
label={`Page ${recipePage + 1} / ${recipePageCount}`}
|
||||
@@ -698,42 +703,46 @@ export function EquipmentScreen({
|
||||
previousDisabled={recipePage <= 0}
|
||||
/>
|
||||
)}
|
||||
<div className="crafting-action-row">
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe?.canCraft || crafting}
|
||||
onClick={craftSelected}
|
||||
type="button"
|
||||
>
|
||||
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="crafting-detail-panel">
|
||||
{selectedRecipe ? (
|
||||
<div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}>
|
||||
<ItemDetail title="Craft Output" item={{ ...selectedRecipe.item, quantity: 1, equipped: false }} />
|
||||
<div className="crafting-detail-heading">
|
||||
<p className="eyebrow">Materials</p>
|
||||
<span>{selectedRecipe.canCraft ? 'Ready' : 'Missing components'}</span>
|
||||
</div>
|
||||
<div className="crafting-components">
|
||||
{selectedRecipe.components.map((component) => (
|
||||
<div
|
||||
className={component.owned >= component.quantity ? 'ready' : 'missing'}
|
||||
key={component.item.id}
|
||||
>
|
||||
<span>{component.item.glyph}</span>
|
||||
<strong>{component.item.name}</strong>
|
||||
<i>{component.owned}/{component.quantity}</i>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="crafting-action-row">
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe?.canCraft || crafting}
|
||||
onClick={craftSelected}
|
||||
type="button"
|
||||
>
|
||||
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="inventory-empty">Select a recipe.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="crafting-detail-panel">
|
||||
{selectedRecipe ? (
|
||||
<div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}>
|
||||
<ItemDetail title="Craft Output" item={{ ...selectedRecipe.item, quantity: 1, equipped: false }} />
|
||||
<div className="crafting-detail-heading">
|
||||
<p className="eyebrow">Materials</p>
|
||||
<span>{selectedRecipe.canCraft ? 'Ready' : 'Missing components'}</span>
|
||||
</div>
|
||||
<div className="crafting-components">
|
||||
{selectedRecipe.components.length === 0 && (
|
||||
<p className="inventory-empty">No materials configured. Crafting disabled.</p>
|
||||
)}
|
||||
{selectedRecipe.components.map((component) => (
|
||||
<div
|
||||
className={component.owned >= component.quantity ? 'ready' : 'missing'}
|
||||
key={component.item.id}
|
||||
>
|
||||
<span>{component.item.glyph}</span>
|
||||
<strong>{component.item.name}</strong>
|
||||
<i>{component.owned}/{component.quantity}</i>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="inventory-empty">Select a recipe.</p>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -364,10 +364,13 @@ function updateCraftingRecipes(profile: CharacterProfile) {
|
||||
...component,
|
||||
owned: owned.get(component.item.id) ?? 0,
|
||||
}))
|
||||
const hasRequiredComponents = components.length > 0
|
||||
&& components.every((component) => component.quantity > 0)
|
||||
return {
|
||||
...recipe,
|
||||
components,
|
||||
canCraft: components.every((component) => component.owned >= component.quantity),
|
||||
canCraft: hasRequiredComponents
|
||||
&& components.every((component) => component.owned >= component.quantity),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1301,12 +1304,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
if (!recipe) throw new Error('That crafting recipe does not exist.')
|
||||
const requiresUpgrade = !DIRECT_CRAFT_ITEM_LEVELS.has(recipe.item.itemLevel)
|
||||
if (requiresUpgrade) throw new Error('Upgrade the previous item tier instead.')
|
||||
if (recipe.components.length === 0) throw new Error('That recipe has no component requirements.')
|
||||
const missing = recipe.components.find((component) => component.owned < component.quantity)
|
||||
if (missing) {
|
||||
throw new Error(`Need ${missing.quantity} ${missing.item.name} to craft this item.`)
|
||||
}
|
||||
|
||||
for (const component of recipe.components) {
|
||||
if (component.quantity <= 0) throw new Error('Recipe components must require at least one item.')
|
||||
const owned = profile.inventory.find((candidate) => candidate.id === component.item.id)
|
||||
if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to craft this item.`)
|
||||
owned.quantity -= component.quantity
|
||||
@@ -1331,12 +1336,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
? selectUpgradeRecipe(profile.gearUpgradePaths ?? [], profile.craftingRecipes, item)
|
||||
: null
|
||||
if (!targetRecipe) throw new Error('No upgrade is available for this item.')
|
||||
if (targetRecipe.components.length === 0) throw new Error('That upgrade has no component requirements.')
|
||||
const missing = targetRecipe.components.find((component) => component.owned < component.quantity)
|
||||
if (missing) {
|
||||
throw new Error(`Need ${missing.quantity} ${missing.item.name} to upgrade this item.`)
|
||||
}
|
||||
|
||||
for (const component of targetRecipe.components) {
|
||||
if (component.quantity <= 0) throw new Error('Upgrade components must require at least one item.')
|
||||
const owned = profile.inventory.find((candidate) => candidate.id === component.item.id)
|
||||
if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to upgrade this item.`)
|
||||
owned.quantity -= component.quantity
|
||||
|
||||
@@ -1797,7 +1797,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1203,
|
||||
@@ -1820,7 +1820,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1201,
|
||||
@@ -1843,7 +1843,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1204,
|
||||
@@ -1866,7 +1866,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1205,
|
||||
@@ -1889,7 +1889,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1206,
|
||||
@@ -1912,7 +1912,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1209,
|
||||
@@ -1935,7 +1935,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1208,
|
||||
@@ -1958,7 +1958,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1207,
|
||||
@@ -1981,7 +1981,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1302,
|
||||
|
||||
Reference in New Issue
Block a user