Skip to content

Commit

Permalink
GetInfoFor() rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
ToxicFrog committed Jul 5, 2022
1 parent b47d65d commit 07b4a6a
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 54 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
different classes; this has no user-facing effect but may be useful for mod
integrations.
- Change: internal cleanup of Legendoom integration code.
- Change: redesign of GetInfoFor* API family.
- Change: more documentation on modding/addons/integrations.

# 0.6.3

Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,32 @@ The ZScript files included in this mod are not loadable as-is; they need to be p

You can also simply download a release pk3, unzip it, and edit the preprocessed files.

Parts of this mod may be of interest to other modders; in particular `TooltipOptionsMenu.zs` is a drop-in replacement for `OptionsMenu` that supports tooltips in the MENUDEF, and the other menus are useful examples of how to do dynamic menu creation in ZScript.
### Reusable Parts

If you want to integrate Laevis with another mod -- in particular, if you want to add new Laevis upgrades -- see `BaseUpgrade.zs` for detailed instructions. The short form is: you need to subclass `TFLV_Upgrade_BaseUpgrade`, override some virtual methods, and then register your new upgrade class(es) on mod startup, probably in `StaticEventHandler.OnRegister()`.
`TooltipOptionsMenu.zs` is a drop-in replacement for `OptionsMenu` that supports tooltips in the MENUDEF. It has no external dependencies, is backwards compatible with existing `OptionsMenu` MENUDEFs, and is (like the rest of this mod) MIT licensed, so feel free to use it in your own mods.

The `GenericMenu`, `StatusDisplay`, and other menu classes are useful examples of how to do dynamic interactive menu creation in ZScript, and how to use a non-interactive OptionsMenu to create a status display.

### Adding new Laevis upgrades

See `BaseUpgrade.zs` for detailed instructions. The short form is: you need to subclass `TFLV_Upgrade_BaseUpgrade`, override some virtual methods, and then register your new upgrade class(es) on mod startup, probably in `StaticEventHandler.OnRegister()`.

### Fiddling with Laevis's internal state

Everything you're likely to want to interact with is stored in the `TFLV_PerPlayerStats` (held in the `PlayerPawn`'s inventory) and the `TFLV_WeaponInfo` (one per gun, stored in the PerPlayerStats). Look at the .zs files for those for details on the fields and methods available.

To get the stats, use the static `TFLV_PerPlayerStats.GetStatsFor(pawn)`. The stats are created in the `PlayerSpawned` event, so this should always succeed in normal gameplay unless something has happened to wipe the player's inventory.

Getting weapon info is slightly more complicated; `WeaponInfo` objects are created on-demand, within a tick of the weapon being wielded for the first time, so even if the player is carrying a weapon it may not have an info object. You have a number of options for getting the info object.

These are safe to call from UI code, but can return null:
- `stats.GetInfoForCurrentWeapon()` is fastest but only returns the info for the player's currently equipped weapon.
- `stats.GetInfoFor(wpn)` will get the info for an arbitrary weapon, but only if the info object already exists; it won't return info for a weapon the player has not yet wielded.

This is not UI-safe, but is more flexible:
- `stats.GetOrCreateInfoFor(wpn)` will return existing info for `wpn` if any exists; if not, it will (if the game settings permit this) attempt to re-use an existing `WeaponInfo` for another weapon of the same type. If both of those fail it will create, register, and return a new `WeaponInfo`. Note that calling this on a `Weapon` that is not in the player's inventory will *work*, in the sense that a `WeaponInfo` will be created and returned, but isn't particularly useful unless you subsequently add the weapon to the player's inventory.

If you have an existing `WeaponInfo` and want to stick it to a new weapon, perhaps to transfer upgrades, you can do so by calling `info.Rebind(new_weapon)`. Note that this removes its association with the old weapon entirely -- the "weapon upgrades are shared by weapons of the same class" option is actually implemented by calling `Rebind()` every time the player switches weapons.

# Upgrade List

Expand Down
6 changes: 3 additions & 3 deletions ca.ancilla.laevis/EventHandler.zs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class ::EventHandler : StaticEventHandler {
}

play void CycleLDEffect(PlayerPawn pawn) {
let info = ::PerPlayerStats.GetStatsFor(pawn).GetOrCreateInfoForCurrentWeapon();
let info = ::PerPlayerStats.GetStatsFor(pawn).GetInfoForCurrentWeapon();
if (info) info.ld_info.CycleEffect();
}

Expand All @@ -87,7 +87,7 @@ class ::EventHandler : StaticEventHandler {
}

