Skip to content

Adding Magical Properties

Chiri Vulpes edited this page Sep 10, 2022 · 6 revisions

Magical property modding support requires Wayward: Wheels & Wetlands (2.12.0-beta) or newer.

Like most custom registrations, adding custom magical properties is done via the @Register decorators.

This documentation's examples will describe adding magical properties for the "Odd Magicks" mod.

Basic Magical Property

Let's say we want to add a magical property called "floaty" which reduces the stamina that jumping takes.

export default class OddMagicks extends Mod {

    @Register.magicalProperty("Floaty", { 
        isApplicable: () => true,
        getInfo: item => ({
            max: 100,
            value: () => item.island.seededRandom.intInRange(100),
        }),
    })
    public readonly magicalPropertyFloaty: MagicalPropertyType;

}

isApplicable (item: Item, description: IItemDescription): boolean

This method is called to determine whether this custom magical property can be applied to a given item. In the example above, true is returned, which makes the "floaty" magical property always applicable — any item can have it.

But wait... is that really what we want? It probably makes more sense if "floaty" only works on equipment:

isApplicable: (item, description) => !!description.equip,

This makes it so that the magical property can only be applied to items that are equippable. We'll make it so that the magical property only takes equipped items into account in our implementation, when we get there.

getInfo (item: Item, description: IItemDescription): IMagicalPropertyInfo

When magical properties are manipulated by the player via actions that add them and whatnot, they all go through getInfo, which is generated for the item that will have/does have the magical property. In other words, getInfo allows for item-specific magical property configuration.

In the example above, we set the maximum value for the magical property to 100, and make the random value generator return an integer between 0 and 100 (inclusive).

But hmm... how do we want to use the floatiness value? What if we make it so that each floatiness piece of equipment adds additional floatiness, and you can have anywhere between 0% floatiness and 90% floatiness. Let's say if a player maxes out four pieces of equipment with floatiness, they'll have enough to reach the cap.

How about this:

getInfo: item => ({
    max: 0.3, // ie 30% floatiness
    value: () => 0.05 // base 5% floatiness
                 + item.island.seededRandom.float() * 0.2, // plus 0-20% additional floatiness generated by chance!
}),

Making the max value greater than the maximum value that can be generated means the magical property "upgrading" system can increase the magical property beyond its natural limit, which is kinda fun I think!

Allowing the player to see it!

  1. As most things, there's a magicalPropertyType dictionary we can extend in the english file to include our new magical property. The name generated is consistent with other mod registrations — mod<your mod name><your registration name>.
"magicalPropertyType": {
	"modOddMagicksFloaty": ["floatiness", "A strange wind appears to hold you aloft, reducing the stamina it requires to jump."]
}

ItemEquipInfo.getMagicalEquipTypes

This is a convenient method we can inject into in the ItemEquipInfo "info-provider", which is what generates the magical property on-equip effects in item tooltips. All we need to do is tweak the return value so that it includes our magical property.

@InjectObject(ItemEquipInfo.methods, "getMagicalEquipTypes", InjectionPosition.Post)
protected onItemEquipInfoGetMagicalEquipTypes(api: IInjectionApi<typeof ItemEquipInfo["methods"], "getMagicalEquipTypes">, info: IGetUseInfo<typeof ItemEquipInfo>) {
	api.returnValue?.add(this.magicalPropertyFloaty);
}

Time to look in the game and see what we get!

image

It's there! But it's giving the raw value instead of a percentage. Can we fix that?

ItemEquipInfo.isMagicalPropertyPercentage

Coincidentally, there's a helper we can inject into for that, too!

@InjectObject(ItemEquipInfo.methods, "isMagicalPropertyPercentage", InjectionPosition.Post)
protected onItemEquipInfoIsMagicalPropertyPercentage(api: IInjectionApi<typeof ItemEquipInfo["methods"], "isMagicalPropertyPercentage">, info: IGetUseInfo<typeof ItemEquipInfo>, type: MagicalPropertyType) {
	if (type === this.magicalPropertyFloaty) {
		api.returnValue = true;
	}
}

Testing in game...

image

Amazing! Now we just need the magical property to actually do something.

Implementing Floaty

We want to tweak the Jump action, and lucky for us, all we need to do is inject into a single method again. This time we'll inject into Jump.canUseHandler, which is what determines if the Jump action is usable. We need to do it here because the jump action will be prevented if the player doesn't have enough stamina, and we're reducing the stamina requirement.

@InjectObject(Jump, "canUseHandler", InjectionPosition.Post)
protected onJumpCanUseHandler(api: IInjectionApi<typeof Jump, "canUseHandler">, action: IActionHandlerApi<Human, IJumpCanUse>) {
	const canUse = api.returnValue;
	if (!canUse?.usable && canUse?.message !== Message.TooExhaustedToJump) {
		// do nothing, the jump failed for some other reason than not enough stamina
		return;
	}

	// get a list of the player's equipped items, and add all the floaty values together
	const floatyAmount = action.executor.getEquippedItems()
		.map(item => item.magic.get(this.magicalPropertyFloaty) ?? 0)
		.splat(Math2.sum);

	const stamina = action.executor.stat.get<IStatMax>(Stat.Stamina)!;
	const jumpStamina = Math.floor(((10 + action.executor.getScaledWeight() / 4) / 75) * stamina.max * Math.max(0.1, 1 - floatyAmount));
	// this formula is pulled from the base game's Jump, and all we do is reduce based on floaty     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

	if (stamina.value < jumpStamina) {
		return api.returnValue = {
			usable: false,
			message: Message.TooExhaustedToJump,
		};
	}

	const jumpPosition: IVector3 = {
		x: action.executor.x + (action.executor.direction.x * 2),
		y: action.executor.y + (action.executor.direction.y * 2),
		z: action.executor.z,
	};

	return api.returnValue = {
		usable: true,
		stamina,
		jumpStamina,
		jumpPosition,
	};
}

And there we go... magical property complete! What else could we do...?

Sub-type Magical Properties

Magical properties don't need to just be a singular property for each registration. You can also have a magical property "sub" type. A base game example of this would be MagicalPropertyType.Skill, which can have any SkillType as a subtype.

(TODO finish this guide)

Getting Started

Mod Content

Script Documentation

(apologies for all the missing guides, we'll get to them at some point)

Clone this wiki locally