diff --git a/core/src/com/riiablo/attributes/StatFormatter.java b/core/src/com/riiablo/attributes/StatFormatter.java new file mode 100644 index 00000000..838b37bc --- /dev/null +++ b/core/src/com/riiablo/attributes/StatFormatter.java @@ -0,0 +1,309 @@ +package com.riiablo.attributes; + +import com.riiablo.CharacterClass; +import com.riiablo.Riiablo; +import com.riiablo.codec.excel.CharStats; +import com.riiablo.codec.excel.ItemStatCost; +import com.riiablo.codec.excel.SkillDesc; +import com.riiablo.codec.excel.Skills; +import com.riiablo.logger.LogManager; +import com.riiablo.logger.Logger; + +public class StatFormatter { + private static final Logger log = LogManager.getLogger(StatFormatter.class); + + private static final StringBuilder builder = new StringBuilder(32); + + private static final CharSequence SPACE; + private static final CharSequence DASH; + private static final CharSequence PERCENT; + private static final CharSequence PLUS; + private static final CharSequence TO; + static { + if (Riiablo.string == null) { + SPACE = " "; + DASH = "-"; + PERCENT = "%"; + PLUS = "+"; + TO = "to"; + } else { + SPACE = Riiablo.string.lookup("space"); + DASH = Riiablo.string.lookup("dash"); + PERCENT = Riiablo.string.lookup("percent"); + PLUS = Riiablo.string.lookup("plus"); + TO = Riiablo.string.lookup("ItemStast1k"); + } + } + + private static final String[] BY_TIME = { + "ModStre9e", "ModStre9g", "ModStre9d", "ModStre9f", + }; + + public CharSequence format(StatGetter stat, Attributes opAttrs) { + final ItemStatCost.Entry entry = stat.entry(); + return format(stat, opAttrs, entry.descfunc, entry.descval, entry.descstrpos, entry.descstrneg, entry.descstr2); + } + + public CharSequence format( + final StatGetter stat, + final Attributes opAttrs, + final int func, + final int valmode, + final String strpos, + final String strneg, + final String str2) { + builder.setLength(0); + switch (func) { + case 1: { // +%d %s1 + final int value = stat.value1(); + if (valmode == 1) builder.append(PLUS).append(value).append(SPACE); + builder.append(Riiablo.string.lookup(value < 0 ? strneg : strpos)); + if (valmode == 2) builder.append(SPACE).append(PLUS).append(value); + return builder.toString(); + } + case 2: { // %d%% %s1 + final int value = stat.value1(); + if (valmode == 1) builder.append(value).append(PERCENT).append(SPACE); + builder.append(Riiablo.string.lookup(value < 0 ? strneg : strpos)); + if (valmode == 2) builder.append(SPACE).append(value).append(PERCENT); + return builder.toString(); + } + case 3: { // %d %s1 + final int value = stat.value1(); + if (valmode == 1) builder.append(value).append(SPACE); + builder.append(Riiablo.string.lookup(value < 0 ? strneg : strpos)); + if (valmode == 2) builder.append(SPACE).append(value); + return builder.toString(); + } + case 4: { // +%d%% %s1 + final int value = stat.value1(); + if (valmode == 1) builder.append(PLUS).append(value).append(PERCENT).append(SPACE); + builder.append(Riiablo.string.lookup(value < 0 ? strneg : strpos)); + if (valmode == 2) builder.append(SPACE).append(PLUS).append(value).append(PERCENT); + return builder.toString(); + } + case 5: { // %d%% %s1 + final int value = Fixed.intBitsToFloatFloor(stat.value1() * 100, 7); + if (valmode == 1) builder.append(value).append(PERCENT).append(SPACE); + builder.append(Riiablo.string.lookup(value < 0 ? strneg : strpos)); + if (valmode == 2) builder.append(SPACE).append(value).append(PERCENT); + return builder.toString(); + } + case 6: { // +%d %s1 %s2 + final int value = op(stat, opAttrs); + if (valmode == 1) builder.append(PLUS).append(value).append(SPACE); + builder + .append(Riiablo.string.lookup(value < 0 ? strneg : strpos)) + .append(SPACE) + .append(Riiablo.string.lookup(str2)); + if (valmode == 2) builder.append(SPACE).append(PLUS).append(value); + return builder.toString(); + } + case 7: { // %d%% %s1 %s2 + final int value = op(stat, opAttrs); + if (valmode == 1) builder.append(value).append(PERCENT).append(SPACE); + builder + .append(Riiablo.string.lookup(value < 0 ? strneg : strpos)) + .append(SPACE) + .append(Riiablo.string.lookup(str2)); + if (valmode == 2) builder.append(SPACE).append(value).append(PERCENT); + return builder.toString(); + } + case 8: { // +%d%% %s1 %s2 + final int value = op(stat, opAttrs); + if (valmode == 1) builder.append(PLUS).append(value).append(PERCENT).append(SPACE); + builder + .append(Riiablo.string.lookup(value < 0 ? strneg : strpos)) + .append(SPACE) + .append(Riiablo.string.lookup(str2)); + if (valmode == 2) builder.append(SPACE).append(PLUS).append(value).append(PERCENT); + return builder.toString(); + } + case 9: { // %d %s1 %s2 + final int value = op(stat, opAttrs); + if (valmode == 1) builder.append(value).append(SPACE); + builder + .append(Riiablo.string.lookup(value < 0 ? strneg : strpos)) + .append(SPACE) + .append(Riiablo.string.lookup(str2)); + if (valmode == 2) builder.append(SPACE).append(value); + return builder.toString(); + } + case 10: { // %d%% %s1 %s2 + final int value = Fixed.intBitsToFloatFloor(stat.value1() * 100, 7); + if (valmode == 1) builder.append(value).append(PERCENT).append(SPACE); + builder + .append(Riiablo.string.lookup(value < 0 ? strneg : strpos)) + .append(SPACE) + .append(Riiablo.string.lookup(str2)); + if (valmode == 2) builder.append(SPACE).append(value).append(PERCENT); + return builder.toString(); + } + case 11: { // Repairs 1 Durability in %d Seconds + final int value = 100 / stat.value1(); + return Riiablo.string.format("ModStre9u", 1, value); + } + case 12: { // +%d %s1 + final int value = stat.value1(); + if (valmode == 1) builder.append(PLUS).append(value).append(SPACE); + builder.append(Riiablo.string.lookup(value < 0 ? strneg : strpos)); + if (valmode == 2) builder.append(SPACE).append(PLUS).append(value); + return builder.toString(); + } + case 13: { // +%d %s | +1 to Paladin Skills + final int value = stat.value1(); + final int param = stat.param1(); + builder + .append(PLUS).append(value) + .append(SPACE) + .append(Riiablo.string.lookup(CharacterClass.get(param).entry().StrAllSkills)); + return builder.toString(); + } + case 14: { // %s %s | +1 to Fire Skills (Sorceress Only) + final int value = stat.value1(); + final int param = stat.param1(); + final CharStats.Entry entry = CharacterClass.get((param >>> 3) & 0x3).entry(); + builder + .append(Riiablo.string.format(entry.StrSkillTab[param & 0x7], value)) + .append(SPACE) + .append(Riiablo.string.lookup(entry.StrClassOnly)); + return builder.toString(); + } + case 15: { // 15% chance to cast Level 5 Life Tap on Striking + final int value = stat.value1(); + final int param = stat.param1(); + final Skills.Entry skill = Riiablo.files.skills.get(stat.param2()); + final SkillDesc.Entry desc = Riiablo.files.skilldesc.get(skill.skilldesc); + return Riiablo.string.format(strpos, value, param, Riiablo.string.lookup(desc.str_name)); + } + case 16: { // Level 16 Defiance Aura When Equipped + final int value = stat.value1(); + final int param = stat.param1(); + final Skills.Entry skill = Riiablo.files.skills.get(param); + final SkillDesc.Entry desc = Riiablo.files.skilldesc.get(skill.skilldesc); + return Riiablo.string.format(strpos, value, Riiablo.string.lookup(desc.str_name)); + } + case 17: { // +10 to Dexterity (Increases Near Dawn) // TODO: untested + if (log.warnEnabled()) log.warn("stat({}) uses untested bytime func({})", stat.debugString(), func); + // value needs to update based on time of day + final int value1 = stat.value1(); + final int value2 = stat.value2(); + final int value3 = stat.value3(); + if (valmode == 1) builder.append(PLUS).append(value3).append(SPACE); + builder.append(Riiablo.string.lookup(strpos)); + if (valmode == 2) builder.append(SPACE).append(PLUS).append(value3); + builder.append(SPACE).append(Riiablo.string.lookup(BY_TIME[value1])); + return builder.toString(); + } + case 18: { // 50% Enhanced Defense (Increases Near Dawn) // TODO: untested + if (log.warnEnabled()) log.warn("stat({}) uses untested bytime func({})", stat.debugString(), func); + // value needs to update based on time of day + final int value1 = stat.value1(); + final int value2 = stat.value2(); + final int value3 = stat.value3(); + if (valmode == 1) builder.append(value3).append(PERCENT).append(SPACE); + builder.append(Riiablo.string.lookup(strpos)); + if (valmode == 2) builder.append(SPACE).append(value3).append(PERCENT); + builder.append(SPACE).append(Riiablo.string.lookup(BY_TIME[value1])); + return builder.toString(); + } + case 19: { // Formats strpos/strneg with value + final int value = stat.value1(); + return Riiablo.string.format(value < 0 ? strneg : strpos, value); + } + case 20: { // -%d%% %s1 + final int value = -stat.value1(); + if (valmode == 1) builder.append(value).append(PERCENT).append(SPACE); + builder.append(Riiablo.string.lookup(value < 0 ? strneg : strpos)); + if (valmode == 2) builder.append(SPACE).append(value).append(PERCENT); + return builder.toString(); + } + case 21: { // -%d %s1 + final int value = -stat.value1(); + if (valmode == 1) builder.append(value).append(SPACE); + builder.append(Riiablo.string.lookup(value < 0 ? strneg : strpos)); + if (valmode == 2) builder.append(SPACE).append(value); + return builder.toString(); + } + case 22: { // +%d%% %s1 %s | +3% Attack Rating Versus: %s // TODO: unsupported for now + if (log.warnEnabled()) log.warn("stat({}) uses unsupported func({})", stat.debugString(), func); + return "ERROR(22)"; + } + case 23: { // %d%% %s1 %s | 3% ReanimateAs: %s // TODO: unsupported for now + if (log.warnEnabled()) log.warn("stat({}) uses unsupported func({})", stat.debugString(), func); + return "ERROR(23)"; + } + case 24: { + final Skills.Entry skill = Riiablo.files.skills.get(stat.param2()); + final SkillDesc.Entry desc = Riiablo.files.skilldesc.get(skill.skilldesc); + builder + .append(Riiablo.string.lookup("ModStre10b")).append(SPACE) + .append(stat.param1()).append(SPACE) + .append(Riiablo.string.lookup(desc.str_name)).append(SPACE) + .append(Riiablo.string.format(strpos, stat.value1(), stat.value2())); + return builder.toString(); + } + case 25: { // TODO: unsupported + if (log.warnEnabled()) log.warn("stat({}) uses unsupported func({})", stat.debugString(), func); + return "ERROR(25)"; + } + case 26: { // TODO: unsupported + if (log.warnEnabled()) log.warn("stat({}) uses unsupported func({})", stat.debugString(), func); + return "ERROR(26)"; + } + case 27: { // +1 to Lightning (Sorceress Only) + final int value = stat.value1(); + final int param = stat.param1(); + final Skills.Entry skill = Riiablo.files.skills.get(param); + final SkillDesc.Entry desc = Riiablo.files.skilldesc.get(skill.skilldesc); + final CharStats.Entry entry = Riiablo.files.skills.getClass(skill.charclass).entry(); + builder + .append(PLUS).append(value).append(SPACE) + .append(TO).append(SPACE) + .append(Riiablo.string.lookup(desc.str_name)).append(SPACE) + .append(Riiablo.string.lookup(entry.StrClassOnly)); + return builder.toString(); + } + case 28: { // +1 to Teleport + final int value = stat.value1(); + final int param = stat.param1(); + final Skills.Entry skill = Riiablo.files.skills.get(param); + final SkillDesc.Entry desc = Riiablo.files.skilldesc.get(skill.skilldesc); + builder + .append(PLUS).append(value).append(SPACE) + .append(TO).append(SPACE) + .append(Riiablo.string.lookup(desc.str_name)); + return builder.toString(); + } + default: + if (log.warnEnabled()) log.warn("stat({}) uses unknown func({})", stat.debugString(), func); + return "ERROR"; + } + } + + /** @see AttributesUpdater#op(CharStats.Entry, StatListBuilder, StatGetter, StatGetter, int, int, int) */ + private static int op(StatGetter stat, Attributes opAttrs) { + final ItemStatCost.Entry entry = stat.entry(); + final int op_param = entry.op_param; + final int op_base = op_param > 0 + ? opAttrs.aggregate().get(Stat.index(entry.op_base)).value1() + : 1; + final int value = stat.value1(); + switch (entry.op) { + default: log.warn("entry.op({}) unknown for stat({})", entry.op, stat.debugString()); // fall-through + case 1: return value; + case 2: return Fixed.intBitsToFloatFloor(value * op_base, op_param); + case 3: return value; + case 4: return Fixed.intBitsToFloatFloor(value * op_base, op_param); + case 5: return Fixed.intBitsToFloatFloor(value * op_base, op_param); + case 6: return value; // Unsupported -- time of day + case 7: return value; // Unsupported -- time of day % + case 8: return value; + case 9: return value; + case 10: return value; + case 11: return value; + case 12: return value; + case 13: return value; + } + } +} diff --git a/core/test/com/riiablo/attributes/StatFormatterTest.java b/core/test/com/riiablo/attributes/StatFormatterTest.java new file mode 100644 index 00000000..77afa297 --- /dev/null +++ b/core/test/com/riiablo/attributes/StatFormatterTest.java @@ -0,0 +1,97 @@ +package com.riiablo.attributes; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.backends.headless.HeadlessApplication; + +import com.riiablo.Files; +import com.riiablo.Riiablo; +import com.riiablo.codec.StringTBLs; +import com.riiablo.logger.Level; +import com.riiablo.logger.LogManager; +import com.riiablo.mpq.MPQFileHandleResolver; + +public class StatFormatterTest { + @BeforeClass + public static void setup() { + Gdx.app = new HeadlessApplication(new ApplicationAdapter() {}); + Riiablo.home = Gdx.files.absolute("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Diablo II"); + Riiablo.mpqs = new MPQFileHandleResolver(); + Riiablo.string = new StringTBLs(Riiablo.mpqs); + Riiablo.files = new Files(); + } + + @AfterClass + public static void teardown() { + Gdx.app.exit(); + } + + @BeforeClass + public static void before() { + LogManager.setLevel("com.riiablo.attributes", Level.TRACE); + } + + private static StatFormatter newInstance() { + return new StatFormatter(); + } + + @Test + public void strength() { // func 1 + StatGetter stat = StatList.obtain().buildList().put(Stat.strength, 5).last(); + Assert.assertEquals("+5 to Strength", newInstance().format(stat, null)); + } + + @Test + public void toblock() { // func 2 + StatGetter stat = StatList.obtain().buildList().put(Stat.toblock, 25).last(); + Assert.assertEquals("25% Increased Chance of Blocking", newInstance().format(stat, null)); + } + + @Test + public void item_maxdamage_percent() { // func 3, valmode 0 + StatGetter stat = StatList.obtain().buildList().put(Stat.item_maxdamage_percent, 300).last(); + Assert.assertEquals("Enhanced Maximum Damage", newInstance().format(stat, null)); + } + + @Test + public void magic_damage_reduction() { // func 3, valmode 2 + StatGetter stat = StatList.obtain().buildList().put(Stat.magic_damage_reduction, 10).last(); + Assert.assertEquals("Magic Damage Reduced by 10", newInstance().format(stat, null)); + } + + @Test + public void fireresist() { // func 4, valmode 2 + StatGetter stat = StatList.obtain().buildList().put(Stat.fireresist, 45).last(); + Assert.assertEquals("Fire Resist +45%", newInstance().format(stat, null)); + } + + @Test + public void maxfireresist() { // func 4, valmode 1 + StatGetter stat = StatList.obtain().buildList().put(Stat.maxfireresist, 5).last(); + Assert.assertEquals("+5% to Maximum Fire Resist", newInstance().format(stat, null)); + } + + @Test + public void item_addclassskills() { // func 13, valmode 1 + StatGetter stat = StatList.obtain().buildList().put(Stat.item_addclassskills, Riiablo.BARBARIAN, 3).last(); + Assert.assertEquals("+3 to Barbarian Skill Levels", newInstance().format(stat, null)); + } + + @Test + public void item_nonclassskill() { // func 28, valmost 0 + StatGetter stat = StatList.obtain().buildList().put(Stat.item_nonclassskill, 54, 1).last(); + Assert.assertEquals("+1 to Teleport", newInstance().format(stat, null)); + } + + @Test + public void item_hp_perlevel() { // func 6, valmode 1, op 2, op_param 3 + Attributes attrs = Attributes.wrappedAttributes(StatList.obtain().buildList().put(Stat.level, 5).build()); + StatGetter stat = StatList.obtain().buildList().put(Stat.item_hp_perlevel, 10 << 3).last(); + Assert.assertEquals("+50 to Life (Based on Character Level)", newInstance().format(stat, attrs)); + } +} \ No newline at end of file