diff --git a/mod/src/main/java/basemod/AtlasLoader.java b/mod/src/main/java/basemod/AtlasLoader.java new file mode 100644 index 000000000..331ca0891 --- /dev/null +++ b/mod/src/main/java/basemod/AtlasLoader.java @@ -0,0 +1,43 @@ +package basemod; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.utils.GdxRuntimeException; +import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; +import com.evacipated.cardcrawl.modthespire.lib.SpirePrefixPatch; + +import java.util.HashMap; + +public class AtlasLoader { + private static HashMap atlases = new HashMap<>(); + + public static TextureAtlas getAtlas(final String atlasString) { + if (atlases.get(atlasString) == null) { + try { + loadAtlas(atlasString); + } catch (GdxRuntimeException e) { + + } + } + return atlases.get(atlasString); + } + + private static void loadAtlas(final String atlasString) throws GdxRuntimeException { + TextureAtlas atlas = new TextureAtlas(Gdx.files.internal(atlasString)); + atlases.put(atlasString, atlas); + } + + public static boolean testAtlas(String filePath) { + return Gdx.files.internal(filePath).exists(); + } + + @SpirePatch(clz = TextureAtlas.class, method = "dispose") + public static class DisposeListener { + @SpirePrefixPatch + public static void DisposeListenerPatch(final TextureAtlas __instance) { + atlases.entrySet().removeIf(entry -> { + return entry.getValue().equals(__instance); + }); + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/BaseMod.java b/mod/src/main/java/basemod/BaseMod.java index b418263c1..8d8994847 100644 --- a/mod/src/main/java/basemod/BaseMod.java +++ b/mod/src/main/java/basemod/BaseMod.java @@ -16,7 +16,6 @@ import basemod.patches.whatmod.WhatMod; import basemod.screens.ModalChoiceScreen; import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Input; import com.badlogic.gdx.Version; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Color; @@ -68,6 +67,7 @@ import com.megacrit.cardcrawl.screens.charSelect.CharacterOption; import com.megacrit.cardcrawl.screens.custom.CustomMod; import com.megacrit.cardcrawl.screens.custom.CustomModeCharacterButton; +import com.megacrit.cardcrawl.screens.stats.StatsScreen; import com.megacrit.cardcrawl.shop.ShopScreen; import com.megacrit.cardcrawl.shop.StorePotion; import com.megacrit.cardcrawl.shop.StoreRelic; @@ -143,6 +143,7 @@ public class BaseMod { private static ArrayList editStringsSubscribers; private static ArrayList addAudioSubscribers; private static ArrayList editKeywordsSubscribers; + private static ArrayList editAchievementsSubscribers; private static ArrayList postBattleSubscribers; private static ArrayList setUnlocksSubscribers; private static ArrayList postPotionUseSubscribers; @@ -246,7 +247,8 @@ public class BaseMod { private static HashMap> unlockBundles; private static HashMap> unlockCards; private static HashMap maxUnlockLevel; - + public static Map> modAchievements = new HashMap<>(); + public static Map modAchievementGrids = new HashMap<>(); private static HashMap customSaveFields = new HashMap<>(); private static HashMap customScreens = new HashMap<>(); @@ -256,6 +258,9 @@ public class BaseMod { private static FrameBuffer animationBuffer; private static Texture animationTexture; private static TextureRegion animationTextureRegion; + private static String achievementModID; + private static String achievementMakeID; + private static TextureAtlas achievementAtlas; public static boolean fixesEnabled = true; @@ -484,6 +489,7 @@ private static void initializeSubscriptions() { editStringsSubscribers = new ArrayList<>(); addAudioSubscribers = new ArrayList<>(); editKeywordsSubscribers = new ArrayList<>(); + editAchievementsSubscribers = new ArrayList<>(); postBattleSubscribers = new ArrayList<>(); setUnlocksSubscribers = new ArrayList<>(); postPotionUseSubscribers = new ArrayList<>(); @@ -1705,6 +1711,7 @@ public static List getModdedCharacters() { return CardCrawlGame.characterManager.getAllCharacters().subList(lastBaseCharacterIndex+1, CardCrawlGame.characterManager.getAllCharacters().size()); } + // add character - the String characterID *must* be the exact same as what // you put in the PlayerClass enum public static void addCharacter(AbstractPlayer character, @@ -2100,6 +2107,54 @@ public static void saveEnergyOrbPortraitTexture(AbstractCard.CardColor color, co colorEnergyOrbPortraitTextureMap.put(color, tex); } + // + // Achievements + // + + public static String getAchievementModID() { + return achievementModID; + } + + public static void registerAchievementGrid(String modID, TextureAtlas atlas, String headerText) { + achievementModID = modID; + achievementAtlas = atlas; + ModAchievementGrid grid = new ModAchievementGrid(modID, headerText); + modAchievementGrids.put(modID, grid); + } + + public static void registerAchievement(String id) { + if (achievementModID == null || achievementAtlas == null) { + throw new IllegalStateException("You must call registerAchievementGrid before registering achievements."); + } + + String fullID = achievementModID + ":" + id; + UIStrings uiStrings = CardCrawlGame.languagePack.getUIString(fullID); + String name = uiStrings.TEXT[0]; + String description = uiStrings.TEXT[1]; + TextureAtlas.AtlasRegion achievementImageUnlocked = achievementAtlas.findRegion("unlocked/" + id); + TextureAtlas.AtlasRegion achievementImageLocked = achievementAtlas.findRegion("locked/" + id); + + if (achievementImageUnlocked == null || achievementImageLocked == null) { + BaseMod.logger.info("Failed to find achievement images for: " + fullID); + return; // Skip adding this achievement + } + + ModAchievement newAchievement = new ModAchievement(name, description, fullID, achievementImageUnlocked, achievementImageLocked, achievementAtlas); + + ModAchievementGrid grid = modAchievementGrids.get(achievementModID); + if (grid == null) { + throw new IllegalStateException("Achievement grid for " + achievementModID + " not found. Make sure to call registerAchievementGrid first."); + } + + for (ModAchievement achievement : grid.items) { + if (achievement.key.equals(fullID)) { + return; // Achievement already exists, skip adding + } + } + + grid.items.add(newAchievement); + } + // // Potions // @@ -2694,6 +2749,21 @@ public static void publishEditKeywords() { } unsubscribeLaterHelper(EditKeywordsSubscriber.class); } + public static void publishEditAchievements(StatsScreen statsScreen) { + logger.info("editing achievements"); + for (EditAchievementsSubscriber sub : editAchievementsSubscribers) { + sub.receiveEditAchievements(); + } + try { + Method calculateScrollBoundsMethod = StatsScreen.class.getDeclaredMethod("calculateScrollBounds"); + calculateScrollBoundsMethod.setAccessible(true); + calculateScrollBoundsMethod.invoke(statsScreen); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.error("Failed to invoke calculateScrollBounds method: " + e.getMessage()); + e.printStackTrace(); + } + unsubscribeLaterHelper(EditAchievementsSubscriber.class); + } // publishOnPowersModified public static void publishOnPowersModified() { @@ -2897,6 +2967,7 @@ public static void subscribe(ISubscriber sub) { subscribeIfInstance(editCharactersSubscribers, sub, EditCharactersSubscriber.class); subscribeIfInstance(editStringsSubscribers, sub, EditStringsSubscriber.class); subscribeIfInstance(editKeywordsSubscribers, sub, EditKeywordsSubscriber.class); + subscribeIfInstance(editAchievementsSubscribers, sub, EditAchievementsSubscriber.class); subscribeIfInstance(postBattleSubscribers, sub, PostBattleSubscriber.class); subscribeIfInstance(setUnlocksSubscribers, sub, SetUnlocksSubscriber.class); subscribeIfInstance(postPotionUseSubscribers, sub, PostPotionUseSubscriber.class); @@ -2981,6 +3052,8 @@ public static void subscribe(ISubscriber sub, Class addit editStringsSubscribers.add((EditStringsSubscriber) sub); } else if (additionClass.equals(EditKeywordsSubscriber.class)) { editKeywordsSubscribers.add((EditKeywordsSubscriber) sub); + } else if (additionClass.equals(EditAchievementsSubscriber.class)) { + editAchievementsSubscribers.add((EditAchievementsSubscriber) sub); } else if (additionClass.equals(PostBattleSubscriber.class)) { postBattleSubscribers.add((PostBattleSubscriber) sub); } else if (additionClass.equals(SetUnlocksSubscriber.class)) { @@ -3055,6 +3128,7 @@ public static void unsubscribe(ISubscriber sub) { unsubscribeIfInstance(editCharactersSubscribers, sub, EditCharactersSubscriber.class); unsubscribeIfInstance(editStringsSubscribers, sub, EditStringsSubscriber.class); unsubscribeIfInstance(editKeywordsSubscribers, sub, EditKeywordsSubscriber.class); + unsubscribeIfInstance(editAchievementsSubscribers, sub, EditAchievementsSubscriber.class); unsubscribeIfInstance(postBattleSubscribers, sub, PostBattleSubscriber.class); unsubscribeIfInstance(setUnlocksSubscribers, sub, SetUnlocksSubscriber.class); unsubscribeIfInstance(postPotionUseSubscribers, sub, PostPotionUseSubscriber.class); @@ -3139,6 +3213,8 @@ public static void unsubscribe(ISubscriber sub, Class rem editStringsSubscribers.remove(sub); } else if (removalClass.equals(EditKeywordsSubscriber.class)) { editKeywordsSubscribers.remove(sub); + } else if (removalClass.equals(EditAchievementsSubscriber.class)) { + editAchievementsSubscribers.remove(sub); } else if (removalClass.equals(AddAudioSubscriber.class)) { addAudioSubscribers.remove(sub); } else if (removalClass.equals(PostBattleSubscriber.class)) { diff --git a/mod/src/main/java/basemod/ModAchievement.java b/mod/src/main/java/basemod/ModAchievement.java new file mode 100644 index 000000000..ac11d3bef --- /dev/null +++ b/mod/src/main/java/basemod/ModAchievement.java @@ -0,0 +1,91 @@ +package basemod; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.megacrit.cardcrawl.core.Settings; +import com.megacrit.cardcrawl.helpers.Hitbox; +import com.megacrit.cardcrawl.helpers.TipHelper; +import com.megacrit.cardcrawl.helpers.input.InputHelper; +import com.megacrit.cardcrawl.unlock.UnlockTracker; + +public class ModAchievement { + private TextureAtlas.AtlasRegion unlockedImg; + private TextureAtlas.AtlasRegion lockedImg; + public TextureAtlas.AtlasRegion currentImg; + private static final Color LOCKED_COLOR = new Color(1.0F, 1.0F, 1.0F, 0.8F); + private TextureAtlas atlas; + private String title; + private String desc; + public String key; + public boolean isUnlocked; + public Hitbox hb; + + public ModAchievement(String title, String desc, String key, TextureAtlas.AtlasRegion unlockedImage, TextureAtlas.AtlasRegion lockedImage, TextureAtlas atlas) { + this.hb = new Hitbox(160.0F * Settings.scale, 160.0F * Settings.scale); + this.isUnlocked = UnlockTracker.isAchievementUnlocked(key); + this.key = key; + this.title = title; + this.desc = desc; + this.unlockedImg = unlockedImage; + this.lockedImg = lockedImage; + this.currentImg = lockedImage; + this.atlas = atlas; + if (this.unlockedImg == null || this.lockedImg == null) { + BaseMod.logger.info("Failed to load images for achievement: " + key); + } + } + + + + public String getKey() { + return key; + } + + public void reloadImg() { + if (this.isUnlocked) { + this.unlockedImg = atlas.findRegion(this.unlockedImg.name); + } else { + this.lockedImg = atlas.findRegion(this.lockedImg.name); + } + } + + public void render(SpriteBatch sb, float x, float y) { + if (sb == null) { + BaseMod.logger.info("SpriteBatch is null in ModAchievement.render"); + return; + } + + TextureAtlas.AtlasRegion currentImg = this.isUnlocked ? this.unlockedImg : this.lockedImg; + if (currentImg == null) { + BaseMod.logger.info("Current image is null for achievement: " + this.key); + return; + } + + this.currentImg = currentImg; + Color currentColor = this.isUnlocked ? Color.WHITE : LOCKED_COLOR; + sb.setColor(currentColor); + + if (this.hb == null) { + this.hb = new Hitbox(160.0F * Settings.scale, 160.0F * Settings.scale); + } + + if (this.hb.hovered) { + sb.draw(currentImg, x - (float)currentImg.packedWidth / 2.0F, y - (float)currentImg.packedHeight / 2.0F, (float)currentImg.packedWidth / 2.0F, (float)currentImg.packedHeight / 2.0F, (float)currentImg.packedWidth, (float)currentImg.packedHeight, Settings.scale * 1.1F, Settings.scale * 1.1F, 0.0F); + } else { + sb.draw(currentImg, x - (float)currentImg.packedWidth / 2.0F, y - (float)currentImg.packedHeight / 2.0F, (float)currentImg.packedWidth / 2.0F, (float)currentImg.packedHeight / 2.0F, (float)currentImg.packedWidth, (float)currentImg.packedHeight, Settings.scale, Settings.scale, 0.0F); + } + + this.hb.move(x, y); + this.hb.render(sb); + } + + public void update() { + if (this.hb != null) { + this.hb.update(); + if (this.hb.hovered) { + TipHelper.renderGenericTip((float) InputHelper.mX + 100.0F * Settings.scale, (float)InputHelper.mY, this.title, this.desc); + } + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/ModAchievementGrid.java b/mod/src/main/java/basemod/ModAchievementGrid.java new file mode 100644 index 000000000..9cd51b106 --- /dev/null +++ b/mod/src/main/java/basemod/ModAchievementGrid.java @@ -0,0 +1,57 @@ +package basemod; + +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.megacrit.cardcrawl.core.Settings; +import com.megacrit.cardcrawl.unlock.UnlockTracker; + +import java.util.ArrayList; + +public class ModAchievementGrid { + public ArrayList items = new ArrayList<>(); + private static final float SPACING = 200.0F * Settings.scale; + private static final int ITEMS_PER_ROW = 5; + public String modID; + public String headerText; + + public ModAchievementGrid(String modID, String headerText) { + this.modID = modID; + this.headerText = headerText; + } + + public void updateAchievementStatus() { + for (ModAchievement item : items) { + String achievementKey = item.getKey(); + boolean isUnlocked = UnlockTracker.isAchievementUnlocked(achievementKey); + item.isUnlocked = isUnlocked; + item.reloadImg(); + } + } + + public void render(SpriteBatch sb, float renderY) { + if (items == null) { + BaseMod.logger.info("Items list is null in ModAchievementGrid for mod: " + modID); + return; + } + for (int i = 0; i < items.size(); ++i) { + ModAchievement achievement = items.get(i); + if (achievement != null) { + achievement.render(sb, 560.0F * Settings.scale + (i % ITEMS_PER_ROW) * SPACING, renderY - (i / ITEMS_PER_ROW) * SPACING + 680.0F * Settings.yScale); + } else { + BaseMod.logger.info("Null achievement found in ModAchievementGrid for mod: " + modID); + } + } + } + + public float calculateHeight() { + int numRows = (items.size() + ITEMS_PER_ROW - 1) / ITEMS_PER_ROW; + float height = numRows * SPACING + 50.0F * Settings.scale; + return height; + } + + public void update() { + updateAchievementStatus(); + for (ModAchievement item : items) { + item.update(); + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/ModAchievementUnlocker.java b/mod/src/main/java/basemod/ModAchievementUnlocker.java new file mode 100644 index 000000000..df7ac0764 --- /dev/null +++ b/mod/src/main/java/basemod/ModAchievementUnlocker.java @@ -0,0 +1,24 @@ +package basemod; + +import com.megacrit.cardcrawl.core.Settings; +import static com.megacrit.cardcrawl.unlock.UnlockTracker.achievementPref; + +public class ModAchievementUnlocker { + public static void unlockAchievement(String id) { + String currentModID = BaseMod.getAchievementModID(); + if (currentModID == null) { + BaseMod.logger.error("Attempted to unlock achievement without a registered mod ID: " + id); + return; + } + + String fullKey = currentModID + ":" + id; + + if (!Settings.isShowBuild && Settings.isStandardRun()) { + if (!achievementPref.getBoolean(fullKey, false)) { + achievementPref.putBoolean(fullKey, true); + achievementPref.flush(); + BaseMod.logger.info("Unlocked achievement: " + fullKey); + } + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/interfaces/EditAchievementsSubscriber.java b/mod/src/main/java/basemod/interfaces/EditAchievementsSubscriber.java new file mode 100644 index 000000000..11b4f354e --- /dev/null +++ b/mod/src/main/java/basemod/interfaces/EditAchievementsSubscriber.java @@ -0,0 +1,5 @@ +package basemod.interfaces; + +public interface EditAchievementsSubscriber extends ISubscriber { + void receiveEditAchievements(); +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/AchievementGridRenderPatch.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/AchievementGridRenderPatch.java new file mode 100644 index 000000000..52bc0fd3c --- /dev/null +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/AchievementGridRenderPatch.java @@ -0,0 +1,43 @@ +package basemod.patches.com.megacrit.cardcrawl.screens.stats.StatsScreen; + +import basemod.BaseMod; +import basemod.ModAchievementGrid; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.evacipated.cardcrawl.modthespire.lib.*; +import com.megacrit.cardcrawl.core.Settings; +import com.megacrit.cardcrawl.screens.stats.StatsScreen; +import javassist.CtBehavior; + +import java.util.Iterator; + +@SpirePatch(clz = StatsScreen.class, method = "renderStatScreen") +public class AchievementGridRenderPatch { + private static boolean achievementsPublished = false; + @SpireInsertPatch(locator = Locator.class, localvars = {"renderY"}) + public static void Insert(StatsScreen __instance, SpriteBatch sb, @ByRef float[] renderY) { + if (!achievementsPublished) { + BaseMod.publishEditAchievements(__instance); + achievementsPublished = true; + } + boolean firstGrid = true; + for (ModAchievementGrid grid : BaseMod.modAchievementGrids.values()) { + if (firstGrid) { + renderY[0] += 50.0F * Settings.scale; + firstGrid = false; + } + StatsScreen.renderHeader(sb, grid.headerText, 300.0F * Settings.scale, renderY[0]); + grid.render(sb, renderY[0]); + renderY[0] -= grid.calculateHeight(); + renderY[0] -= 100.0F * Settings.scale; + } + } + + private static class Locator extends SpireInsertLocator { + @Override + public int[] Locate(CtBehavior ctMethodToPatch) throws Exception { + Matcher finalMatcher = new Matcher.MethodCallMatcher(Iterator.class, "hasNext"); + return LineFinder.findInOrder(ctMethodToPatch, finalMatcher); + } + } + +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/UpdatePatch.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/UpdatePatch.java new file mode 100644 index 000000000..42fe3c16f --- /dev/null +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/UpdatePatch.java @@ -0,0 +1,15 @@ +package basemod.patches.com.megacrit.cardcrawl.screens.stats.StatsScreen; + +import basemod.BaseMod; +import basemod.ModAchievementGrid; +import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; +import com.megacrit.cardcrawl.screens.stats.StatsScreen; + +@SpirePatch(clz = StatsScreen.class, method = "update") +public class UpdatePatch { + public static void Postfix(StatsScreen __instance) { + for (ModAchievementGrid grid : BaseMod.modAchievementGrids.values()) { + grid.update(); + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/UpdateStats.java b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/UpdateStats.java index d7f17bc85..235d88ae3 100644 --- a/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/UpdateStats.java +++ b/mod/src/main/java/basemod/patches/com/megacrit/cardcrawl/screens/stats/StatsScreen/UpdateStats.java @@ -1,6 +1,7 @@ package basemod.patches.com.megacrit.cardcrawl.screens.stats.StatsScreen; import basemod.BaseMod; +import basemod.ModAchievementGrid; import com.evacipated.cardcrawl.modthespire.lib.SpirePatch; import com.megacrit.cardcrawl.core.Settings; import com.megacrit.cardcrawl.screens.stats.StatsScreen; @@ -9,26 +10,35 @@ import java.lang.reflect.Field; -public class UpdateStats -{ +public class UpdateStats { public static final Logger logger = LogManager.getLogger(BaseMod.class.getName()); - public static final float SIZE_PER_CHARACTER = 400.0F; - + @SpirePatch( clz=StatsScreen.class, method="calculateScrollBounds" ) - public static class ScrollBounds - { - public static void Postfix(StatsScreen __instance) - { + public static class ScrollBounds { + public static void Postfix(StatsScreen __instance) { try { Field scrollUpperBoundField = __instance.getClass().getDeclaredField("scrollUpperBound"); scrollUpperBoundField.setAccessible(true); + float modAchievementsHeight = 0.0F; + boolean firstGrid = true; + for (ModAchievementGrid grid : BaseMod.modAchievementGrids.values()) { + float gridHeight = grid.calculateHeight(); + modAchievementsHeight += gridHeight; + if (firstGrid) { + modAchievementsHeight -= 50.0F * Settings.scale; + firstGrid = false; + } + modAchievementsHeight += 100.0F * Settings.scale; + } int characterCount = BaseMod.getModdedCharacters().size(); - scrollUpperBoundField.set(__instance, scrollUpperBoundField.getFloat(__instance) + - (SIZE_PER_CHARACTER * characterCount * Settings.scale)); + float characterHeight = SIZE_PER_CHARACTER * characterCount * Settings.scale; + float currentUpperBound = scrollUpperBoundField.getFloat(__instance); + float newUpperBound = currentUpperBound + characterHeight + modAchievementsHeight; + scrollUpperBoundField.set(__instance, newUpperBound); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { logger.error("could not calculate updated scroll bounds"); logger.error("error was: " + e.toString()); @@ -36,4 +46,4 @@ public static void Postfix(StatsScreen __instance) } } } -} +} \ No newline at end of file