diff --git a/.github/workflows/blacklist.txt b/.github/workflows/blacklist.txt index 9eacd848..e66294ce 100644 --- a/.github/workflows/blacklist.txt +++ b/.github/workflows/blacklist.txt @@ -8,7 +8,8 @@ DO_NOT_USE .flac$ banner.png$ .github/ -media/ +^media/ ^release_checklist.md$ -docs/ -dokka/ \ No newline at end of file +^docs/ +^dokka/ +^lib/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..ae7499cc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "CommunityApiDocs"] + path = CommunityApiDocs + url = git@github.com:StarsectorCommunityApiDocs/CommunityApiDocs.git diff --git a/.idea/artifacts/MagicLib.xml b/.idea/artifacts/MagicLib.xml index a53a9d44..f90a74ff 100644 --- a/.idea/artifacts/MagicLib.xml +++ b/.idea/artifacts/MagicLib.xml @@ -2,8 +2,8 @@ $PROJECT_DIR$/jars - + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..cf247ce4 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index d7333dff..0773fa01 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,3 +1,4 @@ + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddf..63e6ac6b 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/.run/Run Starsector.run.xml b/.run/Run Starsector.run.xml index 69337b6c..bf77367a 100644 --- a/.run/Run Starsector.run.xml +++ b/.run/Run Starsector.run.xml @@ -1,11 +1,11 @@ - \ No newline at end of file diff --git a/data/campaign/rules.csv b/data/campaign/rules.csv index 7b90ebbb..4d0ce7e1 100644 --- a/data/campaign/rules.csv +++ b/data/campaign/rules.csv @@ -1,9 +1,13 @@ id,trigger,conditions,script,text,options,notes -"#RULESET_NAME MagicLib (bounties)",,,,,, -magiclib_bountyboard_addevent,AddBarEvents,"ShouldShowMagicBountyBoard","AddBarEvent magiclib_bountyboard_selected ""Connect to the local unsanctioned bounty board."" ""A subroutine from your implant informs you that this establishment is broadcasting an informal job board.""",,, -magiclib_bountyboard_selected,DialogOptionSelected,"$option == magiclib_bountyboard_selected","ShowMagicBountyBoardCmd",,, +#RULESET_NAME MagicLib (bounties),,,,,, +magiclib_bountyboard_addevent,AddBarEvents,ShouldShowMagicBountyBoard,"AddBarEvent magiclib_bountyboard_selected ""Connect to the local unsanctioned bounty board."" ""A subroutine from your implant informs you that this establishment is broadcasting an informal job board.""",,, +magiclib_bountyboard_selected,DialogOptionSelected,$option == magiclib_bountyboard_selected,ShowMagicBountyBoardCmd,,, magiclib_bountyCommText,OpenCommLink,"$entity.MagicLib_Bounty_target_hasReply score:1000 !$entity.ignorePlayerCommRequests","$entity.ignorePlayerCommRequests = true MagicBountyCommsReplyCmd",,cutCommLinkNoText:Continue, -#magiclib_test_bounty_completed,"magiclib_test_bounty_completed",,"BountyScriptExample",,, - +,,,,,, +magiclib_fleetencounter_shiny,BeginFleetEncounter,DoesFleetHaveShiny score:9999998,"FireBest BeginFleetEncounter +AddText ""One of the ships displayed on sensors appears to have a non-standard paintjob."" +Highlight ""paintjob""",,, +,,,,,, +#magiclib_test_bounty_completed,magiclib_test_bounty_completed,,BountyScriptExample,,, diff --git a/data/config/LunaSettings.csv b/data/config/LunaSettings.csv index a022aa50..72b9c32c 100644 --- a/data/config/LunaSettings.csv +++ b/data/config/LunaSettings.csv @@ -17,8 +17,8 @@ magiclib_subsystems_InfoKeyBind,Info Keybind,Keycode,23,,"Keybind for viewing su magiclib_subsystems_KeyBindText,Description,Text,"Keybinds for subsystems. They are used in order. If any are empty they will be ignored and the next available keybind will be used.",,,,, # 56 = left alt magiclib_subsystems_KeyBind1,Keybind 1,Keycode,56,,"First assigned keybind",,, -# 48 = KEY_B -magiclib_subsystems_KeyBind2,Keybind 2,Keycode,48,,"Second assigned keybind",,, +# 21 = KEY_Y +magiclib_subsystems_KeyBind2,Keybind 2,Keycode,21,,"Second assigned keybind",,, # 35 = KEY_H magiclib_subsystems_KeyBind3,Keybind 3,Keycode,35,,"Third assigned keybind",,, # 36 = KEY_J diff --git a/data/config/magic_achievements.csv b/data/config/magic_achievements.csv index fb1fc05f..c55fcb86 100644 --- a/data/config/magic_achievements.csv +++ b/data/config/magic_achievements.csv @@ -4,7 +4,7 @@ id,name,description,tooltip,script,image,spoilerLevel,rarity, # description: Required. The description of the achievement. Use past tense ("Killed 10 ships"). Aim for one or two sentences. # tooltip: Optional. A hardcoded tooltip to display when the player hovers over the achievement in the achievement list, # for example for any detailed explanation that doesn't fit in the description. -# script: Required. The fully qualified name of the script (Java/Kotlin) class that implements the achievement. Must be a subclass of MagicAchievement. +# script: Optional. The fully qualified name of the script (Java/Kotlin) class that implements the achievement. Must be a subclass of MagicAchievement. If not provided, achievement should be unlocked by your code somewhere else. # image: Optional. The path to the image to display for the achievement once unlocked. If not specified, the image will be based on rarity. # spoilerLevel: Optional. Think about how the visibility may incentivize the player's behavior. # VISIBLE (default): The achievement is visible to the player at all times, and its progress is shown. @@ -18,3 +18,4 @@ id,name,description,tooltip,script,image,spoilerLevel,rarity, # Examples #magiclib_exampleSimple,"Example Regular Achievement","Created your first achievement.",,org.magiclib.achievements.ExampleSimpleAchievement,,VISIBLE,UNCOMMON #magiclib_exampleCustom,"Example Customized Achievement","Created your first achievement with a custom icon and tooltip.","Optional tooltip. To learn how to create an achievement, read the documentation!",org.magiclib.achievements.ExampleCustomAchievement,"/graphics/magic/icons/example_achievement.png",SPOILER,RARE +#magiclib_exampleNoScript,"Example Scriptless Achievement","Created your first achievement without a script.","Optional tooltip. To learn how to create an achievement, read the documentation!",,"/graphics/magic/icons/example_achievement.png",SPOILER,RARE \ No newline at end of file diff --git a/data/config/modFiles/magicBounty_data.json b/data/config/modFiles/magicBounty_data.json index 7694ba76..ea372ac8 100644 --- a/data/config/modFiles/magicBounty_data.json +++ b/data/config/modFiles/magicBounty_data.json @@ -74,6 +74,7 @@ "fleet_flagship_alwaysRecoverable":false, "fleet_preset_ships":{ "tempest_Attack":2, + #"asdf":1, #"shade_Assault":1, #"omen_PD":2, "revenant_Elite":1, diff --git a/data/config/settings.json b/data/config/settings.json index 0d0d9942..72197e91 100644 --- a/data/config/settings.json +++ b/data/config/settings.json @@ -59,6 +59,7 @@ } }, "ruleCommandPackages": [ - "org.magiclib.bounty.rulecmd" + "org.magiclib.bounty.rulecmd", + "org.magiclib.paintjobs.rulecmd" ] } \ No newline at end of file diff --git a/data/console/commands.csv b/data/console/commands.csv index 52f1c4cb..3d8776ea 100644 --- a/data/console/commands.csv +++ b/data/console/commands.csv @@ -2,4 +2,5 @@ command,class,tags,syntax,help MagicLib_ListBounties,org.magiclib.bounty.console.ListBountiesCommand,"MagicLib,campaign",MagicLib_ListBounties [optionalBountyId],Lists bounties loaded on MagicLib's Bounty Board. MagicLib_ListRequirements,org.magiclib.bounty.console.ListBountiesRequirementsCommand,"MagicLib,campaign",MagicLib_ListRequirements [optionalBountyId],Lists the currently active bounties loaded on MagicLib's Bounty Board. and their requirements to show up. MagicLib_ResetBounty,org.magiclib.bounty.console.ResetBountyCommand,"MagicLib,campaign",MagicLib_ResetBounty ,Resets a MagicLib bounty. +MagicLib_ListPaintjobs,org.magiclib.paintjobs.console.ListPaintjobsCommand,"MagicLib,campaign",ListPaintjobsCommand,"List all paintjobs." MagicLib_UnlockPaintjob,org.magiclib.paintjobs.console.UnlockPaintjobCommand,"MagicLib,campaign",MagicLib_UnlockPaintjob [all|],"Use 'all' to unlock all paintjobs permanently, or specify an id. Run with no arguments to list all." diff --git a/dokka/dokka-base-1.9.20.jar b/dokka/dokka-base-1.9.20.jar new file mode 100644 index 00000000..870d5143 Binary files /dev/null and b/dokka/dokka-base-1.9.20.jar differ diff --git a/dokka/dokka-cli-1.9.20.jar b/dokka/dokka-cli-1.9.20.jar new file mode 100644 index 00000000..c1a8b83b Binary files /dev/null and b/dokka/dokka-cli-1.9.20.jar differ diff --git a/jars/MagicLib-Kotlin.jar b/jars/MagicLib-Kotlin.jar index 853b61b1..34711cf0 100644 Binary files a/jars/MagicLib-Kotlin.jar and b/jars/MagicLib-Kotlin.jar differ diff --git a/jars/MagicLib.jar b/jars/MagicLib.jar index 8b5cec99..49889cab 100644 Binary files a/jars/MagicLib.jar and b/jars/MagicLib.jar differ diff --git a/lib/hotswap-agent.jar b/lib/hotswap-agent.jar new file mode 100644 index 00000000..70408e53 Binary files /dev/null and b/lib/hotswap-agent.jar differ diff --git a/magiclib.version b/magiclib.version index c2ba6c43..e8c02dbe 100644 --- a/magiclib.version +++ b/magiclib.version @@ -8,6 +8,6 @@ { "major": "1", "minor": "5", - "patch": "4-RC01" + "patch": "7" } } diff --git a/mod_info.json b/mod_info.json index dd39f582..c2714ba9 100644 --- a/mod_info.json +++ b/mod_info.json @@ -4,7 +4,7 @@ "author": "Modding Community: Dark.Revenant, LazyWizard, Nicke, Originem, Rubi, Schaf-Unschaf, Snrasha, Tartiflette, Wisp, Wyvern...", "utility": "false", # Using the long version format lets the game correctly compare major/minor/patch versions. - "version": { "major": '1', "minor": '5', "patch": '4' }, + "version": { "major": '1', "minor": '5', "patch": '7' }, "description": "A collection of classes to aid modding. Not a mod in itself, but required by other mods.", "gameVersion": "0.98a-RC7", "dependencies": [ diff --git a/src/org/magiclib/achievements/MagicAchievement.java b/src/org/magiclib/achievements/MagicAchievement.java index 73a42b1c..3e8adfb8 100644 --- a/src/org/magiclib/achievements/MagicAchievement.java +++ b/src/org/magiclib/achievements/MagicAchievement.java @@ -215,11 +215,11 @@ public void advanceInCombat(float amount, List events, boolean is */ public void saveChanges() { getLogger().info("Saving achievements triggered by '" + spec.getId() + "' from mod '" + spec.getModName() + "'."); - MagicAchievementManager.getInstance().saveAchievements(true); + MagicAchievementManager.getInstance().saveAchievements(true, false); } private void saveChangesWithoutLogging() { - MagicAchievementManager.getInstance().saveAchievements(false); + MagicAchievementManager.getInstance().saveAchievements(false, false); } /** diff --git a/src/org/magiclib/achievements/MagicAchievementManager.java b/src/org/magiclib/achievements/MagicAchievementManager.java index fd2b65f3..0032e20d 100644 --- a/src/org/magiclib/achievements/MagicAchievementManager.java +++ b/src/org/magiclib/achievements/MagicAchievementManager.java @@ -150,7 +150,7 @@ public void setAchievementsEnabled(boolean areAchievementsEnabled, boolean isSav } else { logger.info("MagicLib achievements are disabled."); removeIntel(); - saveAchievements(true); + saveAchievements(true, false); if (isSaveLoaded) { Global.getSector().removeTransientScriptsOfClass(MagicAchievementRunner.class); @@ -221,13 +221,13 @@ public Map getAchievements() { /** * This writes to disk. */ - protected void saveAchievements(boolean printUnchangedResultToLog) { + protected void saveAchievements(boolean printUnchangedResultToLog, boolean forceSave) { JSONObject commonJson; JSONArray savedAchievements = new JSONArray(); // Prevents accidentally wiping achievements if the feature is disabled on game load. // Also, no reason to save nothing anyway. - if (achievements.isEmpty()) { + if (!forceSave && achievements.isEmpty()) { return; } @@ -246,7 +246,7 @@ protected void saveAchievements(boolean printUnchangedResultToLog) { try { newAchievementsJsonString = savedAchievements.toString(indent); - if (newAchievementsJsonString.equals(lastSavedJson)) { + if (!forceSave && newAchievementsJsonString.equals(lastSavedJson)) { if (printUnchangedResultToLog) { logger.info("Not saving achievements because they haven't changed."); } @@ -303,7 +303,7 @@ public void reloadAchievements(boolean isSaveGameLoaded) { try { // Create file if it doesn't exist. if (!Global.getSettings().fileExistsInCommon(commonFilename)) { - saveAchievements(true); + saveAchievements(true, true); } try { @@ -314,7 +314,7 @@ public void reloadAchievements(boolean isSaveGameLoaded) { logger.warn("Unable to load achievements from " + commonFilename + ", making a backup and remaking it.", ex); Global.getSettings().writeTextFileToCommon(commonFilename + ".backup", Global.getSettings().readTextFileFromCommon(commonFilename)); Global.getSettings().deleteTextFileFromCommon(commonFilename); - saveAchievements(true); + saveAchievements(true, true); commonJson = JSONUtils.loadCommonJSON(commonFilename); savedAchievementsJson = commonJson.getJSONArray(achievementsJsonObjectKey); } @@ -406,8 +406,14 @@ public void reloadAchievements(boolean isSaveGameLoaded) { Map newAchievementsById = new HashMap<>(); for (MagicAchievementSpec spec : specs.values()) { + String script = spec.getScript(); + + if (script == null || script.isBlank()) { + script = MagicAchievement.class.getCanonicalName(); + } + try { - final Class commandClass = Global.getSettings().getScriptClassLoader().loadClass(spec.getScript()); + final Class commandClass = Global.getSettings().getScriptClassLoader().loadClass(script); if (!MagicAchievement.class.isAssignableFrom(commandClass)) { throw new RuntimeException(String.format("%s does not extend %s", commandClass.getCanonicalName(), MagicAchievement.class.getCanonicalName())); } @@ -415,10 +421,10 @@ public void reloadAchievements(boolean isSaveGameLoaded) { MagicAchievement magicAchievement = (MagicAchievement) commandClass.newInstance(); magicAchievement.spec = spec; newAchievementsById.put(spec.getId(), magicAchievement); - logger.info("Loaded achievement " + spec.getId() + " from " + spec.getModId() + " with script " + spec.getScript() + "."); + logger.info("Loaded achievement " + spec.getId() + " from " + spec.getModId() + " with script " + script + "."); } catch (Exception e) { if (spec != null) - logger.warn(String.format("Unable to load achievement '%s' because class '%s' didn't load!", spec.getId(), spec.getScript()), e); + logger.warn(String.format("Unable to load achievement '%s' because class '%s' didn't load!", spec.getId(), script), e); else logger.warn("Unable to load achievement because spec was null! What are you doing?!", e); } @@ -452,6 +458,9 @@ public void reloadAchievements(boolean isSaveGameLoaded) { try { JSONObject item = modCsv.getJSONObject(i); id = item.getString("id").trim(); + + if (id.isBlank()) continue; + String name = item.getString("name").trim(); String description = item.getString("description").trim(); String tooltip = item.getString("tooltip").trim(); diff --git a/src/org/magiclib/achievements/MagicAchievementSpec.kt b/src/org/magiclib/achievements/MagicAchievementSpec.kt index 3cc19b4f..97480cae 100644 --- a/src/org/magiclib/achievements/MagicAchievementSpec.kt +++ b/src/org/magiclib/achievements/MagicAchievementSpec.kt @@ -13,7 +13,7 @@ open class MagicAchievementSpec( var name: String, var description: String, var tooltip: String?, - var script: String, + var script: String?, var image: String?, var spoilerLevel: MagicAchievementSpoilerLevel, var rarity: MagicAchievementRarity, @@ -42,7 +42,7 @@ open class MagicAchievementSpec( val name = json.getString("name") val description = json.getString("description") val tooltip = json.optString("tooltip", null) - val script = json.getString("script") + val script = json.optString("script", null) val image = json.optString("image", null) val spoilerLevel = MagicAchievementSpoilerLevel.valueOf( json.optString("spoilerLevel", "VISIBLE").lowercase().let { Misc.ucFirst(it) }) diff --git a/src/org/magiclib/achievements/MagicTargetListAchievement.java b/src/org/magiclib/achievements/MagicTargetListAchievement.java index 39966828..acda7da7 100644 --- a/src/org/magiclib/achievements/MagicTargetListAchievement.java +++ b/src/org/magiclib/achievements/MagicTargetListAchievement.java @@ -19,7 +19,7 @@ public class MagicTargetListAchievement extends MagicAchievement { protected String KEY = "magictargetlist_targets"; public Map getTargets() { - Object obj = getMemory().get(KEY); + Object obj = getAchievementMemory().get(KEY); if (obj == null) { return new HashMap<>(); @@ -30,7 +30,7 @@ public Map getTargets() { JSONObject jsonTargets = (JSONObject) obj; Map result = new HashMap<>(jsonTargets.length()); - for (Iterator it = jsonTargets.keys(); it.hasNext(); ) { + for (var it = jsonTargets.keys(); it.hasNext(); ) { String key = (String) it.next(); JSONObject jsonData = jsonTargets.getJSONObject(key); result.put(key, new Data(jsonData.getString(Data.DISPLAY_NAME), jsonData.getBoolean(Data.IS_COMPLETE))); @@ -85,7 +85,7 @@ public void setTargets(Map targets) { savedTargets.remove(key); } - getMemory().put(KEY, savedTargets); + getAchievementMemory().put(KEY, savedTargets); } /** @@ -100,7 +100,7 @@ public void addTarget(String targetKey, String displayName) { if (savedTargets.containsKey(targetKey)) return; savedTargets.put(targetKey, new Data(displayName)); - getMemory().put(KEY, savedTargets); + getAchievementMemory().put(KEY, savedTargets); } /** @@ -126,7 +126,7 @@ public void setTargetComplete(String targetKey, boolean isComplete) { savedTargets.put(targetKey, new Data(targetKey, isComplete)); } - getMemory().put(KEY, savedTargets); + getAchievementMemory().put(KEY, savedTargets); } @Override @@ -138,7 +138,7 @@ public void advanceAfterInterval(float amount) { return; // If all targets are complete, complete the achievement. - if (shouldComplete(targets)) return; + if (!shouldComplete(targets)) return; completeAchievement(); saveChanges(); @@ -150,13 +150,14 @@ public void advanceInCombat(float amount, List events, boolean is super.advanceInCombat(amount, events, isSimulation); if (isSimulation) return; + if (isComplete()) return; Map targets = getTargets(); if (targets.isEmpty()) return; - if (shouldComplete(targets)) return; + if (!shouldComplete(targets)) return; completeAchievement(); saveChanges(); @@ -181,10 +182,10 @@ public boolean shouldComplete(Map targets) { for (Data data : targets.values()) { if (!data.isComplete) { - return true; + return false; } } - return false; + return true; } @Override @@ -214,12 +215,7 @@ public void createTooltip(@NotNull TooltipMakerAPI tooltipMakerAPI, boolean isEx tooltipMakerAPI.setBulletedListMode(" - "); List values = new ArrayList<>(getTargets().values()); - Collections.sort(values, new Comparator() { - @Override - public int compare(Data o1, Data o2) { - return o1.displayName.compareTo(o2.displayName); - } - }); + values.sort(Comparator.comparing(o -> o.displayName)); for (Data data : values) { tooltipMakerAPI.addPara(data.displayName, data.isComplete ? Misc.getTextColor() : Misc.getNegativeHighlightColor(), 0f); diff --git a/src/org/magiclib/achievements/ShipKillsAchievement.kt b/src/org/magiclib/achievements/ShipKillsAchievement.kt index 4b3f7732..fce6d2c6 100644 --- a/src/org/magiclib/achievements/ShipKillsAchievement.kt +++ b/src/org/magiclib/achievements/ShipKillsAchievement.kt @@ -8,6 +8,7 @@ import com.fs.starfarer.api.combat.listeners.ApplyDamageResultAPI import com.fs.starfarer.api.combat.listeners.DamageListener import com.fs.starfarer.api.input.InputEventAPI import com.fs.starfarer.api.mission.FleetSide +import org.magiclib.kotlin.magicJoinToString import org.magiclib.paintjobs.MagicPaintjobManager /** @@ -27,10 +28,13 @@ import org.magiclib.paintjobs.MagicPaintjobManager * @property killCount Number of ships the player must kill to complete this achievement. * @property rewardedPaintjobIds Paintjob IDs to unlock when this achievement is completed. */ -abstract class ShipKillsAchievement( + +abstract class ShipKillsAchievement @JvmOverloads constructor( val playerShipHullIds: List? = null, val killCount: Float, - val rewardedPaintjobIds: List + val rewardedPaintjobIds: List = emptyList(), + var generateDescription: Boolean = true, + var generateTooltip: Boolean = true, ) : MagicAchievement() { companion object { val shipIdToListener: MutableMap = @@ -80,6 +84,51 @@ abstract class ShipKillsAchievement( applyShipListeners(ships) } + override fun getDescription(): String { + if (generateDescription) { + val sb = StringBuilder("Destroy ${killCount.toInt()} enemies") + + if (playerShipHullIds?.isNotEmpty() == true) { + sb.append( + " using a ${ + playerShipHullIds.magicJoinToString { + runCatching { + Global.getSettings().getHullSpec(it).hullName + }.getOrDefault(it) + } + }" + ) + } + + sb.append(".") + return sb.toString() + } else { + return super.getDescription() + } + } + + override fun getTooltip(): String? { + return if (generateTooltip && rewardedPaintjobIds.isNotEmpty()) { + buildString { + append("Awards the ") + append( + rewardedPaintjobIds.magicJoinToString( + twoElementSeparator = " and ", + manyElementsFinalSeparator = ", and " + ) { + runCatching { + MagicPaintjobManager.getPaintjob(it)!!.name + }.getOrDefault(it) + }) + append(" paintjob") + if (rewardedPaintjobIds.size > 1) append("s") + append(" when completed.") + } + } else { + super.getTooltip() + } + } + override fun onDestroyed() { super.onDestroyed() shipIdToListener.clear() diff --git a/src/org/magiclib/bounty/ActiveBounty.java b/src/org/magiclib/bounty/ActiveBounty.java index 19eabe19..4e54b559 100644 --- a/src/org/magiclib/bounty/ActiveBounty.java +++ b/src/org/magiclib/bounty/ActiveBounty.java @@ -195,6 +195,7 @@ public void acceptBounty(@NotNull SectorEntityToken bountySource, @Nullable Floa if (intelForBounty == null) { intelForBounty = new MagicBountyIntel(bountyKey); intelManager.addIntel(intelForBounty); + intelForBounty.setImportant(true); } if (MagicTxt.nullStringIfEmpty(spec.job_memKey) != null) { diff --git a/src/org/magiclib/bounty/MagicBountyBattleListener.java b/src/org/magiclib/bounty/MagicBountyBattleListener.java index 9fd9af5f..ae8382b6 100644 --- a/src/org/magiclib/bounty/MagicBountyBattleListener.java +++ b/src/org/magiclib/bounty/MagicBountyBattleListener.java @@ -10,7 +10,9 @@ import com.fs.starfarer.api.impl.campaign.fleets.AutoDespawnScript; import org.jetbrains.annotations.NotNull; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Ends bounties based on battle results. @@ -24,31 +26,68 @@ public final class MagicBountyBattleListener implements FleetEventListener { private boolean isDone = false; @NotNull - private final String bountyKey; + private Set bountyKeys; + @Deprecated + private String bountyKey; + + public MagicBountyBattleListener(@NotNull Set bountyKeys) { + this.bountyKeys = bountyKeys; + } + + /** + * Use `MagicBountyBattleListener(Set bountyKeys)` instead. + */ + @Deprecated public MagicBountyBattleListener(@NotNull String bountyKey) { - this.bountyKey = bountyKey; + this.bountyKeys = new HashSet<>(); + this.bountyKeys.add(bountyKey); } + /** + * If a key is already present, does nothing. + */ + public void addBountyKey(@NotNull String bountyKey) { + handleBackwardsCompat(); + this.bountyKeys.add(bountyKey); + } + + /** + * If the fleet has despawned, end all bounties on the fleet as [EndedWithoutPlayerInvolvement]. + */ @Override public void reportFleetDespawnedToListener(CampaignFleetAPI fleet, CampaignEventListener.FleetDespawnReason reason, Object param) { if (isDone) { return; } + // Backwards compatibility. + handleBackwardsCompat(); + fleet.removeEventListener(this); - ActiveBounty bounty = MagicBountyCoordinator.getInstance().getActiveBounty(bountyKey); + ActiveBounty firstAcceptedBountyOnFleet = null; - if (bounty == null) return; + for (String key : bountyKeys) { + firstAcceptedBountyOnFleet = MagicBountyCoordinator.getInstance().getActiveBounty(key); + if (firstAcceptedBountyOnFleet != null && firstAcceptedBountyOnFleet.getStage() == ActiveBounty.Stage.Accepted) { + break; + } + } + + if (firstAcceptedBountyOnFleet == null) return; - if (fleet.getId().equals(bounty.getFleet().getId())) { + if (fleet.getId().equals(firstAcceptedBountyOnFleet.getFleet().getId())) { fleet.setCommander(fleet.getFaction().createRandomPerson()); - bounty.endBounty(new ActiveBounty.BountyResult.EndedWithoutPlayerInvolvement()); - // Quietly despawn the fleet when player goes away, since they can't complete the bounty. + for (String key : bountyKeys) { + ActiveBounty bounty = MagicBountyCoordinator.getInstance().getActiveBounty(key); + if (bounty != null) { + bounty.endBounty(new ActiveBounty.BountyResult.EndedWithoutPlayerInvolvement()); + } + } + Global.getSector().addScript(new AutoDespawnScript(fleet)); - return; } } @@ -59,24 +98,39 @@ public void reportFleetDespawnedToListener(CampaignFleetAPI fleet, CampaignEvent */ @Override public void reportBattleOccurred(CampaignFleetAPI bountyFleet, CampaignFleetAPI winningFleet, BattleAPI battle) { - ActiveBounty bounty = MagicBountyCoordinator.getInstance().getActiveBounty(bountyKey); + handleBackwardsCompat(); + + ActiveBounty firstAcceptedBountyOnFleet = null; + + for (String key : bountyKeys) { + firstAcceptedBountyOnFleet = MagicBountyCoordinator.getInstance().getActiveBounty(key); + if (firstAcceptedBountyOnFleet != null && firstAcceptedBountyOnFleet.getStage() == ActiveBounty.Stage.Accepted) { + break; + } + } + CampaignFleetAPI playerFleet = Global.getSector().getPlayerFleet(); - if (bounty == null) return; + if (firstAcceptedBountyOnFleet == null) return; /////////// Below is copied (and heavily modified) from PersonBountyIntel.reportBattleOccurred. if (isDone) return; boolean playerInvolved = battle.isPlayerInvolved(); - if (bountyFleet.getId().equals(bounty.getFleet().getId())) { - PersonAPI bountyCommander = bounty.getCaptain(); + if (bountyFleet.getId().equals(firstAcceptedBountyOnFleet.getFleet().getId())) { + PersonAPI bountyCommander = firstAcceptedBountyOnFleet.getCaptain(); if (battle.isInvolved(bountyFleet) && !playerInvolved) { if (bountyFleet.getFlagship() == null || bountyFleet.getFlagship().getCaptain() != bountyCommander) { bountyFleet.setCommander(bountyFleet.getFaction().createRandomPerson()); //Global.getSector().reportEventStage(this, "other_end", market.getPrimaryEntity(), messagePriority); - bounty.endBounty(new ActiveBounty.BountyResult.EndedWithoutPlayerInvolvement()); + for (String key : bountyKeys) { + ActiveBounty bounty = MagicBountyCoordinator.getInstance().getActiveBounty(key); + if (bounty != null) { + bounty.endBounty(new ActiveBounty.BountyResult.EndedWithoutPlayerInvolvement()); + } + } // Quietly despawn the fleet when player goes away, since they can't complete the bounty. Global.getSector().addScript(new AutoDespawnScript(bountyFleet)); // result = new PersonBountyIntel.BountyResult(PersonBountyIntel.BountyResultType.END_OTHER, 0, null); @@ -94,12 +148,12 @@ public void reportBattleOccurred(CampaignFleetAPI bountyFleet, CampaignFleetAPI boolean didPlayerSalvageFlagship = false; List bountyFleetBeforeBattle = bountyFleet.getFleetData().getSnapshot(); - if (bounty.getFlagshipId() != null) { + if (firstAcceptedBountyOnFleet.getFlagshipId() != null) { for (FleetMemberAPI fleetMember : playerFleet.getFleetData().getMembersListCopy()) { for (FleetMemberAPI ship : bountyFleetBeforeBattle) { // Look for the flagship of the bounty fleet's presence in the player fleet. - if (fleetMember.getId().equals(bounty.getFlagshipId()) && fleetMember.getId().equals(ship.getId())) { + if (fleetMember.getId().equals(firstAcceptedBountyOnFleet.getFlagshipId()) && fleetMember.getId().equals(ship.getId())) { Global.getLogger(MagicBountyBattleListener.class).info(String.format("Player salvaged flagship %s (%s)", ship.getShipName(), ship.getId())); didPlayerSalvageFlagship = true; } @@ -107,45 +161,87 @@ public void reportBattleOccurred(CampaignFleetAPI bountyFleet, CampaignFleetAPI } } - switch (bounty.getSpec().job_type) { - case Assassination: - if (didDisableOrDestroyOriginalFlagship) { - bounty.endBounty(new ActiveBounty.BountyResult.Succeeded(true)); - isDone = true; - } + for (String key : bountyKeys) { + ActiveBounty bounty = MagicBountyCoordinator.getInstance().getActiveBounty(key); + if (bounty == null) { + continue; + } + + // Skip bounties that have already been completed (not in "ready to accept" or "accepted" stages) + if (bounty.getStage() != ActiveBounty.Stage.NotAccepted && bounty.getStage() != ActiveBounty.Stage.Accepted) { + continue; + } - break; - case Destruction: - if (didDisableOrDestroyOriginalFlagship) - if (!didPlayerSalvageFlagship) { + // Go through each bounty's win conditions and end the bounty if met. + switch (bounty.getSpec().job_type) { + case Assassination: + if (didDisableOrDestroyOriginalFlagship) { bounty.endBounty(new ActiveBounty.BountyResult.Succeeded(true)); - isDone = true; - } else { - // If the bounty required destroying the target, but player salvaged their ship, they don't get credits. - bounty.endBounty(new ActiveBounty.BountyResult.FailedSalvagedFlagship()); - isDone = true; } - break; - case Obliteration: - if (bountyFleet.getFleetSizeCount() <= 0) { - bounty.endBounty(new ActiveBounty.BountyResult.Succeeded(true)); - isDone = true; - } + break; + case Destruction: + if (didDisableOrDestroyOriginalFlagship) + if (!didPlayerSalvageFlagship) { + bounty.endBounty(new ActiveBounty.BountyResult.Succeeded(true)); + } else { + // If the bounty required destroying the target, but player salvaged their ship, they don't get credits. + bounty.endBounty(new ActiveBounty.BountyResult.FailedSalvagedFlagship()); + } + + break; + case Obliteration: + if (bountyFleet.getFleetSizeCount() <= 0) { + bounty.endBounty(new ActiveBounty.BountyResult.Succeeded(true)); + } - break; - case Neutralization: - case Neutralisation: - float fpPostFight = bountyFleet.getFleetPoints(); + break; + case Neutralization: + case Neutralisation: + float fpPostFight = bountyFleet.getFleetPoints(); + + if ((fpPostFight / bounty.getInitialBountyFleetPoints()) <= (1f / 3f)) { + bounty.endBounty(new ActiveBounty.BountyResult.Succeeded(true)); + } - if ((fpPostFight / bounty.getInitialBountyFleetPoints()) <= (1f / 3f)) { - bounty.endBounty(new ActiveBounty.BountyResult.Succeeded(true)); - isDone = true; + break; + } + } + + if (bountyFleet.getFleetSizeCount() <= 0) { + // Fleet is totally dead, player is not gonna be starting another battle to finish another bounty on this fleet, so cancel out the listener. + isDone = true; + } else { + // Handle the case where a battle occurred but player didn't complete all bounties. + // We keep the BattleListener running until all bounties on the fleet have been completed (or the fleet is defeated, in reportFleetDespawnedToListener). + boolean hasUnfinishedBounty = false; + + for (String key : bountyKeys) { + ActiveBounty bounty = MagicBountyCoordinator.getInstance().getActiveBounty(key); + if (bounty != null) { + if (bounty.getStage() == ActiveBounty.Stage.NotAccepted || bounty.getStage() == ActiveBounty.Stage.Accepted) { + hasUnfinishedBounty = true; + } } + } - break; + if (!hasUnfinishedBounty) { + isDone = true; + } } + } + } + + /** + * If restoring a save from before `bountyKeys` was added, migrate `bountyKey` to `bountyKeys`. + */ + private void handleBackwardsCompat() { + if (bountyKeys == null) { + bountyKeys = new HashSet<>(); + } + if (bountyKey != null) { + bountyKeys.add(bountyKey); } } } diff --git a/src/org/magiclib/bounty/MagicBountyCoordinator.java b/src/org/magiclib/bounty/MagicBountyCoordinator.java index d3d853ef..270417f7 100644 --- a/src/org/magiclib/bounty/MagicBountyCoordinator.java +++ b/src/org/magiclib/bounty/MagicBountyCoordinator.java @@ -478,6 +478,8 @@ public void configureBountyListeners() { for (FleetEventListener eventListener : bounty.getFleet().getEventListeners()) { if (eventListener instanceof MagicBountyBattleListener) { doesBountyHaveListener = true; + // Attempt to add this bounty key if it doesn't already exist, along with the already present one. This lets multiple bounties target the same fleet. + ((MagicBountyBattleListener) eventListener).addBountyKey(bounty.getKey()); break; } } diff --git a/src/org/magiclib/bounty/intel/MagicBountyInfo.kt b/src/org/magiclib/bounty/intel/MagicBountyInfo.kt index 859a63bb..d1d02bb9 100644 --- a/src/org/magiclib/bounty/intel/MagicBountyInfo.kt +++ b/src/org/magiclib/bounty/intel/MagicBountyInfo.kt @@ -4,7 +4,6 @@ import com.fs.starfarer.api.Global import com.fs.starfarer.api.campaign.LocationAPI import com.fs.starfarer.api.campaign.StarSystemAPI import com.fs.starfarer.api.campaign.comm.IntelInfoPlugin -import com.fs.starfarer.api.campaign.rules.MemoryAPI import com.fs.starfarer.api.fleet.FleetMemberAPI import com.fs.starfarer.api.impl.campaign.ids.Factions import com.fs.starfarer.api.impl.campaign.rulecmd.salvage.special.BreadcrumbSpecial @@ -13,6 +12,7 @@ import com.fs.starfarer.api.ui.LabelAPI import com.fs.starfarer.api.ui.MapParams import com.fs.starfarer.api.ui.TooltipMakerAPI import com.fs.starfarer.api.util.Misc +import org.lwjgl.input.Keyboard import org.magiclib.bounty.ActiveBounty import org.magiclib.bounty.MagicBountyCoordinator import org.magiclib.bounty.MagicBountyLoader.* @@ -141,15 +141,19 @@ open class MagicBountyInfo(val bountyKey: String, val bountySpec: MagicBountySpe } //CHECK FOR EXISTING FLEET - if (bountySpec.existing_target_memkey != null) { - var targetFleetGone = true + if (bountySpec.existing_target_memkey != null) { // Bounty has pre-existing target + var targetFleetFound = false for (s in Global.getSector().starSystems) { for (f in s.fleets) { if (f.memoryWithoutUpdate.contains(bountySpec.existing_target_memkey)) { - // The fleet already exists, so don't offer the bounty. - return false + targetFleetFound = true + break } } + if (targetFleetFound) break + } + if (!targetFleetFound) { // If the bounty was intended to target an existing fleet, and no fleet was found + return false // Don't show it, as the target fleet does not exist } } @@ -261,6 +265,7 @@ open class MagicBountyInfo(val bountyKey: String, val bountySpec: MagicBountySpe if (activeBountyLocal.stage == ActiveBounty.Stage.NotAccepted) { val acceptButton = actionTooltip.addButton(bountySpec.job_pick_option, null, rightPanelWidth, 24f, 0f) + acceptButton.setShortcut(Keyboard.KEY_T, true) rightPanelPlugin.addButton(acceptButton) { acceptButton.isChecked = false activeBountyLocal.let { diff --git a/src/org/magiclib/paintjobs/MagicPaintjobManager.kt b/src/org/magiclib/paintjobs/MagicPaintjobManager.kt index 2d2cb209..f6122896 100644 --- a/src/org/magiclib/paintjobs/MagicPaintjobManager.kt +++ b/src/org/magiclib/paintjobs/MagicPaintjobManager.kt @@ -11,6 +11,7 @@ import com.fs.starfarer.api.combat.ShipVariantAPI import com.fs.starfarer.api.combat.WeaponAPI import com.fs.starfarer.api.fleet.FleetMemberAPI import com.fs.starfarer.api.graphics.SpriteAPI +import com.fs.starfarer.api.util.IntervalUtil import com.fs.starfarer.combat.entities.ship.trackers.MultiBarrelRecoilTracker import org.dark.shaders.util.ShaderLib import org.json.JSONArray @@ -42,6 +43,7 @@ object MagicPaintjobManager { private val unlockedPaintjobsInner = mutableSetOf() private val paintjobsInner = mutableListOf() private val completedPaintjobIdsThatUserHasBeenNotifiedFor = mutableListOf() + private val advanceIntervalUtil = IntervalUtil(1f, 1f) private val weaponPaintjobsInner = mutableListOf() @@ -116,7 +118,9 @@ object MagicPaintjobManager { loadUnlockedPaintjobs() initIntel() - Global.getSector().addTransientListener(MagicPaintjobShinyAdder()) + val shinyAdder = MagicPaintjobShinyAdder() + shinyAdder.checkAndApplyShiniesToAllFleetsInPlayerLocation() + Global.getSector().addTransientScript(shinyAdder) if (!Global.getSector().hasTransientScript(MagicPaintjobRunner::class.java)) { Global.getSector().addTransientScript(MagicPaintjobRunner()) } @@ -331,12 +335,12 @@ object MagicPaintjobManager { return@forEach } - /* TODO: Uncomment this for 0.98 + /* TODO: Uncomment this for 0.98 */ if (!Global.getSettings().actuallyAllWeaponSpecs.any { it.weaponId in weaponIds }) { logger.warn("Weapon Paintjob with id: $id has no valid weaponId's, skipping.") return@forEach } - */ + val spriteMap = weaponPJEntry.getString("spriteMap").split(",").mapNotNull { mapEntry -> val parts = mapEntry.split("->") if (parts.size == 2) { @@ -679,24 +683,29 @@ object MagicPaintjobManager { @JvmStatic fun advance(amount: Float) { if (!isEnabled) return - val intel = getIntel() ?: return - - // For all paintjobs that were just unlocked, show intel update. - // Only notify intel if in campaign and not showing a dialog. - // If in combat, the intel will be shown when the player returns to the campaign. - if (Global.getCurrentState() == GameState.CAMPAIGN && Global.getSector().campaignUI.currentInteractionDialog == null) { - for (paintjob in getPaintjobs()) { - if (paintjob.isUnlocked() && !completedPaintjobIdsThatUserHasBeenNotifiedFor.contains(paintjob.id)) { - // Player has unlocked a new paintjob! Let's notify them. - - try { - intel.tempPaintjobForIntelNotification = paintjob - intel.sendUpdateIfPlayerHasIntel(null, false, false) - intel.tempPaintjobForIntelNotification = null + + advanceIntervalUtil.advance(amount) + + if (advanceIntervalUtil.intervalElapsed()) { + val intel = getIntel() ?: return + + // For all paintjobs that were just unlocked, show intel update. + // Only notify intel if in campaign and not showing a dialog. + // If in combat, the intel will be shown when the player returns to the campaign. + if (Global.getCurrentState() == GameState.CAMPAIGN && Global.getSector().campaignUI.currentInteractionDialog == null) { + for (paintjob in getPaintjobs()) { + if (paintjob.isUnlocked() && !completedPaintjobIdsThatUserHasBeenNotifiedFor.contains(paintjob.id)) { + // Player has unlocked a new paintjob! Let's notify them. + + try { + intel.tempPaintjobForIntelNotification = paintjob + intel.sendUpdateIfPlayerHasIntel(null, false, false) + intel.tempPaintjobForIntelNotification = null // MagicAchievementManager.playSoundEffect(paintjob) - completedPaintjobIdsThatUserHasBeenNotifiedFor.add(paintjob.id) - } catch (e: java.lang.Exception) { - logger.w(ex = e, message = { "Unable to notify intel of paintjob " + paintjob.id }) + completedPaintjobIdsThatUserHasBeenNotifiedFor.add(paintjob.id) + } catch (e: java.lang.Exception) { + logger.w(ex = e, message = { "Unable to notify intel of paintjob " + paintjob.id }) + } } } } diff --git a/src/org/magiclib/paintjobs/MagicPaintjobRefitPanel.kt b/src/org/magiclib/paintjobs/MagicPaintjobRefitPanel.kt index 1ee0e7f1..3a97af70 100644 --- a/src/org/magiclib/paintjobs/MagicPaintjobRefitPanel.kt +++ b/src/org/magiclib/paintjobs/MagicPaintjobRefitPanel.kt @@ -13,17 +13,21 @@ import org.lwjgl.input.Keyboard import org.lwjgl.opengl.GL11 import org.magiclib.ReflectionUtils import org.magiclib.internalextensions.* -import org.magiclib.kotlin.* -import java.awt.Color -import org.magiclib.paintjobs.MagicPaintjobSelector.createPaintjobSelector +import org.magiclib.kotlin.alphaf +import org.magiclib.kotlin.bluef +import org.magiclib.kotlin.greenf +import org.magiclib.kotlin.redf import org.magiclib.paintjobs.MagicPaintjobSelector.MagicPaintjobSelectorPlugin +import org.magiclib.paintjobs.MagicPaintjobSelector.createPaintjobSelector import org.magiclib.util.MagicTxt +import java.awt.Color /** * @author Starficz */ internal object MagicPaintjobRefitPanel { private const val BACKGROUND_ALPHA = 0.7f + internal class MagicPaintjobRefitPanelPlugin(private val refitTab: UIPanelAPI) : BaseCustomUIPanelPlugin() { lateinit var paintjobPanel: CustomPanelAPI @@ -63,11 +67,11 @@ internal object MagicPaintjobRefitPanel { GL11.glPopMatrix() } - private fun drawBorder(x1: Float, y1: Float, x2: Float, y2: Float){ - GL11.glRectf(x1, y1, x2+1, y1-1) - GL11.glRectf(x2, y1, x2+1, y2+1) - GL11.glRectf(x1, y2, x1-1, y1-1) - GL11.glRectf(x2, y2, x1-1, y2+1) + private fun drawBorder(x1: Float, y1: Float, x2: Float, y2: Float) { + GL11.glRectf(x1, y1, x2 + 1, y1 - 1) + GL11.glRectf(x2, y1, x2 + 1, y2 + 1) + GL11.glRectf(x1, y2, x1 - 1, y1 - 1) + GL11.glRectf(x2, y2, x1 - 1, y2 + 1) } override fun processInput(events: MutableList?) { @@ -76,7 +80,8 @@ internal object MagicPaintjobRefitPanel { paintjobPanel.parent?.removeComponent(paintjobPanel) event.consume() } else if (!event.isConsumed && (event.isKeyboardEvent || event.isMouseMoveEvent || - event.isMouseDownEvent || event.isMouseScrollEvent)) { + event.isMouseDownEvent || event.isMouseScrollEvent) + ) { event.consume() } } @@ -86,8 +91,10 @@ internal object MagicPaintjobRefitPanel { } } - internal fun createMagicPaintjobRefitPanel(refitTab: UIPanelAPI, refitPanel : UIPanelAPI, - width: Float, height: Float): CustomPanelAPI { + internal fun createMagicPaintjobRefitPanel( + refitTab: UIPanelAPI, refitPanel: UIPanelAPI, + width: Float, height: Float + ): CustomPanelAPI { val endPad = 6f val midPad = 5f val selectorsPerRow = 3 @@ -97,7 +104,7 @@ internal object MagicPaintjobRefitPanel { paintjobPlugin.paintjobPanel = paintjobPanel // borders are drawn outside of panel, so +2 needed to lineup scrollbar with border - val scrollerTooltip = paintjobPanel.createUIElement(width+2f, height, true) + val scrollerTooltip = paintjobPanel.createUIElement(width + 2f, height, true) scrollerTooltip.position.inTL(0f, 0f) val shipDisplay = ReflectionUtils.invoke("getShipDisplay", refitPanel) as UIPanelAPI @@ -105,9 +112,10 @@ internal object MagicPaintjobRefitPanel { val currentPaintjob = MagicPaintjobManager.getCurrentShipPaintjob(baseVariant) val baseHullPaintjobs = MagicPaintjobManager.getPaintjobsForHull( - (baseVariant as ShipVariantAPI).hullSpec, false) + (baseVariant as ShipVariantAPI).hullSpec, false + ) - val selectorWidth = (paintjobPanel.width-(endPad*2+midPad*(selectorsPerRow-1)))/selectorsPerRow + val selectorWidth = (paintjobPanel.width - (endPad * 2 + midPad * (selectorsPerRow - 1))) / selectorsPerRow var firstInRow: UIPanelAPI? = null var prev: UIPanelAPI? = null val selectorPlugins = mutableListOf() @@ -117,7 +125,7 @@ internal object MagicPaintjobRefitPanel { val selectorPanel = createPaintjobSelector(baseVariant, paintjobSpec, selectorWidth) val selectorPlugin = selectorPanel.plugin as MagicPaintjobSelectorPlugin selectorPlugins.add(selectorPlugin) - if(currentPaintjob == paintjobSpec) { + if (currentPaintjob == paintjobSpec) { selectorPlugin.isSelected = true selectorPlugin.highlightFader.forceIn() } @@ -127,17 +135,15 @@ internal object MagicPaintjobRefitPanel { if (prev == null) { pos.inTL(endPad, endPad) firstInRow = selectorPanel - } - else if(index % selectorsPerRow == 0){ + } else if (index % selectorsPerRow == 0) { pos.belowLeft(firstInRow, midPad) firstInRow = selectorPanel - } - else pos.rightOfTop(prev, midPad) + } else pos.rightOfTop(prev, midPad) prev = selectorPanel } // add tooltip to locked paintjobs - if(!selectorPlugin.isUnlocked && !paintjobSpec?.unlockConditions.isNullOrBlank()){ + if (!selectorPlugin.isUnlocked && !paintjobSpec?.unlockConditions.isNullOrBlank()) { scrollerTooltip.addTooltip(selectorPanel, TooltipMakerAPI.TooltipLocation.BELOW, 250f) { tooltip -> tooltip.addTitle(MagicTxt.getString("ml_mp_refit_locked")) tooltip.addPara(paintjobSpec!!.unlockConditions, 0f) @@ -147,23 +153,32 @@ internal object MagicPaintjobRefitPanel { // sync all the selectors, and apply the paintjob for (selectorPlugin in selectorPlugins) { + selectorPlugin.onHoverEnter { + Global.getSoundPlayer().playUISound("ui_button_mouseover", 1f, 1f) + } selectorPlugin.onClick { if (selectorPlugin.isUnlocked || Global.getSettings().isDevMode) { + Global.getSoundPlayer().playUISound("ui_button_pressed", 1f, 1f) selectorPlugins.forEach { it.isSelected = false } selectorPlugin.isSelected = true - if(selectorPlugin.paintjobSpec == null) MagicPaintjobManager.removePaintjobFromShip(baseVariant) - else MagicPaintjobManager.applyPaintjob(baseVariant, selectorPlugin.paintjobSpec) + if (selectorPlugin.paintjobSpec == null) { + MagicPaintjobManager.removePaintjobFromShip(baseVariant) + } else { + MagicPaintjobManager.applyPaintjob(baseVariant, selectorPlugin.paintjobSpec) + } baseVariant.moduleVariants?.values?.forEach { moduleVariant -> - if(selectorPlugin.paintjobSpec == null) + if (selectorPlugin.paintjobSpec == null) MagicPaintjobManager.removePaintjobFromShip(moduleVariant) - else{ + else { val moduleHullID = (moduleVariant as ShipVariantAPI).hullSpec.baseHullId MagicPaintjobManager.getPaintjobsForHull(moduleHullID).firstOrNull { it.paintjobFamily == selectorPlugin.paintjobSpec.paintjobFamily - }?.let { MagicPaintjobManager.applyPaintjob(moduleVariant, it) } + }?.let { + MagicPaintjobManager.applyPaintjob(moduleVariant, it) + } } } ReflectionUtils.invoke("syncWithCurrentVariant", refitPanel) @@ -174,8 +189,8 @@ internal object MagicPaintjobRefitPanel { } // add scroll at end after setting heightSoFar, needed when using addCustom to the tooltip - val rows = (baseHullPaintjobs.size/selectorsPerRow) + 1 - scrollerTooltip.heightSoFar = endPad*2 + prev!!.height*rows + midPad*(rows-1) + val rows = (baseHullPaintjobs.size / selectorsPerRow) + 1 + scrollerTooltip.heightSoFar = endPad * 2 + prev!!.height * rows + midPad * (rows - 1) paintjobPanel.addUIElement(scrollerTooltip) return paintjobPanel } diff --git a/src/org/magiclib/paintjobs/MagicPaintjobRefitPanelCreator.kt b/src/org/magiclib/paintjobs/MagicPaintjobRefitPanelCreator.kt index 711e32a4..e33d2390 100644 --- a/src/org/magiclib/paintjobs/MagicPaintjobRefitPanelCreator.kt +++ b/src/org/magiclib/paintjobs/MagicPaintjobRefitPanelCreator.kt @@ -1,15 +1,19 @@ package org.magiclib.paintjobs +import com.fs.starfarer.api.Global import com.fs.starfarer.api.fleet.FleetMemberAPI -import com.fs.starfarer.api.ui.* +import com.fs.starfarer.api.ui.Alignment +import com.fs.starfarer.api.ui.ButtonAPI +import com.fs.starfarer.api.ui.CutStyle +import com.fs.starfarer.api.ui.UIPanelAPI import org.lwjgl.input.Keyboard import org.magiclib.ReflectionUtils import org.magiclib.internalextensions.* -import java.awt.Color import org.magiclib.kotlin.setAlpha import org.magiclib.paintjobs.MagicPaintjobRefitPanel.createMagicPaintjobRefitPanel import org.magiclib.util.MagicTxt +import java.awt.Color /** * @author Starficz @@ -20,7 +24,8 @@ internal object MagicPaintjobRefitPanelCreator { fun addPaintjobButton(refitTab: UIPanelAPI, inCampaign: Boolean) { val refitPanel = refitTab.findChildWithMethod("syncWithCurrentVariant") as? UIPanelAPI ?: return val statsAndHullmodsPanel = refitPanel.findChildWithMethod("getColorFor") as? UIPanelAPI ?: return - val hullmodsPanel = statsAndHullmodsPanel.findChildWithMethod("removeNotApplicableMods") as? UIPanelAPI ?: return + val hullmodsPanel = + statsAndHullmodsPanel.findChildWithMethod("removeNotApplicableMods") as? UIPanelAPI ?: return val fleetMember = ReflectionUtils.invoke("getMember", refitPanel) as? FleetMemberAPI val existingElements = hullmodsPanel.getChildrenCopy() @@ -41,7 +46,10 @@ internal object MagicPaintjobRefitPanelCreator { // addHullmods button should always exist in hullmodsPanel val addButton = existingElements.filter { ReflectionUtils.hasMethodOfName("getText", it) }.find { - (ReflectionUtils.invoke("getText", it) as String).contains(MagicTxt.getString("ml_mp_refit_vanillaHullmodAddButtonText")) + (ReflectionUtils.invoke( + "getText", + it + ) as String).contains(MagicTxt.getString("ml_mp_refit_vanillaHullmodAddButtonText")) } ?: return // make a new button @@ -61,8 +69,8 @@ internal object MagicPaintjobRefitPanelCreator { newPaintjobButton.setShortcut(Keyboard.KEY_S, true) newPaintjobButton.onClick { // width/height calcs here are to match vanilla's hullmod panel sizes when screen size grow/shrink - val width = if(inCampaign) (refitTab.width - 343).coerceIn(667f, 700f) else 667f - val height = if(inCampaign) (refitTab.height - 12).coerceIn(722f, 800f) else 722f + val width = if (inCampaign) (refitTab.width - 343).coerceIn(667f, 700f) else 667f + val height = if (inCampaign) (refitTab.height - 12).coerceIn(722f, 800f) else 722f val paintjobPanel = createMagicPaintjobRefitPanel(refitTab, refitPanel, width, height) val coreUI = ReflectionUtils.invoke("getCoreUI", refitPanel) as UIPanelAPI @@ -70,7 +78,7 @@ internal object MagicPaintjobRefitPanelCreator { // the numbers might look like magic, but they are actually offsets from where the vanilla refit panel ends up. // the other calcs here do ensure correct relative placement - val xOffset = if(inCampaign) (refitTab.width - 1037).coerceIn(6f, 213f) else 6f + val xOffset = if (inCampaign) (refitTab.width - 1037).coerceIn(6f, 213f) else 6f paintjobPanel.xAlignOffset = refitTab.left - paintjobPanel.left + xOffset paintjobPanel.yAlignOffset = refitTab.top - paintjobPanel.top - 6 diff --git a/src/org/magiclib/paintjobs/MagicPaintjobShinyAdder.kt b/src/org/magiclib/paintjobs/MagicPaintjobShinyAdder.kt index 41d46303..e1ca3578 100644 --- a/src/org/magiclib/paintjobs/MagicPaintjobShinyAdder.kt +++ b/src/org/magiclib/paintjobs/MagicPaintjobShinyAdder.kt @@ -1,46 +1,109 @@ package org.magiclib.paintjobs -import com.fs.starfarer.api.campaign.BaseCampaignEventListener +import com.fs.starfarer.api.EveryFrameScript +import com.fs.starfarer.api.Global import com.fs.starfarer.api.campaign.CampaignFleetAPI +import com.fs.starfarer.api.combat.ShipVariantAPI +import com.fs.starfarer.api.fleet.FleetMemberAPI import com.fs.starfarer.api.impl.campaign.ids.Tags +import com.fs.starfarer.api.loading.VariantSource +import com.fs.starfarer.api.util.IntervalUtil +import com.fs.starfarer.api.util.Misc import org.magiclib.kotlin.getSalvageSeed import kotlin.random.Random -class MagicPaintjobShinyAdder : BaseCampaignEventListener(false) { + +class MagicPaintjobShinyAdder : EveryFrameScript { + companion object { + @JvmStatic + var probability = 25 // 1 in 25 chance of spawning a shiny + } + + private var isDoneInternal = false + override fun isDone() = isDoneInternal + override fun runWhilePaused() = false + + private val interval = IntervalUtil(2f, 3f) private val fleetsCheckedIds = mutableSetOf() - override fun reportFleetSpawned(fleet: CampaignFleetAPI?) { - if (fleet == null || !MagicPaintjobManager.isEnabled || fleet.isPlayerFleet || fleetsCheckedIds.contains(fleet.id)) { - return - } + override fun advance(amount: Float) { + interval.advance(amount) + if (!interval.intervalElapsed()) return val allShinyPaintjobs = MagicPaintjobManager.getPaintjobs(includeShiny = true).filter { it.isShiny } + + // If no shiny paintjobs exist, no point in this script. if (allShinyPaintjobs.isEmpty()) { + isDoneInternal = true return } - val shipsInFleetWithAvailableShiny = - fleet.fleetData.membersListCopy.filter { it.hullId in allShinyPaintjobs.flatMap { pj -> pj.hullIds } } - val probability = 50 // 1 in X chance of getting a shiny - var addedShiny = false - - for (ship in shipsInFleetWithAvailableShiny) { - if (addedShiny) continue // Max of one per fleet - if (Random(fleet.getSalvageSeed()).nextInt(probability) == 1) { - MagicPaintjobManager.applyPaintjob( - ship, - allShinyPaintjobs.filter { ship.hullId in it.hullIds }.random() - ) - - if (!ship.variant.hasTag(Tags.UNRECOVERABLE)) { - ship.variant.addTag(Tags.VARIANT_ALWAYS_RECOVERABLE) - } + checkAndApplyShiniesToAllFleetsInPlayerLocation(allShinyPaintjobs) + } - addedShiny = true + fun checkAndApplyShiniesToAllFleetsInPlayerLocation( + allShinyPaintjobs: List = MagicPaintjobManager.getPaintjobs( + includeShiny = true + ).filter { it.isShiny } + ) { + for (fleet in Global.getSector().playerFleet.containingLocation?.fleets ?: emptyList()) { + if (fleet == null || fleetsCheckedIds.contains(fleet.id) || !MagicPaintjobManager.isEnabled || fleet.isPlayerFleet + ) { + continue } + + val shipsInFleetWithAvailableShiny = + fleet.fleetData.membersListCopy.filter { it.hullId in allShinyPaintjobs.flatMap { pj -> pj.hullIds } } + var addedShiny = false + + for (ship in shipsInFleetWithAvailableShiny) { + if (addedShiny) continue // Max of one per fleet + + // Roll the dice once per fleet member that has an available shiny paintjob. + if (Random(fleet.getSalvageSeed()).nextInt(probability) == 1) { + // If PJ already applied to ship, don't reapply. + if (!ship.variant.hasHullMod(MagicPaintjobHullMod.ID)) { + applyShinyPaintjob(ship, fleet, allShinyPaintjobs) + } + + addedShiny = true + } + } + + // TODO: Removed this optimization that prevents checking a fleet more than once + // because the game keeps erasing the variant tags and therefore paintjobs + // so we need to keep re-applying them! + // fleetsCheckedIds.add(fleet.id) } + } + + private fun applyShinyPaintjob( + ship: FleetMemberAPI, + fleet: CampaignFleetAPI, + allShinyPaintjobs: List + ) { + setClonedVariant(ship) + MagicPaintjobManager.applyPaintjob( + ship, + allShinyPaintjobs.filter { ship.hullId in it.hullIds }.random(Random(fleet.getSalvageSeed())) + ) + + if (!ship.variant.hasTag(Tags.UNRECOVERABLE)) { + ship.variant.addTag(Tags.VARIANT_ALWAYS_RECOVERABLE) + } + } - fleetsCheckedIds.add(fleet.id) + /** + * Clones the variant and sets it as a 'custom' variant, rather than as the base, so that + * the game doesn't replace/mess with it. + */ + private fun setClonedVariant(member: FleetMemberAPI, setNullOrigVariant: Boolean = true): ShipVariantAPI { + val variantClone = member.variant.clone() + variantClone.hullVariantId = member.hullId + "_" + Misc.genUID() + variantClone.source = VariantSource.REFIT + if (setNullOrigVariant) variantClone.originalVariant = null + member.setVariant(variantClone, false, false) + return variantClone } } \ No newline at end of file diff --git a/src/org/magiclib/paintjobs/console/ListPaintjobsCommand.kt b/src/org/magiclib/paintjobs/console/ListPaintjobsCommand.kt new file mode 100644 index 00000000..89231de7 --- /dev/null +++ b/src/org/magiclib/paintjobs/console/ListPaintjobsCommand.kt @@ -0,0 +1,24 @@ +package org.magiclib.paintjobs.console + +import org.lazywizard.console.BaseCommand +import org.lazywizard.console.Console +import org.magiclib.paintjobs.MagicPaintjobManager + +class ListPaintjobsCommand : BaseCommand { + override fun runCommand(args: String, context: BaseCommand.CommandContext): BaseCommand.CommandResult { + val normalPjs = MagicPaintjobManager.getPaintjobs() + val shinyPjs = MagicPaintjobManager.getPaintjobs(includeShiny = true) - normalPjs + + Console.showMessage("Regular Paintjobs") + for (pj in normalPjs) { + Console.showMessage("\t$pj") + } + + Console.showMessage("Shiny Paintjobs") + for (pj in shinyPjs) { + Console.showMessage("\t$pj") + } + + return BaseCommand.CommandResult.SUCCESS + } +} \ No newline at end of file diff --git a/src/org/magiclib/paintjobs/rulecmd/DoesFleetHaveShiny.kt b/src/org/magiclib/paintjobs/rulecmd/DoesFleetHaveShiny.kt new file mode 100644 index 00000000..ec645179 --- /dev/null +++ b/src/org/magiclib/paintjobs/rulecmd/DoesFleetHaveShiny.kt @@ -0,0 +1,24 @@ +package org.magiclib.paintjobs.rulecmd + +import com.fs.starfarer.api.campaign.CampaignFleetAPI +import com.fs.starfarer.api.campaign.InteractionDialogAPI +import com.fs.starfarer.api.campaign.rules.MemoryAPI +import com.fs.starfarer.api.impl.campaign.rulecmd.BaseCommandPlugin +import com.fs.starfarer.api.util.Misc +import org.magiclib.paintjobs.MagicPaintjobManager + + +class DoesFleetHaveShiny : BaseCommandPlugin() { + override fun execute( + ruleId: String?, + dialog: InteractionDialogAPI?, + params: MutableList?, + memoryMap: MutableMap? + ): Boolean { + val fleet = dialog?.interactionTarget as? CampaignFleetAPI + + return fleet?.fleetData?.membersListCopy + ?.map { MagicPaintjobManager.getCurrentShipPaintjob(it) } + ?.any { it?.isShiny == true } == true + } +} diff --git a/src/org/magiclib/subsystems/MagicSubsystem.java b/src/org/magiclib/subsystems/MagicSubsystem.java index f9e62df0..d5dd718c 100644 --- a/src/org/magiclib/subsystems/MagicSubsystem.java +++ b/src/org/magiclib/subsystems/MagicSubsystem.java @@ -1012,6 +1012,7 @@ public void drawHUDBar(ViewportAPI viewport, Vector2f rootLoc, Vector2f barLoc, ship, getBarFill(), nameText, + getHUDColor(), getExtraInfoText(), getExtraInfoColor(), stateText, diff --git a/src/org/magiclib/util/MagicCampaign.java b/src/org/magiclib/util/MagicCampaign.java index ef34e3a7..391639b0 100644 --- a/src/org/magiclib/util/MagicCampaign.java +++ b/src/org/magiclib/util/MagicCampaign.java @@ -157,7 +157,7 @@ public static ShipVariantAPI loadVariant(String path) { // todo: check if order matters if (sMods != null) { for (int k = 0; k < sMods.length(); k++) { - String sModId = hullMods.getString(k); + String sModId = sMods.getString(k); variant.addPermaMod(sModId, true); // variant.addPermaMod(sModId); variant.addMod(sModId); @@ -165,7 +165,7 @@ public static ShipVariantAPI loadVariant(String path) { } if (permaMods != null) { for (int j = 0; j < permaMods.length(); j++) { - String permaModId = hullMods.getString(j); + String permaModId = permaMods.getString(j); variant.addPermaMod(permaModId); if (!variant.getHullMods().contains(permaModId)) { variant.addMod(permaModId); diff --git a/src/org/magiclib/util/MagicUI.java b/src/org/magiclib/util/MagicUI.java index 5180aa5e..ca37ea54 100644 --- a/src/org/magiclib/util/MagicUI.java +++ b/src/org/magiclib/util/MagicUI.java @@ -58,9 +58,11 @@ public class MagicUI { try { LazyFont fontdraw = LazyFont.loadFont("graphics/fonts/victor14.fnt"); TODRAW14 = fontdraw.createText(); + TODRAW14.setBlendSrc(GL11.GL_ONE); fontdraw = LazyFont.loadFont("graphics/fonts/victor10.fnt"); TODRAW10 = fontdraw.createText(); + TODRAW10.setBlendSrc(GL11.GL_ONE); if (UI_SCALING != 1) { TODRAW14.setFontSize(TODRAW14.getFontSize() * UI_SCALING);