Improve glyph preset loading (fixes #3312)

This commit is contained in:
SpectralFlame 2023-05-29 21:43:54 -05:00 committed by cyip92
parent 2a433149d6
commit 0e96cda876
3 changed files with 120 additions and 77 deletions

View File

@ -60,65 +60,98 @@ export default {
const name = this.names[id] === "" ? "" : `: ${this.names[id]}`;
return `Glyph Preset #${id + 1}${name}`;
},
// Let the player load if the currently equipped glyphs are a subset of the preset
canLoadSet(set) {
let toLoad = [...set];
let currActive = [...Glyphs.active.filter(g => g)];
for (const targetGlyph of currActive) {
const matchingGlyph = Glyphs.findByValues(targetGlyph, toLoad, {
level: this.level ? -1 : 0,
strength: this.rarity ? -1 : 0,
effects: this.effects ? -1 : 0
});
if (!matchingGlyph) return false;
toLoad = toLoad.filter(g => g !== matchingGlyph);
currActive = currActive.filter(g => g !== targetGlyph);
}
return toLoad.length > 0;
},
saveGlyphSet(id) {
if (!this.hasEquipped || player.reality.glyphs.sets[id].glyphs.length) return;
player.reality.glyphs.sets[id].glyphs = Glyphs.active.compact();
this.refreshGlyphSets();
EventHub.dispatch(GAME_EVENT.GLYPH_SET_SAVE_CHANGE);
},
loadGlyphSet(set) {
if (!this.canLoadSet(set) || !this.setLengthValid(set)) return;
// A proper full solution to this turns out to contain an NP-hard problem as a subproblem, so instead we do
// somwthing which should work in most cases - we match greedily when it won't obviously lead to an incomplete
// preset match, and leniently when matching greedily may lead to an incomplete set being loaded
loadGlyphSet(set, id) {
if (!this.setLengthValid(set)) return;
let glyphsToLoad = [...set];
const activeGlyphs = [...Glyphs.active.filter(g => g)];
// If we already have a subset of the preset loaded, don't try to load glyphs from that subset again
let toLoad = [...set];
let currActive = [...Glyphs.active.filter(g => g)];
for (const targetGlyph of currActive) {
const matchingGlyph = Glyphs.findByValues(targetGlyph, toLoad, {
// Create an array where each entry contains a single active glyph and all its matches in the preset which it
// could fill in for, based on the preset loading settings
const activeOptions = [];
for (const glyph of activeGlyphs) {
const options = Glyphs.findByValues(glyph, glyphsToLoad, {
level: this.level ? -1 : 0,
strength: this.rarity ? -1 : 0,
effects: this.effects ? -1 : 0
});
if (!matchingGlyph) continue;
toLoad = toLoad.filter(g => g !== matchingGlyph);
currActive = currActive.filter(g => g !== targetGlyph);
activeOptions.push({ glyph, options });
}
// Try to load the rest from the inventory
let missingGlyphs = 0;
for (const targetGlyph of toLoad) {
const matchingGlyph = Glyphs.findByValues(targetGlyph, Glyphs.sortedInventoryList, {
level: this.level ? 1 : 0,
strength: this.rarity ? 1 : 0,
effects: this.effects ? 1 : 0
// Using the active glyphs one by one, select matching to-be-loaded preset glyphs to be removed from the list.
// This makes sure the inventory doesn't attempt to match a glyph which is already satisfied by an equipped one
const selectedFromActive = this.findSelectedGlyphs(activeOptions, 5);
for (const glyph of selectedFromActive) glyphsToLoad = glyphsToLoad.filter(g => g !== glyph);
// For the remaining glyphs to load from the preset, find all their appropriate matches within the inventory.
// This is largely the same as earlier with the equipped glyphs
const remainingOptions = [];
for (let index = 0; index < glyphsToLoad.length; index++) {
const glyph = glyphsToLoad[index];
const options = Glyphs.findByValues(glyph, Glyphs.sortedInventoryList, {
level: this.level ? -1 : 0,
strength: this.rarity ? -1 : 0,
effects: this.effects ? -1 : 0
});
if (!matchingGlyph) {
missingGlyphs++;
continue;
}
remainingOptions[index] = { glyph, options };
}
// This is scanned through similarly to the active slot glyphs, except we need to make sure we don't try to
// match more glyphs than we have room for
const selectedFromInventory = this.findSelectedGlyphs(remainingOptions,
Glyphs.active.countWhere(g => g === null));
for (const glyph of selectedFromInventory) glyphsToLoad = glyphsToLoad.filter(g => g !== glyph);
// Actually equip the glyphs and then notify how successful (or not) the loading was
let missingGlyphs = glyphsToLoad.length;
for (const glyph of selectedFromInventory) {
const idx = Glyphs.active.indexOf(null);
if (idx !== -1) Glyphs.equip(matchingGlyph, idx);
if (idx !== -1) {
Glyphs.equip(glyph, idx);
missingGlyphs--;
}
}
if (missingGlyphs) {
GameUI.notify.error(`Could not find ${missingGlyphs} ${pluralize("Glyph", missingGlyphs)} to load from
Glyph preset.`);
if (missingGlyphs > 0) {
GameUI.notify.error(`Could not find or equip ${missingGlyphs} ${pluralize("Glyph", missingGlyphs)} from
${this.setName(id)}.`);
} else {
GameUI.notify.success(`Successfully loaded ${this.setName(id)}.`);
}
EventHub.dispatch(GAME_EVENT.GLYPH_SET_SAVE_CHANGE);
},
// Given a list of options for suitable matches to those glyphs and a maximum glyph count to match, returns the
// set of glyphs which should be loaded. This is a tricky matching process to do since on one hand we don't want
// early matches to prevent later ones, but on the other hand matching too leniently can cause any passed-on later
// requirements to be too strict (eg. preset 1234 and equipped 234 could match 123, leaving an unmatchable 4).
// The compromise solution here is to check how many choices the next-strictest option list has - if it only has
// one choice then we pick conservatively (the weakest glyph) - otherwise we pick greedily (the strongest glyph).
findSelectedGlyphs(optionList, maxGlyphs) {
// We do a weird composite sorting function here in order to make sure that glyphs get treated by type first, and
// with in type are generally ordered in strictest to most lenient in terms of matches. Note that the options
// are sorted internally starting with the strictest match first
optionList.sort((a, b) => 100 * (a.glyph.type.charCodeAt(0) - b.glyph.type.charCodeAt(0)) +
(a.options.length - b.options.length));
const toLoad = [];
let slotsLeft = maxGlyphs;
for (let index = 0; index < optionList.length; index++) {
if (slotsLeft === 0) break;
const entry = optionList[index];
const greedyPick = index === optionList.length - 1 || optionList[index + 1].options.length > 1;
const filteredOptions = entry.options.filter(g => !toLoad.includes(g));
if (filteredOptions.length === 0) continue;
const selectedGlyph = filteredOptions[greedyPick ? 0 : (filteredOptions.length - 1)];
toLoad.push(selectedGlyph);
slotsLeft--;
}
return toLoad;
},
deleteGlyphSet(id) {
if (!player.reality.glyphs.sets[id].glyphs.length) return;
@ -137,6 +170,11 @@ export default {
setLengthValid(set) {
return set.length && set.length <= Glyphs.activeSlotCount;
},
loadingTooltip(set) {
return this.setLengthValid(set) && this.hasEquipped
? "This set may not load properly because you already have some Glyphs equipped"
: null;
},
glyphSetKey(set, index) {
return `${index} ${Glyphs.hash(set)}`;
}
@ -219,9 +257,10 @@ export default {
Save
</button>
<button
v-tooltip="loadingTooltip(set)"
class="c-glyph-set-save-button"
:class="{'c-glyph-set-save-button--unavailable': !canLoadSet(set) || !setLengthValid(set)}"
@click="loadGlyphSet(set)"
:class="{'c-glyph-set-save-button--unavailable': !setLengthValid(set)}"
@click="loadGlyphSet(set, id)"
>
Load
</button>
@ -254,20 +293,4 @@ export default {
.c-glyph-set-preview-area {
width: 18rem;
}
.l-glyph-sacrifice-options__help {
position: absolute;
top: 0;
left: calc(100% - 1.8rem);
z-index: 2;
}
.c-glyph-sacrifice-options__help {
font-size: 1.2rem;
color: var(--color-reality-dark);
}
.s-base--dark .c-glyph-sacrifice-options__help {
color: var(--color-reality-light);
}
</style>

View File

@ -214,12 +214,6 @@ export function getGlyphIDsFromBitmask(bitmask) {
return getGlyphEffectsFromBitmask(bitmask).map(x => x.id);
}
export function hasAtLeastGlyphEffects(needleBitmask, haystackBitmask) {
const needle = getGlyphIDsFromBitmask(needleBitmask);
const haystack = getGlyphIDsFromBitmask(haystackBitmask);
return haystack.every(x => needle.includes(x));
}
class FunctionalGlyphType {
/**
* @param {Object} setup

View File

@ -192,40 +192,66 @@ export const Glyphs = {
this.validate();
EventHub.dispatch(GAME_EVENT.GLYPHS_CHANGED);
},
// This compares targetGlyph to all the glyphs in searchList, returning a subset of them which fulfills the comparison
// direction specified by the parameters in fuzzyMatch:
// -1: Will find glyphs which are equal to or worse than targetGlyph
// 0: Will only return glyphs which have identical values
// +1: Will find glyphs which are equal to or better than targetGlyph
findByValues(targetGlyph, searchList, fuzzyMatch = { level, strength, effects }) {
// We need comparison to go both ways for normal matching and subset matching for partially-equipped sets
const compFn = (op, comp1, comp2) => {
switch (op) {
case -1:
return comp1 <= comp2;
return comp2 - comp1;
case 0:
return comp1 === comp2;
return comp1 === comp2 ? 0 : -1;
case 1:
return comp1 >= comp2;
return comp1 - comp2;
}
return false;
};
// Returns a number based on how much the small mask is found inside of the large mask. Returns a non-negative
// number if small contains all of large, with a value equal to the number of extra bits. Otherwise, returns a
// negative number equal to the negative of the number of bits that large has which small doesn't.
const matchedEffects = (large, small) => {
if ((large & small) === large) return countValuesFromBitmask(small - large);
return -countValuesFromBitmask(large - (large & small));
};
// Make an array containing all glyphs which match the given criteria, with an additional "quality" prop in order
// to determine roughly how good the glyph itself is relative to other matches
const allMatches = [];
for (const glyph of searchList) {
const type = glyph.type === targetGlyph.type;
let eff = false;
let eff;
switch (fuzzyMatch.effects) {
case -1:
eff = hasAtLeastGlyphEffects(targetGlyph.effects, glyph.effects);
eff = matchedEffects(targetGlyph.effects, glyph.effects);
break;
case 0:
eff = glyph.effects === targetGlyph.effects;
eff = glyph.effects === targetGlyph.effects ? 0 : -1;
break;
case 1:
eff = hasAtLeastGlyphEffects(glyph.effects, targetGlyph.effects);
eff = matchedEffects(glyph.effects, targetGlyph.effects);
break;
}
const str = compFn(fuzzyMatch.strength, glyph.strength, targetGlyph.strength);
const lvl = compFn(fuzzyMatch.level, glyph.level, targetGlyph.level);
const str = compFn(fuzzyMatch.strength, glyph.strength, targetGlyph.strength) / 2.5;
const lvl = compFn(fuzzyMatch.level, glyph.level, targetGlyph.level) / 5000;
const sym = glyph.symbol === targetGlyph.symbol;
if (type && eff && str && lvl && sym) return glyph;
if (type && eff >= 0 && str >= 0 && lvl >= 0 && sym) {
allMatches.push({
glyph,
// Flatten glyph qualities, with 10% rarity, 500 levels, and an extra effect all being equal value. This
// is used to sort the options by some rough measure of distance from the target glyph
gap: str + lvl + eff / 10
});
}
}
return undefined;
// Sort by increasing gap, then discard the value as it's not directly used anywhere else
allMatches.sort((a, b) => a.gap - b.gap);
return allMatches.map(m => m.glyph);
},
findById(id) {
return player.reality.glyphs.inventory.find(glyph => glyph.id === id);