play void SelectLDEffect(PlayerPawn pawn, int index) {
let info = ::PerPlayerStats.GetStatsFor(pawn).GetOrCreateInfoForCurrentWeapon();
let info = ::PerPlayerStats.GetStatsFor(pawn).GetInfoForCurrentWeapon();
if (info) info.ld_info.SelectEffect(index);
}

Expand All @@ -110,7 +110,7 @@ class ::EventHandler : StaticEventHandler {
ChooseLevelUpOption(players[evt.player].mo, evt.args[0]);
} else if (evt.name == "laevis_debug") {
let info = ::PerPlayerStats.GetStatsFor(players[evt.player].mo)
.GetOrCreateInfoForCurrentWeapon();
.GetInfoForCurrentWeapon();
info.upgrades.Add("::Upgrade::FragmentationShots", 1);
info.upgrades.Add("::Upgrade::PiercingShots", 1);
}
Expand Down
119 changes: 70 additions & 49 deletions ca.ancilla.laevis/PerPlayerStats.zs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ class ::PerPlayerStats : ::Force {
::WeaponInfo infoForCurrentWeapon;
int prevScore;

States {
Spawn:
TNT1 A 0 NoDelay Initialize();
Poll:
TNT1 A 1 TickStats();
LOOP;
}

// HACK HACK HACK
// The various level up menus need to be able to get a handle to the specific
// UpgradeGiver associated with that menu, so it puts itself into this field
Expand Down Expand Up @@ -102,9 +110,13 @@ class ::PerPlayerStats : ::Force {
return true;
}

// Return the WeaponInfo for the currently readied weapon. If the player
// does not have a weapon ready, or if the weaponinfo for it hasn't yet been
// created, return null.
// Return the WeaponInfo for the currently readied weapon.
// Returns null if:
// - no weapon is equipped
// - the equipped weapon does not have an associated WeaponInfo
// - the associated WeaponInfo is not stored in infoForCurrentWeapon
// The latter two cases should only happen for one tic after switching weapons,
// and anything calling this should be null-checking anyways.
::WeaponInfo GetInfoForCurrentWeapon() const {
Weapon wielded = owner.player.ReadyWeapon;
if (wielded && infoForCurrentWeapon && infoForCurrentWeapon.weapon == wielded) {
Expand All @@ -113,35 +125,10 @@ class ::PerPlayerStats : ::Force {
return null;
}

// Like GetInfoForCurrentWeapon, but if WeaponInfo doesn't exist for the current
// weapon it will try to find a compatible existing one to attach to it, or,
// failing that, create a new one. As such this is not suitable for use from
// ui scope.
// If the player is not currently wielding a weapon, returns null.
::WeaponInfo GetOrCreateInfoForCurrentWeapon() {
Weapon wielded = owner.player.ReadyWeapon;
if (!wielded) return null;

::WeaponInfo info = GetInfoForCurrentWeapon();
if (info) return info;

// The player is wielding a weapon but it's not the weapon associated with
// the current weapon info. Try to find an existing WeaponInfo associated
// with this weapon.
info = GetInfoFor(wielded);
// Failing that, try to bind it to a compatible existing one.
if (!info) info = BindExistingInfoTo(wielded);
// If even that fails, create a new one ex nihilo.
if (!info) {
info = new("::WeaponInfo");
info.Init(wielded);
weapons.push(info);
}

infoForCurrentWeapon = info;
return info;
}

// Return the WeaponInfo associated with the given weapon. Unlike
// GetInfoForCurrentWeapon(), this always searches the entire info list, so
// it's slower, but will find the info for any weapon as long as it's been
// wielded at least once and is still bound to its info object.
::WeaponInfo GetInfoFor(Weapon wpn) const {
for (int i = 0; i < weapons.size(); ++i) {
if (weapons[i].weapon == wpn) {
Expand All @@ -151,6 +138,47 @@ class ::PerPlayerStats : ::Force {
return null;
}

// Called every tic to ensure that the currently wielded weapon has associated
// info, and that info is stored in infoForCurrentWeapon.
// Returns infoForCurrentWeapon.
// Note that if the player does not currently have a weapon equipped, this
// sets infoForCurrentWeapon to null and returns null.
::WeaponInfo CreateInfoForCurrentWeapon() {
// Fastest path -- WeaponInfo is already initialized and selected.
if (GetInfoForCurrentWeapon()) return infoForCurrentWeapon;

// Otherwise we need to at least select it. This will return it if it
// already exists, rebinding an existing compatible WeaponInfo or creating
// a new one if needed.
// It is guaranteed to succeed.
infoForCurrentWeapon = GetOrCreateInfoFor(owner.player.ReadyWeapon);
return infoForCurrentWeapon;
}

// If a WeaponInfo already exists for this weapon, return it.
// Otherwise, if a compatible orphaned WeaponInfo exists, rebind and return that.
// Otherwise, create a new WeaponInfo, bind it to this weapon, add it to the
// weapon info list, and return it.
::WeaponInfo GetOrCreateInfoFor(Weapon wpn) {
if (!wpn) return null;

// Fast path -- player has a weapon but we need to select the WeaponInfo
// for it.
let info = GetInfoFor(wpn);

// Slow path -- no associated WeaponInfo, but there might be one we can
// re-use, depending on the upgrade binding settings.
if (!info) info = BindExistingInfoTo(wpn);

// Slowest path -- create a new WeaponInfo and stick it to this weapon.
if (!info) {
info = new("::WeaponInfo");
info.Init(wpn);
weapons.push(info);
}
return info;
}

// Given a weapon, try to find a compatible existing unused WeaponInfo we can
// attach to it.
::WeaponInfo BindExistingInfoTo(Weapon wpn) {
Expand Down Expand Up @@ -204,7 +232,7 @@ class ::PerPlayerStats : ::Force {
// Add XP to a weapon. If the weapon leveled up, also do some housekeeping
// and possibly level up the player as well.
void AddXP(int xp) {
::WeaponInfo info = GetOrCreateInfoForCurrentWeapon();
::WeaponInfo info = GetInfoForCurrentWeapon();
if (!info) return;
if (info.AddXP(xp)) {
// Weapon leveled up!
Expand Down Expand Up @@ -249,15 +277,15 @@ class ::PerPlayerStats : ::Force {
// priority of the inciting event against the priority of each upgrade.
void OnProjectileCreated(Actor shot) {
upgrades.OnProjectileCreated(owner, shot);
let info = GetOrCreateInfoForCurrentWeapon();
let info = GetInfoForCurrentWeapon();
if (info) info.upgrades.OnProjectileCreated(owner, shot);
}

void OnDamageDealt(Actor shot, Actor target, uint damage) {
upgrades.OnDamageDealt(owner, shot, target, damage);
// Record whether it was a missile or a projectile, for the purposes of
// deciding what kinds of upgrades to spawn.
let info = GetOrCreateInfoForCurrentWeapon();
let info = GetInfoForCurrentWeapon();
if (!info) return;
if (shot && shot.bMISSILE) {
info.projectile_shots++;
Expand All @@ -269,14 +297,14 @@ class ::PerPlayerStats : ::Force {

void OnDamageReceived(Actor shot, Actor attacker, uint damage) {
upgrades.OnDamageReceived(owner, shot, attacker, damage);
let info = GetOrCreateInfoForCurrentWeapon();
let info = GetInfoForCurrentWeapon();
if (!info) return;
info.upgrades.OnDamageReceived(owner, shot, attacker, damage);
}

void OnKill(Actor shot, Actor target) {
upgrades.OnKill(owner, shot, target);
let info = GetOrCreateInfoForCurrentWeapon();
let info = GetInfoForCurrentWeapon();
if (!info) return;
info.upgrades.OnKill(owner, shot, target);
}
Expand All @@ -289,7 +317,7 @@ class ::PerPlayerStats : ::Force {
if (damage <= 0) {
return;
}
::WeaponInfo info = GetOrCreateInfoForCurrentWeapon();
::WeaponInfo info = GetInfoForCurrentWeapon();
if (passive) {
// Incoming damage.
DEBUG("MD(p): %s <- %s <- %s (%d/%s) flags=%X",
Expand Down Expand Up @@ -335,8 +363,9 @@ class ::PerPlayerStats : ::Force {
// Runs once per tic.
void TickStats() {
// This ensures that the currently wielded weapon always has a WeaponInfo
// struct associated with it. It should be pretty fast.
let info = GetOrCreateInfoForCurrentWeapon();
// struct associated with it. It should be pretty fast, especially in the
// common case where the weapon already has a WeaponInfo associated with it.
let info = CreateInfoForCurrentWeapon();

// No score integration? Nothing else to do.
if (!::Settings.use_score_for_xp()) {
Expand All @@ -356,15 +385,7 @@ class ::PerPlayerStats : ::Force {
}

upgrades.Tick();
info.upgrades.Tick();
}

States {
Spawn:
TNT1 A 0 NoDelay Initialize();
Poll:
TNT1 A 1 TickStats();
LOOP;
if (info) info.upgrades.Tick();
}
}

0 comments on commit 07b4a6a

Please sign in to comment.