Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions apps/save-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- The attribute list offered a second "Magic Circle", identically named to the
one under Skills but without any effect in the game. It is gone; the circle is
set under Skills.
- Version 1.2.1 said an NPC's position cannot be changed because the game
restores it from the level. That was wrong.
- Removing an item from a list and then editing another item in the same list
Expand Down
12 changes: 11 additions & 1 deletion apps/save-editor/lib/features/editor/domain/hero_attributes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ const heroCoreAttributeOrder = [
'Level',
'Experience',
'SkillPoints',
'MagicianLevel',
];

// The per-weapon critical-hit values used to have their own "Kampffertigkeiten"
Expand All @@ -67,11 +66,22 @@ const heroCombatAttributes = <String>[];
/// Attribute ids hidden from the curated hero/NPC attribute view (the game
/// derives these from the learned skills, so editing them by hand is
/// misleading). They remain reachable in the All-data property browser.
///
/// Each one is the attribute a `GE_Skill_*` class raises, and the game
/// re-derives it from that class when the savegame is loaded: a save edited so
/// that only the Magic Circle CLASS said circle 6 — while MagicianLevel still
/// said -1 — let the hero use a circle 4 rune in game, and rune usability is
/// stated against MagicianLevel. So the value written here never survives the
/// load, and the skill's own control (Talente) is the only one that works.
const heroHiddenAttributeIds = <String>{
'Critical_Fists',
'Critical_OneHand',
'Critical_TwoHand',
'Critical_Orc',
// Magic Circle. Its label collided with the Talente row's, so the Attribute
// tab showed two identical "Magischer Kreis" controls, only one of which did
// anything.
'MagicianLevel',
Comment thread
dh0er marked this conversation as resolved.
};

const heroResistanceAttributes = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,32 @@ void main() {
expect(heroAttributeGroup('Critical_OneHand'), HeroAttributeGroup.advanced);
expect(heroAttributeGroup('Resistance_Fire'), HeroAttributeGroup.resistances);
expect(heroAttributeGroup('PickPocketing'), HeroAttributeGroup.thieving);
expect(heroAttributeGroup('MagicianLevel'), HeroAttributeGroup.advanced);
expect(heroAttributeGroup('Swampweed'), HeroAttributeGroup.advanced);
expect(heroAttributeGroup('SomeFutureAttribute'), HeroAttributeGroup.advanced);
});

test('drops attributes the game derives from a learned skill', () {
// The game re-derives each of these from the skill's GameplayEffect class
// when the save is loaded, so a hand-edited value never survives — proven in
// game for the Magic Circle: a save whose class said circle 6 while
// MagicianLevel still said -1 let the hero use a circle 4 rune. Offering
// them here would be a control that silently does nothing, and MagicianLevel
// carried the same label as the Talente row on top of that.
final attributes = parseHeroAttributes([
_heroHit('/Script/G1R.AttributeSet_Mana', 'MagicianLevel', 'BaseValue', '0'),
_heroHit('/Script/G1R.AttributeSet_Mana', 'MagicianLevel', 'CurrentValue', '6'),
_heroHit('/Script/G1R.AttributeSet_Strength', 'Critical_OneHand', 'BaseValue', '0'),
_heroHit('/Script/G1R.AttributeSet_Strength', 'Critical_Fists', 'BaseValue', '0'),
_heroHit('/Script/G1R.AttributeSet_Strength', 'Critical_TwoHand', 'BaseValue', '0'),
_heroHit('/Script/G1R.AttributeSet_Strength', 'Critical_Orc', 'BaseValue', '0'),
_heroHit('/Script/G1R.AttributeSet_Mana', 'MaxMana', 'BaseValue', '35'),
]);

// Only the one attribute the game does NOT derive from a skill survives.
expect(attributes.map((a) => a.id), ['MaxMana']);
});

test('sorts core attributes in display order before unknown ones', () {
final attributes = parseHeroAttributes([
_heroHit('/Script/G1R.AttributeSet_Strength', 'Strength', 'BaseValue', '10'),
Expand Down
Loading