Android build v1.1.5
This commit is contained in:
@@ -46,7 +46,7 @@ import {
|
||||
spellPowerMultiplier,
|
||||
spellResourceCost as modifiedSpellResourceCost,
|
||||
} from '../combat/spellModifiers'
|
||||
import { advanceCooldowns, regenerateResource, tickSeconds } from '../combat/combatTick'
|
||||
import { regenerateResource } from '../combat/combatTick'
|
||||
import { advanceMemberTick, attachJumpedBounceHeals } from '../combat/combatEngine'
|
||||
import { applyCastStateUpdate } from '../combat/combatStateTransitions'
|
||||
import { buildCombatDualScreenState } from '../combat/dualScreenPayloads'
|
||||
@@ -58,7 +58,11 @@ import {
|
||||
appendCombatLog,
|
||||
type BasicFloatingCombatText,
|
||||
} from '../combat/combatPresentation'
|
||||
import { canCastSpell } from '../combat/spellCasting'
|
||||
import {
|
||||
canCastSpell,
|
||||
pruneExpiredCooldowns,
|
||||
reduceCooldown,
|
||||
} from '../combat/spellCasting'
|
||||
import {
|
||||
applySpellEffectProfile,
|
||||
buildSpellTargetPlan,
|
||||
@@ -69,9 +73,11 @@ import {
|
||||
useInput,
|
||||
} from '../input'
|
||||
import { useFloatingCombatText } from '../hooks/useFloatingCombatText'
|
||||
import { useSpellSlots } from '../hooks/useSpellSlots'
|
||||
import { usePartyTargeting } from '../hooks/usePartyTargeting'
|
||||
import { PartyMemberFrame } from './PartyFrames'
|
||||
import { ResourceBar, SpellBar, type SpellSlot } from './SpellBars'
|
||||
import { ResourceBar, SpellBar } from './SpellBars'
|
||||
import { barFillStyle } from './barStyles'
|
||||
import { BonusItemReward, LootRollList, RewardXpSummary } from './RewardPanels'
|
||||
import { ResultScreen } from './ResultScreen'
|
||||
import {
|
||||
@@ -668,10 +674,10 @@ export function CombatScreen({
|
||||
})
|
||||
floatingHeals.forEach((event) => addFloatingHeal(event.memberId, event.value))
|
||||
resourceSpentRef.current += effectiveCost
|
||||
const nextCooldowns = { ...current.cooldowns }
|
||||
let nextCooldowns = current.cooldowns
|
||||
if (spell.name === 'Mend' && activeEffects.has('mend_reduces_radiance_cooldown')) {
|
||||
const radiance = spellByName.get('Radiance')
|
||||
if (radiance) nextCooldowns[radiance.id] = Math.max(0, (nextCooldowns[radiance.id] ?? 0) - 2)
|
||||
if (radiance) nextCooldowns = reduceCooldown(nextCooldowns, radiance.id, 2)
|
||||
}
|
||||
setCombat(applyCastStateUpdate({
|
||||
current,
|
||||
@@ -855,7 +861,7 @@ export function CombatScreen({
|
||||
const runCombatTick = useCallback(() => {
|
||||
const current = combatRef.current
|
||||
const nextElapsedTicks = current.elapsedTicks + 1
|
||||
const nextCooldowns = advanceCooldowns(current.cooldowns, tickSeconds(TICK_MS))
|
||||
const nextCooldowns = pruneExpiredCooldowns(current.cooldowns)
|
||||
let nextResource = regenerateResource(current.resource, 2.4, maxResource)
|
||||
|
||||
const living = current.party.filter((member) => member.health > 0)
|
||||
@@ -1161,17 +1167,17 @@ export function CombatScreen({
|
||||
}).reverse(),
|
||||
[enemyCount, enemyHealth, encounter.maxHealth],
|
||||
)
|
||||
const spellSlots = useMemo<SpellSlot[]>(() => profile.abilitySlots.map((abilityId, slotIndex) => {
|
||||
const spell = spellByKey.get(String(slotIndex + 1))
|
||||
return abilityId && spell
|
||||
? {
|
||||
...spell,
|
||||
cost: spellResourceCost(spell, roguelikeUpgradeCounts, freeCastReady),
|
||||
slotIndex,
|
||||
remaining: cooldowns[spell.id] ?? 0,
|
||||
}
|
||||
: null
|
||||
}), [cooldowns, freeCastReady, profile.abilitySlots, roguelikeUpgradeCounts, spellByKey])
|
||||
const spellSlotCost = useCallback(
|
||||
(spell: Spell) => spellResourceCost(spell, roguelikeUpgradeCounts, freeCastReady),
|
||||
[freeCastReady, roguelikeUpgradeCounts],
|
||||
)
|
||||
const spellSlots = useSpellSlots({
|
||||
spells,
|
||||
abilitySlots: profile.abilitySlots,
|
||||
cooldowns,
|
||||
active: status === 'playing' && !paused,
|
||||
cost: spellSlotCost,
|
||||
})
|
||||
const dualScreenState = useMemo(() => buildCombatDualScreenState({
|
||||
difficultyName: difficulty.name,
|
||||
dungeonName: dungeon.name,
|
||||
@@ -1230,9 +1236,9 @@ export function CombatScreen({
|
||||
useDualScreenPublisher(dualScreenState, dualScreenEnabled)
|
||||
|
||||
return (
|
||||
<main
|
||||
className={`game-shell ${dualScreenEnabled ? 'dual-top-game-shell' : ''}`}
|
||||
data-combat-active={paused ? 'false' : 'true'}
|
||||
<main
|
||||
className={`game-shell ${dualScreenEnabled ? 'dual-top-game-shell' : ''}`}
|
||||
data-combat-active={status === 'playing' && !paused ? 'true' : 'false'}
|
||||
>
|
||||
{!dualScreenEnabled && <header className="topbar">
|
||||
<div>
|
||||
@@ -1266,13 +1272,13 @@ export function CombatScreen({
|
||||
<div className="hard-enemy-bars">
|
||||
{enemyHealthSegments.map((segment) => (
|
||||
<div className="bar enemy-health" key={segment.index}>
|
||||
<span style={{ width: `${segment.percent}%` }} />
|
||||
<span style={barFillStyle(segment.percent)} />
|
||||
<em>{encounter.enemyName} {segment.index + 1}: {Math.ceil(segment.health)} / {encounter.maxHealth}</em>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bar enemy-health"><span style={{ width: `${enemyPercent}%` }} /></div>
|
||||
<div className="bar enemy-health"><span style={barFillStyle(enemyPercent)} /></div>
|
||||
)}
|
||||
<p>{encounter.description}</p>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
memberHotEffects,
|
||||
} from '../combat/rules'
|
||||
import { ControllerBindingLabel } from './ControllerIcons'
|
||||
import { barFillStyle } from './barStyles'
|
||||
|
||||
export type FloatingCombatText = {
|
||||
id: number
|
||||
@@ -131,8 +132,8 @@ export const PartyMemberFrame = memo(function PartyMemberFrame({
|
||||
{showHeaderHealth && <small>{Math.ceil(member.health)} / {maxHealth}</small>}
|
||||
</div>
|
||||
<div className="bar member-health">
|
||||
<span style={{ width: `${(member.health / maxHealth) * 100}%` }} />
|
||||
{member.shield > 0 && <i style={{ width: `${(member.shield / maxHealth) * 100}%` }} />}
|
||||
<span style={barFillStyle((member.health / maxHealth) * 100)} />
|
||||
{member.shield > 0 && <i style={barFillStyle((member.shield / maxHealth) * 100, 'right')} />}
|
||||
{showHealthText && <em className="health-text">{Math.floor(member.health)} / {maxHealth}</em>}
|
||||
</div>
|
||||
<div className="floating-combat-texts" aria-hidden="true">
|
||||
|
||||
@@ -13,9 +13,11 @@ import { completeRoguelike, type DungeonReward } from '../profile'
|
||||
import type { CharacterProfile, DungeonEncounter } from '../profile'
|
||||
import type { GameMode } from '../gameRepository'
|
||||
import { PartyMemberFrame } from './PartyFrames'
|
||||
import { SpellBar, type SpellSlot } from './SpellBars'
|
||||
import { SpellBar } from './SpellBars'
|
||||
import { barFillStyle } from './barStyles'
|
||||
import { focusFirstControl, useGameAction, useInput } from '../input'
|
||||
import { useDeadlineTimer, useRoundCountdown } from '../hooks/useCountdownTimer'
|
||||
import { useSpellSlots } from '../hooks/useSpellSlots'
|
||||
import { useSidedFloatingCombatText } from '../hooks/useFloatingCombatText'
|
||||
import { usePartyTargeting } from '../hooks/usePartyTargeting'
|
||||
import {
|
||||
@@ -37,10 +39,14 @@ import {
|
||||
spellExtraTargets,
|
||||
spellResourceCost as modifiedSpellResourceCost,
|
||||
} from '../combat/spellModifiers'
|
||||
import { advanceCooldowns, regenerateResource, tickSeconds } from '../combat/combatTick'
|
||||
import { regenerateResource } from '../combat/combatTick'
|
||||
import { advanceMemberTick } from '../combat/combatEngine'
|
||||
import { buildPvpRoguelikeDualScreenState } from '../combat/dualScreenPayloads'
|
||||
import { applyPvpSpellCast } from '../combat/pvpSpellCasting'
|
||||
import {
|
||||
pruneExpiredCooldowns,
|
||||
reduceCooldown,
|
||||
} from '../combat/spellCasting'
|
||||
import {
|
||||
nextPvpRoguelikeStage,
|
||||
resolvePvpRoguelikeCombatOutcome,
|
||||
@@ -398,12 +404,16 @@ export function PvPRoguelikeScreen({
|
||||
const cpuAlive = cpuSide.party.some((member) => member.health > 0)
|
||||
const playerBuffCounts = useMemo(() => createStackCounts(playerSide.buffs), [playerSide.buffs])
|
||||
const playerDebuffCounts = useMemo(() => createStackCounts(playerSide.debuffs), [playerSide.debuffs])
|
||||
const playerSpellSlots = useMemo<SpellSlot[]>(() => starterSpells.map((spell, slotIndex) => ({
|
||||
...spell,
|
||||
cost: spellResourceCost(spell, playerBuffCounts, playerDebuffCounts, playerSide.freeCastReady),
|
||||
slotIndex,
|
||||
remaining: playerSide.cooldowns[spell.id] ?? 0,
|
||||
})), [playerBuffCounts, playerDebuffCounts, playerSide.cooldowns, playerSide.freeCastReady, starterSpells])
|
||||
const playerSpellSlotCost = useCallback(
|
||||
(spell: Spell) => spellResourceCost(spell, playerBuffCounts, playerDebuffCounts, playerSide.freeCastReady),
|
||||
[playerBuffCounts, playerDebuffCounts, playerSide.freeCastReady],
|
||||
)
|
||||
const playerSpellSlots = useSpellSlots({
|
||||
spells: starterSpells,
|
||||
cooldowns: playerSide.cooldowns,
|
||||
active: status === 'playing' && !paused,
|
||||
cost: playerSpellSlotCost,
|
||||
})
|
||||
const opponentBuffSummary = useMemo(
|
||||
() => cpuSide.buffs.length > 0 ? summarizeStacks(cpuSide.buffs, selfBuffChoicesCatalog) : 'none',
|
||||
[cpuSide.buffs, selfBuffChoicesCatalog],
|
||||
@@ -880,9 +890,9 @@ export function PvPRoguelikeScreen({
|
||||
damageReductionTargets: new Set<string>(),
|
||||
groupTargets,
|
||||
}
|
||||
const nextCooldowns = { ...current.cooldowns }
|
||||
let nextCooldowns = current.cooldowns
|
||||
if (spell.kind === 'direct' && hasSpellEffect('mend_reduces_radiance_cooldown') && radianceEffect) {
|
||||
nextCooldowns[radianceEffect.id] = Math.max(0, (nextCooldowns[radianceEffect.id] ?? 0) - 2)
|
||||
nextCooldowns = reduceCooldown(nextCooldowns, radianceEffect.id, 2)
|
||||
}
|
||||
return applyPvpSpellCast({
|
||||
current,
|
||||
@@ -994,7 +1004,7 @@ export function PvPRoguelikeScreen({
|
||||
...side,
|
||||
party: nextParty,
|
||||
resource: regenerateResource(side.resource, 2.4, maxResource),
|
||||
cooldowns: advanceCooldowns(side.cooldowns, tickSeconds(TICK_MS)),
|
||||
cooldowns: pruneExpiredCooldowns(side.cooldowns),
|
||||
enemyHealth: Math.max(0, side.enemyHealth - partyDamageOutput(nextParty, encounterValue.partyDamage)),
|
||||
}
|
||||
}, [activeSpellEffects, addFloatingHeal, maxResource])
|
||||
@@ -1428,13 +1438,13 @@ export function PvPRoguelikeScreen({
|
||||
<div className="pvp-clear-wrap">
|
||||
<span>Your clear {Math.max(0, Math.floor(playerSide.enemyHealth))} / {encounter.maxHealth}</span>
|
||||
<div className="bar enemy-health boss-bar">
|
||||
<span style={{ width: `${(playerSide.enemyHealth / encounter.maxHealth) * 100}%` }} />
|
||||
<span style={barFillStyle((playerSide.enemyHealth / encounter.maxHealth) * 100)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pvp-resource-wrap">
|
||||
<span>{gameClass.resourceName} {Math.floor(playerSide.resource)} / {maxResource}</span>
|
||||
{speedMultiplier === 2 && <strong className="speed-badge">2x speed</strong>}
|
||||
<div className="bar mana-bar"><span style={{ width: `${(playerSide.resource / maxResource) * 100}%` }} /></div>
|
||||
<div className="bar mana-bar"><span style={barFillStyle((playerSide.resource / maxResource) * 100)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1468,12 +1478,12 @@ export function PvPRoguelikeScreen({
|
||||
<div className="pvp-clear-wrap">
|
||||
<span>{liveMatch ? `${liveMatch.opponentName} clear` : 'CPU clear'} {Math.max(0, Math.floor(cpuSide.enemyHealth))} / {encounter.maxHealth}</span>
|
||||
<div className="bar enemy-health boss-bar">
|
||||
<span style={{ width: `${(cpuSide.enemyHealth / encounter.maxHealth) * 100}%` }} />
|
||||
<span style={barFillStyle((cpuSide.enemyHealth / encounter.maxHealth) * 100)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pvp-resource-wrap">
|
||||
<span>{gameClass.resourceName} {Math.floor(cpuSide.resource)} / {maxResource}</span>
|
||||
<div className="bar mana-bar"><span style={{ width: `${(cpuSide.resource / maxResource) * 100}%` }} /></div>
|
||||
<div className="bar mana-bar"><span style={barFillStyle((cpuSide.resource / maxResource) * 100)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,11 @@ import { completeRoguelike } from '../profile'
|
||||
import type { CharacterProfile } from '../profile'
|
||||
import type { GameMode } from '../gameRepository'
|
||||
import { PartyMemberFrame } from './PartyFrames'
|
||||
import { SpellBar, type SpellSlot } from './SpellBars'
|
||||
import { SpellBar } from './SpellBars'
|
||||
import { barFillStyle } from './barStyles'
|
||||
import { focusFirstControl, useGameAction, useInput, type InputAction } from '../input'
|
||||
import { useDeadlineTimer, useRoundCountdown } from '../hooks/useCountdownTimer'
|
||||
import { useSpellSlots } from '../hooks/useSpellSlots'
|
||||
import { useSidedFloatingCombatText } from '../hooks/useFloatingCombatText'
|
||||
import { usePartyTargeting } from '../hooks/usePartyTargeting'
|
||||
import { useDualScreen, useDualScreenPublisher } from '../dualScreen'
|
||||
@@ -31,10 +33,13 @@ import {
|
||||
spellPowerMultiplier,
|
||||
spellResourceCost as modifiedSpellResourceCost,
|
||||
} from '../combat/spellModifiers'
|
||||
import { advanceCooldowns, regenerateResource, tickSeconds } from '../combat/combatTick'
|
||||
import { regenerateResource } from '../combat/combatTick'
|
||||
import { advanceMemberTick } from '../combat/combatEngine'
|
||||
import { buildStadiumDualScreenState } from '../combat/dualScreenPayloads'
|
||||
import { applyPvpSpellCast } from '../combat/pvpSpellCasting'
|
||||
import {
|
||||
pruneExpiredCooldowns,
|
||||
} from '../combat/spellCasting'
|
||||
import {
|
||||
appendCombatLog,
|
||||
STADIUM_COMBAT_LOG_LIMIT,
|
||||
@@ -400,12 +405,16 @@ export function PvpStadiumScreen({
|
||||
const opponentLabel = liveMatch ? liveMatch.opponentName : `CPU ${cpuDifficulty ?? 1}`
|
||||
const playerAlive = playerSide.party.some((member) => member.health > 0)
|
||||
const playerBuffCounts = useMemo(() => createStackCounts(playerSide.buffs), [playerSide.buffs])
|
||||
const playerSpellSlots = useMemo<SpellSlot[]>(() => starterSpells.map((spell, slotIndex) => ({
|
||||
...spell,
|
||||
cost: spellResourceCost(spell, playerBuffCounts, playerSide.freeCastReady),
|
||||
slotIndex,
|
||||
remaining: playerSide.cooldowns[spell.id] ?? 0,
|
||||
})), [playerBuffCounts, playerSide.cooldowns, playerSide.freeCastReady, starterSpells])
|
||||
const playerSpellSlotCost = useCallback(
|
||||
(spell: Spell) => spellResourceCost(spell, playerBuffCounts, playerSide.freeCastReady),
|
||||
[playerBuffCounts, playerSide.freeCastReady],
|
||||
)
|
||||
const playerSpellSlots = useSpellSlots({
|
||||
spells: starterSpells,
|
||||
cooldowns: playerSide.cooldowns,
|
||||
active: status === 'playing' && !paused,
|
||||
cost: playerSpellSlotCost,
|
||||
})
|
||||
const opponentBuffSummary = useMemo(
|
||||
() => summarizeStacks(cpuSide.buffs, buffCatalog),
|
||||
[buffCatalog, cpuSide.buffs],
|
||||
@@ -790,7 +799,7 @@ export function PvpStadiumScreen({
|
||||
...side,
|
||||
party: nextParty,
|
||||
resource: regenerateResource(side.resource, RESOURCE_REGEN_PER_TICK, MAX_RESOURCE),
|
||||
cooldowns: advanceCooldowns(side.cooldowns, tickSeconds(TICK_MS)),
|
||||
cooldowns: pruneExpiredCooldowns(side.cooldowns),
|
||||
survivalSeconds: nextSurvival,
|
||||
dampeningPercent,
|
||||
}
|
||||
@@ -1168,16 +1177,16 @@ export function PvpStadiumScreen({
|
||||
</section>
|
||||
|
||||
<section className="dual-top-spell-strip">
|
||||
{starterSpells.map((spell, slotIndex) => {
|
||||
const remaining = playerSide.cooldowns[spell.id] ?? 0
|
||||
const cost = spellResourceCost(spell, playerBuffCounts, playerSide.freeCastReady)
|
||||
{playerSpellSlots.map((spell, slotIndex) => {
|
||||
if (!spell) return null
|
||||
const remaining = spell.remaining
|
||||
const percent = remaining > 0
|
||||
? Math.min(100, (remaining / Math.max(1, spell.cooldown)) * 100)
|
||||
: 0
|
||||
return (
|
||||
<button
|
||||
className="dual-top-spell"
|
||||
disabled={status !== 'playing' || !playerAlive || remaining > 0 || playerSide.resource < cost || paused}
|
||||
disabled={status !== 'playing' || !playerAlive || remaining > 0 || playerSide.resource < spell.cost || paused}
|
||||
key={spell.id}
|
||||
onClick={() => castPlayerSpell(spell)}
|
||||
type="button"
|
||||
@@ -1192,7 +1201,7 @@ export function PvpStadiumScreen({
|
||||
<div className="dual-top-resource">
|
||||
<strong>{gameClass.resourceName} {Math.floor(playerSide.resource)} / {MAX_RESOURCE}</strong>
|
||||
<div className="bar mana-bar">
|
||||
<span style={{ width: `${(playerSide.resource / MAX_RESOURCE) * 100}%` }} />
|
||||
<span style={barFillStyle((playerSide.resource / MAX_RESOURCE) * 100)} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1226,7 +1235,7 @@ export function PvpStadiumScreen({
|
||||
</div>
|
||||
<div className="pvp-resource-wrap">
|
||||
<span>{gameClass.resourceName} {Math.floor(playerSide.resource)} / {MAX_RESOURCE}</span>
|
||||
<div className="bar mana-bar"><span style={{ width: `${(playerSide.resource / MAX_RESOURCE) * 100}%` }} /></div>
|
||||
<div className="bar mana-bar"><span style={barFillStyle((playerSide.resource / MAX_RESOURCE) * 100)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="party-grid pvp-party-grid">
|
||||
@@ -1256,7 +1265,7 @@ export function PvpStadiumScreen({
|
||||
</div>
|
||||
<div className="pvp-resource-wrap">
|
||||
<span>{gameClass.resourceName} {Math.floor(cpuSide.resource)} / {MAX_RESOURCE}</span>
|
||||
<div className="bar mana-bar"><span style={{ width: `${(cpuSide.resource / MAX_RESOURCE) * 100}%` }} /></div>
|
||||
<div className="bar mana-bar"><span style={barFillStyle((cpuSide.resource / MAX_RESOURCE) * 100)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="party-grid pvp-party-grid">
|
||||
|
||||
@@ -32,11 +32,13 @@ export function SettingsScreen({ onBack }: { onBack: () => void }) {
|
||||
capture,
|
||||
controllerIconStyle,
|
||||
directPartyTargeting,
|
||||
combatTouchLocked,
|
||||
beginCapture,
|
||||
cancelCapture,
|
||||
resetBindings,
|
||||
setControllerIconStyle,
|
||||
setDirectPartyTargeting,
|
||||
setCombatTouchLocked,
|
||||
} = useInput()
|
||||
const {
|
||||
enabled: dualScreenEnabled,
|
||||
@@ -180,6 +182,14 @@ export function SettingsScreen({ onBack }: { onBack: () => void }) {
|
||||
>
|
||||
{directPartyTargeting ? 'Direct Targeting On' : 'Direct Targeting Off'}
|
||||
</button>
|
||||
<button
|
||||
aria-pressed={combatTouchLocked}
|
||||
className={combatTouchLocked ? 'selected' : ''}
|
||||
onClick={() => setCombatTouchLocked(!combatTouchLocked)}
|
||||
type="button"
|
||||
>
|
||||
{combatTouchLocked ? 'Combat Touch Locked' : 'Combat Touch Unlocked'}
|
||||
</button>
|
||||
<div className="controller-icon-options">
|
||||
<span>Controller Icons</span>
|
||||
{(['xbox', 'playstation', 'nintendo'] as const).map((style) => (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { memo } from 'react'
|
||||
import type { ControllerIconStyle } from '../input'
|
||||
import type { Spell } from '../game'
|
||||
import { ControllerBindingLabel } from './ControllerIcons'
|
||||
import { barFillStyle } from './barStyles'
|
||||
|
||||
export type SpellSlot = (Spell & {
|
||||
cost: number
|
||||
@@ -28,7 +29,7 @@ export const ResourceBar = memo(function ResourceBar({
|
||||
{unavailableText ?? `${resourceName} ${Math.floor(resource)} / ${maxResource}`}
|
||||
</span>
|
||||
{speedMultiplier === 2 && <strong className="speed-badge">2x speed</strong>}
|
||||
<div className="bar mana-bar"><span style={{ width: `${(resource / maxResource) * 100}%` }} /></div>
|
||||
<div className="bar mana-bar"><span style={barFillStyle((resource / maxResource) * 100)} /></div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
function clampScale(value: number) {
|
||||
if (!Number.isFinite(value)) return 0
|
||||
return Math.min(1, Math.max(0, value))
|
||||
}
|
||||
|
||||
export function barFillStyle(
|
||||
percent: number,
|
||||
origin: 'left' | 'right' = 'left',
|
||||
): CSSProperties {
|
||||
return {
|
||||
transform: `scaleX(${clampScale(percent / 100)})`,
|
||||
transformOrigin: `${origin} center`,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user