undo/redo for editing/clearing monster types

This commit is contained in:
2025-06-05 11:30:27 -05:00
parent 721cc3ab6c
commit 2c3751284d
7 changed files with 201 additions and 7 deletions

View File

@@ -0,0 +1,12 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<!-- NOTE: This file should be updated to use relative positioning the next time it changes. -->
<?xml-stylesheet href="dialog.xsl" type="text/xsl"?>
<dialog defbtn='keep' escbtn='cancel'>
<pict type='dlog' num='7' top='6' left='6'/>
<text name='keep-msg' top='6' left='49' width='256' height='32'>
Keep changes to {{monst}} before editing another monster?
</text>
<button name='cancel' type='regular' top='43' left='109'>Cancel</button>
<button name='revert' type='regular' top='43' left='175'>Discard</button>
<button name='keep' type='regular' top='43' left='240'>Keep</button>
</dialog>

View File

@@ -928,3 +928,110 @@ eMonstAbil uAbility::readFrom(const cTagFile_Page& page) {
return key;
}
bool cMonster::operator==(const cMonster& other) {
CHECK_EQ(other, level);
CHECK_EQ(other, m_name);
CHECK_EQ(other, m_health);
CHECK_EQ(other, armor);
CHECK_EQ(other, skill);
// attacks
for(int i = 0; i < a.size(); ++i){
CHECK_EQ(other, a[i].dice);
CHECK_EQ(other, a[i].sides);
CHECK_EQ(other, a[i].type);
}
CHECK_EQ(other, m_type);
CHECK_EQ(other, speed);
CHECK_EQ(other, mu);
CHECK_EQ(other, cl);
CHECK_EQ(other, treasure);
// abilities
CHECK_EQ(other, abil.size()); // TODO will NO_ABIL ever be a key? If so, it should be ignored.
for(auto ability : abil){
if(other.abil.find(ability.first) == other.abil.end()) return false;
uAbility mine = ability.second;
uAbility theirs = other.abil[ability.first];
// compare all ability types
switch(getMonstAbilCategory(ability.first)){
case eMonstAbilCat::INVALID: break;
case eMonstAbilCat::MISSILE:
if(mine.missile.active != theirs.missile.active) return false;
if(mine.missile.type != theirs.missile.type) return false;
if(mine.missile.pic != theirs.missile.pic) return false;
if(mine.missile.dice != theirs.missile.dice) return false;
if(mine.missile.sides != theirs.missile.sides) return false;
if(mine.missile.skill != theirs.missile.skill) return false;
if(mine.missile.range != theirs.missile.range) return false;
if(mine.missile.odds != theirs.missile.odds) return false;
break;
case eMonstAbilCat::GENERAL:
if(mine.gen.active != theirs.gen.active) return false;
if(mine.gen.type != theirs.gen.type) return false;
if(mine.gen.pic != theirs.gen.pic) return false;
if(mine.gen.strength != theirs.gen.strength) return false;
if(mine.gen.range != theirs.gen.range) return false;
if(mine.gen.odds != theirs.gen.odds) return false;
switch(ability.first){
case eMonstAbil::FIELD:
if(mine.gen.fld != theirs.gen.fld) return false;
break;
case eMonstAbil::DAMAGE: case eMonstAbil::DAMAGE2:
if(mine.gen.dmg != theirs.gen.dmg) return false;
break;
case eMonstAbil::STATUS: case eMonstAbil::STATUS2:
if(mine.gen.stat != theirs.gen.stat) return false;
break;
default: break;
}
break;
case eMonstAbilCat::SUMMON:
if(mine.summon.active != theirs.summon.active) return false;
if(mine.summon.type != theirs.summon.type) return false;
if(mine.summon.what != theirs.summon.what) return false;
if(mine.summon.min != theirs.summon.min) return false;
if(mine.summon.max != theirs.summon.max) return false;
if(mine.summon.len != theirs.summon.len) return false;
if(mine.summon.chance != theirs.summon.chance) return false;
break;
case eMonstAbilCat::RADIATE:
if(mine.radiate.active != theirs.radiate.active) return false;
if(mine.radiate.type != theirs.radiate.type) return false;
if(mine.radiate.chance != theirs.radiate.chance) return false;
if(mine.radiate.pat != theirs.radiate.pat) return false;
break;
case eMonstAbilCat::SPECIAL:
if(mine.special.active != theirs.special.active) return false;
if(mine.special.extra1 != theirs.special.extra1) return false;
if(mine.special.extra2 != theirs.special.extra2) return false;
if(mine.special.extra3 != theirs.special.extra3) return false;
break;
}
}
CHECK_EQ(other, corpse_item);
CHECK_EQ(other, corpse_item_chance);
// resistances
CHECK_EQ(other, resist.size());
for(auto resistance : resist){
if(other.resist.find(resistance.first) == other.resist.end()) return false;
if(resistance.second != other.resist[resistance.first]) return false;
}
CHECK_EQ(other, mindless);
CHECK_EQ(other, invuln);
CHECK_EQ(other, invisible);
CHECK_EQ(other, guard);
CHECK_EQ(other, amorphous);
CHECK_EQ(other, x_width);
CHECK_EQ(other, y_width);
CHECK_EQ(other, default_attitude);
CHECK_EQ(other, summon_type);
CHECK_EQ(other, default_facial_pic);
CHECK_EQ(other, picture_num);
CHECK_EQ(other, ambient_sound);
CHECK_EQ(other, see_spec);
return true;
}

View File

