Skip to content

Commit 643bb71

Browse files
committed
Linear recipe batch time scaling
Add an option for linear batch time scaling, in addition to the existing logistic scaling, and add a bulk threshed oats recipe to demonstrate its use. Variant is maybe overkill, but seemed like a natural approach to the problem.
1 parent f6bc402 commit 643bb71

File tree

4 files changed

+130
-33
lines changed

4 files changed

+130
-33
lines changed

data/json/recipes/food/raw_grain.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,18 @@
164164
"byproducts": [ [ "chaff", 1 ] ],
165165
"components": [ [ [ "threshed_wheat", 1 ] ] ]
166166
},
167+
{
168+
"type": "recipe",
169+
"copy-from": "threshed_oats_flail",
170+
"result": "threshed_oats",
171+
"id_suffix": "flail_bulk",
172+
"name": "bulk %s",
173+
"result_mult": 10,
174+
"time": "19 m 2s",
175+
"batch_time_factors": { "mode": "linear", "offset": "9 m 2 s", "max": 5 },
176+
"byproducts": [ [ "straw_pile", 20 ] ],
177+
"components": [ [ [ "dry_oat_stalks", 10 ] ] ]
178+
},
167179
{
168180
"type": "recipe",
169181
"activity_level": "BRISK_EXERCISE",

doc/JSON/ITEM_CRAFT_AND_DISASSEMBLY.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Crafting recipes are defined as a JSON object with the following fields:
7676
"contained": true, // Boolean value which defines if the resulting item comes in its designated container. Automatically set to true if any container is defined in the recipe.
7777
"container": "jar_glass_sealed", //The resulting item will be contained by the item set here, overrides default container.
7878
"container_variant": "jar_glass_sealed_strawberry_picture", //The container specified above will spawn as the specified variant, overrides the normal weighted behavior.
79-
"batch_time_factors": [25, 15], // Optional factors for batch crafting time reduction. First number specifies maximum crafting time reduction as percentage, and the second number the minimal batch size to reach that number. In this example given batch size of 20 the last 6 crafts will take only 3750 time units.
79+
"batch_time_factors": ..., // See below for details
8080
"charges": 2, // Number of resulting items/charges per craft. Uses default charges if not set. If a container is set, this is the amount that gets put inside it, capped by container capacity.
8181
"result_mult": 2, // Multiplier for resulting items. Also multiplies container items.
8282
"flags": [ // A set of strings describing boolean features of the recipe
@@ -117,6 +117,25 @@ Crafting recipes are defined as a JSON object with the following fields:
117117
]
118118
```
119119

120+
#### `batch_time_factors`
121+
122+
`batch_time_factors supports several formats, with two different scaling functions.
123+
124+
Logistic scaling provides savings of some percent once the batch reaches a certain size.
125+
```jsonc
126+
"batch_time_factors": [ 25, 15 ],
127+
"batch_time_factors": { "mode": "logistic", "percent": 25, "at": 15 }
128+
```
129+
130+
This shows both formats for logistic scaling. The first number specifies the maximum crafting time reduction as a percentage, and the second number the minimal batch size to reach that number. If this recipe took 5000 moves, when made in a batch of 20, the last 6 units would take only 3750 moves to produce.
131+
132+
Linear scaling provides purely linear scaling. There are two parameters, the `offset` time `T`, and the max batch size `M`, which is optional. The time taken for a batch of size `n` for a recipe which takes `t` time is: `(ceil(n/M) * T) + (n * (t - T))`. If `M` is not specified, this simplifes to `T + (n * (t - T))`.
133+
It is specified as follows:
134+
```jsonc
135+
"batch_time_factors": { "mode": "linear": "offset": "12 m" },
136+
"batch_time_factors": { "mode": "linear": "offset": "12 m", "max": 20 },
137+
```
138+
120139
## Practice recipes
121140

122141
Recipes may instead be defined with type "practice", to make them appear in the "PRACTICE" tab of

src/recipe.cpp

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,30 @@ int64_t recipe::time_to_craft_moves( const Character &guy, recipe_time_flag flag
105105
return time * proficiency_time_maluses( guy );
106106
}
107107

108+
double batch_savings::apply( double time, int batch_size ) const
109+
{
110+
if( const linear *lin = std::get_if<linear>( &data ) ) {
111+
int reps = std::ceil( batch_size / static_cast<double>( lin->max_batch.value_or( batch_size ) ) );
112+
return ( reps * lin->offset ) + ( batch_size * ( time - lin->offset ) );
113+
}
114+
if( const logistic *log = std::get_if<logistic>( &data ) ) {
115+
if( log->rscale == 0.0f ) {
116+
return time * batch_size;
117+
}
118+
double ret = 0.0;
119+
// recipe benefits from batching, so batching scale factor needs to be calculated
120+
// At batch_rsize, incremental time increase is 99.5% of batch_rscale
121+
const double scale = log->rsize / 6.0f;
122+
for( int x = 0; x < batch_size; x++ ) {
123+
// scaled logistic function output
124+
const double logf = ( 2.0 / ( 1.0 + std::exp( -( x / scale ) ) ) ) - 1.0;
125+
ret += time * ( 1.0 - ( log->rscale * logf ) );
126+
}
127+
return ret;
128+
}
129+
return time * batch_size;
130+
}
131+
108132
int64_t recipe::batch_time( const Character &guy, int batch, float multiplier,
109133
size_t assistants ) const
110134
{
@@ -116,26 +140,7 @@ int64_t recipe::batch_time( const Character &guy, int batch, float multiplier,
116140
}
117141

118142
const double local_time = static_cast<double>( time_to_craft_moves( guy ) ) / multiplier;
119-
120-
// if recipe does not benefit from batching and we have no assistants, don't do unnecessary additional calculations
121-
if( batch_rscale == 0.0 && assistants == 0 ) {
122-
return static_cast<int64_t>( local_time ) * batch;
123-
}
124-
125-
double total_time = 0.0;
126-
// if recipe does not benefit from batching but we do have assistants, skip calculating the batching scale factor
127-
if( batch_rscale == 0.0f ) {
128-
total_time = local_time * batch;
129-
} else {
130-
// recipe benefits from batching, so batching scale factor needs to be calculated
131-
// At batch_rsize, incremental time increase is 99.5% of batch_rscale
132-
const double scale = batch_rsize / 6.0f;
133-
for( int x = 0; x < batch; x++ ) {
134-
// scaled logistic function output
135-
const double logf = ( 2.0 / ( 1.0 + std::exp( -( x / scale ) ) ) ) - 1.0;
136-
total_time += local_time * ( 1.0 - ( batch_rscale * logf ) );
137-
}
138-
}
143+
double total_time = batch_info.apply( local_time, batch );
139144

140145
//Assistants can decrease the time for production but never less than that of one unit
141146
if( assistants == 1 ) {
@@ -245,11 +250,7 @@ void recipe::load( const JsonObject &jo, const std::string &src )
245250
optional( jo, false, "container_variant", container_variant );
246251
assign( jo, "sealed", sealed, strict );
247252

248-
if( jo.has_array( "batch_time_factors" ) ) {
249-
JsonArray batch = jo.get_array( "batch_time_factors" );
250-
batch_rscale = batch.get_int( 0 ) / 100.0;
251-
batch_rsize = batch.get_int( 1 );
252-
}
253+
optional( jo, was_loaded, "batch_time_factors", batch_info );
253254

254255
assign( jo, "charges", charges );
255256
assign( jo, "result_mult", result_mult );
@@ -1205,11 +1206,25 @@ std::string recipe::required_all_skills_string( const std::map<skill_id, int> &s
12051206
return required_skills_as_string( skillList );
12061207
}
12071208

1209+
std::string batch_savings::savings_string() const
1210+
{
1211+
if( const linear *lin = std::get_if<linear>( &data ) ) {
1212+
std::string time_saved = to_string( time_duration::from_moves<int>( lin->offset ) );
1213+
if( lin->max_batch.has_value() ) {
1214+
return string_format( _( "%s per unit to %d units" ), time_saved, lin->max_batch.value() );
1215+
} else {
1216+
return string_format( _( "%s per unit" ), time_saved );
1217+
}
1218+
}
1219+
if( const logistic *log = std::get_if<logistic>( &data ) ) {
1220+
return string_format( _( "%d%% at >%d units" ), static_cast<int>( log->rscale * 100 ), log->rsize );
1221+
}
1222+
return _( "none" );
1223+
}
1224+
12081225
std::string recipe::batch_savings_string() const
12091226
{
1210-
return ( batch_rsize != 0 ) ?
1211-
string_format( _( "%d%% at >%d units" ), static_cast<int>( batch_rscale * 100 ), batch_rsize )
1212-
: _( "none" );
1227+
return batch_info.savings_string();
12131228
}
12141229

12151230
std::string recipe::result_name( const bool decorated ) const
@@ -1531,6 +1546,34 @@ void recipe::incorporate_build_reqs()
15311546
}
15321547
}
15331548

1549+
void batch_savings::deserialize( const JsonValue &jv )
1550+
{
1551+
if( jv.test_array() ) {
1552+
JsonArray ja = jv.get_array();
1553+
logistic ret;
1554+
ja.read( 0, ret.rscale );
1555+
ret.rscale /= 100.0;
1556+
ja.read( 1, ret.rsize );
1557+
data = ret;
1558+
return;
1559+
}
1560+
JsonObject jo = jv.get_object();
1561+
std::string mode = jo.get_string( "mode" );
1562+
if( mode == "linear" ) {
1563+
linear ret;
1564+
mandatory( jo, false, "offset", ret.offset, time_duration_as_moves_reader{} );
1565+
optional( jo, false, "max", ret.max_batch );
1566+
data = ret;
1567+
} else if( mode == "logistic" ) {
1568+
logistic ret;
1569+
mandatory( jo, false, "percent", ret.rscale, percentile_reader{} );
1570+
mandatory( jo, false, "at", ret.rsize );
1571+
data = ret;
1572+
} else {
1573+
jo.throw_error( string_format( "Unrecognized mode %s", mode ) );
1574+
}
1575+
}
1576+
15341577
void recipe_proficiency::deserialize( const JsonObject &jo )
15351578
{
15361579
load( jo );

src/recipe.h

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <set>
1111
#include <string>
1212
#include <utility>
13+
#include <variant>
1314
#include <vector>
1415

1516
#include "build_reqs.h"
@@ -21,6 +22,7 @@
2122

2223
class Character;
2324
class JsonObject;
25+
class JsonValue;
2426
class cata_variant;
2527
class item;
2628
class item_components;
@@ -83,6 +85,30 @@ struct practice_recipe_data {
8385
void deserialize( const JsonObject &jo );
8486
};
8587

88+
struct batch_savings {
89+
// Linear, time taken for recipe of time T and batch of N is:
90+
// ((N/max_batch) * offset) + (N * (T - offset))
91+
struct linear {
92+
int64_t offset;
93+
std::optional<int> max_batch;
94+
};
95+
struct logistic {
96+
// maximum achievable time reduction, as percentage of the original time.
97+
// if zero then the recipe has no batch crafting time reduction.
98+
double rscale;
99+
int rsize; // minimum batch size to needed to reach batch_rscale
100+
};
101+
struct none { };
102+
103+
std::variant<linear, logistic, none> data;
104+
void deserialize( const JsonValue &jv );
105+
106+
double apply( double time, int batch_size ) const;
107+
std::string savings_string() const;
108+
109+
batch_savings() : data( none{} ) {}
110+
};
111+
86112
class recipe
87113
{
88114
friend class recipe_dictionary;
@@ -357,10 +383,7 @@ class recipe
357383
/** Item group representing byproducts **/
358384
std::optional<item_group_id> byproduct_group;
359385

360-
// maximum achievable time reduction, as percentage of the original time.
361-
// if zero then the recipe has no batch crafting time reduction.
362-
double batch_rscale = 0.0;
363-
int batch_rsize = 0; // minimum batch size to needed to reach batch_rscale
386+
batch_savings batch_info;
364387
int result_mult = 1; // used by certain batch recipes that create more than one stack of the result
365388
update_mapgen_id blueprint;
366389
translation bp_name;

0 commit comments

Comments
 (0)