Lua Conversion Guide
From PMDOWiki
While writing your own custom Lua scripts, there could be several instances where you might replicate the engine source code and modify it from there. However, it can be tricky translating C# into Lua, especially with C# methods and classes involving generics.
Examples
Below are some C# to Lua conversion cases you might encounter:
Calling C# Enum
Dir8
C#:
Dir8 dir = Dir8.DownLeft;
Lua:
local dir = RogueElements.Dir8.DownLeft
- Notice how RogueElements is prepended in the Lua implementation. This is because it is located under the RogueElements namespace. This is essential to keep notice when working with the engine's C# classes.
Creating C# Class
Loc
C#:
Loc origin = new Loc(10, 10); Loc origin2 = new Loc(25); Console.WriteLine(origin.X); Console.WriteLine(Loc.Dot(origin, origin2)); Console.WriteLine(origin2.ToString());
Lua:
local origin = RogueElements.Loc(10, 10) local origin2 = RogueElements.Loc(25) PrintInfo(tostring(origin.X)) PrintInfo(tostring(RogueElements.Loc.Dot(origin, origin2))) PrintInfo(origin:ToString())
- Note that you can call any of the class's constructor
- In Lua, you can access a class member variable through the "." operator
- You also use the "." operator for static methods
- To call a class method (not static) is different. You use the ":" operator in Lua instead
Calling C# Generic Method
StateCollections::GetWithDefault<K>
C#:
AbilityLearnContext learn = context.ContextStates.GetWithDefault<AbilityLearnContext>();
Lua:
AbilityLearnContextType = luanet.import_type('PMDC.Dungeon.AbilityLearnContext') local learn = context.ContextStates:GetWithDefault(luanet.ctype(AbilityLearnContextType))
- Notice that in Lua, you have to import the type directly using luanet.import_type.
- It is also important to note that PMDC.Dungeon is prepended because AbilityLearnContext is located at under the namespace here.
GameContext::GetContextStateInt<T>(int)
C#
int contextVal = context.GetContextStateInt<TargetAtkBoost>(0);
Lua
TargetAtkBoostType = luanet.import_type('PMDC.Dungeon.TargetAtkBoost') local context_val = context:GetContextStateInt(luanet.ctype(TargetAtkBoostType), 0)
Create a Generic C# Class
List<T>
C#:
List<MobSpawn> allSpawns = new List<MobSpawn>();
Lua:
ListType = luanet.import_type('System.Collections.Generic.List`1') MobSpawnType = luanet.import_type('RogueEssence.LevelGen.MobSpawn') local all_spawns = LUA_ENGINE:MakeGenericType( ListType, { MobSpawnType }, { })
PlaceEntranceMobsStep<T, TEntrance>
C#:
SpecificTeamSpawner specificTeam = SpecificTeamSpawner(); MobSpawn postMob = MobSpawn(); postMob.BaseForm = MonsterID("oshawott", 0, "normal", Gender.Unknown); postMob.Tactic = "boss"; mostMob.Level = RandRange(50); specificTeam.Spawns.Add(postMob); PresetMultiTeamSpawner picker = new PresetMultiTeamSpawner<MapGenContext>(); picker.Spawns.Add(specificTeam); PlaceEntranceMobsStep mobPlacement = new PlaceEntranceMobsStep<MapGenContext, MapGenEntrance>(picker);
Lua:
PresetMultiTeamSpawnerType = luanet.import_type('RogueEssence.LevelGen.PresetMultiTeamSpawner`1') EntranceType = luanet.import_type('RogueEssence.LevelGen.MapGenEntrance') PlaceEntranceMobsStepType = luanet.import_type('RogueEssence.LevelGen.PlaceEntranceMobsStep`2') MapGenContextType = luanet.import_type('RogueEssence.LevelGen.ListMapGenContext') local specific_team = RogueEssence.LevelGen.SpecificTeamSpawner() local post_mob = RogueEssence.LevelGen.MobSpawn() post_mob.BaseForm = RogueEssence.Dungeon.MonsterID("oshawott", 0, "normal", Gender.Unknown) post_mob.Tactic = "boss" post_mob.Level = RogueElements.RandRange(50) specific_team.Spawns:Add(post_mob) local picker = LUA_ENGINE:MakeGenericType(PresetMultiTeamSpawnerType, { MapGenContextType }, { }) picker.Spawns:Add(specific_team) local mob_placement = LUA_ENGINE:MakeGenericType(PlaceEntranceMobsStepType, { MapGenContextType, EntranceType }, { picker })
- Notice the `1 and `2 appended at the end of the import_type. These represent how many generics the class requires.
- The LUA_ENGINE:MakeGenericType method takes in three parameters:
- The 1st parameter is what generic type to create
- The 2nd parameter is a table of types necessary to create the generic type
- The 3rd parameter is a table of values that is called with the constructor of the generic type
Source Code Conversion Example
LeechSeedEvent
C#:
public override IEnumerator<YieldInstruction> Apply(GameEventOwner owner, Character ownerChar, SingleCharContext context) { if (context.User.CharStates.Contains<MagicGuardState>()) yield break; //check for someone within 4 tiles away; if there's no one, then remove the status List<Character> targets = AreaAction.GetTargetsInArea(context.User, context.User.CharLoc, Alignment.Foe, 4); int lowestDist = Int32.MaxValue; Character target = null; for (int ii = 0; ii < targets.Count; ii++) { int newDist = (targets[ii].CharLoc - context.User.CharLoc).DistSquared(); if (newDist < lowestDist) { target = targets[ii]; lowestDist = newDist; } } if (target == null) yield return CoroutineManager.Instance.StartCoroutine(context.User.RemoveStatusEffect(((StatusEffect)owner).ID)); else { int seeddmg = Math.Max(1, context.User.MaxHP / 12); DungeonScene.Instance.LogMsg(Text.FormatGrammar(new StringKey("MSG_LEECH_SEED").ToLocal(), context.User.GetDisplayName(false))); GameManager.Instance.BattleSE("DUN_Hit_Neutral"); if (!context.User.Unidentifiable) { SingleEmitter endEmitter = new SingleEmitter(new AnimData("Hit_Neutral", 3)); endEmitter.SetupEmit(context.User.MapLoc, context.User.MapLoc, context.User.CharDir); DungeonScene.Instance.CreateAnim(endEmitter, DrawLayer.NoDraw); } yield return CoroutineManager.Instance.StartCoroutine(context.User.InflictDamage(seeddmg, false)); if (context.User.CharStates.Contains<DrainDamageState>()) { GameManager.Instance.BattleSE("DUN_Toxic"); DungeonScene.Instance.LogMsg(Text.FormatGrammar(new StringKey("MSG_LIQUID_OOZE").ToLocal(), target.GetDisplayName(false))); yield return CoroutineManager.Instance.StartCoroutine(target.InflictDamage(seeddmg * 4, false)); } else if (target.HP < target.MaxHP) { yield return CoroutineManager.Instance.StartCoroutine(target.RestoreHP(seeddmg, false)); } } }
Lua:
SINGLE_CHAR_SCRIPT = {} DrainDamageStateType = luanet.import_type('PMDC.Dungeon.DrainDamageState') MagicGuardStateType = luanet.import_type('PMDC.Dungeon.MagicGuardState') function SINGLE_CHAR_SCRIPT.LeechSeedEvent(owner, ownerChar, context, args) if context.User.CharStates:Contains(luanet.ctype(MagicGuardStateType)) then return end local targets = RogueEssence.Dungeon.AreaAction.GetTargetsInArea(context.User, context.User.CharLoc, RogueEssence.Dungeon.Alignment.Foe, 4) local lowestDist = 10000 local target = nil for ii = 0, targets.Count - 1, 1 do local newDist = (targets[ii].CharLoc - context.User.CharLoc):DistSquared() if newDist < lowestDist then target = targets[ii] lowestDist = newDist end end if target == nil then TASK:WaitTask(context.User:RemoveStatusEffect(owner:GetID())) return else local seeddmg = math.max(1, context.User.MaxHP / 12) _DUNGEON:LogMsg(STRINGS:Format(RogueEssence.StringKey("MSG_LEECH_SEED"):ToLocal(), context.User:GetDisplayName(false))) SOUND:PlayBattleSE('DUN_Hit_Neutral') if not context.User.Unidentifiable then local endEmitter = RogueEssence.Content.SingleEmitter(RogueEssence.Content.AnimData("Hit_Neutral", 3)) endEmitter:SetupEmit(context.User.MapLoc, context.User.MapLoc, context.User.CharDir) _DUNGEON:CreateAnim(endEmitter, RogueEssence.Content.DrawLayer.NoDraw) end TASK:WaitTask(context.User:InflictDamage(seeddmg, false)) if context.User.CharStates:Contains(luanet.ctype(MagicGuardStateType)) then SOUND:PlayBattleSE('DUN_Toxic') _DUNGEON:LogMsg(STRINGS:Format(RogueEssence.StringKey("MSG_LIQUID_OOZE"):ToLocal(), target:GetDisplayName(false))) TASK:WaitTask(target:InflictDamage(seeddmg * 4, false)) elseif target.HP < target.MaxHP then TASK:WaitTask(target:RestoreHP(seeddmg, false)) end end end
This concludes converting Lua to C# code! Knowing how and when to call and use the source engine's classes, methods, and enums allows you write more complex battle events beyond the Dev menu!