Skip to content

Commit 73c31d5

Browse files
committed
feat: add procedural sword descriptions
/home/scarf/.local/bin/assisted-by "openai/gpt-5.4" "opencode"
1 parent 0fdd052 commit 73c31d5

File tree

7 files changed

+173
-1
lines changed

7 files changed

+173
-1
lines changed

src/proc_builder.cpp

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,127 @@ auto facts_have_tag( const std::vector<proc::part_fact> &facts, const std::strin
293293
} );
294294
}
295295

296+
auto join_sentences( const std::vector<std::string> &sentences ) -> std::string
297+
{
298+
auto ret = std::string {};
299+
std::ranges::for_each( sentences, [&]( const std::string & sentence ) {
300+
if( sentence.empty() ) {
301+
return;
302+
}
303+
if( !ret.empty() ) {
304+
ret += ' ';
305+
}
306+
ret += sentence;
307+
} );
308+
return ret;
309+
}
310+
311+
auto primary_material_name( const std::vector<proc::part_fact> &facts ) -> std::string
312+
{
313+
if( std::ranges::any_of( facts, [&]( const proc::part_fact & fact ) {
314+
return has_material( fact, material_id( "steel" ) ) || has_material( fact, material_id( "iron" ) );
315+
} ) ) {
316+
return "metal";
317+
}
318+
if( std::ranges::any_of( facts, [&]( const proc::part_fact & fact ) {
319+
return has_material( fact, material_id( "bone" ) );
320+
} ) ) {
321+
return "bone";
322+
}
323+
if( std::ranges::any_of( facts, [&]( const proc::part_fact & fact ) {
324+
return has_material( fact, material_id( "wood" ) );
325+
} ) ) {
326+
return "wooden";
327+
}
328+
return "simple";
329+
}
330+
331+
auto sword_base_description( const std::string &name ) -> std::string
332+
{
333+
if( name == "nail sword" ) {
334+
return "A rough wooden sword stiffened with driven nails.";
335+
}
336+
if( name == "crude sword" ) {
337+
return "A crude sword pieced together from wood and scavenged scrap.";
338+
}
339+
if( name == "hand-forged sword" ) {
340+
return "A serviceable sword built around a forged metal blade.";
341+
}
342+
if( name == "bone sword" ) {
343+
return "A rough sword built around a sharpened bone blade.";
344+
}
345+
if( name == "2-by-sword" ) {
346+
return "A club-like sword carved from a sturdy length of wood.";
347+
}
348+
return "A makeshift sword assembled from scavenged parts.";
349+
}
350+
351+
auto sword_guard_phrase( const std::vector<proc::part_fact> &guard_facts ) -> std::string
352+
{
353+
if( guard_facts.empty() ) {
354+
return "no guard";
355+
}
356+
const auto material = primary_material_name( guard_facts );
357+
if( material == "metal" ) {
358+
return "a metal guard";
359+
}
360+
if( material == "bone" ) {
361+
return "a bone guard";
362+
}
363+
if( material == "wooden" ) {
364+
return "a wooden guard";
365+
}
366+
return "a simple guard";
367+
}
368+
369+
auto sword_grip_phrase( const std::vector<proc::part_fact> &grip_facts ) -> std::string
370+
{
371+
if( grip_facts.empty() ) {
372+
return {};
373+
}
374+
if( has_itype( grip_facts, itype_id( "leather" ) ) || std::ranges::any_of( grip_facts,
375+
[&]( const proc::part_fact & fact ) {
376+
return has_material( fact, material_id( "leather" ) );
377+
} ) ) {
378+
return "a leather-wrapped grip";
379+
}
380+
if( has_itype( grip_facts, itype_id( "rag" ) ) || std::ranges::any_of( grip_facts,
381+
[&]( const proc::part_fact & fact ) {
382+
return has_material( fact, material_id( "cotton" ) );
383+
} ) ) {
384+
return "a rag-wrapped grip";
385+
}
386+
return "a wrapped grip";
387+
}
388+
389+
auto sword_hilt_sentence( const std::vector<proc::part_fact> &guard_facts,
390+
const std::vector<proc::part_fact> &grip_facts ) -> std::string
391+
{
392+
const auto guard_phrase = sword_guard_phrase( guard_facts );
393+
const auto grip_phrase = sword_grip_phrase( grip_facts );
394+
if( grip_phrase.empty() ) {
395+
return string_format( "The hilt uses %s.", guard_phrase );
396+
}
397+
return string_format( "The hilt uses %s and %s.", guard_phrase, grip_phrase );
398+
}
399+
400+
auto sword_reinforcement_sentence( const std::vector<proc::part_fact> &reinforcement_facts ) ->
401+
std::string
402+
{
403+
const auto has_nails = has_itype( reinforcement_facts, itype_id( "nail" ) );
404+
const auto has_scrap = has_itype( reinforcement_facts, itype_id( "scrap" ) );
405+
if( has_nails && has_scrap ) {
406+
return "Driven nails and scrap reinforcement add stiffness at the cost of weight.";
407+
}
408+
if( has_nails ) {
409+
return "Driven nails add stiffness and a little puncturing power.";
410+
}
411+
if( has_scrap ) {
412+
return "Scrap reinforcement adds weight and stiffness.";
413+
}
414+
return {};
415+
}
416+
296417
auto matching_slot_uses( const proc::schema &sch, const proc::part_fact &fact ) -> int
297418
{
298419
auto uses = 0;
@@ -427,6 +548,20 @@ auto sword_name( const proc::schema &sch, const std::vector<proc::part_fact> &fa
427548
return "sword";
428549
}
429550

551+
auto sword_description( const proc::schema &sch, const std::vector<proc::part_fact> &facts,
552+
const std::vector<proc::craft_pick> &picks,
553+
const std::string &name ) -> std::string
554+
{
555+
const auto guard_facts = picked_facts_for_role( sch, facts, picks, "guard" );
556+
const auto grip_facts = picked_facts_for_role( sch, facts, picks, "grip" );
557+
const auto reinforcement_facts = picked_facts_for_role( sch, facts, picks, "reinforcement" );
558+
return join_sentences( {
559+
sword_base_description( name ),
560+
sword_hilt_sentence( guard_facts, grip_facts ),
561+
sword_reinforcement_sentence( reinforcement_facts ),
562+
} );
563+
}
564+
430565
auto sandwich_name( const proc::schema &sch, const std::vector<proc::part_fact> &facts,
431566
const std::vector<proc::craft_pick> &picks ) -> std::string
432567
{
@@ -577,6 +712,7 @@ auto sword_preview( const proc::schema &sch, const std::vector<proc::part_fact>
577712
blob.melee.to_hit = std::clamp( ( edge_score + bash_score ) / 18 - blob.mass_g / 900, -2, 4 );
578713
blob.melee.dur = std::max( 1, dur_score + blob.mass_g / 250 );
579714
blob.name = sword_name( sch, facts, picks );
715+
blob.description = sword_description( sch, facts, picks, blob.name );
580716
return blob;
581717
}
582718

src/proc_item.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ auto default_food_blob( const item &it,
135135
static_cast<double>( current_charges ) /
136136
static_cast<double>( sample_charges ) ) );
137137
blob.name = sample->type_name();
138+
blob.description = sample->type->description.translated();
138139
if( !sample->is_comestible() ) {
139140
return blob;
140141
}
@@ -168,6 +169,7 @@ auto default_weapon_blob( const itype_id &source_id ) -> proc::fast_blob
168169
blob.mass_g = units::to_gram( source.weight() );
169170
blob.volume_ml = units::to_milliliter( source.volume() );
170171
blob.name = source.type_name();
172+
blob.description = source.type->description.translated();
171173
blob.melee.bash = source.damage_melee( DT_BASH );
172174
blob.melee.cut = source.damage_melee( DT_CUT );
173175
blob.melee.stab = source.damage_melee( DT_STAB );
@@ -456,6 +458,7 @@ auto blob_table( sol::state &lua, const proc::fast_blob &blob ) -> sol::table
456458
tbl["volume_ml"] = blob.volume_ml;
457459
tbl["kcal"] = blob.kcal;
458460
tbl["name"] = blob.name;
461+
tbl["description"] = blob.description;
459462

460463
auto vit_tbl = lua.create_table();
461464
std::ranges::for_each( blob.vit, [&]( const std::pair<const vitamin_id, int> &entry ) {
@@ -517,6 +520,7 @@ auto parse_blob_into( proc::fast_blob &blob, const sol::table &tbl ) -> void
517520
blob.volume_ml = tbl.get_or( "volume_ml", blob.volume_ml );
518521
blob.kcal = tbl.get_or( "kcal", blob.kcal );
519522
blob.name = tbl.get_or<std::string>( "name", blob.name );
523+
blob.description = tbl.get_or<std::string>( "description", blob.description );
520524
const auto vit_obj = tbl.get_or<sol::object>( "vit", sol::lua_nil );
521525
if( vit_obj != sol::lua_nil && vit_obj.is<sol::table>() ) {
522526
blob.vit.clear();
@@ -696,6 +700,9 @@ auto proc::to_json( JsonOut &jsout, const payload &data ) -> void
696700
jsout.member( "volume_ml", data.blob.volume_ml );
697701
jsout.member( "kcal", data.blob.kcal );
698702
jsout.member( "name", data.blob.name );
703+
if( !data.blob.description.empty() ) {
704+
jsout.member( "description", data.blob.description );
705+
}
699706
jsout.member( "melee" );
700707
jsout.start_object();
701708
jsout.member( "bash", data.blob.melee.bash );
@@ -734,6 +741,7 @@ auto proc::from_json( JsonIn &jsin, payload &data ) -> void
734741
blob_jo.read( "volume_ml", data.blob.volume_ml );
735742
blob_jo.read( "kcal", data.blob.kcal );
736743
blob_jo.read( "name", data.blob.name );
744+
blob_jo.read( "description", data.blob.description );
737745
if( blob_jo.has_object( "melee" ) ) {
738746
const auto melee_jo = blob_jo.get_object( "melee" );
739747
melee_jo.read( "bash", data.blob.melee.bash );
@@ -863,6 +871,9 @@ auto proc::write_payload( item &it, const payload &data ) -> void
863871
if( !data.blob.name.empty() ) {
864872
it.set_var( "name", data.blob.name );
865873
}
874+
if( !data.blob.description.empty() ) {
875+
it.set_var( "description", data.blob.description );
876+
}
866877
if( it.is_comestible() && !data.blob.empty() ) {
867878
it.set_flag( flag_NUTRIENT_OVERRIDE );
868879
}

src/proc_types.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,11 @@ struct fast_blob {
9696
std::map<vitamin_id, int> vit;
9797
melee_blob melee;
9898
std::string name;
99+
std::string description;
99100

100101
auto empty() const -> bool {
101102
return mass_g == 0 && volume_ml == 0 && kcal == 0 && vit.empty() && melee.empty() &&
102-
name.empty();
103+
name.empty() && description.empty();
103104
}
104105

105106
auto operator==( const fast_blob & ) const -> bool = default;

tests/proc_builder_test.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,17 +825,28 @@ TEST_CASE( "proc_builder_supports_sword_reinforcements_and_legacy_like_names",
825825

826826
const auto plain = base_state();
827827
CHECK( plain.fast.name == "2-by-sword" );
828+
CHECK( plain.fast.description ==
829+
"A club-like sword carved from a sturdy length of wood. "
830+
"The hilt uses no guard and a rag-wrapped grip." );
828831

829832
auto nailed = base_state();
830833
REQUIRE( proc::add_pick( nailed, sch, proc::slot_id( "reinforcement" ), 4 ) );
831834
REQUIRE( proc::add_pick( nailed, sch, proc::slot_id( "reinforcement" ), 4 ) );
832835
CHECK( nailed.fast.name == "nail sword" );
836+
CHECK( nailed.fast.description ==
837+
"A rough wooden sword stiffened with driven nails. "
838+
"The hilt uses no guard and a rag-wrapped grip. "
839+
"Driven nails add stiffness and a little puncturing power." );
833840
CHECK( nailed.fast.melee.cut > plain.fast.melee.cut );
834841
CHECK( nailed.fast.melee.dur >= plain.fast.melee.dur );
835842

836843
auto crude = base_state();
837844
REQUIRE( proc::add_pick( crude, sch, proc::slot_id( "reinforcement" ), 5 ) );
838845
CHECK( crude.fast.name == "crude sword" );
846+
CHECK( crude.fast.description ==
847+
"A crude sword pieced together from wood and scavenged scrap. "
848+
"The hilt uses no guard and a rag-wrapped grip. "
849+
"Scrap reinforcement adds weight and stiffness." );
839850
CHECK( crude.fast.melee.cut > plain.fast.melee.cut );
840851
CHECK( crude.fast.mass_g > plain.fast.mass_g );
841852
}

tests/proc_lua_test.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ TEST_CASE( "proc_lua_full_bridge_reads_named_function", "[proc][lua]" )
2020
mass_g = 333,
2121
volume_ml = 222,
2222
name = "lua sandwich",
23+
description = "lua description",
2324
vit = { vitC = 12 }
2425
}
2526
end,
@@ -49,6 +50,7 @@ TEST_CASE( "proc_lua_full_bridge_reads_named_function", "[proc][lua]" )
4950
CHECK( full.data.mass_g == 333 );
5051
CHECK( full.data.volume_ml == 222 );
5152
CHECK( full.data.name == "lua sandwich" );
53+
CHECK( full.data.description == "lua description" );
5254
CHECK( full.data.vit.at( vitamin_id( "vitC" ) ) == 12 );
5355

5456
auto opts = proc::make_opts{};
@@ -76,10 +78,12 @@ TEST_CASE( "proc_lua_missing_runtime_keeps_preview_blob", "[proc][lua]" )
7678
preview.mass_g = 60;
7779
preview.volume_ml = 125;
7880
preview.name = "fallback sandwich";
81+
preview.description = "fallback description";
7982

8083
const auto full = proc::run_full( sch, { fact }, preview );
8184
CHECK( full.data.kcal == 90 );
8285
CHECK( full.data.mass_g == 60 );
8386
CHECK( full.data.volume_ml == 125 );
8487
CHECK( full.data.name == "fallback sandwich" );
88+
CHECK( full.data.description == "fallback description" );
8589
}

tests/proc_make_test.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,12 @@ TEST_CASE( "proc_make_item_applies_weapon_blob_to_item", "[proc][make][weapon]"
309309
CHECK( proc::blob_melee( *made )->bash > 0 );
310310
CHECK( made->damage_melee( DT_STAB ) == proc::blob_melee( *made )->stab );
311311
CHECK( made->type_name() == "hand-forged sword" );
312+
CHECK( proc::read_payload( *made )->blob.description ==
313+
"A serviceable sword built around a forged metal blade. "
314+
"The hilt uses no guard and a rag-wrapped grip." );
315+
CHECK( made->get_var( "description" ) ==
316+
"A serviceable sword built around a forged metal blade. "
317+
"The hilt uses no guard and a rag-wrapped grip." );
312318
REQUIRE( proc::read_payload( *made )->parts.size() == 3 );
313319
CHECK( proc::read_payload( *made )->parts[0].role == "blade" );
314320
}

tests/proc_payload_test.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ TEST_CASE( "proc_payload_round_trips_through_item_save", "[proc][payload]" )
7272
payload.blob.mass_g = 220;
7373
payload.blob.volume_ml = 330;
7474
payload.blob.name = "meat sandwich";
75+
payload.blob.description = "A sandwich payload used for save testing.";
7576
payload.blob.melee.stab = 9;
7677
payload.servings = 6;
7778
auto part = proc::compact_part{};
@@ -269,6 +270,7 @@ TEST_CASE( "legacy_sandwiches_gain_proc_payload_on_save_load", "[proc][payload][
269270
CHECK( restored->typeId() == itype_id( "sandwich_generic" ) );
270271
CHECK( restored->type_name() == legacy.type_name() );
271272
CHECK( payload->blob.name == legacy.type_name() );
273+
CHECK( payload->blob.description == legacy.type->description.translated() );
272274
CHECK( payload->blob.mass_g == units::to_gram( legacy.weight() ) );
273275
CHECK( payload->blob.volume_ml == units::to_milliliter( legacy.volume() ) );
274276
CHECK( payload->servings == std::max( legacy.charges, 1 ) );
@@ -391,6 +393,7 @@ TEST_CASE( "legacy_swords_gain_proc_payload_on_save_load", "[proc][payload][migr
391393
CHECK( restored->typeId() == itype_id( "proc_sword_generic" ) );
392394
CHECK( restored->type_name() == legacy.type_name() );
393395
CHECK( payload->blob.name == legacy.type_name() );
396+
CHECK( payload->blob.description == legacy.type->description.translated() );
394397
CHECK( payload->blob.mass_g == units::to_gram( legacy.weight() ) );
395398
CHECK( payload->blob.volume_ml == units::to_milliliter( legacy.volume() ) );
396399
CHECK( units::to_gram( restored->weight() ) == units::to_gram( legacy.weight() ) );

0 commit comments

Comments
 (0)