Skip to content

Commit

Permalink
feat(workshop): Simplify resource crafting
Browse files Browse the repository at this point in the history
There was some edge case handling to balance production between plates
and steel, which seems entirely redundant, given the balancing behavior
of limited crafting.

The trigger applied to some hardcoded material, only a single one, and
only for some crafts. This could cause unintuitive crafting behavior.

Even if the trigger wasn't hit, a resource could still be crafted under
certain conditions.

The behavior of limited crafting was inconsistent.

The way resource crafting behaves now, should be far more intuitive and
consistent. Additionally, it has now been documented.
  • Loading branch information
oliversalzburg committed Jan 26, 2023
1 parent 6c11005 commit c9a19a2
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 146 deletions.
2 changes: 2 additions & 0 deletions packages/documentation/docs/sections/bonfire.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
When you enable a building, this building will be built if all of these are true:

1. Less than **Max** buildings have already been built.

1. _All_ of the resources required for the building to built are filled to **Trigger** of their stock capacity.

1. _All_ of the resources required for the building to built are sufficiently available after considering configured **Stock** and **Consume**.

### Upgrades
Expand Down
2 changes: 1 addition & 1 deletion packages/documentation/docs/sections/village.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ Promotes your leader, as soon as possible.

### Elect leader

Elects a kittens with the given attributes as a leader.
Elects a kitten with the given attributes as a leader.
44 changes: 44 additions & 0 deletions packages/documentation/docs/sections/workshop.md
Original file line number Diff line number Diff line change
@@ -1 +1,45 @@
# Workshop

## Resource Crafting

In general, resources are crafted if they are enabled and less than **Max** resources are already in stock.

Additionally, if crafting a resource requires one or more materials that have a capacity, those materials need to be filled to **Trigger** of their stock capacity.

!!! example

**Wood** has a capacity. If you want to craft **Beams**, and you set a trigger of `0.5`, then beams would be crafted if wood is filled to half of its stock capacity.

When you want to craft **Megaliths**, they are built from **Beams**, **Slabs**, and **Plates**. All of which have _no capacity_. So the trigger value does not apply to this craft.

### Unlimited Crafting

Just craft as many items as possible, respecting your [Resource Control configuration](./resource-control.md).

### Limited Crafting

For a limited item to be crafted, we first look at all the materials that are required for the item to be crafted, and at our current stock for the item. We then calculate how much of our materials would be required to build all the items _we already have in stock_. If we have more materials than _that_, then we allow the additional materials to be crafted into more of the craftable item.

!!! example

Let's assume you want to craft **Beams**. A beam costs 175 **Wood**, and you have 1 beam in stock. KS would then craft the next beam when you have 350 wood available.

### Force Ships to 243

When enabled, **Trade Ships** will be handled as _unlimited_, regardless of the actual configuration, until you have at least 243 ships in stock.

!!! quote

Having 243 or more ships will guarantee that you get titanium from a trade. The exact number is `1700/7` ships, which rounds up to 243.

<https://wiki.kittensgame.com/en/general-information/resources/ship>

## Research Upgrades

Selected upgrades will automatically be researched as soon as possible.

