Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd6a1ce3c7 | |||
| f8b98e6b23 | |||
| fc7c6488ea | |||
| ba6d3b614e | |||
| 88874933c3 |
@@ -2,5 +2,7 @@
|
|||||||
|
|
||||||
- AYN Thor main display: 6-inch AMOLED, 1920 x 1080, 120Hz.
|
- AYN Thor main display: 6-inch AMOLED, 1920 x 1080, 120Hz.
|
||||||
- AYN Thor secondary display: 3.92-inch AMOLED, 1240 x 1080, 60Hz.
|
- AYN Thor secondary display: 3.92-inch AMOLED, 1240 x 1080, 60Hz.
|
||||||
|
- AYN Thor UI sizing must be designed against Android CSS/layout viewport, not physical framebuffer pixels.
|
||||||
|
- Approximate Thor CSS viewports: main display 960 x 540, secondary display 620 x 540.
|
||||||
- User rebuilds app; do not rebuild APK unless explicitly requested.
|
- User rebuilds app; do not rebuild APK unless explicitly requested.
|
||||||
- Apply game changes to both web version and mobile app version.
|
- Apply game changes to both web version and mobile app version.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.warren.iwanttoheal"
|
applicationId "com.warren.iwanttoheal"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 43
|
versionCode 48
|
||||||
versionName "1.0.27"
|
versionName "1.0.30"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
+13
-8
@@ -479,9 +479,7 @@ WHERE id BETWEEN 901 AND 1409;
|
|||||||
UPDATE items
|
UPDATE items
|
||||||
SET rarity = CASE item_level
|
SET rarity = CASE item_level
|
||||||
WHEN 1 THEN 'common'
|
WHEN 1 THEN 'common'
|
||||||
WHEN 5 THEN 'common'
|
|
||||||
WHEN 10 THEN 'uncommon'
|
WHEN 10 THEN 'uncommon'
|
||||||
WHEN 15 THEN 'rare'
|
|
||||||
WHEN 20 THEN 'epic'
|
WHEN 20 THEN 'epic'
|
||||||
WHEN 25 THEN 'legendary'
|
WHEN 25 THEN 'legendary'
|
||||||
ELSE rarity
|
ELSE rarity
|
||||||
@@ -493,9 +491,7 @@ SET name = (
|
|||||||
SELECT
|
SELECT
|
||||||
CASE items.item_level
|
CASE items.item_level
|
||||||
WHEN 1 THEN 'Raw '
|
WHEN 1 THEN 'Raw '
|
||||||
WHEN 5 THEN 'Honed '
|
|
||||||
WHEN 10 THEN 'Green '
|
WHEN 10 THEN 'Green '
|
||||||
WHEN 15 THEN 'Blue '
|
|
||||||
WHEN 20 THEN 'Purple '
|
WHEN 20 THEN 'Purple '
|
||||||
WHEN 25 THEN 'Orange '
|
WHEN 25 THEN 'Orange '
|
||||||
ELSE ''
|
ELSE ''
|
||||||
@@ -1276,12 +1272,23 @@ SET difficulty_id = CASE
|
|||||||
END
|
END
|
||||||
WHERE id BETWEEN 901 AND 1409;
|
WHERE id BETWEEN 901 AND 1409;
|
||||||
|
|
||||||
|
DELETE FROM crafting_recipe_components
|
||||||
|
WHERE recipe_id IN (
|
||||||
|
SELECT crafting_recipes.id
|
||||||
|
FROM crafting_recipes
|
||||||
|
JOIN items ON items.id = crafting_recipes.item_id
|
||||||
|
WHERE items.item_level NOT IN (1, 10, 20, 25)
|
||||||
|
);
|
||||||
|
|
||||||
|
DELETE FROM crafting_recipes
|
||||||
|
WHERE item_id IN (
|
||||||
|
SELECT id FROM items WHERE item_level NOT IN (1, 10, 20, 25)
|
||||||
|
);
|
||||||
|
|
||||||
UPDATE items
|
UPDATE items
|
||||||
SET rarity = CASE item_level
|
SET rarity = CASE item_level
|
||||||
WHEN 1 THEN 'common'
|
WHEN 1 THEN 'common'
|
||||||
WHEN 5 THEN 'common'
|
|
||||||
WHEN 10 THEN 'uncommon'
|
WHEN 10 THEN 'uncommon'
|
||||||
WHEN 15 THEN 'rare'
|
|
||||||
WHEN 20 THEN 'epic'
|
WHEN 20 THEN 'epic'
|
||||||
WHEN 25 THEN 'legendary'
|
WHEN 25 THEN 'legendary'
|
||||||
ELSE rarity
|
ELSE rarity
|
||||||
@@ -1293,9 +1300,7 @@ SET name = (
|
|||||||
SELECT
|
SELECT
|
||||||
CASE items.item_level
|
CASE items.item_level
|
||||||
WHEN 1 THEN 'Raw '
|
WHEN 1 THEN 'Raw '
|
||||||
WHEN 5 THEN 'Honed '
|
|
||||||
WHEN 10 THEN 'Green '
|
WHEN 10 THEN 'Green '
|
||||||
WHEN 15 THEN 'Blue '
|
|
||||||
WHEN 20 THEN 'Purple '
|
WHEN 20 THEN 'Purple '
|
||||||
WHEN 25 THEN 'Orange '
|
WHEN 25 THEN 'Orange '
|
||||||
ELSE ''
|
ELSE ''
|
||||||
|
|||||||
+2107
-8
File diff suppressed because it is too large
Load Diff
+79
-61
@@ -328,7 +328,7 @@ function App() {
|
|||||||
: a.sequence - b.sequence)
|
: a.sequence - b.sequence)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="game-shell">
|
<main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''} ${screen === 'customize' ? 'workshop-shell' : ''}`}>
|
||||||
<header className="topbar app-header">
|
<header className="topbar app-header">
|
||||||
<button
|
<button
|
||||||
className="brand-button"
|
className="brand-button"
|
||||||
@@ -586,19 +586,82 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{(screen === 'dungeons' || screen === 'raids') && (
|
{(screen === 'dungeons' || screen === 'raids') && (
|
||||||
<section className="content-screen">
|
<section className="content-screen dungeon-run-screen">
|
||||||
<ScreenHeading
|
<ScreenHeading
|
||||||
eyebrow="Adventure"
|
eyebrow="Adventure"
|
||||||
title={activity.contentType === 'raid' ? 'Raids' : 'Dungeons'}
|
title={activity.contentType === 'raid' ? 'Raids' : 'Dungeons'}
|
||||||
onBack={() => setScreen('menu')}
|
onBack={() => setScreen('menu')}
|
||||||
/>
|
/>
|
||||||
<section className="run-setup-panel">
|
<div className="dungeon-run-board">
|
||||||
|
<div className="dungeon-run-main">
|
||||||
|
<article className="run-summary-card dungeon-focus-card">
|
||||||
|
<div className={`dungeon-art ${activity.contentType === 'raid' ? 'raid-art' : ''}`}>
|
||||||
|
{activityInitials(activity.name)}
|
||||||
|
</div>
|
||||||
|
<div className="run-summary-copy">
|
||||||
|
<p className="eyebrow">Selected Run</p>
|
||||||
|
<h2>{activity.name}</h2>
|
||||||
|
<p>{activity.description}</p>
|
||||||
|
<div className="tag-row">
|
||||||
|
<span>Level {activity.recommendedLevel}</span>
|
||||||
|
<span>{activity.partySize} Players</span>
|
||||||
|
<span>{selectedDifficulty.name}</span>
|
||||||
|
<span>iLvl {selectedDifficulty.droppedItemLevel}</span>
|
||||||
|
<span>{Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section className="run-setup-panel dungeon-choice-panel">
|
||||||
<div className="run-setup-heading">
|
<div className="run-setup-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Step 1</p>
|
<p className="eyebrow">Pick Run</p>
|
||||||
<h2>Item Level</h2>
|
<h2>{screen === 'raids' ? 'Raid' : 'Dungeon'}</h2>
|
||||||
</div>
|
</div>
|
||||||
<small>{screen === 'raids' ? 'Raid' : 'Dungeon'} tiers unlock by character level.</small>
|
<small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
|
||||||
|
</div>
|
||||||
|
<div className="activity-card-grid dungeon-choice-grid">
|
||||||
|
{tierActivityOptions.map((candidate) => {
|
||||||
|
const difficulty = candidate.difficulties.find(
|
||||||
|
(option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel,
|
||||||
|
) ?? candidate.difficulties[0]
|
||||||
|
const locked = profile.character.level < difficulty.unlockLevel
|
||||||
|
const selected = candidate.id === activity.id
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`activity-card ${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`}
|
||||||
|
disabled={locked}
|
||||||
|
key={candidate.id}
|
||||||
|
onClick={() => {
|
||||||
|
if (screen === 'raids') setSelectedRaidId(candidate.id)
|
||||||
|
else setSelectedDungeonId(candidate.id)
|
||||||
|
setSelectedDifficultyId(difficulty.id)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className={`dungeon-art ${candidate.contentType === 'raid' ? 'raid-art' : ''}`}>
|
||||||
|
{activityInitials(candidate.name)}
|
||||||
|
</span>
|
||||||
|
<strong>{candidate.name}</strong>
|
||||||
|
<small>{candidate.locationName}</small>
|
||||||
|
<i>
|
||||||
|
Level {candidate.recommendedLevel} | {candidate.partySize} Players
|
||||||
|
</i>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside className="dungeon-setup-rail">
|
||||||
|
<section className="run-setup-panel tier-setup-panel">
|
||||||
|
<div className="run-setup-heading">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Item Level</p>
|
||||||
|
<h2>Tier</h2>
|
||||||
|
</div>
|
||||||
|
<small>{screen === 'raids' ? 'Raid' : 'Dungeon'} tiers unlock by level.</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="tier-grid">
|
<div className="tier-grid">
|
||||||
{tierOptions.map((difficulty) => {
|
{tierOptions.map((difficulty) => {
|
||||||
@@ -636,62 +699,13 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="run-setup-panel">
|
<section className="run-setup-panel part-setup-panel">
|
||||||
<div className="run-setup-heading">
|
<div className="run-setup-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Step 2</p>
|
<p className="eyebrow">Start</p>
|
||||||
<h2>{screen === 'raids' ? 'Pick Raid' : 'Pick Dungeon'}</h2>
|
<h2>{sectionName}</h2>
|
||||||
</div>
|
|
||||||
<small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
|
|
||||||
</div>
|
|
||||||
<div className="activity-card-grid">
|
|
||||||
{tierActivityOptions.map((candidate) => {
|
|
||||||
const difficulty = candidate.difficulties.find(
|
|
||||||
(option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel,
|
|
||||||
) ?? candidate.difficulties[0]
|
|
||||||
const locked = profile.character.level < difficulty.unlockLevel
|
|
||||||
const selected = candidate.id === activity.id
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={`activity-card ${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`}
|
|
||||||
disabled={locked}
|
|
||||||
key={candidate.id}
|
|
||||||
onClick={() => {
|
|
||||||
if (screen === 'raids') setSelectedRaidId(candidate.id)
|
|
||||||
else setSelectedDungeonId(candidate.id)
|
|
||||||
setSelectedDifficultyId(difficulty.id)
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span className={`dungeon-art ${candidate.contentType === 'raid' ? 'raid-art' : ''}`}>
|
|
||||||
{activityInitials(candidate.name)}
|
|
||||||
</span>
|
|
||||||
<strong>{candidate.name}</strong>
|
|
||||||
<small>{candidate.locationName}</small>
|
|
||||||
<i>
|
|
||||||
Level {candidate.recommendedLevel} | {candidate.partySize} Players
|
|
||||||
</i>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<article className="run-summary-card">
|
|
||||||
<div className={`dungeon-art ${activity.contentType === 'raid' ? 'raid-art' : ''}`}>
|
|
||||||
{activityInitials(activity.name)}
|
|
||||||
</div>
|
|
||||||
<div className="run-summary-copy">
|
|
||||||
<p className="eyebrow">Step 3</p>
|
|
||||||
<h2>{activity.name}</h2>
|
|
||||||
<p>{activity.description}</p>
|
|
||||||
<div className="tag-row">
|
|
||||||
<span>Level {activity.recommendedLevel}</span>
|
|
||||||
<span>{activity.partySize} Players</span>
|
|
||||||
<span>{selectedDifficulty.name}</span>
|
|
||||||
<span>iLvl {selectedDifficulty.droppedItemLevel}</span>
|
|
||||||
<span>{Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<small>{difficultyLocked ? `Unlocks at level ${selectedDifficulty.unlockLevel}` : 'Choose a section to launch.'}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="part-picker">
|
<div className="part-picker">
|
||||||
{parts.map((p) => (
|
{parts.map((p) => (
|
||||||
@@ -711,7 +725,8 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</section>
|
||||||
|
|
||||||
<div className="difficulty-section compact-difficulty-section">
|
<div className="difficulty-section compact-difficulty-section">
|
||||||
<div className={`difficulty-summary ${difficultyLocked ? 'locked' : ''}`}>
|
<div className={`difficulty-summary ${difficultyLocked ? 'locked' : ''}`}>
|
||||||
<div>
|
<div>
|
||||||
@@ -722,10 +737,11 @@ function App() {
|
|||||||
<div><dt>Health</dt><dd>{selectedDifficulty.healthMultiplier.toFixed(2)}x</dd></div>
|
<div><dt>Health</dt><dd>{selectedDifficulty.healthMultiplier.toFixed(2)}x</dd></div>
|
||||||
<div><dt>Damage</dt><dd>{selectedDifficulty.damageMultiplier.toFixed(2)}x</dd></div>
|
<div><dt>Damage</dt><dd>{selectedDifficulty.damageMultiplier.toFixed(2)}x</dd></div>
|
||||||
<div><dt>XP</dt><dd>{selectedDifficulty.experienceMultiplier.toFixed(1)}x</dd></div>
|
<div><dt>XP</dt><dd>{selectedDifficulty.experienceMultiplier.toFixed(1)}x</dd></div>
|
||||||
<div><dt>Components</dt><dd>iLvl {selectedDifficulty.droppedItemLevel}</dd></div>
|
<div><dt>Loot</dt><dd>iLvl {selectedDifficulty.droppedItemLevel}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="loot-preview-section">
|
<div className="loot-preview-section">
|
||||||
<div className="equipment-heading toggle-heading">
|
<div className="equipment-heading toggle-heading">
|
||||||
<div>
|
<div>
|
||||||
@@ -874,6 +890,8 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import {
|
|||||||
} from '../dualScreen'
|
} from '../dualScreen'
|
||||||
|
|
||||||
const TICK_MS = 700
|
const TICK_MS = 700
|
||||||
const TARGET_RENDER_THROTTLE_MS = 180
|
|
||||||
|
|
||||||
type RoguelikeMode = 'dungeon' | 'raid'
|
type RoguelikeMode = 'dungeon' | 'raid'
|
||||||
type RoguelikeUpgradeTiming = 'boss' | 'encounter'
|
type RoguelikeUpgradeTiming = 'boss' | 'encounter'
|
||||||
@@ -387,8 +386,11 @@ export function CombatScreen({
|
|||||||
const nextFloatingTextId = useRef(1)
|
const nextFloatingTextId = useRef(1)
|
||||||
const combatRef = useRef(initialCombatState)
|
const combatRef = useRef(initialCombatState)
|
||||||
const selectedIdRef = useRef(partyTemplate[0].id)
|
const selectedIdRef = useRef(partyTemplate[0].id)
|
||||||
const selectedRenderTimeoutRef = useRef<number | null>(null)
|
const runCombatTickRef = useRef<() => void>(() => {})
|
||||||
const lastSelectedRenderAtRef = useRef(0)
|
const combatClockActiveRef = useRef(false)
|
||||||
|
const lastCombatTickAtRef = useRef(performance.now())
|
||||||
|
const statusRef = useRef(status)
|
||||||
|
const pausedRef = useRef(paused)
|
||||||
const { party, resource, enemyHealth, cooldowns, freeCastReady } = combatState
|
const { party, resource, enemyHealth, cooldowns, freeCastReady } = combatState
|
||||||
const encounter = encounters[encounterIndex]
|
const encounter = encounters[encounterIndex]
|
||||||
const currentPart = getCurrentPart(encounterIndex)
|
const currentPart = getCurrentPart(encounterIndex)
|
||||||
@@ -418,6 +420,9 @@ export function CombatScreen({
|
|||||||
enabled: dualScreenEnabled,
|
enabled: dualScreenEnabled,
|
||||||
} = useDualScreen()
|
} = useDualScreen()
|
||||||
|
|
||||||
|
statusRef.current = status
|
||||||
|
pausedRef.current = paused
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
runStartedAtRef.current = now
|
runStartedAtRef.current = now
|
||||||
@@ -438,32 +443,27 @@ export function CombatScreen({
|
|||||||
? nextState(combatRef.current)
|
? nextState(combatRef.current)
|
||||||
: nextState
|
: nextState
|
||||||
combatRef.current = next
|
combatRef.current = next
|
||||||
|
setSelectedId(selectedIdRef.current)
|
||||||
setCombatState(next)
|
setCombatState(next)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const syncSelectedTargetDom = useCallback((id: string) => {
|
||||||
|
document.querySelectorAll<HTMLButtonElement>('[data-party-member-id]').forEach((button) => {
|
||||||
|
const selected = button.dataset.partyMemberId === id
|
||||||
|
button.classList.toggle('selected', selected)
|
||||||
|
button.setAttribute('aria-pressed', String(selected))
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const setSelectedTargetId = useCallback((id: string) => {
|
const setSelectedTargetId = useCallback((id: string) => {
|
||||||
if (selectedIdRef.current === id) return
|
if (selectedIdRef.current === id) return
|
||||||
selectedIdRef.current = id
|
selectedIdRef.current = id
|
||||||
const now = performance.now()
|
syncSelectedTargetDom(id)
|
||||||
const elapsed = now - lastSelectedRenderAtRef.current
|
}, [syncSelectedTargetDom])
|
||||||
if (elapsed >= TARGET_RENDER_THROTTLE_MS) {
|
|
||||||
lastSelectedRenderAtRef.current = now
|
|
||||||
setSelectedId(id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (selectedRenderTimeoutRef.current !== null) return
|
|
||||||
selectedRenderTimeoutRef.current = window.setTimeout(() => {
|
|
||||||
selectedRenderTimeoutRef.current = null
|
|
||||||
lastSelectedRenderAtRef.current = performance.now()
|
|
||||||
setSelectedId(selectedIdRef.current)
|
|
||||||
}, TARGET_RENDER_THROTTLE_MS - elapsed)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => {
|
||||||
if (selectedRenderTimeoutRef.current !== null) {
|
syncSelectedTargetDom(selectedIdRef.current)
|
||||||
window.clearTimeout(selectedRenderTimeoutRef.current)
|
}, [combatState, syncSelectedTargetDom])
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => {
|
const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => {
|
||||||
const entry = { id: nextLogId.current++, text, tone }
|
const entry = { id: nextLogId.current++, text, tone }
|
||||||
@@ -854,9 +854,7 @@ export function CombatScreen({
|
|||||||
if (spell) castSpell(spell)
|
if (spell) castSpell(spell)
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
const runCombatTick = useCallback(() => {
|
||||||
if (status !== 'playing' || paused) return
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
const current = combatRef.current
|
const current = combatRef.current
|
||||||
const nextElapsedTicks = current.elapsedTicks + 1
|
const nextElapsedTicks = current.elapsedTicks + 1
|
||||||
const nextCooldowns = Object.fromEntries(
|
const nextCooldowns = Object.fromEntries(
|
||||||
@@ -1050,8 +1048,6 @@ export function CombatScreen({
|
|||||||
enemyHealth: nextEncounter.maxHealth,
|
enemyHealth: nextEncounter.maxHealth,
|
||||||
})
|
})
|
||||||
addLog(`${encounter.enemyName} defeated. ${nextEncounter.enemyName} approaches.`, 'system')
|
addLog(`${encounter.enemyName} defeated. ${nextEncounter.enemyName} approaches.`, 'system')
|
||||||
}, TICK_MS)
|
|
||||||
return () => window.clearInterval(timer)
|
|
||||||
}, [
|
}, [
|
||||||
addLog,
|
addLog,
|
||||||
addFloatingHeal,
|
addFloatingHeal,
|
||||||
@@ -1073,11 +1069,43 @@ export function CombatScreen({
|
|||||||
profile.character.name,
|
profile.character.name,
|
||||||
setCombat,
|
setCombat,
|
||||||
startPart,
|
startPart,
|
||||||
status,
|
|
||||||
currentPart,
|
currentPart,
|
||||||
paused,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
runCombatTickRef.current = runCombatTick
|
||||||
|
}, [runCombatTick])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'playing' && !paused) {
|
||||||
|
if (!combatClockActiveRef.current) {
|
||||||
|
lastCombatTickAtRef.current = performance.now()
|
||||||
|
combatClockActiveRef.current = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
combatClockActiveRef.current = false
|
||||||
|
}, [paused, status])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
if (
|
||||||
|
!combatClockActiveRef.current
|
||||||
|
|| statusRef.current !== 'playing'
|
||||||
|
|| pausedRef.current
|
||||||
|
) return
|
||||||
|
const now = performance.now()
|
||||||
|
const dueTicks = Math.min(4, Math.floor((now - lastCombatTickAtRef.current) / TICK_MS))
|
||||||
|
if (dueTicks <= 0) return
|
||||||
|
lastCombatTickAtRef.current += dueTicks * TICK_MS
|
||||||
|
for (let index = 0; index < dueTicks; index += 1) {
|
||||||
|
if (statusRef.current !== 'playing' || pausedRef.current) return
|
||||||
|
runCombatTickRef.current()
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!reward
|
!reward
|
||||||
@@ -1218,17 +1246,16 @@ export function CombatScreen({
|
|||||||
{party.map((member) => (
|
{party.map((member) => (
|
||||||
<button
|
<button
|
||||||
className={`party-member ${selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
|
className={`party-member ${selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
|
||||||
|
data-party-member-id={member.id}
|
||||||
key={member.id}
|
key={member.id}
|
||||||
onClick={() => setSelectedTargetId(member.id)}
|
onClick={() => setSelectedTargetId(member.id)}
|
||||||
aria-pressed={selectedId === member.id}
|
aria-pressed={selectedId === member.id}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{selectedId === member.id && (
|
|
||||||
<span className="target-marker" aria-hidden="true">
|
<span className="target-marker" aria-hidden="true">
|
||||||
<i />
|
<i />
|
||||||
Target
|
Target
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
<div className="member-header">
|
<div className="member-header">
|
||||||
<span className={`role role-${member.role.toLowerCase()}`}>{member.role[0]}</span>
|
<span className={`role role-${member.role.toLowerCase()}`}>{member.role[0]}</span>
|
||||||
<strong>{member.name}</strong>
|
<strong>{member.name}</strong>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type CharacterProfile,
|
type CharacterProfile,
|
||||||
type GameClass,
|
type GameClass,
|
||||||
} from '../profile'
|
} from '../profile'
|
||||||
|
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
|
||||||
import { EquipmentScreen } from './EquipmentScreen'
|
import { EquipmentScreen } from './EquipmentScreen'
|
||||||
import { TalentScreen } from './TalentScreen'
|
import { TalentScreen } from './TalentScreen'
|
||||||
|
|
||||||
@@ -14,7 +15,8 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
|
export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
|
||||||
const [activeTab, setActiveTab] = useState<'equipment' | 'talents' | 'class'>('class')
|
const [activeTab, setActiveTab] = useState<'equipment' | 'crafting' | 'talents' | 'class'>('class')
|
||||||
|
const { enabled: dualScreenEnabled } = useDualScreen()
|
||||||
const [classId, setClassId] = useState(profile.character.classId)
|
const [classId, setClassId] = useState(profile.character.classId)
|
||||||
const [slots, setSlots] = useState<Array<number | null>>(profile.abilitySlots)
|
const [slots, setSlots] = useState<Array<number | null>>(profile.abilitySlots)
|
||||||
const [selectedSlot, setSelectedSlot] = useState(0)
|
const [selectedSlot, setSelectedSlot] = useState(0)
|
||||||
@@ -63,6 +65,29 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const classWorkshopState = useMemo<DualScreenWorkshopState | null>(() => {
|
||||||
|
if (activeTab !== 'class') return null
|
||||||
|
return {
|
||||||
|
mode: 'class',
|
||||||
|
title: 'Ability Library',
|
||||||
|
subtitle: gameClass.name,
|
||||||
|
summary: `Selected slot ${selectedSlot + 1}. ${message || 'Choose an ability for the active loadout.'}`,
|
||||||
|
items: gameClass.spells.map((ability) => {
|
||||||
|
const locked = ability.unlockLevel > profile.character.level
|
||||||
|
const equipped = slots.includes(ability.id)
|
||||||
|
return {
|
||||||
|
glyph: locked ? 'L' : ability.glyph,
|
||||||
|
title: ability.name,
|
||||||
|
meta: locked ? `Level ${ability.unlockLevel}` : `${ability.cost} ${gameClass.resourceName}`,
|
||||||
|
detail: ability.description,
|
||||||
|
status: equipped ? 'Equipped' : locked ? 'Locked' : '',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}, [activeTab, gameClass, message, profile.character.level, selectedSlot, slots])
|
||||||
|
|
||||||
|
useDualScreenWorkshopPublisher(classWorkshopState, dualScreenEnabled)
|
||||||
|
|
||||||
async function persistChanges() {
|
async function persistChanges() {
|
||||||
saveScroll()
|
saveScroll()
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -80,7 +105,7 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="content-screen customize-screen">
|
<section className="content-screen customize-screen">
|
||||||
<div className="screen-heading">
|
<div className="screen-heading customize-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Character Workshop</p>
|
<p className="eyebrow">Character Workshop</p>
|
||||||
<h1>Customize Character</h1>
|
<h1>Customize Character</h1>
|
||||||
@@ -89,8 +114,10 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="customize-tabs" role="tablist" aria-label="Customize character sections">
|
<div className="customize-tabs" role="tablist" aria-label="Customize character sections">
|
||||||
|
<button className="back-button customize-tab-back" onClick={onBack} type="button">Back</button>
|
||||||
{([
|
{([
|
||||||
{ key: 'equipment', label: 'Equipment' },
|
{ key: 'equipment', label: 'Equipment' },
|
||||||
|
{ key: 'crafting', label: 'Crafting' },
|
||||||
{ key: 'talents', label: 'Talents' },
|
{ key: 'talents', label: 'Talents' },
|
||||||
{ key: 'class', label: 'Class' },
|
{ key: 'class', label: 'Class' },
|
||||||
] as const).map((tab) => (
|
] as const).map((tab) => (
|
||||||
@@ -110,6 +137,18 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
|
|||||||
{activeTab === 'equipment' && (
|
{activeTab === 'equipment' && (
|
||||||
<EquipmentScreen
|
<EquipmentScreen
|
||||||
embedded
|
embedded
|
||||||
|
mode="equipment"
|
||||||
|
showModeTabs={false}
|
||||||
|
profile={profile}
|
||||||
|
onUpdated={onSaved}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'crafting' && (
|
||||||
|
<EquipmentScreen
|
||||||
|
embedded
|
||||||
|
mode="crafting"
|
||||||
|
showModeTabs={false}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
onUpdated={onSaved}
|
onUpdated={onSaved}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+312
-117
@@ -9,6 +9,7 @@ import {
|
|||||||
type EquipmentSlot,
|
type EquipmentSlot,
|
||||||
type Item,
|
type Item,
|
||||||
} from '../profile'
|
} from '../profile'
|
||||||
|
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
|
||||||
|
|
||||||
const SLOT_LABELS: Record<EquipmentSlot, string> = {
|
const SLOT_LABELS: Record<EquipmentSlot, string> = {
|
||||||
weapon: 'Weapon',
|
weapon: 'Weapon',
|
||||||
@@ -24,16 +25,28 @@ const SLOT_LABELS: Record<EquipmentSlot, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EQUIPMENT_LIST_PAGE_SIZE = 3
|
const EQUIPMENT_LIST_PAGE_SIZE = 3
|
||||||
const CRAFTING_LIST_PAGE_SIZE = 6
|
const CRAFTING_LIST_PAGE_SIZE = 3
|
||||||
|
const CRAFTING_FILTER_SLOTS = (Object.keys(SLOT_LABELS) as EquipmentSlot[])
|
||||||
|
.filter((slot) => slot !== 'component')
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
profile: CharacterProfile
|
profile: CharacterProfile
|
||||||
onBack?: () => void
|
onBack?: () => void
|
||||||
onUpdated: (profile: CharacterProfile) => void
|
onUpdated: (profile: CharacterProfile) => void
|
||||||
embedded?: boolean
|
embedded?: boolean
|
||||||
|
mode?: 'equipment' | 'crafting'
|
||||||
|
showModeTabs?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }: Props) {
|
export function EquipmentScreen({
|
||||||
|
profile,
|
||||||
|
onBack,
|
||||||
|
onUpdated,
|
||||||
|
embedded = false,
|
||||||
|
mode,
|
||||||
|
showModeTabs = true,
|
||||||
|
}: Props) {
|
||||||
|
const { enabled: dualScreenEnabled } = useDualScreen()
|
||||||
const totalItemCount = profile.inventory.reduce(
|
const totalItemCount = profile.inventory.reduce(
|
||||||
(total, item) => total + item.quantity,
|
(total, item) => total + item.quantity,
|
||||||
0,
|
0,
|
||||||
@@ -49,7 +62,7 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
const [crafting, setCrafting] = useState(false)
|
const [crafting, setCrafting] = useState(false)
|
||||||
const [upgrading, setUpgrading] = useState(false)
|
const [upgrading, setUpgrading] = useState(false)
|
||||||
const [showSetBonuses, setShowSetBonuses] = useState(false)
|
const [showSetBonuses, setShowSetBonuses] = useState(false)
|
||||||
const [equipmentTab, setEquipmentTab] = useState<'equipment' | 'crafting'>('equipment')
|
const [equipmentTab, setEquipmentTab] = useState<'equipment' | 'crafting'>(mode ?? 'equipment')
|
||||||
const [inventoryPage, setInventoryPage] = useState(0)
|
const [inventoryPage, setInventoryPage] = useState(0)
|
||||||
const [recipePage, setRecipePage] = useState(0)
|
const [recipePage, setRecipePage] = useState(0)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
@@ -126,6 +139,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
},
|
},
|
||||||
[profile.craftingRecipes, slotFilter, levelFilter],
|
[profile.craftingRecipes, slotFilter, levelFilter],
|
||||||
)
|
)
|
||||||
|
const readyRecipeCount = filteredRecipes.filter((recipe) => recipe.canCraft).length
|
||||||
|
const slotRecipeCounts = useMemo(
|
||||||
|
() => new Map(
|
||||||
|
(Object.keys(SLOT_LABELS) as EquipmentSlot[]).map((slot) => [
|
||||||
|
slot,
|
||||||
|
profile.craftingRecipes.filter((recipe) => recipe.item.slot === slot).length,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
[profile.craftingRecipes],
|
||||||
|
)
|
||||||
const recipePageCount = Math.max(
|
const recipePageCount = Math.max(
|
||||||
1,
|
1,
|
||||||
Math.ceil(filteredRecipes.length / CRAFTING_LIST_PAGE_SIZE),
|
Math.ceil(filteredRecipes.length / CRAFTING_LIST_PAGE_SIZE),
|
||||||
@@ -147,12 +170,26 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
setRecipePage((current) => Math.min(current, recipePageCount - 1))
|
setRecipePage((current) => Math.min(current, recipePageCount - 1))
|
||||||
}, [recipePageCount])
|
}, [recipePageCount])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filteredRecipes.length === 0) {
|
||||||
|
setSelectedRecipeId(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!filteredRecipes.some((recipe) => recipe.id === selectedRecipeId)) {
|
||||||
|
setSelectedRecipeId(filteredRecipes[0].id)
|
||||||
|
}
|
||||||
|
}, [filteredRecipes, selectedRecipeId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (equipmentTab === 'crafting') {
|
if (equipmentTab === 'crafting') {
|
||||||
loadProfile().then((fresh) => onUpdated(fresh)).catch(() => {})
|
loadProfile().then((fresh) => onUpdated(fresh)).catch(() => {})
|
||||||
}
|
}
|
||||||
}, [equipmentTab])
|
}, [equipmentTab])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode) setEquipmentTab(mode)
|
||||||
|
}, [mode])
|
||||||
|
|
||||||
function saveScroll() {
|
function saveScroll() {
|
||||||
scrollRef.current = window.scrollY
|
scrollRef.current = window.scrollY
|
||||||
}
|
}
|
||||||
@@ -227,73 +264,15 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = (
|
function renderEquipmentActions() {
|
||||||
|
if (!selectedItem) {
|
||||||
|
return <p>Select an item to inspect it.</p>
|
||||||
|
}
|
||||||
|
if (selectedItem.slot === 'component') {
|
||||||
|
return <p className="component-note">Used in crafting.</p>
|
||||||
|
}
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
{!embedded && (
|
|
||||||
<div className="screen-heading">
|
|
||||||
<div>
|
|
||||||
<p className="eyebrow">Character Loadout</p>
|
|
||||||
<h1>Equipment</h1>
|
|
||||||
</div>
|
|
||||||
<button className="back-button" onClick={onBack} type="button">Back</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="gear-summary">
|
|
||||||
<div className="gear-character">
|
|
||||||
<span style={{ borderColor: profile.character.themeColor, color: profile.character.themeColor }}>
|
|
||||||
{profile.character.className[0]}
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<p className="eyebrow">{profile.character.className}</p>
|
|
||||||
<h2>{profile.character.name}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<GearStat value={profile.gearStats.averageItemLevel.toFixed(1)} label="Average Item Level" />
|
|
||||||
<GearStat value={`+${profile.gearStats.healingPower}`} label="Healing Power" />
|
|
||||||
<GearStat value={`+${profile.gearStats.maxResourceBonus}`} label={`Max ${profile.character.resourceName}`} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="equipment-tabs">
|
|
||||||
<button
|
|
||||||
className={`equipment-tab ${equipmentTab === 'equipment' ? 'active' : ''}`}
|
|
||||||
onClick={() => setEquipmentTab('equipment')}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Equipment
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`equipment-tab ${equipmentTab === 'crafting' ? 'active' : ''}`}
|
|
||||||
onClick={() => setEquipmentTab('crafting')}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Crafting
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{equipmentTab === 'equipment' ? (
|
|
||||||
<>
|
|
||||||
<section className="item-comparison">
|
|
||||||
{selectedItem ? (
|
|
||||||
selectedItem.slot === 'component' ? (
|
|
||||||
<>
|
|
||||||
<ItemDetail title="Crafting Component" item={selectedItem} />
|
|
||||||
<div className="equip-action">
|
|
||||||
<p className="component-note">Used in crafting.</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ItemDetail title={selectedItem.equipped ? 'Selected Equipment' : 'Inventory Item'} item={selectedItem} />
|
|
||||||
<div className="comparison-arrow">vs</div>
|
|
||||||
{comparisonItem && comparisonItem.id !== selectedItem.id ? (
|
|
||||||
<ItemDetail title="Currently Equipped" item={comparisonItem} />
|
|
||||||
) : (
|
|
||||||
<div className="item-detail empty-comparison">
|
|
||||||
<p className="eyebrow">Comparison</p>
|
|
||||||
<h2>{selectedItem.equipped ? 'Already Equipped' : 'Empty Slot'}</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="equip-action">
|
|
||||||
<ComparisonDelta selected={selectedItem} equipped={comparisonItem} />
|
<ComparisonDelta selected={selectedItem} equipped={comparisonItem} />
|
||||||
<button
|
<button
|
||||||
className="primary-button"
|
className="primary-button"
|
||||||
@@ -327,7 +306,166 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
: 'Break Down'}
|
: 'Break Down'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const workshopState = useMemo<DualScreenWorkshopState>(() => {
|
||||||
|
if (equipmentTab === 'crafting') {
|
||||||
|
if (!selectedRecipe) {
|
||||||
|
return {
|
||||||
|
mode: 'crafting',
|
||||||
|
title: 'Craft Output',
|
||||||
|
subtitle: 'No recipe selected',
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mode: 'crafting',
|
||||||
|
title: selectedRecipe.item.name,
|
||||||
|
subtitle: `${SLOT_LABELS[selectedRecipe.item.slot]} - Item Level ${selectedRecipe.item.itemLevel}`,
|
||||||
|
summary: selectedRecipe.item.description,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
glyph: selectedRecipe.item.glyph,
|
||||||
|
title: 'Craft Output',
|
||||||
|
meta: `+${selectedRecipe.item.healingPower} Healing Power / +${selectedRecipe.item.maxResourceBonus} Max Resource`,
|
||||||
|
status: selectedRecipe.canCraft ? 'Ready' : 'Missing components',
|
||||||
|
},
|
||||||
|
...selectedRecipe.components.map((component) => ({
|
||||||
|
glyph: component.item.glyph,
|
||||||
|
title: component.item.name,
|
||||||
|
meta: `Item Level ${component.item.itemLevel}`,
|
||||||
|
status: `${component.owned}/${component.quantity}`,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!selectedItem) {
|
||||||
|
return {
|
||||||
|
mode: 'equipment',
|
||||||
|
title: 'Equipment Detail',
|
||||||
|
subtitle: 'No item selected',
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mode: 'equipment',
|
||||||
|
title: selectedItem.slot === 'component' ? 'Crafting Component' : selectedItem.name,
|
||||||
|
subtitle: `${SLOT_LABELS[selectedItem.slot]} - Item Level ${selectedItem.itemLevel}`,
|
||||||
|
summary: selectedItem.description,
|
||||||
|
items: selectedItem.slot === 'component'
|
||||||
|
? [{
|
||||||
|
glyph: selectedItem.glyph,
|
||||||
|
title: selectedItem.name,
|
||||||
|
meta: `Owned: ${selectedItem.quantity}`,
|
||||||
|
status: 'Component',
|
||||||
|
}]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
glyph: selectedItem.glyph,
|
||||||
|
title: selectedItem.name,
|
||||||
|
meta: `+${selectedItem.healingPower} Healing Power / +${selectedItem.maxResourceBonus} Max Resource`,
|
||||||
|
status: selectedItem.equipped ? 'Equipped' : 'Inventory',
|
||||||
|
},
|
||||||
|
...(comparisonItem && comparisonItem.id !== selectedItem.id
|
||||||
|
? [{
|
||||||
|
glyph: comparisonItem.glyph,
|
||||||
|
title: comparisonItem.name,
|
||||||
|
meta: `+${comparisonItem.healingPower} Healing Power / +${comparisonItem.maxResourceBonus} Max Resource`,
|
||||||
|
status: 'Currently Equipped',
|
||||||
|
}]
|
||||||
|
: [{
|
||||||
|
title: selectedItem.equipped ? 'Already Equipped' : 'Empty Slot',
|
||||||
|
status: 'Comparison',
|
||||||
|
}]),
|
||||||
|
...(upgradeRecipe
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
glyph: upgradeRecipe.item.glyph,
|
||||||
|
title: `Upgrade to ${upgradeRecipe.item.name}`,
|
||||||
|
meta: `Item Level ${upgradeRecipe.item.itemLevel}`,
|
||||||
|
status: upgradeRecipe.canCraft ? 'Ready' : 'Missing materials',
|
||||||
|
},
|
||||||
|
...upgradeRecipe.components.map((component) => ({
|
||||||
|
glyph: component.item.glyph,
|
||||||
|
title: component.item.name,
|
||||||
|
meta: `Required for upgrade`,
|
||||||
|
status: `${component.owned}/${component.quantity}`,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}, [comparisonItem, equipmentTab, selectedItem, selectedRecipe, upgradeRecipe])
|
||||||
|
|
||||||
|
useDualScreenWorkshopPublisher(workshopState, dualScreenEnabled)
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
{!embedded && (
|
||||||
|
<div className="screen-heading">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Character Loadout</p>
|
||||||
|
<h1>Equipment</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="back-button" onClick={onBack} type="button">Back</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="gear-summary">
|
||||||
|
<div className="gear-character">
|
||||||
|
<span style={{ borderColor: profile.character.themeColor, color: profile.character.themeColor }}>
|
||||||
|
{profile.character.className[0]}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">{profile.character.className}</p>
|
||||||
|
<h2>{profile.character.name}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<GearStat value={profile.gearStats.averageItemLevel.toFixed(1)} label="Average Item Level" />
|
||||||
|
<GearStat value={`+${profile.gearStats.healingPower}`} label="Healing Power" />
|
||||||
|
<GearStat value={`+${profile.gearStats.maxResourceBonus}`} label={`Max ${profile.character.resourceName}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showModeTabs && (
|
||||||
|
<nav className="equipment-tabs">
|
||||||
|
<button
|
||||||
|
className={`equipment-tab ${equipmentTab === 'equipment' ? 'active' : ''}`}
|
||||||
|
onClick={() => setEquipmentTab('equipment')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Equipment
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`equipment-tab ${equipmentTab === 'crafting' ? 'active' : ''}`}
|
||||||
|
onClick={() => setEquipmentTab('crafting')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Crafting
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{equipmentTab === 'equipment' ? (
|
||||||
|
<>
|
||||||
|
<section className="item-comparison">
|
||||||
|
{selectedItem ? (
|
||||||
|
selectedItem.slot === 'component' ? (
|
||||||
|
<>
|
||||||
|
<ItemDetail title="Crafting Component" item={selectedItem} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ItemDetail title={selectedItem.equipped ? 'Selected Equipment' : 'Inventory Item'} item={selectedItem} />
|
||||||
|
<div className="comparison-arrow">vs</div>
|
||||||
|
{comparisonItem && comparisonItem.id !== selectedItem.id ? (
|
||||||
|
<ItemDetail title="Currently Equipped" item={comparisonItem} />
|
||||||
|
) : (
|
||||||
|
<div className="item-detail empty-comparison">
|
||||||
|
<p className="eyebrow">Comparison</p>
|
||||||
|
<h2>{selectedItem.equipped ? 'Already Equipped' : 'Empty Slot'}</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -335,6 +473,10 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="equipment-action-strip">
|
||||||
|
{renderEquipmentActions()}
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="equipment-layout">
|
<div className="equipment-layout">
|
||||||
<section className="equipped-panel">
|
<section className="equipped-panel">
|
||||||
<EquipmentHeading eyebrow="Currently Worn" title="Equipment Slots" />
|
<EquipmentHeading eyebrow="Currently Worn" title="Equipment Slots" />
|
||||||
@@ -430,42 +572,81 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
<section className="crafting-panel">
|
<section className="crafting-panel">
|
||||||
<EquipmentHeading
|
<EquipmentHeading
|
||||||
eyebrow="Crafting"
|
eyebrow="Crafting"
|
||||||
title="Recipes"
|
title="Workbench"
|
||||||
detail={`${filteredRecipes.filter((recipe) => recipe.canCraft).length} ready`}
|
detail={`${readyRecipeCount} ready / ${filteredRecipes.length} shown`}
|
||||||
/>
|
/>
|
||||||
<div className="crafting-filter-bar">
|
|
||||||
<select
|
|
||||||
className="filter-select"
|
|
||||||
value={slotFilter}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSlotFilter(e.target.value as EquipmentSlot | 'all')
|
|
||||||
setRecipePage(0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="all">All Slots</option>
|
|
||||||
{(Object.entries(SLOT_LABELS) as [EquipmentSlot, string][]).map(([slot, label]) => (
|
|
||||||
<option key={slot} value={slot}>{label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
className="filter-select"
|
|
||||||
value={levelFilter ?? ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLevelFilter(e.target.value === '' ? null : Number(e.target.value))
|
|
||||||
setRecipePage(0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="">All Levels</option>
|
|
||||||
{availableLevels.map((level) => (
|
|
||||||
<option key={level} value={level}>Item Level {level}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{filteredRecipes.length === 0 && (
|
|
||||||
<p className="inventory-empty">No crafting recipes match filters.</p>
|
|
||||||
)}
|
|
||||||
{filteredRecipes.length > 0 && (
|
|
||||||
<div className="crafting-layout">
|
<div className="crafting-layout">
|
||||||
|
<aside className="crafting-filters">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Slot</p>
|
||||||
|
<div className="crafting-filter-grid">
|
||||||
|
<button
|
||||||
|
className={slotFilter === 'all' ? 'active' : ''}
|
||||||
|
onClick={() => {
|
||||||
|
setSlotFilter('all')
|
||||||
|
setRecipePage(0)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<strong>All</strong>
|
||||||
|
<span>{profile.craftingRecipes.length}</span>
|
||||||
|
</button>
|
||||||
|
{CRAFTING_FILTER_SLOTS.map((slot) => (
|
||||||
|
<button
|
||||||
|
className={slotFilter === slot ? 'active' : ''}
|
||||||
|
disabled={(slotRecipeCounts.get(slot) ?? 0) === 0}
|
||||||
|
key={slot}
|
||||||
|
onClick={() => {
|
||||||
|
setSlotFilter(slot)
|
||||||
|
setRecipePage(0)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<strong>{SLOT_LABELS[slot]}</strong>
|
||||||
|
<span>{slotRecipeCounts.get(slot) ?? 0}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Item Level</p>
|
||||||
|
<div className="crafting-level-row">
|
||||||
|
<button
|
||||||
|
className={levelFilter === null ? 'active' : ''}
|
||||||
|
onClick={() => {
|
||||||
|
setLevelFilter(null)
|
||||||
|
setRecipePage(0)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
{availableLevels.map((level) => (
|
||||||
|
<button
|
||||||
|
className={levelFilter === level ? 'active' : ''}
|
||||||
|
key={level}
|
||||||
|
onClick={() => {
|
||||||
|
setLevelFilter(level)
|
||||||
|
setRecipePage(0)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{level}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</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">
|
<div className="crafting-list">
|
||||||
{recipePageItems.map((recipe) => (
|
{recipePageItems.map((recipe) => (
|
||||||
<button
|
<button
|
||||||
@@ -482,9 +663,13 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
|
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<i>{recipe.canCraft ? 'Ready' : 'Needs materials'}</i>
|
<i className={recipe.canCraft ? 'ready' : 'missing'}>
|
||||||
|
{recipe.canCraft ? 'Ready' : 'Needs materials'}
|
||||||
|
</i>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && (
|
{filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && (
|
||||||
<ListPager
|
<ListPager
|
||||||
label={`Page ${recipePage + 1} / ${recipePageCount}`}
|
label={`Page ${recipePage + 1} / ${recipePageCount}`}
|
||||||
@@ -494,10 +679,26 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
previousDisabled={recipePage <= 0}
|
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>
|
</div>
|
||||||
{selectedRecipe && (
|
</section>
|
||||||
|
|
||||||
|
<section className="crafting-detail-panel">
|
||||||
|
{selectedRecipe ? (
|
||||||
<div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}>
|
<div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}>
|
||||||
<ItemDetail title="Craft Output" item={{ ...selectedRecipe.item, quantity: 1, equipped: false }} />
|
<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">
|
<div className="crafting-components">
|
||||||
{selectedRecipe.components.map((component) => (
|
{selectedRecipe.components.map((component) => (
|
||||||
<div
|
<div
|
||||||
@@ -510,22 +711,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
className="primary-button"
|
|
||||||
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe.canCraft || crafting}
|
|
||||||
onClick={craftSelected}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="inventory-empty">Select a recipe.</p>
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{profile.setBonuses.length > 0 && (
|
{equipmentTab === 'equipment' && profile.setBonuses.length > 0 && (
|
||||||
<section className="set-bonus-panel">
|
<section className="set-bonus-panel">
|
||||||
<div className="equipment-heading toggle-heading">
|
<div className="equipment-heading toggle-heading">
|
||||||
<div>
|
<div>
|
||||||
@@ -561,11 +756,11 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (embedded) {
|
if (embedded) {
|
||||||
return <div className="equipment-screen embedded-screen">{content}</div>
|
return <div className={`equipment-screen embedded-screen ${equipmentTab === 'crafting' ? 'crafting-active' : ''}`}>{content}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="content-screen equipment-screen">
|
<section className={`content-screen equipment-screen ${equipmentTab === 'crafting' ? 'crafting-active' : ''}`}>
|
||||||
{content}
|
{content}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
+100
-1
@@ -56,12 +56,28 @@ export type DualScreenCombatState = {
|
|||||||
targetGroup: 0 | 1 | 2
|
targetGroup: 0 | 1 | 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DualScreenWorkshopState = {
|
||||||
|
mode: 'class' | 'equipment' | 'crafting' | 'talents'
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
summary?: string
|
||||||
|
items: Array<{
|
||||||
|
glyph?: string
|
||||||
|
title: string
|
||||||
|
meta?: string
|
||||||
|
detail?: string
|
||||||
|
status?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
type DualScreenMessage =
|
type DualScreenMessage =
|
||||||
| { type: 'combat-state'; state: DualScreenCombatState }
|
| { type: 'combat-state'; state: DualScreenCombatState }
|
||||||
|
| { type: 'workshop-state'; state: DualScreenWorkshopState }
|
||||||
| { type: 'companion-ready' }
|
| { type: 'companion-ready' }
|
||||||
| { type: 'companion-heartbeat' }
|
| { type: 'companion-heartbeat' }
|
||||||
| { type: 'control-action'; action: InputAction }
|
| { type: 'control-action'; action: InputAction }
|
||||||
| { type: 'combat-ended' }
|
| { type: 'combat-ended' }
|
||||||
|
| { type: 'workshop-ended' }
|
||||||
|
|
||||||
type DualScreenContextValue = {
|
type DualScreenContextValue = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -280,16 +296,64 @@ export function useDualScreenPublisher(
|
|||||||
}, [enabled, state])
|
}, [enabled, state])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDualScreenWorkshopPublisher(
|
||||||
|
state: DualScreenWorkshopState | null,
|
||||||
|
enabled: boolean,
|
||||||
|
) {
|
||||||
|
const stateRef = useRef(state)
|
||||||
|
useEffect(() => {
|
||||||
|
stateRef.current = state
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !state) return
|
||||||
|
const channel = createChannel()
|
||||||
|
if (!channel) return
|
||||||
|
const publish = () => {
|
||||||
|
if (stateRef.current) {
|
||||||
|
channel.postMessage({
|
||||||
|
type: 'workshop-state',
|
||||||
|
state: stateRef.current,
|
||||||
|
} satisfies DualScreenMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
|
||||||
|
if (event.data.type === 'companion-ready') publish()
|
||||||
|
}
|
||||||
|
publish()
|
||||||
|
return () => {
|
||||||
|
channel.postMessage({ type: 'workshop-ended' } satisfies DualScreenMessage)
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
}, [enabled, state])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !state) return
|
||||||
|
const channel = createChannel()
|
||||||
|
channel?.postMessage({ type: 'workshop-state', state } satisfies DualScreenMessage)
|
||||||
|
channel?.close()
|
||||||
|
}, [enabled, state])
|
||||||
|
}
|
||||||
|
|
||||||
export function DualScreenBottomDisplay() {
|
export function DualScreenBottomDisplay() {
|
||||||
const [state, setState] = useState<DualScreenCombatState | null>(loadRecentSnapshot)
|
const [state, setState] = useState<DualScreenCombatState | null>(loadRecentSnapshot)
|
||||||
|
const [workshopState, setWorkshopState] = useState<DualScreenWorkshopState | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const channel = createChannel()
|
const channel = createChannel()
|
||||||
if (!channel) return
|
if (!channel) return
|
||||||
const announce = () => channel.postMessage({ type: 'companion-ready' } satisfies DualScreenMessage)
|
const announce = () => channel.postMessage({ type: 'companion-ready' } satisfies DualScreenMessage)
|
||||||
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
|
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
|
||||||
if (event.data.type === 'combat-state') setState(event.data.state)
|
if (event.data.type === 'combat-state') {
|
||||||
|
setState(event.data.state)
|
||||||
|
setWorkshopState(null)
|
||||||
|
}
|
||||||
|
if (event.data.type === 'workshop-state') {
|
||||||
|
setWorkshopState(event.data.state)
|
||||||
|
setState(null)
|
||||||
|
}
|
||||||
if (event.data.type === 'combat-ended') setState(null)
|
if (event.data.type === 'combat-ended') setState(null)
|
||||||
|
if (event.data.type === 'workshop-ended') setWorkshopState(null)
|
||||||
}
|
}
|
||||||
announce()
|
announce()
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
@@ -307,6 +371,40 @@ export function DualScreenBottomDisplay() {
|
|||||||
channel?.close()
|
channel?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state && workshopState) {
|
||||||
|
return (
|
||||||
|
<main className="dual-bottom-display workshop-bottom-display">
|
||||||
|
<header className="dual-controls-header">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">{workshopState.mode}</p>
|
||||||
|
<h1>{workshopState.title}</h1>
|
||||||
|
</div>
|
||||||
|
<div className="dual-controls-progress">
|
||||||
|
<span>{workshopState.subtitle}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{workshopState.summary && (
|
||||||
|
<section className="workshop-bottom-summary">
|
||||||
|
{workshopState.summary}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
<section className="workshop-bottom-grid">
|
||||||
|
{workshopState.items.map((item, index) => (
|
||||||
|
<article key={`${item.title}-${index}`}>
|
||||||
|
{item.glyph && <span>{item.glyph}</span>}
|
||||||
|
<div>
|
||||||
|
<strong>{item.title}</strong>
|
||||||
|
{item.meta && <small>{item.meta}</small>}
|
||||||
|
{item.detail && <p>{item.detail}</p>}
|
||||||
|
</div>
|
||||||
|
{item.status && <i>{item.status}</i>}
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return (
|
return (
|
||||||
<main className="dual-bottom-display dual-bottom-waiting">
|
<main className="dual-bottom-display dual-bottom-waiting">
|
||||||
@@ -475,6 +573,7 @@ export function DualScreenTopCombat({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`dual-top-member ${state.selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
|
className={`dual-top-member ${state.selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
|
||||||
|
data-party-member-id={member.id}
|
||||||
key={member.id}
|
key={member.id}
|
||||||
onClick={() => onSelectTarget(member.id)}
|
onClick={() => onSelectTarget(member.id)}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -385,9 +385,7 @@ function scaledPvpBossExperience(
|
|||||||
type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string }
|
type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string }
|
||||||
const COMPONENT_ITEMS: Record<number, ComponentTemplate> = {
|
const COMPONENT_ITEMS: Record<number, ComponentTemplate> = {
|
||||||
1: { id: 600, slug: 'minor-component', name: 'Minor Component', itemLevel: 1, glyph: '◆', description: 'A basic crafting component.' },
|
1: { id: 600, slug: 'minor-component', name: 'Minor Component', itemLevel: 1, glyph: '◆', description: 'A basic crafting component.' },
|
||||||
5: { id: 601, slug: 'basic-component', name: 'Basic Component', itemLevel: 5, glyph: '◇', description: 'A standard crafting component.' },
|
|
||||||
10: { id: 602, slug: 'refined-component', name: 'Refined Component', itemLevel: 10, glyph: '◈', description: 'A refined crafting component.' },
|
10: { id: 602, slug: 'refined-component', name: 'Refined Component', itemLevel: 10, glyph: '◈', description: 'A refined crafting component.' },
|
||||||
15: { id: 603, slug: 'advanced-component', name: 'Advanced Component', itemLevel: 15, glyph: '◉', description: 'An advanced crafting component.' },
|
|
||||||
20: { id: 604, slug: 'superior-component', name: 'Superior Component', itemLevel: 20, glyph: '◎', description: 'A superior crafting component.' },
|
20: { id: 604, slug: 'superior-component', name: 'Superior Component', itemLevel: 20, glyph: '◎', description: 'A superior crafting component.' },
|
||||||
25: { id: 605, slug: 'primal-component', name: 'Primal Component', itemLevel: 25, glyph: '✦', description: 'A primal crafting component.' },
|
25: { id: 605, slug: 'primal-component', name: 'Primal Component', itemLevel: 25, glyph: '✦', description: 'A primal crafting component.' },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1211,366 +1211,6 @@
|
|||||||
],
|
],
|
||||||
"canCraft": false
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": 1004,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 12,
|
|
||||||
"item": {
|
|
||||||
"id": 4,
|
|
||||||
"slug": "cinderstep-boots",
|
|
||||||
"name": "Honed Yian Kut-Ku Boots",
|
|
||||||
"slot": "boots",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 3,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "b",
|
|
||||||
"description": "Crafted with Yian Kut-Ku coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 281201,
|
|
||||||
"slug": "yian-kut-ku-coin-ilvl-1",
|
|
||||||
"name": "Raw Yian Kut-Ku Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Yian Kut-Ku used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1002,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 3,
|
|
||||||
"item": {
|
|
||||||
"id": 2,
|
|
||||||
"slug": "wardens-cinderwrap",
|
|
||||||
"name": "Honed Bulldrome Chest",
|
|
||||||
"slot": "chest",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 3,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "C",
|
|
||||||
"description": "Crafted with Bulldrome coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 280301,
|
|
||||||
"slug": "bulldrome-coin-ilvl-1",
|
|
||||||
"name": "Raw Bulldrome Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Bulldrome used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1003,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 3,
|
|
||||||
"item": {
|
|
||||||
"id": 6,
|
|
||||||
"slug": "furnace-tenders-wraps",
|
|
||||||
"name": "Honed Bulldrome Gloves",
|
|
||||||
"slot": "gloves",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 3,
|
|
||||||
"maxResourceBonus": 2,
|
|
||||||
"glyph": "g",
|
|
||||||
"description": "Crafted with Bulldrome coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 280301,
|
|
||||||
"slug": "bulldrome-coin-ilvl-1",
|
|
||||||
"name": "Raw Bulldrome Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Bulldrome used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1001,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 3,
|
|
||||||
"item": {
|
|
||||||
"id": 5,
|
|
||||||
"slug": "adepts-hood",
|
|
||||||
"name": "Honed Bulldrome Helmet",
|
|
||||||
"slot": "helmet",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 3,
|
|
||||||
"maxResourceBonus": 4,
|
|
||||||
"glyph": "^",
|
|
||||||
"description": "Crafted with Bulldrome coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 280301,
|
|
||||||
"slug": "bulldrome-coin-ilvl-1",
|
|
||||||
"name": "Raw Bulldrome Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Bulldrome used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1009,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 22,
|
|
||||||
"item": {
|
|
||||||
"id": 9,
|
|
||||||
"slug": "sootglass-pendant",
|
|
||||||
"name": "Honed Rathian Necklace",
|
|
||||||
"slot": "necklace",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 4,
|
|
||||||
"maxResourceBonus": 4,
|
|
||||||
"glyph": "n",
|
|
||||||
"description": "Crafted with Rathian coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 282201,
|
|
||||||
"slug": "rathian-coin-ilvl-1",
|
|
||||||
"name": "Raw Rathian Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Rathian used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1008,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 22,
|
|
||||||
"item": {
|
|
||||||
"id": 8,
|
|
||||||
"slug": "ashwalker-legwraps",
|
|
||||||
"name": "Honed Rathian Pants",
|
|
||||||
"slot": "pants",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 3,
|
|
||||||
"maxResourceBonus": 3,
|
|
||||||
"glyph": "P",
|
|
||||||
"description": "Crafted with Rathian coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 282201,
|
|
||||||
"slug": "rathian-coin-ilvl-1",
|
|
||||||
"name": "Raw Rathian Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Rathian used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1005,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 12,
|
|
||||||
"item": {
|
|
||||||
"id": 1,
|
|
||||||
"slug": "emberglass-sigil",
|
|
||||||
"name": "Honed Yian Kut-Ku Ring",
|
|
||||||
"slot": "ring",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 4,
|
|
||||||
"maxResourceBonus": 5,
|
|
||||||
"glyph": "o",
|
|
||||||
"description": "Crafted with Yian Kut-Ku coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 281201,
|
|
||||||
"slug": "yian-kut-ku-coin-ilvl-1",
|
|
||||||
"name": "Raw Yian Kut-Ku Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Yian Kut-Ku used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1006,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 12,
|
|
||||||
"item": {
|
|
||||||
"id": 7,
|
|
||||||
"slug": "warden-ember",
|
|
||||||
"name": "Honed Yian Kut-Ku Trinket",
|
|
||||||
"slot": "trinket",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 4,
|
|
||||||
"maxResourceBonus": 4,
|
|
||||||
"glyph": "*",
|
|
||||||
"description": "Crafted with Yian Kut-Ku coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 281201,
|
|
||||||
"slug": "yian-kut-ku-coin-ilvl-1",
|
|
||||||
"name": "Raw Yian Kut-Ku Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Yian Kut-Ku used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1007,
|
|
||||||
"difficultyId": 1,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 22,
|
|
||||||
"item": {
|
|
||||||
"id": 3,
|
|
||||||
"slug": "ashwood-crook",
|
|
||||||
"name": "Honed Rathian Weapon",
|
|
||||||
"slot": "weapon",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 5,
|
|
||||||
"healingPower": 5,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "/",
|
|
||||||
"description": "Crafted with Rathian coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 282201,
|
|
||||||
"slug": "rathian-coin-ilvl-1",
|
|
||||||
"name": "Raw Rathian Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "common",
|
|
||||||
"itemLevel": 1,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Rathian used for item level 1 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 5,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 1104,
|
"id": 1104,
|
||||||
"difficultyId": 2,
|
"difficultyId": 2,
|
||||||
@@ -1931,366 +1571,6 @@
|
|||||||
],
|
],
|
||||||
"canCraft": false
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": 1204,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 12,
|
|
||||||
"item": {
|
|
||||||
"id": 304,
|
|
||||||
"slug": "runed-cinderstep-boots",
|
|
||||||
"name": "Blue Yian Kut-Ku Boots",
|
|
||||||
"slot": "boots",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 9,
|
|
||||||
"maxResourceBonus": 8,
|
|
||||||
"glyph": "b",
|
|
||||||
"description": "Crafted with Yian Kut-Ku coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 281210,
|
|
||||||
"slug": "yian-kut-ku-coin-ilvl-10",
|
|
||||||
"name": "Green Yian Kut-Ku Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Yian Kut-Ku used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1202,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 3,
|
|
||||||
"item": {
|
|
||||||
"id": 302,
|
|
||||||
"slug": "runed-cinderwrap",
|
|
||||||
"name": "Blue Bulldrome Chest",
|
|
||||||
"slot": "chest",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 11,
|
|
||||||
"maxResourceBonus": 3,
|
|
||||||
"glyph": "C",
|
|
||||||
"description": "Crafted with Bulldrome coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 280310,
|
|
||||||
"slug": "bulldrome-coin-ilvl-10",
|
|
||||||
"name": "Green Bulldrome Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Bulldrome used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1203,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 3,
|
|
||||||
"item": {
|
|
||||||
"id": 306,
|
|
||||||
"slug": "runed-furnace-wraps",
|
|
||||||
"name": "Blue Bulldrome Gloves",
|
|
||||||
"slot": "gloves",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 11,
|
|
||||||
"maxResourceBonus": 6,
|
|
||||||
"glyph": "g",
|
|
||||||
"description": "Crafted with Bulldrome coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 280310,
|
|
||||||
"slug": "bulldrome-coin-ilvl-10",
|
|
||||||
"name": "Green Bulldrome Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Bulldrome used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1201,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 3,
|
|
||||||
"item": {
|
|
||||||
"id": 305,
|
|
||||||
"slug": "runed-adepts-hood",
|
|
||||||
"name": "Blue Bulldrome Helmet",
|
|
||||||
"slot": "helmet",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 9,
|
|
||||||
"maxResourceBonus": 9,
|
|
||||||
"glyph": "^",
|
|
||||||
"description": "Crafted with Bulldrome coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 280310,
|
|
||||||
"slug": "bulldrome-coin-ilvl-10",
|
|
||||||
"name": "Green Bulldrome Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Bulldrome used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1209,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 22,
|
|
||||||
"item": {
|
|
||||||
"id": 309,
|
|
||||||
"slug": "runed-sootglass-pendant",
|
|
||||||
"name": "Blue Rathian Necklace",
|
|
||||||
"slot": "necklace",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 12,
|
|
||||||
"maxResourceBonus": 10,
|
|
||||||
"glyph": "n",
|
|
||||||
"description": "Crafted with Rathian coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 282210,
|
|
||||||
"slug": "rathian-coin-ilvl-10",
|
|
||||||
"name": "Green Rathian Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Rathian used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1208,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 22,
|
|
||||||
"item": {
|
|
||||||
"id": 308,
|
|
||||||
"slug": "runed-ashwalker-legwraps",
|
|
||||||
"name": "Blue Rathian Pants",
|
|
||||||
"slot": "pants",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 9,
|
|
||||||
"maxResourceBonus": 9,
|
|
||||||
"glyph": "P",
|
|
||||||
"description": "Crafted with Rathian coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 282210,
|
|
||||||
"slug": "rathian-coin-ilvl-10",
|
|
||||||
"name": "Green Rathian Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Rathian used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1205,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 12,
|
|
||||||
"item": {
|
|
||||||
"id": 301,
|
|
||||||
"slug": "runed-emberglass-sigil",
|
|
||||||
"name": "Blue Yian Kut-Ku Ring",
|
|
||||||
"slot": "ring",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 10,
|
|
||||||
"maxResourceBonus": 13,
|
|
||||||
"glyph": "o",
|
|
||||||
"description": "Crafted with Yian Kut-Ku coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 281210,
|
|
||||||
"slug": "yian-kut-ku-coin-ilvl-10",
|
|
||||||
"name": "Green Yian Kut-Ku Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Yian Kut-Ku used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1206,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 12,
|
|
||||||
"item": {
|
|
||||||
"id": 307,
|
|
||||||
"slug": "runed-warden-ember",
|
|
||||||
"name": "Blue Yian Kut-Ku Trinket",
|
|
||||||
"slot": "trinket",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 12,
|
|
||||||
"maxResourceBonus": 10,
|
|
||||||
"glyph": "*",
|
|
||||||
"description": "Crafted with Yian Kut-Ku coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 281210,
|
|
||||||
"slug": "yian-kut-ku-coin-ilvl-10",
|
|
||||||
"name": "Green Yian Kut-Ku Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Yian Kut-Ku used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1207,
|
|
||||||
"difficultyId": 2,
|
|
||||||
"sourceDungeonId": 1,
|
|
||||||
"sourceEncounterId": 22,
|
|
||||||
"item": {
|
|
||||||
"id": 303,
|
|
||||||
"slug": "runed-ashwood-crook",
|
|
||||||
"name": "Blue Rathian Weapon",
|
|
||||||
"slot": "weapon",
|
|
||||||
"rarity": "rare",
|
|
||||||
"itemLevel": 15,
|
|
||||||
"healingPower": 15,
|
|
||||||
"maxResourceBonus": 3,
|
|
||||||
"glyph": "/",
|
|
||||||
"description": "Crafted with Rathian coins.",
|
|
||||||
"setId": null,
|
|
||||||
"setSlug": null,
|
|
||||||
"setName": null
|
|
||||||
},
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"item": {
|
|
||||||
"id": 282210,
|
|
||||||
"slug": "rathian-coin-ilvl-10",
|
|
||||||
"name": "Green Rathian Coin",
|
|
||||||
"slot": "component",
|
|
||||||
"rarity": "uncommon",
|
|
||||||
"itemLevel": 10,
|
|
||||||
"healingPower": 0,
|
|
||||||
"maxResourceBonus": 0,
|
|
||||||
"glyph": "$",
|
|
||||||
"description": "A boss coin from Rathian used for item level 10 crafting."
|
|
||||||
},
|
|
||||||
"quantity": 15,
|
|
||||||
"owned": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"canCraft": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 1304,
|
"id": 1304,
|
||||||
"difficultyId": 4,
|
"difficultyId": 4,
|
||||||
|
|||||||
Reference in New Issue
Block a user