diff --git a/docs/custom-tab-layouts.md b/docs/custom-tab-layouts.md index 17bbfa7..d0d73dd 100644 --- a/docs/custom-tab-layouts.md +++ b/docs/custom-tab-layouts.md @@ -45,15 +45,15 @@ These are the existing components, but you can create more in [components.js](/j - prestige-button: The button to reset for a currency in this layer. -- text-input: A text input box. The argument is the name of the variable in player[layer] that the input is for, player[layer][argument] +- text-input: A text input box. The argument is the name of the variable in `player[layer]` that the input is for, `player[layer][argument]` (Works with strings, numbers, and Decimals!) - slider: Lets the user input a value with a slider. The argument a 3-element array: [name, min, max]. - The name is the name of the variable in player[layer] that the input is for, and min and max are the limits of the slider. + The name is the name of the variable in `player[layer]` that the input is for, and min and max are the limits of the slider. (Does not work for Decimal values) - drop-down: Lets the user input a value with a dropdown menu. The argument a 2-element array: [name, options]. - The name is the name of the variable in player[layer] that the input is for, and options is an array of strings for options you can use. + The name is the name of the variable in `player[layer]` that the input is for, and options is an array of strings for options you can use. - drop-down-double: Same as `drop-down`, but each option is also an array with the first entry being the value and the second being the display. diff --git a/docs/layer-features.md b/docs/layer-features.md index bcf0b11..f8ecd65 100644 --- a/docs/layer-features.md +++ b/docs/layer-features.md @@ -86,10 +86,10 @@ You can make almost any value dynamic by using a function in its place, includin - type: **optional**. Determines which prestige formula you use. Defaults to "none". - - "normal": The amount of currency you gain is independent of its current amount (like Prestige). The formula before bonuses is based on `baseResource^exponent` - - "static": The cost is dependent on your total after reset. The formula before bonuses is based on `base^(x^exponent)` - - "custom": You can define everything, from the calculations to the text on the button, yourself. (See more at the bottom) - - "none": This layer does not prestige, and therefore does not need any of the other features in this section. + - "normal": The amount of currency you gain is independent of its current amount (like Prestige). The formula before bonuses is based on `baseResource^exponent` + - "static": The cost is dependent on your total after reset. The formula before bonuses is based on `base^(x^exponent)` + - "custom": You can define everything, from the calculations to the text on the button, yourself. (See more at the bottom) + - "none": This layer does not prestige, and therefore does not need any of the other features in this section. - baseResource: The name of the resource that determines how much of the main currency you gain on reset. @@ -147,14 +147,14 @@ You can make almost any value dynamic by using a function in its place, includin ## Other features - doReset(resettingLayer): **optional**. Is triggered when a layer on a row greater than or equal to this one does a reset. The default behavior is to reset everything on the row, but only if it was triggered by a layer in a higher row. `doReset` is always called for side layers, but for these the default behavior is to reset nothing. - + If you want to keep things, determine what to keep based on `resettingLayer`, `milestones`, and such, then call `layerDataReset(layer, keep)`, where `layer` is this layer, and `keep` is an array of the names of things to keep. It can include things like "points", "best", "total" (for this layer's prestige currency), "upgrades", any unique variables like "generatorPower", etc. If you want to only keep specific upgrades or something like that, save them in a separate variable, then call `layerDataReset`, and then set `player[this.layer].upgrades` to the saved upgrades. -- update(diff): **optional**. This function is called every game tick. Use it for any passive resource production or time-based things. `diff` is the time since the last tick. +- update(diff): **optional**. This function is called every game tick. Use it for any passive resource production or time-based things. `diff` is the time since the last tick. - autoUpgrade: **optional**, a boolean value, if true, the game will attempt to buy this layer's upgrades every tick. Defaults to false. -- automate(): **optional**. This function is called every game tick, after production. Use it to activate automation things that aren't otherwise supported. +- automate(): **optional**. This function is called every game tick, after production. Use it to activate automation things that aren't otherwise supported. - resetsNothing: **optional**. Returns true if this layer shouldn't trigger any resets when you prestige. @@ -175,11 +175,12 @@ componentStyles: { - leftTab: **optional**, if true, this layer will use the left tab instead of the right tab. -- previousTab: **optional**, a layer's id. If a layer has a previousTab, the layer will always have a back arrow and pressing the back arrow on this layer will take you to the layer with this id. +- previousTab: **optional**, a layer's id. If a layer has a previousTab, the layer will always have a back arrow and pressing the back arrow on this layer will take you to the layer with this id. - deactivated: **optional**, if this is true, hasUpgrade, hasChallenge, hasAchievement, and hasMilestone will return false for things in the layer, and you will be unable to buy or click things, or gain achievements/milestones on the layer. You will have to disable effects of buyables, the innate layer effect, and possibly other things yourself. -## Custom Prestige type +## Custom Prestige type + (All of these can also be used by other prestige types) - getResetGain(): **mostly for custom prestige type**. Returns how many points you should get if you reset now. You can call `getResetGain(this.layer, useType = "static")` or similar to calculate what your gain would be under another prestige type (provided you have all of the required features in the layer). @@ -189,4 +190,4 @@ componentStyles: { - canReset(): **mostly for custom prestige type**. Return true only if you have the resources required to do a prestige here. - prestigeNotify(): **mostly for custom prestige types**, returns true if this layer should be subtly highlighted to indicate you - can prestige for a meaningful gain. \ No newline at end of file + can prestige for a meaningful gain. diff --git a/js/dummies/dummies.d.ts b/js/dummies/dummies.d.ts index 09ee7e7..263ae11 100644 --- a/js/dummies/dummies.d.ts +++ b/js/dummies/dummies.d.ts @@ -35,20 +35,18 @@ type TabFormatEntries = ['display-text', Computable - baseStyle?: Computable - fillStyle?: Computable - borderStyle?: Computable - textStyle?: Computable - } -]; +['tile', tile] | +['dynabar', { + direction: 0 | 1 | 2 | 3 + progress(): number | Decimal + width: number + height: number + display?: Computable + baseStyle?: Computable + fillStyle?: Computable + borderStyle?: Computable + textStyle?: Computable +}]; type tile = { text?: Computable @@ -1768,6 +1766,7 @@ declare class Item { /** Amount is reset when that row is */ row?: Layer['row'] unlocked?: boolean + categories: categories[] lore: Computable @@ -1791,9 +1790,15 @@ declare class Item { effectDescription(amount?: DecimalSource): string } -type items = 'slime_goo' | 'slime_core_shard' | 'slime_core' | 'dense_slime_core'; +type items = 'slime_goo' | 'slime_core_shard' | 'slime_core' | 'dense_slime_core' | + 'slime_crystal' | 'slime_knife' | 'slime_injector' | 'slime_die'; type monsters = 'slime'; +type drop_sources = `kill:${monsters}`; +type drop_types = 'kill' | 'crafting'; +type categories = 'materials' | 'equipment' | + 'slime'; + type Layers = { // Side ach: Layer<'ach'> & { @@ -1836,7 +1841,7 @@ type Layers = { lore: Computable unlocked?(): boolean } } - monster_list(): monsters[] + list(): monsters[] kill: { color: string total(): Decimal @@ -1845,7 +1850,6 @@ type Layers = { damage: { base(): Decimal mult(): Decimal - total(): Decimal } xp: { base(): Decimal @@ -1861,7 +1865,59 @@ type Layers = { } } // Row 1 + l: Layer<'l'> & { + skill_points: { + color: string + total(): Decimal + remaining(): Decimal + } + } + c: Layer<'c'> & { + chance_multiplier(): Decimal + crafting: { + /** Max multiplier for crafting amount */ + max(): Decimal + /** Total times static recipes have been crafted */ + crafted(): Decimal + /** Divides crafting time */ + speed(): Decimal + } + recipes: { + [id: string]: { + private _id: string | null + readonly id: string + unlocked?: Computable + + /** Items consumed for an output multiplier */ + consumes(amount?: DecimalSource, all_time?: DecimalSource): [items, Decimal][] + /** Items produced with a given multiplier */ + produces(amount?: DecimalSource, all_time?: DecimalSource): [items, Decimal][] + /** Duration to craft an amount of items, if 0 or absent, crafting is instant */ + duration?(amount?: DecimalSource, all_time?: DecimalSource): Decimal + formulas: { + duration?: string + consumes: { [item in items]?: string } + produces: { [item in items]?: string } + } + /** + * If true, all time crafted amount will be counted for consuming and producing + */ + static?: boolean + categories: categories[] + } + } + } // Row 2 + b: Layer<'b'> & { + complete(): Decimal + challenges?: { + [id: string]: Challenge<'b'> & { + progress(): Decimal + display(): string + color: Computable + } + } + } }; type Temp = { displayThings: (string | (() => string))[] @@ -1913,7 +1969,32 @@ type Player = { } } } // Row 1 + l: LayerData & {} + c: LayerData & { + shown: boolean + visiblity: { + inventory: { + [cat in categories]: 'show' | 'hide' | 'ignore' + } + crafting: { + [cat in categories]: 'show' | 'hide' | 'ignore' + } + } + recipes: { + [id: string]: { + target: Decimal + making: Decimal + time: Decimal + /** Total times crafted */ + crafted: Decimal + } + } + compendium: items | false + } // Row 2 + b: LayerData & { + shown: boolean + } }; /** Adds the items in question to the player data */ diff --git a/js/items.js b/js/items.js index d9138d8..fa0d656 100644 --- a/js/items.js +++ b/js/items.js @@ -9,21 +9,34 @@ const item_list = { name: 'slime goo', grid: [0, 0], icon() { - if (inChallenge('b', 11)) return [0, 4]; - return [0, 0]; + let icon = [0, 0]; + + if (inChallenge('b', 11)) icon[1] += 4; + + return icon; }, row: 1, - sources: {}, + sources: { + chance() { + if (D.eq(tmp.c.chance_multiplier, 0)) return {}; + + let chance = D(1 / 2); + + chance = chance.times(tmp.c.chance_multiplier); + + return { 'kill:slime': chance }; + }, + }, lore() { - if (inChallenge('b', 11)) { - return `A chunk of orange goo.
- Feels warm to the touch.
- It tastes spicy, and is hard to chew.`; - } + if (inChallenge('b', 11)) return `A chunk of orange goo.
+ Feels warm to the touch.
+ Tastes bad and spicy.`; + return `A chunk of green goo.
Feels weird to the touch.
Not only does it taste like dirty water, but it's hard to chew.`; }, + categories: ['materials', 'slime'], }, 'slime_core_shard': { id: null, @@ -31,21 +44,34 @@ const item_list = { name: 'slime core shard', grid: [0, 1], icon() { - if (inChallenge('b', 11)) return [0, 5]; - return [0, 1]; + let icon = [0, 1]; + + if (inChallenge('b', 11)) icon[1] += 4; + + return icon; }, row: 1, - sources: {}, + sources: { + chance() { + if (D.eq(tmp.c.chance_multiplier, 0)) return {}; + + let chance = D(1 / 7); + + chance = chance.times(tmp.c.chance_multiplier); + + return { 'kill:slime': chance }; + }, + }, lore() { - if (inChallenge('b', 11)) { - return `A dark red shard.
- Not sharp, but feels like it is.
- Can be combined into an intact core with a bit of goo.`; - } + if (inChallenge('b', 11)) return `A dark red shard.
+ Very sharp, be careful when handling.
+ Can be recombined into an intact core with a bit of goo.`; + return `A dark green shard.
Surprisingly sharp, so careful when handling.
Can be recombined into an intact core with a bit of goo.`; }, + categories: ['materials', 'slime'], }, 'slime_core': { id: null, @@ -53,56 +79,323 @@ const item_list = { name: 'slime core', grid: [0, 2], icon() { - if (inChallenge('b', 11)) return [0, 6]; - return [0, 2]; + let icon = [0, 2]; + + if (inChallenge('b', 11)) icon[1] += 4; + + return icon; }, row: 1, sources: { + chance() { + if (D.eq(tmp.c.chance_multiplier, 0)) return {}; + + let chance = D(1 / 24); + + chance = chance.times(tmp.c.chance_multiplier); + chance = chance.times(item_effect('slime_die').core_chance); + + return { 'kill:slime': chance }; + }, other: ['crafting'], }, lore() { - if (inChallenge('b', 11)) { - return `The very core of a slime.
- Smooth to the touch and valuable.
- Surprisingly solid.`; - } + if (inChallenge('b', 11)) return `The very core of a slime.
+ Hot to the touch and valuable.
+ Hard, but shatters easily.`; + return `The very core of a slime.
Smooth to the touch and valuable.
Fragile! Handle with care.`; }, - unlocked: false, + categories: ['materials', 'slime'], }, 'dense_slime_core': { id: null, color() { return tmp.xp.monsters.slime.color; }, name: 'dense slime core', grid: [0, 3], - icon: [0, 3], icon() { - if (inChallenge('b', 11)) return [0, 7]; - return [0, 3]; + let icon = [0, 3]; + + if (inChallenge('b', 11)) icon[1] += 4; + + return icon; }, row: 1, sources: { other: ['crafting'], }, lore() { - if (inChallenge('b', 11)) { - return `Wha- Oh no...
- It glows in a burning orange light.
- It seethes in anger.`; - } + if (inChallenge('b', 11)) return `Are you sure this is a good idea?
+ It glows in an angry red light.
+ You can feel it pulse in your hands.`; + return `The- This- What even is this?
It glows in a worrying green light.
You can feel it pulse in your hands.`; }, - unlocked: false, + categories: ['materials', 'slime'], + }, + 'slime_crystal': { + id: null, + color() { return tmp.xp.monsters.slime.color; }, + name: 'slime crystal', + grid: [1, 0], + icon() { + let icon = [1, 0]; + + if (inChallenge('b', 11)) icon[1] += 4; + + return icon; + }, + row: 1, + sources: { + other: ['crafting'], + }, + lore() { + if (inChallenge('b', 11)) return `A bright red crystal made of pure slime.
+ Can hold experience better than you.
+ A clunky nightlight.`; + + return `A bright green crystal made of pure slime.
+ Can hold experience better than you.
+ A clunky nightlight.`; + }, + categories: ['equipment', 'slime'], + effect(amount) { + const x = D(amount ?? player.items[this.id].amount); + + let xp_mult, xp_cap; + + if (inChallenge('b', 11) || hasChallenge('b', 11)) { + xp_mult = D.div(x, 9).add(1); + xp_cap = D.pow(1.15, x); + } else { + xp_mult = D.div(x, 10).add(1); + xp_cap = D.pow(1.1, x); + } + + return { xp_mult, xp_cap, }; + }, + effectDescription(amount) { + let gain, cap; + if (shiftDown) { + if (inChallenge('b', 11) || hasChallenge('b', 11)) { + gain = '[amount / 9 + 1]'; + cap = '[1.15 ^ amount]'; + } else { + gain = '[amount / 10 + 1]'; + cap = '[1.1 ^ amount]'; + } + } else { + const x = D(amount ?? player.items[this.id].amount), + effect = item_list[this.id].effect(x); + + gain = format(effect.xp_mult); + cap = format(effect.xp_cap); + } + + return `Multiply xp gain by ${gain} and cap by ${cap}`; + }, + }, + 'slime_knife': { + id: null, + color() { return tmp.xp.monsters.slime.color; }, + name: 'slime knife', + grid: [1, 1], + icon() { + let icon = [1, 1]; + + if (inChallenge('b', 11)) icon[1] += 4; + + return icon; + }, + row: 1, + sources: { + other: ['crafting'], + }, + lore() { + if (inChallenge('b', 11)) return `A sharp red weapon.
+ Requires a license to use in some kingdoms.
+ Chefs love this, as it allows cooking spicy food without spices`; + + return `A crude green weapon.
+ Very sharp, be careful with it.
+ Not recommended for cooking unless you like the taste of slime.`; + }, + categories: ['equipment', 'slime'], + effect(amount) { + const x = D(amount ?? player.items[this.id].amount); + + let damage; + + if (inChallenge('b', 11) || hasChallenge('b', 11)) { + damage = D.pow(1.3, x); + } else { + damage = D.pow(1.2, x); + } + + return { damage, }; + }, + effectDescription(amount) { + let damage; + if (shiftDown) { + if (inChallenge('b', 11) || hasChallenge('b', 11)) { + damage = '[1.3 ^ amount]'; + } else { + damage = '[1.2 ^ amount]'; + } + } else { + const x = D(amount ?? player.items[this.id].amount), + effect = item_list[this.id].effect(x); + + damage = format(effect.damage); + } + + return `Multiply damage dealt by ${damage}`; + }, + }, + 'slime_injector': { + id: null, + color() { return tmp.xp.monsters.slime.color; }, + name: 'slime injector', + grid: [1, 2], + icon() { + let icon = [1, 2]; + + if (inChallenge('b', 11)) icon[1] += 4; + + return icon; + }, + row: 1, + sources: { + other: ['crafting'], + }, + lore() { + if (inChallenge('b', 11)) return `This experimental drug destabilizes slimes.
+ It makes you feel... angrier? Weird.
+ Using too many may have side effects.`; + + return `This experimental drug destabilizes slimes.
+ It also makes you feel... stronger. Whatever that means.
+ Using too many may have side effects.`; + }, + categories: ['equipment', 'slime'], + effect(amount) { + const x = D(amount ?? player.items[this.id].amount); + + let health, level, xp_mult; + + if (inChallenge('b', 11) || hasChallenge('b', 11)) { + health = D.pow(1.1, x); + level = D.div(x, 9).add(1); + xp_mult = D.div(x, 5).root(2).floor().pow_base(.9); + } else { + health = D.pow(1.05, x); + level = D.div(x, 10).add(1); + xp_mult = D.div(x, 5).root(2).floor().pow_base(.95); + } + + return { health, level, xp_mult, }; + }, + effectDescription(amount) { + const x = D(amount ?? player.items[this.id].amount), + effect = item_list[this.id].effect(x), + show_xp = D.neq(effect.xp_mult, 1), + show_level = player.l.unlocked; + let health, level, xp; + if (shiftDown) { + if (inChallenge('b', 11) || hasChallenge('b', 11)) { + health = '[1.1 ^ amount]'; + level = '[amount / 9 + 1]'; + xp = '[0.9 ^ floor(2√(amount / 5))]'; + } else { + health = '[1.05 ^ amount]'; + level = '[amount / 10 + 1]'; + xp = '[0.95 ^ floor(2√(amount / 5))]'; + } + } else { + const effect = item_list[this.id].effect(x); + + health = format(effect.health); + level = format(effect.level); + xp = format(effect.xp_mult); + } + + const text = [`Divide enemy health by ${health}`]; + + if (show_level) text.push(`multiply level gain by ${level}`); + if (show_xp) text.push(`divide xp gain by ${xp}`); + + return listFormat.format(text); + }, + }, + 'slime_die': { + id: null, + color() { return tmp.xp.monsters.slime.color; }, + name: 'slime die', + grid: [1, 3], + icon() { + let icon = [1, 3]; + + if (inChallenge('b', 11)) icon[1] += 4; + + return icon; + }, + row: 1, + sources: { + other: ['crafting'], + }, + lore() { + if (inChallenge('b', 11)) return `A dice that glows in a worrying light.
+ It feels lucky, somehow.
+ Makes you feel like you can grab more from slimes.`; + + return `A green dice that glows in a more... worrying light.
+ It feels lucky, somehow.
+ Makes you feel like you can get more from slimes.`; + }, + categories: ['equipment', 'slime'], + effect(amount) { + const x = D(amount ?? player.items[this.id].amount); + + let luck, core_chance; + + if (inChallenge('b', 11) || hasChallenge('b', 11)) { + luck = D.div(x, 17.5).add(1); + } else { + luck = D.div(x, 20).add(1); + } + core_chance = D.root(x, 2).floor(); + + return { luck, core_chance, }; + }, + effectDescription(amount) { + let luck, core; + if (shiftDown) { + if (inChallenge('b', 11) || hasChallenge('b', 11)) { + luck = '[amount / 17.5 + 1]'; + } else { + luck = '[amount / 20 + 1]'; + } + core = '[floor(2√(amount))]'; + } else { + const x = D(amount ?? player.items[this.id].amount), + effect = item_list[this.id].effect(x); + + luck = format(effect.luck); + core = formatWhole(effect.core_chance); + } + + return `Multiply luck by ${luck} and core drop chances by ${core}`; + }, }, }; const ITEM_SIZES = { - width: 4, - height: 1, + width: 8, + height: 2, }; /** * @type {{[row in Layer['row']]: items[]}} @@ -217,10 +510,6 @@ function source_name(source) { return tmp.xp.monsters[sub[0]].name; case 'crafting': return 'crafting'; - case 'mining': - let text = 'mining'; - if (sub[1] == 'break') text = 'breaking'; - return `${text} ${tmp.m.ores[sub[0]].name}`; } } /** @@ -241,7 +530,7 @@ function source_drops(source) { Object.values(tmp.items).forEach(item => { if (!('sources' in item)) return; - if ('chance' in item.sources && source in item.sources.chance) items.chances[item.id] = item.sources.chance[source]; + if ('chance' in item.sources && source in item.sources.chance && D.gt(item.sources.chance[source], 0)) items.chances[item.id] = item.sources.chance[source]; }); return items; diff --git a/js/layers/main/0/experience.js b/js/layers/main/0/experience.js index 6580369..01c2512 100644 --- a/js/layers/main/0/experience.js +++ b/js/layers/main/0/experience.js @@ -2,7 +2,7 @@ const MONSTER_SIZES = { width: 1, - height: 1, + height: 2, }; addLayer('xp', { row: 0, @@ -64,7 +64,7 @@ addLayer('xp', { ['display-text', () => { const selected = player.xp.selected; - return `You are fighting a level ${formatWhole(tmp.xp.monsters[selected].level)} ${tmp.xp.monsters[selected].name}`; + return `You are fighting a level ${resourceColor(tmp.l.color, formatWhole(tmp.xp.monsters[selected].level))} ${tmp.xp.monsters[selected].name}`; }], ['raw-html', () => { return `
@@ -88,6 +88,19 @@ addLayer('xp', { return `Attack for ${format(damage)} damage`; }], ['display-text', 'Hold to click 5 times per second'], + 'blank', + ['display-text', () => { + if (D.lte(tmp.c.chance_multiplier, 0)) return ''; + + let drops = 'nothing', + count = ''; + const last_drops = player.xp.monsters[player.xp.selected].last_drops, + last_count = player.xp.monsters[player.xp.selected].last_drops_times; + if (last_drops.length) drops = listFormat.format(last_drops.map(([item, amount]) => `${format(amount)} ${tmp.items[item].name}`)); + if (last_count.gt(1)) count = ` (${formatWhole(last_count)})`; + + return `${capitalize(tmp.xp.monsters[player.xp.selected].name)} dropped ${drops}${count}`; + }], ], }, 'Upgrades': { @@ -112,10 +125,272 @@ addLayer('xp', { 'blank', ['column', () => bestiary_content(player.xp.lore)], ], - unlocked() { return hasUpgrade('xp', 23); }, + unlocked() { return hasUpgrade('xp', 21) || hasAchievement('ach', 51); }, }, }, upgrades: { + 11: { + title: 'Larger Sword', + kills: D.dOne, + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + return 'Deal +50% damage'; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D.dTwo, + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D(1.5); }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `*${format(upgradeEffect(this.layer, this.id))}`; + }, + }, + 12: { + title: 'Slimy Records', + kills: D(5), + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + return 'Gain +50% experience'; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D(5), + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D(1.5); }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `*${format(upgradeEffect(this.layer, this.id))}`; + }, + }, + 13: { + title: 'Level Down', + kills: D.dTen, + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + return 'Enemies lose a third of their health'; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D.dTen, + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D(2 / 3); }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `*${format(upgradeEffect(this.layer, this.id))}`; + }, + }, + 21: { + title: 'Blood Knowledge', + kills: D(20), + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + + let text = `Kills boost experience gain`; + if (!hasAchievement('ach', 51)) text += `
Unlock the Bestiary`; + if (shiftDown) text += `
log10(kills + 15)`; + + return text; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D(30), + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D.add(tmp.xp.kill.total, 15).log10(); }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `*${format(upgradeEffect(this.layer, this.id))}`; + }, + }, + 22: { + title: 'Trap', + kills: D(35), + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + return 'Passively deal 100% of your damage'; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D(75), + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D.dOne; }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `${format(tmp.xp.monsters[player.xp.selected].damage_per_second)} /s`; + }, + }, + 23: { + title: 'Deadly Sword', + kills: D(50), + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + + let text = `Kills boost damage dealt`; + if (shiftDown) text += `
log15(experience + 15)`; + + return text; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D(111), + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D.add(tmp.xp.kill.total, 15).log(15); }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `*${format(upgradeEffect(this.layer, this.id))}`; + }, + }, + 31: { + title: 'Unhealthy Knowledge', + kills: D(75), + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + + let text = `Experience lowers enemy health`; + if (shiftDown) text += `
1.1 ^ log5(experience + 5)`; + + return text; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D(200), + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D.add(player.xp.points, 5).log(5).pow_base(1.1); }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `/${format(upgradeEffect(this.layer, this.id))}`; + }, + }, + 32: { + title: 'Weak Points', + kills: D(100), + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + + let text = `Experience boosts damage dealt`; + if (shiftDown) text += `
log20(experience + 20)`; + + return text; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D(250), + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D.add(player.xp.points, 20).log(20); }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `*${format(upgradeEffect(this.layer, this.id))}`; + }, + }, + 33: { + title: 'Power of Riches', + kills: D(150), + show() { return hasUpgrade(this.layer, this.id) || D.gte(tmp.xp.kill.total, this.kills); }, + description() { + if (!tmp[this.layer].upgrades[this.id].show) { + return `Unlocked at ${formatWhole(this.kills)} kills`; + } + + return `Double XP gain
\ + Unlock 2 new layers`; + }, + canAfford() { return tmp[this.layer].upgrades[this.id].show; }, + cost: D(500), + style() { + if (!tmp[this.layer].upgrades[this.id].show) { + return { + 'background-color': 'transparent', + 'border': `5px dashed ${colors[options.theme][1]}`, + 'color': colors[options.theme][1], + }; + } + }, + effect() { return D.dTwo; }, + effectDisplay() { + if (!tmp[this.layer].upgrades[this.id].show) return ''; + return `*${format(upgradeEffect(this.layer, this.id))}`; + }, + }, }, bars: { health: { @@ -176,16 +451,16 @@ addLayer('xp', { }, onClick() { const selected = player.xp.selected, - i = tmp.xp.monster_list.indexOf(selected); + i = tmp.xp.list.indexOf(selected); - player.xp.selected = tmp.xp.monster_list[i - 1]; + player.xp.selected = tmp.xp.list[i - 1]; }, canClick() { const selected = player.xp.selected; - return selected != tmp.xp.monster_list[0]; + return selected != tmp.xp.list[0]; }, - unlocked() { tmp.xp.monster_list.length > 0 }, + unlocked() { tmp.xp.list.length > 0 }, }, 12: { style: { @@ -229,16 +504,16 @@ addLayer('xp', { }, onClick() { const selected = player.xp.selected, - i = tmp.xp.monster_list.indexOf(selected); + i = tmp.xp.list.indexOf(selected); - player.xp.selected = tmp.xp.monster_list[i + 1]; + player.xp.selected = tmp.xp.list[i + 1]; }, canClick() { const selected = player.xp.selected; - return selected != tmp.xp.monster_list[tmp.xp.monster_list.length - 1]; + return selected != tmp.xp.list[tmp.xp.list.length - 1]; }, - unlocked() { tmp.xp.monster_list.length > 0 }, + unlocked() { tmp.xp.list.length > 0 }, }, // Bestiary 21: { @@ -250,18 +525,18 @@ addLayer('xp', { 'background-position': '-120px -120px', }, onClick() { - const list = tmp.xp.monster_list, + const list = tmp.xp.list, i = list.indexOf(player.xp.lore); player.xp.lore = list[i - 1]; }, canClick() { - if (!Array.isArray(tmp.xp.monster_list)) return false; - const i = tmp.xp.monster_list.indexOf(player.xp.lore); + if (!Array.isArray(tmp.xp.list)) return false; + const i = tmp.xp.list.indexOf(player.xp.lore); return i > 0; }, unlocked() { /** @type {monsters[]} */ - const list = tmp.xp.monster_list; + const list = tmp.xp.list; return list.length > 1; }, }, @@ -275,19 +550,19 @@ addLayer('xp', { }, onClick() { /** @type {monsters[]} */ - const list = tmp.xp.monster_list, + const list = tmp.xp.list, i = list.indexOf(player.xp.lore); player.xp.lore = list[i + 1]; }, canClick() { - const list = tmp.xp.monster_list; + const list = tmp.xp.list; if (!Array.isArray(list)) return false; const i = list.indexOf(player.xp.lore); return i < list.length - 1; }, unlocked() { /** @type {monsters[]} */ - const list = tmp.xp.monster_list; + const list = tmp.xp.list; return list.length > 1; }, }, @@ -312,6 +587,19 @@ addLayer('xp', { const level = monster.level(data.kills); data.health = D.add(data.health, monster.health(level)); + + if (D.gt(tmp.c.chance_multiplier, 0)) { + const drops = get_source_drops(`kill:${id}`), + equal = drops.length == data.last_drops.length && + drops.every(([item, amount]) => data.last_drops.some(([litem, lamount]) => litem == item && D.eq_tolerance(amount, lamount, 1e-3))); + if (equal) { + data.last_drops_times = D.add(data.last_drops_times, 1); + } else { + data.last_drops_times = D.dOne; + data.last_drops = drops; + } + gain_items(drops); + } } }); }, @@ -330,12 +618,16 @@ addLayer('xp', { _id: null, get id() { return this._id ??= Object.keys(layers.xp.monsters).find(mon => layers.xp.monsters[mon] == this); }, color() { + if (inChallenge('b', 11)) return '#FF6600'; + return '#55CC11'; }, name: 'slime', position() { let i = 0; + if (inChallenge('b', 11)) i++; + return [0, i]; }, level(kills) { @@ -350,6 +642,8 @@ addLayer('xp', { let health = D.times(level_mult, 5).times(tmp.xp?.modifiers.health.mult ?? 1); + if (inChallenge('b', 11)) health = health.times(2); + return health; }, experience(level) { @@ -358,12 +652,23 @@ addLayer('xp', { let xp = D.times(l, tmp.xp.modifiers.xp.base) .times(tmp.xp.modifiers.xp.mult); + if (inChallenge('b', 11)) xp = xp.times(1.5); + if (hasChallenge('b', 11)) xp = xp.times(1.5); + return xp; }, - damage() { return tmp.xp.modifiers.damage.total; }, + damage() { + let base = tmp.xp.modifiers.damage.base; + + if (hasUpgrade('l', 31)) base = base.add(upgradeEffect('l', 31)[this.id]); + + return D.times(base, tmp.xp.modifiers.damage.mult); + }, damage_per_second() { let mult = D.dZero; + if (hasUpgrade('xp', 22)) mult = D.add(mult, upgradeEffect('xp', 22)); + return D.times(mult, tmp.xp.monsters[this.id].damage); }, lore() { @@ -389,25 +694,54 @@ addLayer('xp', { base() { let base = D.dOne; + if (hasUpgrade('l', 11)) base = base.add(upgradeEffect('l', 11)); + return base; }, mult() { let mult = D.dOne; + if (hasUpgrade('xp', 11)) mult = mult.times(upgradeEffect('xp', 11)); + if (hasUpgrade('xp', 23)) mult = mult.times(upgradeEffect('xp', 23)); + if (hasUpgrade('xp', 32)) mult = mult.times(upgradeEffect('xp', 32)); + + if (hasUpgrade('l', 21)) mult = mult.times(upgradeEffect('l', 21)); + + mult = mult.times(item_effect('slime_knife').damage); + return mult; }, - total() { return D.times(tmp.xp.modifiers.damage.base, tmp.xp.modifiers.damage.mult); }, }, xp: { base() { return D.dOne; }, mult() { let mult = D.dOne; + if (hasUpgrade('xp', 12)) mult = mult.times(upgradeEffect('xp', 12)); + if (hasUpgrade('xp', 21)) mult = mult.times(upgradeEffect('xp', 21)); + if (hasUpgrade('xp', 33)) mult = mult.times(upgradeEffect('xp', 33)); + + if (hasUpgrade('l', 12)) mult = mult.times(upgradeEffect('l', 12)); + + mult = mult.times(item_effect('slime_crystal').xp_mult); + mult = mult.times(item_effect('slime_injector').xp_mult); + + if (hasAchievement('ach', 14)) mult = mult.times(achievementEffect('ach', 14)); + return mult; }, cap() { let cap = D(1_000); + if (hasAchievement('ach', 15)) cap = cap.add(achievementEffect('ach', 15)); + + if (hasUpgrade('l', 13)) cap = cap.times(upgradeEffect('l', 13)); + if (hasUpgrade('l', 23)) cap = cap.times(upgradeEffect('l', 23)); + + cap = cap.times(tmp.l.effect); + + cap = cap.times(item_effect('slime_crystal').xp_cap); + return cap; }, gain_cap() { return D.minus(tmp.xp.modifiers.xp.cap, player.xp.points); }, @@ -416,11 +750,16 @@ addLayer('xp', { mult() { let mult = D.dOne; + if (hasUpgrade('xp', 13)) mult = mult.times(upgradeEffect('xp', 13)); + if (hasUpgrade('xp', 31)) mult = mult.div(upgradeEffect('xp', 31)); + + mult = mult.div(item_effect('slime_injector').health); + return mult; }, }, }, - monster_list() { + list() { return Object.values(tmp.xp.monsters) .filter(mon => mon.unlocked ?? true) .map(mon => mon.id); @@ -428,7 +767,7 @@ addLayer('xp', { doReset(layer) { if (tmp[layer].row <= this.row) return; - const held = item_effect('slime_pocket').hold, + const held = 0, /** @type {number[]} */ upgs = [], /** @type {(keyof Player['xp'])[]} */ diff --git a/js/layers/main/1/crafting.js b/js/layers/main/1/crafting.js new file mode 100644 index 0000000..d88c4a0 --- /dev/null +++ b/js/layers/main/1/crafting.js @@ -0,0 +1,403 @@ +'use strict'; + +addLayer('c', { + name: 'crafting', + row: 1, + position: 1, + // Allows for resets + type: 'static', + baseAmount: D.dZero, + requires: D.dOne, + symbol: 'C', + color: '#996611', + tooltip() { + if (!player.c.shown) return `Reach ${formatWhole(tmp.c.buyables[11].cost)} kills to unlock (you have ${formatWhole(tmp.xp.kill.total)} kills)`; + const sum = Object.values(player.items).reduce((sum, n) => D.add(sum, n.amount), D.dZero); + return `${formatWhole(sum)} items`; + }, + startData() { + return { + points: D.dZero, + unlocked: true, + shown: false, + visiblity: { + inventory: {}, + crafting: {}, + }, + recipes: Object.fromEntries(Object.keys(layers.c.recipes).map(id => [id, { + target: D.dOne, + making: D.dZero, + time: D.dZero, + crafted: D.dZero, + }])), + compendium: false, + }; + }, + layerShown() { return player.c.shown || hasUpgrade('xp', 33); }, + hotkeys: [ + { + key: 'C', + description: 'Shift + C: Display crafting layer', + onPress() { if (player.c.shown) showTab('c'); }, + unlocked() { return player.c.shown; }, + }, + ], + branches: ['xp'], + tabFormat: { + 'Crafting': { + content: [ + ['buyable', 11], + 'blank', + ['row', () => Object.keys(tmp.c.clickables) + .filter(id => id.startsWith('crafting_')) + .map(id => [['clickable', id], 'blank']).flat() + ], + 'blank', + ['column', () => Object.keys(layers.c.recipes).map(id => crafting_show_recipe(id))], + ], + }, + 'Inventory': { + content() { + return [ + ['display-text', `Chance multiplier: ${format(tmp.c.chance_multiplier)}`], + 'blank', + ['row', Object.keys(tmp.c.clickables) + .filter(id => id.startsWith('inventory_')) + .map(id => [['clickable', id], 'blank']).flat() + ], + 'blank', + ...inventory(), + 'blank', + ...compendium_content(player.c.compendium), + ]; + } + }, + }, + chance_multiplier() { + let mult = D.dZero; + + mult = mult.add(buyableEffect('c', 11)); + if (hasAchievement('ach', 44)) mult = mult.add(achievementEffect('ach', 44)); + + mult = mult.times(item_effect('slime_die').luck); + + return mult; + }, + buyables: { + 11: { + title() { return `Looting lv.${formatWhole(getBuyableAmount(this.layer, this.id))}`; }, + display() { + let cost = shiftDown ? '[200 * amount]' : formatWhole(tmp[this.layer].buyables[this.id].cost), + effect = shiftDown ? '[(amount - 1) / 20]' : format(buyableEffect(this.layer, this.id)); + + return `Multiplies item drop chances by ${effect}
+ First level increases effect by 1
+ Performs a loot reset

+ Requires: ${cost} kills`; + }, + cost(x) { + if (tmp[this.layer].deactivated) x = D.dZero; + + return D.add(x, 1).times(200); + }, + canAfford() { return D.gte(tmp.xp.kill.total, tmp[this.layer].buyables[this.id].cost); }, + effect(x) { + if (tmp[this.layer].deactivated) x = D.dZero; + + if (D.lte(x, 0)) return D.dZero; + + return D.div(x, 20).add(.95); + }, + buy() { + if (!this.canAfford()) return; + + addBuyables(this.layer, this.id, 1); + doReset('c', true); + player.c.shown = true; + }, + }, + }, + clickables: { + ...crafting_toggles(), + }, + crafting: { + max() { return D.dTen; }, + crafted() { return Object.values(player.c.recipes).reduce((sum, rec) => D.add(sum, rec.crafted), D.dZero); }, + speed() { + let speed = D.dOne; + + if (hasAchievement('ach', 45)) speed = speed.times(achievementEffect('ach', 45)); + + return speed; + }, + }, + recipes: { + // Materials + slime_core: { + _id: null, + get id() { return this._id ??= Object.entries(layers.c.recipes).find(([, r]) => r == this)[0]; }, + consumes(amount) { + const count = crafting_default_amount(this.id, amount); + + return [ + ['slime_goo', D.times(10, count)], + ['slime_core_shard', D.times(3, count)], + ]; + }, + produces(amount) { + const count = crafting_default_amount(this.id, amount); + + return [ + ['slime_core', count], + ]; + }, + formulas: { + consumes: { + 'slime_goo': '10 * count', + 'slime_core_shard': '3 * count', + }, + produces: { + 'slime_core': 'count', + }, + }, + categories: ['materials', 'slime',], + }, + dense_slime_core: { + _id: null, + get id() { return this._id ??= Object.entries(layers.c.recipes).find(([, r]) => r == this)[0]; }, + consumes(amount) { + const count = crafting_default_amount(this.id, amount); + + return [ + ['slime_goo', D.times(25, count)], + ['slime_core_shard', D.times(7, count)], + ['slime_core', D.times(2, count)], + ]; + }, + produces(amount) { + const count = crafting_default_amount(this.id, amount); + + return [ + ['dense_slime_core', count], + ]; + }, + duration() { + let duration = D.dTen; + + return D.div(duration, tmp.c.crafting.speed); + }, + formulas: { + consumes: { + 'slime_goo': '25 * count', + 'slime_core_shard': '7 * count', + 'slime_core': '2 * count', + }, + produces: { + 'dense_slime_core': 'count', + }, + duration: '10 seconds', + }, + categories: ['materials', 'slime',], + }, + // Equipment + slime_crystal: { + _id: null, + get id() { return this._id ??= Object.entries(layers.c.recipes).find(([, r]) => r == this)[0]; }, + consumes(amount, all_time) { + const count = crafting_default_amount(this.id, amount), + all = crafting_default_all_time(this.id, all_time); + + return [ + ['slime_goo', D.sumGeometricSeries(count, 15, 1.8, all)], + ['slime_core', D.sumGeometricSeries(count, 1, 1.2, all)], + ]; + }, + produces(amount) { + const count = crafting_default_amount(this.id, amount); + + return [ + ['slime_crystal', count], + ]; + }, + duration(amount, all_time) { + const count = crafting_default_amount(this.id, amount), + all = crafting_default_all_time(this.id, all_time); + + let duration = D.times(count, 2).add(all).times(5).add(15); + + return D.div(duration, tmp.c.crafting.speed); + }, + formulas: { + consumes: { + 'slime_goo': '15 * 1.8 ^ amount', + 'slime_core': '1.2 ^ amount', + }, + produces: { + 'slime_crystal': 'amount', + }, + duration: '(crafting * 2 + crafted) * 5 + 30 seconds', + }, + categories: ['equipment', 'slime',], + static: true, + }, + slime_knife: { + _id: null, + get id() { return this._id ??= Object.entries(layers.c.recipes).find(([, r]) => r == this)[0]; }, + consumes(amount, all_time) { + const count = crafting_default_amount(this.id, amount), + all = crafting_default_all_time(this.id, all_time); + + return [ + ['slime_goo', D.sumGeometricSeries(count, 5, 1.8, all)], + ['slime_core_shard', D.sumGeometricSeries(count, 10, 1.4, all)], + ]; + }, + produces(amount) { + const count = crafting_default_amount(this.id, amount); + + return [ + ['slime_knife', count], + ]; + }, + duration(amount, all_time) { + const count = crafting_default_amount(this.id, amount), + all = crafting_default_all_time(this.id, all_time); + + let duration = D.times(count, 2).add(all).times(15); + + return D.div(duration, tmp.c.crafting.speed); + }, + formulas: { + consumes: { + 'slime_goo': '5 * 1.8 ^ amount', + 'slime_core_shard': '10 * 1.4 ^ amount', + }, + produces: { + 'slime_knife': 'amount', + }, + duration: '(crafting * 2 + crafted) * 25 seconds', + }, + categories: ['equipment', 'slime',], + static: true, + }, + slime_injector: { + _id: null, + get id() { return this._id ??= Object.entries(layers.c.recipes).find(([, r]) => r == this)[0]; }, + consumes(amount, all_time) { + const count = crafting_default_amount(this.id, amount), + all = crafting_default_all_time(this.id, all_time); + + return [ + ['slime_goo', D.sumGeometricSeries(count, 25, 1.8, all)], + ['slime_core', D.sumGeometricSeries(count, 2, 1.2, all)], + ['dense_slime_core', D.sumGeometricSeries(count, 1, 1.1, all)], + ]; + }, + produces(amount) { + const count = crafting_default_amount(this.id, amount); + + return [ + ['slime_injector', count], + ]; + }, + duration(amount, all_time) { + const count = crafting_default_amount(this.id, amount), + all = crafting_default_all_time(this.id, all_time); + + let duration = D.times(count, 2).add(all).times(7.5).add(15); + + return D.div(duration, tmp.c.crafting.speed); + }, + formulas: { + consumes: { + 'slime_goo': '25 * 1.8 ^ amount', + 'slime_core': '2 * 1.2 ^ amount', + 'dense_slime_core': '1.1 ^ amount', + }, + produces: { + 'slime_injector': 'amount', + }, + duration: '(crafting * 2 + crafted) * 7.5 + 15 seconds', + }, + categories: ['equipment', 'slime',], + static: true, + }, + slime_die: { + _id: null, + get id() { return this._id ??= Object.entries(layers.c.recipes).find(([, r]) => r == this)[0]; }, + consumes(amount, all_time) { + const count = crafting_default_amount(this.id, amount), + all = crafting_default_all_time(this.id, all_time); + + return [ + ['slime_goo', D.sumGeometricSeries(count, 50, 1.8, all)], + ['slime_core_shard', D.sumGeometricSeries(count, 25, 1.4, all)], + ['dense_slime_core', D.sumGeometricSeries(count, 1, 1.1, all)], + ]; + }, + produces(amount) { + const count = crafting_default_amount(this.id, amount); + + return [ + ['slime_die', count], + ]; + }, + duration(amount, all_time) { + const count = crafting_default_amount(this.id, amount), + all = crafting_default_all_time(this.id, all_time); + + let duration = D.times(count, 4).add(all).times(5).add(20); + + return D.div(duration, tmp.c.crafting.speed); + }, + formulas: { + consumes: { + 'slime_goo': '50 * 1.8 ^ amount', + 'slime_core_shard': '25 * 1.4 ^ amount', + 'dense_slime_core': '1.1 ^ amount', + }, + produces: { + 'slime_crystal': 'amount', + }, + duration: '(crafting * 4 + crafted) * 5 + 20 seconds', + }, + categories: ['equipment', 'slime',], + static: true, + }, + }, + doReset(layer) { + if (tmp[layer].row <= this.row) return; + + /** @type {(keyof Player['c'])[]} */ + const keep = ['shown', 'compendium']; + + layerDataReset(this.layer, keep); + }, + update(diff) { + Object.entries(player.c.recipes).forEach(([id, rec]) => { + if (D.gt(rec.making, 0) && D.lt(rec.time, tmp.c.recipes[id].duration)) { + rec.time = D.add(rec.time, diff); + } + }); + }, + automate() { + Object.entries(player.c.recipes).forEach(([id, rec]) => { + if (D.gt(rec.making, 0) && D.gte(rec.time, tmp.c.recipes[id].duration)) { + gain_items(tmp.c.recipes[id].produces); + rec.time = D.dZero; + rec.making = D.dZero; + } + }); + }, + shouldNotify() { + return canBuyBuyable('c', 11) || + Object.values(tmp.c.recipes).filter(rec => (rec.unlocked ?? true) && + rec.categories.includes('equipment') && crafting_can(rec.id, D.dOne)).length; + }, + nodeStyle: { + 'backgroundColor'() { + if (!player.c.shown && !canBuyBuyable('c', 11)) return colors[options.theme].locked; + return tmp.c.color; + }, + }, +}); diff --git a/js/layers/main/1/level.js b/js/layers/main/1/level.js new file mode 100644 index 0000000..a40c05d --- /dev/null +++ b/js/layers/main/1/level.js @@ -0,0 +1,311 @@ +'use strict'; + +addLayer('l', { + name: 'level', + startData() { + return { + points: D.dZero, + unlocked: false, + }; + }, + color: '#0066CC', + row: 1, + resource: 'level', + effect() { + let base = D.dTwo, + levels = player.l.points; + + if (hasUpgrade('l', 33)) base = base.add(upgradeEffect('l', 33)); + + if (hasAchievement('ach', 35)) levels = levels.add(achievementEffect('ach', 35)); + + return D.pow(base, player.l.points); + }, + layerShown() { return player.l.unlocked || hasUpgrade('xp', 33); }, + tooltip() { return `${formatWhole(player.l.points)} levels
${formatWhole(tmp.l.skill_points.remaining)} skill points`; }, + hotkeys: [ + { + key: 'L', + description: 'Shift + L: Display level layer', + onPress() { if (player.l.unlocked) showTab('l'); }, + unlocked() { return player.l.unlocked; }, + }, + { + key: 'l', + description: 'L: Reset for levels', + onPress() { if (player.l.unlocked) doReset('l'); }, + unlocked() { return player.l.unlocked; }, + }, + ], + tabFormat: { + 'Levels': { + content: [ + ['display-text', () => { + let effect; + if (shiftDown) { + let base = D.dTwo; + + if (hasUpgrade('l', 33)) base = base.add(upgradeEffect('l', 33)); + + effect = `[${formatWhole(base)} ^ levels]`; + } else effect = formatWhole(tmp.l.effect); + + return `You have ${resourceColor(tmp.l.color, formatWhole(player.l.points), 'font-size:1.5em;')} levels,\ + which multiply the XP cap by ${resourceColor(tmp.l.color, effect)}`; + }], + 'blank', + ['row', [ + ['bar', 'progress'], + 'blank', + 'prestige-button' + ]], + 'blank', + ['display-text', () => { + const sp = tmp.l.skill_points; + + return `You have ${resourceColor(sp.color, formatWhole(sp.remaining), 'font-size:1.5em;')}\ + /${resourceColor(sp.color, formatWhole(sp.total))} skill points`; + }], + 'blank', + 'respec-button', + ['upgrade-tree', [ + [11, 12, 13], + [21, 22, 23], + [31, 32, 33], + ]], + ], + }, + }, + buyables: { + respec() { + player.l.upgrades.length = 0; + doReset('l', true); + }, + showRespec: true, + respecMessage: 'Are you sure you want to respec skill upgrades?\nThis will perform a level reset', + respecText: 'Respec skill upgrades', + }, + bars: { + progress: { + direction: RIGHT, + height: 40, + width: 320, + progress() { return D.div(tmp.l.baseAmount, getNextAt('l', true, 'static')); }, + display() { return `${formatWhole(tmp.l.baseAmount)} / ${formatWhole(getNextAt('l', true, 'static'))} experience`; }, + fillStyle: { + 'backgroundColor'() { return tmp.l.color; }, + }, + baseStyle: { 'border-radius': 0, }, + borderStyle: { 'border-radius': 0, }, + }, + }, + upgrades: { + 11: { + title: 'Simple Stab', + description: 'Deal +1 base damage', + effect() { return D.dOne; }, + effectDisplay() { return `+${formatWhole(upgradeEffect(this.layer, this.id))}`; }, + style() { + let style = {}; + + if (!hasUpgrade(this.layer, this.id) && canAffordUpgrade(this.layer, this.id)) style['backgroundColor'] = tmp.l.skill_points.color; + + return style; + }, + cost: D.dOne, + currencyDisplayName: 'skill points', + canAfford() { return D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + }, + 12: { + title: 'Success Knowledge', + description: 'Gain +25% experience', + effect() { return D(1.25); }, + effectDisplay() { return `*${format(upgradeEffect(this.layer, this.id))}`; }, + style() { + let style = {}; + + if (!hasUpgrade(this.layer, this.id) && canAffordUpgrade(this.layer, this.id)) style['backgroundColor'] = tmp.l.skill_points.color; + + return style; + }, + cost: D.dOne, + currencyDisplayName: 'skill points', + canAfford() { return D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + }, + 13: { + title: 'Higher Limit', + description: 'Experience cap +25%', + effect() { return D(1.25); }, + effectDisplay() { return `*${format(upgradeEffect(this.layer, this.id))}`; }, + style() { + let style = {}; + + if (!hasUpgrade(this.layer, this.id) && canAffordUpgrade(this.layer, this.id)) style['backgroundColor'] = tmp.l.skill_points.color; + + return style; + }, + cost: D.dOne, + currencyDisplayName: 'skill points', + canAfford() { return D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + }, + 21: { + title: 'Spin Blade', + description: 'Double damage dealt', + effect() { return D.dTwo; }, + effectDisplay() { return `*${formatWhole(upgradeEffect(this.layer, this.id))}`; }, + style() { + let style = {}; + + if (!hasUpgrade(this.layer, this.id) && canAffordUpgrade(this.layer, this.id)) style['backgroundColor'] = tmp.l.skill_points.color; + + return style; + }, + cost: D.dTwo, + currencyDisplayName: 'skill points', + canAfford() { return this.branches.every(id => hasUpgrade('l', id)) && D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + branches: [11, 12], + }, + 22: { + title: 'Unhealthy Levels', + description: 'Health multipliers affect level', + effect() { return tmp.xp.modifiers.health.mult; }, + effectDisplay() { return `/${format(upgradeEffect(this.layer, this.id).pow(-1))}`; }, + style() { + let style = {}; + + if (!hasUpgrade(this.layer, this.id) && canAffordUpgrade(this.layer, this.id)) style['backgroundColor'] = tmp.l.skill_points.color; + + return style; + }, + cost: D.dTwo, + currencyDisplayName: 'skill points', + canAfford() { return this.branches.every(id => hasUpgrade('l', id)) && D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + branches: [12], + }, + 23: { + title: 'Dynamic Boundary', + description() { + let text = 'Experience boosts experience cap'; + + if (shiftDown) text += '
Formula: log10(experience + 10)'; + + return text; + }, + effect() { return D.add(player.xp.points, 10).log10(); }, + effectDisplay() { return `*${format(upgradeEffect(this.layer, this.id))}`; }, + style() { + let style = {}; + + if (!hasUpgrade(this.layer, this.id) && canAffordUpgrade(this.layer, this.id)) style['backgroundColor'] = tmp.l.skill_points.color; + + return style; + }, + cost: D.dTwo, + currencyDisplayName: 'skill points', + canAfford() { return this.branches.every(id => hasUpgrade('l', id)) && D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + branches: [12, 13], + }, + 31: { + title: 'Lifesteal', + description() { + let text = 'Enemy max health boosts damage'; + + if (shiftDown) text += '
Formula: log10(max health + 1)'; + + return text; + }, + effect() { + return Object.fromEntries( + Object.values(tmp.xp.monsters) + .map(mon => [mon.id, D.add(mon.health, 1).log10()]) + ); + }, + effectDisplay() { return `+${format(upgradeEffect(this.layer, this.id)[player.xp.selected])}`; }, + cost: D(3), + currencyDisplayName: 'skill points', + canAfford() { return this.branches.every(id => hasUpgrade('l', id)) && D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + branches: [21], + }, + 32: { + title: 'Level Down', + description() { + let text = 'Total enemy levels divide level cost'; + + if (shiftDown) text += '
Formula: 2√(∑(levels - 1) + 1)'; + + return text; + }, + effect() { + return Object.values(tmp.xp.monsters).map(mon => (mon.unlocked ?? true) ? D.minus(mon.level, 1) : D.dZero) + .reduce((sum, level) => D.add(sum, level), D.dZero).add(1).root(2); + }, + effectDisplay() { return `/${format(upgradeEffect(this.layer, this.id))}`; }, + cost: D(3), + currencyDisplayName: 'skill points', + canAfford() { return this.branches.every(id => hasUpgrade('l', id)) && D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + branches: [22], + }, + 33: { + title: 'Extra Base', + description: 'Level effect base +1', + effect() { return D.dOne; }, + effectDisplay() { return `+${formatWhole(upgradeEffect(this.layer, this.id))}`; }, + cost: D(3), + currencyDisplayName: 'skill points', + canAfford() { return this.branches.every(id => hasUpgrade('l', id)) && D.gte(tmp.l.skill_points.remaining, tmp[this.layer].upgrades[this.id].cost); }, + pay() { }, + branches: [22, 23], + }, + }, + type: 'static', + baseResource: 'experience', + baseAmount() { return player.xp.points; }, + requires: D(750), + exponent: D.dTwo, + base: D(1.75), + roundUpCost: true, + canBuyMax: true, + symbol: 'L', + position: 0, + branches: ['xp'], + skill_points: { + color: '#00CCCC', + total() { + let points = player.l.points; + + if (hasAchievement('ach', 34)) points = points.add(achievementEffect('ach', 34)); + + return points.floor(); + }, + remaining() { + const spent = player.l.upgrades.map(id => { + const upg = tmp.l.upgrades[id]; + if (upg.currencyDisplayName == 'skill points') return upg.cost; + return D.dZero; + }).reduce((sum, cost) => D.add(sum, cost), D.dZero); + + return D.minus(this.total(), spent); + }, + }, + onPrestige(gain) { + if (D.gt(gain, 1)) giveAchievement('ach', 22); + }, + gainMult() { + let mult = D.dOne; + + if (hasUpgrade('l', 22)) mult = mult.times(upgradeEffect('l', 22)); + if (hasUpgrade('l', 32)) mult = mult.div(upgradeEffect('l', 32)); + + mult = mult.div(item_effect('slime_injector').level); + + return mult; + }, +}); diff --git a/js/layers/main/2/boss.js b/js/layers/main/2/boss.js new file mode 100644 index 0000000..a5d6c4c --- /dev/null +++ b/js/layers/main/2/boss.js @@ -0,0 +1,110 @@ +'use strict'; + +addLayer('b', { + row: 2, + position: 0, + // Allows for resets + type: 'static', + baseAmount: D.dZero, + requires: D.dOne, + resource: 'boss', + name: 'boss', + symbol: 'B', + color: '#CC5555', + tooltip() { + if (!player.b.shown) return `Reach ${formatWhole(500)} kills to fight (you have ${formatWhole(tmp.xp.kill.total)} kills)`; + return `${formatWhole(tmp.b.complete)} bosses beaten`; + }, + startData() { + return { + unlocked: true, + shown: false, + points: D.dZero, + }; + }, + layerShown() { return player.b.shown || D.gte(tmp.xp.kill.total, 250); }, + hotkeys: [ + { + key: 'B', + description: 'Shift + B: Display boss layer', + onPress() { if (player.b.shown) showTab('b'); }, + unlocked() { return player.b.shown; }, + }, + { + key: 'b', + description: 'B: Complete boss challenge (if possible)', + onPress() { + if (player.b.shown) { + const chal = activeChallenge('b'); + if (!chal || !canCompleteChallenge('b', chal)) return; + completeChallenge('b', chal); + } + }, + unlocked() { return player.b.shown; }, + }, + ], + tabFormat: { + 'Bosses': { + content: [ + ['display-text', () => { + return `You have beaten ${resourceColor(tmp.b.color, formatWhole(tmp.b.complete), 'font-size:1.5em;')} bosses`; + }], + 'blank', + ['bar', 'progress'], + 'blank', + ['challenge', 11], + ], + }, + }, + bars: { + progress: { + direction: RIGHT, + height: 40, + width: 320, + progress() { + const chal = activeChallenge('b'); + if (chal && inChallenge('b', chal)) return layers.b.challenges[chal].progress(); + + if (!tmp.b.challenges[11].unlocked) { + return D.div(tmp.xp.kill.total, 500); + } + return D.dZero; + }, + display() { + const chal = activeChallenge('b'); + if (chal && inChallenge('b', chal)) return layers.b.challenges[chal].display(); + + if (!tmp.b.challenges[11].unlocked) { + return `${formatWhole(tmp.xp.kill.total)} / ${formatWhole(500)} kills`; + } + return 'No bosses left'; + }, + fillStyle: { + 'backgroundColor'() { return tmp.b.color; }, + }, + }, + }, + challenges: { + 11: { + name: 'Slime King', + challengeDescription: `Fight the Slime King and anger the slimes
\ + Double slime health and experience, slime items effects are boosted.`, + rewardDescription: `+50% slime experience, slime items effects boost is kept, unlock a new enemy, and XP upgrades stay unlocked`, + goalDescription: 'Kill 500 slimes', + canComplete() { return D.gte(tmp.xp.kill.total, 500); }, + progress() { return D.div(tmp.xp.kill.total, 500); }, + display() { return `${formatWhole(tmp.xp.kill.total)} / ${formatWhole(500)} kills`; }, + unlocked() { return hasChallenge('b', 11) || inChallenge('b', 11) || D.gte(tmp.xp.kill.total, 500) || player.b.shown; }, + onEnter() { player.b.shown = true; }, + color() { return tmp.b.color; }, + }, + }, + complete() { return Object.values(player.b.challenges).reduce((sum, n) => D.add(sum, n), D.dZero); }, + branches: ['l'], + nodeStyle: { + 'backgroundColor'() { + if (!player.b.shown && D.lt(tmp.xp.kill.total, 500)) return colors[options.theme].locked; + return tmp.b.color; + }, + }, +}); diff --git a/js/layers/main/side/achievement.js b/js/layers/main/side/achievement.js index 7ef644b..8e1eb32 100644 --- a/js/layers/main/side/achievement.js +++ b/js/layers/main/side/achievement.js @@ -21,7 +21,7 @@ addLayer('ach', { owned = tmp.ach.categories.normal.owned.length, visible = tmp.ach.categories.normal.visible.length; - return `You have ${resourceColor(color, formatWhole(owned), 'font-size:1.5em;')} / ${resourceColor(color, formatWhole(visible))} achievements`; + return `You have ${resourceColor(color, formatWhole(owned), 'font-size:1.5em;')} /${resourceColor(color, formatWhole(visible))} achievements`; }], 'blank', ['achievements', () => tmp.ach.categories.normal.rows], @@ -34,7 +34,7 @@ addLayer('ach', { owned = tmp.ach.categories.secret.owned.length, visible = tmp.ach.categories.secret.visible.length; - return `You have ${resourceColor(color, formatWhole(owned), 'font-size:1.5em;')} / ${resourceColor(color, formatWhole(visible))} secrets`; + return `You have ${resourceColor(color, formatWhole(owned), 'font-size:1.5em;')} secrets`; }], 'blank', ['achievements', () => tmp.ach.categories.secret.rows], @@ -45,14 +45,289 @@ addLayer('ach', { }, achievements: { //#region Normal + 11: { + name: 'Innocence Mangled', + tooltip: 'Kill a slime', + done() { return D.gte(player.xp.monsters.slime.kills, 1); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.xp.monsters.slime.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.xp.monsters.slime.color; + + return style; + }, + }, + 12: { + name: 'Power Up (Not Yours)', + tooltip: 'Fight a level 2 enemy', + done() { return D.gte(tmp.xp.monsters.slime.level, 2); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.xp.monsters.slime.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.xp.monsters.slime.color; + + return style; + }, + }, + 13: { + name: 'I Can Wait All Day', + tooltip: 'Buy a trap', + done() { return hasUpgrade('xp', 22); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.xp.monsters.slime.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.xp.monsters.slime.color; + + return style; + }, + }, + 14: { + name: 'Half Off', + tooltip: 'Halve an enemy\'s health
Reward: +10% experience gain', + done() { return D.lte(tmp.xp.modifiers.health.mult, .5); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.xp.monsters.slime.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.xp.monsters.slime.color; + style['border'] = `solid 3px ${tmp.ach.color}`; + + return style; + }, + effect() { return D(1.1); }, + }, + 15: { + name: 'Unhardcapped', + tooltip() { return `Get more than ${formatWhole(1000)} experience
Reward: +500 experience cap`; }, + done() { return D.gt(player.xp.points, 1000); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.xp.monsters.slime.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.xp.monsters.slime.color; + style['border'] = `solid 3px ${tmp.ach.color}`; + + return style; + }, + effect() { return D(500); }, + }, + 31: { + name: 'Upwards', + tooltip: 'Level up', + done() { return D.gte(player.l.points, 1); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.l.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.l.color; + + return style; + }, + unlocked() { return player.l.unlocked; }, + }, + 32: { + name: 'Skilled', + tooltip: 'Buy 3 skills', + done() { return D.gte(player.l.upgrades.length, 3); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.l.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.l.color; + + return style; + }, + unlocked() { return player.l.unlocked; }, + }, + 33: { + name: 'More Damage', + tooltip: 'Buy all damage skills', + done() { return [11, 21, 31].every(id => hasUpgrade('l', id)); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.l.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.l.color; + + return style; + }, + unlocked() { return player.l.unlocked; }, + }, + 34: { + name: 'Advanced Skills', + tooltip: 'Buy 5 skills
Reward: +1 skill point', + done() { return D.gte(player.l.upgrades.length, 5); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.l.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.l.color; + style['border'] = `solid 3px ${tmp.ach.color}`; + + return style; + }, + effect() { return D.dOne; }, + unlocked() { return player.l.unlocked; }, + }, + 35: { + name: 'Ultracap', + tooltip: 'Buy 3 skill upgrades that affect the experience cap
Reward: +1 effective level', + done() { return [13, 23, 33].every(id => hasUpgrade('l', id)); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.l.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.l.color; + style['border'] = `solid 3px ${tmp.ach.color}`; + + return style; + }, + effect() { return D.dOne; }, + unlocked() { return player.l.unlocked; }, + }, + 41: { + name: 'Enchanting Table', + tooltip: 'Get a level of looting', + done() { return D.gte(getBuyableAmount('c', 11), 1); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.c.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.c.color; + + return style; + }, + unlocked() { return player.c.shown; }, + }, + 42: { + name: 'The Hard Way', + tooltip: 'Craft a slime core', + done() { return D.gte(player.c.recipes.slime_core.crafted, 1); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.c.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.c.color; + + return style; + }, + unlocked() { return player.c.shown; }, + }, + 43: { + name: 'Armory', + tooltip: 'Obtain a slime knife', + done() { return D.gte(player.items.slime_knife.amount, 1); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.c.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.c.color; + + return style; + }, + unlocked() { return player.c.shown; }, + }, + 44: { + name: 'The Easy(?) Way', + tooltip: 'Loot a slime core
Reward: +0.1 luck', + done() { return player.xp.monsters.slime.last_drops.some(([item]) => item == 'slime_core'); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.c.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.c.color; + style['border'] = `solid 3px ${tmp.ach.color}`; + + return style; + }, + effect() { return D(.1); }, + unlocked() { return player.c.shown; }, + }, + 45: { + name: 'Diminishing Returns', + tooltip: 'Get slime injector\'s negative effect
Reward: slime injector\'s primary effect is applied to crafting speed', + done() { return D.neq(item_effect('slime_injector').xp_mult, 1); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.c.color; + style['border'] = `solid 3px ${tmp.ach.color}`; + + return style; + }, + effect() { return item_effect('slime_injector').health; }, + unlocked() { return player.c.shown; }, + }, + 51: { + name: 'Finally, A Boss Fight', + tooltip: 'Enter a boss fight
Reward: The bestiary stays unlocked', + done() { return activeChallenge('b') !== false; }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Achievement Completed!', 3, tmp.b.color); }, + style() { + let style = {}; + + if (hasAchievement(this.layer, this.id)) style['background-color'] = tmp.b.color; + style['border'] = `solid 3px ${tmp.ach.color}`; + + return style; + }, + unlocked() { return player.b.shown; }, + }, //#endregion Normal //#region Secret + 21: { + name: 'Overkill', + tooltip: 'Deal more damage than the enemy\'s health in one attack', + done() { return Object.values(tmp.xp.monsters).some(mon => D.gte(mon.damage, mon.health)); }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Secret Completed!', 3, tmp.ach.categories.secret.color); }, + style() { + let style = {}; + + style['background-color'] = tmp.xp.monsters.slime.color; + style['border'] = `solid 3px ${tmp.ach.categories.secret.color}`; + + return style; + }, + unlocked() { return hasAchievement(this.layer, this.id); }, + }, + 22: { + name: 'Skip', + tooltip: 'Get more than 1 level at once', + done() { return false; }, + onComplete() { doPopup('achievement', tmp[this.layer].achievements[this.id].name, 'Secret Completed!', 3, tmp.ach.categories.secret.color); }, + style() { + let style = {}; + + style['background-color'] = tmp.l.color; + style['border'] = `solid 3px ${tmp.ach.categories.secret.color}`; + + return style; + }, + unlocked() { return hasAchievement(this.layer, this.id); }, + }, + 23: { + name: 'Disenchanted', + tooltip: 'Get an item without looting', + done() { return D.eq(getBuyableAmount('c', 11), 0) && Object.values(player.items).some(it => D.gt(it.amount, 0)); }, + style() { + let style = {}; + + style['background-color'] = tmp.c.color; + style['border'] = `solid 3px ${tmp.ach.categories.secret.color}`; + + return style; + }, + unlocked() { return hasAchievement(this.layer, this.id); }, + }, //#endregion Secret }, achievementPopups: false, // This is done manually categories: { normal: { - rows: [], + rows: [1, 3, 4, 5], color() { return tmp.ach.color; }, visible() { return Object.values(tmp.ach.achievements) @@ -61,7 +336,7 @@ addLayer('ach', { owned() { return player.ach.achievements.filter(id => this.rows.includes(Math.floor(id / 10))); }, }, secret: { - rows: [], + rows: [2], color: '#FF0077', visible() { return Object.values(tmp.ach.achievements) diff --git a/js/mod.js b/js/mod.js index 6f9383b..45670e4 100644 --- a/js/mod.js +++ b/js/mod.js @@ -31,6 +31,8 @@ let modInfo = { // main 'layers/main/side/achievement.js', 'layers/main/0/experience.js', + 'layers/main/1/level.js', 'layers/main/1/crafting.js', + 'layers/main/2/boss.js', ], /** * If you have a Discord server or other discussion place, you can add a link to it. @@ -123,6 +125,14 @@ function addedPlayerData() { var displayThings = [ () => VERSION.beta ? 'Beta version, things might be a bit unstable' : '', () => isEndgame() ? 'You are past endgame. Content may not be balanced.' : '', + () => { + const chal = activeChallenge('b'); + if (!chal) return ''; + + const chaltemp = tmp.b.challenges[chal]; + + return `You are in ${tmp.b.name} challenge ${resourceColor(chaltemp.color, chaltemp.name)}`; + }, ]; /** @@ -131,7 +141,7 @@ var displayThings = [ * @returns {Boolean} */ function isEndgame() { - return false; + return inChallenge('b', 11); } diff --git a/js/moreutils.js b/js/moreutils.js index 0a50e4b..5992ed3 100644 --- a/js/moreutils.js +++ b/js/moreutils.js @@ -232,6 +232,25 @@ function rgb_opposite_bw(color) { if (sum > .5) return '#000000'; return '#FFFFFF'; } +/** + * Given a color, returns its negative + * + * @param {string} color + */ +function rgb_negative(color) { + return `#${rgb_split(color).map(n => (255 - n).toString(16).padStart(2, '0')).join('')}`; +} +/** + * Given a color, returns its grayscale + * + * @param {string} color + */ +function rgb_grayscale(color) { + const sum = rgb_split(color).map(n => n / 256 / 3) + .reduce((a, b) => a + b, 0) * 256; + + return `#${Math.floor(sum).toString(16).padStart(2, '0').repeat(3)}`; +} /** * Splits a hex color into its red, green, and blue values * @@ -239,6 +258,7 @@ function rgb_opposite_bw(color) { * @returns {[number, number, number]} */ function rgb_split(color) { + if (!color || typeof color != 'string') return [0, 0, 0]; return Array.from({ length: 3 }, (_, i) => parseInt(color.slice(i * 2 + 1, i * 2 + 3), 16)); } /** @@ -278,7 +298,7 @@ function bestiary_content(monster) { ], ['display-text', capitalize(tmonst.name)], 'blank', - ['display-text', `Level ${formatWhole(tmonst.level)}`], + ['display-text', `Level ${resourceColor(tmp.l.color, formatWhole(tmonst.level))}`], ['display-text', `Killed ${resourceColor(tmp.xp.kill.color, formatWhole(player.xp.monsters[monster].kills))} times`], ['display-text', `Gives ${resourceColor(tmp.xp.color, format(tmonst.experience))} XP on kill`], 'blank', @@ -288,10 +308,24 @@ function bestiary_content(monster) { /** @type {TabFormatEntries<'xp'>[]} */ const upgrade_lines = []; - if (hasUpgrade('xp', 32)) upgrade_lines.push([ + + if (hasUpgrade('l', 31)) upgrade_lines.push([ 'display-text', - `${resourceColor(tmp.xp.color, capitalize(tmp.xp.upgrades[32].title))} effect: *${format(upgradeEffect('xp', 32)[monster])} XP` + `${resourceColor(tmp.l.skill_points.color, tmp.l.upgrades[31].title)} effect: +${format(upgradeEffect('l', 31)[monster])} damage`, ]); + // Monster specific upgrades + switch (monster) { + case 'slime': { + if (inChallenge('b', 11)) upgrade_lines.push([ + 'display-text', + `${resourceColor(tmp.b.color, tmp.b.challenges[11].name)} active effect: *${formatWhole(2)} health, *${format(1.5)} experience`, + ]); + if (hasChallenge('b', 11)) upgrade_lines.push([ + 'display-text', + `${resourceColor(tmp.b.color, tmp.b.challenges[11].name)} reward effect: *${format(1.5)} experience`, + ]); + }; break; + } if (upgrade_lines.length > 0) lines.push(...upgrade_lines, 'blank'); @@ -300,6 +334,370 @@ function bestiary_content(monster) { ['display-text', tmonst.lore], ); + if (tmp.c.chance_multiplier.gt(0)) { + const drops = source_drops(`kill:${monster}`); + lines.push( + 'blank', + ['display-text', 'Chance to drop:'], + ['row', Object.entries(drops.chances).map(/**@param{[items,Decimal]}*/([item, chance]) => { + const tile = item_tile(item); + tile.text = `${capitalize(tmp.items[item].name)}
${format_chance(chance)}`; + + return ['tile', tile]; + })], + ); + } + return lines; } +/** + * Returns a map of toggles for the crafting layer + * + * @returns {{[key: `${'inventory'|'crafting'}_${categories}`]: Clickable<'c'>}} + */ +function crafting_toggles() { + /** @type {categories[]} */ + const list = ['materials', 'equipment', 'slime']; + + return Object.fromEntries(list.map(/**@returns{[string, Clickable<'c'>][]}*/cat => { + return [ + [`inventory_${cat}`, { + canClick() { return true; }, + onClick() { + const vis = player.c.visiblity; + + vis.inventory[cat] = { + 'show': 'hide', + 'hide': 'ignore', + 'ignore': 'show', + }[vis.inventory[cat] ??= 'ignore']; + }, + display() { + const vis = player.c.visiblity, + name = { + 'materials': 'Materials', + 'equipment': 'Equipment', + 'slime': 'Slime', + }[cat]; + + let visibility = { + 'show': 'Shown', + 'hide': 'Hidden', + 'ignore': 'Ignored', + }[vis.inventory[cat] ??= 'ignore']; + + return `${name}
\ + ${visibility}`; + }, + style: { + 'backgroundColor'() { + const vis = player.c.visiblity, + color = { + 'materials': tmp.c.color, + 'equipment': tmp.c.color, + 'slime': tmp.xp.monsters.slime.color, + }[cat]; + + switch (vis.inventory[cat]) { + case 'show': + return color; + case 'hide': + return rgb_negative(color); + case 'ignore': + return rgb_grayscale(color); + } + }, + }, + }], + [`crafting_${cat}`, { + canClick() { return true; }, + onClick() { + const vis = player.c.visiblity; + + vis.crafting[cat] = { + 'show': 'hide', + 'hide': 'ignore', + 'ignore': 'show', + }[vis.crafting[cat] ??= 'ignore']; + }, + display() { + const vis = player.c.visiblity, + name = { + 'materials': 'Materials', + 'equipment': 'Equipment', + 'slime': 'Slime', + }[cat]; + + let visibility = { + 'show': 'Shown', + 'hide': 'Hidden', + 'ignore': 'Ignored', + }[vis.crafting[cat] ??= 'ignore']; + + return `${name}
\ + ${visibility}`; + }, + style: { + 'backgroundColor'() { + const vis = player.c.visiblity, + color = { + 'materials': tmp.c.color, + 'equipment': tmp.c.color, + 'slime': tmp.xp.monsters.slime.color, + }[cat]; + + switch (vis.crafting[cat]) { + case 'show': + return color; + case 'hide': + return rgb_negative(color); + case 'ignore': + return rgb_grayscale(color); + } + }, + }, + }], + ]; + }).flat()); +} +/** + * Returns the display for the inventory + * + * @returns {TabFormatEntries<'c'>[]} + */ +function inventory() { + const vis = player.c.visiblity.inventory, + /** @type {{[row: number]: items[]}} */ + grid = {}; + + Object.values(tmp.items) + .filter(item => (item.unlocked ?? true) && + ('grid' in item) && + ( + item.categories.some(cat => vis[cat] == 'show') || + !item.categories.some(cat => vis[cat] == 'hide') + )) + .forEach(item => { + const [row, col] = item.grid; + + (grid[row] ??= [])[col] = item.id; + }); + + return Object.values(grid).map(items => ['row', items.map(item => { + const tile = item_tile(item), + itemp = tmp.items[item]; + let tooltip = ''; + + if ('effectDescription' in itemp) tooltip += itemp.effectDescription() + '
'; + if ('sources' in itemp) { + const { + chance = {}, + per_second = {}, + other = [], + } = itemp.sources, + tooltip_lines = []; + + if (Object.entries(chance).filter(([, c]) => D.gt(c, 0)).length) tooltip_lines.push(...Object.entries(chance) + .map(/**@param{[string,Decimal]}*/([source, chance]) => + `${capitalize(source_name(source))}: ${format_chance(chance)}`)); + if (Object.entries(per_second).filter(([, ps]) => D.gt(ps, 0)).length) tooltip_lines.push(...Object.entries(per_second) + .map(/**@param{[string,Decimal]}*/([source, amount]) => + `${capitalize(source_name(source))}: +${format(amount)} /s`)); + if (other.length) tooltip_lines.push(...other.map(source => capitalize(source_name(source)))); + + tooltip += tooltip_lines.join('
'); + } + + if (!tooltip.length) tooltip = 'No sources'; + + tile.text += `
${formatWhole(player.items[item].amount)}`; + tile.tooltip = tooltip; + tile.onClick = () => { + if (player.c.compendium == item) player.c.compendium = false; + player.c.compendium = item; + }; + + return ['tile', tile]; + })]); +} +/** + * Returns the display for a crafting recipe + * + * @param {string} recipe + * @returns {TabFormatEntries<'c'>[]} + */ +function crafting_show_recipe(recipe) { + const precipe = player.c.recipes[recipe], + trecipe = tmp.c.recipes[recipe], + vis = player.c.visiblity.crafting; + + if (!(trecipe.unlocked ?? true) || ( + trecipe.categories.some(cat => vis[cat] == 'hide') && + !trecipe.categories.some(cat => vis[cat] == 'show') + )) return []; + + const craft = D.gt(precipe.making, 0) ? `
Crafting ${formatWhole(precipe.making)}` : '', + total = trecipe.static ? `
Crafted ${formatWhole(precipe.crafted)}` : '', + /** @template T @type {(list: T[], size?: number) => T[][]} */ + square = (list, size) => { + size ??= Math.ceil(Math.sqrt(list.length)); + if (size <= 0) return []; + + /** @type {T[][]} */ + const sq = []; + let x = 0; + + list.forEach(val => { + if (x >= sq.length) sq.push([val]); + else { + sq[x].push(val); + if (sq[x].length >= size) x++; + } + }); + + return sq; + }, + /** @type {(list: ReturnType>) => TabFormatEntries<'c'>} */ + line = list => { + if (options.colCraft) return ['row', list.map(row => ['column', row])]; + else return ['column', list.map(row => ['row', row])]; + }; + + return ['row', [ + line(square(trecipe.consumes.map(([item, cost]) => { + const tile = item_tile(item), + text = shiftDown ? `[${trecipe.formulas.consumes[item]}]` : `${format(player.items[item].amount)} / ${format(cost)}`; + tile.text = `${capitalize(tmp.items[item].name)}
${text}`; + + return ['tile', tile]; + }))), + 'blank', + ['dynabar', { + direction: RIGHT, + height: 60, + width: 300, + progress() { + if ('duration' in trecipe) return D.div(player.c.recipes[trecipe.id].time, trecipe.duration); + return D.dZero; + }, + display() { + if ('duration' in trecipe) { + if (shiftDown) return `${trecipe.formulas.duration}`; + return `${formatTime(player.c.recipes[trecipe.id].time)} / ${formatTime(trecipe.duration)}`; + } + }, + fillStyle: { + 'backgroundColor': colors[options.theme][3], + }, + }], + 'blank', + line(square(trecipe.produces.map(([item, prod]) => { + const tile = item_tile(item), + text = shiftDown ? `[${trecipe.formulas.produces[item]}]` : format(prod); + tile.text = `${capitalize(tmp.items[item].name)}
${text}`; + + return ['tile', tile]; + }))), + 'blank', + ['column', [ + ['tile', { + text: '+', + style: { + height: '30px', + width: '40px', + }, + canClick() { return D.lt(precipe.target, tmp.c.crafting.max); }, + onClick() { precipe.target = D.add(precipe.target, 1).min(tmp.c.crafting.max); }, + onHold() { precipe.target = D.add(precipe.target, 1).min(tmp.c.crafting.max); }, + }], + ['tile', { + text: `Craft ${formatWhole(precipe.target)}${craft}${total}`, + canClick() { return D.lte(precipe.making, 0) && crafting_can(recipe); }, + onClick() { + gain_items(trecipe.consumes.map(([item, amount]) => [item, amount.neg()])); + precipe.making = precipe.target; + precipe.time = D.dZero; + precipe.crafted = D.add(precipe.crafted, precipe.target); + }, + style: { + height: '60px', + }, + }], + ['tile', { + text: '-', + style: { + height: '30px', + width: '40px', + }, + canClick() { return D.gt(precipe.target, 1); }, + onClick() { precipe.target = D.minus(precipe.target, 1).max(1); }, + onHold() { precipe.target = D.minus(precipe.target, 1).max(1); }, + }], + ]], + ]]; +} +/** + * Returns the default amount for a recipe + * + * @param {string} recipe + * @param {DecimalSource} [amount] + */ +function crafting_default_amount(recipe, amount) { + if (amount && D.gt(amount, 0)) return D(amount); + if (D.gt(player.c.recipes[recipe].time, 0)) return player.c.recipes[recipe].making; + if (D.gt(player.c.recipes[recipe].target, 0)) return player.c.recipes[recipe].target; + return D.dOne; +} +/** + * Returns the default all_time for a recipe + * + * @param {string} recipe + * @param {DecimalSource} [all_time] + */ +function crafting_default_all_time(recipe, all_time) { + if (all_time && D.gt(all_time, 0)) return D(all_time); + if (!(tmp.c.recipes[recipe].static ?? false)) return D.dZero; + return player.c.recipes[recipe].crafted; +} +/** + * Checks whether a recipe can run + * + * @param {string} recipe + * @param {Decimal} amount + * @returns {boolean} + */ +function crafting_can(recipe, amount) { + /** @type {[items, Decimal][]} */ + let items = []; + if (!amount) items = tmp.c.recipes[recipe].consumes; + else items = layers.c.recipes[recipe].consumes(amount); + return items.every(([item, amount]) => D.gte(player.items[item].amount, amount)); +} +/** + * Returns the content for lore in the crafting tabFormat + * + * @param {items|false} item + * @returns {TabFormatEntries<'c'>[]} + */ +function compendium_content(item) { + if (!item) return []; + const itemp = tmp.items[item]; + + if (!(itemp.unlocked ?? true)) return []; + + /** @type {TabFormatEntries<'c'>[]} */ + const lines = [ + ['display-text', capitalize(itemp.name)], + 'blank', + ['display-text', `You have ${resourceColor(tmp.c.color, format(player.items[item].amount))}`], + ['display-text', `Obtained ${resourceColor(tmp.c.color, format(player.items[item].total))} times`], + 'blank', + ]; + + if ('effectDescription' in itemp) lines.push(['display-text', item_list[item].effectDescription()], 'blank'); + + lines.push(['display-text', itemp.lore]); + + return lines; +} diff --git a/js/technical/systemComponents.js b/js/technical/systemComponents.js index 0c91d63..3155126 100644 --- a/js/technical/systemComponents.js +++ b/js/technical/systemComponents.js @@ -179,7 +179,8 @@ var systemComponents = { - + + ` }, diff --git a/js/utils/options.js b/js/utils/options.js index 8910289..d1d1164 100644 --- a/js/utils/options.js +++ b/js/utils/options.js @@ -19,6 +19,7 @@ function getStartOptions() { /** @type {keyof typeof CHANCE_MODE} */ chanceMode: 'NEVER', noRNG: false, + colCraft: false, } } diff --git a/js/utils/themes.js b/js/utils/themes.js index 3c5d9d4..185dc65 100644 --- a/js/utils/themes.js +++ b/js/utils/themes.js @@ -1,7 +1,8 @@ // ************ Themes ************ -var themes = ["default", "aqua"] +/** @type {(keyof typeof colors)[]} */ +const themes = ["default", "aqua"] -var colors = { +const colors = { default: { 1: "#ffffff",//Branch color 1 2: "#bfbfbf",//Branch color 2 @@ -24,7 +25,6 @@ var colors = { }, } function changeTheme() { - colors_theme = colors[options.theme || "default"]; document.body.style.setProperty('--background', colors_theme["background"]); document.body.style.setProperty('--background_tooltip', colors_theme["background_tooltip"]); @@ -32,17 +32,18 @@ function changeTheme() { document.body.style.setProperty('--points', colors_theme["points"]); document.body.style.setProperty("--locked", colors_theme["locked"]); } +/** @returns {keyof typeof colors} */ function getThemeName() { - return options.theme? options.theme : "default"; + return options.theme ? options.theme : "default"; } function switchTheme() { let index = themes.indexOf(options.theme) - if (options.theme === null || index >= themes.length-1 || index < 0) { + if (options.theme === null || index >= themes.length - 1 || index < 0) { options.theme = themes[0]; } else { - index ++; + index++; options.theme = themes[index]; options.theme = themes[1]; } diff --git a/resources/images/UI.png b/resources/images/UI.png index f9defc8..a4d4277 100644 Binary files a/resources/images/UI.png and b/resources/images/UI.png differ diff --git a/resources/images/aap-64-3.pal b/resources/images/aap-64-3.pal index 4fe465e..b3c3b07 100644 --- a/resources/images/aap-64-3.pal +++ b/resources/images/aap-64-3.pal @@ -4,18 +4,18 @@ JASC-PAL 0 0 0 17 17 17 51 17 34 -119 17 51 +119 17 34 187 34 34 -221 68 34 -255 102 17 -255 170 34 +221 51 34 +255 102 0 +255 170 17 255 221 68 255 255 68 -221 238 102 +221 255 102 153 221 68 -85 187 51 -17 153 51 -34 119 68 +85 204 51 +17 170 34 +17 119 51 34 85 51 17 34 34 17 51 102 @@ -24,22 +24,22 @@ JASC-PAL 34 221 204 170 255 221 255 255 255 -255 238 187 +255 255 204 255 221 187 -238 153 153 +255 170 153 238 102 119 187 68 153 119 51 136 68 51 85 34 34 51 -34 34 34 -51 51 34 +34 17 17 +51 34 34 119 68 51 187 119 68 221 170 102 -238 204 153 -221 221 238 -187 187 204 +255 221 153 +221 238 238 +187 187 221 136 153 170 102 119 136 68 85 102 @@ -49,19 +49,19 @@ JASC-PAL 136 85 85 187 119 102 238 187 170 -221 238 255 +238 238 255 187 187 255 -136 153 221 +136 153 238 85 136 187 68 119 136 -34 102 85 +34 102 68 51 136 102 85 170 136 153 221 187 -204 255 221 -221 204 170 -204 170 136 -153 136 102 +204 255 238 +238 221 170 +204 187 136 +170 136 102 119 102 85 -85 85 68 +85 68 68 68 51 51 diff --git a/resources/images/enemies.png b/resources/images/enemies.png index a966b76..d7e8f18 100644 Binary files a/resources/images/enemies.png and b/resources/images/enemies.png differ diff --git a/resources/images/items.png b/resources/images/items.png index 225e435..2c97836 100644 Binary files a/resources/images/items.png and b/resources/images/items.png differ