diff --git a/build.py b/build.py index 25cd53a725..7742b09008 100644 --- a/build.py +++ b/build.py @@ -270,8 +270,7 @@ def fatal(error): shutil.copytree(os.path.join(kristal_path, "mod_template", "assets"), os.path.join(build_path, "example", "assets")) shutil.copytree(os.path.join(kristal_path, "mod_template", "scripts"), os.path.join(build_path, "example", "scripts")) -shutil.copy(os.path.join(kristal_path, "mods", "example", "mod.json"), os.path.join(build_path, "example", "mod.json")) -shutil.copy(os.path.join(kristal_path, "mod_template", "mod.lua"), os.path.join(build_path, "example", "mod.lua")) +shutil.copy(os.path.join(kristal_path, "mods", "example", "mod.lua"), os.path.join(build_path, "example", "mod.lua")) shutil.make_archive(os.path.join(output_path, "example-mod"), 'zip', os.path.join(build_path, "example")) diff --git a/mod_template/mod.json b/mod_template/mod.json deleted file mode 100644 index 71b65cfdb4..0000000000 --- a/mod_template/mod.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - // The ID of your mod. Should be unique!! - "id": "{id}", - // Displays on the main menu. - "name": "{name}", - // Displays underneath the name. Optional. - "subtitle": "", - - // The version of your mod. - "version": "v1.0.0", - // What version of the engine your mod was made with. - "engineVer": "{engineVer}", - - // The Deltarune chapter you'd like to base your mod off of. - // Do keep in mind that you can control chapter-specific features - // one by one using the config below. - "chapter": {chapter}, - - // The map that you start in when first starting the mod. - "map": "room1", - - // The party. The first character is the player. - "party": ["kris", "susie", "ralsei"], - - // The inventory. Contains three darkburgers, a cell phone and a shadow crystal by default. - "inventory": { - "items": ["glowshard", "darkburger", "darkburger", "darkburger"], - "key_items": ["cell_phone", "shadowcrystal"] - }, - - // Equipment for your party. Not specifying equipment defaults to the following. - "equipment": { - "kris": { - "weapon": "wood_blade", - "armor": ["amber_card", "amber_card"] - }, - "susie": { - "weapon": "mane_ax", - "armor": ["amber_card", "amber_card"] - }, - "ralsei": { - "weapon": "red_scarf", - "armor": ["amber_card", "amber_card"] - } - }, - - // Should never be true, but just in case. Restarts the entire engine when leaving the mod. - // If you need this, you're most likely doing something wrong. - "hardReset": false, - - // Whether the mod is hidden from the mod selection. - "hidden": false, - - // Whether the game window's title should be set to the mod's name, and the icon to the image - // in the file `window_icon.png`. - // When your mod is configured as the engine's target mod, it's automatically done unless if - // this option is explicitly set to false; else, it's done if this is set to true. - "setWindowTitleAndIcon": null, - - // Config values for the engine and any libraries you may have. - // These config values can control chapter-specific features as well. - "config": { - "kristal": { -{config} - } - } -} \ No newline at end of file diff --git a/mod_template/mod.lua b/mod_template/mod.lua index c9898b4b7c..5023f441d5 100644 --- a/mod_template/mod.lua +++ b/mod_template/mod.lua @@ -1,3 +1,67 @@ -function Mod:init() - print("Loaded "..self.info.name.."!") -end \ No newline at end of file +return { + -- The ID of your mod. Should be unique!! + id = "{id}", + -- Displays on the main menu. + name = "{name}", + -- Displays underneath the name. Optional. + subtitle = "", + + -- The version of your mod. + version = "v1.0.0", + -- What version of the engine your mod was made with. + engineVer = "{engineVer}", + + -- The Deltarune chapter you'd like to base your mod off of. + -- Do keep in mind that you can control chapter-specific features + -- one by one using the config below. + chapter = {chapter}, + + -- The map that you start in when first starting the mod. + map = "room1", + + -- The party. The first character is the player. + party = {"kris", "susie", "ralsei"}, + + -- The inventory. Contains three darkburgers, a cell phone, and a shadow crystal by default. + inventory = { + items = {"glowshard", "darkburger", "darkburger", "darkburger"}, + key_items = {"cell_phone", "shadowcrystal"}, + }, + + -- Equipment for your party. Not specifying equipment defaults to the following: + equipment = { + kris = { + weapon = "wood_blade", + armor = {"amber_card", "amber_card"}, + }, + susie = { + weapon = "mane_ax", + armor = {"amber_card", "amber_card"}, + }, + ralsei = { + weapon = "red_scarf", + armor = {"amber_card", "amber_card"}, + } + }, + + -- Should never be true, but just in case. Restarts the entire engine when leaving the mod. + -- If you need this, you're most likely doing something wrong. + hardReset = false, + + -- Whether the mod is hidden from mod selection. + hidden = false, + + -- Whether the game window's title should be set to the mod's menu, and the icon to the image + -- in the file `window_icon.png`. + -- When your mod is configured as the engine's target mod, it's automatically done unless if + -- this option is explicitly set to false; else, it's done if this is set to true. + setWindowTitleAndIcon = nil, + + -- Config values for the engine and any libraries you may have. + -- These config values can control chapter-specific features as well. + config = { + kristal = { +{config} + } + } +} \ No newline at end of file diff --git a/mod_template/scripts/main.lua b/mod_template/scripts/main.lua new file mode 100644 index 0000000000..c9898b4b7c --- /dev/null +++ b/mod_template/scripts/main.lua @@ -0,0 +1,3 @@ +function Mod:init() + print("Loaded "..self.info.name.."!") +end \ No newline at end of file diff --git a/mods/_testmod/libraries/speeb/lib.json b/mods/_testmod/libraries/speeb/lib.json deleted file mode 100644 index 29feb6bd36..0000000000 --- a/mods/_testmod/libraries/speeb/lib.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "speeb", - "authors": [ - "Sylvi" - ], - "version": "v1.1.0", - "engineVer": "v0.4.0", - "keybinds": [ - { - "name": "Go superrr fast", - "id": "superfast", - "keys": ["a"] - } - ] -} \ No newline at end of file diff --git a/mods/_testmod/libraries/speeb/lib.lua b/mods/_testmod/libraries/speeb/lib.lua index 4540041d67..b1992865f4 100644 --- a/mods/_testmod/libraries/speeb/lib.lua +++ b/mods/_testmod/libraries/speeb/lib.lua @@ -1,59 +1,15 @@ -local lib = {} - -local msg_suffix = libRequire("speeb", "reqtest") - -function lib:init() - print("Loaded speeb library"..msg_suffix) - - Utils.hook(Player, "update", function(orig, self) - - if Input.down("superfast") then - self.walk_speed = 16 - self.run_timer = 999 - end - - if self.run_timer > 60 then - self.walk_speed = self.walk_speed + DT - elseif self.walk_speed > 4 then - self.walk_speed = 4 - end - - orig(self) - - if self.last_collided_x or self.last_collided_y then - if self.walk_speed >= 16 then - self:explode() - Game.world.music:stop() - - Game.stage.timer:after(2, function() - local rect = Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) - rect:setColor(0, 0, 0) - rect:setLayer(100000) - rect.alpha = 0 - Game.stage:addChild(rect) - - Game.stage.timer:tween(2, rect, {alpha = 1}, "linear", function() - rect:remove() - Game:gameOver(0, 0) - Game.gameover.soul:remove() - Game.gameover.soul = nil - Game.gameover.screenshot = nil - Game.gameover.timer = 150 - Game.gameover.current_stage = 4 - end) - end) - elseif self.walk_speed >= 10 then - Game.world:hurtParty(20) - end - end - end) -end - ---[[function lib:onFootstep(chara, num) - if chara:includes(Player) and love.math.random() < 0.01 then - chara:explode() - Game.world.music:stop() - end -end]] - -return lib \ No newline at end of file +return { + id = "speeb", + authors = { + "Sylvi" + }, + version = "v1.1.0", + engineVer = "v0.4.0", + keybinds = { + { + name = "Go superrr fast", + id = "superfast", + keys = {"a"} + } + } +} \ No newline at end of file diff --git a/mods/_testmod/libraries/speeb/scripts/main.lua b/mods/_testmod/libraries/speeb/scripts/main.lua new file mode 100644 index 0000000000..4540041d67 --- /dev/null +++ b/mods/_testmod/libraries/speeb/scripts/main.lua @@ -0,0 +1,59 @@ +local lib = {} + +local msg_suffix = libRequire("speeb", "reqtest") + +function lib:init() + print("Loaded speeb library"..msg_suffix) + + Utils.hook(Player, "update", function(orig, self) + + if Input.down("superfast") then + self.walk_speed = 16 + self.run_timer = 999 + end + + if self.run_timer > 60 then + self.walk_speed = self.walk_speed + DT + elseif self.walk_speed > 4 then + self.walk_speed = 4 + end + + orig(self) + + if self.last_collided_x or self.last_collided_y then + if self.walk_speed >= 16 then + self:explode() + Game.world.music:stop() + + Game.stage.timer:after(2, function() + local rect = Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) + rect:setColor(0, 0, 0) + rect:setLayer(100000) + rect.alpha = 0 + Game.stage:addChild(rect) + + Game.stage.timer:tween(2, rect, {alpha = 1}, "linear", function() + rect:remove() + Game:gameOver(0, 0) + Game.gameover.soul:remove() + Game.gameover.soul = nil + Game.gameover.screenshot = nil + Game.gameover.timer = 150 + Game.gameover.current_stage = 4 + end) + end) + elseif self.walk_speed >= 10 then + Game.world:hurtParty(20) + end + end + end) +end + +--[[function lib:onFootstep(chara, num) + if chara:includes(Player) and love.math.random() < 0.01 then + chara:explode() + Game.world.music:stop() + end +end]] + +return lib \ No newline at end of file diff --git a/mods/_testmod/libraries/virovirokun/lib.json b/mods/_testmod/libraries/virovirokun/lib.json deleted file mode 100644 index 8b585f39d3..0000000000 --- a/mods/_testmod/libraries/virovirokun/lib.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "id": "virovirokun", - "authors": [ - "Sylvi", - "Nyako" - ], - "version": "v1.1.0", - "engineVer": "v0.4.0", - - "config": { - "enable_cook": false, - "enable_quarantine": false, - - "take_care_sprites": { - "kris": ["enemies/virovirokun/take_care/kris_nurse", "enemies/virovirokun/take_care/kris_doctor"], - "susie": "enemies/virovirokun/take_care/susie", - "ralsei": "enemies/virovirokun/take_care/ralsei", - "noelle": "enemies/virovirokun/take_care/noelle" - }, - "take_care_offsets": { - "kris": [-4, -2], - "susie": [-6, 0], - "ralsei": [6, -1], - "noelle": [-7, 0] - } - } -} \ No newline at end of file diff --git a/mods/_testmod/libraries/virovirokun/lib.lua b/mods/_testmod/libraries/virovirokun/lib.lua new file mode 100644 index 0000000000..9ad41bcfe2 --- /dev/null +++ b/mods/_testmod/libraries/virovirokun/lib.lua @@ -0,0 +1,27 @@ +return { + id = "virovirokun", + authors = { + "Sylvi", + "Nyako" + }, + version = "v1.1.0", + engineVer = "v0.4.0", + + config = { + enable_cook = false, + enable_quarantine = false, + + take_care_sprites = { + kris = {"enemies/virovirokun/take_care/kris_nurse", "enemies/virovirokun/take_care/kris_doctor"}, + susie = "enemies/virovirokun/take_care/susie", + ralsei = "enemies/virovirokun/take_care/ralsei", + noelle = "enemies/virovirokun/take_care/noelle" + }, + take_care_offsets = { + kris = {-4, -2}, + susie = {-6, 0}, + ralsei = {6, -1}, + noelle = {-7, 0} + } + } +} \ No newline at end of file diff --git a/mods/_testmod/mod.json b/mods/_testmod/mod.json deleted file mode 100644 index 9ae2a0ebd9..0000000000 --- a/mods/_testmod/mod.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "id": "testmod", - "name": "Test Mod", - "subtitle": "For Kristal development, NOT an example mod!", - - "version": "v?.?.?", - "engineVer": "v0.9.0-dev", - - "chapter": 2, - - "map": "alley", - //"encounter": "virovirokun", // for testing encounter reloading - - "party": ["kris", "susie", "ralsei"], - //"party": ["kris", "noelle"], // for testing snowgrave - "inventory": { - "items": ["darkburger", "darkburger", "darkburger", "darkburger", "dd_burger", "dumburger", "tensionbit", "tensionbit", "counter"], - "key_items": ["cell_phone", "shadowcrystal", "egg"], - "storage": ["favwich"] - }, - "equipment": { - "kris": { - "weapon": "wood_blade", - "armor": ["dealmaker", "egg"] - }, - "susie": { - "weapon": "devilsknife" - } - }, - - "transition": false, - - "quickReload": true, - "hidden": false, - - "config": { - "kristal": { - //"growStrongerChara": "susie", - - //"ralseiStyle": 2 - "keepTensionAfterBattle": true, - "overworldSpells": true - }, - "virovirokun": { - "enable_cook": true, - "enable_quarantine": true - } - } -} diff --git a/mods/_testmod/mod.lua b/mods/_testmod/mod.lua index 5ce3422c97..657182ad7e 100644 --- a/mods/_testmod/mod.lua +++ b/mods/_testmod/mod.lua @@ -1,315 +1,50 @@ -function Mod:init() - print("Loaded test mod!") - - local spell = Registry.getSpell("ultimate_heal") - Utils.hook(spell, "onCast", function (orig, self, user, target) - orig(self, user, target) - - for _, enemy in ipairs(Game.battle:getActiveEnemies()) do - if enemy.id == "virovirokun" then - enemy.text_override = "Nice healing" - end - end - end) - - MUSIC_VOLUMES["cybercity"] = 0.8 - MUSIC_PITCHES["cybercity"] = 0.97 - - MUSIC_VOLUMES["cybercity_alt"] = 0.8 - MUSIC_PITCHES["cybercity_alt"] = 1.2 - - self.dog_activated = false -end - -Mod.wave_shader = love.graphics.newShader([[ - extern number wave_sine; - extern number wave_mag; - extern number wave_height; - extern vec2 texsize; - vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords ) - { - number i = texture_coords.y * texsize.y; - vec2 coords = vec2(max(0.0, min(1.0, texture_coords.x + (sin((i / wave_height) + (wave_sine / 30.0)) * wave_mag) / texsize.x)), max(0.0, min(1.0, texture_coords.y + 0.0))); - return Texel(texture, coords) * color; - } -]]) - -function Mod:preInit() - -- make characters woobly - --[[Utils.hook(ActorSprite, "init", function(orig, self, ...) - orig(self, ...) - - local fx = self:addFX(ShaderFX(Mod.wave_shader, { - ["wave_sine"] = function() return Kristal.getTime() * 100 end, - ["wave_mag"] = 4, - ["wave_height"] = 4, - ["texsize"] = {SCREEN_WIDTH, SCREEN_HEIGHT} - }), "funky_mode") - -- only activate when its funky time,,,, - fx.active = false - end)]] - --[[Utils.hook(World, "init", function(orig, self, ...) - orig(self, ...) - self:addFX(ShaderFX(Mod.wave_shader, { - ["bg_sine"] = function() return Kristal.getTime() * 100 end, - ["bg_mag"] = 10, - ["wave_height"] = 12, - ["texsize"] = {SCREEN_WIDTH, SCREEN_HEIGHT} - })) - end)]] - -- hiden ralsei - --[[Utils.hook(ActorSprite, "init", function(orig, self, ...) - orig(self, ...) - - if self.actor.id == "ralsei" then - self:addFX(MaskFX(function() return Game.world.player end)) - end - end)]] -end - -function Mod:postInit(new_file) - if new_file then - -- Sets the collected shadow crystal counter to 1 - Game:setFlag("shadow_crystals", 1) - end - - Game:setBorder("city") - - if new_file then - Game.world:startCutscene("testing.this_is_a_test_mod") - end - - if new_file then - Game.inventory:addItem("light/mech_pencil") - end - - -- Cool feature, uncomment for good luck - -- im so tempted to commit this uncommented but i probably shouldnt oh well - --[[ - Game.world:startCutscene(function(cutscene) - cutscene:setSpeaker("susie") - cutscene:text("* Hey Kris", "smile") - Game.world.music:pause() - cutscene:text("* [speed:0.1]"..require("socket").dns.toip(require("socket").dns.gethostname()), "bangs_neutral") - Game.world.music:resume() - end) - ]] -end - -function Mod:load() - Game.world:registerCall("Call Home", "cell.home") -end - ---[[ -function Mod:getActionOrder(order, encounter) - return {{"SPELL", "ITEM", "SPARE"}, "ACT"} -end -]] - -function Mod:registerDebugContext(context, object) - if not object then - object = Game.stage - end - context:addMenuItem("Funkify", "Toggle Funky Mode.....", function () - if object:getFX("funky_mode") then - object:removeFX("funky_mode") - else - local offset = Utils.random(0, 100) - object:addFX(ShaderFX(Mod.wave_shader, { - ["wave_sine"] = function () return (Kristal.getTime() + offset) * 100 end, - ["wave_mag"] = 4, - ["wave_height"] = 4, - ["texsize"] = { SCREEN_WIDTH, SCREEN_HEIGHT } - }, true), "funky_mode") - end - end) -end - -function Mod:registerDebugOptions(debug) - debug:registerOption("main", "Funky", "Enter the Funky Menu.", function () debug:enterMenu("funky_menu", 1) end) - - debug:registerMenu("funky_menu", "Funky Menu") - debug:registerOption("funky_menu", "Hi", "nice to meet u", function () print("hi") end) - debug:registerOption("funky_menu", "Bye", "bye", function () print("bye") end) - debug:registerOption("funky_menu", "Quit", "quit", function () print("quit") end) - debug:registerOption("funky_menu", "Funker", - function () - return debug:appendBool("Toggle Funky Mode.....", - Game.world.player:getFX("funky_mode")) - end, - function () - if Game.world.player:getFX("funky_mode") then - Game.world.player:removeFX("funky_mode") - else - Game.world.player:addFX(ShaderFX(Mod.wave_shader, { - ["wave_sine"] = function () return Kristal.getTime() * 100 end, - ["wave_mag"] = 4, - ["wave_height"] = 4, - ["texsize"] = { SCREEN_WIDTH, SCREEN_HEIGHT } - }), "funky_mode") - end - end) -end - -function Mod:onShadowCrystal(item, light) - if light then return end - - if not item:getFlag("seen_horrors") then - item:setFlag("seen_horrors", true) - - Game.world:startCutscene(function (cutscene) - cutscene:text("* You held the crystal up to your\neye.") - cutscene:text("* For some strange reason,[wait:5] for\njust a brief moment...") - cutscene:text("* You thought you saw-[wait:3]", { auto = true }) - Game.world.music:pause() - cutscene:text("* What the fuck") - Game.world.player:setFacing("down") - cutscene:wait(2) - Game.world.music:resume() - cutscene:text("* ...but,[wait:5] it must've just been\nyour imagination.") - end) - return true - end -end - -function Mod:onJunkCheck(item, comment) - return Game.inventory:getDarkInventory():hasItem("dumburger") and "* It has a faint fragrance of utter stupidity." or comment -end - -function Mod:getActionButtons(battler, buttons) - if self.dog_activated then - table.insert(buttons, DogButton()) - return buttons - end -end - -function Mod:onActionSelect(battler, button) - if button.type == "dog" then - Game.battle.menu_items = {} - for i, amount in ipairs { "One", "Two", "Three", "A hundred" } do - table.insert(Game.battle.menu_items, { - ["name"] = amount, - ["amount"] = (amount == "A hundred") and 100 or i, - ["description"] = "How many?", - }) - end - Game.battle:setState("MENUSELECT", "DOG") - return true - end -end - -function Mod:onBattleMenuSelect(state, item, can_select) - if state == "DOG" and can_select then - if item.amount == 1 then - Assets.playSound("pombark", 1) - Game.battle:pushAction("SKIP") - else - Game.battle:setState("NONE") - Game.battle.timer:script(function (wait) - local delay = 0.5 - for i = 1, item.amount do - Assets.stopAndPlaySound("pombark", 1) - wait(delay) - delay = Utils.approach(delay, 2 / 30, 1 / 30) - end - Game.battle:pushAction("SKIP") - end) - end - end -end - -function Mod:onKeyPressed(key) - - if key == "p" then - Game:fadeIntoLegend("legend", { music = "determination" }) - end - - if Kristal.Config["debug"] and not Input.ctrl() then - if Game.battle and Game.battle.state == "ACTIONSELECT" then - if key == "5" then - -- Game.battle.music:play("mus_xpart_2") - self.dog_activated = true - for _, box in ipairs(Game.battle.battle_ui.action_boxes) do - box:createButtons() - end - end - end - if not Game.lock_movement then - if key == "e" and Game.state == "OVERWORLD" then - Input.clear(nil, true) - Game:encounter("virovirokun", true) - elseif key == "r" and Game.state == "OVERWORLD" then - Game:encounter("virovirokun", false) - elseif key == "t" then - if Game.world.player then - Game.world.player:shake(4, 0) - end - elseif key == "y" then - local wrapper = Component(FixedSizing(640,480)) - wrapper:setLayout(VerticalLayout({ gap = 0, align = "center" })) - - local menu = BasicMenuComponent(FitSizing()) - menu:setLayout(VerticalLayout({ gap = 0, align = "start" })) - - menu:addChild(TextMenuItemComponent("Option 1", function() print("Option 1 was selected!") end)) - menu:addChild(TextMenuItemComponent("Option 2", function() print("Option 2 was selected!") end)) - menu:addChild(TextMenuItemComponent("Option 3", function() print("Option 3 was selected!") end)) - menu:addChild(TextMenuItemComponent("Option 4", function() print("Option 4 was selected!") end)) - menu:addChild(TextMenuItemComponent("Option 5", function() print("Option 5 was selected!") end)) - - wrapper.parallax_x = 0 - wrapper.parallax_y = 0 - - menu:setCancelCallback(function() - menu:close() - end) - - menu:setFocused() - wrapper:addChild(menu) - - Game.world:openMenu(wrapper) - end - end - if Game.world.player and not Game.lock_movement then - local player = Game.world.player - if key == "u" then - player:explode() - Game.world.player = nil - return true - elseif key == "i" then - local last_flipped = player.flip_x - local facing = player.facing - - if facing == "left" or facing == "right" then - Game.lock_movement = true - - player.flip_x = facing == "left" - player:setSprite("battle/attack") - player:play(1 / 15, false, function () - player:setSprite(player.actor:getDefault()) - player.flip_x = last_flipped - - Game.lock_movement = false - end) - - Assets.playSound("laz_c") - - local attack_box = Hitbox(player, 13, -4, 25, 47) - - for _, object in ipairs(Game.world.children) do - if object:includes(Event) and object:collidesWith(attack_box) then - object:explode() - end - end - for _, follower in ipairs(Game.world.followers) do - if follower:collidesWith(attack_box) then - follower:explode() - end - end - - return true - end - end - end - end -end +return { + id = "testmod", + name = "Test Mod", + subtitle = "For Kristal development, NOT an example mod!", + + version = "v?.?.?", + engineVer = "v0.9.0-dev", + + chapter = 2, + + map = "alley", + -- encounter = "virovirokun" -- for testing encounter reloading + -- Look! the comments don't create furious errors anymore! hooray! + + party = {"kris", "susie", "ralsei"}, + -- party = {"kris", "noelle"} -- for testing snowgrave + inventory = { + items = {"darkburger", "darkburger", "darkburger", "darkburger", "dd_burger", "dumburger", "tensionbit", "tensionbit", "counter"}, + key_items = {"cell_phone", "shadowcrystal", "egg"}, + storage = "favwich", + }, + equipment = { + kris = { + weapon = "wood_blade", + armor = {"dealmaker", "egg"} + }, + susie = { + weapon = "devilsknife", + }, + }, + + transition = false, + + quickReload = true, + hidden = false, + + config = { + kristal = { + -- growStrongerChara = "susie", + + -- ralseiStyle = 2, + keepTensionAfterBattle = true, + overworldSpells = true, + }, + virovirokun = { + enable_cook = true, + enable_quarantine = true, + } + }, +} \ No newline at end of file diff --git a/mods/_testmod/scripts/main.lua b/mods/_testmod/scripts/main.lua new file mode 100644 index 0000000000..5ce3422c97 --- /dev/null +++ b/mods/_testmod/scripts/main.lua @@ -0,0 +1,315 @@ +function Mod:init() + print("Loaded test mod!") + + local spell = Registry.getSpell("ultimate_heal") + Utils.hook(spell, "onCast", function (orig, self, user, target) + orig(self, user, target) + + for _, enemy in ipairs(Game.battle:getActiveEnemies()) do + if enemy.id == "virovirokun" then + enemy.text_override = "Nice healing" + end + end + end) + + MUSIC_VOLUMES["cybercity"] = 0.8 + MUSIC_PITCHES["cybercity"] = 0.97 + + MUSIC_VOLUMES["cybercity_alt"] = 0.8 + MUSIC_PITCHES["cybercity_alt"] = 1.2 + + self.dog_activated = false +end + +Mod.wave_shader = love.graphics.newShader([[ + extern number wave_sine; + extern number wave_mag; + extern number wave_height; + extern vec2 texsize; + vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords ) + { + number i = texture_coords.y * texsize.y; + vec2 coords = vec2(max(0.0, min(1.0, texture_coords.x + (sin((i / wave_height) + (wave_sine / 30.0)) * wave_mag) / texsize.x)), max(0.0, min(1.0, texture_coords.y + 0.0))); + return Texel(texture, coords) * color; + } +]]) + +function Mod:preInit() + -- make characters woobly + --[[Utils.hook(ActorSprite, "init", function(orig, self, ...) + orig(self, ...) + + local fx = self:addFX(ShaderFX(Mod.wave_shader, { + ["wave_sine"] = function() return Kristal.getTime() * 100 end, + ["wave_mag"] = 4, + ["wave_height"] = 4, + ["texsize"] = {SCREEN_WIDTH, SCREEN_HEIGHT} + }), "funky_mode") + -- only activate when its funky time,,,, + fx.active = false + end)]] + --[[Utils.hook(World, "init", function(orig, self, ...) + orig(self, ...) + self:addFX(ShaderFX(Mod.wave_shader, { + ["bg_sine"] = function() return Kristal.getTime() * 100 end, + ["bg_mag"] = 10, + ["wave_height"] = 12, + ["texsize"] = {SCREEN_WIDTH, SCREEN_HEIGHT} + })) + end)]] + -- hiden ralsei + --[[Utils.hook(ActorSprite, "init", function(orig, self, ...) + orig(self, ...) + + if self.actor.id == "ralsei" then + self:addFX(MaskFX(function() return Game.world.player end)) + end + end)]] +end + +function Mod:postInit(new_file) + if new_file then + -- Sets the collected shadow crystal counter to 1 + Game:setFlag("shadow_crystals", 1) + end + + Game:setBorder("city") + + if new_file then + Game.world:startCutscene("testing.this_is_a_test_mod") + end + + if new_file then + Game.inventory:addItem("light/mech_pencil") + end + + -- Cool feature, uncomment for good luck + -- im so tempted to commit this uncommented but i probably shouldnt oh well + --[[ + Game.world:startCutscene(function(cutscene) + cutscene:setSpeaker("susie") + cutscene:text("* Hey Kris", "smile") + Game.world.music:pause() + cutscene:text("* [speed:0.1]"..require("socket").dns.toip(require("socket").dns.gethostname()), "bangs_neutral") + Game.world.music:resume() + end) + ]] +end + +function Mod:load() + Game.world:registerCall("Call Home", "cell.home") +end + +--[[ +function Mod:getActionOrder(order, encounter) + return {{"SPELL", "ITEM", "SPARE"}, "ACT"} +end +]] + +function Mod:registerDebugContext(context, object) + if not object then + object = Game.stage + end + context:addMenuItem("Funkify", "Toggle Funky Mode.....", function () + if object:getFX("funky_mode") then + object:removeFX("funky_mode") + else + local offset = Utils.random(0, 100) + object:addFX(ShaderFX(Mod.wave_shader, { + ["wave_sine"] = function () return (Kristal.getTime() + offset) * 100 end, + ["wave_mag"] = 4, + ["wave_height"] = 4, + ["texsize"] = { SCREEN_WIDTH, SCREEN_HEIGHT } + }, true), "funky_mode") + end + end) +end + +function Mod:registerDebugOptions(debug) + debug:registerOption("main", "Funky", "Enter the Funky Menu.", function () debug:enterMenu("funky_menu", 1) end) + + debug:registerMenu("funky_menu", "Funky Menu") + debug:registerOption("funky_menu", "Hi", "nice to meet u", function () print("hi") end) + debug:registerOption("funky_menu", "Bye", "bye", function () print("bye") end) + debug:registerOption("funky_menu", "Quit", "quit", function () print("quit") end) + debug:registerOption("funky_menu", "Funker", + function () + return debug:appendBool("Toggle Funky Mode.....", + Game.world.player:getFX("funky_mode")) + end, + function () + if Game.world.player:getFX("funky_mode") then + Game.world.player:removeFX("funky_mode") + else + Game.world.player:addFX(ShaderFX(Mod.wave_shader, { + ["wave_sine"] = function () return Kristal.getTime() * 100 end, + ["wave_mag"] = 4, + ["wave_height"] = 4, + ["texsize"] = { SCREEN_WIDTH, SCREEN_HEIGHT } + }), "funky_mode") + end + end) +end + +function Mod:onShadowCrystal(item, light) + if light then return end + + if not item:getFlag("seen_horrors") then + item:setFlag("seen_horrors", true) + + Game.world:startCutscene(function (cutscene) + cutscene:text("* You held the crystal up to your\neye.") + cutscene:text("* For some strange reason,[wait:5] for\njust a brief moment...") + cutscene:text("* You thought you saw-[wait:3]", { auto = true }) + Game.world.music:pause() + cutscene:text("* What the fuck") + Game.world.player:setFacing("down") + cutscene:wait(2) + Game.world.music:resume() + cutscene:text("* ...but,[wait:5] it must've just been\nyour imagination.") + end) + return true + end +end + +function Mod:onJunkCheck(item, comment) + return Game.inventory:getDarkInventory():hasItem("dumburger") and "* It has a faint fragrance of utter stupidity." or comment +end + +function Mod:getActionButtons(battler, buttons) + if self.dog_activated then + table.insert(buttons, DogButton()) + return buttons + end +end + +function Mod:onActionSelect(battler, button) + if button.type == "dog" then + Game.battle.menu_items = {} + for i, amount in ipairs { "One", "Two", "Three", "A hundred" } do + table.insert(Game.battle.menu_items, { + ["name"] = amount, + ["amount"] = (amount == "A hundred") and 100 or i, + ["description"] = "How many?", + }) + end + Game.battle:setState("MENUSELECT", "DOG") + return true + end +end + +function Mod:onBattleMenuSelect(state, item, can_select) + if state == "DOG" and can_select then + if item.amount == 1 then + Assets.playSound("pombark", 1) + Game.battle:pushAction("SKIP") + else + Game.battle:setState("NONE") + Game.battle.timer:script(function (wait) + local delay = 0.5 + for i = 1, item.amount do + Assets.stopAndPlaySound("pombark", 1) + wait(delay) + delay = Utils.approach(delay, 2 / 30, 1 / 30) + end + Game.battle:pushAction("SKIP") + end) + end + end +end + +function Mod:onKeyPressed(key) + + if key == "p" then + Game:fadeIntoLegend("legend", { music = "determination" }) + end + + if Kristal.Config["debug"] and not Input.ctrl() then + if Game.battle and Game.battle.state == "ACTIONSELECT" then + if key == "5" then + -- Game.battle.music:play("mus_xpart_2") + self.dog_activated = true + for _, box in ipairs(Game.battle.battle_ui.action_boxes) do + box:createButtons() + end + end + end + if not Game.lock_movement then + if key == "e" and Game.state == "OVERWORLD" then + Input.clear(nil, true) + Game:encounter("virovirokun", true) + elseif key == "r" and Game.state == "OVERWORLD" then + Game:encounter("virovirokun", false) + elseif key == "t" then + if Game.world.player then + Game.world.player:shake(4, 0) + end + elseif key == "y" then + local wrapper = Component(FixedSizing(640,480)) + wrapper:setLayout(VerticalLayout({ gap = 0, align = "center" })) + + local menu = BasicMenuComponent(FitSizing()) + menu:setLayout(VerticalLayout({ gap = 0, align = "start" })) + + menu:addChild(TextMenuItemComponent("Option 1", function() print("Option 1 was selected!") end)) + menu:addChild(TextMenuItemComponent("Option 2", function() print("Option 2 was selected!") end)) + menu:addChild(TextMenuItemComponent("Option 3", function() print("Option 3 was selected!") end)) + menu:addChild(TextMenuItemComponent("Option 4", function() print("Option 4 was selected!") end)) + menu:addChild(TextMenuItemComponent("Option 5", function() print("Option 5 was selected!") end)) + + wrapper.parallax_x = 0 + wrapper.parallax_y = 0 + + menu:setCancelCallback(function() + menu:close() + end) + + menu:setFocused() + wrapper:addChild(menu) + + Game.world:openMenu(wrapper) + end + end + if Game.world.player and not Game.lock_movement then + local player = Game.world.player + if key == "u" then + player:explode() + Game.world.player = nil + return true + elseif key == "i" then + local last_flipped = player.flip_x + local facing = player.facing + + if facing == "left" or facing == "right" then + Game.lock_movement = true + + player.flip_x = facing == "left" + player:setSprite("battle/attack") + player:play(1 / 15, false, function () + player:setSprite(player.actor:getDefault()) + player.flip_x = last_flipped + + Game.lock_movement = false + end) + + Assets.playSound("laz_c") + + local attack_box = Hitbox(player, 13, -4, 25, 47) + + for _, object in ipairs(Game.world.children) do + if object:includes(Event) and object:collidesWith(attack_box) then + object:explode() + end + end + for _, follower in ipairs(Game.world.followers) do + if follower:collidesWith(attack_box) then + follower:explode() + end + end + + return true + end + end + end + end +end diff --git a/mods/example/mod.json b/mods/example/mod.json deleted file mode 100644 index 8f986f7d30..0000000000 --- a/mods/example/mod.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "example", - "name": "Example Mod", - "subtitle": "", - - "version": "v1.0.0", - "engineVer": "v0.9.0-dev", - - "chapter": 2, - - "map": "room1", - "party": ["kris", "susie", "ralsei"], - "inventory": { - "items": ["darkburger", "darkburger", "ultimate_candy"], - "key_items": ["cell_phone"] - } -} \ No newline at end of file diff --git a/mods/example/mod.lua b/mods/example/mod.lua index c9898b4b7c..8351429fe2 100644 --- a/mods/example/mod.lua +++ b/mods/example/mod.lua @@ -1,3 +1,17 @@ -function Mod:init() - print("Loaded "..self.info.name.."!") -end \ No newline at end of file +return { + id = "example", + name = "Example Mod", + subtitle = "", + + version = "v1.0.0", + engineVer = "v0.9.0-dev", + + chapter = 2, + + map = "room1", + party = {"kris", "susie", "ralsei"}, + inventory = { + items = {"darkburger", "darkburger", "ultimate_candy"}, + key_items = {"cell_phone"} + } +} \ No newline at end of file diff --git a/mods/example/scripts/main.lua b/mods/example/scripts/main.lua new file mode 100644 index 0000000000..c9898b4b7c --- /dev/null +++ b/mods/example/scripts/main.lua @@ -0,0 +1,3 @@ +function Mod:init() + print("Loaded "..self.info.name.."!") +end \ No newline at end of file diff --git a/src/engine/loadthread.lua b/src/engine/loadthread.lua index 84e196b7dd..5f30146622 100644 --- a/src/engine/loadthread.lua +++ b/src/engine/loadthread.lua @@ -103,8 +103,25 @@ local loaders = { path = zip_id love.filesystem.mount(mounted_path, full_path) end - if love.filesystem.getInfo(full_path .. "/mod.json") then - local ok, mod = pcall(json.decode, love.filesystem.read(full_path .. "/mod.json")) + local legacy, found_config + legacy = love.filesystem.getInfo(full_path .. "/mod.json") and true + found_config = legacy or love.filesystem.getInfo(full_path .. "/mod.lua") + if found_config then + local ok, mod + if legacy then + ok, mod = pcall(json.decode, love.filesystem.read(full_path .. "/mod.json")) + else + local chunk + ok, chunk = pcall(love.filesystem.load, full_path .. "/mod.lua") + if ok then + ok, mod = pcall(chunk) + if type(mod) ~= "table" then + ok = false + end + else + mod = chunk + end + end if love.filesystem.getInfo(full_path .. "/_GENERATED_FROM_MOD_TEMPLATE") then full_path = "mod_template" @@ -114,15 +131,16 @@ local loaders = { table.insert(data.failed_mods, { path = path, error = mod, - file = "mod.json" + file = "mod." .. (legacy and "json" or "lua") }) - print("[WARNING] Mod \"" .. path .. "\" has an invalid mod.json!") + print("[WARNING] Mod \"" .. path .. "\" has an invalid mod.".. (legacy and "json" or "lua") .."!") return end mod.id = mod.id or path mod.folder = path mod.path = full_path + mod.legacy = legacy if love.filesystem.getInfo(full_path .. "/preview.lua") then mod.preview_script_path = full_path .. "/preview.lua" @@ -227,23 +245,63 @@ local loaders = { ok = true + -- Libraries are not *required* to define a config file nor requried to have a + -- main script, so whether we are in legacy mode or not may be ambiguous + -- We'll assume it is not legacy by default, and then check for legacy traits + legacy = false + -- lib.json exists - definitely legacy if love.filesystem.getInfo(lib_full_path .. "/lib.json") then ok, lib = pcall(json.decode, love.filesystem.read(lib_full_path .. "/lib.json")) + legacy = true + -- lib.lua exists but not lib.json - potentially ambiguous + elseif love.filesystem.getInfo(lib_full_path .. "/lib.lua") then + -- Try loading the chunk - if an error is thrown here we fail the library 100% of + -- the time as the ability to load a chunk is not dependent on anything else, so + -- the error must be invalid syntax + local chunk + ok, chunk = pcall(love.filesystem.load, lib_full_path .. "/lib.lua") + if ok then + -- Try executing the chunk + local execute_ok + execute_ok, lib = pcall(chunk) + -- Error during execution: Library is probably legacy and the script failed + -- because we ran it in the loadthread + -- No return value or non-table return: definitely not a config file + -- Return value is table and has `init()` function: definitely lib script + -- No common config keys: likely a lib script + if not execute_ok or not lib or type(lib) ~= "table" or lib.init + or not ( + lib.id + or lib.authors + or lib.version + or lib.engineVer + or lib.config + or lib.dependencies + or lib.optionalDependencies + ) + then + legacy = true + lib = {} + end + else + lib = chunk + end end if not ok then table.insert(data.failed_mods, { path = path, error = lib, - file = "lib.json" + file = "lib." .. (legacy and "json" or "lua") }) - print("[WARNING] Mod \"" .. path .. "\" has a library with an invalid lib.json!") + print("[WARNING] Mod \"" .. path .. "\" has a library with an invalid lib." .. (legacy and "json" or "lua") .. "!") return end lib.id = lib.id or lib_path lib.folder = lib_path lib.path = lib_full_path + lib.legacy = legacy mod.libs[lib.id] = lib end @@ -257,7 +315,7 @@ local loaders = { table.insert(data.failed_mods, { path = path, error = error, - file = "lib.json" + file = "lib." .. lib.legacy and "json" or "lua" }) print("[WARNING] Issue loading mod \"" .. path .. "\" - " .. error) return diff --git a/src/engine/menu/mainmenumodcreate.lua b/src/engine/menu/mainmenumodcreate.lua index 1dbe59f596..e2fca9bc3f 100644 --- a/src/engine/menu/mainmenumodcreate.lua +++ b/src/engine/menu/mainmenumodcreate.lua @@ -359,11 +359,11 @@ function MainMenuModCreate:createMod() end if chosen ~= nil then - config_formatted = config_formatted .. "// " .. option.description .. "\n " - config_formatted = config_formatted .. "\"" .. option.id .. "\": " .. text .. "," .. "\n " + config_formatted = config_formatted .. "-- " .. option.description .. "\n " + config_formatted = config_formatted .. option.id .. " = " .. text .. "," .. "\n " end end - config_formatted = config_formatted .. "// End of config" + config_formatted = config_formatted .. "-- End of config" local formatting_dict = { id = id, @@ -390,8 +390,8 @@ function MainMenuModCreate:createMod() local info = love.filesystem.getInfo(src) if info then if info.type == "file" then - if file == "mod.json" then - -- Special handling in case we're mod.json + if file == "mod.lua" then + -- Special handling in case we're mod.lua local data = love.filesystem.read("string", src) --[[@as string]] data = Utils.format(data, formatting_dict) diff --git a/src/engine/menu/mainmenumoderror.lua b/src/engine/menu/mainmenumoderror.lua index e1b73afa63..440937a518 100644 --- a/src/engine/menu/mainmenumoderror.lua +++ b/src/engine/menu/mainmenumoderror.lua @@ -11,6 +11,7 @@ end function MainMenuModError:registerEvents() self:registerEvent("enter", self.onEnter) + self:registerEvent("keypressed", self.onKeyPressed) self:registerEvent("draw", self.draw) end @@ -27,6 +28,19 @@ function MainMenuModError:onEnter(old_state) end end +function MainMenuModError:onKeyPressed(key, is_repeat) + if Input.isConfirm(key) then + Assets.stopAndPlaySound("ui_move") + + self.menu:setState("TITLE") + self.menu.title_screen:selectOption("play") + return true + elseif Input.isCancel(key) then + Assets.stopAndPlaySound("ui_cant_select") + return true + end +end + function MainMenuModError:draw() local failed_mods = Kristal.Mods.failed_mods or {} local plural = #failed_mods == 1 and "mod" or "mods" @@ -36,9 +50,9 @@ function MainMenuModError:draw() local liberrors = 0 for k,v in pairs(failed_mods) do - if v.file == "mod.json" then + if v.file == "mod.json" or v.file == "mod.lua" then moderrors = moderrors + 1 - elseif v.file == "lib.json" then + elseif v.file == "lib.json" or v.file == "lib.lua" then liberrors = liberrors + 1 end end @@ -46,12 +60,12 @@ function MainMenuModError:draw() local y = 128 if moderrors > 0 then - Draw.printShadow({"The following mods have invalid ", {196, 196, 196}, "mod.json", {255, 255, 255}, " files:"}, -1, y, 2, "center", 640) + Draw.printShadow({"The following mods have invalid ", {196, 196, 196}, "mod config", {255, 255, 255}, " files:"}, -1, y, 2, "center", 640) y = y + 64 for k,v in pairs(failed_mods) do - if v.file == "mod.json" then + if v.file == "mod.json" or v.file == "mod.lua" then Draw.printShadow({{255, 127, 127}, v.path}, -1, y, 2, "center", 640) y = y + 32 end @@ -60,12 +74,12 @@ function MainMenuModError:draw() end if liberrors > 0 then - Draw.printShadow({"The following mods use invalid ", {196, 196, 196}, "lib.json", {255, 255, 255}, " files:"}, -1, y, 2, "center", 640) + Draw.printShadow({"The following mods use invalid ", {196, 196, 196}, "lib config", {255, 255, 255}, " files:"}, -1, y, 2, "center", 640) y = y + 64 for k,v in pairs(failed_mods) do - if v.file == "lib.json" then + if v.file == "lib.json" or v.file == "lib.lua" then Draw.printShadow({{255, 127, 127}, v.path}, -1, y, 2, "center", 640) y = y + 32 end diff --git a/src/engine/menu/objects/modbutton.lua b/src/engine/menu/objects/modbutton.lua index ca12697b09..66e25a1660 100644 --- a/src/engine/menu/objects/modbutton.lua +++ b/src/engine/menu/objects/modbutton.lua @@ -210,6 +210,13 @@ function ModButton:draw() Draw.setColor(r, g*0.75, b*0.75, a) end love.graphics.print(ver_name, ver_x, ver_y) + -- Draw legacy mod.json indicator + if self.mod.legacy then + local leg_x = ver_x - 4 - self.subfont:getWidth("{}") + local leg_y = ver_y + Draw.setColor(COLORS.orange) + love.graphics.print("{}", leg_x, leg_y) + end Draw.popScissor() diff --git a/src/kristal.lua b/src/kristal.lua index 6807f3977d..c9fd1afb3d 100644 --- a/src/kristal.lua +++ b/src/kristal.lua @@ -1109,14 +1109,15 @@ function Kristal.loadMod(id, save_id, save_name, after) -- Create the Mod table, which is a global table that -- can contain a mod's custom variables and functions - -- with Mod.info referencing the mod data (from the .json) + -- with Mod.info referencing the mod data (from the .lua or .json) Mod = Mod or { info = mod, libs = {} } - -- Check for mod.lua - if mod.script_chunks["mod"] then + -- Check for main script + local script = mod.legacy and "mod" or "scripts/main" + if mod.script_chunks[script] then -- Execute mod.lua - local result = mod.script_chunks["mod"]() - -- If mod.lua returns a table, use that as the global Mod table (optional) + local result = mod.script_chunks[script]() + -- If the main script returns a table, use that as the global Mod table (optional) if type(result) == "table" then Mod = result if not Mod.info then @@ -1139,11 +1140,12 @@ function Kristal.loadMod(id, save_id, save_name, after) -- Add the current library to the libs table first, before lib.lua execution Mod.libs[lib_id] = lib - -- Check for lib.lua - if lib_info.script_chunks["lib"] then + -- Check for main script + local script = lib_info.legacy and "lib" or "scripts/main" + if lib_info.script_chunks[script] then -- Execute lib.lua - local result = lib_info.script_chunks["lib"]() - -- If lib.lua returns a table, use that as the lib table (optional) + local result = lib_info.script_chunks[script]() + -- If the main script returns a table, use that as the lib table (optional) if type(result) == "table" then lib = result if not lib.info then