<!-- prettier-ignore-start -->
*[KG]: Kittens Game
*[KS]: Kitten Scientists
*[UI]: User interface
<!-- prettier-ignore-end -->
2 changes: 1 addition & 1 deletion packages/documentation/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
copyright: Oliver Salzburg
copyright: Oliver Salzburg and Kitten Science Contributors
dev_addr: 0.0.0.0:8000
docs_dir: docs
extra:
Expand Down
114 changes: 26 additions & 88 deletions packages/userscript/source/WorkshopManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,38 +90,38 @@ export class WorkshopManager extends UpgradeManager implements Automation {
autoCraft(
crafts: Partial<Record<ResourceCraftable, CraftSettingsItem>> = this.settings.resources
) {
// TODO: One of the core limitations here is that only a single resource
// is taken into account, the one set as `require` in the definition.
const trigger = this.settings.trigger;

for (const craft of Object.values(crafts)) {
// This will always be `false` while `max` is hardcoded to `0`.
// Otherwise, it would contain the current resource information.
const current = !craft.max ? false : this.getResource(craft.resource);
// The resource information for the requirement of this craft, if any.
const require = !craft.require ? false : this.getResource(craft.require);

const max = craft.max === -1 ? Number.POSITIVE_INFINITY : craft.max;
let amount = 0;
// Ensure that we have reached our cap
if (current && current.value > max) continue;
if (current && max < current.value) {
continue;
}

// If we can't even craft a single item of the resource, skip it.
if (!this.singleCraftPossible(craft.resource)) {
continue;
}

// Craft the resource if it doesn't require anything or we hit the requirement trigger.
if (!require || trigger <= require.value / require.maxValue) {
amount = this.getLowestCraftAmount(craft.resource, craft.limited, true);
const materials = Object.keys(this.getMaterials(craft.resource)) as Array<Resource>;
// The resource information for the requirement of this craft which have a capacity.
const require = materials
.map(material => this.getResource(material))
.filter(material => 0 < material.maxValue);

// If a resource DOES "require" another resource AND its trigger value has NOT been hit
// yet AND it is limited... What?
} else if (craft.limited) {
amount = this.getLowestCraftAmount(craft.resource, craft.limited, false);
const allMaterialsAboveTrigger =
require.filter(material => material.value / material.maxValue < trigger).length === 0;

if (!allMaterialsAboveTrigger) {
continue;
}

const amount = this.getLowestCraftAmount(craft.resource, craft.limited, 0 < require.length);

// If we can craft any of this item, do it.
if (amount > 0) {
if (0 < amount) {
this.craft(craft.resource, amount);
}
}
Expand Down Expand Up @@ -227,81 +227,18 @@ export class WorkshopManager extends UpgradeManager implements Automation {
*
* @param name The resource to craft.
* @param limited Is the crafting of the resource currently limited?
* @param requiredResourceAboveTrigger Is the resource that is required for
* this craft currently above the trigger value?
* @returns ?
* @param capacityControlled Is this craft dependant on materials that have a stock capacity?
* @returns The amount of resources to craft.
*/
getLowestCraftAmount(
name: ResourceCraftable,
limited: boolean,
requiredResourceAboveTrigger: boolean
capacityControlled = false
): number {
const materials = this.getMaterials(name);

const craft = this.getCraft(name);
const ratio = this._host.gamePage.getResCraftRatio(craft.name);
const trigger = this.settings.trigger;

// Safeguard if materials for craft cannot be determined.
if (!materials) {
return 0;
}

// This seems to be a (hopefully) finely balanced act to distribute iron
// between plates and steel.
// One resource will probably be preferred for periods of time, then the
// other resource will take over.
if (name === "steel" && limited) {
// Under some condition, we don't want to craft any steel.
const plateRatio = this._host.gamePage.getResCraftRatio("plate");
// The left term will be larger than 1 if we have more plates than steel.
// The right term is basically 1.25, the relation of iron costs between
// plates and steel. Plates require 125 iron, steel 100.
// This term is weighed in regards to the craft ratio of each resource.
// If we get twice as many plates out of a craft than we would steel, the
// right term is increased above 1.25.
// What this all implies is, only craft steel if a reasonable amount of
// plates are also already crafted.
if (
this.getValueAvailable("plate") / this.getValueAvailable("steel") <
(plateRatio + 1) / 125 / ((ratio + 1) / 100)
) {
return 0;
}
}

// It's not clear *how* this is supposed to work.
// What it tries to do is, if we have coal production, under some condition,
// calculate an appropriate max value for plates to be crafted.
// This amount would be lower than the max plates that could be crafted otherwise.
let plateMax = Number.MAX_VALUE;
if (name === "plate" && limited) {
const steelRatio = this._host.gamePage.getResCraftRatio("steel");
// If we're producing coal, then we could also make steel, and don't want
// to use up all the iron.
const coalPerTick = this._host.gamePage.getResourcePerTick("coal", true);
if (coalPerTick > 0) {
// Here we have the same check as above, but reversed.
// So this would be the case where steel is preferred.
if (
this.getValueAvailable("plate") / this.getValueAvailable("steel") >
(ratio + 1) / 125 / ((steelRatio + 1) / 100)
) {
// The absolute trigger value for coal.
const coalTriggerAmount = this.getResource("coal").maxValue * trigger;
// How many units of coal are we away from the trigger.
const distanceToCoalTrigger = coalTriggerAmount - this.getValue("coal");
// How many ticks until we hit the trigger for coal.
const ticksToCoalTrigger = distanceToCoalTrigger / coalPerTick;
// How much iron will be produce in the time until we hit the trigger for coal.
const ironInTime =
ticksToCoalTrigger * Math.max(this._host.gamePage.getResourcePerTick("iron", true), 0);
// This is some weird voodoo...
plateMax =
(this.getValueAvailable("iron") - Math.max(coalTriggerAmount - ironInTime, 0)) / 125;
}
}
}

// The ship override allows the user to treat ships as "unlimited" while there's less than 243.
const shipOverride = this.settings.shipOverride.enabled;
Expand All @@ -315,11 +252,10 @@ export class WorkshopManager extends UpgradeManager implements Automation {
// The delta is the smallest craft amount based on the current material.
let delta = undefined;

// Either if the build isn't limited, OR we're above the trigger value and have not hit
// our storage limit, OR we're handling the ship override.
// Either if the build isn't limited, or we're handling the ship override.
if (
!limited ||
(requiredResourceAboveTrigger && 0 < this.getResource(resource).maxValue) ||
capacityControlled ||
(name === "ship" && shipOverride && this.getResource("ship").value < 243)
) {
// If there is a storage limit, we can just use everything returned by getValueAvailable,
Expand Down Expand Up @@ -355,14 +291,16 @@ export class WorkshopManager extends UpgradeManager implements Automation {
}
}

amount = Math.min(delta, amount, plateMax);
amount = Math.min(delta, amount);
}

// If we have a maximum value, ensure that we don't produce more than
// this value. This should currently only impact wood crafting, but is
// written generically to ensure it works for any craft that produces a
// good with a maximum value.
if (res.maxValue > 0 && amount > res.maxValue - res.value) amount = res.maxValue - res.value;
if (0 < res.maxValue && res.maxValue - res.value < amount) {
amount = res.maxValue - res.value;
}

return Math.floor(amount);
}
Expand Down
49 changes: 21 additions & 28 deletions packages/userscript/source/settings/WorkshopSettings.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { consumeEntriesPedantic } from "../tools/Entries";
import { isNil, Maybe } from "../tools/Maybe";
import { GamePage, ResourceCraftable } from "../types";
import { Requirement, Setting, SettingLimitedMax, SettingTrigger } from "./Settings";
import { Setting, SettingLimitedMax, SettingTrigger } from "./Settings";
import { UpgradeSettings } from "./UpgradeSettings";

export class CraftSettingsItem extends SettingLimitedMax {
readonly resource: ResourceCraftable;
require: Requirement;

constructor(
resource: ResourceCraftable,
require: Requirement = false,
enabled = true,
limited = true
) {
constructor(resource: ResourceCraftable, enabled = true, limited = true) {
super(enabled, limited);
this.resource = resource;
this.require = require;
}
}

Expand All @@ -32,25 +25,25 @@ export class WorkshopSettings extends SettingTrigger {
enabled = false,
trigger = 0.95,
resources: WorkshopResourceSettings = {
alloy: new CraftSettingsItem("alloy", "titanium"),
beam: new CraftSettingsItem("beam", "wood"),
blueprint: new CraftSettingsItem("blueprint", "science"),
compedium: new CraftSettingsItem("compedium", "science"),
concrate: new CraftSettingsItem("concrate", false),
eludium: new CraftSettingsItem("eludium", "unobtainium"),
gear: new CraftSettingsItem("gear", false),
kerosene: new CraftSettingsItem("kerosene", "oil"),
manuscript: new CraftSettingsItem("manuscript", "culture"),
megalith: new CraftSettingsItem("megalith", false),
parchment: new CraftSettingsItem("parchment", false, true, false),
plate: new CraftSettingsItem("plate", "iron"),
scaffold: new CraftSettingsItem("scaffold", false),
ship: new CraftSettingsItem("ship", false),
slab: new CraftSettingsItem("slab", "minerals"),
steel: new CraftSettingsItem("steel", "coal"),
tanker: new CraftSettingsItem("tanker", false),
thorium: new CraftSettingsItem("thorium", "uranium"),
wood: new CraftSettingsItem("wood", "catnip"),
alloy: new CraftSettingsItem("alloy"),
beam: new CraftSettingsItem("beam"),
blueprint: new CraftSettingsItem("blueprint"),
compedium: new CraftSettingsItem("compedium"),
concrate: new CraftSettingsItem("concrate"),
eludium: new CraftSettingsItem("eludium"),
gear: new CraftSettingsItem("gear"),
kerosene: new CraftSettingsItem("kerosene"),
manuscript: new CraftSettingsItem("manuscript"),
megalith: new CraftSettingsItem("megalith"),
parchment: new CraftSettingsItem("parchment", true, false),
plate: new CraftSettingsItem("plate"),
scaffold: new CraftSettingsItem("scaffold"),
ship: new CraftSettingsItem("ship"),
slab: new CraftSettingsItem("slab"),
steel: new CraftSettingsItem("steel"),
tanker: new CraftSettingsItem("tanker"),
thorium: new CraftSettingsItem("thorium"),
wood: new CraftSettingsItem("wood"),
},
unlockUpgrades = new UpgradeSettings(),
shipOverride = new Setting(true)
Expand Down
50 changes: 22 additions & 28 deletions packages/userscript/source/ui/WorkshopSettingsUi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { CraftSettingsItem, WorkshopSettings } from "../settings/WorkshopSettings";
import { UserScript } from "../UserScript";
import { TriggerButton } from "./components/buttons-icon/TriggerButton";
import { HeaderListItem } from "./components/HeaderListItem";
import { SettingLimitedMaxListItem } from "./components/SettingLimitedMaxListItem";
import { SettingListItem } from "./components/SettingListItem";
import { SettingsList } from "./components/SettingsList";
Expand All @@ -10,8 +9,6 @@ import { UpgradeSettingsUi } from "./UpgradeSettingsUi";

export class WorkshopSettingsUi extends SettingsSectionUi<WorkshopSettings> {
private readonly _trigger: TriggerButton;
private readonly _upgradeUi: UpgradeSettingsUi;
private readonly _shipOverride: SettingListItem;

constructor(host: UserScript, settings: WorkshopSettings) {
const label = host.engine.i18n("ui.craft");
Expand Down Expand Up @@ -63,6 +60,22 @@ export class WorkshopSettingsUi extends SettingsSectionUi<WorkshopSettings> {
this.setting.resources.ship,
this._host.engine.i18n("$workshop.crafts.ship.label")
),
new SettingListItem(
this._host,
this._host.engine.i18n("option.shipOverride"),
this.setting.shipOverride,
{
onCheck: () =>
this._host.engine.imessage("status.sub.enable", [
this._host.engine.i18n("option.shipOverride"),
]),
onUnCheck: () =>
this._host.engine.imessage("status.sub.disable", [
this._host.engine.i18n("option.shipOverride"),
]),
upgradeIndicator: true,
}
),
this._getCraftOption(
this.setting.resources.tanker,
this._host.engine.i18n("$workshop.crafts.tanker.label"),
Expand Down Expand Up @@ -111,32 +124,13 @@ export class WorkshopSettingsUi extends SettingsSectionUi<WorkshopSettings> {
});
this.addChild(listCrafts);

const listAdditional = new SettingsList(this._host, {
hasDisableAll: false,
hasEnableAll: false,
});
listAdditional.addChild(new HeaderListItem(this._host, "Additional options"));

this._upgradeUi = new UpgradeSettingsUi(this._host, this.setting.unlockUpgrades);
listAdditional.addChild(this._upgradeUi);

this._shipOverride = new SettingListItem(
this._host,
this._host.engine.i18n("option.shipOverride"),
this.setting.shipOverride,
{
onCheck: () =>
this._host.engine.imessage("status.sub.enable", [
this._host.engine.i18n("option.shipOverride"),
]),
onUnCheck: () =>
this._host.engine.imessage("status.sub.disable", [
this._host.engine.i18n("option.shipOverride"),
]),
}
this.addChild(
new SettingsList(this._host, {
children: [new UpgradeSettingsUi(this._host, this.setting.unlockUpgrades)],
hasDisableAll: false,
hasEnableAll: false,
})
);
listAdditional.addChild(this._shipOverride);
this.addChild(listAdditional);
}

private _getCraftOption(
Expand Down

0 comments on commit c9a19a2

Please sign in to comment.