I Want To Heal 2 web/server v1.0.0
This commit is contained in:
@@ -0,0 +1,448 @@
|
||||
export type ActionDifficulty = 'ilvl-1' | 'ilvl-10' | 'ilvl-20' | 'ilvl-30'
|
||||
export type ActionDungeonId = 'bulldrome' | 'yian-kut-ku' | 'rathian'
|
||||
export type ActionRunMode = 'hunt' | 'marathon'
|
||||
export type ActionGearSource = 'bulldrome' | 'yian-kut-ku'
|
||||
export type ActionCoinColor = 'white' | 'green' | 'blue' | 'purple'
|
||||
export type ActionCoinWallet = Record<ActionGearSource, Record<ActionCoinColor, number>>
|
||||
|
||||
export type ActionDifficultyTier = {
|
||||
id: ActionDifficulty
|
||||
label: string
|
||||
itemLevel: 1 | 10 | 20 | 30
|
||||
coinColor: ActionCoinColor
|
||||
coinLabel: string
|
||||
healthMultiplier: number
|
||||
damageMultiplier: number
|
||||
lootMultiplier: number
|
||||
experience: number
|
||||
}
|
||||
|
||||
export type ActionGearSlot =
|
||||
| 'weapon'
|
||||
| 'helmet'
|
||||
| 'chest'
|
||||
| 'gloves'
|
||||
| 'boots'
|
||||
| 'pants'
|
||||
| 'ring'
|
||||
| 'necklace'
|
||||
| 'trinket'
|
||||
|
||||
export type ActionGearPiece = {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
source: ActionGearSource
|
||||
slot: ActionGearSlot
|
||||
itemLevel: number
|
||||
}
|
||||
|
||||
export type ActionGearStats = {
|
||||
healingPower: number
|
||||
stamina: number
|
||||
}
|
||||
|
||||
export type ActionRunReward = {
|
||||
coins: number
|
||||
coinName: string
|
||||
experience: number
|
||||
gear: ActionGearPiece[]
|
||||
leveledUp: boolean
|
||||
}
|
||||
|
||||
export type ActionCharacter = {
|
||||
id: string
|
||||
name: string
|
||||
level: number
|
||||
experience: number
|
||||
actionCoins: ActionCoinWallet
|
||||
bulldromeCoins: number
|
||||
yianKutKuCoins: number
|
||||
bulldromeNormalClears: number
|
||||
bulldromeHardClears: number
|
||||
yianKutKuNormalClears: number
|
||||
yianKutKuHardClears: number
|
||||
inventory: ActionGearPiece[]
|
||||
}
|
||||
|
||||
export const ACTION_GEAR_SLOTS: Array<{
|
||||
slot: ActionGearSlot
|
||||
label: string
|
||||
glyph: string
|
||||
}> = [
|
||||
{ slot: 'weapon', label: 'Weapon', glyph: '/' },
|
||||
{ slot: 'helmet', label: 'Helmet', glyph: 'H' },
|
||||
{ slot: 'chest', label: 'Chest', glyph: 'C' },
|
||||
{ slot: 'gloves', label: 'Gloves', glyph: 'G' },
|
||||
{ slot: 'boots', label: 'Boots', glyph: 'B' },
|
||||
{ slot: 'pants', label: 'Pants', glyph: 'P' },
|
||||
{ slot: 'ring', label: 'Ring', glyph: 'O' },
|
||||
{ slot: 'necklace', label: 'Necklace', glyph: 'N' },
|
||||
{ slot: 'trinket', label: 'Trinket', glyph: 'T' },
|
||||
]
|
||||
|
||||
export const ACTION_DIFFICULTY_TIERS: ActionDifficultyTier[] = [
|
||||
{
|
||||
id: 'ilvl-1',
|
||||
label: 'iLvl 1',
|
||||
itemLevel: 1,
|
||||
coinColor: 'white',
|
||||
coinLabel: 'White Coins',
|
||||
healthMultiplier: 1,
|
||||
damageMultiplier: 1,
|
||||
lootMultiplier: 1,
|
||||
experience: 60,
|
||||
},
|
||||
{
|
||||
id: 'ilvl-10',
|
||||
label: 'iLvl 10',
|
||||
itemLevel: 10,
|
||||
coinColor: 'green',
|
||||
coinLabel: 'Green Coins',
|
||||
healthMultiplier: 1.55,
|
||||
damageMultiplier: 1.28,
|
||||
lootMultiplier: 2,
|
||||
experience: 110,
|
||||
},
|
||||
{
|
||||
id: 'ilvl-20',
|
||||
label: 'iLvl 20',
|
||||
itemLevel: 20,
|
||||
coinColor: 'blue',
|
||||
coinLabel: 'Blue Coins',
|
||||
healthMultiplier: 2.25,
|
||||
damageMultiplier: 1.62,
|
||||
lootMultiplier: 3,
|
||||
experience: 180,
|
||||
},
|
||||
{
|
||||
id: 'ilvl-30',
|
||||
label: 'iLvl 30',
|
||||
itemLevel: 30,
|
||||
coinColor: 'purple',
|
||||
coinLabel: 'Purple Coins',
|
||||
healthMultiplier: 3.1,
|
||||
damageMultiplier: 2.05,
|
||||
lootMultiplier: 4,
|
||||
experience: 280,
|
||||
},
|
||||
]
|
||||
|
||||
const ACTION_SAVE_KEY = 'i-want-to-heal:action-mode-save:v2'
|
||||
const LEGACY_ACTION_SAVE_KEY = 'i-want-to-heal:action-mode-save:v1'
|
||||
const MAX_ACTION_LEVEL = 25
|
||||
export const BULLDROME_UPGRADE_COST = 5
|
||||
export const ACTION_GEAR_UPGRADE_COST = 5
|
||||
|
||||
const DEFAULT_ACTION_CHARACTER: ActionCharacter = {
|
||||
id: 'action-local-1',
|
||||
name: 'Action Healer',
|
||||
level: 1,
|
||||
experience: 0,
|
||||
actionCoins: createEmptyCoinWallet(),
|
||||
bulldromeCoins: 0,
|
||||
yianKutKuCoins: 0,
|
||||
bulldromeNormalClears: 0,
|
||||
bulldromeHardClears: 0,
|
||||
yianKutKuNormalClears: 0,
|
||||
yianKutKuHardClears: 0,
|
||||
inventory: [],
|
||||
}
|
||||
|
||||
export function loadActionCharacter(): ActionCharacter {
|
||||
const saved = window.localStorage.getItem(ACTION_SAVE_KEY)
|
||||
?? window.localStorage.getItem(LEGACY_ACTION_SAVE_KEY)
|
||||
if (!saved) return DEFAULT_ACTION_CHARACTER
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(saved) as Partial<ActionCharacter>
|
||||
const experience = Number(parsed.experience ?? DEFAULT_ACTION_CHARACTER.experience)
|
||||
return {
|
||||
...DEFAULT_ACTION_CHARACTER,
|
||||
...parsed,
|
||||
id: DEFAULT_ACTION_CHARACTER.id,
|
||||
experience,
|
||||
level: getActionLevel(experience),
|
||||
actionCoins: normalizeCoinWallet(parsed),
|
||||
bulldromeCoins: Number(parsed.bulldromeCoins ?? DEFAULT_ACTION_CHARACTER.bulldromeCoins),
|
||||
yianKutKuCoins: Number(parsed.yianKutKuCoins ?? DEFAULT_ACTION_CHARACTER.yianKutKuCoins),
|
||||
bulldromeNormalClears: Number(parsed.bulldromeNormalClears ?? DEFAULT_ACTION_CHARACTER.bulldromeNormalClears),
|
||||
bulldromeHardClears: Number(parsed.bulldromeHardClears ?? DEFAULT_ACTION_CHARACTER.bulldromeHardClears),
|
||||
yianKutKuNormalClears: Number(parsed.yianKutKuNormalClears ?? DEFAULT_ACTION_CHARACTER.yianKutKuNormalClears),
|
||||
yianKutKuHardClears: Number(parsed.yianKutKuHardClears ?? DEFAULT_ACTION_CHARACTER.yianKutKuHardClears),
|
||||
inventory: Array.isArray(parsed.inventory) ? parsed.inventory.map(normalizeGearPiece) : [],
|
||||
}
|
||||
} catch {
|
||||
return DEFAULT_ACTION_CHARACTER
|
||||
}
|
||||
}
|
||||
|
||||
export function saveActionCharacter(character: ActionCharacter) {
|
||||
window.localStorage.setItem(ACTION_SAVE_KEY, JSON.stringify(character))
|
||||
}
|
||||
|
||||
export function completeBulldromeHunt(
|
||||
character: ActionCharacter,
|
||||
difficulty: ActionDifficulty,
|
||||
): { character: ActionCharacter, reward: ActionRunReward } {
|
||||
const tier = getActionDifficultyTier(difficulty)
|
||||
const coinRoll = randomInt(1, 3)
|
||||
const lootMultiplier = tier.lootMultiplier
|
||||
const coinReward = coinRoll * lootMultiplier
|
||||
const experienceReward = tier.experience
|
||||
const nextExperience = character.experience + experienceReward
|
||||
const nextLevel = getActionLevel(nextExperience)
|
||||
const gear = rollBulldromeGear(lootMultiplier, tier.itemLevel)
|
||||
const actionCoins = addActionCoins(character.actionCoins, 'bulldrome', tier.coinColor, coinReward)
|
||||
|
||||
return {
|
||||
character: {
|
||||
...character,
|
||||
level: nextLevel,
|
||||
experience: nextExperience,
|
||||
actionCoins,
|
||||
bulldromeCoins: actionCoins.bulldrome.white,
|
||||
bulldromeNormalClears: character.bulldromeNormalClears + (difficulty === 'ilvl-1' ? 1 : 0),
|
||||
bulldromeHardClears: character.bulldromeHardClears + (difficulty !== 'ilvl-1' ? 1 : 0),
|
||||
inventory: [...character.inventory, ...gear],
|
||||
},
|
||||
reward: {
|
||||
coins: coinReward,
|
||||
experience: experienceReward,
|
||||
gear,
|
||||
coinName: `${tier.coinLabel.replace(' Coins', '')} Bulldrome Coins`,
|
||||
leveledUp: nextLevel > character.level,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function completeActionDungeonHunt(
|
||||
character: ActionCharacter,
|
||||
dungeonId: ActionDungeonId,
|
||||
difficulty: ActionDifficulty,
|
||||
): { character: ActionCharacter, reward: ActionRunReward } {
|
||||
if (dungeonId === 'bulldrome') return completeBulldromeHunt(character, difficulty)
|
||||
|
||||
if (dungeonId === 'yian-kut-ku') {
|
||||
const tier = getActionDifficultyTier(difficulty)
|
||||
const coinRoll = randomInt(1, 3)
|
||||
const lootMultiplier = tier.lootMultiplier
|
||||
const coinReward = coinRoll * lootMultiplier
|
||||
const experienceReward = Math.ceil(tier.experience * 1.25)
|
||||
const nextExperience = character.experience + experienceReward
|
||||
const nextLevel = getActionLevel(nextExperience)
|
||||
const gear = rollDungeonGear('yian-kut-ku', lootMultiplier, tier.itemLevel)
|
||||
const actionCoins = addActionCoins(character.actionCoins, 'yian-kut-ku', tier.coinColor, coinReward)
|
||||
|
||||
return {
|
||||
character: {
|
||||
...character,
|
||||
level: nextLevel,
|
||||
experience: nextExperience,
|
||||
actionCoins,
|
||||
yianKutKuCoins: actionCoins['yian-kut-ku'].white,
|
||||
yianKutKuNormalClears: character.yianKutKuNormalClears + (difficulty === 'ilvl-1' ? 1 : 0),
|
||||
yianKutKuHardClears: character.yianKutKuHardClears + (difficulty !== 'ilvl-1' ? 1 : 0),
|
||||
inventory: [...character.inventory, ...gear],
|
||||
},
|
||||
reward: {
|
||||
coins: coinReward,
|
||||
coinName: `${tier.coinLabel.replace(' Coins', '')} Yian Kut-Ku Coins`,
|
||||
experience: experienceReward,
|
||||
gear,
|
||||
leveledUp: nextLevel > character.level,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const experienceReward = getActionDifficultyTier(difficulty).experience
|
||||
const nextExperience = character.experience + experienceReward
|
||||
const nextLevel = getActionLevel(nextExperience)
|
||||
return {
|
||||
character: {
|
||||
...character,
|
||||
level: nextLevel,
|
||||
experience: nextExperience,
|
||||
},
|
||||
reward: {
|
||||
coins: 0,
|
||||
coinName: 'Coins',
|
||||
experience: experienceReward,
|
||||
gear: [],
|
||||
leveledUp: nextLevel > character.level,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function upgradeBulldromeGear(character: ActionCharacter, itemId: string): ActionCharacter {
|
||||
const item = character.inventory.find((candidate) => candidate.id === itemId)
|
||||
if (!item || item.itemLevel >= getActionGearUpgradeCap(item) || getUpgradeCoinCount(character, item) < ACTION_GEAR_UPGRADE_COST) return character
|
||||
|
||||
return {
|
||||
...character,
|
||||
...spendUpgradeCoins(character, item),
|
||||
inventory: character.inventory.map((candidate) => (
|
||||
candidate.id === itemId
|
||||
? { ...candidate, itemLevel: candidate.itemLevel + 1 }
|
||||
: candidate
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
export function getUpgradeCoinCount(character: ActionCharacter, item: Pick<ActionGearPiece, 'source'>) {
|
||||
const fullItem = item as Partial<Pick<ActionGearPiece, 'itemLevel'>>
|
||||
const coinColor = getCoinColorForItemLevel(fullItem.itemLevel ?? 1)
|
||||
return getActionCoinCount(character, item.source, coinColor)
|
||||
}
|
||||
|
||||
export function getUpgradeCoinName(item: Pick<ActionGearPiece, 'source'> & Partial<Pick<ActionGearPiece, 'itemLevel'>>) {
|
||||
const tier = getTierForItemLevel(item.itemLevel ?? 1)
|
||||
const sourceName = item.source === 'yian-kut-ku' ? 'Yian Kut-Ku' : 'Bulldrome'
|
||||
return `${tier.coinLabel.replace(' Coins', '')} ${sourceName} Coins`
|
||||
}
|
||||
|
||||
export function getActionGearStats(item: Pick<ActionGearPiece, 'slot' | 'itemLevel'>): ActionGearStats {
|
||||
const slotWeight = item.slot === 'weapon'
|
||||
? 2
|
||||
: item.slot === 'chest' || item.slot === 'helmet' || item.slot === 'pants'
|
||||
? 1.5
|
||||
: 1
|
||||
const healingPower = Math.ceil(item.itemLevel * slotWeight)
|
||||
const stamina = item.slot === 'ring' || item.slot === 'necklace' || item.slot === 'trinket'
|
||||
? item.itemLevel * 2
|
||||
: item.itemLevel
|
||||
|
||||
return { healingPower, stamina }
|
||||
}
|
||||
|
||||
export function getActionLevel(experience: number) {
|
||||
return Math.min(MAX_ACTION_LEVEL, 1 + Math.floor(Math.max(0, experience) / 220))
|
||||
}
|
||||
|
||||
export function getActionLevelProgress(character: ActionCharacter) {
|
||||
const currentLevelStart = (character.level - 1) * 220
|
||||
const nextLevelStart = character.level * 220
|
||||
const earnedThisLevel = Math.max(0, character.experience - currentLevelStart)
|
||||
const neededThisLevel = Math.max(1, nextLevelStart - currentLevelStart)
|
||||
return {
|
||||
currentLevelStart,
|
||||
nextLevelStart,
|
||||
percent: character.level >= MAX_ACTION_LEVEL
|
||||
? 100
|
||||
: Math.max(0, Math.min(100, (earnedThisLevel / neededThisLevel) * 100)),
|
||||
}
|
||||
}
|
||||
|
||||
export function getActionDifficultyTier(difficulty: ActionDifficulty) {
|
||||
return ACTION_DIFFICULTY_TIERS.find((tier) => tier.id === difficulty) ?? ACTION_DIFFICULTY_TIERS[0]
|
||||
}
|
||||
|
||||
export function getActionCoinCount(
|
||||
character: Pick<ActionCharacter, 'actionCoins' | 'bulldromeCoins' | 'yianKutKuCoins'>,
|
||||
source: ActionGearSource,
|
||||
color: ActionCoinColor,
|
||||
) {
|
||||
if (color === 'white') return source === 'yian-kut-ku' ? character.yianKutKuCoins : character.bulldromeCoins
|
||||
return character.actionCoins?.[source]?.[color] ?? 0
|
||||
}
|
||||
|
||||
export function getTierForItemLevel(itemLevel: number) {
|
||||
return [...ACTION_DIFFICULTY_TIERS]
|
||||
.reverse()
|
||||
.find((tier) => itemLevel >= tier.itemLevel) ?? ACTION_DIFFICULTY_TIERS[0]
|
||||
}
|
||||
|
||||
export function getActionGearUpgradeCap(item: Pick<ActionGearPiece, 'itemLevel'>) {
|
||||
return getTierForItemLevel(item.itemLevel).itemLevel + 4
|
||||
}
|
||||
|
||||
function rollBulldromeGear(rolls: number, itemLevel: ActionDifficultyTier['itemLevel']) {
|
||||
return rollDungeonGear('bulldrome', rolls, itemLevel)
|
||||
}
|
||||
|
||||
function rollDungeonGear(source: ActionGearSource, rolls: number, itemLevel: ActionDifficultyTier['itemLevel']) {
|
||||
const gear: ActionGearPiece[] = []
|
||||
for (let index = 0; index < rolls; index += 1) {
|
||||
if (Math.random() > 0.75) continue
|
||||
const slot = ACTION_GEAR_SLOTS[randomInt(0, ACTION_GEAR_SLOTS.length - 1)]
|
||||
gear.push(createDungeonGear(source, slot.slot, itemLevel))
|
||||
}
|
||||
return gear
|
||||
}
|
||||
|
||||
function createDungeonGear(source: ActionGearSource, slot: ActionGearSlot, itemLevel: ActionDifficultyTier['itemLevel']): ActionGearPiece {
|
||||
const slotMeta = ACTION_GEAR_SLOTS.find((candidate) => candidate.slot === slot)!
|
||||
const sourceName = source === 'yian-kut-ku' ? 'Yian Kut-Ku' : 'Bulldrome'
|
||||
return {
|
||||
id: `${source}-${slot}-${Date.now()}-${Math.floor(Math.random() * 100000)}`,
|
||||
slug: `${source}-${slot}`,
|
||||
name: `${sourceName} ${slotMeta.label}`,
|
||||
source,
|
||||
slot,
|
||||
itemLevel,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGearPiece(item: ActionGearPiece): ActionGearPiece {
|
||||
const source = item.source ?? (item.slug?.startsWith('yian-kut-ku') ? 'yian-kut-ku' : 'bulldrome')
|
||||
return {
|
||||
...item,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
function spendUpgradeCoins(character: ActionCharacter, item: Pick<ActionGearPiece, 'source'>) {
|
||||
const itemWithLevel = item as Pick<ActionGearPiece, 'source'> & Partial<Pick<ActionGearPiece, 'itemLevel'>>
|
||||
const coinColor = getCoinColorForItemLevel(itemWithLevel.itemLevel ?? 1)
|
||||
const actionCoins = addActionCoins(character.actionCoins, item.source, coinColor, -ACTION_GEAR_UPGRADE_COST)
|
||||
if (item.source === 'yian-kut-ku' && coinColor === 'white') {
|
||||
return { actionCoins, yianKutKuCoins: actionCoins['yian-kut-ku'].white }
|
||||
}
|
||||
if (item.source === 'bulldrome' && coinColor === 'white') {
|
||||
return { actionCoins, bulldromeCoins: actionCoins.bulldrome.white }
|
||||
}
|
||||
return { actionCoins }
|
||||
}
|
||||
|
||||
function getCoinColorForItemLevel(itemLevel: number) {
|
||||
return getTierForItemLevel(itemLevel).coinColor
|
||||
}
|
||||
|
||||
function createEmptyCoinWallet(): ActionCoinWallet {
|
||||
return {
|
||||
bulldrome: { white: 0, green: 0, blue: 0, purple: 0 },
|
||||
'yian-kut-ku': { white: 0, green: 0, blue: 0, purple: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCoinWallet(parsed: Partial<ActionCharacter>) {
|
||||
const wallet = createEmptyCoinWallet()
|
||||
const saved = parsed.actionCoins
|
||||
for (const source of ['bulldrome', 'yian-kut-ku'] as const) {
|
||||
for (const color of ['white', 'green', 'blue', 'purple'] as const) {
|
||||
wallet[source][color] = Number(saved?.[source]?.[color] ?? 0)
|
||||
}
|
||||
}
|
||||
wallet.bulldrome.white = Number(parsed.bulldromeCoins ?? wallet.bulldrome.white)
|
||||
wallet['yian-kut-ku'].white = Number(parsed.yianKutKuCoins ?? wallet['yian-kut-ku'].white)
|
||||
return wallet
|
||||
}
|
||||
|
||||
function addActionCoins(
|
||||
wallet: ActionCoinWallet,
|
||||
source: ActionGearSource,
|
||||
color: ActionCoinColor,
|
||||
amount: number,
|
||||
) {
|
||||
return {
|
||||
...wallet,
|
||||
[source]: {
|
||||
...wallet[source],
|
||||
[color]: Math.max(0, wallet[source][color] + amount),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function randomInt(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
Reference in New Issue
Block a user