Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions data/json/recipes/food/raw_grain.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@
"byproducts": [ [ "chaff", 1 ] ],
"components": [ [ [ "threshed_wheat", 1 ] ] ]
},
{
"type": "recipe",
"copy-from": "threshed_oats_flail",
"result": "threshed_oats",
"id_suffix": "flail_bulk",
"name": "bulk %s",
"result_mult": 10,
"time": "19 m 2s",
"batch_time_factors": { "mode": "linear", "setup": "9 m 2 s", "max": 5 },
"byproducts": [ [ "straw_pile", 20 ] ],
"components": [ [ [ "dry_oat_stalks", 10 ] ] ]
},
{
"type": "recipe",
"activity_level": "BRISK_EXERCISE",
Expand Down
22 changes: 21 additions & 1 deletion doc/JSON/ITEM_CRAFT_AND_DISASSEMBLY.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Crafting recipes are defined as a JSON object with the following fields:
"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.
"container": "jar_glass_sealed", //The resulting item will be contained by the item set here, overrides default container.
"container_variant": "jar_glass_sealed_strawberry_picture", //The container specified above will spawn as the specified variant, overrides the normal weighted behavior.
"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.
"batch_time_factors": ..., // See below for details
"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.
"result_mult": 2, // Multiplier for resulting items. Also multiplies container items.
"flags": [ // A set of strings describing boolean features of the recipe
Expand Down Expand Up @@ -117,6 +117,26 @@ Crafting recipes are defined as a JSON object with the following fields:
]
```

#### `batch_time_factors`

`batch_time_factors supports several formats, with two different scaling functions.

Logistic scaling provides savings of some percent once the batch reaches a certain size.
```jsonc
"batch_time_factors": [ 25, 15 ], // legacy
"batch_time_factors": { "mode": "logistic", "percent": 25, "at": 15 }
```

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 5 units would take only 3750 moves to produce.

Linear scaling provides purely linear scaling. There are two parameters, the `setup` 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, it defaults to n, simplifying to `T + (n * (t - T))`.
In other words, max does not limit the max batch size, it merely specifies when the setup cost will be applied again.
It is specified as follows:
```jsonc
"batch_time_factors": { "mode": "linear": "setup": "12 m" },
"batch_time_factors": { "mode": "linear": "setup": "12 m", "max": 20 },
```

## Practice recipes

Recipes may instead be defined with type "practice", to make them appear in the "PRACTICE" tab of
Expand Down
13 changes: 13 additions & 0 deletions src/generic_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -2168,6 +2168,19 @@ class time_bound_reader : public bound_reader<T>
};
};

struct percentile_reader : public generic_typed_reader<percentile_reader> {
double lower;
double upper;

explicit percentile_reader( double l = std::numeric_limits<double>::max(),
double h = std::numeric_limits<double>::max() ) : lower( l ), upper( h ) {}

double get_next( const JsonValue &jv ) const {
double ret = jv.get_float() / 100.0;
return bound_check( lower, upper, jv, ret );
}
};

struct weakpoints;

