├── LICENSE ├── README.md ├── docs ├── concepts │ ├── entities.md │ └── worlds.md ├── intro.md └── user guide │ ├── ai_modules.md │ ├── components.md │ ├── covens.md │ ├── entities.md │ ├── goap_actions.md │ ├── loot_tables.md │ ├── migrating.md │ ├── navigation.md │ ├── npcs.md │ ├── quick_start.md │ ├── schedules.md │ ├── stealth_provider.md │ └── tools.md ├── item_entity_template.tscn ├── item_template.tscn ├── light probe test.tscn ├── light_probe.tscn ├── npc_entity_template.tscn ├── npc_template.tscn ├── octa.mtl ├── octa.obj ├── octa.obj.import ├── plugin.cfg ├── scripts ├── ai │ ├── Modules │ │ ├── default_crime_report.gd │ │ ├── default_damage_module.gd │ │ ├── default_interact_response.gd │ │ ├── default_movement.gd │ │ ├── default_stealth_detection.gd │ │ └── default_threat_response.gd │ ├── PerceptionFSM │ │ ├── machine_perception.gd │ │ ├── state_aware_invisible.gd │ │ ├── state_aware_visible.gd │ │ ├── state_lost.gd │ │ └── state_unaware.gd │ ├── ai_module.gd │ ├── goap_action.gd │ ├── light_estimation_provider.gd │ ├── perception_ears.gd │ └── perception_eyes.gd ├── barter │ ├── barter.gd │ └── transaction.gd ├── components │ ├── attributes_component.gd │ ├── chest_component.gd │ ├── covens_component.gd │ ├── damageable_component.gd │ ├── effects_component.gd │ ├── equipment_component.gd │ ├── goap_component.gd │ ├── interactive_component.gd │ ├── inventory_component.gd │ ├── item_component.gd │ ├── marker_component.gd │ ├── navigator_component.gd │ ├── npc_component.gd │ ├── player_component.gd │ ├── puppet_spawner_component.gd │ ├── script_component.gd │ ├── shop_component.gd │ ├── skills_component.gd │ ├── spell_target_component.gd │ ├── teleport_component.gd │ ├── view_direction_component.gd │ └── vitals_component.gd ├── constants.gd ├── covens │ ├── coven.gd │ ├── coven_rank_data.gd │ └── coven_system.gd ├── crime │ ├── crime.gd │ └── crime_master.gd ├── data │ └── ItemDataComponents │ │ ├── apparel_data_component.gd │ │ ├── equippable_data_component.gd │ │ ├── holdable_data_component.gd │ │ ├── item_data_component.gd │ │ ├── spell_data_component.gd │ │ └── throwable_data_component.gd ├── entities │ ├── entity.gd │ ├── entity_component.gd │ └── entity_manager.gd ├── fsm │ ├── fsm_machine.gd │ └── fsm_state.gd ├── granular_navigation │ ├── nav_point.gd │ ├── navigation_master.gd │ ├── navigation_node.gd │ └── navigation_world.gd ├── instance_data │ └── door_instance.gd ├── loottable │ ├── items │ │ ├── lt_item.gd │ │ ├── lt_item_entity.gd │ │ ├── lt_itemchance.gd │ │ ├── lt_itementry.gd │ │ ├── lt_loottablecurrency.gd │ │ ├── lt_on_condition.gd │ │ └── lt_xofitems.gd │ ├── skloottable.gd │ └── skloottableitem.gd ├── misc │ ├── animation_controller.gd │ ├── audio_emitter.gd │ ├── damage_info.gd │ ├── device_emitter.gd │ ├── device_listener.gd │ ├── device_network.gd │ ├── element_group.gd │ ├── hit_detector.gd │ ├── id_generator.gd │ ├── npc_template_option.gd │ ├── option.gd │ ├── skconfig.gd │ ├── skelesave.gd │ ├── status_effect.gd │ └── status_effect_host.gd ├── network │ ├── Editor │ │ ├── cost_popup.gd │ │ ├── editor_toolbar.tscn │ │ ├── network_editor_utility.gd │ │ └── network_gizmo.gd │ └── Scripts │ │ ├── network.gd │ │ ├── network_edge.gd │ │ ├── network_instance.gd │ │ ├── network_point.gd │ │ ├── network_portal.gd │ │ └── portal_edge.gd ├── npc_tools │ └── npc_tool.tscn ├── points │ ├── furniture.gd │ ├── idle_point.gd │ └── spawn_point.gd ├── puppets │ ├── item_puppet.gd │ └── npc_puppet.gd ├── relationships │ ├── relationship.gd │ └── relationship_association.gd ├── schedules │ ├── continuity_condition.gd │ ├── sandbox_schedule.gd │ ├── schedule.gd │ ├── schedule_condition.gd │ └── schedule_event.gd ├── sk_global.gd ├── spell_casting │ ├── spell.gd │ ├── spell_effect.gd │ ├── spell_hand.gd │ ├── spell_item.gd │ └── spell_projectile.gd ├── system │ ├── game_info.gd │ ├── save_system.gd │ ├── timestamp.gd │ └── world_loader.gd └── world_objects │ ├── damageable_object.gd │ ├── door.gd │ ├── effects_object.gd │ ├── interactive_object.gd │ ├── sk_world_object.gd │ ├── spell_target_object.gd │ └── world_entity.gd ├── skelerealms.gd ├── skelerealms_logo.png ├── skelerealms_logo.png.import ├── skelerealms_logo.svg ├── skelerealms_logo.svg.import ├── tools ├── config_sync_plugin.gd ├── door_connect.gd ├── edit_button.gd ├── point_gizmo.gd ├── schedule_box.gd ├── schedule_box_control.tscn ├── schedule_editor.gd ├── schedule_editor.tscn ├── schedule_editor_plugin.gd ├── schedule_markers.gd ├── scheduledraghandle.gd ├── sk_game_root.gd ├── slot_enum_selector.gd ├── span.gd ├── template_selector.gd ├── template_selector.tscn └── world_entity_plugin.gd ├── world_marker_entity_template.tscn └── world_template.tscn /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2024 Slashscreen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skelerelams 2 | 3 | Welcome to Skelerealms, the framework for Bethesda-style Open World RPGs (Like Skyrim, Fallout New Vegas, etc.). 4 | This addon aims to offer a solution to the most challenging technical challenges faced while engineering games like this. Gameplay is, however, not included. 5 | For those familiar with Creation Engine's inner workings, Skelerealms seeks to primarily cover Actors, Cells, AI Packages, and Factions. 6 | Skelerealms is designed in such a way where you can ignore or replace most of the working components, and allow easy integration into your own gameplay systems. 7 | 8 | ## What does it have? 9 | 10 | - Inter-scene persistence of important objects 11 | - Inter-scene navigation 12 | - A basic framework for skills and attributes 13 | - Loot tables 14 | - Inventory system 15 | - Equipment system 16 | - NPC AI 17 | - Behavious 18 | - GOAP AI System 19 | - Basic perception 20 | - Schedules 21 | - Patrol paths 22 | - Tools to assist development 23 | - Composable design 24 | - Components for entities 25 | - Components for items 26 | - Dungeon puzzle elements 27 | - Factions 28 | - Spells/Status Effects 29 | - Crime 30 | - Bartering 31 | - Spawn zones 32 | - Doors 33 | 34 | ## What does it *not* have? 35 | 36 | - Gameplay 37 | - Terrain 38 | - LOD system, chunks 39 | - UI 40 | - Dialogue 41 | - Quests (Ironically) 42 | - Combat 43 | 44 | ## How do I get started? 45 | 46 | 47 | Visit the [documentation](docs/user%20guide/quick_start.md) for a quick start guide. 48 | 49 | 50 | ## What's the project status? 51 | 52 | The project is active. I am using this to develop my own game, and will occasionally push changes I make upstream. 53 | Please note that the project is in an Alpha state, which means breaking changes can and will happen often. Plan around this. I plan to have feature and API stability once 1.0 is reached. 54 | 55 | ## What's in store? 56 | 57 | - 0.6 (In Development) 58 | - Redesigning the way entities are stored. 59 | - Adding more tools. 60 | - Writing more thorough documentation. 61 | - Integrating NetworkGD. 62 | - 0.7 63 | - Redesigning the save game system. 64 | - Polish cross-scene navigation. 65 | 66 | 67 | ## Star History 68 | 69 | [![Star History Chart](https://api.star-history.com/svg?repos=SlashScreen/skelerealms&type=Timeline&theme=dark)](https://star-history.com/#SlashScreen/skelerealms&Timeline) 70 | -------------------------------------------------------------------------------- /docs/concepts/entities.md: -------------------------------------------------------------------------------- 1 | # Entities and Components 2 | 3 | ## Entities 4 | 5 | Anything that needs to have cross-scene persistence must be an *Entity*. These are roughly equivalent to CE's *Actors*. An *Entity* is defined as a tree of nodes descending from an `SKEntity` node. During runtime, these entities will live underneath the `SKEntityManager`, which keeps track of what entity goes in what scene, among other things. 6 | 7 | ## Components 8 | 9 | *Entites* are largely made up of *Components*. These are nodes that derive from `SKEntityComponent`, special nodes that have built-in management functions. 10 | -------------------------------------------------------------------------------- /docs/concepts/worlds.md: -------------------------------------------------------------------------------- 1 | # Worlds 2 | 3 | *Worlds* are simply a way of expressing what scene an entity belongs to. This is roughly equivalent to CE's *Cells*. 4 | 5 | A scene becoming a World has two requirements: 6 | - The root node (It should be a Node3D but it doesn't have to) has the name of the world 7 | - The scene file is saved in the directory defined by `skelerealms/worlds_path` - by default, `res://worlds` (It can be in a subforlder for organization). The name of the file should exactly match the name of the root node. 8 | 9 | That's it! 10 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | # The Skelerealms Grimoire 2 | #### For Skelerealms Beta 0.6 3 | 4 | ## Introduction 5 | 6 | Welcome! There are the high-level docs for Skelerealms, showing you how to use this addon. If you want methods and variables documentation, documentation comments are provided in-engine, like any other class. 7 | 8 | ### What is Skelerealms? 9 | 10 | Skelerealms is an addon for Godot 4.2+ that aims to provide the foundation for creating a Bethesda-style Open World RPG (The Elder Scrolls, Fallout, Starfield). No gameplay is provided, but a lot of the challenging systems are in place to allow you to focus on telling your story. 11 | 12 | Skelerealms is inpsired by Creation Engine, but aims to tackle many of its shortcomings with the benefit of 20+ years of hindsight. It's also been designed not to lock you in to any specific way of designing your game - most components are purely optional. You don't even need to have a player to run the game (but I'm not sure what the point of that would be...) 13 | 14 | ### What problems does it solve? 15 | 16 | - Cross-scene persistence: If the player drops a book in one room, goes outside, and then comes back inside, that book should still be lying on the floor. This problem is much trickier to solve than it seems; but you don't need to worry about it. Skelerealms has got you covered! 17 | - Cross-scene navigation: If you're running away from an enemy and duck into a building, it only makes sense that the enemy should follow you through the door. This, again, is a tricky problem to solve. Again, though, Skelerealms has solved this problem already, by introducing a supplementary navigation system that exists, no matter what scene is loaded. 18 | - NPC AI: Skelerealms comes with a robust AI system that hopes to smooth out some of the awkwardness of Bethesda's infamous NPCs by using a GOAP (Goal-Orientated Action Planning) system that's also integrated (but not coupled to) a built-in faction and schedule system. 19 | - And much more! 20 | 21 | ### Main Features 22 | 23 | - Cross-scene persistence 24 | - Inter-scene navigation 25 | - GOAP 26 | - Inventory 27 | - Status effects and spells 28 | - Loot tables 29 | - Equipment 30 | - Composable item behaviors 31 | - Bartering system 32 | - Factions 33 | - Schedules 34 | - Sight/stealth mechanics 35 | 36 | ## Table of Contents 37 | 38 | ### Concepts 39 | 40 | ### User guide 41 | 42 | 43 | ## Project Status 44 | 45 | ### Development 46 | 47 | Despite what it may look like on the main repo's commit history, development is ongoing (in the submodule repository.) Skelerelams was originally developed for a game I am making myself, and so I am dog-fooding Skelerealms during the development of this game. As I encounter problems during the development process, I make changes and fixes, and push them upstream. 48 | 49 | ### Places to improve 50 | 51 | - The Savegame system needs to be rethought. 52 | - The way the navigation system is represented in code could be much more memory-efficient. I may end up rewriting it in Zig or something. 53 | - Lots of more processing-heavy parts of code should probably be written in a compiled language. 54 | -------------------------------------------------------------------------------- /docs/user guide/ai_modules.md: -------------------------------------------------------------------------------- 1 | # AI Modules 2 | 3 | NPC AI is made of two parts: AI Modules and GOAP Actions. This article covers the AI Modules. AI Modules determine *what* the NPC wants to do, and the GOAP Actions determine *how* to do it. 4 | 5 | AI Modules are rough equivalents to Creation Engine's AI Packages, minus the schedules. They are nodes that are direct children (also see [tools](/docs/user%20guide/tools.md) for `NodeBundler`) of NPCs that inform the NPCs behavior. Without any modules, the NPC will do absolutely nothing. 6 | AI Modules are expected to function largely autonomously from the NPC. The intended design is to hook into one of the NPC's many, many signals and change behavior in response. To get a better idea of how this looks, take a look at some of the exaples that come with Skelerealms, such as [movement](../../scripts/ai/Modules/default_movement.gd) for a simple example, or [threat response](../../scripts/ai/Modules/default_threat_response.gd) for a more complex one. 7 | 8 | As with anything else, code documentation can be found in the in-engine documentation. 9 | -------------------------------------------------------------------------------- /docs/user guide/components.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | Entities are designed to be used with Components, which are reusable bits oc foce. Such is the Godot way. 4 | Components aren't particularly special; they simply have a few functions for workign with the entities. 5 | 6 | ## Functionality 7 | 8 | - Allows you to specify dependencies, which if it doesn't have, shows up as a warning in the editor 9 | - Virtual functions for generation, spawning, despawning 10 | - Saving, loading 11 | - Gathering debug information 12 | - Printing to console with entity's name, etc. 13 | 14 | ## Making your own 15 | 16 | Simply inherit the SKEntityComponent class, and have at it! Just make sure it renames itself to its class name: 17 | the entity's `get_component()` method looks for names, since GDScript has no proper generics. 18 | Also note that you have an easy way to grab the parent entity: `parent_entity`. 19 | If you would like to provide a custom preview scene for manipulating your entity in the editor with SKWorldEntites, 20 | implement a `get_world_entity_preview() -> Node` method that returns a scene you'd like to appear in the editor. 21 | 22 | ## Built-in components 23 | 24 | A number of built-in components are offered for your convenience. More in-depth usage of these can be found in their in-engine documentation. 25 | 26 | - Attributes: Covers attributes like Strength, Perception, Endurance, etc. 27 | - Chest: Creates a refilling chest system. Expects a loot table. 28 | - Covens: Integrates with Skelerealms Factions system. 29 | - Damageable: Allows this entity to be damaged. 30 | - Effects: Allows this entity to be subject to the status effects system. 31 | - Equipment: This entity can equip items from an inventory into special equipment slots. 32 | - GOAP: Puts the entity under control of the built-in GOAP system. GOAP Actions come as child nodes. 33 | - Interactive: Allows the player (and others) to interact with this entity. 34 | - Inventory: Gives the entity an inventory, along with management tools. Can use a Loot Table to generate an inventory. Also keeps track of currency. 35 | - Item: This entity is now an item, and can be moved from inventory to inventory. Item Components come as child nodes. 36 | - Marker: This entity can be used as waypoints, or whatever else you need. (I use them as destinations to teleport to in the console.) 37 | - Navigator: Can calculate paths in the granular navigation system. 38 | - NPC: This entity is now an NPC, and has many things it can do as a consequence. AI Modules come as child nodes. 39 | - Player: This entity is a Player. 40 | - Puppet Spawner: Spawns scenes into the game world as "puppets" that are linked to the entity. Used by Items and NPCs to give form to their being. 41 | - Script: Deprecated. 42 | - Shop: This entity can now barter in the barter system. Needs an overhaul. 43 | - Skills: Keeps track of an entity's Skills - One-handed, block, archery, etc. 44 | - Spell Target: Deprecated. Use EffectsComponent instead. 45 | - Teleport: This entity can be teleported. 46 | - View Direction: Keeps track of an entites viewing direction, for stealth mechanics and other such things. 47 | - Vitals: Keeps track of health, stamina, magica, etc. 48 | -------------------------------------------------------------------------------- /docs/user guide/covens.md: -------------------------------------------------------------------------------- 1 | # Covens 2 | 3 | Covens are Skelerealms' "Factions" system. The odd name is because the game I was making Skelerealms for before deciding to open-source it calls factions "covens", and I never bothered to change it. Covens are an optional system that determine some things about NPC behavior, particularly the crime tracking system and how NPCs determine the opinion they have of something. Many of the covens' attributes are self-explanatory and have further documentation in-editor, but I should add that covens are loaded at game start, and so should be placed in the covens folder in the plugin settings - by default `res://covens`. An entity is added to a coven by adding a `CovensComponent` and adding a `CovenRankData` resource. This may get reworked in the future. 4 | -------------------------------------------------------------------------------- /docs/user guide/entities.md: -------------------------------------------------------------------------------- 1 | # Entities 2 | 3 | Any scene with SKEntity as the root is considered an Entity. Entities are able to persist between scenes, so use these for everything you want 4 | to stay put when unloaded: NPCs, Items, etc. Entities by themselves do very little - the first layer of children should be made up of SKEntityComponents, 5 | reusable bits of code that inform the behavior of an Entity; for example, keeping track of an inventory. 6 | 7 | ## Functionality 8 | 9 | Entities do have some functionality, though; they keep track of the following: 10 | 11 | - Position and rotation of the entity (Note that SKEntity is *not* a Node3D; Making it one does some funky stuff with puppets.) 12 | - The world the entity is in 13 | - Whether the entity should be in the scene or not 14 | - Form ID (used for determining what sort of thing non-unique entities are) 15 | - Whether this entity is unique. 16 | 17 | It provides some hooks for: 18 | 19 | - Spawning, despawning 20 | - Actions called from dialogue 21 | - Generation 22 | - Getting a preview mesh (Components can provide a preview mesh for you to see when placing them in the editor) 23 | 24 | As well as utility functions for: 25 | 26 | - Saving 27 | - Loading 28 | - Gathering debug information (used for debug consoles and the like) 29 | - Managing components 30 | 31 | ## Concepts 32 | 33 | The life cycle of an entity goes as follows: 34 | ``` 35 | Generation -> Spawning <-> Despawning -> Destruction 36 | ``` 37 | 38 | **Spawning** happens every time an entity appears in a scene. This is used for spawning anything 39 | associated with the entity that the player interacts with (referred to as puppets). 40 | 41 | **Generation** is when the entity appears for the first time in a game. Not a game *session*, but a game. This is distinct from spawning 42 | in that spawning occurs every time an entity appears in a game *session*. Further appearances of the entity will be aided by the save game system. 43 | Generation should be used for filling inventories for the first time. 44 | 45 | **Destruction** means the entity will no longer exist in the game, ever. This stage is a work-in-progress, and will probably come in 0.7. 46 | 47 | **Uniqueness** simply means that there will only be one instance of this entity that exists in the game; usually used for named NPC and unique items. 48 | Internally, non-unique entities will be given a randomly-generated RefID upon generation. Non-unique entities should probably be given a FormID. 49 | 50 | ## Creating an entity 51 | 52 | Simply create a new scene with an SKEntity as the root, and name it with a RefID if it's unique. The first layer of the tree 53 | under the SKEntity should all be made of SKEntityComponent-derived nodes. Beyond that, that's up to whatever the components expect to be beneath them. 54 | Then, save the entity as a scene file (I prefer `.res`) in the entities path under the `skelerealms/entities_path` project setting; `res://entities` by default. 55 | They can be in a subdirectory for organization. 56 | In the very likely chance that you have many entities with the same general shape, like NPCs, you can have an "archetype" scene that you can inherit entities from, using 57 | the editor's "ingerit scene" functionality. This is handy for saving legwork, as well as changing large swaths of entities at once. Some archetypes are provided in the 58 | Skelerealms folder. 59 | To place individual entities in the world, you can create an SKWorldEntity, and place in an Entity in the inspector. Depending on the entity, you will get a preview. 60 | Manipulate the World Entity to your linkng, then hit "Sync position and world" in the inspector. This will automatically edit the entity to set its position and world 61 | so it spawns where you placed it in the world. 62 | -------------------------------------------------------------------------------- /docs/user guide/goap_actions.md: -------------------------------------------------------------------------------- 1 | # GOAP Actions 2 | 3 | NPC AI is made of two parts: AI Modules and GOAP Actions. This article covers the AI Modules. AI Modules determine *what* the NPC wants to do, and the GOAP Actions determine *how* to do it. 4 | 5 | GOAP Actions are a fair bit more complicated than the AI Modules. If you don't understand how GOAP works in theory, that's outside the scope of this article: search online for more information. 6 | 7 | Again, they are nodes directly beneath a `GOAPComponent`. An actions prerequisites and effects are determined by overriding the `get_prerequisites()` and `get_effects()` functions, returning a dictionary of shape `Effect -> Value`. These are used in the planning process, so shouldn't change during runtime, unless you know what you're doing. The GOAP system works on a timeline divided into two parts, form the perspective of an action: 8 | 9 | ## Plan time 10 | 11 | Plan time is simply the time when a plan is created. Every action will have `is_achievable()` called on them. Returning `false`, for any reason you may desire, will exclude them from the planning process. 12 | 13 | ## Action time 14 | 15 | Action time is when the action is being executed. The steps of each sequence are given to you as a series of overrideable functions. Returning `false` from any of them will trigger a plan recalculation. The sequence is as follows: 16 | 17 | 1. `pre_perform()` - Any actions that should be taken when the action is first begun. Finding targets, triggering animations, etc. 18 | 2. `target_reached()` - Something that should happen when a target is reached, determined by the `is_target_reached` function. By default, returns whether a navigation agent has finished navigating to a point. 19 | 3. `post_perform()` - Any actions that should be taken once the action is complete. **This happens once `duration` passes after `target_reached()` is called.** 20 | 21 | - `interrupt()` can happen at any time. This detemines what, if anything, should happen if a plan is recalculated mid-action (between `pre_perform()` and `post_perform()`, when `running` is true). This returns nothing. 22 | 23 | --- 24 | 25 | The intended way for GOAP Actions to have an effect on the world is through the given `parent_gop` and `entity` properties, allowing them to, for instance, move an NPC around, or play an animation. 26 | 27 | The planner itself is contained within the `GOAPComponent`, and will attempt to make a plan every frame where it does not have any plan. 28 | -------------------------------------------------------------------------------- /docs/user guide/loot_tables.md: -------------------------------------------------------------------------------- 1 | # Loot Tables 2 | 3 | Skelerealms comes with an in-house loot table system inspired by that seen in Minecraft. It's created using a tree of `SKLootTableItem`s underneath an associated `SKLootTable`. A "resolved" (rolled) loot table creates a result consisting of: 4 | 5 | - Generated items 6 | - Unique items 7 | - Currencies 8 | 9 | Each of these can be manipulated by creating a nested of various `SKLootTableItem`s. Some loot table items will affect the chances or number of child items, while others will instead contribute items to the output. These can be combined in ways to create complex results. [Consider the following](https://www.youtube.com/watch?v=uI_N2tLw-vI) loot table: 10 | 11 | - SKLootTable 12 | - SKLTItem (Contains sword) 13 | - SKLTXOfItem (Set to between 1 and 3) 14 | - SKLTCurrency (Set to between 5 and 20 gold coins) 15 | 16 | This loot table will always return a sword, and then will generate 5 to 20 coins 1 to 3 times. Not the most practical example, but hopefully you get the idea. 17 | 18 | You can put entities you've created (Unique items) into a loot table with `SKLTItemEntity`. Do note, however, that no matter how many times a unique item is rolled, it will only appear in the result once. Non-item entities will appear in the result, but `InventoryComponent` will not add non-item entities into its inventory. Also, the unique items should still be saved and stored within Skelerealms' entities path, as configured in the project settings. 19 | 20 | Built-in, Loot tables are applicable to the following components: 21 | - `InventoryComponent`, which will roll when the entity is `generate`d 22 | - `ChestComponent`, which will roll and fill an inventory whenever the chest refreshes 23 | -------------------------------------------------------------------------------- /docs/user guide/migrating.md: -------------------------------------------------------------------------------- 1 | # Migrating from 0.5 to 0.6 2 | 3 | This will not be easy. Sorry in advance. 4 | 5 | ## Instance data 6 | 7 | This will be spotty at best, but a button will appear above `InstanceData`-derived classes that, when pressed, will create a roughly-equivalent Entity, and save it under the same directory. AI-related things and Loot boxes will not be conserved. 8 | 9 | ## GOAP 10 | 11 | Luckily, this is easy. Simply change your existing GOAPBehaviors to GOAPActions. Instead of being a property, however, prerequisites and effects have moved into functions to override. 12 | 13 | ## Loot tables, schedules 14 | 15 | You will have to remake these from scratch. Sorry. 16 | -------------------------------------------------------------------------------- /docs/user guide/navigation.md: -------------------------------------------------------------------------------- 1 | # Navigation 2 | 3 | TODO 4 | 5 | This is for cross-scene navigation. I might rework the way you do this soon but basically you use the network tool. You can find the repo [here](https://github.com/SlashScreen/godot-network-graph) and look at the wiki for how to use it, but I'm about to merge it into Skelerealms, I think, since having external dependencies creates complications. Anyway, save the network for a world into the networks folder in the project settings, and the system will pick it up into the pathfinding later. Cross-scene navigation is done using the `NavigatorComponent`, although I'm not sure it works right now. 6 | -------------------------------------------------------------------------------- /docs/user guide/npcs.md: -------------------------------------------------------------------------------- 1 | # NPCs 2 | 3 | Skelerealms' NPCs are easily the most complex (and bug-prone - please report any bugs) part of the entire framework. They bring together a number of other complex systems (AI Modules, GOAP, Schedules, Covens, Navigation, Perception) into one entity. You can skip this entire systme and roll your own if you want to. NPCs are a blank slate, and don't do much on their own: their behavior is defined entirely by AI Modules and other systems. 4 | 5 | The component itself offers a number of flags and settings that may define its behavior: 6 | 7 | ## Flags 8 | 9 | `essential`, `ghost`, `invulnerable`, `unique`, and `affects_stealth_meter` don't actually do anything by default. They are here because they are often used in your own implementation of gameplay mechanics. 10 | 11 | Interactive, however, determines whther you can interact with this NPC or not, say, to start dialogue. 12 | 13 | ## AI 14 | 15 | A few AI settings are here as well, although they are only here to be used by AI Modules. 16 | 17 | - `relationships`: Determines relationships this NPC has with other NPCs, think a father and daughter, a boss and employee, etc. 18 | - `threatening_enemy_types`: The names of components other entities it sees must have for it to determine whether it's a threat or not. It doesn't make much sense for an invisible `Marker` to be a threat, does it? 19 | - `npc_opoinions`: Any opinions it has of any particulat entity, in the shape of `ref_id -> opinion`. For example, an NPC called `biggest_bts_fan` may have an opinion value of 100 of the NPC `jimin`, bringing the opinion calculations up. 20 | - `loyalty`: This determines the "allegiance" on an NPC during opinion calculations; that is, whether the opinion should be weighted more towards the opinions of the NPC's covens or its own loyalties, or no wieght at all. This is only used if the `opinion_mode` is set to `Average`. See "Opinion Calculation". 21 | - `opinion_mode`: How this NPC generates opinions. See "Opinion Calculatin" for details. 22 | 23 | 24 | ## Opinion Calculation 25 | 26 | An NPC can determine its own opinion of another NPC. This is used to influence dialogue choices, whether this NPC should attack another NPC, that sort of thing. 27 | 28 | When an NPC calculates an opinion it has on another NPC, influencing its behavior toward the NPC, it has a lot of different things to consider. Opinions are drawn from two sources: 29 | 30 | - Its own opinions defined in `npc_opinions` 31 | - Opinions that each coven the NPC has has regarding each coven the target entity has 32 | 33 | How these opinions factor into the final opinion depends on `opinion_mode`: 34 | 35 | - `Minimum`: The calculated opinion is the minimum of all opinions gathered. This is the default. 36 | - `Maximum`: The calculated opinion is the maximum of all opinions gathered. 37 | - `Average`: The calculated opinion is the weighted average of all the opinions gathered, deduplicated. The weight is determined by `loyalty_mode`. 38 | 39 | 40 | ## Simulation Level 41 | 42 | The NPCs have 3 simulation levels to keep down processing power for lots of agents: 43 | 44 | - FULL: Full simulation 45 | - GRANULAR: Partial simulation, only handles schedules and inter-scene navigation 46 | - NONE: The NPC will not do anything. 47 | -------------------------------------------------------------------------------- /docs/user guide/quick_start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | ## For New Projects 4 | 5 | Use [the template](https://github.com/SlashScreen/skelerealms-template) project. You now have a basic project! 6 | 7 | ## For Existing Projects 8 | 9 | Use this repo as a submodule in your addons folder. 10 | -------------------------------------------------------------------------------- /docs/user guide/schedules.md: -------------------------------------------------------------------------------- 1 | # Schedules 2 | 3 | Skelerealms comes with a schedule/NPC routines system. It's used by adding a `Schedule` node underneath an `NPCComponent`, and adding a number of `ScheduleEvent`s underneath. 4 | 5 | `ScheduleEvents` are nodes that can influence what an NPC does at different times of day, and are roughly equivalent to Creation Engine's "AI Packages" feature, but limited to the "routines" aspect of the feature. This class is meant to be inherited with your own functionality. For an example of how this is done, see the built-in `SandboxSchedule`. The `SandboxSchedule` is perhaps confusingly named, but it is used for NPCs idle behaviors during a set time - for example, milling about in their house during the evening. The "Sandbox" name is inherited from Creation Kit's name for the same idea. 6 | 7 | Most of the properties are documented or self-explanatory. For them to be considered, the current time of day must be between the times described in `from` and `to`. The timestamps can be adjusted to inform to what degree the timestamps must be matched, so NPCs can, for example, do a schedulke every day, or only on certain days. `ScheduleConditions` can also be added to only allow events to happen if certain conditions are met - for example, only having certain behavior happen when a quest is complete. 8 | -------------------------------------------------------------------------------- /docs/user guide/stealth_provider.md: -------------------------------------------------------------------------------- 1 | # Stealth Provider 2 | 3 | Skelerealms formerly provided some vision "detectors" for stealth mechanics; vision cones, light sensors. As of 0.6, 4 | I decided to remove them as I believed they were out of scope for the project, and I wanted to allow users to use their 5 | own stealth mechanics that fit their game, instead of being forced to use my implementation. I may in the future release 6 | an optional stealth detector add-on for skelerealms as an optional package. 7 | 8 | Despite removing the built-in feature, it was still important to have the ability to add them. My solution was the so-called 9 | "stealth provider", which is basically an interface (or the closest GDScript equivalent) that the built-in components that use 10 | the feature expect to provide data on the entity's surroundings. **Long story short, a Stealth Provider is any Object that has 11 | a set of functions described below**. The components relying on them will call these functions dynamic language-style. 12 | 13 | ## The Shape 14 | 15 | As, for some god-foresaken reason, GDScript has no way to statically type interfaces, I will describe the shape of the 16 | Stealth Provider here. There are two parts: 17 | 18 | ### The Function 19 | 20 | ``get_visible_objects() -> Dictionary``` 21 | This returns a dictionary with the following structure: 22 | ``` 23 | object:Object -> Dictionary { 24 | &"visibility":float, # The visibility factor for this object 25 | &"last_seen_position":Vector3, # Last seen position of this object 26 | } 27 | ``` 28 | 29 | ### The Signals 30 | 31 | ``` 32 | object_entered_view(Object) 33 | object_exited_view(Object) 34 | ``` 35 | 36 | These are pretty self-explanatory. Note that the Object is literally the class `Object`, and is not guaranteed to be an Entity. 37 | 38 | ### The Tag 39 | 40 | This is optional, but the built-in archetypes intended to be used with this system (NPCs and Items) will automatically 41 | add their puppets to the Node Group `perception_target`. There is no code *enforcing* you to do anything with this group, 42 | but it is there just in case. I intended all "entities of interest" to have this tag, and stealth providers would only 43 | report seeing things with this node group, to provide a soft guarantee that anything seen would be an entity. 44 | -------------------------------------------------------------------------------- /docs/user guide/tools.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | Skelerealms offers a few tools to help with your development. 4 | 5 | ## Components 6 | 7 | Components will tell you if any other components they depend on are absent, using the ditor warnings feature. 8 | 9 | ## Node Bundles 10 | 11 | If you want to compose a set of AI Modules, Loot table items, etc. the `NodeBundle` class aims to help with that. All it does is take all of its children and reparent them to be siblings of itself, and then removes itself afterwards. Since some systems rely on a tree's structure to determine functionality, this is handy for having a scene you can drag in while making an entity, helping for composability. Simply make a scene with a NodeBundle as a root and all your items below it, and it will flatten out during runtime. 12 | 13 | ## SKWorldEntity 14 | 15 | The premiere way to spawn unique entities into the world is by using an SKWorldEntity. Put your entity in, and do with it as you wish. 16 | 17 | There is a work-in-progress tool you can use to create entities based on an archetype (see [entities](entities.md)). You can add paths to each archetype you want to use in the project settings, and then create new ones using the menu in the inspector. It's not perfect, but it will get better support once instancing scenes directly within code gets supported (an active PR). 18 | 19 | You can sync the position of any unique entity by hitting the button in the inspector. This will edit the attached entity to make its position the **global** position of the World Entity node, and set its world to the current edited world (Internally, this is determined to be the name of the root node of the currently edited scene). 20 | 21 | ## Doors 22 | 23 | The easiest way to allow the player to move between scenes is the Door. Add a door into the scene. Whenever any entity with a `TeleportComponent` interacts with the door, they will be teleported to the door's other side. To set up a door, do as follows: 24 | 25 | 1. Create a Door node, and position it. You can also add any colliders or whatever you use to determine when to interact with something, as well as your mesh instance. 26 | 2. Create a resource in the `instance` field, and save it to disk somewhere. press the "Sync position" button to sync the door resource. 27 | 3. Create a new door in a different (or the same) scene, and repeat the process. Grab the resource of the door you already have, (or, if you already have a door resource you'd like to link to, grab that instead), and drag it into the `destination_instance` field. You must do this on both sides (a bit tedious, but it allows for more flexible/non-euclidean door layouts). 28 | 4. You now have a door. 29 | 30 | At any time, once you have a `destination_instance`, you can press the "Jump to destination" button to navigate the editor's camera to the other side of the door, opening the destination scene in the editor if necessary (providing that the destination world is in the proper folder). 31 | -------------------------------------------------------------------------------- /item_entity_template.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://bt1x4w2k7dhvf"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/entities/entity.gd" id="1_mtgwt"] 4 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/item_component.gd" id="2_yi40p"] 5 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/interactive_component.gd" id="3_lvppr"] 6 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/puppet_spawner_component.gd" id="4_nkg75"] 7 | 8 | [node name="npc_item_entity" type="Node"] 9 | script = ExtResource("1_mtgwt") 10 | 11 | [node name="ItemComponent" type="Node" parent="."] 12 | script = ExtResource("2_yi40p") 13 | 14 | [node name="InteractiveComponent" type="Node" parent="."] 15 | script = ExtResource("3_lvppr") 16 | 17 | [node name="PuppetSpawnerComponent" type="Node" parent="."] 18 | script = ExtResource("4_nkg75") 19 | -------------------------------------------------------------------------------- /item_template.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://cbph67tp6ro6i"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/puppets/item_puppet.gd" id="1_l0auv"] 4 | 5 | [sub_resource type="BoxShape3D" id="BoxShape3D_ivnhw"] 6 | 7 | [sub_resource type="BoxMesh" id="BoxMesh_qxh5q"] 8 | 9 | [node name="Item" type="RigidBody3D"] 10 | script = ExtResource("1_l0auv") 11 | 12 | [node name="CollisionShape3D" type="CollisionShape3D" parent="."] 13 | shape = SubResource("BoxShape3D_ivnhw") 14 | 15 | [node name="MeshInstance3D" type="MeshInstance3D" parent="CollisionShape3D"] 16 | gi_mode = 2 17 | mesh = SubResource("BoxMesh_qxh5q") 18 | skeleton = NodePath("../../..") 19 | -------------------------------------------------------------------------------- /light probe test.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://brvrv13m1xo0n"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://itfouqmshnrx" path="res://addons/skelerealms/light_probe.tscn" id="1_qygoj"] 4 | 5 | [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_fikaa"] 6 | 7 | [sub_resource type="Sky" id="Sky_6gdxn"] 8 | sky_material = SubResource("ProceduralSkyMaterial_fikaa") 9 | 10 | [sub_resource type="Environment" id="Environment_hboki"] 11 | background_mode = 2 12 | sky = SubResource("Sky_6gdxn") 13 | 14 | [sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_51c16"] 15 | 16 | [node name="LightProbeTest" type="Node3D"] 17 | 18 | [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] 19 | transform = Transform3D(1, 0, 0, 0, 0.843082, 0.537785, 0, -0.537785, 0.843082, 0, 1.96516, -3.74161) 20 | shadow_enabled = true 21 | 22 | [node name="Camera3D" type="Camera3D" parent="."] 23 | transform = Transform3D(1, 0, 0, 0, 0.922619, 0.385713, 0, -0.385713, 0.922619, 0, 2.23804, 7.15823) 24 | environment = SubResource("Environment_hboki") 25 | attributes = SubResource("CameraAttributesPractical_51c16") 26 | current = true 27 | 28 | [node name="Probe" parent="." instance=ExtResource("1_qygoj")] 29 | -------------------------------------------------------------------------------- /light_probe.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://itfouqmshnrx"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/ai/light_estimation_provider.gd" id="1_q2rje"] 4 | [ext_resource type="ArrayMesh" uid="uid://blag3iii8af8j" path="res://addons/skelerealms/octa.obj" id="2_8iqk2"] 5 | 6 | [node name="Probe" type="Node3D"] 7 | script = ExtResource("1_q2rje") 8 | 9 | [node name="SViewportTop" type="SubViewport" parent="."] 10 | debug_draw = 2 11 | gui_disable_input = true 12 | size = Vector2i(32, 32) 13 | render_target_update_mode = 4 14 | 15 | [node name="ProbeOct" type="MeshInstance3D" parent="SViewportTop"] 16 | transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0) 17 | layers = 512 18 | cast_shadow = 0 19 | mesh = ExtResource("2_8iqk2") 20 | skeleton = NodePath("../../..") 21 | 22 | [node name="Camera3D" type="Camera3D" parent="SViewportTop"] 23 | transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0.127643, 0) 24 | cull_mask = 1048064 25 | projection = 1 26 | current = true 27 | size = 0.1 28 | near = 0.001 29 | far = 1.0 30 | 31 | [node name="SViewportBottom" type="SubViewport" parent="."] 32 | debug_draw = 2 33 | gui_disable_input = true 34 | size = Vector2i(32, 32) 35 | render_target_update_mode = 4 36 | 37 | [node name="Camera3D" type="Camera3D" parent="SViewportBottom"] 38 | transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, -0.128, 0) 39 | cull_mask = 1048064 40 | projection = 1 41 | current = true 42 | size = 0.1 43 | near = 0.001 44 | far = 1.0 45 | -------------------------------------------------------------------------------- /npc_entity_template.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=18 format=3 uid="uid://dade7o6bx8ja0"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/entities/entity.gd" id="1_1v31h"] 4 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/npc_component.gd" id="2_macfp"] 5 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/interactive_component.gd" id="3_peb1r"] 6 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/puppet_spawner_component.gd" id="4_27chb"] 7 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/teleport_component.gd" id="5_1ifxk"] 8 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/goap_component.gd" id="6_1ul7y"] 9 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/skills_component.gd" id="7_mnx3l"] 10 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/attributes_component.gd" id="8_vyu84"] 11 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/vitals_component.gd" id="9_efn20"] 12 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/spell_target_component.gd" id="10_12uph"] 13 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/covens_component.gd" id="11_5alr3"] 14 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/damageable_component.gd" id="12_oodnq"] 15 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/navigator_component.gd" id="13_yt8pj"] 16 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/view_direction_component.gd" id="14_2ecvo"] 17 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/equipment_component.gd" id="15_6i1jw"] 18 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/inventory_component.gd" id="16_27xpq"] 19 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/effects_component.gd" id="17_iewsa"] 20 | 21 | [node name="SKEntity" type="Node"] 22 | script = ExtResource("1_1v31h") 23 | 24 | [node name="NPCComponent" type="Node" parent="."] 25 | script = ExtResource("2_macfp") 26 | 27 | [node name="InteractiveComponent" type="Node" parent="."] 28 | script = ExtResource("3_peb1r") 29 | 30 | [node name="PuppetSpawnerComponent" type="Node" parent="."] 31 | script = ExtResource("4_27chb") 32 | 33 | [node name="TeleportComponent" type="Node" parent="."] 34 | script = ExtResource("5_1ifxk") 35 | 36 | [node name="GOAPComponent" type="Node" parent="."] 37 | script = ExtResource("6_1ul7y") 38 | 39 | [node name="SkillsComponent" type="Node" parent="."] 40 | script = ExtResource("7_mnx3l") 41 | 42 | [node name="AttributesComponent" type="Node" parent="."] 43 | script = ExtResource("8_vyu84") 44 | 45 | [node name="VitalsComponent" type="Node" parent="."] 46 | script = ExtResource("9_efn20") 47 | 48 | [node name="SpellTargetComponent" type="Node" parent="."] 49 | script = ExtResource("10_12uph") 50 | 51 | [node name="CovensComponent" type="Node" parent="."] 52 | script = ExtResource("11_5alr3") 53 | 54 | [node name="DamageableComponent" type="Node" parent="."] 55 | script = ExtResource("12_oodnq") 56 | 57 | [node name="NavigatorComponent" type="Node" parent="."] 58 | script = ExtResource("13_yt8pj") 59 | 60 | [node name="ViewDirectionComponent" type="Node" parent="."] 61 | script = ExtResource("14_2ecvo") 62 | 63 | [node name="EquipmentComponent" type="Node" parent="."] 64 | script = ExtResource("15_6i1jw") 65 | 66 | [node name="InventoryComponent" type="Node" parent="."] 67 | script = ExtResource("16_27xpq") 68 | 69 | [node name="EffectsComponent" type="Node" parent="."] 70 | script = ExtResource("17_iewsa") 71 | -------------------------------------------------------------------------------- /npc_template.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://dj6sa6ksct1xi"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/puppets/npc_puppet.gd" id="2_fw0xe"] 4 | 5 | [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_utii8"] 6 | 7 | [sub_resource type="CapsuleMesh" id="CapsuleMesh_s5g5c"] 8 | 9 | [node name="CharacterBody3D" type="CharacterBody3D" groups=["look_target"]] 10 | collision_layer = 17 11 | script = ExtResource("2_fw0xe") 12 | 13 | [node name="CollisionShape3D" type="CollisionShape3D" parent="."] 14 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) 15 | shape = SubResource("CapsuleShape3D_utii8") 16 | 17 | [node name="MeshInstance3D" type="MeshInstance3D" parent="."] 18 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) 19 | mesh = SubResource("CapsuleMesh_s5g5c") 20 | 21 | [node name="NavigationAgent3D" type="NavigationAgent3D" parent="."] 22 | path_desired_distance = 0.5 23 | target_desired_distance = 0.5 24 | avoidance_enabled = true 25 | -------------------------------------------------------------------------------- /octa.mtl: -------------------------------------------------------------------------------- 1 | # Blender 3.4.1 MTL File: 'None' 2 | # www.blender.org 3 | 4 | newmtl Material 5 | Ns 250.000000 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.800000 0.800000 0.800000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.450000 11 | d 1.000000 12 | illum 2 13 | -------------------------------------------------------------------------------- /octa.obj: -------------------------------------------------------------------------------- 1 | # Blender 3.4.1 2 | # www.blender.org 3 | mtllib octa.mtl 4 | o Cube 5 | v 0.500006 -0.000000 -0.500006 6 | v -0.500006 -0.000000 -0.500006 7 | v 0.000000 0.500000 0.000000 8 | v 0.000000 -0.500000 0.000000 9 | v 0.500006 -0.000000 0.500006 10 | v -0.500006 -0.000000 0.500006 11 | vn -0.0000 0.7071 -0.7071 12 | vn -0.0000 -0.7071 -0.7071 13 | vn 0.7071 0.7071 -0.0000 14 | vn 0.7071 -0.7071 -0.0000 15 | vn -0.7071 0.7071 -0.0000 16 | vn -0.7071 -0.7071 -0.0000 17 | vn -0.0000 0.7071 0.7071 18 | vn -0.0000 -0.7071 0.7071 19 | vt 0.500000 0.500000 20 | vt 0.500000 0.750000 21 | vt 0.375000 0.625000 22 | vt 0.500000 0.375000 23 | vt 0.500000 0.250000 24 | vt 0.250000 0.500000 25 | vt 0.625000 0.375000 26 | vt 0.625000 0.875000 27 | vt 0.625000 0.125000 28 | vt 0.250000 0.625000 29 | vt 0.500000 0.875000 30 | vt 0.500000 0.000000 31 | vt 0.250000 0.750000 32 | vt 0.500000 0.125000 33 | vt 0.125000 0.625000 34 | s 0 35 | usemtl Material 36 | f 2/4/1 3/7/1 1/1/1 37 | f 4/10/2 2/6/2 1/3/2 38 | f 1/2/3 3/8/3 5/11/3 39 | f 1/3/4 5/13/4 4/10/4 40 | f 2/5/5 6/14/5 3/9/5 41 | f 6/15/6 2/6/6 4/10/6 42 | f 6/14/7 5/12/7 3/9/7 43 | f 4/10/8 5/13/8 6/15/8 44 | -------------------------------------------------------------------------------- /octa.obj.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wavefront_obj" 4 | importer_version=1 5 | type="Mesh" 6 | uid="uid://blag3iii8af8j" 7 | path="res://.godot/imported/octa.obj-8091aa9bd5ff3f6d8b4a19bfdf4ea78f.mesh" 8 | 9 | [deps] 10 | 11 | files=["res://.godot/imported/octa.obj-8091aa9bd5ff3f6d8b4a19bfdf4ea78f.mesh"] 12 | 13 | source_file="res://addons/skelerealms/octa.obj" 14 | dest_files=["res://.godot/imported/octa.obj-8091aa9bd5ff3f6d8b4a19bfdf4ea78f.mesh", "res://.godot/imported/octa.obj-8091aa9bd5ff3f6d8b4a19bfdf4ea78f.mesh"] 15 | 16 | [params] 17 | 18 | generate_tangents=true 19 | scale_mesh=Vector3(1, 1, 1) 20 | offset_mesh=Vector3(0, 0, 0) 21 | optimize_mesh=true 22 | force_disable_mesh_compression=false 23 | -------------------------------------------------------------------------------- /plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Skelerealms" 4 | description="An extensible open world RPG framework for Godot 4." 5 | author="Slashscreen" 6 | version="beta 0.6" 7 | script="skelerealms.gd" 8 | -------------------------------------------------------------------------------- /scripts/ai/Modules/default_crime_report.gd: -------------------------------------------------------------------------------- 1 | extends AIModule 2 | 3 | 4 | func _ready() -> void: 5 | CrimeMaster.crime_committed.connect(react.bind()) 6 | 7 | 8 | ## React to a committed crime. If the perpetrator can be seen, the crime will be reported. 9 | func react(crime:Crime, pos:Vector3) -> void: 10 | if _npc.can_see_entity(crime.perpetrator): 11 | # TODO: take in whether they report crimes against other covens. 12 | # TODO: Try aggress 13 | _npc.printe("Witnessed crime.") 14 | CrimeMaster.add_crime(crime, _npc.parent_entity.name) 15 | _npc.crime_witnessed.emit() 16 | 17 | 18 | func get_type() -> String: 19 | return "DefaultCrimeReportModule" 20 | -------------------------------------------------------------------------------- /scripts/ai/Modules/default_damage_module.gd: -------------------------------------------------------------------------------- 1 | extends AIModule 2 | ## Example implementation of a damage processing AI Module. 3 | 4 | 5 | @export_category("Physical") 6 | @export var sharp_modifier:float = 1.0 7 | @export var piercing_modifier:float = 1.0 8 | @export var blunt_modifier:float = 1.0 9 | @export var poison_modifier:float = 1.0 10 | @export_category("Magic") 11 | @export var magic_modifier:float = 1.0 12 | @export var light_modifier:float = 1.0 13 | @export var frost_modifier:float = 1.0 14 | @export var flame_modifier:float = 1.0 15 | @export var plant_modifier:float = 1.0 16 | @export_category("Attribute") 17 | @export var stamina_modifier:float = 1.0 18 | @export var will_modifier:float = 1.0 19 | 20 | var spell_component:SpellTargetComponent 21 | var vitals_component:VitalsComponent 22 | 23 | signal damage_received 24 | 25 | 26 | func _initialize() -> void: 27 | _npc.parent_entity.get_component("DamageableComponent").damaged.connect(func(info): 28 | process_damage(info) 29 | ) 30 | spell_component = _npc.parent_entity.get_component("SpellTargetComponent") 31 | vitals_component = _npc.parent_entity.get_component("VitalsComponent") 32 | 33 | 34 | func process_damage(info:DamageInfo) -> void: 35 | # Damage effects 36 | var accumulated_damage = 0 37 | for effect in info.damage_effects: 38 | var effect_amount = info.damage_effects[effect] 39 | # if you have many more than these, some sort of dictionary may be in order. 40 | match effect: 41 | # Physical 42 | &"sharp": 43 | accumulated_damage = effect_amount * sharp_modifier 44 | &"piercing": 45 | accumulated_damage = effect_amount * piercing_modifier 46 | &"blunt": 47 | accumulated_damage = effect_amount * blunt_modifier 48 | &"poison": 49 | accumulated_damage = effect_amount * poison_modifier 50 | # Magic 51 | &"light": 52 | accumulated_damage = effect_amount * light_modifier * magic_modifier 53 | &"frost": 54 | accumulated_damage = effect_amount * frost_modifier * magic_modifier 55 | &"flame": 56 | accumulated_damage = effect_amount * flame_modifier * magic_modifier 57 | &"plant": 58 | accumulated_damage = effect_amount * plant_modifier * magic_modifier 59 | # Attribute 60 | &"moxie": 61 | vitals_component.vitals["moxie"] -= effect_amount * stamina_modifier 62 | &"will": 63 | vitals_component.vitals["will"] -= effect_amount * will_modifier 64 | 65 | _npc.damaged_with_effect.emit(effect) 66 | 67 | # Apply damage 68 | vitals_component.vitals["health"] -= accumulated_damage 69 | 70 | # Add magic effects 71 | for eff in info.spell_effects: 72 | spell_component.add_effect(eff) 73 | 74 | # Send damaging signal if we are hit by an entity 75 | # Could also add behavior somewhere to avoid areas that cause damage. Looking at you, Lydia Skyrim. 76 | if not info.offender == "": 77 | _npc.hit_by.emit(info.offender) 78 | 79 | damage_received.emit() 80 | 81 | 82 | func get_type() -> String: 83 | return "DefaultDamageModule" 84 | -------------------------------------------------------------------------------- /scripts/ai/Modules/default_interact_response.gd: -------------------------------------------------------------------------------- 1 | extends AIModule 2 | 3 | 4 | func _initialize() -> void: 5 | _npc.interacted.connect(on_interact.bind()) 6 | _npc._interactive_component.interact_verb = "TALK" 7 | 8 | 9 | func on_interact(refID:StringName) -> void: 10 | _npc.start_dialogue.emit() 11 | _npc._busy = true 12 | 13 | 14 | func get_type() -> String: 15 | return "DefaultInteractResponseModule" 16 | -------------------------------------------------------------------------------- /scripts/ai/Modules/default_movement.gd: -------------------------------------------------------------------------------- 1 | extends AIModule 2 | 3 | 4 | ## Default movement that doesn't interface with animations at all. Just gliding across the floor like Jamiroquai. 5 | 6 | 7 | func get_type() -> String: 8 | return "DefaultMovementModule" 9 | 10 | 11 | func _initialize() -> void: 12 | _npc.puppet_request_move.connect(move.bind()) 13 | 14 | 15 | func move(puppet:NPCPuppet) -> void: 16 | if puppet.navigation_agent.is_navigation_finished(): 17 | return 18 | 19 | var target:Vector3 = puppet.navigation_agent.get_next_path_position() 20 | var pos:Vector3 = puppet.global_position 21 | 22 | puppet.velocity = pos.direction_to(pos) * puppet.movement_speed 23 | puppet.move_and_slide() 24 | -------------------------------------------------------------------------------- /scripts/ai/Modules/default_stealth_detection.gd: -------------------------------------------------------------------------------- 1 | extends AIModule 2 | 3 | 4 | @export var view_dist:float = 30 5 | 6 | 7 | func _process(delta: float) -> void: 8 | _update_fsm(_npc.perception_memory, delta) 9 | 10 | 11 | func _update_fsm(data:Dictionary, delta:float) -> void: 12 | for ref_id:StringName in data: 13 | if has_node(NodePath(ref_id)): 14 | (get_node(NodePath(ref_id)) as FSM).update(data[ref_id], delta, _npc._puppet.global_position.distance_to(data[ref_id].last_seen_position)) 15 | else: 16 | var fsm := FSM.new() 17 | fsm.name = ref_id 18 | fsm.state_changed.connect(func(s:int) -> void: 19 | _npc.awareness_state_changed.emit(ref_id, s) 20 | ) 21 | add_child(fsm) 22 | fsm.update(data[ref_id], delta, _npc._puppet.global_position.distance_to(data[ref_id].last_seen_position)) 23 | 24 | 25 | class FSM: 26 | extends Node 27 | 28 | 29 | enum { 30 | UNAWARE, 31 | AWARE_VISIBLE, 32 | AWARE_INVISIBLE, 33 | WARY 34 | } 35 | 36 | 37 | const LOSE_TIMER_MAX := 120.0 38 | 39 | var state:int = UNAWARE 40 | var seek_timer:float = INF 41 | 42 | signal state_changed(new_state:int) 43 | 44 | 45 | func update(data:Dictionary, delta:float, dist:float) -> void: 46 | var vis:float = data[&"visibility"] 47 | var in_view_cone:bool = not is_zero_approx(vis) 48 | 49 | match state: 50 | UNAWARE, WARY: 51 | if in_view_cone: 52 | state = AWARE_VISIBLE 53 | AWARE_VISIBLE: 54 | if is_zero_approx(vis): 55 | state = AWARE_INVISIBLE 56 | seek_timer = LOSE_TIMER_MAX 57 | AWARE_INVISIBLE: 58 | if not is_zero_approx(vis): 59 | state = AWARE_VISIBLE 60 | else: 61 | seek_timer -= delta 62 | if seek_timer <= 0.0: 63 | state = WARY 64 | 65 | -------------------------------------------------------------------------------- /scripts/ai/PerceptionFSM/machine_perception.gd: -------------------------------------------------------------------------------- 1 | class_name PerceptionFSM_Machine 2 | extends FSMMachine 3 | ## The NPC perpection tracking is a finite state machine for easily tweakable behavior. 4 | ## This is where the stealth mechanics come in- how the NPC processes seeing stuff. The ? -> ! pipeline in MGS games. I dunno. I'm tired. I hope you get it. 5 | ## The current state machine looks like this: [br] 6 | ## [codeblock] 7 | ## ┌───────┐ ┌──────────────┐ 8 | ## │Unaware│ ┌─────────────┤Lost track of │ 9 | ## └───┬───┘ │ └──────────────┘ 10 | ## │ │ ▲ 11 | ## │ │ │ 12 | ## ▼ ▼ │ 13 | ## ┌────────────┐ ┌─────┴────────┐ 14 | ## │AwareVisible├──────────► │AwareInvisible│ 15 | ## └────────────┘ ◄──────────┴──────────────┘ 16 | ## [/codeblock] 17 | 18 | 19 | ## RefID of tracked entity. 20 | var tracked:String 21 | ## The current visibility of the entity. 0 means it is not visible. 22 | var visibility:float 23 | ## The last known position of the entity. 24 | var last_seen_position:Vector3 25 | ## The last known world of this entity. 26 | var last_seen_world:String 27 | 28 | 29 | func _init(tracked_obj:String, vis:float) -> void: 30 | tracked = tracked_obj 31 | visibility = vis 32 | 33 | 34 | func _ready() -> void: 35 | print("Machine created") 36 | 37 | 38 | ## Remove this FSM from the system. 39 | func remove_fsm() -> void: 40 | (get_parent() as NPCComponent).perception_forget(tracked) 41 | -------------------------------------------------------------------------------- /scripts/ai/PerceptionFSM/state_aware_invisible.gd: -------------------------------------------------------------------------------- 1 | class_name PerceptionFSM_Aware_Invisible 2 | extends FSMState 3 | ## In this state, the line of sight has been broken. THe NPC may look for the target here. 4 | 5 | 6 | ## The time it takes to lose track of something, in seconds 7 | var lose_timer_max:float = 60 8 | var _npc:NPCComponent 9 | var lose_timer:float 10 | 11 | 12 | func _get_state_name() -> String: 13 | return "AwareInvisible" 14 | 15 | 16 | func on_ready() -> void: 17 | _npc = owner as NPCComponent 18 | 19 | 20 | func enter(msg:Dictionary = {}) -> void: 21 | lose_timer = lose_timer_max 22 | 23 | 24 | func update(delta:float) -> void: 25 | lose_timer -= delta # decrease timer 26 | if lose_timer <= 0: 27 | state_machine.transition("Lost") 28 | if not (state_machine as PerceptionFSM_Machine).visibility == 0: 29 | state_machine.transition("AwareVisible") 30 | -------------------------------------------------------------------------------- /scripts/ai/PerceptionFSM/state_aware_visible.gd: -------------------------------------------------------------------------------- 1 | class_name PerceptionFSM_Aware_Visible 2 | extends FSMState 3 | ## In this state, it is actively looking at the target. 4 | 5 | 6 | var _npc:NPCComponent 7 | var e:SKEntity 8 | 9 | 10 | func _get_state_name() -> String: 11 | return "AwareVisible" 12 | 13 | 14 | func on_ready() -> void: 15 | _npc = owner as NPCComponent 16 | 17 | 18 | func update(delta:float) -> void: 19 | (state_machine as PerceptionFSM_Machine).last_seen_position = e.position 20 | (state_machine as PerceptionFSM_Machine).last_seen_world = e.world 21 | if (state_machine as PerceptionFSM_Machine).visibility == 0: # we need the player to be completely invisible to evade detection. We can't just vanish into the shadows 22 | state_machine.transition("AwareInvisible") 23 | 24 | 25 | func enter(msg:Dictionary = {}) -> void: 26 | e = SKEntityManager.instance.get_entity((state_machine as PerceptionFSM_Machine).tracked) 27 | -------------------------------------------------------------------------------- /scripts/ai/PerceptionFSM/state_lost.gd: -------------------------------------------------------------------------------- 1 | class_name PerceptionFSM_Lost 2 | extends FSMState 3 | 4 | 5 | const forget_timer_max:float = 600 6 | var _npc:NPCComponent 7 | var forget_timer:float = 0 8 | 9 | 10 | func _get_state_name() -> String: 11 | return "Lost" 12 | 13 | 14 | func on_ready() -> void: 15 | _npc = owner as NPCComponent 16 | 17 | 18 | func update(delta:float) -> void: 19 | # if the thing is visible again, we are aware of it again. 20 | # if it is in state lost, this NPC will "recognize" it, and immediately remember it. 21 | if (state_machine as PerceptionFSM_Machine).visibility >= _npc.visibility_threshold: 22 | state_machine.transition("AwareVisible") 23 | 24 | forget_timer -= delta 25 | if forget_timer < 0: 26 | (state_machine as PerceptionFSM_Machine).remove_fsm() 27 | 28 | 29 | func enter(msg:Dictionary) -> void: 30 | forget_timer = forget_timer_max 31 | -------------------------------------------------------------------------------- /scripts/ai/PerceptionFSM/state_unaware.gd: -------------------------------------------------------------------------------- 1 | class_name PerceptionFSM_Unaware 2 | extends FSMState 3 | ## In this state, the NPC is processing what it's seeing. 4 | 5 | 6 | var detection_timer_max: float = 1 7 | var _npc:NPCComponent 8 | var detection_speed:float = 1 9 | var detection_timer:float = 0 10 | 11 | 12 | func _get_state_name() -> String: 13 | return "Unaware" 14 | 15 | 16 | func on_ready() -> void: 17 | _npc = owner as NPCComponent 18 | 19 | 20 | func update(delta:float) -> void: 21 | detection_timer += detection_speed * (state_machine as PerceptionFSM_Machine).visibility * delta 22 | if detection_timer >= detection_timer_max: 23 | state_machine.transition("AwareVisible") 24 | return 25 | if (state_machine as PerceptionFSM_Machine).visibility <= _npc.visibility_threshold: 26 | (state_machine as PerceptionFSM_Machine).remove_fsm() 27 | 28 | 29 | func enter(message:Dictionary) -> void: 30 | # if we are tracking an item, skip right to aware visible 31 | if SKEntityManager.instance.get_entity((state_machine as PerceptionFSM_Machine).tracked).get_component("ItemComponent"): 32 | state_machine.transition("AwareVisible") 33 | -------------------------------------------------------------------------------- /scripts/ai/ai_module.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name AIModule 3 | extends Node 4 | ## Base class for AI Packages for NPCs. 5 | ## Skelerealms uses 2 AI systems, each with different roles. 6 | ## The AI Package system determines what goals the NPC should attempt to achieve, and the GOAP AI system figures out how to achieve it. 7 | ## Override this to set custom behaviors by attaching to [NPCComponent]'s many signals. 8 | 9 | 10 | @onready var _npc:NPCComponent = get_parent() 11 | 12 | 13 | ## Link this module to the component. 14 | func link(npc:NPCComponent) -> void: 15 | self._npc = npc 16 | 17 | 18 | ## The "ready" function if you depend on the NPC's variables. 19 | func initialize() -> void: 20 | pass 21 | 22 | 23 | func _clean_up() -> void: 24 | return 25 | 26 | 27 | func get_type() -> String: 28 | return "AIModule" 29 | 30 | 31 | ## Prints a rich text message to the console prepended with the entity name. Used for easier debugging. 32 | func printe(text:String) -> void: 33 | _npc.printe(text) 34 | -------------------------------------------------------------------------------- /scripts/ai/goap_action.gd: -------------------------------------------------------------------------------- 1 | class_name GOAPAction 2 | extends Node 3 | 4 | 5 | ## The cost of this action when making a plan. 6 | var cost:float = 1.0 7 | ## Whether this objective is actively being worked on 8 | var running:bool = false 9 | var parent_goap:GOAPComponent 10 | var entity:SKEntity 11 | ## The duration of this action. 12 | var duration: float 13 | 14 | 15 | func is_achievable_given(state:Dictionary) -> bool: 16 | return state.has_all(get_prerequisites().keys()) 17 | 18 | 19 | func is_achievable() -> bool: 20 | return true 21 | 22 | 23 | func pre_perform() -> bool: 24 | return true 25 | 26 | 27 | func target_reached() -> bool: 28 | return true 29 | 30 | 31 | func post_perform() -> bool: 32 | return true 33 | 34 | 35 | func is_target_reached(agent:NavigationAgent3D) -> bool: 36 | return agent.is_navigation_finished() 37 | 38 | 39 | func interrupt() -> void: 40 | return 41 | 42 | 43 | func get_prerequisites() -> Dictionary: 44 | return {} 45 | 46 | 47 | func get_effects() -> Dictionary: 48 | return {} 49 | 50 | 51 | func get_id() -> StringName: 52 | return &"" 53 | -------------------------------------------------------------------------------- /scripts/ai/light_estimation_provider.gd: -------------------------------------------------------------------------------- 1 | class_name LightEstimation 2 | extends Node3D 3 | 4 | const interpolation_method:Image.Interpolation = Image.INTERPOLATE_BILINEAR 5 | var svpt: SubViewport 6 | var svpb: SubViewport 7 | #@export var render_target: ViewportTexture 8 | 9 | 10 | ## Calculates a light level at a given point. 11 | ## Output appears to be vaguely logarithmic, but has been scaled to have 1 be roughly 12 | ## in direct sunlight. 13 | func get_light_level_for_point(point:Vector3) -> float: 14 | # Move the octahedron to point 15 | position = point 16 | # reset location 17 | await RenderingServer.frame_post_draw 18 | # camera render both sides 19 | var img:Image = svpt.get_texture().get_image() 20 | # resize to 1x1 21 | img.resize(1,1, interpolation_method) 22 | # return luminance 23 | var top = img.get_pixel(0,0).get_luminance() 24 | 25 | # Do the other thing for the other side 26 | img = svpb.get_texture().get_image() 27 | img.resize(1,1, interpolation_method) 28 | var bottom = img.get_pixel(0,0).get_luminance() 29 | 30 | return ((top + bottom) / 2) / 0.4 # average top and bottom 31 | 32 | 33 | func _ready() -> void: 34 | svpt = $SViewportTop 35 | svpb = $SViewportBottom 36 | -------------------------------------------------------------------------------- /scripts/ai/perception_ears.gd: -------------------------------------------------------------------------------- 1 | class_name PerceptionEars 2 | extends CollisionShape3D 3 | ## Add to something to make it be able to hear. 4 | ## Isn't an [SKEntityComponent], so can be added to anything. 5 | ## Be sure to add a shape. 6 | 7 | 8 | ## Called when it hears something. 9 | signal heard_something(emitter:AudioEventEmitter) 10 | 11 | 12 | # Called when the node enters the scene tree for the first time. 13 | func _ready() -> void: 14 | add_to_group("audio_listener") 15 | 16 | 17 | func hear_audio(emitter:AudioEventEmitter): 18 | heard_something.emit(emitter) 19 | -------------------------------------------------------------------------------- /scripts/barter/transaction.gd: -------------------------------------------------------------------------------- 1 | class_name Transaction 2 | extends RefCounted 3 | ## An object keeping track of stuff being bought and sold while bartering. 4 | 5 | 6 | ## Who is selling 7 | var vendor:InventoryComponent 8 | ## Who is buying (Player) 9 | var customer:InventoryComponent 10 | ## What the customer is selling 11 | var selling:Array[String] = [] 12 | ## What the customer is buying 13 | var buying:Array[String] = [] 14 | ## Balance of transaction 15 | var balance:int 16 | 17 | 18 | func _init(v:InventoryComponent, c:InventoryComponent) -> void: 19 | vendor = v 20 | customer = c 21 | 22 | 23 | ## Get the total amount for the transaction, in terms of change in the customer's money. 24 | func total_transaction(selling_modifier:float, buying_modifier:float) -> int: 25 | var total:int = 0 26 | # Total selling amount and add 27 | total += selling.reduce( 28 | func(accum: int, item:String): 29 | return accum + roundi(( SKEntityManager.instance.get_entity(item)\ 30 | .get_component("ItemComponent")\ 31 | as ItemComponent)\ 32 | .data\ 33 | .worth * selling_modifier) 34 | ,0 35 | ) 36 | # Total selling amount and subtract 37 | total -= buying.reduce( 38 | func(accum: int, item:String): 39 | return accum + roundi(( SKEntityManager.instance.get_entity(item)\ 40 | .get_component("ItemComponent")\ 41 | as ItemComponent)\ 42 | .data\ 43 | .worth * buying_modifier) 44 | ,0 45 | ) 46 | return total 47 | -------------------------------------------------------------------------------- /scripts/components/attributes_component.gd: -------------------------------------------------------------------------------- 1 | class_name AttributesComponent 2 | extends SKEntityComponent 3 | ## Holds the attributes of an SKEntity, such as the D&D abilities - Charisma, Dexterity, etc. 4 | 5 | 6 | ## The attributes of this SKEntity. 7 | ## It is in a dictionary so you can add, remove, and customize at will. 8 | @export var attributes:Dictionary: 9 | get: 10 | return attributes 11 | set(val): 12 | attributes = val 13 | dirty = true 14 | # I yearn for Ruby's symbols, but StingName is an adequate substitute. 15 | # I yearn for ruby just in general. 16 | 17 | func _init() -> void: 18 | name = "AttributesComponent" 19 | 20 | 21 | func save() -> Dictionary: 22 | dirty = false 23 | return attributes 24 | 25 | 26 | func load_data(data:Dictionary): 27 | attributes = data 28 | dirty = false 29 | 30 | 31 | func gather_debug_info() -> String: 32 | return """ 33 | [b]AttributesComponent[/b] 34 | Attributes: 35 | %s 36 | """ % [ 37 | JSON.stringify(attributes, '\t').indent("\t\t") 38 | ] 39 | -------------------------------------------------------------------------------- /scripts/components/chest_component.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name ChestComponent 3 | extends SKEntityComponent 4 | 5 | 6 | ## Optionally refreshing inventories. 7 | 8 | 9 | @onready var loot_table:SKLootTable = get_child(0) 10 | @export_range(0, 100, 1, "or_greater") var reset_time_minutes:int ## How long it takes to refresh this chest, in in-game minutes. 0 will not refresh. 11 | @export var owner_id:StringName 12 | var looted_time:Timestamp 13 | 14 | 15 | func _ready() -> void: 16 | if Engine.is_editor_hint(): 17 | return 18 | if reset_time_minutes > 0: 19 | GameInfo.minute_incremented.connect(_check_should_restore.bind()) 20 | # If none provided, just generate a dummy loot table that will do nothing. 21 | if loot_table == null: 22 | var nlt := SKLootTable.new() 23 | add_child(nlt) 24 | loot_table = nlt 25 | 26 | 27 | func _check_should_restore() -> void: 28 | if not looted_time: 29 | return 30 | if parent_entity.in_scene or Timestamp.dict_to_minutes(Timestamp.build_from_world_timestamp().time_since(looted_time)) < reset_time_minutes: # will not refresh while in scene 31 | return 32 | clear() 33 | reroll() 34 | 35 | 36 | func clear() -> void: 37 | var ic:InventoryComponent = parent_entity.get_component("InventoryComponent") 38 | for i:StringName in ic.inventory: 39 | SKEntityManager.instance.remove_entity(i) 40 | ic.inventory.clear() # Doing this instead of the remove item function since looping and removing stuff is bad and I don't need the signal 41 | ic.currencies.clear() 42 | 43 | 44 | func reroll() -> void: 45 | var ic:InventoryComponent = parent_entity.get_component("InventoryComponent") 46 | var res: Dictionary = loot_table.resolve() 47 | 48 | for id:PackedScene in res.items: 49 | var e:SKEntity = SKEntityManager.instance.add_entity(id) 50 | ic.add_to_inventory(e.name) 51 | for id:StringName in res.entities: 52 | ic.add_to_inventory(id) 53 | ic.currencies = res.currencies 54 | 55 | 56 | func on_generate() -> void: 57 | reroll() 58 | 59 | 60 | func get_dependencies() -> Array[String]: 61 | return [ 62 | "InventoryComponent", 63 | ] 64 | -------------------------------------------------------------------------------- /scripts/components/covens_component.gd: -------------------------------------------------------------------------------- 1 | class_name CovensComponent 2 | extends SKEntityComponent 3 | ## Allows an SKEntity to be part of a [Coven]. 4 | ## Covens in this context are analagous to Bethesda games' Factions- groups of NPCs that behave in a similar way. 5 | ## Coven membership is also reflected in groups that the entity is in. 6 | 7 | 8 | ## IDs of covens this entity is a member of. 9 | ## This dictionary is of type StringName:Int, where key is the coven, and int is the rank of this member. 10 | @export var covens:Dictionary 11 | 12 | 13 | func _init(coven_list:Array[CovenRankData] = []) -> void: 14 | name = "CovensComponent" 15 | if coven_list.is_empty(): 16 | return 17 | # Load rank info 18 | for crd in coven_list: 19 | #printe("Adding to coven %s" % crd.coven.coven_id) 20 | covens[crd.coven.coven_id] = crd.rank 21 | 22 | 23 | func _ready(): 24 | super._ready() 25 | # Add corresponding covens. 26 | for c in covens: 27 | parent_entity.add_to_group(c) 28 | 29 | 30 | ## Add this entity to a coven. 31 | func add_to_coven(coven:StringName, rank:int = 1): 32 | covens[coven] = 1 33 | parent_entity.add_to_group(coven) 34 | 35 | 36 | ## Remove this entity from the coven. 37 | func remove_from_coven(coven:StringName): 38 | covens.erase(coven) 39 | parent_entity.remove_from_group(coven) 40 | 41 | 42 | ## Whether the entity is in a coven or not. 43 | func is_in_coven(coven:StringName) -> bool: 44 | return covens.has(coven) 45 | 46 | 47 | ## Get this entity's rank in a coven. Returns 0 if they aren't in the coven. 48 | func get_coven_rank(coven:StringName) -> int: 49 | return covens[coven] if covens.has(coven) else 0 50 | -------------------------------------------------------------------------------- /scripts/components/damageable_component.gd: -------------------------------------------------------------------------------- 1 | class_name DamageableComponent 2 | extends SKEntityComponent 3 | ## Allows an entity to be damaged. 4 | 5 | 6 | signal damaged(info:DamageInfo) 7 | 8 | 9 | func damage(info:DamageInfo): 10 | damaged.emit(info) 11 | 12 | 13 | func _init() -> void: 14 | name = "DamageableComponent" 15 | -------------------------------------------------------------------------------- /scripts/components/effects_component.gd: -------------------------------------------------------------------------------- 1 | class_name EffectsComponent 2 | extends SKEntityComponent 3 | 4 | 5 | ## This component governs active effects on this entity. 6 | 7 | 8 | var host:StatusEffectHost 9 | 10 | 11 | func _init() -> void: 12 | name = "EffectsComponent" 13 | 14 | 15 | func _ready() -> void: 16 | host = StatusEffectHost.new() 17 | add_child(host) 18 | host.message_broadcast.connect(parent_entity.broadcast_message.bind()) 19 | 20 | 21 | ## Pass-through for [method StatusEffectHost.add_effect]. 22 | func add_effect(what:StringName) -> void: 23 | host.add_effect(what) 24 | 25 | 26 | func remove_effect(e:StringName) -> void: 27 | host.remove_effect(e) 28 | -------------------------------------------------------------------------------- /scripts/components/equipment_component.gd: -------------------------------------------------------------------------------- 1 | class_name EquipmentComponent 2 | extends SKEntityComponent 3 | 4 | 5 | var equipment_slot:Dictionary 6 | 7 | signal equipped(item:StringName, slot:StringName) 8 | signal unequipped(item:StringName, slot:StringName) 9 | 10 | 11 | func _init() -> void: 12 | name = "EquipmentComponent" 13 | 14 | 15 | func _ready() -> void: 16 | super._ready() 17 | 18 | 19 | func equip(item:StringName, slot:StringName, silent:bool = false) -> bool: 20 | # Get component 21 | var e = SKEntityManager.instance.get_entity(item) 22 | if not e: 23 | return false 24 | # Get item component 25 | var ic = e.get_component("ItemComponent") 26 | if not ic: 27 | return false 28 | # Get equippable data component 29 | var ec = (ic as ItemComponent).get_component("EquippableDataComponent") 30 | if not ec: 31 | return false 32 | # Check slot validity 33 | if not (ec as EquippableDataComponent).valid_slots.has(slot): 34 | return false 35 | # Unequip if already in slot so we ca nput it in a new slot 36 | unequip_item(item) 37 | 38 | equipment_slot[slot] = item 39 | if not silent: 40 | equipped.emit(item, slot) 41 | return true 42 | 43 | 44 | ## Unequip anything in a slot. 45 | func clear_slot(slot:StringName, silent:bool = false) -> void: 46 | if equipment_slot.has(slot): 47 | var to_unequip = equipment_slot[slot] 48 | equipment_slot[slot] = null 49 | if not silent: 50 | unequipped.emit(to_unequip, slot) 51 | 52 | 53 | ## Unequip a specific item, no matter what slot it's in. 54 | func unequip_item(item:StringName, silent:bool = false) -> void: 55 | for s in equipment_slot: 56 | if equipment_slot[s] == item: 57 | equipment_slot[s] = null 58 | if not silent: 59 | unequipped.emit(item, s) 60 | return 61 | 62 | 63 | func is_item_equipped(item:StringName, slot:StringName) -> bool: 64 | if not equipment_slot.has(slot): 65 | return false 66 | return equipment_slot[slot] == item 67 | 68 | 69 | func is_slot_occupied(slot:StringName) -> Option: 70 | if equipment_slot.has(slot): 71 | return Option.wrap(equipment_slot[slot]) 72 | else: 73 | return Option.none() 74 | -------------------------------------------------------------------------------- /scripts/components/interactive_component.gd: -------------------------------------------------------------------------------- 1 | class_name InteractiveComponent 2 | extends SKEntityComponent 3 | ## Handles interactions on an entity 4 | 5 | ## Emitted when this entity is interacted with. 6 | signal interacted(id:String) 7 | 8 | ## Whether it can be interacted with. 9 | @export var interactible:bool = true 10 | ## What tooltip to display when the cursor hovers over this. The RefID is used as the object name. 11 | @export var interact_verb:String = "INTERACT" 12 | ## A callback (that returns String) that allows you to get a custom string for interact text rather than 13 | ## using the RefID. 14 | ## For example: If you dynamically created an NPC (eg. spawning is a Spider enemy), you could instead grab 15 | ## a translated version of your handmade NPCData's ID rather than trying to translate a randomly generated 16 | ## RefID. 17 | var translation_callback:Callable 18 | ## Gets the translated RefID, or, if applicable, whatever is returned by [member translation_callback] 19 | var interact_name:String: 20 | get: 21 | if not translation_callback.is_null(): 22 | return translation_callback.call() 23 | else: 24 | return tr(parent_entity.name) 25 | 26 | 27 | func _init() -> void: 28 | name = "InteractiveComponent" 29 | 30 | ## Interact with this as the player. 31 | ## Shorthand for [codeblock] interact("Player") [/codeblock]. 32 | func interact_by_player(): 33 | interacted.emit("Player") 34 | 35 | ## Interact with this entity. Pass in the refID of the interactor. 36 | func interact(refID:String): 37 | interacted.emit(refID) 38 | -------------------------------------------------------------------------------- /scripts/components/inventory_component.gd: -------------------------------------------------------------------------------- 1 | class_name InventoryComponent 2 | extends SKEntityComponent 3 | 4 | 5 | ## Keeps track of an inventory and currencies. 6 | ## If you add an [SKLootTable] node underneath, the loot table will be rolled upon generating. See [method SKEntityComponent.on_generate]. 7 | 8 | 9 | ## The RefIDs of the items in the inventory. Put any unique items in here. 10 | @export var inventory: PackedStringArray 11 | ## The amount of cash moneys. 12 | var currencies = {} 13 | 14 | signal added_to_inventory(id:String) 15 | signal removed_from_inventory(id:String) 16 | signal inventory_changed 17 | signal added_money(amount:int) 18 | signal removed_money(amount:int) 19 | 20 | 21 | func _ready() -> void: 22 | added_to_inventory.connect(func(x): inventory_changed.emit()) 23 | removed_from_inventory.connect(func(x): inventory_changed.emit()) 24 | 25 | 26 | ## Add an item to the inventory. 27 | func add_to_inventory(id:String): 28 | var e = SKEntityManager.instance.get_entity(id) 29 | if e: 30 | var ic = e.get_component("ItemComponent") 31 | if ic: 32 | inventory.append(id) 33 | added_to_inventory.emit(id) 34 | 35 | 36 | ## Remove an item from the inventory. 37 | func remove_from_inventory(id:String): 38 | var index = inventory.find(id) 39 | if index == -1: # catch if it doesnt have the item 40 | return 41 | inventory.remove_at(index) 42 | removed_from_inventory.emit(id) 43 | 44 | 45 | ## Add an amount of snails to the inventory. 46 | func add_money(amount:int, currency:StringName): 47 | added_money.emit(amount) 48 | if currencies.has(currency): 49 | currencies[currency] += amount 50 | else: 51 | currencies[currency] = amount 52 | _clamp_money(currency) 53 | 54 | 55 | ## Remove some snails from the inventory. 56 | func remove_money(amount:int, currency:StringName): 57 | removed_money.emit(amount) 58 | if not currencies.has(currency): 59 | currencies[currency] = 0 60 | return 61 | currencies[currency] -= amount 62 | _clamp_money(currency) 63 | 64 | 65 | ## Keeps the number of snails from going below 0. 66 | func _clamp_money(currency:StringName): 67 | if currencies[currency] < 0: 68 | currencies[currency] = 0 69 | 70 | 71 | func count_item_by_data(data_id:String) -> int: 72 | var amount: int = 0 73 | for i in inventory: 74 | var ic:ItemComponent = SKEntityManager.instance.get_entity(i).get_component("ItemComponent") 75 | if ic.data.id == data_id: 76 | amount += 1 77 | return amount 78 | 79 | 80 | func has_item(ref_id:String) -> bool: 81 | return inventory.has(ref_id) 82 | 83 | 84 | func get_items_that(fn: Callable) -> Array[StringName]: 85 | var pt: Array[StringName] = [] 86 | for i in inventory: 87 | if fn.call(i): 88 | pt.append(i) 89 | return pt 90 | 91 | 92 | func get_items_of_form(id:String) -> Array[StringName]: 93 | return get_items_that(func(x:StringName): return ItemComponent.get_item_component(x).parent_entity.form_id == id) 94 | 95 | 96 | func on_generate() -> void: 97 | if get_child_count() == 0: 98 | return 99 | var lt:SKLootTable = get_child(0) as SKLootTable 100 | if not lt: 101 | return 102 | 103 | var res: Dictionary = lt.resolve() 104 | 105 | for id:PackedScene in res.items: 106 | var e:SKEntity = SKEntityManager.instance.add_entity(id) 107 | add_to_inventory(e.name) 108 | for id:StringName in res.entities: 109 | add_to_inventory(id) 110 | currencies = res.currencies 111 | 112 | 113 | func gather_debug_info() -> String: 114 | return """ 115 | [b]InventoryComponent[/b] 116 | Currency: %s 117 | Inventory: %s 118 | """ % [ 119 | JSON.stringify(currencies, "\t"), 120 | JSON.stringify(inventory, "\t"), 121 | ] 122 | -------------------------------------------------------------------------------- /scripts/components/marker_component.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name MarkerComponent 3 | extends SKEntityComponent 4 | ## Component tag for [WorldMarker]s. 5 | 6 | 7 | var rotation:Quaternion 8 | 9 | 10 | func _init(rot:Quaternion = Quaternion.IDENTITY) -> void: 11 | name = "MarkerComponent" 12 | rotation = rot 13 | 14 | 15 | func _ready() -> void: 16 | if Engine.is_editor_hint(): 17 | return 18 | super._ready() 19 | parent_entity.rotation = rotation 20 | 21 | 22 | func get_world_entity_preview() -> Node: 23 | var sphere := MeshInstance3D.new() 24 | sphere.mesh = SphereMesh.new() 25 | 26 | var mat := StandardMaterial3D.new() 27 | mat.albedo_color = Color.BLUE 28 | mat.albedo_color.a = 0.5 29 | 30 | sphere.set_surface_override_material(0, mat) 31 | return sphere 32 | -------------------------------------------------------------------------------- /scripts/components/navigator_component.gd: -------------------------------------------------------------------------------- 1 | class_name NavigatorComponent 2 | extends SKEntityComponent 3 | ## Handles finding paths through the granular navigation system. See [NavigationMaster]. 4 | 5 | 6 | ## Calculate a path from the entity's current position to a [NavPoint]. 7 | ## Array is empty if no path is found. 8 | func calculate_path_to(pt:NavPoint) -> Array[NavPoint]: 9 | var start := NavPoint.new(parent_entity.world, parent_entity.position) 10 | return NavMaster.instance.calculate_path(start, pt) 11 | 12 | 13 | func _init() -> void: 14 | name = "NavigatorComponent" 15 | -------------------------------------------------------------------------------- /scripts/components/player_component.gd: -------------------------------------------------------------------------------- 1 | class_name PlayerComponent 2 | extends SKEntityComponent 3 | ## Player component. 4 | 5 | 6 | var _set_up:bool 7 | 8 | 9 | func _init() -> void: 10 | name = "PlayerComponent" 11 | 12 | 13 | func _ready(): 14 | ($"../TeleportComponent" as TeleportComponent).teleporting.connect(teleport.bind()) 15 | (parent_entity.get_component("DamageableComponent") as DamageableComponent).damaged.connect(on_damage.bind()) 16 | 17 | 18 | func on_damage(info:DamageInfo) -> void: 19 | # TODO: Genericize, calculate buffs and debuffs 20 | (parent_entity.get_component("VitalsComponent") as VitalsComponent).change_health(-info.damage_effects[&"blunt"]) 21 | 22 | 23 | ## Set the entity's position. 24 | func set_entity_position(pos:Vector3): 25 | parent_entity.position = pos 26 | 27 | 28 | func set_entity_rotation(q:Quaternion) -> void: 29 | parent_entity.quaternion = q 30 | 31 | 32 | func _process(delta): 33 | if not parent_entity.world == GameInfo.world: 34 | parent_entity.world = GameInfo.world 35 | 36 | if _set_up: 37 | return 38 | 39 | var pc = $"../PuppetSpawnerComponent".puppet 40 | 41 | if not pc == null: 42 | pc.update_position.connect(set_entity_position.bind()) 43 | _set_up = true 44 | 45 | 46 | ## Teleport the player. 47 | func teleport(world:String, pos:Vector3): 48 | print("teleporting player to %s : %s" % [world, pos]) 49 | GameInfo.world = world # Set the game's world to destination world 50 | parent_entity.world = world # Set this entity world to the destination 51 | (%WorldLoader as WorldLoader).load_world(world) # Load world 52 | ($"../PuppetSpawnerComponent" as PuppetSpawnerComponent).set_puppet_position(pos) # Set player puppet position 53 | -------------------------------------------------------------------------------- /scripts/components/puppet_spawner_component.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name PuppetSpawnerComponent 3 | extends SKEntityComponent 4 | ## Manages spawning and despawning of puppets. 5 | 6 | 7 | var prefab: PackedScene 8 | ## The puppet node. 9 | var puppet:Node 10 | 11 | signal spawned_puppet(puppet:Node) 12 | signal despawned_puppet 13 | 14 | 15 | func _init() -> void: 16 | name = "PuppetSpawnerComponent" 17 | 18 | 19 | func _ready(): 20 | if Engine.is_editor_hint(): 21 | return 22 | super._ready() 23 | # brute force getting the puppet for the player if it already exists. 24 | if get_child_count() > 0: 25 | puppet = get_child(0) 26 | 27 | 28 | func get_world_entity_preview() -> Node: 29 | return get_child(0) 30 | 31 | 32 | func _on_enter_scene() -> void: 33 | spawn() 34 | 35 | 36 | func _on_exit_scene() -> void: 37 | despawn() 38 | 39 | 40 | ## Spawn a new puppet. 41 | func spawn(): 42 | var n:Node3D 43 | if not prefab and get_child_count() > 0: 44 | var ps: PackedScene = PackedScene.new() 45 | ps.pack(get_child(0)) 46 | prefab = ps 47 | n = get_child(0) 48 | else: 49 | if not prefab: 50 | printe("Failed spawning: no prefab.") 51 | return 52 | n = prefab.instantiate() 53 | add_child(n) 54 | n.set_position(parent_entity.position) 55 | puppet = n 56 | spawned_puppet.emit(puppet) 57 | printe("spawned at %s : %s" % [parent_entity.world, parent_entity.position]) 58 | 59 | 60 | ## Despawn a puppet. 61 | func despawn(): 62 | printe("despawned.") 63 | if not prefab: 64 | var ps: PackedScene = PackedScene.new() 65 | ps.pack(get_child(0)) 66 | prefab = ps 67 | 68 | for n in get_children(): 69 | n.queue_free() 70 | puppet = null 71 | despawned_puppet.emit() 72 | 73 | 74 | ## Set the puppet's position. 75 | func set_puppet_position(pos:Vector3): 76 | if not puppet == null: 77 | (puppet as Node3D).position = pos 78 | -------------------------------------------------------------------------------- /scripts/components/script_component.gd: -------------------------------------------------------------------------------- 1 | class_name ScriptComponent 2 | extends SKEntityComponent 3 | ## This class can be bound to any entity and acts as a way to make ad-hoc components, to fill the role Papyrus plays in Creation Kit. 4 | ## To create a script for this, simply extend this class, and add it to the [RefData] or [InstanceData] of the appropriate object. 5 | ## If you want to add a custom script to a world object instead, you can.... write a normal script... 6 | 7 | 8 | ## Stores references to all components of this entity, save for this one. 9 | ## Dictionary layout is "ComponentType" : SKEntityComponent. 10 | ## This is declared in _ready(), so be careful when overriding. 11 | var _components:Dictionary = {} 12 | 13 | 14 | func _init(sc:Script) -> void: 15 | if not sc.get_base_script().get_instance_base_type() == get_script().get_instance_base_type(): 16 | push_warning("The script \"%s\" does not inherit ScriptComponent. Deleting component to prevent unexpected behavior." % sc.get_instance_base_type()) 17 | call_deferred("queue_free") ## Queue next frame. I think. May not work. 18 | set_script(sc) 19 | 20 | 21 | func _ready() -> void: 22 | super._ready() 23 | name = "ScriptComponent" 24 | await parent_entity.instantiated 25 | for c in parent_entity.get_children(): 26 | if c == self: 27 | continue 28 | _components[c.name] = c 29 | -------------------------------------------------------------------------------- /scripts/components/shop_component.gd: -------------------------------------------------------------------------------- 1 | class_name ShopComponent 2 | extends ChestComponent 3 | 4 | 5 | ## Base value of how much (from 0-1) of the total price that the merchant will tollerate haggling. 6 | @export var haggle_tolerance:float 7 | ## Only items with at least one of these tags can be sold to this vendor. 8 | @export var whitelist:Array[StringName] = [] 9 | ## No items with at least one of these tags can be sold to this vendor. Supercedes [member whitelist]. 10 | @export var blacklist:Array[StringName] = [] 11 | ## Whether this merchant accepts stolen goods. 12 | @export var accept_stolen:bool 13 | -------------------------------------------------------------------------------- /scripts/components/skills_component.gd: -------------------------------------------------------------------------------- 1 | class_name SkillsComponent 2 | extends SKEntityComponent 3 | ## Component holding the skills of this entity. 4 | ## Examples in Skyrim would be Destruction, Sneak, Alteration, Smithing. 5 | 6 | 7 | ## The skills of this SKEntity. 8 | ## It is in a dictionary so you can add, remove, and customize at will. 9 | @export var skills:Dictionary: 10 | get: 11 | return skills 12 | set(val): 13 | skills = val 14 | dirty = true 15 | ## Character level of this character 16 | var level:int = 0 17 | ## Used to determine how to save levels. 18 | var _manually_set_level = false 19 | var skill_xp:Dictionary = {} 20 | var character_xp:int = 0 21 | 22 | signal skill_levelled_up(skill:StringName, new_level:int) 23 | signal character_levelled_up(new_level:int) 24 | 25 | 26 | func _init() -> void: 27 | name = "SkillsComponent" 28 | 29 | 30 | func save() -> Dictionary: 31 | dirty = false 32 | return { 33 | "skills": skills, 34 | "level": level if _manually_set_level else -1 35 | } 36 | 37 | 38 | func load_data(data:Dictionary): 39 | skills = data["skills"] 40 | level = data["level"] 41 | dirty = false 42 | 43 | 44 | func gather_debug_info() -> String: 45 | return """ 46 | [b]SkillsComponent[/b] 47 | Skills: 48 | %s 49 | """ % [ 50 | JSON.stringify(skills, '\t').indent("\t\t") 51 | ] 52 | 53 | 54 | func add_skill_xp(skill:StringName, amount:int) -> void: 55 | if not skills.has(skill): 56 | push_warning("SKEntity %s has no skill %s." % [parent_entity.name, skill]) 57 | return 58 | skill_xp[skill] += amount 59 | var target:int = SkeleRealmsGlobal.config.compute_skill(skills[skill]) 60 | if target == -1: 61 | return 62 | if skill_xp[skill] >= target: 63 | skills[skill] += 1 64 | skill_levelled_up.emit(skill, skills[skill]) 65 | 66 | 67 | func add_character_xp(amount:int) -> void: 68 | character_xp += amount 69 | var target:int = SkeleRealmsGlobal.config.compute_character(level) 70 | if target == -1: 71 | return 72 | if character_xp >= amount: 73 | level += 1 74 | character_levelled_up.emit(level) 75 | -------------------------------------------------------------------------------- /scripts/components/spell_target_component.gd: -------------------------------------------------------------------------------- 1 | class_name SpellTargetComponent 2 | extends SKEntityComponent 3 | ## Allows entities to be hit with spells, and keeps track of any applied spell effects. 4 | 5 | 6 | var status_effect:EffectsComponent 7 | 8 | 9 | signal hit_with_spell(spell:Spell) 10 | 11 | 12 | ## Hit this entity with a spell. Doesn't actually do anything apart from emit [signal hit_with_spell]. To apply effects, you can do that on the [Spell] side. 13 | func hit(spell:Spell) -> void: 14 | hit_with_spell.emit(spell) 15 | 16 | 17 | func _init() -> void: 18 | name = "SpellTargetComponent" 19 | 20 | 21 | func _entity_ready() -> void: 22 | status_effect = parent_entity.get_component(&"EffectsComponent") 23 | 24 | 25 | func add_effect(effect:StringName) -> void: 26 | status_effect.add_effect(effect) 27 | 28 | 29 | func remove_effect(eff:StringName) -> void: 30 | status_effect.remove_effect(eff) 31 | -------------------------------------------------------------------------------- /scripts/components/teleport_component.gd: -------------------------------------------------------------------------------- 1 | class_name TeleportComponent 2 | extends SKEntityComponent 3 | ## Allows an entity to warp. 4 | 5 | 6 | ## Emitted when teleporting. Used to let puppet holders know to move their puppets. 7 | signal teleporting(world:String, position:Vector3) 8 | 9 | 10 | ## Teleport the entity to a world and position. 11 | func teleport(world:String, position:Vector3): 12 | parent_entity.world = world 13 | parent_entity.position = position 14 | teleporting.emit(world, position) 15 | 16 | 17 | func _init() -> void: 18 | name = "TeleportComponent" 19 | -------------------------------------------------------------------------------- /scripts/components/view_direction_component.gd: -------------------------------------------------------------------------------- 1 | class_name ViewDirectionComponent 2 | extends SKEntityComponent 3 | 4 | 5 | var view_rot:Vector3 = Vector3.FORWARD 6 | 7 | 8 | func _init() -> void: 9 | name = "ViewDirectionComponent" 10 | -------------------------------------------------------------------------------- /scripts/components/vitals_component.gd: -------------------------------------------------------------------------------- 1 | class_name VitalsComponent 2 | extends SKEntityComponent 3 | ## Component keeping check of the main 3 attributes of an entity - health, stamina, and magica. 4 | 5 | # TODO: This is for player only, make a generalized one 6 | ## Called when this entity's health reaches 0. See [member health]. 7 | signal dies 8 | ## Called when the stamina value reaches 0. See [member moxie]. 9 | signal exhausted 10 | ## Called when the magica value reaches 0. See [member will]. 11 | signal drained 12 | signal hurt 13 | signal vitals_updated(data:Dictionary) 14 | 15 | 16 | const DISHONORED_MODE:bool = false 17 | ## Health, stamina, magica, and max of values. 18 | var vitals = { 19 | "health" = 100.0, 20 | "moxie" = 100.0, 21 | "will" = 100.0, 22 | "max_health" = 100.0, 23 | "max_moxie" = 100.0, 24 | "max_will" = 100.0, 25 | "return_to_will" = 0.0, 26 | }: 27 | get: 28 | return vitals 29 | set(val): 30 | vitals = val 31 | dirty = true 32 | vitals_updated.emit(vitals) 33 | var moxie_recharge_rate:float = 2 34 | var moxie_just_changed:bool 35 | var will_recharge_rate:float = 1 36 | var will_just_changed:bool 37 | 38 | 39 | ## Whether this agent is dead. 40 | var is_dead:bool: 41 | get: 42 | return vitals["health"] < 1 43 | ## Whether this agent is exhausted. 44 | var is_exhausted:bool: 45 | get: 46 | return vitals["moxie"] < 1 47 | ## Whether this agent is drained. 48 | var is_drained:bool: 49 | get: 50 | return vitals["will"] < 1 51 | var will_timer:Timer 52 | var tween:Tween 53 | 54 | 55 | func _init() -> void: 56 | name = "VitalsComponent" 57 | 58 | 59 | func _ready() -> void: 60 | will_timer = Timer.new() 61 | add_child(will_timer) 62 | will_timer.timeout.connect(do_return_to_will.bind()) 63 | will_timer.one_shot = true 64 | 65 | 66 | func set_health(val:float) -> void: 67 | vitals["health"] = clampf(val, 0.0, vitals["max_health"]) 68 | vitals_updated.emit(vitals) 69 | if is_dead: 70 | dies.emit() 71 | 72 | 73 | func change_health(val:float) -> void: 74 | set_health(vitals["health"] + val) 75 | 76 | 77 | func set_moxie(val:float) -> void: 78 | vitals["moxie"] = clampf(val, 0.0, vitals["max_moxie"]) 79 | vitals_updated.emit(vitals) 80 | moxie_just_changed = true 81 | if is_exhausted: 82 | exhausted.emit() 83 | 84 | 85 | func change_moxie(val:float) -> void: 86 | set_moxie(vitals["moxie"] + val) 87 | 88 | 89 | func set_will(val:float) -> void: 90 | vitals["will"] = clampf(val, 0.0, vitals["max_will"]) 91 | vitals_updated.emit(vitals) 92 | will_just_changed = true 93 | if is_drained: 94 | drained.emit() 95 | 96 | 97 | func cast_spell(cost:float) -> void: 98 | if DISHONORED_MODE: 99 | if tween: 100 | tween.kill() 101 | vitals.return_to_will = vitals["will"] 102 | will_just_changed = true 103 | will_timer.start(1.0) 104 | change_will(-cost) 105 | 106 | 107 | func do_return_to_will() -> void: 108 | if tween: 109 | tween.kill() 110 | tween = get_tree().create_tween() 111 | tween.tween_method(set_will.bind(), vitals.will, vitals.return_to_will, 1.0) 112 | tween.tween_callback(func(): 113 | will_just_changed = false) 114 | 115 | 116 | func change_will(val:float) -> void: 117 | set_will(vitals["will"] + val) 118 | 119 | 120 | func save() -> Dictionary: 121 | dirty = false 122 | return vitals 123 | 124 | 125 | func load_data(data:Dictionary): 126 | vitals = data 127 | dirty = false 128 | 129 | 130 | func _physics_process(delta: float) -> void: 131 | if not moxie_just_changed and not vitals.moxie == vitals.max_moxie: 132 | change_moxie(moxie_recharge_rate * delta) 133 | moxie_just_changed = false 134 | 135 | if not will_just_changed and not vitals.will == vitals.max_will: 136 | change_will(will_recharge_rate * delta) 137 | if not DISHONORED_MODE: 138 | will_just_changed = false 139 | 140 | 141 | func gather_debug_info() -> String: 142 | return """ 143 | [b]VitalsComponent[/b] 144 | Vitals: 145 | %s 146 | """ % [ 147 | JSON.stringify(vitals, '\t').indent("\t\t") 148 | ] 149 | -------------------------------------------------------------------------------- /scripts/constants.gd: -------------------------------------------------------------------------------- 1 | class_name SKConstants 2 | 3 | 4 | ## The defacto currency of this game. 5 | const DE_FACTO_CURRENCY = &"snails" 6 | -------------------------------------------------------------------------------- /scripts/covens/coven.gd: -------------------------------------------------------------------------------- 1 | class_name Coven 2 | extends Resource 3 | ## Analagous to a Faction in creation kit games, where a Coven is a group of Entities that behave a certain way. 4 | ## Entities must have a [CovensComponent] to be a part of a coven. 5 | ## Entities are automatically added to a group with the coven's ID when they are a part of a coven, so to get all entities part of a coven, you can get all of group. 6 | ## Unlike Creation Kit, Entties are assigned to a coven on the SKEntity side- the Coven just holds information. 7 | ## To give them a default response to the player, create a "Player" coven, and give them a default reaction to that. 8 | 9 | 10 | @export_category("Information") 11 | ## ID for this coven. Also used as a key in translations. See [member coven_name]. 12 | @export var coven_id:StringName 13 | ## The opinion this coven has of other covens. The dictionary shopuld be of StringName:int. 14 | @export var other_coven_opinions:Dictionary 15 | ## Whether the player should see this in the menu if they are a part of the coven. 16 | @export var hidden_from_player:bool 17 | ## The ranks of this coven. Shape is int:String, where key is the rank, and value is the translation key for the rank. 18 | @export var ranks:Dictionary 19 | @export_category("Crime") 20 | ## Whether members of this coven ignore crimes perpetrated to other members. 21 | @export var ignore_crimes_against_others:bool = false 22 | ## Whether members care abourt crimes done against their own members. 23 | @export var ignore_crimes_against_members:bool = false 24 | ## Whether this coven remembers crimes done against it. 25 | @export var track_crime:bool = true 26 | 27 | 28 | ## Translated coven name. 29 | var coven_name:String: 30 | get: 31 | return tr(coven_id) 32 | 33 | 34 | ## Get the translated name of a rank. 35 | func rank_name(rank:int) -> String: 36 | return tr(ranks[rank]) if ranks.has(rank) else "" 37 | 38 | 39 | ## Returns a list of the opinions it has of a list of covens. 40 | func get_coven_opinions(covens:Array) -> Array[int]: 41 | var opinion_list:Array[int] = [] 42 | 43 | for coven in covens: 44 | if other_coven_opinions.has(coven): 45 | opinion_list.append(other_coven_opinions[coven]) 46 | else: 47 | opinion_list.append(0) 48 | 49 | return opinion_list 50 | 51 | 52 | ## Get the crime opinion modifier for an entity against this coven. 53 | ## The formula is [code]max_crime_severity * -10[/code]. 54 | func get_crime_modifier(who:StringName) -> int: 55 | return CrimeMaster.max_crime_severity(who, coven_id) * -10 56 | 57 | 58 | func get_debug_info() -> String: 59 | return """ 60 | [b]%s[/b] 61 | Opinions: %s 62 | Hidden from player: %s 63 | Ranks: %s 64 | Ignores crimes against others: %s 65 | Ignores crimes against members: %s 66 | Track Crime: %s 67 | """ % [ 68 | coven_id, 69 | JSON.stringify(other_coven_opinions), 70 | hidden_from_player, 71 | JSON.stringify(ranks), 72 | ignore_crimes_against_others, 73 | ignore_crimes_against_members, 74 | track_crime 75 | ] 76 | -------------------------------------------------------------------------------- /scripts/covens/coven_rank_data.gd: -------------------------------------------------------------------------------- 1 | class_name CovenRankData 2 | extends Resource 3 | 4 | 5 | @export var coven:Coven 6 | @export var rank:int 7 | -------------------------------------------------------------------------------- /scripts/covens/coven_system.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | ## Tracks all [Coven]s in the game. 3 | 4 | 5 | var covens:Dictionary 6 | var regex:RegEx 7 | 8 | 9 | func _ready(): 10 | GameInfo.game_started.connect(func(): 11 | regex = RegEx.new() 12 | regex.compile("([^\\/\n\\r]+)\\.t?res") 13 | _cache_covens(ProjectSettings.get_setting("skelerealms/covens_path")) 14 | ) 15 | 16 | 17 | ## Gets a [Coven] if it exists. 18 | func get_coven(coven:StringName) -> Coven: 19 | return covens[coven] if covens.has(coven) else null 20 | 21 | 22 | ## Caches all covens in the project. 23 | func _cache_covens(path:String): 24 | var dir = DirAccess.open(path) 25 | if dir: 26 | dir.list_dir_begin() 27 | var file_name = dir.get_next() 28 | while file_name != "": 29 | if '.remap' in file_name: 30 | file_name = file_name.trim_suffix('.remap') 31 | if dir.current_is_dir(): # if is directory, cache subdirectory 32 | _cache_covens("%s/%s" % [path, file_name]) 33 | else: # if filename, cache 34 | var result = regex.search(file_name) 35 | if result: 36 | covens[result.get_string(1) as StringName] = load("%s/%s" % [path, file_name]) 37 | file_name = dir.get_next() 38 | dir.list_dir_end() 39 | else: 40 | print("An error occurred when trying to access the path.") 41 | 42 | 43 | ## Add coven to system. 44 | func add_coven(c:Coven) -> void: 45 | covens[c.coven_id] = c 46 | 47 | 48 | ## Remove coven from system. 49 | func remove_coven(id:StringName) -> void: 50 | covens.erase(id) 51 | 52 | 53 | ## Change the opinion a coven (of) has of another coven (what) by amount. 54 | func change_opinion(of:StringName, what:StringName, amount:int) -> void: 55 | var c = get_coven(of) 56 | if not c: 57 | return 58 | if c.other_coven_opinions.has(what): 59 | c.other_coven_opinions[what] = c.other_coven_opinions[what] + amount 60 | else: 61 | c.other_coven_opinions[what] = amount 62 | -------------------------------------------------------------------------------- /scripts/crime/crime.gd: -------------------------------------------------------------------------------- 1 | class_name Crime 2 | extends Resource 3 | ## Crime is a resource used to track crimes. 4 | 5 | 6 | ## Possible crime types and their severity. 7 | ## Edit to customize the types of crimes that can be committed. 8 | ## I guess I could do this in a config file (YAML?) but I dont want to do that right now. 9 | const CRIMES:Dictionary = { 10 | &"assault": 2, # Beating someone up 11 | &"theft": 1, # Stealing, pickpocketing 12 | &"murder": 5, # Killing someone 13 | &"tomfoolery":1 # Mischeif 14 | } 15 | 16 | ## Type of crime. See [constant CRIMES]. 17 | var crime_type:StringName 18 | var perpetrator:String 19 | var victim:String 20 | var witnesses:Array[StringName] = [] 21 | ## Severity of this crime 22 | var severity:int: 23 | get: 24 | return CRIMES[crime_type] if CRIMES.has(crime_type) else 0 25 | 26 | 27 | func _init(crime_type:StringName = &"", perpetrator:String = "", victim:String = "") -> void: 28 | self.crime_type = crime_type 29 | self.perpetrator = perpetrator 30 | self.victim = victim 31 | 32 | 33 | func serialize() -> Dictionary: 34 | return { 35 | "crime_type":crime_type, 36 | "perpetrator":perpetrator, 37 | "victim":victim, 38 | } 39 | 40 | 41 | func _to_string() -> String: 42 | return "Type: %s, Perp: %s, Victim: %s, Severity %s, Witnesses %s" % [crime_type, perpetrator, victim, severity, witnesses] 43 | -------------------------------------------------------------------------------- /scripts/crime/crime_master.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | ## OBEY THE CRIME MASTER[br] 3 | ## This keeps track of any crimes committed against various [Coven]s. 4 | 5 | 6 | ## Bounty amounts for various crime severity levels. 7 | const bounty_amount:Dictionary = { 8 | 0 : 0, 9 | 1 : 500, 10 | 2 : 10000, 11 | 5 : 100000, 12 | } 13 | 14 | 15 | ## Tracked crimes. 16 | ## [codeblock] 17 | ## { 18 | ## coven: { 19 | ## "punished" : [] 20 | ## "unpunished": [] 21 | ## } 22 | ## } 23 | ## [/codeblock] 24 | var crimes:Dictionary = {} 25 | ## This is a has set. All crimes reported will go into this set to be processed in the next frame. 26 | ## This is so that the same crime doesn't get reported over and over again. 27 | var crime_queue:Dictionary = {} 28 | signal crimes_against_covens_updated(affected:Array[StringName]) 29 | signal crime_committed(crime:Crime, position:NavPoint) 30 | 31 | 32 | func _ready(): 33 | add_to_group("savegame_gameinfo") 34 | 35 | 36 | ## Move all unpunished crimes to punished crimes. 37 | func punish_crimes(coven:StringName): 38 | crimes[coven]["punished"].append(crimes[coven]["unpunished"]) 39 | crimes[coven]["unpunished"].clear 40 | 41 | 42 | # TODO: Track crimes against others? 43 | ## Report a crime. The caller is also added as a witness. 44 | func add_crime(crime:Crime, witness:StringName): 45 | crime_queue[crime] = true 46 | crime.witnesses.append(witness) 47 | 48 | 49 | func _process(_delta: float) -> void: 50 | _process_crime_queue() 51 | 52 | 53 | func _process_crime_queue() -> void: 54 | if crime_queue.size() > 0: 55 | for crime in crime_queue: 56 | if crime.victim == "": 57 | continue 58 | # add crime to covens 59 | var cc = SKEntityManager.instance.get_entity(crime.victim).get_component("CovensComponent") 60 | if cc: 61 | for coven in (cc as CovensComponent).covens: 62 | ## Skip if doesn't track crime 63 | if not CovenSystem.get_coven(coven).track_crime: 64 | continue 65 | 66 | if crimes.has(coven): 67 | crimes[coven]["unpunished"].append(crime) 68 | else: # if coven doesnt have crimes against it, initialize table 69 | crimes[coven] = { 70 | "punished" : [], 71 | "unpunished" : [crime] 72 | } 73 | crimes_against_covens_updated.emit((cc as CovensComponent).covens) 74 | crime_queue.clear() 75 | 76 | 77 | ## Returns the max wanted level for crimes against a Coven. 78 | func max_crime_severity(id:StringName, coven:StringName) -> int: 79 | if not crimes.has(coven): 80 | return 0 81 | var cr = crimes[coven]["unpunished"]\ 82 | .filter(func(x:Crime): return x.perpetrator == id)\ 83 | .map(func(x:Crime): return x.severity) 84 | return 0 if cr.is_empty() else cr.max() 85 | 86 | 87 | ## Calculate the bounty a Coven has for an entity. 88 | func bounty_for_coven(id:StringName, coven:StringName) -> int: 89 | if not crimes.has(coven): 90 | return 0 91 | return crimes[coven]["unpunished"]\ 92 | .filter(func(x:Crime): return x.perpetrator == id)\ 93 | .reduce(func(sum:int, x:Crime): return sum + bounty_amount[x.severity], 0) 94 | 95 | 96 | func save() -> Dictionary: 97 | return { 98 | "crime" : crimes 99 | } 100 | 101 | 102 | func load_data(data:Dictionary) -> void: 103 | crimes = data["crime"] 104 | 105 | 106 | func reset_data() -> void: 107 | crimes = {} 108 | -------------------------------------------------------------------------------- /scripts/data/ItemDataComponents/apparel_data_component.gd: -------------------------------------------------------------------------------- 1 | class_name ApparelDataComponent 2 | extends ItemDataComponent 3 | ## For clothes. 4 | 5 | ## Whether this has a custom model. 6 | @export var modelled:bool 7 | ## Custom model, if using model. 8 | @export var prefab:PackedScene 9 | ## Material, if not using a model. 10 | @export var material:Material 11 | 12 | 13 | func get_type() -> String: 14 | return "ApparelDataComponent" 15 | -------------------------------------------------------------------------------- /scripts/data/ItemDataComponents/equippable_data_component.gd: -------------------------------------------------------------------------------- 1 | class_name EquippableDataComponent 2 | extends ItemDataComponent 3 | 4 | 5 | @export var valid_slots:Array[StringName] 6 | @export var override_texture: Texture2D 7 | @export var override_material: Material 8 | @export var override_model: PackedScene 9 | 10 | 11 | func get_type() -> String: 12 | return "EquippableDataComponent" 13 | -------------------------------------------------------------------------------- /scripts/data/ItemDataComponents/holdable_data_component.gd: -------------------------------------------------------------------------------- 1 | class_name HoldableDataComponent 2 | extends ItemDataComponent 3 | 4 | 5 | ## Whether this is held in both hands. 6 | @export var two_handed:bool 7 | 8 | 9 | func get_type() -> String: 10 | return "HoldableDataComponent" 11 | -------------------------------------------------------------------------------- /scripts/data/ItemDataComponents/item_data_component.gd: -------------------------------------------------------------------------------- 1 | class_name ItemDataComponent 2 | extends Node 3 | ## Base class for item data components that describe the capabilities of an item. See [ItemData]. [br] 4 | ## Items are a special case, in that they are built up of components. 5 | ## This may seem a bit weird and convoluted, and while it is, this allows for a much more extensible and flexible system. 6 | ## For example, you could give a shoe both the "Can equip to character" component, the "Holdable" component, and the "Throwable" component, 7 | ## and that would allow the character to wear the shoe, take it off, and huck it at somebody's head. 8 | 9 | 10 | ## Used for getting the component type. Override for each new type. 11 | func get_type() -> String: 12 | return "" 13 | -------------------------------------------------------------------------------- /scripts/data/ItemDataComponents/spell_data_component.gd: -------------------------------------------------------------------------------- 1 | class_name SpellDataComponent 2 | extends ItemDataComponent 3 | 4 | 5 | @export var spell:Spell 6 | 7 | 8 | func get_type() -> String: 9 | return "SpellDataComponent" 10 | -------------------------------------------------------------------------------- /scripts/data/ItemDataComponents/throwable_data_component.gd: -------------------------------------------------------------------------------- 1 | class_name ThrowableDataComponent 2 | extends ItemDataComponent 3 | 4 | 5 | func get_type() -> String: 6 | return "ThrowableDataComponent" 7 | -------------------------------------------------------------------------------- /scripts/entities/entity_component.gd: -------------------------------------------------------------------------------- 1 | class_name SKEntityComponent 2 | extends Node 3 | ## A component that is within an [SKEntity]. 4 | ## Extend these to add functionality to an entity. 5 | ## When inheriting, make sure to call super._ready() if overriding. 6 | 7 | 8 | ## Parent entity of this component. 9 | @onready var parent_entity:SKEntity = get_parent() as SKEntity 10 | ## Whether this component should be saved. 11 | var dirty:bool = false 12 | 13 | # Called when the node enters the scene tree for the first time. 14 | func _ready(): 15 | if Engine.is_editor_hint(): 16 | return 17 | 18 | parent_entity = get_parent() as SKEntity 19 | if not parent_entity.left_scene.is_connected(_on_exit_scene.bind()): 20 | parent_entity.left_scene.connect(_on_exit_scene.bind()) 21 | if not parent_entity.entered_scene.is_connected(_on_enter_scene.bind()): 22 | parent_entity.entered_scene.connect(_on_enter_scene.bind()) 23 | 24 | 25 | func _entity_ready() -> void: 26 | pass 27 | 28 | 29 | ## Called when the parent entity enters a scene. See [signal SKEntity.entered_scene]. 30 | func _on_enter_scene(): 31 | pass 32 | 33 | 34 | ## Called when the parent entity exits a scene. See [signal SKEntity.left_scene]. 35 | func _on_exit_scene(): 36 | pass 37 | 38 | 39 | ## Process a dialogue command given to the entity. 40 | func _try_dialogue_command(command:String, args:Array) -> void: 41 | pass 42 | 43 | 44 | ## Gather data to save. 45 | func save() -> Dictionary: 46 | return {} 47 | 48 | 49 | ## Load a data blob from the savegame system. 50 | func load_data(data:Dictionary): 51 | pass 52 | 53 | 54 | ## Gather and format any relevant info for a debug console or some other debugger. 55 | func gather_debug_info() -> String: 56 | return "" 57 | 58 | 59 | func _to_string() -> String: 60 | return gather_debug_info() 61 | 62 | 63 | ## Prints a rich text message to the console prepended with the entity name. Used for easier debugging. 64 | func printe(text:String, show_stack:bool = true) -> void: 65 | if parent_entity: 66 | parent_entity.printe(text, show_stack) 67 | else: 68 | (get_parent() as SKEntity).printe(text, show_stack) 69 | 70 | 71 | ## Get the dependencies for this node, for error warnings. Dependencies are the class name as a string. 72 | func get_dependencies() -> Array[String]: 73 | return [] 74 | 75 | 76 | ## Do any first-time setup needed for this component. For example, roll a loot table, randomize facial attributes, etc. 77 | func on_generate() -> void: 78 | pass 79 | 80 | 81 | func _get_configuration_warnings() -> PackedStringArray: 82 | var output := PackedStringArray() 83 | 84 | if not (get_parent() is SKEntity or get_parent() is SKElementGroup): 85 | output.push_back("Component should be the child of an SKEntity or an SKElementGroup.") 86 | 87 | for dep:String in get_dependencies(): 88 | if not get_parent().has_node(dep): 89 | output.push_back("This component needs %s" % dep) 90 | 91 | return output 92 | -------------------------------------------------------------------------------- /scripts/fsm/fsm_machine.gd: -------------------------------------------------------------------------------- 1 | class_name FSMMachine 2 | extends Node 3 | ## Finite State Machine manager. 4 | 5 | 6 | ## The entry node's name. 7 | var initial_state:String 8 | ## The current state of the machine. 9 | var state:FSMState 10 | 11 | 12 | ## Emit when it has made a transition. String is the new state name. 13 | signal transitioned(state_name:String) 14 | 15 | 16 | ## Set this FSM up with a list of state nodes. 17 | func setup(states:Array[FSMState]) -> void: 18 | # add all children 19 | for s in states: 20 | s.state_machine = self 21 | add_child(s) 22 | owner = get_parent() 23 | # call on ready 24 | for c in get_children(): 25 | c.owner = get_parent() 26 | (c as FSMState).on_ready() 27 | # transition to initial states 28 | transition(initial_state) 29 | 30 | 31 | func _process(delta: float) -> void: 32 | state.update(delta) 33 | 34 | 35 | ## Transition to a new state by state name. DOes nothing if no state with name found. 36 | func transition(state_name:String, msg:Dictionary = {}) -> void: 37 | #print("transitioning from %s state to %s" % [state.name if state else "None", state_name]) 38 | if not has_node(state_name): 39 | return 40 | 41 | if state: 42 | state.exit() 43 | 44 | state = get_node(state_name) 45 | state.enter(msg) 46 | 47 | transitioned.emit(state_name) 48 | -------------------------------------------------------------------------------- /scripts/fsm/fsm_state.gd: -------------------------------------------------------------------------------- 1 | class_name FSMState 2 | extends Node 3 | ## Abstract class for states for the [FSMMachine]. 4 | 5 | 6 | ## Parent state machine. 7 | var state_machine:FSMMachine = null 8 | 9 | 10 | func _init() -> void: 11 | name = _get_state_name() 12 | 13 | 14 | ## get this state node's name. Override to work properly. 15 | func _get_state_name() -> String: 16 | return "State" 17 | 18 | 19 | ## Called when the state machine finishes adding its nodes in [method FSMMachine.setup]. 20 | func on_ready() -> void: 21 | pass 22 | 23 | 24 | ## Same as _process(), but controlled by the machine. 25 | func update(delta:float) -> void: 26 | pass 27 | 28 | 29 | ## Called when the node is entered. Message can pass some data to this state. 30 | func enter(msg:Dictionary) -> void: 31 | pass 32 | 33 | 34 | ## Called when this node is exited. 35 | func exit() -> void: 36 | pass 37 | -------------------------------------------------------------------------------- /scripts/granular_navigation/nav_point.gd: -------------------------------------------------------------------------------- 1 | class_name NavPoint 2 | extends RefCounted 3 | ## Point in a world. 4 | 5 | 6 | var position:Vector3 7 | var world:String 8 | 9 | 10 | func _init(w:String, pos:Vector3) -> void: 11 | world = w 12 | position = pos 13 | -------------------------------------------------------------------------------- /scripts/granular_navigation/navigation_node.gd: -------------------------------------------------------------------------------- 1 | class_name NavNode 2 | extends Node3D 3 | ## A single navigation node in the granular navigation system. 4 | 5 | 6 | ## The connections/edges this node has to other nodes. [br] 7 | ## The structure of this dictionary is: [br] 8 | ## [Codeblock] 9 | ## connected_node:NavNode, cost:float 10 | ## [/Codeblock] 11 | var connections: Dictionary = {} 12 | var dimension:int 13 | var world:String 14 | var left_child:NavNode 15 | var right_child:NavNode 16 | var nav_point:NavPoint: 17 | get: 18 | return NavPoint.new(world, position) 19 | 20 | 21 | # TODO: Figure out connections 22 | func add_nav_node(pos:Vector3) -> NavNode: 23 | # figure out if the dimension is less or greater than ourselves. 24 | # equal is treated as greater. 25 | var is_left:bool = pos[dimension] < position[dimension] 26 | if is_left: 27 | # if our left child exists, tell it to add the node. 28 | if left_child: 29 | return left_child.add_nav_node(pos) 30 | else: 31 | var new_n = NavNode.new() 32 | new_n.position = pos # set position 33 | new_n.dimension = (dimension + 1) % 3 # set dimension and wrap to 3 dimensions 34 | new_n.world = world 35 | new_n.name = NavMaster.format_point_name(pos, world) 36 | add_child(new_n) 37 | left_child = new_n 38 | return new_n 39 | else: 40 | if right_child: 41 | return right_child.add_nav_node(pos) 42 | else: 43 | var new_n = NavNode.new() 44 | new_n.position = pos 45 | new_n.dimension = (dimension + 1) % 3 46 | new_n.world = world 47 | new_n.name = NavMaster.format_point_name(pos, world) 48 | add_child(new_n) 49 | right_child = new_n 50 | return new_n 51 | 52 | 53 | func get_closest_point(pos:Vector3) -> NavNode: 54 | var is_left:bool = pos[get_parent().dimension] < position[get_parent().dimension] 55 | 56 | if is_left: 57 | if left_child: # if we have a left child, call it instead, 58 | return left_child.get_closest_point(pos) 59 | else: # else it's this 60 | return self 61 | else: 62 | if right_child: 63 | return right_child.get_closest_point(pos) 64 | else: 65 | return self 66 | 67 | 68 | func connect_nodes(other:NavNode, cost:float) -> void: 69 | connections[other] = cost 70 | -------------------------------------------------------------------------------- /scripts/granular_navigation/navigation_world.gd: -------------------------------------------------------------------------------- 1 | class_name NavWorld 2 | extends Node 3 | ## A world of the granular navigation system. [br] 4 | 5 | 6 | const dimension = 0 7 | 8 | @export var world:String 9 | 10 | 11 | func add_point(pos:Vector3) -> NavNode: 12 | # if we have no childrenm, add one 13 | if get_child_count() == 0: 14 | var new_n = NavNode.new() 15 | new_n.position = pos # set position 16 | new_n.dimension = 0 17 | new_n.world = world 18 | new_n.name = NavMaster.format_point_name(pos, world) 19 | add_child(new_n) 20 | return new_n 21 | #else, tell that child to add one 22 | return (get_child(0) as NavNode).add_nav_node(pos) 23 | 24 | 25 | ## Gets closest point in world to a position. 26 | func get_closest_point(pos:Vector3) -> NavNode: 27 | if get_child_count() == 0: 28 | return null 29 | else: 30 | return (get_child(0) as NavNode).get_closest_point(pos) 31 | -------------------------------------------------------------------------------- /scripts/instance_data/door_instance.gd: -------------------------------------------------------------------------------- 1 | class_name DoorInstance 2 | extends Resource 3 | 4 | 5 | @export var world:StringName 6 | @export var position:Vector3 7 | @export var rotation:Vector3 8 | -------------------------------------------------------------------------------- /scripts/loottable/items/lt_item.gd: -------------------------------------------------------------------------------- 1 | class_name SKLTItem 2 | extends SKLootTableItem 3 | 4 | 5 | @export var data:PackedScene 6 | 7 | 8 | func resolve() -> SKLootTable.LootTableResult: 9 | return SKLootTable.LootTableResult.new([data], {}) 10 | -------------------------------------------------------------------------------- /scripts/loottable/items/lt_item_entity.gd: -------------------------------------------------------------------------------- 1 | class_name SKLTItemEntity 2 | extends SKLootTableItem 3 | 4 | 5 | ## The unique entity to put in the inventory. 6 | @export var item:PackedScene 7 | 8 | 9 | func resolve() -> SKLootTable.LootTableResult: 10 | var id:StringName = item._bundled.names[0] 11 | return SKLootTable.LootTableResult.new([], {}, [id]) 12 | -------------------------------------------------------------------------------- /scripts/loottable/items/lt_itemchance.gd: -------------------------------------------------------------------------------- 1 | class_name SKLTItemChance 2 | extends SKLootTableItem 3 | 4 | 5 | @export var item:PackedScene 6 | @export_range(0.0, 1.0) var chance:float = 1.0 7 | 8 | 9 | func resolve() -> SKLootTable.LootTableResult: 10 | if randf() > chance: 11 | return SKLootTable.LootTableResult.new([item], {}) 12 | else: 13 | return SKLootTable.LootTableResult.new() 14 | -------------------------------------------------------------------------------- /scripts/loottable/items/lt_itementry.gd: -------------------------------------------------------------------------------- 1 | class_name SKLTItemEntry 2 | extends SKLootTableItem 3 | 4 | 5 | @export var item:PackedScene 6 | 7 | 8 | func resolve() -> SKLootTable.LootTableResult: 9 | return SKLootTable.LootTableResult.new([item], {}) 10 | -------------------------------------------------------------------------------- /scripts/loottable/items/lt_loottablecurrency.gd: -------------------------------------------------------------------------------- 1 | class_name SKLTCurrency 2 | extends SKLootTableItem 3 | 4 | 5 | @export var currency:StringName = &"" 6 | @export_range(0, 100, 1, "or_greater") var amount_min:int = 0 7 | @export_range(0, 100, 1, "or_greater") var amount_max:int = 10 8 | 9 | 10 | func resolve() -> SKLootTable.LootTableResult: 11 | return SKLootTable.LootTableResult.new([], {currency: amount_min if amount_max <= amount_min else randi_range(amount_min, amount_max)}) 12 | -------------------------------------------------------------------------------- /scripts/loottable/items/lt_on_condition.gd: -------------------------------------------------------------------------------- 1 | class_name SKLTOnCondition 2 | extends SKLootTableItem 3 | 4 | 5 | @export_multiline var condition:String = "" 6 | var items:SKLootTable 7 | 8 | 9 | func _ready() -> void: 10 | items = get_child(0) 11 | 12 | 13 | func resolve() -> SKLootTable.LootTableResult: 14 | if not _check_condition(): 15 | return SKLootTable.LootTableResult.new() 16 | 17 | var o:SKLootTable.LootTableResult = SKLootTable.LootTableResult.new() 18 | for i:SKLootTableItem in items.items: 19 | o.concat(i.resolve()) 20 | return o 21 | 22 | 23 | func _check_condition() -> bool: 24 | if condition == "": 25 | return false 26 | 27 | var e:Expression = Expression.new() 28 | 29 | var err:int = e.parse(condition) 30 | if not err == 0: 31 | print("Loot table script error: %s" % e.get_error_text()) 32 | return false 33 | 34 | var res = e.execute() 35 | 36 | if e.has_execute_failed(): 37 | print("Loot table script execution failed.") 38 | return false 39 | if res == null: 40 | return false 41 | if res is bool: 42 | return res 43 | else: 44 | print("Loot table script warning: Expression should return boolean value.") 45 | return true 46 | -------------------------------------------------------------------------------- /scripts/loottable/items/lt_xofitems.gd: -------------------------------------------------------------------------------- 1 | class_name SKLTXOfItem 2 | extends SKLootTableItem 3 | 4 | 5 | @export_range(0, 100, 1, "or_greater") var x_min:int = 1 6 | @export_range(0, 100, 1, "or_greater") var x_max:int = 0 7 | var items: SKLootTable 8 | 9 | 10 | func _ready() -> void: 11 | items = get_child(0) 12 | 13 | 14 | func resolve() -> SKLootTable.LootTableResult: 15 | var x = randi_range(x_min, x_min if x_max <= x_min else x_max) 16 | if items.size() == 0 or x == 0: 17 | return SKLootTable.LootTableResult.new() 18 | 19 | var output:SKLootTable.LootTableResult = SKLootTable.LootTableResult.new() 20 | var i:int = 0 21 | while output.size() < x: 22 | output.concat(items.items[i].resolve()) 23 | i += 1 24 | if i >= items.size(): 25 | i = 0 26 | output.items = output.items.slice(0, x) 27 | return output 28 | -------------------------------------------------------------------------------- /scripts/loottable/skloottable.gd: -------------------------------------------------------------------------------- 1 | class_name SKLootTable 2 | extends Node 3 | 4 | 5 | ## This is a loot table. It can resolve into a collection of items and currencies. 6 | 7 | 8 | var items:Array[SKLootTableItem] = [] 9 | 10 | 11 | func _ready() -> void: 12 | items.resize(get_child_count()) 13 | for c:Node in get_children(): 14 | items.append(c) 15 | 16 | 17 | ## Generate all members of the loot table. Returns a dictionary shaped like {&"items":Array[ItemData], &"currencies":{name:amount,...}} 18 | func resolve() -> Dictionary: 19 | var output:LootTableResult = LootTableResult.new() 20 | for i:SKLootTableItem in items: 21 | output.concat(i.resolve()) 22 | return output.to_dict() 23 | 24 | 25 | class LootTableResult: 26 | extends RefCounted 27 | 28 | 29 | var items: Array[PackedScene] = [] 30 | var currencies: Dictionary = {} 31 | var entities: Array[StringName] = [] 32 | 33 | 34 | func _init(i:Array[PackedScene] = [], c:Dictionary = {}, e:Array[StringName] = []) -> void: 35 | items = i 36 | currencies = c 37 | entities = e 38 | 39 | 40 | func concat(other:LootTableResult) -> void: 41 | items.append_array(other.items) 42 | for c:StringName in other.currencies: 43 | if currencies.has(c): 44 | currencies[c] += other.currencies[c] 45 | else: 46 | currencies[c] = other.currencies[c] 47 | for id:StringName in other.entities: 48 | if not entities.has(id): 49 | entities.append(id) 50 | 51 | 52 | func to_dict() -> Dictionary: 53 | return { 54 | &"items": items, 55 | &"currencies": currencies, 56 | &"entities": entities, 57 | } 58 | -------------------------------------------------------------------------------- /scripts/loottable/skloottableitem.gd: -------------------------------------------------------------------------------- 1 | class_name SKLootTableItem 2 | extends Node 3 | 4 | 5 | func resolve() -> SKLootTable.LootTableResult: 6 | return SKLootTable.LootTableResult.new() 7 | -------------------------------------------------------------------------------- /scripts/misc/animation_controller.gd: -------------------------------------------------------------------------------- 1 | class_name AnimationController 2 | extends Node 3 | 4 | ## This is a basic abstraction layer for animations. Use of this is purely optional. 5 | ## Place this node just under a puppet node, and connect various other nodes meant to control different aspects of animation somewhere beneath it in the tree, 6 | ## And have them subscribe to the signals here to receive animation events from the puppet. 7 | 8 | 9 | signal value_set(key:StringName, value:Variant) 10 | signal triggered(key:StringName) 11 | signal swapped(now_true:StringName, now_false:StringName) 12 | 13 | 14 | var root_motion_callback:Callable = func(): return Vector3(0,0,0) 15 | var root_rotation_callback:Callable = func(): return Vector3(0,0,0) 16 | var root_scale_callback:Callable = func(): return Vector3(0,0,0) 17 | 18 | 19 | func set_value(key:StringName, value:Variant) -> void: 20 | value_set.emit(key, value) 21 | 22 | 23 | func trigger(key:StringName) -> void: 24 | triggered.emit(key) 25 | 26 | 27 | func swap(now_true:StringName, now_false:StringName) -> void: 28 | swapped.emit(now_true, now_false) 29 | 30 | 31 | ## Use this to find the controller in the tree. 32 | static func get_animator(n: Node) -> AnimationController: 33 | if n.get_parent() == null: 34 | return null 35 | 36 | for x in n.get_parent().get_children(): 37 | if x is AnimationController: 38 | return x 39 | 40 | return get_animator(n.get_parent()) 41 | -------------------------------------------------------------------------------- /scripts/misc/audio_emitter.gd: -------------------------------------------------------------------------------- 1 | class_name AudioEventEmitter 2 | extends Node3D 3 | ## Used to emit sounds that should have an effect on other things, like alerting NPCs. 4 | ## Put this beneath some sort of audio emitter, and link signals. 5 | 6 | 7 | @export var ignore_self:bool = false 8 | 9 | 10 | ## Finds every node of group "audio_listener" in a radius of unit_size using a physics shape cast, and attempts to call method "heard_audio" on it, passing self as an argument. 11 | func send_play_event(range:float): 12 | # TODO: Make it less physics based? 13 | var space_state = get_world_3d().direct_space_state # get space state 14 | # create query 15 | var query = PhysicsShapeQueryParameters3D.new() 16 | query.shape = SphereShape3D.new() 17 | # create query position 18 | var t = Transform3D() 19 | t.origin = position 20 | t.scaled(Vector3(range, range, range)) # scale to match radius 21 | query.transform = t 22 | # make query 23 | var res = space_state.intersect_shape(query) 24 | # if ignoring self, filter out all nodes part of this tree 25 | if ignore_self: 26 | res = res.filter(func(x:Dictionary): 27 | return not (x["collider"] as Node).is_ancestor_of(self) and not (x["collider"] as Node).find_child(self.name) 28 | ) 29 | res.filter(func(x:Node): return x.is_in_group("audio_listener")) 30 | # return results, where all colliders are selected from it. 31 | for n in res.map(func(x:Dictionary): return x["collider"] as Node)\ 32 | .filter(func(x:Node): return x.has_method("heard_audio")): 33 | n.heard_audio(self) 34 | -------------------------------------------------------------------------------- /scripts/misc/damage_info.gd: -------------------------------------------------------------------------------- 1 | class_name DamageInfo 2 | extends RefCounted 3 | ## The effects of a damage event. 4 | ## Read the tutorial for how to use these. 5 | ## @tutorial(Damage Effects): https://github.com/SlashScreen/skelerealms/wiki/Damage-Effects 6 | 7 | 8 | ## Who caused the damage? 9 | var offender:String 10 | ## The different kinds of damage. 11 | var damage_effects:Dictionary 12 | ## Optional spell effects. 13 | var spell_effects:Array[StringName] = [] 14 | ## Optional extra info. 15 | var info:Dictionary = {} 16 | 17 | 18 | func _init(offender:String, damage_effects:Dictionary, spell_effects:Array[StringName] = [], info:Dictionary = {}) -> void: 19 | self.offender = offender 20 | self.damage_effects = damage_effects 21 | self.spell_effects = spell_effects 22 | self.info = info 23 | -------------------------------------------------------------------------------- /scripts/misc/device_emitter.gd: -------------------------------------------------------------------------------- 1 | class_name DeviceEmitter 2 | extends Node 3 | 4 | 5 | @export var device_name:StringName 6 | 7 | 8 | func emit_state(state:Variant) -> void: 9 | DeviceNetwork.update_device_state(device_name, state) 10 | -------------------------------------------------------------------------------- /scripts/misc/device_listener.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | 4 | @export var device_name:StringName 5 | -------------------------------------------------------------------------------- /scripts/misc/device_network.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | ## Singleton used for coordinating dungeon puzzle elements. 3 | 4 | 5 | ## Signal called when a device is updated. See [method update_device_state]. 6 | signal device_state_changed(device:StringName, value:Variant) 7 | 8 | 9 | ## Signal a device being updated. See [signal device_state_changed]. 10 | func update_device_state(device:StringName, value:Variant) -> void: 11 | device_state_changed.emit(device, value) 12 | -------------------------------------------------------------------------------- /scripts/misc/element_group.gd: -------------------------------------------------------------------------------- 1 | class_name SKElementGroup 2 | extends Node 3 | 4 | 5 | func _enter_tree() -> void: 6 | while get_child_count() > 0: 7 | get_child(0).reparent(get_parent()) 8 | queue_free() 9 | -------------------------------------------------------------------------------- /scripts/misc/hit_detector.gd: -------------------------------------------------------------------------------- 1 | class_name HitDetector 2 | extends Area3D 3 | ## Hit detector used for melee weapons. 4 | 5 | 6 | var active:bool ## Whether this should listen for collisions 7 | var collision_callback:Callable ## A callable this should use to pass information back. 8 | 9 | 10 | func _ready() -> void: 11 | body_entered.connect(func(body:Node3D) -> void: 12 | if active: 13 | collision_callback.call(body) 14 | ) 15 | 16 | 17 | func activate(cback:Callable) -> void: 18 | collision_callback = cback 19 | active = true 20 | 21 | 22 | func deactivate() -> void: 23 | active = false 24 | -------------------------------------------------------------------------------- /scripts/misc/id_generator.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name SKIDGenerator 3 | extends RefCounted 4 | 5 | 6 | const CHARACTERS: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" 7 | 8 | 9 | static func generate_id(length:int = 10) -> String: 10 | var output := "" 11 | 12 | for _i:int in length: 13 | output += CHARACTERS[randi_range(0, CHARACTERS.length() - 1)] 14 | 15 | return output 16 | -------------------------------------------------------------------------------- /scripts/misc/npc_template_option.gd: -------------------------------------------------------------------------------- 1 | class_name NPCTemplateOption 2 | extends Resource 3 | 4 | 5 | @export var template:PackedScene 6 | @export_range(0, 1) var chance:float = 1 7 | 8 | 9 | func resolve() -> bool: 10 | return randf() <= chance 11 | -------------------------------------------------------------------------------- /scripts/misc/option.gd: -------------------------------------------------------------------------------- 1 | class_name Option 2 | extends RefCounted 3 | ## A crude implementation of an option/maybe type. 4 | ## May get rid of this, because it adds complexity to replicate features that nullability kind of already does... 5 | 6 | 7 | var _data:Variant 8 | 9 | 10 | ## Make a new option containing data. 11 | static func from(d:Variant) -> Option: 12 | var op: Option = Option.new() 13 | op._data = d 14 | return op 15 | 16 | 17 | ## Make a new Option containing nothing. 18 | static func none() -> Option: 19 | var op: Option = Option.new() 20 | op._data = null 21 | return op 22 | 23 | 24 | ## Wrap any value as an option. If it's null, it's none. 25 | static func wrap(data:Variant) -> Option: 26 | if data: 27 | return from(data) 28 | else: 29 | return none() 30 | 31 | 32 | ## Whether it has something in it. 33 | func some() -> bool: 34 | return _data != null 35 | 36 | 37 | ## Get the data from within. May be null. 38 | func unwrap() -> Variant: 39 | return _data 40 | 41 | 42 | ## Call a function on this option if it contains a value. The argument is the unwrapped contents. 43 | func bind(fn:Callable) -> Variant: 44 | if some(): 45 | return fn.call(unwrap()) 46 | else: 47 | return Option.none() 48 | 49 | 50 | ## Return a specified value if the option is none. 51 | func orelse(v:Variant) -> Variant: 52 | if some(): 53 | return _data 54 | else: 55 | return v 56 | -------------------------------------------------------------------------------- /scripts/misc/skconfig.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name SKConfig 3 | extends Resource 4 | 5 | 6 | ## This resource is needed to configure some Skelerealms behavior without changing code in the addon scripts itself. 7 | ## This should be given to an [class SKEntityManager] to be used. 8 | 9 | 10 | ## The game's root. Contains the entity manager and world loader. 11 | @export var game_root:PackedScene 12 | ## The default world the game root loads when none other is specified. 13 | @export var default_world:String 14 | ## The default position for the player when the game first starts. 15 | @export var default_world_position:Vector3 16 | ## The equipment slots available to the equipment. 17 | @export var equipment_slots:Array[StringName] 18 | ## Default skills for [class SkillsComponent]s. 19 | @export var skills:Dictionary = {} 20 | ## Default attributes for [class AttributesComponent]s. 21 | @export var attributes:Dictionary = {} 22 | ## Status effects that will be registered when the game starts. 23 | @export var status_effects:Array[StatusEffect] = [] 24 | ## The formula for determining the amount of XP needed for a skill to level up, in GDScript. The given skill level is the current skill level, 25 | ## and the formula's result (int) is the XP needed to raise to the next level. 26 | ## Inputs: skill_level (int) 27 | ## Outputs: int 28 | @export_multiline var skill_xp_formula:String 29 | ## The formula for determining the amount of XP needed for a character to level up, in GDScript. The given character level is the current character level, 30 | ## and the formula's result (int) is the XP needed to raise to the next level 31 | ## Inputs: character_level (int) 32 | ## Outputs: int 33 | @export_multiline var character_xp_formula:String 34 | ## The compiled skill xp check expression. 35 | var compiled_skill:Expression 36 | ## The compiles character xp check expression. 37 | var compiled_character:Expression 38 | 39 | 40 | func compile() -> void: 41 | compiled_skill = Expression.new() 42 | var err:Error = compiled_skill.parse(skill_xp_formula, PackedStringArray(["skill_level"])) 43 | if not err == 0: 44 | push_error("Skill level expression compilation failed: ", compiled_skill.get_error_text(), " - Check your SKConfig resource.") 45 | return 46 | 47 | compiled_character = Expression.new() 48 | err = compiled_character.parse(character_xp_formula, PackedStringArray(["character_level"])) 49 | if not err == 0: 50 | push_error("Character level expression compilation failed: ", compiled_character.get_error_text(), " - Check your SKConfig resource.") 51 | 52 | 53 | ## Compute the skill xp needed to level up. 54 | func compute_skill(level: int) -> int: 55 | var res:Variant = compiled_skill.execute([level]) 56 | if compiled_skill.has_execute_failed(): 57 | push_error("Skill level expression execution failed: ", compiled_skill.get_error_text(), " - Check your SKConfig resource.") 58 | return -1 59 | if not res is int: 60 | push_error("Skill level expression did not return an integer.") 61 | return -1 62 | return res as int 63 | 64 | 65 | ## Compute the character xp needed to level up. 66 | func compute_character(level: int) -> int: 67 | var res:Variant = compiled_character.execute([level]) 68 | if compiled_character.has_execute_failed(): 69 | push_error("Character level expression execution failed: ", compiled_character.get_error_text(), " - Check your SKConfig resource.") 70 | return -1 71 | if not res is int: 72 | push_error("Character level expression did not return an integer.") 73 | return -1 74 | return res as int 75 | -------------------------------------------------------------------------------- /scripts/misc/skelesave.gd: -------------------------------------------------------------------------------- 1 | class_name Skelesave 2 | 3 | 4 | const CLASS_LOOKUP = [ 5 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 7 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 10 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 13 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 14 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 15 | 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 16 | 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 17 | 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 18 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 19 | 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 7, 20 | 9, 10, 10, 10, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 21 | ] 22 | const TRANSITION_LOOKUP = [ 23 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24 | 0, 1, 0, 0, 0, 2, 3, 5, 4, 6, 7, 8, 25 | 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 26 | 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 27 | 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 28 | 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 29 | 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 30 | 0, 0, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 31 | 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0 32 | ] 33 | const ARR_DELIM = 0xFF 34 | const NULL_DELIM = 0xFE 35 | const VALUE_DELIM = 0xFD 36 | const KEY_DELIM = 0xFC 37 | const ARR_START = 0xFB 38 | 39 | 40 | static func is_valid_utf8(bytes:PackedByteArray) -> bool: 41 | var last_state:int = 1 42 | for byte:int in bytes: 43 | var current_byte_class:int = CLASS_LOOKUP[byte] 44 | var new_lookup_index:int = last_state * 12 + current_byte_class 45 | last_state = TRANSITION_LOOKUP[new_lookup_index] 46 | if last_state == 0: 47 | return false 48 | return last_state == 1 49 | 50 | 51 | static func serialize(data:Dictionary) -> PackedByteArray: 52 | var output:PackedByteArray = PackedByteArray() 53 | for d:Variant in data: 54 | output.append_array(_stringify_value(d)) 55 | output.append(KEY_DELIM) 56 | output.append_array(_stringify_value(data[d])) 57 | output.append(VALUE_DELIM) 58 | return output 59 | 60 | 61 | static func _stringify_value(data:Variant) -> PackedByteArray: 62 | if data == null: 63 | return PackedByteArray([NULL_DELIM]) 64 | elif data is Dictionary: 65 | return serialize(data) 66 | elif data is bool: 67 | return ("true" if data else "false").to_utf8_buffer() 68 | elif data is int: 69 | return ("%d" % data).to_utf8_buffer() 70 | elif data is float: 71 | return ("%f" % data).to_utf8_buffer() 72 | elif data is Array: 73 | var output:PackedByteArray = PackedByteArray() 74 | for i:Variant in data: 75 | output.append_array(_stringify_value(i)) 76 | output.append(ARR_DELIM) 77 | return output 78 | else: 79 | return (data as Object).to_string().to_utf8_buffer() 80 | 81 | 82 | static func deserialize(data:PackedByteArray) -> Dictionary: 83 | var output:Dictionary = {} 84 | var pos:int = 0 85 | var current_phrase:PackedByteArray = PackedByteArray() 86 | var current_array:Array = [] 87 | var current_key:String = "" 88 | var current_value:Variant = null 89 | 90 | while pos < data.size(): 91 | match data[pos]: 92 | KEY_DELIM: 93 | current_key = current_phrase.get_string_from_utf8() 94 | current_phrase.clear() 95 | VALUE_DELIM: 96 | current_value = _decode_value(current_phrase) 97 | current_phrase.clear() 98 | _: 99 | current_phrase.append(data[pos]) 100 | return output 101 | 102 | 103 | static func _decode_value(data:PackedByteArray) -> Variant: 104 | if data[0] == NULL_DELIM: 105 | return null 106 | if data.has(KEY_DELIM): 107 | return deserialize(data) 108 | if data.has(ARR_DELIM): 109 | var output = [] 110 | var current_member:PackedByteArray = PackedByteArray() 111 | # TODO: array 112 | for i:int in range(data.size()): 113 | match data[i]: 114 | ARR_DELIM: 115 | output.append(_decode_value(current_member)) 116 | current_member.clear() 117 | _: 118 | current_member.append(data[i]) 119 | return output # TODO 120 | var stringified:String = data.get_string_from_utf8() 121 | if stringified == "true": 122 | return true 123 | if stringified == "false": 124 | return false 125 | if stringified.is_valid_int(): 126 | return stringified.to_int() 127 | if stringified.is_valid_float(): 128 | return stringified.to_float() 129 | return stringified 130 | -------------------------------------------------------------------------------- /scripts/misc/status_effect.gd: -------------------------------------------------------------------------------- 1 | class_name StatusEffect 2 | extends Resource 3 | 4 | 5 | ## Base class for all status effects. 6 | 7 | 8 | ## The name of this effect. 9 | @export var name:StringName 10 | ## The tags this status effect has. 11 | @export var tags:Array[StringName] = [] 12 | ## Status effects that this status effect will remove when applied to an entity. 13 | ## For example, the "wet" effect will negate the "burning" effect. 14 | @export var negates:Array[StringName] = [] 15 | ## Status effects with these tags will be removed when this status effect is applied. 16 | ## For example, the "muddy" and "slimy" effects may have a "dirty" tag. The "wet" effect 17 | ## would remove the "dirty" tag. 18 | @export var negates_tags:Array[StringName] = [] 19 | ## If an entity has an effect on this list, this effect will not be applied. 20 | @export var incompatible:Array[StringName] = [] 21 | ## If there are any effects with this tag on the entity, this status effect will not be applied. 22 | @export var imcompatible_tags:Array[StringName] = [] 23 | 24 | 25 | ## Called every frame as the effect is active. 26 | func on_update(delta:float, target: StatusEffectHost) -> void: 27 | pass 28 | 29 | 30 | ## Called when the effect first begins. 31 | func on_start_effect(target: StatusEffectHost) -> void: 32 | pass 33 | 34 | 35 | ## Called when the effect ends. 36 | func on_end_effect(target: StatusEffectHost) -> void: 37 | pass 38 | -------------------------------------------------------------------------------- /scripts/misc/status_effect_host.gd: -------------------------------------------------------------------------------- 1 | class_name StatusEffectHost 2 | extends Node 3 | 4 | 5 | ## This class holds and processes [class StatusEffect]s - Updating them, resolivng tag comflicts, so on. It can work by itself, but it's intended to be the child 6 | ## of some kind of "vessel", which can handle [signal message_broadcast] to carry out the will of status effects. That sounds philosophical, but it isn't (unless you want it to be). 7 | 8 | 9 | ## The effects applied to this host. Shape is {StringName:[class StatusEffect]}. 10 | var effects:Dictionary = {} 11 | ## The effects are also organized by tag, for optimization purposes. The shape is {StringName:Array\[[class StatusEffect]\]}. 12 | var tag_map:Dictionary = {} 13 | ## This signal is listened to by a host's vessel (by default, [class EffectsComponent] and [class EffectsObject]), which will relay the message to other nodes. 14 | ## THis is called from the effects if they want to make changes to the object they are attached to. 15 | signal message_broadcast(what:StringName, args:Array) 16 | 17 | 18 | func _process(delta: float) -> void: 19 | for e:StatusEffect in effects.values(): 20 | e.on_update(delta, self) 21 | 22 | 23 | ## Add an effect to this host. It will scan the registered effects (See [member SkeleRealmsGlobal.status_effects]) and add the registered effect. 24 | ## It will also resolve all tag conflicts as well. If it cannot add the effect due to a tag conflict, or if it already has that effect, it will silently fail. If no effect is found in the database, 25 | ## it will push an error. 26 | func add_effect(what:StringName) -> void: 27 | # check if has already 28 | if effects.has(what): 29 | return 30 | 31 | if SkeleRealmsGlobal.status_effects.has(what): 32 | var ne:StatusEffect = (SkeleRealmsGlobal.status_effects[what] as StatusEffect).duplicate() 33 | # Check incompatibilities 34 | for x:StringName in ne.incompatible: 35 | if effects.has(x): 36 | return 37 | for x:StringName in ne.incompatible_tags: 38 | if tag_map.has(x) and not tag_map[x].is_empty(): 39 | return 40 | # Check negates 41 | for x:StringName in ne.negates: 42 | if effects.has(x): 43 | remove_effect(x) 44 | for x:StringName in ne.negates_tags: 45 | var to_remove:Array = tag_map[x].map(func(e:StatusEffect) -> StringName: return e.name) 46 | for i:StringName in to_remove: 47 | remove_effect(i) 48 | # Add effect 49 | effects[what] = ne 50 | ne.on_start_effect(self) 51 | else: 52 | push_error("No status effect \"%s\" registered." % what) 53 | 54 | 55 | ## Remove an effect by its name. If there is no effect by this name, it will silently fail. 56 | func remove_effect(e:StringName) -> void: 57 | if not effects.has(e): 58 | return 59 | var to_remove:StatusEffect = effects[e] 60 | to_remove.on_end_effect(self) 61 | for t:StringName in to_remove.tags: 62 | tag_map[t].erase(to_remove) 63 | effects.erase(e) 64 | 65 | 66 | ## Used by status effects to broadcast messages to a hosts' node tree. 67 | ## For example, broadcasting "damage" from a host attached to an entity could cause it to be damaged, 68 | ## if it has a [class DamageableComponent]. 69 | func send_message(what:StringName, args:Array) -> void: 70 | message_broadcast.emit(what, args) 71 | 72 | 73 | func has_effect_with_tag(tag:StringName) -> bool: 74 | if not effects.has(tag): 75 | return false 76 | if effects[tag].is_empty(): 77 | return false 78 | return true 79 | 80 | 81 | func has_effect(effect:StringName) -> bool: 82 | return effects.has(effect) 83 | -------------------------------------------------------------------------------- /scripts/network/Editor/cost_popup.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ConfirmationDialog 3 | 4 | 5 | signal popup_accepted(text:String) 6 | 7 | 8 | func _ready() -> void: 9 | get_ok_button().button_up.connect(_accept.bind()) 10 | ($LineEdit as LineEdit).text_changed.connect(_check_text.bind()) 11 | 12 | 13 | func _accept() -> void: 14 | popup_accepted.emit(($LineEdit as LineEdit).text) 15 | 16 | 17 | func _check_text(new_text:String) -> void: 18 | get_ok_button().disabled = not new_text.is_valid_float() 19 | -------------------------------------------------------------------------------- /scripts/network/Editor/editor_toolbar.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://rgxfrjvg7gox"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/network/Editor/network_editor_utility.gd" id="1_yifem"] 4 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/network/Editor/cost_popup.gd" id="2_3dol8"] 5 | 6 | [node name="Control" type="Control"] 7 | layout_mode = 3 8 | anchors_preset = 15 9 | anchor_right = 1.0 10 | anchor_bottom = 1.0 11 | grow_horizontal = 2 12 | grow_vertical = 2 13 | script = ExtResource("1_yifem") 14 | 15 | [node name="Box" type="HBoxContainer" parent="."] 16 | layout_mode = 0 17 | offset_right = 305.0 18 | offset_bottom = 31.0 19 | 20 | [node name="Add" type="Button" parent="Box"] 21 | layout_mode = 2 22 | toggle_mode = true 23 | text = "Add" 24 | flat = true 25 | 26 | [node name="Remove" type="Button" parent="Box"] 27 | layout_mode = 2 28 | text = "Remove" 29 | flat = true 30 | 31 | [node name="Link" type="Button" parent="Box"] 32 | layout_mode = 2 33 | text = "Link" 34 | flat = true 35 | 36 | [node name="Unlink" type="Button" parent="Box"] 37 | layout_mode = 2 38 | text = "Unlink" 39 | flat = true 40 | 41 | [node name="Merge" type="Button" parent="Box"] 42 | layout_mode = 2 43 | text = "Merge" 44 | flat = true 45 | 46 | [node name="Dissolve" type="Button" parent="Box"] 47 | layout_mode = 2 48 | text = "Dissolve" 49 | flat = true 50 | 51 | [node name="Subdivide" type="Button" parent="Box"] 52 | layout_mode = 2 53 | text = "Subdivide" 54 | flat = true 55 | 56 | [node name="Portal" type="Button" parent="Box"] 57 | layout_mode = 2 58 | toggle_mode = true 59 | text = "Portal" 60 | flat = true 61 | 62 | [node name="ChangeCost" type="Button" parent="Box"] 63 | layout_mode = 2 64 | text = "Cost" 65 | flat = true 66 | 67 | [node name="VSeparator" type="VSeparator" parent="Box"] 68 | layout_mode = 2 69 | 70 | [node name="CostWindow" type="ConfirmationDialog" parent="."] 71 | title = "Select new cost" 72 | position = Vector2i(525, 152) 73 | size = Vector2i(200, 153) 74 | script = ExtResource("2_3dol8") 75 | 76 | [node name="LineEdit" type="LineEdit" parent="CostWindow"] 77 | anchors_preset = 14 78 | anchor_top = 0.5 79 | anchor_right = 1.0 80 | anchor_bottom = 0.5 81 | offset_left = 8.0 82 | offset_top = -68.5 83 | offset_right = -8.0 84 | offset_bottom = 27.5 85 | grow_horizontal = 2 86 | grow_vertical = 2 87 | size_flags_horizontal = 3 88 | size_flags_vertical = 4 89 | text = "1" 90 | placeholder_text = "1" 91 | 92 | [connection signal="toggled" from="Box/Add" to="." method="_on_add_toggled"] 93 | [connection signal="pressed" from="Box/Remove" to="." method="_on_remove_pressed"] 94 | [connection signal="pressed" from="Box/Link" to="." method="_on_link_pressed"] 95 | [connection signal="pressed" from="Box/Unlink" to="." method="_on_unlink_pressed"] 96 | [connection signal="pressed" from="Box/Merge" to="." method="_on_merge_pressed"] 97 | [connection signal="pressed" from="Box/Dissolve" to="." method="_on_dissolve_pressed"] 98 | [connection signal="pressed" from="Box/Subdivide" to="." method="_on_subdivide_pressed"] 99 | [connection signal="toggled" from="Box/Portal" to="." method="_on_portal_toggled"] 100 | [connection signal="pressed" from="Box/ChangeCost" to="." method="_on_change_cost_pressed"] 101 | -------------------------------------------------------------------------------- /scripts/network/Editor/network_editor_utility.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name NetworkEditorUtility 3 | extends Control 4 | 5 | 6 | var add_mode:bool 7 | var portal_mode:bool 8 | 9 | 10 | signal dissolve 11 | signal merge 12 | signal remove 13 | signal link 14 | signal subdivide 15 | signal unlink 16 | signal change_cost_accepted(text:String) 17 | 18 | 19 | func _ready() -> void: 20 | $CostWindow.popup_accepted.connect(_on_change_cost_accepted.bind()) 21 | 22 | 23 | func _on_add_toggled(button_pressed:bool) -> void: 24 | add_mode = button_pressed 25 | 26 | 27 | func _on_dissolve_pressed() -> void: 28 | dissolve.emit() 29 | 30 | 31 | func _on_merge_pressed() -> void: 32 | merge.emit() 33 | 34 | 35 | func _on_remove_pressed() -> void: 36 | remove.emit() 37 | 38 | 39 | func _on_link_pressed() -> void: 40 | link.emit() 41 | 42 | 43 | func _on_subdivide_pressed() -> void: 44 | subdivide.emit() 45 | 46 | 47 | func _on_portal_toggled(button_pressed:bool) -> void: 48 | portal_mode = button_pressed 49 | 50 | 51 | func reset_portal_mode() -> void: 52 | ($"Box/Portal" as Button).button_pressed = false # will call signal automatically 53 | 54 | 55 | func _on_unlink_pressed() -> void: 56 | unlink.emit() 57 | 58 | 59 | func _on_change_cost_pressed() -> void: 60 | $CostWindow.popup_centered() 61 | 62 | 63 | func _on_change_cost_accepted(text:String) -> void: 64 | change_cost_accepted.emit(text) 65 | -------------------------------------------------------------------------------- /scripts/network/Scripts/network_edge.gd: -------------------------------------------------------------------------------- 1 | class_name NetworkEdge 2 | extends Resource 3 | 4 | 5 | @export var point_a:NetworkPoint 6 | @export var point_b:NetworkPoint 7 | @export var cost:float = 1 8 | @export var bidirectional:bool = true 9 | 10 | 11 | func _init(a:NetworkPoint = null, b:NetworkPoint = null, cost:float = 1, bidirectional:bool = true) -> void: 12 | self.point_a = a 13 | self.point_b = b 14 | self.cost = cost 15 | self.bidirectional = bidirectional 16 | -------------------------------------------------------------------------------- /scripts/network/Scripts/network_instance.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name NetworkInstance 3 | extends Node3D 4 | 5 | 6 | @export var network:Network = Network.new() 7 | 8 | 9 | func _ready() -> void: 10 | if not Engine.is_editor_hint(): 11 | queue_free() 12 | -------------------------------------------------------------------------------- /scripts/network/Scripts/network_point.gd: -------------------------------------------------------------------------------- 1 | class_name NetworkPoint 2 | extends Resource 3 | 4 | 5 | @export var position:Vector3 6 | 7 | 8 | func _init(pt:Vector3 = Vector3()) -> void: 9 | position = pt 10 | -------------------------------------------------------------------------------- /scripts/network/Scripts/network_portal.gd: -------------------------------------------------------------------------------- 1 | class_name NetworkPortal 2 | extends NetworkPoint 3 | ## A special point that allows you to connect two networks. 4 | -------------------------------------------------------------------------------- /scripts/network/Scripts/portal_edge.gd: -------------------------------------------------------------------------------- 1 | class_name PortalEdge 2 | extends Resource 3 | 4 | 5 | @export var portal_from:NetworkPortal 6 | @export var portal_to:NetworkPortal 7 | -------------------------------------------------------------------------------- /scripts/npc_tools/npc_tool.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=3 uid="uid://dj6oxmg3yja48"] 2 | 3 | [node name="NPCTool" type="Control"] 4 | layout_mode = 3 5 | anchors_preset = 15 6 | anchor_right = 1.0 7 | anchor_bottom = 1.0 8 | grow_horizontal = 2 9 | grow_vertical = 2 10 | 11 | [node name="HBoxContainer" type="HBoxContainer" parent="."] 12 | layout_mode = 1 13 | anchors_preset = 15 14 | anchor_right = 1.0 15 | anchor_bottom = 1.0 16 | grow_horizontal = 2 17 | grow_vertical = 2 18 | -------------------------------------------------------------------------------- /scripts/points/furniture.gd: -------------------------------------------------------------------------------- 1 | class_name Furniture 2 | extends IdlePoint 3 | ## A special IdlePoint abstract class that allows for animastions to be played when occupied. 4 | ## Add an [InteractiveObject] node somewhere. 5 | ## Does not enable crafting or anything by default, but you can extend it to do that if you want. 6 | 7 | 8 | ## Animation that plays on an actor when furniture is occupied. 9 | @export var animation:Animation 10 | 11 | # TODO: Animate the actors 12 | # TODO: Allow for multiple users (use sub points? allow for nested furniture?) 13 | -------------------------------------------------------------------------------- /scripts/points/idle_point.gd: -------------------------------------------------------------------------------- 1 | class_name IdlePoint 2 | extends Marker3D 3 | 4 | 5 | @export var owning_entity:String 6 | 7 | var is_occupied:bool: 8 | get: 9 | return is_occupied 10 | set(val): 11 | if val and not is_occupied: 12 | occupied.emit() 13 | elif not val and is_occupied: 14 | unoccupied.emit() 15 | is_occupied = val 16 | 17 | signal occupied 18 | signal unoccupied 19 | 20 | 21 | func _ready() -> void: 22 | add_to_group("idle_points") 23 | 24 | 25 | func occupy(who:String) -> void: 26 | is_occupied = true 27 | owning_entity = who 28 | 29 | 30 | func unoccupy() -> void: 31 | is_occupied = false 32 | -------------------------------------------------------------------------------- /scripts/points/spawn_point.gd: -------------------------------------------------------------------------------- 1 | class_name NPCSpawnPoint 2 | extends Node3D 3 | 4 | 5 | ## This is used for one-shot spawners; the unique ID of the spawner will be stored in here if it 6 | ## spawned its NPC. This is a hash set. 7 | static var spawn_tracker: Dictionary # TODO: Save this 8 | @export var templates: Array[NPCTemplateOption] 9 | @export_enum("One Shot", "Every Time", "Must Be Triggered") var mode:int 10 | @export var despawn_when_exit_scene:bool ## Whether this entity should despawn when it leaves the scene. 11 | 12 | 13 | func _ready() -> void: 14 | if is_visible_in_tree(): 15 | _roll() 16 | else: 17 | visibility_changed.connect(func(s:bool)->void: if s: _roll()) 18 | 19 | 20 | func _roll() -> void: 21 | match mode: 22 | 0: 23 | if not spawn_tracker.has(generate_id()): 24 | spawn() 25 | 1: 26 | spawn() 27 | 28 | 29 | func spawn() -> void: 30 | # set up entity 31 | var t := resolve_templates() 32 | if t == null: 33 | return 34 | 35 | # add that shiz 36 | spawn_tracker[generate_id()] = true 37 | var e := SKEntityManager.instance.add_entity_from_scene(t) 38 | e.rotation = quaternion 39 | e.generated = true 40 | if despawn_when_exit_scene: 41 | e.left_scene.connect(func() -> void: SKEntityManager.instance.remove_entity(e.name)) 42 | 43 | # resolve loot table 44 | if t.loot_table: 45 | for i in t.loot_table.resolve_table_to_instances(): 46 | var ie = SKEntityManager.instance.add_entity(i) # Add entity 47 | (e.get_component("ItemComponent") as ItemComponent).contained_inventory = e.name # set contained inventory 48 | 49 | 50 | func reset_spawner() -> void: 51 | spawn_tracker.erase(generate_id()) 52 | 53 | 54 | ## Get a deterministic ID for this spawner. 55 | func generate_id() -> int: 56 | return get_path().hash() 57 | 58 | 59 | ## Roll and select a template 60 | func resolve_templates() -> PackedScene: 61 | if templates.size() == 0: 62 | push_warning("No templates to spawn from.") 63 | return null 64 | 65 | while true: 66 | for t in templates: 67 | var res = t.resolve() 68 | if res: 69 | return t.template 70 | return null 71 | -------------------------------------------------------------------------------- /scripts/puppets/item_puppet.gd: -------------------------------------------------------------------------------- 1 | class_name ItemPuppet 2 | extends RigidBody3D 3 | 4 | 5 | ## The puppeteer of this item. 6 | var puppeteer:PuppetSpawnerComponent 7 | ## When this is true, the puppet will not sync its position with the puppeteer. 8 | ## This is intended to be used for when an item is in an NPCs hand. 9 | ## This will turn on by default if the node at "../../" relative to this one is not an entity, although this may change in the future. 10 | ## When true, all [CollisionShape3D]s beneath in the heirarchy will be turned off to prevent collisions with whoever is holding it. 11 | var inactive:bool: 12 | get: 13 | return inactive 14 | set(val): 15 | inactive = val 16 | set_collision_state(self, not val) 17 | 18 | signal change_position(Vector3) 19 | signal change_rotation(Quaternion) 20 | 21 | 22 | func _ready(): 23 | if not $"../../" is SKEntity: # TODO: Less brute force method 24 | inactive = true 25 | return 26 | 27 | puppeteer = $"../../".get_component("PuppetSpawnerComponent") 28 | change_position.connect((get_parent().get_parent() as SKEntity)._on_set_position.bind()) 29 | change_rotation.connect((get_parent().get_parent() as SKEntity)._on_set_rotation.bind()) 30 | 31 | 32 | func _process(delta): 33 | if not inactive: 34 | change_position.emit(position) 35 | change_rotation.emit(quaternion) 36 | 37 | 38 | func get_puppeteer() -> PuppetSpawnerComponent: 39 | if inactive: 40 | return null 41 | return puppeteer 42 | 43 | 44 | func set_collision_state(n:Node, state:bool) -> void: 45 | if n is CollisionShape3D and not n.get_parent() is HitDetector: 46 | (n as CollisionShape3D).disabled = not state 47 | for c in n.get_children(): 48 | set_collision_state(c, state) 49 | -------------------------------------------------------------------------------- /scripts/puppets/npc_puppet.gd: -------------------------------------------------------------------------------- 1 | class_name NPCPuppet 2 | extends CharacterBody3D 3 | ## Puppet "brain" for an NPC. 4 | 5 | 6 | @onready var movement_target_position: Vector3 = position # No world because this agent only works in the scene. 7 | ## This is a stealth provider. See the "Sealth Provider" article i nthe documentation for details. 8 | @export var eyes:Node 9 | var npc_component:NPCComponent 10 | var puppeteer:PuppetSpawnerComponent 11 | var view_dir:ViewDirectionComponent 12 | var movement_speed: float = 1.0 13 | var target_reached:bool: 14 | get: 15 | return navigation_agent.is_navigation_finished() 16 | 17 | ## Called every frame to update the entity's position. 18 | signal change_position(Vector3) 19 | 20 | var movement_paused:bool = false 21 | ## The navigation agent. 22 | @onready var navigation_agent: NavigationAgent3D = $NavigationAgent3D 23 | 24 | 25 | # Called when the node enters the scene tree for the first time. 26 | func _ready() -> void: 27 | call_deferred("_actor_setup") 28 | add_to_group("perception_target") 29 | change_position.connect((get_parent().get_parent() as SKEntity)._on_set_position.bind()) 30 | puppeteer = $"../../".get_component("PuppetSpawnerComponent") 31 | npc_component = $"../../".get_component("NPCComponent") 32 | view_dir = $"../../".get_component("ViewDirectionComponent") 33 | if npc_component: 34 | puppeteer.printe("Connecting percieved event") 35 | 36 | npc_component.entered_combat.connect(draw_weapons.bind()) 37 | npc_component.left_combat.connect(lower_weapons.bind()) 38 | 39 | else: 40 | push_warning("NPC Puppet not a child of an entity with an NPCComponent. Perception turned off.") 41 | 42 | 43 | func get_puppeteer() -> PuppetSpawnerComponent: 44 | return puppeteer 45 | 46 | 47 | ## Finds the closest point to this puppet, and jumps to it. 48 | ## This is to avoid getting stuck in things that it may have phased into while navigating out-of-scene. 49 | func snap_to_navmesh() -> void: 50 | position = NavigationServer3D.map_get_closest_point(NavigationServer3D.get_maps()[0], position) 51 | 52 | 53 | ## Set up navigation. 54 | func _actor_setup() -> void: 55 | # Wait for the first physics frame so the NavigationServer can sync. 56 | await get_tree().physics_frame 57 | snap_to_navmesh() # snap to mesh 58 | # Now that the navigation map is no longer empty, set the movement target. 59 | set_movement_target(movement_target_position) 60 | 61 | 62 | ## Set the target for the NPC. 63 | func set_movement_target(movement_target: Vector3) -> void: 64 | navigation_agent.set_target_position(movement_target) 65 | 66 | 67 | func pause_nav() -> void: 68 | movement_paused = true 69 | 70 | 71 | func continue_nav() -> void: 72 | movement_paused = false 73 | 74 | 75 | func _physics_process(delta) -> void: 76 | npc_component.puppet_request_move.emit(self) 77 | 78 | 79 | func _process(delta) -> void: 80 | change_position.emit(position) 81 | view_dir.view_rot = rotation 82 | 83 | 84 | func draw_weapons() -> void: 85 | npc_component.puppet_request_raise_weapons.emit(self) 86 | 87 | 88 | func lower_weapons() -> void: 89 | npc_component.puppet_request_lower_weapons.emit(self) 90 | -------------------------------------------------------------------------------- /scripts/relationships/relationship.gd: -------------------------------------------------------------------------------- 1 | class_name Relationship 2 | extends Resource 3 | 4 | 5 | @export var other_person:String 6 | @export var level:RelationshipLevel = RelationshipLevel.ACQUAINTANCE 7 | @export_category("Optional") 8 | @export var relationship_type:RelationshipAssociation 9 | @export var role:String # gotta figure out how to make it dynamic enum 10 | 11 | 12 | ## Level determining how close the two NPCs are. 13 | enum RelationshipLevel { 14 | NEMESIS, ## NPC Will always engage on sight. 15 | ENEMY, ## Depending on combat settings, NPC may engage this level and below on sight. 16 | FOE, ## Dislike eachother a lot. 17 | RIVAL, ## Homestuck tells me these also smooch. 18 | ACQUAINTANCE, ## No real relationship to speak of. 19 | FRIEND, ## Friendly. 20 | BFF, ## Closer friend. 21 | ALLY, ## Depending on NPC behavior settings, may assist this level and above in combat. 22 | LOVER, ## smouch 23 | } 24 | -------------------------------------------------------------------------------- /scripts/relationships/relationship_association.gd: -------------------------------------------------------------------------------- 1 | class_name RelationshipAssociation 2 | extends Resource 3 | 4 | 5 | @export var relationship_key:String 6 | @export var relationship_roles:Array[String] = [] 7 | -------------------------------------------------------------------------------- /scripts/schedules/continuity_condition.gd: -------------------------------------------------------------------------------- 1 | class_name ContinuityCondition 2 | extends ScheduleCondition 3 | 4 | @export var flag:String 5 | @export var value:float 6 | 7 | func evaluate() -> bool: 8 | # return false if doesn't have flag 9 | if not GameInfo.continuity_flags.has(flag): 10 | return false 11 | # return false if values don't match up 12 | if not GameInfo.continuity_flags[flag] == value: 13 | return false 14 | # else return true 15 | return true 16 | -------------------------------------------------------------------------------- /scripts/schedules/sandbox_schedule.gd: -------------------------------------------------------------------------------- 1 | class_name SandboxSchedule 2 | extends ScheduleEvent 3 | ## A "Sandbox" procedure is a term borrowed from Creation Kit games. This is essentially letting the NPC mill about with nothing more important to do. 4 | 5 | 6 | ## Influences how long an NPC will do an activity for. Represents the midpoint of a random range time duration in seconds. 7 | @export var energy:float 8 | @export var can_swim:bool = false 9 | @export var can_sit:bool = true 10 | @export var can_eat:bool = true 11 | @export var can_sleep:bool = true 12 | ## Whether this entity can engage in conversdation while idling. 13 | @export var can_engage_conversation:bool = true 14 | @export var use_idle_points:bool = true 15 | @export_category("Location") 16 | ## Whether this NPC must be at a certain location to idle. For example: town square, inn. 17 | @export var be_at_location:bool = true 18 | @export var location_position:Vector3 19 | @export var location_world:String 20 | @export var target_radius:float = 25 21 | var _npc:NPCComponent 22 | 23 | 24 | func get_event_location() -> NavPoint: 25 | # Idle points found from goap action 26 | return NavPoint.new(location_world, location_position) 27 | 28 | 29 | func satisfied_at_location(e:SKEntity) -> bool: 30 | # if we dont need to be at location, return true by default 31 | if not be_at_location: 32 | return true 33 | # if world not the same 34 | if not e.world == location_world: 35 | return false 36 | # if too far away 37 | if e.position.distance_to(location_position) > target_radius: 38 | return false 39 | # else, we passed 40 | return true 41 | 42 | 43 | func on_event_started() -> void: 44 | _npc.add_objective({"perform_idle_point":true}, false, 0) 45 | _npc.goap_memory["sandbox_schedule"] = self 46 | 47 | 48 | func on_event_ended() -> void: 49 | _npc.remove_objective_by_goals({"perform_idle_point":true}) 50 | _npc.goap_memory.erase("sandbox_schedule") 51 | 52 | 53 | func attach_npc(n:NPCComponent) -> void: 54 | _npc = n 55 | -------------------------------------------------------------------------------- /scripts/schedules/schedule.gd: -------------------------------------------------------------------------------- 1 | class_name Schedule 2 | extends Node 3 | 4 | 5 | ## Keeps track of the schedule. 6 | ## Schedules are roughly analagous to Creation Kit's "AI packages", although limited to time slots. 7 | ## It is made up of [ScheduleEvent]s. 8 | ## To adjust NPC behavior under circumastances outside of keeping a schedule, see [GOAPComponent] and [ScheduleCondition]. 9 | 10 | 11 | var events:Array[ScheduleEvent] 12 | 13 | 14 | func _ready() -> void: 15 | var es:Array[ScheduleEvent] = [] 16 | for n:Node in get_children(): 17 | if n is ScheduleEvent: 18 | es.append(n) 19 | events = es 20 | 21 | 22 | func find_schedule_activity_for_current_time() -> Option: 23 | # Scan events 24 | var valid_events = events.filter(func(ev:ScheduleEvent): return Timestamp.build_from_world_timestamp().is_in_between(ev.from, ev.to)) # get those that are in the time space 25 | valid_events.sort_custom(func(a:ScheduleEvent, b:ScheduleEvent): return a.priority > b.priority ) # sort descending by priority 26 | # find first one that is valid 27 | for ev in valid_events: 28 | if (ev as ScheduleEvent).condition == null or (ev as ScheduleEvent).condition.evaluate(): 29 | return Option.from(ev) 30 | # If we make it this far, we didn't find any, return none. 31 | return Option.none() 32 | -------------------------------------------------------------------------------- /scripts/schedules/schedule_condition.gd: -------------------------------------------------------------------------------- 1 | class_name ScheduleCondition 2 | extends Resource 3 | ## base class for schedule conditions that involve finer control. 4 | 5 | func evaluate() -> bool: 6 | return false 7 | -------------------------------------------------------------------------------- /scripts/schedules/schedule_event.gd: -------------------------------------------------------------------------------- 1 | class_name ScheduleEvent 2 | extends Node 3 | 4 | 5 | ## These are the different schedule events that can occupy a schedule. 6 | 7 | 8 | ## From what time? 9 | @export var from:Timestamp 10 | ## To what time? 11 | @export var to:Timestamp 12 | ## Anmy condition that needs be checked first. 13 | @export var condition:ScheduleCondition 14 | ## Schedule priotity. 15 | @export var priority:float 16 | 17 | 18 | ## Get the location this event is at. 19 | func get_event_location() -> NavPoint: 20 | return null 21 | 22 | 23 | ## Wthether this entity is "at" the event. 24 | func satisfied_at_location(e:SKEntity) -> bool: 25 | return true 26 | 27 | 28 | ## What to do when the event has begun. 29 | func on_event_started() -> void: 30 | return 31 | 32 | 33 | ## What to do when the event has ended. 34 | func on_event_ended() -> void: 35 | return 36 | -------------------------------------------------------------------------------- /scripts/sk_global.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | ## A singleton that allows any script to access various important nodes without having to deal with scene scope. 4 | ## It also has some important utility functions for working with entities. 5 | 6 | 7 | ## World states for the GOAP system. 8 | var world_states:Dictionary 9 | ## Status effects registered in the game. 10 | var status_effects:Dictionary = {} 11 | ## The SKConfig resource. 12 | var config:SKConfig 13 | 14 | ## Called when the [SKEntityManager] has finished loading. 15 | signal entity_manager_loaded 16 | ## When a chest (or other inventory) is opened. 17 | signal inventory_opened(id:StringName) 18 | 19 | 20 | func _ready() -> void: 21 | ProjectSettings.settings_changed.connect(_reload_config.bind()) 22 | _reload_config() 23 | 24 | 25 | func _reload_config() -> void: 26 | var path:Variant = ProjectSettings.get_setting("skelerealms/config_path") 27 | 28 | if path == null: 29 | return 30 | if not path is String: 31 | return 32 | 33 | if not ResourceLoader.exists(path): 34 | config = null 35 | return 36 | 37 | config = ResourceLoader.load(path) 38 | 39 | if Engine.is_editor_hint(): 40 | return 41 | 42 | config.compile() 43 | for se:StatusEffect in config.status_effects: 44 | SkeleRealmsGlobal.register_effect(se.name, se) 45 | 46 | 47 | ## Attempts to find an entity in the tree above a node. Returns null if none found. Automatically takes account of reparented puppets. 48 | func get_entity_in_tree(child:Node) -> SKEntity: 49 | var checking = child 50 | while not checking.get_parent() == null: 51 | if checking is SKEntity: 52 | return checking 53 | 54 | # Check if puppet and getting puppeteer 55 | if checking.has_method("get_puppeteer"): 56 | if checking.get_puppeteer(): 57 | checking = checking.get_puppeteer() 58 | continue 59 | 60 | checking = checking.get_parent() 61 | 62 | return null 63 | 64 | 65 | ## Recursively get [RID]s of all children below this node if it is a [CollisionObject3D]. 66 | func get_child_rids(child:Node) -> Array: 67 | var output = [] 68 | 69 | for c in child.get_children(): 70 | if c is CollisionObject3D: 71 | output.append(c.get_rid()) 72 | output.append_array(get_child_rids(c)) 73 | 74 | return output 75 | 76 | 77 | ## Get any damageable node in parent chain or children 1 layer deep; either [DamageableObject] or [DamageableComponent]. Null if none found. 78 | func get_damageable_node(n:Node) -> Node: 79 | return _walk_for_component(n, "DamageableComponent", func(x:Node): return x is DamageableObject) 80 | 81 | 82 | ## Get any interactible node in parent chain or children 1 layer deep; either [InteractiveObject] or [InteractiveComponent]. Null if none found. 83 | func get_interactive_node(n:Node) -> Node: 84 | return _walk_for_component(n, "InteractiveComponent", func(x:Node): return x is InteractiveObject) 85 | 86 | 87 | ## Get any spell target node in parent chain or children 1 layer deep; either [SpellTargetObject] or [SpellTargetComponent]. Null if none found. 88 | func get_spell_target_component(n:Node) -> Node: 89 | return _walk_for_component(n, "SpellTargetComponent", func(x:Node): return x is SpellTargetObject) 90 | 91 | 92 | ## Walks the tree in parent chain above or 1 layer of children below for a node that satisfies one of the following condition: 93 | ## - Is an entity with a component of type component_type, returning the component 94 | ## - Makes callable wo_check return true 95 | ## See [method get_damageable_node] for a use case. 96 | func _walk_for_component(n:Node, component_type:String, wo_check:Callable) -> Node: 97 | # Check children 98 | for c in n.get_children(): 99 | if wo_check.call(c): 100 | return c 101 | 102 | # Check for world object in parents 103 | var checking = n 104 | while not checking.get_parent() == null: 105 | if wo_check.call(checking): 106 | return checking 107 | 108 | # Check if puppet and getting puppeteer 109 | if checking.has_method("get_puppeteer"): 110 | if checking.get_puppeteer(): 111 | checking = checking.get_puppeteer() 112 | continue 113 | 114 | checking = checking.get_parent() 115 | 116 | # Check for entity component 117 | var e = get_entity_in_tree(n) 118 | if e: 119 | var dc = e.get_component(component_type) 120 | return dc 121 | 122 | return null 123 | 124 | 125 | func register_effect(what:String, eff:StatusEffect) -> void: 126 | status_effects[what] = eff 127 | -------------------------------------------------------------------------------- /scripts/spell_casting/spell.gd: -------------------------------------------------------------------------------- 1 | class_name Spell 2 | extends Resource 3 | ## This the base class for any spell that NPCs can cast. 4 | ## This essentially a blank slate, and as this is a GDScript file, a spell can do literally anything, sky is the limit. 5 | ## However, with great control comes great responsibility, my uncle once told me. This means you need to deal with basic stuff, like willpower drain, yourself. 6 | ## Despite that, it includes a number of helper methods to do the simple stuff. See [method _find_spell_targets_in_range]. 7 | 8 | 9 | ## Spell's ID for translation. 10 | @export var spell_name:String 11 | ## Custom assets for this spell; particles, floating hand models, whatever. 12 | ## You can pack it all in here so you don't have to use load(), but you can still do that if you want to. 13 | @export var spell_assets:Dictionary 14 | ## An array of spell effects we can apply to something we hit. 15 | @export var spell_effects:Array[StringName] 16 | ## Whether being hit by this spell is to be considered an attack. 17 | @export var aggressive:bool = false 18 | ## The spell caster. 19 | var _caster:SpellHand 20 | 21 | 22 | ## When the spell is first cast. 23 | func on_spell_cast(): 24 | pass 25 | 26 | 27 | ## Called every frame as the spell is being held by the player; eg. as the button is being held to blast flames. 28 | ## You can use the delta to drain willpower, or whatever. 29 | func on_spell_held(delta): 30 | pass 31 | 32 | 33 | ## When the spell is released; eg. when the button is released. 34 | ## This doesn't just have to cancel the spell, though; perhaps the player needs to hold a spell to choose a target or charge a kamehameha, and then release to cast. 35 | func on_spell_released(): 36 | pass 37 | 38 | 39 | ## Called when the spell needs to be reset to cast again, and also when it is loaded for the first time; so this is also like a _ready() function. 40 | ## Only reset your own variables; the variables defined in this parent class will not be re-initialized. 41 | func reset(): 42 | pass 43 | 44 | 45 | func process(delta:float) -> void: 46 | pass 47 | 48 | 49 | func physics_process(delta:float) -> void: 50 | pass 51 | 52 | 53 | ## Find all nodes in a range of a point, with the results in a dictionary matching a physics query (like raycasting). 54 | ## You can use this for an AOE attack. 55 | ## Using ignore_self assumes that the caster's origin defines the root of the actor casting it. 56 | ## Only returns 32 results max. 57 | func _find_spell_targets_in_range(pos:Vector3, radius:float, ignore_self:bool = false) -> Dictionary: 58 | var space_state = _caster.get_world_3d().direct_space_state # get space state 59 | # create query 60 | var query = PhysicsShapeQueryParameters3D.new() 61 | query.shape = SphereShape3D.new() 62 | # create query position 63 | var t = Transform3D() 64 | t.origin = pos 65 | t.scaled(Vector3(radius, radius, radius)) # scale to match radius 66 | query.transform = t 67 | # make query 68 | await _caster.get_tree().physics_frame 69 | var res = space_state.intersect_shape(query) 70 | # if ignoring self, filter out all nodes part of this tree 71 | if ignore_self: 72 | res = res.filter(func(x:Dictionary): 73 | return not (x["collider"] as Node).is_ancestor_of(_caster) and not (x["collider"] as Node).find_child(_caster.name) 74 | ) 75 | # return results, where all colliders are selected from it. 76 | return res 77 | 78 | 79 | ## Apply a spell effect to an object. 80 | ## Only works if target is of type [SpellTargetComponent] or [SpellTargetObject]. 81 | func _apply_spell_effect_to(target, effect:StringName): 82 | # return early if invalid object 83 | if not target is SpellTargetComponent and not target is SpellTargetObject: 84 | return 85 | target.add_effect(effect) 86 | 87 | 88 | ## Casts a ray, and returns anything it hits, with the results in a dictionary matching a physics query (like raycasting). The dictionary is empty if it hits nothing. 89 | ## Using ignore_self assumes that the caster's origin defines the root of the actor casting it. 90 | func _raycast(from:Vector3, direction:Vector3, distance:float, ignore_self:bool = false) -> Dictionary: 91 | var to = from + (direction * distance) 92 | var ray = PhysicsRayQueryParameters3D.create(from, to) 93 | var space_state = _caster.get_world_3d().direct_space_state # get space state 94 | await _caster.get_tree().physics_frame 95 | var res = space_state.intersect_ray(ray) 96 | if res.is_empty(): 97 | return {} 98 | if not res.collider.is_ancestor_of(_caster) and not res.collider.find_child(_caster.name): 99 | return res 100 | return {} 101 | -------------------------------------------------------------------------------- /scripts/spell_casting/spell_effect.gd: -------------------------------------------------------------------------------- 1 | class_name SpellEffect 2 | extends Resource 3 | ## Base class for spell effects. 4 | 5 | 6 | ## The target of this spell effect. 7 | var target:SpellTargetComponent 8 | 9 | 10 | func apply(stc:SpellTargetComponent) -> void: 11 | target = stc 12 | 13 | 14 | ## Called every frame as the spell is active. 15 | func on_update(delta:float) -> void: 16 | pass 17 | 18 | 19 | ## Called when the spell first begins. 20 | func on_start_effect() -> void: 21 | pass 22 | 23 | 24 | ## Called when the spell ends. 25 | func on_end_effect() -> void: 26 | pass 27 | -------------------------------------------------------------------------------- /scripts/spell_casting/spell_hand.gd: -------------------------------------------------------------------------------- 1 | class_name SpellHand 2 | extends Node3D 3 | ## Spell casting origin. This is a Node3D, and in general, it should be placed underneath a hand bone, or the top of a staff, or something like that. 4 | ## You gotta connect these to inputs yourself. 5 | 6 | 7 | ## What spell is active right now. 8 | var _active_spell:Spell 9 | ## The SKEntity this hand is attached to. 10 | var entity:SKEntity 11 | 12 | 13 | func cast_spell(): 14 | if not _active_spell: 15 | return 16 | _active_spell.reset() 17 | _active_spell.on_spell_cast() 18 | 19 | 20 | func hold_spell(delta): 21 | if not _active_spell: 22 | return 23 | _active_spell.on_spell_held(delta) 24 | 25 | 26 | func release_spell(): 27 | if not _active_spell: 28 | return 29 | _active_spell.on_spell_released() 30 | 31 | 32 | func load_spell(sp:Spell): 33 | _active_spell = sp 34 | sp._caster = self 35 | sp.reset() 36 | -------------------------------------------------------------------------------- /scripts/spell_casting/spell_item.gd: -------------------------------------------------------------------------------- 1 | class_name SpellItem 2 | extends Resource 3 | -------------------------------------------------------------------------------- /scripts/spell_casting/spell_projectile.gd: -------------------------------------------------------------------------------- 1 | class_name SpellProjectile 2 | extends RigidBody3D 3 | ## A special script for projectiles: provides a callback when it hits an object. 4 | 5 | 6 | ## Callback when something is hit. 7 | signal hit_target(target:Node3D) 8 | 9 | 10 | func _ready(): 11 | body_entered.connect(func(x:Node3D): 12 | hit_target.emit(x) 13 | ) 14 | -------------------------------------------------------------------------------- /scripts/system/timestamp.gd: -------------------------------------------------------------------------------- 1 | class_name Timestamp 2 | extends Resource 3 | 4 | 5 | # SO much repetitive code 6 | 7 | @export_flags("Minute:1", "Hour:2", "Day:4", "Week:8", "Month:16", "Year:32") var compare:int = 0b00010 8 | var use_minute:bool: 9 | get: 10 | return compare & 1 == 1 11 | @export var minute:int 12 | var use_hour:bool: 13 | get: 14 | return compare & 2 == 2 15 | @export var hour:int 16 | var use_day:bool: 17 | get: 18 | return compare & 4 == 4 19 | @export var day:int 20 | var use_week:bool: 21 | get: 22 | return compare & 8 == 8 23 | @export var week:int 24 | var use_month:bool: 25 | get: 26 | return compare & 16 == 16 27 | @export var month:int 28 | var use_year:bool: 29 | get: 30 | return compare & 32 == 32 31 | @export var year:int 32 | 33 | 34 | static func build_from_world_timestamp() -> Timestamp: 35 | var stamp:Timestamp = Timestamp.new() 36 | 37 | # Set using 38 | stamp.use_minute = true 39 | stamp.use_hour = true 40 | stamp.use_day = true 41 | stamp.use_week = true 42 | stamp.use_month = true 43 | stamp.use_year = true 44 | 45 | # Set times 46 | stamp.minute = GameInfo.minute 47 | stamp.hour = GameInfo.hour 48 | stamp.day = GameInfo.day 49 | stamp.week = GameInfo.week 50 | stamp.month = GameInfo.month 51 | stamp.year = GameInfo.year 52 | 53 | return stamp 54 | 55 | func is_in_between(from:Timestamp, to:Timestamp) -> bool: 56 | # if we are using the minute and the minute is not between the other two. 57 | # Ditto for all fields. 58 | if use_minute and not (from.minute <= minute and minute < to.minute): 59 | return false 60 | if use_hour and not (from.hour <= hour and hour < to.hour): 61 | return false 62 | if use_day and not (from.day <= day and day < to.day): 63 | return false 64 | if use_week and not (from.week <= week and week < to.week): 65 | return false 66 | if use_month and not (from.month <= month and month < to.month): 67 | return false 68 | if use_year and not (from.year <= year and year < to.year): 69 | return false 70 | 71 | return true 72 | 73 | 74 | ## If timestamp is less than or equal to 75 | func lte(to:Timestamp) -> bool: 76 | # if we are using the minute and the minute is less than to.minute 77 | # Ditto for all fields. 78 | if use_minute and not (minute <= to.minute): 79 | return false 80 | if use_hour and not (hour <= to.hour): 81 | return false 82 | if use_day and not (day <= to.day): 83 | return false 84 | if use_week and not (week <= to.week): 85 | return false 86 | if use_month and not ( month <= to.month): 87 | return false 88 | if use_year and not (year <= to.year): 89 | return false 90 | 91 | return true 92 | 93 | 94 | ## If timestamp is greater than or equal to 95 | func gte(to:Timestamp) -> bool: 96 | # if we are using the minute and the minute is less than to.minute 97 | # Ditto for all fields. 98 | if use_minute and not (minute >= to.minute): 99 | return false 100 | if use_hour and not (hour >= to.hour): 101 | return false 102 | if use_day and not (day >= to.day): 103 | return false 104 | if use_week and not (week >= to.week): 105 | return false 106 | if use_month and not ( month >= to.month): 107 | return false 108 | if use_year and not (year >= to.year): 109 | return false 110 | 111 | return true 112 | 113 | 114 | func time_since(other:Timestamp) -> Dictionary: 115 | return { 116 | &"year": other.year - year, 117 | &"month": other.month - month, 118 | &"week": other.week - week, 119 | &"day": other.day - day, 120 | &"hour": other.hour - hour, 121 | &"minute": other.minute - minute 122 | } 123 | 124 | 125 | ## Convert a dictionary time, like produced by [method time_since], to minutes. 126 | static func dict_to_minutes(t:Dictionary) -> int: 127 | var my:int = t.year * ProjectSettings.get_setting("skelerealms/months_in_year") # months from years 128 | var wm: int = (t.month + my) * ProjectSettings.get_setting("skelerealms/weeks_in_month") # weeks from months 129 | var dw: int = (t.week + wm) * ProjectSettings.get_setting("skelerealms/days_per_week") # days from weeks 130 | var hd: int = (t.day + dw) * ProjectSettings.get_setting("skelerealms/hours_per_day") # hours from days 131 | var mh: int = (t.hour + hd) * ProjectSettings.get_setting("skelerealms/minutes_per_hour") # minutes from hours 132 | return t.minute + mh 133 | -------------------------------------------------------------------------------- /scripts/system/world_loader.gd: -------------------------------------------------------------------------------- 1 | class_name WorldLoader 2 | extends Node 3 | ## World scene loader 4 | 5 | 6 | var world_paths:Dictionary = {} 7 | var regex:RegEx 8 | var loading_path:String 9 | var last_load_progress := 0 10 | 11 | 12 | ## Called when the loading process begins. 13 | ## Hook into this to pop up a loading screen. 14 | signal begin_world_loading 15 | ## Called when the world has finished loading, and gameplay can resume. 16 | ## Use this to either continue gameplay, or pop up a button on the loading screen to continue gameplay. 17 | signal world_loading_ready 18 | ## Called while the scene is loading with its progress. Progress from 0 to 1. 19 | signal load_scene_progess_updated(percent:int) 20 | 21 | 22 | func _enter_tree() -> void: 23 | if get_child_count() > 0: 24 | GameInfo.world = get_child(0).name 25 | 26 | 27 | func _ready(): 28 | regex = RegEx.new() 29 | regex.compile("([^\\/\n\\r]+)\\.t?scn") 30 | _cache_worlds(ProjectSettings.get_setting("skelerealms/worlds_path")) 31 | GameInfo.is_loading = false 32 | 33 | 34 | func _process(_delta: float) -> void: 35 | if not GameInfo.is_loading: 36 | return 37 | 38 | var prog = [] 39 | match ResourceLoader.load_threaded_get_status(loading_path, prog): 40 | ResourceLoader.THREAD_LOAD_LOADED: 41 | print("Finishing up...") 42 | var ps := ResourceLoader.load_threaded_get(loading_path) as PackedScene 43 | if not ps: 44 | push_error("Failed to load world at %s" % loading_path) 45 | _abort() 46 | _finish_load.call_deferred(ps) 47 | ResourceLoader.THREAD_LOAD_FAILED, ResourceLoader.THREAD_LOAD_INVALID_RESOURCE: 48 | push_error("Could not load world due to thread loading error.") 49 | _abort() 50 | ResourceLoader.THREAD_LOAD_IN_PROGRESS: 51 | if not last_load_progress == prog[0]: 52 | (func(): load_scene_progess_updated.emit(prog[0])).call_deferred() 53 | last_load_progress = prog[0] 54 | 55 | 56 | ## Load a new world. 57 | func load_world(wid:String) -> void: 58 | print("loading world") 59 | 60 | if not world_paths.has(wid): 61 | push_error("World not found: %s" % wid) 62 | return 63 | 64 | GameInfo.console_unfreeze() 65 | begin_world_loading.emit() 66 | GameInfo.game_loading.emit(wid) 67 | await get_tree().process_frame 68 | print("Processed frame. Continuing...") 69 | GameInfo.is_loading = true 70 | #await get_tree().process_frame 71 | #print("processed frame. Unloading world...") 72 | var e:Error = ResourceLoader.load_threaded_request(world_paths[wid], "PackedScene", true) 73 | if not e == OK: 74 | push_error("Load thread error: %d" % e) 75 | _abort() 76 | return 77 | 78 | last_load_progress = 0 79 | loading_path = world_paths[wid] 80 | 81 | _unload_world() 82 | 83 | 84 | func _finish_load(w:PackedScene) -> void: 85 | print("finished loading world") 86 | add_child(w.instantiate()) 87 | print("finished loading world. Instantiating...") 88 | world_loading_ready.emit() 89 | GameInfo.is_loading = false 90 | print("World instantiated.") 91 | GameInfo.game_loaded.emit() 92 | 93 | 94 | func _unload_world(): 95 | if get_child_count() > 0: 96 | remove_child(get_child(0)) 97 | 98 | 99 | func _abort() -> void: 100 | # TODO: Crash game? 101 | world_loading_ready.emit() 102 | GameInfo.is_loading = false 103 | GameInfo.game_loaded.emit() 104 | 105 | 106 | ## Searches the worlds directory and caches filepaths, matching them to their name 107 | func _cache_worlds(path:String): 108 | var dir = DirAccess.open(path) 109 | if dir: 110 | dir.list_dir_begin() 111 | var file_name = dir.get_next() 112 | while file_name != "": 113 | if '.tscn.remap' in file_name: 114 | file_name = file_name.trim_suffix('.remap') 115 | if dir.current_is_dir(): # if is directory, cache subdirectory 116 | _cache_worlds("%s/%s" % [path, file_name]) 117 | else: # if filename, cache filename 118 | var result = regex.search(file_name) 119 | if result: 120 | world_paths[result.get_string(1)] = "%s/%s" % [path, file_name] 121 | file_name = dir.get_next() 122 | dir.list_dir_end() 123 | 124 | else: 125 | print("An error occurred when trying to access the path.") 126 | -------------------------------------------------------------------------------- /scripts/world_objects/damageable_object.gd: -------------------------------------------------------------------------------- 1 | class_name DamageableObject 2 | extends SKWorldObject 3 | ## For objects in the world that can be damaged but don't have to be tracked, like training dummies 4 | 5 | 6 | signal damaged(info:DamageInfo) 7 | 8 | 9 | func receive_message(msg:StringName, args:Array = []) -> void: 10 | if msg == &"damage": 11 | damage(args[0]) 12 | 13 | 14 | func damage(info:DamageInfo): 15 | damaged.emit(info) 16 | 17 | 18 | func _init() -> void: 19 | name = "DamageableObject" 20 | -------------------------------------------------------------------------------- /scripts/world_objects/door.gd: -------------------------------------------------------------------------------- 1 | class_name Door 2 | extends InteractiveObject 3 | ## Example implementation of an interactive object. 4 | ## Interacting with this teleports the interactor. 5 | 6 | 7 | @export var instance:DoorInstance 8 | @export var destination_instance:DoorInstance 9 | var dest_world:String: 10 | get: 11 | return destination_instance.world 12 | var dest_pos:Vector3: 13 | get: 14 | return destination_instance.position 15 | 16 | 17 | func _ready(): 18 | on_interact.connect(_handle_teleport_request.bind()) 19 | 20 | 21 | # You could also override #interact, instead of binding to signal. 22 | func _handle_teleport_request(id:String): 23 | print("teleporting %s to world %s at position %s" % [id, dest_world, dest_pos]) 24 | var teleportee = SKEntityManager.instance.get_entity(id) # get an entity 25 | if teleportee: # if there is a valid object 26 | var tc = teleportee.get_component("TeleportComponent") # Try to get a teleport component 27 | if tc: 28 | (tc as TeleportComponent).teleport(dest_world, dest_pos) 29 | -------------------------------------------------------------------------------- /scripts/world_objects/effects_object.gd: -------------------------------------------------------------------------------- 1 | class_name EffectsObject 2 | extends SKWorldObject 3 | 4 | 5 | ## This is a vessel for [class StatusEffectHost], intended to give statuseffects to non-entity objects. 6 | ## For example, you could add this and a [class DamageableObject] to a wooden box, and if the box is set 7 | ## on fire, it will turn to ash. 8 | 9 | 10 | var host:StatusEffectHost 11 | 12 | 13 | func _ready() -> void: 14 | host = StatusEffectHost.new() 15 | add_child(host) 16 | host.message_broadcast.connect(broadcast_message.bind()) 17 | 18 | 19 | func add_effect(what:StringName) -> void: 20 | host.add_effect(what) 21 | 22 | 23 | func remove_effect(e:StringName) -> void: 24 | host.remove_effect(e) 25 | 26 | 27 | func receive_message(msg:StringName, args:Array = []) -> void: 28 | match msg: 29 | &"add_effect": 30 | add_effect(args[0]) 31 | &"remove_effect": 32 | remove_effect(args[0]) 33 | -------------------------------------------------------------------------------- /scripts/world_objects/interactive_object.gd: -------------------------------------------------------------------------------- 1 | class_name InteractiveObject 2 | extends SKWorldObject 3 | ## base class for objects in the world that don't need tracking, but can be interacted with, like a sign. 4 | ## See [Door] for an example implementation. 5 | 6 | 7 | signal on_interact(id:String) 8 | 9 | ## Whether it can be interacted with. 10 | @export var interactible:bool = true 11 | ## Verb to use when hovered over. 12 | @export var interact_verb:String = "INTERACT" 13 | ## Name of the object. 14 | @export var object_name:String = "THING" 15 | 16 | 17 | func interact(id:String): 18 | on_interact.emit(id) 19 | 20 | 21 | func receive_message(msg:StringName, args:Array = []) -> void: 22 | if msg == &"interact": 23 | interact(args[0]) 24 | -------------------------------------------------------------------------------- /scripts/world_objects/sk_world_object.gd: -------------------------------------------------------------------------------- 1 | class_name SKWorldObject 2 | extends Node3D 3 | 4 | 5 | ## This is the base class for non-entity objects affected by Skelerealms concepts. 6 | ## This base class is needed to interact with the message broadcasting system used by [class StatusEffect]s - see [class EffectsObject]. 7 | ## [b]Please Note:[/b] I am still not 100% sure about this design decision, and this system may change in the future. 8 | 9 | 10 | ## A list of nodes that will have messages broadcast to them. 11 | var _neighbors:Array[SKWorldObject] = [] 12 | 13 | 14 | func _ready() -> void: 15 | _collect_neighbors() 16 | get_parent().child_order_changed.connect(_collect_neighbors.bind()) 17 | 18 | 19 | ## Broadcast messages to siblings and parent of this node (if they are SKWorldObjects). 20 | func broadcast_message(msg:StringName, args:Array = []) -> void: 21 | for n:SKWorldObject in _neighbors: 22 | n.receive_message(msg, args) 23 | 24 | 25 | ## Override this to handle receiving messages. 26 | func receive_message(_msg:StringName, _args:Array = []) -> void: 27 | return 28 | 29 | 30 | func _collect_neighbors() -> void: 31 | if get_parent() == null: 32 | return 33 | _neighbors.clear() 34 | if get_parent() is SKWorldObject: 35 | _neighbors.append(get_parent()) 36 | for c:Node in get_parent().get_children(): 37 | if c == self: 38 | continue 39 | if c is SKWorldObject: 40 | _neighbors.append(c) 41 | -------------------------------------------------------------------------------- /scripts/world_objects/spell_target_object.gd: -------------------------------------------------------------------------------- 1 | class_name SpellTargetObject 2 | extends Node3D 3 | 4 | # I wish I had mixins or interfaces. maybe I need to restructure something? 5 | @onready var status_effects:EffectsObject = $EffectsObject 6 | 7 | 8 | signal hit_with_spell(sp:Spell) 9 | 10 | 11 | func hit(sp:Spell): 12 | hit_with_spell.emit(sp) 13 | 14 | 15 | func apply_effect(eff:StringName): 16 | status_effects.add_effect(eff) 17 | -------------------------------------------------------------------------------- /scripts/world_objects/world_entity.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name SKWorldEntity 3 | extends Marker3D 4 | 5 | 6 | @export var entity:PackedScene: 7 | set(val): 8 | entity = val 9 | if Engine.is_editor_hint(): 10 | if get_child_count() > 0: 11 | get_child(0) 12 | if val: 13 | _show_preview() 14 | 15 | 16 | func _ready() -> void: 17 | if Engine.is_editor_hint(): 18 | _show_preview() 19 | else: 20 | SKEntityManager.instance.get_entity(entity._bundled.names[0]) 21 | 22 | 23 | func _show_preview() -> void: 24 | if not entity: 25 | return 26 | var e:SKEntity = entity.instantiate() 27 | var n:Node = e.get_world_entity_preview().duplicate() 28 | e.queue_free() 29 | if not n: 30 | return 31 | add_child(n) 32 | 33 | 34 | func _sync() -> void: 35 | if not Engine.is_editor_hint(): 36 | return 37 | 38 | if not entity: 39 | return 40 | 41 | var e:SKEntity = entity.instantiate() 42 | if not e: 43 | return 44 | 45 | e.position = global_position 46 | e.world = EditorInterface.get_edited_scene_root().name 47 | 48 | entity.pack(e) 49 | -------------------------------------------------------------------------------- /skelerealms_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlashScreen/skelerealms/293dfd0b68541066bc505eadf03255d85218b4bf/skelerealms_logo.png -------------------------------------------------------------------------------- /skelerealms_logo.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://doyt1pgujnkik" 6 | path="res://.godot/imported/skelerealms_logo.png-2e40e98784653e32f036998f6e38f67c.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/skelerealms/skelerealms_logo.png" 14 | dest_files=["res://.godot/imported/skelerealms_logo.png-2e40e98784653e32f036998f6e38f67c.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /skelerealms_logo.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://w3p5rvrm7uco" 6 | path="res://.godot/imported/skelerealms_logo.svg-c6e31dd2ffb46bd6b015399c74615ae8.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/skelerealms/skelerealms_logo.svg" 14 | dest_files=["res://.godot/imported/skelerealms_logo.svg-c6e31dd2ffb46bd6b015399c74615ae8.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /tools/config_sync_plugin.gd: -------------------------------------------------------------------------------- 1 | extends EditorInspectorPlugin 2 | 3 | 4 | ## Perhaps unintuitively named, this handles things that need to be synced to the sk config; AttributesComponent, SkillsComponent, Equipment Slots. 5 | 6 | 7 | const SlotSelector = preload("slot_enum_selector.gd") 8 | 9 | 10 | func _can_handle(object: Object) -> bool: 11 | return object is VitalsComponent or object is AttributesComponent or object is SkillsComponent or object is EquippableDataComponent 12 | 13 | 14 | func _parse_begin(object: Object) -> void: 15 | if object is AttributesComponent: 16 | _handle_attributes(object) 17 | elif object is SkillsComponent: 18 | _handle_skills(object) 19 | 20 | 21 | func _parse_property(object: Object, _type: Variant.Type, name: String, _hint_type: PropertyHint, _hint_string: String, _usage_flags: int, _wide: bool) -> bool: 22 | if object is EquippableDataComponent: 23 | return _handle_slots(object, name) 24 | return false 25 | 26 | 27 | func _handle_attributes(object: AttributesComponent) -> void: 28 | var b := Button.new() 29 | b.text = "Sync attributes set" 30 | b.pressed.connect(func() -> void: object.attributes = SkeleRealmsGlobal.config.attributes.duplicate()) 31 | add_custom_control(b) 32 | 33 | 34 | func _handle_skills(object: SkillsComponent) -> void: 35 | var b := Button.new() 36 | b.text = "Sync skill set" 37 | b.pressed.connect(func() -> void: object.skills = SkeleRealmsGlobal.config.skills.duplicate()) 38 | add_custom_control(b) 39 | 40 | 41 | func _handle_slots(object:EquippableDataComponent, n:StringName) -> bool: 42 | if n == "valid_slots": 43 | add_property_editor("valid_slots", SlotSelector.new()) 44 | return true 45 | return false 46 | -------------------------------------------------------------------------------- /tools/door_connect.gd: -------------------------------------------------------------------------------- 1 | extends EditorInspectorPlugin 2 | 3 | 4 | const NODE_3D_VIEWPORT_CLASS_NAME = "Node3DEditorViewport" 5 | 6 | var p:EditorPlugin 7 | var _viewports:Array = [] 8 | var _cams:Array[Camera3D] = [] 9 | 10 | 11 | func _can_handle(object): 12 | return object is Door 13 | 14 | 15 | func _parse_begin(obj:Object): 16 | var go_to_button:Button = Button.new() 17 | go_to_button.text = "Jump to door location" 18 | go_to_button.pressed.connect(func(): _jump_to_door_location(obj as Door)) 19 | add_custom_control(go_to_button) 20 | var set_position_button:Button = Button.new() 21 | set_position_button.text = "Set position data" 22 | set_position_button.pressed.connect(func(): _set_position(obj as Door)) 23 | add_custom_control(set_position_button) 24 | 25 | 26 | func _jump_to_door_location(obj:Door): 27 | var path = ProjectSettings.get_setting("skelerealms/worlds_path") 28 | var res = _find_world(path, obj.destination_instance.world) 29 | if res == "": 30 | return 31 | print("Jumping to location...") 32 | # Workaround from https://github.com/godotengine/godot/issues/75669#issuecomment-1621230016 33 | p.get_editor_interface().open_scene_from_path.call_deferred(res) 34 | p.get_editor_interface().edit_resource.call_deferred(load(res)) # switch tab 35 | p.get_editor_interface().set_main_screen_editor.call_deferred("3D") 36 | # var finish_up = func(): 37 | # print("setting camera position %s" % _cams[0].get_parent().get_parent()) 38 | # print(_cams.map(func(c:Camera3D): return c.global_position)) 39 | # var set_cam_position = func(): 40 | # _cams[0].global_position = obj.destination_instance.position 41 | # set_cam_position.call_deferred() 42 | # finish_up.call_deferred() 43 | 44 | 45 | func _set_position(obj:Door) -> void: 46 | obj.instance.position = obj.global_position 47 | obj.instance.world = obj.owner.name 48 | 49 | 50 | func _find_world(path:String, target:String) -> String: 51 | var dir = DirAccess.open(path) 52 | if dir: 53 | dir.list_dir_begin() 54 | var file_name = dir.get_next() 55 | while file_name != "": 56 | if dir.current_is_dir(): # if is directory, search subdirectory 57 | var res = _find_world(file_name, target) 58 | if not res == "": 59 | return res 60 | else: # if filename, cache filename 61 | var result = file_name.contains(target) 62 | if result: 63 | return "%s/%s" % [path, file_name] 64 | file_name = dir.get_next() 65 | dir.list_dir_end() 66 | return "" 67 | 68 | 69 | func _init(plug:EditorPlugin): 70 | p = plug 71 | _populate_data() 72 | 73 | 74 | func _populate_data() -> void: 75 | _find_viewports(p.get_editor_interface().get_base_control()) 76 | for v in _viewports: 77 | _find_cameras(v) 78 | 79 | 80 | func _find_viewports(n:Node) -> void: 81 | if n.get_class() == NODE_3D_VIEWPORT_CLASS_NAME: 82 | _viewports.append(n) 83 | return 84 | 85 | for c in n.get_children(): 86 | _find_viewports(c) 87 | 88 | 89 | func _find_cameras(n:Node) -> void: 90 | if n is Camera3D: 91 | _cams.append(n) 92 | return 93 | 94 | for c in n.get_children(): 95 | _find_cameras(c) 96 | -------------------------------------------------------------------------------- /tools/edit_button.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Button 3 | 4 | 5 | var sock:ENetMultiplayerPeer = ENetMultiplayerPeer.new() 6 | var spawn_position:Vector3 7 | var spawn_world:String 8 | var sending := false 9 | 10 | 11 | func _ready() -> void: 12 | if not Engine.is_editor_hint(): 13 | return 14 | text = "Open Skelerealms World" 15 | pressed.connect(_on_press.bind()) 16 | 17 | 18 | func _on_press() -> void: 19 | var cam:Camera3D = EditorInterface.get_editor_viewport_3d(0).get_camera_3d() 20 | 21 | OS.set_environment("world", EditorInterface.get_edited_scene_root().name) 22 | OS.set_environment("pos", var_to_str(cam.position)) 23 | 24 | var game_root_path:String = SkeleRealmsGlobal.config.game_root.resource_path 25 | EditorInterface.play_custom_scene(game_root_path) 26 | OS.set_environment("world", "") 27 | OS.set_environment("pos", "") 28 | -------------------------------------------------------------------------------- /tools/point_gizmo.gd: -------------------------------------------------------------------------------- 1 | extends EditorNode3DGizmoPlugin 2 | 3 | 4 | func _get_gizmo_name() -> String: 5 | return "SKR Point Gizmos" 6 | 7 | 8 | func _init() -> void: 9 | create_material("wm", Color(0,0,1)) 10 | create_material("npc", Color(1,0,1)) 11 | create_material("idle", Color(0,1,1)) 12 | create_handle_material("handles") 13 | 14 | 15 | func _has_gizmo(for_node_3d) -> bool: 16 | match for_node_3d.get_script(): 17 | NPCSpawnPoint, IdlePoint: 18 | return true 19 | _: 20 | return false 21 | 22 | 23 | func _redraw(gizmo: EditorNode3DGizmo) -> void: 24 | gizmo.clear() 25 | var mesh = SphereMesh.new() 26 | mesh.radius = 0.5 27 | gizmo.add_mesh(mesh, get_material(get_mat_for_node(gizmo.get_node_3d()), gizmo)) 28 | 29 | 30 | func get_mat_for_node(n:Node) -> String: 31 | match n.get_script(): 32 | NPCSpawnPoint: 33 | return "npc" 34 | IdlePoint: 35 | return "idle" 36 | _: 37 | return "" 38 | -------------------------------------------------------------------------------- /tools/schedule_box.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends PanelContainer 3 | 4 | 5 | const TRACK_WIDTH = 140 6 | const TRACK_OFFSET = 64 7 | 8 | var internal_pos:int 9 | var internal_size:int 10 | var editing:ScheduleEvent 11 | var editor:Control 12 | 13 | signal delete_requested 14 | 15 | 16 | func _ready() -> void: 17 | internal_pos = position.x 18 | internal_size = size.x 19 | if editing == null: 20 | editing = ScheduleEvent.new() 21 | editing.from = Timestamp.new() 22 | editing.to = Timestamp.new() 23 | 24 | 25 | func _on_beginning_point_dragged(offset: Variant) -> void: 26 | internal_pos += offset.x 27 | position.x = editor.snap_to_hour(editor.snap_to_minute(editor.scroll_value + internal_pos)) 28 | 29 | 30 | func _on_end_point_dragged(offset: Variant) -> void: 31 | internal_size += offset.x 32 | size.x = editor.snap_to_hour(editor.snap_to_minute(position.x + internal_size)) - position.x 33 | 34 | 35 | func _process(_delta: float) -> void: 36 | if editor == null: 37 | return 38 | 39 | position.x = editor.snap_to_hour(editor.snap_to_minute(editor.scroll_value + internal_pos)) 40 | 41 | var start:Dictionary = editor.get_time_from_position(position.x) 42 | var end:Dictionary = editor.get_time_from_position(position.x + size.x) 43 | editing.from.hour = start.hour 44 | editing.from.minute = start.minute 45 | editing.to.hour = end.hour 46 | editing.to.minute = end.minute 47 | 48 | 49 | func switch_track(to:int) -> void: 50 | position.y = TRACK_OFFSET + TRACK_WIDTH * to 51 | 52 | 53 | func edit(s:ScheduleEvent, e:Control) -> void: 54 | editing = s 55 | editor = e 56 | internal_pos = editor.position_from_time({ 57 | &"hour": s.from.hour, 58 | &"minute": s.from.minute, 59 | }) 60 | size.x = editor.position_from_time({ 61 | &"hour": s.to.hour, 62 | &"minute": s.to.minute, 63 | }) - internal_pos 64 | $MarginContainer/Controls/LineEdit.text = editing.name 65 | 66 | 67 | func _on_line_edit_text_submitted(new_text: String) -> void: 68 | editing.name = new_text 69 | 70 | 71 | func _on_button_pressed() -> void: 72 | return #EditorInterface.edit_resource(editing) 73 | 74 | 75 | func _on_remove_pressed() -> void: 76 | delete_requested.emit() 77 | -------------------------------------------------------------------------------- /tools/schedule_box_control.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://jbh33qql3r7c"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/tools/schedule_box.gd" id="1_fnsnl"] 4 | [ext_resource type="Script" path="res://addons/skelerealms/tools/scheduledraghandle.gd" id="2_1adky"] 5 | 6 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8y1y4"] 7 | bg_color = Color(0.196078, 0.54902, 0.580392, 1) 8 | corner_radius_top_left = 30 9 | corner_radius_top_right = 30 10 | corner_radius_bottom_right = 30 11 | corner_radius_bottom_left = 30 12 | expand_margin_top = 30.0 13 | expand_margin_bottom = 30.0 14 | shadow_size = 3 15 | shadow_offset = Vector2(3.44, 6.495) 16 | 17 | [node name="ScheduleBoxControl" type="Control"] 18 | layout_mode = 3 19 | anchors_preset = 0 20 | offset_right = 1152.0 21 | offset_bottom = 648.0 22 | mouse_filter = 2 23 | 24 | [node name="PanelContainer" type="PanelContainer" parent="."] 25 | layout_mode = 2 26 | offset_left = 64.0 27 | offset_top = 64.0 28 | offset_right = 180.0 29 | offset_bottom = 192.0 30 | size_flags_horizontal = 3 31 | size_flags_vertical = 3 32 | theme_override_styles/panel = SubResource("StyleBoxFlat_8y1y4") 33 | script = ExtResource("1_fnsnl") 34 | 35 | [node name="MarginContainer" type="MarginContainer" parent="PanelContainer"] 36 | layout_mode = 2 37 | theme_override_constants/margin_left = 15 38 | theme_override_constants/margin_right = 15 39 | 40 | [node name="Controls" type="VBoxContainer" parent="PanelContainer/MarginContainer"] 41 | layout_mode = 2 42 | 43 | [node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/Controls"] 44 | layout_mode = 2 45 | placeholder_text = "Name" 46 | 47 | [node name="Button" type="Button" parent="PanelContainer/MarginContainer/Controls"] 48 | layout_mode = 2 49 | text = "Open in 50 | Inspector" 51 | 52 | [node name="Remove" type="Button" parent="PanelContainer/MarginContainer/Controls"] 53 | layout_mode = 2 54 | text = "Remove" 55 | 56 | [node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer"] 57 | layout_mode = 2 58 | mouse_filter = 2 59 | 60 | [node name="BeginningPoint" type="Control" parent="PanelContainer/HBoxContainer"] 61 | custom_minimum_size = Vector2(32, 0) 62 | layout_mode = 2 63 | mouse_default_cursor_shape = 10 64 | script = ExtResource("2_1adky") 65 | 66 | [node name="VSeparator" type="VSeparator" parent="PanelContainer/HBoxContainer/BeginningPoint"] 67 | layout_mode = 1 68 | anchors_preset = 9 69 | anchor_bottom = 1.0 70 | offset_right = 4.0 71 | grow_vertical = 2 72 | 73 | [node name="Control" type="Control" parent="PanelContainer/HBoxContainer"] 74 | layout_mode = 2 75 | size_flags_horizontal = 3 76 | mouse_filter = 2 77 | 78 | [node name="EndPoint" type="Control" parent="PanelContainer/HBoxContainer"] 79 | custom_minimum_size = Vector2(32, 0) 80 | layout_mode = 2 81 | mouse_default_cursor_shape = 10 82 | script = ExtResource("2_1adky") 83 | 84 | [node name="VSeparator" type="VSeparator" parent="PanelContainer/HBoxContainer/EndPoint"] 85 | layout_mode = 1 86 | anchors_preset = 11 87 | anchor_left = 1.0 88 | anchor_right = 1.0 89 | anchor_bottom = 1.0 90 | offset_left = -4.0 91 | grow_horizontal = 0 92 | grow_vertical = 2 93 | 94 | [connection signal="text_submitted" from="PanelContainer/MarginContainer/Controls/LineEdit" to="PanelContainer" method="_on_line_edit_text_submitted"] 95 | [connection signal="pressed" from="PanelContainer/MarginContainer/Controls/Button" to="PanelContainer" method="_on_button_pressed"] 96 | [connection signal="pressed" from="PanelContainer/MarginContainer/Controls/Remove" to="PanelContainer" method="_on_remove_pressed"] 97 | [connection signal="dragged" from="PanelContainer/HBoxContainer/BeginningPoint" to="PanelContainer" method="_on_beginning_point_dragged"] 98 | [connection signal="dragged" from="PanelContainer/HBoxContainer/EndPoint" to="PanelContainer" method="_on_end_point_dragged"] 99 | -------------------------------------------------------------------------------- /tools/schedule_editor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://slj5y768s6qx"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/tools/schedule_editor.gd" id="1_gl0yl"] 4 | [ext_resource type="Script" path="res://addons/skelerealms/tools/schedule_markers.gd" id="4_0h8dh"] 5 | 6 | [node name="ScheduleEditor" type="PanelContainer"] 7 | anchors_preset = 15 8 | anchor_right = 1.0 9 | anchor_bottom = 1.0 10 | grow_horizontal = 2 11 | grow_vertical = 2 12 | script = ExtResource("1_gl0yl") 13 | 14 | [node name="ScrollContainer" type="ScrollContainer" parent="."] 15 | layout_mode = 2 16 | mouse_filter = 0 17 | 18 | [node name="HBoxContainer" type="HBoxContainer" parent="ScrollContainer"] 19 | custom_minimum_size = Vector2(4096, 0) 20 | layout_mode = 2 21 | size_flags_horizontal = 3 22 | size_flags_vertical = 3 23 | script = ExtResource("4_0h8dh") 24 | 25 | [node name="Container" type="Control" parent="."] 26 | unique_name_in_owner = true 27 | layout_mode = 2 28 | mouse_filter = 2 29 | 30 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 31 | layout_mode = 2 32 | mouse_filter = 2 33 | 34 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] 35 | layout_mode = 2 36 | mouse_filter = 2 37 | 38 | [node name="Button" type="Button" parent="VBoxContainer/HBoxContainer"] 39 | layout_mode = 2 40 | text = "Add Event" 41 | 42 | [node name="OptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"] 43 | layout_mode = 2 44 | item_count = 1 45 | selected = 0 46 | popup/item_0/text = "SandboxSchedule" 47 | popup/item_0/id = 0 48 | -------------------------------------------------------------------------------- /tools/schedule_editor_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorInspectorPlugin 3 | 4 | 5 | const ScheduleEditor := preload("res://addons/skelerealms/tools/schedule_editor.tscn") 6 | 7 | signal request_open(events: Array[ScheduleEvent]) 8 | 9 | 10 | func _can_handle(object: Object) -> bool: 11 | return object is Schedule 12 | 13 | 14 | func _parse_begin(object: Object) -> void: 15 | var b := Button.new() 16 | b.text = "Open schedule editor" 17 | b.pressed.connect(func() -> void: request_open.emit((object as Schedule).events)) 18 | add_custom_control(b) 19 | -------------------------------------------------------------------------------- /tools/schedule_markers.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends HBoxContainer 3 | 4 | 5 | const HOUR_SEPARATION = 256 6 | const H_LINE_WIDTH = 6 7 | const HH_LINE_WIDTH = 2 8 | 9 | 10 | @onready var hpd:int = ProjectSettings.get_setting("skelerealms/hours_per_day") 11 | var default_font:Font 12 | var default_font_size:int 13 | 14 | 15 | func _ready() -> void: 16 | custom_minimum_size = Vector2((hpd + 1) * HOUR_SEPARATION, 0) 17 | default_font = ThemeDB.fallback_font 18 | default_font_size = ThemeDB.fallback_font_size 19 | 20 | 21 | func _draw() -> void: 22 | draw_hour_lines() 23 | draw_half_hour_lines() 24 | 25 | 26 | func draw_hour_lines() -> void: 27 | var arr:PackedVector2Array = PackedVector2Array() 28 | arr.resize((hpd + 1) * 2) 29 | for i in range(hpd + 1): 30 | var x:int = HOUR_SEPARATION * i 31 | arr[i * 2] = Vector2(x, 0) 32 | arr[i * 2 + 1] = Vector2(x, size.y) 33 | draw_string(default_font, Vector2(x + 5, size.y - default_font_size - 5), "%dh" % i) 34 | draw_multiline(arr, Color.DARK_SLATE_GRAY, H_LINE_WIDTH) 35 | 36 | 37 | func draw_half_hour_lines() -> void: 38 | var arr:PackedVector2Array = PackedVector2Array() 39 | arr.resize((hpd + 1) * 2) 40 | for i in range(hpd + 1): 41 | var x:int = (HOUR_SEPARATION * i) + (HOUR_SEPARATION / 2) 42 | arr[i * 2] = Vector2(x, 0) 43 | arr[i * 2 + 1] = Vector2(x, size.y) 44 | draw_multiline(arr, Color.hex(0x55_55_55), HH_LINE_WIDTH) 45 | -------------------------------------------------------------------------------- /tools/scheduledraghandle.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | 5 | var dragging:bool 6 | var editing:ScheduleEvent 7 | signal dragged(offset) 8 | 9 | 10 | func _gui_input(event: InputEvent) -> void: 11 | if not visible: 12 | return 13 | if event is InputEventMouseButton: 14 | if event.button_index == MOUSE_BUTTON_LEFT: 15 | if (event as InputEventMouseButton).pressed: 16 | if _contains(event.global_position): 17 | dragging = true 18 | else: 19 | dragging = false 20 | elif event is InputEventMouseMotion: 21 | if dragging: 22 | dragged.emit(event.relative) 23 | 24 | 25 | func _contains(pos: Vector2) -> bool: 26 | return get_global_rect().has_point(pos) 27 | -------------------------------------------------------------------------------- /tools/sk_game_root.gd: -------------------------------------------------------------------------------- 1 | extends Node3D 2 | 3 | 4 | static var spawn_position:Vector3 5 | static var spawn_world:String 6 | 7 | 8 | func _ready() -> void: 9 | var world:String = OS.get_environment("world") 10 | if world.is_empty(): 11 | world = SkeleRealmsGlobal.config.default_world 12 | 13 | var p := str_to_var(OS.get_environment("pos")) 14 | var pos:Vector3 = p if p else SkeleRealmsGlobal.config.default_world_position 15 | 16 | _move_player(world, pos) 17 | 18 | 19 | func _move_player(world:String, pos:Vector3) -> void: 20 | for c:Node in get_children(): 21 | if c is SKEntityManager: 22 | var tc:TeleportComponent = (c as SKEntityManager).get_entity(&"Player").get_component(&"TeleportComponent") 23 | tc.teleport(world, pos) 24 | GameInfo.start_game() 25 | -------------------------------------------------------------------------------- /tools/slot_enum_selector.gd: -------------------------------------------------------------------------------- 1 | extends EditorProperty 2 | 3 | 4 | var option_button: OptionButton = OptionButton.new() 5 | var updating:bool 6 | var current:Array[StringName] = [] 7 | var parent_vbox := VBoxContainer.new() 8 | var items_vbox := VBoxContainer.new() 9 | 10 | 11 | func _init() -> void: 12 | add_child(parent_vbox) 13 | parent_vbox.add_child(items_vbox) 14 | 15 | var hbox := HBoxContainer.new() 16 | var b := Button.new() 17 | b.text = "Add item" 18 | b.pressed.connect(func() -> void: 19 | _add_item(option_button.get_item_text(option_button.get_selected_id())) 20 | _sync() 21 | ) 22 | hbox.add_child(option_button) 23 | hbox.add_child(b) 24 | parent_vbox.add_child(hbox) 25 | 26 | add_focusable(option_button) 27 | 28 | 29 | func _ready() -> void: 30 | for i:StringName in SkeleRealmsGlobal.config.equipment_slots: 31 | option_button.add_item(i) 32 | var n_i:int = option_button.item_count - 1 33 | 34 | 35 | func _sync() -> void: 36 | var values:Array[StringName] = _get_values() 37 | if not values == get_edited_object()[get_edited_property()]: 38 | current = values 39 | emit_changed(get_edited_property(), values) 40 | return 41 | 42 | 43 | func _update_property() -> void: 44 | var new_value:Array[StringName] = get_edited_object()[get_edited_property()] 45 | 46 | if (new_value == current): 47 | return 48 | 49 | updating = true 50 | current = new_value 51 | 52 | for n:Node in items_vbox.get_children(): 53 | n.queue_free() 54 | for i:StringName in new_value: 55 | _add_item(i) 56 | updating = false 57 | 58 | 59 | func _add_item(kind:StringName = &"") -> void: 60 | if (not kind.is_empty()) and _get_values().has(kind): 61 | return 62 | var hbox := HBoxContainer.new() 63 | 64 | var o:OptionButton = option_button.duplicate() 65 | if not kind.is_empty(): 66 | for i:int in o.item_count: 67 | if o.get_item_text(i) == kind: 68 | o.select(i) 69 | break 70 | o.item_selected.connect(func(_i:int) -> void: _sync()) 71 | hbox.add_child(o) 72 | 73 | var b := Button.new() 74 | b.text = "Delete" 75 | b.pressed.connect(func() -> void: 76 | hbox.queue_free() 77 | _sync() 78 | ) 79 | hbox.add_child(b) 80 | 81 | items_vbox.add_child(hbox) 82 | 83 | 84 | func _get_values() -> Array[StringName]: 85 | var output:Array[StringName] = [] 86 | 87 | for n:Node in items_vbox.get_children(): 88 | var o:OptionButton = ((n as HBoxContainer).get_child(0) as OptionButton) 89 | output.append(o.get_item_text(o.get_selected_id())) 90 | 91 | return output 92 | -------------------------------------------------------------------------------- /tools/span.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | 4 | 5 | var start: float 6 | var end: float 7 | var size: 8 | get: 9 | return end - start 10 | var center:int: 11 | get: 12 | return roundi((start + end) / 2) 13 | 14 | 15 | func overlaps(other: Object) -> bool: 16 | return contains_point(other.start) or contains_point(other.end) or encloses(other) or other.encloses(self) 17 | 18 | 19 | func contains_point(pt:float) -> bool: 20 | return start <= pt and pt <= end 21 | 22 | 23 | func encloses(other: Object) -> bool: 24 | return start <= other.start and end >= other.end 25 | 26 | 27 | func sync(r:Rect2) -> void: 28 | start = r.position.x 29 | end = r.end.x 30 | 31 | 32 | func _init(r:Rect2): 33 | sync(r) 34 | -------------------------------------------------------------------------------- /tools/template_selector.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends PanelContainer 3 | 4 | 5 | const FILE_INHERIT = 1 6 | 7 | @onready var option_button: OptionButton = $VBoxContainer/HBoxContainer/OptionButton 8 | @onready var file_dialog: FileDialog = $FileDialog 9 | 10 | var editing:SKWorldEntity 11 | 12 | 13 | # life will be pain until this gets merged https://github.com/godotengine/godot/pull/90057 14 | 15 | 16 | # Called when the node enters the scene tree for the first time. 17 | func _ready() -> void: 18 | var templates:PackedStringArray = ProjectSettings.get_setting("skelerealms/entity_archetypes") 19 | option_button.clear() 20 | for t:String in templates: 21 | option_button.add_item(t) 22 | 23 | 24 | func edit(what:SKWorldEntity) -> void: 25 | editing = what 26 | 27 | 28 | func _on_button_pressed() -> void: 29 | file_dialog.popup_centered() 30 | #_create_using_editor() 31 | 32 | 33 | func _grab_uid(path:String) -> String: 34 | return ResourceUID.id_to_text(ResourceLoader.get_resource_uid(path)) 35 | 36 | 37 | func _generate_inherited_scene_id() -> String: 38 | return "1_%s" % SKIDGenerator.generate_id(5).to_lower() 39 | 40 | 41 | func _format_scene(from:String, entity_name:String) -> String: 42 | var id:String = _generate_inherited_scene_id() 43 | var uid:String = ResourceUID.id_to_text(ResourceUID.create_id()) 44 | return """ 45 | [gd_scene load_steps=2 format=3 uid=\"%s\"] 46 | 47 | [ext_resource type=\"PackedScene\" uid=\"%s\" path=\"%s\" id=\"%s\"] 48 | 49 | [node name=\"%s\" instance=ExtResource(\"%s\")] 50 | """ % [ 51 | uid, 52 | _grab_uid(from), 53 | from, 54 | id, 55 | entity_name, 56 | id 57 | ] 58 | 59 | 60 | func _on_file_dialog_file_selected(path: String) -> void: 61 | _make_manually(path) 62 | #_create_using_instantiation(path) 63 | 64 | 65 | func _make_manually(path:String) -> void: 66 | var p:String = option_button.get_item_text(option_button.selected) 67 | var contents:String = _format_scene(p, "test_entity") 68 | var fh := FileAccess.open(path, FileAccess.WRITE) 69 | fh.store_string(contents) 70 | fh.close() 71 | EditorInterface.get_resource_filesystem().scan() 72 | EditorInterface.get_resource_filesystem().update_file(path) 73 | EditorInterface.get_resource_previewer().queue_resource_preview(path, self, &"receive_thumbnail", null) 74 | editing.entity = ResourceLoader.load(path) 75 | 76 | 77 | func _create_using_editor() -> void: 78 | EditorInterface.get_file_system_dock().navigate_to_path(option_button.get_item_text(option_button.selected)) 79 | 80 | var popup:PopupMenu = EditorInterface.get_file_system_dock().get_children()\ 81 | .filter(func(n:Node)->bool:return n is PopupMenu)\ 82 | .filter(func(p:PopupMenu) -> bool: return p.item_count > 0)\ 83 | .filter(func(p:PopupMenu) -> bool: return p.get_item_text(0) == "Open Scene")\ 84 | [0] 85 | 86 | if popup: 87 | popup.id_pressed.emit(FILE_INHERIT) 88 | 89 | 90 | func _create_using_instantiation(path:String) -> void: 91 | var p:String = option_button.get_item_text(option_button.selected) 92 | var new_scene:Node = (ResourceLoader.load(p) as PackedScene).instantiate(PackedScene.GEN_EDIT_STATE_MAIN_INHERITED) 93 | new_scene.scene_file_path = p 94 | print(new_scene.scene_file_path) 95 | var new_ps := PackedScene.new() 96 | new_ps.pack(new_scene) 97 | ResourceSaver.save(new_ps, path) 98 | 99 | 100 | func receive_thumbnail(_path:String, _preview:Texture2D, _thumbnail_preview:Texture2D, _userdata:Variant) -> void: 101 | return 102 | -------------------------------------------------------------------------------- /tools/template_selector.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://b0c0y6wqyykt4"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/tools/template_selector.gd" id="1_1j7a1"] 4 | 5 | [node name="PanelContainer" type="PanelContainer"] 6 | offset_right = 40.0 7 | offset_bottom = 40.0 8 | script = ExtResource("1_1j7a1") 9 | 10 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 11 | layout_mode = 2 12 | 13 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] 14 | layout_mode = 2 15 | 16 | [node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"] 17 | layout_mode = 2 18 | size_flags_horizontal = 3 19 | text = "Template:" 20 | 21 | [node name="OptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"] 22 | layout_mode = 2 23 | selected = 0 24 | item_count = 3 25 | popup/item_0/text = "res://assets/iod_npc_generic_template.tscn" 26 | popup/item_1/text = "res://addons/skelerealms/npc_entity_template.tscn" 27 | popup/item_1/id = 1 28 | popup/item_2/text = "res://addons/skelerealms/item_entity_template.tscn" 29 | popup/item_2/id = 2 30 | 31 | [node name="Button" type="Button" parent="VBoxContainer"] 32 | layout_mode = 2 33 | text = "Create" 34 | 35 | [node name="FileDialog" type="FileDialog" parent="."] 36 | filters = PackedStringArray("*.scn, *.tscn") 37 | 38 | [connection signal="pressed" from="VBoxContainer/Button" to="." method="_on_button_pressed"] 39 | [connection signal="file_selected" from="FileDialog" to="." method="_on_file_dialog_file_selected"] 40 | -------------------------------------------------------------------------------- /tools/world_entity_plugin.gd: -------------------------------------------------------------------------------- 1 | extends EditorInspectorPlugin 2 | 3 | 4 | const TemplateSelector = preload("res://addons/skelerealms/tools/template_selector.tscn") 5 | 6 | 7 | func _can_handle(object: Object) -> bool: 8 | return object is SKWorldEntity 9 | 10 | 11 | func _parse_begin(object: Object) -> void: 12 | var b := Button.new() 13 | b.text = "Sync position with entity" 14 | b.pressed.connect(object._sync.bind()) 15 | 16 | var t := TemplateSelector.instantiate() 17 | t.edit(object) 18 | 19 | var vbox := VBoxContainer.new() 20 | vbox.add_child(b) 21 | vbox.add_child(t) 22 | 23 | add_custom_control(vbox) 24 | -------------------------------------------------------------------------------- /world_marker_entity_template.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bymqtmaatptgp"] 2 | 3 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/entities/entity.gd" id="1_rnn1t"] 4 | [ext_resource type="Script" path="res://addons/skelerealms/scripts/components/marker_component.gd" id="2_vcn76"] 5 | 6 | [node name="SKEntity" type="Node"] 7 | script = ExtResource("1_rnn1t") 8 | 9 | [node name="MarkerComponent" type="Node" parent="."] 10 | script = ExtResource("2_vcn76") 11 | -------------------------------------------------------------------------------- /world_template.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=3 uid="uid://b04h6k15qdccd"] 2 | 3 | [node name="world_template" type="Node3D"] 4 | 5 | [node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] 6 | 7 | [node name="VoxelGI" type="VoxelGI" parent="."] 8 | --------------------------------------------------------------------------------