Skip to content

Commit c69a1c6

Browse files
feat: make death drops extendable (#9755)
* feat: support appendable monster death drops Assisted-by: pi:gpt-5.5 Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> * docs: translate appendable death drop docs Assisted-by: pi:gpt-5.5 Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --------- Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
1 parent 78a725c commit c69a1c6

8 files changed

Lines changed: 140 additions & 9 deletions

File tree

data/mods/TEST_DATA/monsters.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,45 @@
2525
"species": [ "ZOMBIE", "HUMAN" ],
2626
"symbol": "Z",
2727
"upgrades": { "age_grow": 14, "into": "mon_zombie" }
28+
},
29+
{
30+
"type": "item_group",
31+
"id": "test_monster_death_drop_base",
32+
"subtype": "collection",
33+
"items": [ "rock" ]
34+
},
35+
{
36+
"type": "item_group",
37+
"id": "test_monster_death_drop_append",
38+
"subtype": "collection",
39+
"items": [ "stick" ]
40+
},
41+
{
42+
"id": "mon_test_death_drops_append",
43+
"copy-from": "debug_mon",
44+
"type": "MONSTER",
45+
"name": { "str": "death drop append test monster" },
46+
"description": "This monster exists only for testing purposes.",
47+
"death_drops": "test_monster_death_drop_base"
48+
},
49+
{
50+
"id": "mon_test_death_drops_append",
51+
"copy-from": "mon_test_death_drops_append",
52+
"type": "MONSTER",
53+
"extend": { "death_drops": "test_monster_death_drop_append" }
54+
},
55+
{
56+
"id": "mon_test_death_drops_clear",
57+
"copy-from": "debug_mon",
58+
"type": "MONSTER",
59+
"name": { "str": "death drop clear test monster" },
60+
"description": "This monster exists only for testing purposes.",
61+
"death_drops": "test_monster_death_drop_base"
62+
},
63+
{
64+
"id": "mon_test_death_drops_clear",
65+
"copy-from": "mon_test_death_drops_clear",
66+
"type": "MONSTER",
67+
"death_drops": ""
2868
}
2969
]

docs/en/mod/json/reference/creatures/monsters.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,8 @@ Can freely be a collection to imply two weapons wielded in both hands, as items
386386
An item group that is used to spawn items when the monster dies. This can be an inlined item group,
387387
see ITEM_SPAWN.md. The default subtype is "distribution".
388388

389+
Use `"extend": { "death_drops": ... }` in an inherited or overridden monster to add another drop group without replacing existing death drops. A top-level `"death_drops"` member replaces inherited death drops.
390+
389391
## "death_function"
390392

391393
(array of strings, optional)

docs/ja/mod/json/reference/creatures/monsters.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ BNのゲームでは、以下の種別が使用可能です: HUMAN(人間)、R
287287

288288
モンスター死亡時にアイテムを生成するために使用されるアイテムグループです。インライン形式のアイテムグループも使用可能です(詳細は ITEM_SPAWN.md を参照)。デフォルトのサブタイプは "distribution"(分布)です。
289289

290+
継承または上書きされたモンスターで `"extend": { "death_drops": ... }` を使うと、既存の死亡時ドロップを置き換えずに別のドロップグループを追加できます。トップレベルの `"death_drops"` メンバーは、継承された死亡時ドロップを置き換えます。
291+
290292
## "death_function"
291293

292294
(文字列の配列、オプション)

docs/ko/mod/json/reference/creatures/monsters.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ Ctxt는 동음이의어(같은 이름을 가진 두 가지 다른 것)의 경우
288288

289289
몬스터가 죽을 때 아이템을 스폰하는 데 사용되는 아이템 그룹. 이것은 인라인 아이템 그룹이 될 수 있습니다. ITEM_SPAWN.md를 참조하세요. 기본 하위 유형은 "distribution"입니다.
290290

291+
상속되거나 오버라이드된 몬스터에서 `"extend": { "death_drops": ... }`를 사용하면 기존 사망 드롭을 대체하지 않고 다른 드롭 그룹을 추가합니다. 최상위 `"death_drops"` 멤버는 상속된 사망 드롭을 대체합니다.
292+
291293
## "death_function"
292294

293295
(array of strings, optional)

src/monster.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
#include <algorithm>
44
#include <cmath>
5+
#include <iterator>
56
#include <limits>
67
#include <memory>
78
#include <optional>
9+
#include <ranges>
810
#include <tuple>
911
#include <unordered_map>
1012
#include <unordered_set>
@@ -3514,12 +3516,17 @@ void monster::drop_items_on_death()
35143516
if( is_hallucination() ) {
35153517
return;
35163518
}
3517-
if( !type->death_drops ) {
3519+
if( type->death_drops.empty() ) {
35183520
return;
35193521
}
35203522

3521-
auto items = item_group::items_from( type->death_drops,
3523+
auto items = item_group::items_from( type->death_drops.front(),
35223524
calendar::start_of_cataclysm );
3525+
for( const item_group_id &death_drop : type->death_drops | std::views::drop( 1 ) ) {
3526+
auto group_items = item_group::items_from( death_drop,
3527+
calendar::start_of_cataclysm );
3528+
std::ranges::move( group_items, std::back_inserter( items ) );
3529+
}
35233530

35243531
// Apply both global and category-specific spawn rates
35253532
const auto global_spawn_rate = get_option<float>( "ITEM_SPAWNRATE" );

src/monstergenerator.cpp

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <optional>
88
#include <set>
99
#include <utility>
10+
#include <vector>
1011

1112
#include "assign.h"
1213
#include "bodypart.h"
@@ -220,6 +221,25 @@ std::string enum_to_string<m_flag>( m_flag data )
220221

221222
} // namespace io
222223

224+
namespace
225+
{
226+
227+
auto add_death_drop_group( std::vector<item_group_id> &death_drops,
228+
const JsonObject &jo, const std::string &member_name ) -> void
229+
{
230+
if( !jo.has_member( member_name ) ) {
231+
return;
232+
}
233+
234+
const auto death_drop = item_group::load_item_group( jo.get_member( member_name ),
235+
"distribution" );
236+
if( death_drop ) {
237+
death_drops.push_back( death_drop );
238+
}
239+
}
240+
241+
} // namespace
242+
223243
// TODO: Make this like any other generic factory so we can use type_id_implement
224244
/** @relates string_id */
225245
template<>
@@ -892,8 +912,13 @@ void mtype::load( const JsonObject &jo, const std::string &src )
892912
"distribution" );
893913
}
894914
if( jo.has_member( "death_drops" ) ) {
895-
death_drops = item_group::load_item_group( jo.get_member( "death_drops" ),
896-
"distribution" );
915+
death_drops.clear();
916+
add_death_drop_group( death_drops, jo, "death_drops" );
917+
}
918+
if( jo.has_object( "extend" ) ) {
919+
auto tmp = jo.get_object( "extend" );
920+
tmp.allow_omitted_members();
921+
add_death_drop_group( death_drops, tmp, "death_drops" );
897922
}
898923