struct weakpoints_reader : generic_typed_reader<weakpoints_reader> {
Expand Down
121 changes: 90 additions & 31 deletions src/recipe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,30 @@ int64_t recipe::time_to_craft_moves( const Character &guy, recipe_time_flag flag
return time * proficiency_time_maluses( guy );
}

double batch_savings::apply( double time, int batch_size ) const
{
if( const linear *lin = std::get_if<linear>( &data ) ) {
int reps = std::ceil( batch_size / static_cast<double>( lin->max_batch.value_or( batch_size ) ) );
return ( reps * lin->offset ) + ( batch_size * ( time - lin->offset ) );
}
if( const logistic *log = std::get_if<logistic>( &data ) ) {
if( log->rscale == 0.0f ) {
return time * batch_size;
}
double ret = 0.0;
// recipe benefits from batching, so batching scale factor needs to be calculated
// At batch_rsize, incremental time increase is 99.5% of batch_rscale
const double scale = log->rsize / 6.0f;
for( int x = 0; x < batch_size; x++ ) {
// scaled logistic function output
const double logf = ( 2.0 / ( 1.0 + std::exp( -( x / scale ) ) ) ) - 1.0;
ret += time * ( 1.0 - ( log->rscale * logf ) );
}
return ret;
}
return time * batch_size;
}

int64_t recipe::batch_time( const Character &guy, int batch, float multiplier,
size_t assistants ) const
{
Expand All @@ -113,26 +137,7 @@ int64_t recipe::batch_time( const Character &guy, int batch, float multiplier,
}

const double local_time = static_cast<double>( time_to_craft_moves( guy ) ) / multiplier;

// if recipe does not benefit from batching and we have no assistants, don't do unnecessary additional calculations
if( batch_rscale == 0.0 && assistants == 0 ) {
return static_cast<int64_t>( local_time ) * batch;
}

double total_time = 0.0;
// if recipe does not benefit from batching but we do have assistants, skip calculating the batching scale factor
if( batch_rscale == 0.0f ) {
total_time = local_time * batch;
} else {
// recipe benefits from batching, so batching scale factor needs to be calculated
// At batch_rsize, incremental time increase is 99.5% of batch_rscale
const double scale = batch_rsize / 6.0f;
for( int x = 0; x < batch; x++ ) {
// scaled logistic function output
const double logf = ( 2.0 / ( 1.0 + std::exp( -( x / scale ) ) ) ) - 1.0;
total_time += local_time * ( 1.0 - ( batch_rscale * logf ) );
}
}
double total_time = batch_info.apply( local_time, batch );

//Assistants can decrease the time for production but never less than that of one unit
if( assistants == 1 ) {
Expand All @@ -152,6 +157,14 @@ bool recipe::has_flag( const std::string &flag_name ) const
return flags.count( flag_name );
}

struct time_duration_as_moves_reader : public generic_typed_reader<time_duration_as_moves_reader> {
int64_t get_next( const JsonValue &jv ) const {
time_duration ret;
jv.read( ret );
return to_moves<int64_t>( ret );
}
};

void recipe::load( const JsonObject &jo, const std::string_view src )
{
abstract = jo.has_string( "abstract" );
Expand Down Expand Up @@ -221,10 +234,7 @@ void recipe::load( const JsonObject &jo, const std::string_view src )
return;
}

if( jo.has_string( "time" ) ) {
time = to_moves<int>( read_from_json_string<time_duration>( jo.get_member( "time" ),
time_duration::units ) );
}
optional( jo, was_loaded, "time", time, time_duration_as_moves_reader{}, 0 );
optional( jo, was_loaded, "difficulty", difficulty, numeric_bound_reader<int> {0, MAX_SKILL} );
optional( jo, was_loaded, "flags", flags );

Expand All @@ -237,10 +247,11 @@ void recipe::load( const JsonObject &jo, const std::string_view src )
optional( jo, false, "container_variant", container_variant );
optional( jo, was_loaded, "sealed", sealed, true );

if( jo.has_array( "batch_time_factors" ) ) {
JsonArray batch = jo.get_array( "batch_time_factors" );
batch_rscale = batch.get_int( 0 ) / 100.0;
batch_rsize = batch.get_int( 1 );
optional( jo, was_loaded, "batch_time_factors", batch_info );
if( batch_savings::linear *lin = std::get_if<batch_savings::linear>( &batch_info.data ) ) {
if( lin->offset > time ) {
jo.throw_error( "batch scaling time greater than recipe time" );
}
}

optional( jo, was_loaded, "charges", charges );
Expand Down Expand Up @@ -1195,11 +1206,25 @@ std::string recipe::required_all_skills_string( const std::map<skill_id, int> &s
return required_skills_as_string( skillList );
}

std::string batch_savings::savings_string() const
{
if( const linear *lin = std::get_if<linear>( &data ) ) {
std::string time_saved = to_string( time_duration::from_moves<int>( lin->offset ) );
if( lin->max_batch.has_value() ) {
return string_format( _( "%s per unit to %d units" ), time_saved, lin->max_batch.value() );
} else {
return string_format( _( "%s per unit" ), time_saved );
}
}
if( const logistic *log = std::get_if<logistic>( &data ) ) {
return string_format( _( "%d%% at >%d units" ), static_cast<int>( log->rscale * 100 ), log->rsize );
}
return _( "none" );
}

std::string recipe::batch_savings_string() const
{
return ( batch_rsize != 0 ) ?
string_format( _( "%d%% at >%d units" ), static_cast<int>( batch_rscale * 100 ), batch_rsize )
: _( "none" );
return batch_info.savings_string();
}

std::string recipe::result_name( const bool decorated ) const
Expand Down Expand Up @@ -1521,6 +1546,40 @@ void recipe::incorporate_build_reqs()
}
}

void batch_savings::deserialize( const JsonValue &jv )
{
if( jv.test_array() ) {
JsonArray ja = jv.get_array();
logistic ret;
ja.read( 0, ret.rscale );
ret.rscale /= 100.0;
ja.read( 1, ret.rsize );
data = ret;
if( ret.rscale > 1.0 || ret.rscale <= 0.0 || ret.rsize < 1 ) {
jv.throw_error( "Invalid batch factors" );
}
return;
}
JsonObject jo = jv.get_object();
std::string mode = jo.get_string( "mode" );
if( mode == "linear" ) {
linear ret;
mandatory( jo, false, "setup", ret.offset, time_duration_as_moves_reader{} );
optional( jo, false, "max", ret.max_batch );
if( ret.max_batch.value_or( 1 ) < 1 ) {
jo.throw_error( "Invalid max value" );
}
data = ret;
} else if( mode == "logistic" ) {
logistic ret;
mandatory( jo, false, "percent", ret.rscale, percentile_reader{0, 100} );
mandatory( jo, false, "at", ret.rsize, numeric_bound_reader{1} );
data = ret;
} else {
jo.throw_error( string_format( "Unrecognized mode %s", mode ) );
}
}

void recipe_proficiency::deserialize( const JsonObject &jo )
{
load( jo );
Expand Down
31 changes: 27 additions & 4 deletions src/recipe.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

#include "build_reqs.h"
Expand All @@ -22,6 +23,7 @@

class Character;
class JsonObject;
class JsonValue;
class cata_variant;
class item;
class item_components;
Expand Down Expand Up @@ -84,6 +86,30 @@ struct practice_recipe_data {
void deserialize( const JsonObject &jo );
};

struct batch_savings {
// Linear, time taken for recipe of time T and batch of N is:
// ((N/max_batch) * offset) + (N * (T - offset))
struct linear {
int64_t offset;
std::optional<int> max_batch;
};
struct logistic {
// maximum achievable time reduction, as percentage of the original time.
// if zero then the recipe has no batch crafting time reduction.
double rscale;
int rsize; // minimum batch size to needed to reach batch_rscale
};
struct none { };

std::variant<linear, logistic, none> data;
void deserialize( const JsonValue &jv );

double apply( double time, int batch_size ) const;
std::string savings_string() const;

batch_savings() : data( none{} ) {}
};

class recipe
{
friend class recipe_dictionary;
Expand Down Expand Up @@ -358,10 +384,7 @@ class recipe
/** Item group representing byproducts **/
std::optional<item_group_id> byproduct_group;

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