@@ -61,7 +61,8 @@ public:
mutable std::map<eMonstAbil, uAbility> abil;
item_num_t corpse_item;
short corpse_item_chance;
std::map<eDamageType, int> resist;
// HACK: This is only really marked mutable so that I can use operator[] from const methods
mutable std::map<eDamageType, int> resist;
bool mindless, invuln, invisible, guard, amorphous;
unsigned int x_width,y_width;
eAttitude default_attitude;
@@ -71,6 +72,10 @@ public:
snd_num_t ambient_sound; // has a chance of being played every move
spec_num_t see_spec;
// For detecting actual changes to types in the scenario editor
bool operator==(const cMonster& other);
bool operator!=(const cMonster& other) { return !(*this == other); }
std::map<eMonstAbil,uAbility>::iterator addAbil(eMonstAbilTemplate what, int param = 0);
int addAttack(unsigned short dice, unsigned short sides, eMonstMelee type = eMonstMelee::SWING);

View File

@@ -1320,8 +1320,12 @@ static bool handle_terpal_action(location cur_point, bool option_hit) {
// option-click type that can't be deleted (because it would break other types' numbers, or is in use somewhere):
// reset type info
else{
scenario.scen_monsters[i] = cMonster();
scenario.scen_monsters[i].m_name = "Unused Monster";
cMonster before = scenario.scen_monsters[i];
cMonster after;
after.m_name = "Unused Monster";
scenario.scen_monsters[i] = after;
undo_list.add(action_ptr(new aEditClearMonster("Clear Monster Type", i, before, after)));
update_edit_menu();
}
break;
case DRAW_ITEM:

View File

@@ -823,28 +823,63 @@ static bool save_monst_info(cDialog& me, cMonster& monst) {
static bool edit_monst_type_event_filter(cDialog& me,std::string hit,cMonster& monst,short& which) {
short i;
short which_before = which;
cMonster temp_monst;
bool commit_changes = false;
if(hit == "okay") {
if(save_monst_info(me,monst)) {
scenario.scen_monsters[which] = monst;
if(monst != scenario.scen_monsters[which]){
commit_changes = true;
}
me.toast(true);
}
} else if(hit == "abils") {
if(!save_monst_info(me,monst)) return false;
temp_monst = edit_monst_abil(monst,which,me);
if(temp_monst.level < 255)
bool abil_changed = (temp_monst != monst);
// Canceling the monster abilities editor sets the temp monster level to 255 as a flag.
// This should be fine unless we ever increase the max monster level (40) by a LOT.
if(abil_changed && temp_monst.level < 255){
monst = temp_monst;
put_monst_info_in_dlog(me,monst,which);
put_monst_info_in_dlog(me,monst,which);
// TODO should probably show in the monster editor that there are now unsaved ability changes.
}
} else if(hit == "left") {
if(!save_monst_info(me,monst)) return false;
scenario.scen_monsters[which] = monst;
// TODO run focus handlers!
if(monst != scenario.scen_monsters[which]){
// Confirm keeping changes
cChoiceDlog dlog("confirm-edit-monst", {"keep","revert","cancel"}, &me);
dlog->getControl("keep-msg").replaceText("{{monst}}", monst.m_name);
std::string choice = dlog.show();
if(choice == "keep"){
commit_changes = true;
}else if(choice == "cancel"){
return true;
}
}
which--;
if(which < 1) which = scenario.scen_monsters.size() - 1;
monst = scenario.scen_monsters[which];
put_monst_info_in_dlog(me,monst,which);
} else if(hit == "right") {
if(!save_monst_info(me,monst)) return false;
// TODO run focus handlers!
if(monst != scenario.scen_monsters[which]){
// Confirm keeping changes
cChoiceDlog dlog("confirm-edit-monst", {"keep","revert","cancel"}, &me);
dlog->getControl("keep-msg").replaceText("{{monst}}", monst.m_name);
std::string choice = dlog.show();
if(choice == "keep"){
commit_changes = true;
}else if(choice == "cancel"){
return true;
}
}
scenario.scen_monsters[which] = monst;
which++;
if(which >= scenario.scen_monsters.size()) which = 1;
@@ -891,6 +926,13 @@ static bool edit_monst_type_event_filter(cDialog& me,std::string hit,cMonster& m
put_monst_info(monstInfo, monst, scenario);
monstInfo.run();
}
if(commit_changes){
undo_list.add(action_ptr(new aEditClearMonster("Edit Monster Type", which_before, scenario.scen_monsters[which_before], monst)));
update_edit_menu();
scenario.scen_monsters[which_before] = monst;
}
return true;
}

View File

@@ -122,6 +122,18 @@ bool aEditClearTerrain::redo_me() {
return true;
}
bool aEditClearMonster::undo_me() {
// TODO show the type
scenario.scen_monsters[which] = before;
return true;
}
bool aEditClearMonster::redo_me() {
// TODO show the type
scenario.scen_monsters[which] = after;
return true;
}
bool aCreateDeleteMonster::undo_me() {
// TODO if not in MODE_EDIT_TYPES, show it
for(cMonster monst : monsters){

View File

@@ -157,4 +157,16 @@ public:
cAction(name), which(which), before(before), after(after) {}
};
/// Action which edits or clears a monster type
class aEditClearMonster : public cAction {
mon_num_t which;
cMonster before;
cMonster after;
bool undo_me() override;
bool redo_me() override;
public:
aEditClearMonster(std::string name, mon_num_t which, cMonster before, cMonster after) :
cAction(name), which(which), before(before), after(after) {}
};
#endif