899924
assign( jo, "harvest", harvest );
@@ -1561,9 +1586,11 @@ void MonsterGenerator::check_monster_definitions() const
15611586
debugmsg( "monster %s has invalid species %s", mon.id.c_str(), spec.c_str() );
15621587
}
15631588
}
1564-
if( mon.death_drops && !item_group::group_is_defined( mon.death_drops ) ) {
1565-
debugmsg( "monster %s has unknown death drop item group: %s", mon.id.c_str(),
1566-
mon.death_drops.c_str() );
1589+
for( const item_group_id &death_drop : mon.death_drops ) {
1590+
if( !item_group::group_is_defined( death_drop ) ) {
1591+
debugmsg( "monster %s has unknown death drop item group: %s", mon.id.c_str(),
1592+
death_drop.c_str() );
1593+
}
15671594
}
15681595
for( auto &m : mon.mat ) {
15691596
if( m.str() == "null" || !m.is_valid() ) {

src/mtype.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,8 @@ struct mtype {
288288
mtype_id id;
289289

290290
std::map<itype_id, int> starting_ammo; // Amount of ammo the monster spawns with.
291-
// Name of item group that is used to create item dropped upon death, or empty.
292-
item_group_id death_drops;
291+
// Names of item groups used to create items dropped upon death.
292+
std::vector<item_group_id> death_drops;
293293

294294
/** Stores effect data for effects placed on attack */
295295
std::vector<mon_effect_data> atk_effs;

tests/monster_test.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
#include "player.h"
1919
#include "state_helpers.h"
2020
#include "test_statistics.h"
21+
#include "type_id.h"
2122
#include "vehicle.h"
2223
#include "vehicle_part.h"
2324
#include "vpart_position.h"
2425

26+
#include <algorithm>
2527
#include <cmath>
2628
#include <fstream>
2729
#include <list>
@@ -35,6 +37,55 @@
3537

3638
using move_statistics = statistics<int>;
3739

40+
namespace {
41+
42+
auto count_items_at(const tripoint_bub_ms& pos, const itype_id& type) -> int {
43+
return std::ranges::count_if(get_map().i_at(pos), [&type](const auto* it) {
44+
return it->typeId() == type;
45+
});
46+
}
47+
48+
} // namespace
49+
50+
TEST_CASE("extended monster death drops append to inherited drops", "[monster][death_drops]") {
51+
clear_all_state();
52+
const auto global_spawn_rate = override_option("ITEM_SPAWNRATE", "1.0");
53+
const auto rock_spawn_rate = override_option("SPAWN_RATE_rocks", "1.0");
54+
const auto wood_spawn_rate = override_option("SPAWN_RATE_scrap_wood", "1.0");
55+
static_cast<void>(global_spawn_rate);
56+
static_cast<void>(rock_spawn_rate);
57+
static_cast<void>(wood_spawn_rate);
58+
move_player_out_of_the_way();
59+
60+
auto& here = get_map();
61+
build_test_map(ter_id("t_floor"));
62+
63+
const auto monster_pos = tripoint_bub_ms(60, 60, 0);
64+
here.i_clear(monster_pos);
65+
66+
auto& test_monster = spawn_test_monster("mon_test_death_drops_append", monster_pos);
67+
test_monster.drop_items_on_death();
68+
69+
CHECK(count_items_at(monster_pos, itype_id("rock")) == 1);
70+
CHECK(count_items_at(monster_pos, itype_id("stick")) == 1);
71+
}
72+
73+
TEST_CASE("empty top-level monster death drops replace inherited drops", "[monster][death_drops]") {
74+
clear_all_state();
75+
move_player_out_of_the_way();
76+
77+
auto& here = get_map();
78+
build_test_map(ter_id("t_floor"));
79+
80+
const auto monster_pos = tripoint_bub_ms(60, 60, 0);
81+
here.i_clear(monster_pos);
82+
83+
auto& test_monster = spawn_test_monster("mon_test_death_drops_clear", monster_pos);
84+
test_monster.drop_items_on_death();
85+
86+
CHECK(here.i_at(monster_pos).empty());
87+
}
88+
3889
TEST_CASE("hallucination_monsters_do_not_open_real_doors", "[monster][hallucination]") {
3990
clear_all_state();
4091
move_player_out_of_the_way();

0 commit comments

Comments
 (0)