diff --git a/Directory.Build.props b/Directory.Build.props
index fef8962..2500099 100755
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,7 +3,8 @@
$(APPDATA)\..\LocalLow\Glitch Pitch\Idol Manager\Mods
$(APPDATA)\..\LocalLow\Glitch Pitch\Idol Manager\Mods
- $(SolutionDir)\..\dll
+ $(SolutionDir)\..\dll
+ $(MSBuildThisFileDirectory)..\dll
Idol Manager Mod
diff --git a/NuGet.Config b/NuGet.Config
index 80334c0..bc14187 100755
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -2,6 +2,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..1a5f466
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,259 @@
+# Changelog
+
+This changelog covers changes made after the `main` branch point at commit `1c6d2aa` (2026-01-19). Entries are ordered chronologically by commit date. Multiple commits to the same mod on the same date are consolidated into the final behavior for that date rather than repeating intermediate fixes.
+
+## 2026-01-19
+
+### Unofficial Patch
+
+- **`mods/Unofficial Patch/Unofficial Patch.cs`**
+ - Hardened the `SEvent_Concerts._concert._projectedValues.GetRevenue` transpiler so the `GetHype` call is found whether the compiler emitted `call` or `callvirt`, then replaced with the static revenue infix without depending on one exact opcode form.
+ - Added a `singles._single.GetSenbatsuParamValue` patch that invokes the private `SenbatsuCalcParam` through Harmony `AccessTools` using the requested `data_girls._paramType`; senbatsu lookups no longer fall back to effectively evaluating every requested stat as Cute.
+ - Added handling for negated VN girl-variable requirements in `vn_requirements.CheckGirl`, so formulas beginning with `!` correctly invert `IsVariable(...)` instead of being interpreted as a literal variable name.
+ - Added shared IL matching/logging helpers and converted brittle literal/opcode checks to named constants and guarded Harmony lookups. Failed pattern matches now fail safely and can emit once-per-patch diagnostics instead of silently producing malformed IL.
+
+## 2026-01-20
+
+### Unofficial Patch
+
+- **`mods/Unofficial Patch/Unofficial Patch.cs`**
+ - Reworked `Profile_Fans_Pies.Render_Pies` correction to reconstruct the Teen/Young Adult/Adult age ratios before applying cumulative fills. The resulting pie cannot overflow 100% because rounding overshoot is normalized back into the `0..1` range.
+ - Changed the relationship breakup patch to capture the pre-breakup `Dating` state in a Harmony Prefix. Partner-status knowledge is now cleared only when the relationship was actually dating before vanilla `BreakUp` changed its state.
+ - Updated projected concert revenue handling so club venues keep their vanilla hype treatment, while non-club venues use the intended post-100% hype curve.
+ - Added the `FUJI_3_TICKETS` 5% ticket bonus to projected concert revenue calculations.
+ - Corrected the projected hype/display ceiling from 100% to the vanilla 200% maximum.
+ - Updated `CodeMatcher` edits to use `Set(...)` for opcode/operand replacement in the concert and partner-string patches.
+
+### Shared build configuration
+
+- **`Directory.Build.props`**
+ - Added a fallback `dllDir` based on `MSBuildThisFileDirectory` when `SolutionDir` is unavailable, allowing project-level builds to resolve the shared DLL directory correctly.
+- **`NuGet.Config`**
+ - Changed the nuget.org source from HTTP to HTTPS.
+
+## 2026-02-23
+
+### FastForward
+
+- **`mods/FastForward/FastForward.cs`**
+ - Replaced direct `double.Parse` use with invariant-culture validation for the configured multiplier, including NaN/infinity rejection and a safe `1x..50x` range.
+ - Centralized super-fast application and button-color updates so both the fast button and the `4` hotkey use the same validated speed path and tolerate missing camera/UI components.
+ - Added audition-popup protection: custom speeds above vanilla fast are temporarily reduced to vanilla fast while the audition popup initializes, then restored from `mainScript.Time_Resume` only if the player has not changed time state in the meantime.
+ - Added cleanup for pending speed restoration when the player leaves fast mode, preventing stale audition state from restoring an old speed later.
+- **`mods/FastForward/FastForward.csproj`**
+ - Version increased from **1.2.0** to **1.2.1**.
+
+### Harmony Checker
+
+- **`mods/Harmony Checker/Harmony Checker.cs`**
+ - Added null/hierarchy guards around the main-menu Mods button lookup. Missing or renamed UI elements no longer throw during menu initialization and interfere with other mod hooks.
+
+### Targeted Auditions
+
+- **`mods/Targeted Auditions/Targeted Auditions.cs`**
+ - Hardened audition-card scrolling setup with null checks, an explicit `ScrollRect.viewport`, reuse of an existing `ContentSizeFitter`, and safe hierarchy reparenting.
+ - Added an audition portrait-load watchdog. `Popup_Audition.Set`, `Reset`, and `Close` maintain load state, and `PortraitsLoaded` can fail safe after six seconds instead of leaving the recruitment popup permanently blocked by one unresolved portrait.
+ - Added fallback portrait handling and visibility restoration when the watchdog has to release a stuck audition popup.
+- **`mods/Targeted Auditions/Targeted Auditions.csproj`**
+ - Version increased from **2.0.1** to **2.0.2**.
+
+### Unofficial Patch
+
+- **`mods/Unofficial Patch/Unofficial Patch.cs`**
+ - Reworked `Theaters.CompleteDay` correction so it no longer overwrites `Doing_Now` before vanilla settles the previous day. The patch now reads the stat row vanilla actually recorded, zeros day-off revenue, and distributes valid ticket/subscription revenue to participating idols from that recorded result.
+ - Added null/empty guards around theaters, stat rows, groups, girl lists, and breakup relationship members.
+ - Replaced the underage `data_girls.girls.GetPartnerString` IL branch rewrite with a Postfix that rebuilds the normal partner/sexuality/interested text for underage idols while leaving vanilla behavior untouched for AOC idols. This removes a fragile dependency on the compiler's branch layout.
+
+## 2026-03-05
+
+### ModMenus
+
+- **`mods/ModMenus/ModMenus.cs`**
+ - Reworked installation of the **Mod Settings** button so it is attempted from startup and whenever the Settings tab opens, instead of assuming the settings hierarchy already exists at one exact moment.
+ - Added `ModMenusBootstrap`, which retries installation while the UI is still being constructed and stops after a bounded number of attempts.
+ - Added hierarchy fallbacks for finding the settings container and a suitable button template, plus detection/repair of an already-created Mod Settings button to avoid duplicates.
+ - Rebinds localized button text and the click listener when repairing an existing button, and forces layout rebuilding after insertion.
+
+### Unofficial Patch
+
+- **`mods/Unofficial Patch/Unofficial Patch.cs`**
+ - Added null guards around both `SaveManager.LoadData(bool)` and `SaveManager.LoadData(string)` overloads.
+ - The previous in-memory `SaveManager.Data` is captured before loading. If a missing/corrupt load path leaves `Data` null, the previous data (or a fresh `SavedData` fallback) is restored so subsequent `SaveEvent`/autosave calls cannot crash on a null save object.
+
+## 2026-08-08
+
+### Targeted Auditions
+
+- **`mods/Targeted Auditions/Popup UI.cs`**
+ - Removed the old `Staff_Nickname`-based per-audition age-entry popup and its Harmony patches.
+- **`mods/Targeted Auditions/Targeted Auditions.cs`**
+ - Centralized age-range loading in `LoadConfiguredAgeRange()`. Reversed minimum/maximum settings are swapped and written back in corrected order.
+ - Centralized birthday creation in `ApplyRandomBirthdayInConfiguredRange(...)` instead of duplicating the generation logic at the patch site.
+ - Tightened the `Popup_Audition.Set` patch to the `(Auditions.data, bool)` overload, starts the portrait watchdog before the original method runs, and clears/logs watchdog state from a Harmony Finalizer when popup setup throws.
+ - Added exception logging/finalizer cleanup around `Auditions.GenerateGirls` so audition-generation failures do not leave supporting state hanging.
+- **`mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json`**
+ - Removed obsolete hidden `AuditionAgeLimit_TogglePopup` controls because age limits are now taken directly from Mod Menu settings.
+- **`mods/Targeted Auditions/Targeted Auditions.csproj`**
+ - Removed the deleted popup source from the project and increased the version from **2.0.2** to **2.0.3**.
+
+## 2026-08-15
+
+### Extended SSK
+
+- **`mods/Extended SSK/Extended SSK.cs`**
+ - Replaced the cached `GetFameBaseVal` delegate, which could remain permanently bound to the first election instance, with a cached `MethodInfo` invoked against the current `SEvent_SSK._SSK` instance. Fame bonuses beyond rank 10 now use the correct election's base fame value.
+
+### Fan Attrition
+
+- **`mods/Fan Attrition/Fan Attrition.cs`**
+ - Corrected MC-fame scaling from integer division (`/ 10`) to floating-point division (`/ 10f`), restoring the intended quadratic bonus for lower fame levels instead of truncating the coefficient.
+ - Excluded Internet shows from the mod's fatigue-based audience multiplier, matching the mod description that only TV and Radio are affected by the additional fatigue penalty.
+- **`mods/Fan Attrition/FanTooltip.cs`**
+ - Added `cafeFans` to the weekly `TOTAL` calculation so the displayed total agrees with the individual café-fan line and the game's actual fan gain.
+
+### Going Viral
+
+- **`mods/Going Viral/TrendingManager.cs`**
+ - Added explicit trend-duration bounds (`-90..90`) and uses the clamped stored duration in notifications. Negative trends are discarded when Fan Attrition is not installed.
+ - Corrected crisis math so negative trending uses the same positive-magnitude fan/churn multiplier as positive trending rather than producing a negative coefficient that could invert fan loss into fan gain.
+ - Hardened weekly fan-source collection with null guards and overflow-safe addition for contracts, shows, and the latest seven café stat rows.
+ - Corrected probability arithmetic to use floating-point division where appropriate.
+ - Rebalanced TV trending to a true **0% to 15%** chance: cast fame, genre level, and optional MC fame are normalized from vanilla's `0..10` scales into `0..1`, averaged into one quality score, then multiplied by the `0..1` same-genre-TV freshness coefficient. The previous `show.fame[0]` contribution was removed so MC/cast fame are not counted twice.
+ - Fan-acquisition weighting now uses non-negative appeal with the documented 3x casual weighting and does not mix opinion into acquisition weights.
+ - Fan-churn weighting now uses non-negative appeal, clamped opinion, stable 3x casual weighting, and the crisis multiplier on total churn rather than allowing the trend sign to distort demographic weights.
+ - Added overflow-safe integer/long scaling and addition helpers used throughout the trending calculations.
+- **`mods/Going Viral/ApplyTrending.cs`**
+ - Reworked single trending so the multiplier applies to the **total** new fans and the resulting bonus is distributed across casual fan buckets without dividing by zero when the original casual gain is zero.
+ - Replaced the show-sales compiler-local transpiler with scoped Harmony patches around `Shows._show.SetSales`, `data_girls.AddFans_Equally`, and `Shows._show.SetNewFans`. Vanilla distribution/rounding remains in place while the viral bonus is recorded and applied to casual buckets.
+ - Added context cleanup through Harmony Finalizers so nested calls or exceptions cannot leak show-trending state into later calculations.
+ - Hardened business and tour trending fan multipliers with null/range-safe scaling.
+- **`mods/Going Viral/FanMechanics.cs`**
+ - Replaced fan-distribution local-variable transpilers with scoped contexts around the game's `AddFans` methods and `GetTotalAppeal`/`GetFamePoints` calls. Only the weighting functions are substituted; vanilla allocation and rounding remain intact.
+ - Added stack-based context/finalizer cleanup so nested Harmony calls and exceptions do not leave stale acquisition/churn state active.
+ - Hardened theater/fan-opinion processing with null checks and bounded arithmetic.
+- **`mods/Going Viral/Resource.cs`**
+ - Reworked trend save persistence so tagged trend data is consumed as a private save marker instead of being allowed to overwrite vanilla Buzz.
+ - Added a high reserved marker/offset that cannot collide with normal Buzz values, migration of legacy `10000 + trend` saves (including negative trends), and reset of static trend state when loading a save with no marker.
+ - Added load-depth/finalizer handling so failed or nested loads cannot leave the marker parser in a stale loading state.
+- **`mods/Going Viral/FanTooltip.cs`**
+ - Replaced hard-coded tooltip child indices with named, created-on-demand lines for demographic ratios, fan sources, churn, and trending status. UI hierarchy changes no longer redirect text into the wrong line or cause index errors.
+ - Weekly totals include café fans and use overflow-safe arithmetic.
+ - Added guards for missing idols/fan-appeal data and zero total fans when rendering demographic ratios.
+- **`mods/Going Viral/BonusTooltip.cs`**
+ - TV genre buttons now calculate the most recent TV show separately for the genre represented by each button instead of reusing the currently selected genre for every tooltip.
+ - Tooltips are rebuilt from the parameter tooltip each render so the Going Viral suffix cannot accumulate repeatedly.
+ - Single marketing trend/crisis percentages use floating-point probability math and are clamped to valid display ranges.
+- **`mods/Going Viral/TriggerTrending.cs`**
+ - Hardened single-marketing, TV-episode, scandal-point, and resource-trigger paths with explicit pre/post trend-state tracking so a trigger is reported only when it actually creates a new trend/crisis.
+ - Prevented duplicate crisis handling when Fake Scandal's resource change has already started the crisis.
+- **`mods/Going Viral/assets/JSON/Constants/constants.json`**
+ - Converted the constants asset to strict quoted JSON and removed the duplicate `TIP__RADIO` entry while retaining the intended localization text, including the TV description's “Up to 15%” wording.
+- **`mods/Going Viral/Going Viral.csproj`**
+ - Version increased from **1.0.0** to **1.0.1**.
+
+### Growing Distant
+
+- **`mods/Growing Distant/Growing Distant.cs`**
+ - Capped salary-based positive influence at the vanilla 512 relationship-point ceiling. High salary satisfaction can no longer accumulate hidden influence above the game's normal maximum and delay later relationship decay.
+
+### MBTI Personalities
+
+- **`mods/MBTI Personalities/MBTI Personalities.cs`**
+ - Corrected the ISTP concert-accident calculation to operate in the game's `0..100` percentage-point scale. The trait now reduces the **remaining failure chance** rather than mixing fractional and percentage units.
+
+### ModMenus
+
+- **`mods/ModMenus/ModMenus.cs`**
+ - Corrected the fallback slider default from `max + min / 2` to the true arithmetic midpoint, `(max + min) / 2f`.
+
+### Never Graduate
+
+- **`mods/Never Graduate/Never Graduate.cs`**
+ - Kept the existing hard stop on `data_girls.UpdateGraduationDates` and added direct blocks for `data_girls.girls.Graduation_Date_Update`, `Graduation_Announce`, and `Graduation_Announce_Confirm` so alternate callers and other mods cannot bypass the no-graduation rule.
+ - If a save already contains an idol in `announced_graduation`, the direct date-update guard restores the idol's previous status before suppressing the graduation check.
+ - The stronger blocking makes the mod authoritative over date-driven graduation while still leaving direct firing available.
+- **`mods/Never Graduate/Never Graduate.csproj`**
+ - Updated the description to reflect the stronger graduation blocking and increased the version from **1.0.0** to **1.2.0**.
+
+### Stale Theater Shows
+
+- **`mods/Stale Theater Shows/Stale Theater Shows.cs`**
+ - Fixed the `Theaters._theater.GetNumberOfVisitors` transpiler's branch target handling. Labels from the original normal-path instruction are transferred to the injected `Ldarg_0`, ensuring branches pass through the custom attendance multiplier instead of skipping it.
+
+### Targeted Auditions
+
+- **`mods/Targeted Auditions/Targeted Auditions.cs`**
+ - Scoped audition-specific sexuality, stat-priority, birthday, and body-ID behavior to `Auditions.GenerateGirls` using an exception-safe generation-depth context. Rival, story, unique-idol, and other non-audition girl generation are no longer modified by Targeted Auditions settings.
+ - Reworked birthday generation to choose an age in the configured inclusive range and then choose a valid birthday for that exact age. Candidates can no longer land one year older than the configured maximum because of month/day offsets.
+ - Body IDs now remain unique until the currently eligible audition body pool is exhausted. Only then is `Auditions.UsedBodyIDs` cleared so larger candidate counts can recycle bodies as needed.
+ - Replaced direct access to the game's private `data_girls_textures.textureAssets` field with `HarmonyLib.AccessTools.Field(...)`, keeping private game-state access within Harmony/reflection.
+- **`mods/Targeted Auditions/assets/steam description.txt`**
+ - Clarified that age limits are inclusive and only affect generated audition candidates, and documented the body-ID reuse behavior for large auditions.
+
+### Traits Expansion
+
+- **`mods/Traits Expansion/Traits Expansion.cs`**
+ - Corrected **Sadistic** detection to count members who are active bullies (`IsBully`) rather than members who are being bullied (`IsBullied`).
+ - Changed **Wooden Acting** drama rewards from a flat subtraction to the intended **50% multiplicative penalty**.
+ - Changed **Quick Wit** variety rewards from a flat addition to the intended **50% multiplicative bonus**.
+
+### Traits Fix
+
+- **`mods/Traits Fix/Traits Fix.cs`**
+ - Replaced the compiler-local-dependent **Live Fast** birthday transpiler with scoped `Birthday_Popup.DoParam`, `data_girls.girls.param.setVal`, and `Birthday_Stat.Set` patches. Actual stat loss and displayed birthday loss now stay synchronized, double only decreases, and remain within valid stat bounds.
+ - Added null-safe handling to random post-peak Live Fast deterioration.
+ - Reworked **Indiscreet** relationship leaks so outside relationships and idol-idol relationships are handled by the appropriate game paths, already-known relationships are not leaked every week, both sides of an idol relationship are synchronized when revealed, and dating-forbidden scandal penalties are applied consistently.
+ - Hardened **Maternal**, **Precocious**, **Arrogant**, and **Forgiving** relationship adjustments with safe relationship-member extraction and corrected dynamic relationship math; Maternal/Precocious positive growth now accounts for the game's positive-value halving behavior.
+ - Hardened **Meme Queen** Internet-show stat bonuses so they modify the actual parameter just added and only force show-popup recalculation when the medium changes.
+ - Corrected **Meme Queen** viral-marketing odds by applying the advertised success and critical-success bonuses without separately subtracting from regular failure chance, which vanilla already derives from the other probability buckets.
+ - Hardened **Annoying** show stamina penalties and **Misandry** handshake appeal penalties against null/inactive/sick casts and avoids unnecessary per-idol checks when the relevant show/single condition is absent.
+ - Fixed **Perfectionist** tour handling by capturing the tour attendance result before vanilla `FinishTour` clears the active tour object; concert handling was likewise made null-safe.
+ - Replaced global trait-calculation booleans with stack-based contexts and Harmony Finalizers around business/show/single/concert stat calculations, preventing nested calls or exceptions from leaving trait modifiers active for unrelated `param.GetVal` calls.
+ - Hardened recent-single/chart lookup used by result-dependent traits so current-month player singles can be resolved from chart data without null/index failures.
+- **`mods/Traits Fix/Traits Fix.csproj`**
+ - Version increased from **1.1.0** to **1.1.1**.
+
+### Worker Rights
+
+- **`mods/Worker Rights/Worker Rights.cs`**
+ - Fixed the low-salary graduation penalty by assigning the immutable `DateTime.AddDays(...)` return value back to `Graduation_Date`.
+ - Applies the full advertised 10x penalty on the game's existing weekly `Graduation_Date_Update` cadence: **-10 days** below 50% salary satisfaction and **-30 days** below 20%.
+ - Added Harmony ordering compatibility with Never Graduate so Never Graduate can remain authoritative when both mods are installed.
+ - Low-fame expected salary is now a floor (`max(vanilla, ¥40,000)`) instead of overwriting a larger vanilla/third-party expectation.
+ - In Hard mode, Fame 10 idols' 10%-of-average-earnings rule is likewise enforced as a floor and ignores NaN/infinite/non-positive earnings.
+ - Added null/policy guards around staff firing, salary rules, generated idols, and graduation-date processing.
+- **`mods/Worker Rights/Worker Rights.csproj`**
+ - Version increased from **1.0.0** to **1.0.1**.
+
+### ModMenus
+- **`mods/ModMenus/ModMenus.cs`**
+ - Fixed ModMenus button injection to detect when the setting tab is selected, reducing lag.
+
+### Policies That Matter
+- **`mods/Policies That Matter/Policies That Matter.cs`**
+ - Added function that removes duplicate policy definitions after the game has loaded all policy JSON files
+
+## Harmony Checker
+- **`mods/Harmony Checker/Harmony Checker.cs`**
+ - Keeps the existing `MainMenu_Buttons_Controller.Start` check.
+ - Adds a post-mod-load refresh via `Mods.StopSpinner`, which runs after `Mods.LoadMods` in the vanilla mod-loading coroutine.
+ - Calls `Lang_Button.ResetText()` after changing the constant so an already-started button visibly updates to `Mods [IM-HI installed`.
+
+### Shared StatLimits library
+
+- **`shared/StatLimits/StatLimits.cs`**
+ - Standardized numeric caps through `Mathf.Clamp` while preserving the existing limits: business proposal coefficients remain `0..20`, and show/senbatsu/concert/team-chemistry stats remain `0..100`.
+ - Added null/empty guards for senbatsu results and cast-parameter lists so the limit patches do not throw when a game/mod call returns no parameter object.
+ - Removed `LINQ Last()` dependencies in cast-parameter patches and accesses the final element only after confirming the list is non-empty.
+
+## 2026-08-16
+
+### ModMenus
+
+- **`mods/ModMenus/ModMenus.cs`**
+ - Changed **Mod Settings** placement to use the vanilla `Settings` GameObject as its explicit ordering anchor, keeping the lookup independent of the button's localized display text.
+ - Changed the vanilla `Settings` button to be the Mod Settings button's clone template as well as its sole ordering anchor.
+ - Existing `ModMenuButton` instances are now reconfigured and repositioned immediately after the vanilla Settings button whenever the Settings tab is activated, repairing their ordering if another UI modification previously moved them.
+ - Removed Save & Quit/Main Menu and `childCount`-relative placement from Mod Settings ordering, leaving later positions available for other mods to insert their own settings buttons independently.
+- **`mods/ModMenus/ModMenus.csproj`**
+ - Version increased from **1.0.1** to **1.0.2**.
\ No newline at end of file
diff --git a/mods/Extended SSK/Extended SSK.cs b/mods/Extended SSK/Extended SSK.cs
index e450901..046ed33 100755
--- a/mods/Extended SSK/Extended SSK.cs
+++ b/mods/Extended SSK/Extended SSK.cs
@@ -69,7 +69,7 @@ public static int Infix(int i)
[HarmonyPatch("RecalcFameBonus")]
public static class SSK_RecalcFameBonusPatch
{
- static Func GetFameBaseValDelegate;
+ static readonly MethodInfo GetFameBaseValInfo = AccessTools.Method(typeof(SEvent_SSK._SSK), "GetFameBaseVal");
///
/// Postfix method to recalculate fame bonuses after the original method execution.
@@ -90,13 +90,8 @@ public static void Postfix(ref SEvent_SSK._SSK __instance)
int limit = int.Parse(variables.Get(varID) ?? defaultRankingsStr);
List list = __instance.FameBonus;
- if(GetFameBaseValDelegate == null)
- {
- MethodInfo GetFameBaseValInfo = AccessTools.Method(typeof(SEvent_SSK._SSK), "GetFameBaseVal");
- GetFameBaseValDelegate = AccessTools.MethodDelegate>(GetFameBaseValInfo, __instance);
- }
-
- int num = Mathf.RoundToInt(GetFameBaseValDelegate() * 0.056f);
+ int fameBaseVal = (int)GetFameBaseValInfo.Invoke(__instance, null);
+ int num = Mathf.RoundToInt(fameBaseVal * 0.056f);
for (int i = 10; i < Math.Min(girlCount, limit); i++)
{
list.Add(num);
diff --git a/mods/Fan Attrition/Fan Attrition.cs b/mods/Fan Attrition/Fan Attrition.cs
index 1c6d082..4cc95bf 100755
--- a/mods/Fan Attrition/Fan Attrition.cs
+++ b/mods/Fan Attrition/Fan Attrition.cs
@@ -62,7 +62,7 @@ public static int Infix(Shows._show __this, int num6)
{
if (__this.mc != null)
{
- float mcCoeff = Mathf.Max(1f, 1f + __this.mc.fame * __this.mc.fame / 10);
+ float mcCoeff = Mathf.Max(1f, 1f + __this.mc.fame * __this.mc.fame / 10f);
if (__this.mc.fame >= 10)
{
mcCoeff += MC_MAX_FAME_BONUS;
@@ -123,6 +123,11 @@ public static IEnumerable Transpiler(IEnumerableThe modified fan count.
public static float Infix(Shows._show __this, float num2)
{
+ if (__this.medium != null && __this.medium.media_type == Shows._param._media_type.internet)
+ {
+ return num2;
+ }
+
if (staticVars.IsHard())
{
num2 *= 1f - __this.GetFatigue() * __this.GetFatigue() / SHOW_FATIGUE_COEFF_HARD;
diff --git a/mods/Fan Attrition/FanTooltip.cs b/mods/Fan Attrition/FanTooltip.cs
index 044fe5b..ae715fa 100755
--- a/mods/Fan Attrition/FanTooltip.cs
+++ b/mods/Fan Attrition/FanTooltip.cs
@@ -303,7 +303,7 @@ public class tooltip_fans_RenderFanChange
public static void Postfix(tooltip_fans __instance)
{
long baseChange = resources.FansChange * 7;
- long totalChange = adFans + dramaFans + netFans + tvFans + radioFans + baseChange;
+ long totalChange = adFans + dramaFans + netFans + tvFans + radioFans + cafeFans + baseChange;
string changeStr = ExtensionMethods.formatNumber(totalChange, false, false) + " " + Language.Data["PER_WEEK"];
if (totalChange > 0)
diff --git a/mods/FastForward/FastForward.cs b/mods/FastForward/FastForward.cs
index 2aff2f6..f88f86b 100755
--- a/mods/FastForward/FastForward.cs
+++ b/mods/FastForward/FastForward.cs
@@ -1,66 +1,238 @@
-using HarmonyLib;
-using UnityEngine;
-using TMPro;
-using static FastForward.FastForward;
-
-namespace FastForward
-{
- public class FastForward
- {
- public const string VARID = "FastForward_Multiplier";
- public const string DEFAULT_VAR = "5";
- }
-
- ///
- /// Patch class for the TimeControlButton's OnClick method to implement faster time acceleration.
- ///
- [HarmonyPatch(typeof(TimeControlButton), "OnClick")]
- public class TimeControlButton_OnClick
- {
- ///
- /// Prefix method to enhance the fast forward functionality when clicking the fast forward button twice.
- ///
- /// The instance of the TimeControlButton being clicked.
- /// Boolean indicating whether the original method should be executed.
- public static bool Prefix(TimeControlButton __instance)
- {
- double mult = double.Parse(variables.Get(VARID) ?? DEFAULT_VAR);
- double speed = 200 * mult;
-
- if (__instance.Type != mainScript._time_state.fast || staticVars.timeState != mainScript._time_state.fast || staticVars.dateTimeAddMinutesPerSecond == speed)
- return true;
-
- staticVars.dateTimeAddMinutesPerSecond = speed;
- Camera.main.GetComponent().TimeControls_Fast.GetComponent().color = mainScript.gold32;
-
- return false;
- }
-
- }
-
- ///
- /// Patch class for the Controls' Update method to implement hotkey-based time acceleration.
- ///
- [HarmonyPatch(typeof(Controls), "Update")]
- public class Controls_Update
- {
- ///
- /// Postfix method to add hotkey functionality for speeding up time when pressing the '4' key.
- ///
- public static void Postfix()
- {
- if (mainScript.IsBlockingHotkeys())
- return;
-
- if (Input.GetKeyDown(KeyCode.Alpha4))
- {
- double mult = double.Parse(variables.Get(VARID) ?? DEFAULT_VAR);
- double speed = 200 * mult;
- Camera.main.GetComponent().Time_SetState(mainScript._time_state.fast);
- staticVars.dateTimeAddMinutesPerSecond = speed;
-
- Camera.main.GetComponent().TimeControls_Fast.GetComponent().color = mainScript.gold32;
- }
- }
- }
-}
+using HarmonyLib;
+using UnityEngine;
+using TMPro;
+using System;
+using System.Globalization;
+using static FastForward.FastForward;
+
+namespace FastForward
+{
+ public class FastForward
+ {
+ public const string VARID = "FastForward_Multiplier";
+ public const string DEFAULT_VAR = "5";
+ public const double BASE_FAST_SPEED = 200d;
+ public const double MAX_MULTIPLIER = 50d;
+ public const double EPSILON = 0.001d;
+
+ // Stores the user's pre-popup super-fast speed so it can be restored after forced popup pause.
+ private static double pendingAuditionRestoreSpeed = 0d;
+
+ internal static double GetConfiguredMultiplier()
+ {
+ string raw = variables.Get(VARID);
+ if (string.IsNullOrWhiteSpace(raw))
+ {
+ raw = DEFAULT_VAR;
+ }
+
+ if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out double multiplier))
+ {
+ multiplier = 5d;
+ }
+
+ if (double.IsNaN(multiplier) || double.IsInfinity(multiplier))
+ {
+ multiplier = 5d;
+ }
+
+ if (multiplier < 1d)
+ {
+ multiplier = 1d;
+ }
+ else if (multiplier > MAX_MULTIPLIER)
+ {
+ multiplier = MAX_MULTIPLIER;
+ }
+
+ return multiplier;
+ }
+
+ internal static double GetConfiguredSpeed()
+ {
+ return BASE_FAST_SPEED * GetConfiguredMultiplier();
+ }
+
+ internal static void ApplySuperFast(mainScript main)
+ {
+ if (main == null)
+ {
+ return;
+ }
+
+ // Use vanilla state transition first so all game-side effects still run as expected.
+ main.Time_SetState(mainScript._time_state.fast);
+ staticVars.dateTimeAddMinutesPerSecond = GetConfiguredSpeed();
+ SetFastButtonColor(mainScript.gold32);
+ }
+
+ internal static void SetFastButtonColor(Color32 color)
+ {
+ if (Camera.main == null)
+ {
+ return;
+ }
+
+ mainScript main = Camera.main.GetComponent();
+ if (main == null || main.TimeControls_Fast == null)
+ {
+ return;
+ }
+
+ TextMeshProUGUI fastLabel = main.TimeControls_Fast.GetComponent();
+ if (fastLabel != null)
+ {
+ fastLabel.color = color;
+ }
+ }
+
+ internal static void SuspendSuperFastForAuditionPopup()
+ {
+ // Only suspend custom speeds; vanilla fast (200) does not need this safety fallback.
+ if (staticVars.timeState != mainScript._time_state.fast || staticVars.dateTimeAddMinutesPerSecond <= BASE_FAST_SPEED + EPSILON)
+ {
+ return;
+ }
+
+ if (pendingAuditionRestoreSpeed <= BASE_FAST_SPEED + EPSILON)
+ {
+ pendingAuditionRestoreSpeed = staticVars.dateTimeAddMinutesPerSecond;
+ }
+
+ // Drop to vanilla fast while popup initialization runs to avoid race conditions in recruitment UI.
+ staticVars.dateTimeAddMinutesPerSecond = BASE_FAST_SPEED;
+ SetFastButtonColor(mainScript.green32);
+ }
+
+ internal static void TryRestoreSuperFastAfterResume(mainScript main)
+ {
+ if (pendingAuditionRestoreSpeed <= BASE_FAST_SPEED + EPSILON)
+ {
+ return;
+ }
+
+ // Respect user state changes made while popup flow was active.
+ if (staticVars.timeState != mainScript._time_state.fast)
+ {
+ pendingAuditionRestoreSpeed = 0d;
+ return;
+ }
+
+ // Only restore when popup system resumes vanilla fast speed.
+ if (staticVars.dateTimeAddMinutesPerSecond > BASE_FAST_SPEED + EPSILON)
+ {
+ pendingAuditionRestoreSpeed = 0d;
+ return;
+ }
+
+ staticVars.dateTimeAddMinutesPerSecond = pendingAuditionRestoreSpeed;
+ pendingAuditionRestoreSpeed = 0d;
+ SetFastButtonColor(mainScript.gold32);
+ }
+
+ internal static void ClearPendingRestoreIfNotFast(mainScript._time_state state)
+ {
+ if (state != mainScript._time_state.fast)
+ {
+ pendingAuditionRestoreSpeed = 0d;
+ }
+ }
+ }
+
+ ///
+ /// Patch class for the TimeControlButton's OnClick method to implement faster time acceleration.
+ ///
+ [HarmonyPatch(typeof(TimeControlButton), "OnClick")]
+ public class TimeControlButton_OnClick
+ {
+ ///
+ /// Prefix method to enhance the fast forward functionality when clicking the fast forward button twice.
+ ///
+ /// The instance of the TimeControlButton being clicked.
+ /// Boolean indicating whether the original method should be executed.
+ public static bool Prefix(TimeControlButton __instance)
+ {
+ double speed = GetConfiguredSpeed();
+
+ if (__instance.Type != mainScript._time_state.fast || staticVars.timeState != mainScript._time_state.fast || Math.Abs(staticVars.dateTimeAddMinutesPerSecond - speed) <= EPSILON)
+ return true;
+
+ ApplySuperFast(Camera.main != null ? Camera.main.GetComponent() : null);
+ return false;
+ }
+ }
+
+ ///
+ /// Patch class for the Controls' Update method to implement hotkey-based time acceleration.
+ ///
+ [HarmonyPatch(typeof(Controls), "Update")]
+ public class Controls_Update
+ {
+ ///
+ /// Postfix method to add hotkey functionality for speeding up time when pressing the '4' key.
+ ///
+ public static void Postfix()
+ {
+ if (mainScript.IsBlockingHotkeys())
+ return;
+
+ if (Input.GetKeyDown(KeyCode.Alpha4))
+ {
+ ApplySuperFast(Camera.main != null ? Camera.main.GetComponent() : null);
+ }
+ }
+ }
+
+ ///
+ /// Ensures custom super-fast speed does not interfere with audition popup initialization.
+ ///
+ [HarmonyPatch(typeof(PopupManager), "Open")]
+ public class PopupManager_Open
+ {
+ ///
+ /// Prefix method to temporarily downgrade custom super-fast speed when opening the audition popup.
+ ///
+ /// Popup type being opened.
+ public static void Prefix(PopupManager._type type)
+ {
+ if (type != PopupManager._type.audition)
+ {
+ return;
+ }
+
+ SuspendSuperFastForAuditionPopup();
+ }
+ }
+
+ ///
+ /// Restores previously suspended super-fast speed after popup-driven forced pause is released.
+ ///
+ [HarmonyPatch(typeof(mainScript), "Time_Resume")]
+ public class mainScript_Time_Resume
+ {
+ ///
+ /// Postfix method to restore the user's pre-popup custom speed.
+ ///
+ /// mainScript instance.
+ public static void Postfix(mainScript __instance)
+ {
+ TryRestoreSuperFastAfterResume(__instance);
+ }
+ }
+
+ ///
+ /// Clears pending speed restoration when user manually switches away from fast mode.
+ ///
+ [HarmonyPatch(typeof(mainScript), "Time_SetState")]
+ public class mainScript_Time_SetState
+ {
+ ///
+ /// Postfix method to cancel restore state if current mode is no longer fast.
+ ///
+ /// Requested time state.
+ public static void Postfix(mainScript._time_state state)
+ {
+ ClearPendingRestoreIfNotFast(state);
+ }
+ }
+}
diff --git a/mods/FastForward/FastForward.csproj b/mods/FastForward/FastForward.csproj
index 28be217..5448677 100755
--- a/mods/FastForward/FastForward.csproj
+++ b/mods/FastForward/FastForward.csproj
@@ -5,7 +5,7 @@
com.tel.fastforward
Press '4' or click the fast-forward button twice to go faster!
Tel
- 1.2.0
+ 1.2.1
["gameplay"]
diff --git a/mods/Going Viral/ApplyTrending.cs b/mods/Going Viral/ApplyTrending.cs
index 39c39ee..cd88f21 100755
--- a/mods/Going Viral/ApplyTrending.cs
+++ b/mods/Going Viral/ApplyTrending.cs
@@ -1,173 +1,243 @@
-using HarmonyLib;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection.Emit;
-using System.Reflection;
-using static GoingViral.TrendingManager;
-using UnityEngine;
-
-namespace GoingViral
-{
-
-
- // Apply fans increase to singles
- [HarmonyPatch(typeof(singles), "GenerateSales")]
- public class singles_GenerateSales
- {
- public static void Postfix(singles._single single)
- {
- if (IsTrending() == TrendingStatus.trending)
- {
- long totalNewFans = 0;
- long casualNewFans = 0;
- foreach (singles._single._sales sale in single.sales)
- {
- totalNewFans += sale.new_fans;
- if (sale.fan.IsType(resources.fanType.casual))
- {
- casualNewFans += sale.new_fans;
- }
- }
- foreach (singles._single._sales sale in single.sales)
- {
- if (sale.fan.IsType(resources.fanType.casual))
- {
- sale.new_fans = (long)Math.Round(sale.new_fans * totalNewFans / casualNewFans * GetTrendingCoeff());
- }
- }
- }
- }
- }
-
- // Apply fans increase to shows
- [HarmonyPatch(typeof(Shows._show), "SetSales")]
- public class Shows__show_SetSales
- {
- public static IEnumerable Transpiler(IEnumerable instructions)
- {
- List instructionList = new(instructions);
-
- int index = -1;
- object newFansOperand = null;
- object fanOperand = null;
- for (int i = 0; i < instructionList.Count; i++)
- {
- if (instructionList[i].opcode == OpCodes.Ldloc_S && instructionList[i].operand is LocalVariableInfo localVariable && localVariable.LocalIndex == 12)
- {
- index = i;
- newFansOperand = instructionList[i].operand;
- }
- if (instructionList[i].opcode == OpCodes.Ldloc_S && instructionList[i].operand is LocalVariableInfo localVariable2 && localVariable2.LocalIndex == 7)
- {
- fanOperand = instructionList[i].operand;
- }
- if (instructionList[i].opcode == OpCodes.Call && (MethodInfo)instructionList[i].operand == AccessTools.Method(typeof(data_girls), "AddFans_Equally", new Type[] { typeof(long), typeof(resources._fan), typeof(List) }))
- {
- break;
- }
- }
-
- if (index != -1)
- {
- instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_S, fanOperand));
- instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Shows__show_SetSales), "Infix")));
- instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Stloc_S, newFansOperand));
- instructionList.Insert(index + 4, new CodeInstruction(OpCodes.Ldloc_S, newFansOperand));
- }
-
- return instructionList.AsEnumerable();
- }
-
- public static int Infix(int fanCount, resources._fan fan)
- {
- if (IsTrending() == TrendingStatus.trending)
- {
- if (fan.IsType(resources.fanType.casual))
- {
- fanCount = Mathf.RoundToInt(fanCount * GetTrendingCoeff());
- }
- }
- return fanCount;
- }
- }
-
- // Apply fans increase to businesses
- [HarmonyPatch(typeof(business._proposal), "set_newFans")]
- public class business__proposal_set_newFans
- {
- public static void Postfix(ref business._proposal __instance)
- {
- if (IsTrending() == TrendingStatus.trending)
- {
- __instance._newFans = Mathf.RoundToInt(__instance._newFans * GetTrendingCoeff());
- }
- }
- }
-
-
- // Reduce fans_per_week to orig value for contracts
- [HarmonyPatch(typeof(business), "AddActiveProposal")]
- public class business_AddActiveProposal
- {
- public static void Postfix(ref business __instance)
- {
- if (IsTrending() == TrendingStatus.trending)
- {
- __instance.ActiveProposals[__instance.ActiveProposals.Count - 1].Fans_per_week = Mathf.RoundToInt(__instance.ActiveProposals[__instance.ActiveProposals.Count - 1].Fans_per_week / GetTrendingCoeff());
- }
- }
- }
-
- // Apply fans increase to business contracts
- [HarmonyPatch(typeof(business), "DoWeeklyFans")]
- public class business_DoWeeklyFans
- {
- public static void Postfix(ref business __instance)
- {
- if (IsTrending() == TrendingStatus.trending)
- {
- foreach (business.active_proposal active_proposal in __instance.ActiveProposals)
- {
- if (active_proposal.Fans_per_week > 0)
- {
- active_proposal.Girl.AddFans(Mathf.RoundToInt(active_proposal.Fans_per_week * (GetTrendingCoeff() - 1)), null);
- }
- }
- }
- }
- }
-
- // Apply fans increase to business contracts
- [HarmonyPatch(typeof(Contracts_Line), "Set")]
- public class Contracts_Line_Set
- {
- public static void Postfix(ref Contracts_Line __instance, business.active_proposal ___ActiveProposal)
- {
- if (IsTrending() == TrendingStatus.trending)
- {
- ExtensionMethods.SetText(
- __instance.NewFans,
- ExtensionMethods.formatNumber(
- Mathf.RoundToInt(___ActiveProposal.Fans_per_week * GetTrendingCoeff()),
- false,
- false
- )
- );
- }
- }
- }
-
- // Apply fans increase to tour
- [HarmonyPatch(typeof(SEvent_Tour.tour), "GetNewFansByAttendance")]
- public class SEvent_Tour_tour_GetNewFansByAttendance
- {
- public static void Postfix(ref int __result)
- {
- if (IsTrending() == TrendingStatus.trending)
- {
- __result = Mathf.RoundToInt(__result * GetTrendingCoeff());
- }
- }
- }
-}
+using HarmonyLib;
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using static GoingViral.TrendingManager;
+
+namespace GoingViral
+{
+ [HarmonyPatch(typeof(singles), "GenerateSales")]
+ public class singles_GenerateSales
+ {
+ public static void Postfix(singles._single single)
+ {
+ if (IsTrending() != TrendingStatus.trending || single == null || single.sales == null)
+ return;
+
+ long totalNewFans = 0;
+ long casualNewFans = 0;
+ List casualSales = new List();
+ foreach (singles._single._sales sale in single.sales)
+ {
+ if (sale == null) continue;
+ totalNewFans = SafeAdd(totalNewFans, sale.new_fans);
+ if (sale.fan != null && sale.fan.IsType(resources.fanType.casual))
+ {
+ casualSales.Add(sale);
+ casualNewFans = SafeAdd(casualNewFans, sale.new_fans);
+ }
+ }
+
+ if (totalNewFans <= 0 || casualSales.Count == 0)
+ return;
+
+ long targetTotal = ScaleLong(totalNewFans, GetTrendingCoeff());
+ long bonus = Math.Max(0, targetTotal - totalNewFans);
+ if (bonus == 0)
+ return;
+
+ long assigned = 0;
+ for (int i = 0; i < casualSales.Count; i++)
+ {
+ long share;
+ if (i == casualSales.Count - 1)
+ {
+ share = bonus - assigned;
+ }
+ else if (casualNewFans > 0)
+ {
+ share = (long)Math.Round(bonus * Math.Max(0d, (double)casualSales[i].new_fans) / casualNewFans, MidpointRounding.AwayFromZero);
+ share = Math.Min(share, bonus - assigned);
+ }
+ else
+ {
+ share = (bonus - assigned) / (casualSales.Count - i);
+ }
+
+ casualSales[i].new_fans = SafeAdd(casualSales[i].new_fans, share);
+ assigned = SafeAdd(assigned, share);
+ }
+ }
+ }
+
+ // Scope show fan additions so the viral bonus can be added without relying on compiler local numbers.
+ [HarmonyPatch(typeof(Shows._show), "SetSales")]
+ public class Shows__show_SetSales
+ {
+ internal sealed class FanBucket
+ {
+ public resources._fan Fan;
+ public long BaseFans;
+ }
+
+ internal sealed class Context
+ {
+ public Shows._show Show;
+ public long BaseNewFans;
+ public bool AddingTrendingBonus;
+ public readonly List CasualBuckets = new List();
+ }
+
+ private static readonly Stack contexts = new Stack();
+ internal static Context Current { get { return contexts.Count > 0 ? contexts.Peek() : null; } }
+
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix(Shows._show __instance)
+ {
+ contexts.Push(new Context { Show = __instance });
+ }
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ if (contexts.Count > 0) contexts.Pop();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls), "AddFans_Equally", new Type[] { typeof(long), typeof(resources._fan), typeof(List) })]
+ public class data_girls_AddFans_Equally_TrendingRecorder
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Prefix(long total_fans, resources._fan _Fan)
+ {
+ Shows__show_SetSales.Context context = Shows__show_SetSales.Current;
+ if (context == null || context.AddingTrendingBonus || total_fans <= 0)
+ return;
+
+ context.BaseNewFans = SafeAdd(context.BaseNewFans, total_fans);
+ if (_Fan != null && _Fan.IsType(resources.fanType.casual))
+ context.CasualBuckets.Add(new Shows__show_SetSales.FanBucket { Fan = _Fan, BaseFans = total_fans });
+ }
+ }
+
+ [HarmonyPatch(typeof(Shows._show), "SetNewFans")]
+ public class Shows__show_SetNewFans_Trending
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Prefix(ref int val)
+ {
+ Shows__show_SetSales.Context context = Shows__show_SetSales.Current;
+ if (context == null || IsTrending() != TrendingStatus.trending || context.BaseNewFans <= 0)
+ return;
+
+ long target = ScaleLong(context.BaseNewFans, GetTrendingCoeff());
+ long bonus = Math.Max(0, target - context.BaseNewFans);
+ if (bonus == 0 || context.CasualBuckets.Count == 0)
+ {
+ val = context.BaseNewFans > int.MaxValue ? int.MaxValue : (int)context.BaseNewFans;
+ return;
+ }
+
+ List cast = context.Show != null ? context.Show.GetCast() : null;
+ if (cast == null || cast.Count == 0)
+ {
+ val = context.BaseNewFans > int.MaxValue ? int.MaxValue : (int)context.BaseNewFans;
+ return;
+ }
+
+ long casualBase = 0;
+ foreach (Shows__show_SetSales.FanBucket bucket in context.CasualBuckets)
+ casualBase = SafeAdd(casualBase, Math.Max(0, bucket.BaseFans));
+
+ long assigned = 0;
+ context.AddingTrendingBonus = true;
+ try
+ {
+ for (int i = 0; i < context.CasualBuckets.Count; i++)
+ {
+ Shows__show_SetSales.FanBucket bucket = context.CasualBuckets[i];
+ long share;
+ if (i == context.CasualBuckets.Count - 1)
+ share = bonus - assigned;
+ else if (casualBase > 0)
+ {
+ share = (long)Math.Round(bonus * (double)Math.Max(0, bucket.BaseFans) / casualBase, MidpointRounding.AwayFromZero);
+ share = Math.Min(share, bonus - assigned);
+ }
+ else
+ share = (bonus - assigned) / (context.CasualBuckets.Count - i);
+
+ if (share > 0 && bucket.Fan != null)
+ {
+ data_girls.AddFans_Equally(share, bucket.Fan, cast);
+ assigned = SafeAdd(assigned, share);
+ }
+ }
+ }
+ finally
+ {
+ context.AddingTrendingBonus = false;
+ }
+
+ long displayed = SafeAdd(context.BaseNewFans, assigned);
+ if (displayed > int.MaxValue) val = int.MaxValue;
+ else if (displayed < int.MinValue) val = int.MinValue;
+ else val = (int)displayed;
+ }
+ }
+
+ [HarmonyPatch(typeof(business._proposal), "set_newFans")]
+ public class business__proposal_set_newFans
+ {
+ public static void Postfix(business._proposal __instance)
+ {
+ if (IsTrending() == TrendingStatus.trending && __instance != null && __instance._newFans > 0)
+ __instance._newFans = ScaleInt(__instance._newFans, GetTrendingCoeff());
+ }
+ }
+
+ [HarmonyPatch(typeof(business), "AddActiveProposal")]
+ public class business_AddActiveProposal
+ {
+ public static void Postfix(business __instance)
+ {
+ if (IsTrending() != TrendingStatus.trending || __instance == null || __instance.ActiveProposals == null || __instance.ActiveProposals.Count == 0)
+ return;
+
+ business.active_proposal proposal = __instance.ActiveProposals[__instance.ActiveProposals.Count - 1];
+ float coeff = GetTrendingCoeff();
+ if (proposal != null && proposal.Fans_per_week > 0 && coeff > 0f)
+ proposal.Fans_per_week = Mathf.RoundToInt(proposal.Fans_per_week / coeff);
+ }
+ }
+
+ [HarmonyPatch(typeof(business), "DoWeeklyFans")]
+ public class business_DoWeeklyFans
+ {
+ public static void Postfix(business __instance)
+ {
+ if (IsTrending() != TrendingStatus.trending || __instance == null || __instance.ActiveProposals == null)
+ return;
+
+ float extraCoeff = GetTrendingCoeff() - 1f;
+ foreach (business.active_proposal proposal in __instance.ActiveProposals)
+ {
+ if (proposal != null && proposal.Girl != null && proposal.Fans_per_week > 0)
+ proposal.Girl.AddFans(ScaleLong(proposal.Fans_per_week, extraCoeff), null);
+ }
+ }
+ }
+
+ [HarmonyPatch(typeof(Contracts_Line), "Set")]
+ public class Contracts_Line_Set
+ {
+ public static void Postfix(Contracts_Line __instance, business.active_proposal ___ActiveProposal)
+ {
+ if (IsTrending() != TrendingStatus.trending || __instance == null || __instance.NewFans == null || ___ActiveProposal == null)
+ return;
+
+ ExtensionMethods.SetText(__instance.NewFans,
+ ExtensionMethods.formatNumber(ScaleLong(___ActiveProposal.Fans_per_week, GetTrendingCoeff()), false, false));
+ }
+ }
+
+ [HarmonyPatch(typeof(SEvent_Tour.tour), "GetNewFansByAttendance")]
+ public class SEvent_Tour_tour_GetNewFansByAttendance
+ {
+ public static void Postfix(ref int __result)
+ {
+ if (IsTrending() == TrendingStatus.trending && __result > 0)
+ __result = ScaleInt(__result, GetTrendingCoeff());
+ }
+ }
+}
diff --git a/mods/Going Viral/BonusTooltip.cs b/mods/Going Viral/BonusTooltip.cs
index 655fd15..129a72c 100755
--- a/mods/Going Viral/BonusTooltip.cs
+++ b/mods/Going Viral/BonusTooltip.cs
@@ -1,141 +1,138 @@
-using HarmonyLib;
-using System;
-using System.Reflection;
-using UnityEngine;
-
-namespace GoingViral
-{
- // tooltip for shows
- [HarmonyPatch(typeof(Show_Popup), "SetParam")]
- public class Show_Popup_SetParam
- {
- public static void Postfix(Show_Popup __instance, Show_Popup_Param_Button._type type, Shows._param ___medium, Shows._param ___genre)
- {
- if (type != Show_Popup_Param_Button._type.medium)
- return;
-
- if (___medium.media_type == Shows._param._media_type.tv)
- {
- Show_Popup_Param_Button[] buttons = __instance.Grid_Genre.GetComponentsInChildren();
- foreach (Show_Popup_Param_Button button in buttons)
- {
- // Get last show
- DateTime? lastShowDate = null;
- foreach (Shows._show show in Shows.shows)
- {
- if (show.LaunchDate != null && show.medium.media_type == Shows._param._media_type.tv && show.genre.id == ___genre.id)
- {
- if (lastShowDate == null || show.LaunchDate > (lastShowDate ?? staticVars.dateTime))
- {
- lastShowDate = show.LaunchDate;
- }
- }
- }
-
- string tooltipText = Traverse.Create(button.GetComponent()).Field("tooltipText").GetValue() as string;
-
-
- if (lastShowDate != null)
- {
- int days = (staticVars.dateTime - (lastShowDate ?? staticVars.dateTime)).Days;
-
- string clr = mainScript.green;
- if (days < 365)
- {
- clr = mainScript.red;
- }
- tooltipText += mainScript.separator + Language.Data["TREND__GENRE"];
-
- if (days != 1)
- {
- tooltipText += ExtensionMethods.color(Language.Insert("DAYS_AGO", new[] { days.ToString() }), clr);
- }
- else
- {
- tooltipText += ExtensionMethods.color(Language.Data["ONE_DAY_AGO"], clr);
- }
- }
- else
- {
- tooltipText += mainScript.separator + Language.Data["TREND__GENRE_NEVER"];
- }
-
- button.GetComponent().SetTooltip(button.param.DescriptionReplaceVariables(tooltipText));
- }
- }
- else
- {
- Show_Popup_Param_Button[] buttons = __instance.Grid_Genre.GetComponentsInChildren();
- foreach (Show_Popup_Param_Button button in buttons)
- {
- button.GetComponent().SetTooltip(button.param.GetTooltip());
- }
- }
- }
-
- }
-
-
- // tooltip for single medium
- [HarmonyPatch(typeof(Shows._param), "GetTooltip")]
- public class Shows__param_GetTooltip
- {
- public static void Postfix(ref Shows._param __instance, ref string __result)
- {
- if (__instance.ParamType == Shows._param._paramType.medium)
- {
- __result += mainScript.separator + Language.Data["TREND__MEDIUM"];
- }
- }
- }
-
- // tooltip for single genres
- [HarmonyPatch(typeof(SinglePopup_GenreButton), "RenderTooltip")]
- public class SinglePopup_GenreButton_RenderTooltip
- {
- public static void Postfix(ref SinglePopup_GenreButton __instance)
- {
- singles._param param = __instance.param;
- if (param.derived != "business" && !param.IsRiskyMarketing())
- {
- return;
- }
-
- singles._param._special_type marketing = param.Special_Type;
- if(marketing != singles._param._special_type.ad_campaign && marketing != singles._param._special_type.viral_campaign && marketing != singles._param._special_type.fake_scandal)
- {
- return;
- }
-
- ButtonDefault buttonDefault = Traverse.Create(__instance).Field("buttonDefault").GetValue() as ButtonDefault;
- string tooltipText = Traverse.Create(buttonDefault).Field("tooltipText").GetValue() as string;
-
-
- tooltipText += mainScript.separator;
- int chanceSuccess = Mathf.RoundToInt(TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.success_crit) * param.GetSuccessChance(Single_Marketing_Roll._result.success_crit) / 100);
- tooltipText += Language.Insert("TREND__SINGLE_SUCCESS", chanceSuccess.ToString());
-
- int chanceFailure = 0;
- switch (marketing)
- {
- case singles._param._special_type.ad_campaign:
- case singles._param._special_type.viral_campaign:
- chanceFailure = Mathf.RoundToInt(TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.fail) * param.GetSuccessChance(Single_Marketing_Roll._result.fail) / 100 +
- TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.fail_crit) * param.GetSuccessChance(Single_Marketing_Roll._result.fail_crit) / 100);
- break;
- case singles._param._special_type.fake_scandal:
- chanceFailure = Mathf.RoundToInt(TrendingManager.GetTrendingChance(param.GetSuccessModifier(Single_Marketing_Roll._result.fail, false)) * param.GetSuccessChance(Single_Marketing_Roll._result.fail) / 100 +
- TrendingManager.GetTrendingChance(param.GetSuccessModifier(Single_Marketing_Roll._result.fail_crit, false)) * param.GetSuccessChance(Single_Marketing_Roll._result.fail_crit) / 100);
- break;
- default:
- break;
- }
-
- tooltipText += "\n" + Language.Insert("TREND__SINGLE_FAIL", chanceFailure.ToString());
- tooltipText += "\n" + Language.Data["TREND__SINGLE_DESC"];
-
- buttonDefault.SetTooltip(param.DescriptionReplaceVariables(tooltipText));
- }
-
- }
-}
+using HarmonyLib;
+using System;
+using UnityEngine;
+
+namespace GoingViral
+{
+ [HarmonyPatch(typeof(Show_Popup), "SetParam")]
+ public class Show_Popup_SetParam
+ {
+ [HarmonyAfter("com.tel.traitsfix")]
+ public static void Postfix(Show_Popup __instance, Show_Popup_Param_Button._type type, Shows._param ___medium)
+ {
+ if (type != Show_Popup_Param_Button._type.medium || __instance == null || __instance.Grid_Genre == null)
+ return;
+
+ Show_Popup_Param_Button[] buttons = __instance.Grid_Genre.GetComponentsInChildren();
+ if (buttons == null)
+ return;
+
+ bool isTv = ___medium != null && ___medium.media_type == Shows._param._media_type.tv;
+ foreach (Show_Popup_Param_Button button in buttons)
+ {
+ if (button == null || button.param == null)
+ continue;
+ ButtonDefault buttonDefault = button.GetComponent();
+ if (buttonDefault == null)
+ continue;
+
+ if (!isTv)
+ {
+ buttonDefault.SetTooltip(button.param.GetTooltip());
+ continue;
+ }
+
+ // Rebuild from the parameter tooltip each time. Other mods patching GetTooltip are
+ // preserved, while our own TV suffix cannot accumulate across repeated SetParam calls.
+ string tooltip = button.param.GetTooltip() ?? "";
+
+ DateTime? lastShowDate = GetLastTvShowDate(button.param.id);
+ if (lastShowDate.HasValue)
+ {
+ int days = Math.Max(0, (staticVars.dateTime - lastShowDate.Value).Days);
+ string color = days >= 365 ? mainScript.green : mainScript.red;
+ tooltip += mainScript.separator + Language.Data["TREND__GENRE"];
+ tooltip += days == 1
+ ? ExtensionMethods.color(Language.Data["ONE_DAY_AGO"], color)
+ : ExtensionMethods.color(Language.Insert("DAYS_AGO", new string[] { days.ToString() }), color);
+ }
+ else
+ {
+ tooltip += mainScript.separator + Language.Data["TREND__GENRE_NEVER"];
+ }
+
+ buttonDefault.SetTooltip(button.param.DescriptionReplaceVariables(tooltip));
+ }
+ }
+
+ private static DateTime? GetLastTvShowDate(int genreId)
+ {
+ DateTime? result = null;
+ if (Shows.shows == null)
+ return null;
+
+ foreach (Shows._show show in Shows.shows)
+ {
+ if (show == null || show.medium == null || show.genre == null || show.LaunchDate == default(DateTime))
+ continue;
+ if (show.medium.media_type != Shows._param._media_type.tv || show.genre.id != genreId)
+ continue;
+ if (!result.HasValue || show.LaunchDate > result.Value)
+ result = show.LaunchDate;
+ }
+ return result;
+ }
+ }
+
+ [HarmonyPatch(typeof(Shows._param), "GetTooltip")]
+ public class Shows__param_GetTooltip
+ {
+ public static void Postfix(Shows._param __instance, ref string __result)
+ {
+ if (__instance != null && __instance.ParamType == Shows._param._paramType.medium)
+ __result = (__result ?? "") + mainScript.separator + Language.Data["TREND__MEDIUM"];
+ }
+ }
+
+ [HarmonyPatch(typeof(SinglePopup_GenreButton), "RenderTooltip")]
+ public class SinglePopup_GenreButton_RenderTooltip
+ {
+ public static void Postfix(SinglePopup_GenreButton __instance)
+ {
+ if (__instance == null || __instance.param == null)
+ return;
+
+ singles._param param = __instance.param;
+ if (param.derived != "business" && !param.IsRiskyMarketing())
+ return;
+
+ singles._param._special_type marketing = param.Special_Type;
+ if (marketing != singles._param._special_type.ad_campaign &&
+ marketing != singles._param._special_type.viral_campaign &&
+ marketing != singles._param._special_type.fake_scandal)
+ return;
+
+ ButtonDefault buttonDefault = Traverse.Create(__instance).Field("buttonDefault").GetValue() as ButtonDefault;
+ if (buttonDefault == null)
+ return;
+
+ string tooltip = Traverse.Create(buttonDefault).Field("tooltipText").GetValue() as string;
+ if (tooltip == null) tooltip = "";
+ tooltip += mainScript.separator;
+
+ float success = TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.success_crit) *
+ param.GetSuccessChance(Single_Marketing_Roll._result.success_crit) / 100f;
+ int chanceSuccess = Mathf.Clamp(Mathf.RoundToInt(success), 0, 100);
+ tooltip += Language.Insert("TREND__SINGLE_SUCCESS", new string[] { chanceSuccess.ToString() });
+
+ float failure = 0f;
+ if (marketing == singles._param._special_type.ad_campaign || marketing == singles._param._special_type.viral_campaign)
+ {
+ failure += TrendingManager.GetTrendingChance(param, Single_Marketing_Roll._result.fail_crit) *
+ param.GetSuccessChance(Single_Marketing_Roll._result.fail_crit) / 100f;
+ }
+ else if (marketing == singles._param._special_type.fake_scandal)
+ {
+ failure += TrendingManager.GetTrendingChance(param.GetSuccessModifier(Single_Marketing_Roll._result.fail, false)) *
+ param.GetSuccessChance(Single_Marketing_Roll._result.fail) / 100f;
+ failure += TrendingManager.GetTrendingChance(param.GetSuccessModifier(Single_Marketing_Roll._result.fail_crit, false)) *
+ param.GetSuccessChance(Single_Marketing_Roll._result.fail_crit) / 100f;
+ }
+
+ int chanceFailure = Mathf.Clamp(Mathf.RoundToInt(failure), 0, 100);
+ tooltip += "\n" + Language.Insert("TREND__SINGLE_FAIL", new string[] { chanceFailure.ToString() });
+ tooltip += "\n" + Language.Data["TREND__SINGLE_DESC"];
+ buttonDefault.SetTooltip(param.DescriptionReplaceVariables(tooltip));
+ }
+ }
+}
diff --git a/mods/Going Viral/FanMechanics.cs b/mods/Going Viral/FanMechanics.cs
index 7fe5065..10a399d 100755
--- a/mods/Going Viral/FanMechanics.cs
+++ b/mods/Going Viral/FanMechanics.cs
@@ -1,276 +1,218 @@
-using HarmonyLib;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection.Emit;
-using System.Reflection;
-using static GoingViral.TrendingManager;
-using UnityEngine;
-using static Achievements;
-using static singles;
-
-namespace GoingViral
-{
-
- // Apply 3x casual bias to fan appeal for adding fans
- // Separate opinion from fan decrease
- [HarmonyPatch(typeof(data_girls.girls), "AddFans", new Type[] { typeof(long), typeof(resources.fanType?) })]
- public class data_girls_girls_AddFans
- {
- public static IEnumerable Transpiler(IEnumerable instructions)
- {
- List instructionList = new(instructions);
-
- int index = -1;
- bool breakFlag = false;
- object operandi = null;
- for (int i = 0; i < instructionList.Count; i++)
- {
- if (breakFlag && instructionList[i].opcode == OpCodes.Stloc_S)
- {
- index = i;
- operandi = instructionList[i].operand;
- break;
- }
- if (instructionList[i].opcode == OpCodes.Callvirt && instructionList[i].operand is MethodBase method && method.Name == "GetTotalAppeal" && method.DeclaringType == typeof(resources._fan))
- {
- breakFlag = true;
- }
- }
-
- if (index != -1)
- {
- instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldarg_0));
- instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Ldloc_3));
- instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Ldarg_1));
- instructionList.Insert(index + 4, new CodeInstruction(OpCodes.Ldloc_S, operandi));
- instructionList.Insert(index + 5, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(data_girls_girls_AddFans), "Infix")));
- instructionList.Insert(index + 6, new CodeInstruction(OpCodes.Stloc_S, operandi));
- }
-
- return instructionList.AsEnumerable();
- }
-
- public static float Infix(data_girls.girls __this, resources._fan fan, long val, float fanProportion)
- {
-
- // Apply default calc with opinions for decrease in fans
- if (val < 0)
- {
- float churnTotal = 0;
-
- foreach (resources._fan _fan in __this.Fans)
- {
- churnTotal += GetFanChurn(_fan.appeal, _fan.Ratio, _fan.hardcoreness);
- }
-
- fanProportion = GetFanChurn(fan.appeal, fan.Ratio, fan.hardcoreness) / churnTotal;
- }
- // Apply new calc excluding opinions for increase in fans
- else if (val > 0)
- {
- float acquisitionTotal = 0;
-
- foreach (resources._fan _fan in __this.Fans)
- {
- acquisitionTotal += GetFanAcquisition(__this, _fan.appeal, _fan.hardcoreness);
- }
-
- fanProportion = GetFanAcquisition(__this, fan.appeal, fan.hardcoreness) / acquisitionTotal;
- }
-
-
- return fanProportion;
- }
-
-
- }
-
- // When losing fans, use appeal and opinion instead of fame to distribute across girls
- [HarmonyPatch(typeof(data_girls), "AddFans")]
- public class data_girls_AddFans
- {
- public static IEnumerable Transpiler(IEnumerable instructions)
- {
- List instructionList = new(instructions);
-
- int index = -1;
- object coeffOperand = null;
- object girlOperand = null;
- for (int i = 0; i < instructionList.Count; i++)
- {
- if (instructionList[i].opcode == OpCodes.Stloc_S && instructionList[i].operand is LocalVariableInfo girl && girl.LocalIndex == 6)
- {
- girlOperand = instructionList[i].operand;
- }
- if (instructionList[i].opcode == OpCodes.Stloc_S && instructionList[i].operand is LocalVariableInfo coeff && coeff.LocalIndex == 7)
- {
- index = i;
- coeffOperand = instructionList[i].operand;
- break;
- }
- }
-
- if (index != -1)
- {
- instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_S, girlOperand));
- instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Ldloc_S, coeffOperand));
- instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Ldarg_0));
- instructionList.Insert(index + 4, new CodeInstruction(OpCodes.Ldarg_1));
- instructionList.Insert(index + 5, new CodeInstruction(OpCodes.Ldarg_2));
- instructionList.Insert(index + 6, new CodeInstruction(OpCodes.Ldarg_3));
- instructionList.Insert(index + 7, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(data_girls_AddFans), "Infix")));
- instructionList.Insert(index + 8, new CodeInstruction(OpCodes.Stloc_S, coeffOperand));
- }
-
- return instructionList.AsEnumerable();
- }
-
- public static float Infix(data_girls.girls girl, float coeff, long total_fans, resources.fanType? fanType, List Girls, data_girls.girls ExceptionGirl)
- {
- if (total_fans >= 0)
- {
- return coeff;
- }
-
- float churnTotal = 0;
-
- foreach (data_girls.girls _girl in Girls)
- {
- if (_girl.status != data_girls._status.graduated && _girl != ExceptionGirl)
- {
- foreach (resources._fan _fan in _girl.Fans)
- {
- if (_fan.IsType(fanType))
- {
- churnTotal += GetFanChurn(_fan.appeal, _fan.Ratio, _fan.hardcoreness);
- }
- }
- }
- }
-
- float girlChurn = 0;
- foreach (resources._fan _fan in girl.Fans)
- {
- if (_fan.IsType(fanType))
- {
- girlChurn += GetFanChurn(_fan.appeal, _fan.Ratio, _fan.hardcoreness);
- }
- }
-
- float fanProportion = girlChurn / churnTotal;
-
-
- return fanProportion;
- }
- }
-
- // Set opinion for theater
- [HarmonyPatch(typeof(resources), "OnNewWeek")]
- public class resources_OnNewWeek
- {
- public static void Postfix()
- {
- foreach (Theaters._theater theater in Theaters.Theaters_)
- {
- if(theater == null || theater.Stats.Count < 7)
- {
- continue;
- }
-
-
- foreach (object fanTypeEnum in Enum.GetValues(typeof(resources.fanType)))
- {
- resources.fanType type = (resources.fanType)fanTypeEnum;
-
- float thisWeek = 0;
- for (int i = 1; i <= 7; i++)
- {
- if(type == theater.Stats[theater.Stats.Count - i].Schedule.FanType)
- {
- thisWeek += 1;
- }
- }
-
- if (theater.Stats.Count < 14)
- {
- if(thisWeek > 0)
- {
- foreach (data_girls.girls girl in theater.GetGroup().GetGirls())
- {
- if (girl != null && girl.status != data_girls._status.graduated)
- {
- girl.AddAppeal(type, 1);
- NotificationManager.AddNotification(Language.Insert("THEATER__FANS_LIKE", new string[]
- {
- ExtensionMethods.color(resources.GetFanTitle(type), mainScript.green),
- theater.GetGroup().Title
- }), mainScript.green32, NotificationManager._notification._type.fans_opinion_change);
- }
- }
- }
- continue;
- }
-
- float pastWeek = 0;
- for (int i = 8; i <= 14; i++)
- {
- if (type == theater.Stats[theater.Stats.Count - i].Schedule.FanType)
- {
- pastWeek += 1;
- }
- }
-
- foreach (data_girls.girls girl in theater.GetGroup().GetGirls())
- {
- if (girl != null && girl.status != data_girls._status.graduated)
- {
- if(thisWeek > pastWeek)
- {
- girl.AddAppeal(type, 1);
- NotificationManager.AddNotification(Language.Insert("THEATER__FANS_LIKE", new string[]
- {
- ExtensionMethods.color(resources.GetFanTitle(type), mainScript.green),
- theater.GetGroup().Title
- }), mainScript.green32, NotificationManager._notification._type.fans_opinion_change);
- }
- else if (thisWeek < pastWeek)
- {
- girl.AddAppeal(type, -1);
- NotificationManager.AddNotification(Language.Insert("THEATER__FANS_DISLIKE", new string[]
- {
- ExtensionMethods.color(resources.GetFanTitle(type), mainScript.red),
- theater.GetGroup().Title
- }), mainScript.red32, NotificationManager._notification._type.fans_opinion_change);
- }
- }
- }
- }
-
- }
- }
- }
-
-
- // Fixed fan opinion to be impacted by concerts, SSK/show cancellation and random events
- [HarmonyPatch(typeof(resources._fanOpinion), "Add")]
- public class resources__fanOpinion_Add
- {
- public static void Postfix(resources._fanOpinion __instance, float val)
- {
- if (Harmony.HasAnyPatches("com.tel.unofficialpatch"))
- {
- return;
- }
- foreach (data_girls.girls girl in data_girls.girl)
- {
- if (girl != null && girl.status != data_girls._status.graduated)
- {
- girl.AddAppeal(__instance.type, val);
- }
- }
- }
- }
-
-
-}
+using HarmonyLib;
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using static GoingViral.TrendingManager;
+
+namespace GoingViral
+{
+ // Replace appeal weights only while an idol is actually distributing a fan change.
+ [HarmonyPatch(typeof(data_girls.girls), "AddFans", new Type[] { typeof(long), typeof(resources.fanType?) })]
+ public class data_girls_girls_AddFans
+ {
+ internal sealed class Context
+ {
+ public data_girls.girls Girl;
+ public long Value;
+ }
+
+ private static readonly Stack contexts = new Stack();
+ internal static Context Current { get { return contexts.Count > 0 ? contexts.Peek() : null; } }
+
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix(data_girls.girls __instance, long val)
+ {
+ contexts.Push(new Context { Girl = __instance, Value = val });
+ }
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ if (contexts.Count > 0) contexts.Pop();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(resources._fan), "GetTotalAppeal")]
+ public class resources__fan_GetTotalAppeal_TrendingWeights
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(resources._fan __instance, ref float __result)
+ {
+ data_girls_girls_AddFans.Context context = data_girls_girls_AddFans.Current;
+ if (context == null || context.Girl == null || __instance == null)
+ return;
+
+ float weight;
+ if (context.Value < 0)
+ weight = GetFanChurn(__instance.appeal, __instance.Ratio, __instance.hardcoreness);
+ else if (context.Value > 0)
+ weight = GetFanAcquisition(context.Girl, __instance.appeal, __instance.hardcoreness);
+ else
+ return;
+
+ if (!float.IsNaN(weight) && !float.IsInfinity(weight))
+ __result = Math.Max(0f, weight);
+ }
+ }
+
+ // While the global AddFans routine allocates losses among idols, substitute churn weights
+ // for fame points. The original allocator and its rounding remain intact for compatibility.
+ [HarmonyPatch(typeof(data_girls), "AddFans", new Type[] { typeof(long), typeof(resources.fanType?), typeof(List), typeof(data_girls.girls) })]
+ public class data_girls_AddFans
+ {
+ internal sealed class Context
+ {
+ public long Value;
+ public resources.fanType? FanType;
+ public List Girls;
+ public data_girls.girls ExceptionGirl;
+ }
+
+ private static readonly Stack contexts = new Stack();
+ internal static Context Current { get { return contexts.Count > 0 ? contexts.Peek() : null; } }
+
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix(long total_fans, resources.fanType? fanType, List Girls, data_girls.girls ExceptionGirl)
+ {
+ contexts.Push(new Context
+ {
+ Value = total_fans,
+ FanType = fanType,
+ Girls = Girls,
+ ExceptionGirl = ExceptionGirl
+ });
+ }
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ if (contexts.Count > 0) contexts.Pop();
+ return __exception;
+ }
+
+ internal static bool IsEligible(Context context, data_girls.girls girl)
+ {
+ if (context == null || girl == null || girl == context.ExceptionGirl || girl.status == data_girls._status.graduated)
+ return false;
+ List pool = context.Girls ?? data_girls.girl;
+ return pool != null && pool.Contains(girl);
+ }
+
+ internal static float GetGirlChurnWeight(Context context, data_girls.girls girl)
+ {
+ if (!IsEligible(context, girl) || girl.Fans == null)
+ return 0f;
+
+ float total = 0f;
+ foreach (resources._fan fan in girl.Fans)
+ {
+ if (fan != null && fan.IsType(context.FanType))
+ total += GetFanChurn(fan.appeal, fan.Ratio, fan.hardcoreness);
+ }
+ return total;
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls.girls), "GetFamePoints")]
+ public class data_girls_girls_GetFamePoints_TrendingChurn
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(data_girls.girls __instance, ref float __result)
+ {
+ data_girls_AddFans.Context context = data_girls_AddFans.Current;
+ if (context == null || context.Value >= 0 || !data_girls_AddFans.IsEligible(context, __instance))
+ return;
+
+ float weight = data_girls_AddFans.GetGirlChurnWeight(context, __instance);
+ if (!float.IsNaN(weight) && !float.IsInfinity(weight) && weight >= 0f)
+ __result = weight * 1000f; // avoids vanilla's <1 fame fallback while preserving proportions
+ }
+ }
+
+ [HarmonyPatch(typeof(resources), "OnNewWeek")]
+ public class resources_OnNewWeek
+ {
+ public static void Postfix()
+ {
+ if (Theaters.Theaters_ == null)
+ return;
+
+ foreach (Theaters._theater theater in Theaters.Theaters_)
+ {
+ if (theater == null || theater.Stats == null || theater.Stats.Count < 7 || theater.GetGroup() == null)
+ continue;
+
+ foreach (resources.fanType type in Enum.GetValues(typeof(resources.fanType)))
+ {
+ int thisWeek = CountScheduledDays(theater, type, 1, 7);
+ if (theater.Stats.Count < 14)
+ {
+ if (thisWeek > 0)
+ ApplyTheaterOpinion(theater, type, 1f);
+ continue;
+ }
+
+ int pastWeek = CountScheduledDays(theater, type, 8, 14);
+ if (thisWeek > pastWeek)
+ ApplyTheaterOpinion(theater, type, 1f);
+ else if (thisWeek < pastWeek)
+ ApplyTheaterOpinion(theater, type, -1f);
+ }
+ }
+ }
+
+ private static int CountScheduledDays(Theaters._theater theater, resources.fanType type, int fromLatest, int toLatest)
+ {
+ int count = 0;
+ if (theater == null || theater.Stats == null)
+ return 0;
+
+ for (int offset = fromLatest; offset <= toLatest; offset++)
+ {
+ int index = theater.Stats.Count - offset;
+ if (index < 0 || index >= theater.Stats.Count)
+ continue;
+ Theaters._theater._stat stat = theater.Stats[index];
+ if (stat != null && stat.Schedule != null && stat.Schedule.FanType == type)
+ count++;
+ }
+ return count;
+ }
+
+ private static void ApplyTheaterOpinion(Theaters._theater theater, resources.fanType type, float value)
+ {
+ Groups._group group = theater != null ? theater.GetGroup() : null;
+ if (group == null || group.GetGirls() == null)
+ return;
+
+ foreach (data_girls.girls girl in group.GetGirls())
+ {
+ if (girl != null && girl.status != data_girls._status.graduated)
+ girl.AddAppeal(type, value);
+ }
+
+ string key = value > 0 ? "THEATER__FANS_LIKE" : "THEATER__FANS_DISLIKE";
+ string color = value > 0 ? mainScript.green : mainScript.red;
+ Color32 color32 = value > 0 ? mainScript.green32 : mainScript.red32;
+ NotificationManager.AddNotification(
+ Language.Insert(key, new string[] { ExtensionMethods.color(resources.GetFanTitle(type), color), group.Title }),
+ color32,
+ NotificationManager._notification._type.fans_opinion_change);
+ }
+ }
+
+ [HarmonyPatch(typeof(resources._fanOpinion), "Add")]
+ public class resources__fanOpinion_Add
+ {
+ public static void Postfix(resources._fanOpinion __instance, float val)
+ {
+ if (Harmony.HasAnyPatches("com.tel.unofficialpatch") || __instance == null || data_girls.girl == null)
+ return;
+
+ foreach (data_girls.girls girl in data_girls.girl)
+ {
+ if (girl != null && girl.status != data_girls._status.graduated)
+ girl.AddAppeal(__instance.type, val);
+ }
+ }
+ }
+}
diff --git a/mods/Going Viral/FanTooltip.cs b/mods/Going Viral/FanTooltip.cs
index 99aaf6b..52d2d9b 100755
--- a/mods/Going Viral/FanTooltip.cs
+++ b/mods/Going Viral/FanTooltip.cs
@@ -1,289 +1,247 @@
-using HarmonyLib;
-using System;
-using System.Reflection;
-using UnityEngine.UI;
-using UnityEngine;
-using static GoingViral.TrendingManager;
-
-namespace GoingViral
-{
- // Render tooltip
- [HarmonyPatch(typeof(tooltip_fans), "Render", new Type[] { })]
- public class tooltip_fans_Render
- {
- [HarmonyAfter("com.tel.fanattrition")]
- public static bool Prefix(tooltip_fans __instance)
- {
- if (__instance != null && __instance.gameObject.transform != null)
- {
- if (!Harmony.HasAnyPatches("com.tel.fanattrition"))
- {
- resources.RecalcFans();
-
- __instance.gameObject.GetComponentsInChildren()[0].text = GetFanLine(resources.fanType.hardcore);
- __instance.gameObject.GetComponentsInChildren()[1].text = GetFanLine(resources.fanType.casual);
- __instance.gameObject.GetComponentsInChildren()[2].text = GetFanLine(resources.fanType.male);
- __instance.gameObject.GetComponentsInChildren()[3].text = GetFanLine(resources.fanType.female);
- __instance.gameObject.GetComponentsInChildren()[4].text = GetFanLine(resources.fanType.teen);
- __instance.gameObject.GetComponentsInChildren()[5].text = GetFanLine(resources.fanType.youngAdult);
- __instance.gameObject.GetComponentsInChildren()[6].text = GetFanLine(resources.fanType.adult);
-
- __instance.gameObject.GetComponentsInChildren()[7].text = mainScript.separator_no_linebreaks;
-
- __instance.gameObject.GetComponentsInChildren()[10].text = GetShowLine(Shows._param._media_type.internet);
- __instance.gameObject.GetComponentsInChildren()[11].text = GetShowLine(Shows._param._media_type.tv);
- __instance.gameObject.GetComponentsInChildren()[12].text = GetShowLine(Shows._param._media_type.radio);
-
- __instance.gameObject.GetComponentsInChildren()[13].text = GetCafeLine();
-
- }
- __instance.gameObject.GetComponentsInChildren()[8].text = GetContractsLine(business._type.ad);
- __instance.gameObject.GetComponentsInChildren()[9].text = GetContractsLine(business._type.tv_drama);
-
- __instance.gameObject.GetComponentsInChildren()[14].text = GetChurnLine();
- __instance.gameObject.GetComponentsInChildren()[15].text = mainScript.separator_no_linebreaks;
- __instance.gameObject.GetComponentsInChildren()[16].text = GetTrendingLine();
-
- var RenderFanChange = __instance.GetType().GetMethod("RenderFanChange", BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
- RenderFanChange.Invoke(__instance, null);
- LayoutRebuilder.ForceRebuildLayoutImmediate(__instance.gameObject.GetComponent());
- return false;
- }
- return true;
- }
-
- static string GetChurnLine()
- {
- if (Harmony.HasAnyPatches("com.tel.fanattrition"))
- {
- if (resources.FansChange < 0)
- {
- return Language.Data["CHURN"] + ": " + ExtensionMethods.color(ExtensionMethods.formatNumber(resources.FansChange * 7) + " " + Language.Data["PER_WEEK"], mainScript.red);
- }
- else
- {
- return Language.Data["CHURN"] + ": " + 0 + " " + Language.Data["PER_WEEK"];
- }
- }
- return "";
- }
-
- static string GetTrendingLine()
- {
- if (trending > 1)
- {
- return Language.Data["TIP__TRENDING"] + ": " + ExtensionMethods.color(GetTrendingCoeff() + "x (" + trending + " " + Language.Data["TIP__DAYS_LEFT"] + ")", mainScript.green);
- }
- else if (trending > 0)
- {
- return Language.Data["TIP__TRENDING"] + ": " + ExtensionMethods.color(GetTrendingCoeff() + "x (" + trending + " " + Language.Data["TIP__DAY_LEFT"] + ")", mainScript.green);
- }
- else if (trending < -1)
- {
- return Language.Data["TIP__TRENDING"] + ": " + ExtensionMethods.color(GetTrendingCoeff() + "x (" + -trending + " " + Language.Data["TIP__DAY_LEFT"] + ")", mainScript.red);
- }
- else if (trending < 0)
- {
- return Language.Data["TIP__TRENDING"] + ": " + ExtensionMethods.color(GetTrendingCoeff() + "x (" + -trending + " " + Language.Data["TIP__DAYS_LEFT"] + ")", mainScript.red);
- }
- else
- {
- return Language.Data["TIP__TRENDING"] + ": " + Language.Data["TIP__NONE"];
- }
- }
-
- static string GetCafeLine()
- {
- if (cafeFans > 0)
- {
- return Language.Data["TIP__CAFE"] + ": " + ExtensionMethods.color("+" + ExtensionMethods.formatNumber(cafeFans) + " " + Language.Data["PER_WEEK"], mainScript.green);
- }
- else
- {
- return Language.Data["TIP__CAFE"] + ": " + cafeFans + " " + Language.Data["PER_WEEK"];
- }
- }
-
- static string GetFanLine(resources.fanType FanType)
- {
- int num = 0;
- float appeal = 0f;
- foreach (data_girls.girls girls in data_girls.GetActiveGirls())
- {
- if (girls != null)
- {
- if (girls.FanAppeal.Count == 0)
- {
- girls.RecalcFanAppeal();
- }
- appeal += girls.GetFanAppeal(FanType).ratio;
- num++;
- }
- }
-
- string appealStr = "0%";
- string ratioStr = "0%";
- if (num > 0)
- {
- float avgAppeal = appeal / num;
- if (avgAppeal >= 0.4)
- {
- appealStr = ExtensionMethods.color(ExtensionMethods.toPercent(avgAppeal) + "%", mainScript.green);
- }
- else if (avgAppeal <= 0.3)
- {
- appealStr = ExtensionMethods.color(ExtensionMethods.toPercent(avgAppeal) + "%", mainScript.red);
- }
- else
- {
- appealStr = ExtensionMethods.toPercent(avgAppeal) + "%";
- }
-
- float fanRatio = (float)resources.GetFansTotal(FanType) / resources.GetFansTotal();
- float upper = 0.6f;
- float lower = 0.4f;
- if (FanType == resources.fanType.teen || FanType == resources.fanType.youngAdult || FanType == resources.fanType.adult)
- {
- upper = 0.4f;
- lower = 0.25f;
- }
- if (fanRatio >= upper)
- {
- ratioStr = ExtensionMethods.color(ExtensionMethods.toPercent(fanRatio) + "%", mainScript.green);
- }
- else if (fanRatio <= lower)
- {
- ratioStr = ExtensionMethods.color(ExtensionMethods.toPercent(fanRatio) + "%", mainScript.red);
- }
- else
- {
- ratioStr = ExtensionMethods.toPercent(fanRatio) + "%";
- }
- }
-
- return resources.GetFanTitle(FanType) + ": " + ratioStr + " " + Language.Data["TIP__OF_TOTAL"] + " (" + appealStr + " " + Language.Data["TIP__APPEAL"] + ")";
- }
-
- static string GetContractsLine(business._type Type)
- {
-
- string fanStr = "0 " + Language.Data["PER_WEEK"];
- if (Type == business._type.ad)
- {
- if (adFans > 0)
- {
- fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(adFans) + " " + Language.Data["PER_WEEK"], mainScript.green);
- }
- return Language.Data["TIP__AD"] + ": " + fanStr;
- }
- else if (Type == business._type.tv_drama)
- {
- if (dramaFans > 0)
- {
- fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(dramaFans) + " " + Language.Data["PER_WEEK"], mainScript.green);
- }
- return Language.Data["TIP__DRAMA"] + ": " + fanStr;
- }
- return "";
- }
-
- static string GetShowLine(Shows._param._media_type Type)
- {
-
- string fanStr = "0 " + Language.Data["PER_WEEK"];
- if (Type == Shows._param._media_type.tv)
- {
- if (tvFans > 0)
- {
- fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(tvFans) + " " + Language.Data["PER_WEEK"], mainScript.green);
- }
- return Language.Data["TIP__TV"] + ": " + fanStr;
- }
- else if (Type == Shows._param._media_type.internet)
- {
- if (netFans > 0)
- {
- fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(netFans) + " " + Language.Data["PER_WEEK"], mainScript.green);
- }
- return Language.Data["TIP__INTERNET"] + ": " + fanStr;
- }
- else if (Type == Shows._param._media_type.radio)
- {
- if (radioFans > 0)
- {
- fanStr = ExtensionMethods.color("+" + ExtensionMethods.formatNumber(radioFans) + " " + Language.Data["PER_WEEK"], mainScript.green);
- }
- return Language.Data["TIP__RADIO"] + ": " + fanStr;
- }
- return "";
- }
- }
-
- // Set up the fan tooltip structure
- [HarmonyPatch(typeof(tooltip_fans), "Start", new Type[] { })]
- public class tooltip_fans_Start
- {
- [HarmonyAfter("com.tel.fanattrition")]
- public static bool Prefix(tooltip_fans __instance)
- {
- var AddPadding = __instance.GetType().GetMethod("AddPadding", BindingFlags.NonPublic | BindingFlags.Instance);
-
- if (!Harmony.HasAnyPatches("com.tel.fanattrition"))
- {
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- AddLine(__instance, "");
- }
- AddLine(__instance, "");
- AddLine(__instance, "");
-
- return true;
- }
-
- static void AddLine(tooltip_fans instance, string txt)
- {
- GameObject gameObject = UnityEngine.Object.Instantiate(instance.prefab_line);
- gameObject.transform.SetParent(instance.gameObject.transform, false);
- gameObject.GetComponent().Set(txt);
- }
- }
-
-
-
- // Render the bottom line of fan tooltip
- [HarmonyPatch(typeof(tooltip_fans), "RenderFanChange", new Type[] { })]
- public class tooltip_fans_RenderFanChange
- {
- [HarmonyAfter("com.tel.fanattrition")]
- public static void Postfix(tooltip_fans __instance)
- {
- long netChange = adFans + dramaFans + netFans + tvFans + radioFans + resources.FansChange * 7;
- string changeStr = ExtensionMethods.formatNumber(netChange, false, false);
- if (netChange > 0)
- {
- changeStr = ExtensionMethods.color("+" + changeStr + " " + Language.Data["PER_WEEK"], mainScript.green);
- }
- else if (netChange < 0)
- {
- changeStr = ExtensionMethods.color(changeStr + " " + Language.Data["PER_WEEK"], mainScript.red);
- }
- ExtensionMethods.SetText(__instance.fan_change, string.Concat(new string[]
- {
- Language.Data["TOTAL"] + ": ",
- changeStr
- }));
- }
- }
-}
+using HarmonyLib;
+using System;
+using UnityEngine;
+using UnityEngine.UI;
+using static GoingViral.TrendingManager;
+
+namespace GoingViral
+{
+ internal static class ViralFanTooltip
+ {
+ private const string Hardcore = "GV_Hardcore";
+ private const string Casual = "GV_Casual";
+ private const string Male = "GV_Male";
+ private const string Female = "GV_Female";
+ private const string Teen = "GV_Teen";
+ private const string YoungAdult = "GV_YoungAdult";
+ private const string Adult = "GV_Adult";
+ private const string Separator1 = "GV_Separator1";
+ private const string Ad = "GV_Ad";
+ private const string Drama = "GV_Drama";
+ private const string Internet = "GV_Internet";
+ private const string TV = "GV_TV";
+ private const string Radio = "GV_Radio";
+ private const string Cafe = "GV_Cafe";
+ private const string Churn = "GV_Churn";
+ private const string Separator2 = "GV_Separator2";
+ private const string Trending = "GV_Trending";
+
+ internal static readonly string[] BaseLines =
+ {
+ Hardcore, Casual, Male, Female, Teen, YoungAdult, Adult, Separator1,
+ Ad, Drama, Internet, TV, Radio, Cafe, Churn
+ };
+
+ internal static void EnsureLine(tooltip_fans instance, string name)
+ {
+ if (instance == null || instance.prefab_line == null || instance.transform == null || instance.transform.Find(name) != null)
+ return;
+ GameObject line = UnityEngine.Object.Instantiate(instance.prefab_line);
+ line.name = name;
+ line.transform.SetParent(instance.transform, false);
+ tooltip_fans_line component = line.GetComponent();
+ if (component != null) component.Set("");
+ }
+
+ internal static void EnsureExtraLines(tooltip_fans instance)
+ {
+ EnsureLine(instance, Separator2);
+ EnsureLine(instance, Trending);
+ }
+
+ internal static void SetLine(tooltip_fans instance, string name, string text)
+ {
+ if (instance == null || instance.transform == null)
+ return;
+ Transform transform = instance.transform.Find(name);
+ if (transform == null)
+ return;
+ tooltip_fans_line line = transform.GetComponent();
+ if (line != null) line.Set(text ?? "");
+ }
+
+ internal static void RenderBaseLines(tooltip_fans instance)
+ {
+ resources.RecalcFans();
+ SetLine(instance, Hardcore, GetFanLine(resources.fanType.hardcore));
+ SetLine(instance, Casual, GetFanLine(resources.fanType.casual));
+ SetLine(instance, Male, GetFanLine(resources.fanType.male));
+ SetLine(instance, Female, GetFanLine(resources.fanType.female));
+ SetLine(instance, Teen, GetFanLine(resources.fanType.teen));
+ SetLine(instance, YoungAdult, GetFanLine(resources.fanType.youngAdult));
+ SetLine(instance, Adult, GetFanLine(resources.fanType.adult));
+ SetLine(instance, Separator1, mainScript.separator_no_linebreaks);
+ SetLine(instance, Ad, GetContractsLine(business._type.ad));
+ SetLine(instance, Drama, GetContractsLine(business._type.tv_drama));
+ SetLine(instance, Internet, GetShowLine(Shows._param._media_type.internet));
+ SetLine(instance, TV, GetShowLine(Shows._param._media_type.tv));
+ SetLine(instance, Radio, GetShowLine(Shows._param._media_type.radio));
+ SetLine(instance, Cafe, GetCafeLine());
+ SetLine(instance, Churn, GetChurnLine());
+ }
+
+ internal static void RenderExtras(tooltip_fans instance)
+ {
+ EnsureExtraLines(instance);
+ SetLine(instance, Separator2, mainScript.separator_no_linebreaks);
+ SetLine(instance, Trending, GetTrendingLine());
+ RenderTotalChange(instance);
+ if (instance != null)
+ {
+ RectTransform rect = instance.GetComponent();
+ if (rect != null) LayoutRebuilder.ForceRebuildLayoutImmediate(rect);
+ }
+ }
+
+ internal static void RenderTotalChange(tooltip_fans instance)
+ {
+ if (instance == null || instance.fan_change == null)
+ return;
+ long netChange = 0;
+ netChange = SafeAdd(netChange, adFans);
+ netChange = SafeAdd(netChange, dramaFans);
+ netChange = SafeAdd(netChange, netFans);
+ netChange = SafeAdd(netChange, tvFans);
+ netChange = SafeAdd(netChange, radioFans);
+ netChange = SafeAdd(netChange, cafeFans);
+ netChange = SafeAdd(netChange, ScaleLong(resources.FansChange, 7f));
+
+ string text = ExtensionMethods.formatNumber(netChange, false, false) + " " + Language.Data["PER_WEEK"];
+ if (netChange > 0) text = ExtensionMethods.color("+" + text, mainScript.green);
+ else if (netChange < 0) text = ExtensionMethods.color(text, mainScript.red);
+ ExtensionMethods.SetText(instance.fan_change, Language.Data["TOTAL"] + ": " + text);
+ }
+
+ private static string GetChurnLine()
+ {
+ if (!Harmony.HasAnyPatches("com.tel.fanattrition"))
+ return "";
+ long churn = Math.Min(ScaleLong(resources.FansChange, 7f), 0L);
+ string text = ExtensionMethods.formatNumber(churn) + " " + Language.Data["PER_WEEK"];
+ if (churn < 0) text = ExtensionMethods.color(text, mainScript.red);
+ return Language.Data["CHURN"] + ": " + text;
+ }
+
+ private static string GetTrendingLine()
+ {
+ if (trending == 0)
+ return Language.Data["TIP__TRENDING"] + ": " + Language.Data["TIP__NONE"];
+
+ long days = Math.Abs(trending);
+ string daysKey = days == 1 ? "TIP__DAY_LEFT" : "TIP__DAYS_LEFT";
+ string value = GetTrendingCoeff() + "x (" + days + " " + Language.Data[daysKey] + ")";
+ return Language.Data["TIP__TRENDING"] + ": " +
+ ExtensionMethods.color(value, trending > 0 ? mainScript.green : mainScript.red);
+ }
+
+ private static string GetCafeLine()
+ {
+ string value = ExtensionMethods.formatNumber(Math.Max(0, cafeFans)) + " " + Language.Data["PER_WEEK"];
+ if (cafeFans > 0) value = ExtensionMethods.color("+" + value, mainScript.green);
+ return Language.Data["TIP__CAFE"] + ": " + value;
+ }
+
+ private static string GetFanLine(resources.fanType type)
+ {
+ int count = 0;
+ float appeal = 0f;
+ var girls = data_girls.GetActiveGirls();
+ if (girls != null)
+ {
+ foreach (data_girls.girls girl in girls)
+ {
+ if (girl == null) continue;
+ if (girl.FanAppeal == null || girl.FanAppeal.Count == 0) girl.RecalcFanAppeal();
+ singles._fanAppeal fanAppeal = girl.GetFanAppeal(type);
+ if (fanAppeal == null) continue;
+ appeal += fanAppeal.ratio;
+ count++;
+ }
+ }
+
+ float avgAppeal = count > 0 ? appeal / count : 0f;
+ string appealText = ExtensionMethods.toPercent(avgAppeal) + "%";
+ if (avgAppeal >= 0.4f) appealText = ExtensionMethods.color(appealText, mainScript.green);
+ else if (avgAppeal <= 0.3f) appealText = ExtensionMethods.color(appealText, mainScript.red);
+
+ long totalFans = resources.GetFansTotal();
+ float fanRatio = totalFans > 0 ? (float)resources.GetFansTotal(type) / totalFans : 0f;
+ string ratioText = ExtensionMethods.toPercent(fanRatio) + "%";
+ float upper = (type == resources.fanType.teen || type == resources.fanType.youngAdult || type == resources.fanType.adult) ? 0.4f : 0.6f;
+ float lower = (type == resources.fanType.teen || type == resources.fanType.youngAdult || type == resources.fanType.adult) ? 0.25f : 0.4f;
+ if (fanRatio >= upper) ratioText = ExtensionMethods.color(ratioText, mainScript.green);
+ else if (fanRatio <= lower) ratioText = ExtensionMethods.color(ratioText, mainScript.red);
+
+ return resources.GetFanTitle(type) + ": " + ratioText + " " + Language.Data["TIP__OF_TOTAL"] +
+ " (" + appealText + " " + Language.Data["TIP__APPEAL"] + ")";
+ }
+
+ private static string GetContractsLine(business._type type)
+ {
+ long value = type == business._type.ad ? adFans : type == business._type.tv_drama ? dramaFans : 0;
+ string label = type == business._type.ad ? Language.Data["TIP__AD"] : Language.Data["TIP__DRAMA"];
+ string text = ExtensionMethods.formatNumber(Math.Max(0, value)) + " " + Language.Data["PER_WEEK"];
+ if (value > 0) text = ExtensionMethods.color("+" + text, mainScript.green);
+ return label + ": " + text;
+ }
+
+ private static string GetShowLine(Shows._param._media_type type)
+ {
+ long value = 0;
+ string label = "";
+ if (type == Shows._param._media_type.tv) { value = tvFans; label = Language.Data["TIP__TV"]; }
+ else if (type == Shows._param._media_type.internet) { value = netFans; label = Language.Data["TIP__INTERNET"]; }
+ else if (type == Shows._param._media_type.radio) { value = radioFans; label = Language.Data["TIP__RADIO"]; }
+ string text = ExtensionMethods.formatNumber(Math.Max(0, value)) + " " + Language.Data["PER_WEEK"];
+ if (value > 0) text = ExtensionMethods.color("+" + text, mainScript.green);
+ return label + ": " + text;
+ }
+ }
+
+ [HarmonyPatch(typeof(tooltip_fans), "Start", new Type[] { })]
+ public class tooltip_fans_Start
+ {
+ [HarmonyAfter("com.tel.fanattrition")]
+ public static void Prefix(tooltip_fans __instance)
+ {
+ if (!Harmony.HasAnyPatches("com.tel.fanattrition"))
+ {
+ foreach (string name in ViralFanTooltip.BaseLines)
+ ViralFanTooltip.EnsureLine(__instance, name);
+ }
+ ViralFanTooltip.EnsureExtraLines(__instance);
+ }
+ }
+
+ [HarmonyPatch(typeof(tooltip_fans), "Render", new Type[] { })]
+ public class tooltip_fans_Render
+ {
+ [HarmonyAfter("com.tel.fanattrition")]
+ public static bool Prefix(tooltip_fans __instance)
+ {
+ if (Harmony.HasAnyPatches("com.tel.fanattrition"))
+ return true;
+ if (__instance == null || __instance.transform == null)
+ return true;
+
+ ViralFanTooltip.RenderBaseLines(__instance);
+ ViralFanTooltip.RenderExtras(__instance);
+ return false;
+ }
+
+ public static void Postfix(tooltip_fans __instance)
+ {
+ ViralFanTooltip.RenderExtras(__instance);
+ }
+ }
+
+ [HarmonyPatch(typeof(tooltip_fans), "RenderFanChange", new Type[] { })]
+ public class tooltip_fans_RenderFanChange
+ {
+ [HarmonyAfter("com.tel.fanattrition")]
+ public static void Postfix(tooltip_fans __instance)
+ {
+ ViralFanTooltip.RenderTotalChange(__instance);
+ }
+ }
+}
diff --git a/mods/Going Viral/Going Viral.csproj b/mods/Going Viral/Going Viral.csproj
index 6fde472..85e24c5 100755
--- a/mods/Going Viral/Going Viral.csproj
+++ b/mods/Going Viral/Going Viral.csproj
@@ -1,25 +1,25 @@
-
-
-
- Going Viral
- com.tel.goingviral
- Introduces a trending mechanism. You have a small chance to go viral and become trending, which doubles your fan growth.
- Tel
- 1.0.0
- ["gameplay"]
-
-
- 0
-
- $(HarmonyID)
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
-
+
+
+
+ Going Viral
+ com.tel.goingviral
+ Introduces a trending mechanism. You have a small chance to go viral and become trending, which doubles your fan growth.
+ Tel
+ 1.0.1
+ ["gameplay"]
+
+
+ 0
+
+ $(HarmonyID)
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
diff --git a/mods/Going Viral/Resource.cs b/mods/Going Viral/Resource.cs
index 483218e..eac35e2 100755
--- a/mods/Going Viral/Resource.cs
+++ b/mods/Going Viral/Resource.cs
@@ -1,62 +1,110 @@
-using HarmonyLib;
-using UnityEngine;
-using System;
-using System.Collections.Generic;
-using System.Reflection.Emit;
-using System.Reflection;
-using System.Linq;
-
-namespace GoingViral
-{
-
- // Trending stat and fan counts
- // Apply trending to churn
- [HarmonyPatch(typeof(resources), "OnNewDay")]
- public class resources_OnNewDay
- {
- [HarmonyAfter("com.tel.fanattrition")]
- public static void Postfix()
- {
- if(TrendingManager.trending > 0)
- {
- TrendingManager.trending = Math.Min(90, Math.Max(0, TrendingManager.trending - 1));
- }
- else if(TrendingManager.trending < 0)
- {
- TrendingManager.trending = Math.Max(-90, Math.Min(0, TrendingManager.trending + 1));
- resources.FansChange = (long)Mathf.Round(resources.FansChange * TrendingManager.GetTrendingCoeff());
- }
- Debug.Log("Trending: " + TrendingManager.trending);
-
- TrendingManager.UpdateFanCount();
- }
- }
-
- // Save trending resource
- [HarmonyPatch(typeof(resources), "SaveFunction")]
- public class resources_SaveFunction
- {
- public static void Postfix()
- {
- Camera.main.GetComponent().GetSavedData().resources__Resources.Add(new resources.ResourceData{Type = resources.type.buzz, Val = TrendingManager.trending + 10000 });
- }
- }
-
- // Set trending resource
- [HarmonyPatch(typeof(resources), "Set")]
- public class resources_Set
- {
- public static bool Prefix(resources.type _type, long val)
- {
- if(_type == resources.type.buzz)
- {
- if(val >= 10000)
- {
- TrendingManager.trending = val - 10000;
- }
- }
- return true;
- }
- }
-
-}
+using HarmonyLib;
+using System;
+using UnityEngine;
+
+namespace GoingViral
+{
+ [HarmonyPatch(typeof(resources), "OnNewDay")]
+ public class resources_OnNewDay
+ {
+ [HarmonyAfter("com.tel.fanattrition")]
+ public static void Postfix()
+ {
+ if (TrendingManager.trending < 0)
+ {
+ // Apply today's crisis before consuming a day, including the final day.
+ resources.FansChange = TrendingManager.ScaleLong(resources.FansChange, TrendingManager.GetTrendingCoeff());
+ TrendingManager.trending = Math.Min(0, TrendingManager.trending + 1);
+ }
+ else if (TrendingManager.trending > 0)
+ {
+ TrendingManager.trending = Math.Max(0, TrendingManager.trending - 1);
+ }
+
+ TrendingManager.UpdateFanCount();
+ }
+ }
+
+ [HarmonyPatch(typeof(resources), "SaveFunction")]
+ public class resources_SaveFunction
+ {
+ private const long SAVE_MARKER = 1000000L;
+ private const long SAVE_OFFSET = 100L;
+
+ public static void Postfix()
+ {
+ if (Camera.main == null)
+ return;
+ mainScript main = Camera.main.GetComponent();
+ if (main == null || main.GetSavedData() == null || main.GetSavedData().resources__Resources == null)
+ return;
+
+ main.GetSavedData().resources__Resources.Add(new resources.ResourceData
+ {
+ Type = resources.type.buzz,
+ Val = SAVE_MARKER + SAVE_OFFSET + TrendingManager.trending
+ });
+ }
+ }
+
+ [HarmonyPatch(typeof(resources), "Set")]
+ public class resources_Set
+ {
+ private const long SAVE_MARKER = 1000000L;
+ private const long SAVE_OFFSET = 100L;
+
+ public static bool Prefix(resources.type _type, long val)
+ {
+ // Tagged Buzz values are private save records. Never reinterpret a runtime Buzz Set.
+ if (!resources_LoadFunction.Loading || _type != resources.type.buzz)
+ return true;
+
+ // New marker. Consume it instead of allowing vanilla Set() to overwrite real Buzz.
+ if (val >= SAVE_MARKER && val <= SAVE_MARKER + 200L)
+ {
+ long loaded = Math.Max(TrendingManager.MIN_TREND_DAYS,
+ Math.Min(TrendingManager.MAX_TREND_DAYS, val - SAVE_MARKER - SAVE_OFFSET));
+ TrendingManager.trending = loaded < 0 && !Harmony.HasAnyPatches("com.tel.fanattrition") ? 0 : loaded;
+ return false;
+ }
+
+ // Backward compatibility with the old 10000 + trending marker, including negative trends.
+ // Legitimate game Buzz is capped far below this range.
+ if (val >= 9900L && val <= 10100L)
+ {
+ long loaded = Math.Max(TrendingManager.MIN_TREND_DAYS,
+ Math.Min(TrendingManager.MAX_TREND_DAYS, val - 10000L));
+ TrendingManager.trending = loaded < 0 && !Harmony.HasAnyPatches("com.tel.fanattrition") ? 0 : loaded;
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ [HarmonyPatch(typeof(resources), "LoadFunction")]
+ public class resources_LoadFunction
+ {
+ private static int loadDepth;
+ internal static bool Loading { get { return loadDepth > 0; } }
+
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix()
+ {
+ loadDepth++;
+ // Prevent a save without a marker from inheriting static trend state from a prior loaded game.
+ TrendingManager.trending = 0;
+ }
+
+ public static void Postfix()
+ {
+ TrendingManager.UpdateFanCount();
+ }
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ if (loadDepth > 0) loadDepth--;
+ return __exception;
+ }
+ }
+}
diff --git a/mods/Going Viral/TrendingManager.cs b/mods/Going Viral/TrendingManager.cs
index 2c30ed9..d2edde3 100755
--- a/mods/Going Viral/TrendingManager.cs
+++ b/mods/Going Viral/TrendingManager.cs
@@ -1,378 +1,329 @@
-using HarmonyLib;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using UnityEngine;
-
-namespace GoingViral
-{
- public class TrendingManager
- {
-
- public static long trending = 0;
-
- public static long adFans = 0;
- public static long dramaFans = 0;
- public static long tvFans = 0;
- public static long radioFans = 0;
- public static long netFans = 0;
- public static long cafeFans = 0;
-
-
- public static TrendingStatus IsTrending()
- {
- if (trending > 0)
- {
- return TrendingStatus.trending;
- }
- else if (trending < 0)
- {
- return TrendingStatus.crisis;
- }
- return TrendingStatus.none;
- }
-
- public static void SetTrending(long val)
- {
- if (IsTrending() != TrendingStatus.none || val == 0)
- {
- return;
- }
-
- int maxTrending = 91;
- int minTrending = -91;
- if (!Harmony.HasAnyPatches("com.tel.fanattrition"))
- {
- minTrending = 0;
- }
- trending = Math.Min(Math.Max(val, minTrending), maxTrending);
- if (trending > 0)
- {
- NotificationManager.AddNotification(
- Language.Insert("NOTIF__TRENDING", new string[] { Groups.GetMainGroup().Title }) + "\n" + Language.Insert("NOTIF__TRENDING_DAYS", new string[] { val.ToString() }),
- mainScript.green32,
- NotificationManager._notification._type.other);
- }
- else if (trending < 0)
- {
- if (Harmony.HasAnyPatches("com.tel.fanattrition"))
- {
- NotificationManager.AddNotification(
- Language.Insert("NOTIF__CRISIS", new string[] { Groups.GetMainGroup().Title }) + "\n" + Language.Insert("NOTIF__CRISIS_DAYS", new string[] { (-val).ToString() }),
- mainScript.red32,
- NotificationManager._notification._type.other);
- }
- }
- }
-
- public static float GetTrendingCoeff()
- {
- float coeff = 1;
-
- if (trending != 0)
- {
- coeff += (float)Math.Round((double)trending / 10 + 1, 1);
- }
- if (trending < 0)
- {
- coeff *= -1;
- }
-
- return coeff;
- }
-
- public static void UpdateFanCount()
- {
-
- adFans = 0;
- dramaFans = 0;
- tvFans = 0;
- radioFans = 0;
- netFans = 0;
- cafeFans = 0;
- foreach (business.active_proposal active_proposal in Camera.main.GetComponent().Data.GetComponent().ActiveProposals)
- {
- if (active_proposal.Fans_per_week > 0)
- {
- if (active_proposal.Type == business._type.ad)
- {
- adFans += active_proposal.Fans_per_week;
- }
- else if (active_proposal.Type == business._type.tv_drama)
- {
- dramaFans += active_proposal.Fans_per_week;
- }
- }
- }
-
- foreach (Shows._show show in Shows.shows)
- {
- if (show.status != Shows._show._status.normal && show.status != Shows._show._status.working && show.status != Shows._show._status.canceled)
- {
- if (show.medium.media_type == Shows._param._media_type.tv)
- {
- tvFans += show.fans[show.fans.Count - 1];
- }
- else if (show.medium.media_type == Shows._param._media_type.radio)
- {
- radioFans += show.fans[show.fans.Count - 1];
- }
- else if (show.medium.media_type == Shows._param._media_type.internet)
- {
- netFans += show.fans[show.fans.Count - 1];
- }
- }
- }
-
- foreach (Cafes._cafe cafe in Cafes.Cafes_)
- {
- int dayCount = 0;
- for (int i = cafe.Stats.Count - 1; i >= 0; i--)
- {
- cafeFans += cafe.Stats[i].New_Fans;
- dayCount++;
- if (dayCount >= 7)
- {
- break;
- }
- }
- }
- }
-
- public enum TrendingStatus
- {
- none,
- trending,
- crisis
- }
-
- public static float GetTrendingChance(float scandalPoints)
- {
- float p = 5;
- if (scandalPoints >= 10)
- {
- p *= 10;
- }
- return p;
- }
- public static long GetTrendingMagnitude(float scandalPoints)
- {
- long magnitude = -UnityEngine.Random.Range(2, 8) * 7;
- if (scandalPoints > 1)
- {
- magnitude = Mathf.RoundToInt(magnitude * 1.5f);
- }
- return magnitude;
- }
-
-
- public static float GetTrendingChance(Shows._show show)
- {
- float fameCoeff = 0;
- float fameSum = 0f;
- int count = 0;
- if (show.fame.Count > 0)
- {
- fameCoeff = show.fame[0] / 20;
- }
- if (show.castType == Shows._show._castType.entireGroup)
- {
- fameSum += (float)resources.GetFameLevel();
- count++;
- }
- else
- {
- foreach (data_girls.girls girls in show.girls)
- {
- if (girls != null)
- {
- fameSum += girls.GetFameLevel();
- count++;
- }
- }
- }
- if (count != 0)
- {
- fameCoeff += fameSum / count / 20;
- }
- if (show.mc != null)
- {
- fameCoeff += show.mc.fame / 20;
- }
- Shows._param genre = show.genre;
-
- float levelCoeff = genre.GetLevel() / 5 + 0.5f;
-
- DateTime? lastShowDate = null;
-
- // Get last show
- foreach (Shows._show _show in Shows.shows)
- {
- if (_show != show && _show.LaunchDate != null && _show.medium.media_type == Shows._param._media_type.tv && _show.genre.id == genre.id)
- {
- if (lastShowDate == null || _show.LaunchDate > (lastShowDate ?? staticVars.dateTime))
- {
- lastShowDate = _show.LaunchDate;
- }
- }
- }
-
- float daysSinceCoeff = 1;
- if (lastShowDate != null)
- {
- daysSinceCoeff = (staticVars.dateTime - (lastShowDate ?? staticVars.dateTime)).Days;
- daysSinceCoeff = Math.Min(365, daysSinceCoeff) / 365;
- }
- return 15f * daysSinceCoeff * fameCoeff * levelCoeff;
- }
- public static long GetTrendingMagnitude(Shows._show show)
- {
- return UnityEngine.Random.Range(31, 61);
- }
-
- public static float GetTrendingChance(singles._param marketing, Single_Marketing_Roll._result marketingResult, Groups._group group = null, float trendCoeff = 0)
- {
- float p = 0;
- if (marketing == null)
- {
- return p;
- }
-
- float saturationCoeff = 1;
- if(group != null)
- {
- saturationCoeff = singles.GetSaturationCoeff(group);
- }
- float modifiedTrendCoeff = (trendCoeff / 0.18f + 1) / 2;
- if (marketingResult == Single_Marketing_Roll._result.success_crit)
- {
- switch (marketing.Special_Type)
- {
- case singles._param._special_type.ad_campaign:
- p = 100 * modifiedTrendCoeff;
- break;
- case singles._param._special_type.viral_campaign:
- p = 70 * modifiedTrendCoeff;
- break;
- case singles._param._special_type.fake_scandal:
- p = 40 * modifiedTrendCoeff + 60;
- break;
- case singles._param._special_type.lewd_pv:
- case singles._param._special_type.edgy_pv:
- case singles._param._special_type.artsy_pv:
- //p = 50 * modifiedTrendCoeff;
- break;
- default:
- break;
- }
- p *= saturationCoeff;
- }
- else if (marketingResult == Single_Marketing_Roll._result.fail_crit)
- {
- switch (marketing.Special_Type)
- {
- case singles._param._special_type.ad_campaign:
- p = 50;
- break;
- case singles._param._special_type.viral_campaign:
- p = 33;
- break;
- case singles._param._special_type.fake_scandal:
- case singles._param._special_type.lewd_pv:
- case singles._param._special_type.edgy_pv:
- case singles._param._special_type.artsy_pv:
- //p = 50;
- break;
- default:
- break;
- }
- }
- return p;
- }
- public static long GetTrendingMagnitude(singles._param marketing, Single_Marketing_Roll._result marketingResult)
- {
- long magnitude = 0;
- if (marketing == null)
- {
- return magnitude;
- }
-
- if (marketingResult == Single_Marketing_Roll._result.success_crit)
- {
- switch (marketing.Special_Type)
- {
- case singles._param._special_type.ad_campaign:
- magnitude = UnityEngine.Random.Range(31, 61);
- break;
- case singles._param._special_type.viral_campaign:
- magnitude = UnityEngine.Random.Range(31, 91);
- break;
- case singles._param._special_type.lewd_pv:
- case singles._param._special_type.edgy_pv:
- case singles._param._special_type.artsy_pv:
- magnitude = UnityEngine.Random.Range(31, 61);
- break;
- case singles._param._special_type.fake_scandal:
- magnitude = UnityEngine.Random.Range(31, 91);
- break;
- default:
- break;
- }
- }
- else if (marketingResult == Single_Marketing_Roll._result.fail_crit)
- {
- switch (marketing.Special_Type)
- {
- case singles._param._special_type.ad_campaign:
- magnitude = -UnityEngine.Random.Range(15, 46);
- break;
- case singles._param._special_type.viral_campaign:
- magnitude = -UnityEngine.Random.Range(31, 61);
- break;
- case singles._param._special_type.lewd_pv:
- case singles._param._special_type.edgy_pv:
- case singles._param._special_type.artsy_pv:
- magnitude = -UnityEngine.Random.Range(15, 46);
- break;
- case singles._param._special_type.fake_scandal:
- default:
- break;
- }
- }
- return magnitude;
- }
-
-
- static int appealMultiplier = 3;
-
- public static float GetFanChurn(float appeal, float opinion, resources.fanType type)
- {
- float aversion = 1 / (appeal + opinion * appeal + 0.001f);
- float x = 1;
- if (type == resources.fanType.casual)
- {
- x = appealMultiplier * -Math.Min(-1, GetTrendingCoeff() * 2);
- }
- return aversion * x;
- }
- public static float GetFanAcquisition(data_girls.girls girl, float appeal, resources.fanType type)
- {
- float x = 1;
- float girlFame = girl.GetFameLevel();
- float groupFame = resources.GetFameLevel();
- float avgFame = (girlFame * 2 + groupFame) / 3 / 10;
- LinearFunction._function getCoeffFromFame = new();
- getCoeffFromFame.Init(3f, -appealMultiplier, 7f, appealMultiplier);
- float fameCoeff = Math.Min(appealMultiplier, Math.Max(-appealMultiplier, getCoeffFromFame.GetY(avgFame)));
-
- if (type == resources.fanType.casual)
- {
- x = fameCoeff * Math.Max(1, GetTrendingCoeff() * 2);
- }
- return appeal * x;
- }
- }
-
-}
+using HarmonyLib;
+using System;
+using UnityEngine;
+
+namespace GoingViral
+{
+ public class TrendingManager
+ {
+ public const long MAX_TREND_DAYS = 90;
+ public const long MIN_TREND_DAYS = -90;
+
+ public static long trending = 0;
+ public static long adFans = 0;
+ public static long dramaFans = 0;
+ public static long tvFans = 0;
+ public static long radioFans = 0;
+ public static long netFans = 0;
+ public static long cafeFans = 0;
+
+ public enum TrendingStatus
+ {
+ none,
+ trending,
+ crisis
+ }
+
+ public static TrendingStatus IsTrending()
+ {
+ if (trending > 0) return TrendingStatus.trending;
+ if (trending < 0) return TrendingStatus.crisis;
+ return TrendingStatus.none;
+ }
+
+ public static void SetTrending(long val)
+ {
+ if (IsTrending() != TrendingStatus.none || val == 0)
+ return;
+
+ long min = Harmony.HasAnyPatches("com.tel.fanattrition") ? MIN_TREND_DAYS : 0;
+ trending = Math.Max(min, Math.Min(MAX_TREND_DAYS, val));
+ if (trending == 0)
+ return;
+
+ Groups._group group = Groups.GetMainGroup();
+ string groupTitle = group != null ? group.Title : "";
+ if (trending > 0)
+ {
+ NotificationManager.AddNotification(
+ Language.Insert("NOTIF__TRENDING", new string[] { groupTitle }) + "\n" +
+ Language.Insert("NOTIF__TRENDING_DAYS", new string[] { trending.ToString() }),
+ mainScript.green32,
+ NotificationManager._notification._type.other);
+ }
+ else
+ {
+ NotificationManager.AddNotification(
+ Language.Insert("NOTIF__CRISIS", new string[] { groupTitle }) + "\n" +
+ Language.Insert("NOTIF__CRISIS_DAYS", new string[] { Math.Abs(trending).ToString() }),
+ mainScript.red32,
+ NotificationManager._notification._type.other);
+ }
+ }
+
+ // Positive trends multiply gains; negative trends multiply churn by the same magnitude.
+ // Keep the original positive curve (day 1 ~= 2.1x, day 90 = 11x) without the old sign flip.
+ public static float GetTrendingCoeff()
+ {
+ if (trending == 0)
+ return 1f;
+ return (float)Math.Round(2d + Math.Abs((double)trending) / 10d, 1);
+ }
+
+ public static void UpdateFanCount()
+ {
+ adFans = dramaFans = tvFans = radioFans = netFans = cafeFans = 0;
+
+ try
+ {
+ mainScript main = Camera.main != null ? Camera.main.GetComponent() : null;
+ business businessData = main != null && main.Data != null ? main.Data.GetComponent() : null;
+ if (businessData != null && businessData.ActiveProposals != null)
+ {
+ foreach (business.active_proposal proposal in businessData.ActiveProposals)
+ {
+ if (proposal == null || proposal.Fans_per_week <= 0)
+ continue;
+ if (proposal.Type == business._type.ad)
+ adFans = SafeAdd(adFans, proposal.Fans_per_week);
+ else if (proposal.Type == business._type.tv_drama)
+ dramaFans = SafeAdd(dramaFans, proposal.Fans_per_week);
+ }
+ }
+ }
+ catch { }
+
+ if (Shows.shows != null)
+ {
+ foreach (Shows._show show in Shows.shows)
+ {
+ if (show == null || show.medium == null || show.fans == null || show.fans.Count == 0)
+ continue;
+ if (show.status == Shows._show._status.normal || show.status == Shows._show._status.working || show.status == Shows._show._status.canceled)
+ continue;
+
+ long latest = show.fans[show.fans.Count - 1];
+ if (show.medium.media_type == Shows._param._media_type.tv)
+ tvFans = SafeAdd(tvFans, latest);
+ else if (show.medium.media_type == Shows._param._media_type.radio)
+ radioFans = SafeAdd(radioFans, latest);
+ else if (show.medium.media_type == Shows._param._media_type.internet)
+ netFans = SafeAdd(netFans, latest);
+ }
+ }
+
+ if (Cafes.Cafes_ != null)
+ {
+ foreach (Cafes._cafe cafe in Cafes.Cafes_)
+ {
+ if (cafe == null || cafe.Stats == null)
+ continue;
+ int start = Math.Max(0, cafe.Stats.Count - 7);
+ for (int i = start; i < cafe.Stats.Count; i++)
+ {
+ if (cafe.Stats[i] != null)
+ cafeFans = SafeAdd(cafeFans, cafe.Stats[i].New_Fans);
+ }
+ }
+ }
+ }
+
+ public static float GetTrendingChance(float scandalPoints)
+ {
+ float p = scandalPoints >= 10f ? 50f : 5f;
+ return Mathf.Clamp(p, 0f, 100f);
+ }
+
+ public static long GetTrendingMagnitude(float scandalPoints)
+ {
+ long magnitude = -UnityEngine.Random.Range(2, 8) * 7L;
+ if (scandalPoints > 1f)
+ magnitude = Mathf.RoundToInt(magnitude * 1.5f);
+ return Math.Max(MIN_TREND_DAYS, magnitude);
+ }
+
+ public static float GetTrendingChance(Shows._show show)
+ {
+ if (show == null || show.genre == null || show.medium == null || show.medium.media_type != Shows._param._media_type.tv)
+ return 0f;
+
+ // Normalize cast fame from vanilla's 0-10 fame scale to 0-1.
+ float castCoeff = 0f;
+ if (show.castType == Shows._show._castType.entireGroup)
+ {
+ castCoeff = Mathf.Clamp01(resources.GetFameLevel() / 10f);
+ }
+ else if (show.girls != null)
+ {
+ float fameSum = 0f;
+ int count = 0;
+ foreach (data_girls.girls girl in show.girls)
+ {
+ if (girl == null)
+ continue;
+
+ fameSum += girl.GetFameLevel();
+ count++;
+ }
+
+ if (count > 0)
+ castCoeff = Mathf.Clamp01((fameSum / count) / 10f);
+ }
+
+ // Normalize genre level from the vanilla 0-10 level scale to 0-1.
+ float genreCoeff = Mathf.Clamp01(show.genre.GetLevel() / 10f);
+
+ // Combine the applicable quality factors instead of multiplying them.
+ // This mirrors Going Viral's single-trend design: build one normalized
+ // quality score first, then apply the maximum trend chance.
+ float qualityTotal = castCoeff + genreCoeff;
+ int qualityFactors = 2;
+
+ // Only count MC fame when the show actually has an MC. A show without
+ // an MC therefore isn't automatically penalized by a zero-valued factor.
+ if (show.mc != null)
+ {
+ float mcCoeff = Mathf.Clamp01(show.mc.fame / 10f);
+ qualityTotal += mcCoeff;
+ qualityFactors++;
+ }
+
+ float qualityCoeff = qualityTotal / qualityFactors;
+
+ DateTime? lastShowDate = null;
+ if (Shows.shows != null)
+ {
+ foreach (Shows._show other in Shows.shows)
+ {
+ if (other == null || other == show || other.medium == null || other.genre == null)
+ continue;
+ if (other.LaunchDate == default(DateTime) || other.medium.media_type != Shows._param._media_type.tv || other.genre.id != show.genre.id)
+ continue;
+ if (!lastShowDate.HasValue || other.LaunchDate > lastShowDate.Value)
+ lastShowDate = other.LaunchDate;
+ }
+ }
+
+ // No previous TV show of this genre means maximum freshness.
+ float daysSinceCoeff = 1f;
+ if (lastShowDate.HasValue)
+ {
+ int days = Math.Max(0, (staticVars.dateTime - lastShowDate.Value).Days);
+ daysSinceCoeff = Math.Min(365, days) / 365f;
+ }
+
+ // Both coefficients are bounded to 0-1, so 15f is now a true maximum
+ // rather than a base multiplier that can balloon above 15%.
+ return Mathf.Clamp(15f * daysSinceCoeff * qualityCoeff, 0f, 15f);
+ }
+
+ public static long GetTrendingMagnitude(Shows._show show)
+ {
+ return UnityEngine.Random.Range(31, 61);
+ }
+
+ public static float GetTrendingChance(singles._param marketing, Single_Marketing_Roll._result marketingResult, Groups._group group = null, float trendCoeff = 0f)
+ {
+ if (marketing == null)
+ return 0f;
+
+ float p = 0f;
+ float saturationCoeff = group != null ? singles.GetSaturationCoeff(group) : 1f;
+ float modifiedTrendCoeff = (trendCoeff / 0.18f + 1f) / 2f;
+
+ if (marketingResult == Single_Marketing_Roll._result.success_crit)
+ {
+ switch (marketing.Special_Type)
+ {
+ case singles._param._special_type.ad_campaign: p = 100f * modifiedTrendCoeff; break;
+ case singles._param._special_type.viral_campaign: p = 70f * modifiedTrendCoeff; break;
+ case singles._param._special_type.fake_scandal: p = 40f * modifiedTrendCoeff + 60f; break;
+ }
+ p *= saturationCoeff;
+ }
+ else if (marketingResult == Single_Marketing_Roll._result.fail_crit)
+ {
+ switch (marketing.Special_Type)
+ {
+ case singles._param._special_type.ad_campaign: p = 50f; break;
+ case singles._param._special_type.viral_campaign: p = 33f; break;
+ }
+ }
+ return Mathf.Clamp(p, 0f, 100f);
+ }
+
+ public static long GetTrendingMagnitude(singles._param marketing, Single_Marketing_Roll._result marketingResult)
+ {
+ if (marketing == null)
+ return 0;
+
+ if (marketingResult == Single_Marketing_Roll._result.success_crit)
+ {
+ switch (marketing.Special_Type)
+ {
+ case singles._param._special_type.ad_campaign: return UnityEngine.Random.Range(31, 61);
+ case singles._param._special_type.viral_campaign: return UnityEngine.Random.Range(31, 91);
+ case singles._param._special_type.fake_scandal: return UnityEngine.Random.Range(31, 91);
+ case singles._param._special_type.lewd_pv:
+ case singles._param._special_type.edgy_pv:
+ case singles._param._special_type.artsy_pv: return UnityEngine.Random.Range(31, 61);
+ }
+ }
+ else if (marketingResult == Single_Marketing_Roll._result.fail_crit)
+ {
+ switch (marketing.Special_Type)
+ {
+ case singles._param._special_type.ad_campaign: return -UnityEngine.Random.Range(15, 46);
+ case singles._param._special_type.viral_campaign: return -UnityEngine.Random.Range(31, 61);
+ case singles._param._special_type.lewd_pv:
+ case singles._param._special_type.edgy_pv:
+ case singles._param._special_type.artsy_pv: return -UnityEngine.Random.Range(15, 46);
+ }
+ }
+ return 0;
+ }
+
+ private const int appealMultiplier = 3;
+
+ public static float GetFanChurn(float appeal, float opinion, resources.fanType type)
+ {
+ float safeAppeal = Math.Max(0f, appeal);
+ float safeOpinion = Mathf.Clamp01(opinion);
+ float aversion = 1f / (safeAppeal * (1f + safeOpinion) + 0.001f);
+ return aversion * (type == resources.fanType.casual ? appealMultiplier : 1f);
+ }
+
+ public static float GetFanAcquisition(data_girls.girls girl, float appeal, resources.fanType type)
+ {
+ float weight = Math.Max(0f, appeal);
+ if (type == resources.fanType.casual)
+ weight *= appealMultiplier;
+ return weight;
+ }
+
+ public static int ScaleInt(int value, float coeff)
+ {
+ long scaled = ScaleLong(value, coeff);
+ if (scaled > int.MaxValue) return int.MaxValue;
+ if (scaled < int.MinValue) return int.MinValue;
+ return (int)scaled;
+ }
+
+ public static long ScaleLong(long value, float coeff)
+ {
+ if (float.IsNaN(coeff) || float.IsInfinity(coeff))
+ return value;
+ double scaled = Math.Round(value * (double)coeff, MidpointRounding.AwayFromZero);
+ if (scaled >= long.MaxValue) return long.MaxValue;
+ if (scaled <= long.MinValue) return long.MinValue;
+ return (long)scaled;
+ }
+
+ public static long SafeAdd(long a, long b)
+ {
+ if (b > 0 && a > long.MaxValue - b) return long.MaxValue;
+ if (b < 0 && a < long.MinValue - b) return long.MinValue;
+ return a + b;
+ }
+ }
+}
diff --git a/mods/Going Viral/TriggerTrending.cs b/mods/Going Viral/TriggerTrending.cs
index fd37897..30911a1 100755
--- a/mods/Going Viral/TriggerTrending.cs
+++ b/mods/Going Viral/TriggerTrending.cs
@@ -1,164 +1,143 @@
-using HarmonyLib;
-using System;
-using TMPro;
-using UnityEngine;
-using static GoingViral.TrendingManager;
-
-namespace GoingViral
-{
- // display single triggering neg trending
- [HarmonyPatch(typeof(Single_Marketing_Roll), "OnFail")]
- public class Single_Marketing_Roll_OnFail
- {
- public static bool Prefix(ref TrendingManager.TrendingStatus __state)
- {
- __state = TrendingManager.IsTrending();
- return true;
- }
- public static void Postfix(ref Single_Marketing_Roll __instance, TrendingManager.TrendingStatus __state)
- {
- if (__state != TrendingStatus.none)
- {
- return;
- }
-
- singles._single single = Traverse.Create(__instance).Field("Single").GetValue() as singles._single;
- if (TrendingManager.IsTrending() == TrendingManager.TrendingStatus.crisis)
- {
- __instance.Description.GetComponent().text += "\n" + Language.Insert("NOTIF__CRISIS", single.GetGroup().Title);
- return;
- }
- }
- }
- // display single triggering neg trending
- [HarmonyPatch(typeof(Single_Marketing_Roll), "OnFailCrit")]
- public class Single_Marketing_Roll_OnFailCrit
- {
- public static bool Prefix(ref TrendingManager.TrendingStatus __state)
- {
- __state = TrendingManager.IsTrending();
- return true;
- }
- public static void Postfix(ref Single_Marketing_Roll __instance, TrendingManager.TrendingStatus __state)
- {
- if (__state != TrendingStatus.none)
- {
- return;
- }
-
- singles._single single = Traverse.Create(__instance).Field("Single").GetValue() as singles._single;
- if (TrendingManager.IsTrending() == TrendingManager.TrendingStatus.crisis)
- {
- __instance.Description.GetComponent().text += "\n" + Language.Insert("NOTIF__CRISIS", single.GetGroup().Title);
- return;
- }
-
- singles._param marketing = single.GetRiskyMarketing();
-
- float coeff = Rivals.GetSinglesCoeff(single);
-
- float p = TrendingManager.GetTrendingChance(marketing, single.Marketing_Result_Status, single.GetGroup(), coeff);
- if (mainScript.chance(p))
- {
- SetTrending(TrendingManager.GetTrendingMagnitude(marketing, single.Marketing_Result_Status));
- __instance.Description.GetComponent().text += "\n" + Language.Insert("NOTIF__CRISIS", single.GetGroup().Title);
- }
- }
- }
- // display single triggering trending
- [HarmonyPatch(typeof(Single_Marketing_Roll), "OnSuccessCrit")]
- public class Single_Marketing_Roll_OnSuccessCrit
- {
- public static bool Prefix(ref TrendingManager.TrendingStatus __state)
- {
- __state = TrendingManager.IsTrending();
- return true;
- }
- public static void Postfix(ref Single_Marketing_Roll __instance, TrendingManager.TrendingStatus __state)
- {
- if (__state != TrendingStatus.none)
- {
- return;
- }
-
- singles._single single = Traverse.Create(__instance).Field("Single").GetValue() as singles._single;
- singles._param marketing = single.GetRiskyMarketing();
-
- float coeff = Rivals.GetSinglesCoeff(single);
-
- float p = TrendingManager.GetTrendingChance(marketing, single.Marketing_Result_Status, single.GetGroup(), coeff);
- if (mainScript.chance(p))
- {
- SetTrending(TrendingManager.GetTrendingMagnitude(marketing, single.Marketing_Result_Status));
- __instance.Description.GetComponent().text += "\n" + Language.Insert("NOTIF__TRENDING", single.GetGroup().Title);
- }
- }
- }
-
-
-
- // chance of trending on crit success after 1 week
- [HarmonyPatch(typeof(Shows._show), "NewEpisode")]
- public class Shows__show_NewEpisode
- {
- public static void Postfix(Shows._show __instance)
- {
- if (IsTrending() != TrendingStatus.none || __instance.episodeCount != 2 || __instance.medium.media_type != Shows._param._media_type.tv)
- {
- return;
- }
-
- float p = TrendingManager.GetTrendingChance(__instance);
- if (mainScript.chance(p))
- {
- SetTrending(TrendingManager.GetTrendingMagnitude(__instance));
- }
- }
- }
-
-
- // chance of crisis upon scandal points
- [HarmonyPatch(typeof(data_girls.girls), "addParam")]
- public class data_girls_girls_addParam
- {
- public static void Postfix(data_girls._paramType type, float val)
- {
- if (IsTrending() != TrendingStatus.none)
- {
- return;
- }
- if (type != data_girls._paramType.scandalPoints || val <= 0)
- {
- return;
- }
- float p = TrendingManager.GetTrendingChance(val);
- if (mainScript.chance(p))
- {
- SetTrending(TrendingManager.GetTrendingMagnitude(val));
- }
- }
- }
-
- // chance of crisis upon scandal points
- [HarmonyPatch(typeof(resources), "_Add")]
- public class resources__Add
- {
- public static void Postfix(resources.type _type, long val)
- {
- if (IsTrending() != TrendingStatus.none)
- {
- return;
- }
- if (_type != resources.type.scandalPoints || val <= 0)
- {
- return;
- }
- float p = TrendingManager.GetTrendingChance(val);
- if (mainScript.chance(p))
- {
- SetTrending(TrendingManager.GetTrendingMagnitude(val));
- }
- }
- }
-
-}
+using HarmonyLib;
+using TMPro;
+using UnityEngine;
+using static GoingViral.TrendingManager;
+
+namespace GoingViral
+{
+ internal static class TrendingRollUI
+ {
+ internal static singles._single GetSingle(Single_Marketing_Roll roll)
+ {
+ return roll == null ? null : Traverse.Create(roll).Field("Single").GetValue() as singles._single;
+ }
+
+ internal static void Append(Single_Marketing_Roll roll, string key, singles._single single)
+ {
+ if (roll == null || roll.Description == null || single == null)
+ return;
+ TextMeshProUGUI text = roll.Description.GetComponent();
+ Groups._group group = single.GetGroup();
+ if (text == null || group == null)
+ return;
+ text.text += "\n" + Language.Insert(key, new string[] { group.Title });
+ }
+ }
+
+ [HarmonyPatch(typeof(Single_Marketing_Roll), "OnFail")]
+ public class Single_Marketing_Roll_OnFail
+ {
+ public static void Prefix(ref TrendingStatus __state)
+ {
+ __state = IsTrending();
+ }
+
+ public static void Postfix(Single_Marketing_Roll __instance, TrendingStatus __state)
+ {
+ if (__state == TrendingStatus.none && IsTrending() == TrendingStatus.crisis)
+ TrendingRollUI.Append(__instance, "NOTIF__CRISIS", TrendingRollUI.GetSingle(__instance));
+ }
+ }
+
+ [HarmonyPatch(typeof(Single_Marketing_Roll), "OnFailCrit")]
+ public class Single_Marketing_Roll_OnFailCrit
+ {
+ public static void Prefix(ref TrendingStatus __state)
+ {
+ __state = IsTrending();
+ }
+
+ public static void Postfix(Single_Marketing_Roll __instance, TrendingStatus __state)
+ {
+ if (__state != TrendingStatus.none)
+ return;
+
+ singles._single single = TrendingRollUI.GetSingle(__instance);
+ if (single == null)
+ return;
+
+ // A fake-scandal resource change may already have started the crisis.
+ if (IsTrending() == TrendingStatus.crisis)
+ {
+ TrendingRollUI.Append(__instance, "NOTIF__CRISIS", single);
+ return;
+ }
+
+ singles._param marketing = single.GetRiskyMarketing();
+ Groups._group group = single.GetGroup();
+ float coeff = Rivals.GetSinglesCoeff(single);
+ float chance = GetTrendingChance(marketing, single.Marketing_Result_Status, group, coeff);
+ if (!mainScript.chance(chance))
+ return;
+
+ SetTrending(GetTrendingMagnitude(marketing, single.Marketing_Result_Status));
+ if (IsTrending() == TrendingStatus.crisis)
+ TrendingRollUI.Append(__instance, "NOTIF__CRISIS", single);
+ }
+ }
+
+ [HarmonyPatch(typeof(Single_Marketing_Roll), "OnSuccessCrit")]
+ public class Single_Marketing_Roll_OnSuccessCrit
+ {
+ public static void Prefix(ref TrendingStatus __state)
+ {
+ __state = IsTrending();
+ }
+
+ public static void Postfix(Single_Marketing_Roll __instance, TrendingStatus __state)
+ {
+ if (__state != TrendingStatus.none)
+ return;
+
+ singles._single single = TrendingRollUI.GetSingle(__instance);
+ if (single == null)
+ return;
+ singles._param marketing = single.GetRiskyMarketing();
+ Groups._group group = single.GetGroup();
+ float coeff = Rivals.GetSinglesCoeff(single);
+ float chance = GetTrendingChance(marketing, single.Marketing_Result_Status, group, coeff);
+ if (!mainScript.chance(chance))
+ return;
+
+ SetTrending(GetTrendingMagnitude(marketing, single.Marketing_Result_Status));
+ if (IsTrending() == TrendingStatus.trending)
+ TrendingRollUI.Append(__instance, "NOTIF__TRENDING", single);
+ }
+ }
+
+ [HarmonyPatch(typeof(Shows._show), "NewEpisode")]
+ public class Shows__show_NewEpisode
+ {
+ public static void Postfix(Shows._show __instance)
+ {
+ if (__instance == null || __instance.medium == null || IsTrending() != TrendingStatus.none ||
+ __instance.episodeCount != 2 || __instance.medium.media_type != Shows._param._media_type.tv)
+ return;
+
+ if (mainScript.chance(GetTrendingChance(__instance)))
+ SetTrending(GetTrendingMagnitude(__instance));
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls.girls), "addParam")]
+ public class data_girls_girls_addParam
+ {
+ public static void Postfix(data_girls._paramType type, float val)
+ {
+ if (IsTrending() == TrendingStatus.none && type == data_girls._paramType.scandalPoints && val > 0f &&
+ mainScript.chance(GetTrendingChance(val)))
+ SetTrending(GetTrendingMagnitude(val));
+ }
+ }
+
+ [HarmonyPatch(typeof(resources), "_Add")]
+ public class resources__Add
+ {
+ public static void Postfix(resources.type _type, long val)
+ {
+ if (IsTrending() == TrendingStatus.none && _type == resources.type.scandalPoints && val > 0 &&
+ mainScript.chance(GetTrendingChance(val)))
+ SetTrending(GetTrendingMagnitude(val));
+ }
+ }
+}
diff --git a/mods/Going Viral/assets/JSON/Constants/constants.json b/mods/Going Viral/assets/JSON/Constants/constants.json
index 18d6235..33f03d6 100755
--- a/mods/Going Viral/assets/JSON/Constants/constants.json
+++ b/mods/Going Viral/assets/JSON/Constants/constants.json
@@ -1,78 +1,98 @@
-[
- {
- id: "CHURN",
- text: "Churn Rate"
- },{
- id: "TIP__OF_TOTAL",
- text: "of Total"
- },{
- id: "TIP__AD",
- text: "Ad Contracts"
- },{
- id: "TIP__DRAMA",
- text: "Drama Contracts"
- },{
- id: "TIP__TV",
- text: "TV Shows"
- },{
- id: "TIP__INTERNET",
- text: "Internet Shows"
- },{
- id: "TIP__RADIO",
- text: "Radio Shows"
- },{
- id: "TIP__RADIO",
- text: "Radio Shows"
- },{
- id: "TIP__TRENDING",
- text: "Trending Multiplier"
- },{
- id: "TIP__NONE",
- text: "None"
- },{
- id: "TIP__DAYS_LEFT",
- text: "days left"
- },{
- id: "TIP__DAY_LEFT",
- text: "day left"
- },{
- id: "NOTIF__TRENDING",
- text: "@ is going viral on social media!"
- },{
- id: "NOTIF__TRENDING_DAYS",
- text: "@ days of bonus fans from singles, shows, businesses and tours"
- },{
- id: "NOTIF__CRISIS",
- text: "@ is getting negative press!"
- },{
- id: "NOTIF__CRISIS_DAYS",
- text: "@ days of penalty to churn rate"
- },{
- id: "TREND__GENRE",
- text: "Last TV show: "
- },{
- id: "TREND__GENRE_NEVER",
- text: "Last TV show: {green}N/A{/color}"
- },{
- id: "TREND__MEDIUM",
- text: "Up to 15% chance of trending based on days since your last TV show, cast fame, mc fame and genre level"
- },{
- id: "TREND__SINGLE_SUCCESS",
- text: "Chance to go viral: {green}@%{/color}"
- },{
- id: "TREND__SINGLE_FAIL",
- text: "Chance for bad press: {red}@%{/color}"
- },{
- id: "TREND__SINGLE_DESC",
- text: "Can change depending on the single's genre, lyrics and choreography trend rankings."
- },{
- id: "ONE_DAY_AGO",
- text: "1 day ago"
- },{
- id: "THEATER__FANS_LIKE",
- text: "@1\nfans like @2's theater schedule"
- },{
- id: "THEATER__FANS_DISLIKE",
- text: "@1\nfans dislike @2's theater schedule"
- }
+[
+ {
+ "id": "CHURN",
+ "text": "Churn Rate"
+ },
+ {
+ "id": "TIP__OF_TOTAL",
+ "text": "of Total"
+ },
+ {
+ "id": "TIP__AD",
+ "text": "Ad Contracts"
+ },
+ {
+ "id": "TIP__DRAMA",
+ "text": "Drama Contracts"
+ },
+ {
+ "id": "TIP__TV",
+ "text": "TV Shows"
+ },
+ {
+ "id": "TIP__INTERNET",
+ "text": "Internet Shows"
+ },
+ {
+ "id": "TIP__RADIO",
+ "text": "Radio Shows"
+ },
+ {
+ "id": "TIP__TRENDING",
+ "text": "Trending Multiplier"
+ },
+ {
+ "id": "TIP__NONE",
+ "text": "None"
+ },
+ {
+ "id": "TIP__DAYS_LEFT",
+ "text": "days left"
+ },
+ {
+ "id": "TIP__DAY_LEFT",
+ "text": "day left"
+ },
+ {
+ "id": "NOTIF__TRENDING",
+ "text": "@ is going viral on social media!"
+ },
+ {
+ "id": "NOTIF__TRENDING_DAYS",
+ "text": "@ days of bonus fans from singles, shows, businesses and tours"
+ },
+ {
+ "id": "NOTIF__CRISIS",
+ "text": "@ is getting negative press!"
+ },
+ {
+ "id": "NOTIF__CRISIS_DAYS",
+ "text": "@ days of penalty to churn rate"
+ },
+ {
+ "id": "TREND__GENRE",
+ "text": "Last TV show: "
+ },
+ {
+ "id": "TREND__GENRE_NEVER",
+ "text": "Last TV show: {green}N/A{/color}"
+ },
+ {
+ "id": "TREND__MEDIUM",
+ "text": "Up to 15% chance of trending based on days since your last TV show, cast fame, mc fame and genre level"
+ },
+ {
+ "id": "TREND__SINGLE_SUCCESS",
+ "text": "Chance to go viral: {green}@%{/color}"
+ },
+ {
+ "id": "TREND__SINGLE_FAIL",
+ "text": "Chance for bad press: {red}@%{/color}"
+ },
+ {
+ "id": "TREND__SINGLE_DESC",
+ "text": "Can change depending on the single's genre, lyrics and choreography trend rankings."
+ },
+ {
+ "id": "ONE_DAY_AGO",
+ "text": "1 day ago"
+ },
+ {
+ "id": "THEATER__FANS_LIKE",
+ "text": "@1\nfans like @2's theater schedule"
+ },
+ {
+ "id": "THEATER__FANS_DISLIKE",
+ "text": "@1\nfans dislike @2's theater schedule"
+ }
]
\ No newline at end of file
diff --git a/mods/Growing Distant/Growing Distant.cs b/mods/Growing Distant/Growing Distant.cs
index 2f5310b..b579949 100755
--- a/mods/Growing Distant/Growing Distant.cs
+++ b/mods/Growing Distant/Growing Distant.cs
@@ -19,6 +19,7 @@ public class data_girls_girls_UpdateRelationshipBasedOnSalary
public const int SALARY_LOWER_THR = 50;
public const int ROMANCE_PENALTY = -1;
public const int FRIEND_PENALTY = -2;
+ public const int RELATIONSHIP_MAX_POINTS = 512;
///
/// Harmony patch for the UpdateRelationshipBasedOnSalary method in data_girls.girls.
@@ -31,7 +32,7 @@ public static void Postfix(ref data_girls.girls __instance)
int salarySatisfaction_Percentage = __instance.GetSalarySatisfaction_Percentage();
if (salarySatisfaction_Percentage >= SALARY_UPPER_THR)
{
- __instance.Rel_Influence_Points += INFLUENCE_BONUS;
+ __instance.Rel_Influence_Points = Math.Min(RELATIONSHIP_MAX_POINTS, __instance.Rel_Influence_Points + INFLUENCE_BONUS);
}
else if (salarySatisfaction_Percentage <= SALARY_LOWER_THR && __instance.Rel_Influence_Points >= -INFLUENCE_PENALTYCRIT)
{
diff --git a/mods/Harmony Checker/Harmony Checker.cs b/mods/Harmony Checker/Harmony Checker.cs
index bbbeff9..13d03a0 100755
--- a/mods/Harmony Checker/Harmony Checker.cs
+++ b/mods/Harmony Checker/Harmony Checker.cs
@@ -1,18 +1,76 @@
-using HarmonyLib;
-using UnityEngine;
-
-namespace HarmonyChecker
-{
- // change button text
- [HarmonyPatch(typeof(MainMenu_Buttons_Controller), "Start")]
- public class MainMenu_Buttons_Controller_Start
- {
- public const string BUTTON_LABEL = "IMHI_INSTALLED";
-
- public static void Postfix(ref MainMenu_Buttons_Controller __instance)
- {
- Lang_Button modButton = __instance.Main_Container.transform.Find("Mods").GetComponentInChildren();
- modButton.Constant = BUTTON_LABEL;
- }
- }
-}
+using HarmonyLib;
+using UnityEngine;
+
+namespace HarmonyChecker
+{
+ internal static class HarmonyCheckerStatus
+ {
+ public const string BUTTON_LABEL = "IMHI_INSTALLED";
+
+ public static void MarkInstalled(MainMenu_Buttons_Controller controller = null)
+ {
+ if (controller == null)
+ {
+ Camera camera = Camera.main;
+ if (camera == null)
+ {
+ return;
+ }
+
+ mainScript main = camera.GetComponent();
+ if (main == null || main.Data == null)
+ {
+ return;
+ }
+
+ controller = main.Data.GetComponent();
+ }
+
+ if (controller == null || controller.Main_Container == null)
+ {
+ return;
+ }
+
+ Transform modsTransform = controller.Main_Container.transform.Find("Mods");
+ if (modsTransform == null)
+ {
+ return;
+ }
+
+ Lang_Button modButton = modsTransform.GetComponentInChildren();
+ if (modButton == null)
+ {
+ return;
+ }
+
+ modButton.Constant = BUTTON_LABEL;
+
+ // MainMenu_Buttons_Controller.Start may already have run by the time IM-HI
+ // finishes loading Workshop Harmony DLLs. Updating Constant alone does not
+ // redraw an already-started Lang_Button, so force the text refresh now.
+ modButton.ResetText();
+ }
+ }
+
+ // Keep the original early path for cases where Harmony mods load before this Start.
+ [HarmonyPatch(typeof(MainMenu_Buttons_Controller), "Start")]
+ public class MainMenu_Buttons_Controller_Start
+ {
+ public static void Postfix(ref MainMenu_Buttons_Controller __instance)
+ {
+ HarmonyCheckerStatus.MarkInstalled(__instance);
+ }
+ }
+
+ // On Steam, Mods.LoadMods() can finish after MainMenu_Buttons_Controller.Start().
+ // IM-HI patches Harmony Checker during Mods.LoadMods(); StopSpinner is called later
+ // by the same loading coroutine, making this a reliable post-load refresh point.
+ [HarmonyPatch(typeof(Mods), "StopSpinner")]
+ public class Mods_StopSpinner
+ {
+ public static void Postfix()
+ {
+ HarmonyCheckerStatus.MarkInstalled();
+ }
+ }
+}
diff --git a/mods/Harmony Checker/Harmony Checker.csproj b/mods/Harmony Checker/Harmony Checker.csproj
index 3006804..97d26d0 100755
--- a/mods/Harmony Checker/Harmony Checker.csproj
+++ b/mods/Harmony Checker/Harmony Checker.csproj
@@ -1,25 +1,20 @@
-
-
-
- IM-HarmonyIntegration Plugin
- com.tel.harmonychecker
- This mod checks if you have IM-HarmonyIntegration (IM-HI) installed. If it is not installed, please visit the IM-HarmonyIntegration github page for instructions.
- Tel, Shelon
- 1.0.0
- ["example", "patches"]
-
-
- 0
-
- $(HarmonyID)
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
-
+
+
+ IM-HarmonyIntegration Plugin
+ com.tel.harmonychecker
+ This mod checks if you have IM-HarmonyIntegration (IM-HI) installed. If it is not installed, please visit the IM-HarmonyIntegration github page for instructions.
+ Tel, Shelon
+ 1.0.1
+ ["example", "patches"]
+ 0
+ $(HarmonyID)
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/mods/MBTI Personalities/MBTI Personalities.cs b/mods/MBTI Personalities/MBTI Personalities.cs
index ce0ce39..e90f20b 100755
--- a/mods/MBTI Personalities/MBTI Personalities.cs
+++ b/mods/MBTI Personalities/MBTI Personalities.cs
@@ -166,7 +166,7 @@ public static void Postfix(ref int __result)
mBTI = GetGirlMBTI(_girls);
if(mBTI == MBTI.ISTP)
{
- __result = Mathf.RoundToInt(ISTPBonus + __result / 100 * (1 - ISTPBonus));
+ __result = Mathf.RoundToInt(__result + (100 - __result) * ISTPBonus);
return;
}
}
diff --git a/mods/ModMenus/ModMenus.cs b/mods/ModMenus/ModMenus.cs
index deb17c9..5867903 100755
--- a/mods/ModMenus/ModMenus.cs
+++ b/mods/ModMenus/ModMenus.cs
@@ -13,39 +13,26 @@
namespace ModMenus
{
///
- /// Ensures the mod menu popup is available when the game starts.
+ /// Lazily installs ModMenus only when the player opens the in-game Settings tab.
+ /// This intentionally does no work on the main menu or while a game is loading.
///
- [HarmonyPatch(typeof(PopupManager), "Start")]
- public class PopupManager_Start
+ [HarmonyPatch(typeof(Tabs_Manager), nameof(Tabs_Manager.OpenTab))]
+ public class Tabs_Manager_OpenTab
{
- ///
- /// Postfix method that generates the mod menu popup.
- ///
- public static void Postfix()
+ public static void Postfix(Tabs_Manager._tab._type __0)
{
- GenerateMenuPopup();
- }
- }
+ if (__0 != Tabs_Manager._tab._type.settings || !ModMenusUtils.IsGameplayReady())
+ {
+ return;
+ }
- ///
- /// Integrates the mod menu access point into the game's existing UI.
- ///
- [HarmonyPatch(typeof(Tabs_Manager), "Awake")]
- public class Tabs_Manager_Awake
- {
- ///
- /// Postfix method that adds a mod menu button to the settings panel.
- ///
- public static void Postfix()
- {
- Transform settingsPanelTransform = Camera.main.GetComponent().Data.GetComponent().GetTab(Tabs_Manager._tab._type.settings).Tab.transform.Find("ScrollRect").Find("Container");
- GameObject mainMenuButton = settingsPanelTransform.Find("Main Menu").gameObject;
- GameObject modMenuButton = CloneButton(mainMenuButton, settingsPanelTransform, BUTTON_OBJ_NAME, BUTTON_LABEL, false, false);
- modMenuButton.transform.SetSiblingIndex(settingsPanelTransform.childCount - 2);
- modMenuButton.GetComponent
- [HarmonyPatch(typeof(Staff_Nickname), "Render")]
- public class Staff_Nickname_Render
- {
- ///
- /// Prefix method to customize the rendering of the age input popup.
- ///
- /// The instance of Staff_Nickname being patched.
- /// False if the original method should be skipped, true otherwise.
- public static bool Prefix(ref Staff_Nickname __instance)
- {
- if (!agePopup)
- {
- return true;
- }
-
- __instance.NicknameField.GetComponent().characterLimit = 50;
- __instance.NicknameField.GetComponent().text = Language.Insert("AGELIMIT__INPUT_FIELD", new string[] { defaultMinAge.ToString(), defaultMaxAge.ToString() });
-
- ExtensionMethods.SetColor(__instance.NicknameText, mainScript.blue32);
- __instance.NicknameImage.GetComponent().color = mainScript.blue32;
-
- __instance.transform.Find("Remove").gameObject.GetComponentInChildren().Constant = "AGELIMIT__DEFAULT";
- __instance.transform.Find("Save").gameObject.GetComponentInChildren().Constant = "AGELIMIT__SAVE";
- __instance.transform.Find("Save").gameObject.GetComponentInChildren().SetTooltip(ExtensionMethods.color(Language.Data["AGELIMIT__TOOLTIP"], mainScript.red));
-
- __instance.Nickname_Set(__instance.NicknameField.GetComponent().text);
-
- return false;
- }
- }
-
- ///
- /// Patches the Staff_Nickname class to handle saving age input.
- ///
- [HarmonyPatch(typeof(Staff_Nickname), "Nickname_onSave")]
- public class Staff_Nickname_Nickname_onSave
- {
- ///
- /// Prefix method to handle saving the age input.
- ///
- /// The instance of Staff_Nickname being patched.
- /// False if the original method should be skipped, true otherwise.
- public static bool Prefix(Staff_Nickname __instance)
- {
- if (!agePopup)
- {
- return true;
- }
- ParseAgeRange(__instance.NicknameField.GetComponent().text);
- Camera.main.GetComponent().Data.GetComponent().Close();
- agePopup = false;
- return false;
- }
- }
-
- ///
- /// Patches the Staff_Nickname class to handle resetting age input.
- ///
- [HarmonyPatch(typeof(Staff_Nickname), "Nickname_onRemove")]
- public class Staff_Nickname_Nickname_onRemove
- {
- ///
- /// Prefix method to handle resetting the age input to default values.
- ///
- /// False if the original method should be skipped, true otherwise.
- public static bool Prefix()
- {
- if (!agePopup)
- {
- return true;
- }
- minAge = defaultMinAge;
- maxAge = defaultMaxAge;
- Camera.main.GetComponent().Data.GetComponent().Close();
- agePopup = false;
- return false;
- }
- }
-
- ///
- /// Patches the Staff_Nickname class to handle clicking on the age input field.
- ///
- [HarmonyPatch(typeof(Staff_Nickname), "Nickname_onClick")]
- public class Staff_Nickname_Nickname_onClick
- {
- ///
- /// Prefix method to handle clicking on the age input field.
- ///
- /// The instance of Staff_Nickname being patched.
- /// False if the original method should be skipped, true otherwise.
- public static bool Prefix(Staff_Nickname __instance)
- {
- if (!agePopup)
- {
- return true;
- }
-
- if (__instance.NicknameField.GetComponent().text == Language.Insert("AGELIMIT__INPUT_FIELD", new string[] { defaultMinAge.ToString(), defaultMaxAge.ToString() }))
- {
- __instance.NicknameField.GetComponent().text = "";
- }
- return false;
- }
- }
-
- ///
- /// Patches the Staff_Nickname class to handle setting the age input.
- ///
- [HarmonyPatch(typeof(Staff_Nickname), "Nickname_Set")]
- public class Staff_Nickname_Nickname_Set
- {
- ///
- /// Prefix method to handle setting and validating the age input.
- ///
- /// The instance of Staff_Nickname being patched.
- /// The input value to set.
- /// False if the original method should be skipped, true otherwise.
- public static bool Prefix(Staff_Nickname __instance, string val)
- {
- if (!agePopup)
- {
- return true;
- }
-
- inputValid = IsInputValid(val);
-
- if (inputValid)
- {
- __instance.transform.Find("Save").gameObject.GetComponentInChildren().SetTooltip("");
- }
- else
- {
- __instance.transform.Find("Save").gameObject.GetComponentInChildren().SetTooltip(ExtensionMethods.color(Language.Data["AGELIMIT__TOOLTIP"], mainScript.red));
- }
-
- __instance.NicknameSave.GetComponent().Activate(inputValid, true);
-
-
- return false;
- }
- }
-
-}
diff --git a/mods/Targeted Auditions/Targeted Auditions.cs b/mods/Targeted Auditions/Targeted Auditions.cs
index 649c2b5..e9fad68 100755
--- a/mods/Targeted Auditions/Targeted Auditions.cs
+++ b/mods/Targeted Auditions/Targeted Auditions.cs
@@ -1,395 +1,659 @@
-using HarmonyLib;
-using System;
-using System.Collections.Generic;
-using System.Reflection.Emit;
-using UnityEngine;
-using UnityEngine.UI;
-using System.Linq;
-using static CustomAuditions.CustomAuditions;
-
-namespace CustomAuditions
-{
-
- ///
- /// Patches the Popup_Audition class to allow scrolling cards in the audition popup.
- ///
- // Set up audition popup to allow scrolling cards
- [HarmonyPatch(typeof(Popup_Audition), "Start")]
- public class Popup_Audition_Start
- {
-
- ///
- /// Sets the properties of a RectTransform.
- ///
- /// The RectTransform to modify.
- /// The minimum anchor point.
- /// The maximum anchor point.
- /// The minimum offset.
- /// The maximum offset.
- private static void SetRectTransform(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 offsetMin, Vector2 offsetMax)
- {
- rt.anchorMin = anchorMin;
- rt.anchorMax = anchorMax;
- rt.offsetMin = offsetMin;
- rt.offsetMax = offsetMax;
- }
-
- ///
- /// Postfix method to set up the scrollable audition popup.
- ///
- /// The instance of Popup_Audition being patched.
- public static void Postfix(Popup_Audition __instance)
- {
- if (__instance.Cards_Container.transform.parent.GetComponent() != null)
- return;
-
- // Create ScrollRect container and attach to panel
- GameObject scrollContainer = new(AUD_SCROLLRECT_NAME, typeof(RectTransform), typeof(ScrollRect));
- RectTransform scrollRectTransform = scrollContainer.GetComponent();
- SetRectTransform(scrollRectTransform, Vector2.zero, Vector2.one, Vector2.zero, Vector2.zero);
-
- // Configure the ScrollRect
- ScrollRect scrollRect = scrollContainer.GetComponent();
- scrollRect.content = __instance.Cards_Container.GetComponent(); // attach content
- scrollRect.vertical = false;
- scrollRect.horizontal = true;
- scrollRect.movementType = ScrollRect.MovementType.Elastic;
- scrollRect.elasticity = 0.1f;
- scrollRect.inertia = false;
- scrollRect.scrollSensitivity = 20;
-
- // Configure hierarchy
- scrollContainer.transform.SetParent(__instance.Cards_Container.transform.parent, false);
- __instance.Cards_Container.transform.SetParent(scrollContainer.transform, false);
-
- __instance.Cards_Container.AddComponent().horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
- }
- }
-
- ///
- /// Patches the Auditions class to set variables at the start of an audition.
- ///
- [HarmonyPatch(typeof(Auditions), "GenerateGirls")]
- public class Auditions_GenerateGirls
- {
- ///
- /// Prefix method to set audition parameters before generating girls.
- ///
- /// The instance of Auditions being patched.
- public static void Prefix(Auditions __instance)
- {
- // Set audition age limits (only if popup is not used)
- bool toggle = int.Parse(variables.Get(VARID_AGELIMIT_POPUP_TOGGLE) ?? DEF_AGELIMIT_POPUP_TOGGLE) == 1;
- if (!toggle)
- {
- minAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MINAGE_STR);
- maxAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MAXAGE_STR);
- if (maxAge < minAge)
- {
- // swap values
- maxAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MAXAGE_STR);
- minAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MINAGE_STR);
-
- // correct default variables
- defaultMaxAge = maxAge;
- defaultMinAge = minAge;
- variables.Set(VARID_MAXAGE, maxAge.ToString());
- variables.Set(VARID_MINAGE, minAge.ToString());
- }
- }
-
- // Set sexual orientation
- float varLesbian = float.Parse(variables.Get(VARID_LESCHANCE) ?? DEF_CHANCE_LES_STR);
- float varBi = float.Parse(variables.Get(VARID_BICHANCE) ?? DEF_CHANCE_BI_STR);
-
- if (varLesbian + varBi > 100)
- {
- varLesbian = Mathf.Floor(varLesbian / (varLesbian + varBi) * 100);
- varBi = 100 - varLesbian;
- variables.Set(VARID_LESCHANCE, varLesbian.ToString());
- variables.Set(VARID_BICHANCE, varBi.ToString());
- }
- chanceLesbian = (int)varLesbian;
- chanceBi = (int)Mathf.Floor(varBi / (100 - chanceLesbian) * 100);
-
-
- // Set stat priorities
- foreach (data_girls._paramType param in paramTypes)
- {
- int value = int.Parse(variables.Get($"{VARID_PRIO_PREFIX}{param}") ?? DEF_PRIO);
- priorityDict[param] = value;
- }
-
- // Set girl count
- __instance.NumberOfGirls = int.Parse(variables.Get(VARID_COUNT) ?? DEF_COUNT);
-
-
- }
- }
-
- ///
- /// Patches the data_girls class to apply girl sexuality.
- ///
- [HarmonyPatch(typeof(data_girls), "GenerateGirl")]
- public class data_girls_GenerateGirl
- {
- ///
- /// Postfix method to set the sexuality of a generated girl.
- ///
- /// The generated girl data.
- public static void Postfix(ref data_girls.girls __result)
- {
- int girlCount = int.Parse(variables.Get(VARID_COUNT) ?? DEF_COUNT);
- if (girlCount > 12)
- {
- Auditions.UsedBodyIDs.Clear();
- }
-
- data_girls.girls._sexuality sexuality = data_girls.girls._sexuality.straight;
- if (mainScript.chance(chanceLesbian))
- {
- sexuality = data_girls.girls._sexuality.lesbian;
- }
- else if (mainScript.chance(chanceBi))
- {
- sexuality = data_girls.girls._sexuality.bi;
- }
- __result.sexuality = sexuality;
- }
- }
-
- ///
- /// Patches the data_girls class to apply custom girl stats.
- ///
- [HarmonyPatch(typeof(data_girls), "GenerateParams")]
- public static class data_girls_GenerateParams
- {
- ///
- /// Transpiler method to modify the IL code for generating girl parameters.
- ///
- /// The original IL instructions.
- /// The modified IL instructions.
- public static IEnumerable Transpiler(IEnumerable instructions)
- {
- List instructionList = new(instructions);
-
- int index = -1;
- for (int i = 0; i < instructionList.Count - 1; i++)
- {
- if (instructionList[i].opcode == OpCodes.Ldloc_1 && instructionList[i + 1].opcode == OpCodes.Call)
- {
- index = i + 1;
- break;
- }
- }
-
- if (index != -1)
- {
- instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_1));
- instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(data_girls_GenerateParams), "Infix")));
- instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Stloc_1));
- }
-
- return instructionList.AsEnumerable();
- }
-
- ///
- /// Infix method to assign stat values based on priorities.
- ///
- /// The list of stat values to assign.
- /// A new list of assigned stat values.
- public static List Infix(List statValues)
- {
- List output = new(statValues);
- Dictionary priorityDictTemp = new(priorityDict);
- List remainingParamTypes = priorityDictTemp.Keys.ToList();
-
- List sortedStatValues = statValues.OrderByDescending(v => v).ToList();
-
- // Assign stats based on priority
- foreach (int statValue in sortedStatValues)
- {
-
- // Calculate total priority
- int totalPriority = remainingParamTypes.Sum(p => priorityDictTemp[p]);
-
- // Roll a random number
- int roll = UnityEngine.Random.Range(1, totalPriority + 1);
-
- // Find which param "wins" this roll
- int cumulativePriority = 0;
- for (int i = 0; i < remainingParamTypes.Count; i++)
- {
- cumulativePriority += priorityDict[remainingParamTypes[i]];
- if (roll <= cumulativePriority)
- {
- output[paramTypes.IndexOf(remainingParamTypes[i])] = statValue;
- remainingParamTypes.RemoveAt(i);
- break;
- }
- }
- }
-
- return output;
- }
-
- }
-
- ///
- /// Patches the data_girls.girls class to apply age limits.
- ///
- [HarmonyPatch(typeof(data_girls.girls), "GenerateBirthday")]
- public class data_girls_girls_GenerateBirthday
- {
- public static void Postfix(ref data_girls.girls __instance)
- {
- DateTime dateTime = staticVars.dateTime
- .AddYears(-maxAge - 1)
- .AddYears(UnityEngine.Random.Range(0, maxAge - minAge + 1))
- .AddMonths(UnityEngine.Random.Range(0, 12))
- .AddDays(UnityEngine.Random.Range(0, 31));
- __instance.SetBirthday(dateTime);
- }
- }
-
- ///
- /// Patches the CM_Player_Audition_Button class to handle age input popup. (obsolete)
- ///
- [HarmonyPatch(typeof(CM_Player_Audition_Button), "OnClick")]
- public class Auditions_GenerateAudition
- {
- ///
- /// Postfix method to show the age input popup if enabled. (disabled)
- ///
- public static void Postfix()
- {
- bool toggle = int.Parse(variables.Get(VARID_AGELIMIT_POPUP_TOGGLE) ?? DEF_AGELIMIT_POPUP_TOGGLE) == 1;
- if (toggle)
- {
- defaultMinAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MINAGE_STR);
- defaultMaxAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MAXAGE_STR);
- if (defaultMaxAge < defaultMinAge)
- {
- // swap values
- defaultMaxAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MAXAGE_STR);
- defaultMinAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MINAGE_STR);
-
- // correct variables
- variables.Set(VARID_MAXAGE, maxAge.ToString());
- variables.Set(VARID_MINAGE, minAge.ToString());
- }
- agePopup = true;
- Camera.main.GetComponent().Data.GetComponent().Open(PopupManager._type.staff_nickname, true);
- }
- }
- }
-
- ///
- /// Contains utility methods and variables for custom auditions.
- ///
- class CustomAuditions
- {
- public const string DEF_MINAGE_STR = "12";
- public const string DEF_MAXAGE_STR = "23";
- public const string DEF_CHANCE_LES_STR = "7";
- public const string DEF_CHANCE_BI_STR = "14";
- public const string DEF_PRIO = "50";
- public const string DEF_COUNT = "5";
-
- public const string VARID_MINAGE = "AuditionAgeLimit_MinAge";
- public const string VARID_MAXAGE = "AuditionAgeLimit_MaxAge";
- public const string VARID_BICHANCE = "CustomAudition_Bi";
- public const string VARID_LESCHANCE = "CustomAudition_Gay";
- public const string VARID_PRIO_PREFIX = "CustomAudition_Prio_";
- public const string VARID_COUNT = "CustomAudition_Count";
-
-
- public const string AUD_SCROLLRECT_NAME = "ScrollContainer";
-
- public const string VARID_AGELIMIT_POPUP_TOGGLE = "AuditionAgeLimit_TogglePopup";
- public const string DEF_AGELIMIT_POPUP_TOGGLE = "0";
-
- public static int defaultMinAge = 12;
- public static int defaultMaxAge = 23;
- public static int minAge = defaultMinAge;
- public static int maxAge = defaultMaxAge;
-
- public static int chanceLesbian = 7;
- public static int chanceBi = 14;
-
- public static bool agePopup = false;
- public static bool inputValid = false;
-
- ///
- /// Parses the age range string and sets the minAge and maxAge values.
- ///
- /// The age range string to parse.
- public static void ParseAgeRange(string ageRange)
- {
- if (IsInputValid(ageRange))
- {
- string[] ageLimits = ageRange.Split('-');
- minAge = int.Parse(ageLimits[0].Trim());
- maxAge = int.Parse(ageLimits[1].Trim());
- }
- else
- {
- minAge = defaultMinAge;
- maxAge = defaultMaxAge;
- }
- }
-
- ///
- /// Validates the input age range string.
- ///
- /// The age range string to validate.
- /// True if the input is valid, false otherwise.
- public static bool IsInputValid(string ageRange)
- {
-
- string[] ageLimits = ageRange.Split('-');
-
- if (ageLimits == null || ageLimits.Length != 2)
- {
- return false;
- }
-
- if (!int.TryParse(ageLimits[0].Trim(), out int min))
- {
- return false;
- }
- if (!int.TryParse(ageLimits[1].Trim(), out int max))
- {
- return false;
- }
-
- if (max < 1 || min < 1)
- {
- return false;
- }
-
- if (max < min)
- {
- return false;
- }
-
- return true;
- }
-
-
- public static List paramTypes = new()
- {
- data_girls._paramType.cute,
- data_girls._paramType.cool,
- data_girls._paramType.sexy,
- data_girls._paramType.pretty,
- data_girls._paramType.vocal,
- data_girls._paramType.dance,
- data_girls._paramType.funny,
- data_girls._paramType.smart
- };
-
- public static Dictionary priorityDict = new();
-
- }
-}
+using HarmonyLib;
+using System;
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using UnityEngine;
+using UnityEngine.UI;
+using System.Linq;
+using static CustomAuditions.CustomAuditions;
+
+namespace CustomAuditions
+{
+
+ ///
+ /// Patches the Popup_Audition class to allow scrolling cards in the audition popup.
+ ///
+ // Set up audition popup to allow scrolling cards
+ [HarmonyPatch(typeof(Popup_Audition), "Start")]
+ public class Popup_Audition_Start
+ {
+
+ ///
+ /// Sets the properties of a RectTransform.
+ ///
+ /// The RectTransform to modify.
+ /// The minimum anchor point.
+ /// The maximum anchor point.
+ /// The minimum offset.
+ /// The maximum offset.
+ private static void SetRectTransform(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 offsetMin, Vector2 offsetMax)
+ {
+ rt.anchorMin = anchorMin;
+ rt.anchorMax = anchorMax;
+ rt.offsetMin = offsetMin;
+ rt.offsetMax = offsetMax;
+ }
+
+ ///
+ /// Postfix method to set up the scrollable audition popup.
+ ///
+ /// The instance of Popup_Audition being patched.
+ public static void Postfix(Popup_Audition __instance)
+ {
+ if (__instance == null || __instance.Cards_Container == null)
+ return;
+
+ Transform currentParent = __instance.Cards_Container.transform.parent;
+ if (currentParent == null)
+ return;
+
+ if (currentParent.GetComponent() != null)
+ return;
+
+ // Create ScrollRect container and attach to panel
+ GameObject scrollContainer = new(AUD_SCROLLRECT_NAME, typeof(RectTransform), typeof(ScrollRect));
+ RectTransform scrollRectTransform = scrollContainer.GetComponent();
+ SetRectTransform(scrollRectTransform, Vector2.zero, Vector2.one, Vector2.zero, Vector2.zero);
+
+ // Configure the ScrollRect
+ ScrollRect scrollRect = scrollContainer.GetComponent();
+ scrollRect.content = __instance.Cards_Container.GetComponent(); // attach content
+ scrollRect.viewport = scrollRectTransform;
+ scrollRect.vertical = false;
+ scrollRect.horizontal = true;
+ scrollRect.movementType = ScrollRect.MovementType.Elastic;
+ scrollRect.elasticity = 0.1f;
+ scrollRect.inertia = false;
+ scrollRect.scrollSensitivity = 20;
+
+ // Configure hierarchy
+ scrollContainer.transform.SetParent(currentParent, false);
+ __instance.Cards_Container.transform.SetParent(scrollContainer.transform, false);
+
+ // Reuse existing fitter if one exists to avoid duplicate component warnings.
+ ContentSizeFitter fitter = __instance.Cards_Container.GetComponent();
+ if (fitter == null)
+ {
+ fitter = __instance.Cards_Container.AddComponent();
+ }
+ fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
+ }
+ }
+
+ ///
+ /// Tracks audition popup loading from the moment Set begins.
+ /// Starting before the original Set keeps the watchdog state aligned with
+ /// Assistant Manager's delayed per-manager audition handoff.
+ ///
+ [HarmonyPatch(typeof(Popup_Audition), "Set", new Type[] { typeof(Auditions.data), typeof(bool) })]
+ public class Popup_Audition_Set
+ {
+ public static void Prefix(Popup_Audition __instance)
+ {
+ if (__instance == null)
+ {
+ return;
+ }
+
+ auditionLoadStartedAt[__instance.GetInstanceID()] = Time.unscaledTime;
+ }
+
+ public static Exception Finalizer(Popup_Audition __instance, Exception __exception)
+ {
+ if (__exception == null)
+ {
+ return null;
+ }
+
+ if (__instance != null)
+ {
+ auditionLoadStartedAt.Remove(__instance.GetInstanceID());
+ }
+
+ Debug.LogError(
+ "[Targeted Auditions] Popup_Audition.Set failed:\n" +
+ __exception);
+
+ // Preserve the original exception.
+ return __exception;
+ }
+ }
+
+ ///
+ /// Clears load watchdog state when audition popup is reset.
+ ///
+ [HarmonyPatch(typeof(Popup_Audition), "Reset")]
+ public class Popup_Audition_Reset
+ {
+ ///
+ /// Postfix method that removes stale watchdog entries.
+ ///
+ /// Popup instance.
+ public static void Postfix(Popup_Audition __instance)
+ {
+ if (__instance == null)
+ {
+ return;
+ }
+
+ auditionLoadStartedAt.Remove(__instance.GetInstanceID());
+ }
+ }
+
+ ///
+ /// Clears load watchdog state when audition popup is closed.
+ ///
+ [HarmonyPatch(typeof(Popup_Audition), "Close")]
+ public class Popup_Audition_Close
+ {
+ ///
+ /// Prefix method that removes stale watchdog entries before close logic runs.
+ ///
+ /// Popup instance.
+ public static void Prefix(Popup_Audition __instance)
+ {
+ if (__instance == null)
+ {
+ return;
+ }
+
+ auditionLoadStartedAt.Remove(__instance.GetInstanceID());
+ }
+ }
+
+ ///
+ /// Prevents recruitment popup deadlocks when one portrait never resolves.
+ ///
+ [HarmonyPatch(typeof(Popup_Audition), "PortraitsLoaded")]
+ public class Popup_Audition_PortraitsLoaded
+ {
+ ///
+ /// Postfix method that applies a timeout fallback for stuck portrait loading.
+ ///
+ /// Popup instance.
+ /// Original readiness result.
+ public static void Postfix(Popup_Audition __instance, ref bool __result)
+ {
+ if (__result || __instance == null || __instance.Cards_Container == null)
+ {
+ return;
+ }
+
+ int popupId = __instance.GetInstanceID();
+ if (!auditionLoadStartedAt.TryGetValue(popupId, out float startedAt))
+ {
+ return;
+ }
+
+ float elapsed = Time.unscaledTime - startedAt;
+ if (elapsed < PORTRAIT_LOAD_TIMEOUT_SECONDS)
+ {
+ return;
+ }
+
+ // The vanilla coroutine waits indefinitely for all portraits. With large candidate counts this can
+ // deadlock the popup (blur shown, cards never become interactive). After timeout, continue anyway.
+ EnsurePopupIsVisible(__instance);
+ Sprite fallbackSprite = FindFallbackPortraitSprite(__instance);
+ bool missingPortraits = FillMissingPortraits(__instance, fallbackSprite);
+ if (missingPortraits)
+ {
+ Debug.Log("[Targeted Auditions] Portrait load timed out. Continuing with fallback portraits.");
+ }
+
+ __result = true;
+ }
+
+ private static void EnsurePopupIsVisible(Popup_Audition popup)
+ {
+ CanvasGroup cg = popup.GetComponent();
+ if (cg != null)
+ {
+ cg.alpha = 1f;
+ cg.blocksRaycasts = true;
+ cg.interactable = true;
+ }
+
+ RectTransform rt = popup.GetComponent();
+ if (rt != null)
+ {
+ rt.localScale = Vector3.one;
+ }
+ }
+
+ private static Sprite FindFallbackPortraitSprite(Popup_Audition popup)
+ {
+ foreach (Transform child in popup.Cards_Container.transform)
+ {
+ Audition_Closed_Card closedCard = child.GetComponent();
+ if (closedCard == null || closedCard.Portrait == null)
+ {
+ continue;
+ }
+
+ Image image = closedCard.Portrait.GetComponent();
+ if (image != null && image.sprite != null)
+ {
+ return image.sprite;
+ }
+ }
+
+ return null;
+ }
+
+ private static bool FillMissingPortraits(Popup_Audition popup, Sprite fallback)
+ {
+ bool hadMissing = false;
+ foreach (Transform child in popup.Cards_Container.transform)
+ {
+ Audition_Closed_Card closedCard = child.GetComponent();
+ if (closedCard == null || closedCard.Portrait == null)
+ {
+ continue;
+ }
+
+ Image image = closedCard.Portrait.GetComponent();
+ if (image == null || image.sprite != null)
+ {
+ continue;
+ }
+
+ hadMissing = true;
+ if (fallback != null)
+ {
+ image.sprite = fallback;
+ }
+ }
+
+ return hadMissing;
+ }
+ }
+
+ ///
+ /// Patches the Auditions class to set variables at the start of an audition.
+ ///
+ [HarmonyPatch(typeof(Auditions), "GenerateGirls")]
+ public class Auditions_GenerateGirls
+ {
+ ///
+ /// Prefix method to set audition parameters before generating girls.
+ ///
+ /// The instance of Auditions being patched.
+ public static void Prefix(Auditions __instance, out bool __state)
+ {
+ __state = false;
+ LoadConfiguredAgeRange();
+
+ // Set sexual orientation
+ float varLesbian = float.Parse(variables.Get(VARID_LESCHANCE) ?? DEF_CHANCE_LES_STR);
+ float varBi = float.Parse(variables.Get(VARID_BICHANCE) ?? DEF_CHANCE_BI_STR);
+
+ if (varLesbian + varBi > 100)
+ {
+ varLesbian = Mathf.Floor(varLesbian / (varLesbian + varBi) * 100);
+ varBi = 100 - varLesbian;
+ variables.Set(VARID_LESCHANCE, varLesbian.ToString());
+ variables.Set(VARID_BICHANCE, varBi.ToString());
+ }
+ chanceLesbian = (int)varLesbian;
+ chanceBi = (int)Mathf.Floor(varBi / (100 - chanceLesbian) * 100);
+
+
+ // Set stat priorities
+ foreach (data_girls._paramType param in paramTypes)
+ {
+ int value = int.Parse(variables.Get($"{VARID_PRIO_PREFIX}{param}") ?? DEF_PRIO);
+ priorityDict[param] = value;
+ }
+
+ // Set girl count
+ __instance.NumberOfGirls = int.Parse(variables.Get(VARID_COUNT) ?? DEF_COUNT);
+
+ BeginAuditionGeneration();
+ __state = true;
+ }
+
+ public static Exception Finalizer(Exception __exception, bool __state)
+ {
+ if (__state)
+ {
+ EndAuditionGeneration();
+ }
+ if (__exception != null)
+ {
+ Debug.LogError(
+ "[Targeted Auditions] Auditions.GenerateGirls failed:\n" +
+ __exception);
+ }
+
+ // Preserve the original exception.
+ return __exception;
+ }
+ }
+
+ ///
+ /// Patches the data_girls class to apply girl sexuality.
+ ///
+ [HarmonyPatch(typeof(data_girls), "GenerateGirl")]
+ public class data_girls_GenerateGirl
+ {
+ ///
+ /// Before an audition candidate is generated, allow body IDs to repeat only after
+ /// every currently eligible body ID has been used once in this audition.
+ ///
+ public static void Prefix(bool genTextures, data_girls_textures._textureAsset BodyAsset)
+ {
+ if (!IsGeneratingAudition || !genTextures || BodyAsset != null)
+ {
+ return;
+ }
+
+ if (!HasUnusedEligibleBody())
+ {
+ Auditions.UsedBodyIDs.Clear();
+ }
+ }
+
+ ///
+ /// Postfix method to set the sexuality of a generated audition candidate.
+ ///
+ /// The generated girl data.
+ public static void Postfix(ref data_girls.girls __result)
+ {
+ if (!IsGeneratingAudition || __result == null)
+ {
+ return;
+ }
+
+ data_girls.girls._sexuality sexuality = data_girls.girls._sexuality.straight;
+ if (mainScript.chance(chanceLesbian))
+ {
+ sexuality = data_girls.girls._sexuality.lesbian;
+ }
+ else if (mainScript.chance(chanceBi))
+ {
+ sexuality = data_girls.girls._sexuality.bi;
+ }
+ __result.sexuality = sexuality;
+ }
+ }
+
+ ///
+ /// Patches the data_girls class to apply custom girl stats.
+ ///
+ [HarmonyPatch(typeof(data_girls), "GenerateParams")]
+ public static class data_girls_GenerateParams
+ {
+ ///
+ /// Transpiler method to modify the IL code for generating girl parameters.
+ ///
+ /// The original IL instructions.
+ /// The modified IL instructions.
+ public static IEnumerable Transpiler(IEnumerable instructions)
+ {
+ List instructionList = new(instructions);
+
+ int index = -1;
+ for (int i = 0; i < instructionList.Count - 1; i++)
+ {
+ if (instructionList[i].opcode == OpCodes.Ldloc_1 && instructionList[i + 1].opcode == OpCodes.Call)
+ {
+ index = i + 1;
+ break;
+ }
+ }
+
+ if (index != -1)
+ {
+ instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_1));
+ instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(data_girls_GenerateParams), "Infix")));
+ instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Stloc_1));
+ }
+
+ return instructionList.AsEnumerable();
+ }
+
+ ///
+ /// Infix method to assign stat values based on priorities.
+ ///
+ /// The list of stat values to assign.
+ /// A new list of assigned stat values.
+ public static List Infix(List statValues)
+ {
+ if (!IsGeneratingAudition)
+ {
+ return statValues;
+ }
+
+ List output = new(statValues);
+ Dictionary priorityDictTemp = new(priorityDict);
+ List remainingParamTypes = priorityDictTemp.Keys.ToList();
+
+ List sortedStatValues = statValues.OrderByDescending(v => v).ToList();
+
+ // Assign stats based on priority
+ foreach (int statValue in sortedStatValues)
+ {
+
+ // Calculate total priority
+ int totalPriority = remainingParamTypes.Sum(p => priorityDictTemp[p]);
+
+ // Roll a random number
+ int roll = UnityEngine.Random.Range(1, totalPriority + 1);
+
+ // Find which param "wins" this roll
+ int cumulativePriority = 0;
+ for (int i = 0; i < remainingParamTypes.Count; i++)
+ {
+ cumulativePriority += priorityDict[remainingParamTypes[i]];
+ if (roll <= cumulativePriority)
+ {
+ output[paramTypes.IndexOf(remainingParamTypes[i])] = statValue;
+ remainingParamTypes.RemoveAt(i);
+ break;
+ }
+ }
+ }
+
+ return output;
+ }
+
+ }
+
+ ///
+ /// Patches the data_girls.girls class to apply age limits.
+ ///
+ [HarmonyPatch(typeof(data_girls.girls), "GenerateBirthday")]
+ public class data_girls_girls_GenerateBirthday
+ {
+ public static void Postfix(ref data_girls.girls __instance)
+ {
+ if (IsGeneratingAudition)
+ {
+ ApplyRandomBirthdayInConfiguredRange(__instance);
+ }
+ }
+ }
+
+ ///
+ /// Contains utility methods and variables for custom auditions.
+ ///
+ class CustomAuditions
+ {
+ public const string DEF_MINAGE_STR = "12";
+ public const string DEF_MAXAGE_STR = "23";
+ public const string DEF_CHANCE_LES_STR = "7";
+ public const string DEF_CHANCE_BI_STR = "14";
+ public const string DEF_PRIO = "50";
+ public const string DEF_COUNT = "5";
+
+ public const string VARID_MINAGE = "AuditionAgeLimit_MinAge";
+ public const string VARID_MAXAGE = "AuditionAgeLimit_MaxAge";
+ public const string VARID_BICHANCE = "CustomAudition_Bi";
+ public const string VARID_LESCHANCE = "CustomAudition_Gay";
+ public const string VARID_PRIO_PREFIX = "CustomAudition_Prio_";
+ public const string VARID_COUNT = "CustomAudition_Count";
+
+
+ public const string AUD_SCROLLRECT_NAME = "ScrollContainer";
+ public const float PORTRAIT_LOAD_TIMEOUT_SECONDS = 6f;
+
+ public const string VARID_AGELIMIT_POPUP_TOGGLE = "AuditionAgeLimit_TogglePopup";
+ public const string DEF_AGELIMIT_POPUP_TOGGLE = "0";
+
+ public static int defaultMinAge = 12;
+ public static int defaultMaxAge = 23;
+ public static int minAge = defaultMinAge;
+ public static int maxAge = defaultMaxAge;
+
+ public static int chanceLesbian = 7;
+ public static int chanceBi = 14;
+
+ public static bool agePopup = false;
+ public static bool inputValid = false;
+ public static Dictionary auditionLoadStartedAt = new();
+
+ ///
+ /// Parses the age range string and sets the minAge and maxAge values.
+ ///
+ /// The age range string to parse.
+ public static void ParseAgeRange(string ageRange)
+ {
+ if (IsInputValid(ageRange))
+ {
+ string[] ageLimits = ageRange.Split('-');
+ minAge = int.Parse(ageLimits[0].Trim());
+ maxAge = int.Parse(ageLimits[1].Trim());
+ }
+ else
+ {
+ minAge = defaultMinAge;
+ maxAge = defaultMaxAge;
+ }
+ }
+
+ ///
+ /// Validates the input age range string.
+ ///
+ /// The age range string to validate.
+ /// True if the input is valid, false otherwise.
+ public static bool IsInputValid(string ageRange)
+ {
+
+ string[] ageLimits = ageRange.Split('-');
+
+ if (ageLimits == null || ageLimits.Length != 2)
+ {
+ return false;
+ }
+
+ if (!int.TryParse(ageLimits[0].Trim(), out int min))
+ {
+ return false;
+ }
+ if (!int.TryParse(ageLimits[1].Trim(), out int max))
+ {
+ return false;
+ }
+
+ if (max < 1 || min < 1)
+ {
+ return false;
+ }
+
+ if (max < min)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+
+ public static List paramTypes = new()
+ {
+ data_girls._paramType.cute,
+ data_girls._paramType.cool,
+ data_girls._paramType.sexy,
+ data_girls._paramType.pretty,
+ data_girls._paramType.vocal,
+ data_girls._paramType.dance,
+ data_girls._paramType.funny,
+ data_girls._paramType.smart
+ };
+
+ public static Dictionary priorityDict = new();
+
+ private static int auditionGenerationDepth = 0;
+ public static bool IsGeneratingAudition => auditionGenerationDepth > 0;
+
+ public static void BeginAuditionGeneration()
+ {
+ auditionGenerationDepth++;
+ }
+
+ public static void EndAuditionGeneration()
+ {
+ if (auditionGenerationDepth > 0)
+ {
+ auditionGenerationDepth--;
+ }
+ }
+
+ private static readonly System.Reflection.FieldInfo textureAssetsField =
+ AccessTools.Field(typeof(data_girls_textures), "textureAssets");
+
+ public static bool HasUnusedEligibleBody()
+ {
+ List textureAssets =
+ textureAssetsField?.GetValue(null) as List;
+ if (textureAssets == null)
+ {
+ return false;
+ }
+
+ return textureAssets.Any(asset =>
+ asset != null &&
+ !asset.Add_To_Default &&
+ asset.type == data_girls_textures._spriteType.body &&
+ !Auditions.UsedBodyIDs.Contains(asset.body_id) &&
+ asset.CanBeHired());
+ }
+
+ public static void LoadConfiguredAgeRange()
+ {
+ // The former per-audition age popup is retired. Always use the Mod Menu range.
+ variables.Set(VARID_AGELIMIT_POPUP_TOGGLE, DEF_AGELIMIT_POPUP_TOGGLE);
+
+ minAge = int.Parse(variables.Get(VARID_MINAGE) ?? DEF_MINAGE_STR);
+ maxAge = int.Parse(variables.Get(VARID_MAXAGE) ?? DEF_MAXAGE_STR);
+ if (maxAge < minAge)
+ {
+ int originalMinAge = minAge;
+ minAge = maxAge;
+ maxAge = originalMinAge;
+
+ defaultMaxAge = maxAge;
+ defaultMinAge = minAge;
+ variables.Set(VARID_MAXAGE, maxAge.ToString());
+ variables.Set(VARID_MINAGE, minAge.ToString());
+ }
+ }
+
+ public static void ApplyRandomBirthdayInConfiguredRange(data_girls.girls girl)
+ {
+ if (girl == null)
+ {
+ return;
+ }
+
+ int age = UnityEngine.Random.Range(minAge, maxAge + 1);
+ DateTime latestBirthday = staticVars.dateTime.AddYears(-age);
+ DateTime earliestBirthday = staticVars.dateTime.AddYears(-age - 1).AddDays(1);
+ int possibleDays = (latestBirthday - earliestBirthday).Days + 1;
+ DateTime dateTime = earliestBirthday.AddDays(UnityEngine.Random.Range(0, possibleDays));
+ girl.SetBirthday(dateTime);
+ }
+
+ }
+}
diff --git a/mods/Targeted Auditions/Targeted Auditions.csproj b/mods/Targeted Auditions/Targeted Auditions.csproj
index d1ed09c..6e56b1e 100755
--- a/mods/Targeted Auditions/Targeted Auditions.csproj
+++ b/mods/Targeted Auditions/Targeted Auditions.csproj
@@ -1,11 +1,11 @@
-
+
Targeted Auditions
com.tel.customauditions
(formerly Audition Age Limits) Customise you auditions to target girls by age, skill and sexuality.
Tel
- 2.0.1
+ 2.0.3
["gameplay"]
diff --git a/mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json b/mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json
index d3985b8..098e24c 100755
--- a/mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json
+++ b/mods/Targeted Auditions/assets/JSON/Mod Menu/modmenu.json
@@ -15,21 +15,6 @@
"maxValue": 100,
"defaultValue": 23
},
- {
- "type": "dropdown",
- "varID": "AuditionAgeLimit_TogglePopup",
- "labelID": "AUDITIONAGELIMIT__MODMENU__TOGGLE",
- "defaultValue": 1,
- "itemIDList": ["No", "Yes"],
- "ignore": true
- },
- {
- "type": "checkbox",
- "varID": "AuditionAgeLimit_TogglePopup",
- "labelID": "AUDITIONAGELIMIT__MODMENU__TOGGLE",
- "defaultValue": false,
- "ignore": true
- },
{
"type": "slider",
"varID": "CustomAudition_Prio_cute",
diff --git a/mods/Targeted Auditions/assets/steam description.txt b/mods/Targeted Auditions/assets/steam description.txt
index 350dbe5..6651496 100755
--- a/mods/Targeted Auditions/assets/steam description.txt
+++ b/mods/Targeted Auditions/assets/steam description.txt
@@ -7,10 +7,10 @@ This mod was formerly called "Audition Age Limits".
Using the ModMenu menu, you will be able to customise:
[list]
-[*][b]Age limits[/b]: Only idols within the desired age range will be selected. Does not apply to unique idols.
+[*][b]Age limits[/b]: Generated audition candidates stay within the desired inclusive age range. Does not apply to unique idols or other non-audition girl generation.
[*][b]Prioritised Skills[/b]: The higher the prioritisation, the higher the chance that the skill will be the girl's best skill.
[*][b]Sexual Orientation[/b]: Increase to make girls easier to date if you are female or decrease to reduce in-group dating.
-[*][b]Number of Candidates[/b]: Have any number of candidates per audition. Click and drag to scroll through cards in the audition.
+[*][b]Number of Candidates[/b]: Have any number of candidates per audition. Body IDs stay unique until the available audition body pool is exhausted, then repeat as needed. Click and drag to scroll through cards in the audition.
[/list]
[url=https://github.com/ui3TD/Tel-Mod-Library]source code[/url]
\ No newline at end of file
diff --git a/mods/Traits Expansion/Traits Expansion.cs b/mods/Traits Expansion/Traits Expansion.cs
index 53788a8..0920167 100755
--- a/mods/Traits Expansion/Traits Expansion.cs
+++ b/mods/Traits Expansion/Traits Expansion.cs
@@ -180,7 +180,7 @@ public static void Postfix()
if (member.IsSick())
continue;
- if (member.trait == (traits._trait._type)NewTraits.Sadistic && clique.IsBullied(member))
+ if (member.trait == (traits._trait._type)NewTraits.Sadistic && clique.IsBully(member))
{
sadistic++;
}
@@ -377,15 +377,15 @@ public static void Prefix()
[HarmonyPriority(Priority.VeryLow)]
public static void Postfix(data_girls.girls _girl, ref float __result, business._proposal __instance)
{
- // Girls with Wooden Acting receive penalty
+ // Girls with Wooden Acting receive a 50% drama reward penalty.
if (__instance.type == business._type.tv_drama && _girl.trait == (traits._trait._type)NewTraits.Wooden_Acting)
{
- __result -= WOODACTING_PENALTY;
+ __result *= 1f - WOODACTING_PENALTY;
}
- // Girls with Wooden Acting receive penalty
+ // Girls with Quick Wit receive a 50% variety reward bonus.
else if (__instance.type == business._type.variety && _girl.trait == (traits._trait._type)NewTraits.Quick_Wit)
{
- __result += QUICKWIT_BONUS;
+ __result *= 1f + QUICKWIT_BONUS;
}
patchGetVal = false;
}
@@ -506,7 +506,7 @@ public class TraitsExpansion
public const int HOMELY_PENALTY = 10;
public const int TONEDEAF_PENALTY = 30;
- public const float WOODACTING_PENALTY = 0.2f;
+ public const float WOODACTING_PENALTY = 0.5f;
public const float QUICKWIT_BONUS = 0.5f;
public const float RECKLESS_THR_UPPER = 60f;
public const float RECKLESS_THR_LOWER = 5f;
diff --git a/mods/Traits Fix/Traits Fix.cs b/mods/Traits Fix/Traits Fix.cs
index ca70a31..fb49cc3 100755
--- a/mods/Traits Fix/Traits Fix.cs
+++ b/mods/Traits Fix/Traits Fix.cs
@@ -1,764 +1,813 @@
-using HarmonyLib;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection.Emit;
-using UnityEngine;
-using static TraitFix.TraitsFix;
-
-namespace TraitFix
-{
-
- [HarmonyPatch(typeof(data_girls), "AgeDeterioration")]
- public class Data_girls_AgeDeterioration
- {
- // Girls with Live Fast trait have double the rate of stat decreases after their peak age
- public static void Postfix()
- {
- foreach (data_girls.girls girls in data_girls.girl)
- {
- if (girls != null
- && girls.status != data_girls._status.graduated
- && girls.trait == traits._trait._type.Live_fast)
- {
- for(int i = 0; i < LIVEFAST_DETERIORATION - 1; i++)
- {
- girls.AgeDeterioration();
- }
- }
- }
- }
- }
-
- [HarmonyPatch(typeof(Birthday_Popup), "DoParam")]
- public class Birthday_Popup_DoParam
- {
- public static IEnumerable Transpiler(IEnumerable instructions)
- {
- List instructionList = new(instructions);
-
- int index = -1;
- bool breakFlag = false;
- for (int i = 0; i < instructionList.Count; i++)
- {
- if (breakFlag && instructionList[i].opcode == OpCodes.Stloc_1)
- {
- index = i;
- break;
- }
- if (instructionList[i].opcode == OpCodes.Ldc_R4 && (float)instructionList[i].operand == 0.975f)
- {
- breakFlag = true;
- }
- }
-
- if (index != -1)
- {
- instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldarg_0));
- instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Ldloc_1));
- instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Ldloc_0));
- instructionList.Insert(index + 4, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Birthday_Popup_DoParam), "Infix")));
- instructionList.Insert(index + 5, new CodeInstruction(OpCodes.Stloc_1));
- }
-
- return instructionList.AsEnumerable();
- }
-
- public static float Infix(Birthday_Popup __this, float newVal, float orig)
- {
- if (__this.Girl.trait != traits._trait._type.Live_fast)
- return orig;
-
- return LIVEFAST_MODIFIER * (newVal - orig) + orig;
- }
- }
-
-
- [HarmonyPatch(typeof(data_girls.girls), "GetAppealOfStat")]
- public class Data_girls_girls_GetAppealOfStat
- {
- // Girls with Trendy trait have 1.5x the appeal to non-adults and 0.5x the appeal to adults.
- public static void Postfix(ref float __result, resources.fanType _FanType, data_girls.girls __instance)
- {
- if (__instance.trait != traits._trait._type.Trendy)
- return;
-
- switch(_FanType)
- {
- case resources.fanType.adult:
- __result *= TRENDY_ADULT_MODIFIER;
- break;
- case resources.fanType.youngAdult:
- __result *= TRENDY_YA_MODIFIER;
- break;
- case resources.fanType.teen:
- __result *= TRENDY_TEEN_MODIFIER;
- break;
- }
- }
- }
-
- [HarmonyPatch(typeof(data_girls.girls), "UpdateDatingStatus")]
- public class Data_girls_girls_UpdateDatingStatus
- {
-
- // If there is an Indiscreet member, girls in dating relationships unknown to the player have a 2% chance
- // of having the relationship revealed each week
- public static void Postfix(ref data_girls.girls __instance)
- {
- if (mainScript.chance(INDISCREET_CHANCE) && __instance.DatingData.Is_Taken() && !__instance.DatingData.Is_Partner_Status_Known)
- {
- bool leaker = false;
- foreach (data_girls.girls girls in data_girls.GetActiveGirls())
- {
- if (girls != __instance && girls.trait == traits._trait._type.Indiscreet)
- {
- leaker = true;
- }
- }
- if (!leaker)
- return;
-
- string labelID = INDISCREET_LABEL_OUTSIDE;
- if (policies.GetSelectedPolicyValue(policies._type.dating).Value == policies._value.dating_forbidden)
- {
- labelID = INDISCREET_LABEL_OUTSIDE_SCANDAL;
- __instance.addParam(data_girls._paramType.scandalPoints, 1f, false);
- }
-
- NotificationManager.AddNotification(
- Language.Insert(labelID, new string[] { __instance.GetName() }),
- mainScript.red32,
- NotificationManager._notification._type.idol_relationship_change
- );
-
- __instance.getParam(data_girls._paramType.mentalStamina).add(-30f, false);
- __instance.DatingData.Is_Partner_Status_Known = true;
- __instance.DatingData.Partner_Status_Known_To_Player = __instance.DatingData.Partner_Status;
- }
- }
- }
-
- [HarmonyPatch(typeof(Relationships._relationship), "CheckDating")]
- public class Relationships__relationship_CheckDating
- {
- // If there is an Indiscreet member, girls in dating relationships unknown to the player have a 2% chance
- // of having the relationship revealed each week
- public static void Postfix(ref Relationships._relationship __instance)
- {
- if (mainScript.chance(INDISCREET_CHANCE) && __instance.Dating && !__instance.IsRelationshipKnown())
- {
- bool leaker = false;
- foreach (data_girls.girls girls in data_girls.GetActiveGirls(null))
- {
- if (girls != __instance.Girls[0] && girls != __instance.Girls[1] && girls.trait == traits._trait._type.Indiscreet)
- {
- leaker = true;
- }
- }
- if (!leaker)
- return;
-
- string labelID = INDISCREET_LABEL_OUTSIDE;
- if (policies.GetSelectedPolicyValue(policies._type.dating).Value == policies._value.dating_forbidden)
- {
- labelID = INDISCREET_LABEL_INSIDE;
- __instance.Girls[0].addParam(data_girls._paramType.scandalPoints, 1f, false);
- __instance.Girls[1].addParam(data_girls._paramType.scandalPoints, 1f, false);
- }
-
- NotificationManager.AddNotification(Language.Insert(labelID, new string[]
- {
- __instance.Girls[0].GetName(),
- __instance.Girls[1].GetName()
- }), mainScript.red32, NotificationManager._notification._type.idol_relationship_change);
-
- __instance.Girls[0].getParam(data_girls._paramType.mentalStamina).add(-30f, false);
- __instance.Girls[1].getParam(data_girls._paramType.mentalStamina).add(-30f, false);
- __instance.Girls[0].DatingData.Is_Partner_Status_Known = true;
- __instance.Girls[1].DatingData.Is_Partner_Status_Known = true;
- __instance.Girls[0].DatingData.Partner_Status_Known_To_Player = data_girls.girls._dating_data._partner_status.taken_idol;
- __instance.Girls[1].DatingData.Partner_Status_Known_To_Player = data_girls.girls._dating_data._partner_status.taken_idol;
- }
- }
- }
-
- // Maternal and Precocious default relationship is positive based on age criteria
- [HarmonyPatch(typeof(Relationships._relationship), "Initialize")]
- public class Relationships__relationship_Initialize
- {
- public static void Postfix(ref Relationships._relationship __instance)
- {
- int age0 = __instance.Girls[0].GetAge();
- int age1 = __instance.Girls[1].GetAge();
- if (__instance.Girls[0].trait == traits._trait._type.Maternal && age0 > age1)
- {
- __instance.Dynamic = Relationships._relationship._dynamic.positive;
- }
- else if (__instance.Girls[1].trait == traits._trait._type.Maternal && age1 > age0)
- {
- __instance.Dynamic = Relationships._relationship._dynamic.positive;
- }
- else if (__instance.Girls[0].trait == traits._trait._type.Precocious && age0 < age1)
- {
- __instance.Dynamic = Relationships._relationship._dynamic.positive;
- }
- else if (__instance.Girls[1].trait == traits._trait._type.Precocious && age1 < age0)
- {
- __instance.Dynamic = Relationships._relationship._dynamic.positive;
- }
- }
- }
-
- // Maternal and Precocious default relationship is positive based on age criteria
- [HarmonyPatch(typeof(Relationships), "Do_Dynamic")]
- public class Relationships_Do_Dynamic
- {
- public static void Postfix()
- {
- foreach (Relationships._relationship relationship in Relationships.RelationshipsData)
- {
- if (relationship.Dynamic == Relationships._relationship._dynamic.positive)
- {
- AdjustForAgeTraits(relationship);
- }
-
- // Get either last center of main group or last center of girl's group
- if (IsCenter(relationship.Girls[0])
- && relationship.Girls[0].trait == traits._trait._type.Arrogant)
- {
- relationship.Add(ARROGANT_PENALTY / 2);
- }
- else if(IsCenter(relationship.Girls[1])
- && relationship.Girls[1].trait == traits._trait._type.Arrogant)
- {
- relationship.Add(ARROGANT_PENALTY / 2);
- }
- }
- }
-
- private static void AdjustForAgeTraits(Relationships._relationship relationship)
- {
- // Maternal
- if (relationship.Girls[0].trait == traits._trait._type.Maternal && relationship.Girls[0].GetAge() > relationship.Girls[1].GetAge())
- {
- relationship.Add(MATERNAL_BONUS / 2);
- }
- else if (relationship.Girls[1].trait == traits._trait._type.Maternal && relationship.Girls[1].GetAge() > relationship.Girls[0].GetAge())
- {
- relationship.Add(MATERNAL_BONUS / 2);
- }
-
- // Precocious
- if (relationship.Girls[0].trait == traits._trait._type.Precocious && relationship.Girls[0].GetAge() < relationship.Girls[1].GetAge())
- {
- relationship.Add(PRECOCIOUS_BONUS / 2);
- }
- else if (relationship.Girls[1].trait == traits._trait._type.Precocious && relationship.Girls[1].GetAge() < relationship.Girls[0].GetAge())
- {
- relationship.Add(PRECOCIOUS_BONUS / 2);
- }
- }
- }
-
- // Girls with Forgiving trait will never dislike or hate any other girls
- [HarmonyPatch(typeof(Relationships._relationship), "Recalc")]
- public class Relationships__relationship_Recalc
- {
- public static void Postfix(ref Relationships._relationship __instance)
- {
- if (__instance.Ratio >= FORGIVING_THR)
- return;
-
- if (__instance.Girls[0].trait == traits._trait._type.Forgiving || __instance.Girls[1].trait == traits._trait._type.Forgiving)
- {
- __instance.Ratio = FORGIVING_THR;
- }
-
- }
- }
-
- // Girls with Meme Queen trait get +10 to all stats for internet shows
- [HarmonyPatch(typeof(Shows._show), "AddCastParam")]
- public class Shows__show_AddCastParam
- {
- public static void Postfix(data_girls._paramType type, List girlList, ref Shows._show __instance)
- {
- if (type == data_girls._paramType.teamChemistry)
- return;
-
- foreach (data_girls.girls girls in girlList)
- {
- if (girls != null
- && !girls.IsSick()
- && girls.trait == traits._trait._type.Meme_queen
- && __instance.medium.media_type == Shows._param._media_type.internet)
- {
- __instance.girlParams.Last().val += MEME_INT_SHOW;
- break;
- }
- }
- }
- }
-
- // Girls with Meme Queen trait get +10 to all stats for internet shows
- [HarmonyPatch(typeof(Show_Popup), "AddCastParam")]
- public class Show_Popup_AddCastParam
- {
- public static void Postfix(data_girls._paramType type, List girlList, ref List ___girlParams, Shows._param ___medium)
- {
- if (type == data_girls._paramType.teamChemistry)
- return;
-
- foreach (data_girls.girls girls in girlList)
- {
- if (girls != null
- && !girls.IsSick()
- && girls.trait == traits._trait._type.Meme_queen
- && ___medium.media_type == Shows._param._media_type.internet)
- {
- ___girlParams.Last().val += MEME_INT_SHOW;
- break;
- }
- }
- }
- }
-
- // Update shows on medium selection just in case of meme queen
- [HarmonyPatch(typeof(Show_Popup), "SetParam")]
- public class Show_Popup_SetParam
- {
- public static void Postfix(ref Show_Popup __instance, Shows._show._castType? ___castType)
- {
- if (___castType == null)
- return;
-
- __instance.SetCastType(___castType.Value);
- }
- }
-
- // Girls with Meme Queen trait get +10% success rate and +5% crit success rate when participating in viral marketing campaigns
- [HarmonyPatch(typeof(singles._param), "GetSuccessChance", new Type[] { typeof(Single_Marketing_Roll._result), typeof(int), typeof(singles._single) })]
- public class Singles__param_GetSuccessChance
- {
- public static void Postfix(ref float __result, singles._param __instance, Single_Marketing_Roll._result Result, singles._single Single)
- {
- if (Single == null || __instance.Special_Type != singles._param._special_type.viral_campaign)
- return;
-
- bool flag = false;
- foreach (data_girls.girls girls in Single.girls)
- {
- if (girls != null
- && !girls.IsSick()
- && girls.trait == traits._trait._type.Meme_queen)
- {
- flag = true;
- break;
- }
- }
- if (flag)
- {
- switch(Result)
- {
- case Single_Marketing_Roll._result.success:
- __result += MEME_VIRAL_SUCCESS;
- break;
- case Single_Marketing_Roll._result.success_crit:
- __result += MEME_VIRAL_SUCCESS_CRIT;
- break;
- case Single_Marketing_Roll._result.fail:
- __result += MEME_VIRAL_FAIL;
- break;
- }
- }
- }
- }
-
- // Girls with Annoying trait cause other members to spend 1.2x physical stamina in shows
- [HarmonyPatch(typeof(Shows._show), "SetStamina")]
- public class Shows__show_SetStamina
- {
- public static void Postfix(Shows._show __instance)
- {
- List cast = __instance.GetCast();
- float staminaCost = __instance.GetStaminaCost();
- int annoyCount = 0;
- foreach (data_girls.girls girls in cast)
- {
- if (girls != null
- && girls.trait == traits._trait._type.Annoying
- && girls.IsActive())
- {
- annoyCount++;
- }
- }
- if (annoyCount == 0)
- return;
-
- foreach (data_girls.girls girls2 in cast)
- {
- if (girls2.IsActive())
- {
- if (annoyCount > 1 || (annoyCount == 1 && girls2.trait != traits._trait._type.Annoying))
- {
- girls2.addParam(data_girls._paramType.physicalStamina, -staminaCost * ANNOYING_MODIFIER, false);
- }
- }
- }
- }
- }
-
- // Girls with Misandry trait have a 20% chance of receiving bad opinions from Male fans when participating in a single with handshakes
- [HarmonyPatch(typeof(singles), "ReleaseSingle")]
- public class Singles_ReleaseSingle
- {
- public static void Postfix(singles._single single)
- {
- foreach (data_girls.girls girls in single.girls)
- {
- if (girls != null
- && !girls.IsSick()
- && girls.trait == traits._trait._type.Misandry
- && (single.IsIndividualHS() || single.IsGroupHS())
- && mainScript.chance(20))
- {
- girls.AddAppeal(resources.fanType.male, MISANDRY_MODIFIER);
- }
- }
- }
- }
-
- // Girls with Perfectionist trait get -20 to mental stamina when world tours end with less than 80% average attendance
- [HarmonyPatch(typeof(SEvent_Tour), "FinishTour")]
- public class SEvent_Tour_FinishTour
- {
- public static void Postfix(SEvent_Tour __instance)
- {
- List activeGirls = data_girls.GetActiveGirls();
- foreach (data_girls.girls girls in activeGirls)
- {
- if (girls.trait == traits._trait._type.Perfectionist
- && __instance.Tour.GetAverageAttendance() < PERFECTIONIST_TOUR_ATT)
- {
- girls.getParam(data_girls._paramType.mentalStamina).add(PERFECTIONIST_MENTAL, false);
- }
- }
- }
- }
-
- // Girls with Perfectionist trait get -20 to mental stamina when they participate in concerts with less than 100% hype.
- [HarmonyPatch(typeof(SEvent_Concerts._concert), "Finish")]
- public class SEvent_Concerts__concert_Finish
- {
- public static void Postfix(SEvent_Concerts._concert __instance)
- {
- foreach (data_girls.girls girls in __instance.GetGirls(true))
- {
- if (girls.trait == traits._trait._type.Perfectionist
- && __instance.Hype < PERFECTIONIST_HYPE)
- {
- girls.getParam(data_girls._paramType.mentalStamina).add(PERFECTIONIST_MENTAL, false);
- }
- }
- }
- }
-
-
- // Apply traits to businesses
- [HarmonyPatch(typeof(business._proposal), "GetGirlCoeff")]
- public class Business__proposal_GetGirlCoeff
- {
- [HarmonyPriority(Priority.First)]
- public static void Prefix()
- {
- patchGetVal = true;
- }
-
- [HarmonyPriority(Priority.VeryLow)]
- public static void Postfix(data_girls.girls _girl, ref float __result, business._proposal __instance)
- {
- // Girls with Photogenic trait have +100% to photoshoots
- if (__instance.type == business._type.photoshoot
- && _girl.trait == traits._trait._type.Photogenic)
- {
- __result += PHOTOGENIC_MODIFIER;
- }
-
- patchGetVal = false;
- }
- }
-
-
- // Stat changes for shows
- [HarmonyPatch(typeof(data_girls), "GetAverageParam")]
- public class Data_girls_GetAverageParam
- {
- [HarmonyPriority(Priority.First)]
- public static void Prefix(List Girls)
- {
- patchGetVal = true;
- showCast = Girls;
- }
-
- [HarmonyPriority(Priority.VeryLow)]
- public static void Postfix()
- {
- patchGetVal = false;
- showCast = null;
- }
- }
-
- // Stat changes for shows (for team chemistray calc)
- [HarmonyPatch(typeof(Shows._show), "SenbatsuCalcParam")]
- public class Shows__show_SenbatsuCalcParam
- {
- [HarmonyPriority(Priority.First)]
- public static void Prefix(List _girls)
- {
- patchGetVal = true;
- showCast = _girls;
- }
-
- [HarmonyPriority(Priority.VeryLow)]
- public static void Postfix()
- {
- patchGetVal = false;
- showCast = null;
- }
- }
-
- // Stat changes for singles
- [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")]
- public class Singles__single_SenbatsuCalcParam
- {
- [HarmonyPriority(Priority.First)]
- public static void Prefix()
- {
- patchGetVal = true;
- }
-
- [HarmonyPriority(Priority.VeryLow)]
- public static void Postfix()
- {
- patchGetVal = false;
- }
- }
-
- // Stat changes for concert songs
- [HarmonyPatch(typeof(SEvent_Concerts._concert._song), "GetSkillValue")]
- public class SEvent_Concerts__concert__song_GetSkillValue
- {
- [HarmonyPriority(Priority.First)]
- public static void Prefix()
- {
- patchGetVal = true;
- }
-
- [HarmonyPriority(Priority.VeryLow)]
- public static void Postfix()
- {
- patchGetVal = false;
- }
- }
-
-
- // Stat changes for concert MCs
- [HarmonyPatch(typeof(SEvent_Concerts._concert._mc), "GetSkillValue")]
- public class SEvent_Concerts__concert__mc_GetSkillValue
- {
- [HarmonyPriority(Priority.First)]
- public static void Prefix()
- {
- patchGetVal = true;
- }
-
-
- [HarmonyPriority(Priority.VeryLow)]
- public static void Postfix()
- {
- patchGetVal = false;
- }
- }
-
- // Apply traits to parameters and skills
- [HarmonyPatch(typeof(data_girls.girls.param), "GetVal")]
- public class data_girls_girls_param_GetVal
- {
- public static void Postfix(ref float __result, data_girls.girls.param __instance)
- {
- if (!patchGetVal)
- return;
-
- __result += GetTraitModifier(__instance.Parent, __instance.type, showCast);
- }
- }
-
-
- public class TraitsFix
- {
- public const int ANXIETY_MODIFIER = -10;
- public const int CLUMSY_DANCE_MODIFIER = -30;
- public const int CLUMSY_FUNNY_MODIFIER = 30;
- public const int WORRIER_MODIFIER = -20;
- public const int COMPLACENT_MODIFIER = -20;
- public const int LONEWOLF_MODIFIER = 40;
- public const int DEFEATIST_MODIFIER = -20;
- public const int UNDERDOG_MODIFIER = 20;
- public const float PHOTOGENIC_MODIFIER = 1f;
- public const float PERFECTIONIST_MENTAL = -20;
- public const float PERFECTIONIST_HYPE = 100;
- public const float PERFECTIONIST_TOUR_ATT = 80;
- public const float MISANDRY_MODIFIER = -1;
- public const float ANNOYING_MODIFIER = 0.2f;
- public const float MEME_VIRAL_SUCCESS = 10;
- public const float MEME_VIRAL_SUCCESS_CRIT = 5;
- public const float MEME_VIRAL_FAIL = -15;
- public const float MEME_INT_SHOW = 10;
- public const float FORGIVING_THR = 0.5f;
- public const float MATERNAL_BONUS = 0.3f;
- public const float PRECOCIOUS_BONUS = 0.3f;
- public const float ARROGANT_PENALTY = -0.5f;
- public const int INDISCREET_CHANCE = 2;
- public const float TRENDY_ADULT_MODIFIER = 0.5f;
- public const float TRENDY_YA_MODIFIER = 1.5f;
- public const float TRENDY_TEEN_MODIFIER = 1.5f;
- public const float LIVEFAST_MODIFIER = 2;
- public const int LIVEFAST_DETERIORATION = 2;
-
- public const string INDISCREET_LABEL_OUTSIDE = "IDOL__OUTSIDE_LEAK";
- public const string INDISCREET_LABEL_INSIDE = "IDOL__INSIDE_LEAK_SCANDAL";
- public const string INDISCREET_LABEL_OUTSIDE_SCANDAL = "IDOL__OUTSIDE_LEAK_SCANDAL";
-
- public static bool patchGetVal = false;
- public static bool patchSetVal = false;
- public static bool patchSet = false;
-
- public static List showCast = null;
-
- // This method calculates the modifier to girl parameters based on their trait.
- public static int GetTraitModifier(data_girls.girls girls, data_girls._paramType type, List cast = null)
- {
- if (girls != null && data_girls.IsStatParam(type))
- {
- switch(girls.trait)
- {
- case traits._trait._type.Anxiety:
- if (IsEventUpcoming())
- return ANXIETY_MODIFIER;
- break;
- case traits._trait._type.Clumsy:
- if (type == data_girls._paramType.dance)
- return CLUMSY_DANCE_MODIFIER;
-
- if (type == data_girls._paramType.funny)
- return CLUMSY_FUNNY_MODIFIER;
- break;
- case traits._trait._type.Worrier:
- if (resources.GetScandalPointsTotal() > 0L)
- return WORRIER_MODIFIER;
- break;
- case traits._trait._type.Complacent:
- if (IsCenter(girls)
- && (type == data_girls._paramType.vocal || type == data_girls._paramType.dance))
- return COMPLACENT_MODIFIER;
- break;
- case traits._trait._type.Lone_Wolf:
- if (cast != null)
- {
- int count = 0;
- foreach (data_girls.girls g in cast)
- {
- if (g != null && !g.IsSick())
- count++;
-
- if (count > 1) break;
- }
- if (count == 1)
- return LONEWOLF_MODIFIER;
- }
- break;
- }
-
- singles._single mainSingle = singles.GetLatestReleasedSingle(false, Groups.GetMainGroup());
- singles._single groupSingle = singles.GetLatestReleasedSingle(false, girls.GetGroup());
-
- singles._single recentSingle = GetRecentSingle(groupSingle, mainSingle);
-
- if (recentSingle != null
- && (staticVars.dateTime - recentSingle.ReleaseData.ReleaseDate).Days >= staticVars.dateTime.Day
- && recentSingle.ReleaseData.Chart_Position != 1)
- {
- if (girls.trait == traits._trait._type.Defeatist)
- {
- return DEFEATIST_MODIFIER;
- }
- else if (girls.trait == traits._trait._type.Underdog)
- {
- return UNDERDOG_MODIFIER;
- }
- }
- }
- return 0;
- }
-
- public static bool IsCenter(data_girls.girls girl)
- {
- singles._single mainSingle = singles.GetLatestReleasedSingle(false, Groups.GetMainGroup());
- singles._single groupSingle = singles.GetLatestReleasedSingle(false, girl.GetGroup());
-
- return (groupSingle?.GetCenter() == girl) || (mainSingle?.GetCenter() == girl);
- }
-
- private static bool IsEventUpcoming()
- {
- foreach (var tour in SEvent_Tour.Tours)
- {
- if (tour.Status != SEvent_Tour.tour._status.finished)
- {
- return true;
- }
- }
- foreach (var sSK in SEvent_SSK.Elections)
- {
- if (sSK.Status != SEvent_Tour.tour._status.finished)
- {
- return true;
- }
- }
- foreach (var concert in SEvent_Concerts.Concerts)
- {
- if (concert.Status != SEvent_Tour.tour._status.finished)
- {
- return true;
- }
- }
- return false;
- }
-
- // Helper method to get the more recent single based on release date and sales
- private static singles._single GetRecentSingle(singles._single groupSingle, singles._single mainSingle)
- {
- if (groupSingle == null) return mainSingle;
- if (mainSingle == null) return groupSingle;
-
- if (groupSingle.ReleaseData.ReleaseDate > mainSingle.ReleaseData.ReleaseDate)
- {
- return groupSingle;
- }
- else if (groupSingle.ReleaseData.ReleaseDate < mainSingle.ReleaseData.ReleaseDate)
- {
- return mainSingle;
- }
- else if (groupSingle.ReleaseData.Sales > mainSingle.ReleaseData.Sales)
- {
- return groupSingle;
- }
- else
- {
- return mainSingle;
- }
- }
- }
-
-}
+using HarmonyLib;
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using static TraitFix.TraitsFix;
+
+namespace TraitFix
+{
+ [HarmonyPatch(typeof(data_girls), "AgeDeterioration")]
+ public class Data_girls_AgeDeterioration
+ {
+ // Live Fast: double random post-peak deterioration.
+ public static void Postfix()
+ {
+ if (data_girls.girl == null)
+ return;
+
+ foreach (data_girls.girls girl in data_girls.girl)
+ {
+ if (girl == null || girl.status == data_girls._status.graduated || girl.trait != traits._trait._type.Live_fast)
+ continue;
+
+ for (int i = 0; i < LIVEFAST_DETERIORATION - 1; i++)
+ girl.AgeDeterioration();
+ }
+ }
+ }
+
+ // Live Fast birthday deterioration without a compiler-local-dependent transpiler.
+ [HarmonyPatch(typeof(Birthday_Popup), "DoParam")]
+ public class Birthday_Popup_DoParam
+ {
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix(Birthday_Popup __instance, data_girls._paramType Prm, ref bool __state)
+ {
+ __state = BeginBirthdayDeterioration(__instance?.Girl, Prm);
+ }
+
+ public static Exception Finalizer(Exception __exception, bool __state)
+ {
+ if (__state)
+ EndBirthdayDeterioration();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls.girls.param), "setVal")]
+ public class data_girls_girls_param_setVal_Birthday
+ {
+ [HarmonyPriority(Priority.VeryLow)]
+ public static void Prefix(data_girls.girls.param __instance, ref float newVal)
+ {
+ if (__instance == null)
+ return;
+ newVal = AdjustBirthdayDeterioration(__instance.Parent, __instance.type, newVal);
+ }
+ }
+
+ [HarmonyPatch(typeof(Birthday_Stat), "Set")]
+ public class Birthday_Stat_Set
+ {
+ [HarmonyPriority(Priority.VeryLow)]
+ public static void Prefix(data_girls._paramType Type, float OldVal, ref float NewVal)
+ {
+ NewVal = AdjustBirthdayDeteriorationDisplay(Type, OldVal, NewVal);
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls.girls), "GetAppealOfStat")]
+ public class Data_girls_girls_GetAppealOfStat
+ {
+ public static void Postfix(ref float __result, resources.fanType _FanType, data_girls.girls __instance)
+ {
+ if (__instance == null || __instance.trait != traits._trait._type.Trendy)
+ return;
+
+ switch (_FanType)
+ {
+ case resources.fanType.adult:
+ __result *= TRENDY_ADULT_MODIFIER;
+ break;
+ case resources.fanType.youngAdult:
+ __result *= TRENDY_YA_MODIFIER;
+ break;
+ case resources.fanType.teen:
+ __result *= TRENDY_TEEN_MODIFIER;
+ break;
+ }
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls.girls), "UpdateDatingStatus")]
+ public class Data_girls_girls_UpdateDatingStatus
+ {
+ // Outside relationships get one reveal roll; idol-idol dating is handled by CheckDating below.
+ public static void Postfix(data_girls.girls __instance)
+ {
+ if (__instance?.DatingData == null
+ || !__instance.DatingData.Is_Taken_Outside()
+ || __instance.DatingData.Is_Partner_Status_Known
+ || !mainScript.chance(INDISCREET_CHANCE)
+ || !HasIndiscreetLeaker(__instance))
+ {
+ return;
+ }
+
+ string labelID = INDISCREET_LABEL_OUTSIDE;
+ if (IsDatingForbidden())
+ {
+ labelID = INDISCREET_LABEL_OUTSIDE_SCANDAL;
+ __instance.addParam(data_girls._paramType.scandalPoints, 1f, false);
+ }
+
+ NotificationManager.AddNotification(
+ Language.Insert(labelID, new string[] { __instance.GetName() }),
+ mainScript.red32,
+ NotificationManager._notification._type.idol_relationship_change);
+
+ __instance.getParam(data_girls._paramType.mentalStamina)?.add(-30f, false);
+ __instance.DatingData.Is_Partner_Status_Known = true;
+ __instance.DatingData.Partner_Status_Known_To_Player = __instance.DatingData.Partner_Status;
+ }
+ }
+
+ [HarmonyPatch(typeof(Relationships._relationship), "CheckDating")]
+ public class Relationships__relationship_CheckDating
+ {
+ public static void Postfix(Relationships._relationship __instance)
+ {
+ if (!TryGetRelationshipGirls(__instance, out data_girls.girls girl0, out data_girls.girls girl1) || !__instance.Dating)
+ return;
+
+ // Repair old one-sided knowledge and prevent the same relationship leaking every week.
+ if (girl0.DatingData.Is_Partner_Status_Known || girl1.DatingData.Is_Partner_Status_Known)
+ {
+ MarkIdolRelationshipKnown(girl0, girl1);
+ return;
+ }
+ if (__instance.IsRelationshipKnown())
+ return;
+
+ if (!mainScript.chance(INDISCREET_CHANCE) || !HasIndiscreetLeaker(girl0, girl1))
+ return;
+
+ string labelID = INDISCREET_LABEL_OUTSIDE;
+ if (IsDatingForbidden())
+ {
+ labelID = INDISCREET_LABEL_INSIDE;
+ girl0.addParam(data_girls._paramType.scandalPoints, 1f, false);
+ girl1.addParam(data_girls._paramType.scandalPoints, 1f, false);
+ }
+
+ NotificationManager.AddNotification(
+ Language.Insert(labelID, new string[] { girl0.GetName(), girl1.GetName() }),
+ mainScript.red32,
+ NotificationManager._notification._type.idol_relationship_change);
+
+ girl0.getParam(data_girls._paramType.mentalStamina)?.add(-30f, false);
+ girl1.getParam(data_girls._paramType.mentalStamina)?.add(-30f, false);
+ MarkIdolRelationshipKnown(girl0, girl1);
+ }
+ }
+
+ [HarmonyPatch(typeof(Relationships._relationship), "Initialize")]
+ public class Relationships__relationship_Initialize
+ {
+ public static void Postfix(Relationships._relationship __instance)
+ {
+ if (!TryGetRelationshipGirls(__instance, out data_girls.girls girl0, out data_girls.girls girl1))
+ return;
+
+ int age0 = girl0.GetAge();
+ int age1 = girl1.GetAge();
+ if (girl0.trait == traits._trait._type.Maternal && age0 > age1)
+ __instance.Dynamic = Relationships._relationship._dynamic.positive;
+ else if (girl1.trait == traits._trait._type.Maternal && age1 > age0)
+ __instance.Dynamic = Relationships._relationship._dynamic.positive;
+ else if (girl0.trait == traits._trait._type.Precocious && age0 < age1)
+ __instance.Dynamic = Relationships._relationship._dynamic.positive;
+ else if (girl1.trait == traits._trait._type.Precocious && age1 < age0)
+ __instance.Dynamic = Relationships._relationship._dynamic.positive;
+ }
+ }
+
+ [HarmonyPatch(typeof(Relationships), "Do_Dynamic")]
+ public class Relationships_Do_Dynamic
+ {
+ public static void Postfix()
+ {
+ if (Relationships.RelationshipsData == null)
+ return;
+
+ foreach (Relationships._relationship relationship in Relationships.RelationshipsData)
+ {
+ if (!TryGetRelationshipGirls(relationship, out data_girls.girls girl0, out data_girls.girls girl1))
+ continue;
+
+ if (relationship.Dynamic == Relationships._relationship._dynamic.positive)
+ AdjustForAgeTraits(relationship, girl0, girl1);
+
+ if (IsCenter(girl0) && girl0.trait == traits._trait._type.Arrogant)
+ relationship.Add(ARROGANT_PENALTY / 2f);
+ else if (IsCenter(girl1) && girl1.trait == traits._trait._type.Arrogant)
+ relationship.Add(ARROGANT_PENALTY / 2f);
+ }
+ }
+
+ private static void AdjustForAgeTraits(Relationships._relationship relationship, data_girls.girls girl0, data_girls.girls girl1)
+ {
+ // Relationship.Add halves positive values. Vanilla already contributes +0.05;
+ // Add(0.3) contributes +0.15 more, producing the advertised 4x (+0.20 total).
+ if (girl0.trait == traits._trait._type.Maternal && girl0.GetAge() > girl1.GetAge())
+ relationship.Add(MATERNAL_BONUS);
+ else if (girl1.trait == traits._trait._type.Maternal && girl1.GetAge() > girl0.GetAge())
+ relationship.Add(MATERNAL_BONUS);
+
+ if (girl0.trait == traits._trait._type.Precocious && girl0.GetAge() < girl1.GetAge())
+ relationship.Add(PRECOCIOUS_BONUS);
+ else if (girl1.trait == traits._trait._type.Precocious && girl1.GetAge() < girl0.GetAge())
+ relationship.Add(PRECOCIOUS_BONUS);
+ }
+ }
+
+ [HarmonyPatch(typeof(Relationships._relationship), "Recalc")]
+ public class Relationships__relationship_Recalc
+ {
+ public static void Postfix(Relationships._relationship __instance)
+ {
+ if (!TryGetRelationshipGirls(__instance, out data_girls.girls girl0, out data_girls.girls girl1)
+ || __instance.Ratio >= FORGIVING_THR)
+ return;
+
+ if (girl0.trait == traits._trait._type.Forgiving || girl1.trait == traits._trait._type.Forgiving)
+ __instance.Ratio = FORGIVING_THR;
+ }
+ }
+
+ [HarmonyPatch(typeof(Shows._show), "AddCastParam")]
+ public class Shows__show_AddCastParam
+ {
+ public static void Postfix(data_girls._paramType type, List girlList, Shows._show __instance)
+ {
+ if (type == data_girls._paramType.teamChemistry
+ || __instance?.medium == null
+ || __instance.medium.media_type != Shows._param._media_type.internet
+ || !HasActiveMemeQueen(girlList)
+ || __instance.girlParams == null
+ || __instance.girlParams.Count == 0)
+ return;
+
+ data_girls.girls.param param = __instance.girlParams[__instance.girlParams.Count - 1];
+ if (param != null && param.type == type)
+ param.val += MEME_INT_SHOW;
+ }
+ }
+
+ [HarmonyPatch(typeof(Show_Popup), "AddCastParam")]
+ public class Show_Popup_AddCastParam
+ {
+ public static void Postfix(data_girls._paramType type, List girlList, List ___girlParams, Shows._param ___medium)
+ {
+ if (type == data_girls._paramType.teamChemistry
+ || ___medium == null
+ || ___medium.media_type != Shows._param._media_type.internet
+ || !HasActiveMemeQueen(girlList)
+ || ___girlParams == null
+ || ___girlParams.Count == 0)
+ return;
+
+ data_girls.girls.param param = ___girlParams[___girlParams.Count - 1];
+ if (param != null && param.type == type)
+ param.val += MEME_INT_SHOW;
+ }
+ }
+
+ // Recalculate only when the medium changes, avoiding a recalculation on every SetParam.
+ [HarmonyPatch(typeof(Show_Popup), "SetParam")]
+ public class Show_Popup_SetParam
+ {
+ public static void Postfix(Show_Popup __instance, Show_Popup_Param_Button._type type, Shows._show._castType? ___castType)
+ {
+ if (__instance == null || type != Show_Popup_Param_Button._type.medium || ___castType == null)
+ return;
+ __instance.SetCastType(___castType.Value);
+ }
+ }
+
+ [HarmonyPatch(typeof(singles._param), "GetSuccessChance", new Type[] { typeof(Single_Marketing_Roll._result), typeof(int), typeof(singles._single) })]
+ public class Singles__param_GetSuccessChance
+ {
+ public static void Postfix(ref float __result, singles._param __instance, Single_Marketing_Roll._result Result, singles._single Single)
+ {
+ if (__instance == null || Single?.girls == null || __instance.Special_Type != singles._param._special_type.viral_campaign)
+ return;
+
+ bool hasMemeQueen = false;
+ foreach (data_girls.girls girl in Single.girls)
+ {
+ if (girl != null && !girl.IsSick() && girl.trait == traits._trait._type.Meme_queen)
+ {
+ hasMemeQueen = true;
+ break;
+ }
+ }
+ if (!hasMemeQueen)
+ return;
+
+ // Do not patch fail directly: vanilla derives regular fail chance from the
+ // success/crit chances, so subtracting 15 from fail double-counted this bonus.
+ if (Result == Single_Marketing_Roll._result.success)
+ __result += MEME_VIRAL_SUCCESS;
+ else if (Result == Single_Marketing_Roll._result.success_crit)
+ __result += MEME_VIRAL_SUCCESS_CRIT;
+ }
+ }
+
+ [HarmonyPatch(typeof(Shows._show), "SetStamina")]
+ public class Shows__show_SetStamina
+ {
+ public static void Postfix(Shows._show __instance)
+ {
+ List cast = __instance?.GetCast();
+ if (cast == null || cast.Count == 0)
+ return;
+
+ float staminaCost = __instance.GetStaminaCost();
+ int annoyingCount = 0;
+ foreach (data_girls.girls girl in cast)
+ {
+ if (girl != null && girl.trait == traits._trait._type.Annoying && girl.IsActive())
+ annoyingCount++;
+ }
+ if (annoyingCount == 0)
+ return;
+
+ foreach (data_girls.girls girl in cast)
+ {
+ if (girl == null || !girl.IsActive())
+ continue;
+ if (annoyingCount > 1 || girl.trait != traits._trait._type.Annoying)
+ girl.addParam(data_girls._paramType.physicalStamina, -staminaCost * ANNOYING_MODIFIER, false);
+ }
+ }
+ }
+
+ [HarmonyPatch(typeof(singles), "ReleaseSingle")]
+ public class Singles_ReleaseSingle
+ {
+ public static void Postfix(singles._single single)
+ {
+ if (single?.girls == null)
+ return;
+
+ bool hasHandshake = single.IsIndividualHS() || single.IsGroupHS();
+ if (!hasHandshake)
+ return;
+
+ foreach (data_girls.girls girl in single.girls)
+ {
+ if (girl != null && !girl.IsSick() && girl.trait == traits._trait._type.Misandry && mainScript.chance(20))
+ girl.AddAppeal(resources.fanType.male, MISANDRY_MODIFIER);
+ }
+ }
+ }
+
+ // FinishTour clears Tour before returning, so capture the attendance result first.
+ [HarmonyPatch(typeof(SEvent_Tour), "FinishTour")]
+ public class SEvent_Tour_FinishTour
+ {
+ public static void Prefix(SEvent_Tour __instance, ref bool __state)
+ {
+ __state = __instance?.Tour != null && __instance.Tour.GetAverageAttendance() < PERFECTIONIST_TOUR_ATT;
+ }
+
+ public static void Postfix(bool __state)
+ {
+ if (!__state)
+ return;
+ List activeGirls = data_girls.GetActiveGirls();
+ if (activeGirls == null)
+ return;
+
+ foreach (data_girls.girls girl in activeGirls)
+ {
+ if (girl != null && girl.trait == traits._trait._type.Perfectionist)
+ girl.getParam(data_girls._paramType.mentalStamina)?.add(PERFECTIONIST_MENTAL, false);
+ }
+ }
+ }
+
+ [HarmonyPatch(typeof(SEvent_Concerts._concert), "Finish")]
+ public class SEvent_Concerts__concert_Finish
+ {
+ public static void Postfix(SEvent_Concerts._concert __instance)
+ {
+ if (__instance == null || __instance.Hype >= PERFECTIONIST_HYPE)
+ return;
+ List girls = __instance.GetGirls(true);
+ if (girls == null)
+ return;
+
+ foreach (data_girls.girls girl in girls)
+ {
+ if (girl != null && girl.trait == traits._trait._type.Perfectionist)
+ girl.getParam(data_girls._paramType.mentalStamina)?.add(PERFECTIONIST_MENTAL, false);
+ }
+ }
+ }
+
+ // Trait stat contexts use a stack plus Finalizers so nested calls and exceptions cannot poison later GetVal calls.
+ [HarmonyPatch(typeof(business._proposal), "GetGirlCoeff")]
+ public class Business__proposal_GetGirlCoeff
+ {
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix() => BeginTraitCalculation();
+
+ public static void Postfix(data_girls.girls _girl, ref float __result, business._proposal __instance)
+ {
+ if (__instance != null && _girl != null && __instance.type == business._type.photoshoot && _girl.trait == traits._trait._type.Photogenic)
+ __result += PHOTOGENIC_MODIFIER;
+ }
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ EndTraitCalculation();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls), "GetAverageParam")]
+ public class Data_girls_GetAverageParam
+ {
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix(List Girls) => BeginTraitCalculation(Girls);
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ EndTraitCalculation();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(Shows._show), "SenbatsuCalcParam")]
+ public class Shows__show_SenbatsuCalcParam
+ {
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix(List _girls) => BeginTraitCalculation(_girls);
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ EndTraitCalculation();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")]
+ public class Singles__single_SenbatsuCalcParam
+ {
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix() => BeginTraitCalculation();
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ EndTraitCalculation();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(SEvent_Concerts._concert._song), "GetSkillValue")]
+ public class SEvent_Concerts__concert__song_GetSkillValue
+ {
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix() => BeginTraitCalculation();
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ EndTraitCalculation();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(SEvent_Concerts._concert._mc), "GetSkillValue")]
+ public class SEvent_Concerts__concert__mc_GetSkillValue
+ {
+ [HarmonyPriority(Priority.First)]
+ public static void Prefix() => BeginTraitCalculation();
+
+ public static Exception Finalizer(Exception __exception)
+ {
+ EndTraitCalculation();
+ return __exception;
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls.girls.param), "GetVal")]
+ public class data_girls_girls_param_GetVal
+ {
+ public static void Postfix(ref float __result, data_girls.girls.param __instance)
+ {
+ if (!IsTraitCalculationActive || __instance == null)
+ return;
+ __result += GetTraitModifier(__instance.Parent, __instance.type, CurrentTraitCast);
+ }
+ }
+
+ public class TraitsFix
+ {
+ public const int ANXIETY_MODIFIER = -10;
+ public const int CLUMSY_DANCE_MODIFIER = -30;
+ public const int CLUMSY_FUNNY_MODIFIER = 30;
+ public const int WORRIER_MODIFIER = -20;
+ public const int COMPLACENT_MODIFIER = -20;
+ public const int LONEWOLF_MODIFIER = 40;
+ public const int DEFEATIST_MODIFIER = -20;
+ public const int UNDERDOG_MODIFIER = 20;
+ public const float PHOTOGENIC_MODIFIER = 1f;
+ public const float PERFECTIONIST_MENTAL = -20;
+ public const float PERFECTIONIST_HYPE = 100;
+ public const float PERFECTIONIST_TOUR_ATT = 80;
+ public const float MISANDRY_MODIFIER = -1;
+ public const float ANNOYING_MODIFIER = 0.2f;
+ public const float MEME_VIRAL_SUCCESS = 10;
+ public const float MEME_VIRAL_SUCCESS_CRIT = 5;
+ public const float MEME_INT_SHOW = 10;
+ public const float FORGIVING_THR = 0.5f;
+ public const float MATERNAL_BONUS = 0.3f;
+ public const float PRECOCIOUS_BONUS = 0.3f;
+ public const float ARROGANT_PENALTY = -0.5f;
+ public const int INDISCREET_CHANCE = 2;
+ public const float TRENDY_ADULT_MODIFIER = 0.5f;
+ public const float TRENDY_YA_MODIFIER = 1.5f;
+ public const float TRENDY_TEEN_MODIFIER = 1.5f;
+ public const float LIVEFAST_MODIFIER = 2f;
+ public const int LIVEFAST_DETERIORATION = 2;
+
+ public const string INDISCREET_LABEL_OUTSIDE = "IDOL__OUTSIDE_LEAK";
+ public const string INDISCREET_LABEL_INSIDE = "IDOL__INSIDE_LEAK_SCANDAL";
+ public const string INDISCREET_LABEL_OUTSIDE_SCANDAL = "IDOL__OUTSIDE_LEAK_SCANDAL";
+
+ private sealed class TraitCalculationContext
+ {
+ public List Cast;
+ }
+
+ private sealed class BirthdayDeteriorationContext
+ {
+ public data_girls.girls Girl;
+ public data_girls._paramType Type;
+ public float OldValue;
+ }
+
+ private static readonly Stack traitCalculationContexts = new Stack();
+ private static readonly Stack birthdayDeteriorationContexts = new Stack();
+
+ public static bool IsTraitCalculationActive => traitCalculationContexts.Count > 0;
+ public static List CurrentTraitCast => IsTraitCalculationActive ? traitCalculationContexts.Peek().Cast : null;
+
+ public static int GetTraitModifier(data_girls.girls girl, data_girls._paramType type, List cast = null)
+ {
+ if (girl == null || !data_girls.IsStatParam(type))
+ return 0;
+
+ switch (girl.trait)
+ {
+ case traits._trait._type.Anxiety:
+ if (IsEventUpcoming()) return ANXIETY_MODIFIER;
+ break;
+ case traits._trait._type.Clumsy:
+ if (type == data_girls._paramType.dance) return CLUMSY_DANCE_MODIFIER;
+ if (type == data_girls._paramType.funny) return CLUMSY_FUNNY_MODIFIER;
+ break;
+ case traits._trait._type.Worrier:
+ if (resources.GetScandalPointsTotal() > 0L) return WORRIER_MODIFIER;
+ break;
+ case traits._trait._type.Complacent:
+ if (IsCenter(girl) && (type == data_girls._paramType.vocal || type == data_girls._paramType.dance))
+ return COMPLACENT_MODIFIER;
+ break;
+ case traits._trait._type.Lone_Wolf:
+ if (cast != null)
+ {
+ int count = 0;
+ foreach (data_girls.girls castGirl in cast)
+ {
+ if (castGirl != null && castGirl.IsActive() && !castGirl.IsSick())
+ count++;
+ if (count > 1) break;
+ }
+ if (count == 1) return LONEWOLF_MODIFIER;
+ }
+ break;
+ }
+
+ if (girl.trait == traits._trait._type.Defeatist || girl.trait == traits._trait._type.Underdog)
+ {
+ singles._single mainSingle = singles.GetLatestReleasedSingle(false, Groups.GetMainGroup());
+ Groups._group girlGroup = girl.GetGroup();
+ singles._single groupSingle = girlGroup != null ? singles.GetLatestReleasedSingle(false, girlGroup) : null;
+ singles._single recentSingle = GetRecentSingle(groupSingle, mainSingle);
+ if (DidSingleMissNumberOne(recentSingle))
+ return girl.trait == traits._trait._type.Defeatist ? DEFEATIST_MODIFIER : UNDERDOG_MODIFIER;
+ }
+
+ return 0;
+ }
+
+ public static bool IsCenter(data_girls.girls girl)
+ {
+ if (girl == null)
+ return false;
+ singles._single mainSingle = singles.GetLatestReleasedSingle(false, Groups.GetMainGroup());
+ Groups._group girlGroup = girl.GetGroup();
+ singles._single groupSingle = girlGroup != null ? singles.GetLatestReleasedSingle(false, girlGroup) : null;
+ return groupSingle?.GetCenter() == girl || mainSingle?.GetCenter() == girl;
+ }
+
+ public static void BeginTraitCalculation(List cast = null)
+ {
+ traitCalculationContexts.Push(new TraitCalculationContext { Cast = cast });
+ }
+
+ public static void EndTraitCalculation()
+ {
+ if (traitCalculationContexts.Count > 0)
+ traitCalculationContexts.Pop();
+ }
+
+ public static bool BeginBirthdayDeterioration(data_girls.girls girl, data_girls._paramType type)
+ {
+ if (girl == null
+ || girl.trait != traits._trait._type.Live_fast
+ || girl.GetAge() <= girl.peakAge
+ || type == data_girls._paramType.funny
+ || type == data_girls._paramType.smart)
+ return false;
+
+ data_girls.girls.param param = girl.getParam(type);
+ if (param == null)
+ return false;
+
+ birthdayDeteriorationContexts.Push(new BirthdayDeteriorationContext
+ {
+ Girl = girl,
+ Type = type,
+ OldValue = param.val
+ });
+ return true;
+ }
+
+ public static void EndBirthdayDeterioration()
+ {
+ if (birthdayDeteriorationContexts.Count > 0)
+ birthdayDeteriorationContexts.Pop();
+ }
+
+ public static float AdjustBirthdayDeterioration(data_girls.girls girl, data_girls._paramType type, float newValue)
+ {
+ if (birthdayDeteriorationContexts.Count == 0)
+ return newValue;
+ BirthdayDeteriorationContext context = birthdayDeteriorationContexts.Peek();
+ if (context.Girl != girl || context.Type != type || newValue >= context.OldValue)
+ return newValue;
+ return Mathf.Clamp(context.OldValue + LIVEFAST_MODIFIER * (newValue - context.OldValue), 1f, 100f);
+ }
+
+ public static float AdjustBirthdayDeteriorationDisplay(data_girls._paramType type, float oldValue, float newValue)
+ {
+ if (birthdayDeteriorationContexts.Count == 0)
+ return newValue;
+ BirthdayDeteriorationContext context = birthdayDeteriorationContexts.Peek();
+ if (context.Type != type || newValue >= oldValue)
+ return newValue;
+ return Mathf.Clamp(oldValue + LIVEFAST_MODIFIER * (newValue - oldValue), 1f, 100f);
+ }
+
+ public static bool IsDatingForbidden()
+ {
+ policies.value datingPolicy = policies.GetSelectedPolicyValue(policies._type.dating);
+ return datingPolicy != null && datingPolicy.Value == policies._value.dating_forbidden;
+ }
+
+ public static bool HasActiveMemeQueen(List girls)
+ {
+ if (girls == null)
+ return false;
+ foreach (data_girls.girls girl in girls)
+ {
+ if (girl != null && girl.IsActive() && !girl.IsSick() && girl.trait == traits._trait._type.Meme_queen)
+ return true;
+ }
+ return false;
+ }
+
+ public static bool HasIndiscreetLeaker(params data_girls.girls[] excludedGirls)
+ {
+ List activeGirls = data_girls.GetActiveGirls(null);
+ if (activeGirls == null)
+ return false;
+
+ foreach (data_girls.girls girl in activeGirls)
+ {
+ if (girl == null || girl.trait != traits._trait._type.Indiscreet)
+ continue;
+ bool excluded = false;
+ if (excludedGirls != null)
+ {
+ foreach (data_girls.girls excludedGirl in excludedGirls)
+ {
+ if (girl == excludedGirl)
+ {
+ excluded = true;
+ break;
+ }
+ }
+ }
+ if (!excluded)
+ return true;
+ }
+ return false;
+ }
+
+ public static bool TryGetRelationshipGirls(Relationships._relationship relationship, out data_girls.girls girl0, out data_girls.girls girl1)
+ {
+ girl0 = null;
+ girl1 = null;
+ if (relationship?.Girls == null || relationship.Girls.Count < 2)
+ return false;
+ girl0 = relationship.Girls[0];
+ girl1 = relationship.Girls[1];
+ return girl0 != null && girl1 != null && girl0.DatingData != null && girl1.DatingData != null;
+ }
+
+ public static void MarkIdolRelationshipKnown(data_girls.girls girl0, data_girls.girls girl1)
+ {
+ if (girl0?.DatingData == null || girl1?.DatingData == null)
+ return;
+ girl0.DatingData.Is_Partner_Status_Known = true;
+ girl1.DatingData.Is_Partner_Status_Known = true;
+ girl0.DatingData.Partner_Status_Known_To_Player = data_girls.girls._dating_data._partner_status.taken_idol;
+ girl1.DatingData.Partner_Status_Known_To_Player = data_girls.girls._dating_data._partner_status.taken_idol;
+ }
+
+ private static bool DidSingleMissNumberOne(singles._single single)
+ {
+ if (single?.ReleaseData == null)
+ return false;
+
+ DateTime releaseDate = single.ReleaseData.ReleaseDate;
+ if (releaseDate.Year == staticVars.dateTime.Year && releaseDate.Month == staticVars.dateTime.Month)
+ return false;
+
+ int chartPosition = single.ReleaseData.Chart_Position;
+ if (chartPosition <= 0)
+ chartPosition = ResolveChartPosition(single);
+ return chartPosition > 1;
+ }
+
+ private static int ResolveChartPosition(singles._single single)
+ {
+ if (single?.ReleaseData == null || Rivals.Date_To_Month == null)
+ return 0;
+
+ // Player singles released in a month are part of that month's chart data.
+ DateTime releaseMonth = single.ReleaseData.ReleaseDate;
+ foreach (Rivals._date_to_month_id month in Rivals.Date_To_Month)
+ {
+ if (month == null || month.Date.Year != releaseMonth.Year || month.Date.Month != releaseMonth.Month)
+ continue;
+
+ List chartSingles = Rivals.GetSingles(month.ID);
+ if (chartSingles == null)
+ return 0;
+ for (int i = 0; i < chartSingles.Count; i++)
+ {
+ Rivals._group._single chartSingle = chartSingles[i];
+ if (chartSingle != null && chartSingle.Player && chartSingle.SingleID == single.id)
+ return i + 1;
+ }
+ return 0;
+ }
+ return 0;
+ }
+
+ private static bool IsEventUpcoming()
+ {
+ if (SEvent_Tour.Tours != null)
+ {
+ foreach (SEvent_Tour.tour tour in SEvent_Tour.Tours)
+ if (tour != null && tour.Status != SEvent_Tour.tour._status.finished) return true;
+ }
+ if (SEvent_SSK.Elections != null)
+ {
+ foreach (SEvent_SSK._SSK election in SEvent_SSK.Elections)
+ if (election != null && election.Status != SEvent_Tour.tour._status.finished) return true;
+ }
+ if (SEvent_Concerts.Concerts != null)
+ {
+ foreach (SEvent_Concerts._concert concert in SEvent_Concerts.Concerts)
+ if (concert != null && concert.Status != SEvent_Tour.tour._status.finished) return true;
+ }
+ return false;
+ }
+
+ private static singles._single GetRecentSingle(singles._single groupSingle, singles._single mainSingle)
+ {
+ if (groupSingle?.ReleaseData == null) return mainSingle?.ReleaseData != null ? mainSingle : null;
+ if (mainSingle?.ReleaseData == null) return groupSingle;
+ if (groupSingle.ReleaseData.ReleaseDate > mainSingle.ReleaseData.ReleaseDate) return groupSingle;
+ if (groupSingle.ReleaseData.ReleaseDate < mainSingle.ReleaseData.ReleaseDate) return mainSingle;
+ return groupSingle.ReleaseData.Sales > mainSingle.ReleaseData.Sales ? groupSingle : mainSingle;
+ }
+ }
+}
diff --git a/mods/Traits Fix/Traits Fix.csproj b/mods/Traits Fix/Traits Fix.csproj
index 408b182..a46c82d 100755
--- a/mods/Traits Fix/Traits Fix.csproj
+++ b/mods/Traits Fix/Traits Fix.csproj
@@ -1,27 +1,27 @@
-
-
-
- Traits Fix
- com.tel.traitsfix
- This mod adds implementations for all traits that don't work in the original game.
- Tel
- 1.1.0
- ["gameplay"]
-
-
- 0
-
- $(HarmonyID)
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
-
-
-
+
+
+
+ Traits Fix
+ com.tel.traitsfix
+ This mod adds implementations for all traits that don't work in the original game.
+ Tel
+ 1.1.1
+ ["gameplay"]
+
+
+ 0
+
+ $(HarmonyID)
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
diff --git a/mods/Unofficial Patch/Unofficial Patch.cs b/mods/Unofficial Patch/Unofficial Patch.cs
index 1f8791d..5acdcdd 100755
--- a/mods/Unofficial Patch/Unofficial Patch.cs
+++ b/mods/Unofficial Patch/Unofficial Patch.cs
@@ -10,146 +10,490 @@
namespace UnofficialPatch
{
- [HarmonyPatch(typeof(Profile_Fans_Pies), "Render_Pies")]
- public class Profile_Fans_Pies_Render_Pies
+ // Centralized logging utilities for Unofficial Patch patches.
+ internal static class PatchLog
{
+ // Shared logging prefix for Unofficial Patch output.
+ private const string LogPrefix = "[UnofficialPatch] ";
+ // Marker used when Harmony metadata cannot be read.
+ private const string UnknownTarget = "UnknownTarget";
+ // Empty-count sentinel for collection checks.
+ private const int EmptyCount = 0;
- // Fixed fan pie charts so that it is correct
- public static void Postfix(Profile_Fans_Pies __instance, data_girls.girls ___Girl)
- {
- if (___Girl.GetFans_Total() == 0L)
- return;
+ // Protects the once-only warning cache.
+ private static readonly object OnceLock = new object();
+ // Tracks warnings that were already emitted to prevent log spam.
+ private static readonly HashSet OnceKeys = new HashSet();
- __instance.Fans_Pie_Adult.GetComponent().fillAmount += __instance.Fans_Pie_YA.GetComponent().fillAmount - __instance.Fans_Pie_Teen.GetComponent().fillAmount;
+ // Logs a warning for the given patch type.
+ public static void Warn(string message)
+ {
+ Warn(typeof(TPatch), message);
}
- }
+ // Logs a warning once per patch + message.
+ public static void WarnOnce(string message)
+ {
+ WarnOnce(typeof(TPatch), message);
+ }
- [HarmonyPatch(typeof(Tour_New_Popup), "Render")]
- public class Tour_New_Popup_Render
- {
- // Fixed tour revenue text color to consider savings
- public static void Postfix(ref Tour_New_Popup __instance)
+ // Logs a warning once per patch type, regardless of message.
+ public static void WarnOncePerPatch(string message)
{
- if (__instance.Tour.ExpectedRevenue <= __instance.Tour.ProductionCost - __instance.Tour.Saving)
- return;
+ WarnOncePerPatch(typeof(TPatch), message);
+ }
- ExtensionMethods.SetColor(__instance.ExpectedRevenue, mainScript.green32);
+ // Logs a warning for the given patch type, including the resolved Harmony target if available.
+ public static void Warn(Type patchType, string message)
+ {
+ bool usedFallback;
+ string target = GetTargetName(patchType, out usedFallback);
+ string prefix = LogPrefix + target;
+ if (usedFallback && patchType != null)
+ {
+ prefix += " (patch: " + patchType.Name + ")";
+ }
+ Debug.LogWarning(prefix + ": " + message);
}
- }
- [HarmonyPatch(typeof(Theaters), "GetStaminaCost")]
- public class Theaters_GetStaminaCost
- {
- // Fixed Theater so that it uses stamina.
- public static void Postfix(Theaters._theater._schedule._type Type, ref float __result)
+ // Logs a warning once per patch + message combination.
+ public static void WarnOnce(Type patchType, string message)
{
- float num = 0f;
- if (Type == Theaters._theater._schedule._type.performance)
+ string key = GetOnceKey(patchType, message);
+ lock (OnceLock)
{
- num = 5f;
+ if (OnceKeys.Contains(key))
+ return;
+ OnceKeys.Add(key);
}
- else if (Type == Theaters._theater._schedule._type.manzai)
+ Warn(patchType, message);
+ }
+
+ // Logs a warning once per patch type to avoid repeated spam.
+ public static void WarnOncePerPatch(Type patchType, string message)
+ {
+ string key = GetOnceKey(patchType, string.Empty);
+ lock (OnceLock)
{
- num = 2f;
+ if (OnceKeys.Contains(key))
+ return;
+ OnceKeys.Add(key);
}
- if (staticVars.IsHard())
+ Warn(patchType, message);
+ }
+
+ // Builds a readable Harmony target name, falling back to UnknownTarget if metadata is missing.
+ private static string GetTargetName(Type patchType, out bool usedFallback)
+ {
+ usedFallback = true;
+ if (patchType == null)
+ return UnknownTarget;
+
+ object[] attrs = patchType.GetCustomAttributes(typeof(HarmonyPatch), true);
+ if (attrs == null || attrs.Length == EmptyCount)
+ return UnknownTarget;
+
+ List targets = new List();
+ foreach (object attr in attrs)
{
- num *= 2f;
+ string target = GetTargetName(attr);
+ if (!string.IsNullOrEmpty(target))
+ {
+ targets.Add(target);
+ }
}
- __result = num;
+
+ if (targets.Count == EmptyCount)
+ return UnknownTarget;
+
+ usedFallback = false;
+ return string.Join(", ", targets.Distinct());
+ }
+
+ // Extracts HarmonyPatch info without hard dependency on a specific Harmony version.
+ private static string GetTargetName(object patchAttribute)
+ {
+ if (patchAttribute == null)
+ return null;
+
+ object info = GetMember(patchAttribute, "info");
+ Type declaringType = GetMember(info, "declaringType") ?? GetMember(patchAttribute, "declaringType");
+ string methodName = GetMember(info, "methodName") ?? GetMember(patchAttribute, "methodName");
+ Type[] argumentTypes = GetMember(info, "argumentTypes") ?? GetMember(patchAttribute, "argumentTypes");
+ object methodType = GetMember(info, "methodType") ?? GetMember(patchAttribute, "methodType");
+
+ string typeName = declaringType != null ? (declaringType.FullName ?? declaringType.Name) : null;
+ string signature = FormatArgs(argumentTypes);
+
+ if (!string.IsNullOrEmpty(typeName) && !string.IsNullOrEmpty(methodName))
+ return typeName + "." + methodName + signature;
+
+ if (!string.IsNullOrEmpty(typeName))
+ return typeName;
+
+ if (!string.IsNullOrEmpty(methodName))
+ return methodName + signature;
+
+ if (methodType != null)
+ return methodType.ToString();
+
+ return null;
+ }
+
+ // Reads a private field/property by name using reflection.
+ private static object GetMember(object instance, string name)
+ {
+ if (instance == null)
+ return null;
+
+ Type type = instance.GetType();
+ const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+ PropertyInfo property = type.GetProperty(name, flags);
+ if (property != null)
+ return property.GetValue(instance, null);
+
+ FieldInfo field = type.GetField(name, flags);
+ if (field != null)
+ return field.GetValue(instance);
+
+ return null;
+ }
+
+ // Strongly-typed wrapper around GetMember.
+ private static T GetMember(object instance, string name) where T : class
+ {
+ return GetMember(instance, name) as T;
+ }
+
+ // Formats argument type lists to help log target signatures.
+ private static string FormatArgs(Type[] argumentTypes)
+ {
+ if (argumentTypes == null)
+ return string.Empty;
+ if (argumentTypes.Length == EmptyCount)
+ return "()";
+ return "(" + string.Join(", ", argumentTypes.Select(t => t != null ? t.Name : "null")) + ")";
+ }
+
+ // Builds a stable cache key for once-only warnings.
+ private static string GetOnceKey(Type patchType, string message)
+ {
+ string typeName = patchType != null ? patchType.FullName : "null";
+ return typeName + "|" + message;
}
}
- [HarmonyPatch(typeof(Theaters), "CompleteDay")]
- public class Theaters_CompleteDay
+ // Shared IL helper predicates to keep opcode checks consistent and reusable.
+ internal static class IlHelpers
{
- // Fixed Theater so that revenue stats are not offset by one day
- public static bool Prefix()
+ // Identifies call/callvirt instructions targeting a specific method.
+ public static bool IsCallTo(CodeInstruction instruction, MethodInfo target)
{
- foreach (Theaters._theater theater in Theaters.Theaters_)
- {
- // Fix so that auto schedules contribute revenue on the day of
- theater.Doing_Now = theater.GetSchedule().Type;
- }
- return true;
+ if (instruction == null || target == null)
+ return false;
+
+ if (instruction.opcode != OpCodes.Call && instruction.opcode != OpCodes.Callvirt)
+ return false;
+
+ return instruction.operand is MethodInfo method && method == target;
}
+ // Checks whether an instruction loads an int constant with any ldc.i4 opcode form.
+ public static bool IsLdcI4(CodeInstruction instruction, int value)
+ {
+ if (instruction == null)
+ return false;
+
+ // Match the specific ldc.i4 opcode variants that encode constant -1..8 directly.
+ if (instruction.opcode == OpCodes.Ldc_I4_M1)
+ return value == -1;
+ if (instruction.opcode == OpCodes.Ldc_I4_0)
+ return value == 0;
+ if (instruction.opcode == OpCodes.Ldc_I4_1)
+ return value == 1;
+ if (instruction.opcode == OpCodes.Ldc_I4_2)
+ return value == 2;
+ if (instruction.opcode == OpCodes.Ldc_I4_3)
+ return value == 3;
+ if (instruction.opcode == OpCodes.Ldc_I4_4)
+ return value == 4;
+ if (instruction.opcode == OpCodes.Ldc_I4_5)
+ return value == 5;
+ if (instruction.opcode == OpCodes.Ldc_I4_6)
+ return value == 6;
+ if (instruction.opcode == OpCodes.Ldc_I4_7)
+ return value == 7;
+ if (instruction.opcode == OpCodes.Ldc_I4_8)
+ return value == 8;
+ if (instruction.opcode == OpCodes.Ldc_I4_S)
+ return instruction.operand is sbyte sbyteValue && sbyteValue == value;
+ if (instruction.opcode == OpCodes.Ldc_I4)
+ return instruction.operand is int intValue && intValue == value;
+
+ return false;
+ }
- // Fixed Theater so that average stats ignore days off, and so that girls earnings are increased by revenue
- public static void Postfix()
+ // Checks whether an instruction loads a local variable.
+ public static bool IsLdloc(CodeInstruction instruction)
{
- foreach (Theaters._theater theater in Theaters.Theaters_)
- {
- // Fix so that auto schedules contribute revenue on the day of
- if (theater.GetSchedule().Type == Theaters._theater._schedule._type.auto &&
- (theater.Doing_Now == Theaters._theater._schedule._type.manzai || theater.Doing_Now == Theaters._theater._schedule._type.performance))
- {
- long rev = theater.GetTicketSales();
- if (rev > 0) resources.Add(resources.type.money, rev);
- if (staticVars.dateTime.Day != 1)
- {
- theater.GetRoom().addFloat(Floats.type.icon_money, "", true, null, 0f, 1f, 0f, null);
- }
- }
+ if (instruction == null)
+ return false;
- // Fix so that days off have no revenue
- if (theater.Doing_Now == Theaters._theater._schedule._type.day_off)
- {
- theater.Stats[theater.Stats.Count - 1].Revenue = 0;
- }
+ return instruction.opcode == OpCodes.Ldloc ||
+ instruction.opcode == OpCodes.Ldloc_S ||
+ instruction.opcode == OpCodes.Ldloc_0 ||
+ instruction.opcode == OpCodes.Ldloc_1 ||
+ instruction.opcode == OpCodes.Ldloc_2 ||
+ instruction.opcode == OpCodes.Ldloc_3;
+ }
- if (theater.Doing_Now == Theaters._theater._schedule._type.performance || theater.Doing_Now == Theaters._theater._schedule._type.manzai)
- {
- int num2 = theater.GetGroup().GetGirls(true, false, null).Count;
- long num = theater.GetTicketSales();
- if (theater.AreSubsUnlocked() && staticVars.dateTime.Day == 1)
- {
- num += theater.GetSubRevenue();
- }
- foreach (data_girls.girls girls2 in theater.GetGroup().GetGirls(true, false, null))
- {
- if (num > 0L && num2 > 0)
- {
- girls2.Earn(num / (long)num2);
- }
- }
- }
- }
+ // Checks whether an instruction stores a local variable.
+ public static bool IsStloc(CodeInstruction instruction)
+ {
+ if (instruction == null)
+ return false;
+
+ return instruction.opcode == OpCodes.Stloc ||
+ instruction.opcode == OpCodes.Stloc_S ||
+ instruction.opcode == OpCodes.Stloc_0 ||
+ instruction.opcode == OpCodes.Stloc_1 ||
+ instruction.opcode == OpCodes.Stloc_2 ||
+ instruction.opcode == OpCodes.Stloc_3;
+ }
+
+ // Checks for conditional branches that jump when the stack value is true.
+ public static bool IsBranchTrue(CodeInstruction instruction)
+ {
+ if (instruction == null)
+ return false;
+
+ return instruction.opcode == OpCodes.Brtrue || instruction.opcode == OpCodes.Brtrue_S;
}
}
- // Fixed Theater so that average stats ignore days off
- [HarmonyPatch(typeof(Theaters._theater), "GetAvgAttendance")]
- public class Theaters__theater_GetAvgAttendance
+ // Fixes fan pie rendering so the adult slice accounts for YA/Teen stacking.
+ [HarmonyPatch(typeof(Profile_Fans_Pies), "Render_Pies")]
+ public class Profile_Fans_Pies_Render_Pies
+ {
+ // No-fan sentinel for early exit.
+ private const long NoFans = 0L;
+ // Cap for cumulative pie fills.
+ private const float MaxPieFill = 1f;
+
+ // Fixes age-pie rendering so the stacked slices match the teen/YA/adult ratios.
+ public static void Postfix(Profile_Fans_Pies __instance, data_girls.girls ___Girl)
+ {
+ // Skip when there are no fans to render, avoiding divide-by-zero ratios in vanilla code.
+ if (___Girl.GetFans_Total() == NoFans)
+ return;
+
+ Image teenImage = __instance.Fans_Pie_Teen.GetComponent();
+ Image yaImage = __instance.Fans_Pie_YA.GetComponent();
+ Image adultImage = __instance.Fans_Pie_Adult.GetComponent();
+
+ float teenRatio = teenImage.fillAmount;
+ float yaRatio = yaImage.fillAmount;
+ // Base game sets Adult fill to adult + teen, so subtract teen to recover the adult slice.
+ float adultRatio = adultImage.fillAmount - teenRatio;
+ if (adultRatio < 0f)
+ {
+ adultRatio = 0f;
+ }
+
+ float totalRatio = teenRatio + yaRatio + adultRatio;
+ // Guard against rounding overshoot by scaling to the expected 0..1 range.
+ if (totalRatio > MaxPieFill && totalRatio > 0f)
+ {
+ float scale = MaxPieFill / totalRatio;
+ teenRatio *= scale;
+ yaRatio *= scale;
+ adultRatio *= scale;
+ }
+
+ // The prefab renders Teen as the base image with Adult and YA as child images on top.
+ // Use cumulative fills so the visible slices match the ratios (YA, then Adult, then Teen).
+ adultImage.fillAmount = adultRatio + yaRatio;
+ teenImage.fillAmount = adultRatio + yaRatio + teenRatio;
+ }
+ }
+
+ // Keeps tour expected revenue text color aligned with profitability.
+ [HarmonyPatch(typeof(Tour_New_Popup), "Render")]
+ public class Tour_New_Popup_Render
{
- public static void Postfix(ref int __result, Theaters._theater __instance)
+ // Keeps the expected revenue color consistent by always evaluating profitability after savings.
+ public static void Postfix(ref Tour_New_Popup __instance)
{
- float num = 0f;
- float num2 = 7f;
- if (__instance.Stats.Count == 0)
- return;
+ // Apply savings before comparing against expected revenue.
+ long effectiveCost = __instance.Tour.ProductionCost - __instance.Tour.Saving;
+ // Positive profit should be green; losses should be red.
+ bool profitable = __instance.Tour.ExpectedRevenue > effectiveCost;
+ ExtensionMethods.SetColor(__instance.ExpectedRevenue, profitable ? mainScript.green32 : mainScript.red32);
+ }
+ }
+
+ // Restores stamina costs for theater schedules.
+ [HarmonyPatch(typeof(Theaters), "GetStaminaCost")]
+ public class Theaters_GetStaminaCost
+ {
+ // Stamina costs for theater schedules.
+ private const float NoStaminaCost = 0f;
+ private const float PerformanceStaminaCost = 5f;
+ private const float ManzaiStaminaCost = 2f;
+ private const float HardModeMultiplier = 2f;
- if (__instance.Stats.Count < 7)
+ // Fixes the vanilla method which always returned 0.
+ public static void Postfix(Theaters._theater._schedule._type Type, ref float __result)
+ {
+ // Start with no stamina cost by default.
+ float staminaCost = NoStaminaCost;
+ if (Type == Theaters._theater._schedule._type.performance)
{
- num2 = __instance.Stats.Count;
+ staminaCost = PerformanceStaminaCost;
}
- int num3 = __instance.Stats.Count - 1;
- int num4 = 0;
- while (num3 >= __instance.Stats.Count - num2)
+ else if (Type == Theaters._theater._schedule._type.manzai)
{
- if (__instance.Stats[num3].Schedule.Type != Theaters._theater._schedule._type.day_off)
- {
- num += __instance.Stats[num3].Attendance;
- num4++;
- }
- num3--;
+ staminaCost = ManzaiStaminaCost;
}
- if (num4 != 0)
+ // Hard mode doubles the cost.
+ if (staticVars.IsHard())
{
- num /= num4;
+ staminaCost *= HardModeMultiplier;
}
- __result = Mathf.RoundToInt(num);
+ // Return the corrected stamina cost.
+ __result = staminaCost;
+ }
+ }
+
+ // Aligns theater revenue timing and payout distribution with the schedule.
+ [HarmonyPatch(typeof(Theaters), "CompleteDay")]
+ public class Theaters_CompleteDay
+ {
+ // Day-of-month used by the base game for subscription revenue.
+ private const int FirstDayOfMonth = 1;
+ // Sentinel values for revenue and counts.
+ private const long NoRevenue = 0L;
+ private const int NoGirls = 0;
+ // Offset for accessing the latest stats entry.
+ private const int LastIndexOffset = 1;
+
+ // Fixes revenue accounting and income distribution after the base method completes.
+ public static void Postfix()
+ {
+ foreach (Theaters._theater theater in Theaters.Theaters_)
+ {
+ // Important: do not rewrite Doing_Now before vanilla runs.
+ // Vanilla uses yesterday's Doing_Now at the beginning of CompleteDay to settle previous-day revenue.
+ // We only read the stat row vanilla just wrote for "today" and correct payouts from that.
+ if (theater == null || theater.Stats == null || theater.Stats.Count == 0)
+ {
+ continue;
+ }
+
+ Theaters._theater._stat latestStat = theater.Stats[theater.Stats.Count - LastIndexOffset];
+ if (latestStat == null || latestStat.Schedule == null)
+ {
+ continue;
+ }
+
+ // Days off should contribute zero revenue to stats.
+ if (latestStat.Schedule.Type == Theaters._theater._schedule._type.day_off)
+ {
+ latestStat.Revenue = NoRevenue;
+ continue;
+ }
+
+ // Split ticket/subscription revenue among participants for actual show days.
+ bool isShowDay =
+ latestStat.Schedule.Type == Theaters._theater._schedule._type.performance ||
+ latestStat.Schedule.Type == Theaters._theater._schedule._type.manzai;
+ if (!isShowDay)
+ {
+ continue;
+ }
+
+ Groups._group group = theater.GetGroup();
+ if (group == null)
+ {
+ continue;
+ }
+
+ List girls = group.GetGirls(true, false, null);
+ int girlCount = girls != null ? girls.Count : NoGirls;
+ if (girlCount <= NoGirls)
+ {
+ continue;
+ }
+
+ // Use the revenue value from the stat row vanilla just recorded for this day.
+ long payout = latestStat.Revenue;
+ if (theater.AreSubsUnlocked() && staticVars.dateTime.Day == FirstDayOfMonth)
+ {
+ // Include monthly subscription revenue only on the first day, matching vanilla timing.
+ payout += theater.GetSubRevenue();
+ }
+
+ if (payout <= NoRevenue)
+ {
+ continue;
+ }
+
+ long split = payout / (long)girlCount;
+ if (split <= NoRevenue)
+ {
+ continue;
+ }
+
+ foreach (data_girls.girls girl in girls)
+ {
+ if (girl != null)
+ {
+ girl.Earn(split);
+ }
+ }
+ }
+ }
+ }
+
+ // Fixed Theater so that average stats ignore days off
+ [HarmonyPatch(typeof(Theaters._theater), "GetAvgAttendance")]
+ public class Theaters__theater_GetAvgAttendance
+ {
+ // Rolling window size for averages.
+ private const int DaysInWeek = 7;
+ private const int NoStats = 0;
+ private const int NoDaysCounted = 0;
+ private const int LastIndexOffset = 1;
+ private const float ZeroAverage = 0f;
+
+ public static void Postfix(ref int __result, Theaters._theater __instance)
+ {
+ // Skip if there are no stats to average.
+ if (__instance.Stats.Count == NoStats)
+ return;
+
+ // Only inspect the most recent week (or fewer days if not enough data).
+ int daysToCheck = Mathf.Min(__instance.Stats.Count, DaysInWeek);
+ float totalAttendance = ZeroAverage;
+ int countedDays = NoDaysCounted;
+ int index = __instance.Stats.Count - LastIndexOffset;
+ while (index >= __instance.Stats.Count - daysToCheck)
+ {
+ // Ignore day-off entries so the average reflects performance days.
+ if (__instance.Stats[index].Schedule.Type != Theaters._theater._schedule._type.day_off)
+ {
+ totalAttendance += __instance.Stats[index].Attendance;
+ countedDays++;
+ }
+ index--;
+ }
+ // Only divide if at least one valid day was counted.
+ if (countedDays != NoDaysCounted)
+ {
+ totalAttendance /= countedDays;
+ }
+ // Return a rounded attendance average.
+ __result = Mathf.RoundToInt(totalAttendance);
}
}
@@ -158,33 +502,41 @@ public static void Postfix(ref int __result, Theaters._theater __instance)
[HarmonyPatch(typeof(Theaters._theater), "GetAvgRevenue")]
public class Theaters__theater_GetAvgRevenue
{
+ // Rolling window size for averages.
+ private const int DaysInWeek = 7;
+ private const int NoStats = 0;
+ private const int NoDaysCounted = 0;
+ private const int LastIndexOffset = 1;
+ private const float ZeroAverage = 0f;
+
public static void Postfix(ref int __result, Theaters._theater __instance)
{
- float num = 0f;
- float num2 = 7f;
- if (__instance.Stats.Count == 0)
+ // Skip if there are no stats to average.
+ if (__instance.Stats.Count == NoStats)
return;
- if (__instance.Stats.Count < 7)
- {
- num2 = __instance.Stats.Count;
- }
- int num3 = __instance.Stats.Count - 1;
- int num4 = 0;
- while (num3 >= __instance.Stats.Count - num2)
- {
- if (__instance.Stats[num3].Schedule.Type != Theaters._theater._schedule._type.day_off)
- {
- num += __instance.Stats[num3].Revenue;
- num4++;
- }
- num3--;
- }
- if (num4 != 0)
- {
- num /= num4;
- }
- __result = Mathf.RoundToInt(num);
+ // Only inspect the most recent week (or fewer days if not enough data).
+ int daysToCheck = Mathf.Min(__instance.Stats.Count, DaysInWeek);
+ float totalRevenue = ZeroAverage;
+ int countedDays = NoDaysCounted;
+ int index = __instance.Stats.Count - LastIndexOffset;
+ while (index >= __instance.Stats.Count - daysToCheck)
+ {
+ // Ignore day-off entries so the average reflects earning days.
+ if (__instance.Stats[index].Schedule.Type != Theaters._theater._schedule._type.day_off)
+ {
+ totalRevenue += __instance.Stats[index].Revenue;
+ countedDays++;
+ }
+ index--;
+ }
+ // Only divide if at least one valid day was counted.
+ if (countedDays != NoDaysCounted)
+ {
+ totalRevenue /= countedDays;
+ }
+ // Return a rounded revenue average.
+ __result = Mathf.RoundToInt(totalRevenue);
}
}
@@ -193,20 +545,29 @@ public static void Postfix(ref int __result, Theaters._theater __instance)
[HarmonyPatch(typeof(Theaters), "GetLastWeekEarning")]
public class Theaters_GetLastWeekEarning
{
+ // Tooltip is intended to show a full week.
+ private const int DaysInWeek = 7;
+ // Approximate weeks per month used by the base game for sub revenue.
+ private const float SubRevenueWeeksPerMonth = 4.35f;
+
public static void Postfix(ref long __result)
{
+ // Start with the base value from the game.
long output = __result;
foreach (Theaters._theater theater in Theaters.Theaters_)
{
- if (theater.Stats.Count >= 7)
+ // Add the missing 7th day for each theater.
+ if (theater.Stats.Count >= DaysInWeek)
{
- output += theater.Stats[theater.Stats.Count - 7].Revenue;
+ output += theater.Stats[theater.Stats.Count - DaysInWeek].Revenue;
}
if (theater.AreSubsUnlocked())
{
- output += (long)Mathf.Round(theater.GetSubRevenue() / 4.35f);
+ // Include subscription revenue spread across an average month.
+ output += (long)Mathf.Round(theater.GetSubRevenue() / SubRevenueWeeksPerMonth);
}
}
+ // Return the corrected tooltip total.
__result = output;
}
}
@@ -215,83 +576,149 @@ public static void Postfix(ref long __result)
[HarmonyPatch(typeof(Cafes), "GetLastWeekEarning")]
public class Cafes_GetLastWeekEarning
{
+ // Tooltip is intended to show a full week.
+ private const int DaysInWeek = 7;
+
public static void Postfix(ref int __result)
{
+ // Start with the base value from the game.
int output = __result;
foreach (Cafes._cafe cafe in Cafes.Cafes_)
{
- if (cafe.Stats.Count >= 7)
+ // Add the missing 7th day for each cafe.
+ if (cafe.Stats.Count >= DaysInWeek)
{
- output += cafe.Stats[cafe.Stats.Count - 7].Profit;
+ output += cafe.Stats[cafe.Stats.Count - DaysInWeek].Profit;
}
}
+ // Return the corrected tooltip total.
__result = output;
}
}
// Fixed so that when girls dating within the group break up, their relationship status is no longer known
- [HarmonyPatch(typeof(Relationships._relationship), "BreakUp")]
- public class Relationships__relationship_BreakUp
- {
- public static void Postfix(ref Relationships._relationship __instance)
- {
- if (!__instance.Dating)
- return;
-
- __instance.Girls[0].DatingData.Is_Partner_Status_Known = false;
- __instance.Girls[1].DatingData.Is_Partner_Status_Known = false;
- }
- }
+ [HarmonyPatch(typeof(Relationships._relationship), "BreakUp")]
+ public class Relationships__relationship_BreakUp
+ {
+ // Indices for the two relationship participants.
+ private const int FirstPartnerIndex = 0;
+ private const int SecondPartnerIndex = 1;
+
+ public static void Prefix(Relationships._relationship __instance, ref bool __state)
+ {
+ // Capture whether the pair was dating before BreakUp clears the flag.
+ __state = __instance != null && __instance.Dating;
+ }
+
+ public static void Postfix(Relationships._relationship __instance, bool __state)
+ {
+ // Only clear known status when the pair was actually dating.
+ if (!__state || __instance == null || __instance.Girls == null || __instance.Girls.Count < 2)
+ return;
+
+ // Hide partner status for both sides after breakup.
+ if (__instance.Girls[FirstPartnerIndex] != null)
+ {
+ __instance.Girls[FirstPartnerIndex].DatingData.Is_Partner_Status_Known = false;
+ }
+ if (__instance.Girls[SecondPartnerIndex] != null)
+ {
+ __instance.Girls[SecondPartnerIndex].DatingData.Is_Partner_Status_Known = false;
+ }
+ }
+ }
- // Fixed Concert revenue formula so that it shows accurate estimated values
+ // Fixed Concert revenue formula so that it shows accurate estimated values
[HarmonyPatch(typeof(SEvent_Concerts._concert._projectedValues), "GetRevenue")]
- public class SEvent_Concerts__concert__projectedValues_GetRevenue
- {
+ public class SEvent_Concerts__concert__projectedValues_GetRevenue
+ {
+ // Hype curve configuration used by the adjusted revenue formula.
+ private const float HypeBaseline = 1f;
+ private const float LinearPointX0 = 0f;
+ private const float LinearPointY0 = 0.5f;
+ private const float LinearPointX1 = 1f;
+ private const float LinearPointY1 = 0.25f;
+ // Variable flag and multiplier for FUJI ticket bonus.
+ private const string FujiTicketsVariable = "FUJI_3_TICKETS";
+ private const string TrueValue = "true";
+ private const float FujiTicketsMultiplier = 1.05f;
+
public static IEnumerable Transpiler(IEnumerable instructions)
{
- List instructionList = new List(instructions);
+ // Locate the original GetHype call and our replacement method.
+ MethodInfo getHype = AccessTools.Method(typeof(SEvent_Concerts._concert._projectedValues), "GetHype");
+ MethodInfo infix = AccessTools.Method(typeof(SEvent_Concerts__concert__projectedValues_GetRevenue), nameof(Infix));
- for (int i = 0; i < instructionList.Count; i++)
+ // Abort if Harmony lookup fails so we don't corrupt IL.
+ if (getHype == null || infix == null)
{
- if (instructionList[i].opcode == OpCodes.Call && (MethodInfo)instructionList[i].operand == AccessTools.Method(typeof(SEvent_Concerts._concert._projectedValues), "GetHype"))
- {
- instructionList[i].operand = AccessTools.Method(typeof(SEvent_Concerts__concert__projectedValues_GetRevenue), "Infix");
- break;
- }
+ PatchLog.WarnOncePerPatch("GetHype/Infix method lookup failed.");
+ return instructions;
}
- return instructionList.AsEnumerable();
- }
-
- public static float Infix(SEvent_Concerts._concert._projectedValues __this)
- {
+ var matcher = new CodeMatcher(instructions);
+ matcher.MatchForward(false, new CodeMatch(ci => IlHelpers.IsCallTo(ci, getHype)));
- float hype = __this.GetHype();
- if (hype > 1)
+ if (matcher.IsInvalid)
{
- LinearFunction._function function = new();
- function.Init(0f, 0.5f, 1f, 0.25f);
-
- float num2 = hype - 1f;
- hype = num2 * function.GetY(num2) + 1f;
+ PatchLog.WarnOncePerPatch("GetHype call not found.");
+ return instructions;
}
- return hype;
+ // Swap to our Infix method to apply the adjusted hype curve.
+ matcher.Set(OpCodes.Call, infix);
+ return matcher.InstructionEnumeration();
}
- }
+
+ public static float Infix(SEvent_Concerts._concert._projectedValues __this)
+ {
+ // Start with the game's base hype calculation.
+ float hype = __this.GetHype();
+
+ // Club venues never use the post-100% hype curve in the base game.
+ bool isClubVenue = __this.Parent != null && __this.Parent.Venue == SEvent_Concerts._venue.club;
+
+ // Only reshape hype above the baseline (1.0) for non-club venues.
+ if (!isClubVenue && hype > HypeBaseline)
+ {
+ // Avoid target-typed new() for max compatibility
+ LinearFunction._function function = new LinearFunction._function();
+ // Configure a linear mapping with points (0, 0.5) and (1, 0.25).
+ function.Init(LinearPointX0, LinearPointY0, LinearPointX1, LinearPointY1);
+
+ // Convert "hype above 1" into a scaled bonus, then re-add the baseline.
+ float excessHype = hype - HypeBaseline;
+ hype = excessHype * function.GetY(excessHype) + HypeBaseline;
+ }
+
+ // Apply the FUJI ticket bonus if enabled.
+ if (variables.Get(FujiTicketsVariable) == TrueValue)
+ {
+ hype *= FujiTicketsMultiplier;
+ }
+
+ // Return the adjusted hype value.
+ return hype;
+ }
+ }
+
// Fixed Concert revenue formula so that it shows accurate estimated values
[HarmonyPatch(typeof(SEvent_Concerts._concert._projectedValues), "GetString")]
- public class SEvent_Concerts__concert__projectedValues_GetString
- {
- public static bool Prefix(ref float _val)
- {
- if (_val >= 99.5)
- {
- _val = 99;
- }
+ public class SEvent_Concerts__concert__projectedValues_GetString
+ {
+ // Hype is capped at 200% (2.0) by the base game.
+ private const float MaxRatio = 2f;
+
+ public static bool Prefix(ref float _val)
+ {
+ // Clamp ratios so hype does not exceed its intended 200% cap.
+ if (_val > MaxRatio)
+ {
+ _val = MaxRatio;
+ }
return true;
}
}
@@ -300,95 +727,254 @@ public static bool Prefix(ref float _val)
[HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")]
public class singles__single_SenbatsuCalcParam
{
+ // Percent scaling constant used by the base formula.
+ private const float PercentScale = 100f;
+ // IL pattern length for the 100f / rows divisor.
+ private const int DivPatternLength = 4;
+ // Senbatsu row limits and guard values.
+ private const int NoIdols = 0;
+ private const int MinRows = 1;
+ private const int MaxRows = 5;
+ private const int FirstIndex = 0;
+ // Constants for triangular number inversion.
+ private const float TriangularScale = 8f;
+ private const float TriangularOffset = 1f;
+ private const float TriangularDivisor = 2f;
+ private const float ZeroPercent = 0f;
+
public static IEnumerable Transpiler(IEnumerable instructions)
{
- List instructionList = new List(instructions);
+ // Resolve the infix method that computes percent based on actual filled rows.
+ MethodInfo infix = AccessTools.Method(typeof(singles__single_SenbatsuCalcParam), nameof(Infix));
+ if (infix == null)
+ {
+ PatchLog.WarnOncePerPatch("Infix method lookup failed.");
+ return instructions;
+ }
+
+ var matcher = new CodeMatcher(instructions);
+ // Find the sequence that divides 100f by the row count (num2).
+ matcher.MatchForward(false,
+ new CodeMatch(ci => ci.opcode == OpCodes.Ldc_R4 && ci.operand is float value && value == PercentScale),
+ new CodeMatch(ci => IlHelpers.IsLdloc(ci)),
+ new CodeMatch(OpCodes.Conv_R4),
+ new CodeMatch(OpCodes.Div));
- int index = -1;
- for (int i = 0; i < instructionList.Count; i++)
+ if (matcher.IsInvalid)
{
- if (instructionList[i].opcode == OpCodes.Stloc_2)
- {
- index = i;
- break;
- }
+ PatchLog.WarnOncePerPatch("100/rows divisor not found.");
+ return instructions;
}
- if (index != -1)
+ // Ensure the divisor result is immediately stored in a local.
+ CodeInstruction storeInstruction = matcher.InstructionAt(DivPatternLength);
+ if (storeInstruction == null || !IlHelpers.IsStloc(storeInstruction))
{
- instructionList.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_0));
- instructionList.Insert(index + 2, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(singles__single_SenbatsuCalcParam), "Infix")));
- instructionList.Insert(index + 3, new CodeInstruction(OpCodes.Stloc_2));
+ PatchLog.WarnOncePerPatch("100/rows divisor found, but store opcode not found.");
+ return instructions;
}
- return instructionList.AsEnumerable();
+ // Replace 100f / num2 with Infix(_girls) to derive rows from actual filled slots.
+ var labels = matcher.Instruction.labels.ToList();
+ var blocks = matcher.Instruction.blocks.ToList();
+ matcher.RemoveInstructions(DivPatternLength);
+ // Ldarg_1 loads the _girls list from the original SenbatsuCalcParam signature.
+ var loadGirls = new CodeInstruction(OpCodes.Ldarg_1);
+ loadGirls.labels.AddRange(labels);
+ loadGirls.blocks.AddRange(blocks);
+ matcher.Insert(loadGirls, new CodeInstruction(OpCodes.Call, infix));
+ return matcher.InstructionEnumeration();
}
- public static float Infix(int num)
+ public static float Infix(List girls)
{
- float num2 = 5;
- if (num == 0)
- {
- num2 = 0;
- }
- else if (num == 1)
- {
- num2 = 1;
- }
- else if (num <= 3)
- {
- num2 = 1 + (num - 1) / 2;
- }
- else if (num <= 6)
- {
- num2 = 2 + (num - 3) / 3;
- }
- else if (num <= 10)
- {
- num2 = 3 + (num - 6) / 4;
- }
- else if (num <= 15)
- {
- num2 = 3 + (num - 10) / 5;
+ // Count only filled slots to determine how many rows are actually used.
+ int idolCount = NoIdols;
+ if (girls != null)
+ {
+ for (int i = FirstIndex; i < girls.Count; i++)
+ {
+ if (girls[i] != null)
+ {
+ idolCount++;
+ }
+ }
}
- return 100f / num2;
+
+ // Total rows in the senbatsu formation:
+ // 1, 2, 3, 4, 5 (total capacity = 15)
+ // Safety: if no idols, don't divide by zero.
+ // (The game probably never passes 0, but this prevents Infinity/NaN.)
+ if (idolCount <= NoIdols)
+ return ZeroPercent;
+
+ // Triangular number inversion:
+ // Assume that r represents the minimum required number of rows to fit all our idols represented by n
+ // Find the smallest r such that r(r+1)/2 >= idolCount
+ //
+ // r = ceil((sqrt(8N + 1) - 1) / 2)
+ float n = idolCount;
+ float r = (Mathf.Sqrt(TriangularScale * n + TriangularOffset) - TriangularOffset) / TriangularDivisor;
+
+ // Round up to the next whole row to ensure all idols fit.
+ int rowsUsed = Mathf.CeilToInt(r);
+
+ // Clamp to the real formation size:
+ // Anything above 15 idols still just uses all 5 rows.
+ rowsUsed = Mathf.Clamp(rowsUsed, MinRows, MaxRows);
+
+ // The game wants a "percentage per used row" kind of factor.
+ return PercentScale / rowsUsed;
}
- }
+ }
- // Dating status is visible for underage members
- [HarmonyPatch(typeof(data_girls.girls), "GetPartnerString")]
- public class data_girls_girls_GetPartnerString
+ // Fix senbatsu parameter queries to use the requested param type.
+ [HarmonyPatch(typeof(singles._single), "GetSenbatsuParamValue")]
+ public class singles__single_GetSenbatsuParamValue
{
- public static IEnumerable Transpiler(IEnumerable instructions)
+ // Cache the private calculator so we can call it with the correct param type.
+ private static readonly MethodInfo SenbatsuCalcParam = AccessTools.Method(
+ typeof(singles._single),
+ "SenbatsuCalcParam",
+ new Type[] { typeof(List), typeof(data_girls._paramType), typeof(Groups._group) });
+
+ public static bool Prefix(singles._single __instance, data_girls._paramType Type, ref float __result)
{
- List instructionList = new List(instructions);
+ // Fall back to vanilla behavior if reflection fails.
+ if (SenbatsuCalcParam == null)
+ return true;
- for (int i = 0; i < instructionList.Count; i++)
+ try
{
- if (instructionList[i].opcode == OpCodes.Ret)
- {
- instructionList[i - 1].opcode = OpCodes.Nop;
- instructionList[i].opcode = OpCodes.Nop;
- break;
- }
+ // Compute the value using the requested param type (instead of always "cute").
+ var param = (data_girls.girls.param)SenbatsuCalcParam.Invoke(__instance, new object[] { __instance.girls, Type, null });
+ __result = param.val;
+ return false;
+ }
+ catch (Exception ex)
+ {
+ // Log once so repeated failures do not spam the log.
+ PatchLog.WarnOncePerPatch("failed: " + ex);
+ return true;
}
-
- return instructionList.AsEnumerable();
}
}
+ // Dating status is visible for underage members.
+ // A postfix is safer than a transpiler here and avoids invalid IL after upstream changes.
+ [HarmonyPatch(typeof(data_girls.girls), "GetPartnerString")]
+ public class data_girls_girls_GetPartnerString
+ {
+ public static void Postfix(data_girls.girls __instance, ref string __result)
+ {
+ if (__instance == null)
+ {
+ return;
+ }
+
+ // Keep vanilla behavior for AOC members.
+ if (__instance.Is_AOC())
+ {
+ return;
+ }
+
+ __result = BuildPartnerString(__instance);
+ }
+
+ private static string BuildPartnerString(data_girls.girls girl)
+ {
+ string text = "";
+ if (!girl.DatingData.Is_Partner_Status_Known)
+ {
+ text += Language.Data["PROFILE__DATING_UNKNOWN"];
+ }
+ else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.free)
+ {
+ text += Language.Data["PROFILE__DATING_NOT_DATING"];
+ }
+ else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.taken_idol)
+ {
+ data_girls.girls girlfriend = girl.GetGirlfriend();
+ if (girlfriend != null)
+ {
+ text += Language.Insert("PROFILE__DATING_IDOL", new string[]
+ {
+ girlfriend.GetName(true)
+ });
+ }
+ else
+ {
+ text += Language.Data["PROFILE__DATING_IDOL_UNKNOWN"];
+ }
+ }
+ else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.taken_outside_bf)
+ {
+ text += Language.Data["PROFILE__DATING_HAS_BF"];
+ }
+ else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.taken_outside_gf)
+ {
+ text += Language.Data["PROFILE__DATING_HAS_GF"];
+ }
+ else if (girl.DatingData.Partner_Status_Known_To_Player == data_girls.girls._dating_data._partner_status.taken_player)
+ {
+ text += Language.Data["PROFILE__DATING_YOU"];
+ }
+
+ text += "\n";
+ if (girl.DatingData.Is_Sexuality_Known)
+ {
+ if (girl.sexuality == data_girls.girls._sexuality.straight)
+ {
+ text += Language.Data["PROFILE__DATING_STRAIGHT"];
+ }
+ else if (girl.sexuality == data_girls.girls._sexuality.lesbian)
+ {
+ text += Language.Data["PROFILE__DATING_LESBIAN"];
+ }
+ else
+ {
+ text += Language.Data["PROFILE__DATING_BI"];
+ }
+ }
+ else
+ {
+ text += Language.Data["PROFILE__DATING_PREF_UNKNOWN"];
+ }
+
+ if (girl.DatingData.Previous_Attempt != Date_Flirt._flirt._category.NONE
+ && girl.DatingData.Partner_Status_Known_To_Player != data_girls.girls._dating_data._partner_status.taken_player)
+ {
+ text += "\n";
+ if (girl.DatingData.Is_Uninterested || (girl.DatingData.Is_Sexuality_Known && !Date_Flirt.IsCompatibleSexuality(girl)))
+ {
+ text += Language.Data["PROFILE__DATING_NOT_INTERESTED"];
+ }
+ else
+ {
+ text += Language.Data["PROFILE__DATING_INTERESTED"];
+ }
+ }
+
+ return text;
+ }
+ }
+
+
// Fixed fan opinion to be impacted by concerts, SSK/show cancellation and random events
[HarmonyPatch(typeof(resources._fanOpinion), "Add")]
public class resources__fanOpinion_Add
{
public static void Postfix(resources._fanOpinion __instance, float val)
{
+ // Propagate global fan opinion changes to each active girl.
foreach (data_girls.girls girl in data_girls.girl)
{
+ // Skip null entries, sick girls, and graduates who should not gain appeal.
if (girl != null && !girl.IsSick() && girl.status != data_girls._status.graduated)
{
+ // Apply the appeal delta for the matching fan type.
girl.AddAppeal(__instance.type, val);
}
}
@@ -400,14 +986,21 @@ public static void Postfix(resources._fanOpinion __instance, float val)
[HarmonyPatch(typeof(Date_Gossip), "GetAvailableGossips")]
public class Date_Gossip_GetAvailableGossips
{
+ // Sentinel values for list bounds.
+ private const int NoGossips = 0;
+ private const int LastIndexOffset = 1;
+
public static void Postfix(ref List __result, data_girls.girls Snitch)
{
- if (__result.Count == 0)
+ // Nothing to filter if the list is empty.
+ if (__result.Count == NoGossips)
{
return;
}
- for (int i = __result.Count - 1; i >= 0; i--)
+ // Walk backwards so removals do not affect remaining indices.
+ for (int i = __result.Count - LastIndexOffset; i >= NoGossips; i--)
{
+ // Remove any gossip targeting the snitch herself.
if (__result[i].BullyingTarget == Snitch)
{
__result.RemoveAt(i);
@@ -419,51 +1012,150 @@ public static void Postfix(ref List __result, data_girls.gi
// Fixed event and dialogue checks for Influence to check for Influence instead of Friendship
[HarmonyPatch(typeof(vn_requirements), "CheckGirl", new Type[] { typeof(data_girls.girls), typeof(string), typeof(string) })]
- public class vn_requirements_CheckGirl
- {
+ public class vn_requirements_CheckGirl
+ {
+ // Requirement key used by the game for influence checks.
+ private const string InfluenceParameter = "influence";
+ // Enum values from Relationships_Player._type used in the original IL.
+ private const int RelationshipFriendshipValue = 1;
+ private const int RelationshipInfluenceValue = 2;
+ // Sentinel for FindIndex failures.
+ private const int NotFoundIndex = -1;
+ // Offset to move from a marker instruction to the next instruction.
+ private const int NextInstructionOffset = 1;
+
public static IEnumerable Transpiler(IEnumerable instructions)
{
- List instructionList = new(instructions);
-
- bool breakFlag = false;
- for (int i = 0; i < instructionList.Count; i++)
+ // Resolve the relationship checker to ensure we update the correct call site.
+ MethodInfo checkRelationship = AccessTools.Method(
+ typeof(vn_requirements),
+ "CheckRelationship",
+ new Type[] { typeof(data_girls.girls), typeof(string), typeof(Relationships_Player._type) });
+ if (checkRelationship == null)
{
- if (instructionList[i].opcode == OpCodes.Ldstr && (string)instructionList[i].operand == "influence")
- {
- breakFlag = true;
- }
- if (breakFlag && instructionList[i].opcode == OpCodes.Ldc_I4_1)
- {
- instructionList[i].opcode = OpCodes.Ldc_I4_2;
- break;
- }
- if (breakFlag && instructionList[i].opcode == OpCodes.Callvirt)
- {
- // abort if it reaches the Callvirt operation without finding Ldc_I4_1
+ PatchLog.WarnOncePerPatch("CheckRelationship method lookup failed.");
+ return instructions;
+ }
+
+ List instructionList = new List(instructions);
+ // Find the "influence" branch marker in the IL.
+ int influenceIndex = instructionList.FindIndex(ci =>
+ ci.opcode == OpCodes.Ldstr && ci.operand is string text && text == InfluenceParameter);
+
+ if (influenceIndex == NotFoundIndex)
+ {
+ PatchLog.WarnOncePerPatch("\"influence\" marker not found.");
+ return instructionList.AsEnumerable();
+ }
+
+ // Locate the call to CheckRelationship that follows the influence branch.
+ int callIndex = NotFoundIndex;
+ for (int i = influenceIndex + NextInstructionOffset; i < instructionList.Count; i++)
+ {
+ if (IlHelpers.IsCallTo(instructionList[i], checkRelationship))
+ {
+ callIndex = i;
break;
}
}
+ if (callIndex == NotFoundIndex)
+ {
+ PatchLog.WarnOncePerPatch("CheckRelationship call not found after \"influence\" marker.");
+ return instructionList.AsEnumerable();
+ }
+
+ // The relationship enum should be the last integer pushed before the call.
+ int enumIndex = callIndex - NextInstructionOffset;
+ if (enumIndex <= influenceIndex)
+ {
+ PatchLog.WarnOncePerPatch("enum constant not found before CheckRelationship call.");
+ return instructionList.AsEnumerable();
+ }
+
+ CodeInstruction enumInstruction = instructionList[enumIndex];
+ if (IlHelpers.IsLdcI4(enumInstruction, RelationshipFriendshipValue))
+ {
+ // Replace Friendship with Influence.
+ enumInstruction.opcode = OpCodes.Ldc_I4_2;
+ // Clear the operand to match the ldc.i4.2 opcode form.
+ enumInstruction.operand = null;
+ instructionList[enumIndex] = enumInstruction;
+ return instructionList.AsEnumerable();
+ }
+
+ if (IlHelpers.IsLdcI4(enumInstruction, RelationshipInfluenceValue))
+ {
+ // Already patched or game fixed it upstream.
+ return instructionList.AsEnumerable();
+ }
+
+ PatchLog.WarnOncePerPatch("unexpected enum opcode before CheckRelationship call.");
return instructionList.AsEnumerable();
}
}
+ // Fix "variable" requirements to respect leading negation.
+ [HarmonyPatch(typeof(vn_requirements), "CheckGirl", new Type[] { typeof(data_girls.girls), typeof(string), typeof(string) })]
+ public class vn_requirements_CheckGirl_Variable
+ {
+ // Prefix used by dialogue scripts to negate variable requirements.
+ private const char NegationPrefix = '!';
+ private const int PrefixIndex = 0;
+ private const int NegationPrefixLength = 1;
+
+ public static bool Prefix(data_girls.girls girl, string parameter, string formula, ref bool __result)
+ {
+ // Only override the "variable" branch; let the rest of CheckGirl run normally.
+ if (parameter != "variable")
+ return true;
+
+ // Preserve base behavior for graduated girls.
+ if (girl.status == data_girls._status.graduated)
+ {
+ __result = false;
+ return false;
+ }
+
+ bool negate = false;
+ if (!string.IsNullOrEmpty(formula) && formula[PrefixIndex] == NegationPrefix)
+ {
+ negate = true;
+ formula = formula.Substring(NegationPrefixLength);
+ }
+
+ // Evaluate the variable and apply negation if requested.
+ bool hasVariable = girl.IsVariable(formula);
+ __result = negate ? !hasVariable : hasVariable;
+ return false;
+ }
+ }
+
// Fix stamina cost of performance thumbnail if Energetic policy
[HarmonyPatch(typeof(Activities._activity), "GetDescription")]
public class Activities__activity_GetDescription
{
+ // Energetic policy overrides the displayed stamina cost.
+ private const int EnergeticStaminaCost = 4;
+ private const string PointsKey = "PT";
+ private const string StaminaKey = "STAMINA";
+ private const string CostPrefix = "-";
+ private const string CostSeparator = " ";
+
public static void Postfix(ref Activities._activity __instance, ref string __result)
{
+ // Only adjust performance activities when Energetic policy is active.
if (__instance.type == Activity._type.performance && policies.GetSelectedPolicyValue(policies._type.performances).Value == policies._value.performances_energy)
{
+ // Build a localized "-4 PT stamina" string for the thumbnail.
__result = string.Concat(new object[]
{
- "-",
- 4,
- Language.Data["PT"],
- " ",
- Language.Data["STAMINA"].ToLower()
+ CostPrefix,
+ EnergeticStaminaCost,
+ Language.Data[PointsKey],
+ CostSeparator,
+ Language.Data[StaminaKey].ToLower()
});
}
}
@@ -471,33 +1163,94 @@ public static void Postfix(ref Activities._activity __instance, ref string __res
// Fix custom event check for "hired a staffer of type"
- [HarmonyPatch(typeof(vn_requirements), "CheckMeta")]
- public class vn_requirements_CheckMeta
- {
+ [HarmonyPatch(typeof(vn_requirements), "CheckMeta")]
+ public class vn_requirements_CheckMeta
+ {
+ // Metadata parameter and formula keys.
+ private const string StaffParameter = "staff";
+ private const string VocalFormula = "vocal";
+ private const string DanceFormula = "dance";
+ private const string OfficeFormula = "office";
+ private const string StyleFormula = "style";
+ private const int MinimumStaffCount = 0;
+
public static void Postfix(string parameter, string formula, ref bool __result)
{
- if (parameter != "staff")
+ // Only handle staff-type checks; let other meta parameters remain unchanged.
+ if (parameter != StaffParameter)
return;
- if (formula == "vocal" && staff.CountStaffersOfType(agency._type.recordingStudio) > 0)
- {
- __result = true;
- }
- else if(formula == "dance" && staff.CountStaffersOfType(agency._type.danceStudio) > 0)
- {
- __result = true;
- }
- else if(formula == "office" && staff.CountStaffersOfType(agency._type.office) > 0)
- {
- __result = true;
- }
- else if(formula == "style" && staff.CountStaffersOfType(agency._type.dressingRoom) > 0)
- {
- __result = true;
- }
+ // Map formula keys to their matching staff room types.
+ if (formula == VocalFormula && staff.CountStaffersOfType(agency._type.recordingStudio) > MinimumStaffCount)
+ {
+ __result = true;
+ }
+ else if(formula == DanceFormula && staff.CountStaffersOfType(agency._type.danceStudio) > MinimumStaffCount)
+ {
+ __result = true;
+ }
+ else if(formula == OfficeFormula && staff.CountStaffersOfType(agency._type.office) > MinimumStaffCount)
+ {
+ __result = true;
+ }
+ else if(formula == StyleFormula && staff.CountStaffersOfType(agency._type.dressingRoom) > MinimumStaffCount)
+ {
+ __result = true;
+ }
- }
- }
-
-}
+ }
+ }
+
+ // Base game bugfix:
+ // SaveManager.LoadData(...) assigns SaveManager.Data = null when the target file is missing/corrupt,
+ // then returns early ("huh"). Autosave later calls SaveEvent, and staticVars.SaveFunction crashes
+ // because Camera.main.GetComponent().GetSavedData() is null.
+ //
+ // Fix:
+ // Preserve the previous in-memory SaveManager.Data before LoadData runs; if LoadData exits with null Data,
+ // restore the previous data (or a fresh SavedData fallback) so autosave and SaveEvent subscribers remain safe.
+ [HarmonyPatch(typeof(SaveManager), "LoadData", new Type[] { typeof(bool) })]
+ public class SaveManager_LoadData_Bool_NullGuard
+ {
+ private const string WarningMessage =
+ "LoadData(bool) left SaveManager.Data null. Restored previous in-memory save data to prevent SaveEvent/autosave null crash.";
+
+ public static void Prefix(SaveManager __instance, ref SaveManager.SavedData __state)
+ {
+ __state = __instance != null ? __instance.Data : null;
+ }
+
+ public static void Postfix(SaveManager __instance, SaveManager.SavedData __state)
+ {
+ if (__instance == null || __instance.Data != null)
+ return;
+
+ __instance.Data = __state ?? new SaveManager.SavedData();
+ PatchLog.WarnOncePerPatch(WarningMessage);
+ }
+ }
+
+ // Same null-guard for the string-path overload used by some load flows and mods.
+ [HarmonyPatch(typeof(SaveManager), "LoadData", new Type[] { typeof(string) })]
+ public class SaveManager_LoadData_Path_NullGuard
+ {
+ private const string WarningMessage =
+ "LoadData(string) left SaveManager.Data null. Restored previous in-memory save data to prevent SaveEvent/autosave null crash.";
+
+ public static void Prefix(SaveManager __instance, ref SaveManager.SavedData __state)
+ {
+ __state = __instance != null ? __instance.Data : null;
+ }
+
+ public static void Postfix(SaveManager __instance, SaveManager.SavedData __state)
+ {
+ if (__instance == null || __instance.Data != null)
+ return;
+
+ __instance.Data = __state ?? new SaveManager.SavedData();
+ PatchLog.WarnOncePerPatch(WarningMessage);
+ }
+ }
+
+}
diff --git a/mods/Worker Rights/Worker Rights.cs b/mods/Worker Rights/Worker Rights.cs
index ce35f6d..32f5884 100755
--- a/mods/Worker Rights/Worker Rights.cs
+++ b/mods/Worker Rights/Worker Rights.cs
@@ -1,95 +1,94 @@
-using HarmonyLib;
-using System;
-using UnityEngine;
-using System.Reflection;
-using static WorkerRights.WorkerRights;
-
-namespace WorkerRights
-{
- public class WorkerRights
- {
- public const int FIRE_MIN_DAYS = 30;
- public const int DEF_SALARY = 20000;
- public const float MAXFAME_SALARY_EARN_COEFF = 0.1f;
- public const int GRAD_DAYS_PENALTY = 9;
- public const int GRAD_DAYS_PENALTY_SEVERE = 27;
- }
-
- // Staff cannot be fired using scandal points within the first month
- [HarmonyPatch(typeof(staff._staff), "CanFire")]
- public class staff__staff_CanFire
- {
- public static void Postfix(staff._staff __instance, ref bool __result)
- {
- if ((staticVars.dateTime - __instance.HireDate).Days < FIRE_MIN_DAYS)
- {
- __result = false;
- }
- }
- }
- // 20000 yen/wk is the expected starting salary for 100% satisfaction
- [HarmonyPatch(typeof(data_girls.girls), "GetExpectedSalary")]
- public class data_girls_girls_GetExpectedSalary
- {
- public static void Postfix(ref int __result, data_girls.girls __instance)
- {
- if (__instance.GetFameLevel() < 1f)
- {
- __result = DEF_SALARY * 2;
- }
- }
- }
-
- //In hard and normal mode, penalty for low salary satisfaction increased 10x
- [HarmonyPatch(typeof(data_girls.girls), "Graduation_Date_Update")]
- public class data_girls_girls_Graduation_Date_Update
- {
- public static void Postfix(ref data_girls.girls __instance)
- {
- int salarySatisfaction_Percentage = __instance.GetSalarySatisfaction_Percentage();
- if (staticVars.IsEasy() || __instance.status == data_girls._status.announced_graduation || policies.GetSelectedPolicyValue(policies._type.salary).Value != policies._value.salary_manual)
- return;
-
- int daysModifier = 0;
- if (salarySatisfaction_Percentage < 20)
- {
- daysModifier -= GRAD_DAYS_PENALTY_SEVERE;
- }
- else if (salarySatisfaction_Percentage < 50)
- {
- daysModifier -= GRAD_DAYS_PENALTY;
- }
- __instance.Graduation_Date.AddDays(daysModifier);
- }
- }
-
- // In hard mode, idols at 10 fame will expect at least 10% of their earnings as salary
- [HarmonyPatch(typeof(data_girls.girls), "GetExpectedSalary_Total")]
- public class data_girls_girls_GetExpectedSalary_Total
- {
- public static void Postfix(ref long __result, data_girls.girls __instance)
- {
- if (!staticVars.IsHard())
- return;
-
- int expected = __instance.GetExpectedSalary();
- float earning = __instance.GetAverageEarnings();
- if (__instance.GetFameLevel() == 10 && (earning * MAXFAME_SALARY_EARN_COEFF) > expected)
- {
- __result = (long)Mathf.Round(earning * MAXFAME_SALARY_EARN_COEFF);
- }
- }
- }
-
-
- // Default salary set to 20000
- [HarmonyPatch(typeof(data_girls), "GenerateGirl")]
- public class data_girls_GenerateGirl
- {
- public static void Postfix(ref data_girls.girls __result)
- {
- __result.salary = DEF_SALARY;
- }
- }
-
-}
+using HarmonyLib;
+using System;
+using UnityEngine;
+using static WorkerRights.WorkerRights;
+
+namespace WorkerRights
+{
+ public class WorkerRights
+ {
+ public const int FIRE_MIN_DAYS = 30;
+ public const int DEF_SALARY = 20000;
+ public const float MAXFAME_SALARY_EARN_COEFF = 0.1f;
+ public const int GRAD_DAYS_PENALTY = 10;
+ public const int GRAD_DAYS_PENALTY_SEVERE = 30;
+ }
+
+ [HarmonyPatch(typeof(staff._staff), "CanFire")]
+ public class staff__staff_CanFire
+ {
+ public static void Postfix(staff._staff __instance, ref bool __result)
+ {
+ if (__instance != null && (staticVars.dateTime - __instance.HireDate).Days < FIRE_MIN_DAYS)
+ __result = false;
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls.girls), "GetExpectedSalary")]
+ public class data_girls_girls_GetExpectedSalary
+ {
+ public static void Postfix(data_girls.girls __instance, ref int __result)
+ {
+ if (__instance != null && __instance.GetFameLevel() < 1f)
+ __result = Math.Max(__result, DEF_SALARY * 2);
+ }
+ }
+
+ // Vanilla's own Graduation_Date_Update calls DateTime.AddDays without assigning the returned DateTime,
+ // so the salary date adjustment is a no-op. Apply the complete advertised 10x low-salary penalty here.
+ // This stays on the game's weekly Graduation_Date_Update cadence rather than silently becoming a daily 7x rebalance.
+ [HarmonyPatch(typeof(data_girls.girls), "Graduation_Date_Update")]
+ public class data_girls_girls_Graduation_Date_Update
+ {
+ [HarmonyBefore("com.tel.nevergraduate")]
+ public static void Postfix(data_girls.girls __instance)
+ {
+ if (__instance == null || staticVars.IsEasy() || __instance.status == data_girls._status.announced_graduation)
+ return;
+
+ policies.value salaryPolicy = policies.GetSelectedPolicyValue(policies._type.salary);
+ if (salaryPolicy == null || salaryPolicy.Value != policies._value.salary_manual)
+ return;
+
+ int satisfaction = __instance.GetSalarySatisfaction_Percentage();
+ int daysModifier = 0;
+ if (satisfaction < 20)
+ daysModifier = -GRAD_DAYS_PENALTY_SEVERE;
+ else if (satisfaction < 50)
+ daysModifier = -GRAD_DAYS_PENALTY;
+
+ if (daysModifier != 0)
+ __instance.Graduation_Date = __instance.Graduation_Date.AddDays(daysModifier);
+ }
+ }
+
+ // In hard mode, 10-fame idols expect AT LEAST 10% of average earnings.
+ // Never lower a larger salary expectation produced by vanilla or another mod.
+ [HarmonyPatch(typeof(data_girls.girls), "GetExpectedSalary_Total")]
+ public class data_girls_girls_GetExpectedSalary_Total
+ {
+ public static void Postfix(data_girls.girls __instance, ref long __result)
+ {
+ if (__instance == null || !staticVars.IsHard() || __instance.GetFameLevel() != 10)
+ return;
+
+ float earning = __instance.GetAverageEarnings();
+ if (float.IsNaN(earning) || float.IsInfinity(earning) || earning <= 0f)
+ return;
+
+ long floor = (long)Mathf.Round(earning * MAXFAME_SALARY_EARN_COEFF);
+ if (floor > __result)
+ __result = floor;
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls), "GenerateGirl")]
+ public class data_girls_GenerateGirl
+ {
+ public static void Postfix(ref data_girls.girls __result)
+ {
+ if (__result != null)
+ __result.salary = DEF_SALARY;
+ }
+ }
+}
diff --git a/mods/Worker Rights/Worker Rights.csproj b/mods/Worker Rights/Worker Rights.csproj
index 888ab2b..45cbea8 100755
--- a/mods/Worker Rights/Worker Rights.csproj
+++ b/mods/Worker Rights/Worker Rights.csproj
@@ -1,25 +1,25 @@
-
-
-
- Worker Rights
- com.tel.workerrights
- Employee rights! Idols will demand fair wages! Brand new staff cannot be blamed for scandals!
- Tel
- 1.0.0
- ["gameplay"]
-
-
- 0
-
- $(HarmonyID)
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
-
+
+
+
+ Worker Rights
+ com.tel.workerrights
+ Employee rights! Idols will demand fair wages! Brand new staff cannot be blamed for scandals!
+ Tel
+ 1.0.1
+ ["gameplay"]
+
+
+ 0
+
+ $(HarmonyID)
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
diff --git a/shared/StatLimits/StatLimits.cs b/shared/StatLimits/StatLimits.cs
index b13a542..9a000f9 100755
--- a/shared/StatLimits/StatLimits.cs
+++ b/shared/StatLimits/StatLimits.cs
@@ -1,111 +1,109 @@
-using HarmonyLib;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using UnityEngine;
-
-namespace StatLimits
-{
-
- // Limits for business proposal stats
- [HarmonyPatch(typeof(business._proposal), "GetGirlCoeff")]
- public class Business__proposal_GetGirlCoeff_Limits
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref float __result)
- {
- __result = Mathf.Max(0, Mathf.Min(20, __result));
- }
- }
-
-
- // Limits for show stats
- [HarmonyPatch(typeof(data_girls), "GetAverageParam")]
- public class Data_girls_GetAverageParam_Limits
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref float __result)
- {
- __result = Mathf.Max(0, Mathf.Min(100, __result));
- }
- }
-
-
- // Limits for single senbatsu stats
- [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")]
- public class Singles__single_SenbatsuCalcParam_Limits
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref data_girls.girls.param __result)
- {
- __result.val = Mathf.Max(0, Mathf.Min(100, __result.val));
- }
- }
-
- // Limits for show senbatsu stats
- [HarmonyPatch(typeof(Shows._show), "SenbatsuCalcParam")]
- public class Shows__show_SenbatsuCalcParam_Limits
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref data_girls.girls.param __result)
- {
- __result.val = Mathf.Max(0, Mathf.Min(100, __result.val));
- }
- }
-
- // Limits for stats of concert songs
- [HarmonyPatch(typeof(SEvent_Concerts._concert._song), "GetSkillValue")]
- public class SEvent_Concerts__concert__song_GetSkillValue_Limits
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref int __result)
- {
- __result = Math.Max(0, Math.Min(100, __result));
- }
- }
-
-
- // Limits for stats for concert MCs
- [HarmonyPatch(typeof(SEvent_Concerts._concert._mc), "GetSkillValue")]
- public class SEvent_Concerts__concert__mc_GetSkillValue_Limits
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref int __result)
- {
- __result = Math.Max(0, Math.Min(100, __result));
- }
- }
-
- // Limit the show cast params
- [HarmonyPatch(typeof(Show_Popup), "AddCastParam")]
- public class Show_Popup_AddCastParam_Limits
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref List ___girlParams)
- {
- ___girlParams.Last().val = Mathf.Max(0, Mathf.Min(100, ___girlParams.Last().val));
- }
- }
-
- // Limit the show cast params
- [HarmonyPatch(typeof(Shows._show), "AddCastParam")]
- public class Shows__show_AddCastParam_Limits
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref Shows._show __instance)
- {
- __instance.girlParams.Last().val = Mathf.Max(0, Mathf.Min(100, __instance.girlParams.Last().val));
- }
- }
-
- // Limit team chemistry
- [HarmonyPatch(typeof(data_girls), "GetTeamChemistry")]
- public class data_girls_GetTeamChemistry_Patch
- {
- [HarmonyPriority(Priority.Last)]
- public static void Postfix(ref float __result)
- {
- __result = Mathf.Max(0, Mathf.Min(100, __result));
- }
- }
-}
+using HarmonyLib;
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace StatLimits
+{
+ [HarmonyPatch(typeof(business._proposal), "GetGirlCoeff")]
+ public class Business__proposal_GetGirlCoeff_Limits
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(ref float __result)
+ {
+ __result = Mathf.Clamp(__result, 0f, 20f);
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls), "GetAverageParam")]
+ public class Data_girls_GetAverageParam_Limits
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(ref float __result)
+ {
+ __result = Mathf.Clamp(__result, 0f, 100f);
+ }
+ }
+
+ [HarmonyPatch(typeof(singles._single), "SenbatsuCalcParam")]
+ public class Singles__single_SenbatsuCalcParam_Limits
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(ref data_girls.girls.param __result)
+ {
+ if (__result != null)
+ __result.val = Mathf.Clamp(__result.val, 0f, 100f);
+ }
+ }
+
+ [HarmonyPatch(typeof(Shows._show), "SenbatsuCalcParam")]
+ public class Shows__show_SenbatsuCalcParam_Limits
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(ref data_girls.girls.param __result)
+ {
+ if (__result != null)
+ __result.val = Mathf.Clamp(__result.val, 0f, 100f);
+ }
+ }
+
+ [HarmonyPatch(typeof(SEvent_Concerts._concert._song), "GetSkillValue")]
+ public class SEvent_Concerts__concert__song_GetSkillValue_Limits
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(ref int __result)
+ {
+ __result = Math.Max(0, Math.Min(100, __result));
+ }
+ }
+
+ [HarmonyPatch(typeof(SEvent_Concerts._concert._mc), "GetSkillValue")]
+ public class SEvent_Concerts__concert__mc_GetSkillValue_Limits
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(ref int __result)
+ {
+ __result = Math.Max(0, Math.Min(100, __result));
+ }
+ }
+
+ [HarmonyPatch(typeof(Show_Popup), "AddCastParam")]
+ public class Show_Popup_AddCastParam_Limits
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(List ___girlParams)
+ {
+ if (___girlParams == null || ___girlParams.Count == 0)
+ return;
+
+ data_girls.girls.param param = ___girlParams[___girlParams.Count - 1];
+ if (param != null)
+ param.val = Mathf.Clamp(param.val, 0f, 100f);
+ }
+ }
+
+ [HarmonyPatch(typeof(Shows._show), "AddCastParam")]
+ public class Shows__show_AddCastParam_Limits
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(Shows._show __instance)
+ {
+ if (__instance == null || __instance.girlParams == null || __instance.girlParams.Count == 0)
+ return;
+
+ data_girls.girls.param param = __instance.girlParams[__instance.girlParams.Count - 1];
+ if (param != null)
+ param.val = Mathf.Clamp(param.val, 0f, 100f);
+ }
+ }
+
+ [HarmonyPatch(typeof(data_girls), "GetTeamChemistry")]
+ public class data_girls_GetTeamChemistry_Patch
+ {
+ [HarmonyPriority(Priority.Last)]
+ public static void Postfix(ref float __result)
+ {
+ __result = Mathf.Clamp(__result, 0f, 100f);
+ }
+ }
+}