Update game 1.0.27
This commit is contained in:
+188
-109
@@ -35,6 +35,7 @@ import {
|
||||
} from '../dualScreen'
|
||||
|
||||
const TICK_MS = 700
|
||||
const TARGET_RENDER_THROTTLE_MS = 180
|
||||
|
||||
type RoguelikeMode = 'dungeon' | 'raid'
|
||||
type RoguelikeUpgradeTiming = 'boss' | 'encounter'
|
||||
@@ -73,6 +74,16 @@ type FloatingCombatText = {
|
||||
value: number
|
||||
}
|
||||
|
||||
type SinglePlayerCombatState = {
|
||||
party: PartyMember[]
|
||||
resource: number
|
||||
enemyHealth: number
|
||||
cooldowns: Record<string, number>
|
||||
elapsedTicks: number
|
||||
castsTowardFree: number
|
||||
freeCastReady: boolean
|
||||
}
|
||||
|
||||
const ROGUELIKE_MECHANICS: RoguelikeMechanic[] = [
|
||||
'party-pulse',
|
||||
'searing-mark',
|
||||
@@ -340,13 +351,18 @@ export function CombatScreen({
|
||||
const sectionName = isRoguelike ? 'Stage' : dungeon.contentType === 'raid' ? 'Phase' : 'Part'
|
||||
const contentName = isRoguelike ? 'Roguelike' : dungeon.contentType === 'raid' ? 'Raid' : 'Dungeon'
|
||||
const initialEncounterIndex = (startPart - 1) * 3
|
||||
const [party, setParty] = useState<PartyMember[]>(partyTemplate)
|
||||
const initialCombatState = useMemo<SinglePlayerCombatState>(() => ({
|
||||
party: partyTemplate,
|
||||
resource: maxResource,
|
||||
enemyHealth: encounters[initialEncounterIndex].maxHealth,
|
||||
cooldowns: {},
|
||||
elapsedTicks: 0,
|
||||
castsTowardFree: 0,
|
||||
freeCastReady: false,
|
||||
}), [encounters, initialEncounterIndex, maxResource, partyTemplate])
|
||||
const [combatState, setCombatState] = useState<SinglePlayerCombatState>(() => initialCombatState)
|
||||
const [selectedId, setSelectedId] = useState(partyTemplate[0].id)
|
||||
const [resource, setResource] = useState(maxResource)
|
||||
const [encounterIndex, setEncounterIndex] = useState(initialEncounterIndex)
|
||||
const [enemyHealth, setEnemyHealth] = useState(encounters[initialEncounterIndex].maxHealth)
|
||||
const [cooldowns, setCooldowns] = useState<Record<string, number>>({})
|
||||
const [, setElapsedTicks] = useState(0)
|
||||
const [status, setStatus] = useState<'playing' | 'won' | 'lost' | 'part-complete' | 'upgrade-choice'>('playing')
|
||||
const [paused, setPaused] = useState(false)
|
||||
const [targetGroup, setTargetGroup] = useState<0 | 1 | 2>(0)
|
||||
@@ -360,8 +376,6 @@ export function CombatScreen({
|
||||
const [floatingTexts, setFloatingTexts] = useState<FloatingCombatText[]>([])
|
||||
const [roguelikeUpgrades, setRoguelikeUpgrades] = useState<RoguelikeUpgrade[]>([])
|
||||
const [upgradeChoices, setUpgradeChoices] = useState<RoguelikeUpgrade[]>([])
|
||||
const [, setCastsTowardFree] = useState(0)
|
||||
const [freeCastReady, setFreeCastReady] = useState(false)
|
||||
const rewardClaimedRef = useRef(false)
|
||||
const profileRefreshedRef = useRef(false)
|
||||
const rolledEncounterIdsRef = useRef(new Set<number>())
|
||||
@@ -371,10 +385,11 @@ export function CombatScreen({
|
||||
const partStartTimesRef = useRef<Record<number, number>>({})
|
||||
const nextLogId = useRef(2)
|
||||
const nextFloatingTextId = useRef(1)
|
||||
const partyRef = useRef(partyTemplate)
|
||||
const enemyHealthRef = useRef(encounters[initialEncounterIndex].maxHealth)
|
||||
const elapsedTicksRef = useRef(0)
|
||||
const combatRef = useRef(initialCombatState)
|
||||
const selectedIdRef = useRef(partyTemplate[0].id)
|
||||
const selectedRenderTimeoutRef = useRef<number | null>(null)
|
||||
const lastSelectedRenderAtRef = useRef(0)
|
||||
const { party, resource, enemyHealth, cooldowns, freeCastReady } = combatState
|
||||
const encounter = encounters[encounterIndex]
|
||||
const currentPart = getCurrentPart(encounterIndex)
|
||||
const firstEncounterIndex = (startPart - 1) * 3
|
||||
@@ -416,9 +431,38 @@ export function CombatScreen({
|
||||
})
|
||||
}, [paused])
|
||||
|
||||
const setCombat = useCallback((
|
||||
nextState: SinglePlayerCombatState | ((current: SinglePlayerCombatState) => SinglePlayerCombatState),
|
||||
) => {
|
||||
const next = typeof nextState === 'function'
|
||||
? nextState(combatRef.current)
|
||||
: nextState
|
||||
combatRef.current = next
|
||||
setCombatState(next)
|
||||
}, [])
|
||||
|
||||
const setSelectedTargetId = useCallback((id: string) => {
|
||||
if (selectedIdRef.current === id) return
|
||||
selectedIdRef.current = id
|
||||
setSelectedId(id)
|
||||
const now = performance.now()
|
||||
const elapsed = now - lastSelectedRenderAtRef.current
|
||||
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(() => () => {
|
||||
if (selectedRenderTimeoutRef.current !== null) {
|
||||
window.clearTimeout(selectedRenderTimeoutRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => {
|
||||
@@ -468,18 +512,19 @@ export function CombatScreen({
|
||||
: []
|
||||
const nextEncounters = roguelikeMode ? nextRoguelikeEncounters : staticEncounters
|
||||
const freshParty = partyTemplate.map((member) => ({ ...member }))
|
||||
partyRef.current = freshParty
|
||||
enemyHealthRef.current = nextEncounters[initialEncounterIndex].maxHealth
|
||||
setCombat({
|
||||
party: freshParty,
|
||||
resource: maxResource,
|
||||
enemyHealth: nextEncounters[initialEncounterIndex].maxHealth,
|
||||
cooldowns: {},
|
||||
elapsedTicks: 0,
|
||||
castsTowardFree: 0,
|
||||
freeCastReady: false,
|
||||
})
|
||||
if (roguelikeMode) setRoguelikeEncounters(nextRoguelikeEncounters)
|
||||
setRoguelikeStage(1)
|
||||
setParty(freshParty)
|
||||
setSelectedTargetId(partyTemplate[0].id)
|
||||
setResource(maxResource)
|
||||
setEncounterIndex(initialEncounterIndex)
|
||||
setEnemyHealth(nextEncounters[initialEncounterIndex].maxHealth)
|
||||
setCooldowns({})
|
||||
elapsedTicksRef.current = 0
|
||||
setElapsedTicks(0)
|
||||
setStatus('playing')
|
||||
setPaused(false)
|
||||
setTargetGroup(0)
|
||||
@@ -490,8 +535,6 @@ export function CombatScreen({
|
||||
setFloatingTexts([])
|
||||
setRoguelikeUpgrades([])
|
||||
setUpgradeChoices([])
|
||||
setCastsTowardFree(0)
|
||||
setFreeCastReady(false)
|
||||
rewardClaimedRef.current = false
|
||||
profileRefreshedRef.current = false
|
||||
rolledEncounterIdsRef.current = new Set()
|
||||
@@ -500,18 +543,19 @@ export function CombatScreen({
|
||||
runStartedAtRef.current = Date.now()
|
||||
partStartTimesRef.current = { [startPart]: runStartedAtRef.current }
|
||||
setLog([{ id: nextLogId.current++, text: 'A new run begins.', tone: 'system' }])
|
||||
}, [difficulty, initialEncounterIndex, maxResource, partyTemplate, roguelikeMode, roguelikePool, setSelectedTargetId, startPart, staticEncounters])
|
||||
}, [difficulty, initialEncounterIndex, maxResource, partyTemplate, roguelikeMode, roguelikePool, setCombat, setSelectedTargetId, startPart, staticEncounters])
|
||||
|
||||
const castSpell = useCallback(
|
||||
(spell: Spell) => {
|
||||
const effectiveCost = spellResourceCost(spell, roguelikeUpgrades, freeCastReady)
|
||||
if (status !== 'playing' || cooldowns[spell.id] > 0 || resource < effectiveCost) return
|
||||
const healer = partyRef.current.find((member) => member.id === 'mira')
|
||||
const current = combatRef.current
|
||||
const effectiveCost = spellResourceCost(spell, roguelikeUpgrades, current.freeCastReady)
|
||||
if (status !== 'playing' || current.cooldowns[spell.id] > 0 || current.resource < effectiveCost) return
|
||||
const healer = current.party.find((member) => member.id === 'mira')
|
||||
if (!healer || healer.health <= 0) return
|
||||
const targetId = selectedIdRef.current
|
||||
const selected = partyRef.current.find((member) => member.id === targetId)
|
||||
const selected = current.party.find((member) => member.id === targetId)
|
||||
if (!selected || selected.health <= 0) return
|
||||
const extraTarget = (blockedIds: string[]) => partyRef.current
|
||||
const extraTarget = (blockedIds: string[]) => current.party
|
||||
.filter((member) => member.health > 0 && !blockedIds.includes(member.id))
|
||||
.sort((left, right) => (left.health / left.maxHealth) - (right.health / right.maxHealth))[0]
|
||||
const directTargets = new Set([targetId])
|
||||
@@ -547,28 +591,7 @@ export function CombatScreen({
|
||||
if (extra) directTargets.add(extra.id)
|
||||
}
|
||||
|
||||
setResource((value) => value - effectiveCost)
|
||||
resourceSpentRef.current += effectiveCost
|
||||
setCooldowns((current) => ({
|
||||
...current,
|
||||
[spell.id]: spell.cooldown * cooldownMultiplier(spell, roguelikeUpgrades),
|
||||
}))
|
||||
if (upgradeStackCount(roguelikeUpgrades, 'fifth-cast-free') > 0) {
|
||||
if (freeCastReady) {
|
||||
setFreeCastReady(false)
|
||||
setCastsTowardFree(0)
|
||||
} else {
|
||||
setCastsTowardFree((current) => {
|
||||
const next = current + 1
|
||||
if (next >= 5) {
|
||||
setFreeCastReady(true)
|
||||
return 0
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
const nextParty = partyRef.current.map((member) => {
|
||||
const nextParty = current.party.map((member) => {
|
||||
if (member.health <= 0) return member
|
||||
if (spell.kind === 'group') {
|
||||
const power = Math.round(spell.power * (1.25 ** upgradeStackCount(roguelikeUpgrades, 'group-heal-boost')))
|
||||
@@ -602,11 +625,35 @@ export function CombatScreen({
|
||||
hotTicks: hotTargets.has(member.id) ? 5 : member.hotTicks,
|
||||
}
|
||||
})
|
||||
partyRef.current = nextParty
|
||||
setParty(nextParty)
|
||||
const freeCastStacks = upgradeStackCount(roguelikeUpgrades, 'fifth-cast-free')
|
||||
const nextFreeCastReady = freeCastStacks > 0 && current.freeCastReady
|
||||
? false
|
||||
: current.freeCastReady
|
||||
const nextCastsTowardFree = freeCastStacks > 0
|
||||
? current.freeCastReady
|
||||
? 0
|
||||
: current.castsTowardFree + 1 >= 5
|
||||
? 0
|
||||
: current.castsTowardFree + 1
|
||||
: current.castsTowardFree
|
||||
const gainedFreeCast = freeCastStacks > 0
|
||||
&& !current.freeCastReady
|
||||
&& current.castsTowardFree + 1 >= 5
|
||||
resourceSpentRef.current += effectiveCost
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: current.resource - effectiveCost,
|
||||
cooldowns: {
|
||||
...current.cooldowns,
|
||||
[spell.id]: spell.cooldown * cooldownMultiplier(spell, roguelikeUpgrades),
|
||||
},
|
||||
castsTowardFree: nextCastsTowardFree,
|
||||
freeCastReady: gainedFreeCast || nextFreeCastReady,
|
||||
})
|
||||
addLog(`${spell.name} cast on ${spell.kind === 'group' ? 'the party' : selected.name}${effectiveCost === 0 ? ' for free' : ''}.`, 'heal')
|
||||
},
|
||||
[activeSetEffects, addFloatingHeal, addLog, cooldowns, freeCastReady, resource, roguelikeUpgrades, status],
|
||||
[activeSetEffects, addFloatingHeal, addLog, roguelikeUpgrades, setCombat, status],
|
||||
)
|
||||
|
||||
const finishRun = useCallback(
|
||||
@@ -669,7 +716,7 @@ export function CombatScreen({
|
||||
)
|
||||
|
||||
const selectRelativeTarget = useCallback((direction: -1 | 1) => {
|
||||
const living = partyRef.current.filter((member) => member.health > 0)
|
||||
const living = combatRef.current.party.filter((member) => member.health > 0)
|
||||
if (living.length === 0) return
|
||||
const currentIndex = living.findIndex((member) => member.id === selectedIdRef.current)
|
||||
const nextIndex = currentIndex < 0
|
||||
@@ -680,14 +727,14 @@ export function CombatScreen({
|
||||
|
||||
const selectDirectionalTarget = useCallback((action: InputAction) => {
|
||||
const columns = dungeon.partySize >= 10 ? 6 : 3
|
||||
const currentIndex = partyRef.current.findIndex((member) => member.id === selectedIdRef.current)
|
||||
const currentIndex = combatRef.current.party.findIndex((member) => member.id === selectedIdRef.current)
|
||||
if (currentIndex < 0) {
|
||||
setSelectedTargetId(partyRef.current[0].id)
|
||||
setSelectedTargetId(combatRef.current.party[0].id)
|
||||
return
|
||||
}
|
||||
const currentRow = Math.floor(currentIndex / columns)
|
||||
const currentColumn = currentIndex % columns
|
||||
const candidates = partyRef.current
|
||||
const candidates = combatRef.current.party
|
||||
.map((member, index) => ({
|
||||
member,
|
||||
index,
|
||||
@@ -721,14 +768,15 @@ export function CombatScreen({
|
||||
|
||||
const selectDirectTarget = useCallback((slot: number) => {
|
||||
const index = slot + (dungeon.partySize > 6 ? targetGroup * 6 : 0)
|
||||
const member = partyRef.current[index]
|
||||
const member = combatRef.current.party[index]
|
||||
if (member) setSelectedTargetId(member.id)
|
||||
}, [dungeon.partySize, setSelectedTargetId, targetGroup])
|
||||
|
||||
const chooseRoguelikeUpgrade = useCallback((upgrade: RoguelikeUpgrade) => {
|
||||
if (!roguelikeMode) return
|
||||
const current = combatRef.current
|
||||
const clearedBoss = encounters[encounterIndex]?.isBoss ?? false
|
||||
const recoveredParty = partyRef.current.map((member) => ({
|
||||
const recoveredParty = current.party.map((member) => ({
|
||||
...member,
|
||||
health: member.health <= 0
|
||||
? 0
|
||||
@@ -747,24 +795,24 @@ export function CombatScreen({
|
||||
? nextSegment[0]
|
||||
: encounters[encounterIndex + 1]
|
||||
if (!nextEncounter) return
|
||||
partyRef.current = recoveredParty
|
||||
enemyHealthRef.current = nextEncounter.maxHealth
|
||||
setRoguelikeUpgrades((current) => [...current, upgrade])
|
||||
if (clearedBoss) {
|
||||
setRoguelikeStage(nextStage)
|
||||
setRoguelikeEncounters((current) => [...current, ...nextSegment])
|
||||
}
|
||||
setParty(recoveredParty)
|
||||
setEncounterIndex((current) => current + 1)
|
||||
setEnemyHealth(nextEncounter.maxHealth)
|
||||
elapsedTicksRef.current = 0
|
||||
setElapsedTicks(0)
|
||||
setCooldowns({})
|
||||
setResource((value) => clamp(value + Math.round(maxResource * 0.25), 0, maxResource))
|
||||
setCombat({
|
||||
...current,
|
||||
party: recoveredParty,
|
||||
enemyHealth: nextEncounter.maxHealth,
|
||||
elapsedTicks: 0,
|
||||
cooldowns: {},
|
||||
resource: clamp(current.resource + Math.round(maxResource * 0.25), 0, maxResource),
|
||||
})
|
||||
setUpgradeChoices([])
|
||||
setStatus('playing')
|
||||
addLog(`${upgrade.name} gained. ${nextEncounter.enemyName} approaches.`, 'system')
|
||||
}, [addLog, difficulty, encounterIndex, encounters, maxResource, roguelikeMode, roguelikePool, roguelikeStage])
|
||||
}, [addLog, difficulty, encounterIndex, encounters, maxResource, roguelikeMode, roguelikePool, roguelikeStage, setCombat])
|
||||
|
||||
useGameAction((action, device) => {
|
||||
if (action === 'pause' || (action === 'back' && device === 'pc')) {
|
||||
@@ -783,10 +831,10 @@ export function CombatScreen({
|
||||
if (action === 'toggleTargetGroup') {
|
||||
if (dungeon.partySize <= 6) return
|
||||
setTargetGroup((current) => {
|
||||
const groupCount = Math.max(1, Math.ceil(partyRef.current.length / 6))
|
||||
const groupCount = Math.max(1, Math.ceil(combatRef.current.party.length / 6))
|
||||
const next = ((current + 1) % groupCount) as 0 | 1 | 2
|
||||
const selectedIndex = partyRef.current.findIndex((member) => member.id === selectedIdRef.current)
|
||||
const nextMember = partyRef.current[(selectedIndex < 0 ? 0 : selectedIndex % 6) + next * 6]
|
||||
const selectedIndex = combatRef.current.party.findIndex((member) => member.id === selectedIdRef.current)
|
||||
const nextMember = combatRef.current.party[(selectedIndex < 0 ? 0 : selectedIndex % 6) + next * 6]
|
||||
if (nextMember) setSelectedTargetId(nextMember.id)
|
||||
return next
|
||||
})
|
||||
@@ -809,20 +857,17 @@ export function CombatScreen({
|
||||
useEffect(() => {
|
||||
if (status !== 'playing' || paused) return
|
||||
const timer = window.setInterval(() => {
|
||||
const nextElapsedTicks = elapsedTicksRef.current + 1
|
||||
elapsedTicksRef.current = nextElapsedTicks
|
||||
setElapsedTicks(nextElapsedTicks)
|
||||
setResource((value) => clamp(value + 2.4, 0, maxResource))
|
||||
setCooldowns((current) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(current).map(([id, seconds]) => [
|
||||
id,
|
||||
Math.max(0, seconds - TICK_MS / 1000),
|
||||
]),
|
||||
),
|
||||
const current = combatRef.current
|
||||
const nextElapsedTicks = current.elapsedTicks + 1
|
||||
const nextCooldowns = Object.fromEntries(
|
||||
Object.entries(current.cooldowns).map(([id, seconds]) => [
|
||||
id,
|
||||
Math.max(0, seconds - TICK_MS / 1000),
|
||||
]),
|
||||
)
|
||||
let nextResource = clamp(current.resource + 2.4, 0, maxResource)
|
||||
|
||||
const living = partyRef.current.filter((member) => member.health > 0)
|
||||
const living = current.party.filter((member) => member.health > 0)
|
||||
if (living.length === 0) {
|
||||
if (isRoguelike) finishRoguelikeRun(encounterIndex)
|
||||
setStatus('lost')
|
||||
@@ -854,12 +899,12 @@ export function CombatScreen({
|
||||
if (appliesHealingReduction) addLog(`${primaryTarget.name} receives reduced healing.`, 'danger')
|
||||
if (tankBuster) addLog(`${encounter.enemyName} crushes the tanks.`, 'danger')
|
||||
if (resourceDrain) {
|
||||
setResource((value) => clamp(value - 8, 0, maxResource))
|
||||
nextResource = clamp(nextResource - 8, 0, maxResource)
|
||||
addLog(`${encounter.enemyName} drains ${gameClass.resourceName}.`, 'danger')
|
||||
}
|
||||
|
||||
const healerBeforeDamage = partyRef.current.find((member) => member.id === 'mira')
|
||||
const nextParty = partyRef.current.map((member) => {
|
||||
const healerBeforeDamage = current.party.find((member) => member.id === 'mira')
|
||||
const nextParty = current.party.map((member) => {
|
||||
if (member.health <= 0) return member
|
||||
let damage = member.id === primaryTarget.id ? encounter.damage : 0
|
||||
if (member.role === 'Tank') damage += encounter.tankDamage
|
||||
@@ -898,8 +943,6 @@ export function CombatScreen({
|
||||
}
|
||||
})
|
||||
const healerAfterDamage = nextParty.find((member) => member.id === 'mira')
|
||||
partyRef.current = nextParty
|
||||
setParty(nextParty)
|
||||
|
||||
if (
|
||||
healerBeforeDamage
|
||||
@@ -911,16 +954,30 @@ export function CombatScreen({
|
||||
}
|
||||
|
||||
if (nextParty.every((member) => member.health <= 0)) {
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: nextResource,
|
||||
cooldowns: nextCooldowns,
|
||||
elapsedTicks: nextElapsedTicks,
|
||||
enemyHealth: current.enemyHealth,
|
||||
})
|
||||
if (isRoguelike) finishRoguelikeRun(encounterIndex)
|
||||
setStatus('lost')
|
||||
addLog('The party has fallen.', 'danger')
|
||||
return
|
||||
}
|
||||
|
||||
const nextEnemyHealth = enemyHealthRef.current - encounter.partyDamage
|
||||
const nextEnemyHealth = current.enemyHealth - encounter.partyDamage
|
||||
if (nextEnemyHealth > 0) {
|
||||
enemyHealthRef.current = nextEnemyHealth
|
||||
setEnemyHealth(nextEnemyHealth)
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: nextResource,
|
||||
cooldowns: nextCooldowns,
|
||||
elapsedTicks: nextElapsedTicks,
|
||||
enemyHealth: nextEnemyHealth,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -929,8 +986,14 @@ export function CombatScreen({
|
||||
}
|
||||
|
||||
if (isRoguelike && (upgradesEveryEncounter || encounter.isBoss)) {
|
||||
enemyHealthRef.current = 0
|
||||
setEnemyHealth(0)
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: nextResource,
|
||||
cooldowns: nextCooldowns,
|
||||
elapsedTicks: nextElapsedTicks,
|
||||
enemyHealth: 0,
|
||||
})
|
||||
setUpgradeChoices(chooseRandom(roguelikeUpgradeCatalog, 3))
|
||||
setStatus('upgrade-choice')
|
||||
addLog(`${encounter.enemyName} defeated. Choose an upgrade.`, 'loot')
|
||||
@@ -938,16 +1001,28 @@ export function CombatScreen({
|
||||
}
|
||||
|
||||
if (isPartBoss && !isFinalBoss) {
|
||||
enemyHealthRef.current = 0
|
||||
setEnemyHealth(0)
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: nextResource,
|
||||
cooldowns: nextCooldowns,
|
||||
elapsedTicks: nextElapsedTicks,
|
||||
enemyHealth: 0,
|
||||
})
|
||||
setStatus('part-complete')
|
||||
addLog(`${encounter.enemyName} is defeated.`, 'loot')
|
||||
return
|
||||
}
|
||||
|
||||
if (encounterIndex === encounters.length - 1) {
|
||||
enemyHealthRef.current = 0
|
||||
setEnemyHealth(0)
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: nextResource,
|
||||
cooldowns: nextCooldowns,
|
||||
elapsedTicks: nextElapsedTicks,
|
||||
enemyHealth: 0,
|
||||
})
|
||||
finishRun(currentPart, startPart)
|
||||
addLog(`${encounter.enemyName} is defeated. Rolling its loot table.`, 'loot')
|
||||
return
|
||||
@@ -965,13 +1040,15 @@ export function CombatScreen({
|
||||
maxHealthPenaltyTicks: undefined,
|
||||
healingReductionTicks: undefined,
|
||||
}))
|
||||
partyRef.current = recoveredParty
|
||||
enemyHealthRef.current = nextEncounter.maxHealth
|
||||
setParty(recoveredParty)
|
||||
setEncounterIndex((value) => value + 1)
|
||||
setEnemyHealth(nextEncounter.maxHealth)
|
||||
elapsedTicksRef.current = 0
|
||||
setElapsedTicks(0)
|
||||
setCombat({
|
||||
...current,
|
||||
party: recoveredParty,
|
||||
resource: nextResource,
|
||||
cooldowns: nextCooldowns,
|
||||
elapsedTicks: 0,
|
||||
enemyHealth: nextEncounter.maxHealth,
|
||||
})
|
||||
addLog(`${encounter.enemyName} defeated. ${nextEncounter.enemyName} approaches.`, 'system')
|
||||
}, TICK_MS)
|
||||
return () => window.clearInterval(timer)
|
||||
@@ -994,6 +1071,7 @@ export function CombatScreen({
|
||||
gameClass.resourceName,
|
||||
requestLootRoll,
|
||||
profile.character.name,
|
||||
setCombat,
|
||||
startPart,
|
||||
status,
|
||||
currentPart,
|
||||
@@ -1398,19 +1476,20 @@ export function CombatScreen({
|
||||
const nextIndex = encounterIndex + 1
|
||||
partStartTimesRef.current[currentPart + 1] = Date.now()
|
||||
const nextEncounter = encounters[nextIndex]
|
||||
const recoveredParty = partyRef.current.map((member) => ({
|
||||
const current = combatRef.current
|
||||
const recoveredParty = current.party.map((member) => ({
|
||||
...member,
|
||||
health: clamp(member.health + 35, 0, member.maxHealth),
|
||||
debuff: undefined,
|
||||
debuffTicks: undefined,
|
||||
}))
|
||||
partyRef.current = recoveredParty
|
||||
enemyHealthRef.current = nextEncounter.maxHealth
|
||||
setParty(recoveredParty)
|
||||
setEncounterIndex(nextIndex)
|
||||
setEnemyHealth(nextEncounter.maxHealth)
|
||||
elapsedTicksRef.current = 0
|
||||
setElapsedTicks(0)
|
||||
setCombat({
|
||||
...current,
|
||||
party: recoveredParty,
|
||||
enemyHealth: nextEncounter.maxHealth,
|
||||
elapsedTicks: 0,
|
||||
})
|
||||
setStatus('playing')
|
||||
addLog(`Proceeding to ${sectionName} ${currentPart + 1}. ${nextEncounter.enemyName} approaches.`, 'system')
|
||||
}}
|
||||
|
||||
@@ -61,15 +61,24 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
||||
firstRecipe?.id ?? null,
|
||||
)
|
||||
const selectedRecipe = profile.craftingRecipes.find((recipe) => recipe.id === selectedRecipeId)
|
||||
const selectedRecipeRequiresUpgrade = selectedRecipe
|
||||
? profile.craftingRecipes.some((recipe) =>
|
||||
recipe.sourceEncounterId === selectedRecipe.sourceEncounterId
|
||||
&& recipe.item.slot === selectedRecipe.item.slot
|
||||
&& recipe.item.itemLevel < selectedRecipe.item.itemLevel,
|
||||
)
|
||||
: false
|
||||
const selectedItemRecipe = selectedItem
|
||||
? profile.craftingRecipes.find((recipe) => recipe.item.id === selectedItem.id)
|
||||
: undefined
|
||||
const upgradeRecipe = selectedItem && selectedItemRecipe
|
||||
? profile.craftingRecipes.find((recipe) =>
|
||||
recipe.sourceEncounterId === selectedItemRecipe.sourceEncounterId
|
||||
&& recipe.item.slot === selectedItem.slot
|
||||
&& recipe.item.itemLevel === selectedItem.itemLevel + 5,
|
||||
)
|
||||
? profile.craftingRecipes
|
||||
.filter((recipe) =>
|
||||
recipe.sourceEncounterId === selectedItemRecipe.sourceEncounterId
|
||||
&& recipe.item.slot === selectedItem.slot
|
||||
&& recipe.item.itemLevel > selectedItem.itemLevel,
|
||||
)
|
||||
.sort((left, right) => left.item.itemLevel - right.item.itemLevel)[0]
|
||||
: undefined
|
||||
const equippedBySlot = useMemo(
|
||||
() => new Map(
|
||||
@@ -503,11 +512,11 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
||||
</div>
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={!selectedRecipe.canCraft || crafting}
|
||||
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe.canCraft || crafting}
|
||||
onClick={craftSelected}
|
||||
type="button"
|
||||
>
|
||||
{crafting ? 'Crafting...' : 'Craft Item'}
|
||